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
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2026 veracioux. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This source code is publicly visible, but no license is granted to use, copy,
|
|
4
|
+
modify, merge, publish, distribute, sublicense, or sell it.
|
|
5
|
+
|
|
6
|
+
The project is UNLICENSED pending selection of a license. Viewing the source and
|
|
7
|
+
submitting feedback through the project's GitHub repository are permitted. Any
|
|
8
|
+
other use requires prior written permission from the copyright holder.
|
package/README.md
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
# buhtig
|
|
2
|
+
|
|
3
|
+
**A local-first, keyboard-driven pull-request review cockpit that's faster than github.com.**
|
|
4
|
+
|
|
5
|
+
`buhtig` is `github` spelled backwards — because it flips the model. Instead of round-tripping
|
|
6
|
+
every diff and click through the GitHub web app, buhtig runs as a single local process: it computes
|
|
7
|
+
diffs straight from local git worktrees, caches aggressively by commit SHA, talks to the GitHub API
|
|
8
|
+
only when it must, and puts every action a keystroke away. The result is a review surface that opens
|
|
9
|
+
instantly, never burns your rate limit on a re-scroll, and is built end-to-end for keyboard-first
|
|
10
|
+
review ergonomics.
|
|
11
|
+
|
|
12
|
+
> Review PRs at the speed of `git`, with the comfort of a real editor and the muscle memory of vim.
|
|
13
|
+
|
|
14
|
+
> **Public-source preview:** buhtig is currently `UNLICENSED`. The source is visible for evaluation
|
|
15
|
+
> and feedback, but no open-source license has been granted yet. See [`LICENSE`](LICENSE).
|
|
16
|
+
|
|
17
|
+
## Install the preview
|
|
18
|
+
|
|
19
|
+
The npm release is being prepared but has not been published yet. Install from a checkout:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/veracioux/buhtig.git
|
|
23
|
+
cd buhtig
|
|
24
|
+
pnpm install --frozen-lockfile
|
|
25
|
+
pnpm package:build
|
|
26
|
+
pnpm link --global
|
|
27
|
+
|
|
28
|
+
gh auth login
|
|
29
|
+
buhtig setup
|
|
30
|
+
buhtig doctor
|
|
31
|
+
buhtig serve
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Open <http://127.0.0.1:10003>. Node 22+, pnpm 11+, `git`, and a GitHub token source are required.
|
|
35
|
+
`gh auth login` is the default and recommended token source.
|
|
36
|
+
|
|
37
|
+
For a persistent install on Linux or macOS:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
buhtig service install
|
|
41
|
+
buhtig service status
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The server binds to loopback by default because its local API has no authentication. Exposing it on
|
|
45
|
+
another interface requires both an explicit address and `--allow-remote`; put an authenticating TLS
|
|
46
|
+
proxy in front of it before doing that.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Why buhtig?
|
|
51
|
+
|
|
52
|
+
| github.com | | **buhtig** |
|
|
53
|
+
|-------------------------|------------------------|----------------------------------------------------------------------------------------------------|
|
|
54
|
+
| API round-trip per view | Diff source | **Local git worktree**, cached by SHA (immutable) |
|
|
55
|
+
| Full reload | PR list | **Virtualized + stale-while-revalidate** (renders from cache, revalidates in the background) |
|
|
56
|
+
| Mouse-first | Navigation | **Keyboard-first**, one central shortcut registry + `?` help |
|
|
57
|
+
| Lives on GitHub | Drafting review notes | **Local change-requests** (a real resource agents can write via an API), published on your command |
|
|
58
|
+
| — | Come-back-to-this pins | **Private bookmarks** at PR / file / line scope, cross-PR, that survive force-pushes |
|
|
59
|
+
| — | Open in your editor | **Open any file (at the changed line) or the whole PR in `$VISUAL`** from a checked-out worktree |
|
|
60
|
+
| New token | Auth | **Reuses your `gh` login** — no new secret to store |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Highlights
|
|
65
|
+
|
|
66
|
+
### ⚡ A PR list that's already loaded
|
|
67
|
+
- **Virtualized** list that stays smooth at thousands of PRs.
|
|
68
|
+
- **Stale-while-revalidate**: the last-seen list is seeded from `localStorage`, so a (re)visit paints
|
|
69
|
+
rows immediately while a background refresh reconciles with GitHub.
|
|
70
|
+
- **At-a-glance status per row**: CI rollup, mergeable signals (conflicts ⚠ / behind-base ↧ /
|
|
71
|
+
required-checks-failing ✗), review state, author, and an inline assignee picker.
|
|
72
|
+
- **Powerful filtering & search** with keyboard shortcuts: draft/ready, assigned-to-me,
|
|
73
|
+
authored-by-me, a "care about" lens, review state, and a **triage queue**. Search understands
|
|
74
|
+
`#number`, a full PR URL, `TICKET-123` ticket keys, `@author`, or plain fuzzy text.
|
|
75
|
+
|
|
76
|
+
### 🔬 A Changes tab built for actually reading code
|
|
77
|
+
- **Diffs computed locally** from the PR worktree — the primary rate-limit mitigation — with
|
|
78
|
+
**syntax highlighting** (Prism One Dark) and **word-level intra-line** edit markers.
|
|
79
|
+
- **Split / unified** toggle, and a **per-file context-lines** control to widen or tighten the
|
|
80
|
+
window around each hunk.
|
|
81
|
+
- **Viewed tracking that understands staleness**: each file's *Viewed* checkbox is keyed on its git
|
|
82
|
+
blob OID, so when the PR head advances the file is marked **stale** (not silently unchecked) — you
|
|
83
|
+
always know you reviewed an older version.
|
|
84
|
+
- **Collapse/expand per file** (viewed files auto-collapse), **collapse-all**, a live **viewed
|
|
85
|
+
progress** counter, **fuzzy or regex file filter**, **status filter** (added/changed/deleted), and
|
|
86
|
+
**custom tri-state regex filters** (off → *only* → *hide*) seeded with test/module presets and
|
|
87
|
+
persisted per repo.
|
|
88
|
+
- **Focus mode** narrows the page to a single file so the browser's native find-in-page is scoped to
|
|
89
|
+
just that diff.
|
|
90
|
+
- **Rich in-diff navigation**: jump file-to-file, hop to the next addition / removal / change /
|
|
91
|
+
comment / unviewed file via a which-key menu, and cycle through every inline comment or bookmark.
|
|
92
|
+
|
|
93
|
+
### 📝 Review without leaving the keyboard
|
|
94
|
+
- **Inline comments** authored by clicking the diff gutter land in a **local change-request** — a
|
|
95
|
+
draft you can edit, retype, delete, reclassify (plain comment ↔ change-request), or **send now**.
|
|
96
|
+
- **Publish** the whole batch as a single GitHub review with a verdict (**Approve / Request changes /
|
|
97
|
+
Comment**), or one-click **Approve** from the header (guarded against approving a stale head).
|
|
98
|
+
- **Every comment surface has the same powers.** A comment is the same object whether you meet it
|
|
99
|
+
inline in the diff, in a Conversation thread, or in the timeline, so all three render one shared
|
|
100
|
+
card: **edit**, **delete** (always behind a confirmation — GitHub has no undelete), **deal to
|
|
101
|
+
deck**, and for a review thread also **resolve/reopen**, **reply**, and its **replies inline**
|
|
102
|
+
(expanded, each editable in its own right). Edit/delete follow GitHub's own `viewerCanUpdate` /
|
|
103
|
+
`viewerCanDelete`, so buhtig doesn't offer an action the API would refuse.
|
|
104
|
+
- **Close** a PR from the header behind a confirmation that doubles as the place to explain it: an
|
|
105
|
+
optional comment (the same deck-backed composer used everywhere else) is posted *before* the close,
|
|
106
|
+
and a closed PR's button becomes **Re-open**, so undoing one doesn't mean a trip to GitHub.
|
|
107
|
+
- **Conversation tab** hoists the top blocking items (change requests + unresolved threads) into a
|
|
108
|
+
sticky rail so you can clear the path to merge fast; reply to threads, resolve/unresolve, post PR
|
|
109
|
+
comments, and edit the description — all with a **side-by-side markdown editor**.
|
|
110
|
+
- **Open in `$VISUAL`** per file (at the changed line) or for the whole PR worktree, plus a
|
|
111
|
+
**worktree** affordance — collapsed to one chip in the header, expanding on hover, and reachable as
|
|
112
|
+
`Alt+W` → `t`/`a`/`s` — for a **terminal**, a **coding agent**, or an explicit **worktree setup**
|
|
113
|
+
whose recorded state expires when the PR, the setup script, or the tree moves on.
|
|
114
|
+
|
|
115
|
+
### 🔖 Private bookmarks that survive rebases
|
|
116
|
+
- Pin a **PR, a file, or a single line** — cross-PR, repo-global, local-only.
|
|
117
|
+
- One keystroke to pin/unpin; an optional one-line note added lazily.
|
|
118
|
+
- **Never auto-deleted**: each pin stores a content fingerprint so on reload it resolves to
|
|
119
|
+
`active`, `moved` (re-anchored, shown with a ↕), or `stale` (greyed, with its captured snippet so
|
|
120
|
+
it stays recognisable and re-pinnable).
|
|
121
|
+
- A **bookmarks hub overlay** lists everything grouped by scope; jump with Enter, plus a
|
|
122
|
+
bookmarked-only filter on the PR list for triage.
|
|
123
|
+
|
|
124
|
+
### ⌨️ Keyboard-first, discoverable everywhere
|
|
125
|
+
- One **central shortcut registry** — the `?` **help overlay** falls out of it for free.
|
|
126
|
+
- **Every binding is yours to change.** The keyboard settings page lists every action buhtig has;
|
|
127
|
+
click one and press the key you want (Backspace unbinds it). Overrides are stored **server-side**,
|
|
128
|
+
so they follow the install rather than the browser, and can live in `config.yml` instead.
|
|
129
|
+
- **Custom actions**: give a name and a shell command, pick a key, and it appears in the `Alt+W`
|
|
130
|
+
menu — or on a shortcut of its own. It runs in the PR worktree, with `$BUHTIG_PR` and friends
|
|
131
|
+
already set.
|
|
132
|
+
- Every primary action is **also reachable by mouse**, and right-click **context menus** label each
|
|
133
|
+
entry with its keybind, so the menus double as a shortcut-discovery surface.
|
|
134
|
+
- Shortcuts are suppressed while you're typing in a field, with careful handling of `+`, `Shift`,
|
|
135
|
+
and modifier combos.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Keyboard shortcuts (a taste)
|
|
140
|
+
|
|
141
|
+
Press **`?`** anywhere for the full, always-current list.
|
|
142
|
+
|
|
143
|
+
| Keys | Scope | Action |
|
|
144
|
+
|-----------------------------------------|---------------|-------------------------------------------------------------------------------------------------------|
|
|
145
|
+
| `?` · `b` | **Global** | Toggle help · open the bookmarks hub |
|
|
146
|
+
| `j`/`k` · `Enter` · `Alt+Enter` | **PR list** | Move cursor · open · open in new tab |
|
|
147
|
+
| `d` · `a` · `s` · `c` · `u` · `t` · `m` | | Cycle draft · assigned-to-me · authored-by-me · care-about · review filter · triage · bookmarked-only |
|
|
148
|
+
| `Ctrl+K` | | Focus search (`#num`, URL, `TICKET-123`, `@author`, fuzzy) |
|
|
149
|
+
| `[` / `]` · `u` | **PR detail** | Previous / next tab · back to list |
|
|
150
|
+
| `Alt+W` → `t`/`a`/`s` | | PR worktree menu: terminal · agent · run worktree setup |
|
|
151
|
+
| `j`/`k` · `Space` · `v` · `e` | **Changes** | Next/prev file · expand/collapse · toggle viewed · open in `$VISUAL` |
|
|
152
|
+
| `c` · `s` · `+`/`-` · `.`/`f` | | Collapse/expand all · cycle status filter · more/less context · focus file |
|
|
153
|
+
| `n` → `a`/`r`/`c`/`m`/`u` | | "Jump to next…" menu (addition / removal / change / comment / unviewed) |
|
|
154
|
+
| `{` / `}` · `'` | | Previous / next inline comment · next bookmark in diff |
|
|
155
|
+
|
|
156
|
+
Every row above is a **default**, not a fact: rebind any of it under Settings → Keyboard, or in
|
|
157
|
+
`config.yml`:
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
keymap:
|
|
161
|
+
changes.next-file: [n] # rebind
|
|
162
|
+
global.help: [] # unbind entirely
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Your own actions
|
|
166
|
+
|
|
167
|
+
A custom action is a name and a command. It shows up in the `Alt+W` menu (or on its own shortcut),
|
|
168
|
+
runs in the PR worktree, and gets its context from the environment — nothing is spliced into the
|
|
169
|
+
command string, so nothing needs quoting:
|
|
170
|
+
|
|
171
|
+
| variable | what it holds |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `BUHTIG_PR` | the pull request number |
|
|
174
|
+
| `BUHTIG_REPO` | `owner/repo` |
|
|
175
|
+
| `BUHTIG_WORKTREE` | absolute path to the PR worktree |
|
|
176
|
+
| `BUHTIG_URL` | this install's API base URL |
|
|
177
|
+
|
|
178
|
+
The same variables reach every other command buhtig runs for you — the `postCheckoutWorktree` setup
|
|
179
|
+
hook and the launched coding agent included.
|
|
180
|
+
|
|
181
|
+
Add one from `Alt+W` → `+`, or from either settings page. In a file:
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
customActions:
|
|
185
|
+
- slug: run-tests
|
|
186
|
+
label: Run tests
|
|
187
|
+
placement: pr.worktree # or `global` for a shortcut of its own
|
|
188
|
+
key: r
|
|
189
|
+
command: pnpm test
|
|
190
|
+
cwd: worktree # worktree | repo-clone | home
|
|
191
|
+
run: terminal # or `background`: awaited, result in a toast
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Put it under a repo's entry (or in its `.git/buhtig.yaml`) to scope it to that repository.
|
|
195
|
+
|
|
196
|
+
Edit one from the menu it lives in — `Shift` + its key, or the ✎ on hover — or from either settings
|
|
197
|
+
page. **An edit goes back to wherever the action was declared**: a file-declared action is rewritten
|
|
198
|
+
in its own file, comments and untouched fields left alone, so the file you keep in the repo stays the
|
|
199
|
+
thing that decides. Saving it as this install's override instead is a separate, named button.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Architecture
|
|
204
|
+
|
|
205
|
+
A single local Node process serves both the API and the built web app — not multi-tenant, no cloud
|
|
206
|
+
component.
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
210
|
+
│ packages/web React 19 + Vite + Tailwind + Radix │
|
|
211
|
+
│ TanStack Query · react-diff-view · Shiki │
|
|
212
|
+
│ (dev: Vite proxies to Hono; prod: Hono │
|
|
213
|
+
│ serves the built static assets) │
|
|
214
|
+
├─────────────────────────────────────────────────────────────┤
|
|
215
|
+
│ packages/server Hono · @hono/zod-openapi │
|
|
216
|
+
│ ├─ GitHub: Octokit (REST + ETag/304 for the list, │
|
|
217
|
+
│ │ GraphQL for batch enrichment); token from │
|
|
218
|
+
│ │ `gh auth token` │
|
|
219
|
+
│ ├─ git: shells out to system `git`, serialized per repo │
|
|
220
|
+
│ │ — diffs from worktrees, not the API │
|
|
221
|
+
│ └─ state: SQLite (Drizzle) for change-requests + viewed │
|
|
222
|
+
├─────────────────────────────────────────────────────────────┤
|
|
223
|
+
│ packages/shared zod schemas + types = the one contract │
|
|
224
|
+
│ (also published as an OpenAPI spec) │
|
|
225
|
+
└─────────────────────────────────────────────────────────────┘
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Three distinct cache layers, never conflated:**
|
|
229
|
+
1. **GitHub API cache** — ETag-keyed, TTL'd; `304`s are free, protecting your rate limit.
|
|
230
|
+
2. **Diff / blob cache** — keyed on commit SHA, immutable, lives on the filesystem (XDG dirs).
|
|
231
|
+
3. **User-state store** — Viewed/stale flags, local change-requests, drafts — in SQLite. (Ephemeral
|
|
232
|
+
UI prefs like the selected repo live in `localStorage` via a typed accessor.)
|
|
233
|
+
|
|
234
|
+
**Agent-facing API.** Change-requests are a first-class resource: an agent can create inline review
|
|
235
|
+
notes through the OpenAPI-described API (`/api/openapi.json`) and a separate call publishes them to
|
|
236
|
+
GitHub — the exact same drafts a human authors in the Changes tab.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Getting started
|
|
241
|
+
|
|
242
|
+
**Requirements:** Node ≥ 22, pnpm ≥ 11, system `git`, and a source of a GitHub token — by default
|
|
243
|
+
the GitHub CLI (`gh`) authenticated with `gh auth login`. See [Auth](#auth) for the alternatives.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# install from source
|
|
247
|
+
pnpm install --frozen-lockfile
|
|
248
|
+
|
|
249
|
+
# run in dev (web + server, hot reload)
|
|
250
|
+
pnpm dev
|
|
251
|
+
|
|
252
|
+
# or build + start the production server
|
|
253
|
+
pnpm build
|
|
254
|
+
pnpm start
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The server listens on **port 10003** by default (override with `BUHTIG_PORT`). Open the web app,
|
|
258
|
+
pick a repo you have access to, and start reviewing.
|
|
259
|
+
|
|
260
|
+
`buhtig doctor` reports whether this machine is set up — Node, `git`, the config file, and each of
|
|
261
|
+
the three separable auth questions (is the token source reachable, does it yield a token, does
|
|
262
|
+
GitHub accept it). `buhtig doctor --json` is the thing to paste into a bug report.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Running the server
|
|
267
|
+
|
|
268
|
+
One `--listen` address in every form, rather than a port flag and a host flag that can disagree:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
buhtig serve # 127.0.0.1:10003
|
|
272
|
+
buhtig serve --listen 10004 --allow-remote # a port on every interface
|
|
273
|
+
buhtig serve --listen 127.0.0.1:10004 # loopback only
|
|
274
|
+
buhtig serve --listen '[::1]:10004' # IPv6 — brackets required
|
|
275
|
+
buhtig serve --listen http://0.0.0.0:80 --allow-remote # URL form, same thing
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`BUHTIG_LISTEN` is the same string. `BUHTIG_PORT` still works and still means a port; where both are
|
|
279
|
+
set, `BUHTIG_LISTEN` wins.
|
|
280
|
+
|
|
281
|
+
Two things are refused by name rather than left to fail later. **`https://`** — buhtig does not
|
|
282
|
+
terminate TLS, and accepting it would listen in plaintext on an address someone believes is
|
|
283
|
+
encrypted. **Unix sockets** — a socket has no address to hand the agents buhtig launches, and every
|
|
284
|
+
one of them is told to `curl` the API; terminating in a proxy and forwarding to loopback keeps that
|
|
285
|
+
answer well-defined. For either, put a proxy in front and point it at a port.
|
|
286
|
+
|
|
287
|
+
### In the background
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
buhtig service install --listen 127.0.0.1:10004
|
|
291
|
+
buhtig service status # --json for a bug report
|
|
292
|
+
buhtig service logs -f
|
|
293
|
+
buhtig service restart | stop | start
|
|
294
|
+
buhtig service uninstall
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**systemd** user unit on Linux, **launchd** LaunchAgent on macOS — each platform's own convention.
|
|
298
|
+
(Not `brew services`, which generates a plist from a Homebrew formula and would leave two supervisors
|
|
299
|
+
for one process; not `SMAppService`, which needs a signed `.app` bundle.)
|
|
300
|
+
|
|
301
|
+
The unit bakes in an absolute `PATH` covering node, `git`, and your token command. This is the
|
|
302
|
+
difference between a service that works and one that boots and fails every request: launchd hands an
|
|
303
|
+
agent `/usr/bin:/bin:/usr/sbin:/sbin`, which contains no Homebrew prefix and no version-manager
|
|
304
|
+
shims. No secret is ever written to a unit — the token is resolved live at every start.
|
|
305
|
+
|
|
306
|
+
`status` decides on a **health probe**, not on what the supervisor claims: launchd calls an agent
|
|
307
|
+
loaded while the process inside it crash-loops. The supervisor's own view and the restart count are
|
|
308
|
+
reported next to it, and a disagreement is called out.
|
|
309
|
+
|
|
310
|
+
Two platform differences the CLI states rather than hides:
|
|
311
|
+
|
|
312
|
+
- **Linux** — a user service dies when your last session ends unless you `loginctl enable-linger`.
|
|
313
|
+
`status` says so when lingering is off.
|
|
314
|
+
- **macOS** — a LaunchAgent stops at logout and there is no per-user equivalent of lingering (the
|
|
315
|
+
system-domain answer is a LaunchDaemon, which runs as root — wrong for a tool authenticating as
|
|
316
|
+
you). It also appears under System Settings → Login Items, where switching it off stops it.
|
|
317
|
+
|
|
318
|
+
Installing refuses to overwrite a `buhtig.service` this CLI did not write — `install.sh` creates one
|
|
319
|
+
— unless you pass `--force`. Set `BUHTIG_SERVICE_UNIT` to run a second instance under its own name.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Auth
|
|
324
|
+
|
|
325
|
+
buhtig never stores a token. Every source is a pointer at a credential you already own, resolved
|
|
326
|
+
fresh at startup, in this order:
|
|
327
|
+
|
|
328
|
+
1. **`GH_TOKEN`** in the environment (`GITHUB_TOKEN` is also read, but don't use that name — under
|
|
329
|
+
GitHub Actions it holds the ephemeral workflow token).
|
|
330
|
+
2. **`auth.tokenCommand`** — any command whose stdout is a token. Defaults to `gh auth token`.
|
|
331
|
+
|
|
332
|
+
```yaml
|
|
333
|
+
auth:
|
|
334
|
+
tokenCommand: [op, read, "op://Private/GitHub/token"] # or: pass show github/token
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`buhtig setup auth` walks through the choice and verifies the result against GitHub.
|
|
338
|
+
|
|
339
|
+
> **Running as a background service?** Prefer a `tokenCommand` over `GH_TOKEN`. A launchd agent or a
|
|
340
|
+
> systemd user unit reads no shell rc, so an exported variable never reaches it — and it gets a
|
|
341
|
+
> four-entry `PATH` with no Homebrew prefix and no version-manager shims, so store the **absolute**
|
|
342
|
+
> path to the binary. Missing the token is fatal at boot, and the error lands in a log file rather
|
|
343
|
+
> than your terminal.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Configuration
|
|
348
|
+
|
|
349
|
+
Config lives at `~/.config/buhtig/config.yml` (XDG-aware) and is keyed by `org/repo`:
|
|
350
|
+
|
|
351
|
+
```yaml
|
|
352
|
+
acme/webapp:
|
|
353
|
+
# Where PR worktrees are checked out. Placeholders:
|
|
354
|
+
# %n = PR number %t = (cleaned) PR title %a = PR author
|
|
355
|
+
worktreePathTemplate: /home/me/work/webapp-pr%n_%t
|
|
356
|
+
|
|
357
|
+
# Link ticket references in PR and commit titles, and expand references entered on stacks.
|
|
358
|
+
ticketing:
|
|
359
|
+
provider: linear
|
|
360
|
+
urlTemplate: https://linear.app/acme/issue/{ticket}
|
|
361
|
+
patterns:
|
|
362
|
+
- '(?<ticket>ENG-\d+)'
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Ticket regexes use the named `ticket` group when present and the full match otherwise. Settings →
|
|
366
|
+
Ticket links provides Linear, Jira, GitHub Issues and Azure Boards presets, a Custom option, and a
|
|
367
|
+
live title preview. An exact URL can always be stored on a stack entry. Pull request numbers are
|
|
368
|
+
linked regardless of the separately gated bare-issue linking feature.
|
|
369
|
+
|
|
370
|
+
Per repo you can also configure a **local clone path** in Settings, so buhtig adds PR worktrees as
|
|
371
|
+
siblings of your existing checkout (sharing its object store), and override the worktree template
|
|
372
|
+
from the UI. Open-in-editor uses `$VISUAL`.
|
|
373
|
+
|
|
374
|
+
### The coding agent
|
|
375
|
+
|
|
376
|
+
Which agent the worktree actions launch is configured, never guessed:
|
|
377
|
+
|
|
378
|
+
```yaml
|
|
379
|
+
agent:
|
|
380
|
+
default: claude # claude | codex | opencode
|
|
381
|
+
commands:
|
|
382
|
+
codex: [codex, --search]
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
A repo can override the *choice* (`agent: codex` in its own entry, or in `.git/buhtig.yaml`); the
|
|
386
|
+
*commands* stay global, because which binary and flags `codex` means is a fact about your machine.
|
|
387
|
+
Until something chooses, the Alt+W → Agent action reads "configure…" and takes you to the settings
|
|
388
|
+
page rather than launching whatever buhtig happened to be written against.
|
|
389
|
+
|
|
390
|
+
A launched agent is **briefed on the way in**: it is told that you can send it a one-line instruction
|
|
391
|
+
from the UI, and given one exact command to watch for one — `buhtig agent listen`, pointed at
|
|
392
|
+
`$BUHTIG_STREAM` and chosen for the agent being launched (Claude Code sets it up as a persistent
|
|
393
|
+
monitor that never returns and wakes it on each message; Codex and opencode run it for one bounded
|
|
394
|
+
window at a time). Where the CLI supports it that briefing goes in as an extra system prompt
|
|
395
|
+
(`claude --append-system-prompt`) so it costs no user turn; everywhere else it rides in
|
|
396
|
+
`$BUHTIG_AGENT_CONTEXT`, which the buhtig skills read.
|
|
397
|
+
|
|
398
|
+
### Settings, and where a value comes from
|
|
399
|
+
|
|
400
|
+
Every option lives in up to three layers, and the settings pages show all of them with the one in
|
|
401
|
+
force marked: buhtig's own store (this install), the repo's `.git/buhtig.yaml` (travels with the
|
|
402
|
+
clone), and `config.yml`. You pick which layer a save lands in. Writes to a YAML file keep its
|
|
403
|
+
comments and key order.
|
|
404
|
+
|
|
405
|
+
`/settings` holds the install-wide settings; each repo's gear holds its own and links there.
|
|
406
|
+
|
|
407
|
+
### Structured review
|
|
408
|
+
|
|
409
|
+
An agent-authored review is a **resource**, not a directory of markdown: a run over a PR at a head
|
|
410
|
+
SHA, carrying one report per facet and scored, line-anchored findings.
|
|
411
|
+
|
|
412
|
+
The facet taxonomy is **per-repo configuration**, resolved from a tree of facet markdown files the
|
|
413
|
+
review skill already reads — so one project has a `TENANCY` facet and another has none, and adding a
|
|
414
|
+
facet is a markdown file rather than a code change.
|
|
415
|
+
|
|
416
|
+
The taxonomy lives in `.git/buhtig/review-facets/` — inside the git directory rather than the tree,
|
|
417
|
+
so no branch, merge or pull request can rewrite the rules of its own review, and every linked
|
|
418
|
+
worktree sees one taxonomy. There is nothing to configure to place it.
|
|
419
|
+
|
|
420
|
+
```yaml
|
|
421
|
+
acme/webapp:
|
|
422
|
+
review:
|
|
423
|
+
# REQUIRED. How many subagents a review of this repo is worth; the orchestrator picks inside the
|
|
424
|
+
# band by change size and packs the facets into that many bundles. There is no default: a repo
|
|
425
|
+
# with no budget refuses to start a run, because the cost of a review has to be a decision
|
|
426
|
+
# somebody made rather than a consequence of how many facet files the taxonomy contains.
|
|
427
|
+
subagentBudget: { min: 3, max: 5 }
|
|
428
|
+
|
|
429
|
+
contract: REVIEW_CONTRACT.md # relative to the taxonomy root
|
|
430
|
+
briefs: [../skills/business-rules/SKILL.md]
|
|
431
|
+
|
|
432
|
+
# The scales ship WITH their definitions — buhtig owns what `P0` means, which is what keeps a
|
|
433
|
+
# repo from having to restate the vocabulary in a reviewer contract of its own. Omit them
|
|
434
|
+
# entirely for P0–P4 and R0–R3; list bare keys to drop or reorder levels while keeping buhtig's
|
|
435
|
+
# prose; write a level out to give it this repo's own risks.
|
|
436
|
+
severities:
|
|
437
|
+
- P0
|
|
438
|
+
- { key: P1, title: Should block, hint: broken team scoping, unhandled failure path }
|
|
439
|
+
# sections defaults to the standard summary set.
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Everything above is editable in the UI at `/<owner>/<repo>/review/settings`, alongside the facet
|
|
443
|
+
tree itself.
|
|
444
|
+
|
|
445
|
+
Discovery follows the taxonomy's own convention: a directory with an `index.md` is part of the tree,
|
|
446
|
+
an `R<n>_` prefix sets priority, an **unprefixed** file inside a facet directory is a sub-facet leaf
|
|
447
|
+
inheriting its parent's priority and slug, and `slug` is a *namespace* several files may share — the
|
|
448
|
+
identity of a unit of review work is its **file path**.
|
|
449
|
+
|
|
450
|
+
Reviews are driven by the `buhtig-review` skill, which buhtig **serves** rather than ships into your
|
|
451
|
+
repositories — `curl localhost:10003/api/skills/buhtig-review`. What you install is a ~25-line
|
|
452
|
+
bootstrap that knows how to fetch the rest, once, at user level:
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
./scripts/install-agent-integration.sh # also run by install.sh
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
That script registers buhtig's **MCP server** with your agent CLI as well, and the two work together:
|
|
459
|
+
an agent holding the MCP tools reads the instructions rendered as tool calls, an agent with only a
|
|
460
|
+
shell reads the same instructions rendered as `curl`, and neither is shown the other's version. The
|
|
461
|
+
tool set is generated from one declared catalogue (`AGENT_CALLS` in `@buhtig/shared`) with schemas
|
|
462
|
+
derived from the OpenAPI document. It is curated: there is no tool to publish a review, merge or
|
|
463
|
+
close a PR, or write settings. Dismissing a finding and staging one for posting *are* there, because
|
|
464
|
+
you will want to ask an agent to do them — they carry a destructive hint so your CLI stops and asks,
|
|
465
|
+
and the instructions are blunt that those marks answer your instruction and never the agent's own
|
|
466
|
+
opinion of a finding.
|
|
467
|
+
|
|
468
|
+
The division of labour is deliberate: the agent supplies prose and `(path, line)`; buhtig derives anchor fingerprints,
|
|
469
|
+
staleness, whether a line is postable to GitHub, and `F<N>` numbering; **the human** dismisses, posts,
|
|
470
|
+
and files follow-ups. An agent cannot post its own findings.
|
|
471
|
+
|
|
472
|
+
Findings anchor durably — resolved against the whole file at the run's head, not the diff, so a
|
|
473
|
+
finding on code the PR never touched stays `active` — and a later push re-resolves them to
|
|
474
|
+
`active` / `moved` / `stale`. Nothing is ever auto-deleted.
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
pnpm server # rebuilds @buhtig/shared, then boots on $BUHTIG_PORT (10003)
|
|
478
|
+
curl -s localhost:10003/api/repos/acme/webapp/review/config | jq '{state, facets: (.facets|length)}'
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
> A bare `tsx packages/server/src/index.ts` resolves `@buhtig/shared` to its **build output**. If
|
|
482
|
+
> `dist/` is stale you get `does not provide an export named …` at import. `pnpm server` and
|
|
483
|
+
> `pnpm dev` both handle this; a raw `tsx` invocation does not.
|
|
484
|
+
|
|
485
|
+
### Repo-local config & hooks
|
|
486
|
+
|
|
487
|
+
A repo can carry its own config at `.git/buhtig.yaml` (unkeyed — it's already repo-scoped). Today it
|
|
488
|
+
supports lifecycle **hooks**:
|
|
489
|
+
|
|
490
|
+
```yaml
|
|
491
|
+
hooks:
|
|
492
|
+
# The PR worktree's setup command, run by the "Setup" button on the PR detail page. The command
|
|
493
|
+
# path is resolved relative to this file (i.e. the repo's .git dir), runs in the worktree's CWD,
|
|
494
|
+
# and gets a BUHTIG_PR env var with the PR number. A non-zero exit surfaces as an error.
|
|
495
|
+
postCheckoutWorktree: ./setup-worktree.sh
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
For example, `.git/setup-worktree.sh` might `gh pr checkout "$BUHTIG_PR"`, install deps, and
|
|
499
|
+
generate a Prisma client so the worktree is ready to work in.
|
|
500
|
+
|
|
501
|
+
Setup is **explicit**: checking a worktree out (to open a terminal, an agent, or a file in
|
|
502
|
+
`$VISUAL`) never runs it. The button carries the worktree's setup state, and that state **decays** —
|
|
503
|
+
buhtig records which worktree was set up, at which head, with which command, and when, then reports
|
|
504
|
+
it as stale once the PR gets new commits, the setup command changes, the worktree is removed, or the
|
|
505
|
+
record is over a day old (a setup script's real inputs — a registry, a lockfile, a database — live
|
|
506
|
+
outside anything buhtig can watch, so an old "ready" is a guess, and says so).
|
|
507
|
+
|
|
508
|
+
Secrets are never written to config or the database — the GitHub token is always sourced live from
|
|
509
|
+
`gh`.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Testing
|
|
514
|
+
|
|
515
|
+
The testing seam fakes **GitHub's egress only** — never buhtig's own API or `git`.
|
|
516
|
+
|
|
517
|
+
- **Unit / integration (Vitest):** a fake `GitHubClient` (selected with `BUHTIG_GH_MODE=fake`) backs
|
|
518
|
+
hand-authored fixtures, while `git` runs for real against ephemeral tmp repos. Backend tests read
|
|
519
|
+
as user scenarios (nested `describe`/`it` + expressive builders); the ephemeral repo is the single
|
|
520
|
+
source of truth for SHAs, so headline flows like "viewed → stale on advance" stay honest.
|
|
521
|
+
- **End-to-end (Playwright):** a real backend booted against an isolated sandbox (tmp XDG dirs,
|
|
522
|
+
fixture `config.yml`, ephemeral git repos, fake `GitHubClient`).
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
pnpm test # unit + integration (vitest)
|
|
526
|
+
pnpm test:watch
|
|
527
|
+
pnpm test:e2e # playwright
|
|
528
|
+
pnpm test:e2e:ui # playwright UI mode
|
|
529
|
+
pnpm typecheck
|
|
530
|
+
pnpm lint # biome
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Project layout
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
packages/
|
|
539
|
+
server/ Hono API, git/worktree plumbing, GitHub client, SQLite state
|
|
540
|
+
(src/skills/ — the buhtig-review skill it serves to agents)
|
|
541
|
+
web/ React app: PR list, PR detail (Conversation + Changes), overlays
|
|
542
|
+
shared/ zod schemas & types — the single source-of-truth contract
|
|
543
|
+
sdk/ Generated OpenAPI client
|
|
544
|
+
github/ Octokit client, ETag cache, REST→domain mapping
|
|
545
|
+
mobile/ Capacitor wrapper (Android)
|
|
546
|
+
e2e/ Playwright specs + fixtures
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
<sub>buhtig is a local single-user tool. It reuses your `gh` credentials and scopes, stores no
|
|
552
|
+
tokens, and talks to GitHub on your behalf only when an action requires it.</sub>
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "buhtig",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Local-first, keyboard-driven pull-request review cockpit",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"buhtig": "./packages/cli/dist/index.js",
|
|
9
|
+
"buhtig-mcp": "./packages/cli/dist/mcp.js"
|
|
10
|
+
},
|
|
11
|
+
"files": ["LICENSE", "README.md", "packages/cli/dist"],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/veracioux/buhtig.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/veracioux/buhtig#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/veracioux/buhtig/issues"
|
|
19
|
+
},
|
|
20
|
+
"author": "veracioux",
|
|
21
|
+
"keywords": ["github", "pull-request", "code-review", "git", "cli", "local-first"],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"packageManager": "pnpm@11.3.0",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22",
|
|
28
|
+
"pnpm": ">=11"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "turbo run build",
|
|
32
|
+
"package:build": "BUHTIG_RELEASE=1 pnpm build && node scripts/build-package.mjs",
|
|
33
|
+
"package:check": "node scripts/check-package.mjs",
|
|
34
|
+
"prepack": "pnpm package:build && pnpm package:check",
|
|
35
|
+
"dev": "turbo run dev",
|
|
36
|
+
"format": "biome format --write .",
|
|
37
|
+
"lint": "biome check .",
|
|
38
|
+
"mobile:sync": "pnpm --filter @buhtig/mobile sync:android",
|
|
39
|
+
"mobile:apk:debug": "pnpm --filter @buhtig/mobile apk:debug",
|
|
40
|
+
"mobile:apk:release": "pnpm --filter @buhtig/mobile apk:release",
|
|
41
|
+
"server": "pnpm --filter @buhtig/shared build && pnpm --filter @buhtig/github build && BUHTIG_PORT=${BUHTIG_PORT:-10003} tsx packages/server/src/index.ts",
|
|
42
|
+
"start": "pnpm --filter @buhtig/cli build && pnpm --filter @buhtig/server start",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:e2e": "playwright test",
|
|
45
|
+
"test:e2e:debug": "playwright test --debug",
|
|
46
|
+
"test:e2e:report": "playwright show-report",
|
|
47
|
+
"test:e2e:ui": "playwright test --ui",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"better-sqlite3": "^11.8.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "^1.9.4",
|
|
56
|
+
"@playwright/test": "^1.50.1",
|
|
57
|
+
"@types/node": "^22.13.1",
|
|
58
|
+
"esbuild": "^0.25.0",
|
|
59
|
+
"tsx": "^4.19.2",
|
|
60
|
+
"turbo": "^2.9.18",
|
|
61
|
+
"typescript": "^5.7.3",
|
|
62
|
+
"vitest": "^4.1.0"
|
|
63
|
+
}
|
|
64
|
+
}
|