sweet-search 2.5.2 → 2.5.3

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.
Files changed (155) hide show
  1. package/core/cli.js +24 -3
  2. package/core/graph/graph-expansion.js +215 -36
  3. package/core/graph/graph-extractor.js +196 -11
  4. package/core/graph/graph-search.js +395 -92
  5. package/core/graph/hcgs-generator.js +2 -1
  6. package/core/graph/index.js +2 -0
  7. package/core/graph/repo-map.js +28 -6
  8. package/core/graph/structural-answer-cues.js +168 -0
  9. package/core/graph/structural-callsite-hints.js +40 -0
  10. package/core/graph/structural-context-format.js +40 -0
  11. package/core/graph/structural-context.js +450 -0
  12. package/core/graph/structural-forward-push.js +156 -0
  13. package/core/graph/structural-header-context.js +19 -0
  14. package/core/graph/structural-importance.js +148 -0
  15. package/core/graph/structural-pagerank.js +197 -0
  16. package/core/graph/summary-manager.js +13 -9
  17. package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
  18. package/core/incremental-indexing/application/file-watcher.mjs +197 -0
  19. package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
  20. package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
  21. package/core/incremental-indexing/application/operator-cli.mjs +554 -0
  22. package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
  23. package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
  24. package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
  25. package/core/incremental-indexing/application/reconciler.mjs +477 -0
  26. package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
  27. package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
  28. package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
  29. package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
  30. package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
  31. package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
  32. package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
  33. package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
  34. package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
  35. package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
  36. package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
  37. package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
  38. package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
  39. package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
  40. package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
  41. package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
  42. package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
  43. package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
  44. package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
  45. package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
  46. package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
  47. package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
  48. package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
  49. package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
  50. package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
  51. package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
  52. package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
  53. package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
  54. package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
  55. package/core/indexing/admission-policy.js +139 -0
  56. package/core/indexing/artifact-builder.js +29 -12
  57. package/core/indexing/ast-chunker.js +107 -30
  58. package/core/indexing/dedup/exemplar-selector.js +19 -1
  59. package/core/indexing/gitignore-filter.js +223 -0
  60. package/core/indexing/incremental-tracker.js +99 -30
  61. package/core/indexing/index-codebase-v21.js +6 -5
  62. package/core/indexing/index-maintainer.mjs +698 -6
  63. package/core/indexing/indexer-ann.js +99 -15
  64. package/core/indexing/indexer-build.js +158 -45
  65. package/core/indexing/indexer-empty-baseline.js +80 -0
  66. package/core/indexing/indexer-manifest.js +66 -0
  67. package/core/indexing/indexer-phases.js +56 -23
  68. package/core/indexing/indexer-sparse-gram.js +54 -13
  69. package/core/indexing/indexer-utils.js +26 -208
  70. package/core/indexing/indexing-file-policy.js +32 -7
  71. package/core/indexing/maintainer-launcher.mjs +137 -0
  72. package/core/indexing/merkle-tracker.js +251 -244
  73. package/core/indexing/model-pool.js +46 -5
  74. package/core/infrastructure/code-graph-repository.js +758 -6
  75. package/core/infrastructure/code-graph-visibility.js +157 -0
  76. package/core/infrastructure/codebase-repository.js +100 -13
  77. package/core/infrastructure/config/search.js +1 -1
  78. package/core/infrastructure/db-utils.js +118 -0
  79. package/core/infrastructure/dedup-hashing.js +10 -13
  80. package/core/infrastructure/hardware-capability.js +17 -7
  81. package/core/infrastructure/index.js +8 -2
  82. package/core/infrastructure/language-patterns/maps.js +4 -1
  83. package/core/infrastructure/language-patterns/registry-core.js +56 -17
  84. package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
  85. package/core/infrastructure/language-patterns.js +69 -0
  86. package/core/infrastructure/model-registry.js +20 -0
  87. package/core/infrastructure/native-inference.js +7 -12
  88. package/core/infrastructure/native-resolver.js +52 -37
  89. package/core/infrastructure/native-sparse-gram.js +261 -20
  90. package/core/infrastructure/native-tokenizer.js +6 -15
  91. package/core/infrastructure/simd-distance.js +10 -16
  92. package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
  93. package/core/infrastructure/structural-alias-resolver.js +122 -0
  94. package/core/infrastructure/structural-candidate-ranker.js +34 -0
  95. package/core/infrastructure/structural-context-repository.js +472 -0
  96. package/core/infrastructure/structural-context-utils.js +51 -0
  97. package/core/infrastructure/structural-graph-signals.js +121 -0
  98. package/core/infrastructure/structural-qualified-resolution.js +15 -0
  99. package/core/infrastructure/structural-source-definitions.js +100 -0
  100. package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
  101. package/core/infrastructure/tree-sitter-provider.js +811 -37
  102. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
  103. package/core/query/query-router.js +55 -5
  104. package/core/ranking/file-kind-ranking.js +2192 -15
  105. package/core/ranking/late-interaction-index.js +87 -12
  106. package/core/search/cli-decoration.js +290 -0
  107. package/core/search/context-expander.js +988 -78
  108. package/core/search/index.js +1 -0
  109. package/core/search/output-policy.js +275 -0
  110. package/core/search/search-anchor.js +499 -0
  111. package/core/search/search-boost.js +93 -1
  112. package/core/search/search-cli.js +61 -204
  113. package/core/search/search-hybrid.js +250 -10
  114. package/core/search/search-pattern-chunks.js +57 -8
  115. package/core/search/search-pattern-planner.js +68 -9
  116. package/core/search/search-pattern-prefilter.js +30 -10
  117. package/core/search/search-pattern-ripgrep.js +40 -4
  118. package/core/search/search-pattern-sparse-overlay.js +256 -0
  119. package/core/search/search-pattern.js +117 -29
  120. package/core/search/search-postprocess.js +479 -5
  121. package/core/search/search-read-semantic.js +260 -23
  122. package/core/search/search-read.js +82 -64
  123. package/core/search/search-reader-pin.js +71 -0
  124. package/core/search/search-rrf.js +279 -0
  125. package/core/search/search-semantic.js +110 -5
  126. package/core/search/search-server.js +130 -57
  127. package/core/search/search-trace.js +107 -0
  128. package/core/search/server-identity.js +93 -0
  129. package/core/search/session-daemon-prewarm.mjs +33 -10
  130. package/core/search/sweet-search.js +399 -7
  131. package/core/skills/sweet-index/SKILL.md +8 -6
  132. package/core/vector-store/binary-hnsw-index.js +194 -30
  133. package/core/vector-store/float-vector-store.js +96 -6
  134. package/core/vector-store/hnsw-index.js +220 -49
  135. package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
  136. package/eval/agent-read-workflows/bin/ss-find +15 -0
  137. package/eval/agent-read-workflows/bin/ss-grep +12 -0
  138. package/eval/agent-read-workflows/bin/ss-read +14 -0
  139. package/eval/agent-read-workflows/bin/ss-search +18 -0
  140. package/eval/agent-read-workflows/bin/ss-semantic +12 -0
  141. package/eval/agent-read-workflows/bin/ss-trace +11 -0
  142. package/mcp/read-tool.js +109 -0
  143. package/mcp/server.js +55 -15
  144. package/mcp/tool-handlers.js +14 -124
  145. package/mcp/trace-tool.js +81 -0
  146. package/package.json +25 -10
  147. package/scripts/hooks/intercept-read.mjs +55 -0
  148. package/scripts/hooks/remind-tools.mjs +40 -0
  149. package/scripts/init.js +698 -54
  150. package/scripts/inject-agent-instructions.js +431 -0
  151. package/scripts/install-prompt-reminders.js +188 -0
  152. package/scripts/install-tool-enforcement.js +220 -0
  153. package/scripts/smoke-test.js +12 -9
  154. package/scripts/uninstall.js +276 -18
  155. package/scripts/write-claude-rules.js +110 -0
