balladeer 1.0.16 → 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/dist/cli.d.ts +1 -1
- package/dist/cli.js +19 -6
- package/dist/commands/guidance.d.ts +2 -1
- package/dist/commands/guidance.js +55 -0
- package/dist/commands/judge.d.ts +168 -9
- package/dist/commands/judge.js +594 -51
- package/dist/commands/map.d.ts +46 -2
- package/dist/commands/map.js +242 -5
- package/dist/commands/mcp.js +2 -2
- package/dist/guidance-hook.mjs +278 -94
- package/dist/hook-trust.d.ts +41 -9
- package/dist/hook-trust.js +98 -16
- package/dist/judge-brief.d.ts +15 -3
- package/dist/judge-brief.js +15 -2
- package/dist/judge-hook.d.ts +188 -14
- package/dist/judge-hook.js +919 -61
- package/dist/judge-said.d.ts +52 -0
- package/dist/judge-said.js +181 -0
- package/dist/promise-meaning.d.ts +8 -0
- package/dist/promise-meaning.js +9 -1
- package/dist/relay.d.ts +71 -0
- package/dist/relay.js +193 -0
- package/dist/remove-earlier.js +4 -2
- package/dist/risk/index.d.ts +1 -1
- package/dist/risk/index.js +1 -1
- package/dist/risk/score.d.ts +10 -0
- package/dist/risk/score.js +11 -2
- package/dist/scratch-worktree.d.ts +5 -1
- package/dist/scratch-worktree.js +9 -2
- package/dist/user-scope.d.ts +74 -3
- package/dist/user-scope.js +270 -9
- package/dist/wire.d.ts +3 -3
- package/dist/wire.js +2 -2
- package/package.json +1 -1
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;
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import { runWhoami } from "./commands/whoami.js";
|
|
|
25
25
|
import { runInstall } from "./install.js";
|
|
26
26
|
import { openInBrowser } from "./open-browser.js";
|
|
27
27
|
import { PUBLISHED_SPECIFIER } from "./release.js";
|
|
28
|
-
import {
|
|
28
|
+
import { codexHookApproval, installUserScope, pushCheckSetupLine, pushCheckState, runningFromCheckout, userHome, } from "./user-scope.js";
|
|
29
29
|
import { updateNotice } from "./currency.js";
|
|
30
30
|
import { describeEarlier, describeRemoval, findEarlier, removeEarlier } from "./remove-earlier.js";
|
|
31
31
|
import { recordSelfUpdateState, SELF_UPDATE_ENVIRONMENT } from "./self-update.js";
|
|
@@ -275,8 +275,9 @@ export function parseArguments(argv) {
|
|
|
275
275
|
const requested = value("--event", inline);
|
|
276
276
|
if (requested !== "SessionStart" &&
|
|
277
277
|
requested !== "UserPromptSubmit" &&
|
|
278
|
-
requested !== "SubagentStart"
|
|
279
|
-
|
|
278
|
+
requested !== "SubagentStart" &&
|
|
279
|
+
requested !== "PostToolUse")
|
|
280
|
+
throw new StoreError("usage", "--event needs SessionStart, UserPromptSubmit, SubagentStart or PostToolUse.");
|
|
280
281
|
event = requested;
|
|
281
282
|
}
|
|
282
283
|
else if (name === "--json")
|
|
@@ -497,12 +498,14 @@ function commandVersion(name) {
|
|
|
497
498
|
function installEverything(options) {
|
|
498
499
|
const published = !runningFromCheckout();
|
|
499
500
|
let writes;
|
|
501
|
+
let pushCheck = "unavailable";
|
|
500
502
|
const register = (installed) => {
|
|
501
503
|
writes = installUserScope({
|
|
502
504
|
environment: process.env,
|
|
503
505
|
published,
|
|
504
506
|
...(installed === undefined ? {} : { installed }),
|
|
505
507
|
});
|
|
508
|
+
pushCheck = pushCheckState(process.env, published, installed);
|
|
506
509
|
return writes;
|
|
507
510
|
};
|
|
508
511
|
const code = runInstall({
|
|
@@ -510,12 +513,15 @@ function installEverything(options) {
|
|
|
510
513
|
json: options.json,
|
|
511
514
|
write: options.write,
|
|
512
515
|
allowPartial: options.allowPartial,
|
|
513
|
-
onInstalled: (result) =>
|
|
516
|
+
onInstalled: (result) => {
|
|
517
|
+
const userScope = register(result.command);
|
|
518
|
+
return { userScope, pushCheck };
|
|
519
|
+
},
|
|
514
520
|
});
|
|
515
521
|
if (writes === undefined) {
|
|
516
522
|
const fallback = register();
|
|
517
523
|
if (options.json)
|
|
518
|
-
options.write(`${JSON.stringify({ step: "user_scope", userScope: fallback })}\n`);
|
|
524
|
+
options.write(`${JSON.stringify({ step: "user_scope", userScope: fallback, pushCheck })}\n`);
|
|
519
525
|
}
|
|
520
526
|
for (const w of writes ?? [])
|
|
521
527
|
if (!options.json)
|
|
@@ -531,7 +537,7 @@ function installEverything(options) {
|
|
|
531
537
|
if (options.json)
|
|
532
538
|
options.write(`${JSON.stringify({ step: "codex_hooks", approval: codex.status === "written" ? "required" : "check", command: "/hooks" })}\n`);
|
|
533
539
|
else
|
|
534
|
-
options.write(`${
|
|
540
|
+
options.write(`${codexHookApproval(pushCheck !== "unavailable")}\n`);
|
|
535
541
|
}
|
|
536
542
|
// With the user-scope form in place, the older per-repository form in this
|
|
537
543
|
// checkout only makes the session hear the guidance twice. Out it goes, with
|
|
@@ -554,6 +560,13 @@ function installEverything(options) {
|
|
|
554
560
|
options.write("Balladeer now runs in every Claude Code" +
|
|
555
561
|
((writes ?? []).some((w) => w.host === "codex") ? " and Codex" : "") +
|
|
556
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`);
|
|
557
570
|
const outcome = (writes ?? []).some((w) => w.status === "refused") ? 4 : code;
|
|
558
571
|
// Started by the session hook's background update: leave how it ended where
|
|
559
572
|
// the next hook fire will find it and tell the server.
|
|
@@ -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;
|
package/dist/commands/judge.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type BehaviorMap } from "../behavior-map-schema.js";
|
|
2
2
|
import { type HeadlessAgentOptions, type HeadlessAgentResult, type HeadlessClient, type LoginCheck } from "../headless-agent.js";
|
|
3
|
-
import { type HookHost } from "../judge-hook.js";
|
|
3
|
+
import { type HookHost, type MergeTarget } from "../judge-hook.js";
|
|
4
4
|
import { type PromiseDocument, type PromiseReader } from "../promise-meaning.js";
|
|
5
|
+
import { type CarriedFile } from "../scratch-worktree.js";
|
|
6
|
+
import type { Features } from "../risk/contract.js";
|
|
5
7
|
/**
|
|
6
8
|
* `balladeer judge`: for a change about to leave this machine, ask the person's
|
|
7
9
|
* own coding agent, one promise at a time, whether the change keeps it.
|
|
@@ -18,7 +20,15 @@ export declare const JUDGE_VERDICT_SCHEMA_VERSION = "balladeer-judge-verdict/v1"
|
|
|
18
20
|
export declare const JUDGE_CONFIG_SCHEMA_VERSION = "balladeer-judge-config/v1";
|
|
19
21
|
export declare const JUDGE_CONFIG_PATH = ".continuity/judge.json";
|
|
20
22
|
export declare const JUDGE_LOG_DIRECTORY = ".continuity/judge-log";
|
|
23
|
+
/**
|
|
24
|
+
* Where a verified reproduction's files are kept, one folder per promise and
|
|
25
|
+
* pushed commit, inside the judge log so git ignores them like the log itself.
|
|
26
|
+
*/
|
|
27
|
+
export declare const KEPT_REPRODUCTIONS_DIRECTORY = ".continuity/judge-log/reproductions";
|
|
28
|
+
export declare const KEPT_REPRODUCTION_SCHEMA_VERSION = "balladeer-kept-reproduction/v1";
|
|
21
29
|
export declare const STILL_JUDGING = "still judging; rerun `balladeer judge --wait`";
|
|
30
|
+
/** Block mode's answer to a line that commits and then pushes: the check runs before the line. */
|
|
31
|
+
export declare const COMMIT_THEN_PUSH = "Held: this command makes a commit and then pushes it, and the check runs before the command, so it cannot see a commit that has not been made yet. Commit first, then push in a separate command.";
|
|
22
32
|
export type JudgeConfig = Readonly<{
|
|
23
33
|
schemaVersion: typeof JUDGE_CONFIG_SCHEMA_VERSION;
|
|
24
34
|
mode: "report" | "block";
|
|
@@ -28,6 +38,13 @@ export type JudgeConfig = Readonly<{
|
|
|
28
38
|
sampleBelowLine: number;
|
|
29
39
|
alwaysJudge: readonly string[];
|
|
30
40
|
client: HeadlessClient | "auto";
|
|
41
|
+
/**
|
|
42
|
+
* Which promises the risk report hands to a judge: "direct" (the default)
|
|
43
|
+
* takes every promise whose own code or tests the change edits, and nothing
|
|
44
|
+
* else; "ranked" takes the top `topK` in the report's order plus a random
|
|
45
|
+
* sample below that line.
|
|
46
|
+
*/
|
|
47
|
+
select: "direct" | "ranked";
|
|
31
48
|
/**
|
|
32
49
|
* From the push hook, also read the change for rules no promise records and
|
|
33
50
|
* hand at most three to the pushing session as offers. Off unless set.
|
|
@@ -64,10 +81,25 @@ export type JudgeVerdict = {
|
|
|
64
81
|
failsOnHead: boolean;
|
|
65
82
|
passesOnBase: boolean;
|
|
66
83
|
digest: string;
|
|
84
|
+
/**
|
|
85
|
+
* Where the reproduction was kept on this machine, repository-relative:
|
|
86
|
+
* a folder holding `manifest.json` and, under `files/`, the files the
|
|
87
|
+
* judge added, at the paths it wrote them. Only for a break verified both
|
|
88
|
+
* ways, and absent when keeping it failed.
|
|
89
|
+
*/
|
|
90
|
+
keptIn?: string;
|
|
91
|
+
/** The kept files' repository-relative paths, each under `<keptIn>/files/`. */
|
|
92
|
+
keptFiles?: string[];
|
|
67
93
|
} | null;
|
|
68
94
|
reproductionRejected: string | null;
|
|
69
95
|
exercisedCases: string[];
|
|
70
96
|
notes: string;
|
|
97
|
+
/**
|
|
98
|
+
* On a could-not-tell the judge itself reached because the tests need
|
|
99
|
+
* something this machine lacks: the one step that would let them run, as
|
|
100
|
+
* the judge wrote it. Absent otherwise. It never changes the verdict.
|
|
101
|
+
*/
|
|
102
|
+
nextStep?: string;
|
|
71
103
|
judge: {
|
|
72
104
|
client: HeadlessClient;
|
|
73
105
|
model: string;
|
|
@@ -101,6 +133,8 @@ export type ReproductionCheck = Readonly<{
|
|
|
101
133
|
rejected: string | null;
|
|
102
134
|
/** The budget ran out while it was being run, so there is no answer yet. */
|
|
103
135
|
stopped: boolean;
|
|
136
|
+
/** The files the judge added, carried to base and fingerprinted into `digest`. */
|
|
137
|
+
files: readonly CarriedFile[];
|
|
104
138
|
}>;
|
|
105
139
|
/**
|
|
106
140
|
* Run a judge's reproduction both ways.
|
|
@@ -123,6 +157,42 @@ export declare function verifyReproduction(input: Readonly<{
|
|
|
123
157
|
signal?: AbortSignal;
|
|
124
158
|
scratch?: string;
|
|
125
159
|
}>): Promise<ReproductionCheck>;
|
|
160
|
+
export type KeptReproduction = Readonly<{
|
|
161
|
+
/** Repository-relative, forward slashes: the folder under the judge log. */
|
|
162
|
+
keptIn: string;
|
|
163
|
+
/** The files kept, repository-relative, each at `<keptIn>/files/<path>`. */
|
|
164
|
+
files: string[];
|
|
165
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* Keep a verified reproduction on this machine, so the break can become a
|
|
168
|
+
* lasting test instead of vanishing with the judge's checkout.
|
|
169
|
+
*
|
|
170
|
+
* The files the judge added are copied from its checkout into
|
|
171
|
+
* `.continuity/judge-log/reproductions/<promise id>-<first 12 of head>/files/`
|
|
172
|
+
* at the paths the judge wrote them, beside a `manifest.json` that says which
|
|
173
|
+
* promise, meaning and change they reproduce a break of, with the command,
|
|
174
|
+
* its cwd, the digest and each file's own fingerprint. The digest can be
|
|
175
|
+
* recomputed from the manifest alone. A reproduction that added no files
|
|
176
|
+
* keeps just the manifest. The judge log carries its own `.gitignore`, so
|
|
177
|
+
* nothing here is ever committed, and nothing here is sent anywhere.
|
|
178
|
+
*
|
|
179
|
+
* The folder is written beside its final name and then renamed into place,
|
|
180
|
+
* replacing an earlier one for the same promise and commit, so a reader never
|
|
181
|
+
* sees half of one. Keeping is a courtesy to the person, never a condition of
|
|
182
|
+
* the verdict: when it fails, the verdict stands and says nothing was kept.
|
|
183
|
+
*/
|
|
184
|
+
export declare function keepReproduction(input: Readonly<{
|
|
185
|
+
root: string;
|
|
186
|
+
headTree: string;
|
|
187
|
+
promiseId: string;
|
|
188
|
+
semanticDigest: string;
|
|
189
|
+
base: string;
|
|
190
|
+
head: string;
|
|
191
|
+
command: string;
|
|
192
|
+
cwd: string;
|
|
193
|
+
digest: string;
|
|
194
|
+
files: readonly CarriedFile[];
|
|
195
|
+
}>): KeptReproduction | undefined;
|
|
126
196
|
/**
|
|
127
197
|
* How this repository runs one test file, when it can be told from the files
|
|
128
198
|
* alone. A runner that cannot be told is `none`, and the judge is left to find
|
|
@@ -137,7 +207,10 @@ type JudgeAnswer = Readonly<{
|
|
|
137
207
|
}>;
|
|
138
208
|
exercisedCases: string[];
|
|
139
209
|
notes: string;
|
|
210
|
+
nextStep?: string;
|
|
140
211
|
}>;
|
|
212
|
+
/** The longest `nextStep` kept from a judge's answer, in characters. */
|
|
213
|
+
export declare const NEXT_STEP_LIMIT = 500;
|
|
141
214
|
/** The judge's final JSON, read strictly enough that a malformed answer is not a verdict. */
|
|
142
215
|
export declare function readJudgeAnswer(json: Record<string, unknown> | undefined): JudgeAnswer | undefined;
|
|
143
216
|
export type JudgePromiseInput = Readonly<{
|
|
@@ -180,6 +253,10 @@ export type JudgeArguments = Readonly<{
|
|
|
180
253
|
mode?: "report" | "block";
|
|
181
254
|
wait: boolean;
|
|
182
255
|
hook?: HookHost;
|
|
256
|
+
/** Run by the entry setup writes at the host's user scope, which stands aside where it has nothing to do. */
|
|
257
|
+
userScope?: true;
|
|
258
|
+
/** Turn this laptop's user-scope push check on or off. */
|
|
259
|
+
pushCheck?: "on" | "off";
|
|
183
260
|
installHook?: HookHost | "agents";
|
|
184
261
|
fromFile?: string;
|
|
185
262
|
map?: string;
|
|
@@ -188,13 +265,20 @@ export type JudgeArguments = Readonly<{
|
|
|
188
265
|
repository?: string;
|
|
189
266
|
controlPlane: string;
|
|
190
267
|
}>;
|
|
191
|
-
export declare const JUDGE_USAGE = " npx -y balladeer@latest judge [--promises <id,id>] [--top <k>] [--base <ref>] [--head <ref>]\n [--report-only | --block] [--wait] [--json] [--client auto|claude|codex]\n [--from-file <promise.json> [--map <map.json>]]\n [--hook claude|codex|git] [--install-hook [claude|codex|git]]\n Judge whether this change keeps the promises it could break, one promise\n at a time, on your own Claude Code or Codex login. A break counts only\n with a command that fails with the change and passes without it, and\n this command runs it both ways itself; anything unproven is \"could not\n tell\". Report mode prints one line per promise and never stops anything;\n --block exits 2 on a reproduced break, and when judges are still running\n at the time limit it holds the push rather than letting it through.\n --wait runs without the time limit. Promises come from --promises, else\n
|
|
268
|
+
export declare const JUDGE_USAGE = " npx -y balladeer@latest judge [--promises <id,id>] [--top <k>] [--base <ref>] [--head <ref>]\n [--report-only | --block] [--wait] [--json] [--client auto|claude|codex]\n [--from-file <promise.json> [--map <map.json>]]\n [--hook claude|codex|git] [--install-hook [claude|codex|git]]\n [--push-check on|off]\n Judge whether this change keeps the promises it could break, one promise\n at a time, on your own Claude Code or Codex login. A break counts only\n with a command that fails with the change and passes without it, and\n this command runs it both ways itself; anything unproven is \"could not\n tell\". Report mode prints one line per promise and never stops anything;\n --block exits 2 on a reproduced break, and when judges are still running\n at the time limit it holds the push rather than letting it through.\n --wait runs without the time limit. Promises come from --promises, else\n from `balladeer risk`: every promise whose own code or tests the\n change edits, or with \"select\": \"ranked\" in .continuity/judge.json the\n top of its ranking and a small random sample; else every mapped\n promise; plus that file's alwaysJudge. Verdicts are kept in\n .continuity/judge-log/, which git ignores. --install-hook adds the push\n hook to .claude/settings.json and .codex/hooks.json, before and after\n each shell command: report mode judges after a push has landed, so the\n push never waits, and block mode judges before it. With git it writes\n .git/hooks/pre-push, which judges before the push in both modes. With\n \"offers\": true in .continuity/judge.json, the push hook also reads the\n change for rules no promise records and offers at most three to\n whoever is pushing; it records nothing and never holds a push (see\n `balladeer offers`). Setup also writes the same pair of entries at\n each host's user scope, running `judge --hook <host> --user-scope`,\n so the check runs in every connected repository with mapped promises\n and says nothing anywhere else; a checkout with its own entry keeps\n the check to that entry. --push-check off turns the user-scope check\n off on this laptop, and --push-check on turns it back on.\n";
|
|
192
269
|
export declare function parseJudgeArguments(argv: readonly string[]): JudgeArguments;
|
|
193
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* The subset of `balladeer-risk/v1` this command reads: enough to order the
|
|
272
|
+
* entries the way `balladeer risk` does. An entry without `features` reads as
|
|
273
|
+
* reaching its promise not at all, and one without `evidence` as none.
|
|
274
|
+
*/
|
|
194
275
|
export type RiskEntry = Readonly<{
|
|
195
276
|
promiseId: string;
|
|
196
277
|
score: number;
|
|
197
278
|
band: string;
|
|
279
|
+
features?: Features;
|
|
280
|
+
/** The total weight of the entry's reasons. */
|
|
281
|
+
evidence?: number;
|
|
198
282
|
}>;
|
|
199
283
|
/** Read a risk report out of whatever `balladeer risk --json` printed. */
|
|
200
284
|
export declare function readRiskOutput(stdout: string): RiskEntry[] | undefined;
|
|
@@ -209,10 +293,20 @@ export type Selection = Readonly<{
|
|
|
209
293
|
why: "file" | "named" | "risk" | "sample" | "mapped" | "always";
|
|
210
294
|
}>;
|
|
211
295
|
/**
|
|
212
|
-
* Which promises to judge: named ones, else
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
296
|
+
* Which promises to judge: named ones, else from the risk report, else every
|
|
297
|
+
* mapped promise; then `alwaysJudge`.
|
|
298
|
+
*
|
|
299
|
+
* From the risk report, in the order `balladeer risk` lists it (how directly
|
|
300
|
+
* the change reaches each promise, then score, then evidence, then id; a score
|
|
301
|
+
* alone ties at 1 and falls to the alphabet):
|
|
302
|
+
*
|
|
303
|
+
* - "direct" takes every promise whose own code or tests the change edits
|
|
304
|
+
* (directness tier 3), and nothing else. The first live evaluation found
|
|
305
|
+
* that this selects about 1.8 promises a change and included the broken one
|
|
306
|
+
* in 17 of 17 cases. Only a `--top` given by hand caps it.
|
|
307
|
+
* - "ranked" takes the top k with a score above zero, plus a random sample
|
|
308
|
+
* below that line. The sample is what keeps a ranking honest: a break found
|
|
309
|
+
* below the line is a miss the ranking made, counted rather than never seen.
|
|
216
310
|
*/
|
|
217
311
|
export declare function selectPromises(input: Readonly<{
|
|
218
312
|
named?: readonly string[];
|
|
@@ -234,8 +328,39 @@ export declare function findLoggedVerdict(root: string, key: Readonly<{
|
|
|
234
328
|
base: string;
|
|
235
329
|
head: string;
|
|
236
330
|
}>, now: Date): JudgeVerdict | undefined;
|
|
237
|
-
/**
|
|
238
|
-
export declare
|
|
331
|
+
/** Said after "fix the behavior": a deliberate change is the person's, and the promise its owner's. */
|
|
332
|
+
export declare const DELIBERATE_CHANGE = "If the person changed this behavior on purpose, do not undo their change: tell them it breaks this promise, whose meaning only its owner can change.";
|
|
333
|
+
export type VerdictLineContext = Readonly<{
|
|
334
|
+
/** The verdict was reached earlier for this same commit and read back from the log. */
|
|
335
|
+
reused?: boolean;
|
|
336
|
+
/**
|
|
337
|
+
* Whether the promise has a Balladeer check bound, as its connection said.
|
|
338
|
+
* Absent when that is unknown (a promise from a file or a map's excerpt),
|
|
339
|
+
* and then the line says nothing about a check rather than guessing.
|
|
340
|
+
*/
|
|
341
|
+
checkBound?: boolean;
|
|
342
|
+
}>;
|
|
343
|
+
/**
|
|
344
|
+
* What to do after a reproduced break, as one line an agent reads in order:
|
|
345
|
+
* where the test that shows it is kept, fix the behavior rather than the
|
|
346
|
+
* test, then make the break a lasting check in this repository's own tests,
|
|
347
|
+
* committed with the fix and linked to the promise's map, and tell the person
|
|
348
|
+
* in one line what was added. When the promise's own Balladeer check missed
|
|
349
|
+
* the break, a repair of that check is proposed to its owner as well.
|
|
350
|
+
*/
|
|
351
|
+
export declare function afterBreakLine(verdict: JudgeVerdict, checkBound?: boolean): string;
|
|
352
|
+
/**
|
|
353
|
+
* The verdict as it may be told now: a reproduction said to be kept whose
|
|
354
|
+
* folder is no longer on this machine (a verdict read back from the log after
|
|
355
|
+
* somebody cleared it) loses its kept fields, so the line never points at a
|
|
356
|
+
* file that is not there.
|
|
357
|
+
*/
|
|
358
|
+
export declare function stillKept(root: string, verdict: JudgeVerdict): JudgeVerdict;
|
|
359
|
+
/**
|
|
360
|
+
* One line per verdict, the promise named by its title and id. A broken one
|
|
361
|
+
* carries a second, indented line saying what to do next.
|
|
362
|
+
*/
|
|
363
|
+
export declare function verdictLine(verdict: JudgeVerdict, title: string, context?: VerdictLineContext): string;
|
|
239
364
|
export type JudgeDependencies = Readonly<{
|
|
240
365
|
runAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
|
|
241
366
|
resolveLogin?: (requested: HeadlessClient | "auto", prefer?: HeadlessClient) => Promise<LoginCheck | undefined>;
|
|
@@ -250,7 +375,15 @@ export type JudgeDependencies = Readonly<{
|
|
|
250
375
|
judge?: (input: JudgePromiseInput) => Promise<JudgeOutcome>;
|
|
251
376
|
/** The agent run that reads the change for offers, when they are on. */
|
|
252
377
|
offerAgent?: (options: HeadlessAgentOptions) => Promise<HeadlessAgentResult>;
|
|
378
|
+
/** The head commit of the pull request a `gh pr merge` names. */
|
|
379
|
+
pullRequestHead?: (root: string, target: MergeTarget) => Promise<string | undefined>;
|
|
380
|
+
/** Whether this laptop has a connection for the checkout, read locally. */
|
|
381
|
+
connected?: (root: string) => boolean;
|
|
253
382
|
}>;
|
|
383
|
+
/** Said instead of verdicts when the check cannot tell which repository a push leaves from. */
|
|
384
|
+
export declare const PUSH_REPOSITORY_UNKNOWN = "Balladeer judge: this push was not judged, because the check could not tell which repository it pushes.";
|
|
385
|
+
/** Said instead of verdicts when every pull request the command opens or merges is in another repository. */
|
|
386
|
+
export declare function pullRequestElsewhereLine(repo: string): string;
|
|
254
387
|
export type JudgeRunOptions = Readonly<{
|
|
255
388
|
args: JudgeArguments;
|
|
256
389
|
cwd: string;
|
|
@@ -259,7 +392,33 @@ export type JudgeRunOptions = Readonly<{
|
|
|
259
392
|
error: (text: string) => void;
|
|
260
393
|
deps?: JudgeDependencies;
|
|
261
394
|
}>;
|
|
395
|
+
/** What `--push-check off` and `--push-check on` say. */
|
|
396
|
+
export declare const PUSH_CHECK_OFF_LINE = "The push check is off on this laptop: after a push it now says nothing, in every repository. A repository's own entry from `balladeer judge --install-hook` still runs. Turn it back on with `balladeer judge --push-check on`.";
|
|
397
|
+
export declare const PUSH_CHECK_ON_LINE = "The push check is back on for this laptop: after a push in a connected repository whose promises have behavior maps, it reports what the push may have broken, and it never holds the push.";
|
|
398
|
+
/**
|
|
399
|
+
* Whether the push check's user-scope entry has this push to itself, decided
|
|
400
|
+
* before anything is started or fetched: the entry runs on every shell command
|
|
401
|
+
* in every folder on the laptop, and most folders have nothing to judge. It
|
|
402
|
+
* stands aside, saying nothing, when the laptop turned the check off, the
|
|
403
|
+
* folder is in no checkout, the checkout has the push check's own project
|
|
404
|
+
* entry for this host (that entry owns the check there, so a push is judged
|
|
405
|
+
* once), or no promise here has a behavior map. Only then is the connection
|
|
406
|
+
* looked for, which reads the checkout's remotes, and a checkout this laptop
|
|
407
|
+
* has not connected is left alone too.
|
|
408
|
+
*/
|
|
409
|
+
export declare function userScopeTakesThePush(input: {
|
|
410
|
+
cwd: string;
|
|
411
|
+
host: "claude" | "codex";
|
|
412
|
+
environment: NodeJS.ProcessEnv;
|
|
413
|
+
connected: (root: string) => boolean;
|
|
414
|
+
}): boolean;
|
|
262
415
|
/** `balladeer judge`, from arguments already parsed. */
|
|
416
|
+
/**
|
|
417
|
+
* The line an answer opens with when the command makes a commit before it
|
|
418
|
+
* pushes. Robert approved it on 30 September 2026: the check keeps running
|
|
419
|
+
* before the push, and says plainly that it did not see the commit.
|
|
420
|
+
*/
|
|
421
|
+
export declare function commitFirstLine(made: string): string;
|
|
263
422
|
export declare function runJudge(options: JudgeRunOptions): Promise<number>;
|
|
264
423
|
/** `balladeer judge ...` from raw arguments, as `cli.ts` hands them over. */
|
|
265
424
|
export declare function judgeCommand(argv: readonly string[], io: Readonly<{
|