balladeer 1.0.18 → 1.0.20
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/README.md +18 -0
- package/dist/anchoring.js +2 -2
- package/dist/anchors-schema.d.ts +3 -1
- package/dist/anchors-schema.js +5 -3
- package/dist/checkout-form.d.ts +11 -0
- package/dist/checkout-form.js +14 -0
- package/dist/cli.js +4 -3
- package/dist/commands/guidance.js +23 -8
- package/dist/commands/judge.d.ts +50 -1
- package/dist/commands/judge.js +768 -110
- 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 +162 -56
- package/dist/guidance-runtime.d.ts +26 -0
- package/dist/guidance-runtime.js +71 -1
- package/dist/headless-agent.d.ts +2 -0
- package/dist/headless-agent.js +5 -1
- package/dist/judge-brief.d.ts +11 -5
- package/dist/judge-brief.js +26 -10
- package/dist/judge-evidence.d.ts +87 -0
- package/dist/judge-evidence.js +393 -0
- package/dist/judge-hook.d.ts +14 -1
- package/dist/judge-hook.js +34 -5
- package/dist/judge-review.d.ts +10 -0
- package/dist/judge-review.js +36 -0
- package/dist/judge-said.d.ts +1 -0
- package/dist/judge-said.js +3 -0
- package/dist/promise-meaning.d.ts +4 -0
- package/dist/promise-review-client.d.ts +84 -0
- package/dist/promise-review-client.js +357 -0
- package/dist/push-operation.d.ts +61 -0
- package/dist/push-operation.js +472 -0
- package/dist/read-only-review.d.ts +2 -0
- package/dist/read-only-review.js +2 -0
- package/dist/review-backstop.d.ts +20 -0
- package/dist/review-backstop.js +54 -0
- package/dist/review-reporting.d.ts +32 -0
- package/dist/review-reporting.js +319 -0
- package/dist/review-selection.d.ts +33 -0
- package/dist/review-selection.js +64 -0
- package/dist/review-settings.d.ts +6 -0
- package/dist/review-settings.js +9 -0
- package/dist/self-update.d.ts +3 -3
- package/dist/self-update.js +38 -8
- package/dist/user-scope.d.ts +1 -10
- package/dist/user-scope.js +12 -7
- package/dist/wire.d.ts +3 -3
- package/dist/wire.js +1 -1
- package/package.json +1 -1
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.20";
|
|
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 {
|
|
@@ -2628,52 +2655,124 @@ function recordGuidanceContext(input, write2) {
|
|
|
2628
2655
|
return true;
|
|
2629
2656
|
}
|
|
2630
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
|
+
|
|
2631
2730
|
// src/guidance-install.ts
|
|
2632
|
-
import { execFileSync } from "node:child_process";
|
|
2731
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2633
2732
|
import {
|
|
2634
|
-
constants as
|
|
2635
|
-
closeSync as
|
|
2636
|
-
fstatSync as
|
|
2637
|
-
openSync as
|
|
2733
|
+
constants as constants3,
|
|
2734
|
+
closeSync as closeSync6,
|
|
2735
|
+
fstatSync as fstatSync3,
|
|
2736
|
+
openSync as openSync6,
|
|
2638
2737
|
existsSync as existsSync3,
|
|
2639
|
-
lstatSync as
|
|
2738
|
+
lstatSync as lstatSync5,
|
|
2640
2739
|
linkSync,
|
|
2641
|
-
mkdirSync as
|
|
2642
|
-
readFileSync as
|
|
2643
|
-
renameSync as
|
|
2644
|
-
unlinkSync as
|
|
2645
|
-
writeFileSync as
|
|
2740
|
+
mkdirSync as mkdirSync7,
|
|
2741
|
+
readFileSync as readFileSync8,
|
|
2742
|
+
renameSync as renameSync7,
|
|
2743
|
+
unlinkSync as unlinkSync6,
|
|
2744
|
+
writeFileSync as writeFileSync5
|
|
2646
2745
|
} from "node:fs";
|
|
2647
2746
|
var import_toml2 = __toESM(require_toml(), 1);
|
|
2648
|
-
import { dirname as dirname4, join as
|
|
2747
|
+
import { dirname as dirname4, join as join8, relative } from "node:path";
|
|
2649
2748
|
|
|
2650
2749
|
// src/codex-config.ts
|
|
2651
2750
|
var import_toml = __toESM(require_toml(), 1);
|
|
2652
2751
|
import {
|
|
2653
|
-
closeSync as
|
|
2752
|
+
closeSync as closeSync5,
|
|
2654
2753
|
existsSync as existsSync2,
|
|
2655
2754
|
fsyncSync as fsyncSync3,
|
|
2656
|
-
lstatSync as
|
|
2657
|
-
mkdirSync as
|
|
2658
|
-
openSync as
|
|
2659
|
-
readFileSync as
|
|
2660
|
-
renameSync as
|
|
2661
|
-
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,
|
|
2662
2761
|
writeSync as writeSync3
|
|
2663
2762
|
} from "node:fs";
|
|
2664
|
-
import { join as
|
|
2763
|
+
import { join as join7 } from "node:path";
|
|
2665
2764
|
|
|
2666
2765
|
// src/mcp-config.ts
|
|
2667
2766
|
import {
|
|
2668
|
-
closeSync as
|
|
2767
|
+
closeSync as closeSync4,
|
|
2669
2768
|
fsyncSync as fsyncSync2,
|
|
2670
|
-
openSync as
|
|
2671
|
-
readFileSync as
|
|
2672
|
-
renameSync as
|
|
2673
|
-
unlinkSync as
|
|
2769
|
+
openSync as openSync4,
|
|
2770
|
+
readFileSync as readFileSync6,
|
|
2771
|
+
renameSync as renameSync5,
|
|
2772
|
+
unlinkSync as unlinkSync4,
|
|
2674
2773
|
writeSync as writeSync2
|
|
2675
2774
|
} from "node:fs";
|
|
2676
|
-
import { basename, dirname as dirname3, isAbsolute, join as
|
|
2775
|
+
import { basename, dirname as dirname3, isAbsolute, join as join6 } from "node:path";
|
|
2677
2776
|
var MCP_CONFIG_FILE = ".mcp.json";
|
|
2678
2777
|
function sameOrigin(left, right) {
|
|
2679
2778
|
try {
|
|
@@ -2716,7 +2815,7 @@ function currentEntry(parsed) {
|
|
|
2716
2815
|
}
|
|
2717
2816
|
function readMcpConfig(repositoryRoot) {
|
|
2718
2817
|
try {
|
|
2719
|
-
return JSON.parse(
|
|
2818
|
+
return JSON.parse(readFileSync6(join6(repositoryRoot, MCP_CONFIG_FILE), "utf8"));
|
|
2720
2819
|
} catch {
|
|
2721
2820
|
return void 0;
|
|
2722
2821
|
}
|
|
@@ -2726,7 +2825,7 @@ function readMcpConfig(repositoryRoot) {
|
|
|
2726
2825
|
var CODEX_CONFIG_FILE = ".codex/config.toml";
|
|
2727
2826
|
function readCodexEntry(root) {
|
|
2728
2827
|
try {
|
|
2729
|
-
const config = (0, import_toml.parse)(
|
|
2828
|
+
const config = (0, import_toml.parse)(readFileSync7(join7(root, CODEX_CONFIG_FILE), "utf8"));
|
|
2730
2829
|
const servers = config.mcp_servers;
|
|
2731
2830
|
return servers && typeof servers === "object" && !Array.isArray(servers) && !(servers instanceof Date) ? servers.balladeer : void 0;
|
|
2732
2831
|
} catch {
|
|
@@ -2735,7 +2834,7 @@ function readCodexEntry(root) {
|
|
|
2735
2834
|
}
|
|
2736
2835
|
function hasManagedCodexEntry(root, controlPlane) {
|
|
2737
2836
|
try {
|
|
2738
|
-
const text =
|
|
2837
|
+
const text = readFileSync7(join7(root, CODEX_CONFIG_FILE), "utf8");
|
|
2739
2838
|
const starts = [...text.matchAll(/^# balladeer:mcp:start\r?$/gm)];
|
|
2740
2839
|
const ends = [...text.matchAll(/^# balladeer:mcp:end\r?$/gm)];
|
|
2741
2840
|
if (starts.length !== 1 || ends.length !== 1 || !starts[0] || !ends[0] || starts[0].index >= ends[0].index)
|
|
@@ -3151,23 +3250,23 @@ var MAX_FILE = 262144;
|
|
|
3151
3250
|
function readRegular(path, allowMissing = false, writable = true) {
|
|
3152
3251
|
if (!existsSync3(path)) {
|
|
3153
3252
|
try {
|
|
3154
|
-
|
|
3253
|
+
lstatSync5(path);
|
|
3155
3254
|
} catch (e) {
|
|
3156
3255
|
if (e.code === "ENOENT" && allowMissing) return void 0;
|
|
3157
3256
|
}
|
|
3158
3257
|
throw new Error("file_unavailable");
|
|
3159
3258
|
}
|
|
3160
|
-
const stat =
|
|
3259
|
+
const stat = lstatSync5(path);
|
|
3161
3260
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE || writable && (stat.mode & 146) === 0)
|
|
3162
3261
|
throw new Error("file_not_safely_writable");
|
|
3163
|
-
return
|
|
3262
|
+
return readFileSync8(path, "utf8");
|
|
3164
3263
|
}
|
|
3165
3264
|
function safeParent(root, path) {
|
|
3166
3265
|
let at = dirname4(path);
|
|
3167
3266
|
while (at !== root) {
|
|
3168
3267
|
if (relative(root, at).startsWith("..")) throw new Error("outside_repository");
|
|
3169
3268
|
try {
|
|
3170
|
-
const stat =
|
|
3269
|
+
const stat = lstatSync5(at);
|
|
3171
3270
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe_directory");
|
|
3172
3271
|
} catch (e) {
|
|
3173
3272
|
if (e.code !== "ENOENT") throw e;
|
|
@@ -3177,12 +3276,12 @@ function safeParent(root, path) {
|
|
|
3177
3276
|
}
|
|
3178
3277
|
function validateProjectGuidanceScope(options) {
|
|
3179
3278
|
try {
|
|
3180
|
-
const root =
|
|
3279
|
+
const root = execFileSync2("git", ["-C", options.cwd, "rev-parse", "--show-toplevel"], {
|
|
3181
3280
|
encoding: "utf8",
|
|
3182
3281
|
stdio: ["ignore", "pipe", "ignore"],
|
|
3183
3282
|
timeout: 1e3
|
|
3184
3283
|
}).trim();
|
|
3185
|
-
const path =
|
|
3284
|
+
const path = join8(root, options.hook === "codex" ? ".codex/config.toml" : ".mcp.json");
|
|
3186
3285
|
safeParent(root, path);
|
|
3187
3286
|
readRegular(path, false, false);
|
|
3188
3287
|
const entry = options.hook === "codex" ? readCodexEntry(root) : currentEntry(readMcpConfig(root));
|
|
@@ -3262,11 +3361,11 @@ function withSafeEndpoint(agent) {
|
|
|
3262
3361
|
}
|
|
3263
3362
|
|
|
3264
3363
|
// src/repository.ts
|
|
3265
|
-
import { execFileSync as
|
|
3364
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3266
3365
|
var REPOSITORY_HINT_PATTERN = /^[A-Za-z0-9._-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
|
|
3267
3366
|
function git(cwd, args) {
|
|
3268
3367
|
try {
|
|
3269
|
-
return
|
|
3368
|
+
return execFileSync3("git", ["-C", cwd, ...args], {
|
|
3270
3369
|
encoding: "utf8",
|
|
3271
3370
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3272
3371
|
}).trim();
|
|
@@ -3296,21 +3395,21 @@ function repositoryHints(cwd = process.cwd()) {
|
|
|
3296
3395
|
}
|
|
3297
3396
|
|
|
3298
3397
|
// src/hook-trust.ts
|
|
3299
|
-
import { mkdirSync as
|
|
3300
|
-
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";
|
|
3301
3400
|
var HOSTS_REQUIRING_TRUST = ["codex"];
|
|
3302
3401
|
var FILE = "hook-trust.json";
|
|
3303
3402
|
var key = (host, set) => set === "guidance" ? host : `${host}:${set}`;
|
|
3304
3403
|
function read(environment) {
|
|
3305
3404
|
try {
|
|
3306
|
-
const parsed = JSON.parse(
|
|
3405
|
+
const parsed = JSON.parse(readFileSync9(join9(configHome(environment), FILE), "utf8"));
|
|
3307
3406
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3308
3407
|
} catch {
|
|
3309
3408
|
return {};
|
|
3310
3409
|
}
|
|
3311
3410
|
}
|
|
3312
3411
|
function pushCheckAwaitingFile(environment, host) {
|
|
3313
|
-
return
|
|
3412
|
+
return join9(configHome(environment), `${host}-push-check-awaiting-first-run`);
|
|
3314
3413
|
}
|
|
3315
3414
|
function forget(path) {
|
|
3316
3415
|
try {
|
|
@@ -3320,13 +3419,13 @@ function forget(path) {
|
|
|
3320
3419
|
}
|
|
3321
3420
|
function write(environment, marks) {
|
|
3322
3421
|
try {
|
|
3323
|
-
|
|
3324
|
-
|
|
3422
|
+
mkdirSync8(configHome(environment), { recursive: true, mode: 448 });
|
|
3423
|
+
writeFileSync6(join9(configHome(environment), FILE), JSON.stringify(marks) + "\n", {
|
|
3325
3424
|
mode: 384
|
|
3326
3425
|
});
|
|
3327
3426
|
for (const host of HOSTS_REQUIRING_TRUST) {
|
|
3328
3427
|
const file = pushCheckAwaitingFile(environment, host);
|
|
3329
|
-
if (awaiting(marks[key(host, "push-check")]))
|
|
3428
|
+
if (awaiting(marks[key(host, "push-check")])) writeFileSync6(file, "", { mode: 384 });
|
|
3330
3429
|
else forget(file);
|
|
3331
3430
|
}
|
|
3332
3431
|
} catch {
|
|
@@ -3460,6 +3559,14 @@ async function runGuidance(options) {
|
|
|
3460
3559
|
return 0;
|
|
3461
3560
|
}
|
|
3462
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
|
+
});
|
|
3463
3570
|
const loaded = await loadGuidance({
|
|
3464
3571
|
agent: agents[0],
|
|
3465
3572
|
environment: options.environment,
|
|
@@ -3467,12 +3574,11 @@ async function runGuidance(options) {
|
|
|
3467
3574
|
hook: options.hook,
|
|
3468
3575
|
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
3469
3576
|
});
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
});
|
|
3577
|
+
startSelfUpdateIfDue(newerVersionPublished(), {
|
|
3578
|
+
environment: options.environment,
|
|
3579
|
+
...options.startUpdate ? { run: options.startUpdate } : {},
|
|
3580
|
+
...options.now ? { now: options.now } : {}
|
|
3581
|
+
});
|
|
3476
3582
|
const sessionId = identifier(record.session_id);
|
|
3477
3583
|
const agentId = identifier(record.agent_id);
|
|
3478
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/headless-agent.d.ts
CHANGED
|
@@ -133,6 +133,7 @@ export declare function verifyLogin(client: HeadlessClient, options: Readonly<{
|
|
|
133
133
|
cwd: string;
|
|
134
134
|
spawn?: SpawnLike;
|
|
135
135
|
killGroup?: KillGroup;
|
|
136
|
+
deadline?: number;
|
|
136
137
|
}>): Promise<LoginCheck>;
|
|
137
138
|
/**
|
|
138
139
|
* The client to run, or none. `auto` tries the hook's own client first, then
|
|
@@ -144,6 +145,7 @@ export declare function resolveClient(requested: HeadlessClient | "auto", option
|
|
|
144
145
|
prefer?: HeadlessClient;
|
|
145
146
|
spawn?: SpawnLike;
|
|
146
147
|
killGroup?: KillGroup;
|
|
148
|
+
deadline?: number;
|
|
147
149
|
}>): Promise<LoginCheck | undefined>;
|
|
148
150
|
/**
|
|
149
151
|
* The last JSON object in a piece of text.
|
package/dist/headless-agent.js
CHANGED
|
@@ -99,13 +99,15 @@ export function codexArguments(input) {
|
|
|
99
99
|
* dropped, the same way the compatibility canary treats them.
|
|
100
100
|
*/
|
|
101
101
|
export async function verifyLogin(client, options) {
|
|
102
|
+
if (options.deadline !== undefined && Date.now() >= options.deadline)
|
|
103
|
+
return { client, verified: false };
|
|
102
104
|
const env = agentEnvironment(options.env);
|
|
103
105
|
const run = await runOwnedProcess({
|
|
104
106
|
command: clientBinary(client, options.env),
|
|
105
107
|
args: client === "claude" ? ["auth", "status", "--json"] : ["login", "status"],
|
|
106
108
|
cwd: options.cwd,
|
|
107
109
|
env,
|
|
108
|
-
timeoutMs: 15_000,
|
|
110
|
+
timeoutMs: Math.max(1, Math.min(15_000, (options.deadline ?? Number.POSITIVE_INFINITY) - Date.now())),
|
|
109
111
|
maxOutputBytes: 64 * 1024,
|
|
110
112
|
...(options.spawn === undefined ? {} : { spawn: options.spawn }),
|
|
111
113
|
...(options.killGroup === undefined ? {} : { killGroup: options.killGroup }),
|
|
@@ -136,6 +138,8 @@ export async function resolveClient(requested, options) {
|
|
|
136
138
|
? ["codex", "claude"]
|
|
137
139
|
: ["claude", "codex"];
|
|
138
140
|
for (const client of order) {
|
|
141
|
+
if (options.deadline !== undefined && Date.now() >= options.deadline)
|
|
142
|
+
return undefined;
|
|
139
143
|
const check = await verifyLogin(client, options);
|
|
140
144
|
if (check.verified)
|
|
141
145
|
return check;
|