sequant 2.9.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 +14 -3
- package/dist/bin/cli.js +97 -17
- 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 +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- 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 +3 -1
- 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/release/SKILL.md +1 -1
- 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/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 +14 -5
- 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.js +37 -4
- 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/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/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 +62 -0
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- 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/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 +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +477 -44
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- 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/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 +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
- 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 +75 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +457 -52
- 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 +14 -0
- package/dist/src/lib/workflow/run-log-schema.js +47 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +141 -20
- 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/types.d.ts +126 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
- package/dist/src/lib/workflow/worktree-manager.js +102 -23
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -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 +8 -7
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +75 -10
- package/templates/scripts/cleanup-worktree.sh +182 -19
- package/templates/scripts/new-feature.sh +264 -16
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +215 -390
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +26 -6
- package/templates/skills/fullsolve/SKILL.md +3 -1
- 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/release/SKILL.md +1 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +2 -0
- 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
|
@@ -11,12 +11,16 @@ import chalk from "chalk";
|
|
|
11
11
|
import { spawnSync } from "child_process";
|
|
12
12
|
import { createPhaseLogFromTiming } from "./log-writer.js";
|
|
13
13
|
import { classifyError, errorTypeToCategory, } from "./error-classifier.js";
|
|
14
|
-
import { getGitDiffStats, getCommitHash } from "./git-diff-utils.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";
|
|
17
18
|
import { parseBodyDependencyMarkers } from "./dependency-markers.js";
|
|
18
19
|
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, DOCS_LABELS, } from "./phase-mapper.js";
|
|
19
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";
|
|
20
24
|
/**
|
|
21
25
|
* Emit a structured progress line to stderr for MCP progress notifications.
|
|
22
26
|
* Only emits when running under an orchestrator (e.g., MCP server).
|
|
@@ -28,29 +32,95 @@ import { activateRelay, deactivateRelay, } from "../relay/activation.js";
|
|
|
28
32
|
* @param extra - Optional fields: durationSeconds (on complete), error (on failed)
|
|
29
33
|
*/
|
|
30
34
|
/**
|
|
31
|
-
* Wrap an `ExecutionConfig` with
|
|
32
|
-
* 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).
|
|
33
46
|
*
|
|
34
|
-
* Returns the input config unchanged when no
|
|
35
|
-
*
|
|
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.
|
|
36
50
|
*
|
|
37
51
|
* @internal Exported for testing only
|
|
38
52
|
*/
|
|
39
|
-
export function withActivityHook(base, issueNumber, phase, onProgress) {
|
|
40
|
-
|
|
53
|
+
export function withActivityHook(base, issueNumber, phase, onProgress, onWaitTransition) {
|
|
54
|
+
const orchestrated = Boolean(process.env.SEQUANT_ORCHESTRATOR);
|
|
55
|
+
if (!onProgress && !orchestrated && !onWaitTransition)
|
|
41
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;
|
|
42
63
|
return {
|
|
43
64
|
...base,
|
|
44
65
|
onActivity: (text) => {
|
|
45
66
|
try {
|
|
46
|
-
onProgress(issueNumber, phase, "activity", { text });
|
|
67
|
+
onProgress?.(issueNumber, phase, "activity", { text });
|
|
47
68
|
}
|
|
48
69
|
catch {
|
|
49
70
|
// Activity events must never disrupt the run.
|
|
50
71
|
}
|
|
51
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
|
+
},
|
|
52
115
|
};
|
|
53
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;
|
|
54
124
|
/**
|
|
55
125
|
* Build enriched prompt context for the /loop phase from a failed phase result (#488).
|
|
56
126
|
* Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
|
|
@@ -94,6 +164,17 @@ export function emitProgressLine(issue, phase, event = "start", extra) {
|
|
|
94
164
|
if (extra?.iteration !== undefined) {
|
|
95
165
|
payload.iteration = extra.iteration;
|
|
96
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
|
+
}
|
|
97
178
|
const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
|
|
98
179
|
process.stderr.write(line);
|
|
99
180
|
}
|
|
@@ -254,8 +335,53 @@ export function getEnvConfig() {
|
|
|
254
335
|
if (process.env.SEQUANT_SECURITY_REVIEW === "true") {
|
|
255
336
|
config.securityReview = true;
|
|
256
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
|
+
}
|
|
257
349
|
return config;
|
|
258
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
|
+
*/
|
|
259
385
|
export async function executeBatch(issueNumbers, batchCtx) {
|
|
260
386
|
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = batchCtx;
|
|
261
387
|
const results = [];
|
|
@@ -291,13 +417,10 @@ export async function executeBatch(issueNumbers, batchCtx) {
|
|
|
291
417
|
};
|
|
292
418
|
const result = await runIssueWithLogging(ctx);
|
|
293
419
|
results.push(result);
|
|
294
|
-
// Record PR info
|
|
295
|
-
|
|
296
|
-
logWriter.setPRInfo(result.prNumber, result.prUrl);
|
|
297
|
-
}
|
|
298
|
-
// 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.
|
|
299
422
|
if (logWriter) {
|
|
300
|
-
logWriter
|
|
423
|
+
recordIssueCompletion(logWriter, result, issueNumber);
|
|
301
424
|
}
|
|
302
425
|
}
|
|
303
426
|
return results;
|
|
@@ -323,6 +446,192 @@ export function deriveFailureCategory(phaseResults) {
|
|
|
323
446
|
classifyError(failedPhase.stderrTail ?? [], failedPhase.exitCode);
|
|
324
447
|
return errorTypeToCategory(typedError);
|
|
325
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
|
+
}
|
|
326
635
|
export async function runIssueWithLogging(ctx) {
|
|
327
636
|
// Destructure context for use throughout the function
|
|
328
637
|
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
|
|
@@ -335,6 +644,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
335
644
|
let loopTriggered = false;
|
|
336
645
|
// Cross-phase resume token, driver-tagged and cwd-bound (#674).
|
|
337
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);
|
|
338
651
|
// In parallel mode, suppress per-issue terminal output to prevent interleaving.
|
|
339
652
|
// The caller (run.ts) handles progress display via updateProgress().
|
|
340
653
|
const log = config.parallel ? () => { } : console.log.bind(console);
|
|
@@ -368,6 +681,19 @@ export async function runIssueWithLogging(ctx) {
|
|
|
368
681
|
}
|
|
369
682
|
}
|
|
370
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;
|
|
371
697
|
// Activate relay (#383) if enabled. Tolerates errors — relay must never
|
|
372
698
|
// block the underlying run.
|
|
373
699
|
let relayActivation = null;
|
|
@@ -420,8 +746,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
420
746
|
}
|
|
421
747
|
const specStartTime = new Date();
|
|
422
748
|
// Note: spec runs in main repo (not worktree) for planning
|
|
423
|
-
const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), resumeHandle, worktreePath, // Will be ignored for spec (non-isolated phase)
|
|
424
|
-
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);
|
|
425
753
|
const specEndTime = new Date();
|
|
426
754
|
if (specResult.resumeHandle) {
|
|
427
755
|
resumeHandle = specResult.resumeHandle;
|
|
@@ -450,16 +778,19 @@ export async function runIssueWithLogging(ctx) {
|
|
|
450
778
|
}
|
|
451
779
|
}
|
|
452
780
|
else {
|
|
453
|
-
// Mirror the main phase loop (#739): a turn-capped spec phase surfaces
|
|
454
|
-
// distinct "partial output preserved" signal
|
|
455
|
-
//
|
|
456
|
-
// failure handling, separate from the
|
|
457
|
-
//
|
|
458
|
-
//
|
|
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.
|
|
459
788
|
const extra = {
|
|
460
789
|
error: specResult.capped
|
|
461
790
|
? "turn cap reached — partial output preserved (resume to continue)"
|
|
462
|
-
: (specResult
|
|
791
|
+
: isBillingOrWindowHalt(specResult)
|
|
792
|
+
? billingHaltReason(specResult)
|
|
793
|
+
: (specResult.error ?? "unknown"),
|
|
463
794
|
};
|
|
464
795
|
emitProgressLine(issueNumber, "spec", "failed", extra);
|
|
465
796
|
try {
|
|
@@ -517,6 +848,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
517
848
|
// State tracking errors shouldn't stop execution
|
|
518
849
|
}
|
|
519
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);
|
|
520
854
|
if (!specResult.success) {
|
|
521
855
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
522
856
|
// Archive relay state on early exit (spec failure).
|
|
@@ -640,6 +974,11 @@ export async function runIssueWithLogging(ctx) {
|
|
|
640
974
|
// retry too, not just the inner /loop spawn — re-running a capped phase
|
|
641
975
|
// would only cap again, and "surface + halt" means the user resumes.
|
|
642
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;
|
|
643
982
|
while (iteration < maxIterations) {
|
|
644
983
|
iteration++;
|
|
645
984
|
if (useQualityLoop && iteration > 1) {
|
|
@@ -672,7 +1011,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
672
1011
|
}
|
|
673
1012
|
}
|
|
674
1013
|
const phaseStartTime = new Date();
|
|
675
|
-
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);
|
|
676
1017
|
const phaseEndTime = new Date();
|
|
677
1018
|
// Capture resume handle for subsequent phases (#674).
|
|
678
1019
|
if (result.resumeHandle) {
|
|
@@ -709,7 +1050,12 @@ export async function runIssueWithLogging(ctx) {
|
|
|
709
1050
|
const extra = {
|
|
710
1051
|
error: result.capped
|
|
711
1052
|
? "turn cap reached — partial output preserved (resume to continue)"
|
|
712
|
-
: (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"),
|
|
713
1059
|
iteration,
|
|
714
1060
|
};
|
|
715
1061
|
emitProgressLine(issueNumber, phase, "failed", extra);
|
|
@@ -722,13 +1068,21 @@ export async function runIssueWithLogging(ctx) {
|
|
|
722
1068
|
}
|
|
723
1069
|
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
724
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;
|
|
725
1077
|
// Capture git diff stats for worktree phases (AC-1, AC-3)
|
|
726
|
-
const diffStats = worktreePath
|
|
727
|
-
? getGitDiffStats(worktreePath,
|
|
1078
|
+
const diffStats = worktreePath && resolvedDiffBase
|
|
1079
|
+
? getGitDiffStats(worktreePath, resolvedDiffBase)
|
|
728
1080
|
: undefined;
|
|
729
|
-
// 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).
|
|
730
1084
|
const commitHash = worktreePath
|
|
731
|
-
? getCommitHash(worktreePath)
|
|
1085
|
+
? getCommitHash(worktreePath, resolvedDiffBase)
|
|
732
1086
|
: undefined;
|
|
733
1087
|
// Read cache metrics for QA phase (AC-7)
|
|
734
1088
|
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
@@ -789,6 +1143,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
789
1143
|
// State tracking errors shouldn't stop execution
|
|
790
1144
|
}
|
|
791
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);
|
|
792
1149
|
if (result.success) {
|
|
793
1150
|
// Phase succeeded — RunRenderer (#618) updates state via onProgress.
|
|
794
1151
|
}
|
|
@@ -797,12 +1154,24 @@ export async function runIssueWithLogging(ctx) {
|
|
|
797
1154
|
if (result.capped) {
|
|
798
1155
|
haltedByCap = true;
|
|
799
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
|
+
}
|
|
800
1164
|
// If quality loop enabled, run loop phase to fix issues.
|
|
801
1165
|
// A turn-capped phase (#739) is incomplete, not a genuine quality
|
|
802
1166
|
// failure: skip the loop and halt cleanly ("surface + halt"). Spawning
|
|
803
1167
|
// /loop on partial output would act on incomplete work — exactly the
|
|
804
1168
|
// risk the capped path is meant to avoid. The user resumes instead.
|
|
805
|
-
|
|
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) {
|
|
806
1175
|
// #624 Item 3 (AC-3.3): the loop phase carries the current outer
|
|
807
1176
|
// iteration so the live-zone status cell can show `loop N/M`.
|
|
808
1177
|
const loopStartExtra = { iteration };
|
|
@@ -823,7 +1192,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
823
1192
|
promptContext: buildLoopContext(result),
|
|
824
1193
|
};
|
|
825
1194
|
const loopStartTime = new Date();
|
|
826
|
-
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);
|
|
827
1198
|
const loopEndTime = new Date();
|
|
828
1199
|
phaseResults.push(loopResult);
|
|
829
1200
|
// #766: record the loop phase in the run log — spec (:655) and the
|
|
@@ -884,7 +1255,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
884
1255
|
}
|
|
885
1256
|
// A turn-capped phase (#739) halts the outer quality-loop retry as well —
|
|
886
1257
|
// re-running would only cap again; the partial work is already preserved.
|
|
887
|
-
|
|
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) {
|
|
888
1261
|
break;
|
|
889
1262
|
}
|
|
890
1263
|
// If we're not in quality loop mode, don't retry
|
|
@@ -896,10 +1269,42 @@ export async function runIssueWithLogging(ctx) {
|
|
|
896
1269
|
// Success is determined by whether all phases completed in any iteration,
|
|
897
1270
|
// not whether all accumulated phase results passed (which would fail after loop recovery)
|
|
898
1271
|
const success = completedSuccessfully;
|
|
899
|
-
//
|
|
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";
|
|
900
1306
|
if (stateManager) {
|
|
901
1307
|
try {
|
|
902
|
-
const finalStatus = success ? "ready_for_merge" : "in_progress";
|
|
903
1308
|
await stateManager.updateIssueStatus(issueNumber, finalStatus);
|
|
904
1309
|
}
|
|
905
1310
|
catch {
|
|
@@ -910,11 +1315,17 @@ export async function runIssueWithLogging(ctx) {
|
|
|
910
1315
|
// #760: chain resume rebases the next link onto this checkpoint, so a failure
|
|
911
1316
|
// here is not silent — warn prominently and record it on the result (AC-4).
|
|
912
1317
|
//
|
|
913
|
-
// Note
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
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.
|
|
918
1329
|
let checkpointFailed = false;
|
|
919
1330
|
if (success && chainMode && worktreePath) {
|
|
920
1331
|
const checkpointOk = createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
|
|
@@ -922,7 +1333,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
922
1333
|
checkpointFailed = true;
|
|
923
1334
|
log(chalk.yellow(` ⚠️ Checkpoint commit for #${issueNumber} could not be created — its uncommitted ` +
|
|
924
1335
|
`changes are NOT on branch ${branch ?? "the feature branch"}. #${issueNumber} stays ` +
|
|
925
|
-
|
|
1336
|
+
`${finalStatus}, so a re-run will skip it and refuse to resume the chain here until the ` +
|
|
926
1337
|
`work is committed in ${worktreePath} (or re-run with --force to redo the whole chain).`));
|
|
927
1338
|
}
|
|
928
1339
|
}
|
|
@@ -941,6 +1352,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
941
1352
|
// Create PR after successful QA + rebase (unless --no-pr)
|
|
942
1353
|
let prNumber;
|
|
943
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;
|
|
944
1359
|
const shouldCreatePR = success && worktreePath && branch && !options.noPr;
|
|
945
1360
|
if (shouldCreatePR) {
|
|
946
1361
|
// #605: under --stacked, target predecessor branch (only for non-first,
|
|
@@ -954,7 +1369,10 @@ export async function runIssueWithLogging(ctx) {
|
|
|
954
1369
|
// #749: surface a non-A+ qa verdict (e.g. AC_MET_BUT_NOT_A_PLUS) in the PR
|
|
955
1370
|
// body so a reviewer sees why the run broke to PR rather than reaching A+.
|
|
956
1371
|
const qaVerdict = phaseResults.find((p) => p.phase === "qa")?.verdict;
|
|
957
|
-
const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions, qaVerdict
|
|
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);
|
|
958
1376
|
if (prResult.success && prResult.prNumber && prResult.prUrl) {
|
|
959
1377
|
prNumber = prResult.prNumber;
|
|
960
1378
|
prUrl = prResult.prUrl;
|
|
@@ -971,6 +1389,11 @@ export async function runIssueWithLogging(ctx) {
|
|
|
971
1389
|
}
|
|
972
1390
|
}
|
|
973
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
|
+
}
|
|
974
1397
|
}
|
|
975
1398
|
// Deactivate relay (#383) — archive inbox/outbox transcripts to
|
|
976
1399
|
// .sequant/logs/relay/ before worktree teardown (AC-D2). Never throws.
|
|
@@ -989,15 +1412,25 @@ export async function runIssueWithLogging(ctx) {
|
|
|
989
1412
|
}
|
|
990
1413
|
}
|
|
991
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;
|
|
992
1418
|
return {
|
|
993
1419
|
issueNumber,
|
|
994
|
-
success,
|
|
1420
|
+
success: overallSuccess,
|
|
995
1421
|
phaseResults,
|
|
996
1422
|
durationSeconds,
|
|
997
1423
|
loopTriggered,
|
|
998
1424
|
prNumber,
|
|
999
1425
|
prUrl,
|
|
1426
|
+
prCreationError,
|
|
1000
1427
|
checkpointFailed,
|
|
1001
|
-
failureCategory:
|
|
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,
|
|
1002
1435
|
};
|
|
1003
1436
|
}
|