balladeer 1.0.16 → 1.0.17
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/cli.d.ts +1 -1
- package/dist/cli.js +19 -6
- package/dist/commands/guidance.d.ts +2 -1
- package/dist/commands/guidance.js +55 -0
- package/dist/commands/judge.d.ts +168 -9
- package/dist/commands/judge.js +594 -51
- package/dist/commands/map.d.ts +46 -2
- package/dist/commands/map.js +242 -5
- package/dist/commands/mcp.js +2 -2
- package/dist/guidance-hook.mjs +278 -94
- package/dist/hook-trust.d.ts +41 -9
- package/dist/hook-trust.js +98 -16
- package/dist/judge-brief.d.ts +15 -3
- package/dist/judge-brief.js +15 -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 +8 -0
- package/dist/promise-meaning.js +9 -1
- package/dist/relay.d.ts +71 -0
- package/dist/relay.js +193 -0
- package/dist/remove-earlier.js +4 -2
- package/dist/risk/index.d.ts +1 -1
- package/dist/risk/index.js +1 -1
- 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 +3 -3
- package/dist/wire.js +2 -2
- package/package.json +1 -1
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,7 @@
|
|
|
1
1
|
import type { BehaviorMap } from "./behavior-map-schema.js";
|
|
2
2
|
import type { PromiseDocument } from "./promise-meaning.js";
|
|
3
3
|
/**
|
|
4
|
-
* The fixed instructions a judge works from, `judge-brief/
|
|
4
|
+
* The fixed instructions a judge works from, `judge-brief/v3`.
|
|
5
5
|
*
|
|
6
6
|
* One promise, one change, one disposable checkout at the change's head. The
|
|
7
7
|
* rules are the contract's: cases first, linked test first, broken only with a
|
|
@@ -14,8 +14,20 @@ import type { PromiseDocument } from "./promise-meaning.js";
|
|
|
14
14
|
* The text is a constant so a verdict can name the exact brief it was given.
|
|
15
15
|
* Changing a word here is a new revision.
|
|
16
16
|
*/
|
|
17
|
-
export declare const JUDGE_BRIEF_REVISION = "judge-brief/
|
|
18
|
-
|
|
17
|
+
export declare const JUDGE_BRIEF_REVISION = "judge-brief/v3";
|
|
18
|
+
/**
|
|
19
|
+
* What `judge-brief/v3` added to v1, and all it added (Robert, 30 September
|
|
20
|
+
* 2026): a way forward when the tests need something the machine lacks.
|
|
21
|
+
*
|
|
22
|
+
* It comes last among the rules and only once the judge would otherwise
|
|
23
|
+
* answer could not tell, never before it has tried to prove or refute, so it
|
|
24
|
+
* cannot spend the time a proof needs. Measured against a same-day control of
|
|
25
|
+
* v1 on 30 September (docs/agentic-testing/evidence/20260930/setup-rule.md),
|
|
26
|
+
* it proved no fewer breaks. Could not tell stays could not tell; the only new
|
|
27
|
+
* thing in the answer is `nextStep`, the one step that would let the tests run.
|
|
28
|
+
*/
|
|
29
|
+
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.";
|
|
30
|
+
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
31
|
export type JudgeBriefInput = Readonly<{
|
|
20
32
|
promise: PromiseDocument;
|
|
21
33
|
map?: BehaviorMap;
|
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/v3`.
|
|
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
|
|
@@ -12,7 +12,19 @@
|
|
|
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
14
|
*/
|
|
15
|
-
export const JUDGE_BRIEF_REVISION = "judge-brief/
|
|
15
|
+
export const JUDGE_BRIEF_REVISION = "judge-brief/v3";
|
|
16
|
+
/**
|
|
17
|
+
* What `judge-brief/v3` added to v1, and all it added (Robert, 30 September
|
|
18
|
+
* 2026): a way forward when the tests need something the machine lacks.
|
|
19
|
+
*
|
|
20
|
+
* It comes last among the rules and only once the judge would otherwise
|
|
21
|
+
* answer could not tell, never before it has tried to prove or refute, so it
|
|
22
|
+
* cannot spend the time a proof needs. Measured against a same-day control of
|
|
23
|
+
* v1 on 30 September (docs/agentic-testing/evidence/20260930/setup-rule.md),
|
|
24
|
+
* it proved no fewer breaks. Could not tell stays could not tell; the only new
|
|
25
|
+
* thing in the answer is `nextStep`, the one step that would let the tests run.
|
|
26
|
+
*/
|
|
27
|
+
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
28
|
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
29
|
|
|
18
30
|
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 +39,7 @@ Rules:
|
|
|
27
39
|
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
40
|
7. Name the promise, never the person. Do not put anyone's name, email or account in your answer.
|
|
29
41
|
8. Do not push, open or merge pull requests, publish anything, or change anything outside this copy. Do not read or print credentials.
|
|
42
|
+
${JUDGE_BRIEF_SETUP_RULE}
|
|
30
43
|
|
|
31
44
|
Your final message must be exactly one JSON object and nothing after it:
|
|
32
45
|
|
package/dist/judge-hook.d.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Where a judge run starts: a coding host's
|
|
3
|
-
* pre-push hook.
|
|
2
|
+
* Where a judge run starts: a coding host's hook around a shell command, or
|
|
3
|
+
* git's own pre-push hook.
|
|
4
4
|
*
|
|
5
|
-
* The host hook fires on every shell command the agent runs,
|
|
6
|
-
*
|
|
7
|
-
* push
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The host hook fires on every shell command the agent runs, twice: before it
|
|
6
|
+
* runs (PreToolUse) and after (PostToolUse). The first job each time is to say
|
|
7
|
+
* "not a push" and get out of the way in milliseconds. Only `git push`, `gh pr
|
|
8
|
+
* create` and `gh pr merge` go further. The command is read the way a shell
|
|
9
|
+
* reads it, so a push inside a quoted commit message is not a push and `git
|
|
10
|
+
* stash push` is not one either.
|
|
11
|
+
*
|
|
12
|
+
* Report mode judges after the push, so the push is never held and the check
|
|
13
|
+
* sees exactly what was pushed. Block mode judges before it, because a push
|
|
14
|
+
* can only be held before it runs.
|
|
10
15
|
*/
|
|
11
16
|
export type HookHost = "claude" | "codex" | "git";
|
|
17
|
+
/** The two host events the judge is installed on, in the order a command meets them. */
|
|
18
|
+
export type HookEvent = "PreToolUse" | "PostToolUse";
|
|
19
|
+
export declare const JUDGE_HOOK_EVENTS: readonly HookEvent[];
|
|
12
20
|
export type HookInput = Readonly<{
|
|
13
21
|
eventName?: string;
|
|
14
22
|
toolName?: string;
|
|
15
23
|
command?: string;
|
|
16
24
|
cwd?: string;
|
|
17
25
|
model?: string;
|
|
26
|
+
/** The host's session, from `session_id`, which both Claude Code and Codex send. */
|
|
27
|
+
sessionId?: string;
|
|
18
28
|
}>;
|
|
19
29
|
/** A coding host's hook payload, or nothing when the text is not one. */
|
|
20
30
|
export declare function parseHookInput(text: string): HookInput | undefined;
|
|
@@ -26,17 +36,118 @@ export type PushedRef = Readonly<{
|
|
|
26
36
|
remoteSha: string;
|
|
27
37
|
}>;
|
|
28
38
|
export declare function parsePrePushInput(text: string): PushedRef[];
|
|
39
|
+
/** What separates or groups commands in a shell line. */
|
|
40
|
+
export type ShellOperator = ";" | "&&" | "||" | "|" | "&" | "\n" | "(" | ")" | "`";
|
|
41
|
+
/** One piece of a shell line: a simple command's words, or what comes between commands. */
|
|
42
|
+
export type ShellPiece = Readonly<{
|
|
43
|
+
kind: "command";
|
|
44
|
+
words: readonly string[];
|
|
45
|
+
}> | Readonly<{
|
|
46
|
+
kind: "operator";
|
|
47
|
+
operator: ShellOperator;
|
|
48
|
+
}>;
|
|
29
49
|
/**
|
|
30
|
-
*
|
|
50
|
+
* A shell line in the order a shell reads it: simple commands and the
|
|
51
|
+
* operators between them.
|
|
52
|
+
*
|
|
53
|
+
* Quotes and backslashes are honored, so quoted text stays one word. `;`,
|
|
54
|
+
* `&&`, `||`, `|`, `&`, newlines, parentheses and backquotes come between
|
|
55
|
+
* commands. A comment runs to the end of its line. A redirection (`>`, `2>&1`,
|
|
56
|
+
* `<file`, `&>log`) and its target are left out of the command's words.
|
|
31
57
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
58
|
+
* A here-document (`<<WORD`, `<<'WORD'`, `<<"WORD"`, `<<-WORD`) is data: the
|
|
59
|
+
* lines after its command, up to a line that is exactly WORD (leading tabs
|
|
60
|
+
* removed for `<<-`), are not commands. A here-string's word (`<<<`) is data
|
|
61
|
+
* too. Two exceptions keep a push inside one from going unseen: text fed to a
|
|
62
|
+
* shell that reads its script from input (`bash <<EOF`, `cat <<EOF | sh`) is
|
|
63
|
+
* read as that script, and the `$(...)` and backquotes in a here-document
|
|
64
|
+
* whose delimiter is not quoted are read as the commands the shell runs while
|
|
65
|
+
* expanding it. Either is read as a subshell, after the pipeline it feeds. A
|
|
66
|
+
* here-document that never reaches its delimiter line is not taken as one, so
|
|
67
|
+
* the lines after it are still read as commands.
|
|
68
|
+
*/
|
|
69
|
+
export declare function scanShell(line: string, depth?: number): ShellPiece[];
|
|
70
|
+
/**
|
|
71
|
+
* The simple commands a shell line runs, as word lists, in the order
|
|
72
|
+
* `scanShell` reads them: `npm test && git push` is two commands, `$(git
|
|
73
|
+
* push)` is found, and a here-document's text is not a command.
|
|
36
74
|
*/
|
|
37
75
|
export declare function shellCommands(line: string): string[][];
|
|
76
|
+
/** Where one push, pull request creation or merge in a shell line runs. */
|
|
77
|
+
export type PushSite = Readonly<{
|
|
78
|
+
kind: "push" | "create" | "merge";
|
|
79
|
+
/** The absolute directory it runs in; absent when that cannot be told for certain. */
|
|
80
|
+
directory?: string;
|
|
81
|
+
/** The repository a pull request command names with `--repo`, `-R` or `GH_REPO`. */
|
|
82
|
+
repo?: string;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Every push, pull request creation and merge in a shell line, with the
|
|
86
|
+
* directory it runs in when that can be told for certain.
|
|
87
|
+
*
|
|
88
|
+
* Starting from `cwd`, each `cd` moves the line along in order, and a git
|
|
89
|
+
* command's own `-C` options apply to that command alone. A `cd` inside
|
|
90
|
+
* parentheses, backquotes or `$(...)` ends with them. Anything that makes the
|
|
91
|
+
* directory uncertain makes it unknown from there on rather than guessed: `cd`
|
|
92
|
+
* with no directory, `cd -`, a variable or pattern, `pushd` or `popd`, a `cd`
|
|
93
|
+
* in a pipeline or in the background, a `cd` inside `if`, a loop, `case` or a
|
|
94
|
+
* function, and a `cd` that ran only when an earlier command succeeded or
|
|
95
|
+
* failed, once the line goes past its `&&` chain. A shell's `-c` script starts
|
|
96
|
+
* where the shell does and moves nothing outside it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function pushSites(line: string, cwd: string | undefined, depth?: number): PushSite[];
|
|
99
|
+
/** Where a shell line's pushes run, when that is one directory that can be told for certain. */
|
|
100
|
+
export type PushPlace = Readonly<{
|
|
101
|
+
known: true;
|
|
102
|
+
directory: string;
|
|
103
|
+
sites: readonly PushSite[];
|
|
104
|
+
}> | Readonly<{
|
|
105
|
+
known: false;
|
|
106
|
+
}>;
|
|
107
|
+
/**
|
|
108
|
+
* The one directory a shell line's pushes run in, starting from `cwd`, or
|
|
109
|
+
* unknown when any of them cannot be told or they run in more than one.
|
|
110
|
+
*/
|
|
111
|
+
export declare function pushPlace(line: string, cwd: string): PushPlace;
|
|
112
|
+
/**
|
|
113
|
+
* A repository as `owner/name` in lower case, from a remote URL or a `--repo`
|
|
114
|
+
* value: `https://github.com/o/r.git`, `git@github.com:o/r.git`,
|
|
115
|
+
* `ssh://git@host/o/r`, `HOST/o/r` or `o/r`. Nothing when there is no owner.
|
|
116
|
+
*/
|
|
117
|
+
export declare function repositoryName(reference: string): string | undefined;
|
|
118
|
+
/** The repositories a checkout's remotes point at, from what `git remote -v` prints. */
|
|
119
|
+
export declare function remoteRepositoryNames(listing: string): Set<string>;
|
|
38
120
|
/** Whether a shell line pushes: `git push`, `gh pr create`, or `gh pr merge`, anywhere in it. */
|
|
39
|
-
export declare function isPushCommand(line: string
|
|
121
|
+
export declare function isPushCommand(line: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Whether a shell line sends this checkout's commits: a `git push`, or a `gh pr
|
|
124
|
+
* create`, which pushes the branch it opens a pull request for. A line whose
|
|
125
|
+
* only push is `gh pr merge` sends none; that pull request's commits were on
|
|
126
|
+
* the remote before the line ran.
|
|
127
|
+
*/
|
|
128
|
+
export declare function sendsCommits(line: string): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The first command in the line that makes a commit before it pushes, as
|
|
131
|
+
* `git <subcommand>`, or nothing.
|
|
132
|
+
*
|
|
133
|
+
* A host's PreToolUse hook sees the checkout before the line runs, so in
|
|
134
|
+
* `git commit -am x && git push` the check judges the code as it was before
|
|
135
|
+
* the commit, and the commit the push carries is one it never saw. Only a
|
|
136
|
+
* commit made before the push matters: one made after it is not in what was
|
|
137
|
+
* pushed, and `gh pr merge` merges a pull request's head, not this checkout's.
|
|
138
|
+
*/
|
|
139
|
+
export declare function commitBeforePush(line: string): string | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* What a `gh pr merge` names. Neither field means the current branch's pull
|
|
142
|
+
* request, which is the change in this checkout; either one can name a pull
|
|
143
|
+
* request whose change is somewhere else entirely.
|
|
144
|
+
*/
|
|
145
|
+
export type MergeTarget = Readonly<{
|
|
146
|
+
selector?: string;
|
|
147
|
+
repo?: string;
|
|
148
|
+
}>;
|
|
149
|
+
/** Every `gh pr merge` in a shell line, with the pull request and repository it names. */
|
|
150
|
+
export declare function mergeTargets(line: string): MergeTarget[];
|
|
40
151
|
export type HookResponse = Readonly<{
|
|
41
152
|
stdout: string;
|
|
42
153
|
stderr: string;
|
|
@@ -47,11 +158,14 @@ export type HookResponse = Readonly<{
|
|
|
47
158
|
* and the push goes ahead; a block is exit 2 with the reason, which both hosts
|
|
48
159
|
* treat as a refusal of that one command. Claude Code also gets the reason as
|
|
49
160
|
* `permissionDecision: "deny"`, which it prefers to stderr; Codex is given the
|
|
50
|
-
* stderr form its documentation names.
|
|
161
|
+
* stderr form its documentation names. After the command ran (PostToolUse)
|
|
162
|
+
* there is nothing left to hold, so the lines are only ever context, named for
|
|
163
|
+
* the event that asked.
|
|
51
164
|
*/
|
|
52
165
|
export declare function hookResponse(host: HookHost, outcome: Readonly<{
|
|
53
166
|
block: boolean;
|
|
54
167
|
lines: readonly string[];
|
|
168
|
+
event?: HookEvent;
|
|
55
169
|
}>): HookResponse;
|
|
56
170
|
/** The status line both hosts show while the hook runs, and the mark of the entry this command owns. */
|
|
57
171
|
export declare const JUDGE_HOOK_MARKER = "Balladeer judge";
|
|
@@ -78,7 +192,67 @@ export type HookInstallResult = Readonly<{
|
|
|
78
192
|
status: "written" | "unchanged" | "refused";
|
|
79
193
|
reason?: string;
|
|
80
194
|
}>;
|
|
195
|
+
/** The hook file the installer writes for a host, relative to the repository root. */
|
|
196
|
+
export declare function judgeHookFile(host: "claude" | "codex"): string;
|
|
197
|
+
/**
|
|
198
|
+
* Whether the hook file the installer writes has this command's entry for one
|
|
199
|
+
* event. It reads the file rather than guessing, so a checkout installed by an
|
|
200
|
+
* earlier release, which wrote only the PreToolUse entry, is told apart from a
|
|
201
|
+
* current one. Anything unreadable counts as no entry.
|
|
202
|
+
*/
|
|
203
|
+
export declare function hasJudgeEntry(root: string, host: "claude" | "codex", event: HookEvent): boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Whether this checkout has the push check's own project entry for a host,
|
|
206
|
+
* under either event. Where it does, that entry owns the check in this
|
|
207
|
+
* checkout and the one setup writes at the host's user scope stands aside, so
|
|
208
|
+
* a push is judged once. Either event counts: a checkout an earlier release
|
|
209
|
+
* installed has only the entry before the push, and it keeps judging there.
|
|
210
|
+
*/
|
|
211
|
+
export declare function hasProjectJudgeHook(root: string, host: "claude" | "codex"): boolean;
|
|
212
|
+
/**
|
|
213
|
+
* The checkout that holds a folder, found by walking up to its `.git` entry (a
|
|
214
|
+
* directory, or a file in a worktree). No process is started, so a folder that
|
|
215
|
+
* is not in a checkout at all is known in well under a millisecond.
|
|
216
|
+
*/
|
|
217
|
+
export declare function checkoutRoot(start: string): string | undefined;
|
|
218
|
+
/**
|
|
219
|
+
* The push check's switch for this laptop, in Balladeer's own config home.
|
|
220
|
+
* Absent means on. Written only by `balladeer judge --push-check off`, and
|
|
221
|
+
* read by the user-scope entry on each push, so turning it off takes effect in
|
|
222
|
+
* sessions that are already open.
|
|
223
|
+
*/
|
|
224
|
+
export declare const PUSH_CHECK_SETTING_FILE = "push-check.json";
|
|
225
|
+
export declare function pushCheckSettingPath(environment: NodeJS.ProcessEnv): string;
|
|
226
|
+
/** True only when this laptop's switch says off; anything unreadable leaves it on. */
|
|
227
|
+
export declare function pushCheckOff(environment: NodeJS.ProcessEnv): boolean;
|
|
228
|
+
/** Turn the user-scope push check off or back on for this laptop. */
|
|
229
|
+
export declare function setPushCheck(environment: NodeJS.ProcessEnv, on: boolean): void;
|
|
81
230
|
/** Where git looks for hooks in this checkout, which is not `.git/hooks` in a worktree. */
|
|
82
231
|
export declare function gitHooksDirectory(root: string): string;
|
|
83
232
|
/** Install the judge hook for one host. Running it twice changes nothing the second time. */
|
|
84
233
|
export declare function installJudgeHook(root: string, target: HookHost): HookInstallResult;
|
|
234
|
+
/**
|
|
235
|
+
* Whether a commit is on a remote now: reachable from a remote-tracking ref,
|
|
236
|
+
* or else the tip of a branch some remote reports.
|
|
237
|
+
*
|
|
238
|
+
* `git push`, `git push origin HEAD:branch` and the push `gh pr create` makes
|
|
239
|
+
* all move the remote-tracking ref of what they pushed, so the first check is
|
|
240
|
+
* local and immediate. The second is there because a push can land without
|
|
241
|
+
* that ref moving: Codex's workspace sandbox keeps `.git` read-only, so its
|
|
242
|
+
* pushes reach the remote and then fail to update `refs/remotes/...` (hook
|
|
243
|
+
* canary, 29 September 2026). Asking each remote for its branch tips settles
|
|
244
|
+
* it without fetching anything, and a push that failed leaves its commit on
|
|
245
|
+
* neither.
|
|
246
|
+
*/
|
|
247
|
+
export declare function pushLanded(root: string, sha: string): Promise<boolean>;
|
|
248
|
+
/**
|
|
249
|
+
* Where the remote default branch stood before this push, when this push is
|
|
250
|
+
* what moved it to `head`; otherwise nothing, and the ordinary fork-point rule
|
|
251
|
+
* applies.
|
|
252
|
+
*
|
|
253
|
+
* Judged after a push straight to the default branch, the fork point with that
|
|
254
|
+
* branch's remote-tracking ref is `head` itself, so the change would shrink to
|
|
255
|
+
* its last commit. git records the push in the ref's reflog ("update by
|
|
256
|
+
* push"), and the entry before it is where the branch stood.
|
|
257
|
+
*/
|
|
258
|
+
export declare function baseBeforePush(root: string, head: string): Promise<string | undefined>;
|