sequant 2.10.0 → 2.12.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 (108) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +19 -2
  4. package/dist/bin/cli.js +47 -2
  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/hooks/pre-tool.sh +331 -12
  8. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
  9. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
  10. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
  11. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
  12. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
  13. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
  14. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
  15. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
  17. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
  18. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  19. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
  20. package/dist/src/commands/doctor.js +20 -18
  21. package/dist/src/commands/locks.d.ts +20 -1
  22. package/dist/src/commands/locks.js +206 -4
  23. package/dist/src/commands/ready.d.ts +6 -0
  24. package/dist/src/commands/ready.js +19 -1
  25. package/dist/src/commands/run-display.js +1 -0
  26. package/dist/src/commands/worktree.d.ts +31 -0
  27. package/dist/src/commands/worktree.js +95 -0
  28. package/dist/src/lib/ac-linter.js +26 -0
  29. package/dist/src/lib/ac-parser.d.ts +40 -0
  30. package/dist/src/lib/ac-parser.js +202 -16
  31. package/dist/src/lib/cli-flags.d.ts +23 -0
  32. package/dist/src/lib/cli-flags.js +43 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
  34. package/dist/src/lib/cli-ui/run-renderer.js +7 -1
  35. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  36. package/dist/src/lib/locks/checkout-lock.js +389 -0
  37. package/dist/src/lib/locks/index.d.ts +6 -3
  38. package/dist/src/lib/locks/index.js +4 -2
  39. package/dist/src/lib/locks/lock-manager.d.ts +81 -1
  40. package/dist/src/lib/locks/lock-manager.js +230 -5
  41. package/dist/src/lib/locks/types.d.ts +72 -0
  42. package/dist/src/lib/locks/types.js +28 -0
  43. package/dist/src/lib/markdown-fence.d.ts +24 -0
  44. package/dist/src/lib/markdown-fence.js +51 -0
  45. package/dist/src/lib/mcp-config.d.ts +24 -0
  46. package/dist/src/lib/mcp-config.js +51 -0
  47. package/dist/src/lib/scope/analyzer.d.ts +4 -0
  48. package/dist/src/lib/scope/analyzer.js +7 -1
  49. package/dist/src/lib/settings.d.ts +111 -1
  50. package/dist/src/lib/settings.js +59 -0
  51. package/dist/src/lib/system.d.ts +7 -3
  52. package/dist/src/lib/system.js +7 -3
  53. package/dist/src/lib/test-tautology-detector.d.ts +4 -3
  54. package/dist/src/lib/test-tautology-detector.js +147 -40
  55. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  56. package/dist/src/lib/workflow/batch-executor.js +154 -23
  57. package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
  58. package/dist/src/lib/workflow/config-resolver.js +90 -0
  59. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  60. package/dist/src/lib/workflow/drivers/claude-code.js +14 -3
  61. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  62. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  63. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  64. package/dist/src/lib/workflow/error-classifier.js +4 -0
  65. package/dist/src/lib/workflow/log-writer.d.ts +10 -1
  66. package/dist/src/lib/workflow/log-writer.js +20 -0
  67. package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
  68. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  69. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  70. package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
  71. package/dist/src/lib/workflow/mutation-marker.js +97 -0
  72. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  73. package/dist/src/lib/workflow/phase-detection.js +5 -1
  74. package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
  75. package/dist/src/lib/workflow/phase-executor.js +60 -4
  76. package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
  77. package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
  78. package/dist/src/lib/workflow/ready-gate.d.ts +53 -1
  79. package/dist/src/lib/workflow/ready-gate.js +105 -14
  80. package/dist/src/lib/workflow/run-log-schema.d.ts +175 -0
  81. package/dist/src/lib/workflow/run-log-schema.js +71 -1
  82. package/dist/src/lib/workflow/run-orchestrator.js +27 -0
  83. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  84. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  85. package/dist/src/lib/workflow/state-schema.d.ts +5 -1
  86. package/dist/src/lib/workflow/state-schema.js +8 -1
  87. package/dist/src/lib/workflow/types.d.ts +78 -0
  88. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  89. package/dist/src/lib/workflow/worktree-manager.js +9 -1
  90. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  91. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  92. package/package.json +4 -3
  93. package/templates/hooks/pre-tool.sh +331 -12
  94. package/templates/scripts/cleanup-worktree.sh +36 -15
  95. package/templates/scripts/new-feature.sh +25 -19
  96. package/templates/skills/_shared/references/subagent-types.md +7 -18
  97. package/templates/skills/assess/SKILL.md +5 -1
  98. package/templates/skills/exec/SKILL.md +62 -8
  99. package/templates/skills/fullsolve/SKILL.md +187 -28
  100. package/templates/skills/loop/SKILL.md +127 -23
  101. package/templates/skills/merger/SKILL.md +130 -13
  102. package/templates/skills/qa/SKILL.md +306 -8
  103. package/templates/skills/release/SKILL.md +79 -0
  104. package/templates/skills/spec/SKILL.md +40 -20
  105. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  106. package/templates/skills/test/SKILL.md +1 -1
  107. package/templates/skills/testgen/SKILL.md +23 -6
  108. package/templates/agents/sequant-explorer.md +0 -24
