@basou/core 0.28.0 → 0.30.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,17 +19,123 @@ 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
  }
23
25
 
26
+ // src/adapters/claude-code/settings-hook.ts
27
+ var STOP_HOOK_TIMEOUT_SECONDS = 20;
28
+ var BASOU_STOP_HOOK = /(?:\bbasou|(?:@basou|packages)\/cli\/dist\/index\.js['"]?)\s+hook\s+stop\b/;
29
+ function isBasouStopHookCommand(command) {
30
+ return BASOU_STOP_HOOK.test(command);
31
+ }
32
+ function shellQuote(value) {
33
+ return `'${value.replace(/'/g, "'\\''")}'`;
34
+ }
35
+ function buildStopHookCommand(options) {
36
+ const flags = [];
37
+ if (options.block === true) flags.push("--block");
38
+ if (options.requireReview === true) flags.push("--require-review");
39
+ if (options.minEdits !== void 0) flags.push(`--min-edits ${options.minEdits}`);
40
+ const suffix = flags.length > 0 ? ` ${flags.join(" ")}` : "";
41
+ return `node ${shellQuote(options.cliEntry)} hook stop${suffix} 2>/dev/null || true`;
42
+ }
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ function cloneSettings(settings) {
47
+ if (settings === void 0 || settings === null) return {};
48
+ if (!isRecord(settings)) {
49
+ throw new Error("Claude settings is not a JSON object.");
50
+ }
51
+ return structuredClone(settings);
52
+ }
53
+ function upsertStopHook(settings, command) {
54
+ const root = cloneSettings(settings);
55
+ if (root.hooks === void 0) {
56
+ root.hooks = {};
57
+ } else if (!isRecord(root.hooks)) {
58
+ throw new Error("The 'hooks' key in Claude settings is not an object.");
59
+ }
60
+ const hooks = root.hooks;
61
+ if (hooks.Stop === void 0) {
62
+ hooks.Stop = [];
63
+ } else if (!Array.isArray(hooks.Stop)) {
64
+ throw new Error("The 'hooks.Stop' key in Claude settings is not an array.");
65
+ }
66
+ const stop = hooks.Stop;
67
+ for (const group of stop) {
68
+ if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
69
+ for (const entry of group.hooks) {
70
+ if (!isRecord(entry)) continue;
71
+ if (typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
72
+ const unchanged = entry.type === "command" && entry.command === command && entry.timeout === STOP_HOOK_TIMEOUT_SECONDS;
73
+ entry.type = "command";
74
+ entry.command = command;
75
+ entry.timeout = STOP_HOOK_TIMEOUT_SECONDS;
76
+ return { settings: root, action: unchanged ? "unchanged" : "updated" };
77
+ }
78
+ }
79
+ }
80
+ stop.push({ hooks: [{ type: "command", command, timeout: STOP_HOOK_TIMEOUT_SECONDS }] });
81
+ return { settings: root, action: "installed" };
82
+ }
83
+ function removeStopHook(settings) {
84
+ const root = cloneSettings(settings);
85
+ if (!isRecord(root.hooks) || !Array.isArray(root.hooks.Stop)) {
86
+ return { settings: root, action: "absent" };
87
+ }
88
+ const hooks = root.hooks;
89
+ const stop = hooks.Stop;
90
+ let removed = false;
91
+ const newStop = [];
92
+ for (const group of stop) {
93
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
94
+ newStop.push(group);
95
+ continue;
96
+ }
97
+ const keptHooks = group.hooks.filter((entry) => {
98
+ if (isRecord(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
99
+ removed = true;
100
+ return false;
101
+ }
102
+ return true;
103
+ });
104
+ if (keptHooks.length === group.hooks.length) {
105
+ newStop.push(group);
106
+ } else if (keptHooks.length > 0) {
107
+ group.hooks = keptHooks;
108
+ newStop.push(group);
109
+ }
110
+ }
111
+ if (!removed) {
112
+ return { settings: root, action: "absent" };
113
+ }
114
+ if (newStop.length === 0) {
115
+ delete hooks.Stop;
116
+ } else {
117
+ hooks.Stop = newStop;
118
+ }
119
+ if (Object.keys(hooks).length === 0) {
120
+ delete root.hooks;
121
+ }
122
+ return { settings: root, action: "removed" };
123
+ }
124
+ function findBasouStopHookCommand(settings) {
125
+ if (!isRecord(settings) || !isRecord(settings.hooks) || !Array.isArray(settings.hooks.Stop)) {
126
+ return null;
127
+ }
128
+ for (const group of settings.hooks.Stop) {
129
+ if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
130
+ for (const entry of group.hooks) {
131
+ if (isRecord(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
132
+ return entry.command;
133
+ }
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+
24
139
  // src/adapters/claude-code/ask-user-question.ts
25
140
  function readString(value) {
26
141
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -106,6 +221,14 @@ var CAPTURE_VERB = /(?:decision\s+(?:capture|record)|note)\b/;
106
221
  var CAPTURE_COMMAND_PATTERN = new RegExp(
107
222
  `(?:^|[\\n;&|(])\\s*${CAPTURE_INVOCATION.source}\\s+${CAPTURE_VERB.source}`
108
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
+ );
109
232
  var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit"]);
110
233
  function evaluateStopHook(input) {
111
234
  const minEdits = input.minEdits ?? DEFAULT_STOP_HOOK_MIN_EDITS;
@@ -113,6 +236,7 @@ function evaluateStopHook(input) {
113
236
  return {
114
237
  kind: "silent",
115
238
  reason: "stop_hook_active",
239
+ review: { fires: false, reason: "stop_hook_active" },
116
240
  commandCount: 0,
117
241
  fileCount: 0,
118
242
  decisionPointCount: 0
@@ -121,6 +245,8 @@ function evaluateStopHook(input) {
121
245
  let commandCount = 0;
122
246
  let fileCount = 0;
123
247
  let captured = false;
248
+ let shipped = false;
249
+ let reviewed = false;
124
250
  for (const record of input.records) {
125
251
  if (readString2(record.type) !== "assistant") continue;
126
252
  for (const tool of toolUsesOf2(record)) {
@@ -130,7 +256,11 @@ function evaluateStopHook(input) {
130
256
  commandCount += 1;
131
257
  const toolInput = isObject2(tool.input) ? tool.input : void 0;
132
258
  const command = toolInput !== void 0 ? readString2(toolInput.command) : void 0;
133
- 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
+ }
134
264
  } else if (FILE_EDIT_TOOLS.has(name)) {
135
265
  fileCount += 1;
136
266
  }
@@ -138,14 +268,21 @@ function evaluateStopHook(input) {
138
268
  }
139
269
  const decisionPointCount = countUncapturedDecisionPoints(input.records);
140
270
  const counts = { commandCount, fileCount, decisionPointCount };
271
+ const review = evaluateReviewGate({ shipped, reviewed, fileCount, minEdits });
141
272
  if (captured) {
142
- return { kind: "silent", reason: "already_captured", ...counts };
273
+ return { kind: "silent", reason: "already_captured", review, ...counts };
143
274
  }
144
275
  const substantive = fileCount >= minEdits || decisionPointCount > 0;
145
276
  if (!substantive) {
146
- return { kind: "silent", reason: "not_substantive", ...counts };
277
+ return { kind: "silent", reason: "not_substantive", review, ...counts };
147
278
  }
148
- 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() };
149
286
  }
150
287
  function renderNudge(counts) {
151
288
  const did = [];
@@ -168,6 +305,14 @@ function renderNudge(counts) {
168
305
  "If nothing is worth capturing, just stop \u2014 do not invent decisions."
169
306
  ].join("\n");
170
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
+ }
171
316
  function readString2(value) {
172
317
  return typeof value === "string" && value.length > 0 ? value : void 0;
173
318
  }
@@ -494,6 +639,16 @@ function toolUses(record) {
494
639
  return result;
495
640
  }
496
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
+
497
652
  // src/adapters/codex/rollout-importer.ts
498
653
  var CODEX_IMPORT_SOURCE = "codex-import";
499
654
  function codexRolloutToImportPayload(records, options) {
@@ -1084,6 +1239,25 @@ var NoteAddedEventSchema = BaseEventSchema.extend({
1084
1239
  // surface. Optional so pre-existing note_added events remain valid.
1085
1240
  kind: z3.enum(["note", "next_step"]).optional()
1086
1241
  });
1242
+ var ReviewFindingSchema = z3.object({
1243
+ title: z3.string().min(1),
1244
+ severity: z3.enum(["high", "medium", "low"]).optional(),
1245
+ location: z3.string().min(1).optional(),
1246
+ summary: z3.string().min(1).optional()
1247
+ });
1248
+ var ReviewBlockedSchema = z3.object({
1249
+ title: z3.string().min(1),
1250
+ reason: z3.enum(["spec-deviation", "design-reversal"]),
1251
+ why: z3.string().min(1).optional()
1252
+ });
1253
+ var ReviewRecordedEventSchema = BaseEventSchema.extend({
1254
+ type: z3.literal("review_recorded"),
1255
+ reviewer: z3.string().min(1),
1256
+ target: z3.string().min(1),
1257
+ verdict: z3.enum(["pass", "needs-attention", "fail"]).optional(),
1258
+ findings: z3.array(ReviewFindingSchema).optional(),
1259
+ blocked: z3.array(ReviewBlockedSchema).optional()
1260
+ });
1087
1261
  var AdapterOutputEventSchema = BaseEventSchema.extend({
1088
1262
  type: z3.literal("adapter_output"),
1089
1263
  stream: z3.enum(["stdout", "stderr"]),
@@ -1111,6 +1285,7 @@ var EventSchema = z3.discriminatedUnion("type", [
1111
1285
  TaskDeletedEventSchema,
1112
1286
  TaskArchivedEventSchema,
1113
1287
  NoteAddedEventSchema,
1288
+ ReviewRecordedEventSchema,
1114
1289
  AdapterOutputEventSchema
1115
1290
  ]);
1116
1291
 
@@ -1409,6 +1584,7 @@ var SessionStatusSchema = z4.enum([
1409
1584
  var SessionSourceKindSchema = z4.enum([
1410
1585
  "claude-code-adapter",
1411
1586
  "claude-code-import",
1587
+ "codex-adapter",
1412
1588
  "codex-import",
1413
1589
  "human",
1414
1590
  "import",
@@ -6755,6 +6931,159 @@ async function findReviewGaps(input) {
6755
6931
  };
6756
6932
  }
6757
6933
 
6934
+ // src/review/review-record.ts
6935
+ var VALID_VERDICTS = /* @__PURE__ */ new Set(["pass", "needs-attention", "fail"]);
6936
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
6937
+ var VALID_BLOCK_REASONS = /* @__PURE__ */ new Set(["spec-deviation", "design-reversal"]);
6938
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
6939
+ "reviewer",
6940
+ "target",
6941
+ "verdict",
6942
+ "findings",
6943
+ "blocked"
6944
+ ]);
6945
+ var ALLOWED_FINDING_KEYS = /* @__PURE__ */ new Set([
6946
+ "title",
6947
+ "severity",
6948
+ "location",
6949
+ "summary"
6950
+ ]);
6951
+ var ALLOWED_BLOCKED_KEYS = /* @__PURE__ */ new Set(["title", "reason", "why"]);
6952
+ var REVIEW_RECORD_NO_INPUT_HINT = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
6953
+ function parseReviewRecordInput(raw) {
6954
+ if (raw.trim().length === 0) {
6955
+ throw new Error(REVIEW_RECORD_NO_INPUT_HINT);
6956
+ }
6957
+ let parsed;
6958
+ try {
6959
+ parsed = JSON.parse(raw);
6960
+ } catch (error) {
6961
+ const detail = error instanceof Error ? error.message : String(error);
6962
+ throw new Error(`Input is not valid JSON: ${detail}`);
6963
+ }
6964
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
6965
+ throw new Error("Input must be a single JSON object describing one review.");
6966
+ }
6967
+ const obj = parsed;
6968
+ for (const key of Object.keys(obj)) {
6969
+ if (!ALLOWED_KEYS.has(key)) {
6970
+ throw new Error(
6971
+ `Unknown field '${key}'. Allowed: reviewer, target, verdict, findings, blocked.`
6972
+ );
6973
+ }
6974
+ }
6975
+ const reviewer = requireNonEmptyString(obj.reviewer, "reviewer");
6976
+ const target = requireNonEmptyString(obj.target, "target");
6977
+ const out = { reviewer, target };
6978
+ if (obj.verdict !== void 0) {
6979
+ if (typeof obj.verdict !== "string" || !VALID_VERDICTS.has(obj.verdict)) {
6980
+ throw new Error(`verdict must be one of pass, needs-attention, fail, got '${obj.verdict}'.`);
6981
+ }
6982
+ out.verdict = obj.verdict;
6983
+ }
6984
+ if (obj.findings !== void 0) {
6985
+ out.findings = parseFindings(obj.findings);
6986
+ }
6987
+ if (obj.blocked !== void 0) {
6988
+ out.blocked = parseBlocked(obj.blocked);
6989
+ }
6990
+ return out;
6991
+ }
6992
+ function parseFindings(value) {
6993
+ if (!Array.isArray(value)) {
6994
+ throw new Error("findings must be an array of objects.");
6995
+ }
6996
+ return value.map((item, i) => {
6997
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
6998
+ throw new Error(`findings[${i}] must be a JSON object.`);
6999
+ }
7000
+ const obj = item;
7001
+ for (const key of Object.keys(obj)) {
7002
+ if (!ALLOWED_FINDING_KEYS.has(key)) {
7003
+ throw new Error(
7004
+ `findings[${i}]: unknown field '${key}'. Allowed: title, severity, location, summary.`
7005
+ );
7006
+ }
7007
+ }
7008
+ const finding = {
7009
+ title: requireNonEmptyString(obj.title, `findings[${i}].title`)
7010
+ };
7011
+ if (obj.severity !== void 0) {
7012
+ if (typeof obj.severity !== "string" || !VALID_SEVERITIES.has(obj.severity)) {
7013
+ throw new Error(
7014
+ `findings[${i}].severity must be one of high, medium, low, got '${obj.severity}'.`
7015
+ );
7016
+ }
7017
+ finding.severity = obj.severity;
7018
+ }
7019
+ if (obj.location !== void 0) {
7020
+ finding.location = requireNonEmptyString(obj.location, `findings[${i}].location`);
7021
+ }
7022
+ if (obj.summary !== void 0) {
7023
+ finding.summary = requireNonEmptyString(obj.summary, `findings[${i}].summary`);
7024
+ }
7025
+ return finding;
7026
+ });
7027
+ }
7028
+ function parseBlocked(value) {
7029
+ if (!Array.isArray(value)) {
7030
+ throw new Error("blocked must be an array of objects.");
7031
+ }
7032
+ return value.map((item, i) => {
7033
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
7034
+ throw new Error(`blocked[${i}] must be a JSON object.`);
7035
+ }
7036
+ const obj = item;
7037
+ for (const key of Object.keys(obj)) {
7038
+ if (!ALLOWED_BLOCKED_KEYS.has(key)) {
7039
+ throw new Error(`blocked[${i}]: unknown field '${key}'. Allowed: title, reason, why.`);
7040
+ }
7041
+ }
7042
+ if (typeof obj.reason !== "string" || !VALID_BLOCK_REASONS.has(obj.reason)) {
7043
+ throw new Error(
7044
+ `blocked[${i}].reason must be one of spec-deviation, design-reversal, got '${obj.reason}'.`
7045
+ );
7046
+ }
7047
+ const blocked = {
7048
+ title: requireNonEmptyString(obj.title, `blocked[${i}].title`),
7049
+ reason: obj.reason
7050
+ };
7051
+ if (obj.why !== void 0) {
7052
+ blocked.why = requireNonEmptyString(obj.why, `blocked[${i}].why`);
7053
+ }
7054
+ return blocked;
7055
+ });
7056
+ }
7057
+ function requireNonEmptyString(value, field) {
7058
+ if (typeof value !== "string" || value.trim().length === 0) {
7059
+ throw new Error(`${field} must be a non-empty string.`);
7060
+ }
7061
+ return value;
7062
+ }
7063
+ function buildReviewRecordedEvent(input) {
7064
+ const { review } = input;
7065
+ return {
7066
+ schema_version: "0.1.0",
7067
+ id: input.eventId,
7068
+ session_id: input.sessionId,
7069
+ occurred_at: input.occurredAt,
7070
+ source: "local-cli",
7071
+ type: "review_recorded",
7072
+ reviewer: review.reviewer,
7073
+ target: review.target,
7074
+ ...review.verdict !== void 0 ? { verdict: review.verdict } : {},
7075
+ ...review.findings !== void 0 ? { findings: review.findings } : {},
7076
+ ...review.blocked !== void 0 ? { blocked: review.blocked } : {}
7077
+ };
7078
+ }
7079
+ var LABEL_FRAGMENT_MAX = 40;
7080
+ function buildReviewRecordLabel(review) {
7081
+ return `Ad-hoc review: ${truncate(review.reviewer)} -> ${truncate(review.target)}`;
7082
+ }
7083
+ function truncate(value) {
7084
+ return value.length > LABEL_FRAGMENT_MAX ? `${value.slice(0, LABEL_FRAGMENT_MAX - 3)}...` : value;
7085
+ }
7086
+
6758
7087
  // src/runtime/child-process-runner.ts
6759
7088
  import { spawn as spawn2 } from "child_process";
6760
7089
  var DEFAULT_KILL_GRACE_MS = 5e3;
@@ -7137,6 +7466,8 @@ var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
7137
7466
  var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
7138
7467
  var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
7139
7468
  var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
7469
+ var ORIENTATION_START = "<!-- BASOU:ORIENTATION:START -->";
7470
+ var ORIENTATION_END = "<!-- BASOU:ORIENTATION:END -->";
7140
7471
  var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
7141
7472
  async function readMarkdownFile(filePath) {
7142
7473
  try {
@@ -7661,9 +7992,13 @@ export {
7661
7992
  IsoTimestampSchema,
7662
7993
  JSON_SCHEMA_VERSION,
7663
7994
  ManifestSchema,
7995
+ ORIENTATION_END,
7996
+ ORIENTATION_START,
7664
7997
  PROTOCOL_END,
7665
7998
  PROTOCOL_START,
7999
+ REVIEW_RECORD_NO_INPUT_HINT,
7666
8000
  RiskLevelSchema,
8001
+ STOP_HOOK_TIMEOUT_SECONDS,
7667
8002
  STUCK_THRESHOLD_MS,
7668
8003
  SchemaVersionSchema,
7669
8004
  SessionIdSchema,
@@ -7690,7 +8025,10 @@ export {
7690
8025
  assertBasouRootSafe,
7691
8026
  basouPaths,
7692
8027
  buildJsonSchemas,
8028
+ buildReviewRecordLabel,
8029
+ buildReviewRecordedEvent,
7693
8030
  buildStatusSnapshot,
8031
+ buildStopHookCommand,
7694
8032
  chainEvents,
7695
8033
  chainRawJsonLines,
7696
8034
  classifyFilesBySourceRoot,
@@ -7698,6 +8036,7 @@ export {
7698
8036
  classifySuspect,
7699
8037
  claudeCodeAdapterMetadata,
7700
8038
  claudeTranscriptToImportPayload,
8039
+ codexAdapterMetadata,
7701
8040
  codexRolloutToImportPayload,
7702
8041
  computeWorkStats,
7703
8042
  createAdHocSessionWithEvent,
@@ -7712,6 +8051,7 @@ export {
7712
8051
  enumerateTaskIds,
7713
8052
  evaluateStopHook,
7714
8053
  finalizeSessionYaml,
8054
+ findBasouStopHookCommand,
7715
8055
  findErrorCode,
7716
8056
  findReviewGaps,
7717
8057
  formatDurationMs,
@@ -7721,6 +8061,7 @@ export {
7721
8061
  importSessionFromJson,
7722
8062
  inspectChainTail,
7723
8063
  instructionMode,
8064
+ isBasouStopHookCommand,
7724
8065
  isGitNotFound,
7725
8066
  isImportDerivedSource,
7726
8067
  isLazyExpired,
@@ -7737,6 +8078,7 @@ export {
7737
8078
  overwriteYamlFile,
7738
8079
  parseDuration,
7739
8080
  parseMarkers,
8081
+ parseReviewRecordInput,
7740
8082
  pathBasename,
7741
8083
  planArchive,
7742
8084
  planGitignore,
@@ -7759,6 +8101,7 @@ export {
7759
8101
  refreshTaskLinkedSessions,
7760
8102
  reimportPreservingId,
7761
8103
  removeMarkerSection,
8104
+ removeStopHook,
7762
8105
  renderDecisions,
7763
8106
  renderHandoff,
7764
8107
  renderOrientation,
@@ -7768,6 +8111,7 @@ export {
7768
8111
  replayEvents,
7769
8112
  resolveBasouRepositoryRoot,
7770
8113
  resolveClaudeCodeCommand,
8114
+ resolveCodexCommand,
7771
8115
  resolveRepositoryRoot,
7772
8116
  resolveSessionId,
7773
8117
  resolveTaskId,
@@ -7788,6 +8132,7 @@ export {
7788
8132
  ulid,
7789
8133
  unknownManifestKeys,
7790
8134
  updateTaskStatusWithEvent,
8135
+ upsertStopHook,
7791
8136
  verifyEventsChain,
7792
8137
  writeEventsBulk,
7793
8138
  writeManifest,