@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.
- package/dist/brain/intelligence.d.ts +5 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +115 -26
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/learning-radar.d.ts +3 -3
- package/dist/brain/learning-radar.d.ts.map +1 -1
- package/dist/brain/learning-radar.js +8 -4
- package/dist/brain/learning-radar.js.map +1 -1
- package/dist/control/intent-router.d.ts +2 -2
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +35 -1
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +10 -2
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +23 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/schema.d.ts +1 -1
- package/dist/curator/schema.d.ts.map +1 -1
- package/dist/curator/schema.js +8 -0
- package/dist/curator/schema.js.map +1 -1
- package/dist/domain-packs/types.d.ts +6 -0
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js +1 -0
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/module-manifest.d.ts +2 -0
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +117 -2
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts +9 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +59 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/facades/types.d.ts +5 -1
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/facades/types.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/operator/operator-context-store.d.ts +54 -0
- package/dist/operator/operator-context-store.d.ts.map +1 -0
- package/dist/operator/operator-context-store.js +434 -0
- package/dist/operator/operator-context-store.js.map +1 -0
- package/dist/operator/operator-context-types.d.ts +101 -0
- package/dist/operator/operator-context-types.d.ts.map +1 -0
- package/dist/operator/operator-context-types.js +27 -0
- package/dist/operator/operator-context-types.js.map +1 -0
- package/dist/packs/index.d.ts +2 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +3 -0
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/types.d.ts +8 -2
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +6 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts +12 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +52 -19
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +6 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +21 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +62 -3
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/task-complexity-assessor.d.ts +42 -0
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -0
- package/dist/planning/task-complexity-assessor.js +132 -0
- package/dist/planning/task-complexity-assessor.js.map +1 -0
- package/dist/plugins/types.d.ts +18 -18
- package/dist/runtime/admin-ops.d.ts +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +118 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +19 -9
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +35 -7
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +4 -2
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +8 -2
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +13 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +10 -12
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +36 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +20 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +109 -31
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/plan-feedback-helper.d.ts +21 -0
- package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
- package/dist/runtime/plan-feedback-helper.js +52 -0
- package/dist/runtime/plan-feedback-helper.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +73 -34
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +9 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +13 -7
- package/dist/skills/sync-skills.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain-intelligence.test.ts +30 -0
- package/src/brain/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +133 -30
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +38 -1
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +29 -1
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +51 -2
- package/src/engine/module-manifest.ts +119 -2
- package/src/engine/register-engine.test.ts +73 -1
- package/src/engine/register-engine.ts +61 -1
- package/src/facades/types.ts +5 -0
- package/src/index.ts +30 -0
- package/src/operator/operator-context-store.test.ts +698 -0
- package/src/operator/operator-context-store.ts +569 -0
- package/src/operator/operator-context-types.ts +139 -0
- package/src/packs/index.ts +3 -1
- package/src/packs/lockfile.ts +3 -0
- package/src/packs/types.ts +9 -0
- package/src/planning/plan-lifecycle.ts +80 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +302 -0
- package/src/planning/task-complexity-assessor.ts +180 -0
- package/src/runtime/admin-ops.test.ts +159 -3
- package/src/runtime/admin-ops.ts +123 -3
- package/src/runtime/admin-setup-ops.ts +30 -10
- package/src/runtime/capture-ops.test.ts +84 -0
- package/src/runtime/capture-ops.ts +35 -7
- package/src/runtime/facades/admin-facade.test.ts +1 -1
- package/src/runtime/facades/brain-facade.ts +6 -3
- package/src/runtime/facades/control-facade.ts +10 -2
- package/src/runtime/facades/curator-facade.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +10 -12
- package/src/runtime/facades/orchestrate-facade.ts +33 -1
- package/src/runtime/facades/plan-facade.test.ts +213 -0
- package/src/runtime/facades/plan-facade.ts +23 -4
- package/src/runtime/orchestrate-ops.test.ts +404 -0
- package/src/runtime/orchestrate-ops.ts +129 -37
- package/src/runtime/plan-feedback-helper.test.ts +173 -0
- package/src/runtime/plan-feedback-helper.ts +63 -0
- package/src/runtime/planning-extra-ops.test.ts +43 -1
- package/src/runtime/planning-extra-ops.ts +96 -33
- package/src/runtime/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/types.ts +3 -0
- package/src/skills/sync-skills.ts +14 -7
- package/src/vault/vault-scaling.test.ts +5 -5
- 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
|
|
71
|
-
expect(ops.length).toBe(
|
|
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(
|
|
377
|
+
expect(checks.length).toBeGreaterThanOrEqual(8);
|
|
222
378
|
});
|
|
223
379
|
|
|
224
380
|
it('reports degraded when LLM unavailable', async () => {
|
package/src/runtime/admin-ops.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Admin / infrastructure operations —
|
|
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: '
|
|
124
|
-
|
|
125
|
-
|
|
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: '
|
|
136
|
-
|
|
137
|
-
|
|
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: '
|
|
148
|
-
|
|
149
|
-
|
|
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
|
},
|