caveat-cli 0.16.0 → 0.16.2

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
@@ -11,11 +11,12 @@ import {
11
11
  __export,
12
12
  __toESM,
13
13
  acquireReindexLock,
14
- appendPendingReminder,
14
+ buildAndPublishPendingReminder,
15
15
  buildCodexSidecarDiagnosticsCommand,
16
16
  buildCodexSidecarReadOnlySmokeCommand,
17
17
  buildCodexSidecarRunCommand,
18
18
  buildHookSignalSidecarContextBlock,
19
+ buildPendingSemanticKey,
19
20
  caveatEntriesToSidecarContextBlocks,
20
21
  cleanupStalePendingDirs,
21
22
  communityAdd,
@@ -26,11 +27,11 @@ import {
26
27
  createKeyserverKeyProvider,
27
28
  decideCodexSidecarExecution,
28
29
  defaultSelfIdentityTokens,
29
- drainGlobalPendingReminders,
30
- drainPendingReminders,
30
+ drainPendingRemindersDetailed,
31
31
  ensureUserConfig,
32
32
  findCaveatHome,
33
- findCaveatsForPrompt,
33
+ findCaveatsForHook,
34
+ findCaveatsForHookSegments,
34
35
  get,
35
36
  hasAnyStruggleSignal,
36
37
  initOwnSync,
@@ -63,7 +64,7 @@ import {
63
64
  userPromptSubmitReminderText,
64
65
  writeDigestMarker,
65
66
  writeUserConfigPatch
66
- } from "./chunk-ZNAFNCPW.js";
67
+ } from "./chunk-3MLJQLWY.js";
67
68
 
68
69
  // ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/compile/codegen/code.js
69
70
  var require_code = __commonJS({
@@ -7075,6 +7076,7 @@ function uninstallClaudeIntegration(opts) {
7075
7076
  // src/codexHookInstall.ts
7076
7077
  import { copyFileSync as copyFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
7077
7078
  import { dirname as dirname3, join as join4 } from "node:path";
7079
+ import { parse as parseToml } from "smol-toml";
7078
7080
  function quote2(p) {
7079
7081
  return p.includes(" ") ? `"${p}"` : p;
7080
7082
  }
@@ -7154,74 +7156,183 @@ function removeHook2(hooksJson, event, command, subcommand) {
7154
7156
  return true;
7155
7157
  }
7156
7158
  function enableCodexHooksFeature(raw) {
7157
- const lines = raw.split(/\r?\n/);
7158
- const unsupportedFeatureShape = lines.find(
7159
- (line) => /^\s*(?:["']features["']|features)\s*(?:\.|=)/.test(line) || /^\s*\[\[?\s*(?:["']features["']|features\.)/.test(line)
7160
- );
7161
- if (unsupportedFeatureShape) {
7159
+ let parsed;
7160
+ try {
7161
+ parsed = parseToml(raw);
7162
+ } catch {
7162
7163
  return {
7163
7164
  status: "blocked",
7164
7165
  text: raw,
7165
7166
  changed: false,
7166
- reason: "config.toml uses a non-canonical features table shape that Caveat will not rewrite safely"
7167
+ reason: "config.toml is invalid TOML; fix it before installing Caveat hooks"
7167
7168
  };
7168
7169
  }
7169
- const featureHeaders = lines.map((line, index) => /^\s*\[features]\s*(?:#.*)?$/.test(line) ? index : -1).filter((index) => index >= 0);
7170
- if (featureHeaders.length > 1) {
7170
+ const featureValue = parsed.features;
7171
+ if (featureValue !== void 0 && !isPlainRecord(featureValue)) {
7172
+ return {
7173
+ status: "blocked",
7174
+ text: raw,
7175
+ changed: false,
7176
+ reason: "config.toml features value is not a table"
7177
+ };
7178
+ }
7179
+ const features = featureValue;
7180
+ const canonicalValue = features?.hooks;
7181
+ const legacyValue = features?.codex_hooks;
7182
+ if (canonicalValue !== void 0 && typeof canonicalValue !== "boolean") {
7183
+ return { status: "blocked", text: raw, changed: false, reason: "[features].hooks must be boolean" };
7184
+ }
7185
+ if (legacyValue !== void 0 && typeof legacyValue !== "boolean") {
7186
+ return { status: "blocked", text: raw, changed: false, reason: "[features].codex_hooks must be boolean" };
7187
+ }
7188
+ if (canonicalValue === false) {
7189
+ return { status: "blocked", text: raw, changed: false, reason: "[features].hooks is explicitly false" };
7190
+ }
7191
+ if (legacyValue === false) {
7171
7192
  return {
7172
7193
  status: "blocked",
7173
7194
  text: raw,
7174
7195
  changed: false,
7175
- reason: "config.toml contains more than one [features] table"
7196
+ reason: canonicalValue === true ? "[features].hooks conflicts with the deprecated codex_hooks alias" : "[features].codex_hooks is explicitly false"
7176
7197
  };
7177
7198
  }
7178
- const featuresStart = featureHeaders[0] ?? -1;
7179
- if (featuresStart === -1) {
7199
+ if (canonicalValue === true && legacyValue === void 0) {
7200
+ return { status: "enabled", text: raw, changed: false };
7201
+ }
7202
+ const lines = raw.split(/\r?\n/);
7203
+ const codeLines = maskTomlStringsAndComments(raw).split(/\r?\n/);
7204
+ const featureHeaders = codeLines.map((line, index) => /^\s*\[\s*features\s*]\s*$/.test(line) ? index : -1).filter((index) => index >= 0);
7205
+ if (featureValue === void 0) {
7180
7206
  const prefix = raw.trimEnd();
7181
7207
  const text = `${prefix}${prefix ? "\n\n" : ""}[features]
7182
- codex_hooks = true
7208
+ hooks = true
7183
7209
  `;
7184
7210
  return { status: "enabled", text, changed: true };
7185
7211
  }
7212
+ if (featureHeaders.length !== 1) {
7213
+ return {
7214
+ status: "blocked",
7215
+ text: raw,
7216
+ changed: false,
7217
+ reason: "config.toml uses a features table shape that Caveat will not rewrite safely"
7218
+ };
7219
+ }
7220
+ const featuresStart = featureHeaders[0];
7186
7221
  const assignments = [];
7187
7222
  for (let i = featuresStart + 1; i < lines.length; i += 1) {
7188
- if (/^\s*\[\[?.+?\]?]\s*(?:#.*)?$/.test(lines[i])) {
7223
+ if (/^\s*\[\[?/.test(codeLines[i])) {
7189
7224
  break;
7190
7225
  }
7191
- const assignment = /^\s*codex_hooks\s*=\s*([^#]*?)\s*(?:#.*)?$/.exec(lines[i]);
7226
+ const assignment = /^\s*(hooks|codex_hooks)\s*=/.exec(codeLines[i]);
7192
7227
  if (assignment) {
7193
- assignments.push({ index: i, value: assignment[1].trim() });
7194
- } else if (/^\s*["']codex_hooks["']\s*=/.test(lines[i])) {
7195
- return {
7196
- status: "blocked",
7197
- text: raw,
7198
- changed: false,
7199
- reason: "[features].codex_hooks uses a quoted key that Caveat will not rewrite safely"
7200
- };
7228
+ assignments.push({ index: i, key: assignment[1] });
7201
7229
  }
7202
7230
  }
7203
- if (assignments.length > 1) {
7231
+ const canonical = assignments.filter((assignment) => assignment.key === "hooks");
7232
+ const legacy = assignments.filter((assignment) => assignment.key === "codex_hooks");
7233
+ if (canonicalValue !== void 0 && canonical.length !== 1 || legacyValue !== void 0 && legacy.length !== 1) {
7204
7234
  return {
7205
7235
  status: "blocked",
7206
7236
  text: raw,
7207
7237
  changed: false,
7208
- reason: "[features].codex_hooks is defined more than once"
7238
+ reason: "[features].hooks uses a key shape that Caveat will not rewrite safely"
7209
7239
  };
7210
7240
  }
7211
- const existing = assignments[0];
7212
- if (existing) {
7213
- if (existing.value === "true") return { status: "enabled", text: raw, changed: false };
7214
- return {
7215
- status: "blocked",
7216
- text: raw,
7217
- changed: false,
7218
- reason: `[features].codex_hooks is explicitly ${existing.value || "set to a non-true value"}`
7219
- };
7241
+ const current = canonical[0];
7242
+ const deprecated = legacy[0];
7243
+ if (canonicalValue === true && legacyValue === true && current && deprecated) {
7244
+ const inlineComment = /(#.*)$/.exec(lines[deprecated.index])?.[1];
7245
+ lines.splice(deprecated.index, 1);
7246
+ if (inlineComment) {
7247
+ const canonicalIndex = current.index > deprecated.index ? current.index - 1 : current.index;
7248
+ lines.splice(canonicalIndex + 1, 0, inlineComment);
7249
+ }
7250
+ return { status: "enabled", text: `${lines.join("\n").trimEnd()}
7251
+ `, changed: true };
7220
7252
  }
7221
- lines.splice(featuresStart + 1, 0, "codex_hooks = true");
7253
+ if (legacyValue === true && canonicalValue === void 0 && deprecated) {
7254
+ lines[deprecated.index] = lines[deprecated.index].replace(/\bcodex_hooks\b/, "hooks");
7255
+ return { status: "enabled", text: `${lines.join("\n").trimEnd()}
7256
+ `, changed: true };
7257
+ }
7258
+ lines.splice(featuresStart + 1, 0, "hooks = true");
7222
7259
  return { status: "enabled", text: `${lines.join("\n").trimEnd()}
7223
7260
  `, changed: true };
7224
7261
  }
7262
+ function isPlainRecord(value) {
7263
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7264
+ }
7265
+ function maskTomlStringsAndComments(raw) {
7266
+ let state = "code";
7267
+ let output = "";
7268
+ for (let i = 0; i < raw.length; i += 1) {
7269
+ const char = raw[i];
7270
+ const triple = raw.slice(i, i + 3);
7271
+ if (char === "\n") {
7272
+ output += "\n";
7273
+ if (state === "comment") state = "code";
7274
+ continue;
7275
+ }
7276
+ if (state === "comment") {
7277
+ output += " ";
7278
+ continue;
7279
+ }
7280
+ if (state === "basic") {
7281
+ output += " ";
7282
+ if (char === "\\" && i + 1 < raw.length) {
7283
+ output += raw[i + 1] === "\n" ? "\n" : " ";
7284
+ i += 1;
7285
+ } else if (char === '"') state = "code";
7286
+ continue;
7287
+ }
7288
+ if (state === "literal") {
7289
+ output += " ";
7290
+ if (char === "'") state = "code";
7291
+ continue;
7292
+ }
7293
+ if (state === "multi-basic") {
7294
+ if (triple === '"""') {
7295
+ output += " ";
7296
+ i += 2;
7297
+ state = "code";
7298
+ } else {
7299
+ output += " ";
7300
+ if (char === "\\" && i + 1 < raw.length) {
7301
+ output += raw[i + 1] === "\n" ? "\n" : " ";
7302
+ i += 1;
7303
+ }
7304
+ }
7305
+ continue;
7306
+ }
7307
+ if (state === "multi-literal") {
7308
+ if (triple === "'''") {
7309
+ output += " ";
7310
+ i += 2;
7311
+ state = "code";
7312
+ } else output += " ";
7313
+ continue;
7314
+ }
7315
+ if (char === "#") {
7316
+ output += " ";
7317
+ state = "comment";
7318
+ } else if (triple === '"""') {
7319
+ output += " ";
7320
+ i += 2;
7321
+ state = "multi-basic";
7322
+ } else if (triple === "'''") {
7323
+ output += " ";
7324
+ i += 2;
7325
+ state = "multi-literal";
7326
+ } else if (char === '"') {
7327
+ output += " ";
7328
+ state = "basic";
7329
+ } else if (char === "'") {
7330
+ output += " ";
7331
+ state = "literal";
7332
+ } else output += char;
7333
+ }
7334
+ return output;
7335
+ }
7225
7336
  function writeConfigWithBackup(path, text) {
7226
7337
  const dir = dirname3(path);
7227
7338
  if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
@@ -7269,7 +7380,7 @@ function installCodexHooks(opts) {
7269
7380
  let configBackupPath;
7270
7381
  if (opts.dryRun) {
7271
7382
  opts.logger.info(`[dry-run] would update ${hooksPath}`);
7272
- opts.logger.info(`[dry-run] would ensure [features].codex_hooks = true in ${configPath}`);
7383
+ opts.logger.info(`[dry-run] would ensure [features].hooks = true in ${configPath}`);
7273
7384
  } else {
7274
7385
  if (anyHookAdded) {
7275
7386
  const backup = writeJsonWithBackup(hooksPath, hooksJson);
@@ -7434,6 +7545,16 @@ import { spawnSync as spawnSync3 } from "node:child_process";
7434
7545
  import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
7435
7546
  import { tmpdir } from "node:os";
7436
7547
  import { join as join6 } from "node:path";
7548
+ var DEFAULT_HOOK_SIDECAR_TIMEOUT_MS = 12e4;
7549
+ var MAX_HOOK_SIDECAR_TIMEOUT_MS = 24e4;
7550
+ function resolveHookSidecarTimeoutMs(raw) {
7551
+ if (raw === void 0) return DEFAULT_HOOK_SIDECAR_TIMEOUT_MS;
7552
+ const value = Number(raw);
7553
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_HOOK_SIDECAR_TIMEOUT_MS) {
7554
+ throw new Error(`CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS must be an integer from 1 to ${MAX_HOOK_SIDECAR_TIMEOUT_MS}`);
7555
+ }
7556
+ return value;
7557
+ }
7437
7558
  function compactFailureMessage(message) {
7438
7559
  const singleLine = message.replace(/\s+/g, " ").trim();
7439
7560
  if (!singleLine) return "unknown error";
@@ -7497,7 +7618,9 @@ function runCodexSidecarAdvisory(input) {
7497
7618
  const result = spawnSync3(process.execPath, args, {
7498
7619
  cwd: input.projectRoot,
7499
7620
  encoding: "utf-8",
7500
- timeout: Number(process.env.CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS ?? 12e4),
7621
+ // Keep at least one minute between the longest supported model call and
7622
+ // the pending single-flight claim TTL (five minutes).
7623
+ timeout: resolveHookSidecarTimeoutMs(process.env.CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS),
7501
7624
  maxBuffer: 10 * 1024 * 1024
7502
7625
  });
7503
7626
  if (result.error) {
@@ -7794,7 +7917,7 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }, depende
7794
7917
  if (result.feature === "blocked") {
7795
7918
  codexHookState = "skipped";
7796
7919
  ctx.logger.warn(
7797
- `${result.blockedReason}; preserving explicit consent. Set \`codex_hooks = true\` in ${join8(codexHome, "config.toml")}, then rerun \`caveat init\`.`
7920
+ `${result.blockedReason}; preserving explicit consent. Set \`hooks = true\` in ${join8(codexHome, "config.toml")}, then rerun \`caveat init\`.`
7798
7921
  );
7799
7922
  } else if (opts.dryRun) {
7800
7923
  codexHookState = "skipped";
@@ -8095,7 +8218,7 @@ function runStats(ctx) {
8095
8218
 
8096
8219
  // src/commands/serve.ts
8097
8220
  async function runServe(opts) {
8098
- const { startServer } = await import("./server-SZAVLE56.js");
8221
+ const { startServer } = await import("./server-LAJCQXS3.js");
8099
8222
  const { port, host } = startServer({ port: opts.port });
8100
8223
  process.stdout.write(`[caveat] web portal: http://${host}:${port}/
8101
8224
  `);
@@ -32560,8 +32683,10 @@ function buildContextSafely() {
32560
32683
  return null;
32561
32684
  }
32562
32685
  }
32563
- function searchCaveatsFromTextSafely(text, surface) {
32564
- if (!text) return [];
32686
+ function searchCaveatsSafely(input) {
32687
+ const inputs = Array.isArray(input) ? input : [input];
32688
+ const queryForLog = inputs.map((item) => item.surface === "user_prompt" ? item.topicText || item.failureText : item.failureText).filter(Boolean).join("\n");
32689
+ if (inputs.length === 0 || inputs.every((item) => !item.topicText && !item.failureText)) return [];
32565
32690
  let db;
32566
32691
  let caveatHome;
32567
32692
  let hits;
@@ -32570,9 +32695,10 @@ function searchCaveatsFromTextSafely(text, surface) {
32570
32695
  if (!ctx || !existsSync10(ctx.paths.dbPath)) return [];
32571
32696
  caveatHome = ctx.caveatHome;
32572
32697
  db = openDb({ path: ctx.paths.dbPath });
32573
- hits = findCaveatsForPrompt(db, text, {
32698
+ const searchOptions = {
32574
32699
  selfIdentity: defaultSelfIdentityTokens()
32575
- });
32700
+ };
32701
+ hits = inputs.length === 1 ? findCaveatsForHook(db, inputs[0], searchOptions) : findCaveatsForHookSegments(db, inputs, searchOptions);
32576
32702
  } catch (err) {
32577
32703
  const msg = err instanceof Error ? err.message : String(err);
32578
32704
  process.stderr.write(`[caveat:hook] search error: ${msg}
@@ -32589,7 +32715,7 @@ function searchCaveatsFromTextSafely(text, surface) {
32589
32715
  }
32590
32716
  } else {
32591
32717
  try {
32592
- logHookQueryMiss({ caveatHome, agent: "claude", surface, query: text });
32718
+ logHookQueryMiss({ caveatHome, agent: "claude", surface: inputs[0].surface, query: queryForLog });
32593
32719
  } catch (err) {
32594
32720
  const msg = err instanceof Error ? err.message : String(err);
32595
32721
  process.stderr.write(`[caveat:hook] query log error: ${msg}
@@ -32615,13 +32741,19 @@ function loadSignalsSafely(path) {
32615
32741
  function systemReminderOutput(text) {
32616
32742
  return `<system-reminder>${text.replace(/</g, "\u2039").replace(/>/g, "\u203A")}</system-reminder>`;
32617
32743
  }
32744
+ function claudePendingCleanupFailureText() {
32745
+ return "[caveat:hook] pending reminder cleanup failed";
32746
+ }
32618
32747
  function drainForSession(sessionId) {
32619
32748
  const ctx = buildContextSafely();
32620
32749
  if (!ctx) return [];
32621
- return [
32622
- ...drainPendingReminders(ctx.caveatHome, sessionId),
32623
- ...drainGlobalPendingReminders(ctx.caveatHome)
32624
- ];
32750
+ const local = drainPendingRemindersDetailed(ctx.caveatHome, sessionId);
32751
+ const global = drainPendingRemindersDetailed(ctx.caveatHome, "_global");
32752
+ for (const _failure of [...local.cleanupFailures, ...global.cleanupFailures]) {
32753
+ process.stderr.write(`${claudePendingCleanupFailureText()}
32754
+ `);
32755
+ }
32756
+ return [...local.reminders, ...global.reminders];
32625
32757
  }
32626
32758
  function claudeContextDedupeKey(text) {
32627
32759
  if (text.startsWith(CLAUDE_STOP_REMINDER_PREFIX)) return "claude-stop-reminder";
@@ -32640,7 +32772,7 @@ function compactClaudeContexts(contexts) {
32640
32772
  }
32641
32773
  selected.reverse();
32642
32774
  const limited = selected.slice(-CLAUDE_MAX_CONTEXT_BLOCKS);
32643
- const omitted = contexts.filter((t) => t.trim().length > 0).length - limited.length;
32775
+ const omitted = selected.length - limited.length;
32644
32776
  if (omitted > 0) {
32645
32777
  limited.push(
32646
32778
  `[caveat] pending reminder ${omitted} \u4EF6\u3092\u91CD\u8907\u307E\u305F\u306F\u4E0A\u9650\u306B\u3088\u308A\u7701\u7565\u3057\u307E\u3057\u305F\u3002`
@@ -32685,8 +32817,20 @@ function queueStopForSession(sessionId, signals, related) {
32685
32817
  if (!ctx) return;
32686
32818
  const key = stopSignalKey(signals, related);
32687
32819
  if (wasStopReminderQueued(ctx.caveatHome, sessionId, key)) return;
32820
+ let result;
32821
+ try {
32822
+ result = buildAndPublishPendingReminder(ctx.caveatHome, sessionId, buildPendingSemanticKey({
32823
+ agent: "claude",
32824
+ surface: "stop",
32825
+ refs: related,
32826
+ stopSignalDigest: key
32827
+ }), () => buildStopReminder(signals, related));
32828
+ } catch {
32829
+ process.stderr.write("[caveat:hook] pending reminder build or publish failed\n");
32830
+ return;
32831
+ }
32832
+ if (!result.ran) return;
32688
32833
  try {
32689
- appendPendingReminder(ctx.caveatHome, sessionId, buildStopReminder(signals, related));
32690
32834
  markStopReminderQueued(ctx.caveatHome, sessionId, key);
32691
32835
  } catch (err) {
32692
32836
  const msg = err instanceof Error ? err.message : String(err);
@@ -32717,6 +32861,20 @@ function extractToolResponseText(response) {
32717
32861
  }
32718
32862
  return "";
32719
32863
  }
32864
+ function toolTopicText(payload) {
32865
+ const parts = [];
32866
+ const toolName = payload.tool_name ?? payload.toolName;
32867
+ if (typeof toolName === "string") parts.push(toolName);
32868
+ const input = payload.tool_input ?? payload.toolInput;
32869
+ if (typeof input === "string") parts.push(input);
32870
+ if (input !== null && typeof input === "object" && !Array.isArray(input)) {
32871
+ const record2 = input;
32872
+ for (const key of ["command", "cmd", "query", "url"]) {
32873
+ if (typeof record2[key] === "string") parts.push(record2[key]);
32874
+ }
32875
+ }
32876
+ return parts.join("\n");
32877
+ }
32720
32878
  function isToolError(payload) {
32721
32879
  if (payload.hook_event_name === "PostToolUseFailure") return true;
32722
32880
  const resp = payload.tool_response ?? payload.toolResponse;
@@ -32740,7 +32898,7 @@ function spawnWorker(job) {
32740
32898
  workDir = mkdtempSync2(join12(root, "job-"));
32741
32899
  chmodSync(workDir, 448);
32742
32900
  workFile = join12(workDir, `${randomBytes(4).toString("hex")}.json`);
32743
- writeFileSync5(workFile, JSON.stringify({ ...job, schemaVersion: "caveat-worker-job/v1" }), { encoding: "utf-8", mode: 384, flag: "wx" });
32901
+ writeFileSync5(workFile, JSON.stringify({ ...job, schemaVersion: "caveat-worker-job/v2" }), { encoding: "utf-8", mode: 384, flag: "wx" });
32744
32902
  } catch (err) {
32745
32903
  const msg = err instanceof Error ? err.message : String(err);
32746
32904
  process.stderr.write(`[caveat:hook] worker writefile error: ${msg}
@@ -32784,15 +32942,27 @@ async function runWorker(workFile) {
32784
32942
  } catch {
32785
32943
  process.exit(0);
32786
32944
  }
32787
- if (!job.searchText || !job.sessionId) process.exit(0);
32788
- const hits = searchCaveatsFromTextSafely(job.searchText, "tool_error");
32945
+ if (!job.failureText || !job.sessionId) process.exit(0);
32946
+ const hits = searchCaveatsSafely({
32947
+ topicText: job.topicText,
32948
+ failureText: job.failureText,
32949
+ surface: "tool_error"
32950
+ });
32789
32951
  if (hits.length === 0) process.exit(0);
32790
32952
  const ctx = buildContextSafely();
32791
32953
  if (!ctx) process.exit(0);
32954
+ let result;
32792
32955
  try {
32793
- appendPendingReminder(ctx.caveatHome, job.sessionId, buildToolErrorReminder(job, hits));
32956
+ result = buildAndPublishPendingReminder(ctx.caveatHome, job.sessionId, buildPendingSemanticKey({
32957
+ agent: "claude",
32958
+ surface: "tool_error",
32959
+ refs: hits
32960
+ }), () => buildToolErrorReminder(job, hits));
32794
32961
  } catch {
32962
+ process.stderr.write("[caveat:hook] pending reminder build or publish failed\n");
32963
+ process.exit(0);
32795
32964
  }
32965
+ if (!result.ran) process.exit(0);
32796
32966
  process.exit(0);
32797
32967
  }
32798
32968
  function cleanupWorkerDir(workDir, root) {
@@ -32859,12 +33029,20 @@ function isStaleWorkerJobDir(path) {
32859
33029
  const stat = lstatSync(file2);
32860
33030
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
32861
33031
  if (!stat.isFile() || stat.isSymbolicLink() || !hasPrivateOwnership(stat, uid)) return false;
32862
- return isWorkerJob(JSON.parse(readFileSync5(file2, "utf-8")));
33032
+ return isKnownStaleWorkerJob(JSON.parse(readFileSync5(file2, "utf-8")));
32863
33033
  }
32864
33034
  function hasPrivateOwnership(stat, uid) {
32865
33035
  return process.platform === "win32" || (stat.mode & 63) === 0 && (uid === void 0 || stat.uid === uid);
32866
33036
  }
32867
33037
  function isWorkerJob(value) {
33038
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
33039
+ const job = value;
33040
+ const keys = Object.keys(job).sort();
33041
+ if (!(keys.length === 4 && keys.join(",") === "failureText,schemaVersion,sessionId,topicText") && !(keys.length === 5 && keys.join(",") === "additionalContext,failureText,schemaVersion,sessionId,topicText")) return false;
33042
+ return job.schemaVersion === "caveat-worker-job/v2" && typeof job.sessionId === "string" && typeof job.topicText === "string" && typeof job.failureText === "string" && (job.additionalContext === void 0 || job.additionalContext !== null && typeof job.additionalContext === "object" && !Array.isArray(job.additionalContext));
33043
+ }
33044
+ function isKnownStaleWorkerJob(value) {
33045
+ if (isWorkerJob(value)) return true;
32868
33046
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
32869
33047
  const job = value;
32870
33048
  const keys = Object.keys(job).sort();
@@ -32960,7 +33138,7 @@ function buildToolErrorReminder(job, hits) {
32960
33138
  const hasSidecarConfig = existsSync10(join12(projectRoot, ".codex-sidecar.yml"));
32961
33139
  if (mode === "auto" && !hasSidecarConfig) return base;
32962
33140
  const advisory = runCodexSidecarAdvisory({
32963
- searchText: job.searchText,
33141
+ searchText: job.failureText,
32964
33142
  limit: hits.length,
32965
33143
  projectRoot,
32966
33144
  prompt: [
@@ -33060,7 +33238,7 @@ async function runHook(name, arg) {
33060
33238
  const contexts = name === "stop" ? [] : drainForSession(sessionId);
33061
33239
  if (name === "user-prompt-submit") {
33062
33240
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
33063
- const hits = searchCaveatsFromTextSafely(prompt, "user_prompt");
33241
+ const hits = searchCaveatsSafely({ topicText: prompt, failureText: prompt, surface: "user_prompt" });
33064
33242
  if (hits.length > 0) {
33065
33243
  contexts.push(userPromptSubmitReminderText(hits));
33066
33244
  }
@@ -33088,7 +33266,8 @@ async function runHook(name, arg) {
33088
33266
  toolName: payload.tool_name ?? payload.toolName,
33089
33267
  failureKind
33090
33268
  });
33091
- spawnWorker({ sessionId, searchText: errText, ...additionalContext ? { additionalContext } : {} });
33269
+ const topicText = toolTopicText(payload);
33270
+ spawnWorker({ sessionId, topicText, failureText: errText, ...additionalContext ? { additionalContext } : {} });
33092
33271
  }
33093
33272
  process.exit(0);
33094
33273
  }
@@ -33121,7 +33300,11 @@ async function runHook(name, arg) {
33121
33300
  const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
33122
33301
  const signals = transcriptPath ? loadSignalsSafely(transcriptPath) : null;
33123
33302
  if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
33124
- const related = searchCaveatsFromTextSafely(struggleSearchText(signals), "stop");
33303
+ const related = searchCaveatsSafely(signals.errorSnippets.map((failureText) => ({
33304
+ topicText: "",
33305
+ failureText,
33306
+ surface: "stop"
33307
+ })));
33125
33308
  queueStopForSession(sessionId, signals, related);
33126
33309
  process.exit(0);
33127
33310
  }
@@ -33175,8 +33358,10 @@ function buildContextSafely2() {
33175
33358
  return null;
33176
33359
  }
33177
33360
  }
33178
- function searchCaveatsFromTextSafely2(text, surface) {
33179
- if (!text) return [];
33361
+ function searchCaveatsSafely2(input) {
33362
+ const inputs = Array.isArray(input) ? input : [input];
33363
+ const queryForLog = inputs.map((item) => item.surface === "user_prompt" ? item.topicText || item.failureText : item.failureText).filter(Boolean).join("\n");
33364
+ if (inputs.length === 0 || inputs.every((item) => !item.topicText && !item.failureText)) return [];
33180
33365
  let db;
33181
33366
  let caveatHome;
33182
33367
  let hits;
@@ -33185,9 +33370,10 @@ function searchCaveatsFromTextSafely2(text, surface) {
33185
33370
  if (!ctx || !existsSync11(ctx.paths.dbPath)) return [];
33186
33371
  caveatHome = ctx.caveatHome;
33187
33372
  db = openDb({ path: ctx.paths.dbPath });
33188
- hits = findCaveatsForPrompt(db, text, {
33373
+ const searchOptions = {
33189
33374
  selfIdentity: defaultSelfIdentityTokens()
33190
- });
33375
+ };
33376
+ hits = inputs.length === 1 ? findCaveatsForHook(db, inputs[0], searchOptions) : findCaveatsForHookSegments(db, inputs, searchOptions);
33191
33377
  } catch (err) {
33192
33378
  const msg = err instanceof Error ? err.message : String(err);
33193
33379
  process.stderr.write(`[caveat:codex-hook] search error: ${msg}
@@ -33204,7 +33390,7 @@ function searchCaveatsFromTextSafely2(text, surface) {
33204
33390
  }
33205
33391
  } else {
33206
33392
  try {
33207
- logHookQueryMiss({ caveatHome, agent: "codex", surface, query: text });
33393
+ logHookQueryMiss({ caveatHome, agent: "codex", surface: inputs[0].surface, query: queryForLog });
33208
33394
  } catch (err) {
33209
33395
  const msg = err instanceof Error ? err.message : String(err);
33210
33396
  process.stderr.write(`[caveat:codex-hook] query log error: ${msg}
@@ -33333,15 +33519,17 @@ function buildCodexPostToolUseWorkerJob(payload) {
33333
33519
  const responseText = extractToolResponseText2(payload.tool_response ?? payload.toolResponse);
33334
33520
  const inputText = toolInputText(payload.tool_input);
33335
33521
  const transcriptOutput = transcriptPath && toolUseId ? transcriptToolOutput(transcriptPath, toolUseId) : null;
33336
- const searchText = [inputText, responseText, transcriptOutput ?? ""].map((s) => s.trim()).filter(Boolean).join("\n");
33522
+ const topicText = inputText.trim();
33523
+ const failureText = [responseText, transcriptOutput ?? ""].map((s) => s.trim()).filter(Boolean).join("\n");
33337
33524
  const knownError = isCodexToolError(payload);
33338
33525
  if (knownError) {
33339
- return { sessionId, searchText, knownError: true, transcriptPath, toolUseId };
33526
+ return { sessionId, topicText, failureText, knownError: true, transcriptPath, toolUseId };
33340
33527
  }
33341
- if (transcriptPath && toolUseId && isShellLikeTool(payload) && searchText) {
33528
+ if (transcriptPath && toolUseId && isShellLikeTool(payload) && (topicText || failureText)) {
33342
33529
  return {
33343
33530
  sessionId,
33344
- searchText,
33531
+ topicText,
33532
+ failureText,
33345
33533
  knownError: false,
33346
33534
  allowSymptomOnly: true,
33347
33535
  transcriptPath,
@@ -33358,13 +33546,19 @@ function codexContextOutput(text, eventName = "UserPromptSubmit") {
33358
33546
  }
33359
33547
  });
33360
33548
  }
33549
+ function codexPendingCleanupFailureText() {
33550
+ return "[caveat:codex-hook] pending reminder cleanup failed";
33551
+ }
33361
33552
  function drainForSession2(sessionId) {
33362
33553
  const ctx = buildContextSafely2();
33363
33554
  if (!ctx) return [];
33364
- return [
33365
- ...drainPendingReminders(ctx.caveatHome, sessionId),
33366
- ...drainGlobalPendingReminders(ctx.caveatHome)
33367
- ];
33555
+ const local = drainPendingRemindersDetailed(ctx.caveatHome, sessionId);
33556
+ const global = drainPendingRemindersDetailed(ctx.caveatHome, "_global");
33557
+ for (const _failure of [...local.cleanupFailures, ...global.cleanupFailures]) {
33558
+ process.stderr.write(`${codexPendingCleanupFailureText()}
33559
+ `);
33560
+ }
33561
+ return [...local.reminders, ...global.reminders];
33368
33562
  }
33369
33563
  function codexContextDedupeKey(text) {
33370
33564
  if (text.startsWith(CODEX_STOP_REMINDER_PREFIX)) return "codex-stop-reminder";
@@ -33383,7 +33577,7 @@ function compactCodexContexts(contexts) {
33383
33577
  }
33384
33578
  selected.reverse();
33385
33579
  const limited = selected.slice(-CODEX_MAX_CONTEXT_BLOCKS);
33386
- const omitted = contexts.filter((t) => t.trim().length > 0).length - limited.length;
33580
+ const omitted = selected.length - limited.length;
33387
33581
  if (omitted > 0) {
33388
33582
  limited.push(`[caveat] pending reminder ${omitted} \u4EF6\u3092\u91CD\u8907\u307E\u305F\u306F\u4E0A\u9650\u306B\u3088\u308A\u7701\u7565\u3057\u307E\u3057\u305F\u3002`);
33389
33583
  }
@@ -33426,8 +33620,20 @@ function queueStopForSession2(sessionId, signals, related) {
33426
33620
  if (!ctx) return;
33427
33621
  const key = stopSignalKey2(signals, related);
33428
33622
  if (wasStopReminderQueued2(ctx.caveatHome, sessionId, key)) return;
33623
+ let result;
33624
+ try {
33625
+ result = buildAndPublishPendingReminder(ctx.caveatHome, sessionId, buildPendingSemanticKey({
33626
+ agent: "codex",
33627
+ surface: "stop",
33628
+ refs: related,
33629
+ stopSignalDigest: key
33630
+ }), () => stopReminderText(signals, related));
33631
+ } catch {
33632
+ process.stderr.write("[caveat:codex-hook] pending reminder build or publish failed\n");
33633
+ return;
33634
+ }
33635
+ if (!result.ran) return;
33429
33636
  try {
33430
- appendPendingReminder(ctx.caveatHome, sessionId, stopReminderText(signals, related));
33431
33637
  markStopReminderQueued2(ctx.caveatHome, sessionId, key);
33432
33638
  } catch (err) {
33433
33639
  const msg = err instanceof Error ? err.message : String(err);
@@ -33445,8 +33651,8 @@ async function waitForTranscriptOutput(transcriptPath, toolUseId) {
33445
33651
  return null;
33446
33652
  }
33447
33653
  async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
33448
- if (!job.searchText || !job.sessionId) return;
33449
- let searchText = job.searchText;
33654
+ if (!job.topicText && !job.failureText || !job.sessionId) return;
33655
+ let failureText = job.failureText;
33450
33656
  let knownError = job.knownError === true;
33451
33657
  if (opts.waitForTranscript && job.transcriptPath && job.toolUseId) {
33452
33658
  const transcriptOutput = await waitForTranscriptOutput(job.transcriptPath, job.toolUseId);
@@ -33456,18 +33662,30 @@ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
33456
33662
  if (exit2 === 0) return;
33457
33663
  knownError = true;
33458
33664
  }
33459
- searchText = [searchText, transcriptOutput].filter(Boolean).join("\n");
33665
+ failureText = [failureText, transcriptOutput].filter(Boolean).join("\n");
33460
33666
  }
33461
33667
  }
33462
33668
  if (!knownError && job.allowSymptomOnly !== true) return;
33463
- const hits = searchCaveatsFromTextSafely2(searchText, "tool_error");
33669
+ const hits = searchCaveatsSafely2({
33670
+ topicText: job.topicText,
33671
+ failureText,
33672
+ surface: "tool_error"
33673
+ });
33464
33674
  if (hits.length === 0) return;
33465
33675
  const ctx = buildContextSafely2();
33466
33676
  if (!ctx) return;
33677
+ let result;
33467
33678
  try {
33468
- appendPendingReminder(ctx.caveatHome, job.sessionId, toolErrorReminderText(hits));
33679
+ result = buildAndPublishPendingReminder(ctx.caveatHome, job.sessionId, buildPendingSemanticKey({
33680
+ agent: "codex",
33681
+ surface: "tool_error",
33682
+ refs: hits
33683
+ }), () => toolErrorReminderText(hits));
33469
33684
  } catch {
33685
+ process.stderr.write("[caveat:codex-hook] pending reminder build or publish failed\n");
33686
+ return;
33470
33687
  }
33688
+ if (!result.ran) return;
33471
33689
  }
33472
33690
  async function runCodexWorker(workFile) {
33473
33691
  let raw;
@@ -33492,10 +33710,11 @@ async function runCodexWorker(workFile) {
33492
33710
  function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join13(homedir3(), ".codex")) {
33493
33711
  const features = spawnSync5("codex", ["features", "list"], {
33494
33712
  encoding: "utf-8",
33495
- maxBuffer: 1024 * 1024
33713
+ maxBuffer: 1024 * 1024,
33714
+ env: codexFeatureListEnv(codexHome)
33496
33715
  });
33497
33716
  const featureOutput = [features.stdout, features.stderr].filter(Boolean).join("\n");
33498
- const hasHooks = /^codex_hooks\s+\S+\s+true\b/m.test(featureOutput);
33717
+ const hasHooks = /^hooks\s+\S+\s+true\b/m.test(featureOutput);
33499
33718
  const installation = detectCodexHookInstallation(codexHome);
33500
33719
  const result = {
33501
33720
  availability: features.error || features.status !== 0 || !hasHooks ? "unavailable" : "available",
@@ -33505,11 +33724,14 @@ function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join13(homedir3(),
33505
33724
  codexHome,
33506
33725
  hooksPath: installation.hooksPath,
33507
33726
  installedHooks: installation.hooks,
33508
- evidence: featureOutput.split("\n").find((line) => line.trim().startsWith("codex_hooks")) ?? null
33727
+ evidence: featureOutput.split("\n").find((line) => line.trim().startsWith("hooks")) ?? null
33509
33728
  };
33510
33729
  process.stdout.write(`${JSON.stringify(result, null, 2)}
33511
33730
  `);
33512
33731
  }
33732
+ function codexFeatureListEnv(codexHome, inherited = process.env) {
33733
+ return { ...inherited, CODEX_HOME: codexHome };
33734
+ }
33513
33735
  async function runCodexHook(name, arg) {
33514
33736
  if (name === "diagnostics") {
33515
33737
  runDiagnostics(arg);
@@ -33535,7 +33757,7 @@ async function runCodexHook(name, arg) {
33535
33757
  if (name === "user-prompt-submit") {
33536
33758
  const contexts = sessionId ? drainForSession2(sessionId) : [];
33537
33759
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
33538
- const hits = searchCaveatsFromTextSafely2(prompt, "user_prompt");
33760
+ const hits = searchCaveatsSafely2({ topicText: prompt, failureText: prompt, surface: "user_prompt" });
33539
33761
  if (hits.length > 0) {
33540
33762
  contexts.push(userPromptSubmitReminderText(hits));
33541
33763
  }
@@ -33580,7 +33802,11 @@ async function runCodexHook(name, arg) {
33580
33802
  const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
33581
33803
  const signals = transcriptPath ? loadSignalsSafely2(transcriptPath) : null;
33582
33804
  if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
33583
- const related = searchCaveatsFromTextSafely2(struggleSearchText(signals), "stop");
33805
+ const related = searchCaveatsSafely2(signals.errorSnippets.map((failureText) => ({
33806
+ topicText: "",
33807
+ failureText,
33808
+ surface: "stop"
33809
+ })));
33584
33810
  if (sessionId) queueStopForSession2(sessionId, signals, related);
33585
33811
  process.exit(0);
33586
33812
  }
@@ -34068,7 +34294,7 @@ program.command("hook <name> [arg]").description(
34068
34294
  await runHook(name, arg);
34069
34295
  });
34070
34296
  var codexHook = program.command("codex-hook").description("Install or run Codex hooks for Caveat");
34071
- codexHook.command("install").description("Install Caveat hooks into ~/.codex/hooks.json and enable codex_hooks").option("--dry-run", "show planned changes without writing", false).option("--codex-home <path>", "Codex home directory", process.env.CODEX_HOME ?? `${process.env.HOME}/.codex`).action((opts) => {
34297
+ codexHook.command("install").description("Install Caveat hooks into ~/.codex/hooks.json and enable hooks").option("--dry-run", "show planned changes without writing", false).option("--codex-home <path>", "Codex home directory", process.env.CODEX_HOME ?? `${process.env.HOME}/.codex`).action((opts) => {
34072
34298
  const cliScriptPath = process.argv[1];
34073
34299
  if (!cliScriptPath) {
34074
34300
  process.stderr.write("[caveat:error] cannot determine CLI script path\n");
@@ -34084,10 +34310,10 @@ codexHook.command("install").description("Install Caveat hooks into ~/.codex/hoo
34084
34310
  stdoutLogger.info(`UserPromptSubmit hook: ${result.hooks.userPromptSubmit}`);
34085
34311
  stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse}`);
34086
34312
  stdoutLogger.info(`Stop hook: ${result.hooks.stop}`);
34087
- stdoutLogger.info(`codex_hooks feature: ${result.feature}`);
34313
+ stdoutLogger.info(`hooks feature: ${result.feature}`);
34088
34314
  if (result.feature === "blocked") {
34089
34315
  stdoutLogger.warn(
34090
- `${result.blockedReason}; preserving explicit consent. Set \`codex_hooks = true\` in ${opts.codexHome}/config.toml, then rerun \`caveat codex-hook install\`.`
34316
+ `${result.blockedReason}; preserving explicit consent. Set \`hooks = true\` in ${opts.codexHome}/config.toml, then rerun \`caveat codex-hook install\`.`
34091
34317
  );
34092
34318
  }
34093
34319
  if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);