Commit Graph
6028 Commits
Author SHA1 Message Date
Tianyi Cui b82c310db3 Fix subagent in-process result scoping (Codex review round 1)
Two merge-blocking bugs in the shared in-process run driver, both rooted in
`readResult` scanning the whole child session and deriving the stop reason only
from `turn/end`:

- A pre-turn `cancel()` cleared the queued prompt before any `turn/end` was
  logged, so the run settled `error` instead of `aborted`, violating the
  `SubagentRun.cancel()` contract. The driver now tracks that a cancel was
  requested and maps the no-turn case to `aborted`.
- A fork child whose own turn produced no `assistant/message` returned the
  SEEDED parent's last message as a `completed` success. `readResult` now scopes
  to the child's OWN events (after the seed prefix), so a message-less child
  yields empty output.

Both fixes carry a regression test proven to go red on the pre-fix driver.

Also: correct the `SubagentRun.id` / event-payload docs (it is the child AGENT
id, not a session id — the backend mints distinct tokens); refresh the stale
`coding-agent` welcome string (subagent is now a tool); and replace the stale
`TODO(sub-agents)` "deferred" prose in the Agent interface, core.md, and
architecture.md with an accurate pointer to the realized seam.
2026-06-22 06:47:20 +08:00
Tianyi Cui 07f4047ff0 Use explicit ts specifiers for declarations
Restore explicit .ts relative specifiers in source and enable rewriteRelativeImportExtensions so emitted JS uses .js while declarations keep explicit .ts specifiers. Add a NodeNext declaration-consumer gate to prevent extensionless declaration regressions.
2026-06-22 06:11:00 +08:00
Tianyi Cui 7aabd2a3df Add in-process subagent backends: spawn (fresh) and fork (seeded)
The second PR of the subagent seam: the two in-process backends that run a
child agent on the same cordis context, reusing the agent factory's quiescent
AgentHandle teardown. Both register on ctx.subagents (PR1's named-provider
registry) and share one run driver.

- dsh-subagent-spawn: a FRESH child via ctx.agents.create — own session, the
  parent's model by default (overridable), zero inherited conversation. Also
  exports the shared in-process run driver (startInProcessRun): mint ids, stamp
  cwd/parentSession-lineage/depth, drive the one-shot (send → whenIdle), read
  the last assistant/message + turn/end reason, dispose to quiescence.
- dsh-subagent-fork: a child SEEDED with the parent's balanced completed-turn
  prefix (the log up to and including its last turn/end), so the child inherits
  context. The in-flight unbalanced turn is excluded — a raw seed would fail the
  invariants replay. Proven: a regression test goes red if the boundary seeds
  the open turn.
- Seam extension: CreateAgentOptions.seed, threaded through AgentLoop.createAgent
  → ctx.sessions.prepare({ seed }) (the primitive resume already used). This is
  the fork-lineage path the TODO(sub-agents) markers anticipated.
- Depth: a merge-extensible AgentOptions.subagentDepth (0 top-level, parent+1 for
  a child); the depthLimit capability refuses a spawn past request.maxDepth.

Tests: real-loop unit tests for both backends (mock MODEL only, real loop +
invariants), a multi-subagent test (one parent drives a fork AND a spawn child
then keeps working), and a with-key e2e (a real parent delegates via the
`subagent` tool to a real child that writes a file on disk — world-verified).
100% per-file coverage. The coding-agent demo wires the spawn backend + tool.