package/scripts/init.js CHANGED
@@ -12,11 +12,13 @@
12
12
 
13
13
  import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
14
14
  import { dirname, isAbsolute, join, relative } from 'node:path';
15
+ import { homedir } from 'node:os';
15
16
  import { spawnSync } from 'node:child_process';
16
17
  import { fileURLToPath } from 'node:url';
17
18
 
18
19
  import {
19
20
  getModelEntry, getModelsForProfile, getSkippedOptInModels, MODEL_REGISTRY, fetchModel, getModelCacheDir,
21
+ isNativeAcceleratedModel,
20
22
  getPlatformInfo, resolveNativeAddon, resolveNativeBinary,
21
23
  detectHardwareCapability,
22
24
  getCoremlCascadeState, getCoremlCascadeReport, fetchCoremlCascade,
@@ -35,6 +37,11 @@ import {
35
37
  } from '../core/ranking/late-interaction-policy.js';
36
38
  import { describeDedupConfig } from '../core/infrastructure/index.js';
37
39
  import { verifyRuntime, getMaxsimTier, getRouterType } from './verify-runtime.js';
40
+ import { ALL_HARNESSES, injectAgentInstructions } from './inject-agent-instructions.js';
41
+ import { writeClaudeRules } from './write-claude-rules.js';
42
+ import { installPromptReminderHook } from './install-prompt-reminders.js';
43
+ import { installToolEnforcement } from './install-tool-enforcement.js';
44
+ import { isNativeInferenceAvailable } from '../core/infrastructure/native-inference.js';
38
45
 
39
46
  const __dirname = dirname(fileURLToPath(import.meta.url));
40
47
  const PACKAGE_ROOT = join(__dirname, '..');
@@ -61,6 +68,21 @@ export function parseInitArgs(args) {
61
68
  liModel: null, // Phase 4: --li-model standard|edge|none (raw user input; 'standard' aliased to 'lateon-code')
62
69
  searchReranking: null, // Phase 4: --search-reranking auto|on|off
63
70
  wizard: false, // Phase 4: --wizard runs interactive prompts
71
+ // P1 (system-prompt opt) — agent-instruction injection.
72
+ // Default: only CLAUDE.md (sweet-search is Claude-first).
73
+ // --agents / --gemini / --cursor opt INTO additional harness files.
74
+ // --no-claude opts OUT of CLAUDE.md *and* every .claude/* write
75
+ // (hooks, skills, rules, settings-json mutations).
76
+ // --no-agent-instructions is the umbrella: skip the four harness
77
+ // instruction files but keep .claude/* hooks unless --no-claude.
78
+ skipAgentInstructions: false,
79
+ symlinkInstructionFiles: true,
80
+ optInHarnesses: new Set(),
81
+ noClaude: false,
82
+ skipPromptReminders: false, // P2: --no-prompt-reminders (default OFF)
83
+ enforceTools: false, // P3: --enforce-tools (default OFF — opt-in strict mode)
84
+ codex: false, // --codex: wire the Codex CLI SessionStart hook
85
+ codexEnableGlobalHooks: false, // --codex-enable-global-hooks: also enable the flag in ~/.codex/config.toml
64
86
  };
65
87
 
66
88
  for (let i = 0; i < args.length; i++) {
@@ -113,12 +135,86 @@ export function parseInitArgs(args) {
113
135
  // native package are present. Equivalent to SWEET_SEARCH_CUDA=0
114
136
  // but persisted through init's diagnostic output.
115
137
  result.skipCuda = true;
138
+ } else if (arg === '--no-agent-instructions') {
139
+ // P1: skip ALL instruction-file writes (CLAUDE.md / AGENTS.md /
140
+ // GEMINI.md / .cursor/rules/sweet-search.mdc / .claude/rules/sweet-search.md).
141
+ result.skipAgentInstructions = true;
142
+ } else if (arg === '--no-symlink-instruction-files') {
143
+ // P1: write GEMINI.md as a copy with @import rather than a symlink to
144
+ // the canonical file. Useful on filesystems that don't support symlinks
145
+ // (e.g. some Windows configurations) or when a tool requires a regular file.
146
+ result.symlinkInstructionFiles = false;
147
+ } else if (arg === '--symlink-instruction-files') {
148
+ // P1: explicit ON — useful for clarity in scripts even though it's the default.
149
+ result.symlinkInstructionFiles = true;
150
+ } else if (arg === '--no-claude') {
151
+ // P1: opt out of CLAUDE.md *and* every other .claude/* write that
152
+ // init normally performs (rules file, /sweet-index skill,
153
+ // index-maintainer hook, prewarm SessionStart entry). Universal
154
+ // gate — the user has signalled they don't use Claude Code.
155
+ // If `--agents` / `--gemini` / `--cursor` is also set, AGENTS.md
156
+ // (or whichever opt-in is canonical) carries the policy instead.
157
+ result.noClaude = true;
158
+ } else if (arg === '--agents') {
159
+ // P1: opt INTO writing AGENTS.md, the multi-harness convention
160
+ // (Codex CLI, OpenCode, and any other tool that adopts AGENTS.md).
161
+ result.optInHarnesses.add('agents');
162
+ } else if (arg === '--gemini') {
163
+ // P1: opt INTO writing GEMINI.md.
164
+ result.optInHarnesses.add('gemini');
165
+ } else if (arg === '--cursor') {
166
+ // P1: opt INTO writing .cursor/rules/sweet-search.mdc.
167
+ result.optInHarnesses.add('cursor');
168
+ } else if (arg === '--codex') {
169
+ // Wire the Codex CLI: write a SessionStart hook into .codex/hooks.json
170
+ // (Codex's hook surface) reusing the same launcher as the Claude prewarm
171
+ // hook, and ship AGENTS.md (Codex's instruction file) by implying
172
+ // --agents. Independent of --no-claude (writes .codex/, not .claude/).
173
+ result.codex = true;
174
+ result.optInHarnesses.add('agents');
175
+ } else if (arg === '--codex-enable-global-hooks') {
176
+ // Legacy/advanced opt-in: also enable the `[features] hooks` feature flag
177
+ // in the user-level ~/.codex/config.toml. NOT required for the normal
178
+ // path — `--codex` already enables the flag in the project config. Off by
179
+ // default because it writes outside the project into the user's
180
+ // hand-curated global config.
181
+ result.codexEnableGlobalHooks = true;
182
+ } else if (arg === '--no-prompt-reminders') {
183
+ // P2: skip the UserPromptSubmit reminder hook. Default-on because
184
+ // the reminder is the cheapest available shift-left for tool
185
+ // selection (~80 tokens per prompt vs avoided re-search loops).
186
+ result.skipPromptReminders = true;
187
+ } else if (arg === '--enforce-tools') {
188
+ // P3: opt-in strict mode — denies native Grep + installs a Read
189
+ // hint hook. Opinionated and Claude-specific (per §4D).
190
+ result.enforceTools = true;
116
191
  }
117
192
  }
118
193
 
119
194
  return result;
120
195
  }
