sequant 2.9.0 → 2.11.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 +20 -5
- package/dist/bin/cli.js +143 -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 +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.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- 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 +31 -3
- package/dist/src/commands/ready.js +53 -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 +38 -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/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- 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 +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -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 +87 -0
- package/dist/src/lib/settings.js +106 -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 +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -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 +552 -60
- 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 +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -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 +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- 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 +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -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 +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -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/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -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 +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- 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 +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- 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 +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- 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 +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- 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
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `sequant locks` — inspect and clear per-issue concurrency locks (#625)
|
|
2
|
+
* `sequant locks` — inspect and clear per-issue concurrency locks (#625) and
|
|
3
|
+
* the checkout-scoped lock (#901).
|
|
3
4
|
*/
|
|
4
5
|
import chalk from "chalk";
|
|
5
|
-
import { LockManager, formatLockedMessage, } from "../lib/locks/index.js";
|
|
6
|
+
import { CheckoutLock, LockManager, describeCheckoutHolderIssue, formatCheckoutLockedMessage, formatLockedMessage, } from "../lib/locks/index.js";
|
|
6
7
|
/** Human-readable line for the `--signal-other` log output (#637). */
|
|
7
8
|
function formatSignalLine(issue, pid, result) {
|
|
8
9
|
switch (result.reason) {
|
|
@@ -14,6 +15,8 @@ function formatSignalLine(issue, pid, result) {
|
|
|
14
15
|
return `Refused to signal PID ${pid} for #${issue} (matches this process or its parent)`;
|
|
15
16
|
case "pid-dead":
|
|
16
17
|
return `Could not signal PID ${pid} for #${issue} (already exited)`;
|
|
18
|
+
case "stale-pid-untrusted":
|
|
19
|
+
return `Refused to signal PID ${pid} for #${issue} (lock is past the age ceiling; that PID has likely been recycled onto an unrelated process)`;
|
|
17
20
|
case "kill-failed":
|
|
18
21
|
return `Could not signal PID ${pid} for #${issue} (kill syscall failed)`;
|
|
19
22
|
case "orchestrator":
|
|
@@ -42,14 +45,35 @@ export async function locksListCommand(options = {}) {
|
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
44
47
|
const listings = manager.list();
|
|
48
|
+
// The checkout lock lives in the same directory but is deliberately not a
|
|
49
|
+
// numeric filename, so `manager.list()` skips it (#901). Query it separately
|
|
50
|
+
// rather than widening the numeric key everywhere.
|
|
51
|
+
const checkout = new CheckoutLock().listing();
|
|
45
52
|
if (options.json) {
|
|
46
|
-
console.log(JSON.stringify({ locks: listings }, null, 2));
|
|
53
|
+
console.log(JSON.stringify({ locks: listings, checkout }, null, 2));
|
|
47
54
|
return;
|
|
48
55
|
}
|
|
49
|
-
if (listings.length === 0) {
|
|
56
|
+
if (listings.length === 0 && !checkout) {
|
|
50
57
|
console.log(chalk.gray("No active locks."));
|
|
51
58
|
return;
|
|
52
59
|
}
|
|
60
|
+
if (checkout) {
|
|
61
|
+
const ageMinutes = Math.floor(checkout.ageMs / 60_000);
|
|
62
|
+
const staleTag = checkout.stale
|
|
63
|
+
? chalk.yellow(` (stale: ${checkout.staleReason})`)
|
|
64
|
+
: "";
|
|
65
|
+
console.log(chalk.bold("Checkout lock (whole working tree):"));
|
|
66
|
+
console.log(` issue=${describeCheckoutHolderIssue(checkout.holder.issue)} ` +
|
|
67
|
+
`pid=${checkout.holder.pid} ` +
|
|
68
|
+
`host=${checkout.holder.hostname} age=${ageMinutes}m ` +
|
|
69
|
+
`started=${checkout.holder.startedAt}${staleTag}`);
|
|
70
|
+
console.log(` command: ${checkout.holder.command}`);
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
if (listings.length === 0) {
|
|
74
|
+
console.log(chalk.gray("No active per-issue locks."));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
53
77
|
console.log(chalk.bold(`Active locks (${listings.length}):`));
|
|
54
78
|
console.log("");
|
|
55
79
|
for (const l of listings) {
|
|
@@ -60,6 +84,186 @@ export async function locksListCommand(options = {}) {
|
|
|
60
84
|
console.log(` command: ${l.holder.command}`);
|
|
61
85
|
}
|
|
62
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate `--issue` for the checkout commands. Shared by `acquire` (where the
|
|
89
|
+
* flag is required) and `release` (where it is optional) so the two cannot
|
|
90
|
+
* drift on what counts as a valid issue.
|
|
91
|
+
*
|
|
92
|
+
* `Number.parseInt` + `Number.isInteger`, never a bare `Number(x)`:
|
|
93
|
+
* `Number(undefined)` is `NaN`, and `NaN !== undefined`, so a `NaN` would sail
|
|
94
|
+
* through the `issue !== undefined` guard in `isCheckoutOwner` and silently
|
|
95
|
+
* refuse every release instead of erroring here (#906).
|
|
96
|
+
*/
|
|
97
|
+
function parseCheckoutIssue(raw) {
|
|
98
|
+
const issue = Number.parseInt(raw, 10);
|
|
99
|
+
if (!Number.isInteger(issue) || issue <= 0) {
|
|
100
|
+
console.error(chalk.red(`Invalid issue number: ${raw}`));
|
|
101
|
+
process.exitCode = 2;
|
|
102
|
+
return { ok: false };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, issue };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `sequant locks checkout <acquire|release|check|clear>` — the working-tree
|
|
108
|
+
* lock (#901).
|
|
109
|
+
*
|
|
110
|
+
* Exit codes mirror the per-issue commands:
|
|
111
|
+
* 0 — success (acquired / released / free / cleared / nothing to release)
|
|
112
|
+
* 1 — held by another session, or refused
|
|
113
|
+
* 2 — invalid arguments
|
|
114
|
+
*/
|
|
115
|
+
export async function locksCheckoutCommand(action, options = {}) {
|
|
116
|
+
const lock = new CheckoutLock();
|
|
117
|
+
if (lock.isNoop) {
|
|
118
|
+
// AC-5: orchestrator/MCP mode is a no-op across the whole surface.
|
|
119
|
+
if (options.json) {
|
|
120
|
+
console.log(JSON.stringify({ action, orchestratorMode: true, ok: true }));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(chalk.gray("Lock operations are disabled (SEQUANT_ORCHESTRATOR set)."));
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
switch (action) {
|
|
128
|
+
case "acquire": {
|
|
129
|
+
if (options.issue === undefined) {
|
|
130
|
+
console.error(chalk.red("`locks checkout acquire` requires --issue"));
|
|
131
|
+
process.exitCode = 2;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const parsed = parseCheckoutIssue(options.issue);
|
|
135
|
+
if (!parsed.ok)
|
|
136
|
+
return;
|
|
137
|
+
const issue = parsed.issue;
|
|
138
|
+
const result = lock.acquire(issue, options.command ?? "unknown", {
|
|
139
|
+
sessionId: options.sessionId,
|
|
140
|
+
skipPidCheck: options.skipPidCheck,
|
|
141
|
+
});
|
|
142
|
+
if (result.acquired) {
|
|
143
|
+
if (options.json) {
|
|
144
|
+
console.log(JSON.stringify({
|
|
145
|
+
acquired: true,
|
|
146
|
+
reentrant: result.reentrant,
|
|
147
|
+
lockPath: result.lockPath,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(chalk.green(result.reentrant
|
|
152
|
+
? `✓ Checkout already held by this session (#${issue})`
|
|
153
|
+
: `✓ Acquired checkout lock for #${issue}`));
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
if (options.json) {
|
|
159
|
+
console.log(JSON.stringify({
|
|
160
|
+
acquired: false,
|
|
161
|
+
holder: result.holder,
|
|
162
|
+
lockPath: result.lockPath,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.error(chalk.yellow(formatCheckoutLockedMessage(result.holder, { issue })));
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
case "release": {
|
|
171
|
+
// `--issue` is optional here, unlike `acquire`: a live process releasing
|
|
172
|
+
// its own lock is identified by PID. It is required in practice for
|
|
173
|
+
// skill shells, whose PID is already gone — see `isCheckoutOwner`.
|
|
174
|
+
let issue;
|
|
175
|
+
if (options.issue !== undefined) {
|
|
176
|
+
const parsed = parseCheckoutIssue(options.issue);
|
|
177
|
+
if (!parsed.ok)
|
|
178
|
+
return;
|
|
179
|
+
issue = parsed.issue;
|
|
180
|
+
}
|
|
181
|
+
// Read the holder *before* releasing, so a refusal can name it.
|
|
182
|
+
const holder = lock.check();
|
|
183
|
+
const released = lock.release({
|
|
184
|
+
sessionId: options.sessionId,
|
|
185
|
+
...lock.selfIdentity,
|
|
186
|
+
issue,
|
|
187
|
+
});
|
|
188
|
+
// Three outcomes, not two (#906). Before ownership was enforced,
|
|
189
|
+
// "released nothing" could only mean "nothing was held". It now also
|
|
190
|
+
// means "held, but not by you" — a real refusal, which must not print
|
|
191
|
+
// the same gray no-op line or exit 0.
|
|
192
|
+
if (options.json) {
|
|
193
|
+
console.log(JSON.stringify({
|
|
194
|
+
released,
|
|
195
|
+
refused: !released && holder !== null,
|
|
196
|
+
...(holder ? { holder } : {}),
|
|
197
|
+
}));
|
|
198
|
+
if (!released && holder)
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (released) {
|
|
203
|
+
console.log(chalk.green("✓ Released checkout lock"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (holder) {
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
console.error(chalk.yellow(`Refusing to release the checkout lock — it belongs to the session working ${describeCheckoutHolderIssue(holder.issue)} ` +
|
|
209
|
+
`(PID ${holder.pid} on ${holder.hostname}, started ${holder.startedAt}).\n` +
|
|
210
|
+
(issue === undefined
|
|
211
|
+
? `You passed no --issue, so nothing identified you as the holder.\n` +
|
|
212
|
+
` • If you are that session: sequant locks checkout release --issue=${holder.issue}\n`
|
|
213
|
+
: `You passed --issue=${issue}.\n`) +
|
|
214
|
+
` • If that session is gone: sequant locks checkout clear --force`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
console.log(chalk.gray("No releasable checkout lock"));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
case "check": {
|
|
221
|
+
const holder = lock.check();
|
|
222
|
+
if (!holder) {
|
|
223
|
+
if (options.json) {
|
|
224
|
+
console.log(JSON.stringify({ locked: false }));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log(chalk.gray("Checkout is not locked"));
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
if (options.json) {
|
|
233
|
+
console.log(JSON.stringify({ locked: true, holder }));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log(chalk.yellow(formatCheckoutLockedMessage(holder)));
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "clear": {
|
|
241
|
+
const result = lock.clear({ safetyCheck: !options.force });
|
|
242
|
+
if (options.json) {
|
|
243
|
+
console.log(JSON.stringify(result));
|
|
244
|
+
if (!result.cleared)
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (result.cleared) {
|
|
249
|
+
console.log(chalk.green("✓ Cleared checkout lock"));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
if (result.reason === "no-lock") {
|
|
254
|
+
console.log(chalk.gray("No checkout lock to clear"));
|
|
255
|
+
process.exitCode = 0;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.log(chalk.yellow("Refusing to clear a fresh checkout lock. " +
|
|
259
|
+
"Re-run with `sequant locks checkout clear --force` if you are sure the holder is gone."));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
console.error(chalk.red(`Unknown action: ${action}. Expected acquire|release|check|clear.`));
|
|
264
|
+
process.exitCode = 2;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
63
267
|
/**
|
|
64
268
|
* `sequant locks clear <issue>` — remove a lock manually.
|
|
65
269
|
* By default refuses to clear a fresh same-host lock whose PID is alive;
|
|
@@ -85,11 +85,17 @@ function displayLogSummary(log, filename, options) {
|
|
|
85
85
|
// `0 passed, 0 failed` (older logs predate the field → default to 0).
|
|
86
86
|
const partial = log.summary.partial ?? 0;
|
|
87
87
|
const total = log.summary.totalIssues;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
// #856: an aborted run is a failure, but saying only "FAILED" sends the
|
|
89
|
+
// reader hunting for a bug in their own code. The run never got to fail on
|
|
90
|
+
// its own terms — something killed it.
|
|
91
|
+
const aborted = log.summary.aborted ?? 0;
|
|
92
|
+
const status = aborted > 0
|
|
93
|
+
? chalk.red("ABORTED")
|
|
94
|
+
: failed > 0
|
|
95
|
+
? chalk.red("FAILED")
|
|
96
|
+
: passed === total
|
|
97
|
+
? chalk.green("PASSED")
|
|
98
|
+
: chalk.yellow("PARTIAL");
|
|
93
99
|
console.log(chalk.blue(`\n Run: ${log.runId.slice(0, 8)}...`));
|
|
94
100
|
console.log(chalk.gray(` File: ${filename}`));
|
|
95
101
|
console.log(chalk.gray(` Time: ${formatTime(log.startTime)}`));
|
|
@@ -98,6 +104,9 @@ function displayLogSummary(log, filename, options) {
|
|
|
98
104
|
(partial > 0 ? `, ${partial} partial` : "") +
|
|
99
105
|
`)`));
|
|
100
106
|
console.log(chalk.gray(` Phases: ${log.config.phases.join(" → ")}`));
|
|
107
|
+
if (log.abortedBy) {
|
|
108
|
+
console.log(chalk.red(` Aborted: run terminated by ${log.abortedBy} — results below are incomplete`));
|
|
109
|
+
}
|
|
101
110
|
// Show issues
|
|
102
111
|
for (const issue of log.issues) {
|
|
103
112
|
const issueStatus = issue.status === "success"
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { spawnSync } from "child_process";
|
|
15
15
|
import { ui, colors } from "../lib/cli-ui.js";
|
|
16
|
-
import { runMergeChecks, formatReportMarkdown, } from "../lib/merge-check/index.js";
|
|
16
|
+
import { runMergeChecks, formatReportMarkdown, resolveBranches, findMostRecentLog, resolveLogDir, } from "../lib/merge-check/index.js";
|
|
17
17
|
import { LockManager, formatLockedMessage } from "../lib/locks/index.js";
|
|
18
|
+
import { waitForChecks, resolveWatchTiming } from "../lib/merge-check/watch.js";
|
|
19
|
+
import { GitHubProvider } from "../lib/workflow/platforms/github.js";
|
|
20
|
+
/** Exit code for a watch that timed out — distinct from the 0/1/2 verdicts. */
|
|
21
|
+
const EXIT_WATCH_TIMEOUT = 3;
|
|
18
22
|
/**
|
|
19
23
|
* Determine exit code from batch verdict
|
|
20
24
|
*/
|
|
@@ -43,6 +47,126 @@ function getRepoRoot() {
|
|
|
43
47
|
}
|
|
44
48
|
return result.stdout.trim();
|
|
45
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the PR numbers to watch for a set of issues.
|
|
52
|
+
*
|
|
53
|
+
* Reuses `resolveBranches` (git remote + worktree scan) for the issue→branch
|
|
54
|
+
* map, then prefers the run-log `prNumber` and falls back to
|
|
55
|
+
* `viewPRByBranchSync`. Issues with no discoverable PR are returned in
|
|
56
|
+
* `unresolved` so the caller can report them rather than silently skipping.
|
|
57
|
+
*/
|
|
58
|
+
function resolveWatchTargets(issueNumbers, repoRoot, gh) {
|
|
59
|
+
const runLog = findMostRecentLog(resolveLogDir());
|
|
60
|
+
const branches = resolveBranches(issueNumbers, repoRoot, runLog);
|
|
61
|
+
const targets = [];
|
|
62
|
+
const unresolved = [];
|
|
63
|
+
for (const b of branches) {
|
|
64
|
+
const pr = b.prNumber ?? gh.viewPRByBranchSync(b.branch)?.number ?? null;
|
|
65
|
+
if (pr) {
|
|
66
|
+
targets.push({ issue: b.issueNumber, pr });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
unresolved.push(b.issueNumber);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { targets, unresolved };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Watch gate (#818): before the normal merge-check report runs, poll each
|
|
76
|
+
* resolved PR's CI rollup until every check is terminal.
|
|
77
|
+
*
|
|
78
|
+
* Contract: this **never merges**. It only decides *when* the existing report
|
|
79
|
+
* runs. Returns `true` when it has handled the outcome and the caller must stop
|
|
80
|
+
* (a dispatch block → BLOCKED/exit 2, a timeout → distinct message/exit 3, or no
|
|
81
|
+
* PR could be resolved); returns `false` when every PR reached a terminal state
|
|
82
|
+
* and the caller should fall through to `runMergeChecks` unchanged.
|
|
83
|
+
*
|
|
84
|
+
* Multiple PRs share a single wall-clock deadline so `--timeout` bounds the whole
|
|
85
|
+
* gate, not each PR independently.
|
|
86
|
+
*/
|
|
87
|
+
async function runWatchGate(issueNumbers, options, repoRoot) {
|
|
88
|
+
const gh = new GitHubProvider();
|
|
89
|
+
// Auto-detect issues from the most recent run log when none were given,
|
|
90
|
+
// mirroring runMergeChecks so `--watch` composes with auto-detection.
|
|
91
|
+
let issues = issueNumbers;
|
|
92
|
+
if (issues.length === 0) {
|
|
93
|
+
const runLog = findMostRecentLog(resolveLogDir());
|
|
94
|
+
if (runLog) {
|
|
95
|
+
issues = runLog.issues.map((i) => i.issueNumber);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const { targets, unresolved } = resolveWatchTargets(issues, repoRoot, gh);
|
|
99
|
+
if (targets.length === 0) {
|
|
100
|
+
const message = "Watch: no open PR found for " +
|
|
101
|
+
(issues.length > 0
|
|
102
|
+
? `issue(s) ${issues.map((i) => `#${i}`).join(", ")}`
|
|
103
|
+
: "the most recent run") +
|
|
104
|
+
". Push the branch and open a PR before running `merge --watch`.";
|
|
105
|
+
if (options.json) {
|
|
106
|
+
console.log(JSON.stringify({ watch: "blocked", error: message }, null, 2));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.error(ui.errorBox("Watch Blocked", message));
|
|
110
|
+
}
|
|
111
|
+
process.exitCode = 2;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
// Validated, never NaN — the shared deadline below depends on it (a NaN
|
|
115
|
+
// deadline would make every per-PR `remaining` NaN and defeat --timeout).
|
|
116
|
+
const { intervalMs, timeoutMs } = resolveWatchTiming(options);
|
|
117
|
+
const deadline = Date.now() + timeoutMs;
|
|
118
|
+
if (unresolved.length > 0 && !options.json) {
|
|
119
|
+
console.log(colors.muted(` ! No PR resolved for: ${unresolved.map((i) => `#${i}`).join(", ")} — skipping in watch.`));
|
|
120
|
+
}
|
|
121
|
+
for (const { issue, pr } of targets) {
|
|
122
|
+
if (!options.json) {
|
|
123
|
+
console.log(colors.muted(`Watching PR #${pr} (issue #${issue})…`));
|
|
124
|
+
}
|
|
125
|
+
const remaining = deadline - Date.now();
|
|
126
|
+
const result = await waitForChecks(pr, {
|
|
127
|
+
intervalMs,
|
|
128
|
+
// Share the deadline across PRs so the whole gate honors --timeout.
|
|
129
|
+
timeoutMs: Math.max(0, remaining),
|
|
130
|
+
onPoll: options.json
|
|
131
|
+
? undefined
|
|
132
|
+
: (msg) => console.log(colors.muted(` #${pr}: ${msg}`)),
|
|
133
|
+
}, gh);
|
|
134
|
+
if (result.status === "blocked") {
|
|
135
|
+
const reason = result.reason ?? "CI dispatch is blocked.";
|
|
136
|
+
if (options.json) {
|
|
137
|
+
console.log(JSON.stringify({
|
|
138
|
+
watch: "blocked",
|
|
139
|
+
pr,
|
|
140
|
+
issue,
|
|
141
|
+
reason,
|
|
142
|
+
checkName: result.checkName,
|
|
143
|
+
}, null, 2));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(ui.errorBox("BLOCKED", `PR #${pr} (issue #${issue}) — ${reason}` +
|
|
147
|
+
(result.checkName ? `\n\nCheck: ${result.checkName}` : "")));
|
|
148
|
+
}
|
|
149
|
+
process.exitCode = 2;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (result.status === "timeout") {
|
|
153
|
+
const reason = result.reason ?? "Watch timed out.";
|
|
154
|
+
if (options.json) {
|
|
155
|
+
console.log(JSON.stringify({ watch: "timeout", pr, issue, reason }, null, 2));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error(ui.errorBox("Watch Timed Out", reason));
|
|
159
|
+
}
|
|
160
|
+
process.exitCode = EXIT_WATCH_TIMEOUT;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
// terminal → this PR is ready to be checked; continue to the next PR.
|
|
164
|
+
if (!options.json) {
|
|
165
|
+
console.log(colors.muted(` #${pr}: all checks terminal after ${result.polls} poll(s).`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
46
170
|
/**
|
|
47
171
|
* Main merge command handler
|
|
48
172
|
*/
|
|
@@ -51,7 +175,14 @@ export async function mergeCommand(issues, options) {
|
|
|
51
175
|
if (!options.check && !options.scan && !options.review && !options.all) {
|
|
52
176
|
options.check = true;
|
|
53
177
|
}
|
|
54
|
-
|
|
178
|
+
let repoRoot;
|
|
179
|
+
try {
|
|
180
|
+
repoRoot = getRepoRoot();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
console.error(colors.error("❌ Not in a git repository. Run merge from inside your project repo."));
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
55
186
|
const issueNumbers = issues
|
|
56
187
|
.map((i) => parseInt(i, 10))
|
|
57
188
|
.filter((n) => !isNaN(n));
|
|
@@ -81,6 +212,27 @@ export async function mergeCommand(issues, options) {
|
|
|
81
212
|
}
|
|
82
213
|
console.log("");
|
|
83
214
|
}
|
|
215
|
+
// Watch gate (#818): wait for CI to reach a terminal state before running the
|
|
216
|
+
// report. Returns true when it has already handled the outcome (dispatch
|
|
217
|
+
// block, timeout, or no PR to watch) and set the exit code — stop here.
|
|
218
|
+
if (options.watch) {
|
|
219
|
+
try {
|
|
220
|
+
const handled = await runWatchGate(issueNumbers, options, repoRoot);
|
|
221
|
+
if (handled)
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
226
|
+
if (options.json) {
|
|
227
|
+
console.log(JSON.stringify({ watch: "error", error: message }, null, 2));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.error(ui.errorBox("Watch Failed", message));
|
|
231
|
+
}
|
|
232
|
+
process.exitCode = 2;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
84
236
|
try {
|
|
85
237
|
const report = await runMergeChecks(issueNumbers, options, repoRoot);
|
|
86
238
|
if (options.json) {
|
|
@@ -77,7 +77,12 @@ export class ReadySnapshotAdapter {
|
|
|
77
77
|
this.status = "failed";
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
// #804: an auto-wait surfaces here the same way activity does — as a
|
|
81
|
+
// refreshed nowLine on the still-running phase. Sharing one body is
|
|
82
|
+
// deliberate: a wait must NOT reach the complete/failed case, which
|
|
83
|
+
// would close out a phase that is merely paused.
|
|
84
|
+
case "activity":
|
|
85
|
+
case "waiting": {
|
|
81
86
|
if (this.currentPhase) {
|
|
82
87
|
this.currentPhase = {
|
|
83
88
|
...this.currentPhase,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* human / no implementation), persists that state, and emits a structured gap
|
|
15
15
|
* report. See `src/lib/workflow/ready-gate.ts` for the engine.
|
|
16
16
|
*/
|
|
17
|
-
import { type ReadyPolicy } from "../lib/settings.js";
|
|
17
|
+
import { type ReadyPolicy, type SequantSettings } from "../lib/settings.js";
|
|
18
18
|
import { type ReadyResult } from "../lib/workflow/ready-gate.js";
|
|
19
19
|
export interface ReadyCommandOptions {
|
|
20
20
|
policy?: string;
|
|
@@ -25,12 +25,21 @@ export interface ReadyCommandOptions {
|
|
|
25
25
|
mcp?: boolean;
|
|
26
26
|
json?: boolean;
|
|
27
27
|
verbose?: boolean;
|
|
28
|
+
/** Per-phase model override (#914). See `RunOptions.models`. */
|
|
29
|
+
models?: string;
|
|
30
|
+
/** Per-phase effort override (#914). See `RunOptions.efforts`. */
|
|
31
|
+
efforts?: string;
|
|
32
|
+
/** Evidence-based effort escalation on QA-pass retries (#915). See `RunOptions.escalateEffort`. */
|
|
33
|
+
escalateEffort?: boolean;
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
30
36
|
* Exit code from a ready result.
|
|
31
37
|
* - 0: ready (awaiting human merge)
|
|
32
|
-
* - 1: not ready — needs human intervention (budget/iterations/stagnation
|
|
33
|
-
*
|
|
38
|
+
* - 1: not ready — needs human intervention (budget/iterations/stagnation,
|
|
39
|
+
* or a NO_VERDICT QA turn (#853): the implementation may exist, so this
|
|
40
|
+
* is a re-run case, not the "nothing there" exit)
|
|
41
|
+
* - 2: not ready — no implementation (#534), uncommitted-only work (#879), or
|
|
42
|
+
* hard error. Same class: nothing certifiable exists on the branch.
|
|
34
43
|
*/
|
|
35
44
|
export declare function getReadyExitCode(result: ReadyResult): number;
|
|
36
45
|
/**
|
|
@@ -40,6 +49,25 @@ export declare function getReadyExitCode(result: ReadyResult): number;
|
|
|
40
49
|
* @internal Exported for testing only.
|
|
41
50
|
*/
|
|
42
51
|
export declare function resolvePolicy(flag: string | undefined, settingsPolicy: ReadyPolicy): ReadyPolicy;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the numeric limits for a ready-gate run: CLI → settings → default.
|
|
54
|
+
*
|
|
55
|
+
* #833: these previously guarded only the CLI value (`typeof x === "number" &&
|
|
56
|
+
* x > 0`) and fell straight through to `settings.run.*` unchecked. But
|
|
57
|
+
* `settings.run.timeout` is user-authored JSON, and `ready`'s value does NOT
|
|
58
|
+
* pass through `buildExecutionConfig` — `ready-gate.ts`'s `buildPhaseConfig`
|
|
59
|
+
* assembles its own `ExecutionConfig`. So a `"timeout": 0` in settings.json
|
|
60
|
+
* reached `setTimeout` as a 0 ms delay and aborted every ready-gate phase on
|
|
61
|
+
* its first tick, with no warning: the #833 defect, on the path the original
|
|
62
|
+
* fix did not cover. Chaining `positiveOr` leaves no layer unchecked.
|
|
63
|
+
*
|
|
64
|
+
* @internal Exported for testing only.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveReadyLimits(options: Pick<ReadyCommandOptions, "maxIterations" | "budget" | "timeout">, settings: Pick<SequantSettings, "run">): {
|
|
67
|
+
maxIterations: number;
|
|
68
|
+
tokenBudget: number | undefined;
|
|
69
|
+
phaseTimeout: number;
|
|
70
|
+
};
|
|
43
71
|
/**
|
|
44
72
|
* Locate the worktree path for an issue from `git worktree list`.
|
|
45
73
|
*
|
|
@@ -15,25 +15,33 @@
|
|
|
15
15
|
* report. See `src/lib/workflow/ready-gate.ts` for the engine.
|
|
16
16
|
*/
|
|
17
17
|
import { ui, colors } from "../lib/cli-ui.js";
|
|
18
|
-
import { getSettings } from "../lib/settings.js";
|
|
18
|
+
import { getSettings, } from "../lib/settings.js";
|
|
19
19
|
import { listWorktrees } from "../lib/workflow/worktree-manager.js";
|
|
20
20
|
import { GitHubProvider } from "../lib/workflow/platforms/github.js";
|
|
21
21
|
import { getStateManager } from "../lib/workflow/state-manager.js";
|
|
22
22
|
import { executePhaseWithRetry } from "../lib/workflow/phase-executor.js";
|
|
23
23
|
import { buildProgressWiring } from "./run-progress.js";
|
|
24
24
|
import { ReadySnapshotAdapter } from "./ready-tui-adapter.js";
|
|
25
|
+
import { DEFAULT_CONFIG } from "../lib/workflow/types.js";
|
|
26
|
+
import { positiveOr, resolvePhasePolicies, } from "../lib/workflow/config-resolver.js";
|
|
27
|
+
import { getPhaseNames } from "../lib/workflow/phase-registry.js";
|
|
25
28
|
import { runReadyGate, parseNonGoals, } from "../lib/workflow/ready-gate.js";
|
|
26
29
|
/**
|
|
27
30
|
* Exit code from a ready result.
|
|
28
31
|
* - 0: ready (awaiting human merge)
|
|
29
|
-
* - 1: not ready — needs human intervention (budget/iterations/stagnation
|
|
30
|
-
*
|
|
32
|
+
* - 1: not ready — needs human intervention (budget/iterations/stagnation,
|
|
33
|
+
* or a NO_VERDICT QA turn (#853): the implementation may exist, so this
|
|
34
|
+
* is a re-run case, not the "nothing there" exit)
|
|
35
|
+
* - 2: not ready — no implementation (#534), uncommitted-only work (#879), or
|
|
36
|
+
* hard error. Same class: nothing certifiable exists on the branch.
|
|
31
37
|
*/
|
|
32
38
|
export function getReadyExitCode(result) {
|
|
33
39
|
if (result.ready)
|
|
34
40
|
return 0;
|
|
35
|
-
if (result.reason === "NO_IMPLEMENTATION"
|
|
41
|
+
if (result.reason === "NO_IMPLEMENTATION" ||
|
|
42
|
+
result.reason === "UNCOMMITTED_ONLY") {
|
|
36
43
|
return 2;
|
|
44
|
+
}
|
|
37
45
|
return 1;
|
|
38
46
|
}
|
|
39
47
|
/**
|
|
@@ -47,6 +55,33 @@ export function resolvePolicy(flag, settingsPolicy) {
|
|
|
47
55
|
return flag;
|
|
48
56
|
return settingsPolicy;
|
|
49
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the numeric limits for a ready-gate run: CLI → settings → default.
|
|
60
|
+
*
|
|
61
|
+
* #833: these previously guarded only the CLI value (`typeof x === "number" &&
|
|
62
|
+
* x > 0`) and fell straight through to `settings.run.*` unchecked. But
|
|
63
|
+
* `settings.run.timeout` is user-authored JSON, and `ready`'s value does NOT
|
|
64
|
+
* pass through `buildExecutionConfig` — `ready-gate.ts`'s `buildPhaseConfig`
|
|
65
|
+
* assembles its own `ExecutionConfig`. So a `"timeout": 0` in settings.json
|
|
66
|
+
* reached `setTimeout` as a 0 ms delay and aborted every ready-gate phase on
|
|
67
|
+
* its first tick, with no warning: the #833 defect, on the path the original
|
|
68
|
+
* fix did not cover. Chaining `positiveOr` leaves no layer unchecked.
|
|
69
|
+
*
|
|
70
|
+
* @internal Exported for testing only.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveReadyLimits(options, settings) {
|
|
73
|
+
return {
|
|
74
|
+
maxIterations: positiveOr(options.maxIterations, positiveOr(settings.run.maxIterations, DEFAULT_CONFIG.maxIterations)),
|
|
75
|
+
// Budget is genuinely optional — `undefined` means "no budget", not "use a
|
|
76
|
+
// default" — so it keeps the two-state form rather than chaining.
|
|
77
|
+
tokenBudget: typeof options.budget === "number" &&
|
|
78
|
+
Number.isFinite(options.budget) &&
|
|
79
|
+
options.budget > 0
|
|
80
|
+
? options.budget
|
|
81
|
+
: undefined,
|
|
82
|
+
phaseTimeout: positiveOr(options.timeout, positiveOr(settings.run.timeout, DEFAULT_CONFIG.phaseTimeout)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
50
85
|
/**
|
|
51
86
|
* Locate the worktree path for an issue from `git worktree list`.
|
|
52
87
|
*
|
|
@@ -70,16 +105,12 @@ export async function readyCommand(issueArg, options) {
|
|
|
70
105
|
}
|
|
71
106
|
const settings = await getSettings();
|
|
72
107
|
const policy = resolvePolicy(options.policy, settings.ready.policy);
|
|
73
|
-
const maxIterations
|
|
74
|
-
? options.maxIterations
|
|
75
|
-
: settings.run.maxIterations;
|
|
76
|
-
const tokenBudget = typeof options.budget === "number" && options.budget > 0
|
|
77
|
-
? options.budget
|
|
78
|
-
: undefined;
|
|
79
|
-
const phaseTimeout = typeof options.timeout === "number" && options.timeout > 0
|
|
80
|
-
? options.timeout
|
|
81
|
-
: settings.run.timeout;
|
|
108
|
+
const { maxIterations, tokenBudget, phaseTimeout } = resolveReadyLimits(options, settings);
|
|
82
109
|
const mcp = options.mcp !== false;
|
|
110
|
+
const phasePolicies = resolvePhasePolicies(options.models, options.efforts, settings.run.phases, getPhaseNames());
|
|
111
|
+
// #915: CLI > settings > default `false`, same precedence as the `run`
|
|
112
|
+
// path's `buildExecutionConfig` (config-resolver.ts).
|
|
113
|
+
const effortEscalation = options.escalateEffort ?? settings.run.effortEscalation ?? false;
|
|
83
114
|
// Resolve the issue's existing worktree (reuses run/state worktree infra).
|
|
84
115
|
const worktreePath = resolveWorktreePath(issueNumber);
|
|
85
116
|
if (!worktreePath) {
|
|
@@ -172,6 +203,8 @@ export async function readyCommand(issueArg, options) {
|
|
|
172
203
|
verbose: options.verbose,
|
|
173
204
|
runPhase,
|
|
174
205
|
onProgress,
|
|
206
|
+
phasePolicies,
|
|
207
|
+
effortEscalation,
|
|
175
208
|
});
|
|
176
209
|
}
|
|
177
210
|
catch (error) {
|
|
@@ -194,6 +227,13 @@ export async function readyCommand(issueArg, options) {
|
|
|
194
227
|
process.exitCode = 2;
|
|
195
228
|
return;
|
|
196
229
|
}
|
|
230
|
+
// #915: surface any escalated qa/loop dispatches — the gate has no live
|
|
231
|
+
// print of its own (see `withEscalatedEffort` in `ready-gate.ts`).
|
|
232
|
+
if (options.verbose) {
|
|
233
|
+
for (const e of result.effortEscalations) {
|
|
234
|
+
console.log(colors.muted(` effort: ${e.base} → ${e.escalated} (${e.phase} retry)`));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
197
237
|
// Persist the terminal state so `sequant status` reflects it (Derived AC).
|
|
198
238
|
// Best-effort: initialize the issue in state if a prior run didn't track it.
|
|
199
239
|
try {
|