@skitterbyte/skitterspec-linear 10.6.0 → 10.8.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.
Files changed (34) hide show
  1. package/assets/claude-md-section.md +19 -9
  2. package/assets/commands/spec-connect.md +13 -0
  3. package/assets/commands/spec-live.md +14 -0
  4. package/assets/core/ci-stages.md +110 -0
  5. package/assets/core/env.config.md +18 -0
  6. package/assets/core/linear.config.json.example +4 -0
  7. package/assets/core/linear.config.md +88 -0
  8. package/assets/rules/commit-trailers.md +37 -5
  9. package/assets/rules/spec-planning.md +19 -4
  10. package/assets/skills/spec/SKILL.md +20 -0
  11. package/assets/skills/spec-bug/SKILL.md +48 -2
  12. package/assets/skills/spec-cancel/SKILL.md +9 -0
  13. package/assets/skills/spec-complete/SKILL.md +12 -1
  14. package/assets/skills/spec-go/SKILL.md +10 -8
  15. package/assets/skills/spec-hotfix/SKILL.md +49 -3
  16. package/assets/skills/spec-linear-setup/SKILL.md +38 -2
  17. package/assets/skills/spec-status/SKILL.md +1 -0
  18. package/assets/skills/spec-sync/SKILL.md +9 -0
  19. package/assets/skills/spec-to-main/SKILL.md +2 -1
  20. package/bin/skitterspec-linear.js +10 -0
  21. package/package.json +1 -1
  22. package/src/cli.js +211 -44
  23. package/src/env/config.js +19 -0
  24. package/src/env/teardown.js +62 -4
  25. package/src/init.js +120 -2
  26. package/src/vendor/linear/cli-sync.js +454 -34
  27. package/src/vendor/linear/config.js +158 -1
  28. package/src/vendor/linear/doctor.js +67 -1
  29. package/src/vendor/linear/released.js +149 -5
  30. package/src/vendor/sync-core/index.js +4 -1
  31. package/src/vendor/sync-core/src/compare.js +59 -3
  32. package/src/vendor/sync-core/src/normalize.js +65 -2
  33. package/assets/skills/spec-connect/SKILL.md +0 -59
  34. package/assets/skills/spec-live/SKILL.md +0 -73
