helloloop 0.2.1 → 0.6.1

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 (58) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/README.md +297 -272
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +19 -9
  6. package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -4
  7. package/hosts/gemini/extension/GEMINI.md +13 -4
  8. package/hosts/gemini/extension/commands/helloloop.toml +19 -8
  9. package/hosts/gemini/extension/gemini-extension.json +1 -1
  10. package/package.json +2 -2
  11. package/scripts/uninstall-home-plugin.ps1 +25 -0
  12. package/skills/helloloop/SKILL.md +42 -7
  13. package/src/analyze_confirmation.mjs +108 -8
  14. package/src/analyze_prompt.mjs +17 -1
  15. package/src/analyze_user_input.mjs +321 -0
  16. package/src/analyzer.mjs +167 -42
  17. package/src/cli.mjs +34 -308
  18. package/src/cli_analyze_command.mjs +248 -0
  19. package/src/cli_args.mjs +106 -0
  20. package/src/cli_command_handlers.mjs +120 -0
  21. package/src/cli_context.mjs +31 -0
  22. package/src/cli_render.mjs +70 -0
  23. package/src/cli_support.mjs +95 -31
  24. package/src/completion_review.mjs +243 -0
  25. package/src/config.mjs +50 -0
  26. package/src/discovery.mjs +243 -9
  27. package/src/discovery_inference.mjs +62 -18
  28. package/src/discovery_paths.mjs +143 -8
  29. package/src/discovery_prompt.mjs +273 -0
  30. package/src/engine_metadata.mjs +79 -0
  31. package/src/engine_selection.mjs +335 -0
  32. package/src/engine_selection_failure.mjs +51 -0
  33. package/src/engine_selection_messages.mjs +119 -0
  34. package/src/engine_selection_probe.mjs +78 -0
  35. package/src/engine_selection_prompt.mjs +48 -0
  36. package/src/engine_selection_settings.mjs +38 -0
  37. package/src/guardrails.mjs +15 -4
  38. package/src/install.mjs +20 -266
  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 +90 -0
  43. package/src/process.mjs +482 -39
  44. package/src/prompt.mjs +9 -5
  45. package/src/prompt_session.mjs +40 -0
  46. package/src/rebuild.mjs +116 -0
  47. package/src/runner.mjs +3 -341
  48. package/src/runner_execute_task.mjs +301 -0
  49. package/src/runner_execution_support.mjs +155 -0
  50. package/src/runner_loop.mjs +106 -0
  51. package/src/runner_once.mjs +29 -0
  52. package/src/runner_status.mjs +104 -0
  53. package/src/runtime_recovery.mjs +301 -0
  54. package/src/shell_invocation.mjs +16 -0
  55. package/templates/analysis-output.schema.json +58 -1
  56. package/templates/policy.template.json +27 -0
  57. package/templates/project.template.json +2 -0
  58. package/templates/task-review-output.schema.json +70 -0
