prreviewbuddy 0.63.0 → 0.69.1

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/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $n as loadWorkspace, $r as detectAgents, $t as bootstrapUrl, A as reanalyseReview, B as telemetryUploadEnabled, Bn as clearClaim, En as checkReview, F as record$1, Fn as PHASES, Gn as groupByLineage, Gr as readAgentPreference, Hn as isClaimed, Hr as resolveAgent, I as stamp, Ir as WITHHELD_REASON, Jr as STORE_ROOT, Kr as writeAgentPreference, Kt as modelHelpLines, L as deliver, Lr as kissOutcome, M as startJob, Mn as isTerminal, N as append, Nn as loadJob, Qt as readIndexToken, R as recordTelemetryUploadConsent, Rn as progressSteps, Rr as resolveTarget, Ur as resolveModel, Ut as readMarker, Vn as driverGone, Vr as git, Wn as followedRefName, Wt as removeWorktree, Xr as DEFAULT_AGENT_ID, Xt as queueForReview, Yr as AGENT_IDS, Zn as lineageKeyFor, Zr as agentById, _ as liveUpdateFor, a as wasBlocked, ar as recentWorkspaces, b as recordKissRun, cn as workspaceUrl, cr as summarise, d as mergeUpdateInto, dn as PACKAGE_NAME, er as matchingWorkspaceIds, f as updateReview, g as UpdateAlreadyRunningError, gn as parseWorkspacePort, hn as MIN_WORKSPACE_PORT, i as liveJobsFor, j as runJob, kn as allJobs, mn as MAX_WORKSPACE_PORT, n as deleteReview$1, nr as positionInLineage, pn as DEFAULT_WORKSPACE_PORT, qr as CONFIG_PATH, qt as relativeTime, rn as reviewsUrl, sn as stopServer, sr as reviewedRepositories, tn as ensureServer, tr as mutateWorkspace, un as BUILD_VERSION, v as runningJobs, vn as statedWorkspacePort, xn as forgeResolver, yn as writePortPreference, z as telemetryUploadConsent, zn as purposeOf, zr as displayRef, zt as MANAGED_ROOT } from "./delete_review-CeJo9t7h.js";
2
+ import { $ as MAX_WORKSPACE_PORT, $r as resolveAgent, A as relativeTime, B as readIndexToken, Dn as MANAGED_ROOT, F as runBatch, Fn as describeSubject, G as reviewsUrl, Gn as loadJob, In as findExistingReview, Jr as resolveTarget, Kr as WITHHELD_REASON, Mn as removeWorktree, Pn as describeMovement, Q as DEFAULT_WORKSPACE_PORT, Qn as clearClaim, Qr as git, R as queueForReview, Tn as modelHelpLines, U as ensureServer, V as bootstrapUrl, Vn as allJobs, X as workspaceUrl, Xn as progressSteps, Y as stopServer, Zn as purposeOf, _ as liveUpdateFor, _r as reviewedRepositories, _t as telemetryUploadConsent, a as wasBlocked, ai as STORE_ROOT, b as recordKissRun, bn as forgeResolver, bt as PACKAGE_NAME, ct as reanalyseReview, d as mergeUpdateInto, dr as mutateWorkspace, dt as append, ei as resolveModel, et as MIN_WORKSPACE_PORT, f as updateReview, fr as positionInLineage, g as UpdateAlreadyRunningError, gt as recordTelemetryUploadConsent, hr as recentWorkspaces, ht as deliver, i as liveJobsFor, ii as CONFIG_PATH, it as writePortPreference, j as batchIdentity, jn as readMarker, li as detectAgents, lr as loadWorkspace, mt as stamp, n as deleteReview$1, ni as readAgentPreference, oi as AGENT_IDS, ot as prepareReview, pt as record$1, qn as PHASES, qr as kissOutcome, ri as writeAgentPreference, rt as statedWorkspacePort, si as agentById, st as settleReview, tr as followedRefName, tt as parseWorkspacePort, ur as matchingWorkspaceIds, ut as startJob, v as runningJobs, vr as summarise, vt as telemetryUploadEnabled, yt as BUILD_VERSION } from "./delete_review-CRPRIRKg.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";
@@ -605,217 +605,6 @@ async function pick(ask, options) {
605
605
  return null;
606
606
  }
607
607
  //#endregion
