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.
- package/dist/anchor-budget.d.ts +40 -0
- package/dist/anchor-budget.js +110 -0
- package/dist/anchoring.d.ts +241 -0
- package/dist/anchoring.js +766 -0
- package/dist/anchors-client.d.ts +62 -0
- package/dist/anchors-client.js +197 -0
- package/dist/anchors-schema.d.ts +85 -0
- package/dist/anchors-schema.js +246 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +24 -7
- package/dist/commands/anchor-usage.d.ts +6 -0
- package/dist/commands/anchor-usage.js +19 -0
- package/dist/commands/anchor.d.ts +127 -0
- package/dist/commands/anchor.js +303 -0
- package/dist/commands/guidance.d.ts +2 -1
- package/dist/commands/guidance.js +55 -0
- package/dist/commands/judge.d.ts +219 -12
- package/dist/commands/judge.js +911 -103
- package/dist/commands/map.d.ts +46 -2
- package/dist/commands/map.js +244 -5
- package/dist/commands/mcp.js +2 -2
- package/dist/guidance-hook.mjs +282 -95
- package/dist/guidance.d.ts +7 -0
- package/dist/guidance.js +10 -1
- package/dist/headless-agent.d.ts +19 -1
- package/dist/headless-agent.js +22 -5
- package/dist/hook-trust.d.ts +41 -9
- package/dist/hook-trust.js +98 -16
- package/dist/judge-brief.d.ts +33 -3
- package/dist/judge-brief.js +39 -2
- package/dist/judge-hook.d.ts +188 -14
- package/dist/judge-hook.js +919 -61
- package/dist/judge-said.d.ts +52 -0
- package/dist/judge-said.js +181 -0
- package/dist/promise-meaning.d.ts +25 -0
- package/dist/promise-meaning.js +24 -7
- package/dist/relay.d.ts +71 -0
- package/dist/relay.js +193 -0
- package/dist/remove-earlier.js +4 -2
- package/dist/risk/git.d.ts +3 -1
- package/dist/risk/git.js +3 -3
- package/dist/risk/graph-cache.d.ts +83 -0
- package/dist/risk/graph-cache.js +291 -0
- package/dist/risk/import-graph.d.ts +43 -0
- package/dist/risk/import-graph.js +88 -34
- package/dist/risk/index.d.ts +1 -1
- package/dist/risk/index.js +1 -1
- package/dist/risk/pipeline.d.ts +13 -0
- package/dist/risk/pipeline.js +15 -4
- package/dist/risk/score.d.ts +10 -0
- package/dist/risk/score.js +11 -2
- package/dist/scratch-worktree.d.ts +5 -1
- package/dist/scratch-worktree.js +9 -2
- package/dist/user-scope.d.ts +74 -3
- package/dist/user-scope.js +270 -9
- package/dist/wire.d.ts +19 -3
- package/dist/wire.js +2 -2
- package/package.json +7 -2
package/dist/guidance.d.ts
CHANGED
|
@@ -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 =
|
|
66
|
+
url.pathname = pathname;
|
|
58
67
|
return url;
|
|
59
68
|
}
|
|
60
69
|
function readJson(path) {
|
package/dist/headless-agent.d.ts
CHANGED
|
@@ -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.
|
package/dist/headless-agent.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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() === "")
|
package/dist/hook-trust.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
/**
|
|
31
|
-
|
|
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;
|
package/dist/hook-trust.js
CHANGED
|
@@ -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
|
|
31
|
-
* no record that it ever had: either way, from now the host has to
|
|
32
|
-
*
|
|
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 (
|
|
85
|
+
if (marks[key(host, set)] === undefined)
|
|
37
86
|
return;
|
|
38
|
-
|
|
87
|
+
const rest = { ...marks };
|
|
88
|
+
delete rest[key(host, set)];
|
|
89
|
+
write(environment, rest);
|
|
39
90
|
}
|
|
40
|
-
/** One of
|
|
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
|
-
|
|
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
|
-
/**
|
|
63
|
-
|
|
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
|
+
}
|
package/dist/judge-brief.d.ts
CHANGED
|
@@ -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/
|
|
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
|
|
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;
|
package/dist/judge-brief.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The fixed instructions a judge works from, `judge-brief/
|
|
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
|
|
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
|
}
|