@skitterbyte/skitterspec-linear 7.0.2 → 8.0.1

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.
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: spec-push
3
- description: Push a spec's local content up to its linked Linear project (repo → Linear), three-way aware and ownership-respecting. Never pushes pull-owned fields or local-only sections; aborts if Linear moved since the last sync unless --force (which backs up the remote side first). Runs `skitterspec spec-sync push` then applies the blessed writes over MCP. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-push", "push to Linear", "sync my spec up to Linear", or "update the Linear project from this spec".
3
+ description: Push a spec up to its linked Linear project (repo → Linear, one-way). The repo is the source of truth; Linear is a generated mirror. Runs `skitterspec spec-sync push` to get a create/update plan, applies it over MCP (project description/status, milestones, issues), stamps the returned ids back into the spec, then records the snapshot. Never reads Linear content back. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-push", "push to Linear", "update the Linear project from this spec".
4
4
  ---
5
5
 
6
- # /spec-push — send spec content up to Linear
6
+ # /spec-push — send a spec up to Linear (one-way)
7
7
 
8
- Repo → Linear. Sends the fields the repo owns/co-authors (description, phases,
9
- tasks per config) up to the linked project. It **never** writes `pull`-owned
10
- fields (status/priority/labels) or `localOnlySections`, and it **aborts** if
11
- Linear moved since the last sync (pull first) unless you `--force`.
8
+ Repo → Linear. The repo is the **source of truth**; Linear is a **generated
9
+ mirror**. This skill computes what changed since the last push and applies it
10
+ it never reads Linear content back or merges. A person editing the mirror in
11
+ Linear will see it overwritten on the next push.
12
12
 
13
13
  **Opt-in**: only runs when `specs/.core/linear.config.json` exists. If absent,
14
14
  tell the user how to enable Linear sync and stop.
@@ -17,58 +17,64 @@ tell the user how to enable Linear sync and stop.
17
17
 
18
18
  Use the argument, else the spec in context; ask if unclear.
19
19
 
20
- ## 2. Fetch the Linear project
20
+ ## 2. Get the plan from the engine
21
21
 
22
- - Read `linear_project_id` from `00-overview.md` frontmatter; if missing, stop
23
- (link via `/spec` first).
24
- - Discover the Linear MCP tools at runtime (project read **and** update). If
25
- Linear isn't connected — or the update tool is missing — relay the fix and stop,
26
- **writing nothing**.
27
- - Call the read tool and write the project JSON to a temp file.
22
+ ```
23
+ skitterspec spec-sync push <spec> --json
24
+ ```
28
25
 
29
- ## 3. Run the engine (the guard)
26
+ The engine prints a JSON **plan** (no network, no remote read):
30
27
 
28
+ ```json
29
+ {
30
+ "project": { "description": "…", "status": "in-progress", "priority": 2, "labels": ["…"] },
31
+ "milestones": { "create": [{ "ref": "01-outbox", "name": "…", "goal": "…" }], "update": [{ "id": "…", "name": "…", "goal": "…" }] },
32
+ "issues": { "create": [{ "ref": "<task text>", "title": "…", "description": "…", "done": false, "milestoneRef": "01-outbox" }], "update": [{ "id": "SKI-1", "title": "…", "description": "…", "done": true }] }
33
+ }
31
34
  ```
