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.
Files changed (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +20 -5
  4. package/dist/bin/cli.js +143 -18
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +56 -2
  8. package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
  14. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
  25. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
  26. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
  27. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
  28. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  31. package/dist/src/commands/assess-render.d.ts +23 -0
  32. package/dist/src/commands/assess-render.js +60 -0
  33. package/dist/src/commands/doctor.js +8 -13
  34. package/dist/src/commands/init.js +13 -1
  35. package/dist/src/commands/locks.d.ts +20 -1
  36. package/dist/src/commands/locks.js +208 -4
  37. package/dist/src/commands/logs.js +14 -5
  38. package/dist/src/commands/merge.js +154 -2
  39. package/dist/src/commands/ready-tui-adapter.js +6 -1
  40. package/dist/src/commands/ready.d.ts +31 -3
  41. package/dist/src/commands/ready.js +53 -13
  42. package/dist/src/commands/resume.d.ts +113 -0
  43. package/dist/src/commands/resume.js +193 -0
  44. package/dist/src/commands/run-display.js +38 -4
  45. package/dist/src/commands/run-flags.d.ts +42 -1
  46. package/dist/src/commands/run-flags.js +53 -1
  47. package/dist/src/commands/run-progress.js +26 -1
  48. package/dist/src/commands/run.js +11 -10
  49. package/dist/src/commands/state.js +7 -0
  50. package/dist/src/commands/status.d.ts +9 -0
  51. package/dist/src/commands/status.js +24 -1
  52. package/dist/src/commands/sync.js +90 -19
  53. package/dist/src/commands/update.js +28 -5
  54. package/dist/src/commands/worktree.d.ts +31 -0
  55. package/dist/src/commands/worktree.js +95 -0
  56. package/dist/src/lib/ac-parser.d.ts +14 -0
  57. package/dist/src/lib/ac-parser.js +99 -5
  58. package/dist/src/lib/assess/index.d.ts +10 -0
  59. package/dist/src/lib/assess/index.js +9 -0
  60. package/dist/src/lib/assess/renderer.d.ts +64 -0
  61. package/dist/src/lib/assess/renderer.js +481 -0
  62. package/dist/src/lib/assess/types.d.ts +224 -0
  63. package/dist/src/lib/assess/types.js +241 -0
  64. package/dist/src/lib/ci/outputs.d.ts +15 -2
  65. package/dist/src/lib/ci/outputs.js +17 -5
  66. package/dist/src/lib/cli-flags.d.ts +56 -0
  67. package/dist/src/lib/cli-flags.js +89 -0
  68. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
  69. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  70. package/dist/src/lib/cli-ui/run-renderer.js +69 -1
  71. package/dist/src/lib/errors.d.ts +78 -6
  72. package/dist/src/lib/errors.js +133 -15
  73. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  74. package/dist/src/lib/locks/checkout-lock.js +389 -0
  75. package/dist/src/lib/locks/index.d.ts +7 -4
  76. package/dist/src/lib/locks/index.js +5 -3
  77. package/dist/src/lib/locks/lock-manager.d.ts +101 -3
  78. package/dist/src/lib/locks/lock-manager.js +277 -8
  79. package/dist/src/lib/locks/types.d.ts +106 -3
  80. package/dist/src/lib/locks/types.js +44 -0
  81. package/dist/src/lib/mcp-config.d.ts +45 -0
  82. package/dist/src/lib/mcp-config.js +77 -1
  83. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  84. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  85. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  86. package/dist/src/lib/merge-check/command-result.js +56 -0
  87. package/dist/src/lib/merge-check/index.d.ts +4 -0
  88. package/dist/src/lib/merge-check/index.js +1 -1
  89. package/dist/src/lib/merge-check/report.js +5 -1
  90. package/dist/src/lib/merge-check/types.d.ts +9 -0
  91. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  92. package/dist/src/lib/merge-check/watch.js +215 -0
  93. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  94. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  95. package/dist/src/lib/settings.d.ts +87 -0
  96. package/dist/src/lib/settings.js +106 -8
  97. package/dist/src/lib/shutdown.d.ts +42 -2
  98. package/dist/src/lib/shutdown.js +75 -5
  99. package/dist/src/lib/skills-check.d.ts +26 -0
  100. package/dist/src/lib/skills-check.js +31 -0
  101. package/dist/src/lib/stacks.d.ts +110 -1
  102. package/dist/src/lib/stacks.js +244 -5
  103. package/dist/src/lib/templates.d.ts +74 -0
  104. package/dist/src/lib/templates.js +171 -16
  105. package/dist/src/lib/test-tautology-detector.d.ts +10 -2
  106. package/dist/src/lib/test-tautology-detector.js +213 -12
  107. package/dist/src/lib/version-check.d.ts +31 -0
  108. package/dist/src/lib/version-check.js +45 -2
  109. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  110. package/dist/src/lib/workflow/batch-executor.js +552 -60
  111. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  112. package/dist/src/lib/workflow/chain-resume.js +24 -20
  113. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  114. package/dist/src/lib/workflow/completed-status.js +57 -0
  115. package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
  116. package/dist/src/lib/workflow/config-resolver.js +143 -2
  117. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  118. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  119. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  120. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  121. package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
  122. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  123. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  124. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  125. package/dist/src/lib/workflow/error-classifier.js +4 -0
  126. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  127. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  128. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  129. package/dist/src/lib/workflow/heartbeat.js +90 -0
  130. package/dist/src/lib/workflow/log-writer.d.ts +54 -2
  131. package/dist/src/lib/workflow/log-writer.js +95 -6
  132. package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
  133. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  134. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  135. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  136. package/dist/src/lib/workflow/phase-detection.js +5 -1
  137. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  138. package/dist/src/lib/workflow/phase-executor.js +467 -52
  139. package/dist/src/lib/workflow/phase-registry.js +2 -1
  140. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  141. package/dist/src/lib/workflow/platforms/github.js +152 -0
  142. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  143. package/dist/src/lib/workflow/qa-cache.js +22 -9
  144. package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
  145. package/dist/src/lib/workflow/ready-gate.js +49 -12
  146. package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
  147. package/dist/src/lib/workflow/run-log-schema.js +78 -2
  148. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  149. package/dist/src/lib/workflow/run-orchestrator.js +168 -20
  150. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  151. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  152. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  153. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  154. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  155. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  156. package/dist/src/lib/workflow/state-manager.js +83 -0
  157. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  158. package/dist/src/lib/workflow/state-schema.js +47 -0
  159. package/dist/src/lib/workflow/types.d.ts +190 -4
  160. package/dist/src/lib/workflow/types.js +1 -0
  161. package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
  162. package/dist/src/lib/workflow/worktree-manager.js +111 -24
  163. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  164. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  165. package/dist/src/mcp/tools/run.d.ts +9 -1
  166. package/dist/src/mcp/tools/run.js +20 -1
  167. package/dist/src/mcp/tools/status.js +6 -0
  168. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  169. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  170. package/dist/src/ui/tui/IssueBox.js +10 -2
  171. package/dist/src/ui/tui/row-cap.js +2 -1
  172. package/dist/src/ui/tui/theme.d.ts +7 -0
  173. package/dist/src/ui/tui/theme.js +9 -0
  174. package/package.json +10 -8
  175. package/templates/hooks/parallel-marker.sh +50 -0
  176. package/templates/hooks/post-tool.sh +20 -7
  177. package/templates/hooks/pre-tool.sh +303 -10
  178. package/templates/scripts/cleanup-worktree.sh +217 -33
  179. package/templates/scripts/new-feature.sh +289 -35
  180. package/templates/skills/_shared/references/subagent-types.md +7 -18
  181. package/templates/skills/_shared/references/trust-model.md +18 -0
  182. package/templates/skills/assess/SKILL.md +220 -391
  183. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  184. package/templates/skills/docs/SKILL.md +4 -4
  185. package/templates/skills/exec/SKILL.md +87 -13
  186. package/templates/skills/fullsolve/SKILL.md +130 -22
  187. package/templates/skills/loop/SKILL.md +94 -13
  188. package/templates/skills/merger/SKILL.md +100 -12
  189. package/templates/skills/qa/SKILL.md +396 -63
  190. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  191. package/templates/skills/qa/references/call-site-review.md +2 -2
  192. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  193. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  194. package/templates/skills/qa/references/quality-gates.md +21 -7
  195. package/templates/skills/qa/references/testing-requirements.md +1 -1
  196. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  197. package/templates/skills/release/SKILL.md +80 -1
  198. package/templates/skills/setup/SKILL.md +5 -5
  199. package/templates/skills/spec/SKILL.md +33 -15
  200. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  201. package/templates/skills/test/SKILL.md +1 -1
  202. package/templates/skills/testgen/SKILL.md +23 -6
  203. package/dist/src/lib/phase-spinner.d.ts +0 -146
  204. package/dist/src/lib/phase-spinner.js +0 -255
  205. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  206. package/dist/src/lib/workflow/pr-operations.js +0 -326
  207. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  208. package/dist/src/lib/workflow/run-summary.js +0 -142
  209. 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
- const status = failed > 0
89
- ? chalk.red("FAILED")
90
- : passed === total
91
- ? chalk.green("PASSED")
92
- : chalk.yellow("PARTIAL");
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
- const repoRoot = getRepoRoot();
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
- case "activity": {
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
- * - 2: not ready no implementation (#534) or hard error
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
- * - 2: not ready no implementation (#534) or hard error
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 = typeof options.maxIterations === "number" && options.maxIterations > 0
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 {