squad-kit 0.2.0 → 0.2.1

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,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.1] - 2026-04-22
11
+
12
+ ### Fixed
13
+
14
+ - **`squad new-plan --api` now handles provider rate limits (HTTP 429) properly.** Previously a 429 surfaced the generic `Run \`squad doctor\` to verify models and credentials.` hint, which is misleading — the credentials are fine, the org is just over its per-minute quota. `runPlanner` now:
15
+ - Detects 429s across Anthropic, OpenAI, and Google (new `errorKind: 'rate_limit'` on `ProviderResponse`).
16
+ - Parses `Retry-After` (seconds or HTTP-date) and Google's body-level `retryDelay` field.
17
+ - Retries **once** automatically, waiting the provider-requested time capped at 30 seconds. A new `onRateLimit(waitSec)` callback lets the UI show a "retrying in Ns" notice.
18
+ - When the retry also fails, throws a dedicated error that names the provider, summarises the wait time, and lists four concrete recovery paths: wait and retry, switch planner model (`squad config set planner`), tighten `planner.budget`, or upgrade tier (with a deep-link to each provider's limits page).
19
+ - The generic (non-429) provider-error hint now reads `Run \`squad doctor\` to diagnose, or retry — most 5xx errors are transient.`, which is accurate for the actual 5xx / network cases it covers.
20
+
21
+ ### Added
22
+
23
+ - `detectRateLimit()` and `rateLimitMessage()` in `src/planner/provider-errors.ts`. Both cover all three supported providers and are unit-tested with real-shaped 429 fixtures.
24
+
10
25
  ## [0.2.0] - 2026-04-22
11
26
 
12
27
  ### tracker-intake-fetch
package/README.md CHANGED
@@ -5,52 +5,41 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
6
  [![Website](https://img.shields.io/badge/site-squad--kit.netlify.app-7cffa0?labelColor=0a0a0c)](https://squad-kit.netlify.app)
7
7
 
8
- **Plan once, execute cheap.** A 3-step SDD-style workflow CLI for AI-assisted coding: **raw story → good plan → implementation**.
9
-
10
- Website & docs: **[squad-kit.netlify.app](https://squad-kit.netlify.app)**
11
-
12
-
13
- Your expensive model plans once. A cheap model executes many times. Squad-kit owns the folder conventions, the plan meta-prompt, and the agent slash-commands so the token cost goes where it pays off.
8
+ **Plan once, execute cheap.** A 3-step SDD-style workflow CLI for AI-assisted coding: **raw story → good plan → implementation**. Your expensive model plans once. A cheap model executes many times. Squad-kit owns the folder conventions, the plan meta-prompt, and the agent slash-commands so the token cost goes where it pays off. Deeper product notes live on **[squad-kit.netlify.app](https://squad-kit.netlify.app)** and in [`docs/`](docs/philosophy.md).
14
9
 
15
10
  ```
16
11
  .squad/
17
- ├── stories/<feature>/<id>/ # intake + attachments (one per story)
18
- └── plans/<feature>/ # NN-story-<slug>.md (one per executable plan)
12
+ ├── stories/<feature>/<id>/ # intake + attachments (one per story)
13
+ └── plans/<feature>/ # NN-story-<slug>.md (one per executable plan)
19
14
  ```
20
15
 
21
- Plan meta-prompts (`generate-plan.md`, `story-skeleton.md`, intake template) are **bundled inside the squad-kit npm package**, not copied into your repo. Upgrade the CLI to change them; to fork and customise, patch `templates/prompts/` in a fork of squad-kit.
22
-
23
- ---
24
-
25
- ## Why not Spec-Kit?
26
-
27
- Both aim at spec-driven development. They make different bets.
28
-
29
- | | squad-kit | Spec-Kit |
30
- |---|---|---|
31
- | Commands | `init`, `new-story`, `new-plan`, `status`, `doctor`, `migrate`, `upgrade`, `list`, `rm`, `tracker link` | `constitution`, `specify`, `clarify`, `plan`, `tasks`, `analyze`, `checklist`, `implement` |
32
- | `/implement` turn starts with | one plan file (~5–15 KB) | 5–7 command templates + cross-artifact reads (~15–25 KB) |
33
- | Model-tier awareness | Built into the philosophy (planner ≠ executor) | Not prescribed |
34
- | Generated artifacts per story | `intake.md`, `NN-story-<slug>.md`, overview row | `spec.md`, `plan.md`, `data-model.md`, `contracts/`, `research.md`, `quickstart.md`, `tasks.md` |
35
- | Customization | Prompts ship with the CLI (fork squad-kit to change them). | Template override stack with presets/extensions |
36
- | Runtime | Node + TypeScript, npm-distributable | Python + `uv` |
37
- | Scope | Intentionally small | Broad, with safety nets (`clarify`, `analyze`) |
38
-
39
- Spec-Kit ships safety rails. Squad-kit ships the cheap path and gets out of the way. Pick squad-kit when your planner already produces trustworthy plans; pick Spec-Kit when you want the process to catch planning mistakes for you.
16
+ ## What's new in 0.2.0
40
17
 
41
- See [`docs/philosophy.md`](docs/philosophy.md) for the token math and [`docs/vs-spec-kit.md`](docs/vs-spec-kit.md) for the full comparison.
18
+ - **Direct planner.** `squad new-plan --api` runs Anthropic, OpenAI, or Google from your terminal and writes the plan file. Demand-driven context keeps tokens bounded.
19
+ - **Tracker auto-fetch.** `squad new-story --id <ID>` pulls title, description, labels, and attachments (≤ 10 MB each) from Jira Cloud or Azure DevOps Services.
20
+ - **Secrets split.** API tokens live in `.squad/secrets.yaml` (git-ignored, `0600` on POSIX). `.squad/config.yaml` never holds secrets; the loader rejects secret-shaped keys.
21
+ - **Interactive-first UX.** Missing input prompts in a TTY; `-y` / `--yes` or `CI=1` opts out and fails fast.
22
+ - **`squad doctor`.** Full read-only health check; `--fix` for non-destructive repairs, `--json` for scripting.
23
+ - **`squad migrate`.** One-shot 0.1.x → 0.2.0 structural migrations (delete legacy `.squad/prompts/`, permissions, config normalisation). Destructive; use `--dry-run` first.
24
+ - **`squad upgrade`.** Safe npm-backed self-update (`--check` to verify only).
25
+ - **`squad config`.** `show`, `set planner` / `set tracker`, `unset …`, `remove-credential …` — edit config and secrets without hand-editing YAML.
26
+ - **`squad rm`.** `rm story` / `rm plan` / `rm feature` with `--dry-run`, `--trash`, and cascading overview updates.
27
+ - **Bundled prompts.** `generate-plan.md`, `intake.md`, and `story-skeleton.md` ship inside the npm package; upgrade the CLI to update them.
42
28
 
43
- ### Tracker auto-fetch (new in 0.2.0)
29
+ Full list in [`CHANGELOG.md`](CHANGELOG.md).
44
30
 
45
- When a supported tracker is configured, `squad new-story <feature> --id <ID>` pulls the work item's title, description, labels, and attachments (≤ 10 MB each) directly into the intake. No more manual copy-paste.
31
+ ## Upgrading from 0.1.x
46
32
 
47
- Supported trackers in 0.2.0: **Jira Cloud**, **Azure DevOps Services**.
33
+ Existing squad-kit users should run two commands, once per repo:
48
34
 
49
- Credentials are prompted during `squad init` and stored in `.squad/secrets.yaml`, which squad-kit auto-adds to your `.gitignore`. Environment variables (`JIRA_API_TOKEN`, `AZURE_DEVOPS_PAT`, or the cross-provider `SQUAD_TRACKER_API_KEY`) also work if you prefer them.
35
+ ```bash
36
+ pnpm add -g squad-kit@0.2.0 # or: npm install -g squad-kit@0.2.0 — or: squad upgrade
37
+ cd your-project && squad migrate
38
+ ```
50
39
 
51
- Pass `--no-fetch` to skip the call and scaffold an empty intake; `--no-attachments` to fetch metadata only.
40
+ `squad migrate` deletes the now-unused `.squad/prompts/` directory, appends the managed `.gitignore` block, tightens `.squad/secrets.yaml` permissions to `0600`, and normalises `.squad/config.yaml`. It is idempotent but destructive — run it with `--dry-run` first if you forked the prompt files.
52
41
 
53
- ---
42
+ Full upgrade walkthrough, including what-if scenarios and how to recover customised prompts: [migration guide (repo)](docs/migrating-from-0.1.md) · [migration guide (site)](https://squad-kit.netlify.app/docs/migrating-from-0.1).
54
43
 
55
44
  ## Install
56
45
 
@@ -62,200 +51,114 @@ pnpm add -g squad-kit@0.2.0
62
51
 
63
52
  Requires Node 18+.
64
53
 
65
- ---
66
-
67
54
  ## Quickstart
68
55
 
69
- In your project root:
70
-
71
- ```bash
72
- squad init # interactive: tracker, agents, name
73
- squad new-story auth --title "SSO support"
74
- # → edit .squad/stories/auth/sso-support/intake.md (paste title, description, criteria)
75
-
76
- # Write an intake without a tracker id (even when naming.includeTrackerId would normally require one):
77
- squad new-story feature-slug --no-tracker --title "quick exploration"
78
- # Per-invocation only — .squad/config.yaml is not modified.
79
- ```
80
-
81
- ### Managing configuration
82
-
83
- squad-kit stores settings in two files:
84
-
85
- - `.squad/config.yaml` — project name, tracker type, naming rules, planner provider.
86
- - `.squad/secrets.yaml` — API keys and tokens, `.gitignore`-d and chmod 0600.
87
-
88
- Never edit these by hand. Use:
89
-
90
- | Command | What it does |
91
- | --- | --- |
92
- | `squad config show` | Detailed view of both files (secrets masked). |
93
- | `squad config set planner` | Interactively change provider, model, and key. |
94
- | `squad config set tracker` | Interactively change tracker type, host, token. |
95
- | `squad config unset planner` | Disable the direct planner; keeps credentials. |
96
- | `squad config unset tracker` | Set tracker to `none`; keeps credentials. |
97
- | `squad config remove-credential planner` | Delete planner keys without touching config. |
98
- | `squad config remove-credential tracker` | Delete tracker keys without touching config. |
99
- | `squad status` | Quick operational summary (stories, plans). |
100
- | `squad doctor` | Full health check. |
101
-
102
56
  ```bash
103
- # then, in your agent (Claude Code / Cursor / Copilot / Gemini):
104
- /squad-plan .squad/stories/auth/sso-support/intake.md
57
+ cd your-project
58
+ squad init # interactive: tracker, planner, agents, name
59
+ squad new-story auth --title "SSO support" # or: squad new-story auth --id ENG-42 to auto-fetch
60
+ # → edit .squad/stories/auth/ENG-42/intake.md
105
61
 
106
- # → produces .squad/plans/auth/01-story-sso-support.md
62
+ /squad-plan .squad/stories/auth/ENG-42/intake.md # in your agent (Claude / Cursor / Copilot / Gemini)
63
+ # → .squad/plans/auth/01-story-sso-support.md
107
64
 
108
- # implementation: new agent chat, attach ONLY the plan file.
109
- # a cheap model can execute it end-to-end.
65
+ # new agent chat · attach ONLY the plan file · a cheap model ships it
110
66
  ```
111
67
 
112
- No slash commands in your agent? Pipe the prompt directly:
68
+ No slash commands in your agent? Skip the meta-prompt copy-paste entirely:
113
69
 
114
70
  ```bash
115
- squad new-plan .squad/stories/auth/sso-support/intake.md # prints to stdout, copies to clipboard
71
+ squad new-plan --api # interactive picker, direct provider call, writes the plan file
116
72
  ```
117
73
 
118
- ---
74
+ Configuration, credential edits, and non-interactive flags: see [`docs/customization.md`](docs/customization.md) and `squad config show`. Full walkthrough: [`docs/getting-started.md`](docs/getting-started.md).
119
75
 
120
76
  ## Commands
121
77
 
122
- | Command | What it does |
123
- |---|---|
124
- | `squad init` | Bootstrap `.squad/` with config and agent slash-commands (prompts are bundled in the package) |
125
- | `squad new-story [feature] [--id ID] [--title ...]` | Scaffold a story intake folder (optional feature prompts in a TTY) |
126
- | `squad new-plan <intake-path>` | Compose the plan-generation meta-prompt with the intake inlined; print to stdout + copy to clipboard |
127
- | `squad status` | Count stories/plans, show next global `NN`, tracker/planner credential hints, warn on duplicates |
128
- | `squad doctor` | Run a health check on your `.squad/` workspace |
129
- | `squad doctor --fix` | Apply non-destructive repairs (`.gitignore` block, `secrets.yaml` mode `0600`, missing dirs) |
130
- | `squad doctor --json` | Emit results as JSON on stdout (for scripting) |
131
- | `squad list [--feature <slug>]` | Table of stories + their plan state |
132
- | `squad rm` | Delete stories, plans, or whole features (`rm story` / `rm plan` / `rm feature`) with `--dry-run`, `--trash`, `-y` |
133
- | `squad tracker link [story] [id]` | Attach or update a tracker id on an intake (optional args prompt in a TTY) |
134
- | `squad migrate` | One-shot structural migrations from 0.1.x to 0.2.0 |
135
- | `squad migrate --dry-run` | Show what would change without applying |
136
- | `squad migrate --yes` | Skip the confirmation prompt |
137
- | `squad upgrade` | Check npm for a newer squad-kit release and install it |
138
- | `squad upgrade --check` | Only check; do not install |
139
- | `squad upgrade --yes` | Skip the confirmation prompt |
140
-
141
- ### Removing things safely
142
-
143
- squad-kit never asks you to `rm -rf` by hand. Use:
144
-
145
78
  | Command | What it does |
146
79
  | --- | --- |
147
- | `squad rm story` | Interactive picker; deletes intake + plan + overview row. |
148
- | `squad rm story <path\|id>` | Same, targeted. |
149
- | `squad rm plan` | Interactive picker; deletes the plan file only. |
150
- | `squad rm feature` | Interactive picker; deletes every story + plan + overview. |
151
- | `... --dry-run` | Preview without changing anything. |
152
- | `... --trash` | Move into `.squad/.trash/<ts>/` instead of deleting. |
153
- | `... -y` | Skip the confirmation prompt (non-interactive). |
154
-
155
- `.squad/.trash/` is git-ignored automatically. Recover with `mv .squad/.trash/<ts>/<item> .squad/...`. Empty it when you are sure: `rm -rf .squad/.trash`.
156
-
157
- ### `squad upgrade`
158
-
159
- `squad upgrade` queries `registry.npmjs.org` for the latest squad-kit release, detects your package manager (pnpm, npm, yarn, bun), and runs the appropriate global-install command with your confirmation. After upgrading, run `squad migrate` in each project to update the `.squad/` structure if needed.
160
-
161
- ### `squad doctor`
162
-
163
- Use this when something feels misconfigured. By default it is read-only: it loads `.squad/config.yaml` and `.squad/secrets.yaml`, checks directory layout, warns if a legacy `.squad/prompts/` directory is still present (removed in 0.2.0), validates planner settings and API keys (including a lightweight `/v1/models` probe — never a paid completion), checks Jira/Azure tracker config and credentials, and optionally hits a small REST endpoint to verify connectivity.
164
-
165
- `--fix` only performs safe, idempotent fixes: creating missing `.squad/stories` or `.squad/plans`, appending the managed `.gitignore` block (including `.squad/.trash/` when missing), and tightening `secrets.yaml` permissions on POSIX. It does not delete files or rewrite config; for legacy prompts cleanup and other migrations, use `squad migrate`.
166
-
167
- ### Upgrading from 0.1.x
168
-
169
- After upgrading the squad-kit package (`pnpm add -g squad-kit@latest`), run `squad migrate` once in each repo that uses squad-kit. This deletes the now-unused `.squad/prompts/` directory, ensures your `.gitignore` has the squad-kit block, tightens permissions on `.squad/secrets.yaml`, and normalises `.squad/config.yaml` formatting. Comments inside `config.yaml` will be dropped.
170
-
171
- ---
172
-
173
- ## CLI conventions
174
-
175
- ### Interactive-first
176
-
177
- Every squad-kit command prompts for missing required input when run in a TTY. Use `-y` or `--yes` to opt out — missing input then fails fast instead of prompting. Useful in scripts and CI, e.g. `CI=1 squad new-story user-auth --id ENG-42 --yes`.
178
-
179
- Set `CI=1` to force non-interactive behaviour even in a TTY (matches how other tools behave in CI environments).
80
+ | `squad init` | Scaffold `.squad/` with config, bundled prompts reference, and agent slash-commands. See [`docs/getting-started.md`](docs/getting-started.md). |
81
+ | `squad new-story [feature] [--id ID] [--title …] [--no-tracker] [--no-fetch] [--no-attachments] [--attachment-mb n]` | Create a story intake. Auto-fetches from the configured tracker when `--id` is given. |
82
+ | `squad new-plan [intake] [--api] [--copy] [--feature <slug>] [--all]` | Generate a plan from an intake. `--api` calls the configured planner; without `--api`, copy-paste mode prints the prompt to stdout and clipboard (`--no-clipboard` to skip the clipboard). |
83
+ | `squad status` | Counts, next `NN`, planner and tracker rows (including credential source). |
84
+ | `squad list [--feature <slug>]` | Table of stories and plan state. |
85
+ | `squad tracker link [story] [id]` | Attach or update a tracker id on an intake. |
86
+ | `squad config <show\|set\|unset\|remove-credential> …` | View and edit `.squad/config.yaml` and `.squad/secrets.yaml` interactively. See [`docs/customization.md`](docs/customization.md). |
87
+ | `squad rm <story\|plan\|feature> [target] [--dry-run] [--trash] [-y]` | Safely delete with cascading overview updates. |
88
+ | `squad doctor [--fix] [--json]` | Full health check; `--fix` applies non-destructive repairs. |
89
+ | `squad migrate [--dry-run] [-y]` | One-shot 0.1.x → 0.2.0 structural migrations. Destructive. |
90
+ | `squad upgrade [--check] [-y]` | Check npm and install a newer squad-kit release. |
91
+
92
+ Deeper option lists: `squad <command> --help` and the `docs/` pages above.
180
93
 
181
94
  ## Direct planner (optional)
182
95
 
183
- Squad can call a strong planning model for you from the terminal so you do not have to paste the prompt into your agent. Off by default.
96
+ Off by default. Enable during `squad init` (answer **yes** to *Enable automatic plan generation?*) or later with `squad config set planner`. Supported providers: Anthropic, OpenAI, Google.
184
97
 
185
- 1. `squad init --force` and answer **yes** to *Enable automatic plan generation?* (or re-run `squad init -y --planner anthropic`). Use `--skip-secrets-prompt` (or `--no-prompt-secrets`) if you want to skip all secret prompts in a TTY session.
186
- 2. Export the provider's API key:
98
+ Credential resolution order:
187
99
 
188
- ```bash
189
- export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY / GOOGLE_API_KEY
190
- ```
100
+ 1. Provider env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`).
101
+ 2. `.squad/secrets.yaml` under `planner.<provider>.apiKey` (or cross-provider `SQUAD_PLANNER_API_KEY`).
102
+ 3. Interactive prompt (TTY only).
103
+ 4. Fail with a recovery hint.
191
104
 
192
- 3. `squad new-plan` — pick an un-planned intake; squad reads only the files the planner asks for (bounded by `planner.budget`), writes the plan to `.squad/plans/<feature>/NN-story-<slug>.md`, and prints a summary.
193
-
194
- Optional: override the default plan-phase model id (per provider) without changing squad-kit:
105
+ Override the default plan-phase model without editing squad-kit:
195
106
 
196
107
  ```yaml
197
108
  planner:
198
109
  enabled: true
199
110
  provider: anthropic
200
- # Optional: pin a specific model for the plan phase, overriding the squad-kit default.
201
- # Useful when riding a newer provider release ahead of a squad-kit update, or when the
202
- # default has been deprecated.
203
111
  modelOverride:
204
- anthropic: claude-opus-5-0
112
+ anthropic: claude-opus-5-0 # riding a newer provider release early
205
113
  ```
206
114
 
207
- Slash commands inside your agent (`/squad-plan`) continue to work unchanged — the agent already has your repo in scope.
208
-
209
- Squad-kit never reads API keys from `.squad/config.yaml`. You can set keys via environment variables, save them in `.squad/secrets.yaml` (prompted by `squad init` when the planner is enabled), or a mix. See [Secrets](#secrets) below.
210
-
211
- #### Secrets
115
+ `squad status` marks the planner row `(override)` when a `modelOverride` is active. `squad doctor` probes the provider's `/v1/models` endpoint (no paid completion) and tells you what to do if the id is no longer served.
212
116
 
213
- - `.squad/config.yaml` — shape and non-sensitive settings. Safe to commit.
214
- - `.squad/secrets.yaml` — API tokens for the planner and tracker. **Always git-ignored** (managed by squad-kit).
215
- - Environment variables override `.squad/secrets.yaml`.
216
- - Priority: env var → `.squad/secrets.yaml` → prompt (interactive) → fail.
117
+ Slash commands inside your agent (`/squad-plan`) continue to work unchanged — the agent already has your repo in scope. The direct planner is an alternative path, not a replacement. For where keys live, see [Secrets](#secrets) below and [`docs/customization.md`](docs/customization.md).
217
118
 
218
- ---
119
+ ## Tracker auto-fetch
219
120
 
220
- ## Agent integration
121
+ `squad new-story <feature> --id <ID>` pulls the work item's title, description, labels, and attachments (≤ 10 MB each) straight into the intake. Supported trackers in 0.2.0: **Jira Cloud**, **Azure DevOps Services**.
221
122
 
222
- At `squad init` you pick which agents get native slash commands. Supported:
123
+ Credentials follow the same resolution order as the planner (env → `.squad/secrets.yaml` → prompt → fail). `squad init` prompts for them when a supported tracker is selected.
223
124
 
224
- - **Claude Code** → `.claude/commands/squad-plan.md`, `.claude/commands/squad-new-story.md`
225
- - **Cursor** → `.cursor/commands/squad-*.md`
226
- - **GitHub Copilot** → `.github/prompts/squad-plan.prompt.md`
227
- - **Gemini CLI** → `.gemini/commands/squad-plan.toml`
125
+ Flags:
228
126
 
229
- Unsupported agents work fine too — `squad new-plan` prints the composed prompt on stdout.
127
+ - `--no-fetch` — scaffold an empty intake; never call the tracker.
128
+ - `--no-attachments` — fetch metadata only.
129
+ - `--attachment-mb <n>` — override the 10 MB per-file cap.
130
+ - `--no-tracker` — skip the tracker id requirement for this story even when `naming.includeTrackerId: true`.
230
131
 
231
- ---
132
+ Full example with Azure DevOps: [`docs/getting-started.md`](docs/getting-started.md).
232
133
 
233
- ## Trackers
134
+ ## Secrets
234
135
 
235
- Optional. Configure in `.squad/config.yaml`:
136
+ squad-kit stores settings in two files:
236
137
 
237
- ```yaml
238
- tracker:
239
- type: linear # none | github | linear | jira | azure
240
- naming:
241
- includeTrackerId: true
242
- globalSequence: true
243
- ```
138
+ | File | Purpose | Git-tracked? | Editable by hand? |
139
+ | --- | --- | --- | --- |
140
+ | `.squad/config.yaml` | Project name, tracker, naming, agents, planner shape. | yes — commit it | yes, but `squad config set` is safer |
141
+ | `.squad/secrets.yaml` | Planner + tracker API tokens. | **no** — auto-ignored, `0600` on POSIX | no — use `squad config set` / `squad init` |
244
142
 
245
- `--id` on `new-story` is validated against the tracker's id format. For **Jira** and **Azure DevOps**, with credentials configured, the CLI can [auto-fetch](#tracker-auto-fetch-new-in-020) issue metadata and attachments. Other trackers (GitHub, Linear) use id format validation only in v0.2.
143
+ `config.yaml` rejects any key matching `apiKey`, `token`, `secret`, or `credential` at load time, so accidental commits fail loud. Rotate a credential with `squad config set planner` or `squad config set tracker`; remove one with `squad config remove-credential <section>`.
246
144
 
247
- ---
145
+ ## Why not Spec-Kit?
248
146
 
249
- ## What's *not* in v0.2
147
+ Both aim at spec-driven development. They make different bets.
250
148
 
251
- We ship lean on purpose. Current non-goals:
149
+ | | squad-kit | Spec-Kit |
150
+ | --- | --- | --- |
151
+ | Commands | `init`, `new-story`, `new-plan`, `status`, `doctor`, `migrate`, `upgrade`, `list`, `rm`, `tracker link`, `config` | `constitution`, `specify`, `clarify`, `plan`, `tasks`, `analyze`, `checklist`, `implement` |
152
+ | `/implement` turn starts with | one plan file (~5–15 KB) | 5–7 command templates + cross-artifact reads (~15–25 KB) |
153
+ | Model-tier awareness | Built into the philosophy (planner ≠ executor) | Not prescribed |
154
+ | Generated artifacts per story | `intake.md`, `NN-story-<slug>.md`, overview row | `spec.md`, `plan.md`, `data-model.md`, `contracts/`, `research.md`, `quickstart.md`, `tasks.md` |
155
+ | Customization | Prompts ship with the CLI (fork squad-kit to change them). | Template override stack with presets/extensions |
156
+ | Runtime | Node + TypeScript, npm-distributable | Python + `uv` |
157
+ | Scope | Intentionally small | Broad, with safety nets (`clarify`, `analyze`) |
252
158
 
253
- - OpenAI-compatible generic endpoint (local models, OpenRouter, etc.); [Direct planner (optional)](#direct-planner-optional) covers hosted Anthropic, OpenAI, and Google
254
- - MCP server
255
- - `/clarify`, `/analyze`, constitution-equivalent
256
- - Telemetry
159
+ Spec-Kit ships safety rails. Squad-kit ships the cheap path and gets out of the way. Pick squad-kit when your planner already produces trustworthy plans; pick Spec-Kit when you want the process to catch planning mistakes for you.
257
160
 
258
- ---
161
+ See [`docs/philosophy.md`](docs/philosophy.md) for the token math and [`docs/vs-spec-kit.md`](docs/vs-spec-kit.md) for the full comparison.
259
162
 
260
163
  ## Tradeoffs to know
261
164
 
@@ -263,7 +166,15 @@ We ship lean on purpose. Current non-goals:
263
166
  - **Plans are project-coupled.** They reference real file paths. That is the point — do not expect portability between projects.
264
167
  - **Global `NN` can collide on parallel branches.** Rebase-and-renumber is the resolution. Documented in [`docs/customization.md`](docs/customization.md).
265
168
 
266
- ---
169
+ ## Non-goals for 0.2
170
+
171
+ We ship lean on purpose. Current non-goals:
172
+
173
+ - OpenAI-compatible generic endpoint (local models, OpenRouter, etc.); [Direct planner (optional)](#direct-planner-optional) covers hosted Anthropic, OpenAI, and Google
174
+ - MCP server
175
+ - `squad implement` (targeted for 0.3)
176
+ - `/clarify`, `/analyze`, constitution-equivalent
177
+ - Telemetry
267
178
 
268
179
  ## License
269
180
 
package/dist/cli.js CHANGED
@@ -2372,7 +2372,91 @@ 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 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.";
2441
+ return [
2442
+ headline,
2443
+ retried,
2444
+ "",
2445
+ "Recovery options:",
2446
+ ` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
2447
+ ` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
2448
+ ` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
2449
+ ` 4. Upgrade your ${err.provider} tier: ${limitsUrl[err.provider]}.`,
2450
+ "",
2451
+ "Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
2452
+ "",
2453
+ `Raw provider response: ${err.rawBody.slice(0, 200)}`
2454
+ ].join("\n");
2455
+ }
2456
+
2375
2457
  // src/planner/loop.ts
2458
+ var MAX_RATE_LIMIT_RETRY_SEC = 30;
2459
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2376
2460
  async function runPlanner(input6) {
2377
2461
  const maxIter = input6.maxIterations ?? 40;
2378
2462
  const turns = [{ role: "user", text: input6.userPrompt }];
@@ -2388,21 +2472,33 @@ async function runPlanner(input6) {
2388
2472
  if (input6.budget.overCost()) {
2389
2473
  return { planText: accumulatedText, budgetExhausted: true, timedOut: false, finishedNormally: false, iterations };
2390
2474
  }
2391
- const response = await input6.provider.send({
2475
+ const request = {
2392
2476
  systemPrompt: input6.systemPrompt,
2393
2477
  model: input6.model,
2394
2478
  tools: [READ_FILE_TOOL],
2395
2479
  turns,
2396
2480
  apiKey: input6.apiKey,
2397
2481
  maxOutputTokens: 8192
2398
- });
2482
+ };
2483
+ let response = await input6.provider.send(request);
2399
2484
  if (response.usage) {
2400
2485
  input6.budget.recordUsage(response.usage);
2401
2486
  input6.onUsage?.(response.usage);
2402
2487
  }
2488
+ let retriedRateLimit = false;
2489
+ 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);
2497
+ }
2498
+ retriedRateLimit = true;
2499
+ }
2403
2500
  if (response.stopReason === "error") {
2404
- const base = response.rawError ?? "planner: provider error";
2405
- throw new Error(`${base} Run \`squad doctor\` to verify models and credentials.`);
2501
+ throw composePlannerError(input6.provider.name, response, retriedRateLimit);
2406
2502
  }
2407
2503
  if (response.text) {
2408
2504
  accumulatedText += response.text;
@@ -2445,26 +2541,22 @@ async function runPlanner(input6) {
2445
2541
  }
2446
2542
  return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally: false, iterations };
2447
2543
  }
2448
-
2449
- // src/planner/provider-errors.ts
2450
- function detectModelNotFound(provider, model, status, rawBody) {
2451
- if (status !== 404) return void 0;
2452
- const body = rawBody.toLowerCase();
2453
- const hints = ["not_found_error", "model_not_found", "not_found", "model:"];
2454
- if (!hints.some((h) => body.includes(h))) return void 0;
2455
- return { provider, model, status, rawBody };
2456
- }
2457
- function modelNotFoundMessage(err) {
2458
- return [
2459
- `The ${err.provider} planner model "${err.model}" is no longer available.`,
2460
- "",
2461
- "Recovery options:",
2462
- ` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
2463
- ` 2. Run \`squad config set planner --provider openai\` (or another \`--provider\`) to switch models via a different provider, then save a key when prompted.`,
2464
- ` 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.`,
2465
- "",
2466
- `Raw provider response: ${err.rawBody.slice(0, 200)}`
2467
- ].join("\n");
2544
+ function composePlannerError(providerName, response, retriedRateLimit) {
2545
+ if (response.errorKind === "rate_limit") {
2546
+ return new Error(
2547
+ rateLimitMessage({
2548
+ provider: providerName,
2549
+ retryAfterSec: response.retryAfterSec,
2550
+ rawBody: response.rawError ?? "",
2551
+ retryAlreadyAttempted: retriedRateLimit
2552
+ })
2553
+ );
2554
+ }
2555
+ if (response.errorKind === "model_not_found") {
2556
+ return new Error(response.rawError ?? "planner: model not found");
2557
+ }
2558
+ const base = response.rawError ?? "planner: provider error";
2559
+ return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
2468
2560
  }
2469
2561
 
2470
2562
  // src/planner/providers/anthropic.ts
@@ -2497,7 +2589,20 @@ async function callAnthropic(req) {
2497
2589
  const text = await res.text().catch(() => "");
2498
2590
  const nf = detectModelNotFound("anthropic", req.model, res.status, text);
2499
2591
  if (nf) {
2500
- return { stopReason: "error", rawError: modelNotFoundMessage(nf) };
2592
+ return {
2593
+ stopReason: "error",
2594
+ rawError: modelNotFoundMessage(nf),
2595
+ errorKind: "model_not_found"
2596
+ };
2597
+ }
2598
+ const rl = detectRateLimit("anthropic", res.status, res.headers, text);
2599
+ if (rl) {
2600
+ return {
2601
+ stopReason: "error",
2602
+ rawError: `anthropic 429: ${text.slice(0, 500)}`,
2603
+ errorKind: "rate_limit",
2604
+ retryAfterSec: rl.retryAfterSec
2605
+ };
2501
2606
  }
2502
2607
  return { stopReason: "error", rawError: `anthropic ${res.status}: ${text.slice(0, 500)}` };
2503
2608
  }
@@ -2567,7 +2672,20 @@ async function callOpenAI(req) {
2567
2672
  const text = await res.text().catch(() => "");
2568
2673
  const nf = detectModelNotFound("openai", req.model, res.status, text);
2569
2674
  if (nf) {
2570
- return { stopReason: "error", rawError: modelNotFoundMessage(nf) };
2675
+ return {
2676
+ stopReason: "error",
2677
+ rawError: modelNotFoundMessage(nf),
2678
+ errorKind: "model_not_found"
2679
+ };
2680
+ }
2681
+ const rl = detectRateLimit("openai", res.status, res.headers, text);
2682
+ if (rl) {
2683
+ return {
2684
+ stopReason: "error",
2685
+ rawError: `openai 429: ${text.slice(0, 500)}`,
2686
+ errorKind: "rate_limit",
2687
+ retryAfterSec: rl.retryAfterSec
2688
+ };
2571
2689
  }
2572
2690
  return { stopReason: "error", rawError: `openai ${res.status}: ${text.slice(0, 500)}` };
2573
2691
  }
@@ -2653,7 +2771,20 @@ async function callGoogle(req) {
2653
2771
  const text = await res.text().catch(() => "");
2654
2772
  const nf = detectModelNotFound("google", req.model, res.status, text);
2655
2773
  if (nf) {
2656
- return { stopReason: "error", rawError: modelNotFoundMessage(nf) };
2774
+ return {
2775
+ stopReason: "error",
2776
+ rawError: modelNotFoundMessage(nf),
2777
+ errorKind: "model_not_found"
2778
+ };
2779
+ }
2780
+ const rl = detectRateLimit("google", res.status, res.headers, text);
2781
+ if (rl) {
2782
+ return {
2783
+ stopReason: "error",
2784
+ rawError: `google 429: ${text.slice(0, 500)}`,
2785
+ errorKind: "rate_limit",
2786
+ retryAfterSec: rl.retryAfterSec
2787
+ };
2657
2788
  }
2658
2789
  return { stopReason: "error", rawError: `google ${res.status}: ${text.slice(0, 500)}` };
2659
2790
  }
@@ -3132,6 +3263,11 @@ async function emitViaApi(story, paths, config2, _opts) {
3132
3263
  onAssistantText: () => {
3133
3264
  sessionSpinner.current?.succeed("planner thinking complete (this chunk)");
3134
3265
  sessionSpinner.current = spinner("thinking\u2026");
3266
+ },
3267
+ onRateLimit: (waitSec) => {
3268
+ sessionSpinner.current?.stop();
3269
+ warning(`${planner.provider} rate limit hit \u2014 retrying in ${waitSec}s`);
3270
+ sessionSpinner.current = spinner("waiting for rate limit to reset\u2026");
3135
3271
  }
3136
3272
  });
3137
3273
  sessionSpinner.current?.stop();
@@ -4704,7 +4840,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
4704
4840
 
4705
4841
  // src/cli.ts
4706
4842
  var program = new Command();
4707
- program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.2.0");
4843
+ program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.2.1");
4708
4844
  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
4845
  "--tracker-workspace <hostOrOrg>",
4710
4846
  "Jira host or Azure org (non-interactive default; also written to config for jira/azure)"