fix: preserve discoverable goal todos

This commit is contained in:
Jeff Scott Ward
2026-06-28 21:40:31 -04:00
parent de3a3d5846
commit f1dc0773a5
3 changed files with 32 additions and 2 deletions
@@ -1,6 +1,6 @@
<todo_context>
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}}
@@ -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,
@@ -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("<todo_context>");
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([