feat(phase2): import chronomind routines from plans
This commit is contained in:
parent
9897d2cd09
commit
fdd3743f28
64
backend/src/lib/ecosystem-phase2.test.ts
Normal file
64
backend/src/lib/ecosystem-phase2.test.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import {
|
||||||
|
buildRoutineFromPlan,
|
||||||
|
loadLatestPlanArtifact,
|
||||||
|
loadLatestPlanCreatedEvent,
|
||||||
|
persistRoutineFromPlan,
|
||||||
|
} from './ecosystem-phase2.ts';
|
||||||
|
|
||||||
|
test('builds and persists a ChronoMind routine from the latest plan artifact', async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), 'chronomind-phase2-'));
|
||||||
|
await mkdir(join(root, 'indexes'), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
join(root, 'indexes', 'latest-plan.json'),
|
||||||
|
`${JSON.stringify({
|
||||||
|
id: 'art_plan_demo',
|
||||||
|
title: 'FlowMonk weekly plan for 2026-04-07',
|
||||||
|
ownership: { userId: 'saravana', orgId: null },
|
||||||
|
provenance: {
|
||||||
|
originProductId: 'flowmonk',
|
||||||
|
originActionId: 'plan_export_demo',
|
||||||
|
sessionId: 'sess_phase2',
|
||||||
|
runId: 'run_phase2_plan',
|
||||||
|
approvalId: null,
|
||||||
|
correlationId: 'corr_phase2',
|
||||||
|
lineage: [{ stepType: 'plan-exported', productId: 'flowmonk', actorType: 'agent', timestamp: '2026-04-03T12:00:00.000Z' }],
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
entries: [
|
||||||
|
{ taskTitle: 'Architecture review', durationMinutes: 60 },
|
||||||
|
{ taskTitle: 'API design', durationMinutes: 45 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}, null, 2)}\n`,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(root, 'indexes', 'latest-plan-created-event.json'),
|
||||||
|
`${JSON.stringify({ eventId: 'evt_plan_created_demo', trace: { correlationId: 'corr_phase2', causationId: null, parentEventId: null } }, null, 2)}\n`,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const planArtifact = await loadLatestPlanArtifact(root);
|
||||||
|
const planCreatedEvent = await loadLatestPlanCreatedEvent(root);
|
||||||
|
const generated = buildRoutineFromPlan({
|
||||||
|
planArtifact,
|
||||||
|
planCreatedEvent,
|
||||||
|
now: '2026-04-03T12:05:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(generated.routine.steps.length, 2);
|
||||||
|
assert.equal(generated.createdEvent.trace.causationId, 'evt_plan_created_demo');
|
||||||
|
|
||||||
|
await persistRoutineFromPlan({ ...generated, root });
|
||||||
|
|
||||||
|
const linkedEvent = JSON.parse(
|
||||||
|
await readFile(join(root, 'indexes', 'latest-routine-linked-event.json'), 'utf-8')
|
||||||
|
) as { payload: { relation: string } };
|
||||||
|
|
||||||
|
assert.equal(linkedEvent.payload.relation, 'generated-routine');
|
||||||
|
});
|
||||||
275
backend/src/lib/ecosystem-phase2.ts
Normal file
275
backend/src/lib/ecosystem-phase2.ts
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import type { RoutineDoc, RoutineStep } from '../modules/routines/types.ts';
|
||||||
|
|
||||||
|
const DEFAULT_PHASE2_ROOT = join(homedir(), '.bytelyst', 'ecosystem', 'phase2');
|
||||||
|
|
||||||
|
type PlanArtifact = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
ownership: { userId: string; orgId?: string | null };
|
||||||
|
provenance: {
|
||||||
|
originProductId: string;
|
||||||
|
originActionId?: string | null;
|
||||||
|
sessionId?: string | null;
|
||||||
|
runId?: string | null;
|
||||||
|
approvalId?: string | null;
|
||||||
|
correlationId?: string | null;
|
||||||
|
lineage: Array<{
|
||||||
|
stepType: string;
|
||||||
|
productId: string;
|
||||||
|
actorType: 'user' | 'agent' | 'system';
|
||||||
|
timestamp: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
payload: {
|
||||||
|
entries: Array<{
|
||||||
|
taskTitle: string;
|
||||||
|
durationMinutes: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlanCreatedEvent = {
|
||||||
|
eventId: string;
|
||||||
|
trace: {
|
||||||
|
correlationId: string | null;
|
||||||
|
causationId: string | null;
|
||||||
|
parentEventId: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type RoutineArtifact = {
|
||||||
|
id: string;
|
||||||
|
artifactType: 'routine';
|
||||||
|
schemaVersion: 1;
|
||||||
|
productId: 'chronomind';
|
||||||
|
sourceSurface: 'backend';
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdBy: { actorType: 'agent'; actorId: string };
|
||||||
|
ownership: { userId: string; orgId: string | null };
|
||||||
|
visibility: { scope: 'private' };
|
||||||
|
status: string;
|
||||||
|
tags: string[];
|
||||||
|
links: Array<{ relation: 'generated-routine'; targetArtifactId: string }>;
|
||||||
|
provenance: PlanArtifact['provenance'] & { runId: string };
|
||||||
|
payload: {
|
||||||
|
routineId: string;
|
||||||
|
stepCount: number;
|
||||||
|
totalDurationMinutes: number;
|
||||||
|
status: string;
|
||||||
|
isTemplate: boolean;
|
||||||
|
category: string;
|
||||||
|
steps: Array<{
|
||||||
|
label: string;
|
||||||
|
durationMinutes: number;
|
||||||
|
transition: string;
|
||||||
|
status: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArtifactEvent = {
|
||||||
|
eventId: string;
|
||||||
|
eventName: 'artifact.created' | 'artifact.linked';
|
||||||
|
eventVersion: 1;
|
||||||
|
occurredAt: string;
|
||||||
|
productId: 'chronomind';
|
||||||
|
sourceSurface: 'backend';
|
||||||
|
userId: string;
|
||||||
|
orgId: string | null;
|
||||||
|
sessionId: string | null;
|
||||||
|
runId: string;
|
||||||
|
artifactId: string;
|
||||||
|
actor: { actorType: 'agent'; actorId: string };
|
||||||
|
trace: {
|
||||||
|
correlationId: string | null;
|
||||||
|
causationId: string | null;
|
||||||
|
parentEventId: string | null;
|
||||||
|
};
|
||||||
|
payload:
|
||||||
|
| { artifactType: 'routine'; title: string; status: string }
|
||||||
|
| { sourceArtifactId: string; targetArtifactId: string; relation: 'generated-routine' };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPhase2Root(): string {
|
||||||
|
return process.env.BYTELYST_ECOSYSTEM_DIR ?? DEFAULT_PHASE2_ROOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadLatestPlanArtifact(root = getPhase2Root()): Promise<PlanArtifact> {
|
||||||
|
const raw = await readFile(join(root, 'indexes', 'latest-plan.json'), 'utf-8');
|
||||||
|
return JSON.parse(raw) as PlanArtifact;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadLatestPlanCreatedEvent(root = getPhase2Root()): Promise<PlanCreatedEvent | null> {
|
||||||
|
try {
|
||||||
|
const raw = await readFile(join(root, 'indexes', 'latest-plan-created-event.json'), 'utf-8');
|
||||||
|
return JSON.parse(raw) as PlanCreatedEvent;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRoutineFromPlan(params: {
|
||||||
|
planArtifact: PlanArtifact;
|
||||||
|
planCreatedEvent?: PlanCreatedEvent | null;
|
||||||
|
now?: string;
|
||||||
|
}) {
|
||||||
|
const now = params.now ?? new Date().toISOString();
|
||||||
|
const routineId = `routine_phase2_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
||||||
|
const artifactId = `art_routine_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
||||||
|
const runId = `run_routine_${randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
||||||
|
const steps: RoutineStep[] = params.planArtifact.payload.entries.map((entry, index) => ({
|
||||||
|
id: `step_${index + 1}`,
|
||||||
|
label: entry.taskTitle,
|
||||||
|
durationMinutes: entry.durationMinutes,
|
||||||
|
transition: '5m_break',
|
||||||
|
status: 'pending',
|
||||||
|
}));
|
||||||
|
const totalDurationMinutes = steps.reduce((sum, step) => sum + step.durationMinutes, 0);
|
||||||
|
|
||||||
|
const routine: RoutineDoc = {
|
||||||
|
id: routineId,
|
||||||
|
userId: params.planArtifact.ownership.userId,
|
||||||
|
productId: 'chronomind',
|
||||||
|
name: `Routine from ${params.planArtifact.title}`,
|
||||||
|
description: params.planArtifact.title,
|
||||||
|
steps,
|
||||||
|
totalDurationMinutes,
|
||||||
|
status: 'ready',
|
||||||
|
currentStepIndex: 0,
|
||||||
|
isTemplate: true,
|
||||||
|
category: 'phase2-import',
|
||||||
|
createdAt: now,
|
||||||
|
elapsedBeforePause: 0,
|
||||||
|
syncVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const artifact: RoutineArtifact = {
|
||||||
|
id: artifactId,
|
||||||
|
artifactType: 'routine',
|
||||||
|
schemaVersion: 1,
|
||||||
|
productId: 'chronomind',
|
||||||
|
sourceSurface: 'backend',
|
||||||
|
title: routine.name,
|
||||||
|
summary: `${steps.length} routine steps imported from FlowMonk plan`,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
createdBy: { actorType: 'agent', actorId: 'phase2-routine-importer' },
|
||||||
|
ownership: {
|
||||||
|
userId: routine.userId,
|
||||||
|
orgId: params.planArtifact.ownership.orgId ?? null,
|
||||||
|
},
|
||||||
|
visibility: { scope: 'private' },
|
||||||
|
status: routine.status,
|
||||||
|
tags: ['ecosystem', 'phase2', 'routine'],
|
||||||
|
links: [{ relation: 'generated-routine', targetArtifactId: params.planArtifact.id }],
|
||||||
|
provenance: {
|
||||||
|
...params.planArtifact.provenance,
|
||||||
|
runId,
|
||||||
|
lineage: [
|
||||||
|
...params.planArtifact.provenance.lineage,
|
||||||
|
{
|
||||||
|
stepType: 'routine-created',
|
||||||
|
productId: 'chronomind',
|
||||||
|
actorType: 'agent',
|
||||||
|
timestamp: now,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
routineId: routine.id,
|
||||||
|
stepCount: routine.steps.length,
|
||||||
|
totalDurationMinutes: routine.totalDurationMinutes,
|
||||||
|
status: routine.status,
|
||||||
|
isTemplate: routine.isTemplate,
|
||||||
|
category: routine.category ?? 'phase2-import',
|
||||||
|
steps: routine.steps.map(step => ({
|
||||||
|
label: step.label,
|
||||||
|
durationMinutes: step.durationMinutes,
|
||||||
|
transition: step.transition,
|
||||||
|
status: step.status,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdEvent: ArtifactEvent = {
|
||||||
|
eventId: `evt_routine_created_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
||||||
|
eventName: 'artifact.created',
|
||||||
|
eventVersion: 1,
|
||||||
|
occurredAt: now,
|
||||||
|
productId: 'chronomind',
|
||||||
|
sourceSurface: 'backend',
|
||||||
|
userId: routine.userId,
|
||||||
|
orgId: params.planArtifact.ownership.orgId ?? null,
|
||||||
|
sessionId: params.planArtifact.provenance.sessionId ?? null,
|
||||||
|
runId,
|
||||||
|
artifactId,
|
||||||
|
actor: { actorType: 'agent', actorId: 'phase2-routine-importer' },
|
||||||
|
trace: {
|
||||||
|
correlationId: params.planArtifact.provenance.correlationId ?? null,
|
||||||
|
causationId: params.planCreatedEvent?.eventId ?? null,
|
||||||
|
parentEventId: params.planCreatedEvent?.eventId ?? null,
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
artifactType: 'routine',
|
||||||
|
title: artifact.title,
|
||||||
|
status: artifact.status,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkedEvent: ArtifactEvent = {
|
||||||
|
eventId: `evt_routine_linked_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
||||||
|
eventName: 'artifact.linked',
|
||||||
|
eventVersion: 1,
|
||||||
|
occurredAt: now,
|
||||||
|
productId: 'chronomind',
|
||||||
|
sourceSurface: 'backend',
|
||||||
|
userId: routine.userId,
|
||||||
|
orgId: params.planArtifact.ownership.orgId ?? null,
|
||||||
|
sessionId: params.planArtifact.provenance.sessionId ?? null,
|
||||||
|
runId,
|
||||||
|
artifactId,
|
||||||
|
actor: { actorType: 'agent', actorId: 'phase2-routine-importer' },
|
||||||
|
trace: {
|
||||||
|
correlationId: params.planArtifact.provenance.correlationId ?? null,
|
||||||
|
causationId: createdEvent.eventId,
|
||||||
|
parentEventId: createdEvent.eventId,
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
sourceArtifactId: artifact.id,
|
||||||
|
targetArtifactId: params.planArtifact.id,
|
||||||
|
relation: 'generated-routine',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { routine, artifact, createdEvent, linkedEvent };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function persistRoutineFromPlan(params: {
|
||||||
|
artifact: RoutineArtifact;
|
||||||
|
createdEvent: ArtifactEvent;
|
||||||
|
linkedEvent: ArtifactEvent;
|
||||||
|
root?: string;
|
||||||
|
}) {
|
||||||
|
const root = params.root ?? getPhase2Root();
|
||||||
|
await writeJson(join(root, 'artifacts', 'routine', `${params.artifact.id}.json`), params.artifact);
|
||||||
|
await writeJson(join(root, 'events', 'artifact.created', `${params.createdEvent.eventId}.json`), params.createdEvent);
|
||||||
|
await writeJson(join(root, 'events', 'artifact.linked', `${params.linkedEvent.eventId}.json`), params.linkedEvent);
|
||||||
|
await writeJson(join(root, 'indexes', 'latest-routine.json'), params.artifact);
|
||||||
|
await writeJson(join(root, 'indexes', 'latest-routine-created-event.json'), params.createdEvent);
|
||||||
|
await writeJson(join(root, 'indexes', 'latest-routine-linked-event.json'), params.linkedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeJson(path: string, payload: unknown) {
|
||||||
|
await mkdir(dirname(path), { recursive: true });
|
||||||
|
await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user