121
196
 
197
+ /**
198
+ * Resolve the active harness list. Default is `claude-code` only;
199
+ * `--agents` / `--gemini` / `--cursor` add to that set; `--no-claude`
200
+ * removes claude-code (and gates every .claude/* write — see init flow).
201
+ *
202
+ * @param {object} args
203
+ * @param {Set<string>|string[]} [args.optInHarnesses] subset of
204
+ * {agents, gemini, cursor} to add on top of the default
205
+ * @param {boolean} [args.noClaude] when true, drops claude-code entirely
206
+ * @returns {string[]} ordered list matching ALL_HARNESSES order
207
+ */
208
+ export function resolveActiveHarnesses({ optInHarnesses, noClaude = false } = {}) {
209
+ const optIn = optInHarnesses instanceof Set
210
+ ? optInHarnesses
211
+ : new Set(optInHarnesses ?? []);
212
+ const active = new Set();
213
+ if (!noClaude) active.add('claude-code');
214
+ for (const h of optIn) active.add(h);
215
+ return ALL_HARNESSES.filter(h => active.has(h));
216
+ }
217
+
122
218
  // ---------------------------------------------------------------------------
123
219
  // Node.js version check
124
220
  // ---------------------------------------------------------------------------
@@ -495,11 +591,53 @@ export function filterModelKeysForLiChoice(modelKeys, liModel) {
495
591
  return modelKeys;
496
592
  }
497
593
 
