prreviewbuddy 0.25.6 → 0.25.8
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 +49 -0
- package/dist/main.js +42 -8
- package/dist/{relative_time-DJ9iHDDG.js → relative_time-D760FOL9.js} +262 -235
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.8
|
|
4
|
+
|
|
5
|
+
**A review that fails says so, instead of reporting itself as ready.** An analysis that failed
|
|
6
|
+
after the agent had started work left no trace of the failure: the job stayed at `Analysing the
|
|
7
|
+
change`, the workspace page kept the running rail over it, and the terminal printed
|
|
8
|
+
`Review ready . Risk not rated . 0 findings`, which reads as a change with nothing wrong with it.
|
|
9
|
+
The review it was reporting did not exist.
|
|
10
|
+
|
|
11
|
+
The record refuses to be failed by anyone but the run that owns it, so that a driver whose job has
|
|
12
|
+
been taken over cannot mark a healthy review as broken. It asked that question of the record's
|
|
13
|
+
timestamp, and the timestamp moves every time the agent reports what it is doing. So a run that
|
|
14
|
+
streamed a single line and then failed looked, to itself, like a run that had been taken over, and
|
|
15
|
+
declined to record its own failure. It asks the claim now, which is who is actually driving.
|
|
16
|
+
|
|
17
|
+
**The terminal reports what happened, and refuses to invent the rest.** `prreviewbuddy review` read
|
|
18
|
+
"the job did not fail" as "the review is ready". A run can also stop without recording why, and now
|
|
19
|
+
says so, naming the phase it stopped in, which is the phase running the command again picks up
|
|
20
|
+
from. A finished job holding no review is reported as a review that stopped rather than as a clean
|
|
21
|
+
one: the standard review is held to the rule the KISS lens has always been held to.
|
|
22
|
+
|
|
23
|
+
**A fenced answer is read to its end, not to the first ``` inside it.** The model is asked for
|
|
24
|
+
JSON and often wraps it in a code fence, and the summary and next steps of a review about code
|
|
25
|
+
frequently hold a fenced snippet of their own. The unwrapping stopped at the first closing marker
|
|
26
|
+
it found, which in that case is the one *opening* a snippet inside one of the answer's own
|
|
27
|
+
strings. A real review of a ten-file pull request was cut in half 24KB in, mid-string, and thrown
|
|
28
|
+
away whole: four themes, six findings, six questions and six next steps, none of which anything
|
|
29
|
+
ever showed. A fence says where an answer starts, not where it ends, so one that does not parse is
|
|
30
|
+
now passed over for the scanner that reads such an object correctly.
|
|
31
|
+
|
|
32
|
+
## 0.25.7
|
|
33
|
+
|
|
34
|
+
**The agent you chose is the agent that runs.** Setting a default agent, with
|
|
35
|
+
`prreviewbuddy config set agent codex` or by picking one when asked, made the terminal say
|
|
36
|
+
`Using Codex.` and then start the review under Claude anyway. Only `--agent` typed on the command
|
|
37
|
+
line reached the job; every other way of arriving at an agent, including the configured default and
|
|
38
|
+
the first-run picker, was worked out, announced and then dropped, and the review fell back to
|
|
39
|
+
Claude because that is what a job with no agent recorded means.
|
|
40
|
+
|
|
41
|
+
Nothing about it was visible unless it failed. A signed-in Claude simply reviewed the change, well,
|
|
42
|
+
under a tool nobody had asked for; the way it surfaced was a sign-in message naming Claude on a
|
|
43
|
+
machine where Codex was the default.
|
|
44
|
+
|
|
45
|
+
**Resuming a paused review answers `--agent` instead of ignoring it.** A resume continues under the
|
|
46
|
+
agent the review was started by, because the phases it has already finished were done by that one
|
|
47
|
+
and switching halfway would leave a review whose provenance and whose execution disagree. That is
|
|
48
|
+
unchanged; what is new is that it says so, and names `--reanalyse --agent` as the way to analyse
|
|
49
|
+
the same commit with another agent. It stays quiet when the flag asks for the agent already
|
|
50
|
+
running.
|
|
51
|
+
|
|
3
52
|
## 0.25.6
|
|
4
53
|
|
|
5
54
|
**A review you open while it is still running now looks like one that is running.** Following the
|
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, Ht as
|
|
2
|
+
import { $ as allJobs, 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 MANAGED_ROOT, L as record, Lt as readAgentPreference, Mt as resolveTarget, N as reanalyseReview, Nt as displayRef, P as runJob, Pt as git, Q as checkFreshness, R as recordTelemetryUploadConsent, Rt as writeAgentPreference, St as summarise, Ut as agentById, V as PACKAGE_NAME, Vt as AGENT_IDS, X as removeWorktree, Y as readMarker, a as liveJobsFor, at as progressSteps, b as MIN_WORKSPACE_PORT, bt as reviewedRepositories, ct as followedRefName, dt as lineageKeyFor, f as ensureServer, g as workspaceUrl, h as stopServer, ht as positionInLineage, it as PHASES, j as recordKissRun, l as readIndexToken, lt as groupByLineage, m as reviewsUrl, mt as matchingWorkspaceIds, nt as loadJob, o as wasBlocked, ot as clearClaim, pt as loadWorkspace, r as deleteReview$1, s as runningJobs, st as isClaimed, t as relativeTime, tt as isTerminal, 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-D760FOL9.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";
|
|
@@ -1250,6 +1250,19 @@ function agentUnusedByUpdate(agentId) {
|
|
|
1250
1250
|
return `An update continues the review with the agent it was made by, so \`--agent ${agentId}\` changes nothing here. To analyse the same commit again with another agent, run \`prreviewbuddy review --reanalyse --agent ${agentId}\`.`;
|
|
1251
1251
|
}
|
|
1252
1252
|
/**
|
|
1253
|
+
* `--agent` was named alongside a resume, where it changes nothing either.
|
|
1254
|
+
*
|
|
1255
|
+
* A paused job carries the agent it was started under, and its finished phases were done by that
|
|
1256
|
+
* agent. Switching engines halfway would leave one review whose provenance and whose execution
|
|
1257
|
+
* disagree, so the flag does not mutate the job; it is said instead, for the reason above.
|
|
1258
|
+
*
|
|
1259
|
+
* Only when the two differ. Resuming a Codex job with `--agent codex` is the agent the reviewer
|
|
1260
|
+
* asked for, and telling them it was ignored would be true and useless.
|
|
1261
|
+
*/
|
|
1262
|
+
function agentUnusedByResume(agentId) {
|
|
1263
|
+
return `A resume continues with the agent the review was started by, so \`--agent ${agentId}\` changes nothing here. To analyse the same commit with it instead, run \`prreviewbuddy review --reanalyse --agent ${agentId}\`.`;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1253
1266
|
* The update's own sentence, which the harness already wrote for the workspace strip, plus the
|
|
1254
1267
|
* link. Not rephrased here: an update that stopped early has a specific reason and the reviewer
|
|
1255
1268
|
* gets the same reason whichever surface asked for it.
|
|
@@ -1722,7 +1735,8 @@ async function review(command, repoPath) {
|
|
|
1722
1735
|
return operate(command, workspace);
|
|
1723
1736
|
}
|
|
1724
1737
|
if (command.lens === "kiss") return kiss(command, target);
|
|
1725
|
-
|
|
1738
|
+
const agent = await chooseFor(command);
|
|
1739
|
+
if (!agent) return 1;
|
|
1726
1740
|
if (command.operation !== "fresh") {
|
|
1727
1741
|
const existing = await findExistingReview(target);
|
|
1728
1742
|
if (existing) {
|
|
@@ -1732,13 +1746,14 @@ async function review(command, repoPath) {
|
|
|
1732
1746
|
out(existingReview(existing, url));
|
|
1733
1747
|
return 0;
|
|
1734
1748
|
}
|
|
1735
|
-
err(resumingReview(failed, existing));
|
|
1749
|
+
err(resumingReview(failed.message, existing));
|
|
1750
|
+
if (command.agentId && command.agentId !== failed.agentId) err(agentUnusedByResume(command.agentId));
|
|
1736
1751
|
return drive(existing.id, url);
|
|
1737
1752
|
}
|
|
1738
1753
|
}
|
|
1739
1754
|
const workspaceId = await startJob({
|
|
1740
1755
|
target,
|
|
1741
|
-
|
|
1756
|
+
agentId: agent.id
|
|
1742
1757
|
});
|
|
1743
1758
|
const url = linkTo(await ensureServer(), workspaceId, loadWorkspace(workspaceId).token);
|
|
1744
1759
|
out(startedReview(url, target));
|
|
@@ -1787,7 +1802,7 @@ async function kiss(command, target) {
|
|
|
1787
1802
|
type: "kiss",
|
|
1788
1803
|
audience
|
|
1789
1804
|
},
|
|
1790
|
-
|
|
1805
|
+
agentId: agent.id
|
|
1791
1806
|
});
|
|
1792
1807
|
const fresh = loadWorkspace(workspaceId);
|
|
1793
1808
|
const run = (fresh.kissRuns ?? [])[0];
|
|
@@ -1905,7 +1920,10 @@ function failureOf(workspaceId) {
|
|
|
1905
1920
|
const job = loadJob(workspace.jobId);
|
|
1906
1921
|
if (!job || !isTerminal(job) || job.phase !== "failed") return null;
|
|
1907
1922
|
if (isClaimed(job.id)) return null;
|
|
1908
|
-
return
|
|
1923
|
+
return {
|
|
1924
|
+
message: job.failure?.message ?? "The reason was not recorded.",
|
|
1925
|
+
agentId: job.env.agentId ?? DEFAULT_AGENT_ID
|
|
1926
|
+
};
|
|
1909
1927
|
}
|
|
1910
1928
|
/**
|
|
1911
1929
|
* Run the job to its end, saying what it is doing while it does it.
|
|
@@ -1932,9 +1950,9 @@ async function drive(workspaceId, url, kissRun) {
|
|
|
1932
1950
|
let finished;
|
|
1933
1951
|
try {
|
|
1934
1952
|
finished = await runJob(workspaceId, {}, kissRun ? "kiss" : "standard");
|
|
1935
|
-
if (finished.phase
|
|
1953
|
+
if (finished.phase !== "done") {
|
|
1936
1954
|
stop();
|
|
1937
|
-
err(reviewFailed(finished
|
|
1955
|
+
err(reviewFailed(failureMessage(finished), await linkNow(workspace, url)));
|
|
1938
1956
|
return 1;
|
|
1939
1957
|
}
|
|
1940
1958
|
} catch (error) {
|
|
@@ -1955,10 +1973,26 @@ async function drive(workspaceId, url, kissRun) {
|
|
|
1955
1973
|
out(kissFinished(link, answer.recommendations.length));
|
|
1956
1974
|
return 0;
|
|
1957
1975
|
}
|
|
1976
|
+
if (!done.session.result) {
|
|
1977
|
+
err(reviewFailed("The analysis finished without recording a review.", link));
|
|
1978
|
+
return 1;
|
|
1979
|
+
}
|
|
1958
1980
|
out(reviewFinished(link, summarise(done), finished.conversation));
|
|
1959
1981
|
return 0;
|
|
1960
1982
|
}
|
|
1961
1983
|
/**
|
|
1984
|
+
* Why a run ended, when it did not end by finishing.
|
|
1985
|
+
*
|
|
1986
|
+
* A job that recorded a failure says so in its own words. One that did not is a driver that went
|
|
1987
|
+
* away mid-phase -- killed, or crashed past the point where it could write -- and the honest thing
|
|
1988
|
+
* to say is which phase it was in, because that is the phase the next run picks up from.
|
|
1989
|
+
*/
|
|
1990
|
+
function failureMessage(job) {
|
|
1991
|
+
if (job.failure?.message) return job.failure.message;
|
|
1992
|
+
const doing = PHASES[job.phase].doing;
|
|
1993
|
+
return `The reason was not recorded. It was ${doing.charAt(0).toLowerCase()}${doing.slice(1)} when it stopped.`;
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1962
1996
|
* Every link this command prints, and the reason they are not plain workspace URLs.
|
|
1963
1997
|
*
|
|
1964
1998
|
* A reviewer who only ever runs this command was never sent to the reviews index, so their browser
|
|
@@ -3869,6 +3869,223 @@ function pruneOldWorkspaces() {
|
|
|
3869
3869
|
} catch {}
|
|
3870
3870
|
}
|
|
3871
3871
|
//#endregion
|
|
3872
|
+
//#region ../../packages/review-harness/src/workspace/exclusive_claim.ts
|
|
3873
|
+
/**
|
|
3874
|
+
* At most one holder of a name at a time, across processes, on this machine.
|
|
3875
|
+
*
|
|
3876
|
+
* Extracted from `job_claim.ts`, which had written all of this for one caller. A second caller then
|
|
3877
|
+
* needed the same thing under a different name -- see `review_lock.ts` -- and the alternative was a
|
|
3878
|
+
* second concurrency system beside a working one, which is how two mechanisms come to disagree
|
|
3879
|
+
* about who holds what.
|
|
3880
|
+
*
|
|
3881
|
+
* The mechanism, and the reasoning behind each half of it, lives here. What a particular claim
|
|
3882
|
+
* *means* stays with the module that takes it: "at most one driver owns a job" is a statement about
|
|
3883
|
+
* jobs, not about `link()`.
|
|
3884
|
+
*
|
|
3885
|
+
* Never holds a credential, and holds nothing about the thing it names: a claim is a pid, a host,
|
|
3886
|
+
* and two timestamps.
|
|
3887
|
+
*/
|
|
3888
|
+
/**
|
|
3889
|
+
* One directory for every kind of claim, keyed by name.
|
|
3890
|
+
*
|
|
3891
|
+
* Kept out of `jobs/`, where `allJobs` reads every `.json` and parses it as a `ReviewJob`: a claim
|
|
3892
|
+
* file there would be swept as a job. Keys from different callers share the directory and are kept
|
|
3893
|
+
* apart by shape -- a job claim is a UUID, a review lock is `review-<id>` -- so neither can name
|
|
3894
|
+
* the other's file by accident.
|
|
3895
|
+
*/
|
|
3896
|
+
var CLAIMS_DIR = join(STORE_ROOT, "claims");
|
|
3897
|
+
/** Comfortably inside the lease, so one slow tick is never enough to lose a claim that is held. */
|
|
3898
|
+
var CLAIM_RENEW_MS = 3e4;
|
|
3899
|
+
function claimPath(key) {
|
|
3900
|
+
return join(CLAIMS_DIR, `${key}.json`);
|
|
3901
|
+
}
|
|
3902
|
+
function readClaim(key) {
|
|
3903
|
+
try {
|
|
3904
|
+
return JSON.parse(readFileSync(claimPath(key), "utf8"));
|
|
3905
|
+
} catch {
|
|
3906
|
+
return null;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Whether the process behind a claim still exists.
|
|
3911
|
+
*
|
|
3912
|
+
* Signal 0 checks for a process without touching it: `ESRCH` means gone, and `EPERM` means it is
|
|
3913
|
+
* there but owned by somebody else, which is still there. Only meaningful for a claim written on
|
|
3914
|
+
* this machine, and `~/.prreviewbuddy` can sit on a synced home directory, so a claim from another
|
|
3915
|
+
* host is left to the lease alone rather than judged against a pid that means nothing here.
|
|
3916
|
+
*/
|
|
3917
|
+
function ownerAlive(claim) {
|
|
3918
|
+
if (claim.host !== hostname()) return true;
|
|
3919
|
+
try {
|
|
3920
|
+
process.kill(claim.pid, 0);
|
|
3921
|
+
return true;
|
|
3922
|
+
} catch (error) {
|
|
3923
|
+
return error.code === "EPERM";
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
/**
|
|
3927
|
+
* The claim on this key, if one is genuinely held.
|
|
3928
|
+
*
|
|
3929
|
+
* Both conditions, because each covers the other's blind spot. A pid check recovers from a killed
|
|
3930
|
+
* holder at once but can be fooled by a reused pid; a lease cannot be fooled but is always late by
|
|
3931
|
+
* up to its own length. Requiring both means a reused pid with a stale lease reads as free, and a
|
|
3932
|
+
* dead pid with a fresh lease reads as free immediately.
|
|
3933
|
+
*/
|
|
3934
|
+
function liveClaim$1(key, now = Date.now()) {
|
|
3935
|
+
const claim = readClaim(key);
|
|
3936
|
+
if (!claim) return null;
|
|
3937
|
+
if (now - claim.renewedAt >= 18e4) return null;
|
|
3938
|
+
if (!ownerAlive(claim)) return null;
|
|
3939
|
+
return claim;
|
|
3940
|
+
}
|
|
3941
|
+
/**
|
|
3942
|
+
* Take the claim on a key, or return null because somebody else holds it.
|
|
3943
|
+
*
|
|
3944
|
+
* Synchronous from the check to the create, with no `await` between them, so two calls inside one
|
|
3945
|
+
* process cannot both pass: this keeps the property a module-level `Set` had, and adds the one it
|
|
3946
|
+
* lacked.
|
|
3947
|
+
*
|
|
3948
|
+
* Across processes the create is `link`, which fails with `EEXIST` if the name is taken and does so
|
|
3949
|
+
* atomically, unlike a write with a prior existence check. A stale claim is unlinked first, and
|
|
3950
|
+
* only if it is still the same stale claim that was read a line earlier, identified by `holder`.
|
|
3951
|
+
* The residual race is two processes stealing the *same* stale claim within the same few
|
|
3952
|
+
* microseconds, where the loser can delete the winner's fresh file. Left rather than solved: it
|
|
3953
|
+
* needs a crashed holder and two attempts fired at the same instant, and `recordFailure` already
|
|
3954
|
+
* refuses to write over a job record that moved underneath it, so the cost of losing that race is
|
|
3955
|
+
* duplicated work rather than a corrupted review.
|
|
3956
|
+
*
|
|
3957
|
+
* **Never waits.** A caller that cannot have the key is told so immediately. Queueing would turn a
|
|
3958
|
+
* deletion racing an analysis into a deletion that happens twenty minutes later, when the person
|
|
3959
|
+
* who asked for it has stopped looking.
|
|
3960
|
+
*/
|
|
3961
|
+
function acquire(key, now = Date.now()) {
|
|
3962
|
+
const existing = readClaim(key);
|
|
3963
|
+
if (existing) {
|
|
3964
|
+
if (liveClaim$1(key, now)) return null;
|
|
3965
|
+
const current = readClaim(key);
|
|
3966
|
+
if (current && current.holder !== existing.holder) return null;
|
|
3967
|
+
try {
|
|
3968
|
+
unlinkSync(claimPath(key));
|
|
3969
|
+
} catch {}
|
|
3970
|
+
}
|
|
3971
|
+
const claim = {
|
|
3972
|
+
key,
|
|
3973
|
+
holder: randomUUID(),
|
|
3974
|
+
pid: process.pid,
|
|
3975
|
+
host: hostname(),
|
|
3976
|
+
acquiredAt: now,
|
|
3977
|
+
renewedAt: now
|
|
3978
|
+
};
|
|
3979
|
+
mkdirSync(CLAIMS_DIR, { recursive: true });
|
|
3980
|
+
const staging = join(CLAIMS_DIR, `.${claim.holder}.tmp`);
|
|
3981
|
+
writeFileSync(staging, JSON.stringify(claim), { mode: 384 });
|
|
3982
|
+
try {
|
|
3983
|
+
linkSync(staging, claimPath(key));
|
|
3984
|
+
} catch {
|
|
3985
|
+
return null;
|
|
3986
|
+
} finally {
|
|
3987
|
+
rmSync(staging, { force: true });
|
|
3988
|
+
}
|
|
3989
|
+
return startHolding(claim);
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Keep the claim fresh for as long as the work runs, and give it up afterwards.
|
|
3993
|
+
*
|
|
3994
|
+
* The timer is unreferenced on purpose. A heartbeat must never be the reason a process stays alive:
|
|
3995
|
+
* the CLI is held open by the await on the job and the workspace server by its socket, and if
|
|
3996
|
+
* neither is true any more then this claim is exactly the kind that should be allowed to expire.
|
|
3997
|
+
*
|
|
3998
|
+
* A renewal that finds the claim *gone* writes it back rather than giving up. A holder whose event
|
|
3999
|
+
* loop was blocked past the lease -- parsing a very large diff, say -- can have its claim judged
|
|
4000
|
+
* expired and swept while it is still perfectly alive, and a holder that then quietly stopped
|
|
4001
|
+
* renewing would leave the key looking free for the rest of its run. Nobody holds the file, so
|
|
4002
|
+
* taking it again is the true statement.
|
|
4003
|
+
*
|
|
4004
|
+
* A renewal that finds the claim held by somebody *else* stops rather than writing over it. By then
|
|
4005
|
+
* the other holder is the real one, and reasserting ownership here would produce the two holders
|
|
4006
|
+
* this whole file exists to prevent.
|
|
4007
|
+
*/
|
|
4008
|
+
function startHolding(claim) {
|
|
4009
|
+
let held = claim;
|
|
4010
|
+
const timer = setInterval(() => {
|
|
4011
|
+
const current = readClaim(held.key);
|
|
4012
|
+
if (current && current.holder !== held.holder) {
|
|
4013
|
+
clearInterval(timer);
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
held = {
|
|
4017
|
+
...held,
|
|
4018
|
+
renewedAt: Date.now()
|
|
4019
|
+
};
|
|
4020
|
+
try {
|
|
4021
|
+
writeFileSync(claimPath(held.key), JSON.stringify(held), { mode: 384 });
|
|
4022
|
+
} catch {}
|
|
4023
|
+
}, CLAIM_RENEW_MS);
|
|
4024
|
+
timer.unref?.();
|
|
4025
|
+
return {
|
|
4026
|
+
get claim() {
|
|
4027
|
+
return held;
|
|
4028
|
+
},
|
|
4029
|
+
release() {
|
|
4030
|
+
clearInterval(timer);
|
|
4031
|
+
const current = readClaim(held.key);
|
|
4032
|
+
if (current && current.holder !== held.holder) return;
|
|
4033
|
+
try {
|
|
4034
|
+
unlinkSync(claimPath(held.key));
|
|
4035
|
+
} catch {}
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
}
|
|
4039
|
+
/** Drop a claim without holding it first. For a key being destroyed, and for tests. */
|
|
4040
|
+
function clearClaim$1(key) {
|
|
4041
|
+
try {
|
|
4042
|
+
unlinkSync(claimPath(key));
|
|
4043
|
+
} catch {}
|
|
4044
|
+
}
|
|
4045
|
+
//#endregion
|
|
4046
|
+
//#region ../../packages/review-harness/src/workspace/job_claim.ts
|
|
4047
|
+
/**
|
|
4048
|
+
* Who is driving a job right now, recorded where another process can see it.
|
|
4049
|
+
*
|
|
4050
|
+
* The invariant this exists to state: **at most one driver owns a job at a time.** `runJob` used to
|
|
4051
|
+
* enforce that with a module-level `Set`, which was correct when one process called it and became
|
|
4052
|
+
* silently wrong the moment three did -- the MCP server, the workspace server's Retry route, and
|
|
4053
|
+
* the CLI. A guard that is invisible across processes is not a guard against the case that
|
|
4054
|
+
* actually happens, which is two surfaces on one machine reaching the same job.
|
|
4055
|
+
*
|
|
4056
|
+
* Idempotence at each collision point was the alternative, and it was rejected: it would have to be
|
|
4057
|
+
* rebuilt separately for `createWorktree`, for context building, for the agent spawn and for
|
|
4058
|
+
* applying the result, and each one would only ever be as good as the last person who remembered.
|
|
4059
|
+
* One claim covers the whole lifecycle.
|
|
4060
|
+
*
|
|
4061
|
+
* The mechanism is `exclusive_claim.ts`, and this file is now the naming: a job is claimed under
|
|
4062
|
+
* its own id, so `claims/<jobId>.json` is where it has always been. The mechanism moved out when a
|
|
4063
|
+
* review lock needed the same guarantee under a different name; what stays here is what a job claim
|
|
4064
|
+
* means, which is the part that is about jobs rather than about `link()`.
|
|
4065
|
+
*/
|
|
4066
|
+
/** The claim on this job, if a driver genuinely holds it. */
|
|
4067
|
+
function liveClaim(jobId, now = Date.now()) {
|
|
4068
|
+
return liveClaim$1(jobId, now);
|
|
4069
|
+
}
|
|
4070
|
+
/** Whether a driver owns this job right now. The question every other module actually asks. */
|
|
4071
|
+
function isClaimed(jobId, now = Date.now()) {
|
|
4072
|
+
return liveClaim(jobId, now) !== null;
|
|
4073
|
+
}
|
|
4074
|
+
/**
|
|
4075
|
+
* Take the claim on a job, or return null because another driver holds it.
|
|
4076
|
+
*
|
|
4077
|
+
* Two retries fired together, a retry racing the run `startJob` already kicked off, or a retry
|
|
4078
|
+
* racing a driver the sweep wrongly called stopped: all of them land here rather than both reaching
|
|
4079
|
+
* `createWorktree` or the analysis spawn.
|
|
4080
|
+
*/
|
|
4081
|
+
function claimJob(jobId, now = Date.now()) {
|
|
4082
|
+
return acquire(jobId, now);
|
|
4083
|
+
}
|
|
4084
|
+
/** Drop a job's claim without holding it first. For a job being deleted, and for tests. */
|
|
4085
|
+
function clearClaim(jobId) {
|
|
4086
|
+
clearClaim$1(jobId);
|
|
4087
|
+
}
|
|
4088
|
+
//#endregion
|
|
3872
4089
|
//#region ../../packages/review-harness/src/workspace/job_progress.ts
|
|
3873
4090
|
/**
|
|
3874
4091
|
* The phases that do work, in the order they run. `done` and `failed` are outcomes, not work.
|
|
@@ -4097,20 +4314,31 @@ function fail(job, message, kind, detail) {
|
|
|
4097
4314
|
};
|
|
4098
4315
|
}
|
|
4099
4316
|
/**
|
|
4100
|
-
* Record a failure,
|
|
4317
|
+
* Record a failure, unless somebody else is driving this job now.
|
|
4318
|
+
*
|
|
4319
|
+
* A run only reaches this after an `await` on the git call or the agent spawn that just rejected,
|
|
4320
|
+
* and whatever this run's own `job` variable holds by then is a snapshot from before that. If the
|
|
4321
|
+
* run that owns the job is no longer this one -- this one's claim lapsed and another driver picked
|
|
4322
|
+
* the job up -- saving this run's failure would take a job that is running fine and record it as
|
|
4323
|
+
* failed, and nothing downstream could tell the difference from a real one: `saveJob` writes
|
|
4324
|
+
* whatever it is given.
|
|
4101
4325
|
*
|
|
4102
|
-
*
|
|
4103
|
-
*
|
|
4104
|
-
*
|
|
4105
|
-
*
|
|
4106
|
-
*
|
|
4107
|
-
*
|
|
4108
|
-
*
|
|
4326
|
+
* The question is asked of the claim, which is who is driving, and not of `updatedAt`, which is
|
|
4327
|
+
* only when the record last moved. Those two came apart the moment the analysis began streaming
|
|
4328
|
+
* progress: `runAnalysisFor` writes every line the agent reports onto the record with a fresh
|
|
4329
|
+
* `updatedAt` and does not hand the new copy back, so a driver's own commentary made its own
|
|
4330
|
+
* snapshot look stale and it declined to record its own failure. The job stayed at `analysing` for
|
|
4331
|
+
* ever, the page kept the running rail over it, and the CLI printed `Review ready . 0 findings`.
|
|
4332
|
+
*
|
|
4333
|
+
* The failure is written onto the record as it now stands rather than onto the snapshot, so the
|
|
4334
|
+
* last thing the agent said survives being failed. Only a driver whose own claim has gone is
|
|
4335
|
+
* refused, so a job nobody holds -- a crashed driver's, a resumed run's -- is still failable.
|
|
4109
4336
|
*/
|
|
4110
|
-
function recordFailure(job, message, kind, detail) {
|
|
4111
|
-
const current = loadJob(job.id);
|
|
4112
|
-
|
|
4113
|
-
|
|
4337
|
+
function recordFailure(job, message, driver, kind, detail) {
|
|
4338
|
+
const current = loadJob(job.id) ?? job;
|
|
4339
|
+
const claim = liveClaim(job.id);
|
|
4340
|
+
if (claim && claim.holder !== driver) return current;
|
|
4341
|
+
const failed = fail(current, message, kind, detail);
|
|
4114
4342
|
saveJob(failed);
|
|
4115
4343
|
return failed;
|
|
4116
4344
|
}
|
|
@@ -4719,223 +4947,6 @@ function message$1(error) {
|
|
|
4719
4947
|
return error instanceof Error ? error.message : String(error);
|
|
4720
4948
|
}
|
|
4721
4949
|
//#endregion
|
|
4722
|
-
//#region ../../packages/review-harness/src/workspace/exclusive_claim.ts
|
|
4723
|
-
/**
|
|
4724
|
-
* At most one holder of a name at a time, across processes, on this machine.
|
|
4725
|
-
*
|
|
4726
|
-
* Extracted from `job_claim.ts`, which had written all of this for one caller. A second caller then
|
|
4727
|
-
* needed the same thing under a different name -- see `review_lock.ts` -- and the alternative was a
|
|
4728
|
-
* second concurrency system beside a working one, which is how two mechanisms come to disagree
|
|
4729
|
-
* about who holds what.
|
|
4730
|
-
*
|
|
4731
|
-
* The mechanism, and the reasoning behind each half of it, lives here. What a particular claim
|
|
4732
|
-
* *means* stays with the module that takes it: "at most one driver owns a job" is a statement about
|
|
4733
|
-
* jobs, not about `link()`.
|
|
4734
|
-
*
|
|
4735
|
-
* Never holds a credential, and holds nothing about the thing it names: a claim is a pid, a host,
|
|
4736
|
-
* and two timestamps.
|
|
4737
|
-
*/
|
|
4738
|
-
/**
|
|
4739
|
-
* One directory for every kind of claim, keyed by name.
|
|
4740
|
-
*
|
|
4741
|
-
* Kept out of `jobs/`, where `allJobs` reads every `.json` and parses it as a `ReviewJob`: a claim
|
|
4742
|
-
* file there would be swept as a job. Keys from different callers share the directory and are kept
|
|
4743
|
-
* apart by shape -- a job claim is a UUID, a review lock is `review-<id>` -- so neither can name
|
|
4744
|
-
* the other's file by accident.
|
|
4745
|
-
*/
|
|
4746
|
-
var CLAIMS_DIR = join(STORE_ROOT, "claims");
|
|
4747
|
-
/** Comfortably inside the lease, so one slow tick is never enough to lose a claim that is held. */
|
|
4748
|
-
var CLAIM_RENEW_MS = 3e4;
|
|
4749
|
-
function claimPath(key) {
|
|
4750
|
-
return join(CLAIMS_DIR, `${key}.json`);
|
|
4751
|
-
}
|
|
4752
|
-
function readClaim(key) {
|
|
4753
|
-
try {
|
|
4754
|
-
return JSON.parse(readFileSync(claimPath(key), "utf8"));
|
|
4755
|
-
} catch {
|
|
4756
|
-
return null;
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
/**
|
|
4760
|
-
* Whether the process behind a claim still exists.
|
|
4761
|
-
*
|
|
4762
|
-
* Signal 0 checks for a process without touching it: `ESRCH` means gone, and `EPERM` means it is
|
|
4763
|
-
* there but owned by somebody else, which is still there. Only meaningful for a claim written on
|
|
4764
|
-
* this machine, and `~/.prreviewbuddy` can sit on a synced home directory, so a claim from another
|
|
4765
|
-
* host is left to the lease alone rather than judged against a pid that means nothing here.
|
|
4766
|
-
*/
|
|
4767
|
-
function ownerAlive(claim) {
|
|
4768
|
-
if (claim.host !== hostname()) return true;
|
|
4769
|
-
try {
|
|
4770
|
-
process.kill(claim.pid, 0);
|
|
4771
|
-
return true;
|
|
4772
|
-
} catch (error) {
|
|
4773
|
-
return error.code === "EPERM";
|
|
4774
|
-
}
|
|
4775
|
-
}
|
|
4776
|
-
/**
|
|
4777
|
-
* The claim on this key, if one is genuinely held.
|
|
4778
|
-
*
|
|
4779
|
-
* Both conditions, because each covers the other's blind spot. A pid check recovers from a killed
|
|
4780
|
-
* holder at once but can be fooled by a reused pid; a lease cannot be fooled but is always late by
|
|
4781
|
-
* up to its own length. Requiring both means a reused pid with a stale lease reads as free, and a
|
|
4782
|
-
* dead pid with a fresh lease reads as free immediately.
|
|
4783
|
-
*/
|
|
4784
|
-
function liveClaim$1(key, now = Date.now()) {
|
|
4785
|
-
const claim = readClaim(key);
|
|
4786
|
-
if (!claim) return null;
|
|
4787
|
-
if (now - claim.renewedAt >= 18e4) return null;
|
|
4788
|
-
if (!ownerAlive(claim)) return null;
|
|
4789
|
-
return claim;
|
|
4790
|
-
}
|
|
4791
|
-
/**
|
|
4792
|
-
* Take the claim on a key, or return null because somebody else holds it.
|
|
4793
|
-
*
|
|
4794
|
-
* Synchronous from the check to the create, with no `await` between them, so two calls inside one
|
|
4795
|
-
* process cannot both pass: this keeps the property a module-level `Set` had, and adds the one it
|
|
4796
|
-
* lacked.
|
|
4797
|
-
*
|
|
4798
|
-
* Across processes the create is `link`, which fails with `EEXIST` if the name is taken and does so
|
|
4799
|
-
* atomically, unlike a write with a prior existence check. A stale claim is unlinked first, and
|
|
4800
|
-
* only if it is still the same stale claim that was read a line earlier, identified by `holder`.
|
|
4801
|
-
* The residual race is two processes stealing the *same* stale claim within the same few
|
|
4802
|
-
* microseconds, where the loser can delete the winner's fresh file. Left rather than solved: it
|
|
4803
|
-
* needs a crashed holder and two attempts fired at the same instant, and `recordFailure` already
|
|
4804
|
-
* refuses to write over a job record that moved underneath it, so the cost of losing that race is
|
|
4805
|
-
* duplicated work rather than a corrupted review.
|
|
4806
|
-
*
|
|
4807
|
-
* **Never waits.** A caller that cannot have the key is told so immediately. Queueing would turn a
|
|
4808
|
-
* deletion racing an analysis into a deletion that happens twenty minutes later, when the person
|
|
4809
|
-
* who asked for it has stopped looking.
|
|
4810
|
-
*/
|
|
4811
|
-
function acquire(key, now = Date.now()) {
|
|
4812
|
-
const existing = readClaim(key);
|
|
4813
|
-
if (existing) {
|
|
4814
|
-
if (liveClaim$1(key, now)) return null;
|
|
4815
|
-
const current = readClaim(key);
|
|
4816
|
-
if (current && current.holder !== existing.holder) return null;
|
|
4817
|
-
try {
|
|
4818
|
-
unlinkSync(claimPath(key));
|
|
4819
|
-
} catch {}
|
|
4820
|
-
}
|
|
4821
|
-
const claim = {
|
|
4822
|
-
key,
|
|
4823
|
-
holder: randomUUID(),
|
|
4824
|
-
pid: process.pid,
|
|
4825
|
-
host: hostname(),
|
|
4826
|
-
acquiredAt: now,
|
|
4827
|
-
renewedAt: now
|
|
4828
|
-
};
|
|
4829
|
-
mkdirSync(CLAIMS_DIR, { recursive: true });
|
|
4830
|
-
const staging = join(CLAIMS_DIR, `.${claim.holder}.tmp`);
|
|
4831
|
-
writeFileSync(staging, JSON.stringify(claim), { mode: 384 });
|
|
4832
|
-
try {
|
|
4833
|
-
linkSync(staging, claimPath(key));
|
|
4834
|
-
} catch {
|
|
4835
|
-
return null;
|
|
4836
|
-
} finally {
|
|
4837
|
-
rmSync(staging, { force: true });
|
|
4838
|
-
}
|
|
4839
|
-
return startHolding(claim);
|
|
4840
|
-
}
|
|
4841
|
-
/**
|
|
4842
|
-
* Keep the claim fresh for as long as the work runs, and give it up afterwards.
|
|
4843
|
-
*
|
|
4844
|
-
* The timer is unreferenced on purpose. A heartbeat must never be the reason a process stays alive:
|
|
4845
|
-
* the CLI is held open by the await on the job and the workspace server by its socket, and if
|
|
4846
|
-
* neither is true any more then this claim is exactly the kind that should be allowed to expire.
|
|
4847
|
-
*
|
|
4848
|
-
* A renewal that finds the claim *gone* writes it back rather than giving up. A holder whose event
|
|
4849
|
-
* loop was blocked past the lease -- parsing a very large diff, say -- can have its claim judged
|
|
4850
|
-
* expired and swept while it is still perfectly alive, and a holder that then quietly stopped
|
|
4851
|
-
* renewing would leave the key looking free for the rest of its run. Nobody holds the file, so
|
|
4852
|
-
* taking it again is the true statement.
|
|
4853
|
-
*
|
|
4854
|
-
* A renewal that finds the claim held by somebody *else* stops rather than writing over it. By then
|
|
4855
|
-
* the other holder is the real one, and reasserting ownership here would produce the two holders
|
|
4856
|
-
* this whole file exists to prevent.
|
|
4857
|
-
*/
|
|
4858
|
-
function startHolding(claim) {
|
|
4859
|
-
let held = claim;
|
|
4860
|
-
const timer = setInterval(() => {
|
|
4861
|
-
const current = readClaim(held.key);
|
|
4862
|
-
if (current && current.holder !== held.holder) {
|
|
4863
|
-
clearInterval(timer);
|
|
4864
|
-
return;
|
|
4865
|
-
}
|
|
4866
|
-
held = {
|
|
4867
|
-
...held,
|
|
4868
|
-
renewedAt: Date.now()
|
|
4869
|
-
};
|
|
4870
|
-
try {
|
|
4871
|
-
writeFileSync(claimPath(held.key), JSON.stringify(held), { mode: 384 });
|
|
4872
|
-
} catch {}
|
|
4873
|
-
}, CLAIM_RENEW_MS);
|
|
4874
|
-
timer.unref?.();
|
|
4875
|
-
return {
|
|
4876
|
-
get claim() {
|
|
4877
|
-
return held;
|
|
4878
|
-
},
|
|
4879
|
-
release() {
|
|
4880
|
-
clearInterval(timer);
|
|
4881
|
-
const current = readClaim(held.key);
|
|
4882
|
-
if (current && current.holder !== held.holder) return;
|
|
4883
|
-
try {
|
|
4884
|
-
unlinkSync(claimPath(held.key));
|
|
4885
|
-
} catch {}
|
|
4886
|
-
}
|
|
4887
|
-
};
|
|
4888
|
-
}
|
|
4889
|
-
/** Drop a claim without holding it first. For a key being destroyed, and for tests. */
|
|
4890
|
-
function clearClaim$1(key) {
|
|
4891
|
-
try {
|
|
4892
|
-
unlinkSync(claimPath(key));
|
|
4893
|
-
} catch {}
|
|
4894
|
-
}
|
|
4895
|
-
//#endregion
|
|
4896
|
-
//#region ../../packages/review-harness/src/workspace/job_claim.ts
|
|
4897
|
-
/**
|
|
4898
|
-
* Who is driving a job right now, recorded where another process can see it.
|
|
4899
|
-
*
|
|
4900
|
-
* The invariant this exists to state: **at most one driver owns a job at a time.** `runJob` used to
|
|
4901
|
-
* enforce that with a module-level `Set`, which was correct when one process called it and became
|
|
4902
|
-
* silently wrong the moment three did -- the MCP server, the workspace server's Retry route, and
|
|
4903
|
-
* the CLI. A guard that is invisible across processes is not a guard against the case that
|
|
4904
|
-
* actually happens, which is two surfaces on one machine reaching the same job.
|
|
4905
|
-
*
|
|
4906
|
-
* Idempotence at each collision point was the alternative, and it was rejected: it would have to be
|
|
4907
|
-
* rebuilt separately for `createWorktree`, for context building, for the agent spawn and for
|
|
4908
|
-
* applying the result, and each one would only ever be as good as the last person who remembered.
|
|
4909
|
-
* One claim covers the whole lifecycle.
|
|
4910
|
-
*
|
|
4911
|
-
* The mechanism is `exclusive_claim.ts`, and this file is now the naming: a job is claimed under
|
|
4912
|
-
* its own id, so `claims/<jobId>.json` is where it has always been. The mechanism moved out when a
|
|
4913
|
-
* review lock needed the same guarantee under a different name; what stays here is what a job claim
|
|
4914
|
-
* means, which is the part that is about jobs rather than about `link()`.
|
|
4915
|
-
*/
|
|
4916
|
-
/** The claim on this job, if a driver genuinely holds it. */
|
|
4917
|
-
function liveClaim(jobId, now = Date.now()) {
|
|
4918
|
-
return liveClaim$1(jobId, now);
|
|
4919
|
-
}
|
|
4920
|
-
/** Whether a driver owns this job right now. The question every other module actually asks. */
|
|
4921
|
-
function isClaimed(jobId, now = Date.now()) {
|
|
4922
|
-
return liveClaim(jobId, now) !== null;
|
|
4923
|
-
}
|
|
4924
|
-
/**
|
|
4925
|
-
* Take the claim on a job, or return null because another driver holds it.
|
|
4926
|
-
*
|
|
4927
|
-
* Two retries fired together, a retry racing the run `startJob` already kicked off, or a retry
|
|
4928
|
-
* racing a driver the sweep wrongly called stopped: all of them land here rather than both reaching
|
|
4929
|
-
* `createWorktree` or the analysis spawn.
|
|
4930
|
-
*/
|
|
4931
|
-
function claimJob(jobId, now = Date.now()) {
|
|
4932
|
-
return acquire(jobId, now);
|
|
4933
|
-
}
|
|
4934
|
-
/** Drop a job's claim without holding it first. For a job being deleted, and for tests. */
|
|
4935
|
-
function clearClaim(jobId) {
|
|
4936
|
-
clearClaim$1(jobId);
|
|
4937
|
-
}
|
|
4938
|
-
//#endregion
|
|
4939
4950
|
//#region ../../packages/review-harness/src/workspace/review_lock.ts
|
|
4940
4951
|
/**
|
|
4941
4952
|
* The lock that makes "nothing is running" a fact rather than an observation.
|
|
@@ -6907,7 +6918,14 @@ var REVIEW_ATTENTIONS = [
|
|
|
6907
6918
|
* reply then finds the block *inside* a string and hands back a snippet of Ruby as the review,
|
|
6908
6919
|
* which fails to parse and loses the whole thing.
|
|
6909
6920
|
*
|
|
6910
|
-
* So a fence is only
|
|
6921
|
+
* So a fence is believed only when what it wraps is the object or array being asked for **and
|
|
6922
|
+
* parses whole**. Looking at the opening alone is not enough, because the marker that ends the
|
|
6923
|
+
* match need not be the one that ends the fence: a model that wraps its answer *and* puts a
|
|
6924
|
+
* fenced `bash` snippet in one of its own strings closes the non-greedy match 24KB early, in the
|
|
6925
|
+
* middle of a string, on a candidate that still begins with `{`. A live review answered in
|
|
6926
|
+
* exactly that shape and lost 26KB of perfectly good JSON. A fence says where the answer is, not
|
|
6927
|
+
* where it ends, so a candidate that does not parse is passed over rather than returned, and the
|
|
6928
|
+
* scanner below -- which tracks strings and reads such an object whole -- gets its turn.
|
|
6911
6929
|
*
|
|
6912
6930
|
* With no fence anywhere, the value is cut out of whatever surrounds it. The prompt asks for JSON
|
|
6913
6931
|
* and nothing else, and that is the *only* instruction in the session this product controls: the
|
|
@@ -6923,10 +6941,19 @@ function extractJsonText(raw) {
|
|
|
6923
6941
|
const text = raw.trim();
|
|
6924
6942
|
for (const match of text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
|
|
6925
6943
|
const inner = match[1].trim();
|
|
6926
|
-
if (inner.startsWith("{") || inner.startsWith("[")) return inner;
|
|
6944
|
+
if ((inner.startsWith("{") || inner.startsWith("[")) && parses(inner)) return inner;
|
|
6927
6945
|
}
|
|
6928
6946
|
return firstJsonValue(text) ?? text;
|
|
6929
6947
|
}
|
|
6948
|
+
/** Whether this is a whole JSON value, which is the only thing that makes a candidate an answer. */
|
|
6949
|
+
function parses(candidate) {
|
|
6950
|
+
try {
|
|
6951
|
+
JSON.parse(candidate);
|
|
6952
|
+
return true;
|
|
6953
|
+
} catch {
|
|
6954
|
+
return false;
|
|
6955
|
+
}
|
|
6956
|
+
}
|
|
6930
6957
|
/**
|
|
6931
6958
|
* The first slice of this text that is a whole JSON value.
|
|
6932
6959
|
*
|
|
@@ -8528,7 +8555,7 @@ async function runJob(workspaceId, runners = {}, reviewType = "standard") {
|
|
|
8528
8555
|
});
|
|
8529
8556
|
if (isBusy(started)) throw busyReason(workspaceId, reviewType);
|
|
8530
8557
|
try {
|
|
8531
|
-
return await drive(started.jobId, workspaceId, runners);
|
|
8558
|
+
return await drive(started.jobId, workspaceId, runners, started.claim.claim.holder);
|
|
8532
8559
|
} finally {
|
|
8533
8560
|
started.claim.release();
|
|
8534
8561
|
}
|
|
@@ -8581,7 +8608,7 @@ function runOfJob(workspace, jobId) {
|
|
|
8581
8608
|
if (!run) throw new Error("This KISS job has no run to write its answer to.");
|
|
8582
8609
|
return run;
|
|
8583
8610
|
}
|
|
8584
|
-
async function drive(jobId, workspaceId, runners) {
|
|
8611
|
+
async function drive(jobId, workspaceId, runners, driver) {
|
|
8585
8612
|
let job = loadJob(jobId);
|
|
8586
8613
|
if (!job) throw new Error("No such job.");
|
|
8587
8614
|
const startedAt = Date.now();
|
|
@@ -8688,7 +8715,7 @@ async function drive(jobId, workspaceId, runners) {
|
|
|
8688
8715
|
} catch (error) {
|
|
8689
8716
|
const kind = error instanceof GuideRejected || error instanceof KissRejected ? "agent_output_invalid" : error instanceof AgentRunError ? error.kind : void 0;
|
|
8690
8717
|
const raw = message(error);
|
|
8691
|
-
job = recordFailure(job, explain(job, raw), kind, raw);
|
|
8718
|
+
job = recordFailure(job, explain(job, raw), driver, kind, raw);
|
|
8692
8719
|
if (job.reviewType === "kiss") failCurrentKissRun(workspaceId, job, job.failure?.message ?? raw, kind, error instanceof KissRejected ? error.answer : void 0);
|
|
8693
8720
|
finished(job, workspaceId, startedAt, false);
|
|
8694
8721
|
return job;
|
|
@@ -10735,4 +10762,4 @@ function relativeTime(ms) {
|
|
|
10735
10762
|
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
10736
10763
|
}
|
|
10737
10764
|
//#endregion
|
|
10738
|
-
export {
|
|
10765
|
+
export { allJobs as $, agentEnvOf as A, processDiscussion as At, BUILD_VERSION as B, STORE_ROOT as Bt, statedWorkspacePort as C, touchWorkspace as Ct, refreshPrContext as D, issuesOutstanding as Dt, updateReview as E, isQuestionOutstanding as Et, startJob as F, agentIdOf as Ft, forgeResolver as G, detectAgents as Gt, feedbackUrl as H, DEFAULT_AGENT_ID as Ht, readEvents as I, clearAgentPreference as It, MANAGED_ROOT as J, fillFileUrlTemplate as K, AgentCancelledError as Kt, record as L, readAgentPreference as Lt, startKissJob as M, resolveTarget as Mt, reanalyseReview as N, displayRef as Nt, isUnchanged as O, questionsOutstanding as Ot, runJob as P, git as Pt, checkFreshness as Q, recordTelemetryUploadConsent as R, writeAgentPreference as Rt, resolveWorkspacePort as S, summarise as St, RefreshUnavailableError as T, isIssueOutstanding as Tt, withUsageRecorded as U, agentById as Ut, PACKAGE_NAME as V, AGENT_IDS as Vt, EXPLAIN_SIMPLY_PROMPT as W, agentFor as Wt, removeWorktree as X, readMarker as Y, checkCodeFreshness as Z, writeServerRecord as _, recentReviewGroups as _t, liveJobsFor as a, progressSteps as at, MIN_WORKSPACE_PORT as b, reviewedRepositories as bt, discardJob as c, followedRefName as ct, clearServerRecord as d, lineageKeyFor as dt, fail as et, ensureServer as f, lineagePosition as ft, workspaceUrl as g, previousKissResult as gt, stopServer as h, positionInLineage as ht, lineageIds as i, PHASES as it, recordKissRun as j, describeAuthorship as jt, askCheckout as k, reviewerDispositions as kt, readIndexToken as l, groupByLineage as lt, reviewsUrl as m, matchingWorkspaceIds as mt, deleteLineage as n, loadJob as nt, wasBlocked as o, clearClaim as ot, readServerRecord as p, loadWorkspace as pt, ReviewBeingDeletedError as q, AgentUnavailableError as qt, deleteReview as r, saveJob as rt, runningJobs as s, isClaimed as st, relativeTime as t, isTerminal as tt, bootstrapUrl as u, latestKissRun as ut, DEFAULT_WORKSPACE_PORT as v, recentWorkspaces as vt, writePortPreference as w, doneVerb as wt, parseWorkspacePort as x, saveWorkspace as xt, MAX_WORKSPACE_PORT as y, reviewedCommit as yt, telemetryUploadConsent as z, CONFIG_PATH as zt };
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as allJobs, A as agentEnvOf, At as processDiscussion, B as BUILD_VERSION, Bt as STORE_ROOT, C as statedWorkspacePort, Ct as touchWorkspace, D as refreshPrContext, Dt as issuesOutstanding, E as updateReview, Et as isQuestionOutstanding, H as feedbackUrl, I as readEvents, It as clearAgentPreference, J as MANAGED_ROOT, K as fillFileUrlTemplate, Kt as AgentCancelledError, L as record, Lt as readAgentPreference, M as startKissJob, O as isUnchanged, Ot as questionsOutstanding, P as runJob, Pt as git, Q as checkFreshness, R as recordTelemetryUploadConsent, Rt as writeAgentPreference, S as resolveWorkspacePort, T as RefreshUnavailableError, Tt as isIssueOutstanding, U as withUsageRecorded, Vt as AGENT_IDS, W as EXPLAIN_SIMPLY_PROMPT, Wt as agentFor, X as removeWorktree, Y as readMarker, Z as checkCodeFreshness, _ as writeServerRecord, _t as recentReviewGroups, a as liveJobsFor, at as progressSteps, b as MIN_WORKSPACE_PORT, c as discardJob, ct as followedRefName, d as clearServerRecord, et as fail, ft as lineagePosition, gt as previousKissResult, i as lineageIds, it as PHASES, jt as describeAuthorship, k as askCheckout, kt as reviewerDispositions, l as readIndexToken, n as deleteLineage, nt as loadJob, o as wasBlocked, p as readServerRecord, pt as loadWorkspace, q as ReviewBeingDeletedError, qt as AgentUnavailableError, r as deleteReview, rt as saveJob, st as isClaimed, t as relativeTime$1, tt as isTerminal, ut as latestKissRun, v as DEFAULT_WORKSPACE_PORT, w as writePortPreference, wt as doneVerb, x as parseWorkspacePort, xt as saveWorkspace, y as MAX_WORKSPACE_PORT, yt as reviewedCommit, z as telemetryUploadConsent } from "./relative_time-D760FOL9.js";
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { timingSafeEqual } from "node:crypto";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.8",
|
|
4
4
|
"description": "Review a branch or pull request in an isolated checkout, from your terminal, with the coding agent you already have installed.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://prreviewbuddy.com",
|