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 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
+ }