594
+ /**
595
+ * Drop native-accelerated FP32 safetensors keys on CPU-only hosts. Those
596
+ * artifacts (coderankembed-fp32, lateon-code-fp32, lateon-code-edge-fp32) are
597
+ * loaded only by the candle/native accelerated indexing path (Metal / CoreML
598
+ * cascade / CUDA); a no-accelerator host indexes with ORT INT8 CPU and never
599
+ * loads them, so fetching ~1.2 GB of FP32 weights is pure waste.
600
+ *
601
+ * `hasAccelerator` is derived from the hardware-capability snapshot plus
602
+ * native-inference availability by the caller. Order-preserving +
603
+ * non-mutating: returns a new array.
604
+ * Classification of "is this a native FP32 model" lives in the registry
605
+ * (`isNativeAcceleratedModel`), so this stays a thin policy filter.
606
+ *
607
+ * Only an explicit `hasAccelerator: false` triggers filtering — `undefined`
608
+ * (flag omitted) and `true` both keep every key. That makes an accidental
609
+ * omission safe (it never silently drops the FP32 backbones).
610
+ */
611
+ export function filterModelKeysForAccelerator(modelKeys, { hasAccelerator } = {}) {
612
+ if (hasAccelerator !== false) return modelKeys;
613
+ return modelKeys.filter((k) => !isNativeAcceleratedModel(k));
614
+ }
615
+
616
+ /**
617
+ * Init-time accelerator availability for model shipping. A hardware
618
+ * accelerator only counts when native inference can actually load it; hosts
619
+ * with SWEET_SEARCH_NATIVE_INFERENCE=0, no native addon, or a native load
620
+ * failure index on ORT INT8 CPU and should skip native FP32 safetensors.
621
+ */
622
+ export function hasUsableIndexAccelerator({
623
+ capability,
624
+ nativeInferenceAvailable = true,
625
+ } = {}) {
626
+ if (nativeInferenceAvailable !== true) return false;
627
+ return capability?.inferenceBackendPreference === 'coreml-cascade'
628
+ || capability?.inferenceBackendPreference === 'candle-metal'
629
+ || capability?.inferenceBackendPreference === 'candle-cuda';
630
+ }
631
+
498
632
  export async function downloadModelsForProfile(profile, options = {}) {
499
633
  let modelKeys = getModelsForProfile(profile);
500
634
  if (options.liModel) {
501
635
  modelKeys = filterModelKeysForLiChoice(modelKeys, options.liModel);
502
636
  }
637
+ // CPU-only hosts skip native FP32 safetensors. The filter only drops keys
638
+ // when `hasAccelerator` is explicitly false; an unset option (legacy
639
+ // callers, accelerator hosts) preserves the "fetch everything" behavior.
640
+ modelKeys = filterModelKeysForAccelerator(modelKeys, { hasAccelerator: options.hasAccelerator });
503
641
  if (modelKeys.length === 0) {
504
642
  return { results: new Map(), totalDownloaded: 0, totalCached: 0, failures: [] };
505
643
  }
@@ -557,7 +695,8 @@ function printReport(report) {
557
695
  const {
558
696
  profile, maxsimTier, routerType, models, verification, runtimeDownloads,
559
697
  capability, cascadeReport, dedupReport, prewarmHookReport, skillReport,
560
- liChoices,
698
+ liChoices, agentInstructionsReport, claudeRulesReport,
699
+ promptReminderReport, toolEnforcementReport,
561
700
  } = report;
562
701
 
563
702
  console.log('');
@@ -663,6 +802,21 @@ function printReport(report) {
663
802
  }
664
803
  }
665
804
 
805
+ if (agentInstructionsReport) {
806
+ const summary = Object.entries(agentInstructionsReport.harnesses)
807
+ .map(([k, v]) => `${k}=${v}`).join(' ');
808
+ console.log(` Agent instructions: ${summary}`);
809
+ }
810
+ if (claudeRulesReport) {
811
+ console.log(` Claude rules file: ${claudeRulesReport.status}`);
812
+ }
813
+ if (promptReminderReport && promptReminderReport.status !== 'skipped') {
814
+ console.log(` Prompt reminder hook: ${promptReminderReport.status}`);
815
+ }
816
+ if (toolEnforcementReport && toolEnforcementReport.status !== 'skipped') {
817
+ console.log(` Tool enforcement: ${toolEnforcementReport.status} (Grep deny + Read hint)`);
818
+ }
819
+
666
820
  console.log(` Runtime downloads: ${runtimeDownloads}`);
667
821
 
668
822
  const passedCount = verification.checks.filter(c => c.status === 'pass').length;
@@ -839,6 +993,264 @@ export function registerPrewarmSessionStartHook({
839
993
  };
840
994
  }
841
995
 
996
+ // ---------------------------------------------------------------------------
997
+ // Codex CLI SessionStart hook (--codex)
998
+ // ---------------------------------------------------------------------------
999
+
1000
+ export const CODEX_HOOKS_FILENAME = 'hooks.json';
1001
+
1002
+ /**
1003
+ * Register (or update) a Codex CLI SessionStart hook in `.codex/hooks.json`
1004
+ * that launches the search server + incremental-index maintainer on every
1005
+ * Codex session — the Codex analogue of `registerPrewarmSessionStartHook`.
1006
+ * Reuses the same harness-agnostic launcher (`session-daemon-prewarm.mjs`,
1007
+ * matched by `PREWARM_HOOK_FILENAME`), which reads only env/cwd and writes
1008
+ * nothing to stdout, so it is safe under Codex's hook contract.
1009
+ *
1010
+ * Codex hook schema (developers.openai.com/codex/hooks):
1011
+ * { "hooks": { "SessionStart": [ { "hooks": [ { type, command, timeout } ] } ] } }
1012
+ *
1013
+ * Non-destructive + idempotent: preserves other events/entries and replaces
1014
+ * the sweet-search-owned entry (matched by the launcher filename) instead of
1015
+ * appending a duplicate. Refuses to write a machine-specific absolute path
1016
+ * into the (often committed) `.codex/hooks.json`, mirroring the Claude path.
1017
+ *
1018
+ * NOTE: hooks only fire when Codex's `[features] hooks` flag is enabled and the
1019
+ * project `.codex/` layer is trusted (reviewed via `/hooks`) — neither of which
1020
+ * a project init can fully guarantee. `ensureCodexHooksFeatureFlag` handles the
1021
+ * flag (best-effort) and the init report prints the trust caveat.
1022
+ *
1023
+ * @returns {{status:'registered'|'skipped'|'error', detail:string, hookPath?:string}}
1024
+ */
1025
+ export function registerCodexSessionStartHook({ projectRoot, packageRoot, skipped = false } = {}) {
1026
+ if (skipped) return { status: 'skipped', detail: '--skip-prewarm-hook flag' };
1027
+
1028
+ const hookScriptAbs = join(packageRoot, 'core', 'search', PREWARM_HOOK_FILENAME);
1029
+ if (!existsSync(hookScriptAbs)) {
1030
+ return { status: 'error', detail: `hook script missing: ${hookScriptAbs}` };
1031
+ }
1032
+
1033
+ const hookPath = relative(projectRoot, hookScriptAbs);
1034
+ if (hookPath.startsWith('..') || isAbsolute(hookPath)) {
1035
+ return {
1036
+ status: 'skipped',
1037
+ detail: 'package lives outside projectRoot (hoisted / linked / global install) — re-run with --skip-prewarm-hook to silence this',
1038
+ };
1039
+ }
1040
+
1041
+ // Codex runs hook commands with the *session* cwd, and per its hooks docs a
1042
+ // session "may be started from a subdirectory", so a bare relative path is
1043
+ // unreliable — the docs explicitly recommend resolving from the git root.
1044
+ // We `cd` to the git toplevel first (falling back to the current dir outside
1045
+ // a repo), which fixes both path resolution AND the launcher's own
1046
+ // `process.cwd()` project-root detection, while staying machine-portable
1047
+ // (no absolute path is written into the often-committed hooks.json).
1048
+ const command = `cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" && node ${JSON.stringify(hookPath)}`;
1049
+ const codexDir = join(projectRoot, '.codex');
1050
+ const hooksPath = join(codexDir, CODEX_HOOKS_FILENAME);
1051
+
1052
+ let doc = {};
1053
+ if (existsSync(hooksPath)) {
1054
+ try {
1055
+ doc = JSON.parse(readFileSync(hooksPath, 'utf-8'));
1056
+ } catch (err) {
1057
+ return { status: 'error', detail: `existing .codex/hooks.json is not valid JSON: ${err.message}` };
1058
+ }
1059
+ }
1060
+
1061
+ doc.hooks = doc.hooks || {};
1062
+ const sessionStart = Array.isArray(doc.hooks.SessionStart) ? doc.hooks.SessionStart : [];
1063
+
1064
+ // `matcher` mirrors the official Codex SessionStart example: fire on a new
1065
+ // session and on resume (the cases where the daemon may not be running yet).
1066
+ // `timeout` is in seconds (per Codex's hooks docs); the launcher detaches and
1067
+ // returns in well under a second, so a small budget is plenty.
1068
+ const entry = {
1069
+ matcher: 'startup|resume',
1070
+ hooks: [
1071
+ {
1072
+ type: 'command',
1073
+ command,
1074
+ timeout: 10,
1075
+ },
1076
+ ],
1077
+ };
1078
+
1079
+ const ownedIdx = sessionStart.findIndex((group) =>
1080
+ Array.isArray(group?.hooks) &&
1081
+ group.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(PREWARM_HOOK_FILENAME))
1082
+ );
1083
+
1084
+ if (ownedIdx >= 0) {
1085
+ sessionStart[ownedIdx] = entry;
1086
+ } else {
1087
+ sessionStart.push(entry);
1088
+ }
1089
+ doc.hooks.SessionStart = sessionStart;
1090
+
1091
+ try {
1092
+ mkdirSync(codexDir, { recursive: true });
1093
+ const tmpPath = hooksPath + '.tmp';
1094
+ writeFileSync(tmpPath, JSON.stringify(doc, null, 2) + '\n', 'utf-8');
1095
+ renameSync(tmpPath, hooksPath);
1096
+ } catch (err) {
1097
+ return { status: 'error', detail: err.message };
1098
+ }
1099
+
1100
+ return {
1101
+ status: 'registered',
1102
+ detail: ownedIdx >= 0 ? 'updated existing entry' : 'added new entry',
1103
+ hookPath,
1104
+ };
1105
+ }
1106
+
1107
+ /**
1108
+ * Ensure the canonical Codex hooks feature flag `[features] hooks = true` is
1109
+ * present in a Codex `config.toml`, preserving the file's existing content +
1110
+ * comments. Targeted text edit (no TOML round-trip) so a hand-curated config is
1111
+ * never reformatted.
1112
+ *
1113
+ * `hooks` is the canonical key as of Codex v0.132+. The earlier `codex_hooks`
1114
+ * key is deprecated (Codex now prints a deprecation warning for it), so this
1115
+ * function also MIGRATES a legacy `codex_hooks` flag to `hooks` instead of
1116
+ * writing the deprecated name. Legacy handling is deliberate: users who ran an
1117
+ * older sweet-search (which wrote `codex_hooks`) or older Codex shouldn't be
1118
+ * left with a deprecated, warning-producing flag.
1119
+ *
1120
+ * Behaviour:
1121
+ * - `hooks = true` already present → no-op ('already'); if a deprecated
1122
+ * `codex_hooks` line also lingers, strip it ('migrated')
1123
+ * - `hooks` present but non-true → respect the user's choice ('present-other')
1124
+ * - legacy `codex_hooks` present (no `hooks` key) → rename the key to `hooks`
1125
+ * in place, preserving its value + inline comment ('migrated')
1126
+ * - a `[features]` table exists → insert `hooks = true` under its header
1127
+ * - no `[features]` table → append a fresh block at EOF
1128
+ * - file absent + `create` → create it; absent + no `create` → 'absent'
1129
+ *
1130
+ * @param {string} configPath
1131
+ * @param {{create?:boolean}} [opts]
1132
+ * @returns {{status:'created'|'added'|'migrated'|'already'|'present-other'|'absent'|'error', path:string, detail?:string}}
1133
+ */
1134
+ export function ensureCodexHooksFeatureFlag(configPath, { create = false } = {}) {
1135
+ const exists = existsSync(configPath);
1136
+ if (!exists && !create) return { status: 'absent', path: configPath };
1137
+
1138
+ let text = '';
1139
+ if (exists) {
1140
+ try {
1141
+ text = readFileSync(configPath, 'utf-8');
1142
+ } catch (err) {
1143
+ return { status: 'error', path: configPath, detail: `read failed: ${err.message}` };
1144
+ }
1145
+ }
1146
+
1147
+ const write = (next, status) => {
1148
+ try {
1149
+ mkdirSync(dirname(configPath), { recursive: true });
1150
+ const tmp = configPath + '.tmp';
1151
+ writeFileSync(tmp, next, 'utf-8');
1152
+ renameSync(tmp, configPath);
1153
+ } catch (err) {
1154
+ return { status: 'error', path: configPath, detail: `write failed: ${err.message}` };
1155
+ }
1156
+ return { status, path: configPath };
1157
+ };
1158
+
1159
+ // `^[ \t]*hooks` cannot match a `codex_hooks` line (the prefix differs), so
1160
+ // these canonical-key checks never collide with the legacy key.
1161
+ const HOOKS_TRUE = /^[ \t]*hooks[ \t]*=[ \t]*true\b/m;
1162
+ const HOOKS_ANY = /^[ \t]*hooks[ \t]*=/m;
1163
+ const LEGACY_ANY = /^[ \t]*codex_hooks[ \t]*=/m;
1164
+ const LEGACY_LINE = /^[ \t]*codex_hooks[ \t]*=.*(?:\r?\n)?/m;
1165
+
1166
+ // Canonical flag already enabled.
1167
+ if (HOOKS_TRUE.test(text)) {
1168
+ if (LEGACY_ANY.test(text)) {
1169
+ // Strip the deprecated `codex_hooks` line so Codex v0.132+ stops warning.
1170
+ return write(text.replace(LEGACY_LINE, ''), 'migrated');
1171
+ }
1172
+ return { status: 'already', path: configPath };
1173
+ }
1174
+
1175
+ // Canonical flag present but explicitly non-true → respect the user's choice;
1176
+ // don't add a duplicate key (which would make the TOML invalid).
1177
+ if (HOOKS_ANY.test(text)) {
1178
+ return { status: 'present-other', path: configPath };
1179
+ }
1180
+
1181
+ // Deprecated `codex_hooks` present (no canonical key) → migrate the key name
1182
+ // in place, preserving its value and any inline comment.
1183
+ if (LEGACY_ANY.test(text)) {
1184
+ return write(text.replace(/^([ \t]*)codex_hooks([ \t]*=.*)$/m, '$1hooks$2'), 'migrated');
1185
+ }
1186
+
1187
+ // Neither key present → add the canonical flag.
1188
+ let next;
1189
+ if (!exists || text.trim() === '') {
1190
+ next = '[features]\nhooks = true\n';
1191
+ } else if (/^[ \t]*\[features\][ \t]*$/m.test(text)) {
1192
+ next = text.replace(/^([ \t]*\[features\][ \t]*)$/m, '$1\nhooks = true');
1193
+ } else {
1194
+ const sep = text.endsWith('\n') ? '' : '\n';
1195
+ next = `${text}${sep}\n[features]\nhooks = true\n`;
1196
+ }
1197
+ return write(next, exists ? 'added' : 'created');
1198
+ }
1199
+
1200
+ /**
1201
+ * Build the post-init Codex guidance message (or `null` when the hook wasn't
1202
+ * registered). Pure / string-only so the UX wording can be unit-tested without
1203
+ * spawning init.
1204
+ *
1205
+ * `init --codex` is the COMPLETE normal setup — it has already written the hook
1206
+ * and enabled the canonical `[features] hooks = true` in the project config — so
1207
+ * this message frames `/hooks` trust as the ONLY remaining manual step. It never
1208
+ * tells the user they must pass `--codex-enable-global-hooks` (a legacy/advanced
1209
+ * opt-in), and it never names the deprecated `codex_hooks` key.
1210
+ *
1211
+ * @param {{hookStatus:string, projectFlagStatus:string}} params
1212
+ * @returns {string|null}
1213
+ */
1214
+ export function formatCodexSetupGuidance({ hookStatus, projectFlagStatus } = {}) {
1215
+ if (hookStatus !== 'registered') return null;
1216
+
1217
+ // `created` / `added` / `already` / `migrated` all mean the canonical
1218
+ // `[features] hooks = true` is now present in the project config — so init has
1219
+ // done the enable and the only manual step left is project trust via `/hooks`.
1220
+ const projectEnabled = ['created', 'added', 'already', 'migrated'].includes(projectFlagStatus);
1221
+ if (projectEnabled) {
1222
+ return (
1223
+ `[init] Codex is set up. Project hooks are enabled ` +
1224
+ `([features] hooks = true in ./.codex/config.toml).\n` +
1225
+ ` One manual step remains (init can't do it for you):\n` +
1226
+ ` • Run \`/hooks\` inside Codex to review and trust the repo-local\n` +
1227
+ ` .codex/hooks.json before Codex will run it.\n` +
1228
+ ` Notes:\n` +
1229
+ ` • Index freshness does NOT depend on this hook — the sweet-search\n` +
1230
+ ` CLI/warm-server starts the incremental maintainer on first use\n` +
1231
+ ` under any agent. The Codex hook is only an early prewarm.\n` +
1232
+ ` • \`codex exec\` (non-interactive) does not fire SessionStart hooks.\n` +
1233
+ ` • MCP is optional and not required for incremental indexing.\n`
1234
+ );
1235
+ }
1236
+
1237
+ if (projectFlagStatus === 'present-other') {
1238
+ return (
1239
+ `[init] Codex hook written, but ./.codex/config.toml already sets ` +
1240
+ `[features] hooks to a non-true value — left as-is.\n` +
1241
+ ` Set \`hooks = true\` (or run \`codex --enable hooks\`), then trust\n` +
1242
+ ` the hook with \`/hooks\` in Codex. Index freshness still works\n` +
1243
+ ` without it (CLI/warm-server first-use).\n`
1244
+ );
1245
+ }
1246
+
1247
+ return (
1248
+ `[init] Codex hook written, but enabling [features] hooks in ` +
1249
+ `./.codex/config.toml did not succeed (status: ${projectFlagStatus}).\n` +
1250
+ ` Set \`hooks = true\` manually, then trust the hook with \`/hooks\`.\n`
1251
+ );
1252
+ }
1253
+
842
1254
  // ---------------------------------------------------------------------------
