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 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 retried = 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.";
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 = 30;
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 waitSec = Math.min(response.retryAfterSec ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
2491
- input6.onRateLimit?.(waitSec);
2492
- await (input6.sleep ?? defaultSleep)(waitSec * 1e3);
2493
- response = await input6.provider.send(request);
2494
- if (response.usage) {
2495
- input6.budget.recordUsage(response.usage);
2496
- input6.onUsage?.(response.usage);
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.1");
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)"