balladeer 1.0.6 → 1.0.7
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 +2 -0
- package/dist/cli.js +39 -1
- package/dist/commands/guidance.d.ts +17 -0
- package/dist/commands/guidance.js +109 -0
- package/dist/commands/mcp.js +9 -0
- 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 +2972 -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/wire.d.ts +8 -2
- package/dist/wire.js +1 -1
- package/package.json +4 -3
package/dist/guidance.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { CLIENT_HEADER, CLIENT_HEADER_VALUE } from "./wire.js";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { configHome } from "./store.js";
|
|
6
|
+
export const GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
|
|
7
|
+
const HEX = /^[a-f0-9]{64}$/;
|
|
8
|
+
const 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;
|
|
9
|
+
const MAX_BYTES = 40_000;
|
|
10
|
+
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
11
|
+
function object(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
export function parseGuidance(value, agent) {
|
|
15
|
+
if (!object(value) ||
|
|
16
|
+
Object.keys(value).sort().join(",") !==
|
|
17
|
+
"captureStyle,digest,instructions,repositoryId,revision,schemaVersion,workspaceId" ||
|
|
18
|
+
value.schemaVersion !== 1 ||
|
|
19
|
+
value.workspaceId !== agent.workspaceId ||
|
|
20
|
+
value.repositoryId !== agent.repositoryId ||
|
|
21
|
+
typeof value.revision !== "string" ||
|
|
22
|
+
!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(value.revision) ||
|
|
23
|
+
typeof value.digest !== "string" ||
|
|
24
|
+
!HEX.test(value.digest) ||
|
|
25
|
+
!["quiet", "thorough", "off"].includes(String(value.captureStyle)) ||
|
|
26
|
+
typeof value.instructions !== "string" ||
|
|
27
|
+
value.instructions.length === 0 ||
|
|
28
|
+
value.instructions.length > 8_000 ||
|
|
29
|
+
sha(value.instructions) !== value.digest) {
|
|
30
|
+
throw new Error("invalid_guidance");
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export function guidanceEndpoint(agent) {
|
|
35
|
+
const url = new URL(agent.mcpUrl);
|
|
36
|
+
const expected = new URL(agent.controlPlane);
|
|
37
|
+
if (url.origin !== expected.origin ||
|
|
38
|
+
url.protocol !== "https:" ||
|
|
39
|
+
url.pathname !== "/api/mcp" ||
|
|
40
|
+
url.username ||
|
|
41
|
+
url.password ||
|
|
42
|
+
url.hash ||
|
|
43
|
+
!UUID.test(agent.workspaceId) ||
|
|
44
|
+
!UUID.test(agent.repositoryId))
|
|
45
|
+
throw new Error("invalid_guidance_scope");
|
|
46
|
+
for (const [key, value] of [
|
|
47
|
+
["workspaceId", agent.workspaceId],
|
|
48
|
+
["repositoryId", agent.repositoryId],
|
|
49
|
+
]) {
|
|
50
|
+
if (url.searchParams.getAll(key).length > 1 ||
|
|
51
|
+
(url.searchParams.has(key) && url.searchParams.get(key) !== value))
|
|
52
|
+
throw new Error("invalid_guidance_scope");
|
|
53
|
+
url.searchParams.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
url.pathname = "/api/agent-guidance";
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
function readJson(path) {
|
|
59
|
+
let fd;
|
|
60
|
+
try {
|
|
61
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
62
|
+
const stat = fstatSync(fd);
|
|
63
|
+
if (!stat.isFile() || stat.size > MAX_BYTES || (stat.mode & 0o077) !== 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
return JSON.parse(readFileSync(fd, "utf8"));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (fd !== undefined)
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function privateDirectory(path) {
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(path, { mode: 0o700 });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* Existing private directory is checked below. */
|
|
81
|
+
}
|
|
82
|
+
const stat = lstatSync(path);
|
|
83
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0)
|
|
84
|
+
throw new Error("cache_unavailable");
|
|
85
|
+
}
|
|
86
|
+
function atomicJson(path, value) {
|
|
87
|
+
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: "wx", mode: 0o600 });
|
|
90
|
+
renameSync(temporary, path);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
try {
|
|
94
|
+
unlinkSync(temporary);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* Renamed or never created. */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function guidanceScopeKey(agent) {
|
|
102
|
+
return sha(JSON.stringify([
|
|
103
|
+
new URL(agent.controlPlane).origin,
|
|
104
|
+
sha(agent.token),
|
|
105
|
+
agent.workspaceId,
|
|
106
|
+
agent.repositoryId,
|
|
107
|
+
1,
|
|
108
|
+
]));
|
|
109
|
+
}
|
|
110
|
+
function cacheDirectory(environment) {
|
|
111
|
+
const home = configHome(environment);
|
|
112
|
+
privateDirectory(home);
|
|
113
|
+
const directory = join(home, "guidance-v1");
|
|
114
|
+
privateDirectory(directory);
|
|
115
|
+
return directory;
|
|
116
|
+
}
|
|
117
|
+
function cached(directory, scopeKey, agent) {
|
|
118
|
+
try {
|
|
119
|
+
const pointer = readJson(join(directory, `${scopeKey}.current.json`));
|
|
120
|
+
if (!object(pointer) || typeof pointer.key !== "string" || !HEX.test(pointer.key))
|
|
121
|
+
return undefined;
|
|
122
|
+
const body = readJson(join(directory, `${pointer.key}.body.json`));
|
|
123
|
+
if (!object(body) || typeof body.etag !== "string" || !/^"[a-f0-9]{64}"$/.test(body.etag))
|
|
124
|
+
return undefined;
|
|
125
|
+
const document = parseGuidance(body.document, agent);
|
|
126
|
+
if (pointer.key !== sha(JSON.stringify([scopeKey, document.revision])))
|
|
127
|
+
return undefined;
|
|
128
|
+
return { document, etag: body.etag, key: pointer.key };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function boundedBody(response) {
|
|
135
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
136
|
+
if (declared > MAX_BYTES)
|
|
137
|
+
throw new Error("guidance_oversized");
|
|
138
|
+
const reader = response.body?.getReader();
|
|
139
|
+
if (!reader)
|
|
140
|
+
throw new Error("guidance_empty");
|
|
141
|
+
const buffers = [];
|
|
142
|
+
let bytes = 0;
|
|
143
|
+
try {
|
|
144
|
+
while (true) {
|
|
145
|
+
const part = await reader.read();
|
|
146
|
+
if (part.done)
|
|
147
|
+
break;
|
|
148
|
+
bytes += part.value.byteLength;
|
|
149
|
+
if (bytes > MAX_BYTES)
|
|
150
|
+
throw new Error("guidance_oversized");
|
|
151
|
+
buffers.push(part.value);
|
|
152
|
+
}
|
|
153
|
+
return JSON.parse(Buffer.concat(buffers).toString("utf8"));
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await reader.cancel().catch(() => undefined);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Never returns stale instructions: even a cache hit requires a fresh scoped server response. */
|
|
160
|
+
export async function loadGuidance(input) {
|
|
161
|
+
const scopeKey = guidanceScopeKey(input.agent);
|
|
162
|
+
let directory;
|
|
163
|
+
try {
|
|
164
|
+
directory = cacheDirectory(input.environment);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* Cache is optional. */
|
|
168
|
+
}
|
|
169
|
+
const previous = directory ? cached(directory, scopeKey, input.agent) : undefined;
|
|
170
|
+
const abort = new AbortController();
|
|
171
|
+
let timer;
|
|
172
|
+
try {
|
|
173
|
+
const task = async () => {
|
|
174
|
+
const response = await (input.fetchImpl ?? fetch)(guidanceEndpoint(input.agent), {
|
|
175
|
+
method: "GET",
|
|
176
|
+
redirect: "manual",
|
|
177
|
+
signal: abort.signal,
|
|
178
|
+
headers: {
|
|
179
|
+
authorization: `Bearer ${input.agent.token}`,
|
|
180
|
+
[CLIENT_HEADER]: CLIENT_HEADER_VALUE,
|
|
181
|
+
accept: "application/json",
|
|
182
|
+
...(previous ? { "if-none-match": previous.etag } : {}),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
if (response.status === 304) {
|
|
186
|
+
if (!previous ||
|
|
187
|
+
response.headers.get("etag") !== previous.etag ||
|
|
188
|
+
response.headers.get("x-balladeer-guidance-revision") !== previous.document.revision) {
|
|
189
|
+
throw new Error("invalid_guidance_revalidation");
|
|
190
|
+
}
|
|
191
|
+
return { status: "revalidated", document: previous.document, scopeKey };
|
|
192
|
+
}
|
|
193
|
+
if (response.status !== 200) {
|
|
194
|
+
await response.body?.cancel();
|
|
195
|
+
throw new Error("guidance_unavailable");
|
|
196
|
+
}
|
|
197
|
+
const document = parseGuidance(await boundedBody(response), input.agent);
|
|
198
|
+
const etag = response.headers.get("etag");
|
|
199
|
+
if (!etag ||
|
|
200
|
+
!/^"[a-f0-9]{64}"$/.test(etag) ||
|
|
201
|
+
response.headers.get("x-balladeer-guidance-revision") !== document.revision)
|
|
202
|
+
throw new Error("invalid_guidance_headers");
|
|
203
|
+
// Do not let a late, nonconforming transport populate cache after the deadline.
|
|
204
|
+
if (abort.signal.aborted)
|
|
205
|
+
throw new Error("guidance_deadline");
|
|
206
|
+
if (directory) {
|
|
207
|
+
try {
|
|
208
|
+
const key = sha(JSON.stringify([scopeKey, document.revision]));
|
|
209
|
+
atomicJson(join(directory, `${key}.body.json`), { document, etag });
|
|
210
|
+
atomicJson(join(directory, `${scopeKey}.current.json`), { key });
|
|
211
|
+
if (previous && previous.key !== key) {
|
|
212
|
+
try {
|
|
213
|
+
unlinkSync(join(directory, `${previous.key}.body.json`));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
/* Concurrent cache is optional. */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* Fresh guidance remains usable without disk caching. */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { status: "fresh", document, scopeKey };
|
|
225
|
+
};
|
|
226
|
+
return await Promise.race([
|
|
227
|
+
task(),
|
|
228
|
+
new Promise((_, reject) => {
|
|
229
|
+
timer = setTimeout(() => {
|
|
230
|
+
abort.abort();
|
|
231
|
+
reject(new Error("guidance_deadline"));
|
|
232
|
+
}, Math.min(input.timeoutMs ?? 1000, 1000));
|
|
233
|
+
}),
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
abort.abort();
|
|
238
|
+
return { status: "unavailable", scopeKey };
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
if (timer)
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Writes context first, then records emission metadata; this never claims model receipt. */
|
|
246
|
+
export function recordGuidanceContext(input, write) {
|
|
247
|
+
const always = input.event !== "UserPromptSubmit" || input.load.status === "unavailable" || !input.sessionId;
|
|
248
|
+
const contextKey = sha(JSON.stringify([
|
|
249
|
+
input.scopeKey,
|
|
250
|
+
input.hook,
|
|
251
|
+
input.sessionId ?? randomBytes(16).toString("hex"),
|
|
252
|
+
input.agentId ?? "root",
|
|
253
|
+
]));
|
|
254
|
+
const document = input.load.document;
|
|
255
|
+
let path;
|
|
256
|
+
let emitted = true;
|
|
257
|
+
try {
|
|
258
|
+
path = join(cacheDirectory(input.environment), `${contextKey}.context.json`);
|
|
259
|
+
const previous = readJson(path);
|
|
260
|
+
emitted =
|
|
261
|
+
always ||
|
|
262
|
+
!object(previous) ||
|
|
263
|
+
previous.status === "unavailable" ||
|
|
264
|
+
previous.digest !== document?.digest ||
|
|
265
|
+
previous.revision !== document?.revision ||
|
|
266
|
+
(previous.runtimeGeneration ?? null) !== (input.runtimeGeneration ?? null);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
/* Cache failure requires emission. */
|
|
270
|
+
}
|
|
271
|
+
if (!emitted)
|
|
272
|
+
return false;
|
|
273
|
+
write();
|
|
274
|
+
if (path) {
|
|
275
|
+
try {
|
|
276
|
+
atomicJson(path, {
|
|
277
|
+
schemaVersion: 1,
|
|
278
|
+
contextKey,
|
|
279
|
+
event: input.event,
|
|
280
|
+
status: input.load.status,
|
|
281
|
+
digest: document?.digest ?? null,
|
|
282
|
+
revision: document?.revision ?? null,
|
|
283
|
+
runtimeGeneration: input.runtimeGeneration ?? null,
|
|
284
|
+
emitted: true,
|
|
285
|
+
emittedAt: new Date(input.now ?? Date.now()).toISOString(),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
/* An unwritten receipt causes another emission, never a skipped one. */
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
}
|
package/dist/wire.d.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export declare const CLI_VERSION = "1.0.
|
|
8
|
+
export declare const CLI_VERSION = "1.0.7";
|
|
9
9
|
export declare const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export declare const CLIENT_HEADER = "x-balladeer-client";
|
|
11
|
-
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.
|
|
11
|
+
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.7";
|
|
12
12
|
export declare const DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
13
13
|
export type DelegatedScope = "repository:enroll" | "agent:issue" | "ci:connect" | "workspace:invite" | "candidate:propose";
|
|
14
14
|
export declare const DELEGATED_SCOPES: readonly DelegatedScope[];
|
|
@@ -222,6 +222,12 @@ export type ClaudeDesktopStep = Readonly<{
|
|
|
222
222
|
reason?: string;
|
|
223
223
|
}>;
|
|
224
224
|
export type JsonStep = Readonly<{
|
|
225
|
+
step: "guidance_loader";
|
|
226
|
+
status: "installed_needs_host_trust" | "installed_trust_unverified" | "unavailable" | "not_applicable";
|
|
227
|
+
changed: boolean;
|
|
228
|
+
hosts: readonly ("codex" | "claude")[];
|
|
229
|
+
reason?: string;
|
|
230
|
+
}> | Readonly<{
|
|
225
231
|
step: "explain";
|
|
226
232
|
version: string;
|
|
227
233
|
}> | Readonly<{
|
package/dist/wire.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export const CLI_VERSION = "1.0.
|
|
8
|
+
export const CLI_VERSION = "1.0.7";
|
|
9
9
|
export const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export const CLIENT_HEADER = "x-balladeer-client";
|
|
11
11
|
export const CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "balladeer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Set up Balladeer from your terminal, or from a coding agent's.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
"node": ">=22.0.0"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"build": "tsc -p tsconfig.json && node build-guidance.mjs",
|
|
25
25
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"typescript": "5.9.3"
|
|
28
|
+
"typescript": "5.9.3",
|
|
29
|
+
"esbuild": "0.25.12"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@iarna/toml": "2.2.5"
|