aios-core 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.aios-core/core/code-intel/code-intel-client.js +19 -5
  2. package/.aios-core/core/code-intel/hook-runtime.js +186 -0
  3. package/.aios-core/core/code-intel/index.js +2 -0
  4. package/.aios-core/core/code-intel/providers/code-graph-provider.js +8 -0
  5. package/.aios-core/core/code-intel/providers/provider-interface.js +9 -0
  6. package/.aios-core/core/code-intel/providers/registry-provider.js +515 -0
  7. package/.aios-core/core/doctor/checks/code-intel.js +95 -21
  8. package/.aios-core/core/doctor/checks/hooks-claude-count.js +15 -4
  9. package/.aios-core/core/doctor/checks/ide-sync.js +24 -7
  10. package/.aios-core/core/synapse/memory/memory-bridge.js +17 -43
  11. package/.aios-core/core/synapse/memory/synapse-memory-provider.js +201 -0
  12. package/.aios-core/data/entity-registry.yaml +836 -812
  13. package/.aios-core/data/workflow-chains.yaml +156 -0
  14. package/.aios-core/development/agents/aios-master.md +17 -10
  15. package/.aios-core/development/agents/analyst.md +17 -10
  16. package/.aios-core/development/agents/architect.md +17 -10
  17. package/.aios-core/development/agents/data-engineer.md +17 -10
  18. package/.aios-core/development/agents/dev.md +17 -10
  19. package/.aios-core/development/agents/devops.md +22 -10
  20. package/.aios-core/development/agents/pm.md +17 -10
  21. package/.aios-core/development/agents/po.md +17 -10
  22. package/.aios-core/development/agents/qa.md +17 -10
  23. package/.aios-core/development/agents/sm.md +17 -10
  24. package/.aios-core/development/agents/squad-creator.md +18 -9
  25. package/.aios-core/development/agents/ux-design-expert.md +16 -9
  26. package/.aios-core/development/tasks/apply-qa-fixes.md +7 -0
  27. package/.aios-core/development/tasks/architect-analyze-impact.md +8 -1
  28. package/.aios-core/development/tasks/brownfield-create-story.md +7 -0
  29. package/.aios-core/development/tasks/build-autonomous.md +7 -0
  30. package/.aios-core/development/tasks/create-deep-research-prompt.md +7 -0
  31. package/.aios-core/development/tasks/create-next-story.md +7 -0
  32. package/.aios-core/development/tasks/create-suite.md +7 -0
  33. package/.aios-core/development/tasks/dev-develop-story.md +8 -0
  34. package/.aios-core/development/tasks/execute-checklist.md +7 -0
  35. package/.aios-core/development/tasks/github-devops-github-pr-automation.md +7 -0
  36. package/.aios-core/development/tasks/github-devops-pre-push-quality-gate.md +7 -0
  37. package/.aios-core/development/tasks/po-close-story.md +7 -0
  38. package/.aios-core/development/tasks/qa-create-fix-request.md +7 -0
  39. package/.aios-core/development/tasks/qa-fix-issues.md +7 -0
  40. package/.aios-core/development/tasks/qa-gate.md +8 -0
  41. package/.aios-core/development/tasks/qa-review-story.md +8 -0
  42. package/.aios-core/development/tasks/release-management.md +7 -0
  43. package/.aios-core/development/tasks/spec-critique.md +8 -0
  44. package/.aios-core/development/tasks/spec-gather-requirements.md +7 -0
  45. package/.aios-core/development/tasks/spec-write-spec.md +5 -0
  46. package/.aios-core/development/tasks/validate-next-story.md +7 -0
  47. package/.aios-core/install-manifest.yaml +105 -89
  48. package/.aios-core/product/templates/ide-rules/claude-rules.md +48 -0
  49. package/package.json +1 -1
  50. package/packages/installer/src/config/templates/core-config-template.js +25 -0
  51. package/packages/installer/src/wizard/ide-config-generator.js +24 -3
  52. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +15 -5
  53. package/packages/installer/tests/unit/claude-md-template-v5/claude-md-template-v5.test.js +3 -3
  54. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +68 -9
