@basou/core 0.37.0 → 0.39.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
@@ -107,21 +107,12 @@ declare function removeStopHook(settings: unknown): StopHookRemoval;
107
107
  declare function findBasouStopHookCommand(settings: unknown): string | null;
108
108
 
109
109
  /**
110
- * Schema for `.basou/manifest.yaml`. The minimal manifest carries
111
- * schema_version, basou_version, workspace metadata, project info, enabled
112
- * capabilities, approval policy, adapter config, and git policy. The
113
- * `adapters."claude-code"` key uses a hyphen; downstream code accesses it
114
- * via bracket notation.
115
- *
116
- * Every object here is `looseObject` (NOT the default strip), so unknown keys
117
- * at every level survive parse. The manifest is the declarative source of truth
118
- * and is git-tracked and read-modify-written by `basou project` commands; with
119
- * the default strip, a field this basou does not recognize — a newer version's
120
- * additive field, a future adapter under `adapters`, a hand-added key — would be
121
- * silently dropped on the next write. Preserving them keeps basou from destroying
122
- * config it does not understand (forward-compatible), while known fields are still
123
- * fully type-checked and validated. {@link unknownManifestKeys} surfaces the
124
- * unrecognized top-level keys so preservation is not silent.
110
+ * The manifest, plus one guard the loose object cannot express: a TOP-LEVEL
111
+ * `confidential` is refused. It belongs under `policies`, and because the
112
+ * object is loose, a stray top-level spelling would otherwise parse fine and be
113
+ * silently ignored an operator would believe the workspace is protected while
114
+ * the face is still written. A safety key that is not honoured must fail
115
+ * loudly, not quietly.
125
116
  */
126
117
  declare const ManifestSchema: z.ZodObject<{
127
118
  schema_version: z.ZodString;
@@ -197,6 +188,12 @@ declare const ManifestSchema: z.ZodObject<{
197
188
  self: "self";
198
189
  }>>;
199
190
  }, z.core.$loose>>>;
191
+ channels: z.ZodOptional<z.ZodObject<{
192
+ codex: z.ZodOptional<z.ZodBoolean>;
193
+ }, z.core.$loose>>;
194
+ policies: z.ZodOptional<z.ZodObject<{
195
+ confidential: z.ZodOptional<z.ZodBoolean>;
196
+ }, z.core.$loose>>;
200
197
  }, z.core.$loose>;
201
198
  /** Inferred runtime type for {@link ManifestSchema}. */
202
199
  type Manifest = z.infer<typeof ManifestSchema>;