@@ -1,6 +1,17 @@
1
1
  # Recommended Workflow Format
2
2
 
3
- This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output. The `parseRecommendedWorkflow()` function parses this format to determine which phases to execute.
3
+ This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output.
4
+
5
+ ## Resolution chain (#921)
6
+
7
+ `sequant run` resolves phases through an ordered chain, not `parseRecommendedWorkflow()` alone:
8
+
9
+ 1. **`SEQUANT_SPEC` marker** — a structured HTML comment in the posted plan comment, e.g. `<!-- SEQUANT_SPEC: {"phases":["testgen","exec","qa"],"qualityLoop":true} -->`. This is the primary, durable channel — always emit it alongside the prose section below.
10
+ 2. **Comment prose** — `parseRecommendedWorkflow()` applied to the plan comment body (same format as this doc).
11
+ 3. **Chat text** — the same parser applied to the spec agent's chat output. Nondeterministic: only present if the agent happens to restate the section in chat rather than posting via a body file (#814).
12
+ 4. **Label fallback** — `detectPhasesFromLabels()`. Can never produce `testgen` or `security-review`.
13
+
14
+ The marker's `phases` array excludes `spec` (it already ran) and must name only registered phases — an unknown phase name invalidates the whole marker and falls through to step 2.
4
15
 
5
16
  ## Format
6
17
 
@@ -10,6 +21,8 @@ This document shows the expected output format for the `## Recommended Workflow`
10
21
  **Phases:** spec → exec → qa
11
22
  **Quality Loop:** disabled
12
23
  **Reasoning:** Brief explanation of why this workflow was chosen.
24
+
25
+ <!-- SEQUANT_SPEC: {"phases":["exec","qa"],"qualityLoop":false} -->
13
26
  ```
14
27
 
15
28
  ## Examples
@@ -582,7 +582,7 @@ Create structured test results:
582
582
  ### 3.2 GitHub Comment
583
583
 
584
584
  **If orchestrated (SEQUANT_ORCHESTRATOR is set):**
585
- - Skip posting GitHub comment (orchestrator handles summary)
585
+ - Skip posting this skill's own GitHub comment no per-phase comment is posted under `sequant run`; test results surface through the run summary and the PR body (#964)
586
586
  - Include test summary in output for orchestrator to capture
587
587
  - Let orchestrator aggregate results across phases
588
588
 
@@ -6,6 +6,7 @@ metadata:
6
6
  author: sequant
7
7
  version: "1.0"
8
8
  allowed-tools:
9
+ - Bash(npx sequant worktree:*)
9
10
  - Read
10
11
  - Write
11
12
  - Edit
@@ -530,17 +531,33 @@ If an AC has verification method "N/A - Trivial", skip test generation and note
530
531
 
531
532
  ### Step 4: Locate Feature Worktree
532
533
 
533
- If generating file-based tests (Unit Test, Integration Test), find the worktree:
534
+ If generating file-based tests (Unit Test, Integration Test), find the worktree.
534
535
 
535
- ```bash
536
- git worktree list | grep -E "feature.*<issue-number>" || true
537
- ```
536
+ <!-- BEGIN: worktree-standalone-lookup (#899) -->
537
+
538
+ Resolve it through git, not the filesystem:
538
539
 
539
- Or check:
540
540
  ```bash
