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.
- package/LICENSE +8 -0
- package/README.md +552 -0
- package/package.json +64 -0
- package/packages/cli/dist/docs/buhtig-review/API.md +486 -0
- package/packages/cli/dist/docs/buhtig-review/BOOTSTRAP.md +36 -0
- package/packages/cli/dist/docs/buhtig-review/FACET_REVIEWER.md +205 -0
- package/packages/cli/dist/docs/buhtig-review/ORCHESTRATOR.md +263 -0
- package/packages/cli/dist/docs/buhtig-review/PROVENANCE.md +119 -0
- package/packages/cli/dist/docs/buhtig-review/SKILL.md +124 -0
- package/packages/cli/dist/index.js +60291 -0
- package/packages/cli/dist/mcp.js +29384 -0
- package/packages/cli/dist/openapi.json +19973 -0
- package/packages/cli/dist/web/assets/index-BVmXlvZB.css +1 -0
- package/packages/cli/dist/web/assets/index-DgFiAR5v.js +890 -0
- package/packages/cli/dist/web/assets/web-BSvOHUC-.js +1 -0
- package/packages/cli/dist/web/assets/web-Bq29swYs.js +1 -0
- package/packages/cli/dist/web/index.html +13 -0
|
@@ -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.
|