prreviewbuddy 0.31.0 → 0.34.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/CHANGELOG.md CHANGED
@@ -1,5 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.0
4
+
5
+ **The Guide shows one theme at a time.** The workspace Guide used to stack every theme in one
6
+ column, so reading the second one meant scrolling past the whole of the first. The rail already
7
+ lists the themes, so it now drives the page: choosing one shows that theme on its own, and the
8
+ rail says how long each is likely to take. The theme tag on an issue or question opens its theme,
9
+ which it always named and never did. In a window too narrow for the rail, the same themes appear
10
+ above the theme itself.
11
+
12
+ A theme now reads in the order the work happens: why it matters, then what to check, then the files
13
+ it touches. That order is better on both surfaces, so the browser extension's Guide gets it too, and
14
+ is unchanged otherwise. Its parts stand apart rather than running together, and the theme itself is
15
+ no longer drawn as a card: with one theme on the page, the outline had nothing on the other side of
16
+ it. Nothing folds a theme away any more either, since there is now nothing behind it to reveal.
17
+
18
+ **The rail says what is left, in words.** A theme row gives the time it takes and, when something is
19
+ still open on it, how much: "~22 min, 1 of 2 issues left". That count used to be a bare digit whose
20
+ meaning only a screen reader was told. The file count has gone from the rail, which is what leaves
21
+ room to spell the rest out. Clicking the words "Review path" folds the list, not just the caret.
22
+
23
+ **The Overview's numbers read as numbers.** Each stat card leads with the figure and sets the label
24
+ beside it, so the four cards line up whether or not one of them carries a second line.
25
+
26
+ Two things that were wrong wherever you looked. The document pane was painted a slightly different
27
+ grey from the page behind it, leaving a seam down every view. And in a window too narrow for the
28
+ rail, the review was squeezed into the width the rail had vacated, with half the window empty beside
29
+ it.
30
+
31
+ The progress line no longer counts what it cannot change: completing a theme is part of guided
32
+ review, which the workspace does not have, so the Guide states the size of the job rather than
33
+ reporting nothing done.
34
+
35
+ ## 0.33.0
36
+
37
+ **A type scale, a palette for the review, and fewer boxes.** Text in the workspace now comes from
38
+ seven named sizes rather than fifteen ad-hoc ones, so a heading, a sentence, a piece of metadata and
39
+ a status chip differ by size instead of by weight, and ordinary text is ordinary weight again. The
40
+ review itself now has its own light palette: three greys where one grey had been written several
41
+ ways, a single hairline where six different border shades were drawing the same line, and one purple
42
+ where there were five. Seven panels that were a box drawn inside another box have lost their
43
+ outline and sit on the page as white cards against a slightly deeper background. Purple now marks
44
+ what you have selected, what has keyboard focus, and the main action on a page, so hovering a row no
45
+ longer shouts as loudly as choosing one. The browser extension is unchanged.
46
+
47
+ ## 0.32.1
48
+
49
+ **The rail on every workspace page.** The reviews list, Notifications and Settings now carry the
50
+ same rail a review does, so Reviews, Notifications and Settings are always one click away without
51
+ opening a review first. A review's rail keeps everything it had, with the workspace places beneath
52
+ it. The unread count sits beside Notifications wherever the rail is. In a window too narrow for the
53
+ rail, the same places appear in the header instead.
54
+
55
+ ## 0.32.0
56
+
57
+ **Notifications.** When a review, reanalysis, update or KISS analysis finishes or fails, PR Review
58
+ Buddy now records it on a new Notifications page in the workspace, so you can start a review, or a
59
+ batch of them, get on with something else, and see what finished when you come back. The reviews
60
+ list shows how many are unread, and every review's rail links to the page. Opening a notification
61
+ takes you to its review and marks it read, and Mark all read clears the rest; visiting the review
62
+ some other way leaves it unread. A review you stop with Ctrl-C is not recorded as a failure.
63
+ Notifications are kept for thirty days, on this machine only, and nothing about them is sent
64
+ anywhere.
65
+
66
+ **Toasts in the workspace.** While any workspace page is open, a notification also appears for a
67
+ few seconds in the bottom-right corner, with Open beside it. It stays while your pointer is on it,
68
+ and no more than three show at once: when a batch finishes together, the last one says how many more
69
+ there are, with View all. Letting a toast go or dismissing it leaves the notification unread on the
70
+ Notifications page.
71
+
72
+ **Closing the terminal no longer strands a review.** The CLI used to say a review continues if its
73
+ terminal closes. It did not: the review stopped with the terminal, and running the command again
74
+ said it was still being made for the next half hour. The message now asks you to keep the terminal
75
+ open, and running the same command again picks the review up at once from the step it reached.
76
+
77
+ Also fixed: a review picked up part way through its analysis showed that step as finished while it
78
+ was still running.
79
+
3
80
  ## 0.31.0
