balladeer 1.0.6 → 1.0.8
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 +21 -4
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +95 -6
- package/dist/commands/guidance.d.ts +19 -0
- package/dist/commands/guidance.js +136 -0
- package/dist/commands/mcp.js +52 -1
- package/dist/commands/propose.js +7 -2
- package/dist/commands/setup.js +26 -3
- package/dist/conventions.d.ts +1 -1
- package/dist/conventions.js +1 -1
- package/dist/copy.d.ts +7 -5
- package/dist/copy.js +78 -85
- package/dist/guidance-hook.mjs +3114 -0
- package/dist/guidance-install.d.ts +29 -0
- package/dist/guidance-install.js +420 -0
- package/dist/guidance-runtime-entry.d.ts +1 -0
- package/dist/guidance-runtime-entry.js +2 -0
- package/dist/guidance-runtime.d.ts +15 -0
- package/dist/guidance-runtime.js +149 -0
- package/dist/guidance.d.ts +39 -0
- package/dist/guidance.js +293 -0
- package/dist/install.d.ts +2 -0
- package/dist/install.js +7 -1
- package/dist/quiet.d.ts +25 -0
- package/dist/quiet.js +73 -0
- package/dist/user-scope.d.ts +55 -0
- package/dist/user-scope.js +234 -0
- package/dist/wire.d.ts +8 -2
- package/dist/wire.js +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -41,10 +41,11 @@ To upgrade the executable and refresh in one command, run `npx -y balladeer@late
|
|
|
41
41
|
|
|
42
42
|
Run `balladeer setup --refresh` to refresh repository instructions without replacing other tools'
|
|
43
43
|
settings. For Codex, keep `--client codex`: `balladeer setup --client codex --refresh`. Existing
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
policy updates arrive through the project guidance loader after its initial activation. Installing a
|
|
45
|
+
package alone does not prove the host loaded that guidance. Refresh can contact npm and write the
|
|
46
|
+
local toolchain when ensuring the executable is installed; it does not perform a Balladeer authority
|
|
47
|
+
action. The repository MCP entry continues to use `npx -y balladeer@latest mcp` so native tools can
|
|
48
|
+
pick up published updates.
|
|
48
49
|
|
|
49
50
|
Automatic installation supports macOS/Linux with zsh, bash or sh. It uses the active npm global
|
|
50
51
|
prefix or a writable personal prefix; when needed, it adds a bounded PATH block to your shell
|
|
@@ -79,6 +80,22 @@ is refused, authorized MCP reads, coding and explicitly requested capture may co
|
|
|
79
80
|
session ID. Do not invent an ID or trailer. This does not fix missing credentials, installation or
|
|
80
81
|
authorization failures, and does not guarantee every CLI command works in every sandbox.
|
|
81
82
|
|
|
83
|
+
## Current project guidance
|
|
84
|
+
|
|
85
|
+
Version 1.0.7 introduces a stable instruction block and project-local guidance hooks. Setup writes
|
|
86
|
+
Codex hooks in the connected project's `.codex/config.toml`, or Claude Code hooks in its
|
|
87
|
+
`.claude/settings.json`. It never installs global hooks. The loader checks that the current
|
|
88
|
+
project's MCP entry matches the requested repository and deployment before reading credentials or
|
|
89
|
+
fetching guidance. An unrelated repository remains untouched.
|
|
90
|
+
|
|
91
|
+
The next MCP launch can migrate recognized older Balladeer blocks; customized or conflicting blocks
|
|
92
|
+
are left for an explicit repair. Review your coding host's normal hook trust prompt once. Project
|
|
93
|
+
trust alone does not prove the hook is trusted or running. After activation, guidance is fetched at
|
|
94
|
+
supported work boundaries, so policy revisions and rollbacks do not require another package
|
|
95
|
+
installation or repository edit. A fetch failure keeps ordinary work moving and suppresses
|
|
96
|
+
unsolicited capture until the current workspace mode is available. A delivery receipt proves context
|
|
97
|
+
was emitted, not that an agent followed it.
|
|
98
|
+
|
|
82
99
|
## If npm cannot start the bootstrap
|
|
83
100
|
|
|
84
101
|
An EPERM or EACCES error naming npm's cache happens before Balladeer starts. Use an allowed writable
|
package/dist/cli.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ type Parsed = Readonly<{
|
|
|
19
19
|
*/
|
|
20
20
|
claudeDesktop: boolean | undefined;
|
|
21
21
|
repo: string | undefined;
|
|
22
|
+
repoScope: boolean;
|
|
22
23
|
file: string | undefined;
|
|
23
24
|
repository: string | undefined;
|
|
24
25
|
/**
|
|
@@ -30,6 +31,8 @@ type Parsed = Readonly<{
|
|
|
30
31
|
* rather than quietly resolved to the last one.
|
|
31
32
|
*/
|
|
32
33
|
repositories: readonly string[];
|
|
34
|
+
hook: "codex" | "claude" | undefined;
|
|
35
|
+
event: "SessionStart" | "UserPromptSubmit" | "SubagentStart" | undefined;
|
|
33
36
|
owner: string | undefined;
|
|
34
37
|
/** `check-seals --install-hook`: write the optional pre-push hook. */
|
|
35
38
|
installHook: boolean;
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { runCheckSeals } from "./commands/check-seals.js";
|
|
|
7
7
|
import { runDiscover } from "./commands/discover.js";
|
|
8
8
|
import { runExplain } from "./commands/explain.js";
|
|
9
9
|
import { parseRole, runInvite } from "./commands/invite.js";
|
|
10
|
+
import { runGuidance } from "./commands/guidance.js";
|
|
10
11
|
import { runMcp } from "./commands/mcp.js";
|
|
11
12
|
import { runPrepare } from "./commands/prepare.js";
|
|
12
13
|
import { runPropose } from "./commands/propose.js";
|
|
@@ -17,15 +18,24 @@ import { runStatus } from "./commands/status.js";
|
|
|
17
18
|
import { runTouchMap } from "./commands/touch-map.js";
|
|
18
19
|
import { runWhoami } from "./commands/whoami.js";
|
|
19
20
|
import { runInstall } from "./install.js";
|
|
21
|
+
import { installUserScope, runningFromCheckout } from "./user-scope.js";
|
|
22
|
+
import { setQuiet } from "./quiet.js";
|
|
20
23
|
import { updateNotice } from "./currency.js";
|
|
21
24
|
import { StoreError, normalizeControlPlane } from "./store.js";
|
|
22
25
|
import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
23
26
|
const USAGE = `balladeer ${CLI_VERSION}
|
|
24
27
|
|
|
25
28
|
${CLI_INVOCATION} install [--json]
|
|
26
|
-
Install this exact release as the durable balladeer command,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
Install this exact release as the durable balladeer command, and register
|
|
30
|
+
Balladeer once for every Claude Code and Codex session on this machine:
|
|
31
|
+
it decides per folder, from the git remote, which connection applies.
|
|
32
|
+
Pairs nothing and changes no workspace, repository, or CI configuration.
|
|
33
|
+
|
|
34
|
+
${CLI_INVOCATION} quiet [--repo]
|
|
35
|
+
${CLI_INVOCATION} track [--repo]
|
|
36
|
+
In a folder Balladeer does not track, each session hears that once. quiet
|
|
37
|
+
stops it for this folder (or --repo, for every checkout of this
|
|
38
|
+
repository); track undoes it.
|
|
29
39
|
|
|
30
40
|
${CLI_INVOCATION} setup [--repository owner/name]... [--json] [--wait] [--control-plane <url>]
|
|
31
41
|
[--create-workspace <name>] [--choose-workspace] [--refresh] [--force]
|
|
@@ -170,6 +180,8 @@ export function parseArguments(argv) {
|
|
|
170
180
|
throw new StoreError("usage", `${command} needs one of: ${SUBCOMMANDS[command].join(", ")}.`);
|
|
171
181
|
}
|
|
172
182
|
}
|
|
183
|
+
let hook;
|
|
184
|
+
let event;
|
|
173
185
|
let json = false;
|
|
174
186
|
let wait = false;
|
|
175
187
|
let refresh = false;
|
|
@@ -185,6 +197,7 @@ export function parseArguments(argv) {
|
|
|
185
197
|
let runner;
|
|
186
198
|
let controlPlane;
|
|
187
199
|
let repo;
|
|
200
|
+
let repoScope = false;
|
|
188
201
|
let file;
|
|
189
202
|
let owner;
|
|
190
203
|
let createWorkspace;
|
|
@@ -216,7 +229,21 @@ export function parseArguments(argv) {
|
|
|
216
229
|
}
|
|
217
230
|
const [name, ...rest] = flag.split("=");
|
|
218
231
|
const inline = rest.length > 0 ? rest.join("=") : undefined;
|
|
219
|
-
if (name === "--
|
|
232
|
+
if (name === "--hook") {
|
|
233
|
+
const requested = value("--hook", inline);
|
|
234
|
+
if (requested !== "codex" && requested !== "claude")
|
|
235
|
+
throw new StoreError("usage", "--hook needs codex or claude.");
|
|
236
|
+
hook = requested;
|
|
237
|
+
}
|
|
238
|
+
else if (name === "--event") {
|
|
239
|
+
const requested = value("--event", inline);
|
|
240
|
+
if (requested !== "SessionStart" &&
|
|
241
|
+
requested !== "UserPromptSubmit" &&
|
|
242
|
+
requested !== "SubagentStart")
|
|
243
|
+
throw new StoreError("usage", "--event needs SessionStart, UserPromptSubmit or SubagentStart.");
|
|
244
|
+
event = requested;
|
|
245
|
+
}
|
|
246
|
+
else if (name === "--json")
|
|
220
247
|
json = true;
|
|
221
248
|
else if (name === "--again")
|
|
222
249
|
again = true;
|
|
@@ -250,6 +277,8 @@ export function parseArguments(argv) {
|
|
|
250
277
|
claudeDesktop = false;
|
|
251
278
|
else if (name === "--control-plane")
|
|
252
279
|
controlPlane = value("--control-plane", inline);
|
|
280
|
+
else if (name === "--repo" && (command === "quiet" || command === "track"))
|
|
281
|
+
repoScope = true;
|
|
253
282
|
else if (name === "--repo")
|
|
254
283
|
repo = value("--repo", inline);
|
|
255
284
|
else if (name === "--file")
|
|
@@ -334,6 +363,10 @@ export function parseArguments(argv) {
|
|
|
334
363
|
if (command !== "setup" && repositories.length > 1) {
|
|
335
364
|
throw new StoreError("usage", `${command} acts on one repository. Name --repository once, or run it again for the other.`);
|
|
336
365
|
}
|
|
366
|
+
if (hook !== undefined && command !== "guidance")
|
|
367
|
+
throw new StoreError("usage", "--hook belongs to guidance.");
|
|
368
|
+
if (event !== undefined && command !== "guidance")
|
|
369
|
+
throw new StoreError("usage", "--event belongs to guidance.");
|
|
337
370
|
const chosen = controlPlane ?? process.env.BALLADEER_CONTROL_PLANE?.trim() ?? DEFAULT_CONTROL_PLANE;
|
|
338
371
|
return {
|
|
339
372
|
command,
|
|
@@ -347,9 +380,12 @@ export function parseArguments(argv) {
|
|
|
347
380
|
existingOnly,
|
|
348
381
|
claudeDesktop,
|
|
349
382
|
repo,
|
|
383
|
+
repoScope,
|
|
350
384
|
file,
|
|
351
385
|
repository: repositories[0],
|
|
352
386
|
repositories,
|
|
387
|
+
hook,
|
|
388
|
+
event,
|
|
353
389
|
owner,
|
|
354
390
|
installHook,
|
|
355
391
|
fresh,
|
|
@@ -386,8 +422,46 @@ async function dispatch(parsed, write) {
|
|
|
386
422
|
switch (parsed.command) {
|
|
387
423
|
case "explain":
|
|
388
424
|
return runExplain(write, parsed.controlPlane);
|
|
389
|
-
case "install":
|
|
390
|
-
|
|
425
|
+
case "install": {
|
|
426
|
+
// Once per laptop: the same server and hook, registered where every
|
|
427
|
+
// session on this machine reads them, deciding per folder at run time.
|
|
428
|
+
// Registered first and independently: the entries run through npx and
|
|
429
|
+
// need no durable command, so a blocked PATH must not keep a laptop
|
|
430
|
+
// from getting them.
|
|
431
|
+
const writes = installUserScope({
|
|
432
|
+
environment: process.env,
|
|
433
|
+
published: !runningFromCheckout(),
|
|
434
|
+
});
|
|
435
|
+
for (const w of writes)
|
|
436
|
+
if (!parsed.json)
|
|
437
|
+
write(`${w.status === "refused" ? "Not changed" : w.status === "written" ? "Registered" : "Already current"}: ${w.path}${w.reason ? ` (${w.reason})` : ""}\n`);
|
|
438
|
+
if (!parsed.json && writes.some((w) => w.status === "written"))
|
|
439
|
+
write("Balladeer now runs in every Claude Code" +
|
|
440
|
+
(writes.some((w) => w.host === "codex") ? " and Codex" : "") +
|
|
441
|
+
" session on this machine. In a folder of a connected repository it works as before; anywhere else it says once that the folder isn't tracked. Approve the new hook when your coding host asks.\n");
|
|
442
|
+
const code = runInstall({
|
|
443
|
+
environment: process.env,
|
|
444
|
+
json: parsed.json,
|
|
445
|
+
write,
|
|
446
|
+
extra: { userScope: writes },
|
|
447
|
+
});
|
|
448
|
+
return writes.some((w) => w.status === "refused") ? 4 : code;
|
|
449
|
+
}
|
|
450
|
+
case "quiet":
|
|
451
|
+
case "track": {
|
|
452
|
+
const scope = parsed.repoScope ? "repo" : "folder";
|
|
453
|
+
try {
|
|
454
|
+
const { key, changed } = setQuiet(process.cwd(), scope, parsed.command === "quiet", process.env);
|
|
455
|
+
write(parsed.command === "quiet"
|
|
456
|
+
? `${changed ? "Balladeer will stay quiet" : "Balladeer was already quiet"} in ${scope === "repo" ? `every checkout of ${key}` : key}. Run \`balladeer track${scope === "repo" ? " --repo" : ""}\` here to undo.\n`
|
|
457
|
+
: `${changed ? "Balladeer will speak again" : "Balladeer was not quiet"} in ${scope === "repo" ? `checkouts of ${key}` : key}.\n`);
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
462
|
+
return 4;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
391
465
|
case "setup":
|
|
392
466
|
return runSetup({
|
|
393
467
|
installCommand: () => {
|
|
@@ -552,6 +626,21 @@ async function dispatch(parsed, write) {
|
|
|
552
626
|
environment: process.env,
|
|
553
627
|
write,
|
|
554
628
|
});
|
|
629
|
+
case "guidance":
|
|
630
|
+
if (parsed.hook === undefined) {
|
|
631
|
+
write("guidance needs --hook codex or claude.\n");
|
|
632
|
+
return 4;
|
|
633
|
+
}
|
|
634
|
+
return runGuidance({
|
|
635
|
+
controlPlane: parsed.controlPlane,
|
|
636
|
+
hook: parsed.hook,
|
|
637
|
+
...(parsed.event === undefined ? {} : { event: parsed.event }),
|
|
638
|
+
...(parsed.repository === undefined ? {} : { repositoryId: parsed.repository }),
|
|
639
|
+
environment: process.env,
|
|
640
|
+
cwd: process.cwd(),
|
|
641
|
+
stdin: process.stdin,
|
|
642
|
+
write,
|
|
643
|
+
});
|
|
555
644
|
case "mcp":
|
|
556
645
|
return runMcp({
|
|
557
646
|
controlPlane: parsed.controlPlane,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type GuidanceEvent } from "../guidance.js";
|
|
2
|
+
/** What a session hears, once, in a folder Balladeer is not tracking. */
|
|
3
|
+
export declare function untrackedLine(remote: string): string;
|
|
4
|
+
export type GuidanceOptions = Readonly<{
|
|
5
|
+
controlPlane: string;
|
|
6
|
+
repositoryId?: string;
|
|
7
|
+
hook: "codex" | "claude";
|
|
8
|
+
event?: GuidanceEvent;
|
|
9
|
+
runtimeGeneration?: string;
|
|
10
|
+
environment: NodeJS.ProcessEnv;
|
|
11
|
+
cwd: string;
|
|
12
|
+
stdin: NodeJS.ReadableStream;
|
|
13
|
+
write: (text: string) => void;
|
|
14
|
+
error?: (text: string) => void;
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}>;
|
|
18
|
+
/** Hook stdout is bounded context only. Host prompts and transcript paths never leave this process. */
|
|
19
|
+
export declare function runGuidance(options: GuidanceOptions): Promise<number>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { validateProjectGuidanceScope } from "../guidance-install.js";
|
|
2
|
+
import { GUIDANCE_UNAVAILABLE, loadGuidance, recordGuidanceContext, } from "../guidance.js";
|
|
3
|
+
import { readCredentials } from "../store.js";
|
|
4
|
+
import { selectAgent } from "../agent.js";
|
|
5
|
+
import { repositoryHint } from "../repository.js";
|
|
6
|
+
import { isQuiet } from "../quiet.js";
|
|
7
|
+
import { PUBLISHED_SPECIFIER } from "../release.js";
|
|
8
|
+
/** What a session hears, once, in a folder Balladeer is not tracking. */
|
|
9
|
+
export function untrackedLine(remote) {
|
|
10
|
+
const here = remote === "unknown/unknown" ? "This folder" : `This folder (${remote})`;
|
|
11
|
+
return (`${here} isn't tracked by Balladeer, so promises can't be tracked from here. ` +
|
|
12
|
+
`To track it, run \`npx -y ${PUBLISHED_SPECIFIER} setup\` in this folder. ` +
|
|
13
|
+
`To stop this message here, run \`npx -y ${PUBLISHED_SPECIFIER} quiet\` (or \`quiet --repo\` for the whole repository).`);
|
|
14
|
+
}
|
|
15
|
+
function readInput(stream) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
const chunks = [];
|
|
19
|
+
const finish = (error) => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
stream.removeListener("data", data);
|
|
22
|
+
stream.removeListener("end", end);
|
|
23
|
+
stream.removeListener("error", failed);
|
|
24
|
+
stream.pause();
|
|
25
|
+
if (error)
|
|
26
|
+
reject(error);
|
|
27
|
+
else {
|
|
28
|
+
try {
|
|
29
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
reject(new Error("invalid_hook_input"));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const data = (chunk) => {
|
|
37
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
38
|
+
bytes += buffer.length;
|
|
39
|
+
if (bytes > 65_536)
|
|
40
|
+
finish(new Error("hook_input_oversized"));
|
|
41
|
+
else
|
|
42
|
+
chunks.push(buffer);
|
|
43
|
+
};
|
|
44
|
+
const end = () => finish();
|
|
45
|
+
const failed = () => finish(new Error("hook_input_unavailable"));
|
|
46
|
+
const timer = setTimeout(() => finish(new Error("hook_input_deadline")), 250);
|
|
47
|
+
stream.on("data", data);
|
|
48
|
+
stream.once("end", end);
|
|
49
|
+
stream.once("error", failed);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function eventFrom(input) {
|
|
53
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
54
|
+
return undefined;
|
|
55
|
+
const event = input.hook_event_name;
|
|
56
|
+
return event === "SessionStart" || event === "SubagentStart" || event === "UserPromptSubmit"
|
|
57
|
+
? event
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
function identifier(input) {
|
|
61
|
+
return typeof input === "string" && input.length > 0 && input.length <= 256 ? input : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Hook stdout is bounded context only. Host prompts and transcript paths never leave this process. */
|
|
64
|
+
export async function runGuidance(options) {
|
|
65
|
+
// A project hook names its repository and must match the checkout it sits
|
|
66
|
+
// in. A user-scope hook names none: the folder's remote decides, and a folder
|
|
67
|
+
// with no connection hears that it is untracked rather than nothing at all.
|
|
68
|
+
const projectScoped = options.repositoryId !== undefined;
|
|
69
|
+
if (projectScoped &&
|
|
70
|
+
!validateProjectGuidanceScope({
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
hook: options.hook,
|
|
73
|
+
repositoryId: options.repositoryId,
|
|
74
|
+
controlPlane: options.controlPlane,
|
|
75
|
+
}))
|
|
76
|
+
return 0;
|
|
77
|
+
let event = options.event ?? "SessionStart";
|
|
78
|
+
const emit = (context) => options.write(`${JSON.stringify({
|
|
79
|
+
hookSpecificOutput: { hookEventName: event, additionalContext: context },
|
|
80
|
+
})}\n`);
|
|
81
|
+
try {
|
|
82
|
+
const input = await readInput(options.stdin);
|
|
83
|
+
const parsedEvent = eventFrom(input);
|
|
84
|
+
if (!parsedEvent || (options.event && parsedEvent !== options.event))
|
|
85
|
+
throw new Error("invalid_hook_event");
|
|
86
|
+
event = parsedEvent;
|
|
87
|
+
const record = input;
|
|
88
|
+
const credentials = readCredentials(options.environment).agents;
|
|
89
|
+
const agents = projectScoped
|
|
90
|
+
? credentials.filter((agent) => agent.controlPlane === options.controlPlane &&
|
|
91
|
+
agent.repositoryId === options.repositoryId)
|
|
92
|
+
: (() => {
|
|
93
|
+
const selection = selectAgent(credentials, options.controlPlane, undefined, repositoryHint(options.cwd));
|
|
94
|
+
return selection.kind === "refused" ? [] : [selection.agent];
|
|
95
|
+
})();
|
|
96
|
+
if (agents.length !== 1 || !agents[0]) {
|
|
97
|
+
if (projectScoped)
|
|
98
|
+
throw new Error("guidance_connection_unavailable");
|
|
99
|
+
// Untracked. Said once, at session start, and never when silenced.
|
|
100
|
+
if (event === "SessionStart" && !isQuiet(options.cwd, options.environment))
|
|
101
|
+
emit(untrackedLine(repositoryHint(options.cwd)));
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
const loaded = await loadGuidance({
|
|
105
|
+
agent: agents[0],
|
|
106
|
+
environment: options.environment,
|
|
107
|
+
timeoutMs: 750,
|
|
108
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
109
|
+
});
|
|
110
|
+
const sessionId = identifier(record.session_id);
|
|
111
|
+
const agentId = identifier(record.agent_id);
|
|
112
|
+
recordGuidanceContext({
|
|
113
|
+
environment: options.environment,
|
|
114
|
+
scopeKey: loaded.scopeKey,
|
|
115
|
+
hook: options.hook,
|
|
116
|
+
event,
|
|
117
|
+
...(options.runtimeGeneration ? { runtimeGeneration: options.runtimeGeneration } : {}),
|
|
118
|
+
load: loaded,
|
|
119
|
+
...(sessionId ? { sessionId } : {}),
|
|
120
|
+
...(agentId ? { agentId } : {}),
|
|
121
|
+
...(options.now ? { now: options.now() } : {}),
|
|
122
|
+
}, () => emit(loaded.document
|
|
123
|
+
? `Current Balladeer guidance (${loaded.document.revision}). This supersedes earlier Balladeer workflow and capture guidance in this context.\n\n${loaded.document.instructions}`
|
|
124
|
+
: GUIDANCE_UNAVAILABLE));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// A fixed fallback contains neither raw input nor credential/error details. Work continues.
|
|
128
|
+
try {
|
|
129
|
+
emit(GUIDANCE_UNAVAILABLE);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* Closed stdout cannot block the host. */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
+
import { installGuidanceLoader } from "../guidance-install.js";
|
|
2
3
|
import { callAgentTool, forwarderHeaders, noAgentCredentialSentence, safeJson, selectAgent, structuredString, updateLine, } from "../agent.js";
|
|
3
4
|
import { noteServerVersion, updateNotice } from "../currency.js";
|
|
4
5
|
import { repositoryHint } from "../repository.js";
|
|
6
|
+
import { CLI_VERSION } from "../wire.js";
|
|
7
|
+
import { untrackedLine } from "./guidance.js";
|
|
5
8
|
import { StoreError, readCredentials } from "../store.js";
|
|
6
9
|
// The three commands that use an agent connection select and address it through
|
|
7
10
|
// one module. These are re-exported because this is where the forwarder's
|
|
@@ -126,6 +129,39 @@ function frameId(line) {
|
|
|
126
129
|
}
|
|
127
130
|
return null;
|
|
128
131
|
}
|
|
132
|
+
/** The smallest server a host accepts: initialize, an empty tools list, ping. */
|
|
133
|
+
async function serveUntracked(options, instructions) {
|
|
134
|
+
const lines = createInterface({ input: options.stdin, crlfDelay: Infinity });
|
|
135
|
+
for await (const line of lines) {
|
|
136
|
+
if (line.trim().length === 0)
|
|
137
|
+
continue;
|
|
138
|
+
let request;
|
|
139
|
+
try {
|
|
140
|
+
request = JSON.parse(line);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (request.id === undefined)
|
|
146
|
+
continue; // a notification
|
|
147
|
+
const result = request.method === "initialize"
|
|
148
|
+
? {
|
|
149
|
+
protocolVersion: request.params?.protocolVersion ?? "2025-06-18",
|
|
150
|
+
capabilities: { tools: {} },
|
|
151
|
+
serverInfo: { name: "balladeer", version: CLI_VERSION },
|
|
152
|
+
instructions,
|
|
153
|
+
}
|
|
154
|
+
: request.method === "tools/list"
|
|
155
|
+
? { tools: [] }
|
|
156
|
+
: request.method === "ping"
|
|
157
|
+
? {}
|
|
158
|
+
: undefined;
|
|
159
|
+
options.write(JSON.stringify(result === undefined
|
|
160
|
+
? { jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } }
|
|
161
|
+
: { jsonrpc: "2.0", id: request.id, result }) + "\n");
|
|
162
|
+
}
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
129
165
|
export async function runMcp(options) {
|
|
130
166
|
let credentials;
|
|
131
167
|
try {
|
|
@@ -143,9 +179,24 @@ export async function runMcp(options) {
|
|
|
143
179
|
options.error(selection.missingFor === undefined
|
|
144
180
|
? `${selection.reason} Run setup in this repository, or issue the connection at ${options.controlPlane}/setup.\n`
|
|
145
181
|
: `${noAgentCredentialSentence(selection.missingFor)}\n`);
|
|
146
|
-
|
|
182
|
+
// A project entry named its repository and got it wrong: that is an error
|
|
183
|
+
// the host should show. A user-scope entry named none and simply landed in
|
|
184
|
+
// a folder nobody tracks: that is ordinary, and a server that dies here
|
|
185
|
+
// shows up red in every untracked folder on the laptop. Answer the host
|
|
186
|
+
// with an empty catalog and the one sentence the hook also says.
|
|
187
|
+
if (options.repositoryId !== undefined)
|
|
188
|
+
return 4;
|
|
189
|
+
return serveUntracked(options, untrackedLine(repositoryHint(options.cwd)));
|
|
147
190
|
}
|
|
148
191
|
const { agent } = selection;
|
|
192
|
+
const guidanceInstall = installGuidanceLoader({
|
|
193
|
+
environment: options.environment,
|
|
194
|
+
cwd: options.cwd,
|
|
195
|
+
repositoryId: agent.repositoryId,
|
|
196
|
+
controlPlane: agent.controlPlane,
|
|
197
|
+
});
|
|
198
|
+
if (guidanceInstall.status !== "not_applicable")
|
|
199
|
+
options.error(`Balladeer guidance loader: ${guidanceInstall.status}${guidanceInstall.reason ? ` (${guidanceInstall.reason})` : ""}. New project hooks may need host trust; no trust is assumed.\n`);
|
|
149
200
|
let saidUpdate = false;
|
|
150
201
|
const lines = createInterface({ input: options.stdin, crlfDelay: Infinity });
|
|
151
202
|
for await (const line of lines) {
|
package/dist/commands/propose.js
CHANGED
|
@@ -123,8 +123,13 @@ export function readTeachBackFile(raw) {
|
|
|
123
123
|
reason: 'teachBack.wrongOutcome is missing. Say what going wrong looks like, in the words the person used and as a must-not: "a second payment must not go out". If no sentence of that shape exists, nothing could ever fail this promise, so do not propose it',
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
-
if (teachBack.leastSure !== undefined
|
|
127
|
-
|
|
126
|
+
if (teachBack.leastSure !== undefined ||
|
|
127
|
+
(teachBack.unresolvedQuestions !== undefined &&
|
|
128
|
+
(!Array.isArray(teachBack.unresolvedQuestions) || teachBack.unresolvedQuestions.length > 0))) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: "Outstanding questions are retired. Clarify material ambiguity with the person before writing, and put their actual answer in the meaning and cases. Nothing was sent.",
|
|
132
|
+
};
|
|
128
133
|
}
|
|
129
134
|
return {
|
|
130
135
|
ok: true,
|
package/dist/commands/setup.js
CHANGED
|
@@ -11,6 +11,7 @@ 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";
|
|
14
15
|
import { findLegacyInstall, legacyMessage } from "../legacy.js";
|
|
15
16
|
import { formatInstant } from "../local-time.js";
|
|
16
17
|
import { checkoutEntryPath, commandLine, runningFromRegistryInstall } from "../release.js";
|
|
@@ -932,13 +933,35 @@ function hostConfigFile(options) {
|
|
|
932
933
|
return options.client === "codex" ? CODEX_CONFIG_FILE : MCP_CONFIG_FILE;
|
|
933
934
|
}
|
|
934
935
|
function mergeHostConfig(options, root, repositoryId, published) {
|
|
935
|
-
const
|
|
936
|
+
const base = stdioEntry(repositoryId, published);
|
|
937
|
+
const entry = { ...base, args: [...base.args, "--control-plane", options.controlPlane] };
|
|
936
938
|
return options.client === "codex"
|
|
937
|
-
? mergeCodexConfig(root,
|
|
939
|
+
? mergeCodexConfig(root, entry, options.controlPlane)
|
|
938
940
|
: mergeMcpConfig(root, entry, options.controlPlane);
|
|
939
941
|
}
|
|
940
942
|
function writeHostConventions(options, root) {
|
|
941
|
-
|
|
943
|
+
const result = writeConventions(root, options.client === "codex" ? { file: "AGENTS.md" } : {});
|
|
944
|
+
if (result.kind === "written") {
|
|
945
|
+
const entry = options.client === "codex" ? readCodexEntry(root) : currentEntry(readMcpConfig(root));
|
|
946
|
+
const repositoryId = entryRepositoryId(entry);
|
|
947
|
+
if (repositoryId !== undefined) {
|
|
948
|
+
const loader = installGuidanceLoader({
|
|
949
|
+
environment: options.environment,
|
|
950
|
+
cwd: root,
|
|
951
|
+
repositoryId,
|
|
952
|
+
controlPlane: options.controlPlane,
|
|
953
|
+
host: options.client === "codex" ? "codex" : "claude",
|
|
954
|
+
});
|
|
955
|
+
emit(options, { step: "guidance_loader", ...loader });
|
|
956
|
+
say(options, loader.status === "installed_needs_host_trust" ||
|
|
957
|
+
loader.status === "installed_trust_unverified"
|
|
958
|
+
? "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."
|
|
959
|
+
: loader.status === "unavailable"
|
|
960
|
+
? `Project guidance loader needs repair (${loader.reason}). No active hook is claimed.`
|
|
961
|
+
: "No matching project guidance integration was found. No guidance hook was installed or activated.");
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return result;
|
|
942
965
|
}
|
|
943
966
|
function hostNextSteps(options) {
|
|
944
967
|
if (options.client === "codex")
|
package/dist/conventions.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare const CONVENTIONS_END = "<!-- balladeer:conventions:end -->";
|
|
|
27
27
|
* instructions marker instead. A bump here with no change to the block would
|
|
28
28
|
* rewrite twelve repositories to say exactly what they already said.
|
|
29
29
|
*/
|
|
30
|
-
export declare const CONVENTIONS_VERSION =
|
|
30
|
+
export declare const CONVENTIONS_VERSION = 20;
|
|
31
31
|
export declare function managedByLine(version: number): string;
|
|
32
32
|
/**
|
|
33
33
|
* Which version of the block a file already carries, or nothing when it carries
|
package/dist/conventions.js
CHANGED
|
@@ -31,7 +31,7 @@ export const CONVENTIONS_END = "<!-- balladeer:conventions:end -->";
|
|
|
31
31
|
* instructions marker instead. A bump here with no change to the block would
|
|
32
32
|
* rewrite twelve repositories to say exactly what they already said.
|
|
33
33
|
*/
|
|
34
|
-
export const CONVENTIONS_VERSION =
|
|
34
|
+
export const CONVENTIONS_VERSION = 20;
|
|
35
35
|
/** Both spellings: the stable marker, and the versioned one version 1 wrote. */
|
|
36
36
|
const START_MARKER = /<!-- balladeer:conventions:start(?: v(\d{1,4}))? -->/g;
|
|
37
37
|
const END_MARKER = /<!-- balladeer:conventions:end -->/g;
|