sequant 2.8.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +9 -1
  4. package/dist/bin/cli.js +2 -1
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/README.md +2 -0
  7. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
  8. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
  9. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +96 -15
  10. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
  11. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
  12. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
  13. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  14. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  15. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +10 -2
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  17. package/dist/src/commands/logs.js +6 -1
  18. package/dist/src/commands/run-display.d.ts +20 -0
  19. package/dist/src/commands/run-display.js +80 -1
  20. package/dist/src/commands/stats.js +47 -0
  21. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  22. package/dist/src/lib/assess-collision-detect.js +68 -4
  23. package/dist/src/lib/cli-ui/run-renderer.js +17 -9
  24. package/dist/src/lib/errors.d.ts +6 -0
  25. package/dist/src/lib/errors.js +9 -2
  26. package/dist/src/lib/manifest.js +1 -17
  27. package/dist/src/lib/version-check.js +1 -5
  28. package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
  29. package/dist/src/lib/workflow/batch-executor.js +81 -18
  30. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  31. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  32. package/dist/src/lib/workflow/chain-resume.d.ts +116 -0
  33. package/dist/src/lib/workflow/chain-resume.js +166 -0
  34. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  35. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  36. package/dist/src/lib/workflow/drivers/claude-code.d.ts +7 -0
  37. package/dist/src/lib/workflow/drivers/claude-code.js +30 -6
  38. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  39. package/dist/src/lib/workflow/error-classifier.js +14 -1
  40. package/dist/src/lib/workflow/log-writer.js +6 -8
  41. package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
  42. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  43. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  44. package/dist/src/lib/workflow/phase-executor.d.ts +32 -0
  45. package/dist/src/lib/workflow/phase-executor.js +77 -5
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +23 -0
  47. package/dist/src/lib/workflow/run-log-schema.js +45 -1
  48. package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
  49. package/dist/src/lib/workflow/run-orchestrator.js +291 -30
  50. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  51. package/dist/src/lib/workflow/status-derivation.js +27 -0
  52. package/dist/src/lib/workflow/types.d.ts +23 -0
  53. package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
  54. package/dist/src/lib/workflow/worktree-manager.js +103 -33
  55. package/dist/src/mcp/tools/run.d.ts +2 -0
  56. package/dist/src/mcp/tools/run.js +2 -0
  57. package/package.json +2 -4
  58. package/templates/hooks/post-tool.sh +18 -3
  59. package/templates/hooks/pre-tool.sh +330 -57
  60. package/templates/scripts/cleanup-worktree.sh +103 -14
  61. package/templates/skills/assess/SKILL.md +96 -15
  62. package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
  63. package/templates/skills/fullsolve/SKILL.md +1 -1
  64. package/templates/skills/reflect/SKILL.md +27 -13
  65. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  66. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  67. package/templates/skills/release/SKILL.md +10 -2
  68. package/templates/skills/spec/references/verification-criteria.md +1 -1
