@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.
- package/dist/bin/bir-hooks.js +5 -0
- package/dist/bin/bir.js +40 -1
- package/dist/bin/investigate.d.ts +195 -0
- package/dist/bin/investigate.js +580 -0
- package/dist/control/server.js +15 -4
- package/dist/record/recorder.d.ts +6 -0
- package/dist/record/remote-recorder.js +2 -0
- package/dist/replay/controller.d.ts +26 -1
- package/dist/replay/controller.js +78 -20
- package/dist/replay/handover.js +5 -0
- package/dist/replay/plan.d.ts +2 -0
- package/dist/replay/plan.js +53 -6
- package/dist/replay/pricing.d.ts +1 -1
- package/dist/replay/pricing.js +12 -4
- package/dist/replay/tool-error.d.ts +15 -0
- package/dist/replay/tool-error.js +17 -0
- package/dist/replay/types.d.ts +48 -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/calculatedReplayGuide.md +34 -0
- package/package.json +1 -1
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -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
|