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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +66 -1
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/src/analyzer.mjs +20 -230
- package/src/analyzer_support.mjs +232 -0
- package/src/cli.mjs +56 -32
- package/src/cli_analyze_command.mjs +10 -7
- package/src/cli_args.mjs +5 -0
- package/src/cli_command_handlers.mjs +31 -0
- package/src/completion_review.mjs +2 -0
- package/src/context.mjs +6 -0
- package/src/engine_process_support.mjs +32 -0
- package/src/engine_selection_settings.mjs +12 -0
- package/src/host_lease.mjs +204 -0
- package/src/process.mjs +7 -654
- package/src/runner_execute_task.mjs +55 -1
- package/src/runner_execution_support.mjs +14 -0
- package/src/runner_status.mjs +12 -1
- package/src/runtime_engine_support.mjs +342 -0
- package/src/runtime_engine_task.mjs +395 -0
- package/src/supervisor_runtime.mjs +314 -0
- package/src/terminal_session_limits.mjs +394 -0
- package/src/verify_runner.mjs +84 -0
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.
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloloop",
|
|
3
|
-
"version": "0.8.
|
|
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
|
|
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
|
+
}
|