@magnusekdahl/parallix 1.3.3 → 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 (47) 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.js +128 -14
  6. package/lib/agents/mistral.ts +145 -5
  7. package/lib/commands/active.js +69 -39
  8. package/lib/commands/active.ts +97 -60
  9. package/lib/commands/config.ts +3 -3
  10. package/lib/commands/coverage-gate.ts +1 -1
  11. package/lib/commands/draft.js +1 -1
  12. package/lib/commands/draft.ts +1 -1
  13. package/lib/commands/handoff.js +13 -8
  14. package/lib/commands/handoff.ts +24 -18
  15. package/lib/commands/rebase.js +1 -1
  16. package/lib/commands/rebase.ts +2 -2
  17. package/lib/commands/repair-handoff.js +141 -20
  18. package/lib/commands/repair-handoff.ts +185 -45
  19. package/lib/commands/resolve-conflict.js +1 -1
  20. package/lib/commands/resolve-conflict.ts +2 -2
  21. package/lib/commands/stats-backfill.ts +10 -10
  22. package/lib/commands/stats.js +38 -11
  23. package/lib/commands/stats.ts +40 -96
  24. package/lib/core/fmt.ts +2 -2
  25. package/lib/core/git.ts +2 -2
  26. package/lib/core/gitignore.ts +2 -2
  27. package/lib/core/mission-utils.js +2 -2
  28. package/lib/core/mission-utils.ts +2 -2
  29. package/lib/core/persistent-data-migration.ts +2 -2
  30. package/lib/core/spawn-tee.ts +1 -1
  31. package/lib/core/state-map.ts +2 -2
  32. package/lib/core/storage.ts +1 -1
  33. package/lib/core/verification.ts +1 -1
  34. package/lib/review/rebase.ts +12 -12
  35. package/lib/review/review-artifacts.ts +35 -35
  36. package/lib/review/review-commands.ts +40 -40
  37. package/lib/review/review-events.ts +12 -12
  38. package/lib/review/review-loop.js +236 -7
  39. package/lib/review/review-loop.ts +338 -22
  40. package/lib/review/review-polling.ts +6 -6
  41. package/lib/review/review-prompts.js +8 -4
  42. package/lib/review/review-prompts.ts +12 -8
  43. package/lib/review/review-state.js +1 -1
  44. package/lib/review/review-state.ts +2 -2
  45. package/package.json +3 -2
  46. package/prompts/review-verbose.md +1 -1
  47. package/prompts/review.md +1 -1
