diffprism 1.4.0 → 1.5.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.
package/README.md CHANGED
@@ -79,7 +79,7 @@ Reviews are **one per repo**: opening a review for a repo that already has one u
79
79
  |------|---------|
80
80
  | `annotate` | Post findings inline on the diff; `warning` flags the session for attention |
81
81
  | `get_review_comments` | Read every thread on the session; `awaiting_reply` narrows to unanswered ones |
82
- | `reply` | Reply to a thread answer the reviewer's question on a line |
82
+ | `reply` | Reply to a thread, then keep listening for what the reviewer does next |
83
83
  | `wait_for_comments` | Block until the reviewer writes something the agent hasn't answered |
84
84
  | `get_review_state` | Session status, attention and new-changes flags, and annotations |
85
85
  | `get_user_focus` | What file/line the user is currently viewing in the browser |
@@ -89,7 +89,7 @@ Reviews are **one per repo**: opening a review for a repo that already has one u
89
89
 
90
90
  In a local review, **Ask agent now** in a line's comment form asks the agent while you're still reviewing: the agent's wait for your decision ends with your question, it answers in the thread, and goes back to waiting.
91
91
 
92
- PR reviews are opened with `diffprism review <PR URL>` or the dashboard — not by `open_review` — and your AI then works inside them with the tools above. Click a line to ask the agent about it; an agent listening with `wait_for_comments` answers in the thread. Nothing starts an agent for you: if none has picked a question up, the thread says so and names the session to ask Claude Code about.
92
+ PR reviews are opened with `diffprism review <PR URL>` or the dashboard — not by `open_review` — and your AI then works inside them with the tools above. Click a line to ask the agent about it; an agent listening with `wait_for_comments` answers in the thread, and stays listening for your next question until you tell it to stop. Nothing starts an agent for you: if none has picked a question up, the thread says so and names the session to ask Claude Code about. That one prompt is enough — the agent is handed the checkout, branch, PR and the code you asked about along with the question.
93
93
 
94
94
  When you're done, **Approve**, **Request changes** or **Comment** from the bar at the bottom: DiffPrism posts it to GitHub as a pull request review. Your threads are a conversation with the agent, so none of them goes to GitHub unless you tick it; a ticked thread posts your opening message as an inline review comment. The token comes from `GITHUB_TOKEN`, `gh auth token`, or `~/.diffprism/config.json`.
95
95
 
@@ -113,7 +113,7 @@ DiffPrism also works for reviewing local agent-generated changes:
113
113
  ```bash
114
114
  diffprism review # Review all changes (staged + unstaged)
115
115
  diffprism review --staged # Staged changes only
116
- diffprism review HEAD~3 # Last 3 commits
116
+ diffprism review HEAD~3..HEAD # Last 3 commits
117
117
  diffprism review main..feature # Branch diff
118
118
  ```
119
119
 
@@ -181,6 +181,7 @@ skill tells them so.
181
181
  ## CLI Reference
182
182
 
183
183
  ```bash
184
+ diffprism demo # Open a sample review
184
185
  diffprism review <ref> # Open a review (PR URL, git ref, or flags)
185
186
  diffprism setup # Configure Claude Code integration
186
187
  diffprism setup --global # Global setup (no git repo needed)
@@ -189,10 +190,12 @@ diffprism server status # Check server status
189
190
  diffprism server stop # Stop the server
190
191
  diffprism hook install # Gate commits on a review
191
192
  diffprism hook uninstall # Remove the gate
193
+ diffprism hook pre-commit # Run the gate (what the installed hook calls)
192
194
  diffprism reply --session <id> <annotation-id> "…" # Answer a reviewer's question, as the agent
193
195
  diffprism feedback # Share feedback as a prefilled GitHub issue
194
196
  diffprism feedback --bug # Report a bug, including the last error
195
197
  diffprism teardown # Remove configuration
198
+ diffprism serve # Run the MCP server over stdio (your MCP client launches this)
196
199
  ```
197
200
 
198
201
  ## Feedback
@@ -215,6 +218,7 @@ cd diffprism
215
218
  pnpm install
216
219
  pnpm test
217
220
  pnpm run build
221
+ pnpm docs:check # Fail if docs disagree with the code (runs in CI)
218
222
  pnpm cli review --staged # Run CLI from source
