helloloop 0.8.2 → 0.8.5

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.2",
3
+ "version": "0.8.5",
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.2",
3
+ "version": "0.8.5",
4
4
  "description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件,Codex 路径为首发与参考实现。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # HelloLoop
2
2
 
3
+ > [!WARNING]
4
+ > **风险提示**
5
+ > 使用 `HelloLoop` 执行持续 / 持久型任务仍存在不可完全控制的风险。虽然项目已经加入高危行为管控与防护策略,但仍不能排除误删数据、误改文件、异常覆盖或其他不可预期后果。请在使用前务必先备份重要数据,并自行通过人工或借助 AI 对 `HelloLoop` 的安全风险做评估与审计;因使用本插件造成的数据丢失或其他损失,项目方不承担责任。
6
+
3
7
  `HelloLoop` 是一个面向 `Codex CLI`、`Claude Code`、`Gemini CLI` 的多宿主开发工作流插件,用来把“根据开发文档持续接续开发、测试、验收,直到最终目标完成”收敛成一条统一、可确认、可追踪的标准流程。
4
8
 
5
9
  它的核心原则很简单:
@@ -93,6 +97,20 @@ npx helloloop gemini <PATH> 接续完成剩余开发
93
97
  - 如果 backlog 清空了,但主线终态复核仍发现文档目标还有缺口,`HelloLoop` 会自动重新分析并继续推进
94
98
  - 如果模型只做了一半就想停下来给“下一步建议”,`HelloLoop` 会优先按主线目标继续推进,而不是把半成品当完成
95
99
 
100
+ ## 后台监督执行
101
+
102
+ 从 `Codex CLI`、`Claude Code`、`Gemini CLI` 这些宿主里发起 `HelloLoop` 时,确认自动执行后会默认切到 **detached supervisor + host lease** 模式:
103
+
104
+ - 当前对话 turn 就算被误按 `Esc`、被宿主暂停、或当前工具调用被中断,后台 supervisor 仍会继续
105
+ - 原有的 15 分钟级恢复、健康探测、同引擎自动恢复,也会继续由这个后台 supervisor 接管
106
+ - 这不是“当前进程死掉后每 15 分钟重新拉起一遍主进程”,而是 supervisor 本身持续存活,所以恢复链不会因为当前 turn 消失而断掉
107
+ - 真正的停止边界改成 **宿主租约**:只要宿主 CLI 窗口 / 进程还活着,任务就继续;如果宿主窗口真的关闭,HelloLoop 才会停止当前子进程并把任务回退为 `pending`
108
+
109
+ 常见场景:
110
+
111
+ - 在 `Codex` / `Claude` / `Gemini` 宿主里运行 `helloloop`:确认后默认转入后台执行,可用 `helloloop status` 查看进度
112
+ - 在普通终端里运行 `npx helloloop run-once --supervised` 或 `npx helloloop run-loop --supervised`:即使中途 `Ctrl+C` 当前命令,只要终端窗口没关,后台 supervisor 仍会继续
113
+
96
114
  ## 无人值守恢复
97
115
 
98
116
  `HelloLoop` 的设计目标不是“跑一轮停一轮”,而是启动前确认一次,启动后持续无人值守推进。
@@ -116,6 +134,13 @@ npx helloloop gemini <PATH> 接续完成剩余开发
116
134
 
117
135
  如果你明确指定或确认了本轮引擎,`HelloLoop` 在自动恢复阶段也会继续锁定该引擎,不会擅自切换。
118
136
 
137
+ 但要特别注意:
138
+
139
+ - 在 `Codex` / `Claude` / `Gemini` 宿主里,当前 turn 被误按 `Esc`、当前工具调用被取消,或当前会话暂时暂停时,只要宿主窗口本身还活着,后台 supervisor 仍会继续,原有自动恢复节奏也会继续生效
140
+ - 如果真正关闭了宿主 CLI 窗口、终端进程结束,或宿主租约已经失效,HelloLoop 会停止当前子进程,并把未完成任务回退为 `pending`
141
+ - 也就是说:**当前 turn 取消 ≠ 停止 HelloLoop;关闭宿主窗口 = 停止 HelloLoop**
142
+ - 宿主窗口已经关闭后,HelloLoop 不会再脱离宿主自行无限后台驻留;这时需要你重新打开宿主并再次显式调用 `helloloop` 来接续
143
+
119
144
  ## 全局告警配置
