balladeer 1.0.17 → 1.0.18

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.
@@ -0,0 +1,127 @@
1
+ import { type AnchorBudget } from "../anchor-budget.js";
2
+ import { anchoringLines, type AnchorRun } from "../anchoring.js";
3
+ import { connectionAnchorsClient, type AnchoredPromise, type AnchorsClient } from "../anchors-client.js";
4
+ import { type PromiseAnchors } from "../anchors-schema.js";
5
+ import { type BehaviorMap, type BehaviorMapMeaning } from "../behavior-map-schema.js";
6
+ import { type HeadlessAgentOptions, type HeadlessAgentResult, type HeadlessClient, type LoginCheck } from "../headless-agent.js";
7
+ import { type PromiseReader } from "../promise-meaning.js";
8
+ import { type RepositoryGraph } from "../risk/graph-cache.js";
9
+ import { ANCHOR_USAGE } from "./anchor-usage.js";
10
+ import { type StoredMap } from "./map.js";
11
+ export { anchoringLines, connectionAnchorsClient };
12
+ /**
13
+ * `balladeer anchor`, and the anchoring step the push check runs before it
14
+ * selects what to judge.
15
+ *
16
+ * Both read the repository's agreed promises and their current anchors from
17
+ * Balladeer, build the repository graph at the commit from this laptop's
18
+ * cache, and bring every promise's anchors current within today's budget (see
19
+ * `../anchoring.ts`). A behavior map for a promise's current meaning still
20
+ * wins over its anchors when promises are selected.
21
+ */
22
+ export type AnchorStage = Readonly<{
23
+ available: true;
24
+ promises: readonly AnchoredPromise[];
25
+ /** Maps in this checkout for their promise's current meaning, by promise id. */
26
+ currentMaps: ReadonlyMap<string, BehaviorMap>;
27
+ repository?: RepositoryGraph;
28
+ run?: AnchorRun;
29
+ }> | Readonly<{
30
+ available: false;
31
+ /** Why the anchors could not be read, for a line when one is said. */
32
+ reason: string;
33
+ /**
34
+ * Anchors do not apply here at all: this laptop has no connection for
35
+ * the repository, or its server has no anchors route yet. Neither is
36
+ * worth a line of its own; everything works from maps, as before.
37
+ */
38
+ unsupported: boolean;
39
+ }>;
40
+ export type AnchorStageInput = Readonly<{
41
+ root: string;
42
+ head: string;
43
+ environment: NodeJS.ProcessEnv;
44
+ maps: readonly StoredMap[];
45
+ client: AnchorsClient | undefined;
46
+ /** Why there is no client, when there is none. */
47
+ clientMissing?: string;
48
+ reader: PromiseReader | undefined;
49
+ login: () => Promise<LoginCheck | undefined>;
50
+ now: () => Date;
51
+ deadline?: number;
52
+ signal?: AbortSignal;
53
+ concurrency?: number;
54
+ scratch?: string;
55
+ force?: ReadonlySet<string>;
56
+ only?: ReadonlySet<string>;
57
+ budget?: AnchorBudget;
58
+ runAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
59
+ /** Replaces the graph build, for tests. */
60
+ graph?: (root: string, head: string) => RepositoryGraph;
61
+ }>;
62
+ /** Every parsed map whose meaning is the promise's current one, by promise id. */
63
+ export declare function currentMapsFor(maps: readonly StoredMap[], promises: readonly AnchoredPromise[]): Map<string, BehaviorMap>;
64
+ /** Read the anchors, and bring them current. Never throws for anything the server or a call does. */
65
+ export declare function anchorStage(input: AnchorStageInput): Promise<AnchorStage>;
66
+ /**
67
+ * Anchors in the shape the risk scorer reads, so a promise selected by its
68
+ * anchors is ordered by the same rule as a mapped one: its entry points and
69
+ * symbols as core, its linked tests, its resources, and the meaning's excerpt
70
+ * when it was read, which is what the scorer compares a change's words with.
71
+ */
72
+ export declare function anchorsAsMap(anchors: PromiseAnchors, meaning?: BehaviorMapMeaning): BehaviorMap;
73
+ /**
74
+ * What a judge is handed where a promise has anchors and no map: the anchors
75
+ * still in the code, the linked tests the graph gives at head, and the files
76
+ * one import step from the anchor files.
77
+ */
78
+ export type JudgeAnchors = Readonly<{
79
+ anchors: PromiseAnchors;
80
+ linkedTests: readonly {
81
+ file: string;
82
+ }[];
83
+ neighborhood: readonly string[];
84
+ }>;
85
+ /** A promise's anchors as the judge receives them, read off the graph at the judged commit. */
86
+ export declare function judgeAnchors(anchors: PromiseAnchors, repository: RepositoryGraph): JudgeAnchors;
87
+ /**
88
+ * The promises a change edits an anchor file of (an entry point's or a
89
+ * symbol's file), from each promise's anchor files. Nothing further: no file
90
+ * that merely imports or is imported by an anchor file.
91
+ */
92
+ export declare function anchoredByChange(anchorFilesOf: ReadonlyMap<string, readonly string[]>, changed: readonly string[]): string[];
93
+ export type AnchorArguments = Readonly<{
94
+ promiseId?: string;
95
+ all: boolean;
96
+ json: boolean;
97
+ repo?: string;
98
+ client: HeadlessClient | "auto";
99
+ repository?: string;
100
+ controlPlane: string;
101
+ }>;
102
+ export { ANCHOR_USAGE };
103
+ export declare function parseAnchorArguments(argv: readonly string[]): AnchorArguments;
104
+ export type AnchorDependencies = Readonly<{
105
+ client?: AnchorsClient;
106
+ reader?: PromiseReader;
107
+ resolveLogin?: (requested: HeadlessClient | "auto") => Promise<LoginCheck | undefined>;
108
+ runAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
109
+ now?: () => Date;
110
+ graph?: (root: string, head: string) => RepositoryGraph;
111
+ scratch?: string;
112
+ }>;
113
+ /** `balladeer anchor`, from arguments already parsed. */
114
+ export declare function runAnchor(options: Readonly<{
115
+ args: AnchorArguments;
116
+ cwd: string;
117
+ environment: NodeJS.ProcessEnv;
118
+ write: (text: string) => void;
119
+ error: (text: string) => void;
120
+ deps?: AnchorDependencies;
121
+ }>): Promise<number>;
122
+ export declare function anchorCommand(argv: readonly string[], io: Readonly<{
123
+ cwd: string;
124
+ environment: NodeJS.ProcessEnv;
125
+ write: (text: string) => void;
126
+ error: (text: string) => void;
127
+ }>): Promise<number>;
@@ -0,0 +1,303 @@
1
+ import { join, resolve } from "node:path";
2
+ import { anchorBudget } from "../anchor-budget.js";
3
+ import { anchoringLines, bringAnchorsCurrent, linkedTestsFor, neighborhood, } from "../anchoring.js";
4
+ import { connectionAnchorsClient, } from "../anchors-client.js";
5
+ import { anchorFiles } from "../anchors-schema.js";
6
+ import { BEHAVIOR_MAP_DIRECTORY, BEHAVIOR_MAP_SCHEMA_VERSION, } from "../behavior-map-schema.js";
7
+ import { repositoryRoot } from "../git.js";
8
+ import { resolveClient, } from "../headless-agent.js";
9
+ import { connectionReader } from "../promise-meaning.js";
10
+ import { buildRepositoryGraph } from "../risk/graph-cache.js";
11
+ import { resolveCommit } from "../scratch-worktree.js";
12
+ import { normalizeControlPlane, StoreError } from "../store.js";
13
+ import { DEFAULT_CONTROL_PLANE } from "../wire.js";
14
+ import { ANCHOR_USAGE } from "./anchor-usage.js";
15
+ import { readBehaviorMaps } from "./map.js";
16
+ // What the push check loads from here once a push is its to judge.
17
+ export { anchoringLines, connectionAnchorsClient };
18
+ /** Every parsed map whose meaning is the promise's current one, by promise id. */
19
+ export function currentMapsFor(maps, promises) {
20
+ const digests = new Map(promises.map((promise) => [promise.promiseId, promise.semanticDigest]));
21
+ const current = new Map();
22
+ for (const entry of maps)
23
+ if (entry.map !== undefined && digests.get(entry.promiseId) === entry.map.semanticDigest)
24
+ current.set(entry.promiseId, entry.map);
25
+ return current;
26
+ }
27
+ /** Read the anchors, and bring them current. Never throws for anything the server or a call does. */
28
+ export async function anchorStage(input) {
29
+ if (input.client === undefined)
30
+ return {
31
+ available: false,
32
+ reason: input.clientMissing ?? "there is no Balladeer connection for this repository",
33
+ unsupported: true,
34
+ };
35
+ const listed = await input.client.list();
36
+ if (!listed.ok)
37
+ return { available: false, reason: listed.reason, unsupported: listed.unsupported === true };
38
+ const promises = listed.promises;
39
+ const currentMaps = currentMapsFor(input.maps, promises);
40
+ if (promises.length === 0)
41
+ return { available: true, promises, currentMaps };
42
+ let repository;
43
+ try {
44
+ repository = (input.graph ??
45
+ ((root, head) => buildRepositoryGraph({ root, rev: head, environment: input.environment })))(input.root, input.head);
46
+ }
47
+ catch (error) {
48
+ return {
49
+ available: false,
50
+ reason: `the repository could not be read at ${input.head.slice(0, 12)}: ${error.message}`,
51
+ unsupported: false,
52
+ };
53
+ }
54
+ const run = await bringAnchorsCurrent({
55
+ promises,
56
+ currentMaps,
57
+ repository,
58
+ client: input.client,
59
+ reader: input.reader,
60
+ login: input.login,
61
+ budget: input.budget ?? anchorBudget(input.environment, input.now),
62
+ environment: input.environment,
63
+ now: input.now,
64
+ repositoryKey: repository.root,
65
+ ...(input.force === undefined ? {} : { force: input.force }),
66
+ ...(input.only === undefined ? {} : { only: input.only }),
67
+ ...(input.deadline === undefined ? {} : { deadline: input.deadline }),
68
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
69
+ ...(input.concurrency === undefined ? {} : { concurrency: input.concurrency }),
70
+ ...(input.scratch === undefined ? {} : { scratch: input.scratch }),
71
+ ...(input.runAgent === undefined ? {} : { runAgent: input.runAgent }),
72
+ });
73
+ repository.save();
74
+ return { available: true, promises, currentMaps, repository, run };
75
+ }
76
+ /**
77
+ * Anchors in the shape the risk scorer reads, so a promise selected by its
78
+ * anchors is ordered by the same rule as a mapped one: its entry points and
79
+ * symbols as core, its linked tests, its resources, and the meaning's excerpt
80
+ * when it was read, which is what the scorer compares a change's words with.
81
+ */
82
+ export function anchorsAsMap(anchors, meaning) {
83
+ return {
84
+ schemaVersion: BEHAVIOR_MAP_SCHEMA_VERSION,
85
+ promiseId: anchors.promiseId,
86
+ semanticDigest: anchors.semanticDigest,
87
+ mappedAt: {
88
+ sha: anchors.anchoredAt.sha,
89
+ at: anchors.anchoredAt.at,
90
+ client: anchors.anchoredAt.client ?? "claude",
91
+ model: anchors.anchoredAt.model ?? "",
92
+ },
93
+ entryPoints: anchors.entryPoints.map((entry) => ({ ...entry, note: "" })),
94
+ symbols: anchors.symbols.map((symbol) => ({ ...symbol, role: "core" })),
95
+ resources: [...anchors.resources],
96
+ linkedTests: anchors.linkedTests.map((test) => ({ file: test.file, name: "", cases: [] })),
97
+ sinks: [],
98
+ confidence: 0,
99
+ notes: "",
100
+ ...(meaning === undefined ? {} : { meaning }),
101
+ };
102
+ }
103
+ /** A promise's anchors as the judge receives them, read off the graph at the judged commit. */
104
+ export function judgeAnchors(anchors, repository) {
105
+ const files = anchorFiles(anchors);
106
+ return {
107
+ anchors,
108
+ linkedTests: linkedTestsFor(repository.graph, files),
109
+ neighborhood: neighborhood(repository.graph, files),
110
+ };
111
+ }
112
+ /**
113
+ * The promises a change edits an anchor file of (an entry point's or a
114
+ * symbol's file), from each promise's anchor files. Nothing further: no file
115
+ * that merely imports or is imported by an anchor file.
116
+ */
117
+ export function anchoredByChange(anchorFilesOf, changed) {
118
+ const edited = new Set(changed);
119
+ const selected = [];
120
+ for (const [promiseId, files] of anchorFilesOf)
121
+ if (files.some((file) => edited.has(file)))
122
+ selected.push(promiseId);
123
+ return selected.sort();
124
+ }
125
+ export { ANCHOR_USAGE };
126
+ export function parseAnchorArguments(argv) {
127
+ const args = [...argv];
128
+ const positional = [];
129
+ let all = false;
130
+ let json = false;
131
+ let repo;
132
+ let repository;
133
+ let controlPlane;
134
+ let client = "auto";
135
+ const value = (flag, inline) => {
136
+ const next = inline ?? args.shift();
137
+ if (next === undefined || next === "")
138
+ throw new StoreError("usage", `${flag} needs a value.`);
139
+ return next;
140
+ };
141
+ while (args.length > 0) {
142
+ const arg = args.shift();
143
+ if (!arg.startsWith("--")) {
144
+ positional.push(arg);
145
+ continue;
146
+ }
147
+ const [name, ...rest] = arg.split("=");
148
+ const inline = rest.length > 0 ? rest.join("=") : undefined;
149
+ if (name === "--all")
150
+ all = true;
151
+ else if (name === "--json")
152
+ json = true;
153
+ else if (name === "--repo")
154
+ repo = value(name, inline);
155
+ else if (name === "--repository")
156
+ repository = value(name, inline);
157
+ else if (name === "--control-plane")
158
+ controlPlane = value(name, inline);
159
+ else if (name === "--client") {
160
+ const requested = value(name, inline);
161
+ if (requested !== "auto" && requested !== "claude" && requested !== "codex")
162
+ throw new StoreError("usage", "--client needs auto, claude or codex.");
163
+ client = requested;
164
+ }
165
+ else
166
+ throw new StoreError("usage", `Unknown option ${arg} for anchor.`);
167
+ }
168
+ if (positional.length > 1)
169
+ throw new StoreError("usage", "anchor takes at most one promise id.");
170
+ const promiseId = positional[0];
171
+ if (promiseId !== undefined && all)
172
+ throw new StoreError("usage", "Name one promise or pass --all, not both.");
173
+ if (promiseId !== undefined && !/^prom_[a-z0-9]{8,64}$/.test(promiseId))
174
+ throw new StoreError("usage", `"${promiseId}" is not a promise id.`);
175
+ const chosen = controlPlane ?? process.env.BALLADEER_CONTROL_PLANE?.trim() ?? DEFAULT_CONTROL_PLANE;
176
+ return {
177
+ ...(promiseId === undefined ? {} : { promiseId }),
178
+ all: promiseId === undefined,
179
+ json,
180
+ ...(repo === undefined ? {} : { repo }),
181
+ client,
182
+ ...(repository === undefined ? {} : { repository }),
183
+ controlPlane: normalizeControlPlane(chosen),
184
+ };
185
+ }
186
+ function count(n, one, many) {
187
+ return `${n} ${n === 1 ? one : many}`;
188
+ }
189
+ function describeOutcome(outcome, anchors) {
190
+ const name = `${outcome.title} (${outcome.promiseId})`;
191
+ const shape = anchors === undefined
192
+ ? ""
193
+ : `: ${count(anchors.entryPoints.length, "entry point", "entry points")}, ${count(anchors.symbols.length, "symbol", "symbols")}, ${count(anchors.linkedTests.length, "linked test", "linked tests")}`;
194
+ switch (outcome.status) {
195
+ case "current":
196
+ return `already anchored: ${name}${shape}`;
197
+ case "anchored":
198
+ return `anchored${outcome.again === true ? " again" : ""}: ${name}${shape}`;
199
+ case "seeded":
200
+ return `anchored from its behavior map: ${name}${shape}`;
201
+ case "stale":
202
+ return `not anchored again: ${name}; some of its anchored code is gone, and what is left is used${outcome.reason ? ` (${outcome.reason})` : ""}`;
203
+ default:
204
+ return `not anchored: ${name}: ${outcome.reason ?? outcome.status}`;
205
+ }
206
+ }
207
+ /** `balladeer anchor`, from arguments already parsed. */
208
+ export async function runAnchor(options) {
209
+ const { args } = options;
210
+ const deps = options.deps ?? {};
211
+ const now = deps.now ?? (() => new Date());
212
+ const fail = (reason, message) => {
213
+ if (args.json) {
214
+ const step = { step: "error", reason, message, changed: false, exitCode: 4 };
215
+ options.write(`${JSON.stringify(step)}\n`);
216
+ }
217
+ else
218
+ options.error(`${message}\n`);
219
+ return 4;
220
+ };
221
+ const root = args.repo !== undefined ? resolve(options.cwd, args.repo) : await repositoryRoot(options.cwd);
222
+ if (root === undefined)
223
+ return fail("not_a_repository", "This is not a git repository, so there is nothing to anchor.");
224
+ const head = await resolveCommit(root, "HEAD");
225
+ if (head === undefined)
226
+ return fail("no_head", "This repository has no commit yet, so there is nothing to anchor.");
227
+ const connection = {
228
+ environment: options.environment,
229
+ controlPlane: args.controlPlane,
230
+ cwd: root,
231
+ ...(args.repository === undefined ? {} : { repository: args.repository }),
232
+ };
233
+ let client = deps.client;
234
+ if (client === undefined) {
235
+ const found = connectionAnchorsClient(connection);
236
+ if (!found.ok)
237
+ return fail("no_connection", `There is no Balladeer connection for this repository on this laptop, so nothing was anchored: ${found.reason}`);
238
+ client = found.client;
239
+ }
240
+ let reader = deps.reader;
241
+ if (reader === undefined) {
242
+ const found = connectionReader(connection);
243
+ if (found.ok)
244
+ reader = found.reader;
245
+ }
246
+ let login;
247
+ const stage = await anchorStage({
248
+ root,
249
+ head,
250
+ environment: options.environment,
251
+ maps: readBehaviorMaps(join(root, BEHAVIOR_MAP_DIRECTORY)),
252
+ client,
253
+ reader,
254
+ login: () => (login ??= (deps.resolveLogin ??
255
+ ((requested) => resolveClient(requested, { env: options.environment, cwd: root })))(args.client)),
256
+ now,
257
+ ...(args.promiseId === undefined
258
+ ? {}
259
+ : { force: new Set([args.promiseId]), only: new Set([args.promiseId]) }),
260
+ ...(deps.runAgent === undefined ? {} : { runAgent: deps.runAgent }),
261
+ ...(deps.graph === undefined ? {} : { graph: deps.graph }),
262
+ ...(deps.scratch === undefined ? {} : { scratch: deps.scratch }),
263
+ });
264
+ if (!stage.available)
265
+ return fail("anchors_unavailable", `Nothing was anchored: ${stage.reason}.`);
266
+ if (args.promiseId !== undefined && !stage.promises.some((p) => p.promiseId === args.promiseId))
267
+ return fail("not_agreed_here", `${args.promiseId} is not an agreed promise of this repository, so it was not anchored.`);
268
+ const outcomes = (stage.run?.outcomes ?? []).filter((outcome) => args.promiseId === undefined || outcome.promiseId === args.promiseId);
269
+ const anchors = stage.run?.anchors ?? new Map();
270
+ if (args.json) {
271
+ for (const outcome of outcomes)
272
+ options.write(`${JSON.stringify({
273
+ step: "anchor",
274
+ promiseId: outcome.promiseId,
275
+ status: outcome.status,
276
+ ...(outcome.reason === undefined ? {} : { reason: outcome.reason }),
277
+ ...(outcome.recorded === undefined ? {} : { recorded: outcome.recorded }),
278
+ ...(anchors.has(outcome.promiseId) ? { anchors: anchors.get(outcome.promiseId) } : {}),
279
+ })}\n`);
280
+ }
281
+ else {
282
+ if (stage.promises.length === 0)
283
+ options.write("This repository has no agreed promise yet, so there is nothing to anchor.\n");
284
+ for (const outcome of outcomes)
285
+ options.write(`${describeOutcome(outcome, anchors.get(outcome.promiseId))}\n`);
286
+ if (stage.run !== undefined)
287
+ for (const line of anchoringLines(stage.run, { push: false }))
288
+ options.write(`${line}\n`);
289
+ }
290
+ const unanchored = outcomes.filter((outcome) => !["current", "anchored", "seeded"].includes(outcome.status));
291
+ return unanchored.length > 0 ? 1 : 0;
292
+ }
293
+ export async function anchorCommand(argv, io) {
294
+ let args;
295
+ try {
296
+ args = parseAnchorArguments(argv);
297
+ }
298
+ catch (error) {
299
+ io.error(`${error instanceof Error ? error.message : String(error)}\n\n${ANCHOR_USAGE}`);
300
+ return 4;
301
+ }
302
+ return runAnchor({ args, ...io });
303
+ }
@@ -1,9 +1,13 @@
1
+ import type { AnchorBudget } from "../anchor-budget.js";
2
+ import type { AnchorsClient } from "../anchors-client.js";
1
3
  import { type BehaviorMap } from "../behavior-map-schema.js";
