helloloop 0.8.5 → 0.9.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "description": "HelloLoop 的 Claude Code 原生插件元数据,用于多 CLI 宿主分发。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件,Codex 路径为首发与参考实现。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
package/README.md CHANGED
@@ -99,8 +99,10 @@ npx helloloop gemini <PATH> 接续完成剩余开发
99
99
 
100
100
  ## 后台监督执行
101
101
 
102
- 从 `Codex CLI`、`Claude Code`、`Gemini CLI` 这些宿主里发起 `HelloLoop` 时,确认自动执行后会默认切到 **detached supervisor + host lease** 模式:
102
+ 从当前版本开始,`HelloLoop` 的自动执行主线统一走 **detached supervisor + host lease** 模式:
103
103
 
104
+ - `analyze` 确认后的自动执行、`run-once`、`run-loop` 都默认切到后台 supervisor
105
+ - 交互终端里默认会**自动附着观察**这个后台 supervisor:你仍然能在当前 CLI 里看到实时进度与流式输出
104
106
  - 当前对话 turn 就算被误按 `Esc`、被宿主暂停、或当前工具调用被中断,后台 supervisor 仍会继续
105
107
  - 原有的 15 分钟级恢复、健康探测、同引擎自动恢复,也会继续由这个后台 supervisor 接管
106
108
  - 这不是“当前进程死掉后每 15 分钟重新拉起一遍主进程”,而是 supervisor 本身持续存活,所以恢复链不会因为当前 turn 消失而断掉
@@ -108,8 +110,11 @@ npx helloloop gemini <PATH> 接续完成剩余开发
108
110
 
109
111
  常见场景:
110
112
 
111
- - 在 `Codex` / `Claude` / `Gemini` 宿主里运行 `helloloop`:确认后默认转入后台执行,可用 `helloloop status` 查看进度
112
- - 在普通终端里运行 `npx helloloop run-once --supervised` 或 `npx helloloop run-loop --supervised`:即使中途 `Ctrl+C` 当前命令,只要终端窗口没关,后台 supervisor 仍会继续
113
+ - 在 `Codex` / `Claude` / `Gemini` 宿主里运行 `helloloop`:确认后默认后台执行,并尽量保持当前 CLI 可观察
114
+ - 在普通终端里运行 `npx helloloop`、`npx helloloop run-once`、`npx helloloop run-loop`:默认也是“后台执行 + 当前终端附着观察”
115
+ - 如果你就是想让命令立刻返回、不占当前终端:显式加 `--detach`
116
+ - 如果你稍后想重新接上实时观察:运行 `helloloop watch` 或 `helloloop status --watch`
117
+ - 因此当前版本**不需要先上 Web 看板** 才能解决“后台执行看不到过程”的问题;CLI 观察链已经是第一优先级
113
118
 
114
119
  ## 无人值守恢复
115
120
 
@@ -175,6 +180,46 @@ npx helloloop gemini <PATH> 接续完成剩余开发
175
180
  - 建议把 SMTP 密码放在环境变量里,不要明文写进配置文件
176
181
  - 邮件只在“本轮不再继续自动重试”时发送,不会每次失败都刷屏
177
182
 
183
+ ## 终端并发上限
184
+
185
+ `HelloLoop` 不会主动再额外弹出一堆可见终端窗口;这里的:
186
+
187
+ - **显示终端** 指当前前台运行中的 `helloloop` 会话
188
+ - **背景终端** 指 detached supervisor 后台会话
189
+
190
+ 默认限制为:
191
+
192
+ - 显示终端最多 `8`
193
+ - 背景终端最多 `8`
194
+ - 显示 + 背景合计最多 `8`
195
+
196
+ 可在:
197
+
198
+ ```text
199
+ ~/.helloloop/settings.json
200
+ ```
201
+
202
+ 中自行调整,例如:
203
+
204
+ ```json
205
+ {
206
+ "runtime": {
207
+ "terminalConcurrency": {
208
+ "enabled": true,
209
+ "visibleMax": 8,
210
+ "backgroundMax": 8,
211
+ "totalMax": 8
212
+ }
213
+ }
214
+ }
215
+ ```
216
+
217
+ 说明:
218
+
219
+ - 只要合计并发达到 `totalMax`,新前台会话或新的后台 supervisor 都不会再继续启动
220
+ - 如果只是想临时完全关闭这个保护,可把 `enabled` 设为 `false`
221
+ - 这个限制主要用于避免同时占用过多本机终端资源,或因为并发过高触发模型 / API 限速
222
+
178
223
  ## 自动发现与交互逻辑
