@skitterbyte/skitterspec-linear 10.4.0 → 10.5.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.
@@ -237,7 +237,19 @@ the repo.
237
237
 
238
238
  ## 8. Smoke test (verify your setup)
239
239
 
240
- With a linked spec, confirm push end-to-end:
240
+ **Start here one command answers "did it work?":**
241
+
242
+ ```
243
+ skitterspec spec-sync doctor # every layer, offline
244
+ skitterspec spec-sync doctor --check-remote # …and prove the key reaches Linear
245
+ ```
246
+
247
+ It reports the scaffold, per-spec isolation, the tracker config and the API key
248
+ in one table, and every row that needs attention names the command that fixes
249
+ it. It exits non-zero only when something is **broken** (configured but wrong) —
250
+ a `missing` row is an opt-in you have not taken, which is fine.
251
+
252
+ Then, with a linked spec, confirm push end-to-end:
241
253
 
242
254
  1. `/spec-status` → shows what would push (`pending — N to create, M to update`).
243
255
  2. `/spec-push` → creates the spec issue and its phase sub-issues and sets the
@@ -29,7 +29,8 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
29
29
  // IDs are read by the MCP adapter; leave blank until you connect the `linear`
30
30
  // MCP server.
31
31
  "linear": {
32
- "teamKey": "", // human-facing key, e.g. "ENG" (optional)
32
+ "teamKey": "", // human-facing key, e.g. "ENG" — the RECORDED key;
33
+ // see "Renaming a team" below (optional)
33
34
  "teamId": "", // Linear team UUID (the issue's team)
34
35
  "projectId": "" // DEFAULT for the project picker (see below)
35
36
  },
@@ -483,3 +484,28 @@ Linear-native triage and are never touched. No base merge, no conflicts, no
483
484
  last-pushed snapshot is content hashes of the last push, so `/spec-push` knows
484
485
  what changed without reading Linear back; each worktree carries its own, so it
485
486
  must travel with the branch.
