Every packages/*/* README now carries a canonical '## Known Limitations and
Deferred Work' section: condensed, evidence-backed bullets for consumer-visible
gaps (unimplemented features, platform caveats, MVP cuts) and consciously
postponed work (TODO/FIXME/XXX markers, RFC deferrals still open). The ten
pre-existing ad-hoc variants ('What is NOT here (TODO)', 'Deferred',
'Limitations (MVP)', 'Known limitations (tracked TODOs)', ...) are normalized
into the canonical heading.
A new doc-sync gate, scripts/verify-readme-limitations.ts, enforces the shape:
exactly one limitations-like heading per package README, byte-equal to the
canonical h2, with at least one bullet; near-miss headings fail so variants
cannot creep back. Packages with genuinely nothing to declare (dsh-brand,
dsh-timeout, dsh-subagent-mock, dsh-app-boot) are whitelisted in the script and
must NOT carry the section; whitelist entries are validated against the scanned
package set so a rename fails loud.
Wired into the doc-sync chain (package.json) and the run-gates doc-sync leaf
set; the standing rule lands in packages/AGENTS.md and the adding-a-package
cookbook; decision record in
docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md
(RFC index regenerated).
Also fixes two stale '(deferred)' markers claiming dsh-compact-basic is
unimplemented (the dsh-compact seam README's package table and the seam's
module doc comment).
@deepseek-ai/dsh-subagent-acp
The out-of-process ACP subagent backend: runs each child agent in a spawned subprocess, driven over the Agent Client Protocol (ACP) as the client. Registers a SubagentProvider on ctx.subagents (the subagent seam), alongside the in-process -spawn/-fork backends — multiple backends coexist by name.
It is the direction-inverted twin of the server-side bridge in @deepseek-ai/dsh-acp: that package is the ACP agent (it answers initialize/newSession/prompt); this one is the ACP client (it calls them and implements the sessionUpdate/requestPermission callbacks). Point the configured command at the acp-agent example to "talk to our own process".
What it does
start(request) spawns the configured command, wraps its stdio in an ACP ClientSideConnection, and drives one session: initialize → newSession → prompt. run.started resolves after newSession publishes the remote session and rejects when initialization fails or cancellation wins first; the service emits no start/end pair for a child that never became live. The child's streamed agent_message_chunk text becomes the SubagentResult.output; the prompt's terminal StopReason maps to the stop reason. dispose() kills the subprocess and awaits its exit.
Fresh process per run. Each start spawns a new child, runs exactly one ACP session, and disposes it. Persistent-process pooling is a future optimization (see the RFC).
Unlike the in-process backends, the child does NOT share this cordis context — it is a separate process with its own session, model client, and tools. So this backend:
- injects only
subagents(noctx.agents); - advertises NO start-time capabilities (an out-of-process child can't enforce the parent's depth/tool-filter);
- ignores
request.parent.
Config
| Key | Type | Default | Notes |
|---|---|---|---|
providerName |
string | acp |
Registry name on ctx.subagents. |
command |
string | — (required) | The executable to spawn for each run (the child ACP agent). |
args |
string[] | [] |
Arguments passed to command. |
cwd |
string | process cwd | Working directory for the child process and its ACP session. |
permission |
'allow' | 'reject' |
reject |
How to auto-answer the child's session/request_permission prompts. reject declines every prompt (answer cancelled); allow approves via the first allow-shaped option. The first cut surfaces no prompt to a human. |
env |
Record<string,string> | {} |
Extra env vars for the child (e.g. its own DEEPSEEK_API_KEY). Forwarded on top of a credential-scrubbed copy of the parent env, so an explicit key reaches the child while ambient secrets do not leak implicitly. |
disposeEofGraceMs |
number | 6000 |
Dispose ladder tier 1: how long the child gets to quiesce on its own after stdin EOF (flush persistence, tear down its nested subprocesses) before SIGTERM. |
disposeGraceMs |
number | 3000 |
Dispose ladder tier 2: grace between SIGTERM and the SIGKILL escalation. |
- id: subagent-acp
name: '@deepseek-ai/dsh-subagent-acp'
config:
providerName: acp
command: node
args: ['--import', 'tsx', './packages/ui/acp-agent/src/bin.ts', './examples/acp-agent/cordis.yml']
permission: reject
env:
DEEPSEEK_API_KEY: !!js process.env.DEEPSEEK_API_KEY
StopReason mapping
ACP StopReason → harness SubagentStopReason:
| ACP | harness |
|---|---|
end_turn |
completed |
max_tokens |
max-tokens |
refusal |
refusal |
cancelled |
aborted |
max_turn_requests |
error (no clean equivalent; the task did not finish) |
| (unknown) | error |
A spawn/transport/RPC failure resolves error (or aborted if a cancel was requested) — result never rejects on a child-level failure, per the seam contract.
Environment scrub
The child env is built by buildChildEnv from @deepseek-ai/dsh-subagent-subprocess — the ambient env minus credential-shaped vars, with config.env layered on top after the scrub; the pattern and full semantics live there. For this backend that means the parent harness's own secrets never leak into the spawned agent implicitly, while the child's OWN DEEPSEEK_API_KEY is supplied deliberately via config.env and survives.
Testing
- Keyless (
subagent-acp.spec.ts): spawns a scripted mock ACP server subprocess (tests/mock-acp-server.ts) and drives it through the real backend over real ACP stdio — connection setup, client callbacks, the prompt round-trip, stop-reason mapping, cancellation (including the early-cancel race and a torn-pipe-after-cancel), permission auto-answer, and quiescent disposal. No model, no key. - With-key e2e (
subagent-acp.e2e.ts): the harness drives ITSELF — the backend spawns the realacp-agentexample process and a real model in that child answers a prompt and does real file work (verified on disk). Self-skips withoutDEEPSEEK_API_KEY.
TODO(acp-subagent-replay): snapshot-tier coverage of an ACP child is a separate replay shape (each child is its own PROCESS with its own single-agent replay, distinct from the in-process per-session keying), deferred — see the RFC.
Plugin export shape
Named name / inject / Config / apply, with no default export: the cordis Loader's unwrapExports does exports.default ?? exports, so a stray default would collapse the module to the bare function and drop the inject namespace (see docs/postmortem/0001).
Known Limitations and Deferred Work
- A fresh process per run — persistent-process pooling is a future optimization (the seam RFC).
- No start-time capability enforcement — an out-of-process child cannot honor the parent's
outputSchema/depthLimit/toolFilter, so the provider advertises none andrequest.parentis ignored. - Only
agent_message_chunktext is collected — the child's tool-call activity, thought chunks, and plan updates are not surfaced to the parent. - Permission prompts are auto-answered (
permission: allow | reject) — no human is surfaced a child'ssession/request_permissionin this cut. - No snapshot-tier replay coverage (
TODO(acp-subagent-replay)) — an ACP child is its own process with its own replay shape, deferred.