219
223
  ```
220
224
 
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-RFDSC6MC.js";
6
6
  import {
7
7
  demo
8
- } from "./chunk-6P6QJPWT.js";
8
+ } from "./chunk-4JLGL3O6.js";
9
9
  import {
10
10
  COMMIT_GATE_DIFF_REF,
11
11
  DEFAULT_DIFF_REF,
@@ -26,14 +26,14 @@ import {
26
26
  recordError,
27
27
  startGlobalServer,
28
28
  submitReviewToServer
29
- } from "./chunk-SCQFIDIK.js";
29
+ } from "./chunk-M5NJBIOV.js";
30
30
  import {
31
31
  getDiff
32
32
  } from "./chunk-GGK6TIQZ.js";
33
33
  import "./chunk-DHCVZGHE.js";
34
34
  import "./chunk-JSBRDJBE.js";
35
35
 
36
- // cli/src/index.ts
36
+ // cli/src/program.ts
37
37
  import { Command } from "commander";
38
38
 
39
39
  // cli/src/commands/hook.ts
@@ -477,7 +477,7 @@ This should happen automatically after significant changes. You don't need the u
477
477
  - **\`dismissed\`** \u2014 the reviewer closed it without deciding. Ask before continuing.
478
478
  - If \`postReviewAction\` is \`"commit"\` \u2014 commit the changes. If \`"commit_and_pr"\` \u2014 commit and open a PR.
479
479
  4. If it returns \`status: "timed_out"\`, the reviewer is still reading. Keep waiting with \`mcp__diffprism__get_review_result\` (\`wait: true\`).
480
- 5. If it returns \`status: "reviewer_asked"\`, the reviewer asked you something before deciding. Answer each thread with \`mcp__diffprism__reply\` (its \`id\` is the \`annotation_id\`) \u2014 and change the code if that's what they asked for. Then wait again with \`mcp__diffprism__get_review_result\` (\`wait: true\`), which can return \`reviewer_asked\` again.
480
+ 5. If it returns \`status: "reviewer_asked"\`, the reviewer asked you something before deciding. Answer each thread with \`mcp__diffprism__reply\` (its \`id\` is the \`annotation_id\`) \u2014 and change the code if that's what they asked for. \`reply\` goes back to waiting by itself and returns what happened next as \`next\`: another question (\`reviewer_asked\`) to answer the same way, the decision (\`decided\`), or \`timed_out\` \u2014 then wait with \`mcp__diffprism__get_review_result\` (\`wait: true\`).
481
481
 
482
482
  **While a review is open, wait for it.** Don't ask the user whether they've finished, and don't move on to other work \u2014 their decision is the answer, and it arrives through the tool. Asking in the terminal splits the conversation in two and the decision gets lost between them.
483
483
 
@@ -514,11 +514,11 @@ Pull requests are opened by the user \u2014 \`diffprism review <PR URL>\` or "Re
514
514
 
515
515
  The reviewer can also ask you questions on lines of the PR. Hold that conversation in the dashboard, not the terminal:
516
516
 
517
- 1. \`mcp__diffprism__wait_for_comments\` \u2014 blocks until the reviewer writes something you haven't answered, then returns those threads.
518
- 2. \`mcp__diffprism__reply\` \u2014 answer each thread, passing its \`annotation_id\`.
519
- 3. Wait again. On \`timed_out\`, nothing new was said \u2014 keep waiting until the user tells you to stop.
517
+ 1. \`mcp__diffprism__wait_for_comments\` \u2014 blocks until the reviewer writes something you haven't answered, then returns those threads. Each carries \`hunk\`, the code it's on, and the response carries \`review\`: the checkout on disk (\`projectPath\`), the branch, and the PR. That's enough to start answering \u2014 you don't need to look the review up first. If \`localRepoConnected\` is false there's no clone here; answer from the hunk and the PR.
518
+ 2. \`mcp__diffprism__reply\` \u2014 answer a thread, passing its \`annotation_id\`. It goes back to listening by itself and returns what happened next as \`next\`: more questions to answer the same way, or \`timed_out\` \u2014 then call \`wait_for_comments\` again.
519
+ 3. Keep going until the user tells you to stop. Answering is not the end of the conversation; the reviewer's next question only reaches you if you're still listening.
520
520
 
521
- When no agent has read a question, the dashboard tells the reviewer so, and to ask you to answer their DiffPrism comments on a session id. When they do, run this loop with that \`session_id\`.
521
+ When no agent has read a question, the dashboard tells the reviewer so, and to ask you to answer their DiffPrism comments on a session id. When they do, call \`wait_for_comments\` with that \`session_id\` \u2014 it returns at once with what's waiting \u2014 and run this loop.
522
522
 
523
523
  A PR review and a working-copy review can be open for the same clone at once. If a tool reports more than one session, pass the \`session_id\` of the one you mean.
524
524
 
@@ -542,8 +542,8 @@ A PR review and a working-copy review can be open for the same clone at once. If
542
542
  |------|---------|
543
543
  | \`annotate\` | Post one or more findings. \`warning\` flags the session for attention. |
544
544
  | \`get_review_comments\` | Every thread on the review; \`awaiting_reply\` narrows to threads waiting for an answer. |
545
- | \`reply\` | Reply to a thread \u2014 answer the reviewer's question or follow up on a finding. |
546
- | \`wait_for_comments\` | Block until the reviewer writes something you haven't answered. |
545
+ | \`reply\` | Reply to a thread, then keep listening and return what happens next. |
546
+ | \`wait_for_comments\` | Block until the reviewer writes something you haven't answered; returns it with the code and where the review is. |
547
547
  | \`get_review_state\` | Session status, attention and new-changes flags, and annotations. |
548
548
  | \`get_user_focus\` | What the user is currently looking at. |
549
549
  | \`get_pr_context\` | PR overview: metadata, briefing, file list, local clone status. |
@@ -721,7 +721,7 @@ async function setupInteractive(flags) {
721
721
  }
722
722
  async function runDemo(dev) {
723
723
  console.log("");
724
- const { demo: demo2 } = await import("./demo-GHOE7YNU.js");
724
+ const { demo: demo2 } = await import("./demo-5UQ7CLKT.js");
725
725
  await demo2({ dev });
726
726
  }
727
727
  async function setupBatch(flags) {
@@ -1218,33 +1218,38 @@ ${url}`
1218
1218
  }
