@soleri/core 9.3.1 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/brain/intelligence.d.ts +5 -0
  2. package/dist/brain/intelligence.d.ts.map +1 -1
  3. package/dist/brain/intelligence.js +115 -26
  4. package/dist/brain/intelligence.js.map +1 -1
  5. package/dist/brain/learning-radar.d.ts +3 -3
  6. package/dist/brain/learning-radar.d.ts.map +1 -1
  7. package/dist/brain/learning-radar.js +8 -4
  8. package/dist/brain/learning-radar.js.map +1 -1
  9. package/dist/control/intent-router.d.ts +2 -2
  10. package/dist/control/intent-router.d.ts.map +1 -1
  11. package/dist/control/intent-router.js +35 -1
  12. package/dist/control/intent-router.js.map +1 -1
  13. package/dist/control/types.d.ts +10 -2
  14. package/dist/control/types.d.ts.map +1 -1
  15. package/dist/curator/curator.d.ts +4 -0
  16. package/dist/curator/curator.d.ts.map +1 -1
  17. package/dist/curator/curator.js +23 -1
  18. package/dist/curator/curator.js.map +1 -1
  19. package/dist/curator/schema.d.ts +1 -1
  20. package/dist/curator/schema.d.ts.map +1 -1
  21. package/dist/curator/schema.js +8 -0
  22. package/dist/curator/schema.js.map +1 -1
  23. package/dist/domain-packs/types.d.ts +6 -0
  24. package/dist/domain-packs/types.d.ts.map +1 -1
  25. package/dist/domain-packs/types.js +1 -0
  26. package/dist/domain-packs/types.js.map +1 -1
  27. package/dist/engine/module-manifest.js +3 -3
  28. package/dist/engine/module-manifest.js.map +1 -1
  29. package/dist/engine/register-engine.d.ts +9 -0
  30. package/dist/engine/register-engine.d.ts.map +1 -1
  31. package/dist/engine/register-engine.js +59 -1
  32. package/dist/engine/register-engine.js.map +1 -1
  33. package/dist/facades/types.d.ts +5 -1
  34. package/dist/facades/types.d.ts.map +1 -1
  35. package/dist/facades/types.js.map +1 -1
  36. package/dist/index.d.ts +4 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/operator/operator-context-store.d.ts +54 -0
  41. package/dist/operator/operator-context-store.d.ts.map +1 -0
  42. package/dist/operator/operator-context-store.js +434 -0
  43. package/dist/operator/operator-context-store.js.map +1 -0
  44. package/dist/operator/operator-context-types.d.ts +101 -0
  45. package/dist/operator/operator-context-types.d.ts.map +1 -0
  46. package/dist/operator/operator-context-types.js +27 -0
  47. package/dist/operator/operator-context-types.js.map +1 -0
  48. package/dist/packs/index.d.ts +2 -2
  49. package/dist/packs/index.d.ts.map +1 -1
  50. package/dist/packs/index.js +1 -1
  51. package/dist/packs/index.js.map +1 -1
  52. package/dist/packs/lockfile.d.ts +3 -0
  53. package/dist/packs/lockfile.d.ts.map +1 -1
  54. package/dist/packs/lockfile.js.map +1 -1
  55. package/dist/packs/types.d.ts +8 -2
  56. package/dist/packs/types.d.ts.map +1 -1
  57. package/dist/packs/types.js +6 -0
  58. package/dist/packs/types.js.map +1 -1
  59. package/dist/planning/plan-lifecycle.d.ts +12 -1
  60. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  61. package/dist/planning/plan-lifecycle.js +52 -19
  62. package/dist/planning/plan-lifecycle.js.map +1 -1
  63. package/dist/planning/planner-types.d.ts +6 -0
  64. package/dist/planning/planner-types.d.ts.map +1 -1
  65. package/dist/planning/planner.d.ts +21 -1
  66. package/dist/planning/planner.d.ts.map +1 -1
  67. package/dist/planning/planner.js +62 -3
  68. package/dist/planning/planner.js.map +1 -1
  69. package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
  70. package/dist/planning/task-complexity-assessor.js.map +1 -1
  71. package/dist/plugins/types.d.ts +18 -18
  72. package/dist/runtime/admin-ops.d.ts +1 -1
  73. package/dist/runtime/admin-ops.d.ts.map +1 -1
  74. package/dist/runtime/admin-ops.js +100 -3
  75. package/dist/runtime/admin-ops.js.map +1 -1
  76. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  77. package/dist/runtime/admin-setup-ops.js +19 -9
  78. package/dist/runtime/admin-setup-ops.js.map +1 -1
  79. package/dist/runtime/capture-ops.d.ts.map +1 -1
  80. package/dist/runtime/capture-ops.js +35 -7
  81. package/dist/runtime/capture-ops.js.map +1 -1
  82. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  83. package/dist/runtime/facades/brain-facade.js +4 -2
  84. package/dist/runtime/facades/brain-facade.js.map +1 -1
  85. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  86. package/dist/runtime/facades/control-facade.js +8 -2
  87. package/dist/runtime/facades/control-facade.js.map +1 -1
  88. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  89. package/dist/runtime/facades/curator-facade.js +13 -0
  90. package/dist/runtime/facades/curator-facade.js.map +1 -1
  91. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  92. package/dist/runtime/facades/memory-facade.js +10 -12
  93. package/dist/runtime/facades/memory-facade.js.map +1 -1
  94. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  95. package/dist/runtime/facades/orchestrate-facade.js +36 -1
  96. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  97. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  98. package/dist/runtime/facades/plan-facade.js +20 -4
  99. package/dist/runtime/facades/plan-facade.js.map +1 -1
  100. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  101. package/dist/runtime/orchestrate-ops.js +71 -4
  102. package/dist/runtime/orchestrate-ops.js.map +1 -1
  103. package/dist/runtime/plan-feedback-helper.d.ts +21 -0
  104. package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
  105. package/dist/runtime/plan-feedback-helper.js +52 -0
  106. package/dist/runtime/plan-feedback-helper.js.map +1 -0
  107. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  108. package/dist/runtime/planning-extra-ops.js +73 -34
  109. package/dist/runtime/planning-extra-ops.js.map +1 -1
  110. package/dist/runtime/session-briefing.d.ts.map +1 -1
  111. package/dist/runtime/session-briefing.js +9 -1
  112. package/dist/runtime/session-briefing.js.map +1 -1
  113. package/dist/runtime/types.d.ts +3 -0
  114. package/dist/runtime/types.d.ts.map +1 -1
  115. package/dist/skills/sync-skills.d.ts.map +1 -1
  116. package/dist/skills/sync-skills.js +13 -7
  117. package/dist/skills/sync-skills.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/brain/brain-intelligence.test.ts +30 -0
  120. package/src/brain/extraction-quality.test.ts +323 -0
  121. package/src/brain/intelligence.ts +133 -30
  122. package/src/brain/learning-radar.ts +8 -5
  123. package/src/brain/second-brain-features.test.ts +1 -1
  124. package/src/control/intent-router.test.ts +73 -3
  125. package/src/control/intent-router.ts +38 -1
  126. package/src/control/types.ts +13 -2
  127. package/src/curator/curator.test.ts +92 -0
  128. package/src/curator/curator.ts +29 -1
  129. package/src/curator/schema.ts +8 -0
  130. package/src/domain-packs/types.ts +8 -0
  131. package/src/engine/module-manifest.test.ts +8 -2
  132. package/src/engine/module-manifest.ts +3 -3
  133. package/src/engine/register-engine.test.ts +73 -1
  134. package/src/engine/register-engine.ts +61 -1
  135. package/src/facades/types.ts +5 -0
  136. package/src/index.ts +22 -0
  137. package/src/operator/operator-context-store.test.ts +698 -0
  138. package/src/operator/operator-context-store.ts +569 -0
  139. package/src/operator/operator-context-types.ts +139 -0
  140. package/src/packs/index.ts +3 -1
  141. package/src/packs/lockfile.ts +3 -0
  142. package/src/packs/types.ts +9 -0
  143. package/src/planning/plan-lifecycle.ts +80 -22
  144. package/src/planning/planner-types.ts +6 -0
  145. package/src/planning/planner.ts +74 -4
  146. package/src/planning/task-complexity-assessor.test.ts +6 -2
  147. package/src/planning/task-complexity-assessor.ts +1 -4
  148. package/src/runtime/admin-ops.test.ts +139 -6
  149. package/src/runtime/admin-ops.ts +104 -3
  150. package/src/runtime/admin-setup-ops.ts +30 -10
  151. package/src/runtime/capture-ops.test.ts +84 -0
  152. package/src/runtime/capture-ops.ts +35 -7
  153. package/src/runtime/facades/admin-facade.test.ts +1 -1
  154. package/src/runtime/facades/brain-facade.ts +6 -3
  155. package/src/runtime/facades/control-facade.ts +10 -2
  156. package/src/runtime/facades/curator-facade.ts +18 -0
  157. package/src/runtime/facades/memory-facade.test.ts +14 -12
  158. package/src/runtime/facades/memory-facade.ts +10 -12
  159. package/src/runtime/facades/orchestrate-facade.ts +33 -1
  160. package/src/runtime/facades/plan-facade.test.ts +213 -0
  161. package/src/runtime/facades/plan-facade.ts +23 -4
  162. package/src/runtime/orchestrate-ops.test.ts +202 -2
  163. package/src/runtime/orchestrate-ops.ts +85 -4
  164. package/src/runtime/plan-feedback-helper.test.ts +173 -0
  165. package/src/runtime/plan-feedback-helper.ts +63 -0
  166. package/src/runtime/planning-extra-ops.test.ts +43 -1
  167. package/src/runtime/planning-extra-ops.ts +96 -33
  168. package/src/runtime/session-briefing.test.ts +1 -0
  169. package/src/runtime/session-briefing.ts +10 -1
  170. package/src/runtime/types.ts +3 -0
  171. package/src/skills/sync-skills.ts +14 -7
  172. package/vitest.config.ts +1 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Admin / infrastructure operations — 8 ops for agent self-management.
