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 +32 -0
- package/dist/main.js +8 -3
- package/dist/{mcp_tools-BR7_XprF.js → mcp_tools-BKPpd9dN.js} +148 -36
- package/dist/server.js +85 -32
- package/package.json +1 -1
- package/static/shell.css +31 -10
- package/static/sidebar.css +25 -6
- package/static/workspace.js +58 -45
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-
|
|
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
|
|
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
|
|
12567
|
-
- "group", by the evidence behind the
|
|
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.
|
|
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
|
|
12587
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
18492
|
-
*
|
|
18493
|
-
*
|
|
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 (
|
|
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,
|