diff --git a/packages/coding-agent/src/prompts/goals/goal-todo-context.md b/packages/coding-agent/src/prompts/goals/goal-todo-context.md index 72dbe4935..7d794393b 100644 --- a/packages/coding-agent/src/prompts/goals/goal-todo-context.md +++ b/packages/coding-agent/src/prompts/goals/goal-todo-context.md @@ -1,6 +1,6 @@ Current persisted todo state for this goal follows. Goal continuations do not get a visible user nudge, so treat this as live progress state, not old transcript decoration. -Before continuing substantial work, compare your next action with these todos. If an item is stale, already finished, or no longer the active pointer, call the `todo` tool first to mark it done or rewrite the list. Do not leave a stale in_progress item while working on later phases. +{{#if canCallTodoTool}}Before continuing substantial work, compare your next action with these todos. If an item is stale, already finished, or no longer the active pointer, call the `todo` tool first to mark it done or rewrite the list. Do not leave a stale in_progress item while working on later phases.{{else}}Before continuing substantial work, compare your next action with these todos as read-only progress state. The `todo` tool is discoverable but not active in this turn; if the list needs edits, activate `todo` first instead of ignoring the persisted state.{{/if}} Overall: {{closed}}/{{total}} done, {{open}} open. {{#each phases}} diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 2a1205022..f8d80e6e3 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -6512,7 +6512,11 @@ export class AgentSession { #buildGoalTodoContext(): string | undefined { if (!this.settings.get("todo.enabled")) return undefined; - if (!this.getActiveToolNames().includes("todo")) return undefined; + const activeToolNames = this.getActiveToolNames(); + const canCallTodoTool = activeToolNames.includes("todo"); + const canDiscoverTodoTool = + !canCallTodoTool && this.getDiscoverableTools({ source: "builtin" }).some(tool => tool.name === "todo"); + if (!canCallTodoTool && !canDiscoverTodoTool) return undefined; const phases = this.getTodoPhases().filter(phase => phase.tasks.length > 0); if (phases.length === 0) return undefined; @@ -6533,6 +6537,7 @@ export class AgentSession { })); return prompt.render(goalTodoContextPrompt, { + canCallTodoTool, closed: String(closed), open: String(open), phases: promptPhases, diff --git a/packages/coding-agent/test/goals/goal-mode-integration.test.ts b/packages/coding-agent/test/goals/goal-mode-integration.test.ts index 1181a7918..d56b1f936 100644 --- a/packages/coding-agent/test/goals/goal-mode-integration.test.ts +++ b/packages/coding-agent/test/goals/goal-mode-integration.test.ts @@ -302,6 +302,31 @@ describe("InteractiveMode goal mode integration", () => { expect(content.match(/<\/todo_context>/g)).toHaveLength(1); }); + it("includes read-only todo state when todo is discoverable but inactive", async () => { + harness.settings.set("tools.discoveryMode", "all"); + await harness.mode.handleGoalModeCommand("Ship the release"); + harness.session.setTodoPhases([ + { + name: "Verification", + tasks: [{ content: "Run focused checks", status: "pending" }], + }, + ]); + expect(harness.session.getActiveToolNames()).not.toContain("todo"); + expect(harness.session.getDiscoverableTools({ source: "builtin" }).some(tool => tool.name === "todo")).toBe(true); + const sendCustomMessage = vi.spyOn(harness.session, "sendCustomMessage").mockResolvedValue(false); + + await harness.session.sendGoalModeContext({ deliverAs: "steer" }); + + const message = sendCustomMessage.mock.calls[0]?.[0]; + const content = typeof message?.content === "string" ? message.content : ""; + expect(message?.customType).toBe("goal-mode-context"); + expect(content).toContain(""); + expect(content).toContain("Run focused checks"); + expect(content).toContain("read-only progress state"); + expect(content).toContain("discoverable but not active"); + expect(content).not.toContain("call the `todo` tool first"); + }); + it("omits persisted todo state when todo tool is inactive", async () => { await harness.mode.handleGoalModeCommand("Ship the release"); harness.session.setTodoPhases([