prreviewbuddy 0.91.1 → 0.92.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.92.0
4
+
5
+ **Older issues catch up on Update.** An issue from an earlier review that is still outstanding now
6
+ gains its summary the next time Update finds new commits or new discussion. The summary is written
7
+ from the evidence the review already holds plus what the update read, and may land in any of the
8
+ four groups. It is a rewording, not a re-check: confidence, severity and the technical account stay
9
+ as they were. Resolved, dismissed and closed issues keep the card they had, and an Update that finds
10
+ nothing new still makes no analysis and changes nothing.
11
+
12
+ **To fix.** Besides **To verify** and **To decide**, a card can now name the change that would
13
+ correct it, marked **To fix**, when the evidence supports one. The line above the issues counts
14
+ fixes separately. Issues are grouped by what they cause and how well that is shown, never by the
15
+ kind of file: a wrong step in a recovery runbook can be a failure.
16
+
17
+ **Questions lead with what they mean.** Each question from a new analysis opens with the question in
18
+ plain terms, why the answer matters, and what is needed. The original question, its reasoning and its
19
+ files are under **Technical context** on the card. Answers so far, including a partial one, stay in
20
+ view. Updates keep each question's framing true, restate it to name only what is still open when part
21
+ of it is answered, and write one for an older question still open.
22
+
23
+ **No leftover notes.** When an update answers a question or changes an issue's status without
24
+ saying anything new about it, the card no longer shows the previous round's note beside the new
25
+ status. Before, a question could read as answered with "this commit did not change it" underneath.
26
+
27
+ **Quieter cards.** The age of the pull request discussion is one line, "PR discussion checked 2 days
28
+ ago", with its explanation one click away and **Re-check in PR context** beside it. Cards are
29
+ tighter, and **Technical details** shares a row with the card's actions.
30
+
31
+ For coding agents, `list_findings` and `findings --json` now carry each issue's `confidence` and
32
+ each question's `framing`, and `next.kind` may be `fix`. `get_finding` carries the same, beside the
33
+ original technical text.
34
+
3
35
  ## 0.91.1
4
36
 