Snapshot coverage of nested agents is deferred to a stacked follow-up
(TODO(subagent-snapshots)): dsh-llm-replay is a single global positional cursor
that cannot route calls to a parent vs. a child on one context. Recorded in the
RFC's deferrals and a new AGENTS.md rule: designing a subsystem must design its
test infrastructure END TO END up front, verifying the snapshot/e2e harness can
express the new shape — a gap this plan hit.
2026-06-22 05:58:40 +08:00
imccyu 94e7355449 docs: update packages hierarchy to current rfc 2026-06-22 01:38:44 +08:00
imccyu 732e121ff6 fix: make constraints, lint and md-links happy 2026-06-22 01:38:44 +08:00
imccyu 88b75181ad fix: apply ts-build-config adjustment to new packages 2026-06-22 01:38:39 +08:00
imccyu 74cdd9a2e5 Merge remote-tracking branch 'origin/master' into feat/adr0016-type-build-check 2026-06-22 00:35:51 +08:00
imccyu fd55d20548 revert: remove the non-branch changes introduced during the rebase 2026-06-21 23:57:00 +08:00
Tianyi Cui 861791d2d8 Contain subagent lifecycle listeners per-listener, not per-emit
A single try/catch around ctx.emit prevented a thrown subagent/start or
subagent/end listener from propagating, but cordis emit dispatches listeners in
a `.map(cb => cb())` that HALTS on the first throw — so a bad subscriber still
starved the listeners registered after it, violating the AGENTS.md
callback-boundary rule ("one bad subscriber must not starve the listeners after
it"). Resolve the listener callbacks via ctx.events.dispatch and contain each
call individually, the same per-listener guarantee BashExecutor.notifyTaskDone
gives its own listener set.

The two containment tests now register TWO listeners where the first throws and
assert the second still observes the event (start) and the settle (end) — a
regression that fails on the per-emit code (verified: reverted, watched both go
red, restored).
2026-06-21 23:44:07 +08:00
Tianyi Cui 25eccdaedc Fix review findings: lifecycle containment, configurable tool name, coverage, type catalog
Address four findings from the first Codex review round:

- Contain subagent/start|end listener throws (emitContainedStart/End): a
  thrown lifecycle listener could escape SubagentService.start() before the
  caller received the live run to dispose it (a leaked child), and a thrown
  subagent/end listener could surface as an unhandled rejection on the detached
  result-settle hook. Both emits now log-and-contain, mirroring the agent
  registry's agent/created|disposed containment.
- Make the model-facing tool name configurable (Config.toolName, default
  subagent). The docs say to load dsh-tool-subagent once per provider to expose
  multiple transports, but the hardcoded name made the second load throw a
  duplicate-tool-name error; a distinct toolName per load is now required and
  documented.
- Reach the per-file 100% coverage gate: tests for the subagent/end error
  branch, lifecycle-listener containment, every stopReasonError arm + the
  merge-extensible default, the multi-provider toolName path, agentOptions
  forwarding, and the direct-apply schema-bypass fallbacks.
- Document the seam vocabulary in docs/core-data-structures/subagent.md with
  verbatim type-equiv blocks + manifest entries, and link it from core.md (a
  brand-new core/seam type the doc-sync gate cannot detect on its own).
2026-06-21 23:15:43 +08:00
Tianyi Cui 1a81f2cccd Add subagent capability seam: interface, mock backend, model-facing tool
Introduce the `packages/subagent/` group and the abstract subagent seam — an
agent delegating to a child agent — as a named-provider registry (`ctx.subagents`),
unlike the single-implementation bash seam, so multiple transports (in-process,
ACP, future A2A) coexist. This first PR lands the interface, a scripted test
backend, and the model-facing tool, validated through the real cordis load path.

- dsh-subagent: SubagentService registry + SubagentProvider/SubagentRun
  vocabulary + subagent/start|end events. Start-time capabilities (outputSchema,
  depthLimit, toolFilter) are checked pre-start and rejected loud; runtime
  capabilities (sendMessage, resume) are optional methods on SubagentRun.
- dsh-subagent-mock (support): scripted provider for keyless, deterministic
  tests through the real Loader/export path.
- dsh-tool-subagent: the model-facing `subagent` tool, config-bound to one
  provider; synchronous collect with try/finally dispose, signal->cancel
  bridging, and non-completed-stop-reason -> isError mapping.
- Proposed RFC documenting the seam, the fork-vs-spawn-as-separate-backends
  decision, own-session isolation, synchronous-collect scope, and the deferral
  of background/poll/spill to a future unification with bash.
- Wire the new group into tsconfigs, build refs, package hierarchy docs, the
  module graph, and the cordis catalog.

RFC: docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md
2026-06-21 22:31:56 +08:00
Tianyi Cui 6b4dc48fbd Merge pull request #89 from deepseek-ai/worktree-lessons-learned
docs(agents): stacked-PR orchestration + sharper real-entry-path lessons
2026-06-21 20:38:31 +08:00
Tianyi Cui a20e3b70b9 Merge remote-tracking branch 'origin/master' into worktree-lessons-learned 2026-06-21 20:37:26 +08:00
Tianyi Cui 5d3e7dd590 Merge pull request #88 from deepseek-ai/worktree-simplify-extract-examples
refactor(examples): extract the app spine into dsh-agent-core + app packages
2026-06-21 20:37:01 +08:00
Tianyi Cui 0b75598c91 Merge remote-tracking branch 'origin/master' into worktree-simplify-extract-examples 2026-06-21 20:36:05 +08:00
Tianyi Cui 7373941493 Merge pull request #87 from deepseek-ai/worktree-simplify-snapshot-goldens
test(snapshot): use session.jsonl as the only session-log artifact
2026-06-21 20:35:41 +08:00
Tianyi Cui c85713d11e Merge remote-tracking branch 'origin/master' into worktree-simplify-snapshot-goldens 2026-06-21 20:34:55 +08:00
Tianyi Cui e770e22486 Merge pull request #86 from deepseek-ai/worktree-simplify-trace-events
simplify(session): fold trace-only usage/error events into load-bearing events
2026-06-21 20:34:41 +08:00
Tianyi Cui 5d7af3be82 Merge remote-tracking branch 'origin/master' into worktree-simplify-trace-events 2026-06-21 20:33:53 +08:00
Tianyi Cui 9f13dd02f6 Merge pull request #85 from deepseek-ai/worktree-simplify-agent-stop
simplify(agent): drop unused public Agent.abort(), keep whenIdle()
2026-06-21 20:33:27 +08:00
Tianyi Cui 4ead67c06d Merge remote-tracking branch 'origin/master' into worktree-simplify-agent-stop 2026-06-21 20:32:21 +08:00
Tianyi Cui 8f50008d8b Merge pull request #84 from deepseek-ai/worktree-simplify-branded-ids
feat(types): brand the bash ids and stop brand erosion
2026-06-21 20:32:05 +08:00
Tianyi Cui 74344a8bd4 Merge remote-tracking branch 'origin/master' into worktree-simplify-branded-ids 2026-06-21 20:30:41 +08:00
Tianyi Cui 2d4c367e51 Merge pull request #83 from deepseek-ai/worktree-simplify-prune-seam
simplify(seams): prune dead methods from the persistence seam
2026-06-21 20:30:14 +08:00
Tianyi Cui fa5cf0afd7 Merge remote-tracking branch 'origin/master' into worktree-simplify-prune-seam 2026-06-21 20:27:19 +08:00
Tianyi Cui 89d9a6679b Merge pull request #82 from deepseek-ai/worktree-simplify-llm-surface
simplify(llm): drop unconsumed adapter-change event and assembled call surfaces
2026-06-21 20:27:04 +08:00
Tianyi Cui df4f8a4dea Merge remote-tracking branch 'origin/worktree-simplify-extract-examples' into worktree-lessons-learned 2026-06-21 20:22:47 +08:00
Tianyi Cui 6cb8f35adb Merge remote-tracking branch 'origin/worktree-simplify-snapshot-goldens' into worktree-simplify-extract-examples
# Conflicts:
#	AGENTS.md
2026-06-21 20:21:37 +08:00
Tianyi Cui 56a2351e08 Merge remote-tracking branch 'origin/worktree-simplify-trace-events' into worktree-simplify-snapshot-goldens 2026-06-21 20:19:16 +08:00
Tianyi Cui fd76791c6b Merge remote-tracking branch 'origin/worktree-simplify-agent-stop' into worktree-simplify-trace-events 2026-06-21 20:18:15 +08:00
Tianyi Cui a6a5892e8a Merge remote-tracking branch 'origin/worktree-simplify-branded-ids' into worktree-simplify-agent-stop
# Conflicts:
#	docs/rfc/README.md
2026-06-21 20:17:29 +08:00
Tianyi Cui 4661905714 Merge remote-tracking branch 'origin/worktree-simplify-prune-seam' into worktree-simplify-branded-ids
# Conflicts:
#	docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
2026-06-21 20:15:21 +08:00
Tianyi Cui 5c0597e9de Merge branch 'worktree-simplify-extract-examples' into worktree-lessons-learned 2026-06-21 19:48:37 +08:00
Tianyi Cui 4d7726ecd3 fix review findings: don't treat disabled entries as load failures; scope the verify-package-paths lib skip to a real package root
assertEntriesLoaded() flagged ANY fiber-less entry as a failed import, but a
`disabled: true` entry settles without a fiber by design (Entry.refresh() skips
init() when disabled) — a valid "plugin off" config, not a broken import. Both
app bins now filter `fiber === undefined && !entry.disabled`. The stdio built-bin
smoke gains a disabled-(unresolvable)-entry config that must still boot.

The verify-package-paths lib-skip was unconditional and ran before the
moved-package check, so a stale group-less `packages/acp-agent/lib/bin.js` (the
exact drift this gate catches) was silently ignored just for containing `lib`.
Scope the skip: only exempt `lib` when it is the segment after an EXISTING
`packages/<group>/<pkg>` root, so a real-but-unbuilt `lib/bin.js` is still exempt
while a stale package path flags.
2026-06-21 19:47:22 +08:00
Tianyi Cui 64976df216 Merge branch 'worktree-simplify-extract-examples' into worktree-lessons-learned 2026-06-21 18:38:56 +08:00
Tianyi Cui 9f5e2b352c Merge branch 'worktree-simplify-snapshot-goldens' into worktree-simplify-extract-examples 2026-06-21 18:38:50 +08:00
Tianyi Cui ebe8c8e21f Merge branch 'worktree-simplify-trace-events' into worktree-simplify-snapshot-goldens 2026-06-21 18:38:40 +08:00
Tianyi Cui f09a88ecee Merge branch 'worktree-simplify-agent-stop' into worktree-simplify-trace-events 2026-06-21 18:38:29 +08:00
Tianyi Cui 2903f96548 fix review findings: RFC says session.jsonl is required for every snapshot scenario
The required-fixture-guard description still said session.jsonl was needed only
for model scenarios, but the harness passes <dir>/session.jsonl to llm-replay
unconditionally, so loadReplayScript() fails for a no-model scenario without it.
The code already requires it for all scenarios; align the RFC prose.
2026-06-21 18:36:13 +08:00
Tianyi Cui d0e1b02a7f fix review findings: correct whenIdle live-consumer claim in the stop-surface RFC
The retained-whenIdle paragraph claimed "live consumers (the ACP bridge's settle
points)", but `packages/ui/acp/src` has no whenIdle() call — the bridge owns its
agents and tears them down via AgentHandle.dispose(). whenIdle()'s live consumers
are ACP and agent TESTS awaiting settlement through the public seam. State that.
2026-06-21 18:36:04 +08:00
Tianyi Cui b7d018580e fix review findings: skip lib/ build-output refs in verify-package-paths; resolve acp built-bin npm deps from the declaring package
verify-package-paths flagged the new built-bin smokes' `lib/bin.js` citations
as stale-source drift, failing CI: doc-sync runs BEFORE build, so the build
output is absent at lint time. The gate targets moved SOURCE paths, so skip any
reference whose target goes through a `lib/` segment — mirroring how the file
scan already excludes `lib/`.