@@ -0,0 +1,515 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { CodeIntelProvider } = require('./provider-interface');
6
+
7
+ // Layer priority for disambiguation (lower index = higher priority)
8
+ const LAYER_PRIORITY = { L1: 0, L2: 1, L3: 2, L4: 3 };
9
+
10
+ /**
11
+ * RegistryProvider — Native code intelligence provider using Entity Registry.
12
+ *
13
+ * Implements 5 of 8 primitives without requiring any MCP server.
14
+ * Data source: .aios-core/data/entity-registry.yaml (737+ entities, 14 categories).
15
+ *
16
+ * AST-only primitives (findCallers, findCallees, analyzeComplexity) return null.
17
+ */
18
+ class RegistryProvider extends CodeIntelProvider {
19
+ constructor(options = {}) {
20
+ super('registry', options);
21
+
22
+ this._registryPath = options.registryPath || null;
23
+ this._registry = null;
24
+ this._registryMtime = null;
25
+
26
+ // In-memory indexes (built on first load)
27
+ this._byName = null; // Map<string, Array<Entity>>
28
+ this._byPath = null; // Map<string, Entity>
29
+ this._byCategory = null; // Map<string, Array<Entity>>
30
+ this._byKeyword = null; // Map<string, Array<Entity>> (inverted index)
31
+ }
32
+
33
+ /**
34
+ * Check if this provider is available (registry loaded and non-empty).
35
+ * @returns {boolean}
36
+ */
37
+ isAvailable() {
38
+ this._ensureLoaded();
39
+ return this._registry !== null && this._byName !== null && this._byName.size > 0;
40
+ }
41
+
42
+ // --- Lazy Loading ---
43
+
44
+ /**
45
+ * Resolve the registry file path from options or default location.
46
+ * @returns {string|null}
47
+ * @private
48
+ */
49
+ _resolveRegistryPath() {
50
+ if (this._registryPath) return this._registryPath;
51
+
52
+ // Default: resolve from project root
53
+ const projectRoot = this.options.projectRoot || process.cwd();
54
+ const defaultPath = path.join(projectRoot, '.aios-core', 'data', 'entity-registry.yaml');
55
+
56
+ if (fs.existsSync(defaultPath)) {
57
+ this._registryPath = defaultPath;
58
+ return defaultPath;
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Ensure registry is loaded (lazy-load on first call).
66
+ * Reloads if file mtime has changed.
67
+ * @private
68
+ */
69
+ _ensureLoaded() {
70
+ const filePath = this._resolveRegistryPath();
71
+ if (!filePath) return;
72
+
73
+ try {
74
+ const stat = fs.statSync(filePath);
75
+ const currentMtime = stat.mtimeMs;
76
+
77
+ // Already loaded and file hasn't changed
78
+ if (this._registry && this._registryMtime === currentMtime) return;
79
+
80
+ const content = fs.readFileSync(filePath, 'utf8');
81
+
82
+ // Use js-yaml with JSON_SCHEMA for safe parsing (no arbitrary types)
83
+ let yaml;
84
+ try {
85
+ yaml = require('js-yaml');
86
+ } catch (_e) {
87
+ // Fallback to yaml package
88
+ yaml = require('yaml');
89
+ const parsed = yaml.parse(content);
90
+ this._buildIndexes(parsed);
91
+ this._registryMtime = currentMtime;
92
+ return;
93
+ }
94
+
95
+ const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
96
+ this._buildIndexes(parsed);
97
+ this._registryMtime = currentMtime;
98
+ } catch (_error) {
99
+ // Graceful degradation: if parse fails, provider returns null for all calls
100
+ this._registry = null;
101
+ this._byName = null;
102
+ this._byPath = null;
103
+ this._byCategory = null;
104
+ this._byKeyword = null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Build in-memory indexes from parsed registry.
110
+ * @param {Object} parsed - Parsed YAML object
111
+ * @private
112
+ */
113
+ _buildIndexes(parsed) {
114
+ if (!parsed || !parsed.entities) {
115
+ this._registry = null;
116
+ this._byName = null;
117
+ this._byPath = null;
118
+ this._byCategory = null;
119
+ this._byKeyword = null;
120
+ return;
121
+ }
122
+
123
+ this._registry = parsed;
124
+ this._byName = new Map();
125
+ this._byPath = new Map();
126
+ this._byCategory = new Map();
127
+ this._byKeyword = new Map();
128
+
129
+ const entities = parsed.entities;
130
+
131
+ for (const [category, categoryEntities] of Object.entries(entities)) {
132
+ if (!categoryEntities || typeof categoryEntities !== 'object') continue;
133
+
134
+ // byCategory
135
+ if (!this._byCategory.has(category)) {
136
+ this._byCategory.set(category, []);
137
+ }
138
+
139
+ for (const [entityName, entityData] of Object.entries(categoryEntities)) {
140
+ if (!entityData || typeof entityData !== 'object') continue;
141
+
142
+ // Validate path: reject entries with '..' segments (defense-in-depth)
143
+ if (entityData.path && entityData.path.includes('..')) continue;
144
+
145
+ const entity = {
146
+ name: entityName,
147
+ category,
148
+ ...entityData,
149
+ };
150
+
151
+ // byName — Map<string, Array<Entity>> to handle duplicates
152
+ if (!this._byName.has(entityName)) {
153
+ this._byName.set(entityName, []);
154
+ }
155
+ this._byName.get(entityName).push(entity);
156
+
157
+ // byPath
158
+ if (entityData.path) {
159
+ this._byPath.set(entityData.path, entity);
160
+ }
161
+
162
+ // byCategory
163
+ this._byCategory.get(category).push(entity);
164
+
165
+ // byKeyword (inverted index)
166
+ if (Array.isArray(entityData.keywords)) {
167
+ for (const keyword of entityData.keywords) {
168
+ const kw = String(keyword).toLowerCase();
169
+ if (!this._byKeyword.has(kw)) {
170
+ this._byKeyword.set(kw, []);
171
+ }
172
+ this._byKeyword.get(kw).push(entity);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ // --- Disambiguation ---
180
+
181
+ /**
182
+ * Score and rank candidates for a symbol lookup.
183
+ * Scoring: exact name+type > exact name > layer priority > alphabetical path.
184
+ * @param {Array<Object>} candidates - Array of entity objects
185
+ * @param {string} symbol - The search symbol
186
+ * @param {Object} [options] - Optional type/layer hints
187
+ * @returns {Array<Object>} Sorted candidates (best match first)
188
+ * @private
189
+ */
190
+ _rankCandidates(candidates, symbol, options = {}) {
191
+ if (!candidates || candidates.length === 0) return [];
192
+
193
+ const symbolLower = symbol.toLowerCase();
194
+
195
+ return candidates
196
+ .map((entity) => {
197
+ let score = 0;
198
+
199
+ // Exact name match
200
+ if (entity.name === symbol) score += 100;
201
+ else if (entity.name.toLowerCase() === symbolLower) score += 90;
202
+
203
+ // Type hint match
204
+ if (options.type && entity.type === options.type) score += 50;
205
+
206
+ // Layer priority (L1=40, L2=30, L3=20, L4=10)
207
+ const layerPriority = LAYER_PRIORITY[entity.layer];
208
+ if (layerPriority !== undefined) {
209
+ score += (4 - layerPriority) * 10;
210
+ }
211
+
212
+ return { entity, score };
213
+ })
214
+ .sort((a, b) => {
215
+ if (b.score !== a.score) return b.score - a.score;
216
+ // Tie-break: alphabetical path
217
+ const pathA = a.entity.path || '';
218
+ const pathB = b.entity.path || '';
219
+ return pathA.localeCompare(pathB);
220
+ })
221
+ .map((item) => item.entity);
222
+ }
223
+
224
+ // --- Fuzzy Matching ---
225
+
226
+ /**
227
+ * Find entities matching a symbol using fuzzy matching.
228
+ * Order: exact name > path contains > keywords contains.
229
+ * @param {string} symbol - Symbol to search
230
+ * @param {Object} [options] - Search options
231
+ * @returns {Array<Object>} Matched entities sorted by relevance
232
+ * @private
233
+ */
234
+ _fuzzyMatch(symbol, _options = {}) {
235
+ this._ensureLoaded();
236
+ if (!this._byName) return [];
237
+
238
+ const symbolLower = symbol.toLowerCase();
239
+ const results = [];
240
+ const seen = new Set();
241
+
242
+ // 1. Exact name match (may return multiple for duplicate names)
243
+ const exactMatches = this._byName.get(symbol) || this._byName.get(symbolLower) || [];
244
+ for (const entity of exactMatches) {
245
+ const key = `${entity.name}:${entity.category}:${entity.path}`;
246
+ if (!seen.has(key)) {
247
+ results.push({ entity, matchType: 'exact', score: 100 });
248
+ seen.add(key);
249
+ }
250
+ }
251
+
252
+ // Also check case-insensitive
253
+ if (exactMatches.length === 0) {
254
+ for (const [name, entities] of this._byName) {
255
+ if (name.toLowerCase() === symbolLower) {
256
+ for (const entity of entities) {
257
+ const key = `${entity.name}:${entity.category}:${entity.path}`;
258
+ if (!seen.has(key)) {
259
+ results.push({ entity, matchType: 'exact-ci', score: 90 });
260
+ seen.add(key);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ // 2. Path contains
268
+ for (const [filePath, entity] of this._byPath) {
269
+ const key = `${entity.name}:${entity.category}:${entity.path}`;
270
+ if (seen.has(key)) continue;
271
+ if (filePath.toLowerCase().includes(symbolLower)) {
272
+ results.push({ entity, matchType: 'path', score: 60 });
273
+ seen.add(key);
274
+ }
275
+ }
276
+
277
+ // 3. Keywords contains
278
+ const keywordMatches = this._byKeyword.get(symbolLower) || [];
279
+ for (const entity of keywordMatches) {
280
+ const key = `${entity.name}:${entity.category}:${entity.path}`;
281
+ if (seen.has(key)) continue;
282
+ results.push({ entity, matchType: 'keyword', score: 40 });
283
+ seen.add(key);
284
+ }
285
+
286
+ // Sort by score, then by layer priority, then alphabetical path
287
+ return results
288
+ .sort((a, b) => {
289
+ if (b.score !== a.score) return b.score - a.score;
290
+ const layerA = LAYER_PRIORITY[a.entity.layer] ?? 99;
291
+ const layerB = LAYER_PRIORITY[b.entity.layer] ?? 99;
292
+ if (layerA !== layerB) return layerA - layerB;
293
+ return (a.entity.path || '').localeCompare(b.entity.path || '');
294
+ })
295
+ .map((r) => r.entity);
296
+ }
297
+
298
+ // --- 5 Implemented Primitives ---
299
+
300
+ async findDefinition(symbol, options = {}) {
301
+ this._ensureLoaded();
302
+ if (!this._byName) return null;
303
+
304
+ const matches = this._fuzzyMatch(symbol, options);
305
+ if (matches.length === 0) return null;
306
+
307
+ const best = this._rankCandidates(matches, symbol, options)[0] || matches[0];
308
+
309
+ return {
310
+ file: best.path || null,
311
+ line: 1,
312
+ column: 0,
313
+ context: best.purpose || `${best.type} in ${best.category}`,
314
+ };
315
+ }
316
+
317
+ async findReferences(symbol, _options = {}) {
318
+ this._ensureLoaded();
319
+ if (!this._byName) return null;
320
+
321
+ const references = [];
322
+ const symbolLower = symbol.toLowerCase();
323
+
324
+ // Search usedBy and dependencies fields across all entities
325
+ for (const [_category, categoryEntities] of Object.entries(this._registry.entities)) {
326
+ if (!categoryEntities || typeof categoryEntities !== 'object') continue;
327
+
328
+ for (const [_entityName, entityData] of Object.entries(categoryEntities)) {
329
+ if (!entityData || typeof entityData !== 'object') continue;
330
+
331
+ const usedBy = Array.isArray(entityData.usedBy) ? entityData.usedBy : [];
332
+ const deps = Array.isArray(entityData.dependencies) ? entityData.dependencies : [];
333
+
334
+ // Check if this entity references the symbol
335
+ const referencesSymbol =
336
+ usedBy.some((u) => String(u).toLowerCase() === symbolLower) ||
337
+ deps.some((d) => String(d).toLowerCase() === symbolLower);
338
+
339
+ if (referencesSymbol) {
340
+ references.push({
341
+ file: entityData.path || null,
342
+ line: 1,
343
+ context: entityData.purpose || `References ${symbol}`,
344
+ });
345
+ }
346
+ }
347
+ }
348
+
349
+ // Also find entities that the symbol's usedBy/deps point to
350
+ const symbolEntities = this._byName.get(symbol) || [];
351
+ for (const entity of symbolEntities) {
352
+ if (Array.isArray(entity.usedBy)) {
353
+ for (const refName of entity.usedBy) {
354
+ const refEntities = this._byName.get(refName) || [];
355
+ for (const ref of refEntities) {
356
+ references.push({
357
+ file: ref.path || null,
358
+ line: 1,
359
+ context: `${ref.name} uses ${symbol}`,
360
+ });
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ return references.length > 0 ? references : null;
367
+ }
368
+
369
+ async analyzeDependencies(targetPath, options = {}) {
370
+ this._ensureLoaded();
371
+ if (!this._byName || !this._registry) return null;
372
+
373
+ const nodes = [];
374
+ const edges = [];
375
+ let unresolvedCount = 0;
376
+ const visited = new Set();
377
+
378
+ // Find entity by path or name
379
+ let rootEntities = [];
380
+ if (this._byPath.has(targetPath)) {
381
+ rootEntities = [this._byPath.get(targetPath)];
382
+ } else {
383
+ rootEntities = this._byName.get(targetPath) || [];
384
+ }
385
+
386
+ if (rootEntities.length === 0) {
387
+ // Try fuzzy match
388
+ const fuzzy = this._fuzzyMatch(targetPath, options);
389
+ if (fuzzy.length > 0) rootEntities = [fuzzy[0]];
390
+ }
391
+
392
+ const queue = [...rootEntities];
393
+
394
+ while (queue.length > 0) {
395
+ const entity = queue.shift();
396
+ const entityKey = `${entity.name}:${entity.category}`;
397
+
398
+ if (visited.has(entityKey)) continue;
399
+ visited.add(entityKey);
400
+
401
+ nodes.push({
402
+ name: entity.name,
403
+ path: entity.path || null,
404
+ layer: entity.layer || null,
405
+ category: entity.category || null,
406
+ });
407
+
408
+ const deps = Array.isArray(entity.dependencies) ? entity.dependencies : [];
409
+ for (const depName of deps) {
410
+ const depEntities = this._byName.get(depName);
411
+ if (depEntities && depEntities.length > 0) {
412
+ const dep = depEntities[0]; // Take first match
413
+ edges.push({
414
+ from: entity.name,
415
+ to: dep.name,
416
+ resolved: true,
417
+ });
418
+
419
+ const depKey = `${dep.name}:${dep.category}`;
420
+ if (!visited.has(depKey)) {
421
+ queue.push(dep);
422
+ }
423
+ } else {
424
+ // Unresolved dependency
425
+ edges.push({
426
+ from: entity.name,
427
+ to: depName,
428
+ resolved: false,
429
+ });
430
+ unresolvedCount++;
431
+ }
432
+ }
433
+ }
434
+
435
+ return {
436
+ nodes,
437
+ edges,
438
+ unresolvedCount,
439
+ };
440
+ }
441
+
442
+ async analyzeCodebase(targetPath, _options = {}) {
443
+ this._ensureLoaded();
444
+ if (!this._byCategory) return null;
445
+
446
+ const files = [];
447
+ const structure = {};
448
+ const patterns = [];
449
+
450
+ for (const [category, entities] of this._byCategory) {
451
+ structure[category] = {
452
+ count: entities.length,
453
+ layers: {},
454
+ };
455
+
456
+ for (const entity of entities) {
457
+ if (entity.path) files.push(entity.path);
458
+
459
+ const layer = entity.layer || 'unknown';
460
+ if (!structure[category].layers[layer]) {
461
+ structure[category].layers[layer] = 0;
462
+ }
463
+ structure[category].layers[layer]++;
464
+ }
465
+
466
+ // Detect patterns from category
467
+ if (entities.length > 5) {
468
+ patterns.push({
469
+ name: `${category}-convention`,
470
+ description: `${entities.length} ${category} entities follow consistent structure`,
471
+ count: entities.length,
472
+ });
473
+ }
474
+ }
475
+
476
+ return { files, structure, patterns };
477
+ }
478
+
479
+ async getProjectStats(_options = {}) {
480
+ this._ensureLoaded();
481
+ if (!this._byCategory || !this._byPath) return null;
482
+
483
+ const languages = {};
484
+ const layerCounts = { L1: 0, L2: 0, L3: 0, L4: 0 };
485
+
486
+ for (const [_path, entity] of this._byPath) {
487
+ // Count by layer
488
+ if (entity.layer && layerCounts[entity.layer] !== undefined) {
489
+ layerCounts[entity.layer]++;
490
+ }
491
+
492
+ // Detect language from file extension
493
+ const ext = path.extname(entity.path || '').slice(1);
494
+ if (ext) {
495
+ languages[ext] = (languages[ext] || 0) + 1;
496
+ }
497
+ }
498
+
499
+ return {
500
+ files: this._byPath.size,
501
+ lines: 0, // Cannot determine without reading files
502
+ languages,
503
+ layers: layerCounts,
504
+ categories: this._byCategory.size,
505
+ totalEntities: this._byName
506
+ ? Array.from(this._byName.values()).reduce((sum, arr) => sum + arr.length, 0)
507
+ : 0,
508
+ };
509
+ }
510
+
511
+ // --- 3 AST-only primitives inherit null from base class ---
512
+ // findCallers, findCallees, analyzeComplexity → return null (base class default)
513
+ }
514
+
515
+ module.exports = { RegistryProvider, LAYER_PRIORITY };
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Doctor Check: Code Intelligence
3
3
  *
4
- * Reads code-intel provider status. Returns INFO (not FAIL) if not configured.
4
+ * Validates code-intel provider status by actually testing provider detection.
5
+ * Checks: module exists → registry-provider available → primitives work.
5
6
  *
6
7
  * @module aios-core/doctor/checks/code-intel
7
- * @story INS-4.1
8
+ * @story INS-4.1, CODEINTEL-RP-001
8
9
  */
9
10
 
10
11
  const path = require('path');
@@ -15,6 +16,7 @@ const name = 'code-intel';
15
16
  async function run(context) {
16
17
  const codeIntelDir = path.join(context.projectRoot, '.aios-core', 'core', 'code-intel');
17
18
 
19
+ // Check 1: Module exists
18
20
  if (!fs.existsSync(codeIntelDir)) {
19
21
  return {
20
22
  check: name,
@@ -24,34 +26,106 @@ async function run(context) {
24
26
  };
25
27
  }
26
28
 
27
- const configPath = path.join(context.projectRoot, '.aios-core', 'core-config.yaml');
28
- if (!fs.existsSync(configPath)) {
29
+ // Check 2: Try to load and detect provider
30
+ try {
31
+ const indexPath = path.join(codeIntelDir, 'index.js');
32
+ if (!fs.existsSync(indexPath)) {
33
+ return {
34
+ check: name,
35
+ status: 'WARN',
36
+ message: 'Code-intel index.js not found',
37
+ fixCommand: null,
38
+ };
39
+ }
40
+
41
+ // Clear require cache to get fresh state
42
+ const resolvedIndex = require.resolve(indexPath);
43
+ delete require.cache[resolvedIndex];
44
+
45
+ const { getClient, isCodeIntelAvailable, _resetForTesting } = require(indexPath);
46
+
47
+ // Reset singleton to test fresh detection
48
+ _resetForTesting();
49
+
50
+ // Initialize client (triggers provider auto-detection)
51
+ const client = getClient({ projectRoot: context.projectRoot });
52
+ const available = isCodeIntelAvailable();
53
+ const metrics = client.getMetrics();
54
+
55
+ // Clean up singleton after test
56
+ _resetForTesting();
57
+
58
+ if (!available) {
59
+ // Check if entity-registry.yaml exists but provider still failed
60
+ const registryPath = path.join(context.projectRoot, '.aios-core', 'data', 'entity-registry.yaml');
61
+ if (fs.existsSync(registryPath)) {
62
+ const stat = fs.statSync(registryPath);
63
+ const sizeKB = Math.round(stat.size / 1024);
64
+ return {
65
+ check: name,
66
+ status: 'WARN',
67
+ message: `Registry exists (${sizeKB}KB) but provider detection failed — may be empty or malformed`,
68
+ fixCommand: 'node .aios-core/development/scripts/populate-entity-registry.js',
69
+ };
70
+ }
71
+
72
+ return {
73
+ check: name,
74
+ status: 'INFO',
75
+ message: 'No provider available (no registry, no MCP) — graceful fallback active',
76
+ fixCommand: 'node .aios-core/development/scripts/populate-entity-registry.js',
77
+ };
78
+ }
79
+
80
+ // Provider is available — report details
81
+ const provider = metrics.activeProvider;
82
+ const cbState = metrics.circuitBreakerState;
83
+
84
+ if (provider === 'registry') {
85
+ // Read entity count from registry metadata
86
+ const registryPath = path.join(context.projectRoot, '.aios-core', 'data', 'entity-registry.yaml');
87
+ let entityInfo = '';
88
+ if (fs.existsSync(registryPath)) {
89
+ const content = fs.readFileSync(registryPath, 'utf8');
90
+ const sizeKB = Math.round(fs.statSync(registryPath).size / 1024);
91
+ // Extract entityCount from metadata header (avoids full YAML parse)
92
+ const countMatch = content.match(/entityCount:\s*(\d+)/);
93
+ const entityCount = countMatch ? countMatch[1] : '?';
94
+ entityInfo = `, ${entityCount} entities, ${sizeKB}KB`;
95
+ }
96
+
97
+ return {
98
+ check: name,
99
+ status: 'PASS',
100
+ message: `RegistryProvider (T1) active, 5/8 primitives${entityInfo}, CB: ${cbState}`,
101
+ fixCommand: null,
102
+ };
103
+ }
104
+
105
+ if (provider === 'code-graph') {
106
+ return {
107
+ check: name,
108
+ status: 'PASS',
109
+ message: `CodeGraphProvider (T3/MCP) active, 8/8 primitives, CB: ${cbState}`,
110
+ fixCommand: null,
111
+ };
112
+ }
113
+
114
+ // Unknown provider (custom)
29
115
  return {
30
116
  check: name,
31
- status: 'INFO',
32
- message: 'Provider not configured (graceful fallback active)',
117
+ status: 'PASS',
118
+ message: `Provider '${provider}' active, CB: ${cbState}`,
33
119
  fixCommand: null,
34
120
  };
35
- }
36
-
37
- const content = fs.readFileSync(configPath, 'utf8');
38
- const hasCodeIntel = content.includes('codeIntel:') || content.includes('code_intel:');
39
-
40
- if (hasCodeIntel) {
121
+ } catch (error) {
41
122
  return {
42
123
  check: name,
43
- status: 'PASS',
44
- message: 'Code-intel provider configured',
124
+ status: 'WARN',
125
+ message: `Provider detection error: ${error.message}`,
45
126
  fixCommand: null,
46
127
  };
47
128
  }
48
-
49
- return {
50
- check: name,
51
- status: 'INFO',
52
- message: 'Provider not configured (graceful fallback active)',
53
- fixCommand: null,
54
- };
55
129
  }
56
130
 
57
131
  module.exports = { name, run };
@@ -61,10 +61,21 @@ async function run(context) {
61
61
  try {
62
62
  const settingsLocal = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8'));
63
63
  const hooks = settingsLocal.hooks || {};
64
- const allHookCommands = Object.values(hooks).flat().map((h) => {
65
- if (typeof h === 'string') return h;
66
- return h.command || h.matcher || '';
67
- });
64
+ // Claude Code hooks schema: { EventName: [{ matcher, hooks: [{ type, command }] }] }
65
+ const allHookCommands = [];
66
+ for (const entries of Object.values(hooks)) {
67
+ if (!Array.isArray(entries)) continue;
68
+ for (const entry of entries) {
69
+ if (entry && Array.isArray(entry.hooks)) {
70
+ for (const h of entry.hooks) {
71
+ if (h && h.command) allHookCommands.push(h.command);
72
+ }
73
+ }
74
+ // Fallback: flat string or direct command
75
+ if (typeof entry === 'string') allHookCommands.push(entry);
76
+ if (entry && typeof entry.command === 'string') allHookCommands.push(entry.command);
77
+ }
78
+ }
68
79
  const hooksStr = allHookCommands.join('\n');
69
80
 
70
81
  // Check if at least some hook files are referenced in settings