@basein/runner 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/bin/bir-hooks.d.ts +6 -5
- package/dist/bin/bir-hooks.js +12 -6
- package/dist/bin/bir.js +44 -5
- package/dist/bin/investigate.d.ts +195 -0
- package/dist/bin/investigate.js +576 -0
- package/dist/control/server.js +8 -0
- package/dist/record/recorder.d.ts +6 -0
- package/dist/record/remote-recorder.js +2 -0
- package/dist/replay/controller.d.ts +3 -1
- package/dist/replay/controller.js +3 -1
- package/dist/util/journal.d.ts +38 -0
- package/dist/util/journal.js +97 -0
- package/dist/util/log.js +4 -0
- package/docs/calculatedReplay.md +3 -3
- package/docs/calculatedReplayGuide.md +37 -5
- package/docs/my-first-sample.md +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,10 +110,11 @@ between the two is not guaranteed.
|
|
|
110
110
|
is a structural fact, not a limitation of this implementation.
|
|
111
111
|
- **`claude-in-chrome`.** It is `scope: "dynamic"`, present in no config file, so
|
|
112
112
|
there is no entry to rewrite.
|
|
113
|
-
- **Replay, unless you
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
- **Replay, unless you install it.** Recording is the whole of what runs out of
|
|
114
|
+
the box. Calculated replay — running a matched prompt's scenario instead of
|
|
115
|
+
rediscovering it — ships, but needs `bir install --replay`. Once installed it
|
|
116
|
+
is on by default; `BIR_REPLAY=0` turns it off, for the reason in the next
|
|
117
|
+
section.
|
|
117
118
|
- **Change what the model does.** Tool descriptions are never edited. The
|
|
118
119
|
model-visible changes are schema relaxation, below, and — only with
|
|
119
120
|
`--replay` — one first-party tool.
|
|
@@ -134,7 +135,7 @@ record, calculate, check, enable, read the audit lines — is
|
|
|
134
135
|
|
|
135
136
|
```bash
|
|
136
137
|
bir install --replay # adds the `bir` MCP server; raises the prompt-hook timeout
|
|
137
|
-
BIR_REPLAY=
|
|
138
|
+
bir-hooks # replay is on by default; BIR_REPLAY=0 bir-hooks turns it off
|
|
138
139
|
```
|
|
139
140
|
|
|
140
141
|
Two switches, not one, because of what replay is: **a scenario is a pre-approved
|
|
@@ -198,7 +199,7 @@ what to do when they register, so there is one switch and no second copy of it:
|
|
|
198
199
|
|
|
199
200
|
| Variable | Effect |
|
|
200
201
|
|---|---|
|
|
201
|
-
| `BIR_REPLAY=
|
|
202
|
+
| `BIR_REPLAY=0` | Disable replay. It is on by default; nothing below matters while this is `0`. |
|
|
202
203
|
| `BIR_REPLAY_ALLOW_SERVERS` | Server keys eligible for **direct** execution. Unset means every wrapped server; setting it is the recommendation. |
|
|
203
204
|
| `BIR_MIN_STEER_SIMILARITY` | Minimum match similarity to replay (default `0.92`, above the service's own `0.9` detection threshold). |
|
|
204
205
|
| `ANTHROPIC_API_KEY` | **Optional.** Working out what a new request is about — which fleet, which file, which date — is done for you by the service on its own key, as long as you are signed in. Set this only to keep that reading on your machine, on your key. Signed out *and* unset, a scenario whose values change between requests is declined rather than replayed on stale ones. |
|
package/dist/bin/bir-hooks.d.ts
CHANGED
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
* BIR_NO_CORRELATION=1 never inject a call id; join on fingerprints
|
|
20
20
|
* BIR_VERBOSE=1 per-step detail lines
|
|
21
21
|
*
|
|
22
|
-
* BIR_REPLAY=
|
|
23
|
-
* match (docs/calculatedReplay.md).
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* BIR_REPLAY=0 turn OFF calculated replay — running a scenario on
|
|
23
|
+
* a similar-meaning match (docs/calculatedReplay.md).
|
|
24
|
+
* ON by default; unset or any value other than "0"
|
|
25
|
+
* keeps it on. Read §13.2: a replayed step is
|
|
26
|
+
* auto-approved, and a directly executed one never
|
|
27
|
+
* reaches the permission system at all.
|
|
27
28
|
* BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
|
|
28
29
|
* execution. Unset means every wrapped server
|
|
29
30
|
* BIR_MIN_STEER_SIMILARITY minimum match similarity to replay (default 0.92)
|
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
* BIR_NO_CORRELATION=1 never inject a call id; join on fingerprints
|
|
20
20
|
* BIR_VERBOSE=1 per-step detail lines
|
|
21
21
|
*
|
|
22
|
-
* BIR_REPLAY=
|
|
23
|
-
* match (docs/calculatedReplay.md).
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* BIR_REPLAY=0 turn OFF calculated replay — running a scenario on
|
|
23
|
+
* a similar-meaning match (docs/calculatedReplay.md).
|
|
24
|
+
* ON by default; unset or any value other than "0"
|
|
25
|
+
* keeps it on. Read §13.2: a replayed step is
|
|
26
|
+
* auto-approved, and a directly executed one never
|
|
27
|
+
* reaches the permission system at all.
|
|
27
28
|
* BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
|
|
28
29
|
* execution. Unset means every wrapped server
|
|
29
30
|
* BIR_MIN_STEER_SIMILARITY minimum match similarity to replay (default 0.92)
|
|
@@ -70,6 +71,7 @@ import { readSidecar } from "../config/generate.js";
|
|
|
70
71
|
import { resolveServers } from "../config/resolve.js";
|
|
71
72
|
import { isWrapped } from "../config/generate.js";
|
|
72
73
|
import { logLine, errText } from "../util/log.js";
|
|
74
|
+
import { openJournal } from "../util/journal.js";
|
|
73
75
|
function parsePort(value) {
|
|
74
76
|
const n = Number(value);
|
|
75
77
|
return Number.isInteger(n) && n > 0 && n < 65536 ? n : DEFAULT_CONTROL_PORT;
|
|
@@ -141,7 +143,7 @@ function parseIntentSources(value) {
|
|
|
141
143
|
* part of the live task.
|
|
142
144
|
*/
|
|
143
145
|
function buildReplayOptions(auth) {
|
|
144
|
-
const enabled = process.env.BIR_REPLAY
|
|
146
|
+
const enabled = process.env.BIR_REPLAY !== "0";
|
|
145
147
|
const allowServers = parseAllowList(process.env.BIR_REPLAY_ALLOW_SERVERS);
|
|
146
148
|
const minSimilarity = Number(process.env.BIR_MIN_STEER_SIMILARITY);
|
|
147
149
|
const opts = {
|
|
@@ -206,6 +208,9 @@ async function main() {
|
|
|
206
208
|
}
|
|
207
209
|
const cwd = process.cwd();
|
|
208
210
|
const sidecar = readSidecar();
|
|
211
|
+
// Open before anything logs: the journal is what `bir investigate` reads
|
|
212
|
+
// later, and the startup lines are part of the story.
|
|
213
|
+
const journalFile = openJournal(cwd);
|
|
209
214
|
const auth = await buildRecorder();
|
|
210
215
|
const server = new ControlServer({
|
|
211
216
|
recorder: auth.recorder,
|
|
@@ -233,6 +238,7 @@ async function main() {
|
|
|
233
238
|
url: address.url,
|
|
234
239
|
cwd,
|
|
235
240
|
discovery: discoveryFile,
|
|
241
|
+
journal: journalFile,
|
|
236
242
|
machine: hostname(),
|
|
237
243
|
wrapped: wrappedServersFor(cwd).join(",") || "(none)",
|
|
238
244
|
});
|
package/dist/bin/bir.js
CHANGED
|
@@ -31,6 +31,9 @@ import { readGenericServers, setGenericServerEntry } from "../config/adapters/ge
|
|
|
31
31
|
import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, deviceLogin, describeAuthService, legacyPasswordLogin, tokenLogin, logout, normalizeAuthUrl, resolveAuthUrl, } from "../auth/client.js";
|
|
32
32
|
import { DEFAULT_CONTROL_PORT } from "../control/server.js";
|
|
33
33
|
import { errText } from "../util/log.js";
|
|
34
|
+
import { journalPath, readJournal } from "../util/journal.js";
|
|
35
|
+
import { investigateCommand } from "./investigate.js";
|
|
36
|
+
import { loadCredentials } from "../auth/client.js";
|
|
34
37
|
import { packageVersion } from "../util/version.js";
|
|
35
38
|
const VERSION = packageVersion();
|
|
36
39
|
const out = (line = "") => {
|
|
@@ -54,6 +57,12 @@ Commands:
|
|
|
54
57
|
scenario replay <scnId> --prompt "…" [--dry]
|
|
55
58
|
replay --scenario <scnId> --prompt "…" [--dry]
|
|
56
59
|
|
|
60
|
+
investigate [<id>] why the newest turn here (or run_/scn_/sexec_ <id>) did what
|
|
61
|
+
it did, what it cost, what to fix — journal + service
|
|
62
|
+
investigate list [--limit <n>] recent turns in this directory and their verdicts
|
|
63
|
+
investigate executions [--limit <n>] [--user <email|id>]
|
|
64
|
+
the ledger, newest first (--user: admins only)
|
|
65
|
+
|
|
57
66
|
Options:
|
|
58
67
|
--config <path> an explicit { "mcpServers": … } file, for clients with no adapter
|
|
59
68
|
--server <name> restrict install/uninstall to one server (repeatable)
|
|
@@ -68,7 +77,9 @@ Options:
|
|
|
68
77
|
--dry replay against recorded outputs only; run no real tools
|
|
69
78
|
--no-browser login: print the link and code, open nothing (SSH, headless)
|
|
70
79
|
--token <value> login: redeem a one-time setup token from the console (no browser)
|
|
71
|
-
--password login: use the old email/password prompt (deprecated)
|
|
80
|
+
--password login: use the old email/password prompt (deprecated)
|
|
81
|
+
--user <ref> investigate executions: another account, by email or id (admin)
|
|
82
|
+
--limit <n> investigate: how many turns / executions to show`);
|
|
72
83
|
process.exit(code);
|
|
73
84
|
}
|
|
74
85
|
function parseArgs(argv) {
|
|
@@ -146,6 +157,18 @@ function parseArgs(argv) {
|
|
|
146
157
|
case "--json":
|
|
147
158
|
args.json = true;
|
|
148
159
|
break;
|
|
160
|
+
case "--user":
|
|
161
|
+
args.user = argv[++i];
|
|
162
|
+
if (!args.user)
|
|
163
|
+
usage(2);
|
|
164
|
+
break;
|
|
165
|
+
case "--limit": {
|
|
166
|
+
const n = Number(argv[++i]);
|
|
167
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
168
|
+
usage(2);
|
|
169
|
+
args.limit = n;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
149
172
|
default:
|
|
150
173
|
if (arg.startsWith("-"))
|
|
151
174
|
usage(2);
|
|
@@ -369,10 +392,10 @@ function install(args) {
|
|
|
369
392
|
}
|
|
370
393
|
if (args.replay) {
|
|
371
394
|
out();
|
|
372
|
-
out("Calculated replay is INSTALLED
|
|
373
|
-
out("`bir-hooks` to
|
|
374
|
-
out("a replayed step is auto-approved,
|
|
375
|
-
out("the permission system at all.");
|
|
395
|
+
out("Calculated replay is INSTALLED and ON by default once `bir-hooks` runs. Set");
|
|
396
|
+
out("BIR_REPLAY=0 where you run `bir-hooks` to turn it off — and read");
|
|
397
|
+
out("docs/calculatedReplayGuide.md §5.1 first: a replayed step is auto-approved,");
|
|
398
|
+
out("and a directly executed one never reaches the permission system at all.");
|
|
376
399
|
}
|
|
377
400
|
return 0;
|
|
378
401
|
}
|
|
@@ -933,6 +956,22 @@ async function main() {
|
|
|
933
956
|
return scenarioCommand(args);
|
|
934
957
|
case "replay":
|
|
935
958
|
return replayCommand(args);
|
|
959
|
+
case "investigate":
|
|
960
|
+
return investigateCommand({ positionals: args.positionals, json: args.json, user: args.user, limit: args.limit }, {
|
|
961
|
+
cwd: process.cwd(),
|
|
962
|
+
out,
|
|
963
|
+
journal: (cwd) => readJournal(journalPath(cwd)),
|
|
964
|
+
journalPath,
|
|
965
|
+
viewerEmail: loadCredentials()?.user?.email,
|
|
966
|
+
service: async (path) => {
|
|
967
|
+
try {
|
|
968
|
+
return await service("GET", path);
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
return { error: errText(err) };
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
});
|
|
936
975
|
case "login": {
|
|
937
976
|
// Settle the URL the way every other command does, then refuse to go on
|
|
938
977
|
// when what is there is not the service. "Signed in" must mean the service
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bir investigate` — why did this turn do what it did, and what did it cost?
|
|
3
|
+
* (docs/calculatedReplayGuide.md §9.1)
|
|
4
|
+
*
|
|
5
|
+
* Two sources, merged:
|
|
6
|
+
*
|
|
7
|
+
* 1. The **journal** this directory's `bir-hooks` keeps (util/journal.ts):
|
|
8
|
+
* the run boundaries, the match, every gate decision, the plan's mode and
|
|
9
|
+
* coverage, the replay outcome and the execution report. This is the only
|
|
10
|
+
* record of a *matched* turn, because the service deliberately keeps no
|
|
11
|
+
* run for one.
|
|
12
|
+
* 2. The **service**'s `GET /investigate/:id`: the recording, its scenario,
|
|
13
|
+
* the baseline, the execution ledger and the service's own findings.
|
|
14
|
+
* Owner-only; an admin may look at anyone's.
|
|
15
|
+
*
|
|
16
|
+
* Each side diagnoses what only it can see. The journal knows which gate
|
|
17
|
+
* declined and what mode armed; the service knows the baseline, the ledger and
|
|
18
|
+
* whether a run ever became a recording. Both lists are printed, worst first.
|
|
19
|
+
*/
|
|
20
|
+
import type { JournalEntry } from "../util/journal.js";
|
|
21
|
+
export interface Turn {
|
|
22
|
+
runId: string;
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
finishedAt?: string;
|
|
26
|
+
prompt?: string;
|
|
27
|
+
matched?: {
|
|
28
|
+
runId: string;
|
|
29
|
+
scenarioId?: string;
|
|
30
|
+
similarity?: number;
|
|
31
|
+
};
|
|
32
|
+
decision?: {
|
|
33
|
+
verdict: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
why?: string;
|
|
36
|
+
threshold?: number;
|
|
37
|
+
};
|
|
38
|
+
plan?: {
|
|
39
|
+
mode: string;
|
|
40
|
+
steps?: number;
|
|
41
|
+
coverage?: string[];
|
|
42
|
+
tools?: string[];
|
|
43
|
+
scenarioId?: string;
|
|
44
|
+
by?: string;
|
|
45
|
+
};
|
|
46
|
+
derived?: {
|
|
47
|
+
params?: number;
|
|
48
|
+
costUsd?: number;
|
|
49
|
+
source?: string;
|
|
50
|
+
};
|
|
51
|
+
done?: {
|
|
52
|
+
outcome: string;
|
|
53
|
+
steps?: string;
|
|
54
|
+
ms?: number;
|
|
55
|
+
};
|
|
56
|
+
reported?: {
|
|
57
|
+
outcome: string;
|
|
58
|
+
derive: number;
|
|
59
|
+
session: number;
|
|
60
|
+
fallback: number;
|
|
61
|
+
savedUsd?: number;
|
|
62
|
+
measured?: boolean;
|
|
63
|
+
ticket?: string;
|
|
64
|
+
scenarioId?: string;
|
|
65
|
+
declined?: string;
|
|
66
|
+
};
|
|
67
|
+
finish?: {
|
|
68
|
+
steps?: number;
|
|
69
|
+
durationMs?: number;
|
|
70
|
+
costUsd?: number;
|
|
71
|
+
measured?: boolean;
|
|
72
|
+
recorded?: boolean;
|
|
73
|
+
};
|
|
74
|
+
/** `replay.*_failed`, `replay.arm_failed`, `replay.diverge`, `replay.handover` … */
|
|
75
|
+
incidents: JournalEntry[];
|
|
76
|
+
events: JournalEntry[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Group journal entries into turns. A turn opens at `run.start` and closes at
|
|
80
|
+
* `run.finish`; an entry joins the turn whose local run id it names, else the
|
|
81
|
+
* turn whose *matched* run id it names (the replay lines log the service's run),
|
|
82
|
+
* else the latest turn — which is where an older build's `execution.reported`,
|
|
83
|
+
* arriving after `run.finish` with no run id, belongs.
|
|
84
|
+
*/
|
|
85
|
+
export declare function turnsFromJournal(entries: readonly JournalEntry[]): Turn[];
|
|
86
|
+
/** The turn an id names: its own run, the run it matched, its scenario or its ticket. */
|
|
87
|
+
export declare function findTurn(turns: readonly Turn[], id: string): Turn | undefined;
|
|
88
|
+
export interface Finding {
|
|
89
|
+
code: string;
|
|
90
|
+
severity: "problem" | "warn" | "info";
|
|
91
|
+
title: string;
|
|
92
|
+
cause: string;
|
|
93
|
+
fix: string;
|
|
94
|
+
evidence?: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
/** The runner's own gate vocabulary, with the cause and the fix (guide §9). */
|
|
97
|
+
export declare const DECLINES: Record<string, {
|
|
98
|
+
cause: string;
|
|
99
|
+
fix: string;
|
|
100
|
+
}>;
|
|
101
|
+
export declare const LOW_SAVING_PCT = 0.3;
|
|
102
|
+
export declare function localFindings(turn: Turn): Finding[];
|
|
103
|
+
/** `saved ÷ (saved + cost)`: the baseline is recoverable from the report alone. */
|
|
104
|
+
export declare function savedPct(turn: Turn): number | null;
|
|
105
|
+
export type ServiceReply = {
|
|
106
|
+
status: number;
|
|
107
|
+
body: unknown;
|
|
108
|
+
} | {
|
|
109
|
+
error: string;
|
|
110
|
+
};
|
|
111
|
+
export interface InvestigateDeps {
|
|
112
|
+
cwd: string;
|
|
113
|
+
out: (line?: string) => void;
|
|
114
|
+
service: (path: string) => Promise<ServiceReply>;
|
|
115
|
+
journal: (cwd: string) => JournalEntry[];
|
|
116
|
+
journalPath: (cwd: string) => string;
|
|
117
|
+
/** The signed-in account, so "owner you" can be told from "owner <email>". */
|
|
118
|
+
viewerEmail?: string;
|
|
119
|
+
}
|
|
120
|
+
export interface InvestigateArgs {
|
|
121
|
+
positionals: string[];
|
|
122
|
+
json: boolean;
|
|
123
|
+
user?: string;
|
|
124
|
+
limit?: number;
|
|
125
|
+
}
|
|
126
|
+
interface ServiceInvestigation {
|
|
127
|
+
subject?: {
|
|
128
|
+
kind: string;
|
|
129
|
+
id: string;
|
|
130
|
+
};
|
|
131
|
+
owner?: {
|
|
132
|
+
id: string;
|
|
133
|
+
email: string | null;
|
|
134
|
+
};
|
|
135
|
+
run?: {
|
|
136
|
+
id: string;
|
|
137
|
+
input: string;
|
|
138
|
+
title: string | null;
|
|
139
|
+
finishedAt: string | null;
|
|
140
|
+
actionCount: number;
|
|
141
|
+
tools: string[];
|
|
142
|
+
originalCostUsd: number | null;
|
|
143
|
+
iterations: number;
|
|
144
|
+
isRecording: boolean;
|
|
145
|
+
embedded: boolean;
|
|
146
|
+
duplicateOf: string | null;
|
|
147
|
+
} | null;
|
|
148
|
+
scenario?: {
|
|
149
|
+
id: string;
|
|
150
|
+
state: string;
|
|
151
|
+
steps: Array<{
|
|
152
|
+
stepIndex: number;
|
|
153
|
+
toolName: string | null;
|
|
154
|
+
failureCount: number;
|
|
155
|
+
}>;
|
|
156
|
+
baseline: {
|
|
157
|
+
costUsd: number | null;
|
|
158
|
+
samples: number;
|
|
159
|
+
method: string | null;
|
|
160
|
+
};
|
|
161
|
+
lastDecline: {
|
|
162
|
+
reason: string;
|
|
163
|
+
at: string | null;
|
|
164
|
+
} | null;
|
|
165
|
+
hitCount: number | null;
|
|
166
|
+
disabledAt: string | null;
|
|
167
|
+
error: string | null;
|
|
168
|
+
} | null;
|
|
169
|
+
executions?: Array<{
|
|
170
|
+
id: string;
|
|
171
|
+
createdAt: string;
|
|
172
|
+
outcome: string;
|
|
173
|
+
costUsd: number;
|
|
174
|
+
deriveCostUsd: number | null;
|
|
175
|
+
sessionCostUsd: number | null;
|
|
176
|
+
fallbackCostUsd: number | null;
|
|
177
|
+
baselineCostUsd: number | null;
|
|
178
|
+
savedUsd: number | null;
|
|
179
|
+
savedPct: number | null;
|
|
180
|
+
measured: boolean;
|
|
181
|
+
durationMs: number | null;
|
|
182
|
+
steps: Array<{
|
|
183
|
+
stepIndex: number;
|
|
184
|
+
toolName?: string;
|
|
185
|
+
status: string;
|
|
186
|
+
error?: string;
|
|
187
|
+
}> | null;
|
|
188
|
+
}>;
|
|
189
|
+
findings?: Finding[];
|
|
190
|
+
}
|
|
191
|
+
export declare function renderTurn(out: (l?: string) => void, turn: Turn): void;
|
|
192
|
+
export declare function renderService(out: (l?: string) => void, s: ServiceInvestigation, viewerIsOwner: boolean): void;
|
|
193
|
+
export declare function investigateCommand(args: InvestigateArgs, deps: InvestigateDeps): Promise<number>;
|
|
194
|
+
export {};
|
|
195
|
+
//# sourceMappingURL=investigate.d.ts.map
|