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
@@ -2027,8 +2027,10 @@ var require_toml = __commonJS({
2027
2027
  });
2028
2028
 
2029
2029
  // src/wire.ts
2030
- var CLI_VERSION = "1.0.14";
2030
+ var CLI_VERSION = "1.0.16";
2031
2031
  var CLIENT_HEADER = "x-balladeer-client";
2032
+ var UPDATE_STATE_HEADER = "x-balladeer-update-state";
2033
+ var HOOK_HOST_HEADER = "x-balladeer-hook";
2032
2034
  var CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
2033
2035
  var DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
2034
2036
 
@@ -2150,6 +2152,102 @@ function newerVersionPublished() {
2150
2152
  return newestSeen;
2151
2153
  }
2152
2154
 
2155
+ // src/self-update.ts
2156
+ import { spawn } from "node:child_process";
2157
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2158
+ import { delimiter, dirname as dirname2, join as join2 } from "node:path";
2159
+
2160
+ // src/release.ts
2161
+ var HOOK_SPECIFIER = "balladeer@1";
2162
+
2163
+ // src/self-update.ts
2164
+ var STAMP = "self-update.json";
2165
+ var STATE = "self-update-state.json";
2166
+ var DAY_MS = 24 * 60 * 60 * 1e3;
2167
+ var SELF_UPDATE_ENVIRONMENT = "BALLADEER_SELF_UPDATE";
2168
+ var STATE_PATTERN = /^(?:started|unstartable|installed|failed):[A-Za-z0-9._-]{1,40}$/;
2169
+ function stampPath(environment) {
2170
+ return join2(configHome(environment), STAMP);
2171
+ }
2172
+ function statePath(environment) {
2173
+ return join2(configHome(environment), STATE);
2174
+ }
2175
+ function lastStarted(environment) {
2176
+ const path = stampPath(environment);
2177
+ if (!existsSync(path)) return 0;
2178
+ try {
2179
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
2180
+ return typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
2181
+ } catch {
2182
+ return 0;
2183
+ }
2184
+ }
2185
+ function writePrivate(path, value, environment) {
2186
+ mkdirSync2(configHome(environment), { recursive: true, mode: 448 });
2187
+ writeFileSync(path, JSON.stringify(value) + "\n", { mode: 384 });
2188
+ }
2189
+ function recordSelfUpdateState(environment, state, at) {
2190
+ if (!STATE_PATTERN.test(state)) return;
2191
+ try {
2192
+ writePrivate(statePath(environment), { state, at }, environment);
2193
+ } catch {
2194
+ }
2195
+ }
2196
+ function selfUpdateState(environment) {
2197
+ try {
2198
+ const parsed = JSON.parse(readFileSync2(statePath(environment), "utf8"));
2199
+ return typeof parsed.state === "string" && STATE_PATTERN.test(parsed.state) ? parsed.state : void 0;
2200
+ } catch {
2201
+ return void 0;
2202
+ }
2203
+ }
2204
+ function resolveLaunch(specifier, environment, execPath = process.execPath, exists = existsSync) {
2205
+ const bin = dirname2(execPath);
2206
+ const tail = ["-y", specifier, "install", "--json"];
2207
+ const child = {
2208
+ ...environment,
2209
+ PATH: [bin, environment.PATH ?? ""].filter((part) => part.length > 0).join(delimiter),
2210
+ [SELF_UPDATE_ENVIRONMENT]: "1"
2211
+ };
2212
+ const script = join2(bin, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js");
2213
+ if (exists(script)) return { command: execPath, args: [script, ...tail], environment: child };
2214
+ const shim = join2(bin, process.platform === "win32" ? "npx.cmd" : "npx");
2215
+ if (exists(shim)) return { command: shim, args: tail, environment: child };
2216
+ return { command: "npx", args: tail, environment: child };
2217
+ }
2218
+ function detached(command, args, environment) {
2219
+ const child = spawn(command, [...args], { detached: true, stdio: "ignore", env: environment });
2220
+ child.on("error", () => void 0);
2221
+ child.unref();
2222
+ return child.pid !== void 0;
2223
+ }
2224
+ function startSelfUpdateIfDue(newer, deps) {
2225
+ if (newer === void 0) return false;
2226
+ const now = deps.now ?? Date.now;
2227
+ if (now() - lastStarted(deps.environment) < DAY_MS) return false;
2228
+ const launch = resolveLaunch(HOOK_SPECIFIER, deps.environment, deps.execPath, deps.exists);
2229
+ let started;
2230
+ try {
2231
+ started = (deps.run ?? detached)(launch.command, launch.args, launch.environment) !== false;
2232
+ } catch {
2233
+ started = false;
2234
+ }
2235
+ if (!started) {
2236
+ recordSelfUpdateState(deps.environment, "unstartable:launcher", now());
2237
+ return false;
2238
+ }
2239
+ try {
2240
+ writePrivate(
2241
+ stampPath(deps.environment),
2242
+ { startedAt: now(), toward: newer },
2243
+ deps.environment
2244
+ );
2245
+ } catch {
2246
+ }
2247
+ recordSelfUpdateState(deps.environment, `started:${newer}`, now());
2248
+ return true;
2249
+ }
2250
+
2153
2251
  // src/guidance.ts
2154
2252
  import { createHash, randomBytes } from "node:crypto";
2155
2253
  import {
@@ -2157,14 +2255,14 @@ import {
2157
2255
  closeSync as closeSync2,
2158
2256
  fstatSync,
2159
2257
  lstatSync as lstatSync2,
2160
- mkdirSync as mkdirSync2,
2258
+ mkdirSync as mkdirSync3,
2161
2259
  openSync as openSync2,
2162
- readFileSync as readFileSync2,
2260
+ readFileSync as readFileSync3,
2163
2261
  renameSync as renameSync2,
2164
2262
  unlinkSync as unlinkSync2,
2165
- writeFileSync
2263
+ writeFileSync as writeFileSync2
2166
2264
  } from "node:fs";
2167
- import { join as join2 } from "node:path";
2265
+ import { join as join3 } from "node:path";
2168
2266
  var GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
2169
2267
  var HEX = /^[a-f0-9]{64}$/;
2170
2268
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
@@ -2201,7 +2299,7 @@ function readJson(path) {
2201
2299
  fd = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
2202
2300
  const stat = fstatSync(fd);
2203
2301
  if (!stat.isFile() || stat.size > MAX_BYTES || (stat.mode & 63) !== 0) return void 0;
2204
- return JSON.parse(readFileSync2(fd, "utf8"));
2302
+ return JSON.parse(readFileSync3(fd, "utf8"));
2205
2303
  } catch {
2206
2304
  return void 0;
2207
2305
  } finally {
@@ -2210,7 +2308,7 @@ function readJson(path) {
2210
2308
  }
2211
2309
  function privateDirectory(path) {
2212
2310
  try {
2213
- mkdirSync2(path, { mode: 448 });
2311
+ mkdirSync3(path, { mode: 448 });
2214
2312
  } catch {
2215
2313
  }
2216
2314
  const stat = lstatSync2(path);
@@ -2220,7 +2318,7 @@ function privateDirectory(path) {
2220
2318
  function atomicJson(path, value) {
2221
2319
  const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
2222
2320
  try {
2223
- writeFileSync(temporary, `${JSON.stringify(value)}
2321
+ writeFileSync2(temporary, `${JSON.stringify(value)}
2224
2322
  `, { flag: "wx", mode: 384 });
2225
2323
  renameSync2(temporary, path);
2226
2324
  } finally {
@@ -2244,16 +2342,16 @@ function guidanceScopeKey(agent) {
2244
2342
  function cacheDirectory(environment) {
2245
2343
  const home = configHome(environment);
2246
2344
  privateDirectory(home);
2247
- const directory = join2(home, "guidance-v1");
2345
+ const directory = join3(home, "guidance-v1");
2248
2346
  privateDirectory(directory);
2249
2347
  return directory;
2250
2348
  }
2251
2349
  function cached(directory, scopeKey, agent) {
2252
2350
  try {
2253
- const pointer = readJson(join2(directory, `${scopeKey}.current.json`));
2351
+ const pointer = readJson(join3(directory, `${scopeKey}.current.json`));
2254
2352
  if (!object(pointer) || typeof pointer.key !== "string" || !HEX.test(pointer.key))
2255
2353
  return void 0;
2256
- const body = readJson(join2(directory, `${pointer.key}.body.json`));
2354
+ const body = readJson(join3(directory, `${pointer.key}.body.json`));
2257
2355
  if (!object(body) || typeof body.etag !== "string" || !/^"[a-f0-9]{64}"$/.test(body.etag))
2258
2356
  return void 0;
2259
2357
  const document = parseGuidance(body.document, agent);
@@ -2291,6 +2389,7 @@ async function loadGuidance(input) {
2291
2389
  } catch {
2292
2390
  }
2293
2391
  const previous = directory ? cached(directory, scopeKey, input.agent) : void 0;
2392
+ const updateState = selfUpdateState(input.environment);
2294
2393
  const abort = new AbortController();
2295
2394
  let timer;
2296
2395
  try {
@@ -2302,6 +2401,10 @@ async function loadGuidance(input) {
2302
2401
  headers: {
2303
2402
  authorization: `Bearer ${input.agent.token}`,
2304
2403
  [CLIENT_HEADER]: CLIENT_HEADER_VALUE,
2404
+ // How the last background update ended, so a laptop that cannot
2405
+ // update itself is seen on the scorecard rather than guessed at.
2406
+ ...updateState === void 0 ? {} : { [UPDATE_STATE_HEADER]: updateState },
2407
+ ...input.hook === void 0 ? {} : { [HOOK_HOST_HEADER]: input.hook },
2305
2408
  accept: "application/json",
2306
2409
  ...previous ? { "if-none-match": previous.etag } : {}
2307
2410
  }
@@ -2325,11 +2428,11 @@ async function loadGuidance(input) {
2325
2428
  if (directory) {
2326
2429
  try {
2327
2430
  const key = sha(JSON.stringify([scopeKey, document.revision]));
2328
- atomicJson(join2(directory, `${key}.body.json`), { document, etag });
2329
- atomicJson(join2(directory, `${scopeKey}.current.json`), { key });
2431
+ atomicJson(join3(directory, `${key}.body.json`), { document, etag });
2432
+ atomicJson(join3(directory, `${scopeKey}.current.json`), { key });
2330
2433
  if (previous && previous.key !== key) {
2331
2434
  try {
2332
- unlinkSync2(join2(directory, `${previous.key}.body.json`));
2435
+ unlinkSync2(join3(directory, `${previous.key}.body.json`));
2333
2436
  } catch {
2334
2437
  }
2335
2438
  }
@@ -2357,7 +2460,7 @@ async function loadGuidance(input) {
2357
2460
  if (timer) clearTimeout(timer);
2358
2461
  }
2359
2462
  }
2360
- function recordGuidanceContext(input, write) {
2463
+ function recordGuidanceContext(input, write2) {
2361
2464
  const always = input.event !== "UserPromptSubmit" || input.load.status === "unavailable" || !input.sessionId;
2362
2465
  const contextKey = sha(
2363
2466
  JSON.stringify([
@@ -2371,13 +2474,13 @@ function recordGuidanceContext(input, write) {
2371
2474
  let path;
2372
2475
  let emitted = true;
2373
2476
  try {
2374
- path = join2(cacheDirectory(input.environment), `${contextKey}.context.json`);
2477
+ path = join3(cacheDirectory(input.environment), `${contextKey}.context.json`);
2375
2478
  const previous = readJson(path);
2376
2479
  emitted = always || !object(previous) || previous.status === "unavailable" || previous.digest !== document?.digest || previous.revision !== document?.revision || (previous.runtimeGeneration ?? null) !== (input.runtimeGeneration ?? null);
2377
2480
  } catch {
2378
2481
  }
2379
2482
  if (!emitted) return false;
2380
- write();
2483
+ write2();
2381
2484
  if (path) {
2382
2485
  try {
2383
2486
  atomicJson(path, {
@@ -2404,50 +2507,45 @@ import {
2404
2507
  closeSync as closeSync5,
2405
2508
  fstatSync as fstatSync2,
2406
2509
  openSync as openSync5,
2407
- existsSync as existsSync2,
2510
+ existsSync as existsSync3,
2408
2511
  lstatSync as lstatSync4,
2409
2512
  linkSync,
2410
- mkdirSync as mkdirSync4,
2411
- readFileSync as readFileSync5,
2513
+ mkdirSync as mkdirSync5,
2514
+ readFileSync as readFileSync6,
2412
2515
  renameSync as renameSync5,
2413
2516
  unlinkSync as unlinkSync5,
2414
- writeFileSync as writeFileSync2
2517
+ writeFileSync as writeFileSync3
2415
2518
  } from "node:fs";
2416
2519
  var import_toml2 = __toESM(require_toml(), 1);
2417
- import { dirname as dirname3, join as join5, relative } from "node:path";
2520
+ import { dirname as dirname4, join as join6, relative } from "node:path";
2418
2521
 
2419
2522
  // src/codex-config.ts
2420
2523
  var import_toml = __toESM(require_toml(), 1);
2421
2524
  import {
2422
2525
  closeSync as closeSync4,
2423
- existsSync,
2526
+ existsSync as existsSync2,
2424
2527
  fsyncSync as fsyncSync3,
2425
2528
  lstatSync as lstatSync3,
2426
- mkdirSync as mkdirSync3,
2529
+ mkdirSync as mkdirSync4,
2427
2530
  openSync as openSync4,
2428
- readFileSync as readFileSync4,
2531
+ readFileSync as readFileSync5,
2429
2532
  renameSync as renameSync4,
2430
2533
  unlinkSync as unlinkSync4,
2431
2534
  writeSync as writeSync3
2432
2535
  } from "node:fs";
2433
- import { join as join4 } from "node:path";
2536
+ import { join as join5 } from "node:path";
2434
2537
 
2435
2538
  // src/mcp-config.ts
2436
2539
  import {
2437
2540
  closeSync as closeSync3,
2438
2541
  fsyncSync as fsyncSync2,
2439
2542
  openSync as openSync3,
2440
- readFileSync as readFileSync3,
2543
+ readFileSync as readFileSync4,
2441
2544
  renameSync as renameSync3,
2442
2545
  unlinkSync as unlinkSync3,
2443
2546
  writeSync as writeSync2
2444
2547
  } from "node:fs";
2445
- import { basename, dirname as dirname2, isAbsolute, join as join3 } from "node:path";
2446
-
2447
- // src/release.ts
2448
- var HOOK_SPECIFIER = "balladeer@1";
2449
-
2450
- // src/mcp-config.ts
2548
+ import { basename, dirname as dirname3, isAbsolute, join as join4 } from "node:path";
2451
2549
  var MCP_CONFIG_FILE = ".mcp.json";
2452
2550
  function sameOrigin(left, right) {
2453
2551
  try {
@@ -2490,7 +2588,7 @@ function currentEntry(parsed) {
2490
2588
  }
2491
2589
  function readMcpConfig(repositoryRoot) {
2492
2590
  try {
2493
- return JSON.parse(readFileSync3(join3(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
2591
+ return JSON.parse(readFileSync4(join4(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
2494
2592
  } catch {
2495
2593
  return void 0;
2496
2594
  }
@@ -2500,7 +2598,7 @@ function readMcpConfig(repositoryRoot) {
2500
2598
  var CODEX_CONFIG_FILE = ".codex/config.toml";
2501
2599
  function readCodexEntry(root) {
2502
2600
  try {
2503
- const config = (0, import_toml.parse)(readFileSync4(join4(root, CODEX_CONFIG_FILE), "utf8"));
2601
+ const config = (0, import_toml.parse)(readFileSync5(join5(root, CODEX_CONFIG_FILE), "utf8"));
2504
2602
  const servers = config.mcp_servers;
2505
2603
  return servers && typeof servers === "object" && !Array.isArray(servers) && !(servers instanceof Date) ? servers.balladeer : void 0;
2506
2604
  } catch {
@@ -2509,7 +2607,7 @@ function readCodexEntry(root) {
2509
2607
  }
2510
2608
  function hasManagedCodexEntry(root, controlPlane) {
2511
2609
  try {
2512
- const text = readFileSync4(join4(root, CODEX_CONFIG_FILE), "utf8");
2610
+ const text = readFileSync5(join5(root, CODEX_CONFIG_FILE), "utf8");
2513
2611
  const starts = [...text.matchAll(/^# balladeer:mcp:start\r?$/gm)];
2514
2612
  const ends = [...text.matchAll(/^# balladeer:mcp:end\r?$/gm)];
2515
2613
  if (starts.length !== 1 || ends.length !== 1 || !starts[0] || !ends[0] || starts[0].index >= ends[0].index)
@@ -2642,6 +2740,72 @@ Balladeer only keeps promises a check can catch. If a customer would notice some
2642
2740
  slips, say that and I will keep that instead."
2643
2741
 
2644
2742
  Then take the customer-visible half if they give you one, and file that instead.`;
2743
+ var VERIFIER_SHAPE = `### The shape of a check that lasts
2744
+
2745
+ Write a test only when the promise is easily testable: the repository's own test runner reaches the
2746
+ behavior in the ordinary way, its existing fixtures set the situation up, no outside system is
2747
+ needed, and the assertion is one of the promise's cases. Put it in the team's own suite, under the
2748
+ team's runner, with a comment naming the promise id, so a later judge can find it, read it and run
2749
+ it first. If reaching the behavior would mean stand-in modules, a scenario repository or a copy of
2750
+ the product's logic, the promise is not easily testable: write nothing and say so. The pre-push
2751
+ judge below covers it.
2752
+
2753
+ A check exists to catch the failure its owner named, and to keep catching it after the code
2754
+ underneath has been rewritten by people who never read the promise. So build it against the
2755
+ behavior, at the boundary the promise names: the request a caller sends, the command a person runs,
2756
+ the row that ends up written, the page that renders. Reach the product through the entry point its
2757
+ users reach it through, never through a helper you found while reading the source, and never through
2758
+ a copy of the product's logic written into the check. A copy passes forever while the product drifts
2759
+ underneath it.
2760
+
2761
+ Assert what the promise's cases say and nothing more. Each passing case states a property; check
2762
+ that property. Each failing case names an outcome that must not happen; make the check refuse
2763
+ exactly that. Do not pin whole outputs, orderings the promise lets vary, or values the owner never
2764
+ mentioned: every one of those becomes a red the owner is told to act on when the behavior has not
2765
+ broken.
2766
+
2767
+ The refactor control is where a brittle check shows itself. It must be a genuine restructuring of
2768
+ the same behavior, moved, renamed or reimplemented, with the same observable outcome, reached
2769
+ through the same interface the check uses to reach the product. If you cannot write it without also
2770
+ editing the check, the check is bound to an implementation rather than to the promise, and what
2771
+ needs fixing is the check. The known-bad control must be the failure the owner wrote down, not the
2772
+ check sabotaging itself.
2773
+
2774
+ Where the repository's own test runner already reaches the behavior, run it through the declared
2775
+ result protocol rather than writing a second harness: the existing suite is the one the team will
2776
+ keep maintaining. And when a check cannot reach a verdict, say so. A check that could not run reads
2777
+ Unknown, which is honest, and never as a pass, which is not.`;
2778
+ var PRE_PUSH_JUDGE = `### Before you push, try to break it
2779
+
2780
+ The sealed check runs in CI, after the fact. The cheapest moment to catch a broken promise is before
2781
+ the push, on the machine where the change can still be fixed, and the way to catch it is not to
2782
+ reread your own diff. Find the promises the change touches: balladeer affected <paths> where this
2783
+ repository has a touch map, otherwise the leads discover_intent gave you for those paths. For each
2784
+ one that has a linked test or a sealed check, run it first; a test named for the promise is the
2785
+ fastest verdict there is, and the runner's run-one-target command prints the four facts a red check
2786
+ carries.
2787
+
2788
+ Then, for every promise the change touches, checked or not, spawn a fresh agent that has not seen
2789
+ your change or your reasoning, however your host spawns one, and hand it three things and nothing
2790
+ else: the promise as get_promise returns it, the changed files, and the brief below. Not the
2791
+ pull-request description and not your notes. One agent per promise; a judge handed six promises at
2792
+ once holds none of them. If your host cannot spawn one, work through the brief yourself in a fresh
2793
+ context after the work is finished, and say that you did.
2794
+
2795
+ The brief, verbatim: "You are checking one promise against one change. Read the promise's cases
2796
+ first. Then find out, by running things, whether the change keeps it. You may report broken only
2797
+ with a reproduction: a command or test that fails with this change and passes without it, which
2798
+ you have run both ways. You may report kept only after you have exercised each of the promise's
2799
+ passing cases against the changed code. Anything else you report as could not tell, with what
2800
+ stopped you. Do not take the author's word for anything, read code rather than descriptions, and
2801
+ name the promise, never the person."
2802
+
2803
+ What to do with the answer. Broken, with a reproduction: fix the behavior before pushing, keep the
2804
+ reproduction, and offer to carry it into the sealed check as a failing case, which means re-sealing
2805
+ and re-qualifying, so ask before you do. Kept: say so in one line, naming the promise. Could not
2806
+ tell: say that, in those words; it is not a pass. None of this is protection, and none of it reaches
2807
+ Balladeer on its own. Protection is the sealed check qualifying in CI. This is the check you run so
2808
+ that CI has nothing to find.`;
2645
2809
  var SETTLING_QUESTIONS = `### When somebody says "tell me about" one
2646
2810
 
2647
2811
  An id is how a person points at something here. Expand a promise id with get_promise and a proposal
@@ -2790,6 +2954,10 @@ especially: it is Balladeer's word for a proposal, so it reads as jargon whateve
2790
2954
 
2791
2955
  ${SESSION_BEHAVIORS}
2792
2956
 
2957
+ ${VERIFIER_SHAPE}
2958
+
2959
+ ${PRE_PUSH_JUDGE}
2960
+
2793
2961
  ${SETTLING_QUESTIONS}
2794
2962
 
2795
2963
  ${SEALED_FILES}
@@ -2853,7 +3021,7 @@ in for them to read. The tool sends nothing itself: an administrator presses Sen
2853
3021
  // src/guidance-install.ts
2854
3022
  var MAX_FILE = 262144;
2855
3023
  function readRegular(path, allowMissing = false, writable = true) {
2856
- if (!existsSync2(path)) {
3024
+ if (!existsSync3(path)) {
2857
3025
  try {
2858
3026
  lstatSync4(path);
2859
3027
  } catch (e) {
@@ -2864,10 +3032,10 @@ function readRegular(path, allowMissing = false, writable = true) {
2864
3032
  const stat = lstatSync4(path);
2865
3033
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE || writable && (stat.mode & 146) === 0)
2866
3034
  throw new Error("file_not_safely_writable");
2867
- return readFileSync5(path, "utf8");
3035
+ return readFileSync6(path, "utf8");
2868
3036
  }
2869
3037
  function safeParent(root, path) {
2870
- let at = dirname3(path);
3038
+ let at = dirname4(path);
2871
3039
  while (at !== root) {
2872
3040
  if (relative(root, at).startsWith("..")) throw new Error("outside_repository");
2873
3041
  try {
@@ -2876,7 +3044,7 @@ function safeParent(root, path) {
2876
3044
  } catch (e) {
2877
3045
  if (e.code !== "ENOENT") throw e;
2878
3046
  }
2879
- at = dirname3(at);
3047
+ at = dirname4(at);
2880
3048
  }
2881
3049
  }
2882
3050
  function validateProjectGuidanceScope(options) {
@@ -2886,7 +3054,7 @@ function validateProjectGuidanceScope(options) {
2886
3054
  stdio: ["ignore", "pipe", "ignore"],
2887
3055
  timeout: 1e3
2888
3056
  }).trim();
2889
- const path = join5(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
3057
+ const path = join6(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
2890
3058
  safeParent(root, path);
2891
3059
  readRegular(path, false, false);
2892
3060
  const entry = options.hook === "codex" ? readCodexEntry(root) : currentEntry(readMcpConfig(root));
@@ -2999,47 +3167,33 @@ function repositoryHints(cwd = process.cwd()) {
2999
3167
  return hints;
3000
3168
  }
3001
3169
 
3002
- // src/self-update.ts
3003
- import { spawn } from "node:child_process";
3004
- import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
3005
- import { join as join6 } from "node:path";
3006
- var STAMP = "self-update.json";
3007
- var DAY_MS = 24 * 60 * 60 * 1e3;
3008
- function stampPath(environment) {
3009
- return join6(configHome(environment), STAMP);
3010
- }
3011
- function lastStarted(environment) {
3012
- const path = stampPath(environment);
3013
- if (!existsSync3(path)) return 0;
3170
+ // src/hook-trust.ts
3171
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
3172
+ import { join as join7 } from "node:path";
3173
+ var FILE = "hook-trust.json";
3174
+ function read(environment) {
3014
3175
  try {
3015
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
3016
- return typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
3176
+ const parsed = JSON.parse(readFileSync7(join7(configHome(environment), FILE), "utf8"));
3177
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3017
3178
  } catch {
3018
- return 0;
3179
+ return {};
3019
3180
  }
3020
3181
  }
3021
- function detached(command, args) {
3022
- const child = spawn(command, [...args], { detached: true, stdio: "ignore" });
3023
- child.on("error", () => void 0);
3024
- child.unref();
3025
- }
3026
- function startSelfUpdateIfDue(newer, deps) {
3027
- if (newer === void 0) return false;
3028
- const now = deps.now ?? Date.now;
3029
- if (now() - lastStarted(deps.environment) < DAY_MS) return false;
3182
+ function write(environment, marks) {
3030
3183
  try {
3031
- mkdirSync5(configHome(deps.environment), { recursive: true, mode: 448 });
3032
- writeFileSync3(
3033
- stampPath(deps.environment),
3034
- JSON.stringify({ startedAt: now(), toward: newer }) + "\n",
3035
- { mode: 384 }
3036
- );
3037
- (deps.run ?? detached)("npx", ["-y", HOOK_SPECIFIER, "install", "--json"]);
3038
- return true;
3184
+ mkdirSync6(configHome(environment), { recursive: true, mode: 448 });
3185
+ writeFileSync4(join7(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
3186
+ mode: 384
3187
+ });
3039
3188
  } catch {
3040
- return false;
3041
3189
  }
3042
3190
  }
3191
+ function noteHookFired(environment, host, now = Date.now()) {
3192
+ const marks = read(environment);
3193
+ const mark = marks[host];
3194
+ if (mark?.firedAt !== void 0 && mark.firedAt >= (mark.writtenAt ?? 0)) return;
3195
+ write(environment, { ...marks, [host]: { ...mark, firedAt: now } });
3196
+ }
3043
3197
 
3044
3198
  // src/commands/guidance.ts
3045
3199
  function readInput(stream) {
@@ -3118,10 +3272,12 @@ async function runGuidance(options) {
3118
3272
  if (projectScoped) throw new Error("guidance_connection_unavailable");
3119
3273
  return 0;
3120
3274
  }
3275
+ if (options.hook === "codex") noteHookFired(options.environment, "codex");
3121
3276
  const loaded = await loadGuidance({
3122
3277
  agent: agents[0],
3123
3278
  environment: options.environment,
3124
3279
  timeoutMs: 750,
3280
+ hook: options.hook,
3125
3281
  ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
3126
3282
  });
3127
3283
  if (event === "SessionStart")
@@ -24,6 +24,8 @@ export declare function loadGuidance(input: {
24
24
  environment: NodeJS.ProcessEnv;
25
25
  fetchImpl?: typeof fetch;
26
26
  timeoutMs?: number;
27
+ /** The host whose hook is asking, so the server can tell a laptop where one host never asks. */
28
+ hook?: "codex" | "claude";
27
29
  }): Promise<GuidanceLoad>;
28
30
  /** Writes context first, then records emission metadata; this never claims model receipt. */
29
31
  export declare function recordGuidanceContext(input: {
package/dist/guidance.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { noteServerVersion } from "./currency.js";
2
- import { CLIENT_HEADER, CLIENT_HEADER_VALUE } from "./wire.js";
2
+ import { CLIENT_HEADER, CLIENT_HEADER_VALUE, HOOK_HOST_HEADER, UPDATE_STATE_HEADER, } from "./wire.js";
3
+ import { selfUpdateState } from "./self-update.js";
3
4
  import { createHash, randomBytes } from "node:crypto";
4
5
  import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
5
6
  import { join } from "node:path";
@@ -168,6 +169,7 @@ export async function loadGuidance(input) {
168
169
  /* Cache is optional. */
169
170
  }
170
171
  const previous = directory ? cached(directory, scopeKey, input.agent) : undefined;
172
+ const updateState = selfUpdateState(input.environment);
171
173
  const abort = new AbortController();
172
174
  let timer;
173
175
  try {
@@ -179,6 +181,10 @@ export async function loadGuidance(input) {
179
181
  headers: {
180
182
  authorization: `Bearer ${input.agent.token}`,
181
183
  [CLIENT_HEADER]: CLIENT_HEADER_VALUE,
184
+ // How the last background update ended, so a laptop that cannot
185
+ // update itself is seen on the scorecard rather than guessed at.
186
+ ...(updateState === undefined ? {} : { [UPDATE_STATE_HEADER]: updateState }),
187
+ ...(input.hook === undefined ? {} : { [HOOK_HOST_HEADER]: input.hook }),
182
188
  accept: "application/json",
183
189
  ...(previous ? { "if-none-match": previous.etag } : {}),
184
190
  },