Commit Graph

142 Commits

Author SHA1 Message Date
Tianyi Cui
bc7da642d4 feat: fold the Code Mode demos into demo:code-mode with a UI argument
Code Mode is the point; the UI is just the surface it happens to wear.
demo:code and demo:acp-code collapse into one dispatcher
(scripts/demo-code-mode.mjs): `pnpm run demo:code-mode [repl|acp]` —
repl (default) boots the stdio REPL over examples/code-agent, acp
serves examples/acp-agent's code-mode overlay; each UI runs the exact
node invocation its standalone script ran, and an unknown argument
fails loud with usage. All nine references across READMEs, the RFC,
the overlay header, and the keyless-smoke comment renamed. Smoked all
three paths: usage exit 2, ACP initialize handshake, REPL boot + EOF.
2026-07-09 12:16:37 +08:00
Tianyi Cui
1be9baeb7b feat: add demo:acp-code — the ACP demo in Code Mode
Boots the acp-agent example through the existing code-mode.cordis.yml
overlay (tool surface collapses to run_code + the generated TypeScript
SDK, dispatching through the worker-thread runtime), mirroring how
demo:code relates to demo:repl on the stdio side. The overlay header
and both READMEs now name the demo as a consumer. Smoke: the server
answers an ACP initialize handshake with a clean frame on stdout.
2026-07-09 11:44:10 +08:00
Tianyi Cui
673f20c990 Merge remote-tracking branch 'origin/master' into code-mode-tools
# Conflicts:
#	docs/event-producer-consumer.md
#	packages/support/acp-snapshot/tests/suite.spec.ts
2026-07-09 11:06:50 +08:00
Tianyi Cui
35ef649716 Merge branch 'code-runtime-worker' into code-mode-tools
Brings in the refreshed base (master merged through the stack after #203
and #205 landed), including the acp-snapshot extraction (#204), and
re-ports this PR's snapshot-suite extensions onto the extracted package:

- dsh-acp-snapshot's Scenario gains headerClass and configPath; the suite
  factory pins the request header PER CLASS (construction rejects a
  missing or duplicated class pin), forwards a scenario's configPath to
  the harness (RunOptions.configPath overrides AgentUnderTest.configPath),
  and a new fixtures meta-test asserts every pinning fixture carries
  exactly one request/header and no deltas.
- The acp-agent example's thin scenario table re-registers code-mode-turn
  and both-mode-turn with their overlay configs and per-class pins; the
  committed fixtures replay unchanged.
- The package's synthetic suites cover the new surface (explicit
  headerClass on one suite, the default on the other, a configPath
  override through the fake bin, and the two construction throws).
2026-07-08 15:55:29 +08:00
kingwl
a0e39db3b6 test(acp-snapshot): add the repeat-tool-guard reminder scenario 2026-07-08 14:24:20 +08:00
Tianyi Cui
84088300bc fix: pre-dispatch rejection of unloggable args, mutation-proof event copies, proto-safe bindings (Codex round 1)
Three findings from the PR-4 convergence round:

(A) A root-undefined binding argument passed normalization untouched, so
the sub-call DISPATCHED and only then failed the tool/code-dispatch append
(Session.append rejects undefined event data) — a sub-call executed with
no log record, violating the nothing-executes-unlogged contract. And the
tool received the SAME object later handed to the append, so a tool
mutating its args desynced the logged record from what was dispatched (or
re-poisoned the append). jsonNormalizeArgs now rejects undefined up front
with a model-correctable message and returns TWO independent parses of the
canonical JSON text: the tool gets one, the event logs the sibling —
identical by construction, mutation-proof.

(B) The bridge built its bindings record with plain-object assignment, so
a registered tool named __proto__ hit the prototype setter and silently
vanished (the runtime host resolves binding names as own properties). The
record is now null-prototype with defineProperty, mirroring the
worker-side namespace build.

(B) The header-pin sanity assertions ran only inside NON-pinning
scenarios, so a class consisting solely of its pinning scenario (the two
Code Mode classes) would accept a re-recorded pin carrying several headers
or a header-delta. A fixtures meta-test now asserts every pinning fixture
directly.
2026-07-08 13:39:51 +08:00
Tianyi Cui
b59d245c7c feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.

run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).