179
224
 
180
225
  ### 1. 只输入 `npx helloloop`
@@ -332,7 +377,7 @@ npx helloloop install --host all --force
332
377
  - `Codex` 会刷新 home 根下的插件源码目录、已安装缓存、`config.toml` 启用项和 marketplace 条目
333
378
  - `Claude` 会刷新 marketplace、缓存插件目录,以及 `settings.json` / `known_marketplaces.json` / `installed_plugins.json` 中的 `helloloop` 条目
334
379
  - `Gemini` 会刷新 `extensions/helloloop/`,不会动同目录下其他扩展
335
- - 安装 / 升级 / 重装时,会同步校准 `~/.helloloop/settings.json` 的当前版本结构:补齐缺失项、清理未知项、保留已知项现有值
380
+ - 安装 / 升级 / 重装时,会把 `~/.helloloop/settings.json` 严格收敛到当前版本 schema:补齐缺失项、清理未知项、把非法值重置为当前版本默认值
336
381
  - 如果 `~/.helloloop/settings.json` 被确认不是合法 JSON,会先备份原文件,再按当前版本结构重建
337
382
  - 如果只是首次读取时出现瞬时异常,但重读后内容合法,则不会误生成备份文件
338
383
  - 如果宿主自己的配置 JSON(如 `Codex marketplace.json`、`Claude settings.json`、`known_marketplaces.json`、`installed_plugins.json`)本身已损坏,`HelloLoop` 会先明确报错并停止,不会先清理现有安装再失败
@@ -401,6 +446,7 @@ npx helloloop
401
446
  | `doctor` | 检查宿主环境、插件资产与目标仓库状态 |
402
447
  | `init` | 手动初始化 `.helloloop/` 模板 |
403
448
  | `status` | 查看 backlog 摘要和当前状态 |
449
+ | `watch` | 重新附着后台 supervisor,持续查看实时进度 |
404
450
  | `next` | 生成下一任务的干跑预览 |
405
451
  | `run-once` | 执行一个任务 |
406
452
  | `run-loop` | 连续执行多个任务 |
@@ -414,7 +460,8 @@ npx helloloop
414
460
  | `-y` / `--yes` | 跳过执行确认直接开始;但如果未显式指定引擎,会直接报错而不是自动选引擎 |
415
461
  | `--repo <dir>` | 高级覆盖:显式指定项目仓库 |
416
462
  | `--docs <dir|file>` | 高级覆盖:显式指定开发文档 |
417
- | `--supervised` | 在普通终端里显式启用 detached supervisor;当前命令被打断时,后台执行仍可继续 |
463
+ | `--watch` | 启动后台 supervisor 后,当前终端继续附着观察实时输出 |
464
+ | `--detach` | 仅启动后台 supervisor,立即返回,不进入观察模式 |
418
465
  | `--rebuild-existing` | 项目与文档冲突时,自动清理现有项目后重建 |
419
466
  | `--host <name>` | 安装宿主:`codex` / `claude` / `gemini` / `all` |
420
467
  | `--config-dir <dir>` | 状态目录名,默认 `.helloloop` |
@@ -423,8 +470,11 @@ npx helloloop
423
470
 
424
471
  ```bash
425
472
  npx helloloop status
473
+ npx helloloop status --watch
474
+ npx helloloop watch
426
475
  npx helloloop next
427
476
  npx helloloop run-once
477
+ npx helloloop run-loop --detach
428
478
  ```
