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.
Files changed (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +18 -2
  4. package/dist/bin/cli.d.ts +1 -1
  5. package/dist/bin/cli.js +12 -2
  6. package/dist/bin/preflight.d.ts +21 -0
  7. package/dist/bin/preflight.js +45 -0
  8. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  9. package/dist/marketplace/external_plugins/sequant/README.md +2 -0
  10. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
  11. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
  12. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/force-push.md +34 -0
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +117 -19
  14. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +29 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +100 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +24 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +285 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +202 -0
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +287 -0
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/test-quality-checklist.md +272 -0
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +40 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +95 -11
  25. package/dist/marketplace/external_plugins/sequant/skills/references/shared/framework-gotchas.md +186 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
  27. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  28. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  29. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +669 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  31. package/dist/marketplace/external_plugins/sequant/skills/test/references/browser-testing-patterns.md +423 -0
  32. package/dist/marketplace/external_plugins/sequant/skills/upstream/SKILL.md +419 -0
  33. package/dist/src/commands/logs.js +6 -1
  34. package/dist/src/commands/run-display.d.ts +20 -0
  35. package/dist/src/commands/run-display.js +80 -1
  36. package/dist/src/commands/stats.js +47 -0
  37. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  38. package/dist/src/lib/assess-collision-detect.js +68 -4
  39. package/dist/src/lib/cli-ui/run-renderer.js +17 -9
  40. package/dist/src/lib/errors.d.ts +91 -0
  41. package/dist/src/lib/errors.js +118 -0
  42. package/dist/src/lib/manifest.js +1 -17
  43. package/dist/src/lib/version-check.d.ts +19 -0
  44. package/dist/src/lib/version-check.js +45 -5
  45. package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
  46. package/dist/src/lib/workflow/batch-executor.js +142 -24
  47. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  48. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  49. package/dist/src/lib/workflow/chain-resume.d.ts +116 -0
  50. package/dist/src/lib/workflow/chain-resume.js +166 -0
  51. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  52. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  53. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +17 -0
  54. package/dist/src/lib/workflow/drivers/claude-code.d.ts +29 -0
  55. package/dist/src/lib/workflow/drivers/claude-code.js +136 -8
  56. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  57. package/dist/src/lib/workflow/error-classifier.js +14 -1
  58. package/dist/src/lib/workflow/log-writer.d.ts +1 -1
  59. package/dist/src/lib/workflow/log-writer.js +6 -8
  60. package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
  61. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  62. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  63. package/dist/src/lib/workflow/phase-executor.d.ts +50 -0
  64. package/dist/src/lib/workflow/phase-executor.js +151 -17
  65. package/dist/src/lib/workflow/run-log-schema.d.ts +26 -0
  66. package/dist/src/lib/workflow/run-log-schema.js +52 -1
  67. package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
  68. package/dist/src/lib/workflow/run-orchestrator.js +291 -30
  69. package/dist/src/lib/workflow/state-manager.d.ts +1 -0
  70. package/dist/src/lib/workflow/state-manager.js +6 -0
  71. package/dist/src/lib/workflow/state-schema.d.ts +3 -0
  72. package/dist/src/lib/workflow/state-schema.js +7 -0
  73. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  74. package/dist/src/lib/workflow/status-derivation.js +27 -0
  75. package/dist/src/lib/workflow/types.d.ts +40 -0
  76. package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
  77. package/dist/src/lib/workflow/worktree-manager.js +103 -33
  78. package/dist/src/mcp/tools/run.d.ts +2 -0
  79. package/dist/src/mcp/tools/run.js +2 -0
  80. package/dist/src/ui/tui/theme.d.ts +18 -4
  81. package/dist/src/ui/tui/theme.js +18 -4
  82. package/package.json +5 -6
  83. package/templates/hooks/post-tool.sh +18 -3
  84. package/templates/hooks/pre-tool.sh +330 -57
  85. package/templates/scripts/cleanup-worktree.sh +103 -14
  86. package/templates/skills/_shared/references/force-push.md +34 -0
  87. package/templates/skills/assess/SKILL.md +117 -19
  88. package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
  89. package/templates/skills/exec/SKILL.md +29 -0
  90. package/templates/skills/fullsolve/SKILL.md +1 -1
  91. package/templates/skills/loop/SKILL.md +100 -2
  92. package/templates/skills/qa/SKILL.md +24 -0
  93. package/templates/skills/qa/references/anti-pattern-detection.md +285 -0
  94. package/templates/skills/qa/references/call-site-review.md +202 -0
  95. package/templates/skills/qa/references/quality-gates.md +287 -0
  96. package/templates/skills/qa/references/test-quality-checklist.md +272 -0
  97. package/templates/skills/qa/references/testing-requirements.md +40 -0
  98. package/templates/skills/qa/scripts/quality-checks.sh +95 -11
  99. package/templates/skills/references/shared/framework-gotchas.md +186 -0
  100. package/templates/skills/reflect/SKILL.md +27 -13
  101. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  102. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  103. package/templates/skills/release/SKILL.md +669 -0
  104. package/templates/skills/spec/references/verification-criteria.md +1 -1
  105. package/templates/skills/test/references/browser-testing-patterns.md +423 -0
  106. 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" or "**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
- const bodyMatch = data.body.match(/\*?\*?depends\s+on\*?\*?:?\s*#?(\d+)/gi);
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
- const extra = { error: specResult.error ?? "unknown" };
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 = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
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", { error: specResult.error, errorContext: specErrorContext });
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
- const extra = { error: result.error ?? "unknown", iteration };
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 = classifyError(result.stderrTail ?? [], result.exitCode);
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, { error: result.error });
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
- // If quality loop enabled, run loop phase to fix issues
730
- if (useQualityLoop && iteration < maxIterations) {
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
- const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions);
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
+ }