balladeer 1.0.14 → 1.0.16

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 +5 -0
  5. package/dist/cli.js +77 -2
  6. package/dist/commands/guidance.d.ts +1 -1
  7. package/dist/commands/guidance.js +6 -0
  8. package/dist/commands/judge.d.ts +271 -0
  9. package/dist/commands/judge.js +1170 -0
  10. package/dist/commands/map.d.ts +120 -0
  11. package/dist/commands/map.js +601 -0
  12. package/dist/commands/mcp.d.ts +6 -0
  13. package/dist/commands/mcp.js +53 -0
  14. package/dist/commands/offers.d.ts +265 -0
  15. package/dist/commands/offers.js +801 -0
  16. package/dist/commands/risk.d.ts +29 -0
  17. package/dist/commands/risk.js +133 -0
  18. package/dist/copy.d.ts +24 -1
  19. package/dist/copy.js +91 -0
  20. package/dist/guidance-hook.mjs +230 -74
  21. package/dist/guidance.d.ts +2 -0
  22. package/dist/guidance.js +7 -1
  23. package/dist/headless-agent.d.ts +166 -0
  24. package/dist/headless-agent.js +416 -0
  25. package/dist/hook-trust.d.ts +31 -0
  26. package/dist/hook-trust.js +63 -0
  27. package/dist/judge-brief.d.ts +36 -0
  28. package/dist/judge-brief.js +89 -0
  29. package/dist/judge-hook.d.ts +84 -0
  30. package/dist/judge-hook.js +420 -0
  31. package/dist/map-brief.d.ts +16 -0
  32. package/dist/map-brief.js +41 -0
  33. package/dist/offer-brief.d.ts +95 -0
  34. package/dist/offer-brief.js +217 -0
  35. package/dist/owned-process.d.ts +65 -0
  36. package/dist/owned-process.js +146 -0
  37. package/dist/promise-meaning.d.ts +126 -0
  38. package/dist/promise-meaning.js +292 -0
  39. package/dist/risk/contract.d.ts +145 -0
  40. package/dist/risk/contract.js +74 -0
  41. package/dist/risk/describe.d.ts +7 -0
  42. package/dist/risk/describe.js +45 -0
  43. package/dist/risk/diff.d.ts +26 -0
  44. package/dist/risk/diff.js +174 -0
  45. package/dist/risk/extract.d.ts +43 -0
  46. package/dist/risk/extract.js +336 -0
  47. package/dist/risk/git.d.ts +30 -0
  48. package/dist/risk/git.js +118 -0
  49. package/dist/risk/import-graph.d.ts +41 -0
  50. package/dist/risk/import-graph.js +487 -0
  51. package/dist/risk/index.d.ts +20 -0
  52. package/dist/risk/index.js +20 -0
  53. package/dist/risk/paths.d.ts +16 -0
  54. package/dist/risk/paths.js +73 -0
  55. package/dist/risk/pipeline.d.ts +47 -0
  56. package/dist/risk/pipeline.js +121 -0
  57. package/dist/risk/priors.d.ts +18 -0
  58. package/dist/risk/priors.js +86 -0
  59. package/dist/risk/resources.d.ts +56 -0
  60. package/dist/risk/resources.js +374 -0
  61. package/dist/risk/score.d.ts +85 -0
  62. package/dist/risk/score.js +543 -0
  63. package/dist/risk/symbols.d.ts +35 -0
  64. package/dist/risk/symbols.js +348 -0
  65. package/dist/risk/text.d.ts +43 -0
  66. package/dist/risk/text.js +277 -0
  67. package/dist/risk/validate.d.ts +19 -0
  68. package/dist/risk/validate.js +154 -0
  69. package/dist/scratch-worktree.d.ts +51 -0
  70. package/dist/scratch-worktree.js +153 -0
  71. package/dist/self-update.d.ts +29 -2
  72. package/dist/self-update.js +96 -11
  73. package/dist/user-scope.d.ts +2 -0
  74. package/dist/user-scope.js +26 -2
  75. package/dist/wire.d.ts +55 -2
  76. package/dist/wire.js +5 -1
  77. package/package.json +1 -1
