Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@
> completion state and remaining P0 gates. No version bump or release claim is

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The user-facing release entry exposes internal parity/eval and runtime implementation details in the summary and itemized sections. Replacing these with observable indexing behavior would keep the changelog within its documented audience and content rules.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CHANGELOG.md, line 19:

<comment>The user-facing release entry exposes internal parity/eval and runtime implementation details in the summary and itemized sections. Replacing these with observable indexing behavior would keep the changelog within its documented audience and content rules.</comment>

<file context>
@@ -7,6 +7,43 @@
+
+### The numbers that matter
+
+Source: `bun run scripts/gstack2/run-parity.ts` and `bun test test/code-intelligence.test.ts` on this release.
+
+| Metric | Before | After |
</file context>
Fix with cubic

> made here while that status holds.

## [1.62.0.0] - 2026-07-23

## **Open a large repo and gstack offers to index it.**
## **Once, with real choices, then never again.**

Every gstack skill now checks the size of the repository it lands in before starting real work. Small repo, nothing happens, grep is already fast. Large repo (1,000+ tracked files) and you have never answered the question, the skill pauses once and asks whether you want code-intelligence indexing, with the actual trade-offs spelled out: GBrain gives semantic "where is X handled?" search but sends repo content to your GBrain database and asks consent per repo. Sourcebot gives fast whole-repo search, self-hosted, local when it runs on localhost. Graphify builds a local tree-sitter code graph and nothing ever leaves your machine, but you install it yourself. Or say no indexing, and gstack remembers that too. Decline once and no skill asks again, on any repo, until you change your mind with `gstack-code-intelligence select`.

### The numbers that matter

Source: `bun run scripts/gstack2/run-parity.ts` and `bun test test/code-intelligence.test.ts` on this release.

| Metric | Before | After |
|---|---|---|
| Skills that surface the indexing option | 0 | all 6 dispatchers |
| Times the question is asked per machine | n/a | at most 1 |
| Large-repo threshold | n/a | 1,000 tracked files |
| Parity checks pinning the behavior | 5,032 | 5,050 |
| code-intelligence tests | 25 | 31 |
Comment on lines +21 to +27

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The release metrics omit the required Δ column, so this entry does not follow the repository's changelog format. Adding the column also makes the +18 parity checks and +6 tests immediately visible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CHANGELOG.md, line 21:

<comment>The release metrics omit the required `Δ` column, so this entry does not follow the repository's changelog format. Adding the column also makes the +18 parity checks and +6 tests immediately visible.</comment>

<file context>
@@ -7,6 +7,43 @@
+
+Source: `bun run scripts/gstack2/run-parity.ts` and `bun test test/code-intelligence.test.ts` on this release.
+
+| Metric | Before | After |
+|---|---|---|
+| Skills that surface the indexing option | 0 | all 6 dispatchers |
</file context>
Suggested change
| Metric | Before | After |
|---|---|---|
| Skills that surface the indexing option | 0 | all 6 dispatchers |
| Times the question is asked per machine | n/a | at most 1 |
| Large-repo threshold | n/a | 1,000 tracked files |
| Parity checks pinning the behavior | 5,027 | 5,045 |
| code-intelligence tests | 25 | 31 |
| Metric | Before | After | Δ |
|---|---|---|---|
| Skills that surface the indexing option | 0 | all 6 dispatchers | +6 dispatchers |
| Times the question is asked per machine | n/a | at most 1 | new |
| Large-repo threshold | n/a | 1,000 tracked files | new |
| Parity checks pinning the behavior | 5,027 | 5,045 | +18 |
| code-intelligence tests | 25 | 31 | +6 |
Fix with cubic


The "at most 1" row is the one that matters. The offer gate (`gstack-code-intelligence suggest`) refuses to fire when a provider is already selected, when you declined before, when the repo is small, or when the directory is not a git repo. A missing helper is a silent skip, not an error.

### What this means for builders

On a 10-file toy, nothing changes. On your 5,000-file production monolith, the first `/plan` or `/review` run offers you semantic search over the whole codebase, you pick a provider (or none) in one question, and every later session benefits without ever being asked again. Grep and the file-only decision store remain the always-working default; indexing is an upgrade, never a dependency.

### Itemized changes