541
- ls ../worktrees/feature/<issue-number>-*/
541
+ WORKTREE="$(npx sequant worktree resolve <issue-number>)" || {
542
+ echo "❌ HALT: no worktree for #<issue-number> in this repository."
543
+ exit 1
544
+ }
545
+ cd "$WORKTREE"
542
546
  ```
543
547
 
548
+ `sequant worktree resolve` reads `git worktree list` in the current repository
549
+ — which reports only *this* repo's worktrees — and selects on the **branch**
550
+ git reports, not the directory name.
551
+
552
+ **Do not glob `../worktrees/feature/<issue-number>-*`, and do not grep
553
+ `git worktree list` for the issue number.** The first matches across sibling
554
+ repositories, which share that directory; the second matches the printed path,
555
+ so it keys on the directory slug — and a slug can drift from its own branch
556
+ after a rename. Because this skill **writes test files**, landing in the wrong
557
+ tree scatters stubs into an unrelated project.
558
+
559
+ <!-- END: worktree-standalone-lookup (#899) -->
560
+
544
561
  Create test directories if needed:
545
562
  ```bash
546
563
  mkdir -p __tests__/integration
@@ -8,7 +8,8 @@ import { GitHubProvider } from "../lib/workflow/platforms/github.js";
8
8
  import { fileExists, isExecutable } from "../lib/fs.js";
9
9
  import { checkSkillsInstalled } from "../lib/skills-check.js";
10
10
  import { getManifest } from "../lib/manifest.js";
11
- import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, getMcpServersConfig, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
11
+ import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
12
+ import { getPhaseMcpServersConfig } from "../lib/mcp-config.js";
12
13
  import { getSettings, DEFAULT_AGENT_SETTINGS } from "../lib/settings.js";
13
14
  import { checkVersionThorough, getVersionWarning, resolveCliInvocation, } from "../lib/version-check.js";
14
15
  import { areSkillsOutdated } from "./sync.js";
@@ -435,23 +436,24 @@ export async function doctorCommand(options = {}) {
435
436
  message: "No optional MCPs configured (Sequant works without them, but they enhance functionality)",
436
437
  });
437
438
  }
438
- // Check: MCP availability for headless mode (sequant run)
439
- const mcpServersConfig = getMcpServersConfig();
440
- if (mcpServersConfig) {
441
- const serverCount = Object.keys(mcpServersConfig).length;
442
- checks.push({
443
- name: "MCP Servers (headless)",
444
- status: "pass",
445
- message: `Available for sequant run (${serverCount} server${serverCount !== 1 ? "s" : ""} configured)`,
446
- });
447
- }
448
- else {
449
- checks.push({
450
- name: "MCP Servers (headless)",
451
- status: "warn",
452
- message: "Not available for sequant run (no Claude Desktop config found or empty mcpServers)",
453
- });
454
- }
439
+ // Check: MCP availability for headless mode (sequant run) (#936)
440
+ //
441
+ // Phase agents read the project's .mcp.json + settings.run.mcpAllowlist,
442
+ // never Claude Desktop config wholesale — see getPhaseMcpServersConfig.
443
+ // The sequant server is always guaranteed, so this check reports what a
444
+ // phase will actually receive rather than pass/warn on presence.
445
+ const phaseServersConfig = getPhaseMcpServersConfig(process.cwd(), {
446
+ desktopAllowlist: settings.run.mcpAllowlist,
447
+ });
448
+ const phaseServerCount = Object.keys(phaseServersConfig).length;
449
+ const extraServerCount = phaseServerCount - 1; // minus the guaranteed sequant entry
450
+ checks.push({
451
+ name: "MCP Servers (headless)",
452
+ status: "pass",
453
+ message: extraServerCount > 0
454
+ ? `Available for sequant run (${phaseServerCount} servers: sequant + ${extraServerCount} from .mcp.json${settings.run.mcpAllowlist?.length ? "/mcpAllowlist" : ""})`
455
+ : "Available for sequant run (sequant only — add servers to .mcp.json, or settings.run.mcpAllowlist for desktop servers, for more)",
456
+ });
455
457
  // Check: Sequant MCP server health
