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
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ inputs:
image-tag:
description: 'Scanner image tag to run (defaults to a pinned, tested release).'
required: false
default: '2026.829.1' # vibgrate:cli-version — stamped by scripts/stamp-release-pins.mjs
default: '2026.903.1' # vibgrate:cli-version — stamped by scripts/stamp-release-pins.mjs
verify:
description: 'Verify the image cosign signature + provenance before running (requires cosign on the runner).'
required: false
Expand Down
2 changes: 1 addition & 1 deletion charts/vibgrate/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type: application
# stamped to the released @vibgrate/cli calendar version by
# scripts/stamp-release-pins.mjs (via the marker on the appVersion line below).
version: 0.1.1
appVersion: "2026.829.1" # vibgrate:cli-version — stamped by scripts/stamp-release-pins.mjs
appVersion: "2026.903.1" # vibgrate:cli-version — stamped by scripts/stamp-release-pins.mjs
home: https://vibgrate.com
icon: https://vibgrate.com/web-app-manifest-512x512.png
sources:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vibgrate/cli",
"version": "2026.829.1",
"version": "2026.903.1",
"description": "vg — local codebase intelligence CLI + MCP server for AI coding agents: deterministic code graph, drift reporting, and version-correct library docs (Apache-2.0)",
"//mcpName": "Official MCP registry ownership proof: the registry fetches the published npm package and requires this field to match the com.vibgrate/ai-context server entry (see docs/marketing/mcp-registry/README.md). Must ship in the published @vibgrate/cli package.json.",
"mcpName": "com.vibgrate/ai-context",
Expand Down
50 changes: 50 additions & 0 deletions releases/v2026.903.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Vibgrate CLI 2026.903.1

_Released 2026-09-03_

This release of the Vibgrate CLI introduces several new features and improvements focused on architecture classification, module management, and callable analysis. Key updates include enhanced callable descriptions, a new duty list reconstruction in `vg build`, and improved module update checks.

## What changed

### New

- An extract-count gate compares fixture gold against `vg build` callable counts.
- Architecture classification installs by default with local modules, with warnings printed on failures.
- `vg update` now checks optional local modules against the registry and updates them if newer versions are available.
- The language server now carries architecture module classifications to editors, enhancing context for callables.
- `vg build` reconstructs an ordered duty list for every callable from its syntax tree.
- `vg build` records what each callable's body calls, providing an effects profile for better analysis.
- When the architecture module classifies the map, various tools now prioritize application shape symbols.

### Changed

- Architecture classification now explains what a callable does using its domain-specific language.

## Benchmarks

Two-arm benchmark of this release against 2026.829.1, interleaved on one runner against the pinned corpus (187 metrics compared).

| Metric | Previous | This release |
| --- | --- | --- |
| Languages with extraction | 19 count | 19 count |
| Definitions extracted (corpus total) | 22334 count | 22334 count |
| Call edges extracted (corpus total) | 12367 count | 12367 count |
| Locate accuracy (top-1) | 0.95 ratio | 0.95 ratio |
| Dependency detection (authored manifest truth) | 0.96 ratio | 0.96 ratio |
| CLI startup (--version, median) | 570.20 ms | 567.30 ms |

3 regression(s) — published, not omitted:
- Token reduction vs baseline agent (equal success): 0.20 → 0.17 (-15.2%)
- Tasks passed on both arms: 36 → 35 (-2.8%)
- Comparable-task rate (both arms passed / total): 0.95 → 0.92 (-2.8%)

Full report and methodology: https://vibgrate.com/cli/benchmarks

## Install or update

```sh
npm install -g @vibgrate/cli
vg
```

Full changelog: https://vibgrate.com/changelog/cli/2026.903.1
9 changes: 8 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { updateCommand } from './reporting/commands/update.js';
import { sbomCommand } from './reporting/commands/sbom.js';
import { evidenceCommand } from './reporting/commands/evidence/index.js';
import { kickRelevanceReadiness } from './install/relevance-module.js';
import { kickHaileReadiness } from './install/haile-module.js';

/** The set of registered subcommand names (kept in sync with registration). */
export const KNOWN_COMMANDS = new Set([
Expand Down Expand Up @@ -340,7 +341,13 @@ export async function main(argv = process.argv): Promise<void> {
// VIBGRATE_NO_KERNEL / a recorded decline / --local skip it entirely.
// Both flags suppress it: `--offline` says "no network", and `--local`
// implies that. Read from raw argv because this runs before commander parses.
if (!raw.includes('--local') && !raw.includes('--offline')) kickRelevanceReadiness();
if (!raw.includes('--local') && !raw.includes('--offline')) {
kickRelevanceReadiness();
// Same posture for the architecture-classify module: installed by default,
// silently, in the background. `vg build` runs the bounded ensure and owns
// the user-visible warning when the module cannot be provisioned.
kickHaileReadiness();
}

// We need cwd for path-based dispatch; read -C/--cwd from the raw args.
const cwd = readCwd(raw);
Expand Down
3 changes: 3 additions & 0 deletions src/code/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

import { buildCodeContext } from './context.js';
import { loadRoleMap } from '../engine/haile/role-preference.js';
import { rankQuestion, type SanitizedRank } from '../engine/relevance-provider.js';
import { loadTopicTags } from '../engine/relevance-enrich.js';
import {
Expand Down Expand Up @@ -751,6 +752,7 @@ export async function runAgent(options: AgentOptions): Promise<AgentResult> {
budget,
files: options.files,
ranked: nextRanked,
roles: loadRoleMap(root, session.graph.provenance?.corpusHash),
readFile: (rel) => fsImpl.read(rel),
repositoryId: repositoryIdFromRoot(root),
provenance: {
Expand Down Expand Up @@ -1422,6 +1424,7 @@ function buildAgentContext(
budget,
files,
ranked,
roles: loadRoleMap(options.root, graph.provenance?.corpusHash),
readFile,
repositoryId: repositoryIdFromRoot(options.root),
extraPinnedFacts: options.extraPinnedFacts,
Expand Down
8 changes: 7 additions & 1 deletion src/code/capsule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ export const TASK_CAPSULE_SCHEMA_VERSION = 'task-capsule/0' as const;
* module-less fallback. Seed content with the module active matches
* 2026.08.3 + the coding-prompt-corpus improvements; the recorded
* relevanceVersion says which engine ranked this capsule. */
export const CAPSULE_RANKING_VERSION = 'capsule-rank@2026.08.4' as const;
/** Bumped 2026.09.1: architecture-module role preference (engine/haile/
* role-preference.ts). When a classify file bound to the graph exists, the
* ranked seeds re-order by a bounded lift — a controller / application_service
* / port rises by at most two places — and utilities the ask did not reach for
* are dropped; the role travels as a structured field, never in the rendered
* text. No classify file = 2026.08.4 behaviour exactly. */
export const CAPSULE_RANKING_VERSION = 'capsule-rank@2026.09.1' as const;
export const CAPSULE_COMPILER_ID = 'vg-task-capsule/0' as const;

export interface BuildCapsuleOptions extends BuildContextOptions {
Expand Down
4 changes: 4 additions & 0 deletions src/code/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import { extractLiteralNeedles, queryGraph } from '../engine/query.js';
import type { SanitizedRank } from '../engine/relevance-provider.js';
import type { RoleMap } from '../engine/haile/role-preference.js';
import { indexFor } from '../engine/relations.js';
import { impactOf } from '../engine/impact.js';
import type { CodeContext } from './types.js';
Expand All @@ -33,6 +34,8 @@ export interface BuildContextOptions {
* ordering AND the plain-language concept map. Absent → the mechanical
* fallback ranks and the concept map is empty. */
ranked?: SanitizedRank | null;
/** Architecture-module roles (loadRoleMap) — orders seeds by role under the budget; absent → untouched. */
roles?: RoleMap | null;
}

/**
Expand Down Expand Up @@ -62,6 +65,7 @@ export function buildCodeContext(graph: VgGraph, instruction: string, options: B
budget: Math.floor(budget * 0.6),
limit: seedLimit * 2,
ranked: options.ranked,
roles: options.roles,
});
// Keep every candidate here: `--file` narrows below, and capping first would
// discard in-scope symbols ranked outside the top `seedLimit`. The cap runs
Expand Down
5 changes: 4 additions & 1 deletion src/code/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/

import * as fs from 'node:fs';
import { loadRoleMap } from '../engine/haile/role-preference.js';
import * as path from 'node:path';
import { buildCodeContext } from './context.js';
import { rankQuestion, type SanitizedRank } from '../engine/relevance-provider.js';
Expand Down Expand Up @@ -266,15 +267,17 @@ function buildSessionContext(
): CodeContext {
const budget = opts.budget;
const files = opts.files;
const roles = loadRoleMap(opts.root, graph.provenance?.corpusHash);
if (!opts.capsule) {
return buildCodeContext(graph, instruction, { budget, files, ranked: opts.ranked });
return buildCodeContext(graph, instruction, { budget, files, ranked: opts.ranked, roles });
}
const capsule = buildTaskCapsule(graph, instruction, {
budget,
files,
readFile: (rel) => opts.fsImpl.read(rel),
repositoryId: repositoryIdFromRoot(opts.root),
ranked: opts.ranked,
roles,
});
return capsuleToCodeContext(capsule);
}
Expand Down
12 changes: 9 additions & 3 deletions src/commands/ask.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Command } from 'commander';
import { loadRoleMap } from '../engine/haile/role-preference.js';
import { queryGraph, queryGraphSemantic, type QueryResult } from '../engine/query.js';
import { rankQuestion } from '../engine/relevance-provider.js';
import { rankingAskFrom } from '../engine/user-ask.js';
Expand Down Expand Up @@ -97,6 +98,10 @@ export function registerAsk(program: Command): void {
// silent on failure) — then rank, falling back mechanically when the
// module still is not available. `--local` stays network-free.
if (!global.offline) await ensureRelevanceModule();
// Architecture-module roles (when `vg build` classified this map): the
// context builders open with controllers / services / ports and leave
// unasked-for utilities out. Null when there is no classify file.
const roles = loadRoleMap(root, graph.provenance?.corpusHash);
const topicTags = await loadTopicTags(graph, root, resolveGraphPath(root, global.graph));
const modRank = await rankQuestion(graph, rankingAskFrom(q), { limit: 48, topicTags });

Expand All @@ -123,6 +128,7 @@ export function registerAsk(program: Command): void {
if (ranked) {
live?.set('answering from the daemon index…');
result = await queryGraphSemantic(graph, q, {
roles,
budget,
semanticRanked: ranked.ranked,
ranked: modRank,
Expand Down Expand Up @@ -153,18 +159,18 @@ export function registerAsk(program: Command): void {
const bar = !global.json ? new ProgressBar(c.dim('embedding')) : undefined;
const vectors = await getNodeEmbeddings(graph, embedder, root, bar ? (d, t) => bar.update(d, t) : undefined);
bar?.done();
result = await queryGraphSemantic(graph, q, { budget, embedder, nodeVectors: vectors, ranked: modRank });
result = await queryGraphSemantic(graph, q, { budget, embedder, nodeVectors: vectors, ranked: modRank, roles });
mode = `semantic (${embedder.id})`;
activity.add('answer', 'ok', `answered in this process · ${embedder.id} · ${vectors.size} vector(s)`);
} else {
result = queryGraph(graph, q, { budget, ranked: modRank });
result = queryGraph(graph, q, { budget, ranked: modRank, roles });
note = reason ? unavailableMessage(reason) : 'semantic unavailable; used lexical';
if (detail) note += ` (${detail})`;
activity.add('answer', 'warn', `lexical only — ${note}`);
}
}
} else {
result = queryGraph(graph, q, { budget, ranked: modRank });
result = queryGraph(graph, q, { budget, ranked: modRank, roles });
if (global.offline) note = 'semantic skipped under --offline; used lexical';
activity.add('answer', 'skip', global.offline ? 'lexical only (--offline)' : 'lexical only (--no-semantic)');
}
Expand Down
17 changes: 17 additions & 0 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fs from 'node:fs';
import { spawn } from 'node:child_process';
import { Command } from 'commander';
import { buildGraph } from '../engine/build.js';
import { ensureHaileModule } from '../install/haile-module.js';
import { verifyDeterminism } from '../engine/verify.js';
import { epistemicBreakdown } from '../engine/epistemic.js';
import { signGraphAttestation, verifyGraphAttestation, type SignSummary } from './attest-actions.js';
Expand Down Expand Up @@ -151,13 +152,29 @@ export async function runBuild(
}
bar?.done();

// Architecture classify is installed by default: run the bounded ensure now
// so this build can write the classify sidecar. A module that cannot be
// provisioned (or was declined / disabled) only costs the role/purpose
// lines — the build itself never waits on the network under --offline and
// never fails because of it.
const haile = global.offline ? null : await ensureHaileModule().catch(() => null);

const written = writeArtifacts(result.graph, {
root,
html: opts.html,
report: opts.report,
graphPath: global.graph,
});

if (haile?.status === 'unavailable' && !global.json && !global.quiet) {
info(
c.yellow(
' architecture module could not be installed — role/purpose lines are omitted; '
+ 'vg retries automatically (disable with VIBGRATE_NO_KERNEL=1)',
),
);
}

// Record the freshness snapshot (stat+hash per corpus file, plus this build's
// scope) so `vg serve`/`vg ask` can auto-refresh the map when the tree drifts.
// Skipped for a custom --graph target: that is an explicit artifact the
Expand Down
31 changes: 20 additions & 11 deletions src/commands/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,28 @@ import {
installHcsModule,
removeHcsModule,
} from '../install/hcs-module.js';
import {
HAILE_DISCLOSURE,
HAILE_MODULE_NAME,
haileModuleInstalled,
installHaileModule,
removeHaileModule,
} from '../install/haile-module.js';
import { type InstallOptions, type InstallResult, kernelDisabled, readConsent, writeConsent } from '../install/module-core.js';
import { loadRelevanceProvider, resetRelevanceProviderCache } from '../engine/relevance-provider.js';
import { loadHcsEngine, resetHcsEngineCache } from '../engine/hcs-provider.js';
import { loadHaileProvider, resetHaileProviderCache } from '../engine/haile/haile-provider.js';
import { CliError, ExitCode, usageError } from '../util/exit.js';
import { c, info, json, out } from '../util/output.js';
import { readGlobal, applyGlobalOptions } from '../cli-options.js';

/** Everything the command needs to manage one optional module. */
interface ManagedModule {
npmName: string;
disclosure: string;
install(opts: InstallOptions): Promise<InstallResult>;
remove(): void;
installed(): { installed: boolean; version?: string };
resetLoader(): void;
/** Load through the seam; report the loaded engine/provider version. */
loadedVersion(): Promise<string | null>;
}

Expand Down Expand Up @@ -58,19 +64,22 @@ const MODULES: Record<string, ManagedModule> = {
return engine ? engine.version() : null;
},
},
haile: {
npmName: HAILE_MODULE_NAME,
disclosure: HAILE_DISCLOSURE,
install: installHaileModule,
remove: removeHaileModule,
installed: haileModuleInstalled,
resetLoader: resetHaileProviderCache,
loadedVersion: async () => {
const provider = await loadHaileProvider();
return provider ? provider.version() : null;
},
},
};

const SUPPORTED = Object.keys(MODULES).join(', ');

/**
* `vg module` — manage optional local modules (relevance, hcs).
*
* `install` fetches the module from the npm registry as a plain tarball,
* verifies its integrity, and unpacks it into the vibgrate cache — the user's
* project is never touched and nothing from the tarball executes at install
* time. `remove` deletes it. `status` reports what is installed and whether
* the seam can load it. All state is per-user, not per-repo.
*/
export function registerModule(program: Command): void {
const cmd = program.command('module').description(`manage optional local modules (${SUPPORTED})`);

Expand Down
14 changes: 13 additions & 1 deletion src/commands/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { applyGlobalOptions, readGlobal } from '../cli-options.js';
import { requireGraph, rootOf } from './util.js';
import { ambiguityError } from './ambiguity.js';
import { c, info, json } from '../util/output.js';
import { resolveGraphPath } from '../engine/artifacts.js';
import { findHaileSymbol, formatHaileLines, haileJsonFields, readHaileSidecar } from '../engine/haile/index.js';

/**
* `vg show <name>` (VG-CLI-SPEC §3.3) — the richest single-node view: what it
Expand All @@ -21,7 +23,7 @@ export function registerShow(program: Command): void {
.option('--pick <n>', 'pick the nth candidate when ambiguous')
.action(function (this: Command, name: string, opts: { pick?: string }) {
const global = readGlobal(this);
const { graph } = requireGraph(global);
const { root, graph } = requireGraph(global);
const { node, candidates } = resolveOne(graph, name, opts.pick ? Number(opts.pick) : undefined);

if (!node) {
Expand All @@ -37,6 +39,12 @@ export function registerShow(program: Command): void {
const extendsEdges = index.out(node.id, 'extends').concat(index.out(node.id, 'implements'));
const supertypes = extendsEdges.map((e) => index.node(e.dst)?.qualifiedName).filter(Boolean);
const area = graph.areas.find((a) => a.id === node.area);
const haile = findHaileSymbol(
readHaileSidecar(resolveGraphPath(root, global.graph), {
corpusHash: graph.provenance?.corpusHash,
}),
node.id,
);

// `show` is the CLI twin of the MCP `get_node` tool — record it under that
// shared name (source `cli`) when an AI host identified itself. Baseline =
Expand Down Expand Up @@ -79,6 +87,7 @@ export function registerShow(program: Command): void {
calls: callees.map((n) => n.qualifiedName),
calledBy: callers.map((n) => n.qualifiedName),
extends: supertypes,
haile: haileJsonFields(haile) ?? null,
});
return;
}
Expand All @@ -89,6 +98,9 @@ export function registerShow(program: Command): void {
info(
` importance ${node.importance.toFixed(3)}${node.isHub ? c.yellow(' ★ hub') : ''} · area #${node.area}${area ? ` ${c.dim(area.label)}` : ''}`,
);
if (haile) {
for (const line of formatHaileLines(haile)) info(line);
}
if (supertypes.length) info(` ${c.dim('extends:')} ${supertypes.join(', ')}`);
info(` ${c.dim('calls')} (${callees.length}): ${callees.slice(0, 12).map((n) => n.qualifiedName).join(', ') || '—'}`);
info(` ${c.dim('called by')} (${callers.length}): ${callers.slice(0, 12).map((n) => n.qualifiedName).join(', ') || '—'}`);
Expand Down
Loading