The acp built-bin smoke resolved `zod`/`@agentclientprotocol/sdk` via
`import.meta.resolve` from the test file's own context, but `acp-agent` does not
declare them — `dsh-acp` does. Under pnpm's strict layout they are not exposed
where the test resolves, so the new CI built-bin step failed with
"Cannot find package 'zod'". Resolve each from the `ui/acp` package URL (the one
that declares it) instead.
2026-06-21 18:35:46 +08:00
Tianyi Cui a96b1e3ae7 docs(agents): capture stacked-PR orchestration + sharper real-entry-path lessons
Three hard-won lessons from running a 7-PR simplification stack through two
waves of review feedback:

- New "## Orchestrating review feedback across a stacked PR chain" section: one
  worktree per branch; a fix belongs on the PR that introduced the issue then
  flows DOWN; review-fixes are separate commits never amends; delegated work is
  trust-but-verify (prove a regression guard FAILS on unfixed code); reply
  in-thread on the merits.
- "## Conventions" gains a "Never rewrite a pushed branch" rule next to the
  merge-commit rule: update a child by merging the parent down, never
  rebase/amend/force-push a pushed branch; a fix lands on its originating PR.
- Extended the "Line coverage is not behavior coverage" defensive-patterns
  bullet with two corollaries this stack re-taught: (1) a real-load-path test
  only GUARDS the export shape if a broken shape actually FAILS it — an
  inject-less composition plugin boots fine on a stray `export default`, so it
  needs an explicit no-default + unwrapExports assertion; (2) "real entry path"
  means the PUBLISHED artifact (built lib/bin.js under plain node), not the dev
  runtime (tsx), which masks boot settle-races, module-resolution differences,
  and a load failure that loader.await()'s Promise.allSettled swallows.