2
+ * Admin / infrastructure operations — 11 ops for agent self-management.
3
3
  *
4
4
  * These ops let agents introspect their own health, configuration, and
5
5
  * runtime state. No new modules needed — uses existing runtime parts.
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import type { OpDefinition } from '../facades/types.js';
12
12
  import type { AgentRuntime } from './types.js';
13
13
  import { ENGINE_MODULE_MANIFEST } from '../engine/module-manifest.js';
14
+ import { discoverSkills } from '../skills/sync-skills.js';
14
15
 
15
16
  /**
16
17
  * Resolve the @soleri/core package.json version.
@@ -42,13 +43,13 @@ function getCoreVersion(): string {
42
43
  * Groups: health (1–2), introspection (4), diagnostics (2), mutation (1).
43
44
  */
44
45
  export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
45
- const { vault, brain, brainIntelligence, llmClient, curator } = runtime;
46
+ const { vault, brain, brainIntelligence, llmClient, curator, packInstaller } = runtime;
46
47
 
47
48
  return [
48
49
  // ─── Health ──────────────────────────────────────────────────────
49
50
  {
50
51
  name: 'admin_health',
51
- description: 'Comprehensive agent health check — vault, LLM, brain status.',
52
+ description: 'Comprehensive agent health check — vault, LLM, brain, skills, hooks status.',
52
53
  auth: 'read',
53
54
  handler: async () => {
54
55
  const vaultStats = vault.stats();
@@ -56,6 +57,24 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
56
57
  const brainStats = brain.getStats();
57
58
  const curatorStatus = curator.getStatus();
58
59
 
60
+ // Skills: agent-level + pack-level
61
+ const agentDir = runtime.config.agentDir;
62
+ const agentSkillsDirs = agentDir ? [join(agentDir, 'skills')] : [];
63
+ const agentSkills = discoverSkills(agentSkillsDirs);
64
+ const packs = packInstaller.list();
65
+ const packSkills = packs.flatMap((p) => p.skills);
66
+ const allSkillNames = [...agentSkills.map((s) => s.name), ...packSkills];
67
+
68
+ // Hooks: pack-level
69
+ const packHooks = packs.flatMap((p) => p.hooks);
70
+
71
+ // Tier breakdown
72
+ const tierCounts = { default: 0, community: 0, premium: 0 };
73
+ for (const pk of packs) {
74
+ const t = (pk.manifest as { tier?: string })?.tier ?? 'community';
75
+ if (t in tierCounts) tierCounts[t as keyof typeof tierCounts]++;
76
+ }
77
+
59
78
  return {
60
79
  status: 'ok',
61
80
  vault: { entries: vaultStats.totalEntries, domains: Object.keys(vaultStats.byDomain) },
@@ -65,6 +84,16 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
65
84
  feedbackCount: brainStats.feedbackCount,
66
85
  },
67
86
  curator: { initialized: curatorStatus.initialized },
87
+ skills: {
88
+ count: allSkillNames.length,
89
+ agent: agentSkills.map((s) => s.name),
90
+ packs: packSkills,
91
+ },
92
+ hooks: {
93
+ count: packHooks.length,
94
+ packs: packHooks,
95
+ },
96
+ packTiers: tierCounts,
68
97
  };
69
98
  },
70
99
  },
