@soleri/core 9.3.0 → 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 (177) 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.d.ts +2 -0
  28. package/dist/engine/module-manifest.d.ts.map +1 -1
  29. package/dist/engine/module-manifest.js +117 -2
  30. package/dist/engine/module-manifest.js.map +1 -1
  31. package/dist/engine/register-engine.d.ts +9 -0
  32. package/dist/engine/register-engine.d.ts.map +1 -1
  33. package/dist/engine/register-engine.js +59 -1
  34. package/dist/engine/register-engine.js.map +1 -1
  35. package/dist/facades/types.d.ts +5 -1
  36. package/dist/facades/types.d.ts.map +1 -1
  37. package/dist/facades/types.js.map +1 -1
  38. package/dist/index.d.ts +6 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/operator/operator-context-store.d.ts +54 -0
  43. package/dist/operator/operator-context-store.d.ts.map +1 -0
  44. package/dist/operator/operator-context-store.js +434 -0
  45. package/dist/operator/operator-context-store.js.map +1 -0
  46. package/dist/operator/operator-context-types.d.ts +101 -0
  47. package/dist/operator/operator-context-types.d.ts.map +1 -0
  48. package/dist/operator/operator-context-types.js +27 -0
  49. package/dist/operator/operator-context-types.js.map +1 -0
  50. package/dist/packs/index.d.ts +2 -2
  51. package/dist/packs/index.d.ts.map +1 -1
  52. package/dist/packs/index.js +1 -1
  53. package/dist/packs/index.js.map +1 -1
  54. package/dist/packs/lockfile.d.ts +3 -0
  55. package/dist/packs/lockfile.d.ts.map +1 -1
  56. package/dist/packs/lockfile.js.map +1 -1
  57. package/dist/packs/types.d.ts +8 -2
  58. package/dist/packs/types.d.ts.map +1 -1
  59. package/dist/packs/types.js +6 -0
  60. package/dist/packs/types.js.map +1 -1
  61. package/dist/planning/plan-lifecycle.d.ts +12 -1
  62. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  63. package/dist/planning/plan-lifecycle.js +52 -19
  64. package/dist/planning/plan-lifecycle.js.map +1 -1
  65. package/dist/planning/planner-types.d.ts +6 -0
  66. package/dist/planning/planner-types.d.ts.map +1 -1
  67. package/dist/planning/planner.d.ts +21 -1
  68. package/dist/planning/planner.d.ts.map +1 -1
  69. package/dist/planning/planner.js +62 -3
  70. package/dist/planning/planner.js.map +1 -1
  71. package/dist/planning/task-complexity-assessor.d.ts +42 -0
  72. package/dist/planning/task-complexity-assessor.d.ts.map +1 -0
  73. package/dist/planning/task-complexity-assessor.js +132 -0
  74. package/dist/planning/task-complexity-assessor.js.map +1 -0
  75. package/dist/plugins/types.d.ts +18 -18
  76. package/dist/runtime/admin-ops.d.ts +1 -1
  77. package/dist/runtime/admin-ops.d.ts.map +1 -1
  78. package/dist/runtime/admin-ops.js +118 -3
  79. package/dist/runtime/admin-ops.js.map +1 -1
  80. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  81. package/dist/runtime/admin-setup-ops.js +19 -9
  82. package/dist/runtime/admin-setup-ops.js.map +1 -1
  83. package/dist/runtime/capture-ops.d.ts.map +1 -1
  84. package/dist/runtime/capture-ops.js +35 -7
  85. package/dist/runtime/capture-ops.js.map +1 -1
  86. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  87. package/dist/runtime/facades/brain-facade.js +4 -2
  88. package/dist/runtime/facades/brain-facade.js.map +1 -1
  89. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  90. package/dist/runtime/facades/control-facade.js +8 -2
  91. package/dist/runtime/facades/control-facade.js.map +1 -1
  92. package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
  93. package/dist/runtime/facades/curator-facade.js +13 -0
  94. package/dist/runtime/facades/curator-facade.js.map +1 -1
  95. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  96. package/dist/runtime/facades/memory-facade.js +10 -12
  97. package/dist/runtime/facades/memory-facade.js.map +1 -1
  98. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  99. package/dist/runtime/facades/orchestrate-facade.js +36 -1
  100. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  101. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  102. package/dist/runtime/facades/plan-facade.js +20 -4
  103. package/dist/runtime/facades/plan-facade.js.map +1 -1
  104. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  105. package/dist/runtime/orchestrate-ops.js +109 -31
  106. package/dist/runtime/orchestrate-ops.js.map +1 -1
  107. package/dist/runtime/plan-feedback-helper.d.ts +21 -0
  108. package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
  109. package/dist/runtime/plan-feedback-helper.js +52 -0
  110. package/dist/runtime/plan-feedback-helper.js.map +1 -0
  111. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  112. package/dist/runtime/planning-extra-ops.js +73 -34
  113. package/dist/runtime/planning-extra-ops.js.map +1 -1
  114. package/dist/runtime/session-briefing.d.ts.map +1 -1
  115. package/dist/runtime/session-briefing.js +9 -1
  116. package/dist/runtime/session-briefing.js.map +1 -1
  117. package/dist/runtime/types.d.ts +3 -0
  118. package/dist/runtime/types.d.ts.map +1 -1
  119. package/dist/skills/sync-skills.d.ts.map +1 -1
  120. package/dist/skills/sync-skills.js +13 -7
  121. package/dist/skills/sync-skills.js.map +1 -1
  122. package/package.json +1 -1
  123. package/src/brain/brain-intelligence.test.ts +30 -0
  124. package/src/brain/extraction-quality.test.ts +323 -0
  125. package/src/brain/intelligence.ts +133 -30
  126. package/src/brain/learning-radar.ts +8 -5
  127. package/src/brain/second-brain-features.test.ts +1 -1
  128. package/src/control/intent-router.test.ts +73 -3
  129. package/src/control/intent-router.ts +38 -1
  130. package/src/control/types.ts +13 -2
  131. package/src/curator/curator.test.ts +92 -0
  132. package/src/curator/curator.ts +29 -1
  133. package/src/curator/schema.ts +8 -0
  134. package/src/domain-packs/types.ts +8 -0
  135. package/src/engine/module-manifest.test.ts +51 -2
  136. package/src/engine/module-manifest.ts +119 -2
  137. package/src/engine/register-engine.test.ts +73 -1
  138. package/src/engine/register-engine.ts +61 -1
  139. package/src/facades/types.ts +5 -0
  140. package/src/index.ts +30 -0
  141. package/src/operator/operator-context-store.test.ts +698 -0
  142. package/src/operator/operator-context-store.ts +569 -0
  143. package/src/operator/operator-context-types.ts +139 -0
  144. package/src/packs/index.ts +3 -1
  145. package/src/packs/lockfile.ts +3 -0
  146. package/src/packs/types.ts +9 -0
  147. package/src/planning/plan-lifecycle.ts +80 -22
  148. package/src/planning/planner-types.ts +6 -0
  149. package/src/planning/planner.ts +74 -4
  150. package/src/planning/task-complexity-assessor.test.ts +302 -0
  151. package/src/planning/task-complexity-assessor.ts +180 -0
  152. package/src/runtime/admin-ops.test.ts +159 -3
  153. package/src/runtime/admin-ops.ts +123 -3
  154. package/src/runtime/admin-setup-ops.ts +30 -10
  155. package/src/runtime/capture-ops.test.ts +84 -0
  156. package/src/runtime/capture-ops.ts +35 -7
  157. package/src/runtime/facades/admin-facade.test.ts +1 -1
  158. package/src/runtime/facades/brain-facade.ts +6 -3
  159. package/src/runtime/facades/control-facade.ts +10 -2
  160. package/src/runtime/facades/curator-facade.ts +18 -0
  161. package/src/runtime/facades/memory-facade.test.ts +14 -12
  162. package/src/runtime/facades/memory-facade.ts +10 -12
  163. package/src/runtime/facades/orchestrate-facade.ts +33 -1
  164. package/src/runtime/facades/plan-facade.test.ts +213 -0
  165. package/src/runtime/facades/plan-facade.ts +23 -4
  166. package/src/runtime/orchestrate-ops.test.ts +404 -0
  167. package/src/runtime/orchestrate-ops.ts +129 -37
  168. package/src/runtime/plan-feedback-helper.test.ts +173 -0
  169. package/src/runtime/plan-feedback-helper.ts +63 -0
  170. package/src/runtime/planning-extra-ops.test.ts +43 -1
  171. package/src/runtime/planning-extra-ops.ts +96 -33
  172. package/src/runtime/session-briefing.test.ts +1 -0
  173. package/src/runtime/session-briefing.ts +10 -1
  174. package/src/runtime/types.ts +3 -0
  175. package/src/skills/sync-skills.ts +14 -7
  176. package/src/vault/vault-scaling.test.ts +5 -5
  177. package/vitest.config.ts +1 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Task Complexity Assessor — pure function that classifies tasks as simple or complex.
