prreviewbuddy 0.79.0 → 0.80.2
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 +71 -0
- package/dist/{delete_review-DwF1NMJ4.js → delete_review-BXfGCvQ1.js} +295 -21
- package/dist/main.js +17 -12
- package/dist/server.js +917 -133
- package/package.json +1 -1
- package/static/chunk-C3E91izb.js +1 -0
- package/static/chunk-C8d9FvLk.js +1 -0
- package/static/chunk-DnOyRP8m.js +1 -0
- package/static/chunk-Dv0Wowan.js +1 -0
- package/static/chunk-h55PfL_M.js +1 -0
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/runs.js +1 -0
- package/static/settings.js +1 -1
- package/static/shell.css +77 -6
- package/static/workspace.js +80 -80
- package/static/chunk-B6LyLjZ7.js +0 -1
- package/static/chunk-DYrelqtW.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.80.2
|
|
4
|
+
|
|
5
|
+
**A review opens the way you left the last one.** Hiding or showing Buddy AI, expanding a pane or
|
|
6
|
+
Buddy AI, and folding parts of the rail are remembered across reviews until you change them. An
|
|
7
|
+
expanded pane carries over as "the document expanded", so the next review opens with whichever pane
|
|
8
|
+
it lands on expanded. Reloading a review, or going Back or Forward to it, keeps that page's own
|
|
9
|
+
layout and changes nothing about how the next review opens. On a narrow window, where Buddy AI would
|
|
10
|
+
cover the review, it stays closed and unexpanded until you open it.
|
|
11
|
+
|
|
12
|
+
**Activity, Settings and Help match the other pages.** Activity's list keeps the same width as the
|
|
13
|
+
Reviews and Runs pages instead of filling the window. Settings and Help sit in the same light,
|
|
14
|
+
rounded border as Activity's list.
|
|
15
|
+
|
|
16
|
+
## 0.80.1
|
|
17
|
+
|
|
18
|
+
**Runs filters say what they filter.** Status and Operation are now Run status and Run type, so
|
|
19
|
+
neither reads as the review's own state, and their first options say All statuses and All types. A
|
|
20
|
+
first analysis is called Initial review, in the filter and on each run, to set it apart from Update
|
|
21
|
+
and Re-run. A click anywhere on a Runs filter opens its menu, as it does on the Reviews page, rather
|
|
22
|
+
than only a click on its value.
|
|
23
|
+
|
|
24
|
+
**Runs sections fold, and stay folded.** In progress and Recent runs fold from their headings, or
|
|
25
|
+
with ⌘K L then I or R, as the Reviews page's sections do, and stay as you left them through the
|
|
26
|
+
page's live reloads and the next visit. The Runs page keeps its own folds: folding In progress here
|
|
27
|
+
leaves it open on Reviews. Fixed: Show older runs no longer appears when every run is already shown.
|
|
28
|
+
|
|
29
|
+
**A finished run's card opens its review.** Click anywhere on it, or move to it with J and K and
|
|
30
|
+
press Enter, as on Reviews. The Open review link is gone. Remove now sits in the card's … menu, as
|
|
31
|
+
Remove from Runs, which Space opens from the keyboard. A run whose review was deleted still says so
|
|
32
|
+
and opens nothing.
|
|
33
|
+
|
|
34
|
+
**Old run records are deleted even when their checkout is already gone.** A record past retention
|
|
35
|
+
whose checkout had been removed some other way was refused on every cleanup and kept for good, since
|
|
36
|
+
the removal guard found no marker in a directory that was not there. The Runs page's cleanup note was
|
|
37
|
+
the first place this showed. They now go on the next pass. A checkout that cannot be looked at, as
|
|
38
|
+
opposed to one that is gone, still keeps its record.
|
|
39
|
+
|
|
40
|
+
## 0.80.0
|
|
41
|
+
|
|
42
|
+
**Runs: what is running, what is waiting, and a week of what ran.** A new Runs page, beside Reviews
|
|
43
|
+
in the sidebar and on ⌘K E from anywhere, lists every analysis, Update review, re-run and KISS
|
|
44
|
+
review as its own entry, whether it was started here or from a terminal. Running work shows the step
|
|
45
|
+
it is on; the pull requests you added from the workspace that are waiting for a slot show as Queued.
|
|
46
|
+
Below that, every run of the last seven days says how it ended (Completed, Failed or Cancelled), when,
|
|
47
|
+
how long it took, who asked for it (Workspace or CLI) and which agent ran it, with the reason when it
|
|
48
|
+
failed. A review analysed, then updated, then re-run is three entries, and a Retry is a new entry
|
|
49
|
+
rather than a rewrite of the attempt it retried.
|
|
50
|
+
|
|
51
|
+
Three counts sit above the lists: Running, Queued, and Failed over the last seven days. Search,
|
|
52
|
+
Status and Operation filters search the whole week, not only the runs on screen, and stay set while
|
|
53
|
+
the page refreshes itself as runs start and end. Open review opens the review as it is now; when the
|
|
54
|
+
review presents a later re-run's analysis instead of a run's own, that run says so. A re-run still going, or one that failed or was stopped, replaces nothing, and
|
|
55
|
+
neither does an update, which changes an analysis in place. Remove takes an ended run off the
|
|
56
|
+
page and changes nothing about its review; it is not offered on a run whose job is running again.
|
|
57
|
+
|
|
58
|
+
A run whose driver has gone, such as a terminal that was closed mid-review, is shown as Not
|
|
59
|
+
responding rather than as running, and reads Failed once the existing half-hour check gives up on it.
|
|
60
|
+
|
|
61
|
+
**What is kept.** Finished runs are now kept for seven days; their isolated checkouts are still
|
|
62
|
+
removed after six hours, and Retry on an older failure makes a new checkout. A review that failed
|
|
63
|
+
therefore keeps saying "Analysis failed" for up to seven days rather than six hours. Deleting a review
|
|
64
|
+
keeps a record that its runs happened, with none of their output, the conversation or the agent's
|
|
65
|
+
words, until the seven days are up; the entry reads "Review deleted" and links nowhere. Nothing older
|
|
66
|
+
than seven days is ever shown. Old records and checkouts are deleted by the workspace server, once
|
|
67
|
+
when it starts and every ten minutes while it runs, so a server left stopped deletes nothing until
|
|
68
|
+
it next starts. If a cleanup cannot remove something, the Runs page says what and why, an open
|
|
69
|
+
page included, and the next pass tries again.
|
|
70
|
+
|
|
71
|
+
Queued work is this server's own: pull requests waiting in a terminal's `--pr` batch appear once
|
|
72
|
+
they start, as on the Reviews page.
|
|
73
|
+
|
|
3
74
|
## 0.79.0
|
|
4
75
|
|
|
5
76
|
**In progress: everything being analysed, in one place.** Reviews now opens with an In progress
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
2
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
3
|
-
import { accessSync, appendFileSync, closeSync, constants, existsSync, linkSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
+
import { accessSync, appendFileSync, closeSync, constants, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -3910,7 +3910,7 @@ function* jsonCandidates(text) {
|
|
|
3910
3910
|
for (const match of text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
|
|
3911
3911
|
const inner = match[1].trim();
|
|
3912
3912
|
if (!inner.startsWith("{") && !inner.startsWith("[")) continue;
|
|
3913
|
-
const value = parsed(inner);
|
|
3913
|
+
const value = parsed$1(inner);
|
|
3914
3914
|
if (value.ok) yield {
|
|
3915
3915
|
text: inner,
|
|
3916
3916
|
value: value.value
|
|
@@ -3922,7 +3922,7 @@ function* jsonCandidates(text) {
|
|
|
3922
3922
|
const end = closingIndex(text, start, opener);
|
|
3923
3923
|
if (end === -1) continue;
|
|
3924
3924
|
const candidate = text.slice(start, end + 1);
|
|
3925
|
-
const value = parsed(candidate);
|
|
3925
|
+
const value = parsed$1(candidate);
|
|
3926
3926
|
if (!value.ok) continue;
|
|
3927
3927
|
yield {
|
|
3928
3928
|
text: candidate,
|
|
@@ -3931,7 +3931,7 @@ function* jsonCandidates(text) {
|
|
|
3931
3931
|
start = end;
|
|
3932
3932
|
}
|
|
3933
3933
|
}
|
|
3934
|
-
function parsed(candidate) {
|
|
3934
|
+
function parsed$1(candidate) {
|
|
3935
3935
|
try {
|
|
3936
3936
|
return {
|
|
3937
3937
|
ok: true,
|
|
@@ -6217,6 +6217,11 @@ function summarise(workspace) {
|
|
|
6217
6217
|
questions: questions.length,
|
|
6218
6218
|
unansweredQuestions: questionsOutstanding(questions, answered).open,
|
|
6219
6219
|
messages: workspace.transcript.length,
|
|
6220
|
+
...workspace.jobId && result ? { analysisJobId: workspace.jobId } : {},
|
|
6221
|
+
...(() => {
|
|
6222
|
+
const kiss = presentedKissRunOf(workspace);
|
|
6223
|
+
return kiss ? { presentedKissJobId: kiss.jobId } : {};
|
|
6224
|
+
})(),
|
|
6220
6225
|
lastViewedAt: workspace.lastViewedAt,
|
|
6221
6226
|
completedAt: workspace.completedAt,
|
|
6222
6227
|
pinned: workspace.pinned === true,
|
|
@@ -6477,6 +6482,17 @@ function workspaceIdFrom(entry) {
|
|
|
6477
6482
|
* the other's file by accident.
|
|
6478
6483
|
*/
|
|
6479
6484
|
var CLAIMS_DIR = join(STORE_ROOT, "claims");
|
|
6485
|
+
/**
|
|
6486
|
+
* How long a claim stands without being renewed.
|
|
6487
|
+
*
|
|
6488
|
+
* Deliberately short, and it can be short because renewal is on a timer rather than tied to the
|
|
6489
|
+
* work. The job record's own `updatedAt` moves only when a phase changes or the agent streams a
|
|
6490
|
+
* line, which is why the sweep has to allow it thirty minutes of silence; a heartbeat keeps ticking
|
|
6491
|
+
* through a twenty minute analysis that says nothing at all. Three minutes leaves room for several
|
|
6492
|
+
* missed renewals on an event loop busy parsing a large diff, and still frees a crashed holder long
|
|
6493
|
+
* before anyone would think to ask why Retry does nothing.
|
|
6494
|
+
*/
|
|
6495
|
+
var CLAIM_LEASE_MS = 18e4;
|
|
6480
6496
|
/** Comfortably inside the lease, so one slow tick is never enough to lose a claim that is held. */
|
|
6481
6497
|
var CLAIM_RENEW_MS = 3e4;
|
|
6482
6498
|
function claimPath(key) {
|
|
@@ -6882,6 +6898,128 @@ var JOBS_DIR = join(STORE_ROOT, "jobs");
|
|
|
6882
6898
|
function reusesImportedConversation(job) {
|
|
6883
6899
|
return job.reviewType === "kiss";
|
|
6884
6900
|
}
|
|
6901
|
+
/**
|
|
6902
|
+
* What survives of a finished run when the reviewer deletes its review.
|
|
6903
|
+
*
|
|
6904
|
+
* Runs is a history of executions, not of reviews, so "the review you deleted was analysed twice
|
|
6905
|
+
* yesterday and the first attempt failed" stays true after the deletion. What the reviewer deleted
|
|
6906
|
+
* must not: everything a run produced or was given goes, and so does every sentence that might
|
|
6907
|
+
* quote it. Kept are identity and times, the outcome as a kind rather than a message, which client
|
|
6908
|
+
* asked and which agent ran, and the change's own name (repository, branch, pull request), which
|
|
6909
|
+
* is what makes the entry recognisable at all.
|
|
6910
|
+
*
|
|
6911
|
+
* An allow-list, built field by field, so a field added to the job later is dropped here until
|
|
6912
|
+
* somebody decides it should not be.
|
|
6913
|
+
*/
|
|
6914
|
+
function strippedForDeletedReview(job) {
|
|
6915
|
+
const request = job.target.pullRequest;
|
|
6916
|
+
const attemptFailure = (failure) => failure ? { failure: {
|
|
6917
|
+
message: "",
|
|
6918
|
+
...failure.kind ? { kind: failure.kind } : {},
|
|
6919
|
+
...failure.stopped ? { stopped: true } : {}
|
|
6920
|
+
} } : {};
|
|
6921
|
+
return {
|
|
6922
|
+
id: job.id,
|
|
6923
|
+
workspaceId: job.workspaceId,
|
|
6924
|
+
target: {
|
|
6925
|
+
originRepoPath: job.target.originRepoPath,
|
|
6926
|
+
branch: job.target.branch,
|
|
6927
|
+
sha: job.target.sha,
|
|
6928
|
+
...request ? { pullRequest: {
|
|
6929
|
+
reference: request.url ?? "",
|
|
6930
|
+
headRef: request.headRef,
|
|
6931
|
+
headSha: request.headSha,
|
|
6932
|
+
...request.number !== void 0 ? { number: request.number } : {},
|
|
6933
|
+
...request.url ? { url: request.url } : {},
|
|
6934
|
+
...request.source ? { source: request.source } : {}
|
|
6935
|
+
} } : {}
|
|
6936
|
+
},
|
|
6937
|
+
...job.purpose ? { purpose: job.purpose } : {},
|
|
6938
|
+
...job.reanalysis ? { reanalysis: true } : {},
|
|
6939
|
+
...job.reviewType ? { reviewType: job.reviewType } : {},
|
|
6940
|
+
phase: job.phase,
|
|
6941
|
+
startedAt: job.startedAt,
|
|
6942
|
+
updatedAt: job.updatedAt,
|
|
6943
|
+
...job.finishedAt !== void 0 ? { finishedAt: job.finishedAt } : {},
|
|
6944
|
+
completed: [],
|
|
6945
|
+
...job.failure ? { failure: {
|
|
6946
|
+
phase: job.failure.phase,
|
|
6947
|
+
message: "",
|
|
6948
|
+
...job.failure.kind ? { kind: job.failure.kind } : {},
|
|
6949
|
+
...job.failure.stopped ? { stopped: true } : {}
|
|
6950
|
+
} } : {},
|
|
6951
|
+
env: job.env.agentId ? { agentId: job.env.agentId } : {},
|
|
6952
|
+
attemptId: attemptIdOf(job),
|
|
6953
|
+
...job.source ? { source: job.source } : {},
|
|
6954
|
+
...job.previousAttempts ? { previousAttempts: job.previousAttempts.map((attempt) => ({
|
|
6955
|
+
attemptId: attempt.attemptId,
|
|
6956
|
+
startedAt: attempt.startedAt,
|
|
6957
|
+
finishedAt: attempt.finishedAt,
|
|
6958
|
+
phase: attempt.phase,
|
|
6959
|
+
...attemptFailure(attempt.failure),
|
|
6960
|
+
...attempt.source ? { source: attempt.source } : {},
|
|
6961
|
+
...attempt.agentId ? { agentId: attempt.agentId } : {}
|
|
6962
|
+
})) } : {},
|
|
6963
|
+
...job.removedFromRuns ? { removedFromRuns: job.removedFromRuns } : {},
|
|
6964
|
+
reviewDeleted: true
|
|
6965
|
+
};
|
|
6966
|
+
}
|
|
6967
|
+
/**
|
|
6968
|
+
* How long a finished run is listed, and kept: measured from when it ended, per attempt.
|
|
6969
|
+
*
|
|
6970
|
+
* A strict limit on what the page shows, not only on what the sweep deletes. The sweep runs inside
|
|
6971
|
+
* the workspace server, so while the server is stopped nothing is deleted, and a record can outlive
|
|
6972
|
+
* its week on disk until the next start; the page never shows it past the week regardless.
|
|
6973
|
+
*/
|
|
6974
|
+
var RUN_RECORD_RETENTION_MS = 6048e5;
|
|
6975
|
+
/** The current attempt's id. See `ReviewJob.attemptId`. */
|
|
6976
|
+
function attemptIdOf(job) {
|
|
6977
|
+
return job.attemptId ?? job.id;
|
|
6978
|
+
}
|
|
6979
|
+
/**
|
|
6980
|
+
* The record a driver should save as it picks this job up, before it does any work.
|
|
6981
|
+
*
|
|
6982
|
+
* A job that had ended is starting a new attempt: the ended one is archived, a new attempt id is
|
|
6983
|
+
* minted and the record leaves its terminal phase, all in the one value returned, so the caller
|
|
6984
|
+
* makes a single write. That is what makes the archive happen exactly once. A crash after the write
|
|
6985
|
+
* leaves a working job nobody drives, which the sweep in time records as stopped, and never a
|
|
6986
|
+
* terminal one that the next pickup would archive a second time; the id check covers a record that
|
|
6987
|
+
* was archived and then, somehow, written back terminal.
|
|
6988
|
+
*
|
|
6989
|
+
* A job that had not ended is the same attempt resuming (a restarted daemon, a driver whose claim
|
|
6990
|
+
* lapsed): only `startedAt` moves, because the page times "Running for" from it.
|
|
6991
|
+
*/
|
|
6992
|
+
function beginAttempt(job, next) {
|
|
6993
|
+
if (!isTerminal(job)) return {
|
|
6994
|
+
...job,
|
|
6995
|
+
startedAt: next.startedAt
|
|
6996
|
+
};
|
|
6997
|
+
const ended = attemptIdOf(job);
|
|
6998
|
+
const earlier = job.previousAttempts ?? [];
|
|
6999
|
+
const current = earlier.filter((attempt) => next.startedAt - attempt.finishedAt < RUN_RECORD_RETENTION_MS);
|
|
7000
|
+
const archived = earlier.some((attempt) => attempt.attemptId === ended) ? current : [...current, {
|
|
7001
|
+
attemptId: ended,
|
|
7002
|
+
startedAt: job.startedAt,
|
|
7003
|
+
finishedAt: endedAt(job),
|
|
7004
|
+
phase: job.phase,
|
|
7005
|
+
...job.failure ? { failure: {
|
|
7006
|
+
message: job.failure.message,
|
|
7007
|
+
...job.failure.kind ? { kind: job.failure.kind } : {},
|
|
7008
|
+
...job.failure.stopped ? { stopped: true } : {}
|
|
7009
|
+
} } : {},
|
|
7010
|
+
...job.source ? { source: job.source } : {},
|
|
7011
|
+
...job.env.agentId ? { agentId: job.env.agentId } : {}
|
|
7012
|
+
}];
|
|
7013
|
+
const { failure: _failure, finishedAt: _finishedAt, progress: _progress, ...rest } = job;
|
|
7014
|
+
return {
|
|
7015
|
+
...rest,
|
|
7016
|
+
phase: next.phase,
|
|
7017
|
+
startedAt: next.startedAt,
|
|
7018
|
+
attemptId: randomUUID(),
|
|
7019
|
+
previousAttempts: archived,
|
|
7020
|
+
...next.source ? { source: next.source } : {}
|
|
7021
|
+
};
|
|
7022
|
+
}
|
|
6885
7023
|
function jobPath(id) {
|
|
6886
7024
|
return join(JOBS_DIR, `${id}.json`);
|
|
6887
7025
|
}
|
|
@@ -6889,6 +7027,7 @@ function createJob(input) {
|
|
|
6889
7027
|
const now = Date.now();
|
|
6890
7028
|
return {
|
|
6891
7029
|
id: randomUUID(),
|
|
7030
|
+
attemptId: randomUUID(),
|
|
6892
7031
|
phase: "preparing",
|
|
6893
7032
|
startedAt: now,
|
|
6894
7033
|
updatedAt: now,
|
|
@@ -6896,9 +7035,30 @@ function createJob(input) {
|
|
|
6896
7035
|
...input
|
|
6897
7036
|
};
|
|
6898
7037
|
}
|
|
7038
|
+
/**
|
|
7039
|
+
* Replace the record whole, so a reader sees the old one or the new one and never a prefix.
|
|
7040
|
+
*
|
|
7041
|
+
* `writeFileSync` truncates on open, and a crash between that and the end of the write left a record
|
|
7042
|
+
* that does not parse, which `loadJob` reads as no job at all: the review's analysis, its attempts
|
|
7043
|
+
* and its checkout's only pointer gone at once. Renamed into place instead, from a name unique to
|
|
7044
|
+
* this write, because more than one process writes job records. Serialised first, so a throw in
|
|
7045
|
+
* `JSON.stringify` cannot leave anything behind. Unlike `writeJsonAtomically`, a failure throws:
|
|
7046
|
+
* every caller of this already handles a write that fails.
|
|
7047
|
+
*/
|
|
6899
7048
|
function saveJob(job) {
|
|
7049
|
+
const document = JSON.stringify(job);
|
|
6900
7050
|
mkdirSync(JOBS_DIR, { recursive: true });
|
|
6901
|
-
|
|
7051
|
+
const path = jobPath(job.id);
|
|
7052
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
7053
|
+
try {
|
|
7054
|
+
writeFileSync(temp, document, { mode: 384 });
|
|
7055
|
+
renameSync(temp, path);
|
|
7056
|
+
} catch (error) {
|
|
7057
|
+
try {
|
|
7058
|
+
unlinkSync(temp);
|
|
7059
|
+
} catch {}
|
|
7060
|
+
throw error;
|
|
7061
|
+
}
|
|
6902
7062
|
}
|
|
6903
7063
|
function loadJob(id) {
|
|
6904
7064
|
try {
|
|
@@ -6925,21 +7085,81 @@ function migrate(job) {
|
|
|
6925
7085
|
}
|
|
6926
7086
|
};
|
|
6927
7087
|
}
|
|
7088
|
+
/**
|
|
7089
|
+
* Every record parsed so far, by file, with the identity of the file it was parsed from.
|
|
7090
|
+
*
|
|
7091
|
+
* Since the Runs page, the directory holds a week of records rather than six hours of them, and the
|
|
7092
|
+
* Reviews and Runs pages each read all of it on every poll. Read and parsed whole, 500 records cost
|
|
7093
|
+
* about 100 ms a time, synchronously, inside the server; asking the filesystem whether each file is
|
|
7094
|
+
* still the one already parsed costs about 1 ms. `saveJob` renames a fresh file into place, so a
|
|
7095
|
+
* record that was written in any way has a new inode, and inode, mtime and size together are a
|
|
7096
|
+
* change no write can slip past.
|
|
7097
|
+
*
|
|
7098
|
+
* Frozen, because every caller now shares one copy: a caller that edited a record in place would
|
|
7099
|
+
* be editing it for everyone, and strict mode turns that into an error where it happens.
|
|
7100
|
+
*/
|
|
7101
|
+
var parsed = /* @__PURE__ */ new Map();
|
|
6928
7102
|
function allJobs() {
|
|
6929
7103
|
let names;
|
|
6930
7104
|
try {
|
|
6931
7105
|
names = readdirSync(JOBS_DIR);
|
|
6932
7106
|
} catch {
|
|
7107
|
+
parsed.clear();
|
|
6933
7108
|
return [];
|
|
6934
7109
|
}
|
|
6935
7110
|
const jobs = [];
|
|
7111
|
+
const present = /* @__PURE__ */ new Set();
|
|
6936
7112
|
for (const name of names) {
|
|
6937
7113
|
if (!name.endsWith(".json")) continue;
|
|
7114
|
+
present.add(name);
|
|
7115
|
+
let identity;
|
|
7116
|
+
try {
|
|
7117
|
+
const stat = statSync(join(JOBS_DIR, name));
|
|
7118
|
+
identity = `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
|
|
7119
|
+
} catch {
|
|
7120
|
+
continue;
|
|
7121
|
+
}
|
|
7122
|
+
const cached = parsed.get(name);
|
|
7123
|
+
if (cached?.identity === identity) {
|
|
7124
|
+
jobs.push(cached.job);
|
|
7125
|
+
continue;
|
|
7126
|
+
}
|
|
6938
7127
|
const job = loadJob(name.slice(0, -5));
|
|
6939
|
-
if (job)
|
|
7128
|
+
if (!job) {
|
|
7129
|
+
parsed.delete(name);
|
|
7130
|
+
continue;
|
|
7131
|
+
}
|
|
7132
|
+
Object.freeze(job);
|
|
7133
|
+
parsed.set(name, {
|
|
7134
|
+
identity,
|
|
7135
|
+
job
|
|
7136
|
+
});
|
|
7137
|
+
jobs.push(job);
|
|
6940
7138
|
}
|
|
7139
|
+
for (const name of parsed.keys()) if (!present.has(name)) parsed.delete(name);
|
|
6941
7140
|
return jobs;
|
|
6942
7141
|
}
|
|
7142
|
+
/**
|
|
7143
|
+
* Temporary files a `saveJob` left behind when its process died between the write and the rename.
|
|
7144
|
+
*
|
|
7145
|
+
* Invisible to every reader, which lists `.json` only, so they cost nothing but disk. A write lasts
|
|
7146
|
+
* milliseconds, so one older than `olderThanMs` is residue rather than a write in flight.
|
|
7147
|
+
*/
|
|
7148
|
+
function pruneJobResidue(now, olderThanMs) {
|
|
7149
|
+
let names;
|
|
7150
|
+
try {
|
|
7151
|
+
names = readdirSync(JOBS_DIR);
|
|
7152
|
+
} catch {
|
|
7153
|
+
return;
|
|
7154
|
+
}
|
|
7155
|
+
for (const name of names) {
|
|
7156
|
+
if (!name.endsWith(".tmp")) continue;
|
|
7157
|
+
const path = join(JOBS_DIR, name);
|
|
7158
|
+
try {
|
|
7159
|
+
if (now - statSync(path).mtimeMs >= olderThanMs) unlinkSync(path);
|
|
7160
|
+
} catch {}
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
6943
7163
|
function deleteJob(id) {
|
|
6944
7164
|
try {
|
|
6945
7165
|
unlinkSync(jobPath(id));
|
|
@@ -15938,6 +16158,7 @@ async function startJob(input) {
|
|
|
15938
16158
|
target,
|
|
15939
16159
|
env: agent.captureEnv(),
|
|
15940
16160
|
purpose: "review",
|
|
16161
|
+
...input.source ? { source: input.source } : {},
|
|
15941
16162
|
...input.requestedModel ? { requestedModel: input.requestedModel } : {},
|
|
15942
16163
|
...review.type === "kiss" ? {
|
|
15943
16164
|
reviewType: "kiss",
|
|
@@ -15971,7 +16192,7 @@ async function startJob(input) {
|
|
|
15971
16192
|
* The previous result is snapshotted before the session is replaced, so the review keeps saying who
|
|
15972
16193
|
* made the analysis it is retiring as well as who made the one it now shows.
|
|
15973
16194
|
*/
|
|
15974
|
-
async function reanalyseReview(workspaceId, agentId, onStarted, requestedModel) {
|
|
16195
|
+
async function reanalyseReview(workspaceId, agentId, onStarted, requestedModel, source) {
|
|
15975
16196
|
const workspace = loadWorkspace(workspaceId);
|
|
15976
16197
|
if (!workspace) throw new Error("No such review.");
|
|
15977
16198
|
const sha = workspace.session.metadata?.commitSha;
|
|
@@ -16003,6 +16224,7 @@ async function reanalyseReview(workspaceId, agentId, onStarted, requestedModel)
|
|
|
16003
16224
|
env: agent.captureEnv(),
|
|
16004
16225
|
purpose: "review",
|
|
16005
16226
|
reanalysis: true,
|
|
16227
|
+
...source ? { source } : {},
|
|
16006
16228
|
...model ? { requestedModel: model } : {}
|
|
16007
16229
|
});
|
|
16008
16230
|
saveJob(job);
|
|
@@ -16062,7 +16284,7 @@ async function runJob(workspaceId, runners = {}, reviewType = "standard", option
|
|
|
16062
16284
|
outer?.addEventListener("abort", () => controller.abort(), { once: true });
|
|
16063
16285
|
const unwatch = watchForStop(started.jobId, controller);
|
|
16064
16286
|
try {
|
|
16065
|
-
return await drive(started.jobId, workspaceId, runners, started.claim.claim.holder, controller.signal);
|
|
16287
|
+
return await drive(started.jobId, workspaceId, runners, started.claim.claim.holder, controller.signal, options.source);
|
|
16066
16288
|
} finally {
|
|
16067
16289
|
unwatch();
|
|
16068
16290
|
clearJobEnding(started.jobId);
|
|
@@ -16117,16 +16339,18 @@ function runOfJob(workspace, jobId) {
|
|
|
16117
16339
|
if (!run) throw new Error("This KISS job has no run to write its answer to.");
|
|
16118
16340
|
return run;
|
|
16119
16341
|
}
|
|
16120
|
-
async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
16342
|
+
async function drive(jobId, workspaceId, runners, driver, signal, source) {
|
|
16121
16343
|
let job = loadJob(jobId);
|
|
16122
16344
|
if (!job) throw new Error("No such job.");
|
|
16123
16345
|
const now = runners.now ?? Date.now;
|
|
16124
16346
|
const startedAt = now();
|
|
16125
|
-
const from = resumeFrom(job);
|
|
16126
|
-
job = {
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16347
|
+
const from = checkoutGone$1(job) ? "preparing" : resumeFrom(job);
|
|
16348
|
+
job = beginAttempt(job, {
|
|
16349
|
+
startedAt,
|
|
16350
|
+
phase: from,
|
|
16351
|
+
...source ? { source } : {}
|
|
16352
|
+
});
|
|
16353
|
+
saveJob(job);
|
|
16130
16354
|
try {
|
|
16131
16355
|
signal?.throwIfAborted();
|
|
16132
16356
|
if (needs(from, "preparing")) {
|
|
@@ -16356,6 +16580,18 @@ function explain(job, detail) {
|
|
|
16356
16580
|
}
|
|
16357
16581
|
}
|
|
16358
16582
|
/** True when this phase is at or after the point the job resumes from. */
|
|
16583
|
+
/**
|
|
16584
|
+
* The record says work was done in a checkout that is no longer there.
|
|
16585
|
+
*
|
|
16586
|
+
* The sweep removes a finished job's checkout after six hours and keeps its record for seven days,
|
|
16587
|
+
* so a Retry of an older failure finds `preparing` complete and nothing on disk. Starting from
|
|
16588
|
+
* `preparing` makes the checkout again at the job's commit, and everything after it with it, because
|
|
16589
|
+
* every later phase reads files from it.
|
|
16590
|
+
*/
|
|
16591
|
+
function checkoutGone$1(job) {
|
|
16592
|
+
if (!job.completed.includes("preparing")) return false;
|
|
16593
|
+
return !job.worktreePath || !existsSync(join(job.worktreePath, ".prreviewbuddy-worktree.json"));
|
|
16594
|
+
}
|
|
16359
16595
|
function needs(from, phase) {
|
|
16360
16596
|
return WORK_PHASES.indexOf(phase) >= WORK_PHASES.indexOf(from);
|
|
16361
16597
|
}
|
|
@@ -16642,7 +16878,7 @@ function failureOf(workspaceId) {
|
|
|
16642
16878
|
* "whatever the default is", so a configured default of Codex announced Codex and then analysed
|
|
16643
16879
|
* with Claude, and nothing said so until the failure named the wrong tool.
|
|
16644
16880
|
*/
|
|
16645
|
-
async function prepareReview(target, agentId, fresh, requestedModel) {
|
|
16881
|
+
async function prepareReview(target, agentId, fresh, requestedModel, source) {
|
|
16646
16882
|
if (!fresh) {
|
|
16647
16883
|
const existing = await findExistingReview(target);
|
|
16648
16884
|
if (existing) {
|
|
@@ -16662,7 +16898,8 @@ async function prepareReview(target, agentId, fresh, requestedModel) {
|
|
|
16662
16898
|
workspaceId: await startJob({
|
|
16663
16899
|
target,
|
|
16664
16900
|
agentId,
|
|
16665
|
-
...requestedModel ? { requestedModel } : {}
|
|
16901
|
+
...requestedModel ? { requestedModel } : {},
|
|
16902
|
+
...source ? { source } : {}
|
|
16666
16903
|
})
|
|
16667
16904
|
};
|
|
16668
16905
|
}
|
|
@@ -16679,10 +16916,13 @@ async function prepareReview(target, agentId, fresh, requestedModel) {
|
|
|
16679
16916
|
* that reads as a clean bill of health, which is the opposite of what happened. A KISS run's
|
|
16680
16917
|
* answer is checked by `drive`, which knows which run it is.
|
|
16681
16918
|
*/
|
|
16682
|
-
async function settleReview(workspaceId, reviewType = "standard", signal) {
|
|
16919
|
+
async function settleReview(workspaceId, reviewType = "standard", signal, source) {
|
|
16683
16920
|
let job;
|
|
16684
16921
|
try {
|
|
16685
|
-
job = await runJob(workspaceId, {}, reviewType,
|
|
16922
|
+
job = await runJob(workspaceId, {}, reviewType, {
|
|
16923
|
+
...signal ? { signal } : {},
|
|
16924
|
+
...source ? { source } : {}
|
|
16925
|
+
});
|
|
16686
16926
|
} catch (error) {
|
|
16687
16927
|
return {
|
|
16688
16928
|
ok: false,
|
|
@@ -17645,6 +17885,7 @@ async function recordKissRun(input) {
|
|
|
17645
17885
|
purpose: "review",
|
|
17646
17886
|
reviewType: "kiss",
|
|
17647
17887
|
audience: input.audience,
|
|
17888
|
+
...input.source ? { source: input.source } : {},
|
|
17648
17889
|
...requestedModel ? { requestedModel } : {}
|
|
17649
17890
|
});
|
|
17650
17891
|
saveJob(job);
|
|
@@ -18608,6 +18849,7 @@ async function updateReview(workspace, runners = {}) {
|
|
|
18608
18849
|
if (workspace.changeSet.targetType === "commit") return refuse(reviewedSha, false, COMMIT_REVIEW_CANNOT_UPDATE);
|
|
18609
18850
|
const held = await claimCheckoutJob(workspace, reviewedSha ?? "", {
|
|
18610
18851
|
purpose: "update",
|
|
18852
|
+
...runners.source ? { source: runners.source } : {},
|
|
18611
18853
|
...workspace.prContext ? { conversation: {
|
|
18612
18854
|
kind: "attached",
|
|
18613
18855
|
context: workspace.prContext
|
|
@@ -19298,7 +19540,7 @@ async function runUpdate(workspaceId, prompt, cwd, originRepoPath, env, onProgre
|
|
|
19298
19540
|
* nothing is running has answered the same question better, and passes 0.
|
|
19299
19541
|
*/
|
|
19300
19542
|
async function discardJob(job, options = {}) {
|
|
19301
|
-
if (job.worktreePath) {
|
|
19543
|
+
if (job.worktreePath && !checkoutGone(job.worktreePath)) {
|
|
19302
19544
|
const outcome = await removeWorktree({
|
|
19303
19545
|
originRepoPath: job.target.originRepoPath,
|
|
19304
19546
|
worktreePath: job.worktreePath,
|
|
@@ -19313,6 +19555,18 @@ async function discardJob(job, options = {}) {
|
|
|
19313
19555
|
clearClaim(job.id);
|
|
19314
19556
|
return { removed: true };
|
|
19315
19557
|
}
|
|
19558
|
+
/**
|
|
19559
|
+
* Only when the filesystem says there is nothing there. Any other failure to look, a permission
|
|
19560
|
+
* error say, is not proof of absence, and the record is the only thing that names the directory.
|
|
19561
|
+
*/
|
|
19562
|
+
function checkoutGone(path) {
|
|
19563
|
+
try {
|
|
19564
|
+
lstatSync(path);
|
|
19565
|
+
return false;
|
|
19566
|
+
} catch (error) {
|
|
19567
|
+
return error.code === "ENOENT";
|
|
19568
|
+
}
|
|
19569
|
+
}
|
|
19316
19570
|
//#endregion
|
|
19317
19571
|
//#region ../../packages/review-harness/src/workspace/delete_review.ts
|
|
19318
19572
|
/**
|
|
@@ -19396,13 +19650,33 @@ async function deleteReview(id) {
|
|
|
19396
19650
|
*
|
|
19397
19651
|
* The record is unlinked **whether or not** a checkout was refused, and that is deliberate. The
|
|
19398
19652
|
* review is what was asked for; a directory the guard declined keeps its job record, so the sweep
|
|
19399
|
-
* comes back to it
|
|
19653
|
+
* comes back to it once the checkout is past retention, with the origin repository still recorded, and the refusal is
|
|
19400
19654
|
* reported so nobody has to discover it later.
|
|
19401
19655
|
*/
|
|
19402
19656
|
async function deleteLocked(id) {
|
|
19403
19657
|
const report = emptyReport();
|
|
19404
19658
|
if (!loadWorkspace(id)) return report;
|
|
19405
19659
|
for (const job of allJobs().filter((job) => job.workspaceId === id)) {
|
|
19660
|
+
if (isTerminal(job) && purposeOf(job) !== "ask") {
|
|
19661
|
+
if (job.worktreePath && existsSync(job.worktreePath)) {
|
|
19662
|
+
const outcome = await removeWorktree({
|
|
19663
|
+
originRepoPath: job.target.originRepoPath,
|
|
19664
|
+
worktreePath: job.worktreePath,
|
|
19665
|
+
minAgeMs: 0
|
|
19666
|
+
});
|
|
19667
|
+
if (!outcome.removed) {
|
|
19668
|
+
report.refusals.push(outcome.message);
|
|
19669
|
+
saveJob({
|
|
19670
|
+
...strippedForDeletedReview(job),
|
|
19671
|
+
worktreePath: job.worktreePath
|
|
19672
|
+
});
|
|
19673
|
+
continue;
|
|
19674
|
+
}
|
|
19675
|
+
report.worktreesRemoved += 1;
|
|
19676
|
+
}
|
|
19677
|
+
saveJob(strippedForDeletedReview(job));
|
|
19678
|
+
continue;
|
|
19679
|
+
}
|
|
19406
19680
|
const outcome = await discardJob(job, { minAgeMs: 0 });
|
|
19407
19681
|
if (outcome.refusal) {
|
|
19408
19682
|
report.refusals.push(outcome.refusal);
|
|
@@ -19461,4 +19735,4 @@ function lineageIds(id) {
|
|
|
19461
19735
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
19462
19736
|
}
|
|
19463
19737
|
//#endregion
|
|
19464
|
-
export { resolveWorkspacePort as $,
|
|
19738
|
+
export { resolveWorkspacePort as $, AgentUnavailableError as $i, attemptIdOf as $n, isIssueOutstanding as $r, moveIds as $t, parsePullRequestLink as A, resolveTarget as Ai, modelHelpLines as An, recentWorkspaces as Ar, readArchitecture as At, ensureServer as B, resolveAgent as Bi, requestJobStop as Bn, isLifecycle as Br, writeJsonAtomically as Bt, completeRun as C, followUpFindingId as Ci, hasAuthorContent as Cn, lineagePosition as Cr, RUNTIME_WORDS as Ct, relativeTime as D, describeAuthorship as Di, CODE_COPY_CLASS as Dn, positionInLineage as Dr, checkArchitecture as Dt, stopRun as E, objectWithAnyOf as Ei, CODE_BLOCK_CLASS as En, mutateWorkspace as Er, ARCHITECTURE_DIR as Et, queueForReview as F, AmbiguousRefError as Fi, createWorktree as Fn, workspaceRevision as Fr, ownersOf as Ft, stopChild as G, CONFIG_PATH as Gi, checkReview as Gn, parseLifecycle as Gr, createArchitectureNotification as Gt, reviewsUrl as H, clearAgentPreference as Hi, describeSubject as Hn, lifecycleItemLabel as Hr, branchOf as Ht, settleQueue as I, shortNameOf as Ii, holdWorktree as In, workspaceStamps as Ir, QUOTE_LIMITS as It, writeServerRecord as J, agentById as Ji, fetchHead as Jn, queueAddMany as Jr, eventName as Jt, stopServer as K, STORE_ROOT as Ki, followedBranch as Kn, sectionOf as Kr, createPrState as Kt, readIndexToken as L, shorthandOf as Li, isWorktreeHeld as Ln, LIFECYCLES as Lr, REPOSITORY_SCOPE as Lt, runBatch as M, DetachedHeadError as Mi, isBusy as Mn, reviewedRepositories as Mr, storeArchitecture as Mt, activateQueued as N, isCommitReview as Ni, withReviewLock as Nn, summarise as Nr, storedScopes as Nt, batchIdentity as O, WITHHELD_REASON as Oi, escapeHtml as On, presentedKissRunOf as Or, loadArchitecture as Ot, compactQueue as P, resolveBase as Pi, MANAGED_ROOT as Pn, touchWorkspace as Pr, trackedFiles as Pt, parseWorkspacePort as Q, AgentCancelledError as Qi, allJobs as Qn, doneVerb as Qr, markReadForReview as Qt, bootstrapUrl as R, git as Ri, readMarker as Rn, activityAttributes as Rr, ancestorsOf as Rt, beginRun as S, wasReassessed as Si, authorPreview as Sn, latestKissRun as Sr, recordRuntimeBestEffort as St, reportRun as T, extractJsonText as Ti, renderMarkdown as Tn, matchingWorkspaceIds as Tr, formatDuration as Tt, serverEntryExists as U, readAgentPreference as Ui, findExistingReview as Un, lifecyclePill as Ur, subjectOf as Ut, readServerRecord as V, resolveModel as Vi, describeMovement as Vn, lifecycleChip as Vr, announceJob as Vt, spawnServer as W, writeAgentPreference as Wi, checkCodeFreshness as Wn, offersLifecycle as Wr, categoryOf as Wt, MAX_WORKSPACE_PORT as X, detectAgents as Xi, fillFileUrlTemplate as Xn, queueStamp as Xr, markAllRead as Xt, DEFAULT_WORKSPACE_PORT as Y, agentFor as Yi, forgeResolver as Yn, queueRemove as Yr, listNotifications as Yt, MIN_WORKSPACE_PORT as Z, isAgentId as Zi, RUN_RECORD_RETENTION_MS as Zn, readQueue as Zr, markRead as Zt, recordKissRun as _, gradeScale as _i, isUnchanged as _n, groupByLineage as _r, BUILD_VERSION as _t, wasBlocked as a, processDiscussion as ai, toneOf as an, saveJob as ar, settleReview as at, track as b, reassessIn as bi, chatConversationSection as bn, kissDecisionRefusal as br, waitingHistory as bt, RefreshUnavailableError as c, explanationOf as ci, withUsageRecorded as cn, isTerminalJobPhase as cr, startJob as ct, mergeUpdateInto as d, ANALYSIS_ONLY as di, adoptLegacyBaseline as dn, purposeOf as dr, record as dt, isQuestionOutstanding as ei, pruneNotifications as en, endedAt as er, statedWorkspacePort as et, updateReview as f, analysisRefOf as fi, prepareConversationWithin as fn, clearClaim as fr, stamp as ft, runningJobs as g, findingIn as gi, refreshPrContext as gn, followedRefName as gr, telemetryUploadEnabled as gt, liveUpdateFor as h, findingAssessment as hi, unforeseenFailure as hn, decideKissRecommendation as hr, telemetryUploadConsent as ht, liveJobsFor as i, reviewerDispositions as ii, retireNotification as in, pruneJobResidue as ir, prepareReview as it, unsupportedReason as j, originRemote as ji, ReviewBeingDeletedError as jn, reviewedCommit as jr, repositoryKey as jt, extractPullRequestLinks as k, kissOutcome as ki, renderInlineText as kn, previousKissResult as kr, partFiles as kt, cancelUpdate as l, isExplanationAction as li, existingPaths as ln, phasesFor as lr, append as lt, UpdateAlreadyRunningError as m, currentGradeIn as mi, storedConversation as mn, CLAIM_LEASE_MS as mr, recordTelemetryUploadConsent as mt, deleteReview as n, questionsOutstanding as ni, readStateOf as nn, isTerminal as nr, failureOf as nt, discardJob as o, recommendationsOf as oi, unreadAround as on, PHASES as or, reanalyseReview as ot, askCheckout as p, assessmentLookup as pi, saveRefresh as pn, isClaimed as pr, deliver as pt, workspaceUrl as q, AGENT_IDS as qi, reviewedShaOf as qn, queueAdd as qr, createPrUpdate as qt, lineageIds as r, resolvedByConversation as ri, removeNotification as rn, loadJob as rr, observeReview as rt, FETCH_TIMEOUT_MS as s, explanationActionOf as si, unreadCount as sn, isLiveJob as sr, runJob as st, deleteLineage as t, issuesOutstanding as ti, readNotification as tn, fail as tr, writePortPreference as tt, cancellableHere as u, withExplanation as ui, ASK_REREAD_CEILING_MS as un, progressSteps as ur, readEvents as ut, startKissJob as v, isGradeFor as vi, agentEnvOf as vn, indexKeptIds as vr, PACKAGE_NAME as vt, endRun as w, asString$1 as wi, GITHUB_MARKDOWN as wn, loadWorkspace as wr, describeRuntimes as wt, RUN_KINDS as x, sourcePhrase as xi, UNESTABLISHED_MOTIVATION as xn, kissDecisionsOf as xr, mapRuntimes as xt, isDriving as y, promptGrade as yi, EXPLAIN_SIMPLY_PROMPT as yn, indexReviewGroups as yr, feedbackUrl as yt, clearServerRecordOwnedBy as z, refExists as zi, removeWorktree as zn, activityLabel as zr, isArchitectureId as zt };
|