456
458
  try {
457
459
  // Verify MCP server can be created (validates SDK availability)
@@ -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) {
@@ -195,9 +200,15 @@ export async function readyCommand(issueArg, options) {
195
200
  nonGoals,
196
201
  phaseTimeout,
197
202
  mcp,
203
+ mcpAllowlist: settings.run.mcpAllowlist,
198
204
  verbose: options.verbose,
199
205
  runPhase,
200
206
  onProgress,
207
+ phasePolicies,
208
+ effortEscalation,
209
+ // #937 AC-4: persist the final gap report so it survives the terminal
210
+ // closing (previously terminal-scrollback only under `ac` policy).
211
+ postReport: (body) => gh.postComment(String(issueNumber), body),
201
212
  });
202
213
  }
203
214
  catch (error) {
@@ -220,6 +231,13 @@ export async function readyCommand(issueArg, options) {
220
231
  process.exitCode = 2;
221
232
  return;
222
233
  }
234
+ // #915: surface any escalated qa/loop dispatches — the gate has no live
235
+ // print of its own (see `withEscalatedEffort` in `ready-gate.ts`).
236
+ if (options.verbose) {
237
+ for (const e of result.effortEscalations) {
238
+ console.log(colors.muted(` effort: ${e.base} → ${e.escalated} (${e.phase} retry)`));
239
+ }
240
+ }
223
241
  // Persist the terminal state so `sequant status` reflects it (Derived AC).
224
242
  // Best-effort: initialize the issue in state if a prior run didn't track it.
225
243
  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
+ }
@@ -272,6 +272,29 @@ function detectTitleBodyTension(ac) {
272
272
  suggestion: "Two verification bars detected. Either (a) tighten the title to match the runtime body (e.g., 'Smoke test execution — capture evidence'), or (b) split the runtime requirement into a separate AC.",
273
273
  };
274
274
  }
275
+ /**
276
+ * Detect a test-type AC (unit/integration/browser) whose verification
277
+ * method came from keyword inference rather than a declared `Evidence:`
278
+ * clause (#938). `manual` ACs are exempt — docs/decision ACs legitimately
279
+ * have no runnable evidence.
280
+ *
281
+ * Warning-only, same convention as the regex-based DEFAULT_LINT_PATTERNS.
282
+ *
283
+ * @param ac - The acceptance criterion to check
284
+ * @returns A lint issue if evidence is missing, otherwise null
285
+ */
286
+ function detectMissingEvidence(ac) {
287
+ if (ac.evidence)
288
+ return null;
289
+ if (ac.verificationMethod === "manual")
290
+ return null;
291
+ return {
292
+ type: "incomplete",
293
+ matchedPattern: ac.verificationMethod,
294
+ problem: `Incomplete: verification not named — method "${ac.verificationMethod}" was inferred from keywords, not declared`,
295
+ suggestion: "Add a trailing `Evidence:` clause naming the command or check that verifies this AC (e.g., `Evidence: \\`npm test -- foo\\``).",
296
+ };
297
+ }
275
298
  /**
276
299
  * Lint a single acceptance criterion against all patterns
277
300
  *
@@ -296,6 +319,9 @@ export function lintAcceptanceCriterion(ac, patterns = DEFAULT_LINT_PATTERNS) {
296
319
  const tension = detectTitleBodyTension(ac);
297
320
  if (tension)
298
321
  issues.push(tension);
322
+ const missingEvidence = detectMissingEvidence(ac);
323
+ if (missingEvidence)
324
+ issues.push(missingEvidence);
299
325
  return {
300
326
  ac,
301
327
  issues,