2026-06-21 17:17:00 +08:00
Tianyi Cui e63641bcfd Merge branch 'worktree-simplify-snapshot-goldens' into worktree-simplify-extract-examples 2026-06-21 15:48:33 +08:00
Tianyi Cui e04ca55374 fix review findings: require session.jsonl for every snapshot scenario
The per-kind fixture guard claimed no-model scenarios need no session.jsonl, but
the replay path requires one for ALL scenarios: runScenario() passes
`fixtureFile: <dir>/session.jsonl` unconditionally and llm-replay's
loadReplayScript() throws "fixture not found" when it is absent and no override
replaces it. A no-model scenario ships a header-only session.jsonl that derives
to an empty script. The guard + its comment now match that reality, so a future
no-model scenario following them won't fail at subprocess startup.
2026-06-21 15:46:38 +08:00
Tianyi Cui 3567808171 fix review findings: harden the app bins + built-bin smokes, arch-exception doc, snapshot fixture-guard
BLOCKER — the published lib/bin.js (stdio + acp) was exercised only via tsx
(demo:* / the src/bin.ts smokes); the built artifact under plain `node` was
unguarded. Root-cause on the BUILT bin:
  1. Settle race: boot() returned once loader.create() registered the include
     ENTRY, but the include loads its child plugins asynchronously — so boot()
     (and main()) resolved while the app plugins (stdin reader, agent loop, ACP
     bridge) were still mounting. A CLI with no attached handles yet exits 0
     silently, and a load error surfaces as an unhandled rejection AFTER boot.
     Fix: `await ctx.loader.await()` after create() — settle the whole tree.
  2. Config-path robustness: hand the include the config's ABSOLUTE file:// URL
     so resolution never depends on ctx.baseUrl / can never fall back to cwd.