5
37
  **Issues lead with what they mean.** Each issue from a new analysis now opens with its consequence:
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as readAskRequest, $a as runnableAgents, $i as explanationOf, Aa as TargetError, Ar as attemptIdOf, At as UpdateAlreadyRunningError, Ba as agentIdOf, Bt as append, Da as resolveTarget, Dt as parkedUpdateRefusal, E as relativeTime, Ea as kissOutcome, Er as forgeResolver, F as spawnRunner, Ft as prepareReview, G as reviewTag, Ga as STORE_ROOT, Gr as isClaimed, Gt as recordTelemetryUploadConsent, H as findingRef, Ha as readAgentPreference, Hr as purposeOf, I as applyUpdate, Ia as git, It as settleReview, J as forgetAskRequest, Ji as reviewerDispositions, Jr as followedRefName, Jt as BUILD_VERSION, Ka as AGENT_IDS, Kt as telemetryUploadConsent, L as modelProvenanceOf, Lr as PHASES, Lt as reanalyseReview, Mt as runningJobs, Na as AmbiguousRefError, Nr as isTerminal$1, Nt as failureOf, P as runDetached, Pr as loadJob, Qa as parkedReason, Ra as resolveAgent, T as runBatch, Ta as WITHHELD_REASON, Ua as writeAgentPreference, Ui as isIssueOutstanding, Ur as clearClaim, Ut as stamp, Vr as progressSteps, Wa as CONFIG_PATH, Wi as isQuestionOutstanding, Wr as driverGone, Wt as deliver, Xa as detectAgents, Y as interruption, Ya as agentIdentityOf, Yi as verdictClosure, Yt as PACKAGE_NAME, Z as isTerminalAsk, Za as isAgentId, _t as parseWorkspacePort, at as healthyPort, br as findExistingReview, bt as writePortPreference, ca as findingAssessment, di as reviewedRepositories, dr as MANAGED_ROOT, dt as stopServer, et as recordAskAccepted, fi as summarise, ft as workspaceUrl, gr as removeWorktree, gt as MIN_WORKSPACE_PORT, hr as readMarker, ht as MAX_WORKSPACE_PORT, i as deleteReview$1, ii as matchingWorkspaceIds, it as ensureServer, j as RUNNER_KINDS, jt as liveUpdateFor, k as queueForReview, kr as allJobs, li as recentWorkspaces, mi as workspaceRevision, mt as DEFAULT_WORKSPACE_PORT, ni as lineagePosition, nt as bootstrapUrl, o as liveJobsFor, oi as positionInLineage, ot as readServerRecord, q as endAskRequest, qa as agentById, qt as telemetryUploadEnabled, ri as loadWorkspace, s as wasBlocked, sr as modelHelpLines, st as reviewsUrl, t as MCP_SERVER_INFO_NAME, tt as readIndexToken, u as recordKissRun, ua as findingStatus, ui as reviewedCommit, vr as describeMovement, x as batchIdentity, xt as COMMIT_REVIEW_CANNOT_UPDATE, yr as describeSubject, yt as statedWorkspacePort, za as resolveModel, zt as startJob } from "./mcp_tools-BR7_XprF.js";
2
+ import { $ as readAskRequest, $a as runnableAgents, $i as explanationOf, Aa as TargetError, Ar as attemptIdOf, At as UpdateAlreadyRunningError, Ba as agentIdOf, Bt as append, Da as resolveTarget, Dt as parkedUpdateRefusal, E as relativeTime, Ea as kissOutcome, Er as forgeResolver, F as spawnRunner, Ft as prepareReview, G as reviewTag, Ga as STORE_ROOT, Gr as isClaimed, Gt as recordTelemetryUploadConsent, H as findingRef, Ha as readAgentPreference, Hr as purposeOf, I as applyUpdate, Ia as git, It as settleReview, J as forgetAskRequest, Ji as reviewerDispositions, Jr as followedRefName, Jt as BUILD_VERSION, Ka as AGENT_IDS, Kt as telemetryUploadConsent, L as modelProvenanceOf, Lr as PHASES, Lt as reanalyseReview, Mt as runningJobs, Na as AmbiguousRefError, Nr as isTerminal$1, Nt as failureOf, P as runDetached, Pr as loadJob, Qa as parkedReason, Ra as resolveAgent, T as runBatch, Ta as WITHHELD_REASON, Ua as writeAgentPreference, Ui as isIssueOutstanding, Ur as clearClaim, Ut as stamp, Vr as progressSteps, Wa as CONFIG_PATH, Wi as isQuestionOutstanding, Wr as driverGone, Wt as deliver, Xa as detectAgents, Y as interruption, Ya as agentIdentityOf, Yi as verdictClosure, Yt as PACKAGE_NAME, Z as isTerminalAsk, Za as isAgentId, _t as parseWorkspacePort, at as healthyPort, br as findExistingReview, bt as writePortPreference, ca as findingAssessment, di as reviewedRepositories, dr as MANAGED_ROOT, dt as stopServer, et as recordAskAccepted, fi as summarise, ft as workspaceUrl, gr as removeWorktree, gt as MIN_WORKSPACE_PORT, hr as readMarker, ht as MAX_WORKSPACE_PORT, i as deleteReview$1, ii as matchingWorkspaceIds, it as ensureServer, j as RUNNER_KINDS, jt as liveUpdateFor, k as queueForReview, kr as allJobs, li as recentWorkspaces, mi as workspaceRevision, mt as DEFAULT_WORKSPACE_PORT, ni as lineagePosition, nt as bootstrapUrl, o as liveJobsFor, oi as positionInLineage, ot as readServerRecord, q as endAskRequest, qa as agentById, qt as telemetryUploadEnabled, ri as loadWorkspace, s as wasBlocked, sr as modelHelpLines, st as reviewsUrl, t as MCP_SERVER_INFO_NAME, tt as readIndexToken, u as recordKissRun, ua as findingStatus, ui as reviewedCommit, vr as describeMovement, x as batchIdentity, xt as COMMIT_REVIEW_CANNOT_UPDATE, yr as describeSubject, yt as statedWorkspacePort, za as resolveModel, zt as startJob } from "./mcp_tools-BKPpd9dN.js";
3
3
  import { basename, dirname, isAbsolute, 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";
@@ -1051,7 +1051,13 @@ function summaryOf(tag, entry, judge) {
1051
1051
  ...issue.consequence.group ? { group: issue.consequence.group } : {},
1052
1052
  ...issue.consequence.next ? { next: { ...issue.consequence.next } } : {}
1053
1053
  } } : {},
