@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,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD tests for brain extraction quality (issue #359).
|
|
3
|
+
*
|
|
4
|
+
* These tests define the DESIRED behavior of extractKnowledge().
|
|
5
|
+
* They are expected to FAIL against the current implementation.
|
|
6
|
+
* Implementation fixes come in issues #360-#366.
|
|
7
|
+
*
|
|
8
|
+
* What's wrong today:
|
|
9
|
+
* - plan_completed rule produces generic "Successful plan: {id}" titles
|
|
10
|
+
* - Extraction rules never read session.context (objective, scope, decisions)
|
|
11
|
+
* - No dedup: same rule + sessionId can produce duplicate proposals
|
|
12
|
+
* - long_session rule fires with low-value noise (to be removed in #360)
|
|
13
|
+
* - No drift_detected rule exists yet (to be added in #366)
|
|
14
|
+
* - Confidence is not adjusted based on context richness
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
18
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
22
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
23
|
+
|
|
24
|
+
describe('Extraction Quality', () => {
|
|
25
|
+
let runtime: AgentRuntime;
|
|
26
|
+
let plannerDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
plannerDir = join(tmpdir(), 'extraction-quality-test-' + Date.now());
|
|
30
|
+
mkdirSync(plannerDir, { recursive: true });
|
|
31
|
+
runtime = createAgentRuntime({
|
|
32
|
+
agentId: 'test-extraction-quality',
|
|
33
|
+
vaultPath: ':memory:',
|
|
34
|
+
plansPath: join(plannerDir, 'plans.json'),
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
runtime.close();
|
|
40
|
+
rmSync(plannerDir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Helper ────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function createSessionWithContext(
|
|
46
|
+
sessionId: string,
|
|
47
|
+
context: string,
|
|
48
|
+
overrides: {
|
|
49
|
+
planId?: string;
|
|
50
|
+
planOutcome?: string;
|
|
51
|
+
toolsUsed?: string[];
|
|
52
|
+
filesModified?: string[];
|
|
53
|
+
domain?: string;
|
|
54
|
+
} = {},
|
|
55
|
+
) {
|
|
56
|
+
runtime.brainIntelligence.lifecycle({
|
|
57
|
+
action: 'start',
|
|
58
|
+
sessionId,
|
|
59
|
+
domain: overrides.domain ?? 'testing',
|
|
60
|
+
context,
|
|
61
|
+
toolsUsed: overrides.toolsUsed ?? [],
|
|
62
|
+
filesModified: overrides.filesModified ?? [],
|
|
63
|
+
planId: overrides.planId,
|
|
64
|
+
});
|
|
65
|
+
runtime.brainIntelligence.lifecycle({
|
|
66
|
+
action: 'end',
|
|
67
|
+
sessionId,
|
|
68
|
+
planOutcome: overrides.planOutcome,
|
|
69
|
+
toolsUsed: overrides.toolsUsed,
|
|
70
|
+
filesModified: overrides.filesModified,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── 1. Actionable titles from rich context ───────────────────────
|
|
75
|
+
|
|
76
|
+
describe('actionable proposals from session context', () => {
|
|
77
|
+
it('should use session context objective in plan_completed proposal title', () => {
|
|
78
|
+
const richContext = JSON.stringify({
|
|
79
|
+
objective: 'Add OAuth2 authentication to the API gateway',
|
|
80
|
+
scope: { included: ['auth module', 'gateway routes'], excluded: ['frontend'] },
|
|
81
|
+
decisions: ['Use passport.js for OAuth2 strategy'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
createSessionWithContext('rich-ctx-1', richContext, {
|
|
85
|
+
planId: 'plan-oauth',
|
|
86
|
+
planOutcome: 'completed',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = runtime.brainIntelligence.extractKnowledge('rich-ctx-1');
|
|
90
|
+
const planProposal = result.proposals.find((p) => p.rule === 'plan_completed');
|
|
91
|
+
|
|
92
|
+
expect(planProposal).toBeDefined();
|
|
93
|
+
// The title should reference the objective, not just the plan ID
|
|
94
|
+
expect(planProposal!.title).not.toContain('Successful plan:');
|
|
95
|
+
expect(planProposal!.title.toLowerCase()).toContain('oauth');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should use session context objective in plan_abandoned proposal title', () => {
|
|
99
|
+
const richContext = JSON.stringify({
|
|
100
|
+
objective: 'Migrate database from Postgres to CockroachDB',
|
|
101
|
+
scope: { included: ['migration scripts', 'connection pool'] },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
createSessionWithContext('rich-ctx-2', richContext, {
|
|
105
|
+
planId: 'plan-migrate',
|
|
106
|
+
planOutcome: 'abandoned',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = runtime.brainIntelligence.extractKnowledge('rich-ctx-2');
|
|
110
|
+
const abandonedProposal = result.proposals.find((p) => p.rule === 'plan_abandoned');
|
|
111
|
+
|
|
112
|
+
expect(abandonedProposal).toBeDefined();
|
|
113
|
+
// The title should reference what was abandoned, not just the plan ID
|
|
114
|
+
expect(abandonedProposal!.title).not.toContain('Abandoned plan:');
|
|
115
|
+
expect(abandonedProposal!.title.toLowerCase()).toContain('migrate');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should include scope details in proposal description when context has scope', () => {
|
|
119
|
+
const richContext = JSON.stringify({
|
|
120
|
+
objective: 'Refactor the billing reconciliation module',
|
|
121
|
+
scope: {
|
|
122
|
+
included: ['stripe-adapter', 'webhook-handler', 'ledger-service'],
|
|
123
|
+
excluded: ['billing-ui', 'invoice-generator'],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
createSessionWithContext('rich-ctx-3', richContext, {
|
|
128
|
+
planId: 'plan-abc',
|
|
129
|
+
planOutcome: 'completed',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = runtime.brainIntelligence.extractKnowledge('rich-ctx-3');
|
|
133
|
+
const planProposal = result.proposals.find((p) => p.rule === 'plan_completed');
|
|
134
|
+
|
|
135
|
+
expect(planProposal).toBeDefined();
|
|
136
|
+
// Description should mention scope components, not just "can be reused for similar tasks"
|
|
137
|
+
expect(planProposal!.description.toLowerCase()).toMatch(/stripe|webhook|ledger/);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ─── 2. Dedup: same rule + sessionId = 1 proposal ────────────────
|
|
142
|
+
|
|
143
|
+
describe('proposal deduplication', () => {
|
|
144
|
+
it('should produce exactly 1 proposal per rule per session', () => {
|
|
145
|
+
createSessionWithContext('dedup-1', 'some context', {
|
|
146
|
+
planId: 'plan-dedup',
|
|
147
|
+
planOutcome: 'completed',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Extract twice on same session (reset extractedAt in between)
|
|
151
|
+
runtime.brainIntelligence.extractKnowledge('dedup-1');
|
|
152
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'dedup-1' });
|
|
153
|
+
runtime.brainIntelligence.extractKnowledge('dedup-1');
|
|
154
|
+
|
|
155
|
+
// Query all proposals for this session
|
|
156
|
+
const proposals = runtime.brainIntelligence.getProposals({
|
|
157
|
+
sessionId: 'dedup-1',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Count proposals per rule
|
|
161
|
+
const ruleCounts = new Map<string, number>();
|
|
162
|
+
for (const p of proposals) {
|
|
163
|
+
ruleCounts.set(p.rule, (ruleCounts.get(p.rule) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Each rule should appear at most once per session
|
|
167
|
+
for (const [rule, count] of ruleCounts) {
|
|
168
|
+
expect(count, `rule "${rule}" should appear exactly once`).toBe(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── 3. long_session rule should not fire ─────────────────────────
|
|
174
|
+
|
|
175
|
+
describe('long_session rule removal', () => {
|
|
176
|
+
it('should NOT produce a long_session proposal', () => {
|
|
177
|
+
// Create session manually with backdated start time to simulate >30 min duration.
|
|
178
|
+
// SQLite datetime('now') uses 'YYYY-MM-DD HH:MM:SS' format (no T/Z), so match that.
|
|
179
|
+
const d = new Date(Date.now() - 35 * 60 * 1000);
|
|
180
|
+
const thirtyFiveMinAgo = d
|
|
181
|
+
.toISOString()
|
|
182
|
+
.replace('T', ' ')
|
|
183
|
+
.replace(/\.\d{3}Z$/, '');
|
|
184
|
+
const provider = runtime.vault.getProvider();
|
|
185
|
+
|
|
186
|
+
// Insert session directly with backdated started_at so auto-extract sees the long duration
|
|
187
|
+
provider.run(
|
|
188
|
+
`INSERT INTO brain_sessions (id, started_at, domain, context, tools_used, files_modified)
|
|
189
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
190
|
+
['long-sess-1', thirtyFiveMinAgo, 'testing', null, '[]', '[]'],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// End the session — this sets ended_at to now(), creating a >30 min gap
|
|
194
|
+
runtime.brainIntelligence.lifecycle({
|
|
195
|
+
action: 'end',
|
|
196
|
+
sessionId: 'long-sess-1',
|
|
197
|
+
toolsUsed: ['search'], // need at least 1 tool for auto-extract gate
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Reset extracted_at so we can manually extract and inspect
|
|
201
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'long-sess-1' });
|
|
202
|
+
|
|
203
|
+
const result = runtime.brainIntelligence.extractKnowledge('long-sess-1');
|
|
204
|
+
|
|
205
|
+
// long_session rule should no longer exist (removal in #360)
|
|
206
|
+
expect(result.rulesApplied).not.toContain('long_session');
|
|
207
|
+
expect(result.proposals.find((p) => p.rule === 'long_session')).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ─── 4. drift_detected rule ───────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
describe('drift_detected rule', () => {
|
|
214
|
+
it('should fire when session context contains drift indicators', () => {
|
|
215
|
+
const contextWithDrift = JSON.stringify({
|
|
216
|
+
objective: 'Implement caching layer for API responses',
|
|
217
|
+
drift: {
|
|
218
|
+
items: [
|
|
219
|
+
{
|
|
220
|
+
type: 'added',
|
|
221
|
+
description: 'Added Redis fallback to in-memory cache',
|
|
222
|
+
impact: 'medium',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'skipped',
|
|
226
|
+
description: 'Skipped cache invalidation webhooks',
|
|
227
|
+
impact: 'high',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
accuracyScore: 65,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
createSessionWithContext('drift-1', contextWithDrift, {
|
|
235
|
+
planId: 'plan-cache',
|
|
236
|
+
planOutcome: 'completed',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const result = runtime.brainIntelligence.extractKnowledge('drift-1');
|
|
240
|
+
|
|
241
|
+
// A drift_detected rule should fire (to be added in #366)
|
|
242
|
+
expect(result.rulesApplied).toContain('drift_detected');
|
|
243
|
+
const driftProposal = result.proposals.find((p) => p.rule === 'drift_detected');
|
|
244
|
+
expect(driftProposal).toBeDefined();
|
|
245
|
+
expect(driftProposal!.type).toBe('anti-pattern');
|
|
246
|
+
expect(driftProposal!.description.toLowerCase()).toMatch(/drift|skipped|deviation/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should NOT fire drift_detected when context has no drift', () => {
|
|
250
|
+
const cleanContext = JSON.stringify({
|
|
251
|
+
objective: 'Add unit tests for auth module',
|
|
252
|
+
scope: { included: ['auth'] },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
createSessionWithContext('no-drift-1', cleanContext, {
|
|
256
|
+
planId: 'plan-tests',
|
|
257
|
+
planOutcome: 'completed',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const result = runtime.brainIntelligence.extractKnowledge('no-drift-1');
|
|
261
|
+
expect(result.rulesApplied).not.toContain('drift_detected');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─── 5. Context richness affects confidence ───────────────────────
|
|
266
|
+
|
|
267
|
+
describe('confidence based on context richness', () => {
|
|
268
|
+
it('should assign higher confidence to proposals with rich session context', () => {
|
|
269
|
+
// Session with rich context
|
|
270
|
+
const richContext = JSON.stringify({
|
|
271
|
+
objective: 'Build notification service',
|
|
272
|
+
scope: { included: ['notifications', 'email-adapter', 'push-adapter'] },
|
|
273
|
+
decisions: ['Use event-driven architecture', 'SNS for push notifications'],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
createSessionWithContext('conf-rich', richContext, {
|
|
277
|
+
planId: 'plan-notify-rich',
|
|
278
|
+
planOutcome: 'completed',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Session with no context
|
|
282
|
+
createSessionWithContext('conf-empty', '', {
|
|
283
|
+
planId: 'plan-notify-empty',
|
|
284
|
+
planOutcome: 'completed',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const richResult = runtime.brainIntelligence.extractKnowledge('conf-rich');
|
|
288
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'conf-empty' });
|
|
289
|
+
const emptyResult = runtime.brainIntelligence.extractKnowledge('conf-empty');
|
|
290
|
+
|
|
291
|
+
const richPlanProposal = richResult.proposals.find((p) => p.rule === 'plan_completed');
|
|
292
|
+
const emptyPlanProposal = emptyResult.proposals.find((p) => p.rule === 'plan_completed');
|
|
293
|
+
|
|
294
|
+
expect(richPlanProposal).toBeDefined();
|
|
295
|
+
expect(emptyPlanProposal).toBeDefined();
|
|
296
|
+
|
|
297
|
+
// Rich context should produce higher confidence than empty context
|
|
298
|
+
expect(richPlanProposal!.confidence).toBeGreaterThan(emptyPlanProposal!.confidence);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should assign lower confidence when session context is null', () => {
|
|
302
|
+
// Session with null context (no context field at all)
|
|
303
|
+
runtime.brainIntelligence.lifecycle({
|
|
304
|
+
action: 'start',
|
|
305
|
+
sessionId: 'conf-null',
|
|
306
|
+
planId: 'plan-null-ctx',
|
|
307
|
+
});
|
|
308
|
+
runtime.brainIntelligence.lifecycle({
|
|
309
|
+
action: 'end',
|
|
310
|
+
sessionId: 'conf-null',
|
|
311
|
+
planOutcome: 'completed',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'conf-null' });
|
|
315
|
+
const result = runtime.brainIntelligence.extractKnowledge('conf-null');
|
|
316
|
+
const planProposal = result.proposals.find((p) => p.rule === 'plan_completed');
|
|
317
|
+
|
|
318
|
+
expect(planProposal).toBeDefined();
|
|
319
|
+
// Without context, confidence should be below the current hardcoded 0.65
|
|
320
|
+
expect(planProposal!.confidence).toBeLessThan(0.65);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -38,7 +38,6 @@ const SPREAD_MAX = 5;
|
|
|
38
38
|
const RECENCY_DECAY_DAYS = 30;
|
|
39
39
|
const EXTRACTION_TOOL_THRESHOLD = 3;
|
|
40
40
|
const EXTRACTION_FILE_THRESHOLD = 3;
|
|
41
|
-
const EXTRACTION_LONG_SESSION_MINUTES = 30;
|
|
42
41
|
const EXTRACTION_HIGH_FEEDBACK_RATIO = 0.8;
|
|
43
42
|
const AUTO_PROMOTE_THRESHOLD = 0.8;
|
|
44
43
|
const AUTO_PROMOTE_PENDING_MIN = 0.4;
|
|
@@ -739,10 +738,18 @@ export class BrainIntelligence {
|
|
|
739
738
|
for (const [tool, count] of toolCounts) {
|
|
740
739
|
if (count >= EXTRACTION_TOOL_THRESHOLD) {
|
|
741
740
|
rulesApplied.push('repeated_tool_usage');
|
|
741
|
+
const ctx = session.context ?? '';
|
|
742
|
+
const objective = this.extractObjective(ctx);
|
|
743
|
+
const toolTitle = objective
|
|
744
|
+
? `Tool pattern: ${tool} (${count}x) during ${objective.slice(0, 60)}`
|
|
745
|
+
: `Frequent use of ${tool} (${count}x)`;
|
|
746
|
+
const toolDescription = objective
|
|
747
|
+
? `Tool ${tool} used ${count} times while working on: ${objective}. This tool-task pairing may indicate a reusable workflow.`
|
|
748
|
+
: `Tool ${tool} was used ${count} times in session. Consider automating or abstracting this workflow.`;
|
|
742
749
|
proposals.push(
|
|
743
750
|
this.createProposal(sessionId, 'repeated_tool_usage', 'pattern', {
|
|
744
|
-
title:
|
|
745
|
-
description:
|
|
751
|
+
title: toolTitle,
|
|
752
|
+
description: toolDescription,
|
|
746
753
|
confidence: Math.min(0.9, 0.5 + count * 0.1),
|
|
747
754
|
}),
|
|
748
755
|
);
|
|
@@ -766,57 +773,107 @@ export class BrainIntelligence {
|
|
|
766
773
|
if (significantDirs.length > 0) {
|
|
767
774
|
const [topDir, topFiles] = significantDirs.sort((a, b) => b[1].length - a[1].length)[0];
|
|
768
775
|
rulesApplied.push('multi_file_edit');
|
|
776
|
+
const ctx = session.context ?? '';
|
|
777
|
+
const objective = this.extractObjective(ctx);
|
|
778
|
+
const isRefactor = /refactor|rename|move|extract|consolidat/i.test(ctx);
|
|
779
|
+
const isFeature = /feat|add|implement|create|new/i.test(ctx);
|
|
780
|
+
const inferredPattern = isRefactor
|
|
781
|
+
? 'Refactoring'
|
|
782
|
+
: isFeature
|
|
783
|
+
? 'Feature'
|
|
784
|
+
: 'Cross-cutting change';
|
|
785
|
+
const mfeTitle = objective
|
|
786
|
+
? `${inferredPattern}: ${objective.slice(0, 70)}`
|
|
787
|
+
: `${inferredPattern} in ${topDir} (${topFiles.length} files)`;
|
|
788
|
+
const mfeDescription = objective
|
|
789
|
+
? `${inferredPattern} across ${topFiles.length} files in ${topDir}: ${objective}`
|
|
790
|
+
: `Session modified ${topFiles.length} files in ${topDir}: ${topFiles.slice(0, 5).join(', ')}${topFiles.length > 5 ? '...' : ''}.`;
|
|
769
791
|
proposals.push(
|
|
770
792
|
this.createProposal(sessionId, 'multi_file_edit', 'pattern', {
|
|
771
|
-
title:
|
|
772
|
-
description:
|
|
793
|
+
title: mfeTitle,
|
|
794
|
+
description: mfeDescription,
|
|
773
795
|
confidence: Math.min(0.8, 0.4 + topFiles.length * 0.05),
|
|
774
796
|
}),
|
|
775
797
|
);
|
|
776
798
|
}
|
|
777
799
|
}
|
|
778
800
|
|
|
779
|
-
// Rule 3:
|
|
780
|
-
if (session.endedAt && session.startedAt) {
|
|
781
|
-
const durationMs =
|
|
782
|
-
new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime();
|
|
783
|
-
const durationMin = durationMs / 60000;
|
|
784
|
-
if (durationMin > EXTRACTION_LONG_SESSION_MINUTES) {
|
|
785
|
-
rulesApplied.push('long_session');
|
|
786
|
-
proposals.push(
|
|
787
|
-
this.createProposal(sessionId, 'long_session', 'pattern', {
|
|
788
|
-
title: `Long session (${Math.round(durationMin)} minutes)`,
|
|
789
|
-
description: `Session lasted ${Math.round(durationMin)} minutes. Deep work session — review if this duration was productive or indicates a need for better tooling.`,
|
|
790
|
-
confidence: 0.3,
|
|
791
|
-
}),
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Rule 4: Plan completed — moderate confidence to avoid auto-promoting generic entries
|
|
801
|
+
// Rule 3: Plan completed — parse session.context for actionable title + dynamic confidence
|
|
797
802
|
if (session.planId && session.planOutcome === 'completed') {
|
|
798
803
|
rulesApplied.push('plan_completed');
|
|
804
|
+
const ctx = session.context ?? '';
|
|
805
|
+
const objective = this.extractObjective(ctx);
|
|
806
|
+
const hasScope = /scope|included|excluded/i.test(ctx);
|
|
807
|
+
const hasCriteria = /criteria|acceptance|verification/i.test(ctx);
|
|
808
|
+
const confidence =
|
|
809
|
+
ctx.length > 0
|
|
810
|
+
? hasScope && hasCriteria
|
|
811
|
+
? 0.85
|
|
812
|
+
: hasScope || hasCriteria
|
|
813
|
+
? 0.8
|
|
814
|
+
: 0.75
|
|
815
|
+
: 0.5;
|
|
816
|
+
const title = objective
|
|
817
|
+
? `Workflow: ${objective.slice(0, 80)}`
|
|
818
|
+
: `Successful plan: ${session.planId}`;
|
|
819
|
+
const description = objective
|
|
820
|
+
? `Completed: ${objective}${hasScope ? '. Scope and constraints documented in session context.' : ''}`
|
|
821
|
+
: `Plan ${session.planId} completed successfully. This workflow can be reused for similar tasks.`;
|
|
799
822
|
proposals.push(
|
|
800
823
|
this.createProposal(sessionId, 'plan_completed', 'workflow', {
|
|
801
|
-
title
|
|
802
|
-
description
|
|
803
|
-
confidence
|
|
824
|
+
title,
|
|
825
|
+
description,
|
|
826
|
+
confidence,
|
|
804
827
|
}),
|
|
805
828
|
);
|
|
806
829
|
}
|
|
807
830
|
|
|
808
|
-
// Rule
|
|
831
|
+
// Rule 4: Plan abandoned — parse context for failure reason
|
|
809
832
|
if (session.planId && session.planOutcome === 'abandoned') {
|
|
810
833
|
rulesApplied.push('plan_abandoned');
|
|
834
|
+
const ctx = session.context ?? '';
|
|
835
|
+
const objective = this.extractObjective(ctx);
|
|
836
|
+
const hasFailureReason = /blocked|failed|wrong|mistake|abandoned|reverted|conflict/i.test(
|
|
837
|
+
ctx,
|
|
838
|
+
);
|
|
839
|
+
const confidence = ctx.length > 0 ? (hasFailureReason ? 0.85 : 0.75) : 0.5;
|
|
840
|
+
const title = objective
|
|
841
|
+
? `Anti-pattern: ${objective.slice(0, 80)}`
|
|
842
|
+
: `Abandoned plan: ${session.planId}`;
|
|
843
|
+
const description = objective
|
|
844
|
+
? `Abandoned: ${objective}${hasFailureReason ? '. Failure indicators found in session context — review for root cause.' : '. Review what went wrong to avoid repeating.'}`
|
|
845
|
+
: `Plan ${session.planId} was abandoned. Review what went wrong to avoid repeating in future sessions.`;
|
|
811
846
|
proposals.push(
|
|
812
847
|
this.createProposal(sessionId, 'plan_abandoned', 'anti-pattern', {
|
|
813
|
-
title
|
|
814
|
-
description
|
|
815
|
-
confidence
|
|
848
|
+
title,
|
|
849
|
+
description,
|
|
850
|
+
confidence,
|
|
816
851
|
}),
|
|
817
852
|
);
|
|
818
853
|
}
|
|
819
854
|
|
|
855
|
+
// Rule 5: Drift detected — fires when plan completed but context contains drift indicators
|
|
856
|
+
if (session.planId && session.planOutcome === 'completed' && session.context) {
|
|
857
|
+
const driftPattern =
|
|
858
|
+
/drift|skipped|added.*unplanned|changed scope|out of scope|deviat|unplanned/i;
|
|
859
|
+
if (driftPattern.test(session.context)) {
|
|
860
|
+
rulesApplied.push('drift_detected');
|
|
861
|
+
const objective = this.extractObjective(session.context);
|
|
862
|
+
const driftMatch =
|
|
863
|
+
session.context.match(/drift[:\s]+(.{1,120})/i) ??
|
|
864
|
+
session.context.match(/skipped[:\s]+(.{1,120})/i) ??
|
|
865
|
+
session.context.match(/unplanned[:\s]+(.{1,120})/i);
|
|
866
|
+
const driftDetail = driftMatch ? driftMatch[1].trim() : 'scope changed during execution';
|
|
867
|
+
proposals.push(
|
|
868
|
+
this.createProposal(sessionId, 'drift_detected', 'anti-pattern', {
|
|
869
|
+
title: `Plan drift: ${objective ? objective.slice(0, 60) : session.planId} — ${driftDetail.slice(0, 40)}`,
|
|
870
|
+
description: `Plan ${objective ?? session.planId} completed with drift: ${driftDetail}. Review scope controls for future planning.`,
|
|
871
|
+
confidence: 0.8,
|
|
872
|
+
}),
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
820
877
|
// Rule 6: High feedback ratio (>80% accept or dismiss)
|
|
821
878
|
const feedbackRow = this.provider.get<{
|
|
822
879
|
total: number;
|
|
@@ -1369,12 +1426,58 @@ export class BrainIntelligence {
|
|
|
1369
1426
|
};
|
|
1370
1427
|
}
|
|
1371
1428
|
|
|
1429
|
+
/**
|
|
1430
|
+
* Extract the objective from session context — first meaningful sentence or line.
|
|
1431
|
+
* Returns empty string if context is empty or unparseable.
|
|
1432
|
+
*/
|
|
1433
|
+
private extractObjective(context: string): string {
|
|
1434
|
+
if (!context || context.trim().length === 0) return '';
|
|
1435
|
+
// Try to find an "Objective:" line
|
|
1436
|
+
const objMatch = context.match(/objective[:\s]+(.+)/i);
|
|
1437
|
+
if (objMatch) return objMatch[1].trim().replace(/\s+/g, ' ');
|
|
1438
|
+
// Fall back to first non-empty line
|
|
1439
|
+
const firstLine = context
|
|
1440
|
+
.split('\n')
|
|
1441
|
+
.map((l) => l.trim())
|
|
1442
|
+
.find((l) => l.length > 0);
|
|
1443
|
+
return firstLine ? firstLine.replace(/\s+/g, ' ') : '';
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1372
1446
|
private createProposal(
|
|
1373
1447
|
sessionId: string,
|
|
1374
1448
|
rule: string,
|
|
1375
1449
|
type: 'pattern' | 'anti-pattern' | 'workflow',
|
|
1376
1450
|
data: { title: string; description: string; confidence: number },
|
|
1377
1451
|
): KnowledgeProposal {
|
|
1452
|
+
// Dedup guard: skip if a proposal with the same rule + sessionId already exists
|
|
1453
|
+
const existing = this.provider.get<{
|
|
1454
|
+
id: string;
|
|
1455
|
+
session_id: string;
|
|
1456
|
+
rule: string;
|
|
1457
|
+
type: string;
|
|
1458
|
+
title: string;
|
|
1459
|
+
description: string;
|
|
1460
|
+
confidence: number;
|
|
1461
|
+
promoted: number;
|
|
1462
|
+
created_at: string;
|
|
1463
|
+
}>('SELECT * FROM brain_proposals WHERE session_id = ? AND rule = ? LIMIT 1', [
|
|
1464
|
+
sessionId,
|
|
1465
|
+
rule,
|
|
1466
|
+
]);
|
|
1467
|
+
if (existing) {
|
|
1468
|
+
return {
|
|
1469
|
+
id: existing.id,
|
|
1470
|
+
sessionId: existing.session_id,
|
|
1471
|
+
rule: existing.rule,
|
|
1472
|
+
type: existing.type as 'pattern' | 'anti-pattern' | 'workflow',
|
|
1473
|
+
title: existing.title,
|
|
1474
|
+
description: existing.description,
|
|
1475
|
+
confidence: existing.confidence,
|
|
1476
|
+
promoted: existing.promoted === 1,
|
|
1477
|
+
createdAt: existing.created_at,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1378
1481
|
const id = randomUUID();
|
|
1379
1482
|
this.provider.run(
|
|
1380
1483
|
`INSERT INTO brain_proposals (id, session_id, rule, type, title, description, confidence)
|
|
@@ -229,14 +229,17 @@ export class LearningRadar {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/**
|
|
232
|
-
* Dismiss
|
|
232
|
+
* Dismiss one or more pending candidates — mark them as not worth capturing.
|
|
233
233
|
*/
|
|
234
|
-
dismiss(
|
|
234
|
+
dismiss(candidateIds: number | number[]): { dismissed: number } {
|
|
235
|
+
const ids = Array.isArray(candidateIds) ? candidateIds : [candidateIds];
|
|
236
|
+
if (ids.length === 0) return { dismissed: 0 };
|
|
237
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
235
238
|
const result = this.provider.run(
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
`UPDATE radar_candidates SET status = 'dismissed' WHERE id IN (${placeholders}) AND status = 'pending'`,
|
|
240
|
+
ids,
|
|
238
241
|
);
|
|
239
|
-
return { dismissed: result.changes
|
|
242
|
+
return { dismissed: result.changes };
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
/**
|
|
@@ -335,7 +335,7 @@ describe('Ambient learning radar (#208)', () => {
|
|
|
335
335
|
const pending = candidates.find((c) => c.title.includes('stale cache'));
|
|
336
336
|
expect(pending).toBeDefined();
|
|
337
337
|
const result = learningRadar.dismiss(pending!.id);
|
|
338
|
-
expect(result.dismissed).toBe(
|
|
338
|
+
expect(result.dismissed).toBe(1);
|
|
339
339
|
});
|
|
340
340
|
|
|
341
341
|
it('getStats returns radar statistics', () => {
|
|
@@ -35,7 +35,7 @@ describe('IntentRouter', () => {
|
|
|
35
35
|
describe('construction', () => {
|
|
36
36
|
it('seeds 10 default modes on first creation', () => {
|
|
37
37
|
const modes = router.getModes();
|
|
38
|
-
expect(modes.length).toBe(
|
|
38
|
+
expect(modes.length).toBe(11);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('starts in GENERAL-MODE', () => {
|
|
@@ -44,7 +44,7 @@ describe('IntentRouter', () => {
|
|
|
44
44
|
|
|
45
45
|
it('is idempotent — second instance does not duplicate modes', () => {
|
|
46
46
|
const router2 = new IntentRouter(vault);
|
|
47
|
-
expect(router2.getModes().length).toBe(
|
|
47
|
+
expect(router2.getModes().length).toBe(11);
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -199,7 +199,7 @@ describe('IntentRouter', () => {
|
|
|
199
199
|
it('adds a new mode to the database', () => {
|
|
200
200
|
router.registerMode(customMode);
|
|
201
201
|
const modes = router.getModes();
|
|
202
|
-
expect(modes.length).toBe(
|
|
202
|
+
expect(modes.length).toBe(12);
|
|
203
203
|
const found = modes.find((m) => m.mode === 'CUSTOM-MODE');
|
|
204
204
|
expect(found).toBeDefined();
|
|
205
205
|
expect(found!.keywords).toEqual(['custom', 'special']);
|
|
@@ -338,6 +338,76 @@ describe('IntentRouter', () => {
|
|
|
338
338
|
});
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
+
// ─── YOLO-MODE ───────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
describe('YOLO-MODE', () => {
|
|
344
|
+
it('route_intent with "yolo" returns YOLO-MODE', () => {
|
|
345
|
+
const result = router.routeIntent('go yolo on this task');
|
|
346
|
+
expect(result.intent).toBe('yolo');
|
|
347
|
+
expect(result.mode).toBe('YOLO-MODE');
|
|
348
|
+
expect(result.matchedKeywords).toContain('yolo');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('morph to YOLO-MODE succeeds when hook pack is installed', () => {
|
|
352
|
+
const result = router.morph('YOLO-MODE', { hookPackInstalled: true });
|
|
353
|
+
expect(result.previousMode).toBe('GENERAL-MODE');
|
|
354
|
+
expect(result.currentMode).toBe('YOLO-MODE');
|
|
355
|
+
expect(result.behaviorRules.length).toBe(5);
|
|
356
|
+
expect(result.blocked).toBeUndefined();
|
|
357
|
+
expect(result.error).toBeUndefined();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('morph to YOLO-MODE fails when hook pack is missing', () => {
|
|
361
|
+
const result = router.morph('YOLO-MODE');
|
|
362
|
+
expect(result.blocked).toBe(true);
|
|
363
|
+
expect(result.error).toContain('yolo-safety hook pack');
|
|
364
|
+
expect(result.error).toContain('soleri hooks add-pack yolo-safety');
|
|
365
|
+
expect(result.currentMode).toBe('GENERAL-MODE'); // unchanged
|
|
366
|
+
expect(router.getCurrentMode()).toBe('GENERAL-MODE'); // not switched
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('morph to YOLO-MODE fails when hookPackInstalled is explicitly false', () => {
|
|
370
|
+
const result = router.morph('YOLO-MODE', { hookPackInstalled: false });
|
|
371
|
+
expect(result.blocked).toBe(true);
|
|
372
|
+
expect(result.error).toContain('yolo-safety hook pack');
|
|
373
|
+
expect(router.getCurrentMode()).toBe('GENERAL-MODE');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('morph to other modes is unaffected by the gate', () => {
|
|
377
|
+
const result = router.morph('BUILD-MODE');
|
|
378
|
+
expect(result.currentMode).toBe('BUILD-MODE');
|
|
379
|
+
expect(result.blocked).toBeUndefined();
|
|
380
|
+
expect(result.error).toBeUndefined();
|
|
381
|
+
expect(router.getCurrentMode()).toBe('BUILD-MODE');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('get_behavior_rules returns 5 rules', () => {
|
|
385
|
+
const rules = router.getBehaviorRules('YOLO-MODE');
|
|
386
|
+
expect(rules).toHaveLength(5);
|
|
387
|
+
expect(rules[0]).toContain('Skip plan approval gates');
|
|
388
|
+
expect(rules[1]).toContain('orchestrate_complete');
|
|
389
|
+
expect(rules[2]).toContain('vault gather-before-execute');
|
|
390
|
+
expect(rules[3]).toContain('Hook pack must be installed');
|
|
391
|
+
expect(rules[4]).toContain('exit YOLO');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('all keywords route to YOLO-MODE', () => {
|
|
395
|
+
const keywords = [
|
|
396
|
+
'yolo',
|
|
397
|
+
'autonomous',
|
|
398
|
+
'fire-and-forget',
|
|
399
|
+
'hands-off',
|
|
400
|
+
'no-approval',
|
|
401
|
+
'skip-gates',
|
|
402
|
+
'full-auto',
|
|
403
|
+
];
|
|
404
|
+
for (const kw of keywords) {
|
|
405
|
+
const result = router.routeIntent(kw);
|
|
406
|
+
expect(result.mode).toBe('YOLO-MODE');
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
341
411
|
// ─── getModes ─────────────────────────────────────────────────
|
|
342
412
|
|
|
343
413
|
describe('getModes', () => {
|