prreviewbuddy 0.57.0 → 0.59.38
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 +73 -0
- package/dist/{delete_review-B3VPeozh.js → delete_review-D59T6NBn.js} +505 -70
- package/dist/main.js +1 -1
- package/dist/server.js +2323 -246
- package/package.json +1 -1
- package/static/chunk-4nFoFuBF.js +1 -0
- package/static/{chunk-BFC1myXj.js → chunk-6Hm8gwfc.js} +1 -1
- package/static/chunk-6olMbSDC.js +1 -0
- package/static/chunk-7g6hehZR.js +1 -0
- package/static/{chunk-DwZ_OKDV.js → chunk-9OYdRhXM.js} +1 -1
- package/static/{chunk-BOGxj-MG.js → chunk-9QJSvyT-.js} +1 -1
- package/static/{chunk-I02WWcy5.js → chunk-A2MXL8RD.js} +1 -1
- package/static/{chunk-CUxm3YXF.js → chunk-B-Up-Dum.js} +1 -1
- package/static/{chunk-Dlx_va8c.js → chunk-B1WAu5pS.js} +1 -1
- package/static/{chunk-BLYKMRgX.js → chunk-B3KwWIxA.js} +1 -1
- package/static/{chunk-CVKo9lqM.js → chunk-BH2wqCle.js} +1 -1
- package/static/chunk-BIhcpq89.js +1 -0
- package/static/chunk-BJt-3Igo.js +1 -0
- package/static/{chunk-Cuz7gAE5.js → chunk-BQ5h3ZKa.js} +1 -1
- package/static/chunk-BV3co1iE.js +1 -0
- package/static/{chunk-CNECTSY3.js → chunk-BW1ANL1o.js} +1 -1
- package/static/{chunk-DN473Oml.js → chunk-BXful7fw.js} +1 -1
- package/static/{chunk-nU5620mN.js → chunk-Baon_ihZ.js} +1 -1
- package/static/chunk-BcyJP52O.js +1 -0
- package/static/{chunk-DdkvfxwB.js → chunk-BfJ5btxI.js} +1 -1
- package/static/{chunk-DOebnhRz.js → chunk-BoKYaT6d.js} +1 -1
- package/static/chunk-Bqz19UgW.js +1 -0
- package/static/chunk-BvKoviOH.js +8 -0
- package/static/{chunk-DFxPlKZ4.js → chunk-C6TLHpBY.js} +1 -1
- package/static/chunk-C8f8HA4G.js +1 -0
- package/static/{chunk-tz4nb_gp.js → chunk-CCkeMk9N.js} +1 -1
- package/static/chunk-CGgxik56.js +1 -0
- package/static/{chunk-DmCmlw77.js → chunk-CIfNEzlc.js} +1 -1
- package/static/{chunk-gyMXflOg.js → chunk-CPbLHMZi.js} +1 -1
- package/static/{chunk-C3zyi6X6.js → chunk-CUEta9tE.js} +1 -1
- package/static/chunk-CU_pLmXg.js +1 -0
- package/static/{chunk-BveIbUwm.js → chunk-CV0YoHS0.js} +1 -1
- package/static/{chunk-Dx2-dNbd.js → chunk-CYEqf7aI.js} +1 -1
- package/static/{chunk-IT8Wx1rP.js → chunk-C_6e3UXl.js} +1 -1
- package/static/chunk-Caya2m0E.js +1 -0
- package/static/{chunk-2cPIwDyu.js → chunk-CkFz1wgR.js} +1 -1
- package/static/chunk-Co7Cgu9F.js +1 -0
- package/static/{chunk-CNG5uajc.js → chunk-CqXFWjSQ.js} +1 -1
- package/static/chunk-CvSN2dS5.js +1 -0
- package/static/{chunk-COmCkMOw.js → chunk-CwCL5WnJ.js} +1 -1
- package/static/chunk-D5bmztPJ.js +1 -0
- package/static/chunk-D6EJPfS7.js +1 -0
- package/static/{chunk-BeqgAIrQ.js → chunk-D6oLtkSC.js} +4 -4
- package/static/{chunk-Wc3Sd0cy.js → chunk-D71XVZIg.js} +1 -1
- package/static/{chunk-DJ4Pz6CG.js → chunk-D7hFEgee.js} +1 -1
- package/static/{chunk-BKQNYMf0.js → chunk-D9Rf_Mu5.js} +1 -1
- package/static/{chunk-CWJTY3LS.js → chunk-DF3g9oko.js} +1 -1
- package/static/{chunk-CN9Gw02i.js → chunk-DIEkEPeW.js} +1 -1
- package/static/{chunk-BhOdd-9U.js → chunk-DInBXUWf.js} +1 -1
- package/static/chunk-DJqLf38C.js +1 -0
- package/static/chunk-D_CdBIAq.js +1 -0
- package/static/{chunk-i2ycf8WY.js → chunk-Dbx-B0iA.js} +1 -1
- package/static/{chunk-B6QMLbPb.js → chunk-DcXKkJre.js} +1 -1
- package/static/{chunk-BwpXVz85.js → chunk-DgcdmK42.js} +1 -1
- package/static/chunk-DmTwixkl.js +1 -0
- package/static/chunk-DmZq9LP2.js +1 -0
- package/static/{chunk-CU_yc7c0.js → chunk-DqxlbQOu.js} +3 -3
- package/static/{chunk-CVHEykQZ.js → chunk-DrNGVTkX.js} +1 -1
- package/static/chunk-Ds3YROGC.js +1 -0
- package/static/{chunk-DecH1K7C.js → chunk-DuX-olw5.js} +1 -1
- package/static/{chunk-BsaS8BTP.js → chunk-DwHSAwhM.js} +1 -1
- package/static/{chunk-CFLfawWY.js → chunk-DyBGMc4d.js} +1 -1
- package/static/{chunk-B-z83rju.js → chunk-G3ZceoN1.js} +1 -1
- package/static/{chunk-Bh1X0--a.js → chunk-Gu4zXr01.js} +1 -1
- package/static/{chunk-DxnpLefu.js → chunk-IkExJtku.js} +1 -1
- package/static/{chunk-Dgyv2z85.js → chunk-J8FddCDs.js} +1 -1
- package/static/{chunk-DYi0PdOM.js → chunk-M1_E44jC.js} +1 -1
- package/static/{chunk-B0sWk9pb.js → chunk-Mg4TtgDp.js} +1 -1
- package/static/chunk-cyhKpRPg.js +1 -0
- package/static/{chunk-xyBkudn9.js → chunk-fE1xdnsY.js} +1 -1
- package/static/{chunk-BgAz67x8.js → chunk-lIN4-wAm.js} +1 -1
- package/static/{chunk-PhC9V0kv.js → chunk-msOfdoxN.js} +1 -1
- package/static/{chunk-Tk2BGjOK.js → chunk-n2czfdCl.js} +1 -1
- package/static/{chunk-CqXMuRFL.js → chunk-rzRU0blB.js} +1 -1
- package/static/{chunk-745C7DWL.js → chunk-zZBc5XuD.js} +1 -1
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +215 -14
- package/static/sidebar.css +2 -7
- package/static/workspace.js +66 -58
- package/static/chunk-5rwdhcVY.js +0 -1
- package/static/chunk-BFRx3GcN.js +0 -1
- package/static/chunk-BNE4vRAl.js +0 -1
- package/static/chunk-BVqE4wlY.js +0 -1
- package/static/chunk-BYPUwVJd.js +0 -8
- package/static/chunk-BZFc_oT5.js +0 -1
- package/static/chunk-BfpeM2_Y.js +0 -1
- package/static/chunk-Bz6J7zHc.js +0 -1
- package/static/chunk-CyJ4bWgC.js +0 -1
- package/static/chunk-D7FarRGf.js +0 -1
- package/static/chunk-DDhk861e.js +0 -1
- package/static/chunk-DTTB1Rjf.js +0 -1
- package/static/chunk-DVXDUcQZ.js +0 -1
- package/static/chunk-DYSi9-67.js +0 -1
- package/static/chunk-DdmKGZXf.js +0 -1
- package/static/chunk-DpHRgEqx.js +0 -1
- package/static/chunk-DufL1VKq.js +0 -1
- package/static/chunk-SWs44m7G.js +0 -1
- package/static/chunk-qMjAlJP9.js +0 -1
- package/static/chunk-qnagBFlM.js +0 -1
- package/static/chunk-yqeNYXbH.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { basename, delimiter, join, resolve, sep } from "node:path";
|
|
2
|
-
import { execFile, spawn } from "node:child_process";
|
|
1
|
+
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
2
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
3
3
|
import { accessSync, appendFileSync, closeSync, constants, existsSync, linkSync, 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";
|
|
@@ -1797,7 +1797,8 @@ async function git(cwd, args, options = {}) {
|
|
|
1797
1797
|
cwd,
|
|
1798
1798
|
encoding: "utf8",
|
|
1799
1799
|
maxBuffer: MAX_BUFFER$1,
|
|
1800
|
-
...options.timeoutMs ? { timeout: options.timeoutMs } : {}
|
|
1800
|
+
...options.timeoutMs ? { timeout: options.timeoutMs } : {},
|
|
1801
|
+
...options.signal ? { signal: options.signal } : {}
|
|
1801
1802
|
});
|
|
1802
1803
|
return stdout;
|
|
1803
1804
|
} catch (error) {
|
|
@@ -1806,6 +1807,30 @@ async function git(cwd, args, options = {}) {
|
|
|
1806
1807
|
throw new GitError(args, err.stderr ?? String(error));
|
|
1807
1808
|
}
|
|
1808
1809
|
}
|
|
1810
|
+
/**
|
|
1811
|
+
* The same, blocking, for the few local questions asked where nothing can wait: which repository a
|
|
1812
|
+
* path belongs to, asked while naming where its records live. Read-only plumbing only; what it may
|
|
1813
|
+
* run is pinned by `git_sync.test.ts`.
|
|
1814
|
+
*/
|
|
1815
|
+
function gitSync(cwd, args) {
|
|
1816
|
+
try {
|
|
1817
|
+
return execFileSync("git", [
|
|
1818
|
+
"-c",
|
|
1819
|
+
"core.quotePath=false",
|
|
1820
|
+
...args
|
|
1821
|
+
], {
|
|
1822
|
+
cwd,
|
|
1823
|
+
encoding: "utf8",
|
|
1824
|
+
stdio: [
|
|
1825
|
+
"ignore",
|
|
1826
|
+
"pipe",
|
|
1827
|
+
"pipe"
|
|
1828
|
+
]
|
|
1829
|
+
});
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
throw new GitError(args, String(error.stderr ?? error));
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1809
1834
|
/** True when the ref resolves. Used to check a base candidate without treating absence as failure. */
|
|
1810
1835
|
async function refExists(cwd, ref) {
|
|
1811
1836
|
try {
|
|
@@ -2805,19 +2830,16 @@ async function peel(repoPath, named) {
|
|
|
2805
2830
|
async function resolveTarget(input) {
|
|
2806
2831
|
const root = await repositoryRoot(input.repoPath);
|
|
2807
2832
|
if (input.commitRef) return commitTarget(root, input.commitRef);
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
if (!(error instanceof AmbiguousRefError)) throw error;
|
|
2813
|
-
const head = await headToMeasure(root, input);
|
|
2814
|
-
throw head ? await withReviewRanges(error, root, head) : error;
|
|
2815
|
-
}
|
|
2833
|
+
const base = input.base;
|
|
2834
|
+
const head = () => headToMeasure(root, input);
|
|
2835
|
+
if (base) await verifyOrMeasure(root, () => verifyBase(root, base), head);
|
|
2836
|
+
else if (!input.prUrl) await verifyOrMeasure(root, () => verifyDefaultBase(root), head);
|
|
2816
2837
|
const named = input.base ? { base: input.base } : {};
|
|
2817
2838
|
if (input.prUrl) {
|
|
2818
2839
|
if (!input.resolveRequest) throw new TargetError(`${input.prUrl} is a pull request, but nothing here can look one up. Check the request out locally and review the branch instead.`);
|
|
2819
2840
|
const request = await input.resolveRequest(root, input.prUrl);
|
|
2820
2841
|
await ensureCommitPresent(root, request.headSha, request.headRef);
|
|
2842
|
+
if (!input.base && !request.baseRef) await verifyOrMeasure(root, () => verifyDefaultBase(root), async () => request.headSha);
|
|
2821
2843
|
const resolvedBase = input.base ? void 0 : await resolvePullRequestBase(root, request);
|
|
2822
2844
|
return {
|
|
2823
2845
|
originRepoPath: root,
|
|
@@ -2968,6 +2990,21 @@ async function verifyBase(root, base) {
|
|
|
2968
2990
|
* than handed to git as typed, because a name that is itself ambiguous would be settled by git's own
|
|
2969
2991
|
* preference, which is the guess this module refuses to make.
|
|
2970
2992
|
*/
|
|
2993
|
+
/**
|
|
2994
|
+
* Run a base check, and give an ambiguity refusal the ranges each choice would review.
|
|
2995
|
+
*
|
|
2996
|
+
* Measured only for the refusal: which commits each choice would review is the one thing that
|
|
2997
|
+
* turns "name the one you mean" into a decision the reviewer can make.
|
|
2998
|
+
*/
|
|
2999
|
+
async function verifyOrMeasure(root, check, head) {
|
|
3000
|
+
try {
|
|
3001
|
+
await check();
|
|
3002
|
+
} catch (error) {
|
|
3003
|
+
if (!(error instanceof AmbiguousRefError)) throw error;
|
|
3004
|
+
const measured = await head();
|
|
3005
|
+
throw measured ? await withReviewRanges(error, root, measured) : error;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
2971
3008
|
async function headToMeasure(root, input) {
|
|
2972
3009
|
if (input.prUrl) return void 0;
|
|
2973
3010
|
if (!input.branchRef) return "HEAD";
|
|
@@ -6299,7 +6336,7 @@ function sanitisePullRequest(value) {
|
|
|
6299
6336
|
};
|
|
6300
6337
|
const title = typeof value.title === "string" ? value.title.trim() : "";
|
|
6301
6338
|
if (title) pr.title = title.slice(0, 200);
|
|
6302
|
-
if (typeof value.body === "string"
|
|
6339
|
+
if (typeof value.body === "string") pr.body = value.body.trim() ? value.body : "";
|
|
6303
6340
|
const headSha = label(value.headSha);
|
|
6304
6341
|
if (headSha) pr.headSha = headSha;
|
|
6305
6342
|
const headRef = label(value.headRef);
|
|
@@ -7559,6 +7596,15 @@ function stopServer() {
|
|
|
7559
7596
|
detail: `Stopped the workspace server (pid ${record.pid}, port ${record.port}). Open reviews are no longer served; running a review starts it again.`
|
|
7560
7597
|
};
|
|
7561
7598
|
}
|
|
7599
|
+
/**
|
|
7600
|
+
* Remove the record only if it is this process's. A server that is shutting down must not delete
|
|
7601
|
+
* the record of the one that is serving: a replacement stopped during a failed handover is signalled
|
|
7602
|
+
* just after the old server has written its own record back, and an unconditional delete there left
|
|
7603
|
+
* a live server with no record, which the next CLI start then could not find.
|
|
7604
|
+
*/
|
|
7605
|
+
function clearServerRecordOwnedBy(pid) {
|
|
7606
|
+
if (readServerRecord()?.pid === pid) clearServerRecord();
|
|
7607
|
+
}
|
|
7562
7608
|
function clearServerRecord() {
|
|
7563
7609
|
try {
|
|
7564
7610
|
unlinkSync(SERVER_RECORD);
|
|
@@ -7730,7 +7776,11 @@ async function ensureServer() {
|
|
|
7730
7776
|
* the new file at the same path, which is the main reason anybody wants one.
|
|
7731
7777
|
*/
|
|
7732
7778
|
function spawnServer() {
|
|
7733
|
-
|
|
7779
|
+
return spawnDetached([serverEntry()]);
|
|
7780
|
+
}
|
|
7781
|
+
/** How `spawnServer` starts node, apart so a test can start something else exactly the same way. */
|
|
7782
|
+
function spawnDetached(args) {
|
|
7783
|
+
const child = spawn(process.execPath, args, {
|
|
7734
7784
|
detached: true,
|
|
7735
7785
|
stdio: "ignore",
|
|
7736
7786
|
cwd: STORE_ROOT
|
|
@@ -7738,6 +7788,24 @@ function spawnServer() {
|
|
|
7738
7788
|
child.unref();
|
|
7739
7789
|
return child;
|
|
7740
7790
|
}
|
|
7791
|
+
/**
|
|
7792
|
+
* Stop a replacement that never answered, and return only once it has gone.
|
|
7793
|
+
*
|
|
7794
|
+
* Waiting is the point. A replacement that bound but answered too slowly is a server like any other:
|
|
7795
|
+
* on SIGTERM it clears the record if the record is its own. Checking and deleting are two steps, so
|
|
7796
|
+
* a server that wrote its record back while that handler ran could lose it. Writing only after the
|
|
7797
|
+
* child has exited leaves nothing to race. SIGKILL after the grace, so a child that ignores the
|
|
7798
|
+
* signal cannot hold the restart open, or the port, for ever.
|
|
7799
|
+
*/
|
|
7800
|
+
async function stopChild(child, graceMs = 2e3) {
|
|
7801
|
+
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
|
|
7802
|
+
child.ref();
|
|
7803
|
+
const exited = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
7804
|
+
child.kill("SIGTERM");
|
|
7805
|
+
if (await Promise.race([exited.then(() => true), setTimeout$1(graceMs).then(() => false)])) return;
|
|
7806
|
+
child.kill("SIGKILL");
|
|
7807
|
+
await exited;
|
|
7808
|
+
}
|
|
7741
7809
|
/** Whether there is anything to start. The one foreseeable reason a restart cannot be attempted. */
|
|
7742
7810
|
function serverEntryExists() {
|
|
7743
7811
|
return existsSync(serverEntry());
|
|
@@ -8024,6 +8092,65 @@ function explainAgentFailure(agent, detail, env, requestedModel) {
|
|
|
8024
8092
|
const hint = modelFailureHint(agent, detail, requestedModel);
|
|
8025
8093
|
return hint ? `${replaced} ${hint}` : replaced;
|
|
8026
8094
|
}
|
|
8095
|
+
function endingPath(jobId) {
|
|
8096
|
+
return jobPath(jobId).replace(/\.json$/, ".ending");
|
|
8097
|
+
}
|
|
8098
|
+
/**
|
|
8099
|
+
* Publish `wanted` unless an ending is already published, and say which ending stands, or null when
|
|
8100
|
+
* none can be read. Null is never a win for either side.
|
|
8101
|
+
*/
|
|
8102
|
+
function claimEnding(jobId, wanted) {
|
|
8103
|
+
const path = endingPath(jobId);
|
|
8104
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
8105
|
+
const draft = `${path}.${randomUUID()}.draft`;
|
|
8106
|
+
writeFileSync(draft, wanted, { mode: 384 });
|
|
8107
|
+
try {
|
|
8108
|
+
linkSync(draft, path);
|
|
8109
|
+
return wanted;
|
|
8110
|
+
} catch (error) {
|
|
8111
|
+
if (error.code !== "EEXIST") throw error;
|
|
8112
|
+
return readEnding(jobId);
|
|
8113
|
+
} finally {
|
|
8114
|
+
unlinkSync(draft);
|
|
8115
|
+
}
|
|
8116
|
+
}
|
|
8117
|
+
function readEnding(jobId) {
|
|
8118
|
+
try {
|
|
8119
|
+
const text = readFileSync(endingPath(jobId), "utf8");
|
|
8120
|
+
return text === "stop" || text === "commit" ? text : null;
|
|
8121
|
+
} catch {
|
|
8122
|
+
return null;
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
/** The page's half. True when the run will stop; false when it had already committed to saving. */
|
|
8126
|
+
function requestJobStop(jobId) {
|
|
8127
|
+
return claimEnding(jobId, "stop") === "stop";
|
|
8128
|
+
}
|
|
8129
|
+
/** The driver's half, before anything is saved. False means a stop got there first. */
|
|
8130
|
+
function commitJob(jobId) {
|
|
8131
|
+
return claimEnding(jobId, "commit") === "commit";
|
|
8132
|
+
}
|
|
8133
|
+
function jobStopRequested(jobId) {
|
|
8134
|
+
return readEnding(jobId) === "stop";
|
|
8135
|
+
}
|
|
8136
|
+
function clearJobEnding(jobId) {
|
|
8137
|
+
try {
|
|
8138
|
+
unlinkSync(endingPath(jobId));
|
|
8139
|
+
} catch {}
|
|
8140
|
+
}
|
|
8141
|
+
/**
|
|
8142
|
+
* Abort `controller` once a stop is requested, so the agent is killed while it works rather than
|
|
8143
|
+
* only refused at the commit. Returns the way to stop watching.
|
|
8144
|
+
*
|
|
8145
|
+
* Unref'd, so a terminal command is never kept alive by the watch alone once its review is over.
|
|
8146
|
+
*/
|
|
8147
|
+
function watchForStop(jobId, controller, intervalMs = 500) {
|
|
8148
|
+
const timer = setInterval(() => {
|
|
8149
|
+
if (jobStopRequested(jobId)) controller.abort();
|
|
8150
|
+
}, intervalMs);
|
|
8151
|
+
timer.unref();
|
|
8152
|
+
return () => clearInterval(timer);
|
|
8153
|
+
}
|
|
8027
8154
|
//#endregion
|
|
8028
8155
|
//#region ../../packages/review-harness/src/workspace/worktree.ts
|
|
8029
8156
|
/**
|
|
@@ -8113,6 +8240,21 @@ function readMarker(worktreePath) {
|
|
|
8113
8240
|
}
|
|
8114
8241
|
}
|
|
8115
8242
|
/**
|
|
8243
|
+
* Worktrees this process is using with no job record behind them: a mapping run's disposable
|
|
8244
|
+
* checkout. The sweep removes an unclaimed worktree git owns at once, whatever its age, so without
|
|
8245
|
+
* this it took one from under a run that lasted past the next sweep. Held in memory on purpose: a
|
|
8246
|
+
* server that dies releases every hold, and what it left behind is residue again.
|
|
8247
|
+
*/
|
|
8248
|
+
var held = /* @__PURE__ */ new Set();
|
|
8249
|
+
/** Hold `path` until the returned release is called. Take it before the worktree is created. */
|
|
8250
|
+
function holdWorktree(path) {
|
|
8251
|
+
held.add(path);
|
|
8252
|
+
return () => held.delete(path);
|
|
8253
|
+
}
|
|
8254
|
+
function isWorktreeHeld(path) {
|
|
8255
|
+
return held.has(path);
|
|
8256
|
+
}
|
|
8257
|
+
/**
|
|
8116
8258
|
* Delete one managed worktree, or explain why not.
|
|
8117
8259
|
*
|
|
8118
8260
|
* Two paths, and the difference between them is where the authority comes from.
|
|
@@ -9236,51 +9378,87 @@ var REVIEW_ATTENTIONS = [
|
|
|
9236
9378
|
* 17KB of perfectly good JSON to 583 characters of prose in front of it, and there was nothing
|
|
9237
9379
|
* wrong with that answer this function could not have read.
|
|
9238
9380
|
*
|
|
9239
|
-
*
|
|
9240
|
-
*
|
|
9381
|
+
* The answer is the first value `isAnswer` accepts, which each caller says in the terms of its own
|
|
9382
|
+
* schema. When none is accepted the first value that parsed comes back, so the parser above still
|
|
9383
|
+
* reports the wrong shape in its own words. Nothing found at all leaves the reply exactly as it
|
|
9384
|
+
* came, so the parser reports on what it was actually given rather than on an empty string this
|
|
9385
|
+
* function invented.
|
|
9241
9386
|
*/
|
|
9242
|
-
function extractJsonText(raw) {
|
|
9387
|
+
function extractJsonText(raw, isAnswer) {
|
|
9243
9388
|
const text = raw.trim();
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9389
|
+
let first;
|
|
9390
|
+
for (const candidate of jsonCandidates(text)) {
|
|
9391
|
+
first ??= candidate.text;
|
|
9392
|
+
if (isAnswer(candidate.value)) return candidate.text;
|
|
9247
9393
|
}
|
|
9248
|
-
return
|
|
9394
|
+
return first ?? text;
|
|
9249
9395
|
}
|
|
9250
|
-
/**
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9396
|
+
/**
|
|
9397
|
+
* The shape check most callers want: an object carrying at least one of the fields its schema asks
|
|
9398
|
+
* for, holding something a reply could use. Every field these schemas name is text or a list, so
|
|
9399
|
+
* that is what counts: words, or an array. `{"summary": null}` or `{"summary": 42}` is not an answer,
|
|
9400
|
+
* and a complete one later in the reply still wins. Deliberately no deeper than that, because each
|
|
9401
|
+
* parser makes its own decision about a thin answer and reports it in its own words; this only has
|
|
9402
|
+
* to tell an answer from an example quoted before it.
|
|
9403
|
+
*/
|
|
9404
|
+
function objectWithAnyOf(...fields) {
|
|
9405
|
+
return (value) => typeof value === "object" && value !== null && !Array.isArray(value) && fields.some((field) => usable(value[field]));
|
|
9406
|
+
}
|
|
9407
|
+
function usable(value) {
|
|
9408
|
+
return typeof value === "string" && value.trim() !== "" || Array.isArray(value);
|
|
9258
9409
|
}
|
|
9259
9410
|
/**
|
|
9260
|
-
*
|
|
9411
|
+
* Every whole JSON value in this text, in the order an answer is looked for: fenced ones first,
|
|
9412
|
+
* then each slice the scanner reads whole.
|
|
9261
9413
|
*
|
|
9262
9414
|
* Every `{` and `[` is tried in turn, because a brace in a sentence about code is not a rare thing
|
|
9263
9415
|
* to find in a paragraph a model wrote before the answer. Balance alone would accept one, so each
|
|
9264
|
-
* candidate is parsed and
|
|
9265
|
-
*
|
|
9416
|
+
* candidate is parsed and only one that survives is offered. For a reply that is only the object,
|
|
9417
|
+
* that is the first character.
|
|
9418
|
+
*
|
|
9419
|
+
* Parsing is not enough to be the answer, which is why the caller judges each one by its schema.
|
|
9420
|
+
* Prose that quotes `{"example": true}` before the real reply parses perfectly well, and taking it
|
|
9421
|
+
* made a usable answer fail downstream as the wrong shape. A value that was offered is skipped
|
|
9422
|
+
* whole, so an object nested inside a quoted example is not offered in its place.
|
|
9266
9423
|
*
|
|
9267
9424
|
* Strings are tracked because a brace inside one is a character rather than a nesting level, and
|
|
9268
9425
|
* counting it would end the value early or never end it at all. Escapes are tracked because a
|
|
9269
9426
|
* quote inside a string is the same problem one level down.
|
|
9270
9427
|
*/
|
|
9271
|
-
function
|
|
9428
|
+
function* jsonCandidates(text) {
|
|
9429
|
+
for (const match of text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
|
|
9430
|
+
const inner = match[1].trim();
|
|
9431
|
+
if (!inner.startsWith("{") && !inner.startsWith("[")) continue;
|
|
9432
|
+
const value = parsed(inner);
|
|
9433
|
+
if (value.ok) yield {
|
|
9434
|
+
text: inner,
|
|
9435
|
+
value: value.value
|
|
9436
|
+
};
|
|
9437
|
+
}
|
|
9272
9438
|
for (let start = 0; start < text.length; start += 1) {
|
|
9273
9439
|
const opener = text[start];
|
|
9274
9440
|
if (opener !== "{" && opener !== "[") continue;
|
|
9275
9441
|
const end = closingIndex(text, start, opener);
|
|
9276
9442
|
if (end === -1) continue;
|
|
9277
9443
|
const candidate = text.slice(start, end + 1);
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9444
|
+
const value = parsed(candidate);
|
|
9445
|
+
if (!value.ok) continue;
|
|
9446
|
+
yield {
|
|
9447
|
+
text: candidate,
|
|
9448
|
+
value: value.value
|
|
9449
|
+
};
|
|
9450
|
+
start = end;
|
|
9451
|
+
}
|
|
9452
|
+
}
|
|
9453
|
+
function parsed(candidate) {
|
|
9454
|
+
try {
|
|
9455
|
+
return {
|
|
9456
|
+
ok: true,
|
|
9457
|
+
value: JSON.parse(candidate)
|
|
9458
|
+
};
|
|
9459
|
+
} catch {
|
|
9460
|
+
return { ok: false };
|
|
9282
9461
|
}
|
|
9283
|
-
return null;
|
|
9284
9462
|
}
|
|
9285
9463
|
/** Where the value opened at `start` closes, or -1 if it never does. */
|
|
9286
9464
|
function closingIndex(text, start, opener) {
|
|
@@ -10391,6 +10569,8 @@ var MOTIVATION_EVIDENCE_KINDS = [
|
|
|
10391
10569
|
];
|
|
10392
10570
|
/** Ours, not the model's: what an unsupported claim is replaced with when a question remains. */
|
|
10393
10571
|
var UNESTABLISHED_MOTIVATION = "It isn't clear why this change is needed.";
|
|
10572
|
+
/** Ours, not the model's: asked when a failed or unclear motive came with no question of its own. */
|
|
10573
|
+
var FALLBACK_INTENT_QUESTION = "What problem does this change solve, or what goal does it serve?";
|
|
10394
10574
|
/** Stable, so "Mark answered" on it survives a reload like any other question's. */
|
|
10395
10575
|
var INTENT_QUESTION_ID = "intent-motivation";
|
|
10396
10576
|
/**
|
|
@@ -10578,6 +10758,26 @@ function withIntentQuestion(questions, motivation) {
|
|
|
10578
10758
|
category: "intent"
|
|
10579
10759
|
}];
|
|
10580
10760
|
}
|
|
10761
|
+
/**
|
|
10762
|
+
* A finished analysis's motive, carried onto the description an update will show beside it.
|
|
10763
|
+
*
|
|
10764
|
+
* Commits pushed since show what changed, not why, so a motive is carried rather than re-asked.
|
|
10765
|
+
* But a `stated` one is a quote, and it is only true of the description it was found in. When the
|
|
10766
|
+
* author has edited that description since, the quote is looked for again, in the same words and by
|
|
10767
|
+
* the same check as at analysis time; one that is gone is no longer the author's stated reason and
|
|
10768
|
+
* becomes unclear, asking our question. An unchanged description, or a motive that never rested on
|
|
10769
|
+
* it, is carried exactly as it was.
|
|
10770
|
+
*/
|
|
10771
|
+
function carryMotivation(motivation, checkedAgainst, describedNow) {
|
|
10772
|
+
if (motivation?.status !== "stated" || checkedAgainst === describedNow) return motivation;
|
|
10773
|
+
if (isAuthorQuote(motivation.sourceQuote, describedNow)) return motivation;
|
|
10774
|
+
return {
|
|
10775
|
+
contractVersion: 1,
|
|
10776
|
+
status: "unclear",
|
|
10777
|
+
summary: UNESTABLISHED_MOTIVATION,
|
|
10778
|
+
authorQuestion: FALLBACK_INTENT_QUESTION
|
|
10779
|
+
};
|
|
10780
|
+
}
|
|
10581
10781
|
/** The candidate checked and, when it ends unclear, its question filed. */
|
|
10582
10782
|
function applyMotivation(result, candidate, authorDescription) {
|
|
10583
10783
|
const { motivation, correction } = verifyMotivation(candidate, authorDescription);
|
|
@@ -10683,8 +10883,10 @@ function parseReviewResponse(raw, authorDescription) {
|
|
|
10683
10883
|
...applied.correction ? { motivationCorrection: applied.correction } : {}
|
|
10684
10884
|
};
|
|
10685
10885
|
}
|
|
10886
|
+
/** What tells this reply's answer from a JSON example quoted in the prose before it. */
|
|
10887
|
+
var REVIEW_ANSWER = objectWithAnyOf("summary", "recommendation", "issues", "nextSteps");
|
|
10686
10888
|
function parseUnchecked(raw) {
|
|
10687
|
-
const jsonText = extractJsonText(raw);
|
|
10889
|
+
const jsonText = extractJsonText(raw, REVIEW_ANSWER);
|
|
10688
10890
|
let parsed;
|
|
10689
10891
|
try {
|
|
10690
10892
|
parsed = JSON.parse(jsonText);
|
|
@@ -11154,8 +11356,10 @@ var WORTHINESSES = [
|
|
|
11154
11356
|
"consider",
|
|
11155
11357
|
"leave"
|
|
11156
11358
|
];
|
|
11359
|
+
/** What tells this reply's answer from a JSON example quoted in the prose before it. */
|
|
11360
|
+
var KISS_ANSWER = objectWithAnyOf("assessment", "summary", "recommendations", "justifiedComplexity");
|
|
11157
11361
|
function parseKissAnswer(raw) {
|
|
11158
|
-
const jsonText = extractJsonText(raw);
|
|
11362
|
+
const jsonText = extractJsonText(raw, KISS_ANSWER);
|
|
11159
11363
|
let parsed;
|
|
11160
11364
|
try {
|
|
11161
11365
|
parsed = JSON.parse(jsonText);
|
|
@@ -11192,14 +11396,24 @@ function optional(key, value) {
|
|
|
11192
11396
|
* what makes it auditable. A polished summary with no basis a reader can check is a claim rather
|
|
11193
11397
|
* than a recommendation. The trade-offs stay optional: some recommendations genuinely cost nothing. What fails is withheld and counted,
|
|
11194
11398
|
* never padded or truncated, so the page can say that something came back incomplete.
|
|
11399
|
+
*
|
|
11400
|
+
* Counted against what came back, not against what survived the first filter: a recommendation
|
|
11401
|
+
* written as a bare string is still a recommendation the reader is not shown, and counting from the
|
|
11402
|
+
* filtered list made `["Remove the wrapper"]` read as a change with nothing to simplify. A
|
|
11403
|
+
* collection that is there but is not a list is one unreadable answer, counted as one, for the same
|
|
11404
|
+
* reason. Absent is not withheld: an answer with nothing to recommend may leave the field out, and
|
|
11405
|
+
* `null` is JSON's way of saying the same, so it is read as absent rather than as unreadable.
|
|
11195
11406
|
*/
|
|
11196
11407
|
function normalizeRecommendations(value) {
|
|
11197
|
-
if (
|
|
11408
|
+
if (value === void 0 || value === null) return {
|
|
11198
11409
|
kept: [],
|
|
11199
11410
|
withheld: 0
|
|
11200
11411
|
};
|
|
11201
|
-
|
|
11202
|
-
|
|
11412
|
+
if (!Array.isArray(value)) return {
|
|
11413
|
+
kept: [],
|
|
11414
|
+
withheld: 1
|
|
11415
|
+
};
|
|
11416
|
+
const kept = value.filter((item) => typeof item === "object" && item !== null).map((item, index) => {
|
|
11203
11417
|
const worthiness = WORTHINESSES.includes(item.commentWorthiness) ? item.commentWorthiness : void 0;
|
|
11204
11418
|
const gains = asStringArray(item.gains).map((gain) => gain.trim()).filter((gain) => gain !== "");
|
|
11205
11419
|
return {
|
|
@@ -11221,7 +11435,7 @@ function normalizeRecommendations(value) {
|
|
|
11221
11435
|
}).filter((rec) => rec.title !== "" && rec.gist !== "" && rec.simplerDirection !== "" && rec.evidence !== "" && rec.whyNotJustified !== "" && rec.locations.length > 0);
|
|
11222
11436
|
return {
|
|
11223
11437
|
kept,
|
|
11224
|
-
withheld:
|
|
11438
|
+
withheld: value.length - kept.length
|
|
11225
11439
|
};
|
|
11226
11440
|
}
|
|
11227
11441
|
/**
|
|
@@ -11664,6 +11878,7 @@ var OPERATION_NAMES = {
|
|
|
11664
11878
|
function eventName(notification) {
|
|
11665
11879
|
if (notification.kind === "pr-updated") return notification.source === "branch" ? "Branch updated" : "PR updated";
|
|
11666
11880
|
if (notification.kind === "pr-state") return notification.state === "merged" ? "PR merged" : "PR closed";
|
|
11881
|
+
if (notification.kind === "architecture") return notification.outcome === "ready" ? "Map ready" : "Mapping failed";
|
|
11667
11882
|
return `${OPERATION_NAMES[notification.operation]} ${notification.outcome}`;
|
|
11668
11883
|
}
|
|
11669
11884
|
/**
|
|
@@ -11684,7 +11899,7 @@ function headline(notification) {
|
|
|
11684
11899
|
* so adding the next kind is a compiler error here instead of a silently untinted toast there.
|
|
11685
11900
|
*/
|
|
11686
11901
|
function toneOf(notification) {
|
|
11687
|
-
return notification.kind === "analysis" ? notification.outcome : "updated";
|
|
11902
|
+
return notification.kind === "analysis" || notification.kind === "architecture" ? notification.outcome : "updated";
|
|
11688
11903
|
}
|
|
11689
11904
|
var OPERATIONS = [
|
|
11690
11905
|
"review",
|
|
@@ -11738,6 +11953,15 @@ function createPrState(input, now = Date.now()) {
|
|
|
11738
11953
|
createdAt: now
|
|
11739
11954
|
});
|
|
11740
11955
|
}
|
|
11956
|
+
/** Record that mapping a repository or a part ended. Same `wx` contract: the id names the run. */
|
|
11957
|
+
function createArchitectureNotification(input, now = Date.now()) {
|
|
11958
|
+
return create({
|
|
11959
|
+
version: 1,
|
|
11960
|
+
kind: "architecture",
|
|
11961
|
+
...input,
|
|
11962
|
+
createdAt: now
|
|
11963
|
+
});
|
|
11964
|
+
}
|
|
11741
11965
|
function create(record) {
|
|
11742
11966
|
if (!ID.test(record.id)) return null;
|
|
11743
11967
|
try {
|
|
@@ -11802,8 +12026,9 @@ function unreadCount() {
|
|
|
11802
12026
|
/**
|
|
11803
12027
|
* Mark these read, and say how many changed.
|
|
11804
12028
|
*
|
|
11805
|
-
*
|
|
11806
|
-
*
|
|
12029
|
+
* Read means the reviewer has been where it points, not that they have dealt with what it said: by
|
|
12030
|
+
* opening it, by one of the two mark buttons, or by opening its review any other way (see
|
|
12031
|
+
* `markReadForReview`). Only `readAt` changes; the record and its history stay as they were.
|
|
11807
12032
|
*/
|
|
11808
12033
|
function markRead(ids, now = Date.now()) {
|
|
11809
12034
|
let changed = 0;
|
|
@@ -11820,6 +12045,16 @@ function markRead(ids, now = Date.now()) {
|
|
|
11820
12045
|
}
|
|
11821
12046
|
return changed;
|
|
11822
12047
|
}
|
|
12048
|
+
/**
|
|
12049
|
+
* Every unread notification about this review, marked read because the review has been opened.
|
|
12050
|
+
*
|
|
12051
|
+
* An unread notification is a pointer back to a review, and once the reviewer is looking at that
|
|
12052
|
+
* review the pointer has done its job; left unread it is badge noise. Idempotent, so two tabs opening
|
|
12053
|
+
* the same review end with it read and nothing else, and it creates nothing.
|
|
12054
|
+
*/
|
|
12055
|
+
function markReadForReview(reviewId, now = Date.now()) {
|
|
12056
|
+
return markRead(listNotifications().filter((notification) => notification.reviewId === reviewId && notification.readAt === void 0).map((notification) => notification.id), now);
|
|
12057
|
+
}
|
|
11823
12058
|
function markAllRead(now = Date.now()) {
|
|
11824
12059
|
return markRead(listNotifications().filter((notification) => notification.readAt === void 0).map((notification) => notification.id), now);
|
|
11825
12060
|
}
|
|
@@ -11865,6 +12100,7 @@ function isNotification(value) {
|
|
|
11865
12100
|
if (record.kind === "analysis") return OPERATIONS.includes(record.operation) && OUTCOMES.includes(record.outcome);
|
|
11866
12101
|
if (record.kind === "pr-updated") return typeof record.headSha === "string" && typeof record.detail === "string" && (record.source === void 0 || record.source === "branch");
|
|
11867
12102
|
if (record.kind === "pr-state") return (record.state === "merged" || record.state === "closed") && typeof record.headSha === "string";
|
|
12103
|
+
if (record.kind === "architecture") return OUTCOMES.includes(record.outcome) && (record.part === void 0 || record.part === true);
|
|
11868
12104
|
return false;
|
|
11869
12105
|
}
|
|
11870
12106
|
//#endregion
|
|
@@ -11881,8 +12117,7 @@ function isNotification(value) {
|
|
|
11881
12117
|
*
|
|
11882
12118
|
* The record is the event. The Notifications page and the workspace's toasts both read it. Any
|
|
11883
12119
|
* delivery that has to be pushed rather than read belongs here, after `createNotification`, and only
|
|
11884
|
-
* for a record this call created, which carries the once-only guarantee over to it for free.
|
|
11885
|
-
* delivery is written that way and parked outside the product: see `desktop_preference.ts`.
|
|
12120
|
+
* for a record this call created, which carries the once-only guarantee over to it for free.
|
|
11886
12121
|
*
|
|
11887
12122
|
* Never throws. This runs on the success path of a review that has already been saved, and the
|
|
11888
12123
|
* accounting must not break the thing it accounts for.
|
|
@@ -12311,9 +12546,17 @@ async function runJob(workspaceId, runners = {}, reviewType = "standard", option
|
|
|
12311
12546
|
};
|
|
12312
12547
|
});
|
|
12313
12548
|
if (isBusy(started)) throw busyReason(workspaceId, reviewType);
|
|
12549
|
+
clearJobEnding(started.jobId);
|
|
12550
|
+
const controller = new AbortController();
|
|
12551
|
+
const outer = options.signal;
|
|
12552
|
+
if (outer?.aborted) controller.abort();
|
|
12553
|
+
outer?.addEventListener("abort", () => controller.abort(), { once: true });
|
|
12554
|
+
const unwatch = watchForStop(started.jobId, controller);
|
|
12314
12555
|
try {
|
|
12315
|
-
return await drive(started.jobId, workspaceId, runners, started.claim.claim.holder,
|
|
12556
|
+
return await drive(started.jobId, workspaceId, runners, started.claim.claim.holder, controller.signal);
|
|
12316
12557
|
} finally {
|
|
12558
|
+
unwatch();
|
|
12559
|
+
clearJobEnding(started.jobId);
|
|
12317
12560
|
started.claim.release();
|
|
12318
12561
|
}
|
|
12319
12562
|
}
|
|
@@ -12475,6 +12718,8 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
12475
12718
|
const payload = await runAnalysisFor(job, workspaceId, runners, signal, (model) => {
|
|
12476
12719
|
observedModel = model;
|
|
12477
12720
|
});
|
|
12721
|
+
signal?.throwIfAborted();
|
|
12722
|
+
if (!commitJob(job.id)) throw new AgentCancelledError("Cancelled from the workspace page.");
|
|
12478
12723
|
if (job.reviewType === "kiss") await applyKiss(job, workspaceId, payload, observedModel);
|
|
12479
12724
|
else await applyGuide(job, workspaceId, payload, observedModel);
|
|
12480
12725
|
job = update(job, "done");
|
|
@@ -12769,6 +13014,126 @@ async function runProvenanceOf(job, observedModel) {
|
|
|
12769
13014
|
};
|
|
12770
13015
|
}
|
|
12771
13016
|
//#endregion
|
|
13017
|
+
//#region ../../packages/review-harness/src/workspace/assistant_runs.ts
|
|
13018
|
+
var RUN_KINDS = ["ask", "diagram"];
|
|
13019
|
+
var runs = /* @__PURE__ */ new Map();
|
|
13020
|
+
function keyOf(workspaceId, kind) {
|
|
13021
|
+
return `${workspaceId}:${kind}`;
|
|
13022
|
+
}
|
|
13023
|
+
/**
|
|
13024
|
+
* Start a run, or learn that one is already going.
|
|
13025
|
+
*
|
|
13026
|
+
* One call rather than a check followed by a start, so there is no gap between them for a second
|
|
13027
|
+
* request to fall through. The existing run comes back rather than a bare refusal because the
|
|
13028
|
+
* caller's next move is to show it, not to report an error.
|
|
13029
|
+
*
|
|
13030
|
+
* A stopped run gives up its place at once, though its handler may still be unwinding. Stop is
|
|
13031
|
+
* over for the reviewer when it answers, and the page reopens Ask then. A stopped run can keep
|
|
13032
|
+
* nothing, and `endRun` leaves a newer run alone, so the two cannot collide. An answered run still
|
|
13033
|
+
* holds its place until it is saved, or the next question would be asked without it.
|
|
13034
|
+
*/
|
|
13035
|
+
function beginRun(workspaceId, kind, detail, now = Date.now()) {
|
|
13036
|
+
const key = keyOf(workspaceId, kind);
|
|
13037
|
+
const existing = runs.get(key);
|
|
13038
|
+
if (existing && existing.state !== "stopped") return { alreadyRunning: existing };
|
|
13039
|
+
const controller = new AbortController();
|
|
13040
|
+
const run = {
|
|
13041
|
+
workspaceId,
|
|
13042
|
+
kind,
|
|
13043
|
+
question: detail.question ?? null,
|
|
13044
|
+
sectionId: detail.sectionId,
|
|
13045
|
+
...detail.drawing ? { drawing: detail.drawing } : {},
|
|
13046
|
+
startedAt: now,
|
|
13047
|
+
signal: controller.signal,
|
|
13048
|
+
state: "running",
|
|
13049
|
+
controller
|
|
13050
|
+
};
|
|
13051
|
+
runs.set(key, run);
|
|
13052
|
+
return { run };
|
|
13053
|
+
}
|
|
13054
|
+
/** Null for a stopped run as well: it is over for the page, whatever its handler is still doing. */
|
|
13055
|
+
function reportRun(workspaceId, kind) {
|
|
13056
|
+
const run = runs.get(keyOf(workspaceId, kind));
|
|
13057
|
+
if (!run || run.state === "stopped") return null;
|
|
13058
|
+
return {
|
|
13059
|
+
question: run.question,
|
|
13060
|
+
sectionId: run.sectionId,
|
|
13061
|
+
...run.drawing ? { drawing: run.drawing } : {},
|
|
13062
|
+
startedAt: run.startedAt
|
|
13063
|
+
};
|
|
13064
|
+
}
|
|
13065
|
+
/**
|
|
13066
|
+
* The reviewer pressed Stop.
|
|
13067
|
+
*
|
|
13068
|
+
* Idempotent: a second press, a press after the answer landed, and a press with nothing running are
|
|
13069
|
+
* all harmless and all answer false. True only when this call is the one that stopped the run.
|
|
13070
|
+
*/
|
|
13071
|
+
function stopRun(workspaceId, kind) {
|
|
13072
|
+
const run = runs.get(keyOf(workspaceId, kind));
|
|
13073
|
+
if (!run || run.state !== "running") return false;
|
|
13074
|
+
run.state = "stopped";
|
|
13075
|
+
run.controller.abort();
|
|
13076
|
+
return true;
|
|
13077
|
+
}
|
|
13078
|
+
/**
|
|
13079
|
+
* The agent answered. Call this in the same continuation that received the answer, before anything
|
|
13080
|
+
* else is awaited, so a Stop handled in between cannot slip ahead of an answer that had already won.
|
|
13081
|
+
*
|
|
13082
|
+
* False when Stop got there first, and then the answer must not be kept however complete it is.
|
|
13083
|
+
*/
|
|
13084
|
+
function completeRun(run) {
|
|
13085
|
+
const held = run;
|
|
13086
|
+
if (held.state !== "running") return false;
|
|
13087
|
+
held.state = "completed";
|
|
13088
|
+
return true;
|
|
13089
|
+
}
|
|
13090
|
+
/**
|
|
13091
|
+
* The run is over, whichever way. Removes this run and only this one: by the time a slow handler
|
|
13092
|
+
* gets here a newer run of the same kind may have begun, and it is not this handler's to end.
|
|
13093
|
+
*/
|
|
13094
|
+
function endRun(run) {
|
|
13095
|
+
const key = keyOf(run.workspaceId, run.kind);
|
|
13096
|
+
if (runs.get(key) === run) runs.delete(key);
|
|
13097
|
+
}
|
|
13098
|
+
/**
|
|
13099
|
+
* Whether any question or drawing is still going here, or has answered and is being saved.
|
|
13100
|
+
*
|
|
13101
|
+
* A stopped run does not count: it can keep nothing, so a restart while its handler unwinds loses
|
|
13102
|
+
* nothing either. `server_work` asks this, because a restart ends the agent child with the process.
|
|
13103
|
+
*/
|
|
13104
|
+
function holdsLiveRun() {
|
|
13105
|
+
for (const run of runs.values()) if (run.state !== "stopped") return true;
|
|
13106
|
+
return false;
|
|
13107
|
+
}
|
|
13108
|
+
//#endregion
|
|
13109
|
+
//#region ../../packages/review-harness/src/workspace/server_work.ts
|
|
13110
|
+
/**
|
|
13111
|
+
* Whether this server process is driving any review work of its own.
|
|
13112
|
+
*
|
|
13113
|
+
* A restart ends this process, and everything a page asks an agent for runs *inside* it: a review,
|
|
13114
|
+
* an update, a KISS review, a map, a Buddy AI answer or drawing. A restart during any of them loses
|
|
13115
|
+
* that work, so each is counted here: the background ones through `track`, and the assistant's runs
|
|
13116
|
+
* through the registry that already holds each one until its answer is saved. A review driven from a terminal has its own
|
|
13117
|
+
* process and is untouched, which is exactly why this is a count kept here rather than a look at
|
|
13118
|
+
* the jobs on disk: `/revision`'s `live` is machine-wide, and refusing a restart because somebody
|
|
13119
|
+
* else's terminal is busy would be the control lying about what it costs.
|
|
13120
|
+
*
|
|
13121
|
+
* In memory on purpose. The question is "is this process busy", and a process cannot outlive its
|
|
13122
|
+
* own answer, so there is nothing to keep in a file and nothing that can go stale. A restart or a
|
|
13123
|
+
* crash resets it to the truth.
|
|
13124
|
+
*/
|
|
13125
|
+
var driving = 0;
|
|
13126
|
+
/** Count one piece of work for as long as it runs, however it ends. */
|
|
13127
|
+
function track(work) {
|
|
13128
|
+
driving += 1;
|
|
13129
|
+
return work.finally(() => {
|
|
13130
|
+
driving -= 1;
|
|
13131
|
+
});
|
|
13132
|
+
}
|
|
13133
|
+
function isDriving() {
|
|
13134
|
+
return driving > 0 || holdsLiveRun();
|
|
13135
|
+
}
|
|
13136
|
+
//#endregion
|
|
12772
13137
|
//#region ../../packages/review-harness/src/workspace/kiss_run.ts
|
|
12773
13138
|
/**
|
|
12774
13139
|
* Starting a KISS run on a workspace that already exists.
|
|
@@ -12801,6 +13166,8 @@ async function recordKissRun(input) {
|
|
|
12801
13166
|
const sha = reviewedCommit(workspace);
|
|
12802
13167
|
if (!sha) throw new Error("This review does not record which commit it describes, so KISS cannot run on it.");
|
|
12803
13168
|
const agent = input.agentId ? agentById(input.agentId) : workspace.jobId ? agentById(agentIdOf(workspace.session)) : defaultAgent();
|
|
13169
|
+
const sameAgent = !workspace.jobId || agent.id === agentIdOf(workspace.session);
|
|
13170
|
+
const requestedModel = input.requestedModel ?? (sameAgent ? resolveModel({ review: workspace }) : null);
|
|
12804
13171
|
const started = await withReviewLock(workspace.id, () => {
|
|
12805
13172
|
const target = {
|
|
12806
13173
|
originRepoPath: workspace.repoPath,
|
|
@@ -12815,7 +13182,7 @@ async function recordKissRun(input) {
|
|
|
12815
13182
|
purpose: "review",
|
|
12816
13183
|
reviewType: "kiss",
|
|
12817
13184
|
audience: input.audience,
|
|
12818
|
-
...
|
|
13185
|
+
...requestedModel ? { requestedModel } : {}
|
|
12819
13186
|
});
|
|
12820
13187
|
saveJob(job);
|
|
12821
13188
|
const run = startKissRun(workspace, {
|
|
@@ -12824,7 +13191,7 @@ async function recordKissRun(input) {
|
|
|
12824
13191
|
reviewedSha: sha,
|
|
12825
13192
|
promptVersion: "3",
|
|
12826
13193
|
agentId: agent.id,
|
|
12827
|
-
...
|
|
13194
|
+
...requestedModel ? { requestedModel } : {}
|
|
12828
13195
|
});
|
|
12829
13196
|
mutateWorkspace(workspace.id, (current) => {
|
|
12830
13197
|
current.kissRuns = [...current.kissRuns ?? [], run];
|
|
@@ -12841,9 +13208,13 @@ async function recordKissRun(input) {
|
|
|
12841
13208
|
* The lock is released before the analysis begins, exactly as `reanalyseReview` does, so a deletion
|
|
12842
13209
|
* during the twenty minutes that follow is refused by the live job claim rather than made to wait
|
|
12843
13210
|
* for it.
|
|
13211
|
+
*
|
|
13212
|
+
* Counted by `track` from the first await, so a restart is refused for the whole of the run rather
|
|
13213
|
+
* than only once a caller remembered to count it. Only the server calls this; the CLI records the
|
|
13214
|
+
* run and drives it in its own process.
|
|
12844
13215
|
*/
|
|
12845
|
-
|
|
12846
|
-
return
|
|
13216
|
+
function startKissJob(input, runners = {}) {
|
|
13217
|
+
return track(recordKissRun(input).then((job) => runJob(job.workspaceId, runners, "kiss")));
|
|
12847
13218
|
}
|
|
12848
13219
|
//#endregion
|
|
12849
13220
|
//#region ../../packages/review-harness/src/workspace/agent_env.ts
|
|
@@ -13062,10 +13433,12 @@ var QUESTION_STATUSES = [
|
|
|
13062
13433
|
"still-open",
|
|
13063
13434
|
"affected"
|
|
13064
13435
|
];
|
|
13436
|
+
/** What tells this reply's answer from a JSON example quoted in the prose before it. */
|
|
13437
|
+
var RECONCILIATION_ANSWER = objectWithAnyOf("findings", "newIssues", "newSections", "questions", "summary", "recommendation", "nextSteps");
|
|
13065
13438
|
function parseReconciliationResult(raw, priorCount, options = {}) {
|
|
13066
13439
|
let parsed;
|
|
13067
13440
|
try {
|
|
13068
|
-
parsed = JSON.parse(extractJsonText(raw));
|
|
13441
|
+
parsed = JSON.parse(extractJsonText(raw, RECONCILIATION_ANSWER));
|
|
13069
13442
|
} catch {
|
|
13070
13443
|
return null;
|
|
13071
13444
|
}
|
|
@@ -13584,7 +13957,7 @@ function movable(fetched) {
|
|
|
13584
13957
|
};
|
|
13585
13958
|
copy("headSha");
|
|
13586
13959
|
copy("title");
|
|
13587
|
-
|
|
13960
|
+
if (typeof fetched.body === "string") updates.body = fetched.body;
|
|
13588
13961
|
copy("filesUrl");
|
|
13589
13962
|
copy("fileUrlTemplate");
|
|
13590
13963
|
copy("author");
|
|
@@ -13700,6 +14073,34 @@ var RefreshUnavailableError = class extends Error {};
|
|
|
13700
14073
|
var FETCH_TIMEOUT_MS = 6e4;
|
|
13701
14074
|
/** Said wherever an update is asked of a commit review. One sentence, so both surfaces agree. */
|
|
13702
14075
|
var COMMIT_REVIEW_CANNOT_UPDATE = "This review is of one commit, and a commit cannot move, so there is nothing to bring it up to date with. To analyse the same commit again, reanalyse it instead.";
|
|
14076
|
+
/** What the notice says after a cancel, and what the terminal would print if it could cancel. */
|
|
14077
|
+
var UPDATE_CANCELLED = "The review is exactly as it was before the update began.";
|
|
14078
|
+
/**
|
|
14079
|
+
* The updates this process is driving, by job id, each with the means to end it.
|
|
14080
|
+
*
|
|
14081
|
+
* In memory, because only the process running the agent can stop it. An update started in a
|
|
14082
|
+
* terminal is in that process's map, not the server's, and Ctrl-C is how it ends; the page offers
|
|
14083
|
+
* Cancel only for an update this server holds (`cancellableHere`). An entry leaves the map the
|
|
14084
|
+
* moment the update commits to writing, so a cancel either stops it before anything is written or
|
|
14085
|
+
* finds nothing to stop. It can never land halfway through the write.
|
|
14086
|
+
*/
|
|
14087
|
+
var cancellers = /* @__PURE__ */ new Map();
|
|
14088
|
+
/** Whether this process can cancel the update with this job id. */
|
|
14089
|
+
function cancellableHere(jobId) {
|
|
14090
|
+
return cancellers.has(jobId);
|
|
14091
|
+
}
|
|
14092
|
+
/**
|
|
14093
|
+
* Stop the update with this job id, if this process is still able to.
|
|
14094
|
+
*
|
|
14095
|
+
* Named by job rather than by review, so a press aimed at one update cannot end the next one.
|
|
14096
|
+
* False when it has finished, has begun writing, or runs somewhere else.
|
|
14097
|
+
*/
|
|
14098
|
+
function cancelUpdate(jobId) {
|
|
14099
|
+
const controller = cancellers.get(jobId);
|
|
14100
|
+
if (!controller || controller.signal.aborted) return false;
|
|
14101
|
+
controller.abort();
|
|
14102
|
+
return true;
|
|
14103
|
+
}
|
|
13703
14104
|
/**
|
|
13704
14105
|
* Computes and mutates the workspace in memory; does not persist.
|
|
13705
14106
|
*
|
|
@@ -13717,30 +14118,55 @@ async function updateReview(workspace, runners = {}) {
|
|
|
13717
14118
|
} } : {}
|
|
13718
14119
|
});
|
|
13719
14120
|
runners.onStarted?.(held.job.id);
|
|
14121
|
+
const controller = new AbortController();
|
|
14122
|
+
cancellers.set(held.job.id, controller);
|
|
14123
|
+
const { signal } = controller;
|
|
13720
14124
|
const current = () => loadJob(held.job.id) ?? held.job;
|
|
13721
14125
|
const step = (phase) => saveJob(advance(current(), phase));
|
|
13722
14126
|
try {
|
|
13723
14127
|
const fetchStartedAt = Date.now();
|
|
13724
|
-
const outcome = await bringForward(workspace, runners, reviewedSha, held, step);
|
|
14128
|
+
const outcome = await bringForward(workspace, runners, reviewedSha, held, step, signal);
|
|
14129
|
+
if (signal.aborted) return cancelled(held, reviewedSha);
|
|
13725
14130
|
if (outcome.ok && !outcome.fetchFailed && outcome.toSha) workspace.headSeenAt = fetchStartedAt;
|
|
13726
14131
|
const ended = outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
|
|
13727
14132
|
saveJob(ended);
|
|
13728
14133
|
announceJob(ended);
|
|
13729
14134
|
return outcome;
|
|
13730
14135
|
} catch (error) {
|
|
14136
|
+
if (signal.aborted) return cancelled(held, reviewedSha);
|
|
13731
14137
|
const failed = fail(current(), error instanceof Error ? error.message : String(error));
|
|
13732
14138
|
saveJob(failed);
|
|
13733
14139
|
announceJob(failed);
|
|
13734
14140
|
throw error;
|
|
13735
14141
|
} finally {
|
|
14142
|
+
cancellers.delete(held.job.id);
|
|
13736
14143
|
held.claim.release();
|
|
13737
14144
|
}
|
|
13738
14145
|
}
|
|
13739
|
-
|
|
13740
|
-
|
|
14146
|
+
/**
|
|
14147
|
+
* The reviewer cancelled. Recorded as a stop, not a failure, so nothing announces it or offers a
|
|
14148
|
+
* retry, and answered as an outcome the page reports on the review it left untouched.
|
|
14149
|
+
*/
|
|
14150
|
+
function cancelled(held, reviewedSha) {
|
|
14151
|
+
recordStop(loadJob(held.job.id) ?? held.job, held.claim.claim.holder);
|
|
14152
|
+
return {
|
|
14153
|
+
at: Date.now(),
|
|
14154
|
+
ok: false,
|
|
14155
|
+
message: UPDATE_CANCELLED,
|
|
14156
|
+
stop: "cancelled",
|
|
14157
|
+
fetchFailed: false,
|
|
14158
|
+
moved: false,
|
|
14159
|
+
fromSha: reviewedSha,
|
|
14160
|
+
toSha: reviewedSha
|
|
14161
|
+
};
|
|
14162
|
+
}
|
|
14163
|
+
async function bringForward(workspace, runners, reviewedSha, held, step, signal) {
|
|
14164
|
+
const remote = await remoteHead(workspace, signal);
|
|
14165
|
+
signal.throwIfAborted();
|
|
13741
14166
|
if (!remote.sha) return refuse(reviewedSha, remote.fetchFailed, noRemoteHeadMessage(workspace));
|
|
13742
14167
|
if (workspace.prContext) step("conversation");
|
|
13743
14168
|
const conversationOk = await reReadConversation(workspace, runners.runRefresh);
|
|
14169
|
+
signal.throwIfAborted();
|
|
13744
14170
|
if (reviewedSha && !await commitPresent(workspace.repoPath, reviewedSha)) return settled(reviewedSha, remote.sha, remote.fetchFailed, conversationOk, "baseline-gone", BASELINE_GONE_MESSAGE);
|
|
13745
14171
|
if (!reviewedSha || reviewedSha === remote.sha) return settled(reviewedSha, remote.sha, remote.fetchFailed, conversationOk, "already-current", alreadyCurrentMessage(conversationOk));
|
|
13746
14172
|
step("context");
|
|
@@ -13751,9 +14177,10 @@ async function bringForward(workspace, runners, reviewedSha, held, step) {
|
|
|
13751
14177
|
if (error instanceof ReviewBeingDeletedError) throw error;
|
|
13752
14178
|
return settled(reviewedSha, remote.sha, remote.fetchFailed, conversationOk, "analysis-failed", "A checkout of the new commits could not be prepared, so nothing was reassessed.");
|
|
13753
14179
|
}
|
|
13754
|
-
|
|
14180
|
+
signal.throwIfAborted();
|
|
14181
|
+
return reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, held.job.id, signal);
|
|
13755
14182
|
}
|
|
13756
|
-
async function reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, jobId) {
|
|
14183
|
+
async function reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, jobId, signal) {
|
|
13757
14184
|
const newHead = remote.sha;
|
|
13758
14185
|
let delta;
|
|
13759
14186
|
try {
|
|
@@ -13784,24 +14211,28 @@ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk
|
|
|
13784
14211
|
let raw;
|
|
13785
14212
|
try {
|
|
13786
14213
|
const prompt = buildUpdatePrompt(workspace, delta, reviewedSha, newHead, worktreePath);
|
|
13787
|
-
raw = runners.run ? await runners.run(prompt, worktreePath, agentEnvOf(workspace), onProgress, onObservedModel) : await runUpdate(workspace.id, prompt, worktreePath, workspace.repoPath, agentEnvOf(workspace), onProgress, requestedModel, onObservedModel);
|
|
14214
|
+
raw = runners.run ? await runners.run(prompt, worktreePath, agentEnvOf(workspace), onProgress, onObservedModel, signal) : await runUpdate(workspace.id, prompt, worktreePath, workspace.repoPath, agentEnvOf(workspace), onProgress, requestedModel, onObservedModel, signal);
|
|
13788
14215
|
} catch (error) {
|
|
13789
14216
|
return settled(reviewedSha, newHead, remote.fetchFailed, conversationOk, "analysis-failed", explainUpdateFailure(error, agentEnvOf(workspace), requestedModel));
|
|
13790
14217
|
}
|
|
13791
14218
|
const parsed = parseReconciliationResult(raw, previous.issues.length, { priorQuestionCount: previous.questions.length });
|
|
13792
14219
|
if (!parsed) return settled(reviewedSha, newHead, remote.fetchFailed, conversationOk, "analysis-failed", "The reassessment came back in a form this could not read, so nothing was changed.");
|
|
13793
14220
|
const issues = [...reconcileIssues(previous.issues, parsed.findings), ...markNewIssues(parsed.newIssues)];
|
|
14221
|
+
const motivation = carryMotivation(previous.motivation, workspace.session.metadata?.authorDescription, workspace.prContext?.pullRequest.body);
|
|
14222
|
+
const questions = reconcileQuestions(previous.questions, parsed.questions);
|
|
13794
14223
|
const result = {
|
|
13795
14224
|
schemaVersion: 1,
|
|
13796
14225
|
summary: parsed.summary || previous.summary,
|
|
13797
|
-
...
|
|
14226
|
+
...motivation ? { motivation } : {},
|
|
13798
14227
|
overallRisk: parsed.overallRisk ?? previous.overallRisk,
|
|
13799
14228
|
recommendation: parsed.recommendation || previous.recommendation,
|
|
13800
14229
|
nextSteps: parsed.nextSteps.length ? parsed.nextSteps : previous.nextSteps,
|
|
13801
14230
|
sections: composeSections(previous.sections, parsed.newSections),
|
|
13802
14231
|
issues,
|
|
13803
|
-
questions:
|
|
14232
|
+
questions: motivation === previous.motivation ? questions : withIntentQuestion(questions, motivation)
|
|
13804
14233
|
};
|
|
14234
|
+
signal.throwIfAborted();
|
|
14235
|
+
cancellers.delete(jobId);
|
|
13805
14236
|
await applyUpdate(workspace, result, previous, newHead, worktreePath, {
|
|
13806
14237
|
...requestedModel ? { requestedModel } : {},
|
|
13807
14238
|
...observedModel ? { observedModel } : {},
|
|
@@ -13835,8 +14266,8 @@ var BASELINE_GONE_MESSAGE = "The commit this review was made from is no longer i
|
|
|
13835
14266
|
* is allowed to reach the network and that panel is not, so it looks at a newer copy of the same
|
|
13836
14267
|
* refs.
|
|
13837
14268
|
*/
|
|
13838
|
-
async function remoteHead(workspace) {
|
|
13839
|
-
const fetchFailed = !await fetchOrigin(workspace.repoPath);
|
|
14269
|
+
async function remoteHead(workspace, signal) {
|
|
14270
|
+
const fetchFailed = !await fetchOrigin(workspace.repoPath, signal);
|
|
13840
14271
|
return {
|
|
13841
14272
|
sha: (await followedBranch(workspace))?.sha ?? null,
|
|
13842
14273
|
fetchFailed
|
|
@@ -13853,9 +14284,12 @@ function noRemoteHeadMessage(workspace) {
|
|
|
13853
14284
|
* back to origin. Naming one ourselves would mean parsing a remote out of an upstream ref, which
|
|
13854
14285
|
* is guesswork on any remote whose name contains a slash.
|
|
13855
14286
|
*/
|
|
13856
|
-
async function fetchOrigin(repoPath) {
|
|
14287
|
+
async function fetchOrigin(repoPath, signal) {
|
|
13857
14288
|
try {
|
|
13858
|
-
await git(repoPath, ["fetch"], {
|
|
14289
|
+
await git(repoPath, ["fetch"], {
|
|
14290
|
+
timeoutMs: FETCH_TIMEOUT_MS,
|
|
14291
|
+
signal
|
|
14292
|
+
});
|
|
13859
14293
|
return true;
|
|
13860
14294
|
} catch {
|
|
13861
14295
|
return false;
|
|
@@ -14148,7 +14582,7 @@ ${buildReconciliationInstructions({
|
|
|
14148
14582
|
* that captured the daemon's environment instead was reconciling one review's findings with
|
|
14149
14583
|
* whichever agent and account the workspace server happened to be started under.
|
|
14150
14584
|
*/
|
|
14151
|
-
async function runUpdate(workspaceId, prompt, cwd, originRepoPath, env, onProgress, requestedModel, onObservedModel) {
|
|
14585
|
+
async function runUpdate(workspaceId, prompt, cwd, originRepoPath, env, onProgress, requestedModel, onObservedModel, signal) {
|
|
14152
14586
|
try {
|
|
14153
14587
|
return await withUsageRecorded(workspaceId, "update", env, (onUsage) => agentFor(env).run({
|
|
14154
14588
|
capability: "read-code",
|
|
@@ -14160,7 +14594,8 @@ async function runUpdate(workspaceId, prompt, cwd, originRepoPath, env, onProgre
|
|
|
14160
14594
|
timeoutMs: UPDATE_TIMEOUT_MS,
|
|
14161
14595
|
onProgress,
|
|
14162
14596
|
onUsage,
|
|
14163
|
-
...onObservedModel ? { onObservedModel } : {}
|
|
14597
|
+
...onObservedModel ? { onObservedModel } : {},
|
|
14598
|
+
...signal ? { signal } : {}
|
|
14164
14599
|
}), void 0, requestedModel);
|
|
14165
14600
|
} catch (error) {
|
|
14166
14601
|
if (error instanceof AgentUnavailableError) throw new RefreshUnavailableError(`${error.message} It is what this review was made with, so this review cannot be brought up to date.`);
|
|
@@ -14353,4 +14788,4 @@ function lineageIds(id) {
|
|
|
14353
14788
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
14354
14789
|
}
|
|
14355
14790
|
//#endregion
|
|
14356
|
-
export {
|
|
14791
|
+
export { unreadCount as $, queueRemove as $n, remoteHost as $t, reanalyseReview as A, matchingWorkspaceIds as An, AgentCancelledError as Ar, clearServerRecordOwnedBy as At, createArchitectureNotification as B, workspaceRevision as Bn, BUILD_VERSION as Bt, track as C, indexReviewGroups as Cn, CONFIG_PATH as Cr, relativeTime as Ct, endRun as D, lineageKeyFor as Dn, agentById as Dr, settleQueue as Dt, completeRun as E, latestKissRun as En, DEFAULT_AGENT_ID as Er, queueForReview as Et, recordTelemetryUploadConsent as F, recentWorkspaces as Fn, spawnServer as Ft, listNotifications as G, lifecycleAction as Gn, MIN_WORKSPACE_PORT as Gt, createPrUpdate as H, LIFECYCLES as Hn, feedbackUrl as Ht, telemetryUploadConsent as I, reviewedCommit as In, stopChild as It, markReadForReview as J, offersLifecycle as Jn, statedWorkspacePort as Jt, markAllRead as K, lifecycleChip as Kn, parseWorkspacePort as Kt, announceJob as L, reviewedRepositories as Ln, stopServer as Lt, startJob as M, positionInLineage as Mn, readServerRecord as Mt, readEvents as N, presentedKissRunOf as Nn, reviewsUrl as Nt, reportRun as O, lineagePosition as On, agentFor as Or, readIndexToken as Ot, record as P, previousKissResult as Pn, serverEntryExists as Pt, toneOf as Q, queueAddMany as Qn, forgeFor as Qt, branchOf as R, summarise as Rn, workspaceUrl as Rt, isDriving as S, indexKeptIds as Sn, writeAgentPreference as Sr, modelHelpLines as St, beginRun as T, kissDecisionsOf as Tn, AGENT_IDS as Tr, compactQueue as Tt, eventName as U, activityLabel as Un, DEFAULT_WORKSPACE_PORT as Ut, createPrState as V, workspaceStamps as Vn, PACKAGE_NAME as Vt, headline as W, isLifecycle as Wn, MAX_WORKSPACE_PORT as Wt, readNotification as X, sectionOf as Xn, claimRequestUrl as Xt, pruneNotifications as Y, parseLifecycle as Yn, writePortPreference as Yt, removeNotification as Z, queueAdd as Zn, forgeResolver as Zt, liveUpdateFor as _, driverGone as _n, gitSync as _r, holdWorktree as _t, wasBlocked as a, allJobs as an, issuesOutstanding as ar, GITHUB_MARKDOWN as at, recordKissRun as b, followedRefName as bn, clearAgentPreference as br, removeWorktree as bt, RefreshUnavailableError as c, isTerminal as cn, processDiscussion as cr, CODE_COPY_CLASS as ct, mergeUpdateInto as d, PHASES as dn, WITHHELD_REASON as dr, asString as dt, fillFileUrlTemplate as en, queueStamp as er, withUsageRecorded as et, updateReview as f, isLiveJob as fn, kissOutcome as fr, extractJsonText as ft, UpdateAlreadyRunningError as g, clearClaim as gn, git as gr, createWorktree as gt, askCheckout as h, purposeOf as hn, isCommitReview as hr, MANAGED_ROOT as ht, liveJobsFor as i, reviewedShaOf as in, isQuestionOutstanding as ir, hasAuthorContent as it, runJob as j, mutateWorkspace as jn, AgentUnavailableError as jr, ensureServer as jt, stopRun as k, loadWorkspace as kn, detectAgents as kr, bootstrapUrl as kt, cancelUpdate as l, loadJob as ln, recommendationsOf as lr, escapeHtml as lt, isUnchanged as m, progressSteps as mn, displayRef as mr, ReviewBeingDeletedError as mt, deleteReview as n, checkReview as nn, doneVerb as nr, UNESTABLISHED_MOTIVATION as nt, discardJob as o, endedAt as on, questionsOutstanding as or, renderMarkdown as ot, refreshPrContext as p, phasesFor as pn, resolveTarget as pr, objectWithAnyOf as pt, markRead as q, lifecyclePill as qn, resolveWorkspacePort as qt, lineageIds as r, followedBranch as rn, isIssueOutstanding as rr, authorPreview as rt, FETCH_TIMEOUT_MS as s, fail as sn, reviewerDispositions as sr, CODE_BLOCK_CLASS as st, deleteLineage as t, checkCodeFreshness as tn, readQueue as tr, EXPLAIN_SIMPLY_PROMPT as tt, cancellableHere as u, saveJob as un, describeAuthorship as ur, renderInlineText as ut, runningJobs as v, isClaimed as vn, resolveAgent as vr, isWorktreeHeld as vt, RUN_KINDS as w, kissDecisionRefusal as wn, STORE_ROOT as wr, activateQueued as wt, startKissJob as x, groupByLineage as xn, readAgentPreference as xr, requestJobStop as xt, agentEnvOf as y, decideKissRecommendation as yn, resolveModel as yr, readMarker as yt, subjectOf as z, touchWorkspace as zn, writeServerRecord as zt };
|