@logicseek/hello-cc 1.0.0 → 1.0.2-dsh.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +36 -0
  3. package/README.zh-CN.md +35 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/guide.md +58 -0
  11. package/docs/guide.zh-CN.md +55 -0
  12. package/docs/mcp.md +9 -0
  13. package/docs/mcp.zh-CN.md +23 -0
  14. package/docs/native.md +295 -0
  15. package/docs/native.zh-CN.md +253 -0
  16. package/docs/web-handoff.zh-CN.md +53 -0
  17. package/lib/cli/app.mjs +27 -6
  18. package/lib/cli/commands/coordination.mjs +3 -1
  19. package/lib/cli/commands/doctor.mjs +17 -2
  20. package/lib/cli/commands/dsh.mjs +79 -0
  21. package/lib/cli/commands/gc.mjs +182 -32
  22. package/lib/cli/commands/hook.mjs +241 -90
  23. package/lib/cli/commands/lock.mjs +34 -19
  24. package/lib/cli/commands/mcp.mjs +28 -0
  25. package/lib/cli/commands/misc.mjs +15 -0
  26. package/lib/cli/commands/native.mjs +130 -0
  27. package/lib/cli/commands/peer.mjs +21 -5
  28. package/lib/cli/commands/run.mjs +66 -21
  29. package/lib/cli/commands/task.mjs +6 -3
  30. package/lib/cli-args.mjs +4 -0
  31. package/lib/coordination-state.mjs +15 -1
  32. package/lib/core/coordination/event-retention.mjs +14 -2
  33. package/lib/core/coordination/gc-plan.mjs +3 -13
  34. package/lib/core/coordination/locks.mjs +45 -0
  35. package/lib/core/coordination/results.mjs +79 -0
  36. package/lib/core/peers/bindings.mjs +9 -3
  37. package/lib/diagnostics/codex.mjs +121 -0
  38. package/lib/integrations/codex-interactions.mjs +114 -0
  39. package/lib/integrations/dsh-collaboration.mjs +170 -0
  40. package/lib/integrations/dsh-cordis.d.ts +19 -0
  41. package/lib/integrations/dsh-cordis.mjs +177 -0
  42. package/lib/integrations/dsh.bundle.yml +7 -0
  43. package/lib/integrations/dsh.mjs +287 -0
  44. package/lib/integrations/native/claude.mjs +451 -0
  45. package/lib/integrations/native/codex.mjs +242 -0
  46. package/lib/integrations/native/dsh-acp.mjs +343 -0
  47. package/lib/integrations/native/index.mjs +19 -0
  48. package/lib/integrations/native/interactions.mjs +53 -0
  49. package/lib/integrations/native/jsonrpc.mjs +185 -0
  50. package/lib/integrations/peers/identity.mjs +36 -10
  51. package/lib/integrations/peers/processes.mjs +22 -8
  52. package/lib/integrations/providers.mjs +6 -2
  53. package/lib/mcp/scope.mjs +104 -0
  54. package/lib/mcp/stdio.mjs +80 -0
  55. package/lib/mcp/tools.mjs +197 -0
  56. package/lib/process/identity.mjs +33 -1
  57. package/lib/release/release-notes.mjs +9 -0
  58. package/lib/runtime/buffer-directory-lease.mjs +2 -1
  59. package/lib/runtime/client.mjs +3 -0
  60. package/lib/runtime/native/client.mjs +38 -0
  61. package/lib/runtime/native/service.mjs +511 -0
  62. package/lib/runtime/native/store.mjs +121 -0
  63. package/lib/shared/file-lock.mjs +11 -4
  64. package/lib/shared/socket-lock-worker.mjs +73 -24
  65. package/lib/terminal/tmux.mjs +53 -23
  66. package/lib/ui/help.mjs +5 -2
  67. package/lib/web/adoption-state.mjs +55 -0
  68. package/lib/web/auto-attach.mjs +18 -2
  69. package/lib/web/buffer-gc-runtime.mjs +2 -2
  70. package/lib/web/codex-app-server.mjs +557 -0
  71. package/lib/web/codex-sessions.mjs +287 -0
  72. package/lib/web/control-lease.mjs +204 -0
  73. package/lib/web/http-routes.mjs +197 -15
  74. package/lib/web/http.mjs +5 -4
  75. package/lib/web/native-sessions.mjs +286 -0
  76. package/lib/web/project-contexts.mjs +8 -2
  77. package/lib/web/pty-sessions.mjs +60 -33
  78. package/lib/web/runtime-main.mjs +101 -6
  79. package/lib/web/session-serialize.mjs +0 -0
  80. package/lib/web/startup.mjs +4 -2
  81. package/lib/web/task-results.mjs +76 -0
  82. package/lib/web/tmux-sessions.mjs +92 -11
  83. package/lib/web/tmux-stream.mjs +2 -2
  84. package/lib/web/ui-codex.mjs +550 -0
  85. package/lib/web/ui-command-palette.mjs +119 -0
  86. package/lib/web/ui-handoff.mjs +102 -0
  87. package/lib/web/ui-history.mjs +118 -0
  88. package/lib/web/ui-i18n.mjs +841 -0
  89. package/lib/web/ui-interactions.mjs +78 -0
  90. package/lib/web/ui-login.mjs +101 -0
  91. package/lib/web/ui-native.mjs +165 -0
  92. package/lib/web/ui-preferences.mjs +278 -0
  93. package/lib/web/ui-review.mjs +132 -0
  94. package/lib/web/ui-template.mjs +1106 -576
  95. package/lib/web/ui-terminal-find.mjs +98 -0
  96. package/lib/web/ui-workbench.mjs +94 -0
  97. package/lib/web/ui-workspace.mjs +222 -0
  98. package/package.json +12 -4
  99. package/scripts/check-free-identifiers.mjs +67 -8
  100. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  101. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  102. package/scripts/dsh-installed-acceptance.mjs +247 -0
  103. package/scripts/dsh-official-acceptance.mjs +180 -0
  104. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  105. package/scripts/github-release.mjs +3 -1
  106. package/scripts/native-live-acceptance.mjs +369 -0
  107. package/scripts/regression.mjs +338 -187
  108. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  109. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  110. package/scripts/web-native-model-acceptance.mjs +138 -0
  111. package/docs/architecture-split-review.md +0 -217
  112. package/docs/current-state-review-2026-08.md +0 -341
  113. package/docs/defect-review-2026-07.md +0 -382
  114. package/docs/layout-split-stack-audit.md +0 -298
  115. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +0 -294
  116. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +0 -400
  117. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +0 -284
  118. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +0 -303
  119. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +0 -71
  120. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +0 -117
  121. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +0 -120
  122. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +0 -117
  123. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +0 -104