843
1255
  // /sweet-index skill installation
844
1256
  // ---------------------------------------------------------------------------
@@ -936,7 +1348,71 @@ Options:
936
1348
  --skip-cuda Force-disable the CUDA backend even when an
937
1349
  NVIDIA GPU and the -cuda native package are
938
1350
  detected. Equivalent to SWEET_SEARCH_CUDA=0.
939
- Indexing falls back to candle-cpu.
1351
+ Indexing falls back to ORT INT8 CPU (and native
1352
+ FP32 safetensors are skipped at fetch time).
1353
+ --no-agent-instructions Skip the agent-instruction injection layer
1354
+ entirely (no CLAUDE.md, no AGENTS.md, no
1355
+ GEMINI.md, no Cursor rule, no
1356
+ .claude/rules/sweet-search.md). Use when you
1357
+ manage your own agent instructions. Idempotent
1358
+ rewrites use a marker block; re-running init
1359
+ never duplicates content.
1360
+ --no-claude Don't ship anything to .claude/. Skips
1361
+ CLAUDE.md + .claude/rules/sweet-search.md +
1362
+ .claude/hooks/index-maintainer.mjs +
1363
+ .claude/skills/sweet-index/ +
1364
+ .claude/settings.json prewarm SessionStart entry.
1365
+ Combine with --agents / --gemini / --cursor to
1366
+ ship the policy through a non-Claude harness.
1367
+ --agents Also ship AGENTS.md, the multi-harness
1368
+ convention read by Codex CLI, OpenCode, and any
1369
+ other tool that adopts AGENTS.md. Without
1370
+ --no-claude, AGENTS.md is a thin @CLAUDE.md
1371
+ import shim; with --no-claude it carries the
1372
+ full canonical policy body.
1373
+ --gemini Also ship GEMINI.md (symlink → canonical, or
1374
+ an @import file when --no-symlink-instruction-files).
1375
+ --cursor Also ship .cursor/rules/sweet-search.mdc with
1376
+ sweet-search frontmatter.
1377
+ --codex Complete Codex CLI setup (the normal path): write a
1378
+ SessionStart hook into .codex/hooks.json (reusing the
1379
+ same launcher as the Claude prewarm hook, so Codex
1380
+ sessions also start the search server + default-on
1381
+ incremental maintainer), enable [features] hooks = true
1382
+ in the project .codex/config.toml (migrating a
1383
+ deprecated codex_hooks flag if present), and ship
1384
+ AGENTS.md (implies --agents). Independent of
1385
+ --no-claude. The only remaining manual step is to
1386
+ review/trust the repo-local hook with /hooks inside
1387
+ Codex. Index freshness does NOT depend on this hook —
1388
+ the sweet-search CLI/warm-server starts the maintainer
1389
+ on first use under any agent; the hook is just an early
1390
+ prewarm. MCP is optional and not required.
1391
+ --codex-enable-global-hooks
1392
+ [legacy/advanced] Not needed for normal setup — the
1393
+ project-level flag written by --codex is sufficient.
1394
+ Use only to deliberately migrate the user-level
1395
+ ~/.codex/config.toml: enables [features] hooks = true
1396
+ there too (append-if-absent, comment-preserving,
1397
+ migrates a deprecated codex_hooks). Off by default
1398
+ because it writes outside the project into your global
1399
+ Codex config.
1400
+ --no-symlink-instruction-files
1401
+ Write GEMINI.md as a regular file with an @import
1402
+ line rather than a symlink to the canonical file.
1403
+ Useful on filesystems / hosts where symlinks
1404
+ aren't supported. Default is to symlink.
1405
+ --no-prompt-reminders Skip the UserPromptSubmit reminder hook (default
1406
+ on). The reminder injects a small (~80 token)
1407
+ sweet-search tool-routing summary into every
1408
+ prompt to prevent drift back to native Grep/Read.
1409
+ Always implied when --no-claude is set.
1410
+ --enforce-tools (Opt-in strict mode) Deny native Grep entirely
1411
+ (forces ss-grep) and install a hint hook for
1412
+ native Read suggesting ss-read / ss-semantic.
1413
+ Read is hinted, not blocked, because edit
1414
+ workflows legitimately need Read. Always
1415
+ implied off when --no-claude is set.
940
1416
  --verbose, -v Enable verbose output
