@magnusekdahl/parallix 1.3.2 → 1.3.4

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 (50) hide show
  1. package/docs/agents.md +1 -1
  2. package/docs/use-cases.md +1 -1
  3. package/lib/agents/agents.js +20 -3
  4. package/lib/agents/agents.ts +14 -3
  5. package/lib/agents/mistral-telemetry.js +122 -23
  6. package/lib/agents/mistral-telemetry.ts +141 -26
  7. package/lib/agents/mistral.js +128 -14
  8. package/lib/agents/mistral.ts +145 -5
  9. package/lib/commands/active.js +69 -39
  10. package/lib/commands/active.ts +97 -60
  11. package/lib/commands/config.ts +3 -3
  12. package/lib/commands/coverage-gate.ts +1 -1
  13. package/lib/commands/draft.js +1 -1
  14. package/lib/commands/draft.ts +1 -1
  15. package/lib/commands/handoff.js +13 -8
  16. package/lib/commands/handoff.ts +24 -18
  17. package/lib/commands/rebase.js +1 -1
  18. package/lib/commands/rebase.ts +2 -2
  19. package/lib/commands/repair-handoff.js +141 -20
  20. package/lib/commands/repair-handoff.ts +185 -45
  21. package/lib/commands/resolve-conflict.js +1 -1
  22. package/lib/commands/resolve-conflict.ts +2 -2
  23. package/lib/commands/stats-backfill.ts +10 -10
  24. package/lib/commands/stats.js +38 -11
  25. package/lib/commands/stats.ts +40 -96
  26. package/lib/core/fmt.ts +2 -2
  27. package/lib/core/git.ts +2 -2
  28. package/lib/core/gitignore.ts +2 -2
  29. package/lib/core/mission-utils.js +2 -2
  30. package/lib/core/mission-utils.ts +2 -2
  31. package/lib/core/persistent-data-migration.ts +2 -2
  32. package/lib/core/spawn-tee.ts +1 -1
  33. package/lib/core/state-map.ts +2 -2
  34. package/lib/core/storage.ts +1 -1
  35. package/lib/core/verification.ts +1 -1
  36. package/lib/review/rebase.ts +12 -12
  37. package/lib/review/review-artifacts.ts +35 -35
  38. package/lib/review/review-commands.ts +40 -40
  39. package/lib/review/review-events.ts +12 -12
  40. package/lib/review/review-loop.js +236 -7
  41. package/lib/review/review-loop.ts +338 -22
  42. package/lib/review/review-polling.ts +6 -6
  43. package/lib/review/review-prompts.js +8 -4
  44. package/lib/review/review-prompts.ts +12 -8
  45. package/lib/review/review-state.js +1 -1
  46. package/lib/review/review-state.ts +2 -2
  47. package/package.json +3 -2
  48. package/prompts/review-verbose.md +1 -1
  49. package/prompts/review.md +1 -1
  50. package/px.js +8 -4
@@ -8,7 +8,8 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as fmt from '../core/fmt.js';
10
10
  import { git, run } from '../core/git.js';
11
- import { findMissionDir, resolveWorktree, missionBranchName } from '../core/mission-utils.js';
11
+ import { findMissionDir, findMissionArea, resolveWorktree, missionBranchName, getPrimaryBranch } from '../core/mission-utils.js';
12
+ import { formatVerificationCommand } from '../core/verification.js';
12
13
  import { resolveTaskFile, getTaskImplementer, getTaskStatus, enforceTaskAssignee, transitionTask, reportTaskResolution } from '../tools/backlog.js';
13
14
  import { toVirtual, transitionVirtual } from '../core/state-map.js';
14
15
  import { getPrStatus, readToken, getLatestReviewForPr, getLatestDispositionForPr, providerAvailable, getComments, postComment, postReview, resolveReviewUser, isProviderEnabled } from './review-adapter.js';