@@ -0,0 +1,301 @@
1
+ import { getEngineDisplayName } from "./engine_metadata.mjs";
2
+ import { tailText } from "./common.mjs";
3
+
4
+ const defaultRuntimeRecoveryPolicy = {
5
+ enabled: true,
6
+ allowEngineSwitch: false,
7
+ heartbeatIntervalSeconds: 60,
8
+ stallWarningSeconds: 900,
9
+ maxIdleSeconds: 2700,
10
+ killGraceSeconds: 10,
11
+ maxPhaseRecoveries: 4,
12
+ retryDelaysSeconds: [120, 300, 900, 1800],
13
+ retryOnUnknownFailure: true,
14
+ maxUnknownRecoveries: 1,
15
+ };
16
+
17
+ const HARD_STOP_MATCHERS = [
18
+ {
19
+ code: "invalid_request",
20
+ reason: "当前错误更像请求、参数、协议或输出格式问题,继续原样自动重试大概率无效。",
21
+ patterns: [
22
+ " 400 ",
23
+ "400 bad request",
24
+ "bad request",
25
+ "invalid request",
26
+ "invalid argument",
27
+ "invalid_argument",
28
+ "failed to parse",
29
+ "parse error",
30
+ "malformed",
31
+ "schema validation",
32
+ "json schema",
33
+ "unexpected argument",
34
+ "unknown option",
35
+ ],
36
+ },
37
+ {
38
+ code: "auth",
39
+ reason: "当前错误更像登录、鉴权、订阅或权限问题,需要先修复环境。",
40
+ patterns: [
41
+ "401",
42
+ "403",
43
+ "unauthorized",
44
+ "forbidden",
45
+ "not authenticated",
46
+ "authentication",
47
+ "login",
48
+ "sign in",
49
+ "api key",
50
+ "token",
51
+ "subscription",
52
+ "insufficient permissions",
53
+ ],
54
+ },
55
+ {
56
+ code: "environment",
57
+ reason: "当前错误更像本地 CLI 缺失、权限不足或文件系统问题,继续自动重试没有意义。",
58
+ patterns: [
59
+ "command not found",
60
+ "is not recognized",
61
+ "enoent",
62
+ "no such file or directory",
63
+ "permission denied",
64
+ "access is denied",
65
+ ],
66
+ },
67
+ ];
68
+
69
+ const RECOVERABLE_MATCHERS = [
70
+ {
71
+ code: "rate_limit",
72
+ reason: "当前引擎可能遇到配额、限流或临时容量不足。",
73
+ patterns: [
74
+ "429",
75
+ "rate limit",
76
+ "too many requests",
77
+ "quota",
78
+ "credit",
79
+ "usage limit",
80
+ "capacity",
81
+ "overloaded",
82
+ "insufficient balance",
83
+ "try again later",
84
+ ],
85
+ },
86
+ {
87
+ code: "server",
88
+ reason: "当前引擎可能遇到临时服务端错误。",
89
+ patterns: [
90
+ "500",
91
+ "501",
92
+ "502",
93
+ "503",
94
+ "504",
95
+ "internal server error",
96
+ "bad gateway",
97
+ "service unavailable",
98
+ "gateway timeout",
99
+ "server error",
100
+ "upstream",
101
+ "temporarily unavailable",
102
+ ],
103
+ },
104
+ {
105
+ code: "network",
106
+ reason: "当前引擎可能遇到临时网络、连接或结果流中断。",
107
+ patterns: [
108
+ "network error",
109
+ "fetch failed",
110
+ "econnreset",
111
+ "etimedout",
112
+ "timed out",
113
+ "timeout",
114
+ "connection reset",
115
+ "connection aborted",
116
+ "connection closed",
117
+ "stream closed",
118
+ "socket hang up",
119
+ "transport error",
120
+ "broken pipe",
121
+ "http2",
122
+ ],
123
+ },
124
+ ];
125
+
126
+ function normalizeSeconds(value, fallback) {
127
+ const numberValue = Number(value);
128
+ if (!Number.isFinite(numberValue) || numberValue < 0) {
129
+ return fallback;
130
+ }
131
+ return numberValue;
132
+ }
133
+
134
+ function normalizeSecondsList(value, fallback) {
135
+ if (!Array.isArray(value) || !value.length) {
136
+ return fallback;
137
+ }
138
+ const normalized = value
139
+ .map((item) => normalizeSeconds(item, -1))
140
+ .filter((item) => item >= 0);
141
+ return normalized.length ? normalized : fallback;
142
+ }
143
+
144
+ function normalizeText(value) {
145
+ return String(value || "").toLowerCase();
146
+ }
147
+
148
+ function hasMatcher(normalizedText, matcher) {
149
+ return matcher.patterns.some((pattern) => normalizedText.includes(String(pattern).toLowerCase()));
150
+ }
151
+
152
+ export function resolveRuntimeRecoveryPolicy(policy = {}) {
153
+ const configured = policy?.runtimeRecovery || {};
154
+ return {
155
+ enabled: configured.enabled !== false,
156
+ allowEngineSwitch: configured.allowEngineSwitch === true,
157
+ heartbeatIntervalSeconds: normalizeSeconds(
158
+ configured.heartbeatIntervalSeconds,
159
+ defaultRuntimeRecoveryPolicy.heartbeatIntervalSeconds,
160
+ ),
161
+ stallWarningSeconds: normalizeSeconds(
162
+ configured.stallWarningSeconds,
163
+ defaultRuntimeRecoveryPolicy.stallWarningSeconds,
164
+ ),
165
+ maxIdleSeconds: normalizeSeconds(
166
+ configured.maxIdleSeconds,
167
+ defaultRuntimeRecoveryPolicy.maxIdleSeconds,
168
+ ),
169
+ killGraceSeconds: normalizeSeconds(
170
+ configured.killGraceSeconds,
171
+ defaultRuntimeRecoveryPolicy.killGraceSeconds,
172
+ ),
173
+ maxPhaseRecoveries: Math.max(
174
+ 0,
175
+ Math.trunc(normalizeSeconds(configured.maxPhaseRecoveries, defaultRuntimeRecoveryPolicy.maxPhaseRecoveries)),
176
+ ),
177
+ retryDelaysSeconds: normalizeSecondsList(
178
+ configured.retryDelaysSeconds,
179
+ defaultRuntimeRecoveryPolicy.retryDelaysSeconds,
180
+ ),
181
+ retryOnUnknownFailure: configured.retryOnUnknownFailure !== false,
182
+ maxUnknownRecoveries: Math.max(
183
+ 0,
184
+ Math.trunc(normalizeSeconds(configured.maxUnknownRecoveries, defaultRuntimeRecoveryPolicy.maxUnknownRecoveries)),
185
+ ),
186
+ };
187
+ }
188
+
189
+ export function selectRuntimeRecoveryDelayMs(recoveryPolicy, nextRecoveryIndex) {
190
+ const delays = Array.isArray(recoveryPolicy?.retryDelaysSeconds) && recoveryPolicy.retryDelaysSeconds.length
191
+ ? recoveryPolicy.retryDelaysSeconds
192
+ : defaultRuntimeRecoveryPolicy.retryDelaysSeconds;
193
+ const offset = Math.max(0, Number(nextRecoveryIndex || 1) - 1);
194
+ const seconds = delays[Math.min(offset, delays.length - 1)] || 0;
195
+ return Math.max(0, seconds) * 1000;
196
+ }
197
+
198
+ export function classifyRuntimeRecoveryFailure({
199
+ result = {},
200
+ recoveryPolicy = defaultRuntimeRecoveryPolicy,
201
+ recoveryCount = 0,
202
+ } = {}) {
203
+ const normalized = normalizeText([
204
+ result.stderr,
205
+ result.stdout,
206
+ result.finalMessage,
207
+ result.watchdogReason,
208
+ ].filter(Boolean).join("\n"));
209
+
210
+ if (result.watchdogTriggered || result.idleTimeout) {
211
+ return {
212
+ recoverable: true,
213
+ code: "watchdog_idle",
214
+ reason: "当前进程长时间没有可见进展,HelloLoop 已按看门狗策略终止并准备同引擎恢复。",
215
+ };
216
+ }
217
+
218
+ for (const matcher of HARD_STOP_MATCHERS) {
219
+ if (hasMatcher(normalized, matcher)) {
220
+ return {
221
+ recoverable: false,
222
+ code: matcher.code,
223
+ reason: matcher.reason,
224
+ };
225
+ }
226
+ }
227
+
228
+ for (const matcher of RECOVERABLE_MATCHERS) {
229
+ if (hasMatcher(normalized, matcher)) {
230
+ return {
231
+ recoverable: true,
232
+ code: matcher.code,
233
+ reason: matcher.reason,
234
+ };
235
+ }
236
+ }
237
+
238
+ const emptyFailure = !normalized.trim() && !result.ok;
239
+ if (emptyFailure) {
240
+ return {
241
+ recoverable: recoveryCount < (recoveryPolicy.maxUnknownRecoveries || 0),
242
+ code: "empty_failure",
243
+ reason: "当前失败没有返回可判定的错误文本,HelloLoop 将按无人值守策略先尝试一次同引擎恢复。",
244
+ };
245
+ }
246
+
247
+ if (recoveryPolicy.retryOnUnknownFailure && recoveryCount < (recoveryPolicy.maxUnknownRecoveries || 0)) {
248
+ return {
249
+ recoverable: true,
250
+ code: "unknown_failure",
251
+ reason: "当前错误类型无法稳定归类,HelloLoop 将按无人值守策略先尝试一次同引擎恢复。",
252
+ };
253
+ }
254
+
255
+ return {
256
+ recoverable: false,
257
+ code: "unknown_failure",
258
+ reason: "当前错误无法判断为可安全自动恢复,已停止本轮自动恢复。",
259
+ };
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 = []) {
291
+ if (!Array.isArray(recoveryHistory) || !recoveryHistory.length) {
292
+ return "";
293
+ }
294
+
295
+ return [
296
+ `HelloLoop 已按无人值守策略进行 ${recoveryHistory.length} 次同引擎自动恢复。`,
297
+ ...recoveryHistory.map((item) => (
298
+ `- 第 ${item.recoveryIndex} 次恢复:${item.reason}(等待 ${item.delaySeconds} 秒)`
299
+ )),
300
+ ].join("\n");
301
+ }
@@ -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
+ }
@@ -50,9 +50,66 @@
50
50
  "type": "string"
