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
package/src/process.mjs CHANGED
@@ -1,58 +1,45 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { spawn } from "node:child_process";
4
3
 
5
- import { ensureDir, nowIso, tailText, writeText } from "./common.mjs";
6
- import { resolveCodexInvocation, resolveVerifyShellInvocation } from "./shell_invocation.mjs";
4
+ import { ensureDir, nowIso, tailText, writeJson, writeText } from "./common.mjs";
5
+ import { getEngineDisplayName, normalizeEngineName } from "./engine_metadata.mjs";
6
+ import {
7
+ buildClaudeArgs,
8
+ buildCodexArgs,
9
+ buildGeminiArgs,
10
+ resolveEngineInvocation,
11
+ resolveVerifyInvocation,
12
+ runChild,
13
+ } from "./engine_process_support.mjs";
14
+ import { sendRuntimeStopNotification } from "./email_notification.mjs";
15
+ import { loadGlobalConfig } from "./global_config.mjs";
16
+ import {
17
+ buildEngineHealthProbePrompt,
18
+ buildRuntimeRecoveryPrompt,
19
+ classifyRuntimeRecoveryFailure,
20
+ renderRuntimeRecoverySummary,
21
+ resolveRuntimeRecoveryPolicy,
22
+ selectRuntimeRecoveryDelayMs,
23
+ } from "./runtime_recovery.mjs";
7
24
 