2
4
  import { type HeadlessAgentOptions, type HeadlessAgentResult, type HeadlessClient, type LoginCheck } from "../headless-agent.js";
3
5
  import { type HookHost, type MergeTarget } from "../judge-hook.js";
4
6
  import { type PromiseDocument, type PromiseReader } from "../promise-meaning.js";
7
+ import type { RepositoryGraph } from "../risk/graph-cache.js";
5
8
  import { type CarriedFile } from "../scratch-worktree.js";
6
9
  import type { Features } from "../risk/contract.js";
10
+ import type { AnchorStage, JudgeAnchors } from "./anchor.js";
7
11
  /**
8
12
  * `balladeer judge`: for a change about to leave this machine, ask the person's
9
13
  * own coding agent, one promise at a time, whether the change keeps it.
@@ -109,6 +113,8 @@ export type JudgeVerdict = {
109
113
  };
110
114
  briefRevision: string;
111
115
  mapUsed: boolean;
116
+ /** The judge was handed the promise's anchors and their neighborhood, where it had no map. */
117
+ anchorsUsed?: boolean;
112
118
  linkedTestRun: {
113
119
  file: string;
114
120
  outcome: "pass" | "fail" | "error" | "none";
@@ -213,10 +219,13 @@ type JudgeAnswer = Readonly<{
213
219
  export declare const NEXT_STEP_LIMIT = 500;
214
220
  /** The judge's final JSON, read strictly enough that a malformed answer is not a verdict. */
215
221
  export declare function readJudgeAnswer(json: Record<string, unknown> | undefined): JudgeAnswer | undefined;
222
+ export type { JudgeAnchors } from "./anchor.js";
216
223
  export type JudgePromiseInput = Readonly<{
217
224
  root: string;
218
225
  promise: PromiseDocument;
219
226
  map?: BehaviorMap;
227
+ /** Handed only when there is no map. */
228
+ anchors?: JudgeAnchors;
220
229
  base: string;
221
230
  head: string;
222
231
  changedFiles: readonly string[];
@@ -265,7 +274,7 @@ export type JudgeArguments = Readonly<{
265
274
  repository?: string;
266
275
  controlPlane: string;
267
276
  }>;
268
- 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 from `balladeer risk`: every promise whose own code or tests the\n change edits, or with \"select\": \"ranked\" in .continuity/judge.json the\n top of its ranking and a small random sample; else every mapped\n promise; plus that file's alwaysJudge. 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 mapped 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";
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";
269
278
  export declare function parseJudgeArguments(argv: readonly string[]): JudgeArguments;
270
279
  /**
271
280
  * The subset of `balladeer-risk/v1` this command reads: enough to order the
@@ -290,28 +299,39 @@ export declare function defaultBase(root: string, head: string): Promise<string
290
299
  export declare function changedFiles(root: string, base: string, head: string): Promise<string[]>;
291
300
  export type Selection = Readonly<{
292
301
  promiseId: string;
293
- why: "file" | "named" | "risk" | "sample" | "mapped" | "always";
302
+ why: "file" | "named" | "risk" | "anchored" | "sample" | "mapped" | "always";
294
303
  }>;
295
304
  /**
296
- * Which promises to judge: named ones, else from the risk report, else every
297
- * mapped promise; then `alwaysJudge`.
305
+ * Which promises to judge: named ones; else the promises the change selects
306
+ * by their anchors and by their maps; then `alwaysJudge`.
298
307
  *
299
- * From the risk report, in the order `balladeer risk` lists it (how directly
300
- * the change reaches each promise, then score, then evidence, then id; a score
301
- * alone ties at 1 and falls to the alphabet):
308
+ * By anchors (`anchored`, chosen before this from the files the change edits):
309
+ * every promise one of whose anchor files (an entry point's or a symbol's
310
+ * file) the change edits, with no widening by import distance. The
311
+ * measurement of 1 October 2026 found that widening multiplied the promises
312
+ * selected and caught nothing more.
302
313
  *
303
- * - "direct" takes every promise whose own code or tests the change edits
304
- * (directness tier 3), and nothing else. The first live evaluation found
305
- * that this selects about 1.8 promises a change and included the broken one
306
- * in 17 of 17 cases. Only a `--top` given by hand caps it.
307
- * - "ranked" takes the top k with a score above zero, plus a random sample
308
- * below that line. The sample is what keeps a ranking honest: a break found
309
- * below the line is a miss the ranking made, counted rather than never seen.
314
+ * By maps, from the risk report, in the order `balladeer risk` lists it (how
315
+ * directly the change reaches each promise, then score, then evidence, then
316
+ * id; a score alone ties at 1 and falls to the alphabet):
317
+ *
318
+ * - "direct" takes every mapped promise whose own code or tests the change
319
+ * edits (directness tier 3), and nothing else. Only a `--top` given by hand
320
+ * caps it, and it caps anchored and mapped promises together.
321
+ * - "ranked" takes the top k mapped promises with a score above zero, plus a
322
+ * random sample below that line. The sample is what keeps a ranking honest:
323
+ * a break found below the line is a miss the ranking made, counted rather
324
+ * than never seen. Anchored promises are taken too, before the sample.
325
+ *
326
+ * Without a risk report, every mapped promise. Anchored and mapped promises
327
+ * are listed together in the report's order, and one the report does not
328
+ * list comes after those it does.
310
329
  */
311
330
  export declare function selectPromises(input: Readonly<{
312
331
  named?: readonly string[];
313
332
  risk?: readonly RiskEntry[];
314
333
  mapped: readonly string[];
334
+ anchored?: readonly string[];
315
335
  config: JudgeConfig;
316
336
  topK?: number;
317
337
  random: () => number;
@@ -364,9 +384,19 @@ export declare function verdictLine(verdict: JudgeVerdict, title: string, contex
364
384
  export type JudgeDependencies = Readonly<{
365
385
  runAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
366
386
  resolveLogin?: (requested: HeadlessClient | "auto", prefer?: HeadlessClient) => Promise<LoginCheck | undefined>;
367
- riskReport?: (root: string, base: string, head: string) => Promise<readonly RiskEntry[] | undefined>;
387
+ riskReport?: (root: string, base: string, head: string,
388
+ /** The maps in use and, in a map's shape, the anchors of the promises they select. */
389
+ maps: readonly BehaviorMap[]) => Promise<readonly RiskEntry[] | undefined>;
368
390
  /** `null` means "no connection", without trying to find one. */
369
391
  reader?: PromiseReader | null;
392
+ /** The anchors route; `null` means none, without trying to find one. */
393
+ anchors?: AnchorsClient | null;
394
+ /** The anchoring call, when it is not the person's own agent. */
395
+ anchorAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
396
+ /** The repository graph at a commit, when it is not built from this laptop's cache. */
397
+ graph?: (root: string, head: string) => RepositoryGraph;
398
+ /** Today's anchoring budget, when it is not this laptop's own. */
399
+ anchorBudget?: AnchorBudget;
370
400
  random?: () => number;
371
401
  now?: () => Date;
372
402
  stdin?: () => Promise<string>;
@@ -394,17 +424,22 @@ export type JudgeRunOptions = Readonly<{
394
424
  }>;
395
425
  /** What `--push-check off` and `--push-check on` say. */
396
426
  export declare const PUSH_CHECK_OFF_LINE = "The push check is off on this laptop: after a push it now says nothing, in every repository. A repository's own entry from `balladeer judge --install-hook` still runs. Turn it back on with `balladeer judge --push-check on`.";
397
- export declare const PUSH_CHECK_ON_LINE = "The push check is back on for this laptop: after a push in a connected repository whose promises have behavior maps, it reports what the push may have broken, and it never holds the push.";
427
+ export declare const PUSH_CHECK_ON_LINE = "The push check is back on for this laptop: after a push in a connected repository with agreed promises, it reports what the push may have broken, and it never holds the push.";
398
428
  /**
399
429
  * Whether the push check's user-scope entry has this push to itself, decided
400
430
  * before anything is started or fetched: the entry runs on every shell command
401
431
  * in every folder on the laptop, and most folders have nothing to judge. It
402
432
  * stands aside, saying nothing, when the laptop turned the check off, the
403
- * folder is in no checkout, the checkout has the push check's own project
433
+ * folder is in no checkout, or the checkout has the push check's own project
404
434
  * entry for this host (that entry owns the check there, so a push is judged
405
- * once), or no promise here has a behavior map. Only then is the connection
406
- * looked for, which reads the checkout's remotes, and a checkout this laptop
407
- * has not connected is left alone too.
435
+ * once). Only then is the connection looked for, which reads the checkout's
436
+ * remotes, and a checkout this laptop has not connected is left alone too.
437
+ *
438
+ * A connected checkout is not left alone for having no behavior map: since
439
+ * promise anchors (approved 30 September 2026), its agreed promises are
440
+ * anchored and checked without one. Whether it has any agreed promise is the
441
+ * server's to say, after the push, and with none (and no map) the entry still
442
+ * says nothing.
408
443
  */
409
444
  export declare function userScopeTakesThePush(input: {
410
445
  cwd: string;
@@ -412,6 +447,20 @@ export declare function userScopeTakesThePush(input: {
412
447
  environment: NodeJS.ProcessEnv;
413
448
  connected: (root: string) => boolean;
414
449
  }): boolean;
450
+ /**
451
+ * The one line said when nothing was selected, and its code: no agreed
452
+ * promise here; nothing anchored or mapped (with why, when the anchors could
453
+ * not be read); or nothing the change touches.
454
+ */
455
+ export declare function nothingSelected(input: Readonly<{
456
+ stage: AnchorStage | undefined;
457
+ maps: number;
458
+ anchors: number;
459
+ push: boolean;
460
+ }>): {
461
+ reason: string;
462
+ message: string;
463
+ };
415
464
  /** `balladeer judge`, from arguments already parsed. */
416
465
  /**
417
466
  * The line an answer opens with when the command makes a commit before it
@@ -427,4 +476,3 @@ export declare function judgeCommand(argv: readonly string[], io: Readonly<{
427
476
  write: (text: string) => void;
428
477
  error: (text: string) => void;
429
478
  }>): Promise<number>;
430
- export {};