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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +67 -0
- 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 +60 -154
- package/src/analyzer_support.mjs +232 -0
- package/src/cli.mjs +8 -0
- package/src/cli_analyze_command.mjs +25 -4
- package/src/cli_args.mjs +5 -0
- package/src/cli_command_handlers.mjs +78 -0
- package/src/completion_review.mjs +2 -0
- package/src/context.mjs +6 -0
- package/src/engine_process_support.mjs +63 -3
- 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/runtime_recovery.mjs +3 -0
- package/src/supervisor_runtime.mjs +285 -0
- package/src/verify_runner.mjs +84 -0
- package/templates/analysis-output.schema.json +18 -4
- package/templates/task-review-output.schema.json +5 -1
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
|
为了方便排查安装 / 更新 / 卸载问题,下面是默认写入位置:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloloop",
|
|
3
|
-
"version": "0.8.
|
|
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
|
|
3
|
+
import { summarizeBacklog } from "./backlog.mjs";
|
|
4
4
|
import { nowIso, writeJson, writeText, readTextIfExists } from "./common.mjs";
|
|
5
|
-
import {
|
|
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:
|
|
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
|
-
|
|
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()));
|