@@ -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;
@@ -454,10 +472,13 @@ export async function runIssueWithLogging(ctx) {
454
472
  // Log spec phase result
455
473
  // Note: Spec runs in main repo, not worktree, so no git diff stats
456
474
  if (logWriter) {
457
- // 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.
458
478
  let specErrorContext;
459
479
  if (!specResult.success && specResult.stderrTail) {
460
- const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
480
+ const specError = specResult.structuredError ??
481
+ classifyError(specResult.stderrTail ?? [], specResult.exitCode);
461
482
  specErrorContext = {
462
483
  stderrTail: specResult.stderrTail ?? [],
463
484
  stdoutTail: specResult.stdoutTail ?? [],
@@ -518,6 +539,7 @@ export async function runIssueWithLogging(ctx) {
518
539
  phaseResults,
519
540
  durationSeconds,
520
541
  loopTriggered: false,
542
+ failureCategory: deriveFailureCategory(phaseResults),
521
543
  };
522
544
  }
523
545
  // Parse recommended workflow from spec output
@@ -710,10 +732,13 @@ export async function runIssueWithLogging(ctx) {
710
732
  : undefined;
711
733
  // Read cache metrics for QA phase (AC-7)
712
734
  const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
713
- // 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.
714
738
  let errorContext;
715
739
  if (!result.success && result.stderrTail) {
716
- const typedError = classifyError(result.stderrTail ?? [], result.exitCode);
740
+ const typedError = result.structuredError ??
741
+ classifyError(result.stderrTail ?? [], result.exitCode);
717
742
  errorContext = {
718
743
  stderrTail: result.stderrTail ?? [],
719
744
  stdoutTail: result.stdoutTail ?? [],
@@ -801,6 +826,23 @@ export async function runIssueWithLogging(ctx) {
801
826
  const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
802
827
  const loopEndTime = new Date();
803
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
+ }
804
846
  // Emit loop completion/failure progress event (AC-8)
805
847
  const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
806
848
  if (loopResult.success) {
@@ -864,9 +906,25 @@ export async function runIssueWithLogging(ctx) {
864
906
  // State tracking errors shouldn't stop execution
865
907
  }
866
908
  }
867
- // 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;
868
919
  if (success && chainMode && worktreePath) {
869
- 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
+ }
870
928
  }
871
929
  // Rebase onto the base branch before PR creation (unless --no-rebase)
872
930
  // This ensures the branch is up-to-date and prevents lockfile drift
@@ -893,7 +951,10 @@ export async function runIssueWithLogging(ctx) {
893
951
  stackManifest: chain.stackManifest,
894
952
  }
895
953
  : undefined;
896
- 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);
897
958
  if (prResult.success && prResult.prNumber && prResult.prUrl) {
898
959
  prNumber = prResult.prNumber;
899
960
  prUrl = prResult.prUrl;
@@ -936,5 +997,7 @@ export async function runIssueWithLogging(ctx) {
936
997
  loopTriggered,
937
998
  prNumber,
938
999
  prUrl,
1000
+ checkpointFailed,
1001
+ failureCategory: success ? undefined : deriveFailureCategory(phaseResults),
939
1002
  };
940
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
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Chain resume planning (#760).
3
+ *
4
+ * When a `--chain` run fails mid-way, earlier links may already be complete
5
+ * (`ready_for_merge`) with a checkpoint commit on their feature branch
6
+ * (`createCheckpointCommit`, worktree-manager.ts). Re-running the same chain
7
+ * should skip that completed prefix and resume at the first incomplete link,
8
+ * rebased onto the last completed link's committed tip — NOT `main` (which is
9
+ * the #748 wrong-base failure this reuses the #748 rebase path to avoid).
10
+ *
11
+ * The existing pre-flight guard (`run-orchestrator.ts`) already drops
12
+ * `ready_for_merge`/`merged` issues from the run, but it is chain-unaware:
13
+ * dropping the completed prefix leaves the first incomplete link at index 0,
14
+ * where `executeSequential`'s successor-rebase never fires, so it silently
15
+ * builds on `main`. This module computes a *chain-correct* resume plan that
16
+ * preserves the completed prefix as the resume base.
17
+ *
18
+ * The planner is pure over an injected {@link CompletedLinkResolver} so the
19
+ * skip/fail-fast state machine (AC-3) is unit-testable without real git; the
20
+ * real-git rebase is covered by the integration test.
21
+ */
22
+ /** A completed link that will be skipped (not re-executed) on resume. */
23
+ export interface ChainResumeSkip {
24
+ issueNumber: number;
25
+ /** Why it was skipped — the terminal-ish status that made it complete. */
26
+ status: "ready_for_merge" | "merged";
27
+ /** The link's local feature branch (from state), if known. */
28
+ branch?: string;
29
+ /** The link's worktree path (from state), if known — used for the dirty check. */
30
+ worktree?: string;
31
+ }
32
+ /** The computed plan for resuming a partially-completed chain. */
33
+ export interface ChainResumePlan {
34
+ /** Contiguous completed prefix that will be skipped (not re-executed). */
35
+ skipped: ChainResumeSkip[];
36
+ /** Issue numbers to actually execute (the incomplete tail). */
37
+ active: number[];
38
+ /**
39
+ * Local branch (or base ref) the first active link must be provisioned from
40
+ * and rebased onto. Undefined on a fresh run (no completed prefix).
41
+ */
42
+ resumeBase?: string;
43
+ /** Commit the resume base resolves to — reported so the user sees the point. */
44
+ resumeBaseCommit?: string;
45
+ /** First incomplete issue number (the resume point), if any. */
46
+ resumeIssue?: number;
47
+ /**
48
+ * Set when resume cannot proceed safely (AC-3): a `ready_for_merge` link's
49
+ * branch/checkpoint is gone and its tip is unreconstructable. The caller must
50
+ * abort rather than silently execute the successor on the wrong base.
51
+ */
52
+ failFast?: string;
53
+ /** True when every link in the chain is already complete. */
54
+ allComplete: boolean;
55
+ }
56
+ /** Resolves the git refs a resume base depends on. Injected for testability. */
57
+ export interface CompletedLinkResolver {
58
+ /**
59
+ * Resolve a local branch ref to its tip commit SHA, or undefined if the
60
+ * branch does not exist (destroyed worktree/branch — the AC-3 fail-fast case).
61
+ */
62
+ resolveBranchTip(branch: string): string | undefined;
63
+ /** Resolve the base branch tip (for merged-resume reporting). */
64
+ resolveBaseTip(): string | undefined;
65
+ /**
66
+ * True iff the worktree has uncommitted changes (tracked or non-ignored
67
+ * untracked). Used to detect a resume base whose checkpoint never landed —
68
+ * see the dirty-tip fail-fast in {@link computeChainResumePlan}. Must return
69
+ * false (not throw) when the worktree is gone or unreadable; branch-tip
70
+ * resolution governs that case instead.
71
+ */
72
+ isWorktreeDirty(worktreePath: string): boolean;
73
+ }
74
+ /** An ordered chain link with its persisted state (status + branch + worktree). */
75
+ export interface ChainLinkState {
76
+ issueNumber: number;
77
+ status?: string;
78
+ branch?: string;
79
+ worktree?: string;
80
+ }
81
+ /**
82
+ * Compute a chain-correct resume plan.
83
+ *
84
+ * Peels the *contiguous* completed prefix off the front of the chain (chain
85
+ * mode breaks on the first failure, so completed links always form a prefix).
86
+ * A completed link that appears *after* an incomplete one is left in `active`
87
+ * and re-executed — the conservative choice, since automatic skipping must not
88
+ * silently drop an issue the user intended to redo.
89
+ *
90
+ * @param orderedLinks Chain links in execution order, with persisted state.
91
+ * @param baseBranch The run's base branch (resume base when the prefix merged).
92
+ * @param resolver Git-ref resolver (injected for tests).
93
+ */
94
+ export declare function computeChainResumePlan(orderedLinks: ChainLinkState[], baseBranch: string, resolver: CompletedLinkResolver): ChainResumePlan;
95
+ /** The subset of persisted issue state the resume planner reads. */
96
+ export interface PersistedLinkState {
97
+ status?: string;
98
+ branch?: string;
99
+ worktree?: string;
100
+ }
101
+ /**
102
+ * Read each link's persisted state and compute the resume plan.
103
+ *
104
+ * Split out of `run-orchestrator.ts` so the state-reading half of resume — in
105
+ * particular "a state lookup failure must treat the link as *incomplete*", the
106
+ * conservative choice that re-executes rather than skipping on bad data — is
107
+ * testable without standing up an orchestrator.
108
+ *
109
+ * @param issueNumbers Chain issues in execution order.
110
+ * @param baseBranch The run's base branch.
111
+ * @param getIssueState Reads persisted state for one issue (may reject).
112
+ * @param resolver Git-ref resolver.
113
+ * @param onStateError Called when a lookup throws; the link is then treated as
114
+ * incomplete (no status), so it stays in `active`.
115
+ */
116
+ export declare function planChainResumeFromState(issueNumbers: number[], baseBranch: string, getIssueState: (issueNumber: number) => Promise<PersistedLinkState | null | undefined>, resolver: CompletedLinkResolver, onStateError?: (issueNumber: number, error: unknown) => void): Promise<ChainResumePlan>;