@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.
- package/MIGRATION.md +1266 -0
- package/README.md +44 -2
- package/assets/claude-md-section.md +85 -0
- package/assets/commands/allow-main.md +33 -0
- package/assets/commands/spec-connect.md +13 -0
- package/assets/commands/spec-live.md +14 -0
- package/assets/commands/spec-remote-review.md +37 -0
- package/assets/commands/spec-skip.md +24 -0
- package/assets/core/SETUP.md +192 -0
- package/assets/core/env.config.json.example +56 -0
- package/assets/core/env.config.md +606 -0
- package/assets/core/gating.config.json.example +4 -0
- package/assets/core/gating.config.md +81 -0
- package/assets/core/jira.config.json.example +18 -0
- package/assets/core/jira.config.md +88 -0
- package/assets/hooks/main-guard.cjs +162 -0
- package/assets/hooks/review-gate.cjs +146 -0
- package/assets/review/page.html +3384 -0
- package/assets/rules/commit-trailers.md +96 -0
- package/assets/rules/negative-checks.md +69 -0
- package/assets/rules/offered-unblocks.md +100 -0
- package/assets/rules/spec-planning.md +702 -0
- package/assets/rules/spec-reports.md +572 -0
- package/assets/skills/no-spec/SKILL.md +249 -0
- package/assets/skills/spec/SKILL.md +714 -0
- package/assets/skills/spec-bug/SKILL.md +585 -0
- package/assets/skills/spec-cancel/SKILL.md +261 -0
- package/assets/skills/spec-complete/SKILL.md +451 -0
- package/assets/skills/spec-diff/SKILL.md +870 -0
- package/assets/skills/spec-hotfix/SKILL.md +607 -0
- package/assets/skills/spec-init/SKILL.md +178 -0
- package/assets/skills/spec-jira-setup/SKILL.md +221 -0
- package/assets/skills/spec-list/SKILL.md +173 -0
- package/assets/skills/spec-next/SKILL.md +678 -0
- package/assets/skills/spec-push/SKILL.md +306 -0
- package/assets/skills/spec-review/SKILL.md +256 -0
- package/assets/skills/spec-reviewed/SKILL.md +284 -0
- package/assets/skills/spec-start/SKILL.md +496 -0
- package/assets/skills/spec-status/SKILL.md +114 -0
- package/assets/skills/spec-sync/SKILL.md +220 -0
- package/assets/skills/spec-to-main/SKILL.md +141 -0
- package/bin/skitterspec-jira.js +68 -0
- package/package.json +40 -4
- package/src/cli.js +5967 -0
- package/src/deprecate.js +138 -0
- package/src/env/bootstrap.js +188 -0
- package/src/env/building.js +143 -0
- package/src/env/classify.js +176 -0
- package/src/env/commitcmd.js +108 -0
- package/src/env/config.js +850 -0
- package/src/env/dev.js +63 -0
- package/src/env/glob.js +34 -0
- package/src/env/hooks.js +226 -0
- package/src/env/hotfix.js +413 -0
- package/src/env/hotfixland.js +302 -0
- package/src/env/integrate.js +167 -0
- package/src/env/live.js +479 -0
- package/src/env/mainguard.js +299 -0
- package/src/env/provision.js +466 -0
- package/src/env/proxy.js +225 -0
- package/src/env/prune.js +119 -0
- package/src/env/registry.js +159 -0
- package/src/env/render.js +17 -0
- package/src/env/resolve.js +716 -0
- package/src/env/review.js +2622 -0
- package/src/env/reviewers/coderabbit.js +228 -0
- package/src/env/reviewers.js +517 -0
- package/src/env/serve.js +1044 -0
- package/src/env/supervise.js +157 -0
- package/src/env/teardown.js +248 -0
- package/src/env/trust.js +87 -0
- package/src/gating.js +155 -0
- package/src/init.js +1150 -0
- package/src/lines-diff.js +114 -0
- package/src/prompts.js +129 -0
- package/src/vendor/jira/adf.js +414 -0
- package/src/vendor/jira/api.js +295 -0
- package/src/vendor/jira/cli-sync.js +1252 -0
- package/src/vendor/jira/commands.js +44 -0
- package/src/vendor/jira/config.js +286 -0
- package/src/vendor/jira/credentials.js +108 -0
- package/src/vendor/jira/dialect.js +45 -0
- package/src/vendor/jira/mcp.js +129 -0
- package/src/vendor/provider-kit/index.js +41 -0
- package/src/vendor/provider-kit/src/adapter.js +23 -0
- package/src/vendor/provider-kit/src/apply.js +207 -0
- package/src/vendor/provider-kit/src/args.js +68 -0
- package/src/vendor/provider-kit/src/assignment.js +277 -0
- package/src/vendor/provider-kit/src/config.js +251 -0
- package/src/vendor/provider-kit/src/credentials.js +404 -0
- package/src/vendor/provider-kit/src/doctor.js +35 -0
- package/src/vendor/provider-kit/src/http.js +63 -0
- package/src/vendor/provider-kit/src/identity.js +93 -0
- package/src/vendor/provider-kit/src/list-assignee.js +71 -0
- package/src/vendor/provider-kit/src/list-rows.js +35 -0
- package/src/vendor/provider-kit/src/md-compare.js +39 -0
- package/src/vendor/provider-kit/src/md-tables.js +94 -0
- package/src/vendor/provider-kit/src/prompt.js +50 -0
- package/src/vendor/provider-kit/src/released.js +219 -0
- package/src/vendor/provider-kit/src/specs.js +248 -0
- package/src/vendor/provider-kit/src/transport.js +22 -0
- package/src/vendor/sync-core/index.js +92 -0
- package/src/vendor/sync-core/src/base.js +81 -0
- package/src/vendor/sync-core/src/compare.js +338 -0
- package/src/vendor/sync-core/src/dialect.js +51 -0
- package/src/vendor/sync-core/src/fields.js +36 -0
- package/src/vendor/sync-core/src/legacy.js +100 -0
- package/src/vendor/sync-core/src/normalize.js +971 -0
- package/src/vendor/sync-core/src/push.js +85 -0
- package/src/vendor/sync-core/src/retarget.js +278 -0
- package/src/vendor/sync-core/src/task-block.js +308 -0
- package/src/vendor/sync-core/src/verify.js +67 -0
- 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
|
+
}
|