prreviewbuddy 0.53.1 → 0.55.1

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.
@@ -1643,7 +1643,8 @@ function completeReviewSession(session, result, context, options = {}) {
1643
1643
  ...session.metadata,
1644
1644
  ...context ? reviewFacts(result, context) : {
1645
1645
  fileCoverage: void 0,
1646
- discussionStats: void 0
1646
+ discussionStats: void 0,
1647
+ authorDescription: void 0
1647
1648
  },
1648
1649
  commitSha: options.commitSha ?? session.metadata.commitSha,
1649
1650
  durationMs: Math.max(0, now - session.metadata.generatedAt)
@@ -1664,7 +1665,8 @@ function reviewFacts(result, context) {
1664
1665
  reviewedFiles: sectionedFiles(result, context),
1665
1666
  totalChangedFiles: context.fileCoverage.totalChangedFiles
1666
1667
  },
1667
- ...context.prContext ? { discussionStats: countDiscussion(context) } : {}
1668
+ ...context.prContext ? { discussionStats: countDiscussion(context) } : {},
1669
+ authorDescription: context.prContext?.pullRequest.body
1668
1670
  };
1669
1671
  }
1670
1672
  /**
@@ -2237,6 +2239,7 @@ async function buildChangeSet(options) {
2237
2239
  number: 0,
2238
2240
  title: changeTitle(subjects, headRef),
2239
2241
  body: subjects.length > 1 ? subjects.map((s) => `- ${s}`).join("\n") : null,
2242
+ bodySource: "commit-subjects",
2240
2243
  base: {
2241
2244
  ref: base.display,
2242
2245
  sha: mergeBase
@@ -6296,6 +6299,7 @@ function sanitisePullRequest(value) {
6296
6299
  };
6297
6300
  const title = typeof value.title === "string" ? value.title.trim() : "";
6298
6301
  if (title) pr.title = title.slice(0, 200);
6302
+ if (typeof value.body === "string" && value.body.trim()) pr.body = value.body;
6299
6303
  const headSha = label(value.headSha);
6300
6304
  if (headSha) pr.headSha = headSha;
6301
6305
  const headRef = label(value.headRef);
@@ -6851,7 +6855,7 @@ function slug(repo) {
6851
6855
  * `baseRefOid` is a diff taken against the wrong *commit*, which is worse because the branch name
6852
6856
  * is right and everything reads as normal. A list nobody asserts is a list somebody tidies.
6853
6857
  */
