prreviewbuddy 0.91.1 → 0.93.0

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.
@@ -4213,6 +4213,11 @@ var IMPACT_GROUPS = [
4213
4213
  "intent",
4214
4214
  "maintenance"
4215
4215
  ];
4216
+ var STEP_KINDS = [
4217
+ "verify",
4218
+ "decide",
4219
+ "fix"
4220
+ ];
4216
4221
  /**
4217
4222
  * The JSON in a reply, whether or not the model wrapped it in a fence.
4218
4223
  *
@@ -4414,11 +4419,33 @@ function normalizeStep(value) {
4414
4419
  const item = value;
4415
4420
  const text = asString$1(item.text).trim();
4416
4421
  if (!text) return void 0;
4417
- return item.kind === "verify" || item.kind === "decide" ? {
4422
+ return STEP_KINDS.includes(item.kind) ? {
4418
4423
  kind: item.kind,
4419
4424
  text
4420
4425
  } : { text };
4421
4426
  }
4427
+ /**
4428
+ * A question's scan-first framing, or nothing.
4429
+ *
4430
+ * Well-formedness only, the consequence's rule: a framing with no question or no reason has nothing
4431
+ * to say and is dropped, so the card shows the original question rather than an empty headline.
4432
+ */
4433
+ function normalizeQuestionFraming(value) {
4434
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
4435
+ const item = value;
4436
+ const question = asString$1(item.question).trim();
4437
+ const why = asString$1(item.why).trim();
4438
+ if (!question || !why) return void 0;
4439
+ const needed = asString$1(item.needed).trim();
4440
+ return {
4441
+ question,
4442
+ why,
4443
+ ...needed ? { needed } : {}
4444
+ };
4445
+ }
4446
+ function withFraming(framing) {
4447
+ return framing ? { framing } : {};
4448
+ }
4422
4449
  /** Spread form, so an issue without one carries no `consequence` key at all. */
4423
4450
  function withConsequence(consequence) {
4424
4451
  return consequence ? { consequence } : {};
@@ -4449,7 +4476,8 @@ function normalizeQuestions(value) {
4449
4476
  importance: REVIEW_ATTENTIONS.includes(item.importance) ? item.importance : void 0,
4450
4477
  whyThisMatters: typeof item.whyThisMatters === "string" ? item.whyThisMatters : void 0,
4451
4478
  file: typeof item.file === "string" ? item.file : void 0,
4452
- sectionId: asSectionId(item)
4479
+ sectionId: asSectionId(item),
4480
+ ...withFraming(normalizeQuestionFraming(item.framing))
4453
4481
  };
4454
4482
  }).filter((question) => question.question.trim() !== "");
4455
4483
  }
@@ -5739,6 +5767,20 @@ function reviewerDispositions(session) {
5739
5767
  isStale: (issue) => issue.reconciliationStatus === "affected" && (byId.get(issue.id)?.sessionId ?? session.id) !== session.id
5740
5768
  };
5741
5769
  }
5770
+ function framableFindings(session) {
5771
+ const dispositions = reviewerDispositions(session);
5772
+ const answered = new Set(session.answeredQuestionIds ?? []);
5773
+ const verdicts = verdictClosure(session);
5774
+ return {
5775
+ issue: (issue) => isIssueOutstanding(issue, dispositions),
5776
+ question: (question) => isQuestionOutstanding(question, answered, verdicts.question)
5777
+ };
5778
+ }
5779
+ /** For a caller with no session: nothing may gain framing. */
5780
+ var NOTHING_FRAMABLE = {
5781
+ issue: () => false,
5782
+ question: () => false
5783
+ };
5742
5784
  /**
5743
5785
  * An issue is done when the commits fixed it, or when the reviewer dismissed it and that decision
5744
5786
  * still stands.
@@ -7699,9 +7741,34 @@ var UPDATE_ORDER = [
7699
7741
  "context",
7700
7742
  "analysing"
7701
7743
  ];
7744
+ /**
7745
+ * The same phases, as a Re-check of the pull request discussion runs them. Nothing is fetched, so
7746
+ * there is no `preparing` step to show, and the checkout is of the commit the review read.
7747
+ */
7748
+ var RECHECK_PHASES = {
7749
+ ...UPDATE_PHASES,
7750
+ conversation: {
7751
+ doing: "Fetching the PR discussion",
7752
+ done: "Fetched the PR discussion"
7753
+ },
7754
+ context: {
7755
+ doing: "Preparing the reviewed code",
7756
+ done: "Prepared the reviewed code"
7757
+ },
7758
+ analysing: {
7759
+ doing: "Reassessing issues and questions",
7760
+ done: "Reassessed issues and questions"
7761
+ }
7762
+ };
7763
+ var RECHECK_ORDER = [
7764
+ "conversation",
7765
+ "context",
7766
+ "analysing"
7767
+ ];
7702
7768
  /** The names this job's phases go by. */
7703
7769
  function phasesFor(job) {
7704
- return purposeOf(job) === "update" ? UPDATE_PHASES : PHASES;
7770
+ if (purposeOf(job) !== "update") return PHASES;
7771
+ return job.scope === "conversation" ? RECHECK_PHASES : UPDATE_PHASES;
7705
7772
  }
7706
7773
  /**
7707
7774
  * Whether a phase is part of *this* review, rather than part of the pipeline in general.
@@ -7741,7 +7808,7 @@ function progressSteps(job) {
7741
7808
  const failedAt = job.phase === "failed" ? job.failure?.phase : void 0;
7742
7809
  const update = purposeOf(job) === "update";
7743
7810
  const names = phasesFor(job);
7744
- const order = (update ? UPDATE_ORDER : WORK_PHASES).filter((phase) => appliesTo(job, phase));
7811
+ const order = (update ? job.scope === "conversation" ? RECHECK_ORDER : UPDATE_ORDER : WORK_PHASES).filter((phase) => appliesTo(job, phase));
7745
7812
  return (update && job.phase === "done" ? order.filter((phase) => job.completed.includes(phase)) : order).map((phase) => {
7746
7813
  const complete = job.completed.includes(phase) || !update && job.phase === "done";
7747
7814
  const failed = failedAt === phase;
@@ -7834,6 +7901,7 @@ function strippedForDeletedReview(job) {
7834
7901
  },
7835
7902
  ...job.purpose ? { purpose: job.purpose } : {},
7836
7903
  ...job.reanalysis ? { reanalysis: true } : {},
7904
+ ...job.scope ? { scope: job.scope } : {},
7837
7905
  ...job.reviewType ? { reviewType: job.reviewType } : {},
7838
7906
  phase: job.phase,
7839
7907
  startedAt: job.startedAt,
@@ -12557,14 +12625,20 @@ var SUMMARY_GUIDANCE = `"summary": WHAT the change does, for a reviewer about to
12557
12625
  * extension's investigation cannot drift apart. The fragment is the whole shape, never an
12558
12626
  * abbreviation: a model cannot follow a schema it was not shown.
12559
12627
  */
12560
- var CONSEQUENCE_FIELD = "\"consequence\": { \"title\": string, \"scenario\": string, \"group\": \"failure\" | \"risk\" | \"intent\" | \"maintenance\", \"next\"?: { \"kind\": \"verify\" | \"decide\", \"text\": string } }";
12628
+ var CONSEQUENCE_FIELD = "\"consequence\": { \"title\": string, \"scenario\": string, \"group\": \"failure\" | \"risk\" | \"intent\" | \"maintenance\", \"next\"?: { \"kind\": \"verify\" | \"decide\" | \"fix\", \"text\": string } }";
12561
12629
  /** The same shape on a prior finding's entry in an update, where null withdraws it and absence keeps it. */
12562
12630
  var CONSEQUENCE_REVISION_FIELD = `${CONSEQUENCE_FIELD.replace("\"consequence\": ", "\"consequence\"?: ")} | null`;
