docs: rebalance prose cleanup and add trimming skill
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
# Both-mode REPLAY overlay: the same patched tree as both-mode.cordis.yml
|
||||
# (registry in `mode: both` + the worker code runtime) with the keyless model
|
||||
# swap from cordis.snapshot.yml (llm-deepseek disabled, llm-replay serving
|
||||
# the recorded fixture). Patches do not compose across nested includes —
|
||||
# an outer include's patch can only target entries in the file IT loads — so
|
||||
# this file patches ./cordis.yml directly with the union of both overlays.
|
||||
# Keyless both mode combines the runtime/registry patch with the DeepSeek-to-replay
|
||||
# swap. Include patches cannot target entries behind a nested include, so this file
|
||||
# applies both overlays directly to `cordis.yml`.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
# Both-mode RECORD overlay: the live acp-agent tree (./cordis.yml) with two
|
||||
# load-time patches — the app entry's config gains `tools: { mode: both }`
|
||||
# (every native tool definition stays on the wire AND run_code + the generated
|
||||
# TypeScript SDK prompt section ride along) and the worker-thread code runtime joins the
|
||||
# tree as `ctx.codeRuntime`. The dsh-acp-agent bin boots this file when the
|
||||
# snapshot harness records the both-mode scenario; DSH_SNAPSHOT=replay swaps
|
||||
# it for the sibling both-mode.cordis.snapshot.yml. A config patch REPLACES
|
||||
# the entry's whole config, so the base entry's fields are restated verbatim.
|
||||
# Both mode adds `ctx.codeRuntime` while keeping native tools on the wire and
|
||||
# adding `run_code` plus its generated TypeScript SDK prompt. The app bin selects
|
||||
# this overlay for snapshot recording and the sibling overlay for replay. A config
|
||||
# patch replaces the whole app config, so unchanged base fields are restated below.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# Code Mode REPLAY overlay: the same patched tree as code-mode.cordis.yml
|
||||
# (registry in `mode: code` + the worker code runtime) with the keyless model
|
||||
# swap from cordis.snapshot.yml (llm-deepseek disabled, llm-replay serving
|
||||
# the recorded fixture). Patches do not compose across nested includes —
|
||||
# an outer include's patch can only target entries in the file IT loads — so
|
||||
# this file patches ./cordis.yml directly with the union of both overlays.
|
||||
# Keyless Code Mode combines the runtime/registry patch with the DeepSeek-to-replay
|
||||
# swap. Include patches cannot target entries behind a nested include, so this file
|
||||
# applies both overlays directly to `cordis.yml`.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
# Code Mode overlay: the live acp-agent tree (./cordis.yml) with two
|
||||
# load-time patches — the app entry's config gains `tools: { mode: code }`
|
||||
# (the registry offers exactly one wire tool, run_code, plus the generated
|
||||
# TypeScript SDK prompt section) and the worker-thread code runtime joins the
|
||||
# tree as `ctx.codeRuntime`. The dsh-acp-agent bin boots this file for
|
||||
# `pnpm run demo:code-mode acp` and when the snapshot harness records the
|
||||
# code-mode scenarios; DSH_SNAPSHOT=replay swaps it for the sibling
|
||||
# code-mode.cordis.snapshot.yml. A config patch REPLACES the entry's whole
|
||||
# config, so the base entry's fields are restated verbatim.
|
||||
# Code Mode adds `ctx.codeRuntime` and changes the registry to one wire tool,
|
||||
# `run_code`, plus its generated TypeScript SDK prompt. The app bin selects this
|
||||
# overlay for `demo:code-mode acp` and snapshot recording, and selects the sibling
|
||||
# replay overlay for `DSH_SNAPSHOT=replay`. A config patch replaces the whole app
|
||||
# config, so unchanged base fields are restated below.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
# Keyless replay includes the live `cordis.yml`, disables the key-requiring
|
||||
# DeepSeek adapter, and inserts `llm-replay` to serve recorded JSONL without a key
|
||||
# or network; every other app entry remains shared.
|
||||
# With `DSH_SNAPSHOT=replay`, the app bin reads `DSH_SNAPSHOT_FILE` and optional
|
||||
# `DSH_SNAPSHOT_OVERRIDE` from the harness. The one-shot patch applies at include
|
||||
# load time, and stdout remains reserved for ACP JSON-RPC.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
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.
|
||||
# `name` asserts the target: a mismatch skips the patch and warns only when
|
||||
# a logger exists. A renamed id leaves a stale adapter entry, but replay still
|
||||
# short-circuits through `llm-replay`.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
# The acp-agent plugin tree: the ACP server. Also the snapshot RECORD config
|
||||
# (the dsh-acp-agent bin selects it for DSH_SNAPSHOT=record): a real llm-deepseek
|
||||
# run whose persisted log the snapshot harness harvests. The swappable DeepSeek
|
||||
# adapter, local bash/filesystem executors, the ACP server app
|
||||
# (@deepseek-ai/dsh-acp-agent), and the optional model-facing fs/subagent/todo
|
||||
# tools loaded below.
|
||||
#
|
||||
# CRITICAL: this tree loads NO stdout logger and NO hmr — stdout is reserved for
|
||||
# the ACP JSON-RPC protocol (see packages/ui/acp). That guarantee is now a
|
||||
# property of @deepseek-ai/dsh-acp-agent (it contains no logger entry), not a
|
||||
# leaf convention: there is no logger here to get wrong.
|
||||
#
|
||||
# Requires DEEPSEEK_API_KEY (and optionally DEEPSEEK_BASE_URL) — the
|
||||
# dsh-acp-agent bin loads the gitignored repo-root .env first (on STDERR only).
|
||||
# ACP server and snapshot-record composition. With `DSH_SNAPSHOT=record`, the
|
||||
# app bin runs the real DeepSeek adapter and the harness harvests its persisted log.
|
||||
# `dsh-acp-agent` loads no stdout logger or HMR because stdout carries ACP JSON-RPC.
|
||||
# It loads the gitignored root `.env` on stderr before reading `DEEPSEEK_API_KEY`
|
||||
# and optional `DEEPSEEK_BASE_URL` here.
|
||||
|
||||
# The DeepSeek adapter.
|
||||
- id: llm-deepseek
|
||||
@@ -23,8 +14,7 @@
|
||||
- deepseek-v4-flash
|
||||
- deepseek-v4-pro
|
||||
|
||||
# Local bash executor for agent-core's tool-bash schema (one of several tool
|
||||
# stacks in this tree: filesystem, subagent, and todo_write load below).
|
||||
# Local executor for the app bundle's bash tool.
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
@@ -38,22 +28,16 @@
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
# The persona: identity + behavior only, nothing about transports or
|
||||
# tooling — tool guidance lives with each tool plugin (descriptions +
|
||||
# prompt sections). {{model}} and {{cwd}} are prompt variables the agent
|
||||
# loop resolves per session (every ACP session carries the client's cwd,
|
||||
# so the persona can state the workspace).
|
||||
# Keep the persona to identity and behavior; tool plugins own tool guidance.
|
||||
# The loop resolves {{model}} and each ACP session's client-supplied {{cwd}}.
|
||||
persona: |
|
||||
You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
|
||||
|
||||
Verify your work by running the code or tests. Keep answers brief and factual.
|
||||
|
||||
# The subagent seam + both in-process backends + two model-facing tools, as leaf
|
||||
# entries after the app (which provides ctx.agents/ctx.tools). spawn (a fresh
|
||||
# child) and fork (a child seeded with the parent's completed-turn prefix) are
|
||||
# both reachable by the model: dsh-tool-subagent is loaded once per backend with
|
||||
# a distinct toolName (subagent → spawn, subagent_fork → fork), so a multi-child
|
||||
# scenario can exercise both transports.
|
||||
# Expose fresh-child `spawn` and completed-prefix `fork` through separate tool
|
||||
# names so multi-child scenarios exercise both transports. These leaves follow
|
||||
# the app because it provides `ctx.agents` and `ctx.tools`.
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
@@ -80,10 +64,8 @@
|
||||
toolName: subagent_fork
|
||||
|
||||
|
||||
# Dynamic workflows: the worker-thread engine (ctx.workflows) over the spawn
|
||||
# subagent backend above, plus the model-facing `workflow` tool. The model
|
||||
# writes a JavaScript orchestration script (meta + body); the engine runs it
|
||||
# in its own worker thread and fans agent() calls out as spawn children.
|
||||
# The worker-thread workflow engine fans a model-written JavaScript script's
|
||||
# `agent()` calls out through the spawn backend; the adjacent tool exposes it to the model.
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
config:
|
||||
@@ -91,23 +73,18 @@
|
||||
|
||||
- id: tool-workflow
|
||||
name: '@deepseek-ai/dsh-tool-workflow'
|
||||
# The model-facing todo_write tool: whole-list task tracking written to the
|
||||
# session log (todo/write), surfaced to the ACP client as a `plan` update.
|
||||
# `todo_write` replaces the logged whole list and surfaces an ACP `plan` update.
|
||||
- id: tool-todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
|
||||
# The repeat-tool-call guard: advisory reminders (injected context, never a
|
||||
# block) when the model re-issues the same tool call with identical arguments;
|
||||
# defaults [3, 5, 8]. Loaded here so the snapshot tier exercises the reminder
|
||||
# transcript (the repeat-tool-guard scenario) — no other scenario repeats a
|
||||
# call three times, so it is inert everywhere else.
|
||||
# Identical repeat calls trigger advisory context, never a block, at the default
|
||||
# thresholds [3, 5, 8]. Only the repeat-tool-guard snapshot scenario reaches them.
|
||||
- id: repeat-tool-guard
|
||||
name: '@deepseek-ai/dsh-repeat-tool-guard'
|
||||
|
||||
# Filesystem capability stack: local provider, read-before-write/edit policy
|
||||
# gate, then the model-facing read/write/edit tools. Relative filesystem paths
|
||||
# resolve from the server launch cwd; the documented Zed setup launches this
|
||||
# demo from the harness checkout with `pnpm --dir`.
|
||||
# Policy loads before the model-facing filesystem tools so writes and edits require
|
||||
# an observed file. Relative paths use the server launch cwd; the Zed setup launches
|
||||
# this demo from the harness checkout with `pnpm --dir`.
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
config:
|
||||
@@ -119,28 +96,19 @@
|
||||
- id: tool-fs
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
# The Claude Code hook bridge. `configPath` is PROCESS-LEVEL: it is read ONCE at
|
||||
# load and the relative `./hooks.json` resolves against the ACP server's launch
|
||||
# cwd, NOT each `session/new.cwd`. So a single `hooks.json` next to where the
|
||||
# server starts applies to every session; a project-local, per-session hooks.json
|
||||
# is NOT discovered (per-session config resolution is a TODO — see the bridge
|
||||
# README). With no file present the parse fails-soft and the bridge registers
|
||||
# nothing (a silent no-op). Hooks THEMSELVES run in the session cwd (the bridge
|
||||
# passes it as the workdir); only WHERE the config is read from is process-level.
|
||||
# stdout is the ACP JSON-RPC channel — the bridge's warnings go through ctx.logger
|
||||
# (no exporter here), never to stdout.
|
||||
# `configPath` is read once at load and resolves from the server launch cwd, not
|
||||
# `session/new.cwd`; one `hooks.json` therefore applies to every session and a
|
||||
# project-local file is not discovered. Missing config registers nothing. Hook
|
||||
# commands still run in the session cwd. Warnings use `ctx.logger`, never stdout;
|
||||
# see packages/hooks/hooks-claude/README.md for the deferred per-session design.
|
||||
- id: hooks-claude
|
||||
name: '@deepseek-ai/dsh-hooks-claude'
|
||||
config:
|
||||
configPath: ./hooks.json
|
||||
|
||||
# The Codex hook bridge, loaded alongside the Claude one. It reads its OWN config
|
||||
# file (`./codex-hooks.json`, Codex's snake_case five-event dialect) — the two
|
||||
# bridges cannot share one file, so each owns a distinct path. Same process-level
|
||||
# read-once semantics and same fails-soft-when-absent contract: a launch cwd with
|
||||
# no `codex-hooks.json` registers nothing (a silent no-op through ctx.logger, never
|
||||
# stdout). The example ships both bridges so a scenario can exercise EITHER dialect
|
||||
# end-to-end by seeding the matching file in its workspace/.
|
||||
# Codex uses its own `codex-hooks.json` and snake_case five-event dialect; it
|
||||
# cannot share Claude's file. It has the same process-level, read-once, missing-is-no-op,
|
||||
# logger-only contract. Shipping both bridges lets a scenario seed and exercise either dialect.
|
||||
- id: hooks-codex
|
||||
name: '@deepseek-ai/dsh-hooks-codex'
|
||||
config:
|
||||
|
||||
@@ -26,12 +26,13 @@ import {
|
||||
* WITHOUT a key, since it only needs the server to boot and answer initialize.
|
||||
*/
|
||||
|
||||
// The dsh-acp-agent bin (the demo:acp entry) and this example's cordis.yml.
|
||||
// The child runs from a temp cwd, so its bin and config path are absolute.
|
||||
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
// Resolve tsx absolutely because the subprocess runs outside the repo.
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// Absolute path to the repo-root tsconfig.
|
||||
// The root tsconfig supplies unbuilt workspace `paths`; making it explicit
|
||||
// avoids accidental resolution through stale built output.
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
interface Spawned {
|
||||
|
||||
@@ -75,12 +75,15 @@ const SCENARIOS: Scenario[] = [
|
||||
// child runs as a spawn subagent under the worker-thread engine (its session is the
|
||||
// child fixture), and the tool result carries the script's return value.
|
||||
{ name: 'workflow-run', hasModelTurn: true, recorded: true, childSessions: 1 },
|
||||
// Hook matrix — one scenario per hook point × its headline Decision outcome, across BOTH
|
||||
// bridges (Claude `hooks.json`, Codex `codex-hooks.json`, seeded in workspace/).
|
||||
// Prompt-submit blocks are authored keylessly: they persist a rejected turn
|
||||
// and hook events without starting a model step, so their logs still compare.
|
||||
{ name: 'hook-cc-promptsubmit-block', hasModelTurn: false, comparesLog: true, recorded: false },
|
||||
{ name: 'hook-codex-promptsubmit-block', hasModelTurn: false, comparesLog: true, recorded: false },
|
||||
// The mid-turn seams fire during a real model turn, so each is recorded with its hook active
|
||||
// (the model's reaction to a deny/block/force-continue is part of the captured transcript).
|
||||
// SessionStart/SubagentStart are excluded because detached injection races log
|
||||
// order; SubagentStop writes no transcript, so a golden could not prove it ran.
|
||||
// Unit tests cover those points; the hook-snapshot-matrix RFC owns the rationale.
|
||||
{ name: 'hook-cc-promptsubmit-context', hasModelTurn: true, recorded: true },
|
||||
{ name: 'hook-cc-pretool-deny', hasModelTurn: true, recorded: true },
|
||||
{ name: 'hook-cc-pretool-ask', hasModelTurn: true, recorded: true },
|
||||
@@ -97,7 +100,7 @@ const SCENARIOS: Scenario[] = [
|
||||
{ name: 'hook-codex-stop-continue', hasModelTurn: true, recorded: true },
|
||||
// Code Mode: the registry in `mode: code` — the wire tool list collapses to [run_code], the
|
||||
// tools:sdk section rides in the prompt, and the program's tool calls land as
|
||||
// tool/code-dispatch events.
|
||||
// tool/code-dispatch events. Each overlay composes and pins its own header class.
|
||||
{ name: 'code-mode-turn', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'code', configPath: CODE_MODE_CONFIG },
|
||||
{ name: 'both-mode-turn', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'both', configPath: BOTH_MODE_CONFIG },
|
||||
]
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
} from '@agentclientprotocol/sdk'
|
||||
|
||||
/**
|
||||
* With-key e2e: the Claude Code hook bridge running against the real acp-agent subprocess and
|
||||
* the real model.
|
||||
* With-key e2e for the Claude hook bridge. The process-level `./hooks.json` is
|
||||
* resolved from a temporary launch cwd and blocks all PreToolUse calls; a real
|
||||
* model is asked to write there, and absence of the file proves interception.
|
||||
* The test owns and disposes the ACP subprocess.
|
||||
*/
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
|
||||
@@ -76,7 +78,8 @@ afterEach(async () => {
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent e2e: a PreToolUse hook blocks bash (real model)', () => {
|
||||
it('denies every bash command, so the requested file is never written (verified on disk)', async () => {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'acp-hooks-e2e-'))
|
||||
// A PreToolUse hook that blocks every tool (exit 2, no matcher = match-all).
|
||||
// `configPath` is process-relative, so placing the match-all hook in the
|
||||
// launch cwd selects it; hook commands themselves run in the session cwd.
|
||||
await writeFile(join(workdir, 'hooks.json'), JSON.stringify({
|
||||
hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'echo "bash blocked by policy" >&2; exit 2' }] }] },
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user