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 +15 -0
- package/README.md +94 -183
- package/dist/cli.js +164 -28
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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)
|
|
6
6
|
[](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>/
|
|
18
|
-
└── plans/<feature>/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
Full list in [`CHANGELOG.md`](CHANGELOG.md).
|
|
44
30
|
|
|
45
|
-
|
|
31
|
+
## Upgrading from 0.1.x
|
|
46
32
|
|
|
47
|
-
|
|
33
|
+
Existing squad-kit users should run two commands, once per repo:
|
|
48
34
|
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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?
|
|
68
|
+
No slash commands in your agent? Skip the meta-prompt copy-paste entirely:
|
|
113
69
|
|
|
114
70
|
```bash
|
|
115
|
-
squad new-plan
|
|
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
|
|
148
|
-
| `squad
|
|
149
|
-
| `squad
|
|
150
|
-
| `squad
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
2. Export the provider's API key:
|
|
98
|
+
Credential resolution order:
|
|
187
99
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
134
|
+
## Secrets
|
|
234
135
|
|
|
235
|
-
|
|
136
|
+
squad-kit stores settings in two files:
|
|
236
137
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
Both aim at spec-driven development. They make different bets.
|
|
250
148
|
|
|
251
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
""
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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.
|
|
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)"
|