@open-cr-agent/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +3 -0
- package/dist/anchor/anchor.d.ts +24 -0
- package/dist/anchor/anchor.js +66 -0
- package/dist/anchor/index.d.ts +3 -0
- package/dist/anchor/index.js +3 -0
- package/dist/anchor/match.d.ts +22 -0
- package/dist/anchor/match.js +94 -0
- package/dist/anchor/relocate.d.ts +5 -0
- package/dist/anchor/relocate.js +38 -0
- package/dist/bundle/bundle.d.ts +22 -0
- package/dist/bundle/bundle.js +102 -0
- package/dist/bundle/grouping.d.ts +16 -0
- package/dist/bundle/grouping.js +25 -0
- package/dist/bundle/index.d.ts +3 -0
- package/dist/bundle/index.js +3 -0
- package/dist/contracts.d.ts +77 -0
- package/dist/contracts.js +2 -0
- package/dist/diff/hunks.d.ts +3 -0
- package/dist/diff/hunks.js +64 -0
- package/dist/diff/index.d.ts +3 -0
- package/dist/diff/index.js +3 -0
- package/dist/diff/parse.d.ts +3 -0
- package/dist/diff/parse.js +116 -0
- package/dist/diff/quoted-path.d.ts +6 -0
- package/dist/diff/quoted-path.js +46 -0
- package/dist/domain.d.ts +126 -0
- package/dist/domain.js +23 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +16 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/judge/index.d.ts +4 -0
- package/dist/judge/index.js +4 -0
- package/dist/judge/judge.d.ts +46 -0
- package/dist/judge/judge.js +122 -0
- package/dist/judge/prompt.d.ts +25 -0
- package/dist/judge/prompt.js +62 -0
- package/dist/judge/verdict.d.ts +9 -0
- package/dist/judge/verdict.js +26 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/memory.d.ts +19 -0
- package/dist/memory/memory.js +49 -0
- package/dist/pipeline/budget.d.ts +9 -0
- package/dist/pipeline/budget.js +19 -0
- package/dist/pipeline/context.d.ts +6 -0
- package/dist/pipeline/context.js +71 -0
- package/dist/pipeline/execute.d.ts +32 -0
- package/dist/pipeline/execute.js +122 -0
- package/dist/pipeline/findings.d.ts +6 -0
- package/dist/pipeline/findings.js +44 -0
- package/dist/pipeline/helpers.d.ts +6 -0
- package/dist/pipeline/helpers.js +32 -0
- package/dist/pipeline/index.d.ts +9 -0
- package/dist/pipeline/index.js +9 -0
- package/dist/pipeline/matrix.d.ts +38 -0
- package/dist/pipeline/matrix.js +93 -0
- package/dist/pipeline/output.d.ts +69 -0
- package/dist/pipeline/output.js +123 -0
- package/dist/pipeline/plan.d.ts +34 -0
- package/dist/pipeline/plan.js +68 -0
- package/dist/pipeline/pool.d.ts +2 -0
- package/dist/pipeline/pool.js +13 -0
- package/dist/pipeline/preview.d.ts +42 -0
- package/dist/pipeline/preview.js +73 -0
- package/dist/pipeline/report.d.ts +118 -0
- package/dist/pipeline/report.js +27 -0
- package/dist/pipeline/run.d.ts +36 -0
- package/dist/pipeline/run.js +301 -0
- package/dist/pipeline/task.d.ts +19 -0
- package/dist/pipeline/task.js +143 -0
- package/dist/pipeline/usage.d.ts +4 -0
- package/dist/pipeline/usage.js +13 -0
- package/dist/plugin/builtin.d.ts +14 -0
- package/dist/plugin/builtin.js +47 -0
- package/dist/plugin/host.d.ts +9 -0
- package/dist/plugin/host.js +78 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +5 -0
- package/dist/plugin/registry.d.ts +37 -0
- package/dist/plugin/registry.js +104 -0
- package/dist/plugin/types.d.ts +53 -0
- package/dist/plugin/types.js +2 -0
- package/dist/rereview/index.d.ts +4 -0
- package/dist/rereview/index.js +4 -0
- package/dist/rereview/presence.d.ts +3 -0
- package/dist/rereview/presence.js +27 -0
- package/dist/rereview/quote.d.ts +4 -0
- package/dist/rereview/quote.js +28 -0
- package/dist/rereview/reconcile.d.ts +20 -0
- package/dist/rereview/reconcile.js +55 -0
- package/dist/review/impact.d.ts +11 -0
- package/dist/review/impact.js +53 -0
- package/dist/review/index.d.ts +10 -0
- package/dist/review/index.js +10 -0
- package/dist/review/plan-phase.d.ts +10 -0
- package/dist/review/plan-phase.js +33 -0
- package/dist/review/prompt-text.d.ts +16 -0
- package/dist/review/prompt-text.js +90 -0
- package/dist/review/prompt.d.ts +28 -0
- package/dist/review/prompt.js +85 -0
- package/dist/review/reviewer.d.ts +17 -0
- package/dist/review/reviewer.js +2 -0
- package/dist/review/reviewers/agents-md.d.ts +3 -0
- package/dist/review/reviewers/agents-md.js +45 -0
- package/dist/review/reviewers/correctness.d.ts +3 -0
- package/dist/review/reviewers/correctness.js +48 -0
- package/dist/review/reviewers/docs.d.ts +3 -0
- package/dist/review/reviewers/docs.js +47 -0
- package/dist/review/reviewers/performance.d.ts +3 -0
- package/dist/review/reviewers/performance.js +59 -0
- package/dist/review/reviewers/scopes.d.ts +3 -0
- package/dist/review/reviewers/scopes.js +9 -0
- package/dist/review/reviewers/security.d.ts +3 -0
- package/dist/review/reviewers/security.js +52 -0
- package/dist/review/tools.d.ts +9 -0
- package/dist/review/tools.js +8 -0
- package/dist/rules/builtin/general.d.ts +2 -0
- package/dist/rules/builtin/general.js +9 -0
- package/dist/rules/builtin/go.d.ts +2 -0
- package/dist/rules/builtin/go.js +9 -0
- package/dist/rules/builtin/index.d.ts +9 -0
- package/dist/rules/builtin/index.js +16 -0
- package/dist/rules/builtin/java.d.ts +2 -0
- package/dist/rules/builtin/java.js +9 -0
- package/dist/rules/builtin/performance.d.ts +2 -0
- package/dist/rules/builtin/performance.js +9 -0
- package/dist/rules/builtin/python.d.ts +2 -0
- package/dist/rules/builtin/python.js +9 -0
- package/dist/rules/builtin/security.d.ts +2 -0
- package/dist/rules/builtin/security.js +11 -0
- package/dist/rules/builtin/typescript.d.ts +2 -0
- package/dist/rules/builtin/typescript.js +9 -0
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/index.js +5 -0
- package/dist/rules/languages.d.ts +3 -0
- package/dist/rules/languages.js +20 -0
- package/dist/rules/repo-rules.d.ts +15 -0
- package/dist/rules/repo-rules.js +22 -0
- package/dist/rules/resolve.d.ts +4 -0
- package/dist/rules/resolve.js +27 -0
- package/dist/rules/rule-set.d.ts +6 -0
- package/dist/rules/rule-set.js +2 -0
- package/dist/select/index.d.ts +2 -0
- package/dist/select/index.js +2 -0
- package/dist/select/patterns.d.ts +6 -0
- package/dist/select/patterns.js +132 -0
- package/dist/select/select.d.ts +19 -0
- package/dist/select/select.js +52 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/jsonl.d.ts +11 -0
- package/dist/session/jsonl.js +57 -0
- package/dist/triage.d.ts +11 -0
- package/dist/triage.js +55 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/verify/prompt.d.ts +24 -0
- package/dist/verify/prompt.js +71 -0
- package/dist/verify/verify.d.ts +29 -0
- package/dist/verify/verify.js +98 -0
- package/package.json +40 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Usage } from "../contracts.js";
|
|
2
|
+
import type { AnchorMethod, ChangeRequest, Finding, PriorFinding, RiskTier, Verdict } from "../domain.js";
|
|
3
|
+
import type { JudgeDecisions } from "../judge/judge.js";
|
|
4
|
+
import type { MemoryEntry } from "../memory/memory.js";
|
|
5
|
+
import type { ExclusionReason } from "../select/select.js";
|
|
6
|
+
import type { RefutedFinding } from "../verify/verify.js";
|
|
7
|
+
import type { SkippedCell } from "./matrix.js";
|
|
8
|
+
export type CoverageEntry = {
|
|
9
|
+
path: string;
|
|
10
|
+
status: "reviewed" | "failed" | "unreviewed" | "unchanged";
|
|
11
|
+
} | {
|
|
12
|
+
path: string;
|
|
13
|
+
status: "excluded";
|
|
14
|
+
reason: ExclusionReason;
|
|
15
|
+
};
|
|
16
|
+
export declare function coverageGaps(coverage: readonly CoverageEntry[]): {
|
|
17
|
+
notReviewed: number;
|
|
18
|
+
nothingReviewed: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type TaskStatus = "completed" | "failed" | "timed_out" | "cancelled";
|
|
21
|
+
export interface TaskOutcome {
|
|
22
|
+
taskId: string;
|
|
23
|
+
reviewer: string;
|
|
24
|
+
bundle: string;
|
|
25
|
+
files: string[];
|
|
26
|
+
status: TaskStatus;
|
|
27
|
+
error?: string;
|
|
28
|
+
findings: number;
|
|
29
|
+
durationMs: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ReviewReport {
|
|
32
|
+
changeRequest: ChangeRequest;
|
|
33
|
+
tier: RiskTier;
|
|
34
|
+
verdict: Verdict;
|
|
35
|
+
summary: string;
|
|
36
|
+
coverage: CoverageEntry[];
|
|
37
|
+
bundles: {
|
|
38
|
+
label: string;
|
|
39
|
+
files: string[];
|
|
40
|
+
}[];
|
|
41
|
+
tasks: TaskOutcome[];
|
|
42
|
+
skipped: SkippedCell[];
|
|
43
|
+
findings: Finding[];
|
|
44
|
+
unverifiedCriticals: number;
|
|
45
|
+
refuted: RefutedFinding[];
|
|
46
|
+
remembered: MemoryEntry[];
|
|
47
|
+
judgement?: JudgeDecisions;
|
|
48
|
+
scope?: {
|
|
49
|
+
mode: "incremental";
|
|
50
|
+
since: string;
|
|
51
|
+
} | {
|
|
52
|
+
mode: "full";
|
|
53
|
+
reason: string;
|
|
54
|
+
};
|
|
55
|
+
rereview?: {
|
|
56
|
+
fixed: PriorFinding[];
|
|
57
|
+
notReproduced: PriorFinding[];
|
|
58
|
+
notRechecked: PriorFinding[];
|
|
59
|
+
unchanged: PriorFinding[];
|
|
60
|
+
dismissed: PriorFinding[];
|
|
61
|
+
};
|
|
62
|
+
anchoring?: AnchoringSummary;
|
|
63
|
+
usage: Usage;
|
|
64
|
+
warnings: string[];
|
|
65
|
+
}
|
|
66
|
+
export type ReviewEvent = {
|
|
67
|
+
type: "run_started";
|
|
68
|
+
changeRequest: ChangeRequest;
|
|
69
|
+
} | {
|
|
70
|
+
type: "files_selected";
|
|
71
|
+
selected: number;
|
|
72
|
+
excluded: number;
|
|
73
|
+
tier: RiskTier;
|
|
74
|
+
} | {
|
|
75
|
+
type: "files_bundled";
|
|
76
|
+
strategy: string;
|
|
77
|
+
bundles: number;
|
|
78
|
+
warnings: string[];
|
|
79
|
+
} | {
|
|
80
|
+
type: "matrix_planned";
|
|
81
|
+
tasks: number;
|
|
82
|
+
skipped: SkippedCell[];
|
|
83
|
+
} | {
|
|
84
|
+
type: "task_started";
|
|
85
|
+
taskId: string;
|
|
86
|
+
reviewer: string;
|
|
87
|
+
bundle: string;
|
|
88
|
+
files: string[];
|
|
89
|
+
} | {
|
|
90
|
+
type: "task_progress";
|
|
91
|
+
taskId: string;
|
|
92
|
+
message: string;
|
|
93
|
+
} | {
|
|
94
|
+
type: "finding";
|
|
95
|
+
taskId: string;
|
|
96
|
+
finding: Finding;
|
|
97
|
+
} | {
|
|
98
|
+
type: "task_finished";
|
|
99
|
+
outcome: TaskOutcome;
|
|
100
|
+
} | {
|
|
101
|
+
type: "verification_finished";
|
|
102
|
+
checked: number;
|
|
103
|
+
refuted: RefutedFinding[];
|
|
104
|
+
} | {
|
|
105
|
+
type: "judge_finished";
|
|
106
|
+
verdict: Verdict;
|
|
107
|
+
judgement?: JudgeDecisions;
|
|
108
|
+
} | {
|
|
109
|
+
type: "run_finished";
|
|
110
|
+
report: ReviewReport;
|
|
111
|
+
};
|
|
112
|
+
export interface AnchoringSummary {
|
|
113
|
+
byMethod: Record<AnchorMethod, number>;
|
|
114
|
+
ambiguous: number;
|
|
115
|
+
relocationCalls: number;
|
|
116
|
+
}
|
|
117
|
+
export declare function summarizeAnchoring(findings: readonly Finding[], relocationCalls: number): AnchoringSummary;
|
|
118
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// How much of the selection this run actually reviewed. Every surface (exit
|
|
2
|
+
// code, terminal, pull request summary) reads it from here, so none of them
|
|
3
|
+
// can call a run that reviewed nothing "approved".
|
|
4
|
+
export function coverageGaps(coverage) {
|
|
5
|
+
const notReviewed = coverage.filter((c) => c.status === "failed" || c.status === "unreviewed").length;
|
|
6
|
+
// Unchanged files were reviewed by an earlier run, and their findings and
|
|
7
|
+
// verdict carry over: a re-review whose new files all failed still has them.
|
|
8
|
+
const reviewed = coverage.some((c) => c.status === "reviewed" || c.status === "unchanged");
|
|
9
|
+
return { notReviewed, nothingReviewed: notReviewed > 0 && !reviewed };
|
|
10
|
+
}
|
|
11
|
+
export function summarizeAnchoring(findings, relocationCalls) {
|
|
12
|
+
const byMethod = {
|
|
13
|
+
hunk: 0,
|
|
14
|
+
file: 0,
|
|
15
|
+
cross_file: 0,
|
|
16
|
+
relocated: 0,
|
|
17
|
+
file_level: 0,
|
|
18
|
+
};
|
|
19
|
+
for (const f of findings)
|
|
20
|
+
byMethod[f.anchor.method] += 1;
|
|
21
|
+
return {
|
|
22
|
+
byMethod,
|
|
23
|
+
ambiguous: findings.filter((f) => f.anchor.ambiguous).length,
|
|
24
|
+
relocationCalls,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AnchorContext } from "../anchor/anchor.js";
|
|
2
|
+
import type { BundlePolicy } from "../bundle/bundle.js";
|
|
3
|
+
import type { FileGrouper } from "../bundle/grouping.js";
|
|
4
|
+
import type { AgentRuntime, VcsAdapter } from "../contracts.js";
|
|
5
|
+
import type { ReviewerDefinition } from "../review/reviewer.js";
|
|
6
|
+
import type { RepoRule } from "../rules/repo-rules.js";
|
|
7
|
+
import type { SelectionPolicy } from "../select/select.js";
|
|
8
|
+
import { type ReviewerOverrides } from "./matrix.js";
|
|
9
|
+
import { type ReviewEvent, type ReviewReport } from "./report.js";
|
|
10
|
+
export { GUIDELINES_PATH } from "./plan.js";
|
|
11
|
+
export interface ReviewOptions {
|
|
12
|
+
vcs: VcsAdapter;
|
|
13
|
+
runtime: AgentRuntime;
|
|
14
|
+
reviewers?: readonly ReviewerDefinition[];
|
|
15
|
+
reviewerOverrides?: ReviewerOverrides;
|
|
16
|
+
rules?: readonly RepoRule[];
|
|
17
|
+
readTrusted?: (path: string) => Promise<string | undefined>;
|
|
18
|
+
selection?: SelectionPolicy;
|
|
19
|
+
bundling?: BundlePolicy;
|
|
20
|
+
grouper?: FileGrouper;
|
|
21
|
+
relocate?: AnchorContext["relocate"] | false;
|
|
22
|
+
concurrency?: number;
|
|
23
|
+
taskTimeoutMs?: number;
|
|
24
|
+
runTimeoutMs?: number;
|
|
25
|
+
abortGraceMs?: number;
|
|
26
|
+
verify?: boolean;
|
|
27
|
+
judge?: boolean;
|
|
28
|
+
maxCostUsd?: number;
|
|
29
|
+
maxTasks?: number;
|
|
30
|
+
fullReview?: boolean;
|
|
31
|
+
ultra?: boolean;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
onEvent?: (event: ReviewEvent) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare function runReview(options: ReviewOptions): Promise<ReviewReport>;
|
|
36
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { runtimeRelocator } from "../anchor/relocate.js";
|
|
2
|
+
import { errorMessage } from "../errors.js";
|
|
3
|
+
import { judgeFindings } from "../judge/judge.js";
|
|
4
|
+
import { applyMemory } from "../memory/memory.js";
|
|
5
|
+
import { priorCodePresence } from "../rereview/presence.js";
|
|
6
|
+
import { reconcile, stillOpen } from "../rereview/reconcile.js";
|
|
7
|
+
import { correctnessReviewer } from "../review/reviewers/correctness.js";
|
|
8
|
+
import { markUnchecked, verifyFindings } from "../verify/verify.js";
|
|
9
|
+
import { spendTracker } from "./budget.js";
|
|
10
|
+
import { runJob } from "./execute.js";
|
|
11
|
+
import { dedupeFindings } from "./findings.js";
|
|
12
|
+
import { DEFAULT_MAX_TASKS, planTasks } from "./matrix.js";
|
|
13
|
+
import { planReview } from "./plan.js";
|
|
14
|
+
import { mapWithConcurrency } from "./pool.js";
|
|
15
|
+
import { coverageGaps, summarizeAnchoring, } from "./report.js";
|
|
16
|
+
import { addUsage, emptyUsage } from "./usage.js";
|
|
17
|
+
export { GUIDELINES_PATH } from "./plan.js";
|
|
18
|
+
const DEFAULTS = { concurrency: 4, taskTimeoutMs: 10 * 60_000, runTimeoutMs: 25 * 60_000 };
|
|
19
|
+
// Plan (deterministic stages) → execute (one agent task per cell) → report.
|
|
20
|
+
export async function runReview(options) {
|
|
21
|
+
const emit = options.onEvent ?? (() => { });
|
|
22
|
+
const timeout = AbortSignal.timeout(options.runTimeoutMs ?? DEFAULTS.runTimeoutMs);
|
|
23
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
24
|
+
const reviewers = options.reviewers ?? [correctnessReviewer];
|
|
25
|
+
if (reviewers.length === 0)
|
|
26
|
+
throw new Error("No reviewer is registered");
|
|
27
|
+
const prior = await loadPriorReview(options.vcs);
|
|
28
|
+
const scope = reviewScope(prior.review, options.fullReview === true);
|
|
29
|
+
const plan = await planReview(scope.only
|
|
30
|
+
? {
|
|
31
|
+
...options,
|
|
32
|
+
reviewOnly: scope.only,
|
|
33
|
+
...(prior.review?.tier ? { priorTier: prior.review.tier } : {}),
|
|
34
|
+
}
|
|
35
|
+
: options, emit, signal);
|
|
36
|
+
const matrix = planTasks(plan.bundles, reviewers, plan.tier, options.reviewerOverrides, {
|
|
37
|
+
ultra: options.ultra === true,
|
|
38
|
+
...(options.maxTasks !== undefined ? { maxTasks: options.maxTasks } : {}),
|
|
39
|
+
hasGuidelines: Boolean(plan.guidelines?.trim()),
|
|
40
|
+
});
|
|
41
|
+
const limited = matrix.skipped.filter((s) => s.reason === "task_limit").length;
|
|
42
|
+
if (limited > 0) {
|
|
43
|
+
plan.warnings.push(`task limit of ${options.maxTasks ?? DEFAULT_MAX_TASKS} reached: ${limited} review task(s) skipped; their files are reported as not reviewed`);
|
|
44
|
+
}
|
|
45
|
+
emit({ type: "matrix_planned", tasks: matrix.cells.length, skipped: matrix.skipped });
|
|
46
|
+
const relocationUsage = [];
|
|
47
|
+
const budget = spendTracker(options.maxCostUsd, plan.usage);
|
|
48
|
+
const relocator = options.relocate === false
|
|
49
|
+
? undefined
|
|
50
|
+
: (options.relocate ??
|
|
51
|
+
runtimeRelocator(options.runtime, signal, (u) => {
|
|
52
|
+
relocationUsage.push(u);
|
|
53
|
+
budget.add(u);
|
|
54
|
+
}));
|
|
55
|
+
const execute = {
|
|
56
|
+
runtime: options.runtime,
|
|
57
|
+
taskTimeoutMs: options.taskTimeoutMs ?? DEFAULTS.taskTimeoutMs,
|
|
58
|
+
abortGraceMs: options.abortGraceMs,
|
|
59
|
+
// Past the spend limit a quote that does not match stays file-level.
|
|
60
|
+
relocate: relocator &&
|
|
61
|
+
(async (request) => (budget.exhausted() ? undefined : relocator(request))),
|
|
62
|
+
ultra: options.ultra === true,
|
|
63
|
+
plans: new Map(),
|
|
64
|
+
emit,
|
|
65
|
+
signal,
|
|
66
|
+
};
|
|
67
|
+
const results = await mapWithConcurrency(matrix.cells, options.concurrency ?? DEFAULTS.concurrency, async (cell) => {
|
|
68
|
+
// Once the run is cancelled or timed out, remaining cells are not started.
|
|
69
|
+
if (signal.aborted)
|
|
70
|
+
return skipCell(cell, "run cancelled before this task started", emit);
|
|
71
|
+
if (budget.reviewExhausted()) {
|
|
72
|
+
return skipCell(cell, `spend limit of $${options.maxCostUsd} reached`, emit);
|
|
73
|
+
}
|
|
74
|
+
const result = await runJob(cell, plan, execute);
|
|
75
|
+
budget.add(result.usage);
|
|
76
|
+
return result;
|
|
77
|
+
});
|
|
78
|
+
// Memory and the previous review filter first, so Verify and Judge are not
|
|
79
|
+
// paid for findings that will not be reported, and a person's dismissal
|
|
80
|
+
// keeps a finding out of the verdict whatever the models say.
|
|
81
|
+
const concurrency = options.concurrency ?? DEFAULTS.concurrency;
|
|
82
|
+
const found = dedupeFindings(results.flatMap((r) => r.findings));
|
|
83
|
+
const fileCoverage = coverage(plan.decisions, results, plan.unchanged, matrix.limited ?? []);
|
|
84
|
+
const remembered = applyMemory(found, plan.memory);
|
|
85
|
+
const reported = new Set(found.map((f) => f.fingerprint));
|
|
86
|
+
const priorReview = withoutRemembered(prior.review, plan.memory);
|
|
87
|
+
const reconciled = reconcile({
|
|
88
|
+
findings: remembered.kept,
|
|
89
|
+
reported,
|
|
90
|
+
prior: priorReview,
|
|
91
|
+
coverage: fileCoverage,
|
|
92
|
+
stillPresent: await priorCodePresence(priorReview, reported, plan.context.readFile),
|
|
93
|
+
});
|
|
94
|
+
const verification = options.verify === false
|
|
95
|
+
? {
|
|
96
|
+
checked: 0,
|
|
97
|
+
kept: markUnchecked(reconciled.findings),
|
|
98
|
+
refuted: [],
|
|
99
|
+
missed: [],
|
|
100
|
+
usage: [],
|
|
101
|
+
warnings: [],
|
|
102
|
+
}
|
|
103
|
+
: await verifyFindings(reconciled.findings, {
|
|
104
|
+
runtime: options.runtime,
|
|
105
|
+
diffs: plan.selected,
|
|
106
|
+
context: plan.context,
|
|
107
|
+
signal,
|
|
108
|
+
concurrency,
|
|
109
|
+
budget,
|
|
110
|
+
});
|
|
111
|
+
if (verification.checked > 0) {
|
|
112
|
+
emit({
|
|
113
|
+
type: "verification_finished",
|
|
114
|
+
checked: verification.checked,
|
|
115
|
+
refuted: verification.refuted,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const judgeWanted = options.judge !== false && verification.kept.length > 0;
|
|
119
|
+
const judgeAffordable = !budget.exhausted();
|
|
120
|
+
const judged = await judgeFindings(verification.kept, {
|
|
121
|
+
runtime: options.runtime,
|
|
122
|
+
changeRequest: plan.changeRequest,
|
|
123
|
+
tier: plan.tier,
|
|
124
|
+
signal,
|
|
125
|
+
enabled: judgeWanted && judgeAffordable,
|
|
126
|
+
keepDropped: options.ultra === true,
|
|
127
|
+
carried: stillOpen(reconciled),
|
|
128
|
+
});
|
|
129
|
+
if (judgeWanted && !judgeAffordable) {
|
|
130
|
+
judged.warnings.push(`spend limit of $${options.maxCostUsd} reached: findings were not judged`);
|
|
131
|
+
}
|
|
132
|
+
const { nothingReviewed } = coverageGaps(fileCoverage);
|
|
133
|
+
if (!nothingReviewed) {
|
|
134
|
+
emit(judged.decisions
|
|
135
|
+
? { type: "judge_finished", verdict: judged.verdict, judgement: judged.decisions }
|
|
136
|
+
: { type: "judge_finished", verdict: judged.verdict });
|
|
137
|
+
}
|
|
138
|
+
const report = {
|
|
139
|
+
changeRequest: plan.changeRequest,
|
|
140
|
+
tier: plan.tier,
|
|
141
|
+
verdict: judged.verdict,
|
|
142
|
+
summary: nothingReviewed
|
|
143
|
+
? "Nothing was reviewed: no reviewer covered or finished any selected file."
|
|
144
|
+
: judged.summary,
|
|
145
|
+
coverage: fileCoverage,
|
|
146
|
+
bundles: plan.bundles.map((b) => ({ label: b.label, files: b.files.map((f) => f.newPath) })),
|
|
147
|
+
tasks: results.map((r) => r.outcome),
|
|
148
|
+
skipped: matrix.skipped,
|
|
149
|
+
findings: sortFindings(judged.findings),
|
|
150
|
+
// Counted before the judge: dropping or downgrading a critical nobody
|
|
151
|
+
// could check must not turn an incomplete run into a clean one.
|
|
152
|
+
unverifiedCriticals: countMissedCriticals(verification.kept, verification.missed),
|
|
153
|
+
refuted: verification.refuted,
|
|
154
|
+
remembered: remembered.remembered,
|
|
155
|
+
usage: sumUsage([
|
|
156
|
+
...plan.usage,
|
|
157
|
+
...results.map((r) => r.usage),
|
|
158
|
+
...relocationUsage,
|
|
159
|
+
...verification.usage,
|
|
160
|
+
...judged.usage,
|
|
161
|
+
]),
|
|
162
|
+
warnings: [
|
|
163
|
+
...plan.warnings,
|
|
164
|
+
...results.flatMap((r) => r.warnings),
|
|
165
|
+
...verification.warnings,
|
|
166
|
+
...judged.warnings,
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
if (judged.decisions)
|
|
170
|
+
report.judgement = judged.decisions;
|
|
171
|
+
report.anchoring = summarizeAnchoring(report.findings, relocationUsage.length);
|
|
172
|
+
if (prior.review) {
|
|
173
|
+
report.rereview = {
|
|
174
|
+
fixed: reconciled.fixed,
|
|
175
|
+
notReproduced: reconciled.notReproduced,
|
|
176
|
+
notRechecked: reconciled.notRechecked,
|
|
177
|
+
unchanged: reconciled.unchanged,
|
|
178
|
+
dismissed: reconciled.dismissed,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (plan.widened) {
|
|
182
|
+
report.scope = {
|
|
183
|
+
mode: "full",
|
|
184
|
+
reason: `the risk tier rose from ${plan.widened.from} to ${plan.widened.to}, which adds reviewers`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
else if (scope.note)
|
|
188
|
+
report.scope = scope.note;
|
|
189
|
+
if (prior.warning)
|
|
190
|
+
report.warnings.push(prior.warning);
|
|
191
|
+
emit({ type: "run_finished", report });
|
|
192
|
+
return report;
|
|
193
|
+
}
|
|
194
|
+
function skipCell(cell, reason, emit) {
|
|
195
|
+
const outcome = {
|
|
196
|
+
taskId: cell.taskId,
|
|
197
|
+
reviewer: cell.reviewer.id,
|
|
198
|
+
bundle: cell.bundle.label,
|
|
199
|
+
files: cell.bundle.files.map((f) => f.newPath),
|
|
200
|
+
status: "cancelled",
|
|
201
|
+
error: reason,
|
|
202
|
+
findings: 0,
|
|
203
|
+
durationMs: 0,
|
|
204
|
+
};
|
|
205
|
+
emit({ type: "task_finished", outcome });
|
|
206
|
+
return { outcome, findings: [], usage: emptyUsage(), warnings: [] };
|
|
207
|
+
}
|
|
208
|
+
// An earlier finding the team has since accepted is no longer open.
|
|
209
|
+
function withoutRemembered(review, memory) {
|
|
210
|
+
if (!review)
|
|
211
|
+
return undefined;
|
|
212
|
+
const accepted = new Set(memory.map((e) => e.fingerprint));
|
|
213
|
+
// Everything else the earlier review carries (replies, tier, head) stays.
|
|
214
|
+
return { ...review, findings: review.findings.filter((f) => !accepted.has(f.fingerprint)) };
|
|
215
|
+
}
|
|
216
|
+
// Review only what changed since the earlier review when the platform can
|
|
217
|
+
// tell; otherwise everything, with the reason in the report.
|
|
218
|
+
function reviewScope(review, full) {
|
|
219
|
+
if (!review)
|
|
220
|
+
return {};
|
|
221
|
+
if (full)
|
|
222
|
+
return { note: { mode: "full", reason: "a full review was requested" } };
|
|
223
|
+
if (review.changedSince) {
|
|
224
|
+
return {
|
|
225
|
+
only: new Set(review.changedSince.files),
|
|
226
|
+
note: { mode: "incremental", since: review.changedSince.head },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const reason = review.fullReviewReason ?? "the platform cannot tell what changed since";
|
|
230
|
+
return { note: { mode: "full", reason } };
|
|
231
|
+
}
|
|
232
|
+
// A missing earlier review only costs the comparison, never the review.
|
|
233
|
+
async function loadPriorReview(vcs) {
|
|
234
|
+
try {
|
|
235
|
+
const review = await vcs.getPriorReview();
|
|
236
|
+
return review ? { review } : {};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
return { warning: `could not load the previous review: ${errorMessage(error)}` };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function coverage(decisions, results, unchanged, limited) {
|
|
243
|
+
// A file is reviewed when every reviewer assigned to it finished at least
|
|
244
|
+
// one of its tasks: under --ultra one completed sample is enough. A
|
|
245
|
+
// reviewer whose task failed makes the file "failed"; one the task limit
|
|
246
|
+
// never started makes it "unreviewed".
|
|
247
|
+
const done = new Map();
|
|
248
|
+
const ran = new Set();
|
|
249
|
+
const key = (reviewer, file) => `${reviewer}\0${file}`;
|
|
250
|
+
for (const { outcome } of results) {
|
|
251
|
+
for (const file of outcome.files) {
|
|
252
|
+
const k = key(outcome.reviewer, file);
|
|
253
|
+
ran.add(k);
|
|
254
|
+
done.set(k, done.get(k) === true || outcome.status === "completed");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const cell of limited) {
|
|
258
|
+
for (const f of cell.bundle.files) {
|
|
259
|
+
const k = key(cell.reviewer.id, f.newPath);
|
|
260
|
+
if (!done.has(k))
|
|
261
|
+
done.set(k, false);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const status = new Map();
|
|
265
|
+
for (const [k, completed] of done) {
|
|
266
|
+
const file = k.split("\0")[1];
|
|
267
|
+
const now = completed ? "reviewed" : ran.has(k) ? "failed" : "unreviewed";
|
|
268
|
+
const before = status.get(file);
|
|
269
|
+
// failed outranks unreviewed, which outranks reviewed.
|
|
270
|
+
if (!before || now === "failed" || (now === "unreviewed" && before === "reviewed")) {
|
|
271
|
+
status.set(file, now);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return decisions.map((d) => {
|
|
275
|
+
const path = d.diff.newPath;
|
|
276
|
+
if (!d.selected)
|
|
277
|
+
return { path, status: "excluded", reason: d.reason };
|
|
278
|
+
if (unchanged.has(path))
|
|
279
|
+
return { path, status: "unchanged" };
|
|
280
|
+
return { path, status: status.get(path) ?? "unreviewed" };
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const SEVERITY_ORDER = { critical: 0, warning: 1, suggestion: 2 };
|
|
284
|
+
function sortFindings(findings) {
|
|
285
|
+
return findings.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] ||
|
|
286
|
+
a.file.localeCompare(b.file) ||
|
|
287
|
+
(a.lineRange?.start ?? 0) - (b.lineRange?.start ?? 0));
|
|
288
|
+
}
|
|
289
|
+
function sumUsage(usages) {
|
|
290
|
+
return usages.reduce(addUsage, emptyUsage());
|
|
291
|
+
}
|
|
292
|
+
// Low-confidence findings do not count toward the verdict, so their missed
|
|
293
|
+
// verification does not make the run incomplete either.
|
|
294
|
+
function countMissedCriticals(findings, missed) {
|
|
295
|
+
const set = new Set(missed);
|
|
296
|
+
return findings.filter((f) => f.severity === "critical" &&
|
|
297
|
+
!f.lowConfidence &&
|
|
298
|
+
f.verification === "unchecked" &&
|
|
299
|
+
set.has(f.fingerprint)).length;
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentRuntime, AgentTaskSpec, Usage } from "../contracts.js";
|
|
2
|
+
import { type ReportedFinding } from "../domain.js";
|
|
3
|
+
import type { TaskStatus } from "./report.js";
|
|
4
|
+
export interface TaskResult {
|
|
5
|
+
status: TaskStatus;
|
|
6
|
+
error?: string;
|
|
7
|
+
findings: ReportedFinding[];
|
|
8
|
+
usage: Usage;
|
|
9
|
+
warnings: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface TaskCallbacks {
|
|
12
|
+
onProgress(message: string): void;
|
|
13
|
+
category: string;
|
|
14
|
+
abortGraceMs?: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function executeTask(runtime: AgentRuntime, spec: AgentTaskSpec, runSignal: AbortSignal, callbacks: TaskCallbacks): Promise<TaskResult>;
|
|
17
|
+
export declare const ABORT_GRACE_MS = 10000;
|
|
18
|
+
export declare const MAX_FINDINGS_PER_TASK = 50;
|
|
19
|
+
//# sourceMappingURL=task.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { reportedFindingSchema } from "../domain.js";
|
|
2
|
+
import { errorMessage } from "../errors.js";
|
|
3
|
+
import { addUsage, emptyUsage } from "./usage.js";
|
|
4
|
+
export async function executeTask(runtime, spec, runSignal, callbacks) {
|
|
5
|
+
const timeout = AbortSignal.timeout(spec.timeoutMs);
|
|
6
|
+
const signal = AbortSignal.any([runSignal, timeout]);
|
|
7
|
+
const result = {
|
|
8
|
+
status: "completed",
|
|
9
|
+
findings: [],
|
|
10
|
+
usage: emptyUsage(),
|
|
11
|
+
warnings: [],
|
|
12
|
+
};
|
|
13
|
+
let iterator;
|
|
14
|
+
let pending;
|
|
15
|
+
try {
|
|
16
|
+
iterator = runtime.runTask(spec, signal)[Symbol.asyncIterator]();
|
|
17
|
+
for (;;) {
|
|
18
|
+
pending = iterator.next();
|
|
19
|
+
const next = await untilAborted(pending, signal);
|
|
20
|
+
pending = undefined;
|
|
21
|
+
if (next.done)
|
|
22
|
+
break;
|
|
23
|
+
if (handle(next.value, result, callbacks)) {
|
|
24
|
+
void iterator.return?.();
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (signal.aborted) {
|
|
31
|
+
const timedOut = timeout.aborted && !runSignal.aborted;
|
|
32
|
+
result.status = timedOut ? "timed_out" : "cancelled";
|
|
33
|
+
result.error = timedOut ? `timed out after ${spec.timeoutMs}ms` : "run cancelled";
|
|
34
|
+
if (iterator)
|
|
35
|
+
await collectAfterAbort(iterator, pending, result, callbacks);
|
|
36
|
+
void iterator?.return?.();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
result.status = "failed";
|
|
40
|
+
result.error = errorMessage(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
// A task cut off by its timeout or by Ctrl-C has still spent tokens and may
|
|
46
|
+
// have reported findings; a runtime that stops its session delivers them
|
|
47
|
+
// shortly after. Waiting a bounded moment keeps them in the report and in
|
|
48
|
+
// the spend limit, without letting a runtime that ignores the abort hang.
|
|
49
|
+
export const ABORT_GRACE_MS = 10_000;
|
|
50
|
+
async function collectAfterAbort(iterator, pending, result, callbacks) {
|
|
51
|
+
const grace = AbortSignal.timeout(callbacks.abortGraceMs ?? ABORT_GRACE_MS);
|
|
52
|
+
let next = pending ?? iterator.next();
|
|
53
|
+
try {
|
|
54
|
+
for (;;) {
|
|
55
|
+
const event = await untilAborted(next, grace);
|
|
56
|
+
if (event.done)
|
|
57
|
+
return;
|
|
58
|
+
const { value } = event;
|
|
59
|
+
if (value.type === "usage" || value.type === "finding")
|
|
60
|
+
handle(value, result, callbacks);
|
|
61
|
+
if (value.type === "done" || value.type === "error")
|
|
62
|
+
return;
|
|
63
|
+
next = iterator.next();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// The runtime did not finish within the grace period, or failed: keep
|
|
68
|
+
// what arrived.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function handle(event, result, callbacks) {
|
|
72
|
+
switch (event.type) {
|
|
73
|
+
case "progress":
|
|
74
|
+
callbacks.onProgress(event.message);
|
|
75
|
+
return false;
|
|
76
|
+
case "finding": {
|
|
77
|
+
const parsed = reportedFindingSchema.safeParse(withReviewerCategory(event.finding, callbacks.category));
|
|
78
|
+
if (!parsed.success) {
|
|
79
|
+
result.warnings.push("runtime reported a finding that failed validation");
|
|
80
|
+
}
|
|
81
|
+
else if (result.findings.length >= MAX_FINDINGS_PER_TASK) {
|
|
82
|
+
const warning = `more than ${MAX_FINDINGS_PER_TASK} findings in one task; the rest were dropped`;
|
|
83
|
+
if (!result.warnings.includes(warning))
|
|
84
|
+
result.warnings.push(warning);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
result.findings.push(bounded(parsed.data));
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
case "usage":
|
|
92
|
+
result.usage = addUsage(result.usage, event);
|
|
93
|
+
return false;
|
|
94
|
+
case "done":
|
|
95
|
+
return true;
|
|
96
|
+
case "error":
|
|
97
|
+
result.status = "failed";
|
|
98
|
+
result.error = event.error;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// A reviewer steered by the change could flood the pull request with
|
|
103
|
+
// comments or pad them without end; both are bounded.
|
|
104
|
+
export const MAX_FINDINGS_PER_TASK = 50;
|
|
105
|
+
const MAX_TITLE = 300;
|
|
106
|
+
const MAX_TEXT = 4_000;
|
|
107
|
+
const MAX_EVIDENCE = 10;
|
|
108
|
+
function bounded(f) {
|
|
109
|
+
const cut = (text, max) => (text.length > max ? `${text.slice(0, max)}…` : text);
|
|
110
|
+
return {
|
|
111
|
+
...f,
|
|
112
|
+
title: cut(f.title, MAX_TITLE),
|
|
113
|
+
body: cut(f.body, MAX_TEXT),
|
|
114
|
+
...(f.suggestion !== undefined ? { suggestion: cut(f.suggestion, MAX_TEXT) } : {}),
|
|
115
|
+
evidence: f.evidence.slice(0, MAX_EVIDENCE).map((e) => cut(e, 500)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// The category is part of the fingerprint, so it comes from the reviewer and
|
|
119
|
+
// never from the model: a model choosing another word would make the same
|
|
120
|
+
// issue look new, and its earlier report look fixed.
|
|
121
|
+
function withReviewerCategory(finding, category) {
|
|
122
|
+
if (typeof finding !== "object" || finding === null)
|
|
123
|
+
return finding;
|
|
124
|
+
return { ...finding, category };
|
|
125
|
+
}
|
|
126
|
+
// Runtimes may ignore the abort signal, so waiting for their next event is
|
|
127
|
+
// raced against it to keep the task and run timeouts authoritative.
|
|
128
|
+
function untilAborted(promise, signal) {
|
|
129
|
+
if (signal.aborted)
|
|
130
|
+
return Promise.reject(signal.reason);
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const onAbort = () => reject(signal.reason);
|
|
133
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
134
|
+
promise.then((value) => {
|
|
135
|
+
signal.removeEventListener("abort", onAbort);
|
|
136
|
+
resolve(value);
|
|
137
|
+
}, (error) => {
|
|
138
|
+
signal.removeEventListener("abort", onAbort);
|
|
139
|
+
reject(error);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=task.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function emptyUsage() {
|
|
2
|
+
return { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cachedTokens: 0, costUsd: 0 };
|
|
3
|
+
}
|
|
4
|
+
export function addUsage(total, next) {
|
|
5
|
+
return {
|
|
6
|
+
inputTokens: total.inputTokens + next.inputTokens,
|
|
7
|
+
outputTokens: total.outputTokens + next.outputTokens,
|
|
8
|
+
reasoningTokens: total.reasoningTokens + next.reasoningTokens,
|
|
9
|
+
cachedTokens: total.cachedTokens + next.cachedTokens,
|
|
10
|
+
costUsd: total.costUsd + next.costUsd,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { OcraPlugin } from "./types.js";
|
|
3
|
+
export declare const correctnessReviewerPlugin: OcraPlugin;
|
|
4
|
+
export declare const securityReviewerPlugin: OcraPlugin;
|
|
5
|
+
export declare const performanceReviewerPlugin: OcraPlugin;
|
|
6
|
+
export declare const docsReviewerPlugin: OcraPlugin;
|
|
7
|
+
export declare const agentsMdReviewerPlugin: OcraPlugin;
|
|
8
|
+
declare const sessionSettings: z.ZodObject<{
|
|
9
|
+
dir: z.ZodString;
|
|
10
|
+
id: z.ZodString;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export declare const sessionJsonlPlugin: OcraPlugin<z.infer<typeof sessionSettings>>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=builtin.d.ts.map
|