@cleocode/adapters 2026.4.38 → 2026.4.40

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.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAI7D;;;;;;;;;;;;;;GAcG;AACH,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,kCAAkC;IAClC,QAAQ,CAAC,EAAE,iBAAiB;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,8BAA8B;IAC9B,QAAQ,CAAC,OAAO,WAAW;IAE3B,+CAA+C;IAC/C,YAAY,EAAE,mBAAmB,CA6B/B;IAEF,8DAA8D;IAC9D,KAAK,EAAE,sBAAsB,CAAC;IAC9B,wEAAwE;IACxE,KAAK,EAAE,uBAAuB,CAAC;IAC/B,+EAA+E;IAC/E,OAAO,EAAE,yBAAyB,CAAC;IACnC,mEAAmE;IACnE,KAAK,EAAE,sBAAsB,CAAC;IAC9B,+EAA+E;IAC/E,cAAc,EAAE,gCAAgC,CAAC;IACjD,wDAAwD;IACxD,SAAS,EAAE,2BAA2B,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,EAAE,0BAA0B,CAAC;IAErC,oEAAoE;IACpE,OAAO,CAAC,UAAU,CAAuB;IACzC,kDAAkD;IAClD,OAAO,CAAC,WAAW,CAAS;;IAY5B;;;;;;;OAOG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B;;;;;;;;;OASG;IACG,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAyCjD;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;CAG/B"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAI7D;;;;;;;;;;;;;;GAcG;AACH,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,kCAAkC;IAClC,QAAQ,CAAC,EAAE,iBAAiB;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,8BAA8B;IAC9B,QAAQ,CAAC,OAAO,WAAW;IAE3B,+CAA+C;IAC/C,YAAY,EAAE,mBAAmB,CA6B/B;IAEF,8DAA8D;IAC9D,KAAK,EAAE,sBAAsB,CAAC;IAC9B,wEAAwE;IACxE,KAAK,EAAE,uBAAuB,CAAC;IAC/B,+EAA+E;IAC/E,OAAO,EAAE,yBAAyB,CAAC;IACnC,mEAAmE;IACnE,KAAK,EAAE,sBAAsB,CAAC;IAC9B,+EAA+E;IAC/E,cAAc,EAAE,gCAAgC,CAAC;IACjD,wDAAwD;IACxD,SAAS,EAAE,2BAA2B,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,EAAE,0BAA0B,CAAC;IAErC,oEAAoE;IACpE,OAAO,CAAC,UAAU,CAAuB;IACzC,kDAAkD;IAClD,OAAO,CAAC,WAAW,CAAS;;IAY5B;;;;;;;OAOG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASnD;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B;;;;;;;;;OASG;IACG,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAyCjD;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;CAG/B"}
@@ -55,33 +55,45 @@ export declare class ClaudeCodeHookProvider implements AdapterHookProvider {
55
55
  * @task T164
56
56
  */
57
57
  mapProviderEvent(providerEvent: string): string | null;
58
+ /** Project directory this hook provider was registered for. */
59
+ private projectDir;
58
60
  /**
59
61
  * Register native hooks for a project.
60
62
  *
61
- * For Claude Code, hooks are registered via the config system
62
- * (`~/.claude/settings.json`), managed by the install provider.
63
- * This method marks hooks as registered without performing filesystem operations.
63
+ * Writes CLEO hook entries to `~/.claude/settings.json` so that Claude Code's
64
+ * native event system calls cleo CLI commands when events fire. This bridges
65
+ * Claude Code's event loop to CLEO's internal hook dispatch.
64
66
  *
65
- * Iterating supported events is handled at install time using
66
- * `getSupportedCanonicalEvents()` to enumerate all 14 supported hooks.
67
+ * Idempotent: skips writing if CLEO hooks already exist in settings.json.
67
68
  *
68
- * @param _projectDir - Project directory (unused; Claude Code uses global config)
69
- * @task T164
69
+ * Hook entries registered:
70
+ * - `Stop` → `cleo session end --quiet` (triggers LLM extraction, reflector, consolidation)
71
+ * - `PostToolUse` (Write|Edit) → brain observation for file modifications
72
+ * - `SubagentStop` → brain observation for agent completion
73
+ *
74
+ * @param projectDir - Project directory for context-scoped hook commands
75
+ * @task T164 @task T555
70
76
  */
71
- registerNativeHooks(_projectDir: string): Promise<void>;
77
+ registerNativeHooks(projectDir: string): Promise<void>;
72
78
  /**
73
79
  * Unregister native hooks.
74
80
  *
75
- * For Claude Code, this is a no-op since hooks are managed through the config
76
- * system. Unregistration happens via the install provider's uninstall method.
81
+ * Removes CLEO hook entries from `~/.claude/settings.json` by filtering out
82
+ * entries containing the `# cleo-hook` marker.
77
83
  *
78
- * @task T164
84
+ * @task T164 @task T555
79
85
  */
80
86
  unregisterNativeHooks(): Promise<void>;
81
87
  /**
82
88
  * Check whether hooks have been registered via `registerNativeHooks`.
83
89
  */
84
90
  isRegistered(): boolean;
91
+ /**
92
+ * Get the project directory this hook provider was registered for.
93
+ *
94
+ * Returns null if hooks have not been registered yet.
95
+ */
96
+ getProjectDir(): string | null;
85
97
  /**
86
98
  * Get the native→canonical event mapping for introspection and debugging.
87
99
  *
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AA8C/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,kEAAkE;IAClE,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAItD;;;;;;;;;;;;OAYG;IACG,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7D;;;;;;;OAOG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5C;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;;;;;;;OAQG;IACH,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI/C;;;;;;;;;;OAUG;IACG,2BAA2B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAStD;;;;;;;;;;OAUG;IACG,kBAAkB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IASnD;;;;;;;;;;OAUG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAW9D;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CA2DrF"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AA8C/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,kEAAkE;IAClE,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAItD,+DAA+D;IAC/D,OAAO,CAAC,UAAU,CAAuB;IAEzC;;;;;;;;;;;;;;;;OAgBG;IACG,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuE5D;;;;;;;OAOG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C5C;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;;;OAIG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B;;;;;;;;OAQG;IACH,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI/C;;;;;;;;;;OAUG;IACG,2BAA2B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAStD;;;;;;;;;;OAUG;IACG,kBAAkB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IASnD;;;;;;;;;;OAUG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAW9D;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CA2DrF"}
@@ -1 +1 @@
1
- {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAY3F;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IAClE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAuExD;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB3C;;;;;;;OAOG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWnD"}
1
+ {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAY3F;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IAClE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAqFxD;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB3C;;;;;;;OAOG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/opencode/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiB3F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAY5F;AA0CD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qBAAsB,YAAW,oBAAoB;IAChE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;;OASG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAiExD;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB3C;;;;;;;OAOG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWnD"}
1
+ {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/opencode/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiB3F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAY5F;AA+CD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qBAAsB,YAAW,oBAAoB;IAChE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;;OASG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IA+ExD;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB3C;;;;;;;OAOG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWnD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/adapters",
3
- "version": "2026.4.38",
3
+ "version": "2026.4.40",
4
4
  "description": "Unified provider adapters for CLEO (Claude Code, OpenCode, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,8 +12,8 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@cleocode/caamp": "2026.4.38",
16
- "@cleocode/contracts": "2026.4.38"
15
+ "@cleocode/caamp": "2026.4.40",
16
+ "@cleocode/contracts": "2026.4.40"
17
17
  },
18
18
  "license": "MIT",
19
19
  "engines": {
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Unit tests for the shared CANT context builder.
3
+ *
4
+ * Tests cover:
5
+ * - discoverCantFiles: finds .cant files, handles missing dirs
6
+ * - resolveThreeTierPaths: XDG-compliant paths with env var overrides
7
+ * - discoverCantFilesMultiTier: 3-tier merge with override semantics
8
+ * - readMemoryBridge: reads file, handles missing/empty
9
+ * - buildMemoryBridgeBlock: wraps content in labeled section
10
+ * - buildMentalModelInjection: pure function, numbered list, empty input
11
+ * - buildCantEnrichedPrompt: full pipeline, fallback on failure
12
+ *
13
+ * @task T555
14
+ */
15
+
16
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
20
+
21
+ import {
22
+ buildCantEnrichedPrompt,
23
+ buildMemoryBridgeBlock,
24
+ buildMentalModelInjection,
25
+ discoverCantFiles,
26
+ discoverCantFilesMultiTier,
27
+ readMemoryBridge,
28
+ resolveThreeTierPaths,
29
+ } from '../cant-context.js';
30
+
31
+ let tempDir: string;
32
+
33
+ beforeEach(() => {
34
+ tempDir = join(tmpdir(), `cleo-cant-ctx-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
35
+ mkdirSync(tempDir, { recursive: true });
36
+ });
37
+
38
+ afterEach(() => {
39
+ rmSync(tempDir, { recursive: true, force: true });
40
+ });
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // discoverCantFiles
44
+ // ---------------------------------------------------------------------------
45
+
46
+ describe('discoverCantFiles', () => {
47
+ it('finds .cant files recursively', () => {
48
+ const cantDir = join(tempDir, 'cant');
49
+ mkdirSync(join(cantDir, 'agents'), { recursive: true });
50
+ writeFileSync(join(cantDir, 'team.cant'), 'team: test');
51
+ writeFileSync(join(cantDir, 'agents', 'worker.cant'), 'agent: worker');
52
+ writeFileSync(join(cantDir, 'agents', 'README.md'), 'ignored');
53
+
54
+ const files = discoverCantFiles(cantDir);
55
+ expect(files).toHaveLength(2);
56
+ expect(files.some((f) => f.endsWith('team.cant'))).toBe(true);
57
+ expect(files.some((f) => f.endsWith('worker.cant'))).toBe(true);
58
+ });
59
+
60
+ it('returns empty array for non-existent directory', () => {
61
+ const files = discoverCantFiles(join(tempDir, 'does-not-exist'));
62
+ expect(files).toEqual([]);
63
+ });
64
+ });
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // resolveThreeTierPaths
68
+ // ---------------------------------------------------------------------------
69
+
70
+ describe('resolveThreeTierPaths', () => {
71
+ it('returns project tier pointing to .cleo/cant/', () => {
72
+ const paths = resolveThreeTierPaths('/my/project');
73
+ expect(paths.project).toBe('/my/project/.cleo/cant');
74
+ });
75
+
76
+ it('respects XDG_DATA_HOME for global tier', () => {
77
+ const originalXdg = process.env['XDG_DATA_HOME'];
78
+ process.env['XDG_DATA_HOME'] = '/custom/data';
79
+ try {
80
+ const paths = resolveThreeTierPaths('/my/project');
81
+ expect(paths.global).toBe('/custom/data/cleo/cant');
82
+ } finally {
83
+ if (originalXdg) process.env['XDG_DATA_HOME'] = originalXdg;
84
+ else delete process.env['XDG_DATA_HOME'];
85
+ }
86
+ });
87
+
88
+ it('respects XDG_CONFIG_HOME for user tier', () => {
89
+ const originalXdg = process.env['XDG_CONFIG_HOME'];
90
+ process.env['XDG_CONFIG_HOME'] = '/custom/config';
91
+ try {
92
+ const paths = resolveThreeTierPaths('/my/project');
93
+ expect(paths.user).toBe('/custom/config/cleo/cant');
94
+ } finally {
95
+ if (originalXdg) process.env['XDG_CONFIG_HOME'] = originalXdg;
96
+ else delete process.env['XDG_CONFIG_HOME'];
97
+ }
98
+ });
99
+ });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // discoverCantFilesMultiTier
103
+ // ---------------------------------------------------------------------------
104
+
105
+ describe('discoverCantFilesMultiTier', () => {
106
+ let origXdgData: string | undefined;
107
+ let origXdgConfig: string | undefined;
108
+
109
+ beforeEach(() => {
110
+ // Override XDG paths so global/user tiers point to empty temp subdirs
111
+ origXdgData = process.env['XDG_DATA_HOME'];
112
+ origXdgConfig = process.env['XDG_CONFIG_HOME'];
113
+ process.env['XDG_DATA_HOME'] = join(tempDir, 'xdg-data');
114
+ process.env['XDG_CONFIG_HOME'] = join(tempDir, 'xdg-config');
115
+ });
116
+
117
+ afterEach(() => {
118
+ if (origXdgData) process.env['XDG_DATA_HOME'] = origXdgData;
119
+ else delete process.env['XDG_DATA_HOME'];
120
+ if (origXdgConfig) process.env['XDG_CONFIG_HOME'] = origXdgConfig;
121
+ else delete process.env['XDG_CONFIG_HOME'];
122
+ });
123
+
124
+ it('discovers files from project tier', () => {
125
+ const cantDir = join(tempDir, '.cleo', 'cant');
126
+ mkdirSync(cantDir, { recursive: true });
127
+ writeFileSync(join(cantDir, 'team.cant'), 'team: test');
128
+
129
+ const result = discoverCantFilesMultiTier(tempDir);
130
+ expect(result.files).toHaveLength(1);
131
+ expect(result.stats.project).toBe(1);
132
+ });
133
+
134
+ it('returns empty when no tiers have .cant files', () => {
135
+ const result = discoverCantFilesMultiTier(tempDir);
136
+ expect(result.files).toHaveLength(0);
137
+ expect(result.stats.merged).toBe(0);
138
+ });
139
+ });
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // readMemoryBridge
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe('readMemoryBridge', () => {
146
+ it('returns null when file does not exist', () => {
147
+ expect(readMemoryBridge(tempDir)).toBeNull();
148
+ });
149
+
150
+ it('returns content when file exists', () => {
151
+ const cleoDir = join(tempDir, '.cleo');
152
+ mkdirSync(cleoDir, { recursive: true });
153
+ writeFileSync(join(cleoDir, 'memory-bridge.md'), '# Memory Bridge\nTest content');
154
+
155
+ const result = readMemoryBridge(tempDir);
156
+ expect(result).toContain('Memory Bridge');
157
+ expect(result).toContain('Test content');
158
+ });
159
+
160
+ it('returns null for empty file', () => {
161
+ const cleoDir = join(tempDir, '.cleo');
162
+ mkdirSync(cleoDir, { recursive: true });
163
+ writeFileSync(join(cleoDir, 'memory-bridge.md'), '');
164
+
165
+ expect(readMemoryBridge(tempDir)).toBeNull();
166
+ });
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // buildMemoryBridgeBlock
171
+ // ---------------------------------------------------------------------------
172
+
173
+ describe('buildMemoryBridgeBlock', () => {
174
+ it('wraps content in labeled section markers', () => {
175
+ const result = buildMemoryBridgeBlock('Test content');
176
+ expect(result).toContain('===== CLEO MEMORY BRIDGE =====');
177
+ expect(result).toContain('Test content');
178
+ expect(result).toContain('===== END MEMORY BRIDGE =====');
179
+ });
180
+ });
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // buildMentalModelInjection
184
+ // ---------------------------------------------------------------------------
185
+
186
+ describe('buildMentalModelInjection', () => {
187
+ it('returns empty string for empty observations', () => {
188
+ expect(buildMentalModelInjection('test-agent', [])).toBe('');
189
+ });
190
+
191
+ it('builds numbered list with preamble', () => {
192
+ const result = buildMentalModelInjection('code-worker', [
193
+ { id: 'O-001', type: 'observation', title: 'Tests pass', date: '2026-04-14' },
194
+ { id: 'O-002', type: 'pattern', title: 'Use vitest' },
195
+ ]);
196
+
197
+ expect(result).toContain('MENTAL MODEL (validate-on-load)');
198
+ expect(result).toContain('Agent: code-worker');
199
+ expect(result).toContain('1. [O-001] (observation) [2026-04-14]: Tests pass');
200
+ expect(result).toContain('2. [O-002] (pattern): Use vitest');
201
+ expect(result).toContain('END MENTAL MODEL');
202
+ });
203
+ });
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // buildCantEnrichedPrompt
207
+ // ---------------------------------------------------------------------------
208
+
209
+ describe('buildCantEnrichedPrompt', () => {
210
+ it('returns basePrompt unchanged when no .cant files exist', async () => {
211
+ const result = await buildCantEnrichedPrompt({
212
+ projectDir: tempDir,
213
+ basePrompt: 'Execute the task.',
214
+ });
215
+ expect(result).toBe('Execute the task.');
216
+ });
217
+
218
+ it('appends memory bridge when .cleo/memory-bridge.md exists', async () => {
219
+ const cleoDir = join(tempDir, '.cleo');
220
+ mkdirSync(cleoDir, { recursive: true });
221
+ writeFileSync(join(cleoDir, 'memory-bridge.md'), '# Bridge\nRecent decisions here');
222
+
223
+ const result = await buildCantEnrichedPrompt({
224
+ projectDir: tempDir,
225
+ basePrompt: 'Execute the task.',
226
+ });
227
+
228
+ expect(result).toContain('Execute the task.');
229
+ expect(result).toContain('CLEO MEMORY BRIDGE');
230
+ expect(result).toContain('Recent decisions here');
231
+ });
232
+
233
+ it('includes both memory bridge and base prompt without duplication', async () => {
234
+ const cleoDir = join(tempDir, '.cleo');
235
+ mkdirSync(cleoDir, { recursive: true });
236
+ writeFileSync(join(cleoDir, 'memory-bridge.md'), 'Bridge content');
237
+
238
+ const result = await buildCantEnrichedPrompt({
239
+ projectDir: tempDir,
240
+ basePrompt: 'My prompt',
241
+ });
242
+
243
+ // Base prompt should appear exactly once at the start
244
+ expect(result.startsWith('My prompt')).toBe(true);
245
+ // Should not duplicate the prompt
246
+ expect(result.indexOf('My prompt')).toBe(result.lastIndexOf('My prompt'));
247
+ });
248
+ });