@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 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 turn it on.** Recording is the default and the whole of
114
- what runs out of the box. Calculated replay — running a matched prompt's
115
- scenario instead of rediscovering it — ships, but needs `bir install --replay`
116
- *and* `BIR_REPLAY=1`, for the reason in the next section.
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=1 bir-hooks # and this is the switch
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=1` | Enable replay. Nothing below matters until it is set. |
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. |
@@ -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=1 run a calculated scenario on a similar-meaning
23
- * match (docs/calculatedReplay.md). OFF by default,
24
- * and read §13.2 before turning it on: a replayed
25
- * step is auto-approved, and a directly executed one
26
- * never reaches the permission system at all.
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)
@@ -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=1 run a calculated scenario on a similar-meaning
23
- * match (docs/calculatedReplay.md). OFF by default,
24
- * and read §13.2 before turning it on: a replayed
25
- * step is auto-approved, and a directly executed one
26
- * never reaches the permission system at all.
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 === "1";
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 but not ON. Set BIR_REPLAY=1 where you run");
373
- out("`bir-hooks` to enable it — and read docs/calculatedReplayGuide.md §5.1 first:");
374
- out("a replayed step is auto-approved, and a directly executed one never reaches");
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