helloloop 0.8.4 → 0.8.6

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.4",
3
+ "version": "0.8.6",
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.4",
3
+ "version": "0.8.6",
4
4
  "description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件,Codex 路径为首发与参考实现。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
package/README.md CHANGED
@@ -97,6 +97,22 @@ npx helloloop gemini <PATH> 接续完成剩余开发
97
97
  - 如果 backlog 清空了,但主线终态复核仍发现文档目标还有缺口,`HelloLoop` 会自动重新分析并继续推进
98
98
  - 如果模型只做了一半就想停下来给“下一步建议”,`HelloLoop` 会优先按主线目标继续推进,而不是把半成品当完成
99
99
 
100
+ ## 后台监督执行
101
+
102
+ 从当前版本开始,`HelloLoop` 的自动执行主线统一走 **detached supervisor + host lease** 模式:
103
+
104
+ - `analyze` 确认后的自动执行、`run-once`、`run-loop` 都默认切到后台 supervisor
105
+ - 当前对话 turn 就算被误按 `Esc`、被宿主暂停、或当前工具调用被中断,后台 supervisor 仍会继续
106
+ - 原有的 15 分钟级恢复、健康探测、同引擎自动恢复,也会继续由这个后台 supervisor 接管
107
+ - 这不是“当前进程死掉后每 15 分钟重新拉起一遍主进程”,而是 supervisor 本身持续存活,所以恢复链不会因为当前 turn 消失而断掉
108
+ - 真正的停止边界改成 **宿主租约**:只要宿主 CLI 窗口 / 进程还活着,任务就继续;如果宿主窗口真的关闭,HelloLoop 才会停止当前子进程并把任务回退为 `pending`
109
+
110
+ 常见场景:
111
+
112
+ - 在 `Codex` / `Claude` / `Gemini` 宿主里运行 `helloloop`:确认后默认转入后台执行,可用 `helloloop status` 查看进度
113
+ - 在普通终端里运行 `npx helloloop`、`npx helloloop run-once`、`npx helloloop run-loop`:默认也会转入后台执行
114
+ - `--supervised` 仍然保留,但现在只是兼容参数,不再是开启后台 supervisor 的前提
115
+
100
116
  ## 无人值守恢复
101
117
 
102
118
  `HelloLoop` 的设计目标不是“跑一轮停一轮”,而是启动前确认一次,启动后持续无人值守推进。
@@ -120,6 +136,13 @@ npx helloloop gemini <PATH> 接续完成剩余开发
120
136
 
121
137
  如果你明确指定或确认了本轮引擎,`HelloLoop` 在自动恢复阶段也会继续锁定该引擎,不会擅自切换。
122
138
 
139
+ 但要特别注意:
140
+
141
+ - 在 `Codex` / `Claude` / `Gemini` 宿主里,当前 turn 被误按 `Esc`、当前工具调用被取消,或当前会话暂时暂停时,只要宿主窗口本身还活着,后台 supervisor 仍会继续,原有自动恢复节奏也会继续生效
142
+ - 如果真正关闭了宿主 CLI 窗口、终端进程结束,或宿主租约已经失效,HelloLoop 会停止当前子进程,并把未完成任务回退为 `pending`
143
+ - 也就是说:**当前 turn 取消 ≠ 停止 HelloLoop;关闭宿主窗口 = 停止 HelloLoop**
144
+ - 宿主窗口已经关闭后,HelloLoop 不会再脱离宿主自行无限后台驻留;这时需要你重新打开宿主并再次显式调用 `helloloop` 来接续
145
+
123
146
  ## 全局告警配置
124
147
 
125
148
  如果希望在“自动恢复彻底停止”后收到邮件告警,可在:
@@ -154,6 +177,46 @@ npx helloloop gemini <PATH> 接续完成剩余开发
154
177
  - 建议把 SMTP 密码放在环境变量里,不要明文写进配置文件
155
178
  - 邮件只在“本轮不再继续自动重试”时发送,不会每次失败都刷屏
156
179
 
180
+ ## 终端并发上限
181
+
182
+ `HelloLoop` 不会主动再额外弹出一堆可见终端窗口;这里的:
183
+
184
+ - **显示终端** 指当前前台运行中的 `helloloop` 会话
185
+ - **背景终端** 指 detached supervisor 后台会话
186
+
187
+ 默认限制为:
188
+
189
+ - 显示终端最多 `8`
190
+ - 背景终端最多 `8`
191
+ - 显示 + 背景合计最多 `8`
192
+
193
+ 可在:
194
+
195
+ ```text
196
+ ~/.helloloop/settings.json
197
+ ```
198
+
199
+ 中自行调整,例如:
200
+
201
+ ```json
202
+ {
203
+ "runtime": {
204
+ "terminalConcurrency": {
205
+ "enabled": true,
206
+ "visibleMax": 8,
207
+ "backgroundMax": 8,
208
+ "totalMax": 8
209
+ }
210
+ }
211
+ }
212
+ ```
213
+
214
+ 说明:
215
+
216
+ - 只要合计并发达到 `totalMax`,新前台会话或新的后台 supervisor 都不会再继续启动
217
+ - 如果只是想临时完全关闭这个保护,可把 `enabled` 设为 `false`
218
+ - 这个限制主要用于避免同时占用过多本机终端资源,或因为并发过高触发模型 / API 限速
219
+
157
220
  ## 自动发现与交互逻辑
