buhtig 0.1.0-alpha.1

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.
@@ -0,0 +1,486 @@
1
+ # The review API
2
+
3
+ Every call the review surface offers, in the order a run uses them.
4
+
5
+ {{preamble}}
6
+
7
+ ---
8
+
9
+ ## 1 · The taxonomy
10
+
11
+ What facets exist, the scales, the summary sections, and which fields you may write:
12
+
13
+ ```call review.config
14
+ { "owner": "$OWNER", "repo": "$REPO" }
15
+ ```
16
+
17
+ `state` is `configured` | `absent` (no `review:` block, or the dir has no facet markdown) |
18
+ `unknown` (configured but buhtig cannot reach it — `detail` says why). On anything but `configured`,
19
+ stop and tell the human; do not invent a taxonomy.
20
+
21
+ The **reviewable units** are the facets with `isFamily: false` — one assignment each. Families are
22
+ containers, not units.
23
+
24
+ `writable` names the fields an agent may set, **per resource and per verb**:
25
+ `writable.finding.create` is what a write takes, `writable.finding.update` what an edit takes. It is
26
+ derived from the request schemas, so it cannot drift from what the API actually accepts — anything
27
+ outside it is stripped, and there is no reason to probe. `writable.disposition` is nearly empty **by
28
+ design**.
29
+
30
+ Facets may also carry three frontmatter declarations, all inherited by descendants — `dormant: true`
31
+ (retired: planned as a `skipped` assignment, never dispatched), `when: { paths: [...] }` (review only
32
+ when the change touches a matching file), and `effort: low|medium|high` (how deep to go). They arrive
33
+ on the assignment as `status`/`reason`, `applies`, and `effort`.
34
+
35
+ ## 2 · One facet's brief
36
+
37
+ ```call review.facet
38
+ { "owner": "$OWNER", "repo": "$REPO", "facetId": "correctness/R1_type-safety.md" }
39
+ ```
40
+
41
+ Returns the shared contract, the shared briefs, and **that facet's files only**. Hand this to the
42
+ subagent; hand it nothing else from this skill except `FACET_REVIEWER`.
43
+
44
+ ## 3 · Start a run
45
+
46
+ ```call review.startRun
47
+ { "owner": "$OWNER", "repo": "$REPO", "number": "$PR", "runKey": "$PR-$HEAD7-1", "headSha": "$HEAD" }
48
+ ```
49
+
50
+ - `runKey` is your idempotency key. **201** = created, **200** = resumed. Convention:
51
+ `<pr>-<head7>-<n>`. A retry with the same key continues the same run.
52
+ - `headSha` is optional but **supply it**: it pins every anchor to the commit you actually read. Omit
53
+ it and the server uses the PR's current head, which may already have moved.
54
+ - `previousRunId` links a re-review to the run it supersedes.
55
+ - `hasBriefing` says whether §4's shared briefing exists yet. Assignments arrive pre-`skipped`
56
+ where the taxonomy already answered the question (`dormant`, or a `when:` predicate the change
57
+ misses); each carries a `reason`, and you may dispatch one anyway.
58
+
59
+ ## 4 · The briefing — write it once, before dispatching
60
+
61
+ ```call review.putBriefing
62
+ { "id": "$RUN_ID", "bodyMd": "# What this PR does\n\n…" }
63
+ ```
64
+
65
+ ```call review.getBriefing
66
+ { "id": "$RUN_ID" }
67
+ ```
68
+
69
+ A **factual map** of the changed surface, not a findings list — see `ORCHESTRATOR` §4 for what goes in
70
+ it. Every facet agent reads it.
71
+
72
+ ## 5 · Assignment bookkeeping
73
+
74
+ Claim it, then close it out. `status` (did the work happen) and `outcome` (what it found) are
75
+ independent — a facet can be partly out of scope AND produce findings.
76
+
77
+ ```call review.updateAssignment
78
+ { "id": "$ASSIGNMENT_ID", "status": "running" }
79
+ ```
80
+
81
+ ```call review.updateAssignment
82
+ {
83
+ "id": "$ASSIGNMENT_ID",
84
+ "status": "complete",
85
+ "outcome": "findings",
86
+ "scopeMd": "Examined the new casts at the repo→service boundary. The frontend was out of scope."
87
+ }
88
+ ```
89
+
90
+ Reading one back — the update already returns it, this is for a resume:
91
+
92
+ ```call review.getAssignment
93
+ { "id": "$ASSIGNMENT_ID" }
94
+ ```
95
+
96
+ `status`: `pending` `running` `complete` `failed` `skipped` · `outcome`: `findings` `clean`
97
+ `not-applicable` · `reason`: required when `skipped` or `failed`.
98
+
99
+ Every write here answers with the stored row, so there is nothing to confirm with a second call.
100
+ `usageTokens` is the orchestrator's to record — the subagent cannot see its own total, and without it
101
+ there is no way to tell whether a change made the fleet cheaper.
102
+
103
+ ## 6 · The report
104
+
105
+ ```call review.putReport
106
+ { "id": "$ASSIGNMENT_ID", "bodyMd": "# Type safety\n\nExamined every `as` cast the PR adds.\n\n## Verified clean\n…" }
107
+ ```
108
+
109
+ One per assignment. It is full-text searchable, and a hit surfaces that facet's findings.
110
+
111
+ ## 7 · Findings — batched, one call per assignment
112
+
113
+ ```call review.writeFindings
114
+ {
115
+ "id": "$ASSIGNMENT_ID",
116
+ "findings": [
117
+ {
118
+ "slugIndex": 1,
119
+ "title": "customer external ids are read with no tenant-scope check on the customer",
120
+ "severity": "P1",
121
+ "reviewPriority": "R0",
122
+ "confidence": "high",
123
+ "blocking": true,
124
+ "audience": "reviewer+author",
125
+ "section": "blockers",
126
+ "path": "src/modules/customer-external-ids.service.ts",
127
+ "line": 50,
128
+ "side": "RIGHT",
129
+ "bodyFinding": "All four routes take `customerId` straight from the path and never resolve it in the caller's tenant scope.",
130
+ "bodyWhy": "Cross-tenant read of a customer's identity records, and cross-tenant write of them.",
131
+ "bodyFix": "Resolve the customer first and 404 when it is not visible."
132
+ }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ Response: `{ "ids": [...], "warnings": [...] }`.
138
+
139
+ - **`slugIndex`** is yours, sequential from 1, unique **within your assignment**. Several facet files
140
+ may declare the same slug; numbering is per assignment, not per slug. Renders as `TYPESAFETY-1`.
141
+ - **`section`** is the promotion signal — a finding carrying one gets an `F<N>` at synthesis and
142
+ appears in the summary. Keys come from `config.sections`. Omit it for the long tail; those stay
143
+ fully stored, searchable, and filterable, and can be promoted later.
144
+ - **`displayIndex` is rejected.** You cannot know your global number.
145
+ - **`bodyAuthor` is rejected too.** The author-facing body is composed from `title` + `bodyFinding` +
146
+ `bodyFix`, with a `Confidence: …` line — read it back on the finding detail, never write it.
147
+ - **Warnings, not errors.** `path-not-found` (that path isn't at the head — usually a typo),
148
+ `not-in-diff` (real code, but GitHub would reject an inline comment there), `path-too-large` (the
149
+ file is there and past the read cap, so the anchor is unproven). All three keep the finding. Fix a
150
+ `path-not-found`; the other two need nothing from you.
151
+ - **A batch without `replace` upserts on `slugIndex`** — re-send one finding to correct it, and its
152
+ siblings are untouched. `replace: true` makes the batch the **complete set** for the assignment,
153
+ deleting anything absent; use it only for a deliberate full rewrite.
154
+ - Multi-line: add `endLine` (+ `endSide`). One-click fix: `suggestionMd`.
155
+ - Merging across facets: set `mergedIntoSlugId` (e.g. `"TENANCY-2"`) on the contributor, at write
156
+ time or by edit at synthesis. It keeps its row and its scores. A slug id matching nothing in the
157
+ run is **rejected**, not dropped.
158
+ - `fixGroupId`: an arbitrary string shared by findings one fix closes. Group them — it is what turns
159
+ sixty findings into twenty decisions.
160
+
161
+ Editing one, including moving it — a changed `path`/`line` is re-anchored server-side:
162
+
163
+ ```call review.updateFinding
164
+ { "id": "$FINDING_ID", "bodyFix": "Resolve the customer through `ScopeFilter` first." }
165
+ ```
166
+
167
+ Dropping one:
168
+
169
+ ```call review.deleteFinding
170
+ { "id": "$FINDING_ID" }
171
+ ```
172
+
173
+ ## 8 · Synthesize, then complete
174
+
175
+ ```call review.updateRun
176
+ {
177
+ "id": "$RUN_ID",
178
+ "recommendationMd": "**Request changes** — one deploy-time break, and the new routes have no tenant authorization.",
179
+ "summaryMd": "Adds the customer billing tab…\n\n**Base:** `main`. **Scope:** 37 files.",
180
+ "reviewerNotesMd": "11 facet reviewers. Static analysis only; no browser session."
181
+ }
182
+ ```
183
+
184
+ ```call review.synthesize
185
+ { "id": "$RUN_ID" }
186
+ ```
187
+
188
+ ```call review.complete
189
+ { "id": "$RUN_ID" }
190
+ ```
191
+
192
+ Ordering is section → severity → taxonomy order → `slugIndex`. Pass `order` (a list of finding ids) to
193
+ override. Re-runnable; it clears and renumbers in one transaction.
194
+
195
+ ## 9 · Reading back
196
+
197
+ ```call review.getRun
198
+ { "id": "$RUN_ID" }
199
+ ```
200
+
201
+ The summary set, in `F<N>` order:
202
+
203
+ ```call review.listRunFindings
204
+ { "id": "$RUN_ID", "promoted": "true" }
205
+ ```
206
+
207
+ Filters: `assignment`, `facet`, `severity` (csv), `section`, `blocking`, `promoted`, `post`,
208
+ `followup`, `lifecycle`, `anchored`, `includeDismissed`, `onlyDismissed`, `q` (full text), `limit`,
209
+ `offset`.
210
+
211
+ ```call review.listRunFindings
212
+ { "id": "$RUN_ID", "blocking": "true", "severity": "P0,P1" }
213
+ ```
214
+
215
+ ```call review.getFinding
216
+ { "id": "$FINDING_ID" }
217
+ ```
218
+
219
+ `bodyAuthor` on that record is what the PR author will actually read — composed, read-only.
220
+
221
+ `rollup.derivedVerdict` describes **what is going to the author** — staged plus posted. Nothing is
222
+ staged until the human stages it, so during a run it reads `APPROVE` however many blockers you filed.
223
+ That is not the review's verdict; `rollup.blocking` is the count you want, and `recommendationMd` is
224
+ where you state the verdict.
225
+
226
+ `anchorState` is `active` | `moved` | `stale`; `anchorResolvedLine` is where it is **now**. After the
227
+ author pushes:
228
+
229
+ ```call review.refreshAnchors
230
+ { "id": "$RUN_ID" }
231
+ ```
232
+
233
+ → `{"head":"…","active":31,"moved":4,"stale":2}`.
234
+
235
+ ## 10 · What the human decided — read it, act on it
236
+
237
+ Three per-finding marks, set in the Review tab. **Dismissed and staged-for-posting are mutually
238
+ exclusive** (dismiss wins: dismissing un-stages, and staging a dismissed finding is refused).
239
+ **Staged-for-posting and wanting a ticket are also mutually exclusive, in both directions** — a
240
+ ticket is what you file when you are *not* asking the author to fix it here, so each mark displaces
241
+ the other. **A ticket is independent of a dismissal** — "not fixing it here, but track it" is the
242
+ common pair.
243
+
244
+ | field | values | means |
245
+ | --- | --- | --- |
246
+ | `dismissed` + `resolution` | `waived`, `fixed-verified`, `withdrawn-scope`, `duplicate`, `not-applicable` | Stopped pursuing it. All but `fixed-verified` suppress it on a later run. |
247
+ | `postState` | `held`, `staged`, `posted` | `staged` = going out with this review. `posted` = already in front of the author. |
248
+ | `followup` + `followupUrl` | `none`, `wanted`, `ticketed` | `wanted` = the human wants a ticket. |
249
+
250
+ Query them PR-scoped — you are given a PR, not a run id, and this resolves the latest run for you.
251
+
252
+ "Create the follow-up tickets" — note `includeDismissed`: a dismissed finding is the LIKELIEST one to
253
+ want a ticket, and without the flag it is invisible.
254
+
255
+ ```call review.listPrFindings
256
+ { "owner": "$OWNER", "repo": "$REPO", "number": "$PR", "followup": "wanted", "includeDismissed": "true" }
257
+ ```
258
+
259
+ What is going to the author with this review:
260
+
261
+ ```call review.listPrFindings
262
+ { "owner": "$OWNER", "repo": "$REPO", "number": "$PR", "post": "staged" }
263
+ ```
264
+
265
+ What was waived, and why — the argument you must not re-litigate next run:
266
+
267
+ ```call review.listPrFindings
268
+ { "owner": "$OWNER", "repo": "$REPO", "number": "$PR", "onlyDismissed": "true", "includeDismissed": "true" }
269
+ ```
270
+
271
+ An unparseable status (`post=stagd`) is **ignored**, not an error — you get the unfiltered list back,
272
+ so check what you got rather than trusting an empty result to mean "none".
273
+
274
+ ### Writing them — on instruction only
275
+
276
+ You can set every mark above. **The rule is not which mark, it is when: the human asks, or you do
277
+ not touch it.** They have all three controls in the Review tab and they use them while you work; a
278
+ mark you set because you formed your own view about a finding overwrites a decision they were in the
279
+ middle of making.
280
+
281
+ That cuts both ways. **A canned prompt that says to dismiss is an instruction** — "verify" and "already
282
+ handled?" both end with "dismiss it as `not-applicable` if it does not hold", and replying "it does
283
+ not hold" instead of dismissing it leaves the human to do by hand the thing they just asked for. Do
284
+ what the message says. What you must not do is dismiss a finding *nobody told you about* because you
285
+ decided it was wrong: say it is wrong, in your reply, and leave the resolution to them.
286
+
287
+ **Read the current dispositions before any bulk write** — see `ORCHESTRATOR` §7. Never overwrite one
288
+ you did not set.
289
+
290
+ Dismissing, when asked. `resolution` is required, and it decides whether the dismissal sticks on a
291
+ later run (everything but `fixed-verified` suppresses the finding next time):
292
+
293
+ ```call review.dispositions
294
+ { "id": "$RUN_ID", "findingIds": ["$FINDING_ID"], "dismissed": true, "resolution": "not-applicable", "resolutionNote": "`ScopeFilter` already applies the tenant predicate two frames down." }
295
+ ```
296
+
297
+ Staging what goes to the author, when asked. Staging composes the draft comment for you — you never
298
+ write one by hand. A reviewer-only finding needs `audienceOverrideReason`, which is logged rather
299
+ than refused:
300
+
301
+ ```call review.dispositions
302
+ { "id": "$RUN_ID", "findingIds": ["$FINDING_ID"], "post": true }
303
+ ```
304
+
305
+ Marking one for a follow-up ticket, when asked:
306
+
307
+ ```call review.dispositions
308
+ { "id": "$RUN_ID", "findingIds": ["$FINDING_ID"], "followup": "wanted" }
309
+ ```
310
+
311
+ The other two are reports on work you have already done, and need no instruction beyond the work
312
+ itself. After you file a ticket for a `wanted` finding, record where it landed:
313
+
314
+ ```call review.dispositions
315
+ { "id": "$RUN_ID", "findingIds": ["$FINDING_ID"], "followup": "ticketed", "followupUrl": "https://linear.app/…" }
316
+ ```
317
+
318
+ After you put a finding in front of the author yourself — a `gh` comment, a message, anything that is
319
+ not buhtig's own publish — record that too, so it stops reading as still-to-send:
320
+
321
+ ```call review.dispositions
322
+ { "id": "$RUN_ID", "findingIds": ["$FINDING_ID"], "posted": true, "githubUrl": "https://github.com/…#discussion_r1" }
323
+ ```
324
+
325
+ `posted` is a **statement of fact, not a request to send** — nothing is sent by this call, and it is
326
+ the only one of the three marks that displaces the others: it un-stages the finding and clears a
327
+ `wanted` follow-up (a `ticketed` one survives — that ticket exists). Sending it together with
328
+ `"post":true` or `"followup":"wanted"` is a 400 rather than a silent winner. `"posted":false` reverts
329
+ to `held` and drops the URL, for a mark you set in error.
330
+
331
+ **Never mark `posted` for something you did not actually send.** It is the one mark that makes the
332
+ review lie: it tells the human the author has already seen a finding, and they will stop trying to
333
+ send it.
334
+
335
+ The exclusivity rules are enforced, not advisory. Dismissed and staged cannot both hold (dismissing
336
+ un-stages; staging a dismissed finding is refused). Staged and `wanted` displace each other in both
337
+ directions — a ticket is what you file when you are *not* asking the author to fix it here. A ticket
338
+ and a dismissal are independent, and "not fixing it here, but track it" is the common pair. A call
339
+ that asks for two exclusive marks at once is a 400 rather than a silent winner, and the response's
340
+ `refused` list names anything the store declined.
341
+
342
+ ## 10.5 · Filing the tickets the human composed
343
+
344
+ The human composes follow-up tickets in the Outbox and marks the ones they want filed `ready`. buhtig
345
+ holds no Linear or Jira credentials; you do.
346
+
347
+ ```call ticket.list
348
+ { "status": "ready" }
349
+ ```
350
+
351
+ `status` also takes `draft` | `filing` | `filed` | `abandoned`.
352
+
353
+ The body is already composed — code permalinks, review deep links, the impact argument:
354
+
355
+ ```call ticket.get
356
+ { "id": "$TICKET_ID" }
357
+ ```
358
+
359
+ File it, then report back. This flips **every** member finding to `followup: ticketed` with the URL:
360
+
361
+ ```call ticket.update
362
+ { "id": "$TICKET_ID", "status": "filed", "url": "https://linear.app/…", "externalKey": "WEB-1235" }
363
+ ```
364
+
365
+ - **Do not re-compose the body.** It was built from the findings and may have been edited by the
366
+ human; `bodyEdited: true` means those words are theirs.
367
+ - **`parentId`** is the epic → sub-ticket hierarchy. File parents first, and set the child's parent in
368
+ the tracker to match.
369
+ - **Marking `filed` is the only write that closes the loop.** Setting `followup: ticketed` on the
370
+ findings by hand instead leaves the ticket row reading `ready` forever.
371
+ - **The rest stays the human's**: never mark a ticket `ready`, never compose one unasked. You may
372
+ create one **when asked to**:
373
+
374
+ ```call ticket.create
375
+ { "owner": "$OWNER", "repo": "$REPO", "number": "$PR", "by": "agent", "title": "…", "bodyMd": "…" }
376
+ ```
377
+
378
+ ## 10.6 · Provenance
379
+
380
+ Tracing what asked for a PR is a separate job with its own brief — see `PROVENANCE`. It is **not**
381
+ review input: no reviewer reads it, no finding cites it, and you never assess whether the PR satisfies
382
+ the ticket.
383
+
384
+ ## 11 · What the human is telling you — answer it
385
+
386
+ From the Review tab the human can send a one-line message attached to a finding and the field they are
387
+ looking at. It is the correction loop for a review that is already written.
388
+
389
+ A **subject** is `<scope>:<key>` — `review-run:<id>`, or `pr:<owner>/<repo>/<number>` for a message about
390
+ the pull request, which no review run has to exist for. `sub` is repeatable, and one connection covering
391
+ every subject you owe is the point: one per subject is how a surface nobody told you about goes
392
+ unanswered indefinitely.
393
+
394
+ You do not learn about a message by asking on a timer. If buhtig launched you, the briefing at
395
+ `$BUHTIG_AGENT_CONTEXT` carries the one command that watches the channel — `buhtig agent listen`, with
396
+ your subjects and your identity already in it. Run that, exactly as written, in the shape the briefing
397
+ gives it: a persistent monitor if it stays connected, one bounded run per idle window if it does not.
398
+
399
+ What you still owe, asked for directly — `pending` spans `open` **and** `claimed`, and asking for
400
+ `status=open` alone silently skips anything a crashed predecessor had claimed:
401
+
402
+ ```call messages.list
403
+ { "sub": ["review-run:$RUN_ID"], "pending": "true" }
404
+ ```
405
+
406
+ ```call messages.claim
407
+ { "id": "$MESSAGE_ID" }
408
+ ```
409
+
410
+ ```call messages.reply
411
+ { "id": "$MESSAGE_ID", "text": "rewrote the fix around ScopeFilter" }
412
+ ```
413
+
414
+ | field | values | means |
415
+ | --- | --- | --- |
416
+ | `topic` | `title` `bodyFinding` `bodyWhy` `bodyFix` | that field of that finding |
417
+ | | `finding` | the whole finding — re-scope it, re-score it, delete it |
418
+ | | `run` | the review — re-check a facet, re-dispatch, re-triage |
419
+ | | `summary` | the run prose — answer with `review.updateRun`, not with a reply |
420
+ | `status` | `open` `claimed` `answered` | `open` = nobody has picked it up |
421
+
422
+ `targetId` is the finding the message is about, and `targetKind` says what it names. `run` and
423
+ `summary` carry none — they are about the run itself.
424
+
425
+ {{http}}
426
+ When buhtig launched you, `$BUHTIG_STREAM` is the channel with your subjects and identity already in
427
+ it, and `$BUHTIG_AGENT_CONTEXT` is a briefing that says all of this — including the exact listen
428
+ command to run against it.
429
+ {{/http}}
430
+
431
+ ### The canned prompts
432
+
433
+ Most messages arrive as one of a fixed set of chips, not as free text, and the wording is always sent
434
+ verbatim. Several of them **grant a disposition**: "verify" and "already handled?" both say to dismiss
435
+ as `not-applicable` when the finding does not hold, and you should do that rather than replying that
436
+ it does not hold. The run-level ones call for work across the whole run:
437
+
438
+ | Chip | What it wants |
439
+ |---|---|
440
+ | `group fixes` | a shared `fixGroupId` on findings one change closes — what makes the Outbox seed real tickets |
441
+ | `dedupe` | dismiss repeats as `duplicate`, keeping the best of each set |
442
+ | `verify low-confidence` | re-check each, raise the confidence or dismiss it |
443
+ | `re-anchor` | re-locate `stale`/`moved` anchors against head |
444
+
445
+ The `summary` ones are all rewrites of `recommendationMd` / `summaryMd` through `review.updateRun`:
446
+
447
+ | Chip | What it wants |
448
+ |---|---|
449
+ | `distill` | the same verdict, shorter — cut the hedging, keep the specifics |
450
+ | `sync` | re-read the findings as now triaged and make the prose describe that review |
451
+ | `filter` | drop what is not actionable, and do not restate the findings — an overview, not the list |
452
+
453
+ Reply with what you changed, not with an argument.
454
+
455
+ How to work them — claim first, oldest first, the edit is the answer, and never turn a message into a
456
+ disposition (§10) — is in `ORCHESTRATOR` §8.
457
+
458
+ ## 12 · Reading the code, without a shell
459
+
460
+ The diff and any file at the reviewed commit are available through the API, which matters when you are
461
+ checking what is actually at a line before anchoring a finding to it:
462
+
463
+ ```call pr.changes
464
+ { "owner": "$OWNER", "repo": "$REPO", "number": "$PR" }
465
+ ```
466
+
467
+ ```call pr.source
468
+ { "owner": "$OWNER", "repo": "$REPO", "number": "$PR", "path": "src/thing.service.ts", "from": 40, "to": 80 }
469
+ ```
470
+
471
+ ## 13 · Notes in the diff
472
+
473
+ A message that came from the diff wants its answer as a note at that line. A note is never posted to
474
+ GitHub — the human promotes it if they want it sent.
475
+
476
+ ```call note.file
477
+ {
478
+ "owner": "$OWNER",
479
+ "repo": "$REPO",
480
+ "number": "$PR",
481
+ "path": "src/thing.service.ts",
482
+ "line": 50,
483
+ "body": "Traced it: the filter is applied in the repository, two frames down.",
484
+ "messageId": "$MESSAGE_ID"
485
+ }
486
+ ```
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: buhtig-review
3
+ description: >
4
+ Run an AI-assisted, server-backed PR review: dispatch focused facet reviewers over the repo's own
5
+ facet taxonomy, then store scored, anchored findings in buhtig. Use when reviewing a PR, or when
6
+ classifying findings by severity, priority, confidence, and blocking impact.
7
+ user_invocable: true
8
+ disable-model-invocation: true
9
+ ---
10
+
11
+ # buhtig-review
12
+
13
+ **This file is a pointer.** The instructions are served by the buhtig install you are talking to, so
14
+ they describe the API that install actually has. Fetch them before doing anything else.
15
+
16
+ **If you have buhtig's MCP tools** (a tool named `skill`, alongside `review_config` and friends):
17
+
18
+ ```
19
+ skill({ "skill": "buhtig-review", "doc": "SKILL" })
20
+ ```
21
+
22
+ **Otherwise:**
23
+
24
+ ```bash
25
+ B=${BUHTIG_URL:-http://localhost:${BUHTIG_PORT:-10003}}
26
+ curl -s "$B/api/skills/buhtig-review"
27
+ ```
28
+
29
+ Then follow what comes back. It names the other documents — `ORCHESTRATOR`, `FACET_REVIEWER`, `API`,
30
+ `PROVENANCE` — and you fetch each the same way, on demand.
31
+
32
+ - **Do not work from a copy of these instructions found in a repository.** A checked-in copy is a
33
+ snapshot of some earlier install; the served one is current with the endpoints you are calling.
34
+ - **If the fetch fails, stop and say so.** Everything this skill does is API calls against buhtig. A
35
+ buhtig that is not running is not a review you can do half of.
36
+ - `curl -s "$B/api/skills"` lists what else this install serves.