@@ -422,9 +419,9 @@ declare const SessionImportPayloadSchema: z.ZodObject<{
422
419
  source: z.ZodString;
423
420
  prev_hash: z.ZodOptional<z.ZodString>;
424
421
  type: z.ZodLiteral<"command_executed">;
425
- command: z.ZodString;
422
+ command: z.ZodNullable<z.ZodString>;
426
423
  args: z.ZodArray<z.ZodString>;
427
- cwd: z.ZodString;
424
+ cwd: z.ZodNullable<z.ZodString>;
428
425
  exit_code: z.ZodNullable<z.ZodNumber>;
429
426
  signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
430
427
  received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -669,8 +666,10 @@ type ClaudeTranscriptToPayloadOptions = {
669
666
  * than mapping one-to-one:
670
667
  *
671
668
  * - `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).
669
+ * - `command_executed` from each `Bash` tool use, recorded as `["-c", <cmd>]`
670
+ * with a NULL executor and a NULL outcome: the transcript carries the shell
671
+ * line and nothing about what ran it or how it ended (#191). Only `<cmd>`
672
+ * was observed; the `-c` is basou's framing of "this is a shell program".
674
673
  * - `file_changed` from each `Edit` / `Write` / `NotebookEdit` tool use.
675
674
  * - `decision_recorded` from each `AskUserQuestion` tool use, but ONLY when the
676
675
  * recorded answer is a confirmed SELECTION — it exactly matches an option the
@@ -860,7 +859,10 @@ type CodexRolloutToPayloadOptions = {
860
859
  * provenance-level events from the rollout's message-level records:
861
860
  *
862
861
  * - `session_started` / `session_ended` from the first / last timestamped record.
863
- * - `command_executed` from each shell execution, recorded as `bash -c "<cmd>"`.
862
+ * - `command_executed` from each shell execution, recorded as `["-c", <cmd>]`
863
+ * with a NULL executor: the rollout carries the shell line, never what ran
864
+ * it, and codex in fact runs through `/bin/zsh -lc` (#191). The `-c` is
865
+ * basou's framing of "this is a shell program"; only `<cmd>` was observed.
864
866
  * Codex has written these two different ways, and BOTH are read (an operator's
865
867
  * `~/.codex/sessions` holds a mix across a CLI upgrade):
866
868
  * - one `function_call` named `exec_command` per command, with JSON
@@ -1153,9 +1155,9 @@ declare const CommandExecutedEventSchema: z.ZodObject<{
1153
1155
  source: z.ZodString;
1154
1156
  prev_hash: z.ZodOptional<z.ZodString>;
1155
1157
  type: z.ZodLiteral<"command_executed">;
1156
- command: z.ZodString;
1158
+ command: z.ZodNullable<z.ZodString>;
1157
1159
  args: z.ZodArray<z.ZodString>;
1158
- cwd: z.ZodString;
1160
+ cwd: z.ZodNullable<z.ZodString>;
1159
1161
  exit_code: z.ZodNullable<z.ZodNumber>;
1160
1162
  signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1161
1163
  received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -1463,9 +1465,9 @@ declare const EventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1463
1465
  source: z.ZodString;
1464
1466
  prev_hash: z.ZodOptional<z.ZodString>;
1465
1467
  type: z.ZodLiteral<"command_executed">;
1466
- command: z.ZodString;
1468
+ command: z.ZodNullable<z.ZodString>;
1467
1469
  args: z.ZodArray<z.ZodString>;
1468
- cwd: z.ZodString;
1470
+ cwd: z.ZodNullable<z.ZodString>;
1469
1471
  exit_code: z.ZodNullable<z.ZodNumber>;
1470
1472
  signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1471
1473
  received_signal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -6051,6 +6053,15 @@ type ReviewGapUnit = {
6051
6053
  * a self-reported gap is still a gap.
6052
6054
  */
6053
6055
  selfReports: SelfReportedReview[];
6056
+ /**
6057
+ * How many of this unit's commits ran with no recorded exit code, so that the
6058
+ * commit landing is assumed rather than observed.
6059
+ *
6060
+ * A caveat, never an input to `verdict`: lowering the count would mean an
6061
+ * unverifiable commit could quietly leave the report, which is the same hole
6062
+ * a self-report must not open. `commitCount` still counts every commit.
6063
+ */
6064
+ commitsWithUnobservedOutcome: number;
6054
6065
  };
6055
6066
  /** Recorded reviews that reached no unit of work, broken down by cause. */
6056
6067
  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(),
@@ -1733,7 +1782,35 @@ var WorkspaceMetaSchema = z4.looseObject({
1733
1782
  */
1734
1783
  view: SourceRootSchema.optional()
1735
1784
  });
1736
- var ManifestSchema = z4.looseObject({
1785
+ var ChannelsSchema = z4.looseObject({
1786
+ /**
1787
+ * Render this workspace's orientation into `~/.codex/AGENTS.md` on `basou
1788
+ * refresh` / `basou run codex`. A boolean today; if a face ever needs
1789
+ * per-block control this widens to `boolean | object`, which existing
1790
+ * manifests keep parsing — so the boolean does not foreclose that.
1791
+ */
1792
+ codex: z4.boolean().optional().meta({
1793
+ description: "Opt in to rendering this workspace's orientation into the user-global ~/.codex/AGENTS.md on `basou refresh` and `basou run codex`. Off when absent: that file is auto-loaded by Codex for every project on the machine, so nothing is written there unless the workspace says so."
1794
+ })
1795
+ });
1796
+ var PoliciesSchema = z4.looseObject({
1797
+ /**
1798
+ * This workspace's provenance must not persist where another workspace's
1799
+ * tool reads it. That is the GOAL the key states; what it gates today is one
1800
+ * thing: rendering the orientation into the user-global `~/.codex/AGENTS.md`
1801
+ * (`basou refresh` / `basou run codex`) — never, regardless of `channels`, so
1802
+ * a confidential workspace cannot be re-enabled by a second declaration in
1803
+ * the same file. It does not yet govern `basou protocol sync` (a global
1804
+ * render, not a per-workspace one), and it gates writing only: it cannot keep
1805
+ * another workspace's block out of this workspace's tool (see `basou channel
1806
+ * clear`). Stated as a goal rather than as "never write" so that a transient
1807
+ * render removed before anyone else can read it remains permissible.
1808
+ */
1809
+ confidential: z4.boolean().optional().meta({
1810
+ description: "This workspace's provenance must not persist where another workspace's tool reads it. Today this means its orientation is never rendered into the user-global ~/.codex/AGENTS.md by `basou refresh` or `basou run codex`, regardless of `channels`. It does not yet govern `basou protocol sync`, and it gates writing only \u2014 it cannot keep another workspace's block out of this workspace's tool."
1811
+ })
1812
+ });
1813
+ var ManifestObjectSchema = z4.looseObject({
1737
1814
  schema_version: SchemaVersionSchema,
1738
1815
  // Same forward-compatible format gate as schema_version (accept 0.x.y, gate a
1739
1816
  // higher major with an upgrade error) rather than a hard literal. `basou_version`
@@ -1747,9 +1824,20 @@ var ManifestSchema = z4.looseObject({
1747
1824
  adapters: AdaptersSchema,
1748
1825
  git: GitConfigSchema,
1749
1826
  import: ImportConfigSchema.optional(),
1750
- repos: z4.array(RepoEntrySchema).min(1).optional()
1827
+ repos: z4.array(RepoEntrySchema).min(1).optional(),
1828
+ channels: ChannelsSchema.optional(),
1829
+ policies: PoliciesSchema.optional()
1751
1830
  });
1752
- var KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(ManifestSchema.shape));
1831
+ var ManifestSchema = ManifestObjectSchema.superRefine((manifest, ctx) => {
1832
+ if ("confidential" in manifest) {
1833
+ ctx.addIssue({
1834
+ code: "custom",
1835
+ path: ["confidential"],
1836
+ message: "`confidential` is declared under `policies` (policies.confidential: true). A top-level `confidential` is not honoured, so it is rejected rather than ignored."
1837
+ });
1838
+ }
1839
+ });
1840
+ var KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(ManifestObjectSchema.shape));
1753
1841
  function unknownManifestKeys(manifest) {
1754
1842
  return Object.keys(manifest).filter((k) => !KNOWN_TOP_LEVEL_KEYS.has(k)).sort();
1755
1843
  }
@@ -2484,8 +2572,10 @@ var SessionSourceSchema = z5.looseObject({
2484
2572
  var InvocationSchema = z5.looseObject({
2485
2573
  command: z5.string().min(1),
2486
2574
  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.
2575
+ // Nullable because the outcome may be UNKNOWN: a signal-terminated run has no
2576
+ // exit code, and an imported session's source may never record one. Unknown
2577
+ // is not success. The same nullability and the same meaning are mirrored in
2578
+ // CommandExecutedEventSchema.
2489
2579
  exit_code: z5.number().int().nullable()
2490
2580
  });
2491
2581
  var SessionMetricsSchema = z5.looseObject({
@@ -7577,9 +7667,13 @@ var CHDIR_OPTIONS = /* @__PURE__ */ new Map([
7577
7667
  ["just", /* @__PURE__ */ new Set(["-d", "--working-directory"])]
7578
7668
  ]);
7579
7669
  function deriveCommandWorkdir(command, args, cwd) {
7670
+ const script = args.length === 2 && args[0] === "-c" ? args[1] : void 0;
7671
+ if (command === null) {
7672
+ if (script === void 0) return ambiguous("unsupported_invocation");
7673
+ return readShellProgram(script, cwd);
7674
+ }
7580
7675
  const program = basename3(command);
7581
7676
  if (SHELLS.has(program)) {
7582
- const script = args.length === 2 && args[0] === "-c" ? args[1] : void 0;
7583
7677
  if (script === void 0) return ambiguous("unsupported_invocation");
7584
7678
  return readShellProgram(script, cwd);
7585
7679
  }
@@ -7900,7 +7994,7 @@ function readCdTarget(operands, cwd) {
7900
7994
  else return ambiguous("tilde_user");
7901
7995
  }
7902
7996
  if (!isAbsolute2(target)) {
7903
- if (!isAbsolute2(cwd)) return ambiguous("unresolvable_relative");
7997
+ if (cwd === null || !isAbsolute2(cwd)) return ambiguous("unresolvable_relative");
7904
7998
  target = resolve3(cwd, target);
7905
7999
  }
7906
8000
  return { kind: "target", path: target };
@@ -8008,8 +8102,9 @@ function commandRepoPath(command, args, cwd) {
8008
8102
  function commandRepo(command, args, cwd) {
8009
8103
  return normalizeRepoPath(commandRepoPath(command, args, cwd));
8010
8104
  }
8011
- function commandFailed(exitCode) {
8012
- return exitCode !== null && exitCode !== 0;
8105
+ function commandOutcome(exitCode) {
8106
+ if (exitCode === null) return "unknown";
8107
+ return exitCode === 0 ? "succeeded" : "failed";
8013
8108
  }
8014
8109
  function commitFiles(args) {
8015
8110
  const a = args.join(" ");
@@ -8065,7 +8160,8 @@ async function findReviewGaps(input) {
8065
8160
  continue;
8066
8161
  }
8067
8162
  if (ev.type !== "command_executed") continue;
8068
- if (commandFailed(ev.exit_code)) continue;
8163
+ const outcome = commandOutcome(ev.exit_code);
8164
+ if (outcome === "failed") continue;
8069
8165
  const at = Date.parse(ev.occurred_at);
8070
8166
  if (isReview) {
8071
8167
  const repo2 = commandRepo(ev.command, ev.args, ev.cwd);
@@ -8092,7 +8188,13 @@ async function findReviewGaps(input) {
8092
8188
  }
8093
8189
  const byRepo = workUnits.get(entry.sessionId) ?? /* @__PURE__ */ new Map();
8094
8190
  const list = byRepo.get(repo) ?? [];
8095
- list.push({ repo, at, files: commitFiles(ev.args), keyResolved });
8191
+ list.push({
8192
+ repo,
8193
+ at,
8194
+ files: commitFiles(ev.args),
8195
+ keyResolved,
8196
+ outcomeObserved: outcome === "succeeded"
8197
+ });
8096
8198
  byRepo.set(repo, list);
8097
8199
  workUnits.set(entry.sessionId, byRepo);
8098
8200
  }
@@ -8154,6 +8256,7 @@ async function findReviewGaps(input) {
8154
8256
  // Attached after the verdict is computed, and deliberately not an input
8155
8257
  // to it: a record must never move a unit out of `gaps`.
8156
8258
  selfReports: selfBound.map((r) => toSelfReportedReview(r, r.at > earliest)),
8259
+ commitsWithUnobservedOutcome: commits.filter((c) => !c.outcomeObserved).length,
8157
8260
  reviews: cited.map((r) => ({
8158
8261
  sessionId: r.sessionId,
8159
8262
  examinedDiff: r.repos.get(repoPath)?.examinedDiff ?? false,
@@ -8179,7 +8282,10 @@ async function findReviewGaps(input) {
8179
8282
  verdict: "unknown",
8180
8283
  reviews: [],
8181
8284
  // No repo key, so nothing a record's `repos` could bind to.
8182
- selfReports: []
8285
+ selfReports: [],
8286
+ // These commits were never placed in a repository, so the per-commit
8287
+ // outcome is not tracked for them; the unit is already a caveat.
8288
+ commitsWithUnobservedOutcome: 0
8183
8289
  });
8184
8290
  }
8185
8291
  const missed = selfReports.filter((r) => !attachedSelfReports.has(r.eventId));
@@ -9088,7 +9194,7 @@ function derivedEventContentKey(event) {
9088
9194
  const base = `${event.type}\0${event.occurred_at}`;
9089
9195
  switch (event.type) {
9090
9196
  case "command_executed":
9091
- return `${base}\0${event.command}\0${event.args.join("")}\0${event.cwd}`;
9197
+ return `${base} ${event.args.join("")} ${event.cwd}`;
9092
9198
  case "file_changed":
9093
9199
  return `${base}\0${event.path}\0${event.change_type}`;
9094
9200
  case "decision_recorded":