@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
package/src/env/dev.js ADDED
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Pure planner for host dev-process supervision (`spec-env dev up|down`).
5
+ *
6
+ * Given a resolved spec and its allocated slot, `planDev` computes — for each
7
+ * configured `dev` process — its resolved port, the env var to inject, the log
8
+ * and pid file paths, the expanded health URL, and the canonical `frontPort` a
9
+ * later proxy (Phase 2) routes to it. No side effects: the CLI's `supervise.js`
10
+ * does the spawning/killing; this stays unit-testable with no processes.
11
+ *
12
+ * Port math: process `i` in the slot's block gets `portBase + slot*portsPerSpec
13
+ * + i` (reusing the same block the Docker stack draws from — see registry.js).
14
+ * So host dev servers get a reserved block even on a worktree-only spec.
15
+ *
16
+ * Log/pid files live beside the registry (default `.spec-env/`) and are keyed by
17
+ * the spec **folder** (not the bare slug) so a `feat-foo`/`bug-foo` pair can't
18
+ * collide — matching how the registry keys slots.
19
+ */
20
+
21
+ const path = require('node:path')
22
+ const { portOffset } = require('./registry.js')
23
+ const { expandTokens } = require('./resolve.js')
24
+
25
+ // The state dir (logs + pids) sits beside the registry file, e.g. `.spec-env`.
26
+ function stateDir(config) {
27
+ return path.posix.dirname(config.registry) || '.spec-env'
28
+ }
29
+
30
+ /**
31
+ * Plan the host dev processes for `spec` at `slot`.
32
+ *
33
+ * @returns {object} { slot, portOffset, procs: [{ name, command, port, portVar,
34
+ * env, frontPort, logFile, pidFile, health }] } — logFile/pidFile are paths
35
+ * relative to the primary checkout root (the CLI resolves them).
36
+ */
37
+ function planDev(spec, slot, config) {
38
+ const base = portOffset(slot, config)
39
+ const dir = stateDir(config)
40
+ const procs = (config.dev || []).map((entry, i) => {
41
+ const port = base + i
42
+ const tokens = {
43
+ [entry.portVar]: String(port),
44
+ port: String(port),
45
+ slug: spec.slug,
46
+ name: entry.name,
47
+ }
48
+ return {
49
+ name: entry.name,
50
+ command: expandTokens(entry.command, tokens),
51
+ port,
52
+ portVar: entry.portVar,
53
+ env: { [entry.portVar]: String(port) },
54
+ frontPort: typeof entry.frontPort === 'number' ? entry.frontPort : null,
55
+ logFile: `${dir}/logs/${spec.folder}-${entry.name}.log`,
56
+ pidFile: `${dir}/pids/${spec.folder}-${entry.name}.pid`,
57
+ health: entry.health ? expandTokens(entry.health, tokens) : null,
58
+ }
59
+ })
60
+ return { slot, portOffset: base, procs }
61
+ }
62
+
63
+ module.exports = { planDev, stateDir }
@@ -0,0 +1,34 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Restricted glob matching for repo-relative paths, shared by the live-overlay
5
+ * migration check and the conditional `afterLand` entries. Zero-dependency.
6
+ */
7
+
8
+ // Translate a restricted glob into an anchored RegExp: `**` matches across path
9
+ // separators (with an optional trailing `/`), `*` matches within a segment, `?`
10
+ // a single non-separator char. Enough for repo-path globs, no dependency.
11
+ function globToRegExp(glob) {
12
+ let re = ''
13
+ for (let i = 0; i < glob.length; i++) {
14
+ const ch = glob[i]
15
+ if (ch === '*') {
16
+ if (glob[i + 1] === '*') {
17
+ re += '.*'
18
+ i++
19
+ if (glob[i + 1] === '/') i++ // consume the `/` in `**/`
20
+ } else {
21
+ re += '[^/]*'
22
+ }
23
+ } else if (ch === '?') {
24
+ re += '[^/]'
25
+ } else if ('.+^${}()|[]\\/'.includes(ch)) {
26
+ re += '\\' + ch
27
+ } else {
28
+ re += ch
29
+ }
30
+ }
31
+ return new RegExp('^' + re + '$')
32
+ }
33
+
34
+ module.exports = { globToRegExp }
@@ -0,0 +1,226 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Register the review-gate hook in the project's Claude Code settings.
5
+ *
6
+ * `.claude/settings.json` rather than `settings.local.json`, and the difference
7
+ * is the point: the trusted-worktree entry is an absolute path and therefore
8
+ * one machine's business, while this is the project's policy — a phase that
9
+ * ended owes a verdict — and it should reach everyone who clones the repo. The
10
+ * command is written with `${CLAUDE_PROJECT_DIR}` so it stays true wherever the
11
+ * checkout lives, worktrees included.
12
+ *
13
+ * Conservative in the same way `trust.js` is: every existing key is preserved,
14
+ * a file it cannot parse is left exactly as it is, and re-running changes
15
+ * nothing once the entry is there. Callers own the reporting.
16
+ */
17
+
18
+ const fs = require('node:fs')
19
+ const path = require('node:path')
20
+
21
+ // `.cjs`, and the extension is load-bearing. This file is copied INTO the target
22
+ // project, where that project's `package.json` decides how node parses a `.js` —
23
+ // so the CommonJS script shipped as `review-gate.js` died on its own first
24
+ // `require` in every `"type": "module"` project, printing a stack trace over
25
+ // every Bash tool call. `.cjs` settles the parse mode at the file, independently
26
+ // of the one file skitterspec does not control.
27
+ const HOOK_STEM = '.claude/hooks/review-gate'
28
+ const HOOK_SCRIPT = `${HOOK_STEM}.cjs`
29
+ const HOOK_COMMAND = `node "\${CLAUDE_PROJECT_DIR}/${HOOK_SCRIPT}"`
30
+
31
+ // Our script under ANY extension, so a registration naming the retired `.js` is
32
+ // recognised as ours and migrated rather than duplicated. The trailing lookahead
33
+ // is the boundary: without it `review-gate-extra.js` — somebody else's hook —
34
+ // matches on the stem and gets silently rewritten.
35
+ const HOOK_SCRIPT_RE = /[.]claude[/\\]hooks[/\\]review-gate(?:[.][A-Za-z0-9]+)?(?![\w.-])/
36
+ // Seconds. The engine call behind this is one git-free read of a small JSON
37
+ // file, so anything approaching this is a wedge rather than slow work — and a
38
+ // hook that times out fails OPEN, which is the answer we want for a wedge.
39
+ const HOOK_TIMEOUT = 10
40
+ const MATCHER = 'Bash'
41
+
42
+ /**
43
+ * Every hook this package registers, as data.
44
+ *
45
+ * ONE LIST, so a second hook cannot arrive as a second copy of the merge logic
46
+ * below — which is where every property this file has earned would have to be
47
+ * re-derived and one of them forgotten. Each entry supplies only what differs:
48
+ * the script's stem, the tools it matches, and its timeout.
49
+ *
50
+ * The `re` for each is built from its stem with the same boundary lookahead the
51
+ * review gate's has, so `main-guard-extra.js` — somebody else's hook — is not
52
+ * silently rewritten as ours.
53
+ */
54
+ function hookStemRe(stem) {
55
+ const name = stem.split('/').pop()
56
+ return new RegExp(`[.]claude[/\\\\]hooks[/\\\\]${name}(?:[.][A-Za-z0-9]+)?(?![\\w.-])`)
57
+ }
58
+
59
+ const HOOKS = [
60
+ {
61
+ key: 'review-gate',
62
+ stem: HOOK_STEM,
63
+ matcher: MATCHER,
64
+ timeout: HOOK_TIMEOUT,
65
+ },
66
+ {
67
+ // Refuses an Edit/Write in the primary checkout while it is on the base
68
+ // branch. A different matcher from the review gate's, because it guards a
69
+ // different moment: the first write, before any work exists.
70
+ key: 'main-guard',
71
+ stem: '.claude/hooks/main-guard',
72
+ matcher: 'Edit|Write|NotebookEdit|MultiEdit',
73
+ timeout: HOOK_TIMEOUT,
74
+ },
75
+ ].map((h) => ({ ...h, script: `${h.stem}.cjs`, re: hookStemRe(h.stem) }))
76
+
77
+ function isObject(value) {
78
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
79
+ }
80
+
81
+ function settingsPath(dir) {
82
+ return path.join(dir, '.claude', 'settings.json')
83
+ }
84
+
85
+ function writeSettings(file, settings) {
86
+ fs.mkdirSync(path.dirname(file), { recursive: true })
87
+ fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n')
88
+ }
89
+
90
+ function commandFor(script) {
91
+ return `node "\${CLAUDE_PROJECT_DIR}/${script}"`
92
+ }
93
+
94
+ function hookEntry(hook = HOOKS[0]) {
95
+ return {
96
+ matcher: hook.matcher,
97
+ hooks: [{ type: 'command', command: commandFor(hook.script), timeout: hook.timeout }],
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Is our hook already registered under `PreToolUse`? Pure.
103
+ *
104
+ * Matched on the SCRIPT PATH, not on the whole command string. An operator who
105
+ * added a timeout, wrapped the invocation, or changed the interpreter has
106
+ * registered our hook their way — re-adding a second copy beside theirs would
107
+ * run it twice and look like a bug in the gate.
108
+ */
109
+ function alreadyRegistered(preToolUse, hook = HOOKS[0]) {
110
+ return registeredHooks(preToolUse, hook).length > 0
111
+ }
112
+
113
+ // Every hook object under `PreToolUse` whose command names our script, whatever
114
+ // extension it names it under. Pure.
115
+ function registeredHooks(preToolUse, hook = HOOKS[0]) {
116
+ if (!Array.isArray(preToolUse)) return []
117
+ const out = []
118
+ for (const group of preToolUse) {
119
+ if (!isObject(group) || !Array.isArray(group.hooks)) continue
120
+ for (const h of group.hooks) {
121
+ if (isObject(h) && typeof h.command === 'string' && hook.re.test(h.command)) out.push(h)
122
+ }
123
+ }
124
+ return out
125
+ }
126
+
127
+ /**
128
+ * Ensure ONE hook is registered in an already-parsed settings object.
129
+ * Mutates `parsed` in place when it migrates a stale path; returns
130
+ * `{ changed, reason }` with the same vocabulary as the wrapper below.
131
+ *
132
+ * Split out so a second hook could be added without a second copy of this
133
+ * logic — which is where every property this file has earned (preserve unknown
134
+ * keys, match on the script PATH, migrate in place, stay idempotent) would have
135
+ * had to be re-derived, and one of them forgotten.
136
+ */
137
+ function ensureOneHook(parsed, hook) {
138
+ const hooks = isObject(parsed.hooks) ? parsed.hooks : {}
139
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : []
140
+
141
+ // Registered already — but possibly under a path that no longer exists, since
142
+ // the script's extension changed. Rewrite the path IN PLACE rather than
143
+ // replacing the command: an operator who wrapped our hook, added a flag or
144
+ // changed the interpreter has registered it their way, and the thing that
145
+ // moved is the file, not their command. Adding a fresh entry beside theirs
146
+ // would run the gate twice and read as a bug in the gate; leaving the old one
147
+ // alone would aim the harness at a file this same upgrade retires.
148
+ const registered = registeredHooks(preToolUse, hook)
149
+ if (registered.length) {
150
+ const stale = registered.filter((h) => !h.command.includes(hook.script))
151
+ if (!stale.length) return { changed: false, reason: 'present' }
152
+ for (const h of stale) h.command = h.command.replace(hook.re, hook.script)
153
+ return { changed: true, reason: 'migrated' }
154
+ }
155
+
156
+ parsed.hooks = { ...hooks, PreToolUse: [...preToolUse, hookEntry(hook)] }
157
+ return { changed: true, reason: 'added' }
158
+ }
159
+
160
+ /**
161
+ * Ensure every hook this package ships is registered in `dir`'s project
162
+ * settings. Idempotent and non-destructive. Returns `{ changed, reason }`:
163
+ * - `created` — no settings file; one was written
164
+ * - `added` — merged into an existing file
165
+ * - `migrated` — registered under a retired path; the path was rewritten
166
+ * - `present` — already registered (no write)
167
+ * - `malformed` — the file exists but is not parseable JSON (left untouched)
168
+ *
169
+ * WITH SEVERAL HOOKS THE REASON IS THE STRONGEST ONE that applied, in the order
170
+ * added > migrated > present. A run that added one hook and found the other
171
+ * already there did change the file, and reporting `present` for it would tell
172
+ * an operator nothing happened when something did.
173
+ */
174
+ function ensureHooks(dir) {
175
+ const file = settingsPath(dir)
176
+
177
+ let raw
178
+ try {
179
+ raw = fs.readFileSync(file, 'utf-8')
180
+ } catch (error) {
181
+ if (error.code === 'ENOENT') {
182
+ writeSettings(file, { hooks: { PreToolUse: HOOKS.map((h) => hookEntry(h)) } })
183
+ return { changed: true, reason: 'created' }
184
+ }
185
+ throw error
186
+ }
187
+
188
+ let parsed
189
+ try {
190
+ parsed = JSON.parse(raw)
191
+ } catch {
192
+ // Never rewrite a settings file we could not read. It is the operator's
193
+ // config and everything else in it would be lost.
194
+ return { changed: false, reason: 'malformed' }
195
+ }
196
+ if (!isObject(parsed)) return { changed: false, reason: 'malformed' }
197
+
198
+ const reasons = HOOKS.map((h) => ensureOneHook(parsed, h))
199
+ const changed = reasons.some((r) => r.changed)
200
+ if (changed) writeSettings(file, parsed)
201
+
202
+ for (const rank of ['added', 'migrated', 'present']) {
203
+ if (reasons.some((r) => r.reason === rank)) return { changed, reason: rank }
204
+ }
205
+ return { changed, reason: 'present' }
206
+ }
207
+
208
+ // The name this shipped under when there was one hook. Kept as an alias rather
209
+ // than renamed away, because it is what an installed `init.js` from an older
210
+ // version calls — and a rename that breaks an upgrade path is not a rename, it
211
+ // is a removal.
212
+ const ensureReviewGateHook = ensureHooks
213
+
214
+ module.exports = {
215
+ ensureHooks,
216
+ ensureReviewGateHook,
217
+ alreadyRegistered,
218
+ registeredHooks,
219
+ hookEntry,
220
+ settingsPath,
221
+ HOOKS,
222
+ HOOK_STEM,
223
+ HOOK_SCRIPT,
224
+ HOOK_SCRIPT_RE,
225
+ HOOK_COMMAND,
226
+ }