@open-cr-agent/core 0.5.0 → 0.6.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.
Files changed (128) hide show
  1. package/dist/agent/json.d.ts +2 -0
  2. package/dist/agent/json.js +12 -0
  3. package/dist/{pipeline → agent}/pool.js +2 -1
  4. package/dist/{pipeline/agents.d.ts → agent/settings.d.ts} +10 -5
  5. package/dist/{pipeline/agents.js → agent/settings.js} +15 -6
  6. package/dist/anchor/match.js +5 -4
  7. package/dist/anchor/relocate.d.ts +1 -2
  8. package/dist/anchor/relocate.js +2 -2
  9. package/dist/at.d.ts +2 -0
  10. package/dist/at.js +12 -0
  11. package/dist/bundle/bundle.d.ts +2 -1
  12. package/dist/bundle/bundle.js +3 -2
  13. package/dist/bundle/grouping.d.ts +0 -1
  14. package/dist/bundle/runtime-grouper.d.ts +5 -0
  15. package/dist/{pipeline/helpers.js → bundle/runtime-grouper.js} +5 -14
  16. package/dist/contracts.d.ts +6 -14
  17. package/dist/contracts.js +2 -1
  18. package/dist/diff/hunks.js +3 -2
  19. package/dist/diff/parse.js +5 -4
  20. package/dist/diff/quoted-path.js +16 -15
  21. package/dist/domain.d.ts +10 -2
  22. package/dist/domain.js +2 -2
  23. package/dist/errors.d.ts +3 -1
  24. package/dist/errors.js +12 -0
  25. package/dist/hash.d.ts +2 -0
  26. package/dist/hash.js +7 -0
  27. package/dist/index.d.ts +26 -12
  28. package/dist/index.js +16 -5
  29. package/dist/internal.d.ts +9 -12
  30. package/dist/internal.js +9 -12
  31. package/dist/judge/judge.d.ts +1 -2
  32. package/dist/judge/judge.js +9 -8
  33. package/dist/judge/verdict.d.ts +0 -1
  34. package/dist/judge/verdict.js +1 -1
  35. package/dist/memory/memory.js +2 -2
  36. package/dist/net/proxied-fetch.js +2 -1
  37. package/dist/pipeline/assemble-report.d.ts +22 -0
  38. package/dist/pipeline/assemble-report.js +99 -0
  39. package/dist/pipeline/check-stage.d.ts +13 -0
  40. package/dist/pipeline/check-stage.js +70 -0
  41. package/dist/pipeline/context.d.ts +2 -1
  42. package/dist/pipeline/coverage.d.ts +1 -1
  43. package/dist/pipeline/coverage.js +2 -1
  44. package/dist/pipeline/execute-stage.d.ts +26 -0
  45. package/dist/pipeline/execute-stage.js +104 -0
  46. package/dist/pipeline/execute.d.ts +2 -4
  47. package/dist/pipeline/execute.js +63 -56
  48. package/dist/pipeline/filter-stage.d.ts +16 -0
  49. package/dist/pipeline/filter-stage.js +46 -0
  50. package/dist/pipeline/findings.js +3 -2
  51. package/dist/pipeline/imports.d.ts +1 -1
  52. package/dist/pipeline/imports.js +1 -1
  53. package/dist/pipeline/matrix.d.ts +1 -8
  54. package/dist/pipeline/options.d.ts +65 -0
  55. package/dist/pipeline/options.js +9 -0
  56. package/dist/pipeline/plan-output.d.ts +90 -0
  57. package/dist/pipeline/plan-output.js +109 -0
  58. package/dist/pipeline/plan.d.ts +2 -3
  59. package/dist/pipeline/plan.js +4 -4
  60. package/dist/pipeline/preview.d.ts +23 -9
  61. package/dist/pipeline/preview.js +25 -4
  62. package/dist/pipeline/provenance.d.ts +4 -27
  63. package/dist/pipeline/provenance.js +2 -2
  64. package/dist/pipeline/run.d.ts +2 -48
  65. package/dist/pipeline/run.js +18 -273
  66. package/dist/pipeline/task.d.ts +1 -3
  67. package/dist/pipeline/task.js +4 -4
  68. package/dist/plugin/registry.d.ts +5 -4
  69. package/dist/plugin/registry.js +3 -3
  70. package/dist/plugin/types.d.ts +3 -5
  71. package/dist/{pipeline → report}/output-schema.d.ts +57 -63
  72. package/dist/{pipeline → report}/output-schema.js +55 -40
  73. package/dist/report/output.d.ts +8 -0
  74. package/dist/report/output.js +211 -0
  75. package/dist/report/provenance.d.ts +26 -0
  76. package/dist/report/provenance.js +2 -0
  77. package/dist/report/read.d.ts +3 -0
  78. package/dist/report/read.js +34 -0
  79. package/dist/{pipeline → report}/report.d.ts +1 -1
  80. package/dist/report/serialize.d.ts +3 -0
  81. package/dist/report/serialize.js +27 -0
  82. package/dist/rereview/presence.js +1 -1
  83. package/dist/rereview/quote.js +2 -2
  84. package/dist/rereview/reconcile.d.ts +1 -1
  85. package/dist/review/impact.d.ts +0 -2
  86. package/dist/review/impact.js +2 -2
  87. package/dist/review/plan-phase.d.ts +1 -2
  88. package/dist/review/plan-phase.js +2 -2
  89. package/dist/review/prompt-text.d.ts +1 -1
  90. package/dist/review/prompt-text.js +1 -1
  91. package/dist/review/prompt.d.ts +0 -3
  92. package/dist/review/prompt.js +3 -3
  93. package/dist/rules/builtin/index.d.ts +0 -4
  94. package/dist/rules/builtin/index.js +1 -2
  95. package/dist/rules/repo-rules.d.ts +0 -6
  96. package/dist/rules/repo-rules.js +3 -3
  97. package/dist/runtime/chain-runner.d.ts +19 -0
  98. package/dist/runtime/chain-runner.js +63 -0
  99. package/dist/runtime/effort-capability.d.ts +14 -0
  100. package/dist/runtime/effort-capability.js +70 -0
  101. package/dist/runtime/failback.d.ts +0 -9
  102. package/dist/runtime/failback.js +14 -4
  103. package/dist/runtime/models.d.ts +1 -2
  104. package/dist/runtime/tools.d.ts +0 -16
  105. package/dist/runtime/tools.js +3 -3
  106. package/dist/sarif/candidates.d.ts +2 -1
  107. package/dist/sarif/schema.d.ts +2 -2
  108. package/dist/sarif/schema.js +4 -4
  109. package/dist/session/jsonl.d.ts +2 -2
  110. package/dist/session/jsonl.js +5 -4
  111. package/dist/triage.d.ts +2 -2
  112. package/dist/triage.js +1 -1
  113. package/dist/vcs.d.ts +15 -0
  114. package/dist/vcs.js +2 -0
  115. package/dist/verify/prompt.d.ts +0 -6
  116. package/dist/verify/prompt.js +1 -1
  117. package/dist/verify/verify.d.ts +2 -3
  118. package/dist/verify/verify.js +4 -4
  119. package/package.json +1 -1
  120. package/dist/pipeline/helpers.d.ts +0 -7
  121. package/dist/pipeline/output.d.ts +0 -79
  122. package/dist/pipeline/output.js +0 -132
  123. /package/dist/{pipeline → agent}/budget.d.ts +0 -0
  124. /package/dist/{pipeline → agent}/budget.js +0 -0
  125. /package/dist/{pipeline → agent}/pool.d.ts +0 -0
  126. /package/dist/{pipeline → agent}/usage.d.ts +0 -0
  127. /package/dist/{pipeline → agent}/usage.js +0 -0
  128. /package/dist/{pipeline → report}/report.js +0 -0