1219
1219
  }
1220
1220
 
1221
+ // cli/src/program.ts
1222
+ function createProgram() {
1223
+ const program = new Command();
1224
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(describeVersion(currentVersion()));
1225
+ program.action(defaultAction);
1226
+ program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
1227
+ program.command("review [ref]").description("Open a browser-based diff review (local git ref or GitHub PR ref like owner/repo#123)").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--reasoning <text>", "Agent reasoning about the changes").option("--dev", "Use Vite dev server with HMR instead of static files").option("--post-to-github", "Automatically post review back to GitHub without prompting").action(review);
1228
+ program.command("review-pr <pr>", { hidden: true }).action((pr, flags) => review(pr, flags));
1229
+ const hookCmd = program.command("hook").description("Git hook integration \u2014 gate commits on a human review");
1230
+ hookCmd.command("pre-commit").description("Run the review gate for the staged changes (called from a git hook)").option("--min-lines <n>", "Staged lines required to trigger a review (default: 120)").option("--dev", "Use Vite dev server").action((flags) => {
1231
+ void preCommitHook(flags);
1232
+ });
1233
+ hookCmd.command("install").description("Add the diffprism gate to this repo's pre-commit hook").action(() => {
1234
+ installHook();
1235
+ });
1236
+ hookCmd.command("uninstall").description("Remove the diffprism gate from this repo's pre-commit hook").action(() => {
1237
+ uninstallHook();
1238
+ });
1239
+ program.command("feedback").description("Open a prefilled GitHub issue to share feedback or report a bug \u2014 you review it before anything is sent").option("--bug", "Report a bug, including the last error DiffPrism hit").option("-m, --message <text>", "Start the issue with this text").option("--print", "Print the issue URL instead of opening a browser").action((flags) => feedback(flags));
1240
+ program.command("reply <annotation-id> <message...>").description("Answer a reviewer's question on an open review, as the agent").requiredOption("--session <id>", "The review the question is on").option("--agent <name>", "Name shown on the reply", "agent").action((annotationId, message2, flags) => reply(annotationId, message2, flags));
1241
+ program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
1242
+ program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Configure globally (skill + permissions, no git repo required)").option("--force", "Overwrite existing configuration files").option("--dev", "Use Vite dev server").option("--no-demo", "Skip the demo review after setup").action((flags) => {
1243
+ setup(flags);
1244
+ });
1245
+ program.command("teardown").description("Remove DiffPrism configuration from the current project").option("--global", "Remove global configuration (skill + permissions at ~/.claude/)").option("-q, --quiet", "Suppress output").action((flags) => {
1246
+ teardown(flags);
1247
+ });
1248
+ const serverCmd = program.command("server").description("Start the global DiffPrism server for multi-session reviews").option("-p, --port <port>", "HTTP API port (default: 24680)").option("--ws-port <port>", "WebSocket port (default: 24681)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--background", "Start server as a background daemon").option("--_daemon", "Internal: run as spawned daemon (do not use directly)").action(server);
1249
+ serverCmd.command("status").description("Check if the global server is running and list active sessions").action(serverStatus);
1250
+ serverCmd.command("stop").description("Stop the running global server").action(serverStop);
1251
+ return program;
1252
+ }
1253
+
1221
1254
  // cli/src/index.ts
