@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.
- 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.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/lib/commands/active.ts
CHANGED
|
@@ -302,36 +302,37 @@ function applyExecuteFallback(opts) {
|
|
|
302
302
|
* @returns {Promise<{relaunched: boolean, error?: string}>} Result of relaunch attempt
|
|
303
303
|
*/
|
|
304
304
|
/**
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
async function attemptAgentRelaunch(slug, worktree, errorMsg, agent, options = {}) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
305
|
+
* @param {string} slug
|
|
306
|
+
* @param {string} worktree
|
|
307
|
+
* @param {string} errorMsg
|
|
308
|
+
* @param {string} agent
|
|
309
|
+
* @param {{isRelaunchableErrorFn?: Function, buildRelaunchPromptFn?: Function, workflowLauncherStatusFn?: Function, startAgentFn?: Function, log?: Function, error?: Function, gateOutput?: {stdout: string, stderr: string}}} [options]
|
|
310
|
+
*/
|
|
311
|
+
async function attemptAgentRelaunch(slug, worktree, errorMsg, agent, options = {}) {
|
|
312
|
+
const {
|
|
313
|
+
isRelaunchableErrorFn = repairHandoff.isRelaunchableError,
|
|
314
|
+
buildRelaunchPromptFn = repairHandoff.buildRelaunchPrompt,
|
|
315
|
+
workflowLauncherStatusFn = agents.workflowLauncherStatus,
|
|
316
|
+
startAgentFn = agents.startAgent,
|
|
317
|
+
log = fmt.log.plain,
|
|
318
|
+
error = fmt.log.plainError,
|
|
319
|
+
gateOutput
|
|
320
|
+
} = options;
|
|
321
|
+
// Check if this is a relaunchable error
|
|
322
|
+
if (!isRelaunchableErrorFn(errorMsg)) {
|
|
323
|
+
log(`Error is not relaunchable: ${errorMsg}`);
|
|
324
|
+
return { relaunched: false, error: 'Error is not relaunchable for agent relaunch' };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check if the agent launcher is available
|
|
328
|
+
const status = workflowLauncherStatusFn(agent);
|
|
329
|
+
if (!status.supported) {
|
|
330
|
+
error(`Agent ${fmt.agent(agent)} is not available for relaunch: ${status.detail || status.reason || 'unknown'}`);
|
|
331
|
+
return { relaunched: false, error: `Agent ${agent} launcher is not available` };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Build the relaunch prompt, passing captured gate output if available (task-1387)
|
|
335
|
+
const prompt = buildRelaunchPromptFn(errorMsg, slug, worktree, gateOutput);
|
|
335
336
|
|
|
336
337
|
log(`Attempting to relaunch ${fmt.agent(agent)} to fix repairable handoff error...`);
|
|
337
338
|
// startAgent handles resume flags internally for resume-capable agents (codex, claude, gemini, custom)
|
|
@@ -430,7 +431,7 @@ async function runHandoffAndReview(slug, worktree, agent, options = {}) {
|
|
|
430
431
|
validateCheckpointsBeforeHandoffFn = validateCheckpointsBeforeHandoff,
|
|
431
432
|
performHandoff: _performHandoff = (/** @type{string} */ s, /** @type{object} */ o) => handoff.performHandoff(s, o),
|
|
432
433
|
startReviewLoop: _startReviewLoop = (/** @type{string} */ s, /** @type{object} */ o) => review.startReviewLoop(s, o),
|
|
433
|
-
repairHandoffFn = /** @type{(s: string, w: string, e: string, o: object) => Promise<{repaired: boolean, blocker?: string}>} */(repairHandoff),
|
|
434
|
+
repairHandoffFn = /** @type{(s: string, w: string, e: string, o: object) => Promise<{repaired: boolean, blocker?: string}>} */(repairHandoff.default),
|
|
434
435
|
attemptAgentRelaunchFn = attemptAgentRelaunch,
|
|
435
436
|
log = fmt.log.plain,
|
|
436
437
|
error = fmt.log.plainError
|
|
@@ -449,37 +450,73 @@ async function runHandoffAndReview(slug, worktree, agent, options = {}) {
|
|
|
449
450
|
let handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree });
|
|
450
451
|
|
|
451
452
|
if (!handoffResult.ok) {
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
453
|
+
// Check for genuine gate failure (task-1387): automatic relaunch with captured output
|
|
454
|
+
const isGenuineGateFailure = handoffResult.gateOutput ||
|
|
455
|
+
(handoffResult.error && (
|
|
456
|
+
/verification gate failed/i.test(handoffResult.error) ||
|
|
457
|
+
(/\bdeclared gate\b/i.test(handoffResult.error) && /\bfailed\b/i.test(handoffResult.error))
|
|
458
|
+
));
|
|
459
|
+
|
|
460
|
+
if (isGenuineGateFailure) {
|
|
461
|
+
// Automatic relaunch with captured gate output, bounded to max 2 attempts
|
|
462
|
+
let relaunchCount = 0;
|
|
463
|
+
const maxRelaunches = 2;
|
|
464
|
+
|
|
465
|
+
while (relaunchCount < maxRelaunches) {
|
|
466
|
+
relaunchCount++;
|
|
467
|
+
log(`\nGenuine gate failure detected. Relaunch attempt ${relaunchCount}/${maxRelaunches}...`);
|
|
468
|
+
const { relaunched, error: relaunchError } = await attemptAgentRelaunchFn(
|
|
469
|
+
slug, worktree, /** @type{string} */(handoffResult.error), agent,
|
|
470
|
+
{ log, error, gateOutput: handoffResult.gateOutput }
|
|
471
|
+
);
|
|
472
|
+
if (relaunched) {
|
|
473
|
+
handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree, force: true });
|
|
474
|
+
if (handoffResult.ok) {
|
|
475
|
+
break; // Success — proceed to review loop
|
|
476
|
+
}
|
|
477
|
+
// Handoff still failed; continue loop for another relaunch attempt
|
|
478
|
+
} else {
|
|
479
|
+
log(`Agent relaunch failed: ${relaunchError || 'unknown error'}`);
|
|
480
|
+
break; // Relaunch itself failed; stop
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!handoffResult.ok && relaunchCount >= maxRelaunches) {
|
|
485
|
+
handoffResult.error = `Gate failure persisting after ${maxRelaunches} relaunch attempts. Manual intervention required.`;
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
// Original logic: attempt single repair for routine hygiene issues (dirty artifacts, rebase needed)
|
|
489
|
+
log(`\nAutomated handoff failed: ${handoffResult.error}`);
|
|
490
|
+
log(`Attempting post-execute repair...`);
|
|
491
|
+
const { repaired, blocker } = await /** @type{Function} */(repairHandoffFn)(slug, worktree, /** @type{string} */(handoffResult.error), { taskFile, log, error });
|
|
492
|
+
if (repaired) {
|
|
493
|
+
log(`Repair successful. Retrying automated handoff...`);
|
|
474
494
|
handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree, force: true });
|
|
475
|
-
|
|
476
|
-
|
|
495
|
+
} else if (blocker) {
|
|
496
|
+
// If repair failed but provided a specific blocker (e.g. rebase failure),
|
|
497
|
+
// report that blocker as the final error instead of the original handoff error.
|
|
498
|
+
handoffResult.error = blocker;
|
|
499
|
+
} else if (!repaired && repairHandoff.isRelaunchableError(handoffResult.error)) {
|
|
500
|
+
// Attempt agent relaunch for repairable content errors (missing goal-check table)
|
|
501
|
+
log(`Content error detected. Attempting agent relaunch to fix...`);
|
|
502
|
+
const { relaunched, error: relaunchError } = await attemptAgentRelaunchFn(
|
|
503
|
+
slug, worktree, /** @type{string} */(handoffResult.error), agent, { log, error }
|
|
504
|
+
);
|
|
505
|
+
if (relaunched) {
|
|
506
|
+
// Agent was relaunched successfully; re-invoke performHandoff to verify
|
|
507
|
+
// the handoff-to-review transition actually completed, matching the
|
|
508
|
+
// contract of the repair-success path above.
|
|
509
|
+
log(`Agent relaunched. It will fix the checkpoint and retry handoff.`);
|
|
510
|
+
handoffResult = await _performHandoff(slug, { forgejoUser: agent, worktree, force: true });
|
|
511
|
+
if (!handoffResult.ok) {
|
|
512
|
+
handoffResult.error = `Post-relaunch handoff failed: ${handoffResult.error || 'unknown'}`;
|
|
513
|
+
}
|
|
514
|
+
// Fall through to gatekeeper pushback / review loop / failure handling below.
|
|
515
|
+
} else {
|
|
516
|
+
// Relaunch failed or was not possible
|
|
517
|
+
log(`Agent relaunch failed: ${relaunchError || 'unknown error'}`);
|
|
518
|
+
// Fall through to manual handoff message
|
|
477
519
|
}
|
|
478
|
-
// Fall through to gatekeeper pushback / review loop / failure handling below.
|
|
479
|
-
} else {
|
|
480
|
-
// Relaunch failed or was not possible
|
|
481
|
-
log(`Agent relaunch failed: ${relaunchError || 'unknown error'}`);
|
|
482
|
-
// Fall through to manual handoff message
|
|
483
520
|
}
|
|
484
521
|
}
|
|
485
522
|
}
|
package/lib/commands/config.ts
CHANGED
|
@@ -4,9 +4,9 @@ import * as fmt from '../core/fmt.js';
|
|
|
4
4
|
import { loadEffectiveConfig, loadWorkflowConfig, validateWorkflowConfig } from '../core/product-config.js';
|
|
5
5
|
|
|
6
6
|
interface ConfigOptions {
|
|
7
|
-
logFn?: (
|
|
8
|
-
errorFn?: (
|
|
9
|
-
exitFn?: (
|
|
7
|
+
logFn?: (_msg: string) => void;
|
|
8
|
+
errorFn?: (_msg: string) => void;
|
|
9
|
+
exitFn?: (_code: number) => void;
|
|
10
10
|
rootDir?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -301,7 +301,7 @@ if (typeof require !== 'undefined' && require.main === module) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
interface CoverageGateOptions {
|
|
304
|
-
exitFn?: (
|
|
304
|
+
exitFn?: (_code: number) => void;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
function run(args: string[], options: CoverageGateOptions = {}) {
|
package/lib/commands/draft.js
CHANGED
|
@@ -468,7 +468,7 @@ function ensureWorktree(mainRepo, targetWorktree, branchName, { existsFn = fs.ex
|
|
|
468
468
|
try {
|
|
469
469
|
gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
|
|
470
470
|
}
|
|
471
|
-
catch (
|
|
471
|
+
catch (_error) {
|
|
472
472
|
// Ignore "already exists" style failures; the directory is already usable.
|
|
473
473
|
}
|
|
474
474
|
return;
|
package/lib/commands/draft.ts
CHANGED
|
@@ -488,7 +488,7 @@ function ensureWorktree(mainRepo, targetWorktree, branchName, {
|
|
|
488
488
|
logFn(fmt.status('PASS', `Worktree directory ${fmt.path(targetWorktree)} already exists.`));
|
|
489
489
|
try {
|
|
490
490
|
gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
|
|
491
|
-
} catch (
|
|
491
|
+
} catch (_error) {
|
|
492
492
|
// Ignore "already exists" style failures; the directory is already usable.
|
|
493
493
|
}
|
|
494
494
|
return;
|
package/lib/commands/handoff.js
CHANGED
|
@@ -94,9 +94,9 @@ function verifyHandoff(slug, options = {}) {
|
|
|
94
94
|
* @returns {Promise<{ ok: boolean, error?: string, gatekeeperPushedBack?: boolean }>}
|
|
95
95
|
*/
|
|
96
96
|
async function performHandoff(slug, options = {}) {
|
|
97
|
-
/** @type{{skipGate?: boolean, worktree?: string|null, force?: boolean, forceWithLease?: boolean, log?: Function, error?: Function, rebaseFn?: Function, isForgejoReviewEnabledFn?: Function}} */
|
|
97
|
+
/** @type{{skipGate?: boolean, worktree?: string|null, force?: boolean, forceWithLease?: boolean, log?: Function, error?: Function, rebaseFn?: Function, isForgejoReviewEnabledFn?: Function, runVerificationGateFn?: Function}} */
|
|
98
98
|
const opts = options;
|
|
99
|
-
const { skipGate = false, worktree = null, force = false, forceWithLease = true, log = fmt.log.info, error = fmt.log.fail, rebaseFn = rebase_js_1.rebaseBeforeReviewRound } = opts;
|
|
99
|
+
const { skipGate = false, worktree = null, force = false, forceWithLease = true, log = fmt.log.info, error = fmt.log.fail, rebaseFn = rebase_js_1.rebaseBeforeReviewRound, runVerificationGateFn = verification_js_1.runVerificationGate } = opts;
|
|
100
100
|
const verification = verifyHandoff(slug, { worktree: worktree || undefined });
|
|
101
101
|
if (!verification.ok) {
|
|
102
102
|
error(verification.error);
|
|
@@ -213,15 +213,17 @@ async function performHandoff(slug, options = {}) {
|
|
|
213
213
|
}
|
|
214
214
|
else {
|
|
215
215
|
log(`Step 1: Running final verification gate for area: ${fmt.bold(area || 'docs')}...`);
|
|
216
|
-
const verifyResult = (
|
|
216
|
+
const verifyResult = runVerificationGateFn(area || 'docs', {
|
|
217
217
|
rootDir,
|
|
218
|
-
stdio: '
|
|
218
|
+
stdio: 'pipe',
|
|
219
219
|
runFn: git.run
|
|
220
220
|
});
|
|
221
221
|
if (verifyResult.status !== 0) {
|
|
222
|
+
const stdout = (verifyResult.stdout || '').trim();
|
|
223
|
+
const stderr = (verifyResult.stderr || '').trim();
|
|
222
224
|
const msg = 'Final verification gate failed. Fix errors before submitting or use --no-gate if appropriate.';
|
|
223
225
|
error(msg);
|
|
224
|
-
return { ok: false, error: msg };
|
|
226
|
+
return { ok: false, error: msg, gateOutput: { stdout, stderr } };
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
// Step 1.5: Rebase mission branch onto latest primary before PR creation
|
|
@@ -356,7 +358,7 @@ async function performHandoff(slug, options = {}) {
|
|
|
356
358
|
if (!gatesResult.ok) {
|
|
357
359
|
const msg = `Declared gate "${gatesResult.gate}" failed for ${fmt.slug(slug)}: ${gatesResult.error || gatesResult.reason}. Blocking handoff — task remains in active.`;
|
|
358
360
|
error(msg);
|
|
359
|
-
return { ok: false, error: msg };
|
|
361
|
+
return { ok: false, error: msg, gateOutput: { stdout: (gatesResult.stdout || ''), stderr: (gatesResult.stderr || '') } };
|
|
360
362
|
}
|
|
361
363
|
if (gatesResult.skipped) {
|
|
362
364
|
log(`No declared gates for ${fmt.slug(slug)} (${gatesResult.reason}).`);
|
|
@@ -464,15 +466,18 @@ function runDeclaredGates(missionDir, rootDir, options = {}) {
|
|
|
464
466
|
const result = (0, node_child_process_1.spawnSync)('bash', ['-c', cmd], {
|
|
465
467
|
cwd: rootDir,
|
|
466
468
|
encoding: 'utf8',
|
|
467
|
-
stdio:
|
|
469
|
+
stdio: 'pipe'
|
|
468
470
|
});
|
|
469
471
|
if (result.status !== 0) {
|
|
472
|
+
const stdout = (result.stdout || '').trim();
|
|
470
473
|
const stderr = (result.stderr || '').trim();
|
|
471
474
|
return {
|
|
472
475
|
ok: false,
|
|
473
476
|
gate: cmd,
|
|
474
477
|
reason: 'gate-failed',
|
|
475
|
-
error: stderr || `Gate exited with status ${result.status}
|
|
478
|
+
error: stderr || `Gate exited with status ${result.status}`,
|
|
479
|
+
stdout,
|
|
480
|
+
stderr
|
|
476
481
|
};
|
|
477
482
|
}
|
|
478
483
|
}
|
package/lib/commands/handoff.ts
CHANGED
|
@@ -58,18 +58,19 @@ import * as nels from '../core/nels.js';
|
|
|
58
58
|
* @param {{skipGate?: boolean, worktree?: string|null, force?: boolean, forceWithLease?: boolean, log?: Function, error?: Function, rebaseFn?: Function, isForgejoReviewEnabledFn?: Function}} [options]
|
|
59
59
|
* @returns {Promise<{ ok: boolean, error?: string, gatekeeperPushedBack?: boolean }>}
|
|
60
60
|
*/
|
|
61
|
-
|
|
62
|
-
/** @type{{skipGate?: boolean, worktree?: string|null, force?: boolean, forceWithLease?: boolean, log?: Function, error?: Function, rebaseFn?: Function, isForgejoReviewEnabledFn?: Function}} */
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
async function performHandoff(slug, options = {}) {
|
|
62
|
+
/** @type{{skipGate?: boolean, worktree?: string|null, force?: boolean, forceWithLease?: boolean, log?: Function, error?: Function, rebaseFn?: Function, isForgejoReviewEnabledFn?: Function, runVerificationGateFn?: Function}} */
|
|
63
|
+
const opts = options;
|
|
64
|
+
const {
|
|
65
|
+
skipGate = false,
|
|
66
|
+
worktree = null,
|
|
67
|
+
force = false,
|
|
68
|
+
forceWithLease = true,
|
|
69
|
+
log = fmt.log.info,
|
|
70
|
+
error = fmt.log.fail,
|
|
71
|
+
rebaseFn = rebaseBeforeReviewRound,
|
|
72
|
+
runVerificationGateFn = runVerificationGate
|
|
73
|
+
} = opts;
|
|
73
74
|
|
|
74
75
|
const verification = verifyHandoff(slug, { worktree: worktree || undefined });
|
|
75
76
|
if (!verification.ok) {
|
|
@@ -198,15 +199,17 @@ import * as nels from '../core/nels.js';
|
|
|
198
199
|
fmt.log.warn('Step 1: Skipping final verification gate (--no-gate)');
|
|
199
200
|
} else {
|
|
200
201
|
log(`Step 1: Running final verification gate for area: ${fmt.bold(area || 'docs')}...`);
|
|
201
|
-
const verifyResult =
|
|
202
|
+
const verifyResult = runVerificationGateFn(area || 'docs', {
|
|
202
203
|
rootDir,
|
|
203
|
-
stdio: '
|
|
204
|
+
stdio: 'pipe',
|
|
204
205
|
runFn: git.run
|
|
205
206
|
});
|
|
206
207
|
if (verifyResult.status !== 0) {
|
|
208
|
+
const stdout = (verifyResult.stdout || '').trim();
|
|
209
|
+
const stderr = (verifyResult.stderr || '').trim();
|
|
207
210
|
const msg = 'Final verification gate failed. Fix errors before submitting or use --no-gate if appropriate.';
|
|
208
211
|
error(msg);
|
|
209
|
-
return { ok: false, error: msg };
|
|
212
|
+
return { ok: false, error: msg, gateOutput: { stdout, stderr } };
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -346,7 +349,7 @@ import * as nels from '../core/nels.js';
|
|
|
346
349
|
if (!gatesResult.ok) {
|
|
347
350
|
const msg = `Declared gate "${gatesResult.gate}" failed for ${fmt.slug(slug)}: ${gatesResult.error || gatesResult.reason}. Blocking handoff — task remains in active.`;
|
|
348
351
|
error(msg);
|
|
349
|
-
return { ok: false, error: msg };
|
|
352
|
+
return { ok: false, error: msg, gateOutput: { stdout: (gatesResult.stdout || ''), stderr: (gatesResult.stderr || '') } };
|
|
350
353
|
}
|
|
351
354
|
if (gatesResult.skipped) {
|
|
352
355
|
log(`No declared gates for ${fmt.slug(slug)} (${gatesResult.reason}).`);
|
|
@@ -463,16 +466,19 @@ function runDeclaredGates(missionDir, rootDir, options = {}) {
|
|
|
463
466
|
const result = spawnSync('bash', ['-c', cmd], {
|
|
464
467
|
cwd: rootDir,
|
|
465
468
|
encoding: 'utf8',
|
|
466
|
-
stdio:
|
|
469
|
+
stdio: 'pipe'
|
|
467
470
|
});
|
|
468
471
|
|
|
469
472
|
if (result.status !== 0) {
|
|
473
|
+
const stdout = (result.stdout || '').trim();
|
|
470
474
|
const stderr = (result.stderr || '').trim();
|
|
471
475
|
return {
|
|
472
476
|
ok: false,
|
|
473
477
|
gate: cmd,
|
|
474
478
|
reason: 'gate-failed',
|
|
475
|
-
error: stderr || `Gate exited with status ${result.status}
|
|
479
|
+
error: stderr || `Gate exited with status ${result.status}`,
|
|
480
|
+
stdout,
|
|
481
|
+
stderr
|
|
476
482
|
};
|
|
477
483
|
}
|
|
478
484
|
}
|
package/lib/commands/rebase.js
CHANGED
|
@@ -58,7 +58,7 @@ const verification_js_1 = require("../core/verification.js");
|
|
|
58
58
|
* Usage: px rebase [<slug>] [--push]
|
|
59
59
|
*/
|
|
60
60
|
/** @param {string[]} args @param {{inferSlugFn?: Function, findMissionDirFn?: Function, findMissionAreaFn?: Function, getCurrentBranchFn?: Function, resolveConflictsFn?: Function, startAgentFn?: Function, createPrFn?: Function, readTokenFn?: Function, resolveForgejoUserFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, detectRebaseStateFn?: Function, resolveMissionBaseBranchFn?: Function, gitFn?: Function, exitFn?: Function, isForgejoReviewEnabledFn?: Function, fetchReviewBranchFn?: Function}} opts */
|
|
61
|
-
async function rebase(args, { inferSlugFn = mission_utils_js_1.inferSlug, findMissionDirFn = mission_utils_js_1.findMissionDir, findMissionAreaFn = mission_utils_js_1.findMissionArea, getCurrentBranchFn = git_js_1.getCurrentBranch, resolveConflictsFn = integrate_js_1.default.resolveConflictsForMission, startAgentFn = agents_js_1.startAgent, createPrFn = forgejo_js_1.createPr, readTokenFn = forgejo_js_1.readToken, resolveForgejoUserFn = forgejo_js_1.resolveForgejoUser, resolveTaskFileFn = backlog_js_1.resolveTaskFile, getTaskImplementerFn = backlog_js_1.getTaskImplementer, detectRebaseStateFn = git_js_1.detectRebaseState, resolveMissionBaseBranchFn = mission_utils_js_1.resolveMissionBaseBranch, gitFn = git_js_1.git, exitFn = ((
|
|
61
|
+
async function rebase(args, { inferSlugFn = mission_utils_js_1.inferSlug, findMissionDirFn = mission_utils_js_1.findMissionDir, findMissionAreaFn = mission_utils_js_1.findMissionArea, getCurrentBranchFn = git_js_1.getCurrentBranch, resolveConflictsFn = integrate_js_1.default.resolveConflictsForMission, startAgentFn = agents_js_1.startAgent, createPrFn = forgejo_js_1.createPr, readTokenFn = forgejo_js_1.readToken, resolveForgejoUserFn = forgejo_js_1.resolveForgejoUser, resolveTaskFileFn = backlog_js_1.resolveTaskFile, getTaskImplementerFn = backlog_js_1.getTaskImplementer, detectRebaseStateFn = git_js_1.detectRebaseState, resolveMissionBaseBranchFn = mission_utils_js_1.resolveMissionBaseBranch, gitFn = git_js_1.git, exitFn = ((_code) => process.exit(_code)), isForgejoReviewEnabledFn = product_config_js_1.isForgejoReviewEnabled, fetchReviewBranchFn = forgejo_js_1.fetchReviewBranch, } = {}) {
|
|
62
62
|
const flags = args.filter(a => a.startsWith('--'));
|
|
63
63
|
const params = args.filter(a => !a.startsWith('--'));
|
|
64
64
|
const isPush = flags.includes('--push');
|
package/lib/commands/rebase.ts
CHANGED
|
@@ -32,10 +32,10 @@ async function rebase(args: string[], {
|
|
|
32
32
|
detectRebaseStateFn = detectRebaseState,
|
|
33
33
|
resolveMissionBaseBranchFn = resolveMissionBaseBranch,
|
|
34
34
|
gitFn = git,
|
|
35
|
-
exitFn = ((
|
|
35
|
+
exitFn = ((_code: number) => process.exit(_code)) as (_code: number) => void,
|
|
36
36
|
isForgejoReviewEnabledFn = isForgejoReviewEnabled,
|
|
37
37
|
fetchReviewBranchFn = fetchReviewBranch,
|
|
38
|
-
}: {inferSlugFn?: Function, findMissionDirFn?: Function, findMissionAreaFn?: Function, getCurrentBranchFn?: Function, resolveConflictsFn?: Function, startAgentFn?: Function, createPrFn?: Function, readTokenFn?: Function, resolveForgejoUserFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, detectRebaseStateFn?: Function, resolveMissionBaseBranchFn?: Function, gitFn?: Function, exitFn?: (
|
|
38
|
+
}: {inferSlugFn?: Function, findMissionDirFn?: Function, findMissionAreaFn?: Function, getCurrentBranchFn?: Function, resolveConflictsFn?: Function, startAgentFn?: Function, createPrFn?: Function, readTokenFn?: Function, resolveForgejoUserFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, detectRebaseStateFn?: Function, resolveMissionBaseBranchFn?: Function, gitFn?: Function, exitFn?: (_code: number) => void, isForgejoReviewEnabledFn?: Function, fetchReviewBranchFn?: Function} = {}) {
|
|
39
39
|
const flags = args.filter(a => a.startsWith('--'));
|
|
40
40
|
const params = args.filter(a => !a.startsWith('--'));
|
|
41
41
|
const isPush = flags.includes('--push');
|
|
@@ -36,6 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.DispatchAction = exports.FailureClass = void 0;
|
|
40
|
+
exports.getDispatchAction = getDispatchAction;
|
|
41
|
+
exports.classifyError = classifyError;
|
|
39
42
|
exports.repairHandoff = repairHandoff;
|
|
40
43
|
exports.isRelaunchableError = isRelaunchableError;
|
|
41
44
|
exports.buildRelaunchPrompt = buildRelaunchPrompt;
|
|
@@ -43,19 +46,129 @@ const git_js_1 = require("../core/git.js");
|
|
|
43
46
|
const missionUtils = __importStar(require("../core/mission-utils.js"));
|
|
44
47
|
const rebase_js_1 = __importDefault(require("./rebase.js"));
|
|
45
48
|
const fmt = __importStar(require("../core/fmt.js"));
|
|
49
|
+
// ── FailureClass: 8 classes from ADR 0048 ────────────────────────────────────
|
|
50
|
+
const FailureClass = {
|
|
51
|
+
UnverifiableClaims: 'UnverifiableClaims',
|
|
52
|
+
MalformedGates: 'MalformedGates',
|
|
53
|
+
MissingArtifacts: 'MissingArtifacts',
|
|
54
|
+
IncompleteEvidence: 'IncompleteEvidence',
|
|
55
|
+
GitBlockers: 'GitBlockers',
|
|
56
|
+
GateFailure: 'GateFailure',
|
|
57
|
+
InfraBlocker: 'InfraBlocker',
|
|
58
|
+
StateMachineViolation: 'StateMachineViolation',
|
|
59
|
+
};
|
|
60
|
+
exports.FailureClass = FailureClass;
|
|
61
|
+
// ── DispatchAction: 3 actions from ADR 0048 ──────────────────────────────────
|
|
62
|
+
const DispatchAction = {
|
|
63
|
+
AutoRepair: 'AutoRepair',
|
|
64
|
+
AutoSendBack: 'AutoSendBack',
|
|
65
|
+
HumanOnly: 'HumanOnly',
|
|
66
|
+
};
|
|
67
|
+
exports.DispatchAction = DispatchAction;
|
|
68
|
+
// ── Dispatch table: maps each failure class to its prescribed action (ADR 0048) ─
|
|
69
|
+
const DISPATCH_TABLE = {
|
|
70
|
+
[FailureClass.UnverifiableClaims]: DispatchAction.AutoSendBack,
|
|
71
|
+
[FailureClass.MalformedGates]: DispatchAction.AutoRepair,
|
|
72
|
+
[FailureClass.MissingArtifacts]: DispatchAction.AutoSendBack,
|
|
73
|
+
[FailureClass.IncompleteEvidence]: DispatchAction.AutoSendBack,
|
|
74
|
+
[FailureClass.GitBlockers]: DispatchAction.AutoRepair,
|
|
75
|
+
[FailureClass.GateFailure]: DispatchAction.AutoSendBack,
|
|
76
|
+
[FailureClass.InfraBlocker]: DispatchAction.HumanOnly,
|
|
77
|
+
[FailureClass.StateMachineViolation]: DispatchAction.HumanOnly,
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Look up the dispatch action for a given failure class.
|
|
81
|
+
*
|
|
82
|
+
* @param failureClass - One of the 8 failure class values from ADR 0048
|
|
83
|
+
* @returns The prescribed dispatch action, or null if unknown
|
|
84
|
+
*/
|
|
85
|
+
function getDispatchAction(failureClass) {
|
|
86
|
+
return DISPATCH_TABLE[failureClass] ?? null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Classify an error message into one of the 8 failure classes from ADR 0048
|
|
90
|
+
* and return the associated dispatch action.
|
|
91
|
+
*
|
|
92
|
+
* Patterns are checked in order of specificity to avoid collisions.
|
|
93
|
+
* Returns a `reason` field for GitBlockers to distinguish dirty-artifact from behind-branch errors,
|
|
94
|
+
* allowing callers to derive repair-strategy flags without duplicating pattern matching.
|
|
95
|
+
*
|
|
96
|
+
* @param errorMsg - The error message to classify
|
|
97
|
+
* @returns Object with failureClass, dispatchAction, and (for GitBlockers) reason
|
|
98
|
+
*/
|
|
99
|
+
function classifyError(errorMsg) {
|
|
100
|
+
if (!errorMsg || typeof errorMsg !== 'string') {
|
|
101
|
+
return { failureClass: FailureClass.InfraBlocker, dispatchAction: DispatchAction.HumanOnly };
|
|
102
|
+
}
|
|
103
|
+
// 1. IncompleteEvidence: goal-check table missing evidence rows (most specific — checked before generic gate patterns)
|
|
104
|
+
if (errorMsg.includes('has a "## Goal Check" section but no evidence rows') &&
|
|
105
|
+
errorMsg.includes('A goal-check table with real evidence is required before handoff')) {
|
|
106
|
+
return { failureClass: FailureClass.IncompleteEvidence, dispatchAction: DispatchAction.AutoSendBack };
|
|
107
|
+
}
|
|
108
|
+
// 2. GitBlockers: dirty/uncommitted mission artifacts (mechanical git blocker — auto-repairable)
|
|
109
|
+
if (errorMsg.includes('is modified but uncommitted') ||
|
|
110
|
+
errorMsg.includes('Commit the mission contract before handoff') ||
|
|
111
|
+
errorMsg.includes('Commit the implementation evidence before handoff')) {
|
|
112
|
+
return { failureClass: FailureClass.GitBlockers, dispatchAction: DispatchAction.AutoRepair, reason: 'dirty' };
|
|
113
|
+
}
|
|
114
|
+
// 3. GitBlockers: branch behind primary / push rejected (mechanical git blocker — auto-repairable via rebase)
|
|
115
|
+
if (errorMsg.includes('Updates were rejected') ||
|
|
116
|
+
errorMsg.includes('fetch first') ||
|
|
117
|
+
errorMsg.includes('non-fast-forward') ||
|
|
118
|
+
errorMsg.includes('behind its remote') ||
|
|
119
|
+
(errorMsg.includes('git push failed') && (errorMsg.includes('rejected') ||
|
|
120
|
+
errorMsg.includes('remote contains work')))) {
|
|
121
|
+
return { failureClass: FailureClass.GitBlockers, dispatchAction: DispatchAction.AutoRepair, reason: 'behind' };
|
|
122
|
+
}
|
|
123
|
+
// 4. GateFailure: verification gate failed
|
|
124
|
+
if (/verification gate failed/i.test(errorMsg)) {
|
|
125
|
+
return { failureClass: FailureClass.GateFailure, dispatchAction: DispatchAction.AutoSendBack };
|
|
126
|
+
}
|
|
127
|
+
// 5. GateFailure: declared gate failed
|
|
128
|
+
if (/\bdeclared gate\b/i.test(errorMsg) && /\bfailed\b/i.test(errorMsg)) {
|
|
129
|
+
return { failureClass: FailureClass.GateFailure, dispatchAction: DispatchAction.AutoSendBack };
|
|
130
|
+
}
|
|
131
|
+
// 6. UnverifiableClaims: test claims that cannot be verified
|
|
132
|
+
if (/test(s?\s+)?passed/i.test(errorMsg) && /cannot\s+verify|unverifiable|proof\s+(not\s+)?found|stale\s+proof/i.test(errorMsg)) {
|
|
133
|
+
return { failureClass: FailureClass.UnverifiableClaims, dispatchAction: DispatchAction.AutoSendBack };
|
|
134
|
+
}
|
|
135
|
+
// 7. MalformedGates: malformed or non-runnable declared gates
|
|
136
|
+
if (/malformed\s+gate|invalid\s+gate\s+config|gate\s+command\s+(not\s+found|syntax\s+error|not\s+runnable)/i.test(errorMsg) ||
|
|
137
|
+
(/gate/i.test(errorMsg) && /syntax\s+error|not\s+found|missing\s+(file|command)/i.test(errorMsg))) {
|
|
138
|
+
return { failureClass: FailureClass.MalformedGates, dispatchAction: DispatchAction.AutoRepair };
|
|
139
|
+
}
|
|
140
|
+
// 8. MissingArtifacts: mandatory mission artifacts missing
|
|
141
|
+
if (/mandatory\s+(artifact|file|document)|missing\s+(mission\s+)?(artifact|file|document)|required\s+(artifact|file|document)\s+(not\s+)?found/i.test(errorMsg) ||
|
|
142
|
+
(/gatekeeper/i.test(errorMsg) && /missing\s+(artifact|file|document)/i.test(errorMsg))) {
|
|
143
|
+
return { failureClass: FailureClass.MissingArtifacts, dispatchAction: DispatchAction.AutoSendBack };
|
|
144
|
+
}
|
|
145
|
+
// 9. StateMachineViolation: task state machine violations
|
|
146
|
+
if (/state\s+violation|invalid\s+state|transition\s+not\s+allowed|cannot\s+(move|transition)\s+(from|to)\s+\w+\s+(to|from)/i.test(errorMsg) ||
|
|
147
|
+
(/task\s+state/i.test(errorMsg) && /invalid|violation|incorrect/i.test(errorMsg))) {
|
|
148
|
+
return { failureClass: FailureClass.StateMachineViolation, dispatchAction: DispatchAction.HumanOnly };
|
|
149
|
+
}
|
|
150
|
+
// 10. InfraBlocker: forgejo/infrastructure blockers
|
|
151
|
+
if (/forgejo|infrastructure|authentication\s+failed|token\s+(expired|invalid|missing)|forbidden|unauthorized\s+(access|request)|rate\s+limit|connection\s+(refused|timed?\s*out)|network\s+error/i.test(errorMsg)) {
|
|
152
|
+
return { failureClass: FailureClass.InfraBlocker, dispatchAction: DispatchAction.HumanOnly };
|
|
153
|
+
}
|
|
154
|
+
// Default: human-only for unrecognized errors
|
|
155
|
+
return { failureClass: FailureClass.InfraBlocker, dispatchAction: DispatchAction.HumanOnly };
|
|
156
|
+
}
|
|
46
157
|
/**
|
|
47
158
|
* Check if an error message indicates a relaunchable content error (missing/empty goal-check table).
|
|
159
|
+
* Delegates to classifyError for backward-compatible classification.
|
|
48
160
|
*
|
|
49
|
-
* @param
|
|
50
|
-
* @returns
|
|
161
|
+
* @param errorMsg - The error message to check
|
|
162
|
+
* @returns True if the error is relaunchable (IncompleteEvidence or GateFailure)
|
|
51
163
|
*/
|
|
52
164
|
function isRelaunchableError(errorMsg) {
|
|
53
165
|
if (!errorMsg || typeof errorMsg !== 'string') {
|
|
54
166
|
return false;
|
|
55
167
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
168
|
+
const { failureClass } = classifyError(errorMsg);
|
|
169
|
+
// IncompleteEvidence and GateFailure are the only classes that were relaunchable under the old logic
|
|
170
|
+
return failureClass === FailureClass.IncompleteEvidence
|
|
171
|
+
|| failureClass === FailureClass.GateFailure;
|
|
59
172
|
}
|
|
60
173
|
/**
|
|
61
174
|
* Build a relaunch prompt for an agent to fix a relaunchable error.
|
|
@@ -65,10 +178,10 @@ function isRelaunchableError(errorMsg) {
|
|
|
65
178
|
* @param {string} worktree - Path to the mission worktree
|
|
66
179
|
* @returns {string} The relaunch prompt
|
|
67
180
|
*/
|
|
68
|
-
function buildRelaunchPrompt(errorMsg, slug, worktree) {
|
|
181
|
+
function buildRelaunchPrompt(errorMsg, slug, worktree, gateOutput) {
|
|
69
182
|
const year = missionUtils.getMissionYear(slug, worktree);
|
|
70
183
|
const missionDir = missionUtils.findMissionDir(slug, worktree) || missionUtils.missionDirForSlug(worktree, slug);
|
|
71
|
-
|
|
184
|
+
let prompt = `Automated handoff failed for mission ${slug} with a repairable error: ${errorMsg}
|
|
72
185
|
|
|
73
186
|
` +
|
|
74
187
|
`Please fix the final checkpoint document in ${missionDir} by adding a Goal Check table ` +
|
|
@@ -103,6 +216,16 @@ function buildRelaunchPrompt(errorMsg, slug, worktree) {
|
|
|
103
216
|
|
|
104
217
|
` +
|
|
105
218
|
`Do NOT add placeholder or generic evidence. Each row must cite real, verifiable artifacts.`;
|
|
219
|
+
// Append captured gate output if available (task-1387)
|
|
220
|
+
if (gateOutput && (gateOutput.stdout || gateOutput.stderr)) {
|
|
221
|
+
const totalOutput = (gateOutput.stdout || '') + (gateOutput.stderr || '');
|
|
222
|
+
// Truncate if total output exceeds 16000 chars; keep last 8000 chars
|
|
223
|
+
const truncated = totalOutput.length > 16000
|
|
224
|
+
? `[truncated — total ${totalOutput.length} chars, showing last 8000]\n` + totalOutput.slice(-8000)
|
|
225
|
+
: totalOutput;
|
|
226
|
+
prompt += `\n\n--- Captured Gate Output ---\n${truncated}`;
|
|
227
|
+
}
|
|
228
|
+
return prompt;
|
|
106
229
|
}
|
|
107
230
|
/**
|
|
108
231
|
* Attempt to repair a failed automated handoff by auto-committing mission
|
|
@@ -129,23 +252,17 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
|
|
|
129
252
|
const cleanPath = (pathPart.startsWith('"') && pathPart.endsWith('"')) ? pathPart.slice(1, -1) : pathPart;
|
|
130
253
|
return { xy, file: cleanPath };
|
|
131
254
|
}
|
|
132
|
-
// 0. Check if error is repairable
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const isBehind =
|
|
137
|
-
|
|
138
|
-
errorMsg.includes('non-fast-forward') ||
|
|
139
|
-
errorMsg.includes('behind its remote') ||
|
|
140
|
-
// Ensure we don't match generic git push failed unless it has non-fast-forward hints
|
|
141
|
-
(errorMsg.includes('git push failed') && (errorMsg.includes('rejected') ||
|
|
142
|
-
errorMsg.includes('remote contains work'))));
|
|
143
|
-
if (!isDirtyError && !isBehind) {
|
|
255
|
+
// 0. Check if error is repairable via classifyError
|
|
256
|
+
const classification = classifyError(errorMsg);
|
|
257
|
+
const isGitBlocker = classification.failureClass === FailureClass.GitBlockers;
|
|
258
|
+
// Derive isBehind from classification result (avoids duplicating classifyError's behind-branch patterns)
|
|
259
|
+
const isBehind = classification.reason === 'behind';
|
|
260
|
+
if (!isGitBlocker) {
|
|
144
261
|
log(`Handoff error is not automatically repairable: ${errorMsg}`);
|
|
145
262
|
return { repaired: false, blocker: null };
|
|
146
263
|
}
|
|
147
264
|
// 1. Auto-commit mission artifacts if uncommitted
|
|
148
|
-
if (
|
|
265
|
+
if (isGitBlocker) {
|
|
149
266
|
const statusResult = gitFn(['-C', rootDir, 'status', '--porcelain']);
|
|
150
267
|
if (statusResult.status === 0 && statusResult.stdout) {
|
|
151
268
|
const dirtyLines = statusResult.stdout.split('\n')
|
|
@@ -236,6 +353,10 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
|
|
|
236
353
|
}
|
|
237
354
|
repairHandoff.isRelaunchableError = isRelaunchableError;
|
|
238
355
|
repairHandoff.buildRelaunchPrompt = buildRelaunchPrompt;
|
|
356
|
+
repairHandoff.classifyError = classifyError;
|
|
357
|
+
repairHandoff.getDispatchAction = getDispatchAction;
|
|
358
|
+
repairHandoff.FailureClass = FailureClass;
|
|
359
|
+
repairHandoff.DispatchAction = DispatchAction;
|
|
239
360
|
exports.default = repairHandoff;
|
|
240
361
|
if (typeof module !== 'undefined') {
|
|
241
362
|
module.exports = repairHandoff;
|