prreviewbuddy 0.56.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 +104 -0
- package/dist/{delete_review-C5bz0Pts.js → delete_review-D59T6NBn.js} +523 -71
- package/dist/main.js +1 -1
- package/dist/server.js +2632 -253
- 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);
|
|
@@ -6471,8 +6508,17 @@ function authStatusArgs(host) {
|
|
|
6471
6508
|
*
|
|
6472
6509
|
* `host` defaults for the one caller that has no repository in hand yet. Both callers that are
|
|
6473
6510
|
* about to make a real request pass the host they are about to make it against.
|
|
6511
|
+
*
|
|
6512
|
+
* **A directory that is not there is not a missing `gh`, and is thrown rather than answered.** Node
|
|
6513
|
+
* reports a spawn into a missing `cwd` as `ENOENT`, exactly as it reports a missing executable, so
|
|
6514
|
+
* without this check a review whose checkout had been deleted read as "`gh` is not installed". The
|
|
6515
|
+
* watcher takes that verdict as true of every request reached the same way, and one stale checkout
|
|
6516
|
+
* then silenced every other check for the rest of the pass. Neither verdict this function returns is
|
|
6517
|
+
* about one directory, so it says neither: the caller gets a failure that belongs to the one
|
|
6518
|
+
* review whose checkout it is.
|
|
6474
6519
|
*/
|
|
6475
6520
|
async function ghAvailable(cwd, env, host = "github.com") {
|
|
6521
|
+
if (!existsSync(cwd)) throw new GhError(`The checkout at ${cwd} no longer exists.`);
|
|
6476
6522
|
try {
|
|
6477
6523
|
await run(["--version"], cwd, env);
|
|
6478
6524
|
} catch {
|
|
@@ -6947,7 +6993,15 @@ function readHeadSha(raw) {
|
|
|
6947
6993
|
};
|
|
6948
6994
|
}
|
|
6949
6995
|
async function headOf(repo, number, env, cwd) {
|
|
6950
|
-
|
|
6996
|
+
let availability;
|
|
6997
|
+
try {
|
|
6998
|
+
availability = await ghAvailable(cwd, env, repo.host);
|
|
6999
|
+
} catch (error) {
|
|
7000
|
+
return {
|
|
7001
|
+
kind: "failed",
|
|
7002
|
+
detail: error.message
|
|
7003
|
+
};
|
|
7004
|
+
}
|
|
6951
7005
|
if (!availability.ok) return {
|
|
6952
7006
|
kind: availability.kind,
|
|
6953
7007
|
cli: availability.cli
|
|
@@ -7542,6 +7596,15 @@ function stopServer() {
|
|
|
7542
7596
|
detail: `Stopped the workspace server (pid ${record.pid}, port ${record.port}). Open reviews are no longer served; running a review starts it again.`
|
|
7543
7597
|
};
|
|
7544
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
|
+
}
|
|
7545
7608
|
function clearServerRecord() {
|
|
7546
7609
|
try {
|
|
7547
7610
|
unlinkSync(SERVER_RECORD);
|
|
@@ -7713,7 +7776,11 @@ async function ensureServer() {
|
|
|
7713
7776
|
* the new file at the same path, which is the main reason anybody wants one.
|
|
7714
7777
|
*/
|
|
7715
7778
|
function spawnServer() {
|
|
7716
|
-
|
|
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, {
|
|
7717
7784
|
detached: true,
|
|
7718
7785
|
stdio: "ignore",
|
|
7719
7786
|
cwd: STORE_ROOT
|
|
@@ -7721,6 +7788,24 @@ function spawnServer() {
|
|
|
7721
7788
|
child.unref();
|
|
7722
7789
|
return child;
|
|
7723
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
|
+
}
|
|
7724
7809
|
/** Whether there is anything to start. The one foreseeable reason a restart cannot be attempted. */
|
|
7725
7810
|
function serverEntryExists() {
|
|
7726
7811
|
return existsSync(serverEntry());
|
|
@@ -8007,6 +8092,65 @@ function explainAgentFailure(agent, detail, env, requestedModel) {
|
|
|
8007
8092
|
const hint = modelFailureHint(agent, detail, requestedModel);
|
|
8008
8093
|
return hint ? `${replaced} ${hint}` : replaced;
|
|
8009
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
|
+
}
|
|
8010
8154
|
//#endregion
|
|
8011
8155
|
//#region ../../packages/review-harness/src/workspace/worktree.ts
|
|
8012
8156
|
/**
|
|
@@ -8096,6 +8240,21 @@ function readMarker(worktreePath) {
|
|
|
8096
8240
|
}
|
|
8097
8241
|
}
|
|
8098
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
|
+
/**
|
|
8099
8258
|
* Delete one managed worktree, or explain why not.
|
|
8100
8259
|
*
|
|
8101
8260
|
* Two paths, and the difference between them is where the authority comes from.
|
|
@@ -9219,51 +9378,87 @@ var REVIEW_ATTENTIONS = [
|
|
|
9219
9378
|
* 17KB of perfectly good JSON to 583 characters of prose in front of it, and there was nothing
|
|
9220
9379
|
* wrong with that answer this function could not have read.
|
|
9221
9380
|
*
|
|
9222
|
-
*
|
|
9223
|
-
*
|
|
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.
|
|
9224
9386
|
*/
|
|
9225
|
-
function extractJsonText(raw) {
|
|
9387
|
+
function extractJsonText(raw, isAnswer) {
|
|
9226
9388
|
const text = raw.trim();
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9389
|
+
let first;
|
|
9390
|
+
for (const candidate of jsonCandidates(text)) {
|
|
9391
|
+
first ??= candidate.text;
|
|
9392
|
+
if (isAnswer(candidate.value)) return candidate.text;
|
|
9230
9393
|
}
|
|
9231
|
-
return
|
|
9394
|
+
return first ?? text;
|
|
9232
9395
|
}
|
|
9233
|
-
/**
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
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);
|
|
9241
9409
|
}
|
|
9242
9410
|
/**
|
|
9243
|
-
*
|
|
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.
|
|
9244
9413
|
*
|
|
9245
9414
|
* Every `{` and `[` is tried in turn, because a brace in a sentence about code is not a rare thing
|
|
9246
9415
|
* to find in a paragraph a model wrote before the answer. Balance alone would accept one, so each
|
|
9247
|
-
* candidate is parsed and
|
|
9248
|
-
*
|
|
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.
|
|
9249
9423
|
*
|
|
9250
9424
|
* Strings are tracked because a brace inside one is a character rather than a nesting level, and
|
|
9251
9425
|
* counting it would end the value early or never end it at all. Escapes are tracked because a
|
|
9252
9426
|
* quote inside a string is the same problem one level down.
|
|
9253
9427
|
*/
|
|
9254
|
-
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
|
+
}
|
|
9255
9438
|
for (let start = 0; start < text.length; start += 1) {
|
|
9256
9439
|
const opener = text[start];
|
|
9257
9440
|
if (opener !== "{" && opener !== "[") continue;
|
|
9258
9441
|
const end = closingIndex(text, start, opener);
|
|
9259
9442
|
if (end === -1) continue;
|
|
9260
9443
|
const candidate = text.slice(start, end + 1);
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
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 };
|
|
9265
9461
|
}
|
|
9266
|
-
return null;
|
|
9267
9462
|
}
|
|
9268
9463
|
/** Where the value opened at `start` closes, or -1 if it never does. */
|
|
9269
9464
|
function closingIndex(text, start, opener) {
|
|
@@ -10374,6 +10569,8 @@ var MOTIVATION_EVIDENCE_KINDS = [
|
|
|
10374
10569
|
];
|
|
10375
10570
|
/** Ours, not the model's: what an unsupported claim is replaced with when a question remains. */
|
|
10376
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?";
|
|
10377
10574
|
/** Stable, so "Mark answered" on it survives a reload like any other question's. */
|
|
10378
10575
|
var INTENT_QUESTION_ID = "intent-motivation";
|
|
10379
10576
|
/**
|
|
@@ -10561,6 +10758,26 @@ function withIntentQuestion(questions, motivation) {
|
|
|
10561
10758
|
category: "intent"
|
|
10562
10759
|
}];
|
|
10563
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
|
+
}
|
|
10564
10781
|
/** The candidate checked and, when it ends unclear, its question filed. */
|
|
10565
10782
|
function applyMotivation(result, candidate, authorDescription) {
|
|
10566
10783
|
const { motivation, correction } = verifyMotivation(candidate, authorDescription);
|
|
@@ -10666,8 +10883,10 @@ function parseReviewResponse(raw, authorDescription) {
|
|
|
10666
10883
|
...applied.correction ? { motivationCorrection: applied.correction } : {}
|
|
10667
10884
|
};
|
|
10668
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");
|
|
10669
10888
|
function parseUnchecked(raw) {
|
|
10670
|
-
const jsonText = extractJsonText(raw);
|
|
10889
|
+
const jsonText = extractJsonText(raw, REVIEW_ANSWER);
|
|
10671
10890
|
let parsed;
|
|
10672
10891
|
try {
|
|
10673
10892
|
parsed = JSON.parse(jsonText);
|
|
@@ -11137,8 +11356,10 @@ var WORTHINESSES = [
|
|
|
11137
11356
|
"consider",
|
|
11138
11357
|
"leave"
|
|
11139
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");
|
|
11140
11361
|
function parseKissAnswer(raw) {
|
|
11141
|
-
const jsonText = extractJsonText(raw);
|
|
11362
|
+
const jsonText = extractJsonText(raw, KISS_ANSWER);
|
|
11142
11363
|
let parsed;
|
|
11143
11364
|
try {
|
|
11144
11365
|
parsed = JSON.parse(jsonText);
|
|
@@ -11175,14 +11396,24 @@ function optional(key, value) {
|
|
|
11175
11396
|
* what makes it auditable. A polished summary with no basis a reader can check is a claim rather
|
|
11176
11397
|
* than a recommendation. The trade-offs stay optional: some recommendations genuinely cost nothing. What fails is withheld and counted,
|
|
11177
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.
|
|
11178
11406
|
*/
|
|
11179
11407
|
function normalizeRecommendations(value) {
|
|
11180
|
-
if (
|
|
11408
|
+
if (value === void 0 || value === null) return {
|
|
11181
11409
|
kept: [],
|
|
11182
11410
|
withheld: 0
|
|
11183
11411
|
};
|
|
11184
|
-
|
|
11185
|
-
|
|
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) => {
|
|
11186
11417
|
const worthiness = WORTHINESSES.includes(item.commentWorthiness) ? item.commentWorthiness : void 0;
|
|
11187
11418
|
const gains = asStringArray(item.gains).map((gain) => gain.trim()).filter((gain) => gain !== "");
|
|
11188
11419
|
return {
|
|
@@ -11204,7 +11435,7 @@ function normalizeRecommendations(value) {
|
|
|
11204
11435
|
}).filter((rec) => rec.title !== "" && rec.gist !== "" && rec.simplerDirection !== "" && rec.evidence !== "" && rec.whyNotJustified !== "" && rec.locations.length > 0);
|
|
11205
11436
|
return {
|
|
11206
11437
|
kept,
|
|
11207
|
-
withheld:
|
|
11438
|
+
withheld: value.length - kept.length
|
|
11208
11439
|
};
|
|
11209
11440
|
}
|
|
11210
11441
|
/**
|
|
@@ -11647,6 +11878,7 @@ var OPERATION_NAMES = {
|
|
|
11647
11878
|
function eventName(notification) {
|
|
11648
11879
|
if (notification.kind === "pr-updated") return notification.source === "branch" ? "Branch updated" : "PR updated";
|
|
11649
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";
|
|
11650
11882
|
return `${OPERATION_NAMES[notification.operation]} ${notification.outcome}`;
|
|
11651
11883
|
}
|
|
11652
11884
|
/**
|
|
@@ -11667,7 +11899,7 @@ function headline(notification) {
|
|
|
11667
11899
|
* so adding the next kind is a compiler error here instead of a silently untinted toast there.
|
|
11668
11900
|
*/
|
|
11669
11901
|
function toneOf(notification) {
|
|
11670
|
-
return notification.kind === "analysis" ? notification.outcome : "updated";
|
|
11902
|
+
return notification.kind === "analysis" || notification.kind === "architecture" ? notification.outcome : "updated";
|
|
11671
11903
|
}
|
|
11672
11904
|
var OPERATIONS = [
|
|
11673
11905
|
"review",
|
|
@@ -11721,6 +11953,15 @@ function createPrState(input, now = Date.now()) {
|
|
|
11721
11953
|
createdAt: now
|
|
11722
11954
|
});
|
|
11723
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
|
+
}
|
|
11724
11965
|
function create(record) {
|
|
11725
11966
|
if (!ID.test(record.id)) return null;
|
|
11726
11967
|
try {
|
|
@@ -11785,8 +12026,9 @@ function unreadCount() {
|
|
|
11785
12026
|
/**
|
|
11786
12027
|
* Mark these read, and say how many changed.
|
|
11787
12028
|
*
|
|
11788
|
-
*
|
|
11789
|
-
*
|
|
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.
|
|
11790
12032
|
*/
|
|
11791
12033
|
function markRead(ids, now = Date.now()) {
|
|
11792
12034
|
let changed = 0;
|
|
@@ -11803,6 +12045,16 @@ function markRead(ids, now = Date.now()) {
|
|
|
11803
12045
|
}
|
|
11804
12046
|
return changed;
|
|
11805
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
|
+
}
|
|
11806
12058
|
function markAllRead(now = Date.now()) {
|
|
11807
12059
|
return markRead(listNotifications().filter((notification) => notification.readAt === void 0).map((notification) => notification.id), now);
|
|
11808
12060
|
}
|
|
@@ -11848,6 +12100,7 @@ function isNotification(value) {
|
|
|
11848
12100
|
if (record.kind === "analysis") return OPERATIONS.includes(record.operation) && OUTCOMES.includes(record.outcome);
|
|
11849
12101
|
if (record.kind === "pr-updated") return typeof record.headSha === "string" && typeof record.detail === "string" && (record.source === void 0 || record.source === "branch");
|
|
11850
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);
|
|
11851
12104
|
return false;
|
|
11852
12105
|
}
|
|
11853
12106
|
//#endregion
|
|
@@ -11864,8 +12117,7 @@ function isNotification(value) {
|
|
|
11864
12117
|
*
|
|
11865
12118
|
* The record is the event. The Notifications page and the workspace's toasts both read it. Any
|
|
11866
12119
|
* delivery that has to be pushed rather than read belongs here, after `createNotification`, and only
|
|
11867
|
-
* for a record this call created, which carries the once-only guarantee over to it for free.
|
|
11868
|
-
* 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.
|
|
11869
12121
|
*
|
|
11870
12122
|
* Never throws. This runs on the success path of a review that has already been saved, and the
|
|
11871
12123
|
* accounting must not break the thing it accounts for.
|
|
@@ -12294,9 +12546,17 @@ async function runJob(workspaceId, runners = {}, reviewType = "standard", option
|
|
|
12294
12546
|
};
|
|
12295
12547
|
});
|
|
12296
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);
|
|
12297
12555
|
try {
|
|
12298
|
-
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);
|
|
12299
12557
|
} finally {
|
|
12558
|
+
unwatch();
|
|
12559
|
+
clearJobEnding(started.jobId);
|
|
12300
12560
|
started.claim.release();
|
|
12301
12561
|
}
|
|
12302
12562
|
}
|
|
@@ -12458,6 +12718,8 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
12458
12718
|
const payload = await runAnalysisFor(job, workspaceId, runners, signal, (model) => {
|
|
12459
12719
|
observedModel = model;
|
|
12460
12720
|
});
|
|
12721
|
+
signal?.throwIfAborted();
|
|
12722
|
+
if (!commitJob(job.id)) throw new AgentCancelledError("Cancelled from the workspace page.");
|
|
12461
12723
|
if (job.reviewType === "kiss") await applyKiss(job, workspaceId, payload, observedModel);
|
|
12462
12724
|
else await applyGuide(job, workspaceId, payload, observedModel);
|
|
12463
12725
|
job = update(job, "done");
|
|
@@ -12752,6 +13014,126 @@ async function runProvenanceOf(job, observedModel) {
|
|
|
12752
13014
|
};
|
|
12753
13015
|
}
|
|
12754
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
|
|
12755
13137
|
//#region ../../packages/review-harness/src/workspace/kiss_run.ts
|
|
12756
13138
|
/**
|
|
12757
13139
|
* Starting a KISS run on a workspace that already exists.
|
|
@@ -12784,6 +13166,8 @@ async function recordKissRun(input) {
|
|
|
12784
13166
|
const sha = reviewedCommit(workspace);
|
|
12785
13167
|
if (!sha) throw new Error("This review does not record which commit it describes, so KISS cannot run on it.");
|
|
12786
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);
|
|
12787
13171
|
const started = await withReviewLock(workspace.id, () => {
|
|
12788
13172
|
const target = {
|
|
12789
13173
|
originRepoPath: workspace.repoPath,
|
|
@@ -12798,7 +13182,7 @@ async function recordKissRun(input) {
|
|
|
12798
13182
|
purpose: "review",
|
|
12799
13183
|
reviewType: "kiss",
|
|
12800
13184
|
audience: input.audience,
|
|
12801
|
-
...
|
|
13185
|
+
...requestedModel ? { requestedModel } : {}
|
|
12802
13186
|
});
|
|
12803
13187
|
saveJob(job);
|
|
12804
13188
|
const run = startKissRun(workspace, {
|
|
@@ -12807,7 +13191,7 @@ async function recordKissRun(input) {
|
|
|
12807
13191
|
reviewedSha: sha,
|
|
12808
13192
|
promptVersion: "3",
|
|
12809
13193
|
agentId: agent.id,
|
|
12810
|
-
...
|
|
13194
|
+
...requestedModel ? { requestedModel } : {}
|
|
12811
13195
|
});
|
|
12812
13196
|
mutateWorkspace(workspace.id, (current) => {
|
|
12813
13197
|
current.kissRuns = [...current.kissRuns ?? [], run];
|
|
@@ -12824,9 +13208,13 @@ async function recordKissRun(input) {
|
|
|
12824
13208
|
* The lock is released before the analysis begins, exactly as `reanalyseReview` does, so a deletion
|
|
12825
13209
|
* during the twenty minutes that follow is refused by the live job claim rather than made to wait
|
|
12826
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.
|
|
12827
13215
|
*/
|
|
12828
|
-
|
|
12829
|
-
return
|
|
13216
|
+
function startKissJob(input, runners = {}) {
|
|
13217
|
+
return track(recordKissRun(input).then((job) => runJob(job.workspaceId, runners, "kiss")));
|
|
12830
13218
|
}
|
|
12831
13219
|
//#endregion
|
|
12832
13220
|
//#region ../../packages/review-harness/src/workspace/agent_env.ts
|
|
@@ -13045,10 +13433,12 @@ var QUESTION_STATUSES = [
|
|
|
13045
13433
|
"still-open",
|
|
13046
13434
|
"affected"
|
|
13047
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");
|
|
13048
13438
|
function parseReconciliationResult(raw, priorCount, options = {}) {
|
|
13049
13439
|
let parsed;
|
|
13050
13440
|
try {
|
|
13051
|
-
parsed = JSON.parse(extractJsonText(raw));
|
|
13441
|
+
parsed = JSON.parse(extractJsonText(raw, RECONCILIATION_ANSWER));
|
|
13052
13442
|
} catch {
|
|
13053
13443
|
return null;
|
|
13054
13444
|
}
|
|
@@ -13567,7 +13957,7 @@ function movable(fetched) {
|
|
|
13567
13957
|
};
|
|
13568
13958
|
copy("headSha");
|
|
13569
13959
|
copy("title");
|
|
13570
|
-
|
|
13960
|
+
if (typeof fetched.body === "string") updates.body = fetched.body;
|
|
13571
13961
|
copy("filesUrl");
|
|
13572
13962
|
copy("fileUrlTemplate");
|
|
13573
13963
|
copy("author");
|
|
@@ -13683,6 +14073,34 @@ var RefreshUnavailableError = class extends Error {};
|
|
|
13683
14073
|
var FETCH_TIMEOUT_MS = 6e4;
|
|
13684
14074
|
/** Said wherever an update is asked of a commit review. One sentence, so both surfaces agree. */
|
|
13685
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
|
+
}
|
|
13686
14104
|
/**
|
|
13687
14105
|
* Computes and mutates the workspace in memory; does not persist.
|
|
13688
14106
|
*
|
|
@@ -13700,30 +14118,55 @@ async function updateReview(workspace, runners = {}) {
|
|
|
13700
14118
|
} } : {}
|
|
13701
14119
|
});
|
|
13702
14120
|
runners.onStarted?.(held.job.id);
|
|
14121
|
+
const controller = new AbortController();
|
|
14122
|
+
cancellers.set(held.job.id, controller);
|
|
14123
|
+
const { signal } = controller;
|
|
13703
14124
|
const current = () => loadJob(held.job.id) ?? held.job;
|
|
13704
14125
|
const step = (phase) => saveJob(advance(current(), phase));
|
|
13705
14126
|
try {
|
|
13706
14127
|
const fetchStartedAt = Date.now();
|
|
13707
|
-
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);
|
|
13708
14130
|
if (outcome.ok && !outcome.fetchFailed && outcome.toSha) workspace.headSeenAt = fetchStartedAt;
|
|
13709
14131
|
const ended = outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
|
|
13710
14132
|
saveJob(ended);
|
|
13711
14133
|
announceJob(ended);
|
|
13712
14134
|
return outcome;
|
|
13713
14135
|
} catch (error) {
|
|
14136
|
+
if (signal.aborted) return cancelled(held, reviewedSha);
|
|
13714
14137
|
const failed = fail(current(), error instanceof Error ? error.message : String(error));
|
|
13715
14138
|
saveJob(failed);
|
|
13716
14139
|
announceJob(failed);
|
|
13717
14140
|
throw error;
|
|
13718
14141
|
} finally {
|
|
14142
|
+
cancellers.delete(held.job.id);
|
|
13719
14143
|
held.claim.release();
|
|
13720
14144
|
}
|
|
13721
14145
|
}
|
|
13722
|
-
|
|
13723
|
-
|
|
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();
|
|
13724
14166
|
if (!remote.sha) return refuse(reviewedSha, remote.fetchFailed, noRemoteHeadMessage(workspace));
|
|
13725
14167
|
if (workspace.prContext) step("conversation");
|
|
13726
14168
|
const conversationOk = await reReadConversation(workspace, runners.runRefresh);
|
|
14169
|
+
signal.throwIfAborted();
|
|
13727
14170
|
if (reviewedSha && !await commitPresent(workspace.repoPath, reviewedSha)) return settled(reviewedSha, remote.sha, remote.fetchFailed, conversationOk, "baseline-gone", BASELINE_GONE_MESSAGE);
|
|
13728
14171
|
if (!reviewedSha || reviewedSha === remote.sha) return settled(reviewedSha, remote.sha, remote.fetchFailed, conversationOk, "already-current", alreadyCurrentMessage(conversationOk));
|
|
13729
14172
|
step("context");
|
|
@@ -13734,9 +14177,10 @@ async function bringForward(workspace, runners, reviewedSha, held, step) {
|
|
|
13734
14177
|
if (error instanceof ReviewBeingDeletedError) throw error;
|
|
13735
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.");
|
|
13736
14179
|
}
|
|
13737
|
-
|
|
14180
|
+
signal.throwIfAborted();
|
|
14181
|
+
return reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, held.job.id, signal);
|
|
13738
14182
|
}
|
|
13739
|
-
async function reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, jobId) {
|
|
14183
|
+
async function reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, jobId, signal) {
|
|
13740
14184
|
const newHead = remote.sha;
|
|
13741
14185
|
let delta;
|
|
13742
14186
|
try {
|
|
@@ -13767,24 +14211,28 @@ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk
|
|
|
13767
14211
|
let raw;
|
|
13768
14212
|
try {
|
|
13769
14213
|
const prompt = buildUpdatePrompt(workspace, delta, reviewedSha, newHead, worktreePath);
|
|
13770
|
-
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);
|
|
13771
14215
|
} catch (error) {
|
|
13772
14216
|
return settled(reviewedSha, newHead, remote.fetchFailed, conversationOk, "analysis-failed", explainUpdateFailure(error, agentEnvOf(workspace), requestedModel));
|
|
13773
14217
|
}
|
|
13774
14218
|
const parsed = parseReconciliationResult(raw, previous.issues.length, { priorQuestionCount: previous.questions.length });
|
|
13775
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.");
|
|
13776
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);
|
|
13777
14223
|
const result = {
|
|
13778
14224
|
schemaVersion: 1,
|
|
13779
14225
|
summary: parsed.summary || previous.summary,
|
|
13780
|
-
...
|
|
14226
|
+
...motivation ? { motivation } : {},
|
|
13781
14227
|
overallRisk: parsed.overallRisk ?? previous.overallRisk,
|
|
13782
14228
|
recommendation: parsed.recommendation || previous.recommendation,
|
|
13783
14229
|
nextSteps: parsed.nextSteps.length ? parsed.nextSteps : previous.nextSteps,
|
|
13784
14230
|
sections: composeSections(previous.sections, parsed.newSections),
|
|
13785
14231
|
issues,
|
|
13786
|
-
questions:
|
|
14232
|
+
questions: motivation === previous.motivation ? questions : withIntentQuestion(questions, motivation)
|
|
13787
14233
|
};
|
|
14234
|
+
signal.throwIfAborted();
|
|
14235
|
+
cancellers.delete(jobId);
|
|
13788
14236
|
await applyUpdate(workspace, result, previous, newHead, worktreePath, {
|
|
13789
14237
|
...requestedModel ? { requestedModel } : {},
|
|
13790
14238
|
...observedModel ? { observedModel } : {},
|
|
@@ -13818,8 +14266,8 @@ var BASELINE_GONE_MESSAGE = "The commit this review was made from is no longer i
|
|
|
13818
14266
|
* is allowed to reach the network and that panel is not, so it looks at a newer copy of the same
|
|
13819
14267
|
* refs.
|
|
13820
14268
|
*/
|
|
13821
|
-
async function remoteHead(workspace) {
|
|
13822
|
-
const fetchFailed = !await fetchOrigin(workspace.repoPath);
|
|
14269
|
+
async function remoteHead(workspace, signal) {
|
|
14270
|
+
const fetchFailed = !await fetchOrigin(workspace.repoPath, signal);
|
|
13823
14271
|
return {
|
|
13824
14272
|
sha: (await followedBranch(workspace))?.sha ?? null,
|
|
13825
14273
|
fetchFailed
|
|
@@ -13836,9 +14284,12 @@ function noRemoteHeadMessage(workspace) {
|
|
|
13836
14284
|
* back to origin. Naming one ourselves would mean parsing a remote out of an upstream ref, which
|
|
13837
14285
|
* is guesswork on any remote whose name contains a slash.
|
|
13838
14286
|
*/
|
|
13839
|
-
async function fetchOrigin(repoPath) {
|
|
14287
|
+
async function fetchOrigin(repoPath, signal) {
|
|
13840
14288
|
try {
|
|
13841
|
-
await git(repoPath, ["fetch"], {
|
|
14289
|
+
await git(repoPath, ["fetch"], {
|
|
14290
|
+
timeoutMs: FETCH_TIMEOUT_MS,
|
|
14291
|
+
signal
|
|
14292
|
+
});
|
|
13842
14293
|
return true;
|
|
13843
14294
|
} catch {
|
|
13844
14295
|
return false;
|
|
@@ -14131,7 +14582,7 @@ ${buildReconciliationInstructions({
|
|
|
14131
14582
|
* that captured the daemon's environment instead was reconciling one review's findings with
|
|
14132
14583
|
* whichever agent and account the workspace server happened to be started under.
|
|
14133
14584
|
*/
|
|
14134
|
-
async function runUpdate(workspaceId, prompt, cwd, originRepoPath, env, onProgress, requestedModel, onObservedModel) {
|
|
14585
|
+
async function runUpdate(workspaceId, prompt, cwd, originRepoPath, env, onProgress, requestedModel, onObservedModel, signal) {
|
|
14135
14586
|
try {
|
|
14136
14587
|
return await withUsageRecorded(workspaceId, "update", env, (onUsage) => agentFor(env).run({
|
|
14137
14588
|
capability: "read-code",
|
|
@@ -14143,7 +14594,8 @@ async function runUpdate(workspaceId, prompt, cwd, originRepoPath, env, onProgre
|
|
|
14143
14594
|
timeoutMs: UPDATE_TIMEOUT_MS,
|
|
14144
14595
|
onProgress,
|
|
14145
14596
|
onUsage,
|
|
14146
|
-
...onObservedModel ? { onObservedModel } : {}
|
|
14597
|
+
...onObservedModel ? { onObservedModel } : {},
|
|
14598
|
+
...signal ? { signal } : {}
|
|
14147
14599
|
}), void 0, requestedModel);
|
|
14148
14600
|
} catch (error) {
|
|
14149
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.`);
|
|
@@ -14336,4 +14788,4 @@ function lineageIds(id) {
|
|
|
14336
14788
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
14337
14789
|
}
|
|
14338
14790
|
//#endregion
|
|
14339
|
-
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 };
|