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

This commit is contained in:
Tianyi Cui
2026-08-08 15:42:47 +08:00
143 changed files with 1211 additions and 2360 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.md
2026-08-08-dsh-run-headless-command.md: d1cc0573d1d94bb7b30e98aa685d2b7d89878a99
2026-08-08-dsh-run-headless-command.zh.md: 7b48dfd708efb197babcfcbeaf4f8a4ca76cde3f

View File

@@ -0,0 +1,41 @@
# Agent Note: `dsh run` owns one-shot headless execution
Status: implemented
English | [中文](2026-08-08-dsh-run-headless-command.zh.md)
## Problem
The product launcher attached optional task text to its generic profile boot: `dsh --profile headless "task"`. That made one argv shape mean either a long-lived profile or a one-shot run according to a row discovered only after composition. The parser's `ProfileInvocation` carried optional task state, help presented a profile implementation detail as the user command, and a custom profile could accept a task only through the same overloaded root.
The former `dsh -p` spelling was already absent from the parser, so restoring it or detecting it specially would add compatibility machinery to a pre-release interface. A separate application-file proposal also used the `run` verb, leaving two incompatible owners for one top-level command.
## Decision
One-shot execution owns an explicit grammar:
```text
dsh run [--profile <name>] [--patch <path>...] <task...>
```
`--profile` defaults to `headless` and remains available for custom one-shot compositions. `--patch` is repeatable and occupies the existing overlay layer. Commander joins the variadic task arguments with spaces and rejects a missing or blank task before boot.
The [profile plugin bundle decision](../architecture/2026-08-05-profile-plugin-bundles.md) owns the composition selected by this grammar.
`RunInvocation` is a separate `DshInvocation` member. The generic profile invocation no longer carries task text, and its root command accepts no positional arguments. Both dispatch paths call the existing deep `runProfile` module: `profile` omits `task`, while `run` supplies it. There is no shallow `run.ts` forwarding module and no alias, warning, or custom detector for former spellings; they fail through the ordinary Commander grammar. A one-shot profile without `headless-runner` still fails through the existing composed-row check, while booting a profile that contains that row without a task points to `dsh run --profile <name> "<task>"`.
The `run` verb belongs to one-shot task execution. Launching an application file must choose another command name; two top-level meanings selected by positional shape would recreate the ambiguity this command removes.
The runner's user-visible contract stays the same: a fresh persisted session, browser observation URL on stderr, final assistant text on stdout, completed/non-completed exit mapping, and bounded signal shutdown. The product-level keyless acceptance exposed that the in-process mux consumer could lag the same-process `agent/status: idle` notification and derive output before reading the final frames. The idle notification now captures the authoritative final session sequence, and the runner waits until the ordered mux reaches that boundary (or the stream ends) before deriving text and exit reason. This enforces the existing idle-to-idle contract without adding a wire field or a timing delay.
## Alternatives considered
- **Keep task text on `dsh --profile`.** Rejected because profile boot and one-shot execution remain one grammar whose meaning depends on a late composition check.
- **Preserve `dsh -p` or the positional profile form as aliases.** Rejected under the pre-release stance: compatibility branches would outlive the interface they were meant to retire.
- **Make `--profile headless` mandatory under `run`.** Rejected because the shipped one-shot surface should have the shortest canonical spelling, while optional `--profile` preserves plugin-defined one-shot compositions.
- **Give `dsh run` to application-file launch and choose another headless verb.** Rejected because `run` describes executing a task through the harness; application-file ownership would make the product's primary one-shot command less direct and collide with custom one-shot profiles.
- **Add `apps/cli/src/run.ts`.** Rejected because it would only forward to `runProfile`, splitting command ownership without hiding any complexity.
## Consequences
This is an intentional breaking CLI change. Documentation, help, parser tests, built-bin acceptance, PTY shutdown coverage, and the assembled keyless snapshot use `dsh run`. Existing custom one-shot profiles keep working through `--profile`; long-lived profiles and config dumps retain their existing root grammar. The competing application-file command must be renamed and rebased separately rather than sharing or overloading `run`.

View File

