@skitterbyte/skitterspec-jira 0.0.0-stage → 2.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.
Files changed (113) hide show
  1. package/MIGRATION.md +1266 -0
  2. package/README.md +44 -2
  3. package/assets/claude-md-section.md +85 -0
  4. package/assets/commands/allow-main.md +33 -0
  5. package/assets/commands/spec-connect.md +13 -0
  6. package/assets/commands/spec-live.md +14 -0
  7. package/assets/commands/spec-remote-review.md +37 -0
  8. package/assets/commands/spec-skip.md +24 -0
  9. package/assets/core/SETUP.md +192 -0
  10. package/assets/core/env.config.json.example +56 -0
  11. package/assets/core/env.config.md +606 -0
  12. package/assets/core/gating.config.json.example +4 -0
  13. package/assets/core/gating.config.md +81 -0
  14. package/assets/core/jira.config.json.example +18 -0
  15. package/assets/core/jira.config.md +88 -0
  16. package/assets/hooks/main-guard.cjs +162 -0
  17. package/assets/hooks/review-gate.cjs +146 -0
  18. package/assets/review/page.html +3384 -0
  19. package/assets/rules/commit-trailers.md +96 -0
  20. package/assets/rules/negative-checks.md +69 -0
  21. package/assets/rules/offered-unblocks.md +100 -0
  22. package/assets/rules/spec-planning.md +702 -0
  23. package/assets/rules/spec-reports.md +572 -0
  24. package/assets/skills/no-spec/SKILL.md +249 -0
  25. package/assets/skills/spec/SKILL.md +714 -0
  26. package/assets/skills/spec-bug/SKILL.md +585 -0
  27. package/assets/skills/spec-cancel/SKILL.md +261 -0
  28. package/assets/skills/spec-complete/SKILL.md +451 -0
  29. package/assets/skills/spec-diff/SKILL.md +870 -0
  30. package/assets/skills/spec-hotfix/SKILL.md +607 -0
  31. package/assets/skills/spec-init/SKILL.md +178 -0
  32. package/assets/skills/spec-jira-setup/SKILL.md +221 -0
  33. package/assets/skills/spec-list/SKILL.md +173 -0
  34. package/assets/skills/spec-next/SKILL.md +678 -0
  35. package/assets/skills/spec-push/SKILL.md +306 -0
  36. package/assets/skills/spec-review/SKILL.md +256 -0
  37. package/assets/skills/spec-reviewed/SKILL.md +284 -0
  38. package/assets/skills/spec-start/SKILL.md +496 -0
  39. package/assets/skills/spec-status/SKILL.md +114 -0
  40. package/assets/skills/spec-sync/SKILL.md +220 -0
  41. package/assets/skills/spec-to-main/SKILL.md +141 -0
  42. package/bin/skitterspec-jira.js +68 -0
  43. package/package.json +40 -4
  44. package/src/cli.js +5967 -0
  45. package/src/deprecate.js +138 -0
  46. package/src/env/bootstrap.js +188 -0
  47. package/src/env/building.js +143 -0
  48. package/src/env/classify.js +176 -0
  49. package/src/env/commitcmd.js +108 -0
  50. package/src/env/config.js +850 -0
  51. package/src/env/dev.js +63 -0
  52. package/src/env/glob.js +34 -0
  53. package/src/env/hooks.js +226 -0
  54. package/src/env/hotfix.js +413 -0
  55. package/src/env/hotfixland.js +302 -0
  56. package/src/env/integrate.js +167 -0
  57. package/src/env/live.js +479 -0
  58. package/src/env/mainguard.js +299 -0
  59. package/src/env/provision.js +466 -0
  60. package/src/env/proxy.js +225 -0
  61. package/src/env/prune.js +119 -0
  62. package/src/env/registry.js +159 -0
  63. package/src/env/render.js +17 -0
  64. package/src/env/resolve.js +716 -0
  65. package/src/env/review.js +2622 -0
  66. package/src/env/reviewers/coderabbit.js +228 -0
  67. package/src/env/reviewers.js +517 -0
  68. package/src/env/serve.js +1044 -0
  69. package/src/env/supervise.js +157 -0
  70. package/src/env/teardown.js +248 -0
  71. package/src/env/trust.js +87 -0
  72. package/src/gating.js +155 -0
  73. package/src/init.js +1150 -0
  74. package/src/lines-diff.js +114 -0
  75. package/src/prompts.js +129 -0
  76. package/src/vendor/jira/adf.js +414 -0
  77. package/src/vendor/jira/api.js +295 -0
  78. package/src/vendor/jira/cli-sync.js +1252 -0
  79. package/src/vendor/jira/commands.js +44 -0
  80. package/src/vendor/jira/config.js +286 -0
  81. package/src/vendor/jira/credentials.js +108 -0
  82. package/src/vendor/jira/dialect.js +45 -0
  83. package/src/vendor/jira/mcp.js +129 -0
  84. package/src/vendor/provider-kit/index.js +41 -0
  85. package/src/vendor/provider-kit/src/adapter.js +23 -0
  86. package/src/vendor/provider-kit/src/apply.js +207 -0
  87. package/src/vendor/provider-kit/src/args.js +68 -0
  88. package/src/vendor/provider-kit/src/assignment.js +277 -0
  89. package/src/vendor/provider-kit/src/config.js +251 -0
  90. package/src/vendor/provider-kit/src/credentials.js +404 -0
  91. package/src/vendor/provider-kit/src/doctor.js +35 -0
  92. package/src/vendor/provider-kit/src/http.js +63 -0
  93. package/src/vendor/provider-kit/src/identity.js +93 -0
  94. package/src/vendor/provider-kit/src/list-assignee.js +71 -0
  95. package/src/vendor/provider-kit/src/list-rows.js +35 -0
  96. package/src/vendor/provider-kit/src/md-compare.js +39 -0
  97. package/src/vendor/provider-kit/src/md-tables.js +94 -0
  98. package/src/vendor/provider-kit/src/prompt.js +50 -0
  99. package/src/vendor/provider-kit/src/released.js +219 -0
  100. package/src/vendor/provider-kit/src/specs.js +248 -0
  101. package/src/vendor/provider-kit/src/transport.js +22 -0
  102. package/src/vendor/sync-core/index.js +92 -0
  103. package/src/vendor/sync-core/src/base.js +81 -0
  104. package/src/vendor/sync-core/src/compare.js +338 -0
  105. package/src/vendor/sync-core/src/dialect.js +51 -0
  106. package/src/vendor/sync-core/src/fields.js +36 -0
  107. package/src/vendor/sync-core/src/legacy.js +100 -0
  108. package/src/vendor/sync-core/src/normalize.js +971 -0
  109. package/src/vendor/sync-core/src/push.js +85 -0
  110. package/src/vendor/sync-core/src/retarget.js +278 -0
  111. package/src/vendor/sync-core/src/task-block.js +308 -0
  112. package/src/vendor/sync-core/src/verify.js +67 -0
  113. package/src/vendor/sync-core/src/write.js +191 -0
