feat(code-intelligence): add sift indexer backend alongside project-rag
Generalize the code-intelligence hook to support multiple indexer backends, with sift (rupurt/sift) as a new option next to the existing project-rag MCP server. Backend is selected via CodebaseMapPreferences. - code-intelligence.ts: new abstraction + sift backend (detect, resolve, status, context-block contribution) - preferences-types.ts: codebaseIndexer field (project-rag | sift | none) - preferences-validation.ts: validate the new field - bootstrap/system-context.ts, commands-codebase.ts: dispatch on backend - tests/code-intelligence.test.ts: sift detection/resolution/status tests (19 pass, 0 fail) project-rag path unchanged and continues to work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0606983d97
commit
8e827147c9
9 changed files with 381 additions and 29 deletions
|
|
@ -150,9 +150,10 @@ export async function buildBeforeAgentStartResult(
|
|||
}
|
||||
|
||||
try {
|
||||
const codebasePrefs = loadedPreferences?.preferences?.codebase;
|
||||
codeIntelligenceBlock = buildCodeIntelligenceContextBlock(
|
||||
process.cwd(),
|
||||
loadedPreferences?.preferences?.codebase,
|
||||
codebasePrefs,
|
||||
);
|
||||
} catch (e) {
|
||||
logWarning("bootstrap", `code intelligence block failed: ${(e as Error).message}`);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
/**
|
||||
* Optional code-intelligence backends for SF.
|
||||
*
|
||||
* CODEBASE.md stays the durable baseline. Project RAG is an optional MCP
|
||||
* accelerator for local hybrid vector + BM25 code retrieval.
|
||||
* CODEBASE.md stays the durable baseline. Codebase indexers are optional
|
||||
* accelerators for local code retrieval.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { delimiter, join, resolve } from "node:path";
|
||||
|
||||
import type { CodebaseMapPreferences } from "./preferences-types.js";
|
||||
import type { CodebaseIndexerBackendName, CodebaseMapPreferences } from "./preferences-types.js";
|
||||
|
||||
export const PROJECT_RAG_MCP_SERVER_NAME = "project-rag";
|
||||
const PROJECT_RAG_BINARY_NAME = process.platform === "win32" ? "project-rag.exe" : "project-rag";
|
||||
const SIFT_BINARY_NAME = process.platform === "win32" ? "sift.exe" : "sift";
|
||||
|
||||
const PROJECT_RAG_SOURCE_CANDIDATES = [
|
||||
"vendor/project-rag",
|
||||
|
|
@ -37,7 +38,29 @@ interface McpConfigFile {
|
|||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ProjectRagDetection {
|
||||
export type CodebaseIndexerStatus = "disabled" | "configured" | "missing";
|
||||
|
||||
export interface CodebaseIndexerDetection {
|
||||
backend: CodebaseIndexerBackendName;
|
||||
status: CodebaseIndexerStatus;
|
||||
reason: string;
|
||||
serverName?: string;
|
||||
configPath?: string;
|
||||
command?: string;
|
||||
binaryPath?: string;
|
||||
sourceDir?: string;
|
||||
}
|
||||
|
||||
export interface CodebaseIndexerBackend<TDetection extends CodebaseIndexerDetection = CodebaseIndexerDetection> {
|
||||
name: CodebaseIndexerBackendName;
|
||||
label: string;
|
||||
detect(projectRoot: string, prefs?: CodebaseMapPreferences, env?: NodeJS.ProcessEnv): TDetection;
|
||||
formatStatus(projectRoot: string, prefs?: CodebaseMapPreferences, env?: NodeJS.ProcessEnv): string;
|
||||
buildContextLines(projectRoot: string, prefs?: CodebaseMapPreferences, env?: NodeJS.ProcessEnv): string[];
|
||||
}
|
||||
|
||||
export interface ProjectRagDetection extends CodebaseIndexerDetection {
|
||||
backend: "projectRag";
|
||||
status: "disabled" | "configured" | "missing";
|
||||
serverName?: string;
|
||||
configPath?: string;
|
||||
|
|
@ -47,6 +70,14 @@ export interface ProjectRagDetection {
|
|||
reason: string;
|
||||
}
|
||||
|
||||
export interface SiftDetection extends CodebaseIndexerDetection {
|
||||
backend: "sift";
|
||||
status: "disabled" | "configured" | "missing";
|
||||
command?: string;
|
||||
binaryPath?: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface EnsureProjectRagMcpConfigResult {
|
||||
configPath: string;
|
||||
serverName: string;
|
||||
|
|
@ -110,24 +141,26 @@ function commandExists(command: string | undefined, env: NodeJS.ProcessEnv = pro
|
|||
export function detectProjectRag(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ProjectRagDetection {
|
||||
const mode = prefs?.project_rag ?? "auto";
|
||||
if (mode === "off") {
|
||||
return { status: "disabled", reason: "codebase.project_rag is off" };
|
||||
return { backend: "projectRag", status: "disabled", reason: "codebase.project_rag is off" };
|
||||
}
|
||||
|
||||
const configuredServer = prefs?.project_rag_server?.trim();
|
||||
const normalizedRoot = normalizeProjectRoot(projectRoot);
|
||||
const binaryPath = resolveProjectRagBinaryForProject(normalizedRoot, process.env) ?? undefined;
|
||||
const sourceDir = findProjectRagSourceDir(normalizedRoot, process.env) ?? undefined;
|
||||
const binaryPath = resolveProjectRagBinaryForProject(normalizedRoot, env) ?? undefined;
|
||||
const sourceDir = findProjectRagSourceDir(normalizedRoot, env) ?? undefined;
|
||||
const entries = readMcpConfigEntries(normalizedRoot);
|
||||
const match = entries.find(({ name, config }) =>
|
||||
configuredServer ? name === configuredServer : configLooksLikeProjectRag(name, config)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const configuredCommandExists = commandExists(match.config.command);
|
||||
const configuredCommandExists = commandExists(match.config.command, env);
|
||||
return {
|
||||
backend: "projectRag",
|
||||
status: "configured",
|
||||
serverName: match.name,
|
||||
configPath: match.configPath,
|
||||
|
|
@ -141,6 +174,7 @@ export function detectProjectRag(
|
|||
}
|
||||
|
||||
return {
|
||||
backend: "projectRag",
|
||||
status: "missing",
|
||||
binaryPath,
|
||||
sourceDir,
|
||||
|
|
@ -151,7 +185,7 @@ export function detectProjectRag(
|
|||
}
|
||||
|
||||
function lookupExecutable(command: string, env: NodeJS.ProcessEnv = process.env): string | null {
|
||||
if (command.includes("/") && existsSync(command)) return command;
|
||||
if (/[\\/]/.test(command) && existsSync(command)) return command;
|
||||
const pathValue = env.PATH ?? "";
|
||||
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
||||
const candidate = join(dir, command);
|
||||
|
|
@ -166,6 +200,55 @@ export function resolveProjectRagBinary(env: NodeJS.ProcessEnv = process.env): s
|
|||
return lookupExecutable("project-rag", env);
|
||||
}
|
||||
|
||||
export function resolveSiftBinary(env: NodeJS.ProcessEnv = process.env): string | null {
|
||||
const explicit = env.SIFT_PATH?.trim();
|
||||
if (explicit) return explicit;
|
||||
return lookupExecutable(SIFT_BINARY_NAME, env)
|
||||
?? (SIFT_BINARY_NAME === "sift" ? null : lookupExecutable("sift", env));
|
||||
}
|
||||
|
||||
export function detectSift(
|
||||
_projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): SiftDetection {
|
||||
if (prefs?.indexer_backend === "none") {
|
||||
return {
|
||||
backend: "sift",
|
||||
status: "disabled",
|
||||
reason: "codebase.indexer_backend is none",
|
||||
};
|
||||
}
|
||||
|
||||
const explicit = env.SIFT_PATH?.trim();
|
||||
const binaryPath = resolveSiftBinary(env) ?? undefined;
|
||||
if (!binaryPath) {
|
||||
return {
|
||||
backend: "sift",
|
||||
status: "missing",
|
||||
reason: "sift binary not found on PATH; set SIFT_PATH or install rupurt/sift.",
|
||||
};
|
||||
}
|
||||
|
||||
if (explicit && !commandExists(explicit, env)) {
|
||||
return {
|
||||
backend: "sift",
|
||||
status: "missing",
|
||||
command: explicit,
|
||||
binaryPath: explicit,
|
||||
reason: "SIFT_PATH is set but does not resolve to an executable file.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
backend: "sift",
|
||||
status: "configured",
|
||||
command: binaryPath,
|
||||
binaryPath,
|
||||
reason: explicit ? "sift binary resolved from SIFT_PATH" : "sift binary found on PATH",
|
||||
};
|
||||
}
|
||||
|
||||
function projectRagBinaryFromSource(sourceDir: string): string | null {
|
||||
const candidate = join(sourceDir, "target", "release", PROJECT_RAG_BINARY_NAME);
|
||||
return existsSync(candidate) ? candidate : null;
|
||||
|
|
@ -328,16 +411,13 @@ function formatToolPrefix(serverName: string): string {
|
|||
return `mcp__${serverName.replace(/[^A-Za-z0-9_]/g, "_")}__`;
|
||||
}
|
||||
|
||||
export function buildCodeIntelligenceContextBlock(
|
||||
function buildProjectRagContextLines(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
): string {
|
||||
const detection = detectProjectRag(projectRoot, prefs);
|
||||
const lines = [
|
||||
"[PROJECT CODE INTELLIGENCE]",
|
||||
"",
|
||||
"- Durable baseline: use `.sf/CODEBASE.md` for structural orientation and persistent project knowledge.",
|
||||
];
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const detection = detectProjectRag(projectRoot, prefs, env);
|
||||
const lines: string[] = [];
|
||||
|
||||
if (detection.status === "disabled") {
|
||||
lines.push("- Project RAG: disabled by `codebase.project_rag: off`.");
|
||||
|
|
@ -364,11 +444,100 @@ export function buildCodeIntelligenceContextBlock(
|
|||
lines.push("- To enable later: build/install Brainwires/project-rag, then run `/sf codebase rag init` or set `SF_PROJECT_RAG_BIN` before initializing MCP config.");
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function buildSiftContextLines(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const detection = detectSift(projectRoot, prefs, env);
|
||||
const lines: string[] = [];
|
||||
|
||||
if (detection.status === "disabled") {
|
||||
lines.push("- Codebase indexer: disabled by `codebase.indexer_backend: none`.");
|
||||
} else if (detection.status === "configured" && detection.binaryPath) {
|
||||
lines.push(`- Sift: configured as local CLI \`${detection.binaryPath}\`.`);
|
||||
lines.push(
|
||||
"- Use Sift for broad code retrieval before manual file-by-file reading, " +
|
||||
"especially conceptual queries, exact identifiers, approximate file/path intent, and synthesis-ready snippets.",
|
||||
);
|
||||
lines.push(
|
||||
"- Command shape: `sift search --json <path> \"<query>\"`. " +
|
||||
"Use `--strategy page-index-hybrid` for the strongest direct-search preset, " +
|
||||
"`--strategy path-hybrid` for path-heavy queries, and `sift search <path> --agent \"<task>\"` for bounded planner-driven exploration.",
|
||||
);
|
||||
lines.push(
|
||||
"- Sift uses a sector-aware cache in the platform cache directory, typically `~/.cache/sift`; " +
|
||||
"if the CLI is missing or fails, continue with `.sf/CODEBASE.md`, `rg`, `lsp`, and scout.",
|
||||
);
|
||||
} else {
|
||||
lines.push("- Sift: not available. This is optional; continue with `.sf/CODEBASE.md`, `rg`, `lsp`, and scout.");
|
||||
lines.push("- To enable later: install `rupurt/sift` on PATH or set `SIFT_PATH` to the sift binary.");
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function buildNoCodebaseIndexerContextLines(): string[] {
|
||||
return [
|
||||
"- Codebase indexer: disabled by `codebase.indexer_backend: none`; continue with `.sf/CODEBASE.md`, `rg`, `lsp`, and scout.",
|
||||
];
|
||||
}
|
||||
|
||||
export function resolveCodebaseIndexerBackendName(
|
||||
prefs?: CodebaseMapPreferences,
|
||||
): CodebaseIndexerBackendName {
|
||||
return prefs?.indexer_backend ?? "projectRag";
|
||||
}
|
||||
|
||||
export function getCodebaseIndexerBackend(
|
||||
prefsOrName?: CodebaseMapPreferences | CodebaseIndexerBackendName,
|
||||
): CodebaseIndexerBackend {
|
||||
const name = typeof prefsOrName === "string"
|
||||
? prefsOrName
|
||||
: resolveCodebaseIndexerBackendName(prefsOrName);
|
||||
return CODEBASE_INDEXER_BACKENDS[name];
|
||||
}
|
||||
|
||||
export function detectCodebaseIndexer(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): CodebaseIndexerDetection {
|
||||
return getCodebaseIndexerBackend(prefs).detect(projectRoot, prefs, env);
|
||||
}
|
||||
|
||||
export function formatCodebaseIndexerStatus(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return getCodebaseIndexerBackend(prefs).formatStatus(projectRoot, prefs, env);
|
||||
}
|
||||
|
||||
export function buildCodeIntelligenceContextBlock(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
const lines = [
|
||||
"[PROJECT CODE INTELLIGENCE]",
|
||||
"",
|
||||
"- Durable baseline: use `.sf/CODEBASE.md` for structural orientation and persistent project knowledge.",
|
||||
...getCodebaseIndexerBackend(prefs).buildContextLines(projectRoot, prefs, env),
|
||||
];
|
||||
|
||||
return `\n\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
export function formatProjectRagStatus(projectRoot: string, prefs?: CodebaseMapPreferences): string {
|
||||
const detection = detectProjectRag(projectRoot, prefs);
|
||||
export function formatProjectRagStatus(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
const detection = detectProjectRag(projectRoot, prefs, env);
|
||||
const lines = ["Project RAG Status", ""];
|
||||
lines.push(`Status: ${detection.status}`);
|
||||
lines.push(`Reason: ${detection.reason}`);
|
||||
|
|
@ -378,7 +547,7 @@ export function formatProjectRagStatus(projectRoot: string, prefs?: CodebaseMapP
|
|||
if (detection.binaryPath) lines.push(`Binary: ${detection.binaryPath}`);
|
||||
if (detection.sourceDir) lines.push(`Source: ${detection.sourceDir}`);
|
||||
if (detection.status === "configured" && detection.command) {
|
||||
lines.push(`Operational: ${commandExists(detection.command) ? "yes" : "no - configured command is missing"}`);
|
||||
lines.push(`Operational: ${commandExists(detection.command, env) ? "yes" : "no - configured command is missing"}`);
|
||||
} else if (detection.binaryPath) {
|
||||
lines.push("Operational: no - binary exists but MCP config is missing; run /sf codebase rag init.");
|
||||
} else if (detection.sourceDir) {
|
||||
|
|
@ -391,3 +560,72 @@ export function formatProjectRagStatus(projectRoot: string, prefs?: CodebaseMapP
|
|||
lines.push("When configured, agents should use index_codebase, query_codebase, search_by_filters, find_definition, find_references, and get_call_graph before manual file-by-file reading.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function formatSiftStatus(
|
||||
projectRoot: string,
|
||||
prefs?: CodebaseMapPreferences,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
const detection = detectSift(projectRoot, prefs, env);
|
||||
const lines = ["Sift Status", ""];
|
||||
lines.push(`Status: ${detection.status}`);
|
||||
lines.push(`Reason: ${detection.reason}`);
|
||||
if (detection.command) lines.push(`Command: ${detection.command}`);
|
||||
if (detection.binaryPath) lines.push(`Binary: ${detection.binaryPath}`);
|
||||
if (detection.status === "configured" && detection.command) {
|
||||
lines.push(`Operational: ${commandExists(detection.command, env) ? "yes" : "no - configured command is missing"}`);
|
||||
} else {
|
||||
lines.push("Operational: no - install rupurt/sift on PATH or set SIFT_PATH.");
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("Sift is optional. SF falls back to CODEBASE.md, rg, lsp, and scout when it is unavailable.");
|
||||
lines.push("When configured, agents should use `sift search --json <path> \"<query>\"`; `page-index-hybrid` is the strongest direct-search preset and `path-hybrid` is best for path-heavy queries.");
|
||||
lines.push("Sift reuses a sector-aware cache in the platform cache directory, typically ~/.cache/sift.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatNoCodebaseIndexerStatus(): string {
|
||||
return [
|
||||
"Codebase Indexer Status",
|
||||
"",
|
||||
"Status: disabled",
|
||||
"Reason: codebase.indexer_backend is none",
|
||||
"Operational: no - optional codebase indexer disabled.",
|
||||
"",
|
||||
"SF will use CODEBASE.md, rg, lsp, and scout for codebase orientation.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export const PROJECT_RAG_CODEBASE_INDEXER_BACKEND: CodebaseIndexerBackend<ProjectRagDetection> = {
|
||||
name: "projectRag",
|
||||
label: "Project RAG",
|
||||
detect: detectProjectRag,
|
||||
formatStatus: formatProjectRagStatus,
|
||||
buildContextLines: buildProjectRagContextLines,
|
||||
};
|
||||
|
||||
export const SIFT_CODEBASE_INDEXER_BACKEND: CodebaseIndexerBackend<SiftDetection> = {
|
||||
name: "sift",
|
||||
label: "Sift",
|
||||
detect: detectSift,
|
||||
formatStatus: formatSiftStatus,
|
||||
buildContextLines: buildSiftContextLines,
|
||||
};
|
||||
|
||||
export const NO_CODEBASE_INDEXER_BACKEND: CodebaseIndexerBackend = {
|
||||
name: "none",
|
||||
label: "None",
|
||||
detect: () => ({
|
||||
backend: "none",
|
||||
status: "disabled",
|
||||
reason: "codebase.indexer_backend is none",
|
||||
}),
|
||||
formatStatus: formatNoCodebaseIndexerStatus,
|
||||
buildContextLines: buildNoCodebaseIndexerContextLines,
|
||||
};
|
||||
|
||||
export const CODEBASE_INDEXER_BACKENDS: Record<CodebaseIndexerBackendName, CodebaseIndexerBackend> = {
|
||||
projectRag: PROJECT_RAG_CODEBASE_INDEXER_BACKEND,
|
||||
sift: SIFT_CODEBASE_INDEXER_BACKEND,
|
||||
none: NO_CODEBASE_INDEXER_BACKEND,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* SF Command — /sf codebase
|
||||
*
|
||||
* Generate and manage the codebase map (.sf/CODEBASE.md).
|
||||
* Subcommands: generate, update, stats, rag, help
|
||||
* Subcommands: generate, update, stats, indexer, rag, help
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI, ExtensionCommandContext } from "@singularity-forge/pi-coding-agent";
|
||||
|
|
@ -17,17 +17,18 @@ import {
|
|||
import {
|
||||
buildProjectRagBinary,
|
||||
ensureProjectRagMcpConfig,
|
||||
formatProjectRagStatus,
|
||||
formatCodebaseIndexerStatus,
|
||||
} from "./code-intelligence.js";
|
||||
import { loadEffectiveSFPreferences } from "./preferences.js";
|
||||
import type { CodebaseMapOptions } from "./codebase-generator.js";
|
||||
|
||||
const USAGE =
|
||||
"Usage: /sf codebase [generate|update|stats|rag]\n\n" +
|
||||
"Usage: /sf codebase [generate|update|stats|indexer|rag]\n\n" +
|
||||
" generate [--max-files N] [--collapse-threshold N] — Generate or regenerate CODEBASE.md\n" +
|
||||
" update [--max-files N] [--collapse-threshold N] — Refresh the CODEBASE.md cache immediately\n" +
|
||||
" stats — Show file count, coverage, and generation time\n" +
|
||||
" rag [status|init|build] — Inspect, build, or configure optional project-rag MCP backend\n" +
|
||||
" indexer [status] — Inspect selected optional codebase-indexer backend\n" +
|
||||
" rag [status|init|build] — Inspect selected backend, or build/configure project-rag MCP\n" +
|
||||
" help — Show this help\n\n" +
|
||||
"With no subcommand, shows stats if a map exists or help if not.\n" +
|
||||
"SF also refreshes CODEBASE.md automatically before prompt injection and after completed units when tracked files change.\n\n" +
|
||||
|
|
@ -36,8 +37,9 @@ const USAGE =
|
|||
" exclude_patterns: [\"docs/\", \"fixtures/\"]\n" +
|
||||
" max_files: 1000\n" +
|
||||
" collapse_threshold: 15\n" +
|
||||
" project_rag: auto # auto | off | required\n" +
|
||||
" project_rag_auto_index: true";
|
||||
" indexer_backend: projectRag # projectRag | sift | none\n" +
|
||||
" project_rag: auto # auto | off | required\n" +
|
||||
" project_rag_auto_index: true";
|
||||
|
||||
export async function handleCodebase(
|
||||
args: string,
|
||||
|
|
@ -109,11 +111,22 @@ export async function handleCodebase(
|
|||
return;
|
||||
}
|
||||
|
||||
case "indexer": {
|
||||
const action = (parts[1] ?? "status").toLowerCase();
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences?.codebase;
|
||||
if (action === "status") {
|
||||
ctx.ui.notify(formatCodebaseIndexerStatus(basePath, prefs), "info");
|
||||
return;
|
||||
}
|
||||
ctx.ui.notify(`Unknown /sf codebase indexer action "${action}". Use status.`, "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
case "rag": {
|
||||
const action = (parts[1] ?? "status").toLowerCase();
|
||||
const prefs = loadEffectiveSFPreferences()?.preferences?.codebase;
|
||||
if (action === "status") {
|
||||
ctx.ui.notify(formatProjectRagStatus(basePath, prefs), "info");
|
||||
ctx.ui.notify(formatCodebaseIndexerStatus(basePath, prefs), "info");
|
||||
return;
|
||||
}
|
||||
if (action === "init") {
|
||||
|
|
|
|||
|
|
@ -163,13 +163,15 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|||
- `skip_reassess`: boolean — force-disable roadmap reassessment even if `reassess_after_slice` is enabled. Default: `false`.
|
||||
- `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
|
||||
|
||||
- `codebase`: configures `.sf/CODEBASE.md` and the optional Project RAG code-intelligence backend. Keys:
|
||||
- `codebase`: configures `.sf/CODEBASE.md` and the optional code-intelligence backend. Keys:
|
||||
- `exclude_patterns`: string[] — extra file or directory patterns to omit from CODEBASE.md.
|
||||
- `max_files`: number — maximum files to include in CODEBASE.md. Default: `500`.
|
||||
- `collapse_threshold`: number — files-per-directory threshold before collapsing a directory summary. Default: `20`.
|
||||
- `indexer_backend`: `"projectRag"`, `"sift"`, or `"none"` — codebase-indexer backend used for prompt guidance and `/sf codebase indexer status`. Default: `"projectRag"`.
|
||||
- `project_rag`: `"auto"`, `"off"`, or `"required"` — use Brainwires/project-rag MCP search when configured. Default: `"auto"`.
|
||||
- `project_rag_server`: string — explicit MCP server name when the server cannot be detected from command or args.
|
||||
- `project_rag_auto_index`: boolean — whether agents should prefer indexing before querying a configured Project RAG backend. Default: `true`.
|
||||
- `/sf codebase indexer status` reports the selected backend status. For `sift`, install `rupurt/sift` on `PATH` or set `SIFT_PATH`.
|
||||
- `/sf codebase rag status` reports whether the Rust backend is actually operational.
|
||||
- `/sf codebase rag init` writes a `.mcp.json` entry when a `project-rag` binary is available.
|
||||
- `/sf codebase rag build` builds vendored Brainwires/project-rag from `vendor/project-rag` (or `SF_PROJECT_RAG_SOURCE`) with `cargo build --release`, then writes the MCP config. The build defaults to `CARGO_BUILD_JOBS=2` so it does not saturate the workstation; override with `SF_PROJECT_RAG_BUILD_JOBS`.
|
||||
|
|
|
|||
|
|
@ -306,6 +306,8 @@ export interface ExperimentalPreferences {
|
|||
dispatch_rules?: DispatchExperimentPreferences;
|
||||
}
|
||||
|
||||
export type CodebaseIndexerBackendName = "projectRag" | "sift" | "none";
|
||||
|
||||
/** Configuration for the codebase map generator (/sf codebase). */
|
||||
export interface CodebaseMapPreferences {
|
||||
/** Additional directory/file patterns to exclude (e.g. ["docs/", "fixtures/"]). Merged with built-in defaults. */
|
||||
|
|
@ -314,6 +316,8 @@ export interface CodebaseMapPreferences {
|
|||
max_files?: number;
|
||||
/** Files-per-directory threshold before collapsing to a summary line. Default: 20. */
|
||||
collapse_threshold?: number;
|
||||
/** Optional codebase-indexer backend. Default: "projectRag" for backward compatibility. */
|
||||
indexer_backend?: CodebaseIndexerBackendName;
|
||||
/** Optional Brainwires/project-rag MCP backend. Default: "auto" (use if configured, never block if missing). */
|
||||
project_rag?: "auto" | "off" | "required";
|
||||
/** MCP server name for project-rag when it cannot be detected from command/args. */
|
||||
|
|
|
|||
|
|
@ -1325,6 +1325,13 @@ export function validatePreferences(preferences: SFPreferences): {
|
|||
errors.push("codebase.collapse_threshold must be a positive integer");
|
||||
}
|
||||
}
|
||||
if (cb.indexer_backend !== undefined) {
|
||||
if (cb.indexer_backend === "projectRag" || cb.indexer_backend === "sift" || cb.indexer_backend === "none") {
|
||||
validCb.indexer_backend = cb.indexer_backend;
|
||||
} else {
|
||||
errors.push('codebase.indexer_backend must be one of "projectRag", "sift", or "none"');
|
||||
}
|
||||
}
|
||||
if (cb.project_rag !== undefined) {
|
||||
if (cb.project_rag === "auto" || cb.project_rag === "off" || cb.project_rag === "required") {
|
||||
validCb.project_rag = cb.project_rag;
|
||||
|
|
@ -1351,6 +1358,7 @@ export function validatePreferences(preferences: SFPreferences): {
|
|||
"exclude_patterns",
|
||||
"max_files",
|
||||
"collapse_threshold",
|
||||
"indexer_backend",
|
||||
"project_rag",
|
||||
"project_rag_server",
|
||||
"project_rag_auto_index",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export type {
|
|||
CmuxPreferences,
|
||||
UokTurnActionMode,
|
||||
UokPreferences,
|
||||
CodebaseIndexerBackendName,
|
||||
CodebaseMapPreferences,
|
||||
SFPreferences,
|
||||
LoadedSFPreferences,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import { tmpdir } from "node:os";
|
|||
import {
|
||||
buildCodeIntelligenceContextBlock,
|
||||
detectProjectRag,
|
||||
detectSift,
|
||||
ensureProjectRagMcpConfig,
|
||||
findProjectRagSourceDir,
|
||||
formatProjectRagStatus,
|
||||
formatSiftStatus,
|
||||
PROJECT_RAG_MCP_SERVER_NAME,
|
||||
resolveProjectRagBuildJobs,
|
||||
resolveProjectRagBinaryForProject,
|
||||
resolveSiftBinary,
|
||||
} from "../code-intelligence.ts";
|
||||
|
||||
function makeProject(): string {
|
||||
|
|
@ -25,6 +28,14 @@ function cleanup(projectRoot: string): void {
|
|||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function writeFakeSiftBinary(projectRoot: string): string {
|
||||
const binDir = join(projectRoot, "bin");
|
||||
const binaryPath = join(binDir, process.platform === "win32" ? "sift.exe" : "sift");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(binaryPath, "", "utf-8");
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
test("detectProjectRag finds a project-rag server even when the MCP name is generic", () => {
|
||||
const projectRoot = makeProject();
|
||||
try {
|
||||
|
|
@ -192,3 +203,71 @@ test("buildCodeIntelligenceContextBlock injects project-rag usage guidance when
|
|||
cleanup(projectRoot);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveSiftBinary honors SIFT_PATH before PATH lookup", () => {
|
||||
const projectRoot = makeProject();
|
||||
try {
|
||||
const fakeSift = writeFakeSiftBinary(projectRoot);
|
||||
|
||||
assert.equal(resolveSiftBinary({ SIFT_PATH: "/custom/sift", PATH: join(projectRoot, "bin") }), "/custom/sift");
|
||||
assert.equal(resolveSiftBinary({ PATH: join(projectRoot, "bin") }), fakeSift);
|
||||
assert.equal(resolveSiftBinary({ PATH: "" }), null);
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
});
|
||||
|
||||
test("detectSift finds mocked sift binary on PATH and honors disabled backend", () => {
|
||||
const projectRoot = makeProject();
|
||||
try {
|
||||
const fakeSift = writeFakeSiftBinary(projectRoot);
|
||||
|
||||
const detection = detectSift(projectRoot, { indexer_backend: "sift" }, { PATH: join(projectRoot, "bin") });
|
||||
assert.equal(detection.status, "configured");
|
||||
assert.equal(detection.binaryPath, fakeSift);
|
||||
assert.match(detection.reason, /PATH/);
|
||||
|
||||
const disabled = detectSift(projectRoot, { indexer_backend: "none" }, { PATH: join(projectRoot, "bin") });
|
||||
assert.equal(disabled.status, "disabled");
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
});
|
||||
|
||||
test("formatSiftStatus reports configured and missing sift backends without running sift", () => {
|
||||
const projectRoot = makeProject();
|
||||
try {
|
||||
const fakeSift = writeFakeSiftBinary(projectRoot);
|
||||
|
||||
const configured = formatSiftStatus(projectRoot, { indexer_backend: "sift" }, { PATH: join(projectRoot, "bin") });
|
||||
assert.match(configured, /Sift Status/);
|
||||
assert.match(configured, /Status: configured/);
|
||||
assert.match(configured, new RegExp(fakeSift.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
assert.match(configured, /sift search --json/);
|
||||
|
||||
const missing = formatSiftStatus(projectRoot, { indexer_backend: "sift" }, { PATH: "" });
|
||||
assert.match(missing, /Status: missing/);
|
||||
assert.match(missing, /SIFT_PATH/);
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
});
|
||||
|
||||
test("buildCodeIntelligenceContextBlock injects sift guidance when selected", () => {
|
||||
const projectRoot = makeProject();
|
||||
try {
|
||||
writeFakeSiftBinary(projectRoot);
|
||||
|
||||
const block = buildCodeIntelligenceContextBlock(
|
||||
projectRoot,
|
||||
{ indexer_backend: "sift" },
|
||||
{ PATH: join(projectRoot, "bin") },
|
||||
);
|
||||
assert.match(block, /PROJECT CODE INTELLIGENCE/);
|
||||
assert.match(block, /Sift: configured/);
|
||||
assert.match(block, /sift search --json/);
|
||||
assert.match(block, /page-index-hybrid/);
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -651,6 +651,7 @@ test("codebase preferences validate and pass through correctly", () => {
|
|||
exclude_patterns: ["docs/", "fixtures/"],
|
||||
max_files: 1000,
|
||||
collapse_threshold: 15,
|
||||
indexer_backend: "sift",
|
||||
project_rag: "auto",
|
||||
project_rag_server: "project",
|
||||
project_rag_auto_index: false,
|
||||
|
|
@ -660,6 +661,7 @@ test("codebase preferences validate and pass through correctly", () => {
|
|||
assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/", "fixtures/"]);
|
||||
assert.equal(result.preferences.codebase?.max_files, 1000);
|
||||
assert.equal(result.preferences.codebase?.collapse_threshold, 15);
|
||||
assert.equal(result.preferences.codebase?.indexer_backend, "sift");
|
||||
assert.equal(result.preferences.codebase?.project_rag, "auto");
|
||||
assert.equal(result.preferences.codebase?.project_rag_server, "project");
|
||||
assert.equal(result.preferences.codebase?.project_rag_auto_index, false);
|
||||
|
|
@ -671,6 +673,7 @@ test("codebase preferences reject invalid types", () => {
|
|||
exclude_patterns: "not-an-array" as any,
|
||||
max_files: -5,
|
||||
collapse_threshold: 0,
|
||||
indexer_backend: "project-rag" as any,
|
||||
project_rag: "yes" as any,
|
||||
project_rag_server: "",
|
||||
project_rag_auto_index: "yes" as any,
|
||||
|
|
@ -679,6 +682,7 @@ test("codebase preferences reject invalid types", () => {
|
|||
assert.ok(result.errors.some(e => e.includes("exclude_patterns must be an array")));
|
||||
assert.ok(result.errors.some(e => e.includes("max_files must be a positive")));
|
||||
assert.ok(result.errors.some(e => e.includes("collapse_threshold must be a positive")));
|
||||
assert.ok(result.errors.some(e => e.includes("indexer_backend must be one of")));
|
||||
assert.ok(result.errors.some(e => e.includes("project_rag must be one of")));
|
||||
assert.ok(result.errors.some(e => e.includes("project_rag_server must be a non-empty string")));
|
||||
assert.ok(result.errors.some(e => e.includes("project_rag_auto_index must be a boolean")));
|
||||
|
|
@ -706,6 +710,7 @@ test("codebase preferences parse from markdown frontmatter", () => {
|
|||
' - ".cache/"',
|
||||
" max_files: 800",
|
||||
" collapse_threshold: 10",
|
||||
" indexer_backend: projectRag",
|
||||
" project_rag: required",
|
||||
" project_rag_server: project",
|
||||
" project_rag_auto_index: true",
|
||||
|
|
@ -718,6 +723,7 @@ test("codebase preferences parse from markdown frontmatter", () => {
|
|||
assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/", ".cache/"]);
|
||||
assert.equal(result.preferences.codebase?.max_files, 800);
|
||||
assert.equal(result.preferences.codebase?.collapse_threshold, 10);
|
||||
assert.equal(result.preferences.codebase?.indexer_backend, "projectRag");
|
||||
assert.equal(result.preferences.codebase?.project_rag, "required");
|
||||
assert.equal(result.preferences.codebase?.project_rag_server, "project");
|
||||
assert.equal(result.preferences.codebase?.project_rag_auto_index, true);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue