@cleocode/core 2026.4.39 → 2026.4.41

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.
package/src/init.ts CHANGED
@@ -832,65 +832,7 @@ export async function initProject(opts: InitOptions = {}): Promise<InitResult> {
832
832
  // This gives the CANT bridge a working team topology on first `cleoos` run.
833
833
  // Only deploys if .cleo/cant/ does not already contain .cant files (idempotent).
834
834
  try {
835
- const cantDir = join(cleoDir, 'cant');
836
- const cantAgentsDir = join(cantDir, 'agents');
837
- const hasCantFiles =
838
- existsSync(cantDir) &&
839
- readdirSync(cantDir, { recursive: true }).some(
840
- (f) => typeof f === 'string' && f.endsWith('.cant'),
841
- );
842
-
843
- if (!hasCantFiles) {
844
- // Resolve the starter-bundle from @cleocode/cleo-os package
845
- let starterBundleSrc: string | null = null;
846
- try {
847
- const { createRequire } = await import('node:module');
848
- const req = createRequire(import.meta.url);
849
- const cleoOsPkgMain = req.resolve('@cleocode/cleo-os/package.json');
850
- const cleoOsPkgRoot = dirname(cleoOsPkgMain);
851
- const candidate = join(cleoOsPkgRoot, 'starter-bundle');
852
- if (existsSync(candidate)) {
853
- starterBundleSrc = candidate;
854
- }
855
- } catch {
856
- // Not resolvable via require.resolve — try workspace fallbacks
857
- }
858
-
859
- if (!starterBundleSrc) {
860
- const packageRoot = getPackageRoot();
861
- const fallbacks = [
862
- join(packageRoot, '..', 'cleo-os', 'starter-bundle'),
863
- join(packageRoot, '..', '..', 'packages', 'cleo-os', 'starter-bundle'),
864
- ];
865
- starterBundleSrc = fallbacks.find((p) => existsSync(p)) ?? null;
866
- }
867
-
868
- if (starterBundleSrc) {
869
- await mkdir(cantDir, { recursive: true });
870
- await mkdir(cantAgentsDir, { recursive: true });
871
-
872
- // Copy team.cant
873
- const teamSrc = join(starterBundleSrc, 'team.cant');
874
- const teamDst = join(cantDir, 'team.cant');
875
- if (existsSync(teamSrc) && !existsSync(teamDst)) {
876
- await copyFile(teamSrc, teamDst);
877
- }
878
-
879
- // Copy agent .cant files
880
- const agentsSrc = join(starterBundleSrc, 'agents');
881
- if (existsSync(agentsSrc)) {
882
- const agentFiles = readdirSync(agentsSrc).filter((f) => f.endsWith('.cant'));
883
- for (const agentFile of agentFiles) {
884
- const dst = join(cantAgentsDir, agentFile);
885
- if (!existsSync(dst)) {
886
- await copyFile(join(agentsSrc, agentFile), dst);
887
- }
888
- }
889
- }
890
-
891
- created.push('starter-bundle: team + agent .cant files deployed to .cleo/cant/');
892
- }
893
- }
835
+ await deployStarterBundle(cleoDir, created, warnings);
894
836
  } catch (err) {
895
837
  warnings.push(`Starter bundle deploy: ${err instanceof Error ? err.message : String(err)}`);
896
838
  }
@@ -1068,3 +1010,92 @@ export async function getVersion(projectRoot?: string): Promise<{ version: strin
1068
1010
 
1069
1011
  return { version: '0.0.0' };
1070
1012
  }
1013
+
1014
+ // ---------------------------------------------------------------------------
1015
+ // Starter bundle deployment (T441) — shared between init and upgrade
1016
+ // ---------------------------------------------------------------------------
1017
+
1018
+ /**
1019
+ * Deploy the starter CANT bundle (team + agents) to a project's `.cleo/cant/`.
1020
+ *
1021
+ * Idempotent: skips deployment if `.cleo/cant/` already contains `.cant` files.
1022
+ * Does not overwrite existing files. Resolves the starter bundle from
1023
+ * `@cleocode/cleo-os/starter-bundle` or workspace fallback paths.
1024
+ *
1025
+ * Called by both `initProject()` and `runUpgrade()` to ensure every project
1026
+ * gets a working team topology for the CANT bridge.
1027
+ *
1028
+ * @param cleoDir - Absolute path to the project's `.cleo/` directory.
1029
+ * @param created - Array to push created-file descriptions into.
1030
+ * @param warnings - Array to push warning messages into.
1031
+ */
1032
+ export async function deployStarterBundle(
1033
+ cleoDir: string,
1034
+ created: string[],
1035
+ warnings: string[],
1036
+ ): Promise<void> {
1037
+ const cantDir = join(cleoDir, 'cant');
1038
+ const cantAgentsDir = join(cantDir, 'agents');
1039
+ const hasCantFiles =
1040
+ existsSync(cantDir) &&
1041
+ readdirSync(cantDir, { recursive: true }).some(
1042
+ (f) => typeof f === 'string' && f.endsWith('.cant'),
1043
+ );
1044
+
1045
+ if (hasCantFiles) return; // Already deployed — idempotent
1046
+
1047
+ // Resolve the starter-bundle from @cleocode/cleo-os package
1048
+ let starterBundleSrc: string | null = null;
1049
+ try {
1050
+ const { createRequire } = await import('node:module');
1051
+ const req = createRequire(import.meta.url);
1052
+ const cleoOsPkgMain = req.resolve('@cleocode/cleo-os/package.json');
1053
+ const cleoOsPkgRoot = dirname(cleoOsPkgMain);
1054
+ const candidate = join(cleoOsPkgRoot, 'starter-bundle');
1055
+ if (existsSync(candidate)) {
1056
+ starterBundleSrc = candidate;
1057
+ }
1058
+ } catch {
1059
+ // Not resolvable via require.resolve — try workspace fallbacks
1060
+ }
1061
+
1062
+ if (!starterBundleSrc) {
1063
+ const packageRoot = getPackageRoot();
1064
+ const fallbacks = [
1065
+ join(packageRoot, '..', 'cleo-os', 'starter-bundle'),
1066
+ join(packageRoot, '..', '..', 'packages', 'cleo-os', 'starter-bundle'),
1067
+ ];
1068
+ starterBundleSrc = fallbacks.find((p) => existsSync(p)) ?? null;
1069
+ }
1070
+
1071
+ if (!starterBundleSrc) {
1072
+ warnings.push(
1073
+ 'Starter bundle not found — .cleo/cant/ will remain empty. Run cleo init in a project with @cleocode/cleo-os installed.',
1074
+ );
1075
+ return;
1076
+ }
1077
+
1078
+ await mkdir(cantDir, { recursive: true });
1079
+ await mkdir(cantAgentsDir, { recursive: true });
1080
+
1081
+ // Copy team.cant
1082
+ const teamSrc = join(starterBundleSrc, 'team.cant');
1083
+ const teamDst = join(cantDir, 'team.cant');
1084
+ if (existsSync(teamSrc) && !existsSync(teamDst)) {
1085
+ await copyFile(teamSrc, teamDst);
1086
+ }
1087
+
1088
+ // Copy agent .cant files
1089
+ const agentsSrc = join(starterBundleSrc, 'agents');
1090
+ if (existsSync(agentsSrc)) {
1091
+ const agentFiles = readdirSync(agentsSrc).filter((f) => f.endsWith('.cant'));
1092
+ for (const agentFile of agentFiles) {
1093
+ const dst = join(cantAgentsDir, agentFile);
1094
+ if (!existsSync(dst)) {
1095
+ await copyFile(join(agentsSrc, agentFile), dst);
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ created.push('starter-bundle: team + agent .cant files deployed to .cleo/cant/');
1101
+ }
package/src/internal.ts CHANGED
@@ -897,6 +897,13 @@ export type {
897
897
  BrainSearchHit,
898
898
  BrainTimelineNeighborRow,
899
899
  } from './memory/brain-row-types.js';
900
+ export {
901
+ autoLinkMemories,
902
+ linkMemoryToCode,
903
+ listCodeLinks,
904
+ queryCodeForMemory,
905
+ queryMemoriesForCode,
906
+ } from './memory/graph-memory-bridge.js';
900
907
  // Memory — LLM extraction gate (additional)
901
908
  export type {
902
909
  ExtractedMemory,
@@ -12,7 +12,22 @@
12
12
  import { mkdir, mkdtemp, rm } from 'node:fs/promises';
13
13
  import { tmpdir } from 'node:os';
14
14
  import { join } from 'node:path';
15
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
16
+
17
+ // Force-pass-through the real modules so that any leaked mocks from other
18
+ // test files in the same vitest shard cannot pollute this integration test.
19
+ // vitest resolves mocks at file-load time; vi.unmock is not sufficient when
20
+ // another file's vi.mock('../../paths.js') already poisoned the module registry.
21
+ vi.mock('../../paths.js', async () => await vi.importActual('../../paths.js'));
22
+ vi.mock(
23
+ '../../store/brain-sqlite.js',
24
+ async () => await vi.importActual('../../store/brain-sqlite.js'),
25
+ );
26
+ vi.mock(
27
+ '../../store/nexus-sqlite.js',
28
+ async () => await vi.importActual('../../store/nexus-sqlite.js'),
29
+ );
30
+ vi.mock('../../config.js', async () => await vi.importActual('../../config.js'));
16
31
 
17
32
  let tempDir: string;
18
33
 
@@ -21,6 +36,9 @@ describe('graph-memory-bridge', () => {
21
36
  tempDir = await mkdtemp(join(tmpdir(), 'cleo-gmb-'));
22
37
  await mkdir(join(tempDir, '.cleo'), { recursive: true });
23
38
  process.env['CLEO_DIR'] = join(tempDir, '.cleo');
39
+ // nexus.db is global (ADR-036) — point CLEO_HOME to temp dir so
40
+ // getNexusDb() creates it here instead of ~/.cleo/ on CI.
41
+ process.env['CLEO_HOME'] = join(tempDir, '.cleo');
24
42
  });
25
43
 
26
44
  afterEach(async () => {
@@ -29,6 +47,7 @@ describe('graph-memory-bridge', () => {
29
47
  closeBrainDb();
30
48
  resetNexusDbState();
31
49
  delete process.env['CLEO_DIR'];
50
+ delete process.env['CLEO_HOME'];
32
51
  await rm(tempDir, { recursive: true, force: true });
33
52
  });
34
53
 
@@ -26,12 +26,14 @@ const {
26
26
  mockStoreLearning,
27
27
  mockStorePattern,
28
28
  mockLoadConfig,
29
+ mockResolveKey,
29
30
  } = vi.hoisted(() => ({
30
31
  mockGetBrainDb: vi.fn().mockResolvedValue({}),
31
32
  mockGetBrainNativeDb: vi.fn(),
32
33
  mockStoreLearning: vi.fn().mockResolvedValue({ id: 'L-test-001' }),
33
34
  mockStorePattern: vi.fn().mockResolvedValue({ id: 'P-test-001' }),
34
35
  mockLoadConfig: vi.fn(),
36
+ mockResolveKey: vi.fn().mockReturnValue(null),
35
37
  }));
36
38
 
37
39
  vi.mock('../../store/brain-sqlite.js', () => ({
@@ -56,6 +58,13 @@ vi.mock('../../config.js', () => ({
56
58
  loadConfig: mockLoadConfig,
57
59
  }));
58
60
 
61
+ // Mock the key resolver so tests don't depend on filesystem state
62
+ // (~/.claude/.credentials.json, ~/.local/share/cleo/anthropic-key).
63
+ vi.mock('../anthropic-key-resolver.js', () => ({
64
+ resolveAnthropicApiKey: (...args: unknown[]) => mockResolveKey(...args),
65
+ clearAnthropicKeyCache: vi.fn(),
66
+ }));
67
+
59
68
  // ============================================================================
60
69
  // Import module under test (after all mocks)
61
70
  // ============================================================================
@@ -69,11 +78,7 @@ import { runObserver, runReflector } from '../observer-reflector.js';
69
78
  const FAKE_API_KEY = 'sk-ant-test-key';
70
79
 
71
80
  function setApiKey(key: string | undefined): void {
72
- if (key === undefined) {
73
- delete process.env['ANTHROPIC_API_KEY'];
74
- } else {
75
- process.env['ANTHROPIC_API_KEY'] = key;
76
- }
81
+ mockResolveKey.mockReturnValue(key ?? null);
77
82
  }
78
83
 
79
84
  type RawObs = {
@@ -171,7 +171,11 @@ function buildMockNativeDb(options: {
171
171
  return { run: mockRun, all: mockAll, get: vi.fn().mockReturnValue({ cnt: 0 }) };
172
172
  });
173
173
 
174
- const stmtMock = { run: mockRun, all: vi.fn().mockReturnValue([]), get: vi.fn().mockReturnValue({ cnt: 0 }) };
174
+ const stmtMock = {
175
+ run: mockRun,
176
+ all: vi.fn().mockReturnValue([]),
177
+ get: vi.fn().mockReturnValue({ cnt: 0 }),
178
+ };
175
179
  return { prepare, _stmtMock: stmtMock };
176
180
  }
177
181
 
package/src/upgrade.ts CHANGED
@@ -855,6 +855,21 @@ export async function runUpgrade(
855
855
  /* best-effort — signaldock.db will be created on first agent operation */
856
856
  }
857
857
 
858
+ // Initialize conduit.db for project-tier agent messaging (T310)
859
+ try {
860
+ const { ensureConduitDb } = await import('./store/conduit-sqlite.js');
861
+ const cdResult = ensureConduitDb(projectRootForMaint);
862
+ if (cdResult.action === 'created') {
863
+ actions.push({
864
+ action: 'ensure_conduit_db',
865
+ status: 'applied',
866
+ details: 'conduit.db created with full schema',
867
+ });
868
+ }
869
+ } catch {
870
+ /* best-effort — conduit.db will be created on first agent operation */
871
+ }
872
+
858
873
  // Regenerate memory-bridge.md
859
874
  try {
860
875
  const { writeMemoryBridge } = await import('./memory/memory-bridge.js');
@@ -900,7 +915,30 @@ export async function runUpgrade(
900
915
  /* best-effort */
901
916
  }
902
917
 
903
- // (Step skipped CLI dispatch only)
918
+ // Deploy starter CANT bundle (team + agents) to .cleo/cant/ if missing.
919
+ // Ensures existing projects get agent definitions on upgrade, not just init. (T555)
920
+ try {
921
+ const { deployStarterBundle } = await import('./init.js');
922
+ const cantCreated: string[] = [];
923
+ const cantWarnings: string[] = [];
924
+ await deployStarterBundle(cleoDir, cantCreated, cantWarnings);
925
+ if (cantCreated.length > 0) {
926
+ actions.push({
927
+ action: 'cant_starter_bundle',
928
+ status: 'applied',
929
+ details: cantCreated.join(', '),
930
+ });
931
+ }
932
+ for (const w of cantWarnings) {
933
+ actions.push({
934
+ action: 'cant_starter_bundle',
935
+ status: 'skipped',
936
+ details: w,
937
+ });
938
+ }
939
+ } catch {
940
+ /* best-effort */
941
+ }
904
942
 
905
943
  // Install core skills
906
944
  try {
@@ -992,6 +1030,51 @@ export async function runUpgrade(
992
1030
  } catch {
993
1031
  /* best-effort */
994
1032
  }
1033
+
1034
+ // Adapter discovery, activation, and install (T5240)
1035
+ // Ensures Claude Code settings.json hooks and other adapter configs stay current.
1036
+ try {
1037
+ const { AdapterManager } = await import('./adapters/index.js');
1038
+ const mgr = AdapterManager.getInstance(projectRootForMaint);
1039
+ const manifests = mgr.discover();
1040
+ if (manifests.length > 0) {
1041
+ const detected = mgr.detectActive();
1042
+ for (const adapterId of detected) {
1043
+ try {
1044
+ const adapter = await mgr.activate(adapterId);
1045
+ const installResult = await adapter.install.install({
1046
+ projectDir: projectRootForMaint,
1047
+ });
1048
+ if (installResult.success) {
1049
+ actions.push({
1050
+ action: 'adapter_install',
1051
+ status: 'applied',
1052
+ details: `Adapter ${adapterId}: installed/updated`,
1053
+ });
1054
+ }
1055
+ } catch {
1056
+ /* best-effort — adapter may not support install */
1057
+ }
1058
+ }
1059
+ }
1060
+ } catch {
1061
+ /* best-effort — adapters are optional */
1062
+ }
1063
+
1064
+ // Ensure the global CleoOS Hub exists (idempotent)
1065
+ try {
1066
+ const { ensureCleoOsHub } = await import('./scaffold.js');
1067
+ const hubResult = await ensureCleoOsHub();
1068
+ if (hubResult.action === 'created') {
1069
+ actions.push({
1070
+ action: 'cleoos_hub',
1071
+ status: 'applied',
1072
+ details: hubResult.details ?? 'CleoOS hub scaffolded',
1073
+ });
1074
+ }
1075
+ } catch {
1076
+ /* best-effort */
1077
+ }
995
1078
  } else {
996
1079
  // Dry-run reporting for new steps
997
1080
  const { existsSync: fsExistsSync } = await import('node:fs');
@@ -1268,6 +1351,66 @@ export async function diagnoseUpgrade(options: { cwd?: string } = {}): Promise<D
1268
1351
  fix: 'Run: cleo upgrade',
1269
1352
  });
1270
1353
  }
1354
+
1355
+ // T528/T531/T549 column validation — ensure brain schema expansions applied
1356
+ const brainColumnChecks: Array<{ table: string; columns: string[]; task: string }> = [
1357
+ {
1358
+ table: 'brain_page_nodes',
1359
+ columns: ['quality_score', 'content_hash', 'last_activity_at', 'updated_at'],
1360
+ task: 'T528',
1361
+ },
1362
+ {
1363
+ table: 'brain_decisions',
1364
+ columns: [
1365
+ 'quality_score',
1366
+ 'memory_tier',
1367
+ 'memory_type',
1368
+ 'verified',
1369
+ 'valid_at',
1370
+ 'invalid_at',
1371
+ 'source_confidence',
1372
+ 'citation_count',
1373
+ ],
1374
+ task: 'T531/T549',
1375
+ },
1376
+ {
1377
+ table: 'brain_observations',
1378
+ columns: ['quality_score', 'memory_tier', 'memory_type', 'verified'],
1379
+ task: 'T531/T549',
1380
+ },
1381
+ ];
1382
+
1383
+ const allBrainColsMissing: string[] = [];
1384
+ for (const { table, columns, task } of brainColumnChecks) {
1385
+ const hasTable = nativeDb
1386
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
1387
+ .get(table) as { name?: string } | undefined;
1388
+ if (!hasTable?.name) continue;
1389
+
1390
+ const cols = nativeDb.prepare(`PRAGMA table_info(${table})`).all() as Array<{
1391
+ name: string;
1392
+ }>;
1393
+ const colNames = new Set(cols.map((c: { name: string }) => c.name));
1394
+ const missing = columns.filter((c) => !colNames.has(c));
1395
+ if (missing.length > 0) {
1396
+ allBrainColsMissing.push(`${table}: ${missing.join(', ')} (${task})`);
1397
+ }
1398
+ }
1399
+
1400
+ if (allBrainColsMissing.length > 0) {
1401
+ findings.push({
1402
+ check: 'brain.db.schema_columns',
1403
+ status: 'error',
1404
+ details: `Missing brain schema columns: ${allBrainColsMissing.join('; ')}`,
1405
+ fix: 'Run: cleo upgrade (will add missing columns automatically)',
1406
+ });
1407
+ } else {
1408
+ findings.push({
1409
+ check: 'brain.db.schema_columns',
1410
+ status: 'ok',
1411
+ details: 'All T528/T531/T549 brain schema columns present',
1412
+ });
1413
+ }
1271
1414
  } else {
1272
1415
  findings.push({
1273
1416
  check: 'brain.db.connection',
@@ -1287,6 +1430,85 @@ export async function diagnoseUpgrade(options: { cwd?: string } = {}): Promise<D
1287
1430
  check: 'brain.db',
1288
1431
  status: 'warning',
1289
1432
  details: 'brain.db not found (will be created on first use)',
1433
+ fix: 'Run: cleo upgrade',
1434
+ });
1435
+ }
1436
+
1437
+ // ── signaldock.db validation ──
1438
+ const signaldockDbPath = join(cleoDir, 'signaldock.db');
1439
+ if (existsSync(signaldockDbPath)) {
1440
+ findings.push({
1441
+ check: 'signaldock.db',
1442
+ status: 'ok',
1443
+ details: 'signaldock.db exists',
1444
+ });
1445
+ } else {
1446
+ findings.push({
1447
+ check: 'signaldock.db',
1448
+ status: 'warning',
1449
+ details: 'signaldock.db not found',
1450
+ fix: 'Run: cleo upgrade',
1451
+ });
1452
+ }
1453
+
1454
+ // ── conduit.db validation ──
1455
+ const conduitDbPath = join(cleoDir, 'conduit.db');
1456
+ if (existsSync(conduitDbPath)) {
1457
+ findings.push({
1458
+ check: 'conduit.db',
1459
+ status: 'ok',
1460
+ details: 'conduit.db exists',
1461
+ });
1462
+ } else {
1463
+ findings.push({
1464
+ check: 'conduit.db',
1465
+ status: 'warning',
1466
+ details: 'conduit.db not found',
1467
+ fix: 'Run: cleo upgrade',
1468
+ });
1469
+ }
1470
+
1471
+ // ── memory-bridge.md validation ──
1472
+ const memoryBridgePath = join(cleoDir, 'memory-bridge.md');
1473
+ if (existsSync(memoryBridgePath)) {
1474
+ try {
1475
+ const content = readFileSync(memoryBridgePath, 'utf-8');
1476
+ const hasAutoGenMarker = content.includes('Auto-generated');
1477
+ const hasGarbage = content.includes('undefined') && content.length < 100;
1478
+ if (hasGarbage) {
1479
+ findings.push({
1480
+ check: 'memory-bridge.md',
1481
+ status: 'error',
1482
+ details: 'memory-bridge.md contains garbage content',
1483
+ fix: 'Run: cleo upgrade (will regenerate)',
1484
+ });
1485
+ } else if (hasAutoGenMarker) {
1486
+ findings.push({
1487
+ check: 'memory-bridge.md',
1488
+ status: 'ok',
1489
+ details: 'memory-bridge.md exists and has valid content',
1490
+ });
1491
+ } else {
1492
+ findings.push({
1493
+ check: 'memory-bridge.md',
1494
+ status: 'warning',
1495
+ details: 'memory-bridge.md exists but may be stale (no auto-generated marker)',
1496
+ fix: 'Run: cleo upgrade (will regenerate)',
1497
+ });
1498
+ }
1499
+ } catch {
1500
+ findings.push({
1501
+ check: 'memory-bridge.md',
1502
+ status: 'warning',
1503
+ details: 'memory-bridge.md exists but could not be read',
1504
+ });
1505
+ }
1506
+ } else {
1507
+ findings.push({
1508
+ check: 'memory-bridge.md',
1509
+ status: 'warning',
1510
+ details: 'memory-bridge.md not found',
1511
+ fix: 'Run: cleo upgrade (will regenerate)',
1290
1512
  });
1291
1513
  }
1292
1514