429
479
 
430
480
  ## Doctor
@@ -485,7 +535,7 @@ git push origin vX.Y.Z-beta.N
485
535
 
486
536
  - 正式版本使用 npm `latest` 渠道,beta 版本使用 npm `beta` 渠道
487
537
  - 如果测试、版本校验或打包检查失败,npm 发布与 GitHub Release 都不会继续执行
488
- - `0.8.5` 起已补齐 host-aware supervisor 后台执行链路:宿主内自动执行默认后台化,当前 turn `Esc` / cancel 不再直接打断 HelloLoop 主线
538
+ - `0.9.1` 起按破坏性升级思路继续收紧:后台 supervisor + CLI 附着观察成为默认工作流,同时 `settings.json` Codex 安装布局只接受当前版本规范,不再为旧布局做兼容兜底
489
539
  - GitHub Release 阶段现已改为使用官方 `gh` CLI + `generate-notes` API 创建 / 更新 release,不再依赖会触发 Node runtime deprecation warning 的第三方 action
490
540
 
491
541
  ## 宿主写入范围
@@ -525,7 +575,7 @@ git push origin vX.Y.Z-beta.N
525
575
  说明:
526
576
 
527
577
  - 这里不保存项目 backlog、状态、运行记录
528
- - 安装 / 升级 / 重装时,会对 `settings.json` 做结构校准,但不会校验或篡改你已存在的已知项内容
578
+ - 安装 / 升级 / 重装时,会对 `settings.json` 做严格 schema 校准;已知字段只要值不合法,也会按当前版本默认值重置
529
579
  - 只有在 `settings.json` 被确认非法时,才会先备份,再重建为当前版本结构
530
580
  - 如果只是读取瞬时异常、重读后合法,不会误生成 `.bak`
531
581
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "description": "HelloLoop 的 Claude Code 原生插件元数据,用于多 CLI 宿主分发。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "description": "HelloLoop 的 Gemini CLI 原生扩展,用于按开发文档接续推进项目开发。",
5
5
  "contextFileName": "GEMINI.md",
6
6
  "excludeTools": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件",
5
5
  "author": "HelloLoop",
6
6
  "license": "Apache-2.0",
@@ -30,7 +30,7 @@
30
30
  "templates"
31
31
  ],