Both bins fixed identically. NOTE: the cordis Loader resolves a config's bare
plugin specifiers via its internal module loader, active only under
`node --expose-internals`; the bin cannot add a node flag itself, so this is
documented in the bin JSDoc + both package READMEs (the demos already comply).
The repo `examples/*/cordis.yml` are tsx-only artifacts (workspace plugins
resolve through the tsconfig paths map, not node_modules), so they are not a
valid plain-node bin target — the smokes use a real-install-shaped temp dir.

Fail loud on a load failure: boot() previously exited 0 SILENTLY when a config
path's directory does not exist — the include plugin fails to IMPORT, the cordis
Loader catches+LOGS it and leaves the entry with no fiber (no rejection), and
`loader.await()` does not rethrow (EntryTree.await uses Promise.allSettled). Fix:
boot() now calls assertEntriesLoaded(ctx) after the tree settles and throws on
any entry with no fiber, so a typo'd config dir exits non-zero with a clear
message. main() also installs an unhandledRejection guard (installFailLoud) that
replaces Node's stack dump with a single labelled stderr line for the
companion case (a missing config FILE in a real dir, whose include-init throw
surfaces as a rejection Node already exits non-zero on). Regression tests added
to both built-bin smokes (missing dir + missing file → non-zero exit + stderr);
verified the missing-dir test fails on the pre-fix bin.