608
- //#region ../../packages/review-harness/src/workspace/existing_review.ts
609
- /**
610
- * Is there already a review of what I am standing in?
611
- *
612
- * `prreviewbuddy review` used to mean "make another review", every time, which made the most-typed
613
- * command in the product the one way to end up with two reviews of one branch and no idea which was
614
- * which. It now means **"give me the review of this branch"**: it creates one the first time and
615
- * hands back the existing one afterwards. Starting again is `--fresh`, deliberately its own flag.
616
- *
617
- * Three properties matter here and all of them are about the answer being trustworthy rather than
618
- * merely fast:
619
- *
620
- * - **It runs before the change set is built.** Building one reads every changed file and its
621
- * patch, which on a large change is seconds of work thrown away the moment the answer turns out
622
- * to be "you already have this". The lookup is a branch name and a directory.
623
- * - **It says how old the answer is.** Reviews are kept for thirty days, so on a long-lived branch
624
- * the honest answer is often "yes, from three weeks and fifty commits ago". Handing back a link
625
- * with no age attached would turn a helpful reply into a trap.
626
- * - **It answers the question that was asked.** A branch is not the whole of what a review is of:
627
- * `--base develop` and `--base main` are two different comparisons of the same commits. Keying
628
- * only on the branch meant a reviewer who named a base was handed a review made against a
629
- * different one, under a sentence saying it was a review of what they asked for. See
630
- * `findExistingReview`.
631
- */
632
- /**
633
- * The latest review of a resolved target, or null.
634
- *
635
- * Takes a target rather than a path because the current branch stopped being the answer the
636
- * moment a review could run in a worktree. Reviewing `!45` from `main` must find the lineage of
637
- * `app-1276`, and file into it, or the one-review-per-branch rule holds only for reviewers who
638
- * happen to be standing in the right place.
639
- *
640
- * **The ref is part of the question, not a detail of the answer.** `--branch feat/foo` and
641
- * `--branch origin/feat/foo` name one branch and can be two different commits, and a review
642
- * follows exactly one of them for the rest of its life. So the review of the local branch is not
643
- * an answer to a command that named the remote ref, even on the day they are level. A target with
644
- * no ref is read as `refs/heads/<branch>`, which is both the pull request case and every review
645
- * stored before 0.15.0; see `lineageKeyFor`.
646
- *
647
- * **An explicit base narrows the lineage.** A base the reviewer named is part of what they asked
648
- * for, so only a review made against that same base can be the answer to it; the newest review of
649
- * this branch against some other base is a different comparison wearing the right branch name. A
650
- * base nobody named narrows nothing, because the command claimed nothing about it: any review of
651
- * the branch is a truthful answer to "give me the review of this branch".
652
- *
653
- * **A pull request number narrows it too.** A request target has no ref, so its lineage key is the
654
- * head branch name, and two fork requests both called `patch-1` share one. Only a review recording
655
- * the same number answers a named request. One recording no number is unknown rather than a
656
- * match, the same strictness as an empty base: the cost is one redundant review, never the wrong one.
657
- */
658
- async function findExistingReview(target) {
659
- const groups = groupByLineage(recentWorkspaces(Number.MAX_SAFE_INTEGER));
660
- const wanted = target.commit ? lineageKeyFor({
661
- kind: "commit",
662
- repoPath: target.originRepoPath,
663
- sha: target.commit.sha
664
- }) : lineageKeyFor({
665
- kind: "branch",
666
- repoPath: target.originRepoPath,
667
- branch: target.branch,
668
- followedRef: target.ref
669
- });
670
- const group = groups.find((entry) => entry.key === wanted);
671
- if (!group) return null;
672
- const requested = target.pullRequest?.number;
673
- const history = target.commit ? group.history : group.history.filter((review) => target.base ? madeAgainst(target.base)(review) : true).filter((review) => requested ? review.prNumber === requested : true);
674
- if (history.length === 0) return null;
675
- const summary = history[0];
676
- const found = {
677
- id: summary.id,
678
- token: summary.token,
679
- title: summary.title,
680
- branch: summary.branch,
681
- baseRef: summary.baseRef,
682
- ...summary.headResolvedRef ? { headResolvedRef: summary.headResolvedRef } : {},
683
- ...summary.targetType === "commit" ? { targetType: "commit" } : {},
684
- createdAt: summary.createdAt,
685
- commitSha: summary.commitSha ?? null,
686
- commitsBehind: null,
687
- baselineGone: false,
688
- total: history.length,
689
- inProgress: false
690
- };
691
- const workspace = loadWorkspace(summary.id);
692
- if (!workspace) return found;
693
- const inProgress = isRunning(workspace.jobId);
694
- try {
695
- const { freshness, availability } = await checkReview(workspace);
696
- if (availability) return {
697
- ...found,
698
- inProgress,
699
- commitAvailable: availability.state === "available"
700
- };
701
- const code = freshness.code;
702
- const baselineGone = code.rewritten || code.reason === "missing-commit";
703
- return {
704
- ...found,
705
- inProgress,
706
- commitSha: code.reviewedSha ?? found.commitSha,
707
- commitsBehind: baselineGone ? null : code.state === "current" ? 0 : code.newCommits,
708
- baselineGone
709
- };
710
- } catch {
711
- return {
712
- ...found,
713
- inProgress
714
- };
715
- }
716
- }
717
- /**
718
- * Was this review made against the base the reviewer named?
719
- *
720
- * Compared in `displayRef`'s short form, which is the form the store holds, so `--base origin/main`
721
- * and a review made against the default remote branch recognise each other. Anything else is
722
- * compared as typed: a tag or a sha is a base like any other, and two different spellings of one
723
- * commit read here as two comparisons, which costs a redundant review rather than a wrong answer.
724
- *
725
- * An empty `baseRef` is a review that stopped before it worked out what it was comparing against,
726
- * and it never matches. Unknown is not the same as "the one you asked for", and this is the one
727
- * place where guessing would put a review of some other base behind a sentence naming this one.
728
- * The cost of being strict is small and bounded: a second review starts, of the comparison that
729
- * was actually requested.
730
- */
731
- function madeAgainst(requested) {
732
- const wanted = displayRef(requested);
733
- return (review) => review.baseRef !== "" && displayRef(review.baseRef) === wanted;
734
- }
735
- /**
736
- * Whether the job behind a stored review is still working. A review with no job never was.
737
- *
738
- * A job whose driver has gone is not working, whatever phase its record was left at. Calling it in
739
- * progress told the reviewer who had just closed a terminal that there was "nothing to do but wait",
740
- * for a process that no longer existed, until the sweep called it failed half an hour later.
741
- */
742
- function isRunning(jobId) {
743
- if (!jobId) return false;
744
- const job = loadJob(jobId);
745
- return !!job && !isTerminal(job) && !driverGone(job.id);
746
- }
747
- /**
748
- * What the review is of, as the clause both surfaces hang off "Made two hours ago".
749
- *
750
- * The base is in it because a review is of a comparison rather than of a branch, and the one thing
751
- * a reviewer cannot see from the outside is which comparison they are being handed. The command
752
- * that named a base now gets one made against it, but the far commoner case is the command that
753
- * named none, and there this clause is the only thing that says what was compared.
754
- *
755
- * Each half is dropped when it is not known, rather than rendered as a placeholder: a review made
756
- * before commits were recorded, or one that stopped before it resolved its base, says less rather
757
- * than saying something untrue.
758
- */
759
- function describeSubject(existing) {
760
- const parts = [...existing.commitSha ? [`of commit ${existing.commitSha.slice(0, 7)}`] : [], ...existing.baseRef ? [`against ${existing.baseRef}`] : []];
761
- return parts.length ? `, ${parts.join(" ")}` : "";
762
- }
763
- /**
764
- * How far the branch has moved since the review was made, in one sentence.
765
- *
766
- * Exported because both surfaces say it and they say the same thing. The sentences around it
767
- * differ, because one is addressed to a session that must decide not to review anything and the
768
- * other to a person deciding whether to press update, but this fact reads identically to both.
769
- *
770
- * It names the branch rather than the reader's checkout, which 0.15.0 corrected rather than
771
- * changed. `commitsBehind` comes from `codeFreshness`, which measures by ref name and never reads
772
- * HEAD, so the fact underneath was always about the branch and the sentence merely called it
773
- * something else. `--branch` turns that from loose into wrong: a review of `feat/foo` made while
774
- * standing on `main` would report `feat/foo`'s movement in a sentence claiming it was the
775
- * reviewer's own working tree.
776
- */
777
- function describeMovement(existing) {
778
- if (existing.targetType === "commit") return existing.commitAvailable === false ? "That commit is no longer in this repository, so the review can be read but not analysed again." : "A commit does not move, so the review still describes exactly that commit.";
779
- const subject = followedRefName(existing.branch, existing.headResolvedRef) ?? existing.branch;
780
- if (existing.baselineGone) return `${subject} has been rebuilt since, so that commit is no longer on it.`;
781
- if (existing.commitsBehind === null) return `How far ${subject} has moved since could not be worked out.`;
782
- if (existing.commitsBehind === 0) return `It is of the latest commit on ${subject}.`;
783
- return `${subject} has moved ${existing.commitsBehind} commit${existing.commitsBehind === 1 ? "" : "s"} since.`;
784
- }
785
- //#endregion
786
- //#region src/batch_schedule.ts
787
- /**
788
- * Run `run` over every item, at most `concurrency` at once, and report each one's ending.
789
- *
790
- * Never throws for an item: one that rejects is recorded and the rest carry on. Once `signal` is
791
- * aborted nothing more starts, and what is already running is waited for, because it is the work's
792
- * own job to notice the abort and stop.
793
- */
794
- async function runBatch(items, options) {
795
- const outcomes = items.map(() => ({ kind: "not-started" }));
796
- let next = 0;
797
- const lane = async () => {
798
- while (next < items.length && !options.signal?.aborted) {
799
- const index = next;
800
- next += 1;
801
- try {
802
- outcomes[index] = {
803
- kind: "done",
804
- value: await options.run(items[index], index)
805
- };
806
- } catch (error) {
807
- outcomes[index] = {
808
- kind: "threw",
809
- error
810
- };
811
- }
812
- }
813
- };
814
- const lanes = Math.max(1, Math.min(options.concurrency, items.length));
815
- await Promise.all(Array.from({ length: lanes }, lane));
816
- return outcomes;
817
- }
818
- //#endregion
819
608
  //#region src/progress.ts
