2026 03 19 Vitto Jarvis Implementation

Documentation for Plans

Vitto Jarvis — 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: Implement the remaining 9 features (S238 scope) from the Vitto Jarvis Master Spec to evolve Vitto into a Jarvis-level super agent.

Architecture: 6 phases, dependency-ordered. Phase 0 (Activity Card System) is blocking — all other phases depend on it for chat visibility. Phases 1-4 can parallelize after Phase 0. Phase 5 (Desktop) is independent. Phase 6 (Soul) is last. Each phase produces working, testable features.

Tech Stack: Angular 20+ (standalone, OnPush, signals), NestJS (backend-vitto-api), MongoDB/Mongoose, BullMQ, SSE streaming, Tauri v2 (desktop).

Spec: docs/superpowers/specs/2026-03-19-vitto-jarvis-master-spec.md

API Boundaries (CRITICAL — do not violate):


File Map

Phase 0: Activity Card System

Action File Responsibility
MODIFY libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-streaming.service.ts Extend StreamEvent union, route activity_card events
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/activity-card-host.component.ts Dynamic card component host
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/activity-card-registry.ts Maps cardType → component class
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/reasoning-trace-card.component.ts Reasoning hypothesis/observation card
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/background-task-card.component.ts Chain progress with cancel/pause
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/proactive-summary-card.component.ts Idle findings summary
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/sub-agent-approval-card.component.ts Approve/deny elevated tools
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/oauth-connect-card.component.ts OAuth connect button
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/skill-usage-card.component.ts Skill used with confidence
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/persona-adapted-card.component.ts Persona slider change with undo
CREATE libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/evolution-milestone-card.component.ts Stage transition
MODIFY libs/libs-ai/vitto-chat/src/lib/components/message/unified-message/unified-message.component.ts Render ActivityCardHostComponent for activity entries
MODIFY libs/libs-ai/vitto-chat/src/lib/components/message/unified-message/unified-message.component.html Template slot for activity cards

Phase 1: Reasoning Layer

Action File Responsibility
CREATE APIs/backend-vitto-api/src/app/engine/reasoning.service.ts Hypothesis extraction, observation linking, trace persistence
MODIFY APIs/backend-vitto-api/src/app/engine/agent-loop.service.ts Inject ReasoningService, enrich tool.starting/tool.called events
MODIFY APIs/backend-vitto-api/src/app/engine/engine.module.ts Register ReasoningService
MODIFY APIs/backend-vitto-api/src/app/mind/schemas/execution-trace.schema.ts Add reasoningTrace field

Phase 2: Background Task Chains

Action File Responsibility
CREATE APIs/backend-vitto-api/src/app/engine/background-task.service.ts Decompose, chain, cancel, pause
CREATE APIs/backend-vitto-api/src/app/engine/schemas/chain-checkpoint.schema.ts MongoDB schema for chain state
CREATE APIs/backend-vitto-api/src/app/engine/background-task.controller.ts REST endpoints for cancel/pause/status
MODIFY APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts Route complex tasks to BackgroundTaskService
MODIFY APIs/backend-vitto-api/src/app/engine/engine.module.ts Register service, schema, controller

Phase 3: Self-Improvement

Action File Responsibility
MODIFY APIs/backend-vitto-api/src/app/engine/skill-manager.service.ts Add confidenceTier, outcomeHistory, graduation logic
MODIFY APIs/backend-vitto-api/src/app/engine/phase-executor.service.ts Record real outcome per skill after VERIFY
MODIFY APIs/backend-vitto-api/src/app/eval/eval-runner.service.ts Add evalVersion, per-category scoring
MODIFY APIs/backend-vitto-api/src/app/engine/dossier-persona-adapter.service.ts Enable auto-apply for low-risk sliders

Phase 4: External Reach

Action File Responsibility
MODIFY APIs/backend-vitto-api/src/app/engine/sub-agent.service.ts Autonomy gating before elevated tool execution
MODIFY APIs/backend-vitto-api/src/app/tools/plugins/oauth.plugin.ts Emit oauth.connect_required SSE on auth failure

Phase 5: Desktop Daemon

Action File Responsibility
MODIFY libs/libs-ai/vitto-core/ (heartbeat service) Native idle detection, notification dispatch
MODIFY apps/vitto-desktop/src-tauri/src/lib.rs Version in tray menu
MODIFY apps/vitto-desktop/src-tauri/Cargo.toml Bump to 0.4.0

Phase 6: Soul & Identity

Action File Responsibility
MODIFY libs/libs-ai/vitto-hub/src/lib/components/brain-visualization/ Wire to live data APIs
MODIFY libs/libs-anim/vitto-avatar/src/lib/services/vitto-mood.service.ts Stage overlay for evolution

Phase 0: Activity Card System

This phase is BLOCKING. Every other phase depends on it for chat visibility. Build this first.

Task 0.1: Activity Card Registry & Host Component

Files:

// activity-card-registry.ts
import { Type } from '@angular/core';

export type ActivityCardType =
  | 'reasoning-trace'
  | 'background-task'
  | 'proactive-summary'
  | 'sub-agent-approval'
  | 'oauth-connect'
  | 'skill-usage'
  | 'persona-adapted'
  | 'evolution-milestone'
  | 'memory-recall'; // Reserved for S239 — card component built when S239 delivers

