Rewrite the hook-protocol RFC's process-relative wording (freshly-landed
/ days-old / week-old) as timeless evidence anchored to the recorded
RFCs, and sweep the same class from the steering, replay-config,
subagent-vocabulary, and vocabulary RFCs. Narrow the fs RFC's acceptance
criterion: replaceAll survives on the request spec and version on other
outcome types by design — name the exact removed surfaces instead of
claiming the spellings vanish.
The branch now bases on the hooks stack (PR #138's head), so every
'in-flight' reference to stack content became a current-state fact.
Audited all nine RFCs + three supplements claim-by-claim against the
merged tree (none invalidated; several strengthened):
- prune-dead-core-spine-surface: describe the landed tools/pre-execute →
dispatch → tools/post-execute pipeline — listeners return Decisions,
the registry builds every result and snapshots it to protect callId,
and a second (mutation-guard) test pins the field; drop the resolved
wait-for-interception hedge; scope the additionalContext ferry out.
- prune-producerless-vocabulary-variants: the ui-stdio fixture migrated
off the continuation trigger (llm-replay is the sole writer now); the
stack's own additions (rejected, prompt/blocked, hook/invoked+result)
all arrived with producers — the admission policy demonstrated live.
- prune-unimplemented-subagent-vocabulary: enrichment landed as
lastAssistantMessage only (agentType was dropped in the stack's own
review — the same judgment this RFC extends); the seam RFC now names
tools/pre-execute deny, which exists, sharpening the re-add path.
- drop-inert-request-knobs / drop-image-content-block /
trim-acp-bridge-unreachable-surface: current-state rewordings (shipped
bridges set no request fields; only compact-basic has explicit image
arms; 13 hook goldens also pin agentInfo).
- generic-long-running-tool-runtime census: second production seam
consumer (hook-protocol runHook: resolve+run, stdin/env, foreground
only — background machinery stays single-consumer); scrub-duplication
blast radius.
- discover-package-inventory: identical 54-entry tsconfig reference
sets; the comparesLog scenario knob (fixture-derivable, like recorded).
- unify-agent-and-session-id: third divergence site (in-process subagent
children mint two UUIDs), the hooks bridge id-lookups, and ui-stdio's
labelBySession map as a consumer that deletes under unification.
New RFCs from the post-stack survey:
- remove-agent-steering-mirror: the last mirror-of-durable event; zero
production listeners; both retention RFCs deferred its fate, and the
'no durable twin' rationale is contradicted by the adjacent append.
- tighten-hook-protocol-contract: producer-less 'native' dialect,
parsed-and-discarded suppressOutput, and hook/result semantics
(truncation + decision-string) defined twice in the bridges instead
of the lib that owns the event.
- single-source-acp-replay-config: cordis.yml/cordis.snapshot.yml differ
by exactly one plugin entry, with no gate on the forced symmetry.
The core-spine RFC's acceptance criterion still asserted all three
surfaces 'appear only in this RFC', contradicting the corrected scope
(runLoop/Inbox stay as package-internal symbols; only the public
re-exports go). Also add the deepseek README image-skip row and the
compact-basic [image]-placeholder row to the image RFC's removal set.
Two overstated evidence claims, reworded to the precise fact:
- prune-dead-core-spine-surface: runLoop has no importer OUTSIDE the
package (its own agent.ts drives the loop with it); the dead surface
is the public re-export, not the function.
- generic-long-running-tool-runtime census: BashTask.done has no
consumer through the public seam, but is production-load-bearing
inside dsh-bash-local (disposal awaits it); only the public
completion surface is single-consumer.
Also fold the reviewer-located doc sites into the removal sets so the
implementing PRs need no re-discovery: the llm/pi-ai/compact-basic
README rows and the adding-an-llm-adapter cookbook line (prefill/image),
the content-block-vocabulary RFC's has-a-home consequence lines
(cache/prefill/image), the tools.md paste + type-equiv manifest row +
tools README row (callId), and the session-surface RFC's
full-rebuild-after-replacement sentence (invalidate).
Load both hook bridges in the ACP example (dsh-hooks-claude → ./hooks.json,
dsh-hooks-codex → ./codex-hooks.json) so the full-transcript snapshot tier can
exercise each dialect against the real app. An absent config file is a silent
no-op, so a scenario carries only the file it needs and the other bridge
vanishes — verified byte-identical against every pre-existing snapshot.
Add a scenario per hook point × its headline Decision outcome, both dialects:
UserPromptSubmit block (authored, keyless) + context-fold, PreToolUse deny/ask,
PostToolUse block/context, Stop force-continue. The mid-turn scenarios are
recorded against the real API with the hook active, so the model's reaction to
a denied/blocked/force-continued turn is part of the replayed transcript.
SessionStart and SubagentStart are deliberately excluded (detached best-effort
inject races the log position — a recorded golden fails 10/10 on its own
replay), as is SubagentStop (observe-only, zero transcript footprint — a golden
could never be proven to fail). Both stay on the bridges' unit coverage. See
docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.md.
Codex review of the vocabulary relocation found two doc-accuracy issues:
- presentation.ts's JSDoc used {@link ToolDefinition...}, which the
TypeScript language service cannot resolve because presentation.ts
deliberately does not import index.ts (that would create the cycle the
split avoids). Demote those three to plain `ToolDefinition` code text;
same-file and imported @links (TerminalResultView, ContentBlock) stay.
- docs/core-data-structures/tools.md's source header listed only index.ts
and schema.ts; add presentation.ts, which now owns the presentation
vocabulary the page documents.
The tool render-intent vocabulary (ToolCallView/ToolResultView + members,
FileLocation, FileDiff, ToolCallKind) is the UI-facing surface of
dsh-tools; it lived inline in index.ts alongside the registry and
execution core. Move it to its own presentation.ts module so index.ts is
the registry + execute waterfall and the presentation vocabulary is a
separate, one-directional dependency.
presentation.ts owns ONLY render-intent types and references none of the
execution types; index.ts imports the view types for ToolDefinition's
presentCall/presentResult signatures (clean acyclic index -> presentation).
The opaque `meta` presentation channel (ToolExecuteReturn, ToolResult,
ToolExecutionResult) is execution plumbing and stays in index.ts.
Public surface unchanged: index.ts re-exports the vocabulary, so consumers
(tool-fs/tool-bash/tool-web/tool-todo, the ACP bridge) keep importing from
@deepseek-ai/dsh-tools with zero churn. No producer/bridge/test edits; a
pure internal relocation with no observable-output change (snapshot goldens
untouched).
The result-time diff card sent view.title raw, so a completed edit/write
of an absolute in-workspace path flipped the card header back from the
relativized `Edit src/b.ts` to the absolute path — the pending card
relativizes, the result did not, and tool_call_update.title replaces the
header. Apply displayTitle to the result diff arm using the diff path,
mirroring the call-side card. Regression test proven red on the unfixed
arm.
Also record the overwrite diff-basis pre-read as a bounded follow-up
(TODO(overwrite-diff-bound) + RFC non-goal): overwriting a large file
reads the whole prior text into memory for a UI-only diff.
Redesign per review: neither language is canonical. A pair is three
sibling files — foo.md, foo.zh.md, foo.i18n.yaml — and either language
may be authored first (a Chinese-first RFC is as legitimate as an
English-first one). The sidecar record holds the FULL git blob hash of
both sides as of the last confirmed-consistent state, replacing the
in-file one-directional fingerprint; editing either side without
re-confirming the pair goes red. New --write mode re-records a pair
after both sides are brought in line, making the confirmation a
reviewable yaml diff. Pairs merge whole (completeness enforced).
- gate rewritten around pair anchors (union of .zh.md and .i18n.yaml
remnants) so half-deleted pairs are caught from either side; red/green
proven for en-only edit, zh-only edit, missing record, and a record
for an excluded file
- verify-rfc-classification now skips .zh.md counterparts (same RFC,
indexed via its English filename; the pairing gate owns consistency)
- docs/i18n/README.md + translation-rules.md reframed bidirectionally
(terminology table binds both directions; typography section governs
the Chinese side); zh counterparts updated; skill workflow updated
- RFC amended to the shipped design, records the English-canonical
in-file-fingerprint model as considered-and-revised; RFC translated
(docs/rfc/.../2026-07-02-bilingual-docs-and-pairing-gate.zh.md) and
added to the required frontier
- generated docs stay excluded with the follow-up recorded: teach the
generators to emit Chinese, then de-list
Codex's stale-prose pass found seven more spots still describing the
result diff as ALWAYS an applied contextual hunk, or a create/binary
overwrite as rendering "only the call-time card": the DiffCallView JSDoc
and the acp bridge diff-arm comment, the FsWriteOutcome.before and
readTextForDiff JSDoc, and three RFC lines. All now say: the result diff
is the applied change — a contextual hunk when there is a before-image,
else a whole-file diff (create / undiffable binary) — and a successful
mutation always returns the result diff so the model-facing text can't
clobber it. Regenerate the cordis catalog (source line shift).
The write-diff-card fix made write's presentResult return an args-derived
whole-file diff (oldText:null) for a create / unchanged overwrite, but the
DiffResultView contract and its mirrored docs still said `diffs` is ALWAYS
the applied contextual hunks computed from before/after. Correct the type
JSDoc, the write execute-side comment, and the four mirrored surfaces
(tools.md, tools README, acp-feature-support, adding-a-tool cookbook) to
say: typically the applied hunks, or a whole-file diff when there is no
before-image (a create) — and that a mutation returns the diff result even
when it duplicates the call-time card, since a tool_call_update.content
replace would otherwise clobber the diff with the model-facing text.
Regenerate the cordis catalog (source line shift).
A Write CREATE rendered its completed tool_call_update as the model-facing
result TEXT (`<path>…</path>…Created file`), which — because an ACP
tool_call_update.content REPLACES the call's content — clobbered the
new-file diff the pending call installed. So Zed showed the diff, then
replaced it with raw XML-ish text; only overwrite/edit looked right
(their result re-sends a diff).
write's presentResult now ALWAYS returns a diff card for a successful
write: the applied contextual hunk from `meta` when there is one
(overwrite), else an args-derived whole-file diff (`oldText: null`) for a
create or an unchanged-content overwrite. This matches claude-agent-acp,
where the create diff rides on the update and no result text replaces it.
An error still falls through to generic rendering so its message shows.
edit is unchanged (it always has a hunk; no whole-file fallback).
Re-recorded fs-write / fs-write-overwrite goldens; the create's completed
update is now a {type:'diff'} block, not the XML result text.
The pasted type-equiv block's JSDoc still said the meta payload is
`{@link JsonValue}`; the source comment is `unknown` (the meta channel is
opaque at the seam). verify-type-equiv compares type structure, not the
comment, so the drift slipped through — align the doc comment.
Address the applied-hunk-diffs review:
- CRLF write overwrite emitted bogus every-line-changed hunks: write's
`before` was LF-normalized but `after` kept the raw model content, so a
CRLF rewrite of an LF file diffed every line. Normalize write's `after`
to LF so both sides share the diff basis (edit already did). Regression
test proves it fails on the raw-after path.
- The tool-private `meta` payload is now typed `unknown` (opaque) at every
seam instead of `JsonValue`. This drops the `dsh-tools -> dsh-session`
package edge that existed only to name the type, and removes the
`FileDiff` index signature that had been widening the type solely for
JsonValue-assignability. Serializability is still enforced at runtime by
`Session.append`'s isJsonValue check, which was always the real guard.
- Sync the docs the new result/meta surface left stale: ToolResultView's
diff card + ToolExecutionResult.meta in tools.md/session.md type-equiv
blocks, the acp/tools READMEs, and the adding-a-tool cookbook; regenerate
the cordis catalog and module graph.
A four-way parallel review of every EN<->ZH pair (one reviewer per pair,
findings verified before applying) surfaced:
- terminology: stale-sense 过期 -> 陈旧 in development.zh.md (the table
reserves 过期 for expired); GitHub workflow -> GitHub 工作流 per the
table; dropped the invented 质量 in 'CI gates' -> CI 门禁; taxonomy ->
分类体系 in translation-rules.zh.md; skill first-occurrence annotation
added in the i18n README
- verbatim spans: <current-blob> restored inside an inline code span the
translation had localized
- typography: full-width dashes normalized to no surrounding spaces
across all four files (the rules' own MUST); one 顿号 between clauses
-> comma; 顿号 before 以及 dropped
- fidelity/wording: must-not rendered 不得 (not 不应); local setup ->
本地环境搭建; enforce -> 强制执行; verified surface -> 受验证的范围;
batch-lands-before-neighbors nuance restored; 更新粘贴内容
freshness -> 新鲜度 confirmed against MDN's zh HTTP caching docs (freshness
lifetime -> 新鲜度生命周期); precedent now cited in the table. The same
source pairs stale with 陈旧, not 过期 (过期 maps to expired), so the stale
entry and the i18n README translation now say 陈旧译文.
Every new term the bilingual-docs work introduced, with precedented
renderings where precedent exists:
- orphan -> 孤立 (git's official zh l10n renders orphan as 孤立, e.g.
孤立分支 — not 孤儿; the translations were corrected to match)
- info string -> 信息字符串 (CommonMark zh convention; corrected in the
i18n README translation)
- fenced code block -> 围栏代码块 (MDN zh), staged -> 暂存 (git zh),
event-sourced -> 事件溯源 (DDD convention), smoke test -> 冒烟测试,
fail-fast -> 快速失败, plus fingerprint/pairing/freshness/stale/contract
- mechanism names coined by this repo, marked as such in the notes:
language switcher -> 语言切换行, structural signature -> 结构签名,
enforcement frontier -> 强制边界
- keep-English entries so future translators don't guess: backlog,
blob hash, CI, doc-sync, e2e, monorepo, PR, worktree
fs write/edit now emit a result-time contextual-diff tool_call_update
(the applied hunk with ±3 context lines, one hunk per replace_all site),
matching what claude-agent-acp sends and what makes an editor render the
change in place. The call-time snippet diff stays; the result hunk
supersedes it (ACP content-replace).
Mechanism:
- A persisted tool-private `meta` channel: execute may return
`{ content, meta }`; `meta` (JsonValue) rides on the tool/result event
and is handed back to presentResult, so the diff reproduces on replay
(event-sourced). JsonValue is now exported from dsh-session.
- The backend returns raw before/after text (storage facts) on
FsWriteOutcome/FsEditOutcome; the tool computes the hunk via the npm
`diff` package's structuredPatch. A create has no before → no result
diff; a failed/aborted mutation carries no meta.
- ToolResultView gains a DiffResultView; the bridge's result-side switch
renders it as {type:'diff'} content blocks.
RFC: docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
(justifies the npm `diff` runtime dep over vendoring and the meta channel);
the render-intent-union RFC's Non-goal is updated to record this shipped.
All fs snapshot goldens re-recorded; edit/overwrite gain the contextual
result diff, create/read/policy-reject unchanged in structure.
Review findings addressed:
- The gate compared only heading and code-block COUNTS, understating the
contract it claims to enforce. It now compares ordered structural
signatures: heading depths, fenced code blocks verbatim (info string +
content), table column counts, list kinds, and every link target except
the language switcher. Proven red on a heading demotion, a reworded
code-block comment, and a retargeted link; green on all existing pairs.
- Stated the gate's limit explicitly (header comment + docs/i18n/README.md
both languages): green means fresh and structurally sound, NOT verified —
translation quality is the reviewer's half of the contract.
- first-line extraction no longer silently drops the last character of a
newline-less file (split with limit instead of indexOf slice).
- isExcluded documents the trailing-slash-is-the-boundary invariant.
- Rollout guidance: grow the required frontier at the pace translation
review is resourced.
- dsh-code-review's doc-sync sublist is now the exhaustive chain.
docs/i18n/README.zh.md updated via the minimal-diff workflow and
re-fingerprinted.
First backlog item translated with the dsh-translate-docs skill: full-file
translation, terminology per docs/i18n/terminology.md, structure locked to
the source (11 headings, 10 byte-identical code blocks), fingerprinted and
added to the manifest's required list.
The closed WebErrorCode union leaked fetch-transport details (redirect,
too-large, content-type) into the seam's shared vocabulary and made web
the only seam with a closed error-code union. Drop it and let WebError
carry an open code: string like LlmError/SubagentError; document the
codes grouped by owner (seam-neutral vs dsh-web-fetch-local transport).
Addresses tianyicui's leaky-abstraction review comment on WebErrorCode.
Establish EN->ZH bilingual documentation for the README and docs tree:
- docs/i18n/README.md — the pairing contract: sibling foo.md <-> foo.zh.md,
English canonical, blob-hash source fingerprints, language switchers,
scope/exclusions, and a manifest-driven rollout ratchet.
- docs/i18n/translation-rules.md — how to translate: faithfulness, structure
preservation, terminology discipline over docs/i18n/terminology.md, and
typography rules grounded in MDN/K8s/Vue/clreq conventions.
- .agents/skills/dsh-translate-docs — the committed agent workflow, following
the dsh-code-review pattern of deferring to docs as sources of truth.
- scripts/verify-translation-pairing.ts + manifest — a doc-sync gate: required
pairs exist; every existing .zh.md is fresh (fingerprint = current source
blob), switcher-linked, structure-matched, and non-orphaned; excluded
(generated) docs stay unpaired. --list prints the translation work list.
- RFC (implemented/process) recording the decision and the alternatives.
- Dogfood: README.zh.md and the two i18n docs translated under their own rules.
Gates: doc-sync green including the new gate; red/green proven for stale
fingerprint, orphan, and excluded-file violations.
Use the new filesystem seam for skill file reads and system skill writes when ctx.fs is available, and include the skill tool in the generated tool catalog.