@@ -98,8 +99,8 @@ export function recordStageStatsSafe(
98
99
  reviewer?: string;
99
100
  result?: Record<string, any>;
100
101
  sinceMs?: number;
101
- log?: (msg: string) => void;
102
- error?: (msg: string) => void;
102
+ log?: (_msg: string) => void;
103
+ error?: (_msg: string) => void;
103
104
  state?: Record<string, any>;
104
105
  writeReviewStateFn?: typeof writeReviewState;
105
106
  model?: string | null;
@@ -149,7 +150,7 @@ const _require = createRequire(__filename);
149
150
 
150
151
  export function maybeUpdateGraphifyBeforeReview(
151
152
  rootDir: string,
152
- { commandRunner = run, log = fmt.log.plain }: { commandRunner?: typeof run; log?: (msg: string) => void } = {}
153
+ { commandRunner = run, log = fmt.log.plain }: { commandRunner?: typeof run; log?: (_msg: string) => void } = {}
153
154
  ): unknown {
154
155
  // Lazy require to break circular dependency with core/mission-utils
155
156
  const { updateGraphifyKnowledgeGraph } = _require('../core/mission-utils.js');
@@ -180,7 +181,7 @@ export function applyAgentFallback(opts: {
180
181
  slug: string;
181
182
  worktree?: string;
182
183
  taskResolution?: Record<string, any>;
183
- log?: (msg: string) => void;
184
+ log?: (_msg: string) => void;
184
185
  writeReviewStateFn?: typeof writeReviewState;
185
186
  enforceTaskAssigneeFn?: typeof enforceTaskAssignee;
186
187
  }): string {
@@ -208,7 +209,7 @@ export function persistNormalizedPhaseRepair(
208
209
  slug: string,
209
210
  state: ReviewState,
210
211
  worktree: string,
211
- { log = fmt.log.plain, writeReviewStateFn = writeReviewState }: { log?: (msg: string) => void; writeReviewStateFn?: typeof writeReviewState } = {}
212
+ { log = fmt.log.plain, writeReviewStateFn = writeReviewState }: { log?: (_msg: string) => void; writeReviewStateFn?: typeof writeReviewState } = {}
212
213
  ): void {
213
214
  if (!state || !state.phaseOriginal || (VALID_PHASES as readonly string[]).includes(state.phaseOriginal)) {
214
215
  return;
@@ -237,6 +238,245 @@ export function stageLaunchSinceMs(result: { startedAt?: string } | null | undef
237
238
  return Number.isFinite(startedMs) ? startedMs : 0;
238
239
  }
239
240
 
241
+ // ============================================================================
242
+ // Pre-review Gate Enforcement (ADR 0048 Control C1 / TASK-1385)
243
+ // ============================================================================
244
+
245
+ // Minimal error classifier stub for gate failures (TASK-1389 dependency).
246
+ // Maps gate failures to Class 6: genuine gate failure — code issue, dispatch: auto-send-back.
247
+ // TASK-1389 will replace this with the full 8-class dispatch table.
248
+ const GATE_FAILURE_CLASS = 'class-6-genuine-gate-failure';
249
+ const GATE_FAILURE_ACTION = 'auto-send-back';
250
+
251
+ export function classifyGateFailure(_output: string): { classification: string; action: string; isRelaunchable: boolean } {
252
+ // Gate failures are genuine code issues — always relaunchable via auto-send-back.
253
+ // The full classifier (TASK-1389) will expand this to 8 classes with nuanced dispatch.
254
+ return {
255
+ classification: GATE_FAILURE_CLASS,
256
+ action: GATE_FAILURE_ACTION,
257
+ isRelaunchable: true,
258
+ };
259
+ }
260
+
261
+ export interface PreReviewGateResult {
262
+ ok: boolean;
263
+ area: string;
264
+ command: string;
265
+ exitCode: number | null;
266
+ stdout: string;
267
+ stderr: string;
268
+ error?: string;
269
+ }
270
+
271
+ /**
272
+ * Run the verification gate with the mission area before a review round.
273
+ * Captures stdout/stderr for use in auto-bounce fix prompts.
274
+ */
275
+ export async function runPreReviewGate(
276
+ slug: string,
277
+ worktree: string,
278
+ opts: {
279
+ findMissionAreaFn?: typeof findMissionArea;
280
+ runFn?: typeof run;
281
+ log?: (_msg: string) => void;
282
+ error?: (_msg: string) => void;
283
+ } = {}
284
+ ): Promise<PreReviewGateResult> {
285
+ const {
286
+ findMissionAreaFn = findMissionArea,
287
+ runFn: runFnOverride = run,
288
+ log = fmt.log.plain,
289
+ error = fmt.log.plainError,
290
+ } = opts;
291
+
292
+ const missionDir = findMissionDir(slug, worktree);
293
+ const area = missionDir ? findMissionAreaFn(missionDir) : 'docs';
294
+ const command = formatVerificationCommand(area, worktree);
295
+
296
+ if (command === NO_GATE_NOTICE_ALIAS) {
297
+ log(fmt.status('INFO', `No verification gate configured for area ${area}; skipping pre-review gate check.`));
298
+ return { ok: true, area, command, exitCode: 0, stdout: '', stderr: '' };
299
+ }
300
+
301
+ log(fmt.status('INFO', `Pre-review gate for area "${area}": ${fmt.command(command)}`));
302
+
303
+ // Run with pipe mode to capture output for auto-bounce fix prompts.
304
+ const result = runFnOverride('bash', ['-lc', command], {
305
+ cwd: worktree,
306
+ stdio: 'pipe',
307
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
308
+ });
309
+
310
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
311
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
312
+
313
+ if (result.status !== 0) {
314
+ error(fmt.status('FAIL', `Pre-review gate failed for area "${area}" (exit ${result.status}).`));
315
+ if (stderr) {
316
+ error(` stderr: ${stderr.split('\n').slice(0, 10).join('\n ')}`);
317
+ }
318
+ return {
319
+ ok: false,
320
+ area,
321
+ command,
322
+ exitCode: result.status,
323
+ stdout,
324
+ stderr,
325
+ error: `verification gate failed with exit code ${result.status}`,
326
+ };
327
+ }
328
+
329
+ log(fmt.status('PASS', `Pre-review gate passed for area "${area}".`));
330
+ return { ok: true, area, command, exitCode: 0, stdout, stderr };
331
+ }
332
+
333
+ const NO_GATE_NOTICE_ALIAS = ': # no verification gate configured (set adapters.verification.command)';
334
+
335
+ /**
336
+ * Handle a pre-review gate failure by auto-bouncing to the implementer.
337
+ * Does NOT consume a reviewer cycle or transition the task out of review status.
338
+ * Tracks retry count in review state metadata.
339
+ * Returns true if bounced, false if retry limit exceeded (mission strands).
340
+ */
341
+ export async function handleGateFailureAutoBounce(
342
+ slug: string,
343
+ worktree: string,
344
+ gateResult: PreReviewGateResult,
345
+ implementer: string,
346
+ opts: {
347
+ startAgentFn?: typeof startAgent;
348
+ writeReviewStateFn?: typeof writeReviewState;
349
+ readReviewStateFn?: typeof readReviewState;
350
+ transitionTaskFn?: typeof transitionTask;
351
+ applyAgentFallbackFn?: typeof applyAgentFallback;
352
+ taskResolution?: { ok: boolean; taskFile?: string };
353
+ enforceTaskAssigneeFn?: typeof enforceTaskAssignee;
354
+ log?: (_msg: string) => void;
355
+ error?: (_msg: string) => void;
356
+ sleepFn?: typeof delay;
357
+ buildCompactActOnReviewPromptFn?: typeof buildCompactActOnReviewPrompt;
358
+ isForgejoReviewEnabledFn?: ((_rootDir?: string) => boolean) | null;
359
+ isReviewProviderEnabledFn?: ((_rootDir?: string) => boolean) | null;
360
+ legacyIsForgejoReviewEnabledFn?: ((_rootDir?: string) => boolean) | null;
361
+ exit?: (_code: number) => never;
362
+ } = {}
363
+ ): Promise<{ bounced: boolean; stranded: boolean }> {
364
+ const {
365
+ startAgentFn = startAgent,
366
+ writeReviewStateFn = writeReviewState,
367
+ readReviewStateFn = readReviewState,
368
+ transitionTaskFn = transitionTask,
369
+ applyAgentFallbackFn = applyAgentFallback,
370
+ taskResolution,
371
+ enforceTaskAssigneeFn,
372
+ log = fmt.log.plain,
373
+ error = fmt.log.plainError,
374
+ sleepFn: _sleepFn = delay,
375
+ buildCompactActOnReviewPromptFn: _buildCompactActOnReviewPromptFn = buildCompactActOnReviewPrompt,
376
+ isForgejoReviewEnabledFn: _isForgejoReviewEnabledFn,
377
+ isReviewProviderEnabledFn: _isReviewProviderEnabledFn,
378
+ legacyIsForgejoReviewEnabledFn: _legacyIsForgejoReviewEnabledFn,
379
+ exit: _exit = process.exit,
380
+ } = opts;
381
+
382
+ const MAX_GATE_RETRY = 2;
383
+
384
+ // Read persisted state to get current retry count
385
+ const persisted = readReviewStateFn(slug, worktree);
386
+ const retryCount = persisted && persisted.metadata && typeof persisted.metadata === 'object'
387
+ ? (Number((persisted.metadata as any).gateFailureRetryCount) || 0)
388
+ : 0;
389
+
390
+ if (retryCount >= MAX_GATE_RETRY) {
391
+ error(fmt.status('FAIL', `Pre-review gate failure: max retries exceeded (${MAX_GATE_RETRY}). Mission stranded for ${slug}.`));
392
+ error(fmt.status('FAIL', `Area "${gateResult.area}" verification failed ${retryCount} times. Human intervention required.`));
393
+ error(fmt.status('FAIL', `Gate output:\n${gateResult.stdout || gateResult.stderr || '(no output)'}\n`));
394
+ return { bounced: false, stranded: true };
395
+ }
396
+
397
+ // Classify the failure
398
+ const combinedOutput = gateResult.stderr || gateResult.stdout || 'Gate failed with exit code ' + gateResult.exitCode;
399
+ const classification = classifyGateFailure(combinedOutput);
400
+
401
+ log(fmt.status('WARN', `Pre-review gate failed for area "${gateResult.area}" (exit ${gateResult.exitCode}). Classification: ${classification.classification}.`));
402
+
403
+ // Build fix prompt with captured gate output
404
+ const fixPrompt = [
405
+ `PRE-REVIEW GATE FAILURE — FIX REQUIRED`,
406
+ ``,
407
+ `Mission: ${slug}`,
408
+ `Area: ${gateResult.area}`,
409
+ `Gate command: ${gateResult.command}`,
410
+ `Exit code: ${gateResult.exitCode}`,
411
+ ``,
412
+ `Gate output (use this to diagnose and fix):`,
413
+ `---`,
414
+ gateResult.stdout || '(no stdout)',
415
+ `---`,
416
+ gateResult.stderr || '(no stderr)',
417
+ `---`,
418
+ ``,
419
+ `Classification: ${classification.classification} — ${classification.action}`,
420
+ `Retry attempt: ${retryCount + 1}/${MAX_GATE_RETRY}`,
421
+ ``,
422
+ `Fix the underlying issue so the verification gate passes for area "${gateResult.area}".`,
423
+ `After fixing, the review loop will re-run the gate before the next review round.`,
424
+ ].join('\n');
425
+
426
+ // Increment retry count in metadata
427
+ if (!persisted || !persisted.metadata || typeof persisted.metadata !== 'object') {
428
+ // Create new metadata
429
+ }
430
+ const metadata = persisted && persisted.metadata && typeof persisted.metadata === 'object'
431
+ ? { ...persisted.metadata }
432
+ : {};
433
+ metadata.gateFailureRetryCount = retryCount + 1;
434
+
435
+ // Update review state with incremented retry count
436
+ if (persisted) {
437
+ const updatedState = { ...persisted, metadata };
438
+ writeReviewStateFn(slug, updatedState as any, worktree);
439
+ } else {
440
+ writeReviewStateFn(slug, { metadata } as any, worktree);
441
+ }
442
+
443
+ // Transition task back to active (implementer phase) without consuming reviewer cycle
444
+ transitionTaskFn(slug, 'active', { rootDir: worktree, log });
445
+ log(fmt.status('INFO', `Auto-bouncing to implementer (${implementer}) with fix prompt. Retry ${retryCount + 1}/${MAX_GATE_RETRY}.`));
446
+
447
+ // Launch implementer with the fix prompt
448
+ try {
449
+ const launchResult = await startAgentFn('act-on-review', {
450
+ agent: implementer,
451
+ prompt: (_actualImplementer: string) => fixPrompt,
452
+ worktree,
453
+ slug,
454
+ role: 'implementer',
455
+ exclude: [],
456
+ });
457
+
458
+ // Apply any agent fallback if needed
459
+ implementer = applyAgentFallbackFn({
460
+ role: 'implementer',
461
+ original: implementer,
462
+ launchResult,
463
+ state: persisted || {},
464
+ slug,
465
+ worktree,
466
+ taskResolution,
467
+ log,
468
+ writeReviewStateFn,
469
+ enforceTaskAssigneeFn,
470
+ });
471
+ } catch (err: unknown) {
472
+ error(fmt.status('FAIL', `Could not relaunch implementer (${implementer}) for gate failure auto-bounce: ${(err as Error).message}`));
473
+ return { bounced: false, stranded: true };
474
+ }
475
+
476
+ log(fmt.status('INFO', `Implementer (${implementer}) relaunched with gate failure fix prompt.`));
477
+ return { bounced: true, stranded: false };
478
+ }
479
+
240
480
  // ============================================================================
241
481
  // Main Review Loop
242
482
  // ============================================================================
@@ -267,12 +507,12 @@ export async function startReviewLoop(slug: string, opts: {
267
507
  buildAutonomousReviewMatrixFn?: typeof buildAutonomousReviewMatrix;
268
508
  formatMatrixSummaryFn?: typeof formatMatrixSummary;
269
509
  selectAgentFn?: typeof selectAgent;
270
- providerAvailableFn?: ((url: string) => Promise<boolean>) | null | undefined;
510
+ providerAvailableFn?: ((_url: string) => Promise<boolean>) | null | undefined;
271
511
  runFn?: typeof run;
272
512
  getPrStatusFn?: typeof getPrStatus;
273
513
  enforceTaskAssigneeFn?: typeof enforceTaskAssignee;
274
514
  resolveReviewUserFn?: (() => string) | null | undefined;
275
- forgejoAvailableFn?: ((url: string) => Promise<boolean>) | null;
515
+ forgejoAvailableFn?: ((_url: string) => Promise<boolean>) | null;
276
516
  resolveForgejoUserFn?: (() => string) | null;
277
517
  readTokenFn?: typeof readToken;
278
518
  getCommentsFn?: typeof getComments;
@@ -291,18 +531,20 @@ export async function startReviewLoop(slug: string, opts: {
291
531
  consumeImplementerArtifactsFn?: typeof consumeImplementerArtifacts;
292
532
  rebaseBeforeReviewRoundFn?: typeof rebaseBeforeReviewRound;
293
533
  eligibleAgentsForStepFn?: typeof eligibleAgentsForStep;
294
- performHandoffFn?: (slug: string, opts?: Record<string, unknown>) => Promise<Record<string, unknown>>;
295
- log?: (msg: string) => void;
296
- error?: (msg: string) => void;
534
+ performHandoffFn?: (_slug: string, _opts?: Record<string, unknown>) => Promise<Record<string, unknown>>;
535
+ log?: (_msg: string) => void;
536
+ error?: (_msg: string) => void;
297
537
  getLatestReviewForPrFn?: typeof getLatestReviewForPr;
298
538
  getLatestDispositionForPrFn?: typeof getLatestDispositionForPr;
299
539
  sleepFn?: typeof delay;
300
- exit?: (code: number) => never;
540
+ exit?: (_code: number) => never;
301
541
  gitFn?: typeof git;
302
- isReviewProviderEnabledFn?: ((rootDir?: string) => boolean) | null | undefined;
303
- legacyIsForgejoReviewEnabledFn?: ((rootDir?: string) => boolean) | null;
304
- isForgejoReviewEnabledFn?: ((rootDir?: string) => boolean) | null;
305
- recordStageStatsSafeFn?: (...args: any[]) => void;
542
+ isReviewProviderEnabledFn?: ((_rootDir?: string) => boolean) | null | undefined;
543
+ legacyIsForgejoReviewEnabledFn?: ((_rootDir?: string) => boolean) | null;
544
+ isForgejoReviewEnabledFn?: ((_rootDir?: string) => boolean) | null;
545
+ recordStageStatsSafeFn?: (..._args: any[]) => void;
546
+ runPreReviewGateFn?: typeof runPreReviewGate;
547
+ handleGateFailureAutoBounceFn?: typeof handleGateFailureAutoBounce;
306
548
  } = {}): Promise<void> {
307
549
  let {
308
550
  implementer,
@@ -317,6 +559,8 @@ export async function startReviewLoop(slug: string, opts: {
317
559
  pollTimeoutSeconds = null,
318
560
  worktree: callerWorktree,
319
561
  missionPath,
562
+ runPreReviewGateFn = runPreReviewGate,
563
+ handleGateFailureAutoBounceFn = handleGateFailureAutoBounce,
320
564
  resetReviewStateFn = resetReviewState,
321
565
  maybeUpdateGraphifyBeforeReviewFn = maybeUpdateGraphifyBeforeReview,
322
566
  readReviewStateFn = readReviewState,
@@ -741,6 +985,30 @@ export async function startReviewLoop(slug: string, opts: {
741
985
  state.advanceRound();
742
986
  }
743
987
 
988
+ // Snapshot the primary branch's HEAD commit for this round. This is a
989
+ // fallback value only, used for paths that don't rebase this round (a
990
+ // dry run, or resuming with an existing reviewState): once
991
+ // rebaseBeforeReviewRoundFn runs below, HEAD is rebased onto primary's
992
+ // *current* tip, so the baseline is re-captured immediately after the
993
+ // rebase completes (see below). Capturing it here, before the rebase,
994
+ // would pin the diff to a stale pre-rebase SHA; since rebase replays
995
+ // primary's newer commits into HEAD's ancestry, diffing against that
996
+ // stale SHA would surface exactly the "not rebased to main" noise this
997
+ // baseline exists to suppress (task-1407).
998
+ // Falls back to undefined (letting the prompt builders resolve the live
999
+ // primary branch ref themselves) if the primary branch cannot be detected,
1000
+ // e.g. in a repo with no main/master branch yet.
1001
+ const captureReviewBaseline = (): string | undefined => {
1002
+ try {
1003
+ const primaryBranchName = getPrimaryBranch(worktree, gitFn);
1004
+ const reviewBaselineResult = gitFn(['-C', worktree, 'rev-parse', primaryBranchName]);
1005
+ return (reviewBaselineResult.stdout || '').trim() || primaryBranchName;
1006
+ } catch {
1007
+ return undefined;
1008
+ }
1009
+ };
1010
+ let reviewBaseline: string | undefined = captureReviewBaseline();
1011
+
744
1012
  let reviewState: unknown;
745
1013
 
746
1014
  if (state.phase === 'reviewing') {
@@ -770,7 +1038,7 @@ export async function startReviewLoop(slug: string, opts: {
770
1038
  log(fmt.status('INFO', `Round ${attempt}: reviewer identity is autonomous; skipping dry-run reviewer prompt and using local review artifacts only.`));
771
1039
  } else {
772
1040
  log(`\n--- DRY-RUN: reviewer (${reviewer}) prompt ---`);
773
- log((buildReviewPromptFn as any)({ reviewer: reviewer!, branch, implementer: implementer!, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer: '{{AGENT_NAME}}' }));
1041
+ log((buildReviewPromptFn as any)({ reviewer: reviewer!, branch, implementer: implementer!, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer: '{{AGENT_NAME}}', reviewBaseline }));
774
1042
  }
775
1043
  }
776
1044
  } else {
@@ -784,9 +1052,57 @@ export async function startReviewLoop(slug: string, opts: {
784
1052
  });
785
1053
  if (!rebaseResult.ok) { exit(1); return; }
786
1054
 
1055
+ // Re-capture after rebase: HEAD is now rebased onto primary's tip,
1056
+ // so the pre-rebase snapshot above is stale and must be replaced
1057
+ // with the SHA that HEAD was actually rebased onto (task-1407).
1058
+ reviewBaseline = captureReviewBaseline();
1059
+
787
1060
  if (!dryRun) { transitionTaskFn(slug, 'review', { rootDir: worktree, log }); }
788
1061
  state.phase = 'reviewing';
789
1062
  writeReviewStateFn(slug, state, worktree);
1063
+
1064
+ // Pre-review gate enforcement (ADR 0048 Control C1 / TASK-1385):
1065
+ // Run the verification gate before the reviewer launches. On failure,
1066
+ // auto-bounce to the implementer with captured gate output as fix prompt.
1067
+ // No reviewer cycle is consumed when the gate fails and auto-bounce occurs.
1068
+ if (!dryRun) {
1069
+ const preReviewGateResult = await runPreReviewGateFn(slug, worktree, {
1070
+ runFn: runFn as any,
1071
+ log,
1072
+ error,
1073
+ });
1074
+ if (!preReviewGateResult.ok) {
1075
+ log(fmt.status('WARN', `Pre-review gate failed for area "${preReviewGateResult.area}" (exit ${preReviewGateResult.exitCode}). Auto-bouncing to implementer.`));
1076
+ const bounceResult = await handleGateFailureAutoBounceFn(slug, worktree, preReviewGateResult, implementer, {
1077
+ startAgentFn: startAgentFn,
1078
+ writeReviewStateFn: writeReviewStateFn,
1079
+ readReviewStateFn: readReviewStateFn,
1080
+ transitionTaskFn: transitionTaskFn,
1081
+ applyAgentFallbackFn: applyAgentFallbackFn,
1082
+ taskResolution,
1083
+ enforceTaskAssigneeFn,
1084
+ log,
1085
+ error,
1086
+ sleepFn,
1087
+ buildCompactActOnReviewPromptFn,
1088
+ isForgejoReviewEnabledFn,
1089
+ isReviewProviderEnabledFn,
1090
+ legacyIsForgejoReviewEnabledFn,
1091
+ exit,
1092
+ });
1093
+ if (bounceResult.stranded) {
1094
+ error(fmt.status('FAIL', `Pre-review gate failure stranded mission ${slug}. Exiting review loop.`));
1095
+ exit(1); return;
1096
+ }
1097
+ if (bounceResult.bounced) {
1098
+ log(fmt.status('INFO', `Auto-bounce succeeded. Continuing to next round.`));
1099
+ continue;
1100
+ }
1101
+ } else {
1102
+ log(fmt.status('PASS', `Pre-review gate passed for area "${preReviewGateResult.area}".`));
1103
+ }
1104
+ }
1105
+
790
1106
  if (reviewer === 'autonomous' && !forgejoEnabled) {
791
1107
  log(fmt.status('INFO', `Round ${attempt}: reviewer identity is autonomous; skipping reviewer launch and using local review artifacts only.`));
792
1108
  } else {
@@ -796,7 +1112,7 @@ export async function startReviewLoop(slug: string, opts: {
796
1112
  try {
797
1113
  reviewerLaunchResult = await startAgentFn('review', {
798
1114
  agent: reviewer,
799
- prompt: (actualReviewer: string) => (buildCompactReviewPromptFn as any)({ reviewer: reviewer!, branch, implementer: implementer!, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer }),
1115
+ prompt: (actualReviewer: string) => (buildCompactReviewPromptFn as any)({ reviewer: reviewer!, branch, implementer: implementer!, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer, reviewBaseline }),
800
1116
  worktree, slug, role: 'reviewer', exclude: [implementer]
801
1117
  });
802
1118
  } catch (err: unknown) {
@@ -861,7 +1177,7 @@ export async function startReviewLoop(slug: string, opts: {
861
1177
  try {
862
1178
  relaunchResult = await startAgentFn('review', {
863
1179
  agent: reviewer,
864
- prompt: (actualReviewer: string) => (buildCompactReviewPromptFn as any)({ reviewer: reviewer!, branch, implementer: implementer!, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer }) + '\n\n' + recoveryPrompt,
1180
+ prompt: (actualReviewer: string) => (buildCompactReviewPromptFn as any)({ reviewer: reviewer!, branch, implementer: implementer!, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer, reviewBaseline }) + '\n\n' + recoveryPrompt,
865
1181
  worktree, slug, role: 'reviewer', exclude: [implementer]
866
1182
  });
867
1183
  } catch (err: unknown) {
@@ -998,7 +1314,7 @@ export async function startReviewLoop(slug: string, opts: {
998
1314
  // First launch or re-launch after stale BLOCKED/PARKED
999
1315
  if (dryRun) {
1000
1316
  log(`\n--- DRY-RUN: implementer (${implementer}) act-on-review prompt ---`);
1001
- log((buildActOnReviewPromptFn as any)({ implementer: implementer!, branch, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer: '{{AGENT_NAME}}' }));
1317
+ log((buildActOnReviewPromptFn as any)({ implementer: implementer!, branch, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer: '{{AGENT_NAME}}', reviewBaseline }));
1002
1318
  if (reLaunch!) {
1003
1319
  log(fmt.status('INFO', `Round ${attempt}: stale BLOCKED/PARKED disposition replaced by fresh implementer action.`));
1004
1320
  }
@@ -1016,7 +1332,7 @@ export async function startReviewLoop(slug: string, opts: {
1016
1332
  try {
1017
1333
  implementerLaunchResult = await startAgentFn('act-on-review', {
1018
1334
  agent: implementer,
1019
- prompt: (actualImplementer: string) => (buildCompactActOnReviewPromptFn as any)({ implementer: implementer!, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer }),
1335
+ prompt: (actualImplementer: string) => (buildCompactActOnReviewPromptFn as any)({ implementer: implementer!, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer, reviewBaseline }),
1020
1336
  worktree, slug, role: 'implementer', exclude: [reviewer]
1021
1337
  });
1022
1338
  } catch (err: unknown) {
@@ -1078,7 +1394,7 @@ export async function startReviewLoop(slug: string, opts: {
1078
1394
  try {
1079
1395
  relaunchResult = await startAgentFn('act-on-review', {
1080
1396
  agent: implementer,
1081
- prompt: (actualImplementer: string) => (buildCompactActOnReviewPromptFn as any)({ implementer: implementer!, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer }) + '\n\n' + recoveryPrompt,
1397
+ prompt: (actualImplementer: string) => (buildCompactActOnReviewPromptFn as any)({ implementer: implementer!, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer, reviewBaseline }) + '\n\n' + recoveryPrompt,
1082
1398
  worktree, slug, role: 'implementer', exclude: [reviewer]
1083
1399
  });
1084
1400
  } catch (err: unknown) {
@@ -64,14 +64,14 @@ async function pollForReview(
64
64
  sinceIso: string,
65
65
  token: string,
66
66
  options: {
67
- getLatestReviewForPrFn?: (prNumber: number, reviewerUser: string, sinceIso: string, token: string) => Promise<unknown>;
68
- sleepFn?: (ms: number) => Promise<void>;
67
+ getLatestReviewForPrFn?: (_prNumber: number, _reviewerUser: string, _sinceIso: string, _token: string) => Promise<unknown>;
68
+ sleepFn?: (_ms: number) => Promise<void>;
69
69
  intervalMs?: number;
70
70
  timeoutMs?: number;
71
71
  verbose?: boolean;
72
72
  label?: string;
73
73
  retryCount?: number;
74
- log?: (msg: string) => void;
74
+ log?: (_msg: string) => void;
75
75
  } = {}
76
76
  ): Promise<string | typeof POLL_TIMEOUT | null> {
77
77
  const {
@@ -131,14 +131,14 @@ async function pollForDisposition(
131
131
  sinceIso: string,
132
132
  token: string,
133
133
  options: {
134
- getLatestDispositionForPrFn?: (prNumber: number, implementerUser: string, sinceIso: string, token: string) => Promise<unknown>;
135
- sleepFn?: (ms: number) => Promise<void>;
134
+ getLatestDispositionForPrFn?: (_prNumber: number, _implementerUser: string, _sinceIso: string, _token: string) => Promise<unknown>;
135
+ sleepFn?: (_ms: number) => Promise<void>;
136
136
  intervalMs?: number;
137
137
  timeoutMs?: number;
138
138
  verbose?: boolean;
139
139
  label?: string;
140
140
  retryCount?: number;
141
- log?: (msg: string) => void;
141
+ log?: (_msg: string) => void;
142
142
  } = {}
143
143
  ): Promise<string | typeof POLL_TIMEOUT | null> {
144
144
  const {
@@ -111,7 +111,7 @@ function resolvePrimaryBranch(repoRoot) {
111
111
  * @param {{reviewer: string, branch: string, implementer: string, focus?: string, attempt: number, repoRoot?: string, missionPath?: string}} opts
112
112
  * @returns {string}
113
113
  */
114
- function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, repoRoot = '', missionPath: missionPathOverride }) {
114
+ function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }) {
115
115
  const entrypoint = reviewEntrypoint(reviewer);
116
116
  const repoLine = repoRoot ? `\noperate from repo root: ${repoRoot}` : '';
117
117
  const slug = branch.replace(/^mission\//, '');
@@ -129,6 +129,7 @@ function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attem
129
129
  .replaceAll('{{missionPath}}', missionPath)
130
130
  .replaceAll('{{artifactDir}}', artifactDir)
131
131
  .replaceAll('{{primaryBranch}}', resolvePrimaryBranch(repoRoot))
132
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || resolvePrimaryBranch(repoRoot))
132
133
  .replaceAll('{{review_entrypoint}}', entrypoint)
133
134
  .replaceAll('YYYY', year)
134
135
  .replaceAll('{{repo_line}}', repoLine ? repoLine.trim() + '\n- ' : '');
@@ -137,7 +138,7 @@ function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attem
137
138
  * @param {{implementer: string, branch: string, attempt: number, repoRoot?: string, missionPath?: string}} opts
138
139
  * @returns {string}
139
140
  */
140
- function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', missionPath: missionPathOverride }) {
141
+ function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }) {
141
142
  const entrypoint = actOnReviewEntrypoint(implementer);
142
143
  const repoLine = repoRoot ? `\noperate from repo root: ${repoRoot}` : '';
143
144
  const slug = branch.replace(/^mission\//, '');
@@ -153,6 +154,7 @@ function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', m
153
154
  .replaceAll('{{missionPath}}', missionPath)
154
155
  .replaceAll('{{artifactDir}}', artifactDir)
155
156
  .replaceAll('{{primaryBranch}}', resolvePrimaryBranch(repoRoot))
157
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || resolvePrimaryBranch(repoRoot))
156
158
  .replaceAll('{{act_on_review_entrypoint}}', entrypoint)
157
159
  .replaceAll('YYYY', year)
158
160
  .replaceAll('{{repo_line}}', repoLine ? repoLine.trim() + '\n- ' : '');
@@ -161,7 +163,7 @@ function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', m
161
163
  * @param {{reviewer: string, branch: string, implementer: string, focus?: string, attempt: number, actualReviewer?: string, repoRoot?: string, missionPath?: string}} opts
162
164
  * @returns {string}
163
165
  */
164
- function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, actualReviewer, repoRoot = '', missionPath: missionPathOverride }) {
166
+ function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, actualReviewer, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }) {
165
167
  const slug = branch.replace(/^mission\//, '');
166
168
  const finalReviewer = actualReviewer || reviewer;
167
169
  const year = (0, mission_utils_js_1.getMissionYear)(slug, repoRoot || process.cwd());
@@ -179,6 +181,7 @@ function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all'
179
181
  .replaceAll('{{missionPath}}', missionPath)
180
182
  .replaceAll('{{artifactDir}}', artifactDir)
181
183
  .replaceAll('{{primaryBranch}}', primaryBranch)
184
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || primaryBranch)
182
185
  .replaceAll('YYYY', year)
183
186
  .replaceAll('{{review_entrypoint}}', reviewEntrypoint(finalReviewer));
184
187
  }
@@ -186,7 +189,7 @@ function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all'
186
189
  * @param {{implementer: string, branch: string, attempt: number, reviewOutcome?: string, actualImplementer?: string, repoRoot?: string, missionPath?: string}} opts
187
190
  * @returns {string}
188
191
  */
189
- function buildCompactActOnReviewPrompt({ implementer, branch, attempt, reviewOutcome = '?', actualImplementer, repoRoot = '', missionPath: missionPathOverride }) {
192
+ function buildCompactActOnReviewPrompt({ implementer, branch, attempt, reviewOutcome = '?', actualImplementer, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }) {
190
193
  const slug = branch.replace(/^mission\//, '');
191
194
  const finalImplementer = actualImplementer || implementer;
192
195
  const year = (0, mission_utils_js_1.getMissionYear)(slug, repoRoot || process.cwd());
@@ -202,6 +205,7 @@ function buildCompactActOnReviewPrompt({ implementer, branch, attempt, reviewOut
202
205
  .replaceAll('{{missionPath}}', missionPath)
203
206
  .replaceAll('{{artifactDir}}', artifactDir)
204
207
  .replaceAll('{{primaryBranch}}', primaryBranch)
208
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || primaryBranch)
205
209
  .replaceAll('{{review_outcome}}', reviewOutcome)
206
210
  .replaceAll('YYYY', year)
207
211
  .replaceAll('{{act_on_review_entrypoint}}', actOnReviewEntrypoint(finalImplementer));
@@ -72,8 +72,8 @@ function resolvePrimaryBranch(repoRoot?: string): string {
72
72
  * @param {{reviewer: string, branch: string, implementer: string, focus?: string, attempt: number, repoRoot?: string, missionPath?: string}} opts
73
73
  * @returns {string}
74
74
  */
75
- export function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, repoRoot = '', missionPath: missionPathOverride }: {
76
- reviewer: string; branch: string; implementer: string; focus?: string; attempt: number; repoRoot?: string; missionPath?: string;
75
+ export function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }: {
76
+ reviewer: string; branch: string; implementer: string; focus?: string; attempt: number; repoRoot?: string; missionPath?: string; reviewBaseline?: string;
77
77
  }): string {
78
78
  const entrypoint = reviewEntrypoint(reviewer);
79
79
  const repoLine = repoRoot ? `\noperate from repo root: ${repoRoot}` : '';
@@ -93,6 +93,7 @@ export function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all'
93
93
  .replaceAll('{{missionPath}}', missionPath)
94
94
  .replaceAll('{{artifactDir}}', artifactDir)
95
95
  .replaceAll('{{primaryBranch}}', resolvePrimaryBranch(repoRoot))
96
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || resolvePrimaryBranch(repoRoot))
96
97
  .replaceAll('{{review_entrypoint}}', entrypoint)
97
98
  .replaceAll('YYYY', year)
98
99
  .replaceAll('{{repo_line}}', repoLine ? repoLine.trim() + '\n- ' : '');
@@ -102,8 +103,8 @@ export function buildReviewPrompt({ reviewer, branch, implementer, focus = 'all'
102
103
  * @param {{implementer: string, branch: string, attempt: number, repoRoot?: string, missionPath?: string}} opts
103
104
  * @returns {string}
104
105
  */
105
- export function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', missionPath: missionPathOverride }: {
106
- implementer: string; branch: string; attempt: number; repoRoot?: string; missionPath?: string;
106
+ export function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }: {
107
+ implementer: string; branch: string; attempt: number; repoRoot?: string; missionPath?: string; reviewBaseline?: string;
107
108
  }): string {
108
109
  const entrypoint = actOnReviewEntrypoint(implementer);
109
110
  const repoLine = repoRoot ? `\noperate from repo root: ${repoRoot}` : '';
@@ -121,6 +122,7 @@ export function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot
121
122
  .replaceAll('{{missionPath}}', missionPath)
122
123
  .replaceAll('{{artifactDir}}', artifactDir)
123
124
  .replaceAll('{{primaryBranch}}', resolvePrimaryBranch(repoRoot))
125
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || resolvePrimaryBranch(repoRoot))
124
126
  .replaceAll('{{act_on_review_entrypoint}}', entrypoint)
125
127
  .replaceAll('YYYY', year)
126
128
  .replaceAll('{{repo_line}}', repoLine ? repoLine.trim() + '\n- ' : '');
@@ -130,8 +132,8 @@ export function buildActOnReviewPrompt({ implementer, branch, attempt, repoRoot
130
132
  * @param {{reviewer: string, branch: string, implementer: string, focus?: string, attempt: number, actualReviewer?: string, repoRoot?: string, missionPath?: string}} opts
131
133
  * @returns {string}
132
134
  */
133
- export function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, actualReviewer, repoRoot = '', missionPath: missionPathOverride }: {
134
- reviewer: string; branch: string; implementer: string; focus?: string; attempt: number; actualReviewer?: string; repoRoot?: string; missionPath?: string;
135
+ export function buildCompactReviewPrompt({ reviewer, branch, implementer, focus = 'all', attempt, actualReviewer, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }: {
136
+ reviewer: string; branch: string; implementer: string; focus?: string; attempt: number; actualReviewer?: string; repoRoot?: string; missionPath?: string; reviewBaseline?: string;
135
137
  }): string {
136
138
  const slug = branch.replace(/^mission\//, '');
137
139
  const finalReviewer = actualReviewer || reviewer;
@@ -150,6 +152,7 @@ export function buildCompactReviewPrompt({ reviewer, branch, implementer, focus
150
152
  .replaceAll('{{missionPath}}', missionPath)
151
153
  .replaceAll('{{artifactDir}}', artifactDir)
152
154
  .replaceAll('{{primaryBranch}}', primaryBranch)
155
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || primaryBranch)
153
156
  .replaceAll('YYYY', year)
154
157
  .replaceAll('{{review_entrypoint}}', reviewEntrypoint(finalReviewer));
155
158
  }
@@ -158,8 +161,8 @@ export function buildCompactReviewPrompt({ reviewer, branch, implementer, focus
158
161
  * @param {{implementer: string, branch: string, attempt: number, reviewOutcome?: string, actualImplementer?: string, repoRoot?: string, missionPath?: string}} opts
159
162
  * @returns {string}
160
163
  */
161
- export function buildCompactActOnReviewPrompt({ implementer, branch, attempt, reviewOutcome = '?', actualImplementer, repoRoot = '', missionPath: missionPathOverride }: {
162
- implementer: string; branch: string; attempt: number; reviewOutcome?: string; actualImplementer?: string; repoRoot?: string; missionPath?: string;
164
+ export function buildCompactActOnReviewPrompt({ implementer, branch, attempt, reviewOutcome = '?', actualImplementer, repoRoot = '', missionPath: missionPathOverride, reviewBaseline }: {
165
+ implementer: string; branch: string; attempt: number; reviewOutcome?: string; actualImplementer?: string; repoRoot?: string; missionPath?: string; reviewBaseline?: string;
163
166
  }): string {
164
167
  const slug = branch.replace(/^mission\//, '');
165
168
  const finalImplementer = actualImplementer || implementer;
@@ -176,6 +179,7 @@ export function buildCompactActOnReviewPrompt({ implementer, branch, attempt, re
176
179
  .replaceAll('{{missionPath}}', missionPath)
177
180
  .replaceAll('{{artifactDir}}', artifactDir)
178
181
  .replaceAll('{{primaryBranch}}', primaryBranch)
182
+ .replaceAll('{{reviewBaseline}}', reviewBaseline || primaryBranch)
179
183
  .replaceAll('{{review_outcome}}', reviewOutcome)
180
184
  .replaceAll('YYYY', year)
181
185
  .replaceAll('{{act_on_review_entrypoint}}', actOnReviewEntrypoint(finalImplementer));