Picks up code reviews exported from Neovim (review.nvim) into .review/inbox and hands them to Claude as MR-style review comments

This flake uses the dendritic pattern via Den: flake-parts + import-tree auto-discover every .nix file under modules/, and each file declares one den.aspects.<name> covering both the NixOS and Home Manager sides of a single concern.
Per-host values (e.g. paths) are typed options declared on the host schema via den.schema.host.imports (see modules/programs/games/battlenet.nix), set on the host as den.hosts.<system>.<host>.<option>, and read in an aspect through its host argument.
Every host (NixOS or not) is an age recipient in .sops.yaml - after either install path below, import the GPG key and register the host's age key per docs/sops.md, then rebuild once secrets can decrypt.
docs/iso-wifi.md for wireless bring-up inside installation ISO.modules/hosts/<host>/_hardware-configuration.nix nixos-install --flake github:faku99/nix#<host> `` nix run home-manager -- switch --flake github:faku99/nix#<home> ` Afterwards, home-manager switch --flake .#<home>` from a local clone.hooks/register.ts 89 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// Neovim drops exported reviews here, under the git root.
4const INBOX = '.review/inbox'
5const DONE = '.review/done'
6const POLL_MS = 1500
7
8// How Claude should treat the review. Questions get answers, not code changes:
9// that's the whole point of reviewing "why" the code is the way it is.
10const PREAMBLE = `Here is my code review of the current changes, in the format of MR review comments.
11Handle each comment in order:
12- If it is a question ("why…?", "is this needed?"), answer it with your reasoning. Do NOT change code for a question unless your answer concludes the code is wrong — then say so and propose the fix.
13- If it is a change request, make the change.
14- If you disagree with a request, say why instead of silently complying.
15Reply comment by comment, quoting the file:line each answer refers to. Finish with a short list of what you changed.
16
17`
18
19// Set while a review is between "picked up" and "turn started", so the timer
20// never submits the same review twice or two at once.
21let inFlight = false
22
23// Git root of the session's working directory, so a subdirectory start still works.
24async function reviewRoot($: EngineInterface): Promise<string> {
25 const top = await $.process.run(['git', 'rev-parse', '--show-toplevel'])
26 const root = top.exitCode === 0 ? top.stdout.trim() : ''
27 return root === '' ? '' : `${root}/`
28}
29
30// Pick up the oldest review in the inbox, if any. Returns what happened, for /nvim-review.
31async function pickUp($: EngineInterface): Promise<string> {
32 if (inFlight) return 'A review is already queued for Claude.'
33 const root = await reviewRoot($)
34 const inbox = root + INBOX
35 const done = root + DONE
36 if (!(await $.fs.exists(inbox))) return `No review waiting (nothing in ${inbox}).`
37
38 const entries = await $.fs.list(inbox)
39 const pending = entries
40 .filter(f => f.kind === 'file' && f.name.endsWith('.md'))
41 .sort((a, b) => a.mtimeMs - b.mtimeMs)
42 if (pending.length === 0) return 'No review waiting (nothing in .review/inbox).'
43
44 // Claim it by moving it out of the inbox first. If another Claude Code session
45 // in the same repo got there before us, mv fails and that session handles it.
46 const name = pending[0].name
47 await $.fs.write(`${done}/.keep`, '')
48 const moved = await $.process.run(['mv', `${inbox}/${name}`, `${done}/${name}`])
49 if (moved.exitCode !== 0) return 'Another session took that review.'
50
51 const markdown = await $.fs.read(`${done}/${name}`)
52 if (markdown.trim() === '') return 'The waiting review was empty; skipped.'
53
54 inFlight = true
55 $.ui.status(`review ${name} queued`)
56 $.ui.toast(`Review received from Neovim (${name})`)
57 // Waits for the session to be idle, then starts the turn. Not awaited, so a
58 // review exported while Claude is busy simply queues behind the current turn.
59 void $.prompt
60 .submit({ text: PREAMBLE + markdown, asUser: true })
61 .finally(() => {
62 inFlight = false
63 $.ui.status(undefined)
64 })
65 return `Sent review ${name} to Claude.`
66}
67
68export const register: Register = on => {
69 on('session.start', async ($, e, next) => {
70 // Watch the inbox: a review exported from Neovim reaches Claude with no typing.
71 $.clock.every(POLL_MS, async () => {
72 await pickUp($)
73 })
74 try {
75 await $.command.register({
76 name: 'nvim-review',
77 description: 'Send the next review exported from Neovim to Claude now',
78 })
79 } catch {
80 // A name clash only costs the manual command; the watcher still runs.
81 }
82 return next(e)
83 })
84
85 on('command.run', { command: 'nvim-review' }, async $ => {
86 return { text: await pickUp($) }
87 })
88}
89