@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.
Files changed (40) hide show
  1. package/README.md +10 -1
  2. package/dist/auth/client.d.ts +141 -13
  3. package/dist/auth/client.js +301 -14
  4. package/dist/bin/bir-hooks.d.ts +13 -0
  5. package/dist/bin/bir-hooks.js +58 -0
  6. package/dist/bin/bir.js +99 -17
  7. package/dist/control/server.d.ts +84 -1
  8. package/dist/control/server.js +546 -51
  9. package/dist/control/transcript.d.ts +40 -0
  10. package/dist/control/transcript.js +105 -0
  11. package/dist/proxy/session.js +5 -4
  12. package/dist/record/recorder.d.ts +178 -4
  13. package/dist/record/recorder.js +6 -0
  14. package/dist/record/remote-recorder.d.ts +20 -2
  15. package/dist/record/remote-recorder.js +66 -6
  16. package/dist/replay/bundle.d.ts +10 -1
  17. package/dist/replay/bundle.js +41 -3
  18. package/dist/replay/controller.d.ts +164 -5
  19. package/dist/replay/controller.js +556 -54
  20. package/dist/replay/coverage.js +2 -2
  21. package/dist/replay/derive.d.ts +20 -1
  22. package/dist/replay/derive.js +69 -12
  23. package/dist/replay/flatten.d.ts +125 -0
  24. package/dist/replay/flatten.js +182 -0
  25. package/dist/replay/handover.d.ts +60 -0
  26. package/dist/replay/handover.js +82 -0
  27. package/dist/replay/logic.d.ts +11 -0
  28. package/dist/replay/logic.js +17 -0
  29. package/dist/replay/plan.d.ts +105 -8
  30. package/dist/replay/plan.js +309 -47
  31. package/dist/replay/source-run.d.ts +24 -10
  32. package/dist/replay/source-run.js +65 -30
  33. package/dist/replay/types.d.ts +108 -5
  34. package/dist/replay/types.js +33 -3
  35. package/docs/calculatedReplayGuide.md +1 -1
  36. package/docs/installRun.md +28 -15
  37. package/docs/loginWeb.md +607 -0
  38. package/docs/my-first-sample.md +545 -0
  39. package/docs/quickstart.md +28 -3
  40. 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, clearCredentials, describeAuthService, normalizeAuthUrl, resolveAuthUrl, } from "../auth/client.js";
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 forget the cached credentials
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 ask for
879
- // a password when what is there is not the service. "Signed in" must mean
880
- // the service at BIR_AUTH_URL accepted these credentials — not that a
881
- // cached token exists for some other address.
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
- const baseUrl = configured ? await normalizeAuthUrl(configured) : "";
884
- if (baseUrl) {
885
- const problem = await describeAuthService(baseUrl);
886
- if (problem) {
887
- process.stderr.write(`[bir] ${baseUrl} is not a BaseIn service: ${problem}\n`);
888
- process.stderr.write(`[bir] ${AUTH_URL_HINT}\n`);
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
- clearCredentials();
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
  }
@@ -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 onToolPost;
276
+ private toolPost;
194
277
  private errorText;
195
278
  /**
196
279
  * `SubagentStart` / `SubagentStop`. These carry `agent_id`, which is the only