@@ -215,6 +244,39 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
215
244
  },
216
245
  },
217
246
 
247
+ // ─── Operator Context ───────────────────────────────────────────
248
+ {
249
+ name: 'operator_context_inspect',
250
+ description:
251
+ 'Inspect the full operator context profile — expertise, corrections, interests, patterns.',
252
+ auth: 'read',
253
+ handler: async () => {
254
+ const store = runtime.operatorContextStore;
255
+ if (!store) {
256
+ return { available: false, message: 'Operator context not configured' };
257
+ }
258
+ return { available: true, ...store.inspect() };
259
+ },
260
+ },
261
+ {
262
+ name: 'operator_context_delete',
263
+ description: 'Delete a specific item from the operator context profile.',
264
+ auth: 'write',
265
+ handler: async (params) => {
266
+ const store = runtime.operatorContextStore;
267
+ if (!store) {
268
+ return { deleted: false, message: 'Operator context not configured' };
269
+ }
270
+ const type = params.type as string;
271
+ const id = params.id as string;
272
+ const deleted = store.deleteItem(type as Parameters<typeof store.deleteItem>[0], id);
273
+ if (deleted) {
274
+ return { deleted: true, type, id };
275
+ }
276
+ return { deleted: false, message: 'Item not found' };
277
+ },
278
+ },
279
+
218
280
  // ─── Diagnostics ─────────────────────────────────────────────────
219
281
  {
220
282
  name: 'admin_diagnostic',
@@ -301,6 +363,45 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
301
363
  });
302
364
  }
303
365
 
