@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
@@ -4,8 +4,18 @@
4
4
  *
5
5
  * Heuristic: tool calls x average payload size x 1.5 overhead factor.
6
6
  * Assumes ~200K token context window for fill estimation.
7
+ *
8
+ * Also integrates with CompactionPolicy to trigger session rotation
9
+ * when policy thresholds are breached.
7
10
  */
8
11
 
12
+ import type {
13
+ CompactionPolicy,
14
+ CompactionResult,
15
+ SessionState,
16
+ } from '../session/compaction-policy.js';
17
+ import { shouldCompact } from '../session/compaction-evaluator.js';
18
+
9
19
  // =============================================================================
10
20
  // TYPES
11
21
  // =============================================================================
@@ -18,6 +28,8 @@ export interface ContextHealthStatus {
18
28
  toolCallCount: number;
19
29
  estimatedTokens: number;
20
30
  recommendation: string;
31
+ /** When a compaction policy is set, this contains the evaluation result. */
32
+ compaction?: CompactionResult;
21
33
  }
22
34
 
23
35
  export interface TrackEvent {
@@ -47,6 +59,17 @@ const RECOMMENDATIONS: Record<HealthLevel, string> = {
47
59
  export class ContextHealthMonitor {
48
60
  private toolCallCount = 0;
49
61
  private totalPayloadSize = 0;
62
+ private compactionPolicy: CompactionPolicy | undefined;
63
+ private sessionStartedAt: string | undefined;
64
+
65
+ /**
66
+ * Set the compaction policy and session start time.
67
+ * When set, `check()` will evaluate compaction thresholds.
68
+ */
69
+ setCompactionPolicy(policy: CompactionPolicy, startedAt?: string): void {
70
+ this.compactionPolicy = policy;
71
+ this.sessionStartedAt = startedAt ?? new Date().toISOString();
72
+ }
50
73
 
51
74
  /** Track a tool call event. */
52
75
  track(event: TrackEvent): void {
@@ -54,19 +77,36 @@ export class ContextHealthMonitor {
54
77
  this.totalPayloadSize += event.payloadSize;
55
78
  }
56
79
 
57
- /** Check current context health status. */
80
+ /** Check current context health status (including compaction policy). */
58
81
  check(): ContextHealthStatus {
59
82
  const estimatedTokens = Math.round(this.totalPayloadSize * OVERHEAD_FACTOR);
60
83
  const estimatedFill = Math.min(estimatedTokens / CONTEXT_WINDOW, 1.0);
61
84
  const level = this.classifyLevel(estimatedFill);
62
85
 
63
- return {
86
+ const status: ContextHealthStatus = {
64
87
  level,
65
88
  estimatedFill: Math.round(estimatedFill * 1000) / 1000,
66
89
  toolCallCount: this.toolCallCount,
67
90
  estimatedTokens,
68
91
  recommendation: RECOMMENDATIONS[level],
69
92
  };
93
+
94
+ // Evaluate compaction policy if configured
95
+ if (this.compactionPolicy && this.sessionStartedAt) {
96
+ const session: SessionState = {
97
+ runCount: this.toolCallCount,
98
+ inputTokens: estimatedTokens,
99
+ startedAt: this.sessionStartedAt,
100
+ };
101
+ status.compaction = shouldCompact(session, this.compactionPolicy);
102
+
103
+ // Escalate recommendation when compaction is triggered
104
+ if (status.compaction.compact) {
105
+ status.recommendation = `Compaction triggered: ${status.compaction.reason}`;
106
+ }
107
+ }
108
+
109
+ return status;
70
110
  }
71
111
 
72
112
  /** Reset all tracking (on session clear). */
@@ -38,6 +38,7 @@ import {
38
38
  import { detectRationalizations } from '../planning/rationalization-detector.js';
39
39
  import { ImpactAnalyzer } from '../planning/impact-analyzer.js';
40
40
  import type { ImpactReport } from '../planning/impact-analyzer.js';
41
+ import { recordPlanFeedback } from './plan-feedback-helper.js';
41
42
 
42
43
  // ---------------------------------------------------------------------------
43
44
  // Intent detection — keyword-based mapping from prompt to intent
@@ -235,7 +236,7 @@ export function createOrchestrateOps(
235
236
  runtime: AgentRuntime,
236
237
  facades?: FacadeConfig[],
237
238
  ): OpDefinition[] {
238
- const { planner, brainIntelligence, vault, contextHealth } = runtime;
239
+ const { planner, brain, brainIntelligence, vault, contextHealth } = runtime;
239
240
  const agentId = runtime.config.agentId;
240
241
 
241
242
  return [
@@ -385,11 +386,149 @@ export function createOrchestrateOps(
385
386
  planId: z.string().describe('ID of the plan to execute (flow planId or legacy planId)'),
386
387
  domain: z.string().optional().describe('Domain for brain session tracking'),
387
388
  context: z.string().optional().describe('Additional context for the brain session'),
389
+ runtime: z
390
+ .string()
391
+ .optional()
392
+ .describe(
393
+ 'Runtime adapter type (e.g. "claude-code", "codex"). ' +
394
+ 'When provided, dispatches via the adapter instead of the flow engine.',
395
+ ),
396
+ subagent: z
397
+ .boolean()
398
+ .optional()
399
+ .describe(
400
+ 'When true, dispatches plan tasks via SubagentDispatcher instead of FlowExecutor. ' +
401
+ 'Each task runs as a separate subagent process.',
402
+ ),
403
+ parallel: z
404
+ .boolean()
405
+ .optional()
406
+ .describe(
407
+ 'Run subagent tasks in parallel (default: true). Only applies when subagent=true.',
408
+ ),
409
+ maxConcurrent: z
410
+ .number()
411
+ .optional()
412
+ .describe('Max concurrent subagents (default: 3). Only applies when subagent=true.'),
388
413
  }),
389
414
  handler: async (params) => {
390
415
  const planId = params.planId as string;
391
416
  const domain = params.domain as string | undefined;
392
417
  const context = params.context as string | undefined;
418
+ const runtimeType = params.runtime as string | undefined;
419
+ const useSubagent = params.subagent as boolean | undefined;
420
+ const parallelMode = params.parallel as boolean | undefined;
421
+ const maxConcurrentParam = params.maxConcurrent as number | undefined;
422
+
423
+ // ── Subagent dispatch path ───────────────────────────────────
424
+ // When subagent=true, dispatch plan tasks via SubagentDispatcher.
425
+ // Each task runs as a separate child process via the adapter layer.
426
+ if (useSubagent && runtime.subagentDispatcher) {
427
+ const entry = planStore.get(planId);
428
+ const legacyPlan = !entry ? planner.get(planId) : undefined;
429
+ const tasks =
430
+ entry?.plan.steps.map((s) => ({
431
+ taskId: s.id,
432
+ prompt: s.name,
433
+ workspace: process.cwd(),
434
+ runtime: runtimeType,
435
+ timeout: 300_000,
436
+ })) ??
437
+ legacyPlan?.tasks?.map((t) => ({
438
+ taskId: t.id,
439
+ prompt: t.title ?? t.description ?? '',
440
+ workspace: process.cwd(),
441
+ runtime: runtimeType,
442
+ timeout: 300_000,
443
+ })) ??
444
+ [];
445
+
446
+ const aggregated = await runtime.subagentDispatcher.dispatch(tasks, {
447
+ parallel: parallelMode ?? true,
448
+ maxConcurrent: maxConcurrentParam ?? 3,
449
+ });
450
+
451
+ // Track in brain session
452
+ const existingSession = brainIntelligence.getSessionByPlanId(planId);
453
+ const session =
454
+ existingSession && !existingSession.endedAt
455
+ ? existingSession
456
+ : brainIntelligence.lifecycle({
457
+ action: 'start',
458
+ domain,
459
+ context,
460
+ planId,
461
+ });
462
+
463
+ contextHealth.track({
464
+ type: 'orchestrate_execute',
465
+ payloadSize: JSON.stringify(aggregated).length,
466
+ });
467
+ const healthStatus = contextHealth.check();
468
+ const healthWarning = buildHealthWarning(healthStatus, vault);
469
+
470
+ return {
471
+ plan: { id: planId, status: 'executing' },
472
+ session,
473
+ subagent: {
474
+ status: aggregated.status,
475
+ totalTasks: aggregated.totalTasks,
476
+ completed: aggregated.completed,
477
+ failed: aggregated.failed,
478
+ durationMs: aggregated.durationMs,
479
+ totalUsage: aggregated.totalUsage,
480
+ },
481
+ ...(healthWarning ? { contextHealth: healthWarning } : {}),
482
+ };
483
+ }
484
+
485
+ // ── Adapter dispatch path ────────────────────────────────────
486
+ // When a runtime is specified, dispatch the plan's prompt via the
487
+ // adapter instead of the flow engine. This is the integration point
488
+ // for multi-runtime support (GH #410).
489
+ if (runtimeType && runtime.adapterRegistry) {
490
+ const adapter = runtime.adapterRegistry.get(runtimeType);
491
+ const entry = planStore.get(planId);
492
+ const prompt = entry?.plan.summary ?? `Execute plan ${planId}`;
493
+
494
+ const adapterResult = await adapter.execute({
495
+ runId: `${planId}-${Date.now()}`,
496
+ prompt,
497
+ workspace: process.cwd(),
498
+ config: { planId, domain },
499
+ });
500
+
501
+ // Track in brain session
502
+ const existingSession = brainIntelligence.getSessionByPlanId(planId);
503
+ const session =
504
+ existingSession && !existingSession.endedAt
505
+ ? existingSession
506
+ : brainIntelligence.lifecycle({
507
+ action: 'start',
508
+ domain,
509
+ context,
510
+ planId,
511
+ });
512
+
513
+ contextHealth.track({
514
+ type: 'orchestrate_execute',
515
+ payloadSize: JSON.stringify(adapterResult).length,
516
+ });
517
+ const healthStatus = contextHealth.check();
518
+ const healthWarning = buildHealthWarning(healthStatus, vault);
519
+
520
+ return {
521
+ plan: { id: planId, status: 'executing' },
522
+ session,
523
+ adapter: {
524
+ type: runtimeType,
525
+ exitCode: adapterResult.exitCode,
526
+ summary: adapterResult.summary,
527
+ usage: adapterResult.usage,
528
+ },
529
+ ...(healthWarning ? { contextHealth: healthWarning } : {}),
530
+ };
531
+ }
393
532
 
394
533
  // Look up flow plan
395
534
  const entry = planStore.get(planId);
@@ -636,6 +775,19 @@ export function createOrchestrateOps(
636
775
  filesModified,
637
776
  });
638
777
 
778
+ // Record brain feedback for vault entries referenced in plan decisions
779
+ if (planObj && planObj.decisions) {
780
+ try {
781
+ recordPlanFeedback(
782
+ { objective: planObj.objective, decisions: planObj.decisions },
783
+ brain,
784
+ brainIntelligence,
785
+ );
786
+ } catch {
787
+ // Brain feedback is best-effort
788
+ }
789
+ }
790
+
639
791
  // Extract knowledge — runs regardless of plan existence
640
792
  let extraction = null;
641
793
  try {
@@ -61,6 +61,9 @@ import { generatePersonaInstructions } from '../persona/prompt-generator.js';
61
61
  import { OperatorProfileStore } from '../operator/operator-profile.js';
62
62
  import { ContextHealthMonitor } from './context-health.js';
63
63
  import { ShutdownRegistry } from './shutdown-registry.js';
64
+ import { RuntimeAdapterRegistry } from '../adapters/registry.js';
65
+ import { ClaudeCodeRuntimeAdapter } from '../adapters/claude-code-adapter.js';
66
+ import { SubagentDispatcher } from '../subagent/dispatcher.js';
64
67
 
65
68
  /**
66
69
  * Create a fully initialized agent runtime.
@@ -322,6 +325,8 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
322
325
  shutdownRegistry.register('pipelineRunner', () => pipelineRunner.stop());
323
326
  // Agency manager — close FSWatchers and debounce timers
324
327
  shutdownRegistry.register('agencyManager', () => agencyManager.disable());
328
+
329
+ shutdownRegistry.register('subagentDispatcher', () => subagentDispatcher.cleanup());
325
330
  // Loop manager — clear accumulated state
326
331
  shutdownRegistry.register('loopManager', () => {
327
332
  if (loop.isActive()) {
@@ -333,6 +338,14 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
333
338
  }
334
339
  });
335
340
 
341
+ // Runtime Adapter Registry — dispatch work to different AI CLIs
342
+ const adapterRegistry = new RuntimeAdapterRegistry();
343
+ adapterRegistry.register('claude-code', new ClaudeCodeRuntimeAdapter());
344
+ adapterRegistry.setDefault('claude-code');
345
+
346
+ // Subagent Dispatcher — spawn and manage child agent processes
347
+ const subagentDispatcher = new SubagentDispatcher({ adapterRegistry });
348
+
336
349
  return {
337
350
  config,
338
351
  logger,
@@ -379,6 +392,8 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
379
392
  const p = loadPersona(agentId, config.persona ?? undefined);
380
393
  return generatePersonaInstructions(p);
381
394
  })(),
395
+ adapterRegistry,
396
+ subagentDispatcher,
382
397
  contextHealth: new ContextHealthMonitor(),
383
398
  shutdownRegistry,
384
399
  createdAt: Date.now(),
@@ -17,6 +17,14 @@ function makeRuntime(overrides?: {
17
17
  toolsUsed: string[];
18
18
  filesModified: string[];
19
19
  }>;
20
+ memories?: Array<{
21
+ id?: string;
22
+ createdAt: number;
23
+ projectPath?: string;
24
+ summary?: string;
25
+ context?: string;
26
+ type?: string;
27
+ }>;
20
28
  plans?: Array<{
21
29
  id: string;
22
30
  status: string;
@@ -45,6 +53,7 @@ function makeRuntime(overrides?: {
45
53
  vault: {
46
54
  stats: () => o.vaultStats ?? { totalEntries: 50, byType: { playbook: 5 } },
47
55
  getRecent: (_n: number) => o.recentEntries ?? [],
56
+ listMemories: () => o.memories ?? [],
48
57
  },
49
58
  curator: {
50
59
  healthAudit: () => ({
@@ -95,11 +104,34 @@ describe('session-briefing', () => {
95
104
  expect(data.sections.find((s) => s.label === 'Welcome')).toBeUndefined();
96
105
  });
97
106
 
98
- it('includes Last session section', async () => {
107
+ it('includes Last session from cross-project memories when fresh', async () => {
108
+ const runtime = makeRuntime({
109
+ memories: [
110
+ {
111
+ id: 'mem-1',
112
+ createdAt: Date.now() - 600_000, // 10 min ago (ms)
113
+ projectPath: '/Users/me/projects/other-app',
114
+ summary: 'Fixed KPI card layout in the dashboard',
115
+ type: 'session',
116
+ },
117
+ ],
118
+ });
119
+ const ops = captureOps(createSessionBriefingOps(runtime));
120
+ const res = await executeOp(ops, 'session_briefing', {});
121
+
122
+ const data = res.data as { sections: Array<{ label: string; content: string }> };
123
+ const session = data.sections.find((s) => s.label === 'Last session');
124
+ expect(session).toBeDefined();
125
+ expect(session!.content).toContain('other-app');
126
+ expect(session!.content).toContain('Fixed KPI card layout');
127
+ });
128
+
129
+ it('falls back to brain sessions when no fresh memories exist', async () => {
99
130
  const runtime = makeRuntime({
131
+ memories: [], // no memories
100
132
  sessions: [
101
133
  {
102
- endedAt: new Date(Date.now() - 3600000).toISOString(),
134
+ endedAt: new Date(Date.now() - 3600_000).toISOString(), // 1h ago
103
135
  domain: 'frontend',
104
136
  context: 'Refactored button component',
105
137
  toolsUsed: ['vault_search', 'brain_recommend'],
@@ -117,6 +149,63 @@ describe('session-briefing', () => {
117
149
  expect(session!.content).toContain('Refactored button component');
118
150
  });
119
151
 
152
+ it('skips Last session when all sessions are stale', async () => {
153
+ const staleTs = Date.now() - 72 * 3600_000; // 72h ago — beyond default 48h
154
+ const runtime = makeRuntime({
155
+ memories: [
156
+ {
157
+ id: 'mem-old',
158
+ createdAt: staleTs,
159
+ projectPath: '/old-project',
160
+ summary: 'Ancient session',
161
+ type: 'session',
162
+ },
163
+ ],
164
+ sessions: [
165
+ {
166
+ endedAt: new Date(staleTs).toISOString(),
167
+ domain: 'old',
168
+ context: 'Ancient brain session',
169
+ toolsUsed: [],
170
+ filesModified: [],
171
+ },
172
+ ],
173
+ });
174
+ const ops = captureOps(createSessionBriefingOps(runtime));
175
+ const res = await executeOp(ops, 'session_briefing', {});
176
+
177
+ const data = res.data as { sections: Array<{ label: string; content: string }> };
178
+ const session = data.sections.find((s) => s.label === 'Last session');
179
+ expect(session).toBeUndefined();
180
+ });
181
+
182
+ it('respects custom recencyHours parameter', async () => {
183
+ const runtime = makeRuntime({
184
+ memories: [
185
+ {
186
+ id: 'mem-3h',
187
+ createdAt: Date.now() - 3 * 3600_000, // 3h ago
188
+ projectPath: '/recent-project',
189
+ summary: 'Recent work',
190
+ type: 'session',
191
+ },
192
+ ],
193
+ });
194
+ const ops = captureOps(createSessionBriefingOps(runtime));
195
+
196
+ // With 1h window — should skip
197
+ const narrow = await executeOp(ops, 'session_briefing', { recencyHours: 1 });
198
+ const narrowData = narrow.data as { sections: Array<{ label: string }> };
199
+ expect(narrowData.sections.find((s) => s.label === 'Last session')).toBeUndefined();
200
+
201
+ // With 4h window — should include
202
+ const wide = await executeOp(ops, 'session_briefing', { recencyHours: 4 });
203
+ const wideData = wide.data as { sections: Array<{ label: string; content: string }> };
204
+ const session = wideData.sections.find((s) => s.label === 'Last session');
205
+ expect(session).toBeDefined();
206
+ expect(session!.content).toContain('Recent work');
207
+ });
208
+
120
209
  it('includes Active plans section', async () => {
121
210
  const runtime = makeRuntime({
122
211
  plans: [
@@ -234,6 +323,9 @@ describe('session-briefing', () => {
234
323
  getRecent: () => {
235
324
  throw new Error('no vault');
236
325
  },
326
+ listMemories: () => {
327
+ throw new Error('no vault');
328
+ },
237
329
  },
238
330
  curator: {
239
331
  healthAudit: () => {
@@ -57,22 +57,58 @@ export function createSessionBriefingOps(runtime: AgentRuntime): OpDefinition[]
57
57
  // Vault stats unavailable — skip
58
58
  }
59
59
 
60
- // 1. Last session
60
+ // 1. Last session — cross-project, with staleness threshold
61
61
  try {
62
- const sessions = brainIntelligence.listSessions({ limit: 1, active: false });
63
- dataPoints += sessions.length;
64
- if (sessions.length > 0) {
65
- const last = sessions[0];
66
- const ago = formatTimeAgo(last.endedAt ? new Date(last.endedAt).getTime() : Date.now());
67
- const domain = last.domain ? ` [${last.domain}]` : '';
68
- const context = last.context ? `: ${last.context.slice(0, 80)}` : '';
69
- const tools = last.toolsUsed.length > 0 ? `, used ${last.toolsUsed.length} tools` : '';
70
- const files =
71
- last.filesModified.length > 0 ? `, modified ${last.filesModified.length} files` : '';
62
+ const recencyMs = (params.recencyHours as number) * 3600_000;
63
+ const cutoff = Date.now() - recencyMs;
64
+
65
+ // Try cross-project memories first (most recent work, any project)
66
+ const recentMemories = vault.listMemories({ type: 'session', limit: 3 });
67
+ const freshMemory = recentMemories.find((m) => {
68
+ const ts = m.createdAt > 1e12 ? m.createdAt : m.createdAt * 1000;
69
+ return ts > cutoff;
70
+ });
71
+
72
+ if (freshMemory) {
73
+ const ts =
74
+ freshMemory.createdAt > 1e12 ? freshMemory.createdAt : freshMemory.createdAt * 1000;
75
+ const ago = formatTimeAgo(ts);
76
+ const project = freshMemory.projectPath
77
+ ? ` [${freshMemory.projectPath.split('/').pop()}]`
78
+ : '';
79
+ const summary = freshMemory.summary
80
+ ? `: ${freshMemory.summary.slice(0, 100)}`
81
+ : freshMemory.context
82
+ ? `: ${freshMemory.context.slice(0, 100)}`
83
+ : '';
84
+ dataPoints += recentMemories.length;
72
85
  sections.push({
73
86
  label: 'Last session',
74
- content: `(${ago})${domain}${context}${tools}${files}`,
87
+ content: `(${ago})${project}${summary}`,
75
88
  });
89
+ } else {
90
+ // Fall back to brain sessions (same-project only) if no fresh memory
91
+ const sessions = brainIntelligence.listSessions({ limit: 1, active: false });
92
+ dataPoints += sessions.length;
93
+ if (sessions.length > 0) {
94
+ const last = sessions[0];
95
+ const endTs = last.endedAt ? new Date(last.endedAt).getTime() : 0;
96
+ if (endTs > cutoff) {
97
+ const ago = formatTimeAgo(endTs);
98
+ const domain = last.domain ? ` [${last.domain}]` : '';
99
+ const context = last.context ? `: ${last.context.slice(0, 80)}` : '';
100
+ const tools =
101
+ last.toolsUsed.length > 0 ? `, used ${last.toolsUsed.length} tools` : '';
102
+ const files =
103
+ last.filesModified.length > 0
104
+ ? `, modified ${last.filesModified.length} files`
105
+ : '';
106
+ sections.push({
107
+ label: 'Last session',
108
+ content: `(${ago})${domain}${context}${tools}${files}`,
109
+ });
110
+ }
111
+ }
76
112
  }
77
113
  } catch {
78
114
  // Session data unavailable — skip
@@ -37,6 +37,8 @@ import type { OperatorProfileStore } from '../operator/operator-profile.js';
37
37
  import type { OperatorContextStore } from '../operator/operator-context-store.js';
38
38
  import type { ContextHealthMonitor } from './context-health.js';
39
39
  import type { ShutdownRegistry } from './shutdown-registry.js';
40
+ import type { RuntimeAdapterRegistry } from '../adapters/registry.js';
41
+ import type { SubagentDispatcher } from '../subagent/dispatcher.js';
40
42
 
41
43
  /**
42
44
  * Configuration for creating an agent runtime.
@@ -129,6 +131,10 @@ export interface AgentRuntime {
129
131
  persona: import('../persona/types.js').PersonaConfig;
130
132
  /** Generated persona system instructions for LLM context. */
131
133
  personaInstructions: import('../persona/types.js').PersonaSystemInstructions;
134
+ /** Runtime adapter registry — dispatch work to different AI CLIs. */
135
+ adapterRegistry: RuntimeAdapterRegistry;
136
+ /** Subagent dispatcher — spawn and manage child agent processes. */
137
+ subagentDispatcher: SubagentDispatcher;
132
138
  /** Context health monitor — tracks tool call volume and context window fill. */
133
139
  contextHealth: ContextHealthMonitor;
134
140
  /** Shutdown registry — centralized cleanup for timers, watchers, child processes. */
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Compaction Evaluator — checks session state against a compaction policy
3
+ * and determines whether the session should be rotated.
4
+ */
5
+
6
+ import type { CompactionPolicy, CompactionResult, SessionState } from './compaction-policy.js';
7
+
8
+ // =============================================================================
9
+ // DURATION PARSER
10
+ // =============================================================================
11
+
12
+ const DURATION_RE = /^(\d+)(ms|s|m|h|d)$/;
13
+
14
+ const MULTIPLIERS: Record<string, number> = {
15
+ ms: 1,
16
+ s: 1_000,
17
+ m: 60_000,
18
+ h: 3_600_000,
19
+ d: 86_400_000,
20
+ };
21
+
22
+ /**
23
+ * Parse a simple duration string (e.g. '72h', '30m', '7d') into milliseconds.
24
+ * Returns `undefined` for invalid input.
25
+ */
26
+ export function parseDuration(duration: string): number | undefined {
27
+ const match = DURATION_RE.exec(duration.trim());
28
+ if (!match) return undefined;
29
+ const [, value, unit] = match;
30
+ return Number(value) * MULTIPLIERS[unit];
31
+ }
32
+
33
+ // =============================================================================
34
+ // EVALUATOR
35
+ // =============================================================================
36
+
37
+ /**
38
+ * Evaluate whether a session should be compacted based on policy thresholds.
39
+ *
40
+ * Returns the first triggered threshold as the reason. Checks in order:
41
+ * 1. maxRuns
42
+ * 2. maxInputTokens
43
+ * 3. maxAge
44
+ */
45
+ export function shouldCompact(
46
+ session: SessionState,
47
+ policy: CompactionPolicy,
48
+ now: Date = new Date(),
49
+ ): CompactionResult {
50
+ const noCompact: CompactionResult = { compact: false, reason: '', handoff: '' };
51
+
52
+ // Check maxRuns
53
+ if (policy.maxRuns !== undefined && session.runCount >= policy.maxRuns) {
54
+ return {
55
+ compact: true,
56
+ reason: `Run count (${session.runCount}) reached threshold (${policy.maxRuns})`,
57
+ handoff: '',
58
+ };
59
+ }
60
+
61
+ // Check maxInputTokens
62
+ if (policy.maxInputTokens !== undefined && session.inputTokens >= policy.maxInputTokens) {
63
+ return {
64
+ compact: true,
65
+ reason: `Input tokens (${session.inputTokens}) reached threshold (${policy.maxInputTokens})`,
66
+ handoff: '',
67
+ };
68
+ }
69
+
70
+ // Check maxAge
71
+ if (policy.maxAge !== undefined) {
72
+ const maxMs = parseDuration(policy.maxAge);
73
+ if (maxMs !== undefined) {
74
+ const startedAt = new Date(session.startedAt).getTime();
75
+ const elapsed = now.getTime() - startedAt;
76
+ if (elapsed >= maxMs) {
77
+ return {
78
+ compact: true,
79
+ reason: `Session age (${Math.round(elapsed / 60_000)}m) reached threshold (${policy.maxAge})`,
80
+ handoff: '',
81
+ };
82
+ }
83
+ }
84
+ }
85
+
86
+ return noCompact;
87
+ }