caveat-cli 0.14.10 → 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,47 +2,68 @@
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,
7
+ KNOWLEDGE_GITIGNORE,
8
+ PublishScanError,
9
+ SyncError,
5
10
  __commonJS,
6
11
  __export,
7
12
  __toESM,
13
+ acquireReindexLock,
8
14
  appendPendingReminder,
9
15
  buildCodexSidecarDiagnosticsCommand,
10
16
  buildCodexSidecarReadOnlySmokeCommand,
11
17
  buildCodexSidecarRunCommand,
18
+ buildHookSignalSidecarContextBlock,
12
19
  caveatEntriesToSidecarContextBlocks,
13
20
  cleanupStalePendingDirs,
14
21
  communityAdd,
15
22
  communityList,
16
23
  communityPull,
17
24
  communityRemove,
25
+ computeEntriesDigest,
26
+ createKeyserverKeyProvider,
18
27
  decideCodexSidecarExecution,
19
28
  defaultSelfIdentityTokens,
29
+ drainGlobalPendingReminders,
20
30
  drainPendingReminders,
21
31
  ensureUserConfig,
22
32
  findCaveatHome,
23
33
  findCaveatsForPrompt,
24
34
  get,
25
35
  hasAnyStruggleSignal,
36
+ initOwnSync,
26
37
  listRecent,
27
38
  listStale,
28
39
  loadConfig,
40
+ logHookQueryMiss,
29
41
  markHit,
30
42
  maybeSweepPendingDirs,
31
43
  openDb,
44
+ prewarmSealedKeys,
45
+ publishOwn,
32
46
  readCodexSessionSignals,
47
+ readDigestMarker,
33
48
  readSessionSignals,
34
49
  rebuildAll,
35
50
  recordEntry,
51
+ reindexAllSources,
52
+ releaseReindexLock,
53
+ resetAutoSyncFailureState,
36
54
  resolvePaths,
37
- scanSource,
55
+ runAutoSync,
38
56
  search,
39
57
  stderrLogger,
40
58
  stopReminderText,
41
59
  struggleSearchText,
60
+ syncOwn,
42
61
  toolErrorReminderText,
43
62
  updateEntry,
44
- userPromptSubmitReminderText
45
- } from "./chunk-ECC5LJZF.js";
63
+ userPromptSubmitReminderText,
64
+ writeDigestMarker,
65
+ writeUserConfigPatch
66
+ } from "./chunk-ZNAFNCPW.js";
46
67
 
47
68
  // ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/compile/codegen/code.js
