prreviewbuddy 0.25.7 → 0.25.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,120 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.13
4
+
5
+ **Two updates of the same review no longer run over each other.** Pressing **Update review** on the
6
+ page while `prreviewbuddy review --update` was working on the same review started a second update
7
+ alongside the first. Whichever finished last replaced the other's result without a word, after
8
+ both had spent minutes of agent time.
9
+
10
+ A second update is now refused while one is running, from wherever it was asked for. On the page,
11
+ the review reloads onto the running update's steps. In the terminal, the command says the review
12
+ is already being updated, gives the link where you can follow it, and exits without changing
13
+ anything. An analysis, a KISS review or a question to the assistant does not block an update.
14
+
15
+ ## 0.25.12
16
+
17
+ **An update shows what it is doing, in the terminal and on the review.** `prreviewbuddy review
18
+ --update` printed nothing until the reassessment was over, usually minutes after you typed it, and
19
+ the review page showed nothing either. Only the reviews list noticed, and it called the whole update
20
+ "Preparing isolated checkout".
21
+
22
+ The command now prints the link straight away and then draws the same step list a new review does:
23
+ fetching the latest commits, re-reading the pull request conversation, preparing a checkout of the
24
+ new commits, and reassessing the review, with the agent's own notes under the step that is running.
25
+ Open the link while it works and those steps tick over above the review, which stays readable
26
+ underneath until the update lands. The page refreshes onto the result when it finishes, and the
27
+ reviews list names each step in the same words.
28
+
29
+ An update that finds no new commits stops after the fetch and says so, without ticking steps it
30
+ never ran. **Update review** on the page now shows the same live steps, where it used to show
31
+ captions guessed from a timer.
32
+
33
+ `--reanalyse` kept the terminal silent in the same way, for a whole analysis, and now draws its
34
+ progress too.
35
+
36
+ ## 0.25.11
37
+
38
+ **A review with nothing committed to read now says why, straight away.** Running
39
+ `prreviewbuddy review` on `main` with a dozen edited files said "No changes found to review",
40
+ which looked like a bug to anyone who could see the edits. It now says there are no *committed*
41
+ changes, counts the uncommitted ones (new files included), and explains that PR Review Buddy
42
+ reviews a fixed snapshot of your code, so work that is not committed yet is not included. Then
43
+ it tells you what to do: commit on a branch, or just commit if you are already on one.
44
+
45
+ The refusal also comes at once. It used to arrive after an isolated checkout had been prepared,
46
+ reported as a paused review that would refuse again every time you tried to carry it on. Nothing
47
+ is created now. A branch that has since been merged still opens the review it already has.
48
+
49
+ ## 0.25.10
50
+
51
+ **The reviews list now shows what is happening, and keeps showing it.** A review being analysed
52
+ right now, one whose analysis failed, and one that was never analysed all looked the same on the
53
+ list: every card said "No standard review yet". Start a review, go back to the list, and you
54
+ watched nothing happen for three minutes.
55
+
56
+ A card now says which step an analysis has reached, in the same words the terminal prints, and says
57
+ when one has stopped. It updates on its own, so a review that finishes while you are looking at the
58
+ list fills in its summary, risk and counts without you touching anything. A review started in
59
+ another terminal appears by itself too.
60
+
61
+ Where a review already has results, they stay put while a new analysis runs over them: a KISS run
62
+ no longer hides the issues you were reading.
63
+
64
+ The list checks in about once a second while something is being made and once every fifteen when
65
+ nothing is, and it waits rather than refreshing under you if you have rows ticked or are partway
66
+ through typing a search.
67
+
68
+ ## 0.25.9
69
+
70
+ **Marking a review reviewed shows up straight away on the reviews list.** It used to keep saying
71
+ what it had said before, and catch up some minutes later on its own. Nothing was actually stale:
72
+ the mark is written to disk before the button finishes repainting, and the list is rebuilt from
73
+ those files on every request. The browser simply stopped asking. No page this server sends had
74
+ ever carried a caching rule, so a browser was free to reuse the copy it already had, and pressing
75
+ Back does not reload a page at all: it restores a frozen one from memory, JavaScript variables and
76
+ all, without a single request.
77
+
78
+ Both doors are shut now, and shut in one place rather than per page. Every response describing a
79
+ review, HTML and JSON alike, says it must not be stored, and that is a property of the two helpers
80
+ that write responses rather than something each route remembers. Every page now reloads itself
81
+ when the browser restores it from history, because a restored page may be stale in any of its
82
+ parts and not only the one that prompted this.
83
+
84
+ Assets went the other way, which is the arrangement they should always have had. The bundles whose
85
+ filenames carry a content hash, which are most of what a page downloads, may now be kept
86
+ indefinitely: a new build asks for different names. The handful named plainly still check in every
87
+ time, because those names survive an upgrade.
88
+
89
+ ## 0.25.8
90
+
91
+ **A review that fails says so, instead of reporting itself as ready.** An analysis that failed
92
+ after the agent had started work left no trace of the failure: the job stayed at `Analysing the
93
+ change`, the workspace page kept the running rail over it, and the terminal printed
94
+ `Review ready . Risk not rated . 0 findings`, which reads as a change with nothing wrong with it.
95
+ The review it was reporting did not exist.
96
+
97
+ The record refuses to be failed by anyone but the run that owns it, so that a driver whose job has
98
+ been taken over cannot mark a healthy review as broken. It asked that question of the record's
99
+ timestamp, and the timestamp moves every time the agent reports what it is doing. So a run that
100
+ streamed a single line and then failed looked, to itself, like a run that had been taken over, and
101
+ declined to record its own failure. It asks the claim now, which is who is actually driving.
102
+
103
+ **The terminal reports what happened, and refuses to invent the rest.** `prreviewbuddy review` read
104
+ "the job did not fail" as "the review is ready". A run can also stop without recording why, and now
105
+ says so, naming the phase it stopped in, which is the phase running the command again picks up
106
+ from. A finished job holding no review is reported as a review that stopped rather than as a clean
107
+ one: the standard review is held to the rule the KISS lens has always been held to.
108
+
109
+ **A fenced answer is read to its end, not to the first ``` inside it.** The model is asked for
110
+ JSON and often wraps it in a code fence, and the summary and next steps of a review about code
111
+ frequently hold a fenced snippet of their own. The unwrapping stopped at the first closing marker
112
+ it found, which in that case is the one *opening* a snippet inside one of the answer's own
113
+ strings. A real review of a ten-file pull request was cut in half 24KB in, mid-string, and thrown
114
+ away whole: four themes, six findings, six questions and six next steps, none of which anything
115
+ ever showed. A fence says where an answer starts, not where it ends, so one that does not parse is
116
+ now passed over for the scanner that reads such an object correctly.
117
+
3
118
  ## 0.25.7
4
119
 
5
120
  **The agent you chose is the agent that runs.** Setting a default agent, with
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { B as BUILD_VERSION, Bt as STORE_ROOT, C as statedWorkspacePort, E as updateReview, F as startJob, Ft as agentIdOf, G as forgeResolver, Gt as detectAgents, Ht as DEFAULT_AGENT_ID, J as clearClaim, L as record, Lt as readAgentPreference, Mt as resolveTarget, N as reanalyseReview, Nt as displayRef, P as runJob, Pt as git, Q as removeWorktree, R as recordTelemetryUploadConsent, Rt as writeAgentPreference, St as summarise, Ut as agentById, V as PACKAGE_NAME, Vt as AGENT_IDS, X as MANAGED_ROOT, Y as isClaimed, Z as readMarker, a as liveJobsFor, b as MIN_WORKSPACE_PORT, bt as reviewedRepositories, ct as followedRefName, dt as lineageKeyFor, et as checkFreshness, f as ensureServer, g as workspaceUrl, h as stopServer, ht as positionInLineage, it as loadJob, j as recordKissRun, l as readIndexToken, lt as groupByLineage, m as reviewsUrl, mt as matchingWorkspaceIds, o as wasBlocked, pt as loadWorkspace, r as deleteReview$1, rt as isTerminal, s as runningJobs, st as progressSteps, t as relativeTime, tt as allJobs, u as bootstrapUrl, v as DEFAULT_WORKSPACE_PORT, vt as recentWorkspaces, w as writePortPreference, x as parseWorkspacePort, xt as saveWorkspace, y as MAX_WORKSPACE_PORT, z as telemetryUploadConsent, zt as CONFIG_PATH } from "./relative_time-CDvK_Zaj.js";
2
+ import { A as liveUpdateFor, B as recordTelemetryUploadConsent, Bt as displayRef, C as writePortPreference, Ct as recentWorkspaces, Dt as summarise, Et as saveWorkspace, F as reanalyseReview, Gt as writeAgentPreference, H as BUILD_VERSION, Ht as agentIdOf, I as runJob, Jt as AGENT_IDS, Kt as CONFIG_PATH, L as startJob, N as recordKissRun, Q as removeWorktree, Qt as detectAgents, S as statedWorkspacePort, T as updateReview, Tt as reviewedRepositories, U as PACKAGE_NAME, V as telemetryUploadConsent, Vt as git, Wt as readAgentPreference, X as MANAGED_ROOT, Xt as agentById, Yt as DEFAULT_AGENT_ID, Z as readMarker, _ as DEFAULT_WORKSPACE_PORT, a as liveJobsFor, at as loadJob, b as parseWorkspacePort, bt as positionInLineage, c as readIndexToken, d as ensureServer, dt as clearClaim, et as checkFreshness, ft as isClaimed, gt as lineageKeyFor, h as workspaceUrl, it as isTerminal, j as runningJobs, k as UpdateAlreadyRunningError, l as bootstrapUrl, lt as progressSteps, m as stopServer, mt as groupByLineage, o as wasBlocked, p as reviewsUrl, pt as followedRefName, q as forgeResolver, qt as STORE_ROOT, r as deleteReview$1, st as PHASES, t as relativeTime, tt as allJobs, ut as purposeOf, v as MAX_WORKSPACE_PORT, vt as loadWorkspace, y as MIN_WORKSPACE_PORT, yt as matchingWorkspaceIds, z as record, zt as resolveTarget } from "./relative_time-vn8Ex9E8.js";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -1267,6 +1267,19 @@ function agentUnusedByResume(agentId) {
1267
1267
  * link. Not rephrased here: an update that stopped early has a specific reason and the reviewer
1268
1268
  * gets the same reason whichever surface asked for it.
1269
1269
  */
1270
+ /**
1271
+ * Said before the update starts, so the command answers at once and the link is there to open while
1272
+ * it works. The same label the creation path uses, because it is the same offer.
1273
+ *
1274
+ * Not "continues if this terminal closes", which the creation path can say and this cannot: the
1275
+ * update runs in this process, so closing the terminal stops it.
1276
+ */
1277
+ function startedUpdate(url, acted) {
1278
+ return `${subjectLine(acted)}\nOpen workspace now ${url}`;
1279
+ }
1280
+ function updateAlreadyRunning(url, acted) {
1281
+ return `${subjectLine(acted)}\nThis review is already being updated. Follow it here ${url}`;
1282
+ }
1270
1283
  function updateFinished(outcome, url, acted) {
1271
1284
  return `${subjectLine(acted)}\n${outcome.message}\n${url}`;
1272
1285
  }
@@ -1572,10 +1585,10 @@ var GREEN = "\x1B[32m";
1572
1585
  var RED = "\x1B[31m";
1573
1586
  var RESET = "\x1B[0m";
1574
1587
  /** The header above the phases: what is being reviewed, and the facts about how. */
1575
- function header(target, colour) {
1588
+ function header(target, colour, purpose = "review") {
1576
1589
  const meta = `${target.sha.slice(0, 7)} · isolated checkout · ${target.originRepoPath}`;
1577
1590
  return [
1578
- `Reviewing ${target.branch}`,
1591
+ `${purpose === "update" ? "Updating" : "Reviewing"} ${target.branch}`,
1579
1592
  colour ? `${DIM}${meta}${RESET}` : meta,
1580
1593
  ""
1581
1594
  ];
@@ -1638,7 +1651,7 @@ function followJob(jobId, target, options = {}) {
1638
1651
  const paint = () => {
1639
1652
  const job = read(jobId);
1640
1653
  if (!job) return;
1641
- const lines = [...header(target, colour), ...phaseLines(job, colour)];
1654
+ const lines = [...header(job.target ?? target, colour, purposeOf(job)), ...phaseLines(job, colour)];
1642
1655
  const block = lines.join("\n");
1643
1656
  if (block === last) return;
1644
1657
  if (inPlace && drawn > 0) stream.write(`\u001b[${drawn}A\u001b[0J`);
@@ -1851,13 +1864,38 @@ async function operate(command, workspace) {
1851
1864
  if (command.operation === "reanalyse") {
1852
1865
  const agent = await chooseFor(command, workspace);
1853
1866
  if (!agent) return 1;
1854
- await reanalyseReview(workspace.id, agent.id);
1867
+ let stopReanalysis;
1868
+ try {
1869
+ await reanalyseReview(workspace.id, agent.id, (jobId) => {
1870
+ stopReanalysis = follow(jobId);
1871
+ });
1872
+ } finally {
1873
+ stopReanalysis?.();
1874
+ }
1855
1875
  const port = await ensureServer();
1856
1876
  out(reanalysed(agent.displayName, linkTo(port, workspace.id, workspace.token), actedOn(workspace)));
1857
1877
  return 0;
1858
1878
  }
1859
1879
  if (command.agentId) err(agentUnusedByUpdate(command.agentId));
1860
- const outcome = await updateReview(workspace);
1880
+ const url = linkTo(await ensureServer(), workspace.id, workspace.token);
1881
+ if (liveUpdateFor(workspace.id)) {
1882
+ out(updateAlreadyRunning(url, actedOn(workspace)));
1883
+ return 1;
1884
+ }
1885
+ out(startedUpdate(url, actedOn(workspace)));
1886
+ let stopUpdate;
1887
+ let outcome;
1888
+ try {
1889
+ outcome = await updateReview(workspace, { onStarted: (jobId) => {
1890
+ stopUpdate = follow(jobId);
1891
+ } });
1892
+ } catch (error) {
1893
+ if (!(error instanceof UpdateAlreadyRunningError)) throw error;
1894
+ out(updateAlreadyRunning(url, actedOn(workspace)));
1895
+ return 1;
1896
+ } finally {
1897
+ stopUpdate?.();
1898
+ }
1861
1899
  workspace.lastUpdate = outcome;
1862
1900
  saveWorkspace(workspace);
1863
1901
  record({
@@ -1872,10 +1910,23 @@ async function operate(command, workspace) {
1872
1910
  newIssues: outcome.counts?.newIssues ?? 0,
1873
1911
  questionsAnswered: outcome.counts?.questionsAnswered ?? 0
1874
1912
  });
1875
- out(updateFinished(outcome, linkTo(await ensureServer(), workspace.id, workspace.token), actedOn(workspace)));
1913
+ out(updateFinished(outcome, url, actedOn(workspace)));
1876
1914
  return 0;
1877
1915
  }
1878
1916
  /**
1917
+ * Draw a job's progress block, from what the job itself records about its target.
1918
+ *
1919
+ * From the record rather than the caller, for the reason `drive` gives: the job knows what it is
1920
+ * working on, and a second copy assembled here could disagree with it.
1921
+ */
1922
+ function follow(jobId) {
1923
+ return followJob(jobId, loadJob(jobId)?.target ?? {
1924
+ branch: "",
1925
+ sha: "",
1926
+ originRepoPath: ""
1927
+ });
1928
+ }
1929
+ /**
1879
1930
  * What to call the review an operation acted on.
1880
1931
  *
1881
1932
  * Read off the record rather than off the command, because the two are not the same question. The
@@ -1950,9 +2001,9 @@ async function drive(workspaceId, url, kissRun) {
1950
2001
  let finished;
1951
2002
  try {
1952
2003
  finished = await runJob(workspaceId, {}, kissRun ? "kiss" : "standard");
1953
- if (finished.phase === "failed") {
2004
+ if (finished.phase !== "done") {
1954
2005
  stop();
1955
- err(reviewFailed(finished.failure?.message ?? "The reason was not recorded.", await linkNow(workspace, url)));
2006
+ err(reviewFailed(failureMessage(finished), await linkNow(workspace, url)));
1956
2007
  return 1;
1957
2008
  }
1958
2009
  } catch (error) {
@@ -1973,10 +2024,26 @@ async function drive(workspaceId, url, kissRun) {
1973
2024
  out(kissFinished(link, answer.recommendations.length));
1974
2025
  return 0;
1975
2026
  }
2027
+ if (!done.session.result) {
2028
+ err(reviewFailed("The analysis finished without recording a review.", link));
2029
+ return 1;
2030
+ }
1976
2031
  out(reviewFinished(link, summarise(done), finished.conversation));
1977
2032
  return 0;
1978
2033
  }
1979
2034
  /**
2035
+ * Why a run ended, when it did not end by finishing.
2036
+ *
2037
+ * A job that recorded a failure says so in its own words. One that did not is a driver that went
2038
+ * away mid-phase -- killed, or crashed past the point where it could write -- and the honest thing
2039
+ * to say is which phase it was in, because that is the phase the next run picks up from.
2040
+ */
2041
+ function failureMessage(job) {
2042
+ if (job.failure?.message) return job.failure.message;
2043
+ const doing = PHASES[job.phase].doing;
2044
+ return `The reason was not recorded. It was ${doing.charAt(0).toLowerCase()}${doing.slice(1)} when it stopped.`;
2045
+ }
2046
+ /**
1980
2047
  * Every link this command prints, and the reason they are not plain workspace URLs.
1981
2048
  *
1982
2049
  * A reviewer who only ever runs this command was never sent to the reviews index, so their browser