fix(prompt): wrap todo block in <system-reminder> so user-message inject doesn't read as fresh user instruction - #14
Merged
Conversation
…as state The variable suffix (TodoList + deferred-tools) was relocated from system prompt into the last user message in b9e53d1 (2026-05-26) to keep the OpenAI Codex auto prefix-cache stable across turns. The deferred-tools block kept its `<system-reminder>` wrapper through that move; the TodoList block did not, so the model read a user-role text block ending with the imperative "Use TodoWrite to keep this list current ..." as a fresh user instruction and silently ended its turn mid-task on 2026-05-26 dogfood (DM-side worker did not finish). Two coordinated changes in `formatTodoSection`: 1. Wrap the whole todo block (empty and non-empty branches) in `<system-reminder>` so it mirrors the deferred-tools reminder framing and the model recognizes it as framework state. 2. Reword the trailing cue from imperative ("Use TodoWrite to keep this list current.") to state + continuation ("This is the framework's snapshot of your todo state, not a fresh user instruction. Keep advancing the in_progress item; update via TodoWrite as items change status. At most one item in_progress."). The "not a fresh user instruction" clause directly blocks the misread that caused the dogfood failure. Verification follows the pre-fix regression discipline: 6 new `prompt.test.ts` cases pin the `<system-reminder>` wrapper, the absence of the old imperative trailer, the "not a fresh user instruction" guard, the "advance the in_progress item" cue, and the "at most one in_progress" invariant across empty and non-empty states. All 6 fail on unchanged `formatTodoSection`; all 6 pass after the wrap+rephrase. Snapshot hashes in `agents/role-prompt.snapshot.test.ts` and `prompt/prompt-for-role.test.ts` updated for the 8 roles whose tool list includes TodoWrite (main + 7 user-dispatchable workers). The 4 internal roles (memoryExtractor / memoryCurator / skillCurator / skillConsolidator) do not carry TodoWrite, so their `includeTodos` path is dead and their hashes are unchanged. `pnpm test` 1755/1755 PASS, `pnpm typecheck` clean, `pnpm build` clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens the per-turn TodoList prompt suffix (now injected into the last user message for caching) so it’s clearly framed as framework state rather than something the model should treat as a fresh user instruction.
Changes:
- Wrapes the TodoList block output by
formatTodoSectionin<system-reminder>...</system-reminder>and rewrites the trailing guidance text. - Adds regression tests ensuring the todo block is wrapped, the old “Use TodoWrite…” line is removed, and key guardrails remain present.
- Updates prompt snapshot hashes impacted by the prompt text change.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/prompt.ts | Wrapes TodoList section in <system-reminder> and updates trailing guidance text. |
| src/prompt.test.ts | Adds regression tests pinning the new framing and updated trailer wording. |
| src/prompt/prompt-for-role.test.ts | Updates prompt snapshot hashes for affected roles. |
| src/agents/role-prompt.snapshot.test.ts | Updates prompt snapshot hashes for affected roles. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+131
to
+132
| // <system-reminder> and reword the trailing cue from imperative to state + | ||
| // continuation, mirroring the deferred-tools reminder framing. |
Comment on lines
+136
to
+151
| '<system-reminder>', | ||
| '## Current Todo List', | ||
| '(no todos yet)', | ||
| 'Use TodoWrite to keep this list current. Keep at most one item in_progress.', | ||
| '', | ||
| "This is the framework's snapshot of your todo state, not a fresh user instruction. When a task needs three or more sequential steps, open with a TodoWrite. At most one item in_progress.", | ||
| '</system-reminder>', | ||
| ].join('\n') | ||
| } | ||
|
|
||
| return [ | ||
| '<system-reminder>', | ||
| '## Current Todo List', | ||
| formatTodosForPrompt(todos), | ||
| 'Use TodoWrite to keep this list current. Keep at most one item in_progress.', | ||
| '', | ||
| "This is the framework's snapshot of your todo state, not a fresh user instruction. Keep advancing the in_progress item; update via TodoWrite as items change status. At most one item in_progress.", | ||
| '</system-reminder>', |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
instructions(system) into the last user message, but unlike the deferred-tools block it stayed bare prose ending with an imperative "Use TodoWrite to keep this list current ...". In user-role content the model read that trailer as a fresh user instruction, integrated/standalone-completed the todos, and stopped.formatTodoSectiononly: wrap the whole block in<system-reminder>(mirrors the deferred-tools framing) and reword the trailer from imperative to state + continuation ("This is the framework's snapshot of your todo state, not a fresh user instruction. Keep advancing the in_progress item ...").prompt.tsis the only production change. Tests + 2 snapshot files updated; 4 internal roles without TodoWrite are unaffected.Test plan
prompt.test.tscases pin the wrapper, the removal of the old imperative trailer, the "not a fresh user instruction" guard, the "advance in_progress" cue, and "at most one in_progress" invariant across empty + non-empty.formatTodoSection.pnpm test1755/1755 PASS,pnpm typecheckclean,pnpm buildclean.main+ 7 user-dispatchable workers (the 8 roles whosetoolsinclude TodoWrite). 4 internal-role hashes unchanged because theirincludeTodospath is dead.🤖 Generated with Claude Code