@humanbased/crosscheck 1.3.1-beta.7 → 1.6.0-beta.11
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 +16 -1
- package/assets/demo-block-to-approve.gif +0 -0
- package/assets/demo-block-to-approve.mp4 +0 -0
- package/dist/__tests__/base-ref-recovery.test.d.ts +2 -0
- package/dist/__tests__/base-ref-recovery.test.d.ts.map +1 -0
- package/dist/__tests__/base-ref-recovery.test.js +102 -0
- package/dist/__tests__/base-ref-recovery.test.js.map +1 -0
- package/dist/__tests__/board.test.js +510 -20
- package/dist/__tests__/board.test.js.map +1 -1
- package/dist/__tests__/codex-env.test.js +4 -0
- package/dist/__tests__/codex-env.test.js.map +1 -1
- package/dist/__tests__/credential-free-origin.test.js +134 -2
- package/dist/__tests__/credential-free-origin.test.js.map +1 -1
- package/dist/__tests__/demo-arc.test.d.ts +2 -0
- package/dist/__tests__/demo-arc.test.d.ts.map +1 -0
- package/dist/__tests__/demo-arc.test.js +73 -0
- package/dist/__tests__/demo-arc.test.js.map +1 -0
- package/dist/__tests__/doc-only-verdict.test.d.ts +2 -0
- package/dist/__tests__/doc-only-verdict.test.d.ts.map +1 -0
- package/dist/__tests__/doc-only-verdict.test.js +79 -0
- package/dist/__tests__/doc-only-verdict.test.js.map +1 -0
- package/dist/__tests__/error-classification.test.js +14 -0
- package/dist/__tests__/error-classification.test.js.map +1 -1
- package/dist/__tests__/fix-requires-recheck.test.d.ts +2 -0
- package/dist/__tests__/fix-requires-recheck.test.d.ts.map +1 -0
- package/dist/__tests__/fix-requires-recheck.test.js +56 -0
- package/dist/__tests__/fix-requires-recheck.test.js.map +1 -0
- package/dist/__tests__/human-feedback.test.d.ts +2 -0
- package/dist/__tests__/human-feedback.test.d.ts.map +1 -0
- package/dist/__tests__/human-feedback.test.js +132 -0
- package/dist/__tests__/human-feedback.test.js.map +1 -0
- package/dist/__tests__/inconclusive-review.test.d.ts +2 -0
- package/dist/__tests__/inconclusive-review.test.d.ts.map +1 -0
- package/dist/__tests__/inconclusive-review.test.js +110 -0
- package/dist/__tests__/inconclusive-review.test.js.map +1 -0
- package/dist/__tests__/lock-ownership.test.d.ts +2 -0
- package/dist/__tests__/lock-ownership.test.d.ts.map +1 -0
- package/dist/__tests__/lock-ownership.test.js +167 -0
- package/dist/__tests__/lock-ownership.test.js.map +1 -0
- package/dist/__tests__/merge-gate.test.d.ts +2 -0
- package/dist/__tests__/merge-gate.test.d.ts.map +1 -0
- package/dist/__tests__/merge-gate.test.js +220 -0
- package/dist/__tests__/merge-gate.test.js.map +1 -0
- package/dist/__tests__/no-verdict.test.d.ts +2 -0
- package/dist/__tests__/no-verdict.test.d.ts.map +1 -0
- package/dist/__tests__/no-verdict.test.js +415 -0
- package/dist/__tests__/no-verdict.test.js.map +1 -0
- package/dist/__tests__/pr-spec.test.js +37 -0
- package/dist/__tests__/pr-spec.test.js.map +1 -1
- package/dist/__tests__/run.test.js +50 -1
- package/dist/__tests__/run.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +82 -1
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/standing-verdict-history.test.d.ts +2 -0
- package/dist/__tests__/standing-verdict-history.test.d.ts.map +1 -0
- package/dist/__tests__/standing-verdict-history.test.js +109 -0
- package/dist/__tests__/standing-verdict-history.test.js.map +1 -0
- package/dist/__tests__/step-plan.test.d.ts +2 -0
- package/dist/__tests__/step-plan.test.d.ts.map +1 -0
- package/dist/__tests__/step-plan.test.js +129 -0
- package/dist/__tests__/step-plan.test.js.map +1 -0
- package/dist/cli.js +14 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/merge.d.ts +11 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +188 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/review.d.ts +3 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +117 -9
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts +53 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +218 -36
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +41 -26
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +8 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/review-strategy.json +5 -5
- package/dist/github/client.d.ts +18 -1
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +19 -3
- package/dist/github/client.js.map +1 -1
- package/dist/github/merge.d.ts +28 -0
- package/dist/github/merge.d.ts.map +1 -1
- package/dist/github/merge.js +41 -0
- package/dist/github/merge.js.map +1 -1
- package/dist/github/review-status.d.ts +33 -0
- package/dist/github/review-status.d.ts.map +1 -1
- package/dist/github/review-status.js +94 -9
- package/dist/github/review-status.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/board.d.ts +60 -4
- package/dist/lib/board.d.ts.map +1 -1
- package/dist/lib/board.js +357 -65
- package/dist/lib/board.js.map +1 -1
- package/dist/lib/clone.d.ts +65 -1
- package/dist/lib/clone.d.ts.map +1 -1
- package/dist/lib/clone.js +252 -16
- package/dist/lib/clone.js.map +1 -1
- package/dist/lib/human-feedback.d.ts +9 -0
- package/dist/lib/human-feedback.d.ts.map +1 -0
- package/dist/lib/human-feedback.js +83 -0
- package/dist/lib/human-feedback.js.map +1 -0
- package/dist/lib/logger.d.ts +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +6 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/merge-gate.d.ts +70 -0
- package/dist/lib/merge-gate.d.ts.map +1 -0
- package/dist/lib/merge-gate.js +138 -0
- package/dist/lib/merge-gate.js.map +1 -0
- package/dist/lib/no-verdict.d.ts +104 -0
- package/dist/lib/no-verdict.d.ts.map +1 -0
- package/dist/lib/no-verdict.js +262 -0
- package/dist/lib/no-verdict.js.map +1 -0
- package/dist/lib/pr-lock.d.ts +37 -1
- package/dist/lib/pr-lock.d.ts.map +1 -1
- package/dist/lib/pr-lock.js +176 -29
- package/dist/lib/pr-lock.js.map +1 -1
- package/dist/lib/pr-spec.d.ts +7 -0
- package/dist/lib/pr-spec.d.ts.map +1 -1
- package/dist/lib/pr-spec.js +12 -1
- package/dist/lib/pr-spec.js.map +1 -1
- package/dist/lib/pr-workflow-state.d.ts +39 -0
- package/dist/lib/pr-workflow-state.d.ts.map +1 -1
- package/dist/lib/pr-workflow-state.js +73 -1
- package/dist/lib/pr-workflow-state.js.map +1 -1
- package/dist/lib/review-strategy.d.ts +14 -0
- package/dist/lib/review-strategy.d.ts.map +1 -1
- package/dist/lib/review-strategy.js +32 -1
- package/dist/lib/review-strategy.js.map +1 -1
- package/dist/lib/runner.d.ts +51 -2
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +211 -105
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/tips.d.ts.map +1 -1
- package/dist/lib/tips.js +1 -0
- package/dist/lib/tips.js.map +1 -1
- package/dist/lib/verdict.d.ts +30 -1
- package/dist/lib/verdict.d.ts.map +1 -1
- package/dist/lib/verdict.js +94 -9
- package/dist/lib/verdict.js.map +1 -1
- package/dist/reviewers/codex-env.d.ts.map +1 -1
- package/dist/reviewers/codex-env.js +2 -0
- package/dist/reviewers/codex-env.js.map +1 -1
- package/dist/reviewers/codex.d.ts.map +1 -1
- package/dist/reviewers/codex.js +3 -1
- package/dist/reviewers/codex.js.map +1 -1
- package/dist/reviewers/fix.d.ts +2 -2
- package/dist/reviewers/fix.d.ts.map +1 -1
- package/dist/reviewers/fix.js +10 -4
- package/dist/reviewers/fix.js.map +1 -1
- package/docs/growth/launch/README.md +59 -0
- package/docs/growth/launch/essay-stop-letting-one-agent-review-its-own-code.md +118 -0
- package/docs/growth/launch/posts.md +175 -0
- package/docs/trust.md +16 -1
- package/get-started.md +17 -0
- package/package.json +11 -3
package/dist/commands/run.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type Vendor } from '../lib/vendor.js';
|
|
|
2
2
|
import { type WorkflowStep } from '../lib/workflow.js';
|
|
3
3
|
import { type PRRef } from '../lib/pr-spec.js';
|
|
4
4
|
import { type ConcurrencyOpts } from '../lib/multi-run.js';
|
|
5
|
+
import { type JudgedRecordShape } from '../lib/no-verdict.js';
|
|
5
6
|
export interface RunOpts {
|
|
6
7
|
config?: string;
|
|
7
8
|
reviewer?: string;
|
|
@@ -14,6 +15,12 @@ export interface RunOpts {
|
|
|
14
15
|
id?: number;
|
|
15
16
|
body: string;
|
|
16
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* A specific review comment to act on, from the `#issuecomment-<id>` anchor on
|
|
20
|
+
* the PR URL. Without it a fix step falls back to "whatever the latest review
|
|
21
|
+
* is", which can be a different comment from the one the user pasted.
|
|
22
|
+
*/
|
|
23
|
+
reviewCommentId?: number;
|
|
17
24
|
expectedHeadSha?: string;
|
|
18
25
|
timeout?: string;
|
|
19
26
|
noTimeout?: boolean;
|
|
@@ -26,6 +33,52 @@ export interface StepVendorOverrides {
|
|
|
26
33
|
}
|
|
27
34
|
export declare function resolveWorkflowSteps(allSteps: WorkflowStep[], stepFilter: string[] | undefined, assignedReviewer: 'claude' | 'codex', overrides?: StepVendorOverrides): WorkflowStep[];
|
|
28
35
|
export declare function buildFixRecheckSteps(steps: WorkflowStep[], allSteps: WorkflowStep[], assignedReviewer: 'claude' | 'codex', overrides?: StepVendorOverrides): WorkflowStep[];
|
|
36
|
+
/**
|
|
37
|
+
* The verdict standing on the PR when the report is printed — which this run is
|
|
38
|
+
* one of the things that can have changed.
|
|
39
|
+
*
|
|
40
|
+
* A run can post a `BLOCK` review and then have its recheck emit no parseable
|
|
41
|
+
* verdict: the workflow ends with `verdict === null` because the recheck is the
|
|
42
|
+
* last step to record one, while the `BLOCK` it just posted is what actually
|
|
43
|
+
* governs the PR. Pre-run history has never heard of that comment, so reporting
|
|
44
|
+
* from it either says nothing stands or names the verdict the run superseded —
|
|
45
|
+
* pointing the reader at an older commit while a fresh judgment sits on HEAD.
|
|
46
|
+
*
|
|
47
|
+
* A successful read supersedes the pre-run selection outright rather than being
|
|
48
|
+
* merged with it: it is a read of the same comments, one workflow later, so
|
|
49
|
+
* where the two disagree the later one is the PR's current state. The pre-run
|
|
50
|
+
* selection is the fallback for a failed read, not a floor.
|
|
51
|
+
*
|
|
52
|
+
* That only holds if the later read can see at least as far back as the earlier
|
|
53
|
+
* one, which is why it is not `fetchStepHistory` — see
|
|
54
|
+
* `fetchStandingVerdictRecords`, whose backward scan is complete where step
|
|
55
|
+
* detection's fast path truncates at the newest annotated review.
|
|
56
|
+
*/
|
|
57
|
+
export declare function standingVerdictForReport(preRun: {
|
|
58
|
+
verdict: string;
|
|
59
|
+
sha?: string;
|
|
60
|
+
} | undefined, fetchHistory: () => Promise<readonly JudgedRecordShape[]>): Promise<{
|
|
61
|
+
verdict: string;
|
|
62
|
+
sha?: string;
|
|
63
|
+
} | undefined>;
|
|
64
|
+
/**
|
|
65
|
+
* The commit the no-verdict report measures the standing verdict against.
|
|
66
|
+
*
|
|
67
|
+
* The head captured at dispatch stops describing the PR the moment a step
|
|
68
|
+
* pushes: a resumed run can land a fix commit and then have its recheck gated
|
|
69
|
+
* out, which is the exact shape this report exists to explain. Comparing the
|
|
70
|
+
* standing verdict against the pre-run head there would claim it covers HEAD
|
|
71
|
+
* while an unjudged fix commit sits on top of it — the inverse of the truth.
|
|
72
|
+
*
|
|
73
|
+
* A failed read yields nothing rather than the pre-run head. Reporting the
|
|
74
|
+
* verdict without a staleness claim is what this report already does for a
|
|
75
|
+
* verdict whose own SHA is unknown; asserting coverage from a commit the run
|
|
76
|
+
* itself moved past is the defect. Nothing else consumes the head, so a run
|
|
77
|
+
* with no standing verdict never pays for the round-trip.
|
|
78
|
+
*/
|
|
79
|
+
export declare function headShaForStalenessClaim(standingVerdict: {
|
|
80
|
+
verdict?: string;
|
|
81
|
+
} | undefined, fetchHead: () => Promise<string>): Promise<string | undefined>;
|
|
29
82
|
export declare function runRun(prUrl: string, opts?: RunOpts): Promise<void>;
|
|
30
83
|
export type RunSpecOpts = RunOpts & ConcurrencyOpts;
|
|
31
84
|
export declare function buildRunChildArgs(ref: PRRef, opts: RunOpts): string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAaA,OAAO,EAAsC,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAKlF,OAAO,EAA0G,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAE9J,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAG3D,OAAO,EAAmG,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
1
|
import { mkdtempSync, rmSync } from 'fs';
|
|
3
2
|
import { tmpdir } from 'os';
|
|
4
3
|
import { join } from 'path';
|
|
@@ -6,8 +5,9 @@ import chalk from 'chalk';
|
|
|
6
5
|
import { execa } from 'execa';
|
|
7
6
|
import { parseDuration } from '../lib/durations.js';
|
|
8
7
|
import ora from 'ora';
|
|
9
|
-
import { createGithubClient } from '../github/client.js';
|
|
10
|
-
import {
|
|
8
|
+
import { createGithubClient, fetchIssueComment } from '../github/client.js';
|
|
9
|
+
import { parseAnnotation } from '../lib/annotation.js';
|
|
10
|
+
import { fetchStandingVerdictRecords, fetchStepHistoryWithRetry, identifyNextWorkflowStep } from '../lib/pr-workflow-state.js';
|
|
11
11
|
import { detectOriginFull, assignReviewer } from '../github/detector.js';
|
|
12
12
|
import { loadConfig, getGithubToken, getLinearApiKey, getLinearCredentials } from '../config/loader.js';
|
|
13
13
|
import { enrichIssueContext } from '../issues/enrich.js';
|
|
@@ -23,9 +23,10 @@ import { closedPRSkip } from '../lib/pr-state.js';
|
|
|
23
23
|
import { resolveCliInvocation } from '../lib/cli-invocation.js';
|
|
24
24
|
import { executeMultiPR, resolveRunConcurrency, printMultiPRSummary, concurrencyError, aggregateExitCode } from '../lib/multi-run.js';
|
|
25
25
|
import { formatVerdict } from '../lib/verdict.js';
|
|
26
|
-
import {
|
|
26
|
+
import { buildNoVerdictReport, renderNoVerdictReport, selectStandingVerdict } from '../lib/no-verdict.js';
|
|
27
|
+
import { clonePRForReview, runGitWithoutHooks, BaseRefUnavailableError } from '../lib/clone.js';
|
|
27
28
|
import { acquirePRLock, releasePRLock } from '../lib/pr-lock.js';
|
|
28
|
-
import { checkRemoteLock, acquireRemoteLock, releaseRemoteLock, startRemoteLockHeartbeat } from '../github/review-status.js';
|
|
29
|
+
import { checkRemoteLock, acquireRemoteLock, claimRemoteLock, releaseRemoteLock, startRemoteLockHeartbeat } from '../github/review-status.js';
|
|
29
30
|
function meetsCrazyStopCondition(verdict, mode) {
|
|
30
31
|
if (verdict === null)
|
|
31
32
|
return false;
|
|
@@ -118,6 +119,62 @@ export function buildFixRecheckSteps(steps, allSteps, assignedReviewer, override
|
|
|
118
119
|
}
|
|
119
120
|
return fixRecheckSteps;
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The verdict standing on the PR when the report is printed — which this run is
|
|
124
|
+
* one of the things that can have changed.
|
|
125
|
+
*
|
|
126
|
+
* A run can post a `BLOCK` review and then have its recheck emit no parseable
|
|
127
|
+
* verdict: the workflow ends with `verdict === null` because the recheck is the
|
|
128
|
+
* last step to record one, while the `BLOCK` it just posted is what actually
|
|
129
|
+
* governs the PR. Pre-run history has never heard of that comment, so reporting
|
|
130
|
+
* from it either says nothing stands or names the verdict the run superseded —
|
|
131
|
+
* pointing the reader at an older commit while a fresh judgment sits on HEAD.
|
|
132
|
+
*
|
|
133
|
+
* A successful read supersedes the pre-run selection outright rather than being
|
|
134
|
+
* merged with it: it is a read of the same comments, one workflow later, so
|
|
135
|
+
* where the two disagree the later one is the PR's current state. The pre-run
|
|
136
|
+
* selection is the fallback for a failed read, not a floor.
|
|
137
|
+
*
|
|
138
|
+
* That only holds if the later read can see at least as far back as the earlier
|
|
139
|
+
* one, which is why it is not `fetchStepHistory` — see
|
|
140
|
+
* `fetchStandingVerdictRecords`, whose backward scan is complete where step
|
|
141
|
+
* detection's fast path truncates at the newest annotated review.
|
|
142
|
+
*/
|
|
143
|
+
export async function standingVerdictForReport(preRun, fetchHistory) {
|
|
144
|
+
try {
|
|
145
|
+
return selectStandingVerdict(await fetchHistory());
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Best-effort: fall back to what step detection already read this run.
|
|
149
|
+
return preRun;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The commit the no-verdict report measures the standing verdict against.
|
|
154
|
+
*
|
|
155
|
+
* The head captured at dispatch stops describing the PR the moment a step
|
|
156
|
+
* pushes: a resumed run can land a fix commit and then have its recheck gated
|
|
157
|
+
* out, which is the exact shape this report exists to explain. Comparing the
|
|
158
|
+
* standing verdict against the pre-run head there would claim it covers HEAD
|
|
159
|
+
* while an unjudged fix commit sits on top of it — the inverse of the truth.
|
|
160
|
+
*
|
|
161
|
+
* A failed read yields nothing rather than the pre-run head. Reporting the
|
|
162
|
+
* verdict without a staleness claim is what this report already does for a
|
|
163
|
+
* verdict whose own SHA is unknown; asserting coverage from a commit the run
|
|
164
|
+
* itself moved past is the defect. Nothing else consumes the head, so a run
|
|
165
|
+
* with no standing verdict never pays for the round-trip.
|
|
166
|
+
*/
|
|
167
|
+
export async function headShaForStalenessClaim(standingVerdict, fetchHead) {
|
|
168
|
+
if (!standingVerdict?.verdict)
|
|
169
|
+
return undefined;
|
|
170
|
+
try {
|
|
171
|
+
return await fetchHead();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Best-effort: the report drops the staleness line rather than guessing.
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
121
178
|
function printRoundModeBanner(mode) {
|
|
122
179
|
const BLINK = '\x1b[5m';
|
|
123
180
|
const RESET = '\x1b[0m';
|
|
@@ -258,17 +315,70 @@ export async function runRun(prUrl, opts = {}) {
|
|
|
258
315
|
}
|
|
259
316
|
let stepFilter = opts.steps?.split(',').map(s => s.trim().toLowerCase());
|
|
260
317
|
let initialReviewComment = opts.initialReviewComment;
|
|
318
|
+
// An explicit `#issuecomment-<id>` anchor names the review to act on. Resolve it
|
|
319
|
+
// here rather than letting the step fall back to the latest review: the two are
|
|
320
|
+
// often the same comment, and when they are not, the silent substitution means
|
|
321
|
+
// the command did something other than what the URL said.
|
|
322
|
+
if (initialReviewComment === undefined && opts.reviewCommentId !== undefined) {
|
|
323
|
+
const anchored = await fetchIssueComment(owner, repo, opts.reviewCommentId, token);
|
|
324
|
+
if (!anchored) {
|
|
325
|
+
console.error(chalk.red(`✗ Comment ${opts.reviewCommentId} not found on ${owner}/${repo}`));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
// Compared case-insensitively: GitHub echoes the repository's canonical casing,
|
|
329
|
+
// so a URL typed with different case (github.com/HumanBased-AI/...) would fail an
|
|
330
|
+
// exact compare while naming the very same PR. The check still pins the comment
|
|
331
|
+
// to this owner/repo/number, which is the property that matters.
|
|
332
|
+
const expectedIssueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`;
|
|
333
|
+
if (anchored.issue_url?.toLowerCase() !== expectedIssueUrl.toLowerCase()) {
|
|
334
|
+
console.error(chalk.red(`✗ Comment ${opts.reviewCommentId} does not belong to ${owner}/${repo}#${number} — nothing to act on`));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
// The annotation tag is plain HTML in a comment body — any PR commenter can post
|
|
338
|
+
// one. It only proves provenance when the comment also comes from the token's own
|
|
339
|
+
// account, the same check watch.ts's onComment handler makes against
|
|
340
|
+
// authenticatedLogin. Fail closed if the authenticated user can't be determined.
|
|
341
|
+
let authenticatedLogin = null;
|
|
342
|
+
try {
|
|
343
|
+
const { data: me } = await createGithubClient(token).rest.users.getAuthenticated();
|
|
344
|
+
authenticatedLogin = me.login;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
authenticatedLogin = null;
|
|
348
|
+
}
|
|
349
|
+
if (authenticatedLogin === null || anchored.user.login !== authenticatedLogin) {
|
|
350
|
+
console.error(chalk.red(`✗ Comment ${opts.reviewCommentId} was not posted by crosscheck — nothing to act on`));
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
// A comment ID is user-supplied and untrusted, so this mutation path requires the
|
|
354
|
+
// stricter annotation-based check rather than isFreshReviewComment's legacy header
|
|
355
|
+
// fallback — the header alone is too permissive to gate a fix dispatch on.
|
|
356
|
+
const parsedAnnotation = parseAnnotation(anchored.body);
|
|
357
|
+
if (!parsedAnnotation || parsedAnnotation.type !== 'review') {
|
|
358
|
+
console.error(chalk.red(`✗ Comment ${opts.reviewCommentId} is not a crosscheck review comment — nothing to act on`));
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
initialReviewComment = { id: opts.reviewCommentId, body: anchored.body };
|
|
362
|
+
console.log(chalk.dim(` targeting review comment ${opts.reviewCommentId}`));
|
|
363
|
+
}
|
|
261
364
|
// When running without an explicit --steps flag, detect the next step from live
|
|
262
365
|
// PR comment history. When triggered by kickass the dispatch is intentionally
|
|
263
366
|
// one-step-at-a-time (watch owns continuation via webhooks). For all other
|
|
264
367
|
// triggers (direct user invocation, backtrace, etc.) run all remaining steps
|
|
265
368
|
// from the detected starting point so a standalone `ck run` still works end-to-end.
|
|
369
|
+
// The verdict already on the PR when this run started, if any — taken from the
|
|
370
|
+
// history step detection has to fetch anyway, so it costs nothing here. The
|
|
371
|
+
// no-verdict report re-reads at reporting time and prefers that; this is what
|
|
372
|
+
// it falls back to when the re-read fails, and all an explicit --steps run
|
|
373
|
+
// (which never fetches here) has to fall back to is nothing.
|
|
374
|
+
let standingVerdict;
|
|
266
375
|
if (!opts.steps) {
|
|
267
376
|
try {
|
|
268
|
-
const history = await
|
|
377
|
+
const history = await fetchStepHistoryWithRetry(owner, repo, number, token);
|
|
269
378
|
// prData comes from pulls.get, which carries `mergeable` — so routing a conflicted
|
|
270
379
|
// PR to conflict-resolve costs no extra call here.
|
|
271
380
|
const nextResult = identifyNextWorkflowStep(history, allSteps, prData.head.sha, { mergeable: prData.mergeable });
|
|
381
|
+
standingVerdict = selectStandingVerdict(history);
|
|
272
382
|
if (nextResult.step === null) {
|
|
273
383
|
if (nextResult.stopReason === 'approved') {
|
|
274
384
|
// This commit is approved; a push moves HEAD and re-opens the workflow.
|
|
@@ -304,7 +414,16 @@ export async function runRun(prUrl, opts = {}) {
|
|
|
304
414
|
// so the subprocess exits non-zero and kickass records a retryable failure.
|
|
305
415
|
throw err;
|
|
306
416
|
}
|
|
307
|
-
|
|
417
|
+
// Fail closed here too. This history is what carries the approval stop and the
|
|
418
|
+
// per-SHA dedup, so falling through without it re-reviews commits that are
|
|
419
|
+
// already approved — measured on 2026-09-19, when a batch of `ck run`
|
|
420
|
+
// invocations rate-limited this fetch and re-reviewed four already-APPROVEd
|
|
421
|
+
// SHAs. `--steps` remains the deliberate way to force a pass without history.
|
|
422
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
423
|
+
fileLog({ level: 'warn', event: 'pr_skipped', repo: `${owner}/${repo}`, pr: number, reason: 'history_unavailable', sha: prData.head.sha, error: message });
|
|
424
|
+
console.log(chalk.yellow(`⚠ could not read PR history (${message}) — skipping rather than risk re-reviewing an approved commit`));
|
|
425
|
+
console.log(chalk.dim(' use --steps review to force a pass without history detection'));
|
|
426
|
+
return;
|
|
308
427
|
}
|
|
309
428
|
}
|
|
310
429
|
const stepVendorOverrides = {
|
|
@@ -441,7 +560,15 @@ export async function runRun(prUrl, opts = {}) {
|
|
|
441
560
|
// request still triggers releaseRemoteLock (GitHub may have already
|
|
442
561
|
// created the pending status server-side).
|
|
443
562
|
lockAttemptStarted = true;
|
|
444
|
-
await
|
|
563
|
+
if (!await claimRemoteLock(octokit, owner, repo, sha)) {
|
|
564
|
+
// Another instance claimed this commit inside the check-then-act window.
|
|
565
|
+
// Its claim is the live one, so leave it alone and drop ours.
|
|
566
|
+
releasePRLock(owner, repo, number, sha);
|
|
567
|
+
lockAttemptStarted = false;
|
|
568
|
+
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repo}`, pr: number, reason: 'lost_remote_claim', sha });
|
|
569
|
+
console.log(chalk.yellow(`⚠ PR #${number} was claimed by another crosscheck instance — skipping`));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
445
572
|
}
|
|
446
573
|
catch (err) {
|
|
447
574
|
releasePRLock(owner, repo, number, sha);
|
|
@@ -470,12 +597,20 @@ export async function runRun(prUrl, opts = {}) {
|
|
|
470
597
|
&& linearWritePossible(config.linear, roundModeCanWrite ? undefined : filteredSteps)
|
|
471
598
|
? await resolveLinearAuth(config.linear, getLinearCredentials(config.linear.auth))
|
|
472
599
|
: null;
|
|
473
|
-
await clonePRForReview({
|
|
474
|
-
owner, repo, prNumber: number, baseRef: prData.base.ref,
|
|
600
|
+
const { baseRefStatus } = await clonePRForReview({
|
|
601
|
+
owner, repo, prNumber: number, baseRef: prData.base.ref, baseSha: prData.base.sha,
|
|
475
602
|
tmpDir, token, protocol: config.clone_protocol,
|
|
476
603
|
onBaseFetchFailed: () => fileLog({ level: 'warn', event: 'base_branch_fetch_skipped', repo: `${owner}/${repo}`, pr: number, base: prData.base.ref }),
|
|
604
|
+
onBaseRefRecovered: status => fileLog({ level: 'info', event: 'base_ref_recovered', repo: `${owner}/${repo}`, pr: number, base: prData.base.ref, via: status }),
|
|
477
605
|
});
|
|
478
606
|
cloneSpinner.succeed('Repo ready');
|
|
607
|
+
if (baseRefStatus === 'unavailable') {
|
|
608
|
+
fileLog({ level: 'error', event: 'base_ref_unavailable', repo: `${owner}/${repo}`, pr: number, base: prData.base.ref, base_sha: prData.base.sha });
|
|
609
|
+
throw new BaseRefUnavailableError(prData.base.ref);
|
|
610
|
+
}
|
|
611
|
+
if (baseRefStatus !== 'fetched') {
|
|
612
|
+
console.log(chalk.yellow(` base ref origin/${prData.base.ref} was missing — recovered ${baseRefStatus === 'recovered_by_sha' ? 'from the PR base commit' : "from the PR's merge ref"}`));
|
|
613
|
+
}
|
|
479
614
|
// Recover the linked tracker issue (if enabled) so the review is anchored
|
|
480
615
|
// to the stated goal, not just the diff. Done here — after the PR and
|
|
481
616
|
// remote locks are secured — so a PR that gets skipped (already under
|
|
@@ -501,6 +636,10 @@ export async function runRun(prUrl, opts = {}) {
|
|
|
501
636
|
overrideTimeoutMs: reviewerTimeoutMs,
|
|
502
637
|
trigger: opts.trigger ?? 'run',
|
|
503
638
|
issueContext,
|
|
639
|
+
// Only the CLI flag counts. Resume also narrows `filteredSteps`, but it
|
|
640
|
+
// starts mid-workflow and still runs to the end, so it is not the operator
|
|
641
|
+
// scoping this run to a subset.
|
|
642
|
+
stepsExplicitlyScoped: opts.steps !== undefined,
|
|
504
643
|
};
|
|
505
644
|
let workflowResult = await runWorkflow({
|
|
506
645
|
...sharedCtx,
|
|
@@ -617,8 +756,8 @@ export async function runRun(prUrl, opts = {}) {
|
|
|
617
756
|
// next attempt starts from a clean state. vendor_limit never touches files.
|
|
618
757
|
if (workflowResult.fixSkipReason === 'fix_error') {
|
|
619
758
|
try {
|
|
620
|
-
|
|
621
|
-
|
|
759
|
+
runGitWithoutHooks(tmpDir, ['reset', '--hard', 'HEAD']);
|
|
760
|
+
runGitWithoutHooks(tmpDir, ['clean', '-fd']);
|
|
622
761
|
}
|
|
623
762
|
catch {
|
|
624
763
|
fileLog({ level: 'warn', event: 'step_skipped', repo: `${owner}/${repo}`, pr: number, reason: 'worktree_reset_failed', mode, round: loopRound });
|
|
@@ -712,33 +851,66 @@ export async function runRun(prUrl, opts = {}) {
|
|
|
712
851
|
}
|
|
713
852
|
}
|
|
714
853
|
activeSpinner.stop();
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
854
|
+
// Every run that ends without a verdict gets the same report, whatever the
|
|
855
|
+
// steps did. Keying on the steps instead — "nothing ran", as this did
|
|
856
|
+
// before — only moves the blind spot: PR #2548 ran its fix step, applied
|
|
857
|
+
// nothing, had recheck gated out by `fix.applied_count > 0`, and printed a
|
|
858
|
+
// bare `verdict —` under a green checkmark. So did a review that ran at
|
|
859
|
+
// full price and emitted no parseable VERDICT: line. What makes a verdict
|
|
860
|
+
// missing is what became of the steps that produce one.
|
|
861
|
+
if (verdict === null) {
|
|
862
|
+
// Both claims the report makes about the PR — what verdict stands, and
|
|
863
|
+
// whether it covers HEAD — are claims about now, so both are read now.
|
|
864
|
+
const standingNow = await standingVerdictForReport(standingVerdict, () => fetchStandingVerdictRecords(owner, repo, number, token));
|
|
865
|
+
const reportHeadSha = await headShaForStalenessClaim(standingNow, async () => {
|
|
866
|
+
const { data: currentPR } = await octokit.rest.pulls.get({ owner, repo, pull_number: number });
|
|
867
|
+
return currentPR.head.sha;
|
|
868
|
+
});
|
|
869
|
+
const report = buildNoVerdictReport({
|
|
870
|
+
workflowSteps: allSteps,
|
|
871
|
+
outcomes: accumulatedStepOutcomes,
|
|
872
|
+
strategySkipped,
|
|
873
|
+
// Resume narrows the step list too, but it still runs to the end of
|
|
874
|
+
// the workflow — only an explicit ask counts as deliberate scoping.
|
|
875
|
+
stepsExplicitlyScoped: opts.steps !== undefined || opts.trigger === 'kickass',
|
|
876
|
+
prUrl,
|
|
877
|
+
...(standingNow && { standingVerdict: standingNow }),
|
|
878
|
+
...(reportHeadSha !== undefined && { headSha: reportHeadSha }),
|
|
879
|
+
});
|
|
880
|
+
console.log('');
|
|
881
|
+
renderNoVerdictReport(report).forEach((line, i) => {
|
|
882
|
+
if (line === '')
|
|
883
|
+
return console.log('');
|
|
884
|
+
// The headline carries the alarm and the recommended command is what
|
|
885
|
+
// the reader is meant to copy, so neither is dimmed into the body.
|
|
886
|
+
if (i === 0)
|
|
887
|
+
return console.log(report.expected ? chalk.dim(` ${line}`) : chalk.yellow(` ${line}`));
|
|
888
|
+
console.log(line.startsWith('→ ') ? ` ${line}` : chalk.dim(` ${line}`));
|
|
889
|
+
});
|
|
890
|
+
fileLog({
|
|
891
|
+
level: report.expected ? 'info' : 'warn',
|
|
892
|
+
event: 'workflow_no_verdict',
|
|
893
|
+
repo: `${owner}/${repo}`,
|
|
894
|
+
pr: number,
|
|
895
|
+
cause: report.cause,
|
|
896
|
+
expected: report.expected,
|
|
897
|
+
verdict_steps: report.verdictSteps,
|
|
898
|
+
...(accumulatedStepOutcomes && {
|
|
899
|
+
ran: accumulatedStepOutcomes.ran,
|
|
900
|
+
skipped: accumulatedStepOutcomes.skipped,
|
|
901
|
+
}),
|
|
902
|
+
});
|
|
903
|
+
// Exit code is unchanged: a run with no verdict is a legitimate outcome,
|
|
904
|
+
// not a failure, and the exit codes are part of the CLI contract. The
|
|
905
|
+
// marker is the signal — ⚠ only when a verdict was actually in reach.
|
|
906
|
+
console.log(report.expected
|
|
907
|
+
? chalk.green(`\n✓ Workflow complete — ${prUrl}\n`)
|
|
908
|
+
: chalk.yellow(`\n⚠ Workflow complete, no verdict — ${prUrl}\n`));
|
|
733
909
|
}
|
|
734
910
|
else {
|
|
735
911
|
console.log(`\n ${formatVerdict(verdict)}`);
|
|
912
|
+
console.log(chalk.green(`\n✓ Workflow complete — ${prUrl}\n`));
|
|
736
913
|
}
|
|
737
|
-
// Exit code is unchanged: a skipped step is a legitimate outcome, not a
|
|
738
|
-
// failure, and the exit codes are part of the CLI contract.
|
|
739
|
-
console.log(ranNothing && !strategySkipped
|
|
740
|
-
? chalk.yellow(`\n⚠ Workflow complete, no step ran — ${prUrl}\n`)
|
|
741
|
-
: chalk.green(`\n✓ Workflow complete — ${prUrl}\n`));
|
|
742
914
|
}
|
|
743
915
|
catch (err) {
|
|
744
916
|
workflowError = err;
|
|
@@ -820,9 +992,19 @@ export async function runRunSpec(spec, opts = {}) {
|
|
|
820
992
|
process.exit(1);
|
|
821
993
|
}
|
|
822
994
|
if (refs.length === 1) {
|
|
823
|
-
await runRun(refs[0].url,
|
|
995
|
+
await runRun(refs[0].url, {
|
|
996
|
+
...opts,
|
|
997
|
+
...(refs[0].commentId !== undefined && { reviewCommentId: refs[0].commentId }),
|
|
998
|
+
});
|
|
824
999
|
return;
|
|
825
1000
|
}
|
|
1001
|
+
// Past this point the spec fans out across PRs; a comment anchor names exactly
|
|
1002
|
+
// one review and cannot be applied to all of them.
|
|
1003
|
+
const anchored = refs.filter(r => r.commentId !== undefined);
|
|
1004
|
+
if (anchored.length > 0) {
|
|
1005
|
+
console.error(chalk.red('✗ an #issuecomment- anchor targets a single review comment and cannot be combined with multiple PRs'));
|
|
1006
|
+
process.exit(1);
|
|
1007
|
+
}
|
|
826
1008
|
// expected-head-sha is a single-PR guard (set by kickass dispatch); it can't
|
|
827
1009
|
// apply to a fan-out across many heads.
|
|
828
1010
|
if (opts.expectedHeadSha !== undefined) {
|