helloloop 0.3.1 → 0.7.0

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.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +4 -4
  3. package/README.md +194 -81
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +17 -13
  6. package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -5
  7. package/hosts/gemini/extension/GEMINI.md +14 -7
  8. package/hosts/gemini/extension/commands/helloloop.toml +17 -12
  9. package/hosts/gemini/extension/gemini-extension.json +1 -1
  10. package/package.json +2 -2
  11. package/skills/helloloop/SKILL.md +18 -7
  12. package/src/analyze_confirmation.mjs +29 -5
  13. package/src/analyze_prompt.mjs +5 -1
  14. package/src/analyze_user_input.mjs +20 -2
  15. package/src/analyzer.mjs +130 -43
  16. package/src/cli.mjs +32 -492
  17. package/src/cli_analyze_command.mjs +248 -0
  18. package/src/cli_args.mjs +106 -0
  19. package/src/cli_command_handlers.mjs +120 -0
  20. package/src/cli_context.mjs +31 -0
  21. package/src/cli_render.mjs +70 -0
  22. package/src/cli_support.mjs +11 -14
  23. package/src/completion_review.mjs +243 -0
  24. package/src/config.mjs +51 -0
  25. package/src/discovery.mjs +21 -2
  26. package/src/discovery_prompt.mjs +2 -27
  27. package/src/email_notification.mjs +343 -0
  28. package/src/engine_metadata.mjs +79 -0
  29. package/src/engine_process_support.mjs +294 -0
  30. package/src/engine_selection.mjs +335 -0
  31. package/src/engine_selection_failure.mjs +51 -0
  32. package/src/engine_selection_messages.mjs +119 -0
  33. package/src/engine_selection_probe.mjs +78 -0
  34. package/src/engine_selection_prompt.mjs +48 -0
  35. package/src/engine_selection_settings.mjs +104 -0
  36. package/src/global_config.mjs +21 -0
  37. package/src/guardrails.mjs +15 -4
  38. package/src/install.mjs +6 -405
  39. package/src/install_claude.mjs +189 -0
  40. package/src/install_codex.mjs +114 -0
  41. package/src/install_gemini.mjs +43 -0
  42. package/src/install_shared.mjs +138 -0
  43. package/src/process.mjs +567 -100
  44. package/src/prompt.mjs +9 -5
  45. package/src/prompt_session.mjs +40 -0
  46. package/src/runner.mjs +3 -341
  47. package/src/runner_execute_task.mjs +255 -0
  48. package/src/runner_execution_support.mjs +146 -0
  49. package/src/runner_loop.mjs +106 -0
  50. package/src/runner_once.mjs +29 -0
  51. package/src/runner_status.mjs +104 -0
  52. package/src/runtime_recovery.mjs +302 -0
  53. package/src/shell_invocation.mjs +16 -0
  54. package/templates/analysis-output.schema.json +0 -1
  55. package/templates/policy.template.json +25 -0
  56. package/templates/project.template.json +2 -0
  57. package/templates/task-review-output.schema.json +70 -0
