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
@@ -37,6 +37,7 @@ export interface PackageManagerConfig {
37
37
  exec: string;
38
38
  install: string;
39
39
  installSilent: string;
40
+ ciInstall: string;
40
41
  addPkg: string;
41
42
  removePkg: string;
42
43
  updatePkg: string;
@@ -56,12 +57,120 @@ export declare function detectPackageManager(): Promise<PackageManager | null>;
56
57
  /**
57
58
  * Synchronous version of detectPackageManager for use in startup code.
58
59
  * Only checks JS lockfiles (not Python) since sequant is a Node.js tool.
60
+ *
61
+ * @param root Directory to look for lockfiles in. Defaults to the process cwd,
62
+ * which is what startup code wants; callers that operate on a specific
63
+ * repository (e.g. merge-check) must pass that repo's root instead.
64
+ */
65
+ export declare function detectPackageManagerSync(root?: string): PackageManager;
66
+ /**
67
+ * Resolve the package manager for a directory, preferring a declared value and
68
+ * falling back to live lockfile detection.
69
+ *
70
+ * The declared value is a *snapshot* — the worktree manifest records whatever
71
+ * `sequant init` saw, so it is absent on manifest-less or pre-init trees. The
72
+ * old spelling at those call sites was `(declared as keyof typeof PM_CONFIG)
73
+ * || "npm"`, which assumed npm whenever the snapshot was missing while the
74
+ * shell provisioning path (`templates/scripts/new-feature.sh`, PM-aware since
75
+ * #847) detected live from the lockfile. On a pnpm/yarn/bun project without a
76
+ * manifest the two paths therefore disagreed about the project's package
77
+ * manager — the dual-producer drift class of #833. Detecting here makes the
78
+ * TypeScript path agree with the shell path by construction (#870).
79
+ *
80
+ * The own-property membership test also retires that unchecked cast: a
81
+ * declared value outside `PackageManager` (a hand-edited manifest, a future
82
+ * `"npm@10"` spelling) used to index `PM_CONFIG` to `undefined` and throw on
83
+ * the next property access. Such a value now routes to detection instead.
84
+ * Python managers (`pip`/`poetry`/`uv`) are `PM_CONFIG` keys, so a declared
85
+ * Python manager still wins over the JS-only detector.
86
+ *
87
+ * The test is `hasOwnProperty`, deliberately not `in`: `in` walks the
88
+ * prototype chain, so `"toString"`, `"constructor"`, and `"__proto__"` would
89
+ * all pass it and be returned as if they named a package manager. Indexing
90
+ * `PM_CONFIG` with one yields an inherited function (or `Object.prototype`)
91
+ * whose `ciInstall` is `undefined`, crashing the caller on `.split(" ")` —
92
+ * reintroducing the exact failure this guard exists to prevent.
93
+ *
94
+ * @param declared Package manager recorded in the manifest, if any
95
+ * @param root Directory whose lockfiles decide the fallback
59
96
  */
60
- export declare function detectPackageManagerSync(): PackageManager;
97
+ export declare function resolvePackageManager(declared: string | undefined, root: string): PackageManager;
61
98
  /**
62
99
  * Get package manager command configuration
63
100
  */
64
101
  export declare function getPackageManagerCommands(pm: PackageManager): PackageManagerConfig;
102
+ /**
103
+ * Frozen install understood by Yarn 1 (classic).
104
+ *
105
+ * Yarn 2+ (berry) renamed this flag to `--immutable`, which is what
106
+ * `PM_CONFIG.yarn.ciInstall` carries. Both yarn spellings therefore exist, and
107
+ * both must appear in `new-feature.sh`'s `pm_ci_install()` table — the drift
108
+ * guard in `__tests__/new-feature-frozen-install.integration.test.ts` asserts
109
+ * exactly that, which is why this is a named export rather than a literal
110
+ * buried in `resolvePackageManagerConfig` (#871).
111
+ */
112
+ export declare const YARN_CLASSIC_CI_INSTALL = "yarn install --frozen-lockfile";
113
+ /**
114
+ * Detect whether a directory is a Yarn 1 (classic) or Yarn 2+ (berry) project.
115
+ *
116
+ * Yarn 1 and berry both use `yarn.lock`, so `detectPackageManagerSync` cannot
117
+ * tell them apart — it answers "yarn", and this answers "which yarn". They are
118
+ * separate axes on purpose: adding a `yarn1` member to `PackageManager` would
119
+ * leak a synthetic name into settings (`pmRun`), doctor output, and templates
120
+ * for what is, today, a one-flag difference.
121
+ *
122
+ * Signals, in order of authority. The ordering follows a single rule: **flag
123
+ * acceptance is decided by the yarn binary that runs, not by the lockfile it
124
+ * reads.** So a Corepack pin outranks the lockfile header — a yarn-1 lockfile
125
+ * under `packageManager: "yarn@4"` still gets berry's `--immutable`, because
126
+ * yarn 4 is what will execute and yarn 4 rejects `--frozen-lockfile`.
127
+ *
128
+ * 1. `packageManager: "yarn@<major>"` in `package.json` — Corepack pins the
129
+ * binary.
130
+ * 2. `.yarnrc.yml` — a berry-only config file; yarn 1 reads `.yarnrc`.
131
+ * 3. `yarn.lock` header — yarn 1 writes `# yarn lockfile v1`; berry writes
132
+ * `__metadata:` instead.
133
+ * 4. Nothing recognizable → berry, the pre-#871 assumption. Staying with it
134
+ * keeps a contentless or unreadable `yarn.lock` behaving exactly as it did
135
+ * before this function existed.
136
+ *
137
+ * Mirrored by `detect_yarn_major` in `templates/scripts/new-feature.sh` (#871).
138
+ *
139
+ * @internal Exported for testing — production code should go through
140
+ * {@link resolvePackageManagerConfig}, whose whole job is to apply this answer.
141
+ * The export exists so the cross-path agreement test can run this and the shell
142
+ * mirror against the same fixtures, which is the gate keeping the two in step.
143
+ *
144
+ * @param root Directory to inspect. Defaults to the process cwd.
145
+ * @returns 1 for Yarn 1 (classic), 2 for Yarn 2+ (berry)
146
+ */
147
+ export declare function detectYarnMajor(root?: string): 1 | 2;
148
+ /**
149
+ * Package manager commands for `pm`, with directory-dependent commands resolved
150
+ * against `root`.
151
+ *
152
+ * A drop-in replacement for `PM_CONFIG[pm]` at every call site that knows which
153
+ * project directory it is acting on. `PM_CONFIG.yarn` is the berry baseline, so
154
+ * for a Yarn 1 project this substitutes every field whose berry spelling is
155
+ * wrong on classic — `ciInstall`, `exec`, and `updatePkg` (see
156
+ * {@link YARN_CLASSIC_OVERRIDES}). Every other package manager, and every
157
+ * major-agnostic yarn field, is returned untouched.
158
+ *
159
+ * Returning the whole config rather than one resolved string is what makes that
160
+ * cheap: call sites that thread a `PackageManagerConfig` onward — merge-check's
161
+ * combined-branch test reads `ciInstall` for three commands and two user-facing
162
+ * messages — stay correct with a single-line change, and each newly discovered
163
+ * classic/berry split is one entry in the overrides table rather than a new
164
+ * call-site edit.
165
+ *
166
+ * Resolve against the tree you are about to act on, not against wherever the
167
+ * process started: `root` decides the answer, and in merge-check the combined
168
+ * (post-merge) state can disagree with the pre-merge one.
169
+ *
170
+ * @param pm Package manager identity, e.g. from `detectPackageManagerSync`
171
+ * @param root Directory whose files decide the directory-dependent commands
172
+ */
173
+ export declare function resolvePackageManagerConfig(pm: PackageManager, root: string): PackageManagerConfig;
65
174
  export interface StackConfig {
66
175
  name: string;
67
176
  displayName: string;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Stack detection and configuration
3
3
  */
4
- import { existsSync } from "fs";
4
+ import { closeSync, existsSync, openSync, readFileSync, readSync } from "fs";
5
5
  import { readdir } from "fs/promises";
6
+ import { join } from "path";
6
7
  import { fileExists, readFile } from "./fs.js";
7
8
  /**
8
9
  * Directories to skip during multi-stack detection
@@ -30,6 +31,7 @@ export const PM_CONFIG = {
30
31
  exec: "npx",
31
32
  install: "npm install",
32
33
  installSilent: "npm install --silent",
34
+ ciInstall: "npm ci",
33
35
  addPkg: "npm install",
34
36
  removePkg: "npm uninstall",
35
37
  updatePkg: "npm update",
@@ -39,34 +41,62 @@ export const PM_CONFIG = {
39
41
  exec: "bunx",
40
42
  install: "bun install",
41
43
  installSilent: "bun install --silent",
44
+ ciInstall: "bun install --frozen-lockfile",
42
45
  addPkg: "bun add",
43
46
  removePkg: "bun remove",
44
47
  updatePkg: "bun update",
45
48
  },
49
+ // Yarn 1 (classic) and Yarn 2+ (berry) are different CLIs sharing one
50
+ // lockfile name, so no single table can be right for both. This entry is the
51
+ // BERRY baseline; `resolvePackageManagerConfig(pm, root)` swaps in the classic
52
+ // spelling for the fields that differ. Read yarn commands through that
53
+ // resolver, never off this table, at any site that knows which project
54
+ // directory it is acting on (#871).
55
+ //
56
+ // Fields with no major-specific difference — `run`, `install`, `addPkg`,
57
+ // `removePkg` — are shared, so the resolver leaves them alone.
46
58
  yarn: {
47
59
  run: "yarn",
60
+ // Berry-only: `dlx` does not exist in Yarn 1. Resolver → `npx` for classic.
48
61
  exec: "yarn dlx",
49
62
  install: "yarn install",
63
+ // Left at the classic spelling deliberately. Berry's `yarn install` takes no
64
+ // `--silent`, but this field has NO consumer anywhere in the codebase, and
65
+ // guessing berry's quiet spelling without a yarn binary to check against
66
+ // would trade a dead-config wart for a real one — the same call
67
+ // new-feature.sh's `pm_quiet_flag` already made for pnpm/bun. Known
68
+ // remaining hybrid, and the one field the resolver does not fix.
50
69
  installSilent: "yarn install --silent",
70
+ // Berry-only: Yarn 1 rejects `--immutable` and wants `--frozen-lockfile`.
71
+ // Resolver → YARN_CLASSIC_CI_INSTALL for classic.
72
+ ciInstall: "yarn install --immutable",
51
73
  addPkg: "yarn add",
52
74
  removePkg: "yarn remove",
53
- updatePkg: "yarn upgrade",
75
+ // Berry renamed `yarn upgrade` to `yarn up`. This was the classic spelling
76
+ // until #871's follow-up, which made the whole entry berry-consistent —
77
+ // meaning the upgrade hint in version-check.ts was wrong for berry users.
78
+ // Resolver → `yarn upgrade` for classic.
79
+ updatePkg: "yarn up",
54
80
  },
55
81
  pnpm: {
56
82
  run: "pnpm run",
57
83
  exec: "pnpm dlx",
58
84
  install: "pnpm install",
59
85
  installSilent: "pnpm install --silent",
86
+ ciInstall: "pnpm install --frozen-lockfile",
60
87
  addPkg: "pnpm add",
61
88
  removePkg: "pnpm remove",
62
89
  updatePkg: "pnpm update",
63
90
  },
64
- // Python package managers
91
+ // Python package managers. None of these has a distinct "frozen lockfile"
92
+ // install mode the way npm/pnpm/yarn/bun do (poetry install already honors
93
+ // poetry.lock), so ciInstall mirrors installSilent rather than inventing a flag.
65
94
  pip: {
66
95
  run: "python -m",
67
96
  exec: "python -m",
68
97
  install: "pip install",
69
98
  installSilent: "pip install -q",
99
+ ciInstall: "pip install -q",
70
100
  addPkg: "pip install",
71
101
  removePkg: "pip uninstall",
72
102
  updatePkg: "pip install --upgrade",
@@ -76,6 +106,7 @@ export const PM_CONFIG = {
76
106
  exec: "poetry run",
77
107
  install: "poetry install",
78
108
  installSilent: "poetry install -q",
109
+ ciInstall: "poetry install -q",
79
110
  addPkg: "poetry add",
80
111
  removePkg: "poetry remove",
81
112
  updatePkg: "poetry update",
@@ -85,6 +116,7 @@ export const PM_CONFIG = {
85
116
  exec: "uvx",
86
117
  install: "uv pip install",
87
118
  installSilent: "uv pip install -q",
119
+ ciInstall: "uv pip install -q",
88
120
  addPkg: "uv pip install",
89
121
  removePkg: "uv pip uninstall",
90
122
  updatePkg: "uv pip install --upgrade",
@@ -143,21 +175,228 @@ export async function detectPackageManager() {
143
175
  /**
144
176
  * Synchronous version of detectPackageManager for use in startup code.
145
177
  * Only checks JS lockfiles (not Python) since sequant is a Node.js tool.
178
+ *
179
+ * @param root Directory to look for lockfiles in. Defaults to the process cwd,
180
+ * which is what startup code wants; callers that operate on a specific
181
+ * repository (e.g. merge-check) must pass that repo's root instead.
146
182
  */
147
- export function detectPackageManagerSync() {
183
+ export function detectPackageManagerSync(root = process.cwd()) {
148
184
  for (const { file, pm } of LOCKFILE_PRIORITY) {
149
- if (existsSync(file)) {
185
+ if (existsSync(join(root, file))) {
150
186
  return pm;
151
187
  }
152
188
  }
153
189
  return "npm";
154
190
  }
191
+ /**
192
+ * Resolve the package manager for a directory, preferring a declared value and
193
+ * falling back to live lockfile detection.
194
+ *
195
+ * The declared value is a *snapshot* — the worktree manifest records whatever
196
+ * `sequant init` saw, so it is absent on manifest-less or pre-init trees. The
197
+ * old spelling at those call sites was `(declared as keyof typeof PM_CONFIG)
198
+ * || "npm"`, which assumed npm whenever the snapshot was missing while the
199
+ * shell provisioning path (`templates/scripts/new-feature.sh`, PM-aware since
200
+ * #847) detected live from the lockfile. On a pnpm/yarn/bun project without a
201
+ * manifest the two paths therefore disagreed about the project's package
202
+ * manager — the dual-producer drift class of #833. Detecting here makes the
203
+ * TypeScript path agree with the shell path by construction (#870).
204
+ *
205
+ * The own-property membership test also retires that unchecked cast: a
206
+ * declared value outside `PackageManager` (a hand-edited manifest, a future
207
+ * `"npm@10"` spelling) used to index `PM_CONFIG` to `undefined` and throw on
208
+ * the next property access. Such a value now routes to detection instead.
209
+ * Python managers (`pip`/`poetry`/`uv`) are `PM_CONFIG` keys, so a declared
210
+ * Python manager still wins over the JS-only detector.
211
+ *
212
+ * The test is `hasOwnProperty`, deliberately not `in`: `in` walks the
213
+ * prototype chain, so `"toString"`, `"constructor"`, and `"__proto__"` would
214
+ * all pass it and be returned as if they named a package manager. Indexing
215
+ * `PM_CONFIG` with one yields an inherited function (or `Object.prototype`)
216
+ * whose `ciInstall` is `undefined`, crashing the caller on `.split(" ")` —
217
+ * reintroducing the exact failure this guard exists to prevent.
218
+ *
219
+ * @param declared Package manager recorded in the manifest, if any
220
+ * @param root Directory whose lockfiles decide the fallback
221
+ */
222
+ export function resolvePackageManager(declared, root) {
223
+ if (declared && Object.prototype.hasOwnProperty.call(PM_CONFIG, declared)) {
224
+ return declared;
225
+ }
226
+ return detectPackageManagerSync(root);
227
+ }
155
228
  /**
156
229
  * Get package manager command configuration
157
230
  */
158
231
  export function getPackageManagerCommands(pm) {
159
232
  return PM_CONFIG[pm];
160
233
  }
234
+ /**
235
+ * Frozen install understood by Yarn 1 (classic).
236
+ *
237
+ * Yarn 2+ (berry) renamed this flag to `--immutable`, which is what
238
+ * `PM_CONFIG.yarn.ciInstall` carries. Both yarn spellings therefore exist, and
239
+ * both must appear in `new-feature.sh`'s `pm_ci_install()` table — the drift
240
+ * guard in `__tests__/new-feature-frozen-install.integration.test.ts` asserts
241
+ * exactly that, which is why this is a named export rather than a literal
242
+ * buried in `resolvePackageManagerConfig` (#871).
243
+ */
244
+ export const YARN_CLASSIC_CI_INSTALL = "yarn install --frozen-lockfile";
245
+ /**
246
+ * Yarn 1 (classic) spellings for every `PM_CONFIG.yarn` field whose berry value
247
+ * is wrong on classic. Applied by {@link resolvePackageManagerConfig}.
248
+ *
249
+ * `exec` is `npx` rather than a `yarn` subcommand because Yarn 1 has no
250
+ * `dlx` equivalent at all — npm ships with node, so `npx` is the run-a-binary
251
+ * -without-installing story a Yarn 1 project actually has.
252
+ *
253
+ * `installSilent` is deliberately absent — see the note on `PM_CONFIG.yarn`.
254
+ */
255
+ const YARN_CLASSIC_OVERRIDES = {
256
+ ciInstall: YARN_CLASSIC_CI_INSTALL,
257
+ exec: "npx",
258
+ updatePkg: "yarn upgrade",
259
+ };
260
+ /**
261
+ * Bytes of `yarn.lock` scanned for its version header.
262
+ *
263
+ * Matches the `head -c` byte count in `new-feature.sh`'s `detect_yarn_major`.
264
+ * Keeping the scanned region identical is what makes the two implementations
265
+ * agree by construction rather than by inspection: a whole-file `grep` in the
266
+ * shell against a prefix-only read in TypeScript would silently diverge on a
267
+ * lockfile that happens to mention the header string further down.
268
+ */
269
+ const YARN_LOCK_HEADER_BYTES = 1024;
270
+ /**
271
+ * Read the `packageManager` pin's yarn major from `package.json`, if declared.
272
+ *
273
+ * Deliberately a regex rather than `JSON.parse`: the shell path cannot parse
274
+ * JSON without adding a `jq` dependency, so both paths match on text. A
275
+ * non-numeric pin (`yarn@stable`, `yarn@berry`) does not match and falls
276
+ * through to the next signal.
277
+ *
278
+ * Matching on text does NOT make the two paths agree for free — `\s*` here
279
+ * spans newlines and `.match` returns the first hit, neither of which a
280
+ * line-oriented `sed`/`grep` gives you. `detect_yarn_major` in
281
+ * `new-feature.sh` collapses newlines and takes the first match specifically
282
+ * to line up with this; see its comment for the two inputs that diverged
283
+ * before it did.
284
+ *
285
+ * @returns the declared major, or null when nothing is declared
286
+ */
287
+ function readDeclaredYarnMajor(root) {
288
+ let raw;
289
+ try {
290
+ raw = readFileSync(join(root, "package.json"), "utf8");
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ const match = raw.match(/"packageManager"\s*:\s*"yarn@v?(\d+)/);
296
+ if (!match) {
297
+ return null;
298
+ }
299
+ // Every major above 1 is berry and shares berry's CLI surface, so the return
300
+ // type is the *behavioral* split (classic vs. berry), not the literal major.
301
+ return match[1] === "1" ? 1 : 2;
302
+ }
303
+ /** Read the first {@link YARN_LOCK_HEADER_BYTES} of `yarn.lock` ("" if absent). */
304
+ function readYarnLockHeader(root) {
305
+ let fd;
306
+ try {
307
+ fd = openSync(join(root, "yarn.lock"), "r");
308
+ }
309
+ catch {
310
+ return "";
311
+ }
312
+ try {
313
+ const buffer = Buffer.alloc(YARN_LOCK_HEADER_BYTES);
314
+ const bytesRead = readSync(fd, buffer, 0, YARN_LOCK_HEADER_BYTES, 0);
315
+ return buffer.toString("utf8", 0, bytesRead);
316
+ }
317
+ finally {
318
+ closeSync(fd);
319
+ }
320
+ }
321
+ /**
322
+ * Detect whether a directory is a Yarn 1 (classic) or Yarn 2+ (berry) project.
323
+ *
324
+ * Yarn 1 and berry both use `yarn.lock`, so `detectPackageManagerSync` cannot
325
+ * tell them apart — it answers "yarn", and this answers "which yarn". They are
326
+ * separate axes on purpose: adding a `yarn1` member to `PackageManager` would
327
+ * leak a synthetic name into settings (`pmRun`), doctor output, and templates
328
+ * for what is, today, a one-flag difference.
329
+ *
330
+ * Signals, in order of authority. The ordering follows a single rule: **flag
331
+ * acceptance is decided by the yarn binary that runs, not by the lockfile it
332
+ * reads.** So a Corepack pin outranks the lockfile header — a yarn-1 lockfile
333
+ * under `packageManager: "yarn@4"` still gets berry's `--immutable`, because
334
+ * yarn 4 is what will execute and yarn 4 rejects `--frozen-lockfile`.
335
+ *
336
+ * 1. `packageManager: "yarn@<major>"` in `package.json` — Corepack pins the
337
+ * binary.
338
+ * 2. `.yarnrc.yml` — a berry-only config file; yarn 1 reads `.yarnrc`.
339
+ * 3. `yarn.lock` header — yarn 1 writes `# yarn lockfile v1`; berry writes
340
+ * `__metadata:` instead.
341
+ * 4. Nothing recognizable → berry, the pre-#871 assumption. Staying with it
342
+ * keeps a contentless or unreadable `yarn.lock` behaving exactly as it did
343
+ * before this function existed.
344
+ *
345
+ * Mirrored by `detect_yarn_major` in `templates/scripts/new-feature.sh` (#871).
346
+ *
347
+ * @internal Exported for testing — production code should go through
348
+ * {@link resolvePackageManagerConfig}, whose whole job is to apply this answer.
349
+ * The export exists so the cross-path agreement test can run this and the shell
350
+ * mirror against the same fixtures, which is the gate keeping the two in step.
351
+ *
352
+ * @param root Directory to inspect. Defaults to the process cwd.
353
+ * @returns 1 for Yarn 1 (classic), 2 for Yarn 2+ (berry)
354
+ */
355
+ export function detectYarnMajor(root = process.cwd()) {
356
+ const declared = readDeclaredYarnMajor(root);
357
+ if (declared !== null) {
358
+ return declared;
359
+ }
360
+ if (existsSync(join(root, ".yarnrc.yml"))) {
361
+ return 2;
362
+ }
363
+ if (readYarnLockHeader(root).includes("yarn lockfile v1")) {
364
+ return 1;
365
+ }
366
+ return 2;
367
+ }
368
+ /**
369
+ * Package manager commands for `pm`, with directory-dependent commands resolved
370
+ * against `root`.
371
+ *
372
+ * A drop-in replacement for `PM_CONFIG[pm]` at every call site that knows which
373
+ * project directory it is acting on. `PM_CONFIG.yarn` is the berry baseline, so
374
+ * for a Yarn 1 project this substitutes every field whose berry spelling is
375
+ * wrong on classic — `ciInstall`, `exec`, and `updatePkg` (see
376
+ * {@link YARN_CLASSIC_OVERRIDES}). Every other package manager, and every
377
+ * major-agnostic yarn field, is returned untouched.
378
+ *
379
+ * Returning the whole config rather than one resolved string is what makes that
380
+ * cheap: call sites that thread a `PackageManagerConfig` onward — merge-check's
381
+ * combined-branch test reads `ciInstall` for three commands and two user-facing
382
+ * messages — stay correct with a single-line change, and each newly discovered
383
+ * classic/berry split is one entry in the overrides table rather than a new
384
+ * call-site edit.
385
+ *
386
+ * Resolve against the tree you are about to act on, not against wherever the
387
+ * process started: `root` decides the answer, and in merge-check the combined
388
+ * (post-merge) state can disagree with the pre-merge one.
389
+ *
390
+ * @param pm Package manager identity, e.g. from `detectPackageManagerSync`
391
+ * @param root Directory whose files decide the directory-dependent commands
392
+ */
393
+ export function resolvePackageManagerConfig(pm, root) {
394
+ const config = PM_CONFIG[pm];
395
+ if (pm !== "yarn" || detectYarnMajor(root) !== 1) {
396
+ return config;
397
+ }
398
+ return { ...config, ...YARN_CLASSIC_OVERRIDES };
399
+ }
161
400
  export const STACKS = {
162
401
  nextjs: {
163
402
  name: "nextjs",
@@ -1,7 +1,47 @@
1
1
  /**
2
2
  * Template management - copy and process templates
3
3
  */
4
+ /**
5
+ * How well a candidate path matches "the templates root of *this* package".
6
+ *
7
+ * `package-root` outranks `exists` because the two offsets are not mutually
8
+ * exclusive: from a source tree at `<repo>/src/lib`, the *compiled* offset
9
+ * resolves to `<repo>/../templates` — a directory one level **above** the repo.
10
+ * A bare existence probe would happily bind to an unrelated `templates/` that
11
+ * happens to sit beside the checkout. Requiring the candidate's parent to be
12
+ * the sequant package root removes that ambiguity without walking the tree.
13
+ */
14
+ export type TemplatesCandidateRank = "package-root" | "exists" | "missing";
15
+ /**
16
+ * Resolve the templates root relative to a module directory, given a ranking
17
+ * function.
18
+ *
19
+ * Split out from `getTemplatesDir` purely so both layouts are testable: a test
20
+ * cannot relocate `import.meta.url`, so without injection the "works from both
21
+ * layouts" assertion could only ever exercise whichever layout the test runner
22
+ * happens to use.
23
+ *
24
+ * Resolution order: the first `package-root` candidate, else the first that
25
+ * merely `exists` (keeps any layout that works today working, even one whose
26
+ * package root is not where we expect), else the **compiled** offset — so the
27
+ * caller's error message names the canonical expected location rather than a
28
+ * source-tree guess.
29
+ */
30
+ export declare function resolveTemplatesDirFrom(baseDir: string, rank: (candidate: string) => TemplatesCandidateRank): string;
4
31
  export declare function getTemplatesDir(): string;
32
+ /**
33
+ * Resolve the templates root and fail loudly when it does not exist.
34
+ *
35
+ * A missing templates *root* means the install is broken: every `copyDir` call
36
+ * below would hit `copyDir`'s per-directory ENOENT skip, no-op, and let the
37
+ * caller print a success message over an empty tree (#822). That skip is
38
+ * deliberate for individual subdirectories — a stack may legitimately ship
39
+ * without `memory/` — but it must not absorb the whole source tree.
40
+ *
41
+ * Throws rather than printing so the lib layer stays free of presentation;
42
+ * commands catch and render.
43
+ */
44
+ export declare function assertTemplatesDirExists(): Promise<string>;
5
45
  /**
6
46
  * Process template variables in content
7
47
  */
@@ -52,6 +92,25 @@ export interface TemplateChange {
52
92
  /** Unified-ish diff (installed → rendered), only set for `modified` */
53
93
  diff?: string;
54
94
  }
95
+ /**
96
+ * Map a bundled template path to the location `copyTemplates` installs it to,
97
+ * or `null` for templates the copy/diff cycle does not manage.
98
+ *
99
+ * This mirrors the routing in `copyTemplates` (write time) so
100
+ * `computeTemplateChanges` (diff time) can never report drift on a file init
101
+ * deliberately places elsewhere — `.claude/mcp.json`, `.claude/relay/*`, and
102
+ * `.claude/scripts/*` read as phantom "new" files on every fresh install when
103
+ * the diff assumed a flat `.claude/<relpath>` layout. #708 gives the same
104
+ * guarantee for the template *variables*; this covers the *destinations*.
105
+ *
106
+ * - `templates/mcp.json` → `null`: the project `.mcp.json` is generated and
107
+ * version-pinned by `mcp-config.ts` (`syncSequantMcpPin`), never copied.
108
+ * - `templates/relay/` → `null`: not installed by `copyTemplates`.
109
+ * - `templates/scripts/` → `scripts/dev/`: symlinked on POSIX, copied on
110
+ * Windows / `--no-symlinks`.
111
+ * - everything else → `.claude/<relpath>`.
112
+ */
113
+ export declare function templateDestination(templatePath: string): string | null;
55
114
  /**
56
115
  * Compare bundled template content against what's installed under `.claude/`.
57
116
  *
@@ -83,6 +142,15 @@ export interface CopyTemplatesOptions {
83
142
  noSymlinks?: boolean;
84
143
  /** Force replacement of existing files/symlinks */
85
144
  force?: boolean;
145
+ /**
146
+ * Opt in to overwriting in-place customizations (files in `CUSTOMIZABLE_FILES`,
147
+ * e.g. the constitution) that already exist and differ from the rendered
148
+ * template. Deliberately separate from `force`: `force` refreshes the managed
149
+ * skills/agents/hooks trees, but that always-on tree overwrite must NOT imply
150
+ * consent to clobber user-owned files. Only an explicit user `--force` sets
151
+ * this. A missing or identical customizable file is written regardless (#814).
152
+ */
153
+ overwriteCustomizable?: boolean;
86
154
  /** Additional stacks to include in constitution notes (for multi-stack projects) */
87
155
  additionalStacks?: string[];
88
156
  }
@@ -102,4 +170,10 @@ export declare function symlinkDir(srcDir: string, destDir: string, options?: {
102
170
  export declare function copyTemplates(stack: string, tokens?: Record<string, string>, options?: CopyTemplatesOptions): Promise<{
103
171
  scriptsSymlinked: boolean;
104
172
  symlinkResults?: SymlinkResult[];
173
+ /**
174
+ * Customizable files that already existed, differed from the rendered
175
+ * template, and were left untouched because `overwriteCustomizable` was not
176
+ * set. Normalized to forward slashes so callers can report them verbatim.
177
+ */
178
+ preservedCustomizable: string[];
105
179
  }>;