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.
@@ -13,7 +13,6 @@ export type RunArgs = Readonly<{
13
13
  model: string;
14
14
  effort: string;
15
15
  resumeSessionId?: string;
16
- forkParentSessionId?: string;
17
16
  abortSignal: AbortSignal;
18
17
  callbacks: RunCallbacks;
19
18
  time: TimeContext;
@@ -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.forkParentSessionId) {
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 {
@@ -12,7 +12,6 @@ export type AiSessionOptions = Readonly<{
12
12
  model: string;
13
13
  effort: string;
14
14
  resumeSessionId?: string | null;
15
- forkParentSessionId?: string | null;
16
15
  onLongWaitStart?(kind: "rate-limit", endTimeMs: number): void;
17
16
  onLongWaitEnd?(): void;
18
17
  }>;
@@ -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,
@@ -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
  }
@@ -98,15 +98,14 @@ class CodexAdapterIterator {
98
98
  this._sawTurnCompleted = false;
99
99
  this._receivedAnyEvent = false;
100
100
  this._fallbackAttempted = false;
101
- this._usedResumeOrFork = false;
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
- const isFork = !useExecFallback && !!this._args.forkParentSessionId;
108
- this._usedResumeOrFork = isResume || isFork;
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._usedResumeOrFork && !this._receivedAnyEvent && !this._fallbackAttempted) {
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/fork unavailable in installed CLI"
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._usedResumeOrFork = false;
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, isFork) {
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
  }
@@ -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 ToolAdapterInvokeArgsFork = ToolAdapterInvokeArgsBase & Readonly<{
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, completedSoFar);
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, iteration) {
245
+ _setActivity(activity) {
245
246
  if (!this._currentTask)
246
247
  return;
247
248
  this._block.setHeader({
248
249
  indexLabel: this._currentIndexLabel,
249
- iteration: iteration === undefined ? this._currentIteration : 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, taskIndex) {
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, prepActive);
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, prepActive);
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 _prepStage(plan, task, ws, taskIndex) {
388
- const prompt = prompts_1.prompts.prep
389
- .split("<PLAN_PATH>").join(plan.path)
390
- .split("<TASK_LINE>").join(String(task.line))
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
- catch (e) {
412
- await this._persistMetrics(plan, task.line);
413
- this._updateMetrics(plan);
414
- this._workspace.preserveOnDispose();
415
- await this._writeErrorLog(ws, `prep stage failed: ${this._stringifyError(e)}`);
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("<TASK_LINE>").join(String(task.line))
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
- ? 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)
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
- 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
- }
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
- async _reviewerStage(plan, task, ws, iteration, prepActive) {
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 launches = reviewers.map((reviewer, idx) => this._runOneReviewerToVerdict(plan, task, ws, iteration, prepActive, reviewer, idx).catch(e => {
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
- perFile.push(await this._workspace.readReviewerErrorLog(i + 1));
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, prepActive, reviewer, idx) {
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("<TASK_LINE>").join(String(task.line))
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
- 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)
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
- this._setReviewerState(idx, "running");
595
- },
596
- register: (session) => {
597
- this._activeReviewerSessions.add(session);
598
- },
599
- unregister: (session) => {
600
- this._activeReviewerSessions.delete(session);
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
- 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);
606
- this._taskTokens.it += result.inputTokens;
607
- this._taskTokens.ot += result.outputTokens;
608
- await this._persistMetrics(plan, task.line);
609
- this._updateMetrics(plan);
610
- aggregateOutput.push(capturedOutput);
611
- if (!await this._workspace.reviewerErrorLogExists(reviewerNum)) {
612
- await this._writeLog(ws.reviewerOutputLog(iteration, reviewerNum), aggregateOutput.join("\n---\n"));
613
- continue;
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
- 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;
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: this._restingFooterKind });
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, 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());
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, forkFromSessionId, callbacks) {
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;