6854
- var PR_FIELDS = "number,url,title,headRefName,headRefOid,baseRefName,baseRefOid,changedFiles,author";
6858
+ var PR_FIELDS = "number,url,title,body,headRefName,headRefOid,baseRefName,baseRefOid,changedFiles,author";
6855
6859
  async function viewRequest(repo, reference, cwd, env) {
6856
6860
  return await ghJson([
6857
6861
  "pr",
@@ -6863,7 +6867,7 @@ async function viewRequest(repo, reference, cwd, env) {
6863
6867
  PR_FIELDS
6864
6868
  ], cwd, env);
6865
6869
  }
6866
- function text(value) {
6870
+ function text$1(value) {
6867
6871
  return typeof value === "string" ? value.trim() : "";
6868
6872
  }
6869
6873
  /**
@@ -6884,30 +6888,31 @@ function fileUrlTemplate(prUrl) {
6884
6888
  * email address all live in `pr_context.ts` and in nothing else.
6885
6889
  */
6886
6890
  function identity(raw) {
6887
- const url = text(raw.url);
6891
+ const url = text$1(raw.url);
6888
6892
  const author = raw.author ?? null;
6889
6893
  return {
6890
6894
  provider: GITHUB_FORGE_ID,
6891
6895
  number: typeof raw.number === "number" ? raw.number : 0,
6892
6896
  url,
6893
- title: text(raw.title),
6894
- headSha: text(raw.headRefOid),
6895
- headRef: text(raw.headRefName),
6896
- baseRef: text(raw.baseRefName),
6897
+ title: text$1(raw.title),
6898
+ body: raw.body,
6899
+ headSha: text$1(raw.headRefOid),
6900
+ headRef: text$1(raw.headRefName),
6901
+ baseRef: text$1(raw.baseRefName),
6897
6902
  filesUrl: url ? `${url}/files` : "",
6898
6903
  fileUrlTemplate: fileUrlTemplate(url),
6899
6904
  author: author ? {
6900
- name: text(author.name),
6901
- handle: text(author.login)
6905
+ name: text$1(author.name),
6906
+ handle: text$1(author.login)
6902
6907
  } : null
6903
6908
  };
6904
6909
  }
6905
6910
  /**
6906
6911
  * The watcher's query: where the branch is, and whether the request is still open.
6907
6912
  *
6908
- * Deliberately not `PR_FIELDS`. That list exists because a review needs all nine of them, and its
6913
+ * Deliberately not `PR_FIELDS`. That list exists because a review needs all ten of them, and its
6909
6914
  * docblock warns that trimming it fails silently rather than loudly. Sharing it here would put a
6910
- * nine-field query on a five-minute timer across the whole working set, and would put the watcher
6915
+ * ten-field query on a five-minute timer across the whole working set, and would put the watcher
6911
6916
  * in a position to "optimise" a list that must never shrink.
6912
6917
  *
6913
6918
  * `state` rides on the same call rather than being a second one, which keeps the watcher's rate
@@ -6930,7 +6935,7 @@ var STATES = /* @__PURE__ */ new Map([
6930
6935
  * about a request GitHub declined to describe, once per cycle, indefinitely.
6931
6936
  */
6932
6937
  function readHeadSha(raw) {
6933
- const sha = text(raw?.headRefOid);
6938
+ const sha = text$1(raw?.headRefOid);
6934
6939
  if (!sha) return {
6935
6940
  kind: "failed",
6936
6941
  detail: "GitHub reported no head commit for this request."
@@ -6938,7 +6943,7 @@ function readHeadSha(raw) {
6938
6943
  return {
6939
6944
  kind: "head",
6940
6945
  sha,
6941
- state: STATES.get(text(raw?.state)) ?? null
6946
+ state: STATES.get(text$1(raw?.state)) ?? null
6942
6947
  };
6943
6948
  }
6944
6949
  async function headOf(repo, number, env, cwd) {
@@ -6973,8 +6978,8 @@ async function resolve$1(repo, reference, env, cwd) {
6973
6978
  } catch (error) {
6974
6979
  throw new TargetError(`${reference} could not be looked up on GitHub: ${error.message}`);
6975
6980
  }
6976
- const headRef = text(raw.headRefName);
6977
- const headSha = text(raw.headRefOid);
6981
+ const headRef = text$1(raw.headRefName);
6982
+ const headSha = text$1(raw.headRefOid);
6978
6983
  if (!headRef || !headSha) throw new TargetError(`${reference} was found on GitHub but reported no branch and commit.`);
6979
6984
  const resolved = {
6980
6985
  reference,
@@ -6982,12 +6987,12 @@ async function resolve$1(repo, reference, env, cwd) {
6982
6987
  headSha,
6983
6988
  provider: GITHUB_FORGE_ID
6984
6989
  };
6985
- const baseRef = text(raw.baseRefName);
6990
+ const baseRef = text$1(raw.baseRefName);
6986
6991
  if (baseRef) resolved.baseRef = baseRef;
6987
- const baseSha = text(raw.baseRefOid);
6992
+ const baseSha = text$1(raw.baseRefOid);
6988
6993
  if (baseSha) resolved.baseSha = baseSha;
6989
6994
  if (typeof raw.changedFiles === "number") resolved.changedFiles = raw.changedFiles;
6990
- const url = text(raw.url);
6995
+ const url = text$1(raw.url);
6991
6996
  if (url) resolved.url = url;
6992
6997
  if (typeof raw.number === "number") resolved.number = raw.number;
6993
6998
  return resolved;
@@ -9081,7 +9086,7 @@ Follow-up Prompts:
9081
9086
  ${buildFollowUpPromptsSpec()}
9082
9087
 
9083
9088
  PR Title: ${raw.pullRequest.title}
9084
- ${raw.pullRequest.body ? `\nPR Description:\n${raw.pullRequest.body}\n` : ""}
9089
+ ${raw.pullRequest.body ? `\n${raw.pullRequest.bodySource === "commit-subjects" ? "Commit subjects (there is no PR Description)" : "PR Description"}:\n${raw.pullRequest.body}\n` : ""}
9085
9090
  ${diff}`;
9086
9091
  const overheadChars = assemble({
9087
9092
  diff: "",
@@ -9390,10 +9395,304 @@ function normalizeSections(value) {
9390
9395
  return sections;
9391
9396
  }
9392
9397
  //#endregion
9398
+ //#region ../../packages/review-harness/src/domain/ai_review/author_description.ts
9399
+ /**
9400
+ * Reading a pull request's description for whether it says anything, without ever changing it.
9401
+ *
9402
+ * Nothing here returns a copy to store or show. The description is shown verbatim, so a cleaned
9403
+ * copy is only ever a means of asking a question of it: whether it says anything, and whether a
9404
+ * quote is something the author said.
9405
+ */
9406
+ /**
9407
+ * Lines that are a pull request template's scaffolding when they stand alone.
9408
+ *
9409
+ * Matched against a whole line only, after its heading marker, bold and trailing colon are gone.
9410
+ * A heading that says something of its own, `## Fix startup crashes`, is not a label and survives.
9411
+ */
9412
+ var TEMPLATE_LABELS = /* @__PURE__ */ new Set([
9413
+ "description",
9414
+ "summary",
9415
+ "changes",
9416
+ "what",
9417
+ "why",
9418
+ "how",
9419
+ "context",
9420
+ "motivation",
9421
+ "motivation and context",
9422
+ "what changed",
9423
+ "what does this pr do",
9424
+ "why is this needed",
9425
+ "type of change",
9426
+ "testing",
9427
+ "tests",
9428
+ "test plan",
9429
+ "how to test",
9430
+ "how has this been tested",
9431
+ "screenshots",
9432
+ "notes",
9433
+ "checklist",
9434
+ "related issues",
9435
+ "related issue",
9436
+ "links"
9437
+ ]);
9438
+ var HTML_COMMENT = /<!--[\s\S]*?-->/g;
9439
+ var HEADING_MARKER = /^#{1,6}\s*/;
9440
+ var UNCHECKED_TASK = /^[-*+]\s+\[ \]/;
9441
+ var EMPTY_LIST_ITEM = /^(?:[-*+]|\d+[.)])\s*$/;
9442
+ function asLabel(line) {
9443
+ return line.replace(HEADING_MARKER, "").replace(/^\*\*(.*)\*\*$/, "$1").replace(/^__(.*)__$/, "$1").replace(/:\s*$/, "").trim().toLowerCase();
9444
+ }
9445
+ /** One line, reduced to whatever in it is the author's own words. Empty when none are. */
9446
+ function substance(line) {
9447
+ const trimmed = line.trim();
9448
+ if (!trimmed || UNCHECKED_TASK.test(trimmed) || EMPTY_LIST_ITEM.test(trimmed)) return "";
9449
+ if (TEMPLATE_LABELS.has(asLabel(trimmed))) return "";
9450
+ return trimmed.replace(HEADING_MARKER, "");
9451
+ }
9452
+ /**
9453
+ * The parts of a description the author wrote, one line each, with the template's scaffolding gone.
9454
+ *
9455
+ * Only HTML comments, unticked checklist items, empty list items and bare template labels are
9456
+ * discounted. A ticked item is the author asserting something, so it stays.
9457
+ */
9458
+ function authorText(description) {
9459
+ if (!description) return "";
9460
+ return description.replace(HTML_COMMENT, "").split("\n").map(substance).filter((line) => line !== "").join("\n");
9461
+ }
9462
+ /**
9463
+ * Whether a description has anything in it beyond an unfilled template.
9464
+ *
9465
+ * Conservative by design: any line that survives counts, so a sparse description is shown rather
9466
+ * than hidden.
9467
+ */
9468
+ function hasAuthorContent(description) {
9469
+ return authorText(description) !== "";
9470
+ }
9471
+ function isLongDescription(description) {
9472
+ const trimmed = description.trim();
9473
+ return trimmed.split("\n").length > 10 || trimmed.length > 700;
9474
+ }
9475
+ //#endregion
9476
+ //#region ../../packages/review-harness/src/domain/ai_review/motivation.ts
9477
+ var MOTIVATION_STATUSES = [
9478
+ "stated",
9479
+ "inferred",
9480
+ "unclear"
9481
+ ];
9482
+ var MOTIVATION_EVIDENCE_KINDS = [
9483
+ "test",
9484
+ "behaviour",
9485
+ "issue",
9486
+ "commit",
9487
+ "docs",
9488
+ "code"
9489
+ ];
9490
+ /** Ours, not the model's: what an unsupported claim is replaced with when a question remains. */
9491
+ var UNESTABLISHED_MOTIVATION = "The reason for this change isn't established by the description or the code.";
9492
+ /** Stable, so "Mark answered" on it survives a reload like any other question's. */
9493
+ var INTENT_QUESTION_ID = "intent-motivation";
9494
+ /**
9495
+ * Fewer words than this cannot attribute a reason to anyone: "the cache" is in most descriptions
9496
+ * that mention a cache. An initial setting, not a measured one.
9497
+ */
9498
+ var MIN_QUOTE_WORDS = 3;
9499
+ function text(value) {
9500
+ if (typeof value !== "string") return void 0;
9501
+ return value.trim() || void 0;
9502
+ }
9503
+ function normalizeEvidence(value) {
9504
+ if (!Array.isArray(value)) return [];
9505
+ return value.flatMap((raw) => {
9506
+ const item = raw ?? {};
9507
+ const kind = item.kind;
9508
+ const detail = text(item.detail);
9509
+ if (!MOTIVATION_EVIDENCE_KINDS.includes(kind) || !detail) return [];
9510
+ const file = text(item.file);
9511
+ return [{
9512
+ kind,
9513
+ detail,
9514
+ ...file ? { file } : {}
9515
+ }];
9516
+ });
9517
+ }
9518
+ /** The model's `motivation`, if it is one at all. Absent, or the wrong shape, is no motivation. */
9519
+ function normalizeMotivation(value) {
9520
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
9521
+ const item = value;
9522
+ const status = item.status;
9523
+ const summary = text(item.summary);
9524
+ if (!MOTIVATION_STATUSES.includes(status) || !summary) return void 0;
9525
+ const sourceQuote = text(item.sourceQuote);
9526
+ const authorQuestion = text(item.authorQuestion);
9527
+ return {
9528
+ status,
9529
+ summary,
9530
+ evidence: normalizeEvidence(item.evidence),
9531
+ ...sourceQuote ? { sourceQuote } : {},
9532
+ ...authorQuestion ? { authorQuestion } : {}
9533
+ };
9534
+ }
9535
+ /** Whitespace is the one thing a faithful quote may differ in: line wrapping is not wording. */
9536
+ function collapse(value) {
9537
+ return value.replace(/\s+/g, " ").trim();
9538
+ }
9539
+ /**
9540
+ * Whether the author wrote this, in these words, in the description the analysis read.
9541
+ *
9542
+ * Searched in the author's own text only. A template's instructions are in the description too, and
9543
+ * "Explain why this change is needed" would otherwise pass as the author explaining why.
9544
+ */
9545
+ function isAuthorQuote(quote, description) {
9546
+ if (!quote) return false;
9547
+ const needle = collapse(quote);
9548
+ if (needle.split(" ").length < MIN_QUOTE_WORDS) return false;
9549
+ return collapse(authorText(description)).includes(needle);
9550
+ }
9551
+ /**
9552
+ * The checks, applied to one candidate.
9553
+ *
9554
+ * Each status has one thing it cannot stand without: a verified quote, evidence, a question. The
9555
+ * fields that belong to the other statuses are dropped rather than carried, so a `stated` motive
9556
+ * cannot also show a guess and an `unclear` one cannot also show a quote.
9557
+ *
9558
+ * A claim that fails becomes unclear only if the model also asked the question that would settle
9559
+ * it, and then in our words rather than its own. Otherwise there is nothing to show that the
9560
+ * reviewer could act on, and nothing is shown.
9561
+ */
9562
+ function verifyMotivation(candidate, authorDescription) {
9563
+ if (!candidate) return {};
9564
+ const base = {
9565
+ contractVersion: 1,
9566
+ summary: candidate.summary
9567
+ };
9568
+ if (candidate.status === "stated" && isAuthorQuote(candidate.sourceQuote, authorDescription)) return { motivation: {
9569
+ ...base,
9570
+ status: "stated",
9571
+ sourceQuote: candidate.sourceQuote
9572
+ } };
9573
+ if (candidate.status === "inferred" && candidate.evidence.length > 0) return { motivation: {
9574
+ ...base,
9575
+ status: "inferred",
9576
+ evidence: candidate.evidence
9577
+ } };
9578
+ if (candidate.status === "unclear" && candidate.authorQuestion) return { motivation: {
9579
+ ...base,
9580
+ status: "unclear",
9581
+ authorQuestion: candidate.authorQuestion
9582
+ } };
9583
+ const reason = candidate.status === "stated" ? candidate.sourceQuote ? "unverified-quote" : "missing-quote" : candidate.status === "inferred" ? "no-evidence" : "no-question";
9584
+ const correction = {
9585
+ status: candidate.status,
9586
+ summary: candidate.summary,
9587
+ reason
9588
+ };
9589
+ if (candidate.status !== "unclear" && candidate.authorQuestion) return {
9590
+ motivation: {
9591
+ contractVersion: 1,
9592
+ status: "unclear",
9593
+ summary: UNESTABLISHED_MOTIVATION,
9594
+ authorQuestion: candidate.authorQuestion
9595
+ },
9596
+ correction: {
9597
+ ...correction,
9598
+ outcome: "downgraded"
9599
+ }
9600
+ };
9601
+ return { correction: {
9602
+ ...correction,
9603
+ outcome: "withheld"
9604
+ } };
9605
+ }
9606
+ /**
9607
+ * The one evidence-link policy, for both hosts.
9608
+ *
9609
+ * A changed file is a diff link and a real unchanged file a source link. A path the host can show
9610
+ * does not exist is removed and returned for the audit. A path the host cannot check keeps its
9611
+ * text and gets no state, so it is never presented as a link the reader can trust.
9612
+ */
9613
+ function placeEvidenceFiles(motivation, paths) {
9614
+ if (!motivation?.evidence) return {
9615
+ motivation,
9616
+ removed: []
9617
+ };
9618
+ const removed = [];
9619
+ const evidence = motivation.evidence.map(({ fileState: _ignored, ...item }) => {
9620
+ if (!item.file) return item;
9621
+ if (paths.changed(item.file)) return {
9622
+ ...item,
9623
+ fileState: "changed"
9624
+ };
9625
+ if (!paths.exists) return item;
9626
+ if (paths.exists(item.file)) return {
9627
+ ...item,
9628
+ fileState: "unchanged"
9629
+ };
9630
+ removed.push(item.file);
9631
+ const { file: _gone, ...rest } = item;
9632
+ return rest;
9633
+ });
9634
+ return {
9635
+ motivation: {
9636
+ ...motivation,
9637
+ evidence
9638
+ },
9639
+ removed
9640
+ };
9641
+ }
9642
+ /** Two questions are the same question when they differ only in case, spacing or end punctuation. */
9643
+ function sameQuestion(a, b) {
9644
+ const key = (value) => collapse(value).toLowerCase().replace(/[?.!\s]+$/, "");
9645
+ return key(a) === key(b);
9646
+ }
9647
+ /**
9648
+ * File an unclear motive's question with the rest, as the one Intent question.
9649
+ *
9650
+ * A question the model already asked in the same words is tagged rather than repeated, so the
9651
+ * reader meets it once, in the Intent group. Telling two differently worded questions apart is
9652
+ * judgement and is left to the prompt.
9653
+ */
9654
+ function withIntentQuestion(questions, motivation) {
9655
+ if (motivation?.status !== "unclear" || !motivation.authorQuestion) return questions;
9656
+ const asked = motivation.authorQuestion;
9657
+ const existing = questions.findIndex((q) => sameQuestion(q.question, asked));
9658
+ if (existing >= 0) return questions.map((q, index) => index === existing ? {
9659
+ ...q,
9660
+ category: "intent"
9661
+ } : 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
+ return [...questions, {
9666
+ id,
9667
+ question: asked,
9668
+ category: "intent"
9669
+ }];
9670
+ }
9671
+ /** The candidate checked and, when it ends unclear, its question filed. */
9672
+ function applyMotivation(result, candidate, authorDescription) {
9673
+ const { motivation, correction } = verifyMotivation(candidate, authorDescription);
9674
+ const { motivation: _unverified, ...rest } = result;
9675
+ return {
9676
+ result: {
9677
+ ...rest,
9678
+ ...motivation ? { motivation } : {},
9679
+ questions: withIntentQuestion(result.questions, motivation)
9680
+ },
9681
+ ...correction ? { correction } : {}
9682
+ };
9683
+ }
9684
+ //#endregion
9393
9685
  //#region ../../packages/review-harness/src/domain/ai_review/pipeline.ts
9394
9686
  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:
9395
9687
  {
9396
9688
  "summary": string,
9689
+ "motivation"?: {
9690
+ "status": "stated" | "inferred" | "unclear",
9691
+ "summary": string,
9692
+ "sourceQuote"?: string,
9693
+ "evidence"?: [{ "kind": "test" | "behaviour" | "issue" | "commit" | "docs" | "code", "detail": string, "file"?: string }],
9694
+ "authorQuestion"?: string
9695
+ },
9397
9696
  "overallRisk": "low" | "medium" | "high",
9398
9697
  "sections": [{
9399
9698
  "id": string,
@@ -9416,6 +9715,11 @@ var STRUCTURED_OUTPUT_INSTRUCTIONS = `In addition to the review principles above
9416
9715
  }
9417
9716
 
9418
9717
  Field guidance:
9718
+ - "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.
9720
+ - "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
+ - "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
+ 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.
9419
9723
  - "sections": partition the pull request into logical implementation sections (e.g. "Authentication", "Database Migration", "API Changes"), grouped by responsibility/purpose, not by directory or file type. Identify a sensible number of sections — most PRs will naturally have between 2 and 8, but do not invent or split sections simply to reach a target; let the actual structure of the change decide the count. For each section: "oneLineSummary" is a single sentence; "purpose" explains why this part of the change exists; "whyReview" is a short note aimed at the reviewer about what to focus on; "checklist" is a short list of things to verify (review prompts, not findings — never report bugs, code smells, or recommendations inside a section, that's what "issues" is for). Assign "order" starting at 1 in the sequence a reviewer should walk through the sections (narrative order, not file count or alphabetical). "reviewWeight" is a relative time-budget indicator from 1 (skim) to 5 (spend the most time here), independent of "priority" and "reviewAttention". Set "recommendedStartingPoint": true on exactly one section — whichever one a reviewer should actually read first for the rest of the review to make sense. This is not necessarily the section with "order": 1 (e.g. a foundational database migration might naturally come first while a reviewer should still start with the authentication logic it enables). Set it to false on every other section. Every file in the diff must appear in exactly one section — the sections are the only file list the developer sees, so an omitted file is a file they never review. Files with no natural home (lockfiles, dependency bumps, generated output, formatting-only changes) go in a single final catch-all section — title it for what it contains, e.g. "Supporting changes" or "Dependency and config updates" — with "priority": "supporting" and "reviewAttention": "low". Never drop a file just because it is uninteresting; say that it is uninteresting instead.
9420
9724
  - "files" inside a section: "path" is the file path exactly as it appears in the diff. "focus" is REQUIRED on every file — if a file is worth listing in a section, it is worth explaining why the reviewer has been brought to it. Keep it to one or two short sentences, roughly 20-35 words, never more than two sentences. Make it actionable rather than descriptive: name the role this specific file plays in the section, then say what the reviewer should verify there. For example: "Introduces the new association reader. Verify it preserves the existing lookup behaviour." / "Implements the retry path. Confirm error handling matches the existing gateway implementation." / "Registers the generated behaviour with Active Record. Check the hook fires for subclasses too." Do not restate the section's summary or checklist, do not use bullet points, and do not write filler such as "Review this file carefully."
9421
9725
  - "file" on an issue or question: prefer anchoring a finding to the changed file responsible for the concern, named exactly as it appears in the diff. If an unchanged repository file is necessary context and no changed-file location accurately represents the finding, it may be referenced as an unchanged related file. Never list an unchanged file in a section's "files" array, and never describe an unchanged file as part of the pull request.
@@ -9426,7 +9730,7 @@ Field guidance:
9426
9730
  - "existingDiscussion" on an issue: set this only when a thread in the PR DISCUSSION CONTEXT block already raises this same concern, and you have independently confirmed from the diff that it still applies. "threadId" must be copied verbatim from that thread's [T_...] marker — never invent an id, never guess, and omit the field entirely if no thread matches. Set "relation" to "same" when the thread already makes this exact point, or "related" when it discusses this area without covering the finding — a reviewer acts differently on "someone said this already" than on "someone was looking near here". Still write the issue normally; this field only records that the point was raised before, so the reviewer is not shown it as a fresh discovery. Do not use it to pad the review with issues you would otherwise have left out.
9427
9731
  - "confidence" on an issue: how sure you are the issue is real, on the same high/medium/low scale as a question's "importance". This is a separate axis from "severity", which is how much it would matter if real — a critical issue you are unsure about and a nit you are certain of are different things to act on, so do not let one field stand in for the other. Use LOW when you are reasoning from a diff excerpt without the surrounding code, and say what would confirm it in the description. Omit the field rather than guessing a middle value.
9428
9732
  - "questions": each one is a genuine open item the reviewer needs answered before they can sign off, not a rhetorical prompt. "importance" is how much attention it deserves, on the same high/medium/low scale as a section's "reviewAttention" (e.g. a question about a possibly-exposed secret is "high"; a naming preference is "low"). "whyThisMatters" is 1-2 sentences explaining the concrete consequence of the question going unanswered — not a restatement of the question itself.
9429
- - Every section's "oneLineSummary"/"purpose"/"whyReview", each file's "focus", and each question's "whyThisMatters" are rendered as plain prose paragraphs, not markdown — write them as normal sentences. Do not use "- " bullet points or numbered lists inside these fields; if you're enumerating several points, join them into flowing sentences instead. Inline code/identifiers may use backtick spans (e.g. \`ErrorSeverity\`) — those do render.
9733
+ - Every section's "oneLineSummary"/"purpose"/"whyReview", each file's "focus", each question's "whyThisMatters", and every text field inside "motivation" are rendered as plain prose paragraphs, not markdown — write them as normal sentences. Do not use "- " bullet points or numbered lists inside these fields; if you're enumerating several points, join them into flowing sentences instead. Inline code/identifiers may use backtick spans (e.g. \`ErrorSeverity\`) — those do render.
9430
9734
  - "summary", "recommendation" and each "nextSteps" entry are rendered as Markdown, so use it where it carries meaning: backtick spans for identifiers, a fenced block for a snippet worth reading as code, a short bullet list where the content genuinely is a list. Prose is still the default and these fields are still short. Do not add headings, and do not turn a paragraph into bullets to look organised.
9431
9735
 
9432
9736
  This JSON object is the only output the developer sees. The free-text sections above (Overview, Guide, Issues, Questions, Follow-up Prompts) are for your own internal reasoning — do not copy them verbatim into any JSON field.`;
@@ -9444,7 +9748,22 @@ function buildStructuredReviewPrompt(pr, options = {}) {
9444
9748
  partialFiles: partialFiles ?? []
9445
9749
  };
9446
9750
  }
9447
- function parseReviewResult(raw) {
9751
+ /**
9752
+ * A reply, parsed, with the motivation checked against the description the analysis read.
9753
+ *
9754
+ * The check happens here rather than after, so there is no parsed result anywhere whose motivation
9755
+ * has not been through it. `correction` says what the check refused, for a caller with an audit to
9756
+ * put it in.
9757
+ */
9758
+ function parseReviewResponse(raw, authorDescription) {
9759
+ const { result, motivation } = parseUnchecked(raw);
9760
+ const applied = applyMotivation(result, motivation, authorDescription);
9761
+ return {
9762
+ result: applied.result,
9763
+ ...applied.correction ? { motivationCorrection: applied.correction } : {}
9764
+ };
9765
+ }
9766
+ function parseUnchecked(raw) {
9448
9767
  const jsonText = extractJsonText(raw);
9449
9768
  let parsed;
9450
9769
  try {
@@ -9458,14 +9777,17 @@ function parseReviewResult(raw) {
9458
9777
  const recommendation = asString(obj.recommendation);
9459
9778
  if (!summary && !recommendation && !Array.isArray(obj.issues) && !Array.isArray(obj.nextSteps)) throw new Error("The AI returned valid JSON, but not in the expected analysis shape (missing summary/recommendation/issues/nextSteps). This usually means the model ignored the requested schema.");
9460
9779
  return {
9461
- schemaVersion: 1,
9462
- summary,
9463
- overallRisk: RISKS.includes(obj.overallRisk) ? obj.overallRisk : "medium",
9464
- sections: normalizeSections(obj.sections),
9465
- issues: normalizeIssues(obj.issues),
9466
- questions: normalizeQuestions(obj.questions),
9467
- recommendation,
9468
- nextSteps: asStringArray(obj.nextSteps)
9780
+ result: {
9781
+ schemaVersion: 1,
9782
+ summary,
9783
+ overallRisk: RISKS.includes(obj.overallRisk) ? obj.overallRisk : "medium",
9784
+ sections: normalizeSections(obj.sections),
9785
+ issues: normalizeIssues(obj.issues),
9786
+ questions: normalizeQuestions(obj.questions),
9787
+ recommendation,
9788
+ nextSteps: asStringArray(obj.nextSteps)
9789
+ },
9790
+ motivation: normalizeMotivation(obj.motivation)
9469
9791
  };
9470
9792
  }
9471
9793
  var EXPLAIN_SIMPLY_PROMPT = "Explain this review in plain English for someone unfamiliar with this codebase. Avoid jargon, use a simple everyday analogy if it helps, and keep it skippable — a couple of short paragraphs is enough.";
@@ -9836,6 +10158,7 @@ async function assemble(changeSet, budget, lens, prContext) {
9836
10158
  ...changeSet.data,
9837
10159
  files: selection.inline,
9838
10160
  changedFileCount: allFiles.length,
10161
+ ...prContext ? { pullRequest: pullRequestFor(changeSet.data.pullRequest, prContext) } : {},
9839
10162
  discussion: prContext?.discussion
9840
10163
  }, selection.inline.length, null);
9841
10164
  const shared = {
@@ -9865,6 +10188,21 @@ async function assemble(changeSet, budget, lens, prContext) {
9865
10188
  };
9866
10189
  }
9867
10190
  /**
10191
+ * The change's pull request, as the prompt should present it once a forge request is attached.
10192
+ *
10193
+ * With no description, the commit subjects stay: they are still the best account of the change
10194
+ * there is, and still labelled as commits rather than as the author's words.
10195
+ */
10196
+ function pullRequestFor(local, prContext) {
10197
+ const body = prContext.pullRequest.body;
10198
+ if (!body) return local;
10199
+ const { bodySource: _commits, ...rest } = local;
10200
+ return {
10201
+ ...rest,
10202
+ body
10203
+ };
10204
+ }
10205
+ /**
9868
10206
  * The inline patches as one blob, for grounding the assistant.
9869
10207
  *
9870
10208
  * No ceiling of its own. `selectInlineDiffs` already bounded this, and a second constant guarding
@@ -10199,13 +10537,14 @@ async function existingPaths(repoPath, paths, commit = "HEAD") {
10199
10537
  * from the change set, and it costs one git call, or none when the guide cites nothing off-diff.
10200
10538
  */
10201
10539
  async function validateGuide(rawResponse, context) {
10202
- const result = parseReviewResult(rawResponse);
10540
+ const { result, motivationCorrection } = parseReviewResponse(rawResponse, context.prContext?.pullRequest.body);
10203
10541
  const changedPaths = new Set(context.changeSet.data.files.map((file) => file.name));
10204
10542
  const sectionIds = new Set(result.sections.map((section) => section.id));
10205
10543
  const findings = [...result.issues, ...result.questions];
10206
10544
  const offDiff = /* @__PURE__ */ new Set();
10207
10545
  for (const section of result.sections) for (const file of section.files) if (!changedPaths.has(file.path)) offDiff.add(file.path);
10208
10546
  for (const finding of findings) if (finding.file && !changedPaths.has(finding.file)) offDiff.add(finding.file);
10547
+ for (const evidence of result.motivation?.evidence ?? []) if (evidence.file && !changedPaths.has(evidence.file)) offDiff.add(evidence.file);
10209
10548
  const realButUnchanged = await existingPaths(context.changeSet.repoPath, [...offDiff]);
10210
10549
  const unknownPaths = /* @__PURE__ */ new Set();
10211
10550
  const sectioned = /* @__PURE__ */ new Set();
@@ -10246,6 +10585,13 @@ async function validateGuide(rawResponse, context) {
10246
10585
  orphanedSectionRefs += 1;
10247
10586
  }
10248
10587
  }
10588
+ const placed = placeEvidenceFiles(result.motivation, {
10589
+ changed: (path) => changedPaths.has(path),
10590
+ exists: (path) => realButUnchanged.has(path)
10591
+ });
10592
+ if (placed.motivation) result.motivation = placed.motivation;
10593
+ for (const path of placed.removed) unknownPaths.add(path);
10594
+ for (const evidence of placed.motivation?.evidence ?? []) if (evidence.fileState === "unchanged" && evidence.file) noteReferenced(evidence.file);
10249
10595
  const attribution = attributeIssuesToDiscussion(result, context.processed.discussion);
10250
10596
  return {
10251
10597
  result: attribution.result,
@@ -10253,7 +10599,8 @@ async function validateGuide(rawResponse, context) {
10253
10599
  unknownPaths: [...unknownPaths].sort(),
10254
10600
  relocatedFindings,
10255
10601
  orphanedSectionRefs,
10256
- rejectedAttributions: attribution.rejected
10602
+ rejectedAttributions: attribution.rejected,
10603
+ ...motivationCorrection ? { motivationCorrection } : {}
10257
10604
  },
10258
10605
  referencedFiles: [...referenced].map(([path, sections]) => ({
10259
10606
  path,
@@ -12317,6 +12664,7 @@ function movable(fetched) {
12317
12664
  };
12318
12665
  copy("headSha");
12319
12666
  copy("title");
12667
+ copy("body");
12320
12668
  copy("filesUrl");
12321
12669
  copy("fileUrlTemplate");
12322
12670
  copy("author");
@@ -12526,6 +12874,7 @@ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk
12526
12874
  const result = {
12527
12875
  schemaVersion: 1,
12528
12876
  summary: parsed.summary || previous.summary,
12877
+ ...previous.motivation ? { motivation: previous.motivation } : {},
12529
12878
  overallRisk: parsed.overallRisk ?? previous.overallRisk,
12530
12879
  recommendation: parsed.recommendation || previous.recommendation,
12531
12880
  nextSteps: parsed.nextSteps.length ? parsed.nextSteps : previous.nextSteps,
@@ -13084,4 +13433,4 @@ function lineageIds(id) {
13084
13433
  return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
13085
13434
  }
13086
13435
  //#endregion
13087
- export { compactQueue as $, agentById as $n, latestKissRun as $t, createPrState as A, isIssueOutstanding as An, followedBranch as At, toneOf as B, resolveTarget as Bn, phasesFor as Bt, readEvents as C, sectionOf as Cn, claimRequestUrl as Ct, announceJob as D, queueStamp as Dn, fillFileUrlTemplate as Dt, telemetryUploadConsent as E, queueRemove as En, remoteHost as Et, markAllRead as F, processDiscussion as Fn, isTerminal as Ft, extractJsonText as G, resolveModel as Gn, isClaimed as Gt, withUsageRecorded as H, isCommitReview as Hn, purposeOf as Ht, markRead as I, recommendationsOf as In, loadJob as It, readMarker as J, writeAgentPreference as Jn, groupByLineage as Jt, ReviewBeingDeletedError as K, clearAgentPreference as Kn, decideKissRecommendation as Kt, pruneNotifications as L, describeAuthorship as Ln, saveJob as Lt, eventName as M, issuesOutstanding as Mn, allJobs as Mt, headline as N, questionsOutstanding as Nn, endedAt as Nt, branchOf as O, readQueue as On, checkCodeFreshness as Ot, listNotifications as P, reviewerDispositions as Pn, fail as Pt, activateQueued as Q, DEFAULT_AGENT_ID as Qn, kissDecisionsOf as Qt, readNotification as R, WITHHELD_REASON as Rn, PHASES as Rt, startJob as S, parseLifecycle as Sn, writePortPreference as St, recordTelemetryUploadConsent as T, queueAddMany as Tn, forgeFor as Tt, EXPLAIN_SIMPLY_PROMPT as U, git as Un, clearClaim as Ut, unreadCount as V, displayRef as Vn, progressSteps as Vt, asString as W, resolveAgent as Wn, driverGone as Wt, modelHelpLines as X, STORE_ROOT as Xn, indexReviewGroups as Xt, removeWorktree as Y, CONFIG_PATH as Yn, indexKeptIds as Yt, relativeTime as Z, AGENT_IDS as Zn, kissDecisionRefusal as Zt, agentEnvOf as _, isLifecycle as _n, MAX_WORKSPACE_PORT as _t, wasBlocked as a, positionInLineage as an, ensureServer as at, reanalyseReview as b, lifecyclePill as bn, resolveWorkspacePort as bt, RefreshUnavailableError as c, recentWorkspaces as cn, serverEntryExists as ct, refreshPrContext as d, summarise as dn, workspaceUrl as dt, lineageKeyFor as en, agentFor as er, queueForReview as et, isUnchanged as f, touchWorkspace as fn, writeServerRecord as ft, runningJobs as g, activityLabel as gn, DEFAULT_WORKSPACE_PORT as gt, liveUpdateFor as h, LIFECYCLES as hn, feedbackUrl as ht, liveJobsFor as i, mutateWorkspace as in, clearServerRecord as it, createPrUpdate as j, isQuestionOutstanding as jn, reviewedShaOf as jt, subjectOf as k, doneVerb as kn, checkReview as kt, mergeUpdateInto as l, reviewedCommit as ln, spawnServer as lt, UpdateAlreadyRunningError as m, workspaceStamps as mn, PACKAGE_NAME as mt, deleteReview as n, loadWorkspace as nn, AgentCancelledError as nr, readIndexToken as nt, discardJob as o, presentedKissRunOf as on, readServerRecord as ot, askCheckout as p, workspaceRevision as pn, BUILD_VERSION as pt, MANAGED_ROOT as q, readAgentPreference as qn, followedRefName as qt, lineageIds as r, matchingWorkspaceIds as rn, AgentUnavailableError as rr, bootstrapUrl as rt, FETCH_TIMEOUT_MS as s, previousKissResult as sn, reviewsUrl as st, deleteLineage as t, lineagePosition as tn, detectAgents as tr, settleQueue as tt, updateReview as u, reviewedRepositories as un, stopServer as ut, recordKissRun as v, lifecycleAction as vn, MIN_WORKSPACE_PORT as vt, record as w, queueAdd as wn, forgeResolver as wt, runJob as x, offersLifecycle as xn, statedWorkspacePort as xt, startKissJob as y, lifecycleChip as yn, parseWorkspacePort as yt, removeNotification as z, kissOutcome as zn, isLiveJob as zt };
13436
+ export { relativeTime as $, AGENT_IDS as $n, kissDecisionRefusal as $t, createPrState as A, readQueue as An, checkCodeFreshness as At, toneOf as B, WITHHELD_REASON as Bn, PHASES as Bt, readEvents as C, offersLifecycle as Cn, statedWorkspacePort as Ct, announceJob as D, queueAddMany as Dn, forgeFor as Dt, telemetryUploadConsent as E, queueAdd as En, forgeResolver as Et, markAllRead as F, questionsOutstanding as Fn, endedAt as Ft, isLongDescription as G, git as Gn, clearClaim as Gt, withUsageRecorded as H, resolveTarget as Hn, phasesFor as Ht, markRead as I, reviewerDispositions as In, fail as It, ReviewBeingDeletedError as J, clearAgentPreference as Jn, decideKissRecommendation as Jt, asString as K, resolveAgent as Kn, driverGone as Kt, pruneNotifications as L, processDiscussion as Ln, isTerminal as Lt, eventName as M, isIssueOutstanding as Mn, followedBranch as Mt, headline as N, isQuestionOutstanding as Nn, reviewedShaOf as Nt, branchOf as O, queueRemove as On, remoteHost as Ot, listNotifications as P, issuesOutstanding as Pn, allJobs as Pt, modelHelpLines as Q, STORE_ROOT as Qn, indexReviewGroups as Qt, readNotification as R, recommendationsOf as Rn, loadJob as Rt, startJob as S, lifecyclePill as Sn, resolveWorkspacePort as St, recordTelemetryUploadConsent as T, sectionOf as Tn, claimRequestUrl as Tt, EXPLAIN_SIMPLY_PROMPT as U, displayRef as Un, progressSteps as Ut, unreadCount as V, kissOutcome as Vn, isLiveJob as Vt, hasAuthorContent as W, isCommitReview as Wn, purposeOf as Wt, readMarker as X, writeAgentPreference as Xn, groupByLineage as Xt, MANAGED_ROOT as Y, readAgentPreference as Yn, followedRefName as Yt, removeWorktree as Z, CONFIG_PATH as Zn, indexKeptIds as Zt, agentEnvOf as _, LIFECYCLES as _n, feedbackUrl as _t, wasBlocked as a, matchingWorkspaceIds as an, AgentUnavailableError as ar, bootstrapUrl as at, reanalyseReview as b, lifecycleAction as bn, MIN_WORKSPACE_PORT as bt, RefreshUnavailableError as c, presentedKissRunOf as cn, readServerRecord as ct, refreshPrContext as d, reviewedCommit as dn, spawnServer as dt, kissDecisionsOf as en, DEFAULT_AGENT_ID as er, activateQueued as et, isUnchanged as f, reviewedRepositories as fn, stopServer as ft, runningJobs as g, workspaceStamps as gn, PACKAGE_NAME as gt, liveUpdateFor as h, workspaceRevision as hn, BUILD_VERSION as ht, liveJobsFor as i, loadWorkspace as in, AgentCancelledError as ir, readIndexToken as it, createPrUpdate as j, doneVerb as jn, checkReview as jt, subjectOf as k, queueStamp as kn, fillFileUrlTemplate as kt, mergeUpdateInto as l, previousKissResult as ln, reviewsUrl as lt, UpdateAlreadyRunningError as m, touchWorkspace as mn, writeServerRecord as mt, deleteReview as n, lineageKeyFor as nn, agentFor as nr, queueForReview as nt, discardJob as o, mutateWorkspace as on, clearServerRecord as ot, askCheckout as p, summarise as pn, workspaceUrl as pt, extractJsonText as q, resolveModel as qn, isClaimed as qt, lineageIds as r, lineagePosition as rn, detectAgents as rr, settleQueue as rt, FETCH_TIMEOUT_MS as s, positionInLineage as sn, ensureServer as st, deleteLineage as t, latestKissRun as tn, agentById as tr, compactQueue as tt, updateReview as u, recentWorkspaces as un, serverEntryExists as ut, recordKissRun as v, activityLabel as vn, DEFAULT_WORKSPACE_PORT as vt, record as w, parseLifecycle as wn, writePortPreference as wt, runJob as x, lifecycleChip as xn, parseWorkspacePort as xt, startKissJob as y, isLifecycle as yn, MAX_WORKSPACE_PORT as yt, removeNotification as z, describeAuthorship as zn, saveJob as zt };
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $n as agentById, Bn as resolveTarget, E as telemetryUploadConsent, Ft as isTerminal, Gn as resolveModel, Gt as isClaimed, Ht as purposeOf, It as loadJob, J as readMarker, Jn as writeAgentPreference, Jt as groupByLineage, Mt as allJobs, Qn as DEFAULT_AGENT_ID, Rn as WITHHELD_REASON, Rt as PHASES, S as startJob, St as writePortPreference, T as recordTelemetryUploadConsent, Un as git, Ut as clearClaim, Vn as displayRef, Vt as progressSteps, Wn as resolveAgent, Wt as driverGone, X as modelHelpLines, Xn as STORE_ROOT, Y as removeWorktree, Yn as CONFIG_PATH, Z as relativeTime, Zn as AGENT_IDS, _t as MAX_WORKSPACE_PORT, a as wasBlocked, an as positionInLineage, at as ensureServer, b as reanalyseReview, cn as recentWorkspaces, dn as summarise, dt as workspaceUrl, en as lineageKeyFor, et as queueForReview, g as runningJobs, gt as DEFAULT_WORKSPACE_PORT, h as liveUpdateFor, i as liveJobsFor, in as mutateWorkspace, kt as checkReview, l as mergeUpdateInto, m as UpdateAlreadyRunningError, mt as PACKAGE_NAME, n as deleteReview$1, nn as loadWorkspace, nt as readIndexToken, pt as BUILD_VERSION, q as MANAGED_ROOT, qn as readAgentPreference, qt as followedRefName, rn as matchingWorkspaceIds, rt as bootstrapUrl, st as reviewsUrl, tr as detectAgents, u as updateReview, un as reviewedRepositories, ut as stopServer, v as recordKissRun, vt as MIN_WORKSPACE_PORT, w as record$1, wt as forgeResolver, x as runJob, xt as statedWorkspacePort, yt as parseWorkspacePort, zn as kissOutcome } from "./delete_review-DMJXD3bf.js";
2
+ import { $ as relativeTime, $n as AGENT_IDS, Bn as WITHHELD_REASON, Bt as PHASES, Ct as statedWorkspacePort, E as telemetryUploadConsent, Et as forgeResolver, Gn as git, Gt as clearClaim, Hn as resolveTarget, Kn as resolveAgent, Kt as driverGone, Lt as isTerminal, Pt as allJobs, Q as modelHelpLines, Qn as STORE_ROOT, Rt as loadJob, S as startJob, T as recordTelemetryUploadConsent, Un as displayRef, Ut as progressSteps, Vn as kissOutcome, Wt as purposeOf, X as readMarker, Xn as writeAgentPreference, Xt as groupByLineage, Y as MANAGED_ROOT, Yn as readAgentPreference, Yt as followedRefName, Z as removeWorktree, Zn as CONFIG_PATH, a as wasBlocked, an as matchingWorkspaceIds, at as bootstrapUrl, b as reanalyseReview, bt as MIN_WORKSPACE_PORT, er as DEFAULT_AGENT_ID, fn as reviewedRepositories, ft as stopServer, g as runningJobs, gt as PACKAGE_NAME, h as liveUpdateFor, ht as BUILD_VERSION, i as liveJobsFor, in as loadWorkspace, it as readIndexToken, jt as checkReview, l as mergeUpdateInto, lt as reviewsUrl, m as UpdateAlreadyRunningError, n as deleteReview$1, nn as lineageKeyFor, nt as queueForReview, on as mutateWorkspace, pn as summarise, pt as workspaceUrl, qn as resolveModel, qt as isClaimed, rr as detectAgents, sn as positionInLineage, st as ensureServer, tr as agentById, u as updateReview, un as recentWorkspaces, v as recordKissRun, vt as DEFAULT_WORKSPACE_PORT, w as record$1, wt as writePortPreference, x as runJob, xt as parseWorkspacePort, yt as MAX_WORKSPACE_PORT } from "./delete_review-B2bqhgtx.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";