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,10 +10,11 @@
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { spawnSync } from "child_process";
|
|
12
12
|
import { createPhaseLogFromTiming } from "./log-writer.js";
|
|
13
|
-
import { classifyError, errorTypeToCategory } from "./error-classifier.js";
|
|
13
|
+
import { classifyError, errorTypeToCategory, } from "./error-classifier.js";
|
|
14
14
|
import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
|
|
15
15
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
16
16
|
import { executePhaseWithRetry } from "./phase-executor.js";
|
|
17
|
+
import { parseBodyDependencyMarkers } from "./dependency-markers.js";
|
|
17
18
|
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, DOCS_LABELS, } from "./phase-mapper.js";
|
|
18
19
|
import { activateRelay, deactivateRelay, } from "../relay/activation.js";
|
|
19
20
|
/**
|
|
@@ -137,17 +138,13 @@ export function parseDependencies(issueNumber) {
|
|
|
137
138
|
return [];
|
|
138
139
|
const data = JSON.parse(result.stdout.toString());
|
|
139
140
|
const dependencies = [];
|
|
140
|
-
// Parse from body: "Depends on: #123"
|
|
141
|
+
// Parse from body: line-leading "Depends on: #123" / "**Depends on**: #123".
|
|
142
|
+
// Delegates to the shared, hardened parser (#767): mid-sentence prose,
|
|
143
|
+
// in-fence examples, and inline-code mentions are ignored, and the `#` is
|
|
144
|
+
// required. Honors ONLY `depends on` — the sorter must not start reordering
|
|
145
|
+
// on `blocked by`, which would be a new silent-reorder class (#762 Open Q #3).
|
|
141
146
|
if (data.body) {
|
|
142
|
-
|
|
143
|
-
if (bodyMatch) {
|
|
144
|
-
for (const match of bodyMatch) {
|
|
145
|
-
const numMatch = match.match(/(\d+)/);
|
|
146
|
-
if (numMatch) {
|
|
147
|
-
dependencies.push(parseInt(numMatch[1], 10));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
147
|
+
dependencies.push(...parseBodyDependencyMarkers(data.body, ["depends on"]));
|
|
151
148
|
}
|
|
152
149
|
// Parse from labels: "depends-on/123" or "depends-on-123"
|
|
153
150
|
if (data.labels && Array.isArray(data.labels)) {
|
|
@@ -305,6 +302,27 @@ export async function executeBatch(issueNumbers, batchCtx) {
|
|
|
305
302
|
}
|
|
306
303
|
return results;
|
|
307
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Derive the bounded-enum failure category for a failed issue (#761 AC-7).
|
|
307
|
+
*
|
|
308
|
+
* Scans for the LAST non-loop failing phase — the same reverse scan
|
|
309
|
+
* `toIssueSummary` uses (#766), so the recorded category and the displayed
|
|
310
|
+
* failure reason describe the same attempt. Prefers the driver's structured
|
|
311
|
+
* cause over stderr-regex classification (#732). Returns only the enum value;
|
|
312
|
+
* message strings never leave this function (metrics privacy contract).
|
|
313
|
+
*
|
|
314
|
+
* @internal Exported for testing
|
|
315
|
+
*/
|
|
316
|
+
export function deriveFailureCategory(phaseResults) {
|
|
317
|
+
const failedPhase = [...phaseResults]
|
|
318
|
+
.reverse()
|
|
319
|
+
.find((p) => !p.success && p.phase !== "loop");
|
|
320
|
+
if (!failedPhase)
|
|
321
|
+
return undefined;
|
|
322
|
+
const typedError = failedPhase.structuredError ??
|
|
323
|
+
classifyError(failedPhase.stderrTail ?? [], failedPhase.exitCode);
|
|
324
|
+
return errorTypeToCategory(typedError);
|
|
325
|
+
}
|
|
308
326
|
export async function runIssueWithLogging(ctx) {
|
|
309
327
|
// Destructure context for use throughout the function
|
|
310
328
|
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
|
|
@@ -432,7 +450,17 @@ export async function runIssueWithLogging(ctx) {
|
|
|
432
450
|
}
|
|
433
451
|
}
|
|
434
452
|
else {
|
|
435
|
-
|
|
453
|
+
// Mirror the main phase loop (#739): a turn-capped spec phase surfaces the
|
|
454
|
+
// distinct "partial output preserved" signal rather than a generic failure
|
|
455
|
+
// reason, so the cap is recognizable on the spec path too (it has its own
|
|
456
|
+
// failure handling, separate from the main loop). The partial output is
|
|
457
|
+
// preserved in `phaseResults` (pushed above) and the run still halts via
|
|
458
|
+
// the early return below.
|
|
459
|
+
const extra = {
|
|
460
|
+
error: specResult.capped
|
|
461
|
+
? "turn cap reached — partial output preserved (resume to continue)"
|
|
462
|
+
: (specResult.error ?? "unknown"),
|
|
463
|
+
};
|
|
436
464
|
emitProgressLine(issueNumber, "spec", "failed", extra);
|
|
437
465
|
try {
|
|
438
466
|
onProgress?.(issueNumber, "spec", "failed", extra);
|
|
@@ -444,10 +472,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
444
472
|
// Log spec phase result
|
|
445
473
|
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
446
474
|
if (logWriter) {
|
|
447
|
-
// Build errorContext from captured stderr/stdout tails (#447)
|
|
475
|
+
// Build errorContext from captured stderr/stdout tails (#447). Prefer
|
|
476
|
+
// the driver's structured cause (#761 AC-6) — stderr-regex
|
|
477
|
+
// classification never sees the SDK's rate-limit/billing signals.
|
|
448
478
|
let specErrorContext;
|
|
449
479
|
if (!specResult.success && specResult.stderrTail) {
|
|
450
|
-
const specError =
|
|
480
|
+
const specError = specResult.structuredError ??
|
|
481
|
+
classifyError(specResult.stderrTail ?? [], specResult.exitCode);
|
|
451
482
|
specErrorContext = {
|
|
452
483
|
stderrTail: specResult.stderrTail ?? [],
|
|
453
484
|
stdoutTail: specResult.stdoutTail ?? [],
|
|
@@ -462,7 +493,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
462
493
|
? "success"
|
|
463
494
|
: specResult.error?.includes("Timeout")
|
|
464
495
|
? "timeout"
|
|
465
|
-
: "failure", {
|
|
496
|
+
: "failure", {
|
|
497
|
+
error: specResult.error,
|
|
498
|
+
// Mark a turn-capped spec phase distinctly in the log (#739), matching
|
|
499
|
+
// the main phase loop: status stays "failure" but `capped` flags it.
|
|
500
|
+
capped: specResult.capped,
|
|
501
|
+
errorContext: specErrorContext,
|
|
502
|
+
});
|
|
466
503
|
logWriter.logPhase(phaseLog);
|
|
467
504
|
}
|
|
468
505
|
// Track spec phase completion in state
|
|
@@ -471,6 +508,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
471
508
|
const phaseStatus = specResult.success ? "completed" : "failed";
|
|
472
509
|
await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
|
|
473
510
|
error: specResult.error,
|
|
511
|
+
// Mark a turn-capped spec halt distinctly in state (#739), matching
|
|
512
|
+
// the run-log marker — status stays "failed", `capped` flags it.
|
|
513
|
+
capped: specResult.capped,
|
|
474
514
|
});
|
|
475
515
|
}
|
|
476
516
|
catch {
|
|
@@ -499,6 +539,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
499
539
|
phaseResults,
|
|
500
540
|
durationSeconds,
|
|
501
541
|
loopTriggered: false,
|
|
542
|
+
failureCategory: deriveFailureCategory(phaseResults),
|
|
502
543
|
};
|
|
503
544
|
}
|
|
504
545
|
// Parse recommended workflow from spec output
|
|
@@ -595,6 +636,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
595
636
|
const useQualityLoop = config.qualityLoop || detectedQualityLoop;
|
|
596
637
|
const maxIterations = useQualityLoop ? config.maxIterations : 1;
|
|
597
638
|
let completedSuccessfully = false;
|
|
639
|
+
// Set when a phase hits its turn cap (#739): halt the outer quality-loop
|
|
640
|
+
// retry too, not just the inner /loop spawn — re-running a capped phase
|
|
641
|
+
// would only cap again, and "surface + halt" means the user resumes.
|
|
642
|
+
let haltedByCap = false;
|
|
598
643
|
while (iteration < maxIterations) {
|
|
599
644
|
iteration++;
|
|
600
645
|
if (useQualityLoop && iteration > 1) {
|
|
@@ -655,7 +700,18 @@ export async function runIssueWithLogging(ctx) {
|
|
|
655
700
|
}
|
|
656
701
|
}
|
|
657
702
|
else {
|
|
658
|
-
|
|
703
|
+
// A turn-capped phase is incomplete-but-not-hard-failed (#739): surface a
|
|
704
|
+
// distinct "partial output preserved" signal instead of a generic failure
|
|
705
|
+
// reason, so the user knows the run halted on a recoverable cap (and can
|
|
706
|
+
// resume) rather than on a genuine error. The partial `result.output` is
|
|
707
|
+
// already preserved in `phaseResults` (pushed above) and the phase log
|
|
708
|
+
// (`capped` flag below); the run still halts cleanly at the `break` below.
|
|
709
|
+
const extra = {
|
|
710
|
+
error: result.capped
|
|
711
|
+
? "turn cap reached — partial output preserved (resume to continue)"
|
|
712
|
+
: (result.error ?? "unknown"),
|
|
713
|
+
iteration,
|
|
714
|
+
};
|
|
659
715
|
emitProgressLine(issueNumber, phase, "failed", extra);
|
|
660
716
|
try {
|
|
661
717
|
onProgress?.(issueNumber, phase, "failed", extra);
|
|
@@ -676,10 +732,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
676
732
|
: undefined;
|
|
677
733
|
// Read cache metrics for QA phase (AC-7)
|
|
678
734
|
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
679
|
-
// Build errorContext from captured stderr/stdout tails (#447, AC-7/AC-8)
|
|
735
|
+
// Build errorContext from captured stderr/stdout tails (#447, AC-7/AC-8).
|
|
736
|
+
// Prefer the driver's structured cause (#761 AC-6) — stderr-regex
|
|
737
|
+
// classification never sees the SDK's rate-limit/billing signals.
|
|
680
738
|
let errorContext;
|
|
681
739
|
if (!result.success && result.stderrTail) {
|
|
682
|
-
const typedError =
|
|
740
|
+
const typedError = result.structuredError ??
|
|
741
|
+
classifyError(result.stderrTail ?? [], result.exitCode);
|
|
683
742
|
errorContext = {
|
|
684
743
|
stderrTail: result.stderrTail ?? [],
|
|
685
744
|
stdoutTail: result.stdoutTail ?? [],
|
|
@@ -696,6 +755,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
696
755
|
? "timeout"
|
|
697
756
|
: "failure", {
|
|
698
757
|
error: result.error,
|
|
758
|
+
// Mark a turn-capped phase distinctly in the log (#739): status stays
|
|
759
|
+
// "failure" (no new enum value) but `capped` flags it as recoverable.
|
|
760
|
+
capped: result.capped,
|
|
699
761
|
verdict: result.verdict,
|
|
700
762
|
summary: result.summary,
|
|
701
763
|
// Observability fields (AC-1, AC-2, AC-3, AC-7)
|
|
@@ -715,7 +777,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
715
777
|
: result.error?.includes("Timeout")
|
|
716
778
|
? "failed"
|
|
717
779
|
: "failed";
|
|
718
|
-
await stateManager.updatePhaseStatus(issueNumber, phase, phaseStatus, {
|
|
780
|
+
await stateManager.updatePhaseStatus(issueNumber, phase, phaseStatus, {
|
|
781
|
+
error: result.error,
|
|
782
|
+
// Mark a turn-capped phase halt distinctly in state (#739),
|
|
783
|
+
// matching the run-log marker — status stays "failed",
|
|
784
|
+
// `capped` flags it as recoverable for the resume path.
|
|
785
|
+
capped: result.capped,
|
|
786
|
+
});
|
|
719
787
|
}
|
|
720
788
|
catch {
|
|
721
789
|
// State tracking errors shouldn't stop execution
|
|
@@ -726,8 +794,15 @@ export async function runIssueWithLogging(ctx) {
|
|
|
726
794
|
}
|
|
727
795
|
else {
|
|
728
796
|
phasesFailed = true;
|
|
729
|
-
|
|
730
|
-
|
|
797
|
+
if (result.capped) {
|
|
798
|
+
haltedByCap = true;
|
|
799
|
+
}
|
|
800
|
+
// If quality loop enabled, run loop phase to fix issues.
|
|
801
|
+
// A turn-capped phase (#739) is incomplete, not a genuine quality
|
|
802
|
+
// failure: skip the loop and halt cleanly ("surface + halt"). Spawning
|
|
803
|
+
// /loop on partial output would act on incomplete work — exactly the
|
|
804
|
+
// risk the capped path is meant to avoid. The user resumes instead.
|
|
805
|
+
if (useQualityLoop && iteration < maxIterations && !result.capped) {
|
|
731
806
|
// #624 Item 3 (AC-3.3): the loop phase carries the current outer
|
|
732
807
|
// iteration so the live-zone status cell can show `loop N/M`.
|
|
733
808
|
const loopStartExtra = { iteration };
|
|
@@ -751,6 +826,23 @@ export async function runIssueWithLogging(ctx) {
|
|
|
751
826
|
const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
|
|
752
827
|
const loopEndTime = new Date();
|
|
753
828
|
phaseResults.push(loopResult);
|
|
829
|
+
// #766: record the loop phase in the run log — spec (:655) and the
|
|
830
|
+
// regular phases (:982) log via logWriter, but the loop was never
|
|
831
|
+
// logged, so a loop that decided the card's verdict was absent from
|
|
832
|
+
// the log you'd use to debug it (AC-6). Loop status never determines
|
|
833
|
+
// the issue verdict (see deriveIssueLogStatus), but the entry with
|
|
834
|
+
// phase/status/duration/error must exist.
|
|
835
|
+
if (logWriter) {
|
|
836
|
+
const loopPhaseLog = createPhaseLogFromTiming("loop", issueNumber, loopStartTime, loopEndTime, loopResult.success
|
|
837
|
+
? "success"
|
|
838
|
+
: loopResult.error?.includes("Timeout")
|
|
839
|
+
? "timeout"
|
|
840
|
+
: "failure", {
|
|
841
|
+
error: loopResult.error,
|
|
842
|
+
capped: loopResult.capped,
|
|
843
|
+
});
|
|
844
|
+
logWriter.logPhase(loopPhaseLog);
|
|
845
|
+
}
|
|
754
846
|
// Emit loop completion/failure progress event (AC-8)
|
|
755
847
|
const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
|
|
756
848
|
if (loopResult.success) {
|
|
@@ -790,6 +882,11 @@ export async function runIssueWithLogging(ctx) {
|
|
|
790
882
|
completedSuccessfully = true;
|
|
791
883
|
break;
|
|
792
884
|
}
|
|
885
|
+
// A turn-capped phase (#739) halts the outer quality-loop retry as well —
|
|
886
|
+
// re-running would only cap again; the partial work is already preserved.
|
|
887
|
+
if (haltedByCap) {
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
793
890
|
// If we're not in quality loop mode, don't retry
|
|
794
891
|
if (!config.qualityLoop) {
|
|
795
892
|
break;
|
|
@@ -809,9 +906,25 @@ export async function runIssueWithLogging(ctx) {
|
|
|
809
906
|
// State tracking errors shouldn't stop execution
|
|
810
907
|
}
|
|
811
908
|
}
|
|
812
|
-
// Create checkpoint commit in chain mode after QA passes
|
|
909
|
+
// Create checkpoint commit in chain mode after QA passes.
|
|
910
|
+
// #760: chain resume rebases the next link onto this checkpoint, so a failure
|
|
911
|
+
// here is not silent — warn prominently and record it on the result (AC-4).
|
|
912
|
+
//
|
|
913
|
+
// Note the status above is already `ready_for_merge`, so a re-run reads this
|
|
914
|
+
// link as a completed prefix and does NOT redo it. Its uncommitted work is
|
|
915
|
+
// therefore absent from the branch tip, which `computeChainResumePlan` detects
|
|
916
|
+
// (dirty worktree → fail fast) rather than wrong-basing the next link. The
|
|
917
|
+
// message states that outcome exactly: the work must be committed, or --force.
|
|
918
|
+
let checkpointFailed = false;
|
|
813
919
|
if (success && chainMode && worktreePath) {
|
|
814
|
-
createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
|
|
920
|
+
const checkpointOk = createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
|
|
921
|
+
if (!checkpointOk) {
|
|
922
|
+
checkpointFailed = true;
|
|
923
|
+
log(chalk.yellow(` ⚠️ Checkpoint commit for #${issueNumber} could not be created — its uncommitted ` +
|
|
924
|
+
`changes are NOT on branch ${branch ?? "the feature branch"}. #${issueNumber} stays ` +
|
|
925
|
+
`ready_for_merge, so a re-run will skip it and refuse to resume the chain here until the ` +
|
|
926
|
+
`work is committed in ${worktreePath} (or re-run with --force to redo the whole chain).`));
|
|
927
|
+
}
|
|
815
928
|
}
|
|
816
929
|
// Rebase onto the base branch before PR creation (unless --no-rebase)
|
|
817
930
|
// This ensures the branch is up-to-date and prevents lockfile drift
|
|
@@ -838,7 +951,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
838
951
|
stackManifest: chain.stackManifest,
|
|
839
952
|
}
|
|
840
953
|
: undefined;
|
|
841
|
-
|
|
954
|
+
// #749: surface a non-A+ qa verdict (e.g. AC_MET_BUT_NOT_A_PLUS) in the PR
|
|
955
|
+
// body so a reviewer sees why the run broke to PR rather than reaching A+.
|
|
956
|
+
const qaVerdict = phaseResults.find((p) => p.phase === "qa")?.verdict;
|
|
957
|
+
const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions, qaVerdict);
|
|
842
958
|
if (prResult.success && prResult.prNumber && prResult.prUrl) {
|
|
843
959
|
prNumber = prResult.prNumber;
|
|
844
960
|
prUrl = prResult.prUrl;
|
|
@@ -881,5 +997,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
881
997
|
loopTriggered,
|
|
882
998
|
prNumber,
|
|
883
999
|
prUrl,
|
|
1000
|
+
checkpointFailed,
|
|
1001
|
+
failureCategory: success ? undefined : deriveFailureCategory(phaseResults),
|
|
884
1002
|
};
|
|
885
1003
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight content pre-flight for `--chain` runs (#762).
|
|
3
|
+
*
|
|
4
|
+
* Chain flag-validation only checks flag *combinations* (see `run.ts`); nothing
|
|
5
|
+
* inspects the *content* of the issues being chained. This module adds a fast,
|
|
6
|
+
* warn-by-default pre-flight that runs before the first worktree is provisioned
|
|
7
|
+
* and surfaces four cheap content-level problems:
|
|
8
|
+
*
|
|
9
|
+
* 1. An issue has no (or an empty) Acceptance Criteria section.
|
|
10
|
+
* 2. An issue declares a blocker (`blocked by #N` / `depends on #N`) that runs
|
|
11
|
+
* *after* it in the CLI order — the order contradicts the declaration.
|
|
12
|
+
* 3. Two chained issues are predicted to modify the same file, but the CLI
|
|
13
|
+
* order contradicts the predicted (ascending) land order.
|
|
14
|
+
* 4. An issue is CLOSED on GitHub — chaining a closed/merged issue is almost
|
|
15
|
+
* certainly unintended.
|
|
16
|
+
*
|
|
17
|
+
* The design follows the #604 philosophy: **suggest, never auto-decide**. False
|
|
18
|
+
* dependency inference is worse than none, so warnings are non-fatal by default;
|
|
19
|
+
* `--strict-preflight` opts in to a hard stop.
|
|
20
|
+
*
|
|
21
|
+
* Overlap prediction is delegated to `assess-collision-detect` (AC-3) rather
|
|
22
|
+
* than reimplemented. The pure `computePreflightWarnings` function is the unit
|
|
23
|
+
* surface (AC-1/AC-5); `runChainPreflight` adds the `gh` fetch and warn-degrades
|
|
24
|
+
* if a fetch fails — the pre-flight must never be the thing that breaks a run.
|
|
25
|
+
*/
|
|
26
|
+
/** The class of content problem a warning describes. */
|
|
27
|
+
export type PreflightWarningKind = "missing-ac" | "dependency-order" | "file-overlap-order" | "closed-issue";
|
|
28
|
+
/** A single content-level pre-flight warning. */
|
|
29
|
+
export interface PreflightWarning {
|
|
30
|
+
/** Primary issue the warning is attached to. */
|
|
31
|
+
issue: number;
|
|
32
|
+
/** The class of problem (one warning per class per AC-1). */
|
|
33
|
+
kind: PreflightWarningKind;
|
|
34
|
+
/** Human-readable, ready to print after a `⚠` prefix. */
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
/** Fetched issue content the pure computation operates over. */
|
|
38
|
+
export interface PreflightIssue {
|
|
39
|
+
number: number;
|
|
40
|
+
/** Raw issue body markdown. */
|
|
41
|
+
body: string;
|
|
42
|
+
/** GitHub issue state, e.g. `"OPEN"` / `"CLOSED"`. */
|
|
43
|
+
state: string;
|
|
44
|
+
/** Issue title (for message context). */
|
|
45
|
+
title: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse the issue numbers a body declares itself blocked by / dependent on.
|
|
49
|
+
* Catches both `depends on #N` and `blocked by #N` (deduped, order-preserving).
|
|
50
|
+
* Only line-leading markers count as declarations — see the shared
|
|
51
|
+
* `dependency-markers.ts` parser for why mid-sentence prose mentions are
|
|
52
|
+
* deliberately ignored.
|
|
53
|
+
*
|
|
54
|
+
* The pre-flight honors BOTH markers (unlike `batch-executor.ts`, which honors
|
|
55
|
+
* only `depends on` so the sorter's ordering semantics stay untouched — #762
|
|
56
|
+
* Open Q #3). The hardened mechanics are shared; the marker set is per-caller.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseDeclaredBlockers(body: string): number[];
|
|
59
|
+
/**
|
|
60
|
+
* Detect whether a body has a non-empty Acceptance Criteria section.
|
|
61
|
+
*
|
|
62
|
+
* Fast path reuses `hasAcceptanceCriteria` (ac-parser) so `AC-N:`-prefixed
|
|
63
|
+
* checklists are recognized directly. The fallback catches AC sections written
|
|
64
|
+
* as bare checkboxes under an "Acceptance Criteria" heading (e.g. this very
|
|
65
|
+
* issue, #762) — those are legitimately non-empty AC sections that the
|
|
66
|
+
* `AC-N:` patterns don't match, and warning on them would be a false positive
|
|
67
|
+
* (worse than no warning, per #604).
|
|
68
|
+
*/
|
|
69
|
+
export declare function hasNonEmptyAcSection(body: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Compute chain pre-flight warnings over already-fetched issue content.
|
|
72
|
+
*
|
|
73
|
+
* Pure and deterministic — this is the unit-test surface (AC-1/AC-5). Issues
|
|
74
|
+
* absent from `issues` (e.g. a `gh` fetch failed for them) are silently skipped
|
|
75
|
+
* so a fetch error degrades to fewer checks rather than aborting the run.
|
|
76
|
+
*
|
|
77
|
+
* @param cliOrder Raw CLI issue order (NOT dep-sorted — see #762 Open Q #1).
|
|
78
|
+
* @param issues Map of issue number → fetched content.
|
|
79
|
+
*/
|
|
80
|
+
export declare function computePreflightWarnings(cliOrder: number[], issues: Map<number, PreflightIssue>): PreflightWarning[];
|
|
81
|
+
/**
|
|
82
|
+
* Run the chain content pre-flight: fetch each issue's body/state/title, then
|
|
83
|
+
* compute warnings. Fetch failures warn-degrade (a gray note is printed and the
|
|
84
|
+
* issue's checks are skipped) — the pre-flight never aborts a run on its own.
|
|
85
|
+
*
|
|
86
|
+
* @param cliOrder Raw CLI issue order (NOT dep-sorted).
|
|
87
|
+
* @returns The list of content warnings (empty when everything looks consistent).
|
|
88
|
+
*/
|
|
89
|
+
export declare function runChainPreflight(cliOrder: number[]): Promise<PreflightWarning[]>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight content pre-flight for `--chain` runs (#762).
|
|
3
|
+
*
|
|
4
|
+
* Chain flag-validation only checks flag *combinations* (see `run.ts`); nothing
|
|
5
|
+
* inspects the *content* of the issues being chained. This module adds a fast,
|
|
6
|
+
* warn-by-default pre-flight that runs before the first worktree is provisioned
|
|
7
|
+
* and surfaces four cheap content-level problems:
|
|
8
|
+
*
|
|
9
|
+
* 1. An issue has no (or an empty) Acceptance Criteria section.
|
|
10
|
+
* 2. An issue declares a blocker (`blocked by #N` / `depends on #N`) that runs
|
|
11
|
+
* *after* it in the CLI order — the order contradicts the declaration.
|
|
12
|
+
* 3. Two chained issues are predicted to modify the same file, but the CLI
|
|
13
|
+
* order contradicts the predicted (ascending) land order.
|
|
14
|
+
* 4. An issue is CLOSED on GitHub — chaining a closed/merged issue is almost
|
|
15
|
+
* certainly unintended.
|
|
16
|
+
*
|
|
17
|
+
* The design follows the #604 philosophy: **suggest, never auto-decide**. False
|
|
18
|
+
* dependency inference is worse than none, so warnings are non-fatal by default;
|
|
19
|
+
* `--strict-preflight` opts in to a hard stop.
|
|
20
|
+
*
|
|
21
|
+
* Overlap prediction is delegated to `assess-collision-detect` (AC-3) rather
|
|
22
|
+
* than reimplemented. The pure `computePreflightWarnings` function is the unit
|
|
23
|
+
* surface (AC-1/AC-5); `runChainPreflight` adds the `gh` fetch and warn-degrades
|
|
24
|
+
* if a fetch fails — the pre-flight must never be the thing that breaks a run.
|
|
25
|
+
*/
|
|
26
|
+
import { spawnSync } from "child_process";
|
|
27
|
+
import chalk from "chalk";
|
|
28
|
+
import { hasAcceptanceCriteria } from "../ac-parser.js";
|
|
29
|
+
import { extractPathsFromIssueBody, detectFileCollisions, } from "../assess-collision-detect.js";
|
|
30
|
+
import { parseBodyDependencyMarkers } from "./dependency-markers.js";
|
|
31
|
+
/**
|
|
32
|
+
* Parse the issue numbers a body declares itself blocked by / dependent on.
|
|
33
|
+
* Catches both `depends on #N` and `blocked by #N` (deduped, order-preserving).
|
|
34
|
+
* Only line-leading markers count as declarations — see the shared
|
|
35
|
+
* `dependency-markers.ts` parser for why mid-sentence prose mentions are
|
|
36
|
+
* deliberately ignored.
|
|
37
|
+
*
|
|
38
|
+
* The pre-flight honors BOTH markers (unlike `batch-executor.ts`, which honors
|
|
39
|
+
* only `depends on` so the sorter's ordering semantics stay untouched — #762
|
|
40
|
+
* Open Q #3). The hardened mechanics are shared; the marker set is per-caller.
|
|
41
|
+
*/
|
|
42
|
+
export function parseDeclaredBlockers(body) {
|
|
43
|
+
return parseBodyDependencyMarkers(body, ["depends on", "blocked by"]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Detect whether a body has a non-empty Acceptance Criteria section.
|
|
47
|
+
*
|
|
48
|
+
* Fast path reuses `hasAcceptanceCriteria` (ac-parser) so `AC-N:`-prefixed
|
|
49
|
+
* checklists are recognized directly. The fallback catches AC sections written
|
|
50
|
+
* as bare checkboxes under an "Acceptance Criteria" heading (e.g. this very
|
|
51
|
+
* issue, #762) — those are legitimately non-empty AC sections that the
|
|
52
|
+
* `AC-N:` patterns don't match, and warning on them would be a false positive
|
|
53
|
+
* (worse than no warning, per #604).
|
|
54
|
+
*/
|
|
55
|
+
export function hasNonEmptyAcSection(body) {
|
|
56
|
+
if (hasAcceptanceCriteria(body))
|
|
57
|
+
return true;
|
|
58
|
+
const lines = body.split("\n");
|
|
59
|
+
let inSection = false;
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
if (/^#{1,6}\s+.*acceptance\s+criteria/i.test(line)) {
|
|
62
|
+
inSection = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// A subsequent heading closes the AC section.
|
|
66
|
+
if (inSection && /^#{1,6}\s+/.test(line))
|
|
67
|
+
break;
|
|
68
|
+
// A checkbox item inside the section proves it is non-empty.
|
|
69
|
+
if (inSection && /^\s*-\s*\[[x\s]\]/i.test(line))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Compute chain pre-flight warnings over already-fetched issue content.
|
|
76
|
+
*
|
|
77
|
+
* Pure and deterministic — this is the unit-test surface (AC-1/AC-5). Issues
|
|
78
|
+
* absent from `issues` (e.g. a `gh` fetch failed for them) are silently skipped
|
|
79
|
+
* so a fetch error degrades to fewer checks rather than aborting the run.
|
|
80
|
+
*
|
|
81
|
+
* @param cliOrder Raw CLI issue order (NOT dep-sorted — see #762 Open Q #1).
|
|
82
|
+
* @param issues Map of issue number → fetched content.
|
|
83
|
+
*/
|
|
84
|
+
export function computePreflightWarnings(cliOrder, issues) {
|
|
85
|
+
const warnings = [];
|
|
86
|
+
const positionOf = new Map();
|
|
87
|
+
cliOrder.forEach((n, i) => positionOf.set(n, i));
|
|
88
|
+
// Per-issue checks, in CLI order for deterministic output.
|
|
89
|
+
for (const num of cliOrder) {
|
|
90
|
+
const issue = issues.get(num);
|
|
91
|
+
if (!issue)
|
|
92
|
+
continue;
|
|
93
|
+
// AC-4: closed/merged issue check (consistent with the #305 state guard).
|
|
94
|
+
if (issue.state.toUpperCase() === "CLOSED") {
|
|
95
|
+
warnings.push({
|
|
96
|
+
issue: num,
|
|
97
|
+
kind: "closed-issue",
|
|
98
|
+
message: `#${num} is CLOSED on GitHub — chaining a closed/merged issue is ` +
|
|
99
|
+
`likely unintended (consistent with the #305 ready_for_merge/merged ` +
|
|
100
|
+
`guard; the #592 in_progress-but-merged gap is not covered here).`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// AC-1: missing/empty Acceptance Criteria section.
|
|
104
|
+
if (!hasNonEmptyAcSection(issue.body)) {
|
|
105
|
+
warnings.push({
|
|
106
|
+
issue: num,
|
|
107
|
+
kind: "missing-ac",
|
|
108
|
+
message: `#${num} has no non-empty Acceptance Criteria section — the chain ` +
|
|
109
|
+
`cannot verify it is ready to implement.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// AC-1: CLI order contradicts a declared dependency marker.
|
|
113
|
+
for (const blocker of parseDeclaredBlockers(issue.body)) {
|
|
114
|
+
const blockerPos = positionOf.get(blocker);
|
|
115
|
+
// Only meaningful if the blocker is itself in this chain.
|
|
116
|
+
if (blockerPos === undefined)
|
|
117
|
+
continue;
|
|
118
|
+
if (blockerPos > positionOf.get(num)) {
|
|
119
|
+
warnings.push({
|
|
120
|
+
issue: num,
|
|
121
|
+
kind: "dependency-order",
|
|
122
|
+
message: `#${num} declares it is blocked by / depends on #${blocker}, but ` +
|
|
123
|
+
`#${blocker} runs AFTER #${num} in the chain order — reorder so ` +
|
|
124
|
+
`#${blocker} comes first.`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// AC-1/AC-3: CLI order contradicts predicted file-overlap order.
|
|
130
|
+
const issuePaths = new Map();
|
|
131
|
+
for (const num of cliOrder) {
|
|
132
|
+
const issue = issues.get(num);
|
|
133
|
+
if (issue)
|
|
134
|
+
issuePaths.set(num, extractPathsFromIssueBody(issue.body));
|
|
135
|
+
}
|
|
136
|
+
for (const collision of detectFileCollisions(issuePaths)) {
|
|
137
|
+
// `collision.issues` is ascending issue-number order = the predicted land
|
|
138
|
+
// order used by /assess. Restrict to issues actually in the chain.
|
|
139
|
+
const predicted = collision.issues.filter((n) => positionOf.has(n));
|
|
140
|
+
if (predicted.length < 2)
|
|
141
|
+
continue;
|
|
142
|
+
const cliRelative = [...predicted].sort((a, b) => positionOf.get(a) - positionOf.get(b));
|
|
143
|
+
const contradicts = predicted.some((n, i) => n !== cliRelative[i]);
|
|
144
|
+
if (contradicts) {
|
|
145
|
+
warnings.push({
|
|
146
|
+
issue: predicted[0],
|
|
147
|
+
kind: "file-overlap-order",
|
|
148
|
+
message: `#${predicted.join(", #")} are predicted to modify ${collision.file}; ` +
|
|
149
|
+
`predicted land order is #${predicted.join(" → #")} but the CLI order ` +
|
|
150
|
+
`is #${cliRelative.join(" → #")} — landing out of order risks the ` +
|
|
151
|
+
`downstream-staleness class (#133).`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return warnings;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fetch a single issue's pre-flight content via `gh`. Returns `null` on any
|
|
159
|
+
* failure so the caller can warn-degrade (skip that issue's checks) rather than
|
|
160
|
+
* abort the run.
|
|
161
|
+
*/
|
|
162
|
+
function fetchPreflightIssue(issueNumber) {
|
|
163
|
+
try {
|
|
164
|
+
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "body,state,title"], { stdio: "pipe" });
|
|
165
|
+
if (result.status !== 0)
|
|
166
|
+
return null;
|
|
167
|
+
const data = JSON.parse(result.stdout.toString());
|
|
168
|
+
return {
|
|
169
|
+
number: issueNumber,
|
|
170
|
+
body: typeof data.body === "string" ? data.body : "",
|
|
171
|
+
state: typeof data.state === "string" ? data.state : "",
|
|
172
|
+
title: typeof data.title === "string" ? data.title : `Issue #${issueNumber}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Run the chain content pre-flight: fetch each issue's body/state/title, then
|
|
181
|
+
* compute warnings. Fetch failures warn-degrade (a gray note is printed and the
|
|
182
|
+
* issue's checks are skipped) — the pre-flight never aborts a run on its own.
|
|
183
|
+
*
|
|
184
|
+
* @param cliOrder Raw CLI issue order (NOT dep-sorted).
|
|
185
|
+
* @returns The list of content warnings (empty when everything looks consistent).
|
|
186
|
+
*/
|
|
187
|
+
export async function runChainPreflight(cliOrder) {
|
|
188
|
+
const issues = new Map();
|
|
189
|
+
for (const num of cliOrder) {
|
|
190
|
+
const fetched = fetchPreflightIssue(num);
|
|
191
|
+
if (fetched) {
|
|
192
|
+
issues.set(num, fetched);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.log(chalk.gray(` (pre-flight: could not fetch #${num} — skipping its content checks)`));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return computePreflightWarnings(cliOrder, issues);
|
|
199
|
+
}
|