balladeer 1.0.15 → 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.
Files changed (77) hide show
  1. package/README.md +2 -0
  2. package/dist/behavior-map-schema.d.ts +107 -0
  3. package/dist/behavior-map-schema.js +232 -0
  4. package/dist/cli.d.ts +6 -1
  5. package/dist/cli.js +74 -6
  6. package/dist/commands/guidance.d.ts +2 -1
  7. package/dist/commands/guidance.js +55 -0
  8. package/dist/commands/judge.d.ts +430 -0
  9. package/dist/commands/judge.js +1713 -0
  10. package/dist/commands/map.d.ts +164 -0
  11. package/dist/commands/map.js +838 -0
  12. package/dist/commands/mcp.js +2 -2
  13. package/dist/commands/offers.d.ts +265 -0
  14. package/dist/commands/offers.js +801 -0
  15. package/dist/commands/risk.d.ts +29 -0
  16. package/dist/commands/risk.js +133 -0
  17. package/dist/copy.d.ts +24 -1
  18. package/dist/copy.js +91 -0
  19. package/dist/guidance-hook.mjs +348 -94
  20. package/dist/headless-agent.d.ts +166 -0
  21. package/dist/headless-agent.js +416 -0
  22. package/dist/hook-trust.d.ts +41 -9
  23. package/dist/hook-trust.js +98 -16
  24. package/dist/judge-brief.d.ts +48 -0
  25. package/dist/judge-brief.js +102 -0
  26. package/dist/judge-hook.d.ts +258 -0
  27. package/dist/judge-hook.js +1278 -0
  28. package/dist/judge-said.d.ts +52 -0
  29. package/dist/judge-said.js +181 -0
  30. package/dist/map-brief.d.ts +16 -0
  31. package/dist/map-brief.js +41 -0
  32. package/dist/offer-brief.d.ts +95 -0
  33. package/dist/offer-brief.js +217 -0
  34. package/dist/owned-process.d.ts +65 -0
  35. package/dist/owned-process.js +146 -0
  36. package/dist/promise-meaning.d.ts +134 -0
  37. package/dist/promise-meaning.js +300 -0
  38. package/dist/relay.d.ts +71 -0
  39. package/dist/relay.js +193 -0
  40. package/dist/remove-earlier.js +4 -2
  41. package/dist/risk/contract.d.ts +145 -0
  42. package/dist/risk/contract.js +74 -0
  43. package/dist/risk/describe.d.ts +7 -0
  44. package/dist/risk/describe.js +45 -0
  45. package/dist/risk/diff.d.ts +26 -0
  46. package/dist/risk/diff.js +174 -0
  47. package/dist/risk/extract.d.ts +43 -0
  48. package/dist/risk/extract.js +336 -0
  49. package/dist/risk/git.d.ts +30 -0
  50. package/dist/risk/git.js +118 -0
  51. package/dist/risk/import-graph.d.ts +41 -0
  52. package/dist/risk/import-graph.js +487 -0
  53. package/dist/risk/index.d.ts +20 -0
  54. package/dist/risk/index.js +20 -0
  55. package/dist/risk/paths.d.ts +16 -0
  56. package/dist/risk/paths.js +73 -0
  57. package/dist/risk/pipeline.d.ts +47 -0
  58. package/dist/risk/pipeline.js +121 -0
  59. package/dist/risk/priors.d.ts +18 -0
  60. package/dist/risk/priors.js +86 -0
  61. package/dist/risk/resources.d.ts +56 -0
  62. package/dist/risk/resources.js +374 -0
  63. package/dist/risk/score.d.ts +95 -0
  64. package/dist/risk/score.js +552 -0
  65. package/dist/risk/symbols.d.ts +35 -0
  66. package/dist/risk/symbols.js +348 -0
  67. package/dist/risk/text.d.ts +43 -0
  68. package/dist/risk/text.js +277 -0
  69. package/dist/risk/validate.d.ts +19 -0
  70. package/dist/risk/validate.js +154 -0
  71. package/dist/scratch-worktree.d.ts +55 -0
  72. package/dist/scratch-worktree.js +160 -0
  73. package/dist/user-scope.d.ts +74 -3
  74. package/dist/user-scope.js +270 -9
  75. package/dist/wire.d.ts +52 -3
  76. package/dist/wire.js +2 -2
  77. package/package.json +1 -1
