balladeer 1.0.18 → 1.0.20

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 (51) hide show
  1. package/README.md +18 -0
  2. package/dist/anchoring.js +2 -2
  3. package/dist/anchors-schema.d.ts +3 -1
  4. package/dist/anchors-schema.js +5 -3
  5. package/dist/checkout-form.d.ts +11 -0
  6. package/dist/checkout-form.js +14 -0
  7. package/dist/cli.js +4 -3
  8. package/dist/commands/guidance.js +23 -8
  9. package/dist/commands/judge.d.ts +50 -1
  10. package/dist/commands/judge.js +768 -110
  11. package/dist/commands/mcp.js +5 -9
  12. package/dist/commands/setup.js +5 -23
  13. package/dist/currency.d.ts +2 -0
  14. package/dist/currency.js +2 -1
  15. package/dist/guidance-hook.mjs +162 -56
  16. package/dist/guidance-runtime.d.ts +26 -0
  17. package/dist/guidance-runtime.js +71 -1
  18. package/dist/headless-agent.d.ts +2 -0
  19. package/dist/headless-agent.js +5 -1
  20. package/dist/judge-brief.d.ts +11 -5
  21. package/dist/judge-brief.js +26 -10
  22. package/dist/judge-evidence.d.ts +87 -0
  23. package/dist/judge-evidence.js +393 -0
  24. package/dist/judge-hook.d.ts +14 -1
  25. package/dist/judge-hook.js +34 -5
  26. package/dist/judge-review.d.ts +10 -0
  27. package/dist/judge-review.js +36 -0
  28. package/dist/judge-said.d.ts +1 -0
  29. package/dist/judge-said.js +3 -0
  30. package/dist/promise-meaning.d.ts +4 -0
  31. package/dist/promise-review-client.d.ts +84 -0
  32. package/dist/promise-review-client.js +357 -0
  33. package/dist/push-operation.d.ts +61 -0
  34. package/dist/push-operation.js +472 -0
  35. package/dist/read-only-review.d.ts +2 -0
  36. package/dist/read-only-review.js +2 -0
  37. package/dist/review-backstop.d.ts +20 -0
  38. package/dist/review-backstop.js +54 -0
  39. package/dist/review-reporting.d.ts +32 -0
  40. package/dist/review-reporting.js +319 -0
  41. package/dist/review-selection.d.ts +33 -0
  42. package/dist/review-selection.js +64 -0
  43. package/dist/review-settings.d.ts +6 -0
  44. package/dist/review-settings.js +9 -0
  45. package/dist/self-update.d.ts +3 -3
  46. package/dist/self-update.js +38 -8
  47. package/dist/user-scope.d.ts +1 -10
  48. package/dist/user-scope.js +12 -7
  49. package/dist/wire.d.ts +3 -3
  50. package/dist/wire.js +1 -1
  51. package/package.json +1 -1
package/README.md CHANGED
@@ -288,3 +288,21 @@ is replaced only after a new pairing starts; all stored agent connections and ot
288
288
  remain. `--create-workspace "Team name"` likewise opens fresh pairing instead of silently reusing
289
289
  the previous workspace. It pre-fills the name; a human still creates the workspace. An interrupted
290
290
  command can resume the same pending choice by running it again with the same flags.
