@skitterbyte/skitterspec 21.0.0 → 22.1.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 +218 -0
- package/README.md +127 -6
- package/assets/claude-md-section.md +42 -18
- package/assets/commands/allow-main.md +33 -0
- package/assets/commands/spec-remote-review.md +22 -0
- package/assets/commands/spec-skip.md +24 -0
- package/assets/core/env.config.json.example +6 -3
- package/assets/core/env.config.md +183 -25
- package/assets/hooks/main-guard.cjs +145 -0
- package/assets/hooks/review-gate.cjs +8 -3
- package/assets/review/page.html +1174 -101
- package/assets/rules/offered-unblocks.md +100 -0
- package/assets/rules/spec-planning.md +195 -14
- package/assets/rules/spec-reports.md +181 -29
- package/assets/skills/no-spec/SKILL.md +219 -0
- package/assets/skills/spec/SKILL.md +248 -21
- package/assets/skills/spec-bug/SKILL.md +34 -24
- package/assets/skills/spec-cancel/SKILL.md +29 -0
- package/assets/skills/spec-complete/SKILL.md +25 -0
- package/assets/skills/spec-diff/SKILL.md +164 -37
- package/assets/skills/spec-hotfix/SKILL.md +22 -19
- package/assets/skills/spec-init/SKILL.md +4 -2
- package/assets/skills/spec-next/SKILL.md +166 -58
- package/assets/skills/spec-review/SKILL.md +87 -0
- package/assets/skills/spec-reviewed/SKILL.md +31 -5
- package/assets/skills/spec-start/SKILL.md +33 -0
- package/package.json +1 -1
- package/src/cli.js +1612 -172
- package/src/env/classify.js +87 -2
- package/src/env/config.js +333 -19
- package/src/env/hooks.js +102 -33
- package/src/env/live.js +124 -1
- package/src/env/mainguard.js +240 -0
- package/src/env/provision.js +106 -10
- package/src/env/registry.js +70 -6
- package/src/env/resolve.js +128 -5
- package/src/env/review.js +854 -29
- package/src/env/reviewers/coderabbit.js +228 -0
- package/src/env/reviewers.js +517 -0
- package/src/env/serve.js +474 -50
- package/src/env/supervise.js +8 -1
- package/src/env/teardown.js +12 -1
- package/src/init.js +185 -24
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The CodeRabbit adapter — `{ "use": "coderabbit" }` is the whole config.
|
|
5
|
+
*
|
|
6
|
+
* A bundled adapter owns two things the generic path cannot: the command line
|
|
7
|
+
* (this tool's flags are its own, and leaking them into skitterspec's config
|
|
8
|
+
* schema was rejected in the spec's Decision 2), and the parser (`cr --agent`
|
|
9
|
+
* emits a JSONL EVENT STREAM, not diagnostics).
|
|
10
|
+
*
|
|
11
|
+
* It also owns the one thing that makes Decision 6 honest. The generic runner
|
|
12
|
+
* sees an exit code and some bytes; it cannot tell a review that found nothing
|
|
13
|
+
* from a CLI that declined because nobody is logged in. This adapter can, and
|
|
14
|
+
* the difference decides whether the page reads `clean` or `did not run — not
|
|
15
|
+
* authenticated` to someone about to commit.
|
|
16
|
+
*
|
|
17
|
+
* SHAPE OF A BUNDLED ADAPTER, so a second one is a file rather than a refactor:
|
|
18
|
+
*
|
|
19
|
+
* command(vars) -> string the shell line, given ${scope}/${base}/…
|
|
20
|
+
* parse(stdout, ctx) -> { findings, malformed, said }
|
|
21
|
+
*
|
|
22
|
+
* `said` is an outcome this adapter is sure of, or null to let the generic
|
|
23
|
+
* rules decide. It can never make a run read as clean that the generic path
|
|
24
|
+
* called failed — it only ever sharpens or downgrades.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Severities `cr` emits, worst first. `error` is not among them, so the generic
|
|
28
|
+
// `levelFor` would call every finding `confirm` — hence the explicit map.
|
|
29
|
+
const FLAG_SEVERITIES = ['critical', 'major']
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Patterns that mean THE REVIEW DID NOT HAPPEN, as opposed to happening and
|
|
33
|
+
* finding nothing.
|
|
34
|
+
*
|
|
35
|
+
* WHAT WOULD FOOL THIS: the CLI's own wording, which is not a documented
|
|
36
|
+
* surface and will change. A pattern that stops matching costs the sharper
|
|
37
|
+
* sentence and nothing else — the run still reports `did not run` with whatever
|
|
38
|
+
* text it did have, because this list only ever runs on output the generic
|
|
39
|
+
* rules ALREADY decided was a failure or an `error` event. It cannot turn a
|
|
40
|
+
* clean review into an accusation, which is the direction that would matter.
|
|
41
|
+
*/
|
|
42
|
+
const NOT_RUN = [
|
|
43
|
+
[/not (?:authenticated|logged in)|unauthori[sz]ed|auth(?:entication)? (?:failed|required)|cr auth login|invalid api key/i, 'not authenticated'],
|
|
44
|
+
[/rate limit|too many requests|quota (?:exceeded|exhausted)|429/i, 'rate limited'],
|
|
45
|
+
[/network|enotfound|econnrefused|etimedout|dns|offline|could not reach/i, 'no network'],
|
|
46
|
+
[/not supported on|unsupported platform/i, 'unsupported platform'],
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
function classify(text) {
|
|
50
|
+
const s = typeof text === 'string' ? text : ''
|
|
51
|
+
for (const [re, label] of NOT_RUN) {
|
|
52
|
+
if (re.test(s)) return label
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The command line, per scope. The two scopes are the render's own two modes,
|
|
59
|
+
* so what the page shows is what was reviewed.
|
|
60
|
+
*
|
|
61
|
+
* `working` — uncommitted and untracked, which is what a phase-end render
|
|
62
|
+
* collects: the page is written BEFORE the commit.
|
|
63
|
+
* `branch` — the whole spec against its base, which is what `--branch` shows.
|
|
64
|
+
*/
|
|
65
|
+
function command(vars) {
|
|
66
|
+
if (vars.scope === 'branch' && vars.base) {
|
|
67
|
+
return `cr review --agent --base ${shellQuote(vars.base)}`
|
|
68
|
+
}
|
|
69
|
+
return 'cr review --agent --uncommitted --include-untracked'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The base comes from a project's own config, not from a page — but it reaches
|
|
73
|
+
// a shell either way, and a branch name is allowed to contain almost anything.
|
|
74
|
+
function shellQuote(s) {
|
|
75
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read the `--agent` stream: one JSON object per line, dispatched on `type`.
|
|
80
|
+
*
|
|
81
|
+
* UNKNOWN EVENT TYPES AND UNKNOWN FIELDS ARE BOTH IGNORED, never fatal. The
|
|
82
|
+
* stream is additive by design, so both mean "a newer CLI than this adapter" —
|
|
83
|
+
* and refusing a whole review because one line carried a field we had not seen
|
|
84
|
+
* would turn every CodeRabbit release into a broken adapter.
|
|
85
|
+
*/
|
|
86
|
+
function parse(stdout, ctx = {}) {
|
|
87
|
+
const findings = []
|
|
88
|
+
let malformed = 0
|
|
89
|
+
let events = 0
|
|
90
|
+
let errorText = null
|
|
91
|
+
let skipped = false
|
|
92
|
+
|
|
93
|
+
for (const raw of String(stdout == null ? '' : stdout).split('\n')) {
|
|
94
|
+
const line = raw.trim()
|
|
95
|
+
if (!line) continue
|
|
96
|
+
let ev
|
|
97
|
+
try {
|
|
98
|
+
ev = JSON.parse(line)
|
|
99
|
+
} catch {
|
|
100
|
+
malformed++
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (!ev || typeof ev !== 'object' || Array.isArray(ev)) {
|
|
104
|
+
malformed++
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
events++
|
|
108
|
+
switch (ev.type) {
|
|
109
|
+
case 'finding': {
|
|
110
|
+
const f = readFinding(ev)
|
|
111
|
+
if (f) findings.push(f)
|
|
112
|
+
else malformed++
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
case 'error':
|
|
116
|
+
// First one wins: what went wrong first is what explains the rest.
|
|
117
|
+
if (!errorText) errorText = textOf(ev.message) || textOf(ev.error) || 'the reviewer reported an error'
|
|
118
|
+
break
|
|
119
|
+
case 'status':
|
|
120
|
+
// Exit 0 with nothing to look at. A real outcome, and a clean one.
|
|
121
|
+
if (ev.status === 'review_skipped') skipped = true
|
|
122
|
+
break
|
|
123
|
+
// `review_context`, `heartbeat`, `complete` and anything added later
|
|
124
|
+
// carry nothing this needs. A heartbeat is a keep-alive; the runner's
|
|
125
|
+
// timeout is generous enough not to need it, and reaching for it would
|
|
126
|
+
// couple the runner to one adapter's stream.
|
|
127
|
+
default:
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { findings, malformed, said: said({ ctx, findings, events, errorText, skipped }) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What this adapter is sure of, or null to let the generic rules decide.
|
|
137
|
+
*
|
|
138
|
+
* IT NEVER UPGRADES A FAILURE TO CLEAN. Every branch below either sharpens a
|
|
139
|
+
* failure's wording or reports one the generic path would have missed; the
|
|
140
|
+
* clean readings are handed back as `null` for `outcomeFor` to reach on its own
|
|
141
|
+
* terms. That asymmetry is the whole safety property — this file is where a
|
|
142
|
+
* vendor-specific guess could quietly turn "nobody is logged in" into a page
|
|
143
|
+
* that reads as approved.
|
|
144
|
+
*/
|
|
145
|
+
function said({ ctx, findings, events, errorText, skipped }) {
|
|
146
|
+
const exitCode = ctx.exitCode
|
|
147
|
+
const stderr = ctx.stderr
|
|
148
|
+
|
|
149
|
+
// An `error` event is the tool telling us directly.
|
|
150
|
+
if (errorText) {
|
|
151
|
+
const why = classify(errorText) || classify(stderr)
|
|
152
|
+
return { state: 'failed', detail: why || firstLine(errorText) }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A non-zero exit with a recognisable reason: say which, rather than echoing
|
|
156
|
+
// whatever sentence happened to be first on stderr.
|
|
157
|
+
if (exitCode !== 0) {
|
|
158
|
+
const why = classify(stderr)
|
|
159
|
+
if (why) return { state: 'failed', detail: why }
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Exit 0 and the tool said it had nothing to review. Clean, and count it as
|
|
164
|
+
// such rather than letting an empty stream look like a truncated one.
|
|
165
|
+
if (skipped && !findings.length) return { state: 'clean', detail: 'nothing to review' }
|
|
166
|
+
|
|
167
|
+
// Exit 0, no findings, and NOT ONE well-formed event: the stream was not the
|
|
168
|
+
// stream we expect. `outcomeFor` would read this as clean when stdout was
|
|
169
|
+
// empty, and that is the false clean this whole feature exists against —
|
|
170
|
+
// cannot-tell routed to the branch that does not claim the code was read.
|
|
171
|
+
if (!events && !findings.length) {
|
|
172
|
+
const why = classify(stderr)
|
|
173
|
+
return { state: 'failed', detail: why || 'no review events — is `cr` authenticated?' }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One finding → one rdjsonl-shaped diagnostic.
|
|
181
|
+
*
|
|
182
|
+
* THE LINE NUMBER IS THE SOFT PART, and worth naming. The documented finding
|
|
183
|
+
* shape is `{severity, fileName, codegenInstructions, suggestions, comment}` —
|
|
184
|
+
* there is NO documented line field. So every plausible spelling is read and a
|
|
185
|
+
* finding with none still renders; it simply loses its jump button rather than
|
|
186
|
+
* being dropped. A finding nobody can place is worth far more than no finding.
|
|
187
|
+
*/
|
|
188
|
+
function readFinding(ev) {
|
|
189
|
+
const file = textOf(ev.fileName) || textOf(ev.file) || textOf(ev.path)
|
|
190
|
+
// `codegenInstructions` first, per the CLI's own guidance, falling back to
|
|
191
|
+
// the human-readable comment when it is absent.
|
|
192
|
+
const message = textOf(ev.codegenInstructions) || textOf(ev.comment) || textOf(ev.message)
|
|
193
|
+
if (!file || !message) return null
|
|
194
|
+
return { file, line: lineOf(ev), severity: severityOf(ev.severity), message }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function lineOf(ev) {
|
|
198
|
+
const nested = ev.range && ev.range.start ? ev.range.start.line : undefined
|
|
199
|
+
for (const v of [nested, ev.line, ev.startLine, ev.lineNumber, ev.start_line]) {
|
|
200
|
+
if (Number.isInteger(v) && v > 0) return v
|
|
201
|
+
}
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Map `cr`'s severity onto the one word the generic `levelFor` turns into a
|
|
207
|
+
* level. `critical` and `major` become `error` → `flag`; everything else,
|
|
208
|
+
* including a severity this build has never seen, becomes `confirm`.
|
|
209
|
+
*
|
|
210
|
+
* An unknown severity landing on `confirm` rather than `flag` is deliberate: a
|
|
211
|
+
* page where everything shouts is a page nobody reads, and the finding is still
|
|
212
|
+
* there either way.
|
|
213
|
+
*/
|
|
214
|
+
function severityOf(severity) {
|
|
215
|
+
const s = typeof severity === 'string' ? severity.trim().toLowerCase() : ''
|
|
216
|
+
return FLAG_SEVERITIES.includes(s) ? 'error' : 'warning'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function textOf(v) {
|
|
220
|
+
return typeof v === 'string' && v.trim() ? v.trim() : ''
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function firstLine(s) {
|
|
224
|
+
const line = String(s || '').split('\n').map((l) => l.trim()).find(Boolean)
|
|
225
|
+
return line ? (line.length > 200 ? line.slice(0, 197) + '…' : line) : null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = { command, parse, classify, readFinding, severityOf, FLAG_SEVERITIES }
|