8
- function runChild(command, args, options = {}) {
25
+ function sleep(ms) {
9
26
  return new Promise((resolve) => {
10
- const child = spawn(command, args, {
11
- cwd: options.cwd,
12
- env: {
13
- ...process.env,
14
- ...(options.env || {}),
15
- },
16
- stdio: ["pipe", "pipe", "pipe"],
17
- shell: Boolean(options.shell),
18
- });
19
-
20
- let stdout = "";
21
- let stderr = "";
22
-
23
- child.stdout.on("data", (chunk) => {
24
- stdout += chunk.toString();
25
- });
26
- child.stderr.on("data", (chunk) => {
27
- stderr += chunk.toString();
28
- });
29
-
30
- if (options.stdin) {
31
- child.stdin.write(options.stdin);
32
- }
33
- child.stdin.end();
34
-
35
- child.on("error", (error) => {
36
- resolve({
37
- ok: false,
38
- code: 1,
39
- stdout,
40
- stderr: String(error?.stack || error || ""),
41
- });
42
- });
27
+ setTimeout(resolve, ms);
28
+ });
29
+ }
43
30
 
44
- child.on("close", (code) => {
45
- resolve({
46
- ok: code === 0,
47
- code: code ?? 1,
48
- stdout,
49
- stderr,
50
- });
31
+ function createRuntimeStatusWriter(runtimeStatusFile, baseState) {
32
+ return function writeRuntimeStatus(status, extra = {}) {
33
+ writeJson(runtimeStatusFile, {
34
+ ...baseState,
35
+ ...extra,
36
+ status,
37
+ updatedAt: nowIso(),
51
38
  });
52
- });
39
+ };
53
40
  }
54
41
 
55
- function writeCodexRunArtifacts(runDir, prefix, result, finalMessage) {
42
+ function writeEngineRunArtifacts(runDir, prefix, result, finalMessage) {
56
43
  writeText(path.join(runDir, `${prefix}-stdout.log`), result.stdout);
57
44
  writeText(path.join(runDir, `${prefix}-stderr.log`), result.stderr);
58
45
  writeText(path.join(runDir, `${prefix}-summary.txt`), [
@@ -64,48 +51,90 @@ function writeCodexRunArtifacts(runDir, prefix, result, finalMessage) {
64
51
  ].join("\n"));
65
52
  }
66
53
 
67
- export async function runCodexTask({
68
- context,
69
- prompt,
70
- runDir,
71
- model = "",
72
- executable = "",
73
- sandbox = "workspace-write",
74
- dangerouslyBypassSandbox = false,
75
- jsonOutput = true,
76
- outputSchemaFile = "",
77
- outputPrefix = "codex",
78
- ephemeral = false,
79
- skipGitRepoCheck = false,
80
- env = {},
81
- }) {
82
- ensureDir(runDir);
83
-
84
- const lastMessageFile = path.join(runDir, `${outputPrefix}-last-message.txt`);
85
- const invocation = resolveCodexInvocation({ explicitExecutable: executable });
86
- const codexArgs = ["exec", "-C", context.repoRoot];
87
-
88
- if (model) {
89
- codexArgs.push("--model", model);
54
+ function resolveEnginePolicy(policy = {}, engine) {
55
+ if (engine === "codex") {
56
+ return policy.codex || {};
90
57
  }
91
- if (dangerouslyBypassSandbox) {
92
- codexArgs.push("--dangerously-bypass-approvals-and-sandbox");
93
- } else {
94
- codexArgs.push("--sandbox", sandbox);
58
+ if (engine === "claude") {
59
+ return policy.claude || {};
95
60
  }
96
- if (skipGitRepoCheck) {
97
- codexArgs.push("--skip-git-repo-check");
61
+ if (engine === "gemini") {
62
+ return policy.gemini || {};
98
63
  }
99
- if (ephemeral) {
100
- codexArgs.push("--ephemeral");
64
+ return {};
65
+ }
66
+
67
+ function buildEngineArgs({
68
+ engine,
69
+ context,
70
+ resolvedPolicy,
71
+ executionMode,
72
+ outputSchemaFile,
73
+ ephemeral,
74
+ skipGitRepoCheck,
75
+ lastMessageFile,
76
+ probeMode = false,
77
+ }) {
78
+ if (engine === "codex") {
79
+ return buildCodexArgs({
80
+ context,
81
+ model: resolvedPolicy.model,
82
+ sandbox: resolvedPolicy.sandbox,
83
+ dangerouslyBypassSandbox: resolvedPolicy.dangerouslyBypassSandbox,
84
+ jsonOutput: probeMode ? false : (resolvedPolicy.jsonOutput !== false),
85
+ outputSchemaFile: probeMode ? "" : outputSchemaFile,
86
+ ephemeral,
87
+ skipGitRepoCheck,
88
+ lastMessageFile,
89
+ });
101
90
  }
102
- if (outputSchemaFile) {
103
- codexArgs.push("--output-schema", outputSchemaFile);
91
+
92
+ if (engine === "claude") {
93
+ return buildClaudeArgs({
94
+ model: resolvedPolicy.model,
95
+ outputSchemaFile: probeMode ? "" : outputSchemaFile,
96
+ executionMode: probeMode ? "execute" : executionMode,
97
+ policy: resolvedPolicy,
98
+ });
104
99
  }
105
- if (jsonOutput) {
106
- codexArgs.push("--json");
100
+
101
+ return buildGeminiArgs({
102
+ model: resolvedPolicy.model,
103
+ executionMode: probeMode ? "execute" : executionMode,
104
+ policy: resolvedPolicy,
105
+ });
106
+ }
107
+
108
+ function readEngineFinalMessage(engine, lastMessageFile, result) {
109
+ if (engine === "codex") {
110
+ return fs.existsSync(lastMessageFile)
111
+ ? fs.readFileSync(lastMessageFile, "utf8").trim()
112
+ : "";
107
113
  }
108
- codexArgs.push("-o", lastMessageFile, "-");
114
+ return String(result.stdout || "").trim();
115
+ }
116
+
117
+ async function runEngineAttempt({
118
+ engine,
119
+ invocation,
120
+ context,
121
+ prompt,
122
+ runDir,
123
+ attemptPrefix,
124
+ resolvedPolicy,
125
+ executionMode,
126
+ outputSchemaFile,
127
+ env,
128
+ recoveryPolicy,
129
+ writeRuntimeStatus,
130
+ recoveryCount,
131
+ recoveryHistory,
132
+ ephemeral = false,
133
+ skipGitRepoCheck = false,
134
+ probeMode = false,
135
+ }) {
136
+ const attemptPromptFile = path.join(runDir, `${attemptPrefix}-prompt.md`);
137
+ const attemptLastMessageFile = path.join(runDir, `${attemptPrefix}-last-message.txt`);
109
138
 
110
139
  if (invocation.error) {
111
140
  const result = {
@@ -113,46 +142,484 @@ export async function runCodexTask({
113
142
  code: 1,
114
143
  stdout: "",
115
144
  stderr: invocation.error,
145
+ signal: "",
146
+ startedAt: nowIso(),
147
+ finishedAt: nowIso(),
148
+ idleTimeout: false,
149
+ watchdogTriggered: false,
150
+ watchdogReason: "",
151
+ };
152
+ writeText(attemptPromptFile, prompt);
153
+ writeEngineRunArtifacts(runDir, attemptPrefix, result, "");
154
+ return {
155
+ result,
156
+ finalMessage: "",
157
+ attemptPrefix,
116
158
  };
117
- writeText(path.join(runDir, `${outputPrefix}-prompt.md`), prompt);
118
- writeCodexRunArtifacts(runDir, outputPrefix, result, "");
119
- return { ...result, finalMessage: "" };
120
159
  }
121
160
 
122
- const args = [...invocation.argsPrefix, ...codexArgs];
161
+ const finalArgs = [
162
+ ...invocation.argsPrefix,
163
+ ...buildEngineArgs({
164
+ engine,
165
+ context,
166
+ resolvedPolicy,
167
+ executionMode,
168
+ outputSchemaFile,
169
+ ephemeral,
170
+ skipGitRepoCheck,
171
+ lastMessageFile: attemptLastMessageFile,
172
+ probeMode,
173
+ }),
174
+ ];
175
+
176
+ writeRuntimeStatus(probeMode ? "probe_running" : (recoveryCount > 0 ? "recovering" : "running"), {
177
+ attemptPrefix,
178
+ recoveryCount,
179
+ recoveryHistory,
180
+ });
123
181
 
124
- const result = await runChild(invocation.command, args, {
182
+ const result = await runChild(invocation.command, finalArgs, {
125
183
  cwd: context.repoRoot,
126
184
  stdin: prompt,
127
185
  env,
128
186
  shell: invocation.shell,
187
+ heartbeatIntervalMs: recoveryPolicy.heartbeatIntervalSeconds * 1000,
188
+ stallWarningMs: recoveryPolicy.stallWarningSeconds * 1000,
189
+ maxIdleMs: recoveryPolicy.maxIdleSeconds * 1000,
190
+ killGraceMs: recoveryPolicy.killGraceSeconds * 1000,
191
+ onHeartbeat(payload) {
192
+ writeRuntimeStatus(payload.status, {
193
+ attemptPrefix,
194
+ recoveryCount,
195
+ recoveryHistory,
196
+ heartbeat: payload,
197
+ });
198
+ },
199
+ });
200
+ const finalMessage = readEngineFinalMessage(engine, attemptLastMessageFile, result);
201
+
202
+ writeText(attemptPromptFile, prompt);
203
+ writeEngineRunArtifacts(runDir, attemptPrefix, result, finalMessage);
204
+
205
+ return {
206
+ result,
207
+ finalMessage,
208
+ attemptPrefix,
209
+ };
210
+ }
211
+
212
+ async function runEngineHealthProbe({
213
+ engine,
214
+ invocation,
215
+ context,
216
+ runDir,
217
+ resolvedPolicy,
218
+ recoveryPolicy,
219
+ writeRuntimeStatus,
220
+ recoveryCount,
221
+ recoveryHistory,
222
+ env,
223
+ probeIndex,
224
+ }) {
225
+ const probePrompt = buildEngineHealthProbePrompt(engine);
226
+ const attemptPrefix = `${engine}-probe-${String(probeIndex).padStart(2, "0")}`;
227
+ writeRuntimeStatus("probe_waiting", {
228
+ attemptPrefix,
229
+ recoveryCount,
230
+ recoveryHistory,
231
+ });
232
+ const attempt = await runEngineAttempt({
233
+ engine,
234
+ invocation,
235
+ context,
236
+ prompt: probePrompt,
237
+ runDir,
238
+ attemptPrefix,
239
+ resolvedPolicy,
240
+ executionMode: "execute",
241
+ outputSchemaFile: "",
242
+ env,
243
+ recoveryPolicy: {
244
+ ...recoveryPolicy,
245
+ maxIdleSeconds: recoveryPolicy.healthProbeTimeoutSeconds,
246
+ },
247
+ writeRuntimeStatus,
248
+ recoveryCount,
249
+ recoveryHistory,
250
+ ephemeral: true,
251
+ skipGitRepoCheck: true,
252
+ probeMode: true,
129
253
  });
130
- const finalMessage = fs.existsSync(lastMessageFile)
131
- ? fs.readFileSync(lastMessageFile, "utf8").trim()
132
- : "";
133
254
 
134
- writeText(path.join(runDir, `${outputPrefix}-prompt.md`), prompt);
135
- writeCodexRunArtifacts(runDir, outputPrefix, result, finalMessage);
255
+ return {
256
+ ...attempt,
257
+ failure: classifyRuntimeRecoveryFailure({
258
+ result: {
259
+ ...attempt.result,
260
+ finalMessage: attempt.finalMessage,
261
+ },
262
+ }),
263
+ };
264
+ }
136
265
 
137
- return { ...result, finalMessage };
266
+ async function maybeSendStopNotification({
267
+ context,
268
+ runDir,
269
+ engine,
270
+ executionMode,
271
+ failure,
272
+ result,
273
+ recoveryHistory,
274
+ }) {
275
+ try {
276
+ return await sendRuntimeStopNotification({
277
+ globalConfig: loadGlobalConfig(),
278
+ context,
279
+ engine: getEngineDisplayName(engine),
280
+ phase: executionMode === "analyze" ? "分析/复核" : "执行",
281
+ failure,
282
+ result,
283
+ recoveryHistory,
284
+ runDir,
285
+ });
286
+ } catch (error) {
287
+ return {
288
+ attempted: true,
289
+ delivered: false,
290
+ reason: String(error?.message || error || "邮件发送失败。"),
291
+ };
292
+ }
293
+ }
294
+
295
+ function buildNotificationNote(notificationResult) {
296
+ if (!notificationResult) {
297
+ return "";
298
+ }
299
+ if (notificationResult.delivered) {
300
+ return `告警邮件已发送:${(notificationResult.recipients || []).join(", ")}`;
301
+ }
302
+ if (notificationResult.attempted) {
303
+ return `告警邮件发送失败:${notificationResult.reason || "未知原因"}`;
304
+ }
305
+ return `未发送告警邮件:${notificationResult.reason || "未启用"}`;
306
+ }
307
+
308
+ export async function runEngineTask({
309
+ engine = "codex",
310
+ context,
311
+ prompt,
312
+ runDir,
313
+ policy = {},
314
+ executionMode = "analyze",
315
+ outputSchemaFile = "",
316
+ outputPrefix = "",
317
+ ephemeral = false,
318
+ skipGitRepoCheck = false,
319
+ env = {},
320
+ }) {
321
+ ensureDir(runDir);
322
+
323
+ const normalizedEngine = normalizeEngineName(engine) || "codex";
324
+ const resolvedPolicy = resolveEnginePolicy(policy, normalizedEngine);
325
+ const prefix = outputPrefix || normalizedEngine;
326
+ const invocation = resolveEngineInvocation(normalizedEngine, resolvedPolicy.executable);
327
+ const recoveryPolicy = resolveRuntimeRecoveryPolicy(policy);
328
+ const runtimeStatusFile = path.join(runDir, `${prefix}-runtime.json`);
329
+ const writeRuntimeStatus = createRuntimeStatusWriter(runtimeStatusFile, {
330
+ engine: normalizedEngine,
331
+ engineDisplayName: getEngineDisplayName(normalizedEngine),
332
+ phase: executionMode,
333
+ outputPrefix: prefix,
334
+ hardRetryBudget: recoveryPolicy.hardRetryDelaysSeconds.length,
335
+ softRetryBudget: recoveryPolicy.softRetryDelaysSeconds.length,
336
+ });
337
+
338
+ const recoveryHistory = [];
339
+ let currentPrompt = prompt;
340
+ let currentRecoveryCount = 0;
341
+ let activeFailure = null;
342
+
343
+ while (true) {
344
+ const attemptPrefix = currentRecoveryCount === 0
345
+ ? prefix
346
+ : `${prefix}-recovery-${String(currentRecoveryCount).padStart(2, "0")}`;
347
+ const taskAttempt = await runEngineAttempt({
348
+ engine: normalizedEngine,
349
+ invocation,
350
+ context,
351
+ prompt: currentPrompt,
352
+ runDir,
353
+ attemptPrefix,
354
+ resolvedPolicy,
355
+ executionMode,
356
+ outputSchemaFile,
357
+ env,
358
+ recoveryPolicy,
359
+ writeRuntimeStatus,
360
+ recoveryCount: currentRecoveryCount,
361
+ recoveryHistory,
362
+ ephemeral,
363
+ skipGitRepoCheck,
364
+ probeMode: false,
365
+ });
366
+
367
+ const taskFailure = classifyRuntimeRecoveryFailure({
368
+ result: {
369
+ ...taskAttempt.result,
370
+ finalMessage: taskAttempt.finalMessage,
371
+ },
372
+ });
373
+
374
+ if (taskAttempt.result.ok || !recoveryPolicy.enabled) {
375
+ const finalRecoverySummary = taskAttempt.result.ok
376
+ ? ""
377
+ : renderRuntimeRecoverySummary(recoveryHistory, taskFailure);
378
+ const notification = taskAttempt.result.ok
379
+ ? null
380
+ : await maybeSendStopNotification({
381
+ context,
382
+ runDir,
383
+ engine: normalizedEngine,
384
+ executionMode,
385
+ failure: taskFailure,
386
+ result: taskAttempt.result,
387
+ recoveryHistory,
388
+ });
389
+ const notificationNote = taskAttempt.result.ok ? "" : buildNotificationNote(notification);
390
+ const finalizedResult = taskAttempt.result.ok
391
+ ? taskAttempt.result
392
+ : {
393
+ ...taskAttempt.result,
394
+ stderr: [
395
+ taskAttempt.result.stderr,
396
+ "",
397
+ finalRecoverySummary,
398
+ notificationNote,
399
+ ].filter(Boolean).join("\n").trim(),
400
+ };
401
+
402
+ writeText(path.join(runDir, `${prefix}-prompt.md`), currentPrompt);
403
+ writeEngineRunArtifacts(runDir, prefix, finalizedResult, taskAttempt.finalMessage);
404
+ if (normalizedEngine === "codex" && taskAttempt.finalMessage) {
405
+ writeText(path.join(runDir, `${prefix}-last-message.txt`), taskAttempt.finalMessage);
406
+ }
407
+ writeRuntimeStatus(taskAttempt.result.ok ? "completed" : "paused_manual", {
408
+ attemptPrefix,
409
+ recoveryCount: recoveryHistory.length,
410
+ recoveryHistory,
411
+ recoverySummary: finalRecoverySummary,
412
+ finalMessage: taskAttempt.finalMessage,
413
+ code: finalizedResult.code,
414
+ failureCode: taskFailure.code,
415
+ failureFamily: taskFailure.family,
416
+ failureReason: taskFailure.reason,
417
+ notification,
418
+ });
419
+
420
+ return {
421
+ ...finalizedResult,
422
+ finalMessage: taskAttempt.finalMessage,
423
+ recoveryCount: recoveryHistory.length,
424
+ recoveryHistory,
425
+ recoverySummary: finalRecoverySummary,
426
+ recoveryFailure: taskAttempt.result.ok
427
+ ? null
428
+ : {
429
+ ...taskFailure,
430
+ shouldStopTask: true,
431
+ exhausted: true,
432
+ },
433
+ notification,
434
+ };
435
+ }
436
+
437
+ activeFailure = taskFailure;
438
+ while (true) {
439
+ const nextRecoveryIndex = recoveryHistory.length + 1;
440
+ const recoveryPrompt = buildRuntimeRecoveryPrompt({
441
+ basePrompt: prompt,
442
+ engine: normalizedEngine,
443
+ phaseLabel: executionMode === "analyze" ? "分析/复核" : "执行",
444
+ failure: activeFailure,
445
+ result: {
446
+ ...taskAttempt.result,
447
+ finalMessage: taskAttempt.finalMessage,
448
+ },
449
+ nextRecoveryIndex,
450
+ maxRecoveries: recoveryPolicy[activeFailure.family === "hard" ? "hardRetryDelaysSeconds" : "softRetryDelaysSeconds"].length,
451
+ });
452
+ writeText(
453
+ path.join(runDir, `${prefix}-auto-recovery-${String(nextRecoveryIndex).padStart(2, "0")}-prompt.md`),
454
+ recoveryPrompt,
455
+ );
456
+ const delayMs = selectRuntimeRecoveryDelayMs(recoveryPolicy, activeFailure.family, nextRecoveryIndex);
457
+ if (delayMs < 0) {
458
+ const finalRecoverySummary = renderRuntimeRecoverySummary(recoveryHistory, activeFailure);
459
+ const notification = await maybeSendStopNotification({
460
+ context,
461
+ runDir,
462
+ engine: normalizedEngine,
463
+ executionMode,
464
+ failure: activeFailure,
465
+ result: taskAttempt.result,
466
+ recoveryHistory,
467
+ });
468
+ const notificationNote = buildNotificationNote(notification);
469
+ const finalizedResult = {
470
+ ...taskAttempt.result,
471
+ stderr: [
472
+ taskAttempt.result.stderr,
473
+ "",
474
+ finalRecoverySummary,
475
+ notificationNote,
476
+ ].filter(Boolean).join("\n").trim(),
477
+ };
478
+
479
+ writeText(path.join(runDir, `${prefix}-prompt.md`), currentPrompt);
480
+ writeEngineRunArtifacts(runDir, prefix, finalizedResult, taskAttempt.finalMessage);
481
+ writeRuntimeStatus("paused_manual", {
482
+ attemptPrefix,
483
+ recoveryCount: recoveryHistory.length,
484
+ recoveryHistory,
485
+ recoverySummary: finalRecoverySummary,
486
+ finalMessage: taskAttempt.finalMessage,
487
+ code: finalizedResult.code,
488
+ failureCode: activeFailure.code,
489
+ failureFamily: activeFailure.family,
490
+ failureReason: activeFailure.reason,
491
+ notification,
492
+ });
493
+
494
+ return {
495
+ ...finalizedResult,
496
+ finalMessage: taskAttempt.finalMessage,
497
+ recoveryCount: recoveryHistory.length,
498
+ recoveryHistory,
499
+ recoverySummary: finalRecoverySummary,
500
+ recoveryFailure: {
501
+ ...activeFailure,
502
+ shouldStopTask: true,
503
+ exhausted: true,
504
+ },
505
+ notification,
506
+ };
507
+ }
508
+
509
+ writeRuntimeStatus("retry_waiting", {
510
+ attemptPrefix,
511
+ recoveryCount: nextRecoveryIndex,
512
+ recoveryHistory,
513
+ nextRetryDelayMs: delayMs,
514
+ nextRetryAt: new Date(Date.now() + delayMs).toISOString(),
515
+ failureCode: activeFailure.code,
516
+ failureFamily: activeFailure.family,
517
+ failureReason: activeFailure.reason,
518
+ });
519
+ if (delayMs > 0) {
520
+ await sleep(delayMs);
521
+ }
522
+
523
+ const probeAttempt = await runEngineHealthProbe({
524
+ engine: normalizedEngine,
525
+ invocation,
526
+ context,
527
+ runDir,
528
+ resolvedPolicy,
529
+ recoveryPolicy,
530
+ writeRuntimeStatus,
531
+ recoveryCount: nextRecoveryIndex,
532
+ recoveryHistory,
533
+ env,
534
+ probeIndex: nextRecoveryIndex,
535
+ });
536
+ const recoveryRecord = {
537
+ recoveryIndex: nextRecoveryIndex,
538
+ family: activeFailure.family,
539
+ code: activeFailure.code,
540
+ reason: activeFailure.reason,
541
+ delaySeconds: Math.floor(delayMs / 1000),
542
+ taskStatus: "failed",
543
+ taskCode: taskAttempt.result.code,
544
+ taskAttemptPrefix: attemptPrefix,
545
+ probeStatus: probeAttempt.result.ok ? "ok" : "failed",
546
+ probeCode: probeAttempt.result.code,
547
+ probeAttemptPrefix: probeAttempt.attemptPrefix,
548
+ probeFailureCode: probeAttempt.failure?.code || "",
549
+ probeFailureFamily: probeAttempt.failure?.family || "",
550
+ probeFailureReason: probeAttempt.failure?.reason || "",
551
+ watchdogTriggered: taskAttempt.result.watchdogTriggered === true || probeAttempt.result.watchdogTriggered === true,
552
+ };
553
+ recoveryHistory.push(recoveryRecord);
554
+ writeJson(path.join(
555
+ runDir,
556
+ `${prefix}-auto-recovery-${String(nextRecoveryIndex).padStart(2, "0")}.json`,
557
+ ), {
558
+ ...recoveryRecord,
559
+ engine: normalizedEngine,
560
+ phase: executionMode,
561
+ stdoutTail: tailText(taskAttempt.result.stdout, 20),
562
+ stderrTail: tailText(taskAttempt.result.stderr, 20),
563
+ finalMessageTail: tailText(taskAttempt.finalMessage, 20),
564
+ probeStdoutTail: tailText(probeAttempt.result.stdout, 20),
565
+ probeStderrTail: tailText(probeAttempt.result.stderr, 20),
566
+ probeFinalMessageTail: tailText(probeAttempt.finalMessage, 20),
567
+ createdAt: nowIso(),
568
+ });
569
+
570
+ if (!probeAttempt.result.ok) {
571
+ activeFailure = probeAttempt.failure;
572
+ writeRuntimeStatus("probe_failed", {
573
+ attemptPrefix: probeAttempt.attemptPrefix,
574
+ recoveryCount: nextRecoveryIndex,
575
+ recoveryHistory,
576
+ failureCode: activeFailure.code,
577
+ failureFamily: activeFailure.family,
578
+ failureReason: activeFailure.reason,
579
+ });
580
+ continue;
581
+ }
582
+
583
+ currentPrompt = recoveryPrompt;
584
+ currentRecoveryCount = nextRecoveryIndex;
585
+ break;
586
+ }
587
+ }
588
+ }
589
+
590
+ export async function runCodexTask(options) {
591
+ return runEngineTask({
592
+ ...options,
593
+ engine: "codex",
594
+ });
138
595
  }
139
596
 
140
597
  export async function runCodexExec({ context, prompt, runDir, policy }) {
141
- return runCodexTask({
598
+ return runEngineTask({
599
+ engine: "codex",
142
600
  context,
143
601
  prompt,
144
602
  runDir,
145
- model: policy.codex.model,
146
- executable: policy.codex.executable,
147
- sandbox: policy.codex.sandbox,
148
- dangerouslyBypassSandbox: policy.codex.dangerouslyBypassSandbox,
149
- jsonOutput: policy.codex.jsonOutput,
603
+ policy,
604
+ executionMode: "execute",
150
605
  outputPrefix: "codex",
151
606
  });
152
607
  }
153
608
 
609
+ export async function runEngineExec({ engine, context, prompt, runDir, policy }) {
610
+ return runEngineTask({
611
+ engine,
612
+ context,
613
+ prompt,
614
+ runDir,
615
+ policy,
616
+ executionMode: "execute",
617
+ outputPrefix: engine,
618
+ });
619
+ }
620
+
154
621
  export async function runShellCommand(context, commandLine, runDir, index) {
155
- const shellInvocation = resolveVerifyShellInvocation();
622
+ const shellInvocation = resolveVerifyInvocation();
156
623
  if (shellInvocation.error) {
157
624
  const result = {
158
625
  command: commandLine,