@@ -0,0 +1,253 @@
1
+ # Native 后台 worker
2
+
3
+ Native 模式运行由 HCC 创建并持有的后台 worker。共享 peer、任务、消息、锁和交接
4
+ 仍由 HCC 管理;每个 provider adapter 管理自己的执行连接,并报告结构化生命周期
5
+ 事件。
6
+
7
+ | Provider | 连接方式 | 会话身份 | 忙碌时的新输入 |
8
+ | --- | --- | --- | --- |
9
+ | Codex | HCC 持有的 `codex app-server`,stdio JSON-RPC | Codex thread ID | adapter 支持带活动 turn 前置条件的 `turn/steer` |
10
+ | Claude | 可选 Claude Agent SDK,持久双向输入流 | SDK 初始化确认的 Claude session ID | 一次一个 submission,暂不支持 steer |
11
+ | DeepSeek Harness | HCC 持有的 `dsh --profile acp` 进程 | ACP session ID | 一次一个 submission,暂不支持 steer |
12
+
13
+ 当前 HCC host 对每个 worker 逐条投递 inbox 消息,Codex 也一样。adapter 支持 steer
14
+ 不代表 host 会把多条待处理消息塞进同一个 turn。ACP 的 resume 和 session close
15
+ 取决于 provider 声明的扩展;不能把 ACP resume 等同于重放原始会话历史。
16
+
17
+ ## 环境准备
18
+
19
+ 需要 Node.js 24 或更新版本,并在运行 HCC 的同一环境中安装、登录对应 provider。
20
+ Native 模式保留身份认证配置,不改写账号、provider 设置或用户主目录。
21
+
22
+ Codex 需要支持 app-server 的 CLI。DeepSeek Harness 使用 ACP profile,实现基线为
23
+ `@deepseek-ai/dsh-acp@0.2.0-rc.2`;此模式不连接已有 dsh Web service。
24
+
25
+ Claude SDK 保持可选,只在打开 Claude worker 时动态加载。在本仓库 checkout 中,
26
+ 手动安装文档指定版本:
27
+
28
+ ```sh
29
+ npm install --no-save --package-lock=false @anthropic-ai/claude-agent-sdk@0.3.287
30
+ ```
31
+
32
+ 如果 HCC 通过 npm 全局安装,在同一个 npm prefix 中安装 SDK:
33
+
34
+ ```sh
35
+ npm install -g @anthropic-ai/claude-agent-sdk@0.3.287
36
+ ```
37
+
38
+ 也可以只在 worker 的项目目录中安装 SDK:
39
+
40
+ ```sh
41
+ cd /absolute/path/to/worker-project
42
+ npm install --no-save --package-lock=false @anthropic-ai/claude-agent-sdk@0.3.287
43
+ ```
44
+
45
+ 默认加载器先查 HCC 安装位置,再查 worker 项目的依赖;因此全局 HCC 也可使用项目内
46
+ 安装的 SDK。HCC 不会自动安装包。找不到 SDK 时给出安装提示;SDK 已找到但导出或
47
+ 内部依赖损坏时保留真实错误,避免误报“未安装”。默认路径的真实 Web 工具审批
48
+ 已有独立回执 (源码目录: `docs/verification/2026-10-02-native-web-claude-default-sdk.json`),不代表
49
+ 发布包或员工设备已完成安装验收。
50
+
51
+ ## CLI 使用
52
+
53
+ 在需要共享状态的项目目录中运行,或使用已有全局 `--root`、`--db` 指定项目和数据库。
54
+
55
+ ```sh
56
+ hcc native up
57
+ hcc native start --peer codex-reviewer --provider codex
58
+ hcc native start --peer claude-reviewer --provider claude
59
+ hcc native start --peer dsh-reviewer --provider dsh
60
+ hcc native send --peer codex-reviewer --from coordinator --body "审查当前修改,报告具体问题和证据。"
61
+ hcc native status
62
+ hcc native deliveries --peer codex-reviewer
63
+ hcc native events --peer codex-reviewer --after 0
64
+ hcc native requests --peer codex-reviewer
65
+ hcc native respond --peer codex-reviewer --request REQUEST_ID --decision accept
66
+ hcc msg inbox --peer coordinator
67
+ ```
68
+
69
+ `start` 会按需启动本项目的 native runtime。`--cwd DIR` 可以指定 worker 工作目录,
70
+ 它仍共享 host 所选项目的 HCC 消息总线。`--model MODEL` 选择 provider 支持的模型;
71
+ `--binary PATH` 覆盖 Codex 或 dsh 可执行文件。Claude 使用 SDK,传入 `--binary`
72
+ 会直接报错。
73
+
74
+ | 命令 | 参数与作用 |
75
+ | --- | --- |
76
+ | `hcc native up` | 启动或复用本项目后台 runtime |
77
+ | `hcc native status` | 查看持久化 worker 状态和 runtime 身份 |
78
+ | `hcc native start` | 必须 `--peer NAME --provider codex\|claude\|dsh`;可选 `--cwd DIR --model MODEL --resume last`;`--binary PATH` 仅适用于 Codex 和 dsh |
79
+ | `hcc native send` | 必须 `--peer NAME --body TEXT`;可选 `--from NAME --task ID`;返回持久消息/submission 回执 |
80
+ | `hcc native deliveries` | 可选 `--peer NAME`,查看投递回执 |
81
+ | `hcc native events` | 必须 `--peer NAME`;可选 `--after ID`,查看有界事件历史 |
82
+ | `hcc native requests` | 必须 `--peer NAME`;查看与该执行器、会话、turn 绑定的待处理请求 |
83
+ | `hcc native respond` | 必须 `--peer NAME --request ID --decision accept\|decline\|cancel`;权限子集或问题答案通过 `--response-file JSON` 提交 |
84
+ | `hcc native interrupt` | 必须 `--peer NAME`;可选 `--turn ID`,请求中断活动 turn |
85
+ | `hcc native close` | 必须 `--peer NAME`,关闭该 worker 持有的连接或进程 |
86
+ | `hcc native down` | 请求关闭 worker 和停止 runtime,返回停止请求回执 |
87
+
88
+ `native down` 的回执只确认停止请求,不证明关闭已完成。需要检查 `native status`
89
+ 确认结果。若 provider 关闭失败,HCC 保留 runtime owner,并在
90
+ `status.shutdown_error` 报告错误;处理所报问题后,再次执行 `native down`。
91
+
92
+ 事件保留语义输出和完成证据,不把 token delta 持久化为投递回执。runtime 状态位于
93
+ `<project>/.hello-cc/native/`,协作记录仍写入所选 HCC 数据库。控制接口是独立于 Web
94
+ 终端传输的本地认证 API;其 runtime pointer 和日志属于项目私有状态。
95
+
96
+ ## 如何理解投递回执
97
+
98
+ `native send` 成功只代表消息已入队,不能据此声称 provider 已读、执行完成或产出
99
+ 真实业务结果。
100
+
101
+ | 状态 | 含义 |
102
+ | --- | --- |
103
+ | `queued` | 已写入 HCC 消息总线,等待投递 |
104
+ | `dispatching` | HCC 开始把 submission 交给 adapter,provider admission 尚未确认 |
105
+ | `submitted` | adapter 已把 prompt 写入或排入输入流,尚无 provider admission 回执 |
106
+ | `accepted` | provider admission 回执或对应 provider 输出证明该 submission/turn 已开始处理,工作仍未完成 |
107
+ | `completed` | 对应的主 turn 权威结果报告成功,HCC ACK 该消息;非 reply 消息可以自动写回回复 |
108
+ | `failed` | 明确拒绝或不成功的主 turn 结果,不代表成功 ACK |
109
+ | `uncertain` | 超时、进程/连接丢失或 runtime 重启后结果不明,自动重试可能重复执行 |
110
+
111
+ SDK 消费输入迭代器只能证明传输提交,不代表 provider 已接受或模型 turn 已完成。
112
+ Claude 观察到对应 assistant 输出后,才从 `submitted` 升为 `accepted`。Claude 在初始化事件
113
+ 中确认真实 session ID;刚打开 worker、尚未发送首条 prompt 时,session ID 可以为
114
+ null。中断请求本身不会完成投递,也不一定取消还在 SDK 队列中的输入;需要对应主
115
+ turn 结果才能归结。
116
+
117
+ inbox 中 `kind: 'reply'` 的消息仍作为 context 投递,主 turn 成功后才 ACK。其输出
118
+ 保留在 `events` 中,但 HCC 不再自动生成 reply,避免回复回路。其他 kind 的消息在
119
+ 主 turn 成功且有输出时,自动向发送方写回 reply。
120
+
121
+ 出现 `uncertain` 时,先检查 `deliveries`、`events` 和 provider 状态,再决定是否发送
122
+ 替代消息。HCC 不自动重投 uncertain 记录;runtime 重启会把在途状态记为 uncertain,
123
+ 不会假定已经失败或完成。
124
+
125
+ ## 会话所有权、hooks 和权限
126
+
127
+ 使用同一个 peer、同一个 provider 恢复 HCC 保存的会话:
128
+
129
+ ```sh
130
+ hcc native close --peer codex-reviewer
131
+ hcc native start --peer codex-reviewer --provider codex --resume last
132
+ ```
133
+
134
+ 显式 session ID 也必须与 HCC 保存的该 peer/provider 记录一致。resume 通过新持有
135
+ 的连接恢复会话历史,不是附着已有的活动 TUI 或桌面实例。HCC 无法证明外部应用
136
+ 没有独立打开同一个保存会话,因此不要同时在 owned runtime 外写入该会话。
137
+
138
+ worker 获得明确的 `HCC_ROOT`、`HCC_DB`、`HCC_PEER`、`HCC_NATIVE_OWNER`。provider hook
139
+ 必须匹配该 peer 的 native transport owner。native hook 路径只刷新 owner 心跳和续
140
+ 期其锁,不覆盖 provider binding 或进程身份、不再次注入 inbox、不 ACK 消息,也
141
+ 不阻塞 Stop hook。owner 标记不匹配时直接拒绝,不回退到普通终端 hook 投递。
142
+
143
+ provider 保留各自的权限检查。runtime 托管的请求等待当前 Web 控制窗口或本地 CLI
144
+ 明确应答;关闭 Web 后,worker 和待处理请求仍可由本地 CLI 继续处理。应答同时
145
+ 检查 runtime generation、worker owner、provider、executor、session、turn 和 request
146
+ ID。观察窗口不能应答,控制权切换后旧窗口的 epoch 失效。
147
+
148
+ | Provider | 已接入的人工交互 |
149
+ | --- | --- |
150
+ | Codex | 命令/文件审批、所请求文件与网络权限的子集、明确的 turn/session 有效期、选项/自由输入/敏感问题,以及单次空表单 MCP 工具确认 |
151
+ | Claude | SDK 工具权限 callback,批准原始输入本次执行,不改写工具参数 |
152
+ | dsh ACP | 选择 provider 提供的 `allow_once`,不自动选择 `allow_always` |
153
+
154
+ rc.2 的 ACP 审批可能只携带工具 ID。适配器会关联同一 session/turn 的工具更新以展示操作输入;等待后仍缺输入或输入被截断时可以拒绝,允许会返回 `NATIVE_APPROVAL_CONTEXT_MISSING`。实际安装与拒绝证明见 dsh 验收 (源码目录: `docs/verification/2026-10-02-dsh-cordis-native.md`)。
155
+
156
+ Codex 交互会话在 thread start/resume 的私有配置中显式开启
157
+ `features.default_mode_request_user_input` 和 `features.request_permissions_tool`,
158
+ 使执行模式下的模型能够调用提问和权限请求工具。Web 专用 App Server 的 start、resume、
159
+ fork 也使用同一配置。它们是已验收 Codex 0.144.6 中的实验能力,不写全局配置,
160
+ 不预先授予文件或网络权限,审批仍使用 `on-request` 和用户审核。未托管且未开启
161
+ interactive 的 adapter 保留原默认值。权限表单合并新旧协议字段中的同一路径,
162
+ 保留不同权限、原字段索引与禁止规则。
163
+
164
+ turn 完成、中断成功、provider 自行解决请求或 worker 关闭时,待处理请求会失效。
165
+ 中断后的迟到请求也保持取消,直到下一轮任务开始。直接使用未托管 adapter 时仍
166
+ 保持拒绝。Claude 使用 `permissionMode: 'default'`,该模式本来允许的工具仍可执行。
167
+
168
+ 问题的直接答案不会写入 HCC 状态快照、事件、投递回执或浏览器存储。表单草稿仅
169
+ 存在当前页面内存,状态刷新会保留输入与焦点;页面关闭后不保留敏感答案。
170
+
171
+ 本地处理 Codex 权限或问题时,先用 `native requests` 读取原请求,再通过 JSON 文件
172
+ 提交。以下是两个独立示例,路径、问题 ID 和选项文本必须与原请求一致:
173
+
174
+ ```json
175
+ {"permissions":{"fileSystem":{"read":["/requested/read/path"]}},"scope":"turn"}
176
+ ```
177
+
178
+ ```json
179
+ {"answers":{"question-id":{"answers":["原请求中的选项或允许的自由答复"]}}}
180
+ ```
181
+
182
+ ```sh
183
+ hcc native respond --peer codex-reviewer --request REQUEST_ID --decision accept --response-file ./response.json
184
+ hcc native respond --peer codex-reviewer --request REQUEST_ID --decision cancel
185
+ ```
186
+
187
+ 响应文件只允许 `permissions`、`scope`、`answers`。不能添加原请求没有的路径或
188
+ 网络权限;授予文件权限时必须保留原请求的 deny 条目。所有问题都需明确回答;
189
+ 请求参数被截断时,只能拒绝或取消。动态工具与账号 token 刷新尚未接入。
190
+
191
+ Codex MCP 工具审批使用 `mcpServer/elicitation/request`,与命令/文件审批的返回结构
192
+ 不同。HCC 对显式 MCP 工具确认提交单次 action/content,不写入 session/always 授权。
193
+ 任意表单字段和 URL 身份认证尚不支持批准,可明确拒绝或取消。
194
+
195
+ ## 当前边界
196
+
197
+ - 尚未实现接管或 attach 已有 Codex/Claude/dsh TUI、桌面会话;这些会话继续使用原
198
+ transport。
199
+ - Web 可发现并控制同一项目的现有 native worker,提供消息、回执、中断和明确关闭。
200
+ 关闭页面或 Web runtime 保留独立 native worker;支持与当前执行器、turn 绑定的人工应答。
201
+ 使用方式见 [Web 继续本地任务](web-handoff.zh-CN.md#本地-native-worker-接手)。
202
+ - Codex、Claude 和 dsh native worker 自动获得项目与 peer 受限的 HCC MCP;
203
+ 发信等写入仍须满足 provider 权限与 HCC 所有权检查。
204
+ - provider 内置 `SendMessage`、subagent/team discovery、Codex delegation 工具没有
205
+ 被暴露为统一的跨厂商协议;跨 provider 消息仍由 HCC 持久总线和 adapter 路由。
206
+ - 任务领取、文件锁、交接、完成核验仍是显式 HCC 协作操作;一次 prompt 成功不能
207
+ 证明任务已获业务验收。
208
+ - adapter 和 stdio fixture 测试验证协议与交互边界;下方真实模型回执单独证明
209
+ 模型执行和 MCP 通信。已发布客户端安装、真实业务任务验收仍需独立证据。
210
+
211
+
212
+ ## 真实模型验收(2026-10-02)
213
+
214
+ 已在独立临时项目、独立 provider 状态目录中,用现有认证分别完成:
215
+
216
+ - Codex 0.144.6、Claude Agent SDK 0.3.287、DSH 0.2.0-rc.2 的真实消息往返、回复关联、ACK、重复 submission 去重、连续上下文、关闭后恢复,以及活跃 turn 中断。
217
+ - Codex 实际命令审批被拒绝后,权限边界外的测试文件没有创建。
218
+ - 模型通过受限 HCC MCP 发信的 `Codex → Claude → dsh → Codex` 三段链路:接收方模型回复、回信消费及 ACK,不生成自动回复循环。
219
+ - Web bridge 发现真实 native worker、通过原 runtime 发信、保留原 provider binding;关闭 bridge 后 worker 继续运行。Claude 延迟初始化的恢复状态已有回归测试。
220
+
221
+ 会话生命周期回执 (源码目录: `docs/verification/2026-10-02-native-live-lifecycle.json`) 与
222
+ 跨 provider 通信回执 (源码目录: `docs/verification/2026-10-02-native-live-communication.json`)
223
+ 分别记录验证时间、调用数量、源码摘要、真实 session/turn 和清理结果。它们是两个
224
+ 独立快照;协议模拟测试、真实模型回执、发布安装与业务任务验收各有自己的边界。
225
+
226
+ 本地验证汇总 (源码目录: `docs/verification/2026-10-02-native-live-validation.json`)记录全量单测
227
+ (785 项通过、1 项平台条件跳过)、63 个模块静态检查、13 阶段回归与脚本失败路径清理。
228
+ 这些记录保留各次执行的时间与摘要。
229
+
230
+ 可重复脚本为 `scripts/native-live-acceptance.mjs`。默认只显示帮助,不调用模型;
231
+ 显式加 `--run-live` 后才使用现有认证,测试过程会调用模型并消耗相应额度:
232
+
233
+ ```sh
234
+ node scripts/native-live-acceptance.mjs --run-live --provider all \
235
+ --codex-bin /absolute/path/to/codex \
236
+ --dsh-bin /absolute/path/to/dsh \
237
+ --claude-sdk /absolute/path/to/claude-agent-sdk/sdk.mjs \
238
+ --permission-probe --cross-provider \
239
+ --output /absolute/path/to/receipt.json
240
+ ```
241
+
242
+ 仅测试跨 provider 通信可加 `--communication-only`。Claude SDK 路径用于显式加载
243
+ 已安装的独立 SDK;本次真实验收用了此注入路径,没有代替默认安装位置的验收。
244
+ 脚本不安装依赖、不接管现有会话;结束时关闭测试进程并移除测试凭据副本。
245
+ Web 验证覆盖服务与 bridge 控制接口,尚不等于完整浏览器操作或已发布客户端验收。
246
+
247
+
248
+ 真实模型与浏览器人工交互可使用 `scripts/web-native-interaction-acceptance.mjs`。
249
+ 默认只显示帮助;`--run-live --provider codex|claude|all` 才调用模型。
250
+ 设置 `HCC_ACCEPTANCE_PLAYWRIGHT`,以及按需设置 `HCC_ACCEPTANCE_CHROME`。
251
+ `--claude-package DIR` 可将已经安装的 SDK 包链接到隔离 worker 项目,由默认加载器
252
+ 解析,不注入 query 函数。脚本验证明确批准与拒绝、原会话续接,退出后关闭测试
253
+ 进程并清理临时 provider home;使用现有认证和调用额度。它不连接现有用户会话。
@@ -0,0 +1,53 @@
1
+ # Web 继续本地任务
2
+
3
+ 项目仍以 CLI/tmux 作为默认接入。它适合继续已经托管的本地 Codex/Claude/shell:Web 打开同一个 pane,不另起执行器。
4
+
5
+ 运行 `hcc web` 后选择已有会话。选中项和未发送草稿按项目保存于当前浏览器;刷新会恢复原位置。第一个 Web 连接取得控制,其他窗口可观察,点击“接管浏览器控制”后才可写入。服务端检查控制 epoch,旧窗口不能继续输入、改变终端尺寸、修改该 peer 或处理审批。本地 tmux client 仍可直接输入,Web 会显示其连接状态。
6
+
7
+ 终端草稿“发送 + Enter”收到回执,只代表运行时已接受字节。若连接中断或没有回执,草稿保留且不自动重发;先核对终端再决定是否补发。浏览器存储不可用时退回内存,关闭浏览器后未必保留。
8
+
9
+ 关闭页面或“释放控制”不会终止本地执行器。“暂停 Web 接管”使 tmux 保持运行,并保存暂停状态,自动发现和 runtime 重启不会立即重新接入。需要恢复时显式连接已有终端,例如 `hcc peer attach codex-a --pane %1`。暂停不是释放 task owner 或项目锁。选择终止 tmux 的复选框才会同时结束它;PTY 和 App Server 的“终止执行器”会结束对应子进程。
10
+
11
+ ## Codex 结构化界面
12
+
13
+ 新建会话时选择 Codex,再把“Codex 接入方式”切换为“结构化 App Server(显式启用)”。需要本地已安装且支持 App Server 的 `codex`,登录和模型环境来自启动 hello-cc 的进程。hello-cc 不自动登录、不更改信任配置、不探测未公开的 TUI 内部 endpoint。
14
+
15
+ 可先运行 `hcc doctor --codex --json` 查看只读诊断。它分别报告已安装版本、CLI help 声明的 App Server 启动参数、hooks 配置与项目中的 hook 调用记录;这些事实不等于完整 RPC 能力、hooks 已受信任、stdout 投递成功或模型已接收。版本/help 探测使用独立临时 HOME/CODEX_HOME 并清理,防止 Codex 启动文件触碰用户目录;hooks 配置仍从原路径只读检查。没有充分回执的状态显示 `unknown`,不会自动触发 hook 或修改 trust。普通 `hcc doctor` 仍只检查数据库。
16
+
17
+ 结构化界面提供消息、当前 turn 状态、追加指令、轮次中断、计划、差异和人工交互。命令/文件审批以及空表单 MCP 工具确认由当前控制窗口明确批准本次或拒绝。文件与网络权限可逐项勾选,并明确选择本轮或会话有效期;只能授予原请求的子集。问题支持选项、其他/自由输入和敏感答案,需逐题明确填写。接管控制本身不会批准工具。
18
+
19
+ 表单输入与焦点会跨状态刷新保留,直接答案不写入 HCC 状态、事件、回执或浏览器存储。原 turn 完成、中断成功、provider 解决请求或执行器关闭后,请求卡片失效;旧控制窗口不能用过期 epoch 应答。参数被截断时只能拒绝或取消。任意 MCP 表单字段、URL 登录、动态工具及账号 token 刷新尚未接入。
20
+
21
+ 运行中发消息使用 steer;空闲时开始新 turn。中断当前轮与终止执行器是不同操作。turn 完成不自动把 hcc task 标记为验收完成。
22
+
23
+ 要恢复一个旧 Codex thread,先停止旧执行器,选择 resume 并填写真实 thread ID,再勾选交接确认。后端仍会验证旧 owner 状态;活跃或证据不明时拒绝恢复。不能把它当作正在运行的 CLI 任务无缝迁移。原 thread 已绑定 hcc peer 时,保留原 peer/task 身份;正在运行的命令和未持久化事件不会迁移。
24
+
25
+ 提交先持久记录 submission ID。重复 ID、断线和超时不会触发自动重发。若读取历史仍不能明确确认接收结果,界面阻止新 turn;停止旧执行器后再恢复保存的 thread。历史清理保留用于去重的最小提交记录。
26
+
27
+ 关闭浏览器后 App Server 仍由 hello-cc runtime 管理;停止 runtime 会关闭它。新 runtime 不会自动重启 App Server 或重放未确认消息,可以从已保存 thread 显式恢复。
28
+
29
+ ## 本地 native worker 接手
30
+
31
+ 先用 `hcc native start --peer codex-worker --provider codex` 启动后台任务,再打开 Web 的同一项目。Web 自动发现实际仍由 native runtime 持有的 worker,显示其原 peer/session、事件和投递回执;发送、中断和明确关闭都通过原 runtime 执行,不新建 provider 子进程。Claude/dsh worker 也走这条路径,按实际能力显示中断操作。
32
+
33
+ 发送成功先显示队列回执;执行器接收、运行和完成分别使用实际投递状态。断线或不确定结果保留 submission ID,刷新不会重放消息。native runtime 重启或 worker 被替换时,旧 Web 视图及其控制凭证失效,重新发现当前 worker 后才能操作。
34
+
35
+ 关页面或停止 Web runtime 不会停止独立 native runtime/worker。界面的“终止执行器”才会明确关闭该 worker。Codex native worker 提供同一套权限与问题表单,并在会话内显式启用相应的实验工具开关;Claude 通过 SDK 工具 callback 批准原始输入本次执行,dsh ACP 只选择 provider 提供的 allow_once。所有请求均绑定当前执行器/session/turn,不自动批准。
36
+
37
+ 回到本地后,用 `hcc native requests --peer codex-worker` 查看待处理请求,再用 `hcc native respond --peer codex-worker --request REQUEST_ID --decision accept|decline|cancel` 应答。权限或问题答复通过 `--response-file JSON` 提交,格式见 [Native 指南](native.zh-CN.md#会话所有权hooks-和权限)。
38
+
39
+ ## 历史、协作工具与结果审阅
40
+
41
+ 项目栏“Codex 历史”可分页浏览当前项目已保存的 thread,读取上下文,再显式恢复或分叉。恢复仍要求旧执行器已确认停止;分叉生成新的 thread/peer,原任务 owner 不变。有正在托管的源会话时,分叉也必须取得该会话的浏览器控制。历史读取不会启动模型 turn。
42
+
43
+ Web 专用 Codex App Server,以及由 HCC 托管的 Codex/Claude/dsh native worker,自动接入 [项目与 peer 受限的 MCP 协作工具](mcp.zh-CN.md)。配置只传给对应执行器或 worker,不写全局 provider 配置;关闭 worker 前撤销其临时权限。模型可查询任务与 inbox、领取任务、发送消息、写 handoff、获取/释放自己的资源锁和记录本地证据;不替用户执行发布或业务验收。
44
+
45
+ 会话栏“结果审阅”汇总结构化 command 的退出码与输出、turn 差异、模型通过 MCP 提交的本地证据,以及人工添加的验收记录。命令退出成功仅说明该命令成功;人工填写“通过”必须附证据引用。界面分别展示本地验证、发布部署、业务验收,并注明记录来源。turn 完成、测试记录或人工验收均不自动改变 hcc task 状态。记录保存于项目数据库 events 中,`gc --history` 保留结果审计记录和最小提交去重凭据。
46
+
47
+ ## 验收边界
48
+
49
+ 本地验证包含真实 SQLite/HTTP、模拟交互请求、隔离 tmux 与桌面/手机浏览器。安装版 Codex 0.144.6 的协议验收另行覆盖 initialize、thread/start/list、10 个 MCP 工具发现和实际 `hcc_state` 读回;此协议检查不调用模型,可用 `node scripts/web-handoff-installed-acceptance.mjs` 重现。
50
+
51
+ 真实模型验收已在隔离项目和 Codex home 中完成“本地 → Web → 本地”三段文件任务,全程保持原 Codex 进程和 thread;关闭 Web runtime 后仍可继续。可用 `scripts/web-native-model-acceptance.mjs` 在明确需要真实模型调用时重现。三种 provider 的真实生命周期、模型 MCP 发信、回复/ACK 和无回复循环另有独立回执。交互表单另有 11 项模拟 provider 浏览器检查。真实 Codex 已经完成“提问 → Web 答复 → 指定文件权限 → 本轮授权 → 写入所选答案 → 后续请求拒绝 → 本地继续”的验收;默认加载的 Claude SDK 已完成真实 Write 工具批准、拒绝和本地续接。dsh 的真实权限弹窗尚无本轮浏览器验收。完整结果与运行条件见 本地验收记录 (源码目录: `docs/verification/2026-10-02-web-local-task-handoff.md`)。
52
+
53
+ 任意未托管 TUI/普通终端的活动执行器迁移、私有 daemon endpoint、任意协议版本的完整兼容矩阵,以及发布安装和真实业务验收仍在本次证据范围之外。
package/lib/cli/app.mjs CHANGED
@@ -33,6 +33,9 @@ import { createTeamCommands } from '../cli/commands/team.mjs';
33
33
  import { createMiscCommands } from '../cli/commands/misc.mjs';
34
34
  import { createInstallCommands } from '../cli/commands/install.mjs';
35
35
  import { createHookCommand } from '../cli/commands/hook.mjs';
36
+ import { createDshCommands } from '../cli/commands/dsh.mjs';
37
+ import { createNativeCommands } from '../cli/commands/native.mjs';
38
+ import { createMcpCommands } from '../cli/commands/mcp.mjs';
36
39
  import { createPeerCommands } from '../cli/commands/peer.mjs';
37
40
  import { createRunCommands } from '../cli/commands/run.mjs';
38
41
  import { createTmuxCommands } from '../cli/commands/tmux.mjs';
@@ -51,6 +54,7 @@ import { readPackageMeta } from '../package-meta.mjs';
51
54
  import { removeGuidanceBlocks as removeGuidanceBlocksForRoot, writeGuidance as writeGuidanceForRoot } from '../guidance.mjs';
52
55
  import { globalRuntimePath, runtimePath } from '../runtime/paths.mjs';
53
56
  import { readRuntime, reclaimRuntimePointerFiles } from '../runtime/state.mjs';
57
+ import { waitForProcessIdentityExit } from '../process/identity.mjs';
54
58
 
55
59
  import { runtimeRequest } from '../runtime/client.mjs';
56
60
 
@@ -290,6 +294,11 @@ const { cmdPeers, cmdStatus, cmdState, cmdPrompt } = createQueryCommands({
290
294
  clockGraceSuppressed, readClockGraceUntil, commandPath
291
295
  });
292
296
 
297
+ const { cmdNative } = createNativeCommands({ connect, upsertPeer, upsertCanonicalPeerBinding,
298
+ sendMessage, ackMessage, queryInbox, addEvent, liveProcessIdentity, detectBranch,
299
+ resolveCurrentPeer });
300
+ const { cmdMcp } = createMcpCommands({ connect, touchPeer, now, addEvent });
301
+
293
302
  const { cmdInit, cmdRegister, cmdEnv, cmdJoin } = createLifecycleCommands({
294
303
  connect, now, tx, addEvent, printResult, CliError, parseOpts, intOpt,
295
304
  registerProjectActivity, resolveCurrentPeer, resolveTargetPeer,
@@ -310,6 +319,7 @@ const { cmdDown, cmdFindRoot, cmdWhichReal } = createMiscCommands({
310
319
  path, fs, process, CliError, parseOpts, printResult,
311
320
  readRuntime, runtimeRequest, runtimePath, globalRuntimePath,
312
321
  reclaimRuntimePointerFiles, helpDown, loadSetup,
322
+ waitForProcessIdentityExit,
313
323
  PRODUCT_NAME
314
324
  });
315
325
 
@@ -339,6 +349,8 @@ const { cmdHook } = createHookCommand({
339
349
  path, fs, process, CliError
340
350
  });
341
351
 
352
+ const { cmdDsh } = createDshCommands();
353
+
342
354
  const { cmdPeer } = createPeerCommands({
343
355
  connect, now, tx, addEvent, auditPayload, printResult, CliError, parseOpts,
344
356
  detectBranch, buildPeerCommand, childSessionEnv, inferPeerKind,
@@ -469,8 +481,8 @@ const { cmdWeb } = createWebRuntime({
469
481
  sameResolvedPath, sendWebHtml, shellCommand, webErrorStatus
470
482
  });
471
483
 
472
- function renderWebIndex(nonce) {
473
- return webUiTemplate.webIndexHtml({ nonce });
484
+ function renderWebIndex(nonceOrOptions) {
485
+ return webUiTemplate.webIndexHtml(typeof nonceOrOptions === 'string' ? { nonce: nonceOrOptions } : nonceOrOptions);
474
486
  }
475
487
 
476
488
  function renderWebLogin(nonce) {
@@ -482,18 +494,24 @@ function renderWebLogin(nonce) {
482
494
  </body></html>`;
483
495
  }
484
496
 
485
- function sendWebHtml(res, render) {
497
+ function sendWebHtml(res, render, { pane = false } = {}) {
486
498
  const nonce = randomBytes(18).toString('base64url');
487
- sendHttp(res, 200, 'text/html; charset=utf-8', render(nonce), { nonce });
499
+ const options = { nonce, pane: pane === true };
500
+ sendHttp(res, 200, 'text/html; charset=utf-8', render(options.pane ? options : nonce), options);
488
501
  }
489
502
 
490
503
  function webErrorStatus(err) {
491
504
  if (!(err instanceof CliError)) return 500;
492
505
  if (err.code === 'REGISTRY_BUSY') return 503;
506
+ if (err.code === 'CODEX_START_FAILED') return 503;
507
+ if (err.code.startsWith('CODEX_') || ['STALE_CONTROL_EPOCH', 'UNKNOWN_CONTROL_CLIENT'].includes(err.code)) return 409;
493
508
  if (['BAD_ARGS', 'BAD_REQUEST', 'PEER_IDENTITY_REQUIRED', 'REQUEST_TOO_LARGE'].includes(err.code)) return 400;
494
- if (['PEER_IDENTITY_MISMATCH', 'TASK_OWNED', 'LOCK_OWNED', 'PROJECT_NOT_REGISTERED', 'PROJECT_PATH_FORBIDDEN'].includes(err.code)) return 403;
509
+ if (['PEER_IDENTITY_MISMATCH', 'TASK_OWNED', 'TASK_OWNER_MISMATCH', 'LOCK_OWNED', 'PROJECT_NOT_REGISTERED', 'PROJECT_PATH_FORBIDDEN', 'RUNTIME_ADMIN_REQUIRED'].includes(err.code)) return 403;
510
+ if (err.code.startsWith('NATIVE_')) return err.code === 'NATIVE_RUNTIME_OFFLINE' ? 503 : 409;
495
511
  if (['NOT_FOUND'].includes(err.code)) return 404;
496
- if (['LOCK_HELD', 'SESSION_NOT_RUNNING'].includes(err.code)) return 409;
512
+ if (['LOCK_HELD', 'SESSION_NOT_RUNNING', 'CONTROL_REQUIRED', 'CONTROL_STALE',
513
+ 'CONTROL_HELD', 'CONTROL_DISCONNECTED', 'HANDOFF_REQUIRED', 'THREAD_IN_USE',
514
+ 'SUBMISSION_EXISTS', 'STRUCTURED_SESSION_NO_TERMINAL', 'CODEX_UNCERTAIN_SUBMISSION', 'CODEX_TURN_ACTIVE'].includes(err.code)) return 409;
497
515
  return 500;
498
516
  }
499
517
 
@@ -596,6 +614,9 @@ async function dispatch(ctx, rest) {
596
614
  if (command === 'event') return cmdEvent(ctx, args);
597
615
  if (command === 'web') return cmdWeb(ctx, args);
598
616
  if (command === 'hook') return cmdHook(ctx, args);
617
+ if (command === 'dsh') return cmdDsh(ctx, args);
618
+ if (command === 'native') return cmdNative(ctx, args);
619
+ if (command === 'mcp') return cmdMcp(ctx, args);
599
620
  if (command === 'install-hooks') return cmdInstallHooks(ctx, args);
600
621
  if (command === 'shim') return cmdShim(ctx, args);
601
622
  if (command === 'setup') return cmdSetup(ctx, args);
@@ -1,6 +1,8 @@
1
1
  // Coordination CLI commands extracted from bin/hcc.mjs: handoff, event,
2
2
  // heartbeat, ask, broadcast, inject. Factory pattern with injected deps.
3
3
 
4
+ import { RUNTIME_SESSION_MUTATION_TIMEOUT_MS } from '../../runtime/client.mjs';
5
+
4
6
  export function createCoordinationCommands(deps) {
5
7
  const {
6
8
  connect,
@@ -228,7 +230,7 @@ export function createCoordinationCommands(deps) {
228
230
  return runtimeRequest(ctx, 'POST', `/api/sessions/${encodeURIComponent(peer)}/input`, {
229
231
  text,
230
232
  enter
231
- }, runtime);
233
+ }, runtime, { timeoutMs: RUNTIME_SESSION_MUTATION_TIMEOUT_MS });
232
234
  }
233
235
 
234
236
  async function cmdInject(ctx, args) {
@@ -2,15 +2,20 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import { printResult } from '../../format.mjs';
5
+ import { diagnoseCodex, unknownCodexDiagnostics } from '../../diagnostics/codex.mjs';
5
6
 
6
- export function createDoctorCommand({ connectReadOnly, readSchemaVersion, DB_SCHEMA_VERSION, CLI_NAME }) {
7
+ export function createDoctorCommand({ connectReadOnly, readSchemaVersion, DB_SCHEMA_VERSION, CLI_NAME,
8
+ codexDiagnostics = diagnoseCodex }) {
7
9
  async function cmdDoctor(ctx, args) {
8
10
  if (args[0] === '--help' || args[0] === '-h') {
9
- console.log(`${CLI_NAME} doctor [--json]
11
+ console.log(`${CLI_NAME} doctor [--codex] [--json]
10
12
 
11
13
  Runs a read-only health check on the project database: PRAGMA integrity_check,
12
14
  schema compatibility, persistent journal mode, DB and WAL file sizes, and
13
15
  per-table row counts. Exits non-zero for corruption or an unsupported schema.
16
+ With --codex, also reads bounded version/help output, hooks configuration and
17
+ project hook invocation evidence. Configuration is not trust or a delivery
18
+ receipt. No model, session, hook, account or trust change is performed.
14
19
  `);
15
20
  return;
16
21
  }
@@ -52,6 +57,11 @@ per-table row counts. Exits non-zero for corruption or an unsupported schema.
52
57
  },
53
58
  row_counts: counts
54
59
  };
60
+ const delimiter = args.indexOf('--');
61
+ if ((delimiter < 0 ? args : args.slice(0, delimiter)).includes('--codex')) {
62
+ try { report.codex = codexDiagnostics(ctx, db); }
63
+ catch { report.codex = unknownCodexDiagnostics(); }
64
+ }
55
65
  } finally {
56
66
  db.close();
57
67
  }
@@ -72,6 +82,11 @@ per-table row counts. Exits non-zero for corruption or an unsupported schema.
72
82
  ` db size: ${r.db_size_bytes ?? '?'} bytes${r.wal_size_bytes ? ` (wal ${r.wal_size_bytes})` : ''}`,
73
83
  ` rows: ` + Object.entries(r.row_counts).map(([k, v]) => `${k}=${v}`).join(' ')
74
84
  ];
85
+ if (r.codex) {
86
+ lines.push(` codex version: ${r.codex.version.value ?? 'unknown'} (${r.codex.version.status})`,
87
+ ` app-server: ${r.codex.app_server.status} startup: ${r.codex.app_server.startup_arguments?.join(' ') || 'unknown'} handshake: unknown`,
88
+ ` codex hooks: configured=${r.codex.hooks.configuration.status} invocation=${r.codex.hooks.invocation.status} stdout receipt=unknown provider acceptance=unknown trust=unknown`);
89
+ }
75
90
  return lines.join('\n');
76
91
  });
77
92
  if (!healthy) process.exitCode = 1;
@@ -0,0 +1,79 @@
1
+ import process from 'node:process';
2
+ import { CliError } from '../../shared/errors.mjs';
3
+ import { parseOpts, validateOpts, wantsHelp } from '../../cli-args.mjs';
4
+ import { commandPath } from '../../cli-runtime.mjs';
5
+ import { printResult } from '../../format.mjs';
6
+ import {
7
+ DSH_INSTALL_HINT, ensureDshIntegration, inspectDshIntegration,
8
+ resolveDshBinary, launchDshWeb
9
+ } from '../../integrations/dsh.mjs';
10
+
11
+ function defaultHelp() {
12
+ console.log([
13
+ 'Usage: hcc dsh setup [--mode hooks|cordis|off]',
14
+ ' hcc dsh status [--dsh-bin PATH]',
15
+ ' hcc dsh web [--mode hooks|cordis|off] [--dsh-bin PATH] [--dsh-home PATH] -- [dsh web arguments]',
16
+ '',
17
+ 'Setup writes project-local configuration under .hello-cc/dsh/. Modes: hooks (default), cordis (native tools), off.',
18
+ 'Web starts the official dsh web runtime; its real sessions join hello-cc through the selected integration.',
19
+ 'Pass runtime arguments after --, for example: hcc dsh web -- --port 8080',
20
+ 'Show dsh web help: hcc dsh web -- --help',
21
+ `Install the tested dsh version: ${DSH_INSTALL_HINT}`
22
+ ].join('\n'));
23
+ }
24
+
25
+ export function createDshCommands(deps = {}) {
26
+ const getCommandPath = deps.commandPath || commandPath;
27
+ const help = deps.helpDsh || defaultHelp;
28
+
29
+ async function cmdDsh(ctx, args) {
30
+ const helpBoundary = args.indexOf('--');
31
+ const wrapperArgs = helpBoundary < 0 ? args : args.slice(0, helpBoundary);
32
+ if (!args.length || wantsHelp(wrapperArgs)) return help();
33
+ const sub = args[0];
34
+ if (!['setup', 'status', 'web'].includes(sub)) throw new CliError('BAD_ARGS', `Unknown dsh subcommand: ${sub}`);
35
+ const tail = args.slice(1);
36
+ const boundary = tail.indexOf('--');
37
+ const forwarded = boundary < 0 ? [] : tail.slice(boundary + 1);
38
+ const opts = parseOpts(boundary < 0 ? tail : tail.slice(0, boundary));
39
+ const allowed = sub === 'setup' ? ['mode'] : sub === 'status' ? ['dsh-bin'] : ['mode', 'dsh-bin', 'dsh-home'];
40
+ validateOpts(`dsh ${sub}`, opts, allowed);
41
+ if (sub !== 'web' && boundary >= 0) throw new CliError('BAD_ARGS', `dsh ${sub}: unexpected -- argument separator`);
42
+ if (opts.mode !== undefined && !['hooks', 'cordis', 'off'].includes(opts.mode)) throw new CliError('BAD_ARGS', '--mode must be hooks, cordis, or off');
43
+ for (const name of allowed.filter(name => name !== 'mode')) {
44
+ if (opts[name] !== undefined && !opts[name]) throw new CliError('BAD_ARGS', `--${name} must be a non-empty path`);
45
+ }
46
+ const config = { hccBin: getCommandPath(), ...(opts.mode !== undefined ? { mode: opts.mode } : {}) };
47
+ if (sub === 'setup') {
48
+ const result = ensureDshIntegration(ctx, config);
49
+ const label = result.changed.length ? 'configured' : 'already current';
50
+ printResult(ctx, result, (data) => [
51
+ `dsh integration ${label}: ${data.root}`,
52
+ `hooks: ${data.hooksPath}`,
53
+ `patch: ${data.patchPath}`,
54
+ `mode: ${data.mode}`,
55
+ 'start: hcc dsh web'
56
+ ].join('\n'));
57
+ return;
58
+ }
59
+ if (sub === 'status') {
60
+ const result = inspectDshIntegration(ctx, config);
61
+ const binary = resolveDshBinary({ dshBin: opts['dsh-bin'], cwd: ctx.cwd });
62
+ const data = { ...result, binary, installed: Boolean(binary), installHint: binary ? null : DSH_INSTALL_HINT };
63
+ const binaryLabel = binary || `missing (install: ${DSH_INSTALL_HINT})`;
64
+ printResult(ctx, data, () => [
65
+ `dsh integration: ${result.state}`,
66
+ `mode: ${result.mode || 'unknown'}`,
67
+ `binary: ${binaryLabel}`,
68
+ result.error?.message,
69
+ !result.ready && result.state !== 'conflict' ? 'configure: hcc dsh setup' : null
70
+ ].filter(Boolean).join('\n'));
71
+ return;
72
+ }
73
+ const result = await launchDshWeb(ctx, { ...config, dshBin: opts['dsh-bin'], dshHome: opts['dsh-home'], args: forwarded });
74
+ if (result.signal) process.kill(process.pid, result.signal);
75
+ else process.exitCode = result.code ?? 1;
76
+ }
77
+
78
+ return { cmdDsh };
79
+ }