@skitterbyte/skitterspec-linear 10.5.2 → 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 +84 -0
- package/assets/rules/spec-planning.md +17 -3
- package/assets/skills/spec/SKILL.md +20 -0
- package/assets/skills/spec-bug/SKILL.md +40 -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 +60 -28
- package/assets/skills/spec-hotfix/SKILL.md +41 -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 +27 -2
- 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 +560 -12
- package/src/vendor/linear/config.js +91 -1
- package/src/vendor/linear/doctor.js +67 -1
- package/src/vendor/linear/released.js +164 -0
- 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
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Commit Trailers
|
|
2
|
+
|
|
3
|
+
Extends `commit-messages.md` — the format, length limits and `Release-Note:`
|
|
4
|
+
grammar there all still apply. This file adds one trailer, and it is only
|
|
5
|
+
installed when a ticketing provider is.
|
|
6
|
+
|
|
7
|
+
## `Refs:` — the ticket this commit belongs to
|
|
8
|
+
|
|
9
|
+
A commit carries the Linear issue **its own changes** belong to, as the **last**
|
|
10
|
+
trailer in the message:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
fix(sync): verify no longer flags every update
|
|
14
|
+
|
|
15
|
+
- Key the apply read-back by ref, resolving id via the projection
|
|
16
|
+
|
|
17
|
+
Release-Note: Pushing a spec whose phases already exist in Linear no
|
|
18
|
+
longer reports every one of them as a possible stale reference.
|
|
19
|
+
|
|
20
|
+
Refs: SKS-29
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Get the value from the engine rather than reading it off a spec by hand:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pnpm exec skitterspec-linear spec-sync ref
|
|
27
|
+
```
|
|
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
|
+
|
|
55
|
+
## Rules
|
|
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>`.
|
|
59
|
+
- **Omit the trailer entirely when there is no ref.** `spec-sync ref` exits
|
|
60
|
+
non-zero and prints nothing on a commit that has no ticket — on `main`, or on
|
|
61
|
+
a spec kept deliberately local. Never invent one, and never write
|
|
62
|
+
`Refs: none`: the release report counts unreferenced commits, and a fabricated
|
|
63
|
+
ref is worse than an honest gap.
|
|
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.
|
|
67
|
+
- **Never use Linear's magic words** — `Fixes`, `Closes`, `Resolves`. Those
|
|
68
|
+
close the issue the moment the commit reaches the default branch, which is
|
|
69
|
+
wrong here: a ticket moves when its work is **released**, not when it merges.
|
|
70
|
+
`Refs:` is deliberately inert to Linear's automation.
|
|
71
|
+
- **Blank line before it**, like `Release-Note:` — it is a git trailer, not body
|
|
72
|
+
text.
|
|
73
|
+
- Put it **after** `Release-Note:` when both are present, so the human-facing
|
|
74
|
+
note reads first.
|
|
75
|
+
|
|
76
|
+
## Why it exists
|
|
77
|
+
|
|
78
|
+
The repo lands specs with `merge --ff-only`, so history is linear and branch
|
|
79
|
+
names never reach it. The commit message is the only artefact that survives into
|
|
80
|
+
the range a release scans, which is what lets
|
|
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
|
|
@@ -311,6 +325,30 @@ test, split the fix into phase files (`01-<slug>.md` …) with a phase index in
|
|
|
311
325
|
`00-overview.md`, and leave the spec in `in-progress` for `/spec-go` to continue.
|
|
312
326
|
Say so explicitly — don't fake green.
|
|
313
327
|
|
|
328
|
+
**Then refresh the mirror (only if a provider is installed).** The Fix tasks are
|
|
329
|
+
ticked, so the repo is now the truth about this fix — and this skill can take a
|
|
330
|
+
bug all the way to green without `/spec-go` ever running. Without a provider this
|
|
331
|
+
is a no-op.
|
|
332
|
+
|
|
333
|
+
**Only when `specs/.core/linear.config.json` exists** and the spec carries a
|
|
334
|
+
`linear_identifier`. Either missing → **skip**, in one line
|
|
335
|
+
(`not linked to Linear — /spec-push to mirror it`), and carry on.
|
|
336
|
+
|
|
337
|
+
**Refresh the mirror now, without asking.** Run `/spec-push`. The repo has just
|
|
338
|
+
become the truth about this phase's progress, and progress is what the mirror
|
|
339
|
+
exists to show. Deferring it to `/spec-complete` is what makes every phase
|
|
340
|
+
sub-issue jump from Backlog straight to Done, with nothing visible in between.
|
|
341
|
+
|
|
342
|
+
- **Never mint.** An unlinked spec is skipped, not created.
|
|
343
|
+
- **Never fatal.** If the push fails — offline, no key, a Linear error — say so
|
|
344
|
+
and **finish the operation anyway**. The phase is done in the repo regardless;
|
|
345
|
+
the mirror is disposable and the next push repairs it.
|
|
346
|
+
- **Expect a dirty `specs/.core/` afterwards.** The push writes a base snapshot
|
|
347
|
+
and stamps any new ids, and these skills do not commit. The next `/commit`
|
|
348
|
+
sweeps it up with the phase's own work.
|
|
349
|
+
- **Say what happened** in the skill's report: mirror updated, skipped as
|
|
350
|
+
unlinked, or failed with the reason.
|
|
351
|
+
|
|
314
352
|
## 6. Report
|
|
315
353
|
|
|
316
354
|
Summarise: root cause, the failing→passing test, the fix, and the full test
|
|
@@ -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
|