helloloop 0.3.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 (53) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/README.md +157 -81
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +13 -10
  6. package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +9 -4
  7. package/hosts/gemini/extension/GEMINI.md +12 -7
  8. package/hosts/gemini/extension/commands/helloloop.toml +14 -10
  9. package/hosts/gemini/extension/gemini-extension.json +1 -1
  10. package/package.json +2 -2
  11. package/skills/helloloop/SKILL.md +16 -6
  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 +50 -0
  25. package/src/discovery_prompt.mjs +2 -27
  26. package/src/engine_metadata.mjs +79 -0
  27. package/src/engine_selection.mjs +335 -0
  28. package/src/engine_selection_failure.mjs +51 -0
  29. package/src/engine_selection_messages.mjs +119 -0
  30. package/src/engine_selection_probe.mjs +78 -0
  31. package/src/engine_selection_prompt.mjs +48 -0
  32. package/src/engine_selection_settings.mjs +38 -0
  33. package/src/guardrails.mjs +15 -4
  34. package/src/install.mjs +6 -405
  35. package/src/install_claude.mjs +189 -0
  36. package/src/install_codex.mjs +114 -0
  37. package/src/install_gemini.mjs +43 -0
  38. package/src/install_shared.mjs +90 -0
  39. package/src/process.mjs +482 -39
  40. package/src/prompt.mjs +9 -5
  41. package/src/prompt_session.mjs +40 -0
  42. package/src/runner.mjs +3 -341
  43. package/src/runner_execute_task.mjs +301 -0
  44. package/src/runner_execution_support.mjs +155 -0
  45. package/src/runner_loop.mjs +106 -0
  46. package/src/runner_once.mjs +29 -0
  47. package/src/runner_status.mjs +104 -0
  48. package/src/runtime_recovery.mjs +301 -0
  49. package/src/shell_invocation.mjs +16 -0
  50. package/templates/analysis-output.schema.json +0 -1
  51. package/templates/policy.template.json +27 -0
  52. package/templates/project.template.json +2 -0
  53. package/templates/task-review-output.schema.json +70 -0
package/src/process.mjs CHANGED
@@ -2,8 +2,33 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
 
5
- import { ensureDir, nowIso, tailText, writeText } from "./common.mjs";
6
- import { resolveCodexInvocation, resolveVerifyShellInvocation } from "./shell_invocation.mjs";
5
+ import { ensureDir, nowIso, tailText, writeJson, writeText } from "./common.mjs";
6
+ import { getEngineDisplayName, normalizeEngineName } from "./engine_metadata.mjs";
7
+ import { resolveCliInvocation, resolveCodexInvocation, resolveVerifyShellInvocation } from "./shell_invocation.mjs";
8
+ import {
9
+ buildRuntimeRecoveryPrompt,
10
+ classifyRuntimeRecoveryFailure,
11
+ renderRuntimeRecoverySummary,
12
+ resolveRuntimeRecoveryPolicy,
13
+ selectRuntimeRecoveryDelayMs,
14
+ } from "./runtime_recovery.mjs";
15
+
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => {
18
+ setTimeout(resolve, ms);
19
+ });
20
+ }
21
+
22
+ function createRuntimeStatusWriter(runtimeStatusFile, baseState) {
23
+ return function writeRuntimeStatus(status, extra = {}) {
24
+ writeJson(runtimeStatusFile, {
25
+ ...baseState,
26
+ ...extra,
27
+ status,
28
+ updatedAt: nowIso(),
29
+ });
30
+ };
31
+ }
7
32
 