48
69
  var require_code = __commonJS({
@@ -421,11 +442,11 @@ var require_codegen = __commonJS({
421
442
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
422
443
  return `${varKind} ${this.name}${rhs};` + _n;
423
444
  }
424
- optimizeNames(names, constants) {
445
+ optimizeNames(names, constants2) {
425
446
  if (!names[this.name.str])
426
447
  return;
427
448
  if (this.rhs)
428
- this.rhs = optimizeExpr(this.rhs, names, constants);
449
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
429
450
  return this;
430
451
  }
431
452
  get names() {
@@ -442,10 +463,10 @@ var require_codegen = __commonJS({
442
463
  render({ _n }) {
443
464
  return `${this.lhs} = ${this.rhs};` + _n;
444
465
  }
445
- optimizeNames(names, constants) {
466
+ optimizeNames(names, constants2) {
446
467
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
447
468
  return;
448
- this.rhs = optimizeExpr(this.rhs, names, constants);
469
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
449
470
  return this;
450
471
  }
451
472
  get names() {
@@ -506,8 +527,8 @@ var require_codegen = __commonJS({
506
527
  optimizeNodes() {
507
528
  return `${this.code}` ? this : void 0;
508
529
  }
509
- optimizeNames(names, constants) {
510
- this.code = optimizeExpr(this.code, names, constants);
530
+ optimizeNames(names, constants2) {
531
+ this.code = optimizeExpr(this.code, names, constants2);
511
532
  return this;
512
533
  }
513
534
  get names() {
@@ -536,12 +557,12 @@ var require_codegen = __commonJS({
536
557
  }
537
558
  return nodes.length > 0 ? this : void 0;
538
559
  }
539
- optimizeNames(names, constants) {
560
+ optimizeNames(names, constants2) {
540
561
  const { nodes } = this;
541
562
  let i = nodes.length;
542
563
  while (i--) {
543
564
  const n = nodes[i];
544
- if (n.optimizeNames(names, constants))
565
+ if (n.optimizeNames(names, constants2))
545
566
  continue;
546
567
  subtractNames(names, n.names);
547
568
  nodes.splice(i, 1);
@@ -594,12 +615,12 @@ var require_codegen = __commonJS({
594
615
  return void 0;
595
616
  return this;
596
617
  }
597
- optimizeNames(names, constants) {
618
+ optimizeNames(names, constants2) {
598
619
  var _a3;
599
- this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
600
- 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))
601
622
  return;
602
- this.condition = optimizeExpr(this.condition, names, constants);
623
+ this.condition = optimizeExpr(this.condition, names, constants2);
603
624
  return this;
604
625
  }
605
626
  get names() {
@@ -622,10 +643,10 @@ var require_codegen = __commonJS({
622
643
  render(opts) {
623
644
  return `for(${this.iteration})` + super.render(opts);
624
645
  }
625
- optimizeNames(names, constants) {
626
- if (!super.optimizeNames(names, constants))
646
+ optimizeNames(names, constants2) {
647
+ if (!super.optimizeNames(names, constants2))
627
648
  return;
628
- this.iteration = optimizeExpr(this.iteration, names, constants);
649
+ this.iteration = optimizeExpr(this.iteration, names, constants2);
629
650
  return this;
630
651
  }
631
652
  get names() {
@@ -661,10 +682,10 @@ var require_codegen = __commonJS({
661
682
  render(opts) {
662
683
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
663
684
  }
664
- optimizeNames(names, constants) {
665
- if (!super.optimizeNames(names, constants))
685
+ optimizeNames(names, constants2) {
686
+ if (!super.optimizeNames(names, constants2))
666
687
  return;
667
- this.iterable = optimizeExpr(this.iterable, names, constants);
688
+ this.iterable = optimizeExpr(this.iterable, names, constants2);
668
689
  return this;
669
690
  }
670
691
  get names() {
@@ -706,11 +727,11 @@ var require_codegen = __commonJS({
706
727
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
707
728
  return this;
708
729
  }
709
- optimizeNames(names, constants) {
730
+ optimizeNames(names, constants2) {
710
731
  var _a3, _b;
711
- super.optimizeNames(names, constants);
712
- (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
713
- (_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);
714
735
  return this;
715
736
  }
716
737
  get names() {
@@ -1011,7 +1032,7 @@ var require_codegen = __commonJS({
1011
1032
  function addExprNames(names, from) {
1012
1033
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
1013
1034
  }
1014
- function optimizeExpr(expr, names, constants) {
1035
+ function optimizeExpr(expr, names, constants2) {
1015
1036
  if (expr instanceof code_1.Name)
1016
1037
  return replaceName(expr);
1017
1038
  if (!canOptimize(expr))
@@ -1026,14 +1047,14 @@ var require_codegen = __commonJS({
1026
1047
  return items;
1027
1048
  }, []));
1028
1049
  function replaceName(n) {
1029
- const c = constants[n.str];
1050
+ const c = constants2[n.str];
1030
1051
  if (c === void 0 || names[n.str] !== 1)
1031
1052
  return n;
1032
1053
  delete names[n.str];
1033
1054
  return c;
1034
1055
  }
1035
1056
  function canOptimize(e) {
1036
- 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);
1037
1058
  }
1038
1059
  }
1039
1060
  function subtractNames(names, from) {
@@ -6854,8 +6875,9 @@ var stdoutLogger = {
6854
6875
  };
6855
6876
 
6856
6877
  // src/commands/init.ts
6857
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, renameSync, rmdirSync, writeFileSync as writeFileSync2 } from "node:fs";
6858
- 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";
6859
6881
 
6860
6882
  // src/claudeInstall.ts
6861
6883
  import { spawnSync } from "node:child_process";
@@ -7050,9 +7072,328 @@ function uninstallClaudeIntegration(opts) {
7050
7072
  };
7051
7073
  }
7052
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
+
7053
7394
  // src/nodePath.ts
7054
- import { existsSync as existsSync2, realpathSync } from "node:fs";
7055
- 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";
7056
7397
  var defaultRealpath = (path) => realpathSync.native(path);
7057
7398
  function safeRealpath(path, realpath = defaultRealpath) {
7058
7399
  try {
@@ -7065,7 +7406,7 @@ function resolveHookNodePath({
7065
7406
  env = process.env,
7066
7407
  execPath = process.execPath,
7067
7408
  platform = process.platform,
7068
- exists = existsSync2,
7409
+ exists = existsSync3,
7069
7410
  realpath = defaultRealpath
7070
7411
  } = {}) {
7071
7412
  const execRealpath = safeRealpath(execPath, realpath);
@@ -7073,7 +7414,7 @@ function resolveHookNodePath({
7073
7414
  const names = platform === "win32" ? ["node.exe", "node.cmd", "node.bat", "node"] : ["node"];
7074
7415
  for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
7075
7416
  for (const name of names) {
7076
- const candidate = join4(dir, name);
7417
+ const candidate = join5(dir, name);
7077
7418
  if (!exists(candidate)) continue;
7078
7419
  const candidateRealpath = safeRealpath(candidate, realpath);
7079
7420
  if (execRealpath && candidateRealpath && candidateRealpath === execRealpath) {
@@ -7084,37 +7425,245 @@ function resolveHookNodePath({
7084
7425
  return execPath;
7085
7426
  }
7086
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
+
7087
7630
  // src/commands/init.ts
7088
- var KNOWLEDGE_GITIGNORE = [
7089
- "# Never commit entries flagged private (visibility: private is enforced by the",
7090
- "# pre-commit gate, but this is a filename-level backup guard).",
7091
- "*.private.md",
7092
- "",
7093
- "# Obsidian per-user config: workspace layout, theme, plugin state, cache.",
7094
- ".obsidian/",
7095
- ""
7096
- ].join("\n");
7097
- 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";
7098
7637
  ensureUserConfig(ctx.userConfigPath);
7099
7638
  ctx.logger.info(`user config: ${ctx.userConfigPath}`);
7100
- if (!existsSync3(ctx.paths.knowledgeRepo)) {
7101
- mkdirSync2(ctx.paths.knowledgeRepo, { recursive: true });
7102
- 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 });
7103
7642
  ctx.logger.info(`knowledge repo scaffolded: ${ctx.paths.knowledgeRepo}`);
7104
7643
  } else {
7105
7644
  ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
7106
7645
  }
7107
7646
  migrateLegacyCommunityDir(ctx);
7108
- const gitignorePath = join5(ctx.paths.knowledgeRepo, ".gitignore");
7109
- if (!existsSync3(gitignorePath)) {
7110
- 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");
7111
7650
  ctx.logger.info(`.gitignore created: ${gitignorePath}`);
7112
7651
  }
7113
7652
  if (!opts.dryRun) {
7114
- const dbDir = dirname3(ctx.paths.dbPath);
7115
- 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
+ }
7116
7660
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
7117
- db.close();
7661
+ try {
7662
+ reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
7663
+ writeDigestMarker(ctx.caveatHome, computeEntriesDigest(ctx.paths));
7664
+ } finally {
7665
+ db.close();
7666
+ }
7118
7667
  ctx.logger.info(`db initialized: ${ctx.paths.dbPath}`);
7119
7668
  } else {
7120
7669
  ctx.logger.info(`[dry-run] db path: ${ctx.paths.dbPath}`);
@@ -7133,39 +7682,179 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
7133
7682
  ctx.logger.info(
7134
7683
  "tip: subscribe to a group repo with `caveat community add <github-url>`, then `caveat pull`."
7135
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
+ }
7136
7760
  if (opts.skipClaude) {
7137
7761
  ctx.logger.info("Claude Code integration skipped (--skip-claude)");
7138
- 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
+ }
7139
7776
  }
7140
- const cliScriptPath = process.argv[1];
7141
- if (!cliScriptPath) {
7142
- ctx.logger.warn("cannot determine CLI script path; skipping Claude integration");
7143
- 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
+ }
7144
7806
  }
7145
- const result = installClaudeIntegration({
7146
- claudeDir: join5(ctx.userHome, ".claude"),
7147
- cliScriptPath,
7148
- nodePath: resolveHookNodePath(),
7149
- dryRun: opts.dryRun,
7150
- 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"
7151
7814
  });
7152
- 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);
7153
7842
  }
7154
7843
  function migrateLegacyCommunityDir(ctx) {
7155
- const legacy = join5(ctx.paths.knowledgeRepo, "community");
7844
+ const legacy = join8(ctx.paths.knowledgeRepo, "community");
7156
7845
  const current = ctx.paths.communityDir;
7157
7846
  if (legacy === current) return;
7158
- if (!existsSync3(legacy)) return;
7159
- if (existsSync3(current)) {
7847
+ if (!existsSync5(legacy)) return;
7848
+ if (existsSync5(current)) {
7160
7849
  ctx.logger.warn(
7161
7850
  `legacy community dir still exists at ${legacy} \u2014 remove manually (new location in use)`
7162
7851
  );
7163
7852
  return;
7164
7853
  }
7165
- mkdirSync2(current, { recursive: true });
7854
+ mkdirSync3(current, { recursive: true });
7166
7855
  for (const entry of readdirSync(legacy, { withFileTypes: true })) {
7167
7856
  if (!entry.isDirectory()) continue;
7168
- renameSync(join5(legacy, entry.name), join5(current, entry.name));
7857
+ renameSync(join8(legacy, entry.name), join8(current, entry.name));
7169
7858
  }
7170
7859
  try {
7171
7860
  rmdirSync(legacy);
@@ -7180,7 +7869,7 @@ function runUninstall(ctx, opts) {
7180
7869
  process.exit(1);
7181
7870
  }
7182
7871
  const result = uninstallClaudeIntegration({
7183
- claudeDir: join5(ctx.userHome, ".claude"),
7872
+ claudeDir: join8(ctx.userHome, ".claude"),
7184
7873
  cliScriptPath,
7185
7874
  nodePath: resolveHookNodePath(),
7186
7875
  dryRun: opts.dryRun,
@@ -7227,37 +7916,38 @@ function reportInstallResult(ctx, result, dryRun) {
7227
7916
  }
7228
7917
 
7229
7918
  // src/commands/indexCmd.ts
7230
- import { existsSync as existsSync4, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "node:fs";
7231
- import { dirname as dirname4, join as join6 } from "node:path";
7232
- function runIndex(ctx, opts) {
7233
- const dbDir = dirname4(ctx.paths.dbPath);
7234
- 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
+ }
7235
7929
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
7236
7930
  try {
7237
7931
  if (opts.full) {
7238
7932
  ctx.logger.info("full rebuild: DELETE FROM entries");
7239
7933
  rebuildAll(db);
7240
7934
  }
7241
- if (existsSync4(ctx.paths.entriesDir)) {
7242
- const result = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
7243
- ctx.logger.info(`own: +${result.added} ~${result.updated} -${result.deleted}`);
7244
- } else {
7245
- ctx.logger.warn(`entries dir not found: ${ctx.paths.entriesDir}`);
7246
- }
7247
- if (existsSync4(ctx.paths.communityDir)) {
7248
- for (const entry of readdirSync2(ctx.paths.communityDir, { withFileTypes: true })) {
7249
- if (!entry.isDirectory()) continue;
7250
- const source = `community/${entry.name}`;
7251
- const root = join6(ctx.paths.communityDir, entry.name, "entries");
7252
- if (!existsSync4(root)) continue;
7253
- const result = scanSource({ db, source, entriesRoot: root });
7254
- ctx.logger.info(`${source}: +${result.added} ~${result.updated} -${result.deleted}`);
7935
+ const result = reindexAllSources({ db, paths: ctx.paths, logger: ctx.logger, keyProvider });
7936
+ for (const [source, scan] of Object.entries(result.perSource)) {
7937
+ if (source === "own") {
7938
+ ctx.logger.info(`own: +${scan.added} ~${scan.updated} -${scan.deleted}`);
7939
+ } else {
7940
+ ctx.logger.info(`${source}: +${scan.added} ~${scan.updated} -${scan.deleted}`);
7255
7941
  }
7256
7942
  }
7943
+ writeDigestMarker(ctx.caveatHome, computeEntriesDigest(ctx.paths));
7257
7944
  } finally {
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-YQ4DJ2H7.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 join7 } 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 = join7(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 readdirSync3 } from "node:fs";
31620
- import { join as join8 } 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 readdirSync3(ctx.paths.communityDir, { withFileTypes: true })) {
31641
- if (!entry.isDirectory()) continue;
31642
- const source = `community/${entry.name}`;
31643
- const root = join8(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,19 +32454,69 @@ async function runMcpServer() {
31754
32454
  }
31755
32455
 
31756
32456
  // src/commands/hookCmd.ts
31757
- import { spawn, spawnSync as spawnSync2 } from "node:child_process";
32457
+ import { spawn as spawn3 } from "node:child_process";
31758
32458
  import {
31759
- existsSync as existsSync6,
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";
32473
+
32474
+ // src/autoReindexTrigger.ts
32475
+ import { spawn } from "node:child_process";
32476
+ import { existsSync as existsSync8 } from "node:fs";
32477
+ import { join as join10 } from "node:path";
32478
+ function maybeTriggerAutoReindex(ctx) {
32479
+ if (process.env.CAVEAT_INDEX_AUTOSYNC === "off") return;
32480
+ if (!existsSync8(ctx.paths.dbPath)) return;
32481
+ if (existsSync8(join10(ctx.caveatHome, "index", ".reindex-lock"))) return;
32482
+ const current = computeEntriesDigest(ctx.paths);
32483
+ const marker = readDigestMarker(ctx.caveatHome);
32484
+ if (marker?.digest === current.digest && marker.fileCount === current.fileCount) return;
32485
+ const cliScript = process.argv[1];
32486
+ if (!cliScript) throw new Error("current Caveat CLI script path is unavailable");
32487
+ const child = spawn(
32488
+ process.execPath,
32489
+ // execArgv を継承して親と同じランタイム(例: テストハーネスの --import tsx)で
32490
+ // worker を起動する。素の node だと TS ソース実行のテスト環境で worker が即死する。
32491
+ [...process.execArgv, "--disable-warning=ExperimentalWarning", cliScript, "hook", "reindex"],
32492
+ { detached: true, stdio: "ignore", windowsHide: true }
32493
+ );
32494
+ child.unref();
32495
+ }
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
+
32519
+ // src/commands/hookCmd.ts
31770
32520
  var silentLogger = {
31771
32521
  info: () => {
31772
32522
  },
@@ -31810,31 +32560,44 @@ function buildContextSafely() {
31810
32560
  return null;
31811
32561
  }
31812
32562
  }
31813
- function searchCaveatsFromTextSafely(text) {
32563
+ function searchCaveatsFromTextSafely(text, surface) {
31814
32564
  if (!text) return [];
31815
32565
  let db;
32566
+ let caveatHome;
32567
+ let hits;
31816
32568
  try {
31817
32569
  const ctx = buildContextSafely();
31818
- if (!ctx || !existsSync6(ctx.paths.dbPath)) return [];
32570
+ if (!ctx || !existsSync10(ctx.paths.dbPath)) return [];
32571
+ caveatHome = ctx.caveatHome;
31819
32572
  db = openDb({ path: ctx.paths.dbPath });
31820
- const hits = findCaveatsForPrompt(db, text, {
32573
+ hits = findCaveatsForPrompt(db, text, {
31821
32574
  selfIdentity: defaultSelfIdentityTokens()
31822
32575
  });
31823
- if (hits.length > 0) {
31824
- try {
31825
- markHit(db, hits);
31826
- } catch (err) {
31827
- const msg = err instanceof Error ? err.message : String(err);
31828
- process.stderr.write(`[caveat:hook] markHit error: ${msg}
31829
- `);
31830
- }
31831
- }
31832
- return hits;
31833
32576
  } catch (err) {
31834
32577
  const msg = err instanceof Error ? err.message : String(err);
31835
32578
  process.stderr.write(`[caveat:hook] search error: ${msg}
31836
32579
  `);
31837
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;
31838
32601
  } finally {
31839
32602
  db?.close();
31840
32603
  }
@@ -31850,12 +32613,15 @@ function loadSignalsSafely(path) {
31850
32613
  }
31851
32614
  }
31852
32615
  function systemReminderOutput(text) {
31853
- return `<system-reminder>${text}</system-reminder>`;
32616
+ return `<system-reminder>${text.replace(/</g, "\u2039").replace(/>/g, "\u203A")}</system-reminder>`;
31854
32617
  }
31855
32618
  function drainForSession(sessionId) {
31856
32619
  const ctx = buildContextSafely();
31857
32620
  if (!ctx) return [];
31858
- return drainPendingReminders(ctx.caveatHome, sessionId);
32621
+ return [
32622
+ ...drainPendingReminders(ctx.caveatHome, sessionId),
32623
+ ...drainGlobalPendingReminders(ctx.caveatHome)
32624
+ ];
31859
32625
  }
31860
32626
  function claudeContextDedupeKey(text) {
31861
32627
  if (text.startsWith(CLAUDE_STOP_REMINDER_PREFIX)) return "claude-stop-reminder";
@@ -31899,20 +32665,20 @@ function stopSignalKey(signals, related) {
31899
32665
  return createHash("sha256").update(body).digest("hex");
31900
32666
  }
31901
32667
  function stopStatePath(caveatHome, sessionId) {
31902
- return join9(caveatHome, CLAUDE_STOP_STATE_DIR, `${sanitizeClaudeStateId(sessionId)}.txt`);
32668
+ return join12(caveatHome, CLAUDE_STOP_STATE_DIR, `${sanitizeClaudeStateId(sessionId)}.txt`);
31903
32669
  }
31904
32670
  function wasStopReminderQueued(caveatHome, sessionId, key) {
31905
32671
  const path = stopStatePath(caveatHome, sessionId);
31906
32672
  try {
31907
- return readFileSync3(path, "utf-8") === key;
32673
+ return readFileSync5(path, "utf-8") === key;
31908
32674
  } catch {
31909
32675
  return false;
31910
32676
  }
31911
32677
  }
31912
32678
  function markStopReminderQueued(caveatHome, sessionId, key) {
31913
32679
  const path = stopStatePath(caveatHome, sessionId);
31914
- mkdirSync4(join9(caveatHome, CLAUDE_STOP_STATE_DIR), { recursive: true });
31915
- writeFileSync3(path, key, "utf-8");
32680
+ mkdirSync5(join12(caveatHome, CLAUDE_STOP_STATE_DIR), { recursive: true });
32681
+ writeFileSync5(path, key, "utf-8");
31916
32682
  }
31917
32683
  function queueStopForSession(sessionId, signals, related) {
31918
32684
  const ctx = buildContextSafely();
@@ -31961,23 +32727,34 @@ function isToolError(payload) {
31961
32727
  if (typeof payload.error === "string" && payload.error.length > 0) return true;
31962
32728
  return false;
31963
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";
31964
32733
  function spawnWorker(job) {
31965
- const workFile = join9(
31966
- tmpdir(),
31967
- `caveat-worker-${Date.now()}-${randomBytes(4).toString("hex")}.json`
31968
- );
32734
+ let root;
32735
+ let workDir;
32736
+ let workFile;
31969
32737
  try {
31970
- 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" });
31971
32744
  } catch (err) {
31972
32745
  const msg = err instanceof Error ? err.message : String(err);
31973
32746
  process.stderr.write(`[caveat:hook] worker writefile error: ${msg}
31974
32747
  `);
32748
+ cleanupWorkerDir(workDir, workDir ? dirname6(workDir) : void 0);
31975
32749
  return;
31976
32750
  }
31977
32751
  const cliScript = process.argv[1];
31978
- if (!cliScript) return;
32752
+ if (!cliScript) {
32753
+ cleanupWorkerDir(workDir, root);
32754
+ return;
32755
+ }
31979
32756
  try {
31980
- const child = spawn(
32757
+ const child = spawn3(
31981
32758
  process.execPath,
31982
32759
  ["--disable-warning=ExperimentalWarning", cliScript, "hook", "worker", workFile],
31983
32760
  { detached: true, stdio: "ignore", windowsHide: true }
@@ -31988,7 +32765,7 @@ function spawnWorker(job) {
31988
32765
  process.stderr.write(`[caveat:hook] worker spawn error: ${msg}
31989
32766
  `);
31990
32767
  try {
31991
- unlinkSync(workFile);
32768
+ cleanupWorkerDir(workDir, root);
31992
32769
  } catch {
31993
32770
  }
31994
32771
  }
@@ -31996,14 +32773,11 @@ function spawnWorker(job) {
31996
32773
  async function runWorker(workFile) {
31997
32774
  let raw;
31998
32775
  try {
31999
- raw = readFileSync3(workFile, "utf-8");
32776
+ raw = readFileSync5(workFile, "utf-8");
32000
32777
  } catch {
32001
32778
  process.exit(0);
32002
32779
  }
32003
- try {
32004
- unlinkSync(workFile);
32005
- } catch {
32006
- }
32780
+ cleanupWorkerDir(dirname6(workFile), dirname6(dirname6(workFile)));
32007
32781
  let job;
32008
32782
  try {
32009
32783
  job = JSON.parse(raw);
@@ -32011,7 +32785,7 @@ async function runWorker(workFile) {
32011
32785
  process.exit(0);
32012
32786
  }
32013
32787
  if (!job.searchText || !job.sessionId) process.exit(0);
32014
- const hits = searchCaveatsFromTextSafely(job.searchText);
32788
+ const hits = searchCaveatsFromTextSafely(job.searchText, "tool_error");
32015
32789
  if (hits.length === 0) process.exit(0);
32016
32790
  const ctx = buildContextSafely();
32017
32791
  if (!ctx) process.exit(0);
@@ -32021,12 +32795,169 @@ async function runWorker(workFile) {
32021
32795
  }
32022
32796
  process.exit(0);
32023
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
+ }
32874
+ function writeLastReindex(caveatHome, value) {
32875
+ try {
32876
+ writeFileSync5(join12(caveatHome, "index", ".last-reindex.json"), JSON.stringify(value), "utf-8");
32877
+ } catch (err) {
32878
+ const msg = err instanceof Error ? err.message : String(err);
32879
+ process.stderr.write(`[caveat:hook] reindex status write error: ${msg}
32880
+ `);
32881
+ }
32882
+ }
32883
+ function errorMessage4(err) {
32884
+ return err instanceof Error ? err.message : String(err);
32885
+ }
32886
+ async function runReindexWorker() {
32887
+ if (process.env.CAVEAT_INDEX_AUTOSYNC === "off") {
32888
+ process.stderr.write("[caveat:hook] auto reindex disabled by CAVEAT_INDEX_AUTOSYNC=off\n");
32889
+ return;
32890
+ }
32891
+ const ctx = buildContextSafely();
32892
+ if (!ctx || !existsSync10(ctx.paths.dbPath)) {
32893
+ process.stderr.write("[caveat:hook] auto reindex skipped: index database does not exist\n");
32894
+ return;
32895
+ }
32896
+ const lock = acquireReindexLock(ctx.caveatHome);
32897
+ if (!lock) return;
32898
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
32899
+ let db;
32900
+ try {
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
+ }
32908
+ db = openDb({ path: ctx.paths.dbPath, logger: silentLogger });
32909
+ const result = reindexAllSources({ db, paths: ctx.paths, logger: silentLogger, keyProvider });
32910
+ writeDigestMarker(ctx.caveatHome, digest);
32911
+ writeLastReindex(ctx.caveatHome, {
32912
+ startedAt,
32913
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
32914
+ perSource: result.perSource
32915
+ });
32916
+ } catch (err) {
32917
+ const msg = err instanceof Error ? err.message : String(err);
32918
+ process.stderr.write(`[caveat:hook] reindex error: ${msg}
32919
+ `);
32920
+ writeLastReindex(ctx.caveatHome, {
32921
+ startedAt,
32922
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
32923
+ error: msg
32924
+ });
32925
+ } finally {
32926
+ db?.close();
32927
+ try {
32928
+ releaseReindexLock(lock);
32929
+ } catch (err) {
32930
+ const msg = err instanceof Error ? err.message : String(err);
32931
+ process.stderr.write(`[caveat:hook] reindex lock release error: ${msg}
32932
+ `);
32933
+ }
32934
+ }
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
+ }
32024
32955
  function buildToolErrorReminder(job, hits) {
32025
32956
  const base = toolErrorReminderText(hits);
32026
32957
  const mode = hookCodexSidecarMode();
32027
32958
  if (mode === "off") return base;
32028
32959
  const projectRoot = process.cwd();
32029
- const hasSidecarConfig = existsSync6(join9(projectRoot, ".codex-sidecar.yml"));
32960
+ const hasSidecarConfig = existsSync10(join12(projectRoot, ".codex-sidecar.yml"));
32030
32961
  if (mode === "auto" && !hasSidecarConfig) return base;
32031
32962
  const advisory = runCodexSidecarAdvisory({
32032
32963
  searchText: job.searchText,
@@ -32036,21 +32967,20 @@ function buildToolErrorReminder(job, hits) {
32036
32967
  "A Claude Code tool just returned an error.",
32037
32968
  "Use the provided Caveat context to give concise next-step advice.",
32038
32969
  "Do not tell Claude to search Caveat again unless the context is insufficient."
32039
- ].join(" ")
32970
+ ].join(" "),
32971
+ additionalContext: job.additionalContext
32040
32972
  });
32041
32973
  if (advisory.status === "ok") {
32042
32974
  return [
32043
32975
  base,
32044
32976
  "",
32045
- "[caveat:codex-sidecar] Codex advisory:",
32046
- advisory.summary,
32047
- advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
32977
+ formatCodexSidecarAdvisory(advisory)
32048
32978
  ].filter(Boolean).join("\n");
32049
32979
  }
32050
32980
  return [
32051
32981
  base,
32052
32982
  "",
32053
- `[caveat:codex-sidecar] advisory unavailable: ${advisory.message}`
32983
+ formatCodexSidecarAdvisory(advisory)
32054
32984
  ].join("\n");
32055
32985
  }
32056
32986
  function hookCodexSidecarMode() {
@@ -32063,7 +32993,7 @@ function buildStopReminder(signals, related) {
32063
32993
  const mode = hookCodexSidecarMode();
32064
32994
  if (mode === "off") return base;
32065
32995
  const projectRoot = process.cwd();
32066
- const hasSidecarConfig = existsSync6(join9(projectRoot, ".codex-sidecar.yml"));
32996
+ const hasSidecarConfig = existsSync10(join12(projectRoot, ".codex-sidecar.yml"));
32067
32997
  if (mode === "auto" && !hasSidecarConfig) return base;
32068
32998
  const advisory = runCodexSidecarAdvisory({
32069
32999
  searchText: struggleSearchText(signals),
@@ -32071,113 +33001,46 @@ function buildStopReminder(signals, related) {
32071
33001
  projectRoot,
32072
33002
  prompt: [
32073
33003
  "A Claude Code session is ending after objective struggle signals.",
32074
- "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.",
32075
33005
  "Be concise and preserve Caveat visibility rules."
32076
- ].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
+ })
32077
33016
  });
32078
33017
  if (advisory.status === "ok") {
32079
33018
  return [
32080
33019
  base,
32081
33020
  "",
32082
- "[caveat:codex-sidecar] Codex advisory:",
32083
- advisory.summary,
32084
- advisory.rawEventLogRef ? `rawEventLogRef: ${advisory.rawEventLogRef}` : ""
33021
+ formatCodexSidecarAdvisory(advisory)
32085
33022
  ].filter(Boolean).join("\n");
32086
33023
  }
32087
33024
  return [
32088
33025
  base,
32089
33026
  "",
32090
- `[caveat:codex-sidecar] advisory unavailable: ${advisory.message}`
33027
+ formatCodexSidecarAdvisory(advisory)
32091
33028
  ].join("\n");
32092
33029
  }
32093
- function compactFailureMessage(message) {
32094
- const singleLine = message.replace(/\s+/g, " ").trim();
32095
- if (!singleLine) return "unknown error";
32096
- return singleLine.length > 220 ? `${singleLine.slice(0, 220)}...` : singleLine;
32097
- }
32098
- function sidecarFailureDetail(output) {
32099
- const protocol = output.match(/PROTOCOL_ERROR:[^"\r\n]+/);
32100
- if (protocol) return compactFailureMessage(protocol[0]);
32101
- const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
32102
- const diagnostic = lines.find(
32103
- (line) => !line.startsWith("[caveat] [codex-sidecar] codex-sidecar ")
32104
- );
32105
- return compactFailureMessage(diagnostic ?? output);
32106
- }
32107
- function runCodexSidecarAdvisory(input) {
32108
- const cliScript = process.argv[1];
32109
- if (!cliScript) {
32110
- return { status: "failed", message: "current Caveat CLI script path is unavailable" };
33030
+ async function runHook(name, arg) {
33031
+ try {
33032
+ sweepStaleWorkerDirs();
33033
+ } catch {
33034
+ process.stderr.write("[caveat:hook] worker stale cleanup failed\n");
32111
33035
  }
32112
- const args = [
32113
- ...process.execArgv,
32114
- "--disable-warning=ExperimentalWarning",
32115
- cliScript,
32116
- "codex-sidecar",
32117
- "run",
32118
- "explore",
32119
- input.prompt,
32120
- "--preset",
32121
- "advisory",
32122
- "--query",
32123
- input.searchText,
32124
- "--limit",
32125
- String(Math.max(1, input.limit)),
32126
- "--host-agent",
32127
- "claude",
32128
- "--availability",
32129
- "operational"
32130
- ];
32131
- const resultDir = mkdtempSync(join9(tmpdir(), "caveat-codex-advisory-"));
32132
- const resultFile = join9(resultDir, "result.json");
32133
- args.push("--save-result", resultFile);
32134
- const nodeCli = process.env.CAVEAT_CODEX_SIDECAR_NODE_CLI;
32135
- if (nodeCli) {
32136
- args.push("--node-cli", nodeCli);
32137
- } else {
32138
- const command = process.env.CAVEAT_CODEX_SIDECAR_COMMAND;
32139
- if (command) args.push("--command", command);
33036
+ if (name === "reindex") {
33037
+ await runReindexWorker();
33038
+ process.exit(0);
32140
33039
  }
32141
- try {
32142
- const result = spawnSync2(process.execPath, args, {
32143
- cwd: input.projectRoot,
32144
- encoding: "utf-8",
32145
- timeout: Number(process.env.CAVEAT_HOOK_CODEX_SIDECAR_TIMEOUT_MS ?? 12e4),
32146
- maxBuffer: 10 * 1024 * 1024
32147
- });
32148
- if (result.error) {
32149
- return { status: "failed", message: compactFailureMessage(result.error.message) };
32150
- }
32151
- if (result.status !== 0) {
32152
- const detail = (result.stderr || result.stdout || `exit ${result.status}`).trim();
32153
- return {
32154
- status: "failed",
32155
- message: `sidecar command failed: ${sidecarFailureDetail(detail)}`
32156
- };
32157
- }
32158
- const parsed = JSON.parse(readFileSync3(resultFile, "utf-8"));
32159
- if (parsed.status !== "ok" || typeof parsed.summary !== "string") {
32160
- return {
32161
- status: "failed",
32162
- message: compactFailureMessage(`unexpected SidecarResult status: ${String(parsed.status)}`)
32163
- };
32164
- }
32165
- return {
32166
- status: "ok",
32167
- summary: parsed.summary,
32168
- ...typeof parsed.rawEventLogRef === "string" ? { rawEventLogRef: parsed.rawEventLogRef } : {}
32169
- };
32170
- } catch (err) {
32171
- const msg = err instanceof Error ? err.message : String(err);
32172
- return {
32173
- status: "failed",
32174
- message: compactFailureMessage(`invalid SidecarResult JSON: ${msg}`)
32175
- };
32176
- } finally {
32177
- rmSync(resultDir, { recursive: true, force: true });
33040
+ if (name === "autosync") {
33041
+ await runAutoSyncWorker();
33042
+ process.exit(0);
32178
33043
  }
32179
- }
32180
- async function runHook(name, arg) {
32181
33044
  if (name === "worker") {
32182
33045
  if (!arg) process.exit(0);
32183
33046
  await runWorker(arg);
@@ -32197,7 +33060,7 @@ async function runHook(name, arg) {
32197
33060
  const contexts = name === "stop" ? [] : drainForSession(sessionId);
32198
33061
  if (name === "user-prompt-submit") {
32199
33062
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
32200
- const hits = searchCaveatsFromTextSafely(prompt);
33063
+ const hits = searchCaveatsFromTextSafely(prompt, "user_prompt");
32201
33064
  if (hits.length > 0) {
32202
33065
  contexts.push(userPromptSubmitReminderText(hits));
32203
33066
  }
@@ -32219,21 +33082,46 @@ async function runHook(name, arg) {
32219
33082
  payload.tool_response ?? payload.toolResponse ?? payload.error ?? payload
32220
33083
  );
32221
33084
  if (errText) {
32222
- 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 } : {} });
32223
33092
  }
32224
33093
  process.exit(0);
32225
33094
  }
32226
33095
  if (name === "stop") {
32227
- try {
32228
- const ctx = buildContextSafely();
32229
- if (ctx) maybeSweepPendingDirs(ctx.caveatHome);
32230
- } catch {
33096
+ const ctx = buildContextSafely();
33097
+ if (ctx) {
33098
+ try {
33099
+ maybeSweepPendingDirs(ctx.caveatHome);
33100
+ } catch (err) {
33101
+ const msg = err instanceof Error ? err.message : String(err);
33102
+ process.stderr.write(`[caveat:hook] pending sweep error: ${msg}
33103
+ `);
33104
+ }
33105
+ try {
33106
+ maybeTriggerAutoReindex(ctx);
33107
+ } catch (err) {
33108
+ const msg = err instanceof Error ? err.message : String(err);
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}
33117
+ `);
33118
+ }
32231
33119
  }
32232
33120
  if (payload.stop_hook_active === true) process.exit(0);
32233
33121
  const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
32234
33122
  const signals = transcriptPath ? loadSignalsSafely(transcriptPath) : null;
32235
33123
  if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
32236
- const related = searchCaveatsFromTextSafely(struggleSearchText(signals));
33124
+ const related = searchCaveatsFromTextSafely(struggleSearchText(signals), "stop");
32237
33125
  queueStopForSession(sessionId, signals, related);
32238
33126
  process.exit(0);
32239
33127
  }
@@ -32243,236 +33131,11 @@ async function runHook(name, arg) {
32243
33131
  }
32244
33132
 
32245
33133
  // src/commands/codexHookCmd.ts
32246
- import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
32247
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
32248
- import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
32249
- 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";
32250
33138
  import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
32251
-
32252
- // src/codexHookInstall.ts
32253
- import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
32254
- import { dirname as dirname5, join as join10 } from "node:path";
32255
- function quote2(p) {
32256
- return p.includes(" ") ? `"${p}"` : p;
32257
- }
32258
- function hookCommand2(nodePath, cliScriptPath, event) {
32259
- return `${quote2(nodePath)} ${quote2(cliScriptPath)} codex-hook ${event}`;
32260
- }
32261
- function eventCommandFragment(event) {
32262
- return `codex-hook ${event}`;
32263
- }
32264
- function isSameHookCommand2(actual, expected) {
32265
- return actual === expected || actual.endsWith(` ${expected}`);
32266
- }
32267
- function isCaveatCodexHookCommand(actual, event) {
32268
- const lower = actual.toLowerCase();
32269
- return lower.includes("caveat") && actual.includes(eventCommandFragment(event));
32270
- }
32271
- function hasCaveatHook(hooksJson, event, fragment) {
32272
- return hooksJson.hooks?.[event]?.some(
32273
- (entry) => entry.hooks?.some((h) => h.command.includes(fragment))
32274
- ) ?? false;
32275
- }
32276
- function readHooks(path) {
32277
- if (!existsSync7(path)) return {};
32278
- return JSON.parse(readFileSync4(path, "utf-8"));
32279
- }
32280
- function writeJsonWithBackup(path, value) {
32281
- const dir = dirname5(path);
32282
- if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
32283
- let backupPath = "";
32284
- if (existsSync7(path)) {
32285
- backupPath = `${path}.caveat-backup-${Date.now()}`;
32286
- copyFileSync2(path, backupPath);
32287
- }
32288
- writeFileSync4(path, `${JSON.stringify(value, null, 2)}
32289
- `, "utf-8");
32290
- return backupPath;
32291
- }
32292
- function upsertHook2(hooksJson, event, command, subcommand) {
32293
- hooksJson.hooks ??= {};
32294
- const list = hooksJson.hooks[event] ??= [];
32295
- for (const entry of list) {
32296
- for (const hook of entry.hooks ?? []) {
32297
- if (isSameHookCommand2(hook.command, command)) return "unchanged";
32298
- if (isCaveatCodexHookCommand(hook.command, subcommand)) {
32299
- hook.command = command;
32300
- hook.timeoutSec = 5;
32301
- hook.async = false;
32302
- hook.statusMessage = null;
32303
- return "added";
32304
- }
32305
- }
32306
- }
32307
- list.push({
32308
- hooks: [
32309
- {
32310
- type: "command",
32311
- command,
32312
- timeoutSec: 5,
32313
- async: false,
32314
- statusMessage: null
32315
- }
32316
- ]
32317
- });
32318
- return "added";
32319
- }
32320
- function removeHook2(hooksJson, event, command, subcommand) {
32321
- const list = hooksJson.hooks?.[event];
32322
- if (!list) return false;
32323
- const before = list.length;
32324
- const filtered = list.filter(
32325
- (entry) => !entry.hooks?.some(
32326
- (h) => isSameHookCommand2(h.command, command) || isCaveatCodexHookCommand(h.command, subcommand)
32327
- )
32328
- );
32329
- if (filtered.length === before) return false;
32330
- hooksJson.hooks[event] = filtered;
32331
- return true;
32332
- }
32333
- function enableCodexHooksFeature(raw) {
32334
- if (/^\s*codex_hooks\s*=\s*true\s*$/m.test(raw)) return { text: raw, changed: false };
32335
- const lines = raw.split(/\r?\n/);
32336
- const featuresStart = lines.findIndex((line) => /^\s*\[features]\s*$/.test(line));
32337
- if (featuresStart === -1) {
32338
- const prefix = raw.trimEnd();
32339
- const text = `${prefix}${prefix ? "\n\n" : ""}[features]
32340
- codex_hooks = true
32341
- `;
32342
- return { text, changed: true };
32343
- }
32344
- let insertAt = featuresStart + 1;
32345
- for (let i = featuresStart + 1; i < lines.length; i += 1) {
32346
- if (/^\s*\[.+]\s*$/.test(lines[i])) {
32347
- break;
32348
- }
32349
- if (/^\s*codex_hooks\s*=/.test(lines[i])) {
32350
- lines[i] = "codex_hooks = true";
32351
- return { text: `${lines.join("\n").trimEnd()}
32352
- `, changed: true };
32353
- }
32354
- }
32355
- lines.splice(insertAt, 0, "codex_hooks = true");
32356
- return { text: `${lines.join("\n").trimEnd()}
32357
- `, changed: true };
32358
- }
32359
- function writeConfigWithBackup(path, text) {
32360
- const dir = dirname5(path);
32361
- if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
32362
- let backupPath = "";
32363
- if (existsSync7(path)) {
32364
- backupPath = `${path}.caveat-backup-${Date.now()}`;
32365
- copyFileSync2(path, backupPath);
32366
- }
32367
- writeFileSync4(path, text, "utf-8");
32368
- return backupPath;
32369
- }
32370
- function installCodexHooks(opts) {
32371
- const hooksPath = join10(opts.codexHome, "hooks.json");
32372
- const configPath = join10(opts.codexHome, "config.toml");
32373
- const hooksJson = readHooks(hooksPath);
32374
- const userPromptSubmit = upsertHook2(
32375
- hooksJson,
32376
- "UserPromptSubmit",
32377
- hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
32378
- "user-prompt-submit"
32379
- );
32380
- const postToolUse = upsertHook2(
32381
- hooksJson,
32382
- "PostToolUse",
32383
- hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
32384
- "post-tool-use"
32385
- );
32386
- const stop = upsertHook2(
32387
- hooksJson,
32388
- "Stop",
32389
- hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
32390
- "stop"
32391
- );
32392
- const rawConfig = existsSync7(configPath) ? readFileSync4(configPath, "utf-8") : "";
32393
- const enabled = enableCodexHooksFeature(rawConfig);
32394
- const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
32395
- let backupPath;
32396
- let configBackupPath;
32397
- if (opts.dryRun) {
32398
- opts.logger.info(`[dry-run] would update ${hooksPath}`);
32399
- opts.logger.info(`[dry-run] would ensure [features].codex_hooks = true in ${configPath}`);
32400
- } else {
32401
- if (anyHookAdded) {
32402
- const backup = writeJsonWithBackup(hooksPath, hooksJson);
32403
- if (backup) backupPath = backup;
32404
- }
32405
- if (enabled.changed) {
32406
- const backup = writeConfigWithBackup(configPath, enabled.text);
32407
- if (backup) configBackupPath = backup;
32408
- }
32409
- }
32410
- return {
32411
- hooks: { userPromptSubmit, postToolUse, stop },
32412
- feature: enabled.changed ? "enabled" : "unchanged",
32413
- backupPath,
32414
- configBackupPath
32415
- };
32416
- }
32417
- function uninstallCodexHooks(opts) {
32418
- const hooksPath = join10(opts.codexHome, "hooks.json");
32419
- const hooksJson = readHooks(hooksPath);
32420
- const userPromptSubmitRemoved = removeHook2(
32421
- hooksJson,
32422
- "UserPromptSubmit",
32423
- hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
32424
- "user-prompt-submit"
32425
- );
32426
- const postToolUseRemoved = removeHook2(
32427
- hooksJson,
32428
- "PostToolUse",
32429
- hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
32430
- "post-tool-use"
32431
- );
32432
- const stopRemoved = removeHook2(
32433
- hooksJson,
32434
- "Stop",
32435
- hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
32436
- "stop"
32437
- );
32438
- let backupPath;
32439
- if (opts.dryRun) {
32440
- opts.logger.info(`[dry-run] would remove Caveat Codex hooks from ${hooksPath}`);
32441
- } else if (userPromptSubmitRemoved || postToolUseRemoved || stopRemoved) {
32442
- const backup = writeJsonWithBackup(hooksPath, hooksJson);
32443
- if (backup) backupPath = backup;
32444
- }
32445
- return {
32446
- hooks: {
32447
- userPromptSubmit: userPromptSubmitRemoved ? "added" : "unchanged",
32448
- postToolUse: postToolUseRemoved ? "added" : "unchanged",
32449
- stop: stopRemoved ? "added" : "unchanged"
32450
- },
32451
- feature: "unchanged",
32452
- backupPath
32453
- };
32454
- }
32455
- function detectCodexHookInstallation(codexHome) {
32456
- const hooksPath = join10(codexHome, "hooks.json");
32457
- const hooksJson = readHooks(hooksPath);
32458
- const hooks = {
32459
- userPromptSubmit: hasCaveatHook(
32460
- hooksJson,
32461
- "UserPromptSubmit",
32462
- eventCommandFragment("user-prompt-submit")
32463
- ),
32464
- postToolUse: hasCaveatHook(hooksJson, "PostToolUse", eventCommandFragment("post-tool-use")),
32465
- stop: hasCaveatHook(hooksJson, "Stop", eventCommandFragment("stop"))
32466
- };
32467
- const count = Object.values(hooks).filter(Boolean).length;
32468
- return {
32469
- installation: count === 0 ? "not-installed" : count === 3 ? "installed" : "partial",
32470
- hooksPath,
32471
- hooks
32472
- };
32473
- }
32474
-
32475
- // src/commands/codexHookCmd.ts
32476
33139
  var silentLogger2 = {
32477
33140
  info: () => {
32478
33141
  },
@@ -32512,31 +33175,44 @@ function buildContextSafely2() {
32512
33175
  return null;
32513
33176
  }
32514
33177
  }
32515
- function searchCaveatsFromTextSafely2(text) {
33178
+ function searchCaveatsFromTextSafely2(text, surface) {
32516
33179
  if (!text) return [];
32517
33180
  let db;
33181
+ let caveatHome;
33182
+ let hits;
32518
33183
  try {
32519
33184
  const ctx = buildContextSafely2();
32520
- if (!ctx || !existsSync8(ctx.paths.dbPath)) return [];
33185
+ if (!ctx || !existsSync11(ctx.paths.dbPath)) return [];
33186
+ caveatHome = ctx.caveatHome;
32521
33187
  db = openDb({ path: ctx.paths.dbPath });
32522
- const hits = findCaveatsForPrompt(db, text, {
33188
+ hits = findCaveatsForPrompt(db, text, {
32523
33189
  selfIdentity: defaultSelfIdentityTokens()
32524
33190
  });
32525
- if (hits.length > 0) {
32526
- try {
32527
- markHit(db, hits);
32528
- } catch (err) {
32529
- const msg = err instanceof Error ? err.message : String(err);
32530
- process.stderr.write(`[caveat:codex-hook] markHit error: ${msg}
32531
- `);
32532
- }
32533
- }
32534
- return hits;
32535
33191
  } catch (err) {
32536
33192
  const msg = err instanceof Error ? err.message : String(err);
32537
33193
  process.stderr.write(`[caveat:codex-hook] search error: ${msg}
32538
33194
  `);
32539
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;
32540
33216
  } finally {
32541
33217
  db?.close();
32542
33218
  }
@@ -32586,10 +33262,10 @@ function processExitCodeFromText(text) {
32586
33262
  return m ? Number(m[1]) : null;
32587
33263
  }
32588
33264
  function transcriptToolOutput(transcriptPath, toolUseId) {
32589
- if (!transcriptPath || !toolUseId || !existsSync8(transcriptPath)) return null;
33265
+ if (!transcriptPath || !toolUseId || !existsSync11(transcriptPath)) return null;
32590
33266
  let raw = "";
32591
33267
  try {
32592
- raw = readFileSync5(transcriptPath, "utf-8");
33268
+ raw = readFileSync6(transcriptPath, "utf-8");
32593
33269
  } catch {
32594
33270
  return null;
32595
33271
  }
@@ -32685,7 +33361,10 @@ function codexContextOutput(text, eventName = "UserPromptSubmit") {
32685
33361
  function drainForSession2(sessionId) {
32686
33362
  const ctx = buildContextSafely2();
32687
33363
  if (!ctx) return [];
32688
- return drainPendingReminders(ctx.caveatHome, sessionId);
33364
+ return [
33365
+ ...drainPendingReminders(ctx.caveatHome, sessionId),
33366
+ ...drainGlobalPendingReminders(ctx.caveatHome)
33367
+ ];
32689
33368
  }
32690
33369
  function codexContextDedupeKey(text) {
32691
33370
  if (text.startsWith(CODEX_STOP_REMINDER_PREFIX)) return "codex-stop-reminder";
@@ -32727,20 +33406,20 @@ function stopSignalKey2(signals, related) {
32727
33406
  return createHash2("sha256").update(body).digest("hex");
32728
33407
  }
32729
33408
  function stopStatePath2(caveatHome, sessionId) {
32730
- return join11(caveatHome, CODEX_STOP_STATE_DIR, `${sanitizeCodexStateId(sessionId)}.txt`);
33409
+ return join13(caveatHome, CODEX_STOP_STATE_DIR, `${sanitizeCodexStateId(sessionId)}.txt`);
32731
33410
  }
32732
33411
  function wasStopReminderQueued2(caveatHome, sessionId, key) {
32733
33412
  const path = stopStatePath2(caveatHome, sessionId);
32734
33413
  try {
32735
- return readFileSync5(path, "utf-8") === key;
33414
+ return readFileSync6(path, "utf-8") === key;
32736
33415
  } catch {
32737
33416
  return false;
32738
33417
  }
32739
33418
  }
32740
33419
  function markStopReminderQueued2(caveatHome, sessionId, key) {
32741
33420
  const path = stopStatePath2(caveatHome, sessionId);
32742
- mkdirSync6(join11(caveatHome, CODEX_STOP_STATE_DIR), { recursive: true });
32743
- writeFileSync5(path, key, "utf-8");
33421
+ mkdirSync6(join13(caveatHome, CODEX_STOP_STATE_DIR), { recursive: true });
33422
+ writeFileSync6(path, key, "utf-8");
32744
33423
  }
32745
33424
  function queueStopForSession2(sessionId, signals, related) {
32746
33425
  const ctx = buildContextSafely2();
@@ -32781,7 +33460,7 @@ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
32781
33460
  }
32782
33461
  }
32783
33462
  if (!knownError && job.allowSymptomOnly !== true) return;
32784
- const hits = searchCaveatsFromTextSafely2(searchText);
33463
+ const hits = searchCaveatsFromTextSafely2(searchText, "tool_error");
32785
33464
  if (hits.length === 0) return;
32786
33465
  const ctx = buildContextSafely2();
32787
33466
  if (!ctx) return;
@@ -32793,12 +33472,12 @@ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
32793
33472
  async function runCodexWorker(workFile) {
32794
33473
  let raw;
32795
33474
  try {
32796
- raw = readFileSync5(workFile, "utf-8");
33475
+ raw = readFileSync6(workFile, "utf-8");
32797
33476
  } catch {
32798
33477
  process.exit(0);
32799
33478
  }
32800
33479
  try {
32801
- unlinkSync2(workFile);
33480
+ unlinkSync(workFile);
32802
33481
  } catch {
32803
33482
  }
32804
33483
  let job;
@@ -32810,8 +33489,8 @@ async function runCodexWorker(workFile) {
32810
33489
  await processCodexWorkerJob(job);
32811
33490
  process.exit(0);
32812
33491
  }
32813
- function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join11(homedir3(), ".codex")) {
32814
- const features = spawnSync3("codex", ["features", "list"], {
33492
+ function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join13(homedir3(), ".codex")) {
33493
+ const features = spawnSync5("codex", ["features", "list"], {
32815
33494
  encoding: "utf-8",
32816
33495
  maxBuffer: 1024 * 1024
32817
33496
  });
@@ -32856,7 +33535,7 @@ async function runCodexHook(name, arg) {
32856
33535
  if (name === "user-prompt-submit") {
32857
33536
  const contexts = sessionId ? drainForSession2(sessionId) : [];
32858
33537
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
32859
- const hits = searchCaveatsFromTextSafely2(prompt);
33538
+ const hits = searchCaveatsFromTextSafely2(prompt, "user_prompt");
32860
33539
  if (hits.length > 0) {
32861
33540
  contexts.push(userPromptSubmitReminderText(hits));
32862
33541
  }
@@ -32873,11 +33552,35 @@ async function runCodexHook(name, arg) {
32873
33552
  process.exit(0);
32874
33553
  }
32875
33554
  if (name === "stop") {
33555
+ try {
33556
+ const ctx = buildContextSafely2();
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
+ }
33574
+ } catch (err) {
33575
+ const msg = err instanceof Error ? err.message : String(err);
33576
+ process.stderr.write(`[caveat:codex-hook] auto reindex trigger error: ${msg}
33577
+ `);
33578
+ }
32876
33579
  if (payload.stop_hook_active === true) process.exit(0);
32877
33580
  const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
32878
33581
  const signals = transcriptPath ? loadSignalsSafely2(transcriptPath) : null;
32879
33582
  if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
32880
- const related = searchCaveatsFromTextSafely2(struggleSearchText(signals));
33583
+ const related = searchCaveatsFromTextSafely2(struggleSearchText(signals), "stop");
32881
33584
  if (sessionId) queueStopForSession2(sessionId, signals, related);
32882
33585
  process.exit(0);
32883
33586
  }
@@ -32887,19 +33590,15 @@ async function runCodexHook(name, arg) {
32887
33590
  }
32888
33591
 
32889
33592
  // src/commands/pull.ts
32890
- import { existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
32891
- import { join as join12 } from "node:path";
33593
+ import { existsSync as existsSync12 } from "node:fs";
32892
33594
  async function runPull(ctx) {
32893
- if (!existsSync9(ctx.paths.communityDir)) {
33595
+ const hasCommunityDir = existsSync12(ctx.paths.communityDir);
33596
+ if (!hasCommunityDir) {
32894
33597
  ctx.logger.info(
32895
33598
  "no community repos yet \u2014 add one with `caveat community add <github-url>`."
32896
33599
  );
32897
- return;
32898
33600
  }
32899
- const pulls = await communityPull({
32900
- communityDir: ctx.paths.communityDir,
32901
- logger: ctx.logger
32902
- });
33601
+ const pulls = hasCommunityDir ? await communityPull({ communityDir: ctx.paths.communityDir, logger: ctx.logger }) : [];
32903
33602
  for (const p of pulls) {
32904
33603
  if (p.status === "ok") {
32905
33604
  ctx.logger.info(`${p.handle}: pulled`);
@@ -32909,29 +33608,88 @@ async function runPull(ctx) {
32909
33608
  }
32910
33609
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
32911
33610
  try {
32912
- rebuildAll(db);
32913
- if (existsSync9(ctx.paths.entriesDir)) {
32914
- const own = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
32915
- ctx.logger.info(`own: +${own.added}`);
32916
- }
32917
- for (const entry of readdirSync4(ctx.paths.communityDir, { withFileTypes: true })) {
32918
- if (!entry.isDirectory()) continue;
32919
- const source = `community/${entry.name}`;
32920
- const root = join12(ctx.paths.communityDir, entry.name, "entries");
32921
- if (!existsSync9(root)) continue;
32922
- const scan = scanSource({ db, source, entriesRoot: root });
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 });
33617
+ for (const [source, scan] of Object.entries(result.perSource)) {
32923
33618
  ctx.logger.info(`${source}: +${scan.added}`);
32924
33619
  }
33620
+ writeDigestMarker(ctx.caveatHome, computeEntriesDigest(ctx.paths));
32925
33621
  } finally {
32926
33622
  db.close();
32927
33623
  }
32928
33624
  }
33625
+ function errorMessage5(err) {
33626
+ return err instanceof Error ? err.message : String(err);
33627
+ }
33628
+
33629
+ // src/commands/sync.ts
33630
+ async function defaultPrivateRepoUrl(opts, deps) {
33631
+ return defaultGitHubRepoUrl({
33632
+ repositoryName: "Caveat-Private",
33633
+ visibility: "private",
33634
+ yes: opts.yes,
33635
+ retryCommand: "caveat sync --init <url>",
33636
+ ...deps
33637
+ });
33638
+ }
33639
+ async function runSync(ctx, opts, dependencies = {}) {
33640
+ try {
33641
+ const ghRunner = dependencies.ghRunner ?? runGh;
33642
+ const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
33643
+ const confirm = dependencies.confirm ?? askOnce;
33644
+ const initValue = opts.init;
33645
+ if (initValue !== void 0 && initValue !== false) {
33646
+ const url2 = opts.repo ?? (typeof initValue === "string" ? initValue : await defaultPrivateRepoUrl(opts, { ghRunner, isTty, confirm }));
33647
+ const result2 = await initOwnSync({
33648
+ ownDir: ctx.paths.knowledgeRepo,
33649
+ url: url2,
33650
+ caveatHome: ctx.caveatHome,
33651
+ paths: ctx.paths,
33652
+ logger: ctx.logger,
33653
+ trustRemotePrivate: opts.trustRemotePrivate
33654
+ });
33655
+ ctx.logger.info(
33656
+ result2.remoteWasEmpty ? `initialized private sync remote: ${url2}` : `checked out existing private remote: ${url2} (branch ${result2.branch})`
33657
+ );
33658
+ return;
33659
+ }
33660
+ if (opts.repo) throw new Error("--repo requires --init");
33661
+ const result = await syncOwn({
33662
+ ownDir: ctx.paths.knowledgeRepo,
33663
+ caveatHome: ctx.caveatHome,
33664
+ paths: ctx.paths,
33665
+ logger: ctx.logger,
33666
+ trustRemotePrivate: opts.trustRemotePrivate,
33667
+ dryRun: opts.dryRun
33668
+ });
33669
+ if (result.dryRun) {
33670
+ ctx.logger.info(
33671
+ `[dry-run] ${result.changedFiles} local change(s) pending; remote ${result.pushUrls.join(", ")} (${result.probe.kind})`
33672
+ );
33673
+ } else {
33674
+ const rebased = result.pulled ? ", rebased onto remote" : "";
33675
+ ctx.logger.info(`synced ${result.branch} \u2192 origin (${result.changedFiles} local change(s)${rebased})`);
33676
+ try {
33677
+ resetAutoSyncFailureState(ctx.caveatHome);
33678
+ } catch {
33679
+ }
33680
+ }
33681
+ } catch (err) {
33682
+ const message = err instanceof Error ? err.message : String(err);
33683
+ ctx.logger.error(message);
33684
+ process.exitCode = 1;
33685
+ }
33686
+ }
32929
33687
 
32930
33688
  // src/commands/codexSidecar.ts
32931
- import { spawnSync as spawnSync4 } from "node:child_process";
32932
- import { mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
32933
- import { tmpdir as tmpdir3 } from "node:os";
32934
- import { dirname as dirname6, join as join13 } 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";
32935
33693
  import { cwd, exit } from "node:process";
32936
33694
  function runCodexSidecarDiagnostics(logger, opts) {
32937
33695
  const plan = buildCodexSidecarDiagnosticsCommand({
@@ -32963,8 +33721,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
32963
33721
  process.stdout.write(JSON.stringify({ status: "skipped", decision }, null, 2) + "\n");
32964
33722
  exit(0);
32965
33723
  }
32966
- const contextDir = mkdtempSync2(join13(tmpdir3(), "caveat-sidecar-context-"));
32967
- const contextFile = join13(contextDir, "context.json");
33724
+ const contextDir = mkdtempSync3(join14(tmpdir4(), "caveat-sidecar-context-"));
33725
+ const contextFile = join14(contextDir, "context.json");
32968
33726
  let status = 1;
32969
33727
  try {
32970
33728
  const blocks = collectCaveatContextBlocks(ctx, {
@@ -32973,7 +33731,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
32973
33731
  source: opts.source,
32974
33732
  visibility: opts.visibility
32975
33733
  });
32976
- 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");
32977
33736
  const plan = buildCodexSidecarRunCommand({
32978
33737
  workflow,
32979
33738
  projectRoot: opts.project ?? cwd(),
@@ -32984,10 +33743,94 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
32984
33743
  });
32985
33744
  status = executePlan(ctx.logger, plan.command, plan.args, { saveResult: opts.saveResult });
32986
33745
  } finally {
32987
- rmSync2(contextDir, { recursive: true, force: true });
33746
+ rmSync3(contextDir, { recursive: true, force: true });
32988
33747
  }
32989
33748
  exit(status);
32990
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
+ }
32991
33834
  function runCodexSidecarWorkSmoke(ctx, prompt, opts) {
32992
33835
  const plan = buildCodexSidecarRunCommand({
32993
33836
  workflow: "work",
@@ -33028,7 +33871,7 @@ function defaultPreset(workflow) {
33028
33871
  }
33029
33872
  function executePlan(logger, command, args, options = {}) {
33030
33873
  logger.info(`[codex-sidecar] ${command} ${args.map(shellDisplayQuote).join(" ")}`);
33031
- const result = spawnSync4(command, args, { encoding: "utf-8", stdio: "pipe" });
33874
+ const result = spawnSync6(command, args, { encoding: "utf-8", stdio: "pipe" });
33032
33875
  if (result.stdout) process.stdout.write(result.stdout);
33033
33876
  if (result.stderr) process.stderr.write(result.stderr);
33034
33877
  if (options.saveResult && result.stdout) {
@@ -33042,8 +33885,8 @@ function executePlan(logger, command, args, options = {}) {
33042
33885
  }
33043
33886
  function saveStructuredResult(path, stdout) {
33044
33887
  const parsed = JSON.parse(stdout);
33045
- mkdirSync7(dirname6(path), { recursive: true });
33046
- 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");
33047
33890
  }
33048
33891
  function shellDisplayQuote(value) {
33049
33892
  return /[\s"'$`]/.test(value) ? JSON.stringify(value) : value;
@@ -33131,10 +33974,13 @@ function runCommunityRemove(ctx, handle, opts) {
33131
33974
 
33132
33975
  // src/index.ts
33133
33976
  var program = new Command();
33977
+ function collectRepeatable(value, previous) {
33978
+ return [...previous, value];
33979
+ }
33134
33980
  program.name("caveat").description("External spec gotcha knowledge base CLI").version(CAVEAT_VERSION);
33135
33981
  program.command("init").description(
33136
33982
  "Initialize ~/.caveatrc.json, ~/.caveat/, and register Claude Code integration. Add knowledge sources later with `caveat community add <github-url>`."
33137
- ).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(
33138
33984
  "--pending-stale-days <days>",
33139
33985
  "sweep `<caveatHome>/pending/<sessionId>/` whose newest entry is older than this (default 7)",
33140
33986
  (raw) => {
@@ -33150,7 +33996,11 @@ program.command("init").description(
33150
33996
  await runInit(ctx, {
33151
33997
  skipClaude: opts.skipClaude,
33152
33998
  dryRun: opts.dryRun,
33153
- pendingStaleDays: opts.pendingStaleDays
33999
+ pendingStaleDays: opts.pendingStaleDays,
34000
+ sync: opts.sync,
34001
+ publishTarget: opts.publishTarget,
34002
+ yes: opts.yes,
34003
+ skipCodexHook: opts.skipCodexHook
33154
34004
  });
33155
34005
  }
33156
34006
  );
@@ -33158,9 +34008,9 @@ program.command("uninstall").description("Remove Claude Code MCP server and hook
33158
34008
  const ctx = buildContext(stdoutLogger);
33159
34009
  runUninstall(ctx, { dryRun: opts.dryRun });
33160
34010
  });
33161
- 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) => {
33162
34012
  const ctx = buildContext(stdoutLogger);
33163
- runIndex(ctx, { full: opts.full });
34013
+ await runIndex(ctx, { full: opts.full });
33164
34014
  });
33165
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(
33166
34016
  (query, opts) => {
@@ -33198,6 +34048,14 @@ program.command("pull").description(
33198
34048
  const ctx = buildContext(stdoutLogger);
33199
34049
  await runPull(ctx);
33200
34050
  });
34051
+ program.command("sync").description("Sync own Caveat entries to a private git remote").option("--init [url]", "initialize private remote sync (optionally with its URL)").option("--repo <url>", "private remote URL (requires --init)").option("--dry-run", "show pending own-repo changes without writing", false).option("--trust-remote-private", "continue when anonymous remote visibility cannot be determined", false).option("--yes", "approve creating the default GitHub private repository", false).action(async (opts) => {
34052
+ const ctx = buildContext(stdoutLogger);
34053
+ await runSync(ctx, opts);
34054
+ });
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) => {
34056
+ const ctx = buildContext(stdoutLogger);
34057
+ await runPublish(ctx, opts);
34058
+ });
33201
34059
  program.command("serve").description("Start the read-only web share portal").option("--port <n>", "port number", (v) => Number(v), 4242).action(async (opts) => {
33202
34060
  await runServe({ port: opts.port });
33203
34061
  });
@@ -33205,7 +34063,7 @@ program.command("mcp-server").description("Run the MCP stdio server (registered
33205
34063
  await runMcpServer();
33206
34064
  });
33207
34065
  program.command("hook <name> [arg]").description(
33208
- "Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker"
34066
+ "Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker | reindex | autosync"
33209
34067
  ).action(async (name, arg) => {
33210
34068
  await runHook(name, arg);
33211
34069
  });
@@ -33227,6 +34085,11 @@ codexHook.command("install").description("Install Caveat hooks into ~/.codex/hoo
33227
34085
  stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse}`);
33228
34086
  stdoutLogger.info(`Stop hook: ${result.hooks.stop}`);
33229
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
+ }
33230
34093
  if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);
33231
34094
  if (result.configBackupPath) {
33232
34095
  stdoutLogger.info(`config.toml backed up: ${result.configBackupPath}`);
@@ -33272,7 +34135,7 @@ codexSidecar.command("diagnostics").description("Run codex-sidecar diagnostics f
33272
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) => {
33273
34136
  runCodexSidecarSmoke(stdoutLogger, opts);
33274
34137
  });
33275
- 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(
33276
34139
  (workflow, prompt, opts) => {
33277
34140
  const normalized = workflow === "risk" ? "risk-check" : workflow;
33278
34141
  if (normalized !== "review" && normalized !== "explore" && normalized !== "opinion" && normalized !== "risk-check") {
@@ -33289,7 +34152,7 @@ codexSidecar.command("work-smoke [prompt]").description("Run codex_work in an is
33289
34152
  runCodexSidecarWorkSmoke(ctx, prompt, opts);
33290
34153
  }
33291
34154
  );
33292
- 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/)");
33293
34156
  community.command("add <url>").description("Shallow-clone a GitHub caveat repo into community/<handle>/").action(async (url2) => {
33294
34157
  const ctx = buildContext(stdoutLogger);
33295
34158
  await runCommunityAdd(ctx, url2);