487
+
488
+ ## Renaming a team
489
+
490
+ Renaming a Linear team rewrites the key in every issue identifier (`ENG-7` →
491
+ `PLT-7`). The repo stamps identifiers in three places — spec frontmatter, the
492
+ `linear-base` snapshot filenames, and the `subIssues` keys inside them — and
493
+ nothing moves them, so afterwards `/spec-push` fails with
494
+ `no Linear issue found for ENG-7`.
495
+
496
+ ```
497
+ skitterspec spec-sync retarget # what would change
498
+ skitterspec spec-sync retarget --yes # apply it
499
+ ```
500
+
501
+ `teamId` survives a rename, so the command asks Linear for that team's *current*
502
+ key and compares it with `teamKey` — which is why `teamKey` is worth setting even
503
+ though nothing else reads it. With `teamKey` empty it falls back to the prefix
504
+ observed in the stamps, and refuses if those disagree rather than guessing.
505
+
506
+ Before reporting a plan as safe it resolves one remapped identifier and compares
507
+ its **title** to the spec's: a team rename preserves issue numbers, and this is
508
+ what proves it did. `--yes` refuses on a dirty tree so the rewrite lands as one
509
+ revertable change, and it rewrites machine-read fields only — identifiers in
510
+ spec prose are the historical record and are left alone. It pushes nothing; the
511
+ next ordinary `/spec-push` reconciles content once its issues resolve again.
@@ -0,0 +1,69 @@
1
+ # Checks That Accuse
2
+
3
+ A check **accuses** when being wrong costs something: it deletes, it exits
4
+ non-zero, or it tells the user their code is broken. Those checks earn the four
5
+ rules below. An ordinary conditional does not — this is about the ones that act
6
+ on what they conclude.
7
+
8
+ Almost every accusation begins as an **absence**: a name not in a list, a
9
+ directory not on disk, a version not in a response. An absence is evidence only
10
+ once you have established that the lookup could have seen the thing. Three
11
+ times in one day, in unrelated code, we established nothing and acted anyway:
12
+
13
+ | Absence observed | Concluded | What had blinded the lookup |
14
+ |------------------|-----------|-----------------------------|
15
+ | ref not in the issue list | "it does not exist" | the query excluded archived issues, and capped at 250 |
16
+ | version not in the registry's list | "the publish failed" | the registry is eventually consistent |
17
+ | lifecycle folder not on disk | "half-installed" | git does not store an empty directory |
18
+
19
+ The bills: 146 healthy refs accused, a valid release tag deleted, a non-zero
20
+ exit on a healthy repo. Each blind spot was knowable in advance.
21
+
22
+ ## 1. Prefer a positive signal to an absence
23
+
24
+ Assert something that must be **present**, not something that must not be
25
+ missing. A positive signal fails loudly when you are wrong about it; an absence
26
+ fails silently whenever the lookup was narrower than you assumed.
27
+
28
+ The scaffold check above stopped asking "is the lifecycle folder there?" — a
29
+ folder git drops as soon as it empties — and started asking whether the config
30
+ folder the installer always writes into is there. Same intent, and the new
31
+ question has an answer.
32
+
33
+ Where no positive signal exists, widen the lookup until absence means
34
+ something (include the archived rows, ask the API for the one id rather than
35
+ scanning a page) — or do not conclude.
36
+
37
+ ## 2. Name the blind spot beside the check
38
+
39
+ A comment naming what could make this lookup lie is what makes the next reader
40
+ check it. Not what the code does — what would fool it:
41
+
42
+ ```js
43
+ // A LIFECYCLE FOLDER IS NOT CHECKED, deliberately. git does not track empty
44
+ // directories, so it disappears whenever the bucket empties and returns the
45
+ // moment something lands in it.
46
+ ```
47
+
48
+ Write it when you write the check, while you still know why it is safe.
49
+
50
+ ## 3. Pair every accusation with a stays-silent test
51
+
52
+ For each accusing check, a test that feeds it a **healthy but unusual** input
53
+ and asserts it says nothing: the empty bucket, the archived record, the
54
+ just-published version, the file the user edited on purpose. The positive test
55
+ proves the check can fire; only this one proves it does not fire at everyone
56
+ else. All three incidents would have been caught by it, and prose alone had
57
+ already failed to prevent them.
58
+
59
+ ## 4. Bias the unknown case toward inaction
60
+
61
+ Three states, not two: yes, no, and *cannot tell*. Route the third to the
62
+ harmless branch — skip, warn, keep, retry — never to the destructive one.
63
+
64
+ The install manifest classifies a file whose hash it does not recognise as
65
+ `customized` rather than stale, so a resync **keeps** it (`managedState`,
66
+ `packages/common/src/init.js`). An unrecognised hash could mean a user's edit or
67
+ a lost manifest; only one of those readings is safe to act on, so it takes that
68
+ one. Being wrong there costs a redundant file on disk. The opposite default
69
+ costs the user their work.
@@ -159,22 +159,33 @@ paraphrase it into "done".
159
159
 
160
160
  ## 8. Report and hand off
161
161
 
162
- Confirm the file written and what it says, then name the next step:
162
+ **Finish by checking, not by describing.** Run:
163
+
164
+ ```
165
+ skitterspec spec-sync doctor
166
+ ```
167
+
168
+ and relay its table. That is the difference between a summary of what setup
169
+ *meant* to do and a check of what is actually true — including the layers this
170
+ skill never touched (the scaffold, isolation) and the one it deliberately does
171
+ not set (the key). Every row that needs attention names its own fix, so there is
172
+ nothing to paraphrase.
173
+
174
+ It exits non-zero only when a layer is **broken** — configured but wrong. A
175
+ `missing` row is an opt-in nobody took, which is fine; report it, don't treat it
176
+ as a failure.
177
+
178
+ Then name the next step:
163
179
 
164
180
  - `/spec` — write a spec; with Linear configured it offers the project picker,
165
181
  creates the linked issue and stamps the id.
166
182
  - `/spec-status` — read-only drift report, the safe way to prove the link works.
167
183
  - `/spec-push` — send a spec up.
168
184
 
