sequant 2.8.0 → 2.10.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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +23 -4
  4. package/dist/bin/cli.js +99 -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 +58 -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 +38 -10
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +405 -67
  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 +250 -344
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +10 -7
  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 +4 -2
  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/reflect/SKILL.md +27 -13
  28. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  29. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  30. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +11 -3
  31. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  32. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  33. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  34. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  35. package/dist/src/commands/assess-render.d.ts +23 -0
  36. package/dist/src/commands/assess-render.js +60 -0
  37. package/dist/src/commands/doctor.js +8 -13
  38. package/dist/src/commands/init.js +13 -1
  39. package/dist/src/commands/locks.js +2 -0
  40. package/dist/src/commands/logs.js +20 -6
  41. package/dist/src/commands/merge.js +154 -2
  42. package/dist/src/commands/ready-tui-adapter.js +6 -1
  43. package/dist/src/commands/ready.d.ts +25 -3
  44. package/dist/src/commands/ready.js +39 -13
  45. package/dist/src/commands/resume.d.ts +113 -0
  46. package/dist/src/commands/resume.js +193 -0
  47. package/dist/src/commands/run-display.d.ts +20 -0
  48. package/dist/src/commands/run-display.js +117 -5
  49. package/dist/src/commands/run-flags.d.ts +42 -1
  50. package/dist/src/commands/run-flags.js +53 -1
  51. package/dist/src/commands/run-progress.js +26 -1
  52. package/dist/src/commands/run.js +11 -10
  53. package/dist/src/commands/state.js +7 -0
  54. package/dist/src/commands/stats.js +47 -0
  55. package/dist/src/commands/status.d.ts +9 -0
  56. package/dist/src/commands/status.js +24 -1
  57. package/dist/src/commands/sync.js +90 -19
  58. package/dist/src/commands/update.js +28 -5
  59. package/dist/src/lib/ac-parser.d.ts +14 -0
  60. package/dist/src/lib/ac-parser.js +99 -5
  61. package/dist/src/lib/assess/index.d.ts +10 -0
  62. package/dist/src/lib/assess/index.js +9 -0
  63. package/dist/src/lib/assess/renderer.d.ts +64 -0
  64. package/dist/src/lib/assess/renderer.js +481 -0
  65. package/dist/src/lib/assess/types.d.ts +224 -0
  66. package/dist/src/lib/assess/types.js +241 -0
  67. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  68. package/dist/src/lib/assess-collision-detect.js +68 -4
  69. package/dist/src/lib/ci/outputs.d.ts +15 -2
  70. package/dist/src/lib/ci/outputs.js +17 -5
  71. package/dist/src/lib/cli-flags.d.ts +33 -0
  72. package/dist/src/lib/cli-flags.js +46 -0
  73. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  74. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  75. package/dist/src/lib/cli-ui/run-renderer.js +79 -9
  76. package/dist/src/lib/errors.d.ts +84 -6
  77. package/dist/src/lib/errors.js +142 -17
  78. package/dist/src/lib/locks/index.d.ts +3 -3
  79. package/dist/src/lib/locks/index.js +2 -2
  80. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  81. package/dist/src/lib/locks/lock-manager.js +47 -3
  82. package/dist/src/lib/locks/types.d.ts +34 -3
  83. package/dist/src/lib/locks/types.js +16 -0
  84. package/dist/src/lib/manifest.js +1 -17
  85. package/dist/src/lib/mcp-config.d.ts +45 -0
  86. package/dist/src/lib/mcp-config.js +77 -1
  87. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  88. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  89. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  90. package/dist/src/lib/merge-check/command-result.js +56 -0
  91. package/dist/src/lib/merge-check/index.d.ts +4 -0
  92. package/dist/src/lib/merge-check/index.js +1 -1
  93. package/dist/src/lib/merge-check/report.js +5 -1
  94. package/dist/src/lib/merge-check/types.d.ts +9 -0
  95. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  96. package/dist/src/lib/merge-check/watch.js +215 -0
  97. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  98. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  99. package/dist/src/lib/settings.d.ts +14 -0
  100. package/dist/src/lib/settings.js +61 -8
  101. package/dist/src/lib/shutdown.d.ts +42 -2
  102. package/dist/src/lib/shutdown.js +75 -5
  103. package/dist/src/lib/skills-check.d.ts +26 -0
  104. package/dist/src/lib/skills-check.js +31 -0
  105. package/dist/src/lib/stacks.d.ts +110 -1
  106. package/dist/src/lib/stacks.js +244 -5
  107. package/dist/src/lib/templates.d.ts +74 -0
  108. package/dist/src/lib/templates.js +171 -16
  109. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  110. package/dist/src/lib/test-tautology-detector.js +153 -12
  111. package/dist/src/lib/version-check.d.ts +31 -0
  112. package/dist/src/lib/version-check.js +46 -7
  113. package/dist/src/lib/workflow/batch-executor.d.ts +138 -6
  114. package/dist/src/lib/workflow/batch-executor.js +550 -54
  115. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  116. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  117. package/dist/src/lib/workflow/chain-resume.d.ts +118 -0
  118. package/dist/src/lib/workflow/chain-resume.js +170 -0
  119. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  120. package/dist/src/lib/workflow/completed-status.js +57 -0
  121. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  122. package/dist/src/lib/workflow/config-resolver.js +54 -2
  123. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  124. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  125. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  126. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  127. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  128. package/dist/src/lib/workflow/drivers/claude-code.d.ts +21 -0
  129. package/dist/src/lib/workflow/drivers/claude-code.js +50 -8
  130. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  131. package/dist/src/lib/workflow/error-classifier.js +14 -1
  132. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  133. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  134. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  135. package/dist/src/lib/workflow/heartbeat.js +90 -0
  136. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  137. package/dist/src/lib/workflow/log-writer.js +81 -14
  138. package/dist/src/lib/workflow/metrics-schema.d.ts +42 -3
  139. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  140. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  141. package/dist/src/lib/workflow/phase-executor.d.ts +234 -13
  142. package/dist/src/lib/workflow/phase-executor.js +533 -56
  143. package/dist/src/lib/workflow/phase-registry.js +2 -1
  144. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  145. package/dist/src/lib/workflow/platforms/github.js +152 -0
  146. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  147. package/dist/src/lib/workflow/qa-cache.js +22 -9
  148. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  149. package/dist/src/lib/workflow/ready-gate.js +25 -9
  150. package/dist/src/lib/workflow/run-log-schema.d.ts +37 -0
  151. package/dist/src/lib/workflow/run-log-schema.js +92 -2
  152. package/dist/src/lib/workflow/run-orchestrator.d.ts +33 -0
  153. package/dist/src/lib/workflow/run-orchestrator.js +429 -47
  154. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  155. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  156. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  157. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  158. package/dist/src/lib/workflow/state-manager.js +83 -0
  159. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  160. package/dist/src/lib/workflow/state-schema.js +47 -0
  161. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  162. package/dist/src/lib/workflow/status-derivation.js +27 -0
  163. package/dist/src/lib/workflow/types.d.ts +149 -4
  164. package/dist/src/lib/workflow/types.js +1 -0
  165. package/dist/src/lib/workflow/worktree-manager.d.ts +70 -2
  166. package/dist/src/lib/workflow/worktree-manager.js +201 -52
  167. package/dist/src/mcp/tools/run.d.ts +11 -1
  168. package/dist/src/mcp/tools/run.js +22 -1
  169. package/dist/src/mcp/tools/status.js +6 -0
  170. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  171. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  172. package/dist/src/ui/tui/IssueBox.js +10 -2
  173. package/dist/src/ui/tui/row-cap.js +2 -1
  174. package/dist/src/ui/tui/theme.d.ts +7 -0
  175. package/dist/src/ui/tui/theme.js +9 -0
  176. package/package.json +9 -10
  177. package/templates/hooks/parallel-marker.sh +50 -0
  178. package/templates/hooks/post-tool.sh +38 -10
  179. package/templates/hooks/pre-tool.sh +405 -67
  180. package/templates/scripts/cleanup-worktree.sh +282 -30
  181. package/templates/scripts/new-feature.sh +264 -16
  182. package/templates/skills/_shared/references/trust-model.md +18 -0
  183. package/templates/skills/assess/SKILL.md +250 -344
  184. package/templates/skills/assess/references/predicted-collision-detection.md +10 -7
  185. package/templates/skills/docs/SKILL.md +4 -4
  186. package/templates/skills/exec/SKILL.md +26 -6
  187. package/templates/skills/fullsolve/SKILL.md +4 -2
  188. package/templates/skills/loop/SKILL.md +38 -2
  189. package/templates/skills/merger/SKILL.md +2 -2
  190. package/templates/skills/qa/SKILL.md +337 -57
  191. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  192. package/templates/skills/qa/references/call-site-review.md +2 -2
  193. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  194. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  195. package/templates/skills/qa/references/quality-gates.md +21 -7
  196. package/templates/skills/qa/references/testing-requirements.md +1 -1
  197. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  198. package/templates/skills/reflect/SKILL.md +27 -13
  199. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  200. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  201. package/templates/skills/release/SKILL.md +11 -3
  202. package/templates/skills/setup/SKILL.md +5 -5
  203. package/templates/skills/spec/SKILL.md +2 -0
  204. package/templates/skills/spec/references/verification-criteria.md +1 -1
  205. package/templates/skills/test/SKILL.md +1 -1
  206. package/dist/src/lib/phase-spinner.d.ts +0 -146
  207. package/dist/src/lib/phase-spinner.js +0 -255
  208. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  209. package/dist/src/lib/workflow/pr-operations.js +0 -326
  210. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  211. package/dist/src/lib/workflow/run-summary.js +0 -142