158
221
 
159
222
  ### 1. 只输入 `npx helloloop`
@@ -393,6 +456,7 @@ npx helloloop
393
456
  | `-y` / `--yes` | 跳过执行确认直接开始;但如果未显式指定引擎,会直接报错而不是自动选引擎 |
394
457
  | `--repo <dir>` | 高级覆盖:显式指定项目仓库 |
395
458
  | `--docs <dir|file>` | 高级覆盖:显式指定开发文档 |
459
+ | `--supervised` | 兼容保留;当前版本默认已启用 detached supervisor |
396
460
  | `--rebuild-existing` | 项目与文档冲突时,自动清理现有项目后重建 |
397
461
  | `--host <name>` | 安装宿主:`codex` / `claude` / `gemini` / `all` |
398
462
  | `--config-dir <dir>` | 状态目录名,默认 `.helloloop` |
@@ -463,7 +527,8 @@ git push origin vX.Y.Z-beta.N
463
527
 
464
528
  - 正式版本使用 npm `latest` 渠道,beta 版本使用 npm `beta` 渠道
465
529
  - 如果测试、版本校验或打包检查失败,npm 发布与 GitHub Release 都不会继续执行
466
- - `0.8.4` 起已补齐 Codex Structured Outputs 严格 schema 兼容,并修复快退出 CLI 在 CI 中可能触发的 `stdin EPIPE` 问题,避免发布工作流被非业务性故障打断
530
+ - `0.8.6` 起已统一为全流程后台 supervisor:`analyze` 确认后的自动执行、`run-once`、`run-loop` 默认都后台化,不再要求普通终端显式追加 `--supervised`
531
+ - GitHub Release 阶段现已改为使用官方 `gh` CLI + `generate-notes` API 创建 / 更新 release,不再依赖会触发 Node runtime deprecation warning 的第三方 action
467
532
 
468
533
  ## 宿主写入范围