169
- Then **check the API key, but never ask for it.** Run:
170
-
171
- ```
172
- skitterspec spec-sync credentials status
173
- ```
174
-
175
- It reports whether a key is set and where from, and never prints the value. If
176
- it says `key: not set`, tell the user to run this **themselves, in their own
177
- terminal**:
185
+ **Never ask for the API key.** `doctor`'s `key` row already reports whether one
186
+ is set and where from, masked — `spec-sync credentials status` says the same in
187
+ more detail if you need it. If the key is missing, tell the user to run this
188
+ **themselves, in their own terminal**:
178
189
 
179
190
  ```
180
191
  skitterspec spec-sync credentials set
@@ -46,6 +46,24 @@ skitterspec spec-sync status <spec> [--remote <issuefile>] [--workspace-states <
46
46
  - With `--workspace-states`, fails loudly if a configured state name isn't in the
47
47
  workspace (Linear would silently no-op it).
48
48
 
49
+ ## 3b. A key mismatch is a different problem
50
+
51
+ If the spec's `linear_identifier` carries a **different team key** than
52
+ `linear.teamKey` in `specs/.core/linear.config.json` — or the issue read in step
53
+ 2 came back under another key — the team was renamed and the repo's stamps are
54
+ stale. That is not push drift and `/spec-push` cannot fix it: it will fail with
55
+ `no Linear issue found for <old>-<n>`.
56
+
57
+ Point the user at the CLI and stop:
58
+
59
+ ```
60
+ pnpm exec skitterspec-linear spec-sync retarget
61
+ ```
62
+
63
+ Read-only until `--yes`. Do not attempt the rewrite by hand — the identifiers
64
+ live in frontmatter, snapshot filenames and the keys inside those snapshots, and
65
+ a hand edit misses some (it has, twice).
66
+
49
67
  ## 4. Report
50
68
 
51
69
  Relay the engine's output verbatim. Suggest `/spec-push` if a push is pending.
@@ -36,7 +36,7 @@ In a project that installs the Linear superset the binary is
36
36
  | "did the mirror survive the push?" | `verify <spec> --stored <file>` |
37
37
  | "link this spec to KEY-1 by hand" | `stamp <spec> --issue KEY-1` |
38
38
  | "mirror the whole backlog / every complete spec" | `apply --all <bucket>` — **confirm first** |
39
- | "is the team key stale?", "did Linear get renamed?" | `doctor` |
39
+ | "is the team key stale?", "did Linear get renamed?" | `retarget` |
40
40
  | push one spec, or "what would push?" | **defer** — see below |
41
41
 
42
42
  **With no argument, run `linked`.** It is the repo-wide overview, it is
@@ -131,44 +131,34 @@ its object exists, so an interrupted run continues rather than duplicating.
131
131
 
132
132
  `--all` refuses over MCP by design — bulk goes through the API path.
133
133
 
134
- ## 7. `doctor` — identifier drift after a team rename
134
+ ## 7. `retarget` — after a Linear team is renamed
135
135
 
136
136
  ```
