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.
361 lines
9.8 KiB
TypeScript
361 lines
9.8 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { ensureRunsDir } from './runs-dir.js';
|
|
import type { GuidelineId } from './guideline-store.js';
|
|
|
|
export type JourneyId = string;
|
|
export type JourneyNodeId = string;
|
|
export type JourneyEdgeId = string;
|
|
|
|
export interface JourneyNode {
|
|
id: JourneyNodeId;
|
|
creationUtc: string;
|
|
action: string | null;
|
|
tools: string[];
|
|
metadata: Record<string, unknown>;
|
|
description: string | null;
|
|
labels: string[];
|
|
}
|
|
|
|
export interface JourneyEdge {
|
|
id: JourneyEdgeId;
|
|
creationUtc: string;
|
|
source: JourneyNodeId;
|
|
target: JourneyNodeId;
|
|
condition: string | null;
|
|
metadata: Record<string, unknown>;
|
|
}
|
|
|
|
export interface Journey {
|
|
id: JourneyId;
|
|
creationUtc: string;
|
|
description: string;
|
|
triggers: GuidelineId[];
|
|
title: string;
|
|
rootId: JourneyNodeId;
|
|
tags: string[];
|
|
labels: string[];
|
|
priority: number;
|
|
}
|
|
|
|
interface JourneyDocument {
|
|
id: string;
|
|
version: string;
|
|
creation_utc: string;
|
|
title: string;
|
|
description: string;
|
|
root_id: JourneyNodeId;
|
|
labels: string[];
|
|
priority: number;
|
|
}
|
|
|
|
interface NodeDocument {
|
|
id: string;
|
|
node_id: JourneyNodeId;
|
|
journey_id: JourneyId;
|
|
creation_utc: string;
|
|
action: string | null;
|
|
tools: string[];
|
|
metadata: Record<string, unknown>;
|
|
description: string | null;
|
|
labels: string[];
|
|
}
|
|
|
|
interface EdgeDocument {
|
|
id: string;
|
|
journey_id: JourneyId;
|
|
creation_utc: string;
|
|
source: JourneyNodeId;
|
|
target: JourneyNodeId;
|
|
condition: string | null;
|
|
metadata: Record<string, unknown>;
|
|
}
|
|
|
|
interface TriggerDocument {
|
|
id: string;
|
|
journey_id: JourneyId;
|
|
trigger: GuidelineId;
|
|
creation_utc: string;
|
|
}
|
|
|
|
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(name: string, projectRoot?: string): string {
|
|
const dir = join(ensureRunsDir(projectRoot), '..', 'journeys');
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
return join(dir, `${name}.json`);
|
|
}
|
|
|
|
function readCollection<T>(name: string, projectRoot?: string): T[] {
|
|
try {
|
|
return JSON.parse(readFileSync(dbPath(name, projectRoot), 'utf-8')) as T[];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function writeCollection<T>(name: string, data: T[], projectRoot?: string): void {
|
|
writeFileSync(dbPath(name, projectRoot), JSON.stringify(data, null, 2), 'utf-8');
|
|
}
|
|
|
|
export class JourneyStore {
|
|
createJourney(params: {
|
|
title: string;
|
|
description: string;
|
|
triggers?: GuidelineId[];
|
|
labels?: string[];
|
|
priority?: number;
|
|
}, projectRoot?: string): Journey {
|
|
const id = `jny_${generateId()}`;
|
|
const rootId = `node_${generateId()}`;
|
|
const creationUtc = new Date().toISOString();
|
|
|
|
const journey: Journey = {
|
|
id,
|
|
creationUtc,
|
|
description: params.description,
|
|
triggers: params.triggers || [],
|
|
title: params.title,
|
|
rootId,
|
|
tags: [],
|
|
labels: params.labels || [],
|
|
priority: params.priority || 0,
|
|
};
|
|
|
|
const journeys = readCollection<JourneyDocument>('journeys', projectRoot);
|
|
journeys.push({
|
|
id,
|
|
version: '0.7.0',
|
|
creation_utc: creationUtc,
|
|
title: params.title,
|
|
description: params.description,
|
|
root_id: rootId,
|
|
labels: params.labels || [],
|
|
priority: params.priority || 0,
|
|
});
|
|
writeCollection('journeys', journeys, projectRoot);
|
|
|
|
const root: JourneyNode = {
|
|
id: rootId,
|
|
creationUtc,
|
|
action: null,
|
|
tools: [],
|
|
metadata: {},
|
|
description: null,
|
|
labels: [],
|
|
};
|
|
const nodes = readCollection<NodeDocument>('nodes', projectRoot);
|
|
nodes.push({
|
|
id: `nd_${generateId()}`,
|
|
node_id: rootId,
|
|
journey_id: id,
|
|
creation_utc: creationUtc,
|
|
action: null,
|
|
tools: [],
|
|
metadata: {},
|
|
description: null,
|
|
labels: [],
|
|
});
|
|
writeCollection('nodes', nodes, projectRoot);
|
|
|
|
return journey;
|
|
}
|
|
|
|
readJourney(id: JourneyId, projectRoot?: string): Journey {
|
|
const journeys = readCollection<JourneyDocument>('journeys', projectRoot);
|
|
const doc = journeys.find(j => j.id === id);
|
|
if (!doc) throw new Error(`Journey '${id}' not found`);
|
|
|
|
const triggers = readCollection<TriggerDocument>('triggers', projectRoot)
|
|
.filter(t => t.journey_id === id)
|
|
.map(t => t.trigger);
|
|
|
|
return {
|
|
id: doc.id,
|
|
creationUtc: doc.creation_utc,
|
|
description: doc.description,
|
|
triggers,
|
|
title: doc.title,
|
|
rootId: doc.root_id,
|
|
tags: [],
|
|
labels: doc.labels || [],
|
|
priority: doc.priority || 0,
|
|
};
|
|
}
|
|
|
|
deleteJourney(id: JourneyId, projectRoot?: string): void {
|
|
let journeys = readCollection<JourneyDocument>('journeys', projectRoot);
|
|
const idx = journeys.findIndex(j => j.id === id);
|
|
if (idx === -1) throw new Error(`Journey '${id}' not found`);
|
|
journeys.splice(idx, 1);
|
|
writeCollection('journeys', journeys, projectRoot);
|
|
|
|
let nodes = readCollection<NodeDocument>('nodes', projectRoot);
|
|
nodes = nodes.filter(n => n.journey_id !== id);
|
|
writeCollection('nodes', nodes, projectRoot);
|
|
|
|
let edges = readCollection<EdgeDocument>('edges', projectRoot);
|
|
edges = edges.filter(e => e.journey_id !== id);
|
|
writeCollection('edges', edges, projectRoot);
|
|
|
|
let triggers = readCollection<TriggerDocument>('triggers', projectRoot);
|
|
triggers = triggers.filter(t => t.journey_id !== id);
|
|
writeCollection('triggers', triggers, projectRoot);
|
|
}
|
|
|
|
listJourneys(projectRoot?: string): Journey[] {
|
|
const journeys = readCollection<JourneyDocument>('journeys', projectRoot);
|
|
return journeys.map(j => this.readJourney(j.id, projectRoot));
|
|
}
|
|
|
|
createNode(journeyId: JourneyId, params: {
|
|
action?: string | null;
|
|
tools?: string[];
|
|
description?: string | null;
|
|
labels?: string[];
|
|
id?: JourneyNodeId;
|
|
}, projectRoot?: string): JourneyNode {
|
|
const nodeId = params.id || `node_${generateId()}`;
|
|
const creationUtc = new Date().toISOString();
|
|
|
|
const node: JourneyNode = {
|
|
id: nodeId,
|
|
creationUtc,
|
|
action: params.action ?? null,
|
|
tools: params.tools || [],
|
|
metadata: {},
|
|
description: params.description ?? null,
|
|
labels: params.labels || [],
|
|
};
|
|
|
|
const nodes = readCollection<NodeDocument>('nodes', projectRoot);
|
|
nodes.push({
|
|
id: `nd_${generateId()}`,
|
|
node_id: nodeId,
|
|
journey_id: journeyId,
|
|
creation_utc: creationUtc,
|
|
action: node.action,
|
|
tools: node.tools,
|
|
metadata: node.metadata,
|
|
description: node.description,
|
|
labels: node.labels,
|
|
});
|
|
writeCollection('nodes', nodes, projectRoot);
|
|
|
|
return node;
|
|
}
|
|
|
|
listNodes(journeyId: JourneyId, projectRoot?: string): JourneyNode[] {
|
|
const docs = readCollection<NodeDocument>('nodes', projectRoot)
|
|
.filter(n => n.journey_id === journeyId);
|
|
|
|
const nodes = docs.map(d => ({
|
|
id: d.node_id,
|
|
creationUtc: d.creation_utc,
|
|
action: d.action,
|
|
tools: d.tools,
|
|
metadata: d.metadata,
|
|
description: d.description,
|
|
labels: d.labels || [],
|
|
}));
|
|
|
|
nodes.push({
|
|
id: 'end' as JourneyNodeId,
|
|
creationUtc: new Date().toISOString(),
|
|
action: null,
|
|
tools: [],
|
|
metadata: {},
|
|
description: null,
|
|
labels: [],
|
|
});
|
|
|
|
return nodes;
|
|
}
|
|
|
|
createEdge(journeyId: JourneyId, params: {
|
|
source: JourneyNodeId;
|
|
target: JourneyNodeId;
|
|
condition?: string | null;
|
|
}, projectRoot?: string): JourneyEdge {
|
|
const creationUtc = new Date().toISOString();
|
|
const edge: JourneyEdge = {
|
|
id: `edge_${generateId()}`,
|
|
creationUtc,
|
|
source: params.source,
|
|
target: params.target,
|
|
condition: params.condition ?? null,
|
|
metadata: {},
|
|
};
|
|
|
|
const edges = readCollection<EdgeDocument>('edges', projectRoot);
|
|
edges.push({
|
|
id: edge.id,
|
|
journey_id: journeyId,
|
|
creation_utc: creationUtc,
|
|
source: params.source,
|
|
target: params.target,
|
|
condition: params.condition ?? null,
|
|
metadata: {},
|
|
});
|
|
writeCollection('edges', edges, projectRoot);
|
|
|
|
return edge;
|
|
}
|
|
|
|
listEdges(journeyId: JourneyId, nodeId?: JourneyNodeId, projectRoot?: string): JourneyEdge[] {
|
|
let docs = readCollection<EdgeDocument>('edges', projectRoot)
|
|
.filter(e => e.journey_id === journeyId);
|
|
|
|
if (nodeId) {
|
|
docs = docs.filter(e => e.source === nodeId || e.target === nodeId);
|
|
}
|
|
|
|
return docs.map(d => ({
|
|
id: d.id,
|
|
creationUtc: d.creation_utc,
|
|
source: d.source,
|
|
target: d.target,
|
|
condition: d.condition,
|
|
metadata: d.metadata,
|
|
}));
|
|
}
|
|
|
|
deleteEdge(edgeId: JourneyEdgeId, projectRoot?: string): void {
|
|
let edges = readCollection<EdgeDocument>('edges', projectRoot);
|
|
const idx = edges.findIndex(e => e.id === edgeId);
|
|
if (idx === -1) throw new Error(`Edge '${edgeId}' not found`);
|
|
edges.splice(idx, 1);
|
|
writeCollection('edges', edges, projectRoot);
|
|
}
|
|
|
|
addTrigger(journeyId: JourneyId, trigger: GuidelineId, projectRoot?: string): boolean {
|
|
const triggers = readCollection<TriggerDocument>('triggers', projectRoot);
|
|
if (triggers.find(t => t.journey_id === journeyId && t.trigger === trigger)) {
|
|
return false;
|
|
}
|
|
triggers.push({
|
|
id: `trg_${generateId()}`,
|
|
journey_id: journeyId,
|
|
trigger,
|
|
creation_utc: new Date().toISOString(),
|
|
});
|
|
writeCollection('triggers', triggers, projectRoot);
|
|
return true;
|
|
}
|
|
|
|
removeTrigger(journeyId: JourneyId, trigger: GuidelineId, projectRoot?: string): boolean {
|
|
let triggers = readCollection<TriggerDocument>('triggers', projectRoot);
|
|
const len = triggers.length;
|
|
triggers = triggers.filter(t => !(t.journey_id === journeyId && t.trigger === trigger));
|
|
writeCollection('triggers', triggers, projectRoot);
|
|
return triggers.length < len;
|
|
}
|
|
}
|