3
+ *
4
+ * Used by the planning module to decide whether a decomposed GH issue
5
+ * needs a full plan or can be executed directly.
6
+ */
7
+
8
+ // ─── Types ──────────────────────────────────────────────────────────
9
+
10
+ export interface AssessmentInput {
11
+ /** User's task description. */
12
+ prompt: string;
13
+ /** Estimated number of files to touch. */
14
+ filesEstimated?: number;
15
+ /** GH issue body if available. */
16
+ parentIssueContext?: string;
17
+ /** Whether the approach is already described in a parent plan. */
18
+ hasParentPlan?: boolean;
19
+ /** Which domains are involved. */
20
+ domains?: string[];
21
+ }
22
+
23
+ export interface AssessmentSignal {
24
+ name: string;
25
+ weight: number;
26
+ triggered: boolean;
27
+ detail: string;
28
+ }
29
+
30
+ export interface AssessmentResult {
31
+ classification: 'simple' | 'complex';
32
+ /** 0-100 complexity score. Threshold at 40. */
33
+ score: number;
34
+ signals: AssessmentSignal[];
35
+ /** One-line explanation. */
36
+ reasoning: string;
37
+ }
38
+
39
+ // ─── Signal Detectors ───────────────────────────────────────────────
40
+
41
+ const CROSS_CUTTING_PATTERNS = [
42
+ /\bauth(?:entication|orization)?\b/i,
43
+ /\bmigrat(?:e|ion|ing)\b/i,
44
+ /\brefactor(?:ing)?\s+across\b/i,
45
+ /\bcross[- ]cutting\b/i,
46
+ ];
47
+
48
+ const NEW_DEPENDENCY_PATTERNS = [
49
+ /\badd\s+dep(?:endency|endencies)?\b/i,
50
+ /\binstall\b/i,
51
+ /\bnew\s+package\b/i,
52
+ /\bnpm\s+install\b/i,
53
+ /\badd\s+(?:a\s+)?(?:new\s+)?(?:npm\s+)?package\b/i,
54
+ ];
55
+
56
+ const DESIGN_DECISION_PATTERNS = [
57
+ /\bhow\s+should\b/i,
58
+ /\bwhich\s+approach\b/i,
59
+ /\bdesign\s+decision\b/i,
60
+ /\barchitectur(?:e|al)\s+(?:decision|choice)\b/i,
61
+ /\btrade[- ]?off/i,
62
+ ];
63
+
64
+ function detectFileCount(input: AssessmentInput): AssessmentSignal {
65
+ const files = input.filesEstimated ?? 0;
66
+ const triggered = files >= 3;
67
+ return {
68
+ name: 'file-count',
69
+ weight: 25,
70
+ triggered,
71
+ detail: triggered
72
+ ? `Estimated ${files} files (≥3 threshold)`
73
+ : files > 0
74
+ ? `Estimated ${files} file${files === 1 ? '' : 's'} (under threshold)`
75
+ : 'No file estimate provided',
76
+ };
77
+ }
78
+
79
+ function detectCrossCutting(input: AssessmentInput): AssessmentSignal {
80
+ const text = input.prompt;
81
+ const match = CROSS_CUTTING_PATTERNS.find((p) => p.test(text));
82
+ return {
83
+ name: 'cross-cutting-keywords',
84
+ weight: 20,
85
+ triggered: !!match,
86
+ detail: match
87
+ ? `Detected cross-cutting keyword: "${text.match(match)?.[0]}"`
88
+ : 'No cross-cutting keywords detected',
89
+ };
90
+ }
91
+
92
+ function detectNewDependencies(input: AssessmentInput): AssessmentSignal {
93
+ const text = input.prompt;
94
+ const match = NEW_DEPENDENCY_PATTERNS.find((p) => p.test(text));
95
+ return {
96
+ name: 'new-dependencies',
97
+ weight: 15,
98
+ triggered: !!match,
99
+ detail: match
100
+ ? `Detected dependency signal: "${text.match(match)?.[0]}"`
101
+ : 'No new dependency signals detected',
102
+ };
103
+ }
104
+
105
+ function detectDesignDecisions(input: AssessmentInput): AssessmentSignal {
106
+ const text = input.prompt;
107
+ const match = DESIGN_DECISION_PATTERNS.find((p) => p.test(text));
108
+ return {
109
+ name: 'design-decisions-needed',
110
+ weight: 20,
111
+ triggered: !!match,
112
+ detail: match
113
+ ? `Detected design decision signal: "${text.match(match)?.[0]}"`
114
+ : 'No design decision signals detected',
115
+ };
116
+ }
117
+
118
+ function detectApproachDescribed(input: AssessmentInput): AssessmentSignal {
119
+ const hasContext = !!(input.hasParentPlan || input.parentIssueContext?.trim());
120
+ return {
121
+ name: 'approach-already-described',
122
+ weight: -15,
123
+ triggered: hasContext,
124
+ detail: hasContext
125
+ ? 'Approach already described in parent plan or issue'
126
+ : 'No pre-existing approach context',
127
+ };
128
+ }
129
+
130
+ function detectMultiDomain(input: AssessmentInput): AssessmentSignal {
131
+ const domains = input.domains ?? [];
132
+ const triggered = domains.length >= 2;
133
+ return {
134
+ name: 'multi-domain',
135
+ weight: 5,
136
+ triggered,
137
+ detail: triggered
138
+ ? `Involves ${domains.length} domains: ${domains.join(', ')}`
139
+ : domains.length === 1
140
+ ? `Single domain: ${domains[0]}`
141
+ : 'No domains specified',
142
+ };
143
+ }
144
+
145
+ // ─── Assessor ───────────────────────────────────────────────────────
146
+
147
+ const COMPLEXITY_THRESHOLD = 40;
148
+
149
+ /**
150
+ * Assess task complexity from structured input.
151
+ *
152
+ * Returns a classification (`simple` | `complex`), a numeric score (0-100),
153
+ * the individual signals that contributed, and a one-line reasoning string.
154
+ *
155
+ * Pure function — no side effects, no DB, no MCP calls.
156
+ */
157
+ export function assessTaskComplexity(input: AssessmentInput): AssessmentResult {
158
+ const signals: AssessmentSignal[] = [
159
+ detectFileCount(input),
160
+ detectCrossCutting(input),
161
+ detectNewDependencies(input),
162
+ detectDesignDecisions(input),
163
+ detectApproachDescribed(input),
164
+ detectMultiDomain(input),
165
+ ];
166
+
167
+ const rawScore = signals.reduce((sum, s) => sum + (s.triggered ? s.weight : 0), 0);
168
+
169
+ // Clamp to 0-100
170
+ const score = Math.max(0, Math.min(100, rawScore));
171
+ const classification = score >= COMPLEXITY_THRESHOLD ? 'complex' : 'simple';
172
+
173
+ const triggered = signals.filter((s) => s.triggered);
174
+ const reasoning =
175
+ triggered.length === 0
176
+ ? 'No complexity signals detected — treating as simple task'
177
+ : `${classification === 'complex' ? 'Complex' : 'Simple'}: ${triggered.map((s) => s.name).join(', ')} (score ${score})`;
178
+
179
+ return { classification, score, signals, reasoning };
180
+ }
@@ -35,6 +35,9 @@ function mockRuntime(): AgentRuntime {
35
35
  curator: {
36
36
  getStatus: vi.fn().mockReturnValue({ initialized: true }),
37
37
  },
38
+ packInstaller: {
39
+ list: vi.fn().mockReturnValue([]),
40
+ },
38
41
  contextHealth: {
39
42
  check: vi.fn().mockReturnValue({
40
43
  level: 'green',
@@ -67,8 +70,8 @@ describe('createAdminOps', () => {
67
70
  ops = createAdminOps(rt);
68
71
  });
69
72
 
70
- it('returns 9 ops', () => {
71
- expect(ops.length).toBe(9);
73
+ it('returns 11 ops', () => {
74
+ expect(ops.length).toBe(11);
72
75
  });
73
76
 
74
77
  // ─── admin_health ─────────────────────────────────────────────
@@ -98,6 +101,50 @@ describe('createAdminOps', () => {
98
101
  expect(llm.openai).toBe(true);
99
102
  expect(llm.anthropic).toBe(false);
100
103
  });
104
+
105
+ it('reports skills status', async () => {
106
+ const op = findOp(ops, 'admin_health');
107
+ const result = (await op.handler({})) as Record<string, unknown>;
108
+ const skills = result.skills as Record<string, unknown>;
109
+ expect(skills).toBeDefined();
110
+ expect(skills.count).toBe(0);
111
+ expect(skills.agent).toEqual([]);
112
+ expect(skills.packs).toEqual([]);
113
+ });
114
+
115
+ it('reports hooks status', async () => {
116
+ const op = findOp(ops, 'admin_health');
117
+ const result = (await op.handler({})) as Record<string, unknown>;
118
+ const hooks = result.hooks as Record<string, unknown>;
119
+ expect(hooks).toBeDefined();
120
+ expect(hooks.count).toBe(0);
121
+ expect(hooks.packs).toEqual([]);
122
+ });
123
+
124
+ it('includes pack skills and hooks when packs are installed', async () => {
125
+ vi.mocked(rt.packInstaller.list).mockReturnValue([
126
+ {
127
+ id: 'test-pack',
128
+ manifest: {} as never,
129
+ directory: '/tmp/pack',
130
+ status: 'installed',
131
+ vaultEntries: 5,
132
+ skills: ['my-skill', 'another-skill'],
133
+ hooks: ['my-hook'],
134
+ facadesRegistered: false,
135
+ installedAt: Date.now(),
136
+ },
137
+ ]);
138
+ const updatedOps = createAdminOps(rt);
139
+ const op = findOp(updatedOps, 'admin_health');
140
+ const result = (await op.handler({})) as Record<string, unknown>;
141
+ const skills = result.skills as Record<string, unknown>;
142
+ expect(skills.count).toBe(2);
143
+ expect(skills.packs).toEqual(['my-skill', 'another-skill']);
144
+ const hooks = result.hooks as Record<string, unknown>;
145
+ expect(hooks.count).toBe(1);
146
+ expect(hooks.packs).toEqual(['my-hook']);
147
+ });
101
148
  });
102
149
 
103
150
  // ─── context_health ───────────────────────────────────────────
@@ -133,6 +180,27 @@ describe('createAdminOps', () => {
133
180
  expect(grouped.vault).toContain('vault_search');
134
181
  });
135
182
 
183
+ it('returns routing hints in grouped mode', async () => {
184
+ const op = findOp(ops, 'admin_tool_list');
185
+ const allOps = [{ name: 'admin_health', description: 'Health check', auth: 'read' }];
186
+ const result = (await op.handler({ _allOps: allOps })) as Record<string, unknown>;
187
+ const routing = result.routing as Record<string, string>;
188
+ expect(routing).toBeDefined();
189
+ expect(typeof routing).toBe('object');
190
+ // Spot-check a few known intent signals
191
+ expect(routing['search knowledge']).toBe('vault.search_intelligent');
192
+ expect(routing['plan this']).toBe('plan.create_plan');
193
+ expect(routing['health check']).toBe('admin.admin_health');
194
+ });
195
+
196
+ it('returns routing hints in fallback mode', async () => {
197
+ const op = findOp(ops, 'admin_tool_list');
198
+ const result = (await op.handler({})) as Record<string, unknown>;
199
+ const routing = result.routing as Record<string, string>;
200
+ expect(routing).toBeDefined();
201
+ expect(Object.keys(routing).length).toBeGreaterThan(0);
202
+ });
203
+
136
204
  it('returns verbose format when verbose=true', async () => {
137
205
  const op = findOp(ops, 'admin_tool_list');
138
206
  const allOps = [{ name: 'admin_health', description: 'Health check', auth: 'read' }];
@@ -206,6 +274,94 @@ describe('createAdminOps', () => {
206
274
  });
207
275
  });
208
276
 
277
+ // ─── operator_context_inspect ────────────────────────────────
278
+
279
+ describe('operator_context_inspect', () => {
280
+ it('returns full profile when store is available', async () => {
281
+ const mockContext = {
282
+ expertise: [
283
+ {
284
+ topic: 'TypeScript',
285
+ level: 'expert',
286
+ confidence: 0.9,
287
+ sessionCount: 5,
288
+ lastObserved: Date.now(),
289
+ },
290
+ ],
291
+ corrections: [],
292
+ interests: [
293
+ { tag: 'testing', confidence: 0.7, mentionCount: 3, lastMentioned: Date.now() },
294
+ ],
295
+ patterns: [],
296
+ sessionCount: 5,
297
+ lastUpdated: Date.now(),
298
+ };
299
+ (rt as Record<string, unknown>).operatorContextStore = {
300
+ inspect: vi.fn().mockReturnValue(mockContext),
301
+ deleteItem: vi.fn(),
302
+ };
303
+ const updatedOps = createAdminOps(rt);
304
+ const op = findOp(updatedOps, 'operator_context_inspect');
305
+ const result = (await op.handler({})) as Record<string, unknown>;
306
+ expect(result.available).toBe(true);
307
+ expect(result.expertise).toEqual(mockContext.expertise);
308
+ expect(result.interests).toEqual(mockContext.interests);
309
+ });
310
+
311
+ it('returns not-available when store is missing', async () => {
312
+ // Default mock runtime has no operatorContextStore
313
+ const op = findOp(ops, 'operator_context_inspect');
314
+ const result = (await op.handler({})) as Record<string, unknown>;
315
+ expect(result.available).toBe(false);
316
+ expect(result.message).toBe('Operator context not configured');
317
+ });
318
+ });
319
+
320
+ // ─── operator_context_delete ───────────────────────────────────
321
+
322
+ describe('operator_context_delete', () => {
323
+ it('removes an item successfully', async () => {
324
+ (rt as Record<string, unknown>).operatorContextStore = {
325
+ inspect: vi.fn(),
326
+ deleteItem: vi.fn().mockReturnValue(true),
327
+ };
328
+ const updatedOps = createAdminOps(rt);
329
+ const op = findOp(updatedOps, 'operator_context_delete');
330
+ const result = (await op.handler({ type: 'expertise', id: 'abc-123' })) as Record<
331
+ string,
332
+ unknown
333
+ >;
334
+ expect(result.deleted).toBe(true);
335
+ expect(result.type).toBe('expertise');
336
+ expect(result.id).toBe('abc-123');
337
+ });
338
+
339
+ it('returns false for missing item', async () => {
340
+ (rt as Record<string, unknown>).operatorContextStore = {
341
+ inspect: vi.fn(),
342
+ deleteItem: vi.fn().mockReturnValue(false),
343
+ };
344
+ const updatedOps = createAdminOps(rt);
345
+ const op = findOp(updatedOps, 'operator_context_delete');
346
+ const result = (await op.handler({ type: 'pattern', id: 'nonexistent' })) as Record<
347
+ string,
348
+ unknown
349
+ >;
350
+ expect(result.deleted).toBe(false);
351
+ expect(result.message).toBe('Item not found');
352
+ });
353
+
354
+ it('returns not-available when store is missing', async () => {
355
+ const op = findOp(ops, 'operator_context_delete');
356
+ const result = (await op.handler({ type: 'expertise', id: 'abc' })) as Record<
357
+ string,
358
+ unknown
359
+ >;
360
+ expect(result.deleted).toBe(false);
361
+ expect(result.message).toBe('Operator context not configured');
362
+ });
363
+ });
364
+
209
365
  // ─── admin_diagnostic ─────────────────────────────────────────
210
366
 
211
367
  describe('admin_diagnostic', () => {
@@ -218,7 +374,7 @@ describe('createAdminOps', () => {
218
374
  expect(result).toHaveProperty('checks');
219
375
  expect(result).toHaveProperty('summary');
220
376
  const checks = result.checks as Array<Record<string, string>>;
221
- expect(checks.length).toBeGreaterThanOrEqual(5);
377
+ expect(checks.length).toBeGreaterThanOrEqual(8);
222
378
  });
223
379
 
224
380
  it('reports degraded when LLM unavailable', async () => {
@@ -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.
@@ -10,6 +10,8 @@ import { join, dirname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import type { OpDefinition } from '../facades/types.js';
12
12
  import type { AgentRuntime } from './types.js';
13
+ import { ENGINE_MODULE_MANIFEST } from '../engine/module-manifest.js';
14
+ import { discoverSkills } from '../skills/sync-skills.js';
13
15
 
14
16
  /**
15
17
  * Resolve the @soleri/core package.json version.
@@ -41,13 +43,13 @@ function getCoreVersion(): string {
41
43
  * Groups: health (1–2), introspection (4), diagnostics (2), mutation (1).
42
44
  */
43
45
  export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
44
- const { vault, brain, brainIntelligence, llmClient, curator } = runtime;
46
+ const { vault, brain, brainIntelligence, llmClient, curator, packInstaller } = runtime;
45
47
 
46
48
  return [
47
49
  // ─── Health ──────────────────────────────────────────────────────
48
50
  {
49
51
  name: 'admin_health',
50
- description: 'Comprehensive agent health check — vault, LLM, brain status.',
52
+ description: 'Comprehensive agent health check — vault, LLM, brain, skills, hooks status.',
51
53
  auth: 'read',
52
54
  handler: async () => {
53
55
  const vaultStats = vault.stats();
@@ -55,6 +57,24 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
55
57
  const brainStats = brain.getStats();
56
58
  const curatorStatus = curator.getStatus();
57
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
+
58
78
  return {
59
79
  status: 'ok',
60
80
  vault: { entries: vaultStats.totalEntries, domains: Object.keys(vaultStats.byDomain) },
@@ -64,6 +84,16 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
64
84
  feedbackCount: brainStats.feedbackCount,
65
85
  },
66
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,
67
97
  };
68
98
  },
69
99
  },
@@ -113,6 +143,7 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
113
143
  return {
114
144
  count: allOps.length,
115
145
  ops: grouped,
146
+ routing: buildRoutingHints(),
116
147
  };
117
148
  }
118
149
  // Fallback — just describe admin ops
@@ -130,6 +161,7 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
130
161
  'admin_diagnostic',
131
162
  ],
132
163
  },
164
+ routing: buildRoutingHints(),
133
165
  };
134
166
  },
135
167
  },
@@ -212,6 +244,39 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
212
244
  },
213
245
  },
214
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
+
215
280
  // ─── Diagnostics ─────────────────────────────────────────────────
216
281
  {
217
282
  name: 'admin_diagnostic',
@@ -298,6 +363,45 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
298
363
  });
