sequant 2.10.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 (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +6 -2
  3. package/dist/bin/cli.js +47 -2
  4. package/dist/src/commands/locks.d.ts +20 -1
  5. package/dist/src/commands/locks.js +206 -4
  6. package/dist/src/commands/ready.d.ts +6 -0
  7. package/dist/src/commands/ready.js +15 -1
  8. package/dist/src/commands/run-display.js +1 -0
  9. package/dist/src/commands/worktree.d.ts +31 -0
  10. package/dist/src/commands/worktree.js +95 -0
  11. package/dist/src/lib/cli-flags.d.ts +23 -0
  12. package/dist/src/lib/cli-flags.js +43 -0
  13. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
  14. package/dist/src/lib/cli-ui/run-renderer.js +7 -1
  15. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  16. package/dist/src/lib/locks/checkout-lock.js +389 -0
  17. package/dist/src/lib/locks/index.d.ts +6 -3
  18. package/dist/src/lib/locks/index.js +4 -2
  19. package/dist/src/lib/locks/lock-manager.d.ts +81 -1
  20. package/dist/src/lib/locks/lock-manager.js +230 -5
  21. package/dist/src/lib/locks/types.d.ts +72 -0
  22. package/dist/src/lib/locks/types.js +28 -0
  23. package/dist/src/lib/settings.d.ts +73 -0
  24. package/dist/src/lib/settings.js +45 -0
  25. package/dist/src/lib/test-tautology-detector.d.ts +4 -3
  26. package/dist/src/lib/test-tautology-detector.js +101 -41
  27. package/dist/src/lib/workflow/batch-executor.js +78 -19
  28. package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
  29. package/dist/src/lib/workflow/config-resolver.js +89 -0
  30. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
  31. package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
  32. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  33. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  34. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  35. package/dist/src/lib/workflow/error-classifier.js +4 -0
  36. package/dist/src/lib/workflow/log-writer.d.ts +10 -1
  37. package/dist/src/lib/workflow/log-writer.js +20 -0
  38. package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
  39. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  40. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  41. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  42. package/dist/src/lib/workflow/phase-detection.js +5 -1
  43. package/dist/src/lib/workflow/phase-executor.js +10 -0
  44. package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
  45. package/dist/src/lib/workflow/ready-gate.js +24 -3
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
  47. package/dist/src/lib/workflow/run-log-schema.js +31 -1
  48. package/dist/src/lib/workflow/run-orchestrator.js +27 -0
  49. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  50. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  51. package/dist/src/lib/workflow/types.d.ts +64 -0
  52. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  53. package/dist/src/lib/workflow/worktree-manager.js +9 -1
  54. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  55. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  56. package/package.json +3 -2
  57. package/templates/hooks/pre-tool.sh +228 -0
  58. package/templates/scripts/cleanup-worktree.sh +36 -15
  59. package/templates/scripts/new-feature.sh +25 -19
  60. package/templates/skills/_shared/references/subagent-types.md +7 -18
  61. package/templates/skills/assess/SKILL.md +5 -1
  62. package/templates/skills/exec/SKILL.md +61 -7
  63. package/templates/skills/fullsolve/SKILL.md +127 -21
  64. package/templates/skills/loop/SKILL.md +56 -11
  65. package/templates/skills/merger/SKILL.md +98 -10
  66. package/templates/skills/qa/SKILL.md +59 -6
  67. package/templates/skills/release/SKILL.md +79 -0
  68. package/templates/skills/spec/SKILL.md +31 -15
  69. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  70. package/templates/skills/testgen/SKILL.md +23 -6
  71. package/templates/agents/sequant-explorer.md +0 -24
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sequant",
3
3
  "description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees and quality gates, through spec → exec → qa phases.",
4
- "version": "2.10.0",
4
+ "version": "2.11.0",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
package/README.md CHANGED
@@ -296,12 +296,16 @@ Multi-issue runs are parallel by default, and a per-issue lock (`.sequant/locks/
296
296
  "run": {
297
297
  "qualityLoop": false,
298
298
  "maxIterations": 3,
299
- "defaultBase": "feature/dashboard" // Optional: custom default base branch
299
+ "defaultBase": "feature/dashboard", // Optional: custom default base branch
300
+ "phases": {
301
+ "exec": { "model": "sonnet", "effort": "medium" } // Optional: per-phase model/effort override
302
+ },
303
+ "effortEscalation": false // Optional: escalate effort one tier on a quality-loop retry
300
304
  }
301
305
  }
302
306
  ```
303
307
 
304
- See [Customization Guide](docs/guides/customization.md) for all options.
308
+ See [Customization Guide](docs/guides/customization.md) for all options, [Per-Phase Model & Effort](docs/reference/run-command.md#per-phase-model--effort) for the `run.phases` shape, the `--models`/`--efforts` flags, and precedence, and [Effort Escalation on Retries](docs/reference/run-command.md#effort-escalation-on-retries) for `run.effortEscalation`/`--escalate-effort`.
305
309
 
306
310
  ---
307
311
 
package/dist/bin/cli.js CHANGED
@@ -18,7 +18,9 @@ import { readFileSync } from "fs";
18
18
  import { initCommand } from "../src/commands/init.js";
19
19
  import { buildHomeStrayWarning, getInstallRoot, isHomeStrayInstall, isLocalNodeModulesInstall, } from "../src/lib/version-check.js";
20
20
  import { configureUI, banner } from "../src/lib/cli-ui.js";
21
- import { parseWholeNumber, parsePositiveSeconds, } from "../src/lib/cli-flags.js";
21
+ import { parseWholeNumber, parsePositiveSeconds, parsePhaseSpecFlag, } from "../src/lib/cli-flags.js";
22
+ import { getPhaseNames } from "../src/lib/workflow/phase-registry.js";
23
+ import { EFFORT_LEVELS } from "../src/lib/settings.js";
22
24
  import { isCI, isStdoutTTY } from "../src/lib/tty.js";
23
25
  import { detectPackageManagerSync, getPackageManagerCommands, } from "../src/lib/stacks.js";
24
26
  // Read version from package.json dynamically
@@ -58,7 +60,8 @@ import { syncCommand, areSkillsOutdated, checkAndWarnSkillsOutdated, } from "../
58
60
  import { mergeCommand } from "../src/commands/merge.js";
59
61
  import { readyCommand, } from "../src/commands/ready.js";
60
62
  import { conventionsCommand } from "../src/commands/conventions.js";
61
- import { locksListCommand, locksClearCommand, locksAcquireCommand, locksReleaseCommand, locksCheckCommand, locksCheckBatchCommand, } from "../src/commands/locks.js";
63
+ import { locksListCommand, locksClearCommand, locksAcquireCommand, locksReleaseCommand, locksCheckCommand, locksCheckBatchCommand, locksCheckoutCommand, } from "../src/commands/locks.js";
64
+ import { worktreeResolveCommand, worktreeVerifyCommand, } from "../src/commands/worktree.js";
62
65
  import { promptCommand } from "../src/commands/prompt.js";
63
66
  import { watchCommand } from "../src/commands/watch.js";
64
67
  import { abortCommand } from "../src/commands/abort.js";
@@ -236,6 +239,14 @@ parseWholeNumber("--auto-wait", {
236
239
  // bounds (policy from settings.ready.policy, iteration cap, stagnation guard,
237
240
  // Non-Goals) — no new policy surface. Never merges; stops at the human gate.
238
241
  .option("--ready-gate", "After phases succeed, run the post-QA ready gate (qa→loop→qa to the configured policy) — never merges, stops at the human merge gate")
242
+ // #914: per-phase model/effort override for the claude-code driver. Bare
243
+ // value applies to every phase; comma list of phase=value pairs applies
244
+ // per phase. CLI > settings.run.phases > absent (resolvePhasePolicies).
245
+ .option("--models <spec>", "Per-phase model override, e.g. 'sonnet' or 'spec=fable,exec=sonnet' (default: none — inherits the CLI default model)", parsePhaseSpecFlag("--models", getPhaseNames()))
246
+ .option("--efforts <spec>", "Per-phase reasoning-effort override (low|medium|high|xhigh|max), same grammar as --models (default: none — inherits the SDK default)", parsePhaseSpecFlag("--efforts", getPhaseNames(), EFFORT_LEVELS))
247
+ // #915: opt-in escalation on quality-loop retries. Raises token cost, so
248
+ // off by default — matches --ready-gate's opt-in shape above.
249
+ .option("--escalate-effort", "On a quality-loop retry (loop iteration ≥ 2), run that retried phase one reasoning-effort tier above its resolved base (default: off — see --efforts)")
239
250
  .option("-f, --force", "Force re-execution of completed issues (bypass pre-flight state guard) and take over per-issue locks")
240
251
  .option("--signal-other", "With --force, SIGTERM the prior PID holding the lock (same-host alive only)")
241
252
  .option("--concurrency <n>", "Max concurrent issues in parallel mode (default: 3)",
@@ -359,6 +370,11 @@ parseWholeNumber("--max-iterations", {
359
370
  }))
360
371
  .option("--timeout <seconds>", "Timeout per phase in seconds", parsePositiveSeconds("--timeout"))
361
372
  .option("--no-mcp", "Disable MCP server injection in headless mode")
373
+ .option("--models <spec>", "Per-phase model override for the qa/loop phases this gate runs, e.g. 'sonnet' or 'qa=sonnet' (default: none)", parsePhaseSpecFlag("--models", getPhaseNames()))
374
+ .option("--efforts <spec>", "Per-phase reasoning-effort override (low|medium|high|xhigh|max), same grammar as --models (default: none)", parsePhaseSpecFlag("--efforts", getPhaseNames(), EFFORT_LEVELS))
375
+ // #915: same opt-in escalation as `sequant run --escalate-effort`, applied
376
+ // to this gate's qa/loop retry passes.
377
+ .option("--escalate-effort", "On a QA-pass retry (pass ≥ 2), run that retried phase one reasoning-effort tier above its resolved base (default: off)")
362
378
  .option("--json", "Output as JSON")
363
379
  .option("-v, --verbose", "Enable verbose output")
364
380
  .action((issue, options) => readyCommand(issue, options));
@@ -486,6 +502,35 @@ locksCmd
486
502
  .description("Batch read-only probe: emit canonical ⚠ warning lines for held issues (for /assess dashboard)")
487
503
  .option("--json", "Output as JSON instead of canonical text lines")
488
504
  .action(locksCheckBatchCommand);
505
+ // Checkout-scoped lock (#901). The per-issue locks above give no mutual
506
+ // exclusion on the shared working tree — two sessions on different issues take
507
+ // different lock files, yet `git checkout`/`reset`/`rebase`/`merge` are global
508
+ // to the tree. This lock represents the tree itself.
509
+ locksCmd
510
+ .command("checkout <action>")
511
+ .description("Working-tree lock: acquire|release|check|clear (guards branch-mutating git in the main checkout)")
512
+ .option("--issue <issue>", "Issue this session is working on (acquire, release; proves ownership on release)")
513
+ .option("--command <command>", "Human-readable command label", "unknown")
514
+ .option("--session-id <id>", "Claude Code session id; preferred holder identity for skill shells")
515
+ .option("--skip-pid-check", "Mark the lock so stale recovery skips same-host PID checks (use from skill shells)")
516
+ .option("-f, --force", "Clear even a fresh holder (clear)")
517
+ .option("--json", "Output as JSON")
518
+ .action(locksCheckoutCommand);
519
+ // Repo-scoped worktree resolution for skill bodies (#899)
520
+ const worktreeCmd = program
521
+ .command("worktree")
522
+ .description("Resolve and verify this repository's issue worktrees");
523
+ worktreeCmd
524
+ .command("resolve <issue>")
525
+ .description("Print the absolute path of this repo's worktree for an issue (exit 1 if none)")
526
+ .option("--json", "Output as JSON")
527
+ .action(worktreeResolveCommand);
528
+ worktreeCmd
529
+ .command("verify <path>")
530
+ .description("Confirm a path is a worktree of this repo, not a foreign or stale one (exit 1 if not)")
531
+ .option("--issue <issue>", "Also require the branch to belong to this issue")
532
+ .option("--json", "Output as JSON")
533
+ .action(worktreeVerifyCommand);
489
534
  // Auto-sync skills after npm upgrade (version mismatch detection)
490
535
  // Only triggers when skills were previously synced (has .sequant-version marker).
491
536
  // Projects that manage skills manually (no marker) are not affected.
@@ -1,5 +1,6 @@
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
  export interface LocksListOptions {
5
6
  json?: boolean;
@@ -26,6 +27,24 @@ export interface LocksCheckBatchOptions {
26
27
  }
27
28
  /** `sequant locks list` — print every active lock with staleness metadata. */
28
29
  export declare function locksListCommand(options?: LocksListOptions): Promise<void>;
30
+ export interface LocksCheckoutOptions {
31
+ issue?: string;
32
+ command?: string;
33
+ sessionId?: string;
34
+ skipPidCheck?: boolean;
35
+ force?: boolean;
36
+ json?: boolean;
37
+ }
38
+ /**
39
+ * `sequant locks checkout <acquire|release|check|clear>` — the working-tree
40
+ * lock (#901).
41
+ *
42
+ * Exit codes mirror the per-issue commands:
43
+ * 0 — success (acquired / released / free / cleared / nothing to release)
44
+ * 1 — held by another session, or refused
45
+ * 2 — invalid arguments
46
+ */
47
+ export declare function locksCheckoutCommand(action: string, options?: LocksCheckoutOptions): Promise<void>;
29
48
  /**
30
49
  * `sequant locks clear <issue>` — remove a lock manually.
31
50
  * By default refuses to clear a fresh same-host lock whose PID is alive;
@@ -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) {
@@ -44,14 +45,35 @@ export async function locksListCommand(options = {}) {
44
45
  return;
45
46
  }
46
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();
47
52
  if (options.json) {
48
- console.log(JSON.stringify({ locks: listings }, null, 2));
53
+ console.log(JSON.stringify({ locks: listings, checkout }, null, 2));
49
54
  return;
50
55
  }
51
- if (listings.length === 0) {
56
+ if (listings.length === 0 && !checkout) {
52
57
  console.log(chalk.gray("No active locks."));
53
58
  return;
54
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
+ }
55
77
  console.log(chalk.bold(`Active locks (${listings.length}):`));
56
78
  console.log("");
57
79
  for (const l of listings) {
@@ -62,6 +84,186 @@ export async function locksListCommand(options = {}) {
62
84
  console.log(` command: ${l.holder.command}`);
63
85
  }
64
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
+ }
65
267
  /**
66
268
  * `sequant locks clear <issue>` — remove a lock manually.
67
269
  * By default refuses to clear a fresh same-host lock whose PID is alive;
@@ -25,6 +25,12 @@ 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.
@@ -23,7 +23,8 @@ 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
25
  import { DEFAULT_CONFIG } from "../lib/workflow/types.js";
26
- import { positiveOr } from "../lib/workflow/config-resolver.js";
26
+ import { positiveOr, resolvePhasePolicies, } from "../lib/workflow/config-resolver.js";
27
+ import { getPhaseNames } from "../lib/workflow/phase-registry.js";
27
28
  import { runReadyGate, parseNonGoals, } from "../lib/workflow/ready-gate.js";
28
29
  /**
29
30
  * Exit code from a ready result.
@@ -106,6 +107,10 @@ export async function readyCommand(issueArg, options) {
106
107
  const policy = resolvePolicy(options.policy, settings.ready.policy);
107
108
  const { maxIterations, tokenBudget, phaseTimeout } = resolveReadyLimits(options, settings);
108
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;
109
114
  // Resolve the issue's existing worktree (reuses run/state worktree infra).
110
115
  const worktreePath = resolveWorktreePath(issueNumber);
111
116
  if (!worktreePath) {
@@ -198,6 +203,8 @@ export async function readyCommand(issueArg, options) {
198
203
  verbose: options.verbose,
199
204
  runPhase,
200
205
  onProgress,
206
+ phasePolicies,
207
+ effortEscalation,
201
208
  });
202
209
  }
203
210
  catch (error) {
@@ -220,6 +227,13 @@ export async function readyCommand(issueArg, options) {
220
227
  process.exitCode = 2;
221
228
  return;
222
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
+ }
223
237
  // Persist the terminal state so `sequant status` reflects it (Derived AC).
224
238
  // Best-effort: initialize the issue in state if a prior run didn't track it.
225
239
  try {
@@ -90,6 +90,7 @@ function toIssueSummary(r) {
90
90
  loopTriggered: r.loopTriggered,
91
91
  prNumber: r.prNumber,
92
92
  prUrl: r.prUrl,
93
+ prSkippedReason: r.prSkippedReason,
93
94
  };
94
95
  if (!r.success) {
95
96
  // #879: a PR-creation failure has no failed phase — fall back to its error
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `sequant worktree` — repo-scoped worktree resolution for skill bodies (#899).
3
+ *
4
+ * `/fullsolve` uses `resolve` to turn an issue number into a real absolute
5
+ * path before exporting `SEQUANT_WORKTREE`; `/exec` uses `verify` to refuse a
6
+ * path that does not exist or belongs to another repository. Both fail closed:
7
+ * a non-zero exit means the caller must halt, not continue where it stands.
8
+ */
9
+ export interface WorktreeResolveOptions {
10
+ json?: boolean;
11
+ }
12
+ export interface WorktreeVerifyOptions {
13
+ issue?: string;
14
+ json?: boolean;
15
+ }
16
+ /**
17
+ * `sequant worktree resolve <issue>` — print the absolute path of this
18
+ * repository's worktree for an issue.
19
+ *
20
+ * On success the path is the *only* thing written to stdout, so callers can
21
+ * capture it with `WT="$(sequant worktree resolve 123)"`. Everything else goes
22
+ * to stderr.
23
+ */
24
+ export declare function worktreeResolveCommand(issueArg: string, options?: WorktreeResolveOptions): Promise<void>;
25
+ /**
26
+ * `sequant worktree verify <path>` — confirm a path is a worktree of this
27
+ * repository (and, with `--issue`, that it belongs to that issue).
28
+ *
29
+ * Exit 1 with a named error is the signal for `/exec` to halt.
30
+ */
31
+ export declare function worktreeVerifyCommand(pathArg: string, options?: WorktreeVerifyOptions): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `sequant worktree` — repo-scoped worktree resolution for skill bodies (#899).
3
+ *
4
+ * `/fullsolve` uses `resolve` to turn an issue number into a real absolute
5
+ * path before exporting `SEQUANT_WORKTREE`; `/exec` uses `verify` to refuse a
6
+ * path that does not exist or belongs to another repository. Both fail closed:
7
+ * a non-zero exit means the caller must halt, not continue where it stands.
8
+ */
9
+ import chalk from "chalk";
10
+ import { resolveIssueWorktree, verifyWorktreePath, } from "../lib/workflow/worktree-resolver.js";
11
+ function parseIssue(arg) {
12
+ const issue = Number.parseInt(arg, 10);
13
+ if (!Number.isInteger(issue) || issue <= 0) {
14
+ console.error(chalk.red(`Invalid issue number: ${arg}`));
15
+ process.exitCode = 2;
16
+ return null;
17
+ }
18
+ return issue;
19
+ }
20
+ /**
21
+ * `sequant worktree resolve <issue>` — print the absolute path of this
22
+ * repository's worktree for an issue.
23
+ *
24
+ * On success the path is the *only* thing written to stdout, so callers can
25
+ * capture it with `WT="$(sequant worktree resolve 123)"`. Everything else goes
26
+ * to stderr.
27
+ */
28
+ export async function worktreeResolveCommand(issueArg, options = {}) {
29
+ const issue = parseIssue(issueArg);
30
+ if (issue === null)
31
+ return;
32
+ const result = resolveIssueWorktree(issue);
33
+ if (!result.ok) {
34
+ process.exitCode = 1;
35
+ if (options.json) {
36
+ console.error(JSON.stringify({
37
+ issue,
38
+ ok: false,
39
+ error: result.error,
40
+ message: result.message,
41
+ candidates: result.candidates,
42
+ }));
43
+ }
44
+ else {
45
+ console.error(chalk.red(result.message));
46
+ }
47
+ return;
48
+ }
49
+ if (options.json) {
50
+ console.log(JSON.stringify({
51
+ issue,
52
+ ok: true,
53
+ path: result.path,
54
+ branch: result.branch,
55
+ }));
56
+ return;
57
+ }
58
+ // Bare path only — this is consumed by `$(...)` in skill bodies.
59
+ console.log(result.path);
60
+ }
61
+ /**
62
+ * `sequant worktree verify <path>` — confirm a path is a worktree of this
63
+ * repository (and, with `--issue`, that it belongs to that issue).
64
+ *
65
+ * Exit 1 with a named error is the signal for `/exec` to halt.
66
+ */
67
+ export async function worktreeVerifyCommand(pathArg, options = {}) {
68
+ let issue;
69
+ if (options.issue !== undefined) {
70
+ const parsed = parseIssue(options.issue);
71
+ if (parsed === null)
72
+ return;
73
+ issue = parsed;
74
+ }
75
+ const result = verifyWorktreePath(pathArg, { issue });
76
+ if (!result.ok) {
77
+ process.exitCode = 1;
78
+ if (options.json) {
79
+ console.error(JSON.stringify({
80
+ ok: false,
81
+ error: result.error,
82
+ message: result.message,
83
+ }));
84
+ }
85
+ else {
86
+ console.error(chalk.red(result.message));
87
+ }
88
+ return;
89
+ }
90
+ if (options.json) {
91
+ console.log(JSON.stringify({ ok: true, path: result.path, branch: result.branch }));
92
+ return;
93
+ }
94
+ console.log(chalk.green(`✓ ${result.path} (${result.branch})`));
95
+ }
@@ -31,3 +31,26 @@ export declare function parseWholeNumber(flag: string, opts: WholeNumberOptions)
31
31
  * `merge --watch` messages this originally shipped with do not change.
32
32
  */
33
33
  export declare function parsePositiveSeconds(flag: string): (value: string) => number;
34
+ /**
35
+ * Build a commander coercion for a `--models`/`--efforts`-shaped flag (#914).
36
+ *
37
+ * Validates the spec via `config-resolver.ts`'s `parsePhaseSpec` — a bare
38
+ * value applies to every phase, a comma list of `phase=value` pairs applies
39
+ * per phase, and a malformed spec (empty segment, mixed bare/pair form, or
40
+ * an unrecognized phase name) fails fast here rather than reaching
41
+ * `resolvePhasePolicies` silently. On success, returns the ORIGINAL raw spec
42
+ * string unchanged (not the parsed map) — `RunOptions.models`/`.efforts`
43
+ * stay plain strings, and `resolvePhasePolicies` re-parses for real when
44
+ * building `ExecutionConfig.phasePolicies`, so parsing has exactly one
45
+ * source of truth even though it runs twice.
46
+ *
47
+ * `allowedValues`, when given, additionally rejects any resolved value not
48
+ * in the set — used for `--efforts` (closed enum: `settings.ts:EFFORT_LEVELS`)
49
+ * so a typo like `--efforts exec=mediu` fails at the CLI boundary the same
50
+ * way it already does when set via `settings.json`, instead of only
51
+ * surfacing once the value reaches the SDK. `--models` passes no
52
+ * `allowedValues` — model aliases/IDs are intentionally unvalidated (they
53
+ * churn independently of sequant releases; the SDK errors clearly on a bad
54
+ * one).
55
+ */
56
+ export declare function parsePhaseSpecFlag(flag: string, phaseNames: string[], allowedValues?: readonly string[]): (value: string) => string;
@@ -8,6 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import { InvalidArgumentError } from "commander";
11
+ import { parsePhaseSpec } from "./workflow/config-resolver.js";
11
12
  /**
12
13
  * Build a commander coercion for a whole-number flag.
13
14
  *
@@ -44,3 +45,45 @@ export function parsePositiveSeconds(flag) {
44
45
  unitSingular: "second",
45
46
  });
46
47
  }
48
+ /**
49
+ * Build a commander coercion for a `--models`/`--efforts`-shaped flag (#914).
50
+ *
51
+ * Validates the spec via `config-resolver.ts`'s `parsePhaseSpec` — a bare
52
+ * value applies to every phase, a comma list of `phase=value` pairs applies
53
+ * per phase, and a malformed spec (empty segment, mixed bare/pair form, or
54
+ * an unrecognized phase name) fails fast here rather than reaching
55
+ * `resolvePhasePolicies` silently. On success, returns the ORIGINAL raw spec
56
+ * string unchanged (not the parsed map) — `RunOptions.models`/`.efforts`
57
+ * stay plain strings, and `resolvePhasePolicies` re-parses for real when
58
+ * building `ExecutionConfig.phasePolicies`, so parsing has exactly one
59
+ * source of truth even though it runs twice.
60
+ *
61
+ * `allowedValues`, when given, additionally rejects any resolved value not
62
+ * in the set — used for `--efforts` (closed enum: `settings.ts:EFFORT_LEVELS`)
63
+ * so a typo like `--efforts exec=mediu` fails at the CLI boundary the same
64
+ * way it already does when set via `settings.json`, instead of only
65
+ * surfacing once the value reaches the SDK. `--models` passes no
66
+ * `allowedValues` — model aliases/IDs are intentionally unvalidated (they
67
+ * churn independently of sequant releases; the SDK errors clearly on a bad
68
+ * one).
69
+ */
70
+ export function parsePhaseSpecFlag(flag, phaseNames, allowedValues) {
71
+ return (value) => {
72
+ let parsed;
73
+ try {
74
+ parsed = parsePhaseSpec(value, phaseNames);
75
+ }
76
+ catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ throw new InvalidArgumentError(`${flag}: ${message}`);
79
+ }
80
+ if (allowedValues) {
81
+ for (const v of Object.values(parsed)) {
82
+ if (!allowedValues.includes(v)) {
83
+ throw new InvalidArgumentError(`${flag}: '${v}' is not one of ${allowedValues.join("|")}.`);
84
+ }
85
+ }
86
+ }
87
+ return value;
88
+ };
89
+ }
@@ -117,6 +117,8 @@ export interface IssueSummary {
117
117
  loopTriggered?: boolean;
118
118
  prNumber?: number;
119
119
  prUrl?: string;
120
+ /** Set when PR creation was skipped for lack of commits ahead of base (#920). */
121
+ prSkippedReason?: string;
120
122
  failureReason?: string;
121
123
  qaVerdict?: string;
122
124
  unmetCount?: number;