prreviewbuddy 0.55.1 → 0.55.6
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 +74 -0
- package/dist/{delete_review-B2bqhgtx.js → delete_review-DEvRanb6.js} +414 -29
- package/dist/main.js +1 -1
- package/dist/server.js +103 -205
- package/package.json +1 -1
- package/static/shell.css +6 -18
- package/static/sidebar.css +63 -23
- package/static/workspace.js +49 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.55.6
|
|
4
|
+
|
|
5
|
+
**File paths are shown whole.** A deep directory above a file name was cut short with an ellipsis,
|
|
6
|
+
and the full path appeared only in a browser tooltip on hover. The directory now wraps onto a
|
|
7
|
+
second line where it needs one, so the whole path is always on screen, and the tooltips are gone.
|
|
8
|
+
This covers file locations on issues and questions, related files, the Guide's file rows, the Files
|
|
9
|
+
tab and KISS locations, in the workspace and the extension. The extension's file list, which had no
|
|
10
|
+
tooltip, no longer hides the end of a long directory either. (Extension 0.25.4.)
|
|
11
|
+
|
|
12
|
+
## 0.55.5
|
|
13
|
+
|
|
14
|
+
**Why this change? says what is known, plainly.** Each state now has a label anyone can read and a
|
|
15
|
+
colour that says how sure it is: **Author's reason** (green), the author's own words in the PR
|
|
16
|
+
description; **Possible reason** (gold), "The PR description doesn't give a clear reason. Based on
|
|
17
|
+
the change itself, this appears to…"; and **Reason unclear** (grey), "It isn't clear why this
|
|
18
|
+
change is needed.", with **Ask the author** and the question to ask. Colour is only a thin rule
|
|
19
|
+
beside the reason and the label's own colour; the text stays dark, and the label says it all
|
|
20
|
+
without colour. Once that question is marked answered the section reads **Answer available**: "This
|
|
21
|
+
question has been marked answered. Run the review again after adding the answer to the PR
|
|
22
|
+
description."
|
|
23
|
+
|
|
24
|
+
**A refused reason is never shown as nothing.** When the analysis says the author gave a reason but
|
|
25
|
+
the words it quotes can't be confirmed in the PR description, the section used to show nothing at
|
|
26
|
+
all. It now shows Reason unclear, asking the analysis's own question when it gave one, or "What
|
|
27
|
+
problem does this change solve, or what goal does it serve?". That question has the same id on
|
|
28
|
+
every analysis, so marking it answered survives a rerun, even when the analysis happens to ask it
|
|
29
|
+
in the same words. The refused wording is still never shown, and the evidence the refused reason
|
|
30
|
+
came with is not presented as a reason.
|
|
31
|
+
|
|
32
|
+
**What the harness corrected moves into Analysis details.** The amber box above the review, which
|
|
33
|
+
showed on every tab, is gone; its notes are now listed under Analysis details on the Overview. The
|
|
34
|
+
note for a refused reason says only what the check found, since a quote also fails when it is two
|
|
35
|
+
words or is the template's text. The refused quote (cut at 500 characters) and its evidence are
|
|
36
|
+
kept in the review's audit record, or the extension's dev trace.
|
|
37
|
+
|
|
38
|
+
The prompt asks for the author's words exactly as written, backticks and punctuation included, and
|
|
39
|
+
treats a reason found only in commits, review comments or a linked issue as inferred. Under
|
|
40
|
+
Possible reason, the kind of each piece of evidence ("Test", "Code") is now the size of the text
|
|
41
|
+
beside it. (Extension 0.25.3.)
|
|
42
|
+
|
|
43
|
+
## 0.55.4
|
|
44
|
+
|
|
45
|
+
**Empty pages use the full width.** On the PR context page, the note saying no pull request
|
|
46
|
+
discussion was brought in, and on Diagrams, the card offering to draw a change map or an
|
|
47
|
+
implementation diagram, were held to a narrow column. They now fill the page's width like the
|
|
48
|
+
rest of the workspace.
|
|
49
|
+
|
|
50
|
+
## 0.55.3
|
|
51
|
+
|
|
52
|
+
**Template comments stay hidden.** HTML comments in a PR description, such as a template's
|
|
53
|
+
`<!-- Describe your change -->`, showed as literal text in Author's context, in both the full view
|
|
54
|
+
and the preview. They are now hidden, as on GitHub. The same goes for comments in review comments
|
|
55
|
+
on the PR context page. A comment inside a code block or written as inline code is still shown,
|
|
56
|
+
since there it is code, and a code fence inside a comment no longer turns the rest of the
|
|
57
|
+
description into code. The preview never shows part of a comment. Model output is unchanged:
|
|
58
|
+
nothing it writes is hidden.
|
|
59
|
+
|
|
60
|
+
## 0.55.2
|
|
61
|
+
|
|
62
|
+
**A lighter Overview.** Author's context, Why this change? and What's changing no longer read as
|
|
63
|
+
one block:
|
|
64
|
+
|
|
65
|
+
- **Author's context** opens on a short preview of the first thing the author wrote, past any
|
|
66
|
+
template heading such as "Summary": a paragraph's first two sentences (or about 400 characters,
|
|
67
|
+
never cut mid-word), a list's first three items, or "Code block included" for a long code block.
|
|
68
|
+
**View full PR description** shows the whole description, as written. A description that fits
|
|
69
|
+
the preview is shown whole, with no button.
|
|
70
|
+
- **Why this change?** shows the label and the motive. The author's quote behind a stated motive
|
|
71
|
+
is under **Author's words**, one click away, since the motive usually says the same thing.
|
|
72
|
+
- **What's changing** is asked for in about 80 to 120 words, and says what the change does without
|
|
73
|
+
repeating why it exists or judging it. Analyses made before this version keep their summary.
|
|
74
|
+
- More space between the three sections, and more again before the risk verdict and the
|
|
75
|
+
suggested review path.
|
|
76
|
+
|
|
3
77
|
## 0.55.1
|
|
4
78
|
|
|
5
79
|
**Load more on the Notifications page.** The list shows the newest fifteen, and Load more reveals
|
|
@@ -9395,13 +9395,252 @@ function normalizeSections(value) {
|
|
|
9395
9395
|
return sections;
|
|
9396
9396
|
}
|
|
9397
9397
|
//#endregion
|
|
9398
|
+
//#region ../../packages/review-harness/src/domain/utils/html.ts
|
|
9399
|
+
function escapeHtml(value) {
|
|
9400
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9401
|
+
}
|
|
9402
|
+
function renderInlineText(value) {
|
|
9403
|
+
return escapeHtml(value).replace(/`([^`\n]+)`/g, "<code>$1</code>");
|
|
9404
|
+
}
|
|
9405
|
+
//#endregion
|
|
9406
|
+
//#region ../../packages/review-harness/src/domain/utils/code_block.ts
|
|
9407
|
+
var CODE_BLOCK_CLASS = "pr-helper-code-block";
|
|
9408
|
+
var CODE_COPY_CLASS = "pr-helper-code-copy";
|
|
9409
|
+
//#endregion
|
|
9410
|
+
//#region ../../packages/review-harness/src/domain/utils/markdown.ts
|
|
9411
|
+
var BLOCK_MARKER = "\0";
|
|
9412
|
+
var INLINE_MARKER = "";
|
|
9413
|
+
var BLOCK_TOKEN = /\u0000(\d+)\u0000/g;
|
|
9414
|
+
var BLOCK_TOKEN_LINE = /^\u0000\d+\u0000$/;
|
|
9415
|
+
var INLINE_TOKEN = /\u0001(\d+)\u0001/g;
|
|
9416
|
+
var SAFE_HREF = /^(?:https?:\/\/|#)/i;
|
|
9417
|
+
function renderMarkdown(value, options = {}) {
|
|
9418
|
+
const { text, blocks } = extractFencedBlocks(scanMarkdownLines(value.replace(/[\u0000\u0001]/g, ""), options));
|
|
9419
|
+
return restoreFencedBlocks(renderBlocks(text), blocks);
|
|
9420
|
+
}
|
|
9421
|
+
/**
|
|
9422
|
+
* The fence character a line opens a code block with, or null when it opens none. Shared with the
|
|
9423
|
+
* Author's context preview, which must find the same code blocks this renderer does.
|
|
9424
|
+
*/
|
|
9425
|
+
function fenceOpening(line) {
|
|
9426
|
+
return line.match(/^\s*(`{3,}|~{3,})\s*[\w+#.-]*\s*$/)?.[1][0] ?? null;
|
|
9427
|
+
}
|
|
9428
|
+
/** Whether a line closes a code block opened with `fence`: only a fence of the same character does. */
|
|
9429
|
+
function closesFence(line, fence) {
|
|
9430
|
+
return new RegExp(`^\\s*${fence}{3,}\\s*$`).test(line);
|
|
9431
|
+
}
|
|
9432
|
+
var COMMENT_START_OR_CODE_SPAN = /`[^`\n]+`|<!--/g;
|
|
9433
|
+
function commentStart(line, from) {
|
|
9434
|
+
COMMENT_START_OR_CODE_SPAN.lastIndex = from;
|
|
9435
|
+
for (let match; match = COMMENT_START_OR_CODE_SPAN.exec(line);) if (match[0] === "<!--") return match.index;
|
|
9436
|
+
return -1;
|
|
9437
|
+
}
|
|
9438
|
+
/**
|
|
9439
|
+
* Each line's part in a code block and, when comments are hidden, what of it is outside one.
|
|
9440
|
+
*
|
|
9441
|
+
* Code blocks and comments are read in one pass, in the order they open, as GitHub reads them: a
|
|
9442
|
+
* comment inside a code block is code, and a fence inside a comment is not a fence. The renderer and
|
|
9443
|
+
* the Author's context preview both read Markdown through this, so they agree on what is code and
|
|
9444
|
+
* what is hidden. A comment never closed is not a comment: its text stays, rather than hiding the
|
|
9445
|
+
* rest of what was said.
|
|
9446
|
+
*/
|
|
9447
|
+
function scanMarkdownLines(value, options = {}) {
|
|
9448
|
+
const result = [];
|
|
9449
|
+
let fence = null;
|
|
9450
|
+
let comment = false;
|
|
9451
|
+
let opener = null;
|
|
9452
|
+
let offset = 0;
|
|
9453
|
+
for (const raw of value.split("\n")) {
|
|
9454
|
+
const lineStart = offset;
|
|
9455
|
+
offset += raw.length + 1;
|
|
9456
|
+
if (fence) {
|
|
9457
|
+
const close = closesFence(raw, fence);
|
|
9458
|
+
if (close) fence = null;
|
|
9459
|
+
result.push({
|
|
9460
|
+
raw,
|
|
9461
|
+
visible: raw,
|
|
9462
|
+
code: close ? "close" : "body"
|
|
9463
|
+
});
|
|
9464
|
+
continue;
|
|
9465
|
+
}
|
|
9466
|
+
if (!comment) {
|
|
9467
|
+
fence = fenceOpening(raw);
|
|
9468
|
+
if (fence) {
|
|
9469
|
+
result.push({
|
|
9470
|
+
raw,
|
|
9471
|
+
visible: raw,
|
|
9472
|
+
code: "open"
|
|
9473
|
+
});
|
|
9474
|
+
continue;
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
if (!options.hideHtmlComments) {
|
|
9478
|
+
result.push({
|
|
9479
|
+
raw,
|
|
9480
|
+
visible: raw
|
|
9481
|
+
});
|
|
9482
|
+
continue;
|
|
9483
|
+
}
|
|
9484
|
+
const joining = comment ? opener : null;
|
|
9485
|
+
let visible = "";
|
|
9486
|
+
let pos = 0;
|
|
9487
|
+
while (pos < raw.length) {
|
|
9488
|
+
if (comment) {
|
|
9489
|
+
const end = raw.indexOf("-->", pos);
|
|
9490
|
+
if (end === -1) break;
|
|
9491
|
+
pos = end + 3;
|
|
9492
|
+
comment = false;
|
|
9493
|
+
continue;
|
|
9494
|
+
}
|
|
9495
|
+
const start = commentStart(raw, pos);
|
|
9496
|
+
if (start === -1 || value.indexOf("-->", lineStart + start + 4) === -1) {
|
|
9497
|
+
visible += raw.slice(pos);
|
|
9498
|
+
break;
|
|
9499
|
+
}
|
|
9500
|
+
visible += raw.slice(pos, start);
|
|
9501
|
+
pos = start + 4;
|
|
9502
|
+
comment = true;
|
|
9503
|
+
}
|
|
9504
|
+
if (joining) {
|
|
9505
|
+
joining.visible += visible;
|
|
9506
|
+
result.push({
|
|
9507
|
+
raw,
|
|
9508
|
+
visible: "",
|
|
9509
|
+
absent: true
|
|
9510
|
+
});
|
|
9511
|
+
continue;
|
|
9512
|
+
}
|
|
9513
|
+
const line = {
|
|
9514
|
+
raw,
|
|
9515
|
+
visible
|
|
9516
|
+
};
|
|
9517
|
+
if (comment) opener = visible.trim() === "" ? null : line;
|
|
9518
|
+
result.push(line);
|
|
9519
|
+
}
|
|
9520
|
+
return result;
|
|
9521
|
+
}
|
|
9522
|
+
function extractFencedBlocks(lines) {
|
|
9523
|
+
const blocks = [];
|
|
9524
|
+
const out = [];
|
|
9525
|
+
let body = null;
|
|
9526
|
+
for (const line of lines) {
|
|
9527
|
+
if (line.absent) continue;
|
|
9528
|
+
if (line.code === "open") body = [];
|
|
9529
|
+
else if (line.code === "body") body.push(line.raw);
|
|
9530
|
+
else if (line.code === "close") {
|
|
9531
|
+
out.push(`${BLOCK_MARKER}${blocks.length}${BLOCK_MARKER}`);
|
|
9532
|
+
blocks.push(body.join("\n"));
|
|
9533
|
+
body = null;
|
|
9534
|
+
} else out.push(line.visible);
|
|
9535
|
+
}
|
|
9536
|
+
if (body) {
|
|
9537
|
+
out.push(`${BLOCK_MARKER}${blocks.length}${BLOCK_MARKER}`);
|
|
9538
|
+
blocks.push(body.join("\n"));
|
|
9539
|
+
}
|
|
9540
|
+
return {
|
|
9541
|
+
text: out.join("\n"),
|
|
9542
|
+
blocks
|
|
9543
|
+
};
|
|
9544
|
+
}
|
|
9545
|
+
function restoreFencedBlocks(html, blocks) {
|
|
9546
|
+
return html.replace(BLOCK_TOKEN, (_match, index) => `<div class="${CODE_BLOCK_CLASS}"><pre><code>${escapeHtml(blocks[Number(index)])}</code></pre><button type="button" class="${CODE_COPY_CLASS}" aria-label="Copy code">Copy</button></div>`);
|
|
9547
|
+
}
|
|
9548
|
+
function renderBlocks(text) {
|
|
9549
|
+
const out = [];
|
|
9550
|
+
let paragraph = [];
|
|
9551
|
+
let quote = [];
|
|
9552
|
+
let list = null;
|
|
9553
|
+
const flush = () => {
|
|
9554
|
+
if (paragraph.length) out.push(`<p>${renderInline(paragraph.join(" "))}</p>`);
|
|
9555
|
+
if (quote.length) out.push(`<blockquote>${renderInline(quote.join(" "))}</blockquote>`);
|
|
9556
|
+
if (list) out.push(`<${list.tag}>${list.items.map((item) => `<li>${renderInline(item)}</li>`).join("")}</${list.tag}>`);
|
|
9557
|
+
paragraph = [];
|
|
9558
|
+
quote = [];
|
|
9559
|
+
list = null;
|
|
9560
|
+
};
|
|
9561
|
+
for (const raw of text.split("\n")) {
|
|
9562
|
+
const line = raw.trim();
|
|
9563
|
+
if (!line) {
|
|
9564
|
+
flush();
|
|
9565
|
+
continue;
|
|
9566
|
+
}
|
|
9567
|
+
if (BLOCK_TOKEN_LINE.test(line)) {
|
|
9568
|
+
flush();
|
|
9569
|
+
out.push(line);
|
|
9570
|
+
continue;
|
|
9571
|
+
}
|
|
9572
|
+
if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(line)) {
|
|
9573
|
+
flush();
|
|
9574
|
+
out.push("<hr>");
|
|
9575
|
+
continue;
|
|
9576
|
+
}
|
|
9577
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
9578
|
+
if (heading) {
|
|
9579
|
+
flush();
|
|
9580
|
+
const level = Math.min(heading[1].length, 3) + 3;
|
|
9581
|
+
out.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
|
|
9582
|
+
continue;
|
|
9583
|
+
}
|
|
9584
|
+
const bullet = line.match(/^[-*+]\s+(.+)$/);
|
|
9585
|
+
if (bullet) {
|
|
9586
|
+
if (list?.tag !== "ul") {
|
|
9587
|
+
flush();
|
|
9588
|
+
list = {
|
|
9589
|
+
tag: "ul",
|
|
9590
|
+
items: []
|
|
9591
|
+
};
|
|
9592
|
+
}
|
|
9593
|
+
list.items.push(bullet[1]);
|
|
9594
|
+
continue;
|
|
9595
|
+
}
|
|
9596
|
+
const ordered = line.match(/^\d{1,9}[.)]\s+(.+)$/);
|
|
9597
|
+
if (ordered) {
|
|
9598
|
+
if (list?.tag !== "ol") {
|
|
9599
|
+
flush();
|
|
9600
|
+
list = {
|
|
9601
|
+
tag: "ol",
|
|
9602
|
+
items: []
|
|
9603
|
+
};
|
|
9604
|
+
}
|
|
9605
|
+
list.items.push(ordered[1]);
|
|
9606
|
+
continue;
|
|
9607
|
+
}
|
|
9608
|
+
const quoted = line.match(/^>\s?(.*)$/);
|
|
9609
|
+
if (quoted) {
|
|
9610
|
+
if (!quote.length) flush();
|
|
9611
|
+
quote.push(quoted[1]);
|
|
9612
|
+
continue;
|
|
9613
|
+
}
|
|
9614
|
+
if (list && list.items.length) {
|
|
9615
|
+
list.items[list.items.length - 1] += ` ${line}`;
|
|
9616
|
+
continue;
|
|
9617
|
+
}
|
|
9618
|
+
if (quote.length) {
|
|
9619
|
+
quote[quote.length - 1] += ` ${line}`;
|
|
9620
|
+
continue;
|
|
9621
|
+
}
|
|
9622
|
+
paragraph.push(line);
|
|
9623
|
+
}
|
|
9624
|
+
flush();
|
|
9625
|
+
return out.join("");
|
|
9626
|
+
}
|
|
9627
|
+
function renderInline(value) {
|
|
9628
|
+
const codeSpans = [];
|
|
9629
|
+
return escapeHtml(value).replace(/`([^`\n]+)`/g, (_match, code) => {
|
|
9630
|
+
codeSpans.push(code);
|
|
9631
|
+
return `${INLINE_MARKER}${codeSpans.length - 1}${INLINE_MARKER}`;
|
|
9632
|
+
}).replace(/(?<!!)\[([^\]\n]+)\]\(([^)\s]+)\)/g, (match, label, href) => SAFE_HREF.test(href) ? `<a href="${href}" target="_blank" rel="noopener noreferrer">${label}</a>` : match).replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>").replace(/__([^_\n]+)__/g, "<strong>$1</strong>").replace(/(^|[^*\w])\*([^*\n]+)\*/g, "$1<em>$2</em>").replace(/(^|[^_\w])_([^_\n]+)_(?![\w_])/g, "$1<em>$2</em>").replace(INLINE_TOKEN, (_match, index) => `<code>${codeSpans[Number(index)]}</code>`);
|
|
9633
|
+
}
|
|
9634
|
+
//#endregion
|
|
9398
9635
|
//#region ../../packages/review-harness/src/domain/ai_review/author_description.ts
|
|
9399
9636
|
/**
|
|
9400
9637
|
* Reading a pull request's description for whether it says anything, without ever changing it.
|
|
9401
9638
|
*
|
|
9402
|
-
* Nothing here returns a copy to store or show. The description is shown
|
|
9403
|
-
* copy is only ever a means of asking a question of it: whether it says
|
|
9404
|
-
* quote is something the author said.
|
|
9639
|
+
* Nothing here returns a copy to store, or a reworded one to show. The description is shown
|
|
9640
|
+
* verbatim, so a cleaned copy is only ever a means of asking a question of it: whether it says
|
|
9641
|
+
* anything, whether a quote is something the author said, and where its preview ends. The preview
|
|
9642
|
+
* itself is the author's own lines, cut short, never edited, less the HTML comments the full view
|
|
9643
|
+
* hides too.
|
|
9405
9644
|
*/
|
|
9406
9645
|
/**
|
|
9407
9646
|
* Lines that are a pull request template's scaffolding when they stand alone.
|
|
@@ -9435,7 +9674,6 @@ var TEMPLATE_LABELS = /* @__PURE__ */ new Set([
|
|
|
9435
9674
|
"related issue",
|
|
9436
9675
|
"links"
|
|
9437
9676
|
]);
|
|
9438
|
-
var HTML_COMMENT = /<!--[\s\S]*?-->/g;
|
|
9439
9677
|
var HEADING_MARKER = /^#{1,6}\s*/;
|
|
9440
9678
|
var UNCHECKED_TASK = /^[-*+]\s+\[ \]/;
|
|
9441
9679
|
var EMPTY_LIST_ITEM = /^(?:[-*+]|\d+[.)])\s*$/;
|
|
@@ -9457,7 +9695,7 @@ function substance(line) {
|
|
|
9457
9695
|
*/
|
|
9458
9696
|
function authorText(description) {
|
|
9459
9697
|
if (!description) return "";
|
|
9460
|
-
return description.
|
|
9698
|
+
return visibleLines(description).map((line) => substance(line.visible)).filter((line) => line !== "").join("\n");
|
|
9461
9699
|
}
|
|
9462
9700
|
/**
|
|
9463
9701
|
* Whether a description has anything in it beyond an unfilled template.
|
|
@@ -9468,9 +9706,138 @@ function authorText(description) {
|
|
|
9468
9706
|
function hasAuthorContent(description) {
|
|
9469
9707
|
return authorText(description) !== "";
|
|
9470
9708
|
}
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9709
|
+
var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s/;
|
|
9710
|
+
var ATX_HEADING = /^#{1,6}(?:\s|$)/;
|
|
9711
|
+
/**
|
|
9712
|
+
* The description as the full view reads it, by the renderer's own scan, so the two always agree
|
|
9713
|
+
* on what is code and what is a hidden comment.
|
|
9714
|
+
*/
|
|
9715
|
+
function visibleLines(description) {
|
|
9716
|
+
return scanMarkdownLines(description, { hideHtmlComments: true }).filter((line) => !line.absent);
|
|
9717
|
+
}
|
|
9718
|
+
/**
|
|
9719
|
+
* The description's Markdown blocks, with a code block kept whole and always a block of its own.
|
|
9720
|
+
*
|
|
9721
|
+
* Split where Markdown itself would end one: a blank line, a heading (always a block of its own)
|
|
9722
|
+
* and a list that starts straight after a paragraph. Descriptions are often written with no blank
|
|
9723
|
+
* lines at all, and on blank lines alone those would be one block. A line that is only a comment
|
|
9724
|
+
* ends a block like a blank one.
|
|
9725
|
+
*/
|
|
9726
|
+
function blocks(description) {
|
|
9727
|
+
const result = [];
|
|
9728
|
+
let current = [];
|
|
9729
|
+
const end = () => {
|
|
9730
|
+
if (current.length) result.push(current);
|
|
9731
|
+
current = [];
|
|
9732
|
+
};
|
|
9733
|
+
for (const line of visibleLines(description)) if (line.code === "open") {
|
|
9734
|
+
end();
|
|
9735
|
+
current.push(line);
|
|
9736
|
+
} else if (line.code === "body") current.push(line);
|
|
9737
|
+
else if (line.code === "close") {
|
|
9738
|
+
current.push(line);
|
|
9739
|
+
end();
|
|
9740
|
+
} else if (line.visible.trim() === "") end();
|
|
9741
|
+
else if (ATX_HEADING.test(line.visible.trim())) {
|
|
9742
|
+
end();
|
|
9743
|
+
result.push([line]);
|
|
9744
|
+
} else if (LIST_ITEM.test(line.visible) && current.length && !LIST_ITEM.test(current[0].visible)) {
|
|
9745
|
+
end();
|
|
9746
|
+
current.push(line);
|
|
9747
|
+
} else current.push(line);
|
|
9748
|
+
end();
|
|
9749
|
+
return result;
|
|
9750
|
+
}
|
|
9751
|
+
var ELLIPSIS = "…";
|
|
9752
|
+
/**
|
|
9753
|
+
* Whether cutting a paragraph after `text` leaves valid Markdown: no code span, link or bold left
|
|
9754
|
+
* open. Approximate by design, and erring towards an earlier cut or none.
|
|
9755
|
+
*/
|
|
9756
|
+
function closesCleanly(text) {
|
|
9757
|
+
if ((text.match(/`/g) ?? []).length % 2) return false;
|
|
9758
|
+
if ((text.match(/\*\*/g) ?? []).length % 2) return false;
|
|
9759
|
+
if ((text.replace(/\*\*/g, "").match(/\*/g) ?? []).length % 2) return false;
|
|
9760
|
+
if ((text.match(/__/g) ?? []).length % 2) return false;
|
|
9761
|
+
if ((text.match(/\[/g) ?? []).length !== (text.match(/\]/g) ?? []).length) return false;
|
|
9762
|
+
if ((text.match(/\]\(/g) ?? []).length !== (text.match(/\]\([^)]*\)/g) ?? []).length) return false;
|
|
9763
|
+
return true;
|
|
9764
|
+
}
|
|
9765
|
+
/**
|
|
9766
|
+
* A paragraph cut to its first sentences, or to a word boundary near the character limit, at a
|
|
9767
|
+
* point that leaves no Markdown open. Null when it fits whole.
|
|
9768
|
+
*/
|
|
9769
|
+
function boundParagraph(paragraph) {
|
|
9770
|
+
const text = paragraph.trimEnd();
|
|
9771
|
+
const cuts = [];
|
|
9772
|
+
for (const match of text.matchAll(/\s+/g)) {
|
|
9773
|
+
const at = match.index;
|
|
9774
|
+
if (!closesCleanly(text.slice(0, at))) continue;
|
|
9775
|
+
cuts.push({
|
|
9776
|
+
at,
|
|
9777
|
+
sentence: /[.!?]["')\]]?$/.test(text.slice(0, at))
|
|
9778
|
+
});
|
|
9779
|
+
}
|
|
9780
|
+
const bySentence = cuts.filter((cut) => cut.sentence)[1];
|
|
9781
|
+
if (text.length <= 400 && !bySentence) return null;
|
|
9782
|
+
if (bySentence && bySentence.at <= 400) return `${text.slice(0, bySentence.at)} ${ELLIPSIS}`;
|
|
9783
|
+
if (text.length <= 400) return null;
|
|
9784
|
+
const within = cuts.filter((cut) => cut.at <= 400).pop() ?? cuts.find((cut) => cut.at > 400);
|
|
9785
|
+
return within ? `${text.slice(0, within.at)} ${ELLIPSIS}` : null;
|
|
9786
|
+
}
|
|
9787
|
+
/** A list cut to its first items, each whole with any lines that continue it. Null when it fits. */
|
|
9788
|
+
function boundList(lines) {
|
|
9789
|
+
const indent = (line) => line.length - line.trimStart().length;
|
|
9790
|
+
const base = indent(lines[0]);
|
|
9791
|
+
const starts = lines.flatMap((line, i) => LIST_ITEM.test(line) && indent(line) <= base ? [i] : []);
|
|
9792
|
+
if (starts.length <= 3) return null;
|
|
9793
|
+
return `${lines.slice(0, starts[3]).join("\n")}\n\n${ELLIPSIS}`;
|
|
9794
|
+
}
|
|
9795
|
+
/**
|
|
9796
|
+
* What the Overview shows of a description before the reader asks for all of it.
|
|
9797
|
+
*
|
|
9798
|
+
* The first block the author wrote decides what to show; the bounds decide how much of it. A
|
|
9799
|
+
* template's labels, comments and unticked items ahead of it are passed over, so "## Summary" opens
|
|
9800
|
+
* on the summary itself. A heading of the author's own is kept, with the block it introduces.
|
|
9801
|
+
* A paragraph is cut after its first sentences or near a character limit, a list after its first
|
|
9802
|
+
* items, and a long code block is left out; nothing is cut mid-word or left as open Markdown.
|
|
9803
|
+
*
|
|
9804
|
+
* Null when the whole description fits: there is nothing more to open.
|
|
9805
|
+
*/
|
|
9806
|
+
function authorPreview(description) {
|
|
9807
|
+
if (!hasAuthorContent(description)) return null;
|
|
9808
|
+
const all = blocks(description);
|
|
9809
|
+
const shown = [];
|
|
9810
|
+
let codeOmitted = false;
|
|
9811
|
+
let cut = false;
|
|
9812
|
+
let i = 0;
|
|
9813
|
+
for (; i < all.length; i++) {
|
|
9814
|
+
const lines = all[i];
|
|
9815
|
+
const first = lines.findIndex((line) => substance(line.visible) !== "");
|
|
9816
|
+
if (first === -1) continue;
|
|
9817
|
+
const raw = lines.slice(first).map((line) => line.visible);
|
|
9818
|
+
if (raw.length === 1 && ATX_HEADING.test(lines[first].visible.trim())) {
|
|
9819
|
+
shown.push(raw[0]);
|
|
9820
|
+
if (shown.length > 1) break;
|
|
9821
|
+
continue;
|
|
9822
|
+
}
|
|
9823
|
+
if (lines[first].code === "open") {
|
|
9824
|
+
if (lines.filter((line) => line.code === "body").length > 6) {
|
|
9825
|
+
codeOmitted = true;
|
|
9826
|
+
cut = true;
|
|
9827
|
+
} else shown.push(raw.join("\n"));
|
|
9828
|
+
} else {
|
|
9829
|
+
const bounded = LIST_ITEM.test(raw[0]) ? boundList(raw) : boundParagraph(raw.join("\n"));
|
|
9830
|
+
cut = bounded !== null;
|
|
9831
|
+
shown.push(bounded ?? raw.join("\n"));
|
|
9832
|
+
}
|
|
9833
|
+
break;
|
|
9834
|
+
}
|
|
9835
|
+
const rest = all.slice(i + 1).flat().map((line) => line.visible);
|
|
9836
|
+
if (!cut && !rest.some((line) => substance(line) !== "")) return null;
|
|
9837
|
+
return {
|
|
9838
|
+
markdown: shown.join("\n\n"),
|
|
9839
|
+
codeOmitted
|
|
9840
|
+
};
|
|
9474
9841
|
}
|
|
9475
9842
|
//#endregion
|
|
9476
9843
|
//#region ../../packages/review-harness/src/domain/ai_review/motivation.ts
|
|
@@ -9488,7 +9855,7 @@ var MOTIVATION_EVIDENCE_KINDS = [
|
|
|
9488
9855
|
"code"
|
|
9489
9856
|
];
|
|
9490
9857
|
/** Ours, not the model's: what an unsupported claim is replaced with when a question remains. */
|
|
9491
|
-
var UNESTABLISHED_MOTIVATION = "
|
|
9858
|
+
var UNESTABLISHED_MOTIVATION = "It isn't clear why this change is needed.";
|
|
9492
9859
|
/** Stable, so "Mark answered" on it survives a reload like any other question's. */
|
|
9493
9860
|
var INTENT_QUESTION_ID = "intent-motivation";
|
|
9494
9861
|
/**
|
|
@@ -9555,9 +9922,9 @@ function isAuthorQuote(quote, description) {
|
|
|
9555
9922
|
* fields that belong to the other statuses are dropped rather than carried, so a `stated` motive
|
|
9556
9923
|
* cannot also show a guess and an `unclear` one cannot also show a quote.
|
|
9557
9924
|
*
|
|
9558
|
-
* A claim that fails
|
|
9559
|
-
*
|
|
9560
|
-
*
|
|
9925
|
+
* A claim that fails keeps none of its summary and becomes unclear, in our words, asking the
|
|
9926
|
+
* model's question or ours. Its evidence goes to the audit. An unclear motive that asked nothing
|
|
9927
|
+
* keeps its own summary, which claims nothing, and gets our question.
|
|
9561
9928
|
*/
|
|
9562
9929
|
function verifyMotivation(candidate, authorDescription) {
|
|
9563
9930
|
if (!candidate) return {};
|
|
@@ -9584,24 +9951,30 @@ function verifyMotivation(candidate, authorDescription) {
|
|
|
9584
9951
|
const correction = {
|
|
9585
9952
|
status: candidate.status,
|
|
9586
9953
|
summary: candidate.summary,
|
|
9587
|
-
reason
|
|
9954
|
+
reason,
|
|
9955
|
+
...rejectedQuote(candidate, reason),
|
|
9956
|
+
...candidate.evidence.length > 0 ? { discardedEvidence: candidate.evidence } : {}
|
|
9588
9957
|
};
|
|
9589
|
-
|
|
9958
|
+
return {
|
|
9590
9959
|
motivation: {
|
|
9591
9960
|
contractVersion: 1,
|
|
9592
9961
|
status: "unclear",
|
|
9593
|
-
summary: UNESTABLISHED_MOTIVATION,
|
|
9594
|
-
authorQuestion: candidate.authorQuestion
|
|
9962
|
+
summary: candidate.status === "unclear" ? candidate.summary : UNESTABLISHED_MOTIVATION,
|
|
9963
|
+
authorQuestion: candidate.authorQuestion ?? "What problem does this change solve, or what goal does it serve?"
|
|
9595
9964
|
},
|
|
9596
9965
|
correction: {
|
|
9597
9966
|
...correction,
|
|
9598
9967
|
outcome: "downgraded"
|
|
9599
9968
|
}
|
|
9600
9969
|
};
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9970
|
+
}
|
|
9971
|
+
function rejectedQuote(candidate, reason) {
|
|
9972
|
+
if (reason !== "unverified-quote" || !candidate.sourceQuote) return {};
|
|
9973
|
+
const quote = candidate.sourceQuote;
|
|
9974
|
+
return quote.length > 500 ? {
|
|
9975
|
+
rejectedQuote: quote.slice(0, 500),
|
|
9976
|
+
rejectedQuoteTruncated: true
|
|
9977
|
+
} : { rejectedQuote: quote };
|
|
9605
9978
|
}
|
|
9606
9979
|
/**
|
|
9607
9980
|
* The one evidence-link policy, for both hosts.
|
|
@@ -9647,21 +10020,23 @@ function sameQuestion(a, b) {
|
|
|
9647
10020
|
/**
|
|
9648
10021
|
* File an unclear motive's question with the rest, as the one Intent question.
|
|
9649
10022
|
*
|
|
9650
|
-
* A question the model already asked in the same words
|
|
9651
|
-
* reader meets it once
|
|
9652
|
-
*
|
|
10023
|
+
* A question the model already asked in the same words becomes the Intent question rather than a
|
|
10024
|
+
* second copy, so the reader meets it once. It takes the stable id either way: a model's ids are
|
|
10025
|
+
* new on every analysis, and "Mark answered" is stored by id. Telling two differently worded
|
|
10026
|
+
* questions apart is judgement and is left to the prompt.
|
|
9653
10027
|
*/
|
|
9654
10028
|
function withIntentQuestion(questions, motivation) {
|
|
9655
10029
|
if (motivation?.status !== "unclear" || !motivation.authorQuestion) return questions;
|
|
9656
10030
|
const asked = motivation.authorQuestion;
|
|
9657
10031
|
const existing = questions.findIndex((q) => sameQuestion(q.question, asked));
|
|
10032
|
+
const taken = new Set(questions.filter((_, index) => index !== existing).map((q) => q.id));
|
|
10033
|
+
let id = INTENT_QUESTION_ID;
|
|
10034
|
+
for (let n = 2; taken.has(id); n += 1) id = `${INTENT_QUESTION_ID}-${n}`;
|
|
9658
10035
|
if (existing >= 0) return questions.map((q, index) => index === existing ? {
|
|
9659
10036
|
...q,
|
|
10037
|
+
id,
|
|
9660
10038
|
category: "intent"
|
|
9661
10039
|
} : q);
|
|
9662
|
-
const taken = new Set(questions.map((q) => q.id));
|
|
9663
|
-
let id = INTENT_QUESTION_ID;
|
|
9664
|
-
for (let n = 2; taken.has(id); n += 1) id = `${INTENT_QUESTION_ID}-${n}`;
|
|
9665
10040
|
return [...questions, {
|
|
9666
10041
|
id,
|
|
9667
10042
|
question: asked,
|
|
@@ -9682,6 +10057,15 @@ function applyMotivation(result, candidate, authorDescription) {
|
|
|
9682
10057
|
};
|
|
9683
10058
|
}
|
|
9684
10059
|
//#endregion
|
|
10060
|
+
//#region ../../packages/review-harness/src/domain/ai_review/summary_guidance.ts
|
|
10061
|
+
/**
|
|
10062
|
+
* What the "summary" field is for, as both the first analysis and a reconciliation ask for it.
|
|
10063
|
+
*
|
|
10064
|
+
* One string so the two prompts cannot drift. It renders as "What's changing" on the Overview,
|
|
10065
|
+
* between "Why this change?" and the risk card, so it says neither why nor how good.
|
|
10066
|
+
*/
|
|
10067
|
+
var SUMMARY_GUIDANCE = `"summary": WHAT the change does, for a reviewer about to read it. Roughly 80-120 words, and fewer for a small change. Name the parts that change and what each now does. Do not explain why the change exists, and do not judge whether it is correct or safe: other fields carry both.`;
|
|
10068
|
+
//#endregion
|
|
9685
10069
|
//#region ../../packages/review-harness/src/domain/ai_review/pipeline.ts
|
|
9686
10070
|
var STRUCTURED_OUTPUT_INSTRUCTIONS = `In addition to the review principles above, first understand the implementation, then respond with a single JSON object (no markdown, no surrounding text) matching exactly this shape:
|
|
9687
10071
|
{
|
|
@@ -9715,8 +10099,9 @@ var STRUCTURED_OUTPUT_INSTRUCTIONS = `In addition to the review principles above
|
|
|
9715
10099
|
}
|
|
9716
10100
|
|
|
9717
10101
|
Field guidance:
|
|
10102
|
+
- ${SUMMARY_GUIDANCE}
|
|
9718
10103
|
- "motivation": WHY this change exists, which is a different question from what it does ("summary" covers that). Name the problem being solved or the goal being served, never the mechanics: "adds a retry loop" is what the code does, "the sync job was failing on transient network errors" is why. Weigh sources in this order: the PR Description and any issue it links; then tests and user-visible behaviour; then commit messages and docs; then the implementation itself. Choose exactly one "status":
|
|
9719
|
-
- "stated": the author's own words in the PR Description say why. "sourceQuote" is REQUIRED: a short passage (one sentence or less, at least three words) copied character for character from the PR Description, which says why. Only the PR Description counts as the author stating it. Never use "stated" when the description only restates what the code does.
|
|
10104
|
+
- "stated": the author's own words in the PR Description say why. "sourceQuote" is REQUIRED: a short passage (one sentence or less, at least three words) copied character for character from the PR Description, which says why. Copy it exactly as written, keeping backticks, punctuation, hyphenation and spelling; never paraphrase, shorten words, join two sentences or correct a typo. Only the PR Description counts as the author stating it: a reason that appears only in commit messages, review comments, or an issue the description merely links to is not "stated" (use "inferred" and cite it as evidence). Never use "stated" when the description only restates what the code does. If the change itself also shows the reason, include it as "evidence".
|
|
9720
10105
|
- "inferred": nothing states it, but the change gives real evidence of it. "evidence" is REQUIRED and non-empty: each entry names what points to the motive, such as a test asserting the fixed behaviour, an issue reference, or a commit message, with "file" set to the changed file it is in where there is one. Never infer a motive from names alone (a branch, a function or a file being called "fix-x" is not evidence). Word the summary as a likely reading, not a fact: "This appears to..." rather than "This fixes...".
|
|
9721
10106
|
- "unclear": neither the description nor the change establishes why, AND knowing why would change how this change should be reviewed (for example, whether a behaviour change is intended). "authorQuestion" is REQUIRED: one focused question for the author that would settle it. Do not also ask that question, in any wording, in "questions".
|
|
9722
10107
|
If the motive is unclear and knowing it would not change the review, omit "motivation" entirely. Do not add it just to fill the field.
|
|
@@ -12107,7 +12492,7 @@ function buildReconciliationInstructions(input, options = {}) {
|
|
|
12107
12492
|
The completeness rule applies here too: one entry per numbered question, and "still-open" with a note rather than an omission when you are unsure. Do not manufacture an answer out of intent you cannot see. A question about why something was done the way it was is often not answerable from code at all, and "still-open" is the correct answer to it.`] : [],
|
|
12108
12493
|
`Read the changed code above as a reviewer, not only as an auditor of the numbered list. These commits have not been reviewed by anyone. Report every genuinely NEW problem you find in them in "newIssues", whether or not it relates to anything on the list — code written to fix a finding is exactly where a regression gets introduced, and reassessing the list and stopping there is half the job. Do NOT repeat a previous finding here: if it still stands, that is what "still-open" is for.`,
|
|
12109
12494
|
`If the new changes introduce an area the earlier analysis had no section for, add it to "newSections". Otherwise return an empty array. Do not restate or restructure the earlier analysis's existing sections.`,
|
|
12110
|
-
`Give a fresh "summary", "overallRisk", "recommendation" and "nextSteps" for the pull request AS IT NOW STANDS, taking both the earlier findings that survive and the new changes into account. Unlike a partial review, you have been shown the earlier analysis in full and are expected to revise its verdict where the new changes justify it
|
|
12495
|
+
`Give a fresh "summary", "overallRisk", "recommendation" and "nextSteps" for the pull request AS IT NOW STANDS, taking both the earlier findings that survive and the new changes into account. Unlike a partial review, you have been shown the earlier analysis in full and are expected to revise its verdict where the new changes justify it. ${SUMMARY_GUIDANCE}`
|
|
12111
12496
|
].map((task, index) => `${index + 1}. ${task}`).join("\n");
|
|
12112
12497
|
return `You are RECONCILING an earlier analysis of this pull request against the changes pushed since that analysis was made.
|
|
12113
12498
|
|
|
@@ -13433,4 +13818,4 @@ function lineageIds(id) {
|
|
|
13433
13818
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
13434
13819
|
}
|
|
13435
13820
|
//#endregion
|
|
13436
|
-
export {
|
|
13821
|
+
export { extractJsonText as $, resolveModel as $n, isClaimed as $t, createPrState as A, parseLifecycle as An, writePortPreference as At, toneOf as B, issuesOutstanding as Bn, allJobs as Bt, readEvents as C, LIFECYCLES as Cn, feedbackUrl as Ct, announceJob as D, lifecycleChip as Dn, parseWorkspacePort as Dt, telemetryUploadConsent as E, lifecycleAction as En, MIN_WORKSPACE_PORT as Et, markAllRead as F, queueStamp as Fn, fillFileUrlTemplate as Ft, authorPreview as G, describeAuthorship as Gn, saveJob as Gt, withUsageRecorded as H, reviewerDispositions as Hn, fail as Ht, markRead as I, readQueue as In, checkCodeFreshness as It, CODE_BLOCK_CLASS as J, resolveTarget as Jn, phasesFor as Jt, hasAuthorContent as K, WITHHELD_REASON as Kn, PHASES as Kt, pruneNotifications as L, doneVerb as Ln, checkReview as Lt, eventName as M, queueAdd as Mn, forgeResolver as Mt, headline as N, queueAddMany as Nn, forgeFor as Nt, branchOf as O, lifecyclePill as On, resolveWorkspacePort as Ot, listNotifications as P, queueRemove as Pn, remoteHost as Pt, asString as Q, resolveAgent as Qn, driverGone as Qt, readNotification as R, isIssueOutstanding as Rn, followedBranch as Rt, startJob as S, workspaceStamps as Sn, PACKAGE_NAME as St, recordTelemetryUploadConsent as T, isLifecycle as Tn, MAX_WORKSPACE_PORT as Tt, EXPLAIN_SIMPLY_PROMPT as U, processDiscussion as Un, isTerminal as Ut, unreadCount as V, questionsOutstanding as Vn, endedAt as Vt, UNESTABLISHED_MOTIVATION as W, recommendationsOf as Wn, loadJob as Wt, escapeHtml as X, isCommitReview as Xn, purposeOf as Xt, CODE_COPY_CLASS as Y, displayRef as Yn, progressSteps as Yt, renderInlineText as Z, git as Zn, clearClaim as Zt, agentEnvOf as _, reviewedCommit as _n, spawnServer as _t, wasBlocked as a, kissDecisionRefusal as an, AGENT_IDS as ar, relativeTime as at, reanalyseReview as b, touchWorkspace as bn, writeServerRecord as bt, RefreshUnavailableError as c, lineageKeyFor as cn, agentFor as cr, queueForReview as ct, refreshPrContext as d, matchingWorkspaceIds as dn, AgentUnavailableError as dr, bootstrapUrl as dt, decideKissRecommendation as en, clearAgentPreference as er, ReviewBeingDeletedError as et, isUnchanged as f, mutateWorkspace as fn, clearServerRecord as ft, runningJobs as g, recentWorkspaces as gn, serverEntryExists as gt, liveUpdateFor as h, previousKissResult as hn, reviewsUrl as ht, liveJobsFor as i, indexReviewGroups as in, STORE_ROOT as ir, modelHelpLines as it, createPrUpdate as j, sectionOf as jn, claimRequestUrl as jt, subjectOf as k, offersLifecycle as kn, statedWorkspacePort as kt, mergeUpdateInto as l, lineagePosition as ln, detectAgents as lr, settleQueue as lt, UpdateAlreadyRunningError as m, presentedKissRunOf as mn, readServerRecord as mt, deleteReview as n, groupByLineage as nn, writeAgentPreference as nr, readMarker as nt, discardJob as o, kissDecisionsOf as on, DEFAULT_AGENT_ID as or, activateQueued as ot, askCheckout as p, positionInLineage as pn, ensureServer as pt, renderMarkdown as q, kissOutcome as qn, isLiveJob as qt, lineageIds as r, indexKeptIds as rn, CONFIG_PATH as rr, removeWorktree as rt, FETCH_TIMEOUT_MS as s, latestKissRun as sn, agentById as sr, compactQueue as st, deleteLineage as t, followedRefName as tn, readAgentPreference as tr, MANAGED_ROOT as tt, updateReview as u, loadWorkspace as un, AgentCancelledError as ur, readIndexToken as ut, recordKissRun as v, reviewedRepositories as vn, stopServer as vt, record as w, activityLabel as wn, DEFAULT_WORKSPACE_PORT as wt, runJob as x, workspaceRevision as xn, BUILD_VERSION as xt, startKissJob as y, summarise as yn, workspaceUrl as yt, removeNotification as z, isQuestionOutstanding as zn, reviewedShaOf as zt };
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $n as resolveModel, $t as isClaimed, At as writePortPreference, Bt as allJobs, Dt as parseWorkspacePort, E as telemetryUploadConsent, Et as MIN_WORKSPACE_PORT, Jn as resolveTarget, Kn as WITHHELD_REASON, Kt as PHASES, Lt as checkReview, Mt as forgeResolver, Qn as resolveAgent, Qt as driverGone, S as startJob, St as PACKAGE_NAME, T as recordTelemetryUploadConsent, Tt as MAX_WORKSPACE_PORT, Ut as isTerminal, Wt as loadJob, Xt as purposeOf, Yn as displayRef, Yt as progressSteps, Zn as git, Zt as clearClaim, a as wasBlocked, ar as AGENT_IDS, at as relativeTime, b as reanalyseReview, cn as lineageKeyFor, ct as queueForReview, dn as matchingWorkspaceIds, dt as bootstrapUrl, fn as mutateWorkspace, g as runningJobs, gn as recentWorkspaces, h as liveUpdateFor, ht as reviewsUrl, i as liveJobsFor, ir as STORE_ROOT, it as modelHelpLines, kt as statedWorkspacePort, l as mergeUpdateInto, lr as detectAgents, m as UpdateAlreadyRunningError, n as deleteReview$1, nn as groupByLineage, nr as writeAgentPreference, nt as readMarker, or as DEFAULT_AGENT_ID, pn as positionInLineage, pt as ensureServer, qn as kissOutcome, rr as CONFIG_PATH, rt as removeWorktree, sr as agentById, tn as followedRefName, tr as readAgentPreference, tt as MANAGED_ROOT, u as updateReview, un as loadWorkspace, ut as readIndexToken, v as recordKissRun, vn as reviewedRepositories, vt as stopServer, w as record$1, wt as DEFAULT_WORKSPACE_PORT, x as runJob, xt as BUILD_VERSION, yn as summarise, yt as workspaceUrl } from "./delete_review-DEvRanb6.js";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|