@@ -51,7 +51,7 @@ export function readTextFlag(
51
51
  inlineFlag: string,
52
52
  fileFlag: string,
53
53
  label: string,
54
- options: { readFileSync?: typeof fs.readFileSync; error?: (msg: string) => void; exit?: (code: number) => never } = {}
54
+ options: { readFileSync?: typeof fs.readFileSync; error?: (_msg: string) => void; exit?: (_code: number) => never } = {}
55
55
  ): string | null {
56
56
  const readFileSync = options.readFileSync || fs.readFileSync;
57
57
  const error = options.error || fmt.log.plainError;
@@ -98,8 +98,8 @@ export function formatStaticReviewSuccess(slug: string): string {
98
98
  function repairStaleActiveTaskAfterReview(
99
99
  slug: string,
100
100
  options: {
101
- log?: (msg: string) => void;
102
- error?: (msg: string) => void;
101
+ log?: (_msg: string) => void;
102
+ error?: (_msg: string) => void;
103
103
  getTaskStatusFn?: typeof getTaskStatus;
104
104
  resolveTaskFileFn?: typeof resolveTaskFile;
105
105
  transitionTaskFn?: typeof transitionTask;
@@ -133,7 +133,7 @@ function repairStaleActiveTaskAfterReview(
133
133
 
134
134
  async function commitPersistedReviewOutputs(
135
135
  slug: string,
136
- options: { worktree?: string; taskFile?: string | null; log?: (msg: string) => void; error?: (msg: string) => void } = {}
136
+ options: { worktree?: string; taskFile?: string | null; log?: (_msg: string) => void; error?: (_msg: string) => void } = {}
137
137
  ): Promise<{ ok: boolean; dirty?: boolean; unsafe?: boolean }> {
138
138
  return commitSafeMissionArtifacts(slug, options.worktree || process.cwd(), {
139
139
  taskFile: options.taskFile || null,
@@ -146,8 +146,8 @@ export function postStaticReviewComment(
146
146
  slug: string,
147
147
  message: string,
148
148
  options: {
149
- log?: (msg: string) => void;
150
- error?: (msg: string) => void;
149
+ log?: (_msg: string) => void;
150
+ error?: (_msg: string) => void;
151
151
  resolveTaskFileFn?: typeof resolveTaskFile;
152
152
  getTaskImplementerFn?: typeof getTaskImplementer;
153
153
  resolveWorktreeFn?: typeof resolveWorktree;
@@ -215,7 +215,7 @@ export function postStaticReviewComment(
215
215
  export function performStaticReview(
216
216
  slug: string,
217
217
  options: {
218
- log?: (msg: string) => void;
218
+ log?: (_msg: string) => void;
219
219
  findMissionDir?: typeof findMissionDir;
220
220
  findCheckpoints?: typeof findCheckpoints;
221
221
  readFileSync?: typeof fs.readFileSync;
@@ -338,9 +338,9 @@ export function verifyReview(
338
338
  slug: string,
339
339
  skipGate: boolean | string,
340
340
  options: {
341
- log?: (msg: string) => void;
342
- error?: (msg: string) => void;
343
- exit?: (code: number) => never;
341
+ log?: (_msg: string) => void;
342
+ error?: (_msg: string) => void;
343
+ exit?: (_code: number) => never;
344
344
  resolveWorktreeFn?: typeof resolveWorktree;
345
345
  findMissionDirFn?: typeof findMissionDir;
346
346
  getCurrentBranchFn?: typeof getCurrentBranch;
@@ -522,15 +522,15 @@ export async function submitForReview(
522
522
  slug: string,
523
523
  skipGate: boolean | string,
524
524
  options: {
525
- exit?: (code: number) => never;
525
+ exit?: (_code: number) => never;
526
526
  resolveTaskFileFn?: typeof resolveTaskFile;
527
527
  getTaskImplementerFn?: typeof getTaskImplementer;
528
528
  resolveWorktreeFn?: typeof resolveWorktree;
529
- performHandoffFn?: (slug: string, opts?: Record<string, unknown>) => Promise<Record<string, unknown>>;
529
+ performHandoffFn?: (_slug: string, _opts?: Record<string, unknown>) => Promise<Record<string, unknown>>;
530
530
  readReviewStateFn?: typeof readReviewState;
531
531
  isReviewProviderEnabledFn?: typeof isProviderEnabled;
532
532
  isForgejoReviewEnabledFn?: typeof isProviderEnabled;
533
- log?: (msg: string) => void;
533
+ log?: (_msg: string) => void;
534
534
  } = {}
535
535
  ): Promise<void> {
536
536
  const exit = options.exit || process.exit;
@@ -583,9 +583,9 @@ export async function submitForReview(
583
583
  export async function readComments(
584
584
  slug: string,
585
585
  options: {
586
- log?: (msg: string) => void;
587
- error?: (msg: string) => void;
588
- exit?: (code: number) => never;
586
+ log?: (_msg: string) => void;
587
+ error?: (_msg: string) => void;
588
+ exit?: (_code: number) => never;
589
589
  readTokenFn?: typeof readToken;
590
590
  readReviewStateFn?: typeof readReviewState;
591
591
  resolveReviewUserFn?: typeof resolveReviewUser;
@@ -676,9 +676,9 @@ export async function readComments(
676
676
  export async function pushRound(
677
677
  slug: string,
678
678
  options: {
679
- log?: (msg: string) => void;
680
- error?: (msg: string) => void;
681
- exit?: (code: number) => never;
679
+ log?: (_msg: string) => void;
680
+ error?: (_msg: string) => void;
681
+ exit?: (_code: number) => never;
682
682
  resolveWorktreeFn?: typeof resolveWorktree;
683
683
  resolveTaskFileFn?: typeof resolveTaskFile;
684
684
  getTaskImplementerFn?: typeof getTaskImplementer;
@@ -800,7 +800,7 @@ export async function pushRound(
800
800
  export function showReviewStatus(
801
801
  slug: string,
802
802
  options: {
803
- log?: (msg: string) => void;
803
+ log?: (_msg: string) => void;
804
804
  readReviewStateFn?: typeof readReviewState;
805
805
  resolveWorktreeFn?: typeof resolveWorktree;
806
806
  } = {}
@@ -843,9 +843,9 @@ export function commentRound(
843
843
  slug: string,
844
844
  message: string,
845
845
  options: {
846
- log?: (msg: string) => void;
847
- error?: (msg: string) => void;
848
- exit?: (code: number) => never;
846
+ log?: (_msg: string) => void;
847
+ error?: (_msg: string) => void;
848
+ exit?: (_code: number) => never;
849
849
  readReviewStateFn?: typeof readReviewState;
850
850
  writeReviewStateFn?: typeof writeReviewState;
851
851
  resolveReviewUserFn?: typeof resolveReviewUser;
@@ -902,8 +902,8 @@ export function commentRound(
902
902
  export async function consumeArtifacts(
903
903
  slug: string,
904
904
  options: {
905
- log?: (msg: string) => void;
906
- error?: (msg: string) => void;
905
+ log?: (_msg: string) => void;
906
+ error?: (_msg: string) => void;
907
907
  resolveWorktreeFn?: typeof resolveWorktree;
908
908
  transitionTaskFn?: typeof transitionTask;
909
909
  consumeReviewerArtifactsFn?: typeof consumeReviewerArtifacts;
@@ -1015,9 +1015,9 @@ export function submitReviewRound(
1015
1015
  outcome: string,
1016
1016
  message: string,
1017
1017
  options: {
1018
- log?: (msg: string) => void;
1019
- error?: (msg: string) => void;
1020
- exit?: (code: number) => never;
1018
+ log?: (_msg: string) => void;
1019
+ error?: (_msg: string) => void;
1020
+ exit?: (_code: number) => never;
1021
1021
  transitionTaskFn?: typeof transitionTask;
1022
1022
  readReviewStateFn?: typeof readReviewState;
1023
1023
  writeReviewStateFn?: typeof writeReviewState;
@@ -1178,9 +1178,9 @@ export function submitReviewRound(
1178
1178
  export async function closeMissionPr(
1179
1179
  slug: string,
1180
1180
  options: {
1181
- log?: (msg: string) => void;
1182
- error?: (msg: string) => void;
1183
- exit?: (code: number) => never;
1181
+ log?: (_msg: string) => void;
1182
+ error?: (_msg: string) => void;
1183
+ exit?: (_code: number) => never;
1184
1184
  readTokenFn?: typeof readToken;
1185
1185
  readReviewStateFn?: typeof readReviewState;
1186
1186
  resolveReviewUserFn?: typeof resolveReviewUser;
@@ -1233,9 +1233,9 @@ export function createEventHandler(
1233
1233
  slug: string,
1234
1234
  args: string[],
1235
1235
  options: {
1236
- log?: (msg: string) => void;
1237
- error?: (msg: string) => void;
1238
- exit?: (code: number) => never;
1236
+ log?: (_msg: string) => void;
1237
+ error?: (_msg: string) => void;
1238
+ exit?: (_code: number) => never;
1239
1239
  resolveWorktreeFn?: typeof resolveWorktree;
1240
1240
  readReviewStateFn?: typeof readReviewState;
1241
1241
  } = {}
@@ -1349,9 +1349,9 @@ export function importLegacyHandler(
1349
1349
  slug: string,
1350
1350
  args: string[],
1351
1351
  options: {
1352
- log?: (msg: string) => void;
1353
- error?: (msg: string) => void;
1354
- exit?: (code: number) => never;
1352
+ log?: (_msg: string) => void;
1353
+ error?: (_msg: string) => void;
1354
+ exit?: (_code: number) => never;
1355
1355
  resolveWorktreeFn?: typeof resolveWorktree;
1356
1356
  } = {}
1357
1357
  ): void {
@@ -1383,9 +1383,9 @@ export async function review(
1383
1383
  args: string[],
1384
1384
  options: {
1385
1385
  inferSlugFn?: typeof inferSlug;
1386
- log?: (msg: string) => void;
1387
- error?: (msg: string) => void;
1388
- exit?: (code: number) => never;
1386
+ log?: (_msg: string) => void;
1387
+ error?: (_msg: string) => void;
1388
+ exit?: (_code: number) => never;
1389
1389
  verifyReviewFn?: typeof verifyReview;
1390
1390
  submitForReviewFn?: typeof submitForReview;
1391
1391
  consumeArtifactsFn?: typeof consumeArtifacts;
@@ -207,8 +207,8 @@ export interface CreateEventParams {
207
207
  export interface CreateEventOptions {
208
208
  skipGit?: boolean;
209
209
  gitFn?: typeof git;
210
- log?: (msg: string) => void;
211
- error?: (msg: string) => void;
210
+ log?: (_msg: string) => void;
211
+ error?: (_msg: string) => void;
212
212
  worktree?: string;
213
213
  allowMissingRequiredFields?: boolean;
214
214
  }
@@ -247,7 +247,7 @@ export interface ReadAllEventsOptions {
247
247
  rootDir?: string;
248
248
  readdirSync?: typeof fs.readdirSync;
249
249
  readFileSync?: typeof fs.readFileSync;
250
- error?: (msg: string) => void;
250
+ error?: (_msg: string) => void;
251
251
  }
252
252
 
253
253
  export interface ParseEventResult {
@@ -352,14 +352,14 @@ function classifyComment(comment: { body?: string }): string | null {
352
352
  }
353
353
 
354
354
  export interface ConsumeHumanNotesOptions {
355
- getCommentsFn?: (branch: string, token: string) => Promise<unknown[]>;
356
- createEventFn?: typeof createEvent;
357
- readTokenFn?: (user: string) => string | null;
355
+ getCommentsFn?: (_branch: string, _token: string) => Promise<unknown[]>;
356
+ createEventFn?: typeof createEvent;
357
+ readTokenFn?: (_user: string) => string | null;
358
358
  reviewIdentity?: string | null;
359
359
  forgejoUser?: string | null;
360
360
  worktree?: string;
361
- log?: (msg: string) => void;
362
- error?: (msg: string) => void;
361
+ log?: (_msg: string) => void;
362
+ error?: (_msg: string) => void;
363
363
  }
364
364
 
365
365
  /**
@@ -578,8 +578,8 @@ function createEvent(slug: string, eventType: string, params: CreateEventParams,
578
578
 
579
579
  export interface ImportLegacyArtifactOptions {
580
580
  tmpDir?: string;
581
- log?: (msg: string) => void;
582
- error?: (msg: string) => void;
581
+ log?: (_msg: string) => void;
582
+ error?: (_msg: string) => void;
583
583
  worktree?: string;
584
584
  }
585
585
 
@@ -650,9 +650,9 @@ const LEGACY_ARTIFACT_TO_EVENT_TYPE: Readonly<Record<string, string>> = Object.f
650
650
 
651
651
  export interface ImportAllLegacyOptions {
652
652
  tmpDir?: string;
653
- error?: (msg: string) => void;
653
+ error?: (_msg: string) => void;
654
654
  worktree?: string;
655
- log?: (msg: string) => void;
655
+ log?: (_msg: string) => void;
656
656
  }
657
657
 
658
658
  /**
@@ -44,12 +44,16 @@ exports.maybeUpdateGraphifyBeforeReview = maybeUpdateGraphifyBeforeReview;
44
44
  exports.applyAgentFallback = applyAgentFallback;
45
45
  exports.persistNormalizedPhaseRepair = persistNormalizedPhaseRepair;
46
46
  exports.stageLaunchSinceMs = stageLaunchSinceMs;
47
+ exports.classifyGateFailure = classifyGateFailure;
48
+ exports.runPreReviewGate = runPreReviewGate;
49
+ exports.handleGateFailureAutoBounce = handleGateFailureAutoBounce;
47
50
  exports.startReviewLoop = startReviewLoop;
48
51
  const fs = __importStar(require("fs"));
49
52
  const path = __importStar(require("path"));
50
53
  const fmt = __importStar(require("../core/fmt.js"));
51
54
  const git_js_1 = require("../core/git.js");
52
55
  const mission_utils_js_1 = require("../core/mission-utils.js");
56
+ const verification_js_1 = require("../core/verification.js");
53
57
  const backlog_js_1 = require("../tools/backlog.js");
54
58
  const state_map_js_1 = require("../core/state-map.js");
55
59
  const review_adapter_js_1 = require("./review-adapter.js");
@@ -232,10 +236,164 @@ function stageLaunchSinceMs(result) {
232
236
  return Number.isFinite(startedMs) ? startedMs : 0;
233
237
  }
234
238
  // ============================================================================
239
+ // Pre-review Gate Enforcement (ADR 0048 Control C1 / TASK-1385)
240
+ // ============================================================================
241
+ // Minimal error classifier stub for gate failures (TASK-1389 dependency).
242
+ // Maps gate failures to Class 6: genuine gate failure — code issue, dispatch: auto-send-back.
243
+ // TASK-1389 will replace this with the full 8-class dispatch table.
244
+ const GATE_FAILURE_CLASS = 'class-6-genuine-gate-failure';
245
+ const GATE_FAILURE_ACTION = 'auto-send-back';
246
+ function classifyGateFailure(_output) {
247
+ // Gate failures are genuine code issues — always relaunchable via auto-send-back.
248
+ // The full classifier (TASK-1389) will expand this to 8 classes with nuanced dispatch.
249
+ return {
250
+ classification: GATE_FAILURE_CLASS,
251
+ action: GATE_FAILURE_ACTION,
252
+ isRelaunchable: true,
253
+ };
254
+ }
255
+ /**
256
+ * Run the verification gate with the mission area before a review round.
257
+ * Captures stdout/stderr for use in auto-bounce fix prompts.
258
+ */
259
+ async function runPreReviewGate(slug, worktree, opts = {}) {
260
+ const { findMissionAreaFn = mission_utils_js_1.findMissionArea, runFn: runFnOverride = git_js_1.run, log = fmt.log.plain, error = fmt.log.plainError, } = opts;
261
+ const missionDir = (0, mission_utils_js_1.findMissionDir)(slug, worktree);
262
+ const area = missionDir ? findMissionAreaFn(missionDir) : 'docs';
263
+ const command = (0, verification_js_1.formatVerificationCommand)(area, worktree);
264
+ if (command === NO_GATE_NOTICE_ALIAS) {
265
+ log(fmt.status('INFO', `No verification gate configured for area ${area}; skipping pre-review gate check.`));
266
+ return { ok: true, area, command, exitCode: 0, stdout: '', stderr: '' };
267
+ }
268
+ log(fmt.status('INFO', `Pre-review gate for area "${area}": ${fmt.command(command)}`));
269
+ // Run with pipe mode to capture output for auto-bounce fix prompts.
270
+ const result = runFnOverride('bash', ['-lc', command], {
271
+ cwd: worktree,
272
+ stdio: 'pipe',
273
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
274
+ });
275
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
276
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
277
+ if (result.status !== 0) {
278
+ error(fmt.status('FAIL', `Pre-review gate failed for area "${area}" (exit ${result.status}).`));
279
+ if (stderr) {
280
+ error(` stderr: ${stderr.split('\n').slice(0, 10).join('\n ')}`);
281
+ }
282
+ return {
283
+ ok: false,
284
+ area,
285
+ command,
286
+ exitCode: result.status,
287
+ stdout,
288
+ stderr,
289
+ error: `verification gate failed with exit code ${result.status}`,
290
+ };
291
+ }
292
+ log(fmt.status('PASS', `Pre-review gate passed for area "${area}".`));
293
+ return { ok: true, area, command, exitCode: 0, stdout, stderr };
294
+ }
295
+ const NO_GATE_NOTICE_ALIAS = ': # no verification gate configured (set adapters.verification.command)';
296
+ /**
297
+ * Handle a pre-review gate failure by auto-bouncing to the implementer.
298
+ * Does NOT consume a reviewer cycle or transition the task out of review status.
299
+ * Tracks retry count in review state metadata.
300
+ * Returns true if bounced, false if retry limit exceeded (mission strands).
301
+ */
302
+ async function handleGateFailureAutoBounce(slug, worktree, gateResult, implementer, opts = {}) {
303
+ const { startAgentFn = agents_js_1.startAgent, writeReviewStateFn = review_state_js_1.writeReviewState, readReviewStateFn = review_state_js_1.readReviewState, transitionTaskFn = backlog_js_1.transitionTask, applyAgentFallbackFn = applyAgentFallback, taskResolution, enforceTaskAssigneeFn, log = fmt.log.plain, error = fmt.log.plainError, sleepFn: _sleepFn = review_polling_js_1.delay, buildCompactActOnReviewPromptFn: _buildCompactActOnReviewPromptFn = review_prompts_js_1.buildCompactActOnReviewPrompt, isForgejoReviewEnabledFn: _isForgejoReviewEnabledFn, isReviewProviderEnabledFn: _isReviewProviderEnabledFn, legacyIsForgejoReviewEnabledFn: _legacyIsForgejoReviewEnabledFn, exit: _exit = process.exit, } = opts;
304
+ const MAX_GATE_RETRY = 2;
305
+ // Read persisted state to get current retry count
306
+ const persisted = readReviewStateFn(slug, worktree);
307
+ const retryCount = persisted && persisted.metadata && typeof persisted.metadata === 'object'
308
+ ? (Number(persisted.metadata.gateFailureRetryCount) || 0)
309
+ : 0;
310
+ if (retryCount >= MAX_GATE_RETRY) {
311
+ error(fmt.status('FAIL', `Pre-review gate failure: max retries exceeded (${MAX_GATE_RETRY}). Mission stranded for ${slug}.`));
312
+ error(fmt.status('FAIL', `Area "${gateResult.area}" verification failed ${retryCount} times. Human intervention required.`));
313
+ error(fmt.status('FAIL', `Gate output:\n${gateResult.stdout || gateResult.stderr || '(no output)'}\n`));
314
+ return { bounced: false, stranded: true };
315
+ }
316
+ // Classify the failure
317
+ const combinedOutput = gateResult.stderr || gateResult.stdout || 'Gate failed with exit code ' + gateResult.exitCode;
318
+ const classification = classifyGateFailure(combinedOutput);
319
+ log(fmt.status('WARN', `Pre-review gate failed for area "${gateResult.area}" (exit ${gateResult.exitCode}). Classification: ${classification.classification}.`));
320
+ // Build fix prompt with captured gate output
321
+ const fixPrompt = [
322
+ `PRE-REVIEW GATE FAILURE — FIX REQUIRED`,
323
+ ``,
324
+ `Mission: ${slug}`,
325
+ `Area: ${gateResult.area}`,
326
+ `Gate command: ${gateResult.command}`,
327
+ `Exit code: ${gateResult.exitCode}`,
328
+ ``,
329
+ `Gate output (use this to diagnose and fix):`,
330
+ `---`,
331
+ gateResult.stdout || '(no stdout)',
332
+ `---`,
333
+ gateResult.stderr || '(no stderr)',
334
+ `---`,
335
+ ``,
336
+ `Classification: ${classification.classification} — ${classification.action}`,
337
+ `Retry attempt: ${retryCount + 1}/${MAX_GATE_RETRY}`,
338
+ ``,
339
+ `Fix the underlying issue so the verification gate passes for area "${gateResult.area}".`,
340
+ `After fixing, the review loop will re-run the gate before the next review round.`,
341
+ ].join('\n');
342
+ // Increment retry count in metadata
343
+ if (!persisted || !persisted.metadata || typeof persisted.metadata !== 'object') {
344
+ // Create new metadata
345
+ }
346
+ const metadata = persisted && persisted.metadata && typeof persisted.metadata === 'object'
347
+ ? { ...persisted.metadata }
348
+ : {};
349
+ metadata.gateFailureRetryCount = retryCount + 1;
350
+ // Update review state with incremented retry count
351
+ if (persisted) {
352
+ const updatedState = { ...persisted, metadata };
353
+ writeReviewStateFn(slug, updatedState, worktree);
354
+ }
355
+ else {
356
+ writeReviewStateFn(slug, { metadata }, worktree);
357
+ }
358
+ // Transition task back to active (implementer phase) without consuming reviewer cycle
359
+ transitionTaskFn(slug, 'active', { rootDir: worktree, log });
360
+ log(fmt.status('INFO', `Auto-bouncing to implementer (${implementer}) with fix prompt. Retry ${retryCount + 1}/${MAX_GATE_RETRY}.`));
361
+ // Launch implementer with the fix prompt
362
+ try {
363
+ const launchResult = await startAgentFn('act-on-review', {
364
+ agent: implementer,
365
+ prompt: (_actualImplementer) => fixPrompt,
366
+ worktree,
367
+ slug,
368
+ role: 'implementer',
369
+ exclude: [],
370
+ });
371
+ // Apply any agent fallback if needed
372
+ implementer = applyAgentFallbackFn({
373
+ role: 'implementer',
374
+ original: implementer,
375
+ launchResult,
376
+ state: persisted || {},
377
+ slug,
378
+ worktree,
379
+ taskResolution,
380
+ log,
381
+ writeReviewStateFn,
382
+ enforceTaskAssigneeFn,
383
+ });
384
+ }
385
+ catch (err) {
386
+ error(fmt.status('FAIL', `Could not relaunch implementer (${implementer}) for gate failure auto-bounce: ${err.message}`));
387
+ return { bounced: false, stranded: true };
388
+ }
389
+ log(fmt.status('INFO', `Implementer (${implementer}) relaunched with gate failure fix prompt.`));
390
+ return { bounced: true, stranded: false };
391
+ }
392
+ // ============================================================================
235
393
  // Main Review Loop
236
394
  // ============================================================================
237
395
  async function startReviewLoop(slug, opts = {}) {
238
- let { implementer, reviewer, focus = 'all', maxAttempts = DEFAULT_MAX_ATTEMPTS, dryRun = false, reset = false, continue: continueFlag = false, isContinue = false, verbose = false, pollTimeoutSeconds = null, worktree: callerWorktree, missionPath, resetReviewStateFn = review_state_js_1.resetReviewState, maybeUpdateGraphifyBeforeReviewFn = maybeUpdateGraphifyBeforeReview, readReviewStateFn = review_state_js_1.readReviewState, resolveTaskFileFn = backlog_js_1.resolveTaskFile, getTaskImplementerFn = backlog_js_1.getTaskImplementer, getTaskStatusFn = backlog_js_1.getTaskStatus, transitionTaskFn = backlog_js_1.transitionTask, toVirtualFn = state_map_js_1.toVirtual, transitionVirtualFn = state_map_js_1.transitionVirtual, workflowLauncherStatusFn = agents_js_1.workflowLauncherStatus, buildAutonomousReviewMatrixFn = runtime_matrix_js_1.buildAutonomousReviewMatrix, formatMatrixSummaryFn = runtime_matrix_js_1.formatMatrixSummary, selectAgentFn = agents_js_1.selectAgent, providerAvailableFn = undefined, runFn = git_js_1.run, getPrStatusFn = review_adapter_js_1.getPrStatus, enforceTaskAssigneeFn = backlog_js_1.enforceTaskAssignee, resolveReviewUserFn = undefined, forgejoAvailableFn = null, resolveForgejoUserFn = null, readTokenFn = review_adapter_js_1.readToken, getCommentsFn = review_adapter_js_1.getComments, postCommentFn = review_adapter_js_1.postComment, postReviewFn = review_adapter_js_1.postReview, writeReviewStateFn = review_state_js_1.writeReviewState, startAgentFn = agents_js_1.startAgent, pollForReviewFn = review_polling_js_1.pollForReview, pollForDispositionFn = review_polling_js_1.pollForDisposition, applyAgentFallbackFn = applyAgentFallback, buildReviewPromptFn = review_prompts_js_1.buildReviewPrompt, buildActOnReviewPromptFn = review_prompts_js_1.buildActOnReviewPrompt, buildCompactReviewPromptFn = review_prompts_js_1.buildCompactReviewPrompt, buildCompactActOnReviewPromptFn = review_prompts_js_1.buildCompactActOnReviewPrompt, consumeReviewerArtifactsFn = review_artifacts_js_1.consumeReviewerArtifacts, consumeImplementerArtifactsFn = review_artifacts_js_1.consumeImplementerArtifacts, rebaseBeforeReviewRoundFn = rebase_js_1.rebaseBeforeReviewRound, eligibleAgentsForStepFn = agents_js_1.eligibleAgentsForStep, log = fmt.log.plain, error = fmt.log.plainError, getLatestReviewForPrFn = review_adapter_js_1.getLatestReviewForPr, getLatestDispositionForPrFn = review_adapter_js_1.getLatestDispositionForPr, sleepFn = review_polling_js_1.delay, exit = process.exit, gitFn = git_js_1.git, isReviewProviderEnabledFn = undefined, legacyIsForgejoReviewEnabledFn = null, isForgejoReviewEnabledFn = null, recordStageStatsSafeFn = () => { } } = opts;
396
+ let { implementer, reviewer, focus = 'all', maxAttempts = DEFAULT_MAX_ATTEMPTS, dryRun = false, reset = false, continue: continueFlag = false, isContinue = false, verbose = false, pollTimeoutSeconds = null, worktree: callerWorktree, missionPath, runPreReviewGateFn = runPreReviewGate, handleGateFailureAutoBounceFn = handleGateFailureAutoBounce, resetReviewStateFn = review_state_js_1.resetReviewState, maybeUpdateGraphifyBeforeReviewFn = maybeUpdateGraphifyBeforeReview, readReviewStateFn = review_state_js_1.readReviewState, resolveTaskFileFn = backlog_js_1.resolveTaskFile, getTaskImplementerFn = backlog_js_1.getTaskImplementer, getTaskStatusFn = backlog_js_1.getTaskStatus, transitionTaskFn = backlog_js_1.transitionTask, toVirtualFn = state_map_js_1.toVirtual, transitionVirtualFn = state_map_js_1.transitionVirtual, workflowLauncherStatusFn = agents_js_1.workflowLauncherStatus, buildAutonomousReviewMatrixFn = runtime_matrix_js_1.buildAutonomousReviewMatrix, formatMatrixSummaryFn = runtime_matrix_js_1.formatMatrixSummary, selectAgentFn = agents_js_1.selectAgent, providerAvailableFn = undefined, runFn = git_js_1.run, getPrStatusFn = review_adapter_js_1.getPrStatus, enforceTaskAssigneeFn = backlog_js_1.enforceTaskAssignee, resolveReviewUserFn = undefined, forgejoAvailableFn = null, resolveForgejoUserFn = null, readTokenFn = review_adapter_js_1.readToken, getCommentsFn = review_adapter_js_1.getComments, postCommentFn = review_adapter_js_1.postComment, postReviewFn = review_adapter_js_1.postReview, writeReviewStateFn = review_state_js_1.writeReviewState, startAgentFn = agents_js_1.startAgent, pollForReviewFn = review_polling_js_1.pollForReview, pollForDispositionFn = review_polling_js_1.pollForDisposition, applyAgentFallbackFn = applyAgentFallback, buildReviewPromptFn = review_prompts_js_1.buildReviewPrompt, buildActOnReviewPromptFn = review_prompts_js_1.buildActOnReviewPrompt, buildCompactReviewPromptFn = review_prompts_js_1.buildCompactReviewPrompt, buildCompactActOnReviewPromptFn = review_prompts_js_1.buildCompactActOnReviewPrompt, consumeReviewerArtifactsFn = review_artifacts_js_1.consumeReviewerArtifacts, consumeImplementerArtifactsFn = review_artifacts_js_1.consumeImplementerArtifacts, rebaseBeforeReviewRoundFn = rebase_js_1.rebaseBeforeReviewRound, eligibleAgentsForStepFn = agents_js_1.eligibleAgentsForStep, log = fmt.log.plain, error = fmt.log.plainError, getLatestReviewForPrFn = review_adapter_js_1.getLatestReviewForPr, getLatestDispositionForPrFn = review_adapter_js_1.getLatestDispositionForPr, sleepFn = review_polling_js_1.delay, exit = process.exit, gitFn = git_js_1.git, isReviewProviderEnabledFn = undefined, legacyIsForgejoReviewEnabledFn = null, isForgejoReviewEnabledFn = null, recordStageStatsSafeFn = () => { } } = opts;
239
397
  const performHandoffFn = opts.performHandoffFn || (await getHandoff()).performHandoff;
240
398
  let prNumber = null;
241
399
  isContinue = Boolean(isContinue || continueFlag);
@@ -595,6 +753,30 @@ async function startReviewLoop(slug, opts = {}) {
595
753
  if (attempt > state.round) {
596
754
  state.advanceRound();
597
755
  }
756
+ // Snapshot the primary branch's HEAD commit for this round. This is a
757
+ // fallback value only, used for paths that don't rebase this round (a
758
+ // dry run, or resuming with an existing reviewState): once
759
+ // rebaseBeforeReviewRoundFn runs below, HEAD is rebased onto primary's
760
+ // *current* tip, so the baseline is re-captured immediately after the
761
+ // rebase completes (see below). Capturing it here, before the rebase,
762
+ // would pin the diff to a stale pre-rebase SHA; since rebase replays
763
+ // primary's newer commits into HEAD's ancestry, diffing against that
764
+ // stale SHA would surface exactly the "not rebased to main" noise this
765
+ // baseline exists to suppress (task-1407).
766
+ // Falls back to undefined (letting the prompt builders resolve the live
767
+ // primary branch ref themselves) if the primary branch cannot be detected,
768
+ // e.g. in a repo with no main/master branch yet.
769
+ const captureReviewBaseline = () => {
770
+ try {
771
+ const primaryBranchName = (0, mission_utils_js_1.getPrimaryBranch)(worktree, gitFn);
772
+ const reviewBaselineResult = gitFn(['-C', worktree, 'rev-parse', primaryBranchName]);
773
+ return (reviewBaselineResult.stdout || '').trim() || primaryBranchName;
774
+ }
775
+ catch {
776
+ return undefined;
777
+ }
778
+ };
779
+ let reviewBaseline = captureReviewBaseline();
598
780
  let reviewState;
599
781
  if (state.phase === 'reviewing') {
600
782
  // Check if we can skip reviewer launch
@@ -626,7 +808,7 @@ async function startReviewLoop(slug, opts = {}) {
626
808
  }
627
809
  else {
628
810
  log(`\n--- DRY-RUN: reviewer (${reviewer}) prompt ---`);
629
- log(buildReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer: '{{AGENT_NAME}}' }));
811
+ log(buildReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer: '{{AGENT_NAME}}', reviewBaseline }));
630
812
  }
631
813
  }
632
814
  }
@@ -643,11 +825,58 @@ async function startReviewLoop(slug, opts = {}) {
643
825
  exit(1);
644
826
  return;
645
827
  }
828
+ // Re-capture after rebase: HEAD is now rebased onto primary's tip,
829
+ // so the pre-rebase snapshot above is stale and must be replaced
830
+ // with the SHA that HEAD was actually rebased onto (task-1407).
831
+ reviewBaseline = captureReviewBaseline();
646
832
  if (!dryRun) {
647
833
  transitionTaskFn(slug, 'review', { rootDir: worktree, log });
648
834
  }
649
835
  state.phase = 'reviewing';
650
836
  writeReviewStateFn(slug, state, worktree);
837
+ // Pre-review gate enforcement (ADR 0048 Control C1 / TASK-1385):
838
+ // Run the verification gate before the reviewer launches. On failure,
839
+ // auto-bounce to the implementer with captured gate output as fix prompt.
840
+ // No reviewer cycle is consumed when the gate fails and auto-bounce occurs.
841
+ if (!dryRun) {
842
+ const preReviewGateResult = await runPreReviewGateFn(slug, worktree, {
843
+ runFn: runFn,
844
+ log,
845
+ error,
846
+ });
847
+ if (!preReviewGateResult.ok) {
848
+ log(fmt.status('WARN', `Pre-review gate failed for area "${preReviewGateResult.area}" (exit ${preReviewGateResult.exitCode}). Auto-bouncing to implementer.`));
849
+ const bounceResult = await handleGateFailureAutoBounceFn(slug, worktree, preReviewGateResult, implementer, {
850
+ startAgentFn: startAgentFn,
851
+ writeReviewStateFn: writeReviewStateFn,
852
+ readReviewStateFn: readReviewStateFn,
853
+ transitionTaskFn: transitionTaskFn,
854
+ applyAgentFallbackFn: applyAgentFallbackFn,
855
+ taskResolution,
856
+ enforceTaskAssigneeFn,
857
+ log,
858
+ error,
859
+ sleepFn,
860
+ buildCompactActOnReviewPromptFn,
861
+ isForgejoReviewEnabledFn,
862
+ isReviewProviderEnabledFn,
863
+ legacyIsForgejoReviewEnabledFn,
864
+ exit,
865
+ });
866
+ if (bounceResult.stranded) {
867
+ error(fmt.status('FAIL', `Pre-review gate failure stranded mission ${slug}. Exiting review loop.`));
868
+ exit(1);
869
+ return;
870
+ }
871
+ if (bounceResult.bounced) {
872
+ log(fmt.status('INFO', `Auto-bounce succeeded. Continuing to next round.`));
873
+ continue;
874
+ }
875
+ }
876
+ else {
877
+ log(fmt.status('PASS', `Pre-review gate passed for area "${preReviewGateResult.area}".`));
878
+ }
879
+ }
651
880
  if (reviewer === 'autonomous' && !forgejoEnabled) {
652
881
  log(fmt.status('INFO', `Round ${attempt}: reviewer identity is autonomous; skipping reviewer launch and using local review artifacts only.`));
653
882
  }
@@ -657,7 +886,7 @@ async function startReviewLoop(slug, opts = {}) {
657
886
  try {
658
887
  reviewerLaunchResult = await startAgentFn('review', {
659
888
  agent: reviewer,
660
- prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer }),
889
+ prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer, reviewBaseline }),
661
890
  worktree, slug, role: 'reviewer', exclude: [implementer]
662
891
  });
663
892
  }
@@ -722,7 +951,7 @@ async function startReviewLoop(slug, opts = {}) {
722
951
  try {
723
952
  relaunchResult = await startAgentFn('review', {
724
953
  agent: reviewer,
725
- prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer }) + '\n\n' + recoveryPrompt,
954
+ prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer, reviewBaseline }) + '\n\n' + recoveryPrompt,
726
955
  worktree, slug, role: 'reviewer', exclude: [implementer]
727
956
  });
728
957
  }
@@ -864,7 +1093,7 @@ async function startReviewLoop(slug, opts = {}) {
864
1093
  // First launch or re-launch after stale BLOCKED/PARKED
865
1094
  if (dryRun) {
866
1095
  log(`\n--- DRY-RUN: implementer (${implementer}) act-on-review prompt ---`);
867
- log(buildActOnReviewPromptFn({ implementer: implementer, branch, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer: '{{AGENT_NAME}}' }));
1096
+ log(buildActOnReviewPromptFn({ implementer: implementer, branch, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer: '{{AGENT_NAME}}', reviewBaseline }));
868
1097
  if (reLaunch) {
869
1098
  log(fmt.status('INFO', `Round ${attempt}: stale BLOCKED/PARKED disposition replaced by fresh implementer action.`));
870
1099
  }
@@ -881,7 +1110,7 @@ async function startReviewLoop(slug, opts = {}) {
881
1110
  try {
882
1111
  implementerLaunchResult = await startAgentFn('act-on-review', {
883
1112
  agent: implementer,
884
- prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer: implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer }),
1113
+ prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer: implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer, reviewBaseline }),
885
1114
  worktree, slug, role: 'implementer', exclude: [reviewer]
886
1115
  });
887
1116
  }
@@ -943,7 +1172,7 @@ async function startReviewLoop(slug, opts = {}) {
943
1172
  try {
944
1173
  relaunchResult = await startAgentFn('act-on-review', {
945
1174
  agent: implementer,
946
- prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer: implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer }) + '\n\n' + recoveryPrompt,
1175
+ prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer: implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer, reviewBaseline }) + '\n\n' + recoveryPrompt,
947
1176
  worktree, slug, role: 'implementer', exclude: [reviewer]
948
1177
  });
949
1178
  }