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.
- package/dist/anchor-budget.d.ts +40 -0
- package/dist/anchor-budget.js +110 -0
- package/dist/anchoring.d.ts +241 -0
- package/dist/anchoring.js +766 -0
- package/dist/anchors-client.d.ts +62 -0
- package/dist/anchors-client.js +197 -0
- package/dist/anchors-schema.d.ts +85 -0
- package/dist/anchors-schema.js +246 -0
- package/dist/checkout-form.d.ts +11 -0
- package/dist/checkout-form.js +14 -0
- package/dist/cli.js +9 -4
- package/dist/commands/anchor-usage.d.ts +6 -0
- package/dist/commands/anchor-usage.js +19 -0
- package/dist/commands/anchor.d.ts +127 -0
- package/dist/commands/anchor.js +303 -0
- package/dist/commands/guidance.js +23 -8
- package/dist/commands/judge.d.ts +69 -21
- package/dist/commands/judge.js +354 -89
- package/dist/commands/map.js +2 -0
- package/dist/commands/mcp.js +5 -9
- package/dist/commands/setup.js +5 -23
- package/dist/currency.d.ts +2 -0
- package/dist/currency.js +2 -1
- package/dist/guidance-hook.mjs +166 -57
- package/dist/guidance-runtime.d.ts +26 -0
- package/dist/guidance-runtime.js +71 -1
- package/dist/guidance.d.ts +7 -0
- package/dist/guidance.js +10 -1
- package/dist/headless-agent.d.ts +19 -1
- package/dist/headless-agent.js +22 -5
- package/dist/judge-brief.d.ts +20 -2
- package/dist/judge-brief.js +26 -2
- package/dist/promise-meaning.d.ts +17 -0
- package/dist/promise-meaning.js +15 -6
- package/dist/risk/git.d.ts +3 -1
- package/dist/risk/git.js +3 -3
- package/dist/risk/graph-cache.d.ts +83 -0
- package/dist/risk/graph-cache.js +291 -0
- package/dist/risk/import-graph.d.ts +43 -0
- package/dist/risk/import-graph.js +88 -34
- package/dist/risk/pipeline.d.ts +13 -0
- package/dist/risk/pipeline.js +15 -4
- package/dist/self-update.d.ts +3 -3
- package/dist/self-update.js +38 -8
- package/dist/wire.d.ts +18 -2
- package/dist/wire.js +1 -1
- package/package.json +7 -2
package/dist/commands/mcp.js
CHANGED
|
@@ -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,
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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")
|
package/dist/currency.d.ts
CHANGED
|
@@ -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
|
-
|
|
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];
|
package/dist/guidance-hook.mjs
CHANGED
|
@@ -2152,13 +2152,31 @@ function relayHook(input, directory) {
|
|
|
2152
2152
|
}
|
|
2153
2153
|
|
|
2154
2154
|
// src/wire.ts
|
|
2155
|
-
var CLI_VERSION = "1.0.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
2632
|
-
closeSync as
|
|
2633
|
-
fstatSync as
|
|
2634
|
-
openSync as
|
|
2733
|
+
constants as constants3,
|
|
2734
|
+
closeSync as closeSync6,
|
|
2735
|
+
fstatSync as fstatSync3,
|
|
2736
|
+
openSync as openSync6,
|
|
2635
2737
|
existsSync as existsSync3,
|
|
2636
|
-
lstatSync as
|
|
2738
|
+
lstatSync as lstatSync5,
|
|
2637
2739
|
linkSync,
|
|
2638
|
-
mkdirSync as
|
|
2639
|
-
readFileSync as
|
|
2640
|
-
renameSync as
|
|
2641
|
-
unlinkSync as
|
|
2642
|
-
writeFileSync as
|
|
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
|
|
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
|
|
2752
|
+
closeSync as closeSync5,
|
|
2651
2753
|
existsSync as existsSync2,
|
|
2652
2754
|
fsyncSync as fsyncSync3,
|
|
2653
|
-
lstatSync as
|
|
2654
|
-
mkdirSync as
|
|
2655
|
-
openSync as
|
|
2656
|
-
readFileSync as
|
|
2657
|
-
renameSync as
|
|
2658
|
-
unlinkSync as
|
|
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
|
|
2763
|
+
import { join as join7 } from "node:path";
|
|
2662
2764
|
|
|
2663
2765
|
// src/mcp-config.ts
|
|
2664
2766
|
import {
|
|
2665
|
-
closeSync as
|
|
2767
|
+
closeSync as closeSync4,
|
|
2666
2768
|
fsyncSync as fsyncSync2,
|
|
2667
|
-
openSync as
|
|
2668
|
-
readFileSync as
|
|
2669
|
-
renameSync as
|
|
2670
|
-
unlinkSync as
|
|
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
|
|
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(
|
|
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)(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
3297
|
-
import { join as
|
|
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(
|
|
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
|
|
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
|
-
|
|
3321
|
-
|
|
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")]))
|
|
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
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
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 {};
|
package/dist/guidance-runtime.js
CHANGED
|
@@ -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 {
|
|
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.
|
package/dist/guidance.d.ts
CHANGED
|
@@ -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 =
|
|
66
|
+
url.pathname = pathname;
|
|
58
67
|
return url;
|
|
59
68
|
}
|
|
60
69
|
function readJson(path) {
|