From fdd3743f28ea2ff4d84a0af3840b9d4d60025896 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Fri, 3 Apr 2026 19:30:11 -0700 Subject: [PATCH] feat(phase2): import chronomind routines from plans --- backend/src/lib/ecosystem-phase2.test.ts | 64 ++++++ backend/src/lib/ecosystem-phase2.ts | 275 +++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 backend/src/lib/ecosystem-phase2.test.ts create mode 100644 backend/src/lib/ecosystem-phase2.ts diff --git a/backend/src/lib/ecosystem-phase2.test.ts b/backend/src/lib/ecosystem-phase2.test.ts new file mode 100644 index 0000000..8de2313 --- /dev/null +++ b/backend/src/lib/ecosystem-phase2.test.ts @@ -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'); +}); diff --git a/backend/src/lib/ecosystem-phase2.ts b/backend/src/lib/ecosystem-phase2.ts new file mode 100644 index 0000000..46b7f03 --- /dev/null +++ b/backend/src/lib/ecosystem-phase2.ts @@ -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 { + 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 { + 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'); +}