import { execFile } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { realpathSync } from "node:fs"; import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; import { dirname, join, relative, resolve } from "node:path"; import { promisify } from "node:util"; import { ApprovalDecisionAction, type ApprovalDecisionAction as ApprovalDecisionActionValue, type BackendConfig, type BindingOverrides, DevflowError, Persona, Template, bindTemplatePersonas, hash, validateArtifact, } from "@devflow/core"; import { type DbClient, RunEventRepository, agentPersonas, approvalDecisions, approvalRequests, artifacts, commands, reviewFindings, runBindings, runEvents, runInputs, runPhases, runs, tuiSessions, workflowTemplates, } from "@devflow/db"; import type { SessionRuntime } from "@devflow/session"; import { and, asc, desc, eq, inArray, sql } from "drizzle-orm"; import { runSingleFakePhase } from "./fake-phase-harness.js"; type Database = DbClient["db"]; type TransactionDb = Parameters[0]>[0]; const terminalRunStates = ["completed", "failed", "aborted"] as const; const phaseMutationRunStates = ["executing", "planning"] as const; const execFileAsync = promisify(execFile); export interface RunEngine { startRun(input: RunStartInput): Promise<{ runId: string }>; signalApproval( runId: string, approvalRequestId: string, action: ApprovalDecisionActionValue, clientToken: string, comment?: string, ): Promise; pauseRun(runId: string): Promise; resumeRun(runId: string): Promise; abortRun(runId: string, reason: string): Promise; getStatus(runId: string): Promise; } export interface RunStartInput { requirementsMd: string; repoPath: string; baseBranch: string; templateName?: string; templateVersion?: number; worktreeRoot?: string; objective?: unknown; extra?: Record; overrides?: Partial; scenarios?: Record; runId?: string; } export type FakePhaseScenario = | string | { scenario?: string; repairScenario?: string; }; export interface DbRunEngineOptions { db: Database; sessions: SessionRuntime; workspaceRoot: string; availableBackends?: readonly BackendConfig[]; maxConcurrentRuns?: number; wait?: { timeoutMs?: number; pollIntervalMs?: number; stableMs?: number; }; } export interface RunStatus { run: { id: string; state: string; repoPath: string; baseBranch: string; worktreeRoot: string; currentPhaseId: string | null; finalReportPath: string | null; startedAt: Date | null; endedAt: Date | null; }; phases: Array<{ id: string; phaseKey: string; seq: number; state: string; attempts: number; }>; approvals: Array<{ id: string; phaseId: string | null; gateKey: string; state: string; }>; eventsTail: Array<{ id: string; seq: string; type: string; payload: unknown; ts: Date; }>; } interface TemplateRecord { id: string; hash: string; definition: unknown; } interface PersonaRecord { id: string; name: string; version: number; hash: string; definition: unknown; } interface StoredRunContext { template: Template; input: { requirementsMd: string; extra: unknown; }; } interface EnginePhaseDefinition { key: string; title: string; roles: string[]; expectedArtifact?: { path: string; schema: string; }; gates: string[]; timeoutMs?: number; } export class DbRunEngine implements RunEngine { private readonly db: Database; private readonly sessions: SessionRuntime; private readonly workspaceRoot: string; private readonly availableBackends: readonly BackendConfig[]; private readonly maxConcurrentRuns: number; private readonly wait: DbRunEngineOptions["wait"]; constructor(options: DbRunEngineOptions) { this.db = options.db; this.sessions = options.sessions; this.workspaceRoot = realpathSync(resolve(options.workspaceRoot)); this.availableBackends = options.availableBackends ?? [ { id: "fake", enabled: true, binaryPath: undefined }, ]; this.maxConcurrentRuns = options.maxConcurrentRuns ?? 4; this.wait = options.wait; } async startRun(input: RunStartInput): Promise<{ runId: string }> { const runId = input.runId ?? randomUUID(); const templateName = input.templateName ?? "development"; const templateVersion = input.templateVersion ?? 1; const repoPath = canonicalExistingPath(input.repoPath); const worktreeRoot = await this.resolveWorktreeRoot(runId, input.worktreeRoot); const templateRecord = await this.loadTemplate(templateName, templateVersion); const template = Template.parse(templateRecord.definition); const personaRecords = await this.loadPersonas(); const personas = personaRecords.map((row) => Persona.parse(row.definition)); const inputExtra = storeEngineMetadata(input.extra, input.scenarios); const inputHash = hash({ templateHash: templateRecord.hash, bindings: [], requirementsMd: input.requirementsMd, objective: input.objective ?? null, repoPath, baseBranch: input.baseBranch, extra: inputExtra, }); let runInserted = false; try { await this.db.transaction(async (tx) => { await this.lockStartAttempt(tx, repoPath, input.baseBranch); await this.assertRunCanStart(tx, repoPath, input.baseBranch); await tx.insert(runs).values({ id: runId, templateId: templateRecord.id, templateHash: templateRecord.hash, state: "created", repoPath, baseBranch: input.baseBranch, worktreeRoot, }); await tx.insert(runInputs).values({ runId, requirementsMd: input.requirementsMd, objective: input.objective ?? null, extra: inputExtra, inputHash, }); await tx.insert(runPhases).values( template.phases.map((phase, index) => ({ runId, phaseKey: phase.key, seq: index + 1, state: "pending", })), ); await new RunEventRepository(this.db).appendInTransaction(tx, { runId, type: "run.created", payload: { templateName, templateVersion }, idempotencyKey: `run.created:${runId}`, }); }); runInserted = true; const canonicalWorktreeRoot = await this.createGitWorktree( repoPath, input.baseBranch, runId, worktreeRoot, ); if (canonicalWorktreeRoot !== worktreeRoot) { await this.db .update(runs) .set({ worktreeRoot: canonicalWorktreeRoot, updatedAt: new Date() }) .where(eq(runs.id, runId)); } } catch (error) { if (isPgConstraintViolation(error, "ux_active_run_repo_base")) { throw await this.activeRunConflict(repoPath, input.baseBranch); } if (runInserted) { await this.markRunFailedIfActive(runId, "worktree_create_failed"); } throw error; } try { await this.lockBindings( runId, template, templateRecord.hash, personaRecords, personas, input, ); await this.advanceRun(runId); } catch (error) { if (await this.shouldPreserveHumanGateRun(runId, error)) { return { runId }; } await this.markRunFailedIfActive(runId, "start_run_failed"); throw error; } return { runId }; } private async lockStartAttempt( tx: TransactionDb, repoPath: string, baseBranch: string, ): Promise { await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('devflow:start-run-global'))`); await tx.execute( sql`SELECT pg_advisory_xact_lock(hashtext('devflow:start-run'), hashtext(${`${repoPath}:${baseBranch}`}))`, ); } private async assertRunCanStart( tx: TransactionDb, repoPath: string, baseBranch: string, ): Promise { const [existing] = await activeRunForRepoBase(tx, repoPath, baseBranch); if (existing !== undefined) { throw activeRunExists(existing.id, existing.state); } const [count] = await tx .select({ value: sql`count(*)::int` }) .from(runs) .where(sql`${runs.state} NOT IN ('completed', 'failed', 'aborted')`); if ((count?.value ?? 0) >= this.maxConcurrentRuns) { throw new DevflowError("Maximum concurrent runs reached", { class: "human_required", code: "max_concurrent_runs", recoveryHint: `maxConcurrentRuns=${this.maxConcurrentRuns}`, }); } } private async activeRunConflict(repoPath: string, baseBranch: string): Promise { const [existing] = await activeRunForRepoBase(this.db, repoPath, baseBranch); return activeRunExists(existing?.id ?? "unknown", existing?.state ?? "unknown"); } async signalApproval( runId: string, approvalRequestId: string, action: ApprovalDecisionActionValue, clientToken: string, comment?: string, ): Promise { const parsedAction = ApprovalDecisionAction.parse(action); const decision = await this.recordApprovalDecision( runId, approvalRequestId, parsedAction, clientToken, comment, ); if (parsedAction === "approve" || parsedAction === "request_changes") { try { await this.advanceRun(runId); } catch (error) { if (await this.shouldPreserveHumanGateRun(runId, error)) { return; } await this.markRunFailedIfActive(runId, "approval_advance_failed"); throw error; } return; } if (parsedAction === "reject") { await this.composeFinalReportBestEffort(runId, "failed"); return; } await this.composeFinalReportBestEffort(runId, "aborted"); } async pauseRun(runId: string): Promise { const eventRepository = new RunEventRepository(this.db); await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined || isTerminalRunState(run.state)) { return; } if (run.state === "paused") { return; } if (!["planning", "executing", "awaiting_approval"].includes(run.state)) { return; } const cause = `signal:${randomUUID()}`; await tx .update(runs) .set({ state: "paused", pausedFromState: run.state, updatedAt: new Date() }) .where(eq(runs.id, runId)); await eventRepository.appendInTransaction(tx, { runId, type: "run.paused", payload: { cause, pausedFromState: run.state }, idempotencyKey: `run.paused:${runId}:${cause}`, }); }); } async resumeRun(runId: string): Promise { const eventRepository = new RunEventRepository(this.db); let shouldAdvance = false; await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined || run.state !== "paused") { return; } if (await hasPendingHumanRequiredGate(tx, runId)) { throw approvalConflict(runId, "pending human-required gate must be resolved first"); } const nextState = run.pausedFromState ?? "executing"; const cause = `signal:${randomUUID()}`; await tx .update(runs) .set({ state: nextState, pausedFromState: null, updatedAt: new Date() }) .where(eq(runs.id, runId)); await eventRepository.appendInTransaction(tx, { runId, type: "run.resumed", payload: { cause }, idempotencyKey: `run.resumed:${runId}:${cause}`, }); shouldAdvance = nextState === "executing" || nextState === "planning"; }); if (shouldAdvance) { try { await this.advanceRun(runId, { resumeActivePhase: true }); } catch (error) { if (await this.shouldPreserveHumanGateRun(runId, error)) { return; } await this.markRunFailedIfActive(runId, "resume_advance_failed"); throw error; } } } async abortRun(runId: string, reason: string): Promise { const eventRepository = new RunEventRepository(this.db); let aborted = false; let sessionsToDispose: string[] = []; await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined || isTerminalRunState(run.state)) { return; } await tx .update(runs) .set({ state: "aborted", currentPhaseId: null, pausedFromState: null, endedAt: new Date(), updatedAt: new Date(), }) .where(eq(runs.id, runId)); await eventRepository.appendInTransaction(tx, { runId, type: "run.aborted", payload: { reason }, idempotencyKey: `run.aborted:${runId}`, }); await failActivePhasesInTransaction(tx, eventRepository, runId, "abort"); await abortPendingApprovalsInTransaction(tx, runId); sessionsToDispose = await markSessionsFailedInTransaction(tx, eventRepository, runId); aborted = true; }); if (aborted) { await this.disposeSessions(sessionsToDispose); await this.composeFinalReportBestEffort(runId, "aborted"); } } async getStatus(runId: string): Promise { const [run] = await this.db .select({ id: runs.id, state: runs.state, repoPath: runs.repoPath, baseBranch: runs.baseBranch, worktreeRoot: runs.worktreeRoot, currentPhaseId: runs.currentPhaseId, finalReportPath: runs.finalReportPath, startedAt: runs.startedAt, endedAt: runs.endedAt, }) .from(runs) .where(eq(runs.id, runId)) .limit(1); if (run === undefined) { throw runNotFound(runId); } const [phases, approvals, eventsTail] = await Promise.all([ this.db .select({ id: runPhases.id, phaseKey: runPhases.phaseKey, seq: runPhases.seq, state: runPhases.state, attempts: runPhases.attempts, }) .from(runPhases) .where(eq(runPhases.runId, runId)) .orderBy(asc(runPhases.seq)), this.db .select({ id: approvalRequests.id, phaseId: approvalRequests.phaseId, gateKey: approvalRequests.gateKey, state: approvalRequests.state, }) .from(approvalRequests) .where(eq(approvalRequests.runId, runId)) .orderBy(asc(approvalRequests.createdAt)), this.db .select({ id: runEvents.id, seq: runEvents.seq, type: runEvents.type, payload: runEvents.payload, ts: runEvents.ts, }) .from(runEvents) .where(eq(runEvents.runId, runId)) .orderBy(desc(runEvents.seq)) .limit(20), ]); return { run, phases, approvals, eventsTail: eventsTail.reverse().map((event) => ({ id: event.id.toString(), seq: event.seq.toString(), type: event.type, payload: event.payload, ts: event.ts, })), }; } private async lockBindings( runId: string, template: Template, templateHash: string, personaRecords: PersonaRecord[], personas: TemplateCompatiblePersona[], input: RunStartInput, ): Promise { const bindInput = { runId, template, personas, templateHash, availableBackends: this.availableBackends, ...(input.overrides === undefined ? {} : { overrides: input.overrides }), }; const result = bindTemplatePersonas(bindInput); const personaRowsByIdentity = new Map( personaRecords.map((row) => [`${row.name}@${row.version}`, row]), ); const bindingHashes = result.bindings .map((binding) => binding.bindingHash) .sort((left, right) => left.localeCompare(right)); const inputHashWithBindings = hash({ templateHash, bindings: bindingHashes, requirementsMd: input.requirementsMd, objective: input.objective ?? null, repoPath: canonicalExistingPath(input.repoPath), baseBranch: input.baseBranch, extra: storeEngineMetadata(input.extra, input.scenarios), }); await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined || run.state !== "created") { throw runStateChanged(runId, undefined, run?.state ?? "missing"); } await tx.insert(runBindings).values( result.bindings.map((binding) => { const personaRow = personaRowsByIdentity.get( `${binding.persona.name}@${binding.persona.version}`, ); if (personaRow === undefined) { throw new DevflowError("Binding persona row is missing", { class: "fatal", code: "internal_state_corruption", runId, }); } return { runId, roleId: binding.roleId, personaId: personaRow.id, personaHash: binding.personaHash, backend: binding.backend, bindingHash: binding.bindingHash, }; }), ); await tx .update(runInputs) .set({ inputHash: inputHashWithBindings }) .where(eq(runInputs.runId, runId)); await tx .update(runs) .set({ state: "bound", startedAt: new Date(), updatedAt: new Date() }) .where(and(eq(runs.id, runId), eq(runs.state, "created"))); await new RunEventRepository(this.db).appendInTransaction(tx, { runId, type: "run.started", payload: { templateHash }, idempotencyKey: `run.started:${runId}`, }); }); } private async advanceRun( runId: string, options: { resumeActivePhase?: boolean } = {}, ): Promise { while (true) { const context = await this.loadRunContext(runId); const [run] = await this.db .select({ state: runs.state, currentPhaseId: runs.currentPhaseId, finalReportPath: runs.finalReportPath, worktreeRoot: runs.worktreeRoot, }) .from(runs) .where(eq(runs.id, runId)) .limit(1); if (run === undefined) { throw runNotFound(runId); } if (run.state === "bound") { await this.promoteBoundRun(runId); continue; } if (run.state === "awaiting_approval" || run.state === "paused") { return; } if (isTerminalRunState(run.state)) { if (run.finalReportPath === null) { await this.composeFinalReportBestEffort(runId, run.state); } return; } if (run.state !== "executing" && run.state !== "planning") { throw new DevflowError("Run is not executable", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: `run_state=${run.state}`, }); } const phaseDefinitions = [ ...context.template.phases.map(toEnginePhaseDefinition), ...(await this.loadPlannedPhaseDefinitions(runId, run.worktreeRoot)), ]; const activePhase = await this.activePhase(runId); if (activePhase !== undefined) { if (!options.resumeActivePhase || run.currentPhaseId !== activePhase.id) { return; } await this.executePhase(runId, run.worktreeRoot, context, activePhase, phaseDefinitions); continue; } const nextPhase = await this.nextPendingPhase(runId); if (nextPhase === undefined) { if ( await this.ensurePlannedPhaseRows( runId, run.worktreeRoot, context.template.phases.map((phase) => phase.key), ) ) { continue; } await this.completeRun(runId); return; } await this.executePhase(runId, run.worktreeRoot, context, nextPhase, phaseDefinitions); } } private async promoteBoundRun(runId: string): Promise { await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined || run.state !== "bound") { return; } await tx .update(runs) .set({ state: "executing", updatedAt: new Date() }) .where(and(eq(runs.id, runId), eq(runs.state, "bound"))); }); } private async executePhase( runId: string, worktreeRoot: string, context: StoredRunContext, phaseRow: { id: string; phaseKey: string }, phaseDefinitions: readonly EnginePhaseDefinition[], ): Promise { const phaseDefinition = phaseDefinitions.find((phase) => phase.key === phaseRow.phaseKey); if (phaseDefinition === undefined) { throw new DevflowError("Run phase is missing from template", { class: "fatal", code: "internal_state_corruption", runId, phaseId: phaseRow.id, }); } if (phaseDefinition.expectedArtifact === undefined) { await this.setCurrentPhase(runId, phaseRow.id); await this.skipPhase(runId, phaseRow.id, phaseRow.phaseKey); await this.clearCurrentPhase(runId, phaseRow.id); return; } await this.prepareRunForPhase(runId, phaseRow.id, phaseDefinition.expectedArtifact.schema); const binding = await this.bindingForPhase(runId, phaseDefinition.roles); const expectedArtifactPath = resolve(worktreeRoot, phaseDefinition.expectedArtifact.path); const wait = phaseDefinition.timeoutMs === undefined ? this.wait : { ...this.wait, timeoutMs: phaseDefinition.timeoutMs }; const workflowApprovalGateKey = phaseDefinition.gates[0]; await this.sessions.trackOperation( runSingleFakePhase({ db: this.db, sessions: this.sessions, runId, phaseId: phaseRow.id, phaseKey: phaseRow.phaseKey, roleId: binding.roleId, worktreeRoot, expectedArtifactPath, expectedSchema: phaseDefinition.expectedArtifact.schema, instructions: buildPhaseInstructions( phaseRow.phaseKey, phaseDefinition.title, context.input.requirementsMd, scenarioForPhase(context.input.extra, phaseRow.phaseKey), ), ...(wait === undefined ? {} : { wait }), terminalRun: false, ...(workflowApprovalGateKey === undefined ? {} : { workflowApprovalGateKey, workflowApprovalPayload: { phaseKey: phaseRow.phaseKey, title: phaseDefinition.title, expectedArtifactPath, expectedSchema: phaseDefinition.expectedArtifact.schema, }, }), }), ); if (workflowApprovalGateKey === undefined) { await this.clearCurrentPhase(runId, phaseRow.id); } } private async completeRun(runId: string): Promise { const eventRepository = new RunEventRepository(this.db); let completed = false; await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined || isTerminalRunState(run.state)) { return; } if (!isPhaseMutationRunState(run.state)) { throw runStateChanged(runId, undefined, run.state); } completed = true; await tx .update(runs) .set({ state: "completed", currentPhaseId: null, pausedFromState: null, endedAt: new Date(), updatedAt: new Date(), }) .where(eq(runs.id, runId)); await eventRepository.appendInTransaction(tx, { runId, type: "run.completed", payload: {}, idempotencyKey: `run.completed:${runId}`, }); }); if (!completed) { return; } await this.composeFinalReportBestEffort(runId, "completed"); } private async prepareRunForPhase( runId: string, phaseId: string, expectedSchema: string, ): Promise { const state = expectedSchema === "dev/phase-plan@1" ? "planning" : "executing"; await this.db.transaction(async (tx) => { await assertRunCanMutatePhaseInTransaction(tx, runId, phaseId); await tx .update(runs) .set({ state, currentPhaseId: phaseId, updatedAt: new Date() }) .where(and(eq(runs.id, runId), inArray(runs.state, ["executing", "planning"]))); }); } private async setCurrentPhase(runId: string, phaseId: string): Promise { await this.db.transaction(async (tx) => { await assertRunCanMutatePhaseInTransaction(tx, runId, phaseId); await tx .update(runs) .set({ currentPhaseId: phaseId, updatedAt: new Date() }) .where(and(eq(runs.id, runId), inArray(runs.state, ["executing", "planning"]))); }); } private async clearCurrentPhase(runId: string, phaseId: string): Promise { await this.db .update(runs) .set({ currentPhaseId: null, updatedAt: new Date() }) .where(and(eq(runs.id, runId), eq(runs.currentPhaseId, phaseId))); } private async recordApprovalDecision( runId: string, approvalRequestId: string, action: ApprovalDecisionActionValue, clientToken: string, comment: string | undefined, ): Promise<{ replayed: boolean }> { const decisionIdempotencyKey = `${approvalRequestId}:${action}:${clientToken}`; const eventRepository = new RunEventRepository(this.db); let sessionsToDispose: string[] = []; const result = await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined) { throw runNotFound(runId); } await tx.execute( sql`SELECT 1 FROM ${approvalRequests} WHERE ${approvalRequests.id} = ${approvalRequestId} FOR UPDATE`, ); const [request] = await tx .select({ id: approvalRequests.id, runId: approvalRequests.runId, phaseId: approvalRequests.phaseId, state: approvalRequests.state, }) .from(approvalRequests) .where(and(eq(approvalRequests.id, approvalRequestId), eq(approvalRequests.runId, runId))) .limit(1); if (request === undefined) { throw new DevflowError("Approval request does not exist", { class: "human_required", code: "approval_not_found", runId, }); } const existingDecision = await existingDecisionForToken(tx, approvalRequestId, clientToken); if (existingDecision !== undefined) { if (existingDecision.action !== action) { throw approvalConflict(runId, "client token already used for a different action"); } return { replayed: true }; } if (isTerminalRunState(run.state)) { throw approvalConflict(runId, `run_state=${run.state}`); } if (run.state !== "awaiting_approval" && run.state !== "paused") { throw approvalConflict(runId, `run_state=${run.state}`); } if (run.state === "paused") { const resolvesHumanRequiredGate = (action === "reject" || action === "abort") && (request.phaseId === null || (await isHumanRequiredApprovalPhase(tx, runId, request.phaseId))); if (!resolvesHumanRequiredGate) { throw approvalConflict(runId, "paused runs must be resumed before approval decisions"); } } if (request.state !== "pending") { throw approvalConflict(runId, `approval_state=${request.state}`); } await tx.insert(approvalDecisions).values({ approvalRequestId, action, comment, idempotencyKey: decisionIdempotencyKey, }); await tx .update(approvalRequests) .set({ state: approvalStateForAction(action), resolvedAt: new Date() }) .where(eq(approvalRequests.id, approvalRequestId)); await eventRepository.appendInTransaction(tx, { runId, ...(request.phaseId === null ? {} : { phaseId: request.phaseId }), type: "approval.resolved", payload: { approvalRequestId, action }, idempotencyKey: `approval.resolved:${approvalRequestId}:${action}`, }); if (action === "approve") { if (request.phaseId !== null) { await completeApprovedPhase(tx, eventRepository, runId, request.phaseId); } await tx .update(runs) .set({ state: "executing", currentPhaseId: null, pausedFromState: null, updatedAt: new Date(), }) .where(eq(runs.id, runId)); await eventRepository.appendInTransaction(tx, { runId, type: "run.resumed", payload: { cause: `approval:${approvalRequestId}:${action}` }, idempotencyKey: `run.resumed:${runId}:approval:${approvalRequestId}:${action}`, }); return { replayed: false }; } if (action === "request_changes") { if (request.phaseId !== null) { await resetPhaseForChanges(tx, eventRepository, runId, request.phaseId); } await tx .update(runs) .set({ state: "planning", currentPhaseId: request.phaseId, pausedFromState: null, updatedAt: new Date(), }) .where(eq(runs.id, runId)); await eventRepository.appendInTransaction(tx, { runId, type: "run.resumed", payload: { cause: `approval:${approvalRequestId}:${action}` }, idempotencyKey: `run.resumed:${runId}:approval:${approvalRequestId}:${action}`, }); return { replayed: false }; } const state: "aborted" | "failed" = action === "abort" ? "aborted" : "failed"; await tx .update(runs) .set({ state, currentPhaseId: null, pausedFromState: null, endedAt: new Date(), updatedAt: new Date(), }) .where(eq(runs.id, runId)); if (request.phaseId !== null) { await failApprovalPhase(tx, eventRepository, runId, request.phaseId, action); } if (action === "abort" || action === "reject") { await abortPendingApprovalsInTransaction(tx, runId); sessionsToDispose = await markSessionsFailedInTransaction(tx, eventRepository, runId); } await eventRepository.appendInTransaction(tx, { runId, type: action === "abort" ? "run.aborted" : "run.failed", payload: { reason: `approval_${action}` }, idempotencyKey: `${action === "abort" ? "run.aborted" : "run.failed"}:${runId}`, }); return { replayed: false }; }); if (sessionsToDispose.length > 0) { await this.disposeSessions(sessionsToDispose); } return result; } private async composeFinalReport( runId: string, status: "completed" | "failed" | "aborted", ): Promise { const [run] = await this.db .select({ id: runs.id, templateHash: runs.templateHash, worktreeRoot: runs.worktreeRoot, finalReportPath: runs.finalReportPath, endedAt: runs.endedAt, }) .from(runs) .where(eq(runs.id, runId)) .limit(1); if (run === undefined) { throw runNotFound(runId); } const endedAt = (run.endedAt ?? new Date()).toISOString(); const report = await this.buildFinalReport(runId, run.templateHash, endedAt, status); const validation = validateArtifact("common/final-report@1", report); if (!validation.ok) { throw new DevflowError("Composed final report failed schema validation", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: JSON.stringify(validation.errors), }); } const reportRoot = join(this.workspaceRoot, runId); await mkdir(reportRoot, { recursive: true }); const jsonPath = join(reportRoot, `${runId}.report.json`); const markdownPath = join(reportRoot, `${runId}.report.md`); await atomicWriteFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`); await atomicWriteFile(markdownPath, renderMarkdownReport(report)); await this.db .update(runs) .set({ finalReportPath: markdownPath, endedAt: new Date(endedAt), updatedAt: new Date(), }) .where(eq(runs.id, runId)); return markdownPath; } private async buildFinalReport( runId: string, templateHash: string, endedAt: string, status: "completed" | "failed" | "aborted", ): Promise> { const [input, bindings, phases, approvals, findings, commandRows, artifactRows, eventsTail] = await Promise.all([ this.db.select().from(runInputs).where(eq(runInputs.runId, runId)).limit(1), this.db .select({ roleId: runBindings.roleId, personaHash: runBindings.personaHash, backend: runBindings.backend, }) .from(runBindings) .where(eq(runBindings.runId, runId)) .orderBy(asc(runBindings.roleId)), this.db .select() .from(runPhases) .where(eq(runPhases.runId, runId)) .orderBy(asc(runPhases.seq)), this.db .select() .from(approvalRequests) .where(eq(approvalRequests.runId, runId)) .orderBy(asc(approvalRequests.createdAt)), this.db .select() .from(reviewFindings) .where(eq(reviewFindings.runId, runId)) .orderBy(asc(reviewFindings.createdAt)), this.db .select({ kind: commands.kind, argv: commands.argv, exitCode: commands.exitCode, }) .from(commands) .where(eq(commands.runId, runId)) .orderBy(asc(commands.createdAt)), this.db .select() .from(artifacts) .where(eq(artifacts.runId, runId)) .orderBy(asc(artifacts.createdAt)), this.db .select({ id: runEvents.id, seq: runEvents.seq, type: runEvents.type, payload: runEvents.payload, ts: runEvents.ts, }) .from(runEvents) .where(eq(runEvents.runId, runId)) .orderBy(desc(runEvents.seq)) .limit(200), ]); const unresolved = approvals .filter((approval) => approval.state === "pending" || approval.state === "paused") .map((approval) => ({ type: "approval", approvalId: approval.id, gateKey: approval.gateKey, state: approval.state, })); return { runId, templateHash, bindings, inputs: serializeJson({ requirementsMd: input[0]?.requirementsMd ?? "", objective: input[0]?.objective ?? null, extra: input[0]?.extra ?? null, inputHash: input[0]?.inputHash ?? "", }), phases: serializeJson(phases), approvals: serializeJson(approvals), findings: serializeJson(findings), commands: commandRows.map((command) => ({ kind: command.kind, argv: command.argv, exit_code: command.exitCode, })), artifacts: serializeJson(artifactRows), events: { tail: eventsTail.reverse().map((event) => ({ id: event.id.toString(), seq: event.seq.toString(), type: event.type, payload: event.payload, ts: event.ts.toISOString(), })), }, unresolved, endedAt, status, }; } private async loadTemplate(name: string, version: number): Promise { const [template] = await this.db .select({ id: workflowTemplates.id, hash: workflowTemplates.hash, definition: workflowTemplates.definition, }) .from(workflowTemplates) .where(and(eq(workflowTemplates.name, name), eq(workflowTemplates.version, version))) .limit(1); if (template === undefined) { throw new DevflowError("Workflow template is not seeded", { class: "fatal", code: "template_load_failed", recoveryHint: `${name}@${version}`, }); } return template; } private async loadPersonas(): Promise { return this.db .select({ id: agentPersonas.id, name: agentPersonas.name, version: agentPersonas.version, hash: agentPersonas.hash, definition: agentPersonas.definition, }) .from(agentPersonas) .orderBy(asc(agentPersonas.name), desc(agentPersonas.version)); } private async loadRunContext(runId: string): Promise { const [row] = await this.db .select({ templateDefinition: workflowTemplates.definition, requirementsMd: runInputs.requirementsMd, extra: runInputs.extra, }) .from(runs) .innerJoin(workflowTemplates, eq(runs.templateId, workflowTemplates.id)) .innerJoin(runInputs, eq(runInputs.runId, runs.id)) .where(eq(runs.id, runId)) .limit(1); if (row === undefined) { throw runNotFound(runId); } return { template: Template.parse(row.templateDefinition), input: { requirementsMd: row.requirementsMd, extra: row.extra, }, }; } private async nextPendingPhase( runId: string, ): Promise<{ id: string; phaseKey: string } | undefined> { const [phase] = await this.db .select({ id: runPhases.id, phaseKey: runPhases.phaseKey }) .from(runPhases) .where(and(eq(runPhases.runId, runId), eq(runPhases.state, "pending"))) .orderBy(asc(runPhases.seq)) .limit(1); return phase; } private async activePhase(runId: string): Promise<{ id: string; phaseKey: string } | undefined> { const [phase] = await this.db .select({ id: runPhases.id, phaseKey: runPhases.phaseKey }) .from(runPhases) .where( and( eq(runPhases.runId, runId), inArray(runPhases.state, [ "running", "awaiting_artifact", "validating", "awaiting_approval", ]), ), ) .orderBy(asc(runPhases.seq)) .limit(1); return phase; } private async bindingForPhase( runId: string, roleIds: readonly string[], ): Promise<{ roleId: string }> { const bindings = await this.db .select({ roleId: runBindings.roleId }) .from(runBindings) .where(eq(runBindings.runId, runId)) .orderBy(asc(runBindings.roleId)); const binding = bindings.find((candidate) => roleIds.some( (roleId) => candidate.roleId === roleId || candidate.roleId.startsWith(`${roleId}#`), ), ); if (binding === undefined) { throw new DevflowError("No run binding satisfies phase roles", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: roleIds.join(","), }); } return binding; } private async ensurePlannedPhaseRows( runId: string, worktreeRoot: string, templatePhaseKeys: readonly string[], ): Promise { const plannedPhases = await this.loadPlannedPhaseDefinitions(runId, worktreeRoot); if (plannedPhases.length === 0) { return false; } assertPlannedPhaseKeys(runId, plannedPhases, templatePhaseKeys); await this.assertPlannedPhaseBindings(runId, plannedPhases); return this.db.transaction(async (tx) => { await assertRunCanMutatePhaseInTransaction(tx, runId); const existingPhases = await tx .select({ phaseKey: runPhases.phaseKey, seq: runPhases.seq }) .from(runPhases) .where(eq(runPhases.runId, runId)); const existingKeys = new Set(existingPhases.map((phase) => phase.phaseKey)); const missingPhases = plannedPhases.filter((phase) => !existingKeys.has(phase.key)); if (missingPhases.length === 0) { return false; } const maxSeq = existingPhases.reduce((max, phase) => Math.max(max, phase.seq), 0); await tx.insert(runPhases).values( missingPhases.map((phase, index) => ({ runId, phaseKey: phase.key, seq: maxSeq + index + 1, state: "pending", })), ); return true; }); } private async loadPlannedPhaseDefinitions( runId: string, _worktreeRoot: string, ): Promise { const [phasePlanArtifact] = await this.db .select({ path: artifacts.path, hash: artifacts.hash, schemaId: artifacts.schemaId }) .from(artifacts) .where( and( eq(artifacts.runId, runId), eq(artifacts.schemaId, "dev/phase-plan@1"), eq(artifacts.valid, true), ), ) .orderBy(desc(artifacts.createdAt)) .limit(1); if (phasePlanArtifact === undefined) { return []; } const bytes = await readFile(phasePlanArtifact.path); const currentHash = sha256Hex(bytes); if (currentHash !== phasePlanArtifact.hash) { throw new DevflowError("Phase plan artifact changed after validation", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: phasePlanArtifact.path, }); } const parsed = JSON.parse(bytes.toString("utf8")) as unknown; const validation = validateArtifact(phasePlanArtifact.schemaId, parsed); if (!validation.ok) { throw new DevflowError("Stored phase plan artifact no longer validates", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: JSON.stringify(validation.errors), }); } return parsePhasePlanDefinitions(runId, parsed); } private async assertPlannedPhaseBindings( runId: string, plannedPhases: readonly EnginePhaseDefinition[], ): Promise { for (const phase of plannedPhases) { await this.assertAllPhaseRolesBound(runId, phase.roles); } } private async assertAllPhaseRolesBound(runId: string, roleIds: readonly string[]): Promise { const bindings = await this.db .select({ roleId: runBindings.roleId }) .from(runBindings) .where(eq(runBindings.runId, runId)) .orderBy(asc(runBindings.roleId)); const missingRoles = roleIds.filter( (roleId) => !bindings.some( (binding) => binding.roleId === roleId || binding.roleId.startsWith(`${roleId}#`), ), ); if (missingRoles.length > 0) { throw new DevflowError("Planned phase role is not bound", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: missingRoles.join(","), }); } } private async skipPhase(runId: string, phaseId: string, phaseKey: string): Promise { const eventRepository = new RunEventRepository(this.db); await this.db.transaction(async (tx) => { await assertRunCanMutatePhaseInTransaction(tx, runId, phaseId); const [phase] = await tx .update(runPhases) .set({ attempts: sql`${runPhases.attempts} + 1`, state: "skipped", endedAt: new Date(), }) .where( and( eq(runPhases.id, phaseId), eq(runPhases.runId, runId), eq(runPhases.state, "pending"), ), ) .returning({ attempts: runPhases.attempts }); if (phase === undefined) { return; } const attempt = phase?.attempts ?? 1; await eventRepository.appendInTransaction(tx, { runId, phaseId, type: "phase.skipped", payload: { phaseKey, attempt }, idempotencyKey: `phase.skipped:${phaseId}:${attempt}`, }); }); } private async markRunFailedIfActive(runId: string, reason: string): Promise { const eventRepository = new RunEventRepository(this.db); let sessionsToDispose: string[] = []; let markedFailed = false; let reportStatus: "completed" | "failed" | "aborted" | undefined; await this.db.transaction(async (tx) => { const [run] = await lockRun(tx, runId); if (run === undefined) { return; } if (isTerminalRunState(run.state)) { if (run.finalReportPath === null) { reportStatus = run.state; } return; } markedFailed = true; reportStatus = "failed"; await failActivePhasesInTransaction(tx, eventRepository, runId, reason); await tx .update(runs) .set({ state: "failed", currentPhaseId: null, pausedFromState: null, endedAt: new Date(), updatedAt: new Date(), }) .where(eq(runs.id, runId)); await eventRepository.appendInTransaction(tx, { runId, type: "run.failed", payload: { reason }, idempotencyKey: `run.failed:${runId}`, }); sessionsToDispose = await markSessionsFailedInTransaction(tx, eventRepository, runId); }); if (markedFailed) { await this.disposeSessions(sessionsToDispose); } if (reportStatus !== undefined) { await this.composeFinalReportBestEffort(runId, reportStatus); } } private async composeFinalReportBestEffort( runId: string, status: "completed" | "failed" | "aborted", ): Promise { try { await this.composeFinalReport(runId, status); return; } catch { await this.writeStubFinalReport(runId, status).catch(() => undefined); } } private async writeStubFinalReport( runId: string, status: "completed" | "failed" | "aborted", ): Promise { const [run] = await this.db .select({ templateHash: runs.templateHash, endedAt: runs.endedAt }) .from(runs) .where(eq(runs.id, runId)) .limit(1); const endedAt = (run?.endedAt ?? new Date()).toISOString(); const report = { runId, templateHash: run?.templateHash ?? "0".repeat(64), bindings: [], inputs: {}, phases: [], approvals: [], findings: [], commands: [], artifacts: [], events: { tail: [] }, unresolved: ["final_report_compose_failed"], endedAt, status, }; const reportRoot = join(this.workspaceRoot, runId); await mkdir(reportRoot, { recursive: true }); const jsonPath = join(reportRoot, `${runId}.report.json`); const markdownPath = join(reportRoot, `${runId}.report.md`); await atomicWriteFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`); await atomicWriteFile(markdownPath, renderMarkdownReport(report)); await this.db .update(runs) .set({ finalReportPath: markdownPath, endedAt: new Date(endedAt), updatedAt: new Date(), }) .where(eq(runs.id, runId)); } private async shouldPreserveHumanGateRun(runId: string, error: unknown): Promise { if (!(error instanceof DevflowError) || error.class !== "human_required") { return false; } const [run] = await this.db .select({ state: runs.state }) .from(runs) .where(eq(runs.id, runId)) .limit(1); return run?.state === "paused" || run?.state === "awaiting_approval"; } async recoverMissingFinalReports( options: { runIds?: readonly string[] } = {}, ): Promise { const conditions = [ inArray(runs.state, ["completed", "failed", "aborted"]), sql`${runs.finalReportPath} IS NULL`, ]; if (options.runIds !== undefined) { if (options.runIds.length === 0) { return []; } conditions.push(inArray(runs.id, [...options.runIds])); } const terminalRuns = await this.db .select({ id: runs.id, state: runs.state }) .from(runs) .where(and(...conditions)); const recoveredRunIds: string[] = []; for (const run of terminalRuns) { await this.composeFinalReportBestEffort( run.id, run.state as "completed" | "failed" | "aborted", ); const [updated] = await this.db .select({ finalReportPath: runs.finalReportPath }) .from(runs) .where(eq(runs.id, run.id)) .limit(1); if (updated?.finalReportPath !== null && updated?.finalReportPath !== undefined) { recoveredRunIds.push(run.id); } } return recoveredRunIds; } private async resolveWorktreeRoot( runId: string, requestedWorktreeRoot?: string, ): Promise { const runRoot = join(this.workspaceRoot, runId); const worktreeRoot = requestedWorktreeRoot ?? join(runRoot, "main"); if (!isPathInsideOrEqual(resolve(worktreeRoot), resolve(runRoot))) { throw new DevflowError("Worktree root must live under the run workspace root", { class: "fatal", code: "workspace_permissions", recoveryHint: worktreeRoot, }); } await mkdir(runRoot, { recursive: true }); const canonicalRunRoot = realpathSync(runRoot); const resolvedWorktreeRoot = resolve(worktreeRoot); await mkdir(dirname(resolvedWorktreeRoot), { recursive: true }); if (!isPathInsideOrEqual(resolvedWorktreeRoot, canonicalRunRoot)) { throw new DevflowError("Resolved worktree root escaped the run workspace root", { class: "fatal", code: "workspace_permissions", recoveryHint: resolvedWorktreeRoot, }); } return resolvedWorktreeRoot; } private async createGitWorktree( repoPath: string, baseBranch: string, runId: string, worktreeRoot: string, ): Promise { const branchName = `devflow/${runId}/main`; try { await execFileAsync( "git", ["-C", repoPath, "worktree", "add", "-b", branchName, worktreeRoot, baseBranch], { env: gitChildEnv(), maxBuffer: 1024 * 1024 }, ); return realpathSync(worktreeRoot); } catch (cause) { throw new DevflowError("Failed to create git worktree", { class: "human_required", code: "workspace_permissions", runId, cause, recoveryHint: `git worktree add -b ${branchName} ${worktreeRoot} ${baseBranch}`, }); } } private async disposeSessions(sessionIds: readonly string[]): Promise { await Promise.all( sessionIds.map((sessionId) => this.sessions.dispose({ sessionId }).catch(() => undefined)), ); } } export interface M4ProcessRestartSweepOptions { runIds?: readonly string[]; } export async function sweepM4ProcessRestart( db: Database, options: M4ProcessRestartSweepOptions = {}, ): Promise<{ sweptRunIds: string[]; failedSessionIds: string[] }> { if (options.runIds !== undefined && options.runIds.length === 0) { return { sweptRunIds: [], failedSessionIds: [] }; } const eventRepository = new RunEventRepository(db); const sweptRunIds: string[] = []; const failedSessionIds: string[] = []; const activeRunFilter = options.runIds === undefined ? sql`${runs.state} NOT IN ('completed', 'failed', 'aborted')` : and( inArray(runs.id, [...options.runIds]), sql`${runs.state} NOT IN ('completed', 'failed', 'aborted')`, ); await db.transaction(async (tx) => { const activeRuns = await tx .select({ id: runs.id }) .from(runs) .where(activeRunFilter) .orderBy(asc(runs.createdAt)); for (const activeRun of activeRuns) { const [run] = await lockRun(tx, activeRun.id); if (run === undefined || isTerminalRunState(run.state)) { continue; } await failActivePhasesInTransaction( tx, eventRepository, activeRun.id, "process_restart_unrecovered", ); await tx .update(runs) .set({ state: "failed", currentPhaseId: null, pausedFromState: null, finalReportPath: null, endedAt: new Date(), updatedAt: new Date(), }) .where(eq(runs.id, activeRun.id)); await eventRepository.appendInTransaction(tx, { runId: activeRun.id, type: "run.failed", payload: { reason: "process_restart_unrecovered" }, idempotencyKey: `run.failed:${activeRun.id}`, }); await abortPendingApprovalsInTransaction(tx, activeRun.id); failedSessionIds.push( ...(await markSessionsFailedInTransaction(tx, eventRepository, activeRun.id)), ); sweptRunIds.push(activeRun.id); } }); return { sweptRunIds, failedSessionIds }; } type TemplateCompatiblePersona = Persona; async function activeRunForRepoBase( db: Database | TransactionDb, repoPath: string, baseBranch: string, ) { return db .select({ id: runs.id, state: runs.state }) .from(runs) .where( and( eq(runs.repoPath, repoPath), eq(runs.baseBranch, baseBranch), sql`${runs.state} NOT IN ('completed', 'failed', 'aborted')`, ), ) .orderBy(asc(runs.createdAt)) .limit(1); } function activeRunExists(currentRunId: string, currentState: string): DevflowError { return new DevflowError("An active run already exists for this repo and base branch", { class: "human_required", code: "active_run_exists", recoveryHint: JSON.stringify({ currentRunId, currentState }), }); } function isPgConstraintViolation(error: unknown, constraint: string): boolean { return ( typeof error === "object" && error !== null && "constraint" in error && (error as { constraint?: unknown }).constraint === constraint ); } async function lockRun(tx: TransactionDb, runId: string) { await tx.execute(sql`SELECT 1 FROM ${runs} WHERE ${runs.id} = ${runId} FOR UPDATE`); return tx .select({ state: runs.state, pausedFromState: runs.pausedFromState, finalReportPath: runs.finalReportPath, }) .from(runs) .where(eq(runs.id, runId)) .limit(1); } async function assertRunCanMutatePhaseInTransaction( tx: TransactionDb, runId: string, phaseId?: string, ) { const [run] = await lockRun(tx, runId); if (run === undefined || !isPhaseMutationRunState(run.state)) { throw runStateChanged(runId, phaseId, run?.state ?? "missing"); } } function isPhaseMutationRunState(state: string): boolean { return phaseMutationRunStates.includes(state as (typeof phaseMutationRunStates)[number]); } function runStateChanged(runId: string, phaseId: string | undefined, state: string): DevflowError { return new DevflowError("Run left active state before engine mutation", { class: "human_required", code: "run_state_changed", runId, ...(phaseId === undefined ? {} : { phaseId }), recoveryHint: `run_state=${state}`, }); } async function hasPendingHumanRequiredGate(tx: TransactionDb, runId: string): Promise { const pendingGates = await tx .select({ phaseId: approvalRequests.phaseId, phaseState: runPhases.state }) .from(approvalRequests) .leftJoin(runPhases, eq(approvalRequests.phaseId, runPhases.id)) .where(and(eq(approvalRequests.runId, runId), eq(approvalRequests.state, "pending"))); return pendingGates.some((gate) => gate.phaseId === null || gate.phaseState === "failed"); } async function isHumanRequiredApprovalPhase( tx: TransactionDb, runId: string, phaseId: string, ): Promise { const [phase] = await tx .select({ state: runPhases.state }) .from(runPhases) .where(and(eq(runPhases.id, phaseId), eq(runPhases.runId, runId))) .limit(1); return phase?.state === "failed"; } async function abortPendingApprovalsInTransaction(tx: TransactionDb, runId: string) { const pendingApprovals = await tx .select({ id: approvalRequests.id, }) .from(approvalRequests) .where(eq(approvalRequests.runId, runId)); for (const approval of pendingApprovals.filter((request) => request.id !== undefined)) { await tx .update(approvalRequests) .set({ state: "aborted", resolvedAt: new Date() }) .where(and(eq(approvalRequests.id, approval.id), eq(approvalRequests.state, "pending"))); } } async function failActivePhasesInTransaction( tx: TransactionDb, eventRepository: RunEventRepository, runId: string, reason: string, ) { const activePhases = await tx .select({ id: runPhases.id, phaseKey: runPhases.phaseKey, attempts: runPhases.attempts, }) .from(runPhases) .where( and( eq(runPhases.runId, runId), inArray(runPhases.state, [ "running", "awaiting_artifact", "validating", "awaiting_approval", ]), ), ); for (const phase of activePhases) { const attempt = Math.max(phase.attempts, 1); const [updated] = await tx .update(runPhases) .set({ state: "failed", endedAt: new Date() }) .where( and( eq(runPhases.id, phase.id), inArray(runPhases.state, [ "running", "awaiting_artifact", "validating", "awaiting_approval", ]), ), ) .returning({ id: runPhases.id }); if (updated === undefined) { continue; } await eventRepository.appendInTransaction(tx, { runId, phaseId: phase.id, type: "phase.failed", payload: { phaseKey: phase.phaseKey, attempt, reason }, idempotencyKey: `phase.failed:${phase.id}:${attempt}`, }); } } async function markSessionsFailedInTransaction( tx: TransactionDb, eventRepository: RunEventRepository, runId: string, ): Promise { const sessions = await tx .select({ id: tuiSessions.id, roleId: tuiSessions.roleId }) .from(tuiSessions) .where(eq(tuiSessions.runId, runId)); const activeSessions = sessions.filter((session) => session.id !== undefined); if (activeSessions.length === 0) { return []; } await tx .update(tuiSessions) .set({ state: "FAILED_NEEDS_HUMAN" }) .where(eq(tuiSessions.runId, runId)); for (const session of activeSessions) { await eventRepository.appendInTransaction(tx, { runId, type: "session.failed", payload: { sessionId: session.id, roleId: session.roleId }, idempotencyKey: `session.failed:${session.id}`, }); } return activeSessions.map((session) => session.id); } async function completeApprovedPhase( tx: TransactionDb, eventRepository: RunEventRepository, runId: string, phaseId: string, ) { const [phase] = await tx .select({ phaseKey: runPhases.phaseKey, attempts: runPhases.attempts }) .from(runPhases) .where(and(eq(runPhases.id, phaseId), eq(runPhases.runId, runId))) .limit(1); if (phase === undefined) { throw new DevflowError("Approval phase does not exist", { class: "fatal", code: "internal_state_corruption", runId, phaseId, }); } await tx .update(runPhases) .set({ state: "completed", endedAt: new Date() }) .where(eq(runPhases.id, phaseId)); await releaseWaitingApprovalSessions(tx, eventRepository, runId, phaseId); await eventRepository.appendInTransaction(tx, { runId, phaseId, type: "phase.completed", payload: { phaseKey: phase.phaseKey, attempt: phase.attempts }, idempotencyKey: `phase.completed:${phaseId}:${phase.attempts}`, }); } async function resetPhaseForChanges( tx: TransactionDb, eventRepository: RunEventRepository, runId: string, phaseId: string, ) { const [phase] = await tx .select({ id: runPhases.id }) .from(runPhases) .where(and(eq(runPhases.id, phaseId), eq(runPhases.runId, runId))) .limit(1); if (phase === undefined) { throw new DevflowError("Approval phase does not exist", { class: "fatal", code: "internal_state_corruption", runId, phaseId, }); } await tx .update(runPhases) .set({ state: "pending", startedAt: null, endedAt: null }) .where(eq(runPhases.id, phaseId)); await releaseWaitingApprovalSessions(tx, eventRepository, runId, phaseId); } async function releaseWaitingApprovalSessions( tx: TransactionDb, eventRepository: RunEventRepository, runId: string, phaseId: string, ) { const waitingSessions = await tx .select({ id: tuiSessions.id, lastPromptHash: tuiSessions.lastPromptHash, roleId: tuiSessions.roleId, }) .from(tuiSessions) .where(and(eq(tuiSessions.runId, runId), eq(tuiSessions.state, "WAITING_FOR_APPROVAL"))); for (const session of waitingSessions) { if (session.lastPromptHash === null) { throw new DevflowError("Approval-waiting session is missing prompt hash", { class: "fatal", code: "internal_state_corruption", runId, phaseId, recoveryHint: `session_id=${session.id}`, }); } await eventRepository.appendInTransaction(tx, { runId, phaseId, type: "session.idle", payload: { sessionId: session.id, roleId: session.roleId, dedupKey: session.lastPromptHash, }, idempotencyKey: `session.idle:${session.id}:${session.lastPromptHash}`, }); } await tx .update(tuiSessions) .set({ state: "READY" }) .where(and(eq(tuiSessions.runId, runId), eq(tuiSessions.state, "WAITING_FOR_APPROVAL"))); } async function failApprovalPhase( tx: TransactionDb, eventRepository: RunEventRepository, runId: string, phaseId: string, action: "reject" | "abort", ) { const [phase] = await tx .select({ phaseKey: runPhases.phaseKey, attempts: runPhases.attempts, state: runPhases.state }) .from(runPhases) .where(and(eq(runPhases.id, phaseId), eq(runPhases.runId, runId))) .limit(1); if (phase === undefined) { return; } if (phase.state === "failed") { return; } await tx .update(runPhases) .set({ state: "failed", endedAt: new Date() }) .where(eq(runPhases.id, phaseId)); await eventRepository.appendInTransaction(tx, { runId, phaseId, type: "phase.failed", payload: { phaseKey: phase.phaseKey, attempt: phase.attempts, reason: `approval_${action}` }, idempotencyKey: `phase.failed:${phaseId}:${phase.attempts}`, }); } async function existingDecisionForToken( tx: TransactionDb, approvalRequestId: string, clientToken: string, ): Promise<{ action: string } | undefined> { const decisions = await tx .select({ action: approvalDecisions.action, idempotencyKey: approvalDecisions.idempotencyKey, }) .from(approvalDecisions) .where(eq(approvalDecisions.approvalRequestId, approvalRequestId)); return decisions.find((decision) => decision.idempotencyKey.endsWith(`:${clientToken}`)); } function approvalStateForAction(action: ApprovalDecisionActionValue) { switch (action) { case "approve": return "approved"; case "reject": return "rejected"; case "request_changes": return "changes_requested"; case "abort": return "aborted"; } } function toEnginePhaseDefinition(phase: Template["phases"][number]): EnginePhaseDefinition { const definition: EnginePhaseDefinition = { key: phase.key, title: phase.title, roles: [...phase.roles], gates: [...phase.gates], }; if (phase.expectedArtifact !== undefined) { definition.expectedArtifact = { path: phase.expectedArtifact.path, schema: phase.expectedArtifact.schema, }; } if (phase.timeoutMs !== undefined) { definition.timeoutMs = phase.timeoutMs; } return definition; } function parsePhasePlanDefinitions(runId: string, value: unknown): EnginePhaseDefinition[] { if ( value === null || typeof value !== "object" || !Array.isArray((value as { phases?: unknown }).phases) ) { throw new DevflowError("Phase plan artifact is missing phases", { class: "fatal", code: "internal_state_corruption", runId, }); } return (value as { phases: unknown[] }).phases.map((phase, index) => parsePhasePlanDefinition(runId, phase, index), ); } function assertPlannedPhaseKeys( runId: string, plannedPhases: readonly EnginePhaseDefinition[], templatePhaseKeys: readonly string[], ) { const seen = new Set(); const templateKeys = new Set(templatePhaseKeys); for (const phase of plannedPhases) { if (seen.has(phase.key)) { throw new DevflowError("Phase plan contains duplicate phase keys", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: phase.key, }); } seen.add(phase.key); if (templateKeys.has(phase.key)) { throw new DevflowError("Phase plan phase key collides with template phase key", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: phase.key, }); } } } function parsePhasePlanDefinition( runId: string, phase: unknown, index: number, ): EnginePhaseDefinition { if (phase === null || typeof phase !== "object") { throw invalidPhasePlan(runId, index); } const record = phase as Record; if ( typeof record.key !== "string" || typeof record.title !== "string" || !Array.isArray(record.roles) ) { throw invalidPhasePlan(runId, index); } const roles = record.roles.filter((role): role is string => typeof role === "string"); if (roles.length !== record.roles.length || roles.length === 0) { throw invalidPhasePlan(runId, index); } const gates = Array.isArray(record.gates) && record.gates.every((gate) => typeof gate === "string") ? record.gates : []; const definition: EnginePhaseDefinition = { key: record.key, title: record.title, roles, gates, }; if (record.expectedArtifact !== undefined) { if (record.expectedArtifact === null || typeof record.expectedArtifact !== "object") { throw invalidPhasePlan(runId, index); } const expectedArtifact = record.expectedArtifact as Record; if (typeof expectedArtifact.path !== "string" || typeof expectedArtifact.schema !== "string") { throw invalidPhasePlan(runId, index); } definition.expectedArtifact = { path: expectedArtifact.path, schema: expectedArtifact.schema, }; } if (typeof record.timeoutMs === "number" && Number.isInteger(record.timeoutMs)) { definition.timeoutMs = record.timeoutMs; } return definition; } function invalidPhasePlan(runId: string, index: number): DevflowError { return new DevflowError("Phase plan artifact contains an invalid phase", { class: "fatal", code: "internal_state_corruption", runId, recoveryHint: `phase_index=${index}`, }); } function storeEngineMetadata( extra: Record | undefined, scenarios: Record | undefined, ): Record { return { ...(extra ?? {}), devflowM4: { scenarios: scenarios ?? {}, }, }; } function scenarioForPhase(extra: unknown, phaseKey: string): Required { const scenario = readScenario(extra, phaseKey); if (typeof scenario === "string") { return { scenario, repairScenario: "ok" }; } return { scenario: scenario?.scenario ?? "ok", repairScenario: scenario?.repairScenario ?? "ok", }; } interface FakePhaseScenarioObject { scenario?: string; repairScenario?: string; } function readScenario(extra: unknown, phaseKey: string): FakePhaseScenario | undefined { if (extra === null || typeof extra !== "object" || !("devflowM4" in extra)) { return undefined; } const metadata = (extra as { devflowM4?: unknown }).devflowM4; if (metadata === null || typeof metadata !== "object" || !("scenarios" in metadata)) { return undefined; } const scenarios = (metadata as { scenarios?: unknown }).scenarios; if (scenarios === null || typeof scenarios !== "object" || !(phaseKey in scenarios)) { return undefined; } const value = (scenarios as Record)[phaseKey]; if (typeof value === "string") { return value; } if (value !== null && typeof value === "object") { const candidate = value as Record; const scenario = typeof candidate.scenario === "string" ? candidate.scenario : undefined; const repairScenario = typeof candidate.repairScenario === "string" ? candidate.repairScenario : undefined; return { ...(scenario === undefined ? {} : { scenario }), ...(repairScenario === undefined ? {} : { repairScenario }), }; } return undefined; } function buildPhaseInstructions( phaseKey: string, title: string, requirementsMd: string, scenario: Required, ): string { return [ `Scenario: ${scenario.scenario}`, `Repair-Scenario: ${scenario.repairScenario}`, `Phase: ${phaseKey}`, `Title: ${title}`, "Requirements:", requirementsMd, ].join("\n"); } function canonicalExistingPath(path: string): string { return realpathSync(resolve(path)); } function gitChildEnv(): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...process.env }; for (const key of gitLocalEnvKeys) { delete env[key]; } return env; } const gitLocalEnvKeys = [ "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CONFIG", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT", "GIT_OBJECT_DIRECTORY", "GIT_DIR", "GIT_WORK_TREE", "GIT_IMPLICIT_WORK_TREE", "GIT_GRAFT_FILE", "GIT_INDEX_FILE", "GIT_NO_REPLACE_OBJECTS", "GIT_REPLACE_REF_BASE", "GIT_PREFIX", "GIT_SHALLOW_FILE", "GIT_COMMON_DIR", ] as const; async function atomicWriteFile(path: string, content: string): Promise { await mkdir(dirname(path), { recursive: true }); const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`; await writeFile(tempPath, content, "utf8"); await rename(tempPath, path); } function sha256Hex(bytes: Buffer): string { return createHash("sha256").update(bytes).digest("hex"); } function renderMarkdownReport(report: Record): string { const runId = typeof report.runId === "string" ? report.runId : "unknown"; const status = typeof report.status === "string" ? report.status : "unknown"; const endedAt = typeof report.endedAt === "string" ? report.endedAt : "unknown"; return [`# Devflow Run ${runId}`, "", `Status: ${status}`, `Ended: ${endedAt}`, ""].join("\n"); } function serializeJson(value: unknown): unknown { if (typeof value === "bigint") { return value.toString(); } if (value instanceof Date) { return value.toISOString(); } if (Array.isArray(value)) { return value.map((item) => serializeJson(item)); } if (value !== null && typeof value === "object") { return Object.fromEntries( Object.entries(value as Record).map(([key, child]) => [ key, serializeJson(child), ]), ); } return value; } function isTerminalRunState(state: string): state is (typeof terminalRunStates)[number] { return terminalRunStates.some((terminalState) => terminalState === state); } function isPathInsideOrEqual(path: string, parent: string): boolean { const relativePath = relative(parent, path); return relativePath === "" || (!relativePath.startsWith("..") && relativePath !== ".."); } function approvalConflict(runId: string, reason: string): DevflowError { return new DevflowError("Approval decision conflicts with the current request state", { class: "human_required", code: "approval_conflict", runId, recoveryHint: reason, }); } function runNotFound(runId: string): DevflowError { return new DevflowError("Run does not exist", { class: "human_required", code: "run_not_found", runId, }); }