291
+
292
+ ## Push review results
293
+
294
+ The push reviewer sends each completed review to Balladeer automatically. The app groups issues by
295
+ commitment and labels specific change results Passed, Broken, or Couldn’t verify. This is separate
296
+ from qualified CI evidence and never starts or changes protection. The reviewer currently selects
297
+ agreed promises; recorded intent can still guide agent work under the workspace’s retrieval
298
+ settings.
299
+
300
+ Only bounded metadata leaves the repository: promise and meaning identities, head and base commit
301
+ SHAs, the reviewed branch when known, verdict, a closed reason code, an optional session and
302
+ reproduction digest, and locally verified ancestry. Notes, reproduction commands, exact setup
303
+ instructions, tests, source and output stay in `.continuity/judge-log/` on the machine.
304
+
305
+ Unacknowledged results remain in a local queue for retry on a later judge run. A pass can resolve a
306
+ reported break only for the same meaning and branch, on a locally established descendant. A pass on
307
+ a different branch or a changed meaning cannot clear it. The app’s coverage remains Unknown without
308
+ a complete PR inventory.
package/dist/anchoring.js CHANGED
@@ -348,7 +348,7 @@ export function seedFromMap(map, at) {
348
348
  .slice(0, ANCHOR_LIMITS.linkedTests)
349
349
  .map((test) => ({ file: test.file })),
350
350
  };
351
- return checkPromiseAnchors(anchors).ok ? anchors : undefined;
351
+ return checkPromiseAnchors(anchors, { allowRecordedId: true }).ok ? anchors : undefined;
352
352
  }
353
353
  /**
354
354
  * Which anchors have gone at a commit, read off the graph with no model: an
@@ -468,7 +468,7 @@ export async function anchorWithCall(input) {
468
468
  ...picks.symbols.map((symbol) => symbol.file),
469
469
  ])),
470
470
  };
471
- const checked = checkPromiseAnchors(anchors);
471
+ const checked = checkPromiseAnchors(anchors, { allowRecordedId: true });
472
472
  if (!checked.ok)
473
473
  return { status: "failed", reason: "its anchors failed the data boundary", tokens };
474
474
  return {
@@ -80,6 +80,8 @@ export declare function isPromiseAnchorId(value: unknown): value is string;
80
80
  * boundary. Problems name the field and the rule, never the value, so a
81
81
  * refusal never repeats what it refused.
82
82
  */
83
- export declare function checkPromiseAnchors(value: unknown): AnchorsCheck;
83
+ export declare function checkPromiseAnchors(value: unknown, options?: Readonly<{
84
+ allowRecordedId?: boolean;
85
+ }>): AnchorsCheck;
84
86
  /** The files a promise is selected by: its entry points' and its symbols' files. */
85
87
  export declare function anchorFiles(anchors: PromiseAnchors): string[];