1222
- var program = new Command();
1223
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(describeVersion(currentVersion()));
1224
- program.action(defaultAction);
1225
- program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
1226
- program.command("review [ref]").description("Open a browser-based diff review (local git ref or GitHub PR ref like owner/repo#123)").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--reasoning <text>", "Agent reasoning about the changes").option("--dev", "Use Vite dev server with HMR instead of static files").option("--post-to-github", "Automatically post review back to GitHub without prompting").action(review);
1227
- program.command("review-pr <pr>", { hidden: true }).action((pr, flags) => review(pr, flags));
1228
- var hookCmd = program.command("hook").description("Git hook integration \u2014 gate commits on a human review");
1229
- hookCmd.command("pre-commit").description("Run the review gate for the staged changes (called from a git hook)").option("--min-lines <n>", "Staged lines required to trigger a review (default: 120)").option("--dev", "Use Vite dev server").action((flags) => {
1230
- void preCommitHook(flags);
1231
- });
1232
- hookCmd.command("install").description("Add the diffprism gate to this repo's pre-commit hook").action(() => {
1233
- installHook();
1234
- });
1235
- hookCmd.command("uninstall").description("Remove the diffprism gate from this repo's pre-commit hook").action(() => {
1236
- uninstallHook();
1237
- });
1238
- program.command("feedback").description("Open a prefilled GitHub issue to share feedback or report a bug \u2014 you review it before anything is sent").option("--bug", "Report a bug, including the last error DiffPrism hit").option("-m, --message <text>", "Start the issue with this text").option("--print", "Print the issue URL instead of opening a browser").action((flags) => feedback(flags));
1239
- program.command("reply <annotation-id> <message...>").description("Answer a reviewer's question on an open review, as the agent").requiredOption("--session <id>", "The review the question is on").option("--agent <name>", "Name shown on the reply", "agent").action((annotationId, message2, flags) => reply(annotationId, message2, flags));
1240
- program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
1241
- program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Configure globally (skill + permissions, no git repo required)").option("--force", "Overwrite existing configuration files").option("--dev", "Use Vite dev server").option("--no-demo", "Skip the demo review after setup").action((flags) => {
1242
- setup(flags);
1243
- });
1244
- program.command("teardown").description("Remove DiffPrism configuration from the current project").option("--global", "Remove global configuration (skill + permissions at ~/.claude/)").option("-q, --quiet", "Suppress output").action((flags) => {
1245
- teardown(flags);
1246
- });
1247
- var serverCmd = program.command("server").description("Start the global DiffPrism server for multi-session reviews").option("-p, --port <port>", "HTTP API port (default: 24680)").option("--ws-port <port>", "WebSocket port (default: 24681)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--background", "Start server as a background daemon").option("--_daemon", "Internal: run as spawned daemon (do not use directly)").action(server);
1248
- serverCmd.command("status").description("Check if the global server is running and list active sessions").action(serverStatus);
1249
- serverCmd.command("stop").description("Stop the running global server").action(serverStop);
1250
- program.parse();
1255
+ createProgram().parse();
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ensureServer,
3
3
  submitReviewToServer
