sequant 2.8.0 → 2.10.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 +23 -4
- package/dist/bin/cli.js +99 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +58 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +38 -10
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +405 -67
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +250 -344
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +10 -7
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +4 -2
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- 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 +11 -3
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.js +2 -0
- package/dist/src/commands/logs.js +20 -6
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +25 -3
- package/dist/src/commands/ready.js +39 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.d.ts +20 -0
- package/dist/src/commands/run-display.js +117 -5
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/stats.js +47 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -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/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +33 -0
- package/dist/src/lib/cli-flags.js +46 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +79 -9
- package/dist/src/lib/errors.d.ts +84 -6
- package/dist/src/lib/errors.js +142 -17
- package/dist/src/lib/locks/index.d.ts +3 -3
- package/dist/src/lib/locks/index.js +2 -2
- package/dist/src/lib/locks/lock-manager.d.ts +20 -2
- package/dist/src/lib/locks/lock-manager.js +47 -3
- package/dist/src/lib/locks/types.d.ts +34 -3
- package/dist/src/lib/locks/types.js +16 -0
- package/dist/src/lib/manifest.js +1 -17
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +14 -0
- package/dist/src/lib/settings.js +61 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +9 -2
- package/dist/src/lib/test-tautology-detector.js +153 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +46 -7
- package/dist/src/lib/workflow/batch-executor.d.ts +138 -6
- package/dist/src/lib/workflow/batch-executor.js +550 -54
- 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 +118 -0
- package/dist/src/lib/workflow/chain-resume.js +170 -0
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
- package/dist/src/lib/workflow/config-resolver.js +54 -2
- package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
- package/dist/src/lib/workflow/dependency-markers.js +79 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +21 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +50 -8
- package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
- package/dist/src/lib/workflow/error-classifier.js +14 -1
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +44 -1
- package/dist/src/lib/workflow/log-writer.js +81 -14
- package/dist/src/lib/workflow/metrics-schema.d.ts +42 -3
- 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 +234 -13
- package/dist/src/lib/workflow/phase-executor.js +533 -56
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
- package/dist/src/lib/workflow/ready-gate.js +25 -9
- package/dist/src/lib/workflow/run-log-schema.d.ts +37 -0
- package/dist/src/lib/workflow/run-log-schema.js +92 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +33 -0
- package/dist/src/lib/workflow/run-orchestrator.js +429 -47
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
- package/dist/src/lib/workflow/status-derivation.js +27 -0
- package/dist/src/lib/workflow/types.d.ts +149 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +70 -2
- package/dist/src/lib/workflow/worktree-manager.js +201 -52
- package/dist/src/mcp/tools/run.d.ts +11 -1
- package/dist/src/mcp/tools/run.js +22 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +9 -10
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +38 -10
- package/templates/hooks/pre-tool.sh +405 -67
- package/templates/scripts/cleanup-worktree.sh +282 -30
- package/templates/scripts/new-feature.sh +264 -16
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +250 -344
- package/templates/skills/assess/references/predicted-collision-detection.md +10 -7
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +26 -6
- package/templates/skills/fullsolve/SKILL.md +4 -2
- package/templates/skills/loop/SKILL.md +38 -2
- package/templates/skills/merger/SKILL.md +2 -2
- package/templates/skills/qa/SKILL.md +337 -57
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- 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 +11 -3
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +2 -0
- package/templates/skills/spec/references/verification-criteria.md +1 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
|
@@ -10,7 +10,7 @@ import chalk from "chalk";
|
|
|
10
10
|
import { spawnSync } from "child_process";
|
|
11
11
|
import pLimit from "p-limit";
|
|
12
12
|
import { formatCoarseNowLine } from "./run-state.js";
|
|
13
|
-
import { detectDefaultBranch, ensureWorktrees, ensureWorktreesChain, getWorktreeDiffStats, } from "./worktree-manager.js";
|
|
13
|
+
import { detectDefaultBranch, ensureWorktrees, ensureWorktreesChain, getWorktreeDiffStats, rebaseOntoLocalBranch, } from "./worktree-manager.js";
|
|
14
14
|
import { LogWriter } from "./log-writer.js";
|
|
15
15
|
import { StateManager } from "./state-manager.js";
|
|
16
16
|
import { ShutdownManager } from "../shutdown.js";
|
|
@@ -27,20 +27,63 @@ function formatSignalLine(issue, pid, result) {
|
|
|
27
27
|
return ` Refused to signal PID ${pid} for #${issue} (matches this process or its parent)`;
|
|
28
28
|
case "pid-dead":
|
|
29
29
|
return ` Could not signal PID ${pid} for #${issue} (already exited)`;
|
|
30
|
+
case "stale-pid-untrusted":
|
|
31
|
+
return ` Refused to signal PID ${pid} for #${issue} (lock is past the age ceiling; that PID has likely been recycled)`;
|
|
30
32
|
case "kill-failed":
|
|
31
33
|
return ` Could not signal PID ${pid} for #${issue} (kill syscall failed)`;
|
|
32
34
|
case "orchestrator":
|
|
33
35
|
return ` Skipped signal for #${issue} (orchestrator mode)`;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a git ref (branch name or base branch) to its tip commit SHA in the
|
|
40
|
+
* current repo, or undefined if the ref does not exist. Used by the chain
|
|
41
|
+
* resume planner (#760) to reconstruct — and validate the existence of — a
|
|
42
|
+
* completed link's committed tip.
|
|
43
|
+
*/
|
|
44
|
+
function revParseRef(ref) {
|
|
45
|
+
const result = spawnSync("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
46
|
+
stdio: "pipe",
|
|
47
|
+
encoding: "utf-8",
|
|
48
|
+
});
|
|
49
|
+
if (result.status !== 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const sha = result.stdout.trim();
|
|
53
|
+
return sha.length > 0 ? sha : undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* True iff `worktreePath` has uncommitted changes (`git status --porcelain`,
|
|
57
|
+
* which already honours .gitignore). Used by the chain resume planner (#760) to
|
|
58
|
+
* reject a resume base whose checkpoint commit never landed.
|
|
59
|
+
*
|
|
60
|
+
* Returns false when the path is missing or not a worktree: an unreadable
|
|
61
|
+
* worktree can't be judged dirty, and the branch-tip check already covers the
|
|
62
|
+
* destroyed-link case.
|
|
63
|
+
*/
|
|
64
|
+
function isWorktreeDirty(worktreePath) {
|
|
65
|
+
const result = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], {
|
|
66
|
+
stdio: "pipe",
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
});
|
|
69
|
+
if (result.status !== 0) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return result.stdout.trim().length > 0;
|
|
73
|
+
}
|
|
74
|
+
import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, recordIssueCompletion, emitRunIdLine, } from "./batch-executor.js";
|
|
37
75
|
import { reconcileStateAtStartup } from "./state-utils.js";
|
|
76
|
+
import { runChainPreflight } from "./chain-preflight.js";
|
|
77
|
+
import { runSkillsPreflight } from "./skills-preflight.js";
|
|
38
78
|
import { getCommitHash } from "./git-diff-utils.js";
|
|
79
|
+
import { planChainResumeFromState, } from "./chain-resume.js";
|
|
80
|
+
import { isCompletedIssueStatus } from "./completed-status.js";
|
|
39
81
|
import { MetricsWriter } from "./metrics-writer.js";
|
|
40
82
|
import { WorkflowEventEmitter } from "./event-emitter.js";
|
|
41
83
|
import { determineOutcome } from "./metrics-schema.js";
|
|
42
84
|
import { getTokenUsageForRun } from "./token-utils.js";
|
|
43
85
|
import { resolveRunOptions, buildExecutionConfig } from "./config-resolver.js";
|
|
86
|
+
import { pipelineHasFailed } from "./status-derivation.js";
|
|
44
87
|
/**
|
|
45
88
|
* Build the stack-manifest line emitted into PR bodies under --stacked.
|
|
46
89
|
*
|
|
@@ -92,6 +135,20 @@ export class RunOrchestrator {
|
|
|
92
135
|
getEmitter() {
|
|
93
136
|
return this.emitter;
|
|
94
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The wrapped progress sink handed to the batch executor — every phase
|
|
140
|
+
* lifecycle event enters the orchestrator through here.
|
|
141
|
+
*
|
|
142
|
+
* @internal Exported for testing only. `applyProgressEvent` is private and
|
|
143
|
+
* depends on `issueStates`/`phaseStartTimes`/`emitter`, so there is no way
|
|
144
|
+
* to exercise its branches short of a full `run()`. Without this seam the
|
|
145
|
+
* #804 `waiting` branch would be untestable — and an untested `waiting`
|
|
146
|
+
* branch is not cosmetic: falling through to the complete/failed handler
|
|
147
|
+
* marks a merely-paused phase `failed` and emits `phase_failed`.
|
|
148
|
+
*/
|
|
149
|
+
getProgressCallback() {
|
|
150
|
+
return this.cfg.onProgress;
|
|
151
|
+
}
|
|
95
152
|
/**
|
|
96
153
|
* Point-in-time view of the entire run.
|
|
97
154
|
*
|
|
@@ -158,6 +215,16 @@ export class RunOrchestrator {
|
|
|
158
215
|
const wasStatus = state.status;
|
|
159
216
|
if (!state.startedAt)
|
|
160
217
|
state.startedAt = new Date();
|
|
218
|
+
// #866: a phase starting means the issue is live again, so any
|
|
219
|
+
// `completedAt` from an earlier terminal verdict is stale and must not
|
|
220
|
+
// outlive it. The #766 quality-loop recovery path reaches here holding
|
|
221
|
+
// one: a non-loop failure pins `failed` and stamps `completedAt` below,
|
|
222
|
+
// then iteration 2 re-runs the phase through this branch. Leaving it set
|
|
223
|
+
// published a completion time for a running issue — which froze the TUI's
|
|
224
|
+
// header clock for the whole recovery window. `startedAt` is deliberately
|
|
225
|
+
// NOT reset: the header measures the issue's total wall clock, spanning
|
|
226
|
+
// every iteration.
|
|
227
|
+
state.completedAt = undefined;
|
|
161
228
|
state.status = "running";
|
|
162
229
|
const now = new Date();
|
|
163
230
|
this.phaseStartTimes.set(`${issue}:${phase}`, now.getTime());
|
|
@@ -185,6 +252,25 @@ export class RunOrchestrator {
|
|
|
185
252
|
}
|
|
186
253
|
return;
|
|
187
254
|
}
|
|
255
|
+
// #804 AC-7: an auto-wait is a *paused running* phase, not a terminal one.
|
|
256
|
+
// This branch must precede the complete/failed fall-through below, which
|
|
257
|
+
// would otherwise close out the phase the moment a wait began. Surfaces the
|
|
258
|
+
// wait through the same `nowLine` the TUI already renders.
|
|
259
|
+
if (event === "waiting") {
|
|
260
|
+
if (!state.currentPhase || state.currentPhase.name !== phase)
|
|
261
|
+
return;
|
|
262
|
+
const line = extra?.text;
|
|
263
|
+
if (!line)
|
|
264
|
+
return;
|
|
265
|
+
state.currentPhase.nowLine = line;
|
|
266
|
+
state.currentPhase.lastActivityAt = new Date();
|
|
267
|
+
void this.emitter.emit("progress", {
|
|
268
|
+
issueNumber: issue,
|
|
269
|
+
phase,
|
|
270
|
+
text: line,
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
188
274
|
if (event === "activity") {
|
|
189
275
|
// Ignore activity for stale phases (race between completion and a
|
|
190
276
|
// final flushed output chunk).
|
|
@@ -218,8 +304,15 @@ export class RunOrchestrator {
|
|
|
218
304
|
const durationSec = elapsedMs !== undefined ? Math.round(elapsedMs / 1000) : undefined;
|
|
219
305
|
if (event === "failed") {
|
|
220
306
|
const prev = state.status;
|
|
221
|
-
|
|
222
|
-
|
|
307
|
+
// #766: a loop-phase failure alone must not pin the issue `failed` — the
|
|
308
|
+
// outer quality loop may still recover on a later iteration. Derive from
|
|
309
|
+
// the phase slots (loop excluded) so only a genuine non-loop failure marks
|
|
310
|
+
// the issue failed; otherwise hold `running` until a later event reaches a
|
|
311
|
+
// terminal verdict (a subsequent iteration always follows a loop failure).
|
|
312
|
+
const nowFailed = pipelineHasFailed(state.phases);
|
|
313
|
+
state.status = nowFailed ? "failed" : "running";
|
|
314
|
+
if (nowFailed)
|
|
315
|
+
state.completedAt = new Date();
|
|
223
316
|
void this.emitter.emit("phase_failed", {
|
|
224
317
|
issueNumber: issue,
|
|
225
318
|
phase,
|
|
@@ -227,11 +320,11 @@ export class RunOrchestrator {
|
|
|
227
320
|
error: extra?.error ?? "unknown",
|
|
228
321
|
iteration: extra?.iteration,
|
|
229
322
|
});
|
|
230
|
-
if (prev !==
|
|
323
|
+
if (prev !== state.status) {
|
|
231
324
|
void this.emitter.emit("issue_status_changed", {
|
|
232
325
|
issueNumber: issue,
|
|
233
326
|
from: prev,
|
|
234
|
-
to:
|
|
327
|
+
to: state.status,
|
|
235
328
|
});
|
|
236
329
|
}
|
|
237
330
|
return;
|
|
@@ -246,9 +339,10 @@ export class RunOrchestrator {
|
|
|
246
339
|
const allDone = state.phases.every((ph) => ph.status === "done" || ph.status === "failed");
|
|
247
340
|
if (allDone) {
|
|
248
341
|
const prev = state.status;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
342
|
+
// #766: exclude a stale failed `loop` slot so a run that failed the loop
|
|
343
|
+
// on an early iteration and then recovered finalizes as `passed`, not
|
|
344
|
+
// `failed`. An unrecovered failure still leaves a non-loop phase failed.
|
|
345
|
+
state.status = pipelineHasFailed(state.phases) ? "failed" : "passed";
|
|
252
346
|
state.completedAt = new Date();
|
|
253
347
|
if (prev !== state.status) {
|
|
254
348
|
void this.emitter.emit("issue_status_changed", {
|
|
@@ -291,6 +385,8 @@ export class RunOrchestrator {
|
|
|
291
385
|
.map((i) => parseInt(i, 10))
|
|
292
386
|
.filter((n) => !isNaN(n));
|
|
293
387
|
}
|
|
388
|
+
// Capture the raw CLI order before dep-sorting (#762 pre-flight needs it).
|
|
389
|
+
const rawIssueOrder = [...issueNumbers];
|
|
294
390
|
if (issueNumbers.length > 1 && !resolvedBatches) {
|
|
295
391
|
issueNumbers = sortByDependencies(issueNumbers);
|
|
296
392
|
}
|
|
@@ -302,6 +398,7 @@ export class RunOrchestrator {
|
|
|
302
398
|
mergedOptions,
|
|
303
399
|
config,
|
|
304
400
|
issueNumbers,
|
|
401
|
+
rawIssueOrder,
|
|
305
402
|
batches: resolvedBatches,
|
|
306
403
|
baseBranch,
|
|
307
404
|
stack: manifest.stack,
|
|
@@ -319,6 +416,15 @@ export class RunOrchestrator {
|
|
|
319
416
|
*/
|
|
320
417
|
static async run(init, issueArgs, batches) {
|
|
321
418
|
const { manifest, onProgress, phasePauseHandle, settings } = init;
|
|
419
|
+
// #867: capture the run's start before any service/log setup, so wall clock
|
|
420
|
+
// is independent of whether the log writer exists (AC-3: correct under
|
|
421
|
+
// --no-log and when log init fails). This single origin is the source of
|
|
422
|
+
// truth for both the printed summary and the run log (AC-2).
|
|
423
|
+
const runStartedAt = Date.now();
|
|
424
|
+
// Every `return` below — the success path and all six early exits — closes
|
|
425
|
+
// the bracket the same way, so the subtraction lives in one place rather
|
|
426
|
+
// than being restated per exit and left to drift.
|
|
427
|
+
const wallClock = () => (Date.now() - runStartedAt) / 1000;
|
|
322
428
|
// ── Config resolution ──────────────────────────────────────────────
|
|
323
429
|
const resolved = RunOrchestrator.resolveConfig(init, issueArgs, batches);
|
|
324
430
|
const { mergedOptions, config, baseBranch } = resolved;
|
|
@@ -334,6 +440,7 @@ export class RunOrchestrator {
|
|
|
334
440
|
config,
|
|
335
441
|
mergedOptions,
|
|
336
442
|
logWriter: null,
|
|
443
|
+
wallClockDurationSeconds: wallClock(),
|
|
337
444
|
};
|
|
338
445
|
}
|
|
339
446
|
// ── Services setup ─────────────────────────────────────────────────
|
|
@@ -355,6 +462,10 @@ export class RunOrchestrator {
|
|
|
355
462
|
logPath: mergedOptions.logPath ?? settings.run.logPath,
|
|
356
463
|
verbose: config.verbose,
|
|
357
464
|
startCommit: getCommitHash(process.cwd()),
|
|
465
|
+
// #867: share the orchestrator's run origin so the log's stored
|
|
466
|
+
// wall clock (finalizeRunLog: (end-start)/1000) is derived from the
|
|
467
|
+
// same start as the summary — AC-2/AC-7 (one producer, two consumers).
|
|
468
|
+
startTime: new Date(runStartedAt),
|
|
358
469
|
});
|
|
359
470
|
await logWriter.initialize(runConfig);
|
|
360
471
|
const runId = logWriter.getRunId();
|
|
@@ -374,8 +485,12 @@ export class RunOrchestrator {
|
|
|
374
485
|
const shutdown = new ShutdownManager();
|
|
375
486
|
if (logWriter) {
|
|
376
487
|
const writer = logWriter;
|
|
377
|
-
|
|
378
|
-
|
|
488
|
+
// #856: forward the abort cause into the log. This cleanup runs on the
|
|
489
|
+
// SIGINT/SIGTERM path, where no phase result will ever arrive for the
|
|
490
|
+
// in-flight issue — without the context, `finalize()` writes that issue
|
|
491
|
+
// out as if the run had simply ended.
|
|
492
|
+
shutdown.registerCleanup("Finalize run logs", async (abort) => {
|
|
493
|
+
await writer.finalize(abort ? { aborted: abort } : undefined);
|
|
379
494
|
});
|
|
380
495
|
}
|
|
381
496
|
// ── Pre-flight state guard ─────────────────────────────────────────
|
|
@@ -392,29 +507,47 @@ export class RunOrchestrator {
|
|
|
392
507
|
logNonFatalWarning(" ! State reconciliation failed, continuing...", error, config.verbose);
|
|
393
508
|
}
|
|
394
509
|
}
|
|
510
|
+
let chainResume;
|
|
395
511
|
if (stateManager && !config.dryRun && !mergedOptions.force) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
512
|
+
if (mergedOptions.chain) {
|
|
513
|
+
// ── Chain-aware resume (#760) ──────────────────────────────────
|
|
514
|
+
// Don't just drop completed links (chain-unaware skip leaves the
|
|
515
|
+
// first incomplete link at index 0, where the #748 successor-rebase
|
|
516
|
+
// never fires → it silently builds on main). Instead compute a
|
|
517
|
+
// chain-correct plan that resumes at the first incomplete link,
|
|
518
|
+
// rebased onto the completed prefix's committed tip.
|
|
519
|
+
const resolver = {
|
|
520
|
+
resolveBranchTip: (branch) => revParseRef(branch),
|
|
521
|
+
resolveBaseTip: () => revParseRef(baseBranch),
|
|
522
|
+
isWorktreeDirty,
|
|
523
|
+
};
|
|
524
|
+
const plan = await planChainResumeFromState(issueNumbers, baseBranch, (issueNumber) => stateManager.getIssueState(issueNumber), resolver, (issueNumber, error) => logNonFatalWarning(` ! State lookup failed for #${issueNumber}, treating as incomplete...`, error, config.verbose));
|
|
525
|
+
if (plan.failFast) {
|
|
526
|
+
console.log(chalk.red(`\n ❌ Chain resume aborted: ${plan.failFast}`));
|
|
527
|
+
shutdown.dispose();
|
|
528
|
+
return {
|
|
529
|
+
results: [
|
|
530
|
+
{
|
|
531
|
+
issueNumber: plan.resumeIssue ?? issueNumbers[0],
|
|
532
|
+
success: false,
|
|
533
|
+
phaseResults: [],
|
|
534
|
+
durationSeconds: 0,
|
|
535
|
+
loopTriggered: false,
|
|
536
|
+
abortReason: `chain resume aborted: ${plan.failFast}`,
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
logPath: null,
|
|
540
|
+
exitCode: 1,
|
|
541
|
+
worktreeMap: new Map(),
|
|
542
|
+
issueInfoMap: new Map(),
|
|
543
|
+
config,
|
|
544
|
+
mergedOptions,
|
|
545
|
+
logWriter: null,
|
|
546
|
+
wallClockDurationSeconds: wallClock(),
|
|
547
|
+
};
|
|
412
548
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
issueNumbers = activeIssues;
|
|
416
|
-
if (issueNumbers.length === 0) {
|
|
417
|
-
console.log(chalk.yellow(`\n All issues already completed. Use --force to re-run.`));
|
|
549
|
+
if (plan.allComplete) {
|
|
550
|
+
console.log(chalk.yellow(`\n All chain links already completed. Use --force to re-run.`));
|
|
418
551
|
shutdown.dispose();
|
|
419
552
|
return {
|
|
420
553
|
results: [],
|
|
@@ -425,8 +558,58 @@ export class RunOrchestrator {
|
|
|
425
558
|
config,
|
|
426
559
|
mergedOptions,
|
|
427
560
|
logWriter: null,
|
|
561
|
+
wallClockDurationSeconds: wallClock(),
|
|
428
562
|
};
|
|
429
563
|
}
|
|
564
|
+
if (plan.skipped.length > 0) {
|
|
565
|
+
chainResume = plan;
|
|
566
|
+
issueNumbers = plan.active;
|
|
567
|
+
const resumeAt = plan.resumeBaseCommit
|
|
568
|
+
? plan.resumeBaseCommit.slice(0, 8)
|
|
569
|
+
: (plan.resumeBase ?? "base");
|
|
570
|
+
for (const s of plan.skipped) {
|
|
571
|
+
console.log(chalk.yellow(` ! #${s.issueNumber}: already ${s.status} — skipping (use --force to re-run)`));
|
|
572
|
+
}
|
|
573
|
+
console.log(chalk.cyan(` ↻ Resuming chain at #${plan.resumeIssue} from ${resumeAt}` +
|
|
574
|
+
` (${plan.resumeBase})`));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// ── Non-chain skip guard (unchanged) ───────────────────────────
|
|
579
|
+
const activeIssues = [];
|
|
580
|
+
for (const issueNumber of issueNumbers) {
|
|
581
|
+
try {
|
|
582
|
+
const issueState = await stateManager.getIssueState(issueNumber);
|
|
583
|
+
if (issueState && isCompletedIssueStatus(issueState.status)) {
|
|
584
|
+
console.log(chalk.yellow(` ! #${issueNumber}: already ${issueState.status} — skipping (use --force to re-run)`));
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
activeIssues.push(issueNumber);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
logNonFatalWarning(` ! State lookup failed for #${issueNumber}, including anyway...`, error, config.verbose);
|
|
592
|
+
activeIssues.push(issueNumber);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (activeIssues.length < issueNumbers.length) {
|
|
596
|
+
issueNumbers = activeIssues;
|
|
597
|
+
if (issueNumbers.length === 0) {
|
|
598
|
+
console.log(chalk.yellow(`\n All issues already completed. Use --force to re-run.`));
|
|
599
|
+
shutdown.dispose();
|
|
600
|
+
return {
|
|
601
|
+
results: [],
|
|
602
|
+
logPath: null,
|
|
603
|
+
exitCode: 0,
|
|
604
|
+
worktreeMap: new Map(),
|
|
605
|
+
issueInfoMap: new Map(),
|
|
606
|
+
config,
|
|
607
|
+
mergedOptions,
|
|
608
|
+
logWriter: null,
|
|
609
|
+
wallClockDurationSeconds: wallClock(),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
430
613
|
}
|
|
431
614
|
}
|
|
432
615
|
// ── Concurrency lock (#625) ────────────────────────────────────────
|
|
@@ -483,14 +666,90 @@ export class RunOrchestrator {
|
|
|
483
666
|
config,
|
|
484
667
|
mergedOptions,
|
|
485
668
|
logWriter: null,
|
|
669
|
+
wallClockDurationSeconds: wallClock(),
|
|
486
670
|
};
|
|
487
671
|
}
|
|
488
672
|
}
|
|
673
|
+
// ── Chain content pre-flight (#762) ────────────────────────────────
|
|
674
|
+
// Warn-by-default; `--strict-preflight` turns warnings fatal. Runs BEFORE
|
|
675
|
+
// any worktree is provisioned. Uses the RAW CLI order (not the dep-sorted
|
|
676
|
+
// `issueNumbers`) so the dependency-order / file-overlap checks aren't
|
|
677
|
+
// masked by `sortByDependencies`, filtered to issues still active after the
|
|
678
|
+
// state guard / lock acquisition.
|
|
679
|
+
if (mergedOptions.chain && issueNumbers.length > 1) {
|
|
680
|
+
const activeSet = new Set(issueNumbers);
|
|
681
|
+
const preflightOrder = resolved.rawIssueOrder.filter((n) => activeSet.has(n));
|
|
682
|
+
const preflightWarnings = await runChainPreflight(preflightOrder);
|
|
683
|
+
if (preflightWarnings.length > 0) {
|
|
684
|
+
for (const w of preflightWarnings) {
|
|
685
|
+
bracketedConsoleLog(phasePauseHandle, chalk.yellow(` ⚠ ${w.message}`));
|
|
686
|
+
}
|
|
687
|
+
if (mergedOptions.strictPreflight) {
|
|
688
|
+
bracketedConsoleLog(phasePauseHandle, chalk.red(`\n ✖ --strict-preflight: ${preflightWarnings.length} pre-flight ` +
|
|
689
|
+
`warning(s) — aborting before any worktree is provisioned.`));
|
|
690
|
+
shutdown.dispose();
|
|
691
|
+
return {
|
|
692
|
+
results: [],
|
|
693
|
+
logPath: null,
|
|
694
|
+
exitCode: 1,
|
|
695
|
+
worktreeMap: new Map(),
|
|
696
|
+
issueInfoMap: new Map(),
|
|
697
|
+
config,
|
|
698
|
+
mergedOptions,
|
|
699
|
+
logWriter: null,
|
|
700
|
+
wallClockDurationSeconds: wallClock(),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
489
705
|
// ── Issue info + worktree setup ────────────────────────────────────
|
|
490
706
|
const issueInfoMap = new Map();
|
|
491
707
|
for (const issueNumber of issueNumbers) {
|
|
492
708
|
issueInfoMap.set(issueNumber, await getIssueInfo(issueNumber));
|
|
493
709
|
}
|
|
710
|
+
// ── Skills pre-flight (#813) ───────────────────────────────────────
|
|
711
|
+
// Fail fast when the driver resolves phases via `.claude/skills/` and a
|
|
712
|
+
// required skill is missing — BEFORE any worktree is provisioned or
|
|
713
|
+
// per-issue state entry written. Without this, the phase agent hunts for
|
|
714
|
+
// a slash command that can never resolve and the run surfaces as a bogus
|
|
715
|
+
// "spec retry" failure. Skipped for non-skill drivers (aider) and for
|
|
716
|
+
// --dry-run (no agent executes).
|
|
717
|
+
if (!config.dryRun) {
|
|
718
|
+
const preflight = await runSkillsPreflight({
|
|
719
|
+
agent: config.agent,
|
|
720
|
+
aiderSettings: config.aiderSettings,
|
|
721
|
+
phases: config.phases,
|
|
722
|
+
autoDetectPhases: resolved.autoDetectPhases,
|
|
723
|
+
qualityLoop: config.qualityLoop,
|
|
724
|
+
testgen: mergedOptions.testgen,
|
|
725
|
+
securityReview: mergedOptions.securityReview,
|
|
726
|
+
issueNumbers,
|
|
727
|
+
issueInfoMap,
|
|
728
|
+
});
|
|
729
|
+
if (!preflight.ok) {
|
|
730
|
+
bracketedConsoleLog(phasePauseHandle, chalk.red(`\n ✖ Skills pre-flight failed: ${preflight.cause}`));
|
|
731
|
+
bracketedConsoleLog(phasePauseHandle, chalk.red(` ${preflight.remedy}`));
|
|
732
|
+
shutdown.dispose();
|
|
733
|
+
return {
|
|
734
|
+
results: issueNumbers.map((issueNumber) => ({
|
|
735
|
+
issueNumber,
|
|
736
|
+
success: false,
|
|
737
|
+
phaseResults: [],
|
|
738
|
+
durationSeconds: 0,
|
|
739
|
+
loopTriggered: false,
|
|
740
|
+
abortReason: `skills pre-flight failed: ${preflight.cause}`,
|
|
741
|
+
})),
|
|
742
|
+
logPath: null,
|
|
743
|
+
exitCode: 1,
|
|
744
|
+
worktreeMap: new Map(),
|
|
745
|
+
issueInfoMap,
|
|
746
|
+
config,
|
|
747
|
+
mergedOptions,
|
|
748
|
+
logWriter: null,
|
|
749
|
+
wallClockDurationSeconds: wallClock(),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
494
753
|
const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
|
|
495
754
|
let worktreeMap = new Map();
|
|
496
755
|
if (useWorktreeIsolation && !config.dryRun) {
|
|
@@ -499,7 +758,11 @@ export class RunOrchestrator {
|
|
|
499
758
|
title: issueInfoMap.get(num)?.title || `Issue #${num}`,
|
|
500
759
|
}));
|
|
501
760
|
if (mergedOptions.chain) {
|
|
502
|
-
|
|
761
|
+
// On resume (#760), provision the first incomplete link from the
|
|
762
|
+
// completed prefix's committed tip (resumeBase) instead of the base
|
|
763
|
+
// branch, so the chain rebuilds onto the work already done.
|
|
764
|
+
const chainBase = chainResume?.resumeBase ?? baseBranch;
|
|
765
|
+
worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager, chainBase);
|
|
503
766
|
}
|
|
504
767
|
else {
|
|
505
768
|
worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager, baseBranch);
|
|
@@ -524,6 +787,7 @@ export class RunOrchestrator {
|
|
|
524
787
|
services: { logWriter, stateManager, shutdownManager: shutdown },
|
|
525
788
|
packageManager: manifest.packageManager,
|
|
526
789
|
baseBranch,
|
|
790
|
+
chainResume,
|
|
527
791
|
onProgress,
|
|
528
792
|
onPhasePlan: init.onPhasePlan,
|
|
529
793
|
phasePauseHandle,
|
|
@@ -557,10 +821,15 @@ export class RunOrchestrator {
|
|
|
557
821
|
endCommit: getCommitHash(process.cwd()),
|
|
558
822
|
});
|
|
559
823
|
}
|
|
824
|
+
// #867: the run's wall clock, computed once from the single origin. Both
|
|
825
|
+
// the metrics record and the RunResult below derive from this value — no
|
|
826
|
+
// consumer re-sums per-issue durations (which double-counts under
|
|
827
|
+
// --parallel). Same notion the log stores via finalizeRunLog above.
|
|
828
|
+
const wallClockDurationSeconds = wallClock();
|
|
560
829
|
// ── Record metrics ─────────────────────────────────────────────
|
|
561
830
|
if (!config.dryRun && results.length > 0) {
|
|
562
831
|
try {
|
|
563
|
-
await RunOrchestrator.recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers);
|
|
832
|
+
await RunOrchestrator.recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers, wallClockDurationSeconds);
|
|
564
833
|
}
|
|
565
834
|
catch (metricsError) {
|
|
566
835
|
logNonFatalWarning(" ! Metrics recording failed, continuing...", metricsError, config.verbose);
|
|
@@ -576,6 +845,7 @@ export class RunOrchestrator {
|
|
|
576
845
|
config,
|
|
577
846
|
mergedOptions,
|
|
578
847
|
logWriter,
|
|
848
|
+
wallClockDurationSeconds,
|
|
579
849
|
};
|
|
580
850
|
}
|
|
581
851
|
finally {
|
|
@@ -639,6 +909,108 @@ export class RunOrchestrator {
|
|
|
639
909
|
if (shutdown?.shuttingDown) {
|
|
640
910
|
break;
|
|
641
911
|
}
|
|
912
|
+
// #760: On chain resume, the completed prefix was skipped, so the first
|
|
913
|
+
// active link (i === 0) is now the resume point. The #748 successor-rebase
|
|
914
|
+
// below only fires for i > 0, so explicitly rebase this first link onto
|
|
915
|
+
// the last completed link's committed tip (resumeBase). This is the
|
|
916
|
+
// authoritative correctness gate: provisioning may have created the
|
|
917
|
+
// worktree fresh (already on resumeBase → no-op) or rebased an existing
|
|
918
|
+
// one, but a provisioning-time rebase failure only warns and continues —
|
|
919
|
+
// here we fail fast so the link never silently executes on the wrong base.
|
|
920
|
+
const chainResume = this.cfg.chainResume;
|
|
921
|
+
if (options.chain && i === 0 && chainResume?.resumeBase) {
|
|
922
|
+
const activeWorktree = this.cfg.worktreeMap.get(issueNumber);
|
|
923
|
+
if (activeWorktree) {
|
|
924
|
+
const rebase = rebaseOntoLocalBranch(activeWorktree.path, chainResume.resumeBase, this.cfg.config.verbose);
|
|
925
|
+
if (!rebase.success) {
|
|
926
|
+
console.log(chalk.yellow(` ⚠️ Chain resume broken: could not rebase #${issueNumber} onto resume base ${chainResume.resumeBase}` +
|
|
927
|
+
(rebase.conflict ? " — merge conflict" : "") +
|
|
928
|
+
`. Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
929
|
+
results.push({
|
|
930
|
+
issueNumber,
|
|
931
|
+
success: false,
|
|
932
|
+
phaseResults: [],
|
|
933
|
+
durationSeconds: 0,
|
|
934
|
+
loopTriggered: false,
|
|
935
|
+
abortReason: rebase.conflict
|
|
936
|
+
? `chain resume rebase conflict onto ${chainResume.resumeBase}`
|
|
937
|
+
: `chain resume rebase failed onto ${chainResume.resumeBase}: ${rebase.error ?? "unknown error"}`,
|
|
938
|
+
});
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
console.log(chalk.yellow(` ⚠️ Chain resume broken: no worktree for #${issueNumber}. ` +
|
|
944
|
+
`Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
945
|
+
results.push({
|
|
946
|
+
issueNumber,
|
|
947
|
+
success: false,
|
|
948
|
+
phaseResults: [],
|
|
949
|
+
durationSeconds: 0,
|
|
950
|
+
loopTriggered: false,
|
|
951
|
+
abortReason: `chain resume could not be established: missing worktree for #${issueNumber}`,
|
|
952
|
+
});
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// #748: Successors were provisioned up-front (ensureWorktreesChain) while
|
|
957
|
+
// the predecessor branch still pointed at the base, so on a fresh run each
|
|
958
|
+
// successor effectively branched from main. Now that the predecessor has
|
|
959
|
+
// executed and committed, re-rebase this successor's worktree onto the
|
|
960
|
+
// predecessor's *local* committed tip — independent of --stacked, and
|
|
961
|
+
// targeting the local feature branch (not origin/main). This is what makes
|
|
962
|
+
// the chain contract ("each branches from previous") actually hold.
|
|
963
|
+
if (options.chain && i > 0) {
|
|
964
|
+
const successorWorktree = this.cfg.worktreeMap.get(issueNumber);
|
|
965
|
+
const predecessorLocalBranch = this.cfg.worktreeMap.get(issueNumbers[i - 1])?.branch;
|
|
966
|
+
if (successorWorktree && predecessorLocalBranch) {
|
|
967
|
+
const rebase = rebaseOntoLocalBranch(successorWorktree.path, predecessorLocalBranch, this.cfg.config.verbose);
|
|
968
|
+
if (!rebase.success) {
|
|
969
|
+
// A broken chain link must NOT silently produce a successor built on
|
|
970
|
+
// the wrong base — that successor would miss the predecessor's work
|
|
971
|
+
// (the original #748 bug) and the break would propagate to every
|
|
972
|
+
// downstream successor. Treat it like a predecessor failure: warn
|
|
973
|
+
// loudly, record the break, and stop the chain so a human can
|
|
974
|
+
// resolve the conflict and re-run.
|
|
975
|
+
console.log(chalk.yellow(` ⚠️ Chain link broken: could not rebase #${issueNumber} onto #${issueNumbers[i - 1]} (${predecessorLocalBranch})` +
|
|
976
|
+
(rebase.conflict ? " — merge conflict" : "") +
|
|
977
|
+
`. Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
978
|
+
results.push({
|
|
979
|
+
issueNumber,
|
|
980
|
+
success: false,
|
|
981
|
+
phaseResults: [],
|
|
982
|
+
durationSeconds: 0,
|
|
983
|
+
loopTriggered: false,
|
|
984
|
+
abortReason: rebase.conflict
|
|
985
|
+
? `chain rebase conflict onto #${issueNumbers[i - 1]} (${predecessorLocalBranch})`
|
|
986
|
+
: `chain rebase failed onto #${issueNumbers[i - 1]} (${predecessorLocalBranch}): ${rebase.error ?? "unknown error"}`,
|
|
987
|
+
});
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
// The worktree map is expected to be fully populated for a chain (the
|
|
993
|
+
// --stacked block below treats a missing predecessor branch as
|
|
994
|
+
// unreachable). If it isn't, the successor cannot be chained onto its
|
|
995
|
+
// predecessor's work — the same end state as a rebase conflict — so we
|
|
996
|
+
// break the chain identically rather than letting the successor
|
|
997
|
+
// silently branch from its un-rebased base (the original #748 bug).
|
|
998
|
+
const missing = !successorWorktree
|
|
999
|
+
? "successor worktree"
|
|
1000
|
+
: `predecessor branch for #${issueNumbers[i - 1]}`;
|
|
1001
|
+
console.log(chalk.yellow(` ⚠️ Chain link could not be established for #${issueNumber}: missing ${missing} in worktree map. ` +
|
|
1002
|
+
`Stopping the chain; #${issueNumber} and any later issues were not run.`));
|
|
1003
|
+
results.push({
|
|
1004
|
+
issueNumber,
|
|
1005
|
+
success: false,
|
|
1006
|
+
phaseResults: [],
|
|
1007
|
+
durationSeconds: 0,
|
|
1008
|
+
loopTriggered: false,
|
|
1009
|
+
abortReason: `chain link could not be established onto #${issueNumbers[i - 1]}: missing ${missing} in worktree map`,
|
|
1010
|
+
});
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
642
1014
|
// #605: under --stacked, non-first PRs target the predecessor branch.
|
|
643
1015
|
// The final PR still targets `main` (AC-3 open-question default) so the
|
|
644
1016
|
// stack can land partially. Manifest renders for every PR in the stack.
|
|
@@ -670,14 +1042,13 @@ export class RunOrchestrator {
|
|
|
670
1042
|
: undefined,
|
|
671
1043
|
});
|
|
672
1044
|
results.push(result);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1045
|
+
// A chain halts on ANY failed link, QA or otherwise: every successor
|
|
1046
|
+
// rebases onto its predecessor's committed work, so continuing past a
|
|
1047
|
+
// failure would build on a broken or absent base. #795 removed a
|
|
1048
|
+
// `--qa-gate` branch here that re-tested for a QA failure before this
|
|
1049
|
+
// same unconditional break — it could never change the outcome.
|
|
1050
|
+
if (!result.success)
|
|
679
1051
|
break;
|
|
680
|
-
}
|
|
681
1052
|
}
|
|
682
1053
|
return results;
|
|
683
1054
|
}
|
|
@@ -762,11 +1133,12 @@ export class RunOrchestrator {
|
|
|
762
1133
|
});
|
|
763
1134
|
}
|
|
764
1135
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1136
|
+
// Record PR info, flip status on a PR-creation failure (#879), and
|
|
1137
|
+
// finalize — via the shared helper so this live path stays in lockstep
|
|
1138
|
+
// with executeBatch. Before #879 this block omitted the failure flip, so
|
|
1139
|
+
// a real `sequant run` left the run-log status at `success` on PR failure.
|
|
768
1140
|
if (logWriter) {
|
|
769
|
-
logWriter
|
|
1141
|
+
recordIssueCompletion(logWriter, result, parallelIssueNumber);
|
|
770
1142
|
}
|
|
771
1143
|
return result;
|
|
772
1144
|
}
|
|
@@ -779,9 +1151,13 @@ export class RunOrchestrator {
|
|
|
779
1151
|
});
|
|
780
1152
|
}
|
|
781
1153
|
}
|
|
782
|
-
static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers) {
|
|
1154
|
+
static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers, wallClockDurationSeconds) {
|
|
783
1155
|
const metricsWriter = new MetricsWriter({ verbose: config.verbose });
|
|
784
|
-
|
|
1156
|
+
// #867: run wall clock, not the sum of per-issue durations — under
|
|
1157
|
+
// --parallel that sum double-counts overlapping issues (same defect the
|
|
1158
|
+
// SUMMARY header had). The orchestrator owns the run's start/end and passes
|
|
1159
|
+
// the single authoritative value in.
|
|
1160
|
+
const totalDuration = wallClockDurationSeconds;
|
|
785
1161
|
const allPhases = new Set();
|
|
786
1162
|
for (const result of results) {
|
|
787
1163
|
for (const pr of result.phaseResults) {
|
|
@@ -826,6 +1202,11 @@ export class RunOrchestrator {
|
|
|
826
1202
|
cliFlags.push("--testgen");
|
|
827
1203
|
const tokenUsage = getTokenUsageForRun(undefined, true);
|
|
828
1204
|
const passed = results.filter((r) => r.success).length;
|
|
1205
|
+
// #761 AC-7: record why the run failed, as a bounded enum. Sequential and
|
|
1206
|
+
// chain runs halt at the first failure, so the first categorized failed
|
|
1207
|
+
// issue is the halting one; in parallel runs it is the earliest-listed
|
|
1208
|
+
// failure. Undefined on success — the field is omitted from the record.
|
|
1209
|
+
const failureCategory = results.find((r) => !r.success && r.failureCategory)?.failureCategory;
|
|
829
1210
|
await metricsWriter.recordRun({
|
|
830
1211
|
issues: issueNumbers,
|
|
831
1212
|
phases: Array.from(allPhases),
|
|
@@ -833,6 +1214,7 @@ export class RunOrchestrator {
|
|
|
833
1214
|
duration: totalDuration,
|
|
834
1215
|
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
835
1216
|
flags: cliFlags,
|
|
1217
|
+
failureCategory,
|
|
836
1218
|
metrics: {
|
|
837
1219
|
tokensUsed: tokenUsage.tokensUsed,
|
|
838
1220
|
filesChanged: totalFilesChanged,
|