137
- pnpm exec skitterspec-linear spec-sync doctor [--json]
137
+ pnpm exec skitterspec-linear spec-sync retarget [--yes]
138
138
  ```
139
139
 
140
- Renaming a Linear team changes every issue's identifier prefix and **nothing in
141
- the repo moves**: the stamps, the config `teamKey`, and the snapshot filenames
142
- and their sub-issue keys all keep the old prefix. `doctor` reports that. It is
143
- read-only and needs the **API transport** — it reads one issue per drifted ref,
144
- which over MCP would be a model round-trip each.
145
-
146
- It reports three things, deliberately separately:
147
-
148
- - **drift** what `--write` would repair: stamps, snapshot filenames and keys,
149
- the config key.
150
- - **mentions** stale refs in spec *prose* (`(REU-61)` beside a task). Reported,
151
- **never rewritten** say so, so nobody reads a repair as total.
152
- - **missing** refs that resolve to no issue under the new key. A different
153
- problem; repair leaves them alone.
154
-
155
- A `missing` count that looks alarmingly high is worth a second look before you
156
- relay it as fact the first hand-run of this check reported 146 of 198 refs as
157
- non-existent when every one was healthy and merely archived.
158
-
159
- **`--write` repairs it — confirm first, and state the counts.** It rewrites the
160
- config key, the frontmatter stamps, the snapshot filenames and the identifier
161
- keys inside them, all together. It refuses on a dirty git tree, because the
162
- repair is one large diff and has to be reviewable (and `git checkout -- .`-able)
163
- on its own — so commit or stash before offering it.
164
-
165
- It exits non-zero when it left anything behind: a ref that resolves to no issue
166
- is **not** rewritten, because repair fixes what is provably repairable rather
167
- than inventing a target. Relay that as an unfinished repair, not a success.
168
-
169
- After a repair, `spec-sync status` should still read `up to date` — the snapshot
170
- hashes are content-derived, so only their keys move. If it does not, say so
171
- rather than pushing over it.
140
+ Renaming a Linear team rewrites the key in every issue identifier, and **nothing
141
+ in the repo moves**: the frontmatter stamps, the config `teamKey`, and the
142
+ snapshot filenames and their sub-issue keys all keep the old prefix, so
143
+ `/spec-push` starts failing with `no Linear issue found for SKI-7`.
144
+
145
+ `retarget` detects the rename (the team id survives it; the key does not) and
146
+ rewrites those fields. Read-only until `--yes`.
147
+
148
+ - It **never takes the new key as an argument** a typo would rewrite every
149
+ stamp to a key that does not exist.
150
+ - It **spot-checks one identifier by title** before reporting the plan as safe.
151
+ Existence is not identity: `SKS-7` existing does not make it the issue that was
152
+ `SKI-7`. A mismatch refuses, and writes nothing.
153
+ - It rewrites **machine-read fields only**. Identifiers in spec prose are the
154
+ historical record and are left alone — so do not report a retarget as having
155
+ made the repo free of the old key.
156
+ - `--yes` refuses on a dirty tree, so the rewrite lands as one revertable change.
157
+ It pushes nothing: only the repo's stamps move, and the next ordinary
158
+ `/spec-push` reconciles content now that its issues resolve again.
159
+
160
+ Over MCP the team key is unreadable (`get_team` does not return it), so it says
161
+ so and asks you to confirm the key rather than guessing.
172
162
 
173
163
  ## 8. Report
174
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec-linear",
3
- "version": "10.4.0",
3
+ "version": "10.5.0",
4
4
  "description": "Spec-driven development for Claude Code, with one-way Linear sync — a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status · /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
5
5
  "keywords": [
6
6
  "claude",
package/src/init.js CHANGED
@@ -406,6 +406,11 @@ function installClaudeMd(dir, { mode }) {
406
406
 
407
407
  // True when the repo looks already set up: any managed file present, any spec
408
408
  // lifecycle folder, or the CLAUDE.md spec marker (Decision 1 — detect eagerly).
409
+ // Is skitterspec already installed here? Matched on what we ACTUALLY install —
410
+ // our managed files, our lifecycle folders, our CLAUDE.md marker — never on
411
+ // `.claude/` merely existing: someone else's skills are not evidence of ours,
412
+ // and reading them as ours would treat every Claude Code project as a
413
+ // half-finished install.
409
414
  function isExistingSetup(dir) {
410
415
  if (managedTargets(dir).some((t) => fs.existsSync(t.abs))) return true
411
416
  if (SPEC_FOLDERS.some((f) => fs.existsSync(path.join(dir, 'specs', f)))) return true
@@ -234,7 +234,7 @@ function makeApiAdapter({ apiKey, fetch: fetchImpl, endpoint, sleep, maxRetries
234
234
  if (data && data.team) return (data.team.projects && data.team.projects.nodes) || []
235
235
  return (data && data.projects && data.projects.nodes) || []
236
236
  },
237
- // The team's CURRENT key, which is what `doctor` compares stamped
237
+ // The team's CURRENT key, which is what `retarget` compares stamped
238
238
  // identifiers against. Read from Linear rather than `config.linear.teamKey`
239
239
  // on purpose: the config key is itself one of the things that goes stale
240
240
  // when a team is renamed, so trusting it would make drift invisible. The