@skitterbyte/skitterspec-linear 10.4.0 → 10.5.2
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/assets/core/SETUP.md +13 -1
- package/assets/core/linear.config.md +43 -1
- package/assets/rules/negative-checks.md +69 -0
- package/assets/skills/spec-linear-setup/SKILL.md +48 -10
- package/assets/skills/spec-status/SKILL.md +18 -0
- package/assets/skills/spec-sync/SKILL.md +25 -35
- package/package.json +1 -1
- package/src/init.js +5 -0
- package/src/vendor/linear/api.js +31 -1
- package/src/vendor/linear/cli-sync.js +476 -111
- package/src/vendor/linear/doctor.js +244 -296
- package/src/vendor/sync-core/index.js +6 -0
- package/src/vendor/sync-core/src/normalize.js +17 -2
- package/src/vendor/sync-core/src/retarget.js +274 -0
- package/src/vendor/sync-core/src/verify.js +5 -0
package/assets/core/SETUP.md
CHANGED
|
@@ -237,7 +237,19 @@ the repo.
|
|
|
237
237
|
|
|
238
238
|
## 8. Smoke test (verify your setup)
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
**Start here — one command answers "did it work?":**
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
skitterspec spec-sync doctor # every layer, offline
|
|
244
|
+
skitterspec spec-sync doctor --check-remote # …and prove the key reaches Linear
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
It reports the scaffold, per-spec isolation, the tracker config and the API key
|
|
248
|
+
in one table, and every row that needs attention names the command that fixes
|
|
249
|
+
it. It exits non-zero only when something is **broken** (configured but wrong) —
|
|
250
|
+
a `missing` row is an opt-in you have not taken, which is fine.
|
|
251
|
+
|
|
252
|
+
Then, with a linked spec, confirm push end-to-end:
|
|
241
253
|
|
|
242
254
|
1. `/spec-status` → shows what would push (`pending — N to create, M to update`).
|
|
243
255
|
2. `/spec-push` → creates the spec issue and its phase sub-issues and sets the
|
|
@@ -29,7 +29,8 @@ absence). A `sync.fieldOwnership` value outside `both|pull|push` is a hard error
|
|
|
29
29
|
// IDs are read by the MCP adapter; leave blank until you connect the `linear`
|
|
30
30
|
// MCP server.
|
|
31
31
|
"linear": {
|
|
32
|
-
"teamKey": "", // human-facing key, e.g. "ENG"
|
|
32
|
+
"teamKey": "", // human-facing key, e.g. "ENG" — the RECORDED key;
|
|
33
|
+
// see "Renaming a team" below (optional)
|
|
33
34
|
"teamId": "", // Linear team UUID (the issue's team)
|
|
34
35
|
"projectId": "" // DEFAULT for the project picker (see below)
|
|
35
36
|
},
|
|
@@ -402,6 +403,22 @@ issue exists, where it lives is Linear's business: move it between projects and
|
|
|
402
403
|
A spec that **adopted** an existing issue (see below) skips the picker entirely —
|
|
403
404
|
it was filed somewhere deliberately.
|
|
404
405
|
|
|
406
|
+
`skitterspec spec-sync doctor` reports what this setting is doing:
|
|
407
|
+
|
|
408
|
+
| `linear.projectId` | Row | Meaning |
|
|
409
|
+
|--------------------|-----|---------|
|
|
410
|
+
| unset | `missing` | specs file to the team, and the picker asks each push — a supported choice, and it never fails the run |
|
|
411
|
+
| set | `ok` | the id is there; add `--check-remote` to resolve it against Linear |
|
|
412
|
+
| set, resolves in the team | `ok` | the project exists and this team is one of its teams |
|
|
413
|
+
| set, resolves elsewhere | `broken` | specs would file out of the team — exits non-zero |
|
|
414
|
+
| set, resolves to nothing | `broken` | a deleted or mistyped id |
|
|
415
|
+
|
|
416
|
+
`doctor --mcp <file>` additionally compares this id — and the team and workspace —
|
|
417
|
+
against what the Linear **MCP server** reports, so the two transports cannot
|
|
418
|
+
quietly point at different places. A mismatch is `broken` and names both sides; it
|
|
419
|
+
is never resolved by rewriting this file, because which side is wrong is yours to
|
|
420
|
+
say.
|
|
421
|
+
|
|
405
422
|
### Known limits — one team per repo, no initiatives
|
|
406
423
|
|
|
407
424
|
Two things this config deliberately cannot express today. Both are limits, not
|
|
@@ -483,3 +500,28 @@ Linear-native triage and are never touched. No base merge, no conflicts, no
|
|
|
483
500
|
last-pushed snapshot is content hashes of the last push, so `/spec-push` knows
|
|
484
501
|
what changed without reading Linear back; each worktree carries its own, so it
|
|
485
502
|
must travel with the branch.
|
|
503
|
+
|
|
504
|
+
## Renaming a team
|
|
505
|
+
|
|
506
|
+
Renaming a Linear team rewrites the key in every issue identifier (`ENG-7` →
|
|
507
|
+
`PLT-7`). The repo stamps identifiers in three places — spec frontmatter, the
|
|
508
|
+
`linear-base` snapshot filenames, and the `subIssues` keys inside them — and
|
|
509
|
+
nothing moves them, so afterwards `/spec-push` fails with
|
|
510
|
+
`no Linear issue found for ENG-7`.
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
skitterspec spec-sync retarget # what would change
|
|
514
|
+
skitterspec spec-sync retarget --yes # apply it
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
`teamId` survives a rename, so the command asks Linear for that team's *current*
|
|
518
|
+
key and compares it with `teamKey` — which is why `teamKey` is worth setting even
|
|
519
|
+
though nothing else reads it. With `teamKey` empty it falls back to the prefix
|
|
520
|
+
observed in the stamps, and refuses if those disagree rather than guessing.
|
|
521
|
+
|
|
522
|
+
Before reporting a plan as safe it resolves one remapped identifier and compares
|
|
523
|
+
its **title** to the spec's: a team rename preserves issue numbers, and this is
|
|
524
|
+
what proves it did. `--yes` refuses on a dirty tree so the rewrite lands as one
|
|
525
|
+
revertable change, and it rewrites machine-read fields only — identifiers in
|
|
526
|
+
spec prose are the historical record and are left alone. It pushes nothing; the
|
|
527
|
+
next ordinary `/spec-push` reconciles content once its issues resolve again.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Checks That Accuse
|
|
2
|
+
|
|
3
|
+
A check **accuses** when being wrong costs something: it deletes, it exits
|
|
4
|
+
non-zero, or it tells the user their code is broken. Those checks earn the four
|
|
5
|
+
rules below. An ordinary conditional does not — this is about the ones that act
|
|
6
|
+
on what they conclude.
|
|
7
|
+
|
|
8
|
+
Almost every accusation begins as an **absence**: a name not in a list, a
|
|
9
|
+
directory not on disk, a version not in a response. An absence is evidence only
|
|
10
|
+
once you have established that the lookup could have seen the thing. Three
|
|
11
|
+
times in one day, in unrelated code, we established nothing and acted anyway:
|
|
12
|
+
|
|
13
|
+
| Absence observed | Concluded | What had blinded the lookup |
|
|
14
|
+
|------------------|-----------|-----------------------------|
|
|
15
|
+
| ref not in the issue list | "it does not exist" | the query excluded archived issues, and capped at 250 |
|
|
16
|
+
| version not in the registry's list | "the publish failed" | the registry is eventually consistent |
|
|
17
|
+
| lifecycle folder not on disk | "half-installed" | git does not store an empty directory |
|
|
18
|
+
|
|
19
|
+
The bills: 146 healthy refs accused, a valid release tag deleted, a non-zero
|
|
20
|
+
exit on a healthy repo. Each blind spot was knowable in advance.
|
|
21
|
+
|
|
22
|
+
## 1. Prefer a positive signal to an absence
|
|
23
|
+
|
|
24
|
+
Assert something that must be **present**, not something that must not be
|
|
25
|
+
missing. A positive signal fails loudly when you are wrong about it; an absence
|
|
26
|
+
fails silently whenever the lookup was narrower than you assumed.
|
|
27
|
+
|
|
28
|
+
The scaffold check above stopped asking "is the lifecycle folder there?" — a
|
|
29
|
+
folder git drops as soon as it empties — and started asking whether the config
|
|
30
|
+
folder the installer always writes into is there. Same intent, and the new
|
|
31
|
+
question has an answer.
|
|
32
|
+
|
|
33
|
+
Where no positive signal exists, widen the lookup until absence means
|
|
34
|
+
something (include the archived rows, ask the API for the one id rather than
|
|
35
|
+
scanning a page) — or do not conclude.
|
|
36
|
+
|
|
37
|
+
## 2. Name the blind spot beside the check
|
|
38
|
+
|
|
39
|
+
A comment naming what could make this lookup lie is what makes the next reader
|
|
40
|
+
check it. Not what the code does — what would fool it:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// A LIFECYCLE FOLDER IS NOT CHECKED, deliberately. git does not track empty
|
|
44
|
+
// directories, so it disappears whenever the bucket empties and returns the
|
|
45
|
+
// moment something lands in it.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Write it when you write the check, while you still know why it is safe.
|
|
49
|
+
|
|
50
|
+
## 3. Pair every accusation with a stays-silent test
|
|
51
|
+
|
|
52
|
+
For each accusing check, a test that feeds it a **healthy but unusual** input
|
|
53
|
+
and asserts it says nothing: the empty bucket, the archived record, the
|
|
54
|
+
just-published version, the file the user edited on purpose. The positive test
|
|
55
|
+
proves the check can fire; only this one proves it does not fire at everyone
|
|
56
|
+
else. All three incidents would have been caught by it, and prose alone had
|
|
57
|
+
already failed to prevent them.
|
|
58
|
+
|
|
59
|
+
## 4. Bias the unknown case toward inaction
|
|
60
|
+
|
|
61
|
+
Three states, not two: yes, no, and *cannot tell*. Route the third to the
|
|
62
|
+
harmless branch — skip, warn, keep, retry — never to the destructive one.
|
|
63
|
+
|
|
64
|
+
The install manifest classifies a file whose hash it does not recognise as
|
|
65
|
+
`customized` rather than stale, so a resync **keeps** it (`managedState`,
|
|
66
|
+
`packages/common/src/init.js`). An unrecognised hash could mean a user's edit or
|
|
67
|
+
a lost manifest; only one of those readings is safe to act on, so it takes that
|
|
68
|
+
one. Being wrong there costs a redundant file on disk. The opposite default
|
|
69
|
+
costs the user their work.
|
|
@@ -47,6 +47,11 @@ anything. The interview offers real lists; it never prompts for a raw id.
|
|
|
47
47
|
| `list_projects` | the projects, per candidate team — names + ids, minus archived/completed |
|
|
48
48
|
| `list_issue_statuses` | the team's **issue workflow-state names**, exactly as spelled |
|
|
49
49
|
| `list_issue_labels` | the label names available for intake routing |
|
|
50
|
+
| `get_workspace` | **which workspace this MCP server is connected to** — id + name |
|
|
51
|
+
|
|
52
|
+
`get_workspace` is not part of the interview: nothing is asked about it. It is
|
|
53
|
+
recorded so step 8 can prove the MCP server and the API key are pointed at the
|
|
54
|
+
same Linear, which nothing else establishes.
|
|
50
55
|
|
|
51
56
|
Projects, labels and statuses are all **team-scoped** — fetch them for the team
|
|
52
57
|
once step 3 has settled it, not for the whole workspace up front.
|
|
@@ -159,22 +164,55 @@ paraphrase it into "done".
|
|
|
159
164
|
|
|
160
165
|
## 8. Report and hand off
|
|
161
166
|
|
|
162
|
-
|
|
167
|
+
**Finish by checking, not by describing.** First write down what the MCP server
|
|
168
|
+
says, from the reads you already made in step 2 — no extra round trip unless a
|
|
169
|
+
project was chosen and you have not read it yet (`get_project`):
|
|
163
170
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
171
|
+
```json
|
|
172
|
+
{ "workspace": {"id": "…", "name": "…"},
|
|
173
|
+
"team": {"id": "…", "key": "SKS"},
|
|
174
|
+
"project": {"id": "…", "name": "…"} }
|
|
175
|
+
```
|
|
168
176
|
|
|
169
|
-
|
|
177
|
+
Every key is optional, and **omit what you could not fetch** rather than guessing
|
|
178
|
+
— an absent field is reported as unchecked, while a wrong one is reported as a
|
|
179
|
+
mismatch. Then run:
|
|
170
180
|
|
|
171
181
|
```
|
|
172
|
-
skitterspec spec-sync
|
|
182
|
+
skitterspec spec-sync doctor --mcp <factsfile>
|
|
173
183
|
```
|
|
174
184
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
and relay its table. That is the difference between a summary of what setup
|
|
186
|
+
*meant* to do and a check of what is actually true — including the layers this
|
|
187
|
+
skill never touched (the scaffold, isolation) and the one it deliberately does
|
|
188
|
+
not set (the key). Every row that needs attention names its own fix, so there is
|
|
189
|
+
nothing to paraphrase.
|
|
190
|
+
|
|
191
|
+
It exits non-zero only when a layer is **broken** — configured but wrong. A
|
|
192
|
+
`missing` row is an opt-in nobody took, which is fine; report it, don't treat it
|
|
193
|
+
as a failure. An empty `project` row is exactly that: specs file to the team and
|
|
194
|
+
the picker asks each push.
|
|
195
|
+
|
|
196
|
+
**A `broken` `mcp` row is the one to stop on.** It means the MCP server and the
|
|
197
|
+
config (or the API key) name different workspaces, teams or projects — so where a
|
|
198
|
+
spec lands depends on which transport ran. Relay both sides as the row prints
|
|
199
|
+
them and **stop**: do not rewrite the config to make them agree. Which one is
|
|
200
|
+
correct is the user's to say — the API key may be the wrong one just as easily as
|
|
201
|
+
the config, and picking a winner silently sends their specs somewhere they did
|
|
202
|
+
not choose. Ask which is right, then re-run this skill (or reconnect the MCP
|
|
203
|
+
server) to match it.
|
|
204
|
+
|
|
205
|
+
Then name the next step:
|
|
206
|
+
|
|
207
|
+
- `/spec` — write a spec; with Linear configured it offers the project picker,
|
|
208
|
+
creates the linked issue and stamps the id.
|
|
209
|
+
- `/spec-status` — read-only drift report, the safe way to prove the link works.
|
|
210
|
+
- `/spec-push` — send a spec up.
|
|
211
|
+
|
|
212
|
+
**Never ask for the API key.** `doctor`'s `key` row already reports whether one
|
|
213
|
+
is set and where from, masked — `spec-sync credentials status` says the same in
|
|
214
|
+
more detail if you need it. If the key is missing, tell the user to run this
|
|
215
|
+
**themselves, in their own terminal**:
|
|
178
216
|
|
|
179
217
|
```
|
|
180
218
|
skitterspec spec-sync credentials set
|
|
@@ -46,6 +46,24 @@ skitterspec spec-sync status <spec> [--remote <issuefile>] [--workspace-states <
|
|
|
46
46
|
- With `--workspace-states`, fails loudly if a configured state name isn't in the
|
|
47
47
|
workspace (Linear would silently no-op it).
|
|
48
48
|
|
|
49
|
+
## 3b. A key mismatch is a different problem
|
|
50
|
+
|
|
51
|
+
If the spec's `linear_identifier` carries a **different team key** than
|
|
52
|
+
`linear.teamKey` in `specs/.core/linear.config.json` — or the issue read in step
|
|
53
|
+
2 came back under another key — the team was renamed and the repo's stamps are
|
|
54
|
+
stale. That is not push drift and `/spec-push` cannot fix it: it will fail with
|
|
55
|
+
`no Linear issue found for <old>-<n>`.
|
|
56
|
+
|
|
57
|
+
Point the user at the CLI and stop:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
pnpm exec skitterspec-linear spec-sync retarget
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Read-only until `--yes`. Do not attempt the rewrite by hand — the identifiers
|
|
64
|
+
live in frontmatter, snapshot filenames and the keys inside those snapshots, and
|
|
65
|
+
a hand edit misses some (it has, twice).
|
|
66
|
+
|
|
49
67
|
## 4. Report
|
|
50
68
|
|
|
51
69
|
Relay the engine's output verbatim. Suggest `/spec-push` if a push is pending.
|
|
@@ -36,7 +36,7 @@ In a project that installs the Linear superset the binary is
|
|
|
36
36
|
| "did the mirror survive the push?" | `verify <spec> --stored <file>` |
|
|
37
37
|
| "link this spec to KEY-1 by hand" | `stamp <spec> --issue KEY-1` |
|
|
38
38
|
| "mirror the whole backlog / every complete spec" | `apply --all <bucket>` — **confirm first** |
|
|
39
|
-
| "is the team key stale?", "did Linear get renamed?" | `
|
|
39
|
+
| "is the team key stale?", "did Linear get renamed?" | `retarget` |
|
|
40
40
|
| push one spec, or "what would push?" | **defer** — see below |
|
|
41
41
|
|
|
42
42
|
**With no argument, run `linked`.** It is the repo-wide overview, it is
|
|
@@ -131,44 +131,34 @@ its object exists, so an interrupted run continues rather than duplicating.
|
|
|
131
131
|
|
|
132
132
|
`--all` refuses over MCP by design — bulk goes through the API path.
|
|
133
133
|
|
|
134
|
-
## 7. `
|
|
134
|
+
## 7. `retarget` — after a Linear team is renamed
|
|
135
135
|
|
|
136
136
|
```
|
|
137
|
-
pnpm exec skitterspec-linear spec-sync
|
|
137
|
+
pnpm exec skitterspec-linear spec-sync retarget [--yes]
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
Renaming a Linear team
|
|
141
|
-
the repo moves**: the stamps, the config `teamKey`, and the
|
|
142
|
-
and their sub-issue keys all keep the old prefix
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- **
|
|
149
|
-
|
|
150
|
-
- **
|
|
151
|
-
|
|
152
|
-
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
repair is one large diff and has to be reviewable (and `git checkout -- .`-able)
|
|
163
|
-
on its own — so commit or stash before offering it.
|
|
164
|
-
|
|
165
|
-
It exits non-zero when it left anything behind: a ref that resolves to no issue
|
|
166
|
-
is **not** rewritten, because repair fixes what is provably repairable rather
|
|
167
|
-
than inventing a target. Relay that as an unfinished repair, not a success.
|
|
168
|
-
|
|
169
|
-
After a repair, `spec-sync status` should still read `up to date` — the snapshot
|
|
170
|
-
hashes are content-derived, so only their keys move. If it does not, say so
|
|
171
|
-
rather than pushing over it.
|
|
140
|
+
Renaming a Linear team rewrites the key in every issue identifier, and **nothing
|
|
141
|
+
in the repo moves**: the frontmatter stamps, the config `teamKey`, and the
|
|
142
|
+
snapshot filenames and their sub-issue keys all keep the old prefix, so
|
|
143
|
+
`/spec-push` starts failing with `no Linear issue found for SKI-7`.
|
|
144
|
+
|
|
145
|
+
`retarget` detects the rename (the team id survives it; the key does not) and
|
|
146
|
+
rewrites those fields. Read-only until `--yes`.
|
|
147
|
+
|
|
148
|
+
- It **never takes the new key as an argument** — a typo would rewrite every
|
|
149
|
+
stamp to a key that does not exist.
|
|
150
|
+
- It **spot-checks one identifier by title** before reporting the plan as safe.
|
|
151
|
+
Existence is not identity: `SKS-7` existing does not make it the issue that was
|
|
152
|
+
`SKI-7`. A mismatch refuses, and writes nothing.
|
|
153
|
+
- It rewrites **machine-read fields only**. Identifiers in spec prose are the
|
|
154
|
+
historical record and are left alone — so do not report a retarget as having
|
|
155
|
+
made the repo free of the old key.
|
|
156
|
+
- `--yes` refuses on a dirty tree, so the rewrite lands as one revertable change.
|
|
157
|
+
It pushes nothing: only the repo's stamps move, and the next ordinary
|
|
158
|
+
`/spec-push` reconciles content now that its issues resolve again.
|
|
159
|
+
|
|
160
|
+
Over MCP the team key is unreadable (`get_team` does not return it), so it says
|
|
161
|
+
so and asks you to confirm the key rather than guessing.
|
|
172
162
|
|
|
173
163
|
## 8. Report
|
|
174
164
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec-linear",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.5.2",
|
|
4
4
|
"description": "Spec-driven development for Claude Code, with one-way Linear sync — a superset of @skitterbyte/skitterspec: the base filesystem workflow plus /spec-status · /spec-push and the spec-sync CLI. The repo is canonical; Linear is a generated mirror. Install this OR the base, not both.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/init.js
CHANGED
|
@@ -406,6 +406,11 @@ function installClaudeMd(dir, { mode }) {
|
|
|
406
406
|
|
|
407
407
|
// True when the repo looks already set up: any managed file present, any spec
|
|
408
408
|
// lifecycle folder, or the CLAUDE.md spec marker (Decision 1 — detect eagerly).
|
|
409
|
+
// Is skitterspec already installed here? Matched on what we ACTUALLY install —
|
|
410
|
+
// our managed files, our lifecycle folders, our CLAUDE.md marker — never on
|
|
411
|
+
// `.claude/` merely existing: someone else's skills are not evidence of ours,
|
|
412
|
+
// and reading them as ours would treat every Claude Code project as a
|
|
413
|
+
// half-finished install.
|
|
409
414
|
function isExistingSetup(dir) {
|
|
410
415
|
if (managedTargets(dir).some((t) => fs.existsSync(t.abs))) return true
|
|
411
416
|
if (SPEC_FOLDERS.some((f) => fs.existsSync(path.join(dir, 'specs', f)))) return true
|
package/src/vendor/linear/api.js
CHANGED
|
@@ -234,7 +234,7 @@ function makeApiAdapter({ apiKey, fetch: fetchImpl, endpoint, sleep, maxRetries
|
|
|
234
234
|
if (data && data.team) return (data.team.projects && data.team.projects.nodes) || []
|
|
235
235
|
return (data && data.projects && data.projects.nodes) || []
|
|
236
236
|
},
|
|
237
|
-
// The team's CURRENT key, which is what `
|
|
237
|
+
// The team's CURRENT key, which is what `retarget` compares stamped
|
|
238
238
|
// identifiers against. Read from Linear rather than `config.linear.teamKey`
|
|
239
239
|
// on purpose: the config key is itself one of the things that goes stale
|
|
240
240
|
// when a team is renamed, so trusting it would make drift invisible. The
|
|
@@ -246,6 +246,36 @@ function makeApiAdapter({ apiKey, fetch: fetchImpl, endpoint, sleep, maxRetries
|
|
|
246
246
|
const data = await call(`query($id: String!) { team(id: $id) { id key name } }`, { id: teamId })
|
|
247
247
|
return (data && data.team) || null
|
|
248
248
|
},
|
|
249
|
+
// The WORKSPACE this key belongs to. The one fact that says whether the API
|
|
250
|
+
// transport and the MCP transport are pointed at the same Linear at all —
|
|
251
|
+
// every other id could match by coincidence across two workspaces, but an
|
|
252
|
+
// organization id is the workspace.
|
|
253
|
+
//
|
|
254
|
+
// API-only, like `readTeam`.
|
|
255
|
+
async readOrganization() {
|
|
256
|
+
const data = await call('query { organization { id name urlKey } }')
|
|
257
|
+
return (data && data.organization) || null
|
|
258
|
+
},
|
|
259
|
+
// One project by id, with the teams it belongs to — enough to answer both
|
|
260
|
+
// halves of the `project` doctor row: does this id resolve at all, and is it
|
|
261
|
+
// a project of the team this repo files into?
|
|
262
|
+
//
|
|
263
|
+
// `teams`, not `team`: a Linear project can span several teams, so belonging
|
|
264
|
+
// is a membership test. Treating it as a single field would report a healthy
|
|
265
|
+
// shared project as foreign.
|
|
266
|
+
//
|
|
267
|
+
// API-only, like `readTeam` — the adapter may add ops, it may only never be
|
|
268
|
+
// missing one (see the operation-contract test).
|
|
269
|
+
async readProject(projectId) {
|
|
270
|
+
const data = await call(
|
|
271
|
+
`query($id: String!) { project(id: $id) { id name teams { nodes { id key } } } }`,
|
|
272
|
+
{ id: projectId },
|
|
273
|
+
)
|
|
274
|
+
const project = (data && data.project) || null
|
|
275
|
+
if (!project) return null
|
|
276
|
+
const teams = (project.teams && project.teams.nodes) || []
|
|
277
|
+
return { id: project.id, name: project.name, teams }
|
|
278
|
+
},
|
|
249
279
|
// The workspace's issue workflow states, in the shape `--workspace-states`
|
|
250
280
|
// already accepts, so the existing state check is reused rather than forked.
|
|
251
281
|
async listIssueStates(teamId) {
|