balladeer 1.0.17 → 1.0.19

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 (47) hide show
  1. package/dist/anchor-budget.d.ts +40 -0
  2. package/dist/anchor-budget.js +110 -0
  3. package/dist/anchoring.d.ts +241 -0
  4. package/dist/anchoring.js +766 -0
  5. package/dist/anchors-client.d.ts +62 -0
  6. package/dist/anchors-client.js +197 -0
  7. package/dist/anchors-schema.d.ts +85 -0
  8. package/dist/anchors-schema.js +246 -0
  9. package/dist/checkout-form.d.ts +11 -0
  10. package/dist/checkout-form.js +14 -0
  11. package/dist/cli.js +9 -4
  12. package/dist/commands/anchor-usage.d.ts +6 -0
  13. package/dist/commands/anchor-usage.js +19 -0
  14. package/dist/commands/anchor.d.ts +127 -0
  15. package/dist/commands/anchor.js +303 -0
  16. package/dist/commands/guidance.js +23 -8
  17. package/dist/commands/judge.d.ts +69 -21
  18. package/dist/commands/judge.js +354 -89
  19. package/dist/commands/map.js +2 -0
  20. package/dist/commands/mcp.js +5 -9
  21. package/dist/commands/setup.js +5 -23
  22. package/dist/currency.d.ts +2 -0
  23. package/dist/currency.js +2 -1
  24. package/dist/guidance-hook.mjs +166 -57
  25. package/dist/guidance-runtime.d.ts +26 -0
  26. package/dist/guidance-runtime.js +71 -1
  27. package/dist/guidance.d.ts +7 -0
  28. package/dist/guidance.js +10 -1
  29. package/dist/headless-agent.d.ts +19 -1
  30. package/dist/headless-agent.js +22 -5
  31. package/dist/judge-brief.d.ts +20 -2
  32. package/dist/judge-brief.js +26 -2
  33. package/dist/promise-meaning.d.ts +17 -0
  34. package/dist/promise-meaning.js +15 -6
  35. package/dist/risk/git.d.ts +3 -1
  36. package/dist/risk/git.js +3 -3
  37. package/dist/risk/graph-cache.d.ts +83 -0
  38. package/dist/risk/graph-cache.js +291 -0
  39. package/dist/risk/import-graph.d.ts +43 -0
  40. package/dist/risk/import-graph.js +88 -34
  41. package/dist/risk/pipeline.d.ts +13 -0
  42. package/dist/risk/pipeline.js +15 -4
  43. package/dist/self-update.d.ts +3 -3
  44. package/dist/self-update.js +38 -8
  45. package/dist/wire.d.ts +18 -2
  46. package/dist/wire.js +1 -1
  47. package/package.json +7 -2
@@ -1,5 +1,4 @@
1
1
  import { createInterface } from "node:readline";
2
- import { installGuidanceLoader } from "../guidance-install.js";
3
2
  import { callAgentTool, forwarderHeaders, noAgentCredentialSentence, safeJson, selectAgent, structuredString, updateLine, } from "../agent.js";
4
3
  import { noteServerVersion, updateNotice } from "../currency.js";
5
4
  import { repositoryHint, repositoryHints } from "../repository.js";
@@ -189,15 +188,12 @@ export async function runMcp(options) {
189
188
  return 4;
190
189
  return serveUntracked(options, untrackedLine(repositoryHint(options.cwd)));
191
190
  }
191
+ // This forwarder used to install the per-repository guidance hook into any
192
+ // checkout whose project config named this repository. From 1.0.8 the hook
193
+ // lives at user scope, so doing that put a second hook beside it and every
194
+ // session heard the guidance twice; it also re-armed the older hook the
195
+ // user-scope hook had just switched off. It installs nothing now.
192
196
  const { agent } = selection;
193
- const guidanceInstall = installGuidanceLoader({
194
- environment: options.environment,
195
- cwd: options.cwd,
196
- repositoryId: agent.repositoryId,
197
- controlPlane: agent.controlPlane,
198
- });
199
- if (guidanceInstall.status !== "not_applicable")
200
- options.error(`Balladeer guidance loader: ${guidanceInstall.status}${guidanceInstall.reason ? ` (${guidanceInstall.reason})` : ""}. New project hooks may need host trust; no trust is assumed.\n`);
201
197
  let saidUpdate = false;
202
198
  // Which host this session belongs to is known from its hello; whether its
203
199
  // hooks are waiting for the person's trust is asked at the first tool call,
