fifony 0.1.43 → 0.1.47
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/app/dist/assets/{CommandPalette-M4VAMxCU.js → CommandPalette-CL8p78lG.js} +1 -1
- package/app/dist/assets/{KeyboardShortcutsHelp-DkvPUXQq.js → KeyboardShortcutsHelp-CqEFfGcE.js} +1 -1
- package/app/dist/assets/OnboardingWizard-BmI50ZUv.js +1 -0
- package/app/dist/assets/analytics.lazy-CXGjZabc.js +1 -0
- package/app/dist/assets/{api-CkVfYg_m.js → api-CEr_D4e5.js} +1 -1
- package/app/dist/assets/{createLucideIcon-Dfk_Hxud.js → createLucideIcon-luywpIq4.js} +1 -1
- package/app/dist/assets/index-CEaccpYh.js +96 -0
- package/app/dist/assets/index-CzzWGzux.css +1 -0
- package/app/dist/assets/vendor-uqBx3VSC.js +9 -0
- package/app/dist/index.html +12 -12
- package/app/dist/service-worker.js +15 -5
- package/dist/agent/pty-daemon.js +3 -2
- package/dist/agent/run-local.js +71 -52
- package/dist/{agent-RMQTTUEC.js → agent-DFSFG6DG.js} +18 -12
- package/dist/{analytics-broadcaster-O6YBP66L.js → analytics-broadcaster-O4AE3RUK.js} +21 -14
- package/dist/approve-plan.command-QGQZZXTQ.js +17 -0
- package/dist/{chunk-E2EWEYA4.js → chunk-2PRRKBG6.js} +20 -10
- package/dist/chunk-5AMWD66T.js +38 -0
- package/dist/{chunk-QQQLP3PL.js → chunk-7TXZYZR5.js} +9 -37
- package/dist/chunk-AAVROEQC.js +859 -0
- package/dist/{chunk-ESWHDHH6.js → chunk-AAZKYWOY.js} +4 -4
- package/dist/chunk-EBCSQFPR.js +682 -0
- package/dist/{chunk-BRSR26VK.js → chunk-FH7HUPZX.js} +2 -2
- package/dist/chunk-HOIOVUHI.js +35 -0
- package/dist/{chunk-AILXZ2TD.js → chunk-JRLWLZOD.js} +20 -13
- package/dist/{chunk-YRSH2CLW.js → chunk-K36BWMUV.js} +1741 -1216
- package/dist/chunk-N4KFNX2G.js +370 -0
- package/dist/chunk-PACI3T4I.js +125 -0
- package/dist/{chunk-FJNH3G2Z.js → chunk-PI7Y77R3.js} +38 -663
- package/dist/{chunk-DVU3CXWA.js → chunk-PXTIWKLQ.js} +2 -1
- package/dist/{chunk-SOBLO4YZ.js → chunk-QH6VCTET.js} +316 -127
- package/dist/{chunk-MVTGAKQK.js → chunk-QHISYRXJ.js} +2 -2
- package/dist/{chunk-42AMQAJG.js → chunk-VM5QAYP5.js} +2 -2
- package/dist/cli.js +17 -11
- package/dist/create-issue.command-VAKYRECC.js +24 -0
- package/dist/{fsm-issue-YGGF7SIL.js → fsm-issue-EHTSKMFN.js} +9 -8
- package/dist/fsm-service-7O4AJG2R.js +32 -0
- package/dist/{helpers-L7NYO5XS.js → helpers-ON2S7UEF.js} +2 -2
- package/dist/{issue-log-broadcaster-WZAHISYB.js → issue-log-broadcaster-FZGVEEIX.js} +20 -13
- package/dist/{issues-3QRR7KM6.js → issues-3YNNTB4U.js} +10 -7
- package/dist/{log-analyzer-K7MXQB4T.js → log-analyzer-EIX6R6PP.js} +82 -18
- package/dist/logger-IFLXTQPS.js +11 -0
- package/dist/mcp/server.js +2 -2
- package/dist/merge-workspace.command-T2NIGR4M.js +24 -0
- package/dist/{parallel-executor-6INE6NDO.js → parallel-executor-DWESCNX3.js} +20 -14
- package/dist/queue-workers-V57BYXAY.js +38 -0
- package/dist/replan-issue.command-2GQ3QXCR.js +17 -0
- package/dist/retry-issue.command-GJBUUYDJ.js +17 -0
- package/dist/scheduler-KYILMWLD.js +32 -0
- package/dist/{settings-ZAWDCFP2.js → settings-SOTIS6ZD.js} +32 -12
- package/dist/settings.resource-JMD3JQOS.js +30 -0
- package/dist/{store-M6NCKMZY.js → store-S3NAYZ3S.js} +18 -12
- package/dist/{web-push-AX5IIK3P.js → web-push-QCTLS7EJ.js} +3 -3
- package/dist/websocket-T2Y3BY4B.js +61 -0
- package/dist/{workspace-CJTWFWTJ.js → workspace-OS7GPMCN.js} +7 -6
- package/package.json +8 -5
- package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +0 -1
- package/app/dist/assets/analytics.lazy-zVJdF880.js +0 -1
- package/app/dist/assets/index-BpiCi7Ew.css +0 -1
- package/app/dist/assets/index-D2INW0zc.js +0 -47
- package/app/dist/assets/vendor-BEoYbFV1.js +0 -9
- package/dist/queue-workers-XFZK3TT5.js +0 -32
- package/dist/replan-issue.command-4UCWYHGZ.js +0 -15
- package/dist/scheduler-ZP7GOZDW.js +0 -26
- package/dist/settings.resource-5CW456AZ.js +0 -24
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
// src/agents/review-profile.ts
|
|
2
|
+
var UI_EXTENSIONS = [".jsx", ".tsx", ".css", ".scss", ".vue", ".svelte", ".html"];
|
|
3
|
+
function collectCandidatePaths(issue) {
|
|
4
|
+
const planPaths = issue.plan?.suggestedPaths ?? [];
|
|
5
|
+
const issuePaths = issue.paths ?? [];
|
|
6
|
+
const contractAreas = issue.plan?.executionContract?.focusAreas ?? [];
|
|
7
|
+
return [.../* @__PURE__ */ new Set([...issuePaths, ...planPaths, ...contractAreas])];
|
|
8
|
+
}
|
|
9
|
+
function collectCandidateText(issue) {
|
|
10
|
+
return [
|
|
11
|
+
issue.title,
|
|
12
|
+
issue.description,
|
|
13
|
+
issue.issueType,
|
|
14
|
+
...issue.labels ?? [],
|
|
15
|
+
...issue.plan?.suggestedPaths ?? []
|
|
16
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
function hasPath(paths, pattern) {
|
|
19
|
+
return paths.some((path) => pattern.test(path));
|
|
20
|
+
}
|
|
21
|
+
function hasCategory(criteria, category) {
|
|
22
|
+
return criteria.some((criterion) => criterion.category === category);
|
|
23
|
+
}
|
|
24
|
+
function includesAny(text, terms) {
|
|
25
|
+
return terms.some((term) => text.includes(term));
|
|
26
|
+
}
|
|
27
|
+
function buildFocusAreas(paths, fallback) {
|
|
28
|
+
const combined = [...paths, ...fallback].filter(Boolean);
|
|
29
|
+
return [...new Set(combined)].slice(0, 6);
|
|
30
|
+
}
|
|
31
|
+
function deriveReviewProfile(issue) {
|
|
32
|
+
const paths = collectCandidatePaths(issue);
|
|
33
|
+
const text = collectCandidateText(issue);
|
|
34
|
+
const criteria = issue.plan?.acceptanceCriteria ?? [];
|
|
35
|
+
const complexity = issue.plan?.estimatedComplexity;
|
|
36
|
+
const lowScope = complexity === "trivial" || complexity === "low";
|
|
37
|
+
const scores = [
|
|
38
|
+
{ name: "general-quality", score: 1, rationale: ["Fallback profile for broad correctness, regression risk, and code quality review."] },
|
|
39
|
+
{ name: "ui-polish", score: 0, rationale: [] },
|
|
40
|
+
{ name: "workflow-fsm", score: 0, rationale: [] },
|
|
41
|
+
{ name: "integration-safety", score: 0, rationale: [] },
|
|
42
|
+
{ name: "api-contract", score: 0, rationale: [] },
|
|
43
|
+
{ name: "security-hardening", score: 0, rationale: [] }
|
|
44
|
+
];
|
|
45
|
+
const pathWeight = lowScope ? 1 : 5;
|
|
46
|
+
const keywordWeight = lowScope ? 1 : 3;
|
|
47
|
+
const broadKeywordWeight = lowScope ? 0 : 2;
|
|
48
|
+
const uiScore = scores.find((entry) => entry.name === "ui-polish");
|
|
49
|
+
if (paths.some((path) => UI_EXTENSIONS.some((ext) => path.endsWith(ext)))) {
|
|
50
|
+
uiScore.score += lowScope ? 2 : 4;
|
|
51
|
+
uiScore.rationale.push("Touched frontend files that can regress visual polish, interaction flow, or responsiveness.");
|
|
52
|
+
}
|
|
53
|
+
if (hasCategory(criteria, "design") || includesAny(text, ["frontend", "ui", "ux", "drawer", "onboarding", "layout", "mobile"])) {
|
|
54
|
+
uiScore.score += keywordWeight;
|
|
55
|
+
uiScore.rationale.push("Issue signals UI/UX work that needs stronger product-behavior and visual scrutiny.");
|
|
56
|
+
}
|
|
57
|
+
const workflowScore = scores.find((entry) => entry.name === "workflow-fsm");
|
|
58
|
+
if (hasPath(paths, /src\/persistence\/plugins\/fsm-|src\/commands\/|src\/domains\/issues\.ts|src\/agents\//)) {
|
|
59
|
+
workflowScore.score += pathWeight;
|
|
60
|
+
workflowScore.rationale.push("Touched workflow/FSM/orchestration code where lifecycle invariants and retry semantics are fragile.");
|
|
61
|
+
}
|
|
62
|
+
if (includesAny(text, ["fsm", "workflow", "queue", "review gate", "lifecycle", "orchestration", "agent"])) {
|
|
63
|
+
workflowScore.score += keywordWeight;
|
|
64
|
+
workflowScore.rationale.push("Issue description or labels indicate orchestration semantics rather than isolated implementation.");
|
|
65
|
+
}
|
|
66
|
+
const integrationScore = scores.find((entry) => entry.name === "integration-safety");
|
|
67
|
+
if (hasPath(paths, /workspace|merge|push|rebase|git|dirty-tracker|services?|store\.ts/)) {
|
|
68
|
+
integrationScore.score += pathWeight;
|
|
69
|
+
integrationScore.rationale.push("Touched integration or git/workspace code where destructive behavior and state drift must be caught.");
|
|
70
|
+
}
|
|
71
|
+
if (hasCategory(criteria, "integration") || hasCategory(criteria, "regression")) {
|
|
72
|
+
integrationScore.score += broadKeywordWeight;
|
|
73
|
+
integrationScore.rationale.push("Acceptance criteria explicitly call out integration or regression guarantees.");
|
|
74
|
+
}
|
|
75
|
+
const apiScore = scores.find((entry) => entry.name === "api-contract");
|
|
76
|
+
if (hasPath(paths, /src\/routes\/|src\/persistence\/resources\/|src\/mcp\//)) {
|
|
77
|
+
apiScore.score += lowScope ? 2 : 4;
|
|
78
|
+
apiScore.rationale.push("Touched API/resource surface that can drift from contract or persistence schema.");
|
|
79
|
+
}
|
|
80
|
+
if (includesAny(text, ["api", "route", "http", "endpoint", "resource", "schema"])) {
|
|
81
|
+
apiScore.score += broadKeywordWeight;
|
|
82
|
+
apiScore.rationale.push("Issue language implies request/response or schema contract changes.");
|
|
83
|
+
}
|
|
84
|
+
const securityScore = scores.find((entry) => entry.name === "security-hardening");
|
|
85
|
+
if (hasCategory(criteria, "security")) {
|
|
86
|
+
securityScore.score += 5;
|
|
87
|
+
securityScore.rationale.push("Security-sensitive acceptance criteria are present and should be treated as blocking by default.");
|
|
88
|
+
} else if (includesAny(text, ["auth", "security", "token", "permission", "secret"])) {
|
|
89
|
+
securityScore.score += lowScope ? 1 : 4;
|
|
90
|
+
securityScore.rationale.push("Issue language hints at security-sensitive work.");
|
|
91
|
+
}
|
|
92
|
+
if (hasPath(paths, /auth|permission|secret|credential|shell|command-executor/)) {
|
|
93
|
+
securityScore.score += lowScope ? 1 : 3;
|
|
94
|
+
securityScore.rationale.push("Touched code paths that can introduce auth, privilege, or command-execution risk.");
|
|
95
|
+
}
|
|
96
|
+
const ranked = [...scores].sort((a, b) => b.score - a.score);
|
|
97
|
+
const primary = ranked[0];
|
|
98
|
+
const secondary = ranked.filter((entry) => entry.name !== primary.name && entry.score >= 3).slice(0, 2).map((entry) => entry.name);
|
|
99
|
+
const byName = {
|
|
100
|
+
"general-quality": {
|
|
101
|
+
focusAreas: buildFocusAreas(paths, ["Correctness under real usage", "Regression risk", "Code quality and maintainability"]),
|
|
102
|
+
failureModes: [
|
|
103
|
+
"Partial implementations that look complete but leave core behavior stubbed",
|
|
104
|
+
"Missing validation, tests, or evidence for blocking criteria",
|
|
105
|
+
"Code that technically works but introduces obvious maintainability debt"
|
|
106
|
+
],
|
|
107
|
+
evidencePriorities: [
|
|
108
|
+
"Run or inspect the most relevant validation commands",
|
|
109
|
+
"Trace the dominant code path end to end",
|
|
110
|
+
"Call out unverified assumptions explicitly instead of hand-waving them"
|
|
111
|
+
],
|
|
112
|
+
severityBias: "Bias toward FAIL when behavior is only implied rather than demonstrated."
|
|
113
|
+
},
|
|
114
|
+
"ui-polish": {
|
|
115
|
+
focusAreas: buildFocusAreas(paths, ["Primary interaction flow", "Responsive layout", "Accessibility and clarity of actions"]),
|
|
116
|
+
failureModes: [
|
|
117
|
+
"Broken or unintuitive interaction flow, especially onboarding, drawers, and primary actions",
|
|
118
|
+
"Visual regressions, overflow, spacing collapse, or inaccessible controls",
|
|
119
|
+
"Interfaces that technically render but feel unfinished or confusing in use"
|
|
120
|
+
],
|
|
121
|
+
evidencePriorities: [
|
|
122
|
+
"Navigate the affected UI and describe what users can and cannot do",
|
|
123
|
+
"Verify mobile-width and edge-state behavior, not just the happy path",
|
|
124
|
+
"Use Playwright evidence when visible behavior is part of the contract"
|
|
125
|
+
],
|
|
126
|
+
severityBias: "Treat usability breaks and visually misleading states as blocking defects, not polish nits."
|
|
127
|
+
},
|
|
128
|
+
"workflow-fsm": {
|
|
129
|
+
focusAreas: buildFocusAreas(paths, ["State transitions", "Retry semantics", "Lifecycle invariants", "Counter reset behavior"]),
|
|
130
|
+
failureModes: [
|
|
131
|
+
"Illegal transitions that bypass approval, review, or terminal-state rules",
|
|
132
|
+
"Retry and checkpoint flows that jump to the wrong phase or double-increment counters",
|
|
133
|
+
"State cleanup/reset bugs that leave stale error, checkpoint, or lifecycle metadata behind"
|
|
134
|
+
],
|
|
135
|
+
evidencePriorities: [
|
|
136
|
+
"Trace the exact state path for the critical scenario, including failure paths",
|
|
137
|
+
"Verify counters and lifecycle fields are reset or preserved intentionally",
|
|
138
|
+
"Treat ambiguous transition behavior as a defect until proven safe"
|
|
139
|
+
],
|
|
140
|
+
severityBias: "Any lifecycle inconsistency that can misroute an issue or bypass a gate is blocking."
|
|
141
|
+
},
|
|
142
|
+
"integration-safety": {
|
|
143
|
+
focusAreas: buildFocusAreas(paths, ["Git/worktree operations", "Persistence side effects", "Idempotency and cleanup"]),
|
|
144
|
+
failureModes: [
|
|
145
|
+
"Destructive workspace behavior that can delete user work or dirty target branches",
|
|
146
|
+
"Cross-system drift between runtime state, resources, and filesystem artifacts",
|
|
147
|
+
"Merge/push/service-management flows that work only in the happy path and break under dirty state"
|
|
148
|
+
],
|
|
149
|
+
evidencePriorities: [
|
|
150
|
+
"Verify failure handling, not just success path behavior",
|
|
151
|
+
"Check idempotency and cleanup paths explicitly",
|
|
152
|
+
"Call out any command or filesystem side effect that is not safely guarded"
|
|
153
|
+
],
|
|
154
|
+
severityBias: "Prefer FAIL when integration code assumes a clean environment or safe side effects without enforcing them."
|
|
155
|
+
},
|
|
156
|
+
"api-contract": {
|
|
157
|
+
focusAreas: buildFocusAreas(paths, ["Route handlers", "Resource schema", "Input/output contract"]),
|
|
158
|
+
failureModes: [
|
|
159
|
+
"HTTP/API behavior that no longer matches route or resource contract",
|
|
160
|
+
"Schema drift between persisted fields, normalization, and route responses",
|
|
161
|
+
"Missing validation or status-code mismatches that break downstream callers"
|
|
162
|
+
],
|
|
163
|
+
evidencePriorities: [
|
|
164
|
+
"Read the route/resource code and trace request-to-response behavior",
|
|
165
|
+
"Verify persisted fields, normalization, and response shape stay aligned",
|
|
166
|
+
"Treat silent contract drift as blocking even if the implementation compiles"
|
|
167
|
+
],
|
|
168
|
+
severityBias: "Contract drift is blocking because it breaks automation and downstream clients silently."
|
|
169
|
+
},
|
|
170
|
+
"security-hardening": {
|
|
171
|
+
focusAreas: buildFocusAreas(paths, ["Authorization boundaries", "Secret handling", "Shell/command safety"]),
|
|
172
|
+
failureModes: [
|
|
173
|
+
"Authorization bypass, over-broad permissions, or unsafe defaults",
|
|
174
|
+
"Leaked secrets, credentials, or unsafe command composition",
|
|
175
|
+
"Security-sensitive criteria marked as effectively optional or unverified"
|
|
176
|
+
],
|
|
177
|
+
evidencePriorities: [
|
|
178
|
+
"Look for privilege escalation and shell/filepath injection opportunities",
|
|
179
|
+
"Verify security checks with concrete evidence, not inference alone",
|
|
180
|
+
"Escalate uncertainty instead of allowing a soft PASS on security-sensitive paths"
|
|
181
|
+
],
|
|
182
|
+
severityBias: "Security uncertainty should fail closed; do not grant benefit of the doubt."
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
primary: primary.name,
|
|
187
|
+
secondary,
|
|
188
|
+
rationale: primary.rationale.length ? primary.rationale : ["Selected as the highest-risk profile based on touched code and acceptance criteria."],
|
|
189
|
+
...byName[primary.name]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/agents/harness-policy.ts
|
|
194
|
+
var HIGH_RISK_PROFILES = /* @__PURE__ */ new Set([
|
|
195
|
+
"workflow-fsm",
|
|
196
|
+
"integration-safety",
|
|
197
|
+
"api-contract",
|
|
198
|
+
"security-hardening"
|
|
199
|
+
]);
|
|
200
|
+
var HIGH_CHECKPOINT_PROFILES = /* @__PURE__ */ new Set([
|
|
201
|
+
"workflow-fsm",
|
|
202
|
+
"integration-safety",
|
|
203
|
+
"security-hardening"
|
|
204
|
+
]);
|
|
205
|
+
var ROUTE_AFFINITY = {
|
|
206
|
+
"general-quality": { claude: 2.4, codex: 1.8, gemini: 1.4 },
|
|
207
|
+
"ui-polish": { claude: 3.2, codex: 1.8, gemini: 1.6 },
|
|
208
|
+
"workflow-fsm": { codex: 3.1, claude: 2, gemini: 1 },
|
|
209
|
+
"integration-safety": { codex: 3, claude: 2, gemini: 1 },
|
|
210
|
+
"api-contract": { codex: 3.1, claude: 1.9, gemini: 1.2 },
|
|
211
|
+
"security-hardening": { claude: 2.6, codex: 2.6, gemini: 0.8 }
|
|
212
|
+
};
|
|
213
|
+
function rate(numerator, denominator) {
|
|
214
|
+
return denominator > 0 ? numerator / denominator : null;
|
|
215
|
+
}
|
|
216
|
+
function isCompletedIssue(issue) {
|
|
217
|
+
return issue.state === "Approved" || issue.state === "Merged";
|
|
218
|
+
}
|
|
219
|
+
function hadReviewRework(issue) {
|
|
220
|
+
return (issue.previousAttemptSummaries ?? []).some((summary) => summary.phase === "review");
|
|
221
|
+
}
|
|
222
|
+
function resolveEffectiveReviewProfile(issue) {
|
|
223
|
+
return issue.reviewProfile ?? deriveReviewProfile(issue);
|
|
224
|
+
}
|
|
225
|
+
function serializeReviewRouteSnapshot(route) {
|
|
226
|
+
const providerLabel = `${route.provider}${route.model ? `/${route.model}` : ""}`;
|
|
227
|
+
const effortLabel = route.reasoningEffort ? `[${route.reasoningEffort}]` : "";
|
|
228
|
+
const overlayLabel = route.overlays?.length ? `overlays:${[...route.overlays].sort().join(",")}` : "";
|
|
229
|
+
return [providerLabel, effortLabel, overlayLabel].filter(Boolean).join(" | ");
|
|
230
|
+
}
|
|
231
|
+
function buildReviewRouteKey(candidate) {
|
|
232
|
+
return serializeReviewRouteSnapshot({
|
|
233
|
+
provider: candidate.provider,
|
|
234
|
+
model: candidate.model,
|
|
235
|
+
reasoningEffort: candidate.reasoningEffort,
|
|
236
|
+
overlays: candidate.overlays ?? []
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function applyHarnessModeToPlan(plan, mode) {
|
|
240
|
+
plan.harnessMode = mode;
|
|
241
|
+
if (mode !== "contractual") {
|
|
242
|
+
plan.executionContract.checkpointPolicy = "final_only";
|
|
243
|
+
} else if (plan.executionContract.checkpointPolicy !== "checkpointed") {
|
|
244
|
+
plan.executionContract.checkpointPolicy = "final_only";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function applyCheckpointPolicyToPlan(plan, checkpointPolicy) {
|
|
248
|
+
plan.executionContract.checkpointPolicy = plan.harnessMode === "contractual" ? checkpointPolicy : "final_only";
|
|
249
|
+
}
|
|
250
|
+
function resolveLatestCompletedReviewRun(issue, scope = "final") {
|
|
251
|
+
const reviewRuns = Array.isArray(issue.reviewRuns) ? issue.reviewRuns : [];
|
|
252
|
+
const completed = reviewRuns.filter((entry) => entry.status === "completed");
|
|
253
|
+
const matchingScope = completed.filter((entry) => entry.scope === scope);
|
|
254
|
+
const pool = matchingScope.length > 0 ? matchingScope : completed;
|
|
255
|
+
if (pool.length === 0) return null;
|
|
256
|
+
return [...pool].sort((left, right) => {
|
|
257
|
+
const leftAt = Date.parse(left.completedAt ?? left.startedAt);
|
|
258
|
+
const rightAt = Date.parse(right.completedAt ?? right.startedAt);
|
|
259
|
+
if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt !== rightAt) return rightAt - leftAt;
|
|
260
|
+
if ((left.planVersion ?? 0) !== (right.planVersion ?? 0)) return (right.planVersion ?? 0) - (left.planVersion ?? 0);
|
|
261
|
+
return (right.attempt ?? 0) - (left.attempt ?? 0);
|
|
262
|
+
})[0] ?? null;
|
|
263
|
+
}
|
|
264
|
+
function resolveLatestCompletedContractNegotiationRuns(issue) {
|
|
265
|
+
const runs = Array.isArray(issue.contractNegotiationRuns) ? issue.contractNegotiationRuns : [];
|
|
266
|
+
const completed = runs.filter((entry) => entry.status === "completed");
|
|
267
|
+
if (completed.length === 0) return [];
|
|
268
|
+
const latestPlanVersion = completed.reduce((maxPlanVersion, entry) => Math.max(maxPlanVersion, entry.planVersion ?? 0), 0);
|
|
269
|
+
return completed.filter((entry) => (entry.planVersion ?? 0) === latestPlanVersion).sort((left, right) => {
|
|
270
|
+
if ((left.attempt ?? 0) !== (right.attempt ?? 0)) return (left.attempt ?? 0) - (right.attempt ?? 0);
|
|
271
|
+
const leftAt = Date.parse(left.completedAt ?? left.startedAt);
|
|
272
|
+
const rightAt = Date.parse(right.completedAt ?? right.startedAt);
|
|
273
|
+
if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt !== rightAt) return leftAt - rightAt;
|
|
274
|
+
return left.id.localeCompare(right.id);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function resolveLatestCompletedScopedReviewRuns(issue, scope) {
|
|
278
|
+
const reviewRuns = Array.isArray(issue.reviewRuns) ? issue.reviewRuns : [];
|
|
279
|
+
const completed = reviewRuns.filter((entry) => entry.status === "completed" && entry.scope === scope);
|
|
280
|
+
if (completed.length === 0) return [];
|
|
281
|
+
const latestPlanVersion = completed.reduce((maxPlanVersion, entry) => Math.max(maxPlanVersion, entry.planVersion ?? 0), 0);
|
|
282
|
+
return completed.filter((entry) => (entry.planVersion ?? 0) === latestPlanVersion).sort((left, right) => {
|
|
283
|
+
if ((left.attempt ?? 0) !== (right.attempt ?? 0)) return (left.attempt ?? 0) - (right.attempt ?? 0);
|
|
284
|
+
const leftAt = Date.parse(left.completedAt ?? left.startedAt);
|
|
285
|
+
const rightAt = Date.parse(right.completedAt ?? right.startedAt);
|
|
286
|
+
if (!Number.isNaN(leftAt) && !Number.isNaN(rightAt) && leftAt !== rightAt) return leftAt - rightAt;
|
|
287
|
+
return left.id.localeCompare(right.id);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function computeHarnessModeStats(issues, profileName) {
|
|
291
|
+
const buckets = {
|
|
292
|
+
solo: { reviewedIssues: 0, completedReviewedIssues: 0, gatePasses: 0, firstPassPasses: 0, reworkIssues: 0 },
|
|
293
|
+
standard: { reviewedIssues: 0, completedReviewedIssues: 0, gatePasses: 0, firstPassPasses: 0, reworkIssues: 0 },
|
|
294
|
+
contractual: { reviewedIssues: 0, completedReviewedIssues: 0, gatePasses: 0, firstPassPasses: 0, reworkIssues: 0 }
|
|
295
|
+
};
|
|
296
|
+
for (const issue of issues) {
|
|
297
|
+
const reviewRun = resolveLatestCompletedReviewRun(issue, "final");
|
|
298
|
+
if (!reviewRun) continue;
|
|
299
|
+
const effectiveProfile = reviewRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
300
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
301
|
+
const mode = issue.plan?.harnessMode ?? "standard";
|
|
302
|
+
const bucket = buckets[mode];
|
|
303
|
+
bucket.reviewedIssues += 1;
|
|
304
|
+
if (isCompletedIssue(issue)) bucket.completedReviewedIssues += 1;
|
|
305
|
+
if (reviewRun.blockingVerdict === "PASS") bucket.gatePasses += 1;
|
|
306
|
+
if ((issue.reviewAttempt ?? 0) <= 1 && !hadReviewRework(issue) && isCompletedIssue(issue)) bucket.firstPassPasses += 1;
|
|
307
|
+
if (hadReviewRework(issue)) bucket.reworkIssues += 1;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
solo: {
|
|
311
|
+
...buckets.solo,
|
|
312
|
+
gatePassRate: rate(buckets.solo.gatePasses, buckets.solo.reviewedIssues),
|
|
313
|
+
firstPassPassRate: rate(buckets.solo.firstPassPasses, buckets.solo.completedReviewedIssues),
|
|
314
|
+
reviewReworkRate: rate(buckets.solo.reworkIssues, buckets.solo.reviewedIssues)
|
|
315
|
+
},
|
|
316
|
+
standard: {
|
|
317
|
+
...buckets.standard,
|
|
318
|
+
gatePassRate: rate(buckets.standard.gatePasses, buckets.standard.reviewedIssues),
|
|
319
|
+
firstPassPassRate: rate(buckets.standard.firstPassPasses, buckets.standard.completedReviewedIssues),
|
|
320
|
+
reviewReworkRate: rate(buckets.standard.reworkIssues, buckets.standard.reviewedIssues)
|
|
321
|
+
},
|
|
322
|
+
contractual: {
|
|
323
|
+
...buckets.contractual,
|
|
324
|
+
gatePassRate: rate(buckets.contractual.gatePasses, buckets.contractual.reviewedIssues),
|
|
325
|
+
firstPassPassRate: rate(buckets.contractual.firstPassPasses, buckets.contractual.completedReviewedIssues),
|
|
326
|
+
reviewReworkRate: rate(buckets.contractual.reworkIssues, buckets.contractual.reviewedIssues)
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function computeCheckpointPolicyStats(issues, profileName) {
|
|
331
|
+
const buckets = {
|
|
332
|
+
final_only: {
|
|
333
|
+
reviewedIssues: 0,
|
|
334
|
+
completedReviewedIssues: 0,
|
|
335
|
+
gatePasses: 0,
|
|
336
|
+
firstPassPasses: 0,
|
|
337
|
+
reworkIssues: 0,
|
|
338
|
+
checkpointFailures: 0,
|
|
339
|
+
checkpointPasses: 0,
|
|
340
|
+
checkpointRuns: 0
|
|
341
|
+
},
|
|
342
|
+
checkpointed: {
|
|
343
|
+
reviewedIssues: 0,
|
|
344
|
+
completedReviewedIssues: 0,
|
|
345
|
+
gatePasses: 0,
|
|
346
|
+
firstPassPasses: 0,
|
|
347
|
+
reworkIssues: 0,
|
|
348
|
+
checkpointFailures: 0,
|
|
349
|
+
checkpointPasses: 0,
|
|
350
|
+
checkpointRuns: 0
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
for (const issue of issues) {
|
|
354
|
+
if ((issue.plan?.harnessMode ?? "standard") !== "contractual") continue;
|
|
355
|
+
const finalReviewRun = resolveLatestCompletedReviewRun(issue, "final");
|
|
356
|
+
if (!finalReviewRun) continue;
|
|
357
|
+
const effectiveProfile = finalReviewRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
358
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
359
|
+
const checkpointPolicy = issue.plan?.executionContract?.checkpointPolicy === "checkpointed" ? "checkpointed" : "final_only";
|
|
360
|
+
const bucket = buckets[checkpointPolicy];
|
|
361
|
+
bucket.reviewedIssues += 1;
|
|
362
|
+
if (isCompletedIssue(issue)) bucket.completedReviewedIssues += 1;
|
|
363
|
+
if (finalReviewRun.blockingVerdict === "PASS") bucket.gatePasses += 1;
|
|
364
|
+
if ((issue.reviewAttempt ?? 0) <= 1 && !hadReviewRework(issue) && isCompletedIssue(issue)) bucket.firstPassPasses += 1;
|
|
365
|
+
if (hadReviewRework(issue)) bucket.reworkIssues += 1;
|
|
366
|
+
if (checkpointPolicy === "checkpointed") {
|
|
367
|
+
const checkpointRuns = resolveLatestCompletedScopedReviewRuns(issue, "checkpoint");
|
|
368
|
+
bucket.checkpointRuns += checkpointRuns.length;
|
|
369
|
+
if (checkpointRuns.some((entry) => entry.blockingVerdict === "FAIL")) bucket.checkpointFailures += 1;
|
|
370
|
+
if (checkpointRuns.some((entry) => entry.blockingVerdict === "PASS")) bucket.checkpointPasses += 1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
final_only: {
|
|
375
|
+
...buckets.final_only,
|
|
376
|
+
gatePassRate: rate(buckets.final_only.gatePasses, buckets.final_only.reviewedIssues),
|
|
377
|
+
firstPassPassRate: rate(buckets.final_only.firstPassPasses, buckets.final_only.completedReviewedIssues),
|
|
378
|
+
reviewReworkRate: rate(buckets.final_only.reworkIssues, buckets.final_only.reviewedIssues),
|
|
379
|
+
checkpointFailureRate: rate(buckets.final_only.checkpointFailures, buckets.final_only.reviewedIssues),
|
|
380
|
+
checkpointPassRate: rate(buckets.final_only.checkpointPasses, buckets.final_only.reviewedIssues),
|
|
381
|
+
avgCheckpointRunsPerIssue: rate(buckets.final_only.checkpointRuns, buckets.final_only.reviewedIssues)
|
|
382
|
+
},
|
|
383
|
+
checkpointed: {
|
|
384
|
+
...buckets.checkpointed,
|
|
385
|
+
gatePassRate: rate(buckets.checkpointed.gatePasses, buckets.checkpointed.reviewedIssues),
|
|
386
|
+
firstPassPassRate: rate(buckets.checkpointed.firstPassPasses, buckets.checkpointed.completedReviewedIssues),
|
|
387
|
+
reviewReworkRate: rate(buckets.checkpointed.reworkIssues, buckets.checkpointed.reviewedIssues),
|
|
388
|
+
checkpointFailureRate: rate(buckets.checkpointed.checkpointFailures, buckets.checkpointed.reviewedIssues),
|
|
389
|
+
checkpointPassRate: rate(buckets.checkpointed.checkpointPasses, buckets.checkpointed.reviewedIssues),
|
|
390
|
+
avgCheckpointRunsPerIssue: rate(buckets.checkpointed.checkpointRuns, buckets.checkpointed.reviewedIssues)
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function computeContractNegotiationStats(issues, profileName) {
|
|
395
|
+
const bucket = {
|
|
396
|
+
negotiatedIssues: 0,
|
|
397
|
+
approvedIssues: 0,
|
|
398
|
+
firstPassApprovals: 0,
|
|
399
|
+
revisedIssues: 0,
|
|
400
|
+
blockingConcernIssues: 0,
|
|
401
|
+
totalRounds: 0
|
|
402
|
+
};
|
|
403
|
+
for (const issue of issues) {
|
|
404
|
+
const planRuns = resolveLatestCompletedContractNegotiationRuns(issue);
|
|
405
|
+
if (planRuns.length === 0) continue;
|
|
406
|
+
const latestRun = planRuns[planRuns.length - 1];
|
|
407
|
+
const effectiveProfile = latestRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
408
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
409
|
+
bucket.negotiatedIssues += 1;
|
|
410
|
+
bucket.totalRounds += planRuns.length;
|
|
411
|
+
if (latestRun.decisionStatus === "approved") bucket.approvedIssues += 1;
|
|
412
|
+
if (planRuns.length === 1 && planRuns[0]?.decisionStatus === "approved") bucket.firstPassApprovals += 1;
|
|
413
|
+
if (planRuns.some((entry) => entry.decisionStatus === "revise" || entry.appliedRefinement)) bucket.revisedIssues += 1;
|
|
414
|
+
if (planRuns.some((entry) => (entry.blockingConcernsCount ?? 0) > 0)) bucket.blockingConcernIssues += 1;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...bucket,
|
|
418
|
+
approvalRate: rate(bucket.approvedIssues, bucket.negotiatedIssues),
|
|
419
|
+
firstPassApprovalRate: rate(bucket.firstPassApprovals, bucket.negotiatedIssues),
|
|
420
|
+
revisionRate: rate(bucket.revisedIssues, bucket.negotiatedIssues),
|
|
421
|
+
blockingConcernRate: rate(bucket.blockingConcernIssues, bucket.negotiatedIssues),
|
|
422
|
+
avgRoundsPerIssue: rate(bucket.totalRounds, bucket.negotiatedIssues)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function recommendCheckpointPolicyForIssue(issues, issue, currentCheckpointPolicy, minSamples = 3) {
|
|
426
|
+
if (issue.plan?.harnessMode !== "contractual") {
|
|
427
|
+
if (currentCheckpointPolicy !== "final_only") {
|
|
428
|
+
const profile2 = resolveEffectiveReviewProfile(issue);
|
|
429
|
+
return {
|
|
430
|
+
checkpointPolicy: "final_only",
|
|
431
|
+
profile: profile2,
|
|
432
|
+
basis: "heuristic",
|
|
433
|
+
rationale: "Non-contractual plans must not request checkpoint review."
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const profile = resolveEffectiveReviewProfile(issue);
|
|
439
|
+
const complexity = issue.plan?.estimatedComplexity ?? "medium";
|
|
440
|
+
const lowScope = complexity === "trivial" || complexity === "low";
|
|
441
|
+
if (lowScope) {
|
|
442
|
+
if (currentCheckpointPolicy !== "final_only") {
|
|
443
|
+
return {
|
|
444
|
+
checkpointPolicy: "final_only",
|
|
445
|
+
profile,
|
|
446
|
+
basis: "heuristic",
|
|
447
|
+
rationale: `Disabled checkpoint review because ${complexity} complexity does not warrant an intermediate review pass.`
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const highCheckpointRisk = HIGH_CHECKPOINT_PROFILES.has(profile.primary);
|
|
453
|
+
const stats = computeCheckpointPolicyStats(issues, profile.primary);
|
|
454
|
+
const finalOnly = stats.final_only;
|
|
455
|
+
const checkpointed = stats.checkpointed;
|
|
456
|
+
const checkpointedSamplesReady = checkpointed.reviewedIssues >= minSamples;
|
|
457
|
+
const finalOnlySamplesReady = finalOnly.reviewedIssues >= minSamples;
|
|
458
|
+
if (currentCheckpointPolicy !== "checkpointed" && highCheckpointRisk) {
|
|
459
|
+
if (checkpointedSamplesReady && (checkpointed.checkpointFailureRate ?? 0) >= 0.15) {
|
|
460
|
+
return {
|
|
461
|
+
checkpointPolicy: "checkpointed",
|
|
462
|
+
profile,
|
|
463
|
+
basis: "historical",
|
|
464
|
+
rationale: `Enabled checkpoint review for ${profile.primary}: checkpointed runs caught blocking issues before final review in ${Math.round((checkpointed.checkpointFailureRate ?? 0) * 100)}% of ${checkpointed.reviewedIssues} comparable issue(s).`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (!checkpointedSamplesReady) {
|
|
468
|
+
return {
|
|
469
|
+
checkpointPolicy: "checkpointed",
|
|
470
|
+
profile,
|
|
471
|
+
basis: "heuristic",
|
|
472
|
+
rationale: `Enabled checkpoint review because ${profile.primary} changes are high-risk enough to benefit from an intermediate gate before final review.`
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (checkpointedSamplesReady && finalOnlySamplesReady) {
|
|
477
|
+
const gateLift = (checkpointed.gatePassRate ?? 0) - (finalOnly.gatePassRate ?? 0);
|
|
478
|
+
const firstPassLift = (checkpointed.firstPassPassRate ?? 0) - (finalOnly.firstPassPassRate ?? 0);
|
|
479
|
+
const checkpointCatchRate = checkpointed.checkpointFailureRate ?? 0;
|
|
480
|
+
if (currentCheckpointPolicy !== "checkpointed" && (checkpointCatchRate >= 0.18 || gateLift >= 0.08 || firstPassLift >= 0.1)) {
|
|
481
|
+
return {
|
|
482
|
+
checkpointPolicy: "checkpointed",
|
|
483
|
+
profile,
|
|
484
|
+
basis: "historical",
|
|
485
|
+
rationale: `Enabled checkpoint review for ${profile.primary}: checkpointed runs show ${Math.round(checkpointCatchRate * 100)}% checkpoint catch rate, ${Math.round(gateLift * 100)}pp final gate lift, and ${Math.round(firstPassLift * 100)}pp first-pass lift over final-only contractual runs.`
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (currentCheckpointPolicy === "checkpointed" && !highCheckpointRisk && checkpointCatchRate <= 0.05 && (finalOnly.gatePassRate ?? 0) >= (checkpointed.gatePassRate ?? 0) - 0.05 && (finalOnly.firstPassPassRate ?? 0) >= (checkpointed.firstPassPassRate ?? 0) - 0.05) {
|
|
489
|
+
return {
|
|
490
|
+
checkpointPolicy: "final_only",
|
|
491
|
+
profile,
|
|
492
|
+
basis: "historical",
|
|
493
|
+
rationale: `Disabled checkpoint review for ${profile.primary}: checkpointed runs almost never catch issues before final review (${Math.round(checkpointCatchRate * 100)}%), while final-only contractual runs stay within 5pp on final gate and first-pass outcomes.`
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
function recommendHarnessModeForIssue(issues, issue, currentMode, minSamples = 3) {
|
|
500
|
+
const profile = resolveEffectiveReviewProfile(issue);
|
|
501
|
+
const complexity = issue.plan?.estimatedComplexity ?? "medium";
|
|
502
|
+
const stats = computeHarnessModeStats(issues, profile.primary);
|
|
503
|
+
const negotiation = computeContractNegotiationStats(issues, profile.primary);
|
|
504
|
+
const highRisk = HIGH_RISK_PROFILES.has(profile.primary);
|
|
505
|
+
const lowScope = complexity === "trivial" || complexity === "low";
|
|
506
|
+
const negotiationSamplesReady = negotiation.negotiatedIssues >= minSamples;
|
|
507
|
+
const highRiskNegotiationPressure = negotiationSamplesReady && ((negotiation.blockingConcernRate ?? 0) >= 0.15 || (negotiation.revisionRate ?? 0) >= 0.3);
|
|
508
|
+
const generalNegotiationPressure = negotiationSamplesReady && !lowScope && ((negotiation.blockingConcernRate ?? 0) >= 0.25 || (negotiation.revisionRate ?? 0) >= 0.4 || (negotiation.avgRoundsPerIssue ?? 0) >= 1.6);
|
|
509
|
+
const negotiationLowValue = negotiationSamplesReady && (negotiation.firstPassApprovalRate ?? 0) >= 0.9 && (negotiation.blockingConcernRate ?? 1) <= 0.08 && (negotiation.revisionRate ?? 1) <= 0.15;
|
|
510
|
+
if (highRisk && currentMode !== "contractual") {
|
|
511
|
+
if (lowScope) {
|
|
512
|
+
if (currentMode === "solo") {
|
|
513
|
+
return {
|
|
514
|
+
mode: "standard",
|
|
515
|
+
profile,
|
|
516
|
+
basis: "heuristic",
|
|
517
|
+
rationale: `Upgraded from solo to standard for ${profile.primary} (high-risk profile), but kept lightweight because complexity is ${complexity}.`
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
if (highRiskNegotiationPressure) {
|
|
523
|
+
return {
|
|
524
|
+
mode: "contractual",
|
|
525
|
+
profile,
|
|
526
|
+
basis: "historical",
|
|
527
|
+
rationale: `Switched to contractual for ${profile.primary}: contract negotiation found blocking concerns in ${Math.round((negotiation.blockingConcernRate ?? 0) * 100)}% of ${negotiation.negotiatedIssues} comparable issue(s) and forced revisions in ${Math.round((negotiation.revisionRate ?? 0) * 100)}%.`
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const contractual2 = stats.contractual;
|
|
531
|
+
if (contractual2.reviewedIssues >= minSamples) {
|
|
532
|
+
return {
|
|
533
|
+
mode: "contractual",
|
|
534
|
+
profile,
|
|
535
|
+
basis: "historical",
|
|
536
|
+
rationale: `Switched to contractual for ${profile.primary}: historical gate pass ${Math.round((contractual2.gatePassRate ?? 0) * 100)}% across ${contractual2.reviewedIssues} reviewed issue(s).`
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
mode: "contractual",
|
|
541
|
+
profile,
|
|
542
|
+
basis: "heuristic",
|
|
543
|
+
rationale: `Switched to contractual because ${profile.primary} is a high-risk profile and needs stronger contract negotiation plus skeptical review semantics.`
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (profile.primary === "general-quality" && lowScope) {
|
|
547
|
+
const solo = stats.solo;
|
|
548
|
+
if (solo.reviewedIssues >= minSamples && (solo.gatePassRate ?? 0) >= 0.95 && (solo.reviewReworkRate ?? 1) <= 0.1) {
|
|
549
|
+
if (currentMode !== "solo") {
|
|
550
|
+
return {
|
|
551
|
+
mode: "solo",
|
|
552
|
+
profile,
|
|
553
|
+
basis: "historical",
|
|
554
|
+
rationale: `Downgraded to solo for low-scope general work: solo gate pass ${Math.round((solo.gatePassRate ?? 0) * 100)}% with low rework over ${solo.reviewedIssues} reviewed issue(s).`
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (currentMode !== "contractual" && generalNegotiationPressure) {
|
|
561
|
+
return {
|
|
562
|
+
mode: "contractual",
|
|
563
|
+
profile,
|
|
564
|
+
basis: "historical",
|
|
565
|
+
rationale: `Switched to contractual for ${profile.primary}: contract negotiation found blocking concerns in ${Math.round((negotiation.blockingConcernRate ?? 0) * 100)}% of ${negotiation.negotiatedIssues} comparable issue(s), with revisions required in ${Math.round((negotiation.revisionRate ?? 0) * 100)}% and ${Math.round((negotiation.avgRoundsPerIssue ?? 0) * 10) / 10} rounds per issue on average.`
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const standard = stats.standard;
|
|
569
|
+
const contractual = stats.contractual;
|
|
570
|
+
const contractualSamplesReady = contractual.reviewedIssues >= minSamples;
|
|
571
|
+
const standardSamplesReady = standard.reviewedIssues >= minSamples;
|
|
572
|
+
if (contractualSamplesReady && standardSamplesReady) {
|
|
573
|
+
const contractualGateLift = (contractual.gatePassRate ?? 0) - (standard.gatePassRate ?? 0);
|
|
574
|
+
const contractualFirstPassLift = (contractual.firstPassPassRate ?? 0) - (standard.firstPassPassRate ?? 0);
|
|
575
|
+
if (currentMode !== "contractual" && (contractualGateLift >= 0.12 || contractualFirstPassLift >= 0.15)) {
|
|
576
|
+
return {
|
|
577
|
+
mode: "contractual",
|
|
578
|
+
profile,
|
|
579
|
+
basis: "historical",
|
|
580
|
+
rationale: `Switched to contractual for ${profile.primary}: first-pass lift ${Math.round(contractualFirstPassLift * 100)}pp and gate lift ${Math.round(contractualGateLift * 100)}pp over standard.`
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (currentMode === "contractual" && !highRisk && !lowScope && negotiationLowValue && (standard.gatePassRate ?? 0) >= (contractual.gatePassRate ?? 0) - 0.05 && (standard.firstPassPassRate ?? 0) >= (contractual.firstPassPassRate ?? 0) - 0.05) {
|
|
584
|
+
return {
|
|
585
|
+
mode: "standard",
|
|
586
|
+
profile,
|
|
587
|
+
basis: "historical",
|
|
588
|
+
rationale: `Downgraded to standard for ${profile.primary}: contract negotiation approved on first pass in ${Math.round((negotiation.firstPassApprovalRate ?? 0) * 100)}% of ${negotiation.negotiatedIssues} comparable issue(s), and standard review performance stays within 5pp of contractual.`
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!highRisk && currentMode === "solo" && !lowScope) {
|
|
593
|
+
return {
|
|
594
|
+
mode: "standard",
|
|
595
|
+
profile,
|
|
596
|
+
basis: "heuristic",
|
|
597
|
+
rationale: `Upgraded from solo to standard because ${complexity} complexity should keep an automated reviewer in the loop.`
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function computeReviewRouteStats(issues, profileName) {
|
|
603
|
+
const buckets = {};
|
|
604
|
+
for (const issue of issues) {
|
|
605
|
+
const reviewRun = resolveLatestCompletedReviewRun(issue, "final");
|
|
606
|
+
if (!reviewRun) continue;
|
|
607
|
+
const effectiveProfile = reviewRun.reviewProfile ?? resolveEffectiveReviewProfile(issue);
|
|
608
|
+
if (effectiveProfile.primary !== profileName) continue;
|
|
609
|
+
const routeKey = serializeReviewRouteSnapshot(reviewRun.routing);
|
|
610
|
+
const bucket = buckets[routeKey] ||= {
|
|
611
|
+
reviewedIssues: 0,
|
|
612
|
+
completedReviewedIssues: 0,
|
|
613
|
+
gatePasses: 0,
|
|
614
|
+
blockingFailRuns: 0,
|
|
615
|
+
advisoryFailRuns: 0
|
|
616
|
+
};
|
|
617
|
+
bucket.reviewedIssues += 1;
|
|
618
|
+
if (isCompletedIssue(issue)) bucket.completedReviewedIssues += 1;
|
|
619
|
+
if (reviewRun.blockingVerdict === "PASS") bucket.gatePasses += 1;
|
|
620
|
+
if (reviewRun.blockingVerdict === "FAIL") bucket.blockingFailRuns += 1;
|
|
621
|
+
if ((reviewRun.advisoryFailedCriteriaCount ?? 0) > 0) bucket.advisoryFailRuns += 1;
|
|
622
|
+
}
|
|
623
|
+
return Object.fromEntries(
|
|
624
|
+
Object.entries(buckets).map(([routeKey, bucket]) => [
|
|
625
|
+
routeKey,
|
|
626
|
+
{
|
|
627
|
+
...bucket,
|
|
628
|
+
gatePassRate: rate(bucket.gatePasses, bucket.reviewedIssues),
|
|
629
|
+
blockingFailRate: rate(bucket.blockingFailRuns, bucket.reviewedIssues)
|
|
630
|
+
}
|
|
631
|
+
])
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
function recommendReviewRouteForIssue(issues, issue, candidates, minSamples = 3) {
|
|
635
|
+
if (candidates.length === 0) return null;
|
|
636
|
+
const profile = resolveEffectiveReviewProfile(issue);
|
|
637
|
+
const routeStats = computeReviewRouteStats(issues, profile.primary);
|
|
638
|
+
const scored = candidates.map((candidate) => {
|
|
639
|
+
const routeKey = buildReviewRouteKey(candidate);
|
|
640
|
+
const stats = routeStats[routeKey];
|
|
641
|
+
const affinity = ROUTE_AFFINITY[profile.primary][candidate.provider] ?? 0;
|
|
642
|
+
const historicalScore = stats ? (stats.gatePassRate ?? 0) * 4 - (stats.blockingFailRate ?? 0) * 3 + Math.min(stats.reviewedIssues, 6) * 0.15 : 0;
|
|
643
|
+
return {
|
|
644
|
+
candidate,
|
|
645
|
+
routeKey,
|
|
646
|
+
stats,
|
|
647
|
+
score: affinity + historicalScore,
|
|
648
|
+
affinity
|
|
649
|
+
};
|
|
650
|
+
}).sort((left, right) => {
|
|
651
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
652
|
+
return left.routeKey.localeCompare(right.routeKey);
|
|
653
|
+
});
|
|
654
|
+
const current = scored[0];
|
|
655
|
+
if (!current) return null;
|
|
656
|
+
if (current.stats && current.stats.reviewedIssues >= minSamples) {
|
|
657
|
+
return {
|
|
658
|
+
candidate: current.candidate,
|
|
659
|
+
profile,
|
|
660
|
+
basis: "historical",
|
|
661
|
+
rationale: `Adaptive reviewer route for ${profile.primary}: ${current.routeKey} has ${Math.round((current.stats.gatePassRate ?? 0) * 100)}% gate pass over ${current.stats.reviewedIssues} reviewed issue(s).`
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
candidate: current.candidate,
|
|
666
|
+
profile,
|
|
667
|
+
basis: "heuristic",
|
|
668
|
+
rationale: `Adaptive reviewer route for ${profile.primary}: preferred ${current.candidate.provider} based on profile affinity while historical samples are still sparse.`
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export {
|
|
673
|
+
deriveReviewProfile,
|
|
674
|
+
serializeReviewRouteSnapshot,
|
|
675
|
+
buildReviewRouteKey,
|
|
676
|
+
applyHarnessModeToPlan,
|
|
677
|
+
applyCheckpointPolicyToPlan,
|
|
678
|
+
recommendCheckpointPolicyForIssue,
|
|
679
|
+
recommendHarnessModeForIssue,
|
|
680
|
+
recommendReviewRouteForIssue
|
|
681
|
+
};
|
|
682
|
+
//# sourceMappingURL=chunk-EBCSQFPR.js.map
|