fix(code-intelligence): consent that means what it says — polarity, receipts, read-only veto

Four review findings on the wave's own Phase 1 port, all red-first:
'consent <repo> no' recorded consent GRANTED (the CLI ignored the
argument and always wrote true) — yes|no is now required and garbage
records nothing; Sourcebot egress receipts claimed consented=true on
paths that never checked consent — the actual consent state is threaded
into every receipt, search is fail-closed on non-loopback, and the
liveness probe's receipt says truthfully that it sends no repo content;
repoPolicyVeto only honored the deny tier while gbrain refresh writes
pages — write-class ops now veto on read-only too, matching the sync
chokepoint, via one shared lib/gbrain-repo-policy-client.ts (win32
bash invocation, spawn-vs-unreadable error distinction) used by both
call sites. Also: source ids get a host+path hash (same-name repos no
longer collide), refresh timeout raised to 120s, availability probes
run concurrently at 3s, graphify status stops JSON.parsing 100MB graphs
for a count, and every ported file carries the fork MIT notice.
+15 tests across the two suites.
This commit is contained in:
Garry Tan
2026-08-14 17:13:22 -07:00
parent 531d9a6e1f
commit 9488173b0a
13 changed files with 640 additions and 100 deletions
+18 -4
View File
@@ -1,6 +1,8 @@
/**
* code-intelligence/contract — the OPTIONAL, repo-oriented provider contract.
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* gstack does not maintain a home-grown indexer. It defines this small contract
* and external providers (GBrain, Sourcebot, Graphify) implement it. The whole
* contract is OPTIONAL: when no provider is available/consented,
@@ -17,6 +19,16 @@
export type CodeProviderId = "gbrain" | "sourcebot" | "graphify";
/**
* Policy op classification for the per-remote trust-tier veto (selection.ts).
* Write-class ops (register_source / index / refresh / add / delete) cause
* pages to be written, so BOTH `deny` and `read-only` tiers veto them — the
* same semantics as runCodeImport in bin/gstack-gbrain-sync.ts ("code ingest
* writes pages"). Read-class ops (search / export / status) write nothing, so
* only `deny` vetoes them. Callers that don't say get "write" — fail-closed.
*/
export type OpClass = "read" | "write";
export type CodeProviderCapability =
| "register_source"
| "refresh"
@@ -72,16 +84,18 @@ export interface CodeSearchHit {
export interface OpOptions {
/**
* Env override for spawned processes. Production callers leave this unset;
* tests inject a synthetic env (fake CLI on PATH). Matches the existing
* gbrain helpers.
* Env override for spawned processes and egress-receipt home resolution.
* Production callers leave this unset; tests inject a synthetic env (fake
* CLI on PATH, temp GSTACK_HOME). Matches the existing gbrain helpers.
*/
env?: NodeJS.ProcessEnv;
/** Timeout in ms for the underlying op. */
timeout?: number;
/**
* Explicit per-repo consent that repo content may leave the machine. Required
* for non-local providers on register_source / refresh / add.
* for non-local providers on register_source / refresh / add / search (the
* search query text is repo-derived content). The recorded value also feeds
* the egress receipt, which attests the ACTUAL consent state — never assumed.
*/
consented?: boolean;
}
+25 -8
View File
@@ -1,6 +1,8 @@
/**
* GBrain adapter — full contract fit over the existing gbrain CLI chokepoint.
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* Reuses lib/gbrain-exec.ts (spawnGbrain, seeded DATABASE_URL) and
* lib/gbrain-sources.ts (ensureSourceRegistered, probeSource, sourcePageCount)
* rather than re-issuing raw commands, so the DATABASE_URL / GBRAIN_HOME /
@@ -39,6 +41,23 @@ const CAPABILITIES: CodeProviderCapability[] = [
];
const DEFAULT_TIMEOUT_MS = 30_000;
/**
* refresh() default. Full code indexing on the 1000+-tracked-file repos this
* feature targets routinely outruns the 30s op default; GraphifyProvider uses
* the same 120s ceiling for the same indexing work. Query/status stay at 30s.
*/
const REFRESH_TIMEOUT_MS = 120_000;
/**
* Environmental (engine / DB / config) failure shapes, shared by #assertOk and
* #wrap so the two paths can never drift. These degrade to
* PROVIDER_UNAVAILABLE (caller falls back to grep / file-only), not a hard
* PROVIDER_ERROR with a raw dump. Covers the real case where gbrain's pglite
* engine fails to init its WASM runtime (garrytan/gbrain#223) as well as
* unreachable/unconfigured databases and a missing CLI.
*/
const ENVIRONMENTAL_ERROR_RE =
/not on PATH|command not found|PGLite|WASM|failed to initialize|Aborted|Cannot connect to database|not configured|config\.json|database (is )?un(reachable|available)/i;
/**
* Parse `gbrain search` text output (`[score] slug -- snippet`) into hits.
@@ -110,7 +129,7 @@ export class GbrainProvider implements CodeProvider {
async refresh(source: SourceRef, opts: OpOptions = {}): Promise<SourceStatus> {
assertEgressConsent(this, opts);
this.#receipt("repo-code-index (sent by gbrain subprocess)", opts);
const timeout = opts.timeout ?? DEFAULT_TIMEOUT_MS;
const timeout = opts.timeout ?? REFRESH_TIMEOUT_MS;
// Two passes, verified end-to-end against real Postgres-backed gbrain 0.42.56:
// 1. default sync (markdown strategy) — indexes docs.
// 2. `sync --strategy code` — the ACTUAL code-indexing pass. Without it code
@@ -219,10 +238,8 @@ export class GbrainProvider implements CodeProvider {
throw new CodeProviderError("PROVIDER_TIMEOUT", "gbrain timed out", this.id);
}
// Engine / DB / config problems are ENVIRONMENTAL — degrade to UNAVAILABLE
// (caller falls back to file-only), not a hard PROVIDER_ERROR with a raw dump.
// Covers the real case where gbrain's pglite engine fails to init its WASM
// runtime (garrytan/gbrain#223) as well as unreachable/unconfigured databases.
if (/PGLite|WASM|failed to initialize|Aborted|Cannot connect to database|not configured|config\.json|database (is )?un(reachable|available)/i.test(stderr)) {
// (caller falls back to file-only). Shapes hoisted to ENVIRONMENTAL_ERROR_RE.
if (ENVIRONMENTAL_ERROR_RE.test(stderr)) {
throw new CodeProviderError("PROVIDER_UNAVAILABLE", firstLine(stderr) || "gbrain engine unavailable", this.id);
}
throw new CodeProviderError("PROVIDER_ERROR", firstLine(stderr) || `gbrain exited ${r.status}`, this.id);
@@ -231,9 +248,9 @@ export class GbrainProvider implements CodeProvider {
#wrap(err: unknown): CodeProviderError {
if (err instanceof CodeProviderError) return err;
const message = err instanceof Error ? err.message : String(err);
// Same environmental-vs-real split as #assertOk: missing CLI, or engine/DB/
// config problems, degrade to UNAVAILABLE so callers fall back to file-only.
if (/not on PATH|command not found|PGLite|WASM|failed to initialize|Aborted|Cannot connect to database|not configured|config\.json/i.test(message)) {
// Same environmental-vs-real split as #assertOk — literally the same
// regex (ENVIRONMENTAL_ERROR_RE), so the two paths can never drift.
if (ENVIRONMENTAL_ERROR_RE.test(message)) {
return new CodeProviderError("PROVIDER_UNAVAILABLE", firstLine(message), this.id);
}
return new CodeProviderError("PROVIDER_ERROR", firstLine(message), this.id);
+22 -5
View File
@@ -1,6 +1,8 @@
/**
* Graphify adapter — real CLI integration (github.com/Graphify-Labs/graphify).
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* Graphify is a LOCAL tree-sitter knowledge graph. For CODE, `graphify <dir>`
* and `graphify update <dir>` produce the SAME AST graph with NO LLM and NO
* network (verified against graphify 0.9.23 — both emit `AST extraction on N
@@ -26,7 +28,7 @@
*/
import { spawnSync } from "child_process";
import { existsSync, readFileSync } from "fs";
import { existsSync, readFileSync, statSync } from "fs";
import { join } from "path";
import {
assertCapability,
@@ -47,6 +49,13 @@ const OUT_DIR = "graphify-out";
const GRAPH_JSON = "graph.json";
const DEFAULT_TIMEOUT_MS = 120_000; // indexing a repo can take a while
const NEEDS_SHELL_ON_WINDOWS = process.platform === "win32"; // graphify is a shim on Windows
/**
* status() only parses graph.json for a node count when the file is at most
* this big. On the 1000+-file repos this feature targets, graph.json can run
* to hundreds of MB — JSON.parsing that for a cosmetic count is a heap spike.
* Above the threshold the display reports the file size instead.
*/
const STATUS_PARSE_MAX_BYTES = 5 * 1024 * 1024;
export interface GraphifyOptions {
/** Directory whose `graphify-out/` search/status/export read. Defaults to cwd. */
@@ -127,13 +136,21 @@ export class GraphifyProvider implements CodeProvider {
const graphPath = join(dir, OUT_DIR, GRAPH_JSON);
if (!existsSync(graphPath)) return { id: dir, state: "absent" };
let itemCount: number | undefined;
let detail = graphPath;
try {
const graph = JSON.parse(readFileSync(graphPath, "utf-8")) as { nodes?: unknown[] };
if (Array.isArray(graph.nodes)) itemCount = graph.nodes.length;
// stat first: parse the whole graph only when it's small (the node count
// is display-only, never worth a hundreds-of-MB JSON.parse heap spike).
const size = statSync(graphPath).size;
if (size <= STATUS_PARSE_MAX_BYTES) {
const graph = JSON.parse(readFileSync(graphPath, "utf-8")) as { nodes?: unknown[] };
if (Array.isArray(graph.nodes)) itemCount = graph.nodes.length;
} else {
detail = `${graphPath} (${(size / (1024 * 1024)).toFixed(1)} MB graph; node count skipped)`;
}
} catch {
// graph.json present but unparseable — still ready, just no count.
// graph.json present but unstatable/unparseable — still ready, just no count.
}
return { id: dir, state: "ready", itemCount, detail: graphPath };
return { id: dir, state: "ready", itemCount, detail };
}
async export(source: SourceRef, _opts: OpOptions = {}): Promise<string> {
+3
View File
@@ -1,5 +1,8 @@
/**
* code-intelligence — the OPTIONAL, repo-oriented provider contract.
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* See docs/designs/CODE_INTELLIGENCE_PROVIDER_CONTRACT.md.
*/
+42 -32
View File
@@ -2,6 +2,8 @@
* Picker — constructs the code-intelligence provider the user selected, and
* offers the recommendation order (GBrain first) for the selection UX.
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* `resolveSelectedProvider()` reads the persisted selection and constructs that
* provider, or returns null when nothing is selected — the provider-OFF path,
* where callers degrade to grep / the file-only decision store. Availability is
@@ -60,41 +62,49 @@ export interface Availability {
detail: string;
}
/**
* Availability probes are DISPLAY probes — they must never stall the CLI. A
* dead non-loopback SOURCEBOT_URL at the adapters' 30s op default meant a 30s
* hang just to print the options table; 3s is plenty for a liveness check.
*/
const PROBE_TIMEOUT_MS = 3_000;
/**
* Probe which providers are usable right now, in recommendation order. Used by
* the `options`/`status` display. GBrain via the real localEngineStatus();
* Graphify via its CLI status; Sourcebot via an HTTP liveness probe.
* Graphify via its CLI status; Sourcebot via an HTTP liveness probe. The three
* probes are independent, so they run concurrently, each capped at
* PROBE_TIMEOUT_MS (localEngineStatus owns its own probe timeout + cache).
*/
export async function detectAvailable(opts: PickerOptions = {}): Promise<Availability[]> {
const gbrainStatus = localEngineStatus({ env: opts.env });
const gbrainOk = gbrainStatus === "ok" || gbrainStatus === "timeout";
// Available = the CLI is installed and selectable (NOT "a graph already exists
// here"). A freshly installed Graphify with no graph yet is still available.
const graphifyOk = graphifyInstalled(opts.env);
let graphifyDetail = "graphify CLI not installed (pip install graphifyy, Python >= 3.10)";
if (graphifyOk) {
try {
const s = await new GraphifyProvider({ env: opts.env, ...opts.graphify }).status();
graphifyDetail = s.state === "ready" ? "installed; graph built in this repo" : "installed; run `index` to build a graph";
} catch {
graphifyDetail = "installed";
}
}
let sourcebotOk = false;
let sourcebotDetail = "server unreachable";
try {
const s = await new SourcebotProvider({ env: opts.env, ...opts.sourcebot }).status();
sourcebotOk = s.state === "ready";
sourcebotDetail = s.detail ?? "";
} catch {
sourcebotOk = false;
}
return [
{ id: "gbrain", available: gbrainOk, detail: `gbrain engine: ${gbrainStatus}` },
{ id: "sourcebot", available: sourcebotOk, detail: sourcebotDetail },
{ id: "graphify", available: graphifyOk, detail: graphifyDetail },
];
const [gbrain, sourcebot, graphify] = await Promise.all([
(async (): Promise<Availability> => {
const status = localEngineStatus({ env: opts.env });
return { id: "gbrain", available: status === "ok" || status === "timeout", detail: `gbrain engine: ${status}` };
})(),
(async (): Promise<Availability> => {
try {
const s = await new SourcebotProvider({ env: opts.env, ...opts.sourcebot }).status(undefined, { timeout: PROBE_TIMEOUT_MS });
return { id: "sourcebot", available: s.state === "ready", detail: s.detail ?? "" };
} catch {
return { id: "sourcebot", available: false, detail: "server unreachable" };
}
})(),
(async (): Promise<Availability> => {
// Available = the CLI is installed and selectable (NOT "a graph already exists
// here"). A freshly installed Graphify with no graph yet is still available.
const installed = graphifyInstalled(opts.env);
let detail = "graphify CLI not installed (pip install graphifyy, Python >= 3.10)";
if (installed) {
try {
const s = await new GraphifyProvider({ env: opts.env, ...opts.graphify }).status(undefined, { timeout: PROBE_TIMEOUT_MS });
detail = s.state === "ready" ? "installed; graph built in this repo" : "installed; run `index` to build a graph";
} catch {
detail = "installed";
}
}
return { id: "graphify", available: installed, detail };
})(),
]);
return [gbrain, sourcebot, graphify];
}
+30 -17
View File
@@ -3,6 +3,8 @@
* per-repo indexing consent. Stored at `$GSTACK_HOME/code-intelligence.json`
* (default `~/.gstack/`), the same home the rest of gstack uses.
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* Consent is per-repo (keyed by absolute repo path), because indexing consent
* is "may THIS repo's content be indexed by the selected provider" — a decision
* a user makes per project, not once for the machine. No selection at all is the
@@ -12,8 +14,9 @@
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
import { homedir } from "os";
import { dirname, join, resolve } from "path";
import { execFileSync, spawnSync } from "child_process";
import type { CodeProviderId } from "./contract";
import { execFileSync } from "child_process";
import { hasRepoPolicyStore, repoPolicyTier } from "../gbrain-repo-policy-client";
import type { CodeProviderId, OpClass } from "./contract";
export interface Selection {
provider: CodeProviderId | null;
@@ -76,14 +79,20 @@ export function setConsent(repoPath: string, consented: boolean, env: NodeJS.Pro
* The per-remote trust store (gstack-gbrain-repo-policy) is the SINGLE
* authority for consent-to-send: a `deny` tier vetoes any recorded
* code-intelligence consent, so two stores can never disagree about whether
* code may leave this repo (R1, fork port wave 2 review). Mirrors the
* gbrain-sync chokepoint's polarity: no policy store → no veto (nothing was
* ever set); unreadable store → veto (fail-closed — a policy the user set
* must not be bypassed by a broken store).
* code may leave this repo (R1, fork port wave 2 review). The veto is
* op-class-aware (R2): `read-only` means "search allowed, page writes never"
* (the exact semantics runCodeImport in bin/gstack-gbrain-sync.ts enforces —
* code ingest writes pages), so it vetoes write-class ops (register / index /
* refresh / add / delete) while read-class ops (search / export / status)
* pass; `deny` vetoes both classes. Mirrors the gbrain-sync chokepoint's
* polarity: no policy store → no veto (nothing was ever set); unreadable
* store OR unspawnable policy helper → veto for every op class (fail-closed —
* a policy the user set must not be bypassed by a broken store or a helper
* that can't run). Reads through the shared lib/gbrain-repo-policy-client.ts
* so this site and the gbrain-sync gate can never drift.
*/
function repoPolicyVeto(repoPath: string, env: NodeJS.ProcessEnv = process.env): boolean {
const home = env.GSTACK_HOME || join(homedir(), ".gstack");
if (!existsSync(join(home, "gbrain-repo-policy.json"))) return false;
function repoPolicyVeto(repoPath: string, opClass: OpClass, env: NodeJS.ProcessEnv = process.env): boolean {
if (!hasRepoPolicyStore(env)) return false; // fast path: nothing was ever set — skip the git spawn too
let url = "";
try {
url = execFileSync("git", ["-C", resolve(repoPath), "remote", "get-url", "origin"], {
@@ -93,17 +102,21 @@ function repoPolicyVeto(repoPath: string, env: NodeJS.ProcessEnv = process.env):
return false; // no remote → policy (keyed by remote) has nothing set for this repo
}
if (!url) return false;
const res = spawnSync(join(import.meta.dir, "..", "..", "bin", "gstack-gbrain-repo-policy"), ["get", url], {
encoding: "utf-8", timeout: 10_000, env: { ...env } as NodeJS.ProcessEnv,
});
if (res.error || res.status !== 0) return true; // fail-closed
const tier = (res.stdout || "").trim();
return tier === "deny";
const res = repoPolicyTier(url, env);
if (res.error) return true; // fail-closed (unreadable store or spawn failure alike)
if (res.tier === "deny") return true; // deny beats consent for every op class
return res.tier === "read-only" && opClass === "write"; // read-only: writes never, reads pass
}
export function hasConsent(repoPath: string, env: NodeJS.ProcessEnv = process.env): boolean {
/**
* Recorded per-repo consent, filtered through the repo-policy veto. `opClass`
* defaults to "write" so a caller that doesn't classify its op gets the
* fail-closed answer; pass "read" only for ops that write no pages (search /
* export / status).
*/
export function hasConsent(repoPath: string, env: NodeJS.ProcessEnv = process.env, opClass: OpClass = "write"): boolean {
if (readSelection(env).consents[resolve(repoPath)] !== true) return false;
return !repoPolicyVeto(repoPath, env);
return !repoPolicyVeto(repoPath, opClass, env);
}
/** Record the repo path a provider last indexed, so search reads the same graph. */
+41 -7
View File
@@ -2,6 +2,8 @@
* Sourcebot adapter — real HTTP + config integration
* (github.com/sourcebot-dev/sourcebot, YC F2025).
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* Sourcebot is a self-hosted server that indexes repos declared in its
* config.json and serves regex code search over `POST /api/search` (zoekt). So
* the runtime drives it with plain HTTP + a config-file edit — no MCP:
@@ -128,32 +130,46 @@ export class SourcebotProvider implements CodeProvider {
return { id: repo.id, state: "registered", detail: "Sourcebot re-indexes on config change" };
}
/** Sourcebot re-indexes automatically; report current liveness. */
/**
* Sourcebot re-indexes automatically; report current liveness. Refresh is a
* write-class op (it represents indexing repo content into the server), so a
* non-loopback server requires per-repo consent even though the HTTP call
* below is only the liveness probe.
*/
async refresh(source: SourceRef, opts: OpOptions = {}): Promise<SourceStatus> {
assertEgressConsent(this, opts); // no-op when the server is loopback (local)
const live = await this.status(source, opts);
return { ...live, detail: "Sourcebot re-indexes automatically (config change + reindexIntervalMs)" };
}
async search(query: string, opts: SearchOptions = {}): Promise<CodeSearchHit[]> {
if (!query.trim()) return [];
// Query text is repo-derived content (sensitive class): a non-loopback
// server needs per-repo consent BEFORE the query is sent. Fail-closed —
// the throw happens before any bytes (or any receipt) exist.
assertEgressConsent(this, opts); // no-op when the server is loopback (local)
const body = {
query: opts.source ? `repo:${opts.source} ${query}` : query,
matches: opts.limit ?? 20,
isRegexEnabled: true,
isCaseSensitivityEnabled: false,
};
const payload = await this.#post("/api/search", body, opts.timeout ?? DEFAULT_TIMEOUT_MS);
const payload = await this.#post("/api/search", body, opts);
return parseSourcebotSearch(payload, opts.limit ?? 20);
}
async status(_source?: SourceRef, opts: OpOptions = {}): Promise<SourceStatus> {
// `redirect: manual` so an auth-gated server (307 -> /login) reads as
// not-usable instead of following to a 200 and falsely reporting "ready".
// The probe body is a FIXED literal (query "sourcebot") — no repo-derived
// content — so it is allowed without consent; the receipt records that
// consent was unchecked instead of pretending it was verified.
try {
const res = await this.#fetchWithTimeout(
`${this.#baseUrl}/api/search`,
{ method: "POST", headers: { "Content-Type": "application/json", ...this.#authHeaders() }, body: JSON.stringify({ query: "sourcebot", matches: 1, isRegexEnabled: false }), redirect: "manual" },
opts.timeout ?? DEFAULT_TIMEOUT_MS,
{ payloadClass: "liveness-probe (fixed query, no repo-derived content)", consented: opts.consented === true, env: opts.env },
);
if (res.status === 401 || res.status === 403) {
return { id: "*", state: "unknown", partial: true, detail: "reachable but login-gated (enable anonymous access with FORCE_ENABLE_ANONYMOUS_ACCESS=true for local use, or set SOURCEBOT_API_KEY)" };
@@ -164,15 +180,20 @@ export class SourcebotProvider implements CodeProvider {
}
}
async #post(path: string, body: unknown, timeout: number): Promise<unknown> {
async #post(path: string, body: unknown, opts: OpOptions): Promise<unknown> {
let res: Response;
try {
res = await this.#fetchWithTimeout(`${this.#baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json", ...this.#authHeaders() },
body: JSON.stringify(body),
}, timeout);
}, opts.timeout ?? DEFAULT_TIMEOUT_MS, {
payloadClass: "code-search-request",
consented: opts.consented === true,
env: opts.env,
});
} catch (err) {
if (err instanceof CodeProviderError) throw err;
throw new CodeProviderError("PROVIDER_UNAVAILABLE", `Sourcebot unreachable at ${this.#baseUrl}: ${(err as Error).message}`, this.id);
}
if (res.status === 401 || res.status === 403) {
@@ -186,19 +207,32 @@ export class SourcebotProvider implements CodeProvider {
}
}
async #fetchWithTimeout(url: string, init: RequestInit, timeout: number): Promise<Response> {
async #fetchWithTimeout(
url: string,
init: RequestInit,
timeout: number,
receipt: { payloadClass: string; consented: boolean; env?: NodeJS.ProcessEnv },
): Promise<Response> {
// Every Sourcebot HTTP call routes through here. A loopback server keeps
// content on this machine (no egress, no receipt); a non-loopback server
// is an off-machine send and gets a fail-closed receipt BEFORE the fetch.
// The receipt's consent field records the ACTUAL consent state passed by
// the op (opts.consented) — the tamper-evident ledger must never attest
// "consented=true" for a send where nothing checked consent. Content-
// bearing ops (search/refresh) assert consent before reaching here; the
// status liveness probe is allowed unconsented and its receipt says so.
if (!this.local) {
const body = typeof init.body === "string" ? init.body : "";
writeReceipt({
env: receipt.env,
sink: "sourcebot",
host: new URL(url).host,
payloadClass: "code-search-request",
payloadClass: receipt.payloadClass,
bytes: Buffer.byteLength(body),
sha256: sha256Hex(body),
consent: "code-intelligence provider=sourcebot (non-loopback SOURCEBOT_URL) + per-repo consented=true",
consent: receipt.consented
? "code-intelligence provider=sourcebot (non-loopback SOURCEBOT_URL) + per-repo consented=true"
: "code-intelligence provider=sourcebot (non-loopback SOURCEBOT_URL) + consent=unchecked (liveness probe only; content-bearing ops assert consent before sending)",
});
}
const controller = new AbortController();
+3 -1
View File
@@ -1,6 +1,8 @@
/**
* suggest — should the session-start indexing offer be made for this repo?
*
* Portions copyright (c) 2026 Sina Matian, time-attack/gstack (GStack 2), MIT.
*
* The offer fires at most once per machine: never when a provider is already
* selected, never after an explicit decline (`select none`), and never for
* small repos where grep is already fast. Detection is cheap and local
@@ -11,7 +13,7 @@ import { spawnSync } from "child_process";
import { resolve } from "path";
import { readSelection } from "./selection";
// ponytail: single tracked-file-count knob for "large"; add a LOC signal if it misfires
// Single tracked-file-count knob for "large"; add a LOC signal if it misfires.
/** Tracked-file count at which indexing starts paying for itself. */
export const LARGE_REPO_FILE_THRESHOLD = 1000;