@@ -11,7 +11,6 @@ import { CI_BRANCH, commitWorkflowOnBranch, repositoryRoot, workflowOnDefaultBra
11
11
  import { MCP_CONFIG_FILE, currentEntry, entryRepositoryId, isOurEntry, mergeMcpConfig, readMcpConfig, stdioEntry, } from "../mcp-config.js";
12
12
  import { DESKTOP_CONFIG_FILE, desktopConfigLocation, desktopServerKey, desktopStdioEntry, mergeDesktopConfig, } from "../desktop-config.js";
13
13
  import { selectAgent } from "../agent.js";
14
- import { installGuidanceLoader } from "../guidance-install.js";
15
14
  import { describeEarlier, describeRemoval, findEarlier, removeEarlier } from "../remove-earlier.js";
16
15
  import { userHome } from "../user-scope.js";
17
16
  import { formatInstant } from "../local-time.js";
@@ -1008,28 +1007,11 @@ function mergeHostConfig(options, root, repositoryId, published) {
1008
1007
  : mergeMcpConfig(root, entry, options.controlPlane);
1009
1008
  }
1010
1009
  function writeHostConventions(options, root) {
1011
- const result = writeConventions(root, options.client === "codex" ? { file: "AGENTS.md" } : {});
1012
- if (result.kind === "written") {
1013
- const entry = options.client === "codex" ? readCodexEntry(root) : currentEntry(readMcpConfig(root));
1014
- const repositoryId = entryRepositoryId(entry);
1015
- if (repositoryId !== undefined) {
1016
- const loader = installGuidanceLoader({
1017
- environment: options.environment,
1018
- cwd: root,
1019
- repositoryId,
1020
- controlPlane: options.controlPlane,
1021
- host: options.client === "codex" ? "codex" : "claude",
1022
- });
1023
- emit(options, { step: "guidance_loader", ...loader });
1024
- say(options, loader.status === "installed_needs_host_trust" ||
1025
- loader.status === "installed_trust_unverified"
1026
- ? "Project guidance loader installed. Review new hooks in your coding host once; future policy updates do not change that hook definition. A fresh session is needed to observe it."
1027
- : loader.status === "unavailable"
1028
- ? `Project guidance loader needs repair (${loader.reason}). No active hook is claimed.`
1029
- : "No matching project guidance integration was found. No guidance hook was installed or activated.");
1030
- }
1031
- }
1032
- return result;
1010
+ // No per-repository guidance hook any more. The install step registers the
1011
+ // hook at user scope, where it runs in every checkout; adding the older
1012
+ // per-repository one here as well put a second hook in this checkout, and
1013
+ // every session heard the guidance twice (Didero, September 2026).
1014
+ return writeConventions(root, options.client === "codex" ? { file: "AGENTS.md" } : {});
1033
1015
  }
1034
1016
  function hostNextSteps(options) {
1035
1017
  if (options.client === "codex")
@@ -15,6 +15,8 @@
15
15
  export declare const CLI_NEWEST_HEADER = "x-balladeer-cli-newest";
16
16
  export declare const CLI_FLOOR_HEADER = "x-balladeer-cli-minimum";
17
17
  export declare const CLI_ADVICE_HEADER = "x-balladeer-cli-advice";
18
+ /** Whether `left` is an older release than `right` (both `x.y.z`). */
19
+ export declare function older(left: string, right: string): boolean;
18
20
  /**
19
21
  * Reads the version signals off one response.
20
22
  *
package/dist/currency.js CHANGED
@@ -25,7 +25,8 @@ const ADVICE_LIMIT = 300;
25
25
  const VERSION = /^\d{1,4}\.\d{1,4}\.\d{1,4}$/;
26
26
  let notice;
27
27
  let newestSeen;
28
- function older(left, right) {
28
+ /** Whether `left` is an older release than `right` (both `x.y.z`). */
29
+ export function older(left, right) {
29
30
  const [a, b] = [left.split(".").map(Number), right.split(".").map(Number)];
30
31
  for (let index = 0; index < 3; index += 1) {
31
32
  const difference = a[index] - b[index];
@@ -2152,13 +2152,31 @@ function relayHook(input, directory) {
2152
2152
  }
2153
2153
 
2154
2154
  // src/wire.ts
2155
- var CLI_VERSION = "1.0.17";
2155
+ var CLI_VERSION = "1.0.19";
2156
2156
  var CLIENT_HEADER = "x-balladeer-client";
2157
2157
  var UPDATE_STATE_HEADER = "x-balladeer-update-state";
2158
2158
  var HOOK_HOST_HEADER = "x-balladeer-hook";
2159
2159
  var CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
2160
2160
  var DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
2161
2161
 
2162
+ // src/guidance-runtime.ts
2163
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
2164
+ import {
2165
+ constants as constants2,
2166
+ closeSync as closeSync3,
2167
+ fstatSync as fstatSync2,
2168
+ lstatSync as lstatSync3,
2169
+ mkdirSync as mkdirSync5,
2170
+ openSync as openSync3,
2171
+ readFileSync as readFileSync5,
2172
+ realpathSync,
2173
+ renameSync as renameSync4,
2174
+ unlinkSync as unlinkSync3,
2175
+ writeFileSync as writeFileSync4
2176
+ } from "node:fs";
2177
+ import { join as join5 } from "node:path";
2178
+ import { execFileSync } from "node:child_process";
2179
+
2162
2180
  // src/store.ts
2163
2181
  import {
2164
2182
  closeSync,
@@ -2289,6 +2307,8 @@ var HOOK_SPECIFIER = "balladeer@1";
2289
2307
  var STAMP = "self-update.json";
2290
2308
  var STATE = "self-update-state.json";
2291
2309
  var DAY_MS = 24 * 60 * 60 * 1e3;
2310
+ var HOUR_MS = 60 * 60 * 1e3;
2311
+ var VERSION2 = /^\d{1,4}\.\d{1,4}\.\d{1,4}$/;
2292
2312
  var SELF_UPDATE_ENVIRONMENT = "BALLADEER_SELF_UPDATE";
2293
2313
  var STATE_PATTERN = /^(?:started|unstartable|installed|failed):[A-Za-z0-9._-]{1,40}$/;
2294
2314
  function stampPath(environment) {
@@ -2297,16 +2317,23 @@ function stampPath(environment) {
2297
2317
  function statePath(environment) {
2298
2318
  return join3(configHome(environment), STATE);
2299
2319
  }
2300
- function lastStarted(environment) {
2320
+ function lastAttempt(environment) {
2301
2321
  const path = stampPath(environment);
2302
- if (!existsSync(path)) return 0;
2322
+ if (!existsSync(path)) return { startedAt: 0 };
2303
2323
  try {
2304
2324
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
2305
- return typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
2325
+ const startedAt = typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
2326
+ return typeof parsed.toward === "string" && VERSION2.test(parsed.toward) ? { startedAt, toward: parsed.toward } : { startedAt };
2306
2327
  } catch {
2307
- return 0;
2328
+ return { startedAt: 0 };
2308
2329
  }
2309
2330
  }
2331
+ function due(newer, last, now) {
2332
+ const since = now - last.startedAt;
2333
+ if (since >= DAY_MS) return true;
2334
+ const newerTarget = last.toward === void 0 || older(last.toward, newer);
2335
+ return newerTarget && since >= HOUR_MS;
2336
+ }
2310
2337
  function writePrivate(path, value, environment) {
2311
2338
  mkdirSync3(configHome(environment), { recursive: true, mode: 448 });
2312
2339
  writeFileSync2(path, JSON.stringify(value) + "\n", { mode: 384 });
@@ -2349,7 +2376,7 @@ function detached(command, args, environment) {
2349
2376
  function startSelfUpdateIfDue(newer, deps) {
2350
2377
  if (newer === void 0) return false;
2351
2378
  const now = deps.now ?? Date.now;
2352
- if (now() - lastStarted(deps.environment) < DAY_MS) return false;
2379
+ if (!due(newer, lastAttempt(deps.environment), now())) return false;
2353
2380
  const launch = resolveLaunch(HOOK_SPECIFIER, deps.environment, deps.execPath, deps.exists);
2354
2381
  let started;
2355
2382
  try {
@@ -2403,6 +2430,9 @@ function parseGuidance(value, agent) {
2403
2430
  return value;
2404
2431
  }
2405
2432
  function guidanceEndpoint(agent) {
2433
+ return agentEndpoint(agent, "/api/agent-guidance");
2434
+ }
2435
+ function agentEndpoint(agent, pathname) {
2406
2436
  const url = new URL(agent.mcpUrl);
2407
2437
  const expected = new URL(agent.controlPlane);
2408
2438
  if (url.origin !== expected.origin || url.protocol !== "https:" || url.pathname !== "/api/mcp" || url.username || url.password || url.hash || !UUID.test(agent.workspaceId) || !UUID.test(agent.repositoryId))
@@ -2415,7 +2445,7 @@ function guidanceEndpoint(agent) {
2415
2445
  throw new Error("invalid_guidance_scope");
2416
2446
  url.searchParams.set(key2, value);
2417
2447
  }
2418
- url.pathname = "/api/agent-guidance";
2448
+ url.pathname = pathname;
2419
2449
  return url;
2420
2450
  }
2421
2451
  function readJson(path) {
@@ -2625,52 +2655,124 @@ function recordGuidanceContext(input, write2) {
2625
2655
  return true;
2626
2656
  }
2627
2657
 
2658
+ // src/guidance-runtime.ts
2659
+ var OWNER = "balladeer-guidance-runtime/1";
2660
+ var sha2 = (value) => createHash3("sha256").update(value).digest("hex");
2661
+ function guidanceRuntimeKey(scope) {
2662
+ return sha2(
2663
+ JSON.stringify([
2664
+ realpathSync(scope.root),
2665
+ new URL(scope.controlPlane).origin,
2666
+ scope.repositoryId,
2667
+ scope.host
2668
+ ])
2669
+ );
2670
+ }
2671
+ function checkDirectory(path) {
2672
+ const stat = lstatSync3(path);
2673
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
2674
+ throw new Error("guidance_runtime_directory_unsafe");
2675
+ }
2676
+ function readPrivate(path, max) {
2677
+ const fd = openSync3(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
2678
+ try {
2679
+ const stat = fstatSync2(fd);
2680
+ if (!stat.isFile() || stat.size > max || (stat.mode & 63) !== 0)
2681
+ throw new Error("guidance_runtime_file_unsafe");
2682
+ return readFileSync5(fd);
2683
+ } finally {
2684
+ closeSync3(fd);
2685
+ }
2686
+ }
2687
+ function retireCheckoutRuntime(input) {
2688
+ try {
2689
+ const base = join5(configHome(input.environment), "guidance-runtime-v1");
2690
+ try {
2691
+ lstatSync3(base);
2692
+ } catch {
2693
+ return "absent";
2694
+ }
2695
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
2696
+ cwd: input.cwd,
2697
+ encoding: "utf8",
2698
+ stdio: ["ignore", "pipe", "ignore"],
2699
+ timeout: 500,
2700
+ maxBuffer: 8192
2701
+ }).trim();
2702
+ if (root.length === 0) return "absent";
2703
+ const key2 = guidanceRuntimeKey({
2704
+ root,
2705
+ host: input.host,
2706
+ repositoryId: input.repositoryId,
2707
+ controlPlane: input.controlPlane
2708
+ });
2709
+ const directory = join5(base, key2);
2710
+ const manifestPath = join5(directory, "current.json");
2711
+ try {
2712
+ checkDirectory(directory);
2713
+ } catch {
2714
+ return "absent";
2715
+ }
2716
+ let manifest;
2717
+ try {
2718
+ manifest = JSON.parse(readPrivate(manifestPath, 4096).toString("utf8"));
2719
+ } catch (error) {
2720
+ return error.code === "ENOENT" ? "absent" : "kept";
2721
+ }
2722
+ if (manifest?.owner !== OWNER || manifest.scopeKey !== key2) return "kept";
2723
+ renameSync4(manifestPath, join5(directory, "retired.json"));
2724
+ return "retired";
2725
+ } catch {
2726
+ return "kept";
2727
+ }
2728
+ }
2729
+
2628
2730
  // src/guidance-install.ts
2629
- import { execFileSync } from "node:child_process";
2731
+ import { execFileSync as execFileSync2 } from "node:child_process";
2630
2732
  import {
2631
- constants as constants2,
2632
- closeSync as closeSync5,
2633
- fstatSync as fstatSync2,
2634
- openSync as openSync5,
2733
+ constants as constants3,
2734
+ closeSync as closeSync6,
2735
+ fstatSync as fstatSync3,
2736
+ openSync as openSync6,
2635
2737
  existsSync as existsSync3,
2636
- lstatSync as lstatSync4,
2738
+ lstatSync as lstatSync5,
2637
2739
  linkSync,
2638
- mkdirSync as mkdirSync6,
2639
- readFileSync as readFileSync7,
2640
- renameSync as renameSync6,
2641
- unlinkSync as unlinkSync5,
2642
- writeFileSync as writeFileSync4
2740
+ mkdirSync as mkdirSync7,
2741
+ readFileSync as readFileSync8,
2742
+ renameSync as renameSync7,
2743
+ unlinkSync as unlinkSync6,
2744
+ writeFileSync as writeFileSync5
2643
2745
  } from "node:fs";
2644
2746
  var import_toml2 = __toESM(require_toml(), 1);
2645
- import { dirname as dirname4, join as join7, relative } from "node:path";
2747
+ import { dirname as dirname4, join as join8, relative } from "node:path";
2646
2748
 
2647
2749
  // src/codex-config.ts
2648
2750
  var import_toml = __toESM(require_toml(), 1);
2649
2751
  import {
2650
- closeSync as closeSync4,
2752
+ closeSync as closeSync5,
2651
2753
  existsSync as existsSync2,
2652
2754
  fsyncSync as fsyncSync3,
2653
- lstatSync as lstatSync3,
2654
- mkdirSync as mkdirSync5,
2655
- openSync as openSync4,
2656
- readFileSync as readFileSync6,
2657
- renameSync as renameSync5,
2658
- unlinkSync as unlinkSync4,
2755
+ lstatSync as lstatSync4,
2756
+ mkdirSync as mkdirSync6,
2757
+ openSync as openSync5,
2758
+ readFileSync as readFileSync7,
2759
+ renameSync as renameSync6,
2760
+ unlinkSync as unlinkSync5,
2659
2761
  writeSync as writeSync3
2660
2762
  } from "node:fs";
2661
- import { join as join6 } from "node:path";
2763
+ import { join as join7 } from "node:path";
2662
2764
 
2663
2765
  // src/mcp-config.ts
2664
2766
  import {
2665
- closeSync as closeSync3,
2767
+ closeSync as closeSync4,
2666
2768
  fsyncSync as fsyncSync2,
2667
- openSync as openSync3,
2668
- readFileSync as readFileSync5,
2669
- renameSync as renameSync4,
2670
- unlinkSync as unlinkSync3,
2769
+ openSync as openSync4,
2770
+ readFileSync as readFileSync6,
2771
+ renameSync as renameSync5,
2772
+ unlinkSync as unlinkSync4,
2671
2773
  writeSync as writeSync2
2672
2774
  } from "node:fs";
2673
- import { basename, dirname as dirname3, isAbsolute, join as join5 } from "node:path";
2775
+ import { basename, dirname as dirname3, isAbsolute, join as join6 } from "node:path";
2674
2776
  var MCP_CONFIG_FILE = ".mcp.json";
2675
2777
  function sameOrigin(left, right) {
2676
2778
  try {
@@ -2713,7 +2815,7 @@ function currentEntry(parsed) {
2713
2815
  }
2714
2816
  function readMcpConfig(repositoryRoot) {
2715
2817
  try {
2716
- return JSON.parse(readFileSync5(join5(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
2818
+ return JSON.parse(readFileSync6(join6(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
2717
2819
  } catch {
2718
2820
  return void 0;
2719
2821
  }
@@ -2723,7 +2825,7 @@ function readMcpConfig(repositoryRoot) {
2723
2825
  var CODEX_CONFIG_FILE = ".codex/config.toml";
2724
2826
  function readCodexEntry(root) {
2725
2827
  try {
2726
- const config = (0, import_toml.parse)(readFileSync6(join6(root, CODEX_CONFIG_FILE), "utf8"));
2828
+ const config = (0, import_toml.parse)(readFileSync7(join7(root, CODEX_CONFIG_FILE), "utf8"));
2727
2829
  const servers = config.mcp_servers;
2728
2830
  return servers && typeof servers === "object" && !Array.isArray(servers) && !(servers instanceof Date) ? servers.balladeer : void 0;
2729
2831
  } catch {
@@ -2732,7 +2834,7 @@ function readCodexEntry(root) {
2732
2834
  }
2733
2835
  function hasManagedCodexEntry(root, controlPlane) {
2734
2836
  try {
2735
- const text = readFileSync6(join6(root, CODEX_CONFIG_FILE), "utf8");
2837
+ const text = readFileSync7(join7(root, CODEX_CONFIG_FILE), "utf8");
2736
2838
  const starts = [...text.matchAll(/^# balladeer:mcp:start\r?$/gm)];
2737
2839
  const ends = [...text.matchAll(/^# balladeer:mcp:end\r?$/gm)];
2738
2840
  if (starts.length !== 1 || ends.length !== 1 || !starts[0] || !ends[0] || starts[0].index >= ends[0].index)
@@ -3148,23 +3250,23 @@ var MAX_FILE = 262144;
3148
3250
  function readRegular(path, allowMissing = false, writable = true) {
3149
3251
  if (!existsSync3(path)) {
3150
3252
  try {
3151
- lstatSync4(path);
3253
+ lstatSync5(path);
3152
3254
  } catch (e) {
3153
3255
  if (e.code === "ENOENT" && allowMissing) return void 0;
3154
3256
  }
3155
3257
  throw new Error("file_unavailable");
3156
3258
  }
3157
- const stat = lstatSync4(path);
3259
+ const stat = lstatSync5(path);
3158
3260
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE || writable && (stat.mode & 146) === 0)
3159
3261
  throw new Error("file_not_safely_writable");
3160
- return readFileSync7(path, "utf8");
3262
+ return readFileSync8(path, "utf8");
3161
3263
  }
3162
3264
  function safeParent(root, path) {
3163
3265
  let at = dirname4(path);
3164
3266
  while (at !== root) {
3165
3267
  if (relative(root, at).startsWith("..")) throw new Error("outside_repository");
3166
3268
  try {
3167
- const stat = lstatSync4(at);
3269
+ const stat = lstatSync5(at);
3168
3270
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe_directory");
3169
3271
  } catch (e) {
3170
3272
  if (e.code !== "ENOENT") throw e;
@@ -3174,12 +3276,12 @@ function safeParent(root, path) {
3174
3276
  }
3175
3277
  function validateProjectGuidanceScope(options) {
3176
3278
  try {
3177
- const root = execFileSync("git", ["-C", options.cwd, "rev-parse", "--show-toplevel"], {
3279
+ const root = execFileSync2("git", ["-C", options.cwd, "rev-parse", "--show-toplevel"], {
3178
3280
  encoding: "utf8",
3179
3281
  stdio: ["ignore", "pipe", "ignore"],
3180
3282
  timeout: 1e3
3181
3283
  }).trim();
3182
- const path = join7(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
3284
+ const path = join8(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
3183
3285
  safeParent(root, path);
3184
3286
  readRegular(path, false, false);
3185
3287
  const entry = options.hook === "codex" ? readCodexEntry(root) : currentEntry(readMcpConfig(root));
@@ -3259,11 +3361,11 @@ function withSafeEndpoint(agent) {
3259
3361
  }
3260
3362
 
3261
3363
  // src/repository.ts
3262
- import { execFileSync as execFileSync2 } from "node:child_process";
3364
+ import { execFileSync as execFileSync3 } from "node:child_process";
3263
3365
  var REPOSITORY_HINT_PATTERN = /^[A-Za-z0-9._-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
3264
3366
  function git(cwd, args) {
3265
3367
  try {
3266
- return execFileSync2("git", ["-C", cwd, ...args], {
3368
+ return execFileSync3("git", ["-C", cwd, ...args], {
3267
3369
  encoding: "utf8",
3268
3370
  stdio: ["ignore", "pipe", "ignore"]
3269
3371
  }).trim();
@@ -3293,21 +3395,21 @@ function repositoryHints(cwd = process.cwd()) {
3293
3395
  }
3294
3396
 
3295
3397
  // src/hook-trust.ts
3296
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
3297
- import { join as join8 } from "node:path";
3398
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync9, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
3399
+ import { join as join9 } from "node:path";
3298
3400
  var HOSTS_REQUIRING_TRUST = ["codex"];
3299
3401
  var FILE = "hook-trust.json";
3300
3402
  var key = (host, set) => set === "guidance" ? host : `${host}:${set}`;
3301
3403
  function read(environment) {
3302
3404
  try {
3303
- const parsed = JSON.parse(readFileSync8(join8(configHome(environment), FILE), "utf8"));
3405
+ const parsed = JSON.parse(readFileSync9(join9(configHome(environment), FILE), "utf8"));
3304
3406
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3305
3407
  } catch {
3306
3408
  return {};
3307
3409
  }
3308
3410
  }
3309
3411
  function pushCheckAwaitingFile(environment, host) {
3310
- return join8(configHome(environment), `${host}-push-check-awaiting-first-run`);
3412
+ return join9(configHome(environment), `${host}-push-check-awaiting-first-run`);
3311
3413
  }
3312
3414
  function forget(path) {
3313
3415
  try {
@@ -3317,13 +3419,13 @@ function forget(path) {
3317
3419
  }
3318
3420
  function write(environment, marks) {
3319
3421
  try {
3320
- mkdirSync7(configHome(environment), { recursive: true, mode: 448 });
3321
- writeFileSync5(join8(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
3422
+ mkdirSync8(configHome(environment), { recursive: true, mode: 448 });
3423
+ writeFileSync6(join9(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
3322
3424
  mode: 384
3323
3425
  });
3324
3426
  for (const host of HOSTS_REQUIRING_TRUST) {
3325
3427
  const file = pushCheckAwaitingFile(environment, host);
3326
- if (awaiting(marks[key(host, "push-check")])) writeFileSync5(file, "", { mode: 384 });
3428
+ if (awaiting(marks[key(host, "push-check")])) writeFileSync6(file, "", { mode: 384 });
3327
3429
  else forget(file);
3328
3430
  }
3329
3431
  } catch {
@@ -3457,6 +3559,14 @@ async function runGuidance(options) {
3457
3559
  return 0;
3458
3560
  }
3459
3561
  if (options.hook === "codex") noteHookFired(options.environment, "codex");
3562
+ if (!projectScoped && event === "SessionStart")
3563
+ retireCheckoutRuntime({
3564
+ cwd: options.cwd,
3565
+ host: options.hook,
3566
+ repositoryId: agents[0].repositoryId,
3567
+ controlPlane: agents[0].controlPlane,
3568
+ environment: options.environment
3569
+ });
3460
3570
  const loaded = await loadGuidance({
3461
3571
  agent: agents[0],
3462
3572
  environment: options.environment,
@@ -3464,12 +3574,11 @@ async function runGuidance(options) {
3464
3574
  hook: options.hook,
3465
3575
  ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
3466
3576
  });
3467
- if (event === "SessionStart")
3468
- startSelfUpdateIfDue(newerVersionPublished(), {
3469
- environment: options.environment,
3470
- ...options.startUpdate ? { run: options.startUpdate } : {},
3471
- ...options.now ? { now: options.now } : {}
3472
- });
3577
+ startSelfUpdateIfDue(newerVersionPublished(), {
3578
+ environment: options.environment,
3579
+ ...options.startUpdate ? { run: options.startUpdate } : {},
3580
+ ...options.now ? { now: options.now } : {}
3581
+ });
3473
3582
  const sessionId = identifier(record.session_id);
3474
3583
  const agentId = identifier(record.agent_id);
3475
3584
  recordGuidanceContext(
@@ -10,6 +10,32 @@ export declare function installGuidanceRuntime(options: Scope & {
10
10
  /** Digest of the final, already-validated project MCP configuration. */
11
11
  configurationDigest: string;
12
12
  }): void;
13
+ export type RetireOutcome = "retired" | "absent" | "kept";
14
+ /**
15
+ * Switches off the older per-repository hook in one checkout, without touching
16
+ * any file in the repository.
17
+ *
18
+ * Releases before 1.0.8 put a hook in the checkout's `.claude/settings.json`
19
+ * or `.codex/config.toml` whose command is the wrapper below. That wrapper
20
+ * reads `current.json` from this checkout's runtime directory before anything
21
+ * else, outside its own try, and its outer catch swallows the failure: with
22
+ * the manifest gone it prints nothing and fetches nothing. So renaming the
23
+ * manifest aside silences it for every later session, while the tracked hook
24
+ * entry stays exactly as the team committed it.
25
+ *
26
+ * Called by the user-scope hook at session start, only once it has found this
27
+ * checkout's connection, so a checkout is switched off only where the
28
+ * user-scope hook is already serving the guidance. Only the directory for this
29
+ * checkout, connection and host is touched, and only when the manifest proves
30
+ * it is ours (owner and scope key). Never throws and never prints.
31
+ */
32
+ export declare function retireCheckoutRuntime(input: {
33
+ cwd: string;
34
+ host: "codex" | "claude";
35
+ repositoryId: string;
36
+ controlPlane: string;
37
+ environment: NodeJS.ProcessEnv;
38
+ }): RetireOutcome;
13
39
  /** Stable, dependency-free wrapper. Current policy exists only in the authenticated server response. */
14
40
  export declare function guidanceRuntimeCommand(host: "codex" | "claude", repositoryId: string, controlPlane: string): string;
15
41
  export {};
@@ -2,7 +2,8 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { ensureStoreDirectory } from "./store.js";
5
+ import { execFileSync } from "node:child_process";
6
+ import { configHome, ensureStoreDirectory } from "./store.js";
6
7
  import { GUIDANCE_UNAVAILABLE } from "./guidance.js";
7
8
  const OWNER = "balladeer-guidance-runtime/1";
8
9
  const HEX = /^[a-f0-9]{64}$/;
@@ -114,6 +115,75 @@ export function installGuidanceRuntime(options) {
114
115
  }
115
116
  }
116
117
  }
118
+ /**
119
+ * Switches off the older per-repository hook in one checkout, without touching
120
+ * any file in the repository.
121
+ *
122
+ * Releases before 1.0.8 put a hook in the checkout's `.claude/settings.json`
123
+ * or `.codex/config.toml` whose command is the wrapper below. That wrapper
124
+ * reads `current.json` from this checkout's runtime directory before anything
125
+ * else, outside its own try, and its outer catch swallows the failure: with
126
+ * the manifest gone it prints nothing and fetches nothing. So renaming the
127
+ * manifest aside silences it for every later session, while the tracked hook
128
+ * entry stays exactly as the team committed it.
129
+ *
130
+ * Called by the user-scope hook at session start, only once it has found this
131
+ * checkout's connection, so a checkout is switched off only where the
132
+ * user-scope hook is already serving the guidance. Only the directory for this
133
+ * checkout, connection and host is touched, and only when the manifest proves
134
+ * it is ours (owner and scope key). Never throws and never prints.
135
+ */
136
+ export function retireCheckoutRuntime(input) {
137
+ try {
138
+ const base = join(configHome(input.environment), "guidance-runtime-v1");
139
+ // Most laptops never had the older form: one stat and nothing else.
140
+ try {
141
+ lstatSync(base);
142
+ }
143
+ catch {
144
+ return "absent";
145
+ }
146
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
147
+ cwd: input.cwd,
148
+ encoding: "utf8",
149
+ stdio: ["ignore", "pipe", "ignore"],
150
+ timeout: 500,
151
+ maxBuffer: 8192,
152
+ }).trim();
153
+ if (root.length === 0)
154
+ return "absent";
155
+ const key = guidanceRuntimeKey({
156
+ root,
157
+ host: input.host,
158
+ repositoryId: input.repositoryId,
159
+ controlPlane: input.controlPlane,
160
+ });
161
+ const directory = join(base, key);
162
+ const manifestPath = join(directory, "current.json");
163
+ try {
164
+ checkDirectory(directory);
165
+ }
166
+ catch {
167
+ return "absent";
168
+ }
169
+ let manifest;
170
+ try {
171
+ manifest = JSON.parse(readPrivate(manifestPath, 4096).toString("utf8"));
172
+ }
173
+ catch (error) {
174
+ return error.code === "ENOENT" ? "absent" : "kept";
175
+ }
176
+ if (manifest?.owner !== OWNER || manifest.scopeKey !== key)
177
+ return "kept";
178
+ // Renamed, not deleted: the pinned runtime stays beside it, and an
179
+ // explicit install of the older form would simply write a new manifest.
180
+ renameSync(manifestPath, join(directory, "retired.json"));
181
+ return "retired";
182
+ }
183
+ catch {
184
+ return "kept";
185
+ }
186
+ }
117
187
  /** Stable, dependency-free wrapper. Current policy exists only in the authenticated server response. */
118
188
  export function guidanceRuntimeCommand(host, repositoryId, controlPlane) {
119
189
  // Keep this readable: it is the exact code the host asks a person to trust once.
@@ -12,6 +12,13 @@ export type GuidanceEvent = "SessionStart" | "SubagentStart" | "UserPromptSubmit
12
12
  export declare const GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
13
13
  export declare function parseGuidance(value: unknown, agent: StoredAgent): GuidanceDocument;
14
14
  export declare function guidanceEndpoint(agent: StoredAgent): URL;
15
+ /**
16
+ * A route of the server this repository's agent connection belongs to, scoped
17
+ * to its workspace and repository the way `/api/agent-guidance` is: same
18
+ * origin as the connection's MCP address, https only, and the scope in the
19
+ * query exactly once.
20
+ */
21
+ export declare function agentEndpoint(agent: StoredAgent, pathname: string): URL;
15
22
  export declare function guidanceScopeKey(agent: StoredAgent): string;
16
23
  export type GuidanceLoad = Readonly<{
17
24
  status: "fresh" | "revalidated" | "unavailable";
package/dist/guidance.js CHANGED
@@ -34,6 +34,15 @@ export function parseGuidance(value, agent) {
34
34
  return value;
35
35
  }
36
36
  export function guidanceEndpoint(agent) {
37
+ return agentEndpoint(agent, "/api/agent-guidance");
38
+ }
39
+ /**
40
+ * A route of the server this repository's agent connection belongs to, scoped
41
+ * to its workspace and repository the way `/api/agent-guidance` is: same
42
+ * origin as the connection's MCP address, https only, and the scope in the
43
+ * query exactly once.
44
+ */
45
+ export function agentEndpoint(agent, pathname) {
37
46
  const url = new URL(agent.mcpUrl);
38
47
  const expected = new URL(agent.controlPlane);
39
48
  if (url.origin !== expected.origin ||
@@ -54,7 +63,7 @@ export function guidanceEndpoint(agent) {
54
63
  throw new Error("invalid_guidance_scope");
55
64
  url.searchParams.set(key, value);
56
65
  }
57
- url.pathname = "/api/agent-guidance";
66
+ url.pathname = pathname;
58
67
  return url;
59
68
  }
60
69
  function readJson(path) {