@skitterbyte/skitterspec-linear 14.0.0 → 17.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 +304 -4
- package/README.md +34 -1
- package/assets/claude-md-section.md +29 -18
- package/assets/commands/spec-remote-review.md +22 -0
- package/assets/core/SETUP.md +10 -6
- package/assets/core/env.config.json.example +4 -2
- package/assets/core/env.config.md +103 -25
- package/assets/core/linear.config.json.example +2 -1
- package/assets/core/linear.config.md +49 -22
- package/assets/review/page.html +1101 -108
- package/assets/rules/spec-planning.md +39 -7
- package/assets/rules/spec-reports.md +210 -31
- package/assets/skills/spec/SKILL.md +161 -4
- package/assets/skills/spec-bug/SKILL.md +102 -49
- package/assets/skills/spec-cancel/SKILL.md +2 -2
- package/assets/skills/spec-claim/SKILL.md +12 -4
- package/assets/skills/spec-complete/SKILL.md +2 -2
- package/assets/skills/spec-diff/SKILL.md +183 -39
- package/assets/skills/spec-hotfix/SKILL.md +96 -49
- package/assets/skills/spec-init/SKILL.md +18 -6
- package/assets/skills/spec-linear-setup/SKILL.md +19 -11
- package/assets/skills/spec-next/SKILL.md +147 -62
- package/assets/skills/spec-push/SKILL.md +45 -0
- package/assets/skills/spec-review/SKILL.md +89 -2
- package/assets/skills/spec-reviewed/SKILL.md +31 -5
- package/assets/skills/spec-start/SKILL.md +26 -3
- package/assets/skills/spec-status/SKILL.md +20 -6
- package/assets/skills/spec-sync/SKILL.md +1 -0
- package/package.json +2 -2
- package/src/cli.js +940 -116
- package/src/env/classify.js +87 -2
- package/src/env/config.js +214 -17
- package/src/env/hooks.js +49 -9
- package/src/env/live.js +94 -0
- package/src/env/resolve.js +36 -2
- package/src/env/review.js +581 -21
- package/src/env/serve.js +298 -19
- package/src/env/supervise.js +8 -1
- package/src/init.js +88 -13
- package/src/vendor/linear/api.js +111 -2
- package/src/vendor/linear/cli-sync.js +661 -11
- package/src/vendor/linear/config.js +41 -13
- package/src/vendor/linear/doctor.js +6 -5
- package/src/vendor/sync-core/index.js +11 -3
- package/src/vendor/sync-core/src/compare.js +65 -0
- package/src/vendor/sync-core/src/normalize.js +26 -0
- package/src/vendor/sync-core/src/retarget.js +1 -1
- package/src/vendor/sync-core/src/task-block.js +2 -2
- /package/assets/hooks/{review-gate.js → review-gate.cjs} +0 -0
package/src/vendor/linear/api.js
CHANGED
|
@@ -117,6 +117,76 @@ const ISSUE_FIELDS =
|
|
|
117
117
|
// resolves by id and can still be assigned.
|
|
118
118
|
const USER_FIELDS = 'id name displayName email active'
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Codes Linear returns that no amount of waiting will clear. `userError` decides
|
|
122
|
+
* it on its own where Linear sends one; this is the fallback for a payload that
|
|
123
|
+
* carries a code and not the flag.
|
|
124
|
+
*
|
|
125
|
+
* DELIBERATELY SHORT. A code absent from this list and carrying no `userError`
|
|
126
|
+
* is a cannot-tell, not a retryable — see `LinearRefusal` below.
|
|
127
|
+
*/
|
|
128
|
+
const UNRETRYABLE_CODES = new Set(['USAGE_LIMIT_EXCEEDED', 'FEATURE_NOT_ACCESSIBLE', 'INVALID_INPUT'])
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* What Linear refused, kept whole.
|
|
132
|
+
*
|
|
133
|
+
* THE FIELDS ARE ADDED BESIDE `message`, NEVER INSTEAD OF IT. Every existing
|
|
134
|
+
* caller prints `error.message`, and this must not require them all to change to
|
|
135
|
+
* go on working — so the joined message is still exactly what it was.
|
|
136
|
+
*
|
|
137
|
+
* `retryable` is THREE-VALUED and the third value is the point:
|
|
138
|
+
*
|
|
139
|
+
* - `false` — Linear said `userError`, or sent a code that never clears. Waiting
|
|
140
|
+
* cannot help, so the retry loop must not spend three attempts proving it.
|
|
141
|
+
* - `true` — set by the throttle path, where waiting is the entire answer.
|
|
142
|
+
* - `null` — nothing said either way. Every error the MCP path produces and
|
|
143
|
+
* every older Linear response lands here, and it must read as neither: a
|
|
144
|
+
* missing field is not evidence (`.claude/rules/negative-checks.md` rule 1).
|
|
145
|
+
*
|
|
146
|
+
* WHAT THIS COST WHEN IT WAS MISSING: a push failed twice with
|
|
147
|
+
* `Linear API error: usage limit exceeded`, which reads like a throttle. An hour
|
|
148
|
+
* went into checking a rate limit that was untouched, while
|
|
149
|
+
* `extensions.userPresentableMessage` — naming both the free-plan issue cap and
|
|
150
|
+
* how to fix it — sat one layer below what was printed.
|
|
151
|
+
*/
|
|
152
|
+
class LinearRefusal extends Error {
|
|
153
|
+
constructor(message, { code = null, userError = null, userPresentableMessage = null, meta = null, retryable = null } = {}) {
|
|
154
|
+
super(message)
|
|
155
|
+
this.name = 'LinearRefusal'
|
|
156
|
+
this.code = code
|
|
157
|
+
this.userError = userError
|
|
158
|
+
this.userPresentableMessage = userPresentableMessage
|
|
159
|
+
this.meta = meta
|
|
160
|
+
this.retryable = retryable
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Read a GraphQL `errors` array into one refusal. Pure.
|
|
166
|
+
*
|
|
167
|
+
* The first entry carries the detail — Linear sends one error per failed field
|
|
168
|
+
* and a mutation fails on one — while the message joins them all, as it always
|
|
169
|
+
* has, so nothing that reads `message` notices this changed.
|
|
170
|
+
*/
|
|
171
|
+
function refusalFrom(errors) {
|
|
172
|
+
const joined = errors.map((e) => (e && e.message) || String(e)).join('; ')
|
|
173
|
+
const ext = (errors[0] && errors[0].extensions) || {}
|
|
174
|
+
const code = typeof ext.code === 'string' ? ext.code : null
|
|
175
|
+
const userError = typeof ext.userError === 'boolean' ? ext.userError : null
|
|
176
|
+
// `false` only on a POSITIVE signal. An unrecognised code with no flag stays
|
|
177
|
+
// `null`, so it keeps whatever retry behaviour it had rather than gaining a
|
|
178
|
+
// verdict nothing supports.
|
|
179
|
+
const retryable = userError === true || (code !== null && UNRETRYABLE_CODES.has(code)) ? false : null
|
|
180
|
+
return new LinearRefusal(`Linear API error: ${joined}`, {
|
|
181
|
+
code,
|
|
182
|
+
userError,
|
|
183
|
+
userPresentableMessage:
|
|
184
|
+
typeof ext.userPresentableMessage === 'string' ? ext.userPresentableMessage : null,
|
|
185
|
+
meta: ext.meta && typeof ext.meta === 'object' ? ext.meta : null,
|
|
186
|
+
retryable,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
120
190
|
/**
|
|
121
191
|
* A GraphQL caller bound to one key. Throws a clear Error on transport failure,
|
|
122
192
|
* on an HTTP error, and on a GraphQL `errors` payload — an `apply` that half
|
|
@@ -160,12 +230,20 @@ function makeClient({ apiKey, fetch: fetchImpl, endpoint = ENDPOINT, sleep, maxR
|
|
|
160
230
|
continue
|
|
161
231
|
}
|
|
162
232
|
if (res.status === 429) {
|
|
163
|
-
|
|
233
|
+
// RETRYABLE, said out loud. This is the failure where waiting is the
|
|
234
|
+
// whole answer, and the one a refusal must be distinguishable from.
|
|
235
|
+
throw new LinearRefusal(
|
|
236
|
+
`Linear rate-limited this request and did not recover after ${maxRetries} retries`,
|
|
237
|
+
{ retryable: true },
|
|
238
|
+
)
|
|
164
239
|
}
|
|
165
240
|
if (!res.ok) throw new Error(`Linear API returned HTTP ${res.status}`)
|
|
166
241
|
const body = await res.json()
|
|
167
242
|
if (body && Array.isArray(body.errors) && body.errors.length) {
|
|
168
|
-
|
|
243
|
+
// NOT RETRIED, when Linear says it cannot succeed. The loop above exists
|
|
244
|
+
// for throttling; a usage cap retried three times is three identical
|
|
245
|
+
// refusals and a slower failure.
|
|
246
|
+
throw refusalFrom(body.errors)
|
|
169
247
|
}
|
|
170
248
|
return body && body.data
|
|
171
249
|
}
|
|
@@ -388,6 +466,34 @@ function makeApiAdapter({ apiKey, fetch: fetchImpl, endpoint, sleep, maxRetries
|
|
|
388
466
|
if (data && data.team) return (data.team.states && data.team.states.nodes) || []
|
|
389
467
|
return (data && data.workflowStates && data.workflowStates.nodes) || []
|
|
390
468
|
},
|
|
469
|
+
// An issue's comments, and a comment posted onto it — the two halves of
|
|
470
|
+
// `spec-sync preserve`, which keeps a reporter's original description alive
|
|
471
|
+
// once the linking push replaces it with the generated spec.
|
|
472
|
+
//
|
|
473
|
+
// COMMENTS ARE NOT PART OF THE PROJECTION and must never become part of it.
|
|
474
|
+
// `sync.fieldOwnership` excludes them deliberately (they are Linear-native
|
|
475
|
+
// triage), which is exactly what makes a comment a safe place to preserve
|
|
476
|
+
// something: no push can clobber it, and no read of one feeds the repo.
|
|
477
|
+
// These two ops exist for one write at adoption and one read to make that
|
|
478
|
+
// write idempotent — nothing else calls them.
|
|
479
|
+
//
|
|
480
|
+
// API-only, like `listIssueStates` and `readTeam`: the contract runs one
|
|
481
|
+
// way, so the adapter may add ops and may only never be missing one.
|
|
482
|
+
async listComments(issueId) {
|
|
483
|
+
const data = await call(
|
|
484
|
+
`query($id: String!) { issue(id: $id) { comments { nodes { id body } } } }`,
|
|
485
|
+
{ id: issueId },
|
|
486
|
+
)
|
|
487
|
+
return (data && data.issue && data.issue.comments && data.issue.comments.nodes) || []
|
|
488
|
+
},
|
|
489
|
+
async createComment(issueId, body) {
|
|
490
|
+
const data = await call(
|
|
491
|
+
`mutation($input: CommentCreateInput!) {
|
|
492
|
+
commentCreate(input: $input) { success comment { id body } } }`,
|
|
493
|
+
{ input: { issueId, body } },
|
|
494
|
+
)
|
|
495
|
+
return (data && data.commentCreate && data.commentCreate.comment) || null
|
|
496
|
+
},
|
|
391
497
|
}
|
|
392
498
|
}
|
|
393
499
|
|
|
@@ -424,6 +530,9 @@ function stateIdFor(bucket, config, states) {
|
|
|
424
530
|
|
|
425
531
|
module.exports = {
|
|
426
532
|
ENDPOINT,
|
|
533
|
+
LinearRefusal,
|
|
534
|
+
refusalFrom,
|
|
535
|
+
UNRETRYABLE_CODES,
|
|
427
536
|
MAX_RETRIES,
|
|
428
537
|
resolveApiKey,
|
|
429
538
|
makeClient,
|