#### Added
- Session-start indexing offer: every dispatcher reads the new generated `references/CODE-INTELLIGENCE.md` once per repository invocation and runs `gstack-code-intelligence suggest` to decide whether to ask. The question presents GBrain (recommended, consent-gated), Sourcebot, Graphify (local, never auto-installed), and No indexing, each with its reason and live availability.
- `gstack-code-intelligence suggest [path] [--json]`: the offer gate as a command. Emits offer/no-offer with the reason (`provider-selected`, `declined`, `not-a-repo`, `small-repo`, `large-repo`) and, when offering, the provider options.
- `gstack-code-intelligence select none` now persists the decline so the question is never repeated.
- The `gstack-code-intelligence` helper ships in the managed runtime, so standard installs can run the offer from `$GSTACK_HOME/bin`.

#### For contributors
- New `lib/code-intelligence/suggest.ts` (`shouldOfferIndexing`, `trackedFileCount`, threshold injectable for tests) and a `declined` flag in the selection store; 6 new tests in `test/code-intelligence.test.ts`.
- Parity pins the per-tree contract (existence, dispatcher wiring, silent-degrade, decline, no-auto-install), 3 checks per dispatcher. Runtime helper surface: `DEFAULT_RUNTIME_HELPERS` + `RUNTIME_HELPER_DEPENDENCIES` in `runtime/install.js`.

## [1.61.2.0] - 2026-07-23

## **The founder-resources pitch now takes no for an answer.**
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.61.2.0
1.62.0.0
46 changes: 44 additions & 2 deletions bin/gstack-code-intelligence
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* fine and callers use grep / the file-only decision store.
*
* Usage:
* gstack-code-intelligence suggest [repo] [--json] # should the one-time indexing offer be made here?
* gstack-code-intelligence options # list providers (GBrain first) + availability
* gstack-code-intelligence status # current selection + availability
* gstack-code-intelligence select <gbrain|sourcebot|graphify|none>
Expand All @@ -30,6 +31,7 @@ import {
setConsent,
setProvider,
setRoot,
shouldOfferIndexing,
type CodeProviderId,
} from "../lib/code-intelligence";

Expand Down Expand Up @@ -69,10 +71,48 @@ async function cmdStatus(): Promise<void> {
for (const a of avail) out(` ${LABEL[a.id]}: ${a.available ? "available" : "unavailable"} (${a.detail})`);
}

/**
* The one-time session-start offer gate. Prints (or emits as JSON) whether an
* agent should ask the user about indexing this repo, and when it should, the
* provider options with their reasons so the question is self-contained.
*/
async function cmdSuggest(rest: string[]): Promise<void> {
const json = rest.includes("--json");
const pathArg = rest.find((a) => !a.startsWith("--"));
const repoPath = resolve(pathArg ?? process.cwd());
const suggestion = shouldOfferIndexing(repoPath);
if (!suggestion.offer) {
if (json) {
out(JSON.stringify({ ...suggestion, repoPath }));
} else {
out(`no offer (${suggestion.reason}${suggestion.fileCount != null ? `, ${suggestion.fileCount} tracked files` : ""})`);
}
return;
}
const avail = await detectAvailable();
if (json) {
out(JSON.stringify({
...suggestion,
repoPath,
options: avail.map((a) => ({
id: a.id,
label: LABEL[a.id],
reason: NOTE[a.id],
local: providerById(a.id).local,
available: a.available,
detail: a.detail,
})),
}));
return;
}
out(`offer indexing: ${suggestion.fileCount} tracked files (threshold ${suggestion.threshold}) and no prior decision`);
await cmdOptions();
}

function cmdSelect(arg: string | undefined): void {
if (arg === "none") {
setProvider(null);
out("code-intelligence provider cleared; gstack uses grep / file-only fallback");
out("code-intelligence declined; gstack uses grep / file-only fallback and will not ask again");
return;
}
if (!arg || !PROVIDER_IDS.has(arg as CodeProviderId)) {
Expand Down Expand Up @@ -144,6 +184,8 @@ function handleProviderError(err: unknown, label: string): never {
async function main(): Promise<void> {
const [action, ...rest] = process.argv.slice(2);
switch (action) {
case "suggest":
return cmdSuggest(rest);
case "options":
return cmdOptions();
case "status":
Expand All @@ -157,7 +199,7 @@ async function main(): Promise<void> {
case "search":
return cmdSearch(rest);
default:
fail("Usage: options | status | select <provider> | consent [path] | index [path] | search <query...>");
fail("Usage: suggest [path] [--json] | options | status | select <provider> | consent [path] | index [path] | search <query...>");
}
}

Expand Down
2 changes: 1 addition & 1 deletion docs/gstack-2/JUDGMENT-PARITY.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions evals/parity/transcripts/policy-units.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions lib/code-intelligence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export {
getRoot,
type Selection,
} from "./selection";
export {
LARGE_REPO_FILE_THRESHOLD,
shouldOfferIndexing,
trackedFileCount,
type Suggestion,
type SuggestReason,
} from "./suggest";
export {
RECOMMENDED_ORDER,
providerById,
Expand Down
9 changes: 7 additions & 2 deletions lib/code-intelligence/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ export interface Selection {
consents: Record<string, boolean>;
/** Provider id → the absolute repo path it last indexed (so search finds it). */
roots: Record<string, string>;
/** User explicitly chose no indexing — never offer again. */
declined: boolean;
}

const EMPTY: Selection = { provider: null, consents: {}, roots: {} };
const EMPTY: Selection = { provider: null, consents: {}, roots: {}, declined: false };

function storePath(env: NodeJS.ProcessEnv = process.env): string {
const home = env.GSTACK_HOME || join(env.HOME || homedir(), ".gstack");
Expand All @@ -38,6 +40,7 @@ export function readSelection(env: NodeJS.ProcessEnv = process.env): Selection {
provider: raw.provider ?? null,
consents: raw.consents && typeof raw.consents === "object" ? raw.consents : {},
roots: raw.roots && typeof raw.roots === "object" ? raw.roots : {},
declined: raw.declined === true,
};
} catch {
return { ...EMPTY };
Expand All @@ -53,7 +56,9 @@ function write(selection: Selection, env: NodeJS.ProcessEnv = process.env): void
}

export function setProvider(provider: CodeProviderId | null, env: NodeJS.ProcessEnv = process.env): Selection {
const next = { ...readSelection(env), provider };
// Choosing a provider clears a prior decline; clearing to null records one,
// so the session-start offer is never repeated after an explicit "none".
const next = { ...readSelection(env), provider, declined: provider === null };
write(next, env);
return next;
}
Expand Down
55 changes: 55 additions & 0 deletions lib/code-intelligence/suggest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* suggest — should the session-start indexing offer be made for this repo?
*
* 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
* (`git ls-files` count); a non-repo directory never triggers the offer.
*/

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
/** Tracked-file count at which indexing starts paying for itself. */
export const LARGE_REPO_FILE_THRESHOLD = 1000;

export type SuggestReason =
| "provider-selected"
| "declined"
| "not-a-repo"
| "small-repo"
| "large-repo";

export interface Suggestion {
offer: boolean;
reason: SuggestReason;
fileCount: number | null;
threshold: number;
}

/** Count of git-tracked files, or null when the path is not a git repo. */
export function trackedFileCount(repoPath: string): number | null {
const result = spawnSync("git", ["-C", resolve(repoPath), "ls-files"], {

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Invoking a skill from a nested directory can suppress the indexing offer because git -C <subdir> ls-files counts only files below that directory, not the whole repository. Use a top-level pathspec such as -- :/ or resolve the Git root before counting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/code-intelligence/suggest.ts, line 34:

<comment>Invoking a skill from a nested directory can suppress the indexing offer because `git -C <subdir> ls-files` counts only files below that directory, not the whole repository. Use a top-level pathspec such as `-- :/` or resolve the Git root before counting.</comment>

<file context>
@@ -0,0 +1,55 @@
+
+/** Count of git-tracked files, or null when the path is not a git repo. */
+export function trackedFileCount(repoPath: string): number | null {
+  const result = spawnSync("git", ["-C", resolve(repoPath), "ls-files"], {
+    encoding: "utf-8",
+    maxBuffer: 64 * 1024 * 1024,
</file context>
Fix with cubic

encoding: "utf-8",
maxBuffer: 64 * 1024 * 1024,

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Repositories whose tracked-path output exceeds 64 MiB are silently classified as not-a-repo, so very large repos can miss the offer. Count a compact constant-size record per file or stream the output instead of buffering all path names under a fixed cap.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/code-intelligence/suggest.ts, line 36:

<comment>Repositories whose tracked-path output exceeds 64 MiB are silently classified as `not-a-repo`, so very large repos can miss the offer. Count a compact constant-size record per file or stream the output instead of buffering all path names under a fixed cap.</comment>

<file context>
@@ -0,0 +1,55 @@
+export function trackedFileCount(repoPath: string): number | null {
+  const result = spawnSync("git", ["-C", resolve(repoPath), "ls-files"], {
+    encoding: "utf-8",
+    maxBuffer: 64 * 1024 * 1024,
+  });
+  if (result.status !== 0 || typeof result.stdout !== "string") return null;
</file context>
Fix with cubic

});
if (result.status !== 0 || typeof result.stdout !== "string") return null;
const out = result.stdout.trim();
return out ? out.split("\n").length : 0;
}

export function shouldOfferIndexing(
repoPath: string,
opts: { env?: NodeJS.ProcessEnv; threshold?: number } = {},
): Suggestion {
const threshold = opts.threshold ?? LARGE_REPO_FILE_THRESHOLD;
const selection = readSelection(opts.env);
if (selection.provider) return { offer: false, reason: "provider-selected", fileCount: null, threshold };
if (selection.declined) return { offer: false, reason: "declined", fileCount: null, threshold };
const fileCount = trackedFileCount(repoPath);
if (fileCount === null) return { offer: false, reason: "not-a-repo", fileCount, threshold };
if (fileCount < threshold) return { offer: false, reason: "small-repo", fileCount, threshold };
return { offer: true, reason: "large-repo", fileCount, threshold };
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.61.2.0",
"version": "1.62.0.0",
"description": "GStack 2 — six portable Agent Skills with an optional host-neutral runtime.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 2 additions & 0 deletions runtime/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const DEFAULT_RUNTIME_HELPERS = Object.freeze({
"gstack-brain-cache": helper("bin/gstack-brain-cache"),
"gstack-brain-sync": helper("bin/gstack-brain-sync"),
"gstack-builder-profile": helper("bin/gstack-builder-profile"),
"gstack-code-intelligence": helper("bin/gstack-code-intelligence"),

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new stable helper is installed but remains absent from the audited runtime-helper closure, so parity cannot verify its source path or consumers. Consider extending runtimeHelperClosure to include generated CODE-INTELLIGENCE.md consumers and adding a reverse assertion that every DEFAULT_RUNTIME_HELPERS entry is declared.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/install.js, line 101:

<comment>The new stable helper is installed but remains absent from the audited runtime-helper closure, so parity cannot verify its source path or consumers. Consider extending `runtimeHelperClosure` to include generated `CODE-INTELLIGENCE.md` consumers and adding a reverse assertion that every `DEFAULT_RUNTIME_HELPERS` entry is declared.</comment>

<file context>
@@ -98,6 +98,7 @@ export const DEFAULT_RUNTIME_HELPERS = Object.freeze({
   "gstack-brain-cache": helper("bin/gstack-brain-cache"),
   "gstack-brain-sync": helper("bin/gstack-brain-sync"),
   "gstack-builder-profile": helper("bin/gstack-builder-profile"),
+  "gstack-code-intelligence": helper("bin/gstack-code-intelligence"),
   "gstack-codex-probe": helper("bin/gstack-codex-probe"),
   "gstack-config": helper("bin/gstack-config"),
</file context>
Fix with cubic

"gstack-codex-probe": helper("bin/gstack-codex-probe"),
"gstack-config": helper("bin/gstack-config"),
"gstack-decision-log": helper("bin/gstack-decision-log"),
Expand Down Expand Up @@ -156,6 +157,7 @@ const RUNTIME_HELPER_DEPENDENCIES = Object.freeze([
"VERSION",
"package.json",
"lib/bin-context.ts",
"lib/code-intelligence",
"lib/conductor-env-shim.ts",
"lib/gbrain-exec.ts",
"lib/gbrain-guards.ts",
Expand Down
Loading
Loading