caveat-cli 0.15.0 → 0.16.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
@@ -2,7 +2,11 @@
2
2
  import { createRequire as __caveat_createRequire } from 'node:module';
3
3
  const require = __caveat_createRequire(import.meta.url);
4
4
  import {
5
+ AUTO_SYNC_DEBOUNCE_MS,
6
+ CAVEAT_AUTO_SYNC_ENV,
5
7
  KNOWLEDGE_GITIGNORE,
8
+ PublishScanError,
9
+ SyncError,
6
10
  __commonJS,
7
11
  __export,
8
12
  __toESM,
@@ -11,6 +15,7 @@ import {
11
15
  buildCodexSidecarDiagnosticsCommand,
12
16
  buildCodexSidecarReadOnlySmokeCommand,
13
17
  buildCodexSidecarRunCommand,
18
+ buildHookSignalSidecarContextBlock,
14
19
  caveatEntriesToSidecarContextBlocks,
15
20
  cleanupStalePendingDirs,
16
21
  communityAdd,
@@ -18,8 +23,10 @@ import {
18
23
  communityPull,
19
24
  communityRemove,
20
25
  computeEntriesDigest,
26
+ createKeyserverKeyProvider,
21
27
  decideCodexSidecarExecution,
22
28
  defaultSelfIdentityTokens,
29
+ drainGlobalPendingReminders,
23
30
  drainPendingReminders,
24
31
  ensureUserConfig,
25
32
  findCaveatHome,
@@ -30,9 +37,11 @@ import {
30
37
  listRecent,
31
38
  listStale,
32
39
  loadConfig,
40
+ logHookQueryMiss,
33
41
  markHit,
34
42
  maybeSweepPendingDirs,
35
43
  openDb,
44
+ prewarmSealedKeys,
36
45
  publishOwn,
37
46
  readCodexSessionSignals,
38
47
  readDigestMarker,
@@ -41,8 +50,9 @@ import {
41
50
  recordEntry,
42
51
  reindexAllSources,
43
52
  releaseReindexLock,
53
+ resetAutoSyncFailureState,
44
54
  resolvePaths,
45
- scanSource,
55
+ runAutoSync,
46
56
  search,
47
57
  stderrLogger,
48
58
  stopReminderText,
@@ -53,7 +63,7 @@ import {
53
63
  userPromptSubmitReminderText,
54
64
  writeDigestMarker,
55
65
  writeUserConfigPatch
56
- } from "./chunk-Z2XLYCR3.js";
66
+ } from "./chunk-ZNAFNCPW.js";
57
67
 
58
68
  // ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/compile/codegen/code.js
59
69
  var require_code = __commonJS({
@@ -432,11 +442,11 @@ var require_codegen = __commonJS({
432
442
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
433
443
  return `${varKind} ${this.name}${rhs};` + _n;
434
444
  }
435
- optimizeNames(names, constants) {
445
+ optimizeNames(names, constants2) {
436
446
  if (!names[this.name.str])
437
447
  return;
438
448
  if (this.rhs)
439
- this.rhs = optimizeExpr(this.rhs, names, constants);
449
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
440
450
  return this;
441
451
  }
442
452
  get names() {
@@ -453,10 +463,10 @@ var require_codegen = __commonJS({
453
463
  render({ _n }) {
454
464
  return `${this.lhs} = ${this.rhs};` + _n;
455
465
  }
456
- optimizeNames(names, constants) {
466
+ optimizeNames(names, constants2) {
457
467
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
458
468
  return;
459
- this.rhs = optimizeExpr(this.rhs, names, constants);
469
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
460
470
  return this;
461
471
  }
462
472
  get names() {
@@ -517,8 +527,8 @@ var require_codegen = __commonJS({
517
527
  optimizeNodes() {
518
528
  return `${this.code}` ? this : void 0;
519
529
  }
520
- optimizeNames(names, constants) {
521
- this.code = optimizeExpr(this.code, names, constants);
530
+ optimizeNames(names, constants2) {
531
+ this.code = optimizeExpr(this.code, names, constants2);
522
532
  return this;
523
533
  }
524
534
  get names() {
@@ -547,12 +557,12 @@ var require_codegen = __commonJS({
547
557
  }
548
558
  return nodes.length > 0 ? this : void 0;
549
559
  }
550
- optimizeNames(names, constants) {
560
+ optimizeNames(names, constants2) {
551
561
  const { nodes } = this;
552
562
  let i = nodes.length;
553
563
  while (i--) {
554
564
  const n = nodes[i];
555
- if (n.optimizeNames(names, constants))
565
+ if (n.optimizeNames(names, constants2))
556
566
  continue;
557
567
  subtractNames(names, n.names);
558
568
  nodes.splice(i, 1);
@@ -605,12 +615,12 @@ var require_codegen = __commonJS({
605
615
  return void 0;
606
616
  return this;
607
617
  }
608
- optimizeNames(names, constants) {
618
+ optimizeNames(names, constants2) {
609
619
  var _a3;
610
- this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
611
- if (!(super.optimizeNames(names, constants) || this.else))
620
+ this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants2);
621
+ if (!(super.optimizeNames(names, constants2) || this.else))
612
622
  return;
613
- this.condition = optimizeExpr(this.condition, names, constants);
623
+ this.condition = optimizeExpr(this.condition, names, constants2);
614
624
  return this;
615
625
  }
616
626
  get names() {
@@ -633,10 +643,10 @@ var require_codegen = __commonJS({
633
643
  render(opts) {
634
644
  return `for(${this.iteration})` + super.render(opts);
635
645
  }
636
- optimizeNames(names, constants) {
637
- if (!super.optimizeNames(names, constants))
646
+ optimizeNames(names, constants2) {
647
+ if (!super.optimizeNames(names, constants2))
638
648
  return;
639
- this.iteration = optimizeExpr(this.iteration, names, constants);
649
+ this.iteration = optimizeExpr(this.iteration, names, constants2);
640
650
  return this;
641
651
  }
642
652
  get names() {
@@ -672,10 +682,10 @@ var require_codegen = __commonJS({
672
682
  render(opts) {
673
683
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
674
684
  }
675
- optimizeNames(names, constants) {
676
- if (!super.optimizeNames(names, constants))
685
+ optimizeNames(names, constants2) {
686
+ if (!super.optimizeNames(names, constants2))
677
687
  return;
678
- this.iterable = optimizeExpr(this.iterable, names, constants);
688
+ this.iterable = optimizeExpr(this.iterable, names, constants2);
679
689
  return this;
680
690
  }
681
691
  get names() {
@@ -717,11 +727,11 @@ var require_codegen = __commonJS({
717
727
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
718
728
  return this;
719
729
  }
720
- optimizeNames(names, constants) {
730
+ optimizeNames(names, constants2) {
721
731
  var _a3, _b;
722
- super.optimizeNames(names, constants);
723
- (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
724
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants);
732
+ super.optimizeNames(names, constants2);
733
+ (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants2);
734
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants2);
725
735
  return this;
726
736
  }
727
737
  get names() {
@@ -1022,7 +1032,7 @@ var require_codegen = __commonJS({
1022
1032
  function addExprNames(names, from) {
1023
1033
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
1024
1034
  }
1025
- function optimizeExpr(expr, names, constants) {
1035
+ function optimizeExpr(expr, names, constants2) {
1026
1036
  if (expr instanceof code_1.Name)
1027
1037
  return replaceName(expr);
1028
1038
  if (!canOptimize(expr))
@@ -1037,14 +1047,14 @@ var require_codegen = __commonJS({
1037
1047
  return items;
1038
1048
  }, []));
1039
1049
  function replaceName(n) {
1040
- const c = constants[n.str];
1050
+ const c = constants2[n.str];
1041
1051
  if (c === void 0 || names[n.str] !== 1)
1042
1052
  return n;
1043
1053
  delete names[n.str];
1044
1054
  return c;
1045
1055
  }
1046
1056
  function canOptimize(e) {
1047
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== void 0);
1057
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants2[c.str] !== void 0);
1048
1058
  }
1049
1059
  }
1050
1060
  function subtractNames(names, from) {
@@ -6865,8 +6875,9 @@ var stdoutLogger = {
6865
6875
  };
6866
6876
 
6867
6877
  // src/commands/init.ts
6868
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, renameSync, rmdirSync, writeFileSync as writeFileSync2 } from "node:fs";
6869
- import { dirname as dirname3, join as join5 } from "node:path";
6878
+ import { spawnSync as spawnSync4 } from "node:child_process";
6879
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, renameSync, rmdirSync, writeFileSync as writeFileSync4 } from "node:fs";
6880
+ import { dirname as dirname4, join as join8 } from "node:path";
6870
6881
 
6871
6882
  // src/claudeInstall.ts
6872
6883
  import { spawnSync } from "node:child_process";
@@ -7061,9 +7072,328 @@ function uninstallClaudeIntegration(opts) {
7061
7072
  };
7062
7073
  }
7063
7074
 
7075
+ // src/codexHookInstall.ts
7076
+ import { copyFileSync as copyFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
7077
+ import { dirname as dirname3, join as join4 } from "node:path";
7078
+ function quote2(p) {
7079
+ return p.includes(" ") ? `"${p}"` : p;
7080
+ }
7081
+ function hookCommand2(nodePath, cliScriptPath, event) {
7082
+ return `${quote2(nodePath)} ${quote2(cliScriptPath)} codex-hook ${event}`;
7083
+ }
7084
+ function eventCommandFragment(event) {
7085
+ return `codex-hook ${event}`;
7086
+ }
7087
+ function isSameHookCommand2(actual, expected) {
7088
+ return actual === expected || actual.endsWith(` ${expected}`);
7089
+ }
7090
+ function isCaveatCodexHookCommand(actual, event) {
7091
+ const lower = actual.toLowerCase();
7092
+ return lower.includes("caveat") && actual.includes(eventCommandFragment(event));
7093
+ }
7094
+ function hasCaveatHook(hooksJson, event, fragment) {
7095
+ return hooksJson.hooks?.[event]?.some(
7096
+ (entry) => entry.hooks?.some((h) => h.command.includes(fragment))
7097
+ ) ?? false;
7098
+ }
7099
+ function readHooks(path) {
7100
+ if (!existsSync2(path)) return {};
7101
+ return JSON.parse(readFileSync3(path, "utf-8"));
7102
+ }
7103
+ function writeJsonWithBackup(path, value) {
7104
+ const dir = dirname3(path);
7105
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
7106
+ let backupPath = "";
7107
+ if (existsSync2(path)) {
7108
+ backupPath = `${path}.caveat-backup-${Date.now()}`;
7109
+ copyFileSync2(path, backupPath);
7110
+ }
7111
+ writeFileSync2(path, `${JSON.stringify(value, null, 2)}
7112
+ `, "utf-8");
7113
+ return backupPath;
7114
+ }
7115
+ function upsertHook2(hooksJson, event, command, subcommand) {
7116
+ hooksJson.hooks ??= {};
7117
+ const list = hooksJson.hooks[event] ??= [];
7118
+ for (const entry of list) {
7119
+ for (const hook of entry.hooks ?? []) {
7120
+ if (isSameHookCommand2(hook.command, command)) return "unchanged";
7121
+ if (isCaveatCodexHookCommand(hook.command, subcommand)) {
7122
+ hook.command = command;
7123
+ hook.timeoutSec = 5;
7124
+ hook.async = false;
7125
+ hook.statusMessage = null;
7126
+ return "added";
7127
+ }
7128
+ }
7129
+ }
7130
+ list.push({
7131
+ hooks: [
7132
+ {
7133
+ type: "command",
7134
+ command,
7135
+ timeoutSec: 5,
7136
+ async: false,
7137
+ statusMessage: null
7138
+ }
7139
+ ]
7140
+ });
7141
+ return "added";
7142
+ }
7143
+ function removeHook2(hooksJson, event, command, subcommand) {
7144
+ const list = hooksJson.hooks?.[event];
7145
+ if (!list) return false;
7146
+ const before = list.length;
7147
+ const filtered = list.filter(
7148
+ (entry) => !entry.hooks?.some(
7149
+ (h) => isSameHookCommand2(h.command, command) || isCaveatCodexHookCommand(h.command, subcommand)
7150
+ )
7151
+ );
7152
+ if (filtered.length === before) return false;
7153
+ hooksJson.hooks[event] = filtered;
7154
+ return true;
7155
+ }
7156
+ 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) {
7162
+ return {
7163
+ status: "blocked",
7164
+ text: raw,
7165
+ changed: false,
7166
+ reason: "config.toml uses a non-canonical features table shape that Caveat will not rewrite safely"
7167
+ };
7168
+ }
7169
+ const featureHeaders = lines.map((line, index) => /^\s*\[features]\s*(?:#.*)?$/.test(line) ? index : -1).filter((index) => index >= 0);
7170
+ if (featureHeaders.length > 1) {
7171
+ return {
7172
+ status: "blocked",
7173
+ text: raw,
7174
+ changed: false,
7175
+ reason: "config.toml contains more than one [features] table"
7176
+ };
7177
+ }
7178
+ const featuresStart = featureHeaders[0] ?? -1;
7179
+ if (featuresStart === -1) {
7180
+ const prefix = raw.trimEnd();
7181
+ const text = `${prefix}${prefix ? "\n\n" : ""}[features]
7182
+ codex_hooks = true
7183
+ `;
7184
+ return { status: "enabled", text, changed: true };
7185
+ }
7186
+ const assignments = [];
7187
+ for (let i = featuresStart + 1; i < lines.length; i += 1) {
7188
+ if (/^\s*\[\[?.+?\]?]\s*(?:#.*)?$/.test(lines[i])) {
7189
+ break;
7190
+ }
7191
+ const assignment = /^\s*codex_hooks\s*=\s*([^#]*?)\s*(?:#.*)?$/.exec(lines[i]);
7192
+ 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
+ };
7201
+ }
7202
+ }
7203
+ if (assignments.length > 1) {
7204
+ return {
7205
+ status: "blocked",
7206
+ text: raw,
7207
+ changed: false,
7208
+ reason: "[features].codex_hooks is defined more than once"
7209
+ };
7210
+ }
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
+ };
7220
+ }
7221
+ lines.splice(featuresStart + 1, 0, "codex_hooks = true");
7222
+ return { status: "enabled", text: `${lines.join("\n").trimEnd()}
7223
+ `, changed: true };
7224
+ }
7225
+ function writeConfigWithBackup(path, text) {
7226
+ const dir = dirname3(path);
7227
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
7228
+ let backupPath = "";
7229
+ if (existsSync2(path)) {
7230
+ backupPath = `${path}.caveat-backup-${Date.now()}`;
7231
+ copyFileSync2(path, backupPath);
7232
+ }
7233
+ writeFileSync2(path, text, "utf-8");
7234
+ return backupPath;
7235
+ }
7236
+ function installCodexHooks(opts) {
7237
+ const hooksPath = join4(opts.codexHome, "hooks.json");
7238
+ const configPath = join4(opts.codexHome, "config.toml");
7239
+ const rawConfig = existsSync2(configPath) ? readFileSync3(configPath, "utf-8") : "";
7240
+ const enabled = enableCodexHooksFeature(rawConfig);
7241
+ if (enabled.status === "blocked") {
7242
+ return {
7243
+ hooks: { userPromptSubmit: "skipped", postToolUse: "skipped", stop: "skipped" },
7244
+ feature: "blocked",
7245
+ blockedReason: enabled.reason
7246
+ };
7247
+ }
7248
+ const hooksJson = readHooks(hooksPath);
7249
+ const userPromptSubmit = upsertHook2(
7250
+ hooksJson,
7251
+ "UserPromptSubmit",
7252
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
7253
+ "user-prompt-submit"
7254
+ );
7255
+ const postToolUse = upsertHook2(
7256
+ hooksJson,
7257
+ "PostToolUse",
7258
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
7259
+ "post-tool-use"
7260
+ );
7261
+ const stop = upsertHook2(
7262
+ hooksJson,
7263
+ "Stop",
7264
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
7265
+ "stop"
7266
+ );
7267
+ const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
7268
+ let backupPath;
7269
+ let configBackupPath;
7270
+ if (opts.dryRun) {
7271
+ opts.logger.info(`[dry-run] would update ${hooksPath}`);
7272
+ opts.logger.info(`[dry-run] would ensure [features].codex_hooks = true in ${configPath}`);
7273
+ } else {
7274
+ if (anyHookAdded) {
7275
+ const backup = writeJsonWithBackup(hooksPath, hooksJson);
7276
+ if (backup) backupPath = backup;
7277
+ }
7278
+ if (enabled.changed) {
7279
+ const backup = writeConfigWithBackup(configPath, enabled.text);
7280
+ if (backup) configBackupPath = backup;
7281
+ }
7282
+ }
7283
+ return {
7284
+ hooks: { userPromptSubmit, postToolUse, stop },
7285
+ feature: enabled.changed ? "enabled" : "unchanged",
7286
+ backupPath,
7287
+ configBackupPath
7288
+ };
7289
+ }
7290
+ function uninstallCodexHooks(opts) {
7291
+ const hooksPath = join4(opts.codexHome, "hooks.json");
7292
+ const hooksJson = readHooks(hooksPath);
7293
+ const userPromptSubmitRemoved = removeHook2(
7294
+ hooksJson,
7295
+ "UserPromptSubmit",
7296
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
7297
+ "user-prompt-submit"
7298
+ );
7299
+ const postToolUseRemoved = removeHook2(
7300
+ hooksJson,
7301
+ "PostToolUse",
7302
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
7303
+ "post-tool-use"
7304
+ );
7305
+ const stopRemoved = removeHook2(
7306
+ hooksJson,
7307
+ "Stop",
7308
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
7309
+ "stop"
7310
+ );
7311
+ let backupPath;
7312
+ if (opts.dryRun) {
7313
+ opts.logger.info(`[dry-run] would remove Caveat Codex hooks from ${hooksPath}`);
7314
+ } else if (userPromptSubmitRemoved || postToolUseRemoved || stopRemoved) {
7315
+ const backup = writeJsonWithBackup(hooksPath, hooksJson);
7316
+ if (backup) backupPath = backup;
7317
+ }
7318
+ return {
7319
+ hooks: {
7320
+ userPromptSubmit: userPromptSubmitRemoved ? "added" : "unchanged",
7321
+ postToolUse: postToolUseRemoved ? "added" : "unchanged",
7322
+ stop: stopRemoved ? "added" : "unchanged"
7323
+ },
7324
+ feature: "unchanged",
7325
+ backupPath
7326
+ };
7327
+ }
7328
+ function detectCodexHookInstallation(codexHome) {
7329
+ const hooksPath = join4(codexHome, "hooks.json");
7330
+ const hooksJson = readHooks(hooksPath);
7331
+ const hooks = {
7332
+ userPromptSubmit: hasCaveatHook(
7333
+ hooksJson,
7334
+ "UserPromptSubmit",
7335
+ eventCommandFragment("user-prompt-submit")
7336
+ ),
7337
+ postToolUse: hasCaveatHook(hooksJson, "PostToolUse", eventCommandFragment("post-tool-use")),
7338
+ stop: hasCaveatHook(hooksJson, "Stop", eventCommandFragment("stop"))
7339
+ };
7340
+ const count = Object.values(hooks).filter(Boolean).length;
7341
+ return {
7342
+ installation: count === 0 ? "not-installed" : count === 3 ? "installed" : "partial",
7343
+ hooksPath,
7344
+ hooks
7345
+ };
7346
+ }
7347
+
7348
+ // src/ghSetup.ts
7349
+ import { spawnSync as spawnSync2 } from "node:child_process";
7350
+ import { readSync } from "node:fs";
7351
+ function runGh(args) {
7352
+ const result = spawnSync2("gh", args, { encoding: "utf-8" });
7353
+ return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "", stderr: typeof result.stderr === "string" ? result.stderr : "" };
7354
+ }
7355
+ function commandError(result, fallback) {
7356
+ return new Error((result.stderr || result.stdout || fallback).trim());
7357
+ }
7358
+ function defaultGitHubRepoUrl(opts) {
7359
+ const version2 = opts.ghRunner(["--version"]);
7360
+ if (version2.status !== 0) {
7361
+ throw new Error(
7362
+ `gh is not available. create a ${opts.visibility} repository named ${opts.repositoryName} on GitHub, then run:
7363
+ ${opts.retryCommand}
7364
+ # or install gh and retry`
7365
+ );
7366
+ }
7367
+ const user = opts.ghRunner(["api", "user", "-q", ".login"]);
7368
+ if (user.status !== 0 || !user.stdout.trim()) {
7369
+ throw new Error(`gh is not authenticated; run \`gh auth login\`, then retry \`${opts.retryCommand}\``);
7370
+ }
7371
+ const login = user.stdout.trim();
7372
+ const qualifiedName = `${login}/${opts.repositoryName}`;
7373
+ const view = opts.ghRunner(["repo", "view", qualifiedName]);
7374
+ if (view.status !== 0) {
7375
+ const question = `create ${opts.visibility} repo github.com/${qualifiedName}? [y/N]`;
7376
+ if (!opts.yes) {
7377
+ if (!opts.isTty()) {
7378
+ throw new Error(`${question}; rerun with --yes to approve non-interactively`);
7379
+ }
7380
+ if (!opts.confirm(question)) throw new Error(`${opts.visibility} repository creation cancelled`);
7381
+ }
7382
+ const create = opts.ghRunner(["repo", "create", opts.repositoryName, `--${opts.visibility}`]);
7383
+ if (create.status !== 0) throw commandError(create, "gh repo create failed");
7384
+ }
7385
+ return `https://github.com/${qualifiedName}.git`;
7386
+ }
7387
+ function askOnce(question) {
7388
+ process.stdout.write(`${question} `);
7389
+ const buf = Buffer.alloc(256);
7390
+ const bytes = readSync(0, buf, 0, buf.length, null);
7391
+ return /^(?:y|yes)$/i.test(buf.toString("utf-8", 0, bytes).trim());
7392
+ }
7393
+
7064
7394
  // src/nodePath.ts
7065
- import { existsSync as existsSync2, realpathSync } from "node:fs";
7066
- import { delimiter, join as join4 } from "node:path";
7395
+ import { existsSync as existsSync3, realpathSync } from "node:fs";
7396
+ import { delimiter, join as join5 } from "node:path";
7067
7397
  var defaultRealpath = (path) => realpathSync.native(path);
7068
7398
  function safeRealpath(path, realpath = defaultRealpath) {
7069
7399
  try {
@@ -7076,7 +7406,7 @@ function resolveHookNodePath({
7076
7406
  env = process.env,
7077
7407
  execPath = process.execPath,
7078
7408
  platform = process.platform,
7079
- exists = existsSync2,
7409
+ exists = existsSync3,
7080
7410
  realpath = defaultRealpath
7081
7411
  } = {}) {
7082
7412
  const execRealpath = safeRealpath(execPath, realpath);
@@ -7084,7 +7414,7 @@ function resolveHookNodePath({
7084
7414
  const names = platform === "win32" ? ["node.exe", "node.cmd", "node.bat", "node"] : ["node"];
7085
7415
  for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
7086
7416
  for (const name of names) {
7087
- const candidate = join4(dir, name);
7417
+ const candidate = join5(dir, name);
7088
7418
  if (!exists(candidate)) continue;
7089
7419
  const candidateRealpath = safeRealpath(candidate, realpath);
7090
7420
  if (execRealpath && candidateRealpath && candidateRealpath === execRealpath) {
@@ -7095,29 +7425,241 @@ function resolveHookNodePath({
7095
7425
  return execPath;
7096
7426
  }
7097
7427
 
7428
+ // src/commands/publish.ts
7429
+ import { existsSync as existsSync4 } from "node:fs";
7430
+ import { join as join7 } from "node:path";
7431
+
7432
+ // src/commands/codexSidecarAdvisory.ts
7433
+ import { spawnSync as spawnSync3 } from "node:child_process";
7434
+ import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
7435
+ import { tmpdir } from "node:os";
7436
+ import { join as join6 } from "node:path";
7437
+ function compactFailureMessage(message) {
7438
+ const singleLine = message.replace(/\s+/g, " ").trim();
7439
+ if (!singleLine) return "unknown error";
7440
+ return singleLine.length > 220 ? `${singleLine.slice(0, 220)}...` : singleLine;
7441
+ }
7442
+ function formatCodexSidecarAdvisoryUnavailable(error51) {
7443
+ const message = error51 instanceof Error ? error51.message : String(error51);
7444
+ return `[caveat:codex-sidecar] advisory unavailable: ${compactFailureMessage(message)}`;
7445
+ }
7446
+ function sidecarFailureDetail(output) {
7447
+ const protocol = output.match(/PROTOCOL_ERROR:[^"\r\n]+/);
7448
+ if (protocol) return compactFailureMessage(protocol[0]);
7449
+ const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
7450
+ const diagnostic = lines.find(
7451
+ (line) => !line.startsWith("[caveat] [codex-sidecar] codex-sidecar ")
7452
+ );
7453
+ return compactFailureMessage(diagnostic ?? output);
7454
+ }
7455
+ function runCodexSidecarAdvisory(input) {
7456
+ const cliScript = process.argv[1];
7457
+ if (!cliScript) {
7458
+ return { status: "failed", message: "current Caveat CLI script path is unavailable" };
7459
+ }
7460
+ const args = [
7461
+ ...process.execArgv,
7462
+ "--disable-warning=ExperimentalWarning",
7463
+ cliScript,
7464
+ "codex-sidecar",
7465
+ "run",
7466
+ "explore",
7467
+ input.prompt,
7468
+ "--preset",
7469
+ "advisory",
7470
+ "--query",
7471
+ input.searchText,
7472
+ "--limit",
7473
+ String(Math.max(1, input.limit)),
7474
+ "--host-agent",
7475
+ "claude",
7476
+ "--availability",
7477
+ "operational"
7478
+ ];
7479
+ let resultDir;
7480
+ let advisory;
7481
+ try {
7482
+ resultDir = mkdtempSync(join6(tmpdir(), "caveat-codex-advisory-"));
7483
+ const resultFile = join6(resultDir, "result.json");
7484
+ args.push("--save-result", resultFile);
7485
+ if (input.additionalContext) {
7486
+ const additionalContextFile = join6(resultDir, "hook-signal.json");
7487
+ writeFileSync3(additionalContextFile, JSON.stringify({ context: [input.additionalContext] }) + "\n", { encoding: "utf-8", mode: 384, flag: "wx" });
7488
+ args.push("--additional-context-file", additionalContextFile);
7489
+ }
7490
+ const nodeCli = process.env.CAVEAT_CODEX_SIDECAR_NODE_CLI;
7491
+ if (nodeCli) {
7492
+ args.push("--node-cli", nodeCli);
7493
+ } else {
7494
+ const command = process.env.CAVEAT_CODEX_SIDECAR_COMMAND;
7495
+ if (command) args.push("--command", command);
7496
+ }
7497
+ const result = spawnSync3(process.execPath, args, {
7498
+ cwd: input.projectRoot,
7499
+ encoding: "utf-8",
7500
+ timeout: Number(process.env.CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS ?? 12e4),
7501
+ maxBuffer: 10 * 1024 * 1024
7502
+ });
7503
+ if (result.error) {
7504
+ advisory = { status: "failed", message: compactFailureMessage(result.error.message) };
7505
+ } else if (result.status !== 0) {
7506
+ const detail = (result.stderr || result.stdout || `exit ${result.status}`).trim();
7507
+ advisory = {
7508
+ status: "failed",
7509
+ message: `sidecar command failed: ${sidecarFailureDetail(detail)}`
7510
+ };
7511
+ } else {
7512
+ const parsed = JSON.parse(readFileSync4(resultFile, "utf-8"));
7513
+ if (parsed.status !== "ok" || typeof parsed.summary !== "string") {
7514
+ advisory = {
7515
+ status: "failed",
7516
+ message: compactFailureMessage(`unexpected SidecarResult status: ${String(parsed.status)}`)
7517
+ };
7518
+ } else {
7519
+ advisory = {
7520
+ status: "ok",
7521
+ summary: parsed.summary,
7522
+ ...typeof parsed.rawEventLogRef === "string" ? { rawEventLogRef: parsed.rawEventLogRef } : {}
7523
+ };
7524
+ }
7525
+ }
7526
+ } catch (err) {
7527
+ const msg = err instanceof Error ? err.message : String(err);
7528
+ advisory = {
7529
+ status: "failed",
7530
+ message: compactFailureMessage(`invalid SidecarResult JSON: ${msg}`)
7531
+ };
7532
+ } finally {
7533
+ if (resultDir) {
7534
+ try {
7535
+ rmSync(resultDir, { recursive: true });
7536
+ } catch (err) {
7537
+ const msg = err instanceof Error ? err.message : String(err);
7538
+ advisory = {
7539
+ status: "failed",
7540
+ message: compactFailureMessage(`sidecar advisory cleanup failed: ${msg}`)
7541
+ };
7542
+ }
7543
+ }
7544
+ }
7545
+ return advisory;
7546
+ }
7547
+ function formatCodexSidecarAdvisory(advisory) {
7548
+ if (advisory.status === "ok") {
7549
+ return [
7550
+ "[caveat:codex-sidecar] Codex advisory:",
7551
+ advisory.summary,
7552
+ advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
7553
+ ].filter(Boolean).join("\n");
7554
+ }
7555
+ return `[caveat:codex-sidecar] advisory unavailable: ${advisory.message}`;
7556
+ }
7557
+
7558
+ // src/commands/publish.ts
7559
+ var HTTPS_TARGET_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+?(?:\.git)?\/?$/;
7560
+ var SCP_TARGET_RE = /^git@github\.com:[^/]+\/[^/]+?(?:\.git)?$/;
7561
+ function validatePublishTarget(url2) {
7562
+ if (!HTTPS_TARGET_RE.test(url2) && !SCP_TARGET_RE.test(url2)) throw new Error("invalid publish target: use https://github.com/<org>/<repo>(.git) or git@github.com:<org>/<repo>(.git)");
7563
+ return url2;
7564
+ }
7565
+ async function defaultPublicRepoUrl(opts, deps) {
7566
+ return defaultGitHubRepoUrl({
7567
+ repositoryName: "Caveat-Public",
7568
+ visibility: "public",
7569
+ yes: opts.yes,
7570
+ retryCommand: "caveat publish --init <url>",
7571
+ ...deps
7572
+ });
7573
+ }
7574
+ async function runPublish(ctx, opts, dependencies = {}) {
7575
+ try {
7576
+ const ghRunner = dependencies.ghRunner ?? runGh;
7577
+ const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
7578
+ const confirm = dependencies.confirm ?? askOnce;
7579
+ const projectRoot = process.cwd();
7580
+ const hasCodexSidecarConfig = dependencies.hasCodexSidecarConfig ?? ((root) => existsSync4(join7(root, ".codex-sidecar.yml")));
7581
+ const publishAdvisory = hasCodexSidecarConfig(projectRoot) ? (changes) => {
7582
+ try {
7583
+ return dependencies.runCodexSidecarAdvisory?.(changes, projectRoot) ?? formatCodexSidecarAdvisory(runCodexSidecarAdvisory({
7584
+ searchText: changes.lines.join("\n"),
7585
+ limit: Math.max(changes.lines.length, 1),
7586
+ projectRoot,
7587
+ prompt: "A public Caveat publish is pending. Use Caveat context to give concise advice about privacy or secret risk. Advisory only: do not authorize or block this publish."
7588
+ }));
7589
+ } catch (err) {
7590
+ return formatCodexSidecarAdvisoryUnavailable(err);
7591
+ }
7592
+ } : void 0;
7593
+ let target = ctx.config.publishTarget;
7594
+ if (opts.init !== void 0) {
7595
+ target = validatePublishTarget(opts.init);
7596
+ writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
7597
+ }
7598
+ if (!target) {
7599
+ target = await defaultPublicRepoUrl(opts, { ghRunner, isTty, confirm });
7600
+ writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
7601
+ }
7602
+ const result = await publishOwn({
7603
+ paths: ctx.paths,
7604
+ config: { ...ctx.config, publishTarget: target },
7605
+ logger: ctx.logger,
7606
+ confirmImpl: confirm,
7607
+ isTty,
7608
+ dryRun: opts.dryRun,
7609
+ yes: opts.yes,
7610
+ allow: opts.allow,
7611
+ saveAllow: opts.save,
7612
+ advisory: publishAdvisory
7613
+ });
7614
+ if (result.dryRun) ctx.logger.info(`[dry-run] ${result.fileCount} public entry(ies) ready to publish`);
7615
+ else if (result.changed) ctx.logger.info(`published ${result.fileCount} public entry(ies)`);
7616
+ } catch (err) {
7617
+ if (err instanceof PublishScanError) {
7618
+ for (const finding of err.findings) {
7619
+ ctx.logger.error(`${finding.relPath}:${finding.line} ${finding.rule} ${finding.excerpt}`);
7620
+ ctx.logger.error(` --allow ${finding.matchDigest}`);
7621
+ }
7622
+ process.exitCode = 1;
7623
+ return;
7624
+ }
7625
+ ctx.logger.error(err instanceof Error ? err.message : String(err));
7626
+ process.exitCode = 1;
7627
+ }
7628
+ }
7629
+
7098
7630
  // src/commands/init.ts
7099
- async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
7631
+ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }, dependencies = {}) {
7632
+ const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
7633
+ const confirm = dependencies.confirm ?? askOnce;
7634
+ const ghRunner = dependencies.ghRunner ?? runGh;
7635
+ let publishTarget = ctx.config.publishTarget;
7636
+ let codexHookState = "not-installed";
7100
7637
  ensureUserConfig(ctx.userConfigPath);
7101
7638
  ctx.logger.info(`user config: ${ctx.userConfigPath}`);
7102
- if (!existsSync3(ctx.paths.knowledgeRepo)) {
7103
- mkdirSync2(ctx.paths.knowledgeRepo, { recursive: true });
7104
- mkdirSync2(ctx.paths.entriesDir, { recursive: true });
7639
+ if (!existsSync5(ctx.paths.knowledgeRepo)) {
7640
+ mkdirSync3(ctx.paths.knowledgeRepo, { recursive: true });
7641
+ mkdirSync3(ctx.paths.entriesDir, { recursive: true });
7105
7642
  ctx.logger.info(`knowledge repo scaffolded: ${ctx.paths.knowledgeRepo}`);
7106
7643
  } else {
7107
7644
  ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
7108
7645
  }
7109
7646
  migrateLegacyCommunityDir(ctx);
7110
- const gitignorePath = join5(ctx.paths.knowledgeRepo, ".gitignore");
7111
- if (!existsSync3(gitignorePath)) {
7112
- writeFileSync2(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
7647
+ const gitignorePath = join8(ctx.paths.knowledgeRepo, ".gitignore");
7648
+ if (!existsSync5(gitignorePath)) {
7649
+ writeFileSync4(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
7113
7650
  ctx.logger.info(`.gitignore created: ${gitignorePath}`);
7114
7651
  }
7115
7652
  if (!opts.dryRun) {
7116
- const dbDir = dirname3(ctx.paths.dbPath);
7117
- if (!existsSync3(dbDir)) mkdirSync2(dbDir, { recursive: true });
7653
+ const dbDir = dirname4(ctx.paths.dbPath);
7654
+ if (!existsSync5(dbDir)) mkdirSync3(dbDir, { recursive: true });
7655
+ const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
7656
+ const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
7657
+ for (const failure of failures) {
7658
+ ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage(failure.error)}`);
7659
+ }
7118
7660
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
7119
7661
  try {
7120
- reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger });
7662
+ reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
7121
7663
  writeDigestMarker(ctx.caveatHome, computeEntriesDigest(ctx.paths));
7122
7664
  } finally {
7123
7665
  db.close();
@@ -7140,39 +7682,179 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
7140
7682
  ctx.logger.info(
7141
7683
  "tip: subscribe to a group repo with `caveat community add <github-url>`, then `caveat pull`."
7142
7684
  );
7685
+ let syncRequested = opts.sync !== void 0 && opts.sync !== false;
7686
+ let syncNudgeAccepted = false;
7687
+ if (opts.sync === void 0 && isTty()) {
7688
+ syncNudgeAccepted = confirm("private \u540C\u671F\u3092\u4ECA\u3059\u3050\u8A2D\u5B9A\u3059\u308B\uFF1F [y/N]");
7689
+ syncRequested = syncNudgeAccepted;
7690
+ }
7691
+ if (syncRequested) {
7692
+ try {
7693
+ if (opts.dryRun) {
7694
+ const target = typeof opts.sync === "string" ? opts.sync : "<user>/Caveat-Private";
7695
+ ctx.logger.info(`[dry-run] would initialize private sync: ${target}`);
7696
+ } else {
7697
+ const url2 = typeof opts.sync === "string" ? opts.sync : defaultGitHubRepoUrl({
7698
+ repositoryName: "Caveat-Private",
7699
+ visibility: "private",
7700
+ yes: (opts.yes ?? false) || syncNudgeAccepted,
7701
+ retryCommand: "caveat init --sync <url>",
7702
+ ghRunner,
7703
+ isTty,
7704
+ confirm
7705
+ });
7706
+ const result = await initOwnSync({
7707
+ ownDir: ctx.paths.knowledgeRepo,
7708
+ url: url2,
7709
+ caveatHome: ctx.caveatHome,
7710
+ paths: ctx.paths,
7711
+ logger: ctx.logger,
7712
+ // file: remotes are governed by local filesystem permissions and have
7713
+ // no anonymous HTTP surface for the remote-visibility probe to inspect.
7714
+ trustRemotePrivate: isFileRemote(url2)
7715
+ });
7716
+ ctx.logger.info(
7717
+ result.remoteWasEmpty ? `initialized private sync remote: ${url2}` : `checked out existing private remote: ${url2} (branch ${result.branch})`
7718
+ );
7719
+ }
7720
+ } catch (err) {
7721
+ if (err instanceof SyncError && err.code === "OWN_REPO_EXISTS") {
7722
+ ctx.logger.info("private sync already configured (own repo exists); skipped");
7723
+ } else {
7724
+ ctx.logger.warn(`private sync setup skipped: ${errorMessage(err)}`);
7725
+ }
7726
+ }
7727
+ }
7728
+ let publishRequested = opts.publishTarget !== void 0 && opts.publishTarget !== false;
7729
+ let publishNudgeAccepted = false;
7730
+ if (opts.publishTarget === void 0 && isTty()) {
7731
+ publishNudgeAccepted = confirm("\u516C\u958B repo\uFF08\u5C01\u7DD8\u30DF\u30E9\u30FC\uFF09\u3082\u8A2D\u5B9A\u3059\u308B\uFF1F [y/N]");
7732
+ publishRequested = publishNudgeAccepted;
7733
+ }
7734
+ if (publishRequested) {
7735
+ try {
7736
+ const requested = typeof opts.publishTarget === "string" ? validatePublishTarget(opts.publishTarget) : publishTarget;
7737
+ const target = requested ?? (opts.dryRun ? "<user>/Caveat-Public" : defaultGitHubRepoUrl({
7738
+ repositoryName: "Caveat-Public",
7739
+ visibility: "public",
7740
+ yes: (opts.yes ?? false) || publishNudgeAccepted,
7741
+ retryCommand: "caveat init --publish-target <url>",
7742
+ ghRunner,
7743
+ isTty,
7744
+ confirm
7745
+ }));
7746
+ if (opts.dryRun) {
7747
+ ctx.logger.info(`[dry-run] would configure publish target: ${target}`);
7748
+ publishTarget = target;
7749
+ } else if (target === publishTarget) {
7750
+ ctx.logger.info(`publish target already configured: ${target}`);
7751
+ } else {
7752
+ writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
7753
+ publishTarget = target;
7754
+ ctx.logger.info(`publish target configured: ${target}`);
7755
+ }
7756
+ } catch (err) {
7757
+ ctx.logger.warn(`publish target setup skipped: ${errorMessage(err)}`);
7758
+ }
7759
+ }
7143
7760
  if (opts.skipClaude) {
7144
7761
  ctx.logger.info("Claude Code integration skipped (--skip-claude)");
7145
- return;
7762
+ } else {
7763
+ const cliScriptPath = process.argv[1];
7764
+ if (!cliScriptPath) {
7765
+ ctx.logger.warn("cannot determine CLI script path; skipping Claude integration");
7766
+ } else {
7767
+ const result = installClaudeIntegration({
7768
+ claudeDir: join8(ctx.userHome, ".claude"),
7769
+ cliScriptPath,
7770
+ nodePath: resolveHookNodePath(),
7771
+ dryRun: opts.dryRun,
7772
+ logger: ctx.logger
7773
+ });
7774
+ reportInstallResult(ctx, result, opts.dryRun);
7775
+ }
7146
7776
  }
7147
- const cliScriptPath = process.argv[1];
7148
- if (!cliScriptPath) {
7149
- ctx.logger.warn("cannot determine CLI script path; skipping Claude integration");
7150
- return;
7777
+ if (opts.skipCodexHook) {
7778
+ codexHookState = "skipped";
7779
+ ctx.logger.info("Codex hook integration skipped (--skip-codex-hook)");
7780
+ } else if ((dependencies.codexAvailable ?? detectCodexAvailability)()) {
7781
+ const cliScriptPath = process.argv[1];
7782
+ if (!cliScriptPath) {
7783
+ codexHookState = "skipped";
7784
+ ctx.logger.warn("cannot determine CLI script path; skipping Codex hook integration");
7785
+ } else {
7786
+ const codexHome = join8(ctx.userHome, ".codex");
7787
+ const result = installCodexHooks({
7788
+ codexHome,
7789
+ cliScriptPath,
7790
+ nodePath: resolveHookNodePath(),
7791
+ dryRun: opts.dryRun,
7792
+ logger: ctx.logger
7793
+ });
7794
+ if (result.feature === "blocked") {
7795
+ codexHookState = "skipped";
7796
+ ctx.logger.warn(
7797
+ `${result.blockedReason}; preserving explicit consent. Set \`codex_hooks = true\` in ${join8(codexHome, "config.toml")}, then rerun \`caveat init\`.`
7798
+ );
7799
+ } else if (opts.dryRun) {
7800
+ codexHookState = "skipped";
7801
+ ctx.logger.info("[dry-run] would install Codex hooks");
7802
+ } else {
7803
+ codexHookState = detectCodexHookInstallation(codexHome).installation;
7804
+ }
7805
+ }
7151
7806
  }
7152
- const result = installClaudeIntegration({
7153
- claudeDir: join5(ctx.userHome, ".claude"),
7154
- cliScriptPath,
7155
- nodePath: resolveHookNodePath(),
7156
- dryRun: opts.dryRun,
7157
- logger: ctx.logger
7807
+ reportEnvironmentSummary(ctx, publishTarget, codexHookState, opts.dryRun);
7808
+ }
7809
+ function detectCodexAvailability() {
7810
+ const result = spawnSync4("codex --version", {
7811
+ shell: true,
7812
+ encoding: "utf-8",
7813
+ stdio: "ignore"
7158
7814
  });
7159
- reportInstallResult(ctx, result, opts.dryRun);
7815
+ return !result.error && result.status === 0;
7816
+ }
7817
+ function isFileRemote(url2) {
7818
+ try {
7819
+ return new URL(url2).protocol === "file:";
7820
+ } catch {
7821
+ return false;
7822
+ }
7823
+ }
7824
+ function gitOutput(args) {
7825
+ const result = spawnSync4("git", args, { encoding: "utf-8" });
7826
+ return result.status === 0 && typeof result.stdout === "string" ? result.stdout.trim() : null;
7827
+ }
7828
+ function reportEnvironmentSummary(ctx, publishTarget, codexHookState, dryRun) {
7829
+ const prefix = dryRun ? "[dry-run] would have " : "";
7830
+ const isRepo = gitOutput(["-C", ctx.paths.knowledgeRepo, "rev-parse", "--is-inside-work-tree"]) === "true";
7831
+ const remote = isRepo ? gitOutput(["-C", ctx.paths.knowledgeRepo, "config", "--get", "remote.origin.url"]) : null;
7832
+ const communityCount = existsSync5(ctx.paths.communityDir) ? readdirSync(ctx.paths.communityDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length : 0;
7833
+ ctx.logger.info(`${prefix}environment summary:`);
7834
+ ctx.logger.info(` own git: ${isRepo ? "repository" : "not initialized"}`);
7835
+ ctx.logger.info(` private remote: ${remote || "not configured"}`);
7836
+ ctx.logger.info(` publish target: ${publishTarget || "not configured"}`);
7837
+ ctx.logger.info(` community sources: ${communityCount}`);
7838
+ ctx.logger.info(` codex hook: ${codexHookState}`);
7839
+ }
7840
+ function errorMessage(err) {
7841
+ return err instanceof Error ? err.message : String(err);
7160
7842
  }
7161
7843
  function migrateLegacyCommunityDir(ctx) {
7162
- const legacy = join5(ctx.paths.knowledgeRepo, "community");
7844
+ const legacy = join8(ctx.paths.knowledgeRepo, "community");
7163
7845
  const current = ctx.paths.communityDir;
7164
7846
  if (legacy === current) return;
7165
- if (!existsSync3(legacy)) return;
7166
- if (existsSync3(current)) {
7847
+ if (!existsSync5(legacy)) return;
7848
+ if (existsSync5(current)) {
7167
7849
  ctx.logger.warn(
7168
7850
  `legacy community dir still exists at ${legacy} \u2014 remove manually (new location in use)`
7169
7851
  );
7170
7852
  return;
7171
7853
  }
7172
- mkdirSync2(current, { recursive: true });
7854
+ mkdirSync3(current, { recursive: true });
7173
7855
  for (const entry of readdirSync(legacy, { withFileTypes: true })) {
7174
7856
  if (!entry.isDirectory()) continue;
7175
- renameSync(join5(legacy, entry.name), join5(current, entry.name));
7857
+ renameSync(join8(legacy, entry.name), join8(current, entry.name));
7176
7858
  }
7177
7859
  try {
7178
7860
  rmdirSync(legacy);
@@ -7187,7 +7869,7 @@ function runUninstall(ctx, opts) {
7187
7869
  process.exit(1);
7188
7870
  }
7189
7871
  const result = uninstallClaudeIntegration({
7190
- claudeDir: join5(ctx.userHome, ".claude"),
7872
+ claudeDir: join8(ctx.userHome, ".claude"),
7191
7873
  cliScriptPath,
7192
7874
  nodePath: resolveHookNodePath(),
7193
7875
  dryRun: opts.dryRun,
@@ -7234,18 +7916,23 @@ function reportInstallResult(ctx, result, dryRun) {
7234
7916
  }
7235
7917
 
7236
7918
  // src/commands/indexCmd.ts
7237
- import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
7238
- import { dirname as dirname4 } from "node:path";
7239
- function runIndex(ctx, opts) {
7240
- const dbDir = dirname4(ctx.paths.dbPath);
7241
- if (!existsSync4(dbDir)) mkdirSync3(dbDir, { recursive: true });
7919
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "node:fs";
7920
+ import { dirname as dirname5 } from "node:path";
7921
+ async function runIndex(ctx, opts) {
7922
+ const dbDir = dirname5(ctx.paths.dbPath);
7923
+ if (!existsSync6(dbDir)) mkdirSync4(dbDir, { recursive: true });
7924
+ const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
7925
+ const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
7926
+ for (const failure of failures) {
7927
+ ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage2(failure.error)}`);
7928
+ }
7242
7929
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
7243
7930
  try {
7244
7931
  if (opts.full) {
7245
7932
  ctx.logger.info("full rebuild: DELETE FROM entries");
7246
7933
  rebuildAll(db);
7247
7934
  }
7248
- const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger });
7935
+ const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
7249
7936
  for (const [source, scan] of Object.entries(result.perSource)) {
7250
7937
  if (source === "own") {
7251
7938
  ctx.logger.info(`own: +${scan.added} ~${scan.updated} -${scan.deleted}`);
@@ -7258,6 +7945,9 @@ function runIndex(ctx, opts) {
7258
7945
  db.close();
7259
7946
  }
7260
7947
  }
7948
+ function errorMessage2(err) {
7949
+ return err instanceof Error ? err.message : String(err);
7950
+ }
7261
7951
 
7262
7952
  // src/commands/search.ts
7263
7953
  function runSearch(ctx, opts) {
@@ -7405,7 +8095,7 @@ function runStats(ctx) {
7405
8095
 
7406
8096
  // src/commands/serve.ts
7407
8097
  async function runServe(opts) {
7408
- const { startServer } = await import("./server-DCE3ODHI.js");
8098
+ const { startServer } = await import("./server-SZAVLE56.js");
7409
8099
  const { port, host } = startServer({ port: opts.port });
7410
8100
  process.stdout.write(`[caveat] web portal: http://${host}:${port}/
7411
8101
  `);
@@ -7784,15 +8474,15 @@ var makeIssue = (params) => {
7784
8474
  message: issueData.message
7785
8475
  };
7786
8476
  }
7787
- let errorMessage = "";
8477
+ let errorMessage6 = "";
7788
8478
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
7789
8479
  for (const map2 of maps) {
7790
- errorMessage = map2(fullIssue, { data, defaultError: errorMessage }).message;
8480
+ errorMessage6 = map2(fullIssue, { data, defaultError: errorMessage6 }).message;
7791
8481
  }
7792
8482
  return {
7793
8483
  ...issueData,
7794
8484
  path: fullPath,
7795
- message: errorMessage
8485
+ message: errorMessage6
7796
8486
  };
7797
8487
  };
7798
8488
  function addIssueToContext(ctx, issueData) {
@@ -27584,19 +28274,19 @@ var getRefs = (options) => {
27584
28274
  };
27585
28275
 
27586
28276
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/errorMessages.js
27587
- function addErrorMessage(res, key, errorMessage, refs) {
28277
+ function addErrorMessage(res, key, errorMessage6, refs) {
27588
28278
  if (!refs?.errorMessages)
27589
28279
  return;
27590
- if (errorMessage) {
28280
+ if (errorMessage6) {
27591
28281
  res.errorMessage = {
27592
28282
  ...res.errorMessage,
27593
- [key]: errorMessage
28283
+ [key]: errorMessage6
27594
28284
  };
27595
28285
  }
27596
28286
  }
27597
- function setResponseValueAndErrors(res, key, value, errorMessage, refs) {
28287
+ function setResponseValueAndErrors(res, key, value, errorMessage6, refs) {
27598
28288
  res[key] = value;
27599
- addErrorMessage(res, key, errorMessage, refs);
28289
+ addErrorMessage(res, key, errorMessage6, refs);
27600
28290
  }
27601
28291
 
27602
28292
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/getRelativePath.js
@@ -28907,8 +29597,8 @@ var Protocol = class {
28907
29597
  if (queuedMessage.type === "response") {
28908
29598
  resolver(message);
28909
29599
  } else {
28910
- const errorMessage = message;
28911
- const error51 = new McpError(errorMessage.error.code, errorMessage.error.message, errorMessage.error.data);
29600
+ const errorMessage6 = message;
29601
+ const error51 = new McpError(errorMessage6.error.code, errorMessage6.error.message, errorMessage6.error.data);
28912
29602
  resolver(error51);
28913
29603
  }
28914
29604
  } else {
@@ -30208,23 +30898,23 @@ var Server = class extends Protocol {
30208
30898
  const wrappedHandler = async (request, extra) => {
30209
30899
  const validatedRequest = safeParse2(CallToolRequestSchema, request);
30210
30900
  if (!validatedRequest.success) {
30211
- const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
30212
- throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
30901
+ const errorMessage6 = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
30902
+ throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage6}`);
30213
30903
  }
30214
30904
  const { params } = validatedRequest.data;
30215
30905
  const result = await Promise.resolve(handler(request, extra));
30216
30906
  if (params.task) {
30217
30907
  const taskValidationResult = safeParse2(CreateTaskResultSchema, result);
30218
30908
  if (!taskValidationResult.success) {
30219
- const errorMessage = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
30220
- throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
30909
+ const errorMessage6 = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
30910
+ throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage6}`);
30221
30911
  }
30222
30912
  return taskValidationResult.data;
30223
30913
  }
30224
30914
  const validationResult = safeParse2(CallToolResultSchema, result);
30225
30915
  if (!validationResult.success) {
30226
- const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
30227
- throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
30916
+ const errorMessage6 = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
30917
+ throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage6}`);
30228
30918
  }
30229
30919
  return validationResult.data;
30230
30920
  };
@@ -30718,12 +31408,12 @@ var McpServer = class {
30718
31408
  * @param errorMessage - The error message.
30719
31409
  * @returns The tool error result.
30720
31410
  */
30721
- createToolError(errorMessage) {
31411
+ createToolError(errorMessage6) {
30722
31412
  return {
30723
31413
  content: [
30724
31414
  {
30725
31415
  type: "text",
30726
- text: errorMessage
31416
+ text: errorMessage6
30727
31417
  }
30728
31418
  ],
30729
31419
  isError: true
@@ -30741,8 +31431,8 @@ var McpServer = class {
30741
31431
  const parseResult = await safeParseAsync2(schemaToParse, args);
30742
31432
  if (!parseResult.success) {
30743
31433
  const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
30744
- const errorMessage = getParseErrorMessage(error51);
30745
- throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`);
31434
+ const errorMessage6 = getParseErrorMessage(error51);
31435
+ throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage6}`);
30746
31436
  }
30747
31437
  return parseResult.data;
30748
31438
  }
@@ -30766,8 +31456,8 @@ var McpServer = class {
30766
31456
  const parseResult = await safeParseAsync2(outputObj, result.structuredContent);
30767
31457
  if (!parseResult.success) {
30768
31458
  const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
30769
- const errorMessage = getParseErrorMessage(error51);
30770
- throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`);
31459
+ const errorMessage6 = getParseErrorMessage(error51);
31460
+ throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage6}`);
30771
31461
  }
30772
31462
  }
30773
31463
  /**
@@ -30979,8 +31669,8 @@ var McpServer = class {
30979
31669
  const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
30980
31670
  if (!parseResult.success) {
30981
31671
  const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
30982
- const errorMessage = getParseErrorMessage(error51);
30983
- throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`);
31672
+ const errorMessage6 = getParseErrorMessage(error51);
31673
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage6}`);
30984
31674
  }
30985
31675
  const args = parseResult.data;
30986
31676
  const cb = prompt.callback;
@@ -31477,11 +32167,11 @@ var StdioServerTransport = class {
31477
32167
 
31478
32168
  // ../mcp/dist/context.js
31479
32169
  import { homedir as homedir2 } from "node:os";
31480
- import { join as join6 } from "node:path";
32170
+ import { join as join9 } from "node:path";
31481
32171
  function buildMcpContext(overrides = {}) {
31482
32172
  const userHome = overrides.userHome ?? homedir2();
31483
32173
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
31484
- const userConfigPath = join6(userHome, ".caveatrc.json");
32174
+ const userConfigPath = join9(userHome, ".caveatrc.json");
31485
32175
  const logger = overrides.logger ?? stderrLogger;
31486
32176
  const config2 = loadConfig(userConfigPath);
31487
32177
  const paths = resolvePaths(caveatHome, config2.knowledgeRepo, userHome);
@@ -31494,7 +32184,9 @@ var sourceFilter = external_exports.enum(["own", "community", "all"]);
31494
32184
  var confidenceSchema = external_exports.enum(["confirmed", "reproduced", "tentative"]);
31495
32185
  var visibilityFilter = external_exports.enum(["public", "private", "all"]);
31496
32186
  var searchInputShape = {
31497
- query: external_exports.string().describe("FTS query (3+ chars for trigram). Empty string lists without text filter."),
32187
+ query: external_exports.string().describe(
32188
+ "FTS query (3+ chars for trigram). Empty string lists without text filter. If a search returns 0 hits, retry with synonyms and Japanese/English paraphrases. Use plain tokens only; FTS operators are not supported."
32189
+ ),
31498
32190
  filters: external_exports.object({
31499
32191
  tags: external_exports.array(external_exports.string()).optional(),
31500
32192
  confidence: external_exports.array(confidenceSchema).optional(),
@@ -31543,7 +32235,9 @@ var outcomeSchema = external_exports.enum(["resolved", "impossible"]);
31543
32235
  var visibilitySchema = external_exports.enum(["public", "private"]);
31544
32236
  var recordInputShape = {
31545
32237
  title: external_exports.string().min(1),
31546
- symptom: external_exports.string().min(1),
32238
+ symptom: external_exports.string().min(1).describe(
32239
+ "Describe the observed failure. Preserve raw errors and exact error strings verbatim; when a stable translation is known, also include the main symptom keywords in Japanese and English to improve retrieval. Do not force a full translation or guess at uncertain wording."
32240
+ ),
31547
32241
  cause: external_exports.string().optional(),
31548
32242
  resolution: external_exports.string().optional(),
31549
32243
  evidence: external_exports.string().optional(),
@@ -31592,14 +32286,19 @@ var patchFrontmatterSchema = external_exports.object({
31592
32286
  });
31593
32287
  var updateInputShape = {
31594
32288
  id: external_exports.string(),
31595
- source: external_exports.string().optional(),
32289
+ source: external_exports.string().optional().describe("Only own entries may be updated. community entries are subscriptions; edit them upstream."),
31596
32290
  patch: external_exports.object({
31597
32291
  frontmatter: patchFrontmatterSchema.optional(),
31598
- sections: external_exports.record(external_exports.string(), external_exports.string()).optional()
32292
+ sections: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
32293
+ "Patch H2 sections by heading. When changing the Symptom section, preserve raw errors and exact error strings verbatim; when a stable translation is known, also include the main symptom keywords in Japanese and English to improve retrieval. Do not force a full translation or guess at uncertain wording."
32294
+ )
31599
32295
  })
31600
32296
  };
31601
32297
  function handleUpdate(ctx, args) {
31602
32298
  const source = args.source ?? "own";
32299
+ if (source !== "own") {
32300
+ throw new Error("community \u30A8\u30F3\u30C8\u30EA\u306F\u8CFC\u8AAD\u7269\u3067\u3059; \u7DE8\u96C6\u306F\u4E0A\u6D41\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044");
32301
+ }
31603
32302
  return updateEntry(args.id, args.patch, {
31604
32303
  db: ctx.db,
31605
32304
  entriesRoot: ctx.paths.entriesDir,
@@ -31616,13 +32315,12 @@ function handleListRecent(ctx, args) {
31616
32315
  }
31617
32316
 
31618
32317
  // ../mcp/dist/tools/pull.js
31619
- import { existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
31620
- import { join as join7 } from "node:path";
32318
+ import { existsSync as existsSync7 } from "node:fs";
31621
32319
  var pullInputShape = {};
31622
32320
  async function handlePull(ctx, _args = {}) {
31623
32321
  const pulled = [];
31624
32322
  const indexed = [];
31625
- if (existsSync5(ctx.paths.communityDir)) {
32323
+ if (existsSync7(ctx.paths.communityDir)) {
31626
32324
  const results = await communityPull({
31627
32325
  communityDir: ctx.paths.communityDir,
31628
32326
  logger: ctx.logger
@@ -31631,23 +32329,25 @@ async function handlePull(ctx, _args = {}) {
31631
32329
  pulled.push({ handle: r.handle, status: r.status, message: r.message });
31632
32330
  }
31633
32331
  }
31634
- rebuildAll(ctx.db);
31635
- if (existsSync5(ctx.paths.entriesDir)) {
31636
- const own = scanSource({ db: ctx.db, source: "own", entriesRoot: ctx.paths.entriesDir });
31637
- indexed.push({ source: "own", ...own });
32332
+ const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
32333
+ const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
32334
+ for (const failure of failures) {
32335
+ ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage3(failure.error)}`);
31638
32336
  }
31639
- if (existsSync5(ctx.paths.communityDir)) {
31640
- for (const entry of readdirSync2(ctx.paths.communityDir, { withFileTypes: true })) {
31641
- if (!entry.isDirectory()) continue;
31642
- const source = `community/${entry.name}`;
31643
- const root = join7(ctx.paths.communityDir, entry.name, "entries");
31644
- if (!existsSync5(root)) continue;
31645
- const scan = scanSource({ db: ctx.db, source, entriesRoot: root });
31646
- indexed.push({ source, ...scan });
31647
- }
32337
+ const result = reindexAllSources({
32338
+ db: ctx.db,
32339
+ paths: ctx.paths,
32340
+ logger: ctx.logger,
32341
+ keyProvider
32342
+ });
32343
+ for (const [source, scan] of Object.entries(result.perSource)) {
32344
+ indexed.push({ source, ...scan });
31648
32345
  }
31649
32346
  return { pulled, indexed };
31650
32347
  }
32348
+ function errorMessage3(err) {
32349
+ return err instanceof Error ? err.message : String(err);
32350
+ }
31651
32351
 
31652
32352
  // ../mcp/dist/registerTools.js
31653
32353
  function jsonResult(data) {
@@ -31665,7 +32365,7 @@ function registerAllTools(server, ctx) {
31665
32365
  "caveat_search",
31666
32366
  {
31667
32367
  title: "caveat_search",
31668
- description: 'Search the "caveats" knowledge base \u2014 records of time-wasting traps in EXTERNAL specs (GPU/driver/CUDA versions, native-module builds, IDE/shell quirks, platform-specific behavior, library version incompatibilities) that someone already diagnosed. Call this FIRST when a problem smells environmental rather than a logic bug \u2014 before reading stack traces top-to-bottom or trying fixes. Query: 3+ chars, plain tokens only (no OR/NEAR/other FTS5 operators). Returns summary rows including `{id, source}` \u2014 pass BOTH to caveat_get for the full body.',
32368
+ description: 'Search the "caveats" knowledge base \u2014 records of time-wasting traps someone already diagnosed, including external-spec issues (GPU/driver/CUDA versions, native-module builds, IDE/shell quirks, platform-specific behavior, library version incompatibilities) and reusable repo-specific context. Call this FIRST when a problem smells like a known trap \u2014 before reading stack traces top-to-bottom or trying fixes. Query: 3+ chars, plain tokens only (no OR/NEAR/other FTS5 operators). If it returns 0 hits, retry with synonyms and Japanese/English paraphrases. Returns summary rows including `{id, source}` \u2014 pass BOTH to caveat_get for the full body.',
31669
32369
  inputSchema: searchInputShape
31670
32370
  },
31671
32371
  async (args) => jsonResult(handleSearch(ctx, args))
@@ -31683,7 +32383,7 @@ function registerAllTools(server, ctx) {
31683
32383
  "caveat_record",
31684
32384
  {
31685
32385
  title: "caveat_record",
31686
- description: "Create a new caveat: a record of an external-spec trap that wasted real time (wrong driver, version mismatch, platform bug, IDE quirk, native-module issue, etc.) so future sessions can find it via caveat_search. REQUIRED BEFORE CALLING: (1) run caveat_search first to avoid duplicates, (2) ASK THE USER whether this should be `public` (shareable to the community DB) or `private` (kept local only) \u2014 never auto-classify visibility; the user owns the knowledge and decides its reach. Qualifies: specific symptom + diagnosed cause (or `outcome: impossible` verdict) + environment fingerprint. Does NOT qualify: project-internal bugs, user preferences, session summaries, ephemeral task notes. Auto-fills source_session and environment defaults; source_project is left null by design (shared knowledge must not leak per-user project names).",
32386
+ description: "Create a new caveat: a reusable record of a time-wasting trap (wrong driver, version mismatch, platform bug, IDE quirk, native-module issue, or repo-specific context) so future sessions can find it via caveat_search. REQUIRED BEFORE CALLING: run caveat_search first to avoid duplicates. Set visibility using this binary criterion: `public` if a third party could reproduce it; `private` if it is repo/workflow-specific or depends on intentional local design; when unclear, prefer `private`; follow an explicit user visibility choice. Qualifies: specific symptom + diagnosed cause (or `outcome: impossible` verdict) + environment fingerprint. Does NOT qualify: ordinary project-internal logic bugs, user preferences, session summaries, or ephemeral task notes; reusable repo-specific traps may be `private`. Auto-fills source_session and environment defaults; source_project is left null by design (shared knowledge must not leak per-user project names).",
31687
32387
  inputSchema: recordInputShape
31688
32388
  },
31689
32389
  async (args) => jsonResult(handleRecord(ctx, args))
@@ -31692,7 +32392,7 @@ function registerAllTools(server, ctx) {
31692
32392
  "caveat_update",
31693
32393
  {
31694
32394
  title: "caveat_update",
31695
- description: "Patch an existing caveat \u2014 use when newer evidence extends or corrects one that already exists. Frontmatter shallow-merges, but array fields (tags etc.) REPLACE rather than append \u2014 to add one tag, read the current list first, then patch with the full new array. Sections match by case-insensitive H2 heading. Immutable keys: id, created_at, source_session, source_project. Common uses: bump `last_verified` after re-confirming, add a resolution when it was `tentative`, flip `outcome` to `impossible`.",
32395
+ description: "Patch an existing caveat \u2014 use when newer evidence extends or corrects one that already exists. Frontmatter shallow-merges, but array fields (tags etc.) REPLACE rather than append \u2014 to add one tag, read the current list first, then patch with the full new array. Sections match by case-insensitive H2 heading. When changing `Symptom`, preserve raw errors verbatim and add stable Japanese/English symptom keywords when known; do not force or guess translations. Immutable keys: id, created_at, source_session, source_project. Common uses: bump `last_verified` after re-confirming, add a resolution when it was `tentative`, flip `outcome` to `impossible`.",
31696
32396
  inputSchema: updateInputShape
31697
32397
  },
31698
32398
  async (args) => jsonResult(handleUpdate(ctx, args))
@@ -31754,28 +32454,31 @@ async function runMcpServer() {
31754
32454
  }
31755
32455
 
31756
32456
  // src/commands/hookCmd.ts
31757
- import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
32457
+ import { spawn as spawn3 } from "node:child_process";
31758
32458
  import {
31759
- existsSync as existsSync7,
31760
- mkdirSync as mkdirSync4,
31761
- mkdtempSync,
31762
- readFileSync as readFileSync3,
31763
- rmSync,
31764
- unlinkSync,
31765
- writeFileSync as writeFileSync3
32459
+ chmodSync,
32460
+ existsSync as existsSync10,
32461
+ lstatSync,
32462
+ mkdtempSync as mkdtempSync2,
32463
+ mkdirSync as mkdirSync5,
32464
+ readdirSync as readdirSync2,
32465
+ readFileSync as readFileSync5,
32466
+ realpathSync as realpathSync2,
32467
+ rmSync as rmSync2,
32468
+ writeFileSync as writeFileSync5
31766
32469
  } from "node:fs";
31767
- import { tmpdir } from "node:os";
31768
- import { join as join9 } from "node:path";
32470
+ import { tmpdir as tmpdir2 } from "node:os";
32471
+ import { basename, dirname as dirname6, join as join12 } from "node:path";
31769
32472
  import { createHash, randomBytes } from "node:crypto";
31770
32473
 
31771
32474
  // src/autoReindexTrigger.ts
31772
32475
  import { spawn } from "node:child_process";
31773
- import { existsSync as existsSync6 } from "node:fs";
31774
- import { join as join8 } from "node:path";
32476
+ import { existsSync as existsSync8 } from "node:fs";
32477
+ import { join as join10 } from "node:path";
31775
32478
  function maybeTriggerAutoReindex(ctx) {
31776
32479
  if (process.env.CAVEAT_INDEX_AUTOSYNC === "off") return;
31777
- if (!existsSync6(ctx.paths.dbPath)) return;
31778
- if (existsSync6(join8(ctx.caveatHome, "index", ".reindex-lock"))) return;
32480
+ if (!existsSync8(ctx.paths.dbPath)) return;
32481
+ if (existsSync8(join10(ctx.caveatHome, "index", ".reindex-lock"))) return;
31779
32482
  const current = computeEntriesDigest(ctx.paths);
31780
32483
  const marker = readDigestMarker(ctx.caveatHome);
31781
32484
  if (marker?.digest === current.digest && marker.fileCount === current.fileCount) return;
@@ -31791,6 +32494,28 @@ function maybeTriggerAutoReindex(ctx) {
31791
32494
  child.unref();
31792
32495
  }
31793
32496
 
32497
+ // src/autoSyncTrigger.ts
32498
+ import { spawn as spawn2 } from "node:child_process";
32499
+ import { existsSync as existsSync9, statSync } from "node:fs";
32500
+ import { join as join11 } from "node:path";
32501
+ function maybeTriggerAutoSync(ctx) {
32502
+ if (process.env[CAVEAT_AUTO_SYNC_ENV] === "off") return;
32503
+ const statePath = join11(ctx.caveatHome, "sync", ".last-autosync.json");
32504
+ const debounceMs = Number(process.env.CAVEAT_AUTO_SYNC_DEBOUNCE_MS ?? AUTO_SYNC_DEBOUNCE_MS);
32505
+ try {
32506
+ if (existsSync9(statePath) && Date.now() - statSync(statePath).mtimeMs < debounceMs) return;
32507
+ } catch {
32508
+ }
32509
+ const cliScript = process.argv[1];
32510
+ if (!cliScript) throw new Error("current Caveat CLI script path is unavailable");
32511
+ const child = spawn2(
32512
+ process.execPath,
32513
+ [...process.execArgv, "--disable-warning=ExperimentalWarning", cliScript, "hook", "autosync"],
32514
+ { detached: true, stdio: "ignore", windowsHide: true }
32515
+ );
32516
+ child.unref();
32517
+ }
32518
+
31794
32519
  // src/commands/hookCmd.ts
31795
32520
  var silentLogger = {
31796
32521
  info: () => {
@@ -31835,31 +32560,44 @@ function buildContextSafely() {
31835
32560
  return null;
31836
32561
  }
31837
32562
  }
31838
- function searchCaveatsFromTextSafely(text) {
32563
+ function searchCaveatsFromTextSafely(text, surface) {
31839
32564
  if (!text) return [];
31840
32565
  let db;
32566
+ let caveatHome;
32567
+ let hits;
31841
32568
  try {
31842
32569
  const ctx = buildContextSafely();
31843
- if (!ctx || !existsSync7(ctx.paths.dbPath)) return [];
32570
+ if (!ctx || !existsSync10(ctx.paths.dbPath)) return [];
32571
+ caveatHome = ctx.caveatHome;
31844
32572
  db = openDb({ path: ctx.paths.dbPath });
31845
- const hits = findCaveatsForPrompt(db, text, {
32573
+ hits = findCaveatsForPrompt(db, text, {
31846
32574
  selfIdentity: defaultSelfIdentityTokens()
31847
32575
  });
31848
- if (hits.length > 0) {
31849
- try {
31850
- markHit(db, hits);
31851
- } catch (err) {
31852
- const msg = err instanceof Error ? err.message : String(err);
31853
- process.stderr.write(`[caveat:hook] markHit error: ${msg}
31854
- `);
31855
- }
31856
- }
31857
- return hits;
31858
32576
  } catch (err) {
31859
32577
  const msg = err instanceof Error ? err.message : String(err);
31860
32578
  process.stderr.write(`[caveat:hook] search error: ${msg}
31861
32579
  `);
31862
32580
  return [];
32581
+ }
32582
+ if (hits.length > 0) {
32583
+ try {
32584
+ markHit(db, hits);
32585
+ } catch (err) {
32586
+ const msg = err instanceof Error ? err.message : String(err);
32587
+ process.stderr.write(`[caveat:hook] markHit error: ${msg}
32588
+ `);
32589
+ }
32590
+ } else {
32591
+ try {
32592
+ logHookQueryMiss({ caveatHome, agent: "claude", surface, query: text });
32593
+ } catch (err) {
32594
+ const msg = err instanceof Error ? err.message : String(err);
32595
+ process.stderr.write(`[caveat:hook] query log error: ${msg}
32596
+ `);
32597
+ }
32598
+ }
32599
+ try {
32600
+ return hits;
31863
32601
  } finally {
31864
32602
  db?.close();
31865
32603
  }
@@ -31875,12 +32613,15 @@ function loadSignalsSafely(path) {
31875
32613
  }
31876
32614
  }
31877
32615
  function systemReminderOutput(text) {
31878
- return `<system-reminder>${text}</system-reminder>`;
32616
+ return `<system-reminder>${text.replace(/</g, "\u2039").replace(/>/g, "\u203A")}</system-reminder>`;
31879
32617
  }
31880
32618
  function drainForSession(sessionId) {
31881
32619
  const ctx = buildContextSafely();
31882
32620
  if (!ctx) return [];
31883
- return drainPendingReminders(ctx.caveatHome, sessionId);
32621
+ return [
32622
+ ...drainPendingReminders(ctx.caveatHome, sessionId),
32623
+ ...drainGlobalPendingReminders(ctx.caveatHome)
32624
+ ];
31884
32625
  }
31885
32626
  function claudeContextDedupeKey(text) {
31886
32627
  if (text.startsWith(CLAUDE_STOP_REMINDER_PREFIX)) return "claude-stop-reminder";
@@ -31924,20 +32665,20 @@ function stopSignalKey(signals, related) {
31924
32665
  return createHash("sha256").update(body).digest("hex");
31925
32666
  }
31926
32667
  function stopStatePath(caveatHome, sessionId) {
31927
- return join9(caveatHome, CLAUDE_STOP_STATE_DIR, `${sanitizeClaudeStateId(sessionId)}.txt`);
32668
+ return join12(caveatHome, CLAUDE_STOP_STATE_DIR, `${sanitizeClaudeStateId(sessionId)}.txt`);
31928
32669
  }
31929
32670
  function wasStopReminderQueued(caveatHome, sessionId, key) {
31930
32671
  const path = stopStatePath(caveatHome, sessionId);
31931
32672
  try {
31932
- return readFileSync3(path, "utf-8") === key;
32673
+ return readFileSync5(path, "utf-8") === key;
31933
32674
  } catch {
31934
32675
  return false;
31935
32676
  }
31936
32677
  }
31937
32678
  function markStopReminderQueued(caveatHome, sessionId, key) {
31938
32679
  const path = stopStatePath(caveatHome, sessionId);
31939
- mkdirSync4(join9(caveatHome, CLAUDE_STOP_STATE_DIR), { recursive: true });
31940
- writeFileSync3(path, key, "utf-8");
32680
+ mkdirSync5(join12(caveatHome, CLAUDE_STOP_STATE_DIR), { recursive: true });
32681
+ writeFileSync5(path, key, "utf-8");
31941
32682
  }
31942
32683
  function queueStopForSession(sessionId, signals, related) {
31943
32684
  const ctx = buildContextSafely();
@@ -31986,23 +32727,34 @@ function isToolError(payload) {
31986
32727
  if (typeof payload.error === "string" && payload.error.length > 0) return true;
31987
32728
  return false;
31988
32729
  }
32730
+ var WORKER_STALE_MS = 24 * 60 * 60 * 1e3;
32731
+ var WORKER_ROOT = "caveat-worker-v1";
32732
+ var WORKER_MARKER = ".caveat-worker-schema-v1";
31989
32733
  function spawnWorker(job) {
31990
- const workFile = join9(
31991
- tmpdir(),
31992
- `caveat-worker-${Date.now()}-${randomBytes(4).toString("hex")}.json`
31993
- );
32734
+ let root;
32735
+ let workDir;
32736
+ let workFile;
31994
32737
  try {
31995
- writeFileSync3(workFile, JSON.stringify(job), "utf-8");
32738
+ root = workerRoot();
32739
+ sweepStaleWorkerDirs(Date.now(), root);
32740
+ workDir = mkdtempSync2(join12(root, "job-"));
32741
+ chmodSync(workDir, 448);
32742
+ 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" });
31996
32744
  } catch (err) {
31997
32745
  const msg = err instanceof Error ? err.message : String(err);
31998
32746
  process.stderr.write(`[caveat:hook] worker writefile error: ${msg}
31999
32747
  `);
32748
+ cleanupWorkerDir(workDir, workDir ? dirname6(workDir) : void 0);
32000
32749
  return;
32001
32750
  }
32002
32751
  const cliScript = process.argv[1];
32003
- if (!cliScript) return;
32752
+ if (!cliScript) {
32753
+ cleanupWorkerDir(workDir, root);
32754
+ return;
32755
+ }
32004
32756
  try {
32005
- const child = spawn2(
32757
+ const child = spawn3(
32006
32758
  process.execPath,
32007
32759
  ["--disable-warning=ExperimentalWarning", cliScript, "hook", "worker", workFile],
32008
32760
  { detached: true, stdio: "ignore", windowsHide: true }
@@ -32013,7 +32765,7 @@ function spawnWorker(job) {
32013
32765
  process.stderr.write(`[caveat:hook] worker spawn error: ${msg}
32014
32766
  `);
32015
32767
  try {
32016
- unlinkSync(workFile);
32768
+ cleanupWorkerDir(workDir, root);
32017
32769
  } catch {
32018
32770
  }
32019
32771
  }
@@ -32021,14 +32773,11 @@ function spawnWorker(job) {
32021
32773
  async function runWorker(workFile) {
32022
32774
  let raw;
32023
32775
  try {
32024
- raw = readFileSync3(workFile, "utf-8");
32776
+ raw = readFileSync5(workFile, "utf-8");
32025
32777
  } catch {
32026
32778
  process.exit(0);
32027
32779
  }
32028
- try {
32029
- unlinkSync(workFile);
32030
- } catch {
32031
- }
32780
+ cleanupWorkerDir(dirname6(workFile), dirname6(dirname6(workFile)));
32032
32781
  let job;
32033
32782
  try {
32034
32783
  job = JSON.parse(raw);
@@ -32036,7 +32785,7 @@ async function runWorker(workFile) {
32036
32785
  process.exit(0);
32037
32786
  }
32038
32787
  if (!job.searchText || !job.sessionId) process.exit(0);
32039
- const hits = searchCaveatsFromTextSafely(job.searchText);
32788
+ const hits = searchCaveatsFromTextSafely(job.searchText, "tool_error");
32040
32789
  if (hits.length === 0) process.exit(0);
32041
32790
  const ctx = buildContextSafely();
32042
32791
  if (!ctx) process.exit(0);
@@ -32046,22 +32795,101 @@ async function runWorker(workFile) {
32046
32795
  }
32047
32796
  process.exit(0);
32048
32797
  }
32798
+ function cleanupWorkerDir(workDir, root) {
32799
+ if (!workDir || !root || !isOwnedWorkerDir(workDir, root)) return;
32800
+ try {
32801
+ rmSync2(workDir, { recursive: true, force: true });
32802
+ } catch {
32803
+ }
32804
+ }
32805
+ function isOwnedWorkerDir(path, root = workerRoot()) {
32806
+ try {
32807
+ const inputStat = lstatSync(path);
32808
+ if (inputStat.isSymbolicLink()) return false;
32809
+ const tmpRoot = realpathSync2(root);
32810
+ const resolved = realpathSync2(path);
32811
+ const stat = lstatSync(resolved);
32812
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
32813
+ return dirname6(resolved) === tmpRoot && basename(resolved).startsWith("job-") && stat.isDirectory() && !stat.isSymbolicLink() && hasPrivateOwnership(stat, uid);
32814
+ } catch {
32815
+ return false;
32816
+ }
32817
+ }
32818
+ function sweepStaleWorkerDirs(now = Date.now(), root = workerRoot()) {
32819
+ let entries;
32820
+ try {
32821
+ entries = readdirSync2(root);
32822
+ } catch {
32823
+ process.stderr.write("[caveat:hook] worker stale cleanup failed\n");
32824
+ return;
32825
+ }
32826
+ for (const entry of entries) {
32827
+ if (!entry.startsWith("job-")) continue;
32828
+ const path = join12(root, entry);
32829
+ try {
32830
+ if (!isOwnedWorkerDir(path, root)) continue;
32831
+ const stat = lstatSync(path);
32832
+ if (now - stat.mtimeMs <= WORKER_STALE_MS || !isStaleWorkerJobDir(path)) continue;
32833
+ rmSync2(path, { recursive: true, force: true });
32834
+ } catch {
32835
+ process.stderr.write("[caveat:hook] worker stale cleanup failed\n");
32836
+ }
32837
+ }
32838
+ }
32839
+ function workerRoot(base = tmpdir2()) {
32840
+ const root = join12(base, WORKER_ROOT);
32841
+ mkdirSync5(root, { recursive: true, mode: 448 });
32842
+ const stat = lstatSync(root);
32843
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
32844
+ if (!stat.isDirectory() || stat.isSymbolicLink() || !hasPrivateOwnership(stat, uid)) throw new Error("worker root is unsafe");
32845
+ const marker = join12(root, WORKER_MARKER);
32846
+ try {
32847
+ writeFileSync5(marker, "caveat-worker/v1\n", { mode: 384, flag: "wx" });
32848
+ } catch (error51) {
32849
+ if (!(error51 && typeof error51 === "object" && "code" in error51 && error51.code === "EEXIST")) throw error51;
32850
+ }
32851
+ const markerStat = lstatSync(marker);
32852
+ if (!markerStat.isFile() || markerStat.isSymbolicLink() || !hasPrivateOwnership(markerStat, uid) || readFileSync5(marker, "utf-8") !== "caveat-worker/v1\n") throw new Error("worker root marker is invalid");
32853
+ return root;
32854
+ }
32855
+ function isStaleWorkerJobDir(path) {
32856
+ const entries = readdirSync2(path);
32857
+ if (entries.length !== 1 || !entries[0].endsWith(".json")) return false;
32858
+ const file2 = join12(path, entries[0]);
32859
+ const stat = lstatSync(file2);
32860
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
32861
+ if (!stat.isFile() || stat.isSymbolicLink() || !hasPrivateOwnership(stat, uid)) return false;
32862
+ return isWorkerJob(JSON.parse(readFileSync5(file2, "utf-8")));
32863
+ }
32864
+ function hasPrivateOwnership(stat, uid) {
32865
+ return process.platform === "win32" || (stat.mode & 63) === 0 && (uid === void 0 || stat.uid === uid);
32866
+ }
32867
+ function isWorkerJob(value) {
32868
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
32869
+ const job = value;
32870
+ const keys = Object.keys(job).sort();
32871
+ if (!(keys.length === 3 && keys.join(",") === "schemaVersion,searchText,sessionId") && !(keys.length === 4 && keys.join(",") === "additionalContext,schemaVersion,searchText,sessionId")) return false;
32872
+ return job.schemaVersion === "caveat-worker-job/v1" && typeof job.sessionId === "string" && typeof job.searchText === "string" && (job.additionalContext === void 0 || job.additionalContext !== null && typeof job.additionalContext === "object" && !Array.isArray(job.additionalContext));
32873
+ }
32049
32874
  function writeLastReindex(caveatHome, value) {
32050
32875
  try {
32051
- writeFileSync3(join9(caveatHome, "index", ".last-reindex.json"), JSON.stringify(value), "utf-8");
32876
+ writeFileSync5(join12(caveatHome, "index", ".last-reindex.json"), JSON.stringify(value), "utf-8");
32052
32877
  } catch (err) {
32053
32878
  const msg = err instanceof Error ? err.message : String(err);
32054
32879
  process.stderr.write(`[caveat:hook] reindex status write error: ${msg}
32055
32880
  `);
32056
32881
  }
32057
32882
  }
32058
- function runReindexWorker() {
32883
+ function errorMessage4(err) {
32884
+ return err instanceof Error ? err.message : String(err);
32885
+ }
32886
+ async function runReindexWorker() {
32059
32887
  if (process.env.CAVEAT_INDEX_AUTOSYNC === "off") {
32060
32888
  process.stderr.write("[caveat:hook] auto reindex disabled by CAVEAT_INDEX_AUTOSYNC=off\n");
32061
32889
  return;
32062
32890
  }
32063
32891
  const ctx = buildContextSafely();
32064
- if (!ctx || !existsSync7(ctx.paths.dbPath)) {
32892
+ if (!ctx || !existsSync10(ctx.paths.dbPath)) {
32065
32893
  process.stderr.write("[caveat:hook] auto reindex skipped: index database does not exist\n");
32066
32894
  return;
32067
32895
  }
@@ -32071,8 +32899,14 @@ function runReindexWorker() {
32071
32899
  let db;
32072
32900
  try {
32073
32901
  const digest = computeEntriesDigest(ctx.paths);
32902
+ const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
32903
+ const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
32904
+ for (const failure of failures) {
32905
+ process.stderr.write(`[caveat:hook] ${failure.source}: sealed key prewarm failed: ${errorMessage4(failure.error)}
32906
+ `);
32907
+ }
32074
32908
  db = openDb({ path: ctx.paths.dbPath, logger: silentLogger });
32075
- const result = reindexAllSources({ db, paths: ctx.paths, logger: silentLogger });
32909
+ const result = reindexAllSources({ db, paths: ctx.paths, logger: silentLogger, keyProvider });
32076
32910
  writeDigestMarker(ctx.caveatHome, digest);
32077
32911
  writeLastReindex(ctx.caveatHome, {
32078
32912
  startedAt,
@@ -32099,12 +32933,31 @@ function runReindexWorker() {
32099
32933
  }
32100
32934
  }
32101
32935
  }
32936
+ async function runAutoSyncWorker() {
32937
+ if (process.env[CAVEAT_AUTO_SYNC_ENV] === "off") {
32938
+ process.stderr.write("[caveat:hook] auto sync disabled by CAVEAT_AUTO_SYNC=off\n");
32939
+ return;
32940
+ }
32941
+ const ctx = buildContextSafely();
32942
+ if (!ctx) return;
32943
+ try {
32944
+ await runAutoSync({
32945
+ caveatHome: ctx.caveatHome,
32946
+ ownDir: ctx.paths.knowledgeRepo,
32947
+ paths: ctx.paths,
32948
+ logger: silentLogger
32949
+ });
32950
+ } catch (err) {
32951
+ process.stderr.write(`[caveat:hook] auto sync error: ${errorMessage4(err)}
32952
+ `);
32953
+ }
32954
+ }
32102
32955
  function buildToolErrorReminder(job, hits) {
32103
32956
  const base = toolErrorReminderText(hits);
32104
32957
  const mode = hookCodexSidecarMode();
32105
32958
  if (mode === "off") return base;
32106
32959
  const projectRoot = process.cwd();
32107
- const hasSidecarConfig = existsSync7(join9(projectRoot, ".codex-sidecar.yml"));
32960
+ const hasSidecarConfig = existsSync10(join12(projectRoot, ".codex-sidecar.yml"));
32108
32961
  if (mode === "auto" && !hasSidecarConfig) return base;
32109
32962
  const advisory = runCodexSidecarAdvisory({
32110
32963
  searchText: job.searchText,
@@ -32114,21 +32967,20 @@ function buildToolErrorReminder(job, hits) {
32114
32967
  "A Claude Code tool just returned an error.",
32115
32968
  "Use the provided Caveat context to give concise next-step advice.",
32116
32969
  "Do not tell Claude to search Caveat again unless the context is insufficient."
32117
- ].join(" ")
32970
+ ].join(" "),
32971
+ additionalContext: job.additionalContext
32118
32972
  });
32119
32973
  if (advisory.status === "ok") {
32120
32974
  return [
32121
32975
  base,
32122
32976
  "",
32123
- "[caveat:codex-sidecar] Codex advisory:",
32124
- advisory.summary,
32125
- advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
32977
+ formatCodexSidecarAdvisory(advisory)
32126
32978
  ].filter(Boolean).join("\n");
32127
32979
  }
32128
32980
  return [
32129
32981
  base,
32130
32982
  "",
32131
- `[caveat:codex-sidecar] advisory unavailable: ${advisory.message}`
32983
+ formatCodexSidecarAdvisory(advisory)
32132
32984
  ].join("\n");
32133
32985
  }
32134
32986
  function hookCodexSidecarMode() {
@@ -32141,7 +32993,7 @@ function buildStopReminder(signals, related) {
32141
32993
  const mode = hookCodexSidecarMode();
32142
32994
  if (mode === "off") return base;
32143
32995
  const projectRoot = process.cwd();
32144
- const hasSidecarConfig = existsSync7(join9(projectRoot, ".codex-sidecar.yml"));
32996
+ const hasSidecarConfig = existsSync10(join12(projectRoot, ".codex-sidecar.yml"));
32145
32997
  if (mode === "auto" && !hasSidecarConfig) return base;
32146
32998
  const advisory = runCodexSidecarAdvisory({
32147
32999
  searchText: struggleSearchText(signals),
@@ -32149,115 +33001,44 @@ function buildStopReminder(signals, related) {
32149
33001
  projectRoot,
32150
33002
  prompt: [
32151
33003
  "A Claude Code session is ending after objective struggle signals.",
32152
- "Use the provided Caveat context and the signal text to advise whether Claude should update an existing caveat or record a new one.",
33004
+ "Use the provided Caveat context and structured hook signal context to advise whether Claude should update an existing caveat or record a new one.",
32153
33005
  "Be concise and preserve Caveat visibility rules."
32154
- ].join(" ")
33006
+ ].join(" "),
33007
+ additionalContext: buildHookSignalSidecarContextBlock({
33008
+ type: "stop",
33009
+ toolFailureCount: signals.toolFailureCount,
33010
+ reeditedFileCount: signals.fileEditCounts.length,
33011
+ webSearchCount: signals.webSearchCount,
33012
+ webFetchCount: signals.webFetchCount,
33013
+ bashRetryCount: signals.bashRetryCount,
33014
+ durationMinutes: signals.durationMinutes
33015
+ })
32155
33016
  });
32156
33017
  if (advisory.status === "ok") {
32157
33018
  return [
32158
33019
  base,
32159
33020
  "",
32160
- "[caveat:codex-sidecar] Codex advisory:",
32161
- advisory.summary,
32162
- advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
33021
+ formatCodexSidecarAdvisory(advisory)
32163
33022
  ].filter(Boolean).join("\n");
32164
33023
  }
32165
33024
  return [
32166
33025
  base,
32167
33026
  "",
32168
- `[caveat:codex-sidecar] advisory unavailable: ${advisory.message}`
33027
+ formatCodexSidecarAdvisory(advisory)
32169
33028
  ].join("\n");
32170
33029
  }
32171
- function compactFailureMessage(message) {
32172
- const singleLine = message.replace(/\s+/g, " ").trim();
32173
- if (!singleLine) return "unknown error";
32174
- return singleLine.length > 220 ? `${singleLine.slice(0, 220)}...` : singleLine;
32175
- }
32176
- function sidecarFailureDetail(output) {
32177
- const protocol = output.match(/PROTOCOL_ERROR:[^"\r\n]+/);
32178
- if (protocol) return compactFailureMessage(protocol[0]);
32179
- const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
32180
- const diagnostic = lines.find(
32181
- (line) => !line.startsWith("[caveat] [codex-sidecar] codex-sidecar ")
32182
- );
32183
- return compactFailureMessage(diagnostic ?? output);
32184
- }
32185
- function runCodexSidecarAdvisory(input) {
32186
- const cliScript = process.argv[1];
32187
- if (!cliScript) {
32188
- return { status: "failed", message: "current Caveat CLI script path is unavailable" };
32189
- }
32190
- const args = [
32191
- ...process.execArgv,
32192
- "--disable-warning=ExperimentalWarning",
32193
- cliScript,
32194
- "codex-sidecar",
32195
- "run",
32196
- "explore",
32197
- input.prompt,
32198
- "--preset",
32199
- "advisory",
32200
- "--query",
32201
- input.searchText,
32202
- "--limit",
32203
- String(Math.max(1, input.limit)),
32204
- "--host-agent",
32205
- "claude",
32206
- "--availability",
32207
- "operational"
32208
- ];
32209
- const resultDir = mkdtempSync(join9(tmpdir(), "caveat-codex-advisory-"));
32210
- const resultFile = join9(resultDir, "result.json");
32211
- args.push("--save-result", resultFile);
32212
- const nodeCli = process.env.CAVEAT_CODEX_SIDECAR_NODE_CLI;
32213
- if (nodeCli) {
32214
- args.push("--node-cli", nodeCli);
32215
- } else {
32216
- const command = process.env.CAVEAT_CODEX_SIDECAR_COMMAND;
32217
- if (command) args.push("--command", command);
32218
- }
33030
+ async function runHook(name, arg) {
32219
33031
  try {
32220
- const result = spawnSync2(process.execPath, args, {
32221
- cwd: input.projectRoot,
32222
- encoding: "utf-8",
32223
- timeout: Number(process.env.CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS ?? 12e4),
32224
- maxBuffer: 10 * 1024 * 1024
32225
- });
32226
- if (result.error) {
32227
- return { status: "failed", message: compactFailureMessage(result.error.message) };
32228
- }
32229
- if (result.status !== 0) {
32230
- const detail = (result.stderr || result.stdout || `exit ${result.status}`).trim();
32231
- return {
32232
- status: "failed",
32233
- message: `sidecar command failed: ${sidecarFailureDetail(detail)}`
32234
- };
32235
- }
32236
- const parsed = JSON.parse(readFileSync3(resultFile, "utf-8"));
32237
- if (parsed.status !== "ok" || typeof parsed.summary !== "string") {
32238
- return {
32239
- status: "failed",
32240
- message: compactFailureMessage(`unexpected SidecarResult status: ${String(parsed.status)}`)
32241
- };
32242
- }
32243
- return {
32244
- status: "ok",
32245
- summary: parsed.summary,
32246
- ...typeof parsed.rawEventLogRef === "string" ? { rawEventLogRef: parsed.rawEventLogRef } : {}
32247
- };
32248
- } catch (err) {
32249
- const msg = err instanceof Error ? err.message : String(err);
32250
- return {
32251
- status: "failed",
32252
- message: compactFailureMessage(`invalid SidecarResult JSON: ${msg}`)
32253
- };
32254
- } finally {
32255
- rmSync(resultDir, { recursive: true, force: true });
33032
+ sweepStaleWorkerDirs();
33033
+ } catch {
33034
+ process.stderr.write("[caveat:hook] worker stale cleanup failed\n");
32256
33035
  }
32257
- }
32258
- async function runHook(name, arg) {
32259
33036
  if (name === "reindex") {
32260
- runReindexWorker();
33037
+ await runReindexWorker();
33038
+ process.exit(0);
33039
+ }
33040
+ if (name === "autosync") {
33041
+ await runAutoSyncWorker();
32261
33042
  process.exit(0);
32262
33043
  }
32263
33044
  if (name === "worker") {
@@ -32279,7 +33060,7 @@ async function runHook(name, arg) {
32279
33060
  const contexts = name === "stop" ? [] : drainForSession(sessionId);
32280
33061
  if (name === "user-prompt-submit") {
32281
33062
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
32282
- const hits = searchCaveatsFromTextSafely(prompt);
33063
+ const hits = searchCaveatsFromTextSafely(prompt, "user_prompt");
32283
33064
  if (hits.length > 0) {
32284
33065
  contexts.push(userPromptSubmitReminderText(hits));
32285
33066
  }
@@ -32301,7 +33082,13 @@ async function runHook(name, arg) {
32301
33082
  payload.tool_response ?? payload.toolResponse ?? payload.error ?? payload
32302
33083
  );
32303
33084
  if (errText) {
32304
- spawnWorker({ sessionId, searchText: errText });
33085
+ const failureKind = payload.hook_event_name === "PostToolUseFailure" ? "post-tool-use-failure" : "error-bearing-post-tool-use";
33086
+ const additionalContext = buildHookSignalSidecarContextBlock({
33087
+ type: "tool-error",
33088
+ toolName: payload.tool_name ?? payload.toolName,
33089
+ failureKind
33090
+ });
33091
+ spawnWorker({ sessionId, searchText: errText, ...additionalContext ? { additionalContext } : {} });
32305
33092
  }
32306
33093
  process.exit(0);
32307
33094
  }
@@ -32320,6 +33107,13 @@ async function runHook(name, arg) {
32320
33107
  } catch (err) {
32321
33108
  const msg = err instanceof Error ? err.message : String(err);
32322
33109
  process.stderr.write(`[caveat:hook] auto reindex trigger error: ${msg}
33110
+ `);
33111
+ }
33112
+ try {
33113
+ maybeTriggerAutoSync(ctx);
33114
+ } catch (err) {
33115
+ const msg = err instanceof Error ? err.message : String(err);
33116
+ process.stderr.write(`[caveat:hook] auto sync trigger error: ${msg}
32323
33117
  `);
32324
33118
  }
32325
33119
  }
@@ -32327,7 +33121,7 @@ async function runHook(name, arg) {
32327
33121
  const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
32328
33122
  const signals = transcriptPath ? loadSignalsSafely(transcriptPath) : null;
32329
33123
  if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
32330
- const related = searchCaveatsFromTextSafely(struggleSearchText(signals));
33124
+ const related = searchCaveatsFromTextSafely(struggleSearchText(signals), "stop");
32331
33125
  queueStopForSession(sessionId, signals, related);
32332
33126
  process.exit(0);
32333
33127
  }
@@ -32337,236 +33131,11 @@ async function runHook(name, arg) {
32337
33131
  }
32338
33132
 
32339
33133
  // src/commands/codexHookCmd.ts
32340
- import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
32341
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
32342
- import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
32343
- import { join as join11 } from "node:path";
33134
+ import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
33135
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
33136
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
33137
+ import { join as join13 } from "node:path";
32344
33138
  import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
32345
-
32346
- // src/codexHookInstall.ts
32347
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
32348
- import { dirname as dirname5, join as join10 } from "node:path";
32349
- function quote2(p) {
32350
- return p.includes(" ") ? `"${p}"` : p;
32351
- }
32352
- function hookCommand2(nodePath, cliScriptPath, event) {
32353
- return `${quote2(nodePath)} ${quote2(cliScriptPath)} codex-hook ${event}`;
32354
- }
32355
- function eventCommandFragment(event) {
32356
- return `codex-hook ${event}`;
32357
- }
32358
- function isSameHookCommand2(actual, expected) {
32359
- return actual === expected || actual.endsWith(` ${expected}`);
32360
- }
32361
- function isCaveatCodexHookCommand(actual, event) {
32362
- const lower = actual.toLowerCase();
32363
- return lower.includes("caveat") && actual.includes(eventCommandFragment(event));
32364
- }
32365
- function hasCaveatHook(hooksJson, event, fragment) {
32366
- return hooksJson.hooks?.[event]?.some(
32367
- (entry) => entry.hooks?.some((h) => h.command.includes(fragment))
32368
- ) ?? false;
32369
- }
32370
- function readHooks(path) {
32371
- if (!existsSync8(path)) return {};
32372
- return JSON.parse(readFileSync4(path, "utf-8"));
32373
- }
32374
- function writeJsonWithBackup(path, value) {
32375
- const dir = dirname5(path);
32376
- if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
32377
- let backupPath = "";
32378
- if (existsSync8(path)) {
32379
- backupPath = `${path}.caveat-backup-${Date.now()}`;
32380
- copyFileSync2(path, backupPath);
32381
- }
32382
- writeFileSync4(path, `${JSON.stringify(value, null, 2)}
32383
- `, "utf-8");
32384
- return backupPath;
32385
- }
32386
- function upsertHook2(hooksJson, event, command, subcommand) {
32387
- hooksJson.hooks ??= {};
32388
- const list = hooksJson.hooks[event] ??= [];
32389
- for (const entry of list) {
32390
- for (const hook of entry.hooks ?? []) {
32391
- if (isSameHookCommand2(hook.command, command)) return "unchanged";
32392
- if (isCaveatCodexHookCommand(hook.command, subcommand)) {
32393
- hook.command = command;
32394
- hook.timeoutSec = 5;
32395
- hook.async = false;
32396
- hook.statusMessage = null;
32397
- return "added";
32398
- }
32399
- }
32400
- }
32401
- list.push({
32402
- hooks: [
32403
- {
32404
- type: "command",
32405
- command,
32406
- timeoutSec: 5,
32407
- async: false,
32408
- statusMessage: null
32409
- }
32410
- ]
32411
- });
32412
- return "added";
32413
- }
32414
- function removeHook2(hooksJson, event, command, subcommand) {
32415
- const list = hooksJson.hooks?.[event];
32416
- if (!list) return false;
32417
- const before = list.length;
32418
- const filtered = list.filter(
32419
- (entry) => !entry.hooks?.some(
32420
- (h) => isSameHookCommand2(h.command, command) || isCaveatCodexHookCommand(h.command, subcommand)
32421
- )
32422
- );
32423
- if (filtered.length === before) return false;
32424
- hooksJson.hooks[event] = filtered;
32425
- return true;
32426
- }
32427
- function enableCodexHooksFeature(raw) {
32428
- if (/^\s*codex_hooks\s*=\s*true\s*$/m.test(raw)) return { text: raw, changed: false };
32429
- const lines = raw.split(/\r?\n/);
32430
- const featuresStart = lines.findIndex((line) => /^\s*\[features]\s*$/.test(line));
32431
- if (featuresStart === -1) {
32432
- const prefix = raw.trimEnd();
32433
- const text = `${prefix}${prefix ? "\n\n" : ""}[features]
32434
- codex_hooks = true
32435
- `;
32436
- return { text, changed: true };
32437
- }
32438
- let insertAt = featuresStart + 1;
32439
- for (let i = featuresStart + 1; i < lines.length; i += 1) {
32440
- if (/^\s*\[.+]\s*$/.test(lines[i])) {
32441
- break;
32442
- }
32443
- if (/^\s*codex_hooks\s*=/.test(lines[i])) {
32444
- lines[i] = "codex_hooks = true";
32445
- return { text: `${lines.join("\n").trimEnd()}
32446
- `, changed: true };
32447
- }
32448
- }
32449
- lines.splice(insertAt, 0, "codex_hooks = true");
32450
- return { text: `${lines.join("\n").trimEnd()}
32451
- `, changed: true };
32452
- }
32453
- function writeConfigWithBackup(path, text) {
32454
- const dir = dirname5(path);
32455
- if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
32456
- let backupPath = "";
32457
- if (existsSync8(path)) {
32458
- backupPath = `${path}.caveat-backup-${Date.now()}`;
32459
- copyFileSync2(path, backupPath);
32460
- }
32461
- writeFileSync4(path, text, "utf-8");
32462
- return backupPath;
32463
- }
32464
- function installCodexHooks(opts) {
32465
- const hooksPath = join10(opts.codexHome, "hooks.json");
32466
- const configPath = join10(opts.codexHome, "config.toml");
32467
- const hooksJson = readHooks(hooksPath);
32468
- const userPromptSubmit = upsertHook2(
32469
- hooksJson,
32470
- "UserPromptSubmit",
32471
- hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
32472
- "user-prompt-submit"
32473
- );
32474
- const postToolUse = upsertHook2(
32475
- hooksJson,
32476
- "PostToolUse",
32477
- hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
32478
- "post-tool-use"
32479
- );
32480
- const stop = upsertHook2(
32481
- hooksJson,
32482
- "Stop",
32483
- hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
32484
- "stop"
32485
- );
32486
- const rawConfig = existsSync8(configPath) ? readFileSync4(configPath, "utf-8") : "";
32487
- const enabled = enableCodexHooksFeature(rawConfig);
32488
- const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
32489
- let backupPath;
32490
- let configBackupPath;
32491
- if (opts.dryRun) {
32492
- opts.logger.info(`[dry-run] would update ${hooksPath}`);
32493
- opts.logger.info(`[dry-run] would ensure [features].codex_hooks = true in ${configPath}`);
32494
- } else {
32495
- if (anyHookAdded) {
32496
- const backup = writeJsonWithBackup(hooksPath, hooksJson);
32497
- if (backup) backupPath = backup;
32498
- }
32499
- if (enabled.changed) {
32500
- const backup = writeConfigWithBackup(configPath, enabled.text);
32501
- if (backup) configBackupPath = backup;
32502
- }
32503
- }
32504
- return {
32505
- hooks: { userPromptSubmit, postToolUse, stop },
32506
- feature: enabled.changed ? "enabled" : "unchanged",
32507
- backupPath,
32508
- configBackupPath
32509
- };
32510
- }
32511
- function uninstallCodexHooks(opts) {
32512
- const hooksPath = join10(opts.codexHome, "hooks.json");
32513
- const hooksJson = readHooks(hooksPath);
32514
- const userPromptSubmitRemoved = removeHook2(
32515
- hooksJson,
32516
- "UserPromptSubmit",
32517
- hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
32518
- "user-prompt-submit"
32519
- );
32520
- const postToolUseRemoved = removeHook2(
32521
- hooksJson,
32522
- "PostToolUse",
32523
- hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
32524
- "post-tool-use"
32525
- );
32526
- const stopRemoved = removeHook2(
32527
- hooksJson,
32528
- "Stop",
32529
- hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
32530
- "stop"
32531
- );
32532
- let backupPath;
32533
- if (opts.dryRun) {
32534
- opts.logger.info(`[dry-run] would remove Caveat Codex hooks from ${hooksPath}`);
32535
- } else if (userPromptSubmitRemoved || postToolUseRemoved || stopRemoved) {
32536
- const backup = writeJsonWithBackup(hooksPath, hooksJson);
32537
- if (backup) backupPath = backup;
32538
- }
32539
- return {
32540
- hooks: {
32541
- userPromptSubmit: userPromptSubmitRemoved ? "added" : "unchanged",
32542
- postToolUse: postToolUseRemoved ? "added" : "unchanged",
32543
- stop: stopRemoved ? "added" : "unchanged"
32544
- },
32545
- feature: "unchanged",
32546
- backupPath
32547
- };
32548
- }
32549
- function detectCodexHookInstallation(codexHome) {
32550
- const hooksPath = join10(codexHome, "hooks.json");
32551
- const hooksJson = readHooks(hooksPath);
32552
- const hooks = {
32553
- userPromptSubmit: hasCaveatHook(
32554
- hooksJson,
32555
- "UserPromptSubmit",
32556
- eventCommandFragment("user-prompt-submit")
32557
- ),
32558
- postToolUse: hasCaveatHook(hooksJson, "PostToolUse", eventCommandFragment("post-tool-use")),
32559
- stop: hasCaveatHook(hooksJson, "Stop", eventCommandFragment("stop"))
32560
- };
32561
- const count = Object.values(hooks).filter(Boolean).length;
32562
- return {
32563
- installation: count === 0 ? "not-installed" : count === 3 ? "installed" : "partial",
32564
- hooksPath,
32565
- hooks
32566
- };
32567
- }
32568
-
32569
- // src/commands/codexHookCmd.ts
32570
33139
  var silentLogger2 = {
32571
33140
  info: () => {
32572
33141
  },
@@ -32606,31 +33175,44 @@ function buildContextSafely2() {
32606
33175
  return null;
32607
33176
  }
32608
33177
  }
32609
- function searchCaveatsFromTextSafely2(text) {
33178
+ function searchCaveatsFromTextSafely2(text, surface) {
32610
33179
  if (!text) return [];
32611
33180
  let db;
33181
+ let caveatHome;
33182
+ let hits;
32612
33183
  try {
32613
33184
  const ctx = buildContextSafely2();
32614
- if (!ctx || !existsSync9(ctx.paths.dbPath)) return [];
33185
+ if (!ctx || !existsSync11(ctx.paths.dbPath)) return [];
33186
+ caveatHome = ctx.caveatHome;
32615
33187
  db = openDb({ path: ctx.paths.dbPath });
32616
- const hits = findCaveatsForPrompt(db, text, {
33188
+ hits = findCaveatsForPrompt(db, text, {
32617
33189
  selfIdentity: defaultSelfIdentityTokens()
32618
33190
  });
32619
- if (hits.length > 0) {
32620
- try {
32621
- markHit(db, hits);
32622
- } catch (err) {
32623
- const msg = err instanceof Error ? err.message : String(err);
32624
- process.stderr.write(`[caveat:codex-hook] markHit error: ${msg}
32625
- `);
32626
- }
32627
- }
32628
- return hits;
32629
33191
  } catch (err) {
32630
33192
  const msg = err instanceof Error ? err.message : String(err);
32631
33193
  process.stderr.write(`[caveat:codex-hook] search error: ${msg}
32632
33194
  `);
32633
33195
  return [];
33196
+ }
33197
+ if (hits.length > 0) {
33198
+ try {
33199
+ markHit(db, hits);
33200
+ } catch (err) {
33201
+ const msg = err instanceof Error ? err.message : String(err);
33202
+ process.stderr.write(`[caveat:codex-hook] markHit error: ${msg}
33203
+ `);
33204
+ }
33205
+ } else {
33206
+ try {
33207
+ logHookQueryMiss({ caveatHome, agent: "codex", surface, query: text });
33208
+ } catch (err) {
33209
+ const msg = err instanceof Error ? err.message : String(err);
33210
+ process.stderr.write(`[caveat:codex-hook] query log error: ${msg}
33211
+ `);
33212
+ }
33213
+ }
33214
+ try {
33215
+ return hits;
32634
33216
  } finally {
32635
33217
  db?.close();
32636
33218
  }
@@ -32680,10 +33262,10 @@ function processExitCodeFromText(text) {
32680
33262
  return m ? Number(m[1]) : null;
32681
33263
  }
32682
33264
  function transcriptToolOutput(transcriptPath, toolUseId) {
32683
- if (!transcriptPath || !toolUseId || !existsSync9(transcriptPath)) return null;
33265
+ if (!transcriptPath || !toolUseId || !existsSync11(transcriptPath)) return null;
32684
33266
  let raw = "";
32685
33267
  try {
32686
- raw = readFileSync5(transcriptPath, "utf-8");
33268
+ raw = readFileSync6(transcriptPath, "utf-8");
32687
33269
  } catch {
32688
33270
  return null;
32689
33271
  }
@@ -32779,7 +33361,10 @@ function codexContextOutput(text, eventName = "UserPromptSubmit") {
32779
33361
  function drainForSession2(sessionId) {
32780
33362
  const ctx = buildContextSafely2();
32781
33363
  if (!ctx) return [];
32782
- return drainPendingReminders(ctx.caveatHome, sessionId);
33364
+ return [
33365
+ ...drainPendingReminders(ctx.caveatHome, sessionId),
33366
+ ...drainGlobalPendingReminders(ctx.caveatHome)
33367
+ ];
32783
33368
  }
32784
33369
  function codexContextDedupeKey(text) {
32785
33370
  if (text.startsWith(CODEX_STOP_REMINDER_PREFIX)) return "codex-stop-reminder";
@@ -32821,20 +33406,20 @@ function stopSignalKey2(signals, related) {
32821
33406
  return createHash2("sha256").update(body).digest("hex");
32822
33407
  }
32823
33408
  function stopStatePath2(caveatHome, sessionId) {
32824
- return join11(caveatHome, CODEX_STOP_STATE_DIR, `${sanitizeCodexStateId(sessionId)}.txt`);
33409
+ return join13(caveatHome, CODEX_STOP_STATE_DIR, `${sanitizeCodexStateId(sessionId)}.txt`);
32825
33410
  }
32826
33411
  function wasStopReminderQueued2(caveatHome, sessionId, key) {
32827
33412
  const path = stopStatePath2(caveatHome, sessionId);
32828
33413
  try {
32829
- return readFileSync5(path, "utf-8") === key;
33414
+ return readFileSync6(path, "utf-8") === key;
32830
33415
  } catch {
32831
33416
  return false;
32832
33417
  }
32833
33418
  }
32834
33419
  function markStopReminderQueued2(caveatHome, sessionId, key) {
32835
33420
  const path = stopStatePath2(caveatHome, sessionId);
32836
- mkdirSync6(join11(caveatHome, CODEX_STOP_STATE_DIR), { recursive: true });
32837
- writeFileSync5(path, key, "utf-8");
33421
+ mkdirSync6(join13(caveatHome, CODEX_STOP_STATE_DIR), { recursive: true });
33422
+ writeFileSync6(path, key, "utf-8");
32838
33423
  }
32839
33424
  function queueStopForSession2(sessionId, signals, related) {
32840
33425
  const ctx = buildContextSafely2();
@@ -32875,7 +33460,7 @@ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
32875
33460
  }
32876
33461
  }
32877
33462
  if (!knownError && job.allowSymptomOnly !== true) return;
32878
- const hits = searchCaveatsFromTextSafely2(searchText);
33463
+ const hits = searchCaveatsFromTextSafely2(searchText, "tool_error");
32879
33464
  if (hits.length === 0) return;
32880
33465
  const ctx = buildContextSafely2();
32881
33466
  if (!ctx) return;
@@ -32887,12 +33472,12 @@ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
32887
33472
  async function runCodexWorker(workFile) {
32888
33473
  let raw;
32889
33474
  try {
32890
- raw = readFileSync5(workFile, "utf-8");
33475
+ raw = readFileSync6(workFile, "utf-8");
32891
33476
  } catch {
32892
33477
  process.exit(0);
32893
33478
  }
32894
33479
  try {
32895
- unlinkSync2(workFile);
33480
+ unlinkSync(workFile);
32896
33481
  } catch {
32897
33482
  }
32898
33483
  let job;
@@ -32904,8 +33489,8 @@ async function runCodexWorker(workFile) {
32904
33489
  await processCodexWorkerJob(job);
32905
33490
  process.exit(0);
32906
33491
  }
32907
- function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join11(homedir3(), ".codex")) {
32908
- const features = spawnSync3("codex", ["features", "list"], {
33492
+ function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join13(homedir3(), ".codex")) {
33493
+ const features = spawnSync5("codex", ["features", "list"], {
32909
33494
  encoding: "utf-8",
32910
33495
  maxBuffer: 1024 * 1024
32911
33496
  });
@@ -32950,7 +33535,7 @@ async function runCodexHook(name, arg) {
32950
33535
  if (name === "user-prompt-submit") {
32951
33536
  const contexts = sessionId ? drainForSession2(sessionId) : [];
32952
33537
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
32953
- const hits = searchCaveatsFromTextSafely2(prompt);
33538
+ const hits = searchCaveatsFromTextSafely2(prompt, "user_prompt");
32954
33539
  if (hits.length > 0) {
32955
33540
  contexts.push(userPromptSubmitReminderText(hits));
32956
33541
  }
@@ -32969,7 +33554,23 @@ async function runCodexHook(name, arg) {
32969
33554
  if (name === "stop") {
32970
33555
  try {
32971
33556
  const ctx = buildContextSafely2();
32972
- if (ctx) maybeTriggerAutoReindex(ctx);
33557
+ if (ctx) {
33558
+ try {
33559
+ maybeSweepPendingDirs(ctx.caveatHome);
33560
+ } catch (err) {
33561
+ const msg = err instanceof Error ? err.message : String(err);
33562
+ process.stderr.write(`[caveat:codex-hook] pending sweep error: ${msg}
33563
+ `);
33564
+ }
33565
+ maybeTriggerAutoReindex(ctx);
33566
+ try {
33567
+ maybeTriggerAutoSync(ctx);
33568
+ } catch (err) {
33569
+ const msg = err instanceof Error ? err.message : String(err);
33570
+ process.stderr.write(`[caveat:codex-hook] auto sync trigger error: ${msg}
33571
+ `);
33572
+ }
33573
+ }
32973
33574
  } catch (err) {
32974
33575
  const msg = err instanceof Error ? err.message : String(err);
32975
33576
  process.stderr.write(`[caveat:codex-hook] auto reindex trigger error: ${msg}
@@ -32979,7 +33580,7 @@ async function runCodexHook(name, arg) {
32979
33580
  const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
32980
33581
  const signals = transcriptPath ? loadSignalsSafely2(transcriptPath) : null;
32981
33582
  if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
32982
- const related = searchCaveatsFromTextSafely2(struggleSearchText(signals));
33583
+ const related = searchCaveatsFromTextSafely2(struggleSearchText(signals), "stop");
32983
33584
  if (sessionId) queueStopForSession2(sessionId, signals, related);
32984
33585
  process.exit(0);
32985
33586
  }
@@ -32989,9 +33590,9 @@ async function runCodexHook(name, arg) {
32989
33590
  }
32990
33591
 
32991
33592
  // src/commands/pull.ts
32992
- import { existsSync as existsSync10 } from "node:fs";
33593
+ import { existsSync as existsSync12 } from "node:fs";
32993
33594
  async function runPull(ctx) {
32994
- const hasCommunityDir = existsSync10(ctx.paths.communityDir);
33595
+ const hasCommunityDir = existsSync12(ctx.paths.communityDir);
32995
33596
  if (!hasCommunityDir) {
32996
33597
  ctx.logger.info(
32997
33598
  "no community repos yet \u2014 add one with `caveat community add <github-url>`."
@@ -33007,8 +33608,12 @@ async function runPull(ctx) {
33007
33608
  }
33008
33609
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
33009
33610
  try {
33010
- rebuildAll(db);
33011
- const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger });
33611
+ const keyProvider = createKeyserverKeyProvider({ caveatHome: ctx.caveatHome });
33612
+ const failures = await prewarmSealedKeys({ paths: ctx.paths, keyProvider });
33613
+ for (const failure of failures) {
33614
+ ctx.logger.warn(`${failure.source}: sealed key prewarm failed: ${errorMessage5(failure.error)}`);
33615
+ }
33616
+ const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
33012
33617
  for (const [source, scan] of Object.entries(result.perSource)) {
33013
33618
  ctx.logger.info(`${source}: +${scan.added}`);
33014
33619
  }
@@ -33017,51 +33622,19 @@ async function runPull(ctx) {
33017
33622
  db.close();
33018
33623
  }
33019
33624
  }
33020
-
33021
- // src/ghSetup.ts
33022
- import { spawnSync as spawnSync4 } from "node:child_process";
33023
- import { readSync } from "node:fs";
33024
- function runGh(args) {
33025
- const result = spawnSync4("gh", args, { encoding: "utf-8" });
33026
- return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "", stderr: typeof result.stderr === "string" ? result.stderr : "" };
33027
- }
33028
- function commandError(result, fallback) {
33029
- return new Error((result.stderr || result.stdout || fallback).trim());
33030
- }
33031
- function askOnce(question) {
33032
- process.stdout.write(`${question} `);
33033
- const buf = Buffer.alloc(256);
33034
- const bytes = readSync(0, buf, 0, buf.length, null);
33035
- return /^(?:y|yes)$/i.test(buf.toString("utf-8", 0, bytes).trim());
33625
+ function errorMessage5(err) {
33626
+ return err instanceof Error ? err.message : String(err);
33036
33627
  }
33037
33628
 
33038
33629
  // src/commands/sync.ts
33039
33630
  async function defaultPrivateRepoUrl(opts, deps) {
33040
- const version2 = deps.ghRunner(["--version"]);
33041
- if (version2.status !== 0) {
33042
- throw new Error(
33043
- "gh is not available. create a private repository named Caveat-Private on GitHub, then run:\n caveat sync --init <url>\n # or install gh and run caveat sync --init"
33044
- );
33045
- }
33046
- const user = deps.ghRunner(["api", "user", "-q", ".login"]);
33047
- if (user.status !== 0 || !user.stdout.trim()) {
33048
- throw new Error("gh is not authenticated; run `gh auth login`, then retry `caveat sync --init`");
33049
- }
33050
- const login = user.stdout.trim();
33051
- const name = `${login}/Caveat-Private`;
33052
- const view = deps.ghRunner(["repo", "view", name]);
33053
- if (view.status !== 0) {
33054
- const question = `create private repo github.com/${name}? [y/N]`;
33055
- if (!opts.yes) {
33056
- if (!deps.isTty()) {
33057
- throw new Error(`${question}; rerun with --yes to approve non-interactively`);
33058
- }
33059
- if (!deps.confirm(question)) throw new Error("private repository creation cancelled");
33060
- }
33061
- const create = deps.ghRunner(["repo", "create", "Caveat-Private", "--private"]);
33062
- if (create.status !== 0) throw commandError(create, "gh repo create failed");
33063
- }
33064
- return `https://github.com/${name}.git`;
33631
+ return defaultGitHubRepoUrl({
33632
+ repositoryName: "Caveat-Private",
33633
+ visibility: "private",
33634
+ yes: opts.yes,
33635
+ retryCommand: "caveat sync --init <url>",
33636
+ ...deps
33637
+ });
33065
33638
  }
33066
33639
  async function runSync(ctx, opts, dependencies = {}) {
33067
33640
  try {
@@ -33100,6 +33673,10 @@ async function runSync(ctx, opts, dependencies = {}) {
33100
33673
  } else {
33101
33674
  const rebased = result.pulled ? ", rebased onto remote" : "";
33102
33675
  ctx.logger.info(`synced ${result.branch} \u2192 origin (${result.changedFiles} local change(s)${rebased})`);
33676
+ try {
33677
+ resetAutoSyncFailureState(ctx.caveatHome);
33678
+ } catch {
33679
+ }
33103
33680
  }
33104
33681
  } catch (err) {
33105
33682
  const message = err instanceof Error ? err.message : String(err);
@@ -33108,60 +33685,11 @@ async function runSync(ctx, opts, dependencies = {}) {
33108
33685
  }
33109
33686
  }
33110
33687
 
33111
- // src/commands/publish.ts
33112
- var HTTPS_TARGET_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+?(?:\.git)?\/?$/;
33113
- var SCP_TARGET_RE = /^git@github\.com:[^/]+\/[^/]+?(?:\.git)?$/;
33114
- function validatePublishTarget(url2) {
33115
- if (!HTTPS_TARGET_RE.test(url2) && !SCP_TARGET_RE.test(url2)) throw new Error("invalid publish target: use https://github.com/<org>/<repo>(.git) or git@github.com:<org>/<repo>(.git)");
33116
- return url2;
33117
- }
33118
- async function defaultPublicRepoUrl(opts, deps) {
33119
- const version2 = deps.ghRunner(["--version"]);
33120
- if (version2.status !== 0) throw new Error("gh is not available. create a public repository named Caveat-Public on GitHub, then run:\n caveat publish --init <url>\n # or install gh and run caveat publish");
33121
- const user = deps.ghRunner(["api", "user", "-q", ".login"]);
33122
- if (user.status !== 0 || !user.stdout.trim()) throw new Error("gh is not authenticated; run `gh auth login`, then retry `caveat publish`");
33123
- const login = user.stdout.trim();
33124
- const name = `${login}/Caveat-Public`;
33125
- const view = deps.ghRunner(["repo", "view", name]);
33126
- if (view.status !== 0) {
33127
- const question = `create public repo github.com/${name}? [y/N]`;
33128
- if (!opts.yes) {
33129
- if (!deps.isTty()) throw new Error(`${question}; rerun with --yes to approve non-interactively`);
33130
- if (!deps.confirm(question)) throw new Error("public repository creation cancelled");
33131
- }
33132
- const create = deps.ghRunner(["repo", "create", "Caveat-Public", "--public"]);
33133
- if (create.status !== 0) throw commandError(create, "gh repo create failed");
33134
- }
33135
- return `https://github.com/${name}.git`;
33136
- }
33137
- async function runPublish(ctx, opts, dependencies = {}) {
33138
- try {
33139
- const ghRunner = dependencies.ghRunner ?? runGh;
33140
- const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
33141
- const confirm = dependencies.confirm ?? askOnce;
33142
- let target = ctx.config.publishTarget;
33143
- if (opts.init !== void 0) {
33144
- target = validatePublishTarget(opts.init);
33145
- writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
33146
- }
33147
- if (!target) {
33148
- target = await defaultPublicRepoUrl(opts, { ghRunner, isTty, confirm });
33149
- writeUserConfigPatch(ctx.userConfigPath, { publishTarget: target });
33150
- }
33151
- const result = await publishOwn({ paths: ctx.paths, config: { ...ctx.config, publishTarget: target }, logger: ctx.logger, confirmImpl: confirm, isTty, dryRun: opts.dryRun, yes: opts.yes });
33152
- if (result.dryRun) ctx.logger.info(`[dry-run] ${result.fileCount} public entry(ies) ready to publish`);
33153
- else if (result.changed) ctx.logger.info(`published ${result.fileCount} public entry(ies)`);
33154
- } catch (err) {
33155
- ctx.logger.error(err instanceof Error ? err.message : String(err));
33156
- process.exitCode = 1;
33157
- }
33158
- }
33159
-
33160
33688
  // src/commands/codexSidecar.ts
33161
- import { spawnSync as spawnSync5 } from "node:child_process";
33162
- import { mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
33163
- import { tmpdir as tmpdir3 } from "node:os";
33164
- import { dirname as dirname6, join as join12 } from "node:path";
33689
+ import { spawnSync as spawnSync6 } from "node:child_process";
33690
+ import { closeSync, constants, fstatSync, lstatSync as lstatSync2, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, openSync, readSync as readSync2, realpathSync as realpathSync3, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
33691
+ import { tmpdir as tmpdir4 } from "node:os";
33692
+ import { basename as basename2, dirname as dirname7, join as join14 } from "node:path";
33165
33693
  import { cwd, exit } from "node:process";
33166
33694
  function runCodexSidecarDiagnostics(logger, opts) {
33167
33695
  const plan = buildCodexSidecarDiagnosticsCommand({
@@ -33193,8 +33721,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
33193
33721
  process.stdout.write(JSON.stringify({ status: "skipped", decision }, null, 2) + "\n");
33194
33722
  exit(0);
33195
33723
  }
33196
- const contextDir = mkdtempSync2(join12(tmpdir3(), "caveat-sidecar-context-"));
33197
- const contextFile = join12(contextDir, "context.json");
33724
+ const contextDir = mkdtempSync3(join14(tmpdir4(), "caveat-sidecar-context-"));
33725
+ const contextFile = join14(contextDir, "context.json");
33198
33726
  let status = 1;
33199
33727
  try {
33200
33728
  const blocks = collectCaveatContextBlocks(ctx, {
@@ -33203,7 +33731,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
33203
33731
  source: opts.source,
33204
33732
  visibility: opts.visibility
33205
33733
  });
33206
- writeFileSync6(contextFile, JSON.stringify({ context: blocks }, null, 2) + "\n", "utf-8");
33734
+ const additionalBlocks = opts.additionalContextFile ? readHookSignalAdditionalContextFile(opts.additionalContextFile) : [];
33735
+ writeFileSync7(contextFile, JSON.stringify({ context: [...blocks, ...additionalBlocks] }, null, 2) + "\n", "utf-8");
33207
33736
  const plan = buildCodexSidecarRunCommand({
33208
33737
  workflow,
33209
33738
  projectRoot: opts.project ?? cwd(),
@@ -33214,10 +33743,94 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
33214
33743
  });
33215
33744
  status = executePlan(ctx.logger, plan.command, plan.args, { saveResult: opts.saveResult });
33216
33745
  } finally {
33217
- rmSync2(contextDir, { recursive: true, force: true });
33746
+ rmSync3(contextDir, { recursive: true, force: true });
33218
33747
  }
33219
33748
  exit(status);
33220
33749
  }
33750
+ var MAX_ADDITIONAL_CONTEXT_BYTES = 4096;
33751
+ function readHookSignalAdditionalContextFile(path, testProbe) {
33752
+ const platform = testProbe?.platform ?? process.platform;
33753
+ assertWindowsPrivateTempContainer(path, platform);
33754
+ const before = lstatSync2(path);
33755
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
33756
+ assertPrivateRegular(before, uid, platform);
33757
+ testProbe?.afterLstat?.();
33758
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
33759
+ let raw;
33760
+ try {
33761
+ const opened = fstatSync(fd);
33762
+ assertPrivateRegular(opened, uid, platform);
33763
+ if (opened.dev !== before.dev || opened.ino !== before.ino) throw new Error("additional context file changed during open");
33764
+ const bytes = Buffer.alloc(MAX_ADDITIONAL_CONTEXT_BYTES + 1);
33765
+ let count = 0;
33766
+ while (count < bytes.length) {
33767
+ const read = readSync2(fd, bytes, count, bytes.length - count, count);
33768
+ if (read === 0) break;
33769
+ count += read;
33770
+ }
33771
+ if (count > MAX_ADDITIONAL_CONTEXT_BYTES) throw new Error(`additional context file exceeds ${MAX_ADDITIONAL_CONTEXT_BYTES} bytes`);
33772
+ const after = fstatSync(fd);
33773
+ assertPrivateRegular(after, uid, platform);
33774
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.dev !== before.dev || after.ino !== before.ino) throw new Error("additional context file changed during read");
33775
+ raw = bytes.subarray(0, count).toString("utf-8");
33776
+ } finally {
33777
+ closeSync(fd);
33778
+ }
33779
+ const parsed = JSON.parse(raw);
33780
+ if (!isRecord(parsed) || !Array.isArray(parsed.context) || parsed.context.length !== 1) {
33781
+ throw new Error("additional context file must contain exactly one context block");
33782
+ }
33783
+ const block = parsed.context[0];
33784
+ if (!isHookSignalBlock(block)) {
33785
+ throw new Error("additional context file contains an invalid hook signal block");
33786
+ }
33787
+ return [block];
33788
+ }
33789
+ function assertPrivateRegular(stat, uid, platform) {
33790
+ const privateOwner = platform === "win32" || (stat.mode & 63) === 0 && (uid === void 0 || stat.uid === uid);
33791
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_ADDITIONAL_CONTEXT_BYTES || !privateOwner) throw new Error("additional context file must be a private owner-only regular file within 4096 bytes");
33792
+ }
33793
+ function assertWindowsPrivateTempContainer(path, platform) {
33794
+ if (platform !== "win32") return;
33795
+ const parent = dirname7(path);
33796
+ const parentStat = lstatSync2(parent);
33797
+ const resolvedParent = realpathSync3(parent);
33798
+ const resolvedTemp = realpathSync3(tmpdir4());
33799
+ if (parentStat.isSymbolicLink() || !parentStat.isDirectory() || dirname7(resolvedParent) !== resolvedTemp || !basename2(resolvedParent).startsWith("caveat-")) {
33800
+ throw new Error("additional context file must be inside a reserved per-user Caveat temporary directory");
33801
+ }
33802
+ }
33803
+ function isRecord(value) {
33804
+ return value !== null && typeof value === "object" && !Array.isArray(value);
33805
+ }
33806
+ function isHookSignalBlock(value) {
33807
+ if (!isRecord(value) || !hasExactKeys(value, ["kind", "source", "trust", "summary", "data"]) || value.kind !== "manual_note" || value.source !== "caveat-hook-signal" || value.trust !== "local" || typeof value.summary !== "string" || !isRecord(value.data)) return false;
33808
+ const data = value.data;
33809
+ if (data.type === "tool-error") {
33810
+ if (!hasExactKeys(data, ["type", "tool", "failure_kind"]) || typeof data.tool !== "string" || data.failure_kind !== "post-tool-use-failure" && data.failure_kind !== "error-bearing-post-tool-use") return false;
33811
+ const canonical2 = buildHookSignalFromData(data);
33812
+ return canonical2 !== null && JSON.stringify(value) === JSON.stringify(canonical2);
33813
+ }
33814
+ if (!hasExactKeys(data, ["type", "tool_failure_count", "reedited_file_count", "web_search_count", "web_fetch_count", "bash_retry_count", "duration_minutes"])) return false;
33815
+ const canonical = buildHookSignalFromData(data);
33816
+ return canonical !== null && JSON.stringify(value) === JSON.stringify(canonical);
33817
+ }
33818
+ function hasExactKeys(value, keys) {
33819
+ const actual = Object.keys(value).sort();
33820
+ return actual.length === keys.length && actual.every((key, index) => key === keys.slice().sort()[index]);
33821
+ }
33822
+ function buildHookSignalFromData(data) {
33823
+ if (data.type === "tool-error") {
33824
+ const validTools = ["bash", "edit", "write", "read", "glob", "grep", "web-search", "web-fetch", "notebook-edit", "other"];
33825
+ if (!validTools.includes(String(data.tool))) return null;
33826
+ const toolNames = { bash: "Bash", edit: "Edit", write: "Write", read: "Read", glob: "Glob", grep: "Grep", "web-search": "WebSearch", "web-fetch": "WebFetch", "notebook-edit": "NotebookEdit", other: "other" };
33827
+ return buildHookSignalSidecarContextBlock({ type: "tool-error", toolName: toolNames[String(data.tool)], failureKind: data.failure_kind });
33828
+ }
33829
+ if (data.type !== "stop") return null;
33830
+ const keys = ["tool_failure_count", "reedited_file_count", "web_search_count", "web_fetch_count", "bash_retry_count", "duration_minutes"];
33831
+ if (!keys.every((key) => typeof data[key] === "number" && Number.isInteger(data[key]) && data[key] >= 0 && data[key] <= 1e4)) return null;
33832
+ return buildHookSignalSidecarContextBlock({ type: "stop", toolFailureCount: data.tool_failure_count, reeditedFileCount: data.reedited_file_count, webSearchCount: data.web_search_count, webFetchCount: data.web_fetch_count, bashRetryCount: data.bash_retry_count, durationMinutes: data.duration_minutes });
33833
+ }
33221
33834
  function runCodexSidecarWorkSmoke(ctx, prompt, opts) {
33222
33835
  const plan = buildCodexSidecarRunCommand({
33223
33836
  workflow: "work",
@@ -33258,7 +33871,7 @@ function defaultPreset(workflow) {
33258
33871
  }
33259
33872
  function executePlan(logger, command, args, options = {}) {
33260
33873
  logger.info(`[codex-sidecar] ${command} ${args.map(shellDisplayQuote).join(" ")}`);
33261
- const result = spawnSync5(command, args, { encoding: "utf-8", stdio: "pipe" });
33874
+ const result = spawnSync6(command, args, { encoding: "utf-8", stdio: "pipe" });
33262
33875
  if (result.stdout) process.stdout.write(result.stdout);
33263
33876
  if (result.stderr) process.stderr.write(result.stderr);
33264
33877
  if (options.saveResult && result.stdout) {
@@ -33272,8 +33885,8 @@ function executePlan(logger, command, args, options = {}) {
33272
33885
  }
33273
33886
  function saveStructuredResult(path, stdout) {
33274
33887
  const parsed = JSON.parse(stdout);
33275
- mkdirSync7(dirname6(path), { recursive: true });
33276
- writeFileSync6(path, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
33888
+ mkdirSync7(dirname7(path), { recursive: true });
33889
+ writeFileSync7(path, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
33277
33890
  }
33278
33891
  function shellDisplayQuote(value) {
33279
33892
  return /[\s"'$`]/.test(value) ? JSON.stringify(value) : value;
@@ -33361,10 +33974,13 @@ function runCommunityRemove(ctx, handle, opts) {
33361
33974
 
33362
33975
  // src/index.ts
33363
33976
  var program = new Command();
33977
+ function collectRepeatable(value, previous) {
33978
+ return [...previous, value];
33979
+ }
33364
33980
  program.name("caveat").description("External spec gotcha knowledge base CLI").version(CAVEAT_VERSION);
33365
33981
  program.command("init").description(
33366
33982
  "Initialize ~/.caveatrc.json, ~/.caveat/, and register Claude Code integration. Add knowledge sources later with `caveat community add <github-url>`."
33367
- ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--dry-run", "show planned changes without writing", false).option(
33983
+ ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--sync [url]", "initialize private sync (optionally with its URL)").option("--publish-target [url]", "configure the sealed public mirror target").option("--yes", "approve creating conventional GitHub repositories", false).option("--skip-codex-hook", "skip Codex hook installation", false).option("--dry-run", "show planned changes without writing", false).option(
33368
33984
  "--pending-stale-days <days>",
33369
33985
  "sweep `<caveatHome>/pending/<sessionId>/` whose newest entry is older than this (default 7)",
33370
33986
  (raw) => {
@@ -33380,7 +33996,11 @@ program.command("init").description(
33380
33996
  await runInit(ctx, {
33381
33997
  skipClaude: opts.skipClaude,
33382
33998
  dryRun: opts.dryRun,
33383
- pendingStaleDays: opts.pendingStaleDays
33999
+ pendingStaleDays: opts.pendingStaleDays,
34000
+ sync: opts.sync,
34001
+ publishTarget: opts.publishTarget,
34002
+ yes: opts.yes,
34003
+ skipCodexHook: opts.skipCodexHook
33384
34004
  });
33385
34005
  }
33386
34006
  );
@@ -33388,9 +34008,9 @@ program.command("uninstall").description("Remove Claude Code MCP server and hook
33388
34008
  const ctx = buildContext(stdoutLogger);
33389
34009
  runUninstall(ctx, { dryRun: opts.dryRun });
33390
34010
  });
33391
- program.command("index").description("Index knowledge repo entries into SQLite").option("--full", "full rebuild (DELETE all then rescan)", false).action((opts) => {
34011
+ program.command("index").description("Index knowledge repo entries into SQLite").option("--full", "full rebuild (DELETE all then rescan)", false).action(async (opts) => {
33392
34012
  const ctx = buildContext(stdoutLogger);
33393
- runIndex(ctx, { full: opts.full });
34013
+ await runIndex(ctx, { full: opts.full });
33394
34014
  });
33395
34015
  program.command("search").description("Search caveats via FTS").argument("<query>", "FTS query (3+ chars for trigram)").option("--source <source>", "own | community | all").option("--tag <tag...>", "filter by tag (repeatable)").option("--confidence <confidence...>", "filter by confidence (repeatable)").action(
33396
34016
  (query, opts) => {
@@ -33432,7 +34052,7 @@ program.command("sync").description("Sync own Caveat entries to a private git re
33432
34052
  const ctx = buildContext(stdoutLogger);
33433
34053
  await runSync(ctx, opts);
33434
34054
  });
33435
- program.command("publish").description("Mirror own public Caveat entries to a public git repository").option("--init <url>", "set the public mirror target").option("--dry-run", "show public mirror changes without committing or pushing", false).option("--yes", "approve creating a default repository or publishing changes", false).action(async (opts) => {
34055
+ program.command("publish").description("Mirror own public Caveat entries to a public git repository").option("--init <url>", "set the public mirror target").option("--dry-run", "show public mirror changes without committing or pushing", false).option("--yes", "approve creating a default repository or publishing changes", false).option("--allow <digest>", "allow a scan finding by its sha256 digest (repeatable)", collectRepeatable, []).option("--save", "persist --allow digests to .caveat-publish-allow.json in the own repo", false).action(async (opts) => {
33436
34056
  const ctx = buildContext(stdoutLogger);
33437
34057
  await runPublish(ctx, opts);
33438
34058
  });
@@ -33443,7 +34063,7 @@ program.command("mcp-server").description("Run the MCP stdio server (registered
33443
34063
  await runMcpServer();
33444
34064
  });
33445
34065
  program.command("hook <name> [arg]").description(
33446
- "Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker | reindex"
34066
+ "Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker | reindex | autosync"
33447
34067
  ).action(async (name, arg) => {
33448
34068
  await runHook(name, arg);
33449
34069
  });
@@ -33465,6 +34085,11 @@ codexHook.command("install").description("Install Caveat hooks into ~/.codex/hoo
33465
34085
  stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse}`);
33466
34086
  stdoutLogger.info(`Stop hook: ${result.hooks.stop}`);
33467
34087
  stdoutLogger.info(`codex_hooks feature: ${result.feature}`);
34088
+ if (result.feature === "blocked") {
34089
+ stdoutLogger.warn(
34090
+ `${result.blockedReason}; preserving explicit consent. Set \`codex_hooks = true\` in ${opts.codexHome}/config.toml, then rerun \`caveat codex-hook install\`.`
34091
+ );
34092
+ }
33468
34093
  if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);
33469
34094
  if (result.configBackupPath) {
33470
34095
  stdoutLogger.info(`config.toml backed up: ${result.configBackupPath}`);
@@ -33510,7 +34135,7 @@ codexSidecar.command("diagnostics").description("Run codex-sidecar diagnostics f
33510
34135
  codexSidecar.command("smoke").description("Run a read-only codex_explore smoke through codex-sidecar").option("--project <path>", "repository root to check").option("--preset <preset>", "codex-sidecar read-only preset", "explore").option("--command <command>", "codex-sidecar executable", "codex-sidecar").option("--node-cli <path>", "development path to codex-sidecar CLI JS").option("--save-result <path>", "write structured SidecarResult JSON to this path").action((opts) => {
33511
34136
  runCodexSidecarSmoke(stdoutLogger, opts);
33512
34137
  });
33513
- codexSidecar.command("run <workflow> [prompt]").description("Run read-only codex-sidecar with relevant Caveat entries as context").option("--project <path>", "repository root to check").option("--preset <preset>", "codex-sidecar preset").option("--query <query>", "Caveat search query; defaults to prompt text").option("--limit <n>", "maximum caveat entries to pass", (v) => Number(v), 5).option("--source <source>", "own | community | all", "all").option("--visibility <visibility>", "public | private | all", "all").option("--host-agent <agent>", "claude | codex | automation | unknown", "unknown").option("--availability <level>", "disabled | unavailable | configured | operational | work-capable", "operational").option("--sidecar-agent <agent>", "codex | disabled | auto").option("--requires-isolation", "mark the sidecar call as isolation-bounded", false).option("--structured-result-required", "mark structured SidecarResult as the delegation boundary", false).option("--explicit-second-pass", "mark the sidecar call as an explicit second-pass review", false).option("--command <command>", "codex-sidecar executable", "codex-sidecar").option("--node-cli <path>", "development path to codex-sidecar CLI JS").option("--save-result <path>", "write structured SidecarResult JSON to this path").action(
34138
+ codexSidecar.command("run <workflow> [prompt]").description("Run read-only codex-sidecar with relevant Caveat entries as context").option("--project <path>", "repository root to check").option("--preset <preset>", "codex-sidecar preset").option("--query <query>", "Caveat search query; defaults to prompt text").option("--limit <n>", "maximum caveat entries to pass", (v) => Number(v), 5).option("--source <source>", "own | community | all", "all").option("--visibility <visibility>", "public | private | all", "all").option("--host-agent <agent>", "claude | codex | automation | unknown", "unknown").option("--availability <level>", "disabled | unavailable | configured | operational | work-capable", "operational").option("--sidecar-agent <agent>", "codex | disabled | auto").option("--requires-isolation", "mark the sidecar call as isolation-bounded", false).option("--structured-result-required", "mark structured SidecarResult as the delegation boundary", false).option("--explicit-second-pass", "mark the sidecar call as an explicit second-pass review", false).option("--command <command>", "codex-sidecar executable", "codex-sidecar").option("--node-cli <path>", "development path to codex-sidecar CLI JS").option("--save-result <path>", "write structured SidecarResult JSON to this path").option("--additional-context-file <path>", "strictly validated bounded hook-signal context file").action(
33514
34139
  (workflow, prompt, opts) => {
33515
34140
  const normalized = workflow === "risk" ? "risk-check" : workflow;
33516
34141
  if (normalized !== "review" && normalized !== "explore" && normalized !== "opinion" && normalized !== "risk-check") {
@@ -33527,7 +34152,7 @@ codexSidecar.command("work-smoke [prompt]").description("Run codex_work in an is
33527
34152
  runCodexSidecarWorkSmoke(ctx, prompt, opts);
33528
34153
  }
33529
34154
  );
33530
- var community = program.command("community").description("Manage community caveat repos (shallow clones under <knowledgeRepo>/community/)");
34155
+ var community = program.command("community").description("Manage community caveat repos (shallow clones under <caveatHome>/community/)");
33531
34156
  community.command("add <url>").description("Shallow-clone a GitHub caveat repo into community/<handle>/").action(async (url2) => {
33532
34157
  const ctx = buildContext(stdoutLogger);
33533
34158
  await runCommunityAdd(ctx, url2);