@@ -0,0 +1,146 @@
1
+ import {
2
+ rememberEngineSelection,
3
+ resolveEngineSelection,
4
+ } from "./engine_selection.mjs";
5
+ import { nowIso } from "./common.mjs";
6
+ import {
7
+ loadBacklog,
8
+ loadPolicy,
9
+ loadProjectConfig,
10
+ loadRepoStateText,
11
+ loadVerifyCommands,
12
+ saveBacklog,
13
+ } from "./config.mjs";
14
+ import { getTask, selectNextTask, unresolvedDependencies, updateTask } from "./backlog.mjs";
15
+ import { makeRunDir } from "./runner_status.mjs";
16
+
17
+ function resolveTask(backlog, options) {
18
+ if (options.taskId) {
19
+ const task = getTask(backlog, options.taskId);
20
+ if (!task) {
21
+ throw new Error(`未找到任务:${options.taskId}`);
22
+ }
23
+ return task;
24
+ }
25
+ return selectNextTask(backlog, options);
26
+ }
27
+
28
+ export async function resolveExecutionSetup(context, options = {}) {
29
+ const policy = loadPolicy(context);
30
+ const projectConfig = loadProjectConfig(context);
31
+ const backlog = loadBacklog(context);
32
+ const task = resolveTask(backlog, options);
33
+ if (!task) {
34
+ return {
35
+ idleResult: { ok: true, kind: "idle", task: null },
36
+ };
37
+ }
38
+
39
+ const unresolved = unresolvedDependencies(backlog, task);
40
+ if (unresolved.length) {
41
+ throw new Error(`任务 ${task.id} 仍有未完成依赖:${unresolved.join(", ")}`);
42
+ }
43
+
44
+ const verifyCommands = Array.isArray(task.verify) && task.verify.length
45
+ ? task.verify
46
+ : loadVerifyCommands(context);
47
+ const maxAttemptsPerStrategy = Math.max(1, Number(options.maxAttempts || policy.maxTaskAttempts || 1));
48
+ const configuredStrategies = Math.max(1, Number(options.maxStrategies || policy.maxTaskStrategies || 1));
49
+ const engineResolution = options.engineResolution?.ok
50
+ ? options.engineResolution
51
+ : await resolveEngineSelection({
52
+ context,
53
+ policy,
54
+ options,
55
+ interactive: !options.yes,
56
+ });
57
+
58
+ return {
59
+ context,
60
+ options,
61
+ policy,
62
+ backlog,
63
+ projectConfig,
64
+ repoStateText: loadRepoStateText(context),
65
+ task,
66
+ verifyCommands,
67
+ runDir: makeRunDir(context, task.id),
68
+ requiredDocs: [...(projectConfig.requiredDocs || []), ...(options.requiredDocs || [])],
69
+ constraints: [...(projectConfig.constraints || []), ...(options.constraints || [])],
70
+ maxAttemptsPerStrategy,
71
+ maxStrategies: policy.stopOnFailure ? 1 : configuredStrategies,
72
+ engineResolution,
73
+ };
74
+ }
75
+
76
+ function updateTaskAndBuildResult(execution, status, result) {
77
+ updateTask(execution.backlog, execution.task.id, {
78
+ status,
79
+ finishedAt: nowIso(),
80
+ lastFailure: result.ok ? "" : (result.summary || ""),
81
+ attempts: result.attempts,
82
+ });
83
+ saveBacklog(execution.context, execution.backlog);
84
+ return result;
85
+ }
86
+
87
+ export function buildFailureResult(execution, kind, summary, attempts, engineResolution) {
88
+ return updateTaskAndBuildResult(execution, "failed", {
89
+ ok: false,
90
+ kind,
91
+ task: execution.task,
92
+ runDir: execution.runDir,
93
+ summary,
94
+ attempts,
95
+ engineResolution,
96
+ });
97
+ }
98
+
99
+ export function buildBlockedResult(execution, summary, attempts, engineResolution) {
100
+ return updateTaskAndBuildResult(execution, "blocked", {
101
+ ok: false,
102
+ kind: "task-blocked",
103
+ task: execution.task,
104
+ runDir: execution.runDir,
105
+ summary,
106
+ attempts,
107
+ engineResolution,
108
+ });
109
+ }
110
+
111
+ export function buildDoneResult(execution, finalMessage, attempts, engineResolution) {
112
+ return updateTaskAndBuildResult(execution, "done", {
113
+ ok: true,
114
+ kind: "done",
115
+ task: execution.task,
116
+ runDir: execution.runDir,
117
+ finalMessage,
118
+ attempts,
119
+ engineResolution,
120
+ });
121
+ }
122
+
123
+ export function recordFailure(failureHistory, strategyIndex, attemptIndex, kind, summary) {
124
+ failureHistory.push({
125
+ strategyIndex,
126
+ attemptIndex,
127
+ kind,
128
+ summary,
129
+ });
130
+ }
131
+
132
+ export function buildAttemptState(runDir, strategyIndex, attemptIndex, makeAttemptDir) {
133
+ return {
134
+ strategyIndex,
135
+ attemptIndex,
136
+ attemptDir: makeAttemptDir(runDir, strategyIndex, attemptIndex),
137
+ };
138
+ }
139
+
140
+ export function bumpFailureForNextStrategy(previousFailure, maxAttemptsPerStrategy) {
141
+ return [
142
+ previousFailure,
143
+ "",
144
+ `上一种策略已连续失败 ${maxAttemptsPerStrategy} 次。下一轮必须明确更换实现或排查思路,不能重复原路径。`,
145
+ ].join("\n").trim();
146
+ }
@@ -0,0 +1,106 @@
1
+ import { selectNextTask, summarizeBacklog } from "./backlog.mjs";
2
+ import { loadBacklog, loadPolicy } from "./config.mjs";
3
+ import { reanalyzeCurrentWorkspace } from "./analyzer.mjs";
4
+ import { runOnce } from "./runner_once.mjs";
5
+
6
+ function shouldRunMainlineReanalysis(options, summary, reanalysisPasses, maxReanalysisPasses) {
7
+ return Boolean(options.fullAutoMainline)
8
+ && summary.pending === 0
9
+ && summary.inProgress === 0
10
+ && summary.failed === 0
11
+ && summary.blocked === 0
12
+ && reanalysisPasses < maxReanalysisPasses;
13
+ }
14
+
15
+ function buildMainlineReopenedResult(nextTask, engineResolution) {
16
+ return {
17
+ ok: true,
18
+ kind: "mainline-reopened",
19
+ task: null,
20
+ summary: [
21
+ "主线终态复核发现仍有剩余工作,已自动重建 backlog 并继续推进。",
22
+ `下一任务:${nextTask.title}`,
23
+ ].join("\n"),
24
+ engineResolution,
25
+ };
26
+ }
27
+
28
+ function buildMainlineCompleteResult(engineResolution) {
29
+ return {
30
+ ok: true,
31
+ kind: "mainline-complete",
32
+ task: null,
33
+ summary: "主线终态复核通过:开发文档目标已闭合,没有发现新的剩余任务。",
34
+ engineResolution,
35
+ };
36
+ }
37
+
38
+ function buildMainlineFailureResult(continuation, engineResolution) {
39
+ return {
40
+ ok: false,
41
+ kind: "mainline-reanalysis-failed",
42
+ task: null,
43
+ summary: continuation.summary || "主线终态复核失败。",
44
+ engineResolution,
45
+ };
46
+ }
47
+
48
+ export async function runLoop(context, options = {}) {
49
+ const policy = loadPolicy(context);
50
+ const explicitMaxTasks = Number(options.maxTasks);
51
+ const maxTasks = Number.isFinite(explicitMaxTasks) && explicitMaxTasks > 0
52
+ ? explicitMaxTasks
53
+ : (options.fullAutoMainline ? Number.POSITIVE_INFINITY : Math.max(1, Number(policy.maxLoopTasks || 1)));
54
+ const maxReanalysisPasses = Math.max(0, Number(options.maxReanalysisPasses || policy.maxReanalysisPasses || 0));
55
+ const results = [];
56
+ let engineResolution = options.engineResolution || null;
57
+ let completedTasks = 0;
58
+ let reanalysisPasses = 0;
59
+
60
+ while (completedTasks < maxTasks) {
61
+ const result = await runOnce(context, { ...options, engineResolution });
62
+ results.push(result);
63
+ if (result.engineResolution?.ok) {
64
+ engineResolution = result.engineResolution;
65
+ }
66
+ if (options.dryRun || !result.ok || !result.task) {
67
+ break;
68
+ }
69
+
70
+ completedTasks += 1;
71
+ const backlog = loadBacklog(context);
72
+ if (selectNextTask(backlog, options)) {
73
+ continue;
74
+ }
75
+
76
+ const summary = summarizeBacklog(backlog);
77
+ if (!shouldRunMainlineReanalysis(options, summary, reanalysisPasses, maxReanalysisPasses)) {
78
+ break;
79
+ }
80
+
81
+ reanalysisPasses += 1;
82
+ const continuation = await reanalyzeCurrentWorkspace(context, {
83
+ ...options,
84
+ engineResolution,
85
+ yes: true,
86
+ });
87
+ if (continuation.engineResolution?.ok) {
88
+ engineResolution = continuation.engineResolution;
89
+ }
90
+ if (!continuation.ok) {
91
+ results.push(buildMainlineFailureResult(continuation, engineResolution));
92
+ break;
93
+ }
94
+
95
+ const continuedNextTask = selectNextTask(loadBacklog(context), options);
96
+ if (continuedNextTask) {
97
+ results.push(buildMainlineReopenedResult(continuedNextTask, engineResolution));
98
+ continue;
99
+ }
100
+
101
+ results.push(buildMainlineCompleteResult(engineResolution));
102
+ break;
103
+ }
104
+
105
+ return results;
106
+ }
@@ -0,0 +1,29 @@
1
+ import { selectNextTask, summarizeBacklog } from "./backlog.mjs";
2
+ import { loadBacklog, writeStateMarkdown, writeStatus } from "./config.mjs";
3
+ import { executeSingleTask } from "./runner_execute_task.mjs";
4
+ import { renderStatusMarkdown } from "./runner_status.mjs";
5
+
6
+ export async function runOnce(context, options = {}) {
7
+ const result = await executeSingleTask(context, options);
8
+ const backlog = loadBacklog(context);
9
+ const summary = summarizeBacklog(backlog);
10
+ const nextTask = selectNextTask(backlog, options);
11
+
12
+ writeStatus(context, {
13
+ ok: result.ok,
14
+ stage: result.kind,
15
+ taskId: result.task?.id || null,
16
+ taskTitle: result.task?.title || "",
17
+ runDir: result.runDir || "",
18
+ summary,
19
+ message: result.summary || result.finalMessage || "",
20
+ });
21
+ writeStateMarkdown(context, renderStatusMarkdown(context, {
22
+ summary,
23
+ currentTask: result.task,
24
+ lastResult: result.ok ? "本轮成功" : (result.summary || result.kind),
25
+ nextTask,
26
+ }));
27
+
28
+ return result;
29
+ }
@@ -0,0 +1,104 @@
1
+ import path from "node:path";
2
+
3
+ import { sanitizeId, tailText, timestampForFile } from "./common.mjs";
4
+ import { renderTaskSummary, selectNextTask, summarizeBacklog } from "./backlog.mjs";
5
+ import { loadBacklog } from "./config.mjs";
6
+
7
+ export function makeRunDir(context, taskId) {
8
+ return path.join(context.runsDir, `${timestampForFile()}-${sanitizeId(taskId)}`);
9
+ }
10
+
11
+ export function makeAttemptDir(runDir, strategyIndex, attemptIndex) {
12
+ return path.join(
13
+ runDir,
14
+ `strategy-${String(strategyIndex).padStart(2, "0")}-attempt-${String(attemptIndex).padStart(2, "0")}`,
15
+ );
16
+ }
17
+
18
+ export function isHardStopFailure(kind, summary) {
19
+ const normalized = String(summary || "").toLowerCase();
20
+ if (!normalized) {
21
+ return false;
22
+ }
23
+ if (kind === "engine" && normalized.includes("enoent")) {
24
+ return true;
25
+ }
26
+
27
+ return [
28
+ "command not found",
29
+ "is not recognized",
30
+ "无法将",
31
+ "找不到路径",
32
+ "no such file or directory",
33
+ "permission denied",
34
+ "access is denied",
35
+ ].some((signal) => normalized.includes(signal));
36
+ }
37
+
38
+ export function buildFailureSummary(kind, payload) {
39
+ if (kind !== "engine") {
40
+ return payload.summary;
41
+ }
42
+
43
+ return [
44
+ `${payload.displayName} 执行失败,退出码:${payload.code}`,
45
+ payload.recoverySummary || "",
46
+ "",
47
+ "stdout 尾部:",
48
+ tailText(payload.stdout, 60),
49
+ "",
50
+ "stderr 尾部:",
51
+ tailText(payload.stderr, 60),
52
+ ].join("\n").trim();
53
+ }
54
+
55
+ export function buildExhaustedSummary({
56
+ failureHistory,
57
+ maxStrategies,
58
+ maxAttemptsPerStrategy,
59
+ }) {
60
+ const lastFailure = failureHistory.at(-1)?.summary || "未知失败。";
61
+ return [
62
+ `已按 Ralph Loop 执行 ${maxStrategies} 轮策略、每轮最多 ${maxAttemptsPerStrategy} 次重试,当前任务仍未收敛。`,
63
+ "",
64
+ "最后一次失败信息:",
65
+ lastFailure,
66
+ ].join("\n").trim();
67
+ }
68
+
69
+ export function renderStatusMarkdown(context, { summary, currentTask, lastResult, nextTask }) {
70
+ return [
71
+ "## 当前状态",
72
+ `- backlog 文件:${context.backlogFile.replaceAll("\\", "/")}`,
73
+ `- 总任务数:${summary.total}`,
74
+ `- 已完成:${summary.done}`,
75
+ `- 待处理:${summary.pending}`,
76
+ `- 进行中:${summary.inProgress}`,
77
+ `- 失败:${summary.failed}`,
78
+ `- 阻塞:${summary.blocked}`,
79
+ `- 当前任务:${currentTask ? currentTask.title : "无"}`,
80
+ `- 最近结果:${lastResult || "暂无"}`,
81
+ `- 下一建议:${nextTask ? nextTask.title : "暂无可执行任务"}`,
82
+ ].join("\n");
83
+ }
84
+
85
+ export function renderStatusText(context, options = {}) {
86
+ const backlog = loadBacklog(context);
87
+ const summary = summarizeBacklog(backlog);
88
+ const nextTask = selectNextTask(backlog, options);
89
+
90
+ return [
91
+ "HelloLoop 状态",
92
+ "============",
93
+ `仓库:${context.repoRoot}`,
94
+ `总任务:${summary.total}`,
95
+ `已完成:${summary.done}`,
96
+ `待处理:${summary.pending}`,
97
+ `进行中:${summary.inProgress}`,
98
+ `失败:${summary.failed}`,
99
+ `阻塞:${summary.blocked}`,
100
+ "",
101
+ nextTask ? "下一任务:" : "下一任务:无",
102
+ nextTask ? renderTaskSummary(nextTask) : "",
103
+ ].filter(Boolean).join("\n");
104
+ }
@@ -0,0 +1,302 @@
1
+ import { getEngineDisplayName } from "./engine_metadata.mjs";
2
+ import { tailText } from "./common.mjs";
3
+
4
+ const defaultRuntimeRecoveryPolicy = {
5
+ enabled: true,
6
+ heartbeatIntervalSeconds: 60,
7
+ stallWarningSeconds: 900,
8
+ maxIdleSeconds: 2700,
9
+ killGraceSeconds: 10,
10
+ healthProbeTimeoutSeconds: 120,
11
+ hardRetryDelaysSeconds: [900, 900, 900, 900, 900],
12
+ softRetryDelaysSeconds: [900, 900, 900, 900, 900, 1800, 1800, 3600, 5400, 7200, 9000, 10800],
13
+ };
14
+
15
+ const HARD_STOP_MATCHERS = [
16
+ {
17
+ code: "invalid_request",
18
+ reason: "当前错误更像请求、参数、协议或输出格式问题,需要人工复核调用与提示词。",
19
+ patterns: [
20
+ " 400 ",
21
+ "400 bad request",
22
+ "bad request",
23
+ "invalid request",
24
+ "invalid argument",
25
+ "invalid_argument",
26
+ "failed to parse",
27
+ "parse error",
28
+ "malformed",
29
+ "schema validation",
30
+ "json schema",
31
+ "unexpected argument",
32
+ "unknown option",
33
+ ],
34
+ },
35
+ {
36
+ code: "auth",
37
+ reason: "当前错误更像登录、鉴权、订阅或权限问题,需要等待环境恢复或人工修复。",
38
+ patterns: [
39
+ "401",
40
+ "403",
41
+ "unauthorized",
42
+ "forbidden",
43
+ "not authenticated",
44
+ "authentication",
45
+ "login",
46
+ "sign in",
47
+ "api key",
48
+ "token",
49
+ "subscription",
50
+ "insufficient permissions",
51
+ ],
52
+ },
53
+ {
54
+ code: "billing",
55
+ reason: "当前错误更像额度、余额、支付或账单问题,短时间内通常不会自行消失。",
56
+ patterns: [
57
+ "payment required",
58
+ "billing",
59
+ "insufficient balance",
60
+ "credit",
61
+ "quota exceeded",
62
+ "hard limit",
63
+ "balance",
64
+ ],
65
+ },
66
+ {
67
+ code: "environment",
68
+ reason: "当前错误更像本地 CLI 缺失、权限不足或文件系统问题,需要人工修复环境。",
69
+ patterns: [
70
+ "command not found",
71
+ "is not recognized",
72
+ "enoent",
73
+ "no such file or directory",
74
+ "permission denied",
75
+ "access is denied",
76
+ ],
77
+ },
78
+ ];
79
+
80
+ const SOFT_STOP_MATCHERS = [
81
+ {
82
+ code: "rate_limit",
83
+ reason: "当前引擎可能遇到配额、限流或临时容量不足。",
84
+ patterns: [
85
+ "429",
86
+ "rate limit",
87
+ "too many requests",
88
+ "quota",
89
+ "usage limit",
90
+ "capacity",
91
+ "overloaded",
92
+ "try again later",
93
+ ],
94
+ },
95
+ {
96
+ code: "server",
97
+ reason: "当前引擎可能遇到临时服务端错误。",
98
+ patterns: [
99
+ "500",
100
+ "501",
101
+ "502",
102
+ "503",
103
+ "504",
104
+ "internal server error",
105
+ "bad gateway",
106
+ "service unavailable",
107
+ "gateway timeout",
108
+ "server error",
109
+ "upstream",
110
+ "temporarily unavailable",
111
+ ],
112
+ },
113
+ {
114
+ code: "network",
115
+ reason: "当前引擎可能遇到临时网络、连接或结果流中断。",
116
+ patterns: [
117
+ "network error",
118
+ "fetch failed",
119
+ "econnreset",
120
+ "etimedout",
121
+ "timed out",
122
+ "timeout",
123
+ "connection reset",
124
+ "connection aborted",
125
+ "connection closed",
126
+ "stream closed",
127
+ "socket hang up",
128
+ "transport error",
129
+ "broken pipe",
130
+ "http2",
131
+ ],
132
+ },
133
+ ];
134
+
135
+ function normalizeSeconds(value, fallback) {
136
+ const numberValue = Number(value);
137
+ if (!Number.isFinite(numberValue) || numberValue < 0) {
138
+ return fallback;
139
+ }
140
+ return numberValue;
141
+ }
142
+
143
+ function normalizeSecondsList(value, fallback) {
144
+ if (!Array.isArray(value) || !value.length) {
145
+ return fallback;
146
+ }
147
+ const normalized = value
148
+ .map((item) => normalizeSeconds(item, -1))
149
+ .filter((item) => item >= 0);
150
+ return normalized.length ? normalized : fallback;
151
+ }
152
+
153
+ function normalizeText(value) {
154
+ return String(value || "").toLowerCase();
155
+ }
156
+
157
+ function hasMatcher(normalizedText, matcher) {
158
+ return matcher.patterns.some((pattern) => normalizedText.includes(String(pattern).toLowerCase()));
159
+ }
160
+
161
+ export function resolveRuntimeRecoveryPolicy(policy = {}) {
162
+ const configured = policy?.runtimeRecovery || {};
163
+ return {
164
+ enabled: configured.enabled !== false,
165
+ heartbeatIntervalSeconds: normalizeSeconds(
166
+ configured.heartbeatIntervalSeconds,
167
+ defaultRuntimeRecoveryPolicy.heartbeatIntervalSeconds,
168
+ ),
169
+ stallWarningSeconds: normalizeSeconds(
170
+ configured.stallWarningSeconds,
171
+ defaultRuntimeRecoveryPolicy.stallWarningSeconds,
172
+ ),
173
+ maxIdleSeconds: normalizeSeconds(
174
+ configured.maxIdleSeconds,
175
+ defaultRuntimeRecoveryPolicy.maxIdleSeconds,
176
+ ),
177
+ killGraceSeconds: normalizeSeconds(
178
+ configured.killGraceSeconds,
179
+ defaultRuntimeRecoveryPolicy.killGraceSeconds,
180
+ ),
181
+ healthProbeTimeoutSeconds: normalizeSeconds(
182
+ configured.healthProbeTimeoutSeconds,
183
+ defaultRuntimeRecoveryPolicy.healthProbeTimeoutSeconds,
184
+ ),
185
+ hardRetryDelaysSeconds: normalizeSecondsList(
186
+ configured.hardRetryDelaysSeconds,
187
+ defaultRuntimeRecoveryPolicy.hardRetryDelaysSeconds,
188
+ ),
189
+ softRetryDelaysSeconds: normalizeSecondsList(
190
+ configured.softRetryDelaysSeconds,
191
+ defaultRuntimeRecoveryPolicy.softRetryDelaysSeconds,
192
+ ),
193
+ };
194
+ }
195
+
196
+ export function getRuntimeRecoverySchedule(recoveryPolicy, family = "soft") {
197
+ return family === "hard"
198
+ ? recoveryPolicy.hardRetryDelaysSeconds
199
+ : recoveryPolicy.softRetryDelaysSeconds;
200
+ }
201
+
202
+ export function selectRuntimeRecoveryDelayMs(recoveryPolicy, family, nextRecoveryIndex) {
203
+ const delays = getRuntimeRecoverySchedule(recoveryPolicy, family);
204
+ const offset = Math.max(0, Number(nextRecoveryIndex || 1) - 1);
205
+ const seconds = delays[offset] ?? null;
206
+ return seconds == null ? -1 : Math.max(0, seconds) * 1000;
207
+ }
208
+
209
+ export function classifyRuntimeRecoveryFailure({ result = {} } = {}) {
210
+ const normalized = normalizeText([
211
+ result.stderr,
212
+ result.stdout,
213
+ result.finalMessage,
214
+ result.watchdogReason,
215
+ ].filter(Boolean).join("\n"));
216
+
217
+ if (result.watchdogTriggered || result.idleTimeout) {
218
+ return {
219
+ code: "watchdog_idle",
220
+ family: "soft",
221
+ reason: "当前进程长时间没有可见进展,HelloLoop 将按软阻塞策略继续探测并恢复。",
222
+ };
223
+ }
224
+
225
+ for (const matcher of HARD_STOP_MATCHERS) {
226
+ if (hasMatcher(normalized, matcher)) {
227
+ return {
228
+ code: matcher.code,
229
+ family: "hard",
230
+ reason: matcher.reason,
231
+ };
232
+ }
233
+ }
234
+
235
+ for (const matcher of SOFT_STOP_MATCHERS) {
236
+ if (hasMatcher(normalized, matcher)) {
237
+ return {
238
+ code: matcher.code,
239
+ family: "soft",
240
+ reason: matcher.reason,
241
+ };
242
+ }
243
+ }
244
+
245
+ return {
246
+ code: "unknown_failure",
247
+ family: "soft",
248
+ reason: "当前错误类型无法稳定归类,HelloLoop 将按软阻塞策略持续探测并恢复。",
249
+ };
250
+ }
251
+
252
+ export function buildEngineHealthProbePrompt(engine) {
253
+ return [
254
+ "HELLOLOOP_ENGINE_HEALTH_PROBE",
255
+ `当前只做 ${getEngineDisplayName(engine)} 引擎健康探测。`,
256
+ "禁止修改仓库、禁止执行开发任务、禁止输出解释。",
257
+ "只需确认自己当前能正常接收请求并返回简短结果。",
258
+ "若当前可用,请直接回复:HELLOLOOP_ENGINE_OK",
259
+ ].join("\n");
260
+ }
261
+
262
+ export function buildRuntimeRecoveryPrompt({
263
+ basePrompt,
264
+ engine,
265
+ phaseLabel,
266
+ failure,
267
+ result = {},
268
+ nextRecoveryIndex,
269
+ maxRecoveries,
270
+ }) {
271
+ return [
272
+ basePrompt,
273
+ "",
274
+ "## HelloLoop 自动恢复上下文",
275
+ `- 执行引擎:${getEngineDisplayName(engine)}`,
276
+ `- 当前阶段:${phaseLabel}`,
277
+ `- 自动恢复序号:${nextRecoveryIndex}/${maxRecoveries}`,
278
+ `- 恢复原因:${failure?.reason || "当前引擎在上一轮执行中断,需要在同一主线下继续恢复。"}`,
279
+ "",
280
+ "你必须把当前仓库视为唯一事实源,直接复用已经完成的修改、进度和中间结果。",
281
+ "不要从头重做,不要另起一套实现,不要等待用户,不要把“下一步建议”当成交付。",
282
+ "先快速检查仓库当前状态与最近失败点,然后从中断位置继续完成本轮任务。",
283
+ "",
284
+ "最近失败片段:",
285
+ `- stdout 尾部:${tailText(result.stdout, 10) || "无"}`,
286
+ `- stderr 尾部:${tailText(result.stderr, 10) || "无"}`,
287
+ ].join("\n");
288
+ }
289
+
290
+ export function renderRuntimeRecoverySummary(recoveryHistory = [], failure = null) {
291
+ if (!Array.isArray(recoveryHistory) || !recoveryHistory.length) {
292
+ return "";
293
+ }
294
+
295
+ return [
296
+ `HelloLoop 已按${failure?.family === "hard" ? "硬阻塞" : "软阻塞"}策略进行 ${recoveryHistory.length} 次自动探测/恢复。`,
297
+ ...recoveryHistory.map((item) => (
298
+ `- 第 ${item.recoveryIndex} 次:等待 ${item.delaySeconds} 秒;探测 ${item.probeStatus || "unknown"};任务 ${item.taskStatus || "unknown"}`
299
+ )),
300
+ "自动恢复额度已用尽,当前已暂停等待用户介入。",
301
+ ].join("\n");
302
+ }
@@ -223,3 +223,19 @@ export function resolveCodexInvocation(options = {}) {
223
223
  toolDisplayName: "Codex",
224
224
  });
225
225
  }
226
+
227
+ export function resolveClaudeInvocation(options = {}) {
228
+ return resolveCliInvocation({
229
+ ...options,
230
+ commandName: "claude",
231
+ toolDisplayName: "Claude",
232
+ });
233
+ }
234
+
235
+ export function resolveGeminiInvocation(options = {}) {
236
+ return resolveCliInvocation({
237
+ ...options,
238
+ commandName: "gemini",
239
+ toolDisplayName: "Gemini",
240
+ });
241
+ }