Built-bin smokes (the reviewer's ask): packages/ui/{stdio,acp}-agent/tests/
built-bin.e2e.ts run the REAL lib/bin.js under `node` (NOT tsx) in a temp
consumer dir, asserting the stdio echo round-trip / the acp initialize response
+ stdout purity, plus the fail-loud cases above. They build-gate (skip if lib/
absent) and run in a new ci.yml step after the build.

Issue 2 — packages/README.md + docs/architecture.md said "plugins depend on
interfaces, never on the concrete loop", but dsh-agent-core imports the concrete
dsh-agent-loop. Scope the rule to EXTENSION plugins and carve out the sanctioned
COMPOSITION/bundle exception (dsh-agent-core composes the concrete spine); note
it in the implemented RFC too.

Issue 3 — examples/acp-agent/tests/acp.snapshot.ts fixture-guard claimed
no-model scenarios need no session.jsonl, but runScenario() always boots
llm-replay with the session.jsonl path and loadReplayScript() throws when it is
absent. Require session.jsonl for ALL scenarios (no-model ones ship a
header-only fixture) and rewrite the comment to match reality.
2026-06-21 15:39:24 +08:00
Tianyi Cui 9e86fd995c fix(ci): repoint the demo smoke test at the dsh-stdio-agent bin
The CI "Demo smoke test" step booted the deleted examples/echo-agent/start.ts
(removed when the boot glue moved into the dsh-stdio-agent bin), so CI failed on
node 24 + 26 while the local gates and e2e passed. Invoke `pnpm run demo:echo`
instead of hardcoding the boot path — that routes through the canonical demo
script, so the smoke can never drift from it again. The output assertions and
the .sessions/_no-cwd/main-session-*.jsonl artifact check are unchanged
(verified the new bin produces identical output).
2026-06-21 14:23:19 +08:00
Tianyi Cui bfc5a9097d fix review findings: soften the last stdout-purity overclaim in the acp-agent module doc
The acp-agent README/RFC overclaim was already softened; this module doc comment
still said the footgun is "structurally unreachable from the leaf". A leaf
cordis.yml CAN still add a sibling @cordisjs/plugin-logger-console — the app does
not prevent it. Reword to the accurate claim: the app gives the default front
door no logger entry to misconfigure, and the "never add a stdout logger to an
ACP leaf" rule still stands.
2026-06-21 13:08:11 +08:00
Tianyi Cui 66e56bd395 fix review findings: stale coding-agent README, export-shape guards, overclaim wording
Codex review of PR #88 found three issues in the example-app extraction:

A1 — examples/coding-agent/README.md's plugin table still listed the OLD
direct-wired leaf entries (agent-loop, session-persistence, src/stdio-chat.ts —
the whole src/ dir is gone). Rewrite it to the four real leaf entries the
current cordis.yml loads (hmr, llm-deepseek, bash, stdio-agent), noting that
tool-bash/persistence/agent/loop now live inside the agent-core + stdio-agent
bundles.

A2 — the three new app/spine packages (agent-core, stdio-agent, acp-agent)
export NO `inject`, so a stray `export default apply` would let unwrapExports
collapse the module and silently DROP name/Config WITHOUT crashing — the
real-load-path smokes would stay green. agent-core is never Loader-unwrapped at
all. Add an explicit export-shape guard per package: assert no `default` export
and that the real Loader.unwrapExports leaves name/Config/apply intact. Verified
each fails when `export default apply` is added.

B — soften "structurally unreachable / cannot wire a stdout logger" overclaims
in the acp-agent/agent-core READMEs and the implemented RFC: a leaf CAN still add
a sibling logger entry; the accurate claim is the app omits one so the default
leaf has nothing to get wrong. Keep the safety directive (never add a stdout
logger to an ACP leaf).
2026-06-21 12:49:59 +08:00
Tianyi Cui 47afcb297f Merge PR6 (snapshot golden removal + PR5 trace-events) into PR7 2026-06-21 12:10:49 +08:00
Tianyi Cui e2bde2902c refactor(examples): extract the app spine into dsh-agent-core + app packages
Implements docs/rfc/.../2026-06-20-extract-example-app-packages.md. Each
example was thick — a hand-rolled start.ts, an infra preamble, nested
base.yml/base-core.yml/acp-tail.yml includes, and a coupled front-door
cluster enforced only by prose. This moves the composition into packages so
each example is a thin leaf cordis.yml: pick the swappable backends, load one
app package.

New packages:
  - @deepseek-ai/dsh-agent-core (packages/core/agent-core): one bundle plugin
    that loads the providerless/executor-less/UI-less spine (timer + llm +
    sessions + system-prompt + tools + agents + invariants + tool-bash +
    agent-loop) via ctx.plugin(...) inside apply(), and forwards agent-loop's
    `agents` list as its own Config (export const Config = AgentLoop.Config,
    default []).
  - @deepseek-ai/dsh-stdio-agent (packages/ui/stdio-agent): terminal chat APP —
    agent-core + console logger + readline UI + a pre-created `main` agent, with
    a bin. The demo:echo/coding front door.
  - @deepseek-ai/dsh-acp-agent (packages/ui/acp-agent): ACP server APP —
    agent-core + JSONL persistence + the acp bridge, NO stdout logger, with a
    bin. The stdout-purity footgun is structurally unreachable from the leaf.

Amendment to the RFC: hmr stays a LEAF cordis.yml entry, not baked into
dsh-stdio-agent. hmr is a Loader-only dev plugin (throws without
--expose-internals; the in-process test tier can't even import its decorator
form), so a package statically importing it could never carry the per-file
coverage gate. Unlike the console logger, a stray hmr is not a stdout-purity
footgun, so leaving it at the leaf costs no safety. With hmr out, all three new
packages carry in-process unit specs at 100%.

Boot glue (Loader tail, .env load, snapshot-mode selection, stdin-dispose
lifecycle) moves into each app's bin; start.ts and base.yml/base-core.yml/
acp-tail.yml are deleted. Each app package gets a keyless real-load-path test
that boots through its bin + the cordis Loader (guarding the unwrapExports
export-shape bug class, postmortem 0001). ACP snapshot replay stays green
against the existing committed goldens (pure boot restructuring). RFC moved
proposed->implemented with the amendment recorded; package/example/architecture
docs and the module graph updated.
2026-06-21 12:06:38 +08:00