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.
- package/CHANGELOG.md +53 -0
- package/dist/main.js +9 -4
- package/dist/{mcp_tools-BR7_XprF.js → mcp_tools-DbFR9Z90.js} +311 -55
- package/dist/server.js +252 -71
- package/package.json +1 -1
- package/static/{chunk-DMxtqzNj.js → chunk-Cg4cN8Vk.js} +1 -1
- package/static/reviews.js +1 -1
- package/static/runs.js +1 -1
- package/static/shell.css +31 -10
- package/static/sidebar.css +25 -6
- package/static/workspace.js +76 -63
|
@@ -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
|
|
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
|
-
|
|
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
|
|
12567
|
-
- "group", by the evidence behind the
|
|
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.
|
|
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
|
|
12587
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
18492
|
-
*
|
|
18493
|
-
*
|
|
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 (
|
|
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
|
|
19008
|
-
|
|
19009
|
-
|
|
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 $,
|
|
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 };
|