Implements audit-harness-inspired session lifecycle: audit session creation/end/recover/report-daily with JSONL buffer and graded context recovery (L0-L4). Guideline service for behavioral compliance rules (condition/action model with criticality). Correction service for persistent user correction tracking across agent sessions. 8 supporting skills: audit-start/end/report-daily/recover + command variants for slash-command integration.
187 lines
5.3 KiB
TypeScript
187 lines
5.3 KiB
TypeScript
import { readFile, writeFile, appendFile } from 'node:fs/promises';
|
|
import { existsSync } from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
|
|
export interface UserCorrectionRecord {
|
|
id: string;
|
|
record_type: 'conversation';
|
|
action_type: 'user_correction';
|
|
priority: 'critical_for_recovery';
|
|
timestamp: string;
|
|
original_claim: string;
|
|
correction: string;
|
|
principle_extracted: string;
|
|
persisted_to: string[];
|
|
}
|
|
|
|
const CORRECTIONS_REL = '.boo/corrections/index.json';
|
|
|
|
function correctionsDir(basePath?: string): string {
|
|
return resolve(basePath ?? process.cwd(), '.boo/corrections');
|
|
}
|
|
|
|
function correctionsPath(basePath?: string): string {
|
|
return resolve(basePath ?? process.cwd(), CORRECTIONS_REL);
|
|
}
|
|
|
|
function tryParseJson<T>(raw: string): T | null {
|
|
try {
|
|
return JSON.parse(raw) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export interface CorrectionsIndex {
|
|
corrections: UserCorrectionRecord[];
|
|
}
|
|
|
|
async function readCorrections(basePath?: string): Promise<CorrectionsIndex> {
|
|
try {
|
|
const raw = await readFile(correctionsPath(basePath), 'utf-8');
|
|
return tryParseJson<CorrectionsIndex>(raw) ?? { corrections: [] };
|
|
} catch {
|
|
return { corrections: [] };
|
|
}
|
|
}
|
|
|
|
async function writeCorrections(idx: CorrectionsIndex, basePath?: string): Promise<void> {
|
|
const dir = correctionsDir(basePath);
|
|
if (!existsSync(dir)) {
|
|
const { mkdir } = await import('node:fs/promises');
|
|
await mkdir(dir, { recursive: true });
|
|
}
|
|
await writeFile(correctionsPath(basePath), JSON.stringify(idx, null, 2), 'utf-8');
|
|
}
|
|
|
|
let idCounter = 0;
|
|
|
|
function nextId(): string {
|
|
idCounter++;
|
|
return `uc_${Date.now()}_${idCounter}`;
|
|
}
|
|
|
|
function isoNow(): string {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
/**
|
|
* Record a user correction. Stores it in .boo/corrections/index.json
|
|
* and returns the record with the assigned id.
|
|
*/
|
|
export async function recordCorrection(
|
|
originalClaim: string,
|
|
correction: string,
|
|
principleExtracted: string,
|
|
persistedTo: string[] = [],
|
|
basePath?: string,
|
|
): Promise<UserCorrectionRecord> {
|
|
const idx = await readCorrections(basePath);
|
|
const record: UserCorrectionRecord = {
|
|
id: nextId(),
|
|
record_type: 'conversation',
|
|
action_type: 'user_correction',
|
|
priority: 'critical_for_recovery',
|
|
timestamp: isoNow(),
|
|
original_claim: originalClaim,
|
|
correction,
|
|
principle_extracted: principleExtracted,
|
|
persisted_to: persistedTo,
|
|
};
|
|
idx.corrections.push(record);
|
|
await writeCorrections(idx, basePath);
|
|
return record;
|
|
}
|
|
|
|
/**
|
|
* Scan an audit_trail.jsonl file for user_correction records.
|
|
* Returns all matching records found in the file.
|
|
*/
|
|
export async function scanForCorrections(
|
|
auditPath: string,
|
|
): Promise<UserCorrectionRecord[]> {
|
|
try {
|
|
const raw = await readFile(auditPath, 'utf-8');
|
|
const lines = raw.split('\n').filter(Boolean);
|
|
const corrections: UserCorrectionRecord[] = [];
|
|
for (const line of lines) {
|
|
const record = tryParseJson<Record<string, unknown>>(line);
|
|
if (record?.action_type === 'user_correction') {
|
|
corrections.push(record as unknown as UserCorrectionRecord);
|
|
}
|
|
}
|
|
return corrections;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a proposed action contradicts any known user correction.
|
|
* Returns an array of contradiction warnings — empty means no contradictions.
|
|
*/
|
|
export function checkContradiction(
|
|
action: string,
|
|
corrections: UserCorrectionRecord[],
|
|
): { contradicts: boolean; warnings: { correction: UserCorrectionRecord; reason: string }[] } {
|
|
const warnings: { correction: UserCorrectionRecord; reason: string }[] = [];
|
|
|
|
for (const c of corrections) {
|
|
// Check if the action mentions the original claim's topic
|
|
const actionLower = action.toLowerCase();
|
|
const claimFragments = c.original_claim.toLowerCase().split(/\s+/).filter((w) => w.length > 4);
|
|
|
|
// If any significant word from the original claim appears in the proposed action,
|
|
// flag this as a potential contradiction
|
|
const matchingFragments = claimFragments.filter((f) => actionLower.includes(f));
|
|
if (matchingFragments.length >= 2) {
|
|
warnings.push({
|
|
correction: c,
|
|
reason: `Action "${action.slice(0, 60)}" may contradict prior correction: "${c.original_claim}" → "${c.correction}" (principle: ${c.principle_extracted})`,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
contradicts: warnings.length > 0,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add a file path to a correction's persisted_to array.
|
|
*/
|
|
export async function markPersisted(
|
|
correctionId: string,
|
|
filePath: string,
|
|
basePath?: string,
|
|
): Promise<UserCorrectionRecord | null> {
|
|
const idx = await readCorrections(basePath);
|
|
const record = idx.corrections.find((c) => c.id === correctionId);
|
|
if (!record) return null;
|
|
|
|
if (!record.persisted_to.includes(filePath)) {
|
|
record.persisted_to.push(filePath);
|
|
}
|
|
await writeCorrections(idx, basePath);
|
|
return record;
|
|
}
|
|
|
|
/**
|
|
* Get all stored user corrections.
|
|
*/
|
|
export async function listCorrections(basePath?: string): Promise<UserCorrectionRecord[]> {
|
|
const idx = await readCorrections(basePath);
|
|
return idx.corrections;
|
|
}
|
|
|
|
/**
|
|
* Append a correction record to an audit_trail.jsonl file (inline storage).
|
|
*/
|
|
export async function appendCorrectionToTrail(
|
|
trailPath: string,
|
|
correction: UserCorrectionRecord,
|
|
): Promise<void> {
|
|
await appendFile(trailPath, JSON.stringify(correction) + '\n', 'utf-8');
|
|
}
|