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
@@ -14,6 +14,22 @@ export const DEFAULT_STALE_AGE_MS = 2 * 60 * 60 * 1000; // 2h
14
14
  * Override per-process via `SEQUANT_SKILL_LOCK_TTL_MS` (milliseconds).
15
15
  */
16
16
  export const DEFAULT_SKILL_LOCK_TTL_MS = 6 * 60 * 60 * 1000; // 6h
17
+ /**
18
+ * Absolute age ceiling (ms) beyond which a lock is stale no matter what
19
+ * (#856). Unlike the two TTLs above this one is NOT conditional on host or
20
+ * PID liveness: the same-host branch of `classifyStaleness` treats a live PID
21
+ * as authoritative proof of freshness, so before this ceiling existed a lock
22
+ * whose PID had been recycled by the OS read as fresh forever and blocked its
23
+ * issue permanently (observed: `505.lock` from 2026-05-14, `708.lock`,
24
+ * `803.lock`). It is also the recovery path for locks leaked by a SIGKILLed
25
+ * run, where no in-process release handler can ever fire.
26
+ *
27
+ * 24h is ~48x the 30-minute default phase timeout and 4x
28
+ * `DEFAULT_SKILL_LOCK_TTL_MS`, so no real run can reach it.
29
+ *
30
+ * Override per-process via `SEQUANT_MAX_LOCK_AGE_MS` (milliseconds).
31
+ */
32
+ export const DEFAULT_MAX_LOCK_AGE_MS = 24 * 60 * 60 * 1000; // 24h
17
33
  /** Default lock directory relative to the project root. */
18
34
  export const DEFAULT_LOCKS_DIR = ".sequant/locks";
19
35
  /** On-disk lock payload. */
@@ -29,3 +45,31 @@ export const LockFileSchema = z.object({
29
45
  */
30
46
  skipPidCheck: z.boolean().optional(),
31
47
  });
48
+ /**
49
+ * Filename of the checkout-scoped lock (#901), stored alongside the numeric
50
+ * `<issue>.lock` files. Deliberately non-numeric so `LockManager.list()` —
51
+ * which parses each filename as an issue number and skips anything else —
52
+ * ignores it rather than surfacing a bogus `NaN` issue.
53
+ */
54
+ export const CHECKOUT_LOCK_FILENAME = "checkout.lock";
55
+ /**
56
+ * On-disk payload for the checkout-scoped lock (#901).
57
+ *
58
+ * The per-issue lock from #625 is keyed on issue number, so two sessions
59
+ * working *different* issues take different lock files and never contend —
60
+ * yet `git checkout`, `reset`, `rebase` and `merge` are global to a working
61
+ * tree. This lock represents the tree itself.
62
+ *
63
+ * Extends `LockFileSchema` with:
64
+ * - `issue` — which issue the holder is working on, so a refusal can name it
65
+ * (AC-2) and point the loser at the right worktree (AC-3).
66
+ * - `sessionId` — Claude Code's per-session id, read from the PreToolUse
67
+ * stdin envelope. Optional: it is the *preferred* holder identity because a
68
+ * skill shell's PID dies immediately after acquire, but the hook falls back
69
+ * to `pid`+`hostname` when the envelope does not carry one, so nothing
70
+ * depends on it being present.
71
+ */
72
+ export const CheckoutLockFileSchema = LockFileSchema.extend({
73
+ issue: z.number().int().positive(),
74
+ sessionId: z.string().optional(),
75
+ });
@@ -6,6 +6,21 @@
6
6
  */
7
7
  /** Path to the project-level MCP config file used by Claude Code */
8
8
  export declare const PROJECT_MCP_JSON = ".mcp.json";