1054
- ...issue?.resolvedBehaviour && status === "resolved" ? { resolved_behaviour: issue.resolvedBehaviour } : {}
1054
+ ...issue?.resolvedBehaviour && status === "resolved" ? { resolved_behaviour: issue.resolvedBehaviour } : {},
1055
+ ...issue?.confidence ? { confidence: issue.confidence } : {},
1056
+ ...question?.framing ? { framing: {
1057
+ question: question.framing.question,
1058
+ why: question.framing.why,
1059
+ ...question.framing.needed ? { needed: question.framing.needed } : {}
1060
+ } } : {}
1055
1061
  };
1056
1062
  }
1057
1063
  /** A question can run to a paragraph. The list carries its first line; `get_finding` has the rest. */
@@ -1088,7 +1094,6 @@ function detailOf(workspace, tag, entry, judge) {
1088
1094
  ...section ? { title: section.title } : {}
1089
1095
  } } : {},
1090
1096
  ...finding.relatedFiles?.length ? { related_files: finding.relatedFiles } : {},
1091
- ...issue?.confidence ? { confidence: issue.confidence } : {},
1092
1097
  ...question?.category ? { category: question.category } : {},
1093
1098
  ...issue?.existingDiscussion ? { existing_discussion: {
1094
1099
  thread_id: issue.existingDiscussion.threadId,
@@ -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.
@@ -12557,14 +12599,20 @@ var SUMMARY_GUIDANCE = `"summary": WHAT the change does, for a reviewer about to
12557
12599
  * extension's investigation cannot drift apart. The fragment is the whole shape, never an
12558
12600
  * abbreviation: a model cannot follow a schema it was not shown.
12559
12601
  */
12560
- var CONSEQUENCE_FIELD = "\"consequence\": { \"title\": string, \"scenario\": string, \"group\": \"failure\" | \"risk\" | \"intent\" | \"maintenance\", \"next\"?: { \"kind\": \"verify\" | \"decide\", \"text\": string } }";
12602
+ var CONSEQUENCE_FIELD = "\"consequence\": { \"title\": string, \"scenario\": string, \"group\": \"failure\" | \"risk\" | \"intent\" | \"maintenance\", \"next\"?: { \"kind\": \"verify\" | \"decide\" | \"fix\", \"text\": string } }";
12561
12603
  /** The same shape on a prior finding's entry in an update, where null withdraws it and absence keeps it. */
12562
12604
  var CONSEQUENCE_REVISION_FIELD = `${CONSEQUENCE_FIELD.replace("\"consequence\": ", "\"consequence\"?: ")} | null`;
12563
12605
  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
12606
  - "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
12607
  - "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.
12608
+ - "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.
12609
+ - "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":
12610
+ - "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.").
12611
+ - "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.
12612
+ - "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.").
12613
+ - "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.").
12614
+ - 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.
12615
+ - "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
12616
  - 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
12617
  - 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
12618
  - 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 +12626,40 @@ var ORIENTATION_GUIDANCE = `"orientation": one sentence, at most about 25 words,
12578
12626
  * consequence that no longer describes the finding is worse than none, so the model is shown each
12579
12627
  * one and must keep, restate or withdraw it.
12580
12628
  */
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.
12629
+ 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.
12630
+ 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
12631
  "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
12632
  /**
12584
12633
  * The impact summary the reviewer reads first, shown under a prior finding so a later pass can keep,
12585
12634
  * 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.
12635
+ * and step are included: either can go stale on its own. A finding stored without one is marked
12636
+ * as needing one only when the update may add it (`framable`, an outstanding finding); otherwise it
12637
+ * shows nothing, which is what tells the model not to add one.
12588
12638
  */
12589
- function formatPriorConsequence(issue) {
12639
+ function formatPriorConsequence(issue, framable = false) {
12590
12640
  const consequence = issue.consequence;
12591
- if (!consequence) return "";
12641
+ if (!consequence) return framable ? "\n [consequence: none yet, write one]" : "";
12592
12642
  const next = consequence.next ? ` Next (${consequence.next.kind ?? "step"}): ${consequence.next.text}` : "";
12593
12643
  return `\n [consequence: group ${consequence.group ?? "none"}] ${consequence.title.trim().replace(/[.!?]$/, "")}. ${consequence.scenario.trim()}${next}`;
12594
12644
  }
12645
+ /** A question's scan-first framing, as every prompt that asks questions asks for it. The whole shape. */
12646
+ var QUESTION_FRAMING_FIELD = "\"framing\": { \"question\": string, \"why\": string, \"needed\"?: string }";
12647
+ /** The same shape on a prior question's entry in an update, where null withdraws it and absence keeps it. */
12648
+ var QUESTION_FRAMING_REVISION_FIELD = `${QUESTION_FRAMING_FIELD.replace("\"framing\": ", "\"framing\"?: ")} | null`;
12649
+ 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.
12650
+ - "question": short, at product or operations level (e.g. "Should rotating the sign-in secret require all customers to sign in again?").
12651
+ - "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.
12652
+ - "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.
12653
+ - A question with several parts names every part, so none is lost from view. Do not answer the question in its framing.`;
12654
+ 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.
12655
+ 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.`;
12656
+ /** A prior question's framing, or the marker asking for one, as the update shows it. */
12657
+ function formatPriorFraming(question, framable = false) {
12658
+ const framing = question.framing;
12659
+ if (!framing) return framable ? "\n [framing: none yet, write one]" : "";
12660
+ const needed = framing.needed ? ` Needed: ${framing.needed.trim()}` : "";
12661
+ return `\n [framing] ${framing.question.trim()} ${framing.why.trim()}${needed}`;
12662
+ }
12595
12663
  //#endregion
12596
12664
  //#region ../../packages/review-harness/src/domain/discussion/discussion_delta.ts
12597
12665
  /**
@@ -13371,7 +13439,7 @@ var STRUCTURED_OUTPUT_INSTRUCTIONS = `In addition to the review principles above
13371
13439
  "recommendedStartingPoint": boolean
13372
13440
  }],
13373
13441
  "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 }],
13442
+ "questions": [{ "id": string, "question": string, "importance"?: "high" | "medium" | "low", "whyThisMatters"?: string, ${QUESTION_FRAMING_FIELD}, "file"?: string, "sectionId"?: string }],
13375
13443
  "recommendation": string,
13376
13444
  "nextSteps": string[]
13377
13445
  }
@@ -13391,6 +13459,7 @@ Field guidance:
13391
13459
  - "nextSteps": concrete actions to take before merging (specific tests to run, files to double-check), not a restatement of the Guide.
13392
13460
  - "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
13461
  - ${CONSEQUENCE_GUIDANCE}
13462
+ - ${QUESTION_FRAMING_GUIDANCE}
13394
13463
  - ${ORIENTATION_GUIDANCE}
13395
13464
  - "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
13465
  - "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 +18158,13 @@ function priorFindingVerdict(issue, flag) {
18089
18158
  * reviewer may have moved it, and a model told only the analysis's grade would re-judge from where
18090
18159
  * the review no longer stands. A caller with no ledger passes `ANALYSIS_ONLY`.
18091
18160
  */
18092
- function formatPriorFindings(issues, assess, reassess = /* @__PURE__ */ new Map(), status = NO_STATUS) {
18161
+ function formatPriorFindings(issues, assess, reassess = /* @__PURE__ */ new Map(), status = NO_STATUS, framable = NOTHING_FRAMABLE) {
18093
18162
  if (!issues.length) return "(The previous analysis raised no issues.)";
18094
18163
  return issues.map((issue, index) => {
18095
18164
  const related = (issue.relatedFiles ?? []).map((file) => ` [related: ${file}, not changed]`).join("");
18096
18165
  const location = (issue.file ? ` [${issue.file}${issue.line ? `:${issue.line}` : ""}]` : "") + related;
18097
18166
  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)}`;
18167
+ 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
18168
  }).join("\n");
18100
18169
  }
18101
18170
  /**
@@ -18104,7 +18173,7 @@ function formatPriorFindings(issues, assess, reassess = /* @__PURE__ */ new Map(
18104
18173
  * of the issue shape returned issues with no severity, no file and in some cases no content.
18105
18174
  * Keep in step with STRUCTURED_OUTPUT_INSTRUCTIONS in pipeline.ts.
18106
18175
  */
18107
- var QUESTION_SHAPE_LINE = " \"questions\": [{ \"sourceIndex\": number, \"status\": \"answered\" | \"still-open\" | \"affected\", \"answer\"?: string, \"note\"?: string, \"importance\"?: \"high\" | \"medium\" | \"low\", \"importanceReason\"?: string }],";
18176
+ 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
18177
  var RECONCILIATION_SHAPE = `{
18109
18178
  "findings": [{ "sourceIndex": number, "status": "resolved" | "still-open" | "affected", "note"?: string, ${CONSEQUENCE_REVISION_FIELD}, "resolvedBehaviour"?: string, "severity"?: "critical" | "major" | "minor" | "nit", "severityReason"?: string }],
18110
18179
  "newIssues": [{ "id": string, "severity": "critical" | "major" | "minor" | "nit", "title": string, "description": string, "file"?: string, "line"?: number, ${CONSEQUENCE_FIELD}, "confidence"?: "high" | "medium" | "low" }],
@@ -18150,7 +18219,7 @@ function reconciliationShape(includeQuestions, withDiscussion = false) {
18150
18219
  * question's own id is withheld so it cannot be echoed back in place of an index, and enough
18151
18220
  * context travels for a judgement without inviting a rewrite of the question itself.
18152
18221
  */
18153
- function formatPriorQuestions(questions, assess, status = NO_STATUS) {
18222
+ function formatPriorQuestions(questions, assess, status = NO_STATUS, framable = NOTHING_FRAMABLE) {
18154
18223
  if (!questions.length) return "(The previous analysis raised no questions.)";
18155
18224
  return questions.map((question, index) => {
18156
18225
  const related = (question.relatedFiles ?? []).map((file) => ` [related: ${file}, not changed]`).join("");
@@ -18159,7 +18228,7 @@ function formatPriorQuestions(questions, assess, status = NO_STATUS) {
18159
18228
  const verdict = alreadyAnswered(question) ? " [previously: answered]" : priorVerdict(question);
18160
18229
  const assessment = assess("question", question);
18161
18230
  const importance = assessment.current ? ` [importance: ${promptGrade(assessment)}]` : "";
18162
- return `${index}.${location}${verdict}${acceptedVerdict(status("question", question.id))}${importance} ${question.question.trim()}${why ? `\n ${why}` : ""}`;
18231
+ return `${index}.${location}${verdict}${acceptedVerdict(status("question", question.id))}${importance} ${question.question.trim()}${why ? `\n ${why}` : ""}${formatPriorFraming(question, framable.question(question))}`;
18163
18232
  }).join("\n");
18164
18233
  }
18165
18234
  /**
@@ -18240,7 +18309,7 @@ function buildReconciliationInstructions(input, options = {}) {
18240
18309
  const discussion = options.discussion;
18241
18310
  const conversationOnly = discussion !== void 0 && !discussion.codeChanged;
18242
18311
  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)}` : "";
18312
+ 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
18313
  const tasks = [
18245
18314
  `${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
18315
  - "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 +18347,7 @@ Overall risk: ${previous.overallRisk}
18278
18347
  Recommendation: ${previous.recommendation}
18279
18348
 
18280
18349
  Previous findings, numbered — refer to each by its number in "sourceIndex":
18281
- ${formatPriorFindings(previous.issues, input.assess, reassess, input.status)}${questionList}${truncationWarning}
18350
+ ${formatPriorFindings(previous.issues, input.assess, reassess, input.status, input.framable)}${questionList}${truncationWarning}
18282
18351
  ${discussion ? `\n${DISCUSSION_RULES}\n` : ""}
18283
18352
  YOUR TASK
18284
18353
 
@@ -18292,7 +18361,7 @@ ${reconciliationShape(includeQuestions, discussion !== void 0)}
18292
18361
  ${CONSEQUENCE_UPDATE_GUIDANCE}
18293
18362
 
18294
18363
  ${CONSEQUENCE_GUIDANCE}
18295
-
18364
+ ${includeQuestions ? `\n${QUESTION_FRAMING_UPDATE_GUIDANCE}\n\n${QUESTION_FRAMING_GUIDANCE}\n` : ""}
18296
18365
  "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
18366
  }
18298
18367
  /**
@@ -18454,14 +18523,14 @@ function parseReconciliationResult(raw, priorCount, options = {}) {
18454
18523
  * Every prior finding therefore survives into the new result, in its original order, carrying its
18455
18524
  * original id, severity and text. Only the status is added.
18456
18525
  */
18457
- function reconcileIssues(prior, findings, record) {
18526
+ function reconcileIssues(prior, findings, record, framable = NOTHING_FRAMABLE) {
18458
18527
  const byIndex = new Map(findings.map((f) => [f.sourceIndex, f]));
18459
18528
  return prior.map((issue, index) => {
18460
18529
  const verdict = byIndex.get(index);
18461
18530
  const flag = issue.reconciliationStatus === "resolved" ? record?.flags?.get(index) : void 0;
18462
18531
  if (flag && record) {
18463
18532
  const entry = reassessedEntry(verdict, flag, record);
18464
- const { resolvedBehaviour: _reopened, ...reassessed } = withRevisedConsequence(issue, verdict);
18533
+ const { resolvedBehaviour: _reopened, ...reassessed } = withRevisedConsequence(issue, verdict, false);
18465
18534
  return {
18466
18535
  ...reassessed,
18467
18536
  reconciliationStatus: entry.status,
@@ -18474,26 +18543,45 @@ function reconcileIssues(prior, findings, record) {
18474
18543
  const status = verdict?.status ?? "still-open";
18475
18544
  const recorded = record && isStatusChange(issue.reconciliationStatus, status) ? { statusHistory: appendStatusChange(issue, changeEntry(status, verdict, record)) } : {};
18476
18545
  const fixedInCode = status === "resolved" && !verdict?.evidence && record?.basis !== "conversation";
18477
- const { resolvedBehaviour: _previous, ...current } = withRevisedConsequence(issue, verdict);
18546
+ const { resolvedBehaviour: _previous, ...current } = withRevisedConsequence(issue, verdict, status !== "resolved" && framable.issue(issue));
18478
18547
  return {
18479
- ...current,
18548
+ ...withNote(current, verdict?.note, isStatusChange(issue.reconciliationStatus, status)),
18480
18549
  ...fixedInCode && verdict?.resolvedBehaviour ? { resolvedBehaviour: verdict.resolvedBehaviour } : {},
18481
18550
  reconciliationStatus: status,
18482
- ...verdict?.note ? { reconciliationNote: verdict.note } : {},
18483
18551
  ...recorded,
18484
18552
  ...record?.against ? { reconciledAgainst: verdict?.evidence ? "conversation" : record.against } : {}
18485
18553
  };
18486
18554
  });
18487
18555
  }
18488
18556
  /**
18489
- * The issue with its consequence kept, restated or withdrawn, as the update said.
18557
+ * The finding with the note this round gave, or the one it had. A note argues for the status it was
18558
+ * written with, so silence keeps it only while that status holds: one this round changed leaves it
18559
+ * behind, where it would contradict the new status on the card.
18560
+ */
18561
+ function withNote(finding, note, statusChanged) {
18562
+ if (note) return {
18563
+ ...finding,
18564
+ reconciliationNote: note
18565
+ };
18566
+ if (!statusChanged) return finding;
18567
+ const { reconciliationNote: _superseded, ...rest } = finding;
18568
+ return rest;
18569
+ }
18570
+ /**
18571
+ * The issue with its consequence kept, restated, withdrawn or added, as the update said.
18490
18572
  *
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.
18573
+ * Silence keeps it: the model was shown it and asked to change it only where it no longer holds.
18574
+ * An issue stored without one gains the one offered only when `mayFrame`: it is still outstanding
18575
+ * after this update, so the card the reviewer is about to work from becomes scan-first. A resolved,
18576
+ * dismissed or verdict-closed issue is never given presentation it was not written with, and a
18577
+ * `null` for an issue that has nothing to withdraw changes nothing.
18494
18578
  */
18495
- function withRevisedConsequence(issue, verdict) {
18496
- if (!issue.consequence || verdict?.consequence === void 0) return issue;
18579
+ function withRevisedConsequence(issue, verdict, mayFrame) {
18580
+ if (verdict?.consequence === void 0) return issue;
18581
+ if (!issue.consequence) return mayFrame && verdict.consequence ? {
18582
+ ...issue,
18583
+ consequence: verdict.consequence
18584
+ } : issue;
18497
18585
  if (verdict.consequence === null) {
18498
18586
  const { consequence: _withdrawn, ...rest } = issue;
18499
18587
  return rest;
@@ -18526,10 +18614,12 @@ function parseQuestions(value, priorCount) {
18526
18614
  const note = asString$1(item.note).trim();
18527
18615
  const answer = asString$1(item.answer).trim();
18528
18616
  const grade = parseGrade("question", item.importance, item.importanceReason);
18617
+ const framing = item.framing === null ? null : normalizeQuestionFraming(item.framing);
18529
18618
  questions.push({
18530
18619
  sourceIndex,
18531
18620
  status,
18532
18621
  ...note ? { note } : {},
18622
+ ...framing !== void 0 ? { framing } : {},
18533
18623
  ...status === "answered" && answer ? { answer } : {},
18534
18624
  ...grade ? { grade: {
18535
18625
  to: grade.to,
@@ -18564,7 +18654,7 @@ function parseQuestions(value, priorCount) {
18564
18654
  function alreadyAnswered(question) {
18565
18655
  return question.reconciliationStatus === "answered" || question.reconciliationAnswer !== void 0;
18566
18656
  }
18567
- function reconcileQuestions(prior, reconciled, against) {
18657
+ function reconcileQuestions(prior, reconciled, against, framable = NOTHING_FRAMABLE) {
18568
18658
  const recorded = against ? { reconciledAgainst: against } : {};
18569
18659
  const byIndex = new Map(reconciled.map((entry) => [entry.sourceIndex, entry]));
18570
18660
  return prior.map((question, index) => {
@@ -18580,15 +18670,35 @@ function reconcileQuestions(prior, reconciled, against) {
18580
18670
  };
18581
18671
  const answered = verdict.status === "answered" && verdict.answer !== void 0;
18582
18672
  const status = verdict.status !== "answered" ? verdict.status : answered ? "answered" : "affected";
18673
+ const changed = (question.reconciliationStatus ?? "still-open") !== status;
18583
18674
  return {
18584
- ...question,
18675
+ ...withNote(withRevisedFraming(question, verdict, status !== "answered" && framable.question(question)), verdict.note, changed),
18585
18676
  reconciliationStatus: status,
18586
- ...verdict.note ? { reconciliationNote: verdict.note } : {},
18587
18677
  ...answered ? { reconciliationAnswer: verdict.answer } : {},
18588
18678
  ...recorded
18589
18679
  };
18590
18680
  });
18591
18681
  }
18682
+ /**
18683
+ * The question with its framing kept, restated, withdrawn or added: `withRevisedConsequence`'s rule.
18684
+ * A framed question is revised whatever its new status, because an answer is exactly what can make
18685
+ * its "needed" line stale; an unframed one gains a framing only while it is still outstanding.
18686
+ */
18687
+ function withRevisedFraming(question, verdict, mayFrame) {
18688
+ if (verdict.framing === void 0) return question;
18689
+ if (!question.framing) return mayFrame && verdict.framing ? {
18690
+ ...question,
18691
+ framing: verdict.framing
18692
+ } : question;
18693
+ if (verdict.framing === null) {
18694
+ const { framing: _withdrawn, ...rest } = question;
18695
+ return rest;
18696
+ }
18697
+ return {
18698
+ ...question,
18699
+ framing: verdict.framing
18700
+ };
18701
+ }
18592
18702
  /** Marks a reconciliation's own findings as new, so the UI can tell them from carried-forward ones. */
18593
18703
  function markNewIssues(issues, against) {
18594
18704
  return issues.map((issue) => ({
@@ -18691,8 +18801,8 @@ function applyReconciliation(previous, parsed, context) {
18691
18801
  basis: context.codeChanged ? "code" : "conversation",
18692
18802
  flags,
18693
18803
  against
18694
- }), ...markNewIssues(parsed.newIssues, against)];
18695
- const questions = reconcileQuestions(previous.questions, parsed.questions, against);
18804
+ }, context.framable), ...markNewIssues(parsed.newIssues, against)];
18805
+ const questions = reconcileQuestions(previous.questions, parsed.questions, against, context.framable);
18696
18806
  const { motivation } = context;
18697
18807
  const composed = {
18698
18808
  schemaVersion: 1,
@@ -19132,7 +19242,8 @@ async function reconcile(workspace, runners, reviewedSha, remote, conversationOk
19132
19242
  codeChanged: delta !== null,
19133
19243
  conversation: prepared,
19134
19244
  assess: assessmentLookup(workspace.session),
19135
- motivation
19245
+ motivation,
19246
+ framable: framableFindings(workspace.session)
19136
19247
  });
19137
19248
  signal.throwIfAborted();
19138
19249
  cancellers.delete(jobId);
@@ -19641,6 +19752,7 @@ ${buildReconciliationInstructions({
19641
19752
  previous,
19642
19753
  assess: assessmentLookup(workspace.session),
19643
19754
  status: statusLookup(workspace.session),
19755
+ framable: framableFindings(workspace.session),
19644
19756
  fromSha: reviewedSha,
19645
19757
  toSha: newHead,
19646
19758
  totalCommits: commits,