balladeer 1.0.15 → 1.0.17
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 +6 -1
- package/dist/cli.js +74 -6
- package/dist/commands/guidance.d.ts +2 -1
- package/dist/commands/guidance.js +55 -0
- package/dist/commands/judge.d.ts +430 -0
- package/dist/commands/judge.js +1713 -0
- package/dist/commands/map.d.ts +164 -0
- package/dist/commands/map.js +838 -0
- package/dist/commands/mcp.js +2 -2
- 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 +348 -94
- package/dist/headless-agent.d.ts +166 -0
- package/dist/headless-agent.js +416 -0
- package/dist/hook-trust.d.ts +41 -9
- package/dist/hook-trust.js +98 -16
- package/dist/judge-brief.d.ts +48 -0
- package/dist/judge-brief.js +102 -0
- package/dist/judge-hook.d.ts +258 -0
- package/dist/judge-hook.js +1278 -0
- package/dist/judge-said.d.ts +52 -0
- package/dist/judge-said.js +181 -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 +134 -0
- package/dist/promise-meaning.js +300 -0
- package/dist/relay.d.ts +71 -0
- package/dist/relay.js +193 -0
- package/dist/remove-earlier.js +4 -2
- 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 +95 -0
- package/dist/risk/score.js +552 -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 +55 -0
- package/dist/scratch-worktree.js +160 -0
- package/dist/user-scope.d.ts +74 -3
- package/dist/user-scope.js +270 -9
- package/dist/wire.d.ts +52 -3
- package/dist/wire.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,6 +206,8 @@ your own branch protection.
|
|
|
206
206
|
| `npx -y balladeer@latest discover` | File a whole catalog, up to ten promises from one file, behind one review link |
|
|
207
207
|
| `npx -y balladeer@latest touch-map` | Record which files each promise's verifier runs, on this machine only |
|
|
208
208
|
| `npx -y balladeer@latest affected` | Say which promises the files you name touch, out of that record |
|
|
209
|
+
| `npx -y balladeer@latest risk` | Rank the promises a change could put at risk, on this machine only |
|
|
210
|
+
| `npx -y balladeer@latest offers` | Read a change for up to three rules no promise records, and record nothing |
|
|
209
211
|
| `npx -y balladeer@latest session` | Print the id for this piece of work, and the line to write into the commit |
|
|
210
212
|
| `npx -y balladeer@latest mcp` | Forward one MCP session over stdio using this repository's connection |
|
|
211
213
|
| `npx -y balladeer@latest explain` | What Balladeer can and cannot see, where to watch it, and what leaving costs |
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `balladeer-behavior-map/v1`, as `docs/agentic-testing/contracts.md` spells it.
|
|
3
|
+
*
|
|
4
|
+
* Written without a schema library on purpose. The published command carries
|
|
5
|
+
* exactly one reviewed runtime dependency (`tests/packaging/cli-package.test.ts`
|
|
6
|
+
* holds it there), so the shape is checked by hand here, with the field names
|
|
7
|
+
* copied from the contract. It is the one reader of maps: `balladeer map` writes
|
|
8
|
+
* through it and the risk ranking (`risk/`) reads through it, so a field added
|
|
9
|
+
* here reaches both, and `tests/risk/contract.test.ts` round-trips a full map.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BEHAVIOR_MAP_SCHEMA_VERSION = "balladeer-behavior-map/v1";
|
|
12
|
+
export declare const BEHAVIOR_MAP_DIRECTORY = ".continuity/behavior-maps";
|
|
13
|
+
export declare const ENTRY_POINT_KINDS: readonly ["route", "command", "ui", "job", "event", "function"];
|
|
14
|
+
export declare const SYMBOL_KINDS: readonly ["function", "class", "method", "const", "type", "module"];
|
|
15
|
+
export declare const SYMBOL_ROLES: readonly ["core", "supporting"];
|
|
16
|
+
export declare const RESOURCE_KINDS: readonly ["table", "column", "route", "flag", "env", "queue", "event", "outbound", "file", "config"];
|
|
17
|
+
export declare const RESOURCE_ACCESS: readonly ["read", "write", "both", "call"];
|
|
18
|
+
/** Positions in the agreed meaning: `passing-1`, `failing-2`, `refactor-1`. */
|
|
19
|
+
export declare const CASE_ID: RegExp;
|
|
20
|
+
export type BehaviorMapEntryPoint = {
|
|
21
|
+
file: string;
|
|
22
|
+
symbol: string;
|
|
23
|
+
kind: (typeof ENTRY_POINT_KINDS)[number];
|
|
24
|
+
note: string;
|
|
25
|
+
};
|
|
26
|
+
export type BehaviorMapSymbol = {
|
|
27
|
+
file: string;
|
|
28
|
+
name: string;
|
|
29
|
+
kind: (typeof SYMBOL_KINDS)[number];
|
|
30
|
+
role: (typeof SYMBOL_ROLES)[number];
|
|
31
|
+
};
|
|
32
|
+
export type BehaviorMapResource = {
|
|
33
|
+
kind: (typeof RESOURCE_KINDS)[number];
|
|
34
|
+
name: string;
|
|
35
|
+
access: (typeof RESOURCE_ACCESS)[number];
|
|
36
|
+
};
|
|
37
|
+
export type BehaviorMapLinkedTest = {
|
|
38
|
+
file: string;
|
|
39
|
+
name: string;
|
|
40
|
+
cases: string[];
|
|
41
|
+
};
|
|
42
|
+
export type BehaviorMapSink = {
|
|
43
|
+
caseId: string;
|
|
44
|
+
effect: string;
|
|
45
|
+
resources: string[];
|
|
46
|
+
/** `file#symbol`. */
|
|
47
|
+
symbols: string[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The part of the agreed meaning a scorer needs without asking the server:
|
|
51
|
+
* the contract addendum of 28 September. `surfaces` is the promise's own scope
|
|
52
|
+
* surfaces, which the scope-only baseline reads.
|
|
53
|
+
*/
|
|
54
|
+
export type BehaviorMapMeaning = {
|
|
55
|
+
title: string;
|
|
56
|
+
observableOutcome: string;
|
|
57
|
+
failingExamples: {
|
|
58
|
+
id: string;
|
|
59
|
+
text: string;
|
|
60
|
+
}[];
|
|
61
|
+
nonGoals: string[];
|
|
62
|
+
surfaces?: string[];
|
|
63
|
+
};
|
|
64
|
+
export type BehaviorMap = {
|
|
65
|
+
schemaVersion: typeof BEHAVIOR_MAP_SCHEMA_VERSION;
|
|
66
|
+
promiseId: string;
|
|
67
|
+
semanticDigest: string;
|
|
68
|
+
mappedAt: {
|
|
69
|
+
sha: string;
|
|
70
|
+
at: string;
|
|
71
|
+
client: "claude" | "codex";
|
|
72
|
+
model: string;
|
|
73
|
+
};
|
|
74
|
+
entryPoints: BehaviorMapEntryPoint[];
|
|
75
|
+
symbols: BehaviorMapSymbol[];
|
|
76
|
+
resources: BehaviorMapResource[];
|
|
77
|
+
linkedTests: BehaviorMapLinkedTest[];
|
|
78
|
+
sinks: BehaviorMapSink[];
|
|
79
|
+
confidence: number;
|
|
80
|
+
notes: string;
|
|
81
|
+
meaning?: BehaviorMapMeaning;
|
|
82
|
+
};
|
|
83
|
+
/** Ceilings, so a map stays a small file a person can read in a review. */
|
|
84
|
+
export declare const MAP_LIMITS: {
|
|
85
|
+
readonly entryPoints: 20;
|
|
86
|
+
readonly symbols: 60;
|
|
87
|
+
readonly resources: 40;
|
|
88
|
+
readonly linkedTests: 20;
|
|
89
|
+
readonly sinks: 20;
|
|
90
|
+
readonly text: 2000;
|
|
91
|
+
readonly name: 400;
|
|
92
|
+
};
|
|
93
|
+
export type ParseResult<T> = {
|
|
94
|
+
ok: true;
|
|
95
|
+
value: T;
|
|
96
|
+
} | {
|
|
97
|
+
ok: false;
|
|
98
|
+
errors: string[];
|
|
99
|
+
};
|
|
100
|
+
/** Promise ids: `prom_` ids from Balladeer, and the evaluation harness's own ids. */
|
|
101
|
+
export declare const PROMISE_ID: RegExp;
|
|
102
|
+
/** A whole map, checked against the contract. Unknown fields are refused. */
|
|
103
|
+
export declare function parseBehaviorMap(value: unknown): ParseResult<BehaviorMap>;
|
|
104
|
+
/** Every file a map names: the derived `files` field, which is never stored. */
|
|
105
|
+
export declare function mapFiles(map: BehaviorMap): string[];
|
|
106
|
+
/** The map as it is written to disk: two-space JSON and a final newline. */
|
|
107
|
+
export declare function serializeBehaviorMap(map: BehaviorMap): string;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `balladeer-behavior-map/v1`, as `docs/agentic-testing/contracts.md` spells it.
|
|
3
|
+
*
|
|
4
|
+
* Written without a schema library on purpose. The published command carries
|
|
5
|
+
* exactly one reviewed runtime dependency (`tests/packaging/cli-package.test.ts`
|
|
6
|
+
* holds it there), so the shape is checked by hand here, with the field names
|
|
7
|
+
* copied from the contract. It is the one reader of maps: `balladeer map` writes
|
|
8
|
+
* through it and the risk ranking (`risk/`) reads through it, so a field added
|
|
9
|
+
* here reaches both, and `tests/risk/contract.test.ts` round-trips a full map.
|
|
10
|
+
*/
|
|
11
|
+
export const BEHAVIOR_MAP_SCHEMA_VERSION = "balladeer-behavior-map/v1";
|
|
12
|
+
export const BEHAVIOR_MAP_DIRECTORY = ".continuity/behavior-maps";
|
|
13
|
+
export const ENTRY_POINT_KINDS = ["route", "command", "ui", "job", "event", "function"];
|
|
14
|
+
export const SYMBOL_KINDS = ["function", "class", "method", "const", "type", "module"];
|
|
15
|
+
export const SYMBOL_ROLES = ["core", "supporting"];
|
|
16
|
+
export const RESOURCE_KINDS = [
|
|
17
|
+
"table",
|
|
18
|
+
"column",
|
|
19
|
+
"route",
|
|
20
|
+
"flag",
|
|
21
|
+
"env",
|
|
22
|
+
"queue",
|
|
23
|
+
"event",
|
|
24
|
+
"outbound",
|
|
25
|
+
"file",
|
|
26
|
+
"config",
|
|
27
|
+
];
|
|
28
|
+
export const RESOURCE_ACCESS = ["read", "write", "both", "call"];
|
|
29
|
+
/** Positions in the agreed meaning: `passing-1`, `failing-2`, `refactor-1`. */
|
|
30
|
+
export const CASE_ID = /^(passing|failing|refactor)-[1-9][0-9]{0,2}$/;
|
|
31
|
+
/** Ceilings, so a map stays a small file a person can read in a review. */
|
|
32
|
+
export const MAP_LIMITS = {
|
|
33
|
+
entryPoints: 20,
|
|
34
|
+
symbols: 60,
|
|
35
|
+
resources: 40,
|
|
36
|
+
linkedTests: 20,
|
|
37
|
+
sinks: 20,
|
|
38
|
+
text: 2000,
|
|
39
|
+
name: 400,
|
|
40
|
+
};
|
|
41
|
+
const text = (max, allowEmpty = true) => (value, at, errors) => {
|
|
42
|
+
if (typeof value !== "string") {
|
|
43
|
+
errors.push(`${at} must be a string`);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!allowEmpty && value.trim() === "") {
|
|
47
|
+
errors.push(`${at} must not be empty`);
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (value.length > max) {
|
|
51
|
+
errors.push(`${at} is longer than ${max} characters`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
const oneOf = (values) => (value, at, errors) => {
|
|
57
|
+
if (typeof value === "string" && values.includes(value))
|
|
58
|
+
return value;
|
|
59
|
+
errors.push(`${at} must be one of ${values.join(", ")}`);
|
|
60
|
+
return undefined;
|
|
61
|
+
};
|
|
62
|
+
/** Repository-relative, forward slashes, inside the repository. */
|
|
63
|
+
const repositoryPath = (value, at, errors) => {
|
|
64
|
+
const path = text(MAP_LIMITS.name, false)(value, at, errors);
|
|
65
|
+
if (path === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (path.startsWith("/") ||
|
|
68
|
+
path.includes("\\") ||
|
|
69
|
+
/^[A-Za-z]:/.test(path) ||
|
|
70
|
+
path.split("/").some((part) => part === "..")) {
|
|
71
|
+
errors.push(`${at} must be a repository-relative path with forward slashes`);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return path;
|
|
75
|
+
};
|
|
76
|
+
const listOf = (item, max) => (value, at, errors) => {
|
|
77
|
+
if (!Array.isArray(value)) {
|
|
78
|
+
errors.push(`${at} must be a list`);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (value.length > max) {
|
|
82
|
+
errors.push(`${at} has more than ${max} entries`);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const out = [];
|
|
86
|
+
let failed = false;
|
|
87
|
+
value.forEach((entry, index) => {
|
|
88
|
+
const checked = item(entry, `${at}[${index}]`, errors);
|
|
89
|
+
if (checked === undefined)
|
|
90
|
+
failed = true;
|
|
91
|
+
else
|
|
92
|
+
out.push(checked);
|
|
93
|
+
});
|
|
94
|
+
return failed ? undefined : out;
|
|
95
|
+
};
|
|
96
|
+
const record = (fields, optional = []) => (value, at, errors) => {
|
|
97
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
98
|
+
errors.push(`${at} must be an object`);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const input = value;
|
|
102
|
+
const unknown = Object.keys(input).filter((key) => !(key in fields));
|
|
103
|
+
if (unknown.length > 0) {
|
|
104
|
+
errors.push(`${at} has fields the contract does not define: ${unknown.join(", ")}`);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const out = {};
|
|
108
|
+
let failed = false;
|
|
109
|
+
for (const key of Object.keys(fields)) {
|
|
110
|
+
if (!(key in input) || input[key] === undefined) {
|
|
111
|
+
if (optional.includes(key))
|
|
112
|
+
continue;
|
|
113
|
+
errors.push(`${at}.${key} is missing`);
|
|
114
|
+
failed = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const checked = fields[key](input[key], `${at}.${key}`, errors);
|
|
118
|
+
if (checked === undefined)
|
|
119
|
+
failed = true;
|
|
120
|
+
else
|
|
121
|
+
out[key] = checked;
|
|
122
|
+
}
|
|
123
|
+
return failed ? undefined : out;
|
|
124
|
+
};
|
|
125
|
+
const caseId = (value, at, errors) => {
|
|
126
|
+
if (typeof value === "string" && CASE_ID.test(value))
|
|
127
|
+
return value;
|
|
128
|
+
errors.push(`${at} must be a case id such as passing-1 or failing-2`);
|
|
129
|
+
return undefined;
|
|
130
|
+
};
|
|
131
|
+
const fileSymbol = (value, at, errors) => {
|
|
132
|
+
const reference = text(MAP_LIMITS.name, false)(value, at, errors);
|
|
133
|
+
if (reference === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
const hash = reference.indexOf("#");
|
|
136
|
+
if (hash <= 0 || hash === reference.length - 1) {
|
|
137
|
+
errors.push(`${at} must be file#symbol`);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return repositoryPath(reference.slice(0, hash), at, errors) === undefined ? undefined : reference;
|
|
141
|
+
};
|
|
142
|
+
const confidence = (value, at, errors) => {
|
|
143
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1)
|
|
144
|
+
return value;
|
|
145
|
+
errors.push(`${at} must be a number from 0 to 1`);
|
|
146
|
+
return undefined;
|
|
147
|
+
};
|
|
148
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
149
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
150
|
+
/** Promise ids: `prom_` ids from Balladeer, and the evaluation harness's own ids. */
|
|
151
|
+
export const PROMISE_ID = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
152
|
+
const pattern = (regex, description) => (value, at, errors) => {
|
|
153
|
+
if (typeof value === "string" && regex.test(value))
|
|
154
|
+
return value;
|
|
155
|
+
errors.push(`${at} must be ${description}`);
|
|
156
|
+
return undefined;
|
|
157
|
+
};
|
|
158
|
+
const isoInstant = (value, at, errors) => {
|
|
159
|
+
if (typeof value === "string" && !Number.isNaN(Date.parse(value)))
|
|
160
|
+
return value;
|
|
161
|
+
errors.push(`${at} must be an ISO timestamp`);
|
|
162
|
+
return undefined;
|
|
163
|
+
};
|
|
164
|
+
const meaningChecker = record({
|
|
165
|
+
title: text(MAP_LIMITS.text, false),
|
|
166
|
+
observableOutcome: text(16_000, false),
|
|
167
|
+
failingExamples: listOf(record({ id: caseId, text: text(8000, false) }), MAP_LIMITS.sinks),
|
|
168
|
+
nonGoals: listOf(text(MAP_LIMITS.text), 40),
|
|
169
|
+
surfaces: listOf(text(MAP_LIMITS.name, false), 40),
|
|
170
|
+
}, ["surfaces"]);
|
|
171
|
+
const mapChecker = record({
|
|
172
|
+
schemaVersion: oneOf([BEHAVIOR_MAP_SCHEMA_VERSION]),
|
|
173
|
+
promiseId: pattern(PROMISE_ID, "a promise id"),
|
|
174
|
+
semanticDigest: pattern(DIGEST, "sha256: followed by 64 hex characters"),
|
|
175
|
+
mappedAt: record({
|
|
176
|
+
sha: pattern(SHA, "a 40-character commit sha"),
|
|
177
|
+
at: isoInstant,
|
|
178
|
+
client: oneOf(["claude", "codex"]),
|
|
179
|
+
model: text(MAP_LIMITS.name),
|
|
180
|
+
}),
|
|
181
|
+
entryPoints: listOf(record({
|
|
182
|
+
file: repositoryPath,
|
|
183
|
+
symbol: text(MAP_LIMITS.name, false),
|
|
184
|
+
kind: oneOf(ENTRY_POINT_KINDS),
|
|
185
|
+
note: text(MAP_LIMITS.text),
|
|
186
|
+
}), MAP_LIMITS.entryPoints),
|
|
187
|
+
symbols: listOf(record({
|
|
188
|
+
file: repositoryPath,
|
|
189
|
+
name: text(MAP_LIMITS.name, false),
|
|
190
|
+
kind: oneOf(SYMBOL_KINDS),
|
|
191
|
+
role: oneOf(SYMBOL_ROLES),
|
|
192
|
+
}), MAP_LIMITS.symbols),
|
|
193
|
+
resources: listOf(record({
|
|
194
|
+
kind: oneOf(RESOURCE_KINDS),
|
|
195
|
+
name: text(MAP_LIMITS.name, false),
|
|
196
|
+
access: oneOf(RESOURCE_ACCESS),
|
|
197
|
+
}), MAP_LIMITS.resources),
|
|
198
|
+
linkedTests: listOf(record({
|
|
199
|
+
file: repositoryPath,
|
|
200
|
+
name: text(MAP_LIMITS.name),
|
|
201
|
+
cases: listOf(caseId, 40),
|
|
202
|
+
}), MAP_LIMITS.linkedTests),
|
|
203
|
+
sinks: listOf(record({
|
|
204
|
+
caseId,
|
|
205
|
+
effect: text(MAP_LIMITS.text, false),
|
|
206
|
+
resources: listOf(text(MAP_LIMITS.name, false), 20),
|
|
207
|
+
symbols: listOf(fileSymbol, 20),
|
|
208
|
+
}), MAP_LIMITS.sinks),
|
|
209
|
+
confidence,
|
|
210
|
+
notes: text(MAP_LIMITS.text),
|
|
211
|
+
meaning: meaningChecker,
|
|
212
|
+
}, ["meaning"]);
|
|
213
|
+
/** A whole map, checked against the contract. Unknown fields are refused. */
|
|
214
|
+
export function parseBehaviorMap(value) {
|
|
215
|
+
const errors = [];
|
|
216
|
+
const map = mapChecker(value, "map", errors);
|
|
217
|
+
return map === undefined ? { ok: false, errors } : { ok: true, value: map };
|
|
218
|
+
}
|
|
219
|
+
/** Every file a map names: the derived `files` field, which is never stored. */
|
|
220
|
+
export function mapFiles(map) {
|
|
221
|
+
return [
|
|
222
|
+
...new Set([
|
|
223
|
+
...map.entryPoints.map((entry) => entry.file),
|
|
224
|
+
...map.symbols.map((entry) => entry.file),
|
|
225
|
+
...map.linkedTests.map((entry) => entry.file),
|
|
226
|
+
]),
|
|
227
|
+
].sort();
|
|
228
|
+
}
|
|
229
|
+
/** The map as it is written to disk: two-space JSON and a final newline. */
|
|
230
|
+
export function serializeBehaviorMap(map) {
|
|
231
|
+
return `${JSON.stringify(map, null, 2)}\n`;
|
|
232
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ type Parsed = Readonly<{
|
|
|
33
33
|
*/
|
|
34
34
|
repositories: readonly string[];
|
|
35
35
|
hook: "codex" | "claude" | undefined;
|
|
36
|
-
event: "SessionStart" | "UserPromptSubmit" | "SubagentStart" | undefined;
|
|
36
|
+
event: "SessionStart" | "UserPromptSubmit" | "SubagentStart" | "PostToolUse" | undefined;
|
|
37
37
|
owner: string | undefined;
|
|
38
38
|
/** `check-seals --install-hook`: write the optional pre-push hook. */
|
|
39
39
|
installHook: boolean;
|
|
@@ -45,6 +45,11 @@ type Parsed = Readonly<{
|
|
|
45
45
|
again: boolean;
|
|
46
46
|
/** `check-seals --runner`: where the pinned runner is, when it has moved. */
|
|
47
47
|
runner: string | undefined;
|
|
48
|
+
/** `risk --base` and `--head`: the two ends of the change. */
|
|
49
|
+
base: string | undefined;
|
|
50
|
+
head: string | undefined;
|
|
51
|
+
/** `risk --maps`: where the behavior maps are, when not in the checkout. */
|
|
52
|
+
maps: string | undefined;
|
|
48
53
|
createWorkspace: string | undefined;
|
|
49
54
|
/** Everything that was not a flag: the addresses `invite` sends to. */
|
|
50
55
|
positional: readonly string[];
|
package/dist/cli.js
CHANGED
|
@@ -9,10 +9,14 @@ import { runDiscover } from "./commands/discover.js";
|
|
|
9
9
|
import { runExplain } from "./commands/explain.js";
|
|
10
10
|
import { parseRole, runInvite } from "./commands/invite.js";
|
|
11
11
|
import { runGuidance } from "./commands/guidance.js";
|
|
12
|
+
import { JUDGE_USAGE, judgeCommand } from "./commands/judge.js";
|
|
13
|
+
import { MAP_USAGE, mapCommand } from "./commands/map.js";
|
|
14
|
+
import { OFFERS_USAGE, offersCommand } from "./commands/offers.js";
|
|
12
15
|
import { runMcp } from "./commands/mcp.js";
|
|
13
16
|
import { runPrepare } from "./commands/prepare.js";
|
|
14
17
|
import { runPropose } from "./commands/propose.js";
|
|
15
18
|
import { runRepositories } from "./commands/repositories.js";
|
|
19
|
+
import { runRisk } from "./commands/risk.js";
|
|
16
20
|
import { CREATE_WORKSPACE_MAX_LENGTH, runSetup } from "./commands/setup.js";
|
|
17
21
|
import { runSession } from "./commands/session.js";
|
|
18
22
|
import { runStatus } from "./commands/status.js";
|
|
@@ -21,7 +25,7 @@ import { runWhoami } from "./commands/whoami.js";
|
|
|
21
25
|
import { runInstall } from "./install.js";
|
|
22
26
|
import { openInBrowser } from "./open-browser.js";
|
|
23
27
|
import { PUBLISHED_SPECIFIER } from "./release.js";
|
|
24
|
-
import {
|
|
28
|
+
import { codexHookApproval, installUserScope, pushCheckSetupLine, pushCheckState, runningFromCheckout, userHome, } from "./user-scope.js";
|
|
25
29
|
import { updateNotice } from "./currency.js";
|
|
26
30
|
import { describeEarlier, describeRemoval, findEarlier, removeEarlier } from "./remove-earlier.js";
|
|
27
31
|
import { recordSelfUpdateState, SELF_UPDATE_ENVIRONMENT } from "./self-update.js";
|
|
@@ -155,6 +159,17 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
155
159
|
answer whose verifier has changed since the map was built. It reads one
|
|
156
160
|
local file and contacts nothing.
|
|
157
161
|
|
|
162
|
+
${MAP_USAGE}
|
|
163
|
+
${JUDGE_USAGE}
|
|
164
|
+
${OFFERS_USAGE}
|
|
165
|
+
${CLI_INVOCATION} risk [--base <ref>] [--head <ref>] [--json] [--maps <dir>]
|
|
166
|
+
Rank the promises this change could put at risk, with the evidence for
|
|
167
|
+
each: a mapped function or test it changes, a table, route or file it
|
|
168
|
+
shares, what imports what, and what its commit messages say. It reads
|
|
169
|
+
the behavior maps in .continuity/behavior-maps/ and the change from git,
|
|
170
|
+
from the merge base with this branch's upstream (else origin/main) to
|
|
171
|
+
HEAD unless named. Runs offline; nothing is sent. --json prints the report.
|
|
172
|
+
|
|
158
173
|
${CLI_INVOCATION} session [--new] [--record] [--repo owner/name] [--repository <uuid>] [--json]
|
|
159
174
|
[--control-plane <url>]
|
|
160
175
|
Print the id for this piece of work, and the one line to write into the
|
|
@@ -211,6 +226,9 @@ export function parseArguments(argv) {
|
|
|
211
226
|
let record = false;
|
|
212
227
|
let again = false;
|
|
213
228
|
let runner;
|
|
229
|
+
let base;
|
|
230
|
+
let head;
|
|
231
|
+
let maps;
|
|
214
232
|
let controlPlane;
|
|
215
233
|
let repo;
|
|
216
234
|
let file;
|
|
@@ -229,6 +247,9 @@ export function parseArguments(argv) {
|
|
|
229
247
|
"--role": "contributor, viewer, or administrator",
|
|
230
248
|
"--client": "codex or claude",
|
|
231
249
|
"--runner": "a path to the pinned runner's cli.js",
|
|
250
|
+
"--base": "a git ref",
|
|
251
|
+
"--head": "a git ref",
|
|
252
|
+
"--maps": "a directory of behavior maps",
|
|
232
253
|
};
|
|
233
254
|
const value = (flag, inline) => {
|
|
234
255
|
const next = inline ?? args.shift();
|
|
@@ -254,8 +275,9 @@ export function parseArguments(argv) {
|
|
|
254
275
|
const requested = value("--event", inline);
|
|
255
276
|
if (requested !== "SessionStart" &&
|
|
256
277
|
requested !== "UserPromptSubmit" &&
|
|
257
|
-
requested !== "SubagentStart"
|
|
258
|
-
|
|
278
|
+
requested !== "SubagentStart" &&
|
|
279
|
+
requested !== "PostToolUse")
|
|
280
|
+
throw new StoreError("usage", "--event needs SessionStart, UserPromptSubmit, SubagentStart or PostToolUse.");
|
|
259
281
|
event = requested;
|
|
260
282
|
}
|
|
261
283
|
else if (name === "--json")
|
|
@@ -270,6 +292,12 @@ export function parseArguments(argv) {
|
|
|
270
292
|
record = true;
|
|
271
293
|
else if (name === "--runner")
|
|
272
294
|
runner = value("--runner", inline);
|
|
295
|
+
else if (name === "--base")
|
|
296
|
+
base = value("--base", inline);
|
|
297
|
+
else if (name === "--head")
|
|
298
|
+
head = value("--head", inline);
|
|
299
|
+
else if (name === "--maps")
|
|
300
|
+
maps = value("--maps", inline);
|
|
273
301
|
else if (name === "--wait")
|
|
274
302
|
wait = true;
|
|
275
303
|
else if (name === "--no-open")
|
|
@@ -380,6 +408,8 @@ export function parseArguments(argv) {
|
|
|
380
408
|
}
|
|
381
409
|
if (hook !== undefined && command !== "guidance")
|
|
382
410
|
throw new StoreError("usage", "--hook belongs to guidance.");
|
|
411
|
+
if ((base !== undefined || head !== undefined || maps !== undefined) && command !== "risk")
|
|
412
|
+
throw new StoreError("usage", "--base, --head and --maps belong to risk.");
|
|
383
413
|
if (event !== undefined && command !== "guidance")
|
|
384
414
|
throw new StoreError("usage", "--event belongs to guidance.");
|
|
385
415
|
const chosen = controlPlane ?? process.env.BALLADEER_CONTROL_PLANE?.trim() ?? DEFAULT_CONTROL_PLANE;
|
|
@@ -407,6 +437,9 @@ export function parseArguments(argv) {
|
|
|
407
437
|
record,
|
|
408
438
|
again,
|
|
409
439
|
runner,
|
|
440
|
+
base,
|
|
441
|
+
head,
|
|
442
|
+
maps,
|
|
410
443
|
createWorkspace,
|
|
411
444
|
positional,
|
|
412
445
|
role,
|
|
@@ -414,6 +447,20 @@ export function parseArguments(argv) {
|
|
|
414
447
|
};
|
|
415
448
|
}
|
|
416
449
|
export async function main(argv) {
|
|
450
|
+
// `map`, `judge` and `offers` read their own flags: `--hook`, `--repo` and
|
|
451
|
+
// `--install-hook` mean something different there, and `judge --hook` runs
|
|
452
|
+
// inside a coding host where a stderr nag would reach the agent.
|
|
453
|
+
if (argv[0] === "map" || argv[0] === "judge" || argv[0] === "offers") {
|
|
454
|
+
const io = {
|
|
455
|
+
cwd: process.cwd(),
|
|
456
|
+
environment: process.env,
|
|
457
|
+
write: (text) => void process.stdout.write(text),
|
|
458
|
+
error: (text) => void process.stderr.write(text),
|
|
459
|
+
};
|
|
460
|
+
if (argv[0] === "offers")
|
|
461
|
+
return offersCommand(argv.slice(1), io);
|
|
462
|
+
return argv[0] === "map" ? mapCommand(argv.slice(1), io) : judgeCommand(argv.slice(1), io);
|
|
463
|
+
}
|
|
417
464
|
let parsed;
|
|
418
465
|
try {
|
|
419
466
|
parsed = parseArguments(argv);
|
|
@@ -451,12 +498,14 @@ function commandVersion(name) {
|
|
|
451
498
|
function installEverything(options) {
|
|
452
499
|
const published = !runningFromCheckout();
|
|
453
500
|
let writes;
|
|
501
|
+
let pushCheck = "unavailable";
|
|
454
502
|
const register = (installed) => {
|
|
455
503
|
writes = installUserScope({
|
|
456
504
|
environment: process.env,
|
|
457
505
|
published,
|
|
458
506
|
...(installed === undefined ? {} : { installed }),
|
|
459
507
|
});
|
|
508
|
+
pushCheck = pushCheckState(process.env, published, installed);
|
|
460
509
|
return writes;
|
|
461
510
|
};
|
|
462
511
|
const code = runInstall({
|
|
@@ -464,12 +513,15 @@ function installEverything(options) {
|
|
|
464
513
|
json: options.json,
|
|
465
514
|
write: options.write,
|
|
466
515
|
allowPartial: options.allowPartial,
|
|
467
|
-
onInstalled: (result) =>
|
|
516
|
+
onInstalled: (result) => {
|
|
517
|
+
const userScope = register(result.command);
|
|
518
|
+
return { userScope, pushCheck };
|
|
519
|
+
},
|
|
468
520
|
});
|
|
469
521
|
if (writes === undefined) {
|
|
470
522
|
const fallback = register();
|
|
471
523
|
if (options.json)
|
|
472
|
-
options.write(`${JSON.stringify({ step: "user_scope", userScope: fallback })}\n`);
|
|
524
|
+
options.write(`${JSON.stringify({ step: "user_scope", userScope: fallback, pushCheck })}\n`);
|
|
473
525
|
}
|
|
474
526
|
for (const w of writes ?? [])
|
|
475
527
|
if (!options.json)
|
|
@@ -485,7 +537,7 @@ function installEverything(options) {
|
|
|
485
537
|
if (options.json)
|
|
486
538
|
options.write(`${JSON.stringify({ step: "codex_hooks", approval: codex.status === "written" ? "required" : "check", command: "/hooks" })}\n`);
|
|
487
539
|
else
|
|
488
|
-
options.write(`${
|
|
540
|
+
options.write(`${codexHookApproval(pushCheck !== "unavailable")}\n`);
|
|
489
541
|
}
|
|
490
542
|
// With the user-scope form in place, the older per-repository form in this
|
|
491
543
|
// checkout only makes the session hear the guidance twice. Out it goes, with
|
|
@@ -508,6 +560,13 @@ function installEverything(options) {
|
|
|
508
560
|
options.write("Balladeer now runs in every Claude Code" +
|
|
509
561
|
((writes ?? []).some((w) => w.host === "codex") ? " and Codex" : "") +
|
|
510
562
|
" session on this machine. In a folder of a connected repository it works as before; anywhere else it stays quiet, and `balladeer status` there says why.\n");
|
|
563
|
+
// The push check rides on the same hooks file, so it is on wherever that
|
|
564
|
+
// file was written: said once per run, with how to turn it off. Under
|
|
565
|
+
// --json it is the `pushCheck` field of the install step, so that step stays
|
|
566
|
+
// one object.
|
|
567
|
+
const hooksFile = (writes ?? []).find((w) => w.path.endsWith("settings.json"));
|
|
568
|
+
if (!options.json && hooksFile !== undefined && hooksFile.status !== "refused")
|
|
569
|
+
options.write(`${pushCheckSetupLine(pushCheck)}\n`);
|
|
511
570
|
const outcome = (writes ?? []).some((w) => w.status === "refused") ? 4 : code;
|
|
512
571
|
// Started by the session hook's background update: leave how it ended where
|
|
513
572
|
// the next hook fire will find it and tell the server.
|
|
@@ -654,6 +713,15 @@ async function dispatch(parsed, write) {
|
|
|
654
713
|
cwd: process.cwd(),
|
|
655
714
|
write,
|
|
656
715
|
});
|
|
716
|
+
case "risk":
|
|
717
|
+
return runRisk({
|
|
718
|
+
json: parsed.json,
|
|
719
|
+
...(parsed.base === undefined ? {} : { base: parsed.base }),
|
|
720
|
+
...(parsed.head === undefined ? {} : { head: parsed.head }),
|
|
721
|
+
...(parsed.maps === undefined ? {} : { maps: parsed.maps }),
|
|
722
|
+
cwd: process.cwd(),
|
|
723
|
+
write,
|
|
724
|
+
});
|
|
657
725
|
case "affected":
|
|
658
726
|
return runAffected({
|
|
659
727
|
json: parsed.json,
|
|
@@ -5,7 +5,8 @@ export type GuidanceOptions = Readonly<{
|
|
|
5
5
|
controlPlane: string;
|
|
6
6
|
repositoryId?: string;
|
|
7
7
|
hook: "codex" | "claude";
|
|
8
|
-
|
|
8
|
+
/** PostToolUse is the relay note, not guidance: see `runRelayNote`. */
|
|
9
|
+
event?: GuidanceEvent | "PostToolUse";
|
|
9
10
|
runtimeGeneration?: string;
|
|
10
11
|
environment: NodeJS.ProcessEnv;
|
|
11
12
|
cwd: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { relayHook } from "../relay.js";
|
|
1
2
|
import { validateProjectGuidanceScope } from "../guidance-install.js";
|
|
2
3
|
import { GUIDANCE_UNAVAILABLE, loadGuidance, recordGuidanceContext, } from "../guidance.js";
|
|
3
4
|
import { readCredentials } from "../store.js";
|
|
@@ -60,8 +61,62 @@ function eventFrom(input) {
|
|
|
60
61
|
function identifier(input) {
|
|
61
62
|
return typeof input === "string" && input.length > 0 && input.length <= 256 ? input : undefined;
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* After a Balladeer tool acts, a note the agent reads before it replies: tell
|
|
66
|
+
* the person what Balladeer did, in your own words (promise
|
|
67
|
+
* prom_45ef4e3d9e5545a9a411; plans/promise-relations/plan.md, step 1). Codex
|
|
68
|
+
* relayed every detail in 10 of 10 lab sessions with it and 6 of 10 without.
|
|
69
|
+
* Local only: no connection, no network, nothing stored. A tool result can be
|
|
70
|
+
* large, so the input bound is wider than guidance's; anything unreadable,
|
|
71
|
+
* oversized or late prints nothing, never the guidance-unavailable notice.
|
|
72
|
+
*/
|
|
73
|
+
async function runRelayNote(options) {
|
|
74
|
+
const input = await new Promise((resolve) => {
|
|
75
|
+
const chunks = [];
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
let settled = false;
|
|
78
|
+
const done = (value) => {
|
|
79
|
+
if (settled)
|
|
80
|
+
return;
|
|
81
|
+
settled = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
options.stdin.removeListener("data", data);
|
|
84
|
+
resolve(value);
|
|
85
|
+
};
|
|
86
|
+
const data = (chunk) => {
|
|
87
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
88
|
+
bytes += buffer.length;
|
|
89
|
+
if (bytes > 1_048_576)
|
|
90
|
+
done(undefined);
|
|
91
|
+
else
|
|
92
|
+
chunks.push(buffer);
|
|
93
|
+
};
|
|
94
|
+
const timer = setTimeout(() => done(undefined), 1_000);
|
|
95
|
+
options.stdin.on("data", data);
|
|
96
|
+
options.stdin.once("end", () => {
|
|
97
|
+
try {
|
|
98
|
+
done(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
done(undefined);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
options.stdin.once("error", () => done(undefined));
|
|
105
|
+
});
|
|
106
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
107
|
+
return 0;
|
|
108
|
+
const record = input;
|
|
109
|
+
if (record.hook_event_name !== "PostToolUse")
|
|
110
|
+
return 0;
|
|
111
|
+
const out = relayHook(record);
|
|
112
|
+
if (out)
|
|
113
|
+
options.write(`${out}\n`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
63
116
|
/** Hook stdout is bounded context only. Host prompts and transcript paths never leave this process. */
|
|
64
117
|
export async function runGuidance(options) {
|
|
118
|
+
if (options.event === "PostToolUse")
|
|
119
|
+
return runRelayNote(options);
|
|
65
120
|
// A project hook names its repository and must match the checkout it sits
|
|
66
121
|
// in. A user-scope hook names none: the folder's remotes decide.
|
|
67
122
|
const projectScoped = options.repositoryId !== undefined;
|