469
534
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
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.4",
3
+ "version": "0.8.6",
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.4",
3
+ "version": "0.8.6",
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"
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/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/terminal_session_limits.test.mjs"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=20"
package/src/analyzer.mjs CHANGED
@@ -1,9 +1,8 @@
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
5
  import {
6
- loadBacklog,
7
6
  loadPolicy,
8
7
  loadProjectConfig,
9
8
  scaffoldIfMissing,
@@ -17,205 +16,17 @@ import {
17
16
  rememberEngineSelection,
18
17
  resolveEngineSelection,
19
18
  } from "./engine_selection.mjs";
19
+ import {
20
+ buildAnalysisSummaryText,
21
+ buildCurrentWorkspaceDiscovery,
22
+ normalizeAnalysisPayload,
23
+ persistAnalysisFailure,
24
+ renderAnalysisState,
25
+ summarizeFailedAnalysisResult,
26
+ } from "./analyzer_support.mjs";
20
27
  import { runEngineTask } from "./process.mjs";
21
28
  import { buildAnalysisPrompt } from "./analyze_prompt.mjs";
22
29
 
23
- function renderAnalysisState(context, backlog, analysis) {
24
- const summary = summarizeBacklog(backlog);
25
- const nextTask = selectNextTask(backlog);
26
-
27
- return [
28
- "## 当前状态",
29
- `- backlog 文件:${path.relative(context.repoRoot, context.backlogFile).replaceAll("\\", "/")}`,
30
- `- 总任务数:${summary.total}`,
31
- `- 已完成:${summary.done}`,
32
- `- 待处理:${summary.pending}`,
33
- `- 进行中:${summary.inProgress}`,
34
- `- 失败:${summary.failed}`,
35
- `- 阻塞:${summary.blocked}`,
36
- `- 当前任务:${nextTask ? nextTask.title : "无"}`,
37
- `- 最近结果:${analysis.summary.currentState}`,
38
- `- 下一建议:${analysis.summary.nextAction}`,
39
- ].join("\n");
40
- }
41
-
42
- function createEmptyBacklogSummary() {
43
- return {
44
- total: 0,
45
- pending: 0,
46
- inProgress: 0,
47
- done: 0,
48
- failed: 0,
49
- blocked: 0,
50
- };
51
- }
52
-
53
- function getExistingBacklogSnapshot(context) {
54
- try {
55
- const backlog = loadBacklog(context);
56
- return {
57
- summary: summarizeBacklog(backlog),
58
- nextTask: selectNextTask(backlog),
59
- };
60
- } catch {
61
- return {
62
- summary: createEmptyBacklogSummary(),
63
- nextTask: null,
64
- };
65
- }
66
- }
67
-
68
- function firstMeaningfulLine(text, fallback) {
69
- return String(text || "")
70
- .split(/\r?\n/)
71
- .map((line) => line.trim())
72
- .find(Boolean) || fallback;
73
- }
74
-
75
- function summarizeFailedAnalysisResult(result, fallback) {
76
- const combined = [
77
- String(result?.stdout || "").trim(),
78
- String(result?.stderr || "").trim(),
79
- ].filter(Boolean).join("\n\n").trim();
80
- return combined || fallback;
81
- }
82
-
83
- function renderAnalysisFailureState(context, backlogSummary, nextTask, failureSummary, runDir = "") {
84
- const runDirHint = runDir
85
- ? path.relative(context.repoRoot, runDir).replaceAll("\\", "/")
86
- : "";
87
-
88
- return [
89
- "## 当前状态",
90
- `- backlog 文件:${path.relative(context.repoRoot, context.backlogFile).replaceAll("\\", "/")}`,
91
- `- 总任务数:${backlogSummary.total}`,
92
- `- 已完成:${backlogSummary.done}`,
93
- `- 待处理:${backlogSummary.pending}`,
94
- `- 进行中:${backlogSummary.inProgress}`,
95
- `- 失败:${backlogSummary.failed}`,
96
- `- 阻塞:${backlogSummary.blocked}`,
97
- `- 当前任务:${nextTask ? nextTask.title : "无"}`,
98
- `- 最近结果:${firstMeaningfulLine(failureSummary, "HelloLoop 分析失败")}`,
99
- `- 下一建议:${runDirHint ? `先检查 ${runDirHint} 中的日志后再重新执行 npx helloloop` : "修复错误后重新执行 npx helloloop"}`,
100
- ].join("\n");
101
- }
102
-
103
- function persistAnalysisFailure(context, failureSummary, runDir = "") {
104
- const snapshot = getExistingBacklogSnapshot(context);
105
- writeStatus(context, {
106
- ok: false,
107
- stage: "analysis_failed",
108
- taskId: null,
109
- taskTitle: "",
110
- runDir,
111
- summary: snapshot.summary,
112
- message: failureSummary,
113
- });
114
- writeStateMarkdown(
115
- context,
116
- renderAnalysisFailureState(context, snapshot.summary, snapshot.nextTask, failureSummary, runDir),
117
- );
118
- }
119
-
120
- function sanitizeTask(task) {
121
- return {
122
- id: String(task.id || "").trim(),
123
- title: String(task.title || "").trim(),
124
- status: ["done", "blocked"].includes(String(task.status || "")) ? String(task.status) : "pending",
125
- priority: ["P0", "P1", "P2", "P3"].includes(String(task.priority || "")) ? String(task.priority) : "P2",
126
- risk: ["medium", "high", "critical"].includes(String(task.risk || "")) ? String(task.risk) : "low",
127
- goal: String(task.goal || "").trim(),
128
- docs: Array.isArray(task.docs) ? task.docs.map((item) => String(item || "").trim()).filter(Boolean) : [],
129
- paths: Array.isArray(task.paths) ? task.paths.map((item) => String(item || "").trim()).filter(Boolean) : [],
130
- acceptance: Array.isArray(task.acceptance) ? task.acceptance.map((item) => String(item || "").trim()).filter(Boolean) : [],
131
- dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.map((item) => String(item || "").trim()).filter(Boolean) : [],
132
- verify: Array.isArray(task.verify) ? task.verify.map((item) => String(item || "").trim()).filter(Boolean) : [],
133
- };
134
- }
135
-
136
- function normalizeAnalysisPayload(payload, docsEntries) {
137
- const summary = {
138
- currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
139
- implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
140
- remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
141
- nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
142
- };
143
- const tasks = Array.isArray(payload.tasks)
144
- ? payload.tasks.map((task) => sanitizeTask(task)).filter((task) => (
145
- task.id && task.title && task.goal && task.acceptance.length
146
- ))
147
- : [];
148
-
149
- if (!tasks.length && summary.remaining.length) {
150
- throw new Error("分析结果无效:仍存在剩余工作,但未生成可用任务。");
151
- }
152
-
153
- const requestInterpretation = payload?.requestInterpretation && typeof payload.requestInterpretation === "object"
154
- ? {
155
- summary: String(payload.requestInterpretation.summary || "").trim(),
156
- priorities: Array.isArray(payload.requestInterpretation.priorities)
157
- ? payload.requestInterpretation.priorities.map((item) => String(item || "").trim()).filter(Boolean)
158
- : [],
159
- cautions: Array.isArray(payload.requestInterpretation.cautions)
160
- ? payload.requestInterpretation.cautions.map((item) => String(item || "").trim()).filter(Boolean)
161
- : [],
162
- }
163
- : null;
164
- const repoDecision = payload?.repoDecision && typeof payload.repoDecision === "object"
165
- ? {
166
- compatibility: ["compatible", "conflict", "uncertain"].includes(String(payload.repoDecision.compatibility || ""))
167
- ? String(payload.repoDecision.compatibility)
168
- : "compatible",
169
- action: ["continue_existing", "confirm_rebuild", "start_new"].includes(String(payload.repoDecision.action || ""))
170
- ? String(payload.repoDecision.action)
171
- : "continue_existing",
172
- reason: String(payload.repoDecision.reason || "").trim(),
173
- }
174
- : null;
175
-
176
- return {
177
- project: String(payload.project || "").trim() || "helloloop-project",
178
- summary,
179
- constraints: Array.isArray(payload.constraints)
180
- ? payload.constraints.map((item) => String(item || "").trim()).filter(Boolean)
181
- : [],
182
- requestInterpretation: requestInterpretation && (
183
- requestInterpretation.summary
184
- || requestInterpretation.priorities.length
185
- || requestInterpretation.cautions.length
186
- )
187
- ? requestInterpretation
188
- : null,
189
- repoDecision: repoDecision && repoDecision.reason
190
- ? repoDecision
191
- : null,
192
- tasks,
193
- requiredDocs: docsEntries,
194
- };
195
- }
196
-
197
- function buildAnalysisSummaryText(context, analysis, backlog, engineResolution) {
198
- const summary = summarizeBacklog(backlog);
199
- const nextTask = selectNextTask(backlog);
200
-
201
- return [
202
- "HelloLoop 已完成接续分析。",
203
- `项目仓库:${context.repoRoot}`,
204
- `开发文档:${analysis.requiredDocs.join(", ")}`,
205
- `执行引擎:${engineResolution?.displayName || "未记录"}`,
206
- "",
207
- "当前进度:",
208
- analysis.summary.currentState,
209
- "",
210
- `任务统计:done ${summary.done} / pending ${summary.pending} / blocked ${summary.blocked}`,
211
- nextTask ? `下一任务:${nextTask.title}` : "下一任务:暂无可执行任务",
212
- "",
213
- "下一步建议:",
214
- `- npx helloloop`,
215
- `- npx helloloop --dry-run`,
216
- ].join("\n");
217
- }
218
-
219
30
  async function analyzeResolvedWorkspace(context, discovery, options = {}) {
220
31
  scaffoldIfMissing(context);
221
32
  const policy = loadPolicy(context);
@@ -268,9 +79,20 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
268
79
  outputSchemaFile: schemaFile,
269
80
  outputPrefix: `${engineResolution.engine}-analysis`,
270
81
  skipGitRepoCheck: true,
82
+ hostLease: options.hostLease || null,
271
83
  });
