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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +3 -3
- package/README.md +297 -272
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +19 -9
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -4
- package/hosts/gemini/extension/GEMINI.md +13 -4
- package/hosts/gemini/extension/commands/helloloop.toml +19 -8
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/scripts/uninstall-home-plugin.ps1 +25 -0
- package/skills/helloloop/SKILL.md +42 -7
- package/src/analyze_confirmation.mjs +108 -8
- package/src/analyze_prompt.mjs +17 -1
- package/src/analyze_user_input.mjs +321 -0
- package/src/analyzer.mjs +167 -42
- package/src/cli.mjs +34 -308
- 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 +95 -31
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +50 -0
- package/src/discovery.mjs +243 -9
- package/src/discovery_inference.mjs +62 -18
- package/src/discovery_paths.mjs +143 -8
- package/src/discovery_prompt.mjs +273 -0
- 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 +20 -266
- 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/rebuild.mjs +116 -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 +58 -1
- package/templates/policy.template.json +27 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { ensureDir, fileExists, readJson, writeJson } from "./common.mjs";
|
|
6
|
+
|
|
7
|
+
export const runtimeBundleEntries = [
|
|
8
|
+
".claude-plugin",
|
|
9
|
+
".codex-plugin",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md",
|
|
12
|
+
"bin",
|
|
13
|
+
"hosts",
|
|
14
|
+
"package.json",
|
|
15
|
+
"scripts",
|
|
16
|
+
"skills",
|
|
17
|
+
"src",
|
|
18
|
+
"templates",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const codexBundleEntries = runtimeBundleEntries.filter((entry) => ![
|
|
22
|
+
".claude-plugin",
|
|
23
|
+
"hosts",
|
|
24
|
+
].includes(entry));
|
|
25
|
+
|
|
26
|
+
export const supportedHosts = ["codex", "claude", "gemini"];
|
|
27
|
+
export const CLAUDE_MARKETPLACE_NAME = "helloloop-local";
|
|
28
|
+
export const CLAUDE_PLUGIN_KEY = "helloloop@helloloop-local";
|
|
29
|
+
|
|
30
|
+
export function resolveHomeDir(homeDir, defaultDirName) {
|
|
31
|
+
return path.resolve(homeDir || path.join(os.homedir(), defaultDirName));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function assertPathInside(parentDir, targetDir, label) {
|
|
35
|
+
const relative = path.relative(parentDir, targetDir);
|
|
36
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
37
|
+
throw new Error(`${label} 超出允许范围:${targetDir}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function removeTargetIfNeeded(targetPath, force) {
|
|
42
|
+
if (!fileExists(targetPath)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!force) {
|
|
46
|
+
throw new Error(`目标目录已存在:${targetPath}。若要覆盖,请追加 --force。`);
|
|
47
|
+
}
|
|
48
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function removePathIfExists(targetPath) {
|
|
52
|
+
if (!fileExists(targetPath)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function copyBundleEntries(bundleRoot, targetRoot, entries) {
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const sourcePath = path.join(bundleRoot, entry);
|
|
62
|
+
if (!fileExists(sourcePath)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fs.cpSync(sourcePath, path.join(targetRoot, entry), {
|
|
67
|
+
force: true,
|
|
68
|
+
recursive: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function copyDirectory(sourceRoot, targetRoot) {
|
|
74
|
+
fs.cpSync(sourceRoot, targetRoot, {
|
|
75
|
+
force: true,
|
|
76
|
+
recursive: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function loadOrInitJson(filePath, fallbackValue) {
|
|
81
|
+
if (!fileExists(filePath)) {
|
|
82
|
+
return fallbackValue;
|
|
83
|
+
}
|
|
84
|
+
return readJson(filePath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function writeJsonFile(filePath, value) {
|
|
88
|
+
ensureDir(path.dirname(filePath));
|
|
89
|
+
writeJson(filePath, value);
|
|
90
|
+
}
|
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 尾部:",
|