flanders 0.1.0 → 0.2.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.
- package/lib/ai/AiRunner.d.ts +24 -0
- package/lib/ai/AiRunner.js +100 -0
- package/lib/ai/AiSession.d.ts +32 -0
- package/lib/ai/AiSession.js +143 -0
- package/lib/ai/ClaudeAdapter.d.ts +12 -0
- package/lib/ai/ClaudeAdapter.js +345 -0
- package/lib/ai/CodexAdapter.d.ts +13 -0
- package/lib/ai/CodexAdapter.js +354 -0
- package/lib/ai/ToolAdapter.d.ts +52 -0
- package/lib/ai/ToolAdapter.js +2 -0
- package/lib/cli.js +56 -35
- package/lib/commands/Implement.d.ts +22 -8
- package/lib/commands/Implement.js +296 -171
- package/lib/commands/Install.d.ts +31 -3
- package/lib/commands/Install.js +649 -49
- package/lib/commands/InstallAvailabilityCheck.d.ts +8 -0
- package/lib/commands/InstallAvailabilityCheck.js +45 -0
- package/lib/commands/InstallModelProbe.d.ts +2 -0
- package/lib/commands/InstallModelProbe.js +74 -0
- package/lib/contexts.d.ts +5 -4
- package/lib/index.d.ts +5 -3
- package/lib/{PlanFile.d.ts → plan/PlanFile.d.ts} +8 -1
- package/lib/{PlanFile.js → plan/PlanFile.js} +44 -5
- package/lib/prompts/prompts.d.ts +48 -0
- package/lib/prompts/prompts.js +328 -0
- package/lib/prompts/skills.d.ts +3 -0
- package/lib/prompts/skills.js +463 -0
- package/lib/{Git.d.ts → system/Git.d.ts} +4 -2
- package/lib/{Git.js → system/Git.js} +63 -3
- package/lib/{ScriptRunner.d.ts → system/ScriptRunner.d.ts} +1 -1
- package/lib/system/ShellScriptContext.d.ts +36 -0
- package/lib/system/ShellScriptContext.js +70 -0
- package/lib/{fsUtils.d.ts → system/fsUtils.d.ts} +1 -1
- package/lib/{wait.d.ts → system/wait.d.ts} +1 -1
- package/lib/ui/BottomBlock.d.ts +9 -1
- package/lib/ui/BottomBlock.js +30 -14
- package/lib/ui/PromptHelper.d.ts +12 -0
- package/lib/ui/PromptHelper.js +32 -0
- package/lib/ui/TerminalSizeSource.d.ts +19 -0
- package/lib/ui/TerminalSizeSource.js +77 -0
- package/lib/ui/formatters.d.ts +14 -0
- package/lib/ui/formatters.js +65 -2
- package/lib/workspace/FlandersConfig.d.ts +23 -0
- package/lib/workspace/FlandersConfig.js +90 -0
- package/lib/workspace/SpecDiscovery.d.ts +12 -0
- package/lib/workspace/SpecDiscovery.js +40 -0
- package/lib/{Workspace.d.ts → workspace/Workspace.d.ts} +10 -2
- package/lib/{Workspace.js → workspace/Workspace.js} +52 -6
- package/package.json +3 -2
- package/lib/Claude.d.ts +0 -129
- package/lib/Claude.js +0 -387
- package/lib/ClaudeSession.d.ts +0 -34
- package/lib/ClaudeSession.js +0 -292
- package/lib/prompts.d.ts +0 -18
- package/lib/prompts.js +0 -221
- package/lib/skills.d.ts +0 -3
- package/lib/skills.js +0 -267
- /package/lib/{ScriptRunner.js → system/ScriptRunner.js} +0 -0
- /package/lib/{fsUtils.js → system/fsUtils.js} +0 -0
- /package/lib/{wait.js → system/wait.js} +0 -0
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Implement = void 0;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
4
|
+
const AiSession_1 = require("../ai/AiSession");
|
|
5
|
+
const ClaudeAdapter_1 = require("../ai/ClaudeAdapter");
|
|
6
|
+
const CodexAdapter_1 = require("../ai/CodexAdapter");
|
|
7
|
+
const FlandersConfig_1 = require("../workspace/FlandersConfig");
|
|
8
|
+
const fsUtils_1 = require("../system/fsUtils");
|
|
9
|
+
const Git_1 = require("../system/Git");
|
|
10
|
+
const SpecDiscovery_1 = require("../workspace/SpecDiscovery");
|
|
11
|
+
const PlanFile_1 = require("../plan/PlanFile");
|
|
12
|
+
const prompts_1 = require("../prompts/prompts");
|
|
13
|
+
const ScriptRunner_1 = require("../system/ScriptRunner");
|
|
10
14
|
const BottomBlock_1 = require("../ui/BottomBlock");
|
|
11
15
|
const formatters_1 = require("../ui/formatters");
|
|
12
|
-
const Workspace_1 = require("../Workspace");
|
|
16
|
+
const Workspace_1 = require("../workspace/Workspace");
|
|
13
17
|
const MAX_ITER = 5;
|
|
14
18
|
class LineBufferedBlock {
|
|
15
19
|
constructor(_block) {
|
|
@@ -45,18 +49,23 @@ class LineBufferedBlock {
|
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
class Implement {
|
|
52
|
+
get config() { return this._config; }
|
|
48
53
|
constructor(rawArgs, _options, _contexts) {
|
|
49
54
|
this._options = _options;
|
|
50
55
|
this._contexts = _contexts;
|
|
51
56
|
this._disposed = false;
|
|
57
|
+
this._config = null;
|
|
52
58
|
this._contractList = [];
|
|
53
59
|
this._ruleList = [];
|
|
60
|
+
this._behaviorRuleList = [];
|
|
54
61
|
this._workspace = null;
|
|
55
62
|
this._block = null;
|
|
56
63
|
this._currentWorkerSessionId = null;
|
|
57
64
|
this._currentPrepSessionId = null;
|
|
58
65
|
this._activeSession = null;
|
|
59
66
|
this._activeScript = null;
|
|
67
|
+
this._activeReviewerSessions = new Set();
|
|
68
|
+
this._reviewerStates = null;
|
|
60
69
|
this._currentIndexLabel = "";
|
|
61
70
|
this._currentIteration = 0;
|
|
62
71
|
this._currentTask = null;
|
|
@@ -64,6 +73,7 @@ class Implement {
|
|
|
64
73
|
this._taskRateLimitMs = 0;
|
|
65
74
|
this._taskRateLimitStartedAt = null;
|
|
66
75
|
this._taskTokens = { it: 0, ot: 0 };
|
|
76
|
+
this._restingFooterKind = "working";
|
|
67
77
|
this._runPromise = this._run(rawArgs);
|
|
68
78
|
this._runPromise.catch(() => { });
|
|
69
79
|
}
|
|
@@ -81,12 +91,8 @@ class Implement {
|
|
|
81
91
|
this._buffered = new LineBufferedBlock(block);
|
|
82
92
|
try {
|
|
83
93
|
const positional = [];
|
|
84
|
-
let noGitFlag = false;
|
|
85
94
|
for (const arg of rawArgs) {
|
|
86
|
-
if (arg
|
|
87
|
-
noGitFlag = true;
|
|
88
|
-
}
|
|
89
|
-
else if (arg.startsWith("-")) {
|
|
95
|
+
if (arg.startsWith("-")) {
|
|
90
96
|
this._buffered.writeError(`Unknown flag: ${arg}\n`);
|
|
91
97
|
this._finalizeBlock("Failed");
|
|
92
98
|
return 1;
|
|
@@ -95,6 +101,16 @@ class Implement {
|
|
|
95
101
|
positional.push(arg);
|
|
96
102
|
}
|
|
97
103
|
}
|
|
104
|
+
const config = await (0, FlandersConfig_1.read)(this._contexts.fs, {
|
|
105
|
+
projectRoot: this._options.projectRoot,
|
|
106
|
+
homeDir: this._contexts.platform.homedir()
|
|
107
|
+
});
|
|
108
|
+
if (config === null) {
|
|
109
|
+
this._buffered.writeError("Missing Flanders configuration. Run 'npx flanders install'.\n");
|
|
110
|
+
this._finalizeBlock("Failed");
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
this._config = config;
|
|
98
114
|
const planPath = await this._selectPlan(positional);
|
|
99
115
|
if (this._disposed) {
|
|
100
116
|
this._finalizeBlock("Interrupted");
|
|
@@ -130,26 +146,25 @@ class Implement {
|
|
|
130
146
|
}
|
|
131
147
|
this._block.setHeader({ indexLabel: `0/${totalTasks}` });
|
|
132
148
|
this._block.setMetrics({ plan: { tokens: planTotals.it + planTotals.ot, seconds: planTotals.t } });
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
const gitAvailable = await (0, Git_1.isGitAvailable)(this._contexts.script, this._contexts.time);
|
|
150
|
+
const insideWorkTree = gitAvailable && await (0, Git_1.isInsideWorkTree)(this._contexts.script, this._contexts.time, this._options.projectRoot);
|
|
151
|
+
if (!insideWorkTree) {
|
|
152
|
+
this._buffered.writeError("The project must be a git repository. Flanders implement requires git on PATH and the project root inside a git work tree.\n");
|
|
153
|
+
this._finalizeBlock("Failed");
|
|
154
|
+
return 1;
|
|
138
155
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return 1;
|
|
145
|
-
}
|
|
156
|
+
const pending = await (0, Git_1.countUnstagedChangesExcept)(this._contexts.script, this._contexts.time, this._options.projectRoot, planPath);
|
|
157
|
+
if (pending > 0) {
|
|
158
|
+
this._buffered.writeError("Working tree has unstaged changes. Please stage, commit, or stash them before re-running.\n");
|
|
159
|
+
this._finalizeBlock("Failed");
|
|
160
|
+
return 1;
|
|
146
161
|
}
|
|
147
|
-
const
|
|
148
|
-
this._contractList =
|
|
149
|
-
|
|
150
|
-
this.
|
|
162
|
+
const specs = await (0, SpecDiscovery_1.discoverSpecs)(this._contexts.script, this._contexts.time, this._options.projectRoot);
|
|
163
|
+
this._contractList = specs.contracts;
|
|
164
|
+
this._ruleList = specs.rules;
|
|
165
|
+
this._behaviorRuleList = specs.flanders;
|
|
151
166
|
this._workspace = new Workspace_1.Workspace(this._contexts.fs, this._contexts.platform);
|
|
152
|
-
const wsPaths = await this._workspace.setup();
|
|
167
|
+
const wsPaths = await this._workspace.setup(this._config.reviewers.length);
|
|
153
168
|
await this._detectBuildAndTest(wsPaths);
|
|
154
169
|
if (this._disposed) {
|
|
155
170
|
this._finalizeBlock("Interrupted");
|
|
@@ -167,7 +182,7 @@ class Implement {
|
|
|
167
182
|
const refreshed = plan.parse();
|
|
168
183
|
const completedSoFar = refreshed.tasks.filter(t => t.done).length;
|
|
169
184
|
const indexLabel = `${completedSoFar + 1}/${totalTasks}`;
|
|
170
|
-
const taskOk = await this._runTask(plan, open, wsPaths, indexLabel, completedSoFar
|
|
185
|
+
const taskOk = await this._runTask(plan, open, wsPaths, indexLabel, completedSoFar);
|
|
171
186
|
if (this._disposed) {
|
|
172
187
|
this._finalizeBlock("Interrupted");
|
|
173
188
|
return 1;
|
|
@@ -212,51 +227,26 @@ class Implement {
|
|
|
212
227
|
if (files.length === 1) {
|
|
213
228
|
return (0, fsUtils_1.joinPath)(plansFolder, files[0]);
|
|
214
229
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
write: text => this._buffered.write(text),
|
|
220
|
-
writeError: text => this._buffered.writeError(text),
|
|
221
|
-
columns: () => this._contexts.output.columns(),
|
|
222
|
-
rows: () => this._contexts.output.rows(),
|
|
223
|
-
onResize: listener => this._contexts.output.onResize(listener)
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
async _promptPlanChoice(plansFolder, files) {
|
|
227
|
-
const askOutput = this._askOutput();
|
|
228
|
-
for (;;) {
|
|
229
|
-
if (this._disposed) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
const [answer] = await this._contexts.ask.askChoices([{
|
|
233
|
-
header: "Plan file",
|
|
234
|
-
question: `Multiple plans found in ${plansFolder}. Which one do you want to implement?`,
|
|
235
|
-
options: files.map(f => ({ label: f })),
|
|
236
|
-
multiSelect: false
|
|
237
|
-
}], askOutput);
|
|
238
|
-
if (this._disposed) {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
if (answer && answer.picked.length > 0) {
|
|
242
|
-
return (0, fsUtils_1.joinPath)(plansFolder, answer.picked[0].label);
|
|
243
|
-
}
|
|
244
|
-
this._buffered.writeError("Please pick one of the listed plans by its number.\n");
|
|
230
|
+
this._buffered.writeError(`Multiple plan files found in ${plansFolder}:\n`);
|
|
231
|
+
for (const f of files) {
|
|
232
|
+
this._buffered.writeError(` ${f}\n`);
|
|
245
233
|
}
|
|
234
|
+
this._buffered.writeError("Re-run with the chosen plan as the [plan] argument.\n");
|
|
235
|
+
return null;
|
|
246
236
|
}
|
|
247
237
|
async _detectBuildAndTest(ws) {
|
|
248
238
|
const prompt = prompts_1.prompts.detectBuildAndTest
|
|
249
239
|
.split("<BUILD_SCRIPT_PATH>").join(ws.buildScript)
|
|
250
240
|
.split("<TEST_SCRIPT_PATH>").join(ws.testScript)
|
|
251
241
|
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList));
|
|
252
|
-
await this.
|
|
242
|
+
await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
|
|
253
243
|
}
|
|
254
|
-
_setActivity(activity) {
|
|
244
|
+
_setActivity(activity, iteration) {
|
|
255
245
|
if (!this._currentTask)
|
|
256
246
|
return;
|
|
257
247
|
this._block.setHeader({
|
|
258
248
|
indexLabel: this._currentIndexLabel,
|
|
259
|
-
iteration: this._currentIteration,
|
|
249
|
+
iteration: iteration === undefined ? this._currentIteration : iteration,
|
|
260
250
|
activity,
|
|
261
251
|
taskNumber: this._currentTask.taskNumber || undefined,
|
|
262
252
|
title: this._currentTask.title
|
|
@@ -282,7 +272,23 @@ class Implement {
|
|
|
282
272
|
plan: { tokens: planTotals.it + planTotals.ot, seconds: planTotals.t }
|
|
283
273
|
});
|
|
284
274
|
}
|
|
285
|
-
|
|
275
|
+
_reviewerMatchesWorker(reviewer) {
|
|
276
|
+
const w = this._config.worker;
|
|
277
|
+
return reviewer.tool === w.tool && reviewer.model === w.model && reviewer.effort === w.effort;
|
|
278
|
+
}
|
|
279
|
+
_prepActive() {
|
|
280
|
+
return this._config.reviewers.some(r => this._reviewerMatchesWorker(r));
|
|
281
|
+
}
|
|
282
|
+
_gitOutputContext() {
|
|
283
|
+
return {
|
|
284
|
+
write: text => this._buffered.write(text),
|
|
285
|
+
writeError: text => this._buffered.writeError(text),
|
|
286
|
+
columns: () => this._contexts.output.columns(),
|
|
287
|
+
rows: () => this._contexts.output.rows(),
|
|
288
|
+
onResize: listener => this._contexts.output.onResize(listener)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async _runTask(plan, task, ws, indexLabel, taskIndex) {
|
|
286
292
|
this._currentTask = task;
|
|
287
293
|
this._currentIndexLabel = indexLabel;
|
|
288
294
|
this._currentWorkerSessionId = null;
|
|
@@ -292,12 +298,20 @@ class Implement {
|
|
|
292
298
|
this._taskRateLimitStartedAt = null;
|
|
293
299
|
this._taskTokens = { it: 0, ot: 0 };
|
|
294
300
|
this._updateMetrics(plan);
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
+
const prepActive = this._prepActive();
|
|
302
|
+
if (prepActive) {
|
|
303
|
+
this._setActivity("preparing", null);
|
|
304
|
+
this._restingFooterKind = "preparing";
|
|
305
|
+
this._block.setFooter({ kind: "preparing" });
|
|
306
|
+
const prepOk = await this._prepStage(plan, task, ws, taskIndex);
|
|
307
|
+
if (this._disposed) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
if (!prepOk) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
this._restingFooterKind = "working";
|
|
314
|
+
this._block.setFooter({ kind: "working" });
|
|
301
315
|
}
|
|
302
316
|
let iteration = 0;
|
|
303
317
|
for (;;) {
|
|
@@ -312,13 +326,18 @@ class Implement {
|
|
|
312
326
|
return false;
|
|
313
327
|
}
|
|
314
328
|
this._setActivity("implementing");
|
|
315
|
-
const workerOk = await this._workerStage(plan, task, ws, iteration);
|
|
329
|
+
const workerOk = await this._workerStage(plan, task, ws, iteration, prepActive);
|
|
316
330
|
if (this._disposed) {
|
|
317
331
|
return false;
|
|
318
332
|
}
|
|
319
333
|
if (!workerOk) {
|
|
320
334
|
continue;
|
|
321
335
|
}
|
|
336
|
+
const workerAddResult = await (0, Git_1.addAll)(this._contexts.script, this._contexts.time, this._gitOutputContext(), this._options.projectRoot);
|
|
337
|
+
if (workerAddResult.code !== 0) {
|
|
338
|
+
await this._writeErrorLog(ws, `git add -A failed (exit ${workerAddResult.code})\n--- stdout ---\n${workerAddResult.stdout}\n--- stderr ---\n${workerAddResult.stderr}`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
322
341
|
this._setActivity("building");
|
|
323
342
|
const buildOk = await this._buildStage(plan, task.line, ws, iteration);
|
|
324
343
|
if (this._disposed) {
|
|
@@ -336,35 +355,27 @@ class Implement {
|
|
|
336
355
|
continue;
|
|
337
356
|
}
|
|
338
357
|
this._setActivity("reviewing");
|
|
339
|
-
const reviewOk = await this._reviewerStage(plan, task, ws, iteration);
|
|
358
|
+
const reviewOk = await this._reviewerStage(plan, task, ws, iteration, prepActive);
|
|
340
359
|
if (this._disposed) {
|
|
341
360
|
return false;
|
|
342
361
|
}
|
|
362
|
+
this._block.setFooter({ kind: "working" });
|
|
343
363
|
if (!reviewOk) {
|
|
344
364
|
continue;
|
|
345
365
|
}
|
|
346
366
|
await plan.markDone(task.line, { it: this._taskTokens.it, ot: this._taskTokens.ot, t: this._activeSeconds() });
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
await plan.markOpen(task.line, { it: this._taskTokens.it, ot: this._taskTokens.ot, t: this._activeSeconds() });
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
const commitResult = await (0, Git_1.commit)(this._contexts.script, this._contexts.time, gitOutput, this._options.projectRoot, commitMessage);
|
|
363
|
-
if (commitResult.code !== 0) {
|
|
364
|
-
await this._writeErrorLog(ws, `git commit failed (exit ${commitResult.code})\n--- stdout ---\n${commitResult.stdout}\n--- stderr ---\n${commitResult.stderr}`);
|
|
365
|
-
await plan.markOpen(task.line, { it: this._taskTokens.it, ot: this._taskTokens.ot, t: this._activeSeconds() });
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
367
|
+
const commitMessage = task.taskNumber ? `${task.taskNumber} ${task.title}` : task.title;
|
|
368
|
+
const addResult = await (0, Git_1.addAll)(this._contexts.script, this._contexts.time, this._gitOutputContext(), this._options.projectRoot);
|
|
369
|
+
if (addResult.code !== 0) {
|
|
370
|
+
await this._writeErrorLog(ws, `git add -A failed (exit ${addResult.code})\n--- stdout ---\n${addResult.stdout}\n--- stderr ---\n${addResult.stderr}`);
|
|
371
|
+
await plan.markOpen(task.line, { it: this._taskTokens.it, ot: this._taskTokens.ot, t: this._activeSeconds() });
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const commitResult = await (0, Git_1.commit)(this._contexts.script, this._contexts.time, this._gitOutputContext(), this._options.projectRoot, commitMessage);
|
|
375
|
+
if (commitResult.code !== 0) {
|
|
376
|
+
await this._writeErrorLog(ws, `git commit failed (exit ${commitResult.code})\n--- stdout ---\n${commitResult.stdout}\n--- stderr ---\n${commitResult.stderr}`);
|
|
377
|
+
await plan.markOpen(task.line, { it: this._taskTokens.it, ot: this._taskTokens.ot, t: this._activeSeconds() });
|
|
378
|
+
continue;
|
|
368
379
|
}
|
|
369
380
|
const planTotals = plan.planTotals();
|
|
370
381
|
const snapshot = (0, formatters_1.formatSnapshotBlock)(indexLabel, iteration, task.taskNumber || undefined, task.title, this._taskTokens.it + this._taskTokens.ot, this._activeSeconds(), planTotals.it + planTotals.ot, planTotals.t, this._contexts.output.columns());
|
|
@@ -379,9 +390,10 @@ class Implement {
|
|
|
379
390
|
.split("<TASK_LINE>").join(String(task.line))
|
|
380
391
|
.split("<TASK_TITLE>").join(task.title)
|
|
381
392
|
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
382
|
-
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
393
|
+
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
394
|
+
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
|
|
383
395
|
try {
|
|
384
|
-
const { result, capturedOutput } = await this.
|
|
396
|
+
const { result, capturedOutput } = await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
|
|
385
397
|
this._taskTokens.it += result.inputTokens;
|
|
386
398
|
this._taskTokens.ot += result.outputTokens;
|
|
387
399
|
await this._persistMetrics(plan, task.line);
|
|
@@ -405,7 +417,7 @@ class Implement {
|
|
|
405
417
|
return false;
|
|
406
418
|
}
|
|
407
419
|
}
|
|
408
|
-
async _workerStage(plan, task, ws, iteration) {
|
|
420
|
+
async _workerStage(plan, task, ws, iteration, prepActive) {
|
|
409
421
|
let prompt = prompts_1.prompts.worker
|
|
410
422
|
.split("<PLAN_PATH>").join(plan.path)
|
|
411
423
|
.split("<TASK_LINE>").join(String(task.line))
|
|
@@ -413,20 +425,23 @@ class Implement {
|
|
|
413
425
|
.split("<BUILD_SCRIPT_PATH>").join(ws.buildScript)
|
|
414
426
|
.split("<TEST_SCRIPT_PATH>").join(ws.testScript)
|
|
415
427
|
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
416
|
-
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
428
|
+
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
429
|
+
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
|
|
430
|
+
if (iteration === 1 && !prepActive) {
|
|
431
|
+
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
432
|
+
}
|
|
417
433
|
if (iteration > 1) {
|
|
418
434
|
const briefing = prompts_1.prompts.previousIterationBriefing
|
|
419
435
|
.split("<ITERATION>").join(String(iteration))
|
|
420
436
|
.split("<ERROR_LOG_PATH>").join(ws.errorLog);
|
|
421
437
|
prompt = `${prompt}\n\n${briefing}`;
|
|
422
438
|
}
|
|
423
|
-
if (iteration === 1 && this._currentPrepSessionId === null) {
|
|
424
|
-
throw new Error(`Worker iteration 1 for task at line ${task.line} ("${task.title}") requires a prep session id but none was captured`);
|
|
425
|
-
}
|
|
426
439
|
try {
|
|
427
440
|
const { result, capturedOutput } = iteration === 1
|
|
428
|
-
?
|
|
429
|
-
|
|
441
|
+
? prepActive
|
|
442
|
+
? await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt, null, this._currentPrepSessionId)
|
|
443
|
+
: await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt)
|
|
444
|
+
: await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt, this._currentWorkerSessionId);
|
|
430
445
|
if (result.sessionId !== null) {
|
|
431
446
|
this._currentWorkerSessionId = result.sessionId;
|
|
432
447
|
}
|
|
@@ -444,6 +459,25 @@ class Implement {
|
|
|
444
459
|
return false;
|
|
445
460
|
}
|
|
446
461
|
}
|
|
462
|
+
async _appendLinkedContent(plan, task, prompt) {
|
|
463
|
+
const linked = plan.linkedPaths(task);
|
|
464
|
+
const allPaths = [...linked.contracts, ...linked.rules];
|
|
465
|
+
if (allPaths.length === 0) {
|
|
466
|
+
return prompt;
|
|
467
|
+
}
|
|
468
|
+
const sections = [];
|
|
469
|
+
for (const relPath of allPaths) {
|
|
470
|
+
const absPath = (0, fsUtils_1.joinPath)(this._options.projectRoot, relPath);
|
|
471
|
+
try {
|
|
472
|
+
const content = await this._contexts.fs.readFile(absPath);
|
|
473
|
+
sections.push(`## ${relPath}\n\n${content}`);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
sections.push(`## ${relPath}\n\n(file not found)`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return `${prompt}\n\n## Linked reference content\n\n${sections.join("\n\n")}`;
|
|
480
|
+
}
|
|
447
481
|
async _buildStage(plan, taskLine, ws, iteration) {
|
|
448
482
|
if (!(await (0, fsUtils_1.isNonEmptyFile)(this._contexts.fs, ws.buildScript))) {
|
|
449
483
|
return true;
|
|
@@ -478,57 +512,114 @@ class Implement {
|
|
|
478
512
|
}
|
|
479
513
|
return true;
|
|
480
514
|
}
|
|
481
|
-
|
|
482
|
-
if (this.
|
|
483
|
-
|
|
515
|
+
_setReviewerState(reviewerIdx, state) {
|
|
516
|
+
if (!this._reviewerStates)
|
|
517
|
+
return;
|
|
518
|
+
const entry = this._reviewerStates[reviewerIdx];
|
|
519
|
+
if (!entry)
|
|
520
|
+
return;
|
|
521
|
+
this._reviewerStates[reviewerIdx] = { tool: entry.tool, model: entry.model, effort: entry.effort, state };
|
|
522
|
+
this._renderReviewingFooter();
|
|
523
|
+
}
|
|
524
|
+
_renderReviewingFooter() {
|
|
525
|
+
if (!this._block || this._block.isFinalized() || !this._reviewerStates)
|
|
526
|
+
return;
|
|
527
|
+
this._block.setFooter({ kind: "reviewing", reviewers: this._reviewerStates.slice() });
|
|
528
|
+
}
|
|
529
|
+
async _reviewerStage(plan, task, ws, iteration, prepActive) {
|
|
530
|
+
const reviewers = this._config.reviewers;
|
|
531
|
+
this._reviewerStates = reviewers.map(r => ({ tool: r.tool, model: r.model, effort: r.effort, state: "running" }));
|
|
532
|
+
this._renderReviewingFooter();
|
|
533
|
+
for (let i = 0; i < reviewers.length; i++) {
|
|
534
|
+
await this._workspace.clearReviewerErrorLog(i + 1);
|
|
535
|
+
}
|
|
536
|
+
await this._workspace.clearErrorLog();
|
|
537
|
+
let failureCaught = null;
|
|
538
|
+
const launches = reviewers.map((reviewer, idx) => this._runOneReviewerToVerdict(plan, task, ws, iteration, prepActive, reviewer, idx).catch(e => {
|
|
539
|
+
if (failureCaught === null) {
|
|
540
|
+
failureCaught = e;
|
|
541
|
+
}
|
|
542
|
+
}));
|
|
543
|
+
await Promise.all(launches);
|
|
544
|
+
if (this._disposed) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
if (failureCaught !== null) {
|
|
548
|
+
await this._persistMetrics(plan, task.line);
|
|
549
|
+
this._updateMetrics(plan);
|
|
550
|
+
await this._writeErrorLog(ws, `reviewer stage failed: ${this._stringifyError(failureCaught)}`);
|
|
551
|
+
return false;
|
|
484
552
|
}
|
|
485
|
-
const
|
|
553
|
+
const perFile = [];
|
|
554
|
+
for (let i = 0; i < reviewers.length; i++) {
|
|
555
|
+
perFile.push(await this._workspace.readReviewerErrorLog(i + 1));
|
|
556
|
+
}
|
|
557
|
+
const aggregate = perFile.join("\n").replace(/^\s+|\s+$/g, "");
|
|
558
|
+
if (aggregate.length === 0) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
await this._workspace.writeErrorLog(aggregate);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
async _runOneReviewerToVerdict(plan, task, ws, iteration, prepActive, reviewer, idx) {
|
|
565
|
+
const reviewerNum = idx + 1;
|
|
566
|
+
const matchesWorker = this._reviewerMatchesWorker(reviewer);
|
|
567
|
+
const useBranchA = prepActive && matchesWorker;
|
|
568
|
+
let prompt = prompts_1.prompts.reviewer
|
|
486
569
|
.split("<PLAN_PATH>").join(plan.path)
|
|
487
570
|
.split("<TASK_LINE>").join(String(task.line))
|
|
488
571
|
.split("<TASK_TITLE>").join(task.title)
|
|
489
572
|
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
490
|
-
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
491
|
-
|
|
492
|
-
|
|
573
|
+
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
574
|
+
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList))
|
|
575
|
+
.split("<ERROR_LOG_PATH>").join(ws.reviewerErrorLog(reviewerNum));
|
|
576
|
+
if (!useBranchA) {
|
|
577
|
+
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
578
|
+
}
|
|
579
|
+
const aggregateOutput = [];
|
|
580
|
+
for (;;) {
|
|
581
|
+
if (this._disposed) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this._setReviewerState(idx, "running");
|
|
585
|
+
const callbacks = {
|
|
586
|
+
onLongWaitStart: () => {
|
|
587
|
+
if (this._disposed)
|
|
588
|
+
return;
|
|
589
|
+
this._setReviewerState(idx, "waiting");
|
|
590
|
+
},
|
|
591
|
+
onLongWaitEnd: () => {
|
|
592
|
+
if (this._disposed)
|
|
593
|
+
return;
|
|
594
|
+
this._setReviewerState(idx, "running");
|
|
595
|
+
},
|
|
596
|
+
register: (session) => {
|
|
597
|
+
this._activeReviewerSessions.add(session);
|
|
598
|
+
},
|
|
599
|
+
unregister: (session) => {
|
|
600
|
+
this._activeReviewerSessions.delete(session);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const { result, capturedOutput } = useBranchA
|
|
604
|
+
? await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, this._currentPrepSessionId, callbacks)
|
|
605
|
+
: await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, null, callbacks);
|
|
493
606
|
this._taskTokens.it += result.inputTokens;
|
|
494
607
|
this._taskTokens.ot += result.outputTokens;
|
|
495
608
|
await this._persistMetrics(plan, task.line);
|
|
496
609
|
this._updateMetrics(plan);
|
|
497
|
-
|
|
498
|
-
await this.
|
|
499
|
-
|
|
500
|
-
: `${capturedOutput}\n\nVerdict: FAIL ${verdict.reason}`);
|
|
501
|
-
if (verdict.pass) {
|
|
502
|
-
return true;
|
|
503
|
-
}
|
|
504
|
-
await this._writeErrorLog(ws, `reviewer rejected: ${verdict.reason}`);
|
|
505
|
-
return false;
|
|
506
|
-
}
|
|
507
|
-
catch (e) {
|
|
508
|
-
await this._persistMetrics(plan, task.line);
|
|
509
|
-
this._updateMetrics(plan);
|
|
510
|
-
await this._writeErrorLog(ws, `reviewer stage failed: ${this._stringifyError(e)}`);
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
_parseReviewVerdict(text) {
|
|
515
|
-
const trimmed = text.trim();
|
|
516
|
-
const lines = trimmed.split(/\r?\n/);
|
|
517
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
518
|
-
const line = lines[i].trim();
|
|
519
|
-
if (!line) {
|
|
610
|
+
aggregateOutput.push(capturedOutput);
|
|
611
|
+
if (!await this._workspace.reviewerErrorLogExists(reviewerNum)) {
|
|
612
|
+
await this._writeLog(ws.reviewerOutputLog(iteration, reviewerNum), aggregateOutput.join("\n---\n"));
|
|
520
613
|
continue;
|
|
521
614
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
break;
|
|
615
|
+
const trimmed = (await this._workspace.readReviewerErrorLog(reviewerNum)).trim();
|
|
616
|
+
this._setReviewerState(idx, trimmed.length === 0 ? "ok" : "fail");
|
|
617
|
+
const verdictLine = trimmed.length === 0
|
|
618
|
+
? "Verdict: PASS"
|
|
619
|
+
: `Verdict: FAIL ${trimmed}`;
|
|
620
|
+
await this._writeLog(ws.reviewerOutputLog(iteration, reviewerNum), `${aggregateOutput.join("\n---\n")}\n\n${verdictLine}`);
|
|
621
|
+
return;
|
|
530
622
|
}
|
|
531
|
-
return { pass: false, reason: `unrecognized reviewer verdict: ${trimmed}` };
|
|
532
623
|
}
|
|
533
624
|
_formatPathList(items) {
|
|
534
625
|
if (items.length === 0) {
|
|
@@ -536,7 +627,53 @@ class Implement {
|
|
|
536
627
|
}
|
|
537
628
|
return items.join("\n");
|
|
538
629
|
}
|
|
539
|
-
|
|
630
|
+
_getAdapter(tool) {
|
|
631
|
+
if (tool === "codex") {
|
|
632
|
+
return new CodexAdapter_1.CodexAdapter({
|
|
633
|
+
script: this._contexts.script,
|
|
634
|
+
time: this._contexts.time,
|
|
635
|
+
random: this._contexts.random
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return new ClaudeAdapter_1.ClaudeAdapter({
|
|
639
|
+
claude: this._contexts.claude,
|
|
640
|
+
time: this._contexts.time
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
_defaultRunAiCallbacks() {
|
|
644
|
+
return {
|
|
645
|
+
onLongWaitStart: (kind, endTimeMs) => {
|
|
646
|
+
if (this._disposed)
|
|
647
|
+
return;
|
|
648
|
+
if (this._currentTask !== null) {
|
|
649
|
+
this._taskRateLimitStartedAt = this._contexts.time.now();
|
|
650
|
+
}
|
|
651
|
+
this._block.setFooter({ kind: "waiting", waitKind: kind, endTime: endTimeMs });
|
|
652
|
+
},
|
|
653
|
+
onLongWaitEnd: () => {
|
|
654
|
+
if (this._taskRateLimitStartedAt !== null) {
|
|
655
|
+
this._taskRateLimitMs += this._contexts.time.now() - this._taskRateLimitStartedAt;
|
|
656
|
+
this._taskRateLimitStartedAt = null;
|
|
657
|
+
}
|
|
658
|
+
if (this._disposed)
|
|
659
|
+
return;
|
|
660
|
+
this._block.setFooter({ kind: this._restingFooterKind });
|
|
661
|
+
},
|
|
662
|
+
register: (session) => {
|
|
663
|
+
this._activeSession = { session };
|
|
664
|
+
},
|
|
665
|
+
unregister: (session) => {
|
|
666
|
+
var _a;
|
|
667
|
+
if (((_a = this._activeSession) === null || _a === void 0 ? void 0 : _a.session) === session) {
|
|
668
|
+
this._activeSession = null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
_runAi(tool, model, effort, prompt, initialSessionId, forkFromSessionId) {
|
|
674
|
+
return this._runAiWith(tool, model, effort, prompt, initialSessionId !== null && initialSessionId !== void 0 ? initialSessionId : null, forkFromSessionId !== null && forkFromSessionId !== void 0 ? forkFromSessionId : null, this._defaultRunAiCallbacks());
|
|
675
|
+
}
|
|
676
|
+
async _runAiWith(tool, model, effort, prompt, initialSessionId, forkFromSessionId, callbacks) {
|
|
540
677
|
if (this._disposed) {
|
|
541
678
|
throw new Error("Implement disposed");
|
|
542
679
|
}
|
|
@@ -554,44 +691,27 @@ class Implement {
|
|
|
554
691
|
rows: () => this._contexts.output.rows(),
|
|
555
692
|
onResize: listener => this._contexts.output.onResize(listener)
|
|
556
693
|
};
|
|
557
|
-
const
|
|
694
|
+
const adapter = this._getAdapter(tool);
|
|
695
|
+
const session = new AiSession_1.AiSession({
|
|
696
|
+
adapter,
|
|
558
697
|
prompt,
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
...(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (this._currentTask !== null) {
|
|
566
|
-
this._taskRateLimitStartedAt = this._contexts.time.now();
|
|
567
|
-
}
|
|
568
|
-
this._block.setFooter({ kind: "waiting", waitKind: kind, endTime: endTimeMs });
|
|
569
|
-
},
|
|
570
|
-
onLongWaitEnd: () => {
|
|
571
|
-
if (this._taskRateLimitStartedAt !== null) {
|
|
572
|
-
this._taskRateLimitMs += this._contexts.time.now() - this._taskRateLimitStartedAt;
|
|
573
|
-
this._taskRateLimitStartedAt = null;
|
|
574
|
-
}
|
|
575
|
-
if (this._disposed)
|
|
576
|
-
return;
|
|
577
|
-
this._block.setFooter({ kind: "working" });
|
|
578
|
-
}
|
|
698
|
+
model,
|
|
699
|
+
effort,
|
|
700
|
+
...(initialSessionId != null ? { resumeSessionId: initialSessionId } : null),
|
|
701
|
+
...(forkFromSessionId != null ? { forkParentSessionId: forkFromSessionId } : null),
|
|
702
|
+
onLongWaitStart: callbacks.onLongWaitStart,
|
|
703
|
+
onLongWaitEnd: callbacks.onLongWaitEnd
|
|
579
704
|
}, {
|
|
580
|
-
claude: this._contexts.claude,
|
|
581
705
|
time: this._contexts.time,
|
|
582
|
-
output: capturingOutput
|
|
583
|
-
ask: this._contexts.ask
|
|
706
|
+
output: capturingOutput
|
|
584
707
|
});
|
|
585
|
-
|
|
586
|
-
this._activeSession = running;
|
|
708
|
+
callbacks.register(session);
|
|
587
709
|
try {
|
|
588
710
|
const result = await session.run();
|
|
589
711
|
return { result, capturedOutput };
|
|
590
712
|
}
|
|
591
713
|
finally {
|
|
592
|
-
|
|
593
|
-
this._activeSession = null;
|
|
594
|
-
}
|
|
714
|
+
callbacks.unregister(session);
|
|
595
715
|
await session.dispose();
|
|
596
716
|
}
|
|
597
717
|
}
|
|
@@ -661,8 +781,10 @@ class Implement {
|
|
|
661
781
|
this._disposed = true;
|
|
662
782
|
const activeSession = (_a = this._activeSession) === null || _a === void 0 ? void 0 : _a.session;
|
|
663
783
|
const activeScript = (_b = this._activeScript) === null || _b === void 0 ? void 0 : _b.script;
|
|
784
|
+
const reviewerSessions = [...this._activeReviewerSessions];
|
|
664
785
|
this._activeSession = null;
|
|
665
786
|
this._activeScript = null;
|
|
787
|
+
this._activeReviewerSessions.clear();
|
|
666
788
|
const closers = [];
|
|
667
789
|
if (activeSession) {
|
|
668
790
|
closers.push(activeSession.dispose());
|
|
@@ -670,6 +792,9 @@ class Implement {
|
|
|
670
792
|
if (activeScript) {
|
|
671
793
|
closers.push(activeScript.dispose());
|
|
672
794
|
}
|
|
795
|
+
for (const session of reviewerSessions) {
|
|
796
|
+
closers.push(session.dispose());
|
|
797
|
+
}
|
|
673
798
|
await Promise.allSettled(closers);
|
|
674
799
|
try {
|
|
675
800
|
await this._runPromise;
|