balladeer 1.0.17 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anchor-budget.d.ts +40 -0
- package/dist/anchor-budget.js +110 -0
- package/dist/anchoring.d.ts +241 -0
- package/dist/anchoring.js +766 -0
- package/dist/anchors-client.d.ts +62 -0
- package/dist/anchors-client.js +197 -0
- package/dist/anchors-schema.d.ts +85 -0
- package/dist/anchors-schema.js +246 -0
- package/dist/cli.js +5 -1
- package/dist/commands/anchor-usage.d.ts +6 -0
- package/dist/commands/anchor-usage.js +19 -0
- package/dist/commands/anchor.d.ts +127 -0
- package/dist/commands/anchor.js +303 -0
- package/dist/commands/judge.d.ts +69 -21
- package/dist/commands/judge.js +354 -89
- package/dist/commands/map.js +2 -0
- package/dist/guidance-hook.mjs +5 -2
- package/dist/guidance.d.ts +7 -0
- package/dist/guidance.js +10 -1
- package/dist/headless-agent.d.ts +19 -1
- package/dist/headless-agent.js +22 -5
- package/dist/judge-brief.d.ts +20 -2
- package/dist/judge-brief.js +26 -2
- package/dist/promise-meaning.d.ts +17 -0
- package/dist/promise-meaning.js +15 -6
- package/dist/risk/git.d.ts +3 -1
- package/dist/risk/git.js +3 -3
- package/dist/risk/graph-cache.d.ts +83 -0
- package/dist/risk/graph-cache.js +291 -0
- package/dist/risk/import-graph.d.ts +43 -0
- package/dist/risk/import-graph.js +88 -34
- package/dist/risk/pipeline.d.ts +13 -0
- package/dist/risk/pipeline.js +15 -4
- package/dist/wire.d.ts +18 -2
- package/dist/wire.js +1 -1
- package/package.json +7 -2
package/dist/guidance-hook.mjs
CHANGED
|
@@ -2152,7 +2152,7 @@ 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.18";
|
|
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";
|
|
@@ -2403,6 +2403,9 @@ function parseGuidance(value, agent) {
|
|
|
2403
2403
|
return value;
|
|
2404
2404
|
}
|
|
2405
2405
|
function guidanceEndpoint(agent) {
|
|
2406
|
+
return agentEndpoint(agent, "/api/agent-guidance");
|
|
2407
|
+
}
|
|
2408
|
+
function agentEndpoint(agent, pathname) {
|
|
2406
2409
|
const url = new URL(agent.mcpUrl);
|
|
2407
2410
|
const expected = new URL(agent.controlPlane);
|
|
2408
2411
|
if (url.origin !== expected.origin || url.protocol !== "https:" || url.pathname !== "/api/mcp" || url.username || url.password || url.hash || !UUID.test(agent.workspaceId) || !UUID.test(agent.repositoryId))
|
|
@@ -2415,7 +2418,7 @@ function guidanceEndpoint(agent) {
|
|
|
2415
2418
|
throw new Error("invalid_guidance_scope");
|
|
2416
2419
|
url.searchParams.set(key2, value);
|
|
2417
2420
|
}
|
|
2418
|
-
url.pathname =
|
|
2421
|
+
url.pathname = pathname;
|
|
2419
2422
|
return url;
|
|
2420
2423
|
}
|
|
2421
2424
|
function readJson(path) {
|
package/dist/guidance.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export type GuidanceEvent = "SessionStart" | "SubagentStart" | "UserPromptSubmit
|
|
|
12
12
|
export declare const GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
|
|
13
13
|
export declare function parseGuidance(value: unknown, agent: StoredAgent): GuidanceDocument;
|
|
14
14
|
export declare function guidanceEndpoint(agent: StoredAgent): URL;
|
|
15
|
+
/**
|
|
16
|
+
* A route of the server this repository's agent connection belongs to, scoped
|
|
17
|
+
* to its workspace and repository the way `/api/agent-guidance` is: same
|
|
18
|
+
* origin as the connection's MCP address, https only, and the scope in the
|
|
19
|
+
* query exactly once.
|
|
20
|
+
*/
|
|
21
|
+
export declare function agentEndpoint(agent: StoredAgent, pathname: string): URL;
|
|
15
22
|
export declare function guidanceScopeKey(agent: StoredAgent): string;
|
|
16
23
|
export type GuidanceLoad = Readonly<{
|
|
17
24
|
status: "fresh" | "revalidated" | "unavailable";
|
package/dist/guidance.js
CHANGED
|
@@ -34,6 +34,15 @@ export function parseGuidance(value, agent) {
|
|
|
34
34
|
return value;
|
|
35
35
|
}
|
|
36
36
|
export function guidanceEndpoint(agent) {
|
|
37
|
+
return agentEndpoint(agent, "/api/agent-guidance");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A route of the server this repository's agent connection belongs to, scoped
|
|
41
|
+
* to its workspace and repository the way `/api/agent-guidance` is: same
|
|
42
|
+
* origin as the connection's MCP address, https only, and the scope in the
|
|
43
|
+
* query exactly once.
|
|
44
|
+
*/
|
|
45
|
+
export function agentEndpoint(agent, pathname) {
|
|
37
46
|
const url = new URL(agent.mcpUrl);
|
|
38
47
|
const expected = new URL(agent.controlPlane);
|
|
39
48
|
if (url.origin !== expected.origin ||
|
|
@@ -54,7 +63,7 @@ export function guidanceEndpoint(agent) {
|
|
|
54
63
|
throw new Error("invalid_guidance_scope");
|
|
55
64
|
url.searchParams.set(key, value);
|
|
56
65
|
}
|
|
57
|
-
url.pathname =
|
|
66
|
+
url.pathname = pathname;
|
|
58
67
|
return url;
|
|
59
68
|
}
|
|
60
69
|
function readJson(path) {
|
package/dist/headless-agent.d.ts
CHANGED
|
@@ -49,6 +49,11 @@ export type HeadlessAgentOptions = Readonly<{
|
|
|
49
49
|
prefer?: HeadlessClient;
|
|
50
50
|
/** Codex's sandbox. Read-only unless the run has to execute tests. */
|
|
51
51
|
sandbox?: "read-only" | "workspace-write";
|
|
52
|
+
/**
|
|
53
|
+
* The run's folder is no repository on purpose (an anchoring call works in
|
|
54
|
+
* an empty folder, so it can read nothing), which Codex must be told.
|
|
55
|
+
*/
|
|
56
|
+
outsideRepository?: boolean;
|
|
52
57
|
/** Claude Code permission rules refused outright, such as `Bash(git push:*)`. */
|
|
53
58
|
disallowedTools?: readonly string[];
|
|
54
59
|
/** Fires when the caller's budget runs out; the run is stopped, not failed. */
|
|
@@ -58,6 +63,15 @@ export type HeadlessAgentOptions = Readonly<{
|
|
|
58
63
|
/** A login already verified by the caller, so a batch checks it once. */
|
|
59
64
|
login?: LoginCheck;
|
|
60
65
|
maxOutputBytes?: number;
|
|
66
|
+
/**
|
|
67
|
+
* The most output tokens the model may write in this run. Claude Code is
|
|
68
|
+
* held to it by `CLAUDE_CODE_MAX_OUTPUT_TOKENS`, so a run that would go
|
|
69
|
+
* further is stopped there. Codex has no such setting (none in its
|
|
70
|
+
* configuration reference, and none in its 0.144 binary), so for Codex the
|
|
71
|
+
* run is held by its time limit and `overOutputCap` says afterwards when it
|
|
72
|
+
* wrote more than this.
|
|
73
|
+
*/
|
|
74
|
+
maxOutputTokens?: number;
|
|
61
75
|
/** Where the private settings directory is made. Defaults to the system temp directory. */
|
|
62
76
|
scratch?: string;
|
|
63
77
|
}>;
|
|
@@ -76,8 +90,10 @@ export type HeadlessAgentResult = Readonly<{
|
|
|
76
90
|
stderrTail: string;
|
|
77
91
|
timedOut: boolean;
|
|
78
92
|
aborted: boolean;
|
|
93
|
+
/** The run reported more output tokens than `maxOutputTokens` allowed, or stopped at that cap. */
|
|
94
|
+
overOutputCap?: boolean;
|
|
79
95
|
/** Why `ok` is false, as a code a caller can branch on. */
|
|
80
|
-
reason?: "no_verified_login" | "spawn_failed" | "timed_out" | "aborted" | "output_bounded" | "exit_nonzero" | "no_answer" | "no_json";
|
|
96
|
+
reason?: "no_verified_login" | "spawn_failed" | "timed_out" | "aborted" | "output_bounded" | "exit_nonzero" | "no_answer" | "no_json" | "output_capped";
|
|
81
97
|
}>;
|
|
82
98
|
/** Set on every run this module starts, so a hook inside it knows not to judge again. */
|
|
83
99
|
export declare const JUDGE_ACTIVE_VARIABLE = "BALLADEER_JUDGE_ACTIVE";
|
|
@@ -102,6 +118,8 @@ export declare function claudeArguments(input: Readonly<{
|
|
|
102
118
|
export declare function codexArguments(input: Readonly<{
|
|
103
119
|
cwd: string;
|
|
104
120
|
sandbox: "read-only" | "workspace-write";
|
|
121
|
+
/** The run is in a folder that is no repository on purpose, such as an empty one. */
|
|
122
|
+
outsideRepository?: boolean;
|
|
105
123
|
}>): string[];
|
|
106
124
|
/**
|
|
107
125
|
* Whether this laptop has the subscription login the client needs.
|
package/dist/headless-agent.js
CHANGED
|
@@ -40,8 +40,13 @@ export function clientBinary(client, env) {
|
|
|
40
40
|
export function claudeArguments(input) {
|
|
41
41
|
const tools = (input.tools ?? input.allowedTools).join(",");
|
|
42
42
|
// Rules narrower than a tool carry spaces (`Bash(git diff:*)`), so they go
|
|
43
|
-
// in as separate values rather than as one comma list.
|
|
44
|
-
|
|
43
|
+
// in as separate values rather than as one comma list. A run with no tools
|
|
44
|
+
// at all (`--tools ""`) has nothing to allow, and the flag is left out.
|
|
45
|
+
const allowed = input.allowedTools.length === 0
|
|
46
|
+
? []
|
|
47
|
+
: input.tools === undefined
|
|
48
|
+
? [tools]
|
|
49
|
+
: [...input.allowedTools];
|
|
45
50
|
return [
|
|
46
51
|
"--print",
|
|
47
52
|
"--output-format",
|
|
@@ -59,8 +64,7 @@ export function claudeArguments(input) {
|
|
|
59
64
|
"dontAsk",
|
|
60
65
|
"--tools",
|
|
61
66
|
tools,
|
|
62
|
-
"--allowedTools",
|
|
63
|
-
...allowed,
|
|
67
|
+
...(allowed.length > 0 ? ["--allowedTools", ...allowed] : []),
|
|
64
68
|
...((input.disallowedTools ?? []).length > 0
|
|
65
69
|
? ["--disallowedTools", ...(input.disallowedTools ?? [])]
|
|
66
70
|
: []),
|
|
@@ -77,6 +81,8 @@ export function codexArguments(input) {
|
|
|
77
81
|
input.sandbox,
|
|
78
82
|
"-C",
|
|
79
83
|
input.cwd,
|
|
84
|
+
// Codex refuses to start outside a git repository unless told this.
|
|
85
|
+
...(input.outsideRepository === true ? ["--skip-git-repo-check"] : []),
|
|
80
86
|
"--json",
|
|
81
87
|
"-c",
|
|
82
88
|
'approval_policy="never"',
|
|
@@ -338,6 +344,8 @@ export async function runHeadlessAgent(options) {
|
|
|
338
344
|
const env = agentEnvironment(options.env);
|
|
339
345
|
let args;
|
|
340
346
|
if (client === "claude") {
|
|
347
|
+
if (options.maxOutputTokens !== undefined)
|
|
348
|
+
env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = String(options.maxOutputTokens);
|
|
341
349
|
const settingsPath = join(privateRoot, "settings.json");
|
|
342
350
|
writeFileSync(settingsPath, JSON.stringify({ disableAllHooks: true }), { mode: 0o600 });
|
|
343
351
|
args = claudeArguments({
|
|
@@ -359,7 +367,11 @@ export async function runHeadlessAgent(options) {
|
|
|
359
367
|
if (existsSync(auth))
|
|
360
368
|
symlinkSync(auth, join(codexHome, "auth.json"));
|
|
361
369
|
env.CODEX_HOME = codexHome;
|
|
362
|
-
args = codexArguments({
|
|
370
|
+
args = codexArguments({
|
|
371
|
+
cwd: options.cwd,
|
|
372
|
+
sandbox: options.sandbox ?? "read-only",
|
|
373
|
+
...(options.outsideRepository === true ? { outsideRepository: true } : {}),
|
|
374
|
+
});
|
|
363
375
|
}
|
|
364
376
|
const run = await runOwnedProcess({
|
|
365
377
|
command: clientBinary(client, options.env),
|
|
@@ -399,6 +411,11 @@ export async function runHeadlessAgent(options) {
|
|
|
399
411
|
return failed("timed_out");
|
|
400
412
|
if (run.bounded)
|
|
401
413
|
return failed("output_bounded");
|
|
414
|
+
// Reaching the cap is a stop, whatever the agent managed to say before it.
|
|
415
|
+
if (options.maxOutputTokens !== undefined &&
|
|
416
|
+
parsed?.outputTokens !== undefined &&
|
|
417
|
+
parsed.outputTokens >= options.maxOutputTokens)
|
|
418
|
+
return { ...failed("output_capped"), overOutputCap: true };
|
|
402
419
|
if (run.exitCode !== 0)
|
|
403
420
|
return failed("exit_nonzero");
|
|
404
421
|
if (parsed === undefined || parsed.isError || parsed.text.trim() === "")
|
package/dist/judge-brief.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { PromiseAnchors } from "./anchors-schema.js";
|
|
1
2
|
import type { BehaviorMap } from "./behavior-map-schema.js";
|
|
2
3
|
import type { PromiseDocument } from "./promise-meaning.js";
|
|
3
4
|
/**
|
|
4
|
-
* The fixed instructions a judge works from, `judge-brief/
|
|
5
|
+
* The fixed instructions a judge works from, `judge-brief/v5`.
|
|
5
6
|
*
|
|
6
7
|
* One promise, one change, one disposable checkout at the change's head. The
|
|
7
8
|
* rules are the contract's: cases first, linked test first, broken only with a
|
|
@@ -13,8 +14,17 @@ import type { PromiseDocument } from "./promise-meaning.js";
|
|
|
13
14
|
*
|
|
14
15
|
* The text is a constant so a verdict can name the exact brief it was given.
|
|
15
16
|
* Changing a word here is a new revision.
|
|
17
|
+
*
|
|
18
|
+
* v5 is v3's rules word for word; what it added is a section of the prompt
|
|
19
|
+
* (1 October 2026, with promise anchors): a promise with anchors and no
|
|
20
|
+
* behavior map is handed its anchors, the linked tests the graph gives at
|
|
21
|
+
* head, and the files one import step from its anchor files, where v3 said
|
|
22
|
+
* "find where its behavior lives yourself". A promise with a map is handed
|
|
23
|
+
* exactly what v3 handed it. v4 belongs to a candidate measured and not
|
|
24
|
+
* adopted on 30 September, so it is never reused. The judge with anchors in
|
|
25
|
+
* place of a map has not been measured yet.
|
|
16
26
|
*/
|
|
17
|
-
export declare const JUDGE_BRIEF_REVISION = "judge-brief/
|
|
27
|
+
export declare const JUDGE_BRIEF_REVISION = "judge-brief/v5";
|
|
18
28
|
/**
|
|
19
29
|
* What `judge-brief/v3` added to v1, and all it added (Robert, 30 September
|
|
20
30
|
* 2026): a way forward when the tests need something the machine lacks.
|
|
@@ -31,6 +41,14 @@ export declare const JUDGE_BRIEF = "You are judging one change to this repositor
|
|
|
31
41
|
export type JudgeBriefInput = Readonly<{
|
|
32
42
|
promise: PromiseDocument;
|
|
33
43
|
map?: BehaviorMap;
|
|
44
|
+
/** Read only when there is no map. */
|
|
45
|
+
anchors?: Readonly<{
|
|
46
|
+
anchors: PromiseAnchors;
|
|
47
|
+
linkedTests: readonly Readonly<{
|
|
48
|
+
file: string;
|
|
49
|
+
}>[];
|
|
50
|
+
neighborhood: readonly string[];
|
|
51
|
+
}>;
|
|
34
52
|
changedFiles: readonly string[];
|
|
35
53
|
base: string;
|
|
36
54
|
head: string;
|
package/dist/judge-brief.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The fixed instructions a judge works from, `judge-brief/
|
|
2
|
+
* The fixed instructions a judge works from, `judge-brief/v5`.
|
|
3
3
|
*
|
|
4
4
|
* One promise, one change, one disposable checkout at the change's head. The
|
|
5
5
|
* rules are the contract's: cases first, linked test first, broken only with a
|
|
@@ -11,8 +11,17 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The text is a constant so a verdict can name the exact brief it was given.
|
|
13
13
|
* Changing a word here is a new revision.
|
|
14
|
+
*
|
|
15
|
+
* v5 is v3's rules word for word; what it added is a section of the prompt
|
|
16
|
+
* (1 October 2026, with promise anchors): a promise with anchors and no
|
|
17
|
+
* behavior map is handed its anchors, the linked tests the graph gives at
|
|
18
|
+
* head, and the files one import step from its anchor files, where v3 said
|
|
19
|
+
* "find where its behavior lives yourself". A promise with a map is handed
|
|
20
|
+
* exactly what v3 handed it. v4 belongs to a candidate measured and not
|
|
21
|
+
* adopted on 30 September, so it is never reused. The judge with anchors in
|
|
22
|
+
* place of a map has not been measured yet.
|
|
14
23
|
*/
|
|
15
|
-
export const JUDGE_BRIEF_REVISION = "judge-brief/
|
|
24
|
+
export const JUDGE_BRIEF_REVISION = "judge-brief/v5";
|
|
16
25
|
/**
|
|
17
26
|
* What `judge-brief/v3` added to v1, and all it added (Robert, 30 September
|
|
18
27
|
* 2026): a way forward when the tests need something the machine lacks.
|
|
@@ -72,6 +81,21 @@ export function judgePrompt(input) {
|
|
|
72
81
|
sections.push("Where this promise's behavior lives, from its behavior map (built earlier; check it against the code):");
|
|
73
82
|
sections.push(JSON.stringify(map, null, 2));
|
|
74
83
|
}
|
|
84
|
+
else if (input.anchors !== undefined) {
|
|
85
|
+
const { anchors, linkedTests, neighborhood } = input.anchors;
|
|
86
|
+
sections.push("Where this promise's behavior lives, from its anchors (picked earlier from the promise's words and a list of this repository's files; check them against the code):");
|
|
87
|
+
sections.push(JSON.stringify({
|
|
88
|
+
entryPoints: anchors.entryPoints,
|
|
89
|
+
symbols: anchors.symbols,
|
|
90
|
+
resources: anchors.resources,
|
|
91
|
+
linkedTests,
|
|
92
|
+
}, null, 2));
|
|
93
|
+
if (neighborhood.length > 0) {
|
|
94
|
+
sections.push("Files one import step from those (what they import and what imports them), where to look next if the behavior reaches further:");
|
|
95
|
+
for (const file of neighborhood)
|
|
96
|
+
sections.push(`- ${file}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
75
99
|
else {
|
|
76
100
|
sections.push("There is no behavior map for this promise; find where its behavior lives yourself.");
|
|
77
101
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BehaviorMapMeaning } from "./behavior-map-schema.js";
|
|
2
|
+
import { type StoredAgent } from "./store.js";
|
|
2
3
|
/**
|
|
3
4
|
* One promise's agreed meaning, the way the map builder and the judge read it.
|
|
4
5
|
*
|
|
@@ -115,6 +116,22 @@ export type PromiseReader = Readonly<{
|
|
|
115
116
|
* is told what happened and what works meanwhile.
|
|
116
117
|
*/
|
|
117
118
|
export declare function olderServerRefusal(text: string): string | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* The agent connection stored on this laptop for the repository a folder is
|
|
121
|
+
* in, or the reason there is none. Read locally; nothing is asked of the server.
|
|
122
|
+
*/
|
|
123
|
+
export declare function connectionAgent(input: Readonly<{
|
|
124
|
+
environment: NodeJS.ProcessEnv;
|
|
125
|
+
controlPlane: string;
|
|
126
|
+
cwd: string;
|
|
127
|
+
repository?: string;
|
|
128
|
+
}>): {
|
|
129
|
+
ok: true;
|
|
130
|
+
agent: StoredAgent;
|
|
131
|
+
} | {
|
|
132
|
+
ok: false;
|
|
133
|
+
reason: string;
|
|
134
|
+
};
|
|
118
135
|
/**
|
|
119
136
|
* This repository's agent connection as a reader of promises, or the reason
|
|
120
137
|
* there is none. Reading is all it does: `get_promise` and `list_promises`,
|
package/dist/promise-meaning.js
CHANGED
|
@@ -211,22 +211,31 @@ export function olderServerRefusal(text) {
|
|
|
211
211
|
"the server is updated; until then, hand the promise over as a file with --from-file.");
|
|
212
212
|
}
|
|
213
213
|
/**
|
|
214
|
-
*
|
|
215
|
-
* there is none.
|
|
216
|
-
* the same two reads an agent in this repository makes.
|
|
214
|
+
* The agent connection stored on this laptop for the repository a folder is
|
|
215
|
+
* in, or the reason there is none. Read locally; nothing is asked of the server.
|
|
217
216
|
*/
|
|
218
|
-
export function
|
|
219
|
-
let agent;
|
|
217
|
+
export function connectionAgent(input) {
|
|
220
218
|
try {
|
|
221
219
|
const credentials = readCredentials(input.environment);
|
|
222
220
|
const selection = selectAgent(credentials.agents, input.controlPlane, input.repository, repositoryHints(input.cwd));
|
|
223
221
|
if (selection.kind === "refused")
|
|
224
222
|
return { ok: false, reason: selection.reason };
|
|
225
|
-
agent
|
|
223
|
+
return { ok: true, agent: selection.agent };
|
|
226
224
|
}
|
|
227
225
|
catch (error) {
|
|
228
226
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
229
227
|
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* This repository's agent connection as a reader of promises, or the reason
|
|
231
|
+
* there is none. Reading is all it does: `get_promise` and `list_promises`,
|
|
232
|
+
* the same two reads an agent in this repository makes.
|
|
233
|
+
*/
|
|
234
|
+
export function connectionReader(input) {
|
|
235
|
+
const found = connectionAgent(input);
|
|
236
|
+
if (!found.ok)
|
|
237
|
+
return found;
|
|
238
|
+
const agent = found.agent;
|
|
230
239
|
const refusal = (kind) => kind === "unreachable"
|
|
231
240
|
? `Balladeer could not be reached at ${input.controlPlane}`
|
|
232
241
|
: kind === "unauthorized"
|
package/dist/risk/git.d.ts
CHANGED
|
@@ -15,11 +15,13 @@ export declare function tryGit(root: string, args: readonly string[]): string |
|
|
|
15
15
|
export declare function resolveCommit(root: string, ref: string): string;
|
|
16
16
|
/** The merge base `git diff base...head` compares against. */
|
|
17
17
|
export declare function mergeBase(root: string, base: string, head: string): string;
|
|
18
|
+
/** One blob in a tree: its path, its size, and the object id its content hashes to. */
|
|
18
19
|
export type TreeEntry = Readonly<{
|
|
19
20
|
path: string;
|
|
20
21
|
size: number;
|
|
22
|
+
sha: string;
|
|
21
23
|
}>;
|
|
22
|
-
/** Every blob in a commit's tree, with its size. */
|
|
24
|
+
/** Every blob in a commit's tree, with its size and object id. */
|
|
23
25
|
export declare function listTree(root: string, rev: string): TreeEntry[];
|
|
24
26
|
/**
|
|
25
27
|
* The contents of many files at one commit, in one `git cat-file --batch`.
|
package/dist/risk/git.js
CHANGED
|
@@ -59,7 +59,7 @@ export function mergeBase(root, base, head) {
|
|
|
59
59
|
throw new GitReadError(["merge-base", base, head], "no merge base between the two commits");
|
|
60
60
|
return sha;
|
|
61
61
|
}
|
|
62
|
-
/** Every blob in a commit's tree, with its size. */
|
|
62
|
+
/** Every blob in a commit's tree, with its size and object id. */
|
|
63
63
|
export function listTree(root, rev) {
|
|
64
64
|
const out = runGit(root, ["ls-tree", "-r", "-z", "--long", rev]).toString("utf8");
|
|
65
65
|
const entries = [];
|
|
@@ -69,10 +69,10 @@ export function listTree(root, rev) {
|
|
|
69
69
|
const tab = record.indexOf("\t");
|
|
70
70
|
if (tab < 0)
|
|
71
71
|
continue;
|
|
72
|
-
const [, type, , size] = record.slice(0, tab).split(/\s+/);
|
|
72
|
+
const [, type, sha, size] = record.slice(0, tab).split(/\s+/);
|
|
73
73
|
if (type !== "blob")
|
|
74
74
|
continue;
|
|
75
|
-
entries.push({ path: record.slice(tab + 1), size: Number(size) || 0 });
|
|
75
|
+
entries.push({ path: record.slice(tab + 1), size: Number(size) || 0, sha: sha ?? "" });
|
|
76
76
|
}
|
|
77
77
|
return entries;
|
|
78
78
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { SymbolKind } from "./contract.js";
|
|
2
|
+
import { type ImportGraph } from "./import-graph.js";
|
|
3
|
+
/**
|
|
4
|
+
* The repository graph of one commit, built from a cache on this laptop.
|
|
5
|
+
*
|
|
6
|
+
* What a file imports and which names it declares depend only on its own
|
|
7
|
+
* content, so they are kept per blob, and a commit parses only the blobs this
|
|
8
|
+
* laptop has not read before. How those imports resolve depends on the whole
|
|
9
|
+
* tree, so that is worked out again on every build, from the cached
|
|
10
|
+
* specifiers; it is cheap. The cache sits in Balladeer's config home, one file
|
|
11
|
+
* per repository (all worktrees of a repository share it), and holds file
|
|
12
|
+
* paths' blob ids, import specifiers and declared names. It is never committed
|
|
13
|
+
* and never sent anywhere.
|
|
14
|
+
*/
|
|
15
|
+
/** Bumped whenever what is read out of a blob changes, so old entries are read again. */
|
|
16
|
+
export declare const GRAPH_CACHE_VERSION = 1;
|
|
17
|
+
export declare const GRAPH_CACHE_DIRECTORY = "graph-cache";
|
|
18
|
+
export type ExportedName = Readonly<{
|
|
19
|
+
name: string;
|
|
20
|
+
kind: SymbolKind;
|
|
21
|
+
}>;
|
|
22
|
+
type BlobSymbols = {
|
|
23
|
+
/** Which reader found them: `typescript@<version>` or `regex`. */
|
|
24
|
+
reader: string;
|
|
25
|
+
/** Top-level names the file exports, in the order it declares them. */
|
|
26
|
+
exported: ExportedName[];
|
|
27
|
+
/** Every name the file declares or exports, which is what an anchor's symbol is checked against. */
|
|
28
|
+
names: string[];
|
|
29
|
+
};
|
|
30
|
+
export type GraphBuildStats = Readonly<{
|
|
31
|
+
/** Source files in the graph. */
|
|
32
|
+
files: number;
|
|
33
|
+
/** Blobs read and parsed in this build. */
|
|
34
|
+
parsed: number;
|
|
35
|
+
/** Blobs whose reading came from the cache. */
|
|
36
|
+
reused: number;
|
|
37
|
+
ms: number;
|
|
38
|
+
}>;
|
|
39
|
+
export type RepositoryGraph = Readonly<{
|
|
40
|
+
root: string;
|
|
41
|
+
/** The commit, as a full sha. */
|
|
42
|
+
sha: string;
|
|
43
|
+
graph: ImportGraph;
|
|
44
|
+
/** Every path in the commit's tree, of any kind. */
|
|
45
|
+
tree: ReadonlySet<string>;
|
|
46
|
+
/** The names a source file exports, read once per blob. Undefined for a file not in the graph. */
|
|
47
|
+
exportedNames(file: string): readonly ExportedName[] | undefined;
|
|
48
|
+
/** Every name a source file declares or exports. Undefined for a file not in the graph. */
|
|
49
|
+
declaredNames(file: string): ReadonlySet<string> | undefined;
|
|
50
|
+
/** Read the names of many files at once, so a whole index costs one git read. */
|
|
51
|
+
prepareNames(files: readonly string[]): void;
|
|
52
|
+
/** Keep what this build read for the next one. Never throws. */
|
|
53
|
+
save(): void;
|
|
54
|
+
stats(): GraphBuildStats;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Names a script file makes available beyond its declarations: `default` for
|
|
58
|
+
* `export default`, each name or alias in `export { a, b as c }`, a namespace
|
|
59
|
+
* re-export's name, and the names bound by `export const { a, b } = ...` or
|
|
60
|
+
* `export const [a, b] = ...`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function exportClauseNames(text: string): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Whether a graph file belongs in the anchoring index: product source, so not
|
|
65
|
+
* a test, and (from `anchor-brief/v3`) not a story or a mock.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isIndexSource(file: string): boolean;
|
|
68
|
+
/** The names a file declares and exports, read the way the anchoring index reads them. */
|
|
69
|
+
export declare function readSymbols(file: string, text: string, root: string): Omit<BlobSymbols, "reader">;
|
|
70
|
+
/** The cache file of the repository a checkout belongs to, shared by all its worktrees. */
|
|
71
|
+
export declare function graphCachePath(root: string, environment: NodeJS.ProcessEnv): string;
|
|
72
|
+
/**
|
|
73
|
+
* The graph of `rev`, from this laptop's cache where it can be. With
|
|
74
|
+
* `cache: false` nothing is read from or written to disk, which is the plain
|
|
75
|
+
* build every time.
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildRepositoryGraph(input: Readonly<{
|
|
78
|
+
root: string;
|
|
79
|
+
rev: string;
|
|
80
|
+
environment?: NodeJS.ProcessEnv;
|
|
81
|
+
cache?: boolean;
|
|
82
|
+
}>): RepositoryGraph;
|
|
83
|
+
export {};
|