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