@skitterbyte/skitterspec-linear 10.6.0 → 10.8.0
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/assets/claude-md-section.md +19 -9
- package/assets/commands/spec-connect.md +13 -0
- package/assets/commands/spec-live.md +14 -0
- package/assets/core/ci-stages.md +110 -0
- package/assets/core/env.config.md +18 -0
- package/assets/core/linear.config.json.example +4 -0
- package/assets/core/linear.config.md +88 -0
- package/assets/rules/commit-trailers.md +37 -5
- package/assets/rules/spec-planning.md +19 -4
- package/assets/skills/spec/SKILL.md +20 -0
- package/assets/skills/spec-bug/SKILL.md +48 -2
- package/assets/skills/spec-cancel/SKILL.md +9 -0
- package/assets/skills/spec-complete/SKILL.md +12 -1
- package/assets/skills/spec-go/SKILL.md +10 -8
- package/assets/skills/spec-hotfix/SKILL.md +49 -3
- package/assets/skills/spec-linear-setup/SKILL.md +38 -2
- package/assets/skills/spec-status/SKILL.md +1 -0
- package/assets/skills/spec-sync/SKILL.md +9 -0
- package/assets/skills/spec-to-main/SKILL.md +2 -1
- package/bin/skitterspec-linear.js +10 -0
- package/package.json +1 -1
- package/src/cli.js +211 -44
- package/src/env/config.js +19 -0
- package/src/env/teardown.js +62 -4
- package/src/init.js +120 -2
- package/src/vendor/linear/cli-sync.js +454 -34
- package/src/vendor/linear/config.js +158 -1
- package/src/vendor/linear/doctor.js +67 -1
- package/src/vendor/linear/released.js +149 -5
- package/src/vendor/sync-core/index.js +4 -1
- package/src/vendor/sync-core/src/compare.js +59 -3
- package/src/vendor/sync-core/src/normalize.js +65 -2
- package/assets/skills/spec-connect/SKILL.md +0 -59
- package/assets/skills/spec-live/SKILL.md +0 -73
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
## Spec workflow
|
|
2
2
|
|
|
3
|
-
Spec-driven development runs through
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Spec-driven development runs through the lifecycle **skills** below — use them so
|
|
4
|
+
structure and lifecycle stay consistent (see `.claude/rules/spec-planning.md`).
|
|
5
|
+
The everyday loop is **`spec → go → commit → complete`**, with `/spec-connect`
|
|
6
|
+
in the middle when you want to test the spec in a browser.
|
|
7
7
|
|
|
8
8
|
| Skill | Action | Status | Folder |
|
|
9
9
|
|-------|--------|--------|--------|
|
|
@@ -13,8 +13,15 @@ when isolation is on) — use them so structure and lifecycle stay consistent (s
|
|
|
13
13
|
| `/spec-go` | Provision the env, bring dev servers up, implement the next phase | `In Progress` | `specs/in-progress/` |
|
|
14
14
|
| `/spec-complete` | Verify all phases done + tests green; land + tear down | `Complete` | `specs/complete/` |
|
|
15
15
|
| `/spec-cancel` | Record progress, stamp a reason; tear down | `Cancelled` | `specs/cancelled/` |
|
|
16
|
+
| `/spec-hotfix` | (Hotfix) Fork a worktree from a release tag, red→green, land by tag | `In Progress` | `specs/in-progress/` |
|
|
17
|
+
| `/spec-to-main` | Land the branch on the base mid-spec, without finishing | (unchanged) | (unchanged) |
|
|
16
18
|
| `/spec-init` | Bootstrap/repair this workflow in a project (idempotent) | — | — |
|
|
17
19
|
|
|
20
|
+
**Skills vs commands.** The table is skills — Claude reads them and exercises
|
|
21
|
+
judgment. `/spec-connect` and `/spec-live` are **slash commands** instead: each
|
|
22
|
+
pre-executes one `spec-env` verb and relays it, so only you can run them. A skill
|
|
23
|
+
that wants one will tell you to type it.
|
|
24
|
+
|
|
18
25
|
Every spec has a **type** (`> **Type:** Feature\|Bug`) and a filename prefix
|
|
19
26
|
(`feat-<name>` / `bug-<name>`) — never `[BUG]` brackets (glob hazard). Specs use
|
|
20
27
|
markdown checkboxes (`- [ ]`) for task tracking and are the single source of
|
|
@@ -32,12 +39,15 @@ State log.
|
|
|
32
39
|
(`skitterspec init --isolation`, or `specs/.core/env.config.json` present),
|
|
33
40
|
**`/spec-go`** gives every in-progress spec its own git worktree automatically —
|
|
34
41
|
parallel specs, no stashing, `main` left free — and brings up the project's host
|
|
35
|
-
**dev servers** (`env.config` → `dev`) on the spec's ports. **`/spec-connect
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
**dev servers** (`env.config` → `dev`) on the spec's ports. The **`/spec-connect <name>`** command then exposes that spec on your canonical
|
|
43
|
+
`localhost` ports so you can test it at the normal URL (`/spec-connect main`
|
|
44
|
+
hands them back); for a code-only spec, **`/spec-live <name>`** reuses the dev
|
|
45
|
+
server you already have running instead. Docker is a
|
|
38
46
|
**per-spec escalation**: `/spec` sets `> **Stack:** worktree` (default) or
|
|
39
47
|
`worktree + docker` when the spec touches the DB / stateful services, and only
|
|
40
48
|
the latter gets a namespaced stack. Teardown is folded into **`/spec-complete`** ·
|
|
41
|
-
**`/spec-cancel`**; beneath the skills, `skitterspec spec-env
|
|
42
|
-
|
|
49
|
+
**`/spec-cancel`**; beneath the skills, `skitterspec spec-env` is the CLI engine — `up`, `down`,
|
|
50
|
+
`prune`, `dev`, `connect`, `integrate`, `hotfix`, `live`, `status` and `resolve`.
|
|
51
|
+
Most are planners the skills run; omit the spec name and it uses the worktree you
|
|
52
|
+
are standing in. Independent of lifecycle
|
|
43
53
|
status; inactive when `env.config.json` is absent.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Expose one spec's dev servers on the canonical localhost ports (or `main` to hand them back)
|
|
3
|
+
argument-hint: "[spec|main]"
|
|
4
|
+
allowed-tools: Bash({{exec}} skitterspec spec-env connect:*)
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
!`{{exec}} skitterspec spec-env connect $ARGUMENTS`
|
|
8
|
+
|
|
9
|
+
Relay the engine output above verbatim. Add nothing and run nothing else.
|
|
10
|
+
|
|
11
|
+
Only if it reports a **canonical port is in use**: say that the main checkout's
|
|
12
|
+
dev server still holds that port, and that stopping it and re-running is the fix
|
|
13
|
+
(the proxy cannot share a port main is bound to).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Put one spec live on the already-running dev server — `<spec>` takes it, a bare `take` takes the spec you are on, `main` hands the instance back
|
|
3
|
+
argument-hint: "[<spec> | main | take | release | abort | status]"
|
|
4
|
+
allowed-tools: Bash({{exec}} skitterspec spec-env live:*)
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
!`{{exec}} skitterspec spec-env live $ARGUMENTS`
|
|
8
|
+
|
|
9
|
+
Relay the engine output above verbatim. Add nothing and run nothing else.
|
|
10
|
+
|
|
11
|
+
The engine enforces every refusal itself and prints why — a hotfix, a stateful
|
|
12
|
+
spec (`Stack: worktree + docker`), a branch touching migrations, a dirty tree, a
|
|
13
|
+
rebase conflict, or another spec already holding the instance. Do not re-explain
|
|
14
|
+
or work around any of them.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Wiring a deploy pipeline to Linear
|
|
2
|
+
|
|
3
|
+
How a CI/CD pipeline moves a release's tickets along the ladder declared in
|
|
4
|
+
`linear.config.json` → `release.stages`. Read `linear.config.md` → "The
|
|
5
|
+
deployment ladder" first for the config itself; this page is the pipeline half.
|
|
6
|
+
|
|
7
|
+
Nothing here applies until a ladder is declared. Without one, `spec-sync stage`
|
|
8
|
+
refuses and every other command behaves as it always has.
|
|
9
|
+
|
|
10
|
+
## The model
|
|
11
|
+
|
|
12
|
+
A spec's lifecycle ends at `complete`. Everything after that is a fact about an
|
|
13
|
+
**environment**, which no folder under `specs/` can derive — so the repo hands
|
|
14
|
+
off. The handoff point is one-way and automatic:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/spec-complete → states.complete (the repo's last word on state)
|
|
18
|
+
CI: spec-sync stage test → release.stages test (the pipeline's, from here on)
|
|
19
|
+
CI: spec-sync stage prod → release.stages prod
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Once a spec is complete, pushes stop re-asserting its workflow state, so the
|
|
23
|
+
pipeline is the only writer. Editing a finished spec's prose still updates the
|
|
24
|
+
mirror's description; it will not drag the ticket back out of the pipeline.
|
|
25
|
+
|
|
26
|
+
## What a stage step runs
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
skitterspec spec-sync stage <key> <previous-tag>..<this-tag> --apply
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **Pass the range explicitly.** With no range it defaults to
|
|
33
|
+
`git describe --tags --abbrev=0`..HEAD, which is a developer convenience — a
|
|
34
|
+
pipeline knows exactly which tag it is deploying, and should say so. The
|
|
35
|
+
resolved range is printed on every run either way.
|
|
36
|
+
- **Drop `--apply` to dry-run.** Same output, no writes. Worth running in a
|
|
37
|
+
pre-deploy job.
|
|
38
|
+
- **`--json`** for a machine-readable result: the moves, the skips by category,
|
|
39
|
+
and the commit counts.
|
|
40
|
+
- **Exit code is 0 unless a write failed**, so a failed move fails the stage.
|
|
41
|
+
A range with nothing to move is a success, not a failure.
|
|
42
|
+
|
|
43
|
+
The tickets come from `Refs:` trailers on the commits in the range (see the
|
|
44
|
+
`commit-trailers` rule). A commit with no trailer is counted and reported, never
|
|
45
|
+
guessed at.
|
|
46
|
+
|
|
47
|
+
## Setup
|
|
48
|
+
|
|
49
|
+
1. **A ladder in `linear.config.json`** — via `/spec-linear-setup`, or
|
|
50
|
+
`spec-sync init-config --stage test="On Test" --stage prod="Done"`.
|
|
51
|
+
2. **An API key as a pipeline secret**, exposed as the env var named by
|
|
52
|
+
`auth.keyEnv` (default `LINEAR_API_KEY`). The config names the *variable*,
|
|
53
|
+
never the key. `--apply` needs the API transport and refuses over MCP.
|
|
54
|
+
3. **Full history for the range** (CI: `fetch-depth: 0`). Fetching tags is not
|
|
55
|
+
enough on its own: it makes the tag *resolve* without deepening the history,
|
|
56
|
+
so `git log <tag>..HEAD` would return only the commits the clone happens to
|
|
57
|
+
hold. `stage` and `released` check this and **refuse** rather than report a
|
|
58
|
+
partial range, so a shallow checkout fails the step loudly instead of
|
|
59
|
+
deploying a release that quietly leaves tickets behind.
|
|
60
|
+
4. **Run `spec-sync doctor --check-remote` once** after setup. It reports the
|
|
61
|
+
ladder's rungs against the workspace and warns if the last rung never closes
|
|
62
|
+
an issue.
|
|
63
|
+
|
|
64
|
+
### Azure Pipelines
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
- task: Bash@3
|
|
68
|
+
displayName: Mark tickets as deployed to test
|
|
69
|
+
env:
|
|
70
|
+
LINEAR_API_KEY: $(LinearApiKey) # a secret variable, not inline
|
|
71
|
+
inputs:
|
|
72
|
+
targetType: inline
|
|
73
|
+
script: |
|
|
74
|
+
set -euo pipefail
|
|
75
|
+
npx skitterspec spec-sync stage test "$(PreviousTag)..$(Build.SourceBranchName)" --apply
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Repeat per environment with a different `<key>`: a post-deploy step on the test
|
|
79
|
+
stage runs `stage test`, the demo approval gate runs `stage demo`, the production
|
|
80
|
+
deploy runs `stage prod`.
|
|
81
|
+
|
|
82
|
+
## Why not call the API directly
|
|
83
|
+
|
|
84
|
+
A hand-rolled `curl` to Linear's GraphQL endpoint loses three things this command
|
|
85
|
+
has, and the loss is silent in all three cases:
|
|
86
|
+
|
|
87
|
+
- **State-name validation.** Linear **silently ignores** an unknown workflow
|
|
88
|
+
state: the mutation succeeds, and the issue never moves. `stage` resolves the
|
|
89
|
+
target state before its first write and refuses if the workspace lacks it.
|
|
90
|
+
- **The skip rules.** A range carries refs that must not be moved — another
|
|
91
|
+
team's, ones no spec claims, and specs that have not finished. A blanket loop
|
|
92
|
+
over every ref in the log moves all of them.
|
|
93
|
+
- **Retries and error classification.** The adapter retries transient failures;
|
|
94
|
+
a bare `curl` reports a 500 as success if you forget to check.
|
|
95
|
+
|
|
96
|
+
## What it will not move, and why
|
|
97
|
+
|
|
98
|
+
Every skipped ref is named in the output with its reason — a silent exclusion and
|
|
99
|
+
a successful move look identical in a pipeline log.
|
|
100
|
+
|
|
101
|
+
| Skipped | Reason |
|
|
102
|
+
|---|---|
|
|
103
|
+
| another team's ref | not `linear.teamKey`; writing to an unconfigured team is the worst failure available here |
|
|
104
|
+
| no spec claims it | tracker-only work, or a typo in a trailer — nothing here can tell which |
|
|
105
|
+
| spec not complete | it landed via `/spec-to-main`; push still owns its state and would bounce it back |
|
|
106
|
+
| unreadable | Linear did not return the issue; dropped rather than guessed at |
|
|
107
|
+
|
|
108
|
+
A move that runs against the declared order — backwards, or skipping a rung —
|
|
109
|
+
**warns and proceeds**. A rollback from test and a hotfix going straight to prod
|
|
110
|
+
are both legitimate, and refusing either would be wrong on healthy input.
|
|
@@ -148,6 +148,24 @@ no live `env.config.json` was found.
|
|
|
148
148
|
"refuseTeardownIfUnpushed": true
|
|
149
149
|
},
|
|
150
150
|
|
|
151
|
+
// What teardown cleans up beyond this machine. `/spec-go` pushes the spec
|
|
152
|
+
// branch when it provisions, so without this a completed spec leaves a merged
|
|
153
|
+
// branch on the remote forever. `deleteRemoteBranch`:
|
|
154
|
+
// "prompt" (default) — plan `git push <remote> --delete <branch>` in its own
|
|
155
|
+
// "confirm with the user first" section; /spec-complete and
|
|
156
|
+
// /spec-cancel ask before running it.
|
|
157
|
+
// "never" — omit it; clean the remote up yourself.
|
|
158
|
+
// "always" — fold it into `run these:` and never ask.
|
|
159
|
+
// Only ever planned for a branch that has LANDED (merged into base, or captured
|
|
160
|
+
// by a hotfix's deploy tag) and whose remote-tracking ref this clone can
|
|
161
|
+
// actually see. Until a branch lands, the remote copy is its only backup, so
|
|
162
|
+
// --force does NOT enable this. A branch pushed from another machine has no
|
|
163
|
+
// local ref here and is simply missed — teardown under-cleans rather than
|
|
164
|
+
// deleting something it cannot see. An unrecognised value means "prompt".
|
|
165
|
+
"teardown": {
|
|
166
|
+
"deleteRemoteBranch": "prompt"
|
|
167
|
+
},
|
|
168
|
+
|
|
151
169
|
// Live overlay (`spec-env live` / `/spec-live`): test a spec on the already-
|
|
152
170
|
// running dev server by checking its branch out in the primary checkout.
|
|
153
171
|
// `migrations` is a list of globs (`**`, `*`, `?`) marking migration files; a
|
|
@@ -85,6 +85,17 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
85
85
|
"overviewFile": "00-overview.md"
|
|
86
86
|
},
|
|
87
87
|
|
|
88
|
+
// The project's OWN deployment ladder — where a ticket goes AFTER its spec is
|
|
89
|
+
// complete. Empty (the default) means no ladder is declared and every
|
|
90
|
+
// stage-aware path is unused. See "The deployment ladder" below.
|
|
91
|
+
//
|
|
92
|
+
// `ignorePaths` is what `released`/`stage` treat as BOOKKEEPING rather than
|
|
93
|
+
// shipped work — see "Bookkeeping commits" below. `[]` opts out.
|
|
94
|
+
"release": {
|
|
95
|
+
"stages": [],
|
|
96
|
+
"ignorePaths": ["specs/"]
|
|
97
|
+
},
|
|
98
|
+
|
|
88
99
|
// Git branch name derived for a linked spec. Tokens: {type}, {slug},
|
|
89
100
|
// {identifier} (the Linear issue/project identifier, e.g. ENG-123). Shared
|
|
90
101
|
// with the isolation engine's branch derivation (src/env/resolve.js).
|
|
@@ -122,6 +133,83 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
122
133
|
}
|
|
123
134
|
```
|
|
124
135
|
|
|
136
|
+
## The deployment ladder (`release.stages`)
|
|
137
|
+
|
|
138
|
+
A spec's lifecycle stops at `complete`. Where a ticket goes **after** that —
|
|
139
|
+
deployed to test, approved for demo, live in prod — is a fact about an
|
|
140
|
+
**environment**, and nothing under `specs/` can derive it. `release.stages` is
|
|
141
|
+
where a project declares that ladder in its own vocabulary:
|
|
142
|
+
|
|
143
|
+
```jsonc
|
|
144
|
+
"release": {
|
|
145
|
+
"stages": [
|
|
146
|
+
{ "key": "test", "state": "On Test" },
|
|
147
|
+
{ "key": "demo", "state": "Ready for Demo" },
|
|
148
|
+
{ "key": "prod", "state": "Done" }
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **`key`** is what CI names (`spec-sync stage test …`); **`state`** is the
|
|
154
|
+
Linear issue state it moves to. Pipelines reference the *key*, never the state
|
|
155
|
+
name, so renaming a Linear column is one edit here rather than a hunt through
|
|
156
|
+
pipeline YAML.
|
|
157
|
+
- **Every `state` is checked against the workspace** alongside `states`, at push
|
|
158
|
+
and setup time. Linear **silently ignores** an unknown issue state, so an
|
|
159
|
+
unchecked typo would deploy cleanly and move nothing, forever.
|
|
160
|
+
- **Order is recorded, not enforced.** It drives reporting and the doctor check;
|
|
161
|
+
a rollback from test, or a hotfix going straight to prod, is legitimate and is
|
|
162
|
+
never refused.
|
|
163
|
+
- **Keys must be unique**, and a malformed entry is a hard error at load — the
|
|
164
|
+
same treatment `sync.fieldOwnership` and `mapping.phases` get.
|
|
165
|
+
- **Absent or empty = the opt-out.** Nothing about the ladder affects a project
|
|
166
|
+
that has not declared one.
|
|
167
|
+
|
|
168
|
+
Unlike `states` and `mapping.phases`, this is **not** keyed by lifecycle bucket:
|
|
169
|
+
the buckets are a closed set the repo derives, and a deployment stage never is.
|
|
170
|
+
|
|
171
|
+
Run `skitterspec spec-sync states` to see the whole configured vocabulary — the
|
|
172
|
+
bucket map and the ladder — against what the workspace actually has.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Bookkeeping commits (`release.ignorePaths`)
|
|
176
|
+
|
|
177
|
+
`spec-sync released` and `spec-sync stage` answer "what did this release
|
|
178
|
+
contain?" from the `Refs:` trailers in the range. Left alone, that over-answers:
|
|
179
|
+
a spec's `chore(spec): complete <name>` commit carries the **same ref** as the
|
|
180
|
+
code it describes, but lands **after** the tag that shipped that code — so the
|
|
181
|
+
ticket turns up in two consecutive release ranges, once for its code and once
|
|
182
|
+
for its paperwork. Downstream, a deployment ladder then drags an issue that had
|
|
183
|
+
already reached the top back down a rung.
|
|
184
|
+
|
|
185
|
+
`release.ignorePaths` names the repo-relative prefixes that are paperwork:
|
|
186
|
+
|
|
187
|
+
```jsonc
|
|
188
|
+
"release": {
|
|
189
|
+
"ignorePaths": ["specs/"]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- **Paths, not subjects.** `chore(spec):` is a convention a mislabelled commit
|
|
194
|
+
escapes; what a commit changed is a fact. A commit touching an ignored path
|
|
195
|
+
**and** a source file still counts — it shipped code.
|
|
196
|
+
- **Prefixes, not globs.** `specs` and `specs/` both mean the directory, and a
|
|
197
|
+
prefix matches only on a path boundary, so `specs/` never swallows
|
|
198
|
+
`specs-archive/`. Name a single file to ignore just that file.
|
|
199
|
+
- **Only a commit whose paths are ALL ignored is dropped**, and only when git
|
|
200
|
+
actually listed paths for it. A merge commit (for which git lists none) counts,
|
|
201
|
+
as does every commit if the path read fails — a lookup that saw nothing knows
|
|
202
|
+
nothing, and dropping a ticket nobody notices is worse than the double-count
|
|
203
|
+
this is fixing.
|
|
204
|
+
- **Every drop is disclosed.** The report says `N commit(s) ignored as
|
|
205
|
+
bookkeeping`, and `--json` carries `ignored` / `ignoredCommits`.
|
|
206
|
+
- **`[]` is the opt-out**; a blank entry is a hard error, since `""` is a prefix
|
|
207
|
+
of every path and would silently empty the release.
|
|
208
|
+
|
|
209
|
+
Default: `["specs/"]`. A project that keeps its paperwork elsewhere names its own
|
|
210
|
+
directories instead.
|
|
211
|
+
|
|
212
|
+
|
|
125
213
|
## Spec → Issue, phases → sub-issues
|
|
126
214
|
|
|
127
215
|
Push maps the spec's structure to Linear's, keyed by id so it updates rather than
|
|
@@ -6,8 +6,8 @@ installed when a ticketing provider is.
|
|
|
6
6
|
|
|
7
7
|
## `Refs:` — the ticket this commit belongs to
|
|
8
8
|
|
|
9
|
-
A commit
|
|
10
|
-
|
|
9
|
+
A commit carries the Linear issue **its own changes** belong to, as the **last**
|
|
10
|
+
trailer in the message:
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
fix(sync): verify no longer flags every update
|
|
@@ -26,15 +26,44 @@ Get the value from the engine rather than reading it off a spec by hand:
|
|
|
26
26
|
pnpm exec skitterspec-linear spec-sync ref
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
Bare, that answers **from the branch** — correct whenever you are committing
|
|
30
|
+
that branch's own implementation work, which is nearly always.
|
|
31
|
+
|
|
32
|
+
## When the branch and the commit disagree
|
|
33
|
+
|
|
34
|
+
The branch is a proxy for "what this commit is about", and it is a good one
|
|
35
|
+
right up until you commit something that is not this branch's work. The usual
|
|
36
|
+
way in: part-way through a spec, a design question warrants its own spec, so
|
|
37
|
+
`/spec` writes a new one into `specs/backlog/` and links it. That commit is
|
|
38
|
+
**entirely the new spec's**, but you are standing on the old spec's branch, and
|
|
39
|
+
the bare command answers with the old spec's ticket.
|
|
40
|
+
|
|
41
|
+
Name the spec instead — still the engine, never a hand-written id:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pnpm exec skitterspec-linear spec-sync ref feat-distance-prescriptions
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The branch is not consulted, so this works from `main` too. An unknown spec name
|
|
48
|
+
fails rather than falling back to the branch, so a typo can't become a
|
|
49
|
+
confidently wrong ref.
|
|
50
|
+
|
|
51
|
+
**Better still, avoid the split:** author backlog specs from the base branch.
|
|
52
|
+
A spec written inside another spec's worktree physically lives on that branch —
|
|
53
|
+
it is not on `main` until that spec lands, and it is cancelled along with it.
|
|
54
|
+
|
|
29
55
|
## Rules
|
|
30
56
|
|
|
57
|
+
- **The ref names the commit's subject, not your location.** If they disagree,
|
|
58
|
+
the commit's subject wins — resolve it with `spec-sync ref <spec>`.
|
|
31
59
|
- **Omit the trailer entirely when there is no ref.** `spec-sync ref` exits
|
|
32
60
|
non-zero and prints nothing on a commit that has no ticket — on `main`, or on
|
|
33
61
|
a spec kept deliberately local. Never invent one, and never write
|
|
34
62
|
`Refs: none`: the release report counts unreferenced commits, and a fabricated
|
|
35
63
|
ref is worse than an honest gap.
|
|
36
|
-
- **One ref per commit.** A commit belongs to one spec
|
|
37
|
-
|
|
64
|
+
- **One ref per commit.** A commit belongs to one spec. If work genuinely spans
|
|
65
|
+
two tickets, that is two commits — and that is the fix when a single commit
|
|
66
|
+
would mix a branch's own work with another spec's files.
|
|
38
67
|
- **Never use Linear's magic words** — `Fixes`, `Closes`, `Resolves`. Those
|
|
39
68
|
close the issue the moment the commit reaches the default branch, which is
|
|
40
69
|
wrong here: a ticket moves when its work is **released**, not when it merges.
|
|
@@ -49,4 +78,7 @@ pnpm exec skitterspec-linear spec-sync ref
|
|
|
49
78
|
The repo lands specs with `merge --ff-only`, so history is linear and branch
|
|
50
79
|
names never reach it. The commit message is the only artefact that survives into
|
|
51
80
|
the range a release scans, which is what lets
|
|
52
|
-
`spec-sync released <range>` report the tickets a release contains.
|
|
81
|
+
`spec-sync released <range>` report the tickets a release contains. A ref that
|
|
82
|
+
names the wrong ticket does not merely lose information — it moves a commit onto
|
|
83
|
+
another ticket's release report, which is worse than the gap it was meant to
|
|
84
|
+
close.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Spec Planning
|
|
2
2
|
|
|
3
|
-
Spec-driven development is driven by nine lifecycle skills (plus
|
|
4
|
-
when isolation is on) — use them rather than hand-rolling specs so the structure
|
|
3
|
+
Spec-driven development is driven by nine lifecycle skills (plus the
|
|
4
|
+
`/spec-connect` and `/spec-live` **commands** when isolation is on) — use them rather than hand-rolling specs so the structure
|
|
5
5
|
and lifecycle stay consistent. Each sets a status on the spec header
|
|
6
6
|
(`> **Status:** …`):
|
|
7
7
|
|
|
@@ -17,6 +17,20 @@ and lifecycle stay consistent. Each sets a status on the spec header
|
|
|
17
17
|
| `/spec-cancel` | Record progress, stamp a reason on the header; tear down | `Cancelled` | `specs/cancelled/` |
|
|
18
18
|
| `/spec-init` | Bootstrap/repair this workflow in a project (idempotent) | — | — |
|
|
19
19
|
|
|
20
|
+
**Skills vs commands.** The table above lists **skills** — Claude reads them and
|
|
21
|
+
exercises judgment. `/spec-connect` and `/spec-live` are instead **slash commands**
|
|
22
|
+
(`.claude/commands/`): each pre-executes one `skitterspec spec-env` verb and
|
|
23
|
+
relays its output, so there is no judgment to apply and no model turn spent
|
|
24
|
+
finding one. They are marked `disable-model-invocation`, meaning **only you can
|
|
25
|
+
run them** — a skill that wants one will tell you to type it rather than invoking
|
|
26
|
+
it.
|
|
27
|
+
|
|
28
|
+
`/spec-to-main`, `/spec-status` and `/spec-sync` stay **skills** — each carries
|
|
29
|
+
real judgment (green tests before a land; an MCP fetch and a team-key check; ten
|
|
30
|
+
subcommands) — but they are marked user-only too, since nobody reaches them
|
|
31
|
+
except by typing them. Everything else in the table above stays model-invocable,
|
|
32
|
+
which is what lets `/spec-go` hand off to `/spec-push` as work progresses.
|
|
33
|
+
|
|
20
34
|
Status flow: `Ready → In Progress → Complete` (or `Cancelled` from any state).
|
|
21
35
|
`/spec` grills to a **Ready** spec directly — there is no separate grooming
|
|
22
36
|
command; it writes `Draft` only when open questions are deliberately left.
|
|
@@ -41,7 +55,8 @@ your canonical `localhost` ports so you can test it at the normal URL
|
|
|
41
55
|
move, header edits, the code) happens on the spec's branch in the worktree; `main`
|
|
42
56
|
changes only when it merges. Teardown is folded into `/spec-complete` ·
|
|
43
57
|
`/spec-cancel`. Beneath the skills, `skitterspec spec-env
|
|
44
|
-
<up|down|prune|dev|connect|integrate|hotfix>` is the CLI
|
|
58
|
+
<up|down|prune|dev|connect|integrate|hotfix|live|status|resolve>` is the CLI
|
|
59
|
+
engine (omit the spec name and it uses the worktree you are standing in). Teardown drops
|
|
45
60
|
the finished spec's own test-DB volume; `spec-env prune` additionally reaps
|
|
46
61
|
**orphaned** volumes left by declined/aborted teardowns, so `/spec-complete` and
|
|
47
62
|
`/spec-cancel` also sweep orphans (confirm-first). A **hotfix** is the one
|
|
@@ -50,7 +65,7 @@ tag and `/spec-complete` lands it via `spec-env hotfix land` (tag + cherry-pick)
|
|
|
50
65
|
not a fast-forward. Isolation is **orthogonal to lifecycle status** and inactive
|
|
51
66
|
when `env.config.json` is absent — every skill then behaves as it does today.
|
|
52
67
|
|
|
53
|
-
**Live overlay (`/spec-live
|
|
68
|
+
**Live overlay (`/spec-live`, a command) — the light way to test a spec.** `/spec-connect`
|
|
54
69
|
runs a spec's *own* dev stack and proxies the canonical ports to it (one stack per
|
|
55
70
|
spec). **Live overlay** instead reuses the one dev server you already have running:
|
|
56
71
|
`/spec-live <spec>` rebases the branch onto base, frees it from its worktree, and
|
|
@@ -143,6 +143,26 @@ Briefly play back the agreed understanding before writing.
|
|
|
143
143
|
|
|
144
144
|
This skill is for **features**. For bugs, use `/spec-bug` (test-first, red→green).
|
|
145
145
|
|
|
146
|
+
### Write it from the base branch
|
|
147
|
+
|
|
148
|
+
**Check where you are before creating the folder.** A backlog spec belongs on
|
|
149
|
+
the base branch (`main`). If you are on another spec's branch — most likely
|
|
150
|
+
inside its worktree, because a design question came up part-way through
|
|
151
|
+
implementing it — say so before writing, and offer to author the spec from the
|
|
152
|
+
primary checkout instead.
|
|
153
|
+
|
|
154
|
+
It matters for more than tidiness: a spec written inside another spec's worktree
|
|
155
|
+
**physically lives on that branch**. It is not on `main` until that spec lands,
|
|
156
|
+
it is invisible to anyone listing `specs/backlog/` meanwhile, and if that spec is
|
|
157
|
+
cancelled the new spec is cancelled with it. Committing it there also mis-stamps
|
|
158
|
+
the commit's ticket trailer, since that is resolved from the branch (see
|
|
159
|
+
`.claude/rules/commit-trailers.md`, installed with a ticketing provider).
|
|
160
|
+
|
|
161
|
+
**Warn, don't refuse** — doing this deliberately is legitimate, and the user may
|
|
162
|
+
have a reason. If they continue, carry on exactly as normal, and mention that
|
|
163
|
+
the trailer for that commit wants `spec-sync ref <new-spec-name>` rather than the
|
|
164
|
+
bare form.
|
|
165
|
+
|
|
146
166
|
- **Every spec is a folder** — never a bare file, even for a one-line change:
|
|
147
167
|
`specs/backlog/feat-<kebab-name>/`. Create it with `mkdir -p`.
|
|
148
168
|
- The entry point is **always `00-overview.md`** — the index/dashboard for the
|
|
@@ -143,8 +143,22 @@ provision from — you'll flesh it out in §4:
|
|
|
143
143
|
the worktree path, the opener, and any `in the worktree, run:` bootstrap steps.
|
|
144
144
|
- Run the printed `git worktree add`. **The worktree forks from `main`'s last
|
|
145
145
|
commit, so your uncommitted stub doesn't travel with it** — move it across so
|
|
146
|
-
`main` is left pristine
|
|
147
|
-
|
|
146
|
+
`main` is left pristine. **Create the destination bucket first:**
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
mkdir -p <worktreePath>/specs/in-progress
|
|
150
|
+
mv specs/in-progress/bug-<name> <worktreePath>/specs/in-progress/
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The `mkdir -p` is not belt-and-braces. Git does not store empty directories,
|
|
154
|
+
so `specs/in-progress/` is **absent** from a fresh worktree whenever that
|
|
155
|
+
bucket happens to be empty on `main` — the common case, since it empties every
|
|
156
|
+
time the last in-progress spec completes. `mv` into a missing destination
|
|
157
|
+
renames your spec folder **to** `specs/in-progress`, silently: the spec's files
|
|
158
|
+
end up one level too high, `00-overview.md` sits where the bucket should be,
|
|
159
|
+
and every later step still appears to work until something cannot find the
|
|
160
|
+
spec. Confirm the result before carrying on — you want
|
|
161
|
+
`<worktreePath>/specs/in-progress/bug-<name>/00-overview.md`.
|
|
148
162
|
- **Bootstrap the worktree.** A fresh worktree has no installed dependencies and
|
|
149
163
|
none of the repo's gitignored files (`.env`, local overrides). Run the printed
|
|
150
164
|
`in the worktree, run:` steps (file seeding, then `setup`) in order, before
|
|
@@ -298,6 +312,38 @@ and **never auto-push git** — Linear's own automation reacts to real branch/PR
|
|
|
298
312
|
events later. Report the Linear issue URL as part of the skill's finish-up
|
|
299
313
|
message.
|
|
300
314
|
|
|
315
|
+
### Picking the Linear Project
|
|
316
|
+
|
|
317
|
+
Run this **only when minting a spec issue** — creating it for the first time. On
|
|
318
|
+
an update the issue already has a project (or deliberately has none), and that
|
|
319
|
+
placement is **Linear's to own**: never send `project` on an update, and never
|
|
320
|
+
record the choice in the spec file or the snapshot. A PM re-homing a spec issue
|
|
321
|
+
must not show up as drift or be overwritten on the next push.
|
|
322
|
+
|
|
323
|
+
1. **List the candidates.** Ask the engine:
|
|
324
|
+
`skitterspec spec-sync projects --json`. On the API path it returns the team's
|
|
325
|
+
projects; on the MCP path it says so, and you call the discovered project-list
|
|
326
|
+
tool instead. Drop archived / completed projects — they can't take new work.
|
|
327
|
+
2. **Offer them.** Show the names (most recently updated first is fine), plus an
|
|
328
|
+
explicit **None (team only)** option. Pre-select `linear.projectId` from
|
|
329
|
+
`linear.config.json` when it's set and still in the list; otherwise pre-select
|
|
330
|
+
**None**.
|
|
331
|
+
3. **Narrow on request.** If the user types a fragment rather than choosing, filter
|
|
332
|
+
the list case-insensitively by name and re-offer. Don't re-fetch.
|
|
333
|
+
4. **Never offer to create a project.** Projects are the PM's surface — if none
|
|
334
|
+
fits, that's **None (team only)**, and someone makes the project in Linear.
|
|
335
|
+
5. **Pass it once**, to whichever thing mints the issue: `--project <chosen id>`
|
|
336
|
+
on `spec-sync apply`, or `project: <chosen id>` on the MCP issue-create call.
|
|
337
|
+
Chose None → omit it entirely (do not pass an empty string).
|
|
338
|
+
|
|
339
|
+
**Degrade, never block.** If the list can't be fetched — Linear not connected, no
|
|
340
|
+
project-list tool, no API key, or `spec-sync projects` reporting it couldn't ask —
|
|
341
|
+
say so in one line — *"project picker unavailable"* — and carry on with
|
|
342
|
+
`linear.projectId` if it's set, else no project at all. A missing picker must never fail the skill that called it.
|
|
343
|
+
|
|
344
|
+
If `linear.projectId` is set but that Project is archived or missing, relay
|
|
345
|
+
Linear's error and stop rather than silently minting an unparented issue.
|
|
346
|
+
|
|
301
347
|
## 5. Drive to GREEN
|
|
302
348
|
|
|
303
349
|
- Implement the **minimal, root-cause** fix. Match surrounding code; honour all
|
|
@@ -113,6 +113,15 @@ directly (the old `/spec-env-down` skill is gone — teardown is folded in here)
|
|
|
113
113
|
3. `skitterspec spec-env down <name>` — then execute the printed commands to
|
|
114
114
|
remove the worktree/stack and free the slot. It respects the teardown guards
|
|
115
115
|
(won't destroy a dirty/unpushed worktree without `--force`).
|
|
116
|
+
|
|
117
|
+
If the plan prints a `remote branch — confirm with the user first:` section,
|
|
118
|
+
**ask before running that line** — it is a `git push <remote> --delete`, and
|
|
119
|
+
the branch is merged, so it loses nothing. Usually there is nothing to ask:
|
|
120
|
+
a cancelled spec is normally **unlanded**, and the planner deliberately never
|
|
121
|
+
offers the remote delete then, because the pushed branch is the only surviving
|
|
122
|
+
copy of abandoned work. Seeing no such section here is the expected case, not
|
|
123
|
+
a fault — and if the user wants the remote branch gone anyway, that is their
|
|
124
|
+
call to make explicitly, not something to tidy away on their behalf.
|
|
116
125
|
4. `skitterspec spec-env prune` — reap orphaned test-DB volumes that belong to no
|
|
117
126
|
live spec (leftovers from declined/aborted teardowns or manual worktree
|
|
118
127
|
removal). Show the orphan list and, **only on the user's confirmation**, run
|
|
@@ -152,7 +152,7 @@ is finished — e.g. to run a later phase in CI or a shared test env? Use
|
|
|
152
152
|
back to base, re-isolates it into its worktree, clears the receipt), then prints
|
|
153
153
|
the normal landing plan. Commit any live fixes to the branch first; it refuses
|
|
154
154
|
if the primary checkout is dirty, or if a *different* spec holds it (release that
|
|
155
|
-
one
|
|
155
|
+
one by typing `/spec-live main`). Teardown (step 7) is unchanged.
|
|
156
156
|
**Work-loss abort.** Before it ends the live session, `integrate` checks the
|
|
157
157
|
work is actually landable and **aborts loudly** rather than finalize a spec
|
|
158
158
|
having landed nothing. Two cases, both leaving the live session intact:
|
|
@@ -208,6 +208,17 @@ worktree and branch are being kept, and go straight to sub-step 4. Mention
|
|
|
208
208
|
still respects the guards (won't destroy a dirty, or unpushed-and-unlanded,
|
|
209
209
|
worktree without `--force`), so if it *does* refuse, relay that and stop
|
|
210
210
|
rather than reaching for `--force`.
|
|
211
|
+
|
|
212
|
+
**If the plan prints a `remote branch — confirm with the user first:`
|
|
213
|
+
section, ask before running it.** `/spec-go` pushed this branch when it
|
|
214
|
+
provisioned, so the remote copy outlives teardown unless someone deletes it.
|
|
215
|
+
The planner only ever offers this for a branch that has **landed**, so say so
|
|
216
|
+
plainly when you ask — the commits are on the base branch (or captured by the
|
|
217
|
+
deploy tag), so deleting the remote branch loses nothing. On a yes, run the
|
|
218
|
+
printed `git push <remote> --delete <branch>`; on a no, leave it and say the
|
|
219
|
+
remote branch is still there. Never fold it into the `run these:` batch — a
|
|
220
|
+
project that wants it unattended sets `teardown.deleteRemoteBranch: "always"`
|
|
221
|
+
in `env.config.json`, and then it appears in `run these:` instead of here.
|
|
211
222
|
4. **Reap orphaned test-DB volumes:** run `skitterspec spec-env prune`. It lists
|
|
212
223
|
Docker volumes in the repo namespace that belong to **no live spec** (no
|
|
213
224
|
worktree) — leftovers from declined/aborted teardowns, manual
|
|
@@ -7,7 +7,8 @@ description: Promote a spec into active development and build the next phase —
|
|
|
7
7
|
|
|
8
8
|
The "up" button: it promotes the spec, provisions its worktree, brings its host
|
|
9
9
|
dev servers up on the spec's reserved ports (with your OK), then builds the phase.
|
|
10
|
-
Diverting your browser to the spec is a separate explicit step
|
|
10
|
+
Diverting your browser to the spec is a separate explicit step the **user**
|
|
11
|
+
types — `/spec-connect` (a slash command, not a skill you can invoke).
|
|
11
12
|
|
|
12
13
|
## 1. Identify the target spec
|
|
13
14
|
|
|
@@ -33,8 +34,8 @@ branch. Instead skip the provisioning bullets and step 2b, leave the spec where
|
|
|
33
34
|
it is, and go straight to **step 4**, implementing the phase **in the primary
|
|
34
35
|
checkout on the branch** — edits and commits there advance the branch, and
|
|
35
36
|
`/spec-complete` lands them. (`spec-env up` refuses while live and says the same.
|
|
36
|
-
To return to an isolated worktree instead,
|
|
37
|
-
re-run `/spec-go`.)
|
|
37
|
+
To return to an isolated worktree instead, ask the user to type `/spec-live main`
|
|
38
|
+
first, then re-run `/spec-go`.)
|
|
38
39
|
|
|
39
40
|
**If per-spec isolation is enabled** (`specs/.core/env.config.json` exists), the
|
|
40
41
|
spec **isn't already live** (the check above), and it doesn't already have a
|
|
@@ -121,11 +122,12 @@ runnable — its UI/API on the spec's reserved port block, isolated from `main`.
|
|
|
121
122
|
dev process detached on its port, logs to `.spec-env/logs/`, and waits on each
|
|
122
123
|
`health` check. With no `dev` configured it's a clean no-op; skip this step.
|
|
123
124
|
- **Diverting your browser is a separate step.** To test the spec at your normal
|
|
124
|
-
`localhost` URL,
|
|
125
|
-
spec on the canonical ports; `/spec-connect main` hands them back).
|
|
126
|
-
never seizes the canonical ports on its own. For a **code-only** spec,
|
|
127
|
-
lighter **`/spec-live <name>`** reuses
|
|
128
|
-
branch-switch, no second stack) — `/spec-live main` hands it back.
|
|
125
|
+
`localhost` URL, the **user** types **`/spec-connect <name>`** (exclusive — it
|
|
126
|
+
exposes this spec on the canonical ports; `/spec-connect main` hands them back).
|
|
127
|
+
`/spec-go` never seizes the canonical ports on its own. For a **code-only** spec,
|
|
128
|
+
the lighter **`/spec-live <name>`** reuses the already-running dev server (a
|
|
129
|
+
branch-switch, no second stack) — `/spec-live main` hands it back. Both are
|
|
130
|
+
user-only slash commands: tell the user to run one, never try to invoke it.
|
|
129
131
|
|
|
130
132
|
## 3. Pre-flight — commit prior work
|
|
131
133
|
|