@@ -0,0 +1,154 @@
1
+ import { PROMISE_ID } from "../behavior-map-schema.js";
2
+ import { BANDS, CHANGE_EXTRACTION_SCHEMA_VERSION, DELTA_KINDS, FILE_STATUSES, INTENT_SOURCES, LANGUAGES, RESOURCE_CHANGES, RESOURCE_KINDS, RISK_REPORT_SCHEMA_VERSION, SIGNALS, SYMBOL_CHANGES, SYMBOL_EXTRACTORS, SYMBOL_KINDS, } from "./contract.js";
3
+ const string = (max = 20_000) => (value, at, errors) => {
4
+ if (typeof value === "string" && value.length <= max)
5
+ return true;
6
+ errors.push(`${at} must be a string of at most ${max} characters`);
7
+ return false;
8
+ };
9
+ const pattern = (regex, description) => (value, at, errors) => {
10
+ if (typeof value === "string" && regex.test(value))
11
+ return true;
12
+ errors.push(`${at} must be ${description}`);
13
+ return false;
14
+ };
15
+ const oneOf = (values) => (value, at, errors) => {
16
+ if (typeof value === "string" && values.includes(value))
17
+ return true;
18
+ errors.push(`${at} must be one of ${values.join(", ")}`);
19
+ return false;
20
+ };
21
+ const number = (min, max, integer = false) => (value, at, errors) => {
22
+ if (typeof value === "number" &&
23
+ Number.isFinite(value) &&
24
+ value >= min &&
25
+ value <= max &&
26
+ (!integer || Number.isInteger(value)))
27
+ return true;
28
+ errors.push(`${at} must be ${integer ? "an integer" : "a number"} from ${min} to ${max}`);
29
+ return false;
30
+ };
31
+ const nullable = (check) => (value, at, errors) => value === null || check(value, at, errors);
32
+ const boolean = (value, at, errors) => {
33
+ if (typeof value === "boolean")
34
+ return true;
35
+ errors.push(`${at} must be true or false`);
36
+ return false;
37
+ };
38
+ const listOf = (item) => (value, at, errors) => {
39
+ if (!Array.isArray(value)) {
40
+ errors.push(`${at} must be a list`);
41
+ return false;
42
+ }
43
+ let ok = true;
44
+ value.forEach((entry, index) => {
45
+ if (!item(entry, `${at}[${index}]`, errors))
46
+ ok = false;
47
+ });
48
+ return ok;
49
+ };
50
+ const record = (fields, optional = []) => (value, at, errors) => {
51
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
52
+ errors.push(`${at} must be an object`);
53
+ return false;
54
+ }
55
+ const input = value;
56
+ const unknown = Object.keys(input).filter((key) => !(key in fields));
57
+ if (unknown.length > 0) {
58
+ errors.push(`${at} has fields the contract does not define: ${unknown.join(", ")}`);
59
+ return false;
60
+ }
61
+ let ok = true;
62
+ for (const [key, check] of Object.entries(fields)) {
63
+ if (!(key in input) || input[key] === undefined) {
64
+ if (optional.includes(key))
65
+ continue;
66
+ errors.push(`${at}.${key} is missing`);
67
+ ok = false;
68
+ continue;
69
+ }
70
+ if (!check(input[key], `${at}.${key}`, errors))
71
+ ok = false;
72
+ }
73
+ return ok;
74
+ };
75
+ const SHA = pattern(/^[0-9a-f]{40}$/, "a 40-character commit sha");
76
+ const promiseId = pattern(PROMISE_ID, "a promise id");
77
+ const count = number(0, Number.MAX_SAFE_INTEGER, true);
78
+ const changedFile = record({
79
+ path: string(1024),
80
+ status: oneOf(FILE_STATUSES),
81
+ oldPath: nullable(string(1024)),
82
+ language: oneOf(LANGUAGES),
83
+ isTest: boolean,
84
+ hunks: listOf(record({ oldStart: count, oldLines: count, newStart: count, newLines: count })),
85
+ symbolsChanged: listOf(record({ name: string(512), kind: oneOf(SYMBOL_KINDS), change: oneOf(SYMBOL_CHANGES) })),
86
+ identifiersAdded: listOf(string(512)),
87
+ identifiersRemoved: listOf(string(512)),
88
+ resources: listOf(record({ kind: oneOf(RESOURCE_KINDS), name: string(600), change: oneOf(RESOURCE_CHANGES) })),
89
+ guardDeletions: listOf(record({ line: count, text: string(2000) })),
90
+ constantChanges: listOf(record({ name: string(512), from: string(2000), to: string(2000) })),
91
+ symbolExtractor: oneOf(SYMBOL_EXTRACTORS),
92
+ }, ["symbolExtractor"]);
93
+ const extraction = record({
94
+ schemaVersion: oneOf([CHANGE_EXTRACTION_SCHEMA_VERSION]),
95
+ base: SHA,
96
+ head: SHA,
97
+ files: listOf(changedFile),
98
+ deltas: listOf(record({ text: string(4000), files: listOf(string(1024)), kind: oneOf(DELTA_KINDS) })),
99
+ intent: record({ summary: string(), source: oneOf(INTENT_SOURCES) }),
100
+ }, ["deltas", "intent"]);
101
+ const unit = number(0, 1);
102
+ const report = record({
103
+ schemaVersion: oneOf([RISK_REPORT_SCHEMA_VERSION]),
104
+ base: SHA,
105
+ head: SHA,
106
+ promises: listOf(record({
107
+ promiseId,
108
+ title: string(),
109
+ score: unit,
110
+ band: oneOf(BANDS),
111
+ reasons: listOf(record({
112
+ signal: oneOf(SIGNALS),
113
+ weight: unit,
114
+ evidence: record({
115
+ file: string(),
116
+ symbol: string(),
117
+ resource: string(),
118
+ delta: string(),
119
+ caseId: string(),
120
+ detail: string(),
121
+ }, ["caseId", "detail"]),
122
+ })),
123
+ features: record({
124
+ symbol_hit: unit,
125
+ graph_forward_distance: nullable(count),
126
+ graph_reverse_distance: nullable(count),
127
+ resource_overlap: unit,
128
+ linked_test_changed: unit,
129
+ sink_reach: unit,
130
+ delta_similarity: unit,
131
+ intent_clash: unit,
132
+ prior_break: count,
133
+ churn: unit,
134
+ }),
135
+ }, ["title"])),
136
+ baselines: record({
137
+ scope_only: listOf(promiseId),
138
+ import_closure: listOf(promiseId),
139
+ all: listOf(promiseId),
140
+ random: listOf(promiseId),
141
+ }),
142
+ });
143
+ function parse(check, value, name) {
144
+ const errors = [];
145
+ return check(value, name, errors) ? { ok: true, value: value } : { ok: false, errors };
146
+ }
147
+ /** A change extraction, checked against the contract. */
148
+ export function parseChangeExtraction(value) {
149
+ return parse(extraction, value, "extraction");
150
+ }
151
+ /** A risk report, checked against the contract. */
152
+ export function parseRiskReport(value) {
153
+ return parse(report, value, "report");
154
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Disposable checkouts of one commit, for an agent to read and run things in.
3
+ *
4
+ * A map builder and a judge each work in a detached `git worktree` of the
5
+ * commit they are about, made under the system temp directory and removed
6
+ * afterwards. Whatever the agent writes there, a scratch test, a build output,
7
+ * an edit it should not have made, goes with the directory, and the person's
8
+ * own checkout is never the one being changed. Nothing here fetches: the
9
+ * commit has to be in the repository already, which it is for a change being
10
+ * pushed.
11
+ */
12
+ export type ScratchWorktree = Readonly<{
13
+ path: string;
14
+ sha: string;
15
+ remove: () => Promise<void>;
16
+ }>;
17
+ export declare function resolveCommit(root: string, ref: string): Promise<string | undefined>;
18
+ export declare function addScratchWorktree(root: string, ref: string, label: string, scratch?: string): Promise<{
19
+ ok: true;
20
+ worktree: ScratchWorktree;
21
+ } | {
22
+ ok: false;
23
+ reason: string;
24
+ }>;
25
+ /** Synchronous, for a process that is being stopped: remove what this process made. */
26
+ export declare function removeLiveWorktreesNow(): void;
27
+ /**
28
+ * The installed dependencies a fresh worktree lacks, linked from the checkout.
29
+ *
30
+ * A new worktree holds only tracked files, so it has no `node_modules` and a
31
+ * test run in it fails before it reaches the behavior. Each `node_modules`
32
+ * beside a tracked `package.json` in the person's checkout, and a `.venv` at
33
+ * the top, is linked into the same place in the worktree. Linked, not copied:
34
+ * the link is removed with the worktree and the dependencies are never touched.
35
+ */
36
+ export declare function shareDependencies(root: string, worktree: string): Promise<number>;
37
+ export type CarriedFile = Readonly<{
38
+ path: string;
39
+ sha256: string;
40
+ }>;
41
+ /**
42
+ * Files an agent added in one worktree, so a command it wrote can run in another.
43
+ *
44
+ * A judge that proves a break usually writes a new test. Its reproduction is
45
+ * then run in a clean checkout of the base commit, where that test does not
46
+ * exist, so the new files travel with it: untracked, not ignored, regular, and
47
+ * small. Edits to tracked files do not travel, and the judge brief says to put
48
+ * a reproduction in a new file for that reason.
49
+ */
50
+ export declare function newFiles(worktree: string): Promise<CarriedFile[]>;
51
+ /**
52
+ * Copy files from one directory into another at the same relative paths, and
53
+ * return the ones copied. A path that would land outside `to` is skipped.
54
+ */
55
+ export declare function carryFiles(from: string, to: string, files: readonly CarriedFile[]): CarriedFile[];
@@ -0,0 +1,160 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { runCommand } from "./gh.js";
7
+ /** Every worktree this process made and has not removed, for a signal handler to clear. */
8
+ const live = new Map();
9
+ export async function resolveCommit(root, ref) {
10
+ const answer = await runCommand("git", ["-C", root, "rev-parse", "--verify", `${ref}^{commit}`]);
11
+ const sha = answer.stdout.trim();
12
+ return answer.ok && /^[0-9a-f]{40}$/.test(sha) ? sha : undefined;
13
+ }
14
+ export async function addScratchWorktree(root, ref, label, scratch) {
15
+ const sha = await resolveCommit(root, ref);
16
+ if (sha === undefined)
17
+ return { ok: false, reason: `${ref} is not a commit in this repository` };
18
+ const parent = realpathSync(mkdtempSync(join(scratch ?? tmpdir(), `balladeer-${label.replace(/[^A-Za-z0-9_-]/g, "-")}-`)));
19
+ const path = join(parent, "tree");
20
+ const added = await runCommand("git", ["-C", root, "worktree", "add", "--detach", "--quiet", path, sha], { timeoutMs: 120_000 });
21
+ if (!added.ok) {
22
+ rmSync(parent, { recursive: true, force: true });
23
+ return {
24
+ ok: false,
25
+ reason: `a scratch checkout of ${sha.slice(0, 12)} could not be made: ${added.stderr}`,
26
+ };
27
+ }
28
+ live.set(path, root);
29
+ return {
30
+ ok: true,
31
+ worktree: {
32
+ path,
33
+ sha,
34
+ remove: async () => {
35
+ await runCommand("git", ["-C", root, "worktree", "remove", "--force", path], {
36
+ timeoutMs: 60_000,
37
+ });
38
+ rmSync(parent, { recursive: true, force: true });
39
+ await runCommand("git", ["-C", root, "worktree", "prune"]);
40
+ live.delete(path);
41
+ },
42
+ },
43
+ };
44
+ }
45
+ /** Synchronous, for a process that is being stopped: remove what this process made. */
46
+ export function removeLiveWorktreesNow() {
47
+ for (const [path, root] of live) {
48
+ try {
49
+ execFileSync("git", ["-C", root, "worktree", "remove", "--force", path], {
50
+ stdio: "ignore",
51
+ timeout: 20_000,
52
+ });
53
+ }
54
+ catch {
55
+ /* The directory removal below still clears the files. */
56
+ }
57
+ rmSync(dirname(path), { recursive: true, force: true });
58
+ live.delete(path);
59
+ }
60
+ }
61
+ /**
62
+ * The installed dependencies a fresh worktree lacks, linked from the checkout.
63
+ *
64
+ * A new worktree holds only tracked files, so it has no `node_modules` and a
65
+ * test run in it fails before it reaches the behavior. Each `node_modules`
66
+ * beside a tracked `package.json` in the person's checkout, and a `.venv` at
67
+ * the top, is linked into the same place in the worktree. Linked, not copied:
68
+ * the link is removed with the worktree and the dependencies are never touched.
69
+ */
70
+ export async function shareDependencies(root, worktree) {
71
+ const listed = await runCommand("git", [
72
+ "-C",
73
+ root,
74
+ "ls-files",
75
+ "--",
76
+ "package.json",
77
+ "*/package.json",
78
+ "*/*/package.json",
79
+ ]);
80
+ const directories = new Set(["."]);
81
+ if (listed.ok)
82
+ for (const line of listed.stdout.split("\n"))
83
+ if (line.trim())
84
+ directories.add(dirname(line.trim()));
85
+ let linked = 0;
86
+ const link = (relativePath) => {
87
+ const source = join(root, relativePath);
88
+ const target = join(worktree, relativePath);
89
+ if (!existsSync(source) || existsSync(target) || !existsSync(dirname(target)))
90
+ return;
91
+ try {
92
+ symlinkSync(source, target, "dir");
93
+ linked += 1;
94
+ }
95
+ catch {
96
+ /* A test that needs it will say so. */
97
+ }
98
+ };
99
+ for (const directory of directories)
100
+ link(join(directory, "node_modules"));
101
+ link(".venv");
102
+ return linked;
103
+ }
104
+ /**
105
+ * Files an agent added in one worktree, so a command it wrote can run in another.
106
+ *
107
+ * A judge that proves a break usually writes a new test. Its reproduction is
108
+ * then run in a clean checkout of the base commit, where that test does not
109
+ * exist, so the new files travel with it: untracked, not ignored, regular, and
110
+ * small. Edits to tracked files do not travel, and the judge brief says to put
111
+ * a reproduction in a new file for that reason.
112
+ */
113
+ export async function newFiles(worktree) {
114
+ const listed = await runCommand("git", [
115
+ "-C",
116
+ worktree,
117
+ "ls-files",
118
+ "--others",
119
+ "--exclude-standard",
120
+ "-z",
121
+ ]);
122
+ if (!listed.ok)
123
+ return [];
124
+ const files = [];
125
+ for (const path of listed.stdout.split("\0")) {
126
+ if (!path || files.length >= 200)
127
+ continue;
128
+ const absolute = join(worktree, path);
129
+ try {
130
+ const stat = lstatSync(absolute);
131
+ if (!stat.isFile() || stat.size > 1024 * 1024)
132
+ continue;
133
+ files.push({
134
+ path,
135
+ sha256: createHash("sha256").update(readFileSync(absolute)).digest("hex"),
136
+ });
137
+ }
138
+ catch {
139
+ /* Gone between the listing and the read. */
140
+ }
141
+ }
142
+ return files.sort((a, b) => a.path.localeCompare(b.path));
143
+ }
144
+ /**
145
+ * Copy files from one directory into another at the same relative paths, and
146
+ * return the ones copied. A path that would land outside `to` is skipped.
147
+ */
148
+ export function carryFiles(from, to, files) {
149
+ const carried = [];
150
+ for (const file of files) {
151
+ const target = resolve(to, file.path);
152
+ const inside = relative(to, target);
153
+ if (inside.startsWith("..") || inside === "" || isAbsolute(inside))
154
+ continue;
155
+ mkdirSync(dirname(target), { recursive: true });
156
+ copyFileSync(join(from, file.path), target);
157
+ carried.push(file);
158
+ }
159
+ return carried;
160
+ }
@@ -15,6 +15,59 @@
15
15
  * shape, never by trust in the name.
16
16
  */
17
17
  export declare const USER_SCOPE_OWNER = "Balladeer current guidance (user scope, loader 1)";
18
+ /**
19
+ * The push check at user scope. Robert, 30 September 2026: setup turns it on
20
+ * for everyone, where it writes Balladeer's other hooks, report-only unless a
21
+ * repository says block, so it runs in every connected repository and every
22
+ * worktree with no step per repository. Two entries on matcher `Bash`, the
23
+ * same pair `balladeer judge --install-hook` writes into a checkout: after the
24
+ * command, where report mode judges, and before it, where block mode does.
25
+ *
26
+ * Its own mark, apart from the guidance hooks' and from the project entry's
27
+ * `Balladeer judge`, so each is recognised, replaced and removed only by
28
+ * itself. The relay note and the push check both sit under PostToolUse.
29
+ */
30
+ export declare const USER_SCOPE_PUSH_CHECK_OWNER = "Balladeer push check (user scope)";
31
+ /**
32
+ * The push check runs on every shell command an agent runs, twice, so it is
33
+ * written only where the command starts in milliseconds: the installed command
34
+ * by path, or this checkout's own entry file. The `npx` form costs npm's own
35
+ * start and a registry lookup each time, which would slow every command the
36
+ * agent runs, so a laptop on it gets no push check until the command installs.
37
+ */
38
+ export declare function pushCheckInstallable(published: boolean, installed?: string): boolean;
39
+ /** Where the push check stands on this laptop after setup wrote the user scope. */
40
+ export type PushCheckState = "on" | "off" | "unavailable";
41
+ export declare function pushCheckState(environment: NodeJS.ProcessEnv, published: boolean, installed?: string): PushCheckState;
42
+ /** Setup's one line about the push check. */
43
+ export declare function pushCheckSetupLine(state: PushCheckState): string;
44
+ /**
45
+ * What the push check's entry asks grep before Node starts: whether the
46
+ * payload's `"command"`, read as JSON (a string, or a list of words as Codex
47
+ * can send it), so much as mentions `push`, `create` or `merge`. Every line
48
+ * the judge reads as a push (`git push`, `gh pr create`, `gh pr merge`) spells
49
+ * one of them out, so none is dropped, and a folder or an output that says
50
+ * "push" wakes nothing. A word split by quotes (`pu''sh`) is the one spelling
51
+ * the judge reads as a push and this does not.
52
+ */
53
+ export declare const PUSH_CHECK_FILTER = "\"command\" *: *(\\[( *\"([^\"\\\\]|\\\\.)*\" *,)* *)?\"([^\"\\\\]|\\\\.)*(push|create|merge)";
54
+ /**
55
+ * The push check's entry as the host runs it. It runs on every shell command
56
+ * an agent runs, before and after, so a POSIX shell looks first and starts
57
+ * Node only for a command that could push: a few milliseconds on anything
58
+ * else instead of Node's start (measured 30 September 2026: 4 to 6 ms against
59
+ * 45 to 90). Both hosts run a hook's command through a shell (Codex checked
60
+ * natively the same day). Only grep's plain "no match" ends it there: a grep
61
+ * that cannot run lets the command through, so a laptop without one pays
62
+ * Node's start and loses nothing. `awaiting` is a file that exists while
63
+ * Codex has yet to run the entry; every command goes through while it does,
64
+ * so that first run is noted without waiting for a push. Windows gets the
65
+ * plain command: there is no /bin/sh to count on.
66
+ */
67
+ export declare function pushCheckCommand(host: UserScopeHost, published: boolean, installed?: string, options?: Readonly<{
68
+ awaiting?: string;
69
+ platform?: NodeJS.Platform;
70
+ }>): string;
18
71
  /** Every tool of the server named `balladeer`, in Claude Code's permission rule form. */
19
72
  export declare const USER_SCOPE_ALLOW_RULE = "mcp__balladeer__*";
20
73
  export type UserScopeHost = "claude" | "codex";
@@ -52,12 +105,28 @@ export declare function isOurUserEntry(entry: unknown): boolean;
52
105
  */
53
106
  export declare function isInstalledCommandPath(command: string): boolean;
54
107
  /** `~/.claude/settings.json` carries user-scope hooks. */
55
- export declare function mergeClaudeUserHooks(home: string, published: boolean, installed?: string): UserScopeWrite;
56
- /** What a person has to do once in Codex, which no command can do for them. */
57
- export declare const CODEX_HOOK_APPROVAL = "Codex: type /hooks in a Codex session and trust Balladeer's three hooks. Codex skips a hook until you have trusted it, so until then Codex has Balladeer's tools and none of its session-start guidance or updates.";
108
+ export declare function mergeClaudeUserHooks(home: string, published: boolean, installed?: string, platform?: NodeJS.Platform): UserScopeWrite;
109
+ /**
110
+ * What a person has to do once in Codex, which no command can do for them,
111
+ * counting the hooks this run gave Codex: the guidance hooks, and the push
112
+ * check's two where it was written.
113
+ */
114
+ export declare function codexHookApproval(pushCheck: boolean): string;
58
115
  /** `~/.codex/config.toml` carries both the server and the hooks for Codex, in
59
116
  * one fenced block this command owns end to end. */
60
117
  export declare function mergeCodexUserConfig(home: string, published: boolean, installed?: string): UserScopeWrite;
118
+ /**
119
+ * The push check's two entries in `~/.codex/config.toml`, in a fenced block of
120
+ * their own after Balladeer's other one, so the guidance block and this one are
121
+ * each written, replaced and removed only by themselves, and Codex's trust in
122
+ * the one says nothing about the other. TOML lets a later block add to
123
+ * `[[hooks.PostToolUse]]`, and Codex 0.144.5 reads the file that results
124
+ * (checked 30 September 2026 with `codex mcp list` on a file of this shape).
125
+ */
126
+ export declare function mergeCodexPushCheck(home: string, published: boolean, installed?: string, options?: Readonly<{
127
+ awaiting?: string;
128
+ platform?: NodeJS.Platform;
129
+ }>): UserScopeWrite;
61
130
  /** A copy of this command running out of a source checkout registers the
62
131
  * checkout; any installed or npx copy registers the published package. */
63
132
  export declare function runningFromCheckout(entry?: string): boolean;
@@ -72,4 +141,6 @@ export declare function installUserScope(options: {
72
141
  home?: string;
73
142
  /** The durable command's absolute path, when one is installed. */
74
143
  installed?: string;
144
+ /** The clock the Codex trust marks are stamped with. */
145
+ now?: () => number;
75
146
  }): UserScopeWrite[];