From 007e8fd92f0b73734c68f4ae6f00c9edfa4089b3 Mon Sep 17 00:00:00 2001 From: Turtle Date: Sat, 25 Jul 2026 14:15:25 +0800 Subject: [PATCH] refactor(cli): bail early in the arg adapter instead of returning errors as data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review and cut ceremony: the adapter no longer models help/version/ errors as DshInvocation members. Commander owns those under exitOverride — it prints usage or the diagnostic and one try/catch in parseDshArgs turns the thrown CommanderError into process.exit with the intended code. bin.ts drops its help/version/error cases; the union is the three real modes. Domain checks bail via command.error(print + exit 1): --prompt rejects an empty task or a stray config/--resume, empty --resume= fails loud, and --host/--port are validated. A repeated --resume or a flag captured as a value is Commander's standard behavior, left alone (a bad id fails loud downstream). dsh --help discloses web via addHelpText. Net: args.ts 185 -> 112 lines. Also fixes review nits: built-bin e2e resolves on `close`; the /resume handoff uses `dsh --resume= -- ` so a config named `web` stays a positional; and stale prose (cordis.yml comment, app-boot module doc + duplicate JSDoc, ui/README, two feature notes, an agent-loop test name) tracks the shipped state. Removes tui-demo's now-dead plugin-include dep and vendor/loader + app-boot tsconfig references. --- ...4-dsh-commander-argument-adapter.i18n.yaml | 4 +- ...26-07-24-dsh-commander-argument-adapter.md | 8 +- ...07-24-dsh-commander-argument-adapter.zh.md | 8 +- ...21-dsh-system-prompt-source-path.i18n.yaml | 4 +- ...026-07-21-dsh-system-prompt-source-path.md | 2 +- ...-07-21-dsh-system-prompt-source-path.zh.md | 2 +- .../2026-07-21-tui-no-banner.i18n.yaml | 4 +- .../feature/2026-07-21-tui-no-banner.md | 2 +- .../feature/2026-07-21-tui-no-banner.zh.md | 2 +- apps/cli/src/args.ts | 147 ++++++------------ apps/cli/src/bin.ts | 11 +- apps/cli/src/headless.ts | 1 - apps/cli/src/tui.ts | 6 +- apps/cli/tests/args.spec.ts | 53 +++++-- apps/cli/tests/built-bin.e2e.ts | 3 +- examples/tui-agent/cordis.yml | 4 +- .../tests/config-session-id.spec.ts | 2 +- packages/examples/tui-demo/package.json | 2 - packages/examples/tui-demo/tsconfig.json | 6 - packages/ui/README.md | 2 +- packages/ui/app-boot/src/index.ts | 3 +- pnpm-lock.yaml | 5 +- 22 files changed, 116 insertions(+), 165 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml index 3a85dedb0d..3a70d276fe 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml @@ -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: c038f4facdc62039b8a31be5d660648fd81aebd2 -2026-07-24-dsh-commander-argument-adapter.zh.md: 285917af7c4b0da769ae7595bb1a6e5966adafd9 +2026-07-24-dsh-commander-argument-adapter.md: 0f6b18848eacc5d5771e500bf226cc6f680f8d3f +2026-07-24-dsh-commander-argument-adapter.zh.md: ae523d0e37d09ce1f85b317bb0850194045474cb diff --git a/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.md b/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.md index c038f4facd..0f6b18848e 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.md +++ b/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.md @@ -10,15 +10,15 @@ 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, 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. +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)` returns a discriminated `DshInvocation` union of the three real modes: `{ mode: 'tui', config?, resume? }`, `{ mode: 'headless', prompt }`, or `{ mode: 'web', host, port, dev }`. It does **not** model help/version/errors as data: Commander owns those, printing usage or the diagnostic and exiting at the point of failure. `exitOverride()` turns each into a thrown `CommanderError` carrying the intended code (0 for help/version, 1 for a parse or domain error), which one `try/catch` in `parseDshArgs` turns into `process.exit`. -`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 0–65535, 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`. +`bin.ts` calls the adapter once and switches on `mode` (closed union, `satisfies never` default), dynamic-importing only the chosen mode's module; only a valid, non-help invocation reaches the switch, so it has no help/version/error cases. Each mode module 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`). Each parser reads Commander's `opts()`/`processedArgs` after `parse()`, then bails via `command.error(...)` (print + exit 1) on the domain checks Commander cannot express: `--prompt` selects headless and rejects an empty task or a stray config/`--resume` rather than silently dropping a TUI input; an empty `--resume=` id fails loud (agent-loop treats `''` as no-resume); `--host` must be loopback/all-interfaces and `--port` an integer in 0–65535, moving both from the inline `runWeb` checks into the parser. `--dev` mounts the client HMR driver and bundle watch. A repeated `--resume`, or a following flag captured as a `--resume`/`--prompt` value, is Commander's standard behavior (last-wins / next-token) and is left alone; a bad id fails loud downstream when the session cannot load. `dsh --help` discloses the `web` mode through an `addHelpText` line (a real `web` subcommand would hijack the `[config]` positional). `--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 ` from the parsed values, so `replaceResumeArg` (which the merge brought in) is dropped alongside `parseResumeArg`. +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 launcher that never provides the slot. The `/resume` in-place handoff (`process.execve`) rebuilds its re-exec argv directly from the parsed values as `dsh --resume= [-- ]` — the `--` keeps a config named `web` or starting with `-` a positional — so `replaceResumeArg` (which the merge brought in) is dropped alongside `parseResumeArg`. ## One terminal front door: `dsh` @@ -44,7 +44,7 @@ The argument surface stays inside `apps/cli`, the assembly tier, not a `packages ## 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 (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. +`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`), and the exit-code behavior for the fail-loud checks (empty resume/prompt, bad host/port, `--prompt` mixed with a config, unknown option) and `--help`/`--version`, captured through a `process.exit` spy. 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 diff --git a/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md b/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md index 285917af7c..ae523d0e37 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md @@ -10,15 +10,15 @@ 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, dev }`、`{ 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 }`。它**不会**将帮助、版本信息或错误建模为数据:这些情况由 Commander 处理,在触发处打印用法或诊断信息并退出。`exitOverride()` 会将每种情况转为抛出的 `CommanderError`,并携带预期退出码(帮助或版本为 0,解析错误或领域错误为 1);唯一一处 `try/catch` 位于 `parseDshArgs` 中,捕获错误后调用 `process.exit`。 -`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` 是一个对 0–65535 做范围检查的 `argParser`,二者都从内联的 `runWeb` 检查移入了解析器;`--dev` 会挂载客户端 HMR(热模块替换)驱动,并启用构建产物监视。两处解析后的检查保留了「绝不静默重新开始」不变式:空的 `--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`)。每个解析器都读取 Commander 的 `opts()`/`processedArgs`(在 `parse()` 之后),随后对 Commander 无法表达的领域校验调用 `command.error(...)` 立即终止(打印信息并以退出码 1 退出):`--prompt` 选择 headless 模式,并在任务为空或存在多余的配置位置参数或 `--resume` 时拒绝调用,而不会静默丢弃 TUI 输入;空的 `--resume=` id 会显式失败(agent-loop 把 `''` 视为不恢复);`--host` 必须是回环地址或全接口地址,`--port` 必须是 0–65535 范围内的整数,这两项校验都从 `runWeb` 的内联检查移入解析器。`--dev` 会挂载客户端 HMR(热模块替换)驱动,并启用构建产物监视。重复提供 `--resume`,或后续标志被捕获为 `--resume` 或 `--prompt` 的值,都是 Commander 的标准行为(最后一次取值生效/将下一 token 作为值),本适配器不作干预;无效 id 会在下游无法加载会话时显式失败。`dsh --help` 会展示 `web` 模式,具体通过一行 `addHelpText` 文本实现(真正的 `web` 子命令会劫持 `[config]` 位置参数)。`--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 `,因此合并时引入的 `replaceResumeArg` 与 `parseResumeArg` 一并删除。 +将与本解析器并行开发的安全会话恢复功能合入时,系统移除了 `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` 守卫使从未提供该槽位的启动器也能正常运行。`/resume` 原地交接(`process.execve`)直接根据解析后的值将重新执行的 argv 构造成 `dsh --resume= [-- ]`;其中 `--` 可确保名称为 `web` 或以 `-` 开头的配置仍被视为位置参数。因此,合并时引入的 `replaceResumeArg` 与 `parseResumeArg` 一并删除。 ## 唯一的终端入口:`dsh` @@ -44,7 +44,7 @@ argv 只在 `apps/cli/src/args.ts` 中解析一次,通过一个 Commander 适 ## 测试 -`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}` 恢复命令。 +`apps/cli/tests/args.spec.ts`(新增;`apps/*/tests` 加入 vitest include,`apps/cli/tests` 加入 `tsconfig.host.json`)覆盖适配器的关键行为:根据参数形态进行模式路由(包括 `web --dev`),并验证以下情况各自的退出码行为:显式报错检查(恢复 id 或提示词为空、host 或 port 无效、`--prompt` 与配置混用、未知选项)以及 `--help` 和 `--version`;这些退出码通过 `process.exit` spy 捕获。`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}` 恢复命令。 ## 影响 diff --git a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml b/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml index f1b9829b73..2c0b4d3404 100644 --- a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml @@ -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-21-dsh-system-prompt-source-path.md: b54d01488fd7c0b49e06200c93af2b056c9fd00b -2026-07-21-dsh-system-prompt-source-path.zh.md: 208e3dce072f63c280999e15276dce62ff4e5c43 +2026-07-21-dsh-system-prompt-source-path.md: 4cb89e8124840bba6633235d195e95957245137c +2026-07-21-dsh-system-prompt-source-path.zh.md: 90c23bed4a3f95155e323c63a68fe2da09543ea6 diff --git a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.md b/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.md index b54d01488f..4cb89e8124 100644 --- a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.md +++ b/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.md @@ -16,7 +16,7 @@ The testable logic lives in `dsh-app-boot`, not in `apps/cli`, because `apps/*` ## Scope -Only the `dsh` CLI adds this. The demo bins (`dsh-tui-demo`, `dsh-acp-demo`) boot their committed trees verbatim and gain no source section: they are not the self-modification surface, and their checkout root is not a fact the model needs. +Only the `dsh` CLI adds this. The demo bins (`dsh-cli-demo`, `dsh-acp-demo`) boot their committed trees verbatim and gain no source section: they are not the self-modification surface, and their checkout root is not a fact the model needs. ## HMR diff --git a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.zh.md b/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.zh.md index 208e3dce07..90c23bed4a 100644 --- a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.zh.md +++ b/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.zh.md @@ -16,7 +16,7 @@ Status: implemented ## Scope -只有 `dsh` CLI 会加入这一段。demo bin(`dsh-tui-demo`、`dsh-acp-demo`)原样引导它们已提交的插件树,不会获得 source 段:它们不是自我修改的接口,其检出根目录也不是模型需要知道的事实。 +只有 `dsh` CLI 会加入这一段。demo bin(`dsh-cli-demo`、`dsh-acp-demo`)原样引导它们已提交的插件树,不会获得 source 段:它们不是自我修改的接口,其检出根目录也不是模型需要知道的事实。 ## HMR diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.i18n.yaml b/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.i18n.yaml index 56333563f5..e5a2eb9f94 100644 --- a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.i18n.yaml @@ -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-21-tui-no-banner.md: f5f4b1b847740e741ec3e33a6116e7497e955bd1 -2026-07-21-tui-no-banner.zh.md: 956fe03e2c0b09ea7378ffd53ffbe8d712d1e152 +2026-07-21-tui-no-banner.md: a6e0956f289cfc810da766fd0cae94b97baf5280 +2026-07-21-tui-no-banner.zh.md: acc5614727cf67881832af1685be557d675696e7 diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.md b/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.md index f5f4b1b847..a6e0956f28 100644 --- a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.md +++ b/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.md @@ -13,7 +13,7 @@ The TUI opened with a boxed product banner ("DEEPSEEK HARNESS" + model/session d ## Decision - `HeaderComponent`, the sweep animation, and its lifecycle wiring are deleted. The TUI mounts straight into the transcript; startup renders nothing above the separator. -- The model name moves into the footer status line's left segment (` ↑tokens ↓tokens`), so the session's driving model stays visible at all times, not just at boot. The session id is no longer displayed — it lives in the session log and `./.sessions` filenames, and `RESUME_SESSION_ID` consumers retrieve it there. +- The model name moves into the footer status line's left segment (` ↑tokens ↓tokens`), so the session's driving model stays visible at all times, not just at boot. The session id is no longer displayed — it lives in the session log and `./.sessions` filenames, where `dsh --resume ` and the `/resume` selector retrieve it. - `welcome`, when configured, renders as the transcript's first line (a muted notice) inside `rebuildTranscript`, so palette swaps preserve it. Unset renders nothing. Fixtures keep their configured welcomes; the PTY smoke's boot marker becomes the footer's model name, the only mounted-TUI text guaranteed to render regardless of cwd length. This supersedes the [banner sweep Agent Note](2026-07-21-tui-banner-sweep.md) entirely: both the sweep and the banner it animated are gone. diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.zh.md b/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.zh.md index 956fe03e2c..acc5614727 100644 --- a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.zh.md +++ b/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.zh.md @@ -13,7 +13,7 @@ TUI 启动时展示一个带框的产品横幅("DEEPSEEK HARNESS" + 模型/会 ## Decision - 删除 `HeaderComponent`、扫入动画及其生命周期接线。TUI 直接挂载进 transcript;启动时分隔线之上不渲染任何东西。 -- 模型名移入页脚状态行的左段(` ↑tokens ↓tokens`),会话使用的模型因此始终可见,而不只是启动时。会话 id 不再显示——它存在于会话日志和 `./.sessions` 文件名中,`RESUME_SESSION_ID` 的使用者从那里获取。 +- 模型名移入页脚状态行的左段(` ↑tokens ↓tokens`),会话使用的模型因此始终可见,而不只是启动时。会话 id 不再显示——它存在于会话日志和 `./.sessions` 文件名中,`dsh --resume ` 和 `/resume` 选择器会从中获取该 id。 - 配置了 `welcome` 时,它作为 transcript 的第一行(一条弱化的通知)在 `rebuildTranscript` 内渲染,因此调色板切换会保留它。未设置则什么也不渲染。fixture 保留各自配置的欢迎语;PTY 冒烟测试的启动标记改为页脚的模型名——无论 cwd 多长都保证渲染的唯一挂载后文本。 本 note 完全取代[横幅扫入 Agent Note](2026-07-21-tui-banner-sweep.md):扫入动画和它所动画的横幅都已移除。 diff --git a/apps/cli/src/args.ts b/apps/cli/src/args.ts index 37e87a1db7..8fc040168f 100644 --- a/apps/cli/src/args.ts +++ b/apps/cli/src/args.ts @@ -1,16 +1,14 @@ /** * Commander adapter for the `dsh` command-line entry: the one place argv is * parsed and routed to a mode. `bin.ts` switches on the returned discriminant - * and dynamic-imports that mode's module; each mode module then consumes the - * already-parsed values instead of re-reading argv. Output is suppressed and - * `exitOverride` is set so Commander never writes or exits on its own — every - * outcome (including `--help`/`--version` and parse errors) is returned to the - * caller as data. The `web` subcommand is a reserved first token dispatched to - * its own parser, so root flags and `web` flags never share a grammar. + * and dynamic-imports that mode's module. Commander owns `--help`/`--version` + * and parse errors: it prints and exits at the point of failure (a domain + * failure routes through `command.error`), so this returns only a resolved mode. + * The `web` subcommand is a reserved first token dispatched to its own parser. * @module @deepseek-ai/dsh/args */ -import { Command, CommanderError, InvalidArgumentError, Option } from 'commander' +import { Command, CommanderError } from 'commander' /** The loopback host `dsh web` binds by default. */ export const LOOPBACK_HOST = '127.0.0.1' @@ -31,10 +29,7 @@ interface HeadlessInvocation { prompt: string } -/** - * Browser UI: `dsh web`. Host constrained to {@link LOOPBACK_HOST}/{@link ALL_INTERFACES_HOST}; - * port already coerced and range-checked; `dev` mounts the client HMR driver and bundle watch. - */ +/** Browser UI: `dsh web`. Host is loopback/all-interfaces, port a 0–65535 integer, `dev` mounts the HMR driver. */ interface WebInvocation { mode: 'web' host: string @@ -42,120 +37,76 @@ interface WebInvocation { dev: boolean } -/** `--help` or `--version` requested: `bin.ts` prints `text` to stdout and exits 0. */ -interface InfoInvocation { - mode: 'help' | 'version' - text: string -} +/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */ +export type DshInvocation = TuiInvocation | HeadlessInvocation | WebInvocation -/** A parse error (unknown option, missing/invalid argument): `bin.ts` prints `message` to stderr and exits 1. */ -interface ErrorInvocation { - mode: 'error' - message: string -} - -/** The resolved `dsh` invocation: exactly one mode, all values parsed and validated. */ -export type DshInvocation = - | TuiInvocation - | HeadlessInvocation - | WebInvocation - | InfoInvocation - | ErrorInvocation - -/** Coerce `--port` to an integer in 0–65535; a bad value fails loud as a parse error. */ -function parsePort(raw: string): number { - const port = Number(raw) - if (!Number.isInteger(port) || port < 0 || port > 65535) { - throw new InvalidArgumentError(`invalid --port ${raw}`) - } - return port -} - -/** - * A configured `Command` under `exitOverride` with output captured into `sink`, - * so `--help`, `--version`, and parse errors surface as thrown `CommanderError`s - * (see {@link settle}) rather than writing to a stream or exiting. - */ -function program(name: string, version: string, sink: string[]): Command { - return new Command() - .name(name) - .version(version, '-V, --version', 'output the version number') - .exitOverride() - .configureOutput({ - writeOut: chunk => void sink.push(chunk), - writeErr: chunk => void sink.push(chunk), - }) -} - -/** - * Run `command.parse` and map its thrown `CommanderError` to an info/error - * invocation, or `undefined` when the parse succeeded (the caller then reads the - * parsed options). - */ -function settle(command: Command, argv: readonly string[], sink: string[]): InfoInvocation | ErrorInvocation | undefined { - try { - command.parse(argv, { from: 'user' }) - return undefined - } catch (error) { - /* v8 ignore next -- Commander only throws CommanderError from parse under exitOverride */ - if (!(error instanceof CommanderError)) throw error - if (error.code === 'commander.helpDisplayed') return { mode: 'help', text: sink.join('') } - if (error.code === 'commander.version') return { mode: 'version', text: sink.join('') } - return { mode: 'error', message: error.message } - } +/** A `Command` under `exitOverride`, so {@link parseDshArgs} owns the exit, named for its usage line. */ +function program(name: string, version: string): Command { + return new Command().name(name).version(version, '-V, --version', 'output the version number').exitOverride() } /** Parse `dsh web` arguments (everything after the `web` token). */ -function parseWeb(argv: readonly string[], version: string): DshInvocation { - const sink: string[] = [] - const web = program('dsh web', version, sink) +function parseWeb(argv: readonly string[], version: string): WebInvocation { + const web = program('dsh web', version) .description('serve the browser UI') - .addOption(new Option('--host ', 'bind host').choices([LOOPBACK_HOST, ALL_INTERFACES_HOST]).default(LOOPBACK_HOST)) - .addOption(new Option('--port ', 'listen port').default(DEFAULT_WEB_PORT).argParser(parsePort)) + .option('--host ', `bind host (${LOOPBACK_HOST} or ${ALL_INTERFACES_HOST})`, LOOPBACK_HOST) + .option('--port ', 'listen port', String(DEFAULT_WEB_PORT)) .option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds') - const settled = settle(web, argv, sink) - if (settled !== undefined) return settled - const { host, port, dev } = web.opts<{ host: string; port: number; dev?: boolean }>() - return { mode: 'web', host, port, dev: dev ?? false } + web.parse(argv, { from: 'user' }) + const { host, port, dev } = web.opts<{ host: string; port: string; dev?: boolean }>() + if (host !== LOOPBACK_HOST && host !== ALL_INTERFACES_HOST) { + web.error(`error: --host must be ${LOOPBACK_HOST} or ${ALL_INTERFACES_HOST}`) + } + const portNumber = Number(port) + if (!/^\d+$/.test(port) || !Number.isInteger(portNumber) || portNumber > 65535) { + web.error('error: --port must be an integer in 0-65535') + } + return { mode: 'web', host, port: portNumber, dev: dev === true } } /** Parse the default (TUI / headless) arguments: `[config]`, `-p/--prompt`, `--resume`. */ function parseRoot(argv: readonly string[], version: string): DshInvocation { - const sink: string[] = [] - const root = program('dsh', version, sink) + const root = program('dsh', version) .description('dsh: interactive TUI, headless task, and browser UI') .argument('[config]', 'config to boot instead of the shipped default (TUI mode)') .option('-p, --prompt ', 'run one headless turn for this task, print the result, and exit') .option('--resume ', 'resume the persisted session with this id (TUI mode)') - const settled = settle(root, argv, sink) - if (settled !== undefined) return settled + // Disclose the web mode in `dsh --help`; a real `web` subcommand would + // hijack the `[config]` positional. `parseDshArgs` intercepts `web` first. + .addHelpText('after', '\nCommands:\n web serve the browser UI (run `dsh web --help`)') + root.parse(argv, { from: 'user' }) const { prompt, resume } = root.opts<{ prompt?: string; resume?: string }>() const config = root.processedArgs[0] as string | undefined if (prompt !== undefined) { - // A headless prompt owns the invocation; an empty task has nothing to run. - if (prompt === '') return { mode: 'error', message: "error: option '-p, --prompt ' must not be empty" } + // A headless prompt owns the invocation; an empty task has nothing to run, + // and a config or --resume alongside it is a TUI input that must not + // silently vanish from the run. + if (prompt === '') root.error('error: --prompt needs a task') + if (config !== undefined || resume !== undefined) root.error('error: --prompt takes no config or --resume') return { mode: 'headless', prompt } } // An empty `--resume=` id would silently start a fresh session downstream // (agent-loop treats '' as no-resume), so a mistyped resume must fail loud. - if (resume === '') return { mode: 'error', message: "error: option '--resume ' must not be empty" } - return { - mode: 'tui', - ...config !== undefined ? { config } : {}, - ...resume !== undefined ? { resume } : {}, - } + if (resume === '') root.error('error: --resume needs a session id') + return { mode: 'tui', ...config !== undefined && { config }, ...resume !== undefined && { resume } } } /** - * Resolve the raw argv into a single {@link DshInvocation}. Never writes to a - * stream and never exits; `--help`/`--version` and every parse error come back - * as data for `bin.ts` to act on. A leading `web` token dispatches to the web - * parser; everything else is the default TUI/headless grammar. + * Resolve the raw argv into a {@link DshInvocation}, or print and exit for + * `--help`/`--version`/a parse error. A leading `web` token dispatches to the + * web parser; everything else is the default TUI/headless grammar. * @param argv - the arguments after the node binary and script (`process.argv.slice(2)`). * @param version - the version string `--version` prints; read from this app's package.json. - * @returns the resolved invocation, discriminated by `mode`. + * @returns the resolved invocation (only reached on a valid, non-help invocation). */ export function parseDshArgs(argv: readonly string[], version: string): DshInvocation { - return argv[0] === 'web' ? parseWeb(argv.slice(1), version) : parseRoot(argv, version) + try { + return argv[0] === 'web' ? parseWeb(argv.slice(1), version) : parseRoot(argv, version) + } catch (error) { + // Commander printed help/version/the error under `exitOverride`; exit with + // the code it chose (0 for help/version, 1 for a parse or domain error). + /* v8 ignore next -- Commander only throws CommanderError from parse/error under exitOverride */ + return process.exit(error instanceof CommanderError ? error.exitCode : 1) + } } diff --git a/apps/cli/src/bin.ts b/apps/cli/src/bin.ts index 3e5f38a859..207064eb89 100644 --- a/apps/cli/src/bin.ts +++ b/apps/cli/src/bin.ts @@ -3,8 +3,8 @@ * dsh — command-line entry. Parses argv once through the Commander adapter and * switches on the resolved mode; dynamic imports keep unrelated modes out of * each dispatch path. `web` and headless prompts run their own module; - * everything else opens the TUI. `--help`/`--version` print and exit 0; a parse - * error prints to stderr and exits 1. + * everything else opens the TUI. The adapter itself prints and exits for + * `--help`/`--version`/a parse error, so only a valid mode reaches the switch. * @module @deepseek-ai/dsh/bin */ @@ -45,13 +45,6 @@ switch (invocation.mode) { await runTui(invocation.config, invocation.resume) break } - case 'help': - case 'version': - process.stdout.write(invocation.text) - process.exit(0) - case 'error': - process.stderr.write(`${invocation.message}\n`) - process.exit(1) default: invocation satisfies never throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`) diff --git a/apps/cli/src/headless.ts b/apps/cli/src/headless.ts index ccfd4c5f8a..50fe0390c8 100644 --- a/apps/cli/src/headless.ts +++ b/apps/cli/src/headless.ts @@ -71,7 +71,6 @@ async function consumeUntilTurnEnd(frames: AsyncIterable>, * @param task - the prompt text for the single turn. */ export async function runHeadless(task: string): Promise { - // A missing DEEPSEEK_API_KEY throws here (plugin load is fail-loud, uncaught by design). const host = await startHost({ boot: { diff --git a/apps/cli/src/tui.ts b/apps/cli/src/tui.ts index 6ddad89302..e741306463 100644 --- a/apps/cli/src/tui.ts +++ b/apps/cli/src/tui.ts @@ -74,13 +74,13 @@ export async function runTui(config: string | undefined, resumeSessionId: string if (current === undefined) throw new Error(`${NAME}: app boot has not completed`) // Rebuild argv from the parsed config plus the selected id: TUI mode's // only arguments are the optional config positional and `--resume `. + // The `--` guard keeps a config named like a flag or `web` a positional. const nextArgv = [ process.execPath, ...process.execArgv, entry, - ...config !== undefined ? [config] : [], - '--resume', - sessionId, + `--resume=${sessionId}`, + ...config !== undefined ? ['--', config] : [], ] try { await current.fiber.dispose() diff --git a/apps/cli/tests/args.spec.ts b/apps/cli/tests/args.spec.ts index f207a04f43..80e64534c5 100644 --- a/apps/cli/tests/args.spec.ts +++ b/apps/cli/tests/args.spec.ts @@ -1,8 +1,28 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { ALL_INTERFACES_HOST, LOOPBACK_HOST, parseDshArgs } from '../src/args.ts' const parse = (argv: string[]) => parseDshArgs(argv, '1.2.3') +/** + * `parseDshArgs` calls `process.exit` for `--help`/`--version`/errors and lets + * Commander print to the real streams; capture the exit code and mute output. + */ +function exitCode(argv: string[]): number { + const exit = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') }) + vi.spyOn(process.stdout, 'write').mockReturnValue(true) + vi.spyOn(process.stderr, 'write').mockReturnValue(true) + try { + parse(argv) + throw new Error(`expected ${JSON.stringify(argv)} to exit`) + } catch { + return exit.mock.calls.at(-1)?.[0] as number + } finally { + vi.restoreAllMocks() + } +} + +afterEach(() => { vi.restoreAllMocks() }) + describe('parseDshArgs', () => { it('routes each mode by its shape: default TUI, -p headless, web subcommand', () => { expect(parse([])).toEqual({ mode: 'tui' }) @@ -10,25 +30,24 @@ describe('parseDshArgs', () => { expect(parse(['--resume', 'sess', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' }) expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' }) expect(parse(['web'])).toEqual({ mode: 'web', host: LOOPBACK_HOST, port: 3080, dev: false }) - expect(parse(['web', '--host', ALL_INTERFACES_HOST, '--port', '8080'])) - .toEqual({ mode: 'web', host: ALL_INTERFACES_HOST, port: 8080, dev: false }) - expect(parse(['web', '--dev'])).toEqual({ mode: 'web', host: LOOPBACK_HOST, port: 3080, dev: true }) + expect(parse(['web', '--host', ALL_INTERFACES_HOST, '--port', '8080', '--dev'])) + .toEqual({ mode: 'web', host: ALL_INTERFACES_HOST, port: 8080, dev: true }) }) - it('fails loud instead of silently starting fresh or serving on bad input', () => { - // An empty resume/prompt would otherwise be swallowed (agent-loop treats an - // empty resume id as no-resume); a bad host/port must not reach the listener. - expect(parse(['--resume=']).mode).toBe('error') - expect(parse(['-p', '']).mode).toBe('error') - expect(parse(['web', '--host', '10.0.0.1']).mode).toBe('error') - expect(parse(['web', '--port', 'abc']).mode).toBe('error') - expect(parse(['--bogus']).mode).toBe('error') + it('exits nonzero instead of silently starting fresh, serving, or dropping inputs', () => { + // Empty resume/prompt would be swallowed downstream; bad host/port must not + // reach the listener; --prompt mixed with TUI inputs must not lose them. + expect(exitCode(['--resume='])).toBe(1) + expect(exitCode(['-p', ''])).toBe(1) + expect(exitCode(['web', '--host', '10.0.0.1'])).toBe(1) + expect(exitCode(['web', '--port', 'abc'])).toBe(1) + expect(exitCode(['web', '--port='])).toBe(1) + expect(exitCode(['config.yml', '-p', 'x'])).toBe(1) + expect(exitCode(['--bogus'])).toBe(1) }) - it('surfaces --help and --version as printable data, not a process exit', () => { - const help = parse(['--help']) - expect(help).toMatchObject({ mode: 'help' }) - if (help.mode === 'help') expect(help.text).toContain('Usage: dsh') - expect(parse(['--version'])).toEqual({ mode: 'version', text: '1.2.3\n' }) + it('exits 0 for --help (disclosing web) and --version', () => { + expect(exitCode(['--help'])).toBe(0) + expect(exitCode(['--version'])).toBe(0) }) }) diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index 6a77e8919d..9fd1d55ab2 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -36,7 +36,8 @@ function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string } child.kill('SIGKILL') reject(new Error(`dsh built bin did not exit within 25s. stdout:\n${stdout}\nstderr:\n${stderr}`)) }, 25_000) - child.on('exit', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) }) + // Resolve on `close` (all stdio drained), not `exit`, so captured output is complete. + child.on('close', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) }) child.on('error', (err) => { clearTimeout(timer); reject(err) }) child.stdin.end() }) diff --git a/examples/tui-agent/cordis.yml b/examples/tui-agent/cordis.yml index af225565e9..e96ef680b2 100644 --- a/examples/tui-agent/cordis.yml +++ b/examples/tui-agent/cordis.yml @@ -34,8 +34,8 @@ model: deepseek-v4-pro # `dsh --resume ` provides the session id on the boot context (the ids # live under ./.sessions); with no flag the identifier is undefined and a - # fresh session starts each run. The demo bin never provides it, so the - # typeof guard reads undefined there rather than throwing. + # fresh session starts each run. The typeof guard tolerates a launcher that + # never provides the slot, reading undefined rather than throwing. resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined" persistenceRoot: './.sessions' # Printed on exit and listed by `/resume`; `{session}` fills the live id. diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts index d144127498..0b6ad2b2ec 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -359,7 +359,7 @@ describe('config-driven session id', () => { await ctx2.fiber.dispose() }) - it('config-driven resumeSessionId continues a persisted session (env-var resume)', async () => { + it('config-driven resumeSessionId continues a persisted session', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-resume-')) dirs.push(root) diff --git a/packages/examples/tui-demo/package.json b/packages/examples/tui-demo/package.json index 26e3e64183..50145e6c29 100644 --- a/packages/examples/tui-demo/package.json +++ b/packages/examples/tui-demo/package.json @@ -27,7 +27,6 @@ ], "license": "BSD-3-Clause", "peerDependencies": { - "@cordisjs/plugin-include": "^1.0.4", "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "^0.0.1", @@ -51,7 +50,6 @@ "schemastery": "^3.17.0" }, "devDependencies": { - "@cordisjs/plugin-include": "workspace:^", "@cordisjs/plugin-loader": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", diff --git a/packages/examples/tui-demo/tsconfig.json b/packages/examples/tui-demo/tsconfig.json index d87f0f1c9e..d26d5b7da6 100644 --- a/packages/examples/tui-demo/tsconfig.json +++ b/packages/examples/tui-demo/tsconfig.json @@ -14,12 +14,6 @@ { "path": "../../../vendor/schemastery" }, - { - "path": "../../../vendor/loader" - }, - { - "path": "../../ui/app-boot" - }, { "path": "../../core/agent" }, diff --git a/packages/ui/README.md b/packages/ui/README.md index f8e4704f20..772b7a9d57 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -18,4 +18,4 @@ A UI integration is a client-driver plugin, not a loop change: it consumes the e `user-approval`, `user-interaction`, and `tool-ask-user` live here because asking a human is a UI-backed product affordance, not part of the providerless core spine. `user-approval` owns the one-shot `ctx.approval` decision mechanism and its policy tier; answerers remain with their UI channel owners. `user-interaction` remains provider-neutral (`ctx.userInteraction`), while `tool-ask-user` is its model-facing consumer and the app/bridge packages provide concrete providers. -The runnable app bundles that bake these bridges into boot bins — the TUI app, ACP server app, and JSON-RPC SDK-runtime bin — live in [`examples/`](../examples/README.md) (`tui-demo`, `acp-demo`, `jsonrpc-demo`), each composed over the [`agent-spine-demo`](../examples/agent-spine-demo/README.md) bundle. `ui/` keeps the reusable bridge/channel plugins and the `app-boot` glue; each front door owns its stdout policy, and a leaf `cordis.yml` supplies backends and optional tools. +The runnable app bundles that compose these bridges — the TUI app, ACP server app, and JSON-RPC SDK-runtime bin — live in [`examples/`](../examples/README.md) (`tui-demo`, `acp-demo`, `jsonrpc-demo`), each composed over the [`agent-spine-demo`](../examples/agent-spine-demo/README.md) bundle. `acp-demo` and `jsonrpc-demo` own boot bins; the `tui-demo` bundle is booted by the product [`dsh`](../../apps/cli/README.md) CLI. `ui/` keeps the reusable bridge/channel plugins and the `app-boot` glue; each front door owns its stdout policy, and a leaf `cordis.yml` supplies backends and optional tools. diff --git a/packages/ui/app-boot/src/index.ts b/packages/ui/app-boot/src/index.ts index 00e316ebe8..df2d2b1ba4 100644 --- a/packages/ui/app-boot/src/index.ts +++ b/packages/ui/app-boot/src/index.ts @@ -1,5 +1,5 @@ /** - * Shared boot glue for the app bins (`dsh-tui-demo`, `dsh-cli-demo`, `dsh-acp-demo`): load the gitignored + * Shared boot glue for the app bins (`dsh`, `dsh-cli-demo`, `dsh-acp-demo`): load the gitignored * `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the * optional personal overlay patches from the Harness home (`~/.dsh`), and drive the cordis Loader * against a leaf `cordis.yml` until the whole tree has settled. @@ -156,7 +156,6 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void { } } -/** /** * Context key a bin sets through {@link boot}'s `prepare` hook to hand a resume * session id to the booted config: `ctx.provide(RESUME_SESSION_ID_KEY, id)` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cccc15282..e0153978b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1539,9 +1539,6 @@ importers: packages/examples/tui-demo: devDependencies: - '@cordisjs/plugin-include': - specifier: workspace:^ - version: link:../../../vendor/include '@cordisjs/plugin-loader': specifier: workspace:^ version: link:../../../vendor/loader @@ -1604,7 +1601,7 @@ importers: version: link:../../context/workspace-context cordis: specifier: ^4.0.0-rc.7 - version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader) schemastery: specifier: ^3.17.0 version: 3.18.0