diff --git a/.agents/notes/implemented/feature/2026-07-19-human-goal-command.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-human-goal-command.i18n.yaml index 78bb900b27..3505cfabca 100644 --- a/.agents/notes/implemented/feature/2026-07-19-human-goal-command.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-human-goal-command.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-human-goal-command.md: f458feae3bed8ef7b0ace6b8baaf5ae8b43e4cc8 -2026-07-19-human-goal-command.zh.md: 2fd79c2f50490e077cc69e93705a7b7bb242a082 +2026-07-19-human-goal-command.md: e2a59c3bddd7e135b0878cb59c964c3e7be64002 +2026-07-19-human-goal-command.zh.md: 0d6a0e22de091cd7a4a00ef59f1ac1b936c930f9 diff --git a/.agents/notes/implemented/feature/2026-07-19-human-goal-command.md b/.agents/notes/implemented/feature/2026-07-19-human-goal-command.md index f458feae3b..e2a59c3bdd 100644 --- a/.agents/notes/implemented/feature/2026-07-19-human-goal-command.md +++ b/.agents/notes/implemented/feature/2026-07-19-human-goal-command.md @@ -12,7 +12,7 @@ The command must also respect the goal design's two kinds of state. Durable phas ## Decision -`@deepseek-ai/dsh-command-goal` in `packages/goal/command-goal/` is a command producer over `ctx.commands` and `ctx.goals`. It registers one global `goal` definition for the TUI and ACP surfaces. The handler receives the exact target agent from command dispatch, reads or mutates that agent's goal through the domain service, and returns direct plain-text UI output. It does not import either adapter or the concrete agent loop. +`@deepseek-ai/dsh-command-goal` in `packages/goal/command-goal/` is a command producer over `ctx.commands` and `ctx.goals`. It registers one global `goal` definition, so every command adapter in the composition discovers the same command; an incompatible app omits this producer rather than masking its registration at an adapter. The handler receives the exact target agent from command dispatch, reads or mutates that agent's goal through the domain service, and returns direct plain-text UI output. It does not import either adapter or the concrete agent loop. The command follows the compact Codex shape in the [public OpenAI Codex TUI dispatcher at commit `678157a`](https://github.com/openai/codex/blob/678157acaa819d5510adfe359abb5d0392cfe461/codex-rs/tui/src/chatwidget/slash_dispatch.rs#L750-L805): bare status, a free-form objective, and `clear`, `edit`, `pause`, or `resume` controls. The commit permalink makes the researched grammar durable even as Codex evolves. This repository keeps its own event-sourced state, round-count policy, and post-resume activation rule rather than copying Codex's SQLite, token budget, or automatic-resume behavior. @@ -30,7 +30,7 @@ Control words are ASCII-case-insensitive after outer whitespace trimming. They a ### Output and failure boundary -Status output omits branded ids and compare-and-set revisions because those are model/plugin coordination details rather than human controls. It includes activation because that fact changes whether work will continue. Command hints are derived from the exact state: an armed active goal offers pause, a disarmed active or resumable stopped goal offers resume, a budget-limited goal explains that the agent must raise `maxGoalRounds` before resume, and a completed goal offers replacement or clear. +Status output omits branded ids and compare-and-set revisions because those are model/plugin coordination details rather than human controls. It includes activation because that fact changes whether work will continue, and a blocked goal includes its durable policy code and human-readable explanation. Command hints are derived from the exact state: an armed active goal offers pause, a disarmed active or paused/blocked goal offers resume, and a completed goal offers replacement or clear. Expected `GoalError` failures become one stable, branded-id-free `CommandResult.error`, so domain diagnostics do not leak compare-and-set internals into the human surface and invalid operations never enter model history. The current status supplies the actionable state-specific recovery. Other exceptions remain adapter-visible command failures; treating programmer faults as ordinary domain errors would hide defects. The command handler performs only synchronous domain mutations, so request cancellation is decided by the command registry before the mutation begins and there is no escaped asynchronous side effect to unwind. @@ -40,11 +40,11 @@ Generic slash input, status text, and errors are not persisted. Successful goal `agent-spine-demo` accepts an optional `goals` composition object containing the goal-domain and model-tool owner configs. Omission or `false` leaves the stack unmounted. This explicit opt-in is important for headless one-shot callers: their result API settles one correlated physical turn and must not silently become a long-running logical goal operation. -The interactive app bundles make the opposite product choice. ACP defaults `goals` to the owner defaults and mounts the goal domain, model tools, same-session driver, command registry, and this producer. The terminal app enables the same goal stack by default but mounts the producer only for TUI mode; line-oriented stdio does not consume the command plane, so a typed `/goal` there remains an ordinary human prompt that the model may interpret through its separately authorized goal tools. Both apps accept `goals: false` as one coherent stack opt-out. The Python SDK runtime closure ships this producer alongside ACP, commands, and the goal stack so an external `cordis.yml` can compose the same command surface. +The interactive app bundles make the opposite product choice. ACP defaults `goals` to the owner defaults and mounts the goal domain, model tools, same-session driver, command registry, and this producer. The terminal app enables the same goal stack by default but mounts the producer only for TUI mode; line-oriented stdio does not consume the command plane, so a typed `/goal` there remains an ordinary human prompt that the model may interpret through its separately authorized goal tools. Both apps accept `goals: false` as one coherent stack opt-out. The Python SDK runtime closure ships this producer alongside ACP, commands, and the goal stack so an external `cordis.yml` can compose the same command. ## Testing -The producer suite uses the real command registry, goal service, agent registry, and session log. It covers Loader-safe exports, discovery on both surfaces, disposal, empty status, objective parsing, unfinished replacement refusal, inline edit, completed replacement, all missing-state controls, pause/resume/clear, every durable phase, armed/disarmed presentation, budget-exhaustion recovery guidance, sanitized domain errors, unexpected failures, and persisted mutation records. App composition tests cover explicit spine opt-in, TUI/ACP defaults, readline producer absence, coherent opt-out, forwarded domain/tool config, command discovery, the packaged-runtime closure, and the expanded model-tool assembly. The keyless ACP snapshots pin the resulting `/goal` discovery metadata and goal tool schemas in the shipped app composition. +The producer suite uses the real command registry, goal service, agent registry, and session log. It covers Loader-safe exports, registry discovery, disposal, empty status, objective parsing, unfinished replacement refusal, inline edit, completed replacement, all missing-state controls, pause/resume/clear, every durable phase, blocked code/explanation presentation, armed/disarmed presentation, sanitized domain errors, unexpected failures, and persisted mutation records. App composition tests cover explicit spine opt-in, TUI/ACP defaults, readline producer absence, coherent opt-out, forwarded domain/tool config, command discovery, the packaged-runtime closure, and the expanded model-tool assembly. A keyless snapshot boots the shipped ACP application, observes its advertised `/goal` metadata, invokes `/goal` directly, and pins the no-model-turn result; the surrounding ACP snapshots also pin the goal tool schemas in that composition. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-19-human-goal-command.zh.md b/.agents/notes/implemented/feature/2026-07-19-human-goal-command.zh.md index 2fd79c2f50..0d6a0e22de 100644 --- a/.agents/notes/implemented/feature/2026-07-19-human-goal-command.zh.md +++ b/.agents/notes/implemented/feature/2026-07-19-human-goal-command.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -位于 `packages/goal/command-goal/` 的 `@deepseek-ai/dsh-command-goal` 是构建在 `ctx.commands` 与 `ctx.goals` 之上的命令生产方。它为 TUI 和 ACP 表面注册一个全局 `goal` 定义。处理器从命令分发接收准确的目标 agent(智能体),通过领域服务读取或改变该 agent 的目标,并返回直接的纯文本 UI 输出。它不导入任何适配器或具体 agent loop(智能体循环)。 +位于 `packages/goal/command-goal/` 的 `@deepseek-ai/dsh-command-goal` 是构建在 `ctx.commands` 与 `ctx.goals` 之上的命令生产方。它注册一个全局 `goal` 定义,因此组合中的每个命令适配器都会发现同一个命令;不兼容的应用应省略该生产方,而不是在适配器处屏蔽其注册。处理器从命令分发接收准确的目标 agent(智能体),通过领域服务读取或改变该 agent 的目标,并返回直接的纯文本 UI 输出。它不导入任何适配器或具体 agent loop(智能体循环)。 该命令遵循 [OpenAI Codex 公共仓库 `678157a` 提交中的 TUI 分发实现](https://github.com/openai/codex/blob/678157acaa819d5510adfe359abb5d0392cfe461/codex-rs/tui/src/chatwidget/slash_dispatch.rs#L750-L805)所呈现的紧凑形态:无参数状态查询、自由形式目标描述,以及 `clear`、`edit`、`pause` 或 `resume` 控制。固定到提交的链接使调研所得语法在 Codex 后续演进时仍可核验。本仓库保留自身的事件溯源状态、回合计数策略与恢复后激活规则,而不复制 Codex 的 SQLite、token 预算或自动恢复行为。 @@ -30,7 +30,7 @@ Status: implemented ### 输出与失败边界 -状态输出省略品牌化 id 与比较并交换修订号,因为它们属于模型/插件协调细节,而不是人类控制项。输出包含激活态,因为该事实会改变工作是否继续。命令提示从准确状态派生:已激活的活跃目标提供暂停,未激活的活跃目标或可恢复的停止目标提供恢复,受预算限制的目标说明 agent 必须先提高 `maxGoalRounds` 才能恢复,已完成目标则提供替换或清除。 +状态输出省略品牌化 id 与比较并交换修订号,因为它们属于模型/插件协调细节,而不是人类控制项。输出包含激活态,因为该事实会改变工作是否继续;被阻塞的目标还会包含其持久策略代码和面向人类的说明。命令提示从准确状态派生:已激活的活跃目标提供暂停,未激活的活跃目标或已暂停/被阻塞目标提供恢复,已完成目标则提供替换或清除。 预期的 `GoalError` 失败会变为一个稳定且不含品牌化 id 的 `CommandResult.error`,使领域诊断不会向人类表面泄露比较并交换内部细节,非法操作也绝不会进入模型历史。当前状态负责提供针对具体状态且可执行的恢复路径。其他异常仍是适配器可见的命令失败;若把程序缺陷当成普通领域错误,就会隐藏问题。命令处理器只执行同步领域变更,因此请求取消会在变更开始前由命令注册表决定,不存在需要回滚的外逸异步副作用。 @@ -40,11 +40,11 @@ Status: implemented `agent-spine-demo` 接受可选的 `goals` 组合对象,其中包含目标领域与模型工具的所有者配置。省略或设为 `false` 时不会挂载该栈。对无头单次调用方而言,明确选择加入非常重要:它们的结果 API 会在一个相关物理轮次后结束,不能静默变成长时间运行的逻辑目标操作。 -交互式应用包作出相反的产品选择。ACP 默认让 `goals` 使用所有者默认值,并挂载目标领域、模型工具、同会话驱动器、命令注册表与本生产方。终端应用默认启用相同目标栈,但只在 TUI 模式挂载本生产方;行式 stdio 不消费命令平面,因此在那里输入的 `/goal` 仍是普通人类提示词,模型可以通过独立授权的目标工具解释它。两个应用都接受 `goals: false` 作为一致的整体退出选项。Python SDK 运行时闭包把本生产方与 ACP、命令及目标栈一并交付,使外部 `cordis.yml` 能组合相同命令表面。 +交互式应用包作出相反的产品选择。ACP 默认让 `goals` 使用所有者默认值,并挂载目标领域、模型工具、同会话驱动器、命令注册表与本生产方。终端应用默认启用相同目标栈,但只在 TUI 模式挂载本生产方;行式 stdio 不消费命令平面,因此在那里输入的 `/goal` 仍是普通人类提示词,模型可以通过独立授权的目标工具解释它。两个应用都接受 `goals: false` 作为一致的整体退出选项。Python SDK 运行时闭包把本生产方与 ACP、命令及目标栈一并交付,使外部 `cordis.yml` 能组合相同命令。 ## 测试 -生产方测试套件使用真实命令注册表、目标服务、agent 注册表与会话日志。它覆盖 Loader 安全导出、两个表面的发现、资源释放、空状态、目标描述解析、拒绝未完成目标替换、行内编辑、已完成目标替换、所有缺失状态控制、暂停/恢复/清除、每个持久阶段、已激活/未激活展示、预算耗尽恢复提示、经净化的领域错误、意外失败与持久变更记录。应用组合测试覆盖显式主干选择加入、TUI/ACP 默认值、readline 不挂载生产方、一致退出、转发的领域/工具配置、命令发现、打包运行时闭包与扩展后的模型工具组装。无密钥 ACP 快照固定了交付应用组合中的 `/goal` 发现元数据和目标工具 schema。 +生产方测试套件使用真实命令注册表、目标服务、agent 注册表与会话日志。它覆盖 Loader 安全导出、注册表发现、资源释放、空状态、目标描述解析、拒绝未完成目标替换、行内编辑、已完成目标替换、所有缺失状态控制、暂停/恢复/清除、每个持久阶段、阻塞代码/说明展示、已激活/未激活展示、经净化的领域错误、意外失败与持久变更记录。应用组合测试覆盖显式主干选择加入、TUI/ACP 默认值、readline 不挂载生产方、一致退出、转发的领域/工具配置、命令发现、打包运行时闭包与扩展后的模型工具组装。一个无密钥快照会启动交付的 ACP 应用,观察其公布的 `/goal` 元数据,直接调用 `/goal`,并固定不经过模型轮次的结果;周边 ACP 快照还会固定该组合中的目标工具 schema。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml index fd97351a0e..e53c591aa5 100644 --- a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-model-facing-goal-tools.md: 2ef77b53cd8b95c9cdffd12e20c723fb1cff3a5d -2026-07-19-model-facing-goal-tools.zh.md: 08619600355c71ccd30d608e3c4e5a7fba753d4e +2026-07-19-model-facing-goal-tools.md: 7cc3907d708115207e166455ea988120a03d768b +2026-07-19-model-facing-goal-tools.zh.md: 1a381160354d6a2a24f957f41bc9e375c1ab01ca diff --git a/packages/examples/acp-demo/tests/acp-agent.spec.ts b/packages/examples/acp-demo/tests/acp-agent.spec.ts index a9a1d6178d..b6886a7e12 100644 --- a/packages/examples/acp-demo/tests/acp-agent.spec.ts +++ b/packages/examples/acp-demo/tests/acp-agent.spec.ts @@ -96,7 +96,7 @@ describe('dsh-acp-demo composition', () => { sessionId: 'disabled-goals' as import('@deepseek-ai/dsh-session').SessionId, agentOptions: { provider: 'mock', model: 'mock' }, }) - expect(ctx.commands.find(handle.agent, 'acp', 'goal')).toBeUndefined() + expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined() await handle.dispose() await ctx.fiber.dispose() }) diff --git a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts index acb93f758b..6c8837652d 100644 --- a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts +++ b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts @@ -121,14 +121,16 @@ describe('dsh-agent-spine-demo bundle', () => { it('opts into the configured persisted-goal domain, tools, and same-session driver', async () => { const ctx = await mount({ workspaceContext: false, + agents: [{ id: SessionId('configured-goal'), provider: 'mock', model: 'mock' }], goals: { domain: { defaultMaxGoalRounds: 17 }, tool: { blockedAfterConsecutiveRounds: 5 }, }, }) - expect(ctx.goals.resolveCreate({ objective: 'configured' })).toEqual({ - objective: 'configured', - maxGoalRounds: 17, + const agent = ctx.agents.list()[0] + if (agent === undefined) throw new Error('configured goal test has no live agent') + expect(ctx.goals.create(agent, { objective: 'configured' })).toMatchObject({ + objective: 'configured', maxGoalRounds: 17, }) expect(['create_goal', 'get_goal', 'update_goal'].map(name => ctx.tools.get(name)?.name)) .toEqual(['create_goal', 'get_goal', 'update_goal']) @@ -199,11 +201,16 @@ describe('dsh-agent-spine-demo bundle', () => { it('uses owner defaults for a schema-bypassing empty goal opt-in', async () => { const ctx = new Context() - agentCore.apply(ctx, { workspaceContext: false, goals: {} }) + agentCore.apply(ctx, { + workspaceContext: false, + agents: [{ id: SessionId('defaulted-goal'), provider: 'mock', model: 'mock' }], + goals: {}, + }) await new Promise(resolve => setTimeout(resolve, 50)) - expect(ctx.goals.resolveCreate({ objective: 'defaulted' })).toEqual({ - objective: 'defaulted', - maxGoalRounds: 256, + const agent = ctx.agents.list()[0] + if (agent === undefined) throw new Error('default goal test has no live agent') + expect(ctx.goals.create(agent, { objective: 'defaulted' })).toMatchObject({ + objective: 'defaulted', maxGoalRounds: 256, }) expect(ctx.tools.get('get_goal')).toBeDefined() await ctx.fiber.dispose() diff --git a/packages/examples/stdio-demo/tests/stdio-agent.spec.ts b/packages/examples/stdio-demo/tests/stdio-agent.spec.ts index 2480cb704d..de8481da4f 100644 --- a/packages/examples/stdio-demo/tests/stdio-agent.spec.ts +++ b/packages/examples/stdio-demo/tests/stdio-agent.spec.ts @@ -153,7 +153,7 @@ describe('dsh-stdio-demo app', () => { expect(agent?.id).toBe(agent?.session.id) expect(agent?.id).toMatch(/^main-session-/) expect(agent?.session.header.cwd).toBe(process.cwd()) - expect(ctx.commands.find(agent!, 'tui', 'goal')).toBeUndefined() + expect(ctx.commands.find(agent!, 'goal')).toBeUndefined() await ctx.fiber.dispose() }) diff --git a/packages/goal/command-goal/README.md b/packages/goal/command-goal/README.md index 2fa7323789..f766cd3ece 100644 --- a/packages/goal/command-goal/README.md +++ b/packages/goal/command-goal/README.md @@ -1,12 +1,12 @@ # @deepseek-ai/dsh-command-goal -Human-facing `/goal` control over [`ctx.goals`](../goal/README.md). The plugin registers one global command through [`ctx.commands`](../../ui/commands/README.md); TUI and ACP discover and execute it without a model turn. The [human goal-command Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md) owns the UX and composition decisions. +Human-facing `/goal` control over [`ctx.goals`](../goal/README.md). The plugin registers one global command through [`ctx.commands`](../../ui/commands/README.md), so every composed command adapter discovers it; the shipped TUI and ACP execute it without a model turn. The [human goal-command Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md) owns the UX and composition decisions. ## Command contract | Input | Result | |---|---| -| `/goal` | Show the current objective, durable phase, round count/cap, process-local activation, and valid next commands; show usage when no goal exists. | +| `/goal` | Show the current objective, durable phase, round count/cap, process-local activation, and valid next commands; a blocked goal also shows its policy code and explanation, while no goal shows usage. | | `/goal ` | Create and arm a goal, or replace a completed goal with a fresh identity. An unfinished goal is never replaced without an explicit clear. | | `/goal edit ` | Edit the current objective without changing its phase or activation. Editing a completed goal creates a fresh active goal. | | `/goal pause` | Pause an active goal and disarm continuation. | diff --git a/packages/goal/command-goal/src/index.ts b/packages/goal/command-goal/src/index.ts index a8e73cba7e..93ed7923b8 100644 --- a/packages/goal/command-goal/src/index.ts +++ b/packages/goal/command-goal/src/index.ts @@ -48,8 +48,6 @@ function phaseLabel(phase: GoalPhase): string { case 'active': return 'active' case 'paused': return 'paused' case 'blocked': return 'blocked' - case 'usage-limited': return 'usage limited' - case 'budget-limited': return 'limited by round budget' case 'complete': return 'complete' /* v8 ignore next 2 -- GoalPhase is closed and every member is handled above */ default: return assertNever(phase, 'goal phase') @@ -66,10 +64,7 @@ function commandHint(goal: GoalView): string { switch (goal.phase) { case 'paused': case 'blocked': - case 'usage-limited': return '/goal edit , /goal resume, /goal clear' - case 'budget-limited': - return '/goal edit , /goal clear; after the agent raises the round cap, /goal resume' case 'complete': return '/goal , /goal clear' /* v8 ignore next 2 -- the active branch and every non-active phase are handled above */ @@ -79,11 +74,16 @@ function commandHint(goal: GoalView): string { /** Render direct UI output without exposing compare-and-set internals. */ function renderGoal(title: string, goal: GoalView): CommandResult { + const reason = goal.phase === 'blocked' ? goal.blockedReason : undefined + /* v8 ignore next -- durable replay guarantees every blocked goal carries its validated reason */ + if (goal.phase === 'blocked' && reason === undefined) throw new TypeError('blocked goal is missing its reason') + const blocker = reason === undefined ? [] : [`Blocker: ${reason.code}: ${reason.message}`] return { kind: 'success', text: [ title, `Status: ${phaseLabel(goal.phase)}`, + ...blocker, `Objective: ${goal.objective}`, `Rounds: ${goal.roundsStarted}/${goal.maxGoalRounds}`, `Activation: ${goal.activation}`, @@ -159,7 +159,7 @@ function executeGoalCommand(ctx: Context, invocation: CommandInvocation): Comman } } -/** Register the Codex-shaped `/goal` human command on TUI and ACP surfaces. */ +/** Register the Codex-shaped `/goal` command for every composed command adapter. */ export function apply(ctx: Context): void { ctx.commands.register({ name: 'goal', diff --git a/packages/goal/command-goal/tests/command-goal.spec.ts b/packages/goal/command-goal/tests/command-goal.spec.ts index a3d48857c4..71b1c59052 100644 --- a/packages/goal/command-goal/tests/command-goal.spec.ts +++ b/packages/goal/command-goal/tests/command-goal.spec.ts @@ -74,7 +74,6 @@ async function harness(): Promise { async function run(test: Harness, suffix = ''): Promise>>> { const result = await test.ctx.commands.execute( test.agent, - 'tui', `/goal${suffix}`, new AbortController().signal, ) @@ -87,20 +86,8 @@ function ref(goal: NonNullable>): GoalRef { return { id: goal.id, revision: goal.revision } } -/** Append one admitted goal round for budget-limited presentation coverage. */ -function appendRound(test: Harness, goal: NonNullable>): void { - const source = { kind: 'goal', goalId: goal.id, revision: goal.revision, round: 1 } as const - const turn = nextTurn(test.session) - test.session.append('turn/start', { turn, trigger: { kind: 'message', source } }) - test.session.append('user/message', { - content: [{ type: 'text', text: 'goal round' }], - source, - }, { surfaceOp: 'append' }) - test.session.append('turn/end', { turn, reason: { kind: 'completed' } }) -} - describe('@deepseek-ai/dsh-command-goal registration', () => { - it('registers one global TUI/ACP command with Loader-safe exports and disposes it', async () => { + it('registers one global command with Loader-safe exports and disposes it', async () => { const test = await harness() expect(commandGoal.name).toBe('command-goal') expect(commandGoal.inject).toEqual(['commands', 'goals']) @@ -108,16 +95,15 @@ describe('@deepseek-ai/dsh-command-goal registration', () => { const loader = Object.create(Loader.prototype) as Loader expect(loader.unwrapExports(commandGoal)).toBe(commandGoal) - expect(test.ctx.commands.list(test.agent, 'tui')).toContainEqual({ + expect(test.ctx.commands.list(test.agent)).toContainEqual({ name: 'goal', description: 'set or view the goal for a long-running task', input: { hint: '[|clear|edit |pause|resume]' }, - surfaces: ['tui', 'acp'], }) - expect(test.ctx.commands.find(test.agent, 'acp', 'goal')).toBeDefined() + expect(test.ctx.commands.find(test.agent, 'goal')).toBeDefined() await test.plugin.dispose() - expect(test.ctx.commands.find(test.agent, 'tui', 'goal')).toBeUndefined() + expect(test.ctx.commands.find(test.agent, 'goal')).toBeUndefined() }) }) @@ -227,22 +213,16 @@ describe('/goal human command', () => { expect((await run(test)).text).toContain('Status: paused') goal = test.ctx.goals.resume(test.agent, ref(goal)) - goal = test.ctx.goals.block(test.agent, ref(goal)) - expect((await run(test)).text).toContain('Status: blocked') + goal = test.ctx.goals.block(test.agent, ref(goal), { + code: 'upstream-unavailable', + message: 'Provider unavailable', + }) + const blocked = await run(test) + expect(blocked.text).toContain('Status: blocked') + expect(blocked.text).toContain('Blocker: upstream-unavailable: Provider unavailable') goal = test.ctx.goals.resume(test.agent, ref(goal)) - goal = test.ctx.goals.markUsageLimited(test.agent, ref(goal)) - expect((await run(test)).text).toContain('Status: usage limited') - - goal = test.ctx.goals.resume(test.agent, ref(goal)) - appendRound(test, goal) - goal = test.ctx.goals.get(test.agent)! - goal = test.ctx.goals.markBudgetLimited(test.agent, ref(goal)) - const limited = await run(test) - expect(limited.text).toContain('Status: limited by round budget') - expect(limited.text).toContain('after the agent raises the round cap, /goal resume') - - goal = test.ctx.goals.complete(test.agent, ref(goal)) + test.ctx.goals.complete(test.agent, ref(goal)) const complete = await run(test) expect(complete.text).toContain('Status: complete') expect(complete.text).toContain('Commands: /goal , /goal clear')