squad-kit 0.2.0 → 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 +28 -0
- package/README.md +94 -183
- package/dist/cli.js +196 -28
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2372,7 +2372,92 @@ function looksBinary(buf) {
|
|
|
2372
2372
|
return false;
|
|
2373
2373
|
}
|
|
2374
2374
|
|
|
2375
|
+
// src/planner/provider-errors.ts
|
|
2376
|
+
function detectModelNotFound(provider, model, status, rawBody) {
|
|
2377
|
+
if (status !== 404) return void 0;
|
|
2378
|
+
const body = rawBody.toLowerCase();
|
|
2379
|
+
const hints = ["not_found_error", "model_not_found", "not_found", "model:"];
|
|
2380
|
+
if (!hints.some((h) => body.includes(h))) return void 0;
|
|
2381
|
+
return { provider, model, status, rawBody };
|
|
2382
|
+
}
|
|
2383
|
+
function modelNotFoundMessage(err) {
|
|
2384
|
+
return [
|
|
2385
|
+
`The ${err.provider} planner model "${err.model}" is no longer available.`,
|
|
2386
|
+
"",
|
|
2387
|
+
"Recovery options:",
|
|
2388
|
+
` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
|
|
2389
|
+
` 2. Run \`squad config set planner --provider openai\` (or another \`--provider\`) to switch models via a different provider, then save a key when prompted.`,
|
|
2390
|
+
` 3. Run \`squad config set planner\` to set \`planner.modelOverride.${err.provider}\` to a still-valid id (no raw YAML), then \`squad doctor\` to confirm.`,
|
|
2391
|
+
"",
|
|
2392
|
+
`Raw provider response: ${err.rawBody.slice(0, 200)}`
|
|
2393
|
+
].join("\n");
|
|
2394
|
+
}
|
|
2395
|
+
function detectRateLimit(provider, status, headers, rawBody) {
|
|
2396
|
+
if (status !== 429) return void 0;
|
|
2397
|
+
return {
|
|
2398
|
+
provider,
|
|
2399
|
+
status: 429,
|
|
2400
|
+
retryAfterSec: parseRetryAfter(headers, rawBody),
|
|
2401
|
+
rawBody
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
function parseRetryAfter(headers, body) {
|
|
2405
|
+
const raw = headerGet(headers, "retry-after");
|
|
2406
|
+
if (raw) {
|
|
2407
|
+
const sec = Number(raw);
|
|
2408
|
+
if (Number.isFinite(sec) && sec >= 0) return Math.ceil(sec);
|
|
2409
|
+
const asDate = Date.parse(raw);
|
|
2410
|
+
if (Number.isFinite(asDate)) {
|
|
2411
|
+
return Math.max(0, Math.ceil((asDate - Date.now()) / 1e3));
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
const bodyMatch = /"retryDelay"\s*:\s*"(\d+)(?:\.\d+)?s"/i.exec(body);
|
|
2415
|
+
if (bodyMatch) {
|
|
2416
|
+
const sec = Number(bodyMatch[1]);
|
|
2417
|
+
if (Number.isFinite(sec) && sec >= 0) return sec;
|
|
2418
|
+
}
|
|
2419
|
+
return void 0;
|
|
2420
|
+
}
|
|
2421
|
+
function headerGet(h, name) {
|
|
2422
|
+
if (typeof h.get === "function") {
|
|
2423
|
+
return h.get(name);
|
|
2424
|
+
}
|
|
2425
|
+
const key = name.toLowerCase();
|
|
2426
|
+
const rec = h;
|
|
2427
|
+
for (const k of Object.keys(rec)) {
|
|
2428
|
+
if (k.toLowerCase() === key) return rec[k] ?? null;
|
|
2429
|
+
}
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
function rateLimitMessage(err) {
|
|
2433
|
+
const waitHint = err.retryAfterSec ?? 60;
|
|
2434
|
+
const limitsUrl = {
|
|
2435
|
+
anthropic: "https://console.anthropic.com/settings/limits",
|
|
2436
|
+
openai: "https://platform.openai.com/settings/organization/limits",
|
|
2437
|
+
google: "https://aistudio.google.com/app/plan_information"
|
|
2438
|
+
};
|
|
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 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.";
|
|
2442
|
+
return [
|
|
2443
|
+
headline,
|
|
2444
|
+
retried,
|
|
2445
|
+
"",
|
|
2446
|
+
"Recovery options:",
|
|
2447
|
+
` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
|
|
2448
|
+
` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
|
|
2449
|
+
` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
|
|
2450
|
+
` 4. Upgrade your ${err.provider} tier: ${limitsUrl[err.provider]}.`,
|
|
2451
|
+
"",
|
|
2452
|
+
"Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
|
|
2453
|
+
"",
|
|
2454
|
+
`Raw provider response: ${err.rawBody.slice(0, 200)}`
|
|
2455
|
+
].join("\n");
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2375
2458
|
// src/planner/loop.ts
|
|
2459
|
+
var MAX_RATE_LIMIT_RETRY_SEC = 90;
|
|
2460
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2376
2461
|
async function runPlanner(input6) {
|
|
2377
2462
|
const maxIter = input6.maxIterations ?? 40;
|
|
2378
2463
|
const turns = [{ role: "user", text: input6.userPrompt }];
|
|
@@ -2388,21 +2473,39 @@ async function runPlanner(input6) {
|
|
|
2388
2473
|
if (input6.budget.overCost()) {
|
|
2389
2474
|
return { planText: accumulatedText, budgetExhausted: true, timedOut: false, finishedNormally: false, iterations };
|
|
2390
2475
|
}
|
|
2391
|
-
const
|
|
2476
|
+
const request = {
|
|
2392
2477
|
systemPrompt: input6.systemPrompt,
|
|
2393
2478
|
model: input6.model,
|
|
2394
2479
|
tools: [READ_FILE_TOOL],
|
|
2395
2480
|
turns,
|
|
2396
2481
|
apiKey: input6.apiKey,
|
|
2397
2482
|
maxOutputTokens: 8192
|
|
2398
|
-
}
|
|
2483
|
+
};
|
|
2484
|
+
let response = await input6.provider.send(request);
|
|
2399
2485
|
if (response.usage) {
|
|
2400
2486
|
input6.budget.recordUsage(response.usage);
|
|
2401
2487
|
input6.onUsage?.(response.usage);
|
|
2402
2488
|
}
|
|
2489
|
+
let retriedRateLimit = false;
|
|
2490
|
+
let retrySkippedReason;
|
|
2491
|
+
if (response.stopReason === "error" && response.errorKind === "rate_limit") {
|
|
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;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2403
2507
|
if (response.stopReason === "error") {
|
|
2404
|
-
|
|
2405
|
-
throw new Error(`${base} Run \`squad doctor\` to verify models and credentials.`);
|
|
2508
|
+
throw composePlannerError(input6.provider.name, response, retriedRateLimit, retrySkippedReason);
|
|
2406
2509
|
}
|
|
2407
2510
|
if (response.text) {
|
|
2408
2511
|
accumulatedText += response.text;
|
|
@@ -2445,26 +2548,24 @@ async function runPlanner(input6) {
|
|
|
2445
2548
|
}
|
|
2446
2549
|
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally: false, iterations };
|
|
2447
2550
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
`Raw provider response: ${err.rawBody.slice(0, 200)}`
|
|
2467
|
-
].join("\n");
|
|
2551
|
+
function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
|
|
2552
|
+
if (response.errorKind === "rate_limit") {
|
|
2553
|
+
return new Error(
|
|
2554
|
+
rateLimitMessage({
|
|
2555
|
+
provider: providerName,
|
|
2556
|
+
retryAfterSec: response.retryAfterSec,
|
|
2557
|
+
rawBody: response.rawError ?? "",
|
|
2558
|
+
retryAlreadyAttempted: retriedRateLimit,
|
|
2559
|
+
retrySkippedReason,
|
|
2560
|
+
maxRetrySec: MAX_RATE_LIMIT_RETRY_SEC
|
|
2561
|
+
})
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
if (response.errorKind === "model_not_found") {
|
|
2565
|
+
return new Error(response.rawError ?? "planner: model not found");
|
|
2566
|
+
}
|
|
2567
|
+
const base = response.rawError ?? "planner: provider error";
|
|
2568
|
+
return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
|
|
2468
2569
|
}
|
|
2469
2570
|
|
|
2470
2571
|
// src/planner/providers/anthropic.ts
|
|
@@ -2497,7 +2598,20 @@ async function callAnthropic(req) {
|
|
|
2497
2598
|
const text = await res.text().catch(() => "");
|
|
2498
2599
|
const nf = detectModelNotFound("anthropic", req.model, res.status, text);
|
|
2499
2600
|
if (nf) {
|
|
2500
|
-
return {
|
|
2601
|
+
return {
|
|
2602
|
+
stopReason: "error",
|
|
2603
|
+
rawError: modelNotFoundMessage(nf),
|
|
2604
|
+
errorKind: "model_not_found"
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
const rl = detectRateLimit("anthropic", res.status, res.headers, text);
|
|
2608
|
+
if (rl) {
|
|
2609
|
+
return {
|
|
2610
|
+
stopReason: "error",
|
|
2611
|
+
rawError: `anthropic 429: ${text.slice(0, 500)}`,
|
|
2612
|
+
errorKind: "rate_limit",
|
|
2613
|
+
retryAfterSec: rl.retryAfterSec
|
|
2614
|
+
};
|
|
2501
2615
|
}
|
|
2502
2616
|
return { stopReason: "error", rawError: `anthropic ${res.status}: ${text.slice(0, 500)}` };
|
|
2503
2617
|
}
|
|
@@ -2567,7 +2681,20 @@ async function callOpenAI(req) {
|
|
|
2567
2681
|
const text = await res.text().catch(() => "");
|
|
2568
2682
|
const nf = detectModelNotFound("openai", req.model, res.status, text);
|
|
2569
2683
|
if (nf) {
|
|
2570
|
-
return {
|
|
2684
|
+
return {
|
|
2685
|
+
stopReason: "error",
|
|
2686
|
+
rawError: modelNotFoundMessage(nf),
|
|
2687
|
+
errorKind: "model_not_found"
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
const rl = detectRateLimit("openai", res.status, res.headers, text);
|
|
2691
|
+
if (rl) {
|
|
2692
|
+
return {
|
|
2693
|
+
stopReason: "error",
|
|
2694
|
+
rawError: `openai 429: ${text.slice(0, 500)}`,
|
|
2695
|
+
errorKind: "rate_limit",
|
|
2696
|
+
retryAfterSec: rl.retryAfterSec
|
|
2697
|
+
};
|
|
2571
2698
|
}
|
|
2572
2699
|
return { stopReason: "error", rawError: `openai ${res.status}: ${text.slice(0, 500)}` };
|
|
2573
2700
|
}
|
|
@@ -2653,7 +2780,20 @@ async function callGoogle(req) {
|
|
|
2653
2780
|
const text = await res.text().catch(() => "");
|
|
2654
2781
|
const nf = detectModelNotFound("google", req.model, res.status, text);
|
|
2655
2782
|
if (nf) {
|
|
2656
|
-
return {
|
|
2783
|
+
return {
|
|
2784
|
+
stopReason: "error",
|
|
2785
|
+
rawError: modelNotFoundMessage(nf),
|
|
2786
|
+
errorKind: "model_not_found"
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
const rl = detectRateLimit("google", res.status, res.headers, text);
|
|
2790
|
+
if (rl) {
|
|
2791
|
+
return {
|
|
2792
|
+
stopReason: "error",
|
|
2793
|
+
rawError: `google 429: ${text.slice(0, 500)}`,
|
|
2794
|
+
errorKind: "rate_limit",
|
|
2795
|
+
retryAfterSec: rl.retryAfterSec
|
|
2796
|
+
};
|
|
2657
2797
|
}
|
|
2658
2798
|
return { stopReason: "error", rawError: `google ${res.status}: ${text.slice(0, 500)}` };
|
|
2659
2799
|
}
|
|
@@ -3132,6 +3272,11 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3132
3272
|
onAssistantText: () => {
|
|
3133
3273
|
sessionSpinner.current?.succeed("planner thinking complete (this chunk)");
|
|
3134
3274
|
sessionSpinner.current = spinner("thinking\u2026");
|
|
3275
|
+
},
|
|
3276
|
+
onRateLimit: (waitSec) => {
|
|
3277
|
+
sessionSpinner.current?.stop();
|
|
3278
|
+
warning(`${planner.provider} rate limit hit \u2014 retrying in ${waitSec}s`);
|
|
3279
|
+
sessionSpinner.current = spinner("waiting for rate limit to reset\u2026");
|
|
3135
3280
|
}
|
|
3136
3281
|
});
|
|
3137
3282
|
sessionSpinner.current?.stop();
|
|
@@ -3690,6 +3835,28 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
3690
3835
|
};
|
|
3691
3836
|
}
|
|
3692
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
|
+
}
|
|
3693
3860
|
async function checkTrackerConfig(_paths, ctx) {
|
|
3694
3861
|
if (!ctx.config) {
|
|
3695
3862
|
return { id: "tracker-config", name: "tracker configuration", status: "skip", detail: "config unavailable" };
|
|
@@ -3811,6 +3978,7 @@ async function runAllChecks(paths, ctx, fix) {
|
|
|
3811
3978
|
await add("planner configuration", () => checkPlannerConfig(paths, ctx));
|
|
3812
3979
|
await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
|
|
3813
3980
|
await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
|
|
3981
|
+
await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
|
|
3814
3982
|
await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
|
|
3815
3983
|
await add("tracker credential resolves", () => checkTrackerCredential(paths, ctx));
|
|
3816
3984
|
await add("tracker connectivity", () => checkTrackerConnectivity(paths, ctx));
|
|
@@ -4704,7 +4872,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
4704
4872
|
|
|
4705
4873
|
// src/cli.ts
|
|
4706
4874
|
var program = new Command();
|
|
4707
|
-
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");
|
|
4708
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(
|
|
4709
4877
|
"--tracker-workspace <hostOrOrg>",
|
|
4710
4878
|
"Jira host or Azure org (non-interactive default; also written to config for jira/azure)"
|