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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +9 -1
- package/dist/bin/cli.js +2 -1
- 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/assess/SKILL.md +96 -15
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
- 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 +10 -2
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
- 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 +6 -0
- package/dist/src/lib/errors.js +9 -2
- package/dist/src/lib/manifest.js +1 -17
- package/dist/src/lib/version-check.js +1 -5
- package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
- package/dist/src/lib/workflow/batch-executor.js +81 -18
- 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/claude-code.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +30 -6
- 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.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 +32 -0
- package/dist/src/lib/workflow/phase-executor.js +77 -5
- package/dist/src/lib/workflow/run-log-schema.d.ts +23 -0
- package/dist/src/lib/workflow/run-log-schema.js +45 -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/status-derivation.d.ts +30 -0
- package/dist/src/lib/workflow/status-derivation.js +27 -0
- package/dist/src/lib/workflow/types.d.ts +23 -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/package.json +2 -4
- 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/assess/SKILL.md +96 -15
- package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
- package/templates/skills/fullsolve/SKILL.md +1 -1
- 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 +10 -2
- 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"
|
|
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;
|
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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>;
|