balladeer 1.0.16 → 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.
Files changed (58) hide show
  1. package/dist/anchor-budget.d.ts +40 -0
  2. package/dist/anchor-budget.js +110 -0
  3. package/dist/anchoring.d.ts +241 -0
  4. package/dist/anchoring.js +766 -0
  5. package/dist/anchors-client.d.ts +62 -0
  6. package/dist/anchors-client.js +197 -0
  7. package/dist/anchors-schema.d.ts +85 -0
  8. package/dist/anchors-schema.js +246 -0
  9. package/dist/cli.d.ts +1 -1
  10. package/dist/cli.js +24 -7
  11. package/dist/commands/anchor-usage.d.ts +6 -0
  12. package/dist/commands/anchor-usage.js +19 -0
  13. package/dist/commands/anchor.d.ts +127 -0
  14. package/dist/commands/anchor.js +303 -0
  15. package/dist/commands/guidance.d.ts +2 -1
  16. package/dist/commands/guidance.js +55 -0
  17. package/dist/commands/judge.d.ts +219 -12
  18. package/dist/commands/judge.js +911 -103
  19. package/dist/commands/map.d.ts +46 -2
  20. package/dist/commands/map.js +244 -5
  21. package/dist/commands/mcp.js +2 -2
  22. package/dist/guidance-hook.mjs +282 -95
  23. package/dist/guidance.d.ts +7 -0
  24. package/dist/guidance.js +10 -1
  25. package/dist/headless-agent.d.ts +19 -1
  26. package/dist/headless-agent.js +22 -5
  27. package/dist/hook-trust.d.ts +41 -9
  28. package/dist/hook-trust.js +98 -16
  29. package/dist/judge-brief.d.ts +33 -3
  30. package/dist/judge-brief.js +39 -2
  31. package/dist/judge-hook.d.ts +188 -14
  32. package/dist/judge-hook.js +919 -61
  33. package/dist/judge-said.d.ts +52 -0
  34. package/dist/judge-said.js +181 -0
  35. package/dist/promise-meaning.d.ts +25 -0
  36. package/dist/promise-meaning.js +24 -7
  37. package/dist/relay.d.ts +71 -0
  38. package/dist/relay.js +193 -0
  39. package/dist/remove-earlier.js +4 -2
  40. package/dist/risk/git.d.ts +3 -1
  41. package/dist/risk/git.js +3 -3
  42. package/dist/risk/graph-cache.d.ts +83 -0
  43. package/dist/risk/graph-cache.js +291 -0
  44. package/dist/risk/import-graph.d.ts +43 -0
  45. package/dist/risk/import-graph.js +88 -34
  46. package/dist/risk/index.d.ts +1 -1
  47. package/dist/risk/index.js +1 -1
  48. package/dist/risk/pipeline.d.ts +13 -0
  49. package/dist/risk/pipeline.js +15 -4
  50. package/dist/risk/score.d.ts +10 -0
  51. package/dist/risk/score.js +11 -2
  52. package/dist/scratch-worktree.d.ts +5 -1
  53. package/dist/scratch-worktree.js +9 -2
  54. package/dist/user-scope.d.ts +74 -3
  55. package/dist/user-scope.js +270 -9
  56. package/dist/wire.d.ts +19 -3
  57. package/dist/wire.js +2 -2
  58. package/package.json +7 -2
@@ -12,6 +12,13 @@ export type GuidanceEvent = "SessionStart" | "SubagentStart" | "UserPromptSubmit
12
12
  export declare const GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
13
13
  export declare function parseGuidance(value: unknown, agent: StoredAgent): GuidanceDocument;
14
14
  export declare function guidanceEndpoint(agent: StoredAgent): URL;
15
+ /**
16
+ * A route of the server this repository's agent connection belongs to, scoped
17
+ * to its workspace and repository the way `/api/agent-guidance` is: same
18
+ * origin as the connection's MCP address, https only, and the scope in the
19
+ * query exactly once.
20
+ */
21
+ export declare function agentEndpoint(agent: StoredAgent, pathname: string): URL;
15
22
  export declare function guidanceScopeKey(agent: StoredAgent): string;
