@basou/core 0.29.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,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) {
@@ -1196,6 +1239,25 @@ var NoteAddedEventSchema = BaseEventSchema.extend({
1196
1239
  // surface. Optional so pre-existing note_added events remain valid.
1197
1240
  kind: z3.enum(["note", "next_step"]).optional()
1198
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
+ });
1199
1261
  var AdapterOutputEventSchema = BaseEventSchema.extend({
1200
1262
  type: z3.literal("adapter_output"),
1201
1263
  stream: z3.enum(["stdout", "stderr"]),
@@ -1223,6 +1285,7 @@ var EventSchema = z3.discriminatedUnion("type", [
1223
1285
  TaskDeletedEventSchema,
1224
1286
  TaskArchivedEventSchema,
1225
1287
  NoteAddedEventSchema,
1288
+ ReviewRecordedEventSchema,
1226
1289
  AdapterOutputEventSchema
1227
1290
  ]);
1228
1291
 
@@ -1521,6 +1584,7 @@ var SessionStatusSchema = z4.enum([
1521
1584
  var SessionSourceKindSchema = z4.enum([
1522
1585
  "claude-code-adapter",
1523
1586
  "claude-code-import",
1587
+ "codex-adapter",
1524
1588
  "codex-import",
1525
1589
  "human",
1526
1590
  "import",
@@ -6867,6 +6931,159 @@ async function findReviewGaps(input) {
6867
6931
  };
6868
6932
  }
6869
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
+
6870
7087
  // src/runtime/child-process-runner.ts
6871
7088
  import { spawn as spawn2 } from "child_process";
6872
7089
  var DEFAULT_KILL_GRACE_MS = 5e3;
@@ -7249,6 +7466,8 @@ var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
7249
7466
  var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
7250
7467
  var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
7251
7468
  var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
7469
+ var ORIENTATION_START = "<!-- BASOU:ORIENTATION:START -->";
7470
+ var ORIENTATION_END = "<!-- BASOU:ORIENTATION:END -->";
7252
7471
  var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
7253
7472
  async function readMarkdownFile(filePath) {
7254
7473
  try {
@@ -7773,8 +7992,11 @@ export {
7773
7992
  IsoTimestampSchema,
7774
7993
  JSON_SCHEMA_VERSION,
7775
7994
  ManifestSchema,
7995
+ ORIENTATION_END,
7996
+ ORIENTATION_START,
7776
7997
  PROTOCOL_END,
7777
7998
  PROTOCOL_START,
7999
+ REVIEW_RECORD_NO_INPUT_HINT,
7778
8000
  RiskLevelSchema,
7779
8001
  STOP_HOOK_TIMEOUT_SECONDS,
7780
8002
  STUCK_THRESHOLD_MS,
@@ -7803,6 +8025,8 @@ export {
7803
8025
  assertBasouRootSafe,
7804
8026
  basouPaths,
7805
8027
  buildJsonSchemas,
8028
+ buildReviewRecordLabel,
8029
+ buildReviewRecordedEvent,
7806
8030
  buildStatusSnapshot,
7807
8031
  buildStopHookCommand,
7808
8032
  chainEvents,
@@ -7812,6 +8036,7 @@ export {
7812
8036
  classifySuspect,
7813
8037
  claudeCodeAdapterMetadata,
7814
8038
  claudeTranscriptToImportPayload,
8039
+ codexAdapterMetadata,
7815
8040
  codexRolloutToImportPayload,
7816
8041
  computeWorkStats,
7817
8042
  createAdHocSessionWithEvent,
@@ -7853,6 +8078,7 @@ export {
7853
8078
  overwriteYamlFile,
7854
8079
  parseDuration,
7855
8080
  parseMarkers,
8081
+ parseReviewRecordInput,
7856
8082
  pathBasename,
7857
8083
  planArchive,
7858
8084
  planGitignore,
@@ -7885,6 +8111,7 @@ export {
7885
8111
  replayEvents,
7886
8112
  resolveBasouRepositoryRoot,
7887
8113
  resolveClaudeCodeCommand,
8114
+ resolveCodexCommand,
7888
8115
  resolveRepositoryRoot,
7889
8116
  resolveSessionId,
7890
8117
  resolveTaskId,