@soleri/core 9.5.0 → 9.7.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 (249) hide show
  1. package/dist/adapters/claude-code-adapter.d.ts +27 -0
  2. package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
  3. package/dist/adapters/claude-code-adapter.js +111 -0
  4. package/dist/adapters/claude-code-adapter.js.map +1 -0
  5. package/dist/adapters/index.d.ts +9 -0
  6. package/dist/adapters/index.d.ts.map +1 -0
  7. package/dist/adapters/index.js +10 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/registry.d.ts +21 -0
  10. package/dist/adapters/registry.d.ts.map +1 -0
  11. package/dist/adapters/registry.js +44 -0
  12. package/dist/adapters/registry.js.map +1 -0
  13. package/dist/adapters/types.d.ts +93 -0
  14. package/dist/adapters/types.d.ts.map +1 -0
  15. package/dist/adapters/types.js +10 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/brain/brain.d.ts +12 -1
  18. package/dist/brain/brain.d.ts.map +1 -1
  19. package/dist/brain/brain.js +106 -44
  20. package/dist/brain/brain.js.map +1 -1
  21. package/dist/brain/intelligence.d.ts.map +1 -1
  22. package/dist/brain/intelligence.js +36 -30
  23. package/dist/brain/intelligence.js.map +1 -1
  24. package/dist/chat/agent-loop.js +1 -1
  25. package/dist/chat/agent-loop.js.map +1 -1
  26. package/dist/chat/notifications.d.ts.map +1 -1
  27. package/dist/chat/notifications.js +4 -0
  28. package/dist/chat/notifications.js.map +1 -1
  29. package/dist/control/intent-router.d.ts +1 -0
  30. package/dist/control/intent-router.d.ts.map +1 -1
  31. package/dist/control/intent-router.js +11 -5
  32. package/dist/control/intent-router.js.map +1 -1
  33. package/dist/curator/curator.d.ts +4 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +141 -27
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/index.d.ts +22 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +18 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/llm/llm-client.d.ts.map +1 -1
  42. package/dist/llm/llm-client.js +1 -0
  43. package/dist/llm/llm-client.js.map +1 -1
  44. package/dist/packs/index.d.ts +3 -2
  45. package/dist/packs/index.d.ts.map +1 -1
  46. package/dist/packs/index.js +3 -2
  47. package/dist/packs/index.js.map +1 -1
  48. package/dist/packs/lockfile.d.ts +23 -1
  49. package/dist/packs/lockfile.d.ts.map +1 -1
  50. package/dist/packs/lockfile.js +50 -4
  51. package/dist/packs/lockfile.js.map +1 -1
  52. package/dist/packs/pack-installer.d.ts +10 -0
  53. package/dist/packs/pack-installer.d.ts.map +1 -1
  54. package/dist/packs/pack-installer.js +69 -2
  55. package/dist/packs/pack-installer.js.map +1 -1
  56. package/dist/packs/pack-lifecycle.d.ts +50 -0
  57. package/dist/packs/pack-lifecycle.d.ts.map +1 -0
  58. package/dist/packs/pack-lifecycle.js +91 -0
  59. package/dist/packs/pack-lifecycle.js.map +1 -0
  60. package/dist/packs/types.d.ts +76 -29
  61. package/dist/packs/types.d.ts.map +1 -1
  62. package/dist/packs/types.js +9 -0
  63. package/dist/packs/types.js.map +1 -1
  64. package/dist/persistence/sqlite-provider.d.ts +5 -1
  65. package/dist/persistence/sqlite-provider.d.ts.map +1 -1
  66. package/dist/persistence/sqlite-provider.js +22 -2
  67. package/dist/persistence/sqlite-provider.js.map +1 -1
  68. package/dist/planning/github-projection.d.ts +11 -9
  69. package/dist/planning/github-projection.d.ts.map +1 -1
  70. package/dist/planning/github-projection.js +47 -43
  71. package/dist/planning/github-projection.js.map +1 -1
  72. package/dist/planning/goal-ancestry.d.ts +72 -0
  73. package/dist/planning/goal-ancestry.d.ts.map +1 -0
  74. package/dist/planning/goal-ancestry.js +137 -0
  75. package/dist/planning/goal-ancestry.js.map +1 -0
  76. package/dist/planning/plan-lifecycle.d.ts +2 -0
  77. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  78. package/dist/planning/plan-lifecycle.js +1 -0
  79. package/dist/planning/plan-lifecycle.js.map +1 -1
  80. package/dist/planning/planner-types.d.ts +2 -0
  81. package/dist/planning/planner-types.d.ts.map +1 -1
  82. package/dist/plugins/types.d.ts +21 -21
  83. package/dist/queue/pipeline-runner.d.ts.map +1 -1
  84. package/dist/queue/pipeline-runner.js +4 -0
  85. package/dist/queue/pipeline-runner.js.map +1 -1
  86. package/dist/runtime/context-health.d.ts +14 -1
  87. package/dist/runtime/context-health.d.ts.map +1 -1
  88. package/dist/runtime/context-health.js +30 -2
  89. package/dist/runtime/context-health.js.map +1 -1
  90. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  91. package/dist/runtime/curator-extra-ops.js +9 -1
  92. package/dist/runtime/curator-extra-ops.js.map +1 -1
  93. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  94. package/dist/runtime/facades/memory-facade.js +169 -0
  95. package/dist/runtime/facades/memory-facade.js.map +1 -1
  96. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  97. package/dist/runtime/orchestrate-ops.js +133 -4
  98. package/dist/runtime/orchestrate-ops.js.map +1 -1
  99. package/dist/runtime/runtime.d.ts.map +1 -1
  100. package/dist/runtime/runtime.js +128 -90
  101. package/dist/runtime/runtime.js.map +1 -1
  102. package/dist/runtime/session-briefing.d.ts.map +1 -1
  103. package/dist/runtime/session-briefing.js +44 -11
  104. package/dist/runtime/session-briefing.js.map +1 -1
  105. package/dist/runtime/shutdown-registry.d.ts +36 -0
  106. package/dist/runtime/shutdown-registry.d.ts.map +1 -0
  107. package/dist/runtime/shutdown-registry.js +74 -0
  108. package/dist/runtime/shutdown-registry.js.map +1 -0
  109. package/dist/runtime/types.d.ts +10 -1
  110. package/dist/runtime/types.d.ts.map +1 -1
  111. package/dist/session/compaction-evaluator.d.ts +20 -0
  112. package/dist/session/compaction-evaluator.d.ts.map +1 -0
  113. package/dist/session/compaction-evaluator.js +73 -0
  114. package/dist/session/compaction-evaluator.js.map +1 -0
  115. package/dist/session/compaction-policy.d.ts +50 -0
  116. package/dist/session/compaction-policy.d.ts.map +1 -0
  117. package/dist/session/compaction-policy.js +17 -0
  118. package/dist/session/compaction-policy.js.map +1 -0
  119. package/dist/session/handoff-renderer.d.ts +22 -0
  120. package/dist/session/handoff-renderer.d.ts.map +1 -0
  121. package/dist/session/handoff-renderer.js +49 -0
  122. package/dist/session/handoff-renderer.js.map +1 -0
  123. package/dist/session/index.d.ts +6 -0
  124. package/dist/session/index.d.ts.map +1 -0
  125. package/dist/session/index.js +5 -0
  126. package/dist/session/index.js.map +1 -0
  127. package/dist/session/policy-resolver.d.ts +20 -0
  128. package/dist/session/policy-resolver.d.ts.map +1 -0
  129. package/dist/session/policy-resolver.js +28 -0
  130. package/dist/session/policy-resolver.js.map +1 -0
  131. package/dist/skills/sync-skills.d.ts +27 -0
  132. package/dist/skills/sync-skills.d.ts.map +1 -1
  133. package/dist/skills/sync-skills.js +92 -1
  134. package/dist/skills/sync-skills.js.map +1 -1
  135. package/dist/skills/trust-classifier.d.ts +32 -0
  136. package/dist/skills/trust-classifier.d.ts.map +1 -0
  137. package/dist/skills/trust-classifier.js +109 -0
  138. package/dist/skills/trust-classifier.js.map +1 -0
  139. package/dist/subagent/concurrency-manager.d.ts +29 -0
  140. package/dist/subagent/concurrency-manager.d.ts.map +1 -0
  141. package/dist/subagent/concurrency-manager.js +73 -0
  142. package/dist/subagent/concurrency-manager.js.map +1 -0
  143. package/dist/subagent/dispatcher.d.ts +45 -0
  144. package/dist/subagent/dispatcher.d.ts.map +1 -0
  145. package/dist/subagent/dispatcher.js +271 -0
  146. package/dist/subagent/dispatcher.js.map +1 -0
  147. package/dist/subagent/index.d.ts +14 -0
  148. package/dist/subagent/index.d.ts.map +1 -0
  149. package/dist/subagent/index.js +15 -0
  150. package/dist/subagent/index.js.map +1 -0
  151. package/dist/subagent/orphan-reaper.d.ts +37 -0
  152. package/dist/subagent/orphan-reaper.d.ts.map +1 -0
  153. package/dist/subagent/orphan-reaper.js +71 -0
  154. package/dist/subagent/orphan-reaper.js.map +1 -0
  155. package/dist/subagent/result-aggregator.d.ts +7 -0
  156. package/dist/subagent/result-aggregator.d.ts.map +1 -0
  157. package/dist/subagent/result-aggregator.js +57 -0
  158. package/dist/subagent/result-aggregator.js.map +1 -0
  159. package/dist/subagent/task-checkout.d.ts +36 -0
  160. package/dist/subagent/task-checkout.d.ts.map +1 -0
  161. package/dist/subagent/task-checkout.js +52 -0
  162. package/dist/subagent/task-checkout.js.map +1 -0
  163. package/dist/subagent/types.d.ts +114 -0
  164. package/dist/subagent/types.d.ts.map +1 -0
  165. package/dist/subagent/types.js +9 -0
  166. package/dist/subagent/types.js.map +1 -0
  167. package/dist/subagent/workspace-resolver.d.ts +35 -0
  168. package/dist/subagent/workspace-resolver.d.ts.map +1 -0
  169. package/dist/subagent/workspace-resolver.js +99 -0
  170. package/dist/subagent/workspace-resolver.js.map +1 -0
  171. package/dist/transport/http-server.d.ts.map +1 -1
  172. package/dist/transport/http-server.js +49 -3
  173. package/dist/transport/http-server.js.map +1 -1
  174. package/dist/transport/ws-server.d.ts.map +1 -1
  175. package/dist/transport/ws-server.js +7 -0
  176. package/dist/transport/ws-server.js.map +1 -1
  177. package/dist/vault/linking.d.ts +3 -4
  178. package/dist/vault/linking.d.ts.map +1 -1
  179. package/dist/vault/linking.js +79 -32
  180. package/dist/vault/linking.js.map +1 -1
  181. package/dist/vault/vault-maintenance.d.ts.map +1 -1
  182. package/dist/vault/vault-maintenance.js +7 -14
  183. package/dist/vault/vault-maintenance.js.map +1 -1
  184. package/dist/vault/vault-memories.d.ts.map +1 -1
  185. package/dist/vault/vault-memories.js +19 -9
  186. package/dist/vault/vault-memories.js.map +1 -1
  187. package/dist/vault/vault-schema.d.ts +1 -0
  188. package/dist/vault/vault-schema.d.ts.map +1 -1
  189. package/dist/vault/vault-schema.js +20 -0
  190. package/dist/vault/vault-schema.js.map +1 -1
  191. package/dist/vault/vault.d.ts.map +1 -1
  192. package/dist/vault/vault.js +7 -3
  193. package/dist/vault/vault.js.map +1 -1
  194. package/package.json +5 -2
  195. package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
  196. package/src/__tests__/adapters/registry.test.ts +100 -0
  197. package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
  198. package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
  199. package/src/__tests__/subagent/dispatcher.test.ts +195 -0
  200. package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
  201. package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
  202. package/src/__tests__/subagent/task-checkout.test.ts +86 -0
  203. package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
  204. package/src/adapters/claude-code-adapter.ts +163 -0
  205. package/src/adapters/index.ts +22 -0
  206. package/src/adapters/registry.ts +53 -0
  207. package/src/adapters/types.ts +114 -0
  208. package/src/curator/curator.ts +1 -0
  209. package/src/index.ts +78 -1
  210. package/src/packs/index.ts +9 -1
  211. package/src/packs/lockfile.ts +70 -5
  212. package/src/packs/pack-installer.ts +78 -2
  213. package/src/packs/pack-lifecycle.ts +115 -0
  214. package/src/packs/pack-lockfile.test.ts +1 -1
  215. package/src/packs/pack-system.test.ts +1 -1
  216. package/src/packs/types.ts +72 -2
  217. package/src/persistence/sqlite-provider.ts +26 -2
  218. package/src/planning/github-projection.ts +6 -0
  219. package/src/planning/goal-ancestry.test.ts +427 -0
  220. package/src/planning/goal-ancestry.ts +187 -0
  221. package/src/planning/plan-lifecycle.ts +3 -0
  222. package/src/planning/planner-types.ts +2 -0
  223. package/src/runtime/admin-setup-ops.test.ts +9 -4
  224. package/src/runtime/context-health.ts +42 -2
  225. package/src/runtime/orchestrate-ops.ts +153 -1
  226. package/src/runtime/runtime.ts +15 -0
  227. package/src/runtime/session-briefing.test.ts +94 -2
  228. package/src/runtime/session-briefing.ts +48 -12
  229. package/src/runtime/types.ts +6 -0
  230. package/src/session/compaction-evaluator.ts +87 -0
  231. package/src/session/compaction-policy.ts +66 -0
  232. package/src/session/compaction.test.ts +259 -0
  233. package/src/session/handoff-renderer.ts +56 -0
  234. package/src/session/index.ts +12 -0
  235. package/src/session/policy-resolver.ts +34 -0
  236. package/src/skills/sync-skills.ts +114 -1
  237. package/src/skills/trust-classifier.test.ts +252 -0
  238. package/src/skills/trust-classifier.ts +127 -0
  239. package/src/subagent/concurrency-manager.ts +89 -0
  240. package/src/subagent/dispatcher.ts +342 -0
  241. package/src/subagent/index.ts +28 -0
  242. package/src/subagent/orphan-reaper.ts +82 -0
  243. package/src/subagent/result-aggregator.ts +66 -0
  244. package/src/subagent/task-checkout.ts +60 -0
  245. package/src/subagent/types.ts +138 -0
  246. package/src/subagent/workspace-resolver.ts +117 -0
  247. package/src/vault/vault-scaling.test.ts +3 -2
  248. package/vitest.config.ts +2 -0
  249. package/src/hooks/index.ts +0 -6
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Session Compaction Policy — types and defaults for session rotation.
3
+ *
4
+ * Three thresholds determine when a session should be compacted:
5
+ * - maxRuns: tool call / interaction count
6
+ * - maxInputTokens: cumulative input token count
7
+ * - maxAge: wall-clock duration (ISO 8601 duration string, e.g. '72h')
8
+ */
9
+
10
+ // =============================================================================
11
+ // TYPES
12
+ // =============================================================================
13
+
14
+ /** Policy thresholds — all optional, merged from three levels. */
15
+ export interface CompactionPolicy {
16
+ /** Maximum number of runs (tool calls / interactions) before compaction. */
17
+ maxRuns?: number;
18
+ /** Maximum cumulative input tokens before compaction. */
19
+ maxInputTokens?: number;
20
+ /** Maximum wall-clock age as an ISO 8601-ish duration string (e.g. '72h', '30m', '7d'). */
21
+ maxAge?: string;
22
+ }
23
+
24
+ /** Result of evaluating whether compaction is needed. */
25
+ export interface CompactionResult {
26
+ /** Whether compaction should happen. */
27
+ compact: boolean;
28
+ /** Human-readable reason (empty string when compact is false). */
29
+ reason: string;
30
+ /** Pre-rendered handoff markdown (empty string when compact is false). */
31
+ handoff: string;
32
+ }
33
+
34
+ /** State snapshot of the current session, used for evaluation. */
35
+ export interface SessionState {
36
+ /** Number of runs (tool calls) so far. */
37
+ runCount: number;
38
+ /** Cumulative input tokens consumed. */
39
+ inputTokens: number;
40
+ /** ISO 8601 timestamp when the session started. */
41
+ startedAt: string;
42
+ }
43
+
44
+ /** Structured handoff note persisted on rotation. */
45
+ export interface HandoffNote {
46
+ /** ISO 8601 timestamp when the session was rotated. */
47
+ rotatedAt: string;
48
+ /** Why the session was rotated. */
49
+ reason: string;
50
+ /** Description of work in progress at rotation time. */
51
+ inProgress: string;
52
+ /** Key decisions made during the session. */
53
+ keyDecisions: string[];
54
+ /** Files modified during the session. */
55
+ filesModified: string[];
56
+ }
57
+
58
+ // =============================================================================
59
+ // ENGINE DEFAULTS
60
+ // =============================================================================
61
+
62
+ export const ENGINE_DEFAULTS: Required<CompactionPolicy> = {
63
+ maxRuns: 200,
64
+ maxInputTokens: 2_000_000,
65
+ maxAge: '72h',
66
+ };
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Session Compaction — colocated contract tests.
3
+ *
4
+ * Tests for:
5
+ * - CompactionEvaluator (shouldCompact)
6
+ * - PolicyResolver (resolvePolicy)
7
+ * - HandoffRenderer (renderHandoff)
8
+ * - Duration parser (parseDuration)
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import { shouldCompact, parseDuration } from './compaction-evaluator.js';
13
+ import { resolvePolicy } from './policy-resolver.js';
14
+ import { renderHandoff } from './handoff-renderer.js';
15
+ import { ENGINE_DEFAULTS } from './compaction-policy.js';
16
+ import type { SessionState, HandoffNote } from './compaction-policy.js';
17
+
18
+ // =============================================================================
19
+ // parseDuration
20
+ // =============================================================================
21
+
22
+ describe('parseDuration', () => {
23
+ it('parses hours', () => {
24
+ expect(parseDuration('72h')).toBe(72 * 3_600_000);
25
+ });
26
+
27
+ it('parses minutes', () => {
28
+ expect(parseDuration('30m')).toBe(30 * 60_000);
29
+ });
30
+
31
+ it('parses days', () => {
32
+ expect(parseDuration('7d')).toBe(7 * 86_400_000);
33
+ });
34
+
35
+ it('parses seconds', () => {
36
+ expect(parseDuration('120s')).toBe(120_000);
37
+ });
38
+
39
+ it('parses milliseconds', () => {
40
+ expect(parseDuration('500ms')).toBe(500);
41
+ });
42
+
43
+ it('returns undefined for invalid input', () => {
44
+ expect(parseDuration('invalid')).toBeUndefined();
45
+ expect(parseDuration('')).toBeUndefined();
46
+ expect(parseDuration('72x')).toBeUndefined();
47
+ });
48
+ });
49
+
50
+ // =============================================================================
51
+ // shouldCompact — evaluator
52
+ // =============================================================================
53
+
54
+ describe('shouldCompact', () => {
55
+ const baseSession: SessionState = {
56
+ runCount: 10,
57
+ inputTokens: 50_000,
58
+ startedAt: new Date().toISOString(),
59
+ };
60
+
61
+ it('returns false when no thresholds are breached', () => {
62
+ const result = shouldCompact(baseSession, {
63
+ maxRuns: 200,
64
+ maxInputTokens: 2_000_000,
65
+ maxAge: '72h',
66
+ });
67
+ expect(result.compact).toBe(false);
68
+ expect(result.reason).toBe('');
69
+ });
70
+
71
+ it('triggers on maxRuns', () => {
72
+ const session = { ...baseSession, runCount: 200 };
73
+ const result = shouldCompact(session, { maxRuns: 200 });
74
+ expect(result.compact).toBe(true);
75
+ expect(result.reason).toContain('Run count');
76
+ expect(result.reason).toContain('200');
77
+ });
78
+
79
+ it('triggers on maxRuns when exceeded', () => {
80
+ const session = { ...baseSession, runCount: 250 };
81
+ const result = shouldCompact(session, { maxRuns: 200 });
82
+ expect(result.compact).toBe(true);
83
+ });
84
+
85
+ it('triggers on maxInputTokens', () => {
86
+ const session = { ...baseSession, inputTokens: 2_500_000 };
87
+ const result = shouldCompact(session, { maxInputTokens: 2_000_000 });
88
+ expect(result.compact).toBe(true);
89
+ expect(result.reason).toContain('Input tokens');
90
+ });
91
+
92
+ it('triggers on maxAge', () => {
93
+ const startedAt = new Date(Date.now() - 80 * 3_600_000).toISOString(); // 80 hours ago
94
+ const session = { ...baseSession, startedAt };
95
+ const result = shouldCompact(session, { maxAge: '72h' });
96
+ expect(result.compact).toBe(true);
97
+ expect(result.reason).toContain('Session age');
98
+ });
99
+
100
+ it('returns first triggered threshold (maxRuns before maxInputTokens)', () => {
101
+ const session: SessionState = {
102
+ runCount: 300,
103
+ inputTokens: 3_000_000,
104
+ startedAt: new Date(Date.now() - 100 * 3_600_000).toISOString(),
105
+ };
106
+ const result = shouldCompact(session, {
107
+ maxRuns: 200,
108
+ maxInputTokens: 2_000_000,
109
+ maxAge: '72h',
110
+ });
111
+ expect(result.compact).toBe(true);
112
+ expect(result.reason).toContain('Run count');
113
+ });
114
+
115
+ it('skips undefined thresholds', () => {
116
+ const result = shouldCompact(baseSession, {});
117
+ expect(result.compact).toBe(false);
118
+ });
119
+
120
+ it('handles invalid maxAge gracefully', () => {
121
+ const result = shouldCompact(baseSession, { maxAge: 'bogus' });
122
+ expect(result.compact).toBe(false);
123
+ });
124
+
125
+ it('accepts custom now parameter for age calculation', () => {
126
+ const startedAt = '2026-01-01T00:00:00.000Z';
127
+ const now = new Date('2026-01-04T00:00:00.000Z'); // 3 days later
128
+ const session = { ...baseSession, startedAt };
129
+ const result = shouldCompact(session, { maxAge: '2d' }, now);
130
+ expect(result.compact).toBe(true);
131
+ });
132
+ });
133
+
134
+ // =============================================================================
135
+ // resolvePolicy — three-level merge
136
+ // =============================================================================
137
+
138
+ describe('resolvePolicy', () => {
139
+ it('returns engine defaults when no overrides', () => {
140
+ const policy = resolvePolicy();
141
+ expect(policy).toEqual(ENGINE_DEFAULTS);
142
+ });
143
+
144
+ it('agent config overrides engine defaults', () => {
145
+ const policy = resolvePolicy({ maxRuns: 100 });
146
+ expect(policy.maxRuns).toBe(100);
147
+ expect(policy.maxInputTokens).toBe(ENGINE_DEFAULTS.maxInputTokens);
148
+ expect(policy.maxAge).toBe(ENGINE_DEFAULTS.maxAge);
149
+ });
150
+
151
+ it('adapter defaults override engine defaults', () => {
152
+ const policy = resolvePolicy(undefined, { maxInputTokens: 1_000_000 });
153
+ expect(policy.maxInputTokens).toBe(1_000_000);
154
+ expect(policy.maxRuns).toBe(ENGINE_DEFAULTS.maxRuns);
155
+ });
156
+
157
+ it('agent config overrides adapter defaults', () => {
158
+ const policy = resolvePolicy({ maxAge: '24h' }, { maxAge: '48h' });
159
+ expect(policy.maxAge).toBe('24h');
160
+ });
161
+
162
+ it('merges individual fields from different levels', () => {
163
+ const policy = resolvePolicy({ maxRuns: 50 }, { maxInputTokens: 500_000, maxAge: '12h' });
164
+ expect(policy.maxRuns).toBe(50); // from agent
165
+ expect(policy.maxInputTokens).toBe(500_000); // from adapter
166
+ expect(policy.maxAge).toBe('12h'); // from adapter
167
+ });
168
+ });
169
+
170
+ // =============================================================================
171
+ // renderHandoff
172
+ // =============================================================================
173
+
174
+ describe('renderHandoff', () => {
175
+ it('renders complete handoff note', () => {
176
+ const note: HandoffNote = {
177
+ rotatedAt: '2026-03-27T12:00:00.000Z',
178
+ reason: 'Run count (200) reached threshold (200)',
179
+ inProgress: 'Implementing session compaction policies',
180
+ keyDecisions: ['Used three-level merge for policy resolution', 'ISO 8601 for timestamps'],
181
+ filesModified: [
182
+ 'packages/core/src/session/compaction-policy.ts',
183
+ 'packages/core/src/index.ts',
184
+ ],
185
+ };
186
+
187
+ const md = renderHandoff(note);
188
+ expect(md).toContain('# Session Handoff');
189
+ expect(md).toContain('**Rotated:** 2026-03-27T12:00:00.000Z');
190
+ expect(md).toContain('**Reason:** Run count');
191
+ expect(md).toContain('## In Progress');
192
+ expect(md).toContain('Implementing session compaction policies');
193
+ expect(md).toContain('## Key Decisions');
194
+ expect(md).toContain('- Used three-level merge');
195
+ expect(md).toContain('## Files Modified');
196
+ expect(md).toContain('- `packages/core/src/session/compaction-policy.ts`');
197
+ });
198
+
199
+ it('omits In Progress section when empty', () => {
200
+ const note: HandoffNote = {
201
+ rotatedAt: '2026-03-27T12:00:00.000Z',
202
+ reason: 'Token threshold',
203
+ inProgress: '',
204
+ keyDecisions: ['Something'],
205
+ filesModified: [],
206
+ };
207
+
208
+ const md = renderHandoff(note);
209
+ expect(md).not.toContain('## In Progress');
210
+ expect(md).toContain('## Key Decisions');
211
+ });
212
+
213
+ it('omits Key Decisions section when empty', () => {
214
+ const note: HandoffNote = {
215
+ rotatedAt: '2026-03-27T12:00:00.000Z',
216
+ reason: 'Age threshold',
217
+ inProgress: 'Working on X',
218
+ keyDecisions: [],
219
+ filesModified: [],
220
+ };
221
+
222
+ const md = renderHandoff(note);
223
+ expect(md).toContain('## In Progress');
224
+ expect(md).not.toContain('## Key Decisions');
225
+ expect(md).not.toContain('## Files Modified');
226
+ });
227
+
228
+ it('omits Files Modified section when empty', () => {
229
+ const note: HandoffNote = {
230
+ rotatedAt: '2026-03-27T12:00:00.000Z',
231
+ reason: 'Threshold',
232
+ inProgress: '',
233
+ keyDecisions: [],
234
+ filesModified: [],
235
+ };
236
+
237
+ const md = renderHandoff(note);
238
+ expect(md).not.toContain('## In Progress');
239
+ expect(md).not.toContain('## Key Decisions');
240
+ expect(md).not.toContain('## Files Modified');
241
+ // Should still have header and metadata
242
+ expect(md).toContain('# Session Handoff');
243
+ expect(md).toContain('**Rotated:**');
244
+ expect(md).toContain('**Reason:**');
245
+ });
246
+
247
+ it('ends with a trailing newline', () => {
248
+ const note: HandoffNote = {
249
+ rotatedAt: '2026-03-27T12:00:00.000Z',
250
+ reason: 'Test',
251
+ inProgress: '',
252
+ keyDecisions: [],
253
+ filesModified: [],
254
+ };
255
+
256
+ const md = renderHandoff(note);
257
+ expect(md.endsWith('\n')).toBe(true);
258
+ });
259
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Handoff Renderer — converts a HandoffNote into markdown for injection
3
+ * into the next session's context.
4
+ *
5
+ * Omits empty sections gracefully.
6
+ */
7
+
8
+ import type { HandoffNote } from './compaction-policy.js';
9
+
10
+ /**
11
+ * Render a HandoffNote as markdown.
12
+ *
13
+ * Sections:
14
+ * - Session Handoff (header)
15
+ * - Rotated At
16
+ * - Reason
17
+ * - In Progress
18
+ * - Key Decisions
19
+ * - Files Modified
20
+ *
21
+ * Empty sections are omitted entirely.
22
+ */
23
+ export function renderHandoff(note: HandoffNote): string {
24
+ const lines: string[] = ['# Session Handoff', ''];
25
+
26
+ lines.push(`**Rotated:** ${note.rotatedAt}`);
27
+ lines.push(`**Reason:** ${note.reason}`);
28
+ lines.push('');
29
+
30
+ if (note.inProgress) {
31
+ lines.push('## In Progress');
32
+ lines.push('');
33
+ lines.push(note.inProgress);
34
+ lines.push('');
35
+ }
36
+
37
+ if (note.keyDecisions.length > 0) {
38
+ lines.push('## Key Decisions');
39
+ lines.push('');
40
+ for (const decision of note.keyDecisions) {
41
+ lines.push(`- ${decision}`);
42
+ }
43
+ lines.push('');
44
+ }
45
+
46
+ if (note.filesModified.length > 0) {
47
+ lines.push('## Files Modified');
48
+ lines.push('');
49
+ for (const file of note.filesModified) {
50
+ lines.push(`- \`${file}\``);
51
+ }
52
+ lines.push('');
53
+ }
54
+
55
+ return lines.join('\n').trimEnd() + '\n';
56
+ }
@@ -0,0 +1,12 @@
1
+ // ─── Session Compaction ─────────────────────────────────────────────
2
+ export type {
3
+ CompactionPolicy,
4
+ CompactionResult,
5
+ SessionState,
6
+ HandoffNote,
7
+ } from './compaction-policy.js';
8
+ export { ENGINE_DEFAULTS } from './compaction-policy.js';
9
+
10
+ export { shouldCompact, parseDuration } from './compaction-evaluator.js';
11
+ export { resolvePolicy } from './policy-resolver.js';
12
+ export { renderHandoff } from './handoff-renderer.js';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Policy Resolver — three-level merge for compaction policies.
3
+ *
4
+ * Merge order (highest to lowest priority):
5
+ * 1. Agent config (agent.yaml → engine.compactionPolicy)
6
+ * 2. Adapter defaults (runtime adapter may provide defaults)
7
+ * 3. Engine defaults (hardcoded fallback)
8
+ *
9
+ * Individual fields override — not whole-object replacement.
10
+ */
11
+
12
+ import type { CompactionPolicy } from './compaction-policy.js';
13
+ import { ENGINE_DEFAULTS } from './compaction-policy.js';
14
+
15
+ /**
16
+ * Resolve a final CompactionPolicy by merging three levels.
17
+ *
18
+ * Each level can provide partial overrides. Fields from higher-priority
19
+ * levels win over lower-priority ones. Missing fields fall through to
20
+ * the next level, bottoming out at ENGINE_DEFAULTS.
21
+ */
22
+ export function resolvePolicy(
23
+ agentConfig?: Partial<CompactionPolicy>,
24
+ adapterDefaults?: Partial<CompactionPolicy>,
25
+ ): Required<CompactionPolicy> {
26
+ return {
27
+ maxRuns: agentConfig?.maxRuns ?? adapterDefaults?.maxRuns ?? ENGINE_DEFAULTS.maxRuns,
28
+ maxInputTokens:
29
+ agentConfig?.maxInputTokens ??
30
+ adapterDefaults?.maxInputTokens ??
31
+ ENGINE_DEFAULTS.maxInputTokens,
32
+ maxAge: agentConfig?.maxAge ?? adapterDefaults?.maxAge ?? ENGINE_DEFAULTS.maxAge,
33
+ };
34
+ }
@@ -7,12 +7,17 @@
7
7
  */
8
8
 
9
9
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
10
- import { join } from 'node:path';
10
+ import { join, dirname } from 'node:path';
11
11
  import { homedir } from 'node:os';
12
+ import type { SkillMetadata, SourceType } from '../packs/types.js';
13
+ import { classifyTrust } from './trust-classifier.js';
14
+ import { checkVersionCompat } from '../packs/resolver.js';
12
15
 
13
16
  export interface SkillEntry {
14
17
  name: string;
15
18
  sourcePath: string;
19
+ /** Trust and source metadata (populated during classification) */
20
+ metadata?: SkillMetadata;
16
21
  }
17
22
 
18
23
  export interface SyncResult {
@@ -22,6 +27,27 @@ export interface SyncResult {
22
27
  failed: string[];
23
28
  }
24
29
 
30
+ /** Error thrown when a skill requires approval due to scripts trust level */
31
+ export class ApprovalRequiredError extends Error {
32
+ readonly skillName: string;
33
+ readonly trust: 'scripts';
34
+ readonly inventory: SkillMetadata['inventory'];
35
+
36
+ constructor(skillName: string, inventory: SkillMetadata['inventory']) {
37
+ super(
38
+ `Skill "${skillName}" contains executable scripts and requires explicit approval. ` +
39
+ `Scripts found: ${inventory
40
+ .filter((i) => i.kind === 'script')
41
+ .map((i) => i.path)
42
+ .join(', ')}`,
43
+ );
44
+ this.name = 'ApprovalRequiredError';
45
+ this.skillName = skillName;
46
+ this.trust = 'scripts';
47
+ this.inventory = inventory;
48
+ }
49
+ }
50
+
25
51
  /** Discover skill files (SKILL.md) in skills directories */
26
52
  export function discoverSkills(skillsDirs: string[]): SkillEntry[] {
27
53
  const skills: SkillEntry[] = [];
@@ -103,3 +129,90 @@ export function syncSkillsToClaudeCode(skillsDirs: string[], agentName?: string)
103
129
 
104
130
  return result;
105
131
  }
132
+
133
+ // =============================================================================
134
+ // TRUST CLASSIFICATION & SOURCE TRACKING
135
+ // =============================================================================
136
+
137
+ /**
138
+ * Check engine version compatibility for a skill.
139
+ * Returns 'compatible', 'unknown' (no version specified), or 'invalid'.
140
+ */
141
+ export function checkSkillCompatibility(
142
+ engineVersion?: string,
143
+ currentVersion?: string,
144
+ ): 'compatible' | 'unknown' | 'invalid' {
145
+ if (!engineVersion) return 'unknown';
146
+ if (!currentVersion) return 'unknown';
147
+ return checkVersionCompat(currentVersion, engineVersion) ? 'compatible' : 'invalid';
148
+ }
149
+
150
+ /**
151
+ * Infer the source type for a skill based on its directory path.
152
+ */
153
+ function inferSourceType(skillDir: string): SourceType {
154
+ if (skillDir.includes('node_modules')) return 'npm';
155
+ if (skillDir.includes('.soleri') || skillDir.includes('.salvador')) return 'builtin';
156
+ return 'local';
157
+ }
158
+
159
+ /**
160
+ * Read engine version from a skill's SKILL.md frontmatter.
161
+ * Looks for `engine:` or `engineVersion:` in YAML frontmatter.
162
+ */
163
+ function readSkillEngineVersion(skillPath: string): string | undefined {
164
+ try {
165
+ const content = readFileSync(skillPath, 'utf-8');
166
+ const fmStart = content.indexOf('---');
167
+ if (fmStart !== 0) return undefined;
168
+ const fmEnd = content.indexOf('---', 3);
169
+ if (fmEnd === -1) return undefined;
170
+ const fm = content.slice(3, fmEnd);
171
+ // eslint-disable-next-line no-control-regex
172
+ const match = fm.match(/^(?:engine|engineVersion)\s*:\s*["']?([^"'\n]+)["']?/m);
173
+ return match?.[1]?.trim();
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ }
178
+
179
+ export interface ClassifySkillsOptions {
180
+ /** Current engine version for compatibility checking */
181
+ currentEngineVersion?: string;
182
+ /** Skills that have been explicitly approved for scripts trust level */
183
+ approvedScripts?: Set<string>;
184
+ }
185
+
186
+ /**
187
+ * Classify skills with trust levels and source tracking.
188
+ * Enriches SkillEntry[] with metadata. Throws ApprovalRequiredError
189
+ * for skills with 'scripts' trust unless explicitly approved.
190
+ */
191
+ export function classifySkills(
192
+ skills: SkillEntry[],
193
+ options: ClassifySkillsOptions = {},
194
+ ): SkillEntry[] {
195
+ return skills.map((skill) => {
196
+ const skillDir = dirname(skill.sourcePath);
197
+ const { trust, inventory } = classifyTrust(skillDir);
198
+
199
+ // Approval gate for scripts
200
+ if (trust === 'scripts' && !options.approvedScripts?.has(skill.name)) {
201
+ throw new ApprovalRequiredError(skill.name, inventory);
202
+ }
203
+
204
+ const engineVersion = readSkillEngineVersion(skill.sourcePath);
205
+ const sourceType = inferSourceType(skillDir);
206
+ const compatibility = checkSkillCompatibility(engineVersion, options.currentEngineVersion);
207
+
208
+ const metadata: SkillMetadata = {
209
+ trust,
210
+ source: { type: sourceType, uri: skillDir },
211
+ compatibility,
212
+ engineVersion,
213
+ inventory,
214
+ };
215
+
216
+ return { ...skill, metadata };
217
+ });
218
+ }