flanders 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/lib/ai/AiRunner.d.ts +24 -0
  2. package/lib/ai/AiRunner.js +100 -0
  3. package/lib/ai/AiSession.d.ts +32 -0
  4. package/lib/ai/AiSession.js +143 -0
  5. package/lib/ai/ClaudeAdapter.d.ts +12 -0
  6. package/lib/ai/ClaudeAdapter.js +345 -0
  7. package/lib/ai/CodexAdapter.d.ts +13 -0
  8. package/lib/ai/CodexAdapter.js +354 -0
  9. package/lib/ai/ToolAdapter.d.ts +52 -0
  10. package/lib/ai/ToolAdapter.js +2 -0
  11. package/lib/cli.js +56 -35
  12. package/lib/commands/Implement.d.ts +22 -8
  13. package/lib/commands/Implement.js +296 -171
  14. package/lib/commands/Install.d.ts +31 -3
  15. package/lib/commands/Install.js +625 -49
  16. package/lib/commands/InstallModelProbe.d.ts +11 -0
  17. package/lib/commands/InstallModelProbe.js +99 -0
  18. package/lib/contexts.d.ts +5 -4
  19. package/lib/index.d.ts +5 -3
  20. package/lib/{PlanFile.d.ts → plan/PlanFile.d.ts} +8 -1
  21. package/lib/{PlanFile.js → plan/PlanFile.js} +44 -5
  22. package/lib/prompts/prompts.d.ts +48 -0
  23. package/lib/prompts/prompts.js +328 -0
  24. package/lib/prompts/skills.d.ts +3 -0
  25. package/lib/prompts/skills.js +464 -0
  26. package/lib/{Git.d.ts → system/Git.d.ts} +4 -2
  27. package/lib/{Git.js → system/Git.js} +63 -3
  28. package/lib/{ScriptRunner.d.ts → system/ScriptRunner.d.ts} +1 -1
  29. package/lib/system/ShellScriptContext.d.ts +36 -0
  30. package/lib/system/ShellScriptContext.js +70 -0
  31. package/lib/{fsUtils.d.ts → system/fsUtils.d.ts} +1 -1
  32. package/lib/{wait.d.ts → system/wait.d.ts} +1 -1
  33. package/lib/ui/BottomBlock.d.ts +9 -1
  34. package/lib/ui/BottomBlock.js +30 -14
  35. package/lib/ui/PromptHelper.d.ts +12 -0
  36. package/lib/ui/PromptHelper.js +32 -0
  37. package/lib/ui/TerminalSizeSource.d.ts +19 -0
  38. package/lib/ui/TerminalSizeSource.js +77 -0
  39. package/lib/ui/formatters.d.ts +14 -0
  40. package/lib/ui/formatters.js +65 -2
  41. package/lib/workspace/FlandersConfig.d.ts +23 -0
  42. package/lib/workspace/FlandersConfig.js +90 -0
  43. package/lib/workspace/SpecDiscovery.d.ts +12 -0
  44. package/lib/workspace/SpecDiscovery.js +40 -0
  45. package/lib/{Workspace.d.ts → workspace/Workspace.d.ts} +10 -2
  46. package/lib/{Workspace.js → workspace/Workspace.js} +52 -6
  47. package/package.json +3 -2
  48. package/lib/Claude.d.ts +0 -129
  49. package/lib/Claude.js +0 -387
  50. package/lib/ClaudeSession.d.ts +0 -34
  51. package/lib/ClaudeSession.js +0 -292
  52. package/lib/prompts.d.ts +0 -18
  53. package/lib/prompts.js +0 -221
  54. package/lib/skills.d.ts +0 -3
  55. package/lib/skills.js +0 -267
  56. /package/lib/{ScriptRunner.js → system/ScriptRunner.js} +0 -0
  57. /package/lib/{fsUtils.js → system/fsUtils.js} +0 -0
  58. /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 ClaudeSession_1 = require("../ClaudeSession");
