refactor(cli): make dsh the sole terminal front door, drop RESUME_SESSION_ID

Remove the redundant dsh-tui-demo bin and the RESUME_SESSION_ID environment
variable, leaving dsh as the one terminal entrypoint.

The dsh-tui-demo package was a plugin (the TUI app bundle mounted by dsh's
config) plus a bin that booted a leaf cordis.yml — the same job `dsh [config]`
does. The bin, its ./bin export, its built-bin.e2e.ts, the tsdown bin entry,
and the now-unused dsh-app-boot dependency are removed; the package keeps its
plugin and invariant. demo:cordis, demo:code-mode, and the tui-agent and
cordis-agent keyless PTY smokes now launch through apps/cli/src/bin.ts with the
config as the positional argument. cli-demo/acp-demo/jsonrpc-demo keep their
bins (distinct surfaces).

RESUME_SESSION_ID was the only bridge from --resume into the shipped config;
--resume now provides the id on the boot context via ctx.provide(
RESUME_SESSION_ID_KEY, id), and the four configs read it as a bare identifier
through a quoted typeof-guarded !!js expression. The TUI resumeCommand fixtures
and docs move to `dsh --resume {session}`.

Agent Note and its Chinese pair updated; config-catalog regenerated.
This commit is contained in:
Turtle
2026-07-25 12:43:59 +08:00
parent 6cd139a25b
commit 870fb1cafa
28 changed files with 76 additions and 189 deletions

View File

@@ -2,5 +2,5 @@
# 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
2026-07-24-dsh-commander-argument-adapter.md: 4decd926c7fffc8f7d24200f8b91044eaa1d00f1
2026-07-24-dsh-commander-argument-adapter.zh.md: eaccc221d362804b0aa3081d0593e9dee8af1d4c
2026-07-24-dsh-commander-argument-adapter.md: 60c47ef40cb0db833f7a2a526437b6a8ce812433
2026-07-24-dsh-commander-argument-adapter.zh.md: 41a98499036c16330263d5072aa0fa454b892a24

View File