@@ -0,0 +1,41 @@
# Agent Note: `dsh run` 负责一次性 headless 执行
Status: implemented
[English](2026-08-08-dsh-run-headless-command.md) | 中文
## 问题
产品启动器过去把可选任务文本挂在通用 profile 启动命令上:`dsh --profile headless "task"`。于是,同一种 argv 形态会表示常驻 profile 或一次性运行,具体含义取决于组合完成后才发现的配置行。解析器的 `ProfileInvocation` 携带可选任务状态,帮助信息把 profile 的实现细节呈现为用户命令,自定义 profile 也只能通过同一个过载的根命令接收任务。
解析器中已经没有原来的 `dsh -p` 写法,因此恢复该写法或加入特殊检测,会给预发布接口增加兼容机制。另一个应用文件提案也使用 `run` 动词,使同一个顶层命令同时归属两个互不兼容的功能。
## 决策
一次性执行采用明确语法:
```text
dsh run [--profile <name>] [--patch <path>...] <task...>
```
`--profile` 默认为 `headless`,同时保留对自定义一次性组合的支持。`--patch` 可重复使用,并沿用既有 overlay 层的位置。Commander 用空格拼接可变数量的任务参数,并在启动前拒绝缺失或空白任务。
[profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)负责该语法所选择的组合。
`RunInvocation` 是单独的 `DshInvocation` 成员。通用 profile 调用不再携带任务文本,其根命令也不接受位置参数。两条分派路径都调用已有的深层 `runProfile` 模块:`profile` 省略 `task`,`run` 则提供该字段。实现中没有只负责转发的浅层 `run.ts` 模块,也没有面向旧写法的别名、警告或自定义检测器;旧写法会按普通 Commander 语法失败。缺少 `headless-runner` 的一次性 profile 仍会触发既有的组合行检查;如果启动的 profile 包含该行却未提供任务,错误会指向 `dsh run --profile <name> "<task>"`。
`run` 动词只负责一次性任务执行。应用文件启动必须选择其他命令名;如果让两个顶层含义由位置参数形态决定,就会重新引入本命令消除的歧义。
运行器面向用户的契约保持不变:创建新的持久化会话,在 stderr 打印浏览器观察 URL,在 stdout 打印最终 assistant 文本,将完成/未完成映射为退出状态,并执行有界的信号关闭。产品级无密钥验收用例发现,进程内 mux 消费方可能落后于同进程的 `agent/status: idle` 通知,在读到最终帧之前就生成输出。idle 通知现在会捕获权威的会话最终事件序号,运行器则等待有序 mux 到达该边界(或流结束),再生成文本和退出原因。这一机制在不增加 wire 字段或定时延迟的前提下,落实了既有的 idle-to-idle 契约。
## 考虑过的替代方案
- **把任务文本保留在 `dsh --profile` 上。** 不予采纳:profile 启动和一次性执行仍共用同一套语法,其含义取决于较晚发生的组合检查。
- **保留 `dsh -p` 或位置参数 profile 形式作为别名。** 不予采纳:根据预发布立场,这些兼容分支会比本应退役的接口存续更久。
- **要求在 `run` 下必须指定 `--profile headless`。** 不予采纳:已交付的一次性接口应采用最短的规范写法,同时用可选的 `--profile` 保留插件定义的一次性组合。
- **把 `dsh run` 交给应用文件启动,并为 headless 选择另一个动词。** 不予采纳:`run` 描述的是通过 harness 执行任务;若归应用文件所有,产品的主要一次性命令会更不直接,并与自定义一次性 profile 冲突。
- **新增 `apps/cli/src/run.ts`。** 不予采纳:它只会转发到 `runProfile`,拆分命令归属,却没有隐藏任何复杂度。
## 后果
这是一次有意为之的 CLI(命令行界面)破坏性变更。文档、帮助信息、解析器测试、构建后二进制验收、PTY 关闭覆盖和组装应用的无密钥快照都使用 `dsh run`。现有自定义一次性 profile 可继续通过 `--profile` 工作;常驻 profile 和配置 dump 保留既有的根命令语法。与之竞争的应用文件命令必须单独改名并 rebase,不得共享或重载 `run`。