@basein/runner 0.1.1 → 0.2.1
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 +10 -1
- package/dist/auth/client.d.ts +141 -13
- package/dist/auth/client.js +301 -14
- package/dist/bin/bir-hooks.d.ts +13 -0
- package/dist/bin/bir-hooks.js +58 -0
- package/dist/bin/bir.js +99 -17
- package/dist/control/server.d.ts +84 -1
- package/dist/control/server.js +546 -51
- package/dist/control/transcript.d.ts +40 -0
- package/dist/control/transcript.js +105 -0
- package/dist/proxy/session.js +5 -4
- package/dist/record/recorder.d.ts +178 -4
- package/dist/record/recorder.js +6 -0
- package/dist/record/remote-recorder.d.ts +20 -2
- package/dist/record/remote-recorder.js +66 -6
- package/dist/replay/bundle.d.ts +10 -1
- package/dist/replay/bundle.js +41 -3
- package/dist/replay/controller.d.ts +164 -5
- package/dist/replay/controller.js +556 -54
- package/dist/replay/coverage.js +2 -2
- package/dist/replay/derive.d.ts +20 -1
- package/dist/replay/derive.js +69 -12
- package/dist/replay/flatten.d.ts +125 -0
- package/dist/replay/flatten.js +182 -0
- package/dist/replay/handover.d.ts +60 -0
- package/dist/replay/handover.js +82 -0
- package/dist/replay/logic.d.ts +11 -0
- package/dist/replay/logic.js +17 -0
- package/dist/replay/plan.d.ts +105 -8
- package/dist/replay/plan.js +309 -47
- package/dist/replay/source-run.d.ts +24 -10
- package/dist/replay/source-run.js +65 -30
- package/dist/replay/types.d.ts +108 -5
- package/dist/replay/types.js +33 -3
- package/docs/calculatedReplayGuide.md +1 -1
- package/docs/installRun.md +28 -15
- package/docs/loginWeb.md +607 -0
- package/docs/my-first-sample.md +545 -0
- package/docs/quickstart.md +28 -3
- package/package.json +1 -1
package/dist/bin/bir.js
CHANGED
|
@@ -28,7 +28,7 @@ import { isScenarioServer, isWrapped, PACKAGE_NAME, readSidecar, scenarioEntry,
|
|
|
28
28
|
import { isRemote, resolveServers } from "../config/resolve.js";
|
|
29
29
|
import { buildHooksBlock, claudeCodePaths, fileForScope, installHooks, readTextOrNull, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
|
|
30
30
|
import { readGenericServers, setGenericServerEntry } from "../config/adapters/generic.js";
|
|
31
|
-
import { AUTH_URL_HINT, authenticate,
|
|
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
34
|
import { packageVersion } from "../util/version.js";
|
|
@@ -45,8 +45,8 @@ Commands:
|
|
|
45
45
|
status what is installed for this directory
|
|
46
46
|
doctor is it actually working right now?
|
|
47
47
|
wrap print a proxied entry for one server (any MCP client)
|
|
48
|
-
login sign in to the BaseIn service
|
|
49
|
-
logout
|
|
48
|
+
login sign in to the BaseIn service (opens your browser)
|
|
49
|
+
logout revoke this machine's session and forget it
|
|
50
50
|
|
|
51
51
|
scenario list recorded runs and their calculated scenarios
|
|
52
52
|
scenario show <runId> a run's scenario: intent, params, steps
|
|
@@ -65,7 +65,10 @@ Options:
|
|
|
65
65
|
--replay install/remove the scenario server, enabling calculated replay
|
|
66
66
|
--port <n> control-server port to write into the hook URLs (default ${DEFAULT_CONTROL_PORT})
|
|
67
67
|
--json machine-readable output for status / doctor
|
|
68
|
-
--dry replay against recorded outputs only; run no real tools
|
|
68
|
+
--dry replay against recorded outputs only; run no real tools
|
|
69
|
+
--no-browser login: print the link and code, open nothing (SSH, headless)
|
|
70
|
+
--token <value> login: redeem a one-time setup token from the console (no browser)
|
|
71
|
+
--password login: use the old email/password prompt (deprecated)`);
|
|
69
72
|
process.exit(code);
|
|
70
73
|
}
|
|
71
74
|
function parseArgs(argv) {
|
|
@@ -83,6 +86,7 @@ function parseArgs(argv) {
|
|
|
83
86
|
positionals: [],
|
|
84
87
|
dry: false,
|
|
85
88
|
force: false,
|
|
89
|
+
password: false,
|
|
86
90
|
};
|
|
87
91
|
for (let i = 1; i < argv.length; i += 1) {
|
|
88
92
|
const arg = argv[i];
|
|
@@ -106,6 +110,15 @@ function parseArgs(argv) {
|
|
|
106
110
|
case "--force":
|
|
107
111
|
args.force = true;
|
|
108
112
|
break;
|
|
113
|
+
case "--password":
|
|
114
|
+
args.password = true;
|
|
115
|
+
break;
|
|
116
|
+
case "--no-browser":
|
|
117
|
+
args.browser = false;
|
|
118
|
+
break;
|
|
119
|
+
case "--token":
|
|
120
|
+
args.token = argv[++i] ?? "";
|
|
121
|
+
break;
|
|
109
122
|
case "--config":
|
|
110
123
|
args.configPath = argv[++i];
|
|
111
124
|
break;
|
|
@@ -531,6 +544,20 @@ async function doctor(args) {
|
|
|
531
544
|
else {
|
|
532
545
|
notes.push(`recording to ${String(health.authUrl ?? "the configured BaseIn service")}`);
|
|
533
546
|
}
|
|
547
|
+
// Whether a recurring sub-task the service found may actually run mid-task
|
|
548
|
+
// (segmented.md R-LIFE-8). Observe-only is the default and must be visible
|
|
549
|
+
// without reading a log: an operator turns arming on by reading what *would*
|
|
550
|
+
// have armed, and needs to know which of the two states this machine is in.
|
|
551
|
+
notes.push(health.segmentArm === true
|
|
552
|
+
? "recurring sub-tasks MAY run mid-task (BIR_SEGMENT_ARM=1)"
|
|
553
|
+
: "recurring sub-tasks are observed only — nothing is replaced (BIR_SEGMENT_ARM unset)");
|
|
554
|
+
const sessions = health.sessions ?? [];
|
|
555
|
+
const probes = sessions.reduce((n, s) => n + (s.intent?.probes ?? 0), 0);
|
|
556
|
+
const armed = sessions.reduce((n, s) => n + (s.intent?.armed ?? 0), 0);
|
|
557
|
+
if (sessions.length > 0) {
|
|
558
|
+
notes.push(`mid-turn matching this session: ${probes} probe${probes === 1 ? "" : "s"}, ` +
|
|
559
|
+
`${armed} armed`);
|
|
560
|
+
}
|
|
534
561
|
}
|
|
535
562
|
else if (!resolveAuthUrl()) {
|
|
536
563
|
// No control server to ask, and nothing in this shell either. That is a
|
|
@@ -875,30 +902,85 @@ async function main() {
|
|
|
875
902
|
case "replay":
|
|
876
903
|
return replayCommand(args);
|
|
877
904
|
case "login": {
|
|
878
|
-
// Settle the URL the way every other command does, then refuse to
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
905
|
+
// Settle the URL the way every other command does, then refuse to go on
|
|
906
|
+
// when what is there is not the service. "Signed in" must mean the service
|
|
907
|
+
// at BIR_AUTH_URL issued this session — not that a cached token exists for
|
|
908
|
+
// some other address.
|
|
882
909
|
const configured = resolveAuthUrl();
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
910
|
+
if (!configured) {
|
|
911
|
+
process.stderr.write("[bir] BIR_AUTH_URL is not set — there is nothing to sign in to.\n");
|
|
912
|
+
process.stderr.write(`[bir] ${AUTH_URL_HINT}\n`);
|
|
913
|
+
return 1;
|
|
914
|
+
}
|
|
915
|
+
const baseUrl = await normalizeAuthUrl(configured);
|
|
916
|
+
const problem = await describeAuthService(baseUrl);
|
|
917
|
+
if (problem) {
|
|
918
|
+
process.stderr.write(`[bir] ${baseUrl} is not a BaseIn service: ${problem}\n`);
|
|
919
|
+
process.stderr.write(`[bir] ${AUTH_URL_HINT}\n`);
|
|
920
|
+
return 1;
|
|
921
|
+
}
|
|
922
|
+
let session;
|
|
923
|
+
if (args.token !== undefined) {
|
|
924
|
+
// A setup token from the console: already approved for one account, so
|
|
925
|
+
// there is no page to open and nothing to wait for.
|
|
926
|
+
if (!args.token.trim()) {
|
|
927
|
+
process.stderr.write("[bir] --token needs the token itself: bir login --token <token>\n");
|
|
889
928
|
return 1;
|
|
890
929
|
}
|
|
930
|
+
try {
|
|
931
|
+
session = await tokenLogin(baseUrl, args.token);
|
|
932
|
+
}
|
|
933
|
+
catch (err) {
|
|
934
|
+
if (err instanceof DeviceFlowAborted) {
|
|
935
|
+
process.stderr.write(`[bir] ${err.message}\n`);
|
|
936
|
+
return 1;
|
|
937
|
+
}
|
|
938
|
+
if (err instanceof DeviceFlowUnsupported) {
|
|
939
|
+
process.stderr.write("[bir] this service does not offer setup tokens; run `bir login` without --token.\n");
|
|
940
|
+
return 1;
|
|
941
|
+
}
|
|
942
|
+
throw err;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else if (args.password) {
|
|
946
|
+
process.stderr.write("[bir] --password is deprecated and will be removed in the next release. " +
|
|
947
|
+
"Browser sign-in works for every account, including Google-only ones.\n");
|
|
948
|
+
session = await legacyPasswordLogin(baseUrl);
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
try {
|
|
952
|
+
session = await deviceLogin(baseUrl, { openBrowser: args.browser });
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
if (err instanceof DeviceFlowUnsupported) {
|
|
956
|
+
// A runner newer than its service. Falling back beats stranding
|
|
957
|
+
// someone on an install that used to work.
|
|
958
|
+
process.stderr.write("[bir] this service does not offer browser sign-in yet; " +
|
|
959
|
+
"falling back to email and password.\n");
|
|
960
|
+
session = await legacyPasswordLogin(baseUrl);
|
|
961
|
+
}
|
|
962
|
+
else if (err instanceof DeviceFlowAborted) {
|
|
963
|
+
process.stderr.write(`[bir] ${err.message}\n`);
|
|
964
|
+
return 1;
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
throw err;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
891
970
|
}
|
|
892
|
-
const session = await authenticate(baseUrl ? { authUrl: baseUrl } : undefined);
|
|
893
971
|
if (!session)
|
|
894
972
|
return 1;
|
|
895
973
|
out(`Signed in to ${baseUrl} as ${session.user.email}.`);
|
|
896
974
|
return 0;
|
|
897
975
|
}
|
|
898
|
-
case "logout":
|
|
899
|
-
|
|
976
|
+
case "logout": {
|
|
977
|
+
// Revoke server-side before forgetting locally, so a stolen credentials
|
|
978
|
+
// file is dead rather than merely absent from this machine.
|
|
979
|
+
const configured = resolveAuthUrl();
|
|
980
|
+
await logout(configured ? await normalizeAuthUrl(configured) : undefined);
|
|
900
981
|
out("Logged out — cached credentials cleared.");
|
|
901
982
|
return 0;
|
|
983
|
+
}
|
|
902
984
|
default:
|
|
903
985
|
usage(2);
|
|
904
986
|
}
|
package/dist/control/server.d.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* token on every route, and never echoes the token back. A loopback port that
|
|
24
24
|
* accepts unauthenticated step reports is a local exfiltration channel.
|
|
25
25
|
*/
|
|
26
|
+
import { type IntentSource } from "./transcript.js";
|
|
26
27
|
import { type Recorder } from "../record/recorder.js";
|
|
27
28
|
import { type ReplayOptions } from "../replay/controller.js";
|
|
28
29
|
/** The subset of the Claude Code hook payload we consume (all optional, all defensive). */
|
|
@@ -41,6 +42,13 @@ export interface HookPayload {
|
|
|
41
42
|
}
|
|
42
43
|
export interface ControlServerOptions {
|
|
43
44
|
recorder: Recorder;
|
|
45
|
+
/**
|
|
46
|
+
* Tool results of the current prompt handed to derivation
|
|
47
|
+
* (`BIR_DERIVE_RECENT_RESULTS`, segmented.md R-PARAM-2). Default 5; 0 off.
|
|
48
|
+
*/
|
|
49
|
+
deriveRecentResults?: number;
|
|
50
|
+
/** `BIR_INTENT_SOURCES` — which intent rungs may be sent (R-INTENT-3). */
|
|
51
|
+
intentSources?: ReadonlySet<IntentSource>;
|
|
44
52
|
/** Session working directory — keys the discovery file. */
|
|
45
53
|
cwd: string;
|
|
46
54
|
/** Preferred port; 0 (or a busy port) falls back to an ephemeral one. */
|
|
@@ -107,6 +115,18 @@ export declare class ControlServer {
|
|
|
107
115
|
private readonly pendingSeals;
|
|
108
116
|
/** Calculated replay. Inert unless `opts.replay.enabled`. */
|
|
109
117
|
private readonly replay;
|
|
118
|
+
/**
|
|
119
|
+
* How many of this prompt's tool results derivation may read
|
|
120
|
+
* (`BIR_DERIVE_RECENT_RESULTS`, segmented.md R-PARAM-2). A target is often
|
|
121
|
+
* named by an earlier step's output and never by the prompt.
|
|
122
|
+
*/
|
|
123
|
+
private readonly deriveRecentResults;
|
|
124
|
+
/**
|
|
125
|
+
* Which rungs of R-INTENT-1 the runner may send (`BIR_INTENT_SOURCES`). It
|
|
126
|
+
* governs only what is *sent*: with every source off, the probe still goes out
|
|
127
|
+
* with an empty text and the server builds the tool line (R-HIT-4).
|
|
128
|
+
*/
|
|
129
|
+
private readonly intentSources;
|
|
110
130
|
constructor(opts: ControlServerOptions);
|
|
111
131
|
listen(): Promise<ControlServerAddress>;
|
|
112
132
|
close(): Promise<void>;
|
|
@@ -167,6 +187,8 @@ export declare class ControlServer {
|
|
|
167
187
|
* false` and zero rather than a number that is confidently wrong.
|
|
168
188
|
*/
|
|
169
189
|
private runCost;
|
|
190
|
+
/** Transcript cost since `mark`; unmeasured (and zero) without one. */
|
|
191
|
+
private costSince;
|
|
170
192
|
private reportExecution;
|
|
171
193
|
/** Seal the run *and* wait for everything queued to reach the service. */
|
|
172
194
|
private finalizeRun;
|
|
@@ -183,6 +205,67 @@ export declare class ControlServer {
|
|
|
183
205
|
* record `tool_selected`. The hook is this step's only observer.
|
|
184
206
|
*/
|
|
185
207
|
private onToolPre;
|
|
208
|
+
/** A controller decision as a `PreToolUse` answer, or undefined to carry on. */
|
|
209
|
+
private preToolAnswer;
|
|
210
|
+
/**
|
|
211
|
+
* Recordings this turn must not count a step hit against (segmented.md
|
|
212
|
+
* R-HIT-7).
|
|
213
|
+
*
|
|
214
|
+
* A repeat of a recording's own prompt is *prompt* recurrence, counted as
|
|
215
|
+
* `iterations`, never step recurrence. Without this, three prompt repeats
|
|
216
|
+
* would reach `SEGMENT_MIN_HITS` on every step of the whole recording and the
|
|
217
|
+
* detector would build a whole-run-sized segment beside the whole-run
|
|
218
|
+
* scenario.
|
|
219
|
+
*/
|
|
220
|
+
private excludeRunsFor;
|
|
221
|
+
/**
|
|
222
|
+
* Per recording a plan of this turn came from, how far that plan got
|
|
223
|
+
* (segmented.md R-OUT-6).
|
|
224
|
+
*
|
|
225
|
+
* The server uses it to refuse a segment that starts *before* where this turn
|
|
226
|
+
* already is: re-running steps the turn has just done is worse than not
|
|
227
|
+
* replaying at all. The largest position per recording wins, and a plan
|
|
228
|
+
* declined for anything but a known-bad first step reports nothing — it ran
|
|
229
|
+
* none of the recording, so it blocks none of it.
|
|
230
|
+
*
|
|
231
|
+
* Rebuilt on every probe rather than cached: a plan that hands over between
|
|
232
|
+
* two calls changes the answer.
|
|
233
|
+
*/
|
|
234
|
+
private ranThroughFor;
|
|
235
|
+
/**
|
|
236
|
+
* Intent matching in the ReAct loop (fallbk.md §Runner 4, segmented.md 10.3).
|
|
237
|
+
*
|
|
238
|
+
* Called only while the model is driving. Every unsteered, non-housekeeping
|
|
239
|
+
* tool call is sent: the service counts it as a hit on whatever recorded step
|
|
240
|
+
* it matches, and may hand back a scenario or a segment to run in its place.
|
|
241
|
+
*
|
|
242
|
+
* A call made with **no reasoning at all** still probes (R-HIT-4): the server
|
|
243
|
+
* builds an intent from the tool name and arguments, and a step the agent took
|
|
244
|
+
* without narrating it is exactly as much a recurrence as one it explained.
|
|
245
|
+
* Every failure is a miss.
|
|
246
|
+
*/
|
|
247
|
+
private tryIntentMatch;
|
|
248
|
+
/**
|
|
249
|
+
* Schedule a fragment once a matched turn's plan has handed over (fallbk.md
|
|
250
|
+
* D7). Only a turn that is not already recording needs one: an unmatched turn
|
|
251
|
+
* whose intent-armed plan handed over is recording the model's work anyway.
|
|
252
|
+
*/
|
|
253
|
+
private noteHandover;
|
|
254
|
+
/**
|
|
255
|
+
* Open the fragment run and record from this call on (fallbk.md D7).
|
|
256
|
+
*
|
|
257
|
+
* The run id is swapped in place: steps allocated before belonged to a turn
|
|
258
|
+
* the service never created a run for, and nothing of theirs may land in the
|
|
259
|
+
* fragment — so pending correlations are latched and built-ins forgotten.
|
|
260
|
+
*/
|
|
261
|
+
private openFragment;
|
|
262
|
+
/** Resolve with `p`, or null once `ms` passes or it rejects. */
|
|
263
|
+
private withinBudget;
|
|
264
|
+
/**
|
|
265
|
+
* `PostToolUse`, and where a hand-over noticed while threading this call's
|
|
266
|
+
* output reaches the model at once, as `additionalContext` (fallbk.md D4).
|
|
267
|
+
*/
|
|
268
|
+
private onToolPost;
|
|
186
269
|
/**
|
|
187
270
|
* `PostToolUse` / `PostToolUseFailure`. For a built-in this closes the pair.
|
|
188
271
|
* For a correlated MCP call it normally does nothing — the proxy owns that
|
|
@@ -190,7 +273,7 @@ export declare class ControlServer {
|
|
|
190
273
|
* or the call never reached it), the hook's own view is recorded after
|
|
191
274
|
* {@link PROXY_REPORT_GRACE_MS} rather than the step being lost entirely.
|
|
192
275
|
*/
|
|
193
|
-
private
|
|
276
|
+
private toolPost;
|
|
194
277
|
private errorText;
|
|
195
278
|
/**
|
|
196
279
|
* `SubagentStart` / `SubagentStop`. These carry `agent_id`, which is the only
|