4
- } from "./chunk-SCQFIDIK.js";
4
+ } from "./chunk-M5NJBIOV.js";
5
5
  import {
6
6
  parseDiff
7
7
  } from "./chunk-GGK6TIQZ.js";
@@ -96,12 +96,22 @@ function pickedUpByAgent(annotation, agentReadAt) {
96
96
  return agentReadAt !== void 0 && agentReadAt > lastMessageAt(annotation);
97
97
  }
98
98
 
99
+ // packages/core/src/diff-scope.ts
100
+ var DEFAULT_DIFF_REF = "working-copy";
101
+ var COMMIT_GATE_DIFF_REF = "staged";
102
+ var DIFF_REF_DESCRIPTION = 'Which changes to review. "working-copy" (the default): everything not yet committed, staged and unstaged shown as separate groups. "staged": only what the next commit would contain. "unstaged": only edits not yet staged. Or a ref range such as "HEAD~3..HEAD" or "main..feature".';
103
+
104
+ // packages/core/src/feedback.ts
105
+ import fs3 from "fs";
106
+ import os2 from "os";
107
+ import path3 from "path";
108
+
99
109
  // packages/core/src/build-info.ts
100
110
  import fs2 from "fs";
101
111
  import path2 from "path";
102
112
  import { fileURLToPath } from "url";
103
113
  function builtAt() {
104
- return true ? 1789619773143 : null;
114
+ return true ? 1789699543800 : null;
105
115
  }
106
116
  function getBuildInfo() {
107
117
  let dir = path2.dirname(fileURLToPath(import.meta.url));
@@ -127,19 +137,11 @@ function describeVersion(version) {
127
137
  return info.dev ? `${version} (dev build \u2014 ${info.root})` : version;
128
138
  }
129
139
 
130
- // packages/core/src/diff-scope.ts
131
- var DEFAULT_DIFF_REF = "working-copy";
132
- var COMMIT_GATE_DIFF_REF = "staged";
133
- var DIFF_REF_DESCRIPTION = 'Which changes to review. "working-copy" (the default): everything not yet committed, staged and unstaged shown as separate groups. "staged": only what the next commit would contain. "unstaged": only edits not yet staged. Or a ref range such as "HEAD~3..HEAD" or "main..feature".';
134
-
135
140
  // packages/core/src/feedback.ts
136
- import fs3 from "fs";
137
- import os2 from "os";
138
- import path3 from "path";
139
141
  var ISSUES_NEW_URL = "https://github.com/CodeJonesW/diffprism/issues/new";
140
142
  var MAX_ERROR_CHARS = 1500;
141
143
  function currentVersion() {
142
- return true ? "1.4.0" : "0.0.0-dev";
144
+ return true ? "1.5.0" : "0.0.0-dev";
143
145
  }
144
146
  function describeEnvironment() {
145
147
  return {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  demo
3
- } from "./chunk-6P6QJPWT.js";
4
- import "./chunk-SCQFIDIK.js";
3
+ } from "./chunk-4JLGL3O6.js";
4
+ import "./chunk-M5NJBIOV.js";
5
5
  import "./chunk-GGK6TIQZ.js";
6
6
  import "./chunk-DHCVZGHE.js";
7
7
  import "./chunk-JSBRDJBE.js";
@@ -14,7 +14,7 @@ import {
14
14
  recordError,
15
15
  submitReviewToServer,
16
16
  waitForDecision
17
- } from "./chunk-SCQFIDIK.js";
17
+ } from "./chunk-M5NJBIOV.js";
18
18
  import {
19
19
  getDiff
20
20
  } from "./chunk-GGK6TIQZ.js";
@@ -81,11 +81,56 @@ async function readThreads(serverInfo, sessionId) {
81
81
  const { annotations } = await response.json();
82
82
  return annotations.map((a) => ({ ...a, awaitingReply: awaitingAgent(a) }));
83
83
  }
