helloloop 0.3.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +4 -4
  3. package/README.md +194 -81
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +17 -13
  6. package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -5
  7. package/hosts/gemini/extension/GEMINI.md +14 -7
  8. package/hosts/gemini/extension/commands/helloloop.toml +17 -12
  9. package/hosts/gemini/extension/gemini-extension.json +1 -1
  10. package/package.json +2 -2
  11. package/skills/helloloop/SKILL.md +18 -7
  12. package/src/analyze_confirmation.mjs +29 -5
  13. package/src/analyze_prompt.mjs +5 -1
  14. package/src/analyze_user_input.mjs +20 -2
  15. package/src/analyzer.mjs +130 -43
  16. package/src/cli.mjs +32 -492
  17. package/src/cli_analyze_command.mjs +248 -0
  18. package/src/cli_args.mjs +106 -0
  19. package/src/cli_command_handlers.mjs +120 -0
  20. package/src/cli_context.mjs +31 -0
  21. package/src/cli_render.mjs +70 -0
  22. package/src/cli_support.mjs +11 -14
  23. package/src/completion_review.mjs +243 -0
  24. package/src/config.mjs +51 -0
  25. package/src/discovery.mjs +21 -2
  26. package/src/discovery_prompt.mjs +2 -27
  27. package/src/email_notification.mjs +343 -0
  28. package/src/engine_metadata.mjs +79 -0
  29. package/src/engine_process_support.mjs +294 -0
  30. package/src/engine_selection.mjs +335 -0
  31. package/src/engine_selection_failure.mjs +51 -0
  32. package/src/engine_selection_messages.mjs +119 -0
  33. package/src/engine_selection_probe.mjs +78 -0
  34. package/src/engine_selection_prompt.mjs +48 -0
  35. package/src/engine_selection_settings.mjs +104 -0
  36. package/src/global_config.mjs +21 -0
  37. package/src/guardrails.mjs +15 -4
  38. package/src/install.mjs +6 -405
  39. package/src/install_claude.mjs +189 -0
  40. package/src/install_codex.mjs +114 -0
  41. package/src/install_gemini.mjs +43 -0
  42. package/src/install_shared.mjs +138 -0
  43. package/src/process.mjs +567 -100
  44. package/src/prompt.mjs +9 -5
  45. package/src/prompt_session.mjs +40 -0
  46. package/src/runner.mjs +3 -341
  47. package/src/runner_execute_task.mjs +255 -0
  48. package/src/runner_execution_support.mjs +146 -0
  49. package/src/runner_loop.mjs +106 -0
  50. package/src/runner_once.mjs +29 -0
  51. package/src/runner_status.mjs +104 -0
  52. package/src/runtime_recovery.mjs +302 -0
  53. package/src/shell_invocation.mjs +16 -0
  54. package/templates/analysis-output.schema.json +0 -1
  55. package/templates/policy.template.json +25 -0
  56. package/templates/project.template.json +2 -0
  57. package/templates/task-review-output.schema.json +70 -0
package/src/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
+ }
package/src/runner.mjs CHANGED
@@ -1,341 +1,3 @@
1
- import path from "node:path";
2
-
3
- import {
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,255 @@
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
+ recordFailure,
18
+ resolveExecutionSetup,
19
+ } from "./runner_execution_support.mjs";
20
+ import {
21
+ buildExhaustedSummary,
22
+ buildFailureSummary,
23
+ isHardStopFailure,
24
+ makeAttemptDir,
25
+ } from "./runner_status.mjs";
26
+
27
+ async function handleEngineFailure(execution, state, attemptState, engineResult) {
28
+ const previousFailure = buildFailureSummary("engine", {
29
+ ...engineResult,
30
+ displayName: getEngineDisplayName(state.engineResolution.engine),
31
+ });
32
+ recordFailure(
33
+ state.failureHistory,
34
+ attemptState.strategyIndex,
35
+ attemptState.attemptIndex,
36
+ state.engineResolution.engine,
37
+ previousFailure,
38
+ );
39
+ return {
40
+ action: "return",
41
+ result: buildFailureResult(
42
+ execution,
43
+ "engine-failed",
44
+ previousFailure,
45
+ state.failureHistory.length,
46
+ state.engineResolution,
47
+ ),
48
+ };
49
+ }
50
+
51
+ function handleVerifyFailure(execution, state, attemptState, verifyResult) {
52
+ const previousFailure = buildFailureSummary("verify", verifyResult);
53
+ recordFailure(state.failureHistory, attemptState.strategyIndex, attemptState.attemptIndex, "verify", previousFailure);
54
+
55
+ if (isHardStopFailure("verify", previousFailure)) {
56
+ return {
57
+ action: "return",
58
+ result: buildFailureResult(
59
+ execution,
60
+ "verify-failed",
61
+ previousFailure,
62
+ state.failureHistory.length,
63
+ state.engineResolution,
64
+ ),
65
+ };
66
+ }
67
+ return { action: "continue", previousFailure };
68
+ }
69
+
70
+ async function handleReviewFailure(execution, state, attemptState, reviewResult) {
71
+ const previousFailure = reviewResult.summary;
72
+ recordFailure(state.failureHistory, attemptState.strategyIndex, attemptState.attemptIndex, "task_review", previousFailure);
73
+ return {
74
+ action: "return",
75
+ result: buildFailureResult(
76
+ execution,
77
+ "task-review-failed",
78
+ previousFailure,
79
+ state.failureHistory.length,
80
+ state.engineResolution,
81
+ ),
82
+ };
83
+ }
84
+
85
+ function handleIncompleteReview(execution, state, attemptState, reviewResult) {
86
+ const previousFailure = reviewResult.summary;
87
+ recordFailure(
88
+ state.failureHistory,
89
+ attemptState.strategyIndex,
90
+ attemptState.attemptIndex,
91
+ reviewResult.review.verdict === "blocked" ? "blocked" : "task_incomplete",
92
+ previousFailure,
93
+ );
94
+
95
+ if (reviewResult.review.verdict === "blocked") {
96
+ return {
97
+ action: "return",
98
+ result: buildBlockedResult(
99
+ execution,
100
+ previousFailure,
101
+ state.failureHistory.length,
102
+ state.engineResolution,
103
+ ),
104
+ };
105
+ }
106
+ return { action: "continue", previousFailure };
107
+ }
108
+
109
+ async function handleVerifyAndReview(execution, state, attemptState, engineResult) {
110
+ const verifyResult = await runVerifyCommands(execution.context, execution.verifyCommands, attemptState.attemptDir);
111
+ if (!verifyResult.ok) {
112
+ return handleVerifyFailure(execution, state, attemptState, verifyResult);
113
+ }
114
+
115
+ const reviewResult = await reviewTaskCompletion({
116
+ engine: state.engineResolution.engine,
117
+ context: execution.context,
118
+ task: execution.task,
119
+ requiredDocs: execution.requiredDocs,
120
+ constraints: execution.constraints,
121
+ repoStateText: execution.repoStateText,
122
+ engineFinalMessage: engineResult.finalMessage,
123
+ verifyResult,
124
+ runDir: attemptState.attemptDir,
125
+ policy: execution.policy,
126
+ });
127
+ if (!reviewResult.ok) {
128
+ return handleReviewFailure(execution, state, attemptState, reviewResult);
129
+ }
130
+ if (!reviewResult.review.isComplete) {
131
+ return handleIncompleteReview(execution, state, attemptState, reviewResult);
132
+ }
133
+
134
+ return {
135
+ action: "return",
136
+ result: buildDoneResult(
137
+ execution,
138
+ engineResult.finalMessage,
139
+ state.failureHistory.length + 1,
140
+ state.engineResolution,
141
+ ),
142
+ };
143
+ }
144
+
145
+ async function runAttempt(execution, state, attemptState) {
146
+ const prompt = buildTaskPrompt({
147
+ task: execution.task,
148
+ repoStateText: execution.repoStateText,
149
+ verifyCommands: execution.verifyCommands,
150
+ requiredDocs: execution.requiredDocs,
151
+ constraints: execution.constraints,
152
+ previousFailure: state.previousFailure,
153
+ failureHistory: state.failureHistory,
154
+ strategyIndex: attemptState.strategyIndex,
155
+ maxStrategies: execution.maxStrategies,
156
+ attemptIndex: attemptState.attemptIndex,
157
+ maxAttemptsPerStrategy: execution.maxAttemptsPerStrategy,
158
+ });
159
+
160
+ const engineResult = await runEngineExec({
161
+ engine: state.engineResolution.engine,
162
+ context: execution.context,
163
+ prompt,
164
+ runDir: attemptState.attemptDir,
165
+ policy: execution.policy,
166
+ });
167
+ if (!engineResult.ok) {
168
+ return handleEngineFailure(execution, state, attemptState, engineResult);
169
+ }
170
+ return handleVerifyAndReview(execution, state, attemptState, engineResult);
171
+ }
172
+
173
+ export async function executeSingleTask(context, options = {}) {
174
+ const execution = await resolveExecutionSetup(context, options);
175
+ if (execution.idleResult) {
176
+ return execution.idleResult;
177
+ }
178
+ if (!execution.engineResolution.ok) {
179
+ return {
180
+ ok: false,
181
+ kind: "engine-selection-failed",
182
+ task: execution.task,
183
+ summary: execution.engineResolution.message,
184
+ engineResolution: execution.engineResolution,
185
+ };
186
+ }
187
+
188
+ rememberEngineSelection(context, execution.engineResolution, options);
189
+ if (options.dryRun) {
190
+ const prompt = buildTaskPrompt({
191
+ task: execution.task,
192
+ repoStateText: execution.repoStateText,
193
+ verifyCommands: execution.verifyCommands,
194
+ requiredDocs: execution.requiredDocs,
195
+ constraints: execution.constraints,
196
+ strategyIndex: 1,
197
+ maxStrategies: execution.maxStrategies,
198
+ attemptIndex: 1,
199
+ maxAttemptsPerStrategy: execution.maxAttemptsPerStrategy,
200
+ });
201
+ ensureDir(execution.runDir);
202
+ writeText(path.join(execution.runDir, `${execution.engineResolution.engine}-prompt.md`), prompt);
203
+ return {
204
+ ok: true,
205
+ kind: "dry-run",
206
+ task: execution.task,
207
+ runDir: execution.runDir,
208
+ prompt,
209
+ verifyCommands: execution.verifyCommands,
210
+ engineResolution: execution.engineResolution,
211
+ };
212
+ }
213
+
214
+ updateTask(execution.backlog, execution.task.id, { status: "in_progress", startedAt: nowIso() });
215
+ saveBacklog(context, execution.backlog);
216
+
217
+ const state = {
218
+ engineResolution: execution.engineResolution,
219
+ previousFailure: "",
220
+ failureHistory: [],
221
+ };
222
+
223
+ for (let strategyIndex = 1; strategyIndex <= execution.maxStrategies; strategyIndex += 1) {
224
+ for (let attemptIndex = 1; attemptIndex <= execution.maxAttemptsPerStrategy; attemptIndex += 1) {
225
+ const outcome = await runAttempt(
226
+ execution,
227
+ state,
228
+ buildAttemptState(execution.runDir, strategyIndex, attemptIndex, makeAttemptDir),
229
+ );
230
+ if (outcome.action === "continue") {
231
+ state.previousFailure = outcome.previousFailure;
232
+ continue;
233
+ }
234
+ return outcome.result;
235
+ }
236
+
237
+ state.previousFailure = bumpFailureForNextStrategy(
238
+ state.previousFailure,
239
+ execution.maxAttemptsPerStrategy,
240
+ );
241
+ }
242
+
243
+ const exhaustedSummary = buildExhaustedSummary({
244
+ failureHistory: state.failureHistory,
245
+ maxStrategies: execution.maxStrategies,
246
+ maxAttemptsPerStrategy: execution.maxAttemptsPerStrategy,
247
+ });
248
+ return buildFailureResult(
249
+ execution,
250
+ "strategy-exhausted",
251
+ exhaustedSummary,
252
+ state.failureHistory.length,
253
+ state.engineResolution,
254
+ );
255
+ }