@@ -28,6 +28,7 @@ export const ANCHOR_LIMITS = {
28
28
  // holds the two copies to the same text and the same answers.
29
29
  /** `prom_` ids as Balladeer issues them. */
30
30
  const PROMISE_ID_RULE = /^prom_[a-z0-9]{8,64}$/;
31
+ const RECORDED_ID_RULE = /^cand_[a-z0-9]{8,64}$/;
31
32
  const DIGEST_RULE = /^sha256:[0-9a-f]{64}$/;
32
33
  const SHA_RULE = /^[0-9a-f]{40}$/;
33
34
  const INSTANT_RULE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
@@ -135,7 +136,7 @@ function hasExactKeys(value, keys) {
135
136
  * boundary. Problems name the field and the rule, never the value, so a
136
137
  * refusal never repeats what it refused.
137
138
  */
138
- export function checkPromiseAnchors(value) {
139
+ export function checkPromiseAnchors(value, options = {}) {
139
140
  const problems = [];
140
141
  const fail = (at, rule) => problems.push(`${at} ${rule}`);
141
142
  if (!isRecord(value))
@@ -153,8 +154,9 @@ export function checkPromiseAnchors(value) {
153
154
  fail("the document", "has fields the contract does not define, or lacks one");
154
155
  if (value.schemaVersion !== PROMISE_ANCHORS_SCHEMA_VERSION)
155
156
  fail("schemaVersion", `is not ${PROMISE_ANCHORS_SCHEMA_VERSION}`);
156
- if (!isPromiseAnchorId(value.promiseId))
157
- fail("promiseId", "is not a prom_ id");
157
+ if (!isPromiseAnchorId(value.promiseId) &&
158
+ !(options.allowRecordedId && isText(value.promiseId) && RECORDED_ID_RULE.test(value.promiseId)))
159
+ fail("promiseId", "is not a promise or proposal id");
158
160
  if (!isText(value.semanticDigest) || !DIGEST_RULE.test(value.semanticDigest))
159
161
  fail("semanticDigest", "is not sha256: and 64 hex characters");
160
162
  const anchoredAt = value.anchoredAt;
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Whether an install may take the older form out of the checkout it runs in:
3
+ * only when a person ran setup or install and the user-scope form is fully in
4
+ * place. The background update a session hook starts leaves the repository's
5
+ * files alone, because an edit to a file the team commits is a diff nobody
6
+ * asked for, and the user-scope hook already switches the older hook off from
7
+ * the laptop side (`retireCheckoutRuntime`), so nothing is lost.
8
+ */
9
+ export declare function checkoutFormRemovalAllowed(environment: NodeJS.ProcessEnv, writes: readonly {
10
+ status: string;
11
+ }[]): boolean;
1
12
  /**
2
13
  * Taking this product's own older, per-repository form out of a checkout once
3
14
  * the user-scope form is in place, so one hook speaks per session.
@@ -2,6 +2,20 @@ import { execFileSync } from "node:child_process";
2
2
  import { copyFileSync, existsSync, lstatSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { isOurEntry, writeJsonAtomically } from "./mcp-config.js";
5
+ import { SELF_UPDATE_ENVIRONMENT } from "./self-update.js";
6
+ /**
7
+ * Whether an install may take the older form out of the checkout it runs in:
8
+ * only when a person ran setup or install and the user-scope form is fully in
9
+ * place. The background update a session hook starts leaves the repository's
10
+ * files alone, because an edit to a file the team commits is a diff nobody
11
+ * asked for, and the user-scope hook already switches the older hook off from
12
+ * the laptop side (`retireCheckoutRuntime`), so nothing is lost.
13
+ */
14
+ export function checkoutFormRemovalAllowed(environment, writes) {
15
+ return (environment[SELF_UPDATE_ENVIRONMENT] !== "1" &&
16
+ writes.length > 0 &&
17
+ !writes.some((w) => w.status === "refused"));
18
+ }
5
19
  const PROJECT_OWNER = "Balladeer current guidance (loader 1)";
6
20
  const CODEX_BLOCKS = [
7
21
  ["# balladeer:guidance:start", "# balladeer:guidance:end"],
package/dist/cli.js CHANGED
@@ -31,7 +31,7 @@ import { updateNotice } from "./currency.js";
31
31
  import { describeEarlier, describeRemoval, findEarlier, removeEarlier } from "./remove-earlier.js";
32
32
  import { recordSelfUpdateState, SELF_UPDATE_ENVIRONMENT } from "./self-update.js";
33
33
  import { hooksAwaitingTrust } from "./hook-trust.js";
34
- import { describeCheckoutFormRemoval, findCheckoutForm, removeCheckoutForm, } from "./checkout-form.js";
34
+ import { checkoutFormRemovalAllowed, describeCheckoutFormRemoval, findCheckoutForm, removeCheckoutForm, } from "./checkout-form.js";
35
35
  import { StoreError, normalizeControlPlane } from "./store.js";
36
36
  import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
37
37
  const USAGE = `balladeer ${CLI_VERSION}
@@ -545,8 +545,9 @@ function installEverything(options) {
545
545
  }
546
546
  // With the user-scope form in place, the older per-repository form in this
547
547
  // checkout only makes the session hear the guidance twice. Out it goes, with
548
- // a copy beside any file git does not already keep.
549
- if ((writes ?? []).length > 0 && !(writes ?? []).some((w) => w.status === "refused")) {
548
+ // a copy beside any file git does not already keep, and never from the
549
+ // background update (see `checkoutFormRemovalAllowed`).
550
+ if (checkoutFormRemovalAllowed(process.env, writes ?? [])) {
550
551
  const checkoutForm = findCheckoutForm({
551
552
  cwd: process.cwd(),
552
553
  controlPlane: options.controlPlane,
@@ -7,6 +7,7 @@ import { repositoryHints } from "../repository.js";
7
7
  import { newerVersionPublished } from "../currency.js";
8
8
  import { startSelfUpdateIfDue } from "../self-update.js";
9
9
  import { noteHookFired } from "../hook-trust.js";
10
+ import { retireCheckoutRuntime } from "../guidance-runtime.js";
10
11
  /** A repository-specific setup step for explicit status and empty-catalog responses. */
11
12
  export function untrackedLine(remote) {
12
13
  const here = remote === "unknown/unknown" ? "This folder" : `This folder (${remote})`;
@@ -158,6 +159,18 @@ export async function runGuidance(options) {
158
159
  // trust are the ones this matters to.
159
160
  if (options.hook === "codex")
160
161
  noteHookFired(options.environment, "codex");
162
+ // This hook covers this checkout now, so the older per-repository hook
163
+ // here (releases before 1.0.8) only makes the session hear the guidance
164
+ // twice. Switch it off from the laptop side, once, before the fetch:
165
+ // nothing printed, no repository file touched, nobody asked to run a thing.
166
+ if (!projectScoped && event === "SessionStart")
167
+ retireCheckoutRuntime({
168
+ cwd: options.cwd,
169
+ host: options.hook,
170
+ repositoryId: agents[0].repositoryId,
171
+ controlPlane: agents[0].controlPlane,
172
+ environment: options.environment,
173
+ });
161
174
  const loaded = await loadGuidance({
162
175
  agent: agents[0],
163
176
  environment: options.environment,
@@ -165,14 +178,16 @@ export async function runGuidance(options) {
165
178
  hook: options.hook,
166
179
  ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
167
180
  });
168
- // Behind? Start the install in the background, once a day at most, and
169
- // carry on serving this session from the copy that is here.
170
- if (event === "SessionStart")
171
- startSelfUpdateIfDue(newerVersionPublished(), {
172
- environment: options.environment,
173
- ...(options.startUpdate ? { run: options.startUpdate } : {}),
174
- ...(options.now ? { now: options.now } : {}),
175
- });
181
+ // Behind? Start the install in the background and carry on serving this
182
+ // session from the copy that is here. Any hook that loaded guidance may
183
+ // start it, not only a session start: sessions run for days, and a laptop
184
+ // whose sessions never restart would otherwise never update (Robert's own,
185
+ // 1 October 2026). The stamp keeps it to once a day per release.
186
+ startSelfUpdateIfDue(newerVersionPublished(), {
187
+ environment: options.environment,
188
+ ...(options.startUpdate ? { run: options.startUpdate } : {}),
189
+ ...(options.now ? { now: options.now } : {}),
190
+ });
176
191
  const sessionId = identifier(record.session_id);
177
192
  const agentId = identifier(record.agent_id);
178
193
  recordGuidanceContext({
@@ -2,7 +2,13 @@ import type { AnchorBudget } from "../anchor-budget.js";
2
2
  import type { AnchorsClient } from "../anchors-client.js";
3
3
  import { type BehaviorMap } from "../behavior-map-schema.js";
4
4
  import { type HeadlessAgentOptions, type HeadlessAgentResult, type HeadlessClient, type LoginCheck } from "../headless-agent.js";
5
+ import { type CodeReview } from "../judge-review.js";
6
+ import type { ReviewSettings } from "../review-settings.js";
7
+ import { type CaseEvidence, type EvidenceReceipt, type FrozenEvidenceFile } from "../judge-evidence.js";
8
+ import { type ReviewsClient } from "../promise-review-client.js";
9
+ import { reviewBackstop } from "../review-backstop.js";
5
10
  import { type HookHost, type MergeTarget } from "../judge-hook.js";
11
+ import { type ReviewPayload } from "../review-reporting.js";
6
12
  import { type PromiseDocument, type PromiseReader } from "../promise-meaning.js";
7
13
  import type { RepositoryGraph } from "../risk/graph-cache.js";
8
14
  import { type CarriedFile } from "../scratch-worktree.js";
@@ -98,6 +104,8 @@ export type JudgeVerdict = {
98
104
  reproductionRejected: string | null;
99
105
  exercisedCases: string[];
100
106
  notes: string;
107
+ /** The model's code assessment. Local only; never part of a server receipt. */
108
+ review?: CodeReview;
101
109
  /**
102
110
  * On a could-not-tell the judge itself reached because the tests need
103
111
  * something this machine lacks: the one step that would let them run, as
@@ -119,6 +127,17 @@ export type JudgeVerdict = {
119
127
  file: string;
120
128
  outcome: "pass" | "fail" | "error" | "none";
121
129
  };
130
+ standing?: "recorded" | "agreed";
131
+ /** Local dependencies and runner identity; never credentials. */
132
+ environmentDigest?: string;
133
+ evidenceReceipts?: readonly Omit<EvidenceReceipt, "output">[];
134
+ independentReview?: Readonly<{
135
+ accepted: boolean;
136
+ reason: string;
137
+ durationMs?: number;
138
+ inputTokens?: number;
139
+ outputTokens?: number;
140
+ }>;
122
141
  };
123
142
  /** A command's exit, as one run of a reproduction or a linked test. */
124
143
  export type CommandRun = Readonly<{
@@ -198,6 +217,8 @@ export declare function keepReproduction(input: Readonly<{
198
217
  cwd: string;
199
218
  digest: string;
200
219
  files: readonly CarriedFile[];
220
+ /** Exact bytes that the runner executed; the exploratory copy may change later. */
221
+ frozenFiles?: readonly FrozenEvidenceFile[];
201
222
  }>): KeptReproduction | undefined;
202
223
  /**
203
224
  * How this repository runs one test file, when it can be told from the files
@@ -213,7 +234,10 @@ type JudgeAnswer = Readonly<{
213
234
  }>;
214
235
  exercisedCases: string[];
215
236
  notes: string;
237
+ review?: CodeReview;
216
238
  nextStep?: string;
239
+ evidenceFiles: readonly string[];
240
+ caseEvidence: readonly CaseEvidence[];
217
241
  }>;
218
242
  /** The longest `nextStep` kept from a judge's answer, in characters. */
219
243
  export declare const NEXT_STEP_LIMIT = 500;
@@ -261,6 +285,14 @@ export type JudgeArguments = Readonly<{
261
285
  json: boolean;
262
286
  mode?: "report" | "block";
263
287
  wait: boolean;
288
+ retry?: boolean;
289
+ /** Verified by a captured local push operation, not guessed from HEAD. */
290
+ destinationRef?: string;
291
+ destinationDigest?: string;
292
+ pushSessionId?: string;
293
+ operationDeadline?: number;
294
+ /** Original host for offers when a native push fans out into per-ref reviews. */
295
+ pushHost?: HookHost;
264
296
  hook?: HookHost;
265
297
  /** Run by the entry setup writes at the host's user scope, which stands aside where it has nothing to do. */
266
298
  userScope?: true;
@@ -274,7 +306,7 @@ export type JudgeArguments = Readonly<{
274
306
  repository?: string;
275
307
  controlPlane: string;
276
308
  }>;
277
- export declare const JUDGE_USAGE = " npx -y balladeer@latest judge [--promises <id,id>] [--top <k>] [--base <ref>] [--head <ref>]\n [--report-only | --block] [--wait] [--json] [--client auto|claude|codex]\n [--from-file <promise.json> [--map <map.json>]]\n [--hook claude|codex|git] [--install-hook [claude|codex|git]]\n [--push-check on|off]\n Judge whether this change keeps the promises it could break, one promise\n at a time, on your own Claude Code or Codex login. A break counts only\n with a command that fails with the change and passes without it, and\n this command runs it both ways itself; anything unproven is \"could not\n tell\". Report mode prints one line per promise and never stops anything;\n --block exits 2 on a reproduced break, and when judges are still running\n at the time limit it holds the push rather than letting it through.\n --wait runs without the time limit. Promises come from --promises, else\n every agreed promise whose anchored code the change edits, and every\n promise with a behavior map whose own code or tests it edits (with\n \"select\": \"ranked\" in .continuity/judge.json, the top of the maps'\n ranking and a small random sample); plus that file's alwaysJudge. A\n promise with no anchors yet is anchored first, by one small call on\n your own login (see `balladeer anchor`), at most 20 a day on this\n laptop, and a line says what that cost. Verdicts are kept in\n .continuity/judge-log/, which git ignores. --install-hook adds the push\n hook to .claude/settings.json and .codex/hooks.json, before and after\n each shell command: report mode judges after a push has landed, so the\n push never waits, and block mode judges before it. With git it writes\n .git/hooks/pre-push, which judges before the push in both modes. With\n \"offers\": true in .continuity/judge.json, the push hook also reads the\n change for rules no promise records and offers at most three to\n whoever is pushing; it records nothing and never holds a push (see\n `balladeer offers`). Setup also writes the same pair of entries at\n each host's user scope, running `judge --hook <host> --user-scope`,\n so the check runs in every connected repository with agreed promises\n and says nothing anywhere else; a checkout with its own entry keeps\n the check to that entry. --push-check off turns the user-scope check\n off on this laptop, and --push-check on turns it back on.\n";
309
+ export declare const JUDGE_USAGE = " npx -y balladeer@latest judge [--promises <id,id>] [--top <k>] [--base <ref>] [--head <ref>]\n [--report-only | --block] [--wait] [--retry] [--json] [--client auto|claude|codex]\n [--from-file <promise.json> [--map <map.json>]]\n [--hook claude|codex|git] [--install-hook [claude|codex|git]]\n [--push-check on|off]\n Judge whether this change keeps the promises it could break, one promise\n at a time, on your own Claude Code or Codex login. Recorded meaning is\n advisory and never blocks. Kept requires executable passing-case evidence\n and a fresh agent assessment. A break counts only\n with a command that fails with the change and passes without it, and\n this command runs it in fresh checkouts and a second local agent checks\n that its assertion actually expresses the promise; anything unproven is \"could not\n tell\". Report mode prints one line per promise and never stops anything;\n --block exits 2 on a reproduced break, and when judges are still running\n at the time limit it holds the push rather than letting it through.\n --wait runs without the time limit. Promises come from --promises, else\n every visible recorded or agreed promise whose anchored code the change edits, and every\n promise with a behavior map whose own code or tests it edits (with\n \"select\": \"ranked\" in .continuity/judge.json, the top of the maps'\n ranking and a small random sample); plus that file's alwaysJudge. A\n remaining current promise receives a bounded local relevance reading; strong\n resource, sink and intent signals can add a promise beyond direct files. A\n promise with no anchors yet is anchored first, by one small call on\n your own login (see `balladeer anchor`), at most 20 a day on this\n laptop, and a line says what that cost. Verdicts are kept in\n .continuity/judge-log/, which git ignores. Coverage and pending work are\n logged separately; --retry retries an inconclusive result. A known pushed\n branch also sends a bounded receipt to Balladeer, with no source or logs. --install-hook adds the push\n hook to .claude/settings.json and .codex/hooks.json, before and after\n each shell command: report mode judges after a push has landed, so the\n push never waits, and block mode judges before it. With git it writes\n .git/hooks/pre-push, which judges before the push in both modes. With\n \"offers\": true in .continuity/judge.json, the push hook also reads the\n change for rules no promise records and offers at most three to\n whoever is pushing; it records nothing and never holds a push (see\n `balladeer offers`). Setup also writes the same pair of entries at\n each host's user scope, running `judge --hook <host> --user-scope`,\n so the check runs in every connected repository with visible promises\n and says nothing anywhere else; a checkout with its own entry keeps\n the check to that entry. --push-check off turns the user-scope check\n off on this laptop, and --push-check on turns it back on.\n";
278
310
  export declare function parseJudgeArguments(argv: readonly string[]): JudgeArguments;
279
311
  /**
280
312
  * The subset of `balladeer-risk/v1` this command reads: enough to order the
@@ -347,7 +379,16 @@ export declare function findLoggedVerdict(root: string, key: Readonly<{
347
379
  semanticDigest: string;
348
380
  base: string;
349
381
  head: string;
382
+ environmentDigest?: string;
383
+ retry?: boolean;
350
384
  }>, now: Date): JudgeVerdict | undefined;
385
+ /**
386
+ * A judge's words, shortened to fit one line an agent relays. A cut ends on a
387
+ * whole sentence, or failing that a whole word, and says where the rest is:
388
+ * the agent repeats the line as it reads it, so a reason cut mid-word reached
389
+ * the person mid-word (found 1 October 2026).
390
+ */
391
+ export declare function sentence(text: string, limit?: number): string;
351
392
  /** Said after "fix the behavior": a deliberate change is the person's, and the promise its owner's. */
352
393
  export declare const DELIBERATE_CHANGE = "If the person changed this behavior on purpose, do not undo their change: tell them it breaks this promise, whose meaning only its owner can change.";
353
394
  export type VerdictLineContext = Readonly<{
@@ -382,6 +423,7 @@ export declare function stillKept(root: string, verdict: JudgeVerdict): JudgeVer
382
423
  */
383
424
  export declare function verdictLine(verdict: JudgeVerdict, title: string, context?: VerdictLineContext): string;
384
425
  export type JudgeDependencies = Readonly<{
426
+ reportReviews?: ((payload: ReviewPayload) => Promise<number>) | null;
385
427
  runAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
386
428
  resolveLogin?: (requested: HeadlessClient | "auto", prefer?: HeadlessClient) => Promise<LoginCheck | undefined>;
387
429
  riskReport?: (root: string, base: string, head: string,
@@ -391,6 +433,11 @@ export type JudgeDependencies = Readonly<{
391
433
  reader?: PromiseReader | null;
392
434
  /** The anchors route; `null` means none, without trying to find one. */
393
435
  anchors?: AnchorsClient | null;
436
+ reviews?: ReviewsClient | null;
437
+ /** Internal operation snapshot, reused across every pushed ref. */
438
+ reviewSettings?: ReviewSettings;
439
+ reviewSettingsResolved?: true;
440
+ backstop?: typeof reviewBackstop;
394
441
  /** The anchoring call, when it is not the person's own agent. */
395
442
  anchorAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
396
443
  /** The repository graph at a commit, when it is not built from this laptop's cache. */
@@ -415,6 +462,8 @@ export declare const PUSH_REPOSITORY_UNKNOWN = "Balladeer judge: this push was n
415
462
  /** Said instead of verdicts when every pull request the command opens or merges is in another repository. */
416
463
  export declare function pullRequestElsewhereLine(repo: string): string;
417
464
  export type JudgeRunOptions = Readonly<{
465
+ /** Collect a child ref's offers for delivery after the whole operation's verdicts. */
466
+ deferOffers?: (reading: Promise<readonly string[]>) => void;
418
467
  args: JudgeArguments;
419
468
  cwd: string;
420
469
  environment: NodeJS.ProcessEnv;