@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.
- package/docs/agents.md +1 -1
- package/docs/use-cases.md +1 -1
- package/lib/agents/agents.js +20 -3
- package/lib/agents/agents.ts +14 -3
- package/lib/agents/mistral-telemetry.js +122 -23
- package/lib/agents/mistral-telemetry.ts +141 -26
- package/lib/agents/mistral.js +128 -14
- package/lib/agents/mistral.ts +145 -5
- package/lib/commands/active.js +69 -39
- package/lib/commands/active.ts +97 -60
- package/lib/commands/config.ts +3 -3
- package/lib/commands/coverage-gate.ts +1 -1
- package/lib/commands/draft.js +1 -1
- package/lib/commands/draft.ts +1 -1
- package/lib/commands/handoff.js +13 -8
- package/lib/commands/handoff.ts +24 -18
- package/lib/commands/rebase.js +1 -1
- package/lib/commands/rebase.ts +2 -2
- package/lib/commands/repair-handoff.js +141 -20
- package/lib/commands/repair-handoff.ts +185 -45
- package/lib/commands/resolve-conflict.js +1 -1
- package/lib/commands/resolve-conflict.ts +2 -2
- package/lib/commands/stats-backfill.ts +10 -10
- package/lib/commands/stats.js +38 -11
- package/lib/commands/stats.ts +40 -96
- package/lib/core/fmt.ts +2 -2
- package/lib/core/git.ts +2 -2
- package/lib/core/gitignore.ts +2 -2
- package/lib/core/mission-utils.js +2 -2
- package/lib/core/mission-utils.ts +2 -2
- package/lib/core/persistent-data-migration.ts +2 -2
- package/lib/core/spawn-tee.ts +1 -1
- package/lib/core/state-map.ts +2 -2
- package/lib/core/storage.ts +1 -1
- package/lib/core/verification.ts +1 -1
- package/lib/review/rebase.ts +12 -12
- package/lib/review/review-artifacts.ts +35 -35
- package/lib/review/review-commands.ts +40 -40
- package/lib/review/review-events.ts +12 -12
- package/lib/review/review-loop.js +236 -7
- package/lib/review/review-loop.ts +338 -22
- package/lib/review/review-polling.ts +6 -6
- package/lib/review/review-prompts.js +8 -4
- package/lib/review/review-prompts.ts +12 -8
- package/lib/review/review-state.js +1 -1
- package/lib/review/review-state.ts +2 -2
- package/package.json +3 -2
- package/prompts/review-verbose.md +1 -1
- package/prompts/review.md +1 -1
- package/px.js +8 -4
|
@@ -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
|
}
|