@skitterbyte/skitterspec-linear 10.6.0 → 10.7.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 +3 -0
- package/assets/core/linear.config.md +46 -0
- package/assets/rules/commit-trailers.md +37 -5
- package/assets/rules/spec-planning.md +17 -3
- package/assets/skills/spec/SKILL.md +20 -0
- package/assets/skills/spec-bug/SKILL.md +16 -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 +17 -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 +1 -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 +176 -39
- 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 +402 -33
- package/src/vendor/linear/config.js +91 -1
- package/src/vendor/linear/doctor.js +67 -1
- package/src/vendor/linear/released.js +85 -1
- 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 (take | release | abort | status)
|
|
3
|
+
argument-hint: "[take <spec> | 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,13 @@ 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
|
+
"release": {
|
|
92
|
+
"stages": []
|
|
93
|
+
},
|
|
94
|
+
|
|
88
95
|
// Git branch name derived for a linked spec. Tokens: {type}, {slug},
|
|
89
96
|
// {identifier} (the Linear issue/project identifier, e.g. ENG-123). Shared
|
|
90
97
|
// with the isolation engine's branch derivation (src/env/resolve.js).
|
|
@@ -122,6 +129,45 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
122
129
|
}
|
|
123
130
|
```
|
|
124
131
|
|
|
132
|
+
## The deployment ladder (`release.stages`)
|
|
133
|
+
|
|
134
|
+
A spec's lifecycle stops at `complete`. Where a ticket goes **after** that —
|
|
135
|
+
deployed to test, approved for demo, live in prod — is a fact about an
|
|
136
|
+
**environment**, and nothing under `specs/` can derive it. `release.stages` is
|
|
137
|
+
where a project declares that ladder in its own vocabulary:
|
|
138
|
+
|
|
139
|
+
```jsonc
|
|
140
|
+
"release": {
|
|
141
|
+
"stages": [
|
|
142
|
+
{ "key": "test", "state": "On Test" },
|
|
143
|
+
{ "key": "demo", "state": "Ready for Demo" },
|
|
144
|
+
{ "key": "prod", "state": "Done" }
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- **`key`** is what CI names (`spec-sync stage test …`); **`state`** is the
|
|
150
|
+
Linear issue state it moves to. Pipelines reference the *key*, never the state
|
|
151
|
+
name, so renaming a Linear column is one edit here rather than a hunt through
|
|
152
|
+
pipeline YAML.
|
|
153
|
+
- **Every `state` is checked against the workspace** alongside `states`, at push
|
|
154
|
+
and setup time. Linear **silently ignores** an unknown issue state, so an
|
|
155
|
+
unchecked typo would deploy cleanly and move nothing, forever.
|
|
156
|
+
- **Order is recorded, not enforced.** It drives reporting and the doctor check;
|
|
157
|
+
a rollback from test, or a hotfix going straight to prod, is legitimate and is
|
|
158
|
+
never refused.
|
|
159
|
+
- **Keys must be unique**, and a malformed entry is a hard error at load — the
|
|
160
|
+
same treatment `sync.fieldOwnership` and `mapping.phases` get.
|
|
161
|
+
- **Absent or empty = the opt-out.** Nothing about the ladder affects a project
|
|
162
|
+
that has not declared one.
|
|
163
|
+
|
|
164
|
+
Unlike `states` and `mapping.phases`, this is **not** keyed by lifecycle bucket:
|
|
165
|
+
the buckets are a closed set the repo derives, and a deployment stage never is.
|
|
166
|
+
|
|
167
|
+
Run `skitterspec spec-sync states` to see the whole configured vocabulary — the
|
|
168
|
+
bucket map and the ladder — against what the workspace actually has.
|
|
169
|
+
|
|
170
|
+
|
|
125
171
|
## Spec → Issue, phases → sub-issues
|
|
126
172
|
|
|
127
173
|
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.
|
|
@@ -50,7 +64,7 @@ tag and `/spec-complete` lands it via `spec-env hotfix land` (tag + cherry-pick)
|
|
|
50
64
|
not a fast-forward. Isolation is **orthogonal to lifecycle status** and inactive
|
|
51
65
|
when `env.config.json` is absent — every skill then behaves as it does today.
|
|
52
66
|
|
|
53
|
-
**Live overlay (`/spec-live
|
|
67
|
+
**Live overlay (`/spec-live`, a command) — the light way to test a spec.** `/spec-connect`
|
|
54
68
|
runs a spec's *own* dev stack and proxies the canonical ports to it (one stack per
|
|
55
69
|
spec). **Live overlay** instead reuses the one dev server you already have running:
|
|
56
70
|
`/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
|
|
@@ -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
|
|
|
@@ -155,8 +155,22 @@ that header — must exist **before** `spec-env up`:
|
|
|
155
155
|
bootstrap steps.
|
|
156
156
|
- Run the printed `git worktree add`. **The worktree is checked out at the tag,
|
|
157
157
|
so your uncommitted stub doesn't travel with it** — move it across so `main`
|
|
158
|
-
stays pristine
|
|
159
|
-
|
|
158
|
+
stays pristine. **Create the destination bucket first:**
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
mkdir -p <worktreePath>/specs/in-progress
|
|
162
|
+
mv specs/in-progress/hotfix-<name> <worktreePath>/specs/in-progress/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The `mkdir -p` is not belt-and-braces. Git does not store empty directories, so
|
|
166
|
+
`specs/in-progress/` is **absent** from the worktree whenever nothing was in
|
|
167
|
+
progress at that point in history — and here that point is an **old release
|
|
168
|
+
tag**, where it is absent more often than not. `mv` into a missing destination
|
|
169
|
+
renames your spec folder **to** `specs/in-progress`, silently: the spec's files
|
|
170
|
+
end up one level too high, `00-overview.md` sits where the bucket should be, and
|
|
171
|
+
every later step still appears to work until something cannot find the spec.
|
|
172
|
+
Confirm the result before carrying on — you want
|
|
173
|
+
`<worktreePath>/specs/in-progress/hotfix-<name>/00-overview.md`.
|
|
160
174
|
- **Bootstrap the worktree.** A fresh worktree has no installed dependencies and
|
|
161
175
|
none of the repo's gitignored files (`.env`, local overrides). Run the printed
|
|
162
176
|
`in the worktree, run:` steps (file seeding, then setup) in order, before
|
|
@@ -349,7 +363,7 @@ full test result. The spec stays in `in-progress`.
|
|
|
349
363
|
|
|
350
364
|
- **`/spec-live` is refused for a hotfix** — its branch is built on an old tag, so
|
|
351
365
|
hot-reloading it onto the running dev server could break the shared instance.
|
|
352
|
-
To test it,
|
|
366
|
+
To test it, the user runs `/spec-connect` (its own isolated stack).
|
|
353
367
|
- Suggest **`/spec-complete`** to land it: it patch-bumps the base tag, tags the
|
|
354
368
|
hotfix branch **locally** (you push it to deploy), and cherry-picks the fix onto
|
|
355
369
|
`main`. Add `--also <tag>` at completion to also patch other release lines
|
|
@@ -142,7 +142,38 @@ never apply one they didn't agree to), and re-run with the `--state` flags. If i
|
|
|
142
142
|
made no suggestion for a bucket, **ask** which state means "finished" here rather
|
|
143
143
|
than guessing.
|
|
144
144
|
|
|
145
|
-
## 7.
|
|
145
|
+
## 7. Does this repo deploy through stages? (optional)
|
|
146
|
+
|
|
147
|
+
A spec's lifecycle ends at `complete`. Where a ticket goes **after** that —
|
|
148
|
+
deployed to test, approved for demo, live in production — is a fact about an
|
|
149
|
+
environment, and nothing in the repo can derive it. If the team runs a deploy
|
|
150
|
+
pipeline, it can declare that ladder here and CI moves tickets along it with
|
|
151
|
+
`spec-sync stage`.
|
|
152
|
+
|
|
153
|
+
**Ask, do not assume.** Most projects have no ladder, and an invented one is
|
|
154
|
+
worse than none: `spec-sync stage` refuses cleanly when none is declared, whereas
|
|
155
|
+
a wrong ladder is a pipeline quietly moving tickets to the wrong column. If the
|
|
156
|
+
user does not raise deployment, offer it once in a line and take "no" for an
|
|
157
|
+
answer.
|
|
158
|
+
|
|
159
|
+
If they do want one, ask for the stages **in deployment order**, and map each to
|
|
160
|
+
a state from the `list_issue_statuses` names you already have:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
--stage test="On Test" --stage demo="Ready for Demo" --stage prod="Done"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- The **key** (`test`) is what a pipeline names; the **state** is the Linear
|
|
167
|
+
column. Pipelines reference the key, so renaming the column later is one edit
|
|
168
|
+
here.
|
|
169
|
+
- Order is recorded, not enforced — a rollback and a hotfix straight to prod are
|
|
170
|
+
both legitimate and are never refused.
|
|
171
|
+
- **Ask what closes an issue.** If the last rung is not a completed-type state,
|
|
172
|
+
tickets that finish the ladder never reach Done. That is fine when Linear
|
|
173
|
+
automation closes them and a problem otherwise — `spec-sync doctor` warns about
|
|
174
|
+
it either way, so say which it is.
|
|
175
|
+
|
|
176
|
+
## 8. Write it
|
|
146
177
|
|
|
147
178
|
```
|
|
148
179
|
skitterspec spec-sync init-config \
|
|
@@ -157,12 +188,17 @@ shows this repo's choices and keeps inheriting everything else. `--force` is
|
|
|
157
188
|
required to replace an existing config — never pass it without the user having
|
|
158
189
|
asked for a rewrite in step 1.
|
|
159
190
|
|
|
191
|
+
Every `--stage` state is checked against the workspace exactly like a bucket
|
|
192
|
+
state, and a bad one is refused the same way — as `release.stages[test]` rather
|
|
193
|
+
than `states.complete`. There is no suggestion for a rung: the vocabulary is the
|
|
194
|
+
project's own, so ask rather than guess.
|
|
195
|
+
|
|
160
196
|
**Relay the engine's report as printed.** It names the team, the project (or
|
|
161
197
|
"team only"), the intake labels, and how many state names were checked against
|
|
162
198
|
the workspace — that report is the evidence the setup is right, so don't
|
|
163
199
|
paraphrase it into "done".
|
|
164
200
|
|
|
165
|
-
##
|
|
201
|
+
## 9. Report and hand off
|
|
166
202
|
|
|
167
203
|
**Finish by checking, not by describing.** First write down what the MCP server
|
|
168
204
|
says, from the reads you already made in step 2 — no extra round trip unless a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-status
|
|
3
3
|
description: Show a spec's one-way sync status against Linear — a read-only drift report. Reports whether the spec changed since the last push (there's something to push) and, optionally, whether Linear's issue workflow-state differs from the spec's. Fetches the Linear issue over MCP and runs `skitterspec spec-sync status`. Changes nothing. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-status", "is this spec in sync with Linear", "what would push", or "show spec sync status".
|
|
4
|
+
disable-model-invocation: true
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# /spec-status — one-way sync drift report
|