12563
12631
  var CONSEQUENCE_GUIDANCE = `"consequence" on an issue: the same issue told as what it means, for someone who knows the product but may not know its implementation. It is shown first; "title" and "description" stay the technical account behind it. Reframe around the consequence, do not just simplify the technical wording.
12564
12632
  - "title": the observable consequence or the meaningful development constraint, as a short sentence (e.g. "Creating a meal can fail halfway, leaving it outside the planner").
12565
12633
  - "scenario": one or two sentences: the triggering condition and what happens (e.g. "If adding it to the day fails after saving, the user sees an error but the meal still exists. Retrying may then be blocked by its name.").
12566
- - "next": the one step that would settle it, when there is a specific one. "kind" is "verify" for a check or test that would establish whether it happens (e.g. "Try a receipt with an unreadable amount.") and "decide" for an acceptance or intent call only the reviewer can make (e.g. "Confirm this broader change is intended."). Omit "next" rather than writing a generic step.
12567
- - "group", by the evidence behind the issue, never by how sure you sound and independently of "severity": "failure" when the code you read establishes an incorrect outcome under a stated condition; "risk" when a plausible undesirable consequence still needs verifying; "intent" when the change has a concrete behavioural consequence whose correctness depends on what was intended; "maintenance" when it constrains future development, maintainability or reliability rather than current behaviour. "confidence" is reported separately and does not decide the group: if the evidence does not establish the failure, it is a "risk", and say what is unverified.
12634
+ - "next": the one specific step this issue calls for, when there is one. "kind" is "verify" for a test or evidence-gathering step that would settle whether it happens (e.g. "Try a receipt with an unreadable amount."), "decide" for an acceptance, requirement or intent call only the reviewer can make (e.g. "Confirm this broader change is intended."), and "fix" for a concrete corrective action the finding itself supports (e.g. "Update the recovery runbook to name the new restore command."). A fix is not a verification: do not label a remedy "verify". Omit "next" rather than write a generic step or invent a remedy the evidence does not support.
12635
+ - "group", by the consequence and the evidence behind it, never by the kind of file, never by how sure you sound and independently of "severity":
12636
+ - "failure" when the evidence you read establishes an incorrect outcome under a stated condition (e.g. "Saving a meal with no ingredients deletes the existing ones.").
12637
+ - "risk" when a plausible adverse consequence still needs verifying (e.g. "Two people editing the same plan at once may overwrite each other; nothing shows whether saves are serialised."), and say what is unverified.
12638
+ - "intent" when there is a concrete behavioural consequence whose acceptability depends on requirements or intent (e.g. "Rotating the sign-in secret now signs every customer out.").
12639
+ - "maintenance" when it constrains future development, maintainability or reliability and no concrete failure or risk fits better (e.g. "Each new payment provider has to be added in four places that must stay in step.").
12640
+ - Documentation, runbooks, configuration and scripts are classified by what they cause like any code: recovery instructions that restore to the wrong database cause an operational failure when the text establishes it, or a risk when unverified. They are not "maintenance" because they are documentation.
12641
+ - "confidence" is reported separately and does not decide the group. A well-evidenced failure stays a failure at medium confidence; a high severity does not make an unverified risk a failure; a low confidence does not by itself turn a failure into a risk.
12568
12642
  - Keep every condition and qualification that affects the decision. Avoid code identifiers unless the consequence cannot be understood without one. Do not state severity.
12569
12643
  - Never claim user harm, a measured performance effect, a reproduction or a certainty the evidence does not give. A performance concern nobody measured says so (e.g. "Opening a meal may do unnecessary work for nested meals... The effect has not been measured."). A maintenance or intent issue is described as one, not as a user-facing failure.
12570
12644
  - An issue names a concrete consequence. A request for missing requirements with no concrete consequence is a question for the author, not an issue, and is never raised as both.`;
@@ -12578,20 +12652,40 @@ var ORIENTATION_GUIDANCE = `"orientation": one sentence, at most about 25 words,
12578
12652
  * consequence that no longer describes the finding is worse than none, so the model is shown each
12579
12653
  * one and must keep, restate or withdraw it.
12580
12654
  */
12581
- var CONSEQUENCE_UPDATE_GUIDANCE = `"consequence" on a finding entry: a finding marked [consequence: ...] has an impact summary the reviewer reads first. Omit "consequence" when it still describes the finding accurately, which is the usual case. Return a restated "consequence" (same shape as on a new issue) when what has changed since alters the condition, the evidence, the next step or the group it describes, most often for an "affected" finding. Return null when it no longer holds and you cannot restate it accurately. Never return "consequence" for a finding without a [consequence: ...] marker.
12655
+ var CONSEQUENCE_UPDATE_GUIDANCE = `"consequence" on a finding entry: a finding marked [consequence: ...] has an impact summary the reviewer reads first. Omit "consequence" when it still describes the finding accurately, which is the usual case. Return a restated "consequence" (same shape as on a new issue) when what has changed since alters the condition, the evidence, the next step or the group it describes, most often for an "affected" finding. Return null when it no longer holds and you cannot restate it accurately.
12656
+ A finding marked [consequence: none yet, write one] is still outstanding and has no impact summary: unless you mark it "resolved", return a "consequence" for it, written from the evidence its description already gives together with anything these changes show. Classify it into whichever of the four groups that evidence supports; do not default to "risk", and do not demand fresh proof to keep a failure the existing evidence already establishes. Keep its uncertainty as stated: rewording a finding is not re-verifying it, so never describe it as newly confirmed or tested. Never return "consequence" for a finding with neither marker.
12582
12657
  "resolvedBehaviour" on a finding entry: only with "resolved", and only when the changed code shows what now happens under the finding's condition, in one sentence for someone who knows the product (e.g. "If adding the meal to the planner fails, the meal is no longer saved."). Omit it when the code does not show that, and whenever the finding is settled by a comment rather than by code.`;
12583
12658
  /**
12584
12659
  * The impact summary the reviewer reads first, shown under a prior finding so a later pass can keep,
12585
12660
  * restate or withdraw it (`CONSEQUENCE_UPDATE_GUIDANCE`, and the investigation's own rule). Group
12586
- * and step are included: either can go stale on its own. A finding stored without one shows
12587
- * nothing, which is also what tells the model not to add one.
12661
+ * and step are included: either can go stale on its own. A finding stored without one is marked
12662
+ * as needing one only when the update may add it (`framable`, an outstanding finding); otherwise it
12663
+ * shows nothing, which is what tells the model not to add one.
12588
12664
  */
