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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { PROMISE_ID } from "../behavior-map-schema.js";
|
|
2
|
+
import { BANDS, CHANGE_EXTRACTION_SCHEMA_VERSION, DELTA_KINDS, FILE_STATUSES, INTENT_SOURCES, LANGUAGES, RESOURCE_CHANGES, RESOURCE_KINDS, RISK_REPORT_SCHEMA_VERSION, SIGNALS, SYMBOL_CHANGES, SYMBOL_EXTRACTORS, SYMBOL_KINDS, } from "./contract.js";
|
|
3
|
+
const string = (max = 20_000) => (value, at, errors) => {
|
|
4
|
+
if (typeof value === "string" && value.length <= max)
|
|
5
|
+
return true;
|
|
6
|
+
errors.push(`${at} must be a string of at most ${max} characters`);
|
|
7
|
+
return false;
|
|
8
|
+
};
|
|
9
|
+
const pattern = (regex, description) => (value, at, errors) => {
|
|
10
|
+
if (typeof value === "string" && regex.test(value))
|
|
11
|
+
return true;
|
|
12
|
+
errors.push(`${at} must be ${description}`);
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
const oneOf = (values) => (value, at, errors) => {
|
|
16
|
+
if (typeof value === "string" && values.includes(value))
|
|
17
|
+
return true;
|
|
18
|
+
errors.push(`${at} must be one of ${values.join(", ")}`);
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
21
|
+
const number = (min, max, integer = false) => (value, at, errors) => {
|
|
22
|
+
if (typeof value === "number" &&
|
|
23
|
+
Number.isFinite(value) &&
|
|
24
|
+
value >= min &&
|
|
25
|
+
value <= max &&
|
|
26
|
+
(!integer || Number.isInteger(value)))
|
|
27
|
+
return true;
|
|
28
|
+
errors.push(`${at} must be ${integer ? "an integer" : "a number"} from ${min} to ${max}`);
|
|
29
|
+
return false;
|
|
30
|
+
};
|
|
31
|
+
const nullable = (check) => (value, at, errors) => value === null || check(value, at, errors);
|
|
32
|
+
const boolean = (value, at, errors) => {
|
|
33
|
+
if (typeof value === "boolean")
|
|
34
|
+
return true;
|
|
35
|
+
errors.push(`${at} must be true or false`);
|
|
36
|
+
return false;
|
|
37
|
+
};
|
|
38
|
+
const listOf = (item) => (value, at, errors) => {
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
errors.push(`${at} must be a list`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
let ok = true;
|
|
44
|
+
value.forEach((entry, index) => {
|
|
45
|
+
if (!item(entry, `${at}[${index}]`, errors))
|
|
46
|
+
ok = false;
|
|
47
|
+
});
|
|
48
|
+
return ok;
|
|
49
|
+
};
|
|
50
|
+
const record = (fields, optional = []) => (value, at, errors) => {
|
|
51
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
52
|
+
errors.push(`${at} must be an object`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const input = value;
|
|
56
|
+
const unknown = Object.keys(input).filter((key) => !(key in fields));
|
|
57
|
+
if (unknown.length > 0) {
|
|
58
|
+
errors.push(`${at} has fields the contract does not define: ${unknown.join(", ")}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
let ok = true;
|
|
62
|
+
for (const [key, check] of Object.entries(fields)) {
|
|
63
|
+
if (!(key in input) || input[key] === undefined) {
|
|
64
|
+
if (optional.includes(key))
|
|
65
|
+
continue;
|
|
66
|
+
errors.push(`${at}.${key} is missing`);
|
|
67
|
+
ok = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!check(input[key], `${at}.${key}`, errors))
|
|
71
|
+
ok = false;
|
|
72
|
+
}
|
|
73
|
+
return ok;
|
|
74
|
+
};
|
|
75
|
+
const SHA = pattern(/^[0-9a-f]{40}$/, "a 40-character commit sha");
|
|
76
|
+
const promiseId = pattern(PROMISE_ID, "a promise id");
|
|
77
|
+
const count = number(0, Number.MAX_SAFE_INTEGER, true);
|
|
78
|
+
const changedFile = record({
|
|
79
|
+
path: string(1024),
|
|
80
|
+
status: oneOf(FILE_STATUSES),
|
|
81
|
+
oldPath: nullable(string(1024)),
|
|
82
|
+
language: oneOf(LANGUAGES),
|
|
83
|
+
isTest: boolean,
|
|
84
|
+
hunks: listOf(record({ oldStart: count, oldLines: count, newStart: count, newLines: count })),
|
|
85
|
+
symbolsChanged: listOf(record({ name: string(512), kind: oneOf(SYMBOL_KINDS), change: oneOf(SYMBOL_CHANGES) })),
|
|
86
|
+
identifiersAdded: listOf(string(512)),
|
|
87
|
+
identifiersRemoved: listOf(string(512)),
|
|
88
|
+
resources: listOf(record({ kind: oneOf(RESOURCE_KINDS), name: string(600), change: oneOf(RESOURCE_CHANGES) })),
|
|
89
|
+
guardDeletions: listOf(record({ line: count, text: string(2000) })),
|
|
90
|
+
constantChanges: listOf(record({ name: string(512), from: string(2000), to: string(2000) })),
|
|
91
|
+
symbolExtractor: oneOf(SYMBOL_EXTRACTORS),
|
|
92
|
+
}, ["symbolExtractor"]);
|
|
93
|
+
const extraction = record({
|
|
94
|
+
schemaVersion: oneOf([CHANGE_EXTRACTION_SCHEMA_VERSION]),
|
|
95
|
+
base: SHA,
|
|
96
|
+
head: SHA,
|
|
97
|
+
files: listOf(changedFile),
|
|
98
|
+
deltas: listOf(record({ text: string(4000), files: listOf(string(1024)), kind: oneOf(DELTA_KINDS) })),
|
|
99
|
+
intent: record({ summary: string(), source: oneOf(INTENT_SOURCES) }),
|
|
100
|
+
}, ["deltas", "intent"]);
|
|
101
|
+
const unit = number(0, 1);
|
|
102
|
+
const report = record({
|
|
103
|
+
schemaVersion: oneOf([RISK_REPORT_SCHEMA_VERSION]),
|
|
104
|
+
base: SHA,
|
|
105
|
+
head: SHA,
|
|
106
|
+
promises: listOf(record({
|
|
107
|
+
promiseId,
|
|
108
|
+
title: string(),
|
|
109
|
+
score: unit,
|
|
110
|
+
band: oneOf(BANDS),
|
|
111
|
+
reasons: listOf(record({
|
|
112
|
+
signal: oneOf(SIGNALS),
|
|
113
|
+
weight: unit,
|
|
114
|
+
evidence: record({
|
|
115
|
+
file: string(),
|
|
116
|
+
symbol: string(),
|
|
117
|
+
resource: string(),
|
|
118
|
+
delta: string(),
|
|
119
|
+
caseId: string(),
|
|
120
|
+
detail: string(),
|
|
121
|
+
}, ["caseId", "detail"]),
|
|
122
|
+
})),
|
|
123
|
+
features: record({
|
|
124
|
+
symbol_hit: unit,
|
|
125
|
+
graph_forward_distance: nullable(count),
|
|
126
|
+
graph_reverse_distance: nullable(count),
|
|
127
|
+
resource_overlap: unit,
|
|
128
|
+
linked_test_changed: unit,
|
|
129
|
+
sink_reach: unit,
|
|
130
|
+
delta_similarity: unit,
|
|
131
|
+
intent_clash: unit,
|
|
132
|
+
prior_break: count,
|
|
133
|
+
churn: unit,
|
|
134
|
+
}),
|
|
135
|
+
}, ["title"])),
|
|
136
|
+
baselines: record({
|
|
137
|
+
scope_only: listOf(promiseId),
|
|
138
|
+
import_closure: listOf(promiseId),
|
|
139
|
+
all: listOf(promiseId),
|
|
140
|
+
random: listOf(promiseId),
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
function parse(check, value, name) {
|
|
144
|
+
const errors = [];
|
|
145
|
+
return check(value, name, errors) ? { ok: true, value: value } : { ok: false, errors };
|
|
146
|
+
}
|
|
147
|
+
/** A change extraction, checked against the contract. */
|
|
148
|
+
export function parseChangeExtraction(value) {
|
|
149
|
+
return parse(extraction, value, "extraction");
|
|
150
|
+
}
|
|
151
|
+
/** A risk report, checked against the contract. */
|
|
152
|
+
export function parseRiskReport(value) {
|
|
153
|
+
return parse(report, value, "report");
|
|
154
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disposable checkouts of one commit, for an agent to read and run things in.
|
|
3
|
+
*
|
|
4
|
+
* A map builder and a judge each work in a detached `git worktree` of the
|
|
5
|
+
* commit they are about, made under the system temp directory and removed
|
|
6
|
+
* afterwards. Whatever the agent writes there, a scratch test, a build output,
|
|
7
|
+
* an edit it should not have made, goes with the directory, and the person's
|
|
8
|
+
* own checkout is never the one being changed. Nothing here fetches: the
|
|
9
|
+
* commit has to be in the repository already, which it is for a change being
|
|
10
|
+
* pushed.
|
|
11
|
+
*/
|
|
12
|
+
export type ScratchWorktree = Readonly<{
|
|
13
|
+
path: string;
|
|
14
|
+
sha: string;
|
|
15
|
+
remove: () => Promise<void>;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function resolveCommit(root: string, ref: string): Promise<string | undefined>;
|
|
18
|
+
export declare function addScratchWorktree(root: string, ref: string, label: string, scratch?: string): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
worktree: ScratchWorktree;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
reason: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** Synchronous, for a process that is being stopped: remove what this process made. */
|
|
26
|
+
export declare function removeLiveWorktreesNow(): void;
|
|
27
|
+
/**
|
|
28
|
+
* The installed dependencies a fresh worktree lacks, linked from the checkout.
|
|
29
|
+
*
|
|
30
|
+
* A new worktree holds only tracked files, so it has no `node_modules` and a
|
|
31
|
+
* test run in it fails before it reaches the behavior. Each `node_modules`
|
|
32
|
+
* beside a tracked `package.json` in the person's checkout, and a `.venv` at
|
|
33
|
+
* the top, is linked into the same place in the worktree. Linked, not copied:
|
|
34
|
+
* the link is removed with the worktree and the dependencies are never touched.
|
|
35
|
+
*/
|
|
36
|
+
export declare function shareDependencies(root: string, worktree: string): Promise<number>;
|
|
37
|
+
export type CarriedFile = Readonly<{
|
|
38
|
+
path: string;
|
|
39
|
+
sha256: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Files an agent added in one worktree, so a command it wrote can run in another.
|
|
43
|
+
*
|
|
44
|
+
* A judge that proves a break usually writes a new test. Its reproduction is
|
|
45
|
+
* then run in a clean checkout of the base commit, where that test does not
|
|
46
|
+
* exist, so the new files travel with it: untracked, not ignored, regular, and
|
|
47
|
+
* small. Edits to tracked files do not travel, and the judge brief says to put
|
|
48
|
+
* a reproduction in a new file for that reason.
|
|
49
|
+
*/
|
|
50
|
+
export declare function newFiles(worktree: string): Promise<CarriedFile[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Copy files from one directory into another at the same relative paths, and
|
|
53
|
+
* return the ones copied. A path that would land outside `to` is skipped.
|
|
54
|
+
*/
|
|
55
|
+
export declare function carryFiles(from: string, to: string, files: readonly CarriedFile[]): CarriedFile[];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { runCommand } from "./gh.js";
|
|
7
|
+
/** Every worktree this process made and has not removed, for a signal handler to clear. */
|
|
8
|
+
const live = new Map();
|
|
9
|
+
export async function resolveCommit(root, ref) {
|
|
10
|
+
const answer = await runCommand("git", ["-C", root, "rev-parse", "--verify", `${ref}^{commit}`]);
|
|
11
|
+
const sha = answer.stdout.trim();
|
|
12
|
+
return answer.ok && /^[0-9a-f]{40}$/.test(sha) ? sha : undefined;
|
|
13
|
+
}
|
|
14
|
+
export async function addScratchWorktree(root, ref, label, scratch) {
|
|
15
|
+
const sha = await resolveCommit(root, ref);
|
|
16
|
+
if (sha === undefined)
|
|
17
|
+
return { ok: false, reason: `${ref} is not a commit in this repository` };
|
|
18
|
+
const parent = realpathSync(mkdtempSync(join(scratch ?? tmpdir(), `balladeer-${label.replace(/[^A-Za-z0-9_-]/g, "-")}-`)));
|
|
19
|
+
const path = join(parent, "tree");
|
|
20
|
+
const added = await runCommand("git", ["-C", root, "worktree", "add", "--detach", "--quiet", path, sha], { timeoutMs: 120_000 });
|
|
21
|
+
if (!added.ok) {
|
|
22
|
+
rmSync(parent, { recursive: true, force: true });
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
reason: `a scratch checkout of ${sha.slice(0, 12)} could not be made: ${added.stderr}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
live.set(path, root);
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
worktree: {
|
|
32
|
+
path,
|
|
33
|
+
sha,
|
|
34
|
+
remove: async () => {
|
|
35
|
+
await runCommand("git", ["-C", root, "worktree", "remove", "--force", path], {
|
|
36
|
+
timeoutMs: 60_000,
|
|
37
|
+
});
|
|
38
|
+
rmSync(parent, { recursive: true, force: true });
|
|
39
|
+
await runCommand("git", ["-C", root, "worktree", "prune"]);
|
|
40
|
+
live.delete(path);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Synchronous, for a process that is being stopped: remove what this process made. */
|
|
46
|
+
export function removeLiveWorktreesNow() {
|
|
47
|
+
for (const [path, root] of live) {
|
|
48
|
+
try {
|
|
49
|
+
execFileSync("git", ["-C", root, "worktree", "remove", "--force", path], {
|
|
50
|
+
stdio: "ignore",
|
|
51
|
+
timeout: 20_000,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* The directory removal below still clears the files. */
|
|
56
|
+
}
|
|
57
|
+
rmSync(dirname(path), { recursive: true, force: true });
|
|
58
|
+
live.delete(path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The installed dependencies a fresh worktree lacks, linked from the checkout.
|
|
63
|
+
*
|
|
64
|
+
* A new worktree holds only tracked files, so it has no `node_modules` and a
|
|
65
|
+
* test run in it fails before it reaches the behavior. Each `node_modules`
|
|
66
|
+
* beside a tracked `package.json` in the person's checkout, and a `.venv` at
|
|
67
|
+
* the top, is linked into the same place in the worktree. Linked, not copied:
|
|
68
|
+
* the link is removed with the worktree and the dependencies are never touched.
|
|
69
|
+
*/
|
|
70
|
+
export async function shareDependencies(root, worktree) {
|
|
71
|
+
const listed = await runCommand("git", [
|
|
72
|
+
"-C",
|
|
73
|
+
root,
|
|
74
|
+
"ls-files",
|
|
75
|
+
"--",
|
|
76
|
+
"package.json",
|
|
77
|
+
"*/package.json",
|
|
78
|
+
"*/*/package.json",
|
|
79
|
+
]);
|
|
80
|
+
const directories = new Set(["."]);
|
|
81
|
+
if (listed.ok)
|
|
82
|
+
for (const line of listed.stdout.split("\n"))
|
|
83
|
+
if (line.trim())
|
|
84
|
+
directories.add(dirname(line.trim()));
|
|
85
|
+
let linked = 0;
|
|
86
|
+
const link = (relativePath) => {
|
|
87
|
+
const source = join(root, relativePath);
|
|
88
|
+
const target = join(worktree, relativePath);
|
|
89
|
+
if (!existsSync(source) || existsSync(target) || !existsSync(dirname(target)))
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
symlinkSync(source, target, "dir");
|
|
93
|
+
linked += 1;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* A test that needs it will say so. */
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
for (const directory of directories)
|
|
100
|
+
link(join(directory, "node_modules"));
|
|
101
|
+
link(".venv");
|
|
102
|
+
return linked;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Files an agent added in one worktree, so a command it wrote can run in another.
|
|
106
|
+
*
|
|
107
|
+
* A judge that proves a break usually writes a new test. Its reproduction is
|
|
108
|
+
* then run in a clean checkout of the base commit, where that test does not
|
|
109
|
+
* exist, so the new files travel with it: untracked, not ignored, regular, and
|
|
110
|
+
* small. Edits to tracked files do not travel, and the judge brief says to put
|
|
111
|
+
* a reproduction in a new file for that reason.
|
|
112
|
+
*/
|
|
113
|
+
export async function newFiles(worktree) {
|
|
114
|
+
const listed = await runCommand("git", [
|
|
115
|
+
"-C",
|
|
116
|
+
worktree,
|
|
117
|
+
"ls-files",
|
|
118
|
+
"--others",
|
|
119
|
+
"--exclude-standard",
|
|
120
|
+
"-z",
|
|
121
|
+
]);
|
|
122
|
+
if (!listed.ok)
|
|
123
|
+
return [];
|
|
124
|
+
const files = [];
|
|
125
|
+
for (const path of listed.stdout.split("\0")) {
|
|
126
|
+
if (!path || files.length >= 200)
|
|
127
|
+
continue;
|
|
128
|
+
const absolute = join(worktree, path);
|
|
129
|
+
try {
|
|
130
|
+
const stat = lstatSync(absolute);
|
|
131
|
+
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
132
|
+
continue;
|
|
133
|
+
files.push({
|
|
134
|
+
path,
|
|
135
|
+
sha256: createHash("sha256").update(readFileSync(absolute)).digest("hex"),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* Gone between the listing and the read. */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Copy files from one directory into another at the same relative paths, and
|
|
146
|
+
* return the ones copied. A path that would land outside `to` is skipped.
|
|
147
|
+
*/
|
|
148
|
+
export function carryFiles(from, to, files) {
|
|
149
|
+
const carried = [];
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
const target = resolve(to, file.path);
|
|
152
|
+
const inside = relative(to, target);
|
|
153
|
+
if (inside.startsWith("..") || inside === "" || isAbsolute(inside))
|
|
154
|
+
continue;
|
|
155
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
156
|
+
copyFileSync(join(from, file.path), target);
|
|
157
|
+
carried.push(file);
|
|
158
|
+
}
|
|
159
|
+
return carried;
|
|
160
|
+
}
|
package/dist/user-scope.d.ts
CHANGED
|
@@ -15,6 +15,59 @@
|
|
|
15
15
|
* shape, never by trust in the name.
|
|
16
16
|
*/
|
|
17
17
|
export declare const USER_SCOPE_OWNER = "Balladeer current guidance (user scope, loader 1)";
|
|
18
|
+
/**
|
|
19
|
+
* The push check at user scope. Robert, 30 September 2026: setup turns it on
|
|
20
|
+
* for everyone, where it writes Balladeer's other hooks, report-only unless a
|
|
21
|
+
* repository says block, so it runs in every connected repository and every
|
|
22
|
+
* worktree with no step per repository. Two entries on matcher `Bash`, the
|
|
23
|
+
* same pair `balladeer judge --install-hook` writes into a checkout: after the
|
|
24
|
+
* command, where report mode judges, and before it, where block mode does.
|
|
25
|
+
*
|
|
26
|
+
* Its own mark, apart from the guidance hooks' and from the project entry's
|
|
27
|
+
* `Balladeer judge`, so each is recognised, replaced and removed only by
|
|
28
|
+
* itself. The relay note and the push check both sit under PostToolUse.
|
|
29
|
+
*/
|
|
30
|
+
export declare const USER_SCOPE_PUSH_CHECK_OWNER = "Balladeer push check (user scope)";
|
|
31
|
+
/**
|
|
32
|
+
* The push check runs on every shell command an agent runs, twice, so it is
|
|
33
|
+
* written only where the command starts in milliseconds: the installed command
|
|
34
|
+
* by path, or this checkout's own entry file. The `npx` form costs npm's own
|
|
35
|
+
* start and a registry lookup each time, which would slow every command the
|
|
36
|
+
* agent runs, so a laptop on it gets no push check until the command installs.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pushCheckInstallable(published: boolean, installed?: string): boolean;
|
|
39
|
+
/** Where the push check stands on this laptop after setup wrote the user scope. */
|
|
40
|
+
export type PushCheckState = "on" | "off" | "unavailable";
|
|
41
|
+
export declare function pushCheckState(environment: NodeJS.ProcessEnv, published: boolean, installed?: string): PushCheckState;
|
|
42
|
+
/** Setup's one line about the push check. */
|
|
43
|
+
export declare function pushCheckSetupLine(state: PushCheckState): string;
|
|
44
|
+
/**
|
|
45
|
+
* What the push check's entry asks grep before Node starts: whether the
|
|
46
|
+
* payload's `"command"`, read as JSON (a string, or a list of words as Codex
|
|
47
|
+
* can send it), so much as mentions `push`, `create` or `merge`. Every line
|
|
48
|
+
* the judge reads as a push (`git push`, `gh pr create`, `gh pr merge`) spells
|
|
49
|
+
* one of them out, so none is dropped, and a folder or an output that says
|
|
50
|
+
* "push" wakes nothing. A word split by quotes (`pu''sh`) is the one spelling
|
|
51
|
+
* the judge reads as a push and this does not.
|
|
52
|
+
*/
|
|
53
|
+
export declare const PUSH_CHECK_FILTER = "\"command\" *: *(\\[( *\"([^\"\\\\]|\\\\.)*\" *,)* *)?\"([^\"\\\\]|\\\\.)*(push|create|merge)";
|
|
54
|
+
/**
|
|
55
|
+
* The push check's entry as the host runs it. It runs on every shell command
|
|
56
|
+
* an agent runs, before and after, so a POSIX shell looks first and starts
|
|
57
|
+
* Node only for a command that could push: a few milliseconds on anything
|
|
58
|
+
* else instead of Node's start (measured 30 September 2026: 4 to 6 ms against
|
|
59
|
+
* 45 to 90). Both hosts run a hook's command through a shell (Codex checked
|
|
60
|
+
* natively the same day). Only grep's plain "no match" ends it there: a grep
|
|
61
|
+
* that cannot run lets the command through, so a laptop without one pays
|
|
62
|
+
* Node's start and loses nothing. `awaiting` is a file that exists while
|
|
63
|
+
* Codex has yet to run the entry; every command goes through while it does,
|
|
64
|
+
* so that first run is noted without waiting for a push. Windows gets the
|
|
65
|
+
* plain command: there is no /bin/sh to count on.
|
|
66
|
+
*/
|
|
67
|
+
export declare function pushCheckCommand(host: UserScopeHost, published: boolean, installed?: string, options?: Readonly<{
|
|
68
|
+
awaiting?: string;
|
|
69
|
+
platform?: NodeJS.Platform;
|
|
70
|
+
}>): string;
|
|
18
71
|
/** Every tool of the server named `balladeer`, in Claude Code's permission rule form. */
|
|
19
72
|
export declare const USER_SCOPE_ALLOW_RULE = "mcp__balladeer__*";
|
|
20
73
|
export type UserScopeHost = "claude" | "codex";
|
|
@@ -52,12 +105,28 @@ export declare function isOurUserEntry(entry: unknown): boolean;
|
|
|
52
105
|
*/
|
|
53
106
|
export declare function isInstalledCommandPath(command: string): boolean;
|
|
54
107
|
/** `~/.claude/settings.json` carries user-scope hooks. */
|
|
55
|
-
export declare function mergeClaudeUserHooks(home: string, published: boolean, installed?: string): UserScopeWrite;
|
|
56
|
-
/**
|
|
57
|
-
|
|
108
|
+
export declare function mergeClaudeUserHooks(home: string, published: boolean, installed?: string, platform?: NodeJS.Platform): UserScopeWrite;
|
|
109
|
+
/**
|
|
110
|
+
* What a person has to do once in Codex, which no command can do for them,
|
|
111
|
+
* counting the hooks this run gave Codex: the guidance hooks, and the push
|
|
112
|
+
* check's two where it was written.
|
|
113
|
+
*/
|
|
114
|
+
export declare function codexHookApproval(pushCheck: boolean): string;
|
|
58
115
|
/** `~/.codex/config.toml` carries both the server and the hooks for Codex, in
|
|
59
116
|
* one fenced block this command owns end to end. */
|
|
60
117
|
export declare function mergeCodexUserConfig(home: string, published: boolean, installed?: string): UserScopeWrite;
|
|
118
|
+
/**
|
|
119
|
+
* The push check's two entries in `~/.codex/config.toml`, in a fenced block of
|
|
120
|
+
* their own after Balladeer's other one, so the guidance block and this one are
|
|
121
|
+
* each written, replaced and removed only by themselves, and Codex's trust in
|
|
122
|
+
* the one says nothing about the other. TOML lets a later block add to
|
|
123
|
+
* `[[hooks.PostToolUse]]`, and Codex 0.144.5 reads the file that results
|
|
124
|
+
* (checked 30 September 2026 with `codex mcp list` on a file of this shape).
|
|
125
|
+
*/
|
|
126
|
+
export declare function mergeCodexPushCheck(home: string, published: boolean, installed?: string, options?: Readonly<{
|
|
127
|
+
awaiting?: string;
|
|
128
|
+
platform?: NodeJS.Platform;
|
|
129
|
+
}>): UserScopeWrite;
|
|
61
130
|
/** A copy of this command running out of a source checkout registers the
|
|
62
131
|
* checkout; any installed or npx copy registers the published package. */
|
|
63
132
|
export declare function runningFromCheckout(entry?: string): boolean;
|
|
@@ -72,4 +141,6 @@ export declare function installUserScope(options: {
|
|
|
72
141
|
home?: string;
|
|
73
142
|
/** The durable command's absolute path, when one is installed. */
|
|
74
143
|
installed?: string;
|
|
144
|
+
/** The clock the Codex trust marks are stamped with. */
|
|
145
|
+
now?: () => number;
|
|
75
146
|
}): UserScopeWrite[];
|