2026 03 23 Intelligence Bridge
Documentation for Plans
Intelligence Bridge Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Wire MCP decision intelligence into Vitto's runtime context so Vitto makes decisions informed by 12K+ past decisions, fix pattern dedup, add decision categorization, and sync specializations to sub-agents.
Architecture: 5 independent gaps closed across both backends. ai-api owns decision storage, pattern mining, and intelligence computation. vitto-api consumes intelligence at runtime via BackendAiClientService and syncs specializations daily. No frontend changes — this is pure backend wiring.
Tech Stack: NestJS, MongoDB/Mongoose, RxJS, Node.js crypto
Spec: docs/superpowers/specs/2026-03-23-intelligence-bridge-design.md
IMPORTANT: Do NOT use git actions (commit, push, branch) — another session may be running concurrently.
File Structure
| File | Action | Responsibility |
|---|---|---|
APIs/backend-ai-api/src/app/learning/jobs/pattern-mining.job.ts |
Modify | Fix dedup: SHA-256 hash + upsert |
APIs/backend-ai-api/src/app/schemas/ai-infrastructure.schema.ts |
Modify | Add domains[] and audience to Decision schema |
APIs/backend-ai-api/src/app/infrastructure/services/decision.service.ts |
Modify | Compute domains + audience on create |
APIs/backend-ai-api/src/app/learning/learning.service.ts |
Modify | Add golden decisions to getPyramidContext |
APIs/backend-vitto-api/src/app/connectivity/backend-ai-client.service.ts |
Modify | Add goldenDecisions to response type, add getSpecializations |
APIs/backend-vitto-api/src/app/engine/context-assembly/context-assembly.service.ts |
Modify | Inject learning intelligence into context pipeline |
APIs/backend-vitto-api/src/app/engine/learning-sync.service.ts |
Modify | Add specialization sync to daily cron |
Task 1: Fix pattern mining deduplication (Gap 2)
Highest value, zero dependencies. The pattern data is polluted and everything else reads from it.
Files:
Modify:
APIs/backend-ai-api/src/app/learning/jobs/pattern-mining.job.ts:381-386(generatePatternId)Modify:
APIs/backend-ai-api/src/app/learning/jobs/pattern-mining.job.ts:114-133(create/update logic)Step 1: Replace generatePatternId with deterministic SHA-256 hash
In pattern-mining.job.ts, find generatePatternId (line ~381) and replace:
// OLD (line 381-386):
// private generatePatternId(pattern: string, category: string): string {
// const hash = pattern.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
// return `auto-${category}-${hash}-${Date.now().toString(36)}`;
// }
// NEW:
private generatePatternId(pattern: string, category: string): string {
const normalized = this.normalizeApproach(pattern);
const hash = require('crypto')
.createHash('sha256')
.update(`${normalized}::${category}`)
.digest('hex')
.slice(0, 16);
return `auto-${category}-${hash}`;
}
Key change: no Date.now() — same pattern+category always produces the same ID.
- Step 2: Replace find+create with findOneAndUpdate upsert
Find the pattern creation logic (line ~114-133) where it does findOne then conditionally create. Replace with an upsert:
// Replace the find+create block with:
const patternId = this.generatePatternId(extracted.pattern, extracted.category);
const normalizedText = this.normalizeApproach(extracted.pattern);
await this.patternModel.findOneAndUpdate(
{ patternId },
{
$set: {
name: extracted.pattern,
normalizedText,
category: extracted.category,
description: extracted.description || '',
successRate: extracted.successRate ?? 0.5,
updatedAt: new Date(),
},
$inc: { frequency: 1 },
$addToSet: {
examples: { $each: extracted.examples || [] },
tags: { $each: extracted.tags || [] },
sourceDecisionIds: { $each: extracted.sourceDecisionIds || [] },
},
$setOnInsert: {
patternId,
createdAt: new Date(),
timesUsed: 0,
},
},
{ upsert: true, new: true },
);
- Step 3: Add normalizedText field to pattern schema if missing
Check the learned_patterns schema in ai-infrastructure.schema.ts. If normalizedText is not a field, add it:
@Prop({ index: true })
normalizedText?: string;
- Step 4: Verify by checking pattern count
After the fix, the next mining run will upsert instead of creating duplicates. To clean existing data, you can run a one-off dedup query (not blocking — old duplicates will be overwritten on next mining cycle).
Task 2: Add decision categorization (Gap 3)
Files:
Modify:
APIs/backend-ai-api/src/app/schemas/ai-infrastructure.schema.ts:317-493(Decision schema)Modify:
APIs/backend-ai-api/src/app/infrastructure/services/decision.service.ts:83-158(create method)Modify:
APIs/backend-ai-api/src/app/command-center/learning-router.service.ts(export detectDomains)Step 1: Add
domainsandaudiencefields to Decision schema
In ai-infrastructure.schema.ts, inside the Decision class (after the tags field, around line ~400):
@Prop({ type: [String], default: [] })
domains: string[];
@Prop({
type: String,
enum: ['developer', 'platform-builder', 'admin', 'all'],
default: 'developer',
})
audience: string;
- Step 2: Make detectDomains accessible from decision.service
In learning-router.service.ts, verify detectDomains is a public method. It should already be — it's called by the controller. If it's public, no change needed. Just verify the import path works from decision.service.
If LearningRouterService is in a different NestJS module than DecisionService, you'll need to inject it. Check if CommandCenterModule exports LearningRouterService. If not, use a simpler approach — inline the domain detection logic:
// In decision.service.ts, add a private method:
private detectDomains(decision: { question: string; chosen: string; reasoning: string; type: string; tags?: string[] }): string[] {
const text = `${decision.question} ${decision.chosen} ${decision.reasoning} ${decision.type}`.toLowerCase();
const domainKeywords: Record<string, string[]> = {
angular: ['component', 'angular', 'signal', 'rxjs', 'ngrx', 'template', 'directive', 'pipe'],
architecture: ['architecture', 'pattern', 'design', 'structure', 'module', 'monorepo'],
backend: ['nestjs', 'controller', 'service', 'mongodb', 'api', 'endpoint', 'guard'],
styling: ['scss', 'css', 'theme', 'style', 'color', 'layout', 'responsive'],
testing: ['test', 'spec', 'jest', 'mock', 'coverage', 'e2e'],
infrastructure: ['deploy', 'ci', 'docker', 'pipeline', 'build', 'nx', 'workspace'],
ai: ['llm', 'claude', 'openai', 'prompt', 'embedding', 'vector', 'agent', 'vitto'],
};
const detected: string[] = [];
for (const [domain, keywords] of Object.entries(domainKeywords)) {
if (keywords.some(kw => text.includes(kw))) {
detected.push(domain);
}
}
return detected.length > 0 ? detected : ['general'];
}
- Step 3: Compute domains and audience in create()
In decision.service.ts, find the create() method (line ~83). After the decision object is constructed but before this.decisionModel.create(), add:
// Compute domains from content
const domains = this.detectDomains({
question: dto.question,
chosen: dto.chosen,
reasoning: dto.reasoning,
type: dto.type,
tags: dto.tags,
});
// Determine audience from platform/userType
const audience = dto.userType === 'executive' || dto.userType === 'admin'
? 'admin'
: dto.platform === 'api' || dto.platform === 'external'
? 'platform-builder'
: 'developer';
// Add to the decision object before saving:
// decision.domains = domains;
// decision.audience = audience;
Add domains and audience to the object passed to this.decisionModel.create().
- Step 4: Verify new decisions get categorized
Restart backend-ai-api. Log a decision via MCP:
log_decision({ type: "architecture", question: "Test", chosen: "Test", reasoning: "Angular component", confidence: 0.9 })
Check MongoDB ai-agents.decisions — the new document should have domains: ["angular"] and audience: "developer".
Task 3: Add golden decisions to intelligence context (Gap 4)
Files:
Modify:
APIs/backend-ai-api/src/app/learning/learning.service.ts:663-720(getPyramidContext)Modify:
APIs/backend-ai-api/src/app/learning/learning.controller.ts:100-104(response shape)Step 1: Add getGoldenDecisionsForTask method
In learning.service.ts, add a new method (can go after getPyramidContext):
/**
* [S262] Fetch golden decisions relevant to a task, filtered by domains.
*/
private async getGoldenDecisionsForTask(task: string, categories: string[], limit = 5): Promise<Array<{
question: string;
chosen: string;
reasoning: string;
confidence: number;
qualityScore: number;
}>> {
try {
const goldenCollection = this.patternModel.db.collection('golden_decisions');
// Try category match first, fall back to keyword search
const query: Record<string, unknown> = {};
if (categories.length > 0) {
query['type'] = { $in: categories };
}
const docs = await goldenCollection
.find(query)
.sort({ qualityScore: -1, confidence: -1 })
.limit(limit)
.toArray();
return docs.map(d => ({
question: (d['question'] as string) || '',
chosen: (d['chosen'] as string) || '',
reasoning: (d['reasoning'] as string) || '',
confidence: (d['confidence'] as number) || 0,
qualityScore: (d['qualityScore'] as number) || 0,
}));
} catch {
return [];
}
}
- Step 2: Include golden decisions in getPyramidContext response
In getPyramidContext() (line ~663), add after the similar decisions block (before the token estimate):
// [S262] 5. Golden decisions — curated high-quality examples
const goldenDecisions = await this.getGoldenDecisionsForTask(task, taskCategories, 5);
Update the return statement to include:
return {
coreRules,
relevantPatterns: relevantPatterns.slice(0, 10),
similarDecisions,
goldenDecisions, // NEW
totalTokens: tokenEstimate + goldenDecisions.length * 150,
...(platformIntelligence ? { platformIntelligence } : {}),
};
- Step 3: Update PyramidContext type
Find the PyramidContext interface/type in learning.service.ts or learning.types.ts and add:
goldenDecisions?: Array<{
question: string;
chosen: string;
reasoning: string;
confidence: number;
qualityScore: number;
}>;
Task 4: Inject intelligence into Vitto's context assembly (Gap 1)
This is the big one — makes Vitto actually READ the intelligence data at runtime.
Files:
Modify:
APIs/backend-vitto-api/src/app/engine/context-assembly/context-assembly.service.tsModify:
APIs/backend-vitto-api/src/app/connectivity/backend-ai-client.service.ts:164-184Step 1: Update BackendAiClientService return type
In backend-ai-client.service.ts, update the getLearningIntelligence return type to include golden decisions:
async getLearningIntelligence(task: string, categories?: string[]): Promise<{
coreRules: Array<{ rule: string; category: string; confidence: number }>;
relevantPatterns: Array<{ name: string; description: string; successRate: number; category: string }>;
similarDecisions: Array<{ question: string; chosen: string; reasoning: string; confidence: number }>;
goldenDecisions?: Array<{ question: string; chosen: string; reasoning: string; confidence: number; qualityScore: number }>;
totalTokens: number;
}> {
Update the empty fallback to include goldenDecisions: [].
- Step 2: Add learning context method to ContextAssemblyService
In context-assembly.service.ts, inject BackendAiClientService if not already injected:
import { BackendAiClientService } from '../../connectivity/backend-ai-client.service';
// In constructor or inject():
private readonly aiClient = inject(BackendAiClientService);
// OR if using constructor injection:
// constructor(private readonly aiClient: BackendAiClientService, ...)
Add a new method:
/**
* [S262] Fetch learning intelligence from ai-api for task context injection.
* Returns formatted string for system prompt injection. Empty string on failure.
*/
async getLearningContext(taskSummary: string): Promise<string> {
if (!taskSummary || taskSummary.length < 10) return '';
try {
const intel = await this.aiClient.getLearningIntelligence(taskSummary);
if (!intel || intel.totalTokens === 0) return '';
const parts: string[] = [];
if (intel.goldenDecisions?.length) {
parts.push('## Proven Solutions (from past successful decisions)');
for (const gd of intel.goldenDecisions.slice(0, 3)) {
parts.push(`- **${gd.question}** → ${gd.chosen} (${Math.round(gd.confidence * 100)}% confidence)`);
}
}
if (intel.relevantPatterns?.length) {
parts.push('## Relevant Patterns');
for (const p of intel.relevantPatterns.slice(0, 5)) {
parts.push(`- **${p.name}**: ${p.description} (${Math.round(p.successRate * 100)}% success)`);
}
}
if (intel.similarDecisions?.length) {
parts.push('## Similar Past Decisions');
for (const sd of intel.similarDecisions.slice(0, 3)) {
parts.push(`- **${sd.question}** → ${sd.chosen}`);
}
}
return parts.length > 0 ? '\n\n' + parts.join('\n') : '';
} catch {
return ''; // Graceful degradation
}
}
- Step 3: Wire learning context into the prompt builder
Find where buildWebSystemPrompt is called with its params. The learning context needs to be added to the system prompt. Look at web-prompt-builder.service.ts or wherever the system prompt is assembled.
Add the learning context as an additional section:
// In the prompt building method, after other context sections:
const learningContext = await this.contextAssembly.getLearningContext(taskSummary);
if (learningContext) {
systemPrompt += `\n\n## Platform Intelligence${learningContext}`;
}
The exact injection point depends on the prompt builder's structure. Read the file to find where sections are concatenated.
- Step 4: Verify by running a task
Restart backend-vitto-api. Submit a task through the Vitto engine. Check the logs for "getLearningIntelligence" call. The system prompt should now include a "Platform Intelligence" section with patterns and decisions.
Task 5: Sync specializations to sub-agent definitions (Gap 5)
Files:
Modify:
APIs/backend-vitto-api/src/app/connectivity/backend-ai-client.service.tsModify:
APIs/backend-vitto-api/src/app/engine/learning-sync.service.ts:27-100Step 1: Add getSpecializations method to BackendAiClientService
In backend-ai-client.service.ts, add:
/**
* [S262] Fetch agent specializations from ai-api.
*/
async getSpecializations(): Promise<Array<{
agentId: string;
domains: Array<{ name: string; successRate: number; decisionCount: number }>;
overallScore: number;
totalDecisions: number;
}>> {
try {
const { data } = await firstValueFrom(
this.http.get(`${this.baseUrl}/agents/specializations`, {
headers: this.headers,
timeout: 3000,
}),
);
return data?.specializations || data?.data || [];
} catch {
return [];
}
}
- Step 2: Add specialization sync to the daily cron
In learning-sync.service.ts, at the end of syncGoldenDecisionsToAgents() (before the final catch), add:
// [S262] Sync specializations to sub-agent performance metrics
try {
const specializations = await this.aiClient.getSpecializations();
for (const spec of specializations) {
// Match specialization to sub-agent by role/domain overlap
for (const def of definitions) {
if (def.status !== 'active') continue;
const roleLower = (def.role || '').toLowerCase();
const matchingDomain = spec.domains.find(d =>
roleLower.includes(d.name.toLowerCase()) || d.name.toLowerCase().includes(roleLower)
);
if (matchingDomain) {
await this.subAgents.updatePerformance(def._id.toString(), {
totalExecutions: spec.totalDecisions,
successRate: matchingDomain.successRate,
avgQualityScore: spec.overallScore,
});
this.logger.debug(`Synced specialization for "${def.name}": ${matchingDomain.name} (${Math.round(matchingDomain.successRate * 100)}%)`);
}
}
}
} catch (err) {
this.logger.debug(`Specialization sync failed: ${(err as Error).message}`);
}
- Step 3: Verify updatePerformance method exists on SubAgentService
Check sub-agent.service.ts for an updatePerformance method. If it doesn't exist, add one:
async updatePerformance(definitionId: string, performance: {
totalExecutions: number;
successRate: number;
avgQualityScore: number;
}): Promise<void> {
await this.definitionModel.findByIdAndUpdate(definitionId, {
$set: { performance },
});
}
- Step 4: Verify by checking sub-agent definitions
After the daily sync runs (or trigger it manually), check MongoDB vitto-mind.sub-agent-definitions. The performance field should now have real data instead of zeros.
Summary
| Task | Gap | Risk | Backend |
|---|---|---|---|
| 1 | Pattern dedup fix | Low — deterministic, no data loss | ai-api |
| 2 | Decision categorization | Low — additive schema change | ai-api |
| 3 | Golden decisions in context | Low — additive to existing method | ai-api |
| 4 | Runtime intelligence injection | Medium — touches prompt pipeline | vitto-api |
| 5 | Specialization sync | Low — extends existing cron | vitto-api |
Tasks 1-3 are independent and can be done in parallel. Task 4 depends on Task 3 (golden decisions need to be in the response). Task 5 is independent.