366
+ // 7. Skills
367
+ try {
368
+ const agentDir = runtime.config.agentDir;
369
+ const skillsDirs = agentDir ? [join(agentDir, 'skills')] : [];
370
+ const agentSkills = discoverSkills(skillsDirs);
371
+ const installedPacks = packInstaller.list();
372
+ const packSkillCount = installedPacks.reduce((sum, p) => sum + p.skills.length, 0);
373
+ const totalSkills = agentSkills.length + packSkillCount;
374
+ const skillStatus = totalSkills > 0 ? 'ok' : agentDir ? 'warn' : 'ok';
375
+ checks.push({
376
+ name: 'skills',
377
+ status: skillStatus,
378
+ detail: `${totalSkills} skills (${agentSkills.length} agent, ${packSkillCount} pack)`,
379
+ });
380
+ } catch (err) {
381
+ checks.push({
382
+ name: 'skills',
383
+ status: 'error',
384
+ detail: err instanceof Error ? err.message : String(err),
385
+ });
386
+ }
387
+
388
+ // 8. Hooks
389
+ try {
390
+ const installedPacks = packInstaller.list();
391
+ const packHookCount = installedPacks.reduce((sum, p) => sum + p.hooks.length, 0);
392
+ checks.push({
393
+ name: 'hooks',
394
+ status: 'ok',
395
+ detail: `${packHookCount} hooks from ${installedPacks.length} packs`,
396
+ });
397
+ } catch (err) {
398
+ checks.push({
399
+ name: 'hooks',
400
+ status: 'error',
401
+ detail: err instanceof Error ? err.message : String(err),
402
+ });
403
+ }
404
+
304
405
  const errorCount = checks.filter((c) => c.status === 'error').length;
305
406
  const warnCount = checks.filter((c) => c.status === 'warn').length;
306
407
  const overall = errorCount > 0 ? 'unhealthy' : warnCount > 0 ? 'degraded' : 'healthy';
@@ -99,7 +99,7 @@ function discoverHookifyFiles(dir: string): Array<{ name: string; path: string }
99
99
  // ─── Settings.json Hook Merging ───────────────────────────────────────
100
100
 
101
101
  interface SettingsHook {
102
- type: 'prompt' | 'agent';
102
+ type: 'prompt' | 'agent' | 'command';
103
103
  prompt?: string;
104
104
  command?: string;
105
105
  timeout?: number;
@@ -110,6 +110,17 @@ interface SettingsHookGroup {
110
110
  hooks: SettingsHook[];
111
111
  }
112
112
 
113
+ /**
114
+ * Build a shell command that checks if the agent's MCP server is configured
115
+ * in the current project's .mcp.json before emitting an instruction.
116
+ * This ensures hooks are globally safe — they no-op in projects without the agent.
117
+ */
118
+ function buildConditionalHookCommand(agentId: string, instruction: string): string {
119
+ // Escape single quotes in instruction for safe shell embedding
120
+ const escaped = instruction.replace(/'/g, "'\\''");
121
+ return `root=$(git rev-parse --show-toplevel 2>/dev/null || echo "."); if grep -q '"${agentId}"' "$root/.mcp.json" 2>/dev/null; then echo '${escaped}'; fi`;
122
+ }
123
+
113
124
  /** Default lifecycle hooks for any Soleri agent */
114
125
  function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookGroup[]> {
115
126
  const marker = `mcp__${agentId}__${agentId}_`;
@@ -120,9 +131,12 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
120
131
  matcher: '',
121
132
  hooks: [
122
133
  {
123
- type: 'prompt',
124
- prompt: `Call ${marker}admin op:admin_health to verify agent is ready. Do not show the result unless there are errors.`,
125
- timeout: 15000,
134
+ type: 'command',
135
+ command: buildConditionalHookCommand(
136
+ agentId,
137
+ `Call ${marker}admin op:admin_health to verify agent is ready. Do not show the result unless there are errors.`,
138
+ ),
139
+ timeout: 5000,
126
140
  },
127
141
  ],
128
142
  },
@@ -132,9 +146,12 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
132
146
  matcher: '',
133
147
  hooks: [
134
148
  {
135
- type: 'agent',
136
- prompt: `Call ${marker}memory op:session_capture with a brief summary of the current session before context is compacted.`,
137
- timeout: 30000,
149
+ type: 'command',
150
+ command: buildConditionalHookCommand(
151
+ agentId,
152
+ `First, call ${marker}plan op:plan_close_stale params:{ olderThanMs: 0 } to auto-close any plans still in non-terminal states. Then call ${marker}memory op:session_capture with a brief summary of the current session before context is compacted. Include any auto-closed plan IDs in the summary.`,
153
+ ),
154
+ timeout: 10000,
138
155
  },
139
156
  ],
140
157
  },
@@ -144,9 +161,12 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
144
161
  matcher: '',
145
162
  hooks: [
146
163
  {
147
- type: 'agent',
148
- prompt: `Call ${marker}memory op:session_capture with a structured summary of what was accomplished, then check ${marker}loop op:loop_status — if a loop is active, remind the user.`,
149
- timeout: 30000,
164
+ type: 'command',
165
+ command: buildConditionalHookCommand(
166
+ agentId,
167
+ `First, call ${marker}plan op:plan_close_stale params:{ olderThanMs: 0 } to auto-close any plans still in non-terminal states. Then call ${marker}memory op:session_capture with a structured summary of what was accomplished, including any auto-closed plan IDs. Finally check ${marker}loop op:loop_status — if a loop is active, remind the user.`,
168
+ ),
169
+ timeout: 10000,
150
170
  },
151
171
  ],
152
172
  },
