sequant 2.7.0 → 2.9.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 +18 -2
- package/dist/bin/cli.d.ts +1 -1
- package/dist/bin/cli.js +12 -2
- package/dist/bin/preflight.d.ts +21 -0
- package/dist/bin/preflight.js +45 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +2 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/force-push.md +34 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +117 -19
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +29 -0
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +100 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +24 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +285 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +202 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +287 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/test-quality-checklist.md +272 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +40 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +95 -11
- package/dist/marketplace/external_plugins/sequant/skills/references/shared/framework-gotchas.md +186 -0
- 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 +669 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/references/browser-testing-patterns.md +423 -0
- package/dist/marketplace/external_plugins/sequant/skills/upstream/SKILL.md +419 -0
- package/dist/src/commands/logs.js +6 -1
- package/dist/src/commands/run-display.d.ts +20 -0
- package/dist/src/commands/run-display.js +80 -1
- package/dist/src/commands/stats.js +47 -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/cli-ui/run-renderer.js +17 -9
- package/dist/src/lib/errors.d.ts +91 -0
- package/dist/src/lib/errors.js +118 -0
- package/dist/src/lib/manifest.js +1 -17
- package/dist/src/lib/version-check.d.ts +19 -0
- package/dist/src/lib/version-check.js +45 -5
- package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
- package/dist/src/lib/workflow/batch-executor.js +142 -24
- 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 +116 -0
- package/dist/src/lib/workflow/chain-resume.js +166 -0
- 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 +17 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +29 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +136 -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/log-writer.d.ts +1 -1
- package/dist/src/lib/workflow/log-writer.js +6 -8
- package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
- 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 +50 -0
- package/dist/src/lib/workflow/phase-executor.js +151 -17
- package/dist/src/lib/workflow/run-log-schema.d.ts +26 -0
- package/dist/src/lib/workflow/run-log-schema.js +52 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
- package/dist/src/lib/workflow/run-orchestrator.js +291 -30
- package/dist/src/lib/workflow/state-manager.d.ts +1 -0
- package/dist/src/lib/workflow/state-manager.js +6 -0
- package/dist/src/lib/workflow/state-schema.d.ts +3 -0
- package/dist/src/lib/workflow/state-schema.js +7 -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 +40 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
- package/dist/src/lib/workflow/worktree-manager.js +103 -33
- package/dist/src/mcp/tools/run.d.ts +2 -0
- package/dist/src/mcp/tools/run.js +2 -0
- package/dist/src/ui/tui/theme.d.ts +18 -4
- package/dist/src/ui/tui/theme.js +18 -4
- package/package.json +5 -6
- package/templates/hooks/post-tool.sh +18 -3
- package/templates/hooks/pre-tool.sh +330 -57
- package/templates/scripts/cleanup-worktree.sh +103 -14
- package/templates/skills/_shared/references/force-push.md +34 -0
- package/templates/skills/assess/SKILL.md +117 -19
- package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
- package/templates/skills/exec/SKILL.md +29 -0
- package/templates/skills/fullsolve/SKILL.md +1 -1
- package/templates/skills/loop/SKILL.md +100 -2
- package/templates/skills/qa/SKILL.md +24 -0
- package/templates/skills/qa/references/anti-pattern-detection.md +285 -0
- package/templates/skills/qa/references/call-site-review.md +202 -0
- package/templates/skills/qa/references/quality-gates.md +287 -0
- package/templates/skills/qa/references/test-quality-checklist.md +272 -0
- package/templates/skills/qa/references/testing-requirements.md +40 -0
- package/templates/skills/qa/scripts/quality-checks.sh +95 -11
- package/templates/skills/references/shared/framework-gotchas.md +186 -0
- 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 +669 -0
- package/templates/skills/spec/references/verification-criteria.md +1 -1
- package/templates/skills/test/references/browser-testing-patterns.md +423 -0
- package/templates/skills/upstream/SKILL.md +419 -0
|
@@ -22,12 +22,28 @@
|
|
|
22
22
|
* users to ignore the warning.
|
|
23
23
|
*/
|
|
24
24
|
export declare const EXCLUDED_PATHS: ReadonlySet<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Markdown H1/H2 section headings whose bodies are background/citation prose,
|
|
27
|
+
* not statements of intent-to-modify. A path named *only* under one of these
|
|
28
|
+
* is a reference to existing code, not a file the issue will touch — so the
|
|
29
|
+
* whole section is stripped before path extraction.
|
|
30
|
+
*
|
|
31
|
+
* Match is case-insensitive and prefix-based, so `## Motivation — concrete
|
|
32
|
+
* recent miss` and `## Additional context (see #533)` both match. This is the
|
|
33
|
+
* cheap version of the #556 proximity-weighting mitigation (#769): exclude
|
|
34
|
+
* background sections wholesale rather than scoring per-path distance to AC
|
|
35
|
+
* bullets. "A path appears only in background" ⟺ "the path is absent from the
|
|
36
|
+
* foreground", so stripping these sections up front subsumes per-path tagging.
|
|
37
|
+
*/
|
|
38
|
+
export declare const BACKGROUND_SECTIONS: ReadonlySet<string>;
|
|
25
39
|
/**
|
|
26
40
|
* Extract the set of file paths an issue body identifies as
|
|
27
41
|
* targets-of-modification.
|
|
28
42
|
*
|
|
29
43
|
* Strategy:
|
|
30
|
-
* 1. Strip fenced code blocks and HTML comments (AC-5 guard)
|
|
44
|
+
* 1. Strip fenced code blocks and HTML comments (AC-5 guard), then strip
|
|
45
|
+
* background/citation sections (#769) so a path named only under
|
|
46
|
+
* `## References`, `## Context`, etc. isn't counted as a target.
|
|
31
47
|
* 2. Pull every backtick-quoted path matching the source-tree regex,
|
|
32
48
|
* normalizing skill-mirror paths to their canonical bare form.
|
|
33
49
|
* 3. If the body mentions "3-dir sync", also pull bare
|
|
@@ -71,7 +87,8 @@ export declare function detectFileCollisions(issuePaths: Map<number, Set<string>
|
|
|
71
87
|
* - `chainSuggestion` — emitted only when ≥3 issues collide on the same
|
|
72
88
|
* file (AC-4); suggest-only, never auto-applied. Annotated with the
|
|
73
89
|
* historical chain-mode success rate at length≥3 (1/6 = 17%, per #604
|
|
74
|
-
* forensics
|
|
90
|
+
* forensics — the entire sample predates the #748/#749 fixes) so users
|
|
91
|
+
* can weigh chain mode against the parallel default.
|
|
75
92
|
*/
|
|
76
93
|
export interface CollisionAnnotations {
|
|
77
94
|
orderLines: string[];
|
|
@@ -27,6 +27,26 @@ export const EXCLUDED_PATHS = new Set([
|
|
|
27
27
|
"yarn.lock",
|
|
28
28
|
"pnpm-lock.yaml",
|
|
29
29
|
]);
|
|
30
|
+
/**
|
|
31
|
+
* Markdown H1/H2 section headings whose bodies are background/citation prose,
|
|
32
|
+
* not statements of intent-to-modify. A path named *only* under one of these
|
|
33
|
+
* is a reference to existing code, not a file the issue will touch — so the
|
|
34
|
+
* whole section is stripped before path extraction.
|
|
35
|
+
*
|
|
36
|
+
* Match is case-insensitive and prefix-based, so `## Motivation — concrete
|
|
37
|
+
* recent miss` and `## Additional context (see #533)` both match. This is the
|
|
38
|
+
* cheap version of the #556 proximity-weighting mitigation (#769): exclude
|
|
39
|
+
* background sections wholesale rather than scoring per-path distance to AC
|
|
40
|
+
* bullets. "A path appears only in background" ⟺ "the path is absent from the
|
|
41
|
+
* foreground", so stripping these sections up front subsumes per-path tagging.
|
|
42
|
+
*/
|
|
43
|
+
export const BACKGROUND_SECTIONS = new Set([
|
|
44
|
+
"references",
|
|
45
|
+
"context",
|
|
46
|
+
"motivation",
|
|
47
|
+
"additional context",
|
|
48
|
+
"see also",
|
|
49
|
+
]);
|
|
30
50
|
/**
|
|
31
51
|
* Slash-command names recognized as references to a skill's SKILL.md.
|
|
32
52
|
* Used by the slash-command-skill derivation rule when an issue body
|
|
@@ -83,10 +103,52 @@ const THREE_DIR_SYNC_PATTERN = /3[- ]dir(?:ectory)?\s+sync|across\s+all\s+three\
|
|
|
83
103
|
* single-backtick wrapper, so this gives us the "paths quoted as code in
|
|
84
104
|
* prose count, paths inside a code block don't" behavior the AC-5 guard
|
|
85
105
|
* specifies.
|
|
106
|
+
*
|
|
107
|
+
* Do NOT unify this with `dependency-markers.ts:stripCodeAndComments`, which
|
|
108
|
+
* looks nearly identical but additionally strips inline spans. The two have
|
|
109
|
+
* opposite requirements for the same syntax: there a backticked marker is a
|
|
110
|
+
* documentation example to discard, here a backticked path is the entire
|
|
111
|
+
* signal. Adding its inline-span strip to this function would delete every
|
|
112
|
+
* path PATH_REGEX is looking for and silently return an empty set.
|
|
86
113
|
*/
|
|
87
114
|
function stripCodeBlocksAndComments(body) {
|
|
88
115
|
return body.replace(/```[\s\S]*?```/g, "").replace(/<!--[\s\S]*?-->/g, "");
|
|
89
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Remove background/citation sections (see `BACKGROUND_SECTIONS`) so a path
|
|
119
|
+
* named only as a reference — under `## References`, `## Context`, etc. — is
|
|
120
|
+
* not counted as a modification target (#769).
|
|
121
|
+
*
|
|
122
|
+
* A section runs from its H1/H2 heading to the next H1/H2 heading (or EOF);
|
|
123
|
+
* `###`+ subsections belong to their parent section and are dropped with it.
|
|
124
|
+
* Only H1/H2 headings toggle sections — a `### Detail` inside a stripped
|
|
125
|
+
* `## Additional context` must not resurrect the rest of the section.
|
|
126
|
+
*
|
|
127
|
+
* Run this *after* `stripCodeBlocksAndComments` so a fenced block containing a
|
|
128
|
+
* `## `-prefixed line can't be misread as a real heading.
|
|
129
|
+
*/
|
|
130
|
+
function stripBackgroundSections(body) {
|
|
131
|
+
const kept = [];
|
|
132
|
+
let stripping = false;
|
|
133
|
+
for (const line of body.split("\n")) {
|
|
134
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
135
|
+
if (heading && heading[1].length <= 2) {
|
|
136
|
+
const text = heading[2].trim().toLowerCase();
|
|
137
|
+
stripping = false;
|
|
138
|
+
for (const section of BACKGROUND_SECTIONS) {
|
|
139
|
+
if (text.startsWith(section)) {
|
|
140
|
+
stripping = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (stripping)
|
|
145
|
+
continue; // drop the heading line itself
|
|
146
|
+
}
|
|
147
|
+
if (!stripping)
|
|
148
|
+
kept.push(line);
|
|
149
|
+
}
|
|
150
|
+
return kept.join("\n");
|
|
151
|
+
}
|
|
90
152
|
/**
|
|
91
153
|
* Collapse a fully-qualified skill-mirror path to its canonical bare form.
|
|
92
154
|
*
|
|
@@ -109,7 +171,9 @@ function normalizeSkillMirrorPath(path) {
|
|
|
109
171
|
* targets-of-modification.
|
|
110
172
|
*
|
|
111
173
|
* Strategy:
|
|
112
|
-
* 1. Strip fenced code blocks and HTML comments (AC-5 guard)
|
|
174
|
+
* 1. Strip fenced code blocks and HTML comments (AC-5 guard), then strip
|
|
175
|
+
* background/citation sections (#769) so a path named only under
|
|
176
|
+
* `## References`, `## Context`, etc. isn't counted as a target.
|
|
113
177
|
* 2. Pull every backtick-quoted path matching the source-tree regex,
|
|
114
178
|
* normalizing skill-mirror paths to their canonical bare form.
|
|
115
179
|
* 3. If the body mentions "3-dir sync", also pull bare
|
|
@@ -125,7 +189,7 @@ function normalizeSkillMirrorPath(path) {
|
|
|
125
189
|
*/
|
|
126
190
|
export function extractPathsFromIssueBody(body) {
|
|
127
191
|
const paths = new Set();
|
|
128
|
-
const cleaned = stripCodeBlocksAndComments(body);
|
|
192
|
+
const cleaned = stripBackgroundSections(stripCodeBlocksAndComments(body));
|
|
129
193
|
for (const m of cleaned.matchAll(PATH_REGEX)) {
|
|
130
194
|
paths.add(normalizeSkillMirrorPath(m[1]));
|
|
131
195
|
}
|
|
@@ -208,9 +272,9 @@ export function formatCollisionAnnotations(results) {
|
|
|
208
272
|
if (r.issues.length >= 3 && !chainSuggestion) {
|
|
209
273
|
const ids = r.issues.join(" ");
|
|
210
274
|
chainSuggestion =
|
|
211
|
-
`Chain: npx sequant run ${ids} --chain
|
|
275
|
+
`Chain: npx sequant run ${ids} --chain -Q ` +
|
|
212
276
|
`# alternative — ${r.issues.length} issues modify ${r.file} ` +
|
|
213
|
-
`(chain length≥3 historically 1/6 = 17
|
|
277
|
+
`(chain length≥3 historically 1/6 = 17%, predates the #748/#749 fixes; see docs/reference/chain-mode-analysis-2026-05.md)`;
|
|
214
278
|
}
|
|
215
279
|
}
|
|
216
280
|
return { orderLines, warnings, chainSuggestion };
|
|
@@ -19,6 +19,7 @@ import chalk from "chalk";
|
|
|
19
19
|
import logUpdate from "log-update";
|
|
20
20
|
import stringWidth from "string-width";
|
|
21
21
|
import { formatElapsedTime, formatTimestamp } from "./format.js";
|
|
22
|
+
import { pipelineHasFailed } from "../workflow/status-derivation.js";
|
|
22
23
|
const DEFAULT_LIVE_TICK_MS = 1000;
|
|
23
24
|
const DEFAULT_NON_TTY_HEARTBEAT_MS = 60_000;
|
|
24
25
|
const NARROW_TERMINAL_THRESHOLD = 80;
|
|
@@ -248,8 +249,13 @@ class BaseRenderer {
|
|
|
248
249
|
else if (phase.startedAt !== undefined) {
|
|
249
250
|
phase.durationMs = this.now() - phase.startedAt;
|
|
250
251
|
}
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
// #766: derive from the phase slots (loop excluded) instead of pinning
|
|
253
|
+
// `failed`, so a loop failure on an early quality-loop iteration doesn't
|
|
254
|
+
// stick after a later iteration recovers. Mirrors the orchestrator's card.
|
|
255
|
+
const nowFailed = pipelineHasFailed(state.phases);
|
|
256
|
+
state.status = nowFailed ? "failed" : "running";
|
|
257
|
+
if (nowFailed)
|
|
258
|
+
state.completedAt = this.now();
|
|
253
259
|
state.currentPhase = undefined;
|
|
254
260
|
if (event.error !== undefined)
|
|
255
261
|
state.failureReason = event.error;
|
|
@@ -267,20 +273,22 @@ class BaseRenderer {
|
|
|
267
273
|
}
|
|
268
274
|
/** Mark an issue done after PR is recorded — derived from phase completion. */
|
|
269
275
|
maybeMarkIssueDone(state) {
|
|
270
|
-
if (state.status === "failed")
|
|
271
|
-
return;
|
|
272
276
|
const allTerminal = state.phases.every((p) => p.status === "done" || p.status === "failed");
|
|
273
277
|
if (allTerminal && state.phases.length > 0) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
// #766: derive the verdict (loop excluded) so a run that failed the loop
|
|
279
|
+
// on an early iteration and then recovered every planned phase resolves
|
|
280
|
+
// to `done`. No early `failed` guard: a stale loop failure must be able
|
|
281
|
+
// to de-escalate once the pipeline recovers.
|
|
282
|
+
state.status = pipelineHasFailed(state.phases) ? "failed" : "done";
|
|
277
283
|
state.completedAt = this.now();
|
|
278
284
|
}
|
|
279
285
|
}
|
|
280
286
|
// ------------ Hooks for subclasses ------------
|
|
281
287
|
afterEvent(_event, state) {
|
|
282
|
-
|
|
283
|
-
|
|
288
|
+
// #766: always re-derive — `maybeMarkIssueDone` guards internally and must
|
|
289
|
+
// run even when `state.status` is currently `failed` so a recovered loop
|
|
290
|
+
// failure can de-escalate to `done`.
|
|
291
|
+
this.maybeMarkIssueDone(state);
|
|
284
292
|
this.afterStateChange();
|
|
285
293
|
}
|
|
286
294
|
afterStateChange() {
|
package/dist/src/lib/errors.d.ts
CHANGED
|
@@ -86,6 +86,97 @@ export declare class SubprocessError extends SequantError {
|
|
|
86
86
|
readonly metadata: SubprocessErrorMetadata;
|
|
87
87
|
constructor(message: string, metadata?: SubprocessErrorMetadata, cause?: Error);
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Metadata carried by {@link RateLimitError} / {@link BillingError}.
|
|
91
|
+
*
|
|
92
|
+
* Fields mirror the structured signals the Claude Agent SDK emits via
|
|
93
|
+
* `rate_limit_event` (`SDKRateLimitInfo`). The `canUserPurchaseCredits` /
|
|
94
|
+
* `hasChargeableSavedPaymentMethod` fields arrived in SDK 0.3.181 and are
|
|
95
|
+
* optional so older streams (or absent fields) degrade gracefully.
|
|
96
|
+
*/
|
|
97
|
+
export interface RateLimitMetadata {
|
|
98
|
+
[key: string]: unknown;
|
|
99
|
+
/** Unix timestamp (seconds or ms) at which the limit resets. */
|
|
100
|
+
resetsAt?: number;
|
|
101
|
+
/** Which limit window was hit (five_hour, seven_day, overage, …). */
|
|
102
|
+
rateLimitType?: string;
|
|
103
|
+
/** Why overage/billing was disabled (e.g. `out_of_credits`). */
|
|
104
|
+
overageDisabledReason?: string;
|
|
105
|
+
/** SDK error code; `credits_required` indicates a billing failure. */
|
|
106
|
+
errorCode?: string;
|
|
107
|
+
/** Whether the user can self-serve purchase credits (≥0.3.181). */
|
|
108
|
+
canUserPurchaseCredits?: boolean;
|
|
109
|
+
/** Whether a chargeable payment method is on file (≥0.3.181). */
|
|
110
|
+
hasChargeableSavedPaymentMethod?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Transient rate-limit error (HTTP 429-style throttle, overloaded API).
|
|
114
|
+
*
|
|
115
|
+
* Retryable: waiting and re-running can succeed once the limit window resets.
|
|
116
|
+
*/
|
|
117
|
+
export declare class RateLimitError extends SequantError {
|
|
118
|
+
readonly metadata: RateLimitMetadata;
|
|
119
|
+
constructor(message: string, metadata?: RateLimitMetadata, cause?: Error);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Billing / out-of-credits error.
|
|
123
|
+
*
|
|
124
|
+
* NOT retryable: a no-MCP retry (or any retry) cannot refill credits, so the
|
|
125
|
+
* executor must surface the real cause instead of looping. Drives the #592
|
|
126
|
+
* fallback-noise skip in phase-executor.
|
|
127
|
+
*/
|
|
128
|
+
export declare class BillingError extends SequantError {
|
|
129
|
+
readonly metadata: RateLimitMetadata;
|
|
130
|
+
constructor(message: string, metadata?: RateLimitMetadata, cause?: Error);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Structural subset of the SDK's `SDKRateLimitInfo` consumed when building a
|
|
134
|
+
* rate-limit error. Declared here (not imported from the SDK) so `errors.ts`
|
|
135
|
+
* stays SDK-free — only the driver owns the `@anthropic-ai/claude-agent-sdk`
|
|
136
|
+
* import.
|
|
137
|
+
*/
|
|
138
|
+
export interface RateLimitInfoLike {
|
|
139
|
+
status?: string;
|
|
140
|
+
resetsAt?: number;
|
|
141
|
+
rateLimitType?: string;
|
|
142
|
+
overageDisabledReason?: string;
|
|
143
|
+
errorCode?: string;
|
|
144
|
+
canUserPurchaseCredits?: boolean;
|
|
145
|
+
hasChargeableSavedPaymentMethod?: boolean;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* True when the rate-limit info represents a billing/credits failure (which a
|
|
149
|
+
* retry cannot fix), rather than a transient throttle.
|
|
150
|
+
*/
|
|
151
|
+
export declare function isBillingFailure(info: RateLimitInfoLike): boolean;
|
|
152
|
+
/**
|
|
153
|
+
* True when the rate-limit info represents an actual failure (rejection or
|
|
154
|
+
* billing), as opposed to an informational `allowed` / `allowed_warning`
|
|
155
|
+
* event. The driver uses this to avoid mis-attributing a stale warning event
|
|
156
|
+
* to an unrelated phase failure.
|
|
157
|
+
*/
|
|
158
|
+
export declare function isRateLimitFailureInfo(info: RateLimitInfoLike): boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Normalize a `resetsAt` timestamp to milliseconds. The SDK does not pin the
|
|
161
|
+
* unit, so use the same heuristic everywhere a `resetsAt` is compared or
|
|
162
|
+
* displayed: values below ~1e12 are seconds, otherwise milliseconds.
|
|
163
|
+
*/
|
|
164
|
+
export declare function resetsAtToMs(resetsAt: number): number;
|
|
165
|
+
/**
|
|
166
|
+
* Build a user-facing message from rate-limit info, naming the real cause:
|
|
167
|
+
* - billing/credits → "Out of credits" (enriched with purchasable vs hard
|
|
168
|
+
* limit when the ≥0.3.181 `canUserPurchaseCredits` field is present)
|
|
169
|
+
* - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
|
|
170
|
+
* "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
|
|
171
|
+
* `resetsAt` is absent)
|
|
172
|
+
*/
|
|
173
|
+
export declare function formatRateLimitMessage(info: RateLimitInfoLike): string;
|
|
174
|
+
/**
|
|
175
|
+
* Construct the appropriate typed error from structured rate-limit info.
|
|
176
|
+
* Billing/credits failures become a non-retryable {@link BillingError};
|
|
177
|
+
* transient throttles become a retryable {@link RateLimitError}.
|
|
178
|
+
*/
|
|
179
|
+
export declare function createRateLimitError(info: RateLimitInfoLike): RateLimitError | BillingError;
|
|
89
180
|
/**
|
|
90
181
|
* Map of error type names to their constructors.
|
|
91
182
|
* Used for deserialization from logs.
|
package/dist/src/lib/errors.js
CHANGED
|
@@ -82,6 +82,122 @@ export class SubprocessError extends SequantError {
|
|
|
82
82
|
this.name = "SubprocessError";
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Transient rate-limit error (HTTP 429-style throttle, overloaded API).
|
|
87
|
+
*
|
|
88
|
+
* Retryable: waiting and re-running can succeed once the limit window resets.
|
|
89
|
+
*/
|
|
90
|
+
export class RateLimitError extends SequantError {
|
|
91
|
+
constructor(message, metadata = {}, cause) {
|
|
92
|
+
super(message, { isRetryable: true, metadata, cause });
|
|
93
|
+
this.name = "RateLimitError";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Billing / out-of-credits error.
|
|
98
|
+
*
|
|
99
|
+
* NOT retryable: a no-MCP retry (or any retry) cannot refill credits, so the
|
|
100
|
+
* executor must surface the real cause instead of looping. Drives the #592
|
|
101
|
+
* fallback-noise skip in phase-executor.
|
|
102
|
+
*/
|
|
103
|
+
export class BillingError extends SequantError {
|
|
104
|
+
constructor(message, metadata = {}, cause) {
|
|
105
|
+
super(message, { isRetryable: false, metadata, cause });
|
|
106
|
+
this.name = "BillingError";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* True when the rate-limit info represents a billing/credits failure (which a
|
|
111
|
+
* retry cannot fix), rather than a transient throttle.
|
|
112
|
+
*/
|
|
113
|
+
export function isBillingFailure(info) {
|
|
114
|
+
return (info.errorCode === "credits_required" ||
|
|
115
|
+
info.overageDisabledReason === "out_of_credits");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* True when the rate-limit info represents an actual failure (rejection or
|
|
119
|
+
* billing), as opposed to an informational `allowed` / `allowed_warning`
|
|
120
|
+
* event. The driver uses this to avoid mis-attributing a stale warning event
|
|
121
|
+
* to an unrelated phase failure.
|
|
122
|
+
*/
|
|
123
|
+
export function isRateLimitFailureInfo(info) {
|
|
124
|
+
return info.status === "rejected" || isBillingFailure(info);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Normalize a `resetsAt` timestamp to milliseconds. The SDK does not pin the
|
|
128
|
+
* unit, so use the same heuristic everywhere a `resetsAt` is compared or
|
|
129
|
+
* displayed: values below ~1e12 are seconds, otherwise milliseconds.
|
|
130
|
+
*/
|
|
131
|
+
export function resetsAtToMs(resetsAt) {
|
|
132
|
+
return resetsAt < 1e12 ? resetsAt * 1000 : resetsAt;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Format a Unix timestamp (seconds or ms) as a local time string.
|
|
136
|
+
*
|
|
137
|
+
* Bare `HH:MM` when the reset falls on the current local calendar day;
|
|
138
|
+
* date-qualified `MM-DD HH:MM` otherwise. Multi-day windows
|
|
139
|
+
* (`rateLimitType: seven_day*`) can reset days out — a bare `HH:MM` there reads
|
|
140
|
+
* as "later today" and misleads the user (#732 QA follow-up), so the date is
|
|
141
|
+
* included whenever the reset is not today.
|
|
142
|
+
*/
|
|
143
|
+
function formatResetTime(resetsAt) {
|
|
144
|
+
const ms = resetsAtToMs(resetsAt);
|
|
145
|
+
const d = new Date(ms);
|
|
146
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
147
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
148
|
+
const now = new Date();
|
|
149
|
+
const sameDay = d.getFullYear() === now.getFullYear() &&
|
|
150
|
+
d.getMonth() === now.getMonth() &&
|
|
151
|
+
d.getDate() === now.getDate();
|
|
152
|
+
if (sameDay) {
|
|
153
|
+
return `${hh}:${mm}`;
|
|
154
|
+
}
|
|
155
|
+
const mon = String(d.getMonth() + 1).padStart(2, "0");
|
|
156
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
157
|
+
return `${mon}-${day} ${hh}:${mm}`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build a user-facing message from rate-limit info, naming the real cause:
|
|
161
|
+
* - billing/credits → "Out of credits" (enriched with purchasable vs hard
|
|
162
|
+
* limit when the ≥0.3.181 `canUserPurchaseCredits` field is present)
|
|
163
|
+
* - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
|
|
164
|
+
* "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
|
|
165
|
+
* `resetsAt` is absent)
|
|
166
|
+
*/
|
|
167
|
+
export function formatRateLimitMessage(info) {
|
|
168
|
+
if (isBillingFailure(info)) {
|
|
169
|
+
if (info.canUserPurchaseCredits === true) {
|
|
170
|
+
return "Out of credits — purchasable";
|
|
171
|
+
}
|
|
172
|
+
if (info.canUserPurchaseCredits === false) {
|
|
173
|
+
return "Out of credits — hard limit";
|
|
174
|
+
}
|
|
175
|
+
return "Out of credits";
|
|
176
|
+
}
|
|
177
|
+
if (info.resetsAt !== undefined) {
|
|
178
|
+
return `Rate limited — resets at ${formatResetTime(info.resetsAt)}`;
|
|
179
|
+
}
|
|
180
|
+
return "Rate limited";
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Construct the appropriate typed error from structured rate-limit info.
|
|
184
|
+
* Billing/credits failures become a non-retryable {@link BillingError};
|
|
185
|
+
* transient throttles become a retryable {@link RateLimitError}.
|
|
186
|
+
*/
|
|
187
|
+
export function createRateLimitError(info) {
|
|
188
|
+
const message = formatRateLimitMessage(info);
|
|
189
|
+
const metadata = {
|
|
190
|
+
resetsAt: info.resetsAt,
|
|
191
|
+
rateLimitType: info.rateLimitType,
|
|
192
|
+
overageDisabledReason: info.overageDisabledReason,
|
|
193
|
+
errorCode: info.errorCode,
|
|
194
|
+
canUserPurchaseCredits: info.canUserPurchaseCredits,
|
|
195
|
+
hasChargeableSavedPaymentMethod: info.hasChargeableSavedPaymentMethod,
|
|
196
|
+
};
|
|
197
|
+
return isBillingFailure(info)
|
|
198
|
+
? new BillingError(message, metadata)
|
|
199
|
+
: new RateLimitError(message, metadata);
|
|
200
|
+
}
|
|
85
201
|
/**
|
|
86
202
|
* Map of error type names to their constructors.
|
|
87
203
|
* Used for deserialization from logs.
|
|
@@ -94,4 +210,6 @@ export const ERROR_TYPE_MAP = {
|
|
|
94
210
|
BuildError: BuildError,
|
|
95
211
|
TimeoutError: TimeoutError,
|
|
96
212
|
SubprocessError: SubprocessError,
|
|
213
|
+
RateLimitError: RateLimitError,
|
|
214
|
+
BillingError: BillingError,
|
|
97
215
|
};
|
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;
|
|
@@ -114,6 +114,25 @@ export declare function fetchLatestVersion(): Promise<string | null>;
|
|
|
114
114
|
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
|
115
115
|
*/
|
|
116
116
|
export declare function compareVersions(a: string, b: string): number;
|
|
117
|
+
/**
|
|
118
|
+
* Pure preflight check for the running Node version against the engines floor.
|
|
119
|
+
*
|
|
120
|
+
* Returns an actionable, multi-line message when `current` is below `floor`,
|
|
121
|
+
* or `null` when it satisfies the floor (or when `floor` is missing/unparseable,
|
|
122
|
+
* in which case the guard is skipped rather than crashing the CLI).
|
|
123
|
+
*
|
|
124
|
+
* `floor` is the raw `engines.node` value (e.g. ">=22.12.0"); the leading range
|
|
125
|
+
* operator is stripped before comparison. Reuses {@link compareVersions} — no
|
|
126
|
+
* `semver` dependency.
|
|
127
|
+
*/
|
|
128
|
+
export declare function getNodeVersionError(current: string, floor: string | null | undefined): string | null;
|
|
129
|
+
/**
|
|
130
|
+
* Side-effecting wrapper around {@link getNodeVersionError}: prints the message
|
|
131
|
+
* and exits non-zero when the running Node is below the floor. Uses only
|
|
132
|
+
* built-in globals (`process.version`, `console`, `process.exit`) so it runs —
|
|
133
|
+
* rather than crashes — on the old Node it rejects.
|
|
134
|
+
*/
|
|
135
|
+
export declare function assertNodeVersion(floor: string | null | undefined): void;
|
|
117
136
|
/**
|
|
118
137
|
* Check if the current version is outdated
|
|
119
138
|
*/
|
|
@@ -214,16 +214,13 @@ export function isCacheFresh(cache) {
|
|
|
214
214
|
* Fetch the latest version from npm registry with timeout
|
|
215
215
|
*/
|
|
216
216
|
export async function fetchLatestVersion() {
|
|
217
|
-
const controller = new AbortController();
|
|
218
|
-
const timeoutId = setTimeout(() => controller.abort(), VERSION_CHECK_TIMEOUT);
|
|
219
217
|
try {
|
|
220
218
|
const response = await fetch(NPM_REGISTRY_URL, {
|
|
221
|
-
signal:
|
|
219
|
+
signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT),
|
|
222
220
|
headers: {
|
|
223
221
|
Accept: "application/json",
|
|
224
222
|
},
|
|
225
223
|
});
|
|
226
|
-
clearTimeout(timeoutId);
|
|
227
224
|
if (!response.ok) {
|
|
228
225
|
return null;
|
|
229
226
|
}
|
|
@@ -231,7 +228,6 @@ export async function fetchLatestVersion() {
|
|
|
231
228
|
return data.version || null;
|
|
232
229
|
}
|
|
233
230
|
catch {
|
|
234
|
-
clearTimeout(timeoutId);
|
|
235
231
|
return null;
|
|
236
232
|
}
|
|
237
233
|
}
|
|
@@ -258,6 +254,50 @@ export function compareVersions(a, b) {
|
|
|
258
254
|
}
|
|
259
255
|
return 0;
|
|
260
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Pure preflight check for the running Node version against the engines floor.
|
|
259
|
+
*
|
|
260
|
+
* Returns an actionable, multi-line message when `current` is below `floor`,
|
|
261
|
+
* or `null` when it satisfies the floor (or when `floor` is missing/unparseable,
|
|
262
|
+
* in which case the guard is skipped rather than crashing the CLI).
|
|
263
|
+
*
|
|
264
|
+
* `floor` is the raw `engines.node` value (e.g. ">=22.12.0"); the leading range
|
|
265
|
+
* operator is stripped before comparison. Reuses {@link compareVersions} — no
|
|
266
|
+
* `semver` dependency.
|
|
267
|
+
*/
|
|
268
|
+
export function getNodeVersionError(current, floor) {
|
|
269
|
+
// Strip any range operator (">=", "^", "~", etc.) from the floor.
|
|
270
|
+
const normalizedFloor = (floor ?? "").replace(/^[^\d]*/, "");
|
|
271
|
+
// No usable floor → skip the guard (metadata problem must not crash the CLI).
|
|
272
|
+
if (!/^\d/.test(normalizedFloor)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (compareVersions(current, normalizedFloor) >= 0) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const currentClean = current.replace(/^v/, "");
|
|
279
|
+
return [
|
|
280
|
+
`Sequant requires Node.js >=${normalizedFloor}, but you are running ${currentClean}.`,
|
|
281
|
+
"",
|
|
282
|
+
"Upgrade Node, then re-run:",
|
|
283
|
+
" • fnm: fnm install 22 && fnm use 22",
|
|
284
|
+
" • nvm: nvm install 22 && nvm use 22",
|
|
285
|
+
" • or download: https://nodejs.org/en/download",
|
|
286
|
+
].join("\n");
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Side-effecting wrapper around {@link getNodeVersionError}: prints the message
|
|
290
|
+
* and exits non-zero when the running Node is below the floor. Uses only
|
|
291
|
+
* built-in globals (`process.version`, `console`, `process.exit`) so it runs —
|
|
292
|
+
* rather than crashes — on the old Node it rejects.
|
|
293
|
+
*/
|
|
294
|
+
export function assertNodeVersion(floor) {
|
|
295
|
+
const error = getNodeVersionError(process.version, floor);
|
|
296
|
+
if (error) {
|
|
297
|
+
console.error(error);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
261
301
|
/**
|
|
262
302
|
* Check if the current version is outdated
|
|
263
303
|
*/
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* creation).
|
|
9
9
|
*/
|
|
10
10
|
import { ExecutionConfig, PhaseResult, IssueResult, type RunOptions, type IssueExecutionContext, type BatchExecutionContext, type ProgressCallback } from "./types.js";
|
|
11
|
+
import { type ErrorCategory } from "./error-classifier.js";
|
|
11
12
|
export type { RunOptions, ProgressCallback, IssueExecutionContext, BatchExecutionContext, } from "./types.js";
|
|
12
13
|
/**
|
|
13
14
|
* Emit a structured progress line to stderr for MCP progress notifications.
|
|
@@ -73,4 +74,16 @@ export declare function parseBatches(batchArgs: string[]): number[][];
|
|
|
73
74
|
*/
|
|
74
75
|
export declare function getEnvConfig(): Partial<RunOptions>;
|
|
75
76
|
export declare function executeBatch(issueNumbers: number[], batchCtx: BatchExecutionContext): Promise<IssueResult[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Derive the bounded-enum failure category for a failed issue (#761 AC-7).
|
|
79
|
+
*
|
|
80
|
+
* Scans for the LAST non-loop failing phase — the same reverse scan
|
|
81
|
+
* `toIssueSummary` uses (#766), so the recorded category and the displayed
|
|
82
|
+
* failure reason describe the same attempt. Prefers the driver's structured
|
|
83
|
+
* cause over stderr-regex classification (#732). Returns only the enum value;
|
|
84
|
+
* message strings never leave this function (metrics privacy contract).
|
|
85
|
+
*
|
|
86
|
+
* @internal Exported for testing
|
|
87
|
+
*/
|
|
88
|
+
export declare function deriveFailureCategory(phaseResults: PhaseResult[]): ErrorCategory | undefined;
|
|
76
89
|
export declare function runIssueWithLogging(ctx: IssueExecutionContext): Promise<IssueResult>;
|