sequant 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -2
- package/dist/bin/cli.d.ts +1 -1
- package/dist/bin/cli.js +12 -2
- package/dist/bin/preflight.d.ts +21 -0
- package/dist/bin/preflight.js +45 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +2 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/force-push.md +34 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +117 -19
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +29 -0
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +100 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +24 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +285 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +202 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +287 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/test-quality-checklist.md +272 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +40 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +95 -11
- package/dist/marketplace/external_plugins/sequant/skills/references/shared/framework-gotchas.md +186 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +669 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/references/browser-testing-patterns.md +423 -0
- package/dist/marketplace/external_plugins/sequant/skills/upstream/SKILL.md +419 -0
- package/dist/src/commands/logs.js +6 -1
- package/dist/src/commands/run-display.d.ts +20 -0
- package/dist/src/commands/run-display.js +80 -1
- package/dist/src/commands/stats.js +47 -0
- package/dist/src/lib/assess-collision-detect.d.ts +19 -2
- package/dist/src/lib/assess-collision-detect.js +68 -4
- package/dist/src/lib/cli-ui/run-renderer.js +17 -9
- package/dist/src/lib/errors.d.ts +91 -0
- package/dist/src/lib/errors.js +118 -0
- package/dist/src/lib/manifest.js +1 -17
- package/dist/src/lib/version-check.d.ts +19 -0
- package/dist/src/lib/version-check.js +45 -5
- package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
- package/dist/src/lib/workflow/batch-executor.js +142 -24
- package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/chain-preflight.js +199 -0
- package/dist/src/lib/workflow/chain-resume.d.ts +116 -0
- package/dist/src/lib/workflow/chain-resume.js +166 -0
- package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
- package/dist/src/lib/workflow/dependency-markers.js +79 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +17 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +29 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +136 -8
- package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
- package/dist/src/lib/workflow/error-classifier.js +14 -1
- package/dist/src/lib/workflow/log-writer.d.ts +1 -1
- package/dist/src/lib/workflow/log-writer.js +6 -8
- package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
- package/dist/src/lib/workflow/metrics-schema.js +16 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +50 -0
- package/dist/src/lib/workflow/phase-executor.js +151 -17
- package/dist/src/lib/workflow/run-log-schema.d.ts +26 -0
- package/dist/src/lib/workflow/run-log-schema.js +52 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
- package/dist/src/lib/workflow/run-orchestrator.js +291 -30
- package/dist/src/lib/workflow/state-manager.d.ts +1 -0
- package/dist/src/lib/workflow/state-manager.js +6 -0
- package/dist/src/lib/workflow/state-schema.d.ts +3 -0
- package/dist/src/lib/workflow/state-schema.js +7 -0
- package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
- package/dist/src/lib/workflow/status-derivation.js +27 -0
- package/dist/src/lib/workflow/types.d.ts +40 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
- package/dist/src/lib/workflow/worktree-manager.js +103 -33
- package/dist/src/mcp/tools/run.d.ts +2 -0
- package/dist/src/mcp/tools/run.js +2 -0
- package/dist/src/ui/tui/theme.d.ts +18 -4
- package/dist/src/ui/tui/theme.js +18 -4
- package/package.json +5 -6
- package/templates/hooks/post-tool.sh +18 -3
- package/templates/hooks/pre-tool.sh +330 -57
- package/templates/scripts/cleanup-worktree.sh +103 -14
- package/templates/skills/_shared/references/force-push.md +34 -0
- package/templates/skills/assess/SKILL.md +117 -19
- package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
- package/templates/skills/exec/SKILL.md +29 -0
- package/templates/skills/fullsolve/SKILL.md +1 -1
- package/templates/skills/loop/SKILL.md +100 -2
- package/templates/skills/qa/SKILL.md +24 -0
- package/templates/skills/qa/references/anti-pattern-detection.md +285 -0
- package/templates/skills/qa/references/call-site-review.md +202 -0
- package/templates/skills/qa/references/quality-gates.md +287 -0
- package/templates/skills/qa/references/test-quality-checklist.md +272 -0
- package/templates/skills/qa/references/testing-requirements.md +40 -0
- package/templates/skills/qa/scripts/quality-checks.sh +95 -11
- package/templates/skills/references/shared/framework-gotchas.md +186 -0
- package/templates/skills/reflect/SKILL.md +27 -13
- package/templates/skills/reflect/references/documentation-tiers.md +80 -68
- package/templates/skills/reflect/references/phase-reflection.md +31 -15
- package/templates/skills/release/SKILL.md +669 -0
- package/templates/skills/spec/references/verification-criteria.md +1 -1
- package/templates/skills/test/references/browser-testing-patterns.md +423 -0
- package/templates/skills/upstream/SKILL.md +419 -0
|
@@ -10,7 +10,7 @@ import chalk from "chalk";
|
|
|
10
10
|
import { spawnSync } from "child_process";
|
|
11
11
|
import pLimit from "p-limit";
|
|
12
12
|
import { formatCoarseNowLine } from "./run-state.js";
|
|
13
|
-
import { detectDefaultBranch, ensureWorktrees, ensureWorktreesChain, getWorktreeDiffStats, } from "./worktree-manager.js";
|
|
13
|
+
import { detectDefaultBranch, ensureWorktrees, ensureWorktreesChain, getWorktreeDiffStats, rebaseOntoLocalBranch, } from "./worktree-manager.js";
|
|
14
14
|
import { LogWriter } from "./log-writer.js";
|
|
15
15
|
import { StateManager } from "./state-manager.js";
|
|
16
16
|
import { ShutdownManager } from "../shutdown.js";
|
|
@@ -33,14 +33,53 @@ function formatSignalLine(issue, pid, result) {
|
|
|
33
33
|
return ` Skipped signal for #${issue} (orchestrator mode)`;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a git ref (branch name or base branch) to its tip commit SHA in the
|
|
38
|
+
* current repo, or undefined if the ref does not exist. Used by the chain
|
|
39
|
+
* resume planner (#760) to reconstruct — and validate the existence of — a
|
|
40
|
+
* completed link's committed tip.
|
|
41
|
+
*/
|
|
42
|
+
function revParseRef(ref) {
|
|
43
|
+
const result = spawnSync("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
44
|
+
stdio: "pipe",
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
});
|
|
47
|
+
if (result.status !== 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const sha = result.stdout.trim();
|
|
51
|
+
return sha.length > 0 ? sha : undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True iff `worktreePath` has uncommitted changes (`git status --porcelain`,
|
|
55
|
+
* which already honours .gitignore). Used by the chain resume planner (#760) to
|
|
56
|
+
* reject a resume base whose checkpoint commit never landed.
|
|
57
|
+
*
|
|
58
|
+
* Returns false when the path is missing or not a worktree: an unreadable
|
|
59
|
+
* worktree can't be judged dirty, and the branch-tip check already covers the
|
|
60
|
+
* destroyed-link case.
|
|
61
|
+
*/
|
|
62
|
+
function isWorktreeDirty(worktreePath) {
|
|
63
|
+
const result = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], {
|
|
64
|
+
stdio: "pipe",
|
|
65
|
+
encoding: "utf-8",
|
|
66
|
+
});
|
|
67
|
+
if (result.status !== 0) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return result.stdout.trim().length > 0;
|
|
71
|
+
}
|
|
36
72
|
import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, emitRunIdLine, } from "./batch-executor.js";
|
|
37
73
|
import { reconcileStateAtStartup } from "./state-utils.js";
|
|
74
|
+
import { runChainPreflight } from "./chain-preflight.js";
|
|
38
75
|
import { getCommitHash } from "./git-diff-utils.js";
|
|
76
|
+
import { planChainResumeFromState, } from "./chain-resume.js";
|
|
39
77
|
import { MetricsWriter } from "./metrics-writer.js";
|
|
40
78
|
import { WorkflowEventEmitter } from "./event-emitter.js";
|
|
41
79
|
import { determineOutcome } from "./metrics-schema.js";
|
|
42
80
|
import { getTokenUsageForRun } from "./token-utils.js";
|
|
43
81
|
import { resolveRunOptions, buildExecutionConfig } from "./config-resolver.js";
|
|
82
|
+
import { pipelineHasFailed } from "./status-derivation.js";
|
|
44
83
|
/**
|
|
45
84
|
* Build the stack-manifest line emitted into PR bodies under --stacked.
|
|
46
85
|
*
|
|
@@ -218,8 +257,15 @@ export class RunOrchestrator {
|
|
|
218
257
|
const durationSec = elapsedMs !== undefined ? Math.round(elapsedMs / 1000) : undefined;
|
|
219
258
|
if (event === "failed") {
|
|
220
259
|
const prev = state.status;
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
// #766: a loop-phase failure alone must not pin the issue `failed` — the
|
|
261
|
+
// outer quality loop may still recover on a later iteration. Derive from
|
|
262
|
+
// the phase slots (loop excluded) so only a genuine non-loop failure marks
|
|
263
|
+
// the issue failed; otherwise hold `running` until a later event reaches a
|
|
264
|
+
// terminal verdict (a subsequent iteration always follows a loop failure).
|
|
265
|
+
const nowFailed = pipelineHasFailed(state.phases);
|
|
266
|
+
state.status = nowFailed ? "failed" : "running";
|
|
267
|
+
if (nowFailed)
|
|
268
|
+
state.completedAt = new Date();
|
|
223
269
|
void this.emitter.emit("phase_failed", {
|
|
224
270
|
issueNumber: issue,
|
|
225
271
|
phase,
|
|
@@ -227,11 +273,11 @@ export class RunOrchestrator {
|
|
|
227
273
|
error: extra?.error ?? "unknown",
|
|
228
274
|
iteration: extra?.iteration,
|
|
229
275
|
});
|
|
230
|
-
if (prev !==
|
|
276
|
+
if (prev !== state.status) {
|
|
231
277
|
void this.emitter.emit("issue_status_changed", {
|
|
232
278
|
issueNumber: issue,
|
|
233
279
|
from: prev,
|
|
234
|
-
to:
|
|
280
|
+
to: state.status,
|
|
235
281
|
});
|
|
236
282
|
}
|
|
237
283
|
return;
|
|
@@ -246,9 +292,10 @@ export class RunOrchestrator {
|
|
|
246
292
|
const allDone = state.phases.every((ph) => ph.status === "done" || ph.status === "failed");
|
|
247
293
|
if (allDone) {
|
|
248
294
|
const prev = state.status;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
295
|
+
// #766: exclude a stale failed `loop` slot so a run that failed the loop
|
|
296
|
+
// on an early iteration and then recovered finalizes as `passed`, not
|
|
297
|
+
// `failed`. An unrecovered failure still leaves a non-loop phase failed.
|
|
298
|
+
state.status = pipelineHasFailed(state.phases) ? "failed" : "passed";
|
|
252
299
|
state.completedAt = new Date();
|
|
253
300
|
if (prev !== state.status) {
|
|
254
301
|
void this.emitter.emit("issue_status_changed", {
|
|
@@ -291,6 +338,8 @@ export class RunOrchestrator {
|
|
|
291
338
|
.map((i) => parseInt(i, 10))
|
|
292
339
|
.filter((n) => !isNaN(n));
|
|
293
340
|
}
|
|
341
|
+
// Capture the raw CLI order before dep-sorting (#762 pre-flight needs it).
|
|
342
|
+
const rawIssueOrder = [...issueNumbers];
|
|
294
343
|
if (issueNumbers.length > 1 && !resolvedBatches) {
|
|
295
344
|
issueNumbers = sortByDependencies(issueNumbers);
|
|
296
345
|
}
|
|
@@ -302,6 +351,7 @@ export class RunOrchestrator {
|
|
|
302
351
|
mergedOptions,
|
|
303
352
|
config,
|
|
304
353
|
issueNumbers,
|
|
354
|
+
rawIssueOrder,
|
|
305
355
|
batches: resolvedBatches,
|
|
306
356
|
baseBranch,
|
|
307
357
|
stack: manifest.stack,
|
|
@@ -392,29 +442,46 @@ export class RunOrchestrator {
|
|
|
392
442
|
logNonFatalWarning(" ! State reconciliation failed, continuing...", error, config.verbose);
|
|
393
443
|
}
|
|
394
444
|
}
|
|
445
|
+
let chainResume;
|
|
395
446
|
if (stateManager && !config.dryRun && !mergedOptions.force) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
447
|
+
if (mergedOptions.chain) {
|
|
448
|
+
// ── Chain-aware resume (#760) ──────────────────────────────────
|
|
449
|
+
// Don't just drop completed links (chain-unaware skip leaves the
|
|
450
|
+
// first incomplete link at index 0, where the #748 successor-rebase
|
|
451
|
+
// never fires → it silently builds on main). Instead compute a
|
|
452
|
+
// chain-correct plan that resumes at the first incomplete link,
|
|
453
|
+
// rebased onto the completed prefix's committed tip.
|
|
454
|
+
const resolver = {
|
|
455
|
+
resolveBranchTip: (branch) => revParseRef(branch),
|
|
456
|
+
resolveBaseTip: () => revParseRef(baseBranch),
|
|
457
|
+
isWorktreeDirty,
|
|
458
|
+
};
|
|
459
|
+
const plan = await planChainResumeFromState(issueNumbers, baseBranch, (issueNumber) => stateManager.getIssueState(issueNumber), resolver, (issueNumber, error) => logNonFatalWarning(` ! State lookup failed for #${issueNumber}, treating as incomplete...`, error, config.verbose));
|
|
460
|
+
if (plan.failFast) {
|
|
461
|
+
console.log(chalk.red(`\n ❌ Chain resume aborted: ${plan.failFast}`));
|
|
462
|
+
shutdown.dispose();
|
|
463
|
+
return {
|
|
464
|
+
results: [
|
|
465
|
+
{
|
|
466
|
+
issueNumber: plan.resumeIssue ?? issueNumbers[0],
|
|
467
|
+
success: false,
|
|
468
|
+
phaseResults: [],
|
|
469
|
+
durationSeconds: 0,
|
|
470
|
+
loopTriggered: false,
|
|
471
|
+
abortReason: `chain resume aborted: ${plan.failFast}`,
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
logPath: null,
|
|
475
|
+
exitCode: 1,
|
|
476
|
+
worktreeMap: new Map(),
|
|
477
|
+
issueInfoMap: new Map(),
|
|
478
|
+
config,
|
|
479
|
+
mergedOptions,
|
|
480
|
+
logWriter: null,
|
|
481
|
+
};
|
|
412
482
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
issueNumbers = activeIssues;
|
|
416
|
-
if (issueNumbers.length === 0) {
|
|
417
|
-
console.log(chalk.yellow(`\n All issues already completed. Use --force to re-run.`));
|
|
483
|
+
if (plan.allComplete) {
|
|
484
|
+
console.log(chalk.yellow(`\n All chain links already completed. Use --force to re-run.`));
|
|
418
485
|
shutdown.dispose();
|
|
419
486
|
return {
|
|
420
487
|
results: [],
|
|
@@ -427,6 +494,56 @@ export class RunOrchestrator {
|
|
|
427
494
|
logWriter: null,
|
|
428
495
|
};
|
|
429
496
|
}
|
|
497
|
+
if (plan.skipped.length > 0) {
|
|
498
|
+
chainResume = plan;
|
|
499
|
+
issueNumbers = plan.active;
|
|
500
|
+
const resumeAt = plan.resumeBaseCommit
|
|
501
|
+
? plan.resumeBaseCommit.slice(0, 8)
|
|
502
|
+
: (plan.resumeBase ?? "base");
|
|
503
|
+
for (const s of plan.skipped) {
|
|
504
|
+
console.log(chalk.yellow(` ! #${s.issueNumber}: already ${s.status} — skipping (use --force to re-run)`));
|
|
505
|
+
}
|
|
506
|
+
console.log(chalk.cyan(` ↻ Resuming chain at #${plan.resumeIssue} from ${resumeAt}` +
|
|
507
|
+
` (${plan.resumeBase})`));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
// ── Non-chain skip guard (unchanged) ───────────────────────────
|
|
512
|
+
const activeIssues = [];
|
|
513
|
+
for (const issueNumber of issueNumbers) {
|
|
514
|
+
try {
|
|
515
|
+
const issueState = await stateManager.getIssueState(issueNumber);
|
|
516
|
+
if (issueState &&
|
|
517
|
+
(issueState.status === "ready_for_merge" ||
|
|
518
|
+
issueState.status === "merged")) {
|
|
519
|
+
console.log(chalk.yellow(` ! #${issueNumber}: already ${issueState.status} — skipping (use --force to re-run)`));
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
activeIssues.push(issueNumber);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
logNonFatalWarning(` ! State lookup failed for #${issueNumber}, including anyway...`, error, config.verbose);
|
|
527
|
+
activeIssues.push(issueNumber);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (activeIssues.length < issueNumbers.length) {
|
|
531
|
+
issueNumbers = activeIssues;
|
|
532
|
+
if (issueNumbers.length === 0) {
|
|
533
|
+
console.log(chalk.yellow(`\n All issues already completed. Use --force to re-run.`));
|
|
534
|
+
shutdown.dispose();
|
|
535
|
+
return {
|
|
536
|
+
results: [],
|
|
537
|
+
logPath: null,
|
|
538
|
+
exitCode: 0,
|
|
539
|
+
worktreeMap: new Map(),
|
|
540
|
+
issueInfoMap: new Map(),
|
|
541
|
+
config,
|
|
542
|
+
mergedOptions,
|
|
543
|
+
logWriter: null,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
430
547
|
}
|
|
431
548
|
}
|
|
432
549
|
// ── Concurrency lock (#625) ────────────────────────────────────────
|
|
@@ -486,6 +603,37 @@ export class RunOrchestrator {
|
|
|
486
603
|
};
|
|
487
604
|
}
|
|
488
605
|
}
|
|
606
|
+
// ── Chain content pre-flight (#762) ────────────────────────────────
|
|
607
|
+
// Warn-by-default; `--strict-preflight` turns warnings fatal. Runs BEFORE
|
|
608
|
+
// any worktree is provisioned. Uses the RAW CLI order (not the dep-sorted
|
|
609
|
+
// `issueNumbers`) so the dependency-order / file-overlap checks aren't
|
|
610
|
+
// masked by `sortByDependencies`, filtered to issues still active after the
|
|
611
|
+
// state guard / lock acquisition.
|
|
612
|
+
if (mergedOptions.chain && issueNumbers.length > 1) {
|
|
613
|
+
const activeSet = new Set(issueNumbers);
|
|
614
|
+
const preflightOrder = resolved.rawIssueOrder.filter((n) => activeSet.has(n));
|
|
615
|
+
const preflightWarnings = await runChainPreflight(preflightOrder);
|
|
616
|
+
if (preflightWarnings.length > 0) {
|
|
617
|
+
for (const w of preflightWarnings) {
|
|
618
|
+
bracketedConsoleLog(phasePauseHandle, chalk.yellow(` ⚠ ${w.message}`));
|
|
619
|
+
}
|
|
620
|
+
if (mergedOptions.strictPreflight) {
|
|
621
|
+
bracketedConsoleLog(phasePauseHandle, chalk.red(`\n ✖ --strict-preflight: ${preflightWarnings.length} pre-flight ` +
|
|
622
|
+
`warning(s) — aborting before any worktree is provisioned.`));
|
|
623
|
+
shutdown.dispose();
|
|
624
|
+
return {
|
|
625
|
+
results: [],
|
|
626
|
+
logPath: null,
|
|
627
|
+
exitCode: 1,
|
|
628
|
+
worktreeMap: new Map(),
|
|
629
|
+
issueInfoMap: new Map(),
|
|
630
|
+
config,
|
|
631
|
+
mergedOptions,
|
|
632
|
+
logWriter: null,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
489
637
|
// ── Issue info + worktree setup ────────────────────────────────────
|
|
490
638
|
const issueInfoMap = new Map();
|
|
491
639
|
for (const issueNumber of issueNumbers) {
|
|
@@ -499,7 +647,11 @@ export class RunOrchestrator {
|
|
|
499
647
|
title: issueInfoMap.get(num)?.title || `Issue #${num}`,
|
|
500
648
|
}));
|
|
501
649
|
if (mergedOptions.chain) {
|
|
502
|
-
|
|
650
|
+
// On resume (#760), provision the first incomplete link from the
|
|
651
|
+
// completed prefix's committed tip (resumeBase) instead of the base
|
|
652
|
+
// branch, so the chain rebuilds onto the work already done.
|
|
653
|
+
const chainBase = chainResume?.resumeBase ?? baseBranch;
|
|
654
|
+
worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager, chainBase);
|
|
503
655
|
}
|
|
504
656
|
else {
|
|
505
657
|
worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager, baseBranch);
|
|
@@ -524,6 +676,7 @@ export class RunOrchestrator {
|
|
|
524
676
|
services: { logWriter, stateManager, shutdownManager: shutdown },
|
|
525
677
|
packageManager: manifest.packageManager,
|
|
526
678
|
baseBranch,
|
|
679
|
+
chainResume,
|
|
527
680
|
onProgress,
|
|
528
681
|
onPhasePlan: init.onPhasePlan,
|
|
529
682
|
phasePauseHandle,
|
|
@@ -639,6 +792,108 @@ export class RunOrchestrator {
|
|
|
639
792
|
if (shutdown?.shuttingDown) {
|
|
640
793
|
break;
|
|
641
794
|
}
|
|
795
|
+
// #760: On chain resume, the completed prefix was skipped, so the first
|
|
796
|
+
// active link (i === 0) is now the resume point. The #748 successor-rebase
|
|
797
|
+
// below only fires for i > 0, so explicitly rebase this first link onto
|
|
798
|
+
// the last completed link's committed tip (resumeBase). This is the
|
|
799
|
+
// authoritative correctness gate: provisioning may have created the
|
|
800
|
+
// worktree fresh (already on resumeBase → no-op) or rebased an existing
|
|
801
|
+
// one, but a provisioning-time rebase failure only warns and continues —
|
|
802
|
+
// here we fail fast so the link never silently executes on the wrong base.
|
|
803
|
+
const chainResume = this.cfg.chainResume;
|
|
804
|
+
if (options.chain && i === 0 && chainResume?.resumeBase) {
|
|
805
|
+
const activeWorktree = this.cfg.worktreeMap.get(issueNumber);
|
|
806
|
+
if (activeWorktree) {
|
|
807
|
+
const rebase = rebaseOntoLocalBranch(activeWorktree.path, chainResume.resumeBase, this.cfg.config.verbose);
|
|
808
|
+
if (!rebase.success) {
|
|
809
|
+
console.log(chalk.yellow(` ⚠️ Chain resume broken: could not rebase #${issueNumber} onto resume base ${chainResume.resumeBase}` +
|
|
810
|
+
(rebase.conflict ? " — merge conflict" : "") +
|
|
811
|
+
`. Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
812
|
+
results.push({
|
|
813
|
+
issueNumber,
|
|
814
|
+
success: false,
|
|
815
|
+
phaseResults: [],
|
|
816
|
+
durationSeconds: 0,
|
|
817
|
+
loopTriggered: false,
|
|
818
|
+
abortReason: rebase.conflict
|
|
819
|
+
? `chain resume rebase conflict onto ${chainResume.resumeBase}`
|
|
820
|
+
: `chain resume rebase failed onto ${chainResume.resumeBase}: ${rebase.error ?? "unknown error"}`,
|
|
821
|
+
});
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
console.log(chalk.yellow(` ⚠️ Chain resume broken: no worktree for #${issueNumber}. ` +
|
|
827
|
+
`Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
828
|
+
results.push({
|
|
829
|
+
issueNumber,
|
|
830
|
+
success: false,
|
|
831
|
+
phaseResults: [],
|
|
832
|
+
durationSeconds: 0,
|
|
833
|
+
loopTriggered: false,
|
|
834
|
+
abortReason: `chain resume could not be established: missing worktree for #${issueNumber}`,
|
|
835
|
+
});
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// #748: Successors were provisioned up-front (ensureWorktreesChain) while
|
|
840
|
+
// the predecessor branch still pointed at the base, so on a fresh run each
|
|
841
|
+
// successor effectively branched from main. Now that the predecessor has
|
|
842
|
+
// executed and committed, re-rebase this successor's worktree onto the
|
|
843
|
+
// predecessor's *local* committed tip — independent of --stacked, and
|
|
844
|
+
// targeting the local feature branch (not origin/main). This is what makes
|
|
845
|
+
// the chain contract ("each branches from previous") actually hold.
|
|
846
|
+
if (options.chain && i > 0) {
|
|
847
|
+
const successorWorktree = this.cfg.worktreeMap.get(issueNumber);
|
|
848
|
+
const predecessorLocalBranch = this.cfg.worktreeMap.get(issueNumbers[i - 1])?.branch;
|
|
849
|
+
if (successorWorktree && predecessorLocalBranch) {
|
|
850
|
+
const rebase = rebaseOntoLocalBranch(successorWorktree.path, predecessorLocalBranch, this.cfg.config.verbose);
|
|
851
|
+
if (!rebase.success) {
|
|
852
|
+
// A broken chain link must NOT silently produce a successor built on
|
|
853
|
+
// the wrong base — that successor would miss the predecessor's work
|
|
854
|
+
// (the original #748 bug) and the break would propagate to every
|
|
855
|
+
// downstream successor. Treat it like a predecessor failure: warn
|
|
856
|
+
// loudly, record the break, and stop the chain so a human can
|
|
857
|
+
// resolve the conflict and re-run.
|
|
858
|
+
console.log(chalk.yellow(` ⚠️ Chain link broken: could not rebase #${issueNumber} onto #${issueNumbers[i - 1]} (${predecessorLocalBranch})` +
|
|
859
|
+
(rebase.conflict ? " — merge conflict" : "") +
|
|
860
|
+
`. Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
861
|
+
results.push({
|
|
862
|
+
issueNumber,
|
|
863
|
+
success: false,
|
|
864
|
+
phaseResults: [],
|
|
865
|
+
durationSeconds: 0,
|
|
866
|
+
loopTriggered: false,
|
|
867
|
+
abortReason: rebase.conflict
|
|
868
|
+
? `chain rebase conflict onto #${issueNumbers[i - 1]} (${predecessorLocalBranch})`
|
|
869
|
+
: `chain rebase failed onto #${issueNumbers[i - 1]} (${predecessorLocalBranch}): ${rebase.error ?? "unknown error"}`,
|
|
870
|
+
});
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
// The worktree map is expected to be fully populated for a chain (the
|
|
876
|
+
// --stacked block below treats a missing predecessor branch as
|
|
877
|
+
// unreachable). If it isn't, the successor cannot be chained onto its
|
|
878
|
+
// predecessor's work — the same end state as a rebase conflict — so we
|
|
879
|
+
// break the chain identically rather than letting the successor
|
|
880
|
+
// silently branch from its un-rebased base (the original #748 bug).
|
|
881
|
+
const missing = !successorWorktree
|
|
882
|
+
? "successor worktree"
|
|
883
|
+
: `predecessor branch for #${issueNumbers[i - 1]}`;
|
|
884
|
+
console.log(chalk.yellow(` ⚠️ Chain link could not be established for #${issueNumber}: missing ${missing} in worktree map. ` +
|
|
885
|
+
`Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
886
|
+
results.push({
|
|
887
|
+
issueNumber,
|
|
888
|
+
success: false,
|
|
889
|
+
phaseResults: [],
|
|
890
|
+
durationSeconds: 0,
|
|
891
|
+
loopTriggered: false,
|
|
892
|
+
abortReason: `chain link could not be established onto #${issueNumbers[i - 1]}: missing ${missing} in worktree map`,
|
|
893
|
+
});
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
642
897
|
// #605: under --stacked, non-first PRs target the predecessor branch.
|
|
643
898
|
// The final PR still targets `main` (AC-3 open-question default) so the
|
|
644
899
|
// stack can land partially. Manifest renders for every PR in the stack.
|
|
@@ -826,6 +1081,11 @@ export class RunOrchestrator {
|
|
|
826
1081
|
cliFlags.push("--testgen");
|
|
827
1082
|
const tokenUsage = getTokenUsageForRun(undefined, true);
|
|
828
1083
|
const passed = results.filter((r) => r.success).length;
|
|
1084
|
+
// #761 AC-7: record why the run failed, as a bounded enum. Sequential and
|
|
1085
|
+
// chain runs halt at the first failure, so the first categorized failed
|
|
1086
|
+
// issue is the halting one; in parallel runs it is the earliest-listed
|
|
1087
|
+
// failure. Undefined on success — the field is omitted from the record.
|
|
1088
|
+
const failureCategory = results.find((r) => !r.success && r.failureCategory)?.failureCategory;
|
|
829
1089
|
await metricsWriter.recordRun({
|
|
830
1090
|
issues: issueNumbers,
|
|
831
1091
|
phases: Array.from(allPhases),
|
|
@@ -833,6 +1093,7 @@ export class RunOrchestrator {
|
|
|
833
1093
|
duration: totalDuration,
|
|
834
1094
|
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
835
1095
|
flags: cliFlags,
|
|
1096
|
+
failureCategory,
|
|
836
1097
|
metrics: {
|
|
837
1098
|
tokensUsed: tokenUsage.tokensUsed,
|
|
838
1099
|
filesChanged: totalFilesChanged,
|
|
@@ -116,6 +116,7 @@ export declare class StateManager {
|
|
|
116
116
|
updatePhaseStatus(issueNumber: number, phase: Phase, status: PhaseStatus, options?: {
|
|
117
117
|
error?: string;
|
|
118
118
|
iteration?: number;
|
|
119
|
+
capped?: boolean;
|
|
119
120
|
}): Promise<void>;
|
|
120
121
|
/**
|
|
121
122
|
* Update the overall issue status
|
|
@@ -300,6 +300,12 @@ export class StateManager {
|
|
|
300
300
|
if (options?.iteration !== undefined) {
|
|
301
301
|
phaseState.iteration = options.iteration;
|
|
302
302
|
}
|
|
303
|
+
// Persist the turn-cap marker (#739) so a halted-on-cap phase is
|
|
304
|
+
// distinguishable from a genuine failure in state, not just the run-log —
|
|
305
|
+
// this is what makes the "reversible later" resume path first-class.
|
|
306
|
+
if (options?.capped !== undefined) {
|
|
307
|
+
phaseState.capped = options.capped;
|
|
308
|
+
}
|
|
303
309
|
// Preserve startedAt if already set
|
|
304
310
|
const existingPhase = issueState.phases[phase];
|
|
305
311
|
if (existingPhase?.startedAt && status !== "pending") {
|
|
@@ -88,6 +88,7 @@ export declare const PhaseStateSchema: z.ZodObject<{
|
|
|
88
88
|
startedAt: z.ZodOptional<z.ZodString>;
|
|
89
89
|
completedAt: z.ZodOptional<z.ZodString>;
|
|
90
90
|
error: z.ZodOptional<z.ZodString>;
|
|
91
|
+
capped: z.ZodOptional<z.ZodBoolean>;
|
|
91
92
|
iteration: z.ZodOptional<z.ZodNumber>;
|
|
92
93
|
}, z.core.$strip>;
|
|
93
94
|
export type PhaseState = z.infer<typeof PhaseStateSchema>;
|
|
@@ -242,6 +243,7 @@ export declare const IssueStateSchema: z.ZodObject<{
|
|
|
242
243
|
startedAt: z.ZodOptional<z.ZodString>;
|
|
243
244
|
completedAt: z.ZodOptional<z.ZodString>;
|
|
244
245
|
error: z.ZodOptional<z.ZodString>;
|
|
246
|
+
capped: z.ZodOptional<z.ZodBoolean>;
|
|
245
247
|
iteration: z.ZodOptional<z.ZodNumber>;
|
|
246
248
|
}, z.core.$strip>>;
|
|
247
249
|
pr: z.ZodOptional<z.ZodObject<{
|
|
@@ -381,6 +383,7 @@ export declare const WorkflowStateSchema: z.ZodObject<{
|
|
|
381
383
|
startedAt: z.ZodOptional<z.ZodString>;
|
|
382
384
|
completedAt: z.ZodOptional<z.ZodString>;
|
|
383
385
|
error: z.ZodOptional<z.ZodString>;
|
|
386
|
+
capped: z.ZodOptional<z.ZodBoolean>;
|
|
384
387
|
iteration: z.ZodOptional<z.ZodNumber>;
|
|
385
388
|
}, z.core.$strip>>;
|
|
386
389
|
pr: z.ZodOptional<z.ZodObject<{
|
|
@@ -85,6 +85,13 @@ export const PhaseStateSchema = z.object({
|
|
|
85
85
|
completedAt: z.string().datetime().optional(),
|
|
86
86
|
/** Error message if phase failed */
|
|
87
87
|
error: z.string().optional(),
|
|
88
|
+
/**
|
|
89
|
+
* Set when the phase halted on its turn cap (`error_max_turns`) (#739).
|
|
90
|
+
* Distinguishes a recoverable cap (partial output preserved, resume to
|
|
91
|
+
* continue) from a genuine failure while keeping `status: "failed"`.
|
|
92
|
+
* Additive/optional — existing persisted state is unaffected.
|
|
93
|
+
*/
|
|
94
|
+
capped: z.boolean().optional(),
|
|
88
95
|
/** Number of loop iterations (for loop phase) */
|
|
89
96
|
iteration: z.number().int().nonnegative().optional(),
|
|
90
97
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared status derivation for the run's live state machines (#766).
|
|
3
|
+
*
|
|
4
|
+
* Both the orchestrator's `IssueRuntimeState` machine and the renderer's
|
|
5
|
+
* `IssueState` machine used to *pin* an issue `failed` on any phase failure and
|
|
6
|
+
* never de-escalate — so a phase that failed on an early quality-loop iteration
|
|
7
|
+
* left the card red even after a later iteration recovered and shipped the PR.
|
|
8
|
+
*
|
|
9
|
+
* The fix derives the failed verdict from the CURRENT phase slots instead.
|
|
10
|
+
* `findOrAppendPhase` (orchestrator) and the renderer both key phases by name,
|
|
11
|
+
* so a retried phase overwrites its slot with the latest attempt — the slot
|
|
12
|
+
* already holds "the last attempt". The one phase that is NOT overwritten on
|
|
13
|
+
* recovery is `loop`: it is an auxiliary recovery step that only runs after a
|
|
14
|
+
* failure, so a stale failed `loop` slot must not pin the issue. Excluding
|
|
15
|
+
* `loop` never hides a genuine failure — an unrecovered pipeline failure always
|
|
16
|
+
* leaves a non-loop phase's latest slot failed too (the qa/exec the loop was
|
|
17
|
+
* trying to fix).
|
|
18
|
+
*/
|
|
19
|
+
/** Auxiliary recovery phase — excluded from the failed verdict (see above). */
|
|
20
|
+
export declare const LOOP_PHASE = "loop";
|
|
21
|
+
/** Minimal phase shape both live state machines satisfy. */
|
|
22
|
+
export interface DerivablePhase {
|
|
23
|
+
name: string;
|
|
24
|
+
status: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* True iff a non-loop phase's latest slot is `failed`. The single source of
|
|
28
|
+
* truth for "is this issue failed" across both live state machines (#766 AC-2).
|
|
29
|
+
*/
|
|
30
|
+
export declare function pipelineHasFailed(phases: DerivablePhase[]): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared status derivation for the run's live state machines (#766).
|
|
3
|
+
*
|
|
4
|
+
* Both the orchestrator's `IssueRuntimeState` machine and the renderer's
|
|
5
|
+
* `IssueState` machine used to *pin* an issue `failed` on any phase failure and
|
|
6
|
+
* never de-escalate — so a phase that failed on an early quality-loop iteration
|
|
7
|
+
* left the card red even after a later iteration recovered and shipped the PR.
|
|
8
|
+
*
|
|
9
|
+
* The fix derives the failed verdict from the CURRENT phase slots instead.
|
|
10
|
+
* `findOrAppendPhase` (orchestrator) and the renderer both key phases by name,
|
|
11
|
+
* so a retried phase overwrites its slot with the latest attempt — the slot
|
|
12
|
+
* already holds "the last attempt". The one phase that is NOT overwritten on
|
|
13
|
+
* recovery is `loop`: it is an auxiliary recovery step that only runs after a
|
|
14
|
+
* failure, so a stale failed `loop` slot must not pin the issue. Excluding
|
|
15
|
+
* `loop` never hides a genuine failure — an unrecovered pipeline failure always
|
|
16
|
+
* leaves a non-loop phase's latest slot failed too (the qa/exec the loop was
|
|
17
|
+
* trying to fix).
|
|
18
|
+
*/
|
|
19
|
+
/** Auxiliary recovery phase — excluded from the failed verdict (see above). */
|
|
20
|
+
export const LOOP_PHASE = "loop";
|
|
21
|
+
/**
|
|
22
|
+
* True iff a non-loop phase's latest slot is `failed`. The single source of
|
|
23
|
+
* truth for "is this issue failed" across both live state machines (#766 AC-2).
|
|
24
|
+
*/
|
|
25
|
+
export function pipelineHasFailed(phases) {
|
|
26
|
+
return phases.some((p) => p.name !== LOOP_PHASE && p.status === "failed");
|
|
27
|
+
}
|
|
@@ -7,6 +7,8 @@ import type { LogWriter } from "./log-writer.js";
|
|
|
7
7
|
import type { StateManager } from "./state-manager.js";
|
|
8
8
|
import type { ShutdownManager } from "../shutdown.js";
|
|
9
9
|
import type { WorktreeInfo } from "./worktree-manager.js";
|
|
10
|
+
import type { SequantError } from "../errors.js";
|
|
11
|
+
import type { ErrorCategory } from "./error-classifier.js";
|
|
10
12
|
export type { WorkflowEventEmitter, WorkflowEvents, WorkflowEventListener, IssueEventStatus, BaseEventPayload, RunEventPayload, PhaseStartedPayload, PhaseCompletedPayload, PhaseFailedPayload, IssueStatusChangedPayload, QaVerdictPayload, ProgressPayload, } from "./event-emitter.js";
|
|
11
13
|
/**
|
|
12
14
|
* Canonical Zod schema for all workflow phases.
|
|
@@ -160,6 +162,22 @@ export interface PhaseResult {
|
|
|
160
162
|
success: boolean;
|
|
161
163
|
durationSeconds?: number;
|
|
162
164
|
error?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Typed error with structured cause data, propagated from the driver's
|
|
167
|
+
* `AgentPhaseResult.structuredError` (#732). When present, the retry logic
|
|
168
|
+
* prefers it over stderr-regex classification and uses its type to gate the
|
|
169
|
+
* MCP fallback (a `BillingError` skips the misleading retry — #592).
|
|
170
|
+
*/
|
|
171
|
+
structuredError?: SequantError;
|
|
172
|
+
/**
|
|
173
|
+
* Set when the phase hit its turn cap (`error_max_turns`), propagated from the
|
|
174
|
+
* driver's `AgentPhaseResult.capped` (#733/#739). A capped phase is
|
|
175
|
+
* incomplete-but-not-hard-failed: the partial work in `output` is preserved
|
|
176
|
+
* rather than discarded, and the retry logic treats it like the `BillingError`
|
|
177
|
+
* skip (#732) — a retry cannot un-cap a turn limit. Additive/optional, same
|
|
178
|
+
* shape as `structuredError?`; existing consumers are unaffected.
|
|
179
|
+
*/
|
|
180
|
+
capped?: boolean;
|
|
163
181
|
/** Captured output from the phase (used for parsing spec recommendations) */
|
|
164
182
|
output?: string;
|
|
165
183
|
/** Parsed QA verdict (only for qa phase) */
|
|
@@ -199,6 +217,21 @@ export interface IssueResult {
|
|
|
199
217
|
startedAt: string;
|
|
200
218
|
command: string;
|
|
201
219
|
};
|
|
220
|
+
/**
|
|
221
|
+
* Set true when the chain-mode checkpoint commit could not be written after
|
|
222
|
+
* this link passed QA (#760). The link's own work is done, but the recovery
|
|
223
|
+
* point resume depends on is missing — surfaced prominently so a later resume
|
|
224
|
+
* failing fast (AC-3) is expected, not surprising.
|
|
225
|
+
*/
|
|
226
|
+
checkpointFailed?: boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Bounded-enum classification of the failure that halted this issue (#761
|
|
229
|
+
* AC-7), derived from the last non-loop failing phase's `structuredError`
|
|
230
|
+
* (preferred) or stderr-regex classification. Carried into
|
|
231
|
+
* `.sequant/metrics.json` — enum only, never a message string (metrics
|
|
232
|
+
* privacy contract). Absent on success.
|
|
233
|
+
*/
|
|
234
|
+
failureCategory?: ErrorCategory;
|
|
202
235
|
}
|
|
203
236
|
/**
|
|
204
237
|
* CLI options for the run command, merged with settings and env config.
|
|
@@ -248,6 +281,13 @@ export interface RunOptions {
|
|
|
248
281
|
* from building on potentially broken code.
|
|
249
282
|
*/
|
|
250
283
|
qaGate?: boolean;
|
|
284
|
+
/**
|
|
285
|
+
* Make `--chain` content pre-flight warnings fatal (#762).
|
|
286
|
+
* By default the pre-flight (missing AC section, dependency-order and
|
|
287
|
+
* file-overlap-order contradictions, closed issues) only warns. When true,
|
|
288
|
+
* any warning aborts the run BEFORE the first worktree is provisioned.
|
|
289
|
+
*/
|
|
290
|
+
strictPreflight?: boolean;
|
|
251
291
|
/**
|
|
252
292
|
* Base branch for worktree creation.
|
|
253
293
|
* Resolution priority: this CLI flag → settings.run.defaultBase → 'main'
|