84
- function reviewerAskedResult(err, waitWith) {
84
+ async function readReview(serverInfo, sessionId) {
85
+ let response;
86
+ try {
87
+ response = await fetch(
88
+ `http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/payload`
89
+ );
90
+ } catch {
91
+ return null;
92
+ }
93
+ if (!response.ok) {
94
+ return null;
95
+ }
96
+ const { payload, projectPath } = await response.json();
97
+ return {
98
+ review: {
99
+ sessionId,
100
+ projectPath,
101
+ localRepoConnected: !projectPath.startsWith("github:"),
102
+ branch: payload.metadata.currentBranch,
103
+ pr: payload.metadata.githubPr ?? null,
104
+ title: payload.metadata.title
105
+ },
106
+ diffSet: payload.diffSet
107
+ };
108
+ }
109
+ function hunkFor(diffSet, annotation) {
110
+ const file = diffSet.files.find((f) => f.path === annotation.file);
111
+ if (!file) {
112
+ return null;
113
+ }
114
+ return file.hunks.find(
115
+ (hunk) => annotation.side === "old" ? annotation.line >= hunk.oldStart && annotation.line < hunk.oldStart + hunk.oldLines : annotation.line >= hunk.newStart && annotation.line < hunk.newStart + hunk.newLines
116
+ ) ?? null;
117
+ }
118
+ function answerableThreads(context, threads) {
119
+ return {
120
+ sessionId: context.review.sessionId,
121
+ review: context.review,
122
+ threads: threads.map((thread) => ({
123
+ ...thread,
124
+ awaitingReply: true,
125
+ hunk: hunkFor(context.diffSet, thread)
126
+ }))
127
+ };
128
+ }
129
+ async function reviewerAskedResult(serverInfo, err, waitWith) {
130
+ const context = await readReview(serverInfo, err.sessionId);
85
131
  return jsonResult({
86
132
  status: "reviewer_asked",
87
- sessionId: err.sessionId,
88
- threads: err.threads.map((t) => ({ ...t, awaitingReply: true })),
133
+ ...context ? answerableThreads(context, err.threads) : { sessionId: err.sessionId, threads: err.threads.map((t) => ({ ...t, awaitingReply: true })) },
89
134
  message: `The reviewer asked you something before deciding. Answer each thread with reply (session_id: ${err.sessionId}, annotation_id from threads) \u2014 change the code too if that's what they asked for. Then ${waitWith}. The review stays open; the decision still comes.`
90
135
  });
91
136
  }
@@ -124,7 +169,7 @@ var annotationSchema = z.object({
124
169
  "other"
125
170
  ]).optional().describe("Category of the finding (defaults to 'other')")
126
171
  });
