@humanbased/crosscheck 1.3.0-beta.91 → 1.3.0-beta.93
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/README.md +1 -1
- package/crosscheck.config.example.yml +9 -0
- package/dist/__tests__/adoption.test.d.ts +2 -0
- package/dist/__tests__/adoption.test.d.ts.map +1 -0
- package/dist/__tests__/adoption.test.js +175 -0
- package/dist/__tests__/adoption.test.js.map +1 -0
- package/dist/__tests__/detector.test.js +87 -1
- package/dist/__tests__/detector.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +161 -1
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adoption.d.ts +6 -0
- package/dist/commands/adoption.d.ts.map +1 -0
- package/dist/commands/adoption.js +132 -0
- package/dist/commands/adoption.js.map +1 -0
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +30 -3
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +30 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +4 -0
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +7 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/github/detector.d.ts.map +1 -1
- package/dist/github/detector.js +28 -1
- package/dist/github/detector.js.map +1 -1
- package/dist/github/webhook.d.ts +1 -0
- package/dist/github/webhook.d.ts.map +1 -1
- package/dist/github/webhook.js.map +1 -1
- package/dist/lib/adoption.d.ts +64 -0
- package/dist/lib/adoption.d.ts.map +1 -0
- package/dist/lib/adoption.js +165 -0
- package/dist/lib/adoption.js.map +1 -0
- package/dist/lib/runner.d.ts +24 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +115 -16
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/workflow.d.ts +4 -0
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js.map +1 -1
- package/docs/metrics.md +115 -0
- package/get-started.md +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Adoption metrics answer "is crosscheck actually being used, and does it reach a
|
|
2
|
+
// verdict fast enough to matter" — a different question from `impact`, which prices
|
|
3
|
+
// the value of reviews that already happened.
|
|
4
|
+
//
|
|
5
|
+
// Every number here is derived from the local NDJSON logs in ~/.crosscheck/logs.
|
|
6
|
+
// Nothing is transmitted: there is no endpoint, no account, and no network call in
|
|
7
|
+
// this file. See docs/metrics.md for the field-by-field inventory.
|
|
8
|
+
// A verdict-bearing latency is only meaningful for a review that produced one; a
|
|
9
|
+
// verdictless review (from `crosscheck review`) has nothing to time to.
|
|
10
|
+
export function prOpenToVerdictMs(createdAt, verdict, now = Date.now()) {
|
|
11
|
+
if (verdict === null || createdAt === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const opened = new Date(createdAt).getTime();
|
|
14
|
+
if (Number.isNaN(opened))
|
|
15
|
+
return undefined;
|
|
16
|
+
const elapsed = now - opened;
|
|
17
|
+
// A negative elapsed time means the clocks disagree, not that the PR was
|
|
18
|
+
// reviewed before it existed. Drop it rather than publish a nonsense number.
|
|
19
|
+
return elapsed >= 0 ? elapsed : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function isoWeekMonday(ts) {
|
|
22
|
+
const d = new Date(ts);
|
|
23
|
+
if (Number.isNaN(d.getTime()))
|
|
24
|
+
return null;
|
|
25
|
+
const day = d.getUTCDay();
|
|
26
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
27
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + diff))
|
|
28
|
+
.toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
export function percentile(sorted, fraction) {
|
|
31
|
+
if (sorted.length === 0)
|
|
32
|
+
return 0;
|
|
33
|
+
// Nearest-rank on the sorted sample: no interpolation, so every reported value
|
|
34
|
+
// is one that actually occurred.
|
|
35
|
+
const rank = Math.ceil(fraction * sorted.length) - 1;
|
|
36
|
+
return sorted[Math.min(Math.max(rank, 0), sorted.length - 1)];
|
|
37
|
+
}
|
|
38
|
+
export function buildAdoptionReport(lines, period) {
|
|
39
|
+
const weekly = new Map();
|
|
40
|
+
const activeRepos = new Set();
|
|
41
|
+
const latencies = [];
|
|
42
|
+
let unmeasured = 0;
|
|
43
|
+
const counts = {
|
|
44
|
+
onboard_started: 0,
|
|
45
|
+
onboard_completed: 0,
|
|
46
|
+
reviews_started: 0,
|
|
47
|
+
reviews_completed: 0,
|
|
48
|
+
rechecks_completed: 0,
|
|
49
|
+
blocking: 0,
|
|
50
|
+
fixes: 0,
|
|
51
|
+
};
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
switch (line.event) {
|
|
54
|
+
case 'onboard_started':
|
|
55
|
+
counts.onboard_started++;
|
|
56
|
+
break;
|
|
57
|
+
case 'onboard_completed':
|
|
58
|
+
// The event is emitted for both outcomes so an abandoned setup is
|
|
59
|
+
// visible; only a success counts as completed.
|
|
60
|
+
if (line.outcome === 'completed')
|
|
61
|
+
counts.onboard_completed++;
|
|
62
|
+
break;
|
|
63
|
+
case 'review_started':
|
|
64
|
+
counts.reviews_started++;
|
|
65
|
+
break;
|
|
66
|
+
case 'review_complete': {
|
|
67
|
+
counts.reviews_completed++;
|
|
68
|
+
if (line.step_type === 'recheck')
|
|
69
|
+
counts.rechecks_completed++;
|
|
70
|
+
if (line.repo) {
|
|
71
|
+
activeRepos.add(line.repo);
|
|
72
|
+
const week = isoWeekMonday(line.ts);
|
|
73
|
+
if (week) {
|
|
74
|
+
const bucket = weekly.get(week) ?? { repos: new Set(), reviews: 0 };
|
|
75
|
+
bucket.repos.add(line.repo);
|
|
76
|
+
bucket.reviews++;
|
|
77
|
+
weekly.set(week, bucket);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (typeof line.open_to_verdict_ms === 'number')
|
|
81
|
+
latencies.push(line.open_to_verdict_ms);
|
|
82
|
+
else if (line.verdict)
|
|
83
|
+
unmeasured++;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case 'blocking_finding_posted':
|
|
87
|
+
counts.blocking++;
|
|
88
|
+
break;
|
|
89
|
+
case 'fix_complete':
|
|
90
|
+
// delivery: 'comment' still counts — a suggestion the author applied is
|
|
91
|
+
// adoption. A no-op fix (applied_count 0) is not.
|
|
92
|
+
if ((line.applied_count ?? 0) > 0)
|
|
93
|
+
counts.fixes++;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
latencies.sort((a, b) => a - b);
|
|
98
|
+
return {
|
|
99
|
+
period,
|
|
100
|
+
onboarding: {
|
|
101
|
+
started: counts.onboard_started,
|
|
102
|
+
completed: counts.onboard_completed,
|
|
103
|
+
abandoned: Math.max(0, counts.onboard_started - counts.onboard_completed),
|
|
104
|
+
},
|
|
105
|
+
activity: {
|
|
106
|
+
reviews_started: counts.reviews_started,
|
|
107
|
+
reviews_completed: counts.reviews_completed,
|
|
108
|
+
rechecks_completed: counts.rechecks_completed,
|
|
109
|
+
blocking_findings_posted: counts.blocking,
|
|
110
|
+
fixes_applied: counts.fixes,
|
|
111
|
+
active_repos: activeRepos.size,
|
|
112
|
+
},
|
|
113
|
+
weekly: [...weekly.entries()]
|
|
114
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
115
|
+
.map(([week, { repos, reviews }]) => ({ week, active_repos: repos.size, reviews })),
|
|
116
|
+
open_to_verdict: {
|
|
117
|
+
count: latencies.length,
|
|
118
|
+
p50_ms: percentile(latencies, 0.5),
|
|
119
|
+
p90_ms: percentile(latencies, 0.9),
|
|
120
|
+
max_ms: latencies.at(-1) ?? 0,
|
|
121
|
+
unmeasured,
|
|
122
|
+
},
|
|
123
|
+
first_run_failures: firstRunFailures(lines),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// "First run" is a session that never completed a review. Its first error is the
|
|
127
|
+
// one that stopped a new install from reaching a verdict — the category worth
|
|
128
|
+
// fixing. Sessions that did complete a review are excluded even if they later
|
|
129
|
+
// errored: those are operational failures, not activation failures.
|
|
130
|
+
export function firstRunFailures(lines) {
|
|
131
|
+
const sessions = [];
|
|
132
|
+
let current = null;
|
|
133
|
+
const close = () => {
|
|
134
|
+
if (current)
|
|
135
|
+
sessions.push(current);
|
|
136
|
+
current = null;
|
|
137
|
+
};
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
if (line.event === 'session_start') {
|
|
140
|
+
close();
|
|
141
|
+
current = { completedReview: false, firstErrorCategory: null };
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Logs from before session events existed, or a truncated file, would
|
|
145
|
+
// otherwise drop every entry on the floor.
|
|
146
|
+
if (current === null)
|
|
147
|
+
current = { completedReview: false, firstErrorCategory: null };
|
|
148
|
+
if (line.event === 'review_complete')
|
|
149
|
+
current.completedReview = true;
|
|
150
|
+
if (line.event === 'error' && current.firstErrorCategory === null) {
|
|
151
|
+
current.firstErrorCategory = line.category ?? 'unknown';
|
|
152
|
+
}
|
|
153
|
+
if (line.event === 'session_end')
|
|
154
|
+
close();
|
|
155
|
+
}
|
|
156
|
+
close();
|
|
157
|
+
const failures = {};
|
|
158
|
+
for (const session of sessions) {
|
|
159
|
+
if (session.completedReview || session.firstErrorCategory === null)
|
|
160
|
+
continue;
|
|
161
|
+
failures[session.firstErrorCategory] = (failures[session.firstErrorCategory] ?? 0) + 1;
|
|
162
|
+
}
|
|
163
|
+
return failures;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=adoption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adoption.js","sourceRoot":"","sources":["../../src/lib/adoption.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -98,13 +98,37 @@ export interface WorkflowResult {
|
|
|
98
98
|
id?: number;
|
|
99
99
|
body: string;
|
|
100
100
|
};
|
|
101
|
+
/** What each dispatched step actually did. Lets a caller tell "ran and found
|
|
102
|
+
* nothing" apart from "never ran", which the verdict alone cannot express —
|
|
103
|
+
* every step of a conflict-resolve run can skip and still leave verdict null,
|
|
104
|
+
* exactly like a review that approved nothing. */
|
|
105
|
+
stepOutcomes?: StepOutcomes;
|
|
101
106
|
}
|
|
107
|
+
export interface StepOutcomes {
|
|
108
|
+
/** Steps that executed. A dispatched step with no recorded result counts as
|
|
109
|
+
* ran: only an explicit skip is evidence that nothing happened. */
|
|
110
|
+
ran: string[];
|
|
111
|
+
/** Steps dispatched but skipped, each with the reason recorded on its
|
|
112
|
+
* step_skipped log entry. */
|
|
113
|
+
skipped: {
|
|
114
|
+
step: string;
|
|
115
|
+
reason: string;
|
|
116
|
+
}[];
|
|
117
|
+
}
|
|
118
|
+
export declare function summariseStepOutcomes(stepsRun: readonly string[], results: Record<string, StepResult>): StepOutcomes;
|
|
119
|
+
export declare function mergeStepOutcomes(base: StepOutcomes | undefined, next: StepOutcomes | undefined): StepOutcomes | undefined;
|
|
102
120
|
export declare function fixCommitSubject(appliedCount: number, vendor: Vendor): string;
|
|
103
121
|
export declare function fixPRCommitSubject(prNumber: number, vendor: Vendor): string;
|
|
104
122
|
export declare function conflictResolveCommitSubject(conflictCount: number, vendor: Vendor): string;
|
|
105
123
|
export declare function resolveFixVendor(stepReviewer: string, origin: PROrigin, config: Config, fallback?: 'claude' | 'codex'): {
|
|
106
124
|
vendor: 'claude' | 'codex' | null;
|
|
107
125
|
usedHumanFallback: boolean;
|
|
126
|
+
substitutedOriginVendor?: 'claude' | 'codex';
|
|
127
|
+
};
|
|
128
|
+
export declare function resolveConflictResolveVendor(stepReviewer: string, origin: PROrigin, config: Config, fallback?: 'claude' | 'codex'): {
|
|
129
|
+
vendor: 'claude' | 'codex' | null;
|
|
130
|
+
usedHumanFallback: boolean;
|
|
131
|
+
substitutedOriginVendor?: 'claude' | 'codex';
|
|
108
132
|
};
|
|
109
133
|
/**
|
|
110
134
|
* Builds the input the review strategy classifies on, from the already-cloned
|
package/dist/lib/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAoB,MAAM,qBAAqB,CAAA;AAEnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAQjE,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAQ9F,OAAO,EAAuE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/runner.js
CHANGED
|
@@ -23,6 +23,7 @@ import { resolveClaudeModel, resolveCodexModel } from '../lib/review-models.js';
|
|
|
23
23
|
import { resolveReviewStrategy, escalate, clampToLevels } from './review-strategy.js';
|
|
24
24
|
import { CLAUDE_EFFORT_LEVELS, CODEX_EFFORT_LEVELS } from '../config/schema.js';
|
|
25
25
|
import { buildStepIdentityFields } from '../lib/event-fields.js';
|
|
26
|
+
import { prOpenToVerdictMs } from '../lib/adoption.js';
|
|
26
27
|
import { buildAttributionFooter, buildFixAppliedCommentBody, buildFixFailedCommentBody, buildConflictResolvedCommentBody, buildRetriedReviewBanner } from '../lib/comment-bodies.js';
|
|
27
28
|
import { linearWritePossible, loadWorkflow, loadHarnessSection, evaluateWhen } from '../lib/workflow.js';
|
|
28
29
|
import { isSubscriptionLimitError, isVendorUnavailableError } from '../lib/smart-switch.js';
|
|
@@ -147,6 +148,45 @@ export function resolveFixLanding(deliveryMode) {
|
|
|
147
148
|
case 'pull_request': return 'branch-then-separate-pr';
|
|
148
149
|
}
|
|
149
150
|
}
|
|
151
|
+
// stepsRun holds every step the runner dispatched, skips included; results holds
|
|
152
|
+
// what each one did. Split them so callers can report a run where nothing
|
|
153
|
+
// happened without re-deriving the reasons from the log file.
|
|
154
|
+
export function summariseStepOutcomes(stepsRun, results) {
|
|
155
|
+
const outcomes = { ran: [], skipped: [] };
|
|
156
|
+
for (const name of new Set(stepsRun)) {
|
|
157
|
+
const result = results[name];
|
|
158
|
+
if (result?.skipped)
|
|
159
|
+
outcomes.skipped.push({ step: name, reason: result.skipReason ?? 'unknown' });
|
|
160
|
+
else
|
|
161
|
+
outcomes.ran.push(name);
|
|
162
|
+
}
|
|
163
|
+
return outcomes;
|
|
164
|
+
}
|
|
165
|
+
// Accumulates outcomes across the fix→recheck rounds of one invocation, so the
|
|
166
|
+
// completion line reports whether the run as a whole did work: a first round
|
|
167
|
+
// that skips everything followed by a round that applies a fix is not a
|
|
168
|
+
// "no step ran" run.
|
|
169
|
+
//
|
|
170
|
+
// Running once wins over skipping any number of times — the step demonstrably
|
|
171
|
+
// happened. A step skipped in several rounds is reported once, carrying its
|
|
172
|
+
// latest reason: that is the state the run ended in, and listing the same step
|
|
173
|
+
// several times reads as several distinct problems.
|
|
174
|
+
export function mergeStepOutcomes(base, next) {
|
|
175
|
+
if (!base)
|
|
176
|
+
return next;
|
|
177
|
+
if (!next)
|
|
178
|
+
return base;
|
|
179
|
+
const ran = [...new Set([...base.ran, ...next.ran])];
|
|
180
|
+
const ranSet = new Set(ran);
|
|
181
|
+
// Map.set on an existing key overwrites the reason but keeps the original
|
|
182
|
+
// position, so order stays first-seen while the reason stays last-seen.
|
|
183
|
+
const skipped = new Map();
|
|
184
|
+
for (const entry of [...base.skipped, ...next.skipped]) {
|
|
185
|
+
if (!ranSet.has(entry.step))
|
|
186
|
+
skipped.set(entry.step, entry.reason);
|
|
187
|
+
}
|
|
188
|
+
return { ran, skipped: [...skipped].map(([step, reason]) => ({ step, reason })) };
|
|
189
|
+
}
|
|
150
190
|
function countComments(reviewText) {
|
|
151
191
|
const bullets = (reviewText.match(/^[-*•]\s/gm) ?? []).length;
|
|
152
192
|
const numbered = (reviewText.match(/^\d+\.\s/gm) ?? []).length;
|
|
@@ -203,16 +243,38 @@ export function fixPRCommitSubject(prNumber, vendor) {
|
|
|
203
243
|
export function conflictResolveCommitSubject(conflictCount, vendor) {
|
|
204
244
|
return `[crosscheck] resolve: resolve ${conflictCount} conflict${conflictCount !== 1 ? 's' : ''} — by ${vendorDisplayName(vendor)}`;
|
|
205
245
|
}
|
|
206
|
-
// Extends resolveReviewer with a human-origin fallback for the
|
|
246
|
+
// Extends resolveReviewer with a human-origin fallback for the steps that write
|
|
247
|
+
// code (fix, conflict-resolve).
|
|
207
248
|
// Scoped to reviewer: 'origin' only — other reviewer types (claude, codex, auto)
|
|
208
249
|
// already encode explicit vendor intent and need no fallback.
|
|
209
250
|
// When origin is 'human' and no vendor resolved, honours routing.fallback_reviewer
|
|
210
|
-
// so the
|
|
211
|
-
// 'auto'
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
251
|
+
// so the step respects the same routing intent as the review step.
|
|
252
|
+
// 'auto' resolves against the vendors that can actually run stepType, so a step
|
|
253
|
+
// only one vendor supports doesn't fall back to the other and skip a line later.
|
|
254
|
+
// An explicit 'claude'/'codex' is an operator decision and is honoured as written
|
|
255
|
+
// even when that vendor cannot run the step — the caller then reports a precise
|
|
256
|
+
// unsupported-step skip instead of silently substituting a different vendor.
|
|
257
|
+
// null disables the fallback entirely.
|
|
258
|
+
function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
|
|
215
259
|
const vendor = resolveReviewer(stepReviewer, origin, config, fallback);
|
|
260
|
+
// Origin detection can assign a vendor that cannot run the step: conflict
|
|
261
|
+
// resolution is Claude-only, so a Codex-origin PR (reviewer: 'origin', origin:
|
|
262
|
+
// 'codex') resolves to 'codex' and the dispatch skips it as unsupported — the
|
|
263
|
+
// conflicts never get resolved. (#284's `Crosscheck-Reviewer: codex` detection
|
|
264
|
+
// introduced this: these crosscheck-authored fix PRs used to detect as 'human'
|
|
265
|
+
// and the auto fallback picked Claude.) Substitute a capable, enabled vendor.
|
|
266
|
+
// Scoped to origin-derived assignment only — an explicit reviewer: claude|codex,
|
|
267
|
+
// reviewer: auto, or routing.fallback_reviewer is an operator decision, left as
|
|
268
|
+
// written so the caller can report the precise unsupported-step skip.
|
|
269
|
+
if (stepReviewer === 'origin' &&
|
|
270
|
+
(origin === 'claude' || origin === 'codex') &&
|
|
271
|
+
vendor !== null &&
|
|
272
|
+
!supportsStep(vendor, stepType)) {
|
|
273
|
+
const capable = vendor === 'claude' ? 'codex' : 'claude';
|
|
274
|
+
if (config.vendors[capable].enabled && supportsStep(capable, stepType)) {
|
|
275
|
+
return { vendor: capable, usedHumanFallback: false, substitutedOriginVendor: vendor };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
216
278
|
if (vendor !== null || origin !== 'human' || stepReviewer !== 'origin') {
|
|
217
279
|
return { vendor, usedHumanFallback: false };
|
|
218
280
|
}
|
|
@@ -223,13 +285,25 @@ export function resolveFixVendor(stepReviewer, origin, config, fallback) {
|
|
|
223
285
|
else if (fb === 'codex')
|
|
224
286
|
humanFallback = config.vendors.codex.enabled ? 'codex' : null;
|
|
225
287
|
else if (fb !== null) {
|
|
226
|
-
// 'auto': prefer codex then claude, same as resolveReviewer's auto path
|
|
227
|
-
|
|
288
|
+
// 'auto': prefer codex then claude, same as resolveReviewer's auto path,
|
|
289
|
+
// narrowed to vendors that support this step type.
|
|
290
|
+
const usable = (v) => config.vendors[v].enabled && supportsStep(v, stepType);
|
|
291
|
+
humanFallback = usable('codex') ? 'codex' : usable('claude') ? 'claude' : null;
|
|
228
292
|
}
|
|
229
293
|
if (!humanFallback)
|
|
230
294
|
return { vendor: null, usedHumanFallback: false };
|
|
231
295
|
return { vendor: humanFallback, usedHumanFallback: true };
|
|
232
296
|
}
|
|
297
|
+
// Exported so callers can detect when the fallback was applied (e.g. for logging).
|
|
298
|
+
export function resolveFixVendor(stepReviewer, origin, config, fallback) {
|
|
299
|
+
return resolveStepVendor('fix', stepReviewer, origin, config, fallback);
|
|
300
|
+
}
|
|
301
|
+
// The default workflow gives conflict-resolve `reviewer: origin`, so every PR
|
|
302
|
+
// crosscheck cannot attribute resolved to null here and skipped with 'no_vendor'
|
|
303
|
+
// — the fix step honoured routing.fallback_reviewer, this one did not.
|
|
304
|
+
export function resolveConflictResolveVendor(stepReviewer, origin, config, fallback) {
|
|
305
|
+
return resolveStepVendor('conflict-resolve', stepReviewer, origin, config, fallback);
|
|
306
|
+
}
|
|
233
307
|
// ─── pr_complexity helpers ────────────────────────────────────────────────────
|
|
234
308
|
const EXT_LANG = {
|
|
235
309
|
ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
|
|
@@ -717,7 +791,7 @@ export async function runWorkflow(ctx) {
|
|
|
717
791
|
const effectiveType = getEffectiveStepType(step.type, ctx.isRecheckRun === true);
|
|
718
792
|
if (exceedsMaxRounds(effectiveType, step.type, ctx.overrideMaxRounds ?? step.max_rounds, ctx.round)) {
|
|
719
793
|
fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'max_rounds' });
|
|
720
|
-
results[step.name] = { skipped: true };
|
|
794
|
+
results[step.name] = { skipped: true, skipReason: 'max_rounds' };
|
|
721
795
|
if (effectiveType === 'fix')
|
|
722
796
|
onPhaseChange('', { phase: 'fixed', fixCount: 0 });
|
|
723
797
|
else if (effectiveType === 'recheck')
|
|
@@ -729,7 +803,7 @@ export async function runWorkflow(ctx) {
|
|
|
729
803
|
// Evaluate when condition — skip step if false
|
|
730
804
|
if (step.when && !evaluateWhen(step.when, results)) {
|
|
731
805
|
fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'when_condition' });
|
|
732
|
-
results[step.name] = { skipped: true };
|
|
806
|
+
results[step.name] = { skipped: true, skipReason: 'when_condition' };
|
|
733
807
|
if (effectiveType === 'fix')
|
|
734
808
|
onPhaseChange('', { phase: 'fixed', fixCount: 0 });
|
|
735
809
|
else if (effectiveType === 'recheck')
|
|
@@ -743,7 +817,7 @@ export async function runWorkflow(ctx) {
|
|
|
743
817
|
// review coerced to a recheck (isRecheckRun) is never blocked here.
|
|
744
818
|
if (step.type === 'recheck' && reviewRanThisSession && !anyFixApplied(results)) {
|
|
745
819
|
fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'no_change_since_review' });
|
|
746
|
-
results[step.name] = { skipped: true };
|
|
820
|
+
results[step.name] = { skipped: true, skipReason: 'no_change_since_review' };
|
|
747
821
|
onPhaseChange('', { phase: 'rechecked' });
|
|
748
822
|
continue;
|
|
749
823
|
}
|
|
@@ -752,7 +826,7 @@ export async function runWorkflow(ctx) {
|
|
|
752
826
|
let reviewer = resolveReviewer(step.reviewer, origin, config, ctx.smartSwitchFallback);
|
|
753
827
|
if (!reviewer) {
|
|
754
828
|
fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'no_reviewer' });
|
|
755
|
-
results[step.name] = { skipped: true };
|
|
829
|
+
results[step.name] = { skipped: true, skipReason: 'no_reviewer' };
|
|
756
830
|
continue;
|
|
757
831
|
}
|
|
758
832
|
// The recheck step is confirmed to run — clear the pending-recheck guard
|
|
@@ -876,7 +950,12 @@ export async function runWorkflow(ctx) {
|
|
|
876
950
|
? `${buildRetriedReviewBanner(retried.timeoutMs, retried.delayMs)}\n\n${baseBody}`
|
|
877
951
|
: baseBody;
|
|
878
952
|
const commentCount = countComments(rawReview);
|
|
879
|
-
|
|
953
|
+
// How long the PR waited for a verdict, measured from when its author opened
|
|
954
|
+
// it — the number a team feels, as distinct from duration_ms (how long the
|
|
955
|
+
// reviewer ran). Omitted rather than guessed when the PR event carried no
|
|
956
|
+
// created_at, so the metric never mixes real latencies with invented ones.
|
|
957
|
+
const openToVerdictMs = prOpenToVerdictMs(pr.created_at, verdict);
|
|
958
|
+
fileLog({ level: 'info', event: 'review_complete', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, model, ...stepIdentity, verdict, duration_ms: Date.now() - stepStart, ...(openToVerdictMs !== undefined && { open_to_verdict_ms: openToVerdictMs }), tokens_used: tokensUsed, skills_activated: activatedSkills.map(skill => skill.name), ...(inputTokens !== undefined && { input_tokens: inputTokens }), ...(outputTokens !== undefined && { output_tokens: outputTokens }), ...(ctx.round !== undefined && { round: ctx.round }), ...(ctx.roundMode && { mode: ctx.roundMode }), ...triggerField });
|
|
880
959
|
// Recheck verdict is stored separately to preserve the original review's commentCount on the board
|
|
881
960
|
const phaseUpdate = isRecheck
|
|
882
961
|
? { recheckVerdict: verdict, phase: donePhase, recheckTokens: tokensUsed, recheckReviewer: reviewer, qualityTier: quality.tier }
|
|
@@ -946,6 +1025,16 @@ export async function runWorkflow(ctx) {
|
|
|
946
1025
|
: undefined);
|
|
947
1026
|
const commentUrl = `github.com/${owner}/${repoName}/pull/${prNumber}`;
|
|
948
1027
|
fileLog({ level: 'info', event: 'comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, url: `https://${commentUrl}` });
|
|
1028
|
+
// A posted verdict that blocks the merge is the product's whole reason to
|
|
1029
|
+
// exist, so it gets its own event rather than being re-derived downstream.
|
|
1030
|
+
// BLOCK blocks by definition; a NEEDS WORK that reached here survived the
|
|
1031
|
+
// severity gate, which only lets it through when a Critical/High/Medium
|
|
1032
|
+
// finding backs it — a nit-only review was already downgraded to APPROVE.
|
|
1033
|
+
// Logged only here, after the comment actually posted, so dry runs and
|
|
1034
|
+
// failed postReviewComment calls are never counted as posted findings.
|
|
1035
|
+
if (verdict === 'BLOCK' || verdict === 'NEEDS WORK') {
|
|
1036
|
+
fileLog({ level: 'info', event: 'blocking_finding_posted', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, model, ...stepIdentity, verdict, ...(ctx.round !== undefined && { round: ctx.round }), ...triggerField });
|
|
1037
|
+
}
|
|
949
1038
|
// Mirror the verdict onto the PR's Linear issue. `run` and `watch` both
|
|
950
1039
|
// land here, so this is the path that matters — reviews posted from
|
|
951
1040
|
// commands/review.ts are the exception, not the rule.
|
|
@@ -985,7 +1074,7 @@ export async function runWorkflow(ctx) {
|
|
|
985
1074
|
const skipFix = (reason) => {
|
|
986
1075
|
lastFixSkipReason = reason;
|
|
987
1076
|
onPhaseChange('', { phase: 'fixed', fixCount: 0 });
|
|
988
|
-
results[step.name] = { skipped: true };
|
|
1077
|
+
results[step.name] = { skipped: true, skipReason: reason };
|
|
989
1078
|
fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason });
|
|
990
1079
|
};
|
|
991
1080
|
if (ctx.dryRun) {
|
|
@@ -1380,7 +1469,7 @@ export async function runWorkflow(ctx) {
|
|
|
1380
1469
|
else if (effectiveType === 'conflict-resolve') {
|
|
1381
1470
|
const skipConflictResolve = (reason) => {
|
|
1382
1471
|
onPhaseChange('', { phase: 'fixed', fixCount: 0 });
|
|
1383
|
-
results[step.name] = { skipped: true };
|
|
1472
|
+
results[step.name] = { skipped: true, skipReason: reason };
|
|
1384
1473
|
fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason });
|
|
1385
1474
|
};
|
|
1386
1475
|
if (ctx.dryRun) {
|
|
@@ -1427,7 +1516,16 @@ export async function runWorkflow(ctx) {
|
|
|
1427
1516
|
skipConflictResolve('no_conflicts');
|
|
1428
1517
|
continue;
|
|
1429
1518
|
}
|
|
1430
|
-
|
|
1519
|
+
// resolveConflictResolveVendor extends resolveReviewer with the same human-origin
|
|
1520
|
+
// fallback the fix step uses, so a PR crosscheck cannot attribute still gets its
|
|
1521
|
+
// conflicts resolved instead of skipping with 'no_vendor'.
|
|
1522
|
+
const { vendor, usedHumanFallback, substitutedOriginVendor } = resolveConflictResolveVendor(step.reviewer, origin, config, ctx.smartSwitchFallback);
|
|
1523
|
+
if (usedHumanFallback && vendor) {
|
|
1524
|
+
fileLog({ level: 'info', event: 'conflict_resolve_vendor_fallback', repo: `${owner}/${repoName}`, pr: prNumber, from: 'none', to: vendor, reason: 'human_origin' });
|
|
1525
|
+
}
|
|
1526
|
+
else if (substitutedOriginVendor && vendor) {
|
|
1527
|
+
fileLog({ level: 'info', event: 'conflict_resolve_vendor_fallback', repo: `${owner}/${repoName}`, pr: prNumber, from: substitutedOriginVendor, to: vendor, reason: 'unsupported_vendor' });
|
|
1528
|
+
}
|
|
1431
1529
|
if (!vendor) {
|
|
1432
1530
|
try {
|
|
1433
1531
|
execSync('git merge --abort', { cwd: tmpDir });
|
|
@@ -1633,6 +1731,7 @@ export async function runWorkflow(ctx) {
|
|
|
1633
1731
|
return {
|
|
1634
1732
|
verdict: verdict ?? null,
|
|
1635
1733
|
fixAppliedCount,
|
|
1734
|
+
stepOutcomes: summariseStepOutcomes(stepsRun, results),
|
|
1636
1735
|
...(fixAppliedCount === undefined && lastFixSkipReason !== undefined && { fixSkipReason: lastFixSkipReason }),
|
|
1637
1736
|
...(latestReviewResult?.commentBody && {
|
|
1638
1737
|
latestReviewComment: {
|