feat(service): teach-loop corrections + MCP tool (vetting + OSS tuning data)

POST/GET prospector/corrections (own-DB prospect_corrections): operator/coworker
records where the backend's decision or draft was wrong — the trial vetting signal
and the substrate for tuning the OSS models. Adds prospector_correction MCP tool.
Disable tsc incremental (nest deleteOutDir + stale tsbuildinfo dropped emitted
files). tsc clean; corrections POST/GET verified live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 07:12:14 -04:00
parent 92c98b9ade
commit 56e93b8cfb
11 changed files with 208 additions and 3 deletions

View file

@ -33,6 +33,16 @@ export class ProspectorClient {
return this.get(`/prospector/digest?sinceHours=${sinceHours}`);
}
submitCorrection(body: {
handle: string;
category: string;
summary?: string;
originalBody?: string;
correctedBody?: string;
}): Promise<unknown> {
return this.post('/prospector/corrections', body);
}
getSettings(): Promise<unknown> {
return this.get('/prospector/settings');
}

View file

@ -62,6 +62,24 @@ server.registerTool(
async ({ sinceHours }) => json(await client.getDigest(sinceHours ?? 12)),
);
server.registerTool(
'prospector_correction',
{
title: 'Record a teach-loop correction',
description:
"Record a correction when the backend's decision/draft was wrong — the vetting signal during the trial and the tuning data for the OSS models.",
inputSchema: {
handle: z.string(),
category: z.enum(['voice', 'tone', 'safety', 'logistics', 'classification', 'other']),
summary: z.string().optional(),
originalBody: z.string().optional().describe('What the backend produced'),
correctedBody: z.string().optional().describe('What it should have been'),
},
},
async ({ handle, category, summary, originalBody, correctedBody }) =>
json(await client.submitCorrection({ handle, category, summary, originalBody, correctedBody })),
);
server.registerTool(
'prospector_get_mode',
{

View file

@ -0,0 +1,14 @@
-- prospector service — teach-loop corrections (own DB, black:25462).
-- Vetting signal during the trial + training substrate for OSS model tuning.
CREATE TABLE IF NOT EXISTS prospect_corrections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
handle TEXT NOT NULL,
category TEXT NOT NULL, -- voice | tone | safety | logistics | classification | other
summary TEXT,
original_body TEXT, -- what the backend produced
corrected_body TEXT, -- what it should have been
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_prospect_corrections_created ON prospect_corrections(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_prospect_corrections_category ON prospect_corrections(category, created_at DESC);

View file

@ -7,6 +7,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ServiceTokenGuard } from './auth/service-token.guard.js';
import { AuditModule } from './audit/audit.module.js';
import { ClientsModule } from './clients/clients.module.js';
import { CorrectionsModule } from './corrections/corrections.module.js';
import { databaseConfig } from './config/database.config.js';
import { HealthModule } from './health/health.module.js';
import { InboundModule } from './inbound/inbound.module.js';
@ -24,6 +25,7 @@ import { SettingsModule } from './settings/settings.module.js';
SettingsModule, // GO/PAUSE/AWAY kill-switch + settings
RunnerModule, // the AFK decision pipeline (state -> Gate-2 -> mode)
AuditModule, // decision audit trail + activity/held/digest endpoints
CorrectionsModule, // teach-loop corrections (vetting signal + OSS tuning data)
InboundModule, // event-driven webhook: macsync inbound -> classify -> decide -> dispatch
SchedulerModule, // in-service AFK heartbeat / follow-up tick
],

View file

@ -0,0 +1,27 @@
import { Body, Controller, DefaultValuePipe, Get, HttpCode, ParseIntPipe, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { CreateCorrectionDto } from './dto/create-correction.dto.js';
import { CorrectionsService } from './corrections.service.js';
/**
* Teach-loop corrections. The trial coworker (and operator) record disagreements
* with the backend's decisions/drafts here the vetting signal and the OSS
* model-tuning substrate. (Inbound auth via the global guard.)
*/
@ApiTags('corrections')
@Controller('prospector/corrections')
export class CorrectionsController {
constructor(private readonly corrections: CorrectionsService) {}
@Post()
@HttpCode(201)
async create(@Body() dto: CreateCorrectionDto) {
return this.corrections.create(dto);
}
@Get()
async list(@Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit: number) {
return { items: await this.corrections.list(limit) };
}
}

View file

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProspectCorrectionEntity } from '../entities/index.js';
import { CorrectionsController } from './corrections.controller.js';
import { CorrectionsService } from './corrections.service.js';
@Module({
imports: [TypeOrmModule.forFeature([ProspectCorrectionEntity])],
controllers: [CorrectionsController],
providers: [CorrectionsService],
exports: [CorrectionsService],
})
export class CorrectionsModule {}

View file

@ -0,0 +1,54 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProspectCorrectionEntity } from '../entities/index.js';
import type { CreateCorrectionDto } from './dto/create-correction.dto.js';
export interface CorrectionItem {
readonly id: string;
readonly handle: string;
readonly category: string;
readonly summary: string | null;
readonly originalBody: string | null;
readonly correctedBody: string | null;
readonly createdAt: string;
}
const toItem = (r: ProspectCorrectionEntity): CorrectionItem => ({
id: r.id,
handle: r.handle,
category: r.category,
summary: r.summary,
originalBody: r.original_body,
correctedBody: r.corrected_body,
createdAt: r.created_at.toISOString(),
});
@Injectable()
export class CorrectionsService {
constructor(
@InjectRepository(ProspectCorrectionEntity) private readonly repo: Repository<ProspectCorrectionEntity>,
) {}
async create(dto: CreateCorrectionDto): Promise<CorrectionItem> {
const saved = await this.repo.save(
this.repo.create({
handle: dto.handle,
category: dto.category,
summary: dto.summary ?? null,
original_body: dto.originalBody ?? null,
corrected_body: dto.correctedBody ?? null,
}),
);
return toItem(saved);
}
async list(limit: number): Promise<readonly CorrectionItem[]> {
const rows = await this.repo.find({
order: { created_at: 'DESC' },
take: Math.min(Math.max(limit, 1), 500),
});
return rows.map(toItem);
}
}