272
84
 
273
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
+ }
274
96
  const failureSummary = summarizeFailedAnalysisResult(
275
97
  analysisResult,
276
98
  `${engineResolution.displayName} 接续分析失败。`,
@@ -368,38 +190,6 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
368
190
  };
369
191
  }
370
192
 
371
- function buildCurrentWorkspaceDiscovery(context, docsEntries) {
372
- return {
373
- ok: true,
374
- repoRoot: context.repoRoot,
375
- docsEntries,
376
- resolvedDocs: docsEntries,
377
- resolution: {
378
- repo: {
379
- source: "current_repo",
380
- sourceLabel: "当前项目",
381
- confidence: "high",
382
- confidenceLabel: "高",
383
- path: context.repoRoot,
384
- exists: true,
385
- basis: [
386
- "已在当前项目基础上执行主线终态复核。",
387
- ],
388
- },
389
- docs: {
390
- source: "existing_state",
391
- sourceLabel: "已有 .helloloop 配置",
392
- confidence: "high",
393
- confidenceLabel: "高",
394
- entries: docsEntries,
395
- basis: [
396
- "已复用 `.helloloop/project.json` 中记录的 requiredDocs。",
397
- ],
398
- },
399
- },
400
- };
401
- }
402
-
403
193
  export async function reanalyzeCurrentWorkspace(context, options = {}) {
404
194
  const existingProjectConfig = loadProjectConfig(context);
405
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
+ }