The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
kingwl
556f847064 feat(acp-snapshot): extract the ACP snapshot suite into a support package
The snapshot tier's machinery leaves examples/acp-agent/tests for
packages/support/acp-snapshot (@deepseek-ai/dsh-acp-snapshot), where the
coverage gate measures it and a second example can consume it instead of
forking it: harness.ts (runScenario, parameterized by an AgentUnderTest
{binScript, configPath, tsconfigPath} instead of module constants),
normalize.ts (moved verbatim), and suite.ts (defineAcpSnapshotSuite — the
per-scenario golden/log compares, record write-back, per-suite header pin
with its uniformity guard, and the fixture guard block, lifted from
acp.snapshot.ts). The example file collapses to its scenario table plus
one factory call; env reading (DSH_SNAPSHOT) stays at that edge.

The exactly-one-pin meta-test generalizes from the hardcoded text-turn
name to "exactly one per suite" — which scenario pins is the scenario
table's reviewable choice (per-suite pinning per the proposal RFC).

Extraction parity: pnpm run test:snapshot is 36 passed + fs-policy-reject
failing BEFORE AND AFTER (BSD-sed environment failure, reproduced at the
base commit in a clean worktree — the recorded golden's sed -i syntax is
GNU-only), with zero byte changes under examples/acp-agent/tests/snapshots/.
Coverage for the new src files lands in the next commit.
2026-07-08 02:38:25 +08:00
Tianyi Cui
e930dd328a Merge branch 'master' into worktree-llm-tool-order 2026-07-07 17:01:32 +08:00
imccyu
b67cda482a fix: update snaphsot 2026-07-07 16:45:27 +08:00
Hypatia May
05acc6ebc3 Merge remote-tracking branch 'origin/master' into session-fork 2026-07-07 09:21:01 +08:00
Hypatia May
4f55c15cf8 Merge remote-tracking branch 'origin/master' into session-fork 2026-07-07 09:06:00 +08:00
Tianyi Cui
515d04339b Address review round 2: preserve delta insert arity; reject unpinned header-deltas
Residuals from the Codex re-review:

1. A system delta's insert was flattened to one token, so deltas differing
   only in inserted-line count compared equal. Now one {{system}} token per
   inserted line — position AND extent survive, content does not.

2. The live uniformity guard folded only request/header snapshots, so a
   mid-run header CHANGE (request/header-delta) could diverge from the pin
   invisibly. Non-pinning runs now assert zero header-delta events: a
   scenario that legitimately changes its header mid-run exists to show
   that change, so it must pin (fail-loud until it does).
2026-07-07 01:26:25 +08:00
Tianyi Cui
a0d8f33b29 Address review: structure-preserving delta scrub, live header-uniformity guard, RFC style
Codex review findings on the pinned-header change:

1. scrubRequestHeaders flattened a request/header-delta's whole
   system/tools payload to one token, so two meaningfully different deltas
   compared equal. Now the structural facts survive — keepStart/keepEnd
   line positions, added/removed/changed tool NAMES — and only the bulk
   (inserted prompt lines, schema bodies) is tokenized.

2. The one-pin design rested on an unasserted premise (all sessions
   compose the same header). Every non-pinning scenario now asserts, live,
   that each request/header its run produces equals the pinned fixture's
   header (both sides normalized against their own volatile values), so a
   session-dependent header fails loud until it gets its own pin.
   Verified the guard bites: perturbing the pinned fixture's prompt fails
   a non-pinned scenario with the intended message.

3. RFC de-slopped per docs/AGENTS.md: no PR reference, no SHOULD
   spec-speak; Decision/Verification/Consequences updated for 1 and 2.
2026-07-07 00:51:55 +08:00
Tianyi Cui
1fd9e2a9ab Pin request-header content in one snapshot scenario
Every session.jsonl fixture embedded the full composed system prompt and
complete tool-schema list in its request/header event (~8 KB on one line,
identical across the suite), so any prompt or tool-schema edit forced a
re-record or hand-edit of every fixture — see the dynamic-workflows PR for
the churn pattern this removes.

Now exactly one scenario (text-turn, flagged pinsHeader) commits and
compares that content verbatim; every other fixture stores and compares it
as {{system}}/{{tools}} tokens via the new pure scrubRequestHeaders
normalizer (applied to both compare sides and to record-mode writes, so a
re-record cannot reintroduce the content). request/header-delta payloads
are scrubbed the same way; config/reason stay verbatim — a model swap
SHOULD churn every fixture, a prompt edit should not. Replay is unaffected:
script derivation reads only assistant/chunk events.

Fixture meta-guards enforce the split: non-pinning fixtures must be fixed
points of the scrub, the pinning fixture must not be, and exactly one
scenario pins. Committed fixtures migrated through the same function.

Docs: pinned-header RFC (implemented/testing), base snapshot RFC + testing
policy + llm-replay module doc/README updated.
2026-07-06 23:41:20 +08:00
Tianyi Cui
c57c7527a5 Merge branch 'master' into codex/parallelize-ci 2026-07-06 21:54:07 +08:00
Hypatia May
50ffa0c9a9 test: use portable command in fs policy snapshot 2026-07-06 16:00:52 +08:00
Hypatia May
6a7bb4045b test: normalize platform sed snapshot output 2026-07-06 14:35:22 +08:00
Tianyi Cui
7376346246 test: re-record snapshot goldens — every transcript gains its request/header event
One re-record after the header events landed: recorded scenarios
re-harvested against the live API; the three fs-writing fixtures are
relativized (this recording's model happened to emit absolute
file_path arguments, which only round-trip through replay when the
path is cwd-independent) and, with the two never-re-recorded authored
scenarios (error-finish, cancel), rewritten in the normalized
authored-fixture form the harness documents — each now carrying the
request/header snapshot the loop logs before its first dispatch.
Keyless replay verified green across all 35 scenarios.
2026-07-06 03:39:48 +08:00
Tianyi Cui
3464f88297 test: shorten compaction e2e smoke 2026-07-06 03:12:37 +08:00
Tianyi Cui
44664551cf test: wait for ACP stdout frame under e2e parallelism 2026-07-06 01:54:09 +08:00
Tianyi Cui
6ea7d7af53 ci: parallelize real-api e2e files 2026-07-06 01:10:27 +08:00
Tianyi Cui
3f83a4ee96 review: the persona becomes the system-prompt plugin's deployment config
Review round 2 (tianyicui inline comments):

- dsh-system-prompt itself registers the harness:identity (-100) and
  deployment:persona (0) sections — they must survive a swapped loop
  plugin, so they leave dsh-agent-loop; the persona text is the plugin's
  own validated 'persona' config. The model/cwd variables STAY on the
  loop: runtime facts of the agents it drives.
- AgentOptions.systemPrompt is deleted with all its forwarding plumbing:
  the app configs' systemPrompt keys become 'persona' routed through
  dsh-agent-core (schema = z.intersect of the owners'), the ACP bridge
  and tool-subagent stop carrying persona configuration, and subagent
  children now render the deployment persona like every other agent.
- Example personas drop transport/interface trivia (ACP, CLI) — facts
  irrelevant to the model.
- Root CONTEXT.md removed (not idiomatic); its persona definition was
  wrong under the new ownership anyway.
- Docs, READMEs, the prompt-variables RFC, and generated catalogs
  updated; new loop test pins the assemble-waterfall escape valve
  (an emptied assembly sends NO system field).
2026-07-05 23:23:46 +08:00
Tianyi Cui
2304f7a245 Merge origin/master: adopt the DeepSeek Harness SDK product name
Reconciliations beyond textual conflicts:
- product rename (DeepSeek Code -> DeepSeek Harness SDK) applied to the
  PR-added assertion in system-prompt.spec.ts that master's rename
  commit could not reach
- architecture.md: master's rewrite kept; this PR's prompt-assembly
  semantics re-added in the new doc's voice (Turn Flow footnote +
  service-spine row), within the 1630-word ceiling
- cordis catalog regenerated into master's split events.md/services.md
  (events-and-services.md deleted); module graph and doc graphs
  regenerated to pick up this PR's new events and dependency edges
2026-07-05 20:54:48 +08:00
Tianyi Cui
dca7a436e4 docs: update product name 2026-07-05 15:31:50 +08:00
Tianyi Cui
54a3c8b2a4 docs: address graph review placement 2026-07-05 02:54:01 +08:00
Tianyi Cui
f256f3961d feat(system-prompt): prompt variables, persona-as-section, tool-guidance ownership
One principle: every fact in the assembled prompt has exactly one owner.

- dsh-system-prompt: merge-extensible AssembleContext on assemble();
  a variable(name, provider) registry; {{name}} interpolation in
  renderPrompt, strict (unknown/valueless/malformed references throw);
  duplicate section and variable names rejected; assembly carries
  resolved section text + variables through the assemble waterfall.
- dsh-agent declares AssembleContext.agent; dsh-agent-loop registers
  the agent:persona section (order 0 - identity renders before tool
  guidance) and the model/cwd variables, and drops its string join:
  renderPrompt(assembly) IS the full prompt.
- Tool guidance moves to its owners: descriptions carry per-tool
  semantics; sections only cross-call habits (tool:bash exit-code
  habit at order 105; read's not-shell nudge). todo/subagent need no
  section - their descriptions already carry the contract.
- SubagentProvider.inheritsParentContext (spawn/acp false, fork true);
  dsh-tool-subagent derives truthful per-provider wording and resolves
  the provider at load (backend must be listed first).
- Example personas shrink to identity + behavior with {{model}} (and
  {{cwd}} in the ACP tree); the welcome banner stops enumerating tools.

RFC: docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
2026-07-05 01:54:46 +08:00
Tianyi Cui
f90f1c358e Merge remote-tracking branch 'origin/master' into simpl-e-acp-trim
# Conflicts:
#	docs/rfc/README.md
2026-07-04 23:22:16 +08:00
Tianyi Cui
060f272093 Merge branch 'simpl-b2-app-boot' into simpl-b3-replay 2026-07-04 21:18:39 +08:00
Tianyi Cui
e2cd7379ed Merge remote-tracking branch 'origin/master' into simpl-e-acp-trim 2026-07-04 21:11:25 +08:00
Tianyi Cui
ef86a36ef6 Merge remote-tracking branch 'origin/master' into simpl-b1-fold-ui-stdio
# Conflicts:
#	AGENTS.md
#	packages/README.md
2026-07-04 20:38:38 +08:00
Tianyi Cui
c073a3bd61 Merge remote-tracking branch 'origin/master' into worktree-docs-overhaul-b-agents 2026-07-04 17:57:16 +08:00
Tianyi Cui
9db3ce7313 test(acp): assert the patch target's name; state the overlay's true failure modes
Codex review, with its own probes, showed the RFC overclaimed: the
disable patch carried no name assertion despite the text crediting one,
and an id rename is not fail-loud — the skipped patch's warning needs a
logger the replay app deliberately lacks, and the resulting keyless
adapter entry fails inside its fiber without reaching the
unhandled-rejection guard (verified by a subprocess probe of the real
installFailLoud + boot composition). The overlay now asserts
name: dsh-llm-deepseek on the patch (a reused id can never disable the
wrong plugin), and the RFC records the honest residual: an id rename
degrades to config rot with replay output still correct (llm-replay
owns the stream short-circuit), plus the insert-collision last-wins
fact.
2026-07-04 17:54:17 +08:00
Tianyi Cui
5a450af738 test(acp): derive the replay config from the live config
examples/acp-agent/cordis.snapshot.yml is a 26-line declarative
overlay: one entry mounts @cordisjs/plugin-include on ./cordis.yml with
patches that disable the llm-deepseek entry by id and insert
llm-replay. Every other entry is the live tree loaded through the
include, so the replay tier exercises exactly what ships and an
app-shape change lands once — the silent-drift class the hand-mirrored
125-line twin invited is structurally gone. The bin is untouched;
recording still boots cordis.yml; assertEntriesLoaded tolerates the
disabled entry by design. All snapshot scenarios pass unchanged,
byte-identical goldens included; the include applies patches at load
time only, which a one-shot replay boot is exactly.

Implements docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md
(moved from proposed/ and amended); the acp-snapshot-tests and
hook-snapshot-matrix RFCs' replay-config facts are amended in the same
change.
2026-07-04 17:29:07 +08:00
Tianyi Cui
e9589b523f docs: fix review findings — support-group summary row, smoke-prose export-shape claims
The packages/README.md group table still described support/ as holding
the stdio UI; and three prose sites credited the keyless smokes with
guarding the app export shape, which a bundle without inject cannot do
(a stray default boots rather than crashes) — the shape is pinned by
the stdio-agent unit suite's explicit unwrapExports assertion; the
smokes prove the composed tree boots.
2026-07-04 15:59:12 +08:00
Tianyi Cui
4036300353 refactor(acp): trim unreachable bridge surface (branding knobs, kind-sniffing fallback)
Two pieces of dsh-acp surface were unreachable from any shipped config:

- AcpConfig.agentName/agentVersion: the app package hands the bridge only
  { model, systemPrompt }, so no leaf cordis.yml could set them; they were
  settable only by direct-mounting the bridge (a unit test). Hardcode
  agentInfo at the initialize site and delete the fields, their schema
  defaults, the ?? fallbacks, and the TODO(double-default) whose subject
  vanishes. The handshake wire value is unchanged (all snapshot initialize
  lines byte-identical).

- The toolKindFor name heuristic special-cased bash*/read*/write/edit*
  names in the generic-fallback path, violating the bridge's own design
  rule ("the bridge never special-cases tool names"). Every first-party
  tool ships its kind via presentCall; the fallback now renders the
  neutral kind 'other'. The fallback is reachable when a presentCall
  throws OR when model args fail the tool schema (defineTool's presentCall
  wrapper returns undefined on violations) — the latter shows up in one
  committed golden (hook-codex-posttool-block: three bash calls missing
  the required description), whose kind cells flip execute->other. That
  3-line golden refresh is the whole transcript delta.

The empty-arguments branch of parseToolArguments lost its only exercise
with the deleted heuristic test; it is live behavior (JSON.parse('')
throws, so the guard is what renders a zero-arg call as rawInput {}), so
it gets a dedicated pin instead of deletion.

RFC moved to docs/rfc/implemented/simplification/ and amended to shipped
reality: fallback reachability includes schema-invalid args, and the
golden churn is exactly the three kind cells (the original zero-churn
claim held only for the branding half).
2026-07-04 15:46:11 +08:00
Tianyi Cui
205f7cd04d refactor(ui): fold the stdio UI helper into the stdio app
The readline UI lives inside @deepseek-ai/dsh-stdio-agent as the
in-package stdio-chat module; the packages/support/ui-stdio package is
gone. The app's front-door cluster always includes this UI and nothing
else composes it, so the boundary bought manifest/tsconfig/module-graph/
README/publint surface for a helper that is not independently
swappable — and a product app no longer depends on a support package
documented as not-product-surface.

createStdioChat, the StdioRuntime test seam, and both unit suites moved
verbatim (imports rewired to the module path); the named
name/inject/Config/apply export shape stays, being the contract the
app's ctx.plugin mount consumes. Coverage stays per-file 100%; the
built-bin smoke under plain node and both keyless Loader-path smokes
prove the published artifact and the demos end-to-end.

Implements docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
(moved from proposed/ and amended to the shipped shape).
2026-07-04 15:38:03 +08:00
Tianyi Cui
7702a33531 docs(AGENTS): rewrite the root standing orders to the 1,500-word budget
Applies the documentation standard to its biggest offender. Every rule
survives as one to three lines plus a link to its durable home; the
stories, duplicate statements, and re-narrations go:

- Situational clusters evict to new homes: docs/testing.md (tiers,
  with-key policy, real-over-mock, world-verification, real-entry-path
  guards), docs/defensive-patterns.md (the bug-class rules), and
  docs/cookbook/responding-to-pr-review-on-a-stack.md (the stacked-PR
  review procedure).
- Doc-authoring rules consolidate into docs/AGENTS.md § Writing rules
  (current-state-never-history, md-wrap, ts-block compilation, @mode,
  catalog same-change, pair same-change).
- packages/README.md drops to the group table + the extension-vs-bundle
  dependency rule; the hand ASCII graph yields to the generated
  module-graph.md; group READMEs are the canonical per-package map.
- packages/AGENTS.md keeps only its packages-specific rules (export
  shape, ctx.get, real-Loader coverage); examples/AGENTS.md repoints
  its with-key-policy link; rfc/README.md loses a narrated-history
  aside; dsh-code-review / dsh-find-simplifications / verify-md-wrap
  references follow the moved content.
- Budget manifest ratchets: AGENTS.md 8200 -> 1500 (now 1,495 words),
  packages/README.md 1900 -> 600, packages/AGENTS.md 600 -> 450; the
  two new eviction docs join the budget set (testing 800, defensive
  550); docs/AGENTS.md raises 1000 -> 1250 for the absorbed writing
  rules (the one justified increase). The doc-tiers RFC's deferred list
  prunes the two items this change ships.
2026-07-04 14:22:47 +08:00
Tianyi Cui
95b9ac0d3e docs: refresh simplification RFC sweep 2026-07-04 11:04:49 +08:00
Tianyi Cui
1546c6060a Merge branch 'worktree-hooks-h-retro' into worktree-hooks-i-snapshots 2026-07-04 03:31:31 +08:00
Tianyi Cui
38cd1a150c Merge branch 'worktree-hooks-e-protocol' into worktree-hooks-f-bridges
# Conflicts:
#	examples/acp-agent/cordis.snapshot.yml
#	examples/acp-agent/cordis.yml
2026-07-04 03:09:37 +08:00
Tianyi Cui
51640362b5 docs: rename coding demo to repl 2026-07-04 01:07:26 +08:00
Tianyi Cui
74f61c89e3 test(hooks): snapshot the CC + Codex hook matrix end-to-end
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.
2026-07-04 00:40:35 +08:00
Tianyi Cui
53b215c646 fix(tool-fs): write always renders a diff card on the completed update
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.
2026-07-03 20:52:36 +08:00
Tianyi Cui
d8fd3225af feat(tool-fs): result-time applied-hunk diffs for write/edit
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.
2026-07-03 17:12:00 +08:00
Tianyi Cui
1a57d67058 refactor(tools): tagged render-intent union for tool-call presentation
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
2026-07-03 02:04:03 +08:00
Tianyi Cui
490fe002a1 test(acp): snapshot the fs-policy rejection card
The fs-policy gate throws FS_NOT_OBSERVED when the model edits a file it
never read; that rejection surfaces as a failed tool_call_update, but no
snapshot pinned it — a regression that dropped or mis-rendered the failed
card would pass every gate. Record a scenario that edits a seeded file
without a preceding read: the edit is vetoed, the file stays unchanged on
disk, and the transcript shows the pending edit card followed by a
status:'failed' update carrying the policy error.
2026-07-02 23:41:02 +08:00
Tianyi Cui
94cbec8162 test(acp): snapshot scenarios for the filesystem tools
Five recorded ACP snapshot scenarios exercising read/write/edit end-to-end
through the real acp-agent subprocess, replayed keyless in CI:

- fs-read — read a seeded file (read tool + presentation + observed-state)
- fs-write — create a file (write, no prior version guard)
- fs-edit — read then literal-replace (read-before-edit authorization)
- fs-write-overwrite — read then rewrite (replaceIfVersion after a read)
- fs-read-window — read lines 5-8 with offset/limit (windowing + the offset
  surfaced as the tool_call location line)

The goldens confirm the tools render with their new presentation — Read/Write/
Edit <path> titles, read/edit kinds, and `locations` (fs-read-window carries
`{path, line:5}`) — and that the prompts steered the model to the fs tools, not
bash (zero bash calls in any golden). Recorded against the real API, filtered to
the new scenarios so no existing fixture churned.
2026-07-02 20:09:09 +08:00
Tianyi Cui
a334395f0c feat(coding-agent): wire the filesystem tools into the demo
Load dsh-fs-local + dsh-fs-policy + dsh-tool-fs after tool-todo (mirroring the
acp-agent wiring), and steer the system prompt to prefer read/write/edit for
file ops with bash for shell/tests/search. Update the welcome line and the
FIXME(config-comments) bash note.

Doc sweep now that both demos ship the fs tools and the seam resolves per-session
cwd: architecture.md and the event-gate RFC no longer say the demos do file ops
through bash / that no config wires the tools; the coding-agent + examples
READMEs and the AGENTS.md layout blurb list the fs tools; the acp-agent README
drops the launch-dir caveat (per-session cwd now works, so the server can launch
anywhere).

(stdio-agent is single-session, so fs-local's cwd = process.cwd() is the
workspace. Keyless boot smoke is blocked locally by an unrelated inotify
watcher-limit ENOSPC that also hits demo:echo; the config parses and the same fs
stack boots green in the acp-agent snapshot tier.)
2026-07-02 19:46:59 +08:00
Tianyi Cui
4fc0d4833c Merge branch 'worktree-hooks-e-protocol' into worktree-hooks-f-bridges 2026-07-02 19:01:13 +08:00