Merge remote-tracking branch 'origin/master' into simpl-e-acp-trim

# Conflicts:
#	docs/rfc/README.md
This commit is contained in:
Tianyi Cui
2026-07-04 23:22:16 +08:00
106 changed files with 994 additions and 1345 deletions

View File

@@ -1,125 +1,33 @@
# Snapshot-test REPLAY config: the acp-agent plugin tree with the model backend
# swapped to llm-replay (serves a recorded session JSONL — no API key, no
# network). The dsh-acp-agent bin selects this file for DSH_SNAPSHOT=replay.
# Snapshot-test REPLAY overlay: the SAME app tree as cordis.yml, derived from
# it by an include — the one difference is the model backend. A keyless replay
# run cannot boot the real adapter (llm-deepseek's apply() throws without
# DEEPSEEK_API_KEY), so the include patches the live tree at load time: the
# llm-deepseek entry is disabled by id, and the llm-replay entry (which serves
# a recorded session JSONL — no API key, no network) is inserted. Every other
# entry — the app, the bash executor, the fs/subagent/todo tools, both hook
# bridges, the system prompt — IS the live tree, so replay exercises exactly
# what ships and an app-shape change lands once, in cordis.yml.
#
# Same app as cordis.yml (@deepseek-ai/dsh-acp-agent: the agent-core spine +
# JSONL persistence + the ACP bridge) — only the LLM backend differs: llm-replay
# here, llm-deepseek there. It can't reuse the real adapter because llm-deepseek's
# apply() throws without DEEPSEEK_API_KEY, killing a keyless replay run at boot.
#
# stdout is reserved for the ACP JSON-RPC protocol — no stdout logger (the app
# package omits it). The replay fixture path comes from $DSH_SNAPSHOT_FILE (and
# an optional $DSH_SNAPSHOT_OVERRIDE sidecar), set by the snapshot harness.
# The replay adapter: short-circuits llm/stream with the recorded log's chunks,
# in place of llm-deepseek.
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'
# Local bash executor for agent-core's tool-bash schema.
# FIXME(config-comments): keep this executor note from implying bash is the
# whole tool set; filesystem, subagent, and todo_write are loaded below.
- id: bash
name: '@deepseek-ai/dsh-bash-local'
# The dsh-acp-agent bin selects this file for DSH_SNAPSHOT=replay. The replay
# fixture path comes from $DSH_SNAPSHOT_FILE (and an optional
# $DSH_SNAPSHOT_OVERRIDE sidecar), set by the snapshot harness. stdout stays
# reserved for the ACP JSON-RPC protocol (the app package loads no stdout
# logger). Patches apply when the include loads the file — a one-shot replay
# boot, so the load-time-only patch semantics are exactly enough.
- id: base
name: '@cordisjs/plugin-include'
config:
timeoutMs: 60000
# The ACP server app — identical to cordis.yml's entry.
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
systemPrompt: |
You are a coding assistant driven over the Agent Client Protocol.
Your tools are read/write/edit for file operations, bash (plus
bash_output/bash_kill for background tasks), and subagent. Use read to
inspect UTF-8 text files, write to create or replace files, and edit for
targeted literal replacements. Use bash for shell commands, tests,
searches, and operations that are not ordinary file reads or edits. Each
bash call runs in a fresh shell — pass workdir instead of cd. Check the
[exit code: N] marker; verify your work. Keep answers brief and factual.
Use the subagent tool to delegate a focused, self-contained subtask to
a fresh child agent (it works in its own context and returns only its
final result) — give it a complete, standalone instruction. Use
subagent_fork instead when the subtask needs THIS conversation's
context: the child inherits the log so far.
For multi-step work, use the todo_write tool to track a task list:
send the WHOLE list each call (it replaces the previous one), keep at
most one task in_progress (exactly one while work remains), and mark a
task completed as soon as it is done. Skip it for trivial single-step
tasks.
# The subagent seam + both in-process backends + two model-facing tools —
# identical to cordis.yml's wiring (only the LLM backend differs above): spawn
# and fork are each reachable via a dsh-tool-subagent bound to it with a distinct
# toolName (subagent → spawn, subagent_fork → fork).
- id: subagent
name: '@deepseek-ai/dsh-subagent'
- id: subagent-spawn
name: '@deepseek-ai/dsh-subagent-spawn'
config:
providerName: spawn
- id: subagent-fork
name: '@deepseek-ai/dsh-subagent-fork'
config:
providerName: fork
- id: tool-subagent
name: '@deepseek-ai/dsh-tool-subagent'
config:
provider: spawn
toolName: subagent
- id: tool-subagent-fork
name: '@deepseek-ai/dsh-tool-subagent'
config:
provider: fork
toolName: subagent_fork
# The model-facing todo_write tool — identical to cordis.yml's wiring, so a
# replayed todo_write tool call resolves to a real tool during snapshot replay.
- id: tool-todo
name: '@deepseek-ai/dsh-tool-todo'
# Filesystem capability stack — identical to cordis.yml's wiring, so replayed
# read/write/edit tool calls resolve to the real tools during snapshot replay.
- id: fs-local
name: '@deepseek-ai/dsh-fs-local'
config:
cwd: !!js process.cwd()
- id: fs-policy
name: '@deepseek-ai/dsh-fs-policy'
- id: tool-fs
name: '@deepseek-ai/dsh-tool-fs'
# The Claude Code hook bridge. `configPath` is read ONCE at load and resolves
# `./hooks.json` against the PROCESS cwd (not per-session) — in these snapshot
# runs the harness launches the subprocess with process cwd = the scenario's temp
# workspace, so a scenario that ships `workspace/hooks.json` (copied into that cwd
# before the run) exercises the hooks path end-to-end; every other scenario has no
# such file, so the parse fails-soft and the bridge registers nothing (a silent
# no-op — the ACP app loads no logger exporter, so the warning never reaches
# stdout). Hooks themselves run in the session cwd (the bridge passes it as workdir).
- id: hooks-claude
name: '@deepseek-ai/dsh-hooks-claude'
config:
configPath: ./hooks.json
# The Codex hook bridge, loaded alongside the Claude one (symmetric with
# cordis.yml so a recorded Codex scenario fires the hook during recording too). It
# reads its OWN file `./codex-hooks.json` (Codex's dialect) — the two bridges
# cannot share one config. Same fails-soft-when-absent contract: a scenario that
# ships `workspace/codex-hooks.json` exercises the Codex path end-to-end; a
# scenario without one registers nothing (a silent no-op, never reaching stdout).
- id: hooks-codex
name: '@deepseek-ai/dsh-hooks-codex'
config:
configPath: ./codex-hooks.json
path: ./cordis.yml
patches:
# The name is an assertion, not an override: the include skips the patch
# (warning if a logger exists) when the id points at a different plugin,
# so this can never disable the wrong entry. If cordis.yml ever RENAMES
# the id, the patch degrades to a skip — replay output stays correct
# (llm-replay still short-circuits the stream) but the stale patch and a
# futile keyless adapter entry linger until review catches them.
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- insert:
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'

