helloloop 0.8.2 → 0.8.4
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 +44 -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 +119 -3
- package/src/engine_process_support.mjs +31 -3
- package/src/runtime_recovery.mjs +3 -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
|
它的核心原则很简单:
|
|
@@ -421,6 +425,46 @@ npx helloloop doctor --host all
|
|
|
421
425
|
npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_HOME> --gemini-home <GEMINI_HOME>
|
|
422
426
|
```
|
|
423
427
|
|
|
428
|
+
## 发布流程
|
|
429
|
+
|
|
430
|
+
`HelloLoop` 当前采用 **tag 驱动发布**:
|
|
431
|
+
|
|
432
|
+
1. 先在源码仓库完成版本号更新(`package.json` 与各宿主 manifest 保持一致)
|
|
433
|
+
2. 本地先执行:
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
npm test
|
|
437
|
+
npm pack --dry-run
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
3. 推送 Git tag:
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
git tag vX.Y.Z
|
|
444
|
+
git push origin vX.Y.Z
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
或 beta 版本:
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
git tag vX.Y.Z-beta.N
|
|
451
|
+
git push origin vX.Y.Z-beta.N
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
随后 GitHub Actions `Publish to npm` 会自动执行:
|
|
455
|
+
|
|
456
|
+
- tag / 版本一致性校验
|
|
457
|
+
- `npm test`
|
|
458
|
+
- `npm pack --dry-run`
|
|
459
|
+
- `npm publish`
|
|
460
|
+
- GitHub Release
|
|
461
|
+
|
|
462
|
+
补充说明:
|
|
463
|
+
|
|
464
|
+
- 正式版本使用 npm `latest` 渠道,beta 版本使用 npm `beta` 渠道
|
|
465
|
+
- 如果测试、版本校验或打包检查失败,npm 发布与 GitHub Release 都不会继续执行
|
|
466
|
+
- `0.8.4` 起已补齐 Codex Structured Outputs 严格 schema 兼容,并修复快退出 CLI 在 CI 中可能触发的 `stdin EPIPE` 问题,避免发布工作流被非业务性故障打断
|
|
467
|
+
|
|
424
468
|
## 宿主写入范围
|
|
425
469
|
|
|
426
470
|
为了方便排查安装 / 更新 / 卸载问题,下面是默认写入位置:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloloop",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=20"
|
package/src/analyzer.mjs
CHANGED
|
@@ -2,7 +2,14 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { summarizeBacklog, selectNextTask } from "./backlog.mjs";
|
|
4
4
|
import { nowIso, writeJson, writeText, readTextIfExists } from "./common.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
loadBacklog,
|
|
7
|
+
loadPolicy,
|
|
8
|
+
loadProjectConfig,
|
|
9
|
+
scaffoldIfMissing,
|
|
10
|
+
writeStateMarkdown,
|
|
11
|
+
writeStatus,
|
|
12
|
+
} from "./config.mjs";
|
|
6
13
|
import { createContext } from "./context.mjs";
|
|
7
14
|
import { discoverWorkspace } from "./discovery.mjs";
|
|
8
15
|
import { readDocumentPackets } from "./doc_loader.mjs";
|
|
@@ -32,6 +39,84 @@ function renderAnalysisState(context, backlog, analysis) {
|
|
|
32
39
|
].join("\n");
|
|
33
40
|
}
|
|
34
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
|
+
|
|
35
120
|
function sanitizeTask(task) {
|
|
36
121
|
return {
|
|
37
122
|
id: String(task.id || "").trim(),
|
|
@@ -186,10 +271,19 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
|
|
|
186
271
|
});
|
|
187
272
|
|
|
188
273
|
if (!analysisResult.ok) {
|
|
274
|
+
const failureSummary = summarizeFailedAnalysisResult(
|
|
275
|
+
analysisResult,
|
|
276
|
+
`${engineResolution.displayName} 接续分析失败。`,
|
|
277
|
+
);
|
|
278
|
+
persistAnalysisFailure(
|
|
279
|
+
context,
|
|
280
|
+
failureSummary,
|
|
281
|
+
runDir,
|
|
282
|
+
);
|
|
189
283
|
return {
|
|
190
284
|
ok: false,
|
|
191
285
|
code: "analysis_failed",
|
|
192
|
-
summary:
|
|
286
|
+
summary: failureSummary,
|
|
193
287
|
engineResolution,
|
|
194
288
|
discovery,
|
|
195
289
|
};
|
|
@@ -199,6 +293,11 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
|
|
|
199
293
|
try {
|
|
200
294
|
payload = JSON.parse(analysisResult.finalMessage);
|
|
201
295
|
} catch (error) {
|
|
296
|
+
persistAnalysisFailure(
|
|
297
|
+
context,
|
|
298
|
+
`${engineResolution.displayName} 分析结果无法解析为 JSON:${String(error?.message || error || "")}`,
|
|
299
|
+
runDir,
|
|
300
|
+
);
|
|
202
301
|
return {
|
|
203
302
|
ok: false,
|
|
204
303
|
code: "invalid_analysis_json",
|
|
@@ -208,7 +307,24 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
|
|
|
208
307
|
};
|
|
209
308
|
}
|
|
210
309
|
|
|
211
|
-
|
|
310
|
+
let analysis;
|
|
311
|
+
try {
|
|
312
|
+
analysis = normalizeAnalysisPayload(payload, discovery.docsEntries);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
persistAnalysisFailure(
|
|
315
|
+
context,
|
|
316
|
+
`${engineResolution.displayName} 分析结果无效:${String(error?.message || error || "")}`,
|
|
317
|
+
runDir,
|
|
318
|
+
);
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
code: "invalid_analysis_payload",
|
|
322
|
+
summary: `${engineResolution.displayName} 分析结果无效:${String(error?.message || error || "")}`,
|
|
323
|
+
engineResolution,
|
|
324
|
+
discovery,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
212
328
|
const backlog = {
|
|
213
329
|
version: 1,
|
|
214
330
|
project: analysis.project,
|
|
@@ -7,6 +7,15 @@ import {
|
|
|
7
7
|
resolveVerifyShellInvocation,
|
|
8
8
|
} from "./shell_invocation.mjs";
|
|
9
9
|
|
|
10
|
+
export function isIgnorableStdinError(error) {
|
|
11
|
+
const code = String(error?.code || "").trim();
|
|
12
|
+
const message = String(error?.message || "").toLowerCase();
|
|
13
|
+
return code === "EPIPE"
|
|
14
|
+
|| code === "ERR_STREAM_DESTROYED"
|
|
15
|
+
|| message.includes("broken pipe")
|
|
16
|
+
|| message.includes("write after end");
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
export function runChild(command, args, options = {}) {
|
|
11
20
|
return new Promise((resolve) => {
|
|
12
21
|
const child = spawn(command, args, {
|
|
@@ -98,10 +107,29 @@ export function runChild(command, args, options = {}) {
|
|
|
98
107
|
emitHeartbeat("running");
|
|
99
108
|
});
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
child.stdin.on("error", (error) => {
|
|
111
|
+
if (isIgnorableStdinError(error)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
stderr = [
|
|
115
|
+
stderr.trim(),
|
|
116
|
+
`[HelloLoop stdin] ${String(error?.stack || error || "")}`,
|
|
117
|
+
].filter(Boolean).join("\n");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
if (options.stdin) {
|
|
122
|
+
child.stdin.write(options.stdin);
|
|
123
|
+
}
|
|
124
|
+
child.stdin.end();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!isIgnorableStdinError(error)) {
|
|
127
|
+
stderr = [
|
|
128
|
+
stderr.trim(),
|
|
129
|
+
`[HelloLoop stdin] ${String(error?.stack || error || "")}`,
|
|
130
|
+
].filter(Boolean).join("\n");
|
|
131
|
+
}
|
|
103
132
|
}
|
|
104
|
-
child.stdin.end();
|
|
105
133
|
|
|
106
134
|
child.on("error", (error) => {
|
|
107
135
|
if (heartbeatTimer) {
|
package/src/runtime_recovery.mjs
CHANGED
|
@@ -21,6 +21,8 @@ const HARD_STOP_MATCHERS = [
|
|
|
21
21
|
"400 bad request",
|
|
22
22
|
"bad request",
|
|
23
23
|
"invalid request",
|
|
24
|
+
"invalid schema",
|
|
25
|
+
"invalid_json_schema",
|
|
24
26
|
"invalid argument",
|
|
25
27
|
"invalid_argument",
|
|
26
28
|
"failed to parse",
|
|
@@ -28,6 +30,7 @@ const HARD_STOP_MATCHERS = [
|
|
|
28
30
|
"malformed",
|
|
29
31
|
"schema validation",
|
|
30
32
|
"json schema",
|
|
33
|
+
"response_format",
|
|
31
34
|
"unexpected argument",
|
|
32
35
|
"unknown option",
|
|
33
36
|
],
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
"required": [
|
|
6
6
|
"project",
|
|
7
7
|
"summary",
|
|
8
|
+
"constraints",
|
|
9
|
+
"requestInterpretation",
|
|
10
|
+
"repoDecision",
|
|
8
11
|
"tasks"
|
|
9
12
|
],
|
|
10
13
|
"properties": {
|
|
@@ -45,13 +48,19 @@
|
|
|
45
48
|
}
|
|
46
49
|
},
|
|
47
50
|
"constraints": {
|
|
48
|
-
"type":
|
|
51
|
+
"type": [
|
|
52
|
+
"array",
|
|
53
|
+
"null"
|
|
54
|
+
],
|
|
49
55
|
"items": {
|
|
50
56
|
"type": "string"
|
|
51
57
|
}
|
|
52
58
|
},
|
|
53
59
|
"requestInterpretation": {
|
|
54
|
-
"type":
|
|
60
|
+
"type": [
|
|
61
|
+
"object",
|
|
62
|
+
"null"
|
|
63
|
+
],
|
|
55
64
|
"additionalProperties": false,
|
|
56
65
|
"required": [
|
|
57
66
|
"summary",
|
|
@@ -78,7 +87,10 @@
|
|
|
78
87
|
}
|
|
79
88
|
},
|
|
80
89
|
"repoDecision": {
|
|
81
|
-
"type":
|
|
90
|
+
"type": [
|
|
91
|
+
"object",
|
|
92
|
+
"null"
|
|
93
|
+
],
|
|
82
94
|
"additionalProperties": false,
|
|
83
95
|
"required": [
|
|
84
96
|
"compatibility",
|
|
@@ -122,7 +134,9 @@
|
|
|
122
134
|
"goal",
|
|
123
135
|
"docs",
|
|
124
136
|
"paths",
|
|
125
|
-
"acceptance"
|
|
137
|
+
"acceptance",
|
|
138
|
+
"dependsOn",
|
|
139
|
+
"verify"
|
|
126
140
|
],
|
|
127
141
|
"properties": {
|
|
128
142
|
"id": {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"summary",
|
|
8
8
|
"acceptanceChecks",
|
|
9
9
|
"missing",
|
|
10
|
+
"blockerReason",
|
|
10
11
|
"nextAction"
|
|
11
12
|
],
|
|
12
13
|
"properties": {
|
|
@@ -60,7 +61,10 @@
|
|
|
60
61
|
}
|
|
61
62
|
},
|
|
62
63
|
"blockerReason": {
|
|
63
|
-
"type":
|
|
64
|
+
"type": [
|
|
65
|
+
"string",
|
|
66
|
+
"null"
|
|
67
|
+
]
|
|
64
68
|
},
|
|
65
69
|
"nextAction": {
|
|
66
70
|
"type": "string",
|