@skitterbyte/skitterspec-linear 12.0.0 → 13.0.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/MIGRATION.md +208 -10
- package/README.md +32 -2
- package/assets/claude-md-section.md +38 -2
- package/assets/commands/spec-connect.md +2 -2
- package/assets/commands/spec-live.md +2 -2
- package/assets/core/SETUP.md +21 -3
- package/assets/core/env.config.json.example +6 -3
- package/assets/core/env.config.md +77 -30
- package/assets/core/linear.config.md +58 -0
- package/assets/review/page.html +1501 -0
- package/assets/rules/spec-planning.md +198 -10
- package/assets/rules/spec-reports.md +269 -0
- package/assets/skills/spec/SKILL.md +33 -5
- package/assets/skills/spec-bug/SKILL.md +172 -9
- package/assets/skills/spec-cancel/SKILL.md +98 -21
- package/assets/skills/spec-claim/SKILL.md +114 -0
- package/assets/skills/spec-complete/SKILL.md +94 -25
- package/assets/skills/spec-diff/SKILL.md +564 -0
- package/assets/skills/spec-hotfix/SKILL.md +172 -11
- package/assets/skills/spec-init/SKILL.md +34 -7
- package/assets/skills/spec-linear-setup/SKILL.md +55 -1
- package/assets/skills/spec-list/SKILL.md +218 -0
- package/assets/skills/spec-next/SKILL.md +299 -6
- package/assets/skills/spec-push/SKILL.md +32 -8
- package/assets/skills/spec-review/SKILL.md +40 -5
- package/assets/skills/spec-reviewed/SKILL.md +241 -0
- package/assets/skills/spec-start/SKILL.md +386 -106
- package/assets/skills/spec-status/SKILL.md +24 -2
- package/assets/skills/spec-sync/SKILL.md +40 -4
- package/assets/skills/spec-to-main/SKILL.md +28 -6
- package/package.json +11 -7
- package/src/cli.js +1513 -89
- package/src/env/building.js +143 -0
- package/src/env/config.js +42 -9
- package/src/env/provision.js +54 -15
- package/src/env/proxy.js +34 -1
- package/src/env/render.js +3 -12
- package/src/env/resolve.js +295 -9
- package/src/env/review.js +1329 -0
- package/src/env/serve.js +549 -0
- package/src/env/teardown.js +13 -6
- package/src/init.js +96 -1
- package/src/vendor/linear/api.js +104 -1
- package/src/vendor/linear/cli-sync.js +854 -17
- package/src/vendor/linear/config.js +8 -0
- package/src/vendor/linear/credentials.js +94 -0
- package/src/vendor/linear/doctor.js +35 -0
- package/src/vendor/linear/identity.js +105 -0
- package/src/vendor/linear/mcp.js +26 -0
- package/src/vendor/sync-core/index.js +6 -2
- package/src/vendor/sync-core/src/compare.js +49 -3
- package/src/vendor/sync-core/src/normalize.js +30 -0
- package/src/vendor/sync-core/src/push.js +11 -1
- package/src/vendor/sync-core/src/write.js +38 -0
- package/LICENSE +0 -21
package/MIGRATION.md
CHANGED
|
@@ -1,5 +1,209 @@
|
|
|
1
1
|
# Migration guide
|
|
2
2
|
|
|
3
|
+
## `@skitterbyte/skitterspec` v18 → v19 (starting a spec offers phase 1)
|
|
4
|
+
|
|
5
|
+
### Breaking change
|
|
6
|
+
|
|
7
|
+
**skitterspec now requires Node 22.13 or newer.** `engines.node` was `>=18`, and
|
|
8
|
+
that floor was a claim nobody was testing: the test suite cannot run without an
|
|
9
|
+
install, and pnpm 11.11 — the package manager this repo pins — itself requires
|
|
10
|
+
22.13. A floor CI cannot exercise is a promise rather than a guarantee, so it was
|
|
11
|
+
raised to the version the toolchain actually needs.
|
|
12
|
+
|
|
13
|
+
On Node 18 or 20, `npm install` now warns — or fails, under `engine-strict`.
|
|
14
|
+
Upgrade Node, or stay on v18 of skitterspec. Nothing in your config changes
|
|
15
|
+
either way.
|
|
16
|
+
|
|
17
|
+
### Releases now carry provenance
|
|
18
|
+
|
|
19
|
+
Every release from v19 onwards is built and signed by GitHub Actions through npm
|
|
20
|
+
Trusted Publishing, so no publish token exists anywhere to be leaked. Each
|
|
21
|
+
published version carries a signed attestation you can verify back to the commit
|
|
22
|
+
it was built from:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
npm view @skitterbyte/skitterspec@19.0.0 dist.attestations
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Nothing to do — it is a property of the package you receive.
|
|
29
|
+
|
|
30
|
+
### Breaking change
|
|
31
|
+
|
|
32
|
+
**`/spec-start` no longer pushes the spec's branch.** It provisions the worktree
|
|
33
|
+
and commits the spec's move to `in-progress/` exactly as before, and then stops.
|
|
34
|
+
Publishing is yours to do, whenever you want the work somewhere other than your
|
|
35
|
+
machine:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
git -C <worktreePath> push -u origin <branch>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Two things change for you, and neither is in your config:
|
|
42
|
+
|
|
43
|
+
- **Spec branches stop appearing on the remote.** Nothing is lost — the branch
|
|
44
|
+
and its commits are in the worktree — but a branch you have not pushed is on
|
|
45
|
+
one machine only, and that is now the default rather than something the
|
|
46
|
+
tooling quietly undid.
|
|
47
|
+
- **Cancelling a spec with unpublished work now refuses.** `/spec-cancel` has
|
|
48
|
+
always respected `guards.refuseTeardownIfUnpushed`, but the guard could never
|
|
49
|
+
fire while provisioning published every branch. It fires now, at the one moment
|
|
50
|
+
it was written for: the work really is about to be destroyed, and the worktree
|
|
51
|
+
is the only copy. `/spec-cancel` names both ways out — publish the branch and
|
|
52
|
+
re-run, or `spec-env down <name> --force` accepting the loss. Nothing was
|
|
53
|
+
removed from your config and nothing needs adding to it.
|
|
54
|
+
|
|
55
|
+
The justification for the old behaviour does not survive reading, which is why
|
|
56
|
+
it went rather than becoming a setting: it claimed to fire the tracker's branch
|
|
57
|
+
automation, and that needs `{identifier}` in `branch.pattern`, which the shipped
|
|
58
|
+
default does not carry. `/spec-bug` never pushed and `/spec-hotfix` forbids it,
|
|
59
|
+
so this also makes the three consistent.
|
|
60
|
+
|
|
61
|
+
**The `open.command` config key is gone.** It was the editor/terminal-agnostic
|
|
62
|
+
opener — `code {worktreePath}`, a `tmux` command, a `warp://` deeplink — that
|
|
63
|
+
`/spec-start` ran when it could not move your session into the worktree.
|
|
64
|
+
|
|
65
|
+
**Leaving it in `env.config.json` is harmless and silent** — which is the part
|
|
66
|
+
to watch. The config merge copies known keys only, so a leftover `open` block is
|
|
67
|
+
ignored rather than rejected: nothing errors, and your editor simply stops
|
|
68
|
+
opening. If you set it deliberately, that absence is the only signal you get.
|
|
69
|
+
|
|
70
|
+
**`/spec-start` no longer moves your session into the worktree either.** It
|
|
71
|
+
provisions the worktree, does the housekeeping there, prints the path, and stops.
|
|
72
|
+
This **supersedes the "opens a session in it" half of v17 → v18 below** — the
|
|
73
|
+
branch still never leaves its worktree, but nothing tries to relocate your shell
|
|
74
|
+
to reach it.
|
|
75
|
+
|
|
76
|
+
| v18 | v19 |
|
|
77
|
+
|-----|-----|
|
|
78
|
+
| Three paths through `/spec-start`: enter the session, or fall back two ways | **One path.** Provision, bootstrap, print the path. |
|
|
79
|
+
| Reaching the work meant getting a shell or a window into the worktree | **`/spec-diff`** renders the worktree's diff as a page you read anywhere |
|
|
80
|
+
| `open.command` opened an editor on the fallback path | Removed. Nothing opens anything. |
|
|
81
|
+
|
|
82
|
+
### What replaced it
|
|
83
|
+
|
|
84
|
+
**`/spec-diff`** — a new skill, and the reason the opener had nothing left to do.
|
|
85
|
+
A phase is built in its own worktree, so `git diff` in your terminal answers
|
|
86
|
+
about the base branch. `/spec-diff` collects that worktree's changes with
|
|
87
|
+
`git -C` and writes a self-contained HTML page: whole-file context that folds
|
|
88
|
+
away, a file tree, untracked files included. Open it locally, or publish it and
|
|
89
|
+
read it on a phone.
|
|
90
|
+
|
|
91
|
+
The page lands in `.spec-env/reviews/<spec>.html` (gitignored), and
|
|
92
|
+
**the diff never passes through the model** — so it costs no context tokens
|
|
93
|
+
however large it is. The optional *written* review is the part that costs, and it
|
|
94
|
+
is offered rather than assumed. `/spec-next` writes the page at the end of every
|
|
95
|
+
phase. Beneath it, `skitterspec spec-env review <spec> [--branch]` is the engine.
|
|
96
|
+
|
|
97
|
+
### `/spec-start` lands you in the worktree, and offers phase 1
|
|
98
|
+
|
|
99
|
+
Two changes to the same moment. In `worktree` mode `/spec-start` used to
|
|
100
|
+
provision the branch, print the path and leave your session where it was; opening
|
|
101
|
+
a session in the worktree was then yours to do. It now
|
|
102
|
+
**moves your session into the worktree** as part of bootstrapping it, and asks
|
|
103
|
+
whether to build phase 1:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
worktree ready — this session is now in it:
|
|
107
|
+
../myrepo-wt/sort-inbox
|
|
108
|
+
|
|
109
|
+
build phase 1 now?
|
|
110
|
+
yes -> carries on into /spec-next
|
|
111
|
+
no -> you are already there; type /spec-next whenever you like
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Your shell will not be where it was.** A session that was on `main` in the
|
|
115
|
+
primary checkout is standing in the spec's worktree afterwards, on the spec's
|
|
116
|
+
branch — so the next command you type runs there. That is the point of it, and it
|
|
117
|
+
is still a real change to plan for. The move is a plain `cd`: nothing prompts you
|
|
118
|
+
for approval, nothing opens a new terminal or window, and your primary checkout
|
|
119
|
+
is untouched and still on the base branch.
|
|
120
|
+
|
|
121
|
+
**`/spec-next` needs no argument now.** Say **yes** and `/spec-start` carries on
|
|
122
|
+
into a bare `/spec-next`; say **no** and typing `/spec-next` an hour later does
|
|
123
|
+
the same thing, because you are already standing in the right place. Neither is
|
|
124
|
+
assumed, because provisioning is cheap and reversible while a phase build is
|
|
125
|
+
neither.
|
|
126
|
+
|
|
127
|
+
**This is not a loosened refusal.** A bare `/spec-next` still refuses to build a
|
|
128
|
+
spec it is not standing in, exactly as before — what changed is where you are
|
|
129
|
+
standing, not how weakly the rule reads. `--worktree <path>` survives beside it as
|
|
130
|
+
the explicit way to build a spec you are *not* in, and a path you pass is still
|
|
131
|
+
not a path anything guessed.
|
|
132
|
+
|
|
133
|
+
**Leaving is a `cd` too.** `/spec-complete` and `/spec-cancel` delete the
|
|
134
|
+
worktree, which is now the directory you are standing in, so both tell you to `cd`
|
|
135
|
+
to the primary checkout first. `git worktree remove` **succeeds** on the tree you
|
|
136
|
+
occupy rather than refusing — the teardown looks fine and every command after it
|
|
137
|
+
dies with `Unable to read current working directory`.
|
|
138
|
+
|
|
139
|
+
**The `--worktree` build checks itself.** On that path, `/spec-next` first records
|
|
140
|
+
what your primary checkout looked like, and afterwards reports anything that
|
|
141
|
+
appeared in it — the signature of a relative path that missed the worktree. It
|
|
142
|
+
reports rather than accuses: it cannot know who wrote a file, so it names both
|
|
143
|
+
readings and deletes nothing.
|
|
144
|
+
`skitterspec spec-env resolve <spec> --record-primary` and
|
|
145
|
+
`--assert-primary-clean` are the engine underneath, usable on their own.
|
|
146
|
+
|
|
147
|
+
### What to do
|
|
148
|
+
|
|
149
|
+
1. **Upgrade** — `npx @skitterbyte/skitterspec update`.
|
|
150
|
+
2. **Delete the `open` block from `specs/.core/env.config.json`**, if you have
|
|
151
|
+
one. Optional — it is ignored either way — but leaving it implies a setting
|
|
152
|
+
that no longer does anything.
|
|
153
|
+
3. **Answer the question `/spec-start` now asks.** In `worktree` mode it offers
|
|
154
|
+
phase 1 before it finishes. Take the offer and it is built there and then;
|
|
155
|
+
decline and you are left standing in the provisioned worktree, free to type
|
|
156
|
+
`/spec-next` whenever you like. Both endings are fully supported — decline when
|
|
157
|
+
the phase is a big one and you would rather spend a fresh context on it.
|
|
158
|
+
4. **Expect your shell to move, whichever you answer.** Anything you had queued
|
|
159
|
+
for the primary checkout — a `git` command, a script, a relative path — now
|
|
160
|
+
runs in the worktree instead. `cd` back when you want the base branch, and note
|
|
161
|
+
that `/spec-complete` and `/spec-cancel` require exactly that before they tear
|
|
162
|
+
the worktree down.
|
|
163
|
+
5. **Use `/spec-diff` to read the work** rather than reaching for a terminal in
|
|
164
|
+
the worktree. It is gated on nothing — half a phase, a hand edit, or a
|
|
165
|
+
colleague's branch are all ordinary inputs.
|
|
166
|
+
|
|
167
|
+
`checkout` mode is unchanged.
|
|
168
|
+
|
|
169
|
+
## `@skitterbyte/skitterspec-linear` v12 → v13 (starting a spec offers phase 1)
|
|
170
|
+
|
|
171
|
+
The same change as `@skitterbyte/skitterspec` v18 → v19 above — this
|
|
172
|
+
distribution composes the same lifecycle skills. Read that entry first.
|
|
173
|
+
|
|
174
|
+
### Breaking change
|
|
175
|
+
|
|
176
|
+
**`spec-sync push` is now `spec-sync plan`.** The verb computes a create/update
|
|
177
|
+
plan and performs no network I/O; `spec-sync apply` is what writes to Linear.
|
|
178
|
+
Calling it `push` put three unrelated things behind one word — this verb, the
|
|
179
|
+
`/spec-push` skill, and `git push` — and it was the one that pushes nothing.
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
skitterspec spec-sync plan <spec> --workspace-states <file> --json > plan.json
|
|
183
|
+
skitterspec spec-sync apply <spec> --plan plan.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The old name is **not** aliased. It is recognised and exits 1 naming its
|
|
187
|
+
replacement, so a script that calls it fails loudly with the fix in the message
|
|
188
|
+
rather than drifting on a name that will be removed later.
|
|
189
|
+
|
|
190
|
+
**Your `linear.config.json` needs no change.** The `"push"` values under
|
|
191
|
+
`sync.fieldOwnership` — `assignee: "push"`, `description: "push"`,
|
|
192
|
+
`workflowState: "push"` — are a different vocabulary: they name a direction of
|
|
193
|
+
ownership, not a subcommand. They are untouched and still mean what they meant.
|
|
194
|
+
Do not search-and-replace `push` in your config.
|
|
195
|
+
|
|
196
|
+
### What else is here
|
|
197
|
+
|
|
198
|
+
**One thing here is Linear-specific.** `/spec-start` now pushes to Linear itself,
|
|
199
|
+
right after it commits the spec's move to `in-progress/`. It used to push nothing
|
|
200
|
+
and leave the mirror to the refresh `/spec-next` runs — immediate in `checkout`
|
|
201
|
+
mode, but in `worktree` mode hours away or never. Until it came, the issue sat in
|
|
202
|
+
its old workflow state with nobody assigned while the repo read `in-progress`
|
|
203
|
+
with a developer on it. Expect one more Linear call per `/spec-start`, and expect
|
|
204
|
+
the issue to be current the moment the spec is in flight. Nothing else changes:
|
|
205
|
+
sync is still one-way, and an unlinked spec is still skipped rather than minted.
|
|
206
|
+
|
|
3
207
|
## `@skitterbyte/skitterspec` v17 → v18 (a spec is built in its own worktree)
|
|
4
208
|
|
|
5
209
|
### Breaking change
|
|
@@ -90,8 +294,7 @@ landed and this one was missed.)
|
|
|
90
294
|
|
|
91
295
|
## `@skitterbyte/skitterspec-linear` v9 → v10 (`push` validates your issue states)
|
|
92
296
|
|
|
93
|
-
**`spec-sync push` now refuses to run until the configured `states` names have
|
|
94
|
-
been checked against your Linear workspace.** The check itself is not new — it
|
|
297
|
+
**`spec-sync push` now refuses to run until the configured `states` names have been checked against your Linear workspace.** The check itself is not new — it
|
|
95
298
|
already existed on `spec-sync status --workspace-states` — but it was advisory,
|
|
96
299
|
and skipping it sent a state name Linear **silently ignores**: the description
|
|
97
300
|
lands, the issue never moves, and nothing errors. The base
|
|
@@ -105,8 +308,7 @@ lands, the issue never moves, and nothing errors. The base
|
|
|
105
308
|
| A configured state absent from the workspace | pushed, silently no-op | **exits 1**, naming the workspace's real states |
|
|
106
309
|
|
|
107
310
|
`/spec-push` handles this for you — it fetches the workspace's issue
|
|
108
|
-
workflow-state names over MCP and passes them on. **Nothing changes if you drive
|
|
109
|
-
sync through the skill.** Only a direct CLI caller needs updating.
|
|
311
|
+
workflow-state names over MCP and passes them on. **Nothing changes if you drive sync through the skill.** Only a direct CLI caller needs updating.
|
|
110
312
|
|
|
111
313
|
### What to do
|
|
112
314
|
|
|
@@ -156,8 +358,7 @@ sync through the skill.** Only a direct CLI caller needs updating.
|
|
|
156
358
|
**v9 remaps the Linear mirror.** A spec is now a Linear **issue** (not a Project),
|
|
157
359
|
each phase a **sub-issue** (not a Milestone), and **tasks are no longer synced**
|
|
158
360
|
(they stay in the repo phase files). This collapses a large spec from ~1 project +
|
|
159
|
-
N milestones + dozens of task-issues down to **one issue + one sub-issue per
|
|
160
|
-
phase**. The base `@skitterbyte/skitterspec` is unaffected (still v15).
|
|
361
|
+
N milestones + dozens of task-issues down to **one issue + one sub-issue per phase**. The base `@skitterbyte/skitterspec` is unaffected (still v15).
|
|
161
362
|
|
|
162
363
|
### Breaking changes
|
|
163
364
|
|
|
@@ -274,10 +475,7 @@ both as optional — `/spec-review` adds them if you want them.
|
|
|
274
475
|
|
|
275
476
|
## `@skitterbyte/skitterspec` v2 → v3 (slimmer surface + local traffic diversion)
|
|
276
477
|
|
|
277
|
-
**v3 shrinks the everyday command surface to five verbs — `spec → go → connect →
|
|
278
|
-
commit → complete` — by folding provisioning, teardown, and grooming into the
|
|
279
|
-
lifecycle skills, and adds `/spec-connect` for testing a worktree at your normal
|
|
280
|
-
`localhost` URL.** (`@skitterbyte/skitterspec-linear` moves to v2.0.0 in lockstep.)
|
|
478
|
+
**v3 shrinks the everyday command surface to five verbs — `spec → go → connect → commit → complete` — by folding provisioning, teardown, and grooming into the lifecycle skills, and adds `/spec-connect` for testing a worktree at your normal `localhost` URL.** (`@skitterbyte/skitterspec-linear` moves to v2.0.0 in lockstep.)
|
|
281
479
|
|
|
282
480
|
### Removed skills (breaking) → where they went
|
|
283
481
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# @skitterbyte/skitterspec-linear
|
|
2
2
|
|
|
3
|
-
Spec-driven development for [Claude Code](https://claude.com/claude-code), **with
|
|
4
|
-
one-way Linear sync**. A strict **superset** of
|
|
3
|
+
Spec-driven development for [Claude Code](https://claude.com/claude-code), **with one-way Linear sync**. A strict **superset** of
|
|
5
4
|
[`@skitterbyte/skitterspec`](https://www.npmjs.com/package/@skitterbyte/skitterspec):
|
|
6
5
|
everything in the base filesystem workflow, plus one-way sync from a spec up to
|
|
7
6
|
its linked Linear issue — the repo is canonical, Linear is a generated mirror.
|
|
@@ -114,6 +113,37 @@ also start **from** an existing issue — `/spec SKI-123`,
|
|
|
114
113
|
`/spec-hotfix v33.16.4 SKI-123`, or `--from-issue` to browse the ones your web
|
|
115
114
|
app filed. There is no pull — the repo is already canonical.
|
|
116
115
|
|
|
116
|
+
## One ending, every skill — `.claude/rules/spec-reports.md`
|
|
117
|
+
|
|
118
|
+
Every skill finishes with the same block, and says nothing while it runs beyond
|
|
119
|
+
a question it cannot answer itself or a failure at the moment it happens:
|
|
120
|
+
|
|
121
|
+
✅ **Phase 2 built** — `feat-orders`, 2 of 4
|
|
122
|
+
|
|
123
|
+
| | |
|
|
124
|
+
|---|---|
|
|
125
|
+
| **Tracker** | [SKS-88](https://linear.app/…/SKS-88) · `feat-orders` · phase 2 moved |
|
|
126
|
+
| **Branch** | `spec/feat-orders` · 3 commits, clean |
|
|
127
|
+
| **Built** | POST /orders handler, orders schema |
|
|
128
|
+
| **Tests** | 128 passed · npm test |
|
|
129
|
+
| **Review** | 7 files, +212 −18 · [open the page](file:///…) — want a written review before you commit? |
|
|
130
|
+
| **Follow-ups** | none |
|
|
131
|
+
| **Next** | `/spec-next` → phase 3 (Auth) |
|
|
132
|
+
|
|
133
|
+
Four verdicts, and the last two are different facts about your repo: `✅` done ·
|
|
134
|
+
`⚠️` done with caveats · `❌` failed part-way, so there is a mess to clear ·
|
|
135
|
+
`⏸` refused before acting, so nothing changed.
|
|
136
|
+
**A refusal emits the block too**, so "nothing happened" is a reported outcome
|
|
137
|
+
rather than an absent one.
|
|
138
|
+
|
|
139
|
+
Fields come from a fixed vocabulary in a fixed order, and a skill emits only the
|
|
140
|
+
ones it declares — `Tracker` first because the id is how you address the work
|
|
141
|
+
outside the repo, `Next` last because it is the only row you act on.
|
|
142
|
+
`Follow-ups` is always there: a recorded `none` is a decision where a missing
|
|
143
|
+
line is an oversight. The block covers **that run only** — what else is in
|
|
144
|
+
flight is a different question, and answering it here leaves you unable to tell
|
|
145
|
+
what followed from the run you just watched.
|
|
146
|
+
|
|
117
147
|
## Opt-in
|
|
118
148
|
|
|
119
149
|
Linear sync is inert until `specs/.core/linear.config.json` exists — copy the
|
|
@@ -2,14 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
Spec-driven development runs through the lifecycle **skills** — use them so
|
|
4
4
|
structure and lifecycle stay consistent. The everyday loop is
|
|
5
|
-
**`spec → start → next → commit → complete`**, with
|
|
6
|
-
the spec in a browser.
|
|
5
|
+
**`spec → start → next → commit → complete`**, with **`/spec-diff`** to read what
|
|
6
|
+
a phase changed and `/spec-connect` when you want to test the spec in a browser.
|
|
7
7
|
|
|
8
8
|
**Skills vs commands.** The lifecycle skills are read by Claude, which exercises
|
|
9
9
|
judgment. `/spec-connect` and `/spec-live` are **slash commands** instead — each
|
|
10
10
|
pre-executes one `spec-env` verb and relays it, so only you can run them; a
|
|
11
11
|
skill that wants one will tell you to type it.
|
|
12
12
|
|
|
13
|
+
**Seeing the work.** A phase is built in its own worktree, so `git diff` in your
|
|
14
|
+
terminal answers about the base branch. **`/spec-diff`** renders that worktree's
|
|
15
|
+
diff as a self-contained HTML page you open locally — or publish, and read on a
|
|
16
|
+
phone. The diff never passes through the model, so the page costs no context
|
|
17
|
+
tokens however large it is; the optional written review is the part that costs,
|
|
18
|
+
and it is offered rather than assumed. `/spec-next` writes the page at the end of
|
|
19
|
+
every phase.
|
|
20
|
+
|
|
21
|
+
**Handing the review back.** The page takes marks: tick `✓ accept` per file as
|
|
22
|
+
you read, note anything against a line or a whole file, answer the questions a
|
|
23
|
+
written review asked — then **end it in a decision**. Three buttons, each
|
|
24
|
+
carrying its own verdict:
|
|
25
|
+
`✓ Commit` commits it, `✓ Commit & Continue` commits and builds the next phase,
|
|
26
|
+
`↺ Request changes` sends it straight back to be worked, `… Discuss first` asks
|
|
27
|
+
you what's up. A **served** page hands the pass to
|
|
28
|
+
the engine, which **holds** it and shows a six-digit code. Type `/spec-reviewed`
|
|
29
|
+
and the waiting pass is picked up and acted on; paste the code after it
|
|
30
|
+
(`/spec-reviewed 324199`) to name one exactly, which matters only when two are
|
|
31
|
+
waiting. **Nothing pushes** — a device that reaches your page cannot reach this
|
|
32
|
+
conversation, which is what keeps a stray approval out of your review, and is
|
|
33
|
+
why the code is an address rather than a password. A `file://` page has no server to talk to, so it
|
|
34
|
+
copies and you paste, as before. Approve is unavailable while a note is open —
|
|
35
|
+
you asked for something, so it cannot also be fine — and it hands off to your
|
|
36
|
+
own commit skill (`review.commitWith`, `/commit` by default) rather than a copy
|
|
37
|
+
living here. Fixes come back as resolutions, so the next render shows each note
|
|
38
|
+
struck through with what changed. An accept remembers the file's content, so it
|
|
39
|
+
lapses by itself when that file changes again. The marks are information —
|
|
40
|
+
nothing gates on them; the verdict is the one thing you choose, once.
|
|
41
|
+
|
|
42
|
+
**One ending.** Every spec skill finishes with the same block — a verdict
|
|
43
|
+
(`✅` · `⚠️` · `❌` · `⏸`), then a table of the fields that skill declares,
|
|
44
|
+
ending on the one thing to do next. A refusal emits it too, so "nothing
|
|
45
|
+
happened" is reported rather than absent, and `Follow-ups` is always there
|
|
46
|
+
because a recorded `none` is a decision. Skills stay quiet while they run. The
|
|
47
|
+
shape lives in **`.claude/rules/spec-reports.md`**.
|
|
48
|
+
|
|
13
49
|
The skill table, the spec type/folder conventions and the per-spec isolation
|
|
14
50
|
model all live in **`.claude/rules/spec-planning.md`**, the canonical reference
|
|
15
51
|
every spec skill points at. Tailor its per-phase test commands to this stack.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Expose one spec's dev servers on the canonical localhost ports
|
|
3
|
-
argument-hint: "[spec|main]"
|
|
2
|
+
description: Expose one spec's dev servers on the canonical localhost ports — bare uses the spec you are on, `main` hands them back
|
|
3
|
+
argument-hint: "[<spec> | main]"
|
|
4
4
|
allowed-tools: Bash({{exec}} skitterspec spec-env connect:*)
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Put one spec live on the already-running dev server —
|
|
3
|
-
argument-hint: "[<spec> | main |
|
|
2
|
+
description: Put one spec live on the already-running dev server — bare takes the spec you are on, `<spec>` takes that one, `main` hands the instance back
|
|
3
|
+
argument-hint: "[<spec> | main | status | release | abort]"
|
|
4
4
|
allowed-tools: Bash({{exec}} skitterspec spec-env live:*)
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
---
|
package/assets/core/SETUP.md
CHANGED
|
@@ -20,9 +20,9 @@ Fresh installs can skip this section. **`MIGRATION.md` ships with the package**
|
|
|
20
20
|
read the entry for the version you are coming from before upgrading a repo with a
|
|
21
21
|
live mirror.
|
|
22
22
|
|
|
23
|
-
**From 9.x —
|
|
24
|
-
to run until the configured `states`
|
|
25
|
-
workspace. `/spec-push` does that for you, so nothing changes if you drive sync
|
|
23
|
+
**From 9.x — the plan verb validates your issue states.** `spec-sync plan`
|
|
24
|
+
(named `spec-sync push` before v13) refuses to run until the configured `states`
|
|
25
|
+
names have been checked against the workspace. `/spec-push` does that for you, so nothing changes if you drive sync
|
|
26
26
|
through the skill; a script calling the CLI directly must pass
|
|
27
27
|
`--workspace-states <file>` (or `--skip-state-check`). See `MIGRATION.md`
|
|
28
28
|
→ "v9 → v10".
|
|
@@ -202,9 +202,14 @@ Two ways to get there:
|
|
|
202
202
|
|---------|-----------|--------------|
|
|
203
203
|
| `/spec-status` | — | Read-only drift report: what would push (create/update), and whether Linear's workflow-state drifted from the spec. Writes nothing. |
|
|
204
204
|
| `/spec-push` | repo → Linear | Computes a create/update plan vs the last-pushed snapshot and applies it (issue description/state, phase sub-issues), stamping new ids back into the spec. |
|
|
205
|
+
| `/spec-claim` | repo → Linear | Take a spec over, `--release` it, or hand it `--to` a teammate. Records the owner in the spec, then pushes. Needs assignment enabled (below). |
|
|
206
|
+
| `/spec-list` | Linear → you | Read-only listing of every spec Linear holds — id, title, state, who holds it and the local spec name to start it by. Filters by state, backlog order, or assignee. Writes nothing. |
|
|
205
207
|
|
|
206
208
|
Typical loop: edit the spec in-repo → `/spec-status` (what's pending) →
|
|
207
209
|
`/spec-push` (send it up). There is no pull — Linear is a generated mirror.
|
|
210
|
+
`/spec-list` is the read side of that: it asks Linear what exists rather than
|
|
211
|
+
the repo, which is the only way to see a spec someone started on a branch you
|
|
212
|
+
have not got.
|
|
208
213
|
|
|
209
214
|
### What gets pushed
|
|
210
215
|
|
|
@@ -229,6 +234,19 @@ Sections listed in `sync.localOnlySections` (default: **State log**, **Changelog
|
|
|
229
234
|
**Open questions**) are stripped from the pushed description — they never leave
|
|
230
235
|
the repo.
|
|
231
236
|
|
|
237
|
+
### Assignment (opt-in)
|
|
238
|
+
|
|
239
|
+
Add `"assignee": "push"` to `sync.fieldOwnership` and the spec issue is assigned
|
|
240
|
+
to whoever is building it: `/spec-start` records them, and the issue is released
|
|
241
|
+
automatically when the spec completes. Left out, nothing about it happens at all
|
|
242
|
+
— see **Assignment** in `linear.config.md` for the full rules.
|
|
243
|
+
|
|
244
|
+
You do not configure *who you are*: it comes from your own API key. Check it
|
|
245
|
+
with `skitterspec spec-sync whoami` (`--set` overrides it if the key is shared or
|
|
246
|
+
a bot's), and `skitterspec spec-sync users <name-or-email>` looks somebody up.
|
|
247
|
+
`skitterspec spec-sync doctor` reports the resolved identity, and stays quiet
|
|
248
|
+
about it in a project that has not opted in.
|
|
249
|
+
|
|
232
250
|
## 7. What to commit
|
|
233
251
|
|
|
234
252
|
- **Commit** `specs/.core/linear-base/` — the last-pushed snapshots (content
|
|
@@ -25,9 +25,6 @@
|
|
|
25
25
|
"enabled": true,
|
|
26
26
|
"host": "127.0.0.1"
|
|
27
27
|
},
|
|
28
|
-
"open": {
|
|
29
|
-
"command": ""
|
|
30
|
-
},
|
|
31
28
|
"registry": ".spec-env/registry.json",
|
|
32
29
|
"branch": {
|
|
33
30
|
"pattern": "{type}/{slug}",
|
|
@@ -41,6 +38,12 @@
|
|
|
41
38
|
"refuseTeardownIfDirty": true,
|
|
42
39
|
"refuseTeardownIfUnpushed": true
|
|
43
40
|
},
|
|
41
|
+
"review": {
|
|
42
|
+
"reader": "detect",
|
|
43
|
+
"servePort": 7777,
|
|
44
|
+
"serveOnRemote": true,
|
|
45
|
+
"commitWith": "/commit"
|
|
46
|
+
},
|
|
44
47
|
"live": {
|
|
45
48
|
"migrations": []
|
|
46
49
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# `env.config.json` — per-spec isolation config
|
|
2
2
|
|
|
3
3
|
Opt-in config for per-spec isolation (git worktree + optional namespaced Docker
|
|
4
|
-
stack + host dev servers + a front-door proxy
|
|
5
|
-
|
|
6
|
-
`/spec-
|
|
7
|
-
`skitterspec spec-env
|
|
8
|
-
|
|
4
|
+
stack + host dev servers + a front-door proxy per in-progress spec).
|
|
5
|
+
Provisioning is folded into `/spec-start`, teardown into `/spec-complete` ·
|
|
6
|
+
`/spec-cancel`, traffic diversion is `/spec-connect`, and reading a spec's diff
|
|
7
|
+
is `/spec-diff`; the `skitterspec spec-env
|
|
8
|
+
<up|down|prune|dev|connect|integrate|hotfix|live|review|stage|status|resolve>` CLI is
|
|
9
|
+
the engine beneath them.
|
|
9
10
|
|
|
10
11
|
**Once this file is present, isolation is the default policy:** `/spec-start` gives
|
|
11
12
|
**every** in-progress spec its own git worktree automatically. Docker is a
|
|
@@ -31,11 +32,12 @@ no live `env.config.json` was found.
|
|
|
31
32
|
// Where a spec's branch gets built.
|
|
32
33
|
//
|
|
33
34
|
// "worktree" (default) — every spec gets its own git worktree. Several
|
|
34
|
-
// specs run side by side and `main` stays free
|
|
35
|
-
//
|
|
35
|
+
// specs run side by side and `main` stays free. `/spec-start`
|
|
36
|
+
// moves your session into the spec's worktree with a `cd`, so
|
|
37
|
+
// the terminal you are already in follows the work.
|
|
36
38
|
// "checkout" — the branch is built in the primary checkout instead. One
|
|
37
|
-
// spec at a time,
|
|
38
|
-
// terminal
|
|
39
|
+
// spec at a time, and nowhere else to stand: the work comes to
|
|
40
|
+
// your terminal rather than your terminal going to it.
|
|
39
41
|
//
|
|
40
42
|
// Pick it for how you work, not for what this repo contains — a project with
|
|
41
43
|
// no dev servers may still want several specs in flight. An unrecognised
|
|
@@ -124,24 +126,6 @@ no live `env.config.json` was found.
|
|
|
124
126
|
"host": "127.0.0.1" // bind host for the canonical ports
|
|
125
127
|
},
|
|
126
128
|
|
|
127
|
-
// Optional, editor/terminal-agnostic opener — the FALLBACK for reaching a
|
|
128
|
-
// worktree. `/spec-start` normally moves the session you typed into into the
|
|
129
|
-
// worktree, and then there is nothing to open: it runs this only when it
|
|
130
|
-
// could not switch in place (the session is already inside another worktree,
|
|
131
|
-
// or the harness cannot move it). Run after provisioning and bootstrap, so
|
|
132
|
-
// the session opens onto a tree that is ready to work in. The template is
|
|
133
|
-
// expanded with {worktreePath}, {slug}, {branch}, {projectName},
|
|
134
|
-
// {portOffset}.
|
|
135
|
-
// Empty = nothing is opened (the path is just printed), which is how you turn
|
|
136
|
-
// the auto-open off, and the right value unless you actually want a second
|
|
137
|
-
// window on the fallback path. A non-interactive run skips it either way — an
|
|
138
|
-
// opened window nobody is sitting at helps no one.
|
|
139
|
-
// Examples: "code {worktreePath}", "tmux new-window -c {worktreePath}",
|
|
140
|
-
// or a "warp://..." deeplink for Warp users.
|
|
141
|
-
"open": {
|
|
142
|
-
"command": ""
|
|
143
|
-
},
|
|
144
|
-
|
|
145
129
|
// Machine-local slot registry (spec → slot index). Resolved against the
|
|
146
130
|
// primary checkout root, shared by all worktrees, gitignored.
|
|
147
131
|
"registry": ".spec-env/registry.json",
|
|
@@ -193,9 +177,10 @@ no live `env.config.json` was found.
|
|
|
193
177
|
"refuseTeardownIfUnpushed": true
|
|
194
178
|
},
|
|
195
179
|
|
|
196
|
-
// What teardown cleans up beyond this machine.
|
|
197
|
-
// branch
|
|
198
|
-
// branch on the remote
|
|
180
|
+
// What teardown cleans up beyond this machine. Nothing publishes a spec
|
|
181
|
+
// branch for you, so a remote copy exists only because you pushed it by hand —
|
|
182
|
+
// and without this a completed spec leaves that merged branch on the remote
|
|
183
|
+
// forever. `deleteRemoteBranch`:
|
|
199
184
|
// "prompt" (default) — plan `git push <remote> --delete <branch>` in its own
|
|
200
185
|
// "confirm with the user first" section; /spec-complete and
|
|
201
186
|
// /spec-cancel ask before running it.
|
|
@@ -232,6 +217,68 @@ no live `env.config.json` was found.
|
|
|
232
217
|
"bump": "patch",
|
|
233
218
|
"cherryPickMain": true,
|
|
234
219
|
"targets": []
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// Reading a spec's diff (`spec-env review`, `/spec-diff`).
|
|
223
|
+
//
|
|
224
|
+
// `reader` decides how the page's LOCATION IS WORDED, and — through
|
|
225
|
+
// `serveOnRemote` below — whether the engine stands its local server up so a
|
|
226
|
+
// remote reader gets a link that opens. It never decides to PUBLISH. Three
|
|
227
|
+
// values:
|
|
228
|
+
// "local" — you are at the machine holding the page; a file:// URL opens.
|
|
229
|
+
// "remote" — you are not; it does not, so the page is served instead.
|
|
230
|
+
// "detect" — work it out (the default).
|
|
231
|
+
// An explicit "local"/"remote" is BELIEVED WITHOUT SNIFFING: you know where
|
|
232
|
+
// you are reading, and no signal outranks being told. Detection is only the
|
|
233
|
+
// default, and it has three outcomes rather than two — local, remote, and
|
|
234
|
+
// unknown. Unknown behaves exactly as the tool did before any of this existed
|
|
235
|
+
// (the file:// URL, no warning), because a wrong "local" prints a dead link
|
|
236
|
+
// and a wrong "remote" warns at someone whose link works fine. An
|
|
237
|
+
// unrecognised value falls through to "detect", so a typo cannot become a
|
|
238
|
+
// confident answer. Default: detect.
|
|
239
|
+
//
|
|
240
|
+
// `servePort` is the default port for `spec-env review serve`, which renders
|
|
241
|
+
// every spec's diff per request on one local server. `--port` overrides it per
|
|
242
|
+
// run. The server binds 127.0.0.1 unless `--host 0.0.0.0` is passed, which
|
|
243
|
+
// mints an unguessable path token and prints the LAN URL including it —
|
|
244
|
+
// anyone holding that URL can read every spec's diff while it runs.
|
|
245
|
+
// Default: 7777.
|
|
246
|
+
//
|
|
247
|
+
// `serveOnRemote` is whether a "remote" reader may have that server started
|
|
248
|
+
// FOR them. On (the default) the engine brings it up, binds 0.0.0.0, and puts
|
|
249
|
+
// the served URL on the `open:` line — best-guess network address first, the
|
|
250
|
+
// rest listed under it, because the guess reads interface names and a VPN or
|
|
251
|
+
// an unusual adapter will fool it. Off, you get the file:// URL with its
|
|
252
|
+
// "will not open where you are reading" marker and the command to type.
|
|
253
|
+
// Either way NOTHING IS PUBLISHED on a detection: a server is one process
|
|
254
|
+
// ended by one flag, while a published page is one this tooling cannot
|
|
255
|
+
// remove, so that half stays an explicit ask. Teardown names a server that
|
|
256
|
+
// served the last spec, and `spec-env prune` reaps a pidfile whose process is
|
|
257
|
+
// gone. Default: true.
|
|
258
|
+
//
|
|
259
|
+
// `commitWith` names the skill a COMMITTING verdict hands off to. A review
|
|
260
|
+
// page ends in a verdict — commit, commit & continue, request changes,
|
|
261
|
+
// discuss — and the point of the first two is that the commit follows from
|
|
262
|
+
// the reading rather than costing a separate decision. Skitterspec never
|
|
263
|
+
// commits through a skill it vendored: `/commit` ships with skittership, a
|
|
264
|
+
// different package, and a copy living here would fork it. Two shapes:
|
|
265
|
+
// "/commit" — the default; hand off to skittership's commit skill.
|
|
266
|
+
// "<name>" — any other skill your project installs.
|
|
267
|
+
// With the named skill unavailable, the committing branch commits directly —
|
|
268
|
+
// stage, typecheck, test, conventional message — and SAYS it took that path,
|
|
269
|
+
// because a commit made under rules nobody configured must not read as one
|
|
270
|
+
// made under /commit.
|
|
271
|
+
//
|
|
272
|
+
// THERE IS NO OFF SWITCH. `"none"` existed and was removed: it produced a
|
|
273
|
+
// verdict that records itself and does nothing, which is the one thing a
|
|
274
|
+
// review page must not offer — a review is the guard in front of an action.
|
|
275
|
+
// Recording an approval for SOMEONE ELSE to act on is a separate mechanism,
|
|
276
|
+
// not a value of this key. Default: "/commit".
|
|
277
|
+
"review": {
|
|
278
|
+
"reader": "detect",
|
|
279
|
+
"servePort": 7777,
|
|
280
|
+
"serveOnRemote": true,
|
|
281
|
+
"commitWith": "/commit"
|
|
235
282
|
}
|
|
236
283
|
}
|
|
237
284
|
```
|
|
@@ -133,6 +133,64 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
133
133
|
}
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## Assignment (`sync.fieldOwnership.assignee`)
|
|
137
|
+
|
|
138
|
+
Off by default. Add one key and the spec issue is assigned to whoever is
|
|
139
|
+
building it:
|
|
140
|
+
|
|
141
|
+
```jsonc
|
|
142
|
+
"fieldOwnership": {
|
|
143
|
+
"description": "push",
|
|
144
|
+
"subIssues": "push",
|
|
145
|
+
"workflowState": "push",
|
|
146
|
+
"assignee": "push" // <- the whole opt-in
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
It is deliberately **not** a config key of its own. `fieldOwnership` is already
|
|
151
|
+
the documented extension point — "any key you add joins the pushed projection" —
|
|
152
|
+
and assignment is exactly one more field the repo owns.
|
|
153
|
+
|
|
154
|
+
- **Absent = inert.** No writes, no prompts, no drift line, and no assignee hash
|
|
155
|
+
in any snapshot. A project that never opts in cannot tell the feature exists,
|
|
156
|
+
which is why it is missing from `linear.config.json.example`: that file is
|
|
157
|
+
copied verbatim into new projects, and shipping the key there would opt
|
|
158
|
+
everyone in by default.
|
|
159
|
+
- **The bucket decides.** The spec's `linear_assignee_id` is pushed while the
|
|
160
|
+
spec is live (`backlog`, `in-progress`) and cleared once it reaches `complete`
|
|
161
|
+
or `cancelled` — so finishing a spec hands the issue back with no unassign
|
|
162
|
+
step for anyone to remember. The stamp stays in the file, and so does
|
|
163
|
+
`> **Developer:**`: they record who *actioned* the work, which outlives who is
|
|
164
|
+
holding it.
|
|
165
|
+
- **Unset means don't touch.** A spec that records nobody sends no assignee at
|
|
166
|
+
all, so an issue a PM assigned in Linear is never overwritten. Only an assignee
|
|
167
|
+
the repo itself pushed is ever cleared — and a snapshot written before you
|
|
168
|
+
opted in counts as "never pushed", not as "was nobody".
|
|
169
|
+
- **Only the spec issue.** Phase sub-issues are never assigned: one person builds
|
|
170
|
+
a spec, and N assigned sub-issues is N notifications for one piece of work.
|
|
171
|
+
They stay independently assignable in Linear.
|
|
172
|
+
|
|
173
|
+
**Who you are** is not configured here, and cannot be — this file is committed,
|
|
174
|
+
so a user id in it would follow the repo to every teammate who clones it. It is
|
|
175
|
+
derived from your own API key (`viewer`) and cached per machine in
|
|
176
|
+
`~/.config/skitterspec/credentials.json` beside the key. `spec-sync whoami`
|
|
177
|
+
shows it, `--set` overrides it when the key is shared or a bot's, and
|
|
178
|
+
`spec-sync users` looks somebody up by name or email.
|
|
179
|
+
|
|
180
|
+
Ownership moves with **`/spec-claim`** — take it, `--release` it, or `--to` a
|
|
181
|
+
teammate.
|
|
182
|
+
|
|
183
|
+
**Reading it back** is **`/spec-list`** (engine: `spec-sync list`), the one
|
|
184
|
+
command that asks Linear what exists instead of the repo. It lists parentless
|
|
185
|
+
issues — a phase sub-issue carries a parent, a spec issue does not — joined to
|
|
186
|
+
the local spec folder that owns each one, so every row carries the name you
|
|
187
|
+
paste into `/spec-start`. Scope is the live states by default
|
|
188
|
+
(`--state`/`--all`/`--in-progress` to change it, `--next N` for the top of the
|
|
189
|
+
backlog in Linear's own order), and `--mine`/`--by <user>` filter by assignee,
|
|
190
|
+
reusing the identity above rather than a second copy of it. It writes nothing,
|
|
191
|
+
and it says what it did not show — the count, the archived exclusion, and any
|
|
192
|
+
cap — rather than implying a completeness it never checked.
|
|
193
|
+
|
|
136
194
|
## The deployment ladder (`release.stages`)
|
|
137
195
|
|
|
138
196
|
A spec's lifecycle stops at `complete`. Where a ticket goes **after** that —
|