32
- skitterspec spec-sync push <spec> --remote <tempfile> --out <mergedfile> [--force]
33
- ```
34
35
 
35
- - **Refused** (`remote-moved` / `concurrent-write` / conflict) relay the message
36
- and **stop**. Do not write to Linear. Suggest `/spec-pull` first.
37
- - **OK** — the engine has confirmed it's safe, rewritten the base, and stamped
38
- `last_synced_at`. Its summary lists the `written` fields (and any `skipped`
39
- because they're not pushable).
40
- - **`--force`** — only when the user explicitly asks. Local wins after the engine
41
- backs up the remote side under `sync.backupDir`. Relay the backup path.
36
+ An empty plan (no project, no create/update) means the mirror is up to date —
37
+ say so and stop.
38
+
39
+ ## 3. Discover the Linear MCP tools
42
40
 
43
- ## 4. Apply the blessed writes to Linear
41
+ Discover project + milestone + issue **create/update** tools at runtime. If
42
+ Linear isn't connected or a needed tool is missing, relay the fix and stop,
43
+ **writing nothing**.
44
44
 
45
- Only when step 3 returned OK: for each `written` field, call the Linear update
46
- tool with that field's local value (e.g. `description` the project description).
47
- The engine has already vetted the change and moved the base so if a Linear
48
- write fails, re-run `/spec-pull` to reconcile rather than retrying blindly.
45
+ **Validate the project states first.** Fetch the workspace's project-status
46
+ names and run `skitterspec spec-sync status <spec> --workspace-states <file>`; if
47
+ it errors (a configured `states` name isn't in the workspace), stop and fix the
48
+ config Linear silently ignores an unknown project status.
49
49
 
50
- **Milestones (`milestonesPush` in the result).** When milestones are keyed, the
51
- engine can't write them itself — apply the plan over MCP:
50
+ ## 4. Apply the plan (order matters)
52
51
 
53
- - `update`: for each `{ id, name, goal }`, call the milestone-save tool with that
54
- `id` (name milestone name, goal its description).
55
- - `create`: for each `{ name, goal }`, call the milestone-save tool with no id to
56
- create it under the project, then **stamp the returned milestone id** into the
57
- matching phase file's frontmatter (`linear_milestone_id`) so it links on the
58
- next sync. Match the phase file by its title.
52
+ 1. **Milestones create** → create each in Linear; for each, stamp the returned id
53
+ into its phase file: the `ref` is the phase-file basename.
54
+ 2. **Issues create** create each (link to its milestone by `milestoneRef`,
55
+ resolving a `create` ref to the id just minted); stamp the returned identifier
56
+ back onto the matching task line (`ref` is the task's text).
57
+ 3. **Milestones/issues update** save by `id`.
58
+ 4. **Project** → set description + status (map `status` — the local bucket — to
59
+ the Linear project-status name via `config.states`). Priority, labels, cycles
60
+ and comments are Linear-native triage — do **not** push them; they're the PM's.
59
61
 
60
- Progress is Linear-derived never push it.
62
+ Map the local status bucket to Linear's project status through `config.states`
63
+ (e.g. `complete → Completed`).
61
64
 
62
- **Issues (`issuesPush` in the result).** When tasks are keyed:
65
+ ## 5. Record the snapshot
66
+
67
+ After everything applied and the ids are stamped into the files:
68
+
69
+ ```
70
+ skitterspec spec-sync record <spec>
71
+ ```
63
72
 
64
- - `update`: for each `{ id, text, done }`, call the issue-save tool with that `id`
65
- (text title; `done` a completed state, else a non-completed state — leave an
66
- already-non-completed issue's exact state untouched).
67
- - `create`: for each `{ text, done }`, create an issue under the project (attach it
68
- to the milestone of the phase the task lives in when known), then **stamp the new
69
- issue identifier inline** on that task line (`… (SKI-123)`), matching by text.
73
+ This writes the last-pushed snapshot from the now-stamped files, so the next
74
+ `/spec-push` produces an empty plan. Commit the stamped spec + snapshot into the
75
+ branch so the mirror-link rides in the PR.
70
76
 
71
- ## 5. Report
77
+ ## 6. Report
72
78
 
73
- Relay the git-like summary (written / skipped / backup / base) plus which Linear
74
- fields you updated.
79
+ Summarise what was created/updated in Linear and confirm the snapshot was
80
+ recorded. There is no pull — Linear is a generated mirror.
@@ -1,46 +1,51 @@
1
1
  ---
2
2
  name: spec-status
3
- description: Show a spec's sync status against its linked Linear project — a read-only, git-status-style per-field divergence (local-only / remote-only / conflict / in-sync). Fetches the Linear project over MCP and runs `skitterspec spec-sync status`. Changes nothing. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-status", "is this spec in sync with Linear", "what's diverged from Linear", or "show spec sync status".
3
+ description: Show a spec's one-way sync status against Linear — a read-only drift report. Reports whether the spec changed since the last push (there's something to push) and, optionally, whether Linear's workflow-state differs from the spec's. Fetches the Linear project over MCP and runs `skitterspec spec-sync status`. Changes nothing. Opt-in — needs specs/.core/linear.config.json. Use when the user says "/spec-status", "is this spec in sync with Linear", "what would push", or "show spec sync status".
4
4
  ---
5
5
 
6
- # /spec-status — show a spec's divergence from Linear
6
+ # /spec-status — one-way sync drift report
7
7
 
8
- Read-only. Prints, per field, whether the spec and its linked Linear project have
9
- diverged since the last sync — the `git status` of the hybrid sync. Writes
10
- nothing to either side.
8
+ Read-only. Reports two things and writes nothing:
11
9
 
12
- This skill is **opt-in**: it only runs when `specs/.core/linear.config.json`
13
- exists. If it's absent, tell the user to copy `linear.config.json.example`
14
- `linear.config.json` to enable Linear sync, and stop.
10
+ 1. **Pending push** has the spec changed since the last push (are there
11
+ milestones/issues/description to create or update)?
12
+ 2. **State drift** — does Linear's project workflow-state differ from the spec's
13
+ status? (The repo wins on the next push; this is just a heads-up, e.g. a card
14
+ moved in Linear.)
15
+
16
+ The repo is the source of truth; Linear is a generated mirror, so there is no
17
+ per-field "conflict" — only "what would the next push send" and "did the mirror
18
+ drift".
19
+
20
+ **Opt-in**: only runs when `specs/.core/linear.config.json` exists. If absent,
21
+ tell the user how to enable Linear sync and stop.
15
22
 
16
23
  ## 1. Identify the target spec
17
24
 
18
- Use the spec named as an argument, else the spec **currently in context**. If
19
- neither is clear, ask which spec.
25
+ Use the argument, else the spec in context; ask if unclear.
26
+
27
+ ## 2. Fetch the Linear project (optional, for drift)
20
28
 
21
- ## 2. Fetch the Linear project (read-only)
29
+ If the spec has a `linear_project_id`, discover the Linear MCP read tool and write
30
+ the project JSON to a temp file — this lets the report compare workflow-state. If
31
+ Linear isn't connected, skip the drift line (still report pending-push).
22
32
 
23
- - Read the spec's `linear_project_id` from `00-overview.md` frontmatter. If it's
24
- missing, the spec isn't linked yet — say so and stop (link it via `/spec`).
25
- - Discover the connected Linear MCP tools at runtime (the project-read tool). If
26
- Linear isn't connected, relay "connect the `linear` MCP server" and stop — do
27
- nothing else.
28
- - Call the project-read tool for that id and write the returned JSON to a temp
29
- file (e.g. under the OS temp dir).
33
+ Optionally fetch the workspace project-status names to a file to validate the
34
+ configured `states` at the same time.
30
35
 
31
36
  ## 3. Run the engine
32
37
 
33
38
  ```
