/* Maintains packages/os/site-kernel/src/workflow/handlers.ts as an authored site-kernel authored module so agents can evolve it without rediscovering local boundaries. Do not execute workflow steps. Do not edit workflow files to repair violations. Module registers the command only — workflow logic lives in sibling modules. RFC-0075: Add agent workflow lint/list handlers. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1276: remove runWorkflowAmendList, WORKFLOW_AMEND_DIR, and the amend RFC command exemption set — the RFC-0135/0136 amend pipeline is superseded RFC-1307: step 5 — remove 00-brief.md path, onboarding.synthesize and brief.validate; cold run registers from intake */ import path from "node:path"; import YAML from "yaml"; import { listStaticCommandSurface } from "../runtime.ts"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-shared/kernel"; import { WORKFLOW_PHASES, type WorkflowFrontmatter, type WorkflowLintResult, type WorkflowLintViolation, type WorkflowListEntry, } from "./types.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const WORKFLOW_DIR = ".agents/workflows"; const PENDING_ACCEPTED_RFC_COMMANDS = new Set([ "biome.tokens.derive", "family.contract.validate", "family.list", "archetype.registry.validate", "cosmic.name.pick", "section.scaffold", "section.contract.validate", "section.similarity.report", "system-md.compile", "constellation.contract.validate", "content.coverage.validate", "content.voice.lint", "pbp.content.validate", "content.references.validate", "audit.agent.readiness.validate", "seo.technical.validate", "seo.structured-data.validate", "seo.internal-linking.validate", "analytics.config.validate", "first-party-data.validate", "infra.brief.validate", "audit.llm.run", "app.qa.validate", "kernel.wire", "onboarding.scaffold", "onboarding.input.validate", "onboarding.phase.validate", "biome.contract.validate", "biome.css.generate", ]); async function pathExists( target: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { return io.exists(target); } async function readWorkflowFiles( workspaceRoot: string, dir: string, io: WorkspaceIO = createDefaultIO().io, ) { const directory = path.join(workspaceRoot, dir); const entries = await io.readdir(directory).catch(() => []); return entries .filter((entry) => entry.isFile && entry.name.endsWith(".md") && entry.name !== "README.md") .map((entry) => path.join(directory, entry.name)) .sort(); } function parseMarkdownFrontmatter(source: string): unknown { if (!source.startsWith("---\n")) return undefined; const end = source.indexOf("\n---", 4); if (end === -1) return undefined; return YAML.parse(source.slice(4, end)); } function isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every((item) => typeof item === "string"); } function isPlannedWorkflowArtifact(root: string, writes: string[]): boolean { return root.startsWith("onboarding/.output/") || writes.includes(root); } function validateFrontmatter(file: string, value: unknown): WorkflowFrontmatter { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`${file}: missing or invalid workflow frontmatter`); } const candidate = value as WorkflowFrontmatter; const valid = typeof candidate.id === "string" && typeof candidate.title === "string" && WORKFLOW_PHASES.includes(candidate.phase) && isStringArray(candidate.reads) && isStringArray(candidate.writes) && Boolean(candidate.scope) && isStringArray(candidate.scope.allowedWriteRoots) && isStringArray(candidate.scope.forbiddenWriteRoots) && isStringArray(candidate.runs) && Array.isArray(candidate.recoveryRules) && candidate.recoveryRules.every( (rule) => typeof rule?.on === "string" && typeof rule?.do === "string", ) && isStringArray(candidate.agentInvariants) && Boolean(candidate.selfOrchestration) && typeof candidate.selfOrchestration.autoRun === "boolean" && isStringArray(candidate.selfOrchestration.pauseFor) && isStringArray(candidate.checkpoints) && (candidate.nextWorkflow === undefined || typeof candidate.nextWorkflow === "string"); if (!valid) throw new Error(`${file}: invalid workflow frontmatter shape`); return candidate; } /** Load the greenfield chain's workflow files. */ async function loadWorkflows(workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io) { const dir = WORKFLOW_DIR; const files = await readWorkflowFiles(workspaceRoot, dir); const workflows: Array<{ relativeFile: string; data: WorkflowFrontmatter; }> = []; const violations: WorkflowLintViolation[] = []; for (const file of files) { const relativeFile = path.relative(workspaceRoot, file).replace(/\\/g, "/"); try { const source = await io.readFile(file); workflows.push({ relativeFile, data: validateFrontmatter(relativeFile, parseMarkdownFrontmatter(source)), }); } catch (error) { violations.push({ file: relativeFile, code: "WF002", message: error instanceof Error ? error.message : String(error), }); } } return { workflows, violations }; } function toListEntries( workflows: Array<{ relativeFile: string; data: WorkflowFrontmatter }>, ): WorkflowListEntry[] { return workflows.map(({ relativeFile, data }) => ({ id: data.id, title: data.title, phase: data.phase, file: relativeFile, reads: data.reads, writes: data.writes, runs: data.runs, nextWorkflow: data.nextWorkflow, })); } export async function runWorkflowList( _input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workflows } = await loadWorkflows(context.workspaceRoot); const entries = toListEntries(workflows); return { data: { command: "workflow.list", workflows: entries }, exitCode: 0, summary: `[workflow.list] ${entries.length} workflow file(s) found`, }; } export async function runWorkflowLint( _input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const workflowDirectory = path.join(context.workspaceRoot, WORKFLOW_DIR); const { workflows, violations } = await loadWorkflows(context.workspaceRoot); const commandNames = new Set( (await listStaticCommandSurface(context.workspaceRoot)).commands.map((c) => c.name), ); for (const commandName of [ "sites-check.run", "sites-check.author", "sites-check.postbuild", "packages-check.run", "app.contract.full", "workflow.lint", "workflow.list", ]) { commandNames.add(commandName); } for (const commandName of PENDING_ACCEPTED_RFC_COMMANDS) commandNames.add(commandName); const workflowIds = new Set(workflows.map(({ data }) => data.id)); for (const { relativeFile, data } of workflows) { for (const runEntry of data.runs) { // RFC-0075: runs entries may carry inline flags (e.g. "onboarding.phase.validate --phase=03-compose"). // The kernel registry stores bare command names; strip flag tokens before lookup. const commandName = runEntry.split(/\s+/, 1)[0] ?? runEntry; if (!commandNames.has(commandName)) { violations.push({ file: relativeFile, code: "WF003", message: `Unknown kernel command in runs: ${commandName}`, }); } } if (!data.scope.forbiddenWriteRoots.includes("onboarding/.input/")) { violations.push({ file: relativeFile, code: "WF004", message: "scope.forbiddenWriteRoots must include onboarding/.input/.", }); } if (data.nextWorkflow && !workflowIds.has(data.nextWorkflow)) { violations.push({ file: relativeFile, code: "WF005", message: `nextWorkflow references missing workflow id: ${data.nextWorkflow}`, }); } for (const root of [...data.reads, ...data.writes, ...data.scope.allowedWriteRoots]) { if ( !root.includes("<") && !root.includes("*") && !root.endsWith("/") && !isPlannedWorkflowArtifact(root, data.writes) && !(await pathExists(path.join(context.workspaceRoot, root))) ) { violations.push({ file: relativeFile, code: "WF007", message: `Path does not exist and is not a pattern: ${root}`, }); } } } return { data: { command: "workflow.lint", workflowDirectory, filesChecked: workflows.length, violations, }, exitCode: violations.length > 0 ? 1 : 0, summary: violations.length > 0 ? `[workflow.lint] ${violations.length} violation${violations.length === 1 ? "" : "s"}` : `[workflow.lint] ${workflows.length} workflow file(s) valid`, nextSteps: violations.length > 0 ? [ { action: "Fix the workflow lint violations listed above, then re-run workflow.lint", kind: "required", }, ] : undefined, }; }