@@ -37,6 +37,19 @@ function createMockRuntime(): AgentRuntime {
37
37
  entry: { id: 'captured-1' },
38
38
  })),
39
39
  intelligentSearch: vi.fn(async () => [{ id: 'r1', title: 'Result 1', score: 0.8 }]),
40
+ scanSearch: vi.fn(async () => [
41
+ {
42
+ id: 'r1',
43
+ title: 'Result 1',
44
+ score: 0.8,
45
+ snippet: 'Short desc...',
46
+ tokenEstimate: 50,
47
+ type: 'pattern',
48
+ domain: 'test',
49
+ severity: 'suggestion',
50
+ tags: ['test'],
51
+ },
52
+ ]),
40
53
  recordFeedback: vi.fn(),
41
54
  },
42
55
  governance: {
@@ -365,6 +378,77 @@ describe('createCaptureOps', () => {
365
378
  })) as Array<Record<string, unknown>>;
366
379
  expect((result[0].score as number) >= (result[1].score as number)).toBe(true);
367
380
  });
381
+
382
+ it('scan mode calls brain.scanSearch instead of intelligentSearch', async () => {
383
+ const result = (await findOp(ops, 'search_intelligent').handler({
384
+ query: 'test patterns',
385
+ mode: 'scan',
386
+ })) as Array<Record<string, unknown>>;
387
+ expect(runtime.brain.scanSearch).toHaveBeenCalledWith(
388
+ 'test patterns',
389
+ expect.objectContaining({ limit: 10 }),
390
+ );
391
+ expect(runtime.brain.intelligentSearch).not.toHaveBeenCalled();
392
+ expect(Array.isArray(result)).toBe(true);
393
+ expect(result[0].source).toBe('vault');
394
+ expect(result[0].snippet).toBeDefined();
395
+ });
396
+
397
+ it('scan mode defaults limit to 10', async () => {
398
+ await findOp(ops, 'search_intelligent').handler({
399
+ query: 'test',
400
+ mode: 'scan',
401
+ });
402
+ expect(runtime.brain.scanSearch).toHaveBeenCalledWith(
403
+ 'test',
404
+ expect.objectContaining({ limit: 10 }),
405
+ );
406
+ });
407
+
408
+ it('full mode (default) still uses intelligentSearch with limit 20', async () => {
409
+ await findOp(ops, 'search_intelligent').handler({
410
+ query: 'test',
411
+ });
412
+ expect(runtime.brain.intelligentSearch).toHaveBeenCalledWith(
413
+ 'test',
414
+ expect.objectContaining({ limit: 20 }),
415
+ );
416
+ expect(runtime.brain.scanSearch).not.toHaveBeenCalled();
417
+ });
418
+
419
+ it('scan mode with includeMemories returns lightweight memory results', async () => {
420
+ vi.mocked(runtime.vault.searchMemories).mockReturnValue([
421
+ {
422
+ id: 'm1',
423
+ summary:
424
+ 'A long memory summary that should be truncated to 120 chars for scan mode lightweight results test',
425
+ context: 'Auth context',
426
+ },
427
+ ] as unknown);
428
+ const result = (await findOp(ops, 'search_intelligent').handler({
429
+ query: 'auth',
430
+ mode: 'scan',
431
+ includeMemories: true,
432
+ })) as Array<Record<string, unknown>>;
433
+ const memResult = result.find((r) => r.source === 'memory');
434
+ expect(memResult).toBeDefined();
435
+ expect(memResult!.id).toBe('m1');
436
+ expect(memResult!.snippet).toBeDefined();
437
+ expect(typeof memResult!.snippet).toBe('string');
438
+ // Should NOT have full memory fields
439
+ expect(memResult!.filesModified).toBeUndefined();
440
+ expect(memResult!.toolsUsed).toBeUndefined();
441
+ });
442
+
443
+ it('scan mode handles search failure gracefully', async () => {
444
+ vi.mocked(runtime.brain.scanSearch).mockRejectedValue(new Error('Scan failed'));
445
+ const result = (await findOp(ops, 'search_intelligent').handler({
446
+ query: 'anything',
447
+ mode: 'scan',
448
+ })) as Array<Record<string, unknown>>;
449
+ expect(Array.isArray(result)).toBe(true);
450
+ expect(result).toHaveLength(0);
451
+ });
368
452
  });
369
453
 
