Commit Graph
199 Commits
Author SHA1 Message Date
Tianyi Cui d72ceffd04 Merge branch 'worktree-hooks-d-subagent' into worktree-hooks-e-protocol 2026-07-02 18:43:40 +08:00
Tianyi Cui 8b701605b3 Merge branch 'worktree-hooks-c-interception' into worktree-hooks-d-subagent 2026-07-02 18:42:52 +08:00
Tianyi Cui 3422f8fa5c Merge branch 'worktree-hooks-b-bash-seam' into worktree-hooks-c-interception
# Conflicts:
#	docs/cordis-catalog/events-and-services.md
#	packages/core/agent/src/types.ts
2026-07-02 18:41:55 +08:00
Tianyi Cui abe80cec68 fix(loop): record a durable prompt/blocked for every vetoed prompt
Address review on the interception-seams PR: PromptDecision.reason is
documented as the durable record of why a prompt was blocked, but the loop
only surfaced it via the fully-blocked batch's `rejected` turn/end. In a MIXED
batch — one queued prompt blocked, another allowed — the turn does not end
`rejected`, so the blocked prompt and its reason vanished from the session log
entirely.

Add a `prompt/blocked` SessionEventMap variant (content + source + reason),
appended in the open turn at the veto point in place of the user/message the
prompt would have become. It is a non-surface, turn-enclosed event (like
todo/write): it never reaches deriveMessages(). The fully-blocked batch still
also ends `rejected` for boundary balance + ACP settlement. Regression test
drives a mixed batch and asserts the blocked prompt is recorded while the
allowed one runs — proven red without the append.
2026-07-02 17:03:53 +08:00
Tianyi Cui 40488e29e5 fix(bash-local): keep /dev/null stdin when no bytes supplied
Address review on the bash stdin/env seam PR: the seam spawned stdin as a
`'pipe'` for EVERY call, closing it empty when no stdin was supplied. That is
NOT observationally equivalent to the pre-seam `'ignore'` default — node's
spawn pipe is an AF_UNIX socket, so `test -c /dev/stdin` (and any fd-0 type
probe) flipped for every model-driven bash call, even though the code claimed
the no-stdin path was unchanged.

Spawn stdin as `'pipe'` only when the caller supplies bytes; otherwise
`'ignore'` (fd 0 → /dev/null), the exact prior default. A literal `stdio`
tuple per branch preserves the typed `spawn` overload's non-null stdout/stderr.
Regression test asserts fd 0 is a char device with no stdin and a socket when
supplied — proven red on the always-pipe code.
2026-07-02 16:39:29 +08:00
Tianyi Cui 9c60fa83f9 Merge worktree-hooks-d-subagent into worktree-hooks-e-protocol
Bring the hook-protocol library branch onto the updated stack (master via A→B→C→D).
No review fix on E (#123 converged clean in its own round). The only conflict was
docs/rfc/README.md: kept D's corrected subagent RFC title (agentType dropped)
alongside E's own hook-protocol RFC index row.
2026-07-02 06:11:26 +08:00
Tianyi Cui 84f3019310 refactor(subagent): drop the agentType lifecycle field
Address review: `agentType` was a Claude-Code concept (`subagent_type`) that
does not fit our own subagent seam — nothing in the harness interprets it, and
its only consumer was the CC-dialect hook bridge. Rather than let a foreign
concept sit on the core seam, remove it:

- `SubagentStartRequest`, `SubagentRunInfo`, `SubagentRunEndInfo`: drop the
  `agentType` field; the `subagent/start`/`subagent/end` payloads now carry
  `provider`/`id` (+ end `stopReason`/`lastAssistantMessage`) only.
- `dsh-tool-subagent`: drop `Config.agentType` and its request plumbing.
- Tests: keep the lastAssistantMessage / clone-containment / reject-path
  coverage (rewritten to not assert agentType); delete the two tool-subagent
  tests that only exercised agentType forwarding (dead behavior).
- Docs: retitle + rewrite the subagent-observe-enrich RFC to the one shipped
  enrichment (lastAssistantMessage), with a note on why agentType was dropped;
  update rfc/README index title, both subagent READMEs, and the
  core-data-structures/subagent.md type-equiv block + prose; regenerate catalog.

The CC bridge (PR-F) will feed Claude Code's own default matcher value
"general-purpose" for its SubagentStart/Stop agent_type matcher instead.
2026-07-02 05:52:02 +08:00
Tianyi Cui 8514bddd82 Merge branch 'worktree-hooks-c-interception' into worktree-hooks-d-subagent 2026-07-02 05:18:28 +08:00
Tianyi Cui d3be934a17 docs(events): fix stale turn-mirror / tools-execute prose caught in review
Codex review of the interception-seams merge flagged current-state prose still
describing removed/renamed surfaces:

- packages/core/agent/src/types.ts module doc: the agent/* "transient emits"
  list still ended with "the turn boundaries" — corrected to state turn/step
  boundaries are durable session/event records, not agent/* emits, and to list
  the actual interception seams (prompt-submit/pre-step/request/step-result/
  turn-continuation) + agent/session-start.
- interception-seams RFC: "agent/turn-end fires and the ACP bridge settles" →
  the durable turn/end is appended and ACP settles off it (no turn mirror).
- two proposed RFCs (acp-agent-client-protocol, optional-code-mode) named the
  pre-split `tools/execute` waterfall → the `tools/pre-execute`/`tools/post-execute`
  pair.

Regenerated the cordis catalog (module-doc change). No code/behavior change.
2026-07-02 05:13:16 +08:00
Tianyi Cui 1323366da3 Merge worktree-hooks-b-bash-seam into worktree-hooks-c-interception
Bring the interception-seams branch onto current master (via A→B). The
substantive reconciliation is master's compaction `agent/pre-step` serial seam
meeting C's interception seams:

- types.ts: keep BOTH master's `agent/pre-step` AND C's new interception events
  (`agent/prompt-submit`, `agent/session-start`, `agent/turn-continuation`→
  `ContinuationDecision`); drop the turn-mirror declarations (removed on A).
- loop.ts: the merged per-turn order is `turn/start` → per queued msg
  `agent/prompt-submit` (rewrite/inject/block) → (fully-blocked ⇒ zero-step
  `rejected`) → per step: drain steering → assemble system prompt →
  `agent/pre-step` (compaction, OUTSIDE the step) → `step/start` → single
  `deriveMessages()` → model → tools/pre-execute·dispatch·post-execute. No
  turn-mirror emits; `closeTurn()` is the A-simplified single-call form.
- Docs (architecture, core.md, agent/agent-loop READMEs, catalog) reconciled to
  show C's interception seams alongside `agent/pre-step`, no turn/step mirrors.
- rfc/README: dropped the stale `proposed/` compaction row (master moved that RFC
  to implemented/); kept C's new `pre-tool-input-rewrite` proposed row.
- interception.spec.ts: migrated its two `agent/turn-end` reason collectors to
  the `turn/end` session event, and ADDED a cross-test proving a
  `prompt-submit` rewrite + additionalContext is VISIBLE to an `agent/pre-step`
  listener on the same turn — pinning the merged seam ordering (compaction sees
  the post-prompt-submit surface, not stale history).
2026-07-02 04:51:24 +08:00
Tianyi Cui 5533bb783a docs(bash): purge remaining trusted-plugin wording caught in review
Codex review of the reframe found stale "trusted-plugin surface/boundary"
wording still in review-relevant spots the first pass missed:

- docs/rfc/README.md index title for the RFC.
- packages/bash/bash-local/src/run.ts (childEnv JSDoc + SpawnSpec stdin/env
  JSDoc + the spawn stdin comment) and src/index.ts (resolve carry-through
  comment); run.ts also pointed at a tool-bash README section name that no
  longer exists.
- the two bash-local test descriptors (run.spec.ts / executor.spec.ts).
- the tool-bash guard test's `boundary-*` call ids and one "boundary
  assertion" comment (renamed to `no-forward-*`).

All reworded to the scrub-is-the-control framing (or neutral wording). The RFC
FILENAME keeps `-trusted-plugin-surface` as a stable id (many links point at it;
the index title and content are corrected). No code or behavior change.
2026-07-02 04:30:40 +08:00
Tianyi Cui bb9ae2ba99 docs(bash): reframe stdin/env — the scrub is the security control, not a trust boundary
Address review: the "trusted-plugin surface" framing overstated the security
story. A model driving the `bash` tool already has equivalent power to set env
vars and feed stdin through ordinary shell syntax (`FOO=bar cmd`, heredocs), so
the `env`/`stdin` seam fields grant it no new capability — and they cannot
exfiltrate the harness's ambient credentials, because the credential SCRUB in
dsh-bash-local (which strips *KEY*/*SECRET*/*TOKEN* from process.env before the
child sees it) is the actual control, and it works regardless of these fields
(tool-call args are static JSON, never shell-evaluated).

So drop the "dangerous / trusted-plugin boundary" language across the RFC, the
three bash-package READMEs, the bash/src/types.ts JSDoc, and docs/bash.md (both
the type-equiv blocks — kept 1:1 with source — and the prose). The reality that
remains: the `bash` tool doesn't EXPOSE env/stdin as parameters because they'd
be redundant with shell syntax; the fields exist for in-process plugins (the
hooks bridges) to pass a JSON payload + CLAUDE_* vars cleanly. The guard test is
kept but reframed: it catches a future `...args` spread that would silently
forward model input into the post-scrub env merge, NOT a trust wall. No code or
behavior change.
2026-07-02 04:08:24 +08:00
Tianyi Cui 318a3fd036 Merge branch 'worktree-hooks-a-taxonomy' into worktree-hooks-b-bash-seam 2026-07-02 03:53:08 +08:00
Tianyi Cui 9e575a2a2c docs(events): fix stale turn-mirror references caught in review
Codex review of the turn-mirror removal found current-state docs/comments that
still claimed the removed `agent/turn-start`/`agent/turn-end` events exist:

- docs/architecture.md: the loop diagram's turn-start line still said "emit
  agent/turn-start" (the turn-end line was already fixed).
- event-domain-semantics RFC: the `agent/*` domain description listed "the turn
  boundaries" among the transient emits.
- docs/core-data-structures/core.md: the agent/* taxonomy blurb listed
  "turn/step boundaries" as agent events.
- the proposed ACP RFC: the settle-signal rows named agent/turn-start /
  agent/turn-end; retargeted to the durable `turn/end` session event + the
  session/event owning-turn correlation.
- loop.ts outer-catch comment: said "closeTurn/failTurn are idempotent" — after
  the emit-param removal closeTurn is called exactly once (mutually exclusive
  normal/catch paths), so corrected to state that and to scope idempotency to
  closeStep (which is still guarded by stepOpen).

Regenerated the cordis catalog. No behavior change.
2026-07-02 03:47:37 +08:00
Tianyi Cui 140f818a42 refactor(events): remove the turn boundary mirror events
Complete the boundary-mirror removal begun with the step mirrors: drop
`agent/turn-start` and `agent/turn-end` from the agent event taxonomy. Turn and
step boundaries are now read exclusively off the durable `session/event` feed
(`turn/start`/`turn/end`/`step/start`/`step/end`) — there is no `agent/*` mirror
for any boundary.

- loop.ts: delete both turn emits; `closeTurn` loses its `emit` parameter and
  its now-unreachable idempotency guard (it is called exactly once per turn, on
  mutually exclusive normal/catch paths); `failTurn` loses the dead post-close
  branch that only a throwing turn-end LISTENER could reach.
- ui-stdio: render turn boundaries from `session/event`, recovering the short
  agent label from an `agent/created`→id map (the `turn/start` event carries only
  the turn number, and the session id is not reliably the agent id). ui-stdio is
  a disposable test REPL, so this migration retires the sole justification the
  event-domain-semantics RFC gave for KEEPING the turn mirrors.
- Tests: reason/turn-number collectors and the boundary-ordering test now read
  `session/event`; the throwing-turn-boundary-LISTENER tests are deleted (that
  code path no longer exists). A new test covers the outer-catch disposed branch
  via a pre-step listener that disposes-then-throws (the surviving real path).
- Docs: promote the "remove agent boundary mirror events" RFC to implemented
  (amended/narrowed — `agent/steering` is RETAINED, not a boundary mirror);
  update the event-domain-semantics + turn-enclosure RFCs, architecture.md, the
  cookbook, the ACP/agent/ui-stdio prose, and regenerate the cordis catalog.

`agent/steering` and `agent/stream-chunk` are explicitly out of scope (not
durable-boundary mirrors). ACP is unaffected — it already settles from the log's
`turn/end` + `agent/status`; snapshot goldens are byte-unchanged.
2026-07-02 03:26:45 +08:00
Tianyi Cui bdf390679b Merge origin/master into worktree-hooks-a-taxonomy
Bring the event-taxonomy branch up to date with master's compaction work.
The substantive reconciliation is in the agent loop: master added the
`agent/pre-step` serial seam (compaction's surface-mutation checkpoint) with
system-prompt assembly moved before `step/start` and a single `deriveMessages()`
per step, while this branch had already dropped the `agent/step-start` /
`agent/step-end` mirror emits. Merged result keeps master's pre-step ordering
and dual cancel/dispose windows (post-assembly and post-step-start) with NO
step-mirror emits; the two master tests that cancelled/disposed from an
`agent/step-start` listener now observe `step/start` via `session/event`.

Regenerated the cordis catalog and module graph from source. Gates: typecheck
clean, agent-loop + compact suites green (226 tests).

Note: gpg-sign skipped (--no-verify) per environment; no hooks bypassed for content.
2026-07-02 02:16:07 +08:00
Tianyi Cui 09531a4e1b Merge remote-tracking branch 'origin/worktree-hooks-d-subagent' into worktree-hooks-e-protocol 2026-07-01 15:58:39 +08:00
Tianyi Cui 198ad2ef0f Merge remote-tracking branch 'origin/worktree-hooks-c-interception' into worktree-hooks-d-subagent 2026-07-01 15:52:58 +08:00
Tianyi Cui 3712f67bc6 fix(events): address review — core.md turn-only taxonomy, RFC mechanism names, post-execute content snapshot
- core-data-structures/core.md: the `agent/*` taxonomy said "turn/step
  boundaries", but the step-boundary mirror emits were dropped — `agent/*`
  mirrors only turn boundaries; step boundaries are durable `step/start`/
  `step/end` session events. Narrow the catalog so plugin authors aren't pointed
  at nonexistent `agent/*` step events.
- interception-seams RFC: replace stack-position phrasing ("a later stack PR",
  "the stack's first change", "the PR that makes...") with durable mechanism/RFC
  names (the hook bridge packages, the event-domain-semantics RFC).
- tools/post-execute snapshot: `dispatched.content` was the same array reference
  as `result.content`, so a listener's in-place `push`/`splice` leaked into the
  returned content while a reassignment was masked — the "protect from tampering"
  comment over-claimed. Copy content into a fresh array so the snapshot guards
  the array structure; comment now states it is not deep immutability. Regression
  extended to push a block in-place and assert it does not leak (proven red
  without the copy).
2026-07-01 15:39:08 +08:00
Tianyi Cui 5bdb40ff34 Merge remote-tracking branch 'origin/worktree-hooks-b-bash-seam' into worktree-hooks-c-interception
# Conflicts:
#	docs/architecture.md
#	packages/core/agent-loop/README.md
2026-07-01 15:37:54 +08:00
Tianyi Cui c658f4d155 fix(hooks): address Codex review — tighten codec to the reference schemas, preserve stdout
Codex's PR-E review found three protocol-fidelity blockers + two doc gaps, all
verified against ~/repos/refs:

- (A) Top-level `decision` accepted allow/deny/ask, but both reference schemas
  reserve those for hookSpecificOutput.permissionDecision — the legacy top-level
  decision is approve/block ONLY. Split topLevelDecisionOf (approve/block) from
  permissionDecisionOf (allow/deny/ask), so an out-of-band {"decision":"deny"} is
  now invalid and ignored instead of becoming a real blocking decision.
- (A) hookSpecificOutput was parsed without its hookEventName discriminator.
  HookOutput now surfaces hookEventName so a bridge can discard a block whose
  claimed event doesn't match the firing one (the schemas key the block by event).
- (A) runHook discarded raw stdout. HookOutput now carries `stdout` (trimmed,
  verbatim) so a bridge can reproduce CC's plain-stdout rendering / Codex's
  plain-stdout-as-additionalContext behavior.
- (B) hook/* SessionEventMap variants were only named in prose; added a payload/role
  table to core-data-structures/session.md (a maintained catalog surface).
- (B) Removed PR-stack-position references (PR-F / "future bridge packages") from a
  test comment and the RFC, per the current-state-wording rule.

New codec tests: top-level allow/deny/ask invalid+ignored, hookEventName capture,
raw stdout preserved on plain + JSON + empty stdout. 51 tests, per-file 100%.
2026-07-01 01:12:04 +08:00
Tianyi Cui 65165b5d54 feat(hooks): dsh-hook-protocol — shared Claude Code / Codex hook wire-protocol core
The two hook bridges (dsh-hooks-claude, dsh-hooks-codex) would otherwise duplicate
the bulk of the protocol — Codex deliberately reimplements a SUBSET of the Claude
Code protocol (same hooks.json shape, exit-code/stdout contract, command-hook
model). This library holds the genuinely-identical primitives; each bridge owns
only what differs (per-event stdin payload, env/substitution, decision mapping).

New packages/hooks/ group; hook-protocol is a LIBRARY (no plugin, registers/injects
nothing):
- matcher: matchesMatcher(pattern, query, mode) — the one dialect axis collapsed to
  a mode param (claude = literal-or-regex with pipe alternation; codex = always
  unanchored regex). Match-all on absent/''/'*'; invalid regex matches nothing.
- codec: parseHookOutput(exit, stdout, stderr) → dialect-neutral HookOutput. Exit 0
  → lenient JSON; exit 2 → blocking error (stderr = reason, surfaced as
  decision:'block'); other → non-blocking. Parses the CC superset
  (continue/stopReason/decision/hookSpecificOutput.{permissionDecision,
  additionalContext,updatedInput}/systemMessage); permissionDecision overrides the
  legacy top-level decision.
- runner: runHook(bash, hook, opts, now) — runs a command hook via ctx.bash (stdin
  payload + trusted-plugin env), honors timeoutSec, never throws (executor reject →
  non-blocking-error HookOutput). Injected clock for testable durations.
- merge: mergeHookOutputs — most-restrictive fold (deny>ask>allow, sticky stop,
  block reasons joined, context/system-messages accumulated).
- hook/* session events (declaration-merged into SessionEventMap, log-only like
  compact/*) + appendHookInvoked/appendHookResult helpers.

updatedInput is parsed but NOT honored (deferred pre-tool-input-rewrite RFC); a
bridge logs+warns. 47 unit tests at per-file 100% (matcher per-mode, codec per
exit-code/field, runner plumbing w/ stub executor, merge precedence, hook/*
helpers). RFC: implemented/feature/2026-06-30-hook-protocol-lib.md.
2026-07-01 00:41:53 +08:00
Tianyi Cui 93106b87b4 fix(subagent): deep-clone lastAssistantMessage onto subagent/end (observe-only)
Codex review caught an observe-only violation: the subagent/end emit fires from a
detached `.then` registered BEFORE start() returns — so before the caller's own
`await run.result` continuation runs. Carrying `result.output` by reference let a
mutating subagent/end listener corrupt the SubagentResult.output the caller/tool
then consumes. structuredClone() makes the event a read-only snapshot. Added a
regression test that mutates the event's array and asserts the caller's result is
untouched; proven to fail red without the clone. Updated the RFC + READMEs to note
the clone is load-bearing for the observe-only guarantee.
2026-06-30 21:52:16 +08:00
Tianyi Cui 7cc7b9cf7f feat(subagent): enrich subagent/start + subagent/end lifecycle events (observe-only)
A hooks bridge translating SubagentStart/SubagentStop needs to know WHICH kind of
subagent ran and WHAT it produced — Claude Code's hooks carry subagent_type and the
child's final message. Enrich the existing lifecycle emits to match, observe-only:

- agentType: an optional caller-supplied subagent-kind label (CC's subagent_type),
  added to SubagentStartRequest and carried VERBATIM onto both subagent/start
  (SubagentRunInfo) and subagent/end (SubagentRunEndInfo). The seam never interprets
  it. dsh-tool-subagent threads it from a new optional Config.agentType, so a
  deployment exposing multiple subagent kinds (one tool load per kind) labels each.
- lastAssistantMessage: the child's final output (SubagentResult.output), added to
  SubagentRunEndInfo on the settle path so an observer sees what the subagent
  produced without holding the run. Absent on the reject path (no result produced).

Strictly observe-only: both events stay plain emits (subagent/end fires from a
detached .then and awaits no listener). A control-flow subagent/end (awaited
waterfall returning a decision) would need the emit→waterfall reshape, awaiting
listeners before settling, and a provider resume capability — deferred to the
background/steering redesign (FIXME(subagent-continuation) anchors it). RFC:
implemented/feature/2026-06-30-subagent-observe-enrich.md.
2026-06-30 21:29:08 +08:00
Tianyi Cui 483e0e5edf fix(events): address Codex review — protect post-execute result, purge stale tools/execute refs
Codex's PR-C review found two (A) blockers:

- tools/post-execute could corrupt the protected outcome. postExecute passed the
  mutable `result` to listeners and then read result.callId / spread result on the
  return paths, so a listener mutating the reference (flipping isError, rewriting
  callId, injecting an error) escaped the decision channel. Now the authoritative
  callId/isError/error are SNAPSHOT before the waterfall and the return value is
  rebuilt from the snapshot + the typed PostToolDecision — the decision is the only
  sanctioned way to change the outcome, and callId is always exec.callId. Added a
  regression test that mutates the result reference and asserts it has no effect;
  proven to fail red on the unfixed code.

- Public docs/JSDoc still advertised the removed `tools/execute` waterfall after the
  split. Swept every current-state reference to tools/pre-execute + tools/post-execute:
  the ToolRegistry class JSDoc (and the regenerated catalog), loop.ts's ASCII flow
  (also added the prompt-submit/session-start steps it was missing), the package-map
  READMEs (packages, core, agent-core), core-data-structures core.md/tools.md, the
  bash + acp + invariants src/READMEs (the deferred permission gate is the
  tools/pre-execute deny/ask seam now), the cookbook, and the implemented RFCs whose
  factual seam catalog drifted. codec.ts's totality prose now lists `rejected`.
  Proposed-RFC references are left as-is (frozen proposals, validated when built).
2026-06-30 20:25:30 +08:00
Tianyi Cui dc95a7881d feat(events): interception seams — the typed-Decision surface for hooks
Reshape the agent's interception surface so every seam returns a small, typed
Decision union, and the set covers the hook points a CC/Codex bridge (and a
native plugin) needs. "Native hooks" are not a package — a native hook is just a
cordis plugin on these canonical events; the bridges (a later PR) only translate
an external protocol onto the same surface.

dsh-agent:
- NEW agent/session-start(agent, source) emit (once before turn 1; SessionStartSource
  startup|resume|clear|compact) — a pure notification, seeds context via inject().
- NEW agent/prompt-submit waterfall → PromptDecision (allow, optionally rewriting the
  prompt or attaching additionalContext, or block).
- RESHAPE agent/turn-continuation boolean → ContinuationDecision ({action:'stop'} |
  {action:'continue', reason?}; a continue reason is recorded as next-step steering).
- New HookContext envelope (required source — inject() would mislabel a missing one).

dsh-tools: split the single tools/execute waterfall into tools/pre-execute
(PreToolDecision allow/deny/ask gate) and tools/post-execute (PostToolDecision
accept/block, optionally replacing content or attaching additionalContext). Core
dispatch sits between as plain code; the tool body keeps its inner try/catch so a
thrown tool still reaches post-execute as an isError. ToolExecutionResult gains
additionalContext (ferried to the loop's per-step buffer). Input rewrite is
deliberately NOT offered (a proposed RFC designs it consistently).

dsh-session: new `rejected` TurnEndReason — a turn whose whole prompt batch was
blocked by prompt-submit.

agent-loop firing points: session-start emitted at create (source threaded —
startup for create/fork, resume for resume()); prompt-submit per drained message
with the always-open-turn rule (a fully-blocked batch is a zero-step rejected
turn); the continuation reshape; post-tool additionalContext buffered and appended
after all tool/results (adjacency). ACP codec maps rejected→cancelled.

A worked native-plugin example (interception.spec.ts) proves all four seams compose
end-to-end through the real loop with NO hook/* events (those belong to the bridge
lib). All existing tools/execute + turn-continuation tests migrated. The
tool-subagent abort test now aborts after a microtask so it still exercises the
live onAbort bridge (execute() awaits pre-execute before the body runs).

RFCs: implemented/feature/2026-06-30-interception-seams.md (the reshape) +
proposed/feature/2026-06-30-pre-tool-input-rewrite.md (the deferred rewrite design).
2026-06-30 17:11:18 +08:00
Tianyi Cui b44066d544 docs(bash): address Codex review — RFC states current truth, fix doc reference
- The implemented RFC named stack positions (PR-A…PR-F, "the hooks bridges (PR-F)")
  as shipped reality, violating the rule that an implemented RFC describes current
  truth and docs never name a change unit the reader cannot see. Rephrased to
  describe the hooks subsystem / a hook bridge as the standing motivating consumer,
  without PR/stack references. The decision and rationale are unchanged.
- childEnv's comment pointed at dsh-tool-bash's "module doc" for the trusted-plugin
  boundary, but that explanation lives in the package README (§ "Trusted-plugin
  boundary"), not the module JSDoc. Fixed the reference.
2026-06-30 14:17:19 +08:00
Tianyi Cui 13c6e847a2 feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:

- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
  optionals on the resolved spec (not required-but-nullable like `owner`): a
  missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
  credential scrub, so a trusted caller's explicit entry wins even on a
  credential-shaped name — the scrub guards the harness's OWN ambient creds from
  model-driven commands, not a trusted plugin. stdin is always a pipe, closed
  immediately (with bytes when supplied, empty otherwise — EOF as before); an
  EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
  request is command/workdir/timeoutMs/signal/owner only). A regression guard
  drives the real tool with adversarial args and asserts the request carries
  neither field — proven to go red if the consumer ever forwards them.

Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
Tianyi Cui a821dcbe0d fix(events): address Codex confirmation review — strengthen /goal guard, fix doc drift
Second-round Codex review of the PR-A taxonomy change found four issues, all
verified against the code:

- The /goal regression guard asserted only that the steered content reached
  requests[1], which passes even with the hasSteering override (loop.ts) disabled:
  leftover steering is re-enqueued as a next-turn queued message and also lands in
  requests[1], one turn later. The guard now asserts the same-turn shape — ONE
  turn, TWO steps, a steering/message recorded before step 2 — which is the
  mechanism the override drives. Proven to fail red with the override disabled.
- The event-domain-semantics RFC's consequence list still described the pre-fix
  behavior (step marked open AFTER step/start, so no step/end owed). It now states
  the shipped behavior: the loop marks the step open BEFORE the append, so a
  throwing step/start listener gets a balancing step/end via closeStep().
- architecture.md's loop pseudocode said only continuation listeners force
  continuation; step/end session-event listeners (the /goal pattern) do too.
- The agent/turn-end JSDoc listed a `rejected` TurnEndReason that does not exist on
  this branch (it belongs to the later interception work). Removed it and
  regenerated the cordis catalog; `interrupted` (a real variant) stays.
2026-06-30 12:46:59 +08:00
Tianyi Cui 05b75abbca refactor(events): document event-domain semantics, drop step-boundary mirror emits
Pin the three-domain rule (session = durable fact log, agent = live runtime
surface, tools = registry/exec): a durable replayable fact is a SessionEvent; a
live interception or transient/live-object signal is an agent/tools Cordis
event. A boundary that is both is mirrored as an agent/* emit ONLY where a live
consumer needs the Agent handle.

Apply it to the boundary twins: drop agent/step-start and agent/step-end (no
production consumer needs the live Agent at a step boundary — consumers read the
durable step/start/step/end session events). Keep agent/turn-start/turn-end (the
stdio UI labels output by agent.id). Tests that observed step boundaries via the
removed emits now observe the durable session events; the pinned behavior is
unchanged.

Conservative subset of the proposed "remove boundary mirror events"
simplification; foundation for the Hooks subsystem's canonical event surface.
2026-06-30 10:32:55 +08:00
Hypatia May 6ae1e229fd docs(cordis): clarify serial bail semantics 2026-06-30 09:40:51 +08:00
Hypatia May 0c4059fc84 Merge remote-tracking branch 'origin/master' into compact-basic-refactor
# Conflicts:
#	docs/cordis-catalog/events-and-services.md
#	examples/AGENTS.md
#	examples/coding-agent/cordis.yml
#	examples/coding-agent/tests/harness.ts
#	scripts/gen-cordis-catalog.ts
2026-06-30 09:13:15 +08:00
Tianyi Cui 9f1caf7c5b fix(todo): address todo_write review feedback 2026-06-29 19:44:38 +08:00
Hypatia May 1808570933 fix(compact): harden summarization convergence
Use maxTokens as the provider generation cap and remove the confusing stored-summary max config.

Strip reasoning blocks before storing compaction summaries, reject non-shrinking summaries, and retry bounded re-compaction when the surface remains over threshold.

Add config validation for numeric and type-shaped knobs plus unit and real-API e2e coverage for reasoning-capable summarization.
2026-06-29 16:56:44 +08:00
Hypatia May 1f35a4446d fix(compact): address PR 110 review findings
Honor cancellation and disposal around async pre-step setup before the loop can open a step or call the model.

Route compaction summarization through agent/request so router agents can select the model, and remove the stale model argument from agent/pre-step.

Document serial events and the approximate convergence bound, regenerate the Cordis catalog, and add regression coverage for router compaction, HMR cleanup, and assembly/pre-step interruption.
2026-06-29 16:02:27 +08:00
Tianyi Cui 46e31d8481 feat(tool-todo): add the model-facing todo_write tool
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).
2026-06-29 10:30:52 +08:00
Hypatia May b13586ff8e docs(compact): align seam docs with the pre-step seam and record session/invariants changes (CBR-003)
Codex round 1 CBR-003: several docs still described compaction as an
`agent/request` waterfall concern, and the implemented compaction RFC
claimed "No changes to dsh-session or dsh-invariants" while the diff
changed both.

- Package READMEs / JSDoc (agent, agent-loop, system-prompt, compact,
  compact-basic): compaction now lives on the serial `agent/pre-step`
  seam (fired after turn/start, before step/start); the structural guard
  is tool-pairing balance (`isToolPairingBalanced`), not step-alignment;
  the convergence bound is strict (`>=` rejects).
- architecture.md / core-data-structures/compaction.md: same seam +
  predicate + dispatch-mode updates; regenerated cordis catalog.
- Implemented compaction RFC, updated in place to describe shipped
  reality: the seam is `agent/pre-step` (@mode serial) fired before
  step/start; alignment is surface tool-pairing balance; the convergence
  invariant rejects `>=`; and the "no dsh-session/dsh-invariants changes"
  claim is corrected — dsh-session gains the tool-pairing predicate and
  dsh-invariants drops its `start <= end` replace assertion (a positional
  replace makes start > end normal).
2026-06-26 13:51:45 +08:00
Hypatia May cec32faa4e refactor(compact): turn-agnostic retention + dedicated agent/pre-request seam
Reform the compaction blueprint so a runaway turn survives and the design
stops drifting across review rounds:

- Drop in-flight-turn protection ("layer 2"). Retention is a uniform tail→head
  whole-unit walk; the only structural guard is step-alignment. A single turn
  that alone exceeds the window now compacts its own early closed steps instead
  of being retained verbatim (the failure mode that motivated this).
- Move auto-compaction off the agent/request waterfall onto a new awaited
  agent/pre-request loop seam, fired before history derivation. Compaction
  mutates the surface; the loop derives once from the result — no double-derive,
  and a listener structurally cannot act on not-yet-derived messages.
- Tighten compactIfNeeded to required (session, system, model, signal).
- Enforce a single-pass convergence invariant in resolveConfig: reject configs
  where summarizationMaxTokens + retainTokens exceeds the threshold, so a
  compaction can never immediately re-trigger.
- Document the crash vs recoverable failure taxonomy; core session repair stays
  compaction-agnostic (a log-only orphaned compact/start is inert).
- Wire dsh-compact-basic into examples/coding-agent and add a with-key
  compaction e2e (compaction's first real-world exercise + runaway net).
- Rewrite the RFC to encode the blueprint and move it to implemented/.

The runaway-turn snapshot is a named deferred follow-up: dsh-llm-replay cannot
yet serve the interleaved summarization model call.
2026-06-26 08:59:33 +08:00
Hypatia May cf70141486 Merge branch 'session-surface' into compact-interface
# Conflicts:
#	docs/core-data-structures/core.md
#	packages/README.md
#	scripts/type-equiv.manifest.json
#	tsconfig.base.json
#	tsconfig.typecheck.json
2026-06-25 09:10:50 +08:00
Hypatia May 828c3f85c9 fix review findings: skip collided SCHEMA_VERSION 3; reject marker-less surface events
P1: both merge parents shipped SCHEMA_VERSION=3 for different layouts (surface
columns vs seed_length), so an on-disk 3 was ambiguous and wrongly accepted.
Bump to 4 (merged layout) so the version check rejects both sibling v3s.

P2: a surface-eligible event with no surfaceOp lands in the log but vanishes
from deriveMessages() (surface is the sole derivation path). The typed append
overload enforces the marker only when the type arg is a literal; it collapses
to optional when widened to the union (a caller iterating raw events). Guard at
runtime in both append() and the seed constructor — no backward-compat for
surface-less logs. Shared seed fixtures carry surfaceOp explicitly and the
appendLog helper forwards it verbatim (no synthesized default). Exports
isSurfaceEligibleType. Regression tests for all three, each verified to fail
on the unfixed code.

Gates: typecheck, test (1115), snapshot (14), doc-sync, lint, build, hygiene green.
2026-06-24 17:45:48 +08:00
Hypatia May 9cc8dc371e Merge remote-tracking branch 'origin/master' into session-surface
Reconciles the session-surface work (surfaceOp/sourceEventSeqs provenance as
the sole derivation path) with master's worktree-subagent series (fork-seed
boundary + out-of-process subagent backends).

Semantic reconciliations beyond the textual auto-merge:
- SQLite SCHEMA_VERSION: both sides bumped 2->3. Merged to a single v3 carrying
  BOTH column families — master's seed_length on `sessions` and surface's
  source_event_seqs/surface_op on `events`. writeRow + both INSERT sites bind
  the full set; the schema doc lists all three added columns as the v2->v3 gap.
- agent-loop runStep request: master's `sessionId: session.id` and surface's
  per-append surfaceOp/sourceEventSeqs coexist (different regions).
- Fork seed + surface: a fork seeds the child from the parent's LIVE events,
  which now carry surfaceOp, so the child's surface rebuilds correctly. Verified
  end-to-end — the subagent-fork replay recalls the inherited "SAFFRON" codeword
  through the seeded prefix.
- Subagent snapshot fixtures (recorded pre-surface) re-enriched via KEYLESS
  deterministic replay: only surfaceOp/sourceEventSeqs added onto existing
  recorded lines (matched by seq), no recorded value changed. Not re-recorded
  against the live API.

Gates: typecheck, test (1112), test:snapshot (14), doc-sync, lint, build,
hygiene all green.
2026-06-24 10:48:01 +08:00
Hypatia May 442f85469e refactor(compact): align compact/summary and CompactionResult on shadowed* naming 2026-06-23 16:51:20 +08:00
Hypatia May bb013e934e docs(compact): bracket the surface mutation inside the compaction lock 2026-06-23 16:13:55 +08:00
Hypatia May bdaf9f651d Merge branch 'session-surface' into compact-interface 2026-06-23 13:59:04 +08:00
Hypatia May 358ae02c56 feat(invariants): enforce replace provenance — sourceEventSeqs must cover every shadowed surface node 2026-06-23 13:37:13 +08:00
Hypatia May 6089e226bc refactor(session): make surface the sole derivation path, drop legacy fallback 2026-06-23 13:05:59 +08:00
Hypatia May 09d497d5c5 docs(rfc): clarify compaction rides the replace op on an existing event type 2026-06-23 09:54:58 +08:00
Tianyi Cui 78d60366ec Record fork and mixed spawn+fork snapshot scenarios
The seed-boundary change made fork-child replay route correctly but shipped
with no recorded fork scenario — the seedLength slice was exercised only by
llm-replay unit tests and a persistence round-trip, never by the full-transcript
snapshot tier. Add two recorded scenarios that drive a real fork child through
it:

- subagent-fork: parent completes a turn, then forks one child (child fixture
  carries a non-zero seedLength, the boundary the replay slice consumes).
- subagent-mixed: parent completes a turn, then delegates once via spawn
  (seedLength 0) and once via fork (non-zero seedLength) in one transcript —
  the first scenario to drive two subagent backends at once, exercising both
  branches of the slice.

Both need a completed turn-1 so the fork seed is a non-empty completed-turn
prefix (a turn-1 fork seeds empty = spawn, which would not exercise the slice).
Removing the slice turns both scenarios red (the fork child receives the
parent's recorded chunks), proving the guard bites.

ACP (out-of-process) subagent replay remains a different shape, still tracked
as TODO(acp-subagent-replay).
2026-06-22 21:20:54 +08:00
Tianyi Cui b3d40d427e Persist the seed boundary so fork-child replay routes correctly
A fork subagent seeds its child session with a prefix of the parent's log, and
that seed becomes the child's persisted log — so a fork child's .jsonl begins
with the PARENT's events, including the parent's assistant/chunk events. The
snapshot replay harness derived a child's script from its whole log, which would
replay the parent's recorded responses as the child's model calls. Spawn-only
scenarios never hit it, but a fork snapshot would mis-route silently.

Record the seed boundary and skip the inherited prefix at replay:

- SessionHeader gains an optional `seedLength` (how many leading events were
  inherited via a seed), threaded through CreateSessionOptions/CreateAgentOptions
  meta and stamped by the fork backend (= seeded-prefix length; absent for spawn).
  It is EXPLICIT, never inferred from seed.length: a resume seeds the whole stored
  log, so the resume path passes the persisted boundary back.
- Both persistence backends round-trip it: JSONL header line, SQLite seed_length
  column. The SQLite table change bumps SCHEMA_VERSION 2->3; per the pre-release
  stance the backend rejects an older user_version on open with NO migration.
- llm-replay's parseSessionHeader reads seedLength and loadSessionScripts derives
  a child script from events AFTER the boundary. seedLength is 0 for spawn, so
  spawn replay is byte-for-byte unchanged.

Closes the routing-correctness gap the per-session snapshot replay RFC under-
stated; a recorded fork scenario remains a future addition but now derives
correctly. RFC: docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md.

Regression coverage: a fork child fixture whose seeded prefix carries a parent
chunk (derived script must exclude it, proven red without the slice); a seedLength
persistence round-trip through the shared coordinator contract (both backends);
the fork backend stamping it; resume preserving it from the persisted header.
2026-06-22 20:55:32 +08:00
Hypatia May e45053f0f5 feat(compact): compaction capability seam — abstract CompactService interface
Adds the @deepseek-ai/dsh-compact interface package: the abstract
CompactService (ctx.compact) with compactIfNeeded / compactRegion, the
compact/* session-event types via SessionEventMap declaration merging, and the
capability-seam RFC. Wires the package into the three root tsconfigs and the
cordis catalog. A backend implementation lands separately.
2026-06-22 15:15:27 +08:00