8
33
  function runChild(command, args, options = {}) {
9
34
  return new Promise((resolve) => {
@@ -19,12 +44,81 @@ function runChild(command, args, options = {}) {
19
44
 
20
45
  let stdout = "";
21
46
  let stderr = "";
47
+ let stdoutBytes = 0;
48
+ let stderrBytes = 0;
49
+ const startedAt = Date.now();
50
+ let lastOutputAt = startedAt;
51
+ let watchdogTriggered = false;
52
+ let watchdogReason = "";
53
+ let stallWarned = false;
54
+ let killTimer = null;
55
+
56
+ const emitHeartbeat = (status, extra = {}) => {
57
+ options.onHeartbeat?.({
58
+ status,
59
+ pid: child.pid ?? null,
60
+ startedAt: new Date(startedAt).toISOString(),
61
+ lastOutputAt: new Date(lastOutputAt).toISOString(),
62
+ stdoutBytes,
63
+ stderrBytes,
64
+ idleSeconds: Math.max(0, Math.floor((Date.now() - lastOutputAt) / 1000)),
65
+ watchdogTriggered,
66
+ watchdogReason,
67
+ ...extra,
68
+ });
69
+ };
70
+
71
+ const heartbeatIntervalMs = Math.max(100, Number(options.heartbeatIntervalMs || 0));
72
+ const stallWarningMs = Math.max(0, Number(options.stallWarningMs || 0));
73
+ const maxIdleMs = Math.max(0, Number(options.maxIdleMs || 0));
74
+ const killGraceMs = Math.max(100, Number(options.killGraceMs || 1000));
75
+
76
+ const heartbeatTimer = heartbeatIntervalMs > 0
77
+ ? setInterval(() => {
78
+ const idleMs = Date.now() - lastOutputAt;
79
+ if (stallWarningMs > 0 && idleMs >= stallWarningMs && !stallWarned) {
80
+ stallWarned = true;
81
+ emitHeartbeat("suspected_stall", {
82
+ message: `当前子进程已连续 ${Math.floor(idleMs / 1000)} 秒没有可见输出,继续观察。`,
83
+ });
84
+ }
85
+
86
+ if (maxIdleMs > 0 && idleMs >= maxIdleMs && !watchdogTriggered) {
87
+ watchdogTriggered = true;
88
+ watchdogReason = `当前子进程已连续 ${Math.floor(idleMs / 1000)} 秒没有可见输出。`;
89
+ stderr = [
90
+ stderr.trim(),
91
+ `[HelloLoop watchdog] ${watchdogReason}`,
92
+ ].filter(Boolean).join("\n");
93
+ emitHeartbeat("watchdog_terminating", {
94
+ message: "已达到无人值守恢复阈值,准备终止当前子进程并发起同引擎恢复。",
95
+ });
96
+ child.kill();
97
+ killTimer = setTimeout(() => {
98
+ child.kill("SIGKILL");
99
+ }, killGraceMs);
100
+ return;
101
+ }
102
+
103
+ emitHeartbeat(watchdogTriggered ? "watchdog_waiting" : "running");
104
+ }, heartbeatIntervalMs)
105
+ : null;
106
+
107
+ emitHeartbeat("running");
22
108
 
23
109
  child.stdout.on("data", (chunk) => {
24
110
  stdout += chunk.toString();
111
+ stdoutBytes += chunk.length;
112
+ lastOutputAt = Date.now();
113
+ stallWarned = false;
114
+ emitHeartbeat("running");
25
115
  });
26
116
  child.stderr.on("data", (chunk) => {
27
117
  stderr += chunk.toString();
118
+ stderrBytes += chunk.length;
119
+ lastOutputAt = Date.now();
120
+ stallWarned = false;
121
+ emitHeartbeat("running");
28
122
  });
29
123
 
30
124
  if (options.stdin) {
@@ -33,26 +127,58 @@ function runChild(command, args, options = {}) {
33
127
  child.stdin.end();
34
128
 
35
129
  child.on("error", (error) => {
130
+ if (heartbeatTimer) {
131
+ clearInterval(heartbeatTimer);
132
+ }
133
+ if (killTimer) {
134
+ clearTimeout(killTimer);
135
+ }
136
+ emitHeartbeat("failed", {
137
+ code: 1,
138
+ signal: "",
139
+ });
36
140
  resolve({
37
141
  ok: false,
38
142
  code: 1,
39
143
  stdout,
40
144
  stderr: String(error?.stack || error || ""),
145
+ signal: "",
146
+ startedAt: new Date(startedAt).toISOString(),
147
+ finishedAt: nowIso(),
148
+ idleTimeout: watchdogTriggered,
149
+ watchdogTriggered,
150
+ watchdogReason,
41
151
  });
42
152
  });
43
153
 
44
- child.on("close", (code) => {
154
+ child.on("close", (code, signal) => {
155
+ if (heartbeatTimer) {
156
+ clearInterval(heartbeatTimer);
157
+ }
158
+ if (killTimer) {
159
+ clearTimeout(killTimer);
160
+ }
161
+ emitHeartbeat(code === 0 ? "completed" : "failed", {
162
+ code: code ?? 1,
163
+ signal: signal || "",
164
+ });
45
165
  resolve({
46
166
  ok: code === 0,
47
167
  code: code ?? 1,
48
168
  stdout,
49
169
  stderr,
170
+ signal: signal || "",
171
+ startedAt: new Date(startedAt).toISOString(),
172
+ finishedAt: nowIso(),
173
+ idleTimeout: watchdogTriggered,
174
+ watchdogTriggered,
175
+ watchdogReason,
50
176
  });
51
177
  });
52
178
  });
53
179
  }
54
180
 
55
- function writeCodexRunArtifacts(runDir, prefix, result, finalMessage) {
181
+ function writeEngineRunArtifacts(runDir, prefix, result, finalMessage) {
56
182
  writeText(path.join(runDir, `${prefix}-stdout.log`), result.stdout);
57
183
  writeText(path.join(runDir, `${prefix}-stderr.log`), result.stderr);
58
184
  writeText(path.join(runDir, `${prefix}-summary.txt`), [
@@ -64,25 +190,57 @@ function writeCodexRunArtifacts(runDir, prefix, result, finalMessage) {
64
190
  ].join("\n"));
65
191
  }
66
192
 
67
- export async function runCodexTask({
193
+ function readSchemaText(outputSchemaFile = "") {
194
+ return outputSchemaFile && fs.existsSync(outputSchemaFile)
195
+ ? fs.readFileSync(outputSchemaFile, "utf8").trim()
196
+ : "";
197
+ }
198
+
199
+ function resolveEngineInvocation(engine, explicitExecutable = "") {
200
+ const envExecutable = String(process.env[`HELLOLOOP_${String(engine || "").toUpperCase()}_EXECUTABLE`] || "").trim();
201
+ const executable = envExecutable || explicitExecutable;
202
+ if (engine === "codex") {
203
+ return resolveCodexInvocation({ explicitExecutable: executable });
204
+ }
205
+
206
+ const meta = {
207
+ claude: {
208
+ commandName: "claude",
209
+ displayName: "Claude",
210
+ },
211
+ gemini: {
212
+ commandName: "gemini",
213
+ displayName: "Gemini",
214
+ },
215
+ }[engine];
216
+
217
+ if (!meta) {
218
+ return {
219
+ command: "",
220
+ argsPrefix: [],
221
+ shell: false,
222
+ error: `不支持的执行引擎:${engine}`,
223
+ };
224
+ }
225
+
226
+ return resolveCliInvocation({
227
+ commandName: meta.commandName,
228
+ toolDisplayName: meta.displayName,
229
+ explicitExecutable: executable,
230
+ });
231
+ }
232
+
233
+ function buildCodexArgs({
68
234
  context,
69
- prompt,
70
- runDir,
71
235
  model = "",
72
- executable = "",
73
236
  sandbox = "workspace-write",
74
237
  dangerouslyBypassSandbox = false,
75
238
  jsonOutput = true,
76
239
  outputSchemaFile = "",
77
- outputPrefix = "codex",
78
240
  ephemeral = false,
79
241
  skipGitRepoCheck = false,
80
- env = {},
242
+ lastMessageFile,
81
243
  }) {
82
- ensureDir(runDir);
83
-
84
- const lastMessageFile = path.join(runDir, `${outputPrefix}-last-message.txt`);
85
- const invocation = resolveCodexInvocation({ explicitExecutable: executable });
86
244
  const codexArgs = ["exec", "-C", context.repoRoot];
87
245
 
88
246
  if (model) {
@@ -106,6 +264,123 @@ export async function runCodexTask({
106
264
  codexArgs.push("--json");
107
265
  }
108
266
  codexArgs.push("-o", lastMessageFile, "-");
267
+ return codexArgs;
268
+ }
269
+
270
+ function buildClaudeArgs({
271
+ model = "",
272
+ outputSchemaFile = "",
273
+ executionMode = "analyze",
274
+ policy = {},
275
+ }) {
276
+ const args = [
277
+ "-p",
278
+ executionMode === "analyze"
279
+ ? "请读取标准输入中的完整分析任务并直接输出最终结果。"
280
+ : "请读取标准输入中的完整开发任务并直接完成它。",
281
+ "--output-format",
282
+ policy.outputFormat || "text",
283
+ "--permission-mode",
284
+ executionMode === "analyze"
285
+ ? (policy.analysisPermissionMode || "plan")
286
+ : (policy.permissionMode || "bypassPermissions"),
287
+ "--no-session-persistence",
288
+ ];
289
+
290
+ if (model) {
291
+ args.push("--model", model);
292
+ }
293
+
294
+ const schemaText = readSchemaText(outputSchemaFile);
295
+ if (schemaText) {
296
+ args.push("--json-schema", schemaText);
297
+ }
298
+
299
+ return args;
300
+ }
301
+
302
+ function buildGeminiArgs({
303
+ model = "",
304
+ executionMode = "analyze",
305
+ policy = {},
306
+ }) {
307
+ const args = [
308
+ "-p",
309
+ executionMode === "analyze"
310
+ ? "请读取标准输入中的完整分析任务并直接输出最终结果。"
311
+ : "请读取标准输入中的完整开发任务并直接完成它。",
312
+ "--output-format",
313
+ policy.outputFormat || "text",
314
+ "--approval-mode",
315
+ executionMode === "analyze"
316
+ ? (policy.analysisApprovalMode || "plan")
317
+ : (policy.approvalMode || "yolo"),
318
+ ];
319
+
320
+ if (model) {
321
+ args.push("--model", model);
322
+ }
323
+
324
+ return args;
325
+ }
326
+
327
+ function resolveEnginePolicy(policy = {}, engine) {
328
+ if (engine === "codex") {
329
+ return policy.codex || {};
330
+ }
331
+ if (engine === "claude") {
332
+ return policy.claude || {};
333
+ }
334
+ if (engine === "gemini") {
335
+ return policy.gemini || {};
336
+ }
337
+ return {};
338
+ }
339
+
340
+ export async function runEngineTask({
341
+ engine = "codex",
342
+ context,
343
+ prompt,
344
+ runDir,
345
+ policy = {},
346
+ executionMode = "analyze",
347
+ outputSchemaFile = "",
348
+ outputPrefix = "",
349
+ ephemeral = false,
350
+ skipGitRepoCheck = false,
351
+ env = {},
352
+ }) {
353
+ ensureDir(runDir);
354
+
355
+ const normalizedEngine = normalizeEngineName(engine) || "codex";
356
+ const resolvedPolicy = resolveEnginePolicy(policy, normalizedEngine);
357
+ const prefix = outputPrefix || normalizedEngine;
358
+ const invocation = resolveEngineInvocation(normalizedEngine, resolvedPolicy.executable);
359
+ const recoveryPolicy = resolveRuntimeRecoveryPolicy(policy);
360
+ const runtimeStatusFile = path.join(runDir, `${prefix}-runtime.json`);
361
+ const writeRuntimeStatus = createRuntimeStatusWriter(runtimeStatusFile, {
362
+ engine: normalizedEngine,
363
+ engineDisplayName: getEngineDisplayName(normalizedEngine),
364
+ phase: executionMode,
365
+ outputPrefix: prefix,
366
+ maxPhaseRecoveries: recoveryPolicy.maxPhaseRecoveries,
367
+ });
368
+
369
+ let args = [];
370
+ if (normalizedEngine === "claude") {
371
+ args = buildClaudeArgs({
372
+ model: resolvedPolicy.model,
373
+ outputSchemaFile,
374
+ executionMode,
375
+ policy: resolvedPolicy,
376
+ });
377
+ } else if (normalizedEngine === "gemini") {
378
+ args = buildGeminiArgs({
379
+ model: resolvedPolicy.model,
380
+ executionMode,
381
+ policy: resolvedPolicy,
382
+ });
383
+ }
109
384
 
110
385
  if (invocation.error) {
111
386
  const result = {
@@ -114,43 +389,211 @@ export async function runCodexTask({
114
389
  stdout: "",
115
390
  stderr: invocation.error,
116
391
  };
117
- writeText(path.join(runDir, `${outputPrefix}-prompt.md`), prompt);
118
- writeCodexRunArtifacts(runDir, outputPrefix, result, "");
392
+ writeText(path.join(runDir, `${prefix}-prompt.md`), prompt);
393
+ writeEngineRunArtifacts(runDir, prefix, result, "");
394
+ writeRuntimeStatus("failed", {
395
+ code: result.code,
396
+ message: invocation.error,
397
+ recoveryCount: 0,
398
+ recoveryHistory: [],
399
+ });
119
400
  return { ...result, finalMessage: "" };
120
401
  }
121
402
 
122
- const args = [...invocation.argsPrefix, ...codexArgs];
403
+ const recoveryHistory = [];
404
+ let currentPrompt = prompt;
405
+ let currentRecoveryCount = 0;
123
406
 
124
- const result = await runChild(invocation.command, args, {
125
- cwd: context.repoRoot,
126
- stdin: prompt,
127
- env,
128
- shell: invocation.shell,
129
- });
130
- const finalMessage = fs.existsSync(lastMessageFile)
131
- ? fs.readFileSync(lastMessageFile, "utf8").trim()
132
- : "";
407
+ while (true) {
408
+ const attemptPrefix = currentRecoveryCount === 0
409
+ ? prefix
410
+ : `${prefix}-recovery-${String(currentRecoveryCount).padStart(2, "0")}`;
411
+ const attemptPromptFile = path.join(runDir, `${attemptPrefix}-prompt.md`);
412
+ const attemptLastMessageFile = path.join(runDir, `${attemptPrefix}-last-message.txt`);
413
+ const finalArgs = normalizedEngine === "codex"
414
+ ? [
415
+ ...invocation.argsPrefix,
416
+ ...buildCodexArgs({
417
+ context,
418
+ model: resolvedPolicy.model,
419
+ sandbox: resolvedPolicy.sandbox,
420
+ dangerouslyBypassSandbox: resolvedPolicy.dangerouslyBypassSandbox,
421
+ jsonOutput: resolvedPolicy.jsonOutput !== false,
422
+ outputSchemaFile,
423
+ ephemeral,
424
+ skipGitRepoCheck,
425
+ lastMessageFile: attemptLastMessageFile,
426
+ }),
427
+ ]
428
+ : [...invocation.argsPrefix, ...args];
133
429
 
134
- writeText(path.join(runDir, `${outputPrefix}-prompt.md`), prompt);
135
- writeCodexRunArtifacts(runDir, outputPrefix, result, finalMessage);
430
+ writeRuntimeStatus(currentRecoveryCount > 0 ? "recovering" : "running", {
431
+ attemptPrefix,
432
+ recoveryCount: currentRecoveryCount,
433
+ recoveryHistory,
434
+ });
435
+
436
+ const result = await runChild(invocation.command, finalArgs, {
437
+ cwd: context.repoRoot,
438
+ stdin: currentPrompt,
439
+ env,
440
+ shell: invocation.shell,
441
+ heartbeatIntervalMs: recoveryPolicy.heartbeatIntervalSeconds * 1000,
442
+ stallWarningMs: recoveryPolicy.stallWarningSeconds * 1000,
443
+ maxIdleMs: recoveryPolicy.maxIdleSeconds * 1000,
444
+ killGraceMs: recoveryPolicy.killGraceSeconds * 1000,
445
+ onHeartbeat(payload) {
446
+ writeRuntimeStatus(payload.status, {
447
+ attemptPrefix,
448
+ recoveryCount: currentRecoveryCount,
449
+ recoveryHistory,
450
+ heartbeat: payload,
451
+ });
452
+ },
453
+ });
454
+ const finalMessage = normalizedEngine === "codex"
455
+ ? (fs.existsSync(attemptLastMessageFile) ? fs.readFileSync(attemptLastMessageFile, "utf8").trim() : "")
456
+ : String(result.stdout || "").trim();
136
457
 
137
- return { ...result, finalMessage };
458
+ writeText(attemptPromptFile, currentPrompt);
459
+ writeEngineRunArtifacts(runDir, attemptPrefix, result, finalMessage);
460
+
461
+ const failure = classifyRuntimeRecoveryFailure({
462
+ result: {
463
+ ...result,
464
+ finalMessage,
465
+ },
466
+ recoveryPolicy,
467
+ recoveryCount: currentRecoveryCount,
468
+ });
469
+
470
+ if (
471
+ result.ok
472
+ || !recoveryPolicy.enabled
473
+ || !failure.recoverable
474
+ || currentRecoveryCount >= recoveryPolicy.maxPhaseRecoveries
475
+ ) {
476
+ const finalRecoverySummary = renderRuntimeRecoverySummary(recoveryHistory);
477
+ const finalizedResult = result.ok || !finalRecoverySummary
478
+ ? result
479
+ : {
480
+ ...result,
481
+ stderr: [result.stderr, "", finalRecoverySummary].filter(Boolean).join("\n").trim(),
482
+ };
483
+
484
+ writeText(path.join(runDir, `${prefix}-prompt.md`), currentPrompt);
485
+ writeEngineRunArtifacts(runDir, prefix, finalizedResult, finalMessage);
486
+ if (normalizedEngine === "codex" && finalMessage) {
487
+ writeText(path.join(runDir, `${prefix}-last-message.txt`), finalMessage);
488
+ }
489
+ writeRuntimeStatus(result.ok ? "completed" : "failed", {
490
+ attemptPrefix,
491
+ recoveryCount: currentRecoveryCount,
492
+ recoveryHistory,
493
+ recoverySummary: finalRecoverySummary,
494
+ finalMessage,
495
+ code: finalizedResult.code,
496
+ failureCode: failure.code,
497
+ failureReason: failure.reason,
498
+ });
499
+
500
+ return {
501
+ ...finalizedResult,
502
+ finalMessage,
503
+ recoveryCount: currentRecoveryCount,
504
+ recoveryHistory,
505
+ recoverySummary: finalRecoverySummary,
506
+ recoveryFailure: failure,
507
+ };
508
+ }
509
+
510
+ const nextRecoveryIndex = currentRecoveryCount + 1;
511
+ const delayMs = selectRuntimeRecoveryDelayMs(recoveryPolicy, nextRecoveryIndex);
512
+ const recoveryPrompt = buildRuntimeRecoveryPrompt({
513
+ basePrompt: prompt,
514
+ engine: normalizedEngine,
515
+ phaseLabel: executionMode === "analyze" ? "分析/复核" : "执行",
516
+ failure,
517
+ result: {
518
+ ...result,
519
+ finalMessage,
520
+ },
521
+ nextRecoveryIndex,
522
+ maxRecoveries: recoveryPolicy.maxPhaseRecoveries,
523
+ });
524
+ const recoveryRecord = {
525
+ recoveryIndex: nextRecoveryIndex,
526
+ code: failure.code,
527
+ reason: failure.reason,
528
+ delaySeconds: Math.floor(delayMs / 1000),
529
+ sourceCode: result.code,
530
+ watchdogTriggered: result.watchdogTriggered === true,
531
+ attemptPrefix,
532
+ };
533
+ recoveryHistory.push(recoveryRecord);
534
+ writeJson(path.join(
535
+ runDir,
536
+ `${prefix}-auto-recovery-${String(nextRecoveryIndex).padStart(2, "0")}.json`,
537
+ ), {
538
+ ...recoveryRecord,
539
+ engine: normalizedEngine,
540
+ phase: executionMode,
541
+ stdoutTail: tailText(result.stdout, 20),
542
+ stderrTail: tailText(result.stderr, 20),
543
+ finalMessageTail: tailText(finalMessage, 20),
544
+ createdAt: nowIso(),
545
+ });
546
+ writeText(
547
+ path.join(runDir, `${prefix}-auto-recovery-${String(nextRecoveryIndex).padStart(2, "0")}-prompt.md`),
548
+ recoveryPrompt,
549
+ );
550
+ writeRuntimeStatus("retry_waiting", {
551
+ attemptPrefix,
552
+ recoveryCount: nextRecoveryIndex,
553
+ recoveryHistory,
554
+ nextRetryDelayMs: delayMs,
555
+ failureCode: failure.code,
556
+ failureReason: failure.reason,
557
+ });
558
+ if (delayMs > 0) {
559
+ await sleep(delayMs);
560
+ }
561
+ currentPrompt = recoveryPrompt;
562
+ currentRecoveryCount = nextRecoveryIndex;
563
+ }
564
+ }
565
+
566
+ export async function runCodexTask(options) {
567
+ return runEngineTask({
568
+ ...options,
569
+ engine: "codex",
570
+ });
138
571
  }
139
572
 
140
573
  export async function runCodexExec({ context, prompt, runDir, policy }) {
141
- return runCodexTask({
574
+ return runEngineTask({
575
+ engine: "codex",
142
576
  context,
143
577
  prompt,
144
578
  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,
579
+ policy,
580
+ executionMode: "execute",
150
581
  outputPrefix: "codex",
151
582
  });
152
583
  }
153
584
 
585
+ export async function runEngineExec({ engine, context, prompt, runDir, policy }) {
586
+ return runEngineTask({
587
+ engine,
588
+ context,
589
+ prompt,
590
+ runDir,
591
+ policy,
592
+ executionMode: "execute",
593
+ outputPrefix: engine,
594
+ });
595
+ }
596
+
154
597
  export async function runShellCommand(context, commandLine, runDir, index) {
155
598
  const shellInvocation = resolveVerifyShellInvocation();
156
599
  if (shellInvocation.error) {
@@ -195,10 +638,10 @@ export async function runVerifyCommands(context, commands, runDir) {
195
638
  ok: false,
196
639
  results,
197
640
  failed: result,
198
- summary: [
199
- `验证失败:${result.command}`,
200
- "",
201
- "stdout 尾部:",
641
+ summary: [
642
+ `验证失败:${result.command}`,
643
+ "",
644
+ "stdout 尾部:",
202
645
  tailText(result.stdout, 40),
203
646
  "",
204
647
  "stderr 尾部:",
package/src/prompt.mjs CHANGED
@@ -2,6 +2,7 @@ import { formatList } from "./common.mjs";
2
2
  import {
3
3
  hasCustomProjectConstraints,
4
4
  listMandatoryGuardrails,
5
+ listMandatoryEngineeringPrinciples,
5
6
  resolveProjectConstraints,
6
7
  } from "./guardrails.mjs";
7
8
 
@@ -69,6 +70,7 @@ export function buildTaskPrompt({
69
70
  ...(task.docs || []),
70
71
  ]);
71
72
  const mandatoryGuardrails = listMandatoryGuardrails();
73
+ const mandatoryEngineeringPrinciples = listMandatoryEngineeringPrinciples();
72
74
  const effectiveConstraints = resolveProjectConstraints(constraints);
73
75
  const usingFallbackConstraints = !hasCustomProjectConstraints(constraints);
74
76
 
@@ -93,6 +95,7 @@ export function buildTaskPrompt({
93
95
  listSection("涉及路径", task.paths || []),
94
96
  listSection("验收条件", task.acceptance || []),
95
97
  listSection("内建安全底线", mandatoryGuardrails),
98
+ listSection("强制编码与产出基线", mandatoryEngineeringPrinciples),
96
99
  listSection(usingFallbackConstraints ? "默认工程约束(文档未明确时生效)" : "项目/用户约束", effectiveConstraints),
97
100
  repoStateText ? section("仓库当前状态", repoStateText) : "",
98
101
  failureHistory.length
@@ -104,11 +107,12 @@ export function buildTaskPrompt({
104
107
  listSection("完成前必须运行的验证", verifyCommands),
105
108
  section("交付要求", [
106
109
  "1. 直接在仓库中完成实现。",
107
- "2. 运行验证;若失败,先分析根因,再修复并重跑。",
108
- "3. 同一路径连续失败后,必须明确换一种实现或排查思路。",
109
- "4. 除非遇到外部权限、环境损坏、文档缺口等硬阻塞,否则不要停止。",
110
- "5. 用简洁中文总结变更、验证结果和剩余风险。",
111
- "6. 不要提问,不要等待确认,直接完成。",
110
+ "2. 用户需求明确且当前任务可完成时,必须一次性做完本轮应交付的全部工作,不要做半成品后停下来问“是否继续”或“如果你要我可以继续”。",
111
+ "3. 运行验证;若失败,先分析根因,再修复并重跑。",
112
+ "4. 同一路径连续失败后,必须明确换一种实现或排查思路。",
113
+ "5. 除非遇到外部权限、环境损坏、文档缺口等硬阻塞,或确实需要用户做关键决策,否则不要停止。",
114
+ "6. 不要提问,不要等待确认,不要把“下一步建议”当成提前停下的理由,直接完成当前任务。",
115
+ "7. 最终只用简洁中文总结必要的变更、验证结果和剩余风险;禁止使用“如果你要”“如果你需要进一步…”“希望这对你有帮助”等套话收尾。",
112
116
  ].join("\n")),
113
117
  ].filter(Boolean).join("\n");
114
118
  }
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs";
2
+ import { createInterface } from "node:readline/promises";
3
+
4
+ let bufferedAnswers = null;
5
+ let bufferedAnswerIndex = 0;
6
+
7
+ function loadBufferedAnswers() {
8
+ if (!bufferedAnswers) {
9
+ bufferedAnswers = fs.readFileSync(0, "utf8").split(/\r?\n/);
10
+ }
11
+ return bufferedAnswers;
12
+ }
13
+
14
+ export function createPromptSession() {
15
+ if (process.stdin.isTTY) {
16
+ const readline = createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout,
19
+ });
20
+ return {
21
+ async question(promptText) {
22
+ return readline.question(promptText);
23
+ },
24
+ close() {
25
+ readline.close();
26
+ },
27
+ };
28
+ }
29
+
30
+ return {
31
+ async question(promptText) {
32
+ process.stdout.write(promptText);
33
+ const answers = loadBufferedAnswers();
34
+ const answer = answers[bufferedAnswerIndex] ?? "";
35
+ bufferedAnswerIndex += 1;
36
+ return answer;
37
+ },
38
+ close() {},
39
+ };
40
+ }