370
454
  describe('search_feedback', () => {
@@ -484,28 +484,42 @@ export function createCaptureOps(runtime: AgentRuntime): OpDefinition[] {
484
484
  {
485
485
  name: 'search_intelligent',
486
486
  description:
487
- 'Project-scoped intelligent search combining vault FTS, brain TF-IDF ranking, and optional memory search.',
487
+ 'Project-scoped intelligent search combining vault FTS, brain TF-IDF ranking, and optional memory search. mode:"scan" returns lightweight results (titles + scores + snippets) for two-pass retrieval. mode:"full" (default) returns complete entries.',
488
488
  auth: 'read',
489
489
  schema: z.object({
490
490
  query: z.string(),
491
491
  projectPath: z.string().optional(),
492
492
  domain: z.string().optional(),
493
493
  type: z.string().optional(),
494
- limit: z.number().optional().default(20),
494
+ limit: z.number().optional(),
495
495
  includeMemories: z.boolean().optional().default(false),
496
+ mode: z
497
+ .enum(['full', 'scan'])
498
+ .optional()
499
+ .default('full')
500
+ .describe(
501
+ 'full = complete entries with scoring breakdowns, scan = lightweight titles + scores for two-pass retrieval',
502
+ ),
496
503
  }),
497
504
  handler: async (params) => {
498
505
  const query = params.query as string;
499
506
  const domain = params.domain as string | undefined;
500
507
  const type = params.type as string | undefined;
501
- const limit = (params.limit as number | undefined) ?? 20;
508
+ const mode = (params.mode as string | undefined) ?? 'full';
509
+ const isScan = mode === 'scan';
510
+ const limit = (params.limit as number | undefined) ?? (isScan ? 10 : 20);
502
511
  const includeMemories = (params.includeMemories as boolean | undefined) ?? false;
503
512
 
504
- // Search vault via brain's intelligent search (TF-IDF ranked)
513
+ // Search vault scan mode returns lightweight results, full returns complete entries
505
514
  let vaultResults: Array<{ source: string; [key: string]: unknown }> = [];
506
515
  try {
507
- const ranked = await brain.intelligentSearch(query, { domain, type, limit });
508
- vaultResults = ranked.map((r) => ({ ...r, source: 'vault' }));
516
+ if (isScan) {
517
+ const scanned = await brain.scanSearch(query, { domain, type, limit });
518
+ vaultResults = scanned.map((r) => ({ ...r, source: 'vault' }));
519
+ } else {
520
+ const ranked = await brain.intelligentSearch(query, { domain, type, limit });
521
+ vaultResults = ranked.map((r) => ({ ...r, source: 'vault' }));
522
+ }
509
523
  } catch {
510
524
  // Graceful degradation — return empty vault results
511
525
  }
@@ -515,7 +529,21 @@ export function createCaptureOps(runtime: AgentRuntime): OpDefinition[] {
515
529
  if (includeMemories) {
516
530
  try {
517
531
  const memories = vault.searchMemories(query, { limit });
518
- memoryResults = memories.map((m) => ({ ...m, source: 'memory', score: 0.5 }));
532
+ if (isScan) {
533
+ // Lightweight memory results for scan mode
534
+ memoryResults = memories.map((m) => {
535
+ const desc = m.summary ?? '';
536
+ return {
537
+ id: m.id,
538
+ title: m.context ?? '',
539
+ snippet: desc.slice(0, 120) + (desc.length > 120 ? '...' : ''),
540
+ score: 0.5,
541
+ source: 'memory',
542
+ };
543
+ });
544
+ } else {
545
+ memoryResults = memories.map((m) => ({ ...m, source: 'memory', score: 0.5 }));
546
+ }
519
547
  } catch {
520
548
  // Graceful degradation — return empty memory results
521
549
  }
@@ -84,7 +84,7 @@ function mockRuntime(): AgentRuntime {
84
84
  pluginRegistry: {
85
85
  get: vi.fn(),
86
86
  },
87
- packInstaller: {},
87
+ packInstaller: { list: vi.fn(() => []) },
88
88
  createdAt: Date.now() - 60000,
89
89
  persona: { name: 'TestAgent' },
90
90
  } as unknown as AgentRuntime;
@@ -466,13 +466,16 @@ export function createBrainFacadeOps(runtime: AgentRuntime): OpDefinition[] {
466
466
  },
467
467
  {
468
468
  name: 'radar_dismiss',
469
- description: 'Dismiss a pending radar candidate — marks it as not worth capturing.',
469
+ description:
470
+ 'Dismiss one or more pending radar candidates — marks them as not worth capturing. Accepts a single ID or an array.',
470
471
  auth: 'write',
471
472
  schema: z.object({
472
- candidateId: z.number().describe('Radar candidate ID to dismiss'),
473
+ candidateId: z
474
+ .union([z.number(), z.array(z.number())])
475
+ .describe('Radar candidate ID(s) to dismiss — single number or array'),
473
476
  }),
474
477
  handler: async (params) => {
475
- return learningRadar.dismiss(params.candidateId as number);
478
+ return learningRadar.dismiss(params.candidateId as number | number[]);
476
479
  },
477
480
  },
478
481
  {
@@ -118,11 +118,19 @@ export function createControlFacadeOps(runtime: AgentRuntime): OpDefinition[] {
118
118
  mode: z
119
119
  .string()
120
120
  .describe(
121
- 'The operational mode to switch to. Valid modes: BUILD-MODE, FIX-MODE, VALIDATE-MODE, DESIGN-MODE, IMPROVE-MODE, DELIVER-MODE, EXPLORE-MODE, PLAN-MODE, REVIEW-MODE, GENERAL-MODE. Use "reset" to return to GENERAL-MODE.',
121
+ 'The operational mode to switch to. Valid modes: BUILD-MODE, FIX-MODE, VALIDATE-MODE, DESIGN-MODE, IMPROVE-MODE, DELIVER-MODE, EXPLORE-MODE, PLAN-MODE, REVIEW-MODE, GENERAL-MODE, YOLO-MODE. Use "reset" to return to GENERAL-MODE.',
122
+ ),
123
+ hookPackInstalled: z
124
+ .boolean()
125
+ .optional()
126
+ .describe(
127
+ 'Whether the yolo-safety hook pack is installed. Required for YOLO-MODE activation. The CLI layer should check for the hook pack before calling morph.',
122
128
  ),
123
129
  }),
124
130
  handler: async (params) => {
125
- return intentRouter.morph(params.mode as OperationalMode);
131
+ return intentRouter.morph(params.mode as OperationalMode, {
132
+ hookPackInstalled: params.hookPackInstalled as boolean | undefined,
133
+ });
126
134
  },
127
135
  },
128
136
  {
@@ -36,6 +36,24 @@ export function createCuratorFacadeOps(runtime: AgentRuntime): OpDefinition[] {
36
36
  );
37
37
  },
38
38
  },
39
+ {
40
+ name: 'curator_dismiss_duplicate',
41
+ description:
42
+ 'Dismiss a flagged duplicate pair — marks two entries as reviewed and intentionally distinct. They will no longer appear in curator_detect_duplicates results.',
43
+ auth: 'write',
44
+ schema: z.object({
45
+ entryIdA: z.string().describe('First entry ID'),
46
+ entryIdB: z.string().describe('Second entry ID'),
47
+ reason: z.string().optional().describe('Why these are not duplicates'),
48
+ }),
49
+ handler: async (params) => {
50
+ return curator.dismissDuplicate(
51
+ params.entryIdA as string,
52
+ params.entryIdB as string,
53
+ params.reason as string | undefined,
54
+ );
55
+ },
56
+ },
39
57
  {
40
58
  name: 'curator_contradictions',
41
59
  description: 'List or detect contradictions between patterns and anti-patterns.',
@@ -67,7 +67,8 @@ describe('memory-facade', () => {
67
67
  it('memory_search returns empty for no matches', async () => {
68
68
  const result = await executeOp(ops, 'memory_search', { query: 'nonexistent' });
69
69
  expect(result.success).toBe(true);
70
- expect((result.data as Record<string, unknown>).total).toBe(0);
70
+ const data = result.data as unknown[];
71
+ expect(data.length).toBe(0);
71
72
  });
72
73
 
73
74
  it('memory_search finds captured memories', async () => {
@@ -79,7 +80,8 @@ describe('memory-facade', () => {
79
80
  });
80
81
  const result = await executeOp(ops, 'memory_search', { query: 'token migration' });
81
82
  expect(result.success).toBe(true);
82
- expect((result.data as Record<string, unknown>).total).toBeGreaterThanOrEqual(1);
83
+ const data = result.data as unknown[];
84
+ expect(data.length).toBeGreaterThanOrEqual(1);
83
85
  });
84
86
 
85
87
  it('memory_search returns summaries by default', async () => {
@@ -91,9 +93,9 @@ describe('memory-facade', () => {
91
93
  });
92
94
  const result = await executeOp(ops, 'memory_search', { query: 'short summary' });
93
95
  expect(result.success).toBe(true);
94
- const data = result.data as { results: Array<{ summary: string }> };
95
- expect(data.results[0]).toHaveProperty('summary');
96
- expect(data.results[0]).not.toHaveProperty('context');
96
+ const data = result.data as Array<{ summary: string }>;
97
+ expect(data[0]).toHaveProperty('summary');
98
+ expect(data[0]).not.toHaveProperty('context');
97
99
  });
98
100
 
99
101
  it('memory_search returns full objects with verbose:true', async () => {
@@ -105,8 +107,8 @@ describe('memory-facade', () => {
105
107
  });
106
108
  const result = await executeOp(ops, 'memory_search', { query: 'verbose test', verbose: true });
107
109
  expect(result.success).toBe(true);
108
- const data = result.data as { results: Array<Record<string, unknown>> };
109
- expect(data.results[0]).toHaveProperty('context');
110
+ const data = result.data as Array<Record<string, unknown>>;
111
+ expect(data[0]).toHaveProperty('context');
110
112
  });
111
113
 
112
114
  // ─── memory_list ───────────────────────────────────────────────
@@ -120,9 +122,9 @@ describe('memory-facade', () => {
120
122
  });
121
123
  const result = await executeOp(ops, 'memory_list', {});
122
124
  expect(result.success).toBe(true);
123
- const data = result.data as { entries: unknown[]; total: number };
124
- expect(data.total).toBeGreaterThanOrEqual(1);
125
- expect(data.entries.length).toBeGreaterThanOrEqual(1);
125
+ const data = result.data as { memories: unknown[]; stats: { total: number } };
126
+ expect(data.stats.total).toBeGreaterThanOrEqual(1);
127
+ expect(data.memories.length).toBeGreaterThanOrEqual(1);
126
128
  });
127
129
 
128
130
  it('memory_list filters by type', async () => {
@@ -140,8 +142,8 @@ describe('memory-facade', () => {
140
142
  });
141
143
  const result = await executeOp(ops, 'memory_list', { type: 'lesson' });
142
144
  expect(result.success).toBe(true);
143
- const data = result.data as { entries: unknown[]; total: number };
144
- expect(data.entries.length).toBe(1);
145
+ const data = result.data as { memories: unknown[]; stats: Record<string, unknown> };
146
+ expect(data.memories.length).toBe(1);
145
147
  });
146
148
 
147
149
  it('memory_list verbose returns full objects', async () => {
@@ -45,17 +45,15 @@ export function createMemoryFacadeOps(runtime: AgentRuntime): OpDefinition[] {
45
45
  limit: (params.limit as number) ?? 10,
46
46
  });
47
47
  if (params.verbose) {
48
- return { results: memories, total: memories.length };
48
+ return memories;
49
49
  }
50
- return {
51
- results: memories.map((m) => ({
52
- id: m.id,
53
- summary: truncateSummary(m.summary || m.context),
54
- score: null,
55
- project: m.projectPath,
56
- })),
57
- total: memories.length,
58
- };
50
+ return memories.map((m) => ({
51
+ id: m.id,
52
+ type: m.type,
53
+ summary: truncateSummary(m.summary || m.context),
54
+ score: null,
55
+ project: m.projectPath,
56
+ }));
59
57
  },
60
58
  },
61
59
  {
@@ -117,13 +115,13 @@ export function createMemoryFacadeOps(runtime: AgentRuntime): OpDefinition[] {
117
115
  return { memories, stats };
118
116
  }
119
117
  return {
120
- entries: memories.map((m) => ({
118
+ memories: memories.map((m) => ({
121
119
  id: m.id,
122
120
  summary: truncateSummary(m.summary || m.context),
123
121
  project: m.projectPath,
124
122
  createdAt: m.createdAt,
125
123
  })),
126
- total: stats.total,
124
+ stats,
127
125
  };
128
126
  },
129
127
  },
@@ -25,7 +25,9 @@ export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[
25
25
  name: z.string().optional().describe('Project display name (derived from path if omitted)'),
26
26
  }),
27
27
  handler: async (params) => {
28
- const { resolve } = await import('node:path');
28
+ const { resolve, join } = await import('node:path');
29
+ const { homedir } = await import('node:os');
30
+ const { existsSync, readdirSync, statSync } = await import('node:fs');
29
31
  const projectPath = resolve((params.projectPath as string) ?? '.');
30
32
  const project = vault.registerProject(projectPath, params.name as string | undefined);
31
33
  // Also track in project registry for cross-project features
@@ -40,6 +42,35 @@ export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[
40
42
  const proposalStats = governance.getProposalStats(projectPath);
41
43
  const quotaStatus = governance.getQuotaStatus(projectPath);
42
44
 
45
+ // Check for stale staging backups (lightweight — stat only, no tree walk)
46
+ let stagingWarning: { count: number; message: string } | undefined;
47
+ try {
48
+ const stagingRoot = join(homedir(), '.soleri', 'staging');
49
+ if (existsSync(stagingRoot)) {
50
+ const maxAgeMs = 7 * 24 * 60 * 60 * 1000; // 7 days
51
+ const cutoff = Date.now() - maxAgeMs;
52
+ const dirs = readdirSync(stagingRoot, { withFileTypes: true });
53
+ let staleCount = 0;
54
+ for (const dir of dirs) {
55
+ if (!dir.isDirectory()) continue;
56
+ try {
57
+ const st = statSync(join(stagingRoot, dir.name));
58
+ if (st.mtimeMs < cutoff) staleCount++;
59
+ } catch {
60
+ // skip unreadable entries
61
+ }
62
+ }
63
+ if (staleCount > 0) {
64
+ stagingWarning = {
65
+ count: staleCount,
66
+ message: `${staleCount} staging backup(s) older than 7 days. Run: soleri staging cleanup --yes`,
67
+ };
68
+ }
69
+ }
70
+ } catch {
71
+ // Non-critical — don't fail session start over staging check
72
+ }
73
+
43
74
  return {
44
75
  project,
45
76
  is_new: isNew,
@@ -56,6 +87,7 @@ export function createOrchestrateFacadeOps(runtime: AgentRuntime): OpDefinition[
56
87
  isQuotaWarning: quotaStatus.isWarning,
57
88
  expiredThisSession: expired,
58
89
  },
90
+ ...(stagingWarning ? { stagingWarning } : {}),
59
91
  };
60
92
  },
61
93
  },