@@ -1,9 +1,9 @@
1
+ import { reviewerCall } from "../agent/settings.js";
1
2
  import { defaultBundlePolicy } from "../bundle/bundle.js";
2
3
  import { memoryFor } from "../memory/memory.js";
3
4
  import { buildReviewPrompt } from "../review/prompt.js";
4
5
  import { correctnessReviewer } from "../review/reviewers/correctness.js";
5
6
  import { resolveRules } from "../rules/resolve.js";
6
- import { reviewerCall } from "./agents.js";
7
7
  import { isLargeBundle } from "./execute.js";
8
8
  import { planTasks } from "./matrix.js";
9
9
  import { planReview } from "./plan.js";
@@ -13,8 +13,8 @@ export async function previewReview(options) {
13
13
  const plan = await planReview(options, () => { }, new AbortController().signal);
14
14
  const reviewers = options.reviewers ?? [correctnessReviewer];
15
15
  const planned = planTasks(plan.bundles, reviewers, plan.tier, options.reviewerOverrides, {
16
- ultra: options.ultra === true,
17
- ...(options.maxTasks !== undefined ? { maxTasks: options.maxTasks } : {}),
16
+ ultra: options.mode?.ultra === true,
17
+ ...(options.limits?.maxTasks !== undefined ? { maxTasks: options.limits?.maxTasks } : {}),
18
18
  hasGuidelines: Boolean(plan.guidelines?.trim()),
19
19
  });
20
20
  const { cells } = planned;
@@ -45,10 +45,14 @@ export async function previewReview(options) {
45
45
  if (models?.length)
46
46
  task.models = [...models];
47
47
  const key = `${cell.reviewer.id}\0${cell.bundle.label}`;
48
- if ((options.ultra || isLargeBundle(cell.bundle.files)) && !plannedBundles.has(key)) {
48
+ if ((options.mode?.ultra || isLargeBundle(cell.bundle.files)) && !plannedBundles.has(key)) {
49
49
  plannedBundles.add(key);
50
50
  task.planPromptTokens = tokens(buildReviewPrompt({ ...input, forPlanning: true }));
51
51
  }
52
+ const first = task.models?.[0];
53
+ if (first !== undefined && options.inputPrice) {
54
+ task.inputCost = inputCost(first, options.inputPrice(first), task.promptTokens + (task.planPromptTokens ?? 0));
55
+ }
52
56
  return task;
53
57
  });
54
58
  const policy = options.bundling ?? defaultBundlePolicy;
@@ -63,6 +67,7 @@ export async function previewReview(options) {
63
67
  skipped: planned.skipped,
64
68
  promptTokens: tasks.reduce((sum, t) => sum + t.promptTokens + (t.planPromptTokens ?? 0), 0),
65
69
  planCalls: tasks.filter((t) => t.planPromptTokens !== undefined).length,
70
+ ...(tasks.some((t) => t.inputCost) ? { inputCost: totalInputCost(tasks) } : {}),
66
71
  warnings: [
67
72
  ...plan.warnings,
68
73
  ...(planned.limited?.length
@@ -73,6 +78,22 @@ export async function previewReview(options) {
73
78
  ],
74
79
  };
75
80
  }
81
+ function inputCost(model, price, promptTokens) {
82
+ if (price === undefined)
83
+ return { model, status: "unknown" };
84
+ if (price === 0)
85
+ return { model, status: "unpriced" };
86
+ return { model, status: "priced", usd: (promptTokens * price) / 1_000_000 };
87
+ }
88
+ function totalInputCost(tasks) {
89
+ const total = { usd: 0, priced: 0, unpriced: 0, unknown: 0 };
90
+ for (const { inputCost } of tasks) {
91
+ if (inputCost?.status === "priced")
92
+ total.usd += inputCost.usd;
93
+ total[inputCost?.status ?? "unknown"] += 1;
94
+ }
95
+ return total;
96
+ }
76
97
  // Four characters per token: an estimate, not a tokenizer.
77
98
  function tokens(prompt) {
78
99
  return Math.ceil((prompt.system.length + prompt.user.length) / 4);
@@ -1,31 +1,8 @@
1
- import type { AgentRuntime, AppliedSampling, Effort, ModelTier, Sampling } from "../contracts.js";
1
+ import type { ResolvedAgent, ReviewerOverrides } from "../agent/settings.js";
2
+ import type { AgentRuntime, AppliedSampling, Sampling } from "../contracts.js";
3
+ import type { AgentProvenance, RunProvenance } from "../report/provenance.js";
2
4
  import type { ReviewerDefinition } from "../review/reviewer.js";
3
- import type { RuleSource, SourcedRule } from "../rules/repo-rules.js";
4
- import type { ResolvedAgent } from "./agents.js";
5
- import type { ReviewerOverrides } from "./matrix.js";
6
- export interface RunProvenance {
7
- ocraVersion: string;
8
- promptHash: string;
9
- configHash: string;
10
- sampling: AppliedSampling;
11
- agents?: Record<string, AgentProvenance>;
12
- rules?: RuleProvenance[];
13
- accountSettings?: {
14
- version: string | null;
15
- };
16
- }
17
- export interface RuleProvenance {
18
- path: string[];
19
- rule: string;
20
- source?: RuleSource;
21
- }
22
- export interface AgentProvenance {
23
- tier: ModelTier;
24
- models?: string[];
25
- effort?: Effort;
26
- applied?: boolean;
27
- notApplied?: (keyof Sampling)[];
28
- }
5
+ import type { SourcedRule } from "../rules/repo-rules.js";
29
6
  export interface ProvenanceInput {
30
7
  ocraVersion: string;
31
8
  configHash: string;
@@ -1,6 +1,6 @@
1
- import { createHash } from "node:crypto";
2
1
  import { RELOCATE_SYSTEM_PROMPT } from "../anchor/relocate.js";
3
2
  import { GROUPING_SYSTEM_PROMPT } from "../bundle/grouping.js";
3
+ import { shortHash } from "../hash.js";
4
4
  import { JUDGE_SYSTEM_PROMPT } from "../judge/prompt.js";
5
5
  import { PLAN_SYSTEM_PROMPT } from "../review/plan-phase.js";
6
6
  import { buildReviewPrompt } from "../review/prompt.js";
@@ -8,7 +8,7 @@ import { VERIFY_SYSTEM_PROMPT } from "../verify/prompt.js";
8
8
  // A short digest of a JSON value, the same whatever order its keys were
9
9
  // written in.
10
10
  export function stableHash(value) {
11
- return createHash("sha256").update(canonicalJson(value)).digest("hex").slice(0, 16);
11
+ return shortHash(canonicalJson(value));
12
12
  }
13
13
  function canonicalJson(value) {
14
14
  if (Array.isArray(value))
@@ -1,51 +1,5 @@
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 MemoryEntry } from "../memory/memory.js";
6
- import type { ModelChains } from "../plugin/types.js";
7
- import type { ReviewerDefinition } from "../review/reviewer.js";
8
- import type { SourcedRule } from "../rules/repo-rules.js";
9
- import type { SarifLog } from "../sarif/schema.js";
10
- import type { SelectionPolicy } from "../select/select.js";
11
- import { type RoleSettings, type TierEfforts } from "./agents.js";
12
- import { type ReviewerOverrides } from "./matrix.js";
13
- import { type ProvenanceInput } from "./provenance.js";
14
- import { type ReviewEvent, type ReviewReport } from "./report.js";
15
- export { GUIDELINES_PATH } from "./plan.js";
16
- export interface ReviewOptions {
17
- vcs: VcsAdapter;
18
- runtime: AgentRuntime;
19
- reviewers?: readonly ReviewerDefinition[];
20
- reviewerOverrides?: ReviewerOverrides;
21
- effort?: TierEfforts;
22
- roles?: RoleSettings;
23
- models?: ModelChains;
24
- rules?: readonly SourcedRule[];
25
- readTrusted?: (path: string) => Promise<string | undefined>;
26
- accountMemory?: readonly MemoryEntry[];
27
- selection?: SelectionPolicy;
28
- concurrency?: number;
29
- taskTimeoutMs?: number;
30
- runTimeoutMs?: number;
31
- verify?: boolean;
32
- judge?: boolean;
33
- maxCostUsd?: number;
34
- maxTasks?: number;
35
- fullReview?: boolean;
36
- ultra?: boolean;
37
- sarif?: readonly SarifLog[];
38
- runId?: string;
39
- provenance?: ProvenanceInput;
40
- signal?: AbortSignal;
41
- onEvent?: (event: ReviewEvent) => void;
42
- }
43
- export interface ReviewHooks {
44
- bundling?: BundlePolicy;
45
- grouper?: FileGrouper;
46
- relocate?: AnchorContext["relocate"] | false;
47
- abortGraceMs?: number;
48
- }
1
+ import type { ReviewReport } from "../report/report.js";
2
+ import { type ReviewHooks, type ReviewOptions } from "./options.js";
49
3
  export declare function review(options: ReviewOptions): Promise<ReviewReport>;
50
4
  export declare function reviewWithHooks(options: ReviewOptions & ReviewHooks): Promise<ReviewReport>;
51
5
  //# sourceMappingURL=run.d.ts.map
@@ -1,42 +1,30 @@
1
- import { runtimeRelocator } from "../anchor/relocate.js";
1
+ import { spendTracker } from "../agent/budget.js";
2
2
  import { errorMessage, OcraError } 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
3
  import { correctnessReviewer } from "../review/reviewers/correctness.js";
8
- import { markUnchecked, verifyFindings } from "../verify/verify.js";
9
- import { effortWarnings, resolveAgents, roleCall, } from "./agents.js";
10
- import { SpendLimitReached, spendTracker } from "./budget.js";
11
- import { coverageOf } from "./coverage.js";
12
- import { runJob } from "./execute.js";
13
- import { dedupeFindings } from "./findings.js";
14
- import { importSarif } from "./imports.js";
15
- import { DEFAULT_MAX_TASKS, planTasks } from "./matrix.js";
4
+ import { assembleReport } from "./assemble-report.js";
5
+ import { checkStage } from "./check-stage.js";
6
+ import { executeStage } from "./execute-stage.js";
7
+ import { filterStage } from "./filter-stage.js";
8
+ import { MAX_TIMER_MS, REVIEW_DEFAULTS } from "./options.js";
16
9
  import { planReview } from "./plan.js";
17
- import { mapWithConcurrency } from "./pool.js";
18
- import { runProvenance } from "./provenance.js";
19
- import { coverageGaps, summarizeAnchoring, } from "./report.js";
20
10
  import { newRunId } from "./run-id.js";
21
- import { addUsage, emptyUsage, unpricedCalls } from "./usage.js";
22
- export { GUIDELINES_PATH } from "./plan.js";
23
- const DEFAULTS = { concurrency: 4, taskTimeoutMs: 10 * 60_000, runTimeoutMs: 25 * 60_000 };
24
11
  // The library entry: plan (deterministic stages) → execute (one agent task
25
- // per cell) → verify → judge → report. The `ocra` command is one caller.
26
- // The manual's Embedding page says which options are a contract.
12
+ // per cell) → filter (memory, the previous review) → check (verify, judge)
13
+ // → report. The `ocra` command is one caller. The manual's Embedding page
14
+ // says which options are a contract.
27
15
  export function review(options) {
28
16
  return reviewWithHooks(options);
29
17
  }
30
18
  export async function reviewWithHooks(options) {
31
19
  const emit = options.onEvent ?? (() => { });
32
- const timeout = AbortSignal.timeout(options.runTimeoutMs ?? DEFAULTS.runTimeoutMs);
20
+ const timeout = AbortSignal.timeout(Math.min(options.limits?.runTimeoutMs ?? REVIEW_DEFAULTS.runTimeoutMs, MAX_TIMER_MS));
33
21
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
34
22
  const reviewers = options.reviewers ?? [correctnessReviewer];
35
23
  if (reviewers.length === 0)
36
24
  throw new OcraError("CONFIG_INVALID", "No reviewer is registered");
37
- const runId = options.runId ?? newRunId();
25
+ const runId = options.identity?.runId ?? newRunId();
38
26
  const prior = await loadPriorReview(options.vcs);
39
- const scope = reviewScope(prior.review, options.fullReview === true);
27
+ const scope = reviewScope(prior.review, options.mode?.full === true);
40
28
  const plan = await planReview(scope.only
41
29
  ? {
42
30
  ...options,
@@ -45,232 +33,15 @@ export async function reviewWithHooks(options) {
45
33
  ...(prior.review?.tier ? { priorTier: prior.review.tier } : {}),
46
34
  }
47
35
  : { ...options, runId }, emit, signal);
48
- const matrix = planTasks(plan.bundles, reviewers, plan.tier, options.reviewerOverrides, {
49
- ultra: options.ultra === true,
50
- ...(options.maxTasks !== undefined ? { maxTasks: options.maxTasks } : {}),
51
- hasGuidelines: Boolean(plan.guidelines?.trim()),
52
- });
53
- const limited = matrix.skipped.filter((s) => s.reason === "task_limit").length;
54
- if (limited > 0) {
55
- plan.warnings.push(`task limit of ${options.maxTasks ?? DEFAULT_MAX_TASKS} reached: ${limited} review task(s) skipped; their files are reported as not reviewed`);
56
- }
57
- emit({ type: "matrix_planned", tasks: matrix.cells.length, skipped: matrix.skipped });
58
- const relocationUsage = [];
59
- const budget = spendTracker(options.maxCostUsd, plan.usage);
60
- const relocator = options.relocate === false
61
- ? undefined
62
- : (options.relocate ??
63
- runtimeRelocator(options.runtime, signal, (u) => {
64
- relocationUsage.push(u);
65
- budget.add(u);
66
- }, roleCall("helper", options)));
67
- // Tasks report spend while they run, so the one that uses up the review
68
- // share stops every task still running, not only the ones not yet started.
69
- const spendLimit = new AbortController();
70
- const spend = (usage) => {
71
- budget.add(usage);
72
- if (budget.reviewExhausted() && !spendLimit.signal.aborted) {
73
- spendLimit.abort(new SpendLimitReached(options.maxCostUsd ?? 0));
74
- }
75
- };
76
- const execute = {
77
- runtime: options.runtime,
78
- taskTimeoutMs: options.taskTimeoutMs ?? DEFAULTS.taskTimeoutMs,
79
- abortGraceMs: options.abortGraceMs,
80
- // Past the spend limit a quote that does not match stays file-level.
81
- relocate: relocator &&
82
- (async (request) => (budget.exhausted() ? undefined : relocator(request))),
83
- ultra: options.ultra === true,
84
- plans: new Map(),
85
- agents: options,
86
- emit,
87
- onUsage: spend,
88
- signal: AbortSignal.any([signal, spendLimit.signal]),
89
- };
90
- // Tasks that never started leave their files unreviewed, not failed.
91
- const notStarted = new Set();
92
- let unaffordable = 0;
93
- const results = await mapWithConcurrency(matrix.cells, options.concurrency ?? DEFAULTS.concurrency, async (cell) => {
94
- // Once the run is cancelled or timed out, remaining cells are not started.
95
- if (signal.aborted) {
96
- notStarted.add(cell.taskId);
97
- return skipCell(cell, "run cancelled before this task started", emit);
98
- }
99
- if (budget.reviewExhausted()) {
100
- notStarted.add(cell.taskId);
101
- unaffordable += 1;
102
- return skipCell(cell, `spend limit of $${options.maxCostUsd} reached`, emit);
103
- }
104
- return runJob(cell, plan, execute);
105
- });
106
- if (unaffordable > 0) {
107
- plan.warnings.push(`spend limit of $${options.maxCostUsd} reached: ${unaffordable} review task(s) did not start; their files are reported as not reviewed`);
108
- }
109
- // Memory and the previous review filter first, so Verify and Judge are not
110
- // paid for findings that will not be reported, and a person's dismissal
111
- // keeps a finding out of the verdict whatever the models say.
112
- const concurrency = options.concurrency ?? DEFAULTS.concurrency;
113
- const imported = options.sarif && options.sarif.length > 0
114
- ? await importSarif(options.sarif, plan, emit)
115
- : { findings: [], outcomes: [], warnings: [] };
116
- const found = dedupeFindings([...results.flatMap((r) => r.findings), ...imported.findings]);
117
- const fileCoverage = coverageOf(plan.decisions, results, plan.unchanged, matrix.limited ?? [], notStarted);
118
- const remembered = applyMemory(found, plan.memory);
119
- const reported = new Set(found.map((f) => f.fingerprint));
120
- const priorReview = withoutRemembered(prior.review, plan.memory);
121
- const reconciled = reconcile({
122
- findings: remembered.kept,
123
- reported,
124
- prior: priorReview,
125
- coverage: fileCoverage,
126
- stillPresent: await priorCodePresence(priorReview, reported, plan.context.readFile),
127
- });
128
- const verification = options.verify === false
129
- ? {
130
- checked: 0,
131
- kept: markUnchecked(reconciled.findings),
132
- refuted: [],
133
- missed: [],
134
- usage: [],
135
- warnings: [],
136
- }
137
- : await verifyFindings(reconciled.findings, {
138
- runtime: options.runtime,
139
- diffs: plan.selected,
140
- context: plan.context,
141
- signal,
142
- concurrency,
143
- budget,
144
- call: roleCall("verifier", options),
145
- });
146
- if (verification.checked > 0) {
147
- emit({
148
- type: "verification_finished",
149
- checked: verification.checked,
150
- refuted: verification.refuted,
151
- });
152
- }
153
- const judgeWanted = options.judge !== false && verification.kept.length > 0;
154
- const judgeAffordable = !budget.exhausted();
155
- const judged = await judgeFindings(verification.kept, {
156
- runtime: options.runtime,
157
- changeRequest: plan.changeRequest,
158
- tier: plan.tier,
159
- signal,
160
- call: roleCall("judge", options),
161
- enabled: judgeWanted && judgeAffordable,
162
- keepDropped: options.ultra === true,
163
- carried: stillOpen(reconciled),
164
- });
165
- if (judgeWanted && !judgeAffordable) {
166
- judged.warnings.push(`spend limit of $${options.maxCostUsd} reached: findings were not judged`);
167
- }
168
- const { nothingReviewed } = coverageGaps({
169
- coverage: fileCoverage,
170
- tasks: results.map((r) => r.outcome),
171
- });
172
- if (!nothingReviewed) {
173
- emit(judged.decisions
174
- ? { type: "judge_finished", verdict: judged.verdict, judgement: judged.decisions }
175
- : { type: "judge_finished", verdict: judged.verdict });
176
- }
177
- const calls = [
178
- ...plan.usage,
179
- ...results.map((r) => r.usage),
180
- ...relocationUsage,
181
- ...verification.usage,
182
- ...judged.usage,
183
- ];
184
- const report = {
185
- runId,
186
- changeRequest: plan.changeRequest,
187
- tier: plan.tier,
188
- verdict: judged.verdict,
189
- summary: nothingReviewed
190
- ? "Nothing was reviewed: no reviewer covered or finished any selected file."
191
- : judged.summary,
192
- coverage: fileCoverage,
193
- bundles: plan.bundles.map((b) => ({ label: b.label, files: b.files.map((f) => f.newPath) })),
194
- tasks: [...results.map((r) => r.outcome), ...imported.outcomes],
195
- skipped: matrix.skipped,
196
- findings: sortFindings(judged.findings),
197
- // Counted before the judge: dropping or downgrading a critical nobody
198
- // could check must not turn an incomplete run into a clean one.
199
- unverifiedCriticals: countMissedCriticals(verification.kept, verification.missed),
200
- refuted: verification.refuted,
201
- remembered: remembered.remembered,
202
- usage: sumUsage(calls),
203
- warnings: [
204
- ...unpricedWarning(calls),
205
- ...plan.warnings,
206
- ...results.flatMap((r) => r.warnings),
207
- ...imported.warnings,
208
- ...verification.warnings,
209
- ...judged.warnings,
210
- ],
211
- };
212
- if (judged.decisions)
213
- report.judgement = judged.decisions;
214
- if (options.maxCostUsd !== undefined) {
215
- const reached = budget.exhausted()
216
- ? "total"
217
- : spendLimit.signal.aborted || unaffordable > 0
218
- ? "review"
219
- : undefined;
220
- report.spendLimit = { usd: options.maxCostUsd, ...(reached ? { reached } : {}) };
221
- }
222
- report.anchoring = summarizeAnchoring(report.findings, relocationUsage.length);
223
- const agents = resolveAgents(reviewers, options);
224
- report.warnings.push(...effortWarnings(agents, options.runtime));
225
- if (options.provenance) {
226
- const { runtime, reviewerOverrides } = options;
227
- report.provenance = runProvenance(options.provenance, runtime, reviewers, agents, reviewerOverrides, plan.repoRules);
228
- }
229
- if (prior.review) {
230
- report.rereview = {
231
- fixed: reconciled.fixed,
232
- notReproduced: reconciled.notReproduced,
233
- notRechecked: reconciled.notRechecked,
234
- unchanged: reconciled.unchanged,
235
- dismissed: reconciled.dismissed,
236
- };
237
- }
238
- if (plan.widened) {
239
- report.scope = {
240
- mode: "full",
241
- reason: `the risk tier rose from ${plan.widened.from} to ${plan.widened.to}, which adds reviewers`,
242
- };
243
- }
244
- else if (scope.note)
245
- report.scope = scope.note;
246
- if (prior.warning)
247
- report.warnings.push(prior.warning);
36
+ const budget = spendTracker(options.limits?.maxCostUsd, plan.usage);
37
+ const context = { options, plan, budget, signal, emit };
38
+ const executed = await executeStage(reviewers, context);
39
+ const filtered = await filterStage(executed, prior.review, context);
40
+ const checked = await checkStage(filtered, context);
41
+ const report = assembleReport({ runId, reviewers, prior, scopeNote: scope.note }, { executed, filtered, checked }, context);
248
42
  emit({ type: "run_finished", report });
249
43
  return report;
250
44
  }
251
- function skipCell(cell, reason, emit) {
252
- const outcome = {
253
- taskId: cell.taskId,
254
- reviewer: cell.reviewer.id,
255
- bundle: cell.bundle.label,
256
- files: cell.bundle.files.map((f) => f.newPath),
257
- status: "cancelled",
258
- error: reason,
259
- findings: 0,
260
- durationMs: 0,
261
- usage: emptyUsage(),
262
- };
263
- emit({ type: "task_finished", outcome });
264
- return { outcome, findings: [], usage: emptyUsage(), warnings: [] };
265
- }
266
- // An earlier finding the team has since accepted is no longer open.
267
- function withoutRemembered(review, memory) {
268
- if (!review)
269
- return undefined;
270
- const accepted = new Set(memory.map((e) => e.fingerprint));
271
- // Everything else the earlier review carries (replies, tier, head) stays.
272
- return { ...review, findings: review.findings.filter((f) => !accepted.has(f.fingerprint)) };
273
- }
274
45
  // Review only what changed since the earlier review when the platform can
275
46
  // tell; otherwise everything, with the reason in the report.
276
47
  function reviewScope(review, full) {
@@ -297,30 +68,4 @@ async function loadPriorReview(vcs) {
297
68
  return { warning: `could not load the previous review: ${errorMessage(error)}` };
298
69
  }
299
70
  }
300
- const SEVERITY_ORDER = { critical: 0, warning: 1, suggestion: 2 };
301
- function sortFindings(findings) {
302
- return findings.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] ||
303
- a.file.localeCompare(b.file) ||
304
- (a.lineRange?.start ?? 0) - (b.lineRange?.start ?? 0));
305
- }
306
- function unpricedWarning(calls) {
307
- const unpriced = unpricedCalls(calls);
308
- return unpriced > 0
309
- ? [
310
- `${unpriced} model call(s) used tokens but reported no cost: their model has no price, so the reported cost and the spend limit do not count them`,
311
- ]
312
- : [];
313
- }
314
- function sumUsage(usages) {
315
- return usages.reduce(addUsage, emptyUsage());
316
- }
317
- // Low-confidence findings do not count toward the verdict, so their missed
318
- // verification does not make the run incomplete either.
319
- function countMissedCriticals(findings, missed) {
320
- const set = new Set(missed);
321
- return findings.filter((f) => f.severity === "critical" &&
322
- !f.lowConfidence &&
323
- f.verification === "unchecked" &&
324
- set.has(f.fingerprint)).length;
325
- }
326
71
  //# sourceMappingURL=run.js.map
@@ -1,6 +1,6 @@
1
1
  import type { AgentRuntime, AgentTaskSpec, Usage } from "../contracts.js";
2
2
  import { type ReportedFinding } from "../domain.js";
3
- import type { TaskStatus } from "./report.js";
3
+ import type { TaskStatus } from "../report/report.js";
4
4
  export interface TaskFinding {
5
5
  reported: ReportedFinding;
6
6
  model?: string;
@@ -19,7 +19,5 @@ export interface TaskCallbacks {
19
19
  abortGraceMs?: number | undefined;
20
20
  }
21
21
  export declare function executeTask(runtime: AgentRuntime, spec: AgentTaskSpec, runSignal: AbortSignal, callbacks: TaskCallbacks): Promise<TaskResult>;
22
- export declare const ABORT_GRACE_MS = 10000;
23
- export declare const MAX_FINDINGS_PER_TASK = 50;
24
22
  export declare function boundFinding(f: ReportedFinding): ReportedFinding;
25
23
  //# sourceMappingURL=task.d.ts.map
@@ -1,7 +1,7 @@
1
+ import { SpendLimitReached } from "../agent/budget.js";
2
+ import { addUsage, emptyUsage } from "../agent/usage.js";
1
3
  import { reportedFindingSchema } from "../domain.js";
2
4
  import { errorMessage } from "../errors.js";
3
- import { SpendLimitReached } from "./budget.js";
4
- import { addUsage, emptyUsage } from "./usage.js";
5
5
  export async function executeTask(runtime, spec, runSignal, callbacks) {
6
6
  const timeout = AbortSignal.timeout(spec.timeoutMs);
7
7
  const signal = AbortSignal.any([runSignal, timeout]);
@@ -58,7 +58,7 @@ export async function executeTask(runtime, spec, runSignal, callbacks) {
58
58
  // have reported findings; a runtime that stops its session delivers them
59
59
  // shortly after. Waiting a bounded moment keeps them in the report and in
60
60
  // the spend limit, without letting a runtime that ignores the abort hang.
61
- export const ABORT_GRACE_MS = 10_000;
61
+ const ABORT_GRACE_MS = 10_000;
62
62
  async function collectAfterAbort(iterator, pending, result, callbacks) {
63
63
  const grace = AbortSignal.timeout(callbacks.abortGraceMs ?? ABORT_GRACE_MS);
64
64
  let next = pending ?? iterator.next();
@@ -120,7 +120,7 @@ function handle(event, result, callbacks) {
120
120
  }
121
121
  // A reviewer steered by the change could flood the pull request with
122
122
  // comments or pad them without end; both are bounded.
123
- export const MAX_FINDINGS_PER_TASK = 50;
123
+ const MAX_FINDINGS_PER_TASK = 50;
124
124
  const MAX_TITLE = 300;
125
125
  const MAX_TEXT = 4_000;
126
126
  const MAX_EVIDENCE = 10;
@@ -1,8 +1,9 @@
1
- import type { AgentRuntime, VcsAdapter } from "../contracts.js";
1
+ import type { AgentRuntime } from "../contracts.js";
2
2
  import { OcraError } from "../errors.js";
3
- import type { ReviewEvent } from "../pipeline/report.js";
3
+ import type { ReviewEvent } from "../report/report.js";
4
4
  import type { ReviewerDefinition } from "../review/reviewer.js";
5
5
  import type { RepoRule } from "../rules/repo-rules.js";
6
+ import type { VcsAdapter } from "../vcs.js";
6
7
  import type { PluginSummary, RuntimeFactory, RuntimeOptions, ToolDefinition, VcsFactory } from "./types.js";
7
8
  export declare class PluginError extends OcraError {
8
9
  constructor(message: string, options?: {
@@ -10,8 +11,6 @@ export declare class PluginError extends OcraError {
10
11
  });
11
12
  }
12
13
  export declare class PluginRegistry {
13
- private readonly reservedToolNames;
14
- private readonly warn;
15
14
  private readonly vcs;
16
15
  private readonly runtimes;
17
16
  private readonly reviewerMap;
@@ -19,6 +18,8 @@ export declare class PluginRegistry {
19
18
  private readonly ruleList;
20
19
  private readonly listeners;
21
20
  private frozen;
21
+ private readonly reservedToolNames;
22
+ private readonly warn;
22
23
  constructor(reservedToolNames: ReadonlySet<string>, warn?: (message: string) => void);
23
24
  registerVcs(owner: string, name: string, factory: VcsFactory): void;
24
25
  registerRuntime(owner: string, name: string, factory: RuntimeFactory): void;
@@ -1,5 +1,5 @@
1
+ import { AGENT_ROLES } from "../agent/settings.js";
1
2
  import { errorMessage, OcraError } from "../errors.js";
2
- import { AGENT_ROLES } from "../pipeline/agents.js";
3
3
  export class PluginError extends OcraError {
4
4
  constructor(message, options) {
5
5
  super("PLUGIN_INVALID", message, options);
@@ -7,8 +7,6 @@ export class PluginError extends OcraError {
7
7
  }
8
8
  }
9
9
  export class PluginRegistry {
10
- reservedToolNames;
11
- warn;
12
10
  vcs = new Map();
13
11
  runtimes = new Map();
14
12
  reviewerMap = new Map();
@@ -16,6 +14,8 @@ export class PluginRegistry {
16
14
  ruleList = [];
17
15
  listeners = [];
18
16
  frozen = false;
17
+ reservedToolNames;
18
+ warn;
19
19
  constructor(reservedToolNames, warn = () => { }) {
20
20
  this.reservedToolNames = reservedToolNames;
21
21
  this.warn = warn;
@@ -1,12 +1,10 @@
1
1
  import type { z } from "zod";
2
- import type { AgentRuntime, ModelTier, ReviewContext, Sampling, VcsAdapter } from "../contracts.js";
3
- import type { ReviewEvent } from "../pipeline/report.js";
2
+ import type { AgentRuntime, ModelChains, ReviewContext, Sampling } from "../contracts.js";
3
+ import type { ReviewEvent } from "../report/report.js";
4
4
  import type { ReviewerDefinition } from "../review/reviewer.js";
5
5
  import type { RepoRule } from "../rules/repo-rules.js";
6
+ import type { VcsAdapter } from "../vcs.js";
6
7
  export type Env = Readonly<Record<string, string | undefined>>;
7
- export type ModelChains = {
8
- [Tier in ModelTier]?: readonly string[];
9
- };
10
8
  export interface RuntimeOptions {
11
9
  models: ModelChains;
12
10
  agentModels?: Readonly<Record<string, readonly string[]>>;