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/runner.mjs
CHANGED
|
@@ -1,341 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ensureDir,
|
|
5
|
-
nowIso,
|
|
6
|
-
sanitizeId,
|
|
7
|
-
tailText,
|
|
8
|
-
timestampForFile,
|
|
9
|
-
writeText,
|
|
10
|
-
} from "./common.mjs";
|
|
11
|
-
import {
|
|
12
|
-
loadBacklog,
|
|
13
|
-
loadPolicy,
|
|
14
|
-
loadProjectConfig,
|
|
15
|
-
loadRepoStateText,
|
|
16
|
-
loadVerifyCommands,
|
|
17
|
-
saveBacklog,
|
|
18
|
-
writeStateMarkdown,
|
|
19
|
-
writeStatus,
|
|
20
|
-
} from "./config.mjs";
|
|
21
|
-
import {
|
|
22
|
-
getTask,
|
|
23
|
-
renderTaskSummary,
|
|
24
|
-
selectNextTask,
|
|
25
|
-
summarizeBacklog,
|
|
26
|
-
unresolvedDependencies,
|
|
27
|
-
updateTask,
|
|
28
|
-
} from "./backlog.mjs";
|
|
29
|
-
import { buildTaskPrompt } from "./prompt.mjs";
|
|
30
|
-
import { runCodexExec, runVerifyCommands } from "./process.mjs";
|
|
31
|
-
|
|
32
|
-
function makeRunDir(context, taskId) {
|
|
33
|
-
return path.join(context.runsDir, `${timestampForFile()}-${sanitizeId(taskId)}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function makeAttemptDir(runDir, strategyIndex, attemptIndex) {
|
|
37
|
-
return path.join(
|
|
38
|
-
runDir,
|
|
39
|
-
`strategy-${String(strategyIndex).padStart(2, "0")}-attempt-${String(attemptIndex).padStart(2, "0")}`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isHardStopFailure(kind, summary) {
|
|
44
|
-
const normalized = String(summary || "").toLowerCase();
|
|
45
|
-
if (!normalized) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (kind === "codex" && normalized.includes("enoent")) {
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return [
|
|
54
|
-
"command not found",
|
|
55
|
-
"is not recognized",
|
|
56
|
-
"无法将",
|
|
57
|
-
"找不到路径",
|
|
58
|
-
"no such file or directory",
|
|
59
|
-
"permission denied",
|
|
60
|
-
"access is denied",
|
|
61
|
-
].some((signal) => normalized.includes(signal));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function buildExhaustedSummary({
|
|
65
|
-
failureHistory,
|
|
66
|
-
maxStrategies,
|
|
67
|
-
maxAttemptsPerStrategy,
|
|
68
|
-
}) {
|
|
69
|
-
const lastFailure = failureHistory.at(-1)?.summary || "未知失败。";
|
|
70
|
-
return [
|
|
71
|
-
`已按 Ralph Loop 执行 ${maxStrategies} 轮策略、每轮最多 ${maxAttemptsPerStrategy} 次重试,当前任务仍未收敛。`,
|
|
72
|
-
"",
|
|
73
|
-
"最后一次失败信息:",
|
|
74
|
-
lastFailure,
|
|
75
|
-
].join("\n").trim();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function renderStatusMarkdown(context, { summary, currentTask, lastResult, nextTask }) {
|
|
79
|
-
return [
|
|
80
|
-
"## 当前状态",
|
|
81
|
-
`- backlog 文件:${context.backlogFile.replaceAll("\\", "/")}`,
|
|
82
|
-
`- 总任务数:${summary.total}`,
|
|
83
|
-
`- 已完成:${summary.done}`,
|
|
84
|
-
`- 待处理:${summary.pending}`,
|
|
85
|
-
`- 进行中:${summary.inProgress}`,
|
|
86
|
-
`- 失败:${summary.failed}`,
|
|
87
|
-
`- 阻塞:${summary.blocked}`,
|
|
88
|
-
`- 当前任务:${currentTask ? currentTask.title : "无"}`,
|
|
89
|
-
`- 最近结果:${lastResult || "暂无"}`,
|
|
90
|
-
`- 下一建议:${nextTask ? nextTask.title : "暂无可执行任务"}`,
|
|
91
|
-
].join("\n");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function resolveTask(backlog, options) {
|
|
95
|
-
if (options.taskId) {
|
|
96
|
-
const task = getTask(backlog, options.taskId);
|
|
97
|
-
if (!task) throw new Error(`未找到任务:${options.taskId}`);
|
|
98
|
-
return task;
|
|
99
|
-
}
|
|
100
|
-
return selectNextTask(backlog, options);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function buildFailureSummary(kind, payload) {
|
|
104
|
-
if (kind === "codex") {
|
|
105
|
-
return [
|
|
106
|
-
`Codex 执行失败,退出码:${payload.code}`,
|
|
107
|
-
"",
|
|
108
|
-
"stdout 尾部:",
|
|
109
|
-
tailText(payload.stdout, 60),
|
|
110
|
-
"",
|
|
111
|
-
"stderr 尾部:",
|
|
112
|
-
tailText(payload.stderr, 60),
|
|
113
|
-
].join("\n").trim();
|
|
114
|
-
}
|
|
115
|
-
return payload.summary;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function executeSingleTask(context, options = {}) {
|
|
119
|
-
const policy = loadPolicy(context);
|
|
120
|
-
const projectConfig = loadProjectConfig(context);
|
|
121
|
-
const backlog = loadBacklog(context);
|
|
122
|
-
const repoStateText = loadRepoStateText(context);
|
|
123
|
-
const task = resolveTask(backlog, options);
|
|
124
|
-
|
|
125
|
-
if (!task) {
|
|
126
|
-
const summary = summarizeBacklog(backlog);
|
|
127
|
-
writeStatus(context, { ok: true, stage: "idle", summary });
|
|
128
|
-
writeStateMarkdown(context, renderStatusMarkdown(context, {
|
|
129
|
-
summary,
|
|
130
|
-
currentTask: null,
|
|
131
|
-
lastResult: "没有可执行任务",
|
|
132
|
-
nextTask: null,
|
|
133
|
-
}));
|
|
134
|
-
return { ok: true, kind: "idle", task: null };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const unresolved = unresolvedDependencies(backlog, task);
|
|
138
|
-
if (unresolved.length) {
|
|
139
|
-
throw new Error(`任务 ${task.id} 仍有未完成依赖:${unresolved.join(", ")}`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const verifyCommands = Array.isArray(task.verify) && task.verify.length
|
|
143
|
-
? task.verify
|
|
144
|
-
: loadVerifyCommands(context);
|
|
145
|
-
const runDir = makeRunDir(context, task.id);
|
|
146
|
-
const requiredDocs = [
|
|
147
|
-
...(projectConfig.requiredDocs || []),
|
|
148
|
-
...(options.requiredDocs || []),
|
|
149
|
-
];
|
|
150
|
-
const constraints = [
|
|
151
|
-
...(projectConfig.constraints || []),
|
|
152
|
-
...(options.constraints || []),
|
|
153
|
-
];
|
|
154
|
-
const maxAttemptsPerStrategy = Math.max(1, Number(options.maxAttempts || policy.maxTaskAttempts || 1));
|
|
155
|
-
const configuredStrategies = Math.max(1, Number(options.maxStrategies || policy.maxTaskStrategies || 1));
|
|
156
|
-
const maxStrategies = policy.stopOnFailure ? 1 : configuredStrategies;
|
|
157
|
-
|
|
158
|
-
if (options.dryRun) {
|
|
159
|
-
const prompt = buildTaskPrompt({
|
|
160
|
-
task,
|
|
161
|
-
repoStateText,
|
|
162
|
-
verifyCommands,
|
|
163
|
-
requiredDocs,
|
|
164
|
-
constraints,
|
|
165
|
-
strategyIndex: 1,
|
|
166
|
-
maxStrategies,
|
|
167
|
-
attemptIndex: 1,
|
|
168
|
-
maxAttemptsPerStrategy,
|
|
169
|
-
});
|
|
170
|
-
ensureDir(runDir);
|
|
171
|
-
writeText(path.join(runDir, "codex-prompt.md"), prompt);
|
|
172
|
-
return { ok: true, kind: "dry-run", task, runDir, prompt, verifyCommands };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
updateTask(backlog, task.id, { status: "in_progress", startedAt: nowIso() });
|
|
176
|
-
saveBacklog(context, backlog);
|
|
177
|
-
|
|
178
|
-
let previousFailure = "";
|
|
179
|
-
const failureHistory = [];
|
|
180
|
-
|
|
181
|
-
for (let strategyIndex = 1; strategyIndex <= maxStrategies; strategyIndex += 1) {
|
|
182
|
-
for (let attemptIndex = 1; attemptIndex <= maxAttemptsPerStrategy; attemptIndex += 1) {
|
|
183
|
-
const prompt = buildTaskPrompt({
|
|
184
|
-
task,
|
|
185
|
-
repoStateText,
|
|
186
|
-
verifyCommands,
|
|
187
|
-
requiredDocs,
|
|
188
|
-
constraints,
|
|
189
|
-
previousFailure,
|
|
190
|
-
failureHistory,
|
|
191
|
-
strategyIndex,
|
|
192
|
-
maxStrategies,
|
|
193
|
-
attemptIndex,
|
|
194
|
-
maxAttemptsPerStrategy,
|
|
195
|
-
});
|
|
196
|
-
const attemptDir = makeAttemptDir(runDir, strategyIndex, attemptIndex);
|
|
197
|
-
const codexResult = await runCodexExec({ context, prompt, runDir: attemptDir, policy });
|
|
198
|
-
|
|
199
|
-
if (!codexResult.ok) {
|
|
200
|
-
previousFailure = buildFailureSummary("codex", codexResult);
|
|
201
|
-
failureHistory.push({
|
|
202
|
-
strategyIndex,
|
|
203
|
-
attemptIndex,
|
|
204
|
-
kind: "codex",
|
|
205
|
-
summary: previousFailure,
|
|
206
|
-
});
|
|
207
|
-
if (isHardStopFailure("codex", previousFailure)) {
|
|
208
|
-
updateTask(backlog, task.id, {
|
|
209
|
-
status: "failed",
|
|
210
|
-
finishedAt: nowIso(),
|
|
211
|
-
lastFailure: previousFailure,
|
|
212
|
-
attempts: failureHistory.length,
|
|
213
|
-
});
|
|
214
|
-
saveBacklog(context, backlog);
|
|
215
|
-
return { ok: false, kind: "codex-failed", task, runDir, summary: previousFailure };
|
|
216
|
-
}
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const verifyResult = await runVerifyCommands(context, verifyCommands, attemptDir);
|
|
221
|
-
if (verifyResult.ok) {
|
|
222
|
-
updateTask(backlog, task.id, {
|
|
223
|
-
status: "done",
|
|
224
|
-
finishedAt: nowIso(),
|
|
225
|
-
lastFailure: "",
|
|
226
|
-
attempts: failureHistory.length + 1,
|
|
227
|
-
});
|
|
228
|
-
saveBacklog(context, backlog);
|
|
229
|
-
return {
|
|
230
|
-
ok: true,
|
|
231
|
-
kind: "done",
|
|
232
|
-
task,
|
|
233
|
-
runDir,
|
|
234
|
-
finalMessage: codexResult.finalMessage,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
previousFailure = buildFailureSummary("verify", verifyResult);
|
|
239
|
-
failureHistory.push({
|
|
240
|
-
strategyIndex,
|
|
241
|
-
attemptIndex,
|
|
242
|
-
kind: "verify",
|
|
243
|
-
summary: previousFailure,
|
|
244
|
-
});
|
|
245
|
-
if (isHardStopFailure("verify", previousFailure)) {
|
|
246
|
-
updateTask(backlog, task.id, {
|
|
247
|
-
status: "failed",
|
|
248
|
-
finishedAt: nowIso(),
|
|
249
|
-
lastFailure: previousFailure,
|
|
250
|
-
attempts: failureHistory.length,
|
|
251
|
-
});
|
|
252
|
-
saveBacklog(context, backlog);
|
|
253
|
-
return { ok: false, kind: "verify-failed", task, runDir, summary: previousFailure };
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
previousFailure = [
|
|
258
|
-
previousFailure,
|
|
259
|
-
"",
|
|
260
|
-
`上一种策略已连续失败 ${maxAttemptsPerStrategy} 次。下一轮必须明确更换实现或排查思路,不能重复原路径。`,
|
|
261
|
-
].join("\n").trim();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const exhaustedSummary = buildExhaustedSummary({
|
|
265
|
-
failureHistory,
|
|
266
|
-
maxStrategies,
|
|
267
|
-
maxAttemptsPerStrategy,
|
|
268
|
-
});
|
|
269
|
-
updateTask(backlog, task.id, {
|
|
270
|
-
status: "failed",
|
|
271
|
-
finishedAt: nowIso(),
|
|
272
|
-
lastFailure: exhaustedSummary,
|
|
273
|
-
attempts: failureHistory.length,
|
|
274
|
-
});
|
|
275
|
-
saveBacklog(context, backlog);
|
|
276
|
-
return { ok: false, kind: "strategy-exhausted", task, runDir, summary: exhaustedSummary };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export async function runOnce(context, options = {}) {
|
|
280
|
-
const result = await executeSingleTask(context, options);
|
|
281
|
-
const backlog = loadBacklog(context);
|
|
282
|
-
const summary = summarizeBacklog(backlog);
|
|
283
|
-
const nextTask = selectNextTask(backlog, options);
|
|
284
|
-
|
|
285
|
-
writeStatus(context, {
|
|
286
|
-
ok: result.ok,
|
|
287
|
-
stage: result.kind,
|
|
288
|
-
taskId: result.task?.id || null,
|
|
289
|
-
taskTitle: result.task?.title || "",
|
|
290
|
-
runDir: result.runDir || "",
|
|
291
|
-
summary,
|
|
292
|
-
message: result.summary || result.finalMessage || "",
|
|
293
|
-
});
|
|
294
|
-
writeStateMarkdown(context, renderStatusMarkdown(context, {
|
|
295
|
-
summary,
|
|
296
|
-
currentTask: result.task,
|
|
297
|
-
lastResult: result.ok ? "本轮成功" : (result.summary || result.kind),
|
|
298
|
-
nextTask,
|
|
299
|
-
}));
|
|
300
|
-
|
|
301
|
-
return result;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export async function runLoop(context, options = {}) {
|
|
305
|
-
const policy = loadPolicy(context);
|
|
306
|
-
const maxTasks = Math.max(1, Number(options.maxTasks || policy.maxLoopTasks || 1));
|
|
307
|
-
const results = [];
|
|
308
|
-
|
|
309
|
-
for (let index = 0; index < maxTasks; index += 1) {
|
|
310
|
-
const result = await runOnce(context, options);
|
|
311
|
-
results.push(result);
|
|
312
|
-
if (options.dryRun) break;
|
|
313
|
-
if (!result.ok || !result.task) break;
|
|
314
|
-
const backlog = loadBacklog(context);
|
|
315
|
-
if (!selectNextTask(backlog, options)) break;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return results;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function renderStatusText(context, options = {}) {
|
|
322
|
-
const backlog = loadBacklog(context);
|
|
323
|
-
const summary = summarizeBacklog(backlog);
|
|
324
|
-
const nextTask = selectNextTask(backlog, options);
|
|
325
|
-
|
|
326
|
-
return [
|
|
327
|
-
"HelloLoop 状态",
|
|
328
|
-
"============",
|
|
329
|
-
`仓库:${context.repoRoot}`,
|
|
330
|
-
`总任务:${summary.total}`,
|
|
331
|
-
`已完成:${summary.done}`,
|
|
332
|
-
`待处理:${summary.pending}`,
|
|
333
|
-
`进行中:${summary.inProgress}`,
|
|
334
|
-
`失败:${summary.failed}`,
|
|
335
|
-
`阻塞:${summary.blocked}`,
|
|
336
|
-
"",
|
|
337
|
-
nextTask ? "下一任务:" : "下一任务:无",
|
|
338
|
-
nextTask ? renderTaskSummary(nextTask) : "",
|
|
339
|
-
].filter(Boolean).join("\n");
|
|
340
|
-
}
|
|
341
|
-
|
|
1
|
+
export { runLoop } from "./runner_loop.mjs";
|
|
2
|
+
export { runOnce } from "./runner_once.mjs";
|
|
3
|
+
export { renderStatusText } from "./runner_status.mjs";
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { rememberEngineSelection } from "./engine_selection.mjs";
|
|
4
|
+
import { getEngineDisplayName } from "./engine_metadata.mjs";
|
|
5
|
+
import { ensureDir, nowIso, writeText } from "./common.mjs";
|
|
6
|
+
import { saveBacklog } from "./config.mjs";
|
|
7
|
+
import { reviewTaskCompletion } from "./completion_review.mjs";
|
|
8
|
+
import { updateTask } from "./backlog.mjs";
|
|
9
|
+
import { buildTaskPrompt } from "./prompt.mjs";
|
|
10
|
+
import { runEngineExec, runVerifyCommands } from "./process.mjs";
|
|
11
|
+
import {
|
|
12
|
+
buildAttemptState,
|
|
13
|
+
buildBlockedResult,
|
|
14
|
+
buildDoneResult,
|
|
15
|
+
buildFailureResult,
|
|
16
|
+
bumpFailureForNextStrategy,
|
|
17
|
+
maybeSwitchEngine,
|
|
18
|
+
recordFailure,
|
|
19
|
+
resolveExecutionSetup,
|
|
20
|
+
} from "./runner_execution_support.mjs";
|
|
21
|
+
import {
|
|
22
|
+
buildExhaustedSummary,
|
|
23
|
+
buildFailureSummary,
|
|
24
|
+
isHardStopFailure,
|
|
25
|
+
makeAttemptDir,
|
|
26
|
+
} from "./runner_status.mjs";
|
|
27
|
+
|
|
28
|
+
async function handleEngineFailure(execution, state, attemptState, engineResult) {
|
|
29
|
+
const previousFailure = buildFailureSummary("engine", {
|
|
30
|
+
...engineResult,
|
|
31
|
+
displayName: getEngineDisplayName(state.engineResolution.engine),
|
|
32
|
+
});
|
|
33
|
+
recordFailure(
|
|
34
|
+
state.failureHistory,
|
|
35
|
+
attemptState.strategyIndex,
|
|
36
|
+
attemptState.attemptIndex,
|
|
37
|
+
state.engineResolution.engine,
|
|
38
|
+
previousFailure,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const nextResolution = await maybeSwitchEngine(execution, state.engineResolution, previousFailure, "执行阶段");
|
|
42
|
+
if (nextResolution) {
|
|
43
|
+
return { action: "switch", previousFailure, engineResolution: nextResolution };
|
|
44
|
+
}
|
|
45
|
+
if (engineResult.recoveryFailure?.recoverable === false) {
|
|
46
|
+
return {
|
|
47
|
+
action: "return",
|
|
48
|
+
result: buildFailureResult(
|
|
49
|
+
execution,
|
|
50
|
+
"engine-failed",
|
|
51
|
+
previousFailure,
|
|
52
|
+
state.failureHistory.length,
|
|
53
|
+
state.engineResolution,
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (isHardStopFailure("engine", previousFailure)) {
|
|
58
|
+
return {
|
|
59
|
+
action: "return",
|
|
60
|
+
result: buildFailureResult(
|
|
61
|
+
execution,
|
|
62
|
+
"engine-failed",
|
|
63
|
+
previousFailure,
|
|
64
|
+
state.failureHistory.length,
|
|
65
|
+
state.engineResolution,
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { action: "continue", previousFailure };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleVerifyFailure(execution, state, attemptState, verifyResult) {
|
|
73
|
+
const previousFailure = buildFailureSummary("verify", verifyResult);
|
|
74
|
+
recordFailure(state.failureHistory, attemptState.strategyIndex, attemptState.attemptIndex, "verify", previousFailure);
|
|
75
|
+
|
|
76
|
+
if (isHardStopFailure("verify", previousFailure)) {
|
|
77
|
+
return {
|
|
78
|
+
action: "return",
|
|
79
|
+
result: buildFailureResult(
|
|
80
|
+
execution,
|
|
81
|
+
"verify-failed",
|
|
82
|
+
previousFailure,
|
|
83
|
+
state.failureHistory.length,
|
|
84
|
+
state.engineResolution,
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { action: "continue", previousFailure };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function handleReviewFailure(execution, state, attemptState, reviewResult) {
|
|
92
|
+
const previousFailure = reviewResult.summary;
|
|
93
|
+
recordFailure(state.failureHistory, attemptState.strategyIndex, attemptState.attemptIndex, "task_review", previousFailure);
|
|
94
|
+
|
|
95
|
+
const nextResolution = await maybeSwitchEngine(execution, state.engineResolution, previousFailure, "任务复核阶段");
|
|
96
|
+
if (nextResolution) {
|
|
97
|
+
return { action: "switch", previousFailure, engineResolution: nextResolution };
|
|
98
|
+
}
|
|
99
|
+
if (reviewResult.raw?.recoveryFailure?.recoverable === false) {
|
|
100
|
+
return {
|
|
101
|
+
action: "return",
|
|
102
|
+
result: buildFailureResult(
|
|
103
|
+
execution,
|
|
104
|
+
"task-review-failed",
|
|
105
|
+
previousFailure,
|
|
106
|
+
state.failureHistory.length,
|
|
107
|
+
state.engineResolution,
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (isHardStopFailure("review", previousFailure)) {
|
|
112
|
+
return {
|
|
113
|
+
action: "return",
|
|
114
|
+
result: buildFailureResult(
|
|
115
|
+
execution,
|
|
116
|
+
"task-review-failed",
|
|
117
|
+
previousFailure,
|
|
118
|
+
state.failureHistory.length,
|
|
119
|
+
state.engineResolution,
|
|
120
|
+
),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { action: "continue", previousFailure };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleIncompleteReview(execution, state, attemptState, reviewResult) {
|
|
127
|
+
const previousFailure = reviewResult.summary;
|
|
128
|
+
recordFailure(
|
|
129
|
+
state.failureHistory,
|
|
130
|
+
attemptState.strategyIndex,
|
|
131
|
+
attemptState.attemptIndex,
|
|
132
|
+
reviewResult.review.verdict === "blocked" ? "blocked" : "task_incomplete",
|
|
133
|
+
previousFailure,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (reviewResult.review.verdict === "blocked") {
|
|
137
|
+
return {
|
|
138
|
+
action: "return",
|
|
139
|
+
result: buildBlockedResult(
|
|
140
|
+
execution,
|
|
141
|
+
previousFailure,
|
|
142
|
+
state.failureHistory.length,
|
|
143
|
+
state.engineResolution,
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { action: "continue", previousFailure };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function handleVerifyAndReview(execution, state, attemptState, engineResult) {
|
|
151
|
+
const verifyResult = await runVerifyCommands(execution.context, execution.verifyCommands, attemptState.attemptDir);
|
|
152
|
+
if (!verifyResult.ok) {
|
|
153
|
+
return handleVerifyFailure(execution, state, attemptState, verifyResult);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const reviewResult = await reviewTaskCompletion({
|
|
157
|
+
engine: state.engineResolution.engine,
|
|
158
|
+
context: execution.context,
|
|
159
|
+
task: execution.task,
|
|
160
|
+
requiredDocs: execution.requiredDocs,
|
|
161
|
+
constraints: execution.constraints,
|
|
162
|
+
repoStateText: execution.repoStateText,
|
|
163
|
+
engineFinalMessage: engineResult.finalMessage,
|
|
164
|
+
verifyResult,
|
|
165
|
+
runDir: attemptState.attemptDir,
|
|
166
|
+
policy: execution.policy,
|
|
167
|
+
});
|
|
168
|
+
if (!reviewResult.ok) {
|
|
169
|
+
return handleReviewFailure(execution, state, attemptState, reviewResult);
|
|
170
|
+
}
|
|
171
|
+
if (!reviewResult.review.isComplete) {
|
|
172
|
+
return handleIncompleteReview(execution, state, attemptState, reviewResult);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
action: "return",
|
|
177
|
+
result: buildDoneResult(
|
|
178
|
+
execution,
|
|
179
|
+
engineResult.finalMessage,
|
|
180
|
+
state.failureHistory.length + 1,
|
|
181
|
+
state.engineResolution,
|
|
182
|
+
),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runAttempt(execution, state, attemptState) {
|
|
187
|
+
const prompt = buildTaskPrompt({
|
|
188
|
+
task: execution.task,
|
|
189
|
+
repoStateText: execution.repoStateText,
|
|
190
|
+
verifyCommands: execution.verifyCommands,
|
|
191
|
+
requiredDocs: execution.requiredDocs,
|
|
192
|
+
constraints: execution.constraints,
|
|
193
|
+
previousFailure: state.previousFailure,
|
|
194
|
+
failureHistory: state.failureHistory,
|
|
195
|
+
strategyIndex: attemptState.strategyIndex,
|
|
196
|
+
maxStrategies: execution.maxStrategies,
|
|
197
|
+
attemptIndex: attemptState.attemptIndex,
|
|
198
|
+
maxAttemptsPerStrategy: execution.maxAttemptsPerStrategy,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const engineResult = await runEngineExec({
|
|
202
|
+
engine: state.engineResolution.engine,
|
|
203
|
+
context: execution.context,
|
|
204
|
+
prompt,
|
|
205
|
+
runDir: attemptState.attemptDir,
|
|
206
|
+
policy: execution.policy,
|
|
207
|
+
});
|
|
208
|
+
if (!engineResult.ok) {
|
|
209
|
+
return handleEngineFailure(execution, state, attemptState, engineResult);
|
|
210
|
+
}
|
|
211
|
+
return handleVerifyAndReview(execution, state, attemptState, engineResult);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function executeSingleTask(context, options = {}) {
|
|
215
|
+
const execution = await resolveExecutionSetup(context, options);
|
|
216
|
+
if (execution.idleResult) {
|
|
217
|
+
return execution.idleResult;
|
|
218
|
+
}
|
|
219
|
+
if (!execution.engineResolution.ok) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
kind: "engine-selection-failed",
|
|
223
|
+
task: execution.task,
|
|
224
|
+
summary: execution.engineResolution.message,
|
|
225
|
+
engineResolution: execution.engineResolution,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
rememberEngineSelection(context, execution.engineResolution, options);
|
|
230
|
+
if (options.dryRun) {
|
|
231
|
+
const prompt = buildTaskPrompt({
|
|
232
|
+
task: execution.task,
|
|
233
|
+
repoStateText: execution.repoStateText,
|
|
234
|
+
verifyCommands: execution.verifyCommands,
|
|
235
|
+
requiredDocs: execution.requiredDocs,
|
|
236
|
+
constraints: execution.constraints,
|
|
237
|
+
strategyIndex: 1,
|
|
238
|
+
maxStrategies: execution.maxStrategies,
|
|
239
|
+
attemptIndex: 1,
|
|
240
|
+
maxAttemptsPerStrategy: execution.maxAttemptsPerStrategy,
|
|
241
|
+
});
|
|
242
|
+
ensureDir(execution.runDir);
|
|
243
|
+
writeText(path.join(execution.runDir, `${execution.engineResolution.engine}-prompt.md`), prompt);
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
kind: "dry-run",
|
|
247
|
+
task: execution.task,
|
|
248
|
+
runDir: execution.runDir,
|
|
249
|
+
prompt,
|
|
250
|
+
verifyCommands: execution.verifyCommands,
|
|
251
|
+
engineResolution: execution.engineResolution,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
updateTask(execution.backlog, execution.task.id, { status: "in_progress", startedAt: nowIso() });
|
|
256
|
+
saveBacklog(context, execution.backlog);
|
|
257
|
+
|
|
258
|
+
const state = {
|
|
259
|
+
engineResolution: execution.engineResolution,
|
|
260
|
+
previousFailure: "",
|
|
261
|
+
failureHistory: [],
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
for (let strategyIndex = 1; strategyIndex <= execution.maxStrategies; strategyIndex += 1) {
|
|
265
|
+
for (let attemptIndex = 1; attemptIndex <= execution.maxAttemptsPerStrategy; attemptIndex += 1) {
|
|
266
|
+
const outcome = await runAttempt(
|
|
267
|
+
execution,
|
|
268
|
+
state,
|
|
269
|
+
buildAttemptState(execution.runDir, strategyIndex, attemptIndex, makeAttemptDir),
|
|
270
|
+
);
|
|
271
|
+
if (outcome.action === "switch") {
|
|
272
|
+
state.previousFailure = outcome.previousFailure;
|
|
273
|
+
state.engineResolution = outcome.engineResolution;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (outcome.action === "continue") {
|
|
277
|
+
state.previousFailure = outcome.previousFailure;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
return outcome.result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
state.previousFailure = bumpFailureForNextStrategy(
|
|
284
|
+
state.previousFailure,
|
|
285
|
+
execution.maxAttemptsPerStrategy,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const exhaustedSummary = buildExhaustedSummary({
|
|
290
|
+
failureHistory: state.failureHistory,
|
|
291
|
+
maxStrategies: execution.maxStrategies,
|
|
292
|
+
maxAttemptsPerStrategy: execution.maxAttemptsPerStrategy,
|
|
293
|
+
});
|
|
294
|
+
return buildFailureResult(
|
|
295
|
+
execution,
|
|
296
|
+
"strategy-exhausted",
|
|
297
|
+
exhaustedSummary,
|
|
298
|
+
state.failureHistory.length,
|
|
299
|
+
state.engineResolution,
|
|
300
|
+
);
|
|
301
|
+
}
|