@basou/core 0.29.0 → 0.31.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.js CHANGED
@@ -1,5 +1,14 @@
1
- // src/adapters/claude-code/claude-code-adapter.ts
1
+ // src/adapters/command-lookup.ts
2
2
  import { spawn } from "child_process";
3
+ async function isOnPath(command) {
4
+ return new Promise((resolve3) => {
5
+ const child = spawn("which", [command], { stdio: "ignore" });
6
+ child.on("error", () => resolve3(false));
7
+ child.on("exit", (code) => resolve3(code === 0));
8
+ });
9
+ }
10
+
11
+ // src/adapters/claude-code/claude-code-adapter.ts
3
12
  var claudeCodeAdapterMetadata = {
4
13
  kind: "claude-code-adapter",
5
14
  version: "0.1.0"
@@ -10,13 +19,6 @@ async function resolveClaudeCodeCommand(lookup = isOnPath) {
10
19
  }
11
20
  throw new Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.");
12
21
  }
13
- async function isOnPath(command) {
14
- return new Promise((resolve3) => {
15
- const child = spawn("which", [command], { stdio: "ignore" });
16
- child.on("error", () => resolve3(false));
17
- child.on("exit", (code) => resolve3(code === 0));
18
- });
19
- }
20
22
  function summarizeAdapterOutput(_stream, _raw) {
21
23
  throw new Error("adapter_output summary is not implemented in this release");
22
24
  }