820
609
  /**
821
610
  * What the review is doing, while it does it.
@@ -2023,7 +1812,7 @@ async function reviewBatch(command, repoPath, deps = realDeps()) {
2023
1812
  row.state = { kind: "not-started" };
2024
1813
  return;
2025
1814
  }
2026
- const identity = `${target.originRepoPath}\n${target.pullRequest?.number ?? target.sha}`;
1815
+ const identity = batchIdentity(target);
2027
1816
  const first = claimed.get(identity);
2028
1817
  if (first !== void 0) {
2029
1818
  row.state = {
@@ -2636,109 +2425,6 @@ function makeCommandFor(command) {
2636
2425
  if (command.commitRef) return `prreviewbuddy review --commit ${command.commitRef}`;
2637
2426
  return "prreviewbuddy review";
2638
2427
  }
2639
- /** Why an unfinished review is being picked up, when nothing recorded a failure because nothing could. */
2640
- var DRIVER_GONE = "the command that was running it ended before it finished, most often because its terminal closed.";
2641
- function failureOf(workspaceId) {
2642
- const workspace = loadWorkspace(workspaceId);
2643
- if (!workspace?.jobId) return null;
2644
- const job = loadJob(workspace.jobId);
2645
- if (!job) return null;
2646
- const abandoned = !isTerminal(job) && driverGone(job.id);
2647
- if (!abandoned && (!isTerminal(job) || job.phase !== "failed")) return null;
2648
- if (isClaimed(job.id)) return null;
2649
- return {
2650
- message: abandoned ? DRIVER_GONE : job.failure?.message ?? "The reason was not recorded.",
2651
- stopped: !abandoned && job.failure?.stopped === true,
2652
- agentId: job.env.agentId ?? DEFAULT_AGENT_ID,
2653
- ...job.requestedModel ? { requestedModel: job.requestedModel } : {}
2654
- };
2655
- }
2656
- /**
2657
- * What to run for a resolved target, decided and started, with nothing printed.
2658
- *
2659
- * The first half of `review`, and the half a batch shares: a batch says all of it in one summary
2660
- * at the end rather than as it goes, so the deciding and the saying have to be apart.
2661
- *
2662
- * A review that exists is usually a review that finished, and handing it back rather than making a
2663
- * second one is the whole rule. A review whose job failed is the exception, and handing that one
2664
- * back is worse than useless: the reviewer typed a command and would get a link to a page reporting
2665
- * the failure they just caused, with the way out being a button in a browser. It is the same resume
2666
- * the workspace's Retry offers, taken here because this is where the person is.
2667
- *
2668
- * The target is the one already resolved, not re-derived. A request reference is resolved by
2669
- * spawning an agent, and resolving it twice risks pinning the job to a different commit than the one
2670
- * the existing-review check just looked at. It carries the base too, which is why none is passed
2671
- * alongside it: two copies of one answer make one of them the authority without saying which.
2672
- *
2673
- * The agent is the resolved one, not the flag. They are the same string only when somebody typed
2674
- * one: a machine default, a review's own agent and the first-run picker all reach a decision the
2675
- * flag never held, and passing the flag threw all three away. The harness reads an absent id as
2676
- * "whatever the default is", so a configured default of Codex announced Codex and then analysed
2677
- * with Claude, and nothing said so until the failure named the wrong tool.
2678
- */
2679
- async function prepareReview(target, agentId, fresh, requestedModel) {
2680
- if (!fresh) {
2681
- const existing = await findExistingReview(target);
2682
- if (existing) {
2683
- const failure = failureOf(existing.id);
2684
- return failure ? {
2685
- kind: "resume",
2686
- existing,
2687
- failure
2688
- } : {
2689
- kind: "existing",
2690
- existing
2691
- };
2692
- }
2693
- }
2694
- return {
2695
- kind: "new",
2696
- workspaceId: await startJob({
2697
- target,
2698
- agentId,
2699
- ...requestedModel ? { requestedModel } : {}
2700
- })
2701
- };
2702
- }
2703
- /**
2704
- * Run a review's job to its end and say whether a review came out of it, with nothing printed.
2705
- *
2706
- * Any ending that is not `done`, not only the one that says `failed`. A run that stopped without
2707
- * recording why still stopped, and reading "not failed" as "finished" is what let a job left at
2708
- * `analysing` print `Review ready` over a review that does not exist.
2709
- *
2710
- * The standard review is also checked for a result. A finished analysis always leaves one -- a
2711
- * reply that could not be read is a failure and never reaches here -- so a null one is a bug, and
2712
- * `Review ready . Risk not rated . 0 findings` is the one sentence it must never be reported as:
2713
- * that reads as a clean bill of health, which is the opposite of what happened. A KISS run's
2714
- * answer is checked by `drive`, which knows which run it is.
2715
- */
2716
- async function settleReview(workspaceId, reviewType = "standard", signal) {
2717
- let job;
2718
- try {
2719
- job = await runJob(workspaceId, {}, reviewType, signal ? { signal } : {});
2720
- } catch (error) {
2721
- return {
2722
- ok: false,
2723
- reason: error instanceof Error ? error.message : String(error),
2724
- stopped: false
2725
- };
2726
- }
2727
- if (job.phase !== "done") return {
2728
- ok: false,
2729
- reason: failureMessage(job),
2730
- stopped: job.failure?.stopped === true
2731
- };
2732
- if (reviewType === "standard" && !loadWorkspace(workspaceId)?.session?.result) return {
2733
- ok: false,
2734
- reason: "The analysis finished without recording a review.",
2735
- stopped: false
2736
- };
2737
- return {
2738
- ok: true,
2739
- job
2740
- };
2741
- }
2742
2428
  /**
2743
2429
  * Run the job to its end, saying what it is doing while it does it.
2744
2430
  *
@@ -2787,18 +2473,6 @@ async function drive(workspaceId, url, kissRun) {
2787
2473
  return 0;
2788
2474
  }
2789
2475
  /**
2790
- * Why a run ended, when it did not end by finishing.
2791
- *
2792
- * A job that recorded a failure says so in its own words. One that did not is a driver that went
2793
- * away mid-phase -- killed, or crashed past the point where it could write -- and the honest thing
2794
- * to say is which phase it was in, because that is the phase the next run picks up from.
2795
- */
2796
- function failureMessage(job) {
2797
- if (job.failure?.message) return job.failure.message;
2798
- const doing = PHASES[job.phase].doing;
2799
- return `The reason was not recorded. It was ${doing.charAt(0).toLowerCase()}${doing.slice(1)} when it stopped.`;
2800
- }
2801
- /**
2802
2476
  * Every link this command prints, and the reason they are not plain workspace URLs.
2803
2477
  *
2804
2478
  * A reviewer who only ever runs this command was never sent to the reviews index, so their browser