9
+ /**
10
+ * npm package specifier used to launch the MCP server (#793).
11
+ *
12
+ * We pin to the installed version rather than `@latest` so that `npx` does not
13
+ * re-resolve and reinstall the package on the first MCP reconnect after every
14
+ * sequant release — the reinstall was surfacing as an opaque
15
+ * `Failed to reconnect to sequant: -32000` when npx's cache was corrupted
16
+ * (npm ENOTEMPTY). `setup`/`update` (re)write this pin, so the MCP server
17
+ * tracks new releases when the user runs `sequant update` — matching how the
18
+ * plugin cache already pins. See docs/troubleshooting.md ("MCP Server Issues").
19
+ *
20
+ * Falls back to `latest` if the installed version cannot be resolved, so a
21
+ * failed lookup never writes a bogus `sequant@0.0.0` pin into `.mcp.json`.
22
+ */
23
+ export declare function getSequantPackageSpec(): string;
9
24
  export type McpClientType = "claude-desktop" | "cursor" | "vscode-continue";
10
25
  export interface McpClientInfo {
11
26
  name: string;
@@ -52,3 +67,33 @@ export interface ProjectMcpJsonResult {
52
67
  * because Claude Code runs from the project root.
53
68
  */
54
69
  export declare function createProjectMcpJson(projectDir?: string): ProjectMcpJsonResult;
70
+ export interface SyncMcpPinResult {
71
+ updated: boolean;
72
+ /** Why no rewrite happened, when updated === false. */
73
+ reason?: "no-file" | "no-entry" | "no-pin" | "already-current";
74
+ /** Previous package spec, when updated === true (e.g. "sequant@latest"). */
75
+ from?: string;
76
+ /** New package spec, when updated === true (e.g. "sequant@2.9.0"). */
77
+ to?: string;
78
+ }
79
+ /**
80
+ * Re-pin an existing project `.mcp.json` sequant entry to the installed version (#793).
81
+ *
82
+ * Unlike {@link createProjectMcpJson} (which skips when a sequant entry already
83
+ * exists), this is the `update`/`sync` path: it refreshes the version pin so the
84
+ * MCP server tracks the release the user just updated to. It only rewrites the
85
+ * `sequant@<version>` token inside `args` and leaves everything else untouched.
86
+ *
87
+ * No-ops (returns `updated: false`) when:
88
+ * - `.mcp.json` doesn't exist (`no-file`) — we never create it here; that's init's job
89
+ * - there's no sequant server entry (`no-entry`)
90
+ * - the entry uses a local-binary form with no `sequant@…` arg (`no-pin`) — a
91
+ * deliberate contributor override we must not clobber
92
+ * - the pin already matches the installed version (`already-current`)
93
+ *
94
+ * With `opts.dryRun`, computes `from`/`to` and returns `updated: true` for a
95
+ * pending change but does not write the file — the caller reports it as a preview.
96
+ */
97
+ export declare function syncSequantMcpPin(projectDir?: string, opts?: {
98
+ dryRun?: boolean;
99
+ }): SyncMcpPinResult;
@@ -7,8 +7,29 @@
7
7
  import * as fs from "fs";
8
8
  import * as os from "os";
9
9
  import * as path from "path";
10
+ import { getVersion } from "./version.js";
10
11
  /** Path to the project-level MCP config file used by Claude Code */
11
12
  export const PROJECT_MCP_JSON = ".mcp.json";
13
+ /**
14
+ * npm package specifier used to launch the MCP server (#793).
15
+ *
16
+ * We pin to the installed version rather than `@latest` so that `npx` does not
17
+ * re-resolve and reinstall the package on the first MCP reconnect after every
18
+ * sequant release — the reinstall was surfacing as an opaque
19
+ * `Failed to reconnect to sequant: -32000` when npx's cache was corrupted
20
+ * (npm ENOTEMPTY). `setup`/`update` (re)write this pin, so the MCP server
21
+ * tracks new releases when the user runs `sequant update` — matching how the
22
+ * plugin cache already pins. See docs/troubleshooting.md ("MCP Server Issues").
23
+ *
24
+ * Falls back to `latest` if the installed version cannot be resolved, so a
25
+ * failed lookup never writes a bogus `sequant@0.0.0` pin into `.mcp.json`.
26
+ */
27
+ export function getSequantPackageSpec() {
28
+ const version = getVersion();
29
+ return version && version !== "0.0.0"
30
+ ? `sequant@${version}`
31
+ : "sequant@latest";
32
+ }
12
33
  /**
13
34
  * Clients that need an explicit cwd because they don't run from the project directory.
14
35
  */
@@ -25,7 +46,7 @@ const CLIENTS_NEEDING_CWD = new Set([
25
46
  export function getSequantMcpConfig(options) {
26
47
  const config = {
27
48
  command: "npx",
28
- args: ["-y", "sequant@latest", "serve"],
49
+ args: ["-y", getSequantPackageSpec(), "serve"],
29
50
  };
30
51
  // Add cwd for clients that don't run from the project directory
31
52
  if (options?.clientType && CLIENTS_NEEDING_CWD.has(options.clientType)) {
@@ -170,3 +191,58 @@ export function createProjectMcpJson(projectDir) {
170
191
  }
171
192
  return { created: true, merged: false, skipped: false };
172
193
  }
194
+ /**
195
+ * Re-pin an existing project `.mcp.json` sequant entry to the installed version (#793).
196
+ *
197
+ * Unlike {@link createProjectMcpJson} (which skips when a sequant entry already
198
+ * exists), this is the `update`/`sync` path: it refreshes the version pin so the
199
+ * MCP server tracks the release the user just updated to. It only rewrites the
200
+ * `sequant@<version>` token inside `args` and leaves everything else untouched.
201
+ *
202
+ * No-ops (returns `updated: false`) when:
203
+ * - `.mcp.json` doesn't exist (`no-file`) — we never create it here; that's init's job
204
+ * - there's no sequant server entry (`no-entry`)
205
+ * - the entry uses a local-binary form with no `sequant@…` arg (`no-pin`) — a
206
+ * deliberate contributor override we must not clobber
207
+ * - the pin already matches the installed version (`already-current`)
208
+ *
209
+ * With `opts.dryRun`, computes `from`/`to` and returns `updated: true` for a
210
+ * pending change but does not write the file — the caller reports it as a preview.
211
+ */
212
+ export function syncSequantMcpPin(projectDir, opts) {
213
+ const mcpJsonPath = path.resolve(projectDir ?? ".", PROJECT_MCP_JSON);
214
+ if (!fs.existsSync(mcpJsonPath)) {
215
+ return { updated: false, reason: "no-file" };
216
+ }
217
+ let config;
218
+ try {
219
+ config = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
220
+ }
221
+ catch {
222
+ // Corrupt file — leave it alone rather than overwriting user content.
223
+ return { updated: false, reason: "no-file" };
224
+ }
225
+ const servers = config?.mcpServers;
226
+ const sequant = servers?.sequant;
227
+ if (!sequant) {
228
+ return { updated: false, reason: "no-entry" };
229
+ }
230
+ const args = sequant.args;
231
+ if (!Array.isArray(args)) {
232
+ return { updated: false, reason: "no-pin" };
233
+ }
234
+ const pinIndex = args.findIndex((a) => typeof a === "string" && a.startsWith("sequant@"));
235
+ if (pinIndex === -1) {
236
+ return { updated: false, reason: "no-pin" };
237
+ }
238
+ const from = args[pinIndex];
239
+ const to = getSequantPackageSpec();
240
+ if (from === to) {
241
+ return { updated: false, reason: "already-current" };
242
+ }
243
+ if (!opts?.dryRun) {
244
+ args[pinIndex] = to;
245
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + "\n");
246
+ }
247
+ return { updated: true, from, to };
248
+ }
@@ -2,11 +2,30 @@
2
2
  * Combined branch testing (AC-1)
3
3
  *
4
4
  * Creates a temporary branch merging all feature branches from a run batch,
5
- * runs npm test && npm run build on the combined state, and reports results.
5
+ * reinstalls dependencies when the merge moved a lockfile, then runs the
6
+ * project's test and build scripts on the combined state and reports results.
6
7
  */
7
8
  import type { BranchInfo, CheckResult, BranchCheckResult, CheckFinding } from "./types.js";
9
+ import { resolveFailureReason, type CommandResult } from "./command-result.js";
10
+ export { resolveFailureReason, type CommandResult };
8
11
  /**
9
- * Create temp branch, merge all feature branches, run tests and build.
12
+ * Run a package-manager command line (e.g. "npm ci", "pnpm run build").
13
+ *
14
+ * @param command Full command line, as stored in PM_CONFIG
15
+ * @param cwd Working directory
16
+ * @param timeoutMs Kill the command after this long
17
+ * @internal Exported for testing
18
+ */
19
+ export declare function runPackageManagerCommand(command: string, cwd: string, timeoutMs: number): CommandResult;
20
+ /**
21
+ * Whether any lockfile differs between `baseRef` and the current HEAD.
22
+ *
23
+ * @internal Exported for testing
24
+ */
25
+ export declare function lockfileChanged(repoRoot: string, baseRef: string): boolean;
26
+ /**
27
+ * Create temp branch, merge all feature branches, reinstall dependencies if the
28
+ * lockfile moved, then run tests and build.
10
29
  *
11
30
  * @param branches - Feature branches to merge
12
31
  * @param repoRoot - Path to the git repository root
@@ -2,10 +2,37 @@
2
2
  * Combined branch testing (AC-1)
3
3
  *
4
4
  * Creates a temporary branch merging all feature branches from a run batch,
5
- * runs npm test && npm run build on the combined state, and reports results.
5
+ * reinstalls dependencies when the merge moved a lockfile, then runs the
6
+ * project's test and build scripts on the combined state and reports results.
6
7
  */
7
8
  import { spawnSync } from "child_process";
8
9
  import { getBranchRef } from "./types.js";
10
+ import { detectPackageManagerSync, resolvePackageManagerConfig, } from "../stacks.js";
11
+ import { toCommandResult, resolveFailureReason, } from "./command-result.js";
12
+ // Re-exported for the existing test suite and any downstream consumer that
13
+ // imported these from here before they moved to ./command-result.js.
14
+ export { resolveFailureReason };
15
+ /**
16
+ * Lockfile names for the JS package managers we support. Kept in sync with the
17
+ * equivalent list in `workflow/worktree-manager.ts`.
18
+ */
19
+ const LOCKFILES = [
20
+ "package-lock.json",
21
+ "pnpm-lock.yaml",
22
+ "bun.lockb",
23
+ "bun.lock",
24
+ "yarn.lock",
25
+ ];
26
+ /**
27
+ * Per-step command timeouts. A dependency install and a full test suite are
28
+ * both slower than the previous flat 2-minute budget allowed. An expired
29
+ * timeout kills the process with `status: null` and no captured output, which
30
+ * showed up as a BLOCKED verdict with no stated reason (#803).
31
+ */
32
+ const INSTALL_TIMEOUT_MS = 300_000; // 5 min
33
+ const TEST_BUILD_TIMEOUT_MS = 600_000; // 10 min
34
+ /** Ref the combined state is built on top of, and compared against. */
35
+ const BASE_REF = "origin/main";
9
36
  /**
10
37
  * Run a git command and return the result
11
38
  */
@@ -22,23 +49,39 @@ function git(args, cwd) {
22
49
  };
23
50
  }
24
51
  /**
25
- * Run npm command and return result
52
+ * Run a package-manager command line (e.g. "npm ci", "pnpm run build").
53
+ *
54
+ * @param command Full command line, as stored in PM_CONFIG
55
+ * @param cwd Working directory
56
+ * @param timeoutMs Kill the command after this long
57
+ * @internal Exported for testing
26
58
  */
27
- function npm(args, cwd) {
28
- const result = spawnSync("npm", args, {
59
+ export function runPackageManagerCommand(command, cwd, timeoutMs) {
60
+ const [bin, ...args] = command.split(" ");
61
+ const result = spawnSync(bin, args, {
29
62
  cwd,
30
63
  stdio: "pipe",
31
64
  encoding: "utf-8",
32
- timeout: 120_000, // 2 min timeout for test/build
65
+ timeout: timeoutMs,
66
+ // On Windows the package managers are `.cmd` shims, which CreateProcess
67
+ // will not resolve from a bare "npm"/"pnpm". `command` is always a constant
68
+ // from PM_CONFIG — never user input — so there is nothing to inject here.
69
+ shell: process.platform === "win32",
33
70
  });
34
- return {
35
- ok: result.status === 0,
36
- stdout: result.stdout?.trim() ?? "",
37
- stderr: result.stderr?.trim() ?? "",
38
- };
71
+ return toCommandResult(result);
39
72
  }
40
73
  /**
41
- * Create temp branch, merge all feature branches, run tests and build.
74
+ * Whether any lockfile differs between `baseRef` and the current HEAD.
75
+ *
76
+ * @internal Exported for testing
77
+ */
78
+ export function lockfileChanged(repoRoot, baseRef) {
79
+ const result = git(["diff", "--name-only", baseRef, "HEAD", "--", ...LOCKFILES], repoRoot);
80
+ return result.ok && result.stdout.length > 0;
81
+ }
82
+ /**
83
+ * Create temp branch, merge all feature branches, reinstall dependencies if the
84
+ * lockfile moved, then run tests and build.
42
85
  *
43
86
  * @param branches - Feature branches to merge
44
87
  * @param repoRoot - Path to the git repository root
@@ -49,123 +92,203 @@ export function runCombinedBranchTest(branches, repoRoot) {
49
92
  const tempBranch = `merge-check/temp-${Date.now()}`;
50
93
  const branchResults = [];
51
94
  const batchFindings = [];
52
- const mergeAttempts = [];
53
- // Save current branch to restore in finally block
95
+ const pm = detectPackageManagerSync(repoRoot);
96
+ // Resolved against the repo rather than read straight off PM_CONFIG: yarn's
97
+ // frozen install differs between classic and berry, and `pm` is "yarn" for
98
+ // both (#871).
99
+ //
100
+ // This resolution describes the PRE-MERGE tree, which is what the restore
101
+ // install in the `finally` below runs against — it fires after
102
+ // `git checkout restoreBranch`, so the original branch's yarn major is the
103
+ // right one there. `runChecks` re-resolves for the combined state, where a
104
+ // merged branch may have changed the major.
105
+ const pmConfig = resolvePackageManagerConfig(pm, repoRoot);
106
+ // Flipped the moment we start installing against the combined lockfile, so
107
+ // the caller's node_modules can be put back afterwards. A mutable holder
108
+ // rather than a return value: if runChecks throws, a return value never
109
+ // arrives and the restore would be skipped precisely when the tree is most
110
+ // likely to be inconsistent.
111
+ const installState = { installedCombinedDeps: false };
112
+ // Save current branch to restore in the cleanup step
54
113
  const originalBranch = git(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
55
114
  try {
56
- // Fetch latest from remote
57
- git(["fetch", "origin"], repoRoot);
58
- // Create temp branch from main
59
- const createResult = git(["checkout", "-b", tempBranch, "origin/main"], repoRoot);
60
- if (!createResult.ok) {
61
- batchFindings.push({
62
- check: "combined-branch-test",
63
- severity: "error",
64
- message: `Failed to create temp branch: ${createResult.stderr}`,
65
- });
66
- return buildResult(branchResults, batchFindings, startTime);
67
- }
68
- // Merge each feature branch
69
- for (const branch of branches) {
70
- const mergeResult = git(["merge", "--no-ff", "--no-edit", getBranchRef(branch)], repoRoot);
71
- if (mergeResult.ok) {
72
- mergeAttempts.push({
73
- issueNumber: branch.issueNumber,
74
- branch: branch.branch,
75
- success: true,
76
- });
77
- branchResults.push({
78
- issueNumber: branch.issueNumber,
79
- verdict: "PASS",
80
- findings: [
81
- {
82
- check: "combined-branch-test",
83
- severity: "info",
84
- message: `Branch merged cleanly into combined state`,
85
- issueNumber: branch.issueNumber,
86
- },
87
- ],
88
- });
89
- }
90
- else {
91
- // Get conflicting files
92
- const conflictResult = git(["diff", "--name-only", "--diff-filter=U"], repoRoot);
93
- const conflictFiles = conflictResult.stdout
94
- ? conflictResult.stdout.split("\n")
95
- : [];
96
- mergeAttempts.push({
97
- issueNumber: branch.issueNumber,
98
- branch: branch.branch,
99
- success: false,
100
- conflictFiles,
101
- error: mergeResult.stderr,
102
- });
103
- branchResults.push({
104
- issueNumber: branch.issueNumber,
105
- verdict: "FAIL",
106
- findings: [
107
- {
108
- check: "combined-branch-test",
109
- severity: "error",
110
- message: `Merge conflict with ${conflictFiles.length} file(s): ${conflictFiles.join(", ")}`,
111
- issueNumber: branch.issueNumber,
112
- },
113
- ],
115
+ runChecks(branches, repoRoot, tempBranch, pm, branchResults, batchFindings, installState);
116
+ }
117
+ finally {
118
+ // Restore original branch and delete temp branch
119
+ const restoreBranch = originalBranch.ok ? originalBranch.stdout : "main";
120
+ git(["checkout", restoreBranch], repoRoot);
121
+ git(["branch", "-D", tempBranch], repoRoot);
122
+ // We installed the combined state's dependency tree into the caller's
123
+ // node_modules; put it back so the restored branch is not left running
124
+ // against another branch's dependencies.
125
+ //
126
+ // Frozen again, for the same reason as the forward install: a plain
127
+ // `npm install` normalizes and rewrites the lockfile, which would leave
128
+ // the user's restored branch with a dirty working tree that this check
129
+ // never asked them for.
130
+ if (installState.installedCombinedDeps) {
131
+ const restoreInstall = runPackageManagerCommand(pmConfig.ciInstall, repoRoot, INSTALL_TIMEOUT_MS);
132
+ if (!restoreInstall.ok) {
133
+ batchFindings.push({
134
+ check: "combined-branch-test",
135
+ severity: "warning",
136
+ message: `Dependencies were reinstalled for the combined state but could not be restored for \`${restoreBranch}\`. Run \`${pmConfig.ciInstall}\` to resync node_modules: ${resolveFailureReason(restoreInstall)}`,
114
137
  });
115
- // Abort the failed merge and continue
116
- git(["merge", "--abort"], repoRoot);
117
138
  }
118
139
  }
119
- // If any merges failed, skip tests but report what we have
120
- const failedMerges = mergeAttempts.filter((m) => !m.success);
121
- if (failedMerges.length > 0) {
122
- batchFindings.push({
123
- check: "combined-branch-test",
124
- severity: "error",
125
- message: `${failedMerges.length}/${branches.length} branches had merge conflicts skipping test/build`,
140
+ }
141
+ // Built after cleanup so findings pushed during cleanup are reflected in the
142
+ // verdict rather than silently arriving too late to affect `passed`.
143
+ return buildResult(branchResults, batchFindings, startTime);
144
+ }
145
+ /**
146
+ * Assemble the combined state and run install/test/build against it.
147
+ *
148
+ * Mutates `branchResults`, `batchFindings`, and `installState` — the last so
149
+ * the caller's cleanup can restore node_modules even if this function throws.
150
+ */
151
+ function runChecks(branches, repoRoot, tempBranch, pm, branchResults, batchFindings, installState) {
152
+ const mergeAttempts = [];
153
+ // Fetch latest from remote
154
+ git(["fetch", "origin"], repoRoot);
155
+ // Create temp branch from main
156
+ const createResult = git(["checkout", "-b", tempBranch, BASE_REF], repoRoot);
157
+ if (!createResult.ok) {
158
+ batchFindings.push({
159
+ check: "combined-branch-test",
160
+ severity: "error",
161
+ message: `Failed to create temp branch: ${createResult.stderr}`,
162
+ });
163
+ return;
164
+ }
165
+ // Merge each feature branch
166
+ for (const branch of branches) {
167
+ const mergeResult = git(["merge", "--no-ff", "--no-edit", getBranchRef(branch)], repoRoot);
168
+ if (mergeResult.ok) {
169
+ mergeAttempts.push({
170
+ issueNumber: branch.issueNumber,
171
+ branch: branch.branch,
172
+ success: true,
126
173
  });
127
- return buildResult(branchResults, batchFindings, startTime);
128
- }
129
- // Run npm test
130
- const testResult = npm(["test"], repoRoot);
131
- if (testResult.ok) {
132
- batchFindings.push({
133
- check: "combined-branch-test",
134
- severity: "info",
135
- message: "npm test passed on combined state",
174
+ branchResults.push({
175
+ issueNumber: branch.issueNumber,
176
+ verdict: "PASS",
177
+ findings: [
178
+ {
179
+ check: "combined-branch-test",
180
+ severity: "info",
181
+ message: `Branch merged cleanly into combined state`,
182
+ issueNumber: branch.issueNumber,
183
+ },
184
+ ],
136
185
  });
137
186
  }
138
187
  else {
139
- batchFindings.push({
140
- check: "combined-branch-test",
141
- severity: "error",
142
- message: `npm test failed on combined state: ${testResult.stderr.slice(0, 500)}`,
188
+ // Get conflicting files
189
+ const conflictResult = git(["diff", "--name-only", "--diff-filter=U"], repoRoot);
190
+ const conflictFiles = conflictResult.stdout
191
+ ? conflictResult.stdout.split("\n")
192
+ : [];
193
+ mergeAttempts.push({
194
+ issueNumber: branch.issueNumber,
195
+ branch: branch.branch,
196
+ success: false,
197
+ conflictFiles,
198
+ error: mergeResult.stderr,
143
199
  });
144
- }
145
- // Run npm run build
146
- const buildResult2 = npm(["run", "build"], repoRoot);
147
- if (buildResult2.ok) {
148
- batchFindings.push({
149
- check: "combined-branch-test",
150
- severity: "info",
151
- message: "npm run build passed on combined state",
200
+ branchResults.push({
201
+ issueNumber: branch.issueNumber,
202
+ verdict: "FAIL",
203
+ findings: [
204
+ {
205
+ check: "combined-branch-test",
206
+ severity: "error",
207
+ message: `Merge conflict with ${conflictFiles.length} file(s): ${conflictFiles.join(", ")}`,
208
+ issueNumber: branch.issueNumber,
209
+ },
210
+ ],
152
211
  });
212
+ // Abort the failed merge and continue
213
+ git(["merge", "--abort"], repoRoot);
153
214
  }
154
- else {
215
+ }
216
+ // If any merges failed, skip tests but report what we have
217
+ const failedMerges = mergeAttempts.filter((m) => !m.success);
218
+ if (failedMerges.length > 0) {
219
+ batchFindings.push({
220
+ check: "combined-branch-test",
221
+ severity: "error",
222
+ message: `${failedMerges.length}/${branches.length} branches had merge conflicts — skipping test/build`,
223
+ });
224
+ return;
225
+ }
226
+ // Resolved HERE, after the merges landed, not from the caller's pre-merge
227
+ // resolution: the commands below run against the combined tree, and a merged
228
+ // branch can move the yarn major (a `packageManager` bump, a new
229
+ // `.yarnrc.yml`). Installing the combined state with the pre-merge major's
230
+ // frozen-install flag would fail on an unknown option and report a false
231
+ // BLOCKED — the exact failure class #803 added this install to prevent (#871).
232
+ const pmConfig = resolvePackageManagerConfig(pm, repoRoot);
233
+ // Reinstall dependencies when the merged branches moved the lockfile (#803).
234
+ // Without this, test/build run against the node_modules of whatever branch
235
+ // the user happened to be on, so a batch that merely added a dependency fails
236
+ // with module-not-found errors — a false BLOCKED on a healthy stack.
237
+ //
238
+ // A frozen install is used rather than a plain one: it mirrors what CI does,
239
+ // it will not rewrite the lockfile (which would dirty the temp branch and
240
+ // break the checkout during cleanup), and it fails loudly on a lockfile that
241
+ // is inconsistent with package.json.
242
+ if (lockfileChanged(repoRoot, BASE_REF)) {
243
+ // Marked before the install runs, not after: a frozen install deletes
244
+ // node_modules up front, so even a failed or interrupted one leaves the
245
+ // caller's tree needing a restore.
246
+ installState.installedCombinedDeps = true;
247
+ const installResult = runPackageManagerCommand(pmConfig.ciInstall, repoRoot, INSTALL_TIMEOUT_MS);
248
+ if (!installResult.ok) {
249
+ // Surface install failures on their own terms rather than letting them
250
+ // resurface downstream as an unexplained test failure.
155
251
  batchFindings.push({
156
252
  check: "combined-branch-test",
157
253
  severity: "error",
158
- message: `npm run build failed on combined state: ${buildResult2.stderr.slice(0, 500)}`,
254
+ message: `Dependency install failed on combined state (\`${pmConfig.ciInstall}\`) — skipping test/build. The merged lockfile may be inconsistent with package.json: ${resolveFailureReason(installResult)}`,
159
255
  });
256
+ return;
160
257
  }
161
- return buildResult(branchResults, batchFindings, startTime);
162
- }
163
- finally {
164
- // Clean up: restore original branch and delete temp branch
165
- const restoreBranch = originalBranch.ok ? originalBranch.stdout : "main";
166
- git(["checkout", restoreBranch], repoRoot);
167
- git(["branch", "-D", tempBranch], repoRoot);
258
+ batchFindings.push({
259
+ check: "combined-branch-test",
260
+ severity: "info",
261
+ message: `Lockfile changed in the combined state reinstalled dependencies with \`${pmConfig.ciInstall}\``,
262
+ });
168
263
  }
264
+ // Run the test suite
265
+ const testCommand = `${pmConfig.run} test`;
266
+ const testResult = runPackageManagerCommand(testCommand, repoRoot, TEST_BUILD_TIMEOUT_MS);
267
+ batchFindings.push(testResult.ok
268
+ ? {
269
+ check: "combined-branch-test",
270
+ severity: "info",
271
+ message: `\`${testCommand}\` passed on combined state`,
272
+ }
273
+ : {
274
+ check: "combined-branch-test",
275
+ severity: "error",
276
+ message: `\`${testCommand}\` failed on combined state: ${resolveFailureReason(testResult)}`,
277
+ });
278
+ // Run the build
279
+ const buildCommand = `${pmConfig.run} build`;
280
+ const buildOutcome = runPackageManagerCommand(buildCommand, repoRoot, TEST_BUILD_TIMEOUT_MS);
281
+ batchFindings.push(buildOutcome.ok
282
+ ? {
283
+ check: "combined-branch-test",
284
+ severity: "info",
285
+ message: `\`${buildCommand}\` passed on combined state`,
286
+ }
287
+ : {
288
+ check: "combined-branch-test",
289
+ severity: "error",
290
+ message: `\`${buildCommand}\` failed on combined state: ${resolveFailureReason(buildOutcome)}`,
291
+ });
169
292
  }
170
293
  export function buildResult(branchResults, batchFindings, startTime) {
171
294
  const hasErrors = batchFindings.some((f) => f.severity === "error");