export interface ActivityCardData {
  cardType: ActivityCardType;
  data: Record<string, unknown>;
  interactive: boolean;
  timestamp: number;
}

// Registry populated lazily by each card component
const CARD_REGISTRY = new Map<ActivityCardType, Type<unknown>>();

export function registerActivityCard(type: ActivityCardType, component: Type<unknown>): void {
  CARD_REGISTRY.set(type, component);
}

export function getActivityCardComponent(type: ActivityCardType): Type<unknown> | undefined {
  return CARD_REGISTRY.get(type);
}
// activity-card-host.component.ts
import { Component, input, computed, ChangeDetectionStrategy } from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { ActivityCardData, getActivityCardComponent } from './activity-card-registry';

@Component({
  selector: 'lib-activity-card-host',
  standalone: true,
  imports: [NgComponentOutlet],
  template: `
    @if (cardComponent()) {
      <div class="activity-card-wrapper" [class.interactive]="cardData().interactive">
        <ng-container
          [ngComponentOutlet]="cardComponent()"
          [ngComponentOutletInputs]="{ data: cardData().data }" />
      </div>
    }
  `,
  styleUrl: './activity-card-host.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActivityCardHostComponent {
  readonly cardData = input.required<ActivityCardData>();

  readonly cardComponent = computed(() => {
    return getActivityCardComponent(this.cardData().cardType);
  });
}
// activity-card-host.component.scss
.activity-card-wrapper {
  margin: 8px 0;
  border-radius: 8px;
  overflow: hidden;

  &.interactive {
    border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
  }
}

Run: yarn build gograb-space --skip-nx-cache 2>&1 | head -20 Expected: 0 errors (component is created but not yet wired)

git add libs/libs-ai/vitto-chat/src/lib/components/activity-cards/
git commit -m "[S238.5] feat: add activity card registry and host component"

Task 0.2: First Card — Reasoning Trace Card

Files:

// reasoning-trace-card.component.ts
import { Component, input, signal, ChangeDetectionStrategy } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { registerActivityCard } from '../activity-card-registry';

interface ReasoningStep {
  toolName: string;
  hypothesis: string;
  observation: string;
  confidence: number;
  success: boolean;
}

@Component({
  selector: 'lib-reasoning-trace-card',
  standalone: true,
  imports: [DecimalPipe],
  templateUrl: './reasoning-trace-card.component.html',
  styleUrl: './reasoning-trace-card.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReasoningTraceCardComponent {
  readonly data = input.required<{
    hypothesis: string;
    steps: ReasoningStep[];
    conclusion?: string;
    overallConfidence?: number;
  }>();

  readonly expanded = signal(false);

  toggle(): void {
    this.expanded.update(v => !v);
  }
}

// Self-register
registerActivityCard('reasoning-trace', ReasoningTraceCardComponent);
<!-- reasoning-trace-card.component.html -->
<div class="reasoning-card" (click)="toggle()">
  <div class="reasoning-header">
    <span class="reasoning-icon">&#x1f9e0;</span>
    <span class="reasoning-title">Reasoning</span>
    <span class="reasoning-chevron" [class.expanded]="expanded()">&#x25B8;</span>
  </div>

  @if (expanded()) {
    <div class="reasoning-body">
      <div class="hypothesis">{{ data().hypothesis }}</div>

      @for (step of data().steps; track step.toolName) {
        <div class="reasoning-step" [class.success]="step.success" [class.failure]="!step.success">
          <span class="step-icon">{{ step.success ? '&#x2713;' : '&#x2717;' }}</span>
          <span class="step-tool">{{ step.toolName }}</span>
          <div class="step-observation">{{ step.observation }}</div>
        </div>
      }

      @if (data().conclusion) {
        <div class="conclusion">
          <strong>Conclusion:</strong> {{ data().conclusion }}
          @if (data().overallConfidence) {
            <span class="confidence">({{ (data().overallConfidence! * 100) | number:'1.0-0' }}%)</span>
          }
        </div>
      }
    </div>
  }
</div>
// reasoning-trace-card.component.scss
.reasoning-card {
  background: var(--surface-secondary, rgba(255, 255, 255, 0.03));
  border-radius: 6px;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 13px;
}

.reasoning-header {
  display: flex;
  align-items: center;
  gap: 6px;
  opacity: 0.7;
}

.reasoning-chevron {
  margin-left: auto;
  transition: transform 0.15s;
  &.expanded { transform: rotate(90deg); }
}

.reasoning-body {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border-color, rgba(255, 255, 255, 0.06));
}

.hypothesis {
  font-style: italic;
  margin-bottom: 8px;
  opacity: 0.8;
}

.reasoning-step {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 4px 0;
  &.success .step-icon { color: var(--success-color, #4caf50); }
  &.failure .step-icon { color: var(--error-color, #f44336); }
}

.step-observation {
  width: 100%;
  padding-left: 20px;
  opacity: 0.6;
  font-size: 12px;
}

.conclusion {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border-color, rgba(255, 255, 255, 0.06));
}

.confidence {
  opacity: 0.6;
  margin-left: 4px;
}

Run: yarn build gograb-space --skip-nx-cache 2>&1 | head -20 Expected: 0 errors

git add libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/reasoning-trace-card*
git commit -m "[S238.5] feat: add reasoning trace activity card component"

Task 0.3: Remaining 7 Card Components

Files:

Each card follows the same pattern as ReasoningTraceCardComponent: standalone, OnPush, signal inputs, self-registers via registerActivityCard().

Follow the pattern from Task 0.2. Each card has:

Key interfaces per card:

Run: yarn build gograb-space --skip-nx-cache 2>&1 | head -20 Expected: 0 errors

git add libs/libs-ai/vitto-chat/src/lib/components/activity-cards/cards/
git commit -m "[S238.5] feat: add 7 activity card components (background, proactive, approval, oauth, skill, persona, evolution)"

Task 0.4: Wire Activity Cards into Streaming Service

Files:

In vitto-chat-streaming.service.ts, find the StreamEvent type (lines 16-25). Add 'activity_card' to the union.

In handleStreamEvent() (lines 161-185), add a case for SSE events that should render as activity cards. Map backend event types to ActivityCardData:

// After existing event handling
if (eventType === 'activity_card' || this.isActivityCardEvent(eventType)) {
  const cardData = this.mapToActivityCard(eventType, eventData);
  if (cardData) {
    this.activityCards.update(cards => [...cards, cardData]);
  }
}

Add helper method:

private isActivityCardEvent(type: string): boolean {
  return type.startsWith('conscience.') ||
         type.startsWith('background.') ||
         type === 'subvitto.approval_required';
}

private mapToActivityCard(type: string, data: unknown): ActivityCardData | null {
  const mapping: Record<string, ActivityCardType> = {
    'conscience.skill_used': 'skill-usage',
    'conscience.persona_adapted': 'persona-adapted',
    'conscience.evolution.visual': 'evolution-milestone',
    'conscience.proactive_summary': 'proactive-summary',
    'background.step_completed': 'background-task',
    'background.chain_status': 'background-task',
    'subvitto.approval_required': 'sub-agent-approval',
  };
  const cardType = mapping[type];
  if (!cardType) return null;
  return { cardType, data: data as Record<string, unknown>, interactive: this.isInteractive(cardType), timestamp: Date.now() };
}
readonly activityCards = signal<ActivityCardData[]>([]);

Reset on new message stream start.

In unified-message.component.ts, import ActivityCardHostComponent. In the template, after tool result cards, render activity cards:

@for (card of activityCards(); track card.timestamp) {
  <lib-activity-card-host [cardData]="card" />
}

The activityCards for each message come from the streaming service, filtered by message/task ID.

Create libs/libs-ai/vitto-chat/src/lib/components/activity-cards/index.ts that imports all 8 card components (triggering their self-registration).

Run: yarn build gograb-space --skip-nx-cache 2>&1 | head -20 Expected: 0 errors

Manual test: Start yarn serve:space, open chat, send a message. No cards should appear yet (backend doesn't emit activity events yet), but the system should not break existing functionality.

git add libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-streaming.service.ts
git add libs/libs-ai/vitto-chat/src/lib/components/message/unified-message/
git add libs/libs-ai/vitto-chat/src/lib/components/activity-cards/index.ts
git commit -m "[S238.5] feat: wire activity card system into streaming service and message rendering"

Phase 1: Reasoning Layer

Task 1.1: ReasoningService Backend

Files:

In execution-trace.schema.ts, add after the existing fields (before the class closing):

@Prop({ type: [{
  hypothesis: String,
  toolName: String,
  observation: String,
  confidence: Number,
  success: Boolean,
  timestamp: Date,
}], default: [] })
reasoningTrace: Array<{
  hypothesis: string;
  toolName: string;
  observation: string;
  confidence: number;
  success: boolean;
  timestamp: Date;
}>;
// reasoning.service.ts
import { Injectable, Logger } from '@nestjs/common';

interface ReasoningEntry {
  hypothesis: string;
  toolName: string;
  observation: string;
  confidence: number;
  success: boolean;
  timestamp: Date;
}

@Injectable()
export class ReasoningService {
  private readonly logger = new Logger(ReasoningService.name);

  /**
   * Extract hypothesis from LLM response text using heuristics.
   * V1: regex-based extraction. Future: structured output from model.
   */
  extractHypothesis(llmResponseText: string, toolName: string): string {
    // Look for intent patterns in the LLM text preceding the tool call
    const patterns = [
      /(?:I'll|I will|Let me|I need to|I should|Going to)\s+(.{10,100}?)(?:\.|$)/i,
      /(?:To |In order to )\s*(.{10,80}?)(?:,|\.)/i,
      /(?:First|Next|Now)\s*,?\s*(.{10,80}?)(?:\.|$)/i,
    ];

    for (const pattern of patterns) {
      const match = llmResponseText.match(pattern);
      if (match?.[1]) return match[1].trim();
    }

    // Fallback: describe what the tool does
    return `Using ${toolName}`;
  }

  /**
   * Create observation from tool result.
   */
  createObservation(toolName: string, success: boolean, outputPreview: string): string {
    if (!success) return `${toolName} failed`;
    if (!outputPreview || outputPreview.length < 5) return `${toolName} completed`;
    // Truncate long outputs
    return outputPreview.length > 150
      ? outputPreview.substring(0, 147) + '...'
      : outputPreview;
  }

  /**
   * Enrich a tool.starting SSE event with reasoning metadata.
   */
  enrichToolStarting(
    eventData: Record<string, unknown>,
    llmResponseText: string,
  ): Record<string, unknown> {
    const toolName = (eventData['tool'] as string) || 'unknown';
    const hypothesis = this.extractHypothesis(llmResponseText, toolName);

    return {
      ...eventData,
      reasoning: { hypothesis },
    };
  }

  /**
   * Enrich a tool.called SSE event with observation.
   */
  enrichToolCalled(
    eventData: Record<string, unknown>,
    hypothesis: string,
  ): { enrichedData: Record<string, unknown>; entry: ReasoningEntry } {
    const toolName = (eventData['tool'] as string) || 'unknown';
    const success = (eventData['success'] as boolean) ?? true;
    const outputPreview = (eventData['outputPreview'] as string) || '';
    const observation = this.createObservation(toolName, success, outputPreview);

    // Simple confidence: 0.9 for success, 0.3 for failure
    const confidence = success ? 0.9 : 0.3;

    const entry: ReasoningEntry = {
      hypothesis,
      toolName,
      observation,
      confidence,
      success,
      timestamp: new Date(),
    };

    return {
      enrichedData: {
        ...eventData,
        reasoning: { hypothesis, observation, confidence },
      },
      entry,
    };
  }
}

In engine.module.ts, add ReasoningService to the providers array.

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

git add APIs/backend-vitto-api/src/app/engine/reasoning.service.ts
git add APIs/backend-vitto-api/src/app/engine/engine.module.ts
git add APIs/backend-vitto-api/src/app/mind/schemas/execution-trace.schema.ts
git commit -m "[S238.5] feat: add ReasoningService with hypothesis extraction and observation linking"

Task 1.2: Wire Reasoning into Agent Loop

Files:

Add to constructor: private readonly reasoning: ReasoningService

At lines ~503-515 (parallel tools path) and ~618-630 (single tool path), before emitting tool.starting, capture the last LLM response text and call reasoning.enrichToolStarting():

// Before the existing sseSubject.next({ type: 'tool.starting', ... })
const lastLlmText = messages[messages.length - 1]?.content || '';
const enrichedData = this.reasoning.enrichToolStarting(
  { tool: toolCall.name, iteration, inputPreview, category },
  typeof lastLlmText === 'string' ? lastLlmText : '',
);
sseSubject.next({ type: 'tool.starting', data: enrichedData });

Store the hypothesis for the tool.called enrichment:

const currentHypothesis = enrichedData.reasoning?.hypothesis || '';

At lines ~566-582 and ~652-668, when emitting tool.called, enrich with observation:

const { enrichedData, entry } = this.reasoning.enrichToolCalled(
  { tool: toolCall.name, iteration, durationMs, success, inputPreview, outputPreview, category },
  currentHypothesis,
);
sseSubject.next({ type: 'tool.called', data: enrichedData });

// Collect for persistence
reasoningTrace.push(entry);

Initialize const reasoningTrace: ReasoningEntry[] = [] at the start of run().

At the end of run(), where execution trace is saved, include the reasoningTrace array. Find where MindService.recordExecution() or similar is called and pass the trace.

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

Start yarn serve:backend-vitto. Open chat. Send a message that triggers tool use (e.g., "What modules exist in this space?"). Check SSE stream — tool.starting events should now have a reasoning.hypothesis field.

git add APIs/backend-vitto-api/src/app/engine/agent-loop.service.ts
git commit -m "[S238.5] feat: wire reasoning service into agent loop SSE events"

Phase 2: Background Task Chains

Task 2.1: Chain Checkpoint Schema & BackgroundTaskService

Files:

// chain-checkpoint.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema({ collection: 'chain-checkpoints', timestamps: true })
export class ChainCheckpoint extends Document {
  @Prop({ required: true, index: true }) chainId: string;
  @Prop({ required: true }) stepIndex: number;
  @Prop({ required: true }) stepName: string;
  @Prop({ type: String, enum: ['pending', 'running', 'completed', 'failed', 'cancelled'], default: 'pending' })
  status: string;
  @Prop({ type: Object }) output: Record<string, unknown>;
  @Prop({ type: Number, default: 0 }) costUsd: number;
  @Prop() startedAt: Date;
  @Prop() completedAt: Date;
  @Prop() taskId: string; // Links to the VittoTask that executed this step
  @Prop({ type: String }) errorMessage: string;
}

export const ChainCheckpointSchema = SchemaFactory.createForClass(ChainCheckpoint);
ChainCheckpointSchema.index({ chainId: 1, stepIndex: 1 }, { unique: true });
// background-task.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { ChainCheckpoint } from './schemas/chain-checkpoint.schema';

export interface ChainTemplate {
  name: string;
  steps: Array<{ name: string; systemPrompt: string; modelHint: 'cheap' | 'balanced' | 'powerful' }>;
}

const TEMPLATES: Record<string, ChainTemplate> = {
  RESEARCH: {
    name: 'Research & Report',
    steps: [
      { name: 'Web research', systemPrompt: 'Search the web for relevant information about: {task}. Collect key findings.', modelHint: 'cheap' },
      { name: 'Analyze findings', systemPrompt: 'Analyze the research findings and identify key themes, patterns, and insights. Previous findings: {prevOutput}', modelHint: 'balanced' },
      { name: 'Draft report', systemPrompt: 'Write a comprehensive report based on the analysis. Analysis: {prevOutput}', modelHint: 'balanced' },
      { name: 'Format & deliver', systemPrompt: 'Format the report for readability. Add sections, bullet points, and a summary. Draft: {prevOutput}', modelHint: 'cheap' },
    ],
  },
  CODE_REVIEW: {
    name: 'Code Review',
    steps: [
      { name: 'Read code', systemPrompt: 'Read and understand the code files related to: {task}', modelHint: 'cheap' },
      { name: 'Analyze patterns', systemPrompt: 'Analyze code quality, patterns, and potential issues. Code context: {prevOutput}', modelHint: 'balanced' },
      { name: 'Write review', systemPrompt: 'Write a structured code review with specific suggestions. Analysis: {prevOutput}', modelHint: 'balanced' },
    ],
  },
  GENERATE: {
    name: 'Generate & Verify',
    steps: [
      { name: 'Plan', systemPrompt: 'Plan the implementation for: {task}. Define the approach and files to create.', modelHint: 'balanced' },
      { name: 'Scaffold', systemPrompt: 'Create the file structure and boilerplate. Plan: {prevOutput}', modelHint: 'balanced' },
      { name: 'Implement', systemPrompt: 'Fill in the implementation details. Scaffold: {prevOutput}', modelHint: 'powerful' },
      { name: 'Verify', systemPrompt: 'Review and verify the implementation. Fix any issues. Implementation: {prevOutput}', modelHint: 'cheap' },
    ],
  },
};

@Injectable()
export class BackgroundTaskService {
  private readonly logger = new Logger(BackgroundTaskService.name);

  constructor(
    @InjectModel(ChainCheckpoint.name) private readonly checkpointModel: Model<ChainCheckpoint>,
  ) {}

  /**
   * Match a task description to a template.
   * Returns null if no template matches (task runs normally).
   */
  matchTemplate(taskDescription: string): ChainTemplate | null {
    const lower = taskDescription.toLowerCase();
    if (lower.includes('research') || lower.includes('investigate') || lower.includes('analyze and report'))
      return TEMPLATES.RESEARCH;
    if (lower.includes('review') && (lower.includes('code') || lower.includes('pr') || lower.includes('module')))
      return TEMPLATES.CODE_REVIEW;
    if (lower.includes('generate') || lower.includes('create') || lower.includes('build'))
      return TEMPLATES.GENERATE;
    return null;
  }

  async createChain(chainId: string, template: ChainTemplate): Promise<void> {
    const checkpoints = template.steps.map((step, index) => ({
      chainId,
      stepIndex: index,
      stepName: step.name,
      status: index === 0 ? 'pending' : 'pending',
    }));
    await this.checkpointModel.insertMany(checkpoints);
  }

  async getChainStatus(chainId: string): Promise<ChainCheckpoint[]> {
    return this.checkpointModel.find({ chainId }).sort({ stepIndex: 1 }).lean();
  }

  async updateStep(chainId: string, stepIndex: number, update: Partial<ChainCheckpoint>): Promise<void> {
    await this.checkpointModel.updateOne({ chainId, stepIndex }, { $set: update });
  }

  async cancelChain(chainId: string): Promise<void> {
    await this.checkpointModel.updateMany(
      { chainId, status: { $in: ['pending', 'running'] } },
      { $set: { status: 'cancelled' } },
    );
  }

  async pauseChain(chainId: string): Promise<{ pausedAfterStep: number }> {
    // Mark all pending steps as cancelled (will be re-created on resume)
    const running = await this.checkpointModel.findOne({ chainId, status: 'running' }).lean();
    const pauseAfter = running?.stepIndex ?? -1;
    await this.checkpointModel.updateMany(
      { chainId, status: 'pending', stepIndex: { $gt: pauseAfter } },
      { $set: { status: 'cancelled' } },
    );
    return { pausedAfterStep: pauseAfter };
  }
}

Add BackgroundTaskService to providers. Add MongooseModule.forFeature([{ name: ChainCheckpoint.name, schema: ChainCheckpointSchema }]) to imports.

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

git add APIs/backend-vitto-api/src/app/engine/schemas/chain-checkpoint.schema.ts
git add APIs/backend-vitto-api/src/app/engine/background-task.service.ts
git add APIs/backend-vitto-api/src/app/engine/engine.module.ts
git commit -m "[S238.5] feat: add BackgroundTaskService with chain checkpoints and templates"

Task 2.2: Background Task Controller & Engine Routing

Files:

// background-task.controller.ts
import { Controller, Get, Post, Param, UseGuards } from '@nestjs/common';
import { BackgroundTaskService } from './background-task.service';

@Controller('engine/background')
export class BackgroundTaskController {
  constructor(private readonly backgroundTask: BackgroundTaskService) {}

  @Get(':chainId/status')
  async getStatus(@Param('chainId') chainId: string) {
    return this.backgroundTask.getChainStatus(chainId);
  }

  @Post(':chainId/cancel')
  async cancel(@Param('chainId') chainId: string) {
    await this.backgroundTask.cancelChain(chainId);
    return { success: true };
  }

  @Post(':chainId/pause')
  async pause(@Param('chainId') chainId: string) {
    return this.backgroundTask.pauseChain(chainId);
  }

  @Post(':chainId/resume')
  async resume(@Param('chainId') chainId: string) {
    return this.backgroundTask.resumeChain(chainId);
  }
}

In vitto-engine.service.ts, find processTaskWorkflow(). Before the call to this.phaseExecutor.execute(task, sseSubject), add background task detection:

// Check if task should be decomposed into background chain
if (task.classification?.complexity === 'complex') {
  const template = this.backgroundTask.matchTemplate(task.description || task.message);
  if (template) {
    // Route to chain execution instead of direct execution
    return this.executeAsChain(task, template, sseSubject);
  }
}
// Otherwise: existing flow
await this.phaseExecutor.execute(task, sseSubject);

Add executeAsChain() method that creates checkpoints, iterates through steps, calls phaseExecutor.execute() per step with the step's system prompt injected, and emits background.step_completed SSE events.

Add BackgroundTaskController to controllers array.

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

git add APIs/backend-vitto-api/src/app/engine/background-task.controller.ts
git add APIs/backend-vitto-api/src/app/engine/vitto-engine.service.ts
git add APIs/backend-vitto-api/src/app/engine/engine.module.ts
git commit -m "[S238.5] feat: add background task controller and engine routing for chain execution"

Phase 3: Self-Improvement

Task 3.1: Skill Confidence Tiers

Files:

In skill-manager.service.ts, find the VittoSkill schema class (lines 28-149). Add these fields:

@Prop({ type: String, enum: ['experimental', 'validated', 'trusted'], default: 'experimental' })
confidenceTier: string;

@Prop({ type: [{ outcome: String, taskId: String, timestamp: Date }], default: [] })
outcomeHistory: Array<{ outcome: string; taskId: string; timestamp: Date }>;

@Prop({ type: Number, default: 0 }) failureCount: number;

@Prop({ type: Number, default: 0.5 }) rollingSuccessRate: number;

@Prop({ type: Date }) warmStartExpiresAt: Date;

Replace the existing recordOutcome() at lines 709-722:

async recordOutcome(skillName: string, outcome: 'success' | 'failure' | 'partial', taskId?: string): Promise<void> {
  try {
    const update: Record<string, unknown> = {
      $inc: { usageCount: 1 },
      $set: { lastUsed: new Date() },
    };

    if (outcome === 'success') {
      (update.$inc as Record<string, number>).successCount = 1;
    } else if (outcome === 'failure') {
      (update.$inc as Record<string, number>).failureCount = 1;
    }

    if (taskId) {
      update.$push = {
        outcomeHistory: {
          $each: [{ outcome, taskId, timestamp: new Date() }],
          $slice: -50, // Keep last 50
        },
      };
    }

    const skill = await this.skillModel.findOneAndUpdate(
      { name: skillName },
      update,
      { new: true },
    );

    if (!skill) return;

    // Recalculate rolling success rate
    const total = (skill.successCount || 0) + (skill.failureCount || 0);
    const rate = total > 0 ? (skill.successCount || 0) / total : 0.5;

    // Determine tier
    let newTier = skill.confidenceTier || 'experimental';
    const inWarmStart = skill.warmStartExpiresAt && new Date() < skill.warmStartExpiresAt;

    if (total >= 25 && rate >= 0.9) newTier = 'trusted';
    else if (total >= 10 && rate >= 0.8) newTier = 'validated';
    else if (total >= 5 && rate < 0.3 && !inWarmStart) newTier = 'experimental';

    await this.skillModel.updateOne(
      { _id: skill._id },
      { $set: { rollingSuccessRate: rate, confidenceTier: newTier } },
    );

    if (newTier !== skill.confidenceTier) {
      this.logger.log(`Skill "${skillName}" graduated: ${skill.confidenceTier} → ${newTier} (rate: ${(rate * 100).toFixed(0)}%)`);
    }
  } catch (err) {
    this.logger.warn(`recordOutcome failed for "${skillName}": ${err.message}`);
  }
}

Find the skill creation method (likely createSkill()). After creating the skill, set warmStartExpiresAt:

// After successful creation
await this.skillModel.updateOne(
  { _id: created._id },
  { $set: { warmStartExpiresAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000) } },
);

In phase-executor.service.ts, find line 611 where recordOutcome is called as this.skillManager.recordOutcome(matchedSkill.name, true). Change to pass the actual VERIFY phase result:

const outcome = verifyResult?.passed ? 'success' : 'failure';
await this.skillManager.recordOutcome(matchedSkill.name, outcome, task.taskId);

If verifyResult is not available at that point, use the task's final outcome status.

Find the skill retrieval logic (likely in skill-manager.service.ts method that returns skills for context injection, or in context-assembly.service.ts). Update to weight and sort by confidence tier:

// When retrieving skills for context injection:
const skills = await this.getRelevantSkills(taskDescription);
const weighted = skills.map(s => {
  const inWarmStart = s.warmStartExpiresAt && new Date() < s.warmStartExpiresAt;
  let weight = 0.4; // experimental default
  if (s.confidenceTier === 'trusted') weight = 1.0;
  else if (s.confidenceTier === 'validated') weight = 0.7;
  else if (inWarmStart) weight = 0.7; // warm start gets validated weight
  return { ...s, weight };
});
return weighted.sort((a, b) => b.weight - a.weight).slice(0, 3);

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

git add APIs/backend-vitto-api/src/app/engine/skill-manager.service.ts
git add APIs/backend-vitto-api/src/app/engine/phase-executor.service.ts
git commit -m "[S238.5] feat: add skill confidence tiers with graduation, warm start, and weighted retrieval"

Task 3.2: Eval Hardening & Persona Auto-Apply

Files:

In eval-runner.service.ts, find where eval results are stored. Add:

const evalVersion = process.env.GIT_COMMIT_SHA || process.env.RAILWAY_GIT_COMMIT_SHA || 'unknown';
// Include in result document:
{ ...result, evalVersion }

In the nightly cron method, group golden dataset entries by category. Calculate per-category averages. Alert on 5% drop per category (not just 10% overall):

const categories = ['general', 'task', 'code', 'memory', 'edge'];
for (const category of categories) {
  const categoryResults = results.filter(r => r.category === category);
  const avg = categoryResults.reduce((sum, r) => sum + r.score, 0) / categoryResults.length;
  const historical = await this.getHistoricalAvg(category, 7);
  if (historical > 0 && avg < historical * 0.95) {
    this.logger.warn(`Eval regression in ${category}: ${avg.toFixed(2)} vs ${historical.toFixed(2)} (7-day avg)`);
    // Emit alert
  }
}

In dossier-persona-adapter.service.ts, find the auto-apply disabled comment (line ~96-97). Change the logic:

// Low-risk sliders: auto-apply when confidence >= 0.8
const LOW_RISK_TRAITS = ['explanationDepth', 'formality'];
const isLowRisk = LOW_RISK_TRAITS.includes(trait);

if (isLowRisk && confidence >= 0.8) {
  // Auto-apply
  await this.applyPersonaChange(userId, trait, newValue);
  // Emit SSE event for chat card
  this.narrativeService.emitConscience(userId, {
    type: 'conscience.persona_adapted',
    data: { trait, oldValue, newValue, reason, autoApplied: true },
  });
} else if (confidence >= 0.5) {
  // Surface as suggestion via proactive dispatch (existing behavior)
  await this.dispatcher.dispatch({ ... });
}

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

git add APIs/backend-vitto-api/src/app/eval/eval-runner.service.ts
git add APIs/backend-vitto-api/src/app/engine/dossier-persona-adapter.service.ts
git commit -m "[S238.5] feat: add eval version tracking, per-category scoring, and persona auto-apply for low-risk traits"

Phase 4: External Reach

Task 4.1: Sub-Agent Autonomy Gating

Files:

private readonly ELEVATED_TOOLS = new Set([
  'run_command', 'write_file', 'execute_code', 'browser_interact',
  'computer_use', 'http_request',
]);

private hasElevatedTools(tools: string[]): boolean {
  return tools.some(t => this.ELEVATED_TOOLS.has(t));
}

In executeTask(), before calling this.agentLoop.run() (line ~719), add:

const toolList = this.getToolsForAgent(definition);
if (this.hasElevatedTools(toolList)) {
  // AutonomyService.shouldProceed() takes positional args: (taskId, phase, action, reasoning, options?)
  const decision = await this.autonomyService.shouldProceed(
    taskId,
    'EXECUTE',
    'sub_agent_elevated_execution',
    `Sub-agent "${definition.name}" requests elevated tools: ${toolList.join(', ')}`,
    { actionType: 'elevated_tool_execution' },
  );
  if (!decision.proceed) {
    // Emit approval request SSE event
    sseSubject?.next({
      type: 'subvitto.approval_required',
      data: {
        agentName: definition.name,
        role: definition.role,
        tools: toolList,
        taskDescription: prompt,
        taskId,
        checkpointId: decision.checkpoint?.id,
      },
    });
    // Poll checkpoint status until approved/denied (30s timeout)
    const approved = await this.pollCheckpointApproval(decision.checkpoint?.id, 30_000);
    if (!approved) {
      return { status: 'denied', result: 'Sub-agent execution denied by user' };
    }
  }
}

Add helper method:

private async pollCheckpointApproval(checkpointId: string | undefined, timeoutMs: number): Promise<boolean> {
  if (!checkpointId) return false;
  const startTime = Date.now();
  while (Date.now() - startTime < timeoutMs) {
    const checkpoint = await this.autonomyService.getCheckpoint(checkpointId);
    if (checkpoint?.status === 'approved') return true;
    if (checkpoint?.status === 'denied') return false;
    await new Promise(resolve => setTimeout(resolve, 2000)); // Poll every 2s
  }
  return false; // Timeout = denied
}

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

git add APIs/backend-vitto-api/src/app/engine/sub-agent.service.ts
git commit -m "[S238.5] feat: add autonomy gating for sub-agents with elevated tools"

Task 4.2: OAuth Chat Wiring

Files:

In oauth.plugin.ts, find where oauth_api_call handles the "not connected" case. Add SSE event emission:

// When oauth_api_call fails because user not connected
if (!connection || connection.status !== 'active') {
  // Emit SSE event for chat card
  const authUrl = await this.oauthService.getAuthUrl(userId, service, requiredScopes);
  sseSubject?.next({
    type: 'tool.called',
    data: {
      tool: 'oauth_connect',
      success: false,
      output: JSON.stringify({ authUrl, service, scopes: requiredScopes }),
      // This triggers the oauth-connect card in the frontend
    },
  });
  return { error: `Not connected to ${service}. Authorization required.`, authUrl };
}

Run: cd APIs/backend-vitto-api && npx tsc --noEmit 2>&1 | head -20 Expected: 0 errors

git add APIs/backend-vitto-api/src/app/tools/plugins/oauth.plugin.ts
git commit -m "[S238.5] feat: emit OAuth connect_required event for chat card rendering"

Phase 5: Desktop Daemon

Task 5.1: Smart Heartbeat & Version Tracking

Files:

Run: grep -rn "VittoHeartbeatService\|IDLE_THRESHOLD" libs/libs-ai/ --include="*.ts" | head -10

Check if idle detection already uses native Tauri commands or JS listeners. If JS-only, upgrade to native.

In the heartbeat service, replace JS mousemove/keydown idle detection with Tauri invoke:

private async getSystemIdleTime(): Promise<number> {
  try {
    // Use Tauri native command for system-wide idle detection
    const info = await invoke<{ idleSeconds: number }>('system_info');
    return info.idleSeconds * 1000; // Convert to ms
  } catch {
    // Fallback to JS-based detection
    return Date.now() - this.lastActivityTimestamp;
  }
}
private async notifyUser(summary: string): Promise<void> {
  try {
    await invoke('plugin:notification|send_notification', {
      title: 'Vitto',
      body: summary,
    });
  } catch (err) {
    this.logger.warn('Native notification failed, falling back to console');
  }
}

In lib.rs, find the system tray menu setup. Add an "About" item:

let version = env!("CARGO_PKG_VERSION");
let about_item = MenuItemBuilder::new(format!("About Vitto v{}", version))
    .enabled(false) // Display only
    .build(app)?;

In Cargo.toml, change version = "0.3.0" to version = "0.4.0".

Update docs/english/Apps/vitto-desktop-app/changelog.md with the v0.4.0 entry.

git add libs/libs-ai/vitto-core/ apps/vitto-desktop/ docs/english/Apps/vitto-desktop-app/changelog.md
git commit -m "[S238.5] feat: smart heartbeat with native idle detection, version tracking in tray"

Phase 6: Soul & Identity

Task 6.1: Wire Brain Visualization to Live Data

Files:

Run: ls libs/libs-ai/vitto-hub/src/lib/components/brain-visualization/

Inject the API services that provide:

Replace static/placeholder data with HTTP calls to backend-vitto-api endpoints. Display:

Run: yarn build gograb-space --skip-nx-cache 2>&1 | head -20

Navigate to /vitto/brain — verify live data appears.

git add libs/libs-ai/vitto-hub/src/lib/components/brain-visualization/
git commit -m "[S238.5] feat: wire brain visualization to live evolution, memory, and skill data"

Task 6.2: Avatar Stage Overlay

Files:

applyStageOverlay(stage: string): Partial<MoodProperties> {
  const overlays: Record<string, Partial<MoodProperties>> = {
    nascent: { auraIntensity: 0.2, particleCount: 0, glowRadius: 5 },
    aware: { auraIntensity: 0.4, particleCount: 3, glowRadius: 10 },
    connected: { auraIntensity: 0.6, particleCount: 8, glowRadius: 18 },
    attuned: { auraIntensity: 0.8, particleCount: 15, glowRadius: 25 },
    transcendent: { auraIntensity: 1.0, particleCount: 25, glowRadius: 35 },
  };
  return overlays[stage] || overlays.nascent;
}

Find where the current mood properties are computed. Merge stage overlay:

const stageOverlay = this.applyStageOverlay(currentEvolutionStage);
const finalProperties = { ...baseMoodProperties, ...stageOverlay };

The currentEvolutionStage can be fetched from the backend once on init and updated via conscience SSE events.

Manual test: Change the maturity data to trigger different stages. Check avatar visual.

git add libs/libs-anim/vitto-avatar/src/lib/services/vitto-mood.service.ts
git commit -m "[S238.5] feat: add evolution stage overlay to avatar mood properties"

Final Verification

After all phases are complete, run through the testing guide from the spec (Part IV: Verification Matrix):