/* RFC-0555: workpiece.read — read a file from a mission workpiece with DNA-22 path validation. The command loads the mission manifest, resolves the workpiece root, validates the requested path against the client-editable surface, and returns the file content. Does not auto-commit — LLMs must call mission.git.commit separately. Does not handle system.md partial field edits — future enhancement. RFC-0555: initial workpiece.read command handler. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt RFC-1254: drop unused io-migration leftovers */ import path from "node:path"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { readMissionManifest, resolveMissionDir } from "../mission/mission-io.ts"; import { isClientEditable } from "./dna-22-checker.ts"; export interface WorkpieceReadResult { path: string; content: string; } function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } export async function runWorkpieceRead( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const missionId = flagString(input, "mission"); const relativePath = flagString(input, "path"); if (!missionId) { throw new Error("[workpiece.read] --mission flag is required"); } if (!relativePath) { throw new Error("[workpiece.read] --path flag is required"); } const { workspaceRoot, logger } = context; const manifestPath = path.join( await resolveMissionDir(workspaceRoot, missionId, context.io), "mission.yaml", ); if (!(await context.io.exists(manifestPath))) { throw new Error(`[workpiece.read] Mission '${missionId}' is not open or does not exist`); } const manifest = await readMissionManifest(workspaceRoot, missionId); if (manifest.state !== "open") { throw new Error( `[workpiece.read] Mission '${missionId}' is not open (state: ${manifest.state})`, ); } const workpieceDir = path.join( await resolveMissionDir(workspaceRoot, missionId, context.io), "workpiece", ); if (!(await context.io.exists(workpieceDir))) { throw new Error( `[workpiece.read] Workpiece for mission '${missionId}' not found. Run mission.materialize first.`, ); } const normalizedPath = relativePath.replace(/\\/g, "/").replace(/^\.\//, ""); const resolvedPath = path.resolve(workpieceDir, normalizedPath); if (!resolvedPath.startsWith(workpieceDir)) { throw new Error("[workpiece.read] Path traversal detected"); } const allowed = await isClientEditable(workpieceDir, normalizedPath); if (!allowed) { throw new Error(`Path '${normalizedPath}' is outside client-editable surface (DNA-22)`); } if (!(await context.io.exists(resolvedPath))) { throw new Error(`[workpiece.read] File not found: ${normalizedPath}`); } const content = await context.io.readFile(resolvedPath); logger.info(`[workpiece.read] ${missionId}: read ${normalizedPath}`); return { data: { path: normalizedPath, content }, exitCode: 0, summary: "[workpiece.read] Done", }; }