feat(cli)!: make dsh run the headless entrypoint
This commit is contained in:
@@ -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 .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.zh.md: bc51542ac8159ee7cba234b4ee8b4db47a7f9b58
|
||||
2026-07-19-gui-layering-and-rpc-protocol.md: 8e020e4fe9b60100671c0cf0e98e28532d850f94
|
||||
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:
|
||||
|
||||
- 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`
|
||||
|
||||
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.
|
||||
- `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/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.
|
||||
|
||||
```
|
||||
@@ -215,7 +215,7 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
|
||||
|
||||
| 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) |
|
||||
| `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 |
|
||||
|
||||
@@ -9,7 +9,7 @@ Status: implemented
|
||||
## Problem
|
||||
|
||||
需要提供 UI 对接层,除已有 ACP/stdio基础版本外,还需要 Web(server) 、 Electron 、等其他产品 UI 形态。我们把这些形态统一称为 Client。希望有如下能力支持:
|
||||
- 以 `dsh` 进程,同时支持 `dsh web`(启动) 和 `dsh -p`(headless) ,一个进程两种模式(设计预留)
|
||||
- 以 `dsh` 进程,同时支持 `dsh web`(启动) 和 `dsh run`(headless) ,一个进程两种模式(设计预留)
|
||||
- 以与 `dsh web` 同构的 Web 技术形态,在 Electron 中启动
|
||||
|
||||
那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 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 服务。
|
||||
- `apps/` 作为对外导出的应用形态入口,可以由 Client / Host 混合组装。
|
||||
- `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 包。
|
||||
|
||||
```
|
||||
@@ -213,7 +213,7 @@ export type ResponseValue<K> =
|
||||
|
||||
| 子类 | 所在包 | 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) |
|
||||
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) |
|
||||
| (将来)IPC 桥子类 | apps/electron | IPC 序列化往返 | 仅换 doFetch,契约/基类零改 |
|
||||
|
||||
@@ -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 .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.zh.md: 0e9ebf657ccb9d05967d90a935b356acf287a24c
|
||||
2026-08-05-profile-plugin-bundles.md: b5bf5411d22ab99b598f667886b3c29ba8ee7b06
|
||||
2026-08-05-profile-plugin-bundles.zh.md: ae790028b5768c05c57acd27d7f68bdc4d612c11
|
||||
|
||||
@@ -12,7 +12,7 @@ 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`.
|
||||
|
||||
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).
|
||||
|
||||
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,7 @@ 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` 使用完全相同的路径。
|
||||
|
||||
已交付的组合改造成了组合包:`@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.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;
|
||||
# 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
|
||||
2026-08-03-cli-signal-shutdown-escalation.md: 2746b5784baad0f3b14258280cd56a621db07c15
|
||||
2026-08-03-cli-signal-shutdown-escalation.zh.md: 0bda83327d4cc8fe2edb61f8145a89138610901e
|
||||
2026-08-03-cli-signal-shutdown-escalation.md: 7c9715c37ee57be9fa0f67af0c19f0bfa84845da
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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` 限制的完成 promise;OTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此,代理/沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。
|
||||
随后有用户复现,headless 命令在打印观察 URL 后立即卡死,重复按 `Ctrl+C` 也没有反应;设置 `DSH_TELEMETRY_DISABLED=1` 后不再卡死,而同一 Linux 沙箱中的独立 Node 信号处理器能够收到 SIGINT。这将待结算的 disposer 定位到遥测,而非终端信号转发。OTel 的 `BatchLogRecordProcessor.shutdown()` 会先等待 `exporter.forceFlush()`,再进入受 `exportTimeoutMillis` 限制的完成 promise;OTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此,代理/沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。
|
||||
|
||||
闩锁随后把这个遥测缺陷变成无法终止的 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: aac2a473760509626d315df8d57eb405eb547abf
|
||||
2026-08-08-dsh-run-headless-command.zh.md: d71d2a34addf1c64b8cb37c54117be5b9c643370
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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.
|
||||
|
||||
`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,39 @@
|
||||
# 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 用空格拼接可变数量的任务参数,并在启动前拒绝缺失或空白任务。
|
||||
|
||||
`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`。
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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
|
||||
2026-08-03-explicit-config-dsh-entrypoint.md: e0d1e954d9cef472ea59345a3d2ef5a67bd03ae8
|
||||
2026-08-03-explicit-config-dsh-entrypoint.zh.md: b5b464e3b45a6f3909bbf087f7005ad3f819424a
|
||||
2026-08-03-explicit-config-dsh-entrypoint.zh.md: 614c2d8600731d85c83d6559bc577350da25e872
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note:显式配置的 dsh 入口
|
||||
# Agent Note: 显式配置的 dsh 入口
|
||||
|
||||
Status: implemented
|
||||
|
||||
|
||||
Reference in New Issue
Block a user