/*
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,
};
}