import { Type, type Static } from "typebox"; import { StringEnum } from "./schema.ts"; import { assertValid, validateSchema } from "./validation.ts"; import type { IdeaSessionInventoryRecord, IdeaSessionInventorySnapshot } from "./idea-session-inventory.ts"; import type { HydrationSourceResult } from "./operations-hydration.ts"; export const WorkflowOperationsReasonCodeSchema = StringEnum(["NO_SESSIONS","NO_MATCHES","ACTION_REQUIRED","READY_FOR_REVIEW","REVIEWED_CLEANUP_PENDING","UNKNOWN_SOURCE","CORRUPT_INVENTORY","CORRUPT_LEDGER","REFRESH_PARTIAL","REFRESH_TIMED_OUT","CURSOR_MISMATCH","CONFIG_ERROR","INTERNAL_ERROR","RETENTION_REPORT","CLEAN_SUCCESS"]); export type WorkflowOperationsReasonCode = Static; export const OperationsDataStateSchema = StringEnum(["NO_IDEA_SESSIONS","NO_MATCHES","COMPLETE","PARTIAL","REFRESH_TIMED_OUT","CORRUPT_SOURCE","ERROR"]); export type OperationsDataState = Static; export const OperationsActionItemSchema = Type.Object({ sessionId: Type.String({ minLength: 1 }), workflowRunId: Type.Optional(Type.String({ minLength: 1 })), what: Type.String({ minLength: 1 }), why: Type.String({ minLength: 1 }), next: Type.String({ minLength: 1 }), reasonCode: WorkflowOperationsReasonCodeSchema, priority: Type.Integer({ minimum: 0 }) }); export const OperationsReadyItemSchema = Type.Object({ sessionId: Type.String({ minLength: 1 }), workflowRunId: Type.Optional(Type.String({ minLength: 1 })), label: Type.String({ minLength: 1 }), next: Type.String({ minLength: 1 }), stateUpdatedAt: Type.String({ minLength: 1 }), priority: Type.Integer({ minimum: 0 }) }); export const OperationsNextActionSchema = Type.Object({ command: Type.String({ minLength: 1 }), reasonCode: WorkflowOperationsReasonCodeSchema }); export const OperationsSummarySchema = Type.Object({ active: Type.Integer({ minimum: 0 }), ready: Type.Integer({ minimum: 0 }), cleanup: Type.Integer({ minimum: 0 }), blocked: Type.Integer({ minimum: 0 }), unknown: Type.Integer({ minimum: 0 }), total: Type.Integer({ minimum: 0 }), noActionRequired: Type.Boolean() }); export const OperationsSourceSchema = Type.Object({ sourceId: Type.String({ minLength: 1 }), status: StringEnum(["local","fresh","stale","failed","unknown"]), repairCommand: Type.Optional(Type.String({ minLength: 1 })) }); export const OperationsViewV1Schema = Type.Object({ schemaVersion: Type.Literal(1), command: Type.String({ minLength: 1 }), generatedAt: Type.String({ minLength: 1 }), dataState: OperationsDataStateSchema, exitCode: Type.Union([Type.Literal(0),Type.Literal(1),Type.Literal(2),Type.Literal(64),Type.Literal(70)]), summary: OperationsSummarySchema, actionRequired: Type.Array(OperationsActionItemSchema), readyItem: Type.Optional(OperationsReadyItemSchema), nextAction: OperationsNextActionSchema, sources: Type.Array(OperationsSourceSchema), truncated: Type.Boolean(), nextCursor: Type.Optional(Type.String({ minLength: 1 })), retentionBanner: Type.Optional(Type.String({ minLength: 1 })), unknownItems: Type.Optional(Type.Array(OperationsActionItemSchema)) }); export type OperationsViewV1 = Static; export class WorkflowOperationsError extends Error { readonly code: WorkflowOperationsReasonCode; readonly source?: string; readonly cause?: unknown; constructor(code: WorkflowOperationsReasonCode, message: string, source?: string, cause?: unknown) { super(message); this.name = "WorkflowOperationsError"; this.code = code; this.source = source; this.cause = cause; } } export interface BuildOperationsViewInput { command: string; inventory: IdeaSessionInventorySnapshot; hydration?: HydrationSourceResult[]; workflowRunId?: string; retentionDryRun?: boolean; pageSize?: number; cursor?: OperationsCursorV1; now?: string } export interface OperationsCursorV1 { schemaVersion: 1; inventoryGeneration: number; queryHash: string; lastSortKey: string } export function encodeOperationsCursor(cursor: OperationsCursorV1): string { return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url"); } export function decodeOperationsCursor(encoded: string): OperationsCursorV1 { const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as OperationsCursorV1; if (parsed.schemaVersion !== 1) throw new WorkflowOperationsError("CURSOR_MISMATCH", "Unsupported cursor schema version"); return parsed; } const actionRequired = (record: IdeaSessionInventoryRecord) => record.corrupt || ["blocked","reviewed_cleanup_pending","starting_failed","terminal_failed","final_package"].includes(record.lifecycleStatus); const actionItem = (record: IdeaSessionInventoryRecord) => record.corrupt ? { sessionId: record.sessionId, workflowRunId: record.workflowRunId, what: "Session inventory source is corrupt", why: record.corruptReason ?? "Ledger or manifest failed validation", next: `/skill:idea-status --workflow-run-id ${record.workflowRunId ?? record.sessionId} --verbose`, reasonCode: "CORRUPT_LEDGER" as const, priority: 0 } : record.lifecycleStatus === "reviewed_cleanup_pending" ? { sessionId: record.sessionId, workflowRunId: record.workflowRunId, what: "Final review committed but stage cleanup is pending", why: "External stage closure did not complete", next: `/skill:idea-status --workflow-run-id ${record.workflowRunId ?? record.sessionId} --verbose`, reasonCode: "REVIEWED_CLEANUP_PENDING" as const, priority: 1 } : record.lifecycleStatus === "final_package" ? { sessionId: record.sessionId, workflowRunId: record.workflowRunId, what: "Workflow reached final package and needs human review", why: "Sandbox lane requires explicit final review", next: `/skill:idea-to-build --resume-run-id ${record.workflowRunId ?? record.sessionId}`, reasonCode: "READY_FOR_REVIEW" as const, priority: 3 } : { sessionId: record.sessionId, workflowRunId: record.workflowRunId, what: `Session is ${record.lifecycleStatus}`, why: "Recovery requires inspecting authoritative ledger state", next: `/skill:idea-status --workflow-run-id ${record.workflowRunId ?? record.sessionId} --verbose`, reasonCode: "ACTION_REQUIRED" as const, priority: 2 }; const readyItem = (record: IdeaSessionInventoryRecord) => record.lifecycleStatus === "final_package" ? { sessionId: record.sessionId, workflowRunId: record.workflowRunId, label: record.parentIdentifier ?? record.sessionId, next: `/skill:idea-to-build --resume-run-id ${record.workflowRunId ?? record.sessionId}`, stateUpdatedAt: record.stateUpdatedAt, priority: 3 } : undefined; const queryHash = (input: BuildOperationsViewInput) => `${input.command}:${input.workflowRunId ?? ""}:${input.retentionDryRun ? "retention" : "status"}`; export function buildOperationsViewV1(input: BuildOperationsViewInput): OperationsViewV1 { const now = input.now ?? new Date().toISOString(); const pageSize = input.pageSize ?? 20; let records = [...input.inventory.records]; if (input.workflowRunId) records = records.filter((record) => record.workflowRunId === input.workflowRunId || record.sessionId === input.workflowRunId); const sources = (input.hydration ?? []).map((item) => ({ sourceId: item.sourceId, status: item.status, repairCommand: item.repairCommand })); const unknownItems = sources.filter((source) => source.status === "unknown" || source.status === "failed").map((source) => ({ sessionId: source.sourceId, what: "Remote hydration source is incomplete", why: "Local facts preserved; remote-dependent fields marked UNKNOWN", next: source.repairCommand ?? `/skill:idea-status --workflow-run-id ${source.sourceId} --refresh`, reasonCode: "UNKNOWN_SOURCE" as const, priority: 0 })); if (!records.length && !input.inventory.records.length) return assertValid(validateSchema(OperationsViewV1Schema, { schemaVersion: 1, command: input.command, generatedAt: now, dataState: "NO_IDEA_SESSIONS", exitCode: 0, summary: { active: 0, ready: 0, cleanup: 0, blocked: 0, unknown: unknownItems.length, total: 0, noActionRequired: true }, actionRequired: unknownItems, nextAction: { command: "/skill:idea-to-build", reasonCode: "NO_SESSIONS" }, sources, truncated: false }), "Invalid operations view"); if (!records.length) return assertValid(validateSchema(OperationsViewV1Schema, { schemaVersion: 1, command: input.command, generatedAt: now, dataState: "NO_MATCHES", exitCode: 0, summary: { active: 0, ready: 0, cleanup: 0, blocked: 0, unknown: unknownItems.length, total: input.inventory.records.length, noActionRequired: unknownItems.length === 0 }, actionRequired: unknownItems, nextAction: { command: "/skill:idea-status", reasonCode: "NO_MATCHES" }, sources, truncated: false }), "Invalid operations view"); const actions = [...unknownItems.map((item) => ({ ...item, workflowRunId: item.sessionId })), ...records.filter(actionRequired).map(actionItem)].sort((a,b) => a.priority - b.priority || ((a.workflowRunId ?? a.sessionId)).localeCompare((b.workflowRunId ?? b.sessionId))); const ready = records.map(readyItem).filter(Boolean).sort((a,b) => a!.priority - b!.priority || a!.stateUpdatedAt.localeCompare(b!.stateUpdatedAt))[0]; const summary = { active: records.filter((r) => ["active","starting"].includes(r.lifecycleStatus)).length, ready: records.filter((r) => r.lifecycleStatus === "final_package").length, cleanup: records.filter((r) => r.lifecycleStatus === "reviewed_cleanup_pending").length, blocked: records.filter((r) => ["blocked","terminal_failed","starting_failed"].includes(r.lifecycleStatus)).length, unknown: unknownItems.length, total: input.inventory.records.length, noActionRequired: actions.length === 0 }; let dataState: OperationsDataState = "COMPLETE"; let exitCode: 0|1|2 = actions.length ? 1 : 0; if (records.some((r) => r.corrupt)) { dataState = "CORRUPT_SOURCE"; exitCode = 2; } else if (unknownItems.length || input.hydration?.some((item) => item.status === "unknown")) { dataState = unknownItems.length ? "PARTIAL" : "REFRESH_TIMED_OUT"; exitCode = 2; } const nextAction = actions[0] ? { command: actions[0].next, reasonCode: actions[0].reasonCode } : ready ? { command: ready.next, reasonCode: "READY_FOR_REVIEW" as const } : input.retentionDryRun ? { command: "/skill:idea-status --retention-dry-run", reasonCode: "RETENTION_REPORT" as const } : { command: "/skill:idea-to-build", reasonCode: "CLEAN_SUCCESS" as const }; const startIndex = input.cursor ? records.findIndex((record) => `${record.stateUpdatedAt}:${record.sessionId}` === input.cursor!.lastSortKey) + 1 : 0; const pageRecords = records.slice(startIndex, startIndex + pageSize); const last = pageRecords.at(-1); const nextCursor = startIndex + pageSize < records.length && last ? encodeOperationsCursor({ schemaVersion: 1, inventoryGeneration: input.inventory.generation, queryHash: queryHash(input), lastSortKey: `${last.stateUpdatedAt}:${last.sessionId}` }) : undefined; return assertValid(validateSchema(OperationsViewV1Schema, { schemaVersion: 1, command: input.command, generatedAt: now, dataState, exitCode, summary, actionRequired: actions.slice(0, pageSize), readyItem: ready, nextAction, sources, truncated: Boolean(nextCursor), nextCursor, retentionBanner: input.retentionDryRun ? "RETENTION DRY-RUN — NO FILES WERE DELETED" : undefined, unknownItems: unknownItems.length ? unknownItems : undefined }), "Invalid operations view"); } export function mapWorkflowOperationsError(error: WorkflowOperationsError): OperationsViewV1 { const exitCode = error.code === "CONFIG_ERROR" ? 64 : error.code === "INTERNAL_ERROR" ? 70 : 2; return assertValid(validateSchema(OperationsViewV1Schema, { schemaVersion: 1, command: "idea-status", generatedAt: new Date().toISOString(), dataState: "ERROR", exitCode, summary: { active: 0, ready: 0, cleanup: 0, blocked: 0, unknown: 0, total: 0, noActionRequired: false }, actionRequired: [{ sessionId: "system", what: "Operations command failed", why: error.message, next: error.code === "CONFIG_ERROR" ? "/skill:idea-status --help" : "/skill:idea-status --rebuild", reasonCode: error.code, priority: 0 }], nextAction: { command: error.code === "CONFIG_ERROR" ? "/skill:idea-status --help" : "/skill:idea-status --rebuild", reasonCode: error.code }, sources: [], truncated: false }), "Invalid error operations view"); } export function wrapUnknownOperationsError(error: unknown): WorkflowOperationsError { return error instanceof WorkflowOperationsError ? error : new WorkflowOperationsError("INTERNAL_ERROR", error instanceof Error ? error.message : String(error)); } export function assertOperationsViewV1(view: OperationsViewV1): OperationsViewV1 { return assertValid(validateSchema(OperationsViewV1Schema, view), "Invalid OperationsViewV1"); }