agent-feedbacker 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "feedbacker",
3
+ "owner": {
4
+ "name": "Pavel Polivka"
5
+ },
6
+ "metadata": {
7
+ "description": "Feedbacker: review a coding agent's changes across several git repos in a local MR-style page."
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "feedbacker",
12
+ "source": "./",
13
+ "description": "Review an agent's changes across several git repos in a local MR-style page and send line comments back to it."
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "feedbacker",
3
+ "version": "0.2.0",
4
+ "description": "Review an agent's changes across several git repos in a local MR-style page and send line comments back to it.",
5
+ "author": { "name": "Pavel Polivka" },
6
+ "repository": "https://github.com/PavlikPolivka/feedbacker",
7
+ "license": "MIT"
8
+ }
package/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "feedbacker": {
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs", "mcp"]
6
+ }
7
+ }
8
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pavel Polivka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,170 @@
1
- # Temporary Holding Version
1
+ # Feedbacker
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A local, merge-request-style review page for coding agents. The agent passes the git repos it changed, a summary and optionally some questions. You see the diffs of **all of them on one page**, comment on lines, answer the questions, and click **Approve** or **Request changes**. The agent gets your feedback back as markdown it can act on.
4
+
5
+ - **Multi-repo and worktrees**: any repos anywhere on disk, each with its own base; `--worktrees` pulls in every git worktree of a repo, so parallel agents' work is reviewed together.
6
+ - **Simple**: one Node process with no runtime dependencies; nothing to install or build to use it. The page is a small React 19 app whose bundle (with highlight.js) is committed in `src/dist/`.
7
+ - **Local**: binds to `127.0.0.1` with a random token in the URL. Nothing leaves your machine, and git is only read.
8
+ - **Three ways in**: a Claude Code plugin (MCP tool + skill), a plain MCP server for other agents, and a CLI for anything with a shell.
9
+
10
+ ![Review page](docs/screenshot.png)
11
+
12
+ ## Install
13
+
14
+ ### Claude Code plugin
15
+
16
+ ```bash
17
+ claude plugin marketplace add PavlikPolivka/feedbacker
18
+ claude plugin install feedbacker@feedbacker
19
+ ```
20
+
21
+ This gives Claude:
22
+
23
+ - the MCP tool `request_review` (it shows up as `mcp__plugin_feedbacker_feedbacker__request_review`)
24
+ - the skill `/feedbacker:review`, which tells Claude when and how to ask for a review
25
+ - hooks for the **session baseline**: each repo's state is recorded when the session first touches it (at session start for the current repo, and before the first edit in any other repo). When Claude asks for a review without a base, the review shows exactly what changed in this session, even if Claude committed along the way, and repos Claude changed but forgot to pass are added. Uncommitted work from before the session is left out, because the baseline is a snapshot of it (`git stash create`, which changes neither your working tree nor your stash list). New untracked files from before the session still show up.
26
+
27
+ Then just say "let me review it" or run `/feedbacker:review`. To try it from a checkout without installing: `claude --plugin-dir /path/to/feedbacker`.
28
+
29
+ ### Any MCP client
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "feedbacker": { "command": "npx", "args": ["-y", "agent-feedbacker", "mcp"] }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### CLI
40
+
41
+ ```bash
42
+ npm install -g agent-feedbacker # or run it once with: npx agent-feedbacker
43
+ ```
44
+
45
+ The npm package is `agent-feedbacker`; the command is `feedbacker`. Node 22 or newer; git on `PATH`. From a checkout, `node bin/feedbacker.mjs` works too.
46
+
47
+ ## Use
48
+
49
+ ```bash
50
+ feedbacker ~/Projects/api ~/Projects/web@main \
51
+ -m "Bind the API to localhost; document the port" \
52
+ -q "Should the port also be a CLI flag?"
53
+ ```
54
+
55
+ - `repo` diffs the working tree (staged, unstaged and untracked files) against `HEAD`.
56
+ - `repo@base` diffs against any commit, branch or tag, so work the agent already committed is included. With a branch, the diff starts where `HEAD` forked from it (like a merge request), so newer commits on that branch don't show up reversed.
57
+ - A worktree path works like any repo. `-w` / `--worktrees` adds every worktree of each repo (main and linked, each labelled with its branch), all against the same base:
58
+
59
+ ```bash
60
+ feedbacker ~/Projects/api@main --worktrees # main checkout + every agent's worktree
61
+ ```
62
+ - The command blocks until you submit, then prints the feedback (`--json` for JSON) and exits 0. Exit 2 means there was nothing to review.
63
+ - `feedbacker list` prints the URLs of reviews waiting for you, for when the browser didn't open (MCP clients usually hide a server's output).
64
+ - `--no-open` only prints the URL; `--port` fixes the port.
65
+
66
+ In the browser:
67
+
68
+ - In changed lines, the changed words are marked.
69
+ - Tick **Viewed** on a file when you're done with it: it collapses, dims in the file tree, and the top bar counts how many you've viewed.
70
+ - Click a line number to comment on that line (a highlighter "+" shows where); shift-click a second one to comment on a range. Ctrl+Enter saves, Esc cancels.
71
+ - With several repos, the picker in the top bar switches between all of them and a single one (the summary, questions and your comments in other repos stay). The top bar also shows the size of the change and keeps **Approve** and **Request changes** in reach; files can be collapsed, and the file tree shows where your comments are.
72
+ - Comments, answers and the general comment are kept in the browser's local storage until you submit, so a refresh loses nothing.
73
+ - **Approve** or **Request changes** sends everything to the agent and closes the server.
74
+
75
+ ## MCP tool
76
+
77
+ `request_review`:
78
+
79
+ | Argument | Type | |
80
+ |---|---|---|
81
+ | `repos` | `[{ path, base?, worktrees? }]` | Required. Absolute paths; `base` defaults to `HEAD` (or, with the plugin's hooks, where the repo stood when the session started); `worktrees: true` adds all worktrees of that repo. (`exact` and `session` are set by the hook.) |
82
+ | `summary` | string | Shown at the top of the page. |
83
+ | `questions` | string[] | Shown with an answer box each. |
84
+
85
+ It blocks until the user submits (Claude Code keeps long MCP calls open; it sends progress notifications every 30 s for clients that need them) and returns markdown like:
86
+
87
+ ````markdown
88
+ # Review feedback
89
+
90
+ Verdict: **changes requested**
91
+
92
+ Reviewed:
93
+
94
+ - /home/me/Projects/api (feature/config): 2 files changed (src/server.ts, src/config.ts), changes since the session started
95
+
96
+ ## General
97
+
98
+ Almost there.
99
+
100
+ ## Answers
101
+
102
+ **Q: Should the port also be a CLI flag?**
103
+
104
+ No, env is enough.
105
+
106
+ ## Comments
107
+
108
+ ### /home/me/Projects/api
109
+
110
+ **src/server.ts:6** (new side)
111
+
112
+ ```diff
113
+ + server.listen(port, "127.0.0.1");
114
+ ```
115
+
116
+ Make the host configurable too.
117
+ ````
118
+
119
+ The result starts with what was reviewed (each repo, its files and base), so the agent knows what the verdict covers. Each comment carries the code it refers to, so line numbers drifting doesn't lose the context. Cancelling the tool call closes the review page.
120
+
121
+ ## How it works
122
+
123
+ ```
124
+ agent ──request_review / CLI──▶ feedbacker ──git diff per repo──▶ review in memory
125
+ feedbacker ──opens──▶ http://127.0.0.1:PORT/?t=TOKEN ──you submit──▶ feedbacker
126
+ feedbacker ──markdown (tool result / stdout)──▶ agent
127
+ ```
128
+
129
+ | File | |
130
+ |---|---|
131
+ | `src/core.mjs` | Reads diffs (`git diff <base>` plus untracked files), parses them, serves the page, formats feedback. |
132
+ | `src/page.html` | Page shell: styles, and the bundle from `src/dist/`. |
133
+ | `src/ui/` | The review page in React 19 (JSX): diff table, inline comments, questions, submit; highlight.js for syntax, `styles.css` for the look (light and dark, colour tokens at the top). |
134
+ | `src/dist/` | The committed bundle (JS, CSS and fonts), built from `src/ui/` by `build.mjs` (esbuild). |
135
+ | `src/mcp.mjs` | Minimal MCP server over stdio (JSON-RPC), one tool. |
136
+ | `src/baseline.mjs`, `hooks/hooks.json` | Session baseline: `feedbacker hook` records repos (SessionStart, PreToolUse on edits) and fills in bases for `request_review` (PreToolUse, via `updatedInput`). Stored in `$XDG_STATE_HOME/feedbacker/sessions/`, forgotten after 30 days. |
137
+ | `bin/feedbacker.mjs` | CLI: review, `list`, `mcp`, `hook`. |
138
+ | `skills/review/SKILL.md`, `.mcp.json`, `.claude-plugin/` | The Claude Code plugin and its marketplace entry. |
139
+
140
+ Open reviews write a small file (URL, repos, pid) to `$XDG_STATE_HOME/feedbacker/` (default `~/.local/state/feedbacker/`, mode 0600) and remove it when they finish; `list` reads them.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ pnpm install # dev dependencies only: React, highlight.js, fonts, esbuild
146
+ pnpm build # after changing src/ui: rebuilds src/dist (commit it)
147
+ pnpm test
148
+ ```
149
+
150
+ Using Feedbacker needs none of this; only working on the page does. A test fails when `src/dist` doesn't match `src/ui`.
151
+
152
+ The tests use real temporary git repos. They cover diff parsing, the HTTP server, the MCP protocol over stdio, and an end-to-end run that drives the page in headless Chromium (skipped when no Chromium is found; set `CHROMIUM=/path` to pick one).
153
+
154
+ CI runs the tests on Node 22 and 24 for every push and pull request (`.github/workflows/ci.yml`).
155
+
156
+ ### Releasing
157
+
158
+ Publishing to npm is done by GitHub Actions (`.github/workflows/publish.yml`) with npm trusted publishing, so no npm token is stored anywhere and each version gets a provenance statement.
159
+
160
+ 1. Bump `version` in `package.json` and `.claude-plugin/plugin.json` (the MCP server reads it from `package.json`), commit, push.
161
+ 2. Create a GitHub release with the tag `vX.Y.Z` matching that version: `gh release create v0.2.0 --generate-notes`.
162
+ 3. The workflow checks the tag, runs the tests and publishes.
163
+
164
+ One-time setup: on npmjs.com, add a trusted publisher for the package (GitHub Actions, repository `PavlikPolivka/feedbacker`, workflow `publish.yml`). npm only offers that once the package exists, so the very first version is published by hand with `npm publish --access public`.
165
+
166
+ Plans: [ROADMAP.md](ROADMAP.md).
167
+
168
+ ## Licence
169
+
170
+ MIT. The bundle in `src/dist/` includes React, ReactDOM and scheduler (MIT), highlight.js (BSD-3-Clause), and the Atkinson Hyperlegible Next and JetBrains Mono fonts (OFL-1.1); their licences are in `src/dist/THIRD_PARTY_LICENSES.txt`. The fonts are served locally, so the page never loads anything from the internet.
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ // Feedbacker CLI: `feedbacker [repos...]` opens a review page and prints the
3
+ // feedback when the user submits; `feedbacker mcp` runs the MCP server.
4
+
5
+ import { buildReview, hasChanges, listReviews, startReview, toMarkdown } from '../src/core.mjs';
6
+ import { runMcp } from '../src/mcp.mjs';
7
+ import { runHook } from '../src/baseline.mjs';
8
+
9
+ const USAGE = `Usage: feedbacker [options] [repo[@base] ...]
10
+ feedbacker list
11
+ feedbacker mcp
12
+ feedbacker hook
13
+
14
+ Opens a review page for the changes in each repo (default: current directory)
15
+ and prints the feedback to stdout when the user submits.
16
+
17
+ repo[@base] Repo or worktree path; diff the working tree (including
18
+ untracked files) against base. Default base: HEAD. With a
19
+ branch as base, the diff starts where HEAD forked from it.
20
+ -w, --worktrees Also review every other worktree of each repo.
21
+ -m, --summary TXT What the agent changed, shown at the top.
22
+ -q, --question TXT A question for the user (repeatable).
23
+ --json Print JSON instead of markdown.
24
+ --port N Port to listen on (default: random).
25
+ --no-open Don't open the browser; only print the URL to stderr.
26
+ -h, --help Show this help.
27
+
28
+ list Show the URLs of reviews that are waiting for you.
29
+ mcp Run as an MCP server on stdio (tool: request_review).
30
+ hook Claude Code hook (reads the event on stdin): records each
31
+ repo's state when a session first touches it, and fills
32
+ that in as the base of request_review.`;
33
+
34
+ function fail(msg) {
35
+ console.error(`feedbacker: ${msg}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ function parseArgs(argv) {
40
+ const opts = { repos: [], questions: [], summary: '', worktrees: false, json: false, port: 0, open: true };
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ const next = () => {
44
+ if (i + 1 >= argv.length) fail(`${a} needs a value`);
45
+ return argv[++i];
46
+ };
47
+ if (a === '-h' || a === '--help') { console.log(USAGE); process.exit(0); }
48
+ else if (a === '-m' || a === '--summary') opts.summary = next();
49
+ else if (a === '-q' || a === '--question') opts.questions.push(next());
50
+ else if (a === '-w' || a === '--worktrees') opts.worktrees = true;
51
+ else if (a === '--json') opts.json = true;
52
+ else if (a === '--port') opts.port = Number(next());
53
+ else if (a === '--no-open') opts.open = false;
54
+ else if (a.startsWith('-')) fail(`unknown option ${a}\n\n${USAGE}`);
55
+ else opts.repos.push(a);
56
+ }
57
+ if (!opts.repos.length) opts.repos.push('.');
58
+ return opts;
59
+ }
60
+
61
+ // Exit cleanly on signals so open reviews remove their state files.
62
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => process.exit(130));
63
+
64
+ const argv = process.argv.slice(2);
65
+ if (argv[0] === 'mcp') {
66
+ runMcp();
67
+ } else if (argv[0] === 'hook') {
68
+ await runHook();
69
+ } else if (argv[0] === 'list') {
70
+ const reviews = listReviews();
71
+ if (!reviews.length) console.log('No reviews waiting.');
72
+ for (const r of reviews) console.log(`${r.url}\n ${r.started} ${r.repos.join(', ')}${r.summary ? `\n ${r.summary}` : ''}`);
73
+ } else {
74
+ const opts = parseArgs(argv);
75
+ const review = buildReview(opts);
76
+ for (const r of review.repos) if (r.error) console.error(`feedbacker: ${r.path}: ${r.error}`);
77
+ if (!hasChanges(review) && !review.questions.length) {
78
+ console.error('feedbacker: no changes to review');
79
+ process.exit(2);
80
+ }
81
+ const { url, feedback } = await startReview(review, { port: opts.port, open: opts.open });
82
+ console.error(`feedbacker: waiting for review at ${url}`);
83
+ const fb = await feedback;
84
+ process.stdout.write(opts.json ? JSON.stringify(fb, null, 2) + '\n' : toMarkdown(review, fb));
85
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ { "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs\" hook", "timeout": 10 }] }
5
+ ],
6
+ "PreToolUse": [
7
+ {
8
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__plugin_feedbacker_feedbacker__request_review",
9
+ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs\" hook", "timeout": 10 }]
10
+ }
11
+ ]
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "agent-feedbacker",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "A local merge-request-style review page that coding agents open to collect your line comments and answers, across several git repos.",
5
+ "type": "module",
6
+ "bin": {
7
+ "feedbacker": "bin/feedbacker.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src/*.mjs",
12
+ "src/page.html",
13
+ "src/dist",
14
+ "hooks",
15
+ "skills",
16
+ ".claude-plugin",
17
+ ".mcp.json"
18
+ ],
19
+ "scripts": {
20
+ "build": "node build.mjs",
21
+ "test": "node --test test/*.test.mjs"
22
+ },
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/PavlikPolivka/feedbacker.git"
29
+ },
30
+ "keywords": [
31
+ "claude-code",
32
+ "mcp",
33
+ "code-review",
34
+ "diff",
35
+ "agent"
36
+ ],
37
+ "author": "Pavel Polivka",
38
+ "license": "MIT",
39
+ "devDependencies": {
40
+ "@fontsource-variable/atkinson-hyperlegible-next": "^5.3.0",
41
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
42
+ "esbuild": "^0.28.2",
43
+ "highlight.js": "^11.12.0",
44
+ "react": "^19.3.1",
45
+ "react-dom": "^19.3.1"
46
+ },
47
+ "pnpm": {
48
+ "onlyBuiltDependencies": [
49
+ "esbuild"
50
+ ]
51
+ },
52
+ "homepage": "https://github.com/PavlikPolivka/feedbacker#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/PavlikPolivka/feedbacker/issues"
55
+ }
56
+ }
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: review
3
+ description: Ask the user to review your changes in a local merge-request-style page and get their line comments, answers and verdict back. Use when the user asks for a review ("let me review", "show me the diff", "feedbacker"), or after finishing a non-trivial change, especially one that spans several git repos, before you commit, push or call the work done.
4
+ ---
5
+
6
+ # Review with Feedbacker
7
+
8
+ Feedbacker opens a review page in the user's browser with the diffs of the repos you pass. The user comments on lines, answers your questions and clicks **Approve** or **Request changes**. You get their feedback back as markdown.
9
+
10
+ ## How
11
+
12
+ 1. List **every git repo and worktree you changed in this session**, as absolute paths. Include repos outside the current directory. If the work is spread over several worktrees of one repo (e.g. parallel agents), pass one of them with `"worktrees": true` instead of listing each.
13
+ 2. Pick a base per repo, usually by leaving it out: the plugin's hooks recorded where each repo stood when this session first touched it, and fill that in, so the review shows exactly what you changed in this session, commits included. Only pass `base` for something else, e.g. a whole branch against `main`. The result starts with a "Reviewed" list showing what the user saw.
14
+ 3. Call the `request_review` tool of the `feedbacker` MCP server with:
15
+ - `repos`: `[{ "path": "/abs/path", "base": "main", "worktrees": false }, ...]`
16
+ - `summary`: two to five sentences on what you changed and why.
17
+ - `questions`: only real decisions you want the user to make (optional).
18
+ 4. Before the call, tell the user a review page is opening in their browser. If it doesn't appear, they can run `node "${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs" list` to get its URL. The tool returns once they submit, however long that takes.
19
+
20
+ If the MCP tool isn't available, run the CLI in the background instead, so the shell timeout doesn't kill it, and read its output when it exits:
21
+
22
+ ```bash
23
+ node "${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs" /abs/repo-a /abs/repo-b@main \
24
+ -m "What changed" -q "A question?"
25
+ ```
26
+
27
+ ## After the review
28
+
29
+ - **Changes requested:** address every comment. Each one names the repo, file, line range and the code it refers to. Answer comments you disagree with instead of silently skipping them. Then offer another review round.
30
+ - **Approved:** carry on (commit, push, or whatever was next), taking any comments and answers into account.
31
+ - Use the answers to your questions as decisions; don't ask them again.
@@ -0,0 +1,122 @@
1
+ // Session baseline: where each repo stood when a Claude Code session first
2
+ // touched it, so a review shows only what changed in that session, even after
3
+ // the agent committed. Recorded and applied by the plugin's hooks
4
+ // (`feedbacker hook`, see hooks/hooks.json).
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { execFileSync } from 'node:child_process';
9
+ import { hasChanges, buildReview, stateDir } from './core.mjs';
10
+
11
+ const TOOL = 'mcp__plugin_feedbacker_feedbacker__request_review';
12
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
13
+ const KEEP_DAYS = 30;
14
+
15
+ function git(cwd, args) {
16
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
17
+ }
18
+
19
+ function sessionFile(id) {
20
+ return path.join(stateDir(), 'sessions', `${String(id).replace(/[^\w-]/g, '')}.json`);
21
+ }
22
+
23
+ export function loadSession(id) {
24
+ try { return JSON.parse(fs.readFileSync(sessionFile(id), 'utf8')); } catch { return { repos: {} }; }
25
+ }
26
+
27
+ function saveSession(id, session) {
28
+ const file = sessionFile(id);
29
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
30
+ fs.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 0o600 });
31
+ // Forget sessions nobody has touched for a month.
32
+ const cutoff = Date.now() - KEEP_DAYS * 86_400_000;
33
+ for (const name of fs.readdirSync(path.dirname(file))) {
34
+ const f = path.join(path.dirname(file), name);
35
+ try { if (fs.statSync(f).mtimeMs < cutoff) fs.rmSync(f); } catch {}
36
+ }
37
+ }
38
+
39
+ // Record the repo containing `dir` unless this session already has it.
40
+ // The baseline is HEAD, or a snapshot commit of uncommitted work
41
+ // (`git stash create`: nothing in the working tree or stash list changes).
42
+ export function record(id, dir) {
43
+ let top;
44
+ try {
45
+ let d = path.resolve(dir);
46
+ while (!fs.existsSync(d)) d = path.dirname(d); // a file that's about to be created
47
+ if (!fs.statSync(d).isDirectory()) d = path.dirname(d);
48
+ top = git(d, ['rev-parse', '--show-toplevel']);
49
+ } catch {
50
+ return null; // not in a git repo
51
+ }
52
+ const session = loadSession(id);
53
+ if (session.repos[top]) return session.repos[top];
54
+ const head = git(top, ['rev-parse', 'HEAD']);
55
+ let snapshot = '';
56
+ try { snapshot = git(top, ['stash', 'create']); } catch {}
57
+ session.started ||= new Date().toISOString();
58
+ session.repos[top] = { base: snapshot || head, head, recorded: new Date().toISOString() };
59
+ saveSession(id, session);
60
+ return session.repos[top];
61
+ }
62
+
63
+ // Fill in the session baseline for repos passed without a base, and add
64
+ // recorded repos that have changes but weren't passed. Returns the new input,
65
+ // or null when nothing changes.
66
+ export function applyBaseline(id, input) {
67
+ const session = loadSession(id);
68
+ const known = Object.keys(session.repos);
69
+ if (!known.length || !Array.isArray(input?.repos)) return null;
70
+ let changed = false;
71
+ const topOf = (p) => { try { return git(path.resolve(p), ['rev-parse', '--show-toplevel']); } catch { return null; } };
72
+ const repos = input.repos.map((r) => {
73
+ const spec = typeof r === 'string' ? { path: r } : { ...r };
74
+ const top = topOf(spec.path);
75
+ if (!spec.base && top && session.repos[top]) {
76
+ spec.base = session.repos[top].base;
77
+ spec.exact = true;
78
+ spec.session = true;
79
+ changed = true;
80
+ }
81
+ return spec;
82
+ });
83
+ const passed = new Set(repos.map((r) => topOf(r.path)).filter(Boolean));
84
+ for (const top of known) {
85
+ if (passed.has(top) || !fs.existsSync(top)) continue;
86
+ const spec = { path: top, base: session.repos[top].base, exact: true, session: true };
87
+ if (hasChanges(buildReview({ repos: [spec] }))) { repos.push(spec); changed = true; }
88
+ }
89
+ return changed ? { ...input, repos } : null;
90
+ }
91
+
92
+ // Entry point for `feedbacker hook`: reads the hook event JSON on stdin.
93
+ // Never fails the tool call: any error just means no baseline.
94
+ export async function runHook() {
95
+ let event;
96
+ try {
97
+ let text = '';
98
+ for await (const chunk of process.stdin) text += chunk;
99
+ event = JSON.parse(text);
100
+ } catch {
101
+ return;
102
+ }
103
+ const id = event.session_id;
104
+ if (!id) return;
105
+ try {
106
+ if (event.hook_event_name === 'SessionStart' && event.cwd) {
107
+ record(id, event.cwd);
108
+ } else if (event.hook_event_name === 'PreToolUse') {
109
+ if (EDIT_TOOLS.has(event.tool_name)) {
110
+ const file = event.tool_input?.file_path || event.tool_input?.notebook_path;
111
+ if (file) record(id, path.resolve(event.cwd || '.', file));
112
+ } else if (event.tool_name === TOOL) {
113
+ const updated = applyBaseline(id, event.tool_input);
114
+ if (updated) {
115
+ process.stdout.write(JSON.stringify({
116
+ hookSpecificOutput: { hookEventName: 'PreToolUse', updatedInput: updated },
117
+ }));
118
+ }
119
+ }
120
+ }
121
+ } catch {}
122
+ }