@@ -10,12 +10,20 @@ The `dsh` CLI entry (`apps/cli`) parsed argv in three hand-rolled idioms that di
## Decision
Argv is parsed once, in `apps/cli/src/args.ts`, through a Commander adapter (the same parser the SDK bins — `create-sdk`, `dsh-scripts` — already standardize on). `parseDshArgs(argv, version)` resolves the invocation into a discriminated `DshInvocation` union: `{ mode: 'tui', config?, resume? }`, `{ mode: 'headless', prompt }`, `{ mode: 'web', host, port }`, `{ mode: 'help' | 'version', text }`, or `{ mode: 'error', message }`. Commander runs under `exitOverride()` with output captured, so it never writes or exits on its own — `--help`, `--version`, and every parse error come back as data.
Argv is parsed once, in `apps/cli/src/args.ts`, through a Commander adapter (the same parser the SDK bins — `create-sdk`, `dsh-scripts` — already standardize on). `parseDshArgs(argv, version)` resolves the invocation into a discriminated `DshInvocation` union: `{ mode: 'tui', config?, resume? }`, `{ mode: 'headless', prompt }`, `{ mode: 'web', host, port, dev }`, `{ mode: 'help' | 'version', text }`, or `{ mode: 'error', message }`. Commander runs under `exitOverride()` with output captured, so it never writes or exits on its own — `--help`, `--version`, and every parse error come back as data.
`bin.ts` calls the adapter once and switches on `mode` (closed union, `satisfies never` default), dynamic-importing only the chosen mode's module. Each mode module now consumes already-parsed values: `runTui(config, resume)`, `runHeadless(task)`, `runWeb(host, port)` — none re-reads argv. `web` is a **reserved first token**: `parseDshArgs` dispatches a leading `web` to its own Commander parser and everything else to the default TUI/headless parser, so root flags and `web` flags never share a grammar — `dsh web -p x` fails loud (`web` has no `-p`) and `dsh -p x web` is just a headless prompt whose second positional is dropped, with no cross-command leakage to guard against. Each parser reads Commander's `opts()`/`processedArgs` after `parse()` rather than through action closures. `--host` is a `.choices([LOOPBACK_HOST, ALL_INTERFACES_HOST])` and `--port` an `argParser` that range-checks 065535, moving both from the inline `runWeb` checks into the parser. Two post-parse checks preserve the "never silently start fresh" invariant: an empty `--resume=` id and an empty `-p` task each become a `mode: 'error'`, because agent-loop treats an empty resume id as no-resume and an empty prompt has nothing to run. A repeated `--resume` is Commander's natural last-wins (the old bespoke scanner rejected it; last-wins is the standard CLI behavior and needs no special case). `--version` reads this app's `package.json`.
`bin.ts` calls the adapter once and switches on `mode` (closed union, `satisfies never` default), dynamic-importing only the chosen mode's module. Each mode module now consumes already-parsed values: `runTui(config, resume)`, `runHeadless(task)`, `runWeb(host, port, dev)` — none re-reads argv. `web` is a **reserved first token**: `parseDshArgs` dispatches a leading `web` to its own Commander parser and everything else to the default TUI/headless parser, so root flags and `web` flags never share a grammar — `dsh web -p x` fails loud (`web` has no `-p`) and `dsh -p x web` is just a headless prompt whose second positional is dropped, with no cross-command leakage to guard against. Each parser reads Commander's `opts()`/`processedArgs` after `parse()` rather than through action closures. `--host` is a `.choices([LOOPBACK_HOST, ALL_INTERFACES_HOST])` and `--port` an `argParser` that range-checks 065535, moving both from the inline `runWeb` checks into the parser; `--dev` mounts the client HMR driver and bundle watch. Two post-parse checks preserve the "never silently start fresh" invariant: an empty `--resume=` id and an empty `-p` task each become a `mode: 'error'`, because agent-loop treats an empty resume id as no-resume and an empty prompt has nothing to run. A repeated `--resume` is Commander's natural last-wins (the old bespoke scanner rejected it; last-wins is the standard CLI behavior and needs no special case). `--version` reads this app's `package.json`.
`parseResumeArg` is deleted from `dsh-app-boot` (its export, its README row, and its unit block); the pre-release stance permits the removal. `dsh-app-boot` keeps its boot/env/config/personal-overlay helpers — only the argv scanner leaves.
## Resume without an environment variable
Merging the concurrent safe-session-resume feature onto this parser retired the `RESUME_SESSION_ID` environment variable, which had been the only bridge from `--resume` into the shipped config's `resumeSessionId: !!js process.env.RESUME_SESSION_ID`. `runTui` now injects the already-parsed id through `boot`'s `prepare(ctx)` hook — `ctx.provide(RESUME_SESSION_ID_KEY, id)` (a new `dsh-app-boot` export, value `'resumeSessionId'`) — and the four tui-agent/cordis configs read it as a bare identifier: `resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`. The expression is quoted because YAML otherwise parses the `?`/`:` as a mapping; the `typeof` guard tolerates a bin that never provides the slot. The `/resume` in-place handoff (`process.execve`) rebuilds its re-exec argv directly as `dsh [config] --resume <id>` from the parsed values, so `replaceResumeArg` (which the merge brought in) is dropped alongside `parseResumeArg`.
## One terminal front door: `dsh`
The `dsh-tui-demo` package was a plugin (the TUI app bundle mounted by `dsh`'s config) plus a redundant `bin` that booted a leaf `cordis.yml` — the same job `dsh [config]` does. The bin is removed: `demo:cordis`, `demo:code-mode`, and both the tui-agent and cordis-agent keyless PTY smokes now launch through `apps/cli/src/bin.ts` with the config as the positional argument, and the package keeps only its plugin and invariant entries. The peer/dev `dsh-app-boot` dependency, the `bin`/`./bin` export, the `built-bin.e2e.ts` (its TUI piped-launch refusal is covered by `dsh`'s own TTY guard in the tui-agent PTY smoke), and the tsdown `bin` entry all leave with it. `cli-demo`, `acp-demo`, and `jsonrpc-demo` keep their bins because each is a distinct surface (headless, ACP, JSON-RPC) `dsh` does not provide.
## Package topology
The argument surface stays inside `apps/cli`, the assembly tier, not a `packages/*` library: it is this one app's routing, not a reusable seam. `dsh-app-boot` shrinks to boot glue with no CLI-parsing responsibility. `commander@^15` is added to `apps/cli/package.json`, matching the SDK bins' pin.
@@ -30,10 +38,14 @@ The argument surface stays inside `apps/cli`, the assembly tier, not a `packages
**Make the argument surface a `packages/*` seam** — rejected: nothing outside `dsh` consumes it, and capability seams are not split preemptively. The Commander adapter is `apps/cli`'s own concern.
**Keep `RESUME_SESSION_ID` as the resume bridge** — rejected: with `--resume` parsed into a value the bin already holds, threading it through an environment variable the config re-reads is indirection with no benefit, and it left the demo bin a second, env-only resume path. Providing the id on the boot context is the same channel `boot`'s `prepare` hook already uses for `tuiResumeHost`.
**Keep the `dsh-tui-demo` bin** — rejected: it duplicated `dsh [config]` exactly, and keeping it forced the demo-only `RESUME_SESSION_ID` fallback to stay alive. Its plugin is what the configs actually mount; only the front-door bin was redundant, and `dsh` is the one terminal entry point.
## Testing
`apps/cli/tests/args.spec.ts` (new; `apps/*/tests` added to the vitest include and `apps/cli/tests` to `tsconfig.host.json`) covers the adapter at the level that matters: mode routing by shape, the fail-loud checks (empty resume/prompt, bad host/port, unknown option), and `--help`/`--version` surfacing as data. The `dsh CLI keyless smoke` group in `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` exercises the real `bin.ts` dispatch end to end through a PTY (default boot, personal overlay, invalid config, `--resume` failure, source-path prompt) and stays green unchanged. `packages/ui/app-boot/tests/app-boot.spec.ts` drops its `parseResumeArg` block.
`apps/cli/tests/args.spec.ts` (new; `apps/*/tests` added to the vitest include and `apps/cli/tests` to `tsconfig.host.json`) covers the adapter at the level that matters: mode routing by shape (including `web --dev`), the fail-loud checks (empty resume/prompt, bad host/port, unknown option), and `--help`/`--version` surfacing as data. Both PTY smoke groups in `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` now drive the real `apps/cli/src/bin.ts`: the `tui-agent` group boots the config as a positional, and the `dsh CLI` group covers default boot, personal overlay, invalid config, the `--resume` config intake, the `process.execve` in-place resume handoff, and the source-path prompt. `examples/cordis-agent/tests/keyless-smoke.e2e.ts` likewise launches through `dsh`. `packages/ui/app-boot/tests/app-boot.spec.ts` drops its `parseResumeArg`/`replaceResumeArg` blocks; the TUI unit and snapshot fixtures use the `dsh --resume {session}` resume command.
## Consequences
`dsh` gains rendered `--help`/`--version` and consistent fail-loud parse errors, and mode routing no longer depends on flag position. Argv parsing lives in one place with one parser idiom shared with the SDK bins, at the cost of a `commander` dependency on `apps/cli` and Commander's parse semantics (its error strings, its `exitOverride` contract) now sitting on the CLI's front door. `dsh-app-boot` no longer owns any CLI-parsing surface; a future consumer needing `--resume`-style parsing composes Commander rather than reviving the deleted scanner.
`dsh` gains rendered `--help`/`--version` and consistent fail-loud parse errors, and mode routing no longer depends on flag position. Argv parsing lives in one place with one parser idiom shared with the SDK bins, at the cost of a `commander` dependency on `apps/cli` and Commander's parse semantics (its error strings, its `exitOverride` contract) now sitting on the CLI's front door. `dsh-app-boot` no longer owns any CLI-parsing surface; a future consumer needing `--resume`-style parsing composes Commander rather than reviving the deleted scanner. Resuming a session needs no environment variable, and `dsh` is the single terminal front door — the `dsh-tui-demo` package is now a plugin bundle with no bin. Anyone who ran `dsh-tui-demo <config>` or `RESUME_SESSION_ID=<id> dsh-tui-demo` uses `dsh <config>` / `dsh --resume <id>` instead.

View File

@@ -10,12 +10,20 @@ Status: implemented
## 决策
argv 只在 `apps/cli/src/args.ts` 中解析一次,通过一个 Commander 适配器(即 SDK bin`create-sdk``dsh-scripts`,已经统一采用的那个解析器)。`parseDshArgs(argv, version)` 将调用解析为一个判别式 `DshInvocation` 联合类型:`{ mode: 'tui', config?, resume? }``{ mode: 'headless', prompt }``{ mode: 'web', host, port }``{ mode: 'help' | 'version', text }``{ mode: 'error', message }`。Commander 在 `exitOverride()` 下运行并捕获输出,因此它自身从不写出或退出:`--help``--version` 和每个解析错误都以数据形式返回。
argv 只在 `apps/cli/src/args.ts` 中解析一次,通过一个 Commander 适配器(即 SDK bin`create-sdk``dsh-scripts`,已经统一采用的那个解析器)。`parseDshArgs(argv, version)` 将调用解析为一个判别式 `DshInvocation` 联合类型:`{ mode: 'tui', config?, resume? }``{ mode: 'headless', prompt }``{ mode: 'web', host, port, dev }``{ mode: 'help' | 'version', text }``{ mode: 'error', message }`。Commander 在 `exitOverride()` 下运行并捕获输出,因此它自身从不写出或退出:`--help``--version` 和每个解析错误都以数据形式返回。
`bin.ts` 调用一次适配器,并对 `mode` 做分支切换(封闭联合类型,默认分支为 `satisfies never`),只动态导入所选模式对应的模块。每个模式模块现在只消费已解析好的值:`runTui(config, resume)``runHeadless(task)``runWeb(host, port)`,都不会再次读取 argv。`web` 是一个**保留的首个 token**`parseDshArgs` 将开头的 `web` 分发给它自己的 Commander 解析器,其余一切分发给默认的 TUI/headless 解析器,因此根级标志与 `web` 标志从不共用同一套语法——`dsh web -p x` 会显式报错(`web` 没有 `-p`),而 `dsh -p x web` 只是一个 headless prompt其第二个位置参数被丢弃无需防范任何跨命令泄漏。每个解析器都在 `parse()` 之后读取 Commander 的 `opts()`/`processedArgs`,而不是通过 action 闭包。`--host` 是一个 `.choices([LOOPBACK_HOST, ALL_INTERFACES_HOST])``--port` 是一个对 065535 做范围检查的 `argParser`,二者都从内联的 `runWeb` 检查移入了解析器。两处解析后的检查保留了「绝不静默重新开始」不变式:空的 `--resume=` id 和空的 `-p` 任务各自变为 `mode: 'error'`,因为 agent-loop 把空的 resume id 视为不恢复,而空的 prompt 没有任何内容可运行。重复出现的 `--resume` 采用 Commander 天然的后者胜出(旧的定制扫描器会拒绝它;后者胜出是标准的 CLI 行为,无需特殊处理)。`--version` 读取本应用的 `package.json`
`bin.ts` 调用一次适配器,并对 `mode` 做分支切换(封闭联合类型,默认分支为 `satisfies never`),只动态导入所选模式对应的模块。每个模式模块现在只消费已解析好的值:`runTui(config, resume)``runHeadless(task)``runWeb(host, port, dev)`,都不会再次读取 argv。`web` 是一个**保留的首个 token**`parseDshArgs` 将开头的 `web` 分发给它自己的 Commander 解析器,其余一切分发给默认的 TUI/headless 解析器,因此根级标志与 `web` 标志从不共用同一套语法——`dsh web -p x` 会显式报错(`web` 没有 `-p`),而 `dsh -p x web` 只是一个 headless prompt其第二个位置参数被丢弃无需防范任何跨命令泄漏。每个解析器都在 `parse()` 之后读取 Commander 的 `opts()`/`processedArgs`,而不是通过 action 闭包。`--host` 是一个 `.choices([LOOPBACK_HOST, ALL_INTERFACES_HOST])``--port` 是一个对 065535 做范围检查的 `argParser`,二者都从内联的 `runWeb` 检查移入了解析器`--dev` 会挂载客户端 HMR热模块替换驱动并启用构建产物监视。两处解析后的检查保留了「绝不静默重新开始」不变式:空的 `--resume=` id 和空的 `-p` 任务各自变为 `mode: 'error'`,因为 agent-loop 把空的 resume id 视为不恢复,而空的 prompt 没有任何内容可运行。重复出现的 `--resume` 采用 Commander 天然的后者胜出(旧的定制扫描器会拒绝它;后者胜出是标准的 CLI 行为,无需特殊处理)。`--version` 读取本应用的 `package.json`
`parseResumeArg``dsh-app-boot` 中删除包括其导出、README 中的对应行以及单元测试块);预发布阶段的立场允许这次删除。`dsh-app-boot` 保留其 boot/env/config/个人覆盖辅助函数,只有 argv 扫描器被移除。
## 无需环境变量即可恢复
将与本解析器并行开发的安全会话恢复功能合入时,系统移除了 `RESUME_SESSION_ID` 环境变量。此前,它是将 `--resume` 的值传给随产品提供的配置字段 `resumeSessionId: !!js process.env.RESUME_SESSION_ID` 的唯一通道。`runTui` 现在通过 `boot``prepare(ctx)` 钩子注入已解析的 id`ctx.provide(RESUME_SESSION_ID_KEY, id)``dsh-app-boot` 的新导出,值为 `'resumeSessionId'`tui-agent 和 cordis-agent 的四份配置将该值作为裸标识符读取:`resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`。这个表达式需要加引号,否则 YAML 会把 `?``:` 解析为映射;`typeof` 守卫使从未提供该槽位的 bin 也能正常运行。`/resume` 原地交接(`process.execve`)直接根据解析后的值将重新执行的 argv 构造成 `dsh [config] --resume <id>`,因此合并时引入的 `replaceResumeArg``parseResumeArg` 一并删除。
## 唯一的终端入口:`dsh`
`dsh-tui-demo`package原本包含一个插件`dsh` 配置挂载的 TUI 应用组合)和一个冗余的 `bin`;后者启动一份叶子配置 `cordis.yml`,所做的工作与 `dsh [config]` 相同。该 bin 已移除:`demo:cordis``demo:code-mode` 以及 tui-agent 和 cordis-agent 的两个无密钥 PTY 冒烟测试现在都通过 `apps/cli/src/bin.ts` 启动,并将配置作为位置参数;该包只保留插件入口和不变式入口。与该 bin 一同移除的还有对 `dsh-app-boot` 的对等依赖peer dependency和开发依赖、`bin``./bin` 导出、`built-bin.e2e.ts`(其中拒绝通过管道启动 TUI 的行为已由 tui-agent PTY 冒烟测试中 `dsh` 自身的 TTY 守卫覆盖),以及 tsdown 的 `bin` 入口。`cli-demo``acp-demo``jsonrpc-demo` 保留各自的 bin因为它们分别提供 `dsh` 所没有的独立接口headless、ACPAgent Client Protocol、JSON-RPC
## 包拓扑
参数解析留在 `apps/cli`(组装层)内,而不是 `packages/*` 库中:它是这一个应用自身的路由,而非可复用的 seam。`dsh-app-boot` 收缩为纯粹的 boot 胶水代码,不再承担 CLI 解析职责。`commander@^15` 被加入 `apps/cli/package.json`,与 SDK bin 锁定的版本一致。
@@ -30,10 +38,14 @@ argv 只在 `apps/cli/src/args.ts` 中解析一次,通过一个 Commander 适
**把参数解析做成 `packages/*` 的 seam。** 已否决:`dsh` 之外没有任何消费方使用它,而能力 seam 不应被提前拆分。这个 Commander 适配器是 `apps/cli` 自身的事务。
**保留 `RESUME_SESSION_ID` 作为恢复通道**:不予采纳。`--resume` 已被解析成 bin 当前持有的值;若再通过环境变量传递并由配置重新读取,只会引入无益的间接层,还会使演示 bin 保留第二条仅依赖环境变量的恢复路径。在启动上下文中提供 id`boot``prepare` 钩子为 `tuiResumeHost` 提供值所采用的是同一通道。
**保留 `dsh-tui-demo` bin**:不予采纳。它与 `dsh [config]` 的功能完全重复;保留它还会迫使演示专用的 `RESUME_SESSION_ID` 回退路径继续存在。配置实际挂载的是该包的插件;冗余的只有作为终端入口的 bin`dsh` 是唯一的终端入口。
## 测试
`apps/cli/tests/args.spec.ts`(新增;`apps/*/tests` 加入 vitest include`apps/cli/tests` 加入 `tsconfig.host.json`在关键层面覆盖适配器:按形态进行的模式路由、显式报错检查(空 resume/prompt、错误的 host/port、未知选项),以及 `--help`/`--version` 以数据形式呈现`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 中的 `dsh CLI keyless smoke` 组通过 PTY 端到端地运行真实的 `bin.ts` 分发(默认启动、个人覆盖、无效配置、`--resume` 失败、源路径 prompt且保持绿色不变。`packages/ui/app-boot/tests/app-boot.spec.ts` 移除其 `parseResumeArg` 测试块
`apps/cli/tests/args.spec.ts`(新增;`apps/*/tests` 加入 vitest include`apps/cli/tests` 加入 `tsconfig.host.json`覆盖适配器的关键行为:根据参数形态选择模式(包括 `web --dev`)、显式报错场景(空的恢复会话 id、空提示词、非法主机、非法端口和未知选项),以及 `--help``--version` 作为数据返回`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 中的两组 PTY 冒烟测试现在都驱动真实的 `apps/cli/src/bin.ts``tui-agent` 组将配置作为位置参数启动,`dsh CLI` 组覆盖默认启动、个人覆盖、无效配置、配置对 `--resume` 的接收、通过 `process.execve` 原地恢复交接,以及包含源码路径的系统提示词。`examples/cordis-agent/tests/keyless-smoke.e2e.ts` 同样通过 `dsh` 启动。`packages/ui/app-boot/tests/app-boot.spec.ts` 移除其 `parseResumeArg``replaceResumeArg` 测试块TUI 单元测试和快照 fixture测试前置数据使用 `dsh --resume {session}` 恢复命令
## 影响
`dsh` 获得了渲染出的 `--help`/`--version` 以及一致的显式报错式解析错误模式路由也不再依赖标志位置。argv 解析集中在一处,并与 SDK bin 共用一套解析器方式,代价是 `apps/cli` 新增一项 `commander` 依赖,且 Commander 的解析语义(它的错误字符串、它的 `exitOverride` 契约)如今落在 CLI 的入口处。`dsh-app-boot` 不再拥有任何 CLI 解析职责;未来需要 `--resume` 式解析的消费方应组合 Commander而不是复活已删除的扫描器。
`dsh` 获得了渲染出的 `--help`/`--version` 以及一致的显式报错式解析错误模式路由也不再依赖标志位置。argv 解析集中在一处,并与 SDK bin 共用一套解析器方式,代价是 `apps/cli` 新增一项 `commander` 依赖,且 Commander 的解析语义(它的错误字符串、它的 `exitOverride` 契约)如今落在 CLI 的入口处。`dsh-app-boot` 不再拥有任何 CLI 解析职责;未来需要 `--resume` 式解析的消费方应组合 Commander而不是复活已删除的扫描器。恢复会话不再需要环境变量,且 `dsh` 是唯一的终端入口;`dsh-tui-demo` 包现在是一个不带 bin 的插件组合包。原先运行 `dsh-tui-demo <config>``RESUME_SESSION_ID=<id> dsh-tui-demo` 的用户,改用 `dsh <config>``dsh --resume <id>`

View File

@@ -2,5 +2,5 @@
# 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
2026-07-20-retire-readline-front-door.md: 7ebcfdc246bdf6971418609c61acbd4019aa90cb
2026-07-20-retire-readline-front-door.zh.md: cf4d03594ed3a0cf31bed96eb2133bd37959084a
2026-07-20-retire-readline-front-door.md: d8e6a5c172b576ce6bc76911186c9f81a4ece88f
2026-07-20-retire-readline-front-door.zh.md: bea685cdc3f8f1530d56ae3eb5dcc34cff0b46af

View File

@@ -26,7 +26,7 @@ Pipes remain the default test medium. PTY-driven subprocess tests are sanctioned
- `examples/echo-agent/tests/echo.e2e.ts` proves the Loader boot + mock-model tool round-trip through `stream-json` records instead of readline transcript lines.
- The CI demo-smoke gate (`scripts/run-gates.ts`, AGENTS.md) runs `demo:echo --output-format stream-json -p "echo ci smoke"` and parses the records structurally.
- `packages/examples/tui-demo/tests/built-bin.e2e.ts` proves the built bin's piped-launch refusal (nonzero exit + pointer at `dsh-cli-demo`); the echo-round-trip-under-plain-Node and missing-config fail-loud proofs live in `cli-demo`'s built-bin suite.
- The TUI's piped-launch refusal (nonzero exit + pointer at the one-shot CLI) is covered by the `dsh` TTY guard exercised in `examples/tui-agent`'s PTY smoke; the echo-round-trip-under-plain-Node and missing-config fail-loud proofs live in `cli-demo`'s built-bin suite.
- `packages/context/time-context/tests/time-context.e2e.ts` runs one one-shot turn; multi-turn elapsed rendering stays unit-covered in its spec.
## Accepted losses

View File

@@ -26,7 +26,7 @@ Status: implemented
- `examples/echo-agent/tests/echo.e2e.ts` 通过 `stream-json` 记录证明 Loader 启动 + mock 模型的工具往返,而不是匹配 readline 文本记录行。
- CI 演示冒烟门禁(`scripts/run-gates.ts`、AGENTS.md运行 `demo:echo --output-format stream-json -p "echo ci smoke"` 并结构化解析记录。
- `packages/examples/tui-demo/tests/built-bin.e2e.ts` 证明构建产物 bin 对管道启动的拒绝(非零退出 + 指向 `dsh-cli-demo` 的提示);纯 Node 下的 echo 往返证明与缺失配置的快速失败证明位于 `cli-demo` 的 built-bin 套件。
- TUI 对管道启动的拒绝(非零退出 + 指向单次任务 CLI 的提示)由 `examples/tui-agent` 的 PTY 冒烟测试所行使的 `dsh` TTY 守卫覆盖;纯 Node 下的 echo 往返证明与缺失配置的快速失败证明位于 `cli-demo` 的 built-bin 套件。
- `packages/context/time-context/tests/time-context.e2e.ts` 运行一个单次任务轮次;多轮 elapsed 渲染仍由其单元测试覆盖。
## 接受的损失