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,12 +10,17 @@
|
|
|
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";
|
|
14
|
-
import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
|
|
13
|
+
import { classifyError, errorTypeToCategory, } from "./error-classifier.js";
|
|
14
|
+
import { getGitDiffStats, getCommitHash, resolveDiffBase, } from "./git-diff-utils.js";
|
|
15
15
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
16
|
-
import { executePhaseWithRetry } from "./phase-executor.js";
|
|
16
|
+
import { AUTO_WAIT_BUFFER_MS, createAutoWaitLedger, executePhaseWithRetry, isWindowExhaustedRateLimit, } from "./phase-executor.js";
|
|
17
|
+
import { BillingError, RateLimitError, resetsAtToMs } from "../errors.js";
|
|
18
|
+
import { parseBodyDependencyMarkers } from "./dependency-markers.js";
|
|
17
19
|
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, DOCS_LABELS, } from "./phase-mapper.js";
|
|
18
20
|
import { activateRelay, deactivateRelay, } from "../relay/activation.js";
|
|
21
|
+
import { getSettings } from "../settings.js";
|
|
22
|
+
import { GitHubProvider } from "./platforms/github.js";
|
|
23
|
+
import { runReadyGate, parseNonGoals, } from "./ready-gate.js";
|
|
19
24
|
/**
|
|
20
25
|
* Emit a structured progress line to stderr for MCP progress notifications.
|
|
21
26
|
* Only emits when running under an orchestrator (e.g., MCP server).
|
|
@@ -27,29 +32,95 @@ import { activateRelay, deactivateRelay, } from "../relay/activation.js";
|
|
|
27
32
|
* @param extra - Optional fields: durationSeconds (on complete), error (on failed)
|
|
28
33
|
*/
|
|
29
34
|
/**
|
|
30
|
-
* Wrap an `ExecutionConfig` with
|
|
31
|
-
* agent-output ping as
|
|
35
|
+
* Wrap an `ExecutionConfig` with the runtime liveness hooks:
|
|
36
|
+
* - `onActivity` — re-emits each agent-output ping as an `"activity"` progress
|
|
37
|
+
* event for the dashboard (#543).
|
|
38
|
+
* - `onAutoWait` — re-emits each auto-wait tick as a `"waiting"` progress
|
|
39
|
+
* event so the renderer and heartbeat can show the pause and its wake time
|
|
40
|
+
* (#804 AC-7). Under `SEQUANT_ORCHESTRATOR` it additionally emits throttled
|
|
41
|
+
* `SEQUANT_PROGRESS` waiting lines (#860): an MCP-driven wait was previously
|
|
42
|
+
* invisible on the JSON channel — indistinguishable from a hang — AND was
|
|
43
|
+
* killed by the MCP inactivity timeout, which resets on progress lines.
|
|
44
|
+
* Optionally notifies `onWaitTransition` on wait start/end so the caller
|
|
45
|
+
* can persist the wait to issue state (`sequant status` truthfulness).
|
|
32
46
|
*
|
|
33
|
-
* Returns the input config unchanged when no
|
|
34
|
-
*
|
|
47
|
+
* Returns the input config unchanged when there is no consumer at all (no
|
|
48
|
+
* `onProgress`, no orchestrator channel, no transition callback), so plain
|
|
49
|
+
* non-TUI runs pay no overhead.
|
|
35
50
|
*
|
|
36
51
|
* @internal Exported for testing only
|
|
37
52
|
*/
|
|
38
|
-
export function withActivityHook(base, issueNumber, phase, onProgress) {
|
|
39
|
-
|
|
53
|
+
export function withActivityHook(base, issueNumber, phase, onProgress, onWaitTransition) {
|
|
54
|
+
const orchestrated = Boolean(process.env.SEQUANT_ORCHESTRATOR);
|
|
55
|
+
if (!onProgress && !orchestrated && !onWaitTransition)
|
|
40
56
|
return base;
|
|
57
|
+
// Throttle the orchestrator waiting lines: the wait ticks every ~15s, and
|
|
58
|
+
// one JSON line per minute is enough to keep the MCP inactivity timeout
|
|
59
|
+
// alive (it resets on every SEQUANT_PROGRESS line) without bloating the
|
|
60
|
+
// captured stderr over a multi-hour pause.
|
|
61
|
+
let lastWaitLineAt = 0;
|
|
62
|
+
let waitAnnounced = false;
|
|
41
63
|
return {
|
|
42
64
|
...base,
|
|
43
65
|
onActivity: (text) => {
|
|
44
66
|
try {
|
|
45
|
-
onProgress(issueNumber, phase, "activity", { text });
|
|
67
|
+
onProgress?.(issueNumber, phase, "activity", { text });
|
|
46
68
|
}
|
|
47
69
|
catch {
|
|
48
70
|
// Activity events must never disrupt the run.
|
|
49
71
|
}
|
|
50
72
|
},
|
|
73
|
+
onAutoWait: (notice) => {
|
|
74
|
+
try {
|
|
75
|
+
onProgress?.(issueNumber, phase, "waiting", {
|
|
76
|
+
text: notice.message,
|
|
77
|
+
// Omitted on the terminal notice — its absence is what tells the
|
|
78
|
+
// consumers to clear the waiting state.
|
|
79
|
+
wakeAtMs: notice.done ? undefined : notice.wakeAtMs,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Liveness notices must never disrupt the run.
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if (notice.done) {
|
|
87
|
+
if (waitAnnounced) {
|
|
88
|
+
waitAnnounced = false;
|
|
89
|
+
lastWaitLineAt = 0;
|
|
90
|
+
emitProgressLine(issueNumber, phase, "waiting", {
|
|
91
|
+
remainingMs: 0,
|
|
92
|
+
});
|
|
93
|
+
onWaitTransition?.(null);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (!waitAnnounced) {
|
|
98
|
+
waitAnnounced = true;
|
|
99
|
+
onWaitTransition?.(notice.wakeAtMs);
|
|
100
|
+
}
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
if (now - lastWaitLineAt >= AUTO_WAIT_PROGRESS_LINE_INTERVAL_MS) {
|
|
103
|
+
lastWaitLineAt = now;
|
|
104
|
+
emitProgressLine(issueNumber, phase, "waiting", {
|
|
105
|
+
wakeAtMs: notice.wakeAtMs,
|
|
106
|
+
remainingMs: notice.remainingMs,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Liveness notices must never disrupt the run.
|
|
113
|
+
}
|
|
114
|
+
},
|
|
51
115
|
};
|
|
52
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Cadence of orchestrator-channel waiting lines during an auto-wait (#860).
|
|
119
|
+
* See {@link withActivityHook}.
|
|
120
|
+
*
|
|
121
|
+
* @internal Exported for testing only
|
|
122
|
+
*/
|
|
123
|
+
export const AUTO_WAIT_PROGRESS_LINE_INTERVAL_MS = 60_000;
|
|
53
124
|
/**
|
|
54
125
|
* Build enriched prompt context for the /loop phase from a failed phase result (#488).
|
|
55
126
|
* Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
|
|
@@ -93,6 +164,17 @@ export function emitProgressLine(issue, phase, event = "start", extra) {
|
|
|
93
164
|
if (extra?.iteration !== undefined) {
|
|
94
165
|
payload.iteration = extra.iteration;
|
|
95
166
|
}
|
|
167
|
+
// #860: auto-wait liveness. `wakeAtMs` is present while waiting and absent
|
|
168
|
+
// on the terminal notice (`remainingMs: 0`), mirroring the in-process
|
|
169
|
+
// ProgressCallback convention. Every line — waiting included — resets the
|
|
170
|
+
// MCP inactivity timeout (prefix-matched in spawnAsync), which is what
|
|
171
|
+
// keeps a legitimate multi-hour pause from being killed as "no progress".
|
|
172
|
+
if (extra?.wakeAtMs !== undefined) {
|
|
173
|
+
payload.wakeAtMs = extra.wakeAtMs;
|
|
174
|
+
}
|
|
175
|
+
if (extra?.remainingMs !== undefined) {
|
|
176
|
+
payload.remainingMs = extra.remainingMs;
|
|
177
|
+
}
|
|
96
178
|
const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
|
|
97
179
|
process.stderr.write(line);
|
|
98
180
|
}
|
|
@@ -137,17 +219,13 @@ export function parseDependencies(issueNumber) {
|
|
|
137
219
|
return [];
|
|
138
220
|
const data = JSON.parse(result.stdout.toString());
|
|
139
221
|
const dependencies = [];
|
|
140
|
-
// Parse from body: "Depends on: #123"
|
|
222
|
+
// Parse from body: line-leading "Depends on: #123" / "**Depends on**: #123".
|
|
223
|
+
// Delegates to the shared, hardened parser (#767): mid-sentence prose,
|
|
224
|
+
// in-fence examples, and inline-code mentions are ignored, and the `#` is
|
|
225
|
+
// required. Honors ONLY `depends on` — the sorter must not start reordering
|
|
226
|
+
// on `blocked by`, which would be a new silent-reorder class (#762 Open Q #3).
|
|
141
227
|
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
|
-
}
|
|
228
|
+
dependencies.push(...parseBodyDependencyMarkers(data.body, ["depends on"]));
|
|
151
229
|
}
|
|
152
230
|
// Parse from labels: "depends-on/123" or "depends-on-123"
|
|
153
231
|
if (data.labels && Array.isArray(data.labels)) {
|
|
@@ -257,8 +335,53 @@ export function getEnvConfig() {
|
|
|
257
335
|
if (process.env.SEQUANT_SECURITY_REVIEW === "true") {
|
|
258
336
|
config.securityReview = true;
|
|
259
337
|
}
|
|
338
|
+
// #804: the env layer for --auto-wait. `resolveRunOptions` does NOT route
|
|
339
|
+
// through the `ConfigResolver` class (it uses `??` chains + this function),
|
|
340
|
+
// so numeric coercion is explicit here rather than free via `coerceEnvValue`.
|
|
341
|
+
// Non-numeric and negative values are ignored so a typo cannot silently
|
|
342
|
+
// enable an unbounded wait.
|
|
343
|
+
if (process.env.SEQUANT_AUTO_WAIT_MINUTES) {
|
|
344
|
+
const autoWait = parseInt(process.env.SEQUANT_AUTO_WAIT_MINUTES, 10);
|
|
345
|
+
if (!isNaN(autoWait) && autoWait >= 0) {
|
|
346
|
+
config.autoWaitMinutes = autoWait;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
260
349
|
return config;
|
|
261
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Record an issue's completion in the run log in ONE place (#879): PR info, the
|
|
353
|
+
* PR-failure status flip, then finalize. Extracted so every batch loop shares a
|
|
354
|
+
* single completion sequence and cannot drift.
|
|
355
|
+
*
|
|
356
|
+
* The #879 defect was exactly such a drift: `markIssueFailed` was wired into
|
|
357
|
+
* `executeBatch`'s loop, but the live `sequant run` path is
|
|
358
|
+
* `RunOrchestrator.executeOneIssue`, which called `setPRInfo` + `completeIssue`
|
|
359
|
+
* without it — so a real run left the run-log status at `success` on a
|
|
360
|
+
* PR-creation failure. Both call sites now go through this helper.
|
|
361
|
+
*
|
|
362
|
+
* A PR-creation failure occurs after every phase has been logged, so
|
|
363
|
+
* `deriveIssueLogStatus` (last run at phase-log time) leaves the issue at
|
|
364
|
+
* `success`; the flip here is what counts it under `failed`. Safe post-hoc:
|
|
365
|
+
* no further phase is logged before `completeIssue`.
|
|
366
|
+
*/
|
|
367
|
+
export function recordIssueCompletion(logWriter, result, issueNumber) {
|
|
368
|
+
if (result.prNumber && result.prUrl) {
|
|
369
|
+
logWriter.setPRInfo(result.prNumber, result.prUrl, issueNumber);
|
|
370
|
+
}
|
|
371
|
+
if (result.prCreationError) {
|
|
372
|
+
logWriter.markIssueFailed(issueNumber);
|
|
373
|
+
}
|
|
374
|
+
logWriter.completeIssue(issueNumber);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* @deprecated No live caller — `sequant run` executes issues via
|
|
378
|
+
* `RunOrchestrator.executeOneIssue`; this survives only as a
|
|
379
|
+
* `commands/run-compat` re-export. Do not build a new execution loop on it:
|
|
380
|
+
* any path that completes an issue MUST go through
|
|
381
|
+
* {@link recordIssueCompletion}, or the #879 status-drift returns (a
|
|
382
|
+
* completion path that skips the PR-failure flip logs a failed issue as
|
|
383
|
+
* `success`). Slated for removal with the run-compat surface.
|
|
384
|
+
*/
|
|
262
385
|
export async function executeBatch(issueNumbers, batchCtx) {
|
|
263
386
|
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = batchCtx;
|
|
264
387
|
const results = [];
|
|
@@ -294,17 +417,221 @@ export async function executeBatch(issueNumbers, batchCtx) {
|
|
|
294
417
|
};
|
|
295
418
|
const result = await runIssueWithLogging(ctx);
|
|
296
419
|
results.push(result);
|
|
297
|
-
// Record PR info
|
|
298
|
-
|
|
299
|
-
logWriter.setPRInfo(result.prNumber, result.prUrl);
|
|
300
|
-
}
|
|
301
|
-
// Complete issue logging
|
|
420
|
+
// Record PR info, flip status on PR failure (#879), and finalize — all via
|
|
421
|
+
// the shared helper so this loop and the orchestrator path cannot drift.
|
|
302
422
|
if (logWriter) {
|
|
303
|
-
logWriter
|
|
423
|
+
recordIssueCompletion(logWriter, result, issueNumber);
|
|
304
424
|
}
|
|
305
425
|
}
|
|
306
426
|
return results;
|
|
307
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Derive the bounded-enum failure category for a failed issue (#761 AC-7).
|
|
430
|
+
*
|
|
431
|
+
* Scans for the LAST non-loop failing phase — the same reverse scan
|
|
432
|
+
* `toIssueSummary` uses (#766), so the recorded category and the displayed
|
|
433
|
+
* failure reason describe the same attempt. Prefers the driver's structured
|
|
434
|
+
* cause over stderr-regex classification (#732). Returns only the enum value;
|
|
435
|
+
* message strings never leave this function (metrics privacy contract).
|
|
436
|
+
*
|
|
437
|
+
* @internal Exported for testing
|
|
438
|
+
*/
|
|
439
|
+
export function deriveFailureCategory(phaseResults) {
|
|
440
|
+
const failedPhase = [...phaseResults]
|
|
441
|
+
.reverse()
|
|
442
|
+
.find((p) => !p.success && p.phase !== "loop");
|
|
443
|
+
if (!failedPhase)
|
|
444
|
+
return undefined;
|
|
445
|
+
const typedError = failedPhase.structuredError ??
|
|
446
|
+
classifyError(failedPhase.stderrTail ?? [], failedPhase.exitCode);
|
|
447
|
+
return errorTypeToCategory(typedError);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* "Halt, don't loop" predicate for the outer `-Q` quality loop (#799).
|
|
451
|
+
*
|
|
452
|
+
* A billing / out-of-credits failure (`BillingError`) or a window-exhausted
|
|
453
|
+
* rate limit (reset hours away, per `isWindowExhaustedRateLimit`) cannot be
|
|
454
|
+
* recovered by re-running the phase — every retry re-spawns into the same
|
|
455
|
+
* closed window and, worse, mislabels the halt as a downstream
|
|
456
|
+
* `QA completed without a parseable verdict`. Mirrors the `haltedByCap` (#739)
|
|
457
|
+
* treatment: surface the real cause and halt so the user resumes once credits
|
|
458
|
+
* or the rate-limit window are restored.
|
|
459
|
+
*
|
|
460
|
+
* A transient / metadata-absent rate limit is NOT a halt: it returns false and
|
|
461
|
+
* keeps today's outer-loop behavior (the inner retry ladder in `phase-executor`
|
|
462
|
+
* handles its backoff, per #761 AC-4/AC-9).
|
|
463
|
+
*
|
|
464
|
+
* @internal Exported for testing
|
|
465
|
+
*/
|
|
466
|
+
export function isBillingOrWindowHalt(result) {
|
|
467
|
+
return isBillingHalt(result) || isWindowHalt(result);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* The billing half of {@link isBillingOrWindowHalt} (#804 AC-8).
|
|
471
|
+
*
|
|
472
|
+
* Split out because the two causes stopped being interchangeable once
|
|
473
|
+
* `--auto-wait` existed: a closed window can now reopen on its own, while
|
|
474
|
+
* out-of-credits cannot — credits are purchased, not waited out. Callers that
|
|
475
|
+
* need to reason about recoverability must be able to tell them apart.
|
|
476
|
+
*
|
|
477
|
+
* @internal Exported for testing
|
|
478
|
+
*/
|
|
479
|
+
export function isBillingHalt(result) {
|
|
480
|
+
return result.structuredError instanceof BillingError;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The rate-limit-window half of {@link isBillingOrWindowHalt} (#804 AC-8).
|
|
484
|
+
*
|
|
485
|
+
* NOTE — this predicate needed no behavioral change for auto-wait, and that is
|
|
486
|
+
* a deliberate finding rather than an oversight. AC-8 anticipated that a phase
|
|
487
|
+
* which waits and then succeeds would still halt the `-Q` loop. It cannot:
|
|
488
|
+
* every call site (`:~700` spec, `:~1030` progress label, `:~1150` halt flag)
|
|
489
|
+
* sits inside the `else` of an `if (result.success)`, so a successful
|
|
490
|
+
* post-wait result never reaches this predicate at all. When auto-wait does
|
|
491
|
+
* NOT fire — the default, an exhausted budget, or a spent wait bound — the
|
|
492
|
+
* result is still a failure carrying a window-exhausted `RateLimitError`, and
|
|
493
|
+
* halting is then the correct outcome (#799 behavior, preserved exactly).
|
|
494
|
+
*
|
|
495
|
+
* @internal Exported for testing
|
|
496
|
+
*/
|
|
497
|
+
export function isWindowHalt(result) {
|
|
498
|
+
return isWindowExhaustedRateLimit(result.structuredError);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Human-readable halt reason for a billing / rate-limit-window failure (#799
|
|
502
|
+
* AC-3). Surfaces the driver's real cause verbatim — `result.error` is already
|
|
503
|
+
* the well-formatted message the driver built via `formatRateLimitMessage`
|
|
504
|
+
* (`Out of credits` for billing, `Rate limited — resets at <local time>` for a
|
|
505
|
+
* throttle with a known reset), so the phase-failed line and run summary name
|
|
506
|
+
* the actual cause instead of a downstream `QA completed without a parseable
|
|
507
|
+
* verdict`.
|
|
508
|
+
*
|
|
509
|
+
* Do NOT re-append `resetsAt` here: the rate-limit message already carries the
|
|
510
|
+
* reset time, and doing so produced a doubled, timezone-inconsistent string
|
|
511
|
+
* (`… resets at 07-24 14:32 — resets at 2026-…Z`). Credits failures carry no
|
|
512
|
+
* reset time by design (they need purchasing, not a window wait).
|
|
513
|
+
*
|
|
514
|
+
* @internal Exported for testing
|
|
515
|
+
*/
|
|
516
|
+
export function billingHaltReason(result) {
|
|
517
|
+
return result.error ?? "Out of credits";
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Epoch ms after which a waitable-window halt can be re-entered (#892 AC-1):
|
|
521
|
+
* the window's `resetsAt` normalized to ms plus the same buffer auto-wait
|
|
522
|
+
* applies (`AUTO_WAIT_BUFFER_MS`), so in-process waits and durable halts wake
|
|
523
|
+
* on the same clock. Returns `null` when the result carries no future-reset
|
|
524
|
+
* rate-limit window — callers must then skip the `windowHalt` write rather
|
|
525
|
+
* than invent a resume time.
|
|
526
|
+
*
|
|
527
|
+
* @internal Exported for testing
|
|
528
|
+
*/
|
|
529
|
+
export function windowHaltResumeAtMs(result) {
|
|
530
|
+
if (!isWindowHalt(result))
|
|
531
|
+
return null;
|
|
532
|
+
const err = result.structuredError;
|
|
533
|
+
if (!(err instanceof RateLimitError))
|
|
534
|
+
return null;
|
|
535
|
+
const resetsAt = err.metadata.resetsAt;
|
|
536
|
+
if (typeof resetsAt !== "number")
|
|
537
|
+
return null;
|
|
538
|
+
return resetsAtToMs(resetsAt) + AUTO_WAIT_BUFFER_MS;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Persist or clear the durable `windowHalt` record for a phase result (#892).
|
|
542
|
+
*
|
|
543
|
+
* A waitable-window failure writes `resumeAt` (preserving any re-entry count);
|
|
544
|
+
* every other outcome — success, or a failure whose cause is not a waitable
|
|
545
|
+
* window — clears the record so `sequant resume` never re-enters on a stale
|
|
546
|
+
* or non-waitable halt. Never throws: state bookkeeping must not mask the
|
|
547
|
+
* phase result it describes.
|
|
548
|
+
*/
|
|
549
|
+
async function recordWindowHaltState(stateManager, issueNumber, phase, result) {
|
|
550
|
+
if (!stateManager)
|
|
551
|
+
return;
|
|
552
|
+
try {
|
|
553
|
+
const resumeAtMs = result.success ? null : windowHaltResumeAtMs(result);
|
|
554
|
+
if (resumeAtMs !== null) {
|
|
555
|
+
await stateManager.updateWindowHalt(issueNumber, phase, resumeAtMs);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
await stateManager.clearWindowHalt(issueNumber);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// State tracking errors shouldn't stop execution
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Run the post-QA ready gate (#817) for a single issue at the run path's
|
|
567
|
+
* post-success / pre-PR seam.
|
|
568
|
+
*
|
|
569
|
+
* Mirrors `src/commands/ready.ts`'s driver: resolve the policy from
|
|
570
|
+
* `settings.ready.policy` (no per-run override — AC-4 forbids new surface),
|
|
571
|
+
* parse the issue's Non-Goals for report-only classification, wrap
|
|
572
|
+
* `executePhaseWithRetry` as the gate's phase runner, and delegate the whole
|
|
573
|
+
* qa→loop→qa loop to `runReadyGate`. The token budget stays disabled (parity
|
|
574
|
+
* with `sequant ready` invoked without `--budget`); the `maxIterations` cap
|
|
575
|
+
* already bounds cost.
|
|
576
|
+
*
|
|
577
|
+
* A gate failure is non-fatal: the standard-phase work is already committed to
|
|
578
|
+
* the worktree, so we log a warning and fall through to normal PR creation
|
|
579
|
+
* rather than aborting the run (the issue then keeps its `ready_for_merge`
|
|
580
|
+
* status — the run has degraded to a standard run, and nothing about the work
|
|
581
|
+
* is actually blocked).
|
|
582
|
+
*
|
|
583
|
+
* The failure is returned rather than swallowed. A dropped gate must not be
|
|
584
|
+
* invisible: the caller opted in with `--ready-gate`, so a run whose gate never
|
|
585
|
+
* executed has to look different in the summary from one that gated cleanly —
|
|
586
|
+
* otherwise a crashed gate is indistinguishable from an approved one, and the
|
|
587
|
+
* whole point of the flag (a second look actually happened) is silently lost.
|
|
588
|
+
*/
|
|
589
|
+
async function runReadyGateForIssue(args) {
|
|
590
|
+
const { issueNumber, worktreePath, config, shutdownManager, phasePauseHandle, onProgress, log, } = args;
|
|
591
|
+
const runGate = args.runGate ?? runReadyGate;
|
|
592
|
+
const getSettingsFn = args.getSettingsFn ?? getSettings;
|
|
593
|
+
const fetchBody = args.fetchBody ??
|
|
594
|
+
((n) => new GitHubProvider().fetchIssueBodySync(String(n)));
|
|
595
|
+
try {
|
|
596
|
+
const settings = await getSettingsFn();
|
|
597
|
+
const policy = settings.ready.policy;
|
|
598
|
+
// Non-Goals feed the gate's report-only classification (ac mode never
|
|
599
|
+
// auto-fixes Non-Goal-touching findings). Best-effort — an unavailable
|
|
600
|
+
// body just yields no Non-Goals.
|
|
601
|
+
const body = fetchBody(issueNumber);
|
|
602
|
+
const nonGoals = body ? parseNonGoals(body) : [];
|
|
603
|
+
// The gate's phase runner: same executePhaseWithRetry wrapper ready.ts
|
|
604
|
+
// uses, bound to this issue's worktree, shutdown manager, and pause handle.
|
|
605
|
+
const runPhase = (phase, phaseConfig, wt) => executePhaseWithRetry(issueNumber, phase, phaseConfig, undefined, wt, shutdownManager, phasePauseHandle);
|
|
606
|
+
log(chalk.blue(`\n Ready gate (#817) — policy: ${policy}, max iterations: ${config.maxIterations}`));
|
|
607
|
+
const result = await runGate({
|
|
608
|
+
issueNumber,
|
|
609
|
+
worktreePath,
|
|
610
|
+
policy,
|
|
611
|
+
maxIterations: config.maxIterations,
|
|
612
|
+
// AC-4: budget stays disabled on the run path (parity with `ready` sans
|
|
613
|
+
// `--budget`); maxIterations bounds cost.
|
|
614
|
+
tokenBudget: undefined,
|
|
615
|
+
nonGoals,
|
|
616
|
+
phaseTimeout: config.phaseTimeout,
|
|
617
|
+
mcp: config.mcp,
|
|
618
|
+
verbose: config.verbose,
|
|
619
|
+
runPhase,
|
|
620
|
+
onProgress,
|
|
621
|
+
});
|
|
622
|
+
log(result.ready
|
|
623
|
+
? chalk.green(` ✓ Ready gate: ${result.reason} — awaiting human merge (never merged)`)
|
|
624
|
+
: chalk.yellow(` ⚠️ Ready gate halted: ${result.reason} — needs human review`));
|
|
625
|
+
return { result };
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
// Non-fatal: keep the run going to PR with the standard status, but hand
|
|
629
|
+
// the reason back so the summary can say the gate did NOT run.
|
|
630
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
631
|
+
log(chalk.yellow(` ⚠️ Ready gate failed for #${issueNumber}: ${error} — continuing to PR without the gate.`));
|
|
632
|
+
return { error };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
308
635
|
export async function runIssueWithLogging(ctx) {
|
|
309
636
|
// Destructure context for use throughout the function
|
|
310
637
|
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
|
|
@@ -317,6 +644,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
317
644
|
let loopTriggered = false;
|
|
318
645
|
// Cross-phase resume token, driver-tagged and cwd-bound (#674).
|
|
319
646
|
let resumeHandle;
|
|
647
|
+
// #804 AC-6: ONE ledger for the whole issue. Created here rather than inside
|
|
648
|
+
// `executePhaseWithRetry` because the bound and the budget are per-issue —
|
|
649
|
+
// a per-phase ledger would silently grant every phase its own full budget.
|
|
650
|
+
const autoWaitLedger = createAutoWaitLedger(config.autoWaitMinutes);
|
|
320
651
|
// In parallel mode, suppress per-issue terminal output to prevent interleaving.
|
|
321
652
|
// The caller (run.ts) handles progress display via updateProgress().
|
|
322
653
|
const log = config.parallel ? () => { } : console.log.bind(console);
|
|
@@ -350,6 +681,19 @@ export async function runIssueWithLogging(ctx) {
|
|
|
350
681
|
}
|
|
351
682
|
}
|
|
352
683
|
}
|
|
684
|
+
// #860: persist auto-wait transitions to issue state so `sequant status`
|
|
685
|
+
// reports "waiting until <wake>" instead of an hours-stale in-progress
|
|
686
|
+
// phase. Fire-and-forget — state bookkeeping must never disturb the wait
|
|
687
|
+
// it describes.
|
|
688
|
+
const makeWaitTransition = (phase) => stateManager
|
|
689
|
+
? (wakeAtMs) => {
|
|
690
|
+
void stateManager
|
|
691
|
+
.updateAutoWait(issueNumber, phase, wakeAtMs)
|
|
692
|
+
.catch(() => {
|
|
693
|
+
// Never let state bookkeeping disturb a live wait.
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
: undefined;
|
|
353
697
|
// Activate relay (#383) if enabled. Tolerates errors — relay must never
|
|
354
698
|
// block the underlying run.
|
|
355
699
|
let relayActivation = null;
|
|
@@ -402,8 +746,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
402
746
|
}
|
|
403
747
|
const specStartTime = new Date();
|
|
404
748
|
// Note: spec runs in main repo (not worktree) for planning
|
|
405
|
-
const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), resumeHandle, worktreePath, // Will be ignored for spec (non-isolated phase)
|
|
406
|
-
shutdownManager, phasePauseHandle
|
|
749
|
+
const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress, makeWaitTransition("spec")), resumeHandle, worktreePath, // Will be ignored for spec (non-isolated phase)
|
|
750
|
+
shutdownManager, phasePauseHandle, undefined, // executePhaseFn — use the default
|
|
751
|
+
undefined, // delayFn — use the default
|
|
752
|
+
autoWaitLedger);
|
|
407
753
|
const specEndTime = new Date();
|
|
408
754
|
if (specResult.resumeHandle) {
|
|
409
755
|
resumeHandle = specResult.resumeHandle;
|
|
@@ -432,16 +778,19 @@ export async function runIssueWithLogging(ctx) {
|
|
|
432
778
|
}
|
|
433
779
|
}
|
|
434
780
|
else {
|
|
435
|
-
// Mirror the main phase loop (#739): a turn-capped spec phase surfaces
|
|
436
|
-
// distinct "partial output preserved" signal
|
|
437
|
-
//
|
|
438
|
-
// failure handling, separate from the
|
|
439
|
-
//
|
|
440
|
-
//
|
|
781
|
+
// Mirror the main phase loop (#739/#799): a turn-capped spec phase surfaces
|
|
782
|
+
// the distinct "partial output preserved" signal, and a billing /
|
|
783
|
+
// rate-limit-window failure names the real cause — so both are recognizable
|
|
784
|
+
// on the spec path too (it has its own failure handling, separate from the
|
|
785
|
+
// main loop). The spec phase already halts on any failure via the early
|
|
786
|
+
// return below; routing billing through billingHaltReason only keeps the
|
|
787
|
+
// message/fallback symmetric with the main loop.
|
|
441
788
|
const extra = {
|
|
442
789
|
error: specResult.capped
|
|
443
790
|
? "turn cap reached — partial output preserved (resume to continue)"
|
|
444
|
-
: (specResult
|
|
791
|
+
: isBillingOrWindowHalt(specResult)
|
|
792
|
+
? billingHaltReason(specResult)
|
|
793
|
+
: (specResult.error ?? "unknown"),
|
|
445
794
|
};
|
|
446
795
|
emitProgressLine(issueNumber, "spec", "failed", extra);
|
|
447
796
|
try {
|
|
@@ -454,10 +803,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
454
803
|
// Log spec phase result
|
|
455
804
|
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
456
805
|
if (logWriter) {
|
|
457
|
-
// Build errorContext from captured stderr/stdout tails (#447)
|
|
806
|
+
// Build errorContext from captured stderr/stdout tails (#447). Prefer
|
|
807
|
+
// the driver's structured cause (#761 AC-6) — stderr-regex
|
|
808
|
+
// classification never sees the SDK's rate-limit/billing signals.
|
|
458
809
|
let specErrorContext;
|
|
459
810
|
if (!specResult.success && specResult.stderrTail) {
|
|
460
|
-
const specError =
|
|
811
|
+
const specError = specResult.structuredError ??
|
|
812
|
+
classifyError(specResult.stderrTail ?? [], specResult.exitCode);
|
|
461
813
|
specErrorContext = {
|
|
462
814
|
stderrTail: specResult.stderrTail ?? [],
|
|
463
815
|
stdoutTail: specResult.stdoutTail ?? [],
|
|
@@ -496,6 +848,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
496
848
|
// State tracking errors shouldn't stop execution
|
|
497
849
|
}
|
|
498
850
|
}
|
|
851
|
+
// Durable halt-and-resume (#892 AC-1): a waitable-window spec halt writes
|
|
852
|
+
// `resumeAt` so `sequant resume` can re-enter after the window reopens.
|
|
853
|
+
await recordWindowHaltState(stateManager, issueNumber, "spec", specResult);
|
|
499
854
|
if (!specResult.success) {
|
|
500
855
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
501
856
|
// Archive relay state on early exit (spec failure).
|
|
@@ -518,6 +873,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
518
873
|
phaseResults,
|
|
519
874
|
durationSeconds,
|
|
520
875
|
loopTriggered: false,
|
|
876
|
+
failureCategory: deriveFailureCategory(phaseResults),
|
|
521
877
|
};
|
|
522
878
|
}
|
|
523
879
|
// Parse recommended workflow from spec output
|
|
@@ -618,6 +974,11 @@ export async function runIssueWithLogging(ctx) {
|
|
|
618
974
|
// retry too, not just the inner /loop spawn — re-running a capped phase
|
|
619
975
|
// would only cap again, and "surface + halt" means the user resumes.
|
|
620
976
|
let haltedByCap = false;
|
|
977
|
+
// Set when a phase fails with a billing / out-of-credits error or a
|
|
978
|
+
// window-exhausted rate limit (#799): like the turn cap, re-running the phase
|
|
979
|
+
// (or spawning /loop) cannot succeed while the window is closed, so halt the
|
|
980
|
+
// outer quality loop and let the user resume once credits/window are restored.
|
|
981
|
+
let haltedByBilling = false;
|
|
621
982
|
while (iteration < maxIterations) {
|
|
622
983
|
iteration++;
|
|
623
984
|
if (useQualityLoop && iteration > 1) {
|
|
@@ -650,7 +1011,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
650
1011
|
}
|
|
651
1012
|
}
|
|
652
1013
|
const phaseStartTime = new Date();
|
|
653
|
-
const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle
|
|
1014
|
+
const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress, makeWaitTransition(phase)), resumeHandle, worktreePath, shutdownManager, phasePauseHandle, undefined, // executePhaseFn — use the default
|
|
1015
|
+
undefined, // delayFn — use the default
|
|
1016
|
+
autoWaitLedger);
|
|
654
1017
|
const phaseEndTime = new Date();
|
|
655
1018
|
// Capture resume handle for subsequent phases (#674).
|
|
656
1019
|
if (result.resumeHandle) {
|
|
@@ -687,7 +1050,12 @@ export async function runIssueWithLogging(ctx) {
|
|
|
687
1050
|
const extra = {
|
|
688
1051
|
error: result.capped
|
|
689
1052
|
? "turn cap reached — partial output preserved (resume to continue)"
|
|
690
|
-
: (result
|
|
1053
|
+
: isBillingOrWindowHalt(result)
|
|
1054
|
+
? // Billing / rate-limit-window halt (#799): name the real cause so
|
|
1055
|
+
// the run summary doesn't cascade into a downstream
|
|
1056
|
+
// `QA completed without a parseable verdict`.
|
|
1057
|
+
billingHaltReason(result)
|
|
1058
|
+
: (result.error ?? "unknown"),
|
|
691
1059
|
iteration,
|
|
692
1060
|
};
|
|
693
1061
|
emitProgressLine(issueNumber, phase, "failed", extra);
|
|
@@ -700,20 +1068,31 @@ export async function runIssueWithLogging(ctx) {
|
|
|
700
1068
|
}
|
|
701
1069
|
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
702
1070
|
if (logWriter) {
|
|
1071
|
+
// Resolve the diff base once (#878): worktrees branch from
|
|
1072
|
+
// origin/<base>, so both the diff stats and the phase-commit check
|
|
1073
|
+
// must compare against the resolved ref, not the local branch name.
|
|
1074
|
+
const resolvedDiffBase = worktreePath
|
|
1075
|
+
? resolveDiffBase(worktreePath, baseBranch ?? "main")
|
|
1076
|
+
: undefined;
|
|
703
1077
|
// Capture git diff stats for worktree phases (AC-1, AC-3)
|
|
704
|
-
const diffStats = worktreePath
|
|
705
|
-
? getGitDiffStats(worktreePath,
|
|
1078
|
+
const diffStats = worktreePath && resolvedDiffBase
|
|
1079
|
+
? getGitDiffStats(worktreePath, resolvedDiffBase)
|
|
706
1080
|
: undefined;
|
|
707
|
-
// Capture commit hash after phase (AC-2)
|
|
1081
|
+
// Capture commit hash after phase (AC-2) — undefined when the branch
|
|
1082
|
+
// never moved off its base, so a base tip is not logged as the
|
|
1083
|
+
// phase's commit (#878).
|
|
708
1084
|
const commitHash = worktreePath
|
|
709
|
-
? getCommitHash(worktreePath)
|
|
1085
|
+
? getCommitHash(worktreePath, resolvedDiffBase)
|
|
710
1086
|
: undefined;
|
|
711
1087
|
// Read cache metrics for QA phase (AC-7)
|
|
712
1088
|
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
713
|
-
// Build errorContext from captured stderr/stdout tails (#447, AC-7/AC-8)
|
|
1089
|
+
// Build errorContext from captured stderr/stdout tails (#447, AC-7/AC-8).
|
|
1090
|
+
// Prefer the driver's structured cause (#761 AC-6) — stderr-regex
|
|
1091
|
+
// classification never sees the SDK's rate-limit/billing signals.
|
|
714
1092
|
let errorContext;
|
|
715
1093
|
if (!result.success && result.stderrTail) {
|
|
716
|
-
const typedError =
|
|
1094
|
+
const typedError = result.structuredError ??
|
|
1095
|
+
classifyError(result.stderrTail ?? [], result.exitCode);
|
|
717
1096
|
errorContext = {
|
|
718
1097
|
stderrTail: result.stderrTail ?? [],
|
|
719
1098
|
stdoutTail: result.stdoutTail ?? [],
|
|
@@ -764,6 +1143,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
764
1143
|
// State tracking errors shouldn't stop execution
|
|
765
1144
|
}
|
|
766
1145
|
}
|
|
1146
|
+
// Durable halt-and-resume (#892 AC-1): a waitable-window halt writes
|
|
1147
|
+
// `resumeAt`; success or a non-window failure clears any stale record.
|
|
1148
|
+
await recordWindowHaltState(stateManager, issueNumber, phase, result);
|
|
767
1149
|
if (result.success) {
|
|
768
1150
|
// Phase succeeded — RunRenderer (#618) updates state via onProgress.
|
|
769
1151
|
}
|
|
@@ -772,12 +1154,24 @@ export async function runIssueWithLogging(ctx) {
|
|
|
772
1154
|
if (result.capped) {
|
|
773
1155
|
haltedByCap = true;
|
|
774
1156
|
}
|
|
1157
|
+
// Billing / rate-limit-window failure (#799): halt the outer quality
|
|
1158
|
+
// loop for the same reason as the turn cap — re-running the phase or
|
|
1159
|
+
// spawning /loop cannot succeed while credits/window are exhausted, and
|
|
1160
|
+
// doing so mislabels the halt as a downstream unparseable-verdict error.
|
|
1161
|
+
if (isBillingOrWindowHalt(result)) {
|
|
1162
|
+
haltedByBilling = true;
|
|
1163
|
+
}
|
|
775
1164
|
// If quality loop enabled, run loop phase to fix issues.
|
|
776
1165
|
// A turn-capped phase (#739) is incomplete, not a genuine quality
|
|
777
1166
|
// failure: skip the loop and halt cleanly ("surface + halt"). Spawning
|
|
778
1167
|
// /loop on partial output would act on incomplete work — exactly the
|
|
779
1168
|
// risk the capped path is meant to avoid. The user resumes instead.
|
|
780
|
-
|
|
1169
|
+
// A billing / rate-limit-window halt (#799) is skipped for the same
|
|
1170
|
+
// reason: /loop would re-spawn into the same closed window.
|
|
1171
|
+
if (useQualityLoop &&
|
|
1172
|
+
iteration < maxIterations &&
|
|
1173
|
+
!result.capped &&
|
|
1174
|
+
!haltedByBilling) {
|
|
781
1175
|
// #624 Item 3 (AC-3.3): the loop phase carries the current outer
|
|
782
1176
|
// iteration so the live-zone status cell can show `loop N/M`.
|
|
783
1177
|
const loopStartExtra = { iteration };
|
|
@@ -798,9 +1192,28 @@ export async function runIssueWithLogging(ctx) {
|
|
|
798
1192
|
promptContext: buildLoopContext(result),
|
|
799
1193
|
};
|
|
800
1194
|
const loopStartTime = new Date();
|
|
801
|
-
const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle
|
|
1195
|
+
const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress, makeWaitTransition("loop")), resumeHandle, worktreePath, shutdownManager, phasePauseHandle, undefined, // executePhaseFn — use the default
|
|
1196
|
+
undefined, // delayFn — use the default
|
|
1197
|
+
autoWaitLedger);
|
|
802
1198
|
const loopEndTime = new Date();
|
|
803
1199
|
phaseResults.push(loopResult);
|
|
1200
|
+
// #766: record the loop phase in the run log — spec (:655) and the
|
|
1201
|
+
// regular phases (:982) log via logWriter, but the loop was never
|
|
1202
|
+
// logged, so a loop that decided the card's verdict was absent from
|
|
1203
|
+
// the log you'd use to debug it (AC-6). Loop status never determines
|
|
1204
|
+
// the issue verdict (see deriveIssueLogStatus), but the entry with
|
|
1205
|
+
// phase/status/duration/error must exist.
|
|
1206
|
+
if (logWriter) {
|
|
1207
|
+
const loopPhaseLog = createPhaseLogFromTiming("loop", issueNumber, loopStartTime, loopEndTime, loopResult.success
|
|
1208
|
+
? "success"
|
|
1209
|
+
: loopResult.error?.includes("Timeout")
|
|
1210
|
+
? "timeout"
|
|
1211
|
+
: "failure", {
|
|
1212
|
+
error: loopResult.error,
|
|
1213
|
+
capped: loopResult.capped,
|
|
1214
|
+
});
|
|
1215
|
+
logWriter.logPhase(loopPhaseLog);
|
|
1216
|
+
}
|
|
804
1217
|
// Emit loop completion/failure progress event (AC-8)
|
|
805
1218
|
const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
|
|
806
1219
|
if (loopResult.success) {
|
|
@@ -842,7 +1255,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
842
1255
|
}
|
|
843
1256
|
// A turn-capped phase (#739) halts the outer quality-loop retry as well —
|
|
844
1257
|
// re-running would only cap again; the partial work is already preserved.
|
|
845
|
-
|
|
1258
|
+
// A billing / rate-limit-window failure (#799) halts for the same reason:
|
|
1259
|
+
// the retry re-spawns into the same closed window and cannot progress.
|
|
1260
|
+
if (haltedByCap || haltedByBilling) {
|
|
846
1261
|
break;
|
|
847
1262
|
}
|
|
848
1263
|
// If we're not in quality loop mode, don't retry
|
|
@@ -854,19 +1269,73 @@ export async function runIssueWithLogging(ctx) {
|
|
|
854
1269
|
// Success is determined by whether all phases completed in any iteration,
|
|
855
1270
|
// not whether all accumulated phase results passed (which would fail after loop recovery)
|
|
856
1271
|
const success = completedSuccessfully;
|
|
857
|
-
//
|
|
1272
|
+
// #817: opt-in post-QA ready gate. When the standard phases succeed AND
|
|
1273
|
+
// `--ready-gate` was passed, drive the existing `sequant ready` engine
|
|
1274
|
+
// (qa→loop→qa to the configured policy) against this worktree BEFORE
|
|
1275
|
+
// checkpoint/rebase/PR — so the gate's auto-fix commits land in the PR. The
|
|
1276
|
+
// engine NEVER merges; it terminates with the issue `waiting_for_human_merge`
|
|
1277
|
+
// (ready) or `blocked` (guard halt). Without the flag this block is skipped
|
|
1278
|
+
// entirely, keeping the run path byte-identical (AC-5).
|
|
1279
|
+
const readyGateOutcome = config.readyGate && success && worktreePath
|
|
1280
|
+
? await runReadyGateForIssue({
|
|
1281
|
+
issueNumber,
|
|
1282
|
+
worktreePath,
|
|
1283
|
+
config,
|
|
1284
|
+
shutdownManager,
|
|
1285
|
+
phasePauseHandle,
|
|
1286
|
+
onProgress,
|
|
1287
|
+
log,
|
|
1288
|
+
})
|
|
1289
|
+
: undefined;
|
|
1290
|
+
const readyGateResult = readyGateOutcome?.result;
|
|
1291
|
+
// Surfaced separately from `readyGateResult` so a gate that *crashed* renders
|
|
1292
|
+
// differently in the summary from one that ran — a silently-skipped gate on a
|
|
1293
|
+
// run the user explicitly opted into is the failure mode worth naming.
|
|
1294
|
+
const readyGateError = readyGateOutcome?.error;
|
|
1295
|
+
// Update final issue status in state. When the gate ran it owns the terminal
|
|
1296
|
+
// status (never `ready_for_merge` — that would read as auto-merge-ready and
|
|
1297
|
+
// defeat the human merge gate the gate deliberately stops at).
|
|
1298
|
+
// Hoisted out of the `if (stateManager)` block below because the checkpoint
|
|
1299
|
+
// warning also has to name this status, and naming the wrong one is exactly
|
|
1300
|
+
// the #837 inaccuracy being fixed here.
|
|
1301
|
+
const finalStatus = readyGateResult
|
|
1302
|
+
? readyGateResult.issueStatus
|
|
1303
|
+
: success
|
|
1304
|
+
? "ready_for_merge"
|
|
1305
|
+
: "in_progress";
|
|
858
1306
|
if (stateManager) {
|
|
859
1307
|
try {
|
|
860
|
-
const finalStatus = success ? "ready_for_merge" : "in_progress";
|
|
861
1308
|
await stateManager.updateIssueStatus(issueNumber, finalStatus);
|
|
862
1309
|
}
|
|
863
1310
|
catch {
|
|
864
1311
|
// State tracking errors shouldn't stop execution
|
|
865
1312
|
}
|
|
866
1313
|
}
|
|
867
|
-
// Create checkpoint commit in chain mode after QA passes
|
|
1314
|
+
// Create checkpoint commit in chain mode after QA passes.
|
|
1315
|
+
// #760: chain resume rebases the next link onto this checkpoint, so a failure
|
|
1316
|
+
// here is not silent — warn prominently and record it on the result (AC-4).
|
|
1317
|
+
//
|
|
1318
|
+
// Note a completed status was already written above — `ready_for_merge`, or
|
|
1319
|
+
// `waiting_for_human_merge` when #817's `--ready-gate` owned the terminal
|
|
1320
|
+
// status (#837) — so a re-run reads this link as a completed prefix and does
|
|
1321
|
+
// NOT redo it. Its uncommitted work is therefore absent from the branch tip,
|
|
1322
|
+
// which `computeChainResumePlan` detects (dirty worktree → fail fast) rather
|
|
1323
|
+
// than wrong-basing the next link. The message states that outcome exactly:
|
|
1324
|
+
// the work must be committed, or --force.
|
|
1325
|
+
//
|
|
1326
|
+
// A gate that halted (`blocked`) is NOT a completed prefix, so that link is
|
|
1327
|
+
// re-executed on resume rather than skipped — see COMPLETED_STATUSES in
|
|
1328
|
+
// chain-resume.ts.
|
|
1329
|
+
let checkpointFailed = false;
|
|
868
1330
|
if (success && chainMode && worktreePath) {
|
|
869
|
-
createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
|
|
1331
|
+
const checkpointOk = createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
|
|
1332
|
+
if (!checkpointOk) {
|
|
1333
|
+
checkpointFailed = true;
|
|
1334
|
+
log(chalk.yellow(` ⚠️ Checkpoint commit for #${issueNumber} could not be created — its uncommitted ` +
|
|
1335
|
+
`changes are NOT on branch ${branch ?? "the feature branch"}. #${issueNumber} stays ` +
|
|
1336
|
+
`${finalStatus}, so a re-run will skip it and refuse to resume the chain here until the ` +
|
|
1337
|
+
`work is committed in ${worktreePath} (or re-run with --force to redo the whole chain).`));
|
|
1338
|
+
}
|
|
870
1339
|
}
|
|
871
1340
|
// Rebase onto the base branch before PR creation (unless --no-rebase)
|
|
872
1341
|
// This ensures the branch is up-to-date and prevents lockfile drift
|
|
@@ -883,6 +1352,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
883
1352
|
// Create PR after successful QA + rebase (unless --no-pr)
|
|
884
1353
|
let prNumber;
|
|
885
1354
|
let prUrl;
|
|
1355
|
+
// #879: a PR-creation failure after passing QA must fail the run, not print a
|
|
1356
|
+
// warning and leave the issue at `success`. Recorded here and folded into the
|
|
1357
|
+
// returned `success` below.
|
|
1358
|
+
let prCreationError;
|
|
886
1359
|
const shouldCreatePR = success && worktreePath && branch && !options.noPr;
|
|
887
1360
|
if (shouldCreatePR) {
|
|
888
1361
|
// #605: under --stacked, target predecessor branch (only for non-first,
|
|
@@ -893,7 +1366,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
893
1366
|
stackManifest: chain.stackManifest,
|
|
894
1367
|
}
|
|
895
1368
|
: undefined;
|
|
896
|
-
|
|
1369
|
+
// #749: surface a non-A+ qa verdict (e.g. AC_MET_BUT_NOT_A_PLUS) in the PR
|
|
1370
|
+
// body so a reviewer sees why the run broke to PR rather than reaching A+.
|
|
1371
|
+
const qaVerdict = phaseResults.find((p) => p.phase === "qa")?.verdict;
|
|
1372
|
+
const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions, qaVerdict,
|
|
1373
|
+
// #817 AC-6: surface the ready-gate outcome in the PR body the same way
|
|
1374
|
+
// `sequant ready` reports it (threshold reached vs guard halt).
|
|
1375
|
+
readyGateResult?.report);
|
|
897
1376
|
if (prResult.success && prResult.prNumber && prResult.prUrl) {
|
|
898
1377
|
prNumber = prResult.prNumber;
|
|
899
1378
|
prUrl = prResult.prUrl;
|
|
@@ -910,6 +1389,11 @@ export async function runIssueWithLogging(ctx) {
|
|
|
910
1389
|
}
|
|
911
1390
|
}
|
|
912
1391
|
}
|
|
1392
|
+
else if (prResult.attempted && !prResult.success) {
|
|
1393
|
+
// #879: PR creation was attempted (branch/QA passed) but failed. This is
|
|
1394
|
+
// a run failure — the deliverable never reached GitHub.
|
|
1395
|
+
prCreationError = prResult.error ?? "PR creation failed";
|
|
1396
|
+
}
|
|
913
1397
|
}
|
|
914
1398
|
// Deactivate relay (#383) — archive inbox/outbox transcripts to
|
|
915
1399
|
// .sequant/logs/relay/ before worktree teardown (AC-D2). Never throws.
|
|
@@ -928,13 +1412,25 @@ export async function runIssueWithLogging(ctx) {
|
|
|
928
1412
|
}
|
|
929
1413
|
}
|
|
930
1414
|
}
|
|
1415
|
+
// #879: fold a PR-creation failure into the issue's overall verdict. Phases
|
|
1416
|
+
// all passed, but the run did not deliver — report it as failed.
|
|
1417
|
+
const overallSuccess = success && !prCreationError;
|
|
931
1418
|
return {
|
|
932
1419
|
issueNumber,
|
|
933
|
-
success,
|
|
1420
|
+
success: overallSuccess,
|
|
934
1421
|
phaseResults,
|
|
935
1422
|
durationSeconds,
|
|
936
1423
|
loopTriggered,
|
|
937
1424
|
prNumber,
|
|
938
1425
|
prUrl,
|
|
1426
|
+
prCreationError,
|
|
1427
|
+
checkpointFailed,
|
|
1428
|
+
failureCategory: overallSuccess
|
|
1429
|
+
? undefined
|
|
1430
|
+
: deriveFailureCategory(phaseResults),
|
|
1431
|
+
// #817: present only when `--ready-gate` ran the gate; the summary renders
|
|
1432
|
+
// its terminal reason (AC-6).
|
|
1433
|
+
readyGate: readyGateResult,
|
|
1434
|
+
readyGateError,
|
|
939
1435
|
};
|
|
940
1436
|
}
|