12589
- function formatPriorConsequence(issue) {
12665
+ function formatPriorConsequence(issue, framable = false) {
12590
12666
  const consequence = issue.consequence;
12591
- if (!consequence) return "";
12667
+ if (!consequence) return framable ? "\n [consequence: none yet, write one]" : "";
12592
12668
  const next = consequence.next ? ` Next (${consequence.next.kind ?? "step"}): ${consequence.next.text}` : "";
12593
12669
  return `\n [consequence: group ${consequence.group ?? "none"}] ${consequence.title.trim().replace(/[.!?]$/, "")}. ${consequence.scenario.trim()}${next}`;
12594
12670
  }
12671
+ /** A question's scan-first framing, as every prompt that asks questions asks for it. The whole shape. */
12672
+ var QUESTION_FRAMING_FIELD = "\"framing\": { \"question\": string, \"why\": string, \"needed\"?: string }";
12673
+ /** The same shape on a prior question's entry in an update, where null withdraws it and absence keeps it. */
12674
+ var QUESTION_FRAMING_REVISION_FIELD = `${QUESTION_FRAMING_FIELD.replace("\"framing\": ", "\"framing\"?: ")} | null`;
12675
+ var QUESTION_FRAMING_GUIDANCE = `"framing" on a question: the same question, asked so a reviewer who knows the product but not the code can see at a glance what is being asked and why. It is shown first; the original "question" and "whyThisMatters" stay the technical account behind it. It is a presentation of this question, never a second one.
12676
+ - "question": short, at product or operations level (e.g. "Should rotating the sign-in secret require all customers to sign in again?").
12677
+ - "why": one or two sentences on why the answer matters, keeping every condition and uncertainty (e.g. "The current encryption ties stored sessions to that secret."). A conditional premise stays conditional: do not turn "if" into a fact.
12678
+ - "needed": the specific answer or confirmation required, when the question does not already make it plain (e.g. "Confirm whether this consequence is acceptable and documented."). Omit it otherwise.
12679
+ - A question with several parts names every part, so none is lost from view. Do not answer the question in its framing.`;
12680
+ var QUESTION_FRAMING_UPDATE_GUIDANCE = `"framing" on a question entry: a question marked [framing: ...] has a short framing the reviewer reads first. Omit "framing" when it still describes the question accurately. Return a restated "framing" (same shape as on a new question) when these changes alter what is being asked, its condition or what is needed; in particular, when you mark a question "answered" or the changes answer part of it, restate it to name only what is still open, or return null if nothing in it still needs asking. Return null when it no longer holds and you cannot restate it.
12681
+ A question marked [framing: none yet, write one] is still outstanding and has no framing: unless you mark it "answered", return a "framing" for it from what the question and its reasons already say together with anything these changes show, keeping its conditions and uncertainty. Never return "framing" for a question with neither marker.`;
12682
+ /** A prior question's framing, or the marker asking for one, as the update shows it. */
12683
+ function formatPriorFraming(question, framable = false) {
12684
+ const framing = question.framing;
12685
+ if (!framing) return framable ? "\n [framing: none yet, write one]" : "";
12686
+ const needed = framing.needed ? ` Needed: ${framing.needed.trim()}` : "";
12687
+ return `\n [framing] ${framing.question.trim()} ${framing.why.trim()}${needed}`;
12688
+ }
12595
12689
  //#endregion
12596
12690
  //#region ../../packages/review-harness/src/domain/discussion/discussion_delta.ts
12597
12691
  /**
@@ -13371,7 +13465,7 @@ var STRUCTURED_OUTPUT_INSTRUCTIONS = `In addition to the review principles above
13371
13465
  "recommendedStartingPoint": boolean
13372
13466
  }],
13373
13467
  "issues": [{ "id": string, "severity": "critical" | "major" | "minor" | "nit", "title": string, "description": string, "file"?: string, "line"?: number, "sectionId"?: string, "confidence"?: "high" | "medium" | "low", ${CONSEQUENCE_FIELD}, "existingDiscussion"?: { "threadId": string, "relation": "same" | "related" } }],
13374
- "questions": [{ "id": string, "question": string, "importance"?: "high" | "medium" | "low", "whyThisMatters"?: string, "file"?: string, "sectionId"?: string }],
13468
+ "questions": [{ "id": string, "question": string, "importance"?: "high" | "medium" | "low", "whyThisMatters"?: string, ${QUESTION_FRAMING_FIELD}, "file"?: string, "sectionId"?: string }],
13375
13469
  "recommendation": string,
13376
13470
  "nextSteps": string[]
13377
13471
  }
@@ -13391,6 +13485,7 @@ Field guidance:
13391
13485
  - "nextSteps": concrete actions to take before merging (specific tests to run, files to double-check), not a restatement of the Guide.
13392
13486
  - "issues": report only issues worth the reviewer's attention — do not invent issues to fill out the array, and do not omit significant issues. Let the count reflect the actual PR (a trivial PR may have zero or one issue; a large or risky PR may have several).
13393
13487
  - ${CONSEQUENCE_GUIDANCE}
13488
+ - ${QUESTION_FRAMING_GUIDANCE}
13394
13489
  - ${ORIENTATION_GUIDANCE}
13395
13490
  - "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.
13396
13491
  - "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.
@@ -18089,13 +18184,13 @@ function priorFindingVerdict(issue, flag) {
18089
18184
  * reviewer may have moved it, and a model told only the analysis's grade would re-judge from where
18090
18185
  * the review no longer stands. A caller with no ledger passes `ANALYSIS_ONLY`.
18091
18186
  */
18092
- function formatPriorFindings(issues, assess, reassess = /* @__PURE__ */ new Map(), status = NO_STATUS) {
18187
+ function formatPriorFindings(issues, assess, reassess = /* @__PURE__ */ new Map(), status = NO_STATUS, framable = NOTHING_FRAMABLE) {
18093
18188
  if (!issues.length) return "(The previous analysis raised no issues.)";
18094
18189
  return issues.map((issue, index) => {
18095
18190
  const related = (issue.relatedFiles ?? []).map((file) => ` [related: ${file}, not changed]`).join("");
18096
18191
  const location = (issue.file ? ` [${issue.file}${issue.line ? `:${issue.line}` : ""}]` : "") + related;
18097
18192
  const description = issue.description.trim();
18098
- return `${index}. (${promptGrade(assess("issue", issue))})${location}${priorFindingVerdict(issue, reassess.get(index))}${acceptedVerdict(status("issue", issue.id))} ${issue.title.trim()}${description ? `\n ${description}` : ""}${formatPriorConsequence(issue)}`;
18193
+ return `${index}. (${promptGrade(assess("issue", issue))})${location}${priorFindingVerdict(issue, reassess.get(index))}${acceptedVerdict(status("issue", issue.id))} ${issue.title.trim()}${description ? `\n ${description}` : ""}${formatPriorConsequence(issue, framable.issue(issue))}`;
18099
18194
  }).join("\n");
18100
18195
  }
18101
18196
  /**
@@ -18104,7 +18199,7 @@ function formatPriorFindings(issues, assess, reassess = /* @__PURE__ */ new Map(
18104
18199
  * of the issue shape returned issues with no severity, no file and in some cases no content.
18105
18200
  * Keep in step with STRUCTURED_OUTPUT_INSTRUCTIONS in pipeline.ts.
18106
18201
  */
18107
- var QUESTION_SHAPE_LINE = " \"questions\": [{ \"sourceIndex\": number, \"status\": \"answered\" | \"still-open\" | \"affected\", \"answer\"?: string, \"note\"?: string, \"importance\"?: \"high\" | \"medium\" | \"low\", \"importanceReason\"?: string }],";
18202
+ var QUESTION_SHAPE_LINE = ` "questions": [{ "sourceIndex": number, "status": "answered" | "still-open" | "affected", "answer"?: string, "note"?: string, ${QUESTION_FRAMING_REVISION_FIELD}, "importance"?: "high" | "medium" | "low", "importanceReason"?: string }],`;
18108
18203
  var RECONCILIATION_SHAPE = `{
18109
18204
  "findings": [{ "sourceIndex": number, "status": "resolved" | "still-open" | "affected", "note"?: string, ${CONSEQUENCE_REVISION_FIELD}, "resolvedBehaviour"?: string, "severity"?: "critical" | "major" | "minor" | "nit", "severityReason"?: string }],
18110
18205
  "newIssues": [{ "id": string, "severity": "critical" | "major" | "minor" | "nit", "title": string, "description": string, "file"?: string, "line"?: number, ${CONSEQUENCE_FIELD}, "confidence"?: "high" | "medium" | "low" }],
@@ -18150,7 +18245,7 @@ function reconciliationShape(includeQuestions, withDiscussion = false) {
18150
18245
  * question's own id is withheld so it cannot be echoed back in place of an index, and enough
18151
18246
  * context travels for a judgement without inviting a rewrite of the question itself.
18152
18247
  */
18153
- function formatPriorQuestions(questions, assess, status = NO_STATUS) {
18248
+ function formatPriorQuestions(questions, assess, status = NO_STATUS, framable = NOTHING_FRAMABLE) {
18154
18249
  if (!questions.length) return "(The previous analysis raised no questions.)";
18155
18250
  return questions.map((question, index) => {
18156
18251
  const related = (question.relatedFiles ?? []).map((file) => ` [related: ${file}, not changed]`).join("");
@@ -18159,7 +18254,7 @@ function formatPriorQuestions(questions, assess, status = NO_STATUS) {
18159
18254
  const verdict = alreadyAnswered(question) ? " [previously: answered]" : priorVerdict(question);
18160
18255
  const assessment = assess("question", question);
18161
18256
  const importance = assessment.current ? ` [importance: ${promptGrade(assessment)}]` : "";
18162
- return `${index}.${location}${verdict}${acceptedVerdict(status("question", question.id))}${importance} ${question.question.trim()}${why ? `\n ${why}` : ""}`;
18257
+ return `${index}.${location}${verdict}${acceptedVerdict(status("question", question.id))}${importance} ${question.question.trim()}${why ? `\n ${why}` : ""}${formatPriorFraming(question, framable.question(question))}`;
18163
18258
  }).join("\n");
18164
18259
  }
18165
18260
  /**
@@ -18240,7 +18335,7 @@ function buildReconciliationInstructions(input, options = {}) {
18240
18335
  const discussion = options.discussion;
18241
18336
  const conversationOnly = discussion !== void 0 && !discussion.codeChanged;
18242
18337
  const truncationWarning = possiblyTruncated ? `\n\nIMPORTANT: the list of changes above is INCOMPLETE — more files changed than could be retrieved. Do not mark a previous finding "resolved" on the strength of absence: a file you cannot see may or may not contain the fix. Prefer "still-open" or "affected" where you cannot see the relevant code, and say so in the note.` : "";
18243
- const questionList = includeQuestions ? `\n\nPrevious open questions, numbered — refer to each by its number in "sourceIndex":\n${formatPriorQuestions(previous.questions, input.assess, input.status)}` : "";
18338
+ const questionList = includeQuestions ? `\n\nPrevious open questions, numbered — refer to each by its number in "sourceIndex":\n${formatPriorQuestions(previous.questions, input.assess, input.status, input.framable)}` : "";
18244
18339
  const tasks = [
18245
18340
  `${discussion ? `For EVERY numbered finding above, decide what ${conversationOnly ? "the changes in the conversation" : "the new commits and the changes in the conversation"} did to it:
18246
18341
  - "resolved": ${conversationOnly ? "" : "the new commits fix it, and you can point at the change that does so; or "}the conversation gives evidence that invalidates its premise, under the rules below.
@@ -18278,7 +18373,7 @@ Overall risk: ${previous.overallRisk}
18278
18373
  Recommendation: ${previous.recommendation}
18279
18374
 
18280
18375
  Previous findings, numbered — refer to each by its number in "sourceIndex":
18281
- ${formatPriorFindings(previous.issues, input.assess, reassess, input.status)}${questionList}${truncationWarning}
18376
+ ${formatPriorFindings(previous.issues, input.assess, reassess, input.status, input.framable)}${questionList}${truncationWarning}
18282
18377
  ${discussion ? `\n${DISCUSSION_RULES}\n` : ""}
18283
18378
  YOUR TASK
18284
18379
 
@@ -18292,7 +18387,7 @@ ${reconciliationShape(includeQuestions, discussion !== void 0)}
18292
18387
  ${CONSEQUENCE_UPDATE_GUIDANCE}
18293
18388
 
18294
18389
  ${CONSEQUENCE_GUIDANCE}
18295
-
18390
+ ${includeQuestions ? `\n${QUESTION_FRAMING_UPDATE_GUIDANCE}\n\n${QUESTION_FRAMING_GUIDANCE}\n` : ""}
18296
18391
  "severity" on a new issue is how much it would matter if real; "confidence" is how sure you are that it is real. Separate axes — do not let one stand in for the other, and omit "confidence" rather than guessing a middle. Report only what is worth the reviewer's attention: empty arrays are valid answers and are far better than padded ones.`;
18297
18392
  }
18298
18393
  /**
@@ -18454,14 +18549,14 @@ function parseReconciliationResult(raw, priorCount, options = {}) {
18454
18549
  * Every prior finding therefore survives into the new result, in its original order, carrying its
18455
18550
  * original id, severity and text. Only the status is added.
18456
18551
  */
18457
- function reconcileIssues(prior, findings, record) {
18552
+ function reconcileIssues(prior, findings, record, framable = NOTHING_FRAMABLE) {
18458
18553
  const byIndex = new Map(findings.map((f) => [f.sourceIndex, f]));
18459
18554
  return prior.map((issue, index) => {
18460
18555
  const verdict = byIndex.get(index);
18461
18556
  const flag = issue.reconciliationStatus === "resolved" ? record?.flags?.get(index) : void 0;
18462
18557
  if (flag && record) {
18463
18558
  const entry = reassessedEntry(verdict, flag, record);
18464
- const { resolvedBehaviour: _reopened, ...reassessed } = withRevisedConsequence(issue, verdict);
18559
+ const { resolvedBehaviour: _reopened, ...reassessed } = withRevisedConsequence(issue, verdict, false);
18465
18560
  return {
18466
18561
  ...reassessed,
18467
18562
  reconciliationStatus: entry.status,
@@ -18474,26 +18569,45 @@ function reconcileIssues(prior, findings, record) {
18474
18569
  const status = verdict?.status ?? "still-open";
18475
18570
  const recorded = record && isStatusChange(issue.reconciliationStatus, status) ? { statusHistory: appendStatusChange(issue, changeEntry(status, verdict, record)) } : {};
18476
18571
  const fixedInCode = status === "resolved" && !verdict?.evidence && record?.basis !== "conversation";
18477
- const { resolvedBehaviour: _previous, ...current } = withRevisedConsequence(issue, verdict);
18572
+ const { resolvedBehaviour: _previous, ...current } = withRevisedConsequence(issue, verdict, status !== "resolved" && framable.issue(issue));
18478
18573
  return {
18479
- ...current,
18574
+ ...withNote(current, verdict?.note, isStatusChange(issue.reconciliationStatus, status)),
18480
18575
  ...fixedInCode && verdict?.resolvedBehaviour ? { resolvedBehaviour: verdict.resolvedBehaviour } : {},
18481
18576
  reconciliationStatus: status,
18482
- ...verdict?.note ? { reconciliationNote: verdict.note } : {},
18483
18577
  ...recorded,
18484
18578
  ...record?.against ? { reconciledAgainst: verdict?.evidence ? "conversation" : record.against } : {}
18485
18579
  };
18486
18580
  });
18487
18581
  }
18488
18582
  /**
18489
- * The issue with its consequence kept, restated or withdrawn, as the update said.
18583
+ * The finding with the note this round gave, or the one it had. A note argues for the status it was
18584
+ * written with, so silence keeps it only while that status holds: one this round changed leaves it
18585
+ * behind, where it would contradict the new status on the card.
18586
+ */
18587
+ function withNote(finding, note, statusChanged) {
18588
+ if (note) return {
18589
+ ...finding,
18590
+ reconciliationNote: note
18591
+ };
18592
+ if (!statusChanged) return finding;
18593
+ const { reconciliationNote: _superseded, ...rest } = finding;
18594
+ return rest;
18595
+ }
18596
+ /**
18597
+ * The issue with its consequence kept, restated, withdrawn or added, as the update said.
18490
18598
  *
18491
- * Only for an issue that already had one. An update is never the place a historical issue gains
18492
- * an impact summary it was not written with, so an answer for one without is ignored. Silence
18493
- * keeps it: the model was shown it and asked to change it only where it no longer holds.
18599
+ * Silence keeps it: the model was shown it and asked to change it only where it no longer holds.
18600
+ * An issue stored without one gains the one offered only when `mayFrame`: it is still outstanding
18601
+ * after this update, so the card the reviewer is about to work from becomes scan-first. A resolved,
18602
+ * dismissed or verdict-closed issue is never given presentation it was not written with, and a
18603
+ * `null` for an issue that has nothing to withdraw changes nothing.
18494
18604
  */
18495
- function withRevisedConsequence(issue, verdict) {
18496
- if (!issue.consequence || verdict?.consequence === void 0) return issue;
18605
+ function withRevisedConsequence(issue, verdict, mayFrame) {
18606
+ if (verdict?.consequence === void 0) return issue;
18607
+ if (!issue.consequence) return mayFrame && verdict.consequence ? {
18608
+ ...issue,
18609
+ consequence: verdict.consequence
18610
+ } : issue;
18497
18611
  if (verdict.consequence === null) {
18498
18612
  const { consequence: _withdrawn, ...rest } = issue;
18499
18613
  return rest;
@@ -18526,10 +18640,12 @@ function parseQuestions(value, priorCount) {
18526
18640
  const note = asString$1(item.note).trim();
18527
18641
  const answer = asString$1(item.answer).trim();
18528
18642
  const grade = parseGrade("question", item.importance, item.importanceReason);
18643
+ const framing = item.framing === null ? null : normalizeQuestionFraming(item.framing);
18529
18644
  questions.push({
18530
18645
  sourceIndex,
18531
18646
  status,
18532
18647
  ...note ? { note } : {},
18648
+ ...framing !== void 0 ? { framing } : {},
18533
18649
  ...status === "answered" && answer ? { answer } : {},
18534
18650
  ...grade ? { grade: {
18535
18651
  to: grade.to,
@@ -18564,7 +18680,7 @@ function parseQuestions(value, priorCount) {
18564
18680
  function alreadyAnswered(question) {
18565
18681
  return question.reconciliationStatus === "answered" || question.reconciliationAnswer !== void 0;
18566
18682
  }
18567
- function reconcileQuestions(prior, reconciled, against) {
18683
+ function reconcileQuestions(prior, reconciled, against, framable = NOTHING_FRAMABLE) {
18568
18684
  const recorded = against ? { reconciledAgainst: against } : {};
18569
18685
  const byIndex = new Map(reconciled.map((entry) => [entry.sourceIndex, entry]));
18570
18686
  return prior.map((question, index) => {
@@ -18580,15 +18696,35 @@ function reconcileQuestions(prior, reconciled, against) {
18580
18696
  };
18581
18697
  const answered = verdict.status === "answered" && verdict.answer !== void 0;
18582
18698
  const status = verdict.status !== "answered" ? verdict.status : answered ? "answered" : "affected";
18699
+ const changed = (question.reconciliationStatus ?? "still-open") !== status;
18583
18700
  return {
18584
- ...question,
18701
+ ...withNote(withRevisedFraming(question, verdict, status !== "answered" && framable.question(question)), verdict.note, changed),
18585
18702
  reconciliationStatus: status,
18586
- ...verdict.note ? { reconciliationNote: verdict.note } : {},
18587
18703
  ...answered ? { reconciliationAnswer: verdict.answer } : {},
18588
18704
  ...recorded
18589
18705
  };
18590
18706
  });
18591
18707
  }
18708
+ /**
18709
+ * The question with its framing kept, restated, withdrawn or added: `withRevisedConsequence`'s rule.
18710
+ * A framed question is revised whatever its new status, because an answer is exactly what can make
18711
+ * its "needed" line stale; an unframed one gains a framing only while it is still outstanding.
18712
+ */
18713
+ function withRevisedFraming(question, verdict, mayFrame) {
18714
+ if (verdict.framing === void 0) return question;
18715
+ if (!question.framing) return mayFrame && verdict.framing ? {
18716
+ ...question,
18717
+ framing: verdict.framing
18718
+ } : question;
18719
+ if (verdict.framing === null) {
18720
+ const { framing: _withdrawn, ...rest } = question;
18721
+ return rest;
18722
+ }
18723
+ return {
18724
+ ...question,
18725
+ framing: verdict.framing
18726
+ };
18727
+ }
18592
18728
  /** Marks a reconciliation's own findings as new, so the UI can tell them from carried-forward ones. */
18593
18729
  function markNewIssues(issues, against) {
18594
18730
  return issues.map((issue) => ({
@@ -18664,6 +18800,13 @@ function countUpdateChanges(previous, result) {
18664
18800
  ...reopened ? { reopened } : {}
18665
18801
  };
18666
18802
  }
18803
+ /**
18804
+ * Whether a reconciliation moved any finding or put anything to the reviewer. Still-open findings
18805
+ * are the ones it left alone, so they are the one count that is not a change.
18806
+ */
18807
+ function findingsChanged(counts) {
18808
+ return counts.resolved + counts.affected + counts.newIssues + counts.questionsAnswered + (counts.reopened ?? 0) + (counts.gradeChanges ?? 0) + (counts.decisionsChallenged ?? 0) > 0;
18809
+ }
18667
18810
  //#endregion
18668
18811
  //#region ../../packages/review-harness/src/domain/ai_review/update_reconciliation.ts
18669
18812
  function reconciledAgainstOf(basis) {
@@ -18691,8 +18834,8 @@ function applyReconciliation(previous, parsed, context) {
18691
18834
  basis: context.codeChanged ? "code" : "conversation",
18692
18835
  flags,
18693
18836
  against
18694
- }), ...markNewIssues(parsed.newIssues, against)];
18695
- const questions = reconcileQuestions(previous.questions, parsed.questions, against);
18837
+ }, context.framable), ...markNewIssues(parsed.newIssues, against)];
18838
+ const questions = reconcileQuestions(previous.questions, parsed.questions, against, context.framable);
18696
18839
  const { motivation } = context;
18697
18840
  const composed = {
18698
18841
  schemaVersion: 1,
@@ -18989,8 +19132,93 @@ async function updateReview(workspace, runners = {}) {
18989
19132
  if (workspace.changeSet.targetType === "commit") return refuse(reviewedSha, false, COMMIT_REVIEW_CANNOT_UPDATE);
18990
19133
  const parked = parkedUpdateRefusal(workspace);
18991
19134
  if (parked) return refuse(reviewedSha, false, parked);
19135
+ let fetchStartedAt = 0;
19136
+ return driveUpdateJob(workspace, runners, reviewedSha, {
19137
+ startedAt,
19138
+ now
19139
+ }, void 0, async (held, step, signal, clock) => {
19140
+ fetchStartedAt = Date.now();
19141
+ const outcome = await bringForward(workspace, runners, reviewedSha, held, step, signal, clock);
19142
+ if (!signal.aborted && outcome.ok && !outcome.fetchFailed && outcome.toSha) workspace.headSeenAt = fetchStartedAt;
19143
+ return outcome;
19144
+ });
19145
+ }
19146
+ /**
19147
+ * Reconcile the review's findings with its pull request discussion as it is now, at the commit the
19148
+ * review read: the Re-check button.
19149
+ *
19150
+ * The discussion half of an update and nothing else. The conversation is re-read exactly as an
19151
+ * update re-reads it, so every "already raised" line is corrected first; then, when something in it
19152
+ * is new to the analysis, the same reconciliation an update runs is asked of the same agent with no
19153
+ * code delta. It may answer a question, resolve or reopen an issue on the evidence the discussion
19154
+ * rules allow, suggest a grade, or raise a concern as a new issue once it has confirmed it in the
19155
+ * reviewed code. It never fetches and never moves the review to a newer commit: that is an update's,
19156
+ * and the review's commit stays where it was so the next update still sees those commits as new.
19157
+ *
19158
+ * Computes and mutates in memory, like `updateReview`; the route saves.
19159
+ */
19160
+ async function recheckConversation(workspace, runners = {}) {
19161
+ const now = runners.now ?? Date.now;
19162
+ const startedAt = now();
19163
+ const reviewedSha = workspace.session.metadata?.commitSha || null;
19164
+ const recheck = (outcome) => ({
19165
+ ...outcome,
19166
+ scope: "conversation"
19167
+ });
19168
+ if (!workspace.prContext) return recheck(refuse(reviewedSha, false, "This review has no pull request conversation attached, so there is nothing to re-read."));
19169
+ const cannotReassess = parkedUpdateRefusal(workspace) ?? (!workspace.session.result || !reviewedSha ? "This review has no analysis to reassess." : null);
19170
+ if (cannotReassess) {
19171
+ adoptLegacyBaseline(workspace);
19172
+ const conversationOk = await reReadConversation(workspace, runners.runRefresh);
19173
+ return recheck(settled(reviewedSha, reviewedSha, false, conversationOk, "refused", conversationOk ? cannotReassess : rereadFailureOf(workspace)));
19174
+ }
19175
+ return driveUpdateJob(workspace, runners, reviewedSha, {
19176
+ startedAt,
19177
+ now
19178
+ }, "conversation", async (held, step, signal, clock) => recheck(await reconcileConversation(workspace, runners, reviewedSha, held, step, signal, clock)));
19179
+ }
19180
+ /** What a failed re-read said, for an outcome that reports it. */
19181
+ function rereadFailureOf(workspace) {
19182
+ return workspace.lastPrRefresh && !workspace.lastPrRefresh.ok ? workspace.lastPrRefresh.message : "The pull request conversation could not be re-read. Nothing here has changed.";
19183
+ }
19184
+ /** Said when the conversation holds nothing the findings have not already been weighed against. */
19185
+ var NOTHING_NEW_IN_DISCUSSION = "Nothing in the PR discussion is new since the findings were last assessed, so no findings changed.";
19186
+ async function reconcileConversation(workspace, runners, reviewedSha, held, step, signal, clock) {
19187
+ adoptLegacyBaseline(workspace);
19188
+ step("conversation");
19189
+ const conversationOk = await reReadConversation(workspace, runners.runRefresh);
19190
+ signal.throwIfAborted();
19191
+ if (!conversationOk) return settled(reviewedSha, reviewedSha, false, false, "refused", rereadFailureOf(workspace));
19192
+ const { prepared } = await prepareConversation(workspace, {
19193
+ reread: false,
19194
+ ...workspace.lastPrRefresh ? { rereadOutcome: workspace.lastPrRefresh } : {}
19195
+ });
19196
+ if (!conversationMoved(prepared)) return settled(reviewedSha, reviewedSha, false, true, "already-current", NOTHING_NEW_IN_DISCUSSION);
19197
+ if (!await commitPresent(workspace.repoPath, reviewedSha)) return settled(reviewedSha, reviewedSha, false, true, "baseline-gone", BASELINE_GONE_MESSAGE);
19198
+ step("context");
19199
+ let worktreePath;
19200
+ try {
19201
+ ({path: worktreePath} = await pinCheckout(workspace, reviewedSha, held));
19202
+ } catch (error) {
19203
+ if (error instanceof ReviewBeingDeletedError) throw error;
19204
+ return settled(reviewedSha, reviewedSha, false, true, "analysis-failed", "A checkout of the reviewed commit could not be prepared, so the findings were not reassessed.");
19205
+ }
19206
+ signal.throwIfAborted();
19207
+ return reconcile(workspace, runners, reviewedSha, {
19208
+ sha: reviewedSha,
19209
+ fetchFailed: false
19210
+ }, true, worktreePath, step, held.job, signal, clock, prepared, true, "conversation");
19211
+ }
19212
+ /**
19213
+ * The life of an update's job, whatever the update does inside it: the claim, the cancel, the steps,
19214
+ * how it ends and what it announces. Shared by Update and Re-check so the two cannot disagree about
19215
+ * how a run is watched, cancelled or reported.
19216
+ */
19217
+ async function driveUpdateJob(workspace, runners, reviewedSha, clock, scope, work) {
19218
+ const { startedAt, now } = clock;
18992
19219
  const held = await claimCheckoutJob(workspace, reviewedSha ?? "", {
18993
19220
  purpose: "update",
19221
+ ...scope ? { scope } : {},
18994
19222
  ...runners.source ? { source: runners.source } : {},
18995
19223
  ...workspace.prContext ? { conversation: {
18996
19224
  kind: "attached",
@@ -19004,16 +19232,11 @@ async function updateReview(workspace, runners = {}) {
19004
19232
  const current = () => loadJob(held.job.id) ?? held.job;
19005
19233
  const step = (phase) => saveJob(advance(current(), phase));
19006
19234
  try {
19007
- const fetchStartedAt = Date.now();
19008
- const outcome = await bringForward(workspace, runners, reviewedSha, held, step, signal, {
19009
- startedAt,
19010
- now
19011
- });
19012
- if (signal.aborted) return cancelled(held, reviewedSha);
19013
- if (outcome.ok && !outcome.fetchFailed && outcome.toSha) workspace.headSeenAt = fetchStartedAt;
19014
- const ended = outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
19235
+ const outcome = await work(held, step, signal, clock);
19236
+ if (signal.aborted) return scoped(cancelled(held, reviewedSha), scope);
19237
+ const ended = outcome.stop === "analysis-failed" || scope && outcome.conversationOk === false ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
19015
19238
  saveJob(ended);
19016
- if (outcome.ok && outcome.stop === null) {
19239
+ if (!scope && outcome.ok && outcome.stop === null) {
19017
19240
  const completedAt = now();
19018
19241
  recordRuntimeBestEffort({
19019
19242
  operation: "update",
@@ -19029,7 +19252,7 @@ async function updateReview(workspace, runners = {}) {
19029
19252
  announceJob(ended, updateFacts(outcome, workspace));
19030
19253
  return outcome;
19031
19254
  } catch (error) {
19032
- if (signal.aborted) return cancelled(held, reviewedSha);
19255
+ if (signal.aborted) return scoped(cancelled(held, reviewedSha), scope);
19033
19256
  const failed = fail(current(), error instanceof Error ? error.message : String(error));
19034
19257
  saveJob(failed);
19035
19258
  announceJob(failed);
@@ -19039,6 +19262,12 @@ async function updateReview(workspace, runners = {}) {
19039
19262
  held.claim.release();
19040
19263
  }
19041
19264
  }
19265
+ function scoped(outcome, scope) {
19266
+ return scope ? {
19267
+ ...outcome,
19268
+ scope
19269
+ } : outcome;
19270
+ }
19042
19271
  /**
19043
19272
  * The reviewer cancelled. Recorded as a stop, not a failure, so nothing announces it or offers a
19044
19273
  * retry, and answered as an outcome the page reports on the review it left untouched.
@@ -19082,7 +19311,7 @@ async function bringForward(workspace, runners, reviewedSha, held, step, signal,
19082
19311
  signal.throwIfAborted();
19083
19312
  return reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, held.job, signal, clock, prepared, discussionMoved);
19084
19313
  }
19085
- async function reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, job, signal, clock, prepared, discussionMoved) {
19314
+ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk, worktreePath, step, job, signal, clock, prepared, discussionMoved, scope) {
19086
19315
  const { id: jobId, env } = job;
19087
19316
  const newHead = remote.sha;
19088
19317
  let delta = null;
@@ -19117,7 +19346,7 @@ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk
19117
19346
  const requestedModel = resolveModel({ review: { session: workspace.session } }) ?? void 0;
19118
19347
  let raw;
19119
19348
  try {
19120
- const prompt = buildUpdatePrompt(workspace, delta, prepared, reviewedSha, newHead, worktreePath);
19349
+ const prompt = buildUpdatePrompt(workspace, delta, prepared, reviewedSha, newHead, worktreePath, scope);
19121
19350
  raw = runners.run ? await runners.run(prompt, worktreePath, env, onProgress, onObservedModel, signal) : await runUpdate(workspace.id, prompt, worktreePath, workspace.repoPath, env, onProgress, requestedModel, onObservedModel, signal);
19122
19351
  } catch (error) {
19123
19352
  return settled(reviewedSha, newHead, remote.fetchFailed, conversationOk, "analysis-failed", explainUpdateFailure(error, env, requestedModel));
@@ -19132,7 +19361,8 @@ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk
19132
19361
  codeChanged: delta !== null,
19133
19362
  conversation: prepared,
19134
19363
  assess: assessmentLookup(workspace.session),
19135
- motivation
19364
+ motivation,
19365
+ framable: framableFindings(workspace.session)
19136
19366
  });
19137
19367
  signal.throwIfAborted();
19138
19368
  cancellers.delete(jobId);
@@ -19333,11 +19563,36 @@ function mergeUpdateInto(fresh, updated) {
19333
19563
  for (const field of UPDATE_OWNED_FIELDS) if (field in updated) target[field] = source[field];
19334
19564
  else delete target[field];
19335
19565
  merged.session = keepReviewerFields(fresh, updated.session);
19566
+ keepNewerConversation(merged, fresh, updated);
19336
19567
  carryUpdateSuggestions(merged, updated);
19337
19568
  dropStaleChangeMap(merged);
19338
19569
  return merged;
19339
19570
  }
19340
19571
  /**
19572
+ * A conversation re-read that landed on disk while this ran, kept rather than put back.
19573
+ *
19574
+ * An Ask turn re-reads the conversation and saves it, and an update holds its copy for minutes. Laid
19575
+ * over whole, the update's older copy replaced the newer one and the "already raised" lines went
19576
+ * back with it. The newer copy stays, and the attributions are derived again from it: arithmetic,
19577
+ * the same pass the re-read itself runs. `analysedConversation` is untouched either way: it is the
19578
+ * snapshot this run actually reconciled against, so whatever arrived since is still new to the next.
19579
+ */
19580
+ function keepNewerConversation(merged, fresh, updated) {
19581
+ const theirs = fresh.prContext ? orderOf(fresh.prContext.discussion) : void 0;
19582
+ const ours = updated.prContext ? orderOf(updated.prContext.discussion) : void 0;
19583
+ if (theirs === void 0 || ours === void 0 || theirs <= ours || !fresh.prContext) return;
19584
+ merged.prContext = fresh.prContext;
19585
+ if (fresh.lastPrRefresh) merged.lastPrRefresh = fresh.lastPrRefresh;
19586
+ const result = merged.session.result;
19587
+ if (result) {
19588
+ const { result: attributed } = attributeIssuesToDiscussion(result, processDiscussion(fresh.prContext.discussion));
19589
+ merged.session = {
19590
+ ...merged.session,
19591
+ result: attributed
19592
+ };
19593
+ }
19594
+ }
19595
+ /**
19341
19596
  * The grade changes this update suggested, onto the review as it is on disk.
19342
19597
  *
19343
19598
  * The ledger is the reviewer's and comes from the fresh read, so the update's proposals, written on
@@ -19614,20 +19869,20 @@ function stillPresent(counts) {
19614
19869
  * Everything after it is the shared builder, so the two clients cannot drift into asking for
19615
19870
  * different shapes.
19616
19871
  */
19617
- function buildUpdatePrompt(workspace, delta, conversation, reviewedSha, newHead, worktreePath) {
19872
+ function buildUpdatePrompt(workspace, delta, conversation, reviewedSha, newHead, worktreePath, scope) {
19618
19873
  const previous = workspace.session.result;
19619
19874
  const discussionChanged = conversationMoved(conversation);
19620
19875
  const conversationBlock = updateConversationBlock(conversation);
19621
- const opening = delta ? `An earlier review of this branch is being brought up to date with the commits pushed since it was written${discussionChanged ? ", and with what has changed in its pull request conversation since" : ""}.` : "An earlier review of this branch is being brought up to date with what has changed in its pull request conversation since it was written. No commits have been pushed since.";
19876
+ const opening = delta ? `An earlier review of this branch is being brought up to date with the commits pushed since it was written${discussionChanged ? ", and with what has changed in its pull request conversation since" : ""}.` : scope === "conversation" ? "An earlier review of this branch is being reconciled with what has changed in its pull request conversation since it was written. Only the conversation is reassessed in this pass, against the commit the review read. Newer commits on the branch, if any, are not part of it: a comment saying a fix landed in one cannot be confirmed from this checkout, so that finding stays open for a later update to check against the code." : "An earlier review of this branch is being brought up to date with what has changed in its pull request conversation since it was written. No commits have been pushed since.";
19622
19877
  const code = delta ? `The change below is what happened between ${reviewedSha} and ${newHead}. It is not the whole branch: the earlier review already covered everything up to ${reviewedSha}.
19623
19878
 
19624
19879
  ${renderChangeInventory(delta.changeSet.data.files, inlinePathsOf(delta))}
19625
19880
 
19626
- ${delta.diff}` : `The code is unchanged since the earlier review: ${newHead} is the commit it read.`;
19881
+ ${delta.diff}` : scope === "conversation" ? `There is no code delta in this pass: ${newHead} is the commit the earlier review read.` : `The code is unchanged since the earlier review: ${newHead} is the commit it read.`;
19627
19882
  const commits = delta ? (delta.changeSet.authors ?? []).reduce((total, author) => total + author.commits, 0) : 0;
19628
19883
  return `${opening}
19629
19884
 
19630
- The working tree at ${worktreePath} is checked out at ${newHead}, which is the code as it now stands. You may read any file in it, and you should: the point of doing this here rather than from a diff alone is that you can open the code and see whether a fix actually holds. Read only. Change nothing, commit nothing, and run no command that writes.
19885
+ The working tree at ${worktreePath} is checked out at ${newHead}, which is ${scope === "conversation" ? "the code the review read" : "the code as it now stands"}. You may read any file in it, and you should: the point of doing this here rather than from a diff alone is that you can open the code and see whether a fix actually holds. Read only. Change nothing, commit nothing, and run no command that writes.
19631
19886
 
19632
19887
  ${code}
19633
19888
 
@@ -19641,6 +19896,7 @@ ${buildReconciliationInstructions({
19641
19896
  previous,
19642
19897
  assess: assessmentLookup(workspace.session),
19643
19898
  status: statusLookup(workspace.session),
19899
+ framable: framableFindings(workspace.session),
19644
19900
  fromSha: reviewedSha,
19645
19901
  toSha: newHead,
19646
19902
  totalCommits: commits,
@@ -21360,4 +21616,4 @@ var MCP_TOOL_NAMES = [
21360
21616
  "ask_buddy"
21361
21617
  ];
21362
21618
  //#endregion
21363
- export { readAskRequest as $, runnableAgents as $a, explanationOf as $i, authorPreview as $n, kissDecisionsOf as $r, recordRuntimeBestEffort as $t, settleQueue as A, TargetError as Aa, createWithMembers as Ai, moveIds as An, attemptIdOf as Ar, UpdateAlreadyRunningError as At, humanizeModelId as B, agentIdOf as Ba, queueStamp as Bi, existingPaths as Bn, phasesFor as Br, append as Bt, parsePullRequestLink as C, objectWithAnyOf as Ca, lifecyclePillAction as Ci, createPrState as Cn, followedBranch as Cr, RefreshUnavailableError as Ct, activateQueued as D, resolveTarget as Da, sectionOf as Di, markAllRead as Dn, fillFileUrlTemplate as Dr, parkedUpdateRefusal as Dt, relativeTime as E, kissOutcome as Ea, parseLifecycle as Ei, listNotifications as En, forgeResolver as Er, mergeUpdateInto as Et, spawnRunner as F, shorthandOf as Fa, setMembership as Fi, retireNotification as Fn, pruneJobResidue as Fr, prepareReview as Ft, reviewTag as G, STORE_ROOT as Ga, issuesOutstanding as Gi, storedConversation as Gn, isClaimed as Gr, recordTelemetryUploadConsent as Gt, findingRef as H, readAgentPreference as Ha, doneVerb as Hi, adoptLegacyBaseline as Hn, purposeOf as Hr, record as Ht, applyUpdate as I, git as Ia, updateCollections as Ii, toneOf as In, saveJob as Ir, settleReview as It, forgetAskRequest as J, agentFor as Ja, reviewerDispositions as Ji, isUnchanged as Jn, followedRefName as Jr, BUILD_VERSION as Jt, reviewTagsIn as K, AGENT_IDS as Ka, questionsOutstanding as Ki, unforeseenFailure as Kn, CLAIM_LEASE_MS as Kr, telemetryUploadConsent as Kt, modelProvenanceOf as L, refExists as La, queueAdd as Li, unreadAround as Ln, PHASES as Lr, reanalyseReview as Lt, RUNNER_MARKER as M, resolveBase as Ma, nameProblemText as Mi, readNotification as Mn, fail as Mr, runningJobs as Mt, cliEntry as N, AmbiguousRefError as Na, readCollections as Ni, readStateOf as Nn, isTerminal as Nr, failureOf as Nt, compactQueue as O, originRemote as Oa, collectionsStamp as Oi, markRead as On, RUN_RECORD_RETENTION_MS as Or, updateReview as Ot, runDetached as P, shortNameOf as Pa, renameCollection as Pi, removeNotification as Pn, loadJob as Pr, observeReview as Pt, markAskRunning as Q, parkedReason as Qa, explanationActionOf as Qi, UNESTABLISHED_MOTIVATION as Qn, kissDecisionRefusal as Qr, mapRuntimes as Qt, provenanceDetailLines as R, resolveAgent as Ra, queueAddMany as Ri, unreadCount as Rn, isLiveJob as Rr, runJob as Rt, extractPullRequestLinks as S, extractJsonText as Sa, lifecycleName as Si, createArchitectureNotification as Sn, checkReview as Sr, FETCH_TIMEOUT_MS as St, runBatch as T, WITHHELD_REASON as Ta, offersLifecycle as Ti, eventName as Tn, fetchHead as Tr, cancellableHere as Tt, findingRefs as U, writeAgentPreference as Ua, isIssueOutstanding as Ui, prepareConversationWithin as Un, clearClaim as Ur, stamp as Ut, findingAtRef as V, clearAgentPreference as Va, readQueue as Vi, ASK_REREAD_CEILING_MS as Vn, progressSteps as Vr, readEvents as Vt, refOf as W, CONFIG_PATH as Wa, isQuestionOutstanding as Wi, saveRefresh as Wn, driverGone as Wr, deliver as Wt, isRequestId as X, detectAgents as Xa, processDiscussion as Xi, EXPLAIN_SIMPLY_PROMPT as Xn, indexKeptIds as Xr, feedbackUrl as Xt, interruption as Y, agentIdentityOf as Ya, verdictClosure as Yi, agentEnvOf as Yn, groupByLineage as Yr, PACKAGE_NAME as Yt, isTerminalAsk as Z, isAgentId as Za, recommendationsOf as Zi, chatConversationSection as Zn, indexReviewGroups as Zr, waitingHistory as Zt, endRun as _, statusLookup as _a, activityAttributes as _i, writeJsonAtomically as _n, requestJobStop as _r, parseWorkspacePort as _t, lineageIds as a, assessmentLookup as aa, mutateWorkspace as ai, loadArchitecture as an, escapeHtml as ar, healthyPort as at, stopRun as b, SEVERITIES as ba, lifecycleChip as bi, subjectOf as bn, findExistingReview as br, writePortPreference as bt, discardJob as c, findingAssessment as ca, previousKissResult as ci, repositoryKey as cn, ReviewBeingDeletedError as cr, serverEntryExists as ct, startKissJob as d, gradeScale as da, reviewedRepositories as di, trackedFiles as dn, MANAGED_ROOT as dr, stopServer as dt, isExplanationAction as ea, latestKissRun as ei, RUNTIME_WORDS as en, versionFrom as eo, hasAuthorContent as er, recordAskAccepted as et, isDriving as f, isGradeFor as fa, summarise as fi, ownersOf as fn, createWorktree as fr, workspaceUrl as ft, completeRun as g, sourcePhrase as ga, LIFECYCLES as gi, isArchitectureId as gn, removeWorktree as gr, MIN_WORKSPACE_PORT as gt, beginRun as h, reassessIn as ha, workspaceStamps as hi, ancestorsOf as hn, readMarker as hr, MAX_WORKSPACE_PORT as ht, deleteReview as i, analysisRefOf as ia, matchingWorkspaceIds as ii, checkArchitecture as in, CODE_COPY_CLASS as ir, ensureServer as it, RUNNER_KINDS as j, isCommitReview as ja, deleteCollection as ji, pruneNotifications as jn, endedAt as jr, liveUpdateFor as jt, queueForReview as k, DetachedHeadError as ka, createCollection as ki, markReadForReview as kn, allJobs as kr, askCheckout as kt, kissAgentId as l, findingIn as la, recentWorkspaces as li, storeArchitecture as ln, isBusy as lr, spawnServer as lt, RUN_KINDS as m, promptGrade as ma, workspaceRevision as mi, REPOSITORY_SCOPE as mn, isWorktreeHeld as mr, DEFAULT_WORKSPACE_PORT as mt, MCP_TOOL_NAMES as n, ANALYSIS_ONLY as na, lineagePosition as ni, formatDuration as nn, AgentCancelledError as no, renderMarkdown as nr, bootstrapUrl as nt, liveJobsFor as o, closesFinding as oa, positionInLineage as oi, partFiles as on, renderInlineText as or, readServerRecord as ot, track as p, isOffered as pa, touchWorkspace as pi, QUOTE_LIMITS as pn, holdWorktree as pr, writeServerRecord as pt, endAskRequest as q, agentById as qa, resolvedByConversation as qi, refreshPrContext as qn, decideKissRecommendation as qr, telemetryUploadEnabled as qt, deleteLineage as r, NO_STATUS as ra, loadWorkspace as ri, ARCHITECTURE_DIR as rn, AgentUnavailableError as ro, CODE_BLOCK_CLASS as rr, clearServerRecordOwnedBy as rt, wasBlocked as s, currentGradeIn as sa, presentedKissRunOf as si, readArchitecture as sn, modelHelpLines as sr, reviewsUrl as st, MCP_SERVER_INFO_NAME as t, withExplanation as ta, lineageKey as ti, describeRuntimes as tn, AGENT_RUN_MARKER as to, GITHUB_MARKDOWN as tr, readIndexToken as tt, recordKissRun as u, findingStatus as ua, reviewedCommit as ui, storedScopes as un, withReviewLock as ur, stopChild as ut, heldRuns as v, wasReassessed as va, activityLabel as vi, announceJob as vn, describeMovement as vr, resolveWorkspacePort as vt, unsupportedReason as w, describeAuthorship as wa, lifecyclePillLabel as wi, createPrUpdate as wn, reviewedShaOf as wr, cancelUpdate as wt, batchIdentity as x, asString$1 as xa, lifecycleItemLabel as xi, categoryOf as xn, checkCodeFreshness as xr, COMMIT_REVIEW_CANNOT_UPDATE as xt, reportRun as y, followUpFindingId as ya, isLifecycle as yi, branchOf as yn, describeSubject as yr, statedWorkspacePort as yt, agentDisplayName as z, resolveModel as za, queueRemove as zi, withUsageRecorded as zn, isTerminalJobPhase as zr, startJob as zt };
21619
+ export { readAskRequest as $, versionFrom as $a, isExplanationAction as $i, hasAuthorContent as $n, latestKissRun as $r, waitingHistory as $t, settleQueue as A, isCommitReview as Aa, deleteCollection as Ai, markRead as An, endedAt as Ar, askCheckout as At, humanizeModelId as B, clearAgentPreference as Ba, readQueue as Bi, unreadCount as Bn, progressSteps as Br, runJob as Bt, parsePullRequestLink as C, describeAuthorship as Ca, lifecyclePillLabel as Ci, categoryOf as Cn, reviewedShaOf as Cr, RefreshUnavailableError as Ct, activateQueued as D, originRemote as Da, collectionsStamp as Di, eventName as Dn, RUN_RECORD_RETENTION_MS as Dr, parkedUpdateRefusal as Dt, relativeTime as E, resolveTarget as Ea, sectionOf as Ei, createPrUpdate as En, fillFileUrlTemplate as Er, mergeUpdateInto as Et, spawnRunner as F, git as Fa, updateCollections as Fi, readStateOf as Fn, saveJob as Fr, failureOf as Ft, reviewTag as G, AGENT_IDS as Ga, questionsOutstanding as Gi, saveRefresh as Gn, CLAIM_LEASE_MS as Gr, stamp as Gt, findingRef as H, writeAgentPreference as Ha, isIssueOutstanding as Hi, existingPaths as Hn, clearClaim as Hr, append as Ht, applyUpdate as I, refExists as Ia, queueAdd as Ii, removeNotification as In, PHASES as Ir, observeReview as It, forgetAskRequest as J, agentIdentityOf as Ja, verdictClosure as Ji, agentEnvOf as Jn, groupByLineage as Jr, telemetryUploadConsent as Jt, reviewTagsIn as K, agentById as Ka, resolvedByConversation as Ki, storedConversation as Kn, decideKissRecommendation as Kr, deliver as Kt, modelProvenanceOf as L, resolveAgent as La, queueAddMany as Li, retireNotification as Ln, isLiveJob as Lr, prepareReview as Lt, RUNNER_MARKER as M, AmbiguousRefError as Ma, readCollections as Mi, moveIds as Mn, isTerminal as Mr, UpdateAlreadyRunningError as Mt, cliEntry as N, shortNameOf as Na, renameCollection as Ni, pruneNotifications as Nn, loadJob as Nr, liveUpdateFor as Nt, compactQueue as O, DetachedHeadError as Oa, createCollection as Oi, listNotifications as On, allJobs as Or, recheckConversation as Ot, runDetached as P, shorthandOf as Pa, setMembership as Pi, readNotification as Pn, pruneJobResidue as Pr, runningJobs as Pt, markAskRunning as Q, runnableAgents as Qa, explanationOf as Qi, authorPreview as Qn, kissDecisionsOf as Qr, feedbackUrl as Qt, provenanceDetailLines as R, resolveModel as Ra, queueRemove as Ri, toneOf as Rn, isTerminalJobPhase as Rr, settleReview as Rt, extractPullRequestLinks as S, objectWithAnyOf as Sa, lifecyclePillAction as Si, subjectOf as Sn, followedBranch as Sr, FETCH_TIMEOUT_MS as St, runBatch as T, kissOutcome as Ta, parseLifecycle as Ti, createPrState as Tn, forgeResolver as Tr, cancellableHere as Tt, findingRefs as U, CONFIG_PATH as Ua, isQuestionOutstanding as Ui, ASK_REREAD_CEILING_MS as Un, driverGone as Ur, readEvents as Ut, findingAtRef as V, readAgentPreference as Va, doneVerb as Vi, withUsageRecorded as Vn, purposeOf as Vr, startJob as Vt, refOf as W, STORE_ROOT as Wa, issuesOutstanding as Wi, prepareConversationWithin as Wn, isClaimed as Wr, record as Wt, isRequestId as X, isAgentId as Xa, recommendationsOf as Xi, chatConversationSection as Xn, indexReviewGroups as Xr, BUILD_VERSION as Xt, interruption as Y, detectAgents as Ya, processDiscussion as Yi, EXPLAIN_SIMPLY_PROMPT as Yn, indexKeptIds as Yr, telemetryUploadEnabled as Yt, isTerminalAsk as Z, parkedReason as Za, explanationActionOf as Zi, UNESTABLISHED_MOTIVATION as Zn, kissDecisionRefusal as Zr, PACKAGE_NAME as Zt, endRun as _, wasReassessed as _a, activityLabel as _i, ancestorsOf as _n, describeMovement as _r, parseWorkspacePort as _t, lineageIds as a, closesFinding as aa, positionInLineage as ai, ARCHITECTURE_DIR as an, renderInlineText as ar, healthyPort as at, stopRun as b, asString$1 as ba, lifecycleItemLabel as bi, announceJob as bn, checkCodeFreshness as br, writePortPreference as bt, discardJob as c, findingIn as ca, recentWorkspaces as ci, partFiles as cn, isBusy as cr, serverEntryExists as ct, startKissJob as d, isGradeFor as da, summarise as di, storeArchitecture as dn, createWorktree as dr, stopServer as dt, withExplanation as ea, lineageKey as ei, mapRuntimes as en, AGENT_RUN_MARKER as eo, GITHUB_MARKDOWN as er, recordAskAccepted as et, isDriving as f, isOffered as fa, touchWorkspace as fi, storedScopes as fn, holdWorktree as fr, workspaceUrl as ft, completeRun as g, statusLookup as ga, activityAttributes as gi, REPOSITORY_SCOPE as gn, requestJobStop as gr, MIN_WORKSPACE_PORT as gt, beginRun as h, sourcePhrase as ha, LIFECYCLES as hi, QUOTE_LIMITS as hn, removeWorktree as hr, MAX_WORKSPACE_PORT as ht, deleteReview as i, assessmentLookup as ia, mutateWorkspace as ii, formatDuration as in, escapeHtml as ir, ensureServer as it, RUNNER_KINDS as j, resolveBase as ja, nameProblemText as ji, markReadForReview as jn, fail as jr, findingsChanged as jt, queueForReview as k, TargetError as ka, createWithMembers as ki, markAllRead as kn, attemptIdOf as kr, updateReview as kt, kissAgentId as l, findingStatus as la, reviewedCommit as li, readArchitecture as ln, withReviewLock as lr, spawnServer as lt, RUN_KINDS as m, reassessIn as ma, workspaceStamps as mi, ownersOf as mn, readMarker as mr, DEFAULT_WORKSPACE_PORT as mt, MCP_TOOL_NAMES as n, NO_STATUS as na, loadWorkspace as ni, RUNTIME_WORDS as nn, AgentUnavailableError as no, CODE_BLOCK_CLASS as nr, bootstrapUrl as nt, liveJobsFor as o, currentGradeIn as oa, presentedKissRunOf as oi, checkArchitecture as on, modelHelpLines as or, readServerRecord as ot, track as p, promptGrade as pa, workspaceRevision as pi, trackedFiles as pn, isWorktreeHeld as pr, writeServerRecord as pt, endAskRequest as q, agentFor as qa, reviewerDispositions as qi, isUnchanged as qn, followedRefName as qr, recordTelemetryUploadConsent as qt, deleteLineage as r, analysisRefOf as ra, matchingWorkspaceIds as ri, describeRuntimes as rn, CODE_COPY_CLASS as rr, clearServerRecordOwnedBy as rt, wasBlocked as s, findingAssessment as sa, previousKissResult as si, loadArchitecture as sn, ReviewBeingDeletedError as sr, reviewsUrl as st, MCP_SERVER_INFO_NAME as t, ANALYSIS_ONLY as ta, lineagePosition as ti, recordRuntimeBestEffort as tn, AgentCancelledError as to, renderMarkdown as tr, readIndexToken as tt, recordKissRun as u, gradeScale as ua, reviewedRepositories as ui, repositoryKey as un, MANAGED_ROOT as ur, stopChild as ut, heldRuns as v, followUpFindingId as va, isLifecycle as vi, isArchitectureId as vn, describeSubject as vr, resolveWorkspacePort as vt, unsupportedReason as w, WITHHELD_REASON as wa, offersLifecycle as wi, createArchitectureNotification as wn, fetchHead as wr, cancelUpdate as wt, batchIdentity as x, extractJsonText as xa, lifecycleName as xi, branchOf as xn, checkReview as xr, COMMIT_REVIEW_CANNOT_UPDATE as xt, reportRun as y, SEVERITIES as ya, lifecycleChip as yi, writeJsonAtomically as yn, findExistingReview as yr, statedWorkspacePort as yt, agentDisplayName as z, agentIdOf as za, queueStamp as zi, unreadAround as zn, phasesFor as zr, reanalyseReview as zt };