@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,299 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The main guard — is this write about to land on the base branch?
|
|
5
|
+
*
|
|
6
|
+
* `main` is where work **lands**, not where it happens. Two routes reach it
|
|
7
|
+
* otherwise, and both cost someone else something: a dirty base branch that
|
|
8
|
+
* every in-flight spec has to replay over and no release can be cut through,
|
|
9
|
+
* and — worse — work that renders no review page at all, because nothing in
|
|
10
|
+
* this workflow renders one for the primary checkout.
|
|
11
|
+
*
|
|
12
|
+
* Both routes now have somewhere to go: `/spec` authors in its own `--docs`
|
|
13
|
+
* worktree, and `/no-spec` gives mechanical work a branch of its own. So the
|
|
14
|
+
* guard is a push toward a path that exists rather than a refusal with no exit.
|
|
15
|
+
*
|
|
16
|
+
* THE ACCUSATION IS POSITIVE, AND ALL FOUR PARTS MUST HOLD. It is
|
|
17
|
+
* `.claude/rules/negative-checks.md` rule 1 applied to a check that stops
|
|
18
|
+
* someone working: isolation is configured, the guard is not switched off, this
|
|
19
|
+
* directory IS the primary checkout, and HEAD IS the base branch. Anything the
|
|
20
|
+
* engine cannot establish — no repo, no config, an unresolvable base, a
|
|
21
|
+
* worktree, an unreadable allow file — allows the write and says nothing.
|
|
22
|
+
* Being wrong the permissive way costs one unguarded edit on a rule the skills
|
|
23
|
+
* also carry; being wrong the other way costs someone their work with no idea
|
|
24
|
+
* why.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs')
|
|
28
|
+
const path = require('node:path')
|
|
29
|
+
|
|
30
|
+
const { assertPrimaryOnMain, resolvePrimaryCheckout } = require('./resolve.js')
|
|
31
|
+
|
|
32
|
+
// Where the allow lives. Gitignored, machine-local and beside the registry —
|
|
33
|
+
// this is one person's decision about one session, never the project's policy.
|
|
34
|
+
const ALLOW_FILE = 'main-allow.json'
|
|
35
|
+
|
|
36
|
+
function allowPath(rootDir, config) {
|
|
37
|
+
return path.resolve(rootDir, path.dirname(config.registry), ALLOW_FILE)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read the recorded allow. Pure but for the read.
|
|
42
|
+
*
|
|
43
|
+
* MISSING AND MALFORMED BOTH READ AS "NO ALLOW", which is the *strict* answer
|
|
44
|
+
* here rather than the lenient one — and that is the right way round only
|
|
45
|
+
* because the strict answer is also the safe one. An unreadable allow file
|
|
46
|
+
* makes the guard fire, and firing costs a refusal naming two exits; silently
|
|
47
|
+
* treating it as permission would let a stale file switch the guard off
|
|
48
|
+
* forever.
|
|
49
|
+
*/
|
|
50
|
+
function readAllow(rootDir, config) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(fs.readFileSync(allowPath(rootDir, config), 'utf-8'))
|
|
53
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
54
|
+
return parsed
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeAllow(rootDir, config, value) {
|
|
61
|
+
const file = allowPath(rootDir, config)
|
|
62
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
63
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clearAllow(rootDir, config) {
|
|
67
|
+
try {
|
|
68
|
+
fs.unlinkSync(allowPath(rootDir, config))
|
|
69
|
+
return true
|
|
70
|
+
} catch {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Does `allow` permit a write from `sessionId`? Pure.
|
|
77
|
+
*
|
|
78
|
+
* TWO KINDS, AND THEY MUST NOT LOOK ALIKE. A **session** allow names the
|
|
79
|
+
* session that asked for it, so it lapses the moment that conversation ends —
|
|
80
|
+
* which is what "temporary" has to mean for something nobody will remember to
|
|
81
|
+
* switch back. A **repo** allow is the degraded form for a harness that exposes
|
|
82
|
+
* no session id; it outlives the session and is cleared only by hand, so
|
|
83
|
+
* `mainGuardStatus` says which kind is in force rather than reporting a bare
|
|
84
|
+
* "allowed".
|
|
85
|
+
*
|
|
86
|
+
* A session allow with no session id to compare against does NOT apply. The
|
|
87
|
+
* alternative — treating "cannot tell whose session this is" as a match — would
|
|
88
|
+
* turn every session allow into a repo one the moment a caller forgot to pass
|
|
89
|
+
* the id.
|
|
90
|
+
*/
|
|
91
|
+
function allowApplies(allow, sessionId) {
|
|
92
|
+
if (!allow) return false
|
|
93
|
+
if (allow.scope === 'repo') return true
|
|
94
|
+
if (allow.scope === 'session') return Boolean(sessionId) && allow.session === sessionId
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Should a write in `dir` be refused? Pure — every fact arrives in `ctx`.
|
|
100
|
+
*
|
|
101
|
+
* ctx: { configured, enabled, isPrimary, onBase, allow, sessionId }
|
|
102
|
+
*
|
|
103
|
+
* Returns `{ refuse, reason }`. `reason` is for the operator and names both
|
|
104
|
+
* exits; `refuse: false` always carries a null reason, because a guard that
|
|
105
|
+
* explains why it is NOT firing is the narration this codebase bans.
|
|
106
|
+
*
|
|
107
|
+
* WHAT WOULD FOOL THIS: nothing here inspects the *file* being written. A write
|
|
108
|
+
* to `specs/`, to `.claude/`, to a scratch file — all refused alike, and that is
|
|
109
|
+
* the decision rather than an oversight. An allowlist was considered and is
|
|
110
|
+
* empty by construction: with `/spec` authoring into its own worktree and
|
|
111
|
+
* `/no-spec` catching ad-hoc work, no legitimate writer to the base branch is
|
|
112
|
+
* left. `/spec-init` is the apparent exception and is not one — it bootstraps a
|
|
113
|
+
* repo that has no `env.config.json` yet, so `configured` is false and the
|
|
114
|
+
* guard never reaches this function.
|
|
115
|
+
*/
|
|
116
|
+
function judgeMainWrite(ctx) {
|
|
117
|
+
const { configured, enabled, isPrimary, onBase, allow, sessionId } = ctx || {}
|
|
118
|
+
|
|
119
|
+
// Every one of these is a CANNOT-TELL or a NOT-APPLICABLE, and each routes to
|
|
120
|
+
// the harmless branch on its own rather than through a combined condition —
|
|
121
|
+
// so a later reader can delete one without silently widening the others.
|
|
122
|
+
if (!configured) return { refuse: false, reason: null }
|
|
123
|
+
if (enabled === false) return { refuse: false, reason: null }
|
|
124
|
+
if (!isPrimary) return { refuse: false, reason: null }
|
|
125
|
+
if (!onBase) return { refuse: false, reason: null }
|
|
126
|
+
if (allowApplies(allow, sessionId)) return { refuse: false, reason: null }
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
refuse: true,
|
|
130
|
+
reason:
|
|
131
|
+
'the primary checkout is on the base branch, and the base branch is a landing zone — ' +
|
|
132
|
+
'work lands there, it does not happen there.\n\n' +
|
|
133
|
+
'Move the work instead of writing here:\n' +
|
|
134
|
+
' /no-spec <name> a one-off with no spec — its own branch, worktree and review page\n' +
|
|
135
|
+
' /spec-start <name> a spec that is ready to be built\n' +
|
|
136
|
+
' /spec a change that wants grilling first\n\n' +
|
|
137
|
+
'If this really does belong on the base branch, the USER types /allow-main — ' +
|
|
138
|
+
'it is theirs to run, not yours.',
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The checkout a path lives in: the `--show-toplevel` of its nearest existing
|
|
144
|
+
* ancestor, or null when that is in no git repo at all.
|
|
145
|
+
*
|
|
146
|
+
* NEAREST EXISTING ANCESTOR, because a `Write` usually creates its file and
|
|
147
|
+
* often its folder too — `git -C` on a directory that does not exist yet fails,
|
|
148
|
+
* and reading that failure as "not in a repo" would wave through exactly the
|
|
149
|
+
* new file the guard exists for.
|
|
150
|
+
*/
|
|
151
|
+
function checkoutOf(target, gitAt) {
|
|
152
|
+
let at = path.resolve(target)
|
|
153
|
+
while (!fs.existsSync(at)) {
|
|
154
|
+
const up = path.dirname(at)
|
|
155
|
+
if (up === at) return null
|
|
156
|
+
at = up
|
|
157
|
+
}
|
|
158
|
+
if (!fs.statSync(at).isDirectory()) at = path.dirname(at)
|
|
159
|
+
return gitAt(at)(['rev-parse', '--show-toplevel'])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Two spellings of one directory — `/tmp` and `/private/tmp` on macOS — are
|
|
163
|
+
// the same directory. Cannot-tell is "not the same", the branch that allows.
|
|
164
|
+
function sameDir(a, b) {
|
|
165
|
+
try {
|
|
166
|
+
return fs.realpathSync(a) === fs.realpathSync(b)
|
|
167
|
+
} catch {
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ALLOWS = { refuse: false, reason: null, branch: null, baseBranch: null }
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gather the facts and judge. The only IO in the module's hot path.
|
|
176
|
+
*
|
|
177
|
+
* `git` is an injected reader, as everywhere in this package: the caller owns
|
|
178
|
+
* the child-process boundary, which is what keeps `judgeMainWrite` testable
|
|
179
|
+
* without one.
|
|
180
|
+
*
|
|
181
|
+
* `opts.target` is the path being written, and when it is given IT decides
|
|
182
|
+
* where the write lands — not `dir`, which is only where the session stands.
|
|
183
|
+
* Judging the session's directory refused every write a session on the base
|
|
184
|
+
* branch made anywhere, its own scratchpad under /tmp included, while a session
|
|
185
|
+
* standing in a worktree could write into the primary checkout unseen
|
|
186
|
+
* (bug-main-guard-judges-cwd). `opts.gitAt(dir)` makes a reader for another
|
|
187
|
+
* directory; it is required alongside `target`.
|
|
188
|
+
*
|
|
189
|
+
* This reads the write's LOCATION, never its name, so it is not the exception
|
|
190
|
+
* list `judgeMainWrite` refuses to grow: a path inside the primary checkout is
|
|
191
|
+
* judged exactly as before, whatever it is called.
|
|
192
|
+
*
|
|
193
|
+
* WHAT WOULD FOOL THIS: a target in ANOTHER configured repo is judged against
|
|
194
|
+
* this session's repo, so it is allowed — that repo's guard is its own sessions'
|
|
195
|
+
* business. And without `target` (an older hook, a tool with no path) the
|
|
196
|
+
* session's directory is judged as it always was.
|
|
197
|
+
*/
|
|
198
|
+
function checkMainWrite(dir, config, git, opts = {}) {
|
|
199
|
+
// `present` from `loadEnvConfig` — the caller has it and this module must not
|
|
200
|
+
// re-read the file to find out. A defaults-only config is indistinguishable
|
|
201
|
+
// from a configured one that happens to match the defaults, so asking the
|
|
202
|
+
// config object would answer the wrong question.
|
|
203
|
+
const configured = opts.present === true
|
|
204
|
+
const enabled = !(config && config.guards && config.guards.mainIsLandingZone === false)
|
|
205
|
+
|
|
206
|
+
// IS THIS THE PRIMARY CHECKOUT? A worktree is somebody's spec branch and none
|
|
207
|
+
// of this guard's business, so the question is asked positively rather than by
|
|
208
|
+
// looking for the absence of a worktree marker.
|
|
209
|
+
let isPrimary = false
|
|
210
|
+
let primary = null
|
|
211
|
+
// Which checkout's HEAD decides "on the base branch". With a target that is
|
|
212
|
+
// the primary checkout's own, wherever the session is standing.
|
|
213
|
+
let branchGit = git
|
|
214
|
+
try {
|
|
215
|
+
primary = resolvePrimaryCheckout(dir, git)
|
|
216
|
+
if (opts.target && typeof opts.gitAt === 'function') {
|
|
217
|
+
const top = checkoutOf(opts.target, opts.gitAt)
|
|
218
|
+
isPrimary = Boolean(top) && Boolean(primary) && sameDir(top, primary)
|
|
219
|
+
if (!isPrimary) return ALLOWS
|
|
220
|
+
branchGit = opts.gitAt(primary)
|
|
221
|
+
} else {
|
|
222
|
+
isPrimary = Boolean(primary) && path.resolve(primary) === path.resolve(dir)
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Not a git repo, or git unreadable. Cannot tell → allow.
|
|
226
|
+
return ALLOWS
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let onBase = false
|
|
230
|
+
let branch = null
|
|
231
|
+
let baseBranch = null
|
|
232
|
+
try {
|
|
233
|
+
const state = assertPrimaryOnMain(config, branchGit)
|
|
234
|
+
onBase = state.onBase
|
|
235
|
+
branch = state.branch
|
|
236
|
+
baseBranch = state.baseBranch
|
|
237
|
+
} catch {
|
|
238
|
+
return ALLOWS
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const root = primary || dir
|
|
242
|
+
const verdict = judgeMainWrite({
|
|
243
|
+
configured,
|
|
244
|
+
enabled,
|
|
245
|
+
isPrimary,
|
|
246
|
+
onBase,
|
|
247
|
+
allow: readAllow(root, config),
|
|
248
|
+
sessionId: opts.sessionId || null,
|
|
249
|
+
})
|
|
250
|
+
return { ...verdict, branch, baseBranch }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Record an allow. `sessionId` present → a session allow; absent → a repo one.
|
|
255
|
+
* Returns what was written, so the caller can report which kind it is.
|
|
256
|
+
*/
|
|
257
|
+
function recordAllow(rootDir, config, { sessionId = null, reason = null, at = null } = {}) {
|
|
258
|
+
const value = sessionId
|
|
259
|
+
? { scope: 'session', session: sessionId, reason, at }
|
|
260
|
+
: { scope: 'repo', reason, at }
|
|
261
|
+
writeAllow(rootDir, config, value)
|
|
262
|
+
return value
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* What is in force, as data. Four states, and the caller reports all four —
|
|
267
|
+
* `off` (the project switched the guard off), `unconfigured`, `allowed` (with
|
|
268
|
+
* its scope and reason), or `guarded`.
|
|
269
|
+
*/
|
|
270
|
+
function mainGuardStatus(rootDir, config, opts = {}) {
|
|
271
|
+
if (opts.present !== true) return { state: 'unconfigured' }
|
|
272
|
+
if (config && config.guards && config.guards.mainIsLandingZone === false) {
|
|
273
|
+
return { state: 'off' }
|
|
274
|
+
}
|
|
275
|
+
const allow = readAllow(rootDir, config)
|
|
276
|
+
if (allowApplies(allow, opts.sessionId || null)) {
|
|
277
|
+
return { state: 'allowed', scope: allow.scope, reason: allow.reason || null, at: allow.at || null }
|
|
278
|
+
}
|
|
279
|
+
// A session allow belonging to ANOTHER session is reported rather than hidden:
|
|
280
|
+
// the operator asked for one somewhere, and a bare "guarded" would send them
|
|
281
|
+
// hunting for why their /allow-main did nothing.
|
|
282
|
+
if (allow && allow.scope === 'session') {
|
|
283
|
+
return { state: 'guarded', otherSession: true }
|
|
284
|
+
}
|
|
285
|
+
return { state: 'guarded' }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
ALLOW_FILE,
|
|
290
|
+
allowPath,
|
|
291
|
+
readAllow,
|
|
292
|
+
writeAllow,
|
|
293
|
+
clearAllow,
|
|
294
|
+
allowApplies,
|
|
295
|
+
judgeMainWrite,
|
|
296
|
+
checkMainWrite,
|
|
297
|
+
recordAllow,
|
|
298
|
+
mainGuardStatus,
|
|
299
|
+
}
|