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,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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rememberEngineSelection,
|
|
3
|
+
resolveEngineSelection,
|
|
4
|
+
} from "./engine_selection.mjs";
|
|
5
|
+
import { nowIso } from "./common.mjs";
|
|
6
|
+
import {
|
|
7
|
+
loadBacklog,
|
|
8
|
+
loadPolicy,
|
|
9
|
+
loadProjectConfig,
|
|
10
|
+
loadRepoStateText,
|
|
11
|
+
loadVerifyCommands,
|
|
12
|
+
saveBacklog,
|
|
13
|
+
} from "./config.mjs";
|
|
14
|
+
import { getTask, selectNextTask, unresolvedDependencies, updateTask } from "./backlog.mjs";
|
|
15
|
+
import { makeRunDir } from "./runner_status.mjs";
|
|
16
|
+
import { resolveRuntimeRecoveryPolicy } from "./runtime_recovery.mjs";
|
|
17
|
+
|
|
18
|
+
function resolveTask(backlog, options) {
|
|
19
|
+
if (options.taskId) {
|
|
20
|
+
const task = getTask(backlog, options.taskId);
|
|
21
|
+
if (!task) {
|
|
22
|
+
throw new Error(`未找到任务:${options.taskId}`);
|
|
23
|
+
}
|
|
24
|
+
return task;
|
|
25
|
+
}
|
|
26
|
+
return selectNextTask(backlog, options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function resolveExecutionSetup(context, options = {}) {
|
|
30
|
+
const policy = loadPolicy(context);
|
|
31
|
+
const projectConfig = loadProjectConfig(context);
|
|
32
|
+
const backlog = loadBacklog(context);
|
|
33
|
+
const task = resolveTask(backlog, options);
|
|
34
|
+
if (!task) {
|
|
35
|
+
return {
|
|
36
|
+
idleResult: { ok: true, kind: "idle", task: null },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const unresolved = unresolvedDependencies(backlog, task);
|
|
41
|
+
if (unresolved.length) {
|
|
42
|
+
throw new Error(`任务 ${task.id} 仍有未完成依赖:${unresolved.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const verifyCommands = Array.isArray(task.verify) && task.verify.length
|
|
46
|
+
? task.verify
|
|
47
|
+
: loadVerifyCommands(context);
|
|
48
|
+
const maxAttemptsPerStrategy = Math.max(1, Number(options.maxAttempts || policy.maxTaskAttempts || 1));
|
|
49
|
+
const configuredStrategies = Math.max(1, Number(options.maxStrategies || policy.maxTaskStrategies || 1));
|
|
50
|
+
const engineResolution = options.engineResolution?.ok
|
|
51
|
+
? options.engineResolution
|
|
52
|
+
: await resolveEngineSelection({
|
|
53
|
+
context,
|
|
54
|
+
policy,
|
|
55
|
+
options,
|
|
56
|
+
interactive: !options.yes,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
context,
|
|
61
|
+
options,
|
|
62
|
+
policy,
|
|
63
|
+
backlog,
|
|
64
|
+
projectConfig,
|
|
65
|
+
repoStateText: loadRepoStateText(context),
|
|
66
|
+
task,
|
|
67
|
+
verifyCommands,
|
|
68
|
+
runDir: makeRunDir(context, task.id),
|
|
69
|
+
requiredDocs: [...(projectConfig.requiredDocs || []), ...(options.requiredDocs || [])],
|
|
70
|
+
constraints: [...(projectConfig.constraints || []), ...(options.constraints || [])],
|
|
71
|
+
maxAttemptsPerStrategy,
|
|
72
|
+
maxStrategies: policy.stopOnFailure ? 1 : configuredStrategies,
|
|
73
|
+
engineResolution,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function updateTaskAndBuildResult(execution, status, result) {
|
|
78
|
+
updateTask(execution.backlog, execution.task.id, {
|
|
79
|
+
status,
|
|
80
|
+
finishedAt: nowIso(),
|
|
81
|
+
lastFailure: result.ok ? "" : (result.summary || ""),
|
|
82
|
+
attempts: result.attempts,
|
|
83
|
+
});
|
|
84
|
+
saveBacklog(execution.context, execution.backlog);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildFailureResult(execution, kind, summary, attempts, engineResolution) {
|
|
89
|
+
return updateTaskAndBuildResult(execution, "failed", {
|
|
90
|
+
ok: false,
|
|
91
|
+
kind,
|
|
92
|
+
task: execution.task,
|
|
93
|
+
runDir: execution.runDir,
|
|
94
|
+
summary,
|
|
95
|
+
attempts,
|
|
96
|
+
engineResolution,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildBlockedResult(execution, summary, attempts, engineResolution) {
|
|
101
|
+
return updateTaskAndBuildResult(execution, "blocked", {
|
|
102
|
+
ok: false,
|
|
103
|
+
kind: "task-blocked",
|
|
104
|
+
task: execution.task,
|
|
105
|
+
runDir: execution.runDir,
|
|
106
|
+
summary,
|
|
107
|
+
attempts,
|
|
108
|
+
engineResolution,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildDoneResult(execution, finalMessage, attempts, engineResolution) {
|
|
113
|
+
return updateTaskAndBuildResult(execution, "done", {
|
|
114
|
+
ok: true,
|
|
115
|
+
kind: "done",
|
|
116
|
+
task: execution.task,
|
|
117
|
+
runDir: execution.runDir,
|
|
118
|
+
finalMessage,
|
|
119
|
+
attempts,
|
|
120
|
+
engineResolution,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function maybeSwitchEngine(execution, engineResolution, previousFailure, phaseLabel) {
|
|
125
|
+
const recoveryPolicy = resolveRuntimeRecoveryPolicy(execution.policy);
|
|
126
|
+
if (!recoveryPolicy.allowEngineSwitch) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function recordFailure(failureHistory, strategyIndex, attemptIndex, kind, summary) {
|
|
133
|
+
failureHistory.push({
|
|
134
|
+
strategyIndex,
|
|
135
|
+
attemptIndex,
|
|
136
|
+
kind,
|
|
137
|
+
summary,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function buildAttemptState(runDir, strategyIndex, attemptIndex, makeAttemptDir) {
|
|
142
|
+
return {
|
|
143
|
+
strategyIndex,
|
|
144
|
+
attemptIndex,
|
|
145
|
+
attemptDir: makeAttemptDir(runDir, strategyIndex, attemptIndex),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function bumpFailureForNextStrategy(previousFailure, maxAttemptsPerStrategy) {
|
|
150
|
+
return [
|
|
151
|
+
previousFailure,
|
|
152
|
+
"",
|
|
153
|
+
`上一种策略已连续失败 ${maxAttemptsPerStrategy} 次。下一轮必须明确更换实现或排查思路,不能重复原路径。`,
|
|
154
|
+
].join("\n").trim();
|
|
155
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { selectNextTask, summarizeBacklog } from "./backlog.mjs";
|
|
2
|
+
import { loadBacklog, loadPolicy } from "./config.mjs";
|
|
3
|
+
import { reanalyzeCurrentWorkspace } from "./analyzer.mjs";
|
|
4
|
+
import { runOnce } from "./runner_once.mjs";
|
|
5
|
+
|
|
6
|
+
function shouldRunMainlineReanalysis(options, summary, reanalysisPasses, maxReanalysisPasses) {
|
|
7
|
+
return Boolean(options.fullAutoMainline)
|
|
8
|
+
&& summary.pending === 0
|
|
9
|
+
&& summary.inProgress === 0
|
|
10
|
+
&& summary.failed === 0
|
|
11
|
+
&& summary.blocked === 0
|
|
12
|
+
&& reanalysisPasses < maxReanalysisPasses;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildMainlineReopenedResult(nextTask, engineResolution) {
|
|
16
|
+
return {
|
|
17
|
+
ok: true,
|
|
18
|
+
kind: "mainline-reopened",
|
|
19
|
+
task: null,
|
|
20
|
+
summary: [
|
|
21
|
+
"主线终态复核发现仍有剩余工作,已自动重建 backlog 并继续推进。",
|
|
22
|
+
`下一任务:${nextTask.title}`,
|
|
23
|
+
].join("\n"),
|
|
24
|
+
engineResolution,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildMainlineCompleteResult(engineResolution) {
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
kind: "mainline-complete",
|
|
32
|
+
task: null,
|
|
33
|
+
summary: "主线终态复核通过:开发文档目标已闭合,没有发现新的剩余任务。",
|
|
34
|
+
engineResolution,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildMainlineFailureResult(continuation, engineResolution) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
kind: "mainline-reanalysis-failed",
|
|
42
|
+
task: null,
|
|
43
|
+
summary: continuation.summary || "主线终态复核失败。",
|
|
44
|
+
engineResolution,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runLoop(context, options = {}) {
|
|
49
|
+
const policy = loadPolicy(context);
|
|
50
|
+
const explicitMaxTasks = Number(options.maxTasks);
|
|
51
|
+
const maxTasks = Number.isFinite(explicitMaxTasks) && explicitMaxTasks > 0
|
|
52
|
+
? explicitMaxTasks
|
|
53
|
+
: (options.fullAutoMainline ? Number.POSITIVE_INFINITY : Math.max(1, Number(policy.maxLoopTasks || 1)));
|
|
54
|
+
const maxReanalysisPasses = Math.max(0, Number(options.maxReanalysisPasses || policy.maxReanalysisPasses || 0));
|
|
55
|
+
const results = [];
|
|
56
|
+
let engineResolution = options.engineResolution || null;
|
|
57
|
+
let completedTasks = 0;
|
|
58
|
+
let reanalysisPasses = 0;
|
|
59
|
+
|
|
60
|
+
while (completedTasks < maxTasks) {
|
|
61
|
+
const result = await runOnce(context, { ...options, engineResolution });
|
|
62
|
+
results.push(result);
|
|
63
|
+
if (result.engineResolution?.ok) {
|
|
64
|
+
engineResolution = result.engineResolution;
|
|
65
|
+
}
|
|
66
|
+
if (options.dryRun || !result.ok || !result.task) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
completedTasks += 1;
|
|
71
|
+
const backlog = loadBacklog(context);
|
|
72
|
+
if (selectNextTask(backlog, options)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const summary = summarizeBacklog(backlog);
|
|
77
|
+
if (!shouldRunMainlineReanalysis(options, summary, reanalysisPasses, maxReanalysisPasses)) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
reanalysisPasses += 1;
|
|
82
|
+
const continuation = await reanalyzeCurrentWorkspace(context, {
|
|
83
|
+
...options,
|
|
84
|
+
engineResolution,
|
|
85
|
+
yes: true,
|
|
86
|
+
});
|
|
87
|
+
if (continuation.engineResolution?.ok) {
|
|
88
|
+
engineResolution = continuation.engineResolution;
|
|
89
|
+
}
|
|
90
|
+
if (!continuation.ok) {
|
|
91
|
+
results.push(buildMainlineFailureResult(continuation, engineResolution));
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const continuedNextTask = selectNextTask(loadBacklog(context), options);
|
|
96
|
+
if (continuedNextTask) {
|
|
97
|
+
results.push(buildMainlineReopenedResult(continuedNextTask, engineResolution));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
results.push(buildMainlineCompleteResult(engineResolution));
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { selectNextTask, summarizeBacklog } from "./backlog.mjs";
|
|
2
|
+
import { loadBacklog, writeStateMarkdown, writeStatus } from "./config.mjs";
|
|
3
|
+
import { executeSingleTask } from "./runner_execute_task.mjs";
|
|
4
|
+
import { renderStatusMarkdown } from "./runner_status.mjs";
|
|
5
|
+
|
|
6
|
+
export async function runOnce(context, options = {}) {
|
|
7
|
+
const result = await executeSingleTask(context, options);
|
|
8
|
+
const backlog = loadBacklog(context);
|
|
9
|
+
const summary = summarizeBacklog(backlog);
|
|
10
|
+
const nextTask = selectNextTask(backlog, options);
|
|
11
|
+
|
|
12
|
+
writeStatus(context, {
|
|
13
|
+
ok: result.ok,
|
|
14
|
+
stage: result.kind,
|
|
15
|
+
taskId: result.task?.id || null,
|
|
16
|
+
taskTitle: result.task?.title || "",
|
|
17
|
+
runDir: result.runDir || "",
|
|
18
|
+
summary,
|
|
19
|
+
message: result.summary || result.finalMessage || "",
|
|
20
|
+
});
|
|
21
|
+
writeStateMarkdown(context, renderStatusMarkdown(context, {
|
|
22
|
+
summary,
|
|
23
|
+
currentTask: result.task,
|
|
24
|
+
lastResult: result.ok ? "本轮成功" : (result.summary || result.kind),
|
|
25
|
+
nextTask,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { sanitizeId, tailText, timestampForFile } from "./common.mjs";
|
|
4
|
+
import { renderTaskSummary, selectNextTask, summarizeBacklog } from "./backlog.mjs";
|
|
5
|
+
import { loadBacklog } from "./config.mjs";
|
|
6
|
+
|
|
7
|
+
export function makeRunDir(context, taskId) {
|
|
8
|
+
return path.join(context.runsDir, `${timestampForFile()}-${sanitizeId(taskId)}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function makeAttemptDir(runDir, strategyIndex, attemptIndex) {
|
|
12
|
+
return path.join(
|
|
13
|
+
runDir,
|
|
14
|
+
`strategy-${String(strategyIndex).padStart(2, "0")}-attempt-${String(attemptIndex).padStart(2, "0")}`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isHardStopFailure(kind, summary) {
|
|
19
|
+
const normalized = String(summary || "").toLowerCase();
|
|
20
|
+
if (!normalized) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (kind === "engine" && normalized.includes("enoent")) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
"command not found",
|
|
29
|
+
"is not recognized",
|
|
30
|
+
"无法将",
|
|
31
|
+
"找不到路径",
|
|
32
|
+
"no such file or directory",
|
|
33
|
+
"permission denied",
|
|
34
|
+
"access is denied",
|
|
35
|
+
].some((signal) => normalized.includes(signal));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildFailureSummary(kind, payload) {
|
|
39
|
+
if (kind !== "engine") {
|
|
40
|
+
return payload.summary;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
`${payload.displayName} 执行失败,退出码:${payload.code}`,
|
|
45
|
+
payload.recoverySummary || "",
|
|
46
|
+
"",
|
|
47
|
+
"stdout 尾部:",
|
|
48
|
+
tailText(payload.stdout, 60),
|
|
49
|
+
"",
|
|
50
|
+
"stderr 尾部:",
|
|
51
|
+
tailText(payload.stderr, 60),
|
|
52
|
+
].join("\n").trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildExhaustedSummary({
|
|
56
|
+
failureHistory,
|
|
57
|
+
maxStrategies,
|
|
58
|
+
maxAttemptsPerStrategy,
|
|
59
|
+
}) {
|
|
60
|
+
const lastFailure = failureHistory.at(-1)?.summary || "未知失败。";
|
|
61
|
+
return [
|
|
62
|
+
`已按 Ralph Loop 执行 ${maxStrategies} 轮策略、每轮最多 ${maxAttemptsPerStrategy} 次重试,当前任务仍未收敛。`,
|
|
63
|
+
"",
|
|
64
|
+
"最后一次失败信息:",
|
|
65
|
+
lastFailure,
|
|
66
|
+
].join("\n").trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function renderStatusMarkdown(context, { summary, currentTask, lastResult, nextTask }) {
|
|
70
|
+
return [
|
|
71
|
+
"## 当前状态",
|
|
72
|
+
`- backlog 文件:${context.backlogFile.replaceAll("\\", "/")}`,
|
|
73
|
+
`- 总任务数:${summary.total}`,
|
|
74
|
+
`- 已完成:${summary.done}`,
|
|
75
|
+
`- 待处理:${summary.pending}`,
|
|
76
|
+
`- 进行中:${summary.inProgress}`,
|
|
77
|
+
`- 失败:${summary.failed}`,
|
|
78
|
+
`- 阻塞:${summary.blocked}`,
|
|
79
|
+
`- 当前任务:${currentTask ? currentTask.title : "无"}`,
|
|
80
|
+
`- 最近结果:${lastResult || "暂无"}`,
|
|
81
|
+
`- 下一建议:${nextTask ? nextTask.title : "暂无可执行任务"}`,
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function renderStatusText(context, options = {}) {
|
|
86
|
+
const backlog = loadBacklog(context);
|
|
87
|
+
const summary = summarizeBacklog(backlog);
|
|
88
|
+
const nextTask = selectNextTask(backlog, options);
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
"HelloLoop 状态",
|
|
92
|
+
"============",
|
|
93
|
+
`仓库:${context.repoRoot}`,
|
|
94
|
+
`总任务:${summary.total}`,
|
|
95
|
+
`已完成:${summary.done}`,
|
|
96
|
+
`待处理:${summary.pending}`,
|
|
97
|
+
`进行中:${summary.inProgress}`,
|
|
98
|
+
`失败:${summary.failed}`,
|
|
99
|
+
`阻塞:${summary.blocked}`,
|
|
100
|
+
"",
|
|
101
|
+
nextTask ? "下一任务:" : "下一任务:无",
|
|
102
|
+
nextTask ? renderTaskSummary(nextTask) : "",
|
|
103
|
+
].filter(Boolean).join("\n");
|
|
104
|
+
}
|