32
32
  "scripts": {
33
- "test": "node --test tests/analyze_cli.test.mjs tests/analyze_intent_cli.test.mjs tests/analyze_runtime_failure.test.mjs tests/output_schema_contract.test.mjs tests/engine_process_support.test.mjs tests/engine_selection_cli.test.mjs tests/cli_surface.test.mjs tests/cli_doctor_surface.test.mjs tests/host_lifecycle_integrity.test.mjs tests/host_single_host_integrity.test.mjs tests/install_script.test.mjs tests/mainline_continuation.test.mjs tests/multi_host_runtime.test.mjs tests/process_shell.test.mjs tests/prompt_guardrails.test.mjs tests/ralph_loop.test.mjs tests/runtime_recovery.test.mjs tests/plugin_bundle.test.mjs tests/supervisor_runtime.test.mjs"
33
+ "test": "node --test tests/analyze_cli.test.mjs tests/analyze_cli_path_resolution.test.mjs tests/analyze_intent_cli.test.mjs tests/analyze_runtime_failure.test.mjs tests/output_schema_contract.test.mjs tests/engine_process_support.test.mjs tests/engine_selection_cli.test.mjs tests/engine_selection_runtime.test.mjs tests/cli_surface.test.mjs tests/cli_doctor_surface.test.mjs tests/host_lifecycle_integrity.test.mjs tests/user_settings_lifecycle.test.mjs tests/host_single_host_integrity.test.mjs tests/install_script.test.mjs tests/mainline_continuation.test.mjs tests/multi_host_runtime.test.mjs tests/multi_host_runtime_recovery.test.mjs tests/process_shell.test.mjs tests/prompt_guardrails.test.mjs tests/ralph_loop.test.mjs tests/runtime_recovery.test.mjs tests/plugin_bundle.test.mjs tests/supervisor_runtime.test.mjs tests/supervisor_watch.test.mjs tests/terminal_session_limits.test.mjs"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=20"
package/src/cli.mjs CHANGED
@@ -10,53 +10,71 @@ import {
10
10
  handleRunOnceCommand,
11
11
  handleStatusCommand,
12
12
  handleUninstallCommand,
13
+ handleWatchCommand,
13
14
  } from "./cli_command_handlers.mjs";
14
15
  import { resolveContextFromOptions, resolveStandardCommandOptions } from "./cli_context.mjs";
15
16
  import { runDoctor } from "./cli_support.mjs";
16
17
  import { runSupervisedCommandFromSessionFile } from "./supervisor_runtime.mjs";
18
+ import {
19
+ acquireVisibleTerminalSession,
20
+ releaseCurrentTerminalSession,
21
+ shouldTrackVisibleTerminalCommand,
22
+ } from "./terminal_session_limits.mjs";
17
23
 
18
24
  export async function runCli(argv) {
19
- const parsed = parseArgs(argv);
20
- const command = parsed.command;
21
- if (command === "help" || command === "--help" || command === "-h") {
22
- printHelp();
23
- return;
24
- }
25
- if (command === "__supervise") {
26
- if (!parsed.options.sessionFile) {
27
- throw new Error("缺少 --session-file,无法启动 HelloLoop supervisor。");
25
+ try {
26
+ const parsed = parseArgs(argv);
27
+ const command = parsed.command;
28
+ if (command === "help" || command === "--help" || command === "-h") {
29
+ printHelp();
30
+ return;
31
+ }
32
+ if (command === "__supervise") {
33
+ if (!parsed.options.sessionFile) {
34
+ throw new Error("缺少 --session-file,无法启动 HelloLoop supervisor。");
35
+ }
36
+ await runSupervisedCommandFromSessionFile(parsed.options.sessionFile);
37
+ return;
28
38
  }
29
- await runSupervisedCommandFromSessionFile(parsed.options.sessionFile);
30
- return;
31
- }
32
39
 
33
- if (command === "analyze") {
34
- process.exitCode = await handleAnalyzeCommand(normalizeAnalyzeOptions(parsed.options, process.cwd()));
35
- return;
36
- }
40
+ if (process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1" && shouldTrackVisibleTerminalCommand(command)) {
41
+ acquireVisibleTerminalSession({
42
+ command,
43
+ repoRoot: process.cwd(),
44
+ });
45
+ }
37
46
 
38
- const options = resolveStandardCommandOptions(parsed.options);
39
- if (command === "install") {
40
- process.exitCode = handleInstallCommand(options);
41
- return;
42
- }
43
- if (command === "uninstall") {
44
- process.exitCode = handleUninstallCommand(options);
45
- return;
46
- }
47
+ if (command === "analyze") {
48
+ process.exitCode = await handleAnalyzeCommand(normalizeAnalyzeOptions(parsed.options, process.cwd()));
49
+ return;
50
+ }
47
51
 
48
- const context = resolveContextFromOptions(options);
49
- const handlers = {
50
- doctor: () => handleDoctorCommand(context, options, runDoctor),
51
- init: () => handleInitCommand(context),
52
- next: () => handleNextCommand(context, options),
53
- "run-loop": () => handleRunLoopCommand(context, options),
54
- "run-once": () => handleRunOnceCommand(context, options),
55
- status: () => handleStatusCommand(context, options),
56
- };
57
- if (!handlers[command]) {
58
- throw new Error(`未知命令:${command}`);
59
- }
52
+ const options = resolveStandardCommandOptions(parsed.options);
53
+ if (command === "install") {
54
+ process.exitCode = handleInstallCommand(options);
55
+ return;
56
+ }
57
+ if (command === "uninstall") {
58
+ process.exitCode = handleUninstallCommand(options);
59
+ return;
60
+ }
60
61
 
61
- process.exitCode = await handlers[command]();
62
+ const context = resolveContextFromOptions(options);
63
+ const handlers = {
64
+ doctor: () => handleDoctorCommand(context, options, runDoctor),
65
+ init: () => handleInitCommand(context),
66
+ next: () => handleNextCommand(context, options),
67
+ "run-loop": () => handleRunLoopCommand(context, options),
68
+ "run-once": () => handleRunOnceCommand(context, options),
69
+ status: () => handleStatusCommand(context, options),
70
+ watch: () => handleWatchCommand(context, options),
71
+ };
72
+ if (!handlers[command]) {
73
+ throw new Error(`未知命令:${command}`);
74
+ }
75
+
76
+ process.exitCode = await handlers[command]();
77
+ } finally {
78
+ releaseCurrentTerminalSession();
79
+ }
62
80
  }
@@ -4,7 +4,6 @@ import {
4
4
  confirmAutoExecution,
5
5
  confirmRepoConflictResolution,
6
6
  renderAnalyzeStopMessage,
7
- renderAutoRunSummary,
8
7
  renderRepoConflictStopMessage,
9
8
  } from "./cli_support.mjs";
10
9
  import { analyzeWorkspace } from "./analyzer.mjs";
@@ -12,18 +11,14 @@ import {
12
11
  hasBlockingInputIssues,
13
12
  renderBlockingInputIssueMessage,
14
13
  } from "./analyze_user_input.mjs";
15
- import { loadBacklog, loadPolicy } from "./config.mjs";
14
+ import { loadPolicy } from "./config.mjs";
16
15
  import { createContext } from "./context.mjs";
17
16
  import { createDiscoveryPromptSession, resolveDiscoveryFailureInteractively } from "./discovery_prompt.mjs";
18
- import { resolveEngineSelection, resolveHostContext } from "./engine_selection.mjs";
17
+ import { resolveEngineSelection } from "./engine_selection.mjs";
19
18
  import { resetRepoForRebuild } from "./rebuild.mjs";
20
19
  import { renderRebuildSummary } from "./cli_render.mjs";
21
20
  import { shouldConfirmRepoRebuild } from "./cli_support.mjs";
22
- import {
23
- launchSupervisedCommand,
24
- renderSupervisorLaunchSummary,
25
- waitForSupervisedResult,
26
- } from "./supervisor_runtime.mjs";
21
+ import { launchAndMaybeWatchSupervisedCommand } from "./supervisor_cli_support.mjs";
27
22
 
28
23
  async function resolveAnalyzeEngineSelection(options) {
29
24
  if (options.engineResolution?.ok) {
@@ -206,11 +201,6 @@ async function prepareAnalyzeExecution(initialOptions) {
206
201
  }
207
202
  }
208
203
 
209
- function shouldDetachSupervisor(options = {}) {
210
- return process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1"
211
- && resolveHostContext(options) !== "terminal";
212
- }
213
-
214
204
  async function maybeRunAutoExecution(result, activeOptions) {
215
205
  const execution = analyzeExecution(result.backlog, activeOptions);
216
206
 
@@ -231,27 +221,13 @@ async function maybeRunAutoExecution(result, activeOptions) {
231
221
 
232
222
  console.log("");
233
223
  console.log("开始自动接续执行...");
234
- const session = launchSupervisedCommand(result.context, "run-loop", {
224
+ const payload = await launchAndMaybeWatchSupervisedCommand(result.context, "run-loop", {
235
225
  ...activeOptions,
236
- supervised: false,
237
226
  engineResolution: result.engineResolution?.ok ? result.engineResolution : activeOptions.engineResolution,
238
227
  maxTasks: resolveAutoRunMaxTasks(result.backlog, activeOptions) || undefined,
239
228
  fullAutoMainline: true,
240
229
  });
241
- console.log(renderSupervisorLaunchSummary(session));
242
- if (shouldDetachSupervisor(activeOptions)) {
243
- console.log("- 已切换为后台执行;可稍后运行 `helloloop status` 查看进度。");
244
- return 0;
245
- }
246
- const supervisorPayload = await waitForSupervisedResult(result.context, session);
247
- const results = supervisorPayload.results;
248
- if (!results) {
249
- console.error(supervisorPayload.error || "HelloLoop supervisor 执行失败。");
250
- return supervisorPayload.exitCode || 1;
251
- }
252
- const refreshedBacklog = loadBacklog(result.context);
253
- console.log(renderAutoRunSummary(result.context, refreshedBacklog, results, activeOptions));
254
- return supervisorPayload.exitCode || 0;
230
+ return payload.exitCode || 0;
255
231
  }
256
232
 
257
233
  export async function handleAnalyzeCommand(options) {
package/src/cli_args.mjs CHANGED
@@ -7,6 +7,7 @@ const KNOWN_COMMANDS = new Set([
7
7
  "uninstall",
8
8
  "init",
9
9
  "status",
10
+ "watch",
10
11
  "next",
11
12
  "run-once",
12
13
  "run-loop",
@@ -51,7 +52,10 @@ export function parseArgs(argv) {
51
52
  else if (arg === "--gemini-home") { options.geminiHome = rest[index + 1]; index += 1; }
52
53
  else if (arg === "--config-dir") { options.configDirName = rest[index + 1]; index += 1; }
53
54
  else if (arg === "--session-file") { options.sessionFile = rest[index + 1]; index += 1; }
54
- else if (arg === "--supervised") options.supervised = true;
55
+ else if (arg === "--watch") options.watch = true;
56
+ else if (arg === "--detach") options.detach = true;
57
+ else if (arg === "--session-id") { options.sessionId = rest[index + 1]; index += 1; }
58
+ else if (arg === "--watch-poll-ms") { options.watchPollMs = Number(rest[index + 1]); index += 1; }
55
59
  else if (arg === "--required-doc") { options.requiredDocs.push(rest[index + 1]); index += 1; }
56
60
  else if (arg === "--constraint") { options.constraints.push(rest[index + 1]); index += 1; }
57
61
  else { options.positionalArgs.push(arg); }
@@ -70,6 +74,7 @@ function helpText() {
70
74
  " uninstall 从所选宿主卸载插件并清理注册信息",
71
75
  " init 初始化 .helloloop 配置",
72
76
  " status 查看 backlog 与下一任务",
77
+ " watch 附着到后台 supervisor,持续查看实时进度",
73
78
  " next 生成下一任务干跑预览",
74
79
  " run-once 执行一个任务",
75
80
  " run-loop 连续执行多个任务",
@@ -90,7 +95,9 @@ function helpText() {
90
95
  " --max-tasks <n> run-loop 最多执行 n 个任务",
91
96
  " --max-attempts <n> 每种策略内最多重试 n 次",
92
97
  " --max-strategies <n> 单任务最多切换 n 种策略继续重试",
93
- " --supervised 通过独立 supervisor 执行;当前 turn 被中断时任务仍可继续",
98
+ " --watch 启动后台 supervisor 后,当前终端继续附着观察实时输出",
99
+ " --detach 仅启动后台 supervisor 后立即返回,不进入观察模式",
100
+ " --session-id <id> status/watch 指定附着的后台会话 ID",
94
101
  " --allow-high-risk 允许执行 medium/high/critical 风险任务",
95
102
  " --rebuild-existing 分析判断当前项目与文档冲突时,自动清理当前项目后按文档重建",
96
103
  " --required-doc <p> 增加一个全局必读文档(AGENTS.md 会被自动忽略)",
@@ -99,7 +106,9 @@ function helpText() {
99
106
  "补充说明:",
100
107
  " analyze 默认支持在命令后混合传入引擎、路径和自然语言要求。",
101
108
  " 如果同时检测到多个可用引擎且没有明确指定,会先询问你选择。",
102
- " Codex / Claude / Gemini 宿主内,确认后的自动执行会默认切到后台 supervisor。",
109
+ " 当前版本默认会把自动执行 / run-once / run-loop 切到后台 supervisor。",
110
+ " 交互终端默认会自动附着观察;如只想立即返回,请显式加 --detach。",
111
+ " 任何时候都可运行 `helloloop watch` 或 `helloloop status --watch` 重新附着观察。",
103
112
  " 示例:npx helloloop claude <DOCS_PATH> <PROJECT_ROOT> 先分析偏差,不要执行",
104
113
  ].join("\n");
105
114
  }
@@ -2,41 +2,15 @@ import path from "node:path";
2
2
 
3
3
  import { createContext } from "./context.mjs";
4
4
  import { loadBacklog, scaffoldIfMissing } from "./config.mjs";
5
- import { resolveHostContext } from "./engine_selection.mjs";
6
5
  import { syncUserSettingsFile } from "./engine_selection_settings.mjs";
7
6
  import { installPluginBundle, uninstallPluginBundle } from "./install.mjs";
8
7
  import { runLoop, runOnce, renderStatusText } from "./runner.mjs";
9
8
  import { renderInstallSummary, renderUninstallSummary } from "./cli_render.mjs";
10
9
  import {
11
- launchSupervisedCommand,
12
- renderSupervisorLaunchSummary,
13
- waitForSupervisedResult,
14
- } from "./supervisor_runtime.mjs";
15
-
16
- function shouldUseSupervisor(options = {}) {
17
- return !options.dryRun
18
- && process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1"
19
- && (Boolean(options.supervised) || resolveHostContext(options) !== "terminal");
20
- }
21
-
22
- function shouldDetachSupervisor(options = {}) {
23
- return resolveHostContext(options) !== "terminal";
24
- }
25
-
26
- async function runSupervised(context, command, options = {}) {
27
- const session = launchSupervisedCommand(context, command, options);
28
- console.log(renderSupervisorLaunchSummary(session));
29
- if (shouldDetachSupervisor(options)) {
30
- console.log("- 已切换为后台执行;可稍后运行 `helloloop status` 查看进度。");
31
- return {
32
- detached: true,
33
- exitCode: 0,
34
- ok: true,
35
- session,
36
- };
37
- }
38
- return waitForSupervisedResult(context, session);
39
- }
10
+ launchAndMaybeWatchSupervisedCommand,
11
+ shouldUseSupervisor,
12
+ } from "./supervisor_cli_support.mjs";
13
+ import { watchSupervisorSession } from "./supervisor_watch.mjs";
40
14
 
41
15
  export function handleInstallCommand(options) {
42
16
  const userSettings = syncUserSettingsFile({
@@ -89,9 +63,33 @@ export async function handleDoctorCommand(context, options, runDoctor) {
89
63
  return 0;
90
64
  }
91
65
 
92
- export function handleStatusCommand(context, options) {
66
+ export async function handleStatusCommand(context, options) {
93
67
  console.log(renderStatusText(context, options));
94
- return 0;
68
+ if (!options.watch) {
69
+ return 0;
70
+ }
71
+
72
+ const result = await watchSupervisorSession(context, {
73
+ sessionId: options.sessionId,
74
+ pollMs: options.watchPollMs,
75
+ });
76
+ if (result.empty) {
77
+ console.log("当前没有正在运行的后台 supervisor。");
78
+ return 1;
79
+ }
80
+ return result.exitCode || 0;
81
+ }
82
+
83
+ export async function handleWatchCommand(context, options) {
84
+ const result = await watchSupervisorSession(context, {
85
+ sessionId: options.sessionId,
86
+ pollMs: options.watchPollMs,
87
+ });
88
+ if (result.empty) {
89
+ console.log("当前没有正在运行的后台 supervisor。");
90
+ return 1;
91
+ }
92
+ return result.exitCode || 0;
95
93
  }
96
94
 
97
95
  export async function handleNextCommand(context, options) {
@@ -119,25 +117,8 @@ export async function handleNextCommand(context, options) {
119
117
 
120
118
  export async function handleRunOnceCommand(context, options) {
121
119
  if (shouldUseSupervisor(options)) {
122
- const payload = await runSupervised(context, "run-once", options);
123
- if (payload.detached) {
124
- return payload.exitCode || 0;
125
- }
126
- if (!payload.result) {
127
- console.error(payload.error || "HelloLoop supervisor 执行失败。");
128
- return payload.exitCode || 1;
129
- }
130
-
131
- const result = payload.result;
132
- if (!result.ok) {
133
- console.error(result.summary || "执行失败。");
134
- return payload.exitCode || 1;
135
- }
136
-
137
- console.log(result.task
138
- ? `完成任务:${result.task.title}\n运行目录:${result.runDir}`
139
- : "没有可执行任务。");
140
- return 0;
120
+ const payload = await launchAndMaybeWatchSupervisedCommand(context, "run-once", options);
121
+ return payload.exitCode || 0;
141
122
  }
142
123
 
143
124
  const result = await runOnce(context, options);
@@ -160,28 +141,8 @@ export async function handleRunOnceCommand(context, options) {
160
141
 
161
142
  export async function handleRunLoopCommand(context, options) {
162
143
  if (shouldUseSupervisor(options)) {
163
- const payload = await runSupervised(context, "run-loop", options);
164
- if (payload.detached) {
165
- return payload.exitCode || 0;
166
- }
167
- if (!payload.results) {
168
- console.error(payload.error || "HelloLoop supervisor 执行失败。");
169
- return payload.exitCode || 1;
170
- }
171
-
172
- const failed = payload.results.find((item) => !item.ok);
173
- for (const item of payload.results) {
174
- if (!item.task) {
175
- console.log("没有更多可执行任务。");
176
- break;
177
- }
178
- console.log(`${item.ok ? "成功" : "失败"}:${item.task.title}`);
179
- }
180
- if (failed) {
181
- console.error(failed.summary || "连续执行中断。");
182
- return payload.exitCode || 1;
183
- }
184
- return 0;
144
+ const payload = await launchAndMaybeWatchSupervisedCommand(context, "run-loop", options);
145
+ return payload.exitCode || 0;
185
146
  }
186
147
 
187
148
  const results = await runLoop(context, options);
package/src/common.mjs CHANGED
@@ -26,6 +26,11 @@ export function writeText(filePath, content) {
26
26
  fs.writeFileSync(filePath, content, "utf8");
27
27
  }
28
28
 
29
+ export function appendText(filePath, content) {
30
+ ensureDir(path.dirname(filePath));
31
+ fs.appendFileSync(filePath, content, "utf8");
32
+ }
33
+
29
34
  export function writeJson(filePath, value) {
30
35
  writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
31
36
  }
@@ -64,3 +69,9 @@ export function resolveFrom(rootDir, ...segments) {
64
69
  export function normalizeRelative(rootDir, targetPath) {
65
70
  return path.relative(rootDir, targetPath).replaceAll("\\", "/");
66
71
  }
72
+
73
+ export function sleep(ms) {
74
+ return new Promise((resolve) => {
75
+ setTimeout(resolve, Math.max(0, Number(ms || 0)));
76
+ });
77
+ }
@@ -121,17 +121,21 @@ export function runChild(command, args, options = {}) {
121
121
  emitHeartbeat("running");
122
122
 
123
123
  child.stdout.on("data", (chunk) => {
124
- stdout += chunk.toString();
124
+ const text = chunk.toString();
125
+ stdout += text;
125
126
  stdoutBytes += chunk.length;
126
127
  lastOutputAt = Date.now();
127
128
  stallWarned = false;
129
+ options.onStdout?.(text);
128
130
  emitHeartbeat("running");
129
131
  });
130
132
  child.stderr.on("data", (chunk) => {
131
- stderr += chunk.toString();
133
+ const text = chunk.toString();
134
+ stderr += text;
132
135
  stderrBytes += chunk.length;
133
136
  lastOutputAt = Date.now();
134
137
  stallWarned = false;
138
+ options.onStderr?.(text);
135
139
  emitHeartbeat("running");
136
140
  });
137
141