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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +3 -3
- package/README.md +157 -81
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +13 -10
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +9 -4
- package/hosts/gemini/extension/GEMINI.md +12 -7
- package/hosts/gemini/extension/commands/helloloop.toml +14 -10
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/skills/helloloop/SKILL.md +16 -6
- package/src/analyze_confirmation.mjs +29 -5
- package/src/analyze_prompt.mjs +5 -1
- package/src/analyze_user_input.mjs +20 -2
- package/src/analyzer.mjs +130 -43
- package/src/cli.mjs +32 -492
- package/src/cli_analyze_command.mjs +248 -0
- package/src/cli_args.mjs +106 -0
- package/src/cli_command_handlers.mjs +120 -0
- package/src/cli_context.mjs +31 -0
- package/src/cli_render.mjs +70 -0
- package/src/cli_support.mjs +11 -14
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +50 -0
- package/src/discovery_prompt.mjs +2 -27
- package/src/engine_metadata.mjs +79 -0
- package/src/engine_selection.mjs +335 -0
- package/src/engine_selection_failure.mjs +51 -0
- package/src/engine_selection_messages.mjs +119 -0
- package/src/engine_selection_probe.mjs +78 -0
- package/src/engine_selection_prompt.mjs +48 -0
- package/src/engine_selection_settings.mjs +38 -0
- package/src/guardrails.mjs +15 -4
- package/src/install.mjs +6 -405
- package/src/install_claude.mjs +189 -0
- package/src/install_codex.mjs +114 -0
- package/src/install_gemini.mjs +43 -0
- package/src/install_shared.mjs +90 -0
- package/src/process.mjs +482 -39
- package/src/prompt.mjs +9 -5
- package/src/prompt_session.mjs +40 -0
- package/src/runner.mjs +3 -341
- package/src/runner_execute_task.mjs +301 -0
- package/src/runner_execution_support.mjs +155 -0
- package/src/runner_loop.mjs +106 -0
- package/src/runner_once.mjs +29 -0
- package/src/runner_status.mjs +104 -0
- package/src/runtime_recovery.mjs +301 -0
- package/src/shell_invocation.mjs +16 -0
- package/templates/analysis-output.schema.json +0 -1
- package/templates/policy.template.json +27 -0
- package/templates/project.template.json +2 -0
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, `${
|
|
118
|
-
|
|
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
|
|
403
|
+
const recoveryHistory = [];
|
|
404
|
+
let currentPrompt = prompt;
|
|
405
|
+
let currentRecoveryCount = 0;
|
|
123
406
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
574
|
+
return runEngineTask({
|
|
575
|
+
engine: "codex",
|
|
142
576
|
context,
|
|
143
577
|
prompt,
|
|
144
578
|
runDir,
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
}
|