@basou/core 0.37.0 → 0.38.0

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/index.d.ts CHANGED
@@ -422,9 +422,9 @@ declare const SessionImportPayloadSchema: z.ZodObject<{
422
422
  source: z.ZodString;
423
423
  prev_hash: z.ZodOptional<z.ZodString>;
424
424
  type: z.ZodLiteral<"command_executed">;
425
- command: z.ZodString;
425
+ command: z.ZodNullable<z.ZodString>;
426
426
  args: z.ZodArray<z.ZodString>;
427
- cwd: z.ZodString;
427
+ cwd: z.ZodNullable<z.ZodString>;
428
428
  exit_code: z.ZodNullable<z.ZodNumber>;
429
429
  signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
430
430
  received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -669,8 +669,10 @@ type ClaudeTranscriptToPayloadOptions = {
669
669
  * than mapping one-to-one:
670
670
  *
671
671
  * - `session_started` / `session_ended` from the first / last timestamped record.
672
- * - `command_executed` from each `Bash` tool use, recorded as `bash -c "<cmd>"`
673
- * (the transcript carries the shell line, not a parsed argv).
672
+ * - `command_executed` from each `Bash` tool use, recorded as `["-c", <cmd>]`
673
+ * with a NULL executor and a NULL outcome: the transcript carries the shell
674
+ * line and nothing about what ran it or how it ended (#191). Only `<cmd>`
675
+ * was observed; the `-c` is basou's framing of "this is a shell program".
674
676
  * - `file_changed` from each `Edit` / `Write` / `NotebookEdit` tool use.
675
677
  * - `decision_recorded` from each `AskUserQuestion` tool use, but ONLY when the
676
678
  * recorded answer is a confirmed SELECTION — it exactly matches an option the
@@ -860,7 +862,10 @@ type CodexRolloutToPayloadOptions = {
860
862
  * provenance-level events from the rollout's message-level records:
861
863
  *
862
864
  * - `session_started` / `session_ended` from the first / last timestamped record.
863
- * - `command_executed` from each shell execution, recorded as `bash -c "<cmd>"`.
865
+ * - `command_executed` from each shell execution, recorded as `["-c", <cmd>]`
866
+ * with a NULL executor: the rollout carries the shell line, never what ran
867
+ * it, and codex in fact runs through `/bin/zsh -lc` (#191). The `-c` is
868
+ * basou's framing of "this is a shell program"; only `<cmd>` was observed.
864
869
  * Codex has written these two different ways, and BOTH are read (an operator's
865
870
  * `~/.codex/sessions` holds a mix across a CLI upgrade):
866
871
  * - one `function_call` named `exec_command` per command, with JSON
@@ -1153,9 +1158,9 @@ declare const CommandExecutedEventSchema: z.ZodObject<{
1153
1158
  source: z.ZodString;
1154
1159
  prev_hash: z.ZodOptional<z.ZodString>;
1155
1160
  type: z.ZodLiteral<"command_executed">;
1156
- command: z.ZodString;
1161
+ command: z.ZodNullable<z.ZodString>;
1157
1162
  args: z.ZodArray<z.ZodString>;
1158
- cwd: z.ZodString;
1163
+ cwd: z.ZodNullable<z.ZodString>;
1159
1164
  exit_code: z.ZodNullable<z.ZodNumber>;
1160
1165
  signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1161
1166
  received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -1463,9 +1468,9 @@ declare const EventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1463
1468
  source: z.ZodString;
1464
1469
  prev_hash: z.ZodOptional<z.ZodString>;
1465
1470
  type: z.ZodLiteral<"command_executed">;
1466
- command: z.ZodString;
1471
+ command: z.ZodNullable<z.ZodString>;
1467
1472
  args: z.ZodArray<z.ZodString>;
1468
- cwd: z.ZodString;
1473
+ cwd: z.ZodNullable<z.ZodString>;
1469
1474
  exit_code: z.ZodNullable<z.ZodNumber>;
1470
1475
  signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1471
1476
  received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -6051,6 +6056,15 @@ type ReviewGapUnit = {
6051
6056
  * a self-reported gap is still a gap.
6052
6057
  */
6053
6058
  selfReports: SelfReportedReview[];
6059
+ /**
6060
+ * How many of this unit's commits ran with no recorded exit code, so that the
6061
+ * commit landing is assumed rather than observed.
6062
+ *
6063
+ * A caveat, never an input to `verdict`: lowering the count would mean an
6064
+ * unverifiable commit could quietly leave the report, which is the same hole
6065
+ * a self-report must not open. `commitCount` still counts every commit.
6066
+ */
6067
+ commitsWithUnobservedOutcome: number;
6054
6068
  };
6055
6069
  /** Recorded reviews that reached no unit of work, broken down by cause. */
6056
6070
  type UnattachedSelfReports = {
package/dist/index.js CHANGED
@@ -471,7 +471,7 @@ function claudeTranscriptToImportPayload(records, options) {
471
471
  cachedInputTokens += readNonNegInt(usage.cache_read_input_tokens);
472
472
  }
473
473
  }
474
- const cwd = readString3(record.cwd) ?? workingDir ?? ".";
474
+ const cwd = readString3(record.cwd) ?? workingDir ?? null;
475
475
  for (const item of toolUses(record)) {
476
476
  const name = readString3(item.name);
477
477
  const input = isObject3(item.input) ? item.input : void 0;
@@ -581,9 +581,20 @@ function commandExecutedEvent(occurredAt, sessionId, command, cwd) {
581
581
  return {
582
582
  ...baseEvent(occurredAt, sessionId),
583
583
  type: "command_executed",
584
- command: "bash",
584
+ // The transcript records the Bash tool's command STRING and nothing about
585
+ // what ran it. `command: "bash"` used to be written here, which put an
586
+ // unobserved executor inside the hash chain; null says what is true. `-c`
587
+ // is kept so the line still reads as a shell program rather than an argv,
588
+ // which is the one bit readers need (#191).
589
+ command: null,
585
590
  args: ["-c", command],
586
591
  cwd,
592
+ // UNKNOWN rather than a signal termination or a success. NOT because the
593
+ // transcript is silent about the outcome -- it is not. A failed Bash tool
594
+ // result carries `is_error: true` and its text begins `Exit code N`, so the
595
+ // code is recoverable for the commands that failed (measured: 66 of 2,636
596
+ // tool results across twelve transcripts). Reading it is its own change and
597
+ // has not been made, so nothing is claimed here yet.
587
598
  exit_code: null,
588
599
  duration_ms: 0
589
600
  };
@@ -717,7 +728,7 @@ function codexRolloutToImportPayload(records, options) {
717
728
  ts,
718
729
  placeholderSessionId,
719
730
  command2.cmd,
720
- command2.workdir ?? workingDir ?? ".",
731
+ scriptedCommandCwd(command2.workdir, workingDir),
721
732
  {
722
733
  exitCode: null,
723
734
  durationMs
@@ -731,7 +742,7 @@ function codexRolloutToImportPayload(records, options) {
731
742
  if (readString4(payload2.name) !== "exec_command") continue;
732
743
  const command = readExecCommand(payload2.arguments);
733
744
  if (command === void 0) continue;
734
- const cwd = command.workdir ?? workingDir ?? ".";
745
+ const cwd = scriptedCommandCwd(command.workdir, workingDir);
735
746
  const output = readCallId(payload2.call_id, outputsByCallId);
736
747
  const execTsMs = Date.parse(ts);
737
748
  if (Number.isFinite(execTsMs)) engagementTsMs.push(execTsMs);
@@ -832,7 +843,14 @@ function commandExecutedEvent2(occurredAt, sessionId, command, cwd, outcome) {
832
843
  return {
833
844
  ...baseEvent2(occurredAt, sessionId),
834
845
  type: "command_executed",
835
- command: "bash",
846
+ // `command: "bash"` used to be written here and was WRONG, not merely
847
+ // unobserved: codex runs its commands through `/bin/zsh -lc`. The rollout
848
+ // carries no per-call shell to read, and the environment block's
849
+ // `environments.local.shell` describes the environment rather than what ran
850
+ // a given command — so writing `zsh` would be a better-informed guess, not
851
+ // an observation. null is what was actually observed about the executor
852
+ // (#191). `-c` is kept so the line still reads as a shell program.
853
+ command: null,
836
854
  args: ["-c", command],
837
855
  cwd,
838
856
  exit_code: outcome.exitCode,
@@ -990,6 +1008,9 @@ function findObjectEnd(script, start) {
990
1008
  }
991
1009
  function readExecArguments(literal) {
992
1010
  const values = /* @__PURE__ */ new Map();
1011
+ let workdirUnreadable = false;
1012
+ const survivesSpread = { cmd: false, workdir: false };
1013
+ let sawSpread = false;
993
1014
  let i = 1;
994
1015
  while (i < literal.length) {
995
1016
  const at = skipWhitespaceAndComments(literal, i);
@@ -1000,7 +1021,15 @@ function readExecArguments(literal) {
1000
1021
  i = at + 1;
1001
1022
  continue;
1002
1023
  }
1003
- if (literal.startsWith("...", at)) return void 0;
1024
+ if (literal.startsWith("...", at)) {
1025
+ sawSpread = true;
1026
+ survivesSpread.cmd = false;
1027
+ survivesSpread.workdir = false;
1028
+ const next2 = skipToPropertyEnd(literal, at);
1029
+ if (next2 === -1) return void 0;
1030
+ i = next2;
1031
+ continue;
1032
+ }
1004
1033
  let key;
1005
1034
  let afterKey;
1006
1035
  if (ch === '"' || ch === "'") {
@@ -1016,7 +1045,8 @@ function readExecArguments(literal) {
1016
1045
  const colon = skipWhitespaceAndComments(literal, afterKey);
1017
1046
  if (colon === -1) return void 0;
1018
1047
  if (literal[colon] !== ":") {
1019
- if (key === "cmd" || key === "workdir") return void 0;
1048
+ if (key === "cmd") return void 0;
1049
+ if (key === "workdir") workdirUnreadable = true;
1020
1050
  i = colon;
1021
1051
  const next2 = skipToPropertyEnd(literal, colon);
1022
1052
  if (next2 === -1) return void 0;
@@ -1039,23 +1069,42 @@ function readExecArguments(literal) {
1039
1069
  if (terminator === "," || terminator === "}") {
1040
1070
  value = decoded.length > 0 ? decoded : void 0;
1041
1071
  afterValue = after;
1042
- } else if (key === "cmd" || key === "workdir") {
1072
+ } else if (key === "cmd") {
1043
1073
  return void 0;
1074
+ } else if (key === "workdir") {
1075
+ workdirUnreadable = true;
1044
1076
  }
1045
- } else if (key === "cmd" || key === "workdir") {
1077
+ } else if (key === "cmd") {
1046
1078
  return void 0;
1079
+ } else if (key === "workdir") {
1080
+ workdirUnreadable = true;
1047
1081
  }
1048
1082
  if (key === "cmd" || key === "workdir") {
1049
- if (values.has(key)) return void 0;
1050
- values.set(key, value);
1083
+ if (values.has(key)) {
1084
+ if (key === "cmd") return void 0;
1085
+ workdirUnreadable = true;
1086
+ } else {
1087
+ values.set(key, value);
1088
+ if (value !== void 0) survivesSpread[key] = true;
1089
+ }
1051
1090
  }
1052
1091
  const next = skipToPropertyEnd(literal, afterValue);
1053
1092
  if (next === -1) return void 0;
1054
1093
  i = next;
1055
1094
  }
1056
1095
  const cmd = values.get("cmd");
1057
- if (cmd === void 0) return void 0;
1058
- return { cmd, workdir: values.get("workdir") };
1096
+ if (cmd === void 0 || !survivesSpread.cmd) return void 0;
1097
+ return { cmd, workdir: resolveScriptedWorkdir() };
1098
+ function resolveScriptedWorkdir() {
1099
+ if (workdirUnreadable) return null;
1100
+ if (values.has("workdir") && survivesSpread.workdir) return values.get("workdir");
1101
+ if (sawSpread) return null;
1102
+ return void 0;
1103
+ }
1104
+ }
1105
+ function scriptedCommandCwd(workdir, sessionCwd) {
1106
+ if (workdir === null) return null;
1107
+ return workdir ?? sessionCwd ?? null;
1059
1108
  }
1060
1109
  function skipToPropertyEnd(literal, at) {
1061
1110
  let i = at;
@@ -1422,9 +1471,9 @@ var ApprovalExpiredEventSchema = BaseEventSchema.extend({
1422
1471
  });
1423
1472
  var CommandExecutedEventSchema = BaseEventSchema.extend({
1424
1473
  type: z3.literal("command_executed"),
1425
- command: z3.string(),
1474
+ command: z3.string().nullable(),
1426
1475
  args: z3.array(z3.string()),
1427
- cwd: z3.string(),
1476
+ cwd: z3.string().nullable(),
1428
1477
  exit_code: z3.number().int().nullable(),
1429
1478
  signal: z3.string().nullable().optional(),
1430
1479
  received_signal: z3.string().nullable().optional(),
@@ -2484,8 +2533,10 @@ var SessionSourceSchema = z5.looseObject({
2484
2533
  var InvocationSchema = z5.looseObject({
2485
2534
  command: z5.string().min(1),
2486
2535
  args: z5.array(z5.string()).default([]),
2487
- // Nullable to record signal-terminated runs where the child has no exit
2488
- // code; the same nullability is mirrored in CommandExecutedEventSchema.
2536
+ // Nullable because the outcome may be UNKNOWN: a signal-terminated run has no
2537
+ // exit code, and an imported session's source may never record one. Unknown
2538
+ // is not success. The same nullability and the same meaning are mirrored in
2539
+ // CommandExecutedEventSchema.
2489
2540
  exit_code: z5.number().int().nullable()
2490
2541
  });
2491
2542
  var SessionMetricsSchema = z5.looseObject({
@@ -7577,9 +7628,13 @@ var CHDIR_OPTIONS = /* @__PURE__ */ new Map([
7577
7628
  ["just", /* @__PURE__ */ new Set(["-d", "--working-directory"])]
7578
7629
  ]);
7579
7630
  function deriveCommandWorkdir(command, args, cwd) {
7631
+ const script = args.length === 2 && args[0] === "-c" ? args[1] : void 0;
7632
+ if (command === null) {
7633
+ if (script === void 0) return ambiguous("unsupported_invocation");
7634
+ return readShellProgram(script, cwd);
7635
+ }
7580
7636
  const program = basename3(command);
7581
7637
  if (SHELLS.has(program)) {
7582
- const script = args.length === 2 && args[0] === "-c" ? args[1] : void 0;
7583
7638
  if (script === void 0) return ambiguous("unsupported_invocation");
7584
7639
  return readShellProgram(script, cwd);
7585
7640
  }
@@ -7900,7 +7955,7 @@ function readCdTarget(operands, cwd) {
7900
7955
  else return ambiguous("tilde_user");
7901
7956
  }
7902
7957
  if (!isAbsolute2(target)) {
7903
- if (!isAbsolute2(cwd)) return ambiguous("unresolvable_relative");
7958
+ if (cwd === null || !isAbsolute2(cwd)) return ambiguous("unresolvable_relative");
7904
7959
  target = resolve3(cwd, target);
7905
7960
  }
7906
7961
  return { kind: "target", path: target };
@@ -8008,8 +8063,9 @@ function commandRepoPath(command, args, cwd) {
8008
8063
  function commandRepo(command, args, cwd) {
8009
8064
  return normalizeRepoPath(commandRepoPath(command, args, cwd));
8010
8065
  }
8011
- function commandFailed(exitCode) {
8012
- return exitCode !== null && exitCode !== 0;
8066
+ function commandOutcome(exitCode) {
8067
+ if (exitCode === null) return "unknown";
8068
+ return exitCode === 0 ? "succeeded" : "failed";
8013
8069
  }
8014
8070
  function commitFiles(args) {
8015
8071
  const a = args.join(" ");
@@ -8065,7 +8121,8 @@ async function findReviewGaps(input) {
8065
8121
  continue;
8066
8122
  }
8067
8123
  if (ev.type !== "command_executed") continue;
8068
- if (commandFailed(ev.exit_code)) continue;
8124
+ const outcome = commandOutcome(ev.exit_code);
8125
+ if (outcome === "failed") continue;
8069
8126
  const at = Date.parse(ev.occurred_at);
8070
8127
  if (isReview) {
8071
8128
  const repo2 = commandRepo(ev.command, ev.args, ev.cwd);
@@ -8092,7 +8149,13 @@ async function findReviewGaps(input) {
8092
8149
  }
8093
8150
  const byRepo = workUnits.get(entry.sessionId) ?? /* @__PURE__ */ new Map();
8094
8151
  const list = byRepo.get(repo) ?? [];
8095
- list.push({ repo, at, files: commitFiles(ev.args), keyResolved });
8152
+ list.push({
8153
+ repo,
8154
+ at,
8155
+ files: commitFiles(ev.args),
8156
+ keyResolved,
8157
+ outcomeObserved: outcome === "succeeded"
8158
+ });
8096
8159
  byRepo.set(repo, list);
8097
8160
  workUnits.set(entry.sessionId, byRepo);
8098
8161
  }
@@ -8154,6 +8217,7 @@ async function findReviewGaps(input) {
8154
8217
  // Attached after the verdict is computed, and deliberately not an input
8155
8218
  // to it: a record must never move a unit out of `gaps`.
8156
8219
  selfReports: selfBound.map((r) => toSelfReportedReview(r, r.at > earliest)),
8220
+ commitsWithUnobservedOutcome: commits.filter((c) => !c.outcomeObserved).length,
8157
8221
  reviews: cited.map((r) => ({
8158
8222
  sessionId: r.sessionId,
8159
8223
  examinedDiff: r.repos.get(repoPath)?.examinedDiff ?? false,
@@ -8179,7 +8243,10 @@ async function findReviewGaps(input) {
8179
8243
  verdict: "unknown",
8180
8244
  reviews: [],
8181
8245
  // No repo key, so nothing a record's `repos` could bind to.
8182
- selfReports: []
8246
+ selfReports: [],
8247
+ // These commits were never placed in a repository, so the per-commit
8248
+ // outcome is not tracked for them; the unit is already a caveat.
8249
+ commitsWithUnobservedOutcome: 0
8183
8250
  });
8184
8251
  }
8185
8252
  const missed = selfReports.filter((r) => !attachedSelfReports.has(r.eventId));
@@ -9088,7 +9155,7 @@ function derivedEventContentKey(event) {
9088
9155
  const base = `${event.type}\0${event.occurred_at}`;
9089
9156
  switch (event.type) {
9090
9157
  case "command_executed":
9091
- return `${base}\0${event.command}\0${event.args.join("")}\0${event.cwd}`;
9158
+ return `${base} ${event.args.join("")} ${event.cwd}`;
9092
9159
  case "file_changed":
9093
9160
  return `${base}\0${event.path}\0${event.change_type}`;
9094
9161
  case "decision_recorded":