Merge origin/master into codex/dsh-badge-plugin

This commit is contained in:
Tianyi Cui
2026-08-08 15:42:47 +08:00
143 changed files with 1211 additions and 2360 deletions
+3
View File
@@ -376,6 +376,9 @@
"simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml": "sha256:531c446f0e95054f8ced17be9a180f8b0a823f7e9d5ce466c94c2f9cff90a111", "simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml": "sha256:531c446f0e95054f8ced17be9a180f8b0a823f7e9d5ce466c94c2f9cff90a111",
"simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md": "sha256:a35a6372aabdf7cbc211f1bd5820d85d3467c9ed50f84e05caa3339382379ce7", "simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md": "sha256:a35a6372aabdf7cbc211f1bd5820d85d3467c9ed50f84e05caa3339382379ce7",
"simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md": "sha256:a6ed9530289a783c3d7a1ddb038fba6b7daf7feb773298a57e811791e354d438", "simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md": "sha256:a6ed9530289a783c3d7a1ddb038fba6b7daf7feb773298a57e811791e354d438",
"simplification/2026-08-03-explicit-config-dsh-entrypoint.i18n.yaml": "sha256:5466161f3fb8f2e8117fe8ff242675cc9fe9ef264d1e29b9bc586891c73c051a",
"simplification/2026-08-03-explicit-config-dsh-entrypoint.md": "sha256:f23accae7d05c2e75cb73ec69b492307f1ce7526ecfa9f6b12a621e02fd1a0c3",
"simplification/2026-08-03-explicit-config-dsh-entrypoint.zh.md": "sha256:a32d2c6ecf748a16a2c35b59cd2da2fda75769e3ab24be6a2e026d8655466db4",
"testing/2026-06-20-remove-redundant-snapshot-log-expected-output.i18n.yaml": "sha256:4177012c0821a8c22499852ecdf096af56d7263cb91c5d9d1bcd552cc26a3e00", "testing/2026-06-20-remove-redundant-snapshot-log-expected-output.i18n.yaml": "sha256:4177012c0821a8c22499852ecdf096af56d7263cb91c5d9d1bcd552cc26a3e00",
"testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md": "sha256:45234e7cc04b6010c6141f8d5924c04547300098f96262d423c50108e7c7011a", "testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md": "sha256:45234e7cc04b6010c6141f8d5924c04547300098f96262d423c50108e7c7011a",
"testing/2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md": "sha256:15e5a4ad3dee0bb711480cabe45cd97ec37bbdba19c2c2b47d1e9c203b07a48b", "testing/2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md": "sha256:15e5a4ad3dee0bb711480cabe45cd97ec37bbdba19c2c2b47d1e9c203b07a48b",
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-03-explicit-config-dsh-entrypoint.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-03-explicit-config-dsh-entrypoint.md
2026-08-03-explicit-config-dsh-entrypoint.md: e0d1e954d9cef472ea59345a3d2ef5a67bd03ae8 2026-08-03-explicit-config-dsh-entrypoint.md: 4474e786b3a99ff0ee81ac54fcb6eb5aaff5ee04
2026-08-03-explicit-config-dsh-entrypoint.zh.md: b5b464e3b45a6f3909bbf087f7005ad3f819424a 2026-08-03-explicit-config-dsh-entrypoint.zh.md: 11b7b27c7560aaa43a9fdcfb12a9a17f9433e619
@@ -1,6 +1,7 @@
# Agent Note: Explicit-config dsh entrypoint # Agent Note: Explicit-config dsh entrypoint
Status: implemented Status: implemented
Archived: 2026-08-08
English | [中文](2026-08-03-explicit-config-dsh-entrypoint.zh.md) English | [中文](2026-08-03-explicit-config-dsh-entrypoint.zh.md)
@@ -1,6 +1,7 @@
# Agent Note显式配置的 dsh 入口 # Agent Note: 显式配置的 dsh 入口
Status: implemented Status: implemented
Archived: 2026-08-08
[English](2026-08-03-explicit-config-dsh-entrypoint.md) | 中文 [English](2026-08-03-explicit-config-dsh-entrypoint.md) | 中文
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
2026-07-19-gui-layering-and-rpc-protocol.md: 34077302c53081f6ee9171d64dce9af342710d71 2026-07-19-gui-layering-and-rpc-protocol.md: 8e020e4fe9b60100671c0cf0e98e28532d850f94
2026-07-19-gui-layering-and-rpc-protocol.zh.md: bc51542ac8159ee7cba234b4ee8b4db47a7f9b58 2026-07-19-gui-layering-and-rpc-protocol.zh.md: 55fa8084083aa83fbb4a38f8e41d2e5624b6e58e
@@ -10,7 +10,7 @@ English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)
We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product UI shapes are coming — Web (server), Electron, and others. We call these shapes Clients, uniformly, and want the following capabilities: We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product UI shapes are coming — Web (server), Electron, and others. We call these shapes Clients, uniformly, and want the following capabilities:
- One `dsh` process supporting both `dsh web` (serve) and `dsh -p` (headless) — one process, two modes (a design reservation) - One `dsh` process supporting both `dsh web` (serve) and `dsh run` (headless) — one process, two modes (a design reservation)
- Launching inside Electron with the same Web technology shape as `dsh web` - Launching inside Electron with the same Web technology shape as `dsh web`
That demands a stable layered responsibility model in the engineering codebase, so future client shapes plug in cleanly. That demands a stable layered responsibility model in the engineering codebase, so future client shapes plug in cleanly.
@@ -31,7 +31,7 @@ Directories layer as follows:
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services. - **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- `apps/` holds the externally exported application shapes, assembled from Client / Host mixtures. - `apps/` holds the externally exported application shapes, assembled from Client / Host mixtures.
- `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`. - `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
- `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = startHost + webserver + the built `dsh-frontend` dist; `dsh -p` = headless in-process calls, zero HTTP. - `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = startHost + webserver + the built `dsh-frontend` dist; `dsh run` = headless in-process calls, zero HTTP.
- A future Electron shape reuses the same web client packages over an IPC fetch carrier. - A future Electron shape reuses the same web client packages over an IPC fetch carrier.
``` ```
@@ -215,7 +215,7 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
| Subclass | Package | doFetch | Purpose | | Subclass | Package | doFetch | Purpose |
|---|---|---|---| |---|---|---|---|
| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing — `dsh -p` headless is the protocol's second real consumer | | `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing — `dsh run` headless is the protocol's second real consumer |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser shape; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) | | `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser shape; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) | | `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) |
| (future) IPC bridge subclass | apps/electron | IPC serialization round trip | swaps only doFetch; contract and base class unchanged | | (future) IPC bridge subclass | apps/electron | IPC serialization round trip | swaps only doFetch; contract and base class unchanged |
@@ -9,7 +9,7 @@ Status: implemented
## Problem ## Problem
需要提供 UI 对接层,除已有 ACP/stdio基础版本外,还需要 Web(server) 、 Electron 、等其他产品 UI 形态。我们把这些形态统一称为 Client。希望有如下能力支持: 需要提供 UI 对接层,除已有 ACP/stdio基础版本外,还需要 Web(server) 、 Electron 、等其他产品 UI 形态。我们把这些形态统一称为 Client。希望有如下能力支持:
-`dsh` 进程,同时支持 `dsh web`(启动) 和 `dsh -p`(headless) ,一个进程两种模式(设计预留) -`dsh` 进程,同时支持 `dsh web`(启动) 和 `dsh run`(headless) ,一个进程两种模式(设计预留)
- 以与 `dsh web` 同构的 Web 技术形态,在 Electron 中启动 - 以与 `dsh web` 同构的 Web 技术形态,在 Electron 中启动
那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client 形态。 那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client 形态。
@@ -29,7 +29,7 @@ Status: implemented
- **fetch 到达插件包**`ui-layout``ui-sidebar``ui-conversation``ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。 - **fetch 到达插件包**`ui-layout``ui-sidebar``ui-conversation``ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
- `apps/` 作为对外导出的应用形态入口,可以由 Client / Host 混合组装。 - `apps/` 作为对外导出的应用形态入口,可以由 Client / Host 混合组装。
- `apps/web``dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts` - `apps/web``dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`
- `apps/cli``@deepseek-ai/dsh`)做形态分发:`dsh web` = startHost + webserver + 构建出的 `dsh-frontend` dist`dsh -p` = headless 进程内直调,零 HTTP。 - `apps/cli``@deepseek-ai/dsh`)做形态分发:`dsh web` = startHost + webserver + 构建出的 `dsh-frontend` dist`dsh run` = headless 进程内直调,零 HTTP。
- 将来的 Electron 形态经由 IPC fetch 载体复用同一套 web client 包。 - 将来的 Electron 形态经由 IPC fetch 载体复用同一套 web client 包。
``` ```
@@ -213,7 +213,7 @@ export type ResponseValue<K> =
| 子类 | 所在包 | doFetch | 用途 | | 子类 | 所在包 | doFetch | 用途 |
|---|---|---|---| |---|---|---|---|
| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧——`dsh -p` headless 即协议第二真实消费者 | | `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧——`dsh run` headless 即协议第二真实消费者 |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器形态;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) | | `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器形态;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) | | `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) |
| (将来)IPC 桥子类 | apps/electron | IPC 序列化往返 | 仅换 doFetch,契约/基类零改 | | (将来)IPC 桥子类 | apps/electron | IPC 序列化往返 | 仅换 doFetch,契约/基类零改 |
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md
2026-07-29-dsh-source-launch-tsx-esm.md: 21e912c7c7bbdd70142c202105d9a3035442884a 2026-07-29-dsh-source-launch-tsx-esm.md: aabe6c2d6a92b5cc6568eee1d42df3a6998cf0ca
2026-07-29-dsh-source-launch-tsx-esm.zh.md: dc6150b7017777eea99778cc9813e17402354462 2026-07-29-dsh-source-launch-tsx-esm.zh.md: fdeb8a976e02bb9311a6ec9d097d09151a998a04
@@ -35,4 +35,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`
- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line. - One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line.
- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode. - TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode.
- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures. - The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures.
- Startup improves ~0.4s over the full tsx default (`demo:headless` and ACP keep `--import tsx`; their graphs were not audited for CJS-hook dependence and their launch latency is not on the interactive path). - Startup improves ~0.4s over the full tsx default (`demo:headless` now aliases the same `dsh run` source launch; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path).
@@ -35,4 +35,4 @@ node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke``
- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。 - 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。
- TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。 - TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。
- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。 - 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。
- 启动相比完整 tsx 默认形态快约 0.4s(`demo:headless` ACP 保 `--import tsx`:其依赖图未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。 - 启动相比完整 tsx 默认形态快约 0.4s(`demo:headless` 现为同一条 `dsh run` 源码启动命令的别名;ACP 保 `--import tsx`,因为它的依赖图未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
2026-08-05-profile-plugin-bundles.md: 11a8ac3d4005371ca9596ba237aaf42a8e770dee 2026-08-05-profile-plugin-bundles.md: 8613e600ad633818abb4319e614230340c3b3876
2026-08-05-profile-plugin-bundles.zh.md: 0e9ebf657ccb9d05967d90a935b356acf287a24c 2026-08-05-profile-plugin-bundles.zh.md: 03a771364c4f3262801f28e68b96ec5de633e5ec
@@ -12,7 +12,9 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`. Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`.
The shipped compositions became bundles: `@deepseek-ai/dsh-base` (the former base rows as one insert), `@deepseek-ai/dsh-web-app` (the former web overlay plus a runtime glue plugin that owns what used to be launcher code — frontend-dist resolution, the web-surface prompt section, bash runtime variables, the URL line), and `@deepseek-ai/dsh-headless` (a one-shot runner plugin over base + web-app). `dsh web` stays as an alias for `--profile web` carrying the Web flag family; `dsh --profile headless "task"` replaces `-p`; `dsh --config` is removed (its uses migrate to `--patch`). `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` after `add`/`remove` (a bundle-less package warns and stays a plain dependency). The shipped compositions became bundles: `@deepseek-ai/dsh-base` (the former base rows as one insert), `@deepseek-ai/dsh-web-app` (the former web overlay plus a runtime glue plugin that owns what used to be launcher code — frontend-dist resolution, the web-surface prompt section, bash runtime variables, the URL line), and `@deepseek-ai/dsh-headless` (a one-shot runner plugin over base + web-app). `dsh web` stays as an alias for `--profile web` carrying the Web flag family; `dsh run [--profile <name>] "task"` owns one-shot execution and defaults to the headless profile, while generic `dsh --profile <name>` boots without a task; `dsh --config` is removed (its uses migrate to `--patch`). `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` after `add`/`remove` (a bundle-less package warns and stays a plain dependency).
The [`dsh run` command decision](../feature/2026-08-08-dsh-run-headless-command.md) owns the one-shot grammar; this note owns the profile composition it selects.
Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch). Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
@@ -12,7 +12,9 @@ Status: implemented
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。 一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。
已交付的组合改造成了组合包:`@deepseek-ai/dsh-base`(原有基础行合并为一次插入)、`@deepseek-ai/dsh-web-app`(原 web overlay,外加一个接管原启动器代码的运行时粘合插件——前端 dist 解析、web 表层提示词段落、bash 运行时变量、URL 行)、`@deepseek-ai/dsh-headless`(叠加在 base + web-app 之上的一次性 runner 插件)。`dsh web` 保留为携带 Web flag 家族的 `--profile web` 别名;`dsh --profile headless "task"` 取代 `-p``dsh --config` 被移除(其用途迁移到 `--patch`)。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并在 `add``remove` 后调和 `dsh.profile.bundles`(没有组合包声明的包会给出警告,保持为普通依赖)。 已交付的组合改造成了组合包:`@deepseek-ai/dsh-base`(原有基础行合并为一次插入)、`@deepseek-ai/dsh-web-app`(原 web overlay,外加一个接管原启动器代码的运行时粘合插件——前端 dist 解析、web 表层提示词段落、bash 运行时变量、URL 行)、`@deepseek-ai/dsh-headless`(叠加在 base + web-app 之上的一次性 runner 插件)。`dsh web` 保留为携带 Web flag 家族的 `--profile web` 别名;`dsh run [--profile <name>] "task"` 负责一次性执行,默认使用 headless profile,而通用的 `dsh --profile <name>` 只启动 profile,不携带任务`dsh --config` 被移除(其用途迁移到 `--patch`)。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并在 `add``remove` 后调和 `dsh.profile.bundles`(没有组合包声明的包会给出警告,保持为普通依赖)。
[`dsh run` 命令决策](../feature/2026-08-08-dsh-run-headless-command.md)负责一次性语法;本 Agent Note 负责该语法所选择的 profile 组合。
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。 解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md
2026-07-20-error-cause-chain-diagnostics.md: 7de1f4f631cec90048ccc8eab7a6654560d84846 2026-07-20-error-cause-chain-diagnostics.md: 89edd4529cc16617fdca736dbbea4b170400d6bd
2026-07-20-error-cause-chain-diagnostics.zh.md: 6ce642eb4a18a1a412fb63c3bb00042a921f4da3 2026-07-20-error-cause-chain-diagnostics.zh.md: 673921c913a9f1fefe6f85303cf5952f8edbcc1d
@@ -34,4 +34,4 @@ A TUI run against an unreachable DeepSeek endpoint failed with the single notice
- Durable `turn/end` error messages include cause detail. Existing snapshot fixtures replay byte-identically because their scripted errors carry no `cause` (for such errors `errorChain(err)` equals `err.message`); only unit-test expectation strings changed. A fixture recorded from a real transport failure would carry the chain. - Durable `turn/end` error messages include cause detail. Existing snapshot fixtures replay byte-identically because their scripted errors carry no `cause` (for such errors `errorChain(err)` equals `err.message`); only unit-test expectation strings changed. A fixture recorded from a real transport failure would carry the chain.
- `errorChain` renders `message` without the class name (`String(error)` rendered `Error: <message>`), so a bare `TypeError` in a log line loses its type label unless its message is empty (then the name is the fallback). The chain detail was judged worth more than the class name at these seams. - `errorChain` renders `message` without the class name (`String(error)` rendered `Error: <message>`), so a bare `TypeError` in a log line loses its type label unless its message is empty (then the name is the fallback). The chain detail was judged worth more than the class name at these seams.
- `dsh-stdio` output for failed turns is no longer silent; piped consumers that parsed the transcript see new `[turn …]` lines. - `dsh-stdio` output for failed turns is no longer silent; piped consumers that parsed the transcript see new `[turn …]` lines.
- Remaining `renderThrown` copies in `dsh-subagent`, `dsh-workflow`, `dsh-skill`, `dsh-workflow-workerthread`, and `cli-demo` still render without the chain; they wrap package-local errors that carry their own messages, and can adopt `errorChain` when their diagnostics prove insufficient. - Remaining `renderThrown` copies in `dsh-subagent`, `dsh-workflow`, `dsh-skill`, and `dsh-workflow-workerthread` still render without the chain; they wrap package-local errors that carry their own messages, and can adopt `errorChain` when their diagnostics prove insufficient.
@@ -34,4 +34,4 @@ TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed`
- 持久化的 `turn/end` 错误消息包含 cause 细节。现有快照 fixture(测试前置数据)字节级一致地回放,因为其脚本化错误不带 `cause`(对这类错误 `errorChain(err)` 等于 `err.message`);只有单元测试的期望字符串有变化。从真实传输失败录制的 fixture 会携带完整链。 - 持久化的 `turn/end` 错误消息包含 cause 细节。现有快照 fixture(测试前置数据)字节级一致地回放,因为其脚本化错误不带 `cause`(对这类错误 `errorChain(err)` 等于 `err.message`);只有单元测试的期望字符串有变化。从真实传输失败录制的 fixture 会携带完整链。
- `errorChain` 渲染 `message` 而不带类名(`String(error)` 会渲染 `Error: <message>`),因此日志行里的裸 `TypeError` 会丢失类型标签,除非消息为空(此时回退到类名)。在这些诊断界面上,链细节被判断为比类名更有价值。 - `errorChain` 渲染 `message` 而不带类名(`String(error)` 会渲染 `Error: <message>`),因此日志行里的裸 `TypeError` 会丢失类型标签,除非消息为空(此时回退到类名)。在这些诊断界面上,链细节被判断为比类名更有价值。
- `dsh-stdio` 对失败轮次的输出不再沉默;解析 transcript 的管道消费方会看到新的 `[turn …]` 行。 - `dsh-stdio` 对失败轮次的输出不再沉默;解析 transcript 的管道消费方会看到新的 `[turn …]` 行。
- `dsh-subagent``dsh-workflow``dsh-skill``dsh-workflow-workerthread``cli-demo` 里剩余的 `renderThrown` 副本仍不渲染链;它们包装的是自带消息的包内错误,等诊断信息证明不足时再采用 `errorChain` - `dsh-subagent``dsh-workflow``dsh-skill``dsh-workflow-workerthread` 里剩余的 `renderThrown` 副本仍不渲染链;它们包装的是自带消息的包内错误,等诊断信息证明不足时再采用 `errorChain`
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md
2026-08-03-cli-signal-shutdown-escalation.md: 2746b5784baad0f3b14258280cd56a621db07c15 2026-08-03-cli-signal-shutdown-escalation.md: 7c9715c37ee57be9fa0f67af0c19f0bfa84845da
2026-08-03-cli-signal-shutdown-escalation.zh.md: 0bda83327d4cc8fe2edb61f8145a89138610901e 2026-08-03-cli-signal-shutdown-escalation.zh.md: f3485edc9e453c0b774f442bfce6d678d63f2224
@@ -6,9 +6,9 @@ English | [中文](2026-08-03-cli-signal-shutdown-escalation.zh.md)
## Problem ## Problem
The default telemetry mount added SIGINT/SIGTERM handlers to `dsh web` and `dsh -p` so process exit could drain the Cordis tree instead of dropping queued telemetry. Each handler used a one-way boolean latch and exited only after `ctx.fiber.dispose()` settled. Headless normal completion also awaited that disposal without a bound. The default telemetry mount added SIGINT/SIGTERM handlers to `dsh web` and the headless command (now `dsh run`) so process exit could drain the Cordis tree instead of dropping queued telemetry. Each handler used a one-way boolean latch and exited only after `ctx.fiber.dispose()` settled. Headless normal completion also awaited that disposal without a bound.
A user then reproduced `dsh -p` hanging immediately after the observation URL and ignoring repeated `Ctrl+C`; `DSH_TELEMETRY_DISABLED=1` removed the hang, while a standalone Node handler in the same Linux sandbox received SIGINT. This isolated the pending disposer to telemetry rather than terminal signal forwarding. OTel's `BatchLogRecordProcessor.shutdown()` awaits `exporter.forceFlush()` before the `exportTimeoutMillis`-bounded completion promise, and the OTLP exporter's `forceFlush()` waits directly on its in-flight HTTP Promise. A proxy/sandbox connection that never obtains a socket can therefore leave provider shutdown pending despite both configured SDK timeouts. A user then reproduced the headless command hanging immediately after the observation URL and ignoring repeated `Ctrl+C`; `DSH_TELEMETRY_DISABLED=1` removed the hang, while a standalone Node handler in the same Linux sandbox received SIGINT. This isolated the pending disposer to telemetry rather than terminal signal forwarding. OTel's `BatchLogRecordProcessor.shutdown()` awaits `exporter.forceFlush()` before the `exportTimeoutMillis`-bounded completion promise, and the OTLP exporter's `forceFlush()` waits directly on its in-flight HTTP Promise. A proxy/sandbox connection that never obtains a socket can therefore leave provider shutdown pending despite both configured SDK timeouts.
The latch then turned that telemetry defect into an unkillable CLI: normal completion was already awaiting the single-shot root disposal; the first SIGINT joined the same pending disposal and set the signal latch; later SIGINTs returned at the latch, so the process had no remaining escape. A signal received before normal completion had the same unbounded wait. Web used the same latch shape. The latch then turned that telemetry defect into an unkillable CLI: normal completion was already awaiting the single-shot root disposal; the first SIGINT joined the same pending disposal and set the signal latch; later SIGINTs returned at the latch, so the process had no remaining escape. A signal received before normal completion had the same unbounded wait. Web used the same latch shape.
@@ -6,9 +6,9 @@
## 问题 ## 问题
默认挂载遥测后,`dsh web``dsh -p` 新增了 SIGINT/SIGTERM 处理器,使进程退出时可以排空 Cordis 插件树,而不是丢弃排队中的遥测数据。每个处理器都使用单向布尔闩锁(latch),并且只有在 `ctx.fiber.dispose()` 结算后才退出。headless 正常完成时同样会无界等待整棵树执行 dispose(资源释放)。 默认挂载遥测后,`dsh web` headless 命令(现为 `dsh run`新增了 SIGINT/SIGTERM 处理器,使进程退出时可以排空 Cordis 插件树,而不是丢弃排队中的遥测数据。每个处理器都使用单向布尔闩锁(latch),并且只有在 `ctx.fiber.dispose()` 结算后才退出。headless 正常完成时同样会无界等待整棵树执行 dispose(资源释放)。
随后有用户复现,`dsh -p` 在打印观察 URL 后立即卡死,重复按 `Ctrl+C` 也没有反应;设置 `DSH_TELEMETRY_DISABLED=1` 后不再卡死,而同一 Linux 沙箱中的独立 Node 信号处理器能够收到 SIGINT。这将待结算的 disposer 定位到遥测,而非终端信号转发。OTel 的 `BatchLogRecordProcessor.shutdown()` 会先等待 `exporter.forceFlush()`,再进入受 `exportTimeoutMillis` 限制的完成 promiseOTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此,代理/沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。 随后有用户复现,headless 命令在打印观察 URL 后立即卡死,重复按 `Ctrl+C` 也没有反应;设置 `DSH_TELEMETRY_DISABLED=1` 后不再卡死,而同一 Linux 沙箱中的独立 Node 信号处理器能够收到 SIGINT。这将待结算的 disposer 定位到遥测,而非终端信号转发。OTel 的 `BatchLogRecordProcessor.shutdown()` 会先等待 `exporter.forceFlush()`,再进入受 `exportTimeoutMillis` 限制的完成 promiseOTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此,代理/沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。
闩锁随后把这个遥测缺陷变成无法终止的 CLI(命令行界面):正常完成流程已经在等待单次根级 dispose;第一次 SIGINT 会加入同一个待结算的 dispose,并设置信号闩锁;后续 SIGINT 在闩锁处直接返回,因此进程再无退出途径。正常完成之前收到信号时,同样会陷入无界等待。Web 使用的闩锁结构与此相同。 闩锁随后把这个遥测缺陷变成无法终止的 CLI(命令行界面):正常完成流程已经在等待单次根级 dispose;第一次 SIGINT 会加入同一个待结算的 dispose,并设置信号闩锁;后续 SIGINT 在闩锁处直接返回,因此进程再无退出途径。正常完成之前收到信号时,同样会陷入无界等待。Web 使用的闩锁结构与此相同。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.md
2026-08-08-dsh-run-headless-command.md: d1cc0573d1d94bb7b30e98aa685d2b7d89878a99
2026-08-08-dsh-run-headless-command.zh.md: 7b48dfd708efb197babcfcbeaf4f8a4ca76cde3f
@@ -0,0 +1,41 @@
# Agent Note: `dsh run` owns one-shot headless execution
Status: implemented
English | [中文](2026-08-08-dsh-run-headless-command.zh.md)
## Problem
The product launcher attached optional task text to its generic profile boot: `dsh --profile headless "task"`. That made one argv shape mean either a long-lived profile or a one-shot run according to a row discovered only after composition. The parser's `ProfileInvocation` carried optional task state, help presented a profile implementation detail as the user command, and a custom profile could accept a task only through the same overloaded root.
The former `dsh -p` spelling was already absent from the parser, so restoring it or detecting it specially would add compatibility machinery to a pre-release interface. A separate application-file proposal also used the `run` verb, leaving two incompatible owners for one top-level command.
## Decision
One-shot execution owns an explicit grammar:
```text
dsh run [--profile <name>] [--patch <path>...] <task...>
```
`--profile` defaults to `headless` and remains available for custom one-shot compositions. `--patch` is repeatable and occupies the existing overlay layer. Commander joins the variadic task arguments with spaces and rejects a missing or blank task before boot.
The [profile plugin bundle decision](../architecture/2026-08-05-profile-plugin-bundles.md) owns the composition selected by this grammar.
`RunInvocation` is a separate `DshInvocation` member. The generic profile invocation no longer carries task text, and its root command accepts no positional arguments. Both dispatch paths call the existing deep `runProfile` module: `profile` omits `task`, while `run` supplies it. There is no shallow `run.ts` forwarding module and no alias, warning, or custom detector for former spellings; they fail through the ordinary Commander grammar. A one-shot profile without `headless-runner` still fails through the existing composed-row check, while booting a profile that contains that row without a task points to `dsh run --profile <name> "<task>"`.
The `run` verb belongs to one-shot task execution. Launching an application file must choose another command name; two top-level meanings selected by positional shape would recreate the ambiguity this command removes.
The runner's user-visible contract stays the same: a fresh persisted session, browser observation URL on stderr, final assistant text on stdout, completed/non-completed exit mapping, and bounded signal shutdown. The product-level keyless acceptance exposed that the in-process mux consumer could lag the same-process `agent/status: idle` notification and derive output before reading the final frames. The idle notification now captures the authoritative final session sequence, and the runner waits until the ordered mux reaches that boundary (or the stream ends) before deriving text and exit reason. This enforces the existing idle-to-idle contract without adding a wire field or a timing delay.
## Alternatives considered
- **Keep task text on `dsh --profile`.** Rejected because profile boot and one-shot execution remain one grammar whose meaning depends on a late composition check.
- **Preserve `dsh -p` or the positional profile form as aliases.** Rejected under the pre-release stance: compatibility branches would outlive the interface they were meant to retire.
- **Make `--profile headless` mandatory under `run`.** Rejected because the shipped one-shot surface should have the shortest canonical spelling, while optional `--profile` preserves plugin-defined one-shot compositions.
- **Give `dsh run` to application-file launch and choose another headless verb.** Rejected because `run` describes executing a task through the harness; application-file ownership would make the product's primary one-shot command less direct and collide with custom one-shot profiles.
- **Add `apps/cli/src/run.ts`.** Rejected because it would only forward to `runProfile`, splitting command ownership without hiding any complexity.
## Consequences
This is an intentional breaking CLI change. Documentation, help, parser tests, built-bin acceptance, PTY shutdown coverage, and the assembled keyless snapshot use `dsh run`. Existing custom one-shot profiles keep working through `--profile`; long-lived profiles and config dumps retain their existing root grammar. The competing application-file command must be renamed and rebased separately rather than sharing or overloading `run`.
@@ -0,0 +1,41 @@
# Agent Note: `dsh run` 负责一次性 headless 执行
Status: implemented
[English](2026-08-08-dsh-run-headless-command.md) | 中文
## 问题
产品启动器过去把可选任务文本挂在通用 profile 启动命令上:`dsh --profile headless "task"`。于是,同一种 argv 形态会表示常驻 profile 或一次性运行,具体含义取决于组合完成后才发现的配置行。解析器的 `ProfileInvocation` 携带可选任务状态,帮助信息把 profile 的实现细节呈现为用户命令,自定义 profile 也只能通过同一个过载的根命令接收任务。
解析器中已经没有原来的 `dsh -p` 写法,因此恢复该写法或加入特殊检测,会给预发布接口增加兼容机制。另一个应用文件提案也使用 `run` 动词,使同一个顶层命令同时归属两个互不兼容的功能。
## 决策
一次性执行采用明确语法:
```text
dsh run [--profile <name>] [--patch <path>...] <task...>
```
`--profile` 默认为 `headless`,同时保留对自定义一次性组合的支持。`--patch` 可重复使用,并沿用既有 overlay 层的位置。Commander 用空格拼接可变数量的任务参数,并在启动前拒绝缺失或空白任务。
[profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)负责该语法所选择的组合。
`RunInvocation` 是单独的 `DshInvocation` 成员。通用 profile 调用不再携带任务文本,其根命令也不接受位置参数。两条分派路径都调用已有的深层 `runProfile` 模块:`profile` 省略 `task``run` 则提供该字段。实现中没有只负责转发的浅层 `run.ts` 模块,也没有面向旧写法的别名、警告或自定义检测器;旧写法会按普通 Commander 语法失败。缺少 `headless-runner` 的一次性 profile 仍会触发既有的组合行检查;如果启动的 profile 包含该行却未提供任务,错误会指向 `dsh run --profile <name> "<task>"`
`run` 动词只负责一次性任务执行。应用文件启动必须选择其他命令名;如果让两个顶层含义由位置参数形态决定,就会重新引入本命令消除的歧义。
运行器面向用户的契约保持不变:创建新的持久化会话,在 stderr 打印浏览器观察 URL,在 stdout 打印最终 assistant 文本,将完成/未完成映射为退出状态,并执行有界的信号关闭。产品级无密钥验收用例发现,进程内 mux 消费方可能落后于同进程的 `agent/status: idle` 通知,在读到最终帧之前就生成输出。idle 通知现在会捕获权威的会话最终事件序号,运行器则等待有序 mux 到达该边界(或流结束),再生成文本和退出原因。这一机制在不增加 wire 字段或定时延迟的前提下,落实了既有的 idle-to-idle 契约。
## 考虑过的替代方案
- **把任务文本保留在 `dsh --profile` 上。** 不予采纳:profile 启动和一次性执行仍共用同一套语法,其含义取决于较晚发生的组合检查。
- **保留 `dsh -p` 或位置参数 profile 形式作为别名。** 不予采纳:根据预发布立场,这些兼容分支会比本应退役的接口存续更久。
- **要求在 `run` 下必须指定 `--profile headless`。** 不予采纳:已交付的一次性接口应采用最短的规范写法,同时用可选的 `--profile` 保留插件定义的一次性组合。
- **把 `dsh run` 交给应用文件启动,并为 headless 选择另一个动词。** 不予采纳:`run` 描述的是通过 harness 执行任务;若归应用文件所有,产品的主要一次性命令会更不直接,并与自定义一次性 profile 冲突。
- **新增 `apps/cli/src/run.ts`。** 不予采纳:它只会转发到 `runProfile`,拆分命令归属,却没有隐藏任何复杂度。
## 后果
这是一次有意为之的 CLI(命令行界面)破坏性变更。文档、帮助信息、解析器测试、构建后二进制验收、PTY 关闭覆盖和组装应用的无密钥快照都使用 `dsh run`。现有自定义一次性 profile 可继续通过 `--profile` 工作;常驻 profile 和配置 dump 保留既有的根命令语法。与之竞争的应用文件命令必须单独改名并 rebase,不得共享或重载 `run`
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-node-engine-floor.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-node-engine-floor.md
2026-07-06-node-engine-floor.md: ef047d885a442106a35922f4716d2996d8a98ca7 2026-07-06-node-engine-floor.md: e42b809f83fe388526ecbd74192989e0bcdbba67
2026-07-06-node-engine-floor.zh.md: c409b006baa3451eb5fc5260be4e14ffe88776c8 2026-07-06-node-engine-floor.zh.md: 92fa00f7a270f4506cba6c899e9edf6a2fdd3209
@@ -15,7 +15,7 @@ Set `engines.node` to `^22.19.0 || >=24.0.0` and test keyless CI on `['22.19', 2
Two Node features gate the source runtime: Two Node features gate the source runtime:
- **`node:sqlite`** — `packages/session-persistence/session-persistence-sqlite` does a top-level `import { DatabaseSync } from 'node:sqlite'`. The module dropped its `--experimental-sqlite` flag requirement at **22.13** (LTS) and **23.4** (Current); before those, importing it throws at load. - **`node:sqlite`** — `packages/session-persistence/session-persistence-sqlite` does a top-level `import { DatabaseSync } from 'node:sqlite'`. The module dropped its `--experimental-sqlite` flag requirement at **22.13** (LTS) and **23.4** (Current); before those, importing it throws at load.
- **Native TypeScript type-stripping** — the built-mode `examples/headless-agent/tests/keyless-smoke.e2e.ts` smoke boots `dsh-cli-demo`'s published `lib/bin.js` under plain `node` (no tsx) and loads the example's `.ts` test adapter (`cli-mock-llm.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`. - **Native TypeScript type-stripping** — the built-mode `examples/headless-agent/tests/keyless-smoke.e2e.ts` smoke boots its unexported `.ts` driver under plain `node` (no tsx) and loads the example's `.ts` test adapter (`cli-mock-llm.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`.
Those source features clear on the 22.x line at **22.18**, but the installed Pi adapter dependency raises the advertised LTS floor. `@deepseek-ai/dsh-llm-pi-ai` depends on `@earendil-works/pi-ai@0.79.3`, whose package declares `engines.node >=22.19.0`, so the LTS floor is **22.19**. The 24.x branch remains `>=24.0.0`. The disjoint range excludes Node 23 entirely: Node 23.023.5 still has at least one flagged source feature, and the 23 line is non-LTS/EOL, so advertising `>=23.6` would add a dead release line and a CI leg no deployment should use. Those source features clear on the 22.x line at **22.18**, but the installed Pi adapter dependency raises the advertised LTS floor. `@deepseek-ai/dsh-llm-pi-ai` depends on `@earendil-works/pi-ai@0.79.3`, whose package declares `engines.node >=22.19.0`, so the LTS floor is **22.19**. The 24.x branch remains `>=24.0.0`. The disjoint range excludes Node 23 entirely: Node 23.023.5 still has at least one flagged source feature, and the 23 line is non-LTS/EOL, so advertising `>=23.6` would add a dead release line and a CI leg no deployment should use.
@@ -25,7 +25,7 @@ Those source features clear on the 22.x line at **22.18**, but the installed Pi
- The advertised LTS branch no longer undercuts the Pi adapter dependency floor. - The advertised LTS branch no longer undercuts the Pi adapter dependency floor.
- CI proves the Node 22 LTS floor directly with Node 22.19, keeps primary coverage on `node: 24`, and exercises Node 26 as the next even line; focused compatibility smokes run on all three versions. - CI proves the Node 22 LTS floor directly with Node 22.19, keeps primary coverage on `node: 24`, and exercises Node 26 as the next even line; focused compatibility smokes run on all three versions.
- The built-bin smoke needs no version-conditional flag: at 22.19 type-stripping is already the default, so the test stays the plain `node lib/bin.js` path it documents. - The built-mode smoke needs no version-conditional flag: at 22.19 type-stripping is already the default, so the example-owned TypeScript driver stays a plain `node fixture.ts` path.
- A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this Agent Note in the same change. - A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this Agent Note in the same change.
## Alternatives considered ## Alternatives considered
@@ -15,7 +15,7 @@ Status: implemented
两个 Node 特性决定了源码运行时的门槛: 两个 Node 特性决定了源码运行时的门槛:
- **`node:sqlite`**`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**LTS)和 **23.4**Current)取消了 `--experimental-sqlite` 标志要求;在此之前,导入它会在加载时抛出异常。 - **`node:sqlite`**`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**LTS)和 **23.4**Current)取消了 `--experimental-sqlite` 标志要求;在此之前,导入它会在加载时抛出异常。
- **原生 TypeScript 类型剥离**——构建模式的 `examples/headless-agent/tests/keyless-smoke.e2e.ts` 冒烟测试使用纯 `node`(无 tsx)启动 `dsh-cli-demo` 已发布的 `lib/bin.js`,并加载示例的 `.ts` 测试适配器(`cli-mock-llm.ts`)。类型剥离从 **22.18**LTS)和 **23.6**(Current)起成为默认行为;更早版本需要 `--experimental-strip-types` - **原生 TypeScript 类型剥离**——构建模式的 `examples/headless-agent/tests/keyless-smoke.e2e.ts` 冒烟测试使用纯 `node`(无 tsx)启动该示例未导出的 `.ts` driver,并加载示例的 `.ts` 测试适配器(`cli-mock-llm.ts`)。类型剥离从 **22.18**LTS)和 **23.6**(Current)起成为默认行为;更早版本需要 `--experimental-strip-types`
这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步提高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的包声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不相交范围完全排除了 Node 23:Node 23.0–23.5 至少还有一个源码特性需要标志,而 23 线是非 LTS/已 EOL 的,宣传 `>=23.6` 会增加一条已终止的发布线和一条 CI 分支,而没有任何部署应当使用它。 这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步提高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的包声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不相交范围完全排除了 Node 23:Node 23.0–23.5 至少还有一个源码特性需要标志,而 23 线是非 LTS/已 EOL 的,宣传 `>=23.6` 会增加一条已终止的发布线和一条 CI 分支,而没有任何部署应当使用它。
@@ -25,7 +25,7 @@ Status: implemented
- 宣传的 LTS 分支不再低于 Pi 适配器依赖的下限。 - 宣传的 LTS 分支不再低于 Pi 适配器依赖的下限。
- CI 通过 Node 22.19 直接验证 Node 22 LTS 下限,将主要覆盖率任务保留在 `node: 24`,并用 Node 26 验证下一个偶数线;三个版本均运行聚焦的兼容性冒烟测试。 - CI 通过 Node 22.19 直接验证 Node 22 LTS 下限,将主要覆盖率任务保留在 `node: 24`,并用 Node 26 验证下一个偶数线;三个版本均运行聚焦的兼容性冒烟测试。
- built-bin 冒烟测试无需版本条件标志:在 22.19 上类型剥离已是默认行为,因此测试保持其文档所述的纯 `node lib/bin.js` 路径。 - 构建模式冒烟测试无需版本条件标志:在 22.19 上类型剥离已是默认行为,因此示例自有的 TypeScript driver 保持使用纯 `node fixture.ts` 路径。
- 未来若依赖或源码 API 提高运行时下限,必须在同一变更中同步调整 `engines.node`、兼容性矩阵和本 Agent Note。 - 未来若依赖或源码 API 提高运行时下限,必须在同一变更中同步调整 `engines.node`、兼容性矩阵和本 Agent Note。
## 曾考虑的替代方案 ## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
2026-07-20-remove-stdio-and-echo-agents.md: 1c7ff4b5337341ea7678c1455dd7bbd3365df6ba 2026-07-20-remove-stdio-and-echo-agents.md: 256e626f4ff41016d0227eef7cc3e4e51e15058b
2026-07-20-remove-stdio-and-echo-agents.zh.md: 67445d25f6a0534c92c50568da898a5bffc73a52 2026-07-20-remove-stdio-and-echo-agents.zh.md: bb7a2abd8be73ac3ea089121592b3f26162bf619
@@ -19,18 +19,18 @@ The stdio and Echo agents are removed without compatibility packages, modes, com
The remaining application roles are explicit: The remaining application roles are explicit:
- `@deepseek-ai/dsh-tui` owns terminal-interactive execution. It rejects non-TTY streams before Loader boot; `apps/cli/config/base.cordis.yml` plus the `tui.cordis.yml` overlay own the complete coding composition, with PTY plus terminal-snapshot coverage in `apps/cli/tests/`. - `@deepseek-ai/dsh-tui` owns terminal-interactive execution. It rejects non-TTY streams before Loader boot; `apps/cli/config/base.cordis.yml` plus the `tui.cordis.yml` overlay own the complete coding composition, with PTY plus terminal-snapshot coverage in `apps/cli/tests/`.
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) owns non-interactive execution, including pipes. `examples/headless-agent` owns the real-model one-shot composition, replay snapshots, generic real-agent suites, and test-only keyless Loader fixtures. - [`dsh run`](../../../../apps/cli/README.md) owns non-interactive execution. Its `headless` profile is the product composition; `examples/headless-agent` owns replay snapshots, generic real-agent suites, and an unexported keyless Loader driver.
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations. - [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations.
The SDK project model and create/config workflows replace the `stdio` run-interface option with `tui`; generated TUI projects compose `@deepseek-ai/dsh-tui` and create or resume one exact session. Repository-facing demo documentation requires a DeepSeek API key and leads with the real Headless or TUI agents. The SDK project model and create/config workflows replace the `stdio` run-interface option with `tui`; generated TUI projects compose `@deepseek-ai/dsh-tui` and create or resume one exact session. Repository-facing demo documentation requires a DeepSeek API key and leads with the real Headless or TUI agents.
Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapter to exercise a real tool round trip, the CLI built-bin suite pins output, persistence, failure, and signal semantics, and package-specific Loader tests keep deterministic adapters beside their scenarios. None is exposed as a runnable mock agent. Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapter to exercise a real tool round trip, the `dsh` built-bin suite pins the published one-shot entry and output, the product Headless snapshot pins persistence, and the Headless PTY shutdown e2e pins signal escalation. Package-specific Loader tests keep deterministic adapters beside their scenarios. None is exposed as a runnable mock agent.
## Verification ## Verification
TUI and Headless Loader coverage run the real app packages in source and built modes. PTY-driven subprocess coverage is reserved for the TUI lifecycle; other entry-point smokes use the one-shot pipe protocol. Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, SDK-interface, `createStdioChat`, and `StdioRuntime` references. TUI and Headless Loader coverage run the real app packages in source and built modes. PTY-driven subprocess coverage is reserved for the TUI lifecycle; other entry-point smokes use the one-shot pipe protocol. Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, SDK-interface, `createStdioChat`, and `StdioRuntime` references.
The built `dsh` bin rejects a piped TUI launch before Loader boot and points at its one-shot `-p` mode; `apps/cli/tests/built-bin.e2e.ts` pins that path, while `cli-demo`'s built-bin suite runs text, JSON, and structurally parsed `stream-json` output under plain Node, persists fresh sessions, and rejects invalid arguments and missing config without contaminating stdout. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the real Headless composition for two ordered turns, while its package tests own finer elapsed-time behavior. The built `dsh` bin rejects a piped TUI launch before Loader boot and points at `dsh run`; `apps/cli/tests/built-bin.e2e.ts` pins the product one-shot entry under plain Node, including output and invalid arguments. `examples/headless-agent/tests/headless.snapshot.ts` pins product persistence, while `apps/cli/tests/headless-shutdown.e2e.ts` owns bounded signal escalation. The headless example's test-only JSONL driver preserves assembled canonical-event snapshots without creating a second CLI contract. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the explicit Headless test composition for two ordered turns, while its package tests own finer elapsed-time behavior.
## Alternatives considered ## Alternatives considered
@@ -19,18 +19,18 @@ DeepSeek Harness 在 TUI 和 Headless coding agent 之外,还提供了两个
保留的应用角色均有明确归属: 保留的应用角色均有明确归属:
- `@deepseek-ai/dsh-tui` 负责终端交互式执行。它会在 Loader 启动前拒绝非 TTY 流;`apps/cli/config/base.cordis.yml``tui.cordis.yml` overlay 拥有完整 coding 组装,PTY 与终端快照覆盖则位于 `apps/cli/tests/` - `@deepseek-ai/dsh-tui` 负责终端交互式执行。它会在 Loader 启动前拒绝非 TTY 流;`apps/cli/config/base.cordis.yml``tui.cordis.yml` overlay 拥有完整 coding 组装,PTY 与终端快照覆盖则位于 `apps/cli/tests/`
- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) 负责非交互式执行,包括管道方式。`examples/headless-agent` 拥有真实模型的单次任务组装、回放快照、通用真实 agent 测试套件,以及仅供测试使用的无密钥 Loader fixture - [`dsh run`](../../../../apps/cli/README.md) 负责非交互式执行。其 `headless` profile 是产品组装;`examples/headless-agent` 负责回放快照、通用真实 agent 测试套件和未导出的无密钥 Loader driver
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。 - [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。
SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换为 `tui`;生成的 TUI 工程组合 `@deepseek-ai/dsh-tui`,并创建或恢复一个确切会话。仓库中的演示文档要求 DeepSeek API key,并优先引导到真实的 Headless 或 TUI agent。 SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换为 `tui`;生成的 TUI 工程组合 `@deepseek-ai/dsh-tui`,并创建或恢复一个确切会话。仓库中的演示文档要求 DeepSeek API key,并优先引导到真实的 Headless 或 TUI agent。
无密钥验证由测试负责。Headless Loader 冒烟测试使用 fixture 适配器验证真实工具往返;CLI built-bin 测试套件固定输出、持久化、失败和信号语义;各包专属的 Loader 测试则将确定性适配器放在对应场景旁。其中任何一项都不会作为可运行的 mock agent 对外暴露。 无密钥验证由测试负责。Headless Loader 冒烟测试使用 fixture 适配器验证真实工具往返;`dsh` built-bin 测试套件固定已发布的一次性入口和输出;产品 Headless 快照固定持久化;Headless PTY 关闭 e2e 固定信号升级。各包专属的 Loader 测试则将确定性适配器放在对应场景旁。其中任何一项都不会作为可运行的 mock agent 对外暴露。
## 验证 ## 验证
TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat``StdioRuntime` 引用。 TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat``StdioRuntime` 引用。
构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向其单次 `-p` 模式`apps/cli/tests/built-bin.e2e.ts` 固定了该执行路径,而 `cli-demo` 的 built-bin 套件在普通 Node 下运行文本、JSON 和经过结构化解析的 `stream-json` 输出,持久化新建会话,并在不污染 stdout 的情况下拒绝无效参数和缺失配置。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过真实 Headless 组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。 构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向 `dsh run``apps/cli/tests/built-bin.e2e.ts` 在普通 Node 下固定产品的一次性入口,包括输出和无效参数。`examples/headless-agent/tests/headless.snapshot.ts` 固定产品持久化,`apps/cli/tests/headless-shutdown.e2e.ts` 则负责有界信号升级。headless 示例仅供测试的 JSONL driver 保留组装后的规范事件快照,而不会创建第二套 CLI(命令行界面)契约。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过显式的 Headless 测试组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
## 曾考虑的替代方案 ## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-04-remove-tui-package.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-04-remove-tui-package.md
2026-08-04-remove-tui-package.md: 7f7a0dd86ddd36e940ed8b7d6154185d9740341c 2026-08-04-remove-tui-package.md: 1057243c70f6f2775a5d0c5f5eddcb72cbad699e
2026-08-04-remove-tui-package.zh.md: 36cb4b6a5e4eddd152eccee92c913bcca5b7fbae 2026-08-04-remove-tui-package.zh.md: 0e03d6913aafaa3ce01c0f5732935d2c304c8e0e
@@ -16,7 +16,7 @@ The `packages/ui/tui` package is deleted without a compatibility package or alia
The SDK run-interface union now contains only `acp` and `embed`. `create-sdk` defaults to ACP, generated templates contain no terminal startup, resume, session-environment, or model-argument branch, and the builtin `ask-user` feature is removed because neither remaining generated interface supplies a `UserInteractionProvider`. Host applications may still mount the provider-neutral `dsh-user-interaction`, `dsh-commands`, and presentation seams directly. The SDK run-interface union now contains only `acp` and `embed`. `create-sdk` defaults to ACP, generated templates contain no terminal startup, resume, session-environment, or model-argument branch, and the builtin `ask-user` feature is removed because neither remaining generated interface supplies a `UserInteractionProvider`. Host applications may still mount the provider-neutral `dsh-user-interaction`, `dsh-commands`, and presentation seams directly.
This decision supersedes the reusable-package retention in [the explicit-config `dsh` entrypoint decision](2026-08-03-explicit-config-dsh-entrypoint.md) and the current applicability of the archived TUI implementation notes. Their historical records remain frozen, but they are not authority for the supported package or application inventory. This decision supersedes the reusable-package retention in [the explicit-config `dsh` entrypoint decision](../../archived/simplification/2026-08-03-explicit-config-dsh-entrypoint.md) and the current applicability of the archived TUI implementation notes. Their historical records remain frozen, but they are not authority for the supported package or application inventory.
This note consolidates the deleted package-only records that could not remain current after removal. The terminal UI had kept session identity visible during long conversations, removed duplicate model labels, attached elapsed timing and phase status to messages, showed workspace and branch context beside the prompt, and conservatively parsed complete XML wrappers for human-readable fallback output. Those choices improved one terminal frontend but do not justify retaining it without a deployment. A future XML fallback must still use a real parser rather than regular expressions. This note consolidates the deleted package-only records that could not remain current after removal. The terminal UI had kept session identity visible during long conversations, removed duplicate model labels, attached elapsed timing and phase status to messages, showed workspace and branch context beside the prompt, and conservatively parsed complete XML wrappers for human-readable fallback output. Those choices improved one terminal frontend but do not justify retaining it without a deployment. A future XML fallback must still use a real parser rather than regular expressions.
@@ -16,7 +16,7 @@ Status: implemented
SDK 的运行接口联合类型现在只包含 `acp``embed``create-sdk` 默认使用 ACP,生成的模板不再包含终端启动、恢复、会话环境或模型参数分支;内置的 `ask-user` 功能也被移除,因为剩余两个生成接口都不提供 `UserInteractionProvider`。宿主应用仍可直接挂载提供方无关的 `dsh-user-interaction``dsh-commands` 和呈现 seam。 SDK 的运行接口联合类型现在只包含 `acp``embed``create-sdk` 默认使用 ACP,生成的模板不再包含终端启动、恢复、会话环境或模型参数分支;内置的 `ask-user` 功能也被移除,因为剩余两个生成接口都不提供 `UserInteractionProvider`。宿主应用仍可直接挂载提供方无关的 `dsh-user-interaction``dsh-commands` 和呈现 seam。
本决策取代[显式配置 `dsh` 入口决策](2026-08-03-explicit-config-dsh-entrypoint.md)中保留可复用包的决定,也使已归档 TUI 实现记录不再适用于当前状态。这些历史记录继续保持冻结,但不再作为受支持包或应用清单的依据。 本决策取代[显式配置 `dsh` 入口决策](../../archived/simplification/2026-08-03-explicit-config-dsh-entrypoint.md)中保留可复用包的决定,也使已归档 TUI 实现记录不再适用于当前状态。这些历史记录继续保持冻结,但不再作为受支持包或应用清单的依据。
本记录汇总了删除后无法继续保持当前状态的仅限包记录。终端 UI 曾在长对话期间保持会话身份可见、移除重复模型标签、为消息附加耗时与阶段状态、在提示词旁显示 workspace 与分支上下文,并保守地解析完整 XML 包装层,以生成人类可读的回退输出。这些选择改善了一个终端前端,但没有部署时不足以证明应保留它。未来的 XML 回退仍必须使用真实解析器而非正则表达式。 本记录汇总了删除后无法继续保持当前状态的仅限包记录。终端 UI 曾在长对话期间保持会话身份可见、移除重复模型标签、为消息附加耗时与阶段状态、在提示词旁显示 workspace 与分支上下文,并保守地解析完整 XML 包装层,以生成人类可读的回退输出。这些选择改善了一个终端前端,但没有部署时不足以证明应保留它。未来的 XML 回退仍必须使用真实解析器而非正则表达式。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
2026-08-08-remove-cli-demo.md: c1153f5e9fcc89585e926f8f088829c849d1f6c1
2026-08-08-remove-cli-demo.zh.md: 1f9719b176c05009900a268a5bb2d2d7fcc79df3
@@ -0,0 +1,34 @@
# Agent Note: Remove the separate CLI demo
Status: implemented
English | [中文](2026-08-08-remove-cli-demo.zh.md)
## Problem
After [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two front doors also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
The replay suites still need canonical session events to pin assembled backend behavior. That testing need does not require a published command or compatibility contract.
## Decision
Delete `@deepseek-ai/dsh-cli-demo` completely: its package, bin, parser, app plugin, output formats, tests, workspace references, generated-catalog entries, and active documentation. No alias or compatibility package remains. The root `demo:headless` script is retained only as a direct alias of `dsh run`; the product command owns final-text stdout, the observation URL on stderr, persistence, exit status, and shutdown.
`examples/headless-agent` becomes an explicit test composition. Its Loader configs mount `@deepseek-ai/dsh-agent-spine-demo`, one root agent, JSONL persistence, and checkpoint policy as separate rows instead of hiding them behind an app bundle. The support-tier `@deepseek-ai/dsh-loader-smoke` package owns the shared direct-agent turn helper; unexported example-local drivers select their Loader configuration and render canonical events as JSONL. They are launched only by tests, have no bin, and do not define a supported product output format.
## Alternatives considered
- **Keep `dsh-cli-demo` as an alias or wrapper around `dsh run`.** Rejected because a second bin and package would preserve two discoverable owners without adding capability.
- **Move JSON and stream-JSON flags onto `dsh run`.** Rejected because no current product consumer requires them; adopting the old demo protocol would enlarge the canonical CLI contract solely to save test machinery.
- **Delete the canonical-event snapshots with the package.** Rejected because they pin model-visible assembled behavior that final-text product acceptance cannot observe.
- **Keep the app plugin but delete only its bin.** Rejected because the hidden composition would still duplicate the explicit headless profile and conceal which services the test leaf mounts.
## Consequences
This is intentionally breaking. `dsh-cli-demo`, its `--output-format` choices, and imports from `@deepseek-ai/dsh-cli-demo/src/cli.ts` no longer resolve. There is no public event-stream replacement in this change; callers use `dsh run` for one-shot execution and must choose an existing protocol surface when they need structured automation.
The repository retains backend replay coverage through test-only infrastructure, while product smoke and built-bin acceptance exercise `dsh run`. A separate one-shot package may return only if it owns a genuinely independent, versioned protocol that cannot belong to the product launcher; a second spelling or output shim is not enough.
## Verification
Focused Loader smokes cover the explicit composition in source and plain-Node built modes, snapshot tests diff its canonical JSONL and persisted logs, product acceptance covers `dsh run`, and documentation plus generated graph/catalog gates reject live references to the removed package. The frozen Agent Note archive remains historical evidence and is not rewritten.
@@ -0,0 +1,34 @@
# Agent Note: 移除独立的 CLI demo
Status: implemented
[English](2026-08-08-remove-cli-demo.md) | 中文
## 问题
在 [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) 成为产品的一次性命令后,`@deepseek-ai/dsh-cli-demo` 仍是承担同一工作的第二个应用包。它另行拥有一套可执行文件、参数语法、应用组装、取消生命周期、文本/JSON/stream-JSON 输出契约、构建产物、配套文档和测试套件。两个入口组装的树也不相同,因此 demo 成功不能证明已交付的 `headless` profile 可用,用户还必须在功能重叠的命令之间作出选择。
回放套件仍需要规范会话事件来固定组装后的后端行为。这一测试需求不需要已发布命令或兼容性契约。
## 决策
彻底删除 `@deepseek-ai/dsh-cli-demo`:包括它的包、bin、解析器、应用插件、输出格式、测试、workspace 引用、生成目录条目和现行文档。不保留别名或兼容包。根目录的 `demo:headless` 脚本仅作为 `dsh run` 的直接别名保留;stdout 上的最终文本、stderr 上的观察 URL、持久化、退出状态和关闭行为均由产品命令负责。
`examples/headless-agent` 成为显式测试组装。其 Loader 配置把 `@deepseek-ai/dsh-agent-spine-demo`、一个根 agent(智能体)、JSONL 持久化和检查点策略挂载为独立配置行,不再将其隐藏在应用组合包之后。支持层的 `@deepseek-ai/dsh-loader-smoke` 包负责共享的直接 agent 轮次 helper;未导出的示例本地 driver 选择各自的 Loader 配置,并将规范事件渲染为 JSONL。这些 driver 只由测试启动,不提供 bin,也不定义受支持的产品输出格式。
## 考虑过的替代方案
- **保留 `dsh-cli-demo` 作为 `dsh run` 的别名或包装层。** 不予采纳:第二个 bin 和包会让同一功能继续存在两个可发现的归属方,却没有增加任何能力。
- **把 JSON 和 stream-JSON 标志移到 `dsh run`。** 不予采纳:当前没有产品消费方需要这些标志;沿用旧 demo 协议,只会为了保留测试机制而扩大规范 CLI(命令行界面)契约。
- **随包一并删除规范事件快照。** 不予采纳:这些快照固定了模型可见的组装行为,而只检查最终文本的产品验收无法观察这些行为。
- **保留应用插件,只删除它的 bin。** 不予采纳:隐藏的组装仍会重复显式的 headless profile,并掩盖测试叶节点挂载了哪些服务。
## 后果
这是有意为之的破坏性变更。`dsh-cli-demo`、它的 `--output-format` 选项以及对 `@deepseek-ai/dsh-cli-demo/src/cli.ts` 的导入都不再可解析。本变更不提供公开的事件流替代接口;调用方使用 `dsh run` 执行一次性任务,需要结构化自动化时则必须选择现有的协议接口。
仓库通过仅供测试的基础设施保留后端回放覆盖,产品冒烟测试和 built-bin 验收则运行 `dsh run`。只有当独立的一次性包负责一套真正独立、带版本且不能归产品启动器所有的协议时,它才可以重新引入;第二种命令写法或输出 shim 并不足以构成理由。
## 验证
聚焦的 Loader 冒烟测试在源码模式和由普通 Node 启动的构建模式下覆盖显式组装,快照测试对比其规范 JSONL 和持久化日志,产品验收覆盖 `dsh run`,文档检查及生成图谱/目录门禁则拒绝对已移除包的活跃引用。冻结的 Agent Note 归档保留为历史证据,不会被重写。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write README.md # pnpm run verify-translation-pairing --write README.md
README.md: d8d3e767d5a9805f34f4df57a5b1f8ff7fdaa955 README.md: 64f06c0aec0905fa7deabbec0deea61e1c7a40d4
README.zh.md: 89abf8d817deeed2bf4416035790c8696c8c8e33 README.zh.md: fee03118926028833c828809764ebb5f6375259e
+1 -1
View File
@@ -56,7 +56,7 @@ The [CLI contract](apps/cli/README.md#profiles) describes profile layout, layer
Run one task, print the final answer, and exit: Run one task, print the final answer, and exit:
```sh ```sh
dsh --profile headless "summarize this workspace" dsh run "summarize this workspace"
``` ```
### Automation and SDKs ### Automation and SDKs
+1 -1
View File
@@ -56,7 +56,7 @@ profile 布局、层语义与配置输出命令详见 [CLI(命令行界面)
运行一项任务,打印最终答案后退出: 运行一项任务,打印最终答案后退出:
```sh ```sh
dsh --profile headless "summarize this workspace" dsh run "summarize this workspace"
``` ```
### 自动化与 SDK ### 自动化与 SDK
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/README.md # pnpm run verify-translation-pairing --write apps/cli/README.md
README.md: f50f26ec5de54e094e17221f7cd355483483754e README.md: 12108fcbff4e649d0bcb3e01e688fa334ab91b14
README.zh.md: 242e64a0c42064b9f0b7621e665e85fe44523fe5 README.zh.md: 9518feed1d5d40c3e5ec2d346b929118ccc08810
+2 -2
View File
@@ -9,11 +9,11 @@ The `dsh` command is the product launcher for profiles: ordered stacks of plugin
| Command | Purpose | | Command | Purpose |
|---|---| |---|---|
| `dsh --profile <name>` | Boot the named profile under `$DSH_HOME/profiles/<name>`. | | `dsh --profile <name>` | Boot the named profile under `$DSH_HOME/profiles/<name>`. |
| `dsh --profile headless "task"` | Run one fresh persisted session, print the final answer, and exit. | | `dsh run [--profile <name>] [--patch <path>...] "task"` | Run one fresh persisted session, print the final answer, and exit; the profile defaults to `headless`. |
| `dsh web` | Alias of `--profile web` with the Web flag family (`--host`, `--port`, `--dev`, ...). | | `dsh web` | Alias of `--profile web` with the Web flag family (`--host`, `--port`, `--dev`, ...). |
| `dsh plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. | | `dsh plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |
The invoking directory is the default workspace root. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`. The invoking directory is the default workspace root. `dsh run` requires non-blank task text and the selected profile must mount the `headless-runner` row; `--profile` preserves custom one-shot profiles. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`.
## Profiles ## Profiles
+2 -2
View File
@@ -9,11 +9,11 @@
| 命令 | 用途 | | 命令 | 用途 |
|---|---| |---|---|
| `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 | | `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 |
| `dsh --profile headless "task"` | 运行一个新的持久化会话,打印最终答案并退出。 | | `dsh run [--profile <name>] [--patch <path>...] "task"` | 运行一个新的持久化会话,打印最终答案并退出profile 默认为 `headless`。 |
| `dsh web` | `--profile web` 的别名,附带 Web flag 系列(`--host``--port``--dev` 等)。 | | `dsh web` | `--profile web` 的别名,附带 Web flag 系列(`--host``--port``--dev` 等)。 |
| `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 | | `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
调用目录是默认 workspace 根目录。`web``headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 调用目录是默认 workspace 根目录。`dsh run` 要求任务文本非空白,且所选 profile 必须挂载 `headless-runner` 行;`--profile` 保留对自定义一次性 profile 的支持。`web``headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
## Profile ## Profile
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md # pnpm run verify-translation-pairing --write apps/cli/reference/README.md
README.md: c7c7b2aa231d4c9f4b3fbf31663237c8457eb051 README.md: 496cecdb64e3254a2a77690f55f760b4cd90b521
README.zh.md: 5439aa78b74415c8e6264d21f5c52e5cee5b38ee README.zh.md: 4673bf764347307a9b91e2a5474a8439cf67b481
+8 -2
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md) English | [中文](README.zh.md)
This reference defines the profile, web-alias, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner. This reference defines the profile, one-shot run, web-alias, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner.
## Profile boot ## Profile boot
@@ -12,7 +12,7 @@ Bundle names resolve from the dsh installation first, then from the profile dire
The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + web-app + headless). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`. The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + web-app + headless). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
A positional task (`dsh --profile headless "run the tests"`) requires the composition to mount the one-shot runner row (`headless-runner`); the launcher patches the task text into that row, the runner drives one fresh persisted session through the in-process API carrier, prints the final assistant text on stdout, and exits 0 on a completed turn, else 1. The session's Web host runs on an OS-assigned port and is announced on stderr, so the run is observable in a browser. Profile boot accepts no positional task. A profile that mounts the one-shot runner row (`headless-runner`) therefore fails loud with the canonical `dsh run --profile <name> "<task>"` command instead of reaching the row's raw required-field error.
Inspect the composed tree without booting it: Inspect the composed tree without booting it:
@@ -23,6 +23,12 @@ dsh --profile web --patch ./extra.yml --dump-config
`--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print provenance comments per layer; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr. `--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print provenance comments per layer; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr.
## One-shot run
`dsh run [--profile <name>] [--patch <path>...] <task...>` joins the task arguments with spaces, rejects a missing or blank task, and defaults `--profile` to `headless`. Repeatable `--patch` overlays occupy the same layer position as profile-boot overlays. A custom selected profile must mount `headless-runner`; otherwise launch fails before boot with a diagnostic naming that missing row.
The launcher patches the task text into the runner row, which drives one fresh persisted session through the in-process API carrier, prints the final assistant text on stdout, and exits 0 on a completed turn, else 1. At the idle boundary, the runner waits until its mux consumer has observed the session's final event sequence before deriving that output and exit reason. The session's Web host runs on an OS-assigned port and is announced on stderr, so the run is observable in a browser.
## Plugin management ## Plugin management
`dsh plugin --profile <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack. `dsh plugin --profile <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack.
+8 -2
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文 [English](README.md) | 中文
本参考定义 profile、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。 本参考定义 profile、一次性运行、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
## Profile 启动 ## Profile 启动
@@ -12,7 +12,7 @@
`web``headless` profile 首次使用时会从随附模板自动初始化(`web`base + web-app`headless`base + web-app + headless)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>` `web``headless` profile 首次使用时会从随附模板自动初始化(`web`base + web-app`headless`base + web-app + headless)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`
位置参数任务(`dsh --profile headless "run the tests"`)要求组合挂载一次性运行器行(`headless-runner`;启动器把任务文本 patch 进该行,运行器通过进程内 API 载体驱动一个全新的持久化会话,在 stdout 打印最终 assistant 文本,并在轮次完成时以 0 退出,否则以 1 退出。会话的 Web 宿主运行在 OS 分配的端口上并公布到 stderr,因此该次运行可在浏览器中观察 Profile 启动不接受位置参数任务。因此,挂载一次性运行器行(`headless-runner`的 profile 会显式报错,并提示规范命令 `dsh run --profile <name> "<task>"`,而不会触发该行原始的必填字段错误
可在不启动的情况下检查组合出的配置树: 可在不启动的情况下检查组合出的配置树:
@@ -23,6 +23,12 @@ dsh --profile web --patch ./extra.yml --dump-config
`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml``--patch` overlay。两者都会按层打印来源注释;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。 `--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml``--patch` overlay。两者都会按层打印来源注释;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。
## 一次性运行
`dsh run [--profile <name>] [--patch <path>...] <task...>` 会用空格拼接任务参数,拒绝缺失或空白任务,并让 `--profile` 默认为 `headless`。可重复使用的 `--patch` overlay 与 profile 启动的 overlay 位于同一层。所选的自定义 profile 必须挂载 `headless-runner`;否则启动器会在启动前失败,并在诊断中指明缺少该行。
启动器把任务文本 patch 进运行器行,运行器再通过进程内 API 载体驱动一个全新的持久化会话,在 stdout 打印最终 assistant 文本,并在轮次完成时以 0 退出,否则以 1 退出。到达 idle 边界时,运行器会等到 mux 消费方观察到会话的最终事件序号,再生成输出与退出原因。会话的 Web 宿主运行在 OS 分配的端口上并公布到 stderr,因此该次运行可在浏览器中观察。
## 插件管理 ## 插件管理
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm``add``remove``why``update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.``../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,`dsh.profile.bundles` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有组合包声明的依赖保持为普通依赖并给出一次性警告,已移除的依赖则退出层栈。 `dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm``add``remove``why``update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.``../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,`dsh.profile.bundles` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有组合包声明的依赖保持为普通依赖并给出一次性警告,已移除的依赖则退出层栈。
+41 -17
View File
@@ -1,10 +1,10 @@
/** /**
* Commander adapter for the `dsh` command-line entry. The default command * Commander adapter for the `dsh` command-line entry. The default command
* boots a named profile (`--profile <name>`), optionally with extra `--patch` * boots a named profile (`--profile <name>`), optionally with extra `--patch`
* overlays and a positional task (one-shot mode for profiles mounting the * overlays. `run` owns one-shot task execution, defaulting to the headless
* headless runner). `web` is a hardcoded alias for `--profile web` that adds * profile; `web` is a hardcoded alias for `--profile web` that adds the Web
* the Web flag family; `plugin` manages a profile's plugin dependencies by * flag family; `plugin` manages a profile's plugin dependencies by forwarding
* forwarding to pnpm. Commander owns help, version, and parse errors. * to pnpm. Commander owns help, version, and parse errors.
* @module @deepseek-ai/dsh/args * @module @deepseek-ai/dsh/args
*/ */
@@ -16,8 +16,16 @@ interface ProfileInvocation {
profile: string profile: string
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */ /** Extra patch-list overlays applied after the profile's own layer, in argv order. */
patches: string[] patches: string[]
/** Positional task text joined by spaces; non-empty only for one-shot runs. */ }
task?: string
/** Run one task through a profile mounting the headless runner. */
interface RunInvocation {
mode: 'run'
profile: string
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
patches: string[]
/** Non-blank task text joined from the variadic positional arguments. */
task: string
} }
/** Print a composed profile tree and exit without booting. */ /** Print a composed profile tree and exit without booting. */
@@ -54,7 +62,7 @@ interface PluginInvocation {
} }
/** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */ /** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */
export type DshInvocation = ProfileInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation export type DshInvocation = ProfileInvocation | RunInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation
/** Raw web-subcommand options straight from Commander. */ /** Raw web-subcommand options straight from Commander. */
interface WebOptions { interface WebOptions {
@@ -68,6 +76,12 @@ interface WebOptions {
dumpDefaultConfig?: boolean dumpDefaultConfig?: boolean
} }
/** Raw run-subcommand options straight from Commander. */
interface RunOptions {
profile: string
patch?: string[]
}
/** /**
* Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never * Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never
* variadic — a variadic `--patch` would swallow a following positional task. * variadic — a variadic `--patch` would swallow a following positional task.
@@ -90,19 +104,19 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
.addHelpText('after', ` .addHelpText('after', `
Examples: Examples:
dsh --profile web boot the web profile (same as: dsh web) dsh --profile web boot the web profile (same as: dsh web)
dsh --profile headless "run the tests" answer one task, print the result, and exit dsh run "run the tests" answer one task, print the result, and exit
dsh run --profile custom "run the tests" run one task through a custom one-shot profile
dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay
dsh plugin --profile tui add <package> install a plugin into the tui profile dsh plugin --profile tui add <package> install a plugin into the tui profile
dsh web --port 8080 the web alias with its flag family dsh web --port 8080 the web alias with its flag family
`) `)
.exitOverride() .exitOverride()
.enablePositionalOptions() .enablePositionalOptions()
.argument('[task...]', 'one-shot task text for profiles mounting the headless runner')
.option('--profile <name>', 'the profile under $DSH_HOME/profiles to boot') .option('--profile <name>', 'the profile under $DSH_HOME/profiles to boot')
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect) .option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
.option('--dump-config', 'print the composed profile tree and exit') .option('--dump-config', 'print the composed profile tree and exit')
.option('--dump-default-config', 'print the profile tree without its user layer or --patch overlays and exit') .option('--dump-default-config', 'print the profile tree without its user layer or --patch overlays and exit')
.action((task: string[], options: { .action((options: {
profile?: string profile?: string
patch?: string[] patch?: string[]
dumpConfig?: boolean dumpConfig?: boolean
@@ -116,7 +130,6 @@ Examples:
if (options.dumpConfig === true && options.dumpDefaultConfig === true) { if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
program.error('error: --dump-config and --dump-default-config are mutually exclusive') program.error('error: --dump-config and --dump-default-config are mutually exclusive')
} }
if (task.length > 0) program.error('error: --dump-config/--dump-default-config take no task')
const defaultOnly = options.dumpDefaultConfig === true const defaultOnly = options.dumpDefaultConfig === true
if (defaultOnly && patches.length > 0) { if (defaultOnly && patches.length > 0) {
program.error('error: --dump-default-config prints the bundle layers and takes no --patch') program.error('error: --dump-default-config prints the bundle layers and takes no --patch')
@@ -124,12 +137,7 @@ Examples:
resolved = { mode: 'dump-config', profile, defaultOnly, patches } resolved = { mode: 'dump-config', profile, defaultOnly, patches }
return return
} }
resolved = { resolved = { mode: 'profile', profile, patches }
mode: 'profile',
profile,
patches,
...task.length > 0 ? { task: task.join(' ') } : {},
}
}) })
/** Reject parent options that crossed a subcommand boundary. */ /** Reject parent options that crossed a subcommand boundary. */
@@ -146,6 +154,22 @@ Examples:
} }
} }
const run = program.command('run').description('run one task through a profile mounting the headless runner')
run
.option('--profile <name>', 'one-shot profile under $DSH_HOME/profiles', 'headless')
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
.argument('<task...>', 'task text')
.action((task: string[], options: RunOptions) => {
rejectParentOptions('run')
const profile = options.profile
if (profile === '') program.error('error: --profile needs a name')
const patches = options.patch ?? []
if (patches.includes('')) program.error('error: --patch needs a path')
const joined = task.join(' ')
if (joined.trim() === '') program.error('error: run needs a non-blank task')
resolved = { mode: 'run', profile, patches, task: joined }
})
const web = program.command('web').description('serve the browser UI (alias of --profile web) on the configured host and port') const web = program.command('web').description('serve the browser UI (alias of --profile web) on the configured host and port')
web web
.option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect) .option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
+10 -1
View File
@@ -33,7 +33,16 @@ switch (invocation.mode) {
environment: loadLayeredEnv('dsh'), environment: loadLayeredEnv('dsh'),
profile: invocation.profile, profile: invocation.profile,
patchFiles: invocation.patches, patchFiles: invocation.patches,
...invocation.task !== undefined && { task: invocation.task }, })
break
}
case 'run': {
const { runProfile } = await import('./profile-boot.ts')
await runProfile({
environment: loadLayeredEnv('dsh'),
profile: invocation.profile,
patchFiles: invocation.patches,
task: invocation.task,
}) })
break break
} }
+3 -3
View File
@@ -47,7 +47,7 @@ export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.me
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */ /** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
const TELEMETRY_ROW_ID = 'telemetry-otel' const TELEMETRY_ROW_ID = 'telemetry-otel'
/** The one-shot runner row a positional task requires and configures. */ /** The one-shot runner row a `dsh run` task requires and configures. */
const HEADLESS_ROW_ID = 'headless-runner' const HEADLESS_ROW_ID = 'headless-runner'
/** The empty root entry list every profile tree patches over. */ /** The empty root entry list every profile tree patches over. */
@@ -160,7 +160,7 @@ export interface RunProfileOptions {
patchFiles: readonly string[] patchFiles: readonly string[]
/** Launcher hook turning the pre-flag composed rows into flag patches (the web alias's flag family). */ /** Launcher hook turning the pre-flag composed rows into flag patches (the web alias's flag family). */
deriveFlagPatches?: (rows: ProfileRows) => PatchOptions[] deriveFlagPatches?: (rows: ProfileRows) => PatchOptions[]
/** One-shot task text; requires the composition to mount the headless runner row. */ /** `dsh run` task text; requires the composition to mount the headless runner row. */
task?: string task?: string
/** Surface setup registered after Loader installation and before any config-tree entry mounts. */ /** Surface setup registered after Loader installation and before any config-tree entry mounts. */
prepare?: (ctx: Context, rows: ProfileRows) => Promise<void> | void prepare?: (ctx: Context, rows: ProfileRows) => Promise<void> | void
@@ -190,7 +190,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
// error naming no fix. // error naming no fix.
throw new Error( throw new Error(
`dsh: profile ${JSON.stringify(options.profile)} mounts the one-shot runner and needs a task: ` `dsh: profile ${JSON.stringify(options.profile)} mounts the one-shot runner and needs a task: `
+ `dsh --profile ${options.profile} "<task>"`, + `dsh run --profile ${options.profile} "<task>"`,
) )
} }
+15 -3
View File
@@ -21,12 +21,16 @@ function exitCode(argv: string[]): number {
afterEach(() => { vi.restoreAllMocks() }) afterEach(() => { vi.restoreAllMocks() })
describe('parseDshArgs', () => { describe('parseDshArgs', () => {
it('routes profile boots, one-shot tasks, and the web alias', () => { it('routes profile boots, one-shot runs, and the web alias', () => {
expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [] }) expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [] })
expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
.toEqual({ mode: 'profile', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--patch', 'b.yml'])) expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--patch', 'b.yml']))
.toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'] }) .toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'] })
expect(parse(['run', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: 'run the tests' })
expect(parse(['run', '--profile', 'custom', '--patch', 'a.yml', '--patch', 'b.yml', 'run', 'the', 'tests']))
.toEqual({ mode: 'run', profile: 'custom', patches: ['a.yml', 'b.yml'], task: 'run the tests' })
expect(parse(['run', '--', '--profile', 'is', 'task', 'text']))
.toEqual({ mode: 'run', profile: 'headless', patches: [], task: '--profile is task text' })
expect(parse(['web'])).toEqual({ mode: 'web', dev: false, patches: [] }) expect(parse(['web'])).toEqual({ mode: 'web', dev: false, patches: [] })
expect(parse(['web', '--patch', 'web.yml'])).toEqual({ mode: 'web', dev: false, patches: ['web.yml'] }) expect(parse(['web', '--patch', 'web.yml'])).toEqual({ mode: 'web', dev: false, patches: ['web.yml'] })
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w'])) expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
@@ -65,6 +69,13 @@ describe('parseDshArgs', () => {
expect(exitCode(['tui'])).toBe(1) // a bare word is a task without --profile expect(exitCode(['tui'])).toBe(1) // a bare word is a task without --profile
expect(exitCode(['--config', 'c.yml'])).toBe(1) // removed expect(exitCode(['--config', 'c.yml'])).toBe(1) // removed
expect(exitCode(['-p', 'task'])).toBe(1) // removed expect(exitCode(['-p', 'task'])).toBe(1) // removed
expect(exitCode(['--profile', 'headless', 'task'])).toBe(1) // tasks belong to `run`
expect(exitCode(['run'])).toBe(1)
expect(exitCode(['run', ''])).toBe(1)
expect(exitCode(['run', '--profile', '', 'task'])).toBe(1)
expect(exitCode(['run', '--patch=', 'task'])).toBe(1)
expect(exitCode(['--profile', 'headless', 'run', 'task'])).toBe(1)
expect(exitCode(['--patch', 'parent.yml', 'run', 'task'])).toBe(1)
expect(exitCode(['--profile', ''])).toBe(1) expect(exitCode(['--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', '--patch='])).toBe(1) expect(exitCode(['--profile', 'x', '--patch='])).toBe(1)
expect(exitCode(['--dump-config'])).toBe(1) expect(exitCode(['--dump-config'])).toBe(1)
@@ -90,6 +101,7 @@ describe('parseDshArgs', () => {
it('exits 0 for help and version', () => { it('exits 0 for help and version', () => {
expect(exitCode(['--help'])).toBe(0) expect(exitCode(['--help'])).toBe(0)
expect(exitCode(['run', '--help'])).toBe(0)
expect(exitCode(['--version'])).toBe(0) expect(exitCode(['--version'])).toBe(0)
}) })
}) })
+43 -1
View File
@@ -182,14 +182,56 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
const help = await runBuiltBin(['--help']) const help = await runBuiltBin(['--help'])
expect(help.code).toBe(0) expect(help.code).toBe(0)
expect(help.stdout).toContain('dsh --profile web') expect(help.stdout).toContain('dsh --profile web')
expect(help.stdout).toContain('dsh run "run the tests"')
expect(help.stdout).toContain('dsh plugin --profile') expect(help.stdout).toContain('dsh plugin --profile')
expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu) expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu)
for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task']]) { for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task'], ['--profile', 'headless', 'task']]) {
const result = await runBuiltBin(removed) const result = await runBuiltBin(removed)
expect(result.code).toBe(1) expect(result.code).toBe(1)
} }
}, 30_000) }, 30_000)
it('prints run help without initializing the selected profile', async () => {
const parent = mkdtempSync(join(tmpdir(), 'dsh-run-help-'))
const home = join(parent, 'not-created')
try {
const result = await runBuiltBin(['run', '--help'], { DSH_HOME: home })
expect(result.code).toBe(0)
expect(result.stderr).toBe('')
expect(result.stdout).toContain('Usage: dsh run [options] <task...>')
expect(existsSync(home)).toBe(false)
} finally {
rmSync(parent, { recursive: true, force: true })
}
})
it('runs the default headless profile through the published run command', async () => {
const apiKey = 'built-dsh-run-key'
const server = await startMockLlmServer({
sequence: ['success'],
apiKey,
successText: 'published dsh run reached the mock',
})
const home = mkdtempSync(join(tmpdir(), 'dsh-built-run-'))
try {
const result = await runBuiltBin(['run', 'answer', 'from', 'the', 'published', 'entry'], {
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: apiKey,
DEEPSEEK_BASE_URL: server.baseURL,
})
expect(result.code, result.stderr).toBe(0)
expect(result.stdout).toBe('published dsh run reached the mock')
expect(result.stderr).toMatch(/^dsh: observing at http:\/\/127\.0\.0\.1:\d+$/u)
expect(server.requests.length).toBeGreaterThan(0)
expect(server.requests.every(request => request.path === '/chat/completions')).toBe(true)
expect(JSON.stringify(server.requests.map(request => request.body))).toContain('answer from the published entry')
} finally {
await server.close()
rmSync(home, { recursive: true, force: true })
}
}, 30_000)
it('does not load a project environment for --version', async () => { it('does not load a project environment for --version', async () => {
const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-')) const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-'))
writeFileSync(join(project, '.env'), 'PATH=/project-only-path\n') writeFileSync(join(project, '.env'), 'PATH=/project-only-path\n')
+2 -2
View File
@@ -66,7 +66,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
try { try {
const home = join(cwd, '.dsh') const home = join(cwd, '.dsh')
// Pre-initialize the headless profile with the never-dispose row in its // Pre-initialize the headless profile with the never-dispose row in its
// user patch layer (the same file `dsh --profile headless` hot-reloads). // user patch layer (the same file a long-lived profile boot hot-reloads).
const profileDir = join(home, 'profiles', 'headless') const profileDir = join(home, 'profiles', 'headless')
await mkdir(profileDir, { recursive: true }) await mkdir(profileDir, { recursive: true })
await writeFile(join(profileDir, 'package.json'), JSON.stringify({ await writeFile(join(profileDir, 'package.json'), JSON.stringify({
@@ -83,7 +83,7 @@ async function runHeadlessPtySmoke(): Promise<string> {
].join('\n')) ].join('\n'))
const launch = resolveExampleLaunch({ const launch = resolveExampleLaunch({
srcBin: dshBinScript, srcBin: dshBinScript,
configArgs: ['--profile', 'headless', 'never complete'], configArgs: ['run', 'never complete'],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_HOME: home, DSH_HOME: home,
+2 -5
View File
@@ -21,7 +21,6 @@ flowchart LR
pkg_session["session"] pkg_session["session"]
svc_sessions["ctx.sessions<br/>In-memory session store"] svc_sessions["ctx.sessions<br/>In-memory session store"]
pkg_agent["agent"] pkg_agent["agent"]
pkg_cli_demo["cli-demo"]
pkg_session_persistence["session-persistence"] pkg_session_persistence["session-persistence"]
pkg_session_query["session-query"] pkg_session_query["session-query"]
pkg_session_query_sqlite["session-query-sqlite"] pkg_session_query_sqlite["session-query-sqlite"]
@@ -262,7 +261,6 @@ flowchart LR
svc_agentLoop --> pkg_agent_spine_demo svc_agentLoop --> pkg_agent_spine_demo
svc_agents --> pkg_acp svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop svc_agents --> pkg_agent_loop
svc_agents --> pkg_cli_demo
svc_agents --> pkg_subagent_inprocess svc_agents --> pkg_subagent_inprocess
svc_approval --> pkg_tool_bash svc_approval --> pkg_tool_bash
svc_approval --> pkg_tools svc_approval --> pkg_tools
@@ -309,7 +307,6 @@ flowchart LR
svc_sessionQuery --> pkg_tool_session_query svc_sessionQuery --> pkg_tool_session_query
svc_sessions --> pkg_agent svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_cli_demo
svc_sessions --> pkg_invariants svc_sessions --> pkg_invariants
svc_sessions --> pkg_session_persistence svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query svc_sessions --> pkg_session_query
@@ -367,7 +364,7 @@ flowchart LR
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. | | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | | `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`cli-demo`](../packages/examples/cli-demo), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. | | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. | | `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. | | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. | | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
@@ -389,7 +386,7 @@ flowchart LR
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session-projection/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session-title/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. | | `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session-projection/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session-title/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. |
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session-projection/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. | | `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session-projection/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | | `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. | | `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. | | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. | | `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. | | `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. |
+1 -41
View File
@@ -249,46 +249,6 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local)
Source: [`packages/bash/bash-sandbox/src/index.ts:35`](../packages/bash/bash-sandbox/src/index.ts) Source: [`packages/bash/bash-sandbox/src/index.ts:35`](../packages/bash/bash-sandbox/src/index.ts)
## `@deepseek-ai/dsh-cli-demo`
```ts config-catalog
/** App config forwarded to the spine, configured agent, and JSONL backend. */
export interface Config {
/** Provider route for the configured agent. */
provider: string
/** Model name for the configured agent; a matching adapter must be registered. */
model: string
/** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
maxParallelToolCalls?: number
/** Deployment persona forwarded to the system-prompt plugin. */
persona?: string
/** Explicit model-facing tool order forwarded to the system-prompt plugin. */
toolOrder?: string[]
/** Tool-registry presentation config forwarded through agent-spine-demo. */
tools?: ToolsConfig
/** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
dshHome?: string
/** Fallback session-title limits forwarded through agent-spine-demo. */
sessionTitle?: NonNullable<agentCore.Config['sessionTitle']>
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
persistenceCompression?: JsonlCompression
/** Skill registry, local-provider, and model-facing consumer config. */
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-spine-demo. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Generic background-task control-tool config forwarded through agent-spine-demo. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
}
```
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
Source: [`packages/examples/cli-demo/src/index.ts:26`](../packages/examples/cli-demo/src/index.ts)
## `@deepseek-ai/dsh-client-connection` ## `@deepseek-ai/dsh-client-connection`
Requires: `httpServer` Requires: `httpServer`
@@ -509,7 +469,7 @@ export interface Config {
} }
``` ```
Source: [`packages/bundle/headless/src/index.ts:33`](../packages/bundle/headless/src/index.ts) Source: [`packages/bundle/headless/src/index.ts:32`](../packages/bundle/headless/src/index.ts)
## `@deepseek-ai/dsh-hooks-claude` ## `@deepseek-ai/dsh-hooks-claude`
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/cookbook/extension-cookbook.md # pnpm run verify-translation-pairing --write docs/cookbook/extension-cookbook.md
extension-cookbook.md: 26f20b8f6cb57103b8e7340fcd21089ff1b0e5f6 extension-cookbook.md: e04f6ffcf9a32ba3ee7344db18cad113b347c667
extension-cookbook.zh.md: b8bafdb73e91b56dd411cc5e96c21ccf2c5db123 extension-cookbook.zh.md: c2fa2f83c39ca0eb93397f39c88265c028224c7a
+1 -1
View File
@@ -90,7 +90,7 @@ export function apply(ctx: Context) {
## Runnable wirings ## Runnable wirings
Runnable leaves load their plugin trees from `examples/*/cordis.yml`; the root `demo:*` scripts and those leaf directories are the authoritative inventory. Non-interactive leaves use [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), ACP leaves use [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), JSON-RPC leaves use [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo), and the app packages share [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo). Runnable leaves load their plugin trees from `examples/*/cordis.yml`; the root `demo:*` scripts and those leaf directories are the authoritative inventory. The product `dsh` launcher owns Web and one-shot headless execution, ACP leaves use [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and JSON-RPC leaves use [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo). The headless snapshot leaf mounts [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) and JSONL persistence explicitly, then drives them through an example-owned test fixture rather than a shipped app package.
## The feature → mechanism map ## The feature → mechanism map
+2 -2
View File
@@ -90,7 +90,7 @@ export function apply(ctx: Context) {
## 可运行的组装示例 ## 可运行的组装示例
可运行叶子从 `examples/*/cordis.yml` 加载各自的插件树;根目录的 `demo:*` 脚本和这些叶子目录是权威清单。非交互式叶子使用 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo)ACP 叶子使用 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo)JSON-RPC 叶子使用 [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo),应用包共享 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo)。 可运行叶子从 `examples/*/cordis.yml` 加载各自的插件树;根目录的 `demo:*` 脚本和这些叶子目录是权威清单。产品 `dsh` 启动器负责 Web 和一次性 headless 执行ACP 叶子使用 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo)JSON-RPC 叶子使用 [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo)。headless 快照叶节点显式挂载 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) 和 JSONL 持久化,再通过示例自有的测试 fixture(测试前置数据)驱动这些组件,而不是通过已交付的 app 包
## 功能→机制映射 ## 功能→机制映射
@@ -122,7 +122,7 @@ export function apply(ctx: Context) {
| Skill(技能) | section + 工具注册;调用时通过 `inject()` 注入 skill 内容 | | Skill(技能) | section + 工具注册;调用时通过 `inject()` 注入 skill 内容 |
| 记忆 | section provider + 工具 | | 记忆 | section provider + 工具 |
| 定时任务(cron) | 插件注册面向模型的调度工具;定时器触发 → 空闲时 `followup(…, {source: {kind: 'cron', …}})`/忙碌时 `inject()` 通知 | | 定时任务(cron) | 插件注册面向模型的调度工具;定时器触发 → 空闲时 `followup(…, {source: {kind: 'cron', …}})`/忙碌时 `inject()` 通知 |
| UIGUICLI 输出 JSONL | 监听 `session/event`(助手分片、边界、工具活动);输入 → `followup()` | | UIGUICLI(命令行界面)输出 JSONL | 监听 `session/event`(助手分片、边界、工具活动);输入 → `followup()` |
| 遥测 / 可回放 trace | `session/event` → JSONL;回放 = `sessions.create(id, { seed })` | | 遥测 / 可回放 trace | `session/event` → JSONL;回放 = `sessions.create(id, { seed })` |
| 模型适配器 | 通过 `registerAdapter` 注册 `LlmAdapter` 子类(`dsh-llm-deepseek``dsh-llm-pi-ai` | | 模型适配器 | 通过 `registerAdapter` 注册 `LlmAdapter` 子类(`dsh-llm-deepseek``dsh-llm-pi-ai` |
| 插件热重载 | 每个注册都是一个 `ctx.effect` → vendor 的 HMR(热模块替换)直接生效 | | 插件热重载 | 每个注册都是一个 `ctx.effect` → vendor 的 HMR(热模块替换)直接生效 |
+1 -1
View File
@@ -32,7 +32,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-title`](../packages/session-title/session-title) | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-title`](../packages/session-title/session-title) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-title`](../packages/session-title/session-title) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-title`](../packages/session-title/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`cli-demo`](../packages/examples/cli-demo), [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`loader-smoke`](../packages/support/loader-smoke), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+5 -14
View File
@@ -206,7 +206,6 @@ flowchart TD
subgraph group_examples["packages/examples"] subgraph group_examples["packages/examples"]
pkg_acp_demo["acp-demo"] pkg_acp_demo["acp-demo"]
pkg_agent_spine_demo["agent-spine-demo"] pkg_agent_spine_demo["agent-spine-demo"]
pkg_cli_demo["cli-demo"]
pkg_jsonrpc_demo["jsonrpc-demo"] pkg_jsonrpc_demo["jsonrpc-demo"]
end end
subgraph group_feedback["packages/feedback"] subgraph group_feedback["packages/feedback"]
@@ -301,7 +300,6 @@ flowchart TD
pkg_timeout --> pkg_invariants pkg_timeout --> pkg_invariants
pkg_scope --> pkg_invariants pkg_scope --> pkg_invariants
pkg_llm_mock_server --> pkg_invariants pkg_llm_mock_server --> pkg_invariants
pkg_loader_smoke --> pkg_invariants
pkg_base --> pkg_invariants pkg_base --> pkg_invariants
pkg_client_modules --> pkg_invariants pkg_client_modules --> pkg_invariants
pkg_client_schema_form --> pkg_invariants pkg_client_schema_form --> pkg_invariants
@@ -562,6 +560,10 @@ flowchart TD
pkg_llm_replay --> pkg_invariants pkg_llm_replay --> pkg_invariants
pkg_llm_replay --> pkg_llm pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session pkg_llm_replay --> pkg_session
pkg_loader_smoke --> pkg_agent
pkg_loader_smoke --> pkg_invariants
pkg_loader_smoke --> pkg_llm
pkg_loader_smoke --> pkg_session
pkg_commands --> pkg_agent pkg_commands --> pkg_agent
pkg_commands --> pkg_brand pkg_commands --> pkg_brand
pkg_commands --> pkg_invariants pkg_commands --> pkg_invariants
@@ -1148,16 +1150,6 @@ flowchart TD
pkg_acp_demo --> pkg_session_query_sqlite pkg_acp_demo --> pkg_session_query_sqlite
pkg_acp_demo --> pkg_tools pkg_acp_demo --> pkg_tools
pkg_acp_demo --> pkg_workspace_context pkg_acp_demo --> pkg_workspace_context
pkg_cli_demo --> pkg_agent
pkg_cli_demo --> pkg_agent_spine_demo
pkg_cli_demo --> pkg_app_boot
pkg_cli_demo --> pkg_invariants
pkg_cli_demo --> pkg_llm
pkg_cli_demo --> pkg_session
pkg_cli_demo --> pkg_session_checkpoint_policy
pkg_cli_demo --> pkg_session_persistence_jsonl
pkg_cli_demo --> pkg_tools
pkg_cli_demo --> pkg_workspace_context
``` ```
| Package | Group | Depends on | | Package | Group | Depends on |
@@ -1172,7 +1164,6 @@ flowchart TD
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) | | [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) |
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) | | [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) |
| [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) | | [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`invariants`](../packages/support/invariants) |
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) | | [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) | | [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) | | [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) |
@@ -1257,6 +1248,7 @@ flowchart TD
| [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | | [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) |
| [`session-title`](../packages/session-title/session-title) | `session-title` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) | | [`session-title`](../packages/session-title/session-title) | `session-title` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`commands`](../packages/ui/commands) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) | | [`commands`](../packages/ui/commands) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
@@ -1354,4 +1346,3 @@ flowchart TD
| [`sdk-client`](../packages/sdk/sdk-client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session) | | [`sdk-client`](../packages/sdk/sdk-client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/sdk-protocol), [`session`](../packages/core/session) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/sdk-client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/sdk-client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | | [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
| [`cli-demo`](../packages/examples/cli-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/testing.md # pnpm run verify-translation-pairing --write docs/testing.md
testing.md: 514ca4e1df7505b02350470d5de4a5ee3647634b testing.md: fecf02fe887aac5c92c739523f10a0562eba6242
testing.zh.md: 6d24d8e74d726d53fa3500f57e34238483c5481c testing.zh.md: a34f56fac72baa9916d5f96e83048ad54cf23d4a
+2 -2
View File
@@ -9,7 +9,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
- **Unit** (`pnpm run test`): vitest over package and example specs under their `tests/**` directories plus repository script specs under `scripts/**/*.spec.ts`; tests stay with the code area they exercise. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`). - **Unit** (`pnpm run test`): vitest over package and example specs under their `tests/**` directories plus repository script specs under `scripts/**/*.spec.ts`; tests stay with the code area they exercise. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`).
- **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. Per-file 100% on `packages/bash/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar. - **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. Per-file 100% on `packages/bash/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar.
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)). - **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
- **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless pins `stream-json` through its real one-shot process. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). - **Snapshot** (`pnpm run test:snapshot`): keyless expected outputs cover external behavior — transport contracts and presentation, while persisted logs pin assembled backend behavior. ACP boots the real automation-server example, replays a recorded session, and diffs normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); headless backend scenarios boot their explicit example composition through an unexported JSONL test driver, while `apps/cli` separately owns product `dsh run` acceptance. Use `pnpm run test:snapshot:record` when a model transcript changes and `pnpm run test:snapshot:refresh` when replay input remains valid; review every JSONL and expected-output diff. One ACP scenario (`text-turn`) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line ([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
- **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares replayed browser output with `apps/web/tests/snapshots/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` [builds first](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md) for plugin CSS. - **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares replayed browser output with `apps/web/tests/snapshots/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` [builds first](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md) for plugin CSS.
Committed session-format JSONL uses the canonical packed-row layout, and the keyless snapshot gate discovers every such fixture by its `session` header. In-flight branches carrying older fixture edits merge current `master` and run the [temporary migrator](../scripts/migrate-packed-session-fixtures.ts) through `pnpm run migrate:packed-session-fixtures`; the [removal proposal](../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) retires that command and these links after all affected branches converge. Committed session-format JSONL uses the canonical packed-row layout, and the keyless snapshot gate discovers every such fixture by its `session` header. In-flight branches carrying older fixture edits merge current `master` and run the [temporary migrator](../scripts/migrate-packed-session-fixtures.ts) through `pnpm run migrate:packed-session-fixtures`; the [removal proposal](../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) retires that command and these links after all affected branches converge.
@@ -46,4 +46,4 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword
## When a snapshot test is required ## When a snapshot test is required
Every non-trivial model-, protocol-, or human-visible change adds or updates a keyless scenario in the same PR through a runnable example's owning snapshot suite. Package tests, e2e assertions, mock/test-only compositions, and PR rationale do not replace the assembled transcript; extend the harness when needed. ACP automation scenarios use `examples/<name>/tests/snapshots/`, a scenario table over the [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) suite factory (`examples/acp-agent` is primary); `examples/headless-agent` owns the `stream-json` snapshot and replay fixtures. The `pwsh-tool-turn` ACP scenario boots real `pwsh` and skips where it is absent. Completed interactive-terminal journeys use JSONL-driven scenarios under `apps/cli/tests/snapshots/`; transient presentation uses the package-local semantic matrix, with a PTY case when input, Loader selection, or terminal teardown changes. Browser-rendered web GUI journeys use `apps/web/tests/snapshots/`. New capability seams, lifecycle shapes, or transcript surfaces name every coverage tier at plan time and verify the harness can express it before implementation. Every non-trivial model-, protocol-, or human-visible change adds or updates a keyless scenario in the same PR through a runnable example's owning snapshot suite. Package tests, e2e assertions, mock/test-only compositions, and PR rationale do not replace the assembled transcript; extend the harness when needed. ACP automation scenarios use `examples/<name>/tests/snapshots/`, a scenario table over the [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) suite factory (`examples/acp-agent` is primary); `examples/headless-agent` owns the internal canonical-event JSONL snapshots and replay fixtures. The `pwsh-tool-turn` ACP scenario boots real `pwsh` and skips where it is absent. Completed interactive-terminal journeys use JSONL-driven scenarios under `apps/cli/tests/snapshots/`; transient presentation uses the package-local semantic matrix, with a PTY case when input, Loader selection, or terminal teardown changes. Browser-rendered web GUI journeys use `apps/web/tests/snapshots/`. New capability seams, lifecycle shapes, or transcript surfaces name every coverage tier at plan time and verify the harness can express it before implementation.
+2 -2
View File
@@ -9,7 +9,7 @@
- **单元测试**`pnpm run test`):vitest 运行包(package)和示例各自的 `tests/**` 目录下的测试,以及匹配 `scripts/**/*.spec.ts` 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR(热模块替换)安全测试(dispose(资源释放)贡献的 fiber,断言清理完成)。优先覆盖边界情况、错误路径、事件顺序、并发竞态,以及永久性契约回归(见 `packages/core/agent-loop/tests/contract-regressions.spec.ts`)。 - **单元测试**`pnpm run test`):vitest 运行包(package)和示例各自的 `tests/**` 目录下的测试,以及匹配 `scripts/**/*.spec.ts` 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR(热模块替换)安全测试(dispose(资源释放)贡献的 fiber,断言清理完成)。优先覆盖边界情况、错误路径、事件顺序、并发竞态,以及永久性契约回归(见 `packages/core/agent-loop/tests/contract-regressions.spec.ts`)。
- **覆盖率门禁**`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。`packages/bash/pwsh-local/src` 的按文件 100% 覆盖需要真实的 `pwsh`:缺少它时其 executor 套件会自动跳过,`vitest.config.ts` 会豁免该文件以使无 pwsh 的主机保持绿色,而 CI runner 自带 pwsh,仍按完整标准执行门禁。 - **覆盖率门禁**`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。`packages/bash/pwsh-local/src` 的按文件 100% 覆盖需要真实的 `pwsh`:缺少它时其 executor 套件会自动跳过,`vitest.config.ts` 会豁免该文件以使无 pwsh 的主机保持绿色,而 CI runner 自带 pwsh,仍按完整标准执行门禁。
- **真实 API e2e**`pnpm run test:e2e`):带密钥测试调用真实提供方 API,包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY``PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md))。 - **真实 API e2e**`pnpm run test:e2e`):带密钥测试调用真实提供方 API,包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY``PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md))。
- **快照**`pnpm run test:snapshot`):无密钥预期输出覆盖对外行为(传输契约与呈现),持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff([ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md));headless 通过真实单次运行进程固定 `stream-json`。当模型 transcript(文本记录)发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture(测试前置数据)将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。 - **快照**`pnpm run test:snapshot`):无密钥预期输出覆盖对外行为(传输契约与呈现),持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diff([ACP 快照 Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md));headless 后端场景通过未导出的 JSONL 测试 driver 启动各自显式的示例组装,而 `apps/cli` 则单独负责产品 CLI(命令行界面)`dsh run` 的验收。当模型 transcript(文本记录)发生变化时使用 `pnpm run test:snapshot:record`,回放输入仍然有效时使用 `pnpm run test:snapshot:refresh`;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(`text-turn`)固定完整的系统提示词与工具 schema 内容;其他 fixture(测试前置数据)将其 token 化,因此修改只会扰动一行([pinned-header Agent Note](../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
- **Web 浏览器快照**`pnpm run test:web`;必需的 Linux PRPull Request)门禁):Chromium 将回放后的浏览器输出与 `apps/web/tests/snapshots/` 比较。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md))。`test:web` 会[先构建](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)以交付插件 CSS。 - **Web 浏览器快照**`pnpm run test:web`;必需的 Linux PRPull Request)门禁):Chromium 将回放后的浏览器输出与 `apps/web/tests/snapshots/` 比较。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md))。`test:web` 会[先构建](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)以交付插件 CSS。
签入仓库的会话格式 JSONL 使用规范打包行布局,无密钥快照门禁会通过 `session` header 发现每一份此类 fixture。仍携带旧版 fixture 改动的在途分支应合并当前 `master`,并通过 `pnpm run migrate:packed-session-fixtures` 运行[临时迁移器](../scripts/migrate-packed-session-fixtures.ts);待所有受影响分支收敛后,[移除提案](../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md)会移除该命令及这些链接。 签入仓库的会话格式 JSONL 使用规范打包行布局,无密钥快照门禁会通过 `session` header 发现每一份此类 fixture。仍携带旧版 fixture 改动的在途分支应合并当前 `master`,并通过 `pnpm run migrate:packed-session-fixtures` 运行[临时迁移器](../scripts/migrate-packed-session-fixtures.ts);待所有受影响分支收敛后,[移除提案](../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md)会移除该命令及这些链接。
@@ -46,4 +46,4 @@ e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身
## 何时需要快照测试 ## 何时需要快照测试
每项非平凡的模型可见、协议可见或人类可见变更,都必须在同一 PR 中,通过可运行示例所属的快照套件添加或更新无密钥场景。包测试、e2e 断言、mock 与仅测试组合、PR 理由都不能取代组装后的 transcript;必要时应扩展 harness。ACP 自动化场景使用 `examples/<name>/tests/snapshots/`,即基于 [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) 套件工厂的场景表(`examples/acp-agent` 为主套件);`examples/headless-agent` 拥有 `stream-json` 快照与回放 fixture。`pwsh-tool-turn` ACP 场景启动真实 `pwsh`,在无 `pwsh` 的主机上跳过。已完成的交互式终端旅程使用 `apps/cli/tests/snapshots/` 下由 JSONL 驱动的场景;瞬态呈现使用包内语义矩阵,输入、Loader 选择或终端清理发生变化时还要添加 PTY 用例。新的能力 seam、生命周期形态或 transcript 呈现接口在计划阶段就要列出每个覆盖层级,并在实现前验证 harness 能够表达它们。 每项非平凡的模型可见、协议可见或人类可见变更,都必须在同一 PR 中,通过可运行示例所属的快照套件添加或更新无密钥场景。包测试、e2e 断言、mock 与仅测试组合、PR 理由都不能取代组装后的 transcript;必要时应扩展 harness。ACP 自动化场景使用 `examples/<name>/tests/snapshots/`,即基于 [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) 套件工厂的场景表(`examples/acp-agent` 为主套件);`examples/headless-agent` 拥有内部规范事件 JSONL 快照与回放 fixture。`pwsh-tool-turn` ACP 场景启动真实 `pwsh`,在无 `pwsh` 的主机上跳过。已完成的交互式终端旅程使用 `apps/cli/tests/snapshots/` 下由 JSONL 驱动的场景;瞬态呈现使用包内语义矩阵,输入、Loader 选择或终端清理发生变化时还要添加 PTY 用例。新的能力 seam、生命周期形态或 transcript 呈现接口在计划阶段就要列出每个覆盖层级,并在实现前验证 harness 能够表达它们。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/guide/index.md # pnpm run verify-translation-pairing --write docs/user/guide/index.md
index.md: ede09506a996193fe5cf4ae6cd9b64d3529798a6 index.md: a04698e29755d4a08b012f8b61accb79c470dcb0
index.zh.md: 5f72a6d3099d2d4721eccebae92eacfe72d33bce index.zh.md: 85990f49c7b3cdf7518237c6b558375b7a636243
+7 -5
View File
@@ -12,12 +12,14 @@ Harness implements every capability an AI agent needs—including LLM calls, too
# Select the LLM backend # Select the LLM backend
- name: '@deepseek-ai/dsh-llm-deepseek' - name: '@deepseek-ai/dsh-llm-deepseek'
# Select the one-shot application # Compose one configured agent
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: deepseek-official agents:
model: deepseek-v4-flash - id: main
provider: deepseek-official
model: deepseek-v4-flash
workspaceContext: false workspaceContext: false
``` ```
+7 -5
View File
@@ -12,12 +12,14 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
# Select the LLM backend # Select the LLM backend
- name: '@deepseek-ai/dsh-llm-deepseek' - name: '@deepseek-ai/dsh-llm-deepseek'
# Select the one-shot application # Compose one configured agent
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: deepseek-official agents:
model: deepseek-v4-flash - id: main
provider: deepseek-official
model: deepseek-v4-flash
workspaceContext: false workspaceContext: false
``` ```
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/guide/quickstart.md # pnpm run verify-translation-pairing --write docs/user/guide/quickstart.md
quickstart.md: 8b84017ad33bf02579891bc4dcf83eaf7ec39022 quickstart.md: 9d1a04f0a094919bb008a7e94564a6612e10732d
quickstart.zh.md: dfb24f8fa194866908406c709d059bf1f6595d59 quickstart.zh.md: 7182f63586f4f08524c643a7c6c1ad599b2de098
+5 -5
View File
@@ -22,6 +22,7 @@ pnpm -v
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
cd deepseek-harness cd deepseek-harness
pnpm install pnpm install
pnpm run build
``` ```
Create the gitignored repository-root `.env`: Create the gitignored repository-root `.env`:
@@ -35,17 +36,16 @@ DEEPSEEK_API_KEY=sk-your-key-here
Run a non-interactive task and print its final answer: Run a non-interactive task and print its final answer:
```sh ```sh
pnpm run demo:headless "summarize the architecture of this workspace" pnpm run dsh run "summarize the architecture of this workspace"
``` ```
Headless runs one complete model/tool turn, persists the session, prints the result, and exits. Use `--output-format stream-json` when you need the canonical event stream. `dsh run` creates and persists a fresh session, prints the final assistant answer, and exits. While it runs, stderr prints the local browser URL where the session can be observed.
## Step 3: use the Web UI ## Step 3: use the Web UI
Build and start the browser interface: Start the browser interface:
```sh ```sh
pnpm run build
pnpm run dsh web pnpm run dsh web
``` ```
@@ -53,7 +53,7 @@ Open `http://127.0.0.1:3080`. The agent can read and write files, run commands,
## What happened ## What happened
headless-agent uses the `@deepseek-ai/dsh-cli-demo` app. `dsh web` instead boots the `web` profile: the [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml) and [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) bundle patch layers composed over an empty root. Both select the DeepSeek model and capability plugins appropriate to their entry mode. `dsh run` boots the `headless` profile: [`dsh-base`](../../../packages/bundle/base/cordis.patch.yml), [`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml), and [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) compose over an empty root. `dsh web` uses the first two layers without the one-shot runner. Both select the DeepSeek model and capability plugins appropriate to their entry mode.
## Next steps ## Next steps
+5 -5
View File
@@ -22,6 +22,7 @@ pnpm -v
git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
cd deepseek-harness cd deepseek-harness
pnpm install pnpm install
pnpm run build
``` ```
在仓库根目录创建已被 Git 忽略的 `.env` 在仓库根目录创建已被 Git 忽略的 `.env`
@@ -35,17 +36,16 @@ DEEPSEEK_API_KEY=sk-your-key-here
运行一个非交互式任务并打印最终回答: 运行一个非交互式任务并打印最终回答:
```sh ```sh
pnpm run demo:headless "summarize the architecture of this workspace" pnpm run dsh run "summarize the architecture of this workspace"
``` ```
Headless 运行一个完整的模型/工具轮次,持久化会话,打印结果后退出。需要规范事件流时可使用 `--output-format stream-json` `dsh run` 创建并持久化一个新会话,打印最终 assistant 回答,然后退出。运行期间,stderr 会打印可用于观察该会话的本地浏览器 URL
## 第三步:使用 Web UI ## 第三步:使用 Web UI
构建并启动浏览器界面: 启动浏览器界面:
```sh ```sh
pnpm run build
pnpm run dsh web pnpm run dsh web
``` ```
@@ -53,7 +53,7 @@ pnpm run dsh web
## 回头看 ## 回头看
headless-agent 使用 `@deepseek-ai/dsh-cli-demo` app。`dsh web` 启动 `web` profile[`dsh-base`](../../../packages/bundle/base/cordis.patch.yml)[`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 两个组合包的 patch 层在空根之上组合而成。二者都会根据各自入口模式选择 DeepSeek 模型和能力插件。 `dsh run` 启动 `headless` profile[`dsh-base`](../../../packages/bundle/base/cordis.patch.yml)[`dsh-web-app`](../../../packages/bundle/web-app/cordis.patch.yml) 和 [`dsh-headless`](../../../packages/bundle/headless/cordis.patch.yml) 在空根之上组合。`dsh web` 使用前两层,不包含一次性 runner。二者都会根据各自入口模式选择 DeepSeek 模型和能力插件。
## 下一步 ## 下一步
@@ -34,12 +34,22 @@
# budget, so the local numeric default cannot apply here. # budget, so the local numeric default cannot apply here.
maxDepth: 'provider-managed' maxDepth: 'provider-managed'
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: mock agents:
model: mock-delegate - id: main
provider: mock
model: mock-delegate
cwd: !!js process.cwd()
persona: 'Test ACP subagent cwd inheritance.' persona: 'Test ACP subagent cwd inheritance.'
persistenceRoot: './.sessions'
persistenceCompression: 'none'
workspaceContext: false workspaceContext: false
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: 'none'
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
@@ -2,14 +2,14 @@
/** Test driver: one delegation turn through a headless Loader composition. */ /** Test driver: one delegation turn through a headless Loader composition. */
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts' import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
const configPath = process.argv[2] const configPath = process.argv[2]
if (configPath === undefined) throw new Error('acp-subagent cwd driver requires a config path') if (configPath === undefined) throw new Error('acp-subagent cwd driver requires a config path')
const ctx = await boot('acp-subagent-cwd-e2e', resolveConfigPath(configPath, undefined)) const ctx = await boot('acp-subagent-cwd-e2e', resolveConfigPath(configPath, undefined))
try { try {
await runOneShot(ctx, { task: 'delegate' }) await runFixtureTurn(ctx, { task: 'delegate' })
} finally { } finally {
await ctx.fiber.dispose() await ctx.fiber.dispose()
} }
@@ -31,10 +31,21 @@
enableRunInBackground: false enableRunInBackground: false
maxDepth: 'provider-managed' maxDepth: 'provider-managed'
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: mock agents:
model: mock-delegate - id: main
provider: mock
model: mock-delegate
cwd: !!js process.cwd()
persona: 'This composition test must not start a model turn.' persona: 'This composition test must not start a model turn.'
workspaceContext: false workspaceContext: false
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
@@ -20,10 +20,21 @@
enableRunInBackground: false enableRunInBackground: false
maxDepth: 'provider-managed' maxDepth: 'provider-managed'
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: mock agents:
model: mock-delegate - id: main
provider: mock
model: mock-delegate
cwd: !!js process.cwd()
persona: 'This composition test must not start a model turn.' persona: 'This composition test must not start a model turn.'
workspaceContext: false workspaceContext: false
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write examples/headless-agent/README.md # pnpm run verify-translation-pairing --write examples/headless-agent/README.md
README.md: 670a91ab402cf98585f2ece70787beb3b4aaf4dc README.md: e00f3d2d4fd21a860239f3d3a3e5eb2d7520f14a
README.zh.md: 6c8b3b5694403c5e09f2904f5c3ca18fe569163e README.zh.md: 6cd845783b1c112ba73676474b176ec28a4d0b78
+5 -9
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md) English | [中文](README.zh.md)
Headless one-shot agent wiring: DeepSeek V4 + local bash and filesystem tools + subagent delegation + workflows and fresh-agent Ralph iteration + `todo_write` + JSONL persistence, with [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo) as the app front door. This directory owns the replay and real-model test composition for a headless coding agent: DeepSeek V4 + local bash and filesystem tools + subagent delegation + workflows and fresh-agent Ralph iteration + `todo_write` + JSONL persistence. It explicitly mounts the shared agent spine, one root agent, persistence, and checkpoint policy; it is not a second product front door.
## Run it ## Run it
@@ -10,17 +10,13 @@ Headless one-shot agent wiring: DeepSeek V4 + local bash and filesystem tools +
# repo root .env (gitignored) or exported env: # repo root .env (gitignored) or exported env:
# DEEPSEEK_API_KEY=sk-… # DEEPSEEK_API_KEY=sk-…
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API # DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
pnpm run demo:headless "fix the failing test in this workspace" pnpm run dsh run "fix the failing test in this workspace"
pnpm run demo:headless --output-format json -- "summarize the implementation"
pnpm run demo:headless --output-format stream-json -- "run the focused tests"
``` ```
Exactly one nonblank positional task is required; quote tasks containing spaces. There is no `-p` flag. `text` prints the last text-bearing assistant message, `json` prints one DSH-native result record, and `stream-json` emits the top-level session's canonical task-turn events before that record. Child sessions surface only through parent tool events and results. The product command is [`dsh run`](../../apps/cli/README.md): it accepts one nonblank task, creates and persists a fresh session, prints the final assistant text, and exits. The root `demo:headless` script is only an alias of that command.
Each invocation creates and persists a fresh session, runs all model and tool steps in one turn, flushes, disposes, and exits. This is non-interactive automation: there is no prompt, approval, resume, second turn, or stdin context. The configured tools can mutate the launch workspace, run commands, spawn child agents, and consume provider tokens. Snapshot suites run this directory's configuration through [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts), an unexported test-only process that emits canonical session events as JSONL before its result record. That stream is test infrastructure, not a supported CLI output format. Child sessions surface only through parent tool events and results.
## Advanced configuration ## Advanced configuration
[`advanced.cordis.yml`](advanced.cordis.yml) adds Code Mode and the Cordis tools to the shipped leaf. [`advanced.cordis.yml`](advanced.cordis.yml) adds Code Mode and the Cordis tools to the test composition.
The package-level [CLI contract](../../packages/examples/cli-demo/README.md) documents output records, exit status, cancellation, persistence, and model/token effects.
+5 -9
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文 [English](README.md) | 中文
无头单次 agent(智能体)接线DeepSeek V4 + 本地 bash 与文件系统工具 + subagent 委托 + 工作流与全新 agent Ralph 迭代 + `todo_write` + JSONL 持久化,并以 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo) 作为应用入口。 本目录负责 headless coding agent(智能体)的回放和真实模型测试组装DeepSeek V4 + 本地 bash 与文件系统工具 + subagent 委托 + 工作流与全新 agent Ralph 迭代 + `todo_write` + JSONL 持久化。本目录显式挂载共享 agent 主干、一个根 agent、持久化和检查点策略;它不是第二个产品入口。
## 运行 ## 运行
@@ -10,17 +10,13 @@
# repo root .env (gitignored) or exported env: # repo root .env (gitignored) or exported env:
# DEEPSEEK_API_KEY=sk-… # DEEPSEEK_API_KEY=sk-…
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API # DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
pnpm run demo:headless "fix the failing test in this workspace" pnpm run dsh run "fix the failing test in this workspace"
pnpm run demo:headless --output-format json -- "summarize the implementation"
pnpm run demo:headless --output-format stream-json -- "run the focused tests"
``` ```
必须提供一个且仅一个非空任务位置参数;含空格的任务需要加引号。没有 `-p` 标志。`text` 打印最后一条包含文本的 assistant 消息,`json` 打印一条 DSH 原生结果记录,`stream-json` 则在该记录之前发出顶层会话的规范任务轮次事件。子会话只通过父会话的工具事件和结果对外显示 产品命令是 [`dsh run`](../../apps/cli/README.md):它接受一项非空任务,创建并持久化新会话,打印最终 assistant 文本,然后退出。根目录的 `demo:headless` 脚本只是该命令的别名
每次调用都会创建并持久化新会话,在一个轮次中运行所有模型和工具步骤,然后刷写持久化数据、执行 dispose(资源释放),再退出。这是非交互式自动化:没有提示符、批准、恢复、第二轮次或 stdin 上下文。已配置工具可以修改启动时所在的工作区、运行命令、spawn 子 agent,并消耗提供方 token 快照套件通过 [`tests/fixtures/headless-driver.ts`](tests/fixtures/headless-driver.ts) 运行本目录的配置。这个未导出且仅供测试使用的进程会在结果记录之前,以 JSONL 发出规范会话事件。该事件流属于测试基础设施,不是受支持的 CLI(命令行界面)输出格式。子会话只通过父会话的工具事件和结果对外显示
## 高级配置 ## 高级配置
[`advanced.cordis.yml`](advanced.cordis.yml) 在已交付叶节点上添加 Code Mode 和 Cordis 工具。 [`advanced.cordis.yml`](advanced.cordis.yml) 在测试组装中添加 Code Mode 和 Cordis 工具。
这份包级 [CLI(命令行界面)契约](../../packages/examples/cli-demo/README.md) 说明输出记录、退出状态、取消、持久化以及模型/token 影响。
@@ -1,6 +1,6 @@
# Replay counterpart to advanced.cordis.yml. It includes the base `cordis.yml` # Replay counterpart to advanced.cordis.yml. It includes the base `cordis.yml`
# directly — a config patch cannot target an entry behind a nested include — and # directly — a config patch cannot target an entry behind a nested include — and
# restates advanced.cordis.yml's overlay (the cli-agent config plus the # restates advanced.cordis.yml's overlay (the agent and persistence configs plus the
# code-runtime and tool-cordis inserts) so the whole app config lives in one patch. # code-runtime and tool-cordis inserts) so the whole app config lives in one patch.
# It re-pins `deepseek-v4-flash`: `cordis.yml` ships `deepseek-v4-pro`, but the # It re-pins `deepseek-v4-flash`: `cordis.yml` ships `deepseek-v4-pro`, but the
# recorded corpus (request headers, provenance) was captured on flash, so replay # recorded corpus (request headers, provenance) was captured on flash, so replay
@@ -15,15 +15,14 @@
- id: llm-deepseek - id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek' name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true disabled: true
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: deepseek-official agents:
model: deepseek-v4-flash - id: main
persistenceRoot: './.sessions' provider: deepseek-official
# Replay fixtures are raw JSONL; the whole-config patch must restate model: deepseek-v4-flash
# the compression choice or the default zstd frames hide the logs. cwd: !!js process.cwd()
persistenceCompression: none
workspaceContext: workspaceContext:
maxBytes: 65536 maxBytes: 65536
tools: tools:
@@ -32,6 +31,13 @@
You are headless-agent, a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. You are headless-agent, 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. Verify your work by running the code or tests. Keep answers brief and factual.
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
# Replay fixtures are raw JSONL; the whole-config patch must restate
# the compression choice or the default zstd frames hide the logs.
compression: none
- insert: - insert:
- id: code-runtime - id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker' name: '@deepseek-ai/dsh-code-runtime-worker'
+12 -6
View File
@@ -4,13 +4,14 @@
config: config:
path: ./cordis.yml path: ./cordis.yml
patches: patches:
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: deepseek-official agents:
model: deepseek-v4-pro - id: main
persistenceRoot: './.sessions' provider: deepseek-official
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'" model: deepseek-v4-pro
cwd: !!js process.cwd()
workspaceContext: workspaceContext:
maxBytes: 65536 maxBytes: 65536
tools: tools:
@@ -19,6 +20,11 @@
You are headless-agent, a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. You are headless-agent, 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. Verify your work by running the code or tests. Keep answers brief and factual.
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
- insert: - insert:
- id: code-runtime - id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker' name: '@deepseek-ai/dsh-code-runtime-worker'
+11 -12
View File
@@ -1,9 +1,9 @@
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand. <!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
Run `pnpm run gen-doc-graphs` to regenerate. --> Run `pnpm run gen-doc-graphs` to regenerate. -->
# Headless Agent App Composition # Headless Agent Snapshot Composition
The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session. The headless snapshot composition combines the real DeepSeek adapter and coding capabilities with one explicitly configured persisted top-level agent; its JSONL driver is test-only.
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -18,15 +18,12 @@ flowchart LR
cfg --> plugin_headless_subprocess cfg --> plugin_headless_subprocess
plugin_headless_bash["bash<br/>@deepseek-ai/dsh-bash-local"] plugin_headless_bash["bash<br/>@deepseek-ai/dsh-bash-local"]
cfg --> plugin_headless_bash cfg --> plugin_headless_bash
plugin_headless_cli_agent["cli-agent<br/>@deepseek-ai/dsh-cli-demo"] plugin_headless_agent_spine["agent-spine<br/>@deepseek-ai/dsh-agent-spine-demo"]
cfg --> plugin_headless_cli_agent cfg --> plugin_headless_agent_spine
plugin_headless_cli_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"] plugin_headless_persistence["persistence<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
plugin_headless_cli_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"] cfg --> plugin_headless_persistence
plugin_headless_cli_agent --> frontdoor_cli["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"] plugin_headless_checkpoint_policy["checkpoint-policy<br/>@deepseek-ai/dsh-session-checkpoint-policy"]
bundle_agent_core --> spine_llm["ctx.llm"] cfg --> plugin_headless_checkpoint_policy
bundle_agent_core --> spine_sessions["ctx.sessions"]
bundle_agent_core --> spine_tools["ctx.tools + tool-bash"]
bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"]
plugin_headless_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"] plugin_headless_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"]
cfg --> plugin_headless_token_meter cfg --> plugin_headless_token_meter
plugin_headless_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"] plugin_headless_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"]
@@ -70,7 +67,9 @@ flowchart LR
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` | | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` | | `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
| `bash` | `@deepseek-ai/dsh-bash-local` | | `bash` | `@deepseek-ai/dsh-bash-local` |
| `cli-agent` | `@deepseek-ai/dsh-cli-demo` | | `agent-spine` | `@deepseek-ai/dsh-agent-spine-demo` |
| `persistence` | `@deepseek-ai/dsh-session-persistence-jsonl` |
| `checkpoint-policy` | `@deepseek-ai/dsh-session-checkpoint-policy` |
| `token-meter` | `@deepseek-ai/dsh-token-meter` | | `token-meter` | `@deepseek-ai/dsh-token-meter` |
| `compact-basic` | `@deepseek-ai/dsh-compact-basic` | | `compact-basic` | `@deepseek-ai/dsh-compact-basic` |
| `session-projection` | `@deepseek-ai/dsh-session-projection` | | `session-projection` | `@deepseek-ai/dsh-session-projection` |
+20 -10
View File
@@ -40,17 +40,18 @@
config: config:
timeoutMs: 60000 timeoutMs: 60000
# The app bundle pre-creates one fresh `main` agent per invocation. # The example composition pre-creates one fresh `main` agent for its test driver.
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: deepseek-official agents:
# Stays on flash: the goal/ralph replay corpora were recorded on it, and - id: main
# their nested-include overlays cannot re-pin the app config (a config provider: deepseek-official
# patch cannot target an entry behind a nested include). # Stays on flash: the goal/ralph replay corpora were recorded on it, and
model: deepseek-v4-flash # their nested-include overlays cannot re-pin the app config (a config
persistenceRoot: './.sessions' # patch cannot target an entry behind a nested include).
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'" model: deepseek-v4-flash
cwd: !!js process.cwd()
workspaceContext: workspaceContext:
maxBytes: 65536 maxBytes: 65536
persona: | persona: |
@@ -59,6 +60,15 @@
Verify your work by running the code or tests. Keep answers brief and Verify your work by running the code or tests. Keep answers brief and
factual. factual.
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
# Summarize an older range when derived history approaches the context window. # Summarize an older range when derived history approaches the context window.
- id: token-meter - id: token-meter
name: '@deepseek-ai/dsh-token-meter' name: '@deepseek-ai/dsh-token-meter'
+11 -5
View File
@@ -12,15 +12,21 @@
- id: llm-deepseek - id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek' name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true disabled: true
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: cli-mock agents:
model: cli-mock - id: main
persistenceRoot: './.sessions' provider: cli-mock
model: cli-mock
cwd: !!js process.cwd()
workspaceContext: false workspaceContext: false
dshHome: './.dsh-home' dshHome: './.dsh-home'
skills: skills:
local: local:
agentsHome: './.agents-home' agentsHome: './.agents-home'
persona: 'Keyless headless-agent smoke.' persona: 'Keyless headless-agent smoke.'
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
@@ -7,10 +7,15 @@
config: config:
baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
thinking: disabled thinking: disabled
- id: cli-agent - id: agent-spine
config: config:
provider: deepseek-official agents:
model: deepseek-v4-flash - id: main
persistenceRoot: './.sessions' provider: deepseek-official
model: deepseek-v4-flash
cwd: !!js process.cwd()
workspaceContext: false workspaceContext: false
persona: 'Keyless DeepSeek adapter defaults snapshot.' persona: 'Keyless DeepSeek adapter defaults snapshot.'
- id: persistence
config:
root: './.sessions'
@@ -0,0 +1,8 @@
- id: api-gateway
config:
provider: cli-mock
model: cli-mock
- insert:
- id: cli-mock-llm
name: './snapshot-fixtures/cli-mock-llm.ts'
@@ -17,12 +17,22 @@
- id: seed-goal - id: seed-goal
name: './seed-goal.ts' name: './seed-goal.ts'
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: cli-mock agents:
model: cli-mock - id: main
provider: cli-mock
model: cli-mock
cwd: !!js process.cwd()
persona: 'Test the persisted goal domain.' persona: 'Test the persisted goal domain.'
persistenceRoot: './.sessions'
persistenceCompression: none
workspaceContext: false workspaceContext: false
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: none
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
@@ -0,0 +1,33 @@
#!/usr/bin/env node
/** Snapshot-only Loader driver: stream one fixture turn as canonical JSONL. */
import type { Context } from 'cordis'
import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
const NAME = 'headless-test-driver'
const [configPath, ...taskParts] = process.argv.slice(2)
if (configPath === undefined || taskParts.length === 0 || taskParts.every(part => part.trim() === '')) {
throw new Error(`${NAME}: expected <config-path> <task...>`)
}
const uninstallFailLoud = installFailLoud(NAME)
let ctx: Context | undefined
try {
loadEnv(NAME)
ctx = await boot(NAME, resolveConfigPath(configPath, undefined))
const result = await runFixtureTurn(ctx, {
task: taskParts.join(' '),
onEvent: (sessionId: string, event: SessionEvent) => {
process.stdout.write(`${JSON.stringify({ type: 'session_event', sessionId, event })}\n`)
},
})
process.stdout.write(`${JSON.stringify(result)}\n`)
} catch (error: unknown) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
process.exitCode = 1
} finally {
await ctx?.fiber.dispose()
uninstallFailLoud()
}
@@ -10,8 +10,8 @@ import { writeFile } from 'node:fs/promises'
import { createServer } from 'node:http' import { createServer } from 'node:http'
import { once } from 'node:events' import { once } from 'node:events'
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
import { recordFeedback } from '@deepseek-ai/dsh-command-feedback' import { recordFeedback } from '@deepseek-ai/dsh-command-feedback'
import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
const configPath = process.argv[2] const configPath = process.argv[2]
if (configPath === undefined) throw new Error('telemetry-otel driver requires a config path') if (configPath === undefined) throw new Error('telemetry-otel driver requires a config path')
@@ -35,14 +35,14 @@ const ctx = await boot('telemetry-otel-e2e', resolveConfigPath(configPath, undef
try { try {
// The fixture credential rides the model-visible user message; the exported // The fixture credential rides the model-visible user message; the exported
// copy must scrub it while the canonical log keeps the original bytes. // copy must scrub it while the canonical log keeps the original bytes.
await runOneShot(ctx, { task: 'prove telemetry with key sk-e2efixture1234567890' }) await runFixtureTurn(ctx, { task: 'prove telemetry with key sk-e2efixture1234567890' })
const mode = process.env.DSH_TELEMETRY_E2E_MODE ?? 'FULL' const mode = process.env.DSH_TELEMETRY_E2E_MODE ?? 'FULL'
if (mode !== 'FULL') { if (mode !== 'FULL') {
const [agent] = ctx.get('agents')?.roots() ?? [] const [agent] = ctx.get('agents')?.roots() ?? []
if (agent === undefined) throw new Error('telemetry-otel driver requires one root agent') if (agent === undefined) throw new Error('telemetry-otel driver requires one root agent')
recordFeedback(agent.session, 'fixture feedback') recordFeedback(agent.session, 'fixture feedback')
if (mode === 'FEEDBACK_ONLY') { if (mode === 'FEEDBACK_ONLY') {
await runOneShot(ctx, { task: 'post-feedback private suffix' }) await runFixtureTurn(ctx, { task: 'post-feedback private suffix' })
} }
} }
} finally { } finally {
@@ -30,12 +30,22 @@
exporter: exporter:
url: !!js process.env.DSH_TELEMETRY_E2E_URL url: !!js process.env.DSH_TELEMETRY_E2E_URL
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: cli-mock agents:
model: cli-mock - id: main
provider: cli-mock
model: cli-mock
cwd: !!js process.cwd()
persona: 'Test the session-telemetry-otel plugin.' persona: 'Test the session-telemetry-otel plugin.'
persistenceRoot: './.sessions'
persistenceCompression: 'none'
workspaceContext: false workspaceContext: false
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: 'none'
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
@@ -2,15 +2,15 @@
/** Test driver that sends two turns through one Headless Loader composition. */ /** Test driver that sends two turns through one Headless Loader composition. */
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts' import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
const configPath = process.argv[2] const configPath = process.argv[2]
if (configPath === undefined) throw new Error('time-context driver requires a config path') if (configPath === undefined) throw new Error('time-context driver requires a config path')
const ctx = await boot('time-context-e2e', resolveConfigPath(configPath, undefined)) const ctx = await boot('time-context-e2e', resolveConfigPath(configPath, undefined))
try { try {
await runOneShot(ctx, { task: 'first' }) await runFixtureTurn(ctx, { task: 'first' })
await runOneShot(ctx, { task: 'second' }) await runFixtureTurn(ctx, { task: 'second' })
} finally { } finally {
await ctx.fiber.dispose() await ctx.fiber.dispose()
} }
@@ -12,12 +12,22 @@
- id: time-context - id: time-context
name: '@deepseek-ai/dsh-time-context' name: '@deepseek-ai/dsh-time-context'
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: time-context-mock agents:
model: time-context-mock - id: main
provider: time-context-mock
model: time-context-mock
cwd: !!js process.cwd()
persona: 'Test the time-context plugin.' persona: 'Test the time-context plugin.'
persistenceRoot: './.sessions'
persistenceCompression: 'none'
workspaceContext: false workspaceContext: false
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: 'none'
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
@@ -1,4 +1,4 @@
import { readFile, readdir, writeFile } from 'node:fs/promises' import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http' import { createServer } from 'node:http'
import type { IncomingMessage, ServerResponse } from 'node:http' import type { IncomingMessage, ServerResponse } from 'node:http'
import { delimiter, dirname, join } from 'node:path' import { delimiter, dirname, join } from 'node:path'
@@ -14,6 +14,10 @@ import {
type NormalizeContext, type NormalizeContext,
} from '@deepseek-ai/dsh-acp-snapshot' } from '@deepseek-ai/dsh-acp-snapshot'
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
import {
decompressZstdFrame,
scanZstdFrames,
} from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.ts'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
const snapshotsDir = join(dirname(fileURLToPath(import.meta.url)), 'snapshots') const snapshotsDir = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
@@ -43,10 +47,14 @@ const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url)) const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
const startupFailureConfigPath = fileURLToPath(new URL('./fixtures/startup-activation-error/cordis.yml', import.meta.url)) const startupFailureConfigPath = fileURLToPath(new URL('./fixtures/startup-activation-error/cordis.yml', import.meta.url))
const startupFailureExpected = join(snapshotsDir, 'startup-activation-error', 'stderr.expected.txt') const startupFailureExpected = join(snapshotsDir, 'startup-activation-error', 'stderr.expected.txt')
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url)) const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url)) const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
const deepseekDefaultsConfigPath = fileURLToPath(new URL('./fixtures/deepseek-defaults.cordis.yml', import.meta.url)) const deepseekDefaultsConfigPath = fileURLToPath(new URL('./fixtures/deepseek-defaults.cordis.yml', import.meta.url))
const dshRunOverlayPath = fileURLToPath(new URL('./fixtures/dsh-run.cordis.yml', import.meta.url))
const dshRunSessionExpected = join(snapshotsDir, 'dsh-run', 'session.expected.jsonl')
const cliMockLlmPluginPath = fileURLToPath(new URL('./fixtures/cli-mock-llm.ts', import.meta.url))
const refreshing = process.env.DSH_SNAPSHOT === 'refresh' const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
interface JsonObject { interface JsonObject {
@@ -167,23 +175,76 @@ async function scenarioPrompt(dir: string, label: string): Promise<string> {
return prompt return prompt
} }
async function persistedLogs(cwd: string): Promise<PersistedLog[]> { async function readPersistedLog(file: string): Promise<string> {
const root = join(cwd, '.sessions') const content = await readFile(file)
const files = (await readdir(root, { recursive: true })).filter(file => file.endsWith('.jsonl')) if (!file.endsWith('.zstd')) return content.toString('utf8')
const scan = scanZstdFrames(content)
if (scan.tornStart !== undefined) throw new Error(`persisted snapshot log has a torn Zstandard frame: ${file}`)
const decoded: Buffer[] = []
for (const frame of scan.frames) {
decoded.push(await decompressZstdFrame(content.subarray(frame.start, frame.end)))
}
return Buffer.concat(decoded).toString('utf8')
}
async function persistedLogs(cwd: string, root: string = join(cwd, '.sessions')): Promise<PersistedLog[]> {
const files = (await readdir(root, { recursive: true }))
.filter(file => file.endsWith('.jsonl') || file.endsWith('.jsonl.zstd'))
return Promise.all(files.map(async (file) => { return Promise.all(files.map(async (file) => {
const content = await readFile(join(root, file), 'utf8') const content = await readPersistedLog(join(root, file))
return { content, header: parseJsonl(content)[0] ?? {} } return { content, header: parseJsonl(content)[0] ?? {} }
})) }))
} }
describe('headless stream-json snapshots', () => { describe('headless stream-json snapshots', () => {
it('runs one task through the product dsh run command', async () => {
const task = 'Prove the product dsh run path with one real tool round trip.'
const result = await runLoaderSmoke({
label: 'product dsh run snapshot',
tempDirPrefix: 'headless-snapshot-dsh-run-',
binScript: dshBinScript,
configPath: dshRunOverlayPath,
binArgs: ['run', '--patch', dshRunOverlayPath, task],
tsconfigPath,
env: {
DSH_PERMISSION_MODE: 'danger-full-access',
DSH_TELEMETRY_DISABLED: '1',
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
},
prepare: async (cwd) => {
const fixtureDir = join(cwd, '.dsh', 'profiles', 'headless', 'snapshot-fixtures')
await mkdir(fixtureDir, { recursive: true })
await Promise.all([
copyFile(cliMockLlmPluginPath, join(fixtureDir, 'cli-mock-llm.ts')),
writeFile(join(fixtureDir, 'package.json'), '{"type":"module"}\n'),
])
},
inspect: async (cwd) => {
const logs = await persistedLogs(cwd, join(cwd, '.dsh', 'sessions'))
expect(logs).toHaveLength(1)
const actual = logs[0]
if (actual === undefined) throw new Error('dsh run did not persist its session')
const context = contextFromLogs([actual.content])
const session = scrubRequestHeaders(normalizeSessionLog(actual.content, context))
if (refreshing) await writeFile(dshRunSessionExpected, session)
expect(session).toBe(await readFile(dshRunSessionExpected, 'utf8'))
expect(session).toContain(task)
expect(session).toContain('CLI tool round trip complete: CLI_TOOL_ROUND_TRIP')
},
})
expect(result.stdout).toBe('CLI tool round trip complete: CLI_TOOL_ROUND_TRIP\n')
expect(result.stderr).toMatch(/^dsh: observing at http:\/\/127\.0\.0\.1:\d+\n$/u)
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('prints the original Loader activation error through the assembled one-shot app', async () => { it('prints the original Loader activation error through the assembled one-shot app', async () => {
const result = await runLoaderSmoke({ const result = await runLoaderSmoke({
label: 'headless startup activation error snapshot', label: 'headless startup activation error snapshot',
tempDirPrefix: 'headless-snapshot-startup-error-', tempDirPrefix: 'headless-snapshot-startup-error-',
binScript, binScript,
libBinScript: binScript,
configPath: startupFailureConfigPath, configPath: startupFailureConfigPath,
binArgs: ['--config', startupFailureConfigPath, '--output-format', 'stream-json', 'unreachable task'], binArgs: [startupFailureConfigPath, 'unreachable task'],
tsconfigPath, tsconfigPath,
expectedExitCode: 1, expectedExitCode: 1,
}) })
@@ -199,8 +260,9 @@ describe('headless stream-json snapshots', () => {
label: 'provider retry headless stream-json snapshot', label: 'provider retry headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-provider-retry-', tempDirPrefix: 'headless-snapshot-provider-retry-',
binScript, binScript,
libBinScript: binScript,
configPath: retryConfigPath, configPath: retryConfigPath,
binArgs: ['--config', retryConfigPath, '--output-format', 'stream-json', prompt], binArgs: [retryConfigPath, prompt],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT: 'replay', DSH_SNAPSHOT: 'replay',
@@ -239,8 +301,9 @@ describe('headless stream-json snapshots', () => {
label: 'compaction recovery headless stream-json snapshot', label: 'compaction recovery headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-compaction-recovery-', tempDirPrefix: 'headless-snapshot-compaction-recovery-',
binScript, binScript,
libBinScript: binScript,
configPath: compactionConfigPath, configPath: compactionConfigPath,
binArgs: ['--config', compactionConfigPath, '--output-format', 'stream-json', prompt], binArgs: [compactionConfigPath, prompt],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT: 'replay', DSH_SNAPSHOT: 'replay',
@@ -307,8 +370,9 @@ describe('headless stream-json snapshots', () => {
label: 'missing-credential headless stream-json snapshot', label: 'missing-credential headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-missing-credential-', tempDirPrefix: 'headless-snapshot-missing-credential-',
binScript, binScript,
libBinScript: binScript,
configPath: credentialsConfigPath, configPath: credentialsConfigPath,
binArgs: ['--config', credentialsConfigPath, '--output-format', 'stream-json', 'say pong'], binArgs: [credentialsConfigPath, 'say pong'],
tsconfigPath, tsconfigPath,
env: { env: {
// First-run posture: no key in the environment, none under ./.dsh. // First-run posture: no key in the environment, none under ./.dsh.
@@ -344,8 +408,9 @@ describe('headless stream-json snapshots', () => {
label: 'invalid-credential headless stream-json snapshot', label: 'invalid-credential headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-invalid-credential-', tempDirPrefix: 'headless-snapshot-invalid-credential-',
binScript, binScript,
libBinScript: binScript,
configPath: credentialsConfigPath, configPath: credentialsConfigPath,
binArgs: ['--config', credentialsConfigPath, '--output-format', 'stream-json', 'say pong'], binArgs: [credentialsConfigPath, 'say pong'],
tsconfigPath, tsconfigPath,
env: { env: {
// A key that exists but no HTTP header can carry — the paste this // A key that exists but no HTTP header can carry — the paste this
@@ -378,8 +443,9 @@ describe('headless stream-json snapshots', () => {
label: 'reasoning effort headless stream-json snapshot', label: 'reasoning effort headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-reasoning-effort-', tempDirPrefix: 'headless-snapshot-reasoning-effort-',
binScript, binScript,
libBinScript: binScript,
configPath: reasoningConfigPath, configPath: reasoningConfigPath,
binArgs: ['--config', reasoningConfigPath, '--output-format', 'stream-json', 'prove dynamic reasoning effort'], binArgs: [reasoningConfigPath, 'prove dynamic reasoning effort'],
tsconfigPath, tsconfigPath,
}) })
@@ -420,12 +486,10 @@ describe('headless stream-json snapshots', () => {
label: 'DeepSeek adapter defaults headless stream-json snapshot', label: 'DeepSeek adapter defaults headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-deepseek-defaults-', tempDirPrefix: 'headless-snapshot-deepseek-defaults-',
binScript, binScript,
libBinScript: binScript,
configPath: deepseekDefaultsConfigPath, configPath: deepseekDefaultsConfigPath,
binArgs: [ binArgs: [
'--config',
deepseekDefaultsConfigPath, deepseekDefaultsConfigPath,
'--output-format',
'stream-json',
'return the deterministic response', 'return the deterministic response',
], ],
tsconfigPath, tsconfigPath,
@@ -480,8 +544,9 @@ describe('headless stream-json snapshots', () => {
label: 'advanced headless stream-json snapshot', label: 'advanced headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-advanced-', tempDirPrefix: 'headless-snapshot-advanced-',
binScript, binScript,
libBinScript: binScript,
configPath: advancedConfigPath, configPath: advancedConfigPath,
binArgs: ['--config', advancedConfigPath, '--output-format', 'stream-json', prompt], binArgs: [advancedConfigPath, prompt],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT: 'replay', DSH_SNAPSHOT: 'replay',
@@ -551,8 +616,9 @@ describe('headless stream-json snapshots', () => {
label: 'goal tools headless stream-json snapshot', label: 'goal tools headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-goal-tools-', tempDirPrefix: 'headless-snapshot-goal-tools-',
binScript, binScript,
libBinScript: binScript,
configPath: goalConfigPath, configPath: goalConfigPath,
binArgs: ['--config', goalConfigPath, '--output-format', 'stream-json', prompt], binArgs: [goalConfigPath, prompt],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT: 'replay', DSH_SNAPSHOT: 'replay',
@@ -607,8 +673,9 @@ describe('headless stream-json snapshots', () => {
label: 'Ralph loop headless stream-json snapshot', label: 'Ralph loop headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-ralph-loop-', tempDirPrefix: 'headless-snapshot-ralph-loop-',
binScript, binScript,
libBinScript: binScript,
configPath: ralphConfigPath, configPath: ralphConfigPath,
binArgs: ['--config', ralphConfigPath, '--output-format', 'stream-json', prompt], binArgs: [ralphConfigPath, prompt],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT: 'replay', DSH_SNAPSHOT: 'replay',
@@ -688,8 +755,9 @@ describe('headless stream-json snapshots', () => {
label: 'headless persistent PTY snapshot', label: 'headless persistent PTY snapshot',
tempDirPrefix: 'headless-snapshot-pty-', tempDirPrefix: 'headless-snapshot-pty-',
binScript, binScript,
libBinScript: binScript,
configPath: ptyConfigPath, configPath: ptyConfigPath,
binArgs: ['--config', ptyConfigPath, '--output-format', 'stream-json', prompt], binArgs: [ptyConfigPath, prompt],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT: 'replay', DSH_SNAPSHOT: 'replay',
@@ -9,7 +9,7 @@ import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-l
import { PREPARED_ENTRY_FILENAME, prepareDshPlugin } from '@deepseek-ai/dsh-repository-plugin' import { PREPARED_ENTRY_FILENAME, prepareDshPlugin } from '@deepseek-ai/dsh-repository-plugin'
import type { SessionEvent } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session'
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url)) const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
const configPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url)) const configPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const decompress = promisify(zstdDecompress) const decompress = promisify(zstdDecompress)
@@ -21,8 +21,9 @@ describe('headless-agent keyless smoke', () => {
label: 'headless-agent', label: 'headless-agent',
tempDirPrefix: 'headless-agent-smoke-', tempDirPrefix: 'headless-agent-smoke-',
binScript, binScript,
libBinScript: binScript,
configPath, configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'prove the tool path'], binArgs: [configPath, 'prove the tool path'],
tsconfigPath, tsconfigPath,
inspect: async (cwd) => { inspect: async (cwd) => {
const files = await readdir(cwd, { recursive: true }) const files = await readdir(cwd, { recursive: true })
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url)) const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url)) const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const hasKey = Boolean(process.env.DEEPSEEK_API_KEY) const hasKey = Boolean(process.env.DEEPSEEK_API_KEY)
@@ -16,9 +16,9 @@ describe.skipIf(!hasKey)('headless-agent with real model', () => {
label: 'headless-agent real model', label: 'headless-agent real model',
tempDirPrefix: 'headless-agent-real-', tempDirPrefix: 'headless-agent-real-',
binScript, binScript,
libBinScript: binScript,
configPath, configPath,
binArgs: [ binArgs: [
'--config',
configPath, configPath,
'Read task.txt, replace its complete contents with exactly "value=after" followed by a newline, read it again, and report briefly.', 'Read task.txt, replace its complete contents with exactly "value=after" followed by a newline, read it again, and report briefly.',
], ],
@@ -14,7 +14,7 @@ const replayFixture = join(fixtureDir, 'replay.jsonl')
const replayOverride = join(fixtureDir, 'replay.override.json') const replayOverride = join(fixtureDir, 'replay.override.json')
const sessionExpected = join(fixtureDir, 'session.expected.jsonl') const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
const configPath = fileURLToPath(new URL('../semantic-checkpoint.cordis.snapshot.yml', import.meta.url)) const configPath = fileURLToPath(new URL('../semantic-checkpoint.cordis.snapshot.yml', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url)) const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const sessionId = SessionId('semantic-checkpoint-unknown-outcome') const sessionId = SessionId('semantic-checkpoint-unknown-outcome')
const refreshing = process.env.DSH_SNAPSHOT === 'refresh' const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
@@ -87,8 +87,9 @@ describe('semantic checkpoint recovery snapshot', () => {
label: 'semantic checkpoint headless stream-json snapshot', label: 'semantic checkpoint headless stream-json snapshot',
tempDirPrefix: 'dsh-semantic-snapshot-', tempDirPrefix: 'dsh-semantic-snapshot-',
binScript, binScript,
libBinScript: binScript,
configPath, configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', task], binArgs: [configPath, task],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT_FILE: replayFixture, DSH_SNAPSHOT_FILE: replayFixture,
@@ -0,0 +1,33 @@
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"permission/preset","seq":0,"time":0,"data":{"preset":"danger-full-access"}}
{"type":"sandbox/mode","seq":1,"time":0,"data":{"mode":"danger-full-access"}}
{"type":"approval/policy","seq":2,"time":0,"data":{"policy":"never"}}
{"type":"agent/inbox/spliced","seq":3,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prove the product dsh run path with one real tool round trip."}],"source":{"kind":"user","rpcId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}
{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":7,"time":0,"data":{"content":[{"type":"text","text":"Prove the product dsh run path with one real tool round trip."}],"source":{"kind":"user","rpcId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":9,"time":0,"data":{"title":"Prove the product dsh run","messageSeqs":[7],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":11,"time":0,"data":{"provider":"cli-mock","model":"cli-mock"}}
{"type":"session/title-llm-request","seq":12,"time":0,"data":{"titleProvider":"session-title-first-message-llm","messageSeqs":[7],"route":{"provider":"cli-mock","model":"cli-mock"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":7,\"text\":\"Prove the product dsh run path with one real tool round trip.\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}
{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"cli-smoke-call","name":"bash","argumentsDelta":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}}}
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}}}}
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":11,"outputTokens":3,"cacheReadTokens":2}}}}
{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}],"source":{"kind":"model","provider":"cli-mock","model":"cli-mock"},"id":"{{sessionId}}"},"usage":{"inputTokens":11,"outputTokens":3,"cacheReadTokens":2}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
{"type":"tool/call","seq":19,"time":0,"data":{"turn":1,"step":1,"callId":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}}
{"type":"tool/result","seq":20,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"cli-smoke-call"},"content":[{"type":"tool-result","toolCallId":"cli-smoke-call","content":[{"type":"text","text":"CLI_TOOL_ROUND_TRIP"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
{"type":"step/end","seq":21,"time":0,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":22,"time":0,"data":{"turn":1,"step":2}}
{"type":"request/header","seq":23,"time":0,"data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"off"},"system":"{{system}}","tools":"{{tools}}"},"reason":"change"}}
{"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"CLI tool round trip complete: CLI_TOOL_ROUND_TRIP"}}}
{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CLI tool round trip complete: CLI_TOOL_ROUND_TRIP"}}}}
{"type":"assistant/chunk","seq":27,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":7,"outputTokens":5,"reasoningTokens":1}}}}
{"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":29,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"CLI tool round trip complete: CLI_TOOL_ROUND_TRIP"}],"source":{"kind":"model","provider":"cli-mock","model":"cli-mock"},"id":"{{sessionId}}"},"usage":{"inputTokens":7,"outputTokens":5,"reasoningTokens":1}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
{"type":"step/end","seq":30,"time":0,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":31,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -1,3 +1,3 @@
dsh-cli-demo: dsh-cli-demo: plugin tree failed to load: failed to apply loader entry include (cordis:include): failed to apply loader entry activation-error (./activation-error.mjs): startup activation snapshot failure headless-test-driver: plugin tree failed to load: failed to apply loader entry include (cordis:include): failed to apply loader entry activation-error (./activation-error.mjs): startup activation snapshot failure
Error: startup activation snapshot failure Error: startup activation snapshot failure
at activation-error-fixture at activation-error-fixture
@@ -19,7 +19,7 @@ const fixtureDir = fileURLToPath(new URL('./subagent-diagnostic-snapshots/descri
const replayOverride = join(fixtureDir, 'replay.override.json') const replayOverride = join(fixtureDir, 'replay.override.json')
const parentExpected = join(fixtureDir, 'parent.expected.jsonl') const parentExpected = join(fixtureDir, 'parent.expected.jsonl')
const configPath = fileURLToPath(new URL('../subagent-diagnostic.cordis.snapshot.yml', import.meta.url)) const configPath = fileURLToPath(new URL('../subagent-diagnostic.cordis.snapshot.yml', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url)) const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const parentId = SessionId('subagent-diagnostic-parent') const parentId = SessionId('subagent-diagnostic-parent')
const childId = SessionId('subagent-diagnostic-child') const childId = SessionId('subagent-diagnostic-child')
@@ -77,8 +77,9 @@ describe('descriptor-less cold child diagnostic snapshot', () => {
label: 'subagent diagnostic headless stream-json snapshot', label: 'subagent diagnostic headless stream-json snapshot',
tempDirPrefix: 'dsh-subagent-diag-', tempDirPrefix: 'dsh-subagent-diag-',
binScript, binScript,
libBinScript: binScript,
configPath, configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', task], binArgs: [configPath, task],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT_FILE: replayOverride, DSH_SNAPSHOT_FILE: replayOverride,
@@ -20,7 +20,7 @@ const childReplay = join(fixtureDir, 'child.replay.jsonl')
const parentExpected = join(fixtureDir, 'parent.expected.jsonl') const parentExpected = join(fixtureDir, 'parent.expected.jsonl')
const childExpected = join(fixtureDir, 'child.expected.jsonl') const childExpected = join(fixtureDir, 'child.expected.jsonl')
const configPath = fileURLToPath(new URL('../subagent-inheritance.cordis.snapshot.yml', import.meta.url)) const configPath = fileURLToPath(new URL('../subagent-inheritance.cordis.snapshot.yml', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url)) const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const sessionId = SessionId('subagent-inheritance-parent') const sessionId = SessionId('subagent-inheritance-parent')
const refreshing = process.env.DSH_SNAPSHOT === 'refresh' const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
@@ -59,8 +59,9 @@ describe('parent-only override inheritance snapshot', () => {
label: 'subagent inheritance headless stream-json snapshot', label: 'subagent inheritance headless stream-json snapshot',
tempDirPrefix: 'dsh-subagent-inherit-', tempDirPrefix: 'dsh-subagent-inherit-',
binScript, binScript,
libBinScript: binScript,
configPath, configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', task], binArgs: [configPath, task],
tsconfigPath, tsconfigPath,
env: { env: {
// The primary fixture path must exist for llm-replay's config guard; // The primary fixture path must exist for llm-replay's config guard;
@@ -28,7 +28,7 @@ const replayOverride = join(fixtureDir, 'replay.override.json')
const sessionExpected = join(fixtureDir, 'session.expected.jsonl') const sessionExpected = join(fixtureDir, 'session.expected.jsonl')
const precedenceExpected = join(dirname(fixtureDir), 'precedence-change/session.expected.jsonl') const precedenceExpected = join(dirname(fixtureDir), 'precedence-change/session.expected.jsonl')
const configPath = fileURLToPath(new URL('../workspace-context-resume.cordis.snapshot.yml', import.meta.url)) const configPath = fileURLToPath(new URL('../workspace-context-resume.cordis.snapshot.yml', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url)) const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const sessionId = SessionId('workspace-context-resume') const sessionId = SessionId('workspace-context-resume')
const refreshing = process.env.DSH_SNAPSHOT === 'refresh' const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
@@ -119,8 +119,9 @@ describe('workspace-context resume snapshot', () => {
label: 'workspace-context resume headless stream-json snapshot', label: 'workspace-context resume headless stream-json snapshot',
tempDirPrefix: 'dsh-workspace-context-resume-', tempDirPrefix: 'dsh-workspace-context-resume-',
binScript, binScript,
libBinScript: binScript,
configPath, configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'Acknowledge the current workspace instruction.'], binArgs: [configPath, 'Acknowledge the current workspace instruction.'],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT_FILE: replayFixture, DSH_SNAPSHOT_FILE: replayFixture,
@@ -175,8 +176,9 @@ describe('workspace-context resume snapshot', () => {
label: 'workspace-context precedence-change resume snapshot', label: 'workspace-context precedence-change resume snapshot',
tempDirPrefix: 'dsh-workspace-context-precedence-', tempDirPrefix: 'dsh-workspace-context-precedence-',
binScript, binScript,
libBinScript: binScript,
configPath, configPath,
binArgs: ['--config', configPath, '--output-format', 'stream-json', 'Acknowledge the current workspace instruction.'], binArgs: [configPath, 'Acknowledge the current workspace instruction.'],
tsconfigPath, tsconfigPath,
env: { env: {
DSH_SNAPSHOT_FILE: replayFixture, DSH_SNAPSHOT_FILE: replayFixture,
@@ -32,12 +32,22 @@
# recursion budget, so the local numeric default cannot apply here. # recursion budget, so the local numeric default cannot apply here.
maxDepth: 'provider-managed' maxDepth: 'provider-managed'
- id: cli-agent - id: agent-spine
name: '@deepseek-ai/dsh-cli-demo' name: '@deepseek-ai/dsh-agent-spine-demo'
config: config:
provider: mock agents:
model: mock-delegate - id: main
provider: mock
model: mock-delegate
cwd: !!js process.cwd()
persona: 'Test SDK subagent cwd inheritance.' persona: 'Test SDK subagent cwd inheritance.'
persistenceRoot: './.sessions'
persistenceCompression: 'none'
workspaceContext: false workspaceContext: false
- id: persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: './.sessions'
compression: 'none'
- id: checkpoint-policy
name: '@deepseek-ai/dsh-session-checkpoint-policy'
@@ -2,14 +2,14 @@
/** Test driver: one delegation turn through a headless Loader composition. */ /** Test driver: one delegation turn through a headless Loader composition. */
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts' import { runFixtureTurn } from '@deepseek-ai/dsh-loader-smoke'
const configPath = process.argv[2] const configPath = process.argv[2]
if (configPath === undefined) throw new Error('sdk-subagent cwd driver requires a config path') if (configPath === undefined) throw new Error('sdk-subagent cwd driver requires a config path')
const ctx = await boot('sdk-subagent-cwd-e2e', resolveConfigPath(configPath, undefined)) const ctx = await boot('sdk-subagent-cwd-e2e', resolveConfigPath(configPath, undefined))
try { try {
await runOneShot(ctx, { task: 'delegate' }) await runFixtureTurn(ctx, { task: 'delegate' })
} finally { } finally {
await ctx.fiber.dispose() await ctx.fiber.dispose()
} }
+1 -1
View File
@@ -18,7 +18,6 @@
"@deepseek-ai/dsh-bash-env": "workspace:*", "@deepseek-ai/dsh-bash-env": "workspace:*",
"@deepseek-ai/dsh-bash-local": "workspace:*", "@deepseek-ai/dsh-bash-local": "workspace:*",
"@deepseek-ai/dsh-bash-sandbox": "workspace:*", "@deepseek-ai/dsh-bash-sandbox": "workspace:*",
"@deepseek-ai/dsh-cli-demo": "workspace:*",
"@deepseek-ai/dsh-code-runtime-worker": "workspace:*", "@deepseek-ai/dsh-code-runtime-worker": "workspace:*",
"@deepseek-ai/dsh-command-feedback": "workspace:*", "@deepseek-ai/dsh-command-feedback": "workspace:*",
"@deepseek-ai/dsh-command-goal": "workspace:*", "@deepseek-ai/dsh-command-goal": "workspace:*",
@@ -40,6 +39,7 @@
"@deepseek-ai/dsh-llm-deepseek": "workspace:*", "@deepseek-ai/dsh-llm-deepseek": "workspace:*",
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*", "@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
"@deepseek-ai/dsh-llm-replay": "workspace:*", "@deepseek-ai/dsh-llm-replay": "workspace:*",
"@deepseek-ai/dsh-loader-smoke": "workspace:*",
"@deepseek-ai/dsh-lsp": "workspace:*", "@deepseek-ai/dsh-lsp": "workspace:*",
"@deepseek-ai/dsh-lsp-local": "workspace:*", "@deepseek-ai/dsh-lsp-local": "workspace:*",
"@deepseek-ai/dsh-permission": "workspace:*", "@deepseek-ai/dsh-permission": "workspace:*",
-10
View File
@@ -519,16 +519,6 @@
"tests/**/*.ts" "tests/**/*.ts"
] ]
}, },
"packages/examples/cli-demo": {
"entry": [
"tests/**/*.spec.ts",
"tests/**/*.e2e.ts"
],
"project": [
"src/**/*.ts",
"tests/**/*.ts"
]
},
"packages/examples/jsonrpc-demo": { "packages/examples/jsonrpc-demo": {
"project": [ "project": [
"src/**/*.ts" "src/**/*.ts"
+1 -1
View File
@@ -116,7 +116,7 @@
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure && pnpm run verify-vendored-links", "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure && pnpm run verify-vendored-links",
"publish:npm-baseline": "tsx scripts/publish-npm-baseline.ts", "publish:npm-baseline": "tsx scripts/publish-npm-baseline.ts",
"dsh": "node --import tsx/esm apps/cli/src/bin.ts", "dsh": "node --import tsx/esm apps/cli/src/bin.ts",
"demo:headless": "node --import tsx packages/examples/cli-demo/src/bin.ts --config examples/headless-agent/cordis.yml", "demo:headless": "node --import tsx/esm apps/cli/src/bin.ts run",
"demo:code-mode": "node scripts/demo-code-mode.mjs", "demo:code-mode": "node scripts/demo-code-mode.mjs",
"demo:cordis": "node scripts/demo-cordis.mjs", "demo:cordis": "node scripts/demo-cordis.mjs",
"demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml", "demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml",
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bundle/headless/README.md # pnpm run verify-translation-pairing --write packages/bundle/headless/README.md
README.md: d08fb08e2aca3c4e5ccd733b37fc415d492974ca README.md: 661b377817482d22f58f22b573075722646729a2
README.zh.md: 99a64ef04c4fd8fb0c6a979d3f09f1bd98b434a0 README.zh.md: a6b91a8e60fdcc06ba23e07dcb2f4208ea1020f7

Some files were not shown because too many files have changed in this diff Show More