51
51
  }
52
52
  },
53
+ "requestInterpretation": {
54
+ "type": "object",
55
+ "additionalProperties": false,
56
+ "required": [
57
+ "summary",
58
+ "priorities",
59
+ "cautions"
60
+ ],
61
+ "properties": {
62
+ "summary": {
63
+ "type": "string",
64
+ "minLength": 1
65
+ },
66
+ "priorities": {
67
+ "type": "array",
68
+ "items": {
69
+ "type": "string"
70
+ }
71
+ },
72
+ "cautions": {
73
+ "type": "array",
74
+ "items": {
75
+ "type": "string"
76
+ }
77
+ }
78
+ }
79
+ },
80
+ "repoDecision": {
81
+ "type": "object",
82
+ "additionalProperties": false,
83
+ "required": [
84
+ "compatibility",
85
+ "action",
86
+ "reason"
87
+ ],
88
+ "properties": {
89
+ "compatibility": {
90
+ "type": "string",
91
+ "enum": [
92
+ "compatible",
93
+ "conflict",
94
+ "uncertain"
95
+ ]
96
+ },
97
+ "action": {
98
+ "type": "string",
99
+ "enum": [
100
+ "continue_existing",
101
+ "confirm_rebuild",
102
+ "start_new"
103
+ ]
104
+ },
105
+ "reason": {
106
+ "type": "string",
107
+ "minLength": 1
108
+ }
109
+ }
110
+ },
53
111
  "tasks": {
54
112
  "type": "array",
55
- "minItems": 1,
56
113
  "items": {
57
114
  "type": "object",
58
115
  "additionalProperties": false,
@@ -4,13 +4,40 @@
4
4
  "maxLoopTasks": 4,
5
5
  "maxTaskAttempts": 2,
6
6
  "maxTaskStrategies": 4,
7
+ "maxReanalysisPasses": 3,
7
8
  "stopOnFailure": false,
8
9
  "stopOnHighRisk": true,
10
+ "runtimeRecovery": {
11
+ "enabled": true,
12
+ "allowEngineSwitch": false,
13
+ "heartbeatIntervalSeconds": 60,
14
+ "stallWarningSeconds": 900,
15
+ "maxIdleSeconds": 2700,
16
+ "killGraceSeconds": 10,
17
+ "maxPhaseRecoveries": 4,
18
+ "retryDelaysSeconds": [120, 300, 900, 1800],
19
+ "retryOnUnknownFailure": true,
20
+ "maxUnknownRecoveries": 1
21
+ },
9
22
  "codex": {
10
23
  "model": "",
11
24
  "executable": "",
12
25
  "sandbox": "workspace-write",
13
26
  "dangerouslyBypassSandbox": false,
14
27
  "jsonOutput": true
28
+ },
29
+ "claude": {
30
+ "model": "",
31
+ "executable": "",
32
+ "permissionMode": "bypassPermissions",
33
+ "analysisPermissionMode": "plan",
34
+ "outputFormat": "text"
35
+ },
36
+ "gemini": {
37
+ "model": "",
38
+ "executable": "",
39
+ "approvalMode": "yolo",
40
+ "analysisApprovalMode": "plan",
41
+ "outputFormat": "text"
15
42
  }
16
43
  }
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "requiredDocs": [],
3
3
  "constraints": [],
4
+ "defaultEngine": "",
5
+ "lastSelectedEngine": "",
4
6
  "planner": {
5
7
  "minTasks": 3,
6
8
  "maxTasks": 8,
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "required": [
6
+ "verdict",
7
+ "summary",
8
+ "acceptanceChecks",
9
+ "missing",
10
+ "nextAction"
11
+ ],
12
+ "properties": {
13
+ "verdict": {
14
+ "type": "string",
15
+ "enum": [
16
+ "complete",
17
+ "incomplete",
18
+ "blocked"
19
+ ]
20
+ },
21
+ "summary": {
22
+ "type": "string",
23
+ "minLength": 1
24
+ },
25
+ "acceptanceChecks": {
26
+ "type": "array",
27
+ "minItems": 1,
28
+ "items": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": [
32
+ "item",
33
+ "status",
34
+ "evidence"
35
+ ],
36
+ "properties": {
37
+ "item": {
38
+ "type": "string",
39
+ "minLength": 1
40
+ },
41
+ "status": {
42
+ "type": "string",
43
+ "enum": [
44
+ "met",
45
+ "not_met",
46
+ "uncertain"
47
+ ]
48
+ },
49
+ "evidence": {
50
+ "type": "string",
51
+ "minLength": 1
52
+ }
53
+ }
54
+ }
55
+ },
56
+ "missing": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "string"
60
+ }
61
+ },
62
+ "blockerReason": {
63
+ "type": "string"
64
+ },
65
+ "nextAction": {
66
+ "type": "string",
67
+ "minLength": 1
68
+ }
69
+ }
70
+ }