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:

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.

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 },
);

Check the learned_patterns schema in ai-infrastructure.schema.ts. If normalizedText is not a field, add it:

@Prop({ index: true })
normalizedText?: string;

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:

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;

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'];
}

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().

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:

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 [];
  }
}

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 } : {}),
};

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:

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: [].

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
  }
}

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.

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:

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 [];
  }
}

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}`);
}

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 },
  });
}

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.