@@ -107,34 +107,146 @@ export class BillingError extends SequantError {
107
107
  }
108
108
  }
109
109
  /**
110
- * True when the rate-limit info represents a billing/credits failure (which a
111
- * retry cannot fix), rather than a transient throttle.
110
+ * Recognized rate-limit *window* vocabulary limit types whose exhaustion is
111
+ * a pause that reopens at `resetsAt`, not a wallet state. `five_hour` is the
112
+ * subscription session window; `seven_day` is prefix-matched because the SDK
113
+ * emits model-qualified variants (`seven_day*`, see {@link formatResetTime}).
114
+ *
115
+ * Deliberately an allowlist (#860 AC-3): this encodes an inference about
116
+ * Anthropic's payload vocabulary, so an unlisted type (e.g. `overage`) must
117
+ * fail closed to the terminal billing path rather than into a five-hour sleep.
118
+ */
119
+ const WAITABLE_WINDOW_TYPE_RE = /^(five_hour|seven_day)/;
120
+ /**
121
+ * True when the rate-limit info describes an exhausted *window* that will
122
+ * reopen at a known future time: a recognized window `rateLimitType` plus a
123
+ * `resetsAt` still in the future (#860 AC-1).
124
+ *
125
+ * This is the discriminator between "subscription window closed until 07:00"
126
+ * and "account needs credits". The fields that would answer that directly
127
+ * (`canUserPurchaseCredits`, `hasChargeableSavedPaymentMethod`) are absent
128
+ * from every real captured payload, so the window shape is the proxy — and
129
+ * any unrecognized shape returns false (fail closed, #860 AC-3).
130
+ *
131
+ * Explicitly informational statuses (`allowed` / `allowed_warning`) are never
132
+ * waitable. The driver retains marker-carrying warnings as failure-grade
133
+ * (pre-#732 semantics, unchanged), so a stale "you are nearing your limit"
134
+ * warning can be misattributed to an unrelated phase failure — classifying it
135
+ * waitable would upgrade that misattribution from a cheap immediate halt to a
136
+ * multi-hour sleep. The 26 real captures carry no `status` field at all, so
137
+ * absent status stays waitable; only a status that affirmatively says
138
+ * "not a rejection" is excluded.
139
+ */
140
+ export function isWaitableWindow(info, now = Date.now()) {
141
+ if (info.status === "allowed" || info.status === "allowed_warning") {
142
+ return false;
143
+ }
144
+ if (typeof info.rateLimitType !== "string")
145
+ return false;
146
+ if (!WAITABLE_WINDOW_TYPE_RE.test(info.rateLimitType))
147
+ return false;
148
+ if (typeof info.resetsAt !== "number")
149
+ return false;
150
+ return resetsAtToMs(info.resetsAt) > now;
151
+ }
152
+ /**
153
+ * Vocabulary-drift telemetry for the #860 fail-closed path: returns a hint
154
+ * string when a payload was terminal ONLY because its `rateLimitType` is not
155
+ * in the recognized window allowlist — i.e. it carries `out_of_credits` plus a
156
+ * live future `resetsAt` and a *present but unrecognized* window type, and no
157
+ * explicit `credits_required`. If Anthropic renames or adds a window type,
158
+ * the halt message names the rejected type instead of silently reading as an
159
+ * ordinary wallet failure. A missing `rateLimitType` is NOT drift evidence
160
+ * (API-account payloads may legitimately omit it), so no hint fires there.
112
161
  */
113
- export function isBillingFailure(info) {
162
+ export function unrecognizedWindowHint(info, now = Date.now()) {
163
+ if (info.errorCode === "credits_required")
164
+ return null;
165
+ if (info.overageDisabledReason !== "out_of_credits")
166
+ return null;
167
+ if (info.status === "allowed" || info.status === "allowed_warning") {
168
+ return null;
169
+ }
170
+ if (typeof info.rateLimitType !== "string")
171
+ return null;
172
+ if (WAITABLE_WINDOW_TYPE_RE.test(info.rateLimitType))
173
+ return null;
174
+ if (typeof info.resetsAt !== "number")
175
+ return null;
176
+ if (resetsAtToMs(info.resetsAt) <= now)
177
+ return null;
178
+ return `unrecognized window type "${info.rateLimitType}" with a future reset — treated as terminal (auto-wait recognizes five_hour/seven_day)`;
179
+ }
180
+ /**
181
+ * True when the info carries an explicit billing/credits marker, regardless
182
+ * of whether a live window would make it waitable. This is the pre-#860
183
+ * `isBillingFailure` predicate, kept for failure-*detection* sites
184
+ * ({@link isRateLimitFailureInfo}) whose retention semantics must not narrow.
185
+ */
186
+ function hasBillingMarkers(info) {
114
187
  return (info.errorCode === "credits_required" ||
115
188
  info.overageDisabledReason === "out_of_credits");
116
189
  }
190
+ /**
191
+ * True when the rate-limit info represents a billing/credits failure (which
192
+ * waiting cannot fix), rather than a transient throttle or an exhausted
193
+ * window.
194
+ *
195
+ * Narrowed by #860: a subscription plan hitting its five-hour cap with
196
+ * overage disabled emits `overageDisabledReason: "out_of_credits"` *plus* a
197
+ * window type and a live `resetsAt` — a pause, not a wallet failure. That
198
+ * shape is excluded here so it classifies as a retryable {@link RateLimitError}
199
+ * and `--auto-wait` (#804) can act on it. An explicit
200
+ * `errorCode: "credits_required"` stays terminal even alongside window
201
+ * evidence — it is the SDK's direct "purchase needed" signal (#860 AC-2).
202
+ * Anything short of the full recognized window shape remains terminal
203
+ * (fail closed, #860 AC-3).
204
+ */
205
+ export function isBillingFailure(info, now = Date.now()) {
206
+ if (info.errorCode === "credits_required")
207
+ return true;
208
+ return (info.overageDisabledReason === "out_of_credits" &&
209
+ !isWaitableWindow(info, now));
210
+ }
117
211
  /**
118
212
  * True when the rate-limit info represents an actual failure (rejection or
119
213
  * billing), as opposed to an informational `allowed` / `allowed_warning`
120
214
  * event. The driver uses this to avoid mis-attributing a stale warning event
121
215
  * to an unrelated phase failure.
216
+ *
217
+ * Built on the raw billing *markers*, not the #860-narrowed classification:
218
+ * the captured five-hour payloads cannot prove they carried
219
+ * `status: "rejected"`, so narrowing here could silently drop the very events
220
+ * #860 exists to keep (they'd fall back to a metadata-less assistant error and
221
+ * auto-wait would stay inert). Retention semantics are unchanged; only the
222
+ * billing-vs-waitable *classification* narrowed.
122
223
  */
123
224
  export function isRateLimitFailureInfo(info) {
124
- return info.status === "rejected" || isBillingFailure(info);
225
+ return info.status === "rejected" || hasBillingMarkers(info);
226
+ }
227
+ /**
228
+ * Normalize a `resetsAt` timestamp to milliseconds. The SDK does not pin the
229
+ * unit, so use the same heuristic everywhere a `resetsAt` is compared or
230
+ * displayed: values below ~1e12 are seconds, otherwise milliseconds.
231
+ */
232
+ export function resetsAtToMs(resetsAt) {
233
+ return resetsAt < 1e12 ? resetsAt * 1000 : resetsAt;
125
234
  }
126
235
  /**
127
236
  * Format a Unix timestamp (seconds or ms) as a local time string.
128
237
  *
129
238
  * Bare `HH:MM` when the reset falls on the current local calendar day;
130
- * date-qualified `MM-DD HH:MM` otherwise. Multi-day windows
239
+ * date-qualified `MM-DD HH:MM` otherwise. Also used for #804's auto-wait wake
240
+ * time (a wake is `resetsAt + buffer`, already in ms, which `resetsAtToMs`
241
+ * passes through unchanged) so both render in one convention.
242
+ *
243
+ * Multi-day windows
131
244
  * (`rateLimitType: seven_day*`) can reset days out — a bare `HH:MM` there reads
132
245
  * as "later today" and misleads the user (#732 QA follow-up), so the date is
133
246
  * included whenever the reset is not today.
134
247
  */
135
- function formatResetTime(resetsAt) {
136
- // Heuristic: values below ~1e12 are seconds, otherwise milliseconds.
137
- const ms = resetsAt < 1e12 ? resetsAt * 1000 : resetsAt;
248
+ export function formatResetTime(resetsAt) {
249
+ const ms = resetsAtToMs(resetsAt);
138
250
  const d = new Date(ms);
139
251
  const hh = String(d.getHours()).padStart(2, "0");
140
252
  const mm = String(d.getMinutes()).padStart(2, "0");
@@ -156,16 +268,27 @@ function formatResetTime(resetsAt) {
156
268
  * - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
157
269
  * "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
158
270
  * `resetsAt` is absent)
271
+ *
272
+ * `now` feeds the #860 waitable-window classification so message and error
273
+ * type are derived against the same instant (and tests can pin the clock).
274
+ * A waitable window renders through the rate-limited branch — its reset time
275
+ * is the actionable fact; "Out of credits" would misname a pause as a wallet
276
+ * failure.
159
277
  */
160
- export function formatRateLimitMessage(info) {
161
- if (isBillingFailure(info)) {
278
+ export function formatRateLimitMessage(info, now = Date.now()) {
279
+ if (isBillingFailure(info, now)) {
280
+ // #860 drift telemetry: when the ONLY reason this is terminal is an
281
+ // unrecognized window type, say so — the message is the one channel that
282
+ // reaches run output, logs, and `PhaseResult.error` on every display path.
283
+ const hint = unrecognizedWindowHint(info, now);
284
+ const suffix = hint ? ` (${hint})` : "";
162
285
  if (info.canUserPurchaseCredits === true) {
163
- return "Out of credits — purchasable";
286
+ return `Out of credits — purchasable${suffix}`;
164
287
  }
165
288
  if (info.canUserPurchaseCredits === false) {
166
- return "Out of credits — hard limit";
289
+ return `Out of credits — hard limit${suffix}`;
167
290
  }
168
- return "Out of credits";
291
+ return `Out of credits${suffix}`;
169
292
  }
170
293
  if (info.resetsAt !== undefined) {
171
294
  return `Rate limited — resets at ${formatResetTime(info.resetsAt)}`;
@@ -175,10 +298,12 @@ export function formatRateLimitMessage(info) {
175
298
  /**
176
299
  * Construct the appropriate typed error from structured rate-limit info.
177
300
  * Billing/credits failures become a non-retryable {@link BillingError};
178
- * transient throttles become a retryable {@link RateLimitError}.
301
+ * transient throttles AND exhausted-but-reopening windows (#860) become a
302
+ * retryable {@link RateLimitError}. `now` pins the waitable-window check to
303
+ * one instant across message and classification.
179
304
  */
180
- export function createRateLimitError(info) {
181
- const message = formatRateLimitMessage(info);
305
+ export function createRateLimitError(info, now = Date.now()) {
306
+ const message = formatRateLimitMessage(info, now);
182
307
  const metadata = {
183
308
  resetsAt: info.resetsAt,
184
309
  rateLimitType: info.rateLimitType,
@@ -187,7 +312,7 @@ export function createRateLimitError(info) {
187
312
  canUserPurchaseCredits: info.canUserPurchaseCredits,
188
313
  hasChargeableSavedPaymentMethod: info.hasChargeableSavedPaymentMethod,
189
314
  };
190
- return isBillingFailure(info)
315
+ return isBillingFailure(info, now)
191
316
  ? new BillingError(message, metadata)
192
317
  : new RateLimitError(message, metadata);
193
318
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Public surface for the issue-level concurrency lock (#625).
3
3
  */
4
- export { LockManager, classifyStaleness, defaultIsPidAlive, formatLockedMessage, isOrchestratorMode, resolveLocksDir, } from "./lock-manager.js";
4
+ export { LockManager, classifyStaleness, defaultIsPidAlive, formatLockedMessage, isOrchestratorMode, resolveLocksDir, resolveMaxLockAgeMs, } from "./lock-manager.js";
5
5
  export type { LockManagerOptions } from "./lock-manager.js";
6
- export { DEFAULT_LOCKS_DIR, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";
7
- export type { AcquireResult, LockFile, LockListing, SignalOtherResult, SignalReason, } from "./types.js";
6
+ export { DEFAULT_LOCKS_DIR, DEFAULT_MAX_LOCK_AGE_MS, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";
7
+ export type { AcquireResult, LockFile, LockListing, SignalOtherResult, SignalReason, StaleReason, } from "./types.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Public surface for the issue-level concurrency lock (#625).
3
3
  */
4
- export { LockManager, classifyStaleness, defaultIsPidAlive, formatLockedMessage, isOrchestratorMode, resolveLocksDir, } from "./lock-manager.js";
5
- export { DEFAULT_LOCKS_DIR, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";
4
+ export { LockManager, classifyStaleness, defaultIsPidAlive, formatLockedMessage, isOrchestratorMode, resolveLocksDir, resolveMaxLockAgeMs, } from "./lock-manager.js";
5
+ export { DEFAULT_LOCKS_DIR, DEFAULT_MAX_LOCK_AGE_MS, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";
@@ -7,6 +7,8 @@
7
7
  * `state.json`) keeps acquisition atomic — no read-modify-write race.
8
8
  *
9
9
  * Stale detection (in order):
10
+ * 0. Absolute ceiling (any host, any PID state): `startedAt > maxLockAgeMs
11
+ * ago` → cleared. Guards against recycled PIDs and SIGKILL leaks (#856).
10
12
  * 1. `hostname === os.hostname()`: check `process.kill(pid, 0)`.
11
13
  * Not alive → cleared.
12
14
  * 2. Cross-host: PID check is meaningless. Use age only.
@@ -16,7 +18,7 @@
16
18
  * method is a no-op (no fs touches, no warnings). Mirrors the
17
19
  * `OrchestratorRenderer` pattern at `src/lib/cli-ui/run-renderer.ts:244`.
18
20
  */
19
- import { type AcquireResult, type LockFile, type LockListing, type SignalOtherResult } from "./types.js";
21
+ import { type AcquireResult, type LockFile, type LockListing, type SignalOtherResult, type StaleReason } from "./types.js";
20
22
  export interface LockManagerOptions {
21
23
  /** Directory holding `<issue>.lock` files (default: `.sequant/locks`). */
22
24
  locksDir?: string;
@@ -31,6 +33,12 @@ export interface LockManagerOptions {
31
33
  * the lock has to bridge long /fullsolve runs with multi-iteration QA loops.
32
34
  */
33
35
  skillLockTtlMs?: number;
36
+ /**
37
+ * Absolute age ceiling (ms). A lock older than this is stale regardless of
38
+ * host, PID liveness, or `skipPidCheck`. Default 24h. See
39
+ * `DEFAULT_MAX_LOCK_AGE_MS` (#856).
40
+ */
41
+ maxLockAgeMs?: number;
34
42
  /** Override for orchestrator detection (test seam). */
35
43
  orchestratorMode?: boolean;
36
44
  /** Override for `os.hostname()` (test seam). */
@@ -52,6 +60,13 @@ export declare function resolveLocksDir(explicit?: string): string;
52
60
  * caller can fall back to the constructor option / default.
53
61
  */
54
62
  export declare function resolveSkillLockTtlMs(): number | null;
63
+ /**
64
+ * Resolve `SEQUANT_MAX_LOCK_AGE_MS` (milliseconds) — env override for the
65
+ * absolute lock-age ceiling (#856). Returns `null` when unset or unparseable
66
+ * so the caller can fall back to the constructor option / default. Mirrors
67
+ * `resolveSkillLockTtlMs`.
68
+ */
69
+ export declare function resolveMaxLockAgeMs(): number | null;
55
70
  /** Default same-host PID check. `process.kill(pid, 0)` throws if not alive. */
56
71
  export declare function defaultIsPidAlive(pid: number): boolean;
57
72
  /** Build the canonical "issue is in use" error message (AC: error format). */
@@ -67,12 +82,15 @@ export declare function classifyStaleness(args: {
67
82
  staleAgeMs: number;
68
83
  /** TTL for skill-shell (skipPidCheck) locks; falls back to staleAgeMs. */
69
84
  skillLockTtlMs?: number;
85
+ /** Absolute ceiling; falls back to `DEFAULT_MAX_LOCK_AGE_MS`. */
86
+ maxLockAgeMs?: number;
70
87
  isPidAlive: (pid: number) => boolean;
71
- }): "pid-dead" | "age-exceeded" | null;
88
+ }): StaleReason | null;
72
89
  export declare class LockManager {
73
90
  private readonly locksDir;
74
91
  private readonly staleAgeMs;
75
92
  private readonly skillLockTtlMs;
93
+ private readonly maxLockAgeMs;
76
94
  private readonly orchestratorMode;
77
95
  private readonly hostname;
78
96
  private readonly pid;
@@ -7,6 +7,8 @@
7
7
  * `state.json`) keeps acquisition atomic — no read-modify-write race.
8
8
  *
9
9
  * Stale detection (in order):
10
+ * 0. Absolute ceiling (any host, any PID state): `startedAt > maxLockAgeMs
11
+ * ago` → cleared. Guards against recycled PIDs and SIGKILL leaks (#856).
10
12
  * 1. `hostname === os.hostname()`: check `process.kill(pid, 0)`.
11
13
  * Not alive → cleared.
12
14
  * 2. Cross-host: PID check is meaningless. Use age only.
@@ -19,7 +21,7 @@
19
21
  import { openSync, closeSync, writeSync, readFileSync, existsSync, unlinkSync, mkdirSync, readdirSync, statSync, } from "fs";
20
22
  import { join, resolve } from "path";
21
23
  import * as os from "os";
22
- import { DEFAULT_LOCKS_DIR, DEFAULT_SKILL_LOCK_TTL_MS, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";
24
+ import { DEFAULT_LOCKS_DIR, DEFAULT_MAX_LOCK_AGE_MS, DEFAULT_SKILL_LOCK_TTL_MS, DEFAULT_STALE_AGE_MS, LockFileSchema, } from "./types.js";
23
25
  /** Detect orchestrator mode purely from env (no caching) so tests can mutate. */
24
26
  export function isOrchestratorMode() {
25
27
  return Boolean(process.env.SEQUANT_ORCHESTRATOR);
@@ -43,6 +45,21 @@ export function resolveSkillLockTtlMs() {
43
45
  return null;
44
46
  return ms;
45
47
  }
48
+ /**
49
+ * Resolve `SEQUANT_MAX_LOCK_AGE_MS` (milliseconds) — env override for the
50
+ * absolute lock-age ceiling (#856). Returns `null` when unset or unparseable
51
+ * so the caller can fall back to the constructor option / default. Mirrors
52
+ * `resolveSkillLockTtlMs`.
53
+ */
54
+ export function resolveMaxLockAgeMs() {
55
+ const raw = process.env.SEQUANT_MAX_LOCK_AGE_MS;
56
+ if (raw === undefined || raw === "")
57
+ return null;
58
+ const ms = Number.parseInt(raw, 10);
59
+ if (!Number.isFinite(ms) || ms <= 0)
60
+ return null;
61
+ return ms;
62
+ }
46
63
  /** Default same-host PID check. `process.kill(pid, 0)` throws if not alive. */
47
64
  export function defaultIsPidAlive(pid) {
48
65
  if (!Number.isInteger(pid) || pid <= 0)
@@ -72,6 +89,19 @@ export function formatLockedMessage(issue, holder) {
72
89
  export function classifyStaleness(args) {
73
90
  const { holder, myHostname, now, staleAgeMs, isPidAlive } = args;
74
91
  const skillTtl = args.skillLockTtlMs ?? staleAgeMs;
92
+ const maxAge = args.maxLockAgeMs ?? DEFAULT_MAX_LOCK_AGE_MS;
93
+ const ageMs = now - Date.parse(holder.startedAt);
94
+ const ageKnown = Number.isFinite(ageMs);
95
+ // 0. Absolute ceiling, checked FIRST and unconditionally (#856). The
96
+ // same-host branch below treats a live PID as proof of freshness, but a
97
+ // PID is only a stable identity while its process lives — once the OS
98
+ // recycles it, an abandoned lock points at an unrelated process and
99
+ // reads as fresh forever. Nothing legitimate holds a lock this long
100
+ // (24h vs a 30-minute phase timeout), so age wins over PID liveness
101
+ // past the ceiling. Also the sole recovery path for locks leaked by a
102
+ // SIGKILLed run, whose release handlers never got to run.
103
+ if (ageKnown && ageMs > maxAge)
104
+ return "max-age-exceeded";
75
105
  // 1. Same-host PID check is authoritative — except when the holder asked
76
106
  // us to skip it (skill shells exit before the lock is released; their
77
107
  // PID is dead but the skill is still running in Claude Code).
@@ -85,8 +115,7 @@ export function classifyStaleness(args) {
85
115
  // with multi-iteration QA loops don't lose their own lock; cross-host
86
116
  // uses the stricter staleAgeMs (default 2h).
87
117
  const ttl = holder.skipPidCheck ? skillTtl : staleAgeMs;
88
- const ageMs = now - Date.parse(holder.startedAt);
89
- if (!Number.isFinite(ageMs))
118
+ if (!ageKnown)
90
119
  return null;
91
120
  if (ageMs > ttl)
92
121
  return "age-exceeded";
@@ -96,6 +125,7 @@ export class LockManager {
96
125
  locksDir;
97
126
  staleAgeMs;
98
127
  skillLockTtlMs;
128
+ maxLockAgeMs;
99
129
  orchestratorMode;
100
130
  hostname;
101
131
  pid;
@@ -110,6 +140,8 @@ export class LockManager {
110
140
  options.skillLockTtlMs ??
111
141
  resolveSkillLockTtlMs() ??
112
142
  DEFAULT_SKILL_LOCK_TTL_MS;
143
+ this.maxLockAgeMs =
144
+ options.maxLockAgeMs ?? resolveMaxLockAgeMs() ?? DEFAULT_MAX_LOCK_AGE_MS;
113
145
  this.orchestratorMode = options.orchestratorMode ?? isOrchestratorMode();
114
146
  this.hostname = options.hostname ?? os.hostname();
115
147
  this.pid = options.pid ?? process.pid;
@@ -156,6 +188,7 @@ export class LockManager {
156
188
  now: this.now(),
157
189
  staleAgeMs: this.staleAgeMs,
158
190
  skillLockTtlMs: this.skillLockTtlMs,
191
+ maxLockAgeMs: this.maxLockAgeMs,
159
192
  isPidAlive: this.isPidAlive,
160
193
  });
161
194
  if (staleReason) {
@@ -211,6 +244,15 @@ export class LockManager {
211
244
  if (holder.pid === this.pid || holder.pid === process.ppid) {
212
245
  return { sent: false, reason: "self-or-parent" };
213
246
  }
247
+ // #856: past the absolute ceiling, the PID is no longer trustworthy
248
+ // identity — the OS has almost certainly recycled it onto an unrelated
249
+ // process. `acquire` already treats such a lock as abandoned; signalling
250
+ // it would kill a stranger's program on behalf of a lock nobody holds.
251
+ // The liveness probe below cannot catch this: a recycled PID *is* alive.
252
+ const ageMs = this.now() - Date.parse(holder.startedAt);
253
+ if (Number.isFinite(ageMs) && ageMs > this.maxLockAgeMs) {
254
+ return { sent: false, reason: "stale-pid-untrusted" };
255
+ }
214
256
  if (!this.isPidAlive(holder.pid))
215
257
  return { sent: false, reason: "pid-dead" };
216
258
  try {
@@ -304,6 +346,7 @@ export class LockManager {
304
346
  now,
305
347
  staleAgeMs: this.staleAgeMs,
306
348
  skillLockTtlMs: this.skillLockTtlMs,
349
+ maxLockAgeMs: this.maxLockAgeMs,
307
350
  isPidAlive: this.isPidAlive,
308
351
  });
309
352
  out.push({
@@ -338,6 +381,7 @@ export class LockManager {
338
381
  now: this.now(),
339
382
  staleAgeMs: this.staleAgeMs,
340
383
  skillLockTtlMs: this.skillLockTtlMs,
384
+ maxLockAgeMs: this.maxLockAgeMs,
341
385
  isPidAlive: this.isPidAlive,
342
386
  });
343
387
  if (!staleReason) {
@@ -14,8 +14,32 @@ export declare const DEFAULT_STALE_AGE_MS: number;
14
14
  * Override per-process via `SEQUANT_SKILL_LOCK_TTL_MS` (milliseconds).
15
15
  */
16
16
  export declare const DEFAULT_SKILL_LOCK_TTL_MS: number;
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 declare const DEFAULT_MAX_LOCK_AGE_MS: number;
17
33
  /** Default lock directory relative to the project root. */
18
34
  export declare const DEFAULT_LOCKS_DIR = ".sequant/locks";
35
+ /**
36
+ * Why a lock is considered stale, or `null` when it is fresh.
37
+ *
38
+ * - `pid-dead` — same-host holder PID is gone.
39
+ * - `age-exceeded` — cross-host / skill-shell lock past its TTL.
40
+ * - `max-age-exceeded` — past the absolute ceiling; applies unconditionally.
41
+ */
42
+ export type StaleReason = "pid-dead" | "age-exceeded" | "max-age-exceeded";
19
43
  /** On-disk lock payload. */
20
44
  export declare const LockFileSchema: z.ZodObject<{
21
45
  pid: z.ZodNumber;
@@ -35,7 +59,7 @@ export type AcquireResult = {
35
59
  lockPath: string;
36
60
  /** True when the holder appears stale and could be cleared with `--force`. */
37
61
  stale: boolean;
38
- staleReason?: "pid-dead" | "age-exceeded" | null;
62
+ staleReason?: StaleReason | null;
39
63
  };
40
64
  /** Listing entry from `LockManager.list()`. */
41
65
  export interface LockListing {
@@ -43,7 +67,7 @@ export interface LockListing {
43
67
  holder: LockFile;
44
68
  ageMs: number;
45
69
  stale: boolean;
46
- staleReason: "pid-dead" | "age-exceeded" | null;
70
+ staleReason: StaleReason | null;
47
71
  lockPath: string;
48
72
  }
49
73
  /**
@@ -51,7 +75,14 @@ export interface LockListing {
51
75
  * that previously all collapsed to `false`, so callers can produce accurate
52
76
  * log lines (#637).
53
77
  */
54
- export type SignalReason = "sent" | "orchestrator" | "cross-host" | "self-or-parent" | "pid-dead" | "kill-failed";
78
+ export type SignalReason = "sent" | "orchestrator" | "cross-host" | "self-or-parent" | "pid-dead"
79
+ /**
80
+ * Holder is past the absolute age ceiling, so its PID is not trustworthy
81
+ * identity (#856). Signalling it would target whatever process the OS has
82
+ * since recycled that PID onto — an unrelated program, killed by a
83
+ * `--force --signal-other` aimed at a lock abandoned weeks ago.
84
+ */
85
+ | "stale-pid-untrusted" | "kill-failed";
55
86
  /** Outcome of `LockManager.signalOther()`. */
56
87
  export interface SignalOtherResult {
57
88
  sent: boolean;
@@ -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. */
@@ -2,6 +2,7 @@
2
2
  * Manifest management for tracking installed version
3
3
  */
4
4
  import { readFile, writeFile, fileExists } from "./fs.js";
5
+ import { compareVersions } from "./version-check.js";
5
6
  import { fileURLToPath } from "url";
6
7
  import { dirname, resolve } from "path";
7
8
  import { readFileSync } from "fs";
@@ -32,23 +33,6 @@ const PACKAGE_VERSION = pkg.version;
32
33
  export function getPackageVersion() {
33
34
  return PACKAGE_VERSION;
34
35
  }
35
- /**
36
- * Compare two semver versions.
37
- * Returns: 1 if a > b, -1 if a < b, 0 if equal
38
- */
39
- function compareVersions(a, b) {
40
- const partsA = a.split(".").map(Number);
41
- const partsB = b.split(".").map(Number);
42
- for (let i = 0; i < 3; i++) {
43
- const numA = partsA[i] || 0;
44
- const numB = partsB[i] || 0;
45
- if (numA > numB)
46
- return 1;
47
- if (numA < numB)
48
- return -1;
49
- }
50
- return 0;
51
- }
52
36
  export async function getManifest() {
53
37
  if (!(await fileExists(MANIFEST_PATH))) {
54
38
  return null;
@@ -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;