@@ -0,0 +1,119 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Pure planner for `spec-env prune` — the orphaned-test-DB reaper.
5
+ *
6
+ * Per-spec isolation gives each Docker-escalated spec its own compose stack
7
+ * namespaced by `{repoSlug}_{slug}`; the DB storage is a Docker named volume
8
+ * (`{repoSlug}_{slug}_<volname>`). Those volumes only ever get dropped by an
9
+ * explicit single-spec `spec-env down`, so they leak whenever that path is
10
+ * skipped (declined/guard-aborted teardown, manual `git worktree remove`,
11
+ * `--keep-volumes`). This planner reconciles the live namespace volumes against
12
+ * the specs that are still live and returns the `docker volume rm` commands for
13
+ * the orphans.
14
+ *
15
+ * **Liveness = an existing worktree, NOT the slot registry.** The registry is
16
+ * exactly what goes stale (a declined teardown leaves both the slot and the
17
+ * volume behind), so the caller derives `liveSlugs` from real worktrees and the
18
+ * registry is only reconciled afterwards. See the spec Decisions.
19
+ *
20
+ * We never parse a slug out of an orphan. Instead we build a protected-prefix
21
+ * set from the live slugs (`{repoSlug}_{slug}_`) and keep any volume matching
22
+ * one — everything else in the `{repoSlug}_` namespace is an orphan. The
23
+ * trailing `_` makes the slug match exact: slugs are kebab-case (`[a-z0-9-]`)
24
+ * and `_` is the compose project/volume separator, so `add` never protects
25
+ * `add-widget`.
26
+ *
27
+ * Side-effect free and deterministic: no `Date.now()` — the caller supplies
28
+ * `now` (and per-volume `createdAt`) when age-gating.
29
+ */
30
+
31
+ const { splitPrefix } = require('./resolve.js')
32
+
33
+ /**
34
+ * @param {Array<string|{name:string,createdAt?:number|null}>} volumes
35
+ * live Docker volume names (or objects carrying `createdAt` epoch-ms for age
36
+ * gating). Plain strings are treated as unknown-age.
37
+ * @param {Iterable<string>} liveSlugs slugs of specs that still have a worktree.
38
+ * @param {object} opts { repoSlug, olderThanDays?, now? }
39
+ * `repoSlug` (required) is the namespace prefix. `olderThanDays` (optional)
40
+ * keeps only orphans strictly older than the cutoff; when set, `now`
41
+ * (epoch-ms) is required and volumes of unknown age are conservatively kept.
42
+ * @returns {{ orphans: Array<{name:string,createdAt:number|null}>, commands: string[] }}
43
+ */
44
+ function planPrune(volumes, liveSlugs, opts = {}) {
45
+ const { repoSlug, olderThanDays = null, now = null } = opts
46
+ if (!repoSlug) throw new Error('planPrune: opts.repoSlug is required')
47
+
48
+ const namespace = `${repoSlug}_`
49
+ const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || [])
50
+ const protectedPrefixes = [...live].map((slug) => `${repoSlug}_${slug}_`)
51
+
52
+ let orphans = (volumes || [])
53
+ .map((v) => (typeof v === 'string' ? { name: v, createdAt: null } : v))
54
+ .filter((v) => {
55
+ const name = v && v.name
56
+ if (!name || !name.startsWith(namespace)) return false
57
+ return !protectedPrefixes.some((p) => name.startsWith(p))
58
+ })
59
+ .map((v) => ({ name: v.name, createdAt: v.createdAt == null ? null : v.createdAt }))
60
+
61
+ if (olderThanDays != null) {
62
+ if (now == null) throw new Error('planPrune: olderThanDays requires opts.now')
63
+ const cutoff = now - olderThanDays * 24 * 60 * 60 * 1000
64
+ // Unknown age (createdAt == null) is kept — never drop what we can't date.
65
+ orphans = orphans.filter((v) => v.createdAt != null && v.createdAt <= cutoff)
66
+ }
67
+
68
+ const commands = orphans.map((v) => `docker volume rm ${v.name}`)
69
+ return { orphans, commands }
70
+ }
71
+
72
+ /**
73
+ * Derive the set of live spec slugs from the specs that still have a git
74
+ * worktree. The **worktree is the liveness signal** (see module header): the
75
+ * registry is not consulted here. Pure — the caller supplies the spec list and
76
+ * the set of live worktree paths (both gathered via IO).
77
+ *
78
+ * @param {Array<{slug:string, worktreePath:string}>} specs
79
+ * @param {Set<string>|Iterable<string>} liveWorktreePaths absolute paths.
80
+ * @returns {Set<string>} slugs whose worktree currently exists.
81
+ */
82
+ function liveSlugsForSpecs(specs, liveWorktreePaths) {
83
+ const live = liveWorktreePaths instanceof Set ? liveWorktreePaths : new Set(liveWorktreePaths || [])
84
+ const slugs = new Set()
85
+ for (const spec of specs || []) {
86
+ if (spec && spec.worktreePath && live.has(spec.worktreePath)) slugs.add(spec.slug)
87
+ }
88
+ return slugs
89
+ }
90
+
91
+ /**
92
+ * Reconcile the slot registry against the volumes we're about to reap. A stale
93
+ * slot is one whose spec's DB volume is an orphan — freeing it converges the
94
+ * registry back to what actually exists (the registry is keyed by spec *folder*,
95
+ * so we split each folder's prefix to get its slug and match it to a reaped
96
+ * volume by the same `{repoSlug}_{slug}_` prefix used for orphan detection).
97
+ * Pure: returns a new registry object plus the folders freed; never mutates.
98
+ *
99
+ * @param {{slots: Object<string,number>}} registry
100
+ * @param {Array<{name:string}>} orphans the volumes planPrune decided to reap.
101
+ * @param {string} repoSlug
102
+ * @returns {{ registry: {slots: Object<string,number>}, freed: string[] }}
103
+ */
104
+ function reconcileRegistry(registry, orphans, repoSlug) {
105
+ const slots = { ...((registry && registry.slots) || {}) }
106
+ const reaped = (orphans || []).map((o) => o.name)
107
+ const freed = []
108
+ for (const folder of Object.keys(slots)) {
109
+ const { slug } = splitPrefix(folder)
110
+ const prefix = `${repoSlug}_${slug}_`
111
+ if (reaped.some((name) => name.startsWith(prefix))) {
112
+ delete slots[folder]
113
+ freed.push(folder)
114
+ }
115
+ }
116
+ return { registry: { slots }, freed }
117
+ }
118
+
119
+ module.exports = { planPrune, liveSlugsForSpecs, reconcileRegistry }
@@ -0,0 +1,159 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Slot registry for per-spec isolation — the single source of truth for which
5
+ * spec owns which slot index. It lives at the **primary checkout root** (shared
6
+ * by all worktrees, machine-local, gitignored) at the config-driven `registry`
7
+ * path (default `.spec-env/registry.json`).
8
+ *
9
+ * Slot `n` → a reserved port block: `portOffset = portBase + n * portsPerSpec`.
10
+ *
11
+ * The allocation helpers are pure transforms on a registry object so they can be
12
+ * unit-tested with no filesystem; `readRegistry`/`writeRegistry` are the only IO
13
+ * and are the seam the CLI drives. No `Date.now()`/`Math.random()` — determinism
14
+ * matters (callers pass timestamps when needed).
15
+ */
16
+
17
+ const fs = require('node:fs')
18
+ const path = require('node:path')
19
+
20
+ function isObject(value) {
21
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
22
+ }
23
+
24
+ // Absolute path to the registry file, resolved against the primary checkout root.
25
+ function registryPath(rootDir, config) {
26
+ return path.resolve(rootDir, config.registry)
27
+ }
28
+
29
+ // Read the registry from disk. Missing file → an empty registry (never throws
30
+ // on absence). Malformed JSON → a clear Error.
31
+ function readRegistry(rootDir, config) {
32
+ const file = registryPath(rootDir, config)
33
+ let raw
34
+ try {
35
+ raw = fs.readFileSync(file, 'utf-8')
36
+ } catch (error) {
37
+ if (error.code === 'ENOENT') return { slots: {}, specless: {} }
38
+ throw error
39
+ }
40
+ let parsed
41
+ try {
42
+ parsed = JSON.parse(raw)
43
+ } catch (error) {
44
+ throw new Error(`Invalid registry ${config.registry}: ${error.message}`)
45
+ }
46
+ return {
47
+ slots: isObject(parsed.slots) ? { ...parsed.slots } : {},
48
+ specless: isObject(parsed.specless) ? { ...parsed.specless } : {},
49
+ }
50
+ }
51
+
52
+ // Persist the registry, creating its parent dir as needed.
53
+ function writeRegistry(rootDir, config, registry) {
54
+ const file = registryPath(rootDir, config)
55
+ fs.mkdirSync(path.dirname(file), { recursive: true })
56
+ // `specless` is written only when it holds something, so a project that never
57
+ // ran `/no-spec` keeps a registry file byte-identical to the one it had
58
+ // before this key existed.
59
+ const payload = { slots: registry.slots }
60
+ if (registry.specless && Object.keys(registry.specless).length) {
61
+ payload.specless = registry.specless
62
+ }
63
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2) + '\n')
64
+ }
65
+
66
+ /**
67
+ * Allocate the lowest free slot index to `name`. Idempotent: if `name` already
68
+ * holds a slot, that slot is returned and the registry is unchanged. Returns a
69
+ * new registry object (does not mutate the input).
70
+ */
71
+ function allocateSlot(registry, name) {
72
+ const slots = { ...registry.slots }
73
+ // CARRIED THROUGH, NOT REBUILT. Every one of these helpers returns a whole
74
+ // registry, so a helper that forgets a key silently deletes it on the next
75
+ // write — which is how a `/no-spec` branch would have become unresolvable the
76
+ // moment any Docker spec was provisioned beside it.
77
+ const specless = { ...(registry.specless || {}) }
78
+ if (Object.prototype.hasOwnProperty.call(slots, name)) {
79
+ return { registry: { slots, specless }, slot: slots[name] }
80
+ }
81
+ const used = new Set(Object.values(slots))
82
+ let slot = 0
83
+ while (used.has(slot)) slot++
84
+ slots[name] = slot
85
+ return { registry: { slots, specless }, slot }
86
+ }
87
+
88
+ /**
89
+ * Free `name`'s slot. Idempotent: freeing an absent spec is a clean no-op.
90
+ * Returns a new registry object (does not mutate the input).
91
+ */
92
+ function freeSlot(registry, name) {
93
+ const slots = { ...registry.slots }
94
+ delete slots[name]
95
+ return { slots, specless: { ...(registry.specless || {}) } }
96
+ }
97
+
98
+ // Port block base for a slot.
99
+ function portOffset(slot, config) {
100
+ return config.docker.portBase + slot * config.docker.portsPerSpec
101
+ }
102
+
103
+ /**
104
+ * Record a SPECLESS branch — `/no-spec` work, which has a worktree and a branch
105
+ * and no spec document anywhere.
106
+ *
107
+ * A SEPARATE KEY RATHER THAN A SLOT, and that is the whole design of it. A slot
108
+ * means a reserved port block, which mechanical work does not want; and every
109
+ * consumer of `slots` reads an entry there as "this spec runs a Docker stack".
110
+ * Sharing the map would have made `/no-spec` allocate ports it never binds and
111
+ * appear as a stateful spec in `status`.
112
+ *
113
+ * It is what makes a specless name RESOLVABLE. Without a record, "no spec folder
114
+ * for this name" is indistinguishable from a typo — and `resolve.js` would have
115
+ * to treat every unknown name as possibly-specless, which is exactly the
116
+ * absence-as-evidence this codebase refuses (`.claude/rules/negative-checks.md`
117
+ * rule 1). This is the positive signal: the engine wrote this name down.
118
+ *
119
+ * Idempotent, and pure — returns a new registry object.
120
+ */
121
+ function recordSpecless(registry, name, entry = {}) {
122
+ const specless = { ...(registry.specless || {}) }
123
+ specless[name] = { ...(specless[name] || {}), ...entry }
124
+ return { slots: { ...registry.slots }, specless }
125
+ }
126
+
127
+ /**
128
+ * Forget a specless branch. Idempotent: forgetting an absent one is a clean
129
+ * no-op, which is what lets teardown call it without looking first.
130
+ */
131
+ function forgetSpecless(registry, name) {
132
+ const specless = { ...(registry.specless || {}) }
133
+ delete specless[name]
134
+ return { slots: { ...registry.slots }, specless }
135
+ }
136
+
137
+ // Is this name a recorded specless branch? Pure, and the positive signal
138
+ // `resolve.js` asks for before it will resolve a name with no spec folder.
139
+ function isSpecless(registry, name) {
140
+ return Object.prototype.hasOwnProperty.call((registry && registry.specless) || {}, name)
141
+ }
142
+
143
+ // Every recorded specless name, sorted so output is stable. Pure.
144
+ function speclessNames(registry) {
145
+ return Object.keys((registry && registry.specless) || {}).sort()
146
+ }
147
+
148
+ module.exports = {
149
+ registryPath,
150
+ readRegistry,
151
+ writeRegistry,
152
+ allocateSlot,
153
+ freeSlot,
154
+ portOffset,
155
+ recordSpecless,
156
+ forgetSpecless,
157
+ isSpecless,
158
+ speclessNames,
159
+ }
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Pure renderers for per-spec isolation artifacts.
5
+ *
6
+ * `renderEnvFile` produces the worktree's `.env` body — the only file the engine
7
+ * writes. No side effects — unit-testable in isolation.
8
+ */
9
+
10
+ // The worktree `.env`: COMPOSE_PROJECT_NAME namespaces the Docker stack and its
11
+ // named volumes; PORT_OFFSET shifts the spec's reserved port block.
12
+ function renderEnvFile({ projectName, portOffset }) {
13
+ return `COMPOSE_PROJECT_NAME=${projectName}\nPORT_OFFSET=${portOffset}\n`
14
+ }
15
+
16
+
17
+ module.exports = { renderEnvFile }