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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -4
- package/dist/bin/cli.js +99 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +58 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +38 -10
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +405 -67
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +250 -344
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +10 -7
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +4 -2
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +11 -3
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.js +2 -0
- package/dist/src/commands/logs.js +20 -6
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +25 -3
- package/dist/src/commands/ready.js +39 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.d.ts +20 -0
- package/dist/src/commands/run-display.js +117 -5
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/stats.js +47 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/assess-collision-detect.d.ts +19 -2
- package/dist/src/lib/assess-collision-detect.js +68 -4
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +33 -0
- package/dist/src/lib/cli-flags.js +46 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +79 -9
- package/dist/src/lib/errors.d.ts +84 -6
- package/dist/src/lib/errors.js +142 -17
- package/dist/src/lib/locks/index.d.ts +3 -3
- package/dist/src/lib/locks/index.js +2 -2
- package/dist/src/lib/locks/lock-manager.d.ts +20 -2
- package/dist/src/lib/locks/lock-manager.js +47 -3
- package/dist/src/lib/locks/types.d.ts +34 -3
- package/dist/src/lib/locks/types.js +16 -0
- package/dist/src/lib/manifest.js +1 -17
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +14 -0
- package/dist/src/lib/settings.js +61 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +9 -2
- package/dist/src/lib/test-tautology-detector.js +153 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +46 -7
- package/dist/src/lib/workflow/batch-executor.d.ts +138 -6
- package/dist/src/lib/workflow/batch-executor.js +550 -54
- package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/chain-preflight.js +199 -0
- package/dist/src/lib/workflow/chain-resume.d.ts +118 -0
- package/dist/src/lib/workflow/chain-resume.js +170 -0
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
- package/dist/src/lib/workflow/config-resolver.js +54 -2
- package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
- package/dist/src/lib/workflow/dependency-markers.js +79 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +21 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +50 -8
- package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
- package/dist/src/lib/workflow/error-classifier.js +14 -1
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +44 -1
- package/dist/src/lib/workflow/log-writer.js +81 -14
- package/dist/src/lib/workflow/metrics-schema.d.ts +42 -3
- package/dist/src/lib/workflow/metrics-schema.js +16 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +234 -13
- package/dist/src/lib/workflow/phase-executor.js +533 -56
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
- package/dist/src/lib/workflow/ready-gate.js +25 -9
- package/dist/src/lib/workflow/run-log-schema.d.ts +37 -0
- package/dist/src/lib/workflow/run-log-schema.js +92 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +33 -0
- package/dist/src/lib/workflow/run-orchestrator.js +429 -47
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
- package/dist/src/lib/workflow/status-derivation.js +27 -0
- package/dist/src/lib/workflow/types.d.ts +149 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +70 -2
- package/dist/src/lib/workflow/worktree-manager.js +201 -52
- package/dist/src/mcp/tools/run.d.ts +11 -1
- package/dist/src/mcp/tools/run.js +22 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +9 -10
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +38 -10
- package/templates/hooks/pre-tool.sh +405 -67
- package/templates/scripts/cleanup-worktree.sh +282 -30
- package/templates/scripts/new-feature.sh +264 -16
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +250 -344
- package/templates/skills/assess/references/predicted-collision-detection.md +10 -7
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +26 -6
- package/templates/skills/fullsolve/SKILL.md +4 -2
- package/templates/skills/loop/SKILL.md +38 -2
- package/templates/skills/merger/SKILL.md +2 -2
- package/templates/skills/qa/SKILL.md +337 -57
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/reflect/SKILL.md +27 -13
- package/templates/skills/reflect/references/documentation-tiers.md +80 -68
- package/templates/skills/reflect/references/phase-reflection.md +31 -15
- package/templates/skills/release/SKILL.md +11 -3
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +2 -0
- package/templates/skills/spec/references/verification-criteria.md +1 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
package/dist/src/lib/errors.js
CHANGED
|
@@ -107,34 +107,146 @@ export class BillingError extends SequantError {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
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" ||
|
|
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.
|
|
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
|
-
|
|
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
|
|
286
|
+
return `Out of credits — purchasable${suffix}`;
|
|
164
287
|
}
|
|
165
288
|
if (info.canUserPurchaseCredits === false) {
|
|
166
|
-
return
|
|
289
|
+
return `Out of credits — hard limit${suffix}`;
|
|
167
290
|
}
|
|
168
|
-
return
|
|
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
|
|
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
|
-
}):
|
|
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
|
-
|
|
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?:
|
|
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:
|
|
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"
|
|
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. */
|
package/dist/src/lib/manifest.js
CHANGED
|
@@ -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;
|