5
- const fsUtils_1 = require("../fsUtils");
6
- const Git_1 = require("../Git");
7
- const PlanFile_1 = require("../PlanFile");
8
- const prompts_1 = require("../prompts");
9
- const ScriptRunner_1 = require("../ScriptRunner");
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 === "--no-git") {
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
- let gitActive = false;
134
- if (!noGitFlag) {
135
- if (await (0, Git_1.isGitAvailable)(this._contexts.script, this._contexts.time)) {
136
- gitActive = await (0, Git_1.isInsideWorkTree)(this._contexts.script, this._contexts.time, this._options.projectRoot);
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
- if (gitActive) {
140
- const pending = await (0, Git_1.countPendingChangesExcept)(this._contexts.script, this._contexts.time, this._options.projectRoot, planPath);
141
- if (pending > 0) {
142
- this._buffered.writeError("Working tree has uncommitted changes. Please commit or stash them before re-running.\n");
143
- this._finalizeBlock("Failed");
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 contractFiles = await (0, fsUtils_1.listFilesRecursive)(this._contexts.fs, (0, fsUtils_1.joinPath)(this._options.projectRoot, "contracts"));
148
- this._contractList = contractFiles.map(f => `contracts/${f}`);
149
- const ruleFiles = await (0, fsUtils_1.listFilesRecursive)(this._contexts.fs, (0, fsUtils_1.joinPath)(this._options.projectRoot, "rules"));
150
- this._ruleList = ruleFiles.map(f => `rules/${f}`);
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, gitActive);
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
- return await this._promptPlanChoice(plansFolder, files);
216
- }
217
- _askOutput() {
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._runClaude(prompt);
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
- async _runTask(plan, task, ws, indexLabel, taskIndex, gitActive) {
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 prepOk = await this._prepStage(plan, task, ws, taskIndex);
296
- if (this._disposed) {
297
- return false;
298
- }
299
- if (!prepOk) {
300
- return false;
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
- if (gitActive) {
348
- const commitMessage = task.taskNumber ? `${task.taskNumber} ${task.title}` : task.title;
349
- const gitOutput = {
350
- write: text => this._buffered.write(text),
351
- writeError: text => this._buffered.writeError(text),
352
- columns: () => this._contexts.output.columns(),
353
- rows: () => this._contexts.output.rows(),
354
- onResize: listener => this._contexts.output.onResize(listener)
355
- };
356
- const addResult = await (0, Git_1.addAll)(this._contexts.script, this._contexts.time, gitOutput, this._options.projectRoot);
357
- if (addResult.code !== 0) {
358
- await this._writeErrorLog(ws, `git add -A failed (exit ${addResult.code})\n--- stdout ---\n${addResult.stdout}\n--- stderr ---\n${addResult.stderr}`);
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._runClaude(prompt);
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
- ? await this._runClaude(prompt, null, this._currentPrepSessionId)
429
- : await this._runClaude(prompt, this._currentWorkerSessionId);
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
- async _reviewerStage(plan, task, ws, iteration) {
482
- if (this._currentPrepSessionId === null) {
483
- throw new Error(`Reviewer for task at line ${task.line} ("${task.title}") requires a prep session id but none was captured`);
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 prompt = prompts_1.prompts.reviewer
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
- try {
492
- const { result, capturedOutput } = await this._runClaude(prompt, null, this._currentPrepSessionId);
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
- const verdict = this._parseReviewVerdict(result.text);
498
- await this._writeLog(ws.reviewerLog(iteration), verdict.pass
499
- ? `${capturedOutput}\n\nVerdict: PASS`
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
- if (/^pass$/i.test(line)) {
523
- return { pass: true, reason: "" };
524
- }
525
- const failMatch = /^fail\b\s*(.*)$/i.exec(line);
526
- if (failMatch) {
527
- return { pass: false, reason: failMatch[1].trim() };
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
- async _runClaude(prompt, initialSessionId, forkFromSessionId) {
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 session = new ClaudeSession_1.ClaudeSession({
694
+ const adapter = this._getAdapter(tool);
695
+ const session = new AiSession_1.AiSession({
696
+ adapter,
558
697
  prompt,
559
- cwd: this._options.projectRoot,
560
- ...(initialSessionId != null ? { initialSessionId } : null),
561
- ...(forkFromSessionId != null ? { forkFromSessionId } : null),
562
- onLongWaitStart: (kind, endTimeMs) => {
563
- if (this._disposed)
564
- return;
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
- const running = { session };
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
- if (this._activeSession === running) {
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;