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.
252 lines
7.3 KiB
TypeScript
252 lines
7.3 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { ensureRunsDir } from './runs-dir.js';
|
|
|
|
export type GuidelineId = string;
|
|
export type TagId = string;
|
|
export type Criticality = 'low' | 'medium' | 'high';
|
|
export type GuidelineDocumentVersion = string;
|
|
|
|
export interface GuidelineContent {
|
|
condition: string;
|
|
action: string | null;
|
|
description: string | null;
|
|
}
|
|
|
|
export interface Guideline {
|
|
id: GuidelineId;
|
|
creationUtc: string;
|
|
content: GuidelineContent;
|
|
enabled: boolean;
|
|
tags: TagId[];
|
|
labels: string[];
|
|
metadata: Record<string, unknown>;
|
|
criticality: Criticality;
|
|
title: string | null;
|
|
priority: number;
|
|
}
|
|
|
|
export interface GuidelineDocument {
|
|
id: string;
|
|
version: GuidelineDocumentVersion;
|
|
creation_utc: string;
|
|
condition: string;
|
|
action: string | null;
|
|
description: string | null;
|
|
title: string | null;
|
|
criticality: string;
|
|
enabled: boolean;
|
|
metadata: Record<string, unknown>;
|
|
labels: string[];
|
|
priority: number;
|
|
}
|
|
|
|
export interface GuidelineUpdateParams {
|
|
condition?: string;
|
|
action?: string | null;
|
|
description?: string | null;
|
|
title?: string | null;
|
|
criticality?: Criticality;
|
|
enabled?: boolean;
|
|
priority?: number;
|
|
}
|
|
|
|
function generateId(): string {
|
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
let result = '';
|
|
for (let i = 0; i < 10; i++) {
|
|
result += chars[Math.floor(Math.random() * chars.length)];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function dbPath(projectRoot?: string): string {
|
|
const dir = join(ensureRunsDir(projectRoot), '..', 'guidelines');
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
return join(dir, 'guidelines.json');
|
|
}
|
|
|
|
function readDb(projectRoot?: string): GuidelineDocument[] {
|
|
const path = dbPath(projectRoot);
|
|
try {
|
|
return JSON.parse(readFileSync(path, 'utf-8')) as GuidelineDocument[];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function writeDb(docs: GuidelineDocument[], projectRoot?: string): void {
|
|
writeFileSync(dbPath(projectRoot), JSON.stringify(docs, null, 2), 'utf-8');
|
|
}
|
|
|
|
function toDocument(g: Guideline): GuidelineDocument {
|
|
return {
|
|
id: g.id,
|
|
version: '0.11.0',
|
|
creation_utc: g.creationUtc,
|
|
condition: g.content.condition,
|
|
action: g.content.action,
|
|
description: g.content.description,
|
|
title: g.title,
|
|
criticality: g.criticality,
|
|
enabled: g.enabled,
|
|
metadata: g.metadata,
|
|
labels: g.labels,
|
|
priority: g.priority,
|
|
};
|
|
}
|
|
|
|
function fromDocument(d: GuidelineDocument): Guideline {
|
|
return {
|
|
id: d.id,
|
|
creationUtc: d.creation_utc,
|
|
content: {
|
|
condition: d.condition,
|
|
action: d.action ?? null,
|
|
description: d.description ?? null,
|
|
},
|
|
title: d.title ?? null,
|
|
criticality: (d.criticality || 'medium') as Criticality,
|
|
enabled: d.enabled ?? true,
|
|
tags: [],
|
|
labels: d.labels ?? [],
|
|
metadata: d.metadata ?? {},
|
|
priority: d.priority ?? 0,
|
|
};
|
|
}
|
|
|
|
export class GuidelineDocumentStore {
|
|
createGuideline(params: {
|
|
condition: string;
|
|
action?: string | null;
|
|
description?: string | null;
|
|
title?: string | null;
|
|
criticality?: Criticality;
|
|
enabled?: boolean;
|
|
labels?: string[];
|
|
priority?: number;
|
|
id?: GuidelineId;
|
|
}, projectRoot?: string): Guideline {
|
|
const docs = readDb(projectRoot);
|
|
const id = params.id || `gl_${generateId()}`;
|
|
|
|
if (docs.find(d => d.id === id)) {
|
|
throw new Error(`Guideline with id '${id}' already exists`);
|
|
}
|
|
|
|
const guideline: Guideline = {
|
|
id,
|
|
creationUtc: new Date().toISOString(),
|
|
content: {
|
|
condition: params.condition,
|
|
action: params.action ?? null,
|
|
description: params.description ?? null,
|
|
},
|
|
title: params.title ?? null,
|
|
criticality: params.criticality ?? 'medium',
|
|
enabled: params.enabled ?? true,
|
|
tags: [],
|
|
labels: params.labels ?? [],
|
|
metadata: {},
|
|
priority: params.priority ?? 0,
|
|
};
|
|
|
|
docs.push(toDocument(guideline));
|
|
writeDb(docs, projectRoot);
|
|
return guideline;
|
|
}
|
|
|
|
listGuidelines(params?: {
|
|
tags?: TagId[];
|
|
labels?: string[];
|
|
}, projectRoot?: string): Guideline[] {
|
|
let docs = readDb(projectRoot);
|
|
|
|
if (params?.tags && params.tags.length > 0) {
|
|
const tagSet = new Set(params.tags);
|
|
docs = docs.filter(d => d.metadata['tags'] &&
|
|
Array.isArray(d.metadata['tags']) &&
|
|
(d.metadata['tags'] as string[]).some(t => tagSet.has(t)));
|
|
}
|
|
|
|
if (params?.labels && params.labels.length > 0) {
|
|
const labelSet = new Set(params.labels);
|
|
docs = docs.filter(d => {
|
|
const gl = fromDocument(d);
|
|
return params.labels!.every(l => gl.labels.includes(l));
|
|
});
|
|
}
|
|
|
|
return docs.map(fromDocument);
|
|
}
|
|
|
|
readGuideline(id: GuidelineId, projectRoot?: string): Guideline {
|
|
const docs = readDb(projectRoot);
|
|
const doc = docs.find(d => d.id === id);
|
|
if (!doc) throw new Error(`Guideline '${id}' not found`);
|
|
return fromDocument(doc);
|
|
}
|
|
|
|
updateGuideline(id: GuidelineId, params: GuidelineUpdateParams, projectRoot?: string): Guideline {
|
|
const docs = readDb(projectRoot);
|
|
const idx = docs.findIndex(d => d.id === id);
|
|
if (idx === -1) throw new Error(`Guideline '${id}' not found`);
|
|
|
|
const doc = docs[idx]!;
|
|
if (params.condition !== undefined) doc.condition = params.condition;
|
|
if (params.action !== undefined) doc.action = params.action;
|
|
if (params.description !== undefined) doc.description = params.description;
|
|
if (params.title !== undefined) doc.title = params.title;
|
|
if (params.criticality !== undefined) doc.criticality = params.criticality;
|
|
if (params.enabled !== undefined) doc.enabled = params.enabled;
|
|
if (params.priority !== undefined) doc.priority = params.priority;
|
|
|
|
docs[idx] = doc;
|
|
writeDb(docs, projectRoot);
|
|
return fromDocument(doc);
|
|
}
|
|
|
|
deleteGuideline(id: GuidelineId, projectRoot?: string): void {
|
|
const docs = readDb(projectRoot);
|
|
const idx = docs.findIndex(d => d.id === id);
|
|
if (idx === -1) throw new Error(`Guideline '${id}' not found`);
|
|
docs.splice(idx, 1);
|
|
writeDb(docs, projectRoot);
|
|
}
|
|
|
|
findGuideline(content: GuidelineContent, projectRoot?: string): Guideline {
|
|
const docs = readDb(projectRoot);
|
|
const doc = docs.find(d =>
|
|
d.condition === content.condition &&
|
|
(content.action === undefined || d.action === content.action),
|
|
);
|
|
if (!doc) throw new Error(`Guideline not found for condition='${content.condition}'`);
|
|
return fromDocument(doc);
|
|
}
|
|
|
|
upsertLabels(id: GuidelineId, labels: string[], projectRoot?: string): Guideline {
|
|
const docs = readDb(projectRoot);
|
|
const idx = docs.findIndex(d => d.id === id);
|
|
if (idx === -1) throw new Error(`Guideline '${id}' not found`);
|
|
|
|
const doc = docs[idx]!;
|
|
const current = new Set(doc.labels || []);
|
|
for (const l of labels) current.add(l);
|
|
doc.labels = [...current];
|
|
writeDb(docs, projectRoot);
|
|
return fromDocument(doc);
|
|
}
|
|
|
|
removeLabels(id: GuidelineId, labels: string[], projectRoot?: string): Guideline {
|
|
const docs = readDb(projectRoot);
|
|
const idx = docs.findIndex(d => d.id === id);
|
|
if (idx === -1) throw new Error(`Guideline '${id}' not found`);
|
|
|
|
const doc = docs[idx]!;
|
|
const removeSet = new Set(labels);
|
|
doc.labels = (doc.labels || []).filter(l => !removeSet.has(l));
|
|
writeDb(docs, projectRoot);
|
|
return fromDocument(doc);
|
|
}
|
|
}
|