sift-light 1.0.0 → 1.0.1

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/src/mcp-server.ts CHANGED
@@ -8,10 +8,7 @@ import {
8
8
  createDefaultSiftLightMcpService,
9
9
  startSiftLightMcpServer,
10
10
  } from "./mcp.js";
11
- import {
12
- createMcpSemanticJudgeIntegration,
13
- mcpSemanticJudgeConfigSource,
14
- } from "./mcp-semantic-judge.js";
11
+ import { createMcpSearchFeatures, mcpSemanticJudgeConfigSource } from "./mcp-semantic-judge.js";
15
12
  import type { SemanticJudgeIntegration } from "./semantic-judge.js";
16
13
  import { parseSiftLightMcpTransport, SIFT_LIGHT_MCP_USAGE } from "./mcp-cli.js";
17
14
  import { parseSiftLightMcpOutputMode, type SiftLightMcpOutputMode } from "./mcp-output.js";
@@ -41,10 +38,6 @@ function allowedOrigins(): string[] {
41
38
  .filter((origin) => origin.length > 0);
42
39
  }
43
40
 
44
- async function configuredSemanticJudge(): Promise<SemanticJudgeIntegration> {
45
- return createMcpSemanticJudgeIntegration();
46
- }
47
-
48
41
  function logSemanticJudgeStatus(integration: SemanticJudgeIntegration): void {
49
42
  const status = integration.config.enabled ? "enabled" : "disabled";
50
43
  process.stderr.write(
@@ -55,12 +48,13 @@ function logSemanticJudgeStatus(integration: SemanticJudgeIntegration): void {
55
48
  async function runHttpServer(
56
49
  outputMode: SiftLightMcpOutputMode,
57
50
  semanticJudge: SemanticJudgeIntegration | undefined,
51
+ vectorSearchEnabled: boolean,
58
52
  ): Promise<void> {
59
53
  const running = await startSiftLightMcpServer({
60
54
  cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.cwd(),
61
55
  host: process.env.SIFT_LIGHT_MCP_HOST ?? DEFAULT_MCP_HOST,
62
56
  port: environmentInteger("SIFT_LIGHT_MCP_PORT", DEFAULT_MCP_PORT, 0, 65_535),
63
- createService: () => createDefaultSiftLightMcpService(semanticJudge),
57
+ createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
64
58
  maxSessions: environmentInteger(
65
59
  "SIFT_LIGHT_MCP_MAX_SESSIONS",
66
60
  DEFAULT_MCP_MAX_SESSIONS,
@@ -108,11 +102,12 @@ async function runHttpServer(
108
102
  async function runStdioServer(
109
103
  outputMode: SiftLightMcpOutputMode,
110
104
  semanticJudge: SemanticJudgeIntegration | undefined,
105
+ vectorSearchEnabled: boolean,
111
106
  ): Promise<void> {
112
107
  const running = await startSiftLightMcpStdioServer({
113
108
  cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
114
109
  outputMode,
115
- createService: () => createDefaultSiftLightMcpService(semanticJudge),
110
+ createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
116
111
  });
117
112
  process.stderr.write("sift-light MCP serving one local client over stdio\n");
118
113
  process.stderr.write(`sift-light MCP working directory: ${running.cwd}\n`);
@@ -140,13 +135,16 @@ async function main(): Promise<void> {
140
135
  return;
141
136
  }
142
137
  const outputMode = parseSiftLightMcpOutputMode(process.env.SIFT_LIGHT_MCP_OUTPUT_MODE);
143
- const semanticJudge = await configuredSemanticJudge();
138
+ const { semanticJudge, vectorSearchEnabled } = await createMcpSearchFeatures();
144
139
  logSemanticJudgeStatus(semanticJudge);
140
+ process.stderr.write(
141
+ `sift-light MCP vector search: ${vectorSearchEnabled ? "enabled" : "disabled"}\n`,
142
+ );
145
143
  if (transport === "stdio") {
146
- await runStdioServer(outputMode, semanticJudge);
144
+ await runStdioServer(outputMode, semanticJudge, vectorSearchEnabled);
147
145
  return;
148
146
  }
149
- await runHttpServer(outputMode, semanticJudge);
147
+ await runHttpServer(outputMode, semanticJudge, vectorSearchEnabled);
150
148
  }
151
149
 
152
150
  try {
package/src/mcp.ts CHANGED
@@ -93,12 +93,14 @@ export interface SiftLightMcpService {
93
93
 
94
94
  export function createDefaultSiftLightMcpService(
95
95
  semanticJudge?: SemanticJudgeIntegration,
96
+ vectorSearchEnabled = false,
96
97
  ): SiftLightMcpService {
97
98
  const resolvedSemanticJudge =
98
99
  semanticJudge ?? createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
99
100
  return new SiftLightService({
100
101
  runRipgrep: createRipgrepRunner(),
101
102
  structure: createCtagsStructureProvider(),
103
+ vectorSearchEnabled,
102
104
  semanticJudge: resolvedSemanticJudge,
103
105
  });
104
106
  }
@@ -73,19 +73,14 @@ export interface RequestContractProjection {
73
73
  text: string;
74
74
  }
75
75
 
76
- function projectRequestContract(
77
- projected: RequestContractDetails,
78
- serialized: string,
79
- message: string,
80
- ): string {
81
- const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
76
+ function projectRequestContract(projected: RequestContractDetails, serialized: string): string {
82
77
  const recovery = projected.recovery;
83
78
  const next = recovery.nextRequest
84
79
  ? "\nCopy the nested recovery.nextRequest object unchanged; do not repeat the original query."
85
- : `\nRecovery action: ${recovery.action}. ${recovery.reason}`;
80
+ : `\nRecovery action: ${recovery.action}.`;
86
81
  // Put the machine payload first so simple host adapters that locate the
87
82
  // first `nextRequest` marker see the exact nested request object.
88
- return `\nError details: ${serialized}\n${prefix}${next}`;
83
+ return `\nsift-light failed: request rejected [${projected.code}].\nError details: ${serialized}${next}`;
89
84
  }
90
85
 
91
86
  /** Build the one bounded contract projection consumed by structured and text hosts. */
@@ -94,11 +89,10 @@ export function requestContractProjection(
94
89
  ): RequestContractProjection {
95
90
  const details = boundedRequestContractDetails(error.details);
96
91
  const serializedDetails = JSON.stringify(details);
97
- const boundedMessage = error.message.toWellFormed().slice(0, 1_024);
98
- const result = projectRequestContract(details, serializedDetails, boundedMessage);
92
+ const result = projectRequestContract(details, serializedDetails);
99
93
  if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES) return { details, text: result };
100
94
  const compact: RequestContractDetails = {
101
- code: boundedMessage.length > 0 ? details.code : "E_REQUEST_CONTRACT_PAYLOAD",
95
+ code: details.code,
102
96
  ...(details.mode ? { mode: details.mode } : {}),
103
97
  issues: [
104
98
  {
@@ -111,10 +105,6 @@ export function requestContractProjection(
111
105
  reason: "Exact recovery was omitted; correct the request explicitly.",
112
106
  },
113
107
  };
114
- const compactText = projectRequestContract(
115
- compact,
116
- JSON.stringify(compact),
117
- "The request-contract error exceeded the bounded payload budget; correct the request explicitly.",
118
- );
108
+ const compactText = projectRequestContract(compact, JSON.stringify(compact));
119
109
  return { details: compact, text: compactText };
120
110
  }
package/src/omp-index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import {
4
- DEFAULT_SEMANTIC_JUDGE_CONFIG,
5
4
  normalizeSearchEnforcement,
6
5
  readSiftLightConfigFile,
7
6
  resolveSiftLightConfigPath,
@@ -27,7 +26,7 @@ import {
27
26
  } from "./search-policy.js";
28
27
  import { siftLightSchema } from "./tool-schema.js";
29
28
  import { modelErrorText } from "./model-error.js";
30
- import { createSemanticJudgeIntegration } from "./semantic-judge.js";
29
+ import { createConfiguredSemanticJudgeIntegration } from "./semantic-judge.js";
31
30
 
32
31
  const SIFT_LIGHT_LABEL = "sift-light";
33
32
  const OMP_REPLACED_SEARCH_TOOLS = new Set(["grep", "glob"]);
@@ -188,13 +187,12 @@ export async function registerOmpSiftLightExtension(
188
187
  (await readSiftLightConfigFile(resolveSiftLightConfigPath(ompAgentDir()), {
189
188
  missing: process.env[SIFT_LIGHT_CONFIG_ENV]?.trim() ? "error" : "defaults",
190
189
  }));
191
- const semanticJudge = createSemanticJudgeIntegration(
192
- resolvedConfig.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG,
193
- );
190
+ const semanticJudge = createConfiguredSemanticJudgeIntegration(resolvedConfig);
194
191
  const runtime = new SiftLightRuntime(
195
192
  new SiftLightService({
196
193
  runRipgrep: createRipgrepRunner(),
197
194
  structure: createCtagsStructureProvider(),
195
+ vectorSearchEnabled: resolvedConfig.vectorSearchEnabled === true,
198
196
  semanticJudge,
199
197
  }),
200
198
  );
@@ -218,7 +216,7 @@ export async function registerOmpSiftLightExtension(
218
216
  name: SIFT_LIGHT_LABEL,
219
217
  label: SIFT_LIGHT_LABEL,
220
218
  description:
221
- "Search and navigate code with bounded, verifiable evidence. Use pattern for content or mode=files with query for filenames.",
219
+ "Search code with bounded evidence. Routine searches use pattern for fast exact content or mode=files with query for filenames; concept/hybrid require vectorSearchEnabled:true in sift-light.json.",
222
220
  approval: "read",
223
221
  promptSnippet: "Search file contents without flooding context",
224
222
  promptGuidelines: siftLightPromptGuidelines(),
@@ -5,7 +5,7 @@ const SOURCE_OUTPUT_GUIDANCE =
5
5
 
6
6
  export function siftLightPromptGuidelines(structuredOutput = true): string[] {
7
7
  return [
8
- `Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
8
+ `Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} For routine development searches, start with fast exact content, filename or applicable structural modes when the request has a usable name, symbol, error text or other literal clue. Omitted mode is ordinary exact search and never loads the local embedding model. Vector search is disabled by default; concept/hybrid require vectorSearchEnabled:true in sift-light.json and an installed model. They can take tens of seconds on an uncached scope, so select them only when semantic recall is needed. Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
9
9
  `An omitted path searches the project cwd. Use scope:"strict" for a question restricted to one path; otherwise, if an explicit subpath has zero matches, ordinary and content-analysis searches retry from cwd and return project-wide counts with an expansion notice. Explicit absolute paths and .. traversal can search outside cwd, except protected external system areas and .git internals. Git changes mode remains cwd-scoped.`,
10
10
  `Search output includes counts, categories, ranked paths, coverage and continuation metadata. Source excerpts may contain the searched text. Use mode="inspect" or the host read capability when exact source is required for an edit or verification.`,
11
11
  `Use file and directory distributions to choose evidence. Reuse the visible cursor with path or paths for match metadata; mode="summary" pages the remaining file statistics. Match counts are not relevance scores.`,
@@ -14,7 +14,7 @@ export function siftLightPromptGuidelines(structuredOutput = true): string[] {
14
14
  `Use anyOf:["term1","term2"] when every exact occurrence of 2-64 literals is needed in one version-bound result. It is case-sensitive, reports anonymized condition counts, and runs requests above eight terms as bounded parallel chunks. Large condition inventories have separate continuation pages; copy those requests to retrieve the complete counts.`,
15
15
  `For a changed-code question, add changes:{base:"HEAD",scope:"lines",side:"new"}; omit target for the working tree, use side:"old" for deleted-side statistics. Copy returned continuation requests to preserve source versions.`,
16
16
  `Use mode:"capabilities" when the language or requested operation is unclear to get a compact lazy inventory. Use mode:"outline" with a concrete source file path for symbol counts and locations, mode:"imports" for static relationships, and mode:"tests" for related-test candidates. Their text pages summarize metadata; structured details can retain source evidence.`,
17
- `Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; business concepts belong in hybrid/concept. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
17
+ `Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; use hybrid/concept only for business concepts that cannot be located by a literal clue. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
18
18
  `Use mode:"structure" only for JS/TS/TSX/Go, with a required nonempty ast-grep pattern such as "compare($X, $X)" or "send()" for code shapes across whitespace; the text page reports structural counts and locations; structured details can retain matched source evidence.`,
19
19
  `Use mode:"concept" plus a natural-language query when names are unknown. It runs a pinned local multilingual model only after explicit installation; no search downloads weights or sends code to a remote model. Results expose candidate counts, score statistics, paths and the bounded ranked passage behind each candidate. Similarity scores identify candidates, not correctness.`,
20
20
  `Use mode:"hybrid" plus query when wording may differ from the source. It reports exact and semantic counts separately, removes overlap, and retains one pageable evidence snapshot. conceptLimit changes only the semantic candidate count; retained candidates keep their bounded source passages.`,
@@ -28,9 +28,9 @@ export function siftLightPromptGuidelines(structuredOutput = true): string[] {
28
28
 
29
29
  function siftLightModelGuidelines(): string[] {
30
30
  return [
31
- `Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE} Omit mode/limit for automatic detail/summary selection. Compact model analysis pages may defer source excerpts to inspect.`,
32
- `Use ranked paths and condition counts to choose evidence. Cursor continuation pages the retained snapshot; summary file selection returns ordinary match metadata. Use mode="inspect" when an edit requires exact source.`,
33
- `Modes: capabilities for language prerequisites; files+query for filenames; anyOf/allOf for literal condition statistics; outline/imports/tests for structural and relationship summaries; structure for AST match statistics; concept/hybrid for semantic candidate statistics. Similarity and static links are not proof.`,
31
+ `Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE} Default search is exact and model-free. concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model; uncached runs may take tens of seconds. Omit mode/limit for auto pages.`,
32
+ `Use ranked paths and counts; Cursor continuation pages retained results. Use mode="inspect" for exact source before editing.`,
33
+ `Modes: files+query for filenames; anyOf/allOf for literals; outline/imports/tests for static code; structure for AST; concept/hybrid for semantic candidates. Similarity is not proof.`,
34
34
  `On rejection keep the strongest applicable mode and apply the stated repair once. Do not repeat the rejected request or include its error. Only explicit capability-unavailable permits a visibly partial alternative.`,
35
35
  ];
36
36
  }
@@ -115,6 +115,23 @@ function schemaError(
115
115
  );
116
116
  }
117
117
 
118
+ function plainFilePatternRecovery(
119
+ mode: SiftLightMode,
120
+ input: Record<string, unknown>,
121
+ invalid: readonly string[],
122
+ ): boolean {
123
+ return (
124
+ mode === "files" &&
125
+ invalid.includes("pattern") &&
126
+ input.query === undefined &&
127
+ typeof input.pattern === "string" &&
128
+ input.pattern.trim().length > 0 &&
129
+ input.pattern.length <= 256 &&
130
+ input.pattern.isWellFormed() &&
131
+ !/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern)
132
+ );
133
+ }
134
+
118
135
  function safeNextRequest(
119
136
  input: Record<string, unknown>,
120
137
  mode: SiftLightMode,
@@ -122,9 +139,12 @@ function safeNextRequest(
122
139
  ): Record<string, unknown> | undefined {
123
140
  if (input.redact === true && containsSensitiveText(input)) return undefined;
124
141
  const safe = new Set<string>(SAFE_DROP_FIELDS[mode] ?? []);
142
+ const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
143
+ if (plainFilePattern) safe.add("pattern");
125
144
  if (invalid.some((field) => !safe.has(field))) return undefined;
126
145
  const next: Record<string, unknown> = { ...input };
127
146
  for (const field of invalid) delete next[field];
147
+ if (plainFilePattern) next.query = input.pattern;
128
148
  if (selectorIssuesFor(next, mode).length > 0) return undefined;
129
149
  // A copied request must remain within the transport's bounded line budget.
130
150
  try {
@@ -165,7 +185,14 @@ function fieldsError(
165
185
  // the signature of arguments shaped as a per-mode object, which the flat
166
186
  // schema never advertises. Naming the accepted fields replaces an otherwise
167
187
  // puzzling rejection with the rule and the valid names.
168
- const flatRule = `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`;
188
+ const nestedModeObject = invalid.includes(mode);
189
+ const flatRule = nestedModeObject
190
+ ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.`
191
+ : "";
192
+ const filesPatternRule =
193
+ mode === "files" && invalid.includes("pattern")
194
+ ? "For filename or path discovery, put the literal name text in query; pattern is a content-search regex, so check any regex syntax before copying it."
195
+ : "";
169
196
  return new RequestContractError(
170
197
  {
171
198
  code: "E_MODE_FIELDS",
@@ -174,17 +201,26 @@ function fieldsError(
174
201
  recovery: nextRequest
175
202
  ? {
176
203
  action: "retry",
177
- reason: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
204
+ reason: plainFilePatternRecovery(mode, input, invalid)
205
+ ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved."
206
+ : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
178
207
  nextRequest,
179
208
  }
180
209
  : {
181
210
  action: "manual",
182
- reason: `${reason}; choose the mode explicitly or remove the fields yourself without changing the requested scope. ${flatRule}`,
211
+ reason: [
212
+ reason,
213
+ filesPatternRule ||
214
+ "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
215
+ flatRule,
216
+ ]
217
+ .filter(Boolean)
218
+ .join(" "),
183
219
  },
184
220
  },
185
221
  nextRequest
186
- ? `${reason}; retry the exact nextRequest without repeating the original query.`
187
- : `${reason}; no semantics-preserving automatic request is available. ${flatRule} Preserve valid path, filters, redact and cursor fields when choosing the next request.`,
222
+ ? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.`
223
+ : `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`,
188
224
  );
189
225
  }
190
226
 
@@ -1,4 +1,8 @@
1
- import type { SemanticJudgeConfig } from "./config-reader.js";
1
+ import {
2
+ DEFAULT_SEMANTIC_JUDGE_CONFIG,
3
+ type SemanticJudgeConfig,
4
+ type SiftLightConfig,
5
+ } from "./config-reader.js";
2
6
  import {
3
7
  SEMANTIC_JUDGE_CLASSIFICATIONS,
4
8
  SEMANTIC_JUDGE_NON_PROOF_CLAIM,
@@ -134,8 +138,7 @@ function requestBody(
134
138
  for (const candidate of candidates) {
135
139
  questions[candidate.id] = {
136
140
  type: "choice",
137
- instructions:
138
- "Classify the candidate by what it actually does for the requested behavior. Judge the code excerpt, not just matching words.",
141
+ instructions: `Classify only candidate ${candidate.id} in state.candidates for state.query. Judge that candidate's excerpt by its actual behavior, not by matching words or the other candidates.`,
139
142
  criteria: {
140
143
  "implementation-candidate":
141
144
  "The excerpt appears to implement the requested behavior or its core decision/side effect.",
@@ -374,6 +377,17 @@ export function createSemanticJudgeIntegration(
374
377
  }
375
378
  return { config, runner: createJevRunner(config, key, fetcher) };
376
379
  }
380
+
381
+ export function createConfiguredSemanticJudgeIntegration(
382
+ config: SiftLightConfig,
383
+ environment: NodeJS.ProcessEnv = process.env,
384
+ ): SemanticJudgeIntegration {
385
+ const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
386
+ return config.vectorSearchEnabled === true
387
+ ? createSemanticJudgeIntegration(judge, environment)
388
+ : createDisabledSemanticJudgeIntegration(judge);
389
+ }
390
+
377
391
  function baseDetails(config: SemanticJudgeConfig): SemanticJudgeDetails {
378
392
  return {
379
393
  enabled: config.enabled,
package/src/service.ts CHANGED
@@ -87,6 +87,7 @@ export interface SiftLightServiceOptions {
87
87
  snapshots?: SnapshotStore;
88
88
  summaryFileLimit?: number;
89
89
  structure?: CodeStructureProvider;
90
+ vectorSearchEnabled?: boolean;
90
91
  conceptSearch?: ConceptSearchRunner;
91
92
  semanticJudge?: SemanticJudgeIntegration;
92
93
  }
@@ -367,6 +368,7 @@ export class SiftLightService {
367
368
  readonly #runRipgrep: RipgrepRunner;
368
369
  readonly #snapshots: SnapshotStore;
369
370
  readonly #summaryFileLimit: number;
371
+ readonly #vectorSearchEnabled: boolean;
370
372
  readonly #capabilities = new LanguageCapabilityCatalog();
371
373
  readonly #evidence: EvidenceService;
372
374
  #lifecycle = new AbortController();
@@ -378,6 +380,7 @@ export class SiftLightService {
378
380
  this.#runRipgrep = options.runRipgrep;
379
381
  this.#snapshots = options.snapshots ?? new SnapshotStore();
380
382
  this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
383
+ this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
381
384
  this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
382
385
  this.#evidence = new EvidenceService(
383
386
  this.#runRipgrep,
@@ -396,6 +399,11 @@ export class SiftLightService {
396
399
  ): Promise<SiftLightResult> {
397
400
  validateRawSearchInput(input);
398
401
  validateRequestContract(input);
402
+ if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
403
+ throw new SiftLightError(
404
+ `${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`,
405
+ );
406
+ }
399
407
  let request: Promise<SiftLightResult>;
400
408
  if (input.mode === "await" || input.mode === "cancel") {
401
409
  request = this.#operationCommand(input, cwd, signal);
@@ -37,15 +37,15 @@ function stringEnum<const Values extends readonly string[]>(
37
37
  });
38
38
  }
39
39
 
40
- export const SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
40
+ export const SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Routine searches should use exact content, filenames or applicable structural modes first. Omitted mode is ordinary exact search and does not load the embedding model; concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model and may take tens of seconds on an uncached scope. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
41
41
 
42
- export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
42
+ export const SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. Default exact search is model-free; concept/hybrid require vectorSearchEnabled:true in sift-light.json and a model. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
43
43
 
44
44
  export const siftLightSchema = Type.Object({
45
45
  query: Type.Optional(
46
46
  Type.String({
47
47
  maxLength: 256,
48
- description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`,
48
+ description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require vectorSearchEnabled:true in sift-light.json and an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`,
49
49
  }),
50
50
  ),
51
51
  scope: Type.Optional(