@@ -33,6 +35,7 @@ function shellQuote(value) {
33
35
  function buildStopHookCommand(options) {
34
36
  const flags = [];
35
37
  if (options.block === true) flags.push("--block");
38
+ if (options.requireReview === true) flags.push("--require-review");
36
39
  if (options.minEdits !== void 0) flags.push(`--min-edits ${options.minEdits}`);
37
40
  const suffix = flags.length > 0 ? ` ${flags.join(" ")}` : "";
38
41
  return `node ${shellQuote(options.cliEntry)} hook stop${suffix} 2>/dev/null || true`;
@@ -218,6 +221,14 @@ var CAPTURE_VERB = /(?:decision\s+(?:capture|record)|note)\b/;
218
221
  var CAPTURE_COMMAND_PATTERN = new RegExp(
219
222
  `(?:^|[\\n;&|(])\\s*${CAPTURE_INVOCATION.source}\\s+${CAPTURE_VERB.source}`
220
223
  );
224
+ var SHIP_ACT_PATTERN = /(?:^|[\n;&|(])\s*(?:git\s+push|git\s+merge|gh\s+pr\s+(?:create|merge))(?![-\w])/;
225
+ var DRY_RUN_PUSH_PATTERN = /(?:^|[\n;&|(])\s*git\s+push\b[^\n;&|()]*?\s-(?:-dry-run|n)\b/;
226
+ function isShipAct(command) {
227
+ return SHIP_ACT_PATTERN.test(command) && !DRY_RUN_PUSH_PATTERN.test(command);
228
+ }
229
+ var REVIEW_RECORD_PATTERN = new RegExp(
230
+ `(?:^|[\\n;&|(])\\s*${CAPTURE_INVOCATION.source}\\s+review\\s+record\\b`
231
+ );
221
232
  var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit"]);
222
233
  function evaluateStopHook(input) {
223
234
  const minEdits = input.minEdits ?? DEFAULT_STOP_HOOK_MIN_EDITS;
@@ -225,6 +236,7 @@ function evaluateStopHook(input) {
225
236
  return {
226
237
  kind: "silent",
227
238
  reason: "stop_hook_active",
239
+ review: { fires: false, reason: "stop_hook_active" },
228
240
  commandCount: 0,
229
241
  fileCount: 0,
230
242
  decisionPointCount: 0
@@ -233,6 +245,8 @@ function evaluateStopHook(input) {
233
245
  let commandCount = 0;
234
246
  let fileCount = 0;
235
247
  let captured = false;
248
+ let shipped = false;
249
+ let reviewed = false;
236
250
  for (const record of input.records) {
237
251
  if (readString2(record.type) !== "assistant") continue;
238
252
  for (const tool of toolUsesOf2(record)) {
@@ -242,7 +256,11 @@ function evaluateStopHook(input) {
242
256
  commandCount += 1;
243
257
  const toolInput = isObject2(tool.input) ? tool.input : void 0;
244
258
  const command = toolInput !== void 0 ? readString2(toolInput.command) : void 0;
245
- if (command !== void 0 && CAPTURE_COMMAND_PATTERN.test(command)) captured = true;
259
+ if (command !== void 0) {
260
+ if (CAPTURE_COMMAND_PATTERN.test(command)) captured = true;
261
+ if (isShipAct(command)) shipped = true;
262
+ if (REVIEW_RECORD_PATTERN.test(command)) reviewed = true;
263
+ }
246
264
  } else if (FILE_EDIT_TOOLS.has(name)) {
247
265
  fileCount += 1;
248
266
  }
@@ -250,14 +268,21 @@ function evaluateStopHook(input) {
250
268
  }
251
269
  const decisionPointCount = countUncapturedDecisionPoints(input.records);
252
270
  const counts = { commandCount, fileCount, decisionPointCount };
271
+ const review = evaluateReviewGate({ shipped, reviewed, fileCount, minEdits });
253
272
  if (captured) {
254
- return { kind: "silent", reason: "already_captured", ...counts };
273
+ return { kind: "silent", reason: "already_captured", review, ...counts };
255
274
  }
256
275
  const substantive = fileCount >= minEdits || decisionPointCount > 0;
257
276
  if (!substantive) {
258
- return { kind: "silent", reason: "not_substantive", ...counts };
277
+ return { kind: "silent", reason: "not_substantive", review, ...counts };
259
278
  }
260
- return { kind: "nudge", additionalContext: renderNudge(counts), ...counts };
279
+ return { kind: "nudge", additionalContext: renderNudge(counts), review, ...counts };
280
+ }
281
+ function evaluateReviewGate(input) {
282
+ if (!input.shipped) return { fires: false, reason: "no_ship_act" };
283
+ if (input.fileCount < input.minEdits) return { fires: false, reason: "not_substantive_code" };
284
+ if (input.reviewed) return { fires: false, reason: "already_reviewed" };
285
+ return { fires: true, additionalContext: renderReviewNudge() };
261
286
  }
262
287
  function renderNudge(counts) {
263
288
  const did = [];
@@ -280,6 +305,14 @@ function renderNudge(counts) {
280
305
  "If nothing is worth capturing, just stop \u2014 do not invent decisions."
281
306
  ].join("\n");
282
307
  }
308
+ function renderReviewNudge() {
309
+ return [
310
+ "This session shipped code (a push / PR / merge) after substantive edits but recorded no review.",
311
+ "An adversarial / second-opinion review before shipping is the discipline here. If a review ran, record it now so it lands on the durable trail:",
312
+ ' - run `basou review record` and pipe a JSON object: { "reviewer": "...", "target": "...", with optional "verdict" / "findings" / "blocked" } (an explicit "blocked": [] records that you blocked nothing).',
313
+ "If no review ran, that is the gap this is meant to catch \u2014 review before relying on this. If a review genuinely was not warranted, just stop \u2014 do not fabricate a review record."
314
+ ].join("\n");
315
+ }
283
316
  function readString2(value) {
284
317
  return typeof value === "string" && value.length > 0 ? value : void 0;
285
318
  }
@@ -606,6 +639,16 @@ function toolUses(record) {
606
639
  return result;
607
640
  }
608
641
 
642
+ // src/adapters/codex/codex-adapter.ts
643
+ var codexAdapterMetadata = {
644
+ kind: "codex-adapter",
645
+ version: "0.1.0"
646
+ };
647
+ async function resolveCodexCommand(lookup = isOnPath) {
648
+ if (await lookup("codex")) return { command: "codex" };
649
+ throw new Error("Codex CLI not found in PATH. Install codex first.");
650
+ }
651
+
609
652
  // src/adapters/codex/rollout-importer.ts
610
653
  var CODEX_IMPORT_SOURCE = "codex-import";
611
654
  function codexRolloutToImportPayload(records, options) {
@@ -867,7 +910,10 @@ import { z as z2 } from "zod";
867
910
 
868
911
  // src/schemas/shared.schema.ts
869
912
  import { z } from "zod";
870
- var SchemaVersionSchema = z.literal("0.1.0");
913
+ var SchemaVersionSchema = z.string().regex(/^0\.\d+\.\d+$/, {
914
+ message: "unsupported .basou format version: this basou reads format major 0 (0.x.y). If this workspace was written by a newer basou, upgrade basou to open it."
915
+ });
916
+ var CacheVersionSchema = z.literal("0.1.0");
871
917
  var IsoTimestampSchema = z.string().datetime({ offset: true });
872
918
  var createPrefixedIdSchema = (prefix) => {
873
919
  const refiner = (value) => isValidPrefixedId(value) && value.startsWith(`${prefix}_`);
@@ -887,14 +933,14 @@ var EventSourceSchema = z.string().min(1);
887
933
 
888
934
  // src/schemas/approval.schema.ts
889
935
  var ApprovalStatusSchema = z2.enum(["pending", "approved", "rejected", "expired"]);
890
- var ApprovalSchema = z2.object({
936
+ var ApprovalSchema = z2.looseObject({
891
937
  schema_version: SchemaVersionSchema,
892
938
  id: ApprovalIdSchema,
893
939
  session_id: SessionIdSchema,
894
940
  created_at: IsoTimestampSchema,
895
941
  status: ApprovalStatusSchema,
896
942
  risk_level: RiskLevelSchema,
897
- action: z2.object({ kind: z2.string() }).passthrough(),
943
+ action: z2.looseObject({ kind: z2.string() }).passthrough(),
898
944
  reason: z2.string(),
899
945
  expires_at: IsoTimestampSchema.nullable().default(null),
900
946
  // The four fields below are null while `status === "pending"` and set
@@ -1196,6 +1242,25 @@ var NoteAddedEventSchema = BaseEventSchema.extend({
1196
1242
  // surface. Optional so pre-existing note_added events remain valid.
1197
1243
  kind: z3.enum(["note", "next_step"]).optional()
1198
1244
  });
1245
+ var ReviewFindingSchema = z3.object({
1246
+ title: z3.string().min(1),
1247
+ severity: z3.enum(["high", "medium", "low"]).optional(),
1248
+ location: z3.string().min(1).optional(),
1249
+ summary: z3.string().min(1).optional()
1250
+ });
1251
+ var ReviewBlockedSchema = z3.object({
1252
+ title: z3.string().min(1),
1253
+ reason: z3.enum(["spec-deviation", "design-reversal"]),
1254
+ why: z3.string().min(1).optional()
1255
+ });
1256
+ var ReviewRecordedEventSchema = BaseEventSchema.extend({
1257
+ type: z3.literal("review_recorded"),
1258
+ reviewer: z3.string().min(1),
1259
+ target: z3.string().min(1),
1260
+ verdict: z3.enum(["pass", "needs-attention", "fail"]).optional(),
1261
+ findings: z3.array(ReviewFindingSchema).optional(),
1262
+ blocked: z3.array(ReviewBlockedSchema).optional()
1263
+ });
1199
1264
  var AdapterOutputEventSchema = BaseEventSchema.extend({
1200
1265
  type: z3.literal("adapter_output"),
1201
1266
  stream: z3.enum(["stdout", "stderr"]),
@@ -1223,6 +1288,7 @@ var EventSchema = z3.discriminatedUnion("type", [
1223
1288
  TaskDeletedEventSchema,
1224
1289
  TaskArchivedEventSchema,
1225
1290
  NoteAddedEventSchema,
1291
+ ReviewRecordedEventSchema,
1226
1292
  AdapterOutputEventSchema
1227
1293
  ]);
1228
1294
 
@@ -1521,12 +1587,13 @@ var SessionStatusSchema = z4.enum([
1521
1587
  var SessionSourceKindSchema = z4.enum([
1522
1588
  "claude-code-adapter",
1523
1589
  "claude-code-import",
1590
+ "codex-adapter",
1524
1591
  "codex-import",
1525
1592
  "human",
1526
1593
  "import",
1527
1594
  "terminal"
1528
1595
  ]);
1529
- var SessionSourceSchema = z4.object({
1596
+ var SessionSourceSchema = z4.looseObject({
1530
1597
  kind: SessionSourceKindSchema,
1531
1598
  version: z4.literal("0.1.0"),
1532
1599
  // Optional id of the originating session in the SOURCE tool's own
@@ -1542,20 +1609,20 @@ var SessionSourceSchema = z4.object({
1542
1609
  // fresh import or `--force`).
1543
1610
  source_size_bytes: z4.number().int().nonnegative().optional()
1544
1611
  });
1545
- var InvocationSchema = z4.object({
1612
+ var InvocationSchema = z4.looseObject({
1546
1613
  command: z4.string().min(1),
1547
1614
  args: z4.array(z4.string()).default([]),
1548
1615
  // Nullable to record signal-terminated runs where the child has no exit
1549
1616
  // code; the same nullability is mirrored in CommandExecutedEventSchema.
1550
1617
  exit_code: z4.number().int().nullable()
1551
1618
  });
1552
- var SessionMetricsSchema = z4.object({
1619
+ var SessionMetricsSchema = z4.looseObject({
1553
1620
  output_tokens: z4.number().int().nonnegative().optional(),
1554
1621
  input_tokens: z4.number().int().nonnegative().optional(),
1555
1622
  cached_input_tokens: z4.number().int().nonnegative().optional(),
1556
1623
  reasoning_output_tokens: z4.number().int().nonnegative().optional(),
1557
1624
  active_time_ms: z4.number().int().nonnegative().optional(),
1558
- active_intervals: z4.array(z4.object({ start: IsoTimestampSchema, end: IsoTimestampSchema })).optional(),
1625
+ active_intervals: z4.array(z4.looseObject({ start: IsoTimestampSchema, end: IsoTimestampSchema })).optional(),
1559
1626
  active_gap_cap_ms: z4.number().int().nonnegative().optional(),
1560
1627
  active_time_method: z4.string().optional(),
1561
1628
  machine_active_time_ms: z4.number().int().nonnegative().optional()
@@ -1564,7 +1631,7 @@ var SessionIntegritySchema = z4.object({
1564
1631
  head_hash: z4.string(),
1565
1632
  event_count: z4.number().int().nonnegative()
1566
1633
  }).strict();
1567
- var SessionInnerSchema = z4.object({
1634
+ var SessionInnerSchema = z4.looseObject({
1568
1635
  id: SessionIdSchema,
1569
1636
  label: z4.string().optional(),
1570
1637
  task_id: TaskIdSchema.nullable().optional(),
@@ -1582,7 +1649,7 @@ var SessionInnerSchema = z4.object({
1582
1649
  metrics: SessionMetricsSchema.optional(),
1583
1650
  integrity: SessionIntegritySchema.optional()
1584
1651
  });
1585
- var SessionSchema = z4.object({
1652
+ var SessionSchema = z4.looseObject({
1586
1653
  schema_version: SchemaVersionSchema,
1587
1654
  session: SessionInnerSchema
1588
1655
  });
@@ -2082,12 +2149,16 @@ import * as fsp from "fs/promises";
2082
2149
  // src/schemas/status.schema.ts
2083
2150
  import { z as z5 } from "zod";
2084
2151
  var StatusSchema = z5.object({
2085
- schema_version: SchemaVersionSchema,
2152
+ // status.json is a rebuildable cache: exact-match-or-rebuild, not the
2153
+ // durable forward-compat gate.
2154
+ schema_version: CacheVersionSchema,
2086
2155
  generated_at: IsoTimestampSchema,
2087
2156
  workspace: z5.object({
2088
2157
  id: WorkspaceIdSchema,
2089
2158
  name: z5.string().min(1),
2090
- basou_version: z5.literal("0.1.0")
2159
+ // Mirrors the manifest's basou_version, so it uses the same
2160
+ // forward-compatible format gate (accept 0.x.y) rather than a literal.
2161
+ basou_version: SchemaVersionSchema
2091
2162
  }).strict(),
2092
2163
  directories_present: z5.object({
2093
2164
  sessions: z5.boolean(),
@@ -2447,7 +2518,7 @@ import { z as z8 } from "zod";
2447
2518
  // src/schemas/task.schema.ts
2448
2519
  import { z as z6 } from "zod";
2449
2520
  var TaskStatusSchema = z6.enum(["planned", "in_progress", "done", "cancelled"]);
2450
- var TaskInnerSchema = z6.object({
2521
+ var TaskInnerSchema = z6.looseObject({
2451
2522
  id: TaskIdSchema,
2452
2523
  title: z6.string().min(1),
2453
2524
  label: z6.string().min(1).optional(),
@@ -2476,7 +2547,7 @@ var TaskInnerSchema = z6.object({
2476
2547
  */
2477
2548
  linked_sessions: z6.array(SessionIdSchema).default([])
2478
2549
  });
2479
- var TaskSchema = z6.object({
2550
+ var TaskSchema = z6.looseObject({
2480
2551
  schema_version: SchemaVersionSchema,
2481
2552
  task: TaskInnerSchema
2482
2553
  });
@@ -2726,7 +2797,8 @@ var TaskIndexEntrySchema = z7.object({
2726
2797
  updated_at: IsoTimestampSchema
2727
2798
  }).strict();
2728
2799
  var TaskIndexSchema = z7.object({
2729
- schema_version: SchemaVersionSchema,
2800
+ // Rebuildable cache: exact-match-or-rebuild, not the durable forward-compat gate.
2801
+ schema_version: CacheVersionSchema,
2730
2802
  tasks: z7.array(TaskIndexEntrySchema),
2731
2803
  last_rebuilt_at: IsoTimestampSchema
2732
2804
  }).strict();
@@ -4664,8 +4736,7 @@ import { lstat as lstat3 } from "fs/promises";
4664
4736
  import { z as z9 } from "zod";
4665
4737
  var ProjectSchema = z9.looseObject({
4666
4738
  name: z9.string().optional(),
4667
- description: z9.string().optional(),
4668
- repository_url: z9.string().nullable().optional()
4739
+ description: z9.string().optional()
4669
4740
  });
4670
4741
  var CapabilitiesSchema = z9.looseObject({
4671
4742
  enabled: z9.array(z9.string())
@@ -4723,7 +4794,11 @@ var WorkspaceMetaSchema = z9.looseObject({
4723
4794
  });
4724
4795
  var ManifestSchema = z9.looseObject({
4725
4796
  schema_version: SchemaVersionSchema,
4726
- basou_version: z9.literal("0.1.0"),
4797
+ // Same forward-compatible format gate as schema_version (accept 0.x.y, gate a
4798
+ // higher major with an upgrade error) rather than a hard literal. `basou_version`
4799
+ // is a format stamp, not the npm/product version; consolidating it with
4800
+ // schema_version is a candidate cleanup for the M4 freeze pass.
4801
+ basou_version: SchemaVersionSchema,
4727
4802
  workspace: WorkspaceMetaSchema,
4728
4803
  project: ProjectSchema,
4729
4804
  capabilities: CapabilitiesSchema,
@@ -4747,8 +4822,7 @@ function createManifest(input) {
4747
4822
  const workspaceId = input.workspaceId ?? prefixedUlid("ws");
4748
4823
  const project = {
4749
4824
  ...input.projectName !== void 0 ? { name: input.projectName } : {},
4750
- ...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
4751
- ...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
4825
+ ...input.projectDescription !== void 0 ? { description: input.projectDescription } : {}
4752
4826
  };
4753
4827
  const manifest = {
4754
4828
  schema_version: "0.1.0",
@@ -4778,6 +4852,7 @@ function createManifest(input) {
4778
4852
  async function writeManifest(paths, manifest, options) {
4779
4853
  const force = options?.force === true;
4780
4854
  const validated = ManifestSchema.parse(manifest);
4855
+ delete validated.project.repository_url;
4781
4856
  if (!force) {
4782
4857
  let existed = false;
4783
4858
  try {
@@ -6867,6 +6942,159 @@ async function findReviewGaps(input) {
6867
6942
  };
6868
6943
  }
6869
6944
 
6945
+ // src/review/review-record.ts
6946
+ var VALID_VERDICTS = /* @__PURE__ */ new Set(["pass", "needs-attention", "fail"]);
6947
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
6948
+ var VALID_BLOCK_REASONS = /* @__PURE__ */ new Set(["spec-deviation", "design-reversal"]);
6949
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
6950
+ "reviewer",
6951
+ "target",
6952
+ "verdict",
6953
+ "findings",
6954
+ "blocked"
6955
+ ]);
6956
+ var ALLOWED_FINDING_KEYS = /* @__PURE__ */ new Set([
6957
+ "title",
6958
+ "severity",
6959
+ "location",
6960
+ "summary"
6961
+ ]);
6962
+ var ALLOWED_BLOCKED_KEYS = /* @__PURE__ */ new Set(["title", "reason", "why"]);
6963
+ var REVIEW_RECORD_NO_INPUT_HINT = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
6964
+ function parseReviewRecordInput(raw) {
6965
+ if (raw.trim().length === 0) {
6966
+ throw new Error(REVIEW_RECORD_NO_INPUT_HINT);
6967
+ }
6968
+ let parsed;
6969
+ try {
6970
+ parsed = JSON.parse(raw);
6971
+ } catch (error) {
6972
+ const detail = error instanceof Error ? error.message : String(error);
6973
+ throw new Error(`Input is not valid JSON: ${detail}`);
6974
+ }
6975
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
6976
+ throw new Error("Input must be a single JSON object describing one review.");
6977
+ }
6978
+ const obj = parsed;
6979
+ for (const key of Object.keys(obj)) {
6980
+ if (!ALLOWED_KEYS.has(key)) {
6981
+ throw new Error(
6982
+ `Unknown field '${key}'. Allowed: reviewer, target, verdict, findings, blocked.`
6983
+ );
6984
+ }
6985
+ }
6986
+ const reviewer = requireNonEmptyString(obj.reviewer, "reviewer");
6987
+ const target = requireNonEmptyString(obj.target, "target");
6988
+ const out = { reviewer, target };
6989
+ if (obj.verdict !== void 0) {
6990
+ if (typeof obj.verdict !== "string" || !VALID_VERDICTS.has(obj.verdict)) {
6991
+ throw new Error(`verdict must be one of pass, needs-attention, fail, got '${obj.verdict}'.`);
6992
+ }
6993
+ out.verdict = obj.verdict;
6994
+ }
6995
+ if (obj.findings !== void 0) {
6996
+ out.findings = parseFindings(obj.findings);
6997
+ }
6998
+ if (obj.blocked !== void 0) {
6999
+ out.blocked = parseBlocked(obj.blocked);
7000
+ }
7001
+ return out;
7002
+ }
7003
+ function parseFindings(value) {
7004
+ if (!Array.isArray(value)) {
7005
+ throw new Error("findings must be an array of objects.");
7006
+ }
7007
+ return value.map((item, i) => {
7008
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
7009
+ throw new Error(`findings[${i}] must be a JSON object.`);
7010
+ }
7011
+ const obj = item;
7012
+ for (const key of Object.keys(obj)) {
7013
+ if (!ALLOWED_FINDING_KEYS.has(key)) {
7014
+ throw new Error(
7015
+ `findings[${i}]: unknown field '${key}'. Allowed: title, severity, location, summary.`
7016
+ );
7017
+ }
7018
+ }
7019
+ const finding = {
7020
+ title: requireNonEmptyString(obj.title, `findings[${i}].title`)
7021
+ };
7022
+ if (obj.severity !== void 0) {
7023
+ if (typeof obj.severity !== "string" || !VALID_SEVERITIES.has(obj.severity)) {
7024
+ throw new Error(
7025
+ `findings[${i}].severity must be one of high, medium, low, got '${obj.severity}'.`
7026
+ );
7027
+ }
7028
+ finding.severity = obj.severity;
7029
+ }
7030
+ if (obj.location !== void 0) {
7031
+ finding.location = requireNonEmptyString(obj.location, `findings[${i}].location`);
7032
+ }
7033
+ if (obj.summary !== void 0) {
7034
+ finding.summary = requireNonEmptyString(obj.summary, `findings[${i}].summary`);
7035
+ }
7036
+ return finding;
7037
+ });
7038
+ }
7039
+ function parseBlocked(value) {
7040
+ if (!Array.isArray(value)) {
7041
+ throw new Error("blocked must be an array of objects.");
7042
+ }
7043
+ return value.map((item, i) => {
7044
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
7045
+ throw new Error(`blocked[${i}] must be a JSON object.`);
7046
+ }
7047
+ const obj = item;
7048
+ for (const key of Object.keys(obj)) {
7049
+ if (!ALLOWED_BLOCKED_KEYS.has(key)) {
7050
+ throw new Error(`blocked[${i}]: unknown field '${key}'. Allowed: title, reason, why.`);
7051
+ }
7052
+ }
7053
+ if (typeof obj.reason !== "string" || !VALID_BLOCK_REASONS.has(obj.reason)) {
7054
+ throw new Error(
7055
+ `blocked[${i}].reason must be one of spec-deviation, design-reversal, got '${obj.reason}'.`
7056
+ );
7057
+ }
7058
+ const blocked = {
7059
+ title: requireNonEmptyString(obj.title, `blocked[${i}].title`),
7060
+ reason: obj.reason
7061
+ };
7062
+ if (obj.why !== void 0) {
7063
+ blocked.why = requireNonEmptyString(obj.why, `blocked[${i}].why`);
7064
+ }
7065
+ return blocked;
7066
+ });
7067
+ }
7068
+ function requireNonEmptyString(value, field) {
7069
+ if (typeof value !== "string" || value.trim().length === 0) {
7070
+ throw new Error(`${field} must be a non-empty string.`);
7071
+ }
7072
+ return value;
7073
+ }
7074
+ function buildReviewRecordedEvent(input) {
7075
+ const { review } = input;
7076
+ return {
7077
+ schema_version: "0.1.0",
7078
+ id: input.eventId,
7079
+ session_id: input.sessionId,
7080
+ occurred_at: input.occurredAt,
7081
+ source: "local-cli",
7082
+ type: "review_recorded",
7083
+ reviewer: review.reviewer,
7084
+ target: review.target,
7085
+ ...review.verdict !== void 0 ? { verdict: review.verdict } : {},
7086
+ ...review.findings !== void 0 ? { findings: review.findings } : {},
7087
+ ...review.blocked !== void 0 ? { blocked: review.blocked } : {}
7088
+ };
7089
+ }
7090
+ var LABEL_FRAGMENT_MAX = 40;
7091
+ function buildReviewRecordLabel(review) {
7092
+ return `Ad-hoc review: ${truncate(review.reviewer)} -> ${truncate(review.target)}`;
7093
+ }
7094
+ function truncate(value) {
7095
+ return value.length > LABEL_FRAGMENT_MAX ? `${value.slice(0, LABEL_FRAGMENT_MAX - 3)}...` : value;
7096
+ }
7097
+
6870
7098
  // src/runtime/child-process-runner.ts
6871
7099
  import { spawn as spawn2 } from "child_process";
6872
7100
  var DEFAULT_KILL_GRACE_MS = 5e3;
@@ -7249,6 +7477,8 @@ var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
7249
7477
  var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
7250
7478
  var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
7251
7479
  var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
7480
+ var ORIENTATION_START = "<!-- BASOU:ORIENTATION:START -->";
7481
+ var ORIENTATION_END = "<!-- BASOU:ORIENTATION:END -->";
7252
7482
  var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
7253
7483
  async function readMarkdownFile(filePath) {
7254
7484
  try {
@@ -7773,8 +8003,11 @@ export {
7773
8003
  IsoTimestampSchema,
7774
8004
  JSON_SCHEMA_VERSION,
7775
8005
  ManifestSchema,
8006
+ ORIENTATION_END,
8007
+ ORIENTATION_START,
7776
8008
  PROTOCOL_END,
7777
8009
  PROTOCOL_START,
8010
+ REVIEW_RECORD_NO_INPUT_HINT,
7778
8011
  RiskLevelSchema,
7779
8012
  STOP_HOOK_TIMEOUT_SECONDS,
7780
8013
  STUCK_THRESHOLD_MS,
@@ -7803,6 +8036,8 @@ export {
7803
8036
  assertBasouRootSafe,
7804
8037
  basouPaths,
7805
8038
  buildJsonSchemas,
8039
+ buildReviewRecordLabel,
8040
+ buildReviewRecordedEvent,
7806
8041
  buildStatusSnapshot,
7807
8042
  buildStopHookCommand,
7808
8043
  chainEvents,
@@ -7812,6 +8047,7 @@ export {
7812
8047
  classifySuspect,
7813
8048
  claudeCodeAdapterMetadata,
7814
8049
  claudeTranscriptToImportPayload,
8050
+ codexAdapterMetadata,
7815
8051
  codexRolloutToImportPayload,
7816
8052
  computeWorkStats,
7817
8053
  createAdHocSessionWithEvent,
@@ -7853,6 +8089,7 @@ export {
7853
8089
  overwriteYamlFile,
7854
8090
  parseDuration,
7855
8091
  parseMarkers,
8092
+ parseReviewRecordInput,
7856
8093
  pathBasename,
7857
8094
  planArchive,
7858
8095
  planGitignore,
@@ -7885,6 +8122,7 @@ export {
7885
8122
  replayEvents,
7886
8123
  resolveBasouRepositoryRoot,
7887
8124
  resolveClaudeCodeCommand,
8125
+ resolveCodexCommand,
7888
8126
  resolveRepositoryRoot,
7889
8127
  resolveSessionId,
7890
8128
  resolveTaskId,