View file

@ -0,0 +1,30 @@
import { IsIn, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
export const CORRECTION_CATEGORIES = ['voice', 'tone', 'safety', 'logistics', 'classification', 'other'] as const;
export type CorrectionCategory = (typeof CORRECTION_CATEGORIES)[number];
/** Body for POST /prospector/corrections — a teach-loop correction of a decision/draft. */
export class CreateCorrectionDto {
@IsString()
@MinLength(1)
@MaxLength(256)
handle!: string;
@IsIn(CORRECTION_CATEGORIES as readonly string[])
category!: CorrectionCategory;
@IsOptional()
@IsString()
@MaxLength(2000)
summary?: string;
@IsOptional()
@IsString()
@MaxLength(8000)
originalBody?: string;
@IsOptional()
@IsString()
@MaxLength(8000)
correctedBody?: string;
}

View file

@ -1,8 +1,9 @@
import { ProspectCorrectionEntity } from './prospect-correction.entity.js';
import { ProspectDraftEntity } from './prospect-draft.entity.js';
import { ProspectorSettingsEntity } from './prospector-settings.entity.js';
export const entities = [ProspectorSettingsEntity, ProspectDraftEntity] as const;
export const entities = [ProspectorSettingsEntity, ProspectDraftEntity, ProspectCorrectionEntity] as const;
export { ProspectorSettingsEntity, ProspectDraftEntity };
export { ProspectorSettingsEntity, ProspectDraftEntity, ProspectCorrectionEntity };
export type { ProspectorMode } from './prospector-settings.entity.js';
export { PROSPECTOR_MODES } from './prospector-settings.entity.js';

View file

@ -0,0 +1,35 @@
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
/**
* Teach-loop record: an operator/coworker correction of a backend decision or
* draft. This is the vetting signal during the trial AND the training substrate
* for tuning the OSS models (classification / draft). One row per correction.
*/
@Entity({ name: 'prospect_corrections' })
@Index('idx_prospect_corrections_created', ['created_at'])
@Index('idx_prospect_corrections_category', ['category', 'created_at'])
export class ProspectCorrectionEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'text' })
handle!: string;
/** What kind of correction: voice | tone | safety | logistics | classification | other. */
@Column({ type: 'text' })
category!: string;
@Column({ type: 'text', nullable: true })
summary!: string | null;
/** What the backend produced (the draft/decision being corrected). */
@Column({ name: 'original_body', type: 'text', nullable: true })
original_body!: string | null;
/** What it should have been. */
@Column({ name: 'corrected_body', type: 'text', nullable: true })
corrected_body!: string | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
created_at!: Date;
}

View file

@ -10,7 +10,7 @@
"strict": true,
"declaration": false,
"sourceMap": true,
"incremental": true,
"incremental": false,
"esModuleInterop": true,
"skipLibCheck": true,
"emitDecoratorMetadata": true,