941
1417
  --help, -h Show this help
942
1418
 
@@ -999,7 +1475,25 @@ export async function runInit(args) {
999
1475
  // and final config write all see the same choices. Uses an early
1000
1476
  // hardware capability snapshot for the recommendation; the snapshot
1001
1477
  // is recomputed later for the runtime config (cheap + cacheable).
1478
+ //
1479
+ // --skip-cuda: translate to SWEET_SEARCH_CUDA=0 BEFORE the first
1480
+ // detectHardwareCapability() call below, which caches its result. Setting it
1481
+ // here (not just before step 8) ensures the early snapshot used for model
1482
+ // fetching, the persisted runtime config, and the cascade decision all see
1483
+ // CUDA as disabled — a --skip-cuda host is treated as CPU-only end to end.
1484
+ if (parsed.skipCuda && !process.env.SWEET_SEARCH_CUDA) {
1485
+ process.env.SWEET_SEARCH_CUDA = '0';
1486
+ }
1002
1487
  const earlyCapability = detectHardwareCapability();
1488
+ const nativeInferenceAvailable = isNativeInferenceAvailable();
1489
+ // A host has a usable accelerator only when hardware detection found one AND
1490
+ // native inference can actually load. If native inference is disabled or the
1491
+ // addon is absent/unloadable, init skips native FP32 safetensors because
1492
+ // indexing will stay on ORT INT8 CPU at runtime.
1493
+ const hasAccelerator = hasUsableIndexAccelerator({
1494
+ capability: earlyCapability,
1495
+ nativeInferenceAvailable,
1496
+ });
1003
1497
  let liChoices;
1004
1498
  try {
1005
1499
  liChoices = await resolveLiPolicyChoices({
@@ -1053,13 +1547,26 @@ export async function runInit(args) {
1053
1547
  // variants; 'edge' skips standard variants. Saves disk + bandwidth on
1054
1548
  // constrained installs.
1055
1549
  const allModelKeys = getModelsForProfile(profile);
1056
- const modelKeys = filterModelKeysForLiChoice(allModelKeys, liChoices.liModel);
1057
- const skippedByLiChoice = allModelKeys.filter((k) => !modelKeys.includes(k));
1550
+ const liFilteredKeys = filterModelKeysForLiChoice(allModelKeys, liChoices.liModel);
1551
+ // CPU-only hosts (no Metal/CoreML/CUDA) skip native FP32 safetensors. Apply
1552
+ // the same filter to the keys handed to verification below, so we only verify
1553
+ // what we actually fetched — verifyRuntime checks on-disk presence and would
1554
+ // otherwise FAIL on the intentionally-absent FP32 artifacts.
1555
+ const modelKeys = filterModelKeysForAccelerator(liFilteredKeys, { hasAccelerator });
1556
+ const skippedByLiChoice = allModelKeys.filter((k) => !liFilteredKeys.includes(k));
1557
+ const skippedByAccelerator = liFilteredKeys.filter((k) => !modelKeys.includes(k));
1058
1558
  if (skippedByLiChoice.length > 0 && parsed.verbose) {
1059
1559
  process.stderr.write(
1060
1560
  `[init] liModel=${liChoices.liModel} → skipping ${skippedByLiChoice.length} model key(s): ${skippedByLiChoice.join(', ')}\n`,
1061
1561
  );
1062
1562
  }
1563
+ if (skippedByAccelerator.length > 0) {
1564
+ process.stderr.write(
1565
+ `[init] no inference accelerator (${earlyCapability.inferenceBackendPreference}) → `
1566
+ + `skipping ${skippedByAccelerator.length} native FP32 model key(s): ${skippedByAccelerator.join(', ')} `
1567
+ + `(indexing uses ORT INT8 CPU)\n`,
1568
+ );
1569
+ }
1063
1570
  const skippedOptIns = getSkippedOptInModels(profile);
1064
1571
  let modelResults = new Map();
1065
1572
 
@@ -1082,6 +1589,7 @@ export async function runInit(args) {
1082
1589
  const downloadResult = await downloadModelsForProfile(profile, {
1083
1590
  force: parsed.force,
1084
1591
  liModel: liChoices.liModel,
1592
+ hasAccelerator,
1085
1593
  });
1086
1594
 
1087
1595
  modelResults = downloadResult.results;
@@ -1112,13 +1620,6 @@ export async function runInit(args) {
1112
1620
  process.stderr.write(`[init] No models required for profile "${profile}"\n`);
1113
1621
  }
1114
1622
 
1115
- // --skip-cuda: translate to SWEET_SEARCH_CUDA=0 so the shared
1116
- // capability detection on hardware-capability.js honors it. Set before
1117
- // detectHardwareCapability() runs because that call caches its result.
1118
- if (parsed.skipCuda && !process.env.SWEET_SEARCH_CUDA) {
1119
- process.env.SWEET_SEARCH_CUDA = '0';
1120
- }
1121
-
1122
1623
  // 8. Resolve hardware capability + CoreML cascade state.
1123
1624
  //
1124
1625
  // The cascade is an M3+ Apple Silicon acceleration for native inference.
@@ -1286,55 +1787,194 @@ export async function runInit(args) {
1286
1787
  process.exit(1);
1287
1788
  }
1288
1789
 
1289
- // 10. Install index-maintainer daemon hook
1290
- try {
1291
- const hookDir = join(projectRoot, '.claude', 'hooks');
1292
- const hookDest = join(hookDir, 'index-maintainer.mjs');
1293
- const hookSrc = join(PACKAGE_ROOT, 'core', 'indexing', 'index-maintainer.mjs');
1294
- if (!existsSync(hookDest)) {
1295
- mkdirSync(hookDir, { recursive: true });
1296
- copyFileSync(hookSrc, hookDest);
1297
- process.stderr.write(`[init] Installed index-maintainer daemon to ${hookDir}\n`);
1790
+ // 10. Install index-maintainer daemon hook (Claude-only — gated on
1791
+ // --no-claude per the universal "don't touch .claude/" contract).
1792
+ let skillReport = null;
1793
+ let prewarmHookReport = null;
1794
+ if (parsed.noClaude) {
1795
+ if (parsed.verbose) {
1796
+ process.stderr.write(`[init] Skipping all .claude/ writes (--no-claude): index-maintainer hook, /sweet-index skill, prewarm SessionStart entry\n`);
1797
+ }
1798
+ } else {
1799
+ try {
1800
+ const hookDir = join(projectRoot, '.claude', 'hooks');
1801
+ const hookDest = join(hookDir, 'index-maintainer.mjs');
1802
+ const hookSrc = join(PACKAGE_ROOT, 'core', 'indexing', 'index-maintainer.mjs');
1803
+ if (!existsSync(hookDest)) {
1804
+ mkdirSync(hookDir, { recursive: true });
1805
+ copyFileSync(hookSrc, hookDest);
1806
+ process.stderr.write(`[init] Installed index-maintainer daemon to ${hookDir}\n`);
1807
+ } else {
1808
+ process.stderr.write(`[init] Index-maintainer daemon already installed\n`);
1809
+ }
1810
+ } catch (e) {
1811
+ process.stderr.write(`[init] Warning: Could not install index-maintainer: ${e.message}\n`);
1812
+ }
1813
+
1814
+ // 11. Install /sweet-index skill — Claude-only artifact, gated on --no-claude.
1815
+ skillReport = installSweetIndexSkill({
1816
+ projectRoot,
1817
+ packageRoot: PACKAGE_ROOT,
1818
+ });
1819
+ if (skillReport.status === 'installed') {
1820
+ process.stderr.write(`[init] Installed /sweet-index skill to ${skillReport.detail}\n`);
1821
+ } else if (skillReport.status === 'already-installed') {
1822
+ process.stderr.write(`[init] /sweet-index skill already installed\n`);
1823
+ } else if (skillReport.status === 'error') {
1824
+ process.stderr.write(`[init] Warning: Could not install /sweet-index skill: ${skillReport.detail}\n`);
1825
+ }
1826
+
1827
+ // 11.5. Register Claude Code SessionStart daemon-prewarm hook.
1828
+ // Spawns the search daemon detached in the background so the user's
1829
+ // first `sweet-search <query>` hits a warm daemon (~80ms) instead of
1830
+ // paying ~2.5s cold-start. The hook exits immediately; the daemon
1831
+ // loads models + indexes while the user reads Claude's first reply.
1832
+ // Never blocks init — any write failure collapses to a silent no-op
1833
+ // on next session start.
1834
+ prewarmHookReport = registerPrewarmSessionStartHook({
1835
+ projectRoot,
1836
+ packageRoot: PACKAGE_ROOT,
1837
+ skipped: parsed.skipPrewarmHook,
1838
+ });
1839
+ if (parsed.verbose || prewarmHookReport.status === 'error') {
1840
+ process.stderr.write(`[init] Prewarm hook: ${prewarmHookReport.status} — ${prewarmHookReport.detail}\n`);
1841
+ }
1842
+ }
1843
+
1844
+ // 11.6. Codex CLI session-start hook + feature flag (opt-in via --codex).
1845
+ // `--codex` is the COMPLETE normal Codex setup: it writes Codex's hook
1846
+ // surface (.codex/hooks.json), enables the canonical [features] hooks
1847
+ // flag in the PROJECT .codex/config.toml (migrating a deprecated
1848
+ // codex_hooks), and ships AGENTS.md. Independent of --no-claude (it
1849
+ // touches .codex/, never .claude/). The one thing init can't do for the
1850
+ // user is project trust — surfaced as a single `/hooks` instruction. The
1851
+ // user-level global flag is a legacy/advanced opt-in
1852
+ // (--codex-enable-global-hooks), never required for the normal path. The
1853
+ // Codex hook is a prewarm convenience only: default index freshness is
1854
+ // guaranteed by the core CLI/warm-server first-use launcher regardless of
1855
+ // editor or MCP.
1856
+ let codexHookReport = null;
1857
+ if (parsed.codex) {
1858
+ codexHookReport = registerCodexSessionStartHook({
1859
+ projectRoot,
1860
+ packageRoot: PACKAGE_ROOT,
1861
+ skipped: parsed.skipPrewarmHook,
1862
+ });
1863
+ const projectFlag = ensureCodexHooksFeatureFlag(
1864
+ join(projectRoot, '.codex', 'config.toml'),
1865
+ { create: true },
1866
+ );
1867
+ let globalFlag = null;
1868
+ if (parsed.codexEnableGlobalHooks) {
1869
+ globalFlag = ensureCodexHooksFeatureFlag(
1870
+ join(homedir(), '.codex', 'config.toml'),
1871
+ { create: true },
1872
+ );
1873
+ }
1874
+
1875
+ process.stderr.write(`[init] Codex hook: ${codexHookReport.status} — ${codexHookReport.detail}\n`);
1876
+ process.stderr.write(`[init] Codex [features] hooks flag (project .codex/config.toml): ${projectFlag.status}\n`);
1877
+ if (globalFlag) {
1878
+ process.stderr.write(`[init] Codex [features] hooks flag (~/.codex/config.toml): ${globalFlag.status}\n`);
1879
+ }
1880
+ const guidance = formatCodexSetupGuidance({
1881
+ hookStatus: codexHookReport.status,
1882
+ projectFlagStatus: projectFlag.status,
1883
+ });
1884
+ if (guidance) process.stderr.write(guidance);
1885
+ }
1886
+
1887
+ // 12-15. Inject the sweet-search policy across coding-agent harnesses.
1888
+ // Default: only CLAUDE.md (sweet-search is Claude-first; the
1889
+ // existing project CLAUDE.md is where users look). Opt INTO
1890
+ // additional harnesses with --agents (AGENTS.md) / --gemini /
1891
+ // --cursor. --no-claude opts OUT of CLAUDE.md AND every other
1892
+ // .claude/* write (universal gate enforced above).
1893
+ // Plan §4A + §4B + §10 (the canonical flip from AGENTS.md →
1894
+ // CLAUDE.md is a user-driven product call — plan doc updated
1895
+ // in §3.3 / §10).
1896
+ // Idempotent marker block so re-init never duplicates content.
1897
+ // `--no-agent-instructions` is the umbrella that skips the
1898
+ // instruction-file injection layer entirely.
1899
+ let agentInstructionsReport = null;
1900
+ let claudeRulesReport = null;
1901
+ if (!parsed.skipAgentInstructions) {
1902
+ const activeHarnesses = resolveActiveHarnesses({
1903
+ optInHarnesses: parsed.optInHarnesses,
1904
+ noClaude: parsed.noClaude,
1905
+ });
1906
+ if (activeHarnesses.length === 0) {
1907
+ process.stderr.write(
1908
+ `[init] Agent instructions: nothing to write — pass --agents / --gemini / --cursor `
1909
+ + `to opt into additional harness files (claude-code is disabled by --no-claude).\n`,
1910
+ );
1298
1911
  } else {
1299
- process.stderr.write(`[init] Index-maintainer daemon already installed\n`);
1912
+ try {
1913
+ agentInstructionsReport = injectAgentInstructions({
1914
+ projectRoot,
1915
+ harnesses: activeHarnesses,
1916
+ useSymlinks: parsed.symlinkInstructionFiles,
1917
+ });
1918
+ const summary = Object.entries(agentInstructionsReport.harnesses)
1919
+ .map(([k, v]) => `${k}=${v}`).join(' ');
1920
+ const canonical = agentInstructionsReport.canonical
1921
+ ? ` (canonical=${agentInstructionsReport.canonical})` : '';
1922
+ process.stderr.write(`[init] Agent instructions: ${summary || '(none)'}${canonical}\n`);
1923
+ } catch (err) {
1924
+ process.stderr.write(`[init] Warning: Agent-instruction injection failed: ${err.message}\n`);
1925
+ }
1926
+ // Claude rules file is only useful when claude-code is enabled — the
1927
+ // sole load path is the @.claude/rules/sweet-search.md import line that
1928
+ // injectAgentInstructions writes into CLAUDE.md.
1929
+ if (activeHarnesses.includes('claude-code')) {
1930
+ try {
1931
+ const status = writeClaudeRules({ projectRoot });
1932
+ claudeRulesReport = { status };
1933
+ process.stderr.write(`[init] Claude rules: ${status}\n`);
1934
+ } catch (err) {
1935
+ process.stderr.write(`[init] Warning: Could not write Claude rules: ${err.message}\n`);
1936
+ }
1937
+ }
1938
+ }
1939
+ } else if (parsed.verbose) {
1940
+ process.stderr.write(`[init] Agent instructions: skipped (--no-agent-instructions)\n`);
1941
+ }
1942
+
1943
+ // 16. UserPromptSubmit reminder hook (P2 — plan §4C / §10 step 16).
1944
+ // Universal `--no-claude` gate already enforced at step 10. Default-on;
1945
+ // `--no-prompt-reminders` opts out. The hook lives at
1946
+ // `.claude/hooks/sweet-search-remind-tools.mjs` with a
1947
+ // `hooks.UserPromptSubmit` entry in `.claude/settings.json` keyed by
1948
+ // filename so re-init updates rather than duplicates.
1949
+ let promptReminderReport = null;
1950
+ if (!parsed.noClaude) {
1951
+ promptReminderReport = installPromptReminderHook({
1952
+ projectRoot,
1953
+ packageRoot: PACKAGE_ROOT,
1954
+ skipped: parsed.skipPromptReminders,
1955
+ });
1956
+ if (parsed.verbose || promptReminderReport.status === 'error') {
1957
+ process.stderr.write(`[init] Prompt reminder hook: ${promptReminderReport.status} — ${promptReminderReport.detail}\n`);
1300
1958
  }
1301
- } catch (e) {
1302
- process.stderr.write(`[init] Warning: Could not install index-maintainer: ${e.message}\n`);
1303
1959
  }
1304
1960
 
1305
- // 11. Install /sweet-index skillalways, even if .claude/ doesn't exist.
1306
- // Users who haven't adopted Claude Code yet still get the skill in place
1307
- // the moment they do; we treat the skill as part of the product, not a
1308
- // Claude-Code-conditional add-on.
1309
- const skillReport = installSweetIndexSkill({
1310
- projectRoot,
1311
- packageRoot: PACKAGE_ROOT,
1312
- });
1313
- if (skillReport.status === 'installed') {
1314
- process.stderr.write(`[init] Installed /sweet-index skill to ${skillReport.detail}\n`);
1315
- } else if (skillReport.status === 'already-installed') {
1316
- process.stderr.write(`[init] /sweet-index skill already installed\n`);
1317
- } else if (skillReport.status === 'error') {
1318
- process.stderr.write(`[init] Warning: Could not install /sweet-index skill: ${skillReport.detail}\n`);
1319
- }
1320
-
1321
- // 11.5. Register Claude Code SessionStart daemon-prewarm hook.
1322
- // Spawns the search daemon detached in the background so the user's
1323
- // first `sweet-search <query>` hits a warm daemon (~80ms) instead of
1324
- // paying ~2.5s cold-start. The hook exits immediately; the daemon
1325
- // loads models + indexes while the user reads Claude's first reply.
1326
- // Never blocks init — any write failure collapses to a silent no-op
1327
- // on next session start.
1328
- const prewarmHookReport = registerPrewarmSessionStartHook({
1329
- projectRoot,
1330
- packageRoot: PACKAGE_ROOT,
1331
- skipped: parsed.skipPrewarmHook,
1332
- });
1333
- if (parsed.verbose || prewarmHookReport.status === 'error') {
1334
- process.stderr.write(`[init] Prewarm hook: ${prewarmHookReport.status} — ${prewarmHookReport.detail}\n`);
1961
+ // 17. Tool enforcement (P3plan §4D / §10 step 17). Opt-in via
1962
+ // `--enforce-tools`; universal `--no-claude` gate above. Adds
1963
+ // `permissions.deny: ["Grep"]` and a PreToolUse hint hook for `Read`
1964
+ // in `.claude/settings.json`. Strict + opinionated; off by default.
1965
+ let toolEnforcementReport = null;
1966
+ if (!parsed.noClaude) {
1967
+ toolEnforcementReport = installToolEnforcement({
1968
+ projectRoot,
1969
+ packageRoot: PACKAGE_ROOT,
1970
+ skipped: !parsed.enforceTools,
1971
+ });
1972
+ if (parsed.verbose || toolEnforcementReport.status === 'error') {
1973
+ process.stderr.write(`[init] Tool enforcement: ${toolEnforcementReport.status} ${toolEnforcementReport.detail}\n`);
1974
+ }
1335
1975
  }
1336
1976
 
1337
- // 12. Print report
1977
+ // 18. Print report
1338
1978
  printReport({
1339
1979
  profile,
1340
1980
  maxsimTier,
@@ -1348,6 +1988,10 @@ export async function runInit(args) {
1348
1988
  prewarmHookReport,
1349
1989
  skillReport,
1350
1990
  liChoices,
1991
+ agentInstructionsReport,
1992
+ claudeRulesReport,
1993
+ promptReminderReport,
1994
+ toolEnforcementReport,
1351
1995
  });
1352
1996
  }
1353
1997