4
81
 
5
82
  **Review a single commit.** `prreviewbuddy review --commit <commit>` reviews the change one commit
@@ -2759,7 +2759,7 @@ async function resolveTarget(input) {
2759
2759
  ...named
2760
2760
  };
2761
2761
  }
2762
- const branch = await branchOf(root);
2762
+ const branch = await branchOf$1(root);
2763
2763
  let sha;
2764
2764
  try {
2765
2765
  sha = (await git(root, ["rev-parse", "HEAD"])).trim();
@@ -2941,7 +2941,7 @@ async function refuseEmptyTarget(target) {
2941
2941
  async function uncommittedIn(root, target) {
2942
2942
  if (target.pullRequest) return 0;
2943
2943
  try {
2944
- if ((await git(root, ["rev-parse", "HEAD"])).trim() !== target.sha || await branchOf(root) !== target.branch) return 0;
2944
+ if ((await git(root, ["rev-parse", "HEAD"])).trim() !== target.sha || await branchOf$1(root) !== target.branch) return 0;
2945
2945
  return (await git(root, ["status", "--porcelain"])).split("\n").filter((line) => line.trim()).length;
2946
2946
  } catch {
2947
2947
  return 0;
@@ -3023,7 +3023,7 @@ async function repositoryRoot(repoPath) {
3023
3023
  } catch {}
3024
3024
  throw new TargetError(`${repoPath} is not inside a git repository.`);
3025
3025
  }
3026
- async function branchOf(root) {
3026
+ async function branchOf$1(root) {
3027
3027
  let branch = "";
3028
3028
  try {
3029
3029
  branch = (await git(root, [
@@ -4766,6 +4766,18 @@ function isClaimed(jobId, now = Date.now()) {
4766
4766
  return liveClaim(jobId, now) !== null;
4767
4767
  }
4768
4768
  /**
4769
+ * Whether a driver took this job and is no longer there to finish it.
4770
+ *
4771
+ * The claim file is the evidence. A job minted a moment ago and not yet driven has no claim at all,
4772
+ * and a driver that finished released its claim on the way out, so a claim that is still on disk
4773
+ * with nobody live behind it can only belong to a run that ended without finishing: a terminal that
4774
+ * closed, a process that was killed. Answered at once rather than after the sweep's half hour,
4775
+ * because the reviewer asking is usually the one who just closed that terminal.
4776
+ */
4777
+ function driverGone(jobId, now = Date.now()) {
4778
+ return readClaim(jobId) !== null && liveClaim(jobId, now) === null;
4779
+ }
4780
+ /**
4769
4781
  * Take the claim on a job, or return null because another driver holds it.
4770
4782
  *
4771
4783
  * Two retries fired together, a retry racing the run `startJob` already kicked off, or a retry
@@ -5034,7 +5046,7 @@ function deleteJob(id) {
5034
5046
  }
5035
5047
  function advance(job, phase, progress) {
5036
5048
  const leaving = job.phase;
5037
- const completed = WORK_PHASES.includes(leaving) && !job.completed.includes(leaving) ? [...job.completed, leaving] : job.completed;
5049
+ const completed = WORK_PHASES.includes(leaving) && leaving !== phase && !job.completed.includes(leaving) ? [...job.completed, leaving] : job.completed;
5038
5050
  return {
5039
5051
  ...job,
5040
5052
  phase,
@@ -9493,6 +9505,229 @@ async function withUsageRecorded(workspaceId, kind, env, body, runId, requestedM
9493
9505
  }
9494
9506
  }
9495
9507
  //#endregion
9508
+ //#region ../../packages/review-harness/src/workspace/notifications.ts
9509
+ /**
9510
+ * What happened while the reviewer was looking at something else, kept until they have seen it.
9511
+ *
9512
+ * **One file per notification, not one list.** Notifications are written by whichever process
9513
+ * drove the work to its end: the CLI for a review typed at a terminal, the workspace server for one
9514
+ * started from the page, the sweep for one whose driver died. A single `notifications.json` read,
9515
+ * modified and written by several processes loses whichever entry was written second, and the lost
9516
+ * one is by definition something the reviewer was not watching. `jobs/` is laid out the same way for
9517
+ * the same reason.
9518
+ *
9519
+ * **Created once, with `wx`.** The id is derived from the transition it records, so a second attempt
9520
+ * to record the same transition fails at the filesystem and returns null, and a caller that only
9521
+ * delivers what it created cannot deliver twice. Nothing that reads a job ever creates one: a page
9522
+ * reloading, a client polling or a review being reopened cannot announce anything.
9523
+ *
9524
+ * Imports only `store_root`, for the cycle `queue.ts` describes.
9525
+ */
9526
+ var NOTIFICATIONS_DIR = join(STORE_ROOT, "notifications");
9527
+ /** As long as a review is kept, so the list never names a review nobody can open for that reason. */
9528
+ var NOTIFICATION_RETENTION_MS = 2592e6;
9529
+ var OPERATION_NAMES = {
9530
+ review: "Review",
9531
+ reanalysis: "Reanalysis",
9532
+ update: "Review update",
9533
+ kiss: "KISS analysis"
9534
+ };
9535
+ /** What happened, in the words the list uses: `Review ready`, `Review update failed`. */
9536
+ function eventName(notification) {
9537
+ return `${OPERATION_NAMES[notification.operation]} ${notification.outcome}`;
9538
+ }
9539
+ /** The whole line, for anything that shows one event on its own: what happened, then to what. */
9540
+ function headline(notification) {
9541
+ return `${eventName(notification)} · ${notification.subject}`;
9542
+ }
9543
+ var OPERATIONS = [
9544
+ "review",
9545
+ "reanalysis",
9546
+ "update",
9547
+ "kiss"
9548
+ ];
9549
+ var OUTCOMES = ["ready", "failed"];
9550
+ /** An id is a file name, so it is held to a shape that cannot leave the directory. */
9551
+ var ID = /^[A-Za-z0-9-]{1,200}$/;
9552
+ function pathOf(id) {
9553
+ return join(NOTIFICATIONS_DIR, `${id}.json`);
9554
+ }
9555
+ /**
9556
+ * Record one event, or return null because it is already recorded or could not be written.
9557
+ *
9558
+ * Never throws. A notification that could not be written is a notification the reviewer does not
9559
+ * get, which is a smaller loss than the review job that was being announced.
9560
+ */
9561
+ function createNotification(input, now = Date.now()) {
9562
+ if (!ID.test(input.id)) return null;
9563
+ const record = {
9564
+ version: 1,
9565
+ kind: "analysis",
9566
+ ...input,
9567
+ createdAt: now
9568
+ };
9569
+ try {
9570
+ mkdirSync(NOTIFICATIONS_DIR, { recursive: true });
9571
+ writeFileSync(pathOf(input.id), JSON.stringify(record), {
9572
+ flag: "wx",
9573
+ mode: 384
9574
+ });
9575
+ return record;
9576
+ } catch {
9577
+ return null;
9578
+ }
9579
+ }
9580
+ /** Every notification, newest first. Unreadable and malformed files are skipped, not reported. */
9581
+ function listNotifications() {
9582
+ let names;
9583
+ try {
9584
+ names = readdirSync(NOTIFICATIONS_DIR);
9585
+ } catch {
9586
+ return [];
9587
+ }
9588
+ const found = [];
9589
+ for (const name of names) {
9590
+ if (!name.endsWith(".json")) continue;
9591
+ const record = readNotification(name.slice(0, -5));
9592
+ if (record) found.push(record);
9593
+ }
9594
+ return found.sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? -1 : 1));
9595
+ }
9596
+ function readNotification(id) {
9597
+ if (!ID.test(id)) return null;
9598
+ try {
9599
+ const parsed = JSON.parse(readFileSync(pathOf(id), "utf8"));
9600
+ return isNotification(parsed) && parsed.id === id ? parsed : null;
9601
+ } catch {
9602
+ return null;
9603
+ }
9604
+ }
9605
+ function unreadCount() {
9606
+ return listNotifications().filter((notification) => notification.readAt === void 0).length;
9607
+ }
9608
+ /**
9609
+ * Mark these read, and say how many changed.
9610
+ *
9611
+ * Only ever by something the reviewer did to the notification itself: opening it, or one of the two
9612
+ * mark buttons. Opening the review it points at is not the same act and does not come through here.
9613
+ */
9614
+ function markRead(ids, now = Date.now()) {
9615
+ let changed = 0;
9616
+ for (const id of ids) {
9617
+ const record = readNotification(id);
9618
+ if (!record || record.readAt !== void 0) continue;
9619
+ try {
9620
+ writeFileSync(pathOf(id), JSON.stringify({
9621
+ ...record,
9622
+ readAt: now
9623
+ }), { mode: 384 });
9624
+ changed += 1;
9625
+ } catch {}
9626
+ }
9627
+ return changed;
9628
+ }
9629
+ function markAllRead(now = Date.now()) {
9630
+ return markRead(listNotifications().filter((notification) => notification.readAt === void 0).map((notification) => notification.id), now);
9631
+ }
9632
+ /**
9633
+ * Forget what is older than the retention, and anything past the cap, oldest first.
9634
+ *
9635
+ * Files that do not parse are removed once they are as old as the retention, judged by their
9636
+ * modification time, so a stray file neither lingers for ever nor disappears the moment it is seen.
9637
+ */
9638
+ function pruneNotifications(now = Date.now()) {
9639
+ let names;
9640
+ try {
9641
+ names = readdirSync(NOTIFICATIONS_DIR);
9642
+ } catch {
9643
+ return 0;
9644
+ }
9645
+ const kept = listNotifications();
9646
+ const keep = new Set(kept.filter((notification) => now - notification.createdAt < NOTIFICATION_RETENTION_MS).slice(0, 200).map((notification) => `${notification.id}.json`));
9647
+ const valid = new Set(kept.map((notification) => `${notification.id}.json`));
9648
+ let removed = 0;
9649
+ for (const name of names) {
9650
+ if (!name.endsWith(".json") || keep.has(name)) continue;
9651
+ const path = join(NOTIFICATIONS_DIR, name);
9652
+ try {
9653
+ if (!valid.has(name) && now - statSync(path).mtimeMs < 2592e6) continue;
9654
+ unlinkSync(path);
9655
+ removed += 1;
9656
+ } catch {}
9657
+ }
9658
+ return removed;
9659
+ }
9660
+ function isNotification(value) {
9661
+ if (!value || typeof value !== "object") return false;
9662
+ const record = value;
9663
+ return record.version === 1 && record.kind === "analysis" && typeof record.id === "string" && OPERATIONS.includes(record.operation) && OUTCOMES.includes(record.outcome) && typeof record.reviewId === "string" && typeof record.subject === "string" && (record.branch === void 0 || typeof record.branch === "string") && typeof record.repo === "string" && typeof record.createdAt === "number" && (record.readAt === void 0 || typeof record.readAt === "number");
9664
+ }
9665
+ //#endregion
9666
+ //#region ../../packages/review-harness/src/workspace/announce.ts
9667
+ /**
9668
+ * Tell the reviewer a job ended, once, after the ending has been written.
9669
+ *
9670
+ * Called by the three places that write an ending: `drive` in `review_job.ts`, `updateReview`, and
9671
+ * the sweep for a job whose driver died. Each calls it with the record it has just saved, in the
9672
+ * process that saved it, so the answer never depends on who happens to be looking.
9673
+ *
9674
+ * The id is the job and the moment it ended, so two calls about the same ending make one record,
9675
+ * and a job picked up again that ends later is a new event with its own.
9676
+ *
9677
+ * The record is the event. The Notifications page and the workspace's toasts both read it. Any
9678
+ * delivery that has to be pushed rather than read belongs here, after `createNotification`, and only
9679
+ * for a record this call created, which carries the once-only guarantee over to it for free. Desktop
9680
+ * delivery is written that way and parked outside the product: see `desktop_preference.ts`.
9681
+ *
9682
+ * Never throws. This runs on the success path of a review that has already been saved, and the
9683
+ * accounting must not break the thing it accounts for.
9684
+ */
9685
+ function announceJob(job) {
9686
+ try {
9687
+ if (!isTerminal(job) || job.failure?.stopped || purposeOf(job) === "ask") return;
9688
+ const workspace = loadWorkspace(job.workspaceId);
9689
+ if (!workspace) return;
9690
+ const branch = branchOf(workspace);
9691
+ createNotification({
9692
+ id: `${job.id}-${endedAt(job)}`,
9693
+ operation: operationOf(job),
9694
+ outcome: job.phase === "done" ? "ready" : "failed",
9695
+ reviewId: workspace.id,
9696
+ subject: subjectOf(workspace),
9697
+ ...branch ? { branch } : {},
9698
+ repo: basename(workspace.repoPath)
9699
+ });
9700
+ } catch {}
9701
+ }
9702
+ function operationOf(job) {
9703
+ if (job.reviewType === "kiss") return "kiss";
9704
+ if (purposeOf(job) === "update") return "update";
9705
+ return job.reanalysis ? "reanalysis" : "review";
9706
+ }
9707
+ /**
9708
+ * What was reviewed, as the reviewer would name it, and nothing that locates it on disk.
9709
+ *
9710
+ * A pull request by its number, spelled the way its forge spells it; a commit by its short sha; a
9711
+ * branch by the ref it follows, which is the same rule the reviews list and the CLI use.
9712
+ */
9713
+ function subjectOf(workspace) {
9714
+ const changeSet = workspace.changeSet;
9715
+ if (changeSet.targetType === "commit") return `commit ${changeSet.data.pullRequest.head.sha.slice(0, 7)}`;
9716
+ const number = changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number;
9717
+ if (number) return /\/merge_requests\//.test(workspace.prContext?.pullRequest.url ?? "") ? `MR !${number}` : `PR #${number}`;
9718
+ return followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef;
9719
+ }
9720
+ /**
9721
+ * The branch beside a pull request's number, which alone does not say what the change is. Nothing
9722
+ * for a branch review, whose subject already is the branch, or for a commit, which has none.
9723
+ */
9724
+ function branchOf(workspace) {
9725
+ const changeSet = workspace.changeSet;
9726
+ if (changeSet.targetType === "commit") return void 0;
9727
+ if (!(changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number)) return void 0;
9728
+ return (followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef) || void 0;
9729
+ }
9730
+ //#endregion
9496
9731
  //#region ../../packages/review-harness/src/telemetry/consent.ts
9497
9732
  /**
9498
9733
  * Who this machine is counted as, and what it has agreed to.
@@ -9815,6 +10050,7 @@ async function reanalyseReview(workspaceId, agentId, onStarted, requestedModel)
9815
10050
  target,
9816
10051
  env: agent.captureEnv(),
9817
10052
  purpose: "review",
10053
+ reanalysis: true,
9818
10054
  ...model ? { requestedModel: model } : {}
9819
10055
  });
9820
10056
  saveJob(job);
@@ -10030,6 +10266,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
10030
10266
  else await applyGuide(job, workspaceId, payload, observedModel);
10031
10267
  job = update(job, "done");
10032
10268
  finished(job, workspaceId, startedAt, true);
10269
+ announceJob(job);
10033
10270
  return job;
10034
10271
  } catch (error) {
10035
10272
  if (error instanceof AgentCancelledError || signal?.aborted) {
@@ -10043,6 +10280,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
10043
10280
  job = recordFailure(job, explain(job, raw), driver, kind, raw);
10044
10281
  if (job.reviewType === "kiss") failCurrentKissRun(workspaceId, job, job.failure?.message ?? raw, kind, error instanceof KissRejected ? error.answer : void 0);
10045
10282
  finished(job, workspaceId, startedAt, false);
10283
+ announceJob(job);
10046
10284
  return job;
10047
10285
  }
10048
10286
  }
@@ -11221,10 +11459,14 @@ async function updateReview(workspace, runners = {}) {
11221
11459
  const step = (phase) => saveJob(advance(current(), phase));
11222
11460
  try {
11223
11461
  const outcome = await bringForward(workspace, runners, reviewedSha, held, step);
11224
- saveJob(outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message));
11462
+ const ended = outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
11463
+ saveJob(ended);
11464
+ announceJob(ended);
11225
11465
  return outcome;
11226
11466
  } catch (error) {
11227
- saveJob(fail(current(), error instanceof Error ? error.message : String(error)));
11467
+ const failed = fail(current(), error instanceof Error ? error.message : String(error));
11468
+ saveJob(failed);
11469
+ announceJob(failed);
11228
11470
  throw error;
11229
11471
  } finally {
11230
11472
  held.claim.release();
@@ -11814,4 +12056,4 @@ function lineageIds(id) {
11814
12056
  return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
11815
12057
  }
11816
12058
  //#endregion
11817
- export { fillFileUrlTemplate as $, writeAgentPreference as $t, removeWorktree as A, workspaceRevision as At, workspaceUrl as B, isQuestionOutstanding as Bt, recordTelemetryUploadConsent as C, previousKissResult as Ct, ReviewBeingDeletedError as D, saveWorkspace as Dt, EXPLAIN_SIMPLY_PROMPT as E, reviewedRepositories as Et, clearServerRecord as F, queueStamp as Ft, DEFAULT_WORKSPACE_PORT as G, describeAuthorship as Gt, BUILD_VERSION as H, questionsOutstanding as Ht, ensureServer as I, readQueue as It, parseWorkspacePort as J, git as Jt, MAX_WORKSPACE_PORT as K, resolveTarget as Kt, readServerRecord as L, writeQueue as Lt, relativeTime as M, dequeueOnComplete as Mt, readIndexToken as N, queueAdd as Nt, MANAGED_ROOT as O, summarise as Ot, bootstrapUrl as P, queueRemove as Pt, forgeResolver as Q, readAgentPreference as Qt, reviewsUrl as R, doneVerb as Rt, record as S, positionInLineage as St, withUsageRecorded as T, reviewedCommit as Tt, PACKAGE_NAME as U, reviewerDispositions as Ut, writeServerRecord as V, issuesOutstanding as Vt, feedbackUrl as W, processDiscussion as Wt, statedWorkspacePort as X, resolveModel as Xt, resolveWorkspacePort as Y, resolveAgent as Yt, writePortPreference as Z, clearAgentPreference as Zt, startKissJob as _, latestKissRun as _t, wasBlocked as a, agentFor as an, isTerminal as at, startJob as b, loadWorkspace as bt, updateReview as c, AgentUnavailableError as cn, PHASES as ct, askCheckout as d, purposeOf as dt, CONFIG_PATH as en, checkCodeFreshness as et, UpdateAlreadyRunningError as f, clearClaim as ft, recordKissRun as g, indexReviewGroups as gt, agentEnvOf as h, groupByLineage as ht, liveJobsFor as i, agentById as in, fail as it, modelHelpLines as j, workspaceStamps as jt, readMarker as k, touchWorkspace as kt, refreshPrContext as l, phasesFor as lt, runningJobs as m, followedRefName as mt, deleteReview as n, AGENT_IDS as nn, allJobs as nt, discardJob as o, detectAgents as on, loadJob as ot, liveUpdateFor as p, isClaimed as pt, MIN_WORKSPACE_PORT as q, displayRef as qt, lineageIds as r, DEFAULT_AGENT_ID as rn, endedAt as rt, RefreshUnavailableError as s, AgentCancelledError as sn, saveJob as st, deleteLineage as t, STORE_ROOT as tn, checkReview as tt, isUnchanged as u, progressSteps as ut, reanalyseReview as v, lineageKeyFor as vt, telemetryUploadConsent as w, recentWorkspaces as wt, readEvents as x, matchingWorkspaceIds as xt, runJob as y, lineagePosition as yt, stopServer as z, isIssueOutstanding as zt };
12059
+ export { feedbackUrl as $, reviewerDispositions as $t, markRead as A, loadWorkspace as At, modelHelpLines as B, workspaceRevision as Bt, recordTelemetryUploadConsent as C, isClaimed as Ct, headline as D, latestKissRun as Dt, eventName as E, indexReviewGroups as Et, EXPLAIN_SIMPLY_PROMPT as F, reviewedCommit as Ft, ensureServer as G, queueStamp as Gt, readIndexToken as H, dequeueOnComplete as Ht, ReviewBeingDeletedError as I, reviewedRepositories as It, stopServer as J, doneVerb as Jt, readServerRecord as K, readQueue as Kt, MANAGED_ROOT as L, saveWorkspace as Lt, readNotification as M, positionInLineage as Mt, unreadCount as N, previousKissResult as Nt, listNotifications as O, lineageKeyFor as Ot, withUsageRecorded as P, recentWorkspaces as Pt, PACKAGE_NAME as Q, questionsOutstanding as Qt, readMarker as R, summarise as Rt, record as S, driverGone as St, announceJob as T, groupByLineage as Tt, bootstrapUrl as U, queueAdd as Ut, relativeTime as V, workspaceStamps as Vt, clearServerRecord as W, queueRemove as Wt, writeServerRecord as X, isQuestionOutstanding as Xt, workspaceUrl as Y, isIssueOutstanding as Yt, BUILD_VERSION as Z, issuesOutstanding as Zt, startKissJob as _, AgentCancelledError as _n, PHASES as _t, wasBlocked as a, resolveAgent as an, statedWorkspacePort as at, startJob as b, purposeOf as bt, updateReview as c, readAgentPreference as cn, fillFileUrlTemplate as ct, askCheckout as d, STORE_ROOT as dn, allJobs as dt, processDiscussion as en, DEFAULT_WORKSPACE_PORT as et, UpdateAlreadyRunningError as f, AGENT_IDS as fn, endedAt as ft, recordKissRun as g, detectAgents as gn, saveJob as gt, agentEnvOf as h, agentFor as hn, loadJob as ht, liveJobsFor as i, git as in, resolveWorkspacePort as it, pruneNotifications as j, matchingWorkspaceIds as jt, markAllRead as k, lineagePosition as kt, refreshPrContext as l, writeAgentPreference as ln, checkCodeFreshness as lt, runningJobs as m, agentById as mn, isTerminal as mt, deleteReview as n, resolveTarget as nn, MIN_WORKSPACE_PORT as nt, discardJob as o, resolveModel as on, writePortPreference as ot, liveUpdateFor as p, DEFAULT_AGENT_ID as pn, fail as pt, reviewsUrl as q, writeQueue as qt, lineageIds as r, displayRef as rn, parseWorkspacePort as rt, RefreshUnavailableError as s, clearAgentPreference as sn, forgeResolver as st, deleteLineage as t, describeAuthorship as tn, MAX_WORKSPACE_PORT as tt, isUnchanged as u, CONFIG_PATH as un, checkReview as ut, reanalyseReview as v, AgentUnavailableError as vn, phasesFor as vt, telemetryUploadConsent as w, followedRefName as wt, readEvents as x, clearClaim as xt, runJob as y, progressSteps as yt, removeWorktree as z, touchWorkspace as zt };
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $t as writeAgentPreference, A as removeWorktree, B as workspaceUrl, C as recordTelemetryUploadConsent, Dt as saveWorkspace, Et as reviewedRepositories, G as DEFAULT_WORKSPACE_PORT, H as BUILD_VERSION, I as ensureServer, J as parseWorkspacePort, Jt as git, K as MAX_WORKSPACE_PORT, Kt as resolveTarget, M as relativeTime, N as readIndexToken, O as MANAGED_ROOT, Ot as summarise, P as bootstrapUrl, Q as forgeResolver, Qt as readAgentPreference, R as reviewsUrl, S as record$1, St as positionInLineage, U as PACKAGE_NAME, X as statedWorkspacePort, Xt as resolveModel, Yt as resolveAgent, Z as writePortPreference, a as wasBlocked, at as isTerminal, b as startJob, bt as loadWorkspace, c as updateReview, ct as PHASES, dt as purposeOf, en as CONFIG_PATH, f as UpdateAlreadyRunningError, ft as clearClaim, g as recordKissRun, ht as groupByLineage, i as liveJobsFor, in as agentById, j as modelHelpLines, k as readMarker, m as runningJobs, mt as followedRefName, n as deleteReview$1, nn as AGENT_IDS, nt as allJobs, on as detectAgents, ot as loadJob, p as liveUpdateFor, pt as isClaimed, q as MIN_WORKSPACE_PORT, qt as displayRef, rn as DEFAULT_AGENT_ID, tn as STORE_ROOT, tt as checkReview, ut as progressSteps, v as reanalyseReview, vt as lineageKeyFor, w as telemetryUploadConsent, wt as recentWorkspaces, xt as matchingWorkspaceIds, y as runJob, z as stopServer } from "./delete_review-C2wax0ED.js";
2
+ import { At as loadWorkspace, B as modelHelpLines, C as recordTelemetryUploadConsent, Ct as isClaimed, G as ensureServer, H as readIndexToken, It as reviewedRepositories, J as stopServer, L as MANAGED_ROOT, Lt as saveWorkspace, Mt as positionInLineage, Ot as lineageKeyFor, Pt as recentWorkspaces, Q as PACKAGE_NAME, R as readMarker, Rt as summarise, S as record$1, St as driverGone, Tt as groupByLineage, U as bootstrapUrl, V as relativeTime, Y as workspaceUrl, Z as BUILD_VERSION, _t as PHASES, a as wasBlocked, an as resolveAgent, at as statedWorkspacePort, b as startJob, bt as purposeOf, c as updateReview, cn as readAgentPreference, dn as STORE_ROOT, dt as allJobs, et as DEFAULT_WORKSPACE_PORT, f as UpdateAlreadyRunningError, fn as AGENT_IDS, g as recordKissRun, gn as detectAgents, ht as loadJob, i as liveJobsFor, in as git, jt as matchingWorkspaceIds, ln as writeAgentPreference, m as runningJobs, mn as agentById, mt as isTerminal, n as deleteReview$1, nn as resolveTarget, nt as MIN_WORKSPACE_PORT, on as resolveModel, ot as writePortPreference, p as liveUpdateFor, pn as DEFAULT_AGENT_ID, q as reviewsUrl, rn as displayRef, rt as parseWorkspacePort, st as forgeResolver, tt as MAX_WORKSPACE_PORT, un as CONFIG_PATH, ut as checkReview, v as reanalyseReview, w as telemetryUploadConsent, wt as followedRefName, xt as clearClaim, y as runJob, yt as progressSteps, z as removeWorktree } from "./delete_review-TNIFNct3.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";
@@ -705,11 +705,17 @@ function madeAgainst(requested) {
705
705
  const wanted = displayRef(requested);
706
706
  return (review) => review.baseRef !== "" && displayRef(review.baseRef) === wanted;
707
707
  }
708
- /** Whether the job behind a stored review is still working. A review with no job never was. */
708
+ /**
709
+ * Whether the job behind a stored review is still working. A review with no job never was.
710
+ *
711
+ * A job whose driver has gone is not working, whatever phase its record was left at. Calling it in
712
+ * progress told the reviewer who had just closed a terminal that there was "nothing to do but wait",
713
+ * for a process that no longer existed, until the sweep called it failed half an hour later.
714
+ */
709
715
  function isRunning(jobId) {
710
716
  if (!jobId) return false;
711
717
  const job = loadJob(jobId);
712
- return !!job && !isTerminal(job);
718
+ return !!job && !isTerminal(job) && !driverGone(job.id);
713
719
  }
714
720
  /**
715
721
  * What the review is of, as the clause both surfaces hang off "Made two hours ago".
@@ -1264,7 +1270,7 @@ function unknownAgent(id, known) {
1264
1270
  * mistake; `Open workspace now` and `Review ready` read as a beginning and an end.
1265
1271
  */
1266
1272
  function startedReview(url, _target) {
1267
- return [`Open workspace now ${url}`, "The review continues if this terminal closes."].join("\n");
1273
+ return [`Open workspace now ${url}`, "Keep this terminal open while it runs. If it closes, run the same command again to carry on."].join("\n");
1268
1274
  }
1269
1275
  /**
1270
1276
  * The one-review-per-branch rule, said to a person rather than to a session.
@@ -1517,8 +1523,9 @@ function commitReviewCannotUpdate(sha) {
1517
1523
  * Said before the update starts, so the command answers at once and the link is there to open while
1518
1524
  * it works. The same label the creation path uses, because it is the same offer.
1519
1525
  *
1520
- * Not "continues if this terminal closes", which the creation path can say and this cannot: the
1521
- * update runs in this process, so closing the terminal stops it.
1526
+ * Without the creation path's line about carrying on if the terminal closes. An update runs in this
1527
+ * process and ends with it, and running the command again starts the update over rather than
1528
+ * picking one up, because `liveUpdateFor` reads a dead driver as nothing running.
1522
1529
  */
1523
1530
  function startedUpdate(url, acted) {
1524
1531
  return `${subjectLine(acted)}\nOpen workspace now ${url}`;
@@ -2178,8 +2185,10 @@ function describe(deps, id) {
2178
2185
  * One thing differs, and it is a property of the surface rather than of the review. An MCP tool
2179
2186
  * returns to a session that must not sit and wait, so it fires `runJob` and returns the link at
2180
2187
  * once. A person who typed a command is already waiting, so this awaits the job and reports what
2181
- * it is doing. Either way the job is durable and either way the link works from the moment it is
2182
- * printed, so a terminal closed halfway through loses nothing.
2188
+ * it is doing. Either way the job is durable and the link works from the moment it is printed. The
2189
+ * job does not outlive this process, though: the agent shares the terminal's process group, so a
2190
+ * terminal closed halfway through ends both. What it loses is time, not work. The claim it leaves
2191
+ * behind is `driverGone`, and running the command again carries on from the phase it reached.
2183
2192
  *
2184
2193
  * **Two axes, and this file is where they meet.** A command names a target and an operation, and
2185
2194
  * the operations are `update` and `reanalyse`, which were `prreviewbuddy update` until 0.15.0.
@@ -2480,15 +2489,19 @@ function makeCommandFor(command) {
2480
2489
  if (command.commitRef) return `prreviewbuddy review --commit ${command.commitRef}`;
2481
2490
  return "prreviewbuddy review";
2482
2491
  }
2492
+ /** Why an unfinished review is being picked up, when nothing recorded a failure because nothing could. */
2493
+ var DRIVER_GONE = "the command that was running it ended before it finished, most often because its terminal closed.";
2483
2494
  function failureOf(workspaceId) {
2484
2495
  const workspace = loadWorkspace(workspaceId);
2485
2496
  if (!workspace?.jobId) return null;
2486
2497
  const job = loadJob(workspace.jobId);
2487
- if (!job || !isTerminal(job) || job.phase !== "failed") return null;
2498
+ if (!job) return null;
2499
+ const abandoned = !isTerminal(job) && driverGone(job.id);
2500
+ if (!abandoned && (!isTerminal(job) || job.phase !== "failed")) return null;
2488
2501
  if (isClaimed(job.id)) return null;
2489
2502
  return {
2490
- message: job.failure?.message ?? "The reason was not recorded.",
2491
- stopped: job.failure?.stopped === true,
2503
+ message: abandoned ? DRIVER_GONE : job.failure?.message ?? "The reason was not recorded.",
2504
+ stopped: !abandoned && job.failure?.stopped === true,
2492
2505
  agentId: job.env.agentId ?? DEFAULT_AGENT_ID,
2493
2506
  ...job.requestedModel ? { requestedModel: job.requestedModel } : {}
2494
2507
  };
@@ -3202,8 +3215,7 @@ async function cleanUp(options) {
3202
3215
  *
3203
3216
  * It stops the workspace server, which the help screen says on purpose there is no command to do.
3204
3217
  * That is not a hole in the rule but the end of it: "no command to stop the server" exists so that
3205
- * a review outlives the terminal that asked for it, and there are no reviews left to outlive
3206
- * anything here.
3218
+ * the workspace outlives the terminal that opened it, and there are no reviews left to serve here.
3207
3219
  *
3208
3220
  * What it does not do is remove its own npm package. A running binary deleting the install it is
3209
3221
  * executing from depends on which package manager put it there and fails in ways that leave a