127
- async function startMcpServer() {
172
+ function createMcpServer() {
128
173
  const server = new McpServer({
129
174
  name: "diffprism",
130
175
  version: currentVersion()
@@ -176,7 +221,7 @@ async function startMcpServer() {
176
221
  });
177
222
  } catch (err) {
178
223
  if (err instanceof ReviewerAskedError) {
179
- return reviewerAskedResult(err, `wait for the decision with get_review_result (session_id: ${err.sessionId}, wait: true)`);
224
+ return await reviewerAskedResult(serverInfo, err, `wait for the decision with get_review_result (session_id: ${err.sessionId}, wait: true)`);
180
225
  }
181
226
  if (err instanceof ReviewTimeoutError) {
182
227
  return jsonResult({
@@ -217,7 +262,7 @@ ${REPORT_HINT}`);
217
262
  return jsonResult(await waitForDecision(serverInfo, sessionId, Math.min(timeout ?? 300, 600) * 1e3));
218
263
  } catch (err) {
219
264
  if (err instanceof ReviewerAskedError) {
220
- return reviewerAskedResult(err, "call get_review_result with wait: true again");
265
+ return await reviewerAskedResult(serverInfo, err, "call get_review_result with wait: true again");
221
266
  }
222
267
  if (err instanceof ReviewTimeoutError) {
223
268
  return jsonResult({
@@ -382,14 +427,17 @@ ${REPORT_HINT}`);
382
427
  );
383
428
  server.tool(
384
429
  "reply",
385
- "Reply to a thread on an open review \u2014 answer the reviewer's question, or follow up on a finding. The reply appears under the thread in the dashboard straight away.",
430
+ "Reply to a thread on an open review \u2014 answer the reviewer's question, or follow up on a finding. The reply appears under the thread in the dashboard straight away, and then this goes back to listening and returns whatever happens next: the reviewer's next question, their decision, or `timed_out`. Stay in that loop until they tell you to stop.",
386
431
  {
387
432
  ...targetParams,
388
433
  annotation_id: z.string().describe("The thread to reply to (an annotation id from get_review_comments or wait_for_comments)"),
389
434
  body: z.string().describe("Your reply"),
390
- source_agent: z.string().optional().describe("Who is replying, e.g. 'pr-reviewer'")
435
+ source_agent: z.string().optional().describe("Who is replying, e.g. 'pr-reviewer'"),
436
+ then_wait: z.boolean().optional().describe(
437
+ "Keep listening after replying (default true). With false, returns as soon as the reply is posted \u2014 use it when you have several threads to answer and want to post them all first."
438
+ )
391
439
  },
392
- async ({ session_id, repo_path, annotation_id, body, source_agent }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
440
+ async ({ session_id, repo_path, annotation_id, body, source_agent, then_wait }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
393
441
  const response = await fetch(
394
442
  `http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations/${annotation_id}/replies`,
395
443
  {
@@ -402,7 +450,33 @@ ${REPORT_HINT}`);
402
450
  if (!response.ok) {
403
451
  return toolError(`Error: ${data.error ?? `server returned ${response.status}`}`);
404
452
  }
405
- return jsonResult({ sessionId, annotationId: annotation_id, replyId: data.replyId });
453
+ const posted = { sessionId, annotationId: annotation_id, replyId: data.replyId };
454
+ if (then_wait === false) {
455
+ return jsonResult(posted);
456
+ }
457
+ try {
458
+ const result = await waitForDecision(serverInfo, sessionId, DEFAULT_WAIT_MS);
459
+ return jsonResult({ ...posted, next: { status: "decided", result } });
460
+ } catch (err) {
461
+ if (err instanceof ReviewerAskedError) {
462
+ const context = await readReview(serverInfo, sessionId);
463
+ return jsonResult({
464
+ ...posted,
465
+ next: context ? { status: "reviewer_asked", ...answerableThreads(context, err.threads) } : { status: "reviewer_asked", sessionId, threads: err.threads }
466
+ });
467
+ }
468
+ if (err instanceof ReviewTimeoutError) {
469
+ return jsonResult({
470
+ ...posted,
471
+ next: {
472
+ status: "timed_out",
473
+ sessionId,
474
+ message: "Your reply is posted and the reviewer hasn't said anything since. Keep listening: wait_for_comments in a PR review, or get_review_result with wait: true if you're waiting for their decision. Don't ask them in the terminal."
475
+ }
476
+ });
477
+ }
478
+ throw err;
479
+ }
406
480
  })
407
481
  );
408
482
  server.tool(
@@ -422,7 +496,10 @@ ${REPORT_HINT}`);
422
496
  }
423
497
  const waiting = threads.filter((t) => t.awaitingReply);
424
498
  if (waiting.length > 0) {
425
- return jsonResult({ sessionId, threads: waiting });
499
+ const context = await readReview(serverInfo, sessionId);
500
+ return jsonResult(
501
+ context ? answerableThreads(context, waiting) : { sessionId, threads: waiting }
502
+ );
426
503
  }
427
504
  if (Date.now() - start >= maxWaitMs) {
428
505
  return jsonResult({
@@ -568,11 +645,16 @@ ${available.join("\n")}`);
568
645
  });
569
646
  })
570
647
  );
648
+ return server;
649
+ }
650
+ async function startMcpServer() {
651
+ const server = createMcpServer();
571
652
  const transport = new StdioServerTransport();
572
653
  await server.connect(transport);
573
654
  }
574
655
  export {
575
656
  DEFAULT_WAIT_MS,
657
+ createMcpServer,
576
658
  resolveTarget,
577
659
  startMcpServer
578
660
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "dev": "pnpm -r run dev",
17
17
  "lint": "pnpm -r run lint",
18
18
  "test": "pnpm -r run test",
19
+ "docs:check": "pnpm -F diffprism-cli docs:check",
19
20
  "cli": "pnpm -F @diffprism/ui build && tsx cli/src/index.ts",
20
21
  "prepublishOnly": "pnpm run build"
21
22
  },