16
23
  export type GuidanceLoad = Readonly<{
17
24
  status: "fresh" | "revalidated" | "unavailable";
package/dist/guidance.js CHANGED
@@ -34,6 +34,15 @@ export function parseGuidance(value, agent) {
34
34
  return value;
35
35
  }
36
36
  export function guidanceEndpoint(agent) {
37
+ return agentEndpoint(agent, "/api/agent-guidance");
38
+ }
39
+ /**
40
+ * A route of the server this repository's agent connection belongs to, scoped
41
+ * to its workspace and repository the way `/api/agent-guidance` is: same
42
+ * origin as the connection's MCP address, https only, and the scope in the
43
+ * query exactly once.
44
+ */
45
+ export function agentEndpoint(agent, pathname) {
37
46
  const url = new URL(agent.mcpUrl);
38
47
  const expected = new URL(agent.controlPlane);
39
48
  if (url.origin !== expected.origin ||
@@ -54,7 +63,7 @@ export function guidanceEndpoint(agent) {
54
63
  throw new Error("invalid_guidance_scope");
55
64
  url.searchParams.set(key, value);
56
65
  }
57
- url.pathname = "/api/agent-guidance";
66
+ url.pathname = pathname;
58
67
  return url;
59
68
  }
60
69
  function readJson(path) {
@@ -49,6 +49,11 @@ export type HeadlessAgentOptions = Readonly<{
49
49
  prefer?: HeadlessClient;
50
50
  /** Codex's sandbox. Read-only unless the run has to execute tests. */
51
51
  sandbox?: "read-only" | "workspace-write";
52
+ /**
53
+ * The run's folder is no repository on purpose (an anchoring call works in
54
+ * an empty folder, so it can read nothing), which Codex must be told.
55
+ */
56
+ outsideRepository?: boolean;
52
57
  /** Claude Code permission rules refused outright, such as `Bash(git push:*)`. */
53
58
  disallowedTools?: readonly string[];
54
59
  /** Fires when the caller's budget runs out; the run is stopped, not failed. */
@@ -58,6 +63,15 @@ export type HeadlessAgentOptions = Readonly<{
58
63
  /** A login already verified by the caller, so a batch checks it once. */
59
64
  login?: LoginCheck;
60
65
  maxOutputBytes?: number;
66
+ /**
67
+ * The most output tokens the model may write in this run. Claude Code is
68
+ * held to it by `CLAUDE_CODE_MAX_OUTPUT_TOKENS`, so a run that would go
69
+ * further is stopped there. Codex has no such setting (none in its
70
+ * configuration reference, and none in its 0.144 binary), so for Codex the
71
+ * run is held by its time limit and `overOutputCap` says afterwards when it
72
+ * wrote more than this.
73
+ */
74
+ maxOutputTokens?: number;
61
75
  /** Where the private settings directory is made. Defaults to the system temp directory. */
62
76
  scratch?: string;
63
77
  }>;
@@ -76,8 +90,10 @@ export type HeadlessAgentResult = Readonly<{
76
90
  stderrTail: string;
77
91
  timedOut: boolean;
78
92
  aborted: boolean;
93
+ /** The run reported more output tokens than `maxOutputTokens` allowed, or stopped at that cap. */
94
+ overOutputCap?: boolean;
79
95
  /** Why `ok` is false, as a code a caller can branch on. */
80
- reason?: "no_verified_login" | "spawn_failed" | "timed_out" | "aborted" | "output_bounded" | "exit_nonzero" | "no_answer" | "no_json";
96
+ reason?: "no_verified_login" | "spawn_failed" | "timed_out" | "aborted" | "output_bounded" | "exit_nonzero" | "no_answer" | "no_json" | "output_capped";
81
97
  }>;
82
98
  /** Set on every run this module starts, so a hook inside it knows not to judge again. */
83
99
  export declare const JUDGE_ACTIVE_VARIABLE = "BALLADEER_JUDGE_ACTIVE";
@@ -102,6 +118,8 @@ export declare function claudeArguments(input: Readonly<{
102
118
  export declare function codexArguments(input: Readonly<{
103
119
  cwd: string;
104
120
  sandbox: "read-only" | "workspace-write";
121
+ /** The run is in a folder that is no repository on purpose, such as an empty one. */
122
+ outsideRepository?: boolean;
105
123
  }>): string[];
106
124
  /**
107
125
  * Whether this laptop has the subscription login the client needs.
@@ -40,8 +40,13 @@ export function clientBinary(client, env) {
40
40
  export function claudeArguments(input) {
41
41
  const tools = (input.tools ?? input.allowedTools).join(",");
42
42
  // Rules narrower than a tool carry spaces (`Bash(git diff:*)`), so they go
43
- // in as separate values rather than as one comma list.
44
- const allowed = input.tools === undefined ? [tools] : [...input.allowedTools];
43
+ // in as separate values rather than as one comma list. A run with no tools
44
+ // at all (`--tools ""`) has nothing to allow, and the flag is left out.
45
+ const allowed = input.allowedTools.length === 0
46
+ ? []
47
+ : input.tools === undefined
48
+ ? [tools]
49
+ : [...input.allowedTools];
45
50
  return [
46
51
  "--print",
47
52
  "--output-format",
@@ -59,8 +64,7 @@ export function claudeArguments(input) {
59
64
  "dontAsk",
60
65
  "--tools",
61
66
  tools,
62
- "--allowedTools",
63
- ...allowed,
67
+ ...(allowed.length > 0 ? ["--allowedTools", ...allowed] : []),
64
68
  ...((input.disallowedTools ?? []).length > 0
65
69
  ? ["--disallowedTools", ...(input.disallowedTools ?? [])]
66
70
  : []),
@@ -77,6 +81,8 @@ export function codexArguments(input) {
77
81
  input.sandbox,
78
82
  "-C",
79
83
  input.cwd,
84
+ // Codex refuses to start outside a git repository unless told this.
85
+ ...(input.outsideRepository === true ? ["--skip-git-repo-check"] : []),
80
86
  "--json",
81
87
  "-c",
82
88
  'approval_policy="never"',
@@ -338,6 +344,8 @@ export async function runHeadlessAgent(options) {
338
344
  const env = agentEnvironment(options.env);
339
345
  let args;
340
346
  if (client === "claude") {
347
+ if (options.maxOutputTokens !== undefined)
348
+ env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = String(options.maxOutputTokens);
341
349
  const settingsPath = join(privateRoot, "settings.json");
342
350
  writeFileSync(settingsPath, JSON.stringify({ disableAllHooks: true }), { mode: 0o600 });
343
351
  args = claudeArguments({
@@ -359,7 +367,11 @@ export async function runHeadlessAgent(options) {
359
367
  if (existsSync(auth))
360
368
  symlinkSync(auth, join(codexHome, "auth.json"));
361
369
  env.CODEX_HOME = codexHome;
362
- args = codexArguments({ cwd: options.cwd, sandbox: options.sandbox ?? "read-only" });
370
+ args = codexArguments({
371
+ cwd: options.cwd,
372
+ sandbox: options.sandbox ?? "read-only",
373
+ ...(options.outsideRepository === true ? { outsideRepository: true } : {}),
374
+ });
363
375
  }
364
376
  const run = await runOwnedProcess({
365
377
  command: clientBinary(client, options.env),
@@ -399,6 +411,11 @@ export async function runHeadlessAgent(options) {
399
411
  return failed("timed_out");
400
412
  if (run.bounded)
401
413
  return failed("output_bounded");
414
+ // Reaching the cap is a stop, whatever the agent managed to say before it.
415
+ if (options.maxOutputTokens !== undefined &&
416
+ parsed?.outputTokens !== undefined &&
417
+ parsed.outputTokens >= options.maxOutputTokens)
418
+ return { ...failed("output_capped"), overOutputCap: true };
402
419
  if (run.exitCode !== 0)
403
420
  return failed("exit_nonzero");
404
421
  if (parsed === undefined || parsed.isError || parsed.text.trim() === "")
@@ -16,16 +16,48 @@ export type TrustingHost = "codex";
16
16
  /** Hosts that run a hook only after the person has approved it. */
17
17
  export declare const HOSTS_REQUIRING_TRUST: readonly TrustingHost[];
18
18
  /**
19
- * This command wrote or changed the host's hooks, or found them in place with
20
- * no record that it ever had: either way, from now the host has to have run
21
- * them once before anybody assumes it does.
19
+ * Hooks this command writes as one set, each set with its own marks. The
20
+ * guidance hooks and the push check are written separately, and a host keeps
21
+ * running an unchanged hook it already trusted, so one set running says
22
+ * nothing about the other: once the push check was added, the trusted
23
+ * guidance hooks would run at the next session start and settle a single mark
24
+ * while the new push check still waited for review.
22
25
  */
23
- export declare function noteHooksWritten(environment: NodeJS.ProcessEnv, host: TrustingHost, changed: boolean, now?: number): void;
24
- /** One of the host's hooks ran, which it does only once they are trusted. */
25
- export declare function noteHookFired(environment: NodeJS.ProcessEnv, host: TrustingHost, now?: number): void;
26
- /** True while the hooks last written for this host have never run in it. */
26
+ export type HookSet = "guidance" | "push-check";
27
+ /**
28
+ * The push check's entry starts Node only for a command that could push
29
+ * (`pushCheckCommand` in user-scope.ts), so the run that settles its mark
30
+ * would wait for the first push, and meanwhile the person would be told to
31
+ * trust hooks they already trusted. This file exists exactly while that set
32
+ * waits for its first run in the host, and the entry sends every command
33
+ * through to Node while it does.
34
+ */
35
+ export declare function pushCheckAwaitingFile(environment: NodeJS.ProcessEnv, host: TrustingHost): string;
36
+ /**
37
+ * This command wrote or changed one set of the host's hooks, or found it in
38
+ * place with no record that it ever had: either way, from now the host has to
39
+ * have run that set once before anybody assumes it does. `hooks` is how many
40
+ * hooks the set is, which the notice counts.
41
+ */
42
+ export declare function noteHooksWritten(environment: NodeJS.ProcessEnv, host: TrustingHost, changed: boolean, now?: number, options?: Readonly<{
43
+ set?: HookSet;
44
+ hooks?: number;
45
+ }>): void;
46
+ /** This command took one set of the host's hooks out: nothing of it is left to trust. */
47
+ export declare function noteHooksRemoved(environment: NodeJS.ProcessEnv, host: TrustingHost, set: HookSet): void;
48
+ /** One hook of that set ran, which it does only once it is trusted. */
49
+ export declare function noteHookFired(environment: NodeJS.ProcessEnv, host: TrustingHost, now?: number, set?: HookSet): void;
50
+ /** True while some set of the hooks last written for this host has never run in it. */
27
51
  export declare function hooksAwaitingTrust(environment: NodeJS.ProcessEnv, host: TrustingHost): boolean;
52
+ /** A small count the way a sentence says it. */
53
+ export declare function countWord(count: number): string;
54
+ /** How many of Balladeer's hooks this host was last given, over every set written. */
55
+ export declare function hooksWritten(environment: NodeJS.ProcessEnv, host: TrustingHost): number;
28
56
  /** The host a session belongs to, from the name it gave when it said hello. */
29
57
  export declare function trustingHost(clientName: unknown): TrustingHost | undefined;
30
- /** What the agent is asked to pass on, once, in the host where the approval happens. */
31
- export declare const HOOKS_AWAITING_TRUST_NOTICE = "Note from Balladeer on this laptop, to pass on to the person once: Balladeer's hooks here were added or changed and Codex has not run them. Codex runs a hook only after the person trusts it. Ask them to type /hooks in Codex and trust Balladeer's three hooks; until then Codex has Balladeer's tools without its session-start guidance or its updates.";
58
+ /**
59
+ * What the agent is asked to pass on, once, in the host where the approval
60
+ * happens. It counts the hooks this command last wrote there, so a laptop given
61
+ * the push check is told to trust six and one without it four.
62
+ */
63
+ export declare function hooksAwaitingTrustNotice(environment: NodeJS.ProcessEnv, host?: TrustingHost): string;
@@ -1,9 +1,14 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { configHome } from "./store.js";
4
4
  /** Hosts that run a hook only after the person has approved it. */
5
5
  export const HOSTS_REQUIRING_TRUST = ["codex"];
6
+ const SETS = ["guidance", "push-check"];
7
+ /** How many hooks a set is, for a mark an earlier release wrote without saying. */
8
+ const DEFAULT_COUNT = { guidance: 4, "push-check": 2 };
6
9
  const FILE = "hook-trust.json";
10
+ /** The guidance set keeps the key earlier releases wrote, so their marks still count. */
11
+ const key = (host, set) => set === "guidance" ? host : `${host}:${set}`;
7
12
  function read(environment) {
8
13
  try {
9
14
  const parsed = JSON.parse(readFileSync(join(configHome(environment), FILE), "utf8"));
@@ -15,49 +20,126 @@ function read(environment) {
15
20
  return {};
16
21
  }
17
22
  }
23
+ /**
24
+ * The push check's entry starts Node only for a command that could push
25
+ * (`pushCheckCommand` in user-scope.ts), so the run that settles its mark
26
+ * would wait for the first push, and meanwhile the person would be told to
27
+ * trust hooks they already trusted. This file exists exactly while that set
28
+ * waits for its first run in the host, and the entry sends every command
29
+ * through to Node while it does.
30
+ */
31
+ export function pushCheckAwaitingFile(environment, host) {
32
+ return join(configHome(environment), `${host}-push-check-awaiting-first-run`);
33
+ }
34
+ function forget(path) {
35
+ try {
36
+ rmSync(path, { force: true });
37
+ }
38
+ catch {
39
+ /* Left behind, it costs Node's start on each command, never a session. */
40
+ }
41
+ }
18
42
  function write(environment, marks) {
19
43
  try {
20
44
  mkdirSync(configHome(environment), { recursive: true, mode: 0o700 });
21
45
  writeFileSync(join(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
22
46
  mode: 0o600,
23
47
  });
48
+ for (const host of HOSTS_REQUIRING_TRUST) {
49
+ const file = pushCheckAwaitingFile(environment, host);
50
+ if (awaiting(marks[key(host, "push-check")]))
51
+ writeFileSync(file, "", { mode: 0o600 });
52
+ else
53
+ forget(file);
54
+ }
24
55
  }
25
56
  catch {
26
57
  /* A mark that cannot be written costs a notice, never a session. */
27
58
  }
28
59
  }
29
60
  /**
30
- * This command wrote or changed the host's hooks, or found them in place with
31
- * no record that it ever had: either way, from now the host has to have run
32
- * them once before anybody assumes it does.
61
+ * This command wrote or changed one set of the host's hooks, or found it in
62
+ * place with no record that it ever had: either way, from now the host has to
63
+ * have run that set once before anybody assumes it does. `hooks` is how many
64
+ * hooks the set is, which the notice counts.
33
65
  */
34
- export function noteHooksWritten(environment, host, changed, now = Date.now()) {
66
+ export function noteHooksWritten(environment, host, changed, now = Date.now(), options = {}) {
67
+ const set = options.set ?? "guidance";
68
+ const marks = read(environment);
69
+ const mark = marks[key(host, set)];
70
+ const counted = options.hooks === undefined || mark?.hooks === options.hooks;
71
+ if (!changed && mark?.writtenAt !== undefined && counted)
72
+ return;
73
+ write(environment, {
74
+ ...marks,
75
+ [key(host, set)]: {
76
+ ...mark,
77
+ ...(changed || mark?.writtenAt === undefined ? { writtenAt: now } : {}),
78
+ ...(options.hooks === undefined ? {} : { hooks: options.hooks }),
79
+ },
80
+ });
81
+ }
82
+ /** This command took one set of the host's hooks out: nothing of it is left to trust. */
83
+ export function noteHooksRemoved(environment, host, set) {
35
84
  const marks = read(environment);
36
- if (!changed && marks[host]?.writtenAt !== undefined)
85
+ if (marks[key(host, set)] === undefined)
37
86
  return;
38
- write(environment, { ...marks, [host]: { ...marks[host], writtenAt: now } });
87
+ const rest = { ...marks };
88
+ delete rest[key(host, set)];
89
+ write(environment, rest);
39
90
  }
40
- /** One of the host's hooks ran, which it does only once they are trusted. */
41
- export function noteHookFired(environment, host, now = Date.now()) {
91
+ /** One hook of that set ran, which it does only once it is trusted. */
92
+ export function noteHookFired(environment, host, now = Date.now(), set = "guidance") {
93
+ // Settled or not, a file left behind would send every command to Node for good.
94
+ if (set === "push-check")
95
+ forget(pushCheckAwaitingFile(environment, host));
42
96
  const marks = read(environment);
43
- const mark = marks[host];
97
+ const mark = marks[key(host, set)];
44
98
  // Nothing to settle, or already settled: no write on the session's path.
45
99
  if (mark?.firedAt !== undefined && mark.firedAt >= (mark.writtenAt ?? 0))
46
100
  return;
47
- write(environment, { ...marks, [host]: { ...mark, firedAt: now } });
101
+ write(environment, { ...marks, [key(host, set)]: { ...mark, firedAt: now } });
48
102
  }
49
- /** True while the hooks last written for this host have never run in it. */
50
- export function hooksAwaitingTrust(environment, host) {
51
- const mark = read(environment)[host];
103
+ function awaiting(mark) {
52
104
  if (mark?.writtenAt === undefined)
53
105
  return false;
54
106
  return mark.firedAt === undefined || mark.firedAt < mark.writtenAt;
55
107
  }
108
+ /** True while some set of the hooks last written for this host has never run in it. */
109
+ export function hooksAwaitingTrust(environment, host) {
110
+ const marks = read(environment);
111
+ return SETS.some((set) => awaiting(marks[key(host, set)]));
112
+ }
113
+ const WORDS = ["no", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"];
114
+ /** A small count the way a sentence says it. */
115
+ export function countWord(count) {
116
+ return WORDS[count] ?? String(count);
117
+ }
118
+ /** How many of Balladeer's hooks this host was last given, over every set written. */
119
+ export function hooksWritten(environment, host) {
120
+ const marks = read(environment);
121
+ return SETS.reduce((total, set) => {
122
+ const mark = marks[key(host, set)];
123
+ return mark?.writtenAt === undefined ? total : total + (mark.hooks ?? DEFAULT_COUNT[set]);
124
+ }, 0);
125
+ }
56
126
  /** The host a session belongs to, from the name it gave when it said hello. */
57
127
  export function trustingHost(clientName) {
58
128
  return typeof clientName === "string" && clientName.toLowerCase().startsWith("codex")
59
129
  ? "codex"
60
130
  : undefined;
61
131
  }
62
- /** What the agent is asked to pass on, once, in the host where the approval happens. */
63
- export const HOOKS_AWAITING_TRUST_NOTICE = "Note from Balladeer on this laptop, to pass on to the person once: Balladeer's hooks here were added or changed and Codex has not run them. Codex runs a hook only after the person trusts it. Ask them to type /hooks in Codex and trust Balladeer's three hooks; until then Codex has Balladeer's tools without its session-start guidance or its updates.";
132
+ /**
133
+ * What the agent is asked to pass on, once, in the host where the approval
134
+ * happens. It counts the hooks this command last wrote there, so a laptop given
135
+ * the push check is told to trust six and one without it four.
136
+ */
137
+ export function hooksAwaitingTrustNotice(environment, host = "codex") {
138
+ const pushCheck = read(environment)[key(host, "push-check")]?.writtenAt !== undefined;
139
+ const count = hooksWritten(environment, host) || DEFAULT_COUNT.guidance;
140
+ return ("Note from Balladeer on this laptop, to pass on to the person once: Balladeer's hooks here were added or changed and Codex has not run them. Codex runs a hook only after the person trusts it. " +
141
+ `Ask them to type /hooks in Codex and trust Balladeer's ${countWord(count)} hooks; until then Codex has Balladeer's tools without its session-start guidance, its updates, ` +
142
+ (pushCheck
143
+ ? "the note that reminds you to tell them what Balladeer did, or its check after a push."
144
+ : "or the note that reminds you to tell them what Balladeer did."));
145
+ }
@@ -1,7 +1,8 @@
1
+ import type { PromiseAnchors } from "./anchors-schema.js";
1
2
  import type { BehaviorMap } from "./behavior-map-schema.js";
2
3
  import type { PromiseDocument } from "./promise-meaning.js";
3
4
  /**
4
- * The fixed instructions a judge works from, `judge-brief/v1`.
5
+ * The fixed instructions a judge works from, `judge-brief/v5`.
5
6
  *
6
7
  * One promise, one change, one disposable checkout at the change's head. The
7
8
  * rules are the contract's: cases first, linked test first, broken only with a
@@ -13,12 +14,41 @@ import type { PromiseDocument } from "./promise-meaning.js";
13
14
  *
14
15
  * The text is a constant so a verdict can name the exact brief it was given.
15
16
  * Changing a word here is a new revision.
17
+ *
18
+ * v5 is v3's rules word for word; what it added is a section of the prompt
19
+ * (1 October 2026, with promise anchors): a promise with anchors and no
20
+ * behavior map is handed its anchors, the linked tests the graph gives at
21
+ * head, and the files one import step from its anchor files, where v3 said
22
+ * "find where its behavior lives yourself". A promise with a map is handed
23
+ * exactly what v3 handed it. v4 belongs to a candidate measured and not
24
+ * adopted on 30 September, so it is never reused. The judge with anchors in
25
+ * place of a map has not been measured yet.
26
+ */
27
+ export declare const JUDGE_BRIEF_REVISION = "judge-brief/v5";
28
+ /**
29
+ * What `judge-brief/v3` added to v1, and all it added (Robert, 30 September
30
+ * 2026): a way forward when the tests need something the machine lacks.
31
+ *
32
+ * It comes last among the rules and only once the judge would otherwise
33
+ * answer could not tell, never before it has tried to prove or refute, so it
34
+ * cannot spend the time a proof needs. Measured against a same-day control of
35
+ * v1 on 30 September (docs/agentic-testing/evidence/20260930/setup-rule.md),
36
+ * it proved no fewer breaks. Could not tell stays could not tell; the only new
37
+ * thing in the answer is `nextStep`, the one step that would let the tests run.
16
38
  */
17
- export declare const JUDGE_BRIEF_REVISION = "judge-brief/v1";
18
- export declare const JUDGE_BRIEF = "You are judging one change to this repository against one promise about the software's behavior. A named person agreed the promise's meaning. Your working directory is a disposable copy of the repository checked out at the change's head commit; anything you write here is thrown away afterwards.\n\nDecide whether this change keeps the promise, breaks it, or whether you could not tell, and back what you say with evidence you produced yourself.\n\nRules:\n\n1. Read the promise's cases first. The passing cases (passing-1, passing-2, and so on, in the order listed) say what must keep happening. The failing cases (failing-1, and so on) say what must never happen. The non-goals say what the promise does not cover; a change there is not a break.\n2. If linked tests are listed, run them first, before anything else. If the runner already reports a linked test's outcome below, use it and rerun only when you need its output.\n3. Read the code, not descriptions of it. Commit messages, comments, documentation and pull request text say what someone intended, not what the code does.\n4. Report \"broken\" only with a command that fails (exits non-zero) with this change and passes (exits zero) without it, and only after you have run it both ways yourself. To run it without the change, check out the base commit in this copy with `git checkout --detach <base>`, run the command, then return with `git checkout --detach <head>`. If the command needs a test you wrote, put that test in a new file rather than editing an existing one, so it exists on both commits. The command must run from the repository root or from the directory you name as its cwd, and must not depend on anything outside this copy.\n5. Report \"kept\" only after you have exercised each passing case against the changed code, by running a test or a command that shows it, and list every case you exercised.\n6. Anything else is \"could_not_tell\". Say in notes what stopped you: a missing dependency, a service you could not reach, a case you could not exercise, time running out.\n7. Name the promise, never the person. Do not put anyone's name, email or account in your answer.\n8. Do not push, open or merge pull requests, publish anything, or change anything outside this copy. Do not read or print credentials.\n\nYour final message must be exactly one JSON object and nothing after it:\n\n{\"promiseId\": \"<the promise id below>\", \"verdict\": \"kept\" | \"broken\" | \"could_not_tell\", \"reproduction\": {\"command\": \"<shell command>\", \"cwd\": \".\"} or null, \"exercisedCases\": [\"passing-1\", ...], \"notes\": \"<at most a few sentences>\"}\n\nA reproduction is required for \"broken\" and optional otherwise.";
39
+ export declare const JUDGE_BRIEF_SETUP_RULE = "9. Only when you would otherwise answer \"could_not_tell\" because the tests need something this machine does not have, such as a test database, a running service, a browser or an environment variable, and only after you have tried to show a break or a keep as the rules above say: look for how the repository documents running its tests, in its README, its agent instruction files (AGENTS.md, CLAUDE.md and the like) and its package scripts, and try the steps it documents. Take no step it does not document, and even among those never install system software, and never ask for or read credentials. If the tests then run, carry on under the rules above. If they still cannot run, or trying would leave no time to answer, the answer stays \"could_not_tell\", never \"kept\", and you add \"nextStep\" to your JSON answer: the one step that would let the tests run, written so the person's agent can act on it, such as the exact documented command that starts the test database. Otherwise leave nextStep out.";
40
+ export declare const JUDGE_BRIEF = "You are judging one change to this repository against one promise about the software's behavior. A named person agreed the promise's meaning. Your working directory is a disposable copy of the repository checked out at the change's head commit; anything you write here is thrown away afterwards.\n\nDecide whether this change keeps the promise, breaks it, or whether you could not tell, and back what you say with evidence you produced yourself.\n\nRules:\n\n1. Read the promise's cases first. The passing cases (passing-1, passing-2, and so on, in the order listed) say what must keep happening. The failing cases (failing-1, and so on) say what must never happen. The non-goals say what the promise does not cover; a change there is not a break.\n2. If linked tests are listed, run them first, before anything else. If the runner already reports a linked test's outcome below, use it and rerun only when you need its output.\n3. Read the code, not descriptions of it. Commit messages, comments, documentation and pull request text say what someone intended, not what the code does.\n4. Report \"broken\" only with a command that fails (exits non-zero) with this change and passes (exits zero) without it, and only after you have run it both ways yourself. To run it without the change, check out the base commit in this copy with `git checkout --detach <base>`, run the command, then return with `git checkout --detach <head>`. If the command needs a test you wrote, put that test in a new file rather than editing an existing one, so it exists on both commits. The command must run from the repository root or from the directory you name as its cwd, and must not depend on anything outside this copy.\n5. Report \"kept\" only after you have exercised each passing case against the changed code, by running a test or a command that shows it, and list every case you exercised.\n6. Anything else is \"could_not_tell\". Say in notes what stopped you: a missing dependency, a service you could not reach, a case you could not exercise, time running out.\n7. Name the promise, never the person. Do not put anyone's name, email or account in your answer.\n8. Do not push, open or merge pull requests, publish anything, or change anything outside this copy. Do not read or print credentials.\n9. Only when you would otherwise answer \"could_not_tell\" because the tests need something this machine does not have, such as a test database, a running service, a browser or an environment variable, and only after you have tried to show a break or a keep as the rules above say: look for how the repository documents running its tests, in its README, its agent instruction files (AGENTS.md, CLAUDE.md and the like) and its package scripts, and try the steps it documents. Take no step it does not document, and even among those never install system software, and never ask for or read credentials. If the tests then run, carry on under the rules above. If they still cannot run, or trying would leave no time to answer, the answer stays \"could_not_tell\", never \"kept\", and you add \"nextStep\" to your JSON answer: the one step that would let the tests run, written so the person's agent can act on it, such as the exact documented command that starts the test database. Otherwise leave nextStep out.\n\nYour final message must be exactly one JSON object and nothing after it:\n\n{\"promiseId\": \"<the promise id below>\", \"verdict\": \"kept\" | \"broken\" | \"could_not_tell\", \"reproduction\": {\"command\": \"<shell command>\", \"cwd\": \".\"} or null, \"exercisedCases\": [\"passing-1\", ...], \"notes\": \"<at most a few sentences>\"}\n\nA reproduction is required for \"broken\" and optional otherwise.";
19
41
  export type JudgeBriefInput = Readonly<{
20
42
  promise: PromiseDocument;
21
43
  map?: BehaviorMap;
44
+ /** Read only when there is no map. */
45
+ anchors?: Readonly<{
46
+ anchors: PromiseAnchors;
47
+ linkedTests: readonly Readonly<{
48
+ file: string;
49
+ }>[];
50
+ neighborhood: readonly string[];
51
+ }>;
22
52
  changedFiles: readonly string[];
23
53
  base: string;
24
54
  head: string;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The fixed instructions a judge works from, `judge-brief/v1`.
2
+ * The fixed instructions a judge works from, `judge-brief/v5`.
3
3
  *
4
4
  * One promise, one change, one disposable checkout at the change's head. The
5
5
  * rules are the contract's: cases first, linked test first, broken only with a
@@ -11,8 +11,29 @@
11
11
  *
12
12
  * The text is a constant so a verdict can name the exact brief it was given.
13
13
  * Changing a word here is a new revision.
14
+ *
15
+ * v5 is v3's rules word for word; what it added is a section of the prompt
16
+ * (1 October 2026, with promise anchors): a promise with anchors and no
17
+ * behavior map is handed its anchors, the linked tests the graph gives at
18
+ * head, and the files one import step from its anchor files, where v3 said
19
+ * "find where its behavior lives yourself". A promise with a map is handed
20
+ * exactly what v3 handed it. v4 belongs to a candidate measured and not
21
+ * adopted on 30 September, so it is never reused. The judge with anchors in
22
+ * place of a map has not been measured yet.
23
+ */
24
+ export const JUDGE_BRIEF_REVISION = "judge-brief/v5";
25
+ /**
26
+ * What `judge-brief/v3` added to v1, and all it added (Robert, 30 September
27
+ * 2026): a way forward when the tests need something the machine lacks.
28
+ *
29
+ * It comes last among the rules and only once the judge would otherwise
30
+ * answer could not tell, never before it has tried to prove or refute, so it
31
+ * cannot spend the time a proof needs. Measured against a same-day control of
32
+ * v1 on 30 September (docs/agentic-testing/evidence/20260930/setup-rule.md),
33
+ * it proved no fewer breaks. Could not tell stays could not tell; the only new
34
+ * thing in the answer is `nextStep`, the one step that would let the tests run.
14
35
  */
15
- export const JUDGE_BRIEF_REVISION = "judge-brief/v1";
36
+ export const JUDGE_BRIEF_SETUP_RULE = `9. Only when you would otherwise answer "could_not_tell" because the tests need something this machine does not have, such as a test database, a running service, a browser or an environment variable, and only after you have tried to show a break or a keep as the rules above say: look for how the repository documents running its tests, in its README, its agent instruction files (AGENTS.md, CLAUDE.md and the like) and its package scripts, and try the steps it documents. Take no step it does not document, and even among those never install system software, and never ask for or read credentials. If the tests then run, carry on under the rules above. If they still cannot run, or trying would leave no time to answer, the answer stays "could_not_tell", never "kept", and you add "nextStep" to your JSON answer: the one step that would let the tests run, written so the person's agent can act on it, such as the exact documented command that starts the test database. Otherwise leave nextStep out.`;
16
37
  export const JUDGE_BRIEF = `You are judging one change to this repository against one promise about the software's behavior. A named person agreed the promise's meaning. Your working directory is a disposable copy of the repository checked out at the change's head commit; anything you write here is thrown away afterwards.
17
38
 
18
39
  Decide whether this change keeps the promise, breaks it, or whether you could not tell, and back what you say with evidence you produced yourself.
@@ -27,6 +48,7 @@ Rules:
27
48
  6. Anything else is "could_not_tell". Say in notes what stopped you: a missing dependency, a service you could not reach, a case you could not exercise, time running out.
28
49
  7. Name the promise, never the person. Do not put anyone's name, email or account in your answer.
29
50
  8. Do not push, open or merge pull requests, publish anything, or change anything outside this copy. Do not read or print credentials.
51
+ ${JUDGE_BRIEF_SETUP_RULE}
30
52
 
31
53
  Your final message must be exactly one JSON object and nothing after it:
32
54
 
@@ -59,6 +81,21 @@ export function judgePrompt(input) {
59
81
  sections.push("Where this promise's behavior lives, from its behavior map (built earlier; check it against the code):");
60
82
  sections.push(JSON.stringify(map, null, 2));
61
83
  }
84
+ else if (input.anchors !== undefined) {
85
+ const { anchors, linkedTests, neighborhood } = input.anchors;
86
+ sections.push("Where this promise's behavior lives, from its anchors (picked earlier from the promise's words and a list of this repository's files; check them against the code):");
87
+ sections.push(JSON.stringify({
88
+ entryPoints: anchors.entryPoints,
89
+ symbols: anchors.symbols,
90
+ resources: anchors.resources,
91
+ linkedTests,
92
+ }, null, 2));
93
+ if (neighborhood.length > 0) {
94
+ sections.push("Files one import step from those (what they import and what imports them), where to look next if the behavior reaches further:");
95
+ for (const file of neighborhood)
96
+ sections.push(`- ${file}`);
97
+ }
98
+ }
62
99
  else {
63
100
  sections.push("There is no behavior map for this promise; find where its behavior lives yourself.");
64
101
  }