@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,67 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Compare what the tracker STORED against what we sent, and report lost text.
5
+ *
6
+ * Why this is not a pull. One-way sync's rule is about **authority**: the
7
+ * tracker must never influence repo content. This reads a description back to
8
+ * *check* it — it merges nothing, writes nothing, and feeds nothing into the
9
+ * projection or the snapshot. The repo remains the only source of truth; the
10
+ * only output is a warning for a human. Without it, a parser that silently eats
11
+ * characters produces a mirror that looks pushed and is wrong, which is exactly
12
+ * how the nested-table corruption went unnoticed (see the Linear provider's
13
+ * dialect, where that flattener now lives).
14
+ *
15
+ * The engine is offline, so the read itself belongs to the push skill — it
16
+ * fetches over MCP and hands the result here, the same split
17
+ * `--workspace-states` already uses.
18
+ *
19
+ * Each tracker reserialises text its own way, so the reduction that makes its
20
+ * benign transforms invisible is the provider's `config.dialect.canonicalForCompare`
21
+ * (see `src/dialect.js`); with no dialect the identity reduction applies.
22
+ *
23
+ * Pure: no I/O, no clock, no randomness.
24
+ */
25
+
26
+ const { dialectOf } = require('./dialect.js')
27
+
28
+ // The word-character stream: everything that carries meaning, with every
29
+ // reformatting artefact (whitespace, bullets, asterisk boundaries, pipes,
30
+ // separators) removed. Comparing these catches dropped characters while
31
+ // ignoring every benign transform the dialect names.
32
+ function stream(text, config) {
33
+ return dialectOf(config).canonicalForCompare(text).replace(/[^\p{L}\p{N}]/gu, '')
34
+ }
35
+
36
+ /**
37
+ * @param {string} sent what we pushed
38
+ * @param {string} stored what the tracker returned
39
+ * @param {object} [config] the provider config (for its dialect)
40
+ * @returns {{ok:boolean, at:number|null, lost:number, sentContext:string, storedContext:string}}
41
+ * `ok` false means word characters differ — content was lost or altered.
42
+ * `at` is the index in the reduced stream where they first diverge, with ~40
43
+ * characters of each side around it so the warning names the damage.
44
+ */
45
+ // BLIND SPOT: a `stored` the caller never fetched is indistinguishable here
46
+ // from a description the tracker really did store empty — both reduce to `''`
47
+ // and read as total loss. The evidence that the read happened lives with the
48
+ // caller, so it must pass only a description it actually read back (`verifyLines`
49
+ // gates on `typeof stored.issue === 'string'` for exactly this reason).
50
+ function compareStored(sent, stored, config) {
51
+ const a = stream(sent, config)
52
+ const b = stream(stored, config)
53
+ if (a === b) return { ok: true, at: null, lost: 0, sentContext: '', storedContext: '' }
54
+
55
+ let at = 0
56
+ while (at < a.length && at < b.length && a[at] === b[at]) at++
57
+ const window = (s) => s.slice(Math.max(0, at - 10), at + 30)
58
+ return {
59
+ ok: false,
60
+ at,
61
+ lost: a.length - b.length,
62
+ sentContext: window(a),
63
+ storedContext: window(b),
64
+ }
65
+ }
66
+
67
+ module.exports = { compareStored, stream }
@@ -0,0 +1,191 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Push-side writeback: after `push` creates the spec issue + phase sub-issues in
5
+ * Linear, the skill stamps each returned id back into the repo so the next push
6
+ * updates rather than recreates. Everything here edits the repo in place:
7
+ *
8
+ * - `stampSubIssueId(dir, file, id, config)` — add/update the provider's
9
+ * phase-stamp field (`config.fields.issueId`) in a phase
10
+ * file's frontmatter (locate the file with `findPhaseFileByTitle`).
11
+ * - `writeFrontmatter(dir, config, patch)` — patch `00-overview.md` frontmatter
12
+ * (e.g. the spec issue's `spec_identifier`, `last_synced_at`).
13
+ * - `stampIssueId(dir, text, id)` — legacy: append `(ID)` to a task line
14
+ * (tasks are no longer synced; kept for the sanitise/util paths).
15
+ *
16
+ * No remote read, no pull writeback — the repo is the source of truth.
17
+ */
18
+
19
+ const fs = require('node:fs')
20
+ const path = require('node:path')
21
+ const { findTaskBlocks, renderTaskBlock, collapse, inferWidth } = require('./task-block.js')
22
+ const { fieldsOf } = require('./fields.js')
23
+
24
+ // Serialize a JS value as a YAML-ish frontmatter scalar.
25
+ function serialize(value) {
26
+ if (Array.isArray(value)) return JSON.stringify(value)
27
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
28
+ return JSON.stringify(String(value)) // quoted string
29
+ }
30
+
31
+ // Split `---\n…\n---\n` frontmatter off the top. Returns { fmLines, body, had }.
32
+ function splitFrontmatter(raw) {
33
+ const m = /^---\n([\s\S]*?)\n---\n?/.exec(raw)
34
+ if (!m) return { fmLines: [], body: raw, had: false }
35
+ return { fmLines: m[1].split('\n'), body: raw.slice(m[0].length), had: true }
36
+ }
37
+
38
+ // Apply a key→value patch onto frontmatter lines, preserving order.
39
+ function patchFrontmatterLines(lines, patch) {
40
+ const keys = new Set(Object.keys(patch))
41
+ const out = []
42
+ const seen = new Set()
43
+ for (const line of lines) {
44
+ const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
45
+ if (kv && keys.has(kv[1])) {
46
+ out.push(`${kv[1]}: ${serialize(patch[kv[1]])}`)
47
+ seen.add(kv[1])
48
+ } else {
49
+ out.push(line)
50
+ }
51
+ }
52
+ for (const key of Object.keys(patch)) {
53
+ if (!seen.has(key)) out.push(`${key}: ${serialize(patch[key])}`)
54
+ }
55
+ return out
56
+ }
57
+
58
+ /**
59
+ * Update `00-overview.md` frontmatter under `snapshotDir` with `patch`
60
+ * (key → value; nullish values are skipped). Returns the list of keys written.
61
+ */
62
+ function writeFrontmatter(snapshotDir, config, patch) {
63
+ const overviewFile = (config && config.snapshot && config.snapshot.overviewFile) || '00-overview.md'
64
+ const file = path.join(snapshotDir, overviewFile)
65
+ const raw = fs.readFileSync(file, 'utf-8')
66
+
67
+ const clean = {}
68
+ for (const [k, v] of Object.entries(patch)) {
69
+ if (v !== null && v !== undefined) clean[k] = v
70
+ }
71
+ if (!Object.keys(clean).length) return []
72
+
73
+ const { fmLines, body, had } = splitFrontmatter(raw)
74
+ const patched = patchFrontmatterLines(fmLines, clean)
75
+ const frontmatter = `---\n${patched.join('\n')}\n---\n`
76
+ const next = had ? frontmatter + body : frontmatter + '\n' + raw
77
+
78
+ fs.writeFileSync(file, next, 'utf-8')
79
+ return Object.keys(clean)
80
+ }
81
+
82
+ /**
83
+ * Remove keys from `00-overview.md` frontmatter. Returns the keys actually
84
+ * removed (absent ones are a clean no-op, not an error).
85
+ *
86
+ * A separate function because `writeFrontmatter` SKIPS nullish values by
87
+ * design — that is what lets a caller pass a sparse patch without clearing the
88
+ * fields it left out, and it means "remove this key" is inexpressible there. A
89
+ * released assignment has to actually leave the file: setting it empty would
90
+ * leave a spec claiming to be assigned to nobody in particular, and the
91
+ * projection reads presence, not emptiness.
92
+ */
93
+ function deleteFrontmatter(snapshotDir, config, keys) {
94
+ const overviewFile = (config && config.snapshot && config.snapshot.overviewFile) || '00-overview.md'
95
+ const file = path.join(snapshotDir, overviewFile)
96
+ const raw = fs.readFileSync(file, 'utf-8')
97
+ const { fmLines, body, had } = splitFrontmatter(raw)
98
+ if (!had) return []
99
+
100
+ const drop = new Set(keys)
101
+ const removed = []
102
+ const kept = fmLines.filter((line) => {
103
+ const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
104
+ if (kv && drop.has(kv[1])) {
105
+ removed.push(kv[1])
106
+ return false
107
+ }
108
+ return true
109
+ })
110
+ if (!removed.length) return []
111
+
112
+ // An emptied block is dropped entirely rather than left as `---\n---`, which
113
+ // some markdown renderers show as a horizontal rule.
114
+ const next = kept.length ? `---\n${kept.join('\n')}\n---\n${body}` : body
115
+ fs.writeFileSync(file, next, 'utf-8')
116
+ return removed
117
+ }
118
+
119
+ // Phase files in a snapshot dir (01-*.md …), execution order.
120
+ function listPhaseFiles(snapshotDir) {
121
+ try {
122
+ return fs
123
+ .readdirSync(snapshotDir)
124
+ .filter((f) => /^\d\d-.*\.md$/.test(f) && !f.startsWith('00-'))
125
+ .sort()
126
+ } catch {
127
+ return []
128
+ }
129
+ }
130
+
131
+ // Find the phase file whose h1 title matches `name` (used to stamp a freshly
132
+ // created sub-issue's id back into the phase it came from).
133
+ function findPhaseFileByTitle(snapshotDir, name) {
134
+ const want = String(name).trim()
135
+ for (const file of listPhaseFiles(snapshotDir)) {
136
+ const raw = fs.readFileSync(path.join(snapshotDir, file), 'utf-8')
137
+ const h1 = /^#\s+(.*)$/m.exec(splitFrontmatter(raw).body)
138
+ if (!h1) continue
139
+ const title = h1[1]
140
+ .replace(/\s*[⬜🔄✅]\s*$/u, '')
141
+ .replace(/^Phase\s+\d+\s*[—–-]\s*/i, '')
142
+ .trim()
143
+ if (title === want) return file
144
+ }
145
+ return null
146
+ }
147
+
148
+ // Add/update the provider's phase-stamp field (`config.fields.issueId`) in a
149
+ // phase file's frontmatter (in place) — the sub-issue id for that phase.
150
+ function stampSubIssueId(snapshotDir, file, id, config) {
151
+ const p = path.join(snapshotDir, file)
152
+ const raw = fs.readFileSync(p, 'utf-8')
153
+ const { fmLines, body, had } = splitFrontmatter(raw)
154
+ const patched = patchFrontmatterLines(fmLines, { [fieldsOf(config).issueId]: String(id) })
155
+ const fm = `---\n${patched.join('\n')}\n---\n`
156
+ fs.writeFileSync(p, had ? fm + body : fm + '\n' + raw, 'utf-8')
157
+ }
158
+
159
+ const INLINE_ID_RE = /\s*\(([A-Za-z][A-Za-z0-9]*-\d+)\)\s*$/
160
+
161
+ // Stamp an inline id onto the (idless) task line whose text matches — used after
162
+ // the skill creates an issue for a new local task.
163
+ function stampIssueId(snapshotDir, text, id) {
164
+ const want = collapse(text)
165
+ for (const file of listPhaseFiles(snapshotDir)) {
166
+ const p = path.join(snapshotDir, file)
167
+ const lines = fs.readFileSync(p, 'utf-8').split('\n')
168
+ const width = inferWidth(lines)
169
+ for (const b of findTaskBlocks(lines)) {
170
+ if (!b.checkbox) continue // a plain sub-bullet is not a task — never stamp one
171
+ if (INLINE_ID_RE.test(b.text)) continue
172
+ if (b.text !== want) continue
173
+ const rendered = renderTaskBlock({ indent: b.indent, done: b.mark === 'x', text: want, id }, width)
174
+ lines.splice(b.start, b.end - b.start, ...rendered)
175
+ fs.writeFileSync(p, lines.join('\n'), 'utf-8')
176
+ return file
177
+ }
178
+ }
179
+ return null
180
+ }
181
+
182
+ module.exports = {
183
+ writeFrontmatter,
184
+ deleteFrontmatter,
185
+ splitFrontmatter,
186
+ serialize,
187
+ listPhaseFiles,
188
+ findPhaseFileByTitle,
189
+ stampSubIssueId,
190
+ stampIssueId,
191
+ }