120
145
 
121
146
  如果希望在“自动恢复彻底停止”后收到邮件告警,可在:
@@ -389,6 +414,7 @@ npx helloloop
389
414
  | `-y` / `--yes` | 跳过执行确认直接开始;但如果未显式指定引擎,会直接报错而不是自动选引擎 |
390
415
  | `--repo <dir>` | 高级覆盖:显式指定项目仓库 |
391
416
  | `--docs <dir|file>` | 高级覆盖:显式指定开发文档 |
417
+ | `--supervised` | 在普通终端里显式启用 detached supervisor;当前命令被打断时,后台执行仍可继续 |
392
418
  | `--rebuild-existing` | 项目与文档冲突时,自动清理现有项目后重建 |
393
419
  | `--host <name>` | 安装宿主:`codex` / `claude` / `gemini` / `all` |
394
420
  | `--config-dir <dir>` | 状态目录名,默认 `.helloloop` |
@@ -421,6 +447,47 @@ npx helloloop doctor --host all
421
447
  npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_HOME> --gemini-home <GEMINI_HOME>
422
448
  ```
423
449
 
450
+ ## 发布流程
451
+
452
+ `HelloLoop` 当前采用 **tag 驱动发布**:
453
+
454
+ 1. 先在源码仓库完成版本号更新(`package.json` 与各宿主 manifest 保持一致)
455
+ 2. 本地先执行:
456
+
457
+ ```bash
458
+ npm test
459
+ npm pack --dry-run
460
+ ```
461
+
462
+ 3. 推送 Git tag:
463
+
464
+ ```bash
465
+ git tag vX.Y.Z
466
+ git push origin vX.Y.Z
467
+ ```
468
+
469
+ 或 beta 版本:
470
+
471
+ ```bash
472
+ git tag vX.Y.Z-beta.N
473
+ git push origin vX.Y.Z-beta.N
474
+ ```
475
+
476
+ 随后 GitHub Actions `Publish to npm` 会自动执行:
477
+
478
+ - tag / 版本一致性校验
479
+ - `npm test`
480
+ - `npm pack --dry-run`
481
+ - `npm publish`
482
+ - GitHub Release
483
+
484
+ 补充说明:
485
+
486
+ - 正式版本使用 npm `latest` 渠道,beta 版本使用 npm `beta` 渠道
487
+ - 如果测试、版本校验或打包检查失败,npm 发布与 GitHub Release 都不会继续执行
488
+ - `0.8.5` 起已补齐 host-aware supervisor 后台执行链路:宿主内自动执行默认后台化,当前 turn 被 `Esc` / cancel 不再直接打断 HelloLoop 主线
489
+ - GitHub Release 阶段现已改为使用官方 `gh` CLI + `generate-notes` API 创建 / 更新 release,不再依赖会触发 Node runtime deprecation warning 的第三方 action
490
+
424
491
  ## 宿主写入范围
425
492
 
426
493
  为了方便排查安装 / 更新 / 卸载问题,下面是默认写入位置:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.2",
3
+ "version": "0.8.5",
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.2",
3
+ "version": "0.8.5",
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.2",
3
+ "version": "0.8.5",
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/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"
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"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=20"
package/src/analyzer.mjs CHANGED
@@ -1,8 +1,14 @@
1
1
  import path from "node:path";
2
2
 
3
- import { summarizeBacklog, selectNextTask } from "./backlog.mjs";
3
+ import { summarizeBacklog } from "./backlog.mjs";
4
4
  import { nowIso, writeJson, writeText, readTextIfExists } from "./common.mjs";
5
- import { loadPolicy, loadProjectConfig, scaffoldIfMissing, writeStateMarkdown, writeStatus } from "./config.mjs";
5
+ import {
6
+ loadPolicy,
7
+ loadProjectConfig,
8
+ scaffoldIfMissing,
9
+ writeStateMarkdown,
10
+ writeStatus,
11
+ } from "./config.mjs";
6
12
  import { createContext } from "./context.mjs";
7
13
  import { discoverWorkspace } from "./discovery.mjs";
8
14
  import { readDocumentPackets } from "./doc_loader.mjs";
@@ -10,127 +16,17 @@ import {
10
16
  rememberEngineSelection,
11
17
  resolveEngineSelection,
12
18
  } from "./engine_selection.mjs";
19
+ import {
20
+ buildAnalysisSummaryText,
21
+ buildCurrentWorkspaceDiscovery,
22
+ normalizeAnalysisPayload,
23
+ persistAnalysisFailure,
24
+ renderAnalysisState,
25
+ summarizeFailedAnalysisResult,
26
+ } from "./analyzer_support.mjs";
13
27
  import { runEngineTask } from "./process.mjs";
14
28
  import { buildAnalysisPrompt } from "./analyze_prompt.mjs";
15
29
 
16
- function renderAnalysisState(context, backlog, analysis) {
17
- const summary = summarizeBacklog(backlog);
18
- const nextTask = selectNextTask(backlog);
19
-
20
- return [
21
- "## 当前状态",
22
- `- backlog 文件:${path.relative(context.repoRoot, context.backlogFile).replaceAll("\\", "/")}`,
23
- `- 总任务数:${summary.total}`,
24
- `- 已完成:${summary.done}`,
25
- `- 待处理:${summary.pending}`,
26
- `- 进行中:${summary.inProgress}`,
27
- `- 失败:${summary.failed}`,
28
- `- 阻塞:${summary.blocked}`,
29
- `- 当前任务:${nextTask ? nextTask.title : "无"}`,
30
- `- 最近结果:${analysis.summary.currentState}`,
31
- `- 下一建议:${analysis.summary.nextAction}`,
32
- ].join("\n");
33
- }
34
-
35
- function sanitizeTask(task) {
36
- return {
37
- id: String(task.id || "").trim(),
38
- title: String(task.title || "").trim(),
39
- status: ["done", "blocked"].includes(String(task.status || "")) ? String(task.status) : "pending",
40
- priority: ["P0", "P1", "P2", "P3"].includes(String(task.priority || "")) ? String(task.priority) : "P2",
41
- risk: ["medium", "high", "critical"].includes(String(task.risk || "")) ? String(task.risk) : "low",
42
- goal: String(task.goal || "").trim(),
43
- docs: Array.isArray(task.docs) ? task.docs.map((item) => String(item || "").trim()).filter(Boolean) : [],
44
- paths: Array.isArray(task.paths) ? task.paths.map((item) => String(item || "").trim()).filter(Boolean) : [],
45
- acceptance: Array.isArray(task.acceptance) ? task.acceptance.map((item) => String(item || "").trim()).filter(Boolean) : [],
46
- dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.map((item) => String(item || "").trim()).filter(Boolean) : [],
47
- verify: Array.isArray(task.verify) ? task.verify.map((item) => String(item || "").trim()).filter(Boolean) : [],
48
- };
49
- }
50
-
51
- function normalizeAnalysisPayload(payload, docsEntries) {
52
- const summary = {
53
- currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
54
- implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
55
- remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
56
- nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
57
- };
58
- const tasks = Array.isArray(payload.tasks)
59
- ? payload.tasks.map((task) => sanitizeTask(task)).filter((task) => (
60
- task.id && task.title && task.goal && task.acceptance.length
61
- ))
62
- : [];
63
-
64
- if (!tasks.length && summary.remaining.length) {
65
- throw new Error("分析结果无效:仍存在剩余工作,但未生成可用任务。");
66
- }
67
-
68
- const requestInterpretation = payload?.requestInterpretation && typeof payload.requestInterpretation === "object"
69
- ? {
70
- summary: String(payload.requestInterpretation.summary || "").trim(),
71
- priorities: Array.isArray(payload.requestInterpretation.priorities)
72
- ? payload.requestInterpretation.priorities.map((item) => String(item || "").trim()).filter(Boolean)
73
- : [],
74
- cautions: Array.isArray(payload.requestInterpretation.cautions)
75
- ? payload.requestInterpretation.cautions.map((item) => String(item || "").trim()).filter(Boolean)
76
- : [],
77
- }
78
- : null;
79
- const repoDecision = payload?.repoDecision && typeof payload.repoDecision === "object"
80
- ? {
81
- compatibility: ["compatible", "conflict", "uncertain"].includes(String(payload.repoDecision.compatibility || ""))
82
- ? String(payload.repoDecision.compatibility)
83
- : "compatible",
84
- action: ["continue_existing", "confirm_rebuild", "start_new"].includes(String(payload.repoDecision.action || ""))
85
- ? String(payload.repoDecision.action)
86
- : "continue_existing",
87
- reason: String(payload.repoDecision.reason || "").trim(),
88
- }
89
- : null;
90
-
91
- return {
92
- project: String(payload.project || "").trim() || "helloloop-project",
93
- summary,
94
- constraints: Array.isArray(payload.constraints)
95
- ? payload.constraints.map((item) => String(item || "").trim()).filter(Boolean)
96
- : [],
97
- requestInterpretation: requestInterpretation && (
98
- requestInterpretation.summary
99
- || requestInterpretation.priorities.length
100
- || requestInterpretation.cautions.length
101
- )
102
- ? requestInterpretation
103
- : null,
104
- repoDecision: repoDecision && repoDecision.reason
105
- ? repoDecision
106
- : null,
107
- tasks,
108
- requiredDocs: docsEntries,
109
- };
110
- }
111
-
112
- function buildAnalysisSummaryText(context, analysis, backlog, engineResolution) {
113
- const summary = summarizeBacklog(backlog);
114
- const nextTask = selectNextTask(backlog);
115
-
116
- return [
117
- "HelloLoop 已完成接续分析。",
118
- `项目仓库:${context.repoRoot}`,
119
- `开发文档:${analysis.requiredDocs.join(", ")}`,
120
- `执行引擎:${engineResolution?.displayName || "未记录"}`,
121
- "",
122
- "当前进度:",
123
- analysis.summary.currentState,
124
- "",
125
- `任务统计:done ${summary.done} / pending ${summary.pending} / blocked ${summary.blocked}`,
126
- nextTask ? `下一任务:${nextTask.title}` : "下一任务:暂无可执行任务",
127
- "",
128
- "下一步建议:",
129
- `- npx helloloop`,
130
- `- npx helloloop --dry-run`,
131
- ].join("\n");
132
- }
133
-
134
30
  async function analyzeResolvedWorkspace(context, discovery, options = {}) {
135
31
  scaffoldIfMissing(context);
136
32
  const policy = loadPolicy(context);
@@ -183,13 +79,33 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
183
79
  outputSchemaFile: schemaFile,
184
80
  outputPrefix: `${engineResolution.engine}-analysis`,
185
81
  skipGitRepoCheck: true,
82
+ hostLease: options.hostLease || null,
186
83
  });
187
84
 
188
85
  if (!analysisResult.ok) {
86
+ if (analysisResult.leaseExpired) {
87
+ return {
88
+ ok: false,
89
+ code: "host-lease-stopped",
90
+ summary: analysisResult.leaseReason || "检测到宿主窗口已关闭,主线终态复核已停止。",
91
+ stopped: true,
92
+ engineResolution,
93
+ discovery,
94
+ };
95
+ }
96
+ const failureSummary = summarizeFailedAnalysisResult(
97
+ analysisResult,
98
+ `${engineResolution.displayName} 接续分析失败。`,
99
+ );
100
+ persistAnalysisFailure(
101
+ context,
102
+ failureSummary,
103
+ runDir,
104
+ );
189
105
  return {
190
106
  ok: false,
191
107
  code: "analysis_failed",
192
- summary: analysisResult.stderr || analysisResult.stdout || `${engineResolution.displayName} 接续分析失败。`,
108
+ summary: failureSummary,
193
109
  engineResolution,
194
110
  discovery,
195
111
  };
@@ -199,6 +115,11 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
199
115
  try {
200
116
  payload = JSON.parse(analysisResult.finalMessage);
201
117
  } catch (error) {
118
+ persistAnalysisFailure(
119
+ context,
120
+ `${engineResolution.displayName} 分析结果无法解析为 JSON:${String(error?.message || error || "")}`,
121
+ runDir,
122
+ );
202
123
  return {
203
124
  ok: false,
204
125
  code: "invalid_analysis_json",
@@ -208,7 +129,24 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
208
129
  };
209
130
  }
210
131
 
211
- const analysis = normalizeAnalysisPayload(payload, discovery.docsEntries);
132
+ let analysis;
133
+ try {
134
+ analysis = normalizeAnalysisPayload(payload, discovery.docsEntries);
135
+ } catch (error) {
136
+ persistAnalysisFailure(
137
+ context,
138
+ `${engineResolution.displayName} 分析结果无效:${String(error?.message || error || "")}`,
139
+ runDir,
140
+ );
141
+ return {
142
+ ok: false,
143
+ code: "invalid_analysis_payload",
144
+ summary: `${engineResolution.displayName} 分析结果无效:${String(error?.message || error || "")}`,
145
+ engineResolution,
146
+ discovery,
147
+ };
148
+ }
149
+
212
150
  const backlog = {
213
151
  version: 1,
214
152
  project: analysis.project,
@@ -252,38 +190,6 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
252
190
  };
253
191
  }
254
192
 
255
- function buildCurrentWorkspaceDiscovery(context, docsEntries) {
256
- return {
257
- ok: true,
258
- repoRoot: context.repoRoot,
259
- docsEntries,
260
- resolvedDocs: docsEntries,
261
- resolution: {
262
- repo: {
263
- source: "current_repo",
264
- sourceLabel: "当前项目",
265
- confidence: "high",
266
- confidenceLabel: "高",
267
- path: context.repoRoot,
268
- exists: true,
269
- basis: [
270
- "已在当前项目基础上执行主线终态复核。",
271
- ],
272
- },
273
- docs: {
274
- source: "existing_state",
275
- sourceLabel: "已有 .helloloop 配置",
276
- confidence: "high",
277
- confidenceLabel: "高",
278
- entries: docsEntries,
279
- basis: [
280
- "已复用 `.helloloop/project.json` 中记录的 requiredDocs。",
281
- ],
282
- },
283
- },
284
- };
285
- }
286
-
287
193
  export async function reanalyzeCurrentWorkspace(context, options = {}) {
288
194
  const existingProjectConfig = loadProjectConfig(context);
289
195
  const docsEntries = Array.isArray(options.requiredDocs) && options.requiredDocs.length
@@ -0,0 +1,232 @@
1
+ import path from "node:path";
2
+
3
+ import { selectNextTask, summarizeBacklog } from "./backlog.mjs";
4
+ import { loadBacklog, writeStateMarkdown, writeStatus } from "./config.mjs";
5
+
6
+ export function renderAnalysisState(context, backlog, analysis) {
7
+ const summary = summarizeBacklog(backlog);
8
+ const nextTask = selectNextTask(backlog);
9
+
10
+ return [
11
+ "## 当前状态",
12
+ `- backlog 文件:${path.relative(context.repoRoot, context.backlogFile).replaceAll("\\", "/")}`,
13
+ `- 总任务数:${summary.total}`,
14
+ `- 已完成:${summary.done}`,
15
+ `- 待处理:${summary.pending}`,
16
+ `- 进行中:${summary.inProgress}`,
17
+ `- 失败:${summary.failed}`,
18
+ `- 阻塞:${summary.blocked}`,
19
+ `- 当前任务:${nextTask ? nextTask.title : "无"}`,
20
+ `- 最近结果:${analysis.summary.currentState}`,
21
+ `- 下一建议:${analysis.summary.nextAction}`,
22
+ ].join("\n");
23
+ }
24
+
25
+ function createEmptyBacklogSummary() {
26
+ return {
27
+ total: 0,
28
+ pending: 0,
29
+ inProgress: 0,
30
+ done: 0,
31
+ failed: 0,
32
+ blocked: 0,
33
+ };
34
+ }
35
+
36
+ export function getExistingBacklogSnapshot(context) {
37
+ try {
38
+ const backlog = loadBacklog(context);
39
+ return {
40
+ summary: summarizeBacklog(backlog),
41
+ nextTask: selectNextTask(backlog),
42
+ };
43
+ } catch {
44
+ return {
45
+ summary: createEmptyBacklogSummary(),
46
+ nextTask: null,
47
+ };
48
+ }
49
+ }
50
+
51
+ function firstMeaningfulLine(text, fallback) {
52
+ return String(text || "")
53
+ .split(/\r?\n/)
54
+ .map((line) => line.trim())
55
+ .find(Boolean) || fallback;
56
+ }
57
+
58
+ export function summarizeFailedAnalysisResult(result, fallback) {
59
+ const combined = [
60
+ String(result?.stdout || "").trim(),
61
+ String(result?.stderr || "").trim(),
62
+ ].filter(Boolean).join("\n\n").trim();
63
+ return combined || fallback;
64
+ }
65
+
66
+ function renderAnalysisFailureState(context, backlogSummary, nextTask, failureSummary, runDir = "") {
67
+ const runDirHint = runDir
68
+ ? path.relative(context.repoRoot, runDir).replaceAll("\\", "/")
69
+ : "";
70
+
71
+ return [
72
+ "## 当前状态",
73
+ `- backlog 文件:${path.relative(context.repoRoot, context.backlogFile).replaceAll("\\", "/")}`,
74
+ `- 总任务数:${backlogSummary.total}`,
75
+ `- 已完成:${backlogSummary.done}`,
76
+ `- 待处理:${backlogSummary.pending}`,
77
+ `- 进行中:${backlogSummary.inProgress}`,
78
+ `- 失败:${backlogSummary.failed}`,
79
+ `- 阻塞:${backlogSummary.blocked}`,
80
+ `- 当前任务:${nextTask ? nextTask.title : "无"}`,
81
+ `- 最近结果:${firstMeaningfulLine(failureSummary, "HelloLoop 分析失败")}`,
82
+ `- 下一建议:${runDirHint ? `先检查 ${runDirHint} 中的日志后再重新执行 npx helloloop` : "修复错误后重新执行 npx helloloop"}`,
83
+ ].join("\n");
84
+ }
85
+
86
+ export function persistAnalysisFailure(context, failureSummary, runDir = "") {
87
+ const snapshot = getExistingBacklogSnapshot(context);
88
+ writeStatus(context, {
89
+ ok: false,
90
+ stage: "analysis_failed",
91
+ taskId: null,
92
+ taskTitle: "",
93
+ runDir,
94
+ summary: snapshot.summary,
95
+ message: failureSummary,
96
+ });
97
+ writeStateMarkdown(
98
+ context,
99
+ renderAnalysisFailureState(context, snapshot.summary, snapshot.nextTask, failureSummary, runDir),
100
+ );
101
+ }
102
+
103
+ function sanitizeTask(task) {
104
+ return {
105
+ id: String(task.id || "").trim(),
106
+ title: String(task.title || "").trim(),
107
+ status: ["done", "blocked"].includes(String(task.status || "")) ? String(task.status) : "pending",
108
+ priority: ["P0", "P1", "P2", "P3"].includes(String(task.priority || "")) ? String(task.priority) : "P2",
109
+ risk: ["medium", "high", "critical"].includes(String(task.risk || "")) ? String(task.risk) : "low",
110
+ goal: String(task.goal || "").trim(),
111
+ docs: Array.isArray(task.docs) ? task.docs.map((item) => String(item || "").trim()).filter(Boolean) : [],
112
+ paths: Array.isArray(task.paths) ? task.paths.map((item) => String(item || "").trim()).filter(Boolean) : [],
113
+ acceptance: Array.isArray(task.acceptance) ? task.acceptance.map((item) => String(item || "").trim()).filter(Boolean) : [],
114
+ dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.map((item) => String(item || "").trim()).filter(Boolean) : [],
115
+ verify: Array.isArray(task.verify) ? task.verify.map((item) => String(item || "").trim()).filter(Boolean) : [],
116
+ };
117
+ }
118
+
119
+ export function normalizeAnalysisPayload(payload, docsEntries) {
120
+ const summary = {
121
+ currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
122
+ implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
123
+ remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
124
+ nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
125
+ };
126
+ const tasks = Array.isArray(payload.tasks)
127
+ ? payload.tasks.map((task) => sanitizeTask(task)).filter((task) => (
128
+ task.id && task.title && task.goal && task.acceptance.length
129
+ ))
130
+ : [];
131
+
132
+ if (!tasks.length && summary.remaining.length) {
133
+ throw new Error("分析结果无效:仍存在剩余工作,但未生成可用任务。");
134
+ }
135
+
136
+ const requestInterpretation = payload?.requestInterpretation && typeof payload.requestInterpretation === "object"
137
+ ? {
138
+ summary: String(payload.requestInterpretation.summary || "").trim(),
139
+ priorities: Array.isArray(payload.requestInterpretation.priorities)
140
+ ? payload.requestInterpretation.priorities.map((item) => String(item || "").trim()).filter(Boolean)
141
+ : [],
142
+ cautions: Array.isArray(payload.requestInterpretation.cautions)
143
+ ? payload.requestInterpretation.cautions.map((item) => String(item || "").trim()).filter(Boolean)
144
+ : [],
145
+ }
146
+ : null;
147
+ const repoDecision = payload?.repoDecision && typeof payload.repoDecision === "object"
148
+ ? {
149
+ compatibility: ["compatible", "conflict", "uncertain"].includes(String(payload.repoDecision.compatibility || ""))
150
+ ? String(payload.repoDecision.compatibility)
151
+ : "compatible",
152
+ action: ["continue_existing", "confirm_rebuild", "start_new"].includes(String(payload.repoDecision.action || ""))
153
+ ? String(payload.repoDecision.action)
154
+ : "continue_existing",
155
+ reason: String(payload.repoDecision.reason || "").trim(),
156
+ }
157
+ : null;
158
+
159
+ return {
160
+ project: String(payload.project || "").trim() || "helloloop-project",
161
+ summary,
162
+ constraints: Array.isArray(payload.constraints)
163
+ ? payload.constraints.map((item) => String(item || "").trim()).filter(Boolean)
164
+ : [],
165
+ requestInterpretation: requestInterpretation && (
166
+ requestInterpretation.summary
167
+ || requestInterpretation.priorities.length
168
+ || requestInterpretation.cautions.length
169
+ )
170
+ ? requestInterpretation
171
+ : null,
172
+ repoDecision: repoDecision && repoDecision.reason
173
+ ? repoDecision
174
+ : null,
175
+ tasks,
176
+ requiredDocs: docsEntries,
177
+ };
178
+ }
179
+
180
+ export function buildAnalysisSummaryText(context, analysis, backlog, engineResolution) {
181
+ const summary = summarizeBacklog(backlog);
182
+ const nextTask = selectNextTask(backlog);
183
+
184
+ return [
185
+ "HelloLoop 已完成接续分析。",
186
+ `项目仓库:${context.repoRoot}`,
187
+ `开发文档:${analysis.requiredDocs.join(", ")}`,
188
+ `执行引擎:${engineResolution?.displayName || "未记录"}`,
189
+ "",
190
+ "当前进度:",
191
+ analysis.summary.currentState,
192
+ "",
193
+ `任务统计:done ${summary.done} / pending ${summary.pending} / blocked ${summary.blocked}`,
194
+ nextTask ? `下一任务:${nextTask.title}` : "下一任务:暂无可执行任务",
195
+ "",
196
+ "下一步建议:",
197
+ `- npx helloloop`,
198
+ `- npx helloloop --dry-run`,
199
+ ].join("\n");
200
+ }
201
+
202
+ export function buildCurrentWorkspaceDiscovery(context, docsEntries) {
203
+ return {
204
+ ok: true,
205
+ repoRoot: context.repoRoot,
206
+ docsEntries,
207
+ resolvedDocs: docsEntries,
208
+ resolution: {
209
+ repo: {
210
+ source: "current_repo",
211
+ sourceLabel: "当前项目",
212
+ confidence: "high",
213
+ confidenceLabel: "高",
214
+ path: context.repoRoot,
215
+ exists: true,
216
+ basis: [
217
+ "已在当前项目基础上执行主线终态复核。",
218
+ ],
219
+ },
220
+ docs: {
221
+ source: "existing_state",
222
+ sourceLabel: "已有 .helloloop 配置",
223
+ confidence: "high",
224
+ confidenceLabel: "高",
225
+ entries: docsEntries,
226
+ basis: [
227
+ "已复用 `.helloloop/project.json` 中记录的 requiredDocs。",
228
+ ],
229
+ },
230
+ },
231
+ };
232
+ }
package/src/cli.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  } from "./cli_command_handlers.mjs";
14
14
  import { resolveContextFromOptions, resolveStandardCommandOptions } from "./cli_context.mjs";
15
15
  import { runDoctor } from "./cli_support.mjs";
16
+ import { runSupervisedCommandFromSessionFile } from "./supervisor_runtime.mjs";
16
17
 
17
18
  export async function runCli(argv) {
18
19
  const parsed = parseArgs(argv);
@@ -21,6 +22,13 @@ export async function runCli(argv) {
21
22
  printHelp();
22
23
  return;
23
24
  }
25
+ if (command === "__supervise") {
26
+ if (!parsed.options.sessionFile) {
27
+ throw new Error("缺少 --session-file,无法启动 HelloLoop supervisor。");
28
+ }
29
+ await runSupervisedCommandFromSessionFile(parsed.options.sessionFile);
30
+ return;
31
+ }
24
32
 
25
33
  if (command === "analyze") {
26
34
  process.exitCode = await handleAnalyzeCommand(normalizeAnalyzeOptions(parsed.options, process.cwd()));