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.
- package/core/cli.js +24 -3
- package/core/graph/graph-expansion.js +215 -36
- package/core/graph/graph-extractor.js +196 -11
- package/core/graph/graph-search.js +395 -92
- package/core/graph/hcgs-generator.js +2 -1
- package/core/graph/index.js +2 -0
- package/core/graph/repo-map.js +28 -6
- package/core/graph/structural-answer-cues.js +168 -0
- package/core/graph/structural-callsite-hints.js +40 -0
- package/core/graph/structural-context-format.js +40 -0
- package/core/graph/structural-context.js +450 -0
- package/core/graph/structural-forward-push.js +156 -0
- package/core/graph/structural-header-context.js +19 -0
- package/core/graph/structural-importance.js +148 -0
- package/core/graph/structural-pagerank.js +197 -0
- package/core/graph/summary-manager.js +13 -9
- package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
- package/core/incremental-indexing/application/file-watcher.mjs +197 -0
- package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
- package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
- package/core/incremental-indexing/application/operator-cli.mjs +554 -0
- package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
- package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
- package/core/incremental-indexing/application/reconciler.mjs +477 -0
- package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
- package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
- package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
- package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
- package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
- package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
- package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
- package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
- package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
- package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
- package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
- package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
- package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
- package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
- package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
- package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
- package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
- package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
- package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
- package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
- package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
- package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
- package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
- package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
- package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
- package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
- package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
- package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
- package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
- package/core/indexing/admission-policy.js +139 -0
- package/core/indexing/artifact-builder.js +29 -12
- package/core/indexing/ast-chunker.js +107 -30
- package/core/indexing/dedup/exemplar-selector.js +19 -1
- package/core/indexing/gitignore-filter.js +223 -0
- package/core/indexing/incremental-tracker.js +99 -30
- package/core/indexing/index-codebase-v21.js +6 -5
- package/core/indexing/index-maintainer.mjs +698 -6
- package/core/indexing/indexer-ann.js +99 -15
- package/core/indexing/indexer-build.js +158 -45
- package/core/indexing/indexer-empty-baseline.js +80 -0
- package/core/indexing/indexer-manifest.js +66 -0
- package/core/indexing/indexer-phases.js +56 -23
- package/core/indexing/indexer-sparse-gram.js +54 -13
- package/core/indexing/indexer-utils.js +26 -208
- package/core/indexing/indexing-file-policy.js +32 -7
- package/core/indexing/maintainer-launcher.mjs +137 -0
- package/core/indexing/merkle-tracker.js +251 -244
- package/core/indexing/model-pool.js +46 -5
- package/core/infrastructure/code-graph-repository.js +758 -6
- package/core/infrastructure/code-graph-visibility.js +157 -0
- package/core/infrastructure/codebase-repository.js +100 -13
- package/core/infrastructure/config/search.js +1 -1
- package/core/infrastructure/db-utils.js +118 -0
- package/core/infrastructure/dedup-hashing.js +10 -13
- package/core/infrastructure/hardware-capability.js +17 -7
- package/core/infrastructure/index.js +8 -2
- package/core/infrastructure/language-patterns/maps.js +4 -1
- package/core/infrastructure/language-patterns/registry-core.js +56 -17
- package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
- package/core/infrastructure/language-patterns.js +69 -0
- package/core/infrastructure/model-registry.js +20 -0
- package/core/infrastructure/native-inference.js +7 -12
- package/core/infrastructure/native-resolver.js +52 -37
- package/core/infrastructure/native-sparse-gram.js +261 -20
- package/core/infrastructure/native-tokenizer.js +6 -15
- package/core/infrastructure/simd-distance.js +10 -16
- package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
- package/core/infrastructure/structural-alias-resolver.js +122 -0
- package/core/infrastructure/structural-candidate-ranker.js +34 -0
- package/core/infrastructure/structural-context-repository.js +472 -0
- package/core/infrastructure/structural-context-utils.js +51 -0
- package/core/infrastructure/structural-graph-signals.js +121 -0
- package/core/infrastructure/structural-qualified-resolution.js +15 -0
- package/core/infrastructure/structural-source-definitions.js +100 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
- package/core/infrastructure/tree-sitter-provider.js +811 -37
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
- package/core/query/query-router.js +55 -5
- package/core/ranking/file-kind-ranking.js +2192 -15
- package/core/ranking/late-interaction-index.js +87 -12
- package/core/search/cli-decoration.js +290 -0
- package/core/search/context-expander.js +988 -78
- package/core/search/index.js +1 -0
- package/core/search/output-policy.js +275 -0
- package/core/search/search-anchor.js +499 -0
- package/core/search/search-boost.js +93 -1
- package/core/search/search-cli.js +61 -204
- package/core/search/search-hybrid.js +250 -10
- package/core/search/search-pattern-chunks.js +57 -8
- package/core/search/search-pattern-planner.js +68 -9
- package/core/search/search-pattern-prefilter.js +30 -10
- package/core/search/search-pattern-ripgrep.js +40 -4
- package/core/search/search-pattern-sparse-overlay.js +256 -0
- package/core/search/search-pattern.js +117 -29
- package/core/search/search-postprocess.js +479 -5
- package/core/search/search-read-semantic.js +260 -23
- package/core/search/search-read.js +82 -64
- package/core/search/search-reader-pin.js +71 -0
- package/core/search/search-rrf.js +279 -0
- package/core/search/search-semantic.js +110 -5
- package/core/search/search-server.js +130 -57
- package/core/search/search-trace.js +107 -0
- package/core/search/server-identity.js +93 -0
- package/core/search/session-daemon-prewarm.mjs +33 -10
- package/core/search/sweet-search.js +399 -7
- package/core/skills/sweet-index/SKILL.md +8 -6
- package/core/vector-store/binary-hnsw-index.js +194 -30
- package/core/vector-store/float-vector-store.js +96 -6
- package/core/vector-store/hnsw-index.js +220 -49
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
- package/eval/agent-read-workflows/bin/ss-find +15 -0
- package/eval/agent-read-workflows/bin/ss-grep +12 -0
- package/eval/agent-read-workflows/bin/ss-read +14 -0
- package/eval/agent-read-workflows/bin/ss-search +18 -0
- package/eval/agent-read-workflows/bin/ss-semantic +12 -0
- package/eval/agent-read-workflows/bin/ss-trace +11 -0
- package/mcp/read-tool.js +109 -0
- package/mcp/server.js +55 -15
- package/mcp/tool-handlers.js +14 -124
- package/mcp/trace-tool.js +81 -0
- package/package.json +25 -10
- package/scripts/hooks/intercept-read.mjs +55 -0
- package/scripts/hooks/remind-tools.mjs +40 -0
- package/scripts/init.js +698 -54
- package/scripts/inject-agent-instructions.js +431 -0
- package/scripts/install-prompt-reminders.js +188 -0
- package/scripts/install-tool-enforcement.js +220 -0
- package/scripts/smoke-test.js +12 -9
- package/scripts/uninstall.js +276 -18
- 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
|
|
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
|
|
1057
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if (
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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 (P3 — plan §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
|
-
//
|
|
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
|
|