@@ -818,10 +818,31 @@ function phasesWithheld(snapshotDir, config) {
818
818
 
819
819
  // --- remote projection ------------------------------------------------------
820
820
 
821
+ // The lifecycle bucket a deployment ladder descends FROM. A spec is handed over
822
+ // to the deploy pipeline once it is finished, so every rung sits downstream of
823
+ // `complete` — a ticket on "On Test" is a completed spec that has been deployed,
824
+ // not a spec that moved somewhere else.
825
+ const LADDER_ORIGIN_BUCKET = 'complete'
826
+
827
+ // The declared ladder rung with this state name, or null.
828
+ function stageForState(state, config) {
829
+ if (state == null) return null
830
+ const want = String(state).toLowerCase().trim()
831
+ const stages = (config && config.release && config.release.stages) || []
832
+ if (!Array.isArray(stages)) return null
833
+ return stages.find((s) => s && typeof s.state === 'string' && s.state.toLowerCase().trim() === want) || null
834
+ }
835
+
821
836
  // Map a remote workflow-state name back to the local lifecycle bucket (the
822
837
  // vocabulary `spec_status` uses) via config.states, so local and remote
823
838
  // workflowState hash equal when semantically equal. Falls back to a lowercased
824
839
  // raw value when the state isn't one of the configured names.
840
+ //
841
+ // A DECLARED DEPLOYMENT STAGE reads as `complete`, deliberately. Without this
842
+ // the fallback lowercases it — "On Test" becomes "on test", which equals no
843
+ // bucket — and every deployed spec reports as drifted forever, for the whole
844
+ // time it sits in the pipeline. The project told us these states are downstream
845
+ // of a finished spec, so they are not a disagreement about where the spec is.
825
846
  function bucketForState(state, config) {
826
847
  if (state == null) return null
827
848
  const states = (config && config.states) || {}
@@ -829,6 +850,7 @@ function bucketForState(state, config) {
829
850
  for (const [bucket, name] of Object.entries(states)) {
830
851
  if (typeof name === 'string' && name.toLowerCase().trim() === want) return bucket
831
852
  }
853
+ if (stageForState(state, config)) return LADDER_ORIGIN_BUCKET
832
854
  return want
833
855
  }
834
856
 
@@ -860,6 +882,13 @@ function remoteWorkflowState(issue, config) {
860
882
  return name != null ? bucketForState(name, config) : null
861
883
  }
862
884
 
885
+ // The declared ladder rung a remote issue is currently sitting on, or null.
886
+ // Pairs with `remoteWorkflowState`: that says which bucket the issue maps to,
887
+ // this says whether it got there by being deployed. Read-only.
888
+ function remoteStage(issue, config) {
889
+ return stageForState(remoteStateName(issue || {}), config)
890
+ }
891
+
863
892
  // A Linear issue title is plain text, so markdown emphasis is noise there — and
864
893
  // worse, an emphasis run cut mid-title (or a bold LABEL like `**1. Foo**`) can
865
894
  // leave a dangling `**`. Strip `*` emphasis markers and unwrap `[text](url)` to
@@ -932,11 +961,26 @@ function titleFromText(text, max = 100) {
932
961
  return stripTitleMarkup(title)
933
962
  }
934
963
 
964
+ // Every state NAME the config points at: the lifecycle bucket map, plus the
965
+ // project's deployment ladder when it declares one. Both reach Linear the same
966
+ // way and fail the same way, so both are checked by the same lookup.
967
+ function configuredStateNames(config) {
968
+ const names = Object.values((config && config.states) || {}).filter((v) => typeof v === 'string')
969
+ const stages = (config && config.release && config.release.stages) || []
970
+ for (const stage of Array.isArray(stages) ? stages : []) {
971
+ if (stage && typeof stage.state === 'string') names.push(stage.state)
972
+ }
973
+ return names
974
+ }
975
+
935
976
  // Which configured state NAMES are absent from the live workspace. The skill
936
977
  // fetches the workspace's project-status names over MCP and passes them here;
937
978
  // a non-empty result means a typo/rename that Linear would silently no-op.
979
+ //
980
+ // A project with no `release.stages` contributes nothing here, so the ladder
981
+ // cannot make this accuse a config that was fine before it existed.
938
982
  function validateStates(config, workspaceStates) {
939
- const configured = Object.values((config && config.states) || {}).filter((v) => typeof v === 'string')
983
+ const configured = configuredStateNames(config)
940
984
  const have = new Set((workspaceStates || []).map((s) => String(s).toLowerCase().trim()))
941
985
  return configured.filter((name) => !have.has(name.toLowerCase().trim()))
942
986
  }
@@ -971,13 +1015,29 @@ function stateSuggestions(config, workspaceStates) {
971
1015
  if (have.has(configured.toLowerCase().trim())) continue
972
1016
  const words = BUCKET_WORDS[bucket] || []
973
1017
  const suggestion = names.find((n) => words.includes(n.toLowerCase().trim())) || null
974
- out.push({ bucket, configured, suggestion })
1018
+ out.push({ bucket, configured, suggestion, label: `states.${bucket}` })
1019
+ }
1020
+ // Deployment-ladder rungs, reported the same way. No suggestion is offered:
1021
+ // BUCKET_WORDS describes lifecycle vocabulary, and a stage name is the
1022
+ // project's own — guessing "On Test" meant "In Progress" would be worse than
1023
+ // saying nothing.
1024
+ const stages = (config && config.release && config.release.stages) || []
1025
+ for (const stage of Array.isArray(stages) ? stages : []) {
1026
+ if (!stage || typeof stage.state !== 'string') continue
1027
+ if (have.has(stage.state.toLowerCase().trim())) continue
1028
+ out.push({
1029
+ bucket: null,
1030
+ configured: stage.state,
1031
+ suggestion: null,
1032
+ label: `release.stages[${stage.key}]`,
1033
+ })
975
1034
  }
976
1035
  return out
977
1036
  }
978
1037
 
979
1038
  module.exports = {
980
1039
  stateSuggestions,
1040
+ configuredStateNames,
981
1041
  normalizeLocal,
982
1042
  phaseProjection,
983
1043
  phaseModeFor,
@@ -994,5 +1054,8 @@ module.exports = {
994
1054
  canonicalizeMarkdown,
995
1055
  joinEmphasisAcrossBreaks,
996
1056
  bucketForState,
1057
+ stageForState,
1058
+ remoteStage,
997
1059
  remoteWorkflowState,
1060
+ LADDER_ORIGIN_BUCKET,
998
1061
  }
@@ -1,59 +0,0 @@
1
- ---
2
- name: spec-connect
3
- description: Point your local canonical origin (localhost:3000/:8080) at a spec's running dev servers so you can test a worktree's UI/API changes at the normal URL — or `spec-connect main` to hand the ports back to your main checkout. Runs `skitterspec spec-env connect` (a small bundled reverse proxy). Opt-in — needs specs/.core/env.config.json with a `dev` block. Use when the user says "/spec-connect", "test <spec> locally", "point local at <spec>", or "connect to <spec>".
4
- ---
5
-
6
- # /spec-connect — expose one spec on the canonical ports
7
-
8
- Make `http://localhost:<frontPort>` serve a **spec's** warm dev servers instead of
9
- your main checkout's, so you can test a worktree's UI/API at the exact URL you
10
- always use — no bookmark, base-URL, or OAuth-callback changes. **Exclusive:** one
11
- spec is exposed at a time. `spec-connect main` stops the proxy and hands the ports
12
- back to your primary checkout.
13
-
14
- This skill is **opt-in**: it needs `specs/.core/env.config.json` with a `dev`
15
- block (host dev servers + their `frontPort`s). If isolation or `dev` is absent,
16
- say so and stop.
17
-
18
- **Lighter alternative for a code-only spec:** `/spec-live` reuses the dev server
19
- you already have running (it branch-switches the primary checkout) instead of
20
- starting a second stack — no proxy, one process. Prefer it for code-only specs;
21
- use `/spec-connect` when a spec has its own Docker stack, or to run several stacks
22
- in parallel.
23
-
24
- ## 1. Identify the target
25
-
26
- - Use the spec named as an argument. The literal `main` means **disconnect**
27
- (hand the ports back to the primary checkout). Else use the spec **currently in
28
- context**; if unclear, ask.
29
-
30
- ## 2. Make sure the spec's dev servers are running
31
-
32
- `connect` proxies to a spec's dev servers on its reserved port block — it does
33
- **not** start them. If they aren't up yet, start them first:
34
-
35
- ```
36
- skitterspec spec-env dev up <spec>
37
- ```
38
-
39
- (This is automatic under `/spec-go`; run it by hand only when connecting a spec
40
- whose servers you stopped.)
41
-
42
- ## 3. Connect (or disconnect)
43
-
44
- ```
45
- skitterspec spec-env connect <spec> # expose <spec> on the canonical ports
46
- skitterspec spec-env connect main # stop the proxy — main owns the ports
47
- ```
48
-
49
- The engine (re)starts a small bundled Node reverse proxy and **prints** the
50
- canonical URL → spec-port mapping. **If it reports a canonical port is in use**,
51
- your **main dev server still holds it** — stop main on that port, then re-run
52
- (the proxy can't share a port main is bound to). Relay the printed message.
53
-
54
- ## 4. Report
55
-
56
- Echo which spec is now on the canonical ports (and the URLs), or that the proxy
57
- was stopped and main owns them again. Switching to a different spec is just
58
- `spec-connect <other>` — the dev servers stay warm, so it's a near-instant
59
- re-point.
@@ -1,73 +0,0 @@
1
- ---
2
- name: spec-live
3
- description: Test a spec on your already-running dev server by checking its branch out in the primary checkout — no second stack, no proxy. `spec-live <spec>` takes the running instance for that spec; `spec-live main` releases it. Runs `skitterspec spec-env live`. Opt-in — needs specs/.core/env.config.json. Code-only specs; stateful (Docker/migration) specs use /spec-connect. Use when the user says "/spec-live", "go live with <spec>", "take the instance for <spec>", or "test <spec> on the running server".
4
- ---
5
-
6
- # /spec-live — put one spec live on the running instance
7
-
8
- Instead of running a second dev stack for a spec (that's `/spec-connect`), **reuse
9
- the one instance you already have**: rebase the spec's branch onto base, hand it
10
- from its worktree to the **primary checkout**, and let your running dev server
11
- hot-reload it. You test at your normal URL, with one process. The branch that's
12
- checked out in the primary checkout **is** the lock — exactly one spec is live at
13
- a time, and `/spec-live main` hands the instance back.
14
-
15
- This skill is **opt-in**: it needs `specs/.core/env.config.json`. If isolation is
16
- absent, say so and stop.
17
-
18
- **Code-only.** Live overlay refuses a **stateful** spec — one whose `> **Stack:**`
19
- is `worktree + docker`, or whose branch changes migrations (per
20
- `env.config.json` → `live.migrations`). Those keep their isolated stack; use
21
- `/spec-connect` for them. It also **always refuses a `Type: Hotfix` spec** — its
22
- branch is built on an old release tag, so hot-reloading it onto the running dev
23
- server could break the shared instance; test a hotfix with `/spec-connect`. The
24
- engine enforces all of this and prints why.
25
-
26
- ## 1. Identify the target
27
-
28
- - Use the spec named as an argument. The literal `main` means **release** (hand
29
- the instance back to base). Else use the spec **currently in context**; if
30
- unclear, ask.
31
-
32
- ## 2. Make sure a dev server is running
33
-
34
- `live take` **verifies** a dev server is up on your canonical ports and switches
35
- the branch under it — it does **not** start one. If nothing is listening it
36
- refuses; start your dev server first (however you normally run it, or
37
- `skitterspec spec-env dev up <spec>`). (Projects with no `dev` servers configured
38
- have nothing to hot-reload — the switch still happens, with a warning.)
39
-
40
- ## 3. Take (or release)
41
-
42
- ```
43
- skitterspec spec-env live take <spec> # rebase → detach worktree → checkout in primary
44
- skitterspec spec-env live release # hand the instance back to base, re-isolate the branch
45
- skitterspec spec-env live abort # crash recovery (see below)
46
- skitterspec spec-env live status # who's live (branch in the primary checkout + receipt)
47
- ```
48
-
49
- **Take** rebases the branch onto base, frees it from its worktree
50
- (`switch --detach`), checks it out in the primary checkout, then writes a receipt
51
- (`.spec-env/live.json`). Relay its output. **If it reports the rebase hit
52
- conflicts**, it left everything untouched — rebase the branch in its worktree,
53
- resolve, then retry. **If it says a spec already holds the instance**, release it
54
- first. If it warns dependencies changed, restart your dev server after the switch.
55
-
56
- **Release** (`/spec-live main`) is the graceful exit of an unfinished session:
57
- `skitterspec spec-env live release` reads the live spec from the receipt, checks
58
- base back out in the primary checkout, re-attaches the branch to its worktree, and
59
- clears the receipt. Commit any fixes to the branch first — it refuses on a dirty
60
- tree rather than discard them. (To *finish* a live spec instead of releasing it,
61
- use `/spec-complete`, which is live-aware.)
62
-
63
- **Abort** is crash recovery, for when a session died mid-take and left the primary
64
- checkout on a feature branch: `skitterspec spec-env live abort` restores base from
65
- the receipt and re-isolates. It refuses if the primary checkout has uncommitted
66
- changes (it won't discard them) — commit or stash first.
67
-
68
- ## 4. Report
69
-
70
- Echo which spec is now live on the primary checkout (and any warning), that it was
71
- released / recovered, or — for `status` — which branch the primary checkout is on
72
- and whether the instance is free. Fixes you make while live commit straight onto
73
- the spec's branch.