View File

@@ -9,8 +9,8 @@ import { afterEach, describe, expect, it } from 'vitest'
* Keyless Loader-path smoke for examples/coding-agent: boot the REAL example
* through the `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` (the
* cordis Loader, `unwrapExports`, the full plugin tree incl. the
* `@deepseek-ai/dsh-agent-core` bundle and the extracted
* `@deepseek-ai/dsh-ui-stdio`), then close stdin with no prompt and assert the
* `@deepseek-ai/dsh-agent-core` bundle and the app's in-package readline UI
* module), then close stdin with no prompt and assert the
* ready banner + a clean exit.
*
* No prompt is ever sent, so the model is NEVER called — this is why it runs
@@ -18,9 +18,10 @@ import { afterEach, describe, expect, it } from 'vitest'
* `apply()` only requires a key to be PRESENT (it does not validate it and only
* uses it when a stream actually starts), so a dummy key lets the tree boot
* while the absence of any prompt guarantees no network call. The value is the
* real-Loader-path guard for the app + bundle + UI plugin export shapes (a broken
* `export default` that drops `inject`/`Config` would crash here — see postmortem
* 0001), complementing coding-agent's with-key e2e suites which prove the real
* real-Loader-path guard that the composed tree boots (see postmortem 0001;
* the app carries no `inject`, so its export SHAPE is pinned by the stdio-agent
* unit suite's unwrap assertion, not by a crash here),
* complementing coding-agent's with-key e2e suites which prove the real
* product.
*/

View File

@@ -13,11 +13,12 @@ import { afterEach, describe, expect, it } from 'vitest'
*
* This is the guard the per-file unit suite structurally cannot be: it drives
* the `@deepseek-ai/dsh-stdio-agent` app plugin, the `@deepseek-ai/dsh-agent-core`
* bundle it loads, the extracted `@deepseek-ai/dsh-ui-stdio` plugin, AND the
* example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path, so
* a broken plugin export shape (a stray `export default` that `unwrapExports`
* would collapse, dropping `inject`/`Config`) fails here even though hand-mounted
* unit tests stay green (see docs/postmortem/0001). It needs no API key — the
* bundle it loads, the app's in-package readline UI module, AND the
* example-local `mock-llm.ts` / `echo-tool.ts` through their REAL load path
* (see docs/postmortem/0001). The app itself carries no `inject`, so a stray
* `export default` would boot rather than crash here — the export SHAPE is
* pinned by the explicit unwrap assertion in the stdio-agent unit suite; this
* smoke proves the composed tree actually runs. It needs no API key — the
* `mock-echo` adapter never touches the network — so it runs in the default e2e
* gate.
*