299
364
  }
300
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
+
301
405
  const errorCount = checks.filter((c) => c.status === 'error').length;
302
406
  const warnCount = checks.filter((c) => c.status === 'warn').length;
303
407
  const overall = errorCount > 0 ? 'unhealthy' : warnCount > 0 ? 'degraded' : 'healthy';
@@ -321,6 +425,22 @@ function formatBytes(bytes: number): string {
321
425
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
322
426
  }
323
427
 
428
+ /**
429
+ * Build a flat routing map from ENGINE_MODULE_MANIFEST intentSignals.
430
+ * Keys are natural-language phrases, values are `{suffix}.{op}` paths.
431
+ */
432
+ function buildRoutingHints(): Record<string, string> {
433
+ const routing: Record<string, string> = {};
434
+ for (const mod of ENGINE_MODULE_MANIFEST) {
435
+ if (mod.intentSignals) {
436
+ for (const [phrase, op] of Object.entries(mod.intentSignals)) {
437
+ routing[phrase] = `${mod.suffix}.${op}`;
438
+ }
439
+ }
440
+ }
441
+ return routing;
442
+ }
443
+
324
444
  function formatUptime(seconds: number): string {
325
445
  if (seconds < 60) return `${seconds}s`;
326
446
  const minutes = Math.floor(seconds / 60);
@@ -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
  },