package/README.md CHANGED
@@ -206,6 +206,8 @@ your own branch protection.
206
206
  | `npx -y balladeer@latest discover` | File a whole catalog, up to ten promises from one file, behind one review link |
207
207
  | `npx -y balladeer@latest touch-map` | Record which files each promise's verifier runs, on this machine only |
208
208
  | `npx -y balladeer@latest affected` | Say which promises the files you name touch, out of that record |
209
+ | `npx -y balladeer@latest risk` | Rank the promises a change could put at risk, on this machine only |
210
+ | `npx -y balladeer@latest offers` | Read a change for up to three rules no promise records, and record nothing |
209
211
  | `npx -y balladeer@latest session` | Print the id for this piece of work, and the line to write into the commit |
210
212
  | `npx -y balladeer@latest mcp` | Forward one MCP session over stdio using this repository's connection |
211
213
  | `npx -y balladeer@latest explain` | What Balladeer can and cannot see, where to watch it, and what leaving costs |
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `balladeer-behavior-map/v1`, as `docs/agentic-testing/contracts.md` spells it.
3
+ *
4
+ * Written without a schema library on purpose. The published command carries
5
+ * exactly one reviewed runtime dependency (`tests/packaging/cli-package.test.ts`
6
+ * holds it there), so the shape is checked by hand here, with the field names
7
+ * copied from the contract. It is the one reader of maps: `balladeer map` writes
8
+ * through it and the risk ranking (`risk/`) reads through it, so a field added
9
+ * here reaches both, and `tests/risk/contract.test.ts` round-trips a full map.
10
+ */
11
+ export declare const BEHAVIOR_MAP_SCHEMA_VERSION = "balladeer-behavior-map/v1";
12
+ export declare const BEHAVIOR_MAP_DIRECTORY = ".continuity/behavior-maps";
13
+ export declare const ENTRY_POINT_KINDS: readonly ["route", "command", "ui", "job", "event", "function"];
14
+ export declare const SYMBOL_KINDS: readonly ["function", "class", "method", "const", "type", "module"];
15
+ export declare const SYMBOL_ROLES: readonly ["core", "supporting"];
16
+ export declare const RESOURCE_KINDS: readonly ["table", "column", "route", "flag", "env", "queue", "event", "outbound", "file", "config"];
17
+ export declare const RESOURCE_ACCESS: readonly ["read", "write", "both", "call"];
18
+ /** Positions in the agreed meaning: `passing-1`, `failing-2`, `refactor-1`. */
19
+ export declare const CASE_ID: RegExp;
20
+ export type BehaviorMapEntryPoint = {
21
+ file: string;
22
+ symbol: string;
23
+ kind: (typeof ENTRY_POINT_KINDS)[number];
24
+ note: string;
25
+ };
26
+ export type BehaviorMapSymbol = {
27
+ file: string;
28
+ name: string;
29
+ kind: (typeof SYMBOL_KINDS)[number];
30
+ role: (typeof SYMBOL_ROLES)[number];
31
+ };
32
+ export type BehaviorMapResource = {
33
+ kind: (typeof RESOURCE_KINDS)[number];
34
+ name: string;
35
+ access: (typeof RESOURCE_ACCESS)[number];
36
+ };
37
+ export type BehaviorMapLinkedTest = {
38
+ file: string;
39
+ name: string;
40
+ cases: string[];
41
+ };
42
+ export type BehaviorMapSink = {
43
+ caseId: string;
44
+ effect: string;
45
+ resources: string[];
46
+ /** `file#symbol`. */
47
+ symbols: string[];
48
+ };
49
+ /**
50
+ * The part of the agreed meaning a scorer needs without asking the server:
51
+ * the contract addendum of 28 September. `surfaces` is the promise's own scope
52
+ * surfaces, which the scope-only baseline reads.
53
+ */
54
+ export type BehaviorMapMeaning = {
55
+ title: string;
56
+ observableOutcome: string;
57
+ failingExamples: {
58
+ id: string;
59
+ text: string;
60
+ }[];
61
+ nonGoals: string[];
62
+ surfaces?: string[];
63
+ };
64
+ export type BehaviorMap = {
65
+ schemaVersion: typeof BEHAVIOR_MAP_SCHEMA_VERSION;
66
+ promiseId: string;
67
+ semanticDigest: string;
68
+ mappedAt: {
69
+ sha: string;
70
+ at: string;
71
+ client: "claude" | "codex";
72
+ model: string;
73
+ };
74
+ entryPoints: BehaviorMapEntryPoint[];
75
+ symbols: BehaviorMapSymbol[];
76
+ resources: BehaviorMapResource[];
77
+ linkedTests: BehaviorMapLinkedTest[];
78
+ sinks: BehaviorMapSink[];
79
+ confidence: number;
80
+ notes: string;
81
+ meaning?: BehaviorMapMeaning;
82
+ };
83
+ /** Ceilings, so a map stays a small file a person can read in a review. */
84
+ export declare const MAP_LIMITS: {
85
+ readonly entryPoints: 20;
86
+ readonly symbols: 60;
87
+ readonly resources: 40;
88
+ readonly linkedTests: 20;
89
+ readonly sinks: 20;
90
+ readonly text: 2000;
91
+ readonly name: 400;
92
+ };
93
+ export type ParseResult<T> = {
94
+ ok: true;
95
+ value: T;
96
+ } | {
97
+ ok: false;
98
+ errors: string[];
99
+ };
100
+ /** Promise ids: `prom_` ids from Balladeer, and the evaluation harness's own ids. */
101
+ export declare const PROMISE_ID: RegExp;
102
+ /** A whole map, checked against the contract. Unknown fields are refused. */
103
+ export declare function parseBehaviorMap(value: unknown): ParseResult<BehaviorMap>;
104
+ /** Every file a map names: the derived `files` field, which is never stored. */
105
+ export declare function mapFiles(map: BehaviorMap): string[];
106
+ /** The map as it is written to disk: two-space JSON and a final newline. */
107
+ export declare function serializeBehaviorMap(map: BehaviorMap): string;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * `balladeer-behavior-map/v1`, as `docs/agentic-testing/contracts.md` spells it.
3
+ *
4
+ * Written without a schema library on purpose. The published command carries
5
+ * exactly one reviewed runtime dependency (`tests/packaging/cli-package.test.ts`
6
+ * holds it there), so the shape is checked by hand here, with the field names
7
+ * copied from the contract. It is the one reader of maps: `balladeer map` writes
8
+ * through it and the risk ranking (`risk/`) reads through it, so a field added
9
+ * here reaches both, and `tests/risk/contract.test.ts` round-trips a full map.
10
+ */
11
+ export const BEHAVIOR_MAP_SCHEMA_VERSION = "balladeer-behavior-map/v1";
12
+ export const BEHAVIOR_MAP_DIRECTORY = ".continuity/behavior-maps";
13
+ export const ENTRY_POINT_KINDS = ["route", "command", "ui", "job", "event", "function"];
14
+ export const SYMBOL_KINDS = ["function", "class", "method", "const", "type", "module"];
15
+ export const SYMBOL_ROLES = ["core", "supporting"];
16
+ export const RESOURCE_KINDS = [
17
+ "table",
18
+ "column",
19
+ "route",
20
+ "flag",
21
+ "env",
22
+ "queue",
23
+ "event",
24
+ "outbound",
25
+ "file",
26
+ "config",
27
+ ];
28
+ export const RESOURCE_ACCESS = ["read", "write", "both", "call"];
29
+ /** Positions in the agreed meaning: `passing-1`, `failing-2`, `refactor-1`. */
30
+ export const CASE_ID = /^(passing|failing|refactor)-[1-9][0-9]{0,2}$/;
31
+ /** Ceilings, so a map stays a small file a person can read in a review. */
32
+ export const MAP_LIMITS = {
33
+ entryPoints: 20,
34
+ symbols: 60,
35
+ resources: 40,
36
+ linkedTests: 20,
37
+ sinks: 20,
38
+ text: 2000,
39
+ name: 400,
40
+ };
41
+ const text = (max, allowEmpty = true) => (value, at, errors) => {
42
+ if (typeof value !== "string") {
43
+ errors.push(`${at} must be a string`);
44
+ return undefined;
45
+ }
46
+ if (!allowEmpty && value.trim() === "") {
47
+ errors.push(`${at} must not be empty`);
48
+ return undefined;
49
+ }
50
+ if (value.length > max) {
51
+ errors.push(`${at} is longer than ${max} characters`);
52
+ return undefined;
53
+ }
54
+ return value;
55
+ };
56
+ const oneOf = (values) => (value, at, errors) => {
57
+ if (typeof value === "string" && values.includes(value))
58
+ return value;
59
+ errors.push(`${at} must be one of ${values.join(", ")}`);
60
+ return undefined;
61
+ };
62
+ /** Repository-relative, forward slashes, inside the repository. */
63
+ const repositoryPath = (value, at, errors) => {
64
+ const path = text(MAP_LIMITS.name, false)(value, at, errors);
65
+ if (path === undefined)
66
+ return undefined;
67
+ if (path.startsWith("/") ||
68
+ path.includes("\\") ||
69
+ /^[A-Za-z]:/.test(path) ||
70
+ path.split("/").some((part) => part === "..")) {
71
+ errors.push(`${at} must be a repository-relative path with forward slashes`);
72
+ return undefined;
73
+ }
74
+ return path;
75
+ };
76
+ const listOf = (item, max) => (value, at, errors) => {
77
+ if (!Array.isArray(value)) {
78
+ errors.push(`${at} must be a list`);
79
+ return undefined;
80
+ }
81
+ if (value.length > max) {
82
+ errors.push(`${at} has more than ${max} entries`);
83
+ return undefined;
84
+ }
85
+ const out = [];
86
+ let failed = false;
87
+ value.forEach((entry, index) => {
88
+ const checked = item(entry, `${at}[${index}]`, errors);
89
+ if (checked === undefined)
90
+ failed = true;
91
+ else
92
+ out.push(checked);
93
+ });
94
+ return failed ? undefined : out;
95
+ };
96
+ const record = (fields, optional = []) => (value, at, errors) => {
97
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
98
+ errors.push(`${at} must be an object`);
99
+ return undefined;
100
+ }
101
+ const input = value;
102
+ const unknown = Object.keys(input).filter((key) => !(key in fields));
103
+ if (unknown.length > 0) {
104
+ errors.push(`${at} has fields the contract does not define: ${unknown.join(", ")}`);
105
+ return undefined;
106
+ }
107
+ const out = {};
108
+ let failed = false;
109
+ for (const key of Object.keys(fields)) {
110
+ if (!(key in input) || input[key] === undefined) {
111
+ if (optional.includes(key))
112
+ continue;
113
+ errors.push(`${at}.${key} is missing`);
114
+ failed = true;
115
+ continue;
116
+ }
117
+ const checked = fields[key](input[key], `${at}.${key}`, errors);
118
+ if (checked === undefined)
119
+ failed = true;
120
+ else
121
+ out[key] = checked;
122
+ }
123
+ return failed ? undefined : out;
124
+ };
125
+ const caseId = (value, at, errors) => {
126
+ if (typeof value === "string" && CASE_ID.test(value))
127
+ return value;
128
+ errors.push(`${at} must be a case id such as passing-1 or failing-2`);
129
+ return undefined;
130
+ };
131
+ const fileSymbol = (value, at, errors) => {
132
+ const reference = text(MAP_LIMITS.name, false)(value, at, errors);
133
+ if (reference === undefined)
134
+ return undefined;
135
+ const hash = reference.indexOf("#");
136
+ if (hash <= 0 || hash === reference.length - 1) {
137
+ errors.push(`${at} must be file#symbol`);
138
+ return undefined;
139
+ }
140
+ return repositoryPath(reference.slice(0, hash), at, errors) === undefined ? undefined : reference;
141
+ };
142
+ const confidence = (value, at, errors) => {
143
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1)
144
+ return value;
145
+ errors.push(`${at} must be a number from 0 to 1`);
146
+ return undefined;
147
+ };
148
+ const SHA = /^[0-9a-f]{40}$/;
149
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
150
+ /** Promise ids: `prom_` ids from Balladeer, and the evaluation harness's own ids. */
151
+ export const PROMISE_ID = /^[A-Za-z0-9_.-]{1,128}$/;
152
+ const pattern = (regex, description) => (value, at, errors) => {
153
+ if (typeof value === "string" && regex.test(value))
154
+ return value;
155
+ errors.push(`${at} must be ${description}`);
156
+ return undefined;
157
+ };
158
+ const isoInstant = (value, at, errors) => {
159
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value)))
160
+ return value;
161
+ errors.push(`${at} must be an ISO timestamp`);
162
+ return undefined;
163
+ };
164
+ const meaningChecker = record({
165
+ title: text(MAP_LIMITS.text, false),
166
+ observableOutcome: text(16_000, false),
167
+ failingExamples: listOf(record({ id: caseId, text: text(8000, false) }), MAP_LIMITS.sinks),
168
+ nonGoals: listOf(text(MAP_LIMITS.text), 40),
169
+ surfaces: listOf(text(MAP_LIMITS.name, false), 40),
170
+ }, ["surfaces"]);
171
+ const mapChecker = record({
172
+ schemaVersion: oneOf([BEHAVIOR_MAP_SCHEMA_VERSION]),
173
+ promiseId: pattern(PROMISE_ID, "a promise id"),
174
+ semanticDigest: pattern(DIGEST, "sha256: followed by 64 hex characters"),
175
+ mappedAt: record({
176
+ sha: pattern(SHA, "a 40-character commit sha"),
177
+ at: isoInstant,
178
+ client: oneOf(["claude", "codex"]),
179
+ model: text(MAP_LIMITS.name),
180
+ }),
181
+ entryPoints: listOf(record({
182
+ file: repositoryPath,
183
+ symbol: text(MAP_LIMITS.name, false),
184
+ kind: oneOf(ENTRY_POINT_KINDS),
185
+ note: text(MAP_LIMITS.text),
186
+ }), MAP_LIMITS.entryPoints),
187
+ symbols: listOf(record({
188
+ file: repositoryPath,
189
+ name: text(MAP_LIMITS.name, false),
190
+ kind: oneOf(SYMBOL_KINDS),
191
+ role: oneOf(SYMBOL_ROLES),
192
+ }), MAP_LIMITS.symbols),
193
+ resources: listOf(record({
194
+ kind: oneOf(RESOURCE_KINDS),
195
+ name: text(MAP_LIMITS.name, false),
196
+ access: oneOf(RESOURCE_ACCESS),
197
+ }), MAP_LIMITS.resources),
198
+ linkedTests: listOf(record({
199
+ file: repositoryPath,
200
+ name: text(MAP_LIMITS.name),
201
+ cases: listOf(caseId, 40),
202
+ }), MAP_LIMITS.linkedTests),
203
+ sinks: listOf(record({
204
+ caseId,
205
+ effect: text(MAP_LIMITS.text, false),
206
+ resources: listOf(text(MAP_LIMITS.name, false), 20),
207
+ symbols: listOf(fileSymbol, 20),
208
+ }), MAP_LIMITS.sinks),
209
+ confidence,
210
+ notes: text(MAP_LIMITS.text),
211
+ meaning: meaningChecker,
212
+ }, ["meaning"]);
213
+ /** A whole map, checked against the contract. Unknown fields are refused. */
214
+ export function parseBehaviorMap(value) {
215
+ const errors = [];
216
+ const map = mapChecker(value, "map", errors);
217
+ return map === undefined ? { ok: false, errors } : { ok: true, value: map };
218
+ }
219
+ /** Every file a map names: the derived `files` field, which is never stored. */
220
+ export function mapFiles(map) {
221
+ return [
222
+ ...new Set([
223
+ ...map.entryPoints.map((entry) => entry.file),
224
+ ...map.symbols.map((entry) => entry.file),
225
+ ...map.linkedTests.map((entry) => entry.file),
226
+ ]),
227
+ ].sort();
228
+ }
229
+ /** The map as it is written to disk: two-space JSON and a final newline. */
230
+ export function serializeBehaviorMap(map) {
231
+ return `${JSON.stringify(map, null, 2)}\n`;
232
+ }
package/dist/cli.d.ts CHANGED
@@ -45,6 +45,11 @@ type Parsed = Readonly<{
45
45
  again: boolean;
46
46
  /** `check-seals --runner`: where the pinned runner is, when it has moved. */
47
47
  runner: string | undefined;
48
+ /** `risk --base` and `--head`: the two ends of the change. */
49
+ base: string | undefined;
50
+ head: string | undefined;
51
+ /** `risk --maps`: where the behavior maps are, when not in the checkout. */
52
+ maps: string | undefined;
48
53
  createWorkspace: string | undefined;
49
54
  /** Everything that was not a flag: the addresses `invite` sends to. */
50
55
  positional: readonly string[];
package/dist/cli.js CHANGED
@@ -9,10 +9,14 @@ import { runDiscover } from "./commands/discover.js";
9
9
  import { runExplain } from "./commands/explain.js";
10
10
  import { parseRole, runInvite } from "./commands/invite.js";
11
11
  import { runGuidance } from "./commands/guidance.js";
12
+ import { JUDGE_USAGE, judgeCommand } from "./commands/judge.js";
13
+ import { MAP_USAGE, mapCommand } from "./commands/map.js";
14
+ import { OFFERS_USAGE, offersCommand } from "./commands/offers.js";
12
15
  import { runMcp } from "./commands/mcp.js";
13
16
  import { runPrepare } from "./commands/prepare.js";
14
17
  import { runPropose } from "./commands/propose.js";
15
18
  import { runRepositories } from "./commands/repositories.js";
19
+ import { runRisk } from "./commands/risk.js";
16
20
  import { CREATE_WORKSPACE_MAX_LENGTH, runSetup } from "./commands/setup.js";
17
21
  import { runSession } from "./commands/session.js";
18
22
  import { runStatus } from "./commands/status.js";
@@ -21,9 +25,11 @@ import { runWhoami } from "./commands/whoami.js";
21
25
  import { runInstall } from "./install.js";
22
26
  import { openInBrowser } from "./open-browser.js";
23
27
  import { PUBLISHED_SPECIFIER } from "./release.js";
24
- import { installUserScope, runningFromCheckout, userHome, } from "./user-scope.js";
28
+ import { CODEX_HOOK_APPROVAL, installUserScope, runningFromCheckout, userHome, } from "./user-scope.js";
25
29
  import { updateNotice } from "./currency.js";
26
30
  import { describeEarlier, describeRemoval, findEarlier, removeEarlier } from "./remove-earlier.js";
31
+ import { recordSelfUpdateState, SELF_UPDATE_ENVIRONMENT } from "./self-update.js";
32
+ import { hooksAwaitingTrust } from "./hook-trust.js";
27
33
  import { describeCheckoutFormRemoval, findCheckoutForm, removeCheckoutForm, } from "./checkout-form.js";
28
34
  import { StoreError, normalizeControlPlane } from "./store.js";
29
35
  import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
@@ -153,6 +159,17 @@ const USAGE = `balladeer ${CLI_VERSION}
153
159
  answer whose verifier has changed since the map was built. It reads one
154
160
  local file and contacts nothing.
155
161
 
162
+ ${MAP_USAGE}
163
+ ${JUDGE_USAGE}
164
+ ${OFFERS_USAGE}
165
+ ${CLI_INVOCATION} risk [--base <ref>] [--head <ref>] [--json] [--maps <dir>]
166
+ Rank the promises this change could put at risk, with the evidence for
167
+ each: a mapped function or test it changes, a table, route or file it
168
+ shares, what imports what, and what its commit messages say. It reads
169
+ the behavior maps in .continuity/behavior-maps/ and the change from git,
170
+ from the merge base with this branch's upstream (else origin/main) to
171
+ HEAD unless named. Runs offline; nothing is sent. --json prints the report.
172
+
156
173
  ${CLI_INVOCATION} session [--new] [--record] [--repo owner/name] [--repository <uuid>] [--json]
157
174
  [--control-plane <url>]
158
175
  Print the id for this piece of work, and the one line to write into the
@@ -209,6 +226,9 @@ export function parseArguments(argv) {
209
226
  let record = false;
210
227
  let again = false;
211
228
  let runner;
229
+ let base;
230
+ let head;
231
+ let maps;
212
232
  let controlPlane;
213
233
  let repo;
214
234
  let file;
@@ -227,6 +247,9 @@ export function parseArguments(argv) {
227
247
  "--role": "contributor, viewer, or administrator",
228
248
  "--client": "codex or claude",
229
249
  "--runner": "a path to the pinned runner's cli.js",
250
+ "--base": "a git ref",
251
+ "--head": "a git ref",
252
+ "--maps": "a directory of behavior maps",
230
253
  };
231
254
  const value = (flag, inline) => {
232
255
  const next = inline ?? args.shift();
@@ -268,6 +291,12 @@ export function parseArguments(argv) {
268
291
  record = true;
269
292
  else if (name === "--runner")
270
293
  runner = value("--runner", inline);
294
+ else if (name === "--base")
295
+ base = value("--base", inline);
296
+ else if (name === "--head")
297
+ head = value("--head", inline);
298
+ else if (name === "--maps")
299
+ maps = value("--maps", inline);
271
300
  else if (name === "--wait")
272
301
  wait = true;
273
302
  else if (name === "--no-open")
@@ -378,6 +407,8 @@ export function parseArguments(argv) {
378
407
  }
379
408
  if (hook !== undefined && command !== "guidance")
380
409
  throw new StoreError("usage", "--hook belongs to guidance.");
410
+ if ((base !== undefined || head !== undefined || maps !== undefined) && command !== "risk")
411
+ throw new StoreError("usage", "--base, --head and --maps belong to risk.");
381
412
  if (event !== undefined && command !== "guidance")
382
413
  throw new StoreError("usage", "--event belongs to guidance.");
383
414
  const chosen = controlPlane ?? process.env.BALLADEER_CONTROL_PLANE?.trim() ?? DEFAULT_CONTROL_PLANE;
@@ -405,6 +436,9 @@ export function parseArguments(argv) {
405
436
  record,
406
437
  again,
407
438
  runner,
439
+ base,
440
+ head,
441
+ maps,
408
442
  createWorkspace,
409
443
  positional,
410
444
  role,
@@ -412,6 +446,20 @@ export function parseArguments(argv) {
412
446
  };
413
447
  }
414
448
  export async function main(argv) {
449
+ // `map`, `judge` and `offers` read their own flags: `--hook`, `--repo` and
450
+ // `--install-hook` mean something different there, and `judge --hook` runs
451
+ // inside a coding host where a stderr nag would reach the agent.
452
+ if (argv[0] === "map" || argv[0] === "judge" || argv[0] === "offers") {
453
+ const io = {
454
+ cwd: process.cwd(),
455
+ environment: process.env,
456
+ write: (text) => void process.stdout.write(text),
457
+ error: (text) => void process.stderr.write(text),
458
+ };
459
+ if (argv[0] === "offers")
460
+ return offersCommand(argv.slice(1), io);
461
+ return argv[0] === "map" ? mapCommand(argv.slice(1), io) : judgeCommand(argv.slice(1), io);
462
+ }
415
463
  let parsed;
416
464
  try {
417
465
  parsed = parseArguments(argv);
@@ -472,6 +520,19 @@ function installEverything(options) {
472
520
  for (const w of writes ?? [])
473
521
  if (!options.json)
474
522
  options.write(`${w.status === "refused" ? "Not changed" : w.status === "written" ? "Registered" : "Already current"}: ${w.path}${w.reason ? ` (${w.reason})` : ""}\n`);
523
+ // Codex runs a hook only once the person has trusted it, and trusts it by
524
+ // the hash of its definition, so a hook this command just wrote or changed
525
+ // is skipped until they do. Found 29 September 2026: Codex was the most
526
+ // common host at Didero and not one of its sessions had run the hook.
527
+ const codex = (writes ?? []).find((w) => w.host === "codex" && w.status !== "refused");
528
+ // Said whenever it is true, which is after any install that wrote or changed
529
+ // the hooks and until Codex has run one of them; silent once it has.
530
+ if (codex !== undefined && hooksAwaitingTrust(process.env, "codex")) {
531
+ if (options.json)
532
+ options.write(`${JSON.stringify({ step: "codex_hooks", approval: codex.status === "written" ? "required" : "check", command: "/hooks" })}\n`);
533
+ else
534
+ options.write(`${CODEX_HOOK_APPROVAL}\n`);
535
+ }
475
536
  // With the user-scope form in place, the older per-repository form in this
476
537
  // checkout only makes the session hear the guidance twice. Out it goes, with
477
538
  // a copy beside any file git does not already keep.
@@ -493,7 +554,12 @@ function installEverything(options) {
493
554
  options.write("Balladeer now runs in every Claude Code" +
494
555
  ((writes ?? []).some((w) => w.host === "codex") ? " and Codex" : "") +
495
556
  " session on this machine. In a folder of a connected repository it works as before; anywhere else it stays quiet, and `balladeer status` there says why.\n");
496
- return (writes ?? []).some((w) => w.status === "refused") ? 4 : code;
557
+ const outcome = (writes ?? []).some((w) => w.status === "refused") ? 4 : code;
558
+ // Started by the session hook's background update: leave how it ended where
559
+ // the next hook fire will find it and tell the server.
560
+ if (process.env[SELF_UPDATE_ENVIRONMENT] === "1")
561
+ recordSelfUpdateState(process.env, outcome === 0 ? `installed:${CLI_VERSION}` : `failed:exit-${outcome}`, Date.now());
562
+ return outcome;
497
563
  }
498
564
  async function dispatch(parsed, write) {
499
565
  switch (parsed.command) {
@@ -634,6 +700,15 @@ async function dispatch(parsed, write) {
634
700
  cwd: process.cwd(),
635
701
  write,
636
702
  });
703
+ case "risk":
704
+ return runRisk({
705
+ json: parsed.json,
706
+ ...(parsed.base === undefined ? {} : { base: parsed.base }),
707
+ ...(parsed.head === undefined ? {} : { head: parsed.head }),
708
+ ...(parsed.maps === undefined ? {} : { maps: parsed.maps }),
709
+ cwd: process.cwd(),
710
+ write,
711
+ });
637
712
  case "affected":
638
713
  return runAffected({
639
714
  json: parsed.json,
@@ -15,7 +15,7 @@ export type GuidanceOptions = Readonly<{
15
15
  fetchImpl?: typeof fetch;
16
16
  now?: () => number;
17
17
  /** Runs the background update; injected so tests never reach npm. */
18
- startUpdate?: (command: string, args: readonly string[]) => void;
18
+ startUpdate?: (command: string, args: readonly string[], environment: NodeJS.ProcessEnv) => boolean | void;
19
19
  }>;
20
20
  /** Hook stdout is bounded context only. Host prompts and transcript paths never leave this process. */
21
21
  export declare function runGuidance(options: GuidanceOptions): Promise<number>;
@@ -5,6 +5,7 @@ import { selectAgent } from "../agent.js";
5
5
  import { repositoryHints } from "../repository.js";
6
6
  import { newerVersionPublished } from "../currency.js";
7
7
  import { startSelfUpdateIfDue } from "../self-update.js";
8
+ import { noteHookFired } from "../hook-trust.js";
8
9
  /** A repository-specific setup step for explicit status and empty-catalog responses. */
9
10
  export function untrackedLine(remote) {
10
11
  const here = remote === "unknown/unknown" ? "This folder" : `This folder (${remote})`;
@@ -98,10 +99,15 @@ export async function runGuidance(options) {
98
99
  // Unconnected folders stay silent. An explicit status request explains setup.
99
100
  return 0;
100
101
  }
102
+ // A hook that runs has been trusted by its host; hosts that ask for that
103
+ // trust are the ones this matters to.
104
+ if (options.hook === "codex")
105
+ noteHookFired(options.environment, "codex");
101
106
  const loaded = await loadGuidance({
102
107
  agent: agents[0],
103
108
  environment: options.environment,
104
109
  timeoutMs: 750,
110
+ hook: options.hook,
105
111
  ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
106
112
  });
107
113
  // Behind? Start the install in the background, once a day at most, and