@basein/runner 0.2.6 → 0.2.8

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.
@@ -71,6 +71,7 @@ import { readSidecar } from "../config/generate.js";
71
71
  import { resolveServers } from "../config/resolve.js";
72
72
  import { isWrapped } from "../config/generate.js";
73
73
  import { logLine, errText } from "../util/log.js";
74
+ import { openJournal } from "../util/journal.js";
74
75
  function parsePort(value) {
75
76
  const n = Number(value);
76
77
  return Number.isInteger(n) && n > 0 && n < 65536 ? n : DEFAULT_CONTROL_PORT;
@@ -207,6 +208,9 @@ async function main() {
207
208
  }
208
209
  const cwd = process.cwd();
209
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);
210
214
  const auth = await buildRecorder();
211
215
  const server = new ControlServer({
212
216
  recorder: auth.recorder,
@@ -234,6 +238,7 @@ async function main() {
234
238
  url: address.url,
235
239
  cwd,
236
240
  discovery: discoveryFile,
241
+ journal: journalFile,
237
242
  machine: hostname(),
238
243
  wrapped: wrappedServersFor(cwd).join(",") || "(none)",
239
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);
@@ -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