34
- skitterspec spec-sync status <spec> --remote <tempfile>
39
+ skitterspec spec-sync status <spec> [--remote <projectfile>] [--workspace-states <statesfile>]
35
40
  ```
36
41
 
37
- The engine does the three-way compare (local vs Linear vs the committed base) and
38
- prints each diverged field with its classification and sync direction. Without
39
- `--remote` it falls back to a local-vs-base comparison (still read-only).
42
+ - Reports `push: pending N to create, M to update` or `up to date`.
43
+ - With `--remote`, adds a `drift:` line comparing Linear's workflow-state to the
44
+ spec's status.
45
+ - With `--workspace-states`, fails loudly if a configured state name isn't in the
46
+ workspace (Linear would silently no-op it).
40
47
 
41
48
  ## 4. Report
42
49
 
43
- Relay the engine's summary verbatim, then offer the natural next step:
44
- `/spec-pull` for remote-only changes, `/spec-push` for local-only, and — for a
45
- `conflict` — resolve locally or use `--force` (which backs up the losing side).
46
- Never write anything from this skill.
50
+ Relay the engine's output verbatim. Suggest `/spec-push` if a push is pending.
51
+ Never write to either side.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skitterbyte/skitterspec-linear",
3
- "version": "7.0.2",
4
- "description": "Spec-driven development for Claude Code, with Linear hybrid-sync — a superset of @skitterbyte/skitterspec: the base filesystem workflow plus git-like /spec-status · /spec-pull · /spec-push and the spec-sync CLI. Install this OR the base, not both.",
3
+ "version": "8.0.1",
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",
7
7
  "claude-code",
package/src/init.js CHANGED
@@ -259,6 +259,30 @@ function removeRetiredFiles(dir) {
259
259
  }
260
260
  }
261
261
 
262
+ // Prune a file the manifest records as managed but the current package no longer
263
+ // ships — a retired skill (e.g. `spec-pull` after the one-way switch), rule, or
264
+ // template. Without this, upgrading leaves a live, model-visible skill on disk
265
+ // whose instructions invoke a command that no longer exists. Delete it only when
266
+ // PRISTINE (still matches the hash we last wrote) so a user edit is never lost; a
267
+ // customized retired file is kept with a warning. An emptied skill folder is
268
+ // removed. Takes the pre-flush manifest (which still holds the retired entries).
269
+ function pruneRetiredManaged(dir, manifest) {
270
+ const managedRel = new Set(managedTargets(dir).map((t) => t.relPath))
271
+ for (const relPath of Object.keys(manifest.files || {})) {
272
+ if (managedRel.has(relPath)) continue // still shipped by this version
273
+ const abs = path.join(dir, relPath)
274
+ if (!fs.existsSync(abs)) continue // already gone
275
+ if (managedState(dir, relPath, manifest) === 'customized') {
276
+ report.warnings.push(`retired but kept (you edited it): ${relPath} — delete manually if unused`)
277
+ continue
278
+ }
279
+ fs.unlinkSync(abs)
280
+ report.removed.push(rel(dir, abs))
281
+ const folder = path.dirname(abs)
282
+ if (fs.existsSync(folder) && !fs.readdirSync(folder).length) fs.rmdirSync(folder)
283
+ }
284
+ }
285
+
262
286
  // Scaffold the opt-in isolation templates into specs/.core/ (the example config
263
287
  // + its field docs). Copied, not activated: the feature stays off until the
264
288
  // consumer copies env.config.json.example → env.config.json.
@@ -409,6 +433,7 @@ function resync(dir, { force = false, claudeMd = true } = {}) {
409
433
  for (const t of managedTargets(dir)) resyncManagedFile(dir, t, manifest, force)
410
434
  installFolders(dir)
411
435
  removeRetiredFiles(dir)
436
+ pruneRetiredManaged(dir, manifest)
412
437
  if (claudeMd) installClaudeMd(dir, { mode: 'update' })
413
438
  flushManifest(dir)
414
439
  printReport(dir, 'resync')
Binary file