squad-kit 0.2.1 → 0.2.2
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/CHANGELOG.md +13 -0
- package/dist/cli.js +46 -14
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.2] - 2026-04-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Rate-limit retry no longer burns a doomed request.** 0.2.1 capped the auto-retry wait at 30 s. When a provider asked for longer than that (common on Anthropic Tier 1, which regularly asks for 60–180 s), squad-kit would retry *inside the same throttle window* and fail a second time. 0.2.2:
|
|
15
|
+
- Raises the retry cap from 30 s to **90 s**, which comfortably covers the provider-suggested wait for most Tier 1 / free-tier 429s.
|
|
16
|
+
- **Skips the retry entirely** when `Retry-After` is longer than the 90 s cap, and surfaces a dedicated message explaining the decision (`squad-kit did not auto-retry: the provider's 132s wait is longer than our 90s cap, so retrying would just burn another request inside the same throttle window.`) instead of wasting a request and showing the "already retried" variant.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`squad doctor` now reports planner tier vs. model awareness** (check id `planner-tier`). When `planner.provider === 'anthropic'` and the resolved plan model id contains `opus`, the check emits a **warn** with a fix hint suggesting `squad config set planner` to pick Sonnet or Haiku, which live in separate per-model rate-limit buckets on Anthropic Tier 1 (10 k input tokens / minute). For non-Anthropic providers and non-Opus models, the check is `skip` or `ok` — no noise for users who aren't affected.
|
|
21
|
+
- `rateLimitMessage` gained a `retrySkippedReason` + `maxRetrySec` field, so the error text can distinguish "we retried and got throttled again" (firm over-quota) from "we did not retry because your wait is longer than our cap" (retry would be pointless). Both cases still surface the same four recovery options.
|
|
22
|
+
|
|
10
23
|
## [0.2.1] - 2026-04-22
|
|
11
24
|
|
|
12
25
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -2437,7 +2437,8 @@ function rateLimitMessage(err) {
|
|
|
2437
2437
|
google: "https://aistudio.google.com/app/plan_information"
|
|
2438
2438
|
};
|
|
2439
2439
|
const headline = err.retryAfterSec ? `${err.provider} rate limit hit \u2014 provider asked us to wait ${err.retryAfterSec}s before retrying.` : `${err.provider} rate limit hit.`;
|
|
2440
|
-
const
|
|
2440
|
+
const cap = err.maxRetrySec ?? 90;
|
|
2441
|
+
const retried = err.retrySkippedReason === "retry_after_too_long" ? `squad-kit did not auto-retry: the provider's ${err.retryAfterSec}s wait is longer than our ${cap}s cap, so retrying would just burn another request inside the same throttle window.` : err.retryAlreadyAttempted ? "squad-kit already retried once automatically and was throttled again \u2014 your org is firmly over its per-minute quota." : "squad-kit aborted before retrying.";
|
|
2441
2442
|
return [
|
|
2442
2443
|
headline,
|
|
2443
2444
|
retried,
|
|
@@ -2455,7 +2456,7 @@ function rateLimitMessage(err) {
|
|
|
2455
2456
|
}
|
|
2456
2457
|
|
|
2457
2458
|
// src/planner/loop.ts
|
|
2458
|
-
var MAX_RATE_LIMIT_RETRY_SEC =
|
|
2459
|
+
var MAX_RATE_LIMIT_RETRY_SEC = 90;
|
|
2459
2460
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2460
2461
|
async function runPlanner(input6) {
|
|
2461
2462
|
const maxIter = input6.maxIterations ?? 40;
|
|
@@ -2486,19 +2487,25 @@ async function runPlanner(input6) {
|
|
|
2486
2487
|
input6.onUsage?.(response.usage);
|
|
2487
2488
|
}
|
|
2488
2489
|
let retriedRateLimit = false;
|
|
2490
|
+
let retrySkippedReason;
|
|
2489
2491
|
if (response.stopReason === "error" && response.errorKind === "rate_limit") {
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
input6.
|
|
2496
|
-
input6.
|
|
2492
|
+
const asked = response.retryAfterSec;
|
|
2493
|
+
if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
|
|
2494
|
+
retrySkippedReason = "retry_after_too_long";
|
|
2495
|
+
} else {
|
|
2496
|
+
const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
2497
|
+
input6.onRateLimit?.(waitSec);
|
|
2498
|
+
await (input6.sleep ?? defaultSleep)(waitSec * 1e3);
|
|
2499
|
+
response = await input6.provider.send(request);
|
|
2500
|
+
if (response.usage) {
|
|
2501
|
+
input6.budget.recordUsage(response.usage);
|
|
2502
|
+
input6.onUsage?.(response.usage);
|
|
2503
|
+
}
|
|
2504
|
+
retriedRateLimit = true;
|
|
2497
2505
|
}
|
|
2498
|
-
retriedRateLimit = true;
|
|
2499
2506
|
}
|
|
2500
2507
|
if (response.stopReason === "error") {
|
|
2501
|
-
throw composePlannerError(input6.provider.name, response, retriedRateLimit);
|
|
2508
|
+
throw composePlannerError(input6.provider.name, response, retriedRateLimit, retrySkippedReason);
|
|
2502
2509
|
}
|
|
2503
2510
|
if (response.text) {
|
|
2504
2511
|
accumulatedText += response.text;
|
|
@@ -2541,14 +2548,16 @@ async function runPlanner(input6) {
|
|
|
2541
2548
|
}
|
|
2542
2549
|
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally: false, iterations };
|
|
2543
2550
|
}
|
|
2544
|
-
function composePlannerError(providerName, response, retriedRateLimit) {
|
|
2551
|
+
function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
|
|
2545
2552
|
if (response.errorKind === "rate_limit") {
|
|
2546
2553
|
return new Error(
|
|
2547
2554
|
rateLimitMessage({
|
|
2548
2555
|
provider: providerName,
|
|
2549
2556
|
retryAfterSec: response.retryAfterSec,
|
|
2550
2557
|
rawBody: response.rawError ?? "",
|
|
2551
|
-
retryAlreadyAttempted: retriedRateLimit
|
|
2558
|
+
retryAlreadyAttempted: retriedRateLimit,
|
|
2559
|
+
retrySkippedReason,
|
|
2560
|
+
maxRetrySec: MAX_RATE_LIMIT_RETRY_SEC
|
|
2552
2561
|
})
|
|
2553
2562
|
);
|
|
2554
2563
|
}
|
|
@@ -3826,6 +3835,28 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
3826
3835
|
};
|
|
3827
3836
|
}
|
|
3828
3837
|
}
|
|
3838
|
+
async function checkPlannerTierAwareness(_paths, ctx) {
|
|
3839
|
+
const id = "planner-tier";
|
|
3840
|
+
const name = "planner tier vs. model";
|
|
3841
|
+
if (!ctx.config || ctx.config.planner?.enabled !== true) {
|
|
3842
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
3843
|
+
}
|
|
3844
|
+
const provider = ctx.config.planner.provider;
|
|
3845
|
+
if (provider !== "anthropic") {
|
|
3846
|
+
return { id, name, status: "skip", detail: `not applicable for ${provider}` };
|
|
3847
|
+
}
|
|
3848
|
+
const planModel = modelFor(provider, "plan", ctx.config.planner.modelOverride);
|
|
3849
|
+
if (!/opus/i.test(planModel)) {
|
|
3850
|
+
return { id, name, status: "ok", detail: `${planModel} is comfortably under Tier 1` };
|
|
3851
|
+
}
|
|
3852
|
+
return {
|
|
3853
|
+
id,
|
|
3854
|
+
name,
|
|
3855
|
+
status: "warn",
|
|
3856
|
+
detail: `${planModel} on Anthropic Tier 1 (10k input tokens/min) is throttle-prone for plans over ~3 file reads`,
|
|
3857
|
+
fixHint: "Run `squad config set planner` and pick a Sonnet or Haiku id for planner.modelOverride.anthropic, or upgrade tier at https://console.anthropic.com/settings/limits. squad-kit will auto-retry a 429 once (waiting up to 90s), but repeated throttling means the plan model is simply too big for your quota."
|
|
3858
|
+
};
|
|
3859
|
+
}
|
|
3829
3860
|
async function checkTrackerConfig(_paths, ctx) {
|
|
3830
3861
|
if (!ctx.config) {
|
|
3831
3862
|
return { id: "tracker-config", name: "tracker configuration", status: "skip", detail: "config unavailable" };
|
|
@@ -3947,6 +3978,7 @@ async function runAllChecks(paths, ctx, fix) {
|
|
|
3947
3978
|
await add("planner configuration", () => checkPlannerConfig(paths, ctx));
|
|
3948
3979
|
await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
|
|
3949
3980
|
await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
|
|
3981
|
+
await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
|
|
3950
3982
|
await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
|
|
3951
3983
|
await add("tracker credential resolves", () => checkTrackerCredential(paths, ctx));
|
|
3952
3984
|
await add("tracker connectivity", () => checkTrackerConnectivity(paths, ctx));
|
|
@@ -4840,7 +4872,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
4840
4872
|
|
|
4841
4873
|
// src/cli.ts
|
|
4842
4874
|
var program = new Command();
|
|
4843
|
-
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.2.
|
|
4875
|
+
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.2.2");
|
|
4844
4876
|
program.command("init").description("Bootstrap .squad/ in the current directory").option("--agents <list>", "Comma-separated agent list: claude-code,cursor,copilot,gemini").option("--tracker <type>", "Tracker type: none|github|linear|jira|azure").option(
|
|
4845
4877
|
"--tracker-workspace <hostOrOrg>",
|
|
4846
4878
|
"Jira host or Azure org (non-interactive default; also written to config for jira/azure)"
|