flanders 0.3.0 → 0.5.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 +0 -1
- package/lib/ai/AiRunner.js +1 -4
- package/lib/ai/AiSession.d.ts +0 -1
- package/lib/ai/AiSession.js +0 -1
- package/lib/ai/ClaudeAdapter.js +0 -3
- package/lib/ai/CodexAdapter.js +7 -11
- package/lib/ai/ToolAdapter.d.ts +1 -7
- package/lib/commands/Implement.d.ts +5 -5
- package/lib/commands/Implement.js +144 -135
- package/lib/commands/Install.d.ts +2 -0
- package/lib/commands/Install.js +151 -1
- package/lib/commands/InstallModelProbe.js +39 -41
- package/lib/plan/PlanFile.d.ts +2 -0
- package/lib/plan/PlanFile.js +32 -18
- package/lib/prompts/prompts.d.ts +1 -3
- package/lib/prompts/prompts.js +18 -68
- package/lib/prompts/skills.d.ts +1 -1
- package/lib/prompts/skills.js +39 -30
- package/lib/ui/BottomBlock.d.ts +1 -3
- package/lib/ui/BottomBlock.js +3 -5
- package/lib/ui/formatters.d.ts +0 -1
- package/lib/ui/formatters.js +1 -5
- package/lib/workspace/FlandersConfig.d.ts +5 -1
- package/lib/workspace/FlandersConfig.js +9 -1
- package/lib/workspace/SpecDiscovery.js +4 -4
- package/package.json +1 -1
package/lib/ai/AiRunner.d.ts
CHANGED
package/lib/ai/AiRunner.js
CHANGED
|
@@ -18,10 +18,7 @@ async function run(args) {
|
|
|
18
18
|
const base = { prompt, model, effort, abortSignal, onUsage: callbacks.onUsage };
|
|
19
19
|
let invokeArgs;
|
|
20
20
|
if (firstInvocation) {
|
|
21
|
-
if (args.
|
|
22
|
-
invokeArgs = { ...base, forkParentSessionId: args.forkParentSessionId };
|
|
23
|
-
}
|
|
24
|
-
else if (args.resumeSessionId) {
|
|
21
|
+
if (args.resumeSessionId) {
|
|
25
22
|
invokeArgs = { ...base, resumeSessionId: args.resumeSessionId };
|
|
26
23
|
}
|
|
27
24
|
else {
|
package/lib/ai/AiSession.d.ts
CHANGED
package/lib/ai/AiSession.js
CHANGED
|
@@ -72,7 +72,6 @@ class AiSession {
|
|
|
72
72
|
model: this._options.model,
|
|
73
73
|
effort: this._options.effort,
|
|
74
74
|
...(this._options.resumeSessionId != null ? { resumeSessionId: this._options.resumeSessionId } : null),
|
|
75
|
-
...(this._options.forkParentSessionId != null ? { forkParentSessionId: this._options.forkParentSessionId } : null),
|
|
76
75
|
abortSignal: controller.signal,
|
|
77
76
|
callbacks: {
|
|
78
77
|
onOutput,
|
package/lib/ai/ClaudeAdapter.js
CHANGED
|
@@ -167,9 +167,6 @@ class ClaudeAdapterIterator {
|
|
|
167
167
|
if (this._args.resumeSessionId) {
|
|
168
168
|
argv.push("--resume", this._args.resumeSessionId);
|
|
169
169
|
}
|
|
170
|
-
else if (this._args.forkParentSessionId) {
|
|
171
|
-
argv.push("--resume", this._args.forkParentSessionId, "--fork-session");
|
|
172
|
-
}
|
|
173
170
|
if (this._args.model) {
|
|
174
171
|
argv.push("--model", this._args.model);
|
|
175
172
|
}
|
package/lib/ai/CodexAdapter.js
CHANGED
|
@@ -98,15 +98,14 @@ class CodexAdapterIterator {
|
|
|
98
98
|
this._sawTurnCompleted = false;
|
|
99
99
|
this._receivedAnyEvent = false;
|
|
100
100
|
this._fallbackAttempted = false;
|
|
101
|
-
this.
|
|
101
|
+
this._usedResume = false;
|
|
102
102
|
this._start(false);
|
|
103
103
|
}
|
|
104
104
|
_start(useExecFallback) {
|
|
105
105
|
var _a, _b, _c;
|
|
106
106
|
const isResume = !useExecFallback && !!this._args.resumeSessionId;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const argv = this._buildArgv(isResume, isFork);
|
|
107
|
+
this._usedResume = isResume;
|
|
108
|
+
const argv = this._buildArgv(isResume);
|
|
110
109
|
const spawnOptions = { stdio: "pipe" };
|
|
111
110
|
const proc = this._contexts.script.spawn("codex", argv, spawnOptions);
|
|
112
111
|
this._proc = proc;
|
|
@@ -149,18 +148,18 @@ class CodexAdapterIterator {
|
|
|
149
148
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
150
149
|
return;
|
|
151
150
|
}
|
|
152
|
-
if (this.
|
|
151
|
+
if (this._usedResume && !this._receivedAnyEvent && !this._fallbackAttempted) {
|
|
153
152
|
this._fallbackAttempted = true;
|
|
154
153
|
this._queue.push({
|
|
155
154
|
type: "output",
|
|
156
155
|
title: "Continuity lost",
|
|
157
156
|
subtitle: "",
|
|
158
|
-
details: "codex resume
|
|
157
|
+
details: "codex resume unavailable in installed CLI"
|
|
159
158
|
});
|
|
160
159
|
this._cleanup();
|
|
161
160
|
this._sawTurnCompleted = false;
|
|
162
161
|
this._receivedAnyEvent = false;
|
|
163
|
-
this.
|
|
162
|
+
this._usedResume = false;
|
|
164
163
|
exitResolve === null || exitResolve === void 0 ? void 0 : exitResolve();
|
|
165
164
|
this._start(true);
|
|
166
165
|
this._wake();
|
|
@@ -201,14 +200,11 @@ class CodexAdapterIterator {
|
|
|
201
200
|
this._args.abortSignal.addEventListener("abort", this._abortListener, { once: true });
|
|
202
201
|
}
|
|
203
202
|
}
|
|
204
|
-
_buildArgv(isResume
|
|
203
|
+
_buildArgv(isResume) {
|
|
205
204
|
const argv = [];
|
|
206
205
|
if (isResume) {
|
|
207
206
|
argv.push("resume", this._args.resumeSessionId);
|
|
208
207
|
}
|
|
209
|
-
else if (isFork) {
|
|
210
|
-
argv.push("fork", this._args.forkParentSessionId);
|
|
211
|
-
}
|
|
212
208
|
else {
|
|
213
209
|
argv.push("exec");
|
|
214
210
|
}
|
package/lib/ai/ToolAdapter.d.ts
CHANGED
|
@@ -34,17 +34,11 @@ type ToolAdapterInvokeArgsBase = Readonly<{
|
|
|
34
34
|
}>;
|
|
35
35
|
export type ToolAdapterInvokeArgsFresh = ToolAdapterInvokeArgsBase & Readonly<{
|
|
36
36
|
resumeSessionId?: undefined;
|
|
37
|
-
forkParentSessionId?: undefined;
|
|
38
37
|
}>;
|
|
39
38
|
export type ToolAdapterInvokeArgsResume = ToolAdapterInvokeArgsBase & Readonly<{
|
|
40
39
|
resumeSessionId: string;
|
|
41
|
-
forkParentSessionId?: undefined;
|
|
42
40
|
}>;
|
|
43
|
-
export type
|
|
44
|
-
resumeSessionId?: undefined;
|
|
45
|
-
forkParentSessionId: string;
|
|
46
|
-
}>;
|
|
47
|
-
export type ToolAdapterInvokeArgs = ToolAdapterInvokeArgsFresh | ToolAdapterInvokeArgsResume | ToolAdapterInvokeArgsFork;
|
|
41
|
+
export type ToolAdapterInvokeArgs = ToolAdapterInvokeArgsFresh | ToolAdapterInvokeArgsResume;
|
|
48
42
|
export interface ToolAdapter {
|
|
49
43
|
invoke(args: ToolAdapterInvokeArgs): AsyncIterable<ToolEvent>;
|
|
50
44
|
}
|
|
@@ -27,11 +27,13 @@ export declare class Implement {
|
|
|
27
27
|
private _block;
|
|
28
28
|
private _buffered;
|
|
29
29
|
private _currentWorkerSessionId;
|
|
30
|
-
private _currentPrepSessionId;
|
|
31
30
|
private _activeSession;
|
|
32
31
|
private _activeScript;
|
|
33
32
|
private _activeReviewerSessions;
|
|
34
33
|
private _reviewerStates;
|
|
34
|
+
private _reviewerLogicalStatuses;
|
|
35
|
+
private _reviewerAbortControllers;
|
|
36
|
+
private _cancelledReviewers;
|
|
35
37
|
private _currentIndexLabel;
|
|
36
38
|
private _currentIteration;
|
|
37
39
|
private _currentTask;
|
|
@@ -39,7 +41,6 @@ export declare class Implement {
|
|
|
39
41
|
private _taskRateLimitMs;
|
|
40
42
|
private _taskRateLimitStartedAt;
|
|
41
43
|
private _taskTokens;
|
|
42
|
-
private _restingFooterKind;
|
|
43
44
|
private _runPromise;
|
|
44
45
|
get config(): FlandersConfig | null;
|
|
45
46
|
constructor(rawArgs: readonly string[], _options: ImplementOptions, _contexts: ImplementContexts);
|
|
@@ -51,17 +52,16 @@ export declare class Implement {
|
|
|
51
52
|
private _activeSeconds;
|
|
52
53
|
private _persistMetrics;
|
|
53
54
|
private _updateMetrics;
|
|
54
|
-
private _reviewerMatchesWorker;
|
|
55
|
-
private _prepActive;
|
|
56
55
|
private _gitOutputContext;
|
|
57
56
|
private _runTask;
|
|
58
|
-
private _prepStage;
|
|
59
57
|
private _workerStage;
|
|
60
58
|
private _appendLinkedContent;
|
|
61
59
|
private _buildStage;
|
|
62
60
|
private _testStage;
|
|
63
61
|
private _setReviewerState;
|
|
64
62
|
private _renderReviewingFooter;
|
|
63
|
+
private _setReviewerLogicalStatus;
|
|
64
|
+
private _evaluateReviewRoundCompletion;
|
|
65
65
|
private _reviewerStage;
|
|
66
66
|
private _runOneReviewerToVerdict;
|
|
67
67
|
private _formatPathList;
|
|
@@ -61,11 +61,13 @@ class Implement {
|
|
|
61
61
|
this._workspace = null;
|
|
62
62
|
this._block = null;
|
|
63
63
|
this._currentWorkerSessionId = null;
|
|
64
|
-
this._currentPrepSessionId = null;
|
|
65
64
|
this._activeSession = null;
|
|
66
65
|
this._activeScript = null;
|
|
67
66
|
this._activeReviewerSessions = new Set();
|
|
68
67
|
this._reviewerStates = null;
|
|
68
|
+
this._reviewerLogicalStatuses = [];
|
|
69
|
+
this._reviewerAbortControllers = new Map();
|
|
70
|
+
this._cancelledReviewers = new Set();
|
|
69
71
|
this._currentIndexLabel = "";
|
|
70
72
|
this._currentIteration = 0;
|
|
71
73
|
this._currentTask = null;
|
|
@@ -73,7 +75,6 @@ class Implement {
|
|
|
73
75
|
this._taskRateLimitMs = 0;
|
|
74
76
|
this._taskRateLimitStartedAt = null;
|
|
75
77
|
this._taskTokens = { it: 0, ot: 0 };
|
|
76
|
-
this._restingFooterKind = "working";
|
|
77
78
|
this._runPromise = this._run(rawArgs);
|
|
78
79
|
this._runPromise.catch(() => { });
|
|
79
80
|
}
|
|
@@ -182,7 +183,7 @@ class Implement {
|
|
|
182
183
|
const refreshed = plan.parse();
|
|
183
184
|
const completedSoFar = refreshed.tasks.filter(t => t.done).length;
|
|
184
185
|
const indexLabel = `${completedSoFar + 1}/${totalTasks}`;
|
|
185
|
-
const taskOk = await this._runTask(plan, open, wsPaths, indexLabel
|
|
186
|
+
const taskOk = await this._runTask(plan, open, wsPaths, indexLabel);
|
|
186
187
|
if (this._disposed) {
|
|
187
188
|
this._finalizeBlock("Interrupted");
|
|
188
189
|
return 1;
|
|
@@ -241,12 +242,12 @@ class Implement {
|
|
|
241
242
|
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList));
|
|
242
243
|
await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
|
|
243
244
|
}
|
|
244
|
-
_setActivity(activity
|
|
245
|
+
_setActivity(activity) {
|
|
245
246
|
if (!this._currentTask)
|
|
246
247
|
return;
|
|
247
248
|
this._block.setHeader({
|
|
248
249
|
indexLabel: this._currentIndexLabel,
|
|
249
|
-
iteration:
|
|
250
|
+
iteration: this._currentIteration,
|
|
250
251
|
activity,
|
|
251
252
|
taskNumber: this._currentTask.taskNumber || undefined,
|
|
252
253
|
title: this._currentTask.title
|
|
@@ -272,13 +273,6 @@ class Implement {
|
|
|
272
273
|
plan: { tokens: planTotals.it + planTotals.ot, seconds: planTotals.t }
|
|
273
274
|
});
|
|
274
275
|
}
|
|
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
276
|
_gitOutputContext() {
|
|
283
277
|
return {
|
|
284
278
|
write: text => this._buffered.write(text),
|
|
@@ -288,31 +282,15 @@ class Implement {
|
|
|
288
282
|
onResize: listener => this._contexts.output.onResize(listener)
|
|
289
283
|
};
|
|
290
284
|
}
|
|
291
|
-
async _runTask(plan, task, ws, indexLabel
|
|
285
|
+
async _runTask(plan, task, ws, indexLabel) {
|
|
292
286
|
this._currentTask = task;
|
|
293
287
|
this._currentIndexLabel = indexLabel;
|
|
294
288
|
this._currentWorkerSessionId = null;
|
|
295
|
-
this._currentPrepSessionId = null;
|
|
296
289
|
this._taskStartedAt = this._contexts.time.now();
|
|
297
290
|
this._taskRateLimitMs = 0;
|
|
298
291
|
this._taskRateLimitStartedAt = null;
|
|
299
292
|
this._taskTokens = { it: 0, ot: 0 };
|
|
300
293
|
this._updateMetrics(plan);
|
|
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" });
|
|
315
|
-
}
|
|
316
294
|
let iteration = 0;
|
|
317
295
|
for (;;) {
|
|
318
296
|
iteration++;
|
|
@@ -326,7 +304,7 @@ class Implement {
|
|
|
326
304
|
return false;
|
|
327
305
|
}
|
|
328
306
|
this._setActivity("implementing");
|
|
329
|
-
const workerOk = await this._workerStage(plan, task, ws, iteration
|
|
307
|
+
const workerOk = await this._workerStage(plan, task, ws, iteration);
|
|
330
308
|
if (this._disposed) {
|
|
331
309
|
return false;
|
|
332
310
|
}
|
|
@@ -355,7 +333,7 @@ class Implement {
|
|
|
355
333
|
continue;
|
|
356
334
|
}
|
|
357
335
|
this._setActivity("reviewing");
|
|
358
|
-
const reviewOk = await this._reviewerStage(plan, task, ws, iteration
|
|
336
|
+
const reviewOk = await this._reviewerStage(plan, task, ws, iteration);
|
|
359
337
|
if (this._disposed) {
|
|
360
338
|
return false;
|
|
361
339
|
}
|
|
@@ -384,52 +362,25 @@ class Implement {
|
|
|
384
362
|
return true;
|
|
385
363
|
}
|
|
386
364
|
}
|
|
387
|
-
async
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
.
|
|
391
|
-
.split("<TASK_TITLE>").join(task.title)
|
|
392
|
-
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
393
|
-
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
394
|
-
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
|
|
395
|
-
try {
|
|
396
|
-
const { result, capturedOutput } = await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt);
|
|
397
|
-
this._taskTokens.it += result.inputTokens;
|
|
398
|
-
this._taskTokens.ot += result.outputTokens;
|
|
399
|
-
await this._persistMetrics(plan, task.line);
|
|
400
|
-
this._updateMetrics(plan);
|
|
401
|
-
await this._writeLog(ws.prepLog(taskIndex), capturedOutput);
|
|
402
|
-
if (result.sessionId === null) {
|
|
403
|
-
this._workspace.preserveOnDispose();
|
|
404
|
-
await this._writeErrorLog(ws, `prep returned no session id for task at line ${task.line} ("${task.title}")`);
|
|
405
|
-
this._buffered.writeError(`Hard stop: prep for task at line ${task.line} ("${task.title}") returned no session id. Inspect logs at ${ws.root}.\n`);
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
this._currentPrepSessionId = result.sessionId;
|
|
409
|
-
return true;
|
|
365
|
+
async _workerStage(plan, task, ws, iteration) {
|
|
366
|
+
let taskText;
|
|
367
|
+
if (iteration === 1) {
|
|
368
|
+
taskText = plan.fullTaskText(task);
|
|
410
369
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
this._buffered.writeError(`Hard stop: prep for task at line ${task.line} ("${task.title}") failed. Inspect logs at ${ws.root}.\n`);
|
|
417
|
-
return false;
|
|
370
|
+
else if (this._currentWorkerSessionId !== null) {
|
|
371
|
+
taskText = "(The full task text and the contracts and rules it references were provided when this session began and remain available to you through session continuity; they are not repeated here.)";
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
taskText = `(Your previous session for this task could not be resumed, so this is a fresh invocation. You are continuing work on the task at line ${task.line} of the plan file, titled "${task.title}". Re-read that task and the contracts and rules it references directly from the files on disk — they are not re-injected here — and address the previous-iteration briefing below.)`;
|
|
418
375
|
}
|
|
419
|
-
}
|
|
420
|
-
async _workerStage(plan, task, ws, iteration, prepActive) {
|
|
421
376
|
let prompt = prompts_1.prompts.worker
|
|
422
377
|
.split("<PLAN_PATH>").join(plan.path)
|
|
423
|
-
.split("<
|
|
424
|
-
.split("<TASK_TITLE>").join(task.title)
|
|
378
|
+
.split("<TASK_TEXT>").join(taskText)
|
|
425
379
|
.split("<BUILD_SCRIPT_PATH>").join(ws.buildScript)
|
|
426
380
|
.split("<TEST_SCRIPT_PATH>").join(ws.testScript)
|
|
427
381
|
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
428
382
|
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
429
383
|
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList));
|
|
430
|
-
if (iteration === 1 && !prepActive) {
|
|
431
|
-
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
432
|
-
}
|
|
433
384
|
if (iteration > 1) {
|
|
434
385
|
const briefing = prompts_1.prompts.previousIterationBriefing
|
|
435
386
|
.split("<ITERATION>").join(String(iteration))
|
|
@@ -437,10 +388,11 @@ class Implement {
|
|
|
437
388
|
prompt = `${prompt}\n\n${briefing}`;
|
|
438
389
|
}
|
|
439
390
|
try {
|
|
391
|
+
if (iteration === 1) {
|
|
392
|
+
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
393
|
+
}
|
|
440
394
|
const { result, capturedOutput } = iteration === 1
|
|
441
|
-
?
|
|
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)
|
|
395
|
+
? await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt)
|
|
444
396
|
: await this._runAi(this._config.worker.tool, this._config.worker.model, this._config.worker.effort, prompt, this._currentWorkerSessionId);
|
|
445
397
|
if (result.sessionId !== null) {
|
|
446
398
|
this._currentWorkerSessionId = result.sessionId;
|
|
@@ -468,15 +420,10 @@ class Implement {
|
|
|
468
420
|
const sections = [];
|
|
469
421
|
for (const relPath of allPaths) {
|
|
470
422
|
const absPath = (0, fsUtils_1.joinPath)(this._options.projectRoot, relPath);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
sections.push(`## ${relPath}\n\n${content}`);
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
sections.push(`## ${relPath}\n\n(file not found)`);
|
|
477
|
-
}
|
|
423
|
+
const content = await this._contexts.fs.readFile(absPath);
|
|
424
|
+
sections.push(`## ${relPath}\n\n${content}`);
|
|
478
425
|
}
|
|
479
|
-
return `${prompt}\n\n## Linked reference content\n\n${sections.join("\n\n")}`;
|
|
426
|
+
return `${prompt}\n\n## Linked reference content\n\nThe full content of every contract and rule this task references is included below, so you do not need to open these files for this iteration:\n\n${sections.join("\n\n")}`;
|
|
480
427
|
}
|
|
481
428
|
async _buildStage(plan, taskLine, ws, iteration) {
|
|
482
429
|
if (!(await (0, fsUtils_1.isNonEmptyFile)(this._contexts.fs, ws.buildScript))) {
|
|
@@ -526,16 +473,49 @@ class Implement {
|
|
|
526
473
|
return;
|
|
527
474
|
this._block.setFooter({ kind: "reviewing", reviewers: this._reviewerStates.slice() });
|
|
528
475
|
}
|
|
529
|
-
|
|
476
|
+
_setReviewerLogicalStatus(idx, status) {
|
|
477
|
+
this._reviewerLogicalStatuses[idx] = status;
|
|
478
|
+
if (status !== "running") {
|
|
479
|
+
this._evaluateReviewRoundCompletion();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
_evaluateReviewRoundCompletion() {
|
|
483
|
+
const statuses = this._reviewerLogicalStatuses;
|
|
484
|
+
const reviewers = this._config.reviewers;
|
|
485
|
+
if (statuses.some(s => s === "running")) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
for (let i = 0; i < reviewers.length; i++) {
|
|
489
|
+
if (!reviewers[i].optional && statuses[i] !== "done") {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const doneCount = statuses.filter(s => s === "done").length;
|
|
494
|
+
if (doneCount < this._config.minimumReviews) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (let i = 0; i < statuses.length; i++) {
|
|
498
|
+
if (statuses[i] === "waiting") {
|
|
499
|
+
this._cancelledReviewers.add(i);
|
|
500
|
+
this._reviewerAbortControllers.get(i).abort();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async _reviewerStage(plan, task, ws, iteration) {
|
|
530
505
|
const reviewers = this._config.reviewers;
|
|
531
506
|
this._reviewerStates = reviewers.map(r => ({ tool: r.tool, model: r.model, effort: r.effort, state: "running" }));
|
|
507
|
+
this._reviewerLogicalStatuses = reviewers.map(() => "running");
|
|
508
|
+
this._cancelledReviewers = new Set();
|
|
532
509
|
this._renderReviewingFooter();
|
|
533
510
|
for (let i = 0; i < reviewers.length; i++) {
|
|
534
511
|
await this._workspace.clearReviewerErrorLog(i + 1);
|
|
535
512
|
}
|
|
536
513
|
await this._workspace.clearErrorLog();
|
|
537
514
|
let failureCaught = null;
|
|
538
|
-
const
|
|
515
|
+
const outcomes = reviewers.map(() => "cancelled");
|
|
516
|
+
const launches = reviewers.map((reviewer, idx) => this._runOneReviewerToVerdict(plan, task, ws, iteration, reviewer, idx)
|
|
517
|
+
.then(outcome => { outcomes[idx] = outcome; })
|
|
518
|
+
.catch(e => {
|
|
539
519
|
if (failureCaught === null) {
|
|
540
520
|
failureCaught = e;
|
|
541
521
|
}
|
|
@@ -552,7 +532,9 @@ class Implement {
|
|
|
552
532
|
}
|
|
553
533
|
const perFile = [];
|
|
554
534
|
for (let i = 0; i < reviewers.length; i++) {
|
|
555
|
-
|
|
535
|
+
if (outcomes[i] === "verdict") {
|
|
536
|
+
perFile.push(await this._workspace.readReviewerErrorLog(i + 1));
|
|
537
|
+
}
|
|
556
538
|
}
|
|
557
539
|
const aggregate = perFile.join("\n").replace(/^\s+|\s+$/g, "");
|
|
558
540
|
if (aggregate.length === 0) {
|
|
@@ -561,64 +543,88 @@ class Implement {
|
|
|
561
543
|
await this._workspace.writeErrorLog(aggregate);
|
|
562
544
|
return false;
|
|
563
545
|
}
|
|
564
|
-
async _runOneReviewerToVerdict(plan, task, ws, iteration,
|
|
546
|
+
async _runOneReviewerToVerdict(plan, task, ws, iteration, reviewer, idx) {
|
|
565
547
|
const reviewerNum = idx + 1;
|
|
566
|
-
const matchesWorker = this._reviewerMatchesWorker(reviewer);
|
|
567
|
-
const useBranchA = prepActive && matchesWorker;
|
|
568
548
|
let prompt = prompts_1.prompts.reviewer
|
|
569
549
|
.split("<PLAN_PATH>").join(plan.path)
|
|
570
|
-
.split("<
|
|
571
|
-
.split("<TASK_TITLE>").join(task.title)
|
|
550
|
+
.split("<TASK_TEXT>").join(plan.fullTaskText(task))
|
|
572
551
|
.split("<CONTRACT_LIST>").join(this._formatPathList(this._contractList))
|
|
573
552
|
.split("<RULE_LIST>").join(this._formatPathList(this._ruleList))
|
|
574
553
|
.split("<BEHAVIOR_RULE_LIST>").join(this._formatPathList(this._behaviorRuleList))
|
|
575
554
|
.split("<ERROR_LOG_PATH>").join(ws.reviewerErrorLog(reviewerNum));
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
},
|
|
591
|
-
onLongWaitEnd: () => {
|
|
592
|
-
if (this._disposed)
|
|
555
|
+
prompt = await this._appendLinkedContent(plan, task, prompt);
|
|
556
|
+
const controller = new AbortController();
|
|
557
|
+
this._reviewerAbortControllers.set(idx, controller);
|
|
558
|
+
try {
|
|
559
|
+
const aggregateOutput = [];
|
|
560
|
+
for (;;) {
|
|
561
|
+
if (this._disposed) {
|
|
562
|
+
return "cancelled";
|
|
563
|
+
}
|
|
564
|
+
this._setReviewerState(idx, "running");
|
|
565
|
+
this._setReviewerLogicalStatus(idx, "running");
|
|
566
|
+
let currentSession = null;
|
|
567
|
+
const onAbort = () => {
|
|
568
|
+
if (!currentSession)
|
|
593
569
|
return;
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
570
|
+
void currentSession.dispose();
|
|
571
|
+
};
|
|
572
|
+
const callbacks = {
|
|
573
|
+
onLongWaitStart: () => {
|
|
574
|
+
if (this._disposed)
|
|
575
|
+
return;
|
|
576
|
+
this._setReviewerState(idx, "waiting");
|
|
577
|
+
this._setReviewerLogicalStatus(idx, "waiting");
|
|
578
|
+
},
|
|
579
|
+
onLongWaitEnd: () => {
|
|
580
|
+
if (this._disposed)
|
|
581
|
+
return;
|
|
582
|
+
this._setReviewerState(idx, "running");
|
|
583
|
+
this._setReviewerLogicalStatus(idx, "running");
|
|
584
|
+
},
|
|
585
|
+
register: (session) => {
|
|
586
|
+
currentSession = session;
|
|
587
|
+
this._activeReviewerSessions.add(session);
|
|
588
|
+
controller.signal.addEventListener("abort", onAbort);
|
|
589
|
+
},
|
|
590
|
+
unregister: (session) => {
|
|
591
|
+
this._activeReviewerSessions.delete(session);
|
|
592
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
593
|
+
currentSession = null;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
let runResult;
|
|
597
|
+
try {
|
|
598
|
+
runResult = await this._runAiWith(reviewer.tool, reviewer.model, reviewer.effort, prompt, null, callbacks);
|
|
601
599
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
600
|
+
catch (e) {
|
|
601
|
+
if (this._cancelledReviewers.has(idx)) {
|
|
602
|
+
return "cancelled";
|
|
603
|
+
}
|
|
604
|
+
throw e;
|
|
605
|
+
}
|
|
606
|
+
const { result, capturedOutput } = runResult;
|
|
607
|
+
this._taskTokens.it += result.inputTokens;
|
|
608
|
+
this._taskTokens.ot += result.outputTokens;
|
|
609
|
+
await this._persistMetrics(plan, task.line);
|
|
610
|
+
this._updateMetrics(plan);
|
|
611
|
+
aggregateOutput.push(capturedOutput);
|
|
612
|
+
if (!await this._workspace.reviewerErrorLogExists(reviewerNum)) {
|
|
613
|
+
await this._writeLog(ws.reviewerOutputLog(iteration, reviewerNum), aggregateOutput.join("\n---\n"));
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const trimmed = (await this._workspace.readReviewerErrorLog(reviewerNum)).trim();
|
|
617
|
+
this._setReviewerState(idx, trimmed.length === 0 ? "ok" : "fail");
|
|
618
|
+
this._setReviewerLogicalStatus(idx, "done");
|
|
619
|
+
const verdictLine = trimmed.length === 0
|
|
620
|
+
? "Verdict: PASS"
|
|
621
|
+
: `Verdict: FAIL ${trimmed}`;
|
|
622
|
+
await this._writeLog(ws.reviewerOutputLog(iteration, reviewerNum), `${aggregateOutput.join("\n---\n")}\n\n${verdictLine}`);
|
|
623
|
+
return "verdict";
|
|
614
624
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
? "Verdict: PASS"
|
|
619
|
-
: `Verdict: FAIL ${trimmed}`;
|
|
620
|
-
await this._writeLog(ws.reviewerOutputLog(iteration, reviewerNum), `${aggregateOutput.join("\n---\n")}\n\n${verdictLine}`);
|
|
621
|
-
return;
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
this._reviewerAbortControllers.delete(idx);
|
|
622
628
|
}
|
|
623
629
|
}
|
|
624
630
|
_formatPathList(items) {
|
|
@@ -657,7 +663,7 @@ class Implement {
|
|
|
657
663
|
}
|
|
658
664
|
if (this._disposed)
|
|
659
665
|
return;
|
|
660
|
-
this._block.setFooter({ kind:
|
|
666
|
+
this._block.setFooter({ kind: "working" });
|
|
661
667
|
},
|
|
662
668
|
register: (session) => {
|
|
663
669
|
this._activeSession = { session };
|
|
@@ -670,10 +676,10 @@ class Implement {
|
|
|
670
676
|
}
|
|
671
677
|
};
|
|
672
678
|
}
|
|
673
|
-
_runAi(tool, model, effort, prompt, initialSessionId
|
|
674
|
-
return this._runAiWith(tool, model, effort, prompt, initialSessionId !== null && initialSessionId !== void 0 ? initialSessionId : null,
|
|
679
|
+
_runAi(tool, model, effort, prompt, initialSessionId) {
|
|
680
|
+
return this._runAiWith(tool, model, effort, prompt, initialSessionId !== null && initialSessionId !== void 0 ? initialSessionId : null, this._defaultRunAiCallbacks());
|
|
675
681
|
}
|
|
676
|
-
async _runAiWith(tool, model, effort, prompt, initialSessionId,
|
|
682
|
+
async _runAiWith(tool, model, effort, prompt, initialSessionId, callbacks) {
|
|
677
683
|
if (this._disposed) {
|
|
678
684
|
throw new Error("Implement disposed");
|
|
679
685
|
}
|
|
@@ -698,7 +704,6 @@ class Implement {
|
|
|
698
704
|
model,
|
|
699
705
|
effort,
|
|
700
706
|
...(initialSessionId != null ? { resumeSessionId: initialSessionId } : null),
|
|
701
|
-
...(forkFromSessionId != null ? { forkParentSessionId: forkFromSessionId } : null),
|
|
702
707
|
onLongWaitStart: callbacks.onLongWaitStart,
|
|
703
708
|
onLongWaitEnd: callbacks.onLongWaitEnd
|
|
704
709
|
}, {
|
|
@@ -779,6 +784,10 @@ class Implement {
|
|
|
779
784
|
return;
|
|
780
785
|
}
|
|
781
786
|
this._disposed = true;
|
|
787
|
+
for (const controller of this._reviewerAbortControllers.values()) {
|
|
788
|
+
controller.abort();
|
|
789
|
+
}
|
|
790
|
+
this._reviewerAbortControllers.clear();
|
|
782
791
|
const activeSession = (_a = this._activeSession) === null || _a === void 0 ? void 0 : _a.session;
|
|
783
792
|
const activeScript = (_b = this._activeScript) === null || _b === void 0 ? void 0 : _b.script;
|
|
784
793
|
const reviewerSessions = [...this._activeReviewerSessions];
|
|
@@ -23,6 +23,8 @@ export type ResolvedAnswers = Readonly<{
|
|
|
23
23
|
workerModel?: string;
|
|
24
24
|
workerEffort?: string;
|
|
25
25
|
reviewers?: readonly ReviewerFlagAnswers[];
|
|
26
|
+
optionalReviewerIndices?: readonly number[];
|
|
27
|
+
reviewerMinimum?: number;
|
|
26
28
|
}>;
|
|
27
29
|
export declare function parseInstallFlags(rawArgs: readonly string[]): Readonly<{
|
|
28
30
|
ok: true;
|