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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "HelloLoop 的 Claude Code 原生插件元数据,用于多 CLI 宿主分发。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件,Codex 路径为首发与参考实现。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # HelloLoop
2
2
 
3
+ > [!WARNING]
4
+ > **风险提示**
5
+ > 使用 `HelloLoop` 执行持续 / 持久型任务仍存在不可完全控制的风险。虽然项目已经加入高危行为管控与防护策略,但仍不能排除误删数据、误改文件、异常覆盖或其他不可预期后果。请在使用前务必先备份重要数据,并自行通过人工或借助 AI 对 `HelloLoop` 的安全风险做评估与审计;因使用本插件造成的数据丢失或其他损失,项目方不承担责任。
6
+
3
7
  `HelloLoop` 是一个面向 `Codex CLI`、`Claude Code`、`Gemini CLI` 的多宿主开发工作流插件,用来把“根据开发文档持续接续开发、测试、验收,直到最终目标完成”收敛成一条统一、可确认、可追踪的标准流程。
4
8
 
5
9
  它的核心原则很简单:
@@ -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
  为了方便排查安装 / 更新 / 卸载问题,下面是默认写入位置:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "HelloLoop 的 Claude Code 原生插件元数据,用于多 CLI 宿主分发。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "HelloLoop 的 Gemini CLI 原生扩展,用于按开发文档接续推进项目开发。",
5
5
  "contextFileName": "GEMINI.md",
6
6
  "excludeTools": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.8.2",
3
+ "version": "0.8.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 { loadPolicy, loadProjectConfig, scaffoldIfMissing, writeStateMarkdown, writeStatus } from "./config.mjs";
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: analysisResult.stderr || analysisResult.stdout || `${engineResolution.displayName} 接续分析失败。`,
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
- const analysis = normalizeAnalysisPayload(payload, discovery.docsEntries);
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
- if (options.stdin) {
102
- child.stdin.write(options.stdin);
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) {
@@ -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": "array",
51
+ "type": [
52
+ "array",
53
+ "null"
54
+ ],
49
55
  "items": {
50
56
  "type": "string"
51
57
  }
52
58
  },
53
59
  "requestInterpretation": {
54
- "type": "object",
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": "object",
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": "string"
64
+ "type": [
65
+ "string",
66
+ "null"
67
+ ]
64
68
  },
65
69
  "nextAction": {
66
70
  "type": "string",