The consumer-side restated declare-merges retire (user ruling: one home per
projection key): TodoPanel imports the todos merge and TodoItem through
@deepseek-ai/dsh-tool-todo/client, and the manager takes the title merge
through @deepseek-ai/dsh-session-title/client — both pure-type outlets
re-exporting the domain's single-source types.ts, so no host value import or
Context merge enters the client program (type-only edges, exempt from the
plugin value-import ban). Workspace deps and tsconfig references added.
Also aligns the fixture's empty-log tail block with the host convention
(asOfSeq -1 with empty values, block always present on tail requests).
additionalProperties stays true in the published schema while execute
rejected extra keys, so generated typings and validation disagreed with
runtime behavior. The item schema now declares additionalProperties:
false — the registry's arg validation rejects extra keys with a
path-qualified violation before execute runs — and the redundant manual
check is dropped (tool catalog regenerated).
An item carrying keys beyond content/status (ids, children, priority) was
flattened to {content, status} on append, so the logged snapshot diverged
from what the model believed it wrote (model-visible must equal logged).
Reject loudly; the isError result lets the model self-correct.
The definitive sweep (audit every AGENTS.md mention in packages/, docs/,
examples/, scripts/) found thirteen more citations of relocated policy
and two citations of rules that never existed as quoted:
- with-key policy comments (web deepseek/perplexity e2e headers) ->
docs/testing.md; real-impl-over-mock comments (acp harness, load,
stream-update specs) -> docs/testing.md; defensive-pattern quotes
(acp index.ts x3, stream-update) -> docs/defensive-patterns.md.
- md-tier repoints: real-api-e2e RFC, tool-schema-catalog RFC,
postmortem 0001 guardrail row, adding-a-package cookbook,
drop-bash-output-spill-files RFC, acp-subagent-backend RFC phrasing.
- Two false attributions dropped in favor of self-contained reasoning:
tool-todo's 'don't validate scenarios that can't happen' and the
bash-stdin-env RFC's 'Don't add features beyond what the task
requires' (neither rule ever existed under those names).
- Citations of the two 'not golden truth' doctrines stay: those bullets
survive verbatim in the root conventions.
Note: packages/support/ui-stdio readline TTY spec flakes under full
coverage on a heavily loaded box (passes standalone and passed the
same tree's coverage run minutes earlier); untouched by this stack.
Replace the "bag of optional fields" tool-presentation types
(ToolCallPresentation / ToolResultPresentation / ToolTerminal) with a
card-tagged discriminated union — the standing FIXME(tool-presentation).
A tool declares one render intent per call/result and the ACP bridge
switches on `card`:
ToolCallView = generic | terminal | diff
ToolResultView = generic | terminal
The `diff` card is new: fs write/edit now emit an ACP {type:'diff'}
content block (an editor's inline diff), which the old shapes could not
express. The bridge also relativizes a file card's title against the
session cwd (mirroring claude-agent-acp's toDisplayPath) while keeping
locations/diff paths raw, and derives the no-capability fenced console
fallback from a terminal result's output. read gains the window-in-title
(`Read foo.txt (5 - 8)`) and an always-set location line, matching the
reference adapter field-for-field.
Migrates all three producer families (tool-fs, tool-bash, tool-todo) and
the sole consumer (the ACP bridge) together — the source does not compile
piecewise. Adds snapshot coverage for the terminal _meta path (a new
capability-advertising scenario) and re-records the fs goldens to show the
diff cards. Applied-hunk (result-time, context-line) diffs need a new
result/event shape and are a follow-up.
RFC: docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md
The registry's validateArgs rejects a bad `status` enum before execute runs, so
the in-body re-check (`status !== 'pending' && …` → throw) was unreachable dead
code — line 70 was uncovered, failing the per-file 100% coverage gate. Narrow
the registry-guaranteed value with `status as TodoItem['status']` instead of
re-validating it, mirroring tool-bash (which only checks what the DSL can't
express). The malformed-status test still passes — it exercises the registry's
rejection, the actual path. Coverage back to 100%.
toTodoList dedupes and length-checks on the trimmed content but stored the raw
item.content, so a todo with leading/trailing whitespace was deduped by its
trimmed form yet persisted untrimmed — the stored value and the uniqueness key
could differ. Store the trimmed content so the persisted list matches what was
validated.
Add @deepseek-ai/dsh-tool-todo (a new packages/todo/ group): a model-facing
todo_write(todos: [{content, status}]) tool with whole-list-replace semantics.
Each call appends the full list as a todo/write event to the calling agent's
session log; the current list is the most recent such event (last-write-wins).
Single-owner — a non-agent caller is rejected. Beyond the schema's
type/required/enum checks, execute rejects empty/duplicate content and more than
one in_progress task, narrowing the loosely-typed args into a real TodoItem[].
Both UIs render off the existing session/event: the stdio UI prints a glyphed
checklist; the ACP bridge maps the list to a `plan` sessionUpdate (todosToPlan
synthesizes the priority ACP requires; status maps 1:1). Wired into the
coding-agent, acp-agent, and snapshot example configs with a system-prompt nudge.
Tests: unit (schema, validation, append/replace, no-agent rejection, presentCall,
HMR-safety, Loader export-shape guard), full-loop integration through the agent
loop, the ACP todosToPlan mapping + stream-update arm, the stdio render arm, and
a session/load replay that re-emits the plan. New-group TS wiring added to
tsconfig.base/json/build. RFC + a doc-inventory sweep (architecture, packages
README, AGENTS layout, cookbook group list, example READMEs) ship with it.
The todo-plan ACP snapshot scenario is recorded separately (needs an API key).