diff --git a/.aios-core/core/code-intel/code-intel-client.js b/.aios-core/core/code-intel/code-intel-client.js
index 2717d699f..f850c9eca 100644
--- a/.aios-core/core/code-intel/code-intel-client.js
+++ b/.aios-core/core/code-intel/code-intel-client.js
@@ -1,6 +1,7 @@
'use strict';
const { CodeGraphProvider } = require('./providers/code-graph-provider');
+const { RegistryProvider } = require('./providers/registry-provider');
// --- Constants (adjustable, not hardcoded magic numbers) ---
const CIRCUIT_BREAKER_THRESHOLD = 3;
@@ -51,10 +52,19 @@ class CodeIntelClient {
/**
* Register default providers based on configuration.
+ * Provider priority: RegistryProvider FIRST (native, T1), then CodeGraphProvider (MCP, T3).
+ * First provider with isAvailable() === true wins.
* @private
*/
_registerDefaultProviders(options) {
- // Code Graph MCP is the primary (and currently only) provider
+ // RegistryProvider — native, T1, always available when registry exists
+ const registryProvider = new RegistryProvider({
+ registryPath: options.registryPath || null,
+ projectRoot: options.projectRoot || null,
+ });
+ this._providers.push(registryProvider);
+
+ // Code Graph MCP — T3, available when mcpCallFn is configured
const codeGraphProvider = new CodeGraphProvider({
mcpServerName: options.mcpServerName || 'code-graph',
mcpCallFn: options.mcpCallFn || null,
@@ -74,6 +84,7 @@ class CodeIntelClient {
/**
* Detect and return the first available provider.
+ * Uses polymorphic isAvailable() — first provider that returns true wins.
* @returns {import('./providers/provider-interface').CodeIntelProvider|null}
* @private
*/
@@ -81,10 +92,13 @@ class CodeIntelClient {
if (this._activeProvider) return this._activeProvider;
for (const provider of this._providers) {
- // A provider is considered "available" if it has a configured mcpCallFn
- if (provider.options && typeof provider.options.mcpCallFn === 'function') {
- this._activeProvider = provider;
- return provider;
+ try {
+ if (typeof provider.isAvailable === 'function' && provider.isAvailable()) {
+ this._activeProvider = provider;
+ return provider;
+ }
+ } catch (_err) {
+ // Provider threw during availability check — treat as unavailable
}
}
diff --git a/.aios-core/core/code-intel/hook-runtime.js b/.aios-core/core/code-intel/hook-runtime.js
new file mode 100644
index 000000000..5b9ceda61
--- /dev/null
+++ b/.aios-core/core/code-intel/hook-runtime.js
@@ -0,0 +1,186 @@
+'use strict';
+
+const path = require('path');
+const { RegistryProvider } = require('./providers/registry-provider');
+
+/** Cached provider instance (survives across hook invocations in same process). */
+let _provider = null;
+let _providerRoot = null;
+
+/**
+ * Get or create a RegistryProvider singleton.
+ * Resets if projectRoot changes between calls.
+ * @param {string} projectRoot - Project root directory
+ * @returns {RegistryProvider}
+ */
+function getProvider(projectRoot) {
+ if (!_provider || _providerRoot !== projectRoot) {
+ _provider = new RegistryProvider({ projectRoot });
+ _providerRoot = projectRoot;
+ }
+ return _provider;
+}
+
+/**
+ * Resolve code intelligence context for a file being written/edited.
+ *
+ * Queries RegistryProvider for:
+ * - Entity definition (path, layer, purpose, type)
+ * - References (files that use this entity)
+ * - Dependencies (entities this file depends on)
+ *
+ * @param {string} filePath - Absolute or relative path to the target file
+ * @param {string} cwd - Project root / working directory
+ * @returns {{ entity: Object|null, references: Array|null, dependencies: Object|null }|null}
+ */
+async function resolveCodeIntel(filePath, cwd) {
+ if (!filePath || !cwd) return null;
+
+ try {
+ const provider = getProvider(cwd);
+ if (!provider.isAvailable()) return null;
+
+ // Normalize to relative path (registry uses relative paths)
+ let relativePath = filePath;
+ if (path.isAbsolute(filePath)) {
+ relativePath = path.relative(cwd, filePath).replace(/\\/g, '/');
+ } else {
+ relativePath = filePath.replace(/\\/g, '/');
+ }
+
+ // Run all three queries in parallel
+ const [definition, references, dependencies] = await Promise.all([
+ provider.findDefinition(relativePath),
+ provider.findReferences(relativePath),
+ provider.analyzeDependencies(relativePath),
+ ]);
+
+ // Treat empty dependency graph as no data
+ const hasUsefulDeps = dependencies && dependencies.nodes && dependencies.nodes.length > 0;
+
+ // If nothing found at all, try searching by the file basename
+ if (!definition && !references && !hasUsefulDeps) {
+ const basename = path.basename(relativePath, path.extname(relativePath));
+ const fallbackDef = await provider.findDefinition(basename);
+ if (!fallbackDef) return null;
+
+ const [fallbackRefs, fallbackDeps] = await Promise.all([
+ provider.findReferences(basename),
+ provider.analyzeDependencies(basename),
+ ]);
+
+ return {
+ entity: fallbackDef,
+ references: fallbackRefs,
+ dependencies: fallbackDeps,
+ };
+ }
+
+ return {
+ entity: definition,
+ references,
+ dependencies,
+ };
+ } catch (_err) {
+ // Guard against provider exceptions to avoid unhandled rejections in hook runtime
+ return null;
+ }
+}
+
+/**
+ * Format code intelligence data as XML for injection into Claude context.
+ *
+ * @param {Object|null} intel - Result from resolveCodeIntel()
+ * @param {string} filePath - Target file path (for display)
+ * @returns {string|null} XML string or null if no data
+ */
+function formatAsXml(intel, filePath) {
+ if (!intel) return null;
+
+ const { entity, references, dependencies } = intel;
+
+ // At least one piece of data must exist
+ if (!entity && !references && !dependencies) return null;
+
+ const lines = [''];
+ lines.push(` ${escapeXml(filePath)}`);
+
+ // Entity definition
+ if (entity) {
+ lines.push(' ');
+ if (entity.file) lines.push(` ${escapeXml(entity.file)}`);
+ if (entity.context) lines.push(` ${escapeXml(entity.context)}`);
+ lines.push(' ');
+ }
+
+ // References
+ if (references && references.length > 0) {
+ // Deduplicate by file path
+ const uniqueRefs = [];
+ const seen = new Set();
+ for (const ref of references) {
+ if (ref.file && !seen.has(ref.file)) {
+ seen.add(ref.file);
+ uniqueRefs.push(ref);
+ }
+ }
+
+ lines.push(` `);
+ for (const ref of uniqueRefs.slice(0, 15)) {
+ const ctx = ref.context ? ` context="${escapeXml(ref.context)}"` : '';
+ lines.push(` `);
+ }
+ if (uniqueRefs.length > 15) {
+ lines.push(` `);
+ }
+ lines.push(' ');
+ }
+
+ // Dependencies
+ if (dependencies && dependencies.nodes && dependencies.nodes.length > 1) {
+ // First node is the target itself, rest are dependencies
+ const depNodes = dependencies.nodes.slice(1);
+ lines.push(` `);
+ for (const dep of depNodes.slice(0, 10)) {
+ const layer = dep.layer ? ` layer="${dep.layer}"` : '';
+ lines.push(` `);
+ }
+ if (depNodes.length > 10) {
+ lines.push(` `);
+ }
+ lines.push(' ');
+ }
+
+ lines.push('');
+ return lines.join('\n');
+}
+
+/**
+ * Escape special XML characters.
+ * @param {string} str
+ * @returns {string}
+ */
+function escapeXml(str) {
+ if (!str) return '';
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+/**
+ * Reset cached provider (for testing).
+ */
+function _resetForTesting() {
+ _provider = null;
+ _providerRoot = null;
+}
+
+module.exports = {
+ resolveCodeIntel,
+ formatAsXml,
+ escapeXml,
+ getProvider,
+ _resetForTesting,
+};
diff --git a/.aios-core/core/code-intel/index.js b/.aios-core/core/code-intel/index.js
index 5be19f8af..27932d274 100644
--- a/.aios-core/core/code-intel/index.js
+++ b/.aios-core/core/code-intel/index.js
@@ -4,6 +4,7 @@ const { CodeIntelClient } = require('./code-intel-client');
const { CodeIntelEnricher } = require('./code-intel-enricher');
const { CodeIntelProvider, CAPABILITIES } = require('./providers/provider-interface');
const { CodeGraphProvider, TOOL_MAP } = require('./providers/code-graph-provider');
+const { RegistryProvider } = require('./providers/registry-provider');
// Singleton client instance (lazily initialized)
let _defaultClient = null;
@@ -127,6 +128,7 @@ module.exports = {
CodeIntelEnricher,
CodeIntelProvider,
CodeGraphProvider,
+ RegistryProvider,
// Constants
CAPABILITIES,
diff --git a/.aios-core/core/code-intel/providers/code-graph-provider.js b/.aios-core/core/code-intel/providers/code-graph-provider.js
index 46fd6eab1..fc6b98f9e 100644
--- a/.aios-core/core/code-intel/providers/code-graph-provider.js
+++ b/.aios-core/core/code-intel/providers/code-graph-provider.js
@@ -29,6 +29,14 @@ class CodeGraphProvider extends CodeIntelProvider {
this._mcpServerName = options.mcpServerName || 'code-graph';
}
+ /**
+ * Code Graph provider is available when mcpCallFn is configured.
+ * @returns {boolean}
+ */
+ isAvailable() {
+ return typeof this.options.mcpCallFn === 'function';
+ }
+
/**
* Execute an MCP tool call via the configured server.
* This method is the single point of MCP communication — all capabilities route through here.
diff --git a/.aios-core/core/code-intel/providers/provider-interface.js b/.aios-core/core/code-intel/providers/provider-interface.js
index c4fbb9c18..bba3fa86c 100644
--- a/.aios-core/core/code-intel/providers/provider-interface.js
+++ b/.aios-core/core/code-intel/providers/provider-interface.js
@@ -14,6 +14,15 @@ class CodeIntelProvider {
this.options = options;
}
+ /**
+ * Check if this provider is available and can serve requests.
+ * Subclasses MUST override this to indicate availability.
+ * @returns {boolean}
+ */
+ isAvailable() {
+ return false;
+ }
+
/**
* Locate the definition of a symbol.
* @param {string} symbol - Symbol name to find
diff --git a/.aios-core/core/code-intel/providers/registry-provider.js b/.aios-core/core/code-intel/providers/registry-provider.js
new file mode 100644
index 000000000..b759bc328
--- /dev/null
+++ b/.aios-core/core/code-intel/providers/registry-provider.js
@@ -0,0 +1,515 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { CodeIntelProvider } = require('./provider-interface');
+
+// Layer priority for disambiguation (lower index = higher priority)
+const LAYER_PRIORITY = { L1: 0, L2: 1, L3: 2, L4: 3 };
+
+/**
+ * RegistryProvider — Native code intelligence provider using Entity Registry.
+ *
+ * Implements 5 of 8 primitives without requiring any MCP server.
+ * Data source: .aios-core/data/entity-registry.yaml (737+ entities, 14 categories).
+ *
+ * AST-only primitives (findCallers, findCallees, analyzeComplexity) return null.
+ */
+class RegistryProvider extends CodeIntelProvider {
+ constructor(options = {}) {
+ super('registry', options);
+
+ this._registryPath = options.registryPath || null;
+ this._registry = null;
+ this._registryMtime = null;
+
+ // In-memory indexes (built on first load)
+ this._byName = null; // Map>
+ this._byPath = null; // Map
+ this._byCategory = null; // Map>
+ this._byKeyword = null; // Map> (inverted index)
+ }
+
+ /**
+ * Check if this provider is available (registry loaded and non-empty).
+ * @returns {boolean}
+ */
+ isAvailable() {
+ this._ensureLoaded();
+ return this._registry !== null && this._byName !== null && this._byName.size > 0;
+ }
+
+ // --- Lazy Loading ---
+
+ /**
+ * Resolve the registry file path from options or default location.
+ * @returns {string|null}
+ * @private
+ */
+ _resolveRegistryPath() {
+ if (this._registryPath) return this._registryPath;
+
+ // Default: resolve from project root
+ const projectRoot = this.options.projectRoot || process.cwd();
+ const defaultPath = path.join(projectRoot, '.aios-core', 'data', 'entity-registry.yaml');
+
+ if (fs.existsSync(defaultPath)) {
+ this._registryPath = defaultPath;
+ return defaultPath;
+ }
+
+ return null;
+ }
+
+ /**
+ * Ensure registry is loaded (lazy-load on first call).
+ * Reloads if file mtime has changed.
+ * @private
+ */
+ _ensureLoaded() {
+ const filePath = this._resolveRegistryPath();
+ if (!filePath) return;
+
+ try {
+ const stat = fs.statSync(filePath);
+ const currentMtime = stat.mtimeMs;
+
+ // Already loaded and file hasn't changed
+ if (this._registry && this._registryMtime === currentMtime) return;
+
+ const content = fs.readFileSync(filePath, 'utf8');
+
+ // Use js-yaml with JSON_SCHEMA for safe parsing (no arbitrary types)
+ let yaml;
+ try {
+ yaml = require('js-yaml');
+ } catch (_e) {
+ // Fallback to yaml package
+ yaml = require('yaml');
+ const parsed = yaml.parse(content);
+ this._buildIndexes(parsed);
+ this._registryMtime = currentMtime;
+ return;
+ }
+
+ const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
+ this._buildIndexes(parsed);
+ this._registryMtime = currentMtime;
+ } catch (_error) {
+ // Graceful degradation: if parse fails, provider returns null for all calls
+ this._registry = null;
+ this._byName = null;
+ this._byPath = null;
+ this._byCategory = null;
+ this._byKeyword = null;
+ }
+ }
+
+ /**
+ * Build in-memory indexes from parsed registry.
+ * @param {Object} parsed - Parsed YAML object
+ * @private
+ */
+ _buildIndexes(parsed) {
+ if (!parsed || !parsed.entities) {
+ this._registry = null;
+ this._byName = null;
+ this._byPath = null;
+ this._byCategory = null;
+ this._byKeyword = null;
+ return;
+ }
+
+ this._registry = parsed;
+ this._byName = new Map();
+ this._byPath = new Map();
+ this._byCategory = new Map();
+ this._byKeyword = new Map();
+
+ const entities = parsed.entities;
+
+ for (const [category, categoryEntities] of Object.entries(entities)) {
+ if (!categoryEntities || typeof categoryEntities !== 'object') continue;
+
+ // byCategory
+ if (!this._byCategory.has(category)) {
+ this._byCategory.set(category, []);
+ }
+
+ for (const [entityName, entityData] of Object.entries(categoryEntities)) {
+ if (!entityData || typeof entityData !== 'object') continue;
+
+ // Validate path: reject entries with '..' segments (defense-in-depth)
+ if (entityData.path && entityData.path.includes('..')) continue;
+
+ const entity = {
+ name: entityName,
+ category,
+ ...entityData,
+ };
+
+ // byName — Map> to handle duplicates
+ if (!this._byName.has(entityName)) {
+ this._byName.set(entityName, []);
+ }
+ this._byName.get(entityName).push(entity);
+
+ // byPath
+ if (entityData.path) {
+ this._byPath.set(entityData.path, entity);
+ }
+
+ // byCategory
+ this._byCategory.get(category).push(entity);
+
+ // byKeyword (inverted index)
+ if (Array.isArray(entityData.keywords)) {
+ for (const keyword of entityData.keywords) {
+ const kw = String(keyword).toLowerCase();
+ if (!this._byKeyword.has(kw)) {
+ this._byKeyword.set(kw, []);
+ }
+ this._byKeyword.get(kw).push(entity);
+ }
+ }
+ }
+ }
+ }
+
+ // --- Disambiguation ---
+
+ /**
+ * Score and rank candidates for a symbol lookup.
+ * Scoring: exact name+type > exact name > layer priority > alphabetical path.
+ * @param {Array