chore: initialize POM 2 repo (docs guards green; release gates recorded NOT RUN pending install)

This commit is contained in:
Antigravity
2026-08-19 10:31:00 +02:00
commit 7ffd00b5c5
111 changed files with 12522 additions and 0 deletions

300
src/command.ts Normal file
View File

@@ -0,0 +1,300 @@
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import { join } from "node:path";
import { loadPomConfig, updateProjectPomConfig } from "./config";
import { stage, STAGES, type PomMotion, type PomToolProfile, type PomView, type ValidationScope } from "./domain";
import { bootstrapProject, loadState, recordHiveEvent, syncKnowledgeBase, writeHiveMission } from "./persistence";
import { createState, gotoStage, recordEvidence, startStage, summarizeState } from "./state";
import { renderStagePrompt, getPrompt, listPrompts } from "./prompts";
import { validatePom } from "./validators";
import { applyToolProfile } from "./tool-profiles";
import { routeStage } from "./router";
import { ensureGit, enterStageBranch } from "./git";
import { exportProject } from "./exporter";
import { openCommandCenter, updateChrome } from "./ui";
import type { CommandCenterAction } from "./visual";
import type { PomRuntime } from "./runtime";
import { buildHiveMission } from "./studio";
import { writeSwarmDefinition, type PomSwarmMode } from "./swarm";
import { installBundledThemes } from "./themes";
import { parseCommand } from "./parser";
export { parseCommand } from "./parser";
const HELP = `POM · Produce Magnum Opus
/pom Open cinematic command center
/pom new <title> Create a POM project, vault, ledgers, and Git root
/pom run [next|all|0..8] Start authoritative production
/pom hive [lean|stage|audit] Launch native task + IRC specialist flow
/pom swarm [mode] [count] Generate an optional OMP Swarm DAG and queue /swarm run
/pom evidence Record acceptance evidence interactively
/pom check [scope] Run deterministic validation
/pom vault Insert the production-board mention
/pom export [checkpoint|final]
/pom status [--json]
/pom prompt [list|show|run] [id]
/pom tools [show|reset|profile <name>]
/pom theme install Install POM Nocturne/Parchment themes
/pom settings
/pom doctor
/pom help`;
async function recordEvidenceInteractive(ctx: ExtensionContext, runtime: PomRuntime): Promise<void> {
const state = runtime.get();
const spec = stage(state.currentStage);
const criterion = await ctx.ui.select("Record acceptance evidence", spec.acceptance.map((item) => ({ label: `${item.id} · ${item.label}`, value: item.id, description: item.description })), { outline: true });
if (!criterion) return;
const criterionId = criterion.split(" · ")[0];
const summary = await ctx.ui.input("Evidence summary", "What was verified and why is it sufficient?");
if (!summary) return;
const pathsText = await ctx.ui.input("Evidence paths", "Comma-separated project-relative paths");
const paths = (pathsText ?? "").split(",").map((item) => item.trim()).filter(Boolean);
await runtime.set(recordEvidence(state, criterionId, summary, paths, "Main"));
ctx.ui.notify(`Evidence recorded: ${criterionId}`, "info");
}
async function experienceSettings(ctx: ExtensionContext, runtime: PomRuntime): Promise<void> {
const state = structuredClone(runtime.get());
const view = await ctx.ui.select("POM visual density", [
{ label: "cinematic", description: "Three-line HUD, richer cards, IRC ticker, and thinking lane" },
{ label: "rich", description: "Two-line HUD and evidence-focused expanded cards" },
{ label: "compact", description: "One-line HUD and terse cards" },
{ label: "minimal", description: "Status line plus critical milestones and failures" },
], { initialIndex: ["cinematic", "rich", "compact", "minimal"].indexOf(state.view), outline: true });
if (view) state.view = view as PomView;
const motion = await ctx.ui.select("POM motion", [
{ label: "off", description: "Static semantic symbols only" },
{ label: "subtle", description: "Two-frame active-work pulse" },
{ label: "full", description: "Four-frame tool activity and live coordination cues" },
], { initialIndex: ["off", "subtle", "full"].indexOf(state.motion), outline: true });
if (motion) state.motion = motion as PomMotion;
const currentConfig = runtime.getConfig();
const lane = await ctx.ui.select("Thinking-lane annotation", [
{ label: "on", description: "Show stage and execution lane below visible thinking" },
{ label: "off", description: "Leave visible thinking unannotated" },
], { initialIndex: currentConfig.showThinkingLane ? 0 : 1, outline: true });
const placement = await ctx.ui.select("HUD placement", [
{ label: "aboveEditor", description: "Production truth directly above the prompt editor" },
{ label: "belowEditor", description: "Production truth beneath the prompt editor" },
], { initialIndex: currentConfig.hudPlacement === "aboveEditor" ? 0 : 1, outline: true });
await runtime.set(state);
if (state.initialized) {
await updateProjectPomConfig(state.projectRoot, {
showThinkingLane: lane ? lane === "on" : currentConfig.showThinkingLane,
hudPlacement: (placement ?? currentConfig.hudPlacement) as "aboveEditor" | "belowEditor",
});
await runtime.refreshConfig(state.projectRoot);
}
updateChrome(ctx, runtime.get(), runtime.getConfig(), runtime);
ctx.ui.notify(`POM experience · ${state.view} · motion ${state.motion} · lane ${runtime.getConfig().showThinkingLane ? "on" : "off"}`, "info");
}
async function runStage(pi: ExtensionAPI, ctx: ExtensionContext, runtime: PomRuntime, targetRaw: string): Promise<void> {
let state = runtime.get();
const allStages = targetRaw === "all";
const target = allStages || targetRaw === "next" ? state.currentStage : Number(targetRaw);
if (!Number.isInteger(target) || target < 0 || target > 8) throw new Error("Run target must be next, all, or 0..8");
state = startStage(gotoStage(state, target), target, allStages);
await runtime.set(state);
const config = await loadPomConfig(state.projectRoot);
await enterStageBranch(state, config);
await routeStage(pi, ctx, state);
updateChrome(ctx, state, config, runtime);
pi.sendUserMessage(await renderStagePrompt(state, allStages), { deliverAs: "followUp" });
}
async function dispatchAction(pi: ExtensionAPI, ctx: ExtensionContext, runtime: PomRuntime, action: CommandCenterAction): Promise<void> {
if (action === "run") return runStage(pi, ctx, runtime, "next");
if (action === "hive") { pi.sendUserMessage("Call pom_hive with op=plan and preset=stage. Then call native task exactly once with the returned batch. After all agents finish, integrate and call pom_hive op=synthesize.", { deliverAs: "followUp" }); return; }
if (action === "validate") {
const report = await validatePom(runtime.get(), runtime.get().currentStage === 8 ? "delivery" : "stage");
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${report.scope}`, display: true, details: report }, { triggerTurn: false });
return;
}
if (action === "evidence") return recordEvidenceInteractive(ctx, runtime);
if (action === "vault") { ctx.ui.setEditorText("@content/production-board.md "); return; }
if (action === "export") {
const result = await exportProject(runtime.get(), runtime.get().currentStage === 8 ? "final" : "checkpoint");
ctx.ui.notify(`Archive verified: ${result.zipPath}`, "info");
return;
}
if (action === "theme") {
const installed = await installBundledThemes();
ctx.ui.setEditorText("/theme pom-nocturne");
ctx.ui.notify(`Installed ${installed.map((item) => item.name).join(", ")}. Press Enter to activate POM Nocturne.`, "info");
return;
}
if (action === "settings") return experienceSettings(ctx, runtime);
}
export function registerPomCommand(pi: ExtensionAPI, runtime: PomRuntime): void {
const completions = ["new", "run", "hive", "swarm", "evidence", "check", "vault", "export", "status", "prompt", "tools", "theme", "settings", "doctor", "help"];
pi.registerCommand("pom", {
description: "POM production command center",
getArgumentCompletions: (prefix) => completions.filter((item) => item.startsWith(prefix)).map((item) => ({ value: item, label: item })),
handler: async (raw, ctx: ExtensionCommandContext) => {
const command = parseCommand(raw);
if (command.verb === "dashboard") {
const action = await openCommandCenter(ctx, runtime.get());
await dispatchAction(pi, ctx, runtime, action);
return;
}
if (command.verb === "help") { ctx.ui.notify(HELP, "info"); return; }
if (command.verb === "new") {
const title = command.args.join(" ") || await ctx.ui.input("Project title", "Untitled Magnum Opus") || "Untitled Magnum Opus";
const config = await loadPomConfig(ctx.cwd);
const state = createState(title, ctx.cwd, config, true);
await bootstrapProject(state);
await ensureGit(state, config);
await runtime.set(state);
await pi.setSessionName(`POM · ${state.projectTitle}`);
updateChrome(ctx, state, config, runtime);
ctx.ui.notify(`POM project created at ${state.projectRoot}`, "info");
await runStage(pi, ctx, runtime, "0");
return;
}
if (command.verb === "resume") {
const path = command.args[0] ?? join(ctx.cwd, "00_admin", "project_state.json");
await runtime.set(await loadState(path));
const config = await loadPomConfig(runtime.get().projectRoot);
updateChrome(ctx, runtime.get(), config, runtime);
ctx.ui.notify(summarizeState(runtime.get()), "info");
return;
}
if (command.verb === "run" || command.verb === "next") {
await runStage(pi, ctx, runtime, command.verb === "next" ? "next" : command.args[0] ?? "next");
return;
}
if (command.verb === "hive") {
const preset = command.args[0] ?? "stage";
if (!["lean", "stage", "audit"].includes(preset)) throw new Error("Hive preset must be lean, stage, or audit");
pi.sendUserMessage(`Call pom_hive with {"op":"plan","preset":"${preset}"}. Invoke native task exactly once with its returned batch. Use native IRC for live coordination and pom_hive signal for durable milestones. Integrate, then synthesize the mission.`, { deliverAs: "followUp" });
return;
}
if (command.verb === "swarm") {
const mode = (command.args[0] ?? "parallel") as PomSwarmMode;
if (!["parallel", "sequential", "pipeline"].includes(mode)) throw new Error("Swarm mode must be parallel, sequential, or pipeline");
const count = Number(command.args[1] ?? "1");
const config = await loadPomConfig(runtime.get().projectRoot);
const state = structuredClone(runtime.get());
const mission = buildHiveMission(state, config, "stage");
state.hive.activeMissionId = mission.id;
state.hive.missions[mission.id] = mission;
state.stageRuns[String(state.currentStage)].hiveMissionIds.push(mission.id);
await runtime.set(state);
const missionPath = await writeHiveMission(state, mission);
const swarmPath = await writeSwarmDefinition(state, mission, mode, count);
await recordHiveEvent(state, { type: "swarm.definition.created", missionId: mission.id, missionPath, swarmPath, mode, count });
const swarmAvailable = pi.getCommands().some((item) => item.name === "swarm");
const invocation = `/swarm run ${join(state.projectRoot, swarmPath)}`;
if (swarmAvailable && ctx.hasUI) {
ctx.ui.setEditorText(invocation);
ctx.ui.notify(`OMP Swarm DAG prepared. Press Enter to run:
${invocation}`, "info");
} else {
ctx.ui.notify(`Swarm YAML created at ${swarmPath}. Install/enable @oh-my-pi/swarm-extension, then run:
${invocation}`, "warning");
}
return;
}
if (command.verb === "evidence") { await recordEvidenceInteractive(ctx, runtime); return; }
if (command.verb === "check") {
const scope = (command.args[0] ?? (runtime.get().currentStage === 8 ? "delivery" : "stage")) as ValidationScope;
if (!["quick", "stage", "canon", "continuity", "knowledge", "files", "delivery", "all"].includes(scope)) throw new Error(`Invalid validation scope: ${scope}`);
const previous = runtime.get().toolProfile;
try {
await applyToolProfile(pi, "audit");
const report = await validatePom(runtime.get(), scope);
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${scope}`, display: true, details: report }, { triggerTurn: false });
} finally {
await applyToolProfile(pi, previous);
}
return;
}
if (command.verb === "vault") { ctx.ui.setEditorText("@content/production-board.md "); return; }
if (command.verb === "status") { ctx.ui.notify(command.flags.has("json") ? JSON.stringify(runtime.get(), null, 2) : summarizeState(runtime.get()), "info"); return; }
if (command.verb === "export") {
const kind = command.args[0] ?? (runtime.get().currentStage === 8 ? "final" : "checkpoint");
if (kind !== "final" && kind !== "checkpoint") throw new Error("Export kind must be checkpoint or final");
const result = await exportProject(runtime.get(), kind);
ctx.ui.notify(`PASS · ${result.zipPath}\n${result.verification.sha256}`, "info");
return;
}
if (command.verb === "prompt") {
const action = command.args[0] ?? "list";
if (action === "list") {
const prompts = await listPrompts(runtime.get().initialized ? runtime.get() : undefined);
ctx.ui.notify(prompts.map((item) => `${item.id} · ${item.source} · ${item.sha256.slice(0, 12)}`).join("\n"), "info");
return;
}
const id = command.args[1];
if (!id) throw new Error("Prompt id required");
const prompt = await getPrompt(runtime.get(), id);
if (action === "show") await ctx.ui.editor(`POM prompt · ${id}`, prompt.content);
else if (action === "run") pi.sendUserMessage(prompt.content, { deliverAs: "followUp" });
else throw new Error("Prompt action must be list, show, or run");
return;
}
if (command.verb === "tools") {
const action = command.args[0] ?? "show";
if (action === "show") { ctx.ui.notify(pi.getActiveTools().join(", "), "info"); return; }
const profile = action === "profile" ? command.args[1] : action === "reset" ? "auto" : undefined;
if (!profile || !["auto", "creative", "research", "draft", "audit", "delivery"].includes(profile)) throw new Error("Invalid tool profile");
const selected = await applyToolProfile(pi, profile as PomToolProfile);
const state = structuredClone(runtime.get());
state.toolProfile = profile as typeof state.toolProfile;
await runtime.set(state);
ctx.ui.notify(selected.join(", "), "info");
return;
}
if (command.verb === "theme") {
const action = command.args[0] ?? "install";
if (action !== "install") throw new Error("Theme action must be install");
const installed = await installBundledThemes();
if (ctx.hasUI) ctx.ui.setEditorText("/theme pom-nocturne");
ctx.ui.notify(`Installed ${installed.map((item) => item.name).join(", ")}. Run /theme pom-nocturne or press Enter.`, "info");
return;
}
if (command.verb === "settings") { await experienceSettings(ctx, runtime); return; }
if (command.verb === "doctor") {
const report = await validatePom(runtime.get(), "quick");
const all = new Set(pi.getAllTools());
const required = ["read", "write", "edit", "task", "irc", "todo", "eval", "pom_stage", "pom_hive", "pom_validate"];
const missing = required.filter((tool) => !all.has(tool));
ctx.ui.notify([
`State ${report.passed ? "PASS" : "FAIL"}`,
`Tools ${missing.length ? `missing ${missing.join(", ")}` : "PASS"}`,
`OMP ${pi.pi ? "API loaded" : "API unavailable"}`,
`Project ${runtime.get().projectRoot}`,
].join("\n"), report.passed && !missing.length ? "info" : "error");
return;
}
ctx.ui.notify(`Unknown POM command: ${command.verb}\n\n${HELP}`, "error");
},
});
pi.registerShortcut("alt+p", {
description: "Open POM command center",
handler: async (ctx) => {
const action = await openCommandCenter(ctx, runtime.get());
await dispatchAction(pi, ctx, runtime, action);
},
});
}

69
src/config.ts Normal file
View File

@@ -0,0 +1,69 @@
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { DEFAULT_CONFIG, type PomConfig } from "./domain";
import { pathExists } from "./persistence";
async function readJsonObject(path: string): Promise<Record<string, unknown>> {
if (!(await pathExists(path))) return {};
const text = await readFile(path, "utf8");
let value: unknown;
try {
value = JSON.parse(text);
} catch (error) {
throw new Error(`Invalid POM configuration at ${path}: ${error instanceof Error ? error.message : String(error)}`);
}
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`POM configuration must be an object: ${path}`);
return value as Record<string, unknown>;
}
function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T, key: string): T {
if (value === undefined) return fallback;
if (typeof value !== "string" || !allowed.includes(value as T)) throw new Error(`Invalid POM setting ${key}: ${String(value)}`);
return value as T;
}
function pickBoolean(value: unknown, fallback: boolean, key: string): boolean {
if (value === undefined) return fallback;
if (typeof value !== "boolean") throw new Error(`Invalid POM setting ${key}: expected boolean`);
return value;
}
function pickInteger(value: unknown, fallback: number, key: string, min: number, max: number): number {
if (value === undefined) return fallback;
if (!Number.isInteger(value) || Number(value) < min || Number(value) > max) throw new Error(`Invalid POM setting ${key}: expected ${min}..${max}`);
return Number(value);
}
export async function loadPomConfig(cwd: string): Promise<PomConfig> {
const globalConfig = await readJsonObject(join(homedir(), ".omp", "agent", "pom.json"));
const projectConfig = await readJsonObject(join(cwd, ".omp", "pom.json"));
const merged = { ...globalConfig, ...projectConfig };
return {
autoToolProfiles: pickBoolean(merged.autoToolProfiles, DEFAULT_CONFIG.autoToolProfiles, "autoToolProfiles"),
autoModelRouting: pickBoolean(merged.autoModelRouting, DEFAULT_CONFIG.autoModelRouting, "autoModelRouting"),
dashboardWidget: pickBoolean(merged.dashboardWidget, DEFAULT_CONFIG.dashboardWidget, "dashboardWidget"),
stopGuard: pickBoolean(merged.stopGuard, DEFAULT_CONFIG.stopGuard, "stopGuard"),
defaultMode: pickEnum(merged.defaultMode, ["autopilot", "collaborative", "strict-autopilot"], DEFAULT_CONFIG.defaultMode, "defaultMode"),
defaultView: pickEnum(merged.defaultView, ["cinematic", "rich", "compact", "minimal"], DEFAULT_CONFIG.defaultView, "defaultView"),
defaultMotion: pickEnum(merged.defaultMotion, ["off", "subtle", "full"], DEFAULT_CONFIG.defaultMotion, "defaultMotion"),
defaultToolProfile: pickEnum(merged.defaultToolProfile, ["auto", "creative", "research", "draft", "audit", "delivery"], DEFAULT_CONFIG.defaultToolProfile, "defaultToolProfile"),
maxStudioAgents: pickInteger(merged.maxStudioAgents, DEFAULT_CONFIG.maxStudioAgents, "maxStudioAgents", 1, 12),
projectRootMode: pickEnum(merged.projectRootMode, ["child", "cwd"], DEFAULT_CONFIG.projectRootMode, "projectRootMode"),
destructiveProjectCommands: pickEnum(merged.destructiveProjectCommands, ["block", "allow"], DEFAULT_CONFIG.destructiveProjectCommands, "destructiveProjectCommands"),
gitMode: pickEnum(merged.gitMode, ["off", "checkpoint", "branch-per-stage"], DEFAULT_CONFIG.gitMode, "gitMode"),
hudPlacement: pickEnum(merged.hudPlacement, ["aboveEditor", "belowEditor"], DEFAULT_CONFIG.hudPlacement, "hudPlacement"),
showThinkingLane: pickBoolean(merged.showThinkingLane, DEFAULT_CONFIG.showThinkingLane, "showThinkingLane"),
};
}
export async function updateProjectPomConfig(projectRoot: string, patch: Partial<PomConfig>): Promise<PomConfig> {
const path = join(projectRoot, ".omp", "pom.json");
const current = await readJsonObject(path);
const next = { ...current, ...patch };
await mkdir(dirname(path), { recursive: true });
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
await writeFile(temp, `${JSON.stringify(next, null, 2)}\n`, "utf8");
await rename(temp, path);
return loadPomConfig(projectRoot);
}

501
src/domain.ts Normal file
View File

@@ -0,0 +1,501 @@
export const POM_VERSION = "2.0.0";
export const POM_SCHEMA_VERSION = "2.0.0" as const;
export type PomMode = "autopilot" | "collaborative" | "strict-autopilot";
export type PomView = "cinematic" | "rich" | "compact" | "minimal";
export type PomMotion = "off" | "subtle" | "full";
export type PomToolProfile = "auto" | "creative" | "research" | "draft" | "audit" | "delivery";
export type PomStageStatus = "queued" | "active" | "blocked" | "complete";
export type ValidationScope = "quick" | "stage" | "canon" | "continuity" | "knowledge" | "files" | "delivery" | "all";
export interface TodoPhase {
phase: string;
items: string[];
}
export interface ArtifactRequirement {
path: string;
kind?: "file" | "directory";
description: string;
minBytes?: number;
minEntries?: number;
requiredHeadings?: string[];
requiredPatterns?: string[];
forbiddenPatterns?: string[];
}
export interface AcceptanceCriterion {
id: string;
label: string;
description: string;
}
export interface StageSpec {
id: number;
slug: string;
name: string;
icon: string;
toolProfile: PomToolProfile;
modelRole: string;
agents: string[];
artifacts: ArtifactRequirement[];
acceptance: AcceptanceCriterion[];
todos: TodoPhase[];
}
const phase = (phaseName: string, ...items: string[]): TodoPhase => ({ phase: phaseName, items });
const criterion = (id: string, label: string, description: string): AcceptanceCriterion => ({ id, label, description });
export const STAGES: readonly StageSpec[] = [
{
id: 0,
slug: "init",
name: "Project Initialization",
icon: "🏗",
toolProfile: "creative",
modelRole: "pi/slow",
agents: ["pom-architect"],
artifacts: [
{
path: "01_planning/00_project_brief.md",
description: "Authoritative project brief",
minBytes: 900,
requiredHeadings: ["Vision", "Audience", "Constraints", "Success Criteria", "Open Questions"],
forbiddenPatterns: ["TBD", "TODO", "lorem ipsum"],
},
],
acceptance: [
criterion("identity", "Identity resolved", "Title, format, audience, intended scope, and canonical root are explicit."),
criterion("north-star", "North star defined", "The project has a concise creative promise and measurable success criteria."),
criterion("ambiguity", "Foundational ambiguity cleared", "Blocking unknowns are resolved or represented as explicit blockers."),
],
todos: [
phase("Foundation", "Resolve project identity", "Define creative north star", "Record constraints and assumptions"),
phase("Artifacts", "Create project brief", "Initialize vault and ledgers", "Validate stage 0"),
],
},
{
id: 1,
slug: "genre",
name: "Genre Architecture",
icon: "🎭",
toolProfile: "research",
modelRole: "pi/slow",
agents: ["pom-researcher", "pom-concept-forge", "pom-critic"],
artifacts: [
{
path: "01_planning/01_genre_grid.md",
description: "Genre landscape and promise map",
minBytes: 1400,
requiredHeadings: ["Candidate Genres", "Audience Promise", "Cliche Risks", "Differentiation"],
},
{
path: "01_planning/02_genre_fusion_matrix.md",
description: "Ranked fusion matrix",
minBytes: 1000,
requiredHeadings: ["Scoring Model", "Matrix", "Recommendation"],
},
],
acceptance: [
criterion("genre-selected", "Primary genre selected", "One primary genre and optional secondary genres are explicitly locked."),
criterion("promise", "Genre promise recorded", "Reader expectations, pleasures, and boundaries are stated."),
criterion("cliche-risk", "Cliche risks documented", "High-risk conventions and specific differentiation strategies are recorded."),
],
todos: [
phase("Research", "Map genre candidates", "Research audience expectations", "Analyze cliches and differentiation"),
phase("Selection", "Build fusion matrix", "Rank directions", "Lock genre promise"),
phase("Gate", "Save artifacts", "Record acceptance evidence", "Validate stage 1"),
],
},
{
id: 2,
slug: "voice",
name: "Author Persona and Voice",
icon: "✒",
toolProfile: "creative",
modelRole: "pi/slow",
agents: ["pom-voice-architect", "pom-rights-auditor"],
artifacts: [
{
path: "02_story_bible/01_author_persona.md",
description: "Original author persona",
minBytes: 1200,
requiredHeadings: ["Creative Identity", "Audience Relationship", "Aesthetic Philosophy", "Boundaries"],
},
{
path: "02_story_bible/02_voice_guide.md",
description: "Testable voice system",
minBytes: 1600,
requiredHeadings: ["Voice Profile", "Always", "Avoid", "Syntax", "Diction", "Sample"],
},
],
acceptance: [
criterion("persona", "Original persona stable", "The persona is internally coherent and not a disguised living-author imitation."),
criterion("voice-laws", "Voice laws testable", "Voice rules can be checked against prose with concrete positive and negative examples."),
criterion("rights", "Imitation risk cleared", "Protected-author imitation and misleading attribution risks are explicitly audited."),
],
todos: [
phase("Persona", "Design original persona", "Define audience relationship", "Audit rights boundaries"),
phase("Voice", "Define voice profile", "Write always/avoid laws", "Create original sample"),
phase("Gate", "Save artifacts", "Record acceptance evidence", "Validate stage 2"),
],
},
{
id: 3,
slug: "store",
name: "Story Store",
icon: "📚",
toolProfile: "creative",
modelRole: "pi/slow",
agents: ["pom-concept-forge", "pom-critic"],
artifacts: [
{
path: "01_planning/04_story_store.md",
description: "Twenty structurally diverse concepts",
minBytes: 3800,
requiredHeadings: ["Concepts", "Scoring", "Top Five"],
requiredPatterns: ["Concept 20"],
},
{
path: "01_planning/05_selected_direction.md",
description: "Selected and strengthened premise",
minBytes: 1400,
requiredHeadings: ["Premise", "Protagonist", "Conflict", "Stakes", "Distinctive Engine", "Why This Wins"],
},
],
acceptance: [
criterion("twenty", "Twenty concepts produced", "Exactly twenty meaningfully different concepts exist and are individually inspectable."),
criterion("winner", "Winner selected", "A transparent scoring and comparison process selects the strongest direction."),
criterion("premise", "Premise stable", "The selected premise contains character, conflict, stakes, engine, and differentiation."),
],
todos: [
phase("Generate", "Generate twenty diverse concepts", "Score concept potential", "Reject cosmetic duplicates"),
phase("Select", "Rank strongest five", "Compare top three", "Strengthen winner"),
phase("Gate", "Save story store", "Save selected direction", "Validate stage 3"),
],
},
{
id: 4,
slug: "bible",
name: "Story Blueprint and Bible",
icon: "🌐",
toolProfile: "creative",
modelRole: "pi/slow",
agents: ["pom-story-architect", "pom-character-world", "pom-continuity-auditor", "pom-visual-director"],
artifacts: [
{
path: "02_story_bible/00_story_bible_index.md",
description: "Story-bible map of content",
minBytes: 800,
requiredHeadings: ["Canon", "Characters", "World", "Structure", "Visual Language"],
},
{
path: "02_story_bible/13_canon_ledger.md",
description: "Atomic canon ledger",
minBytes: 1600,
requiredHeadings: ["Characters", "World Rules", "Timeline", "Objects", "Unresolved"],
},
{
path: "02_story_bible/16_visual_bible.md",
description: "Visual system and reference policy",
minBytes: 1400,
requiredHeadings: ["Visual Thesis", "Palette", "Composition", "Motifs", "Rights"],
},
],
acceptance: [
criterion("ending", "Ending known", "The ending and its causal prerequisites are explicit."),
criterion("arcs", "Major arcs defined", "Primary character, relationship, mystery, and thematic arcs have start, turns, and resolution."),
criterion("world", "World rules stable", "Rules, limits, costs, and exceptions are documented without unresolved contradiction."),
criterion("outline-ready", "Canon supports outline", "The bible contains enough stable information to build a causal outline."),
],
todos: [
phase("Identity", "Finalize story identity", "Write ending synopsis", "Define dramatic questions"),
phase("Bible", "Build character bible", "Build world rules", "Build themes and visual language"),
phase("Canon", "Create timeline and ledgers", "Map mysteries and payoffs", "Resolve contradictions"),
phase("Gate", "Record evidence", "Run continuity audit", "Validate stage 4"),
],
},
{
id: 5,
slug: "outline",
name: "Master Story Outline",
icon: "🧩",
toolProfile: "creative",
modelRole: "pi/slow",
agents: ["pom-structure-engineer", "pom-continuity-auditor", "pom-critic"],
artifacts: [
{
path: "03_manuscript/00_master_outline.md",
description: "Causal master outline",
minBytes: 3200,
requiredHeadings: ["Act I", "Act II", "Act III", "Ending", "Causality Audit"],
},
{
path: "03_manuscript/02_scene_index.md",
description: "Scene-level causal index",
minBytes: 2600,
requiredHeadings: ["Scene Index", "Setups", "Payoffs", "State Changes"],
},
],
acceptance: [
criterion("ending-represented", "Ending represented", "The final outcome and its setup chain are represented in the outline."),
criterion("causality", "Causality survives review", "Major beats arise from prior choices, pressures, and consequences rather than convenience."),
criterion("setup-payoff", "Setups mapped", "Every major payoff has a setup, escalation, and resolution path."),
],
todos: [
phase("Structure", "Design acts and sequences", "Map turning points", "Allocate narrative weight"),
phase("Scenes", "Build causal scene index", "Track state changes", "Link setups and payoffs"),
phase("Audit", "Audit agency and escalation", "Repair weak causality", "Validate stage 5"),
],
},
{
id: 6,
slug: "chapters",
name: "Chapter Architecture",
icon: "📑",
toolProfile: "creative",
modelRole: "pi/slow",
agents: ["pom-structure-engineer", "pom-continuity-auditor", "pom-visual-director"],
artifacts: [
{
path: "03_manuscript/01_chapter_outline.md",
description: "Complete chapter architecture",
minBytes: 3000,
requiredHeadings: ["Chapter Map", "POV Plan", "Pacing", "Continuity Audit"],
},
{
path: "03_manuscript/02_scene_index.md",
description: "Updated scene index",
minBytes: 3200,
requiredHeadings: ["Scene Index", "Chapter Allocation", "Reveals", "Payoffs"],
},
],
acceptance: [
criterion("mapped", "All chapters mapped", "Every intended chapter has objective, conflict, change, and exit state."),
criterion("pov", "POV logic stable", "POV assignment and transitions have explicit dramatic purpose."),
criterion("gaps", "No narrative gaps", "Timeline, reveal order, and causal transitions survive review."),
],
todos: [
phase("Architecture", "Map every chapter", "Define state changes", "Balance POV and pacing"),
phase("Continuity", "Verify transitions", "Verify reveals and payoffs", "Repair gaps and redundancies"),
phase("Gate", "Update scene index", "Record evidence", "Validate stage 6"),
],
},
{
id: 7,
slug: "draft",
name: "Drafting and Revision",
icon: "📜",
toolProfile: "draft",
modelRole: "pi/default",
agents: ["pom-prose-smith", "pom-developmental-editor", "pom-continuity-auditor"],
artifacts: [
{
path: "03_manuscript/chapters",
kind: "directory",
description: "Individual drafted chapters",
minEntries: 1,
},
{
path: "03_manuscript/03_complete_manuscript.md",
description: "Compiled complete manuscript",
minBytes: 8000,
requiredHeadings: ["Chapter"],
},
],
acceptance: [
criterion("complete", "All planned chapters complete", "The chapter map and chapter files reconcile without missing planned content."),
criterion("counts", "Word counts verified", "Measured word counts are recorded and reconcile with the compiled manuscript."),
criterion("continuity-resolved", "Continuity resolved", "Known continuity defects are resolved or explicitly disclosed as blockers."),
],
todos: [
phase("Preflight", "Load chapter dependencies", "Verify canon", "Set chapter objective"),
phase("Draft", "Draft chapter", "Preserve voice and POV", "End in changed state"),
phase("Postflight", "Measure words", "Run voice and repetition checks", "Update ledgers"),
phase("Manuscript", "Compile manuscript", "Run developmental review", "Validate stage 7"),
],
},
{
id: 8,
slug: "delivery",
name: "Final Compilation and Delivery",
icon: "📦",
toolProfile: "delivery",
modelRole: "pi/slow",
agents: ["pom-delivery-auditor", "pom-rights-auditor", "pom-continuity-auditor"],
artifacts: [
{
path: "00_admin/artifact_manifest.json",
description: "Curated delivery manifest",
minBytes: 64,
},
{
path: "00_admin/SHA256SUMS.txt",
description: "Checksums for packaged artifacts",
minBytes: 64,
},
{
path: "07_exports/final_delivery_report.md",
description: "Truthful delivery report",
minBytes: 900,
requiredHeadings: ["Contents", "Validation", "Warnings", "Omissions", "Archive"],
},
],
acceptance: [
criterion("requested", "Requested files exist", "All promised deliverables exist as non-empty regular files."),
criterion("hashes", "Manifest and hashes match", "Manifest metadata and SHA-256 values match canonical files."),
criterion("archive", "Fresh archive verified", "A newly created archive is parsed and every member is decompressed and checked."),
criterion("disclosure", "Omissions disclosed", "Warnings, limitations, rights status, and omissions are explicit."),
],
todos: [
phase("Compile", "Compile manuscript", "Assemble requested exports", "Assemble approved visuals"),
phase("Validate", "Run structure checks", "Run continuity and rights checks", "Verify manifest and hashes"),
phase("Package", "Create fresh archive", "Verify every member", "Write delivery report"),
phase("Deliver", "Expose real paths", "Disclose warnings and omissions", "Validate stage 8"),
],
},
] as const;
export interface ArtifactRecord {
path: string;
category: string;
stage: number;
version: string;
status: "draft" | "reviewed" | "approved" | "superseded";
bytes: number;
sha256: string;
updatedAt: string;
rightsStatus: "original" | "licensed" | "public-domain" | "reference-only" | "unknown";
source?: string;
notes?: string;
}
export interface EvidenceRecord {
id: string;
criterionId: string;
summary: string;
paths: string[];
agent?: string;
createdAt: string;
}
export interface StageBlocker {
id: string;
reason: string;
owner?: string;
createdAt: string;
resolvedAt?: string;
resolution?: string;
}
export interface StageRunState {
status: PomStageStatus;
attempts: number;
startedAt?: string;
completedAt?: string;
blockers: StageBlocker[];
evidence: EvidenceRecord[];
validationReportPath?: string;
hiveMissionIds: string[];
}
export interface HiveTask {
name: string;
agent: string;
objective: string;
owns: string[];
acceptance: string[];
isolated?: boolean;
}
export interface HiveMission {
id: string;
stage: number;
title: string;
status: "planned" | "running" | "synthesizing" | "complete" | "blocked";
createdAt: string;
updatedAt: string;
tasks: HiveTask[];
synthesis?: string;
taskToolCallId?: string;
}
export interface PomState {
schemaVersion: typeof POM_SCHEMA_VERSION;
pomVersion: string;
projectId: string;
projectTitle: string;
projectSlug: string;
projectRoot: string;
initialized: boolean;
currentStage: number;
stageRuns: Record<string, StageRunState>;
mode: PomMode;
view: PomView;
motion: PomMotion;
toolProfile: PomToolProfile;
autoToolProfiles: boolean;
autoModelRouting: boolean;
completedStages: number[];
decisions: string[];
assumptions: string[];
warnings: string[];
activeRun?: { id: string; stage: number; startedAt: string; continuationCount: number; allStages: boolean };
hive: {
activeMissionId?: string;
missions: Record<string, HiveMission>;
signalsLogged: number;
};
artifactVersion: string;
canonVersion: string;
updatedAt: string;
}
export interface PomConfig {
autoToolProfiles: boolean;
autoModelRouting: boolean;
dashboardWidget: boolean;
stopGuard: boolean;
defaultMode: PomMode;
defaultView: PomView;
defaultMotion: PomMotion;
defaultToolProfile: PomToolProfile;
maxStudioAgents: number;
projectRootMode: "child" | "cwd";
destructiveProjectCommands: "block" | "allow";
gitMode: "off" | "checkpoint" | "branch-per-stage";
hudPlacement: "aboveEditor" | "belowEditor";
showThinkingLane: boolean;
}
export const DEFAULT_CONFIG: PomConfig = {
autoToolProfiles: true,
autoModelRouting: true,
dashboardWidget: true,
stopGuard: true,
defaultMode: "autopilot",
defaultView: "cinematic",
defaultMotion: "subtle",
defaultToolProfile: "auto",
maxStudioAgents: 6,
projectRootMode: "child",
destructiveProjectCommands: "block",
gitMode: "branch-per-stage",
hudPlacement: "aboveEditor",
showThinkingLane: true,
};
export function stage(id: number): StageSpec {
const value = STAGES[id];
if (!value) throw new Error(`Invalid POM stage: ${id}`);
return value;
}
export function freshStageRuns(): Record<string, StageRunState> {
return Object.fromEntries(
STAGES.map((item) => [
String(item.id),
{ status: "queued", attempts: 0, blockers: [], evidence: [], hiveMissionIds: [] } satisfies StageRunState,
]),
);
}

208
src/events.ts Normal file
View File

@@ -0,0 +1,208 @@
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import { fileURLToPath } from "node:url";
import { stage } from "./domain";
import { addBlocker } from "./state";
import { composePromptStack } from "./prompts";
import { applyToolProfile } from "./tool-profiles";
import { routeStage } from "./router";
import { clearChrome, updateChrome } from "./ui";
import { recordHiveEvent, syncKnowledgeBase } from "./persistence";
import type { PomRuntime } from "./runtime";
import { shouldBlockProjectCommand } from "./policy";
async function chrome(ctx: ExtensionContext, runtime: PomRuntime): Promise<void> {
const state = runtime.get();
const config = await runtime.refreshConfig(state.initialized ? state.projectRoot : ctx.cwd);
updateChrome(ctx, state, config, runtime);
}
export function registerPomEvents(pi: ExtensionAPI, runtime: PomRuntime): void {
pi.on("resources_discover", async () => ({
skillPaths: [fileURLToPath(new URL("../skills", import.meta.url))],
promptPaths: [fileURLToPath(new URL("../prompts", import.meta.url))],
themePaths: [fileURLToPath(new URL("../themes", import.meta.url))],
}));
pi.on("session_start", async (_event, ctx) => {
const state = await runtime.restore(ctx);
await chrome(ctx, runtime);
if (state.initialized) {
if (state.activeRun) await routeStage(pi, ctx, state);
else if (state.autoToolProfiles && state.toolProfile !== "auto") await applyToolProfile(pi, state.toolProfile);
const currentName = pi.getSessionName();
if (!currentName) await pi.setSessionName(`POM · ${state.projectTitle}`);
}
});
const restoreChrome = async (_event: unknown, ctx: ExtensionContext): Promise<void> => {
await runtime.restore(ctx);
await chrome(ctx, runtime);
};
pi.on("session_branch", restoreChrome);
pi.on("session_tree", restoreChrome);
pi.on("session_switch", restoreChrome);
pi.on("before_agent_start", async (event) => {
const state = runtime.get();
if (!state.initialized || !state.activeRun) return;
const stack = await composePromptStack(state, ["stage-run"], {
STAGE_CONTRACT: JSON.stringify(stage(state.currentStage), null, 2),
});
return {
systemPrompt: [...event.systemPrompt, stack.rendered],
message: {
customType: "pom:event",
content: `Stage ${state.currentStage} production context injected`,
display: false,
details: { title: `POM stage ${state.currentStage} active`, level: "info", lines: [`root ${state.projectRoot}`, `prompt ${stack.sha256.slice(0, 12)}`] },
},
};
});
pi.on("tool_execution_start", async (event, ctx) => {
runtime.telemetry.activeTool = event.toolName;
runtime.telemetry.activeIntent = event.intent;
runtime.telemetry.toolStartedAt = Date.now();
ctx.ui.setWorkingMessage(`POM · ${event.toolName}${event.intent ? ` · ${event.intent}` : ""}`);
await chrome(ctx, runtime);
});
pi.on("tool_execution_update", async (event, ctx) => {
if (event.toolName === "task") ctx.ui.setWorkingMessage("POM hive · specialists working…");
else if (event.toolName === "pom_validate") ctx.ui.setWorkingMessage("POM gate · verifying evidence…");
});
pi.on("tool_execution_end", async (event, ctx) => {
runtime.telemetry.lastToolResult = event.isError ? "fail" : "pass";
runtime.telemetry.activeTool = undefined;
runtime.telemetry.activeIntent = undefined;
runtime.telemetry.toolStartedAt = undefined;
ctx.ui.setWorkingMessage();
await chrome(ctx, runtime);
});
pi.on("tool_call", async (event, ctx) => {
const state = runtime.get();
if (!state.initialized) return;
const config = await runtime.refreshConfig(state.projectRoot);
if (event.toolName === "bash" && config.destructiveProjectCommands === "block") {
const command = String(event.input.command ?? "");
if (shouldBlockProjectCommand(command, state.projectRoot, state.projectSlug, String(event.input.cwd ?? ctx.cwd))) {
ctx.ui.notify("POM blocked a destructive command against the canonical production root.", "error");
return { block: true, reason: "POM canonical-root policy blocks destructive recursive deletion or history destruction. Use versioned, targeted repairs." };
}
}
if (event.toolName === "task") {
const next = structuredClone(state);
const missionId = next.hive.activeMissionId;
if (missionId && next.hive.missions[missionId]) {
next.hive.missions[missionId].status = "running";
next.hive.missions[missionId].taskToolCallId = event.toolCallId;
next.hive.missions[missionId].updatedAt = new Date().toISOString();
runtime.telemetry.activeAgents = next.hive.missions[missionId].tasks.length;
runtime.telemetry.completedAgents = 0;
await runtime.set(next);
await recordHiveEvent(next, { type: "native.task.started", missionId, toolCallId: event.toolCallId, input: event.input });
}
}
if (event.toolName === "irc") {
const input = event.input;
if (input.op === "send") {
runtime.telemetry.lastIrcSignal = `${String(input.to ?? "peer")}: ${String(input.message ?? "")}`;
await recordHiveEvent(state, { type: "native.irc.send", toolCallId: event.toolCallId, to: input.to, message: input.message, replyTo: input.replyTo });
}
}
});
pi.on("tool_result", async (event) => {
const state = runtime.get();
if (!state.initialized) return;
if (event.toolName === "task") {
const next = structuredClone(state);
const missionId = next.hive.activeMissionId;
if (missionId && next.hive.missions[missionId]) {
next.hive.missions[missionId].status = event.isError ? "blocked" : "synthesizing";
next.hive.missions[missionId].updatedAt = new Date().toISOString();
runtime.telemetry.completedAgents = event.isError ? 0 : next.hive.missions[missionId].tasks.length;
await runtime.set(next);
await recordHiveEvent(next, { type: "native.task.finished", missionId, toolCallId: event.toolCallId, isError: event.isError, details: event.details });
}
}
if (event.toolName === "irc") {
await recordHiveEvent(state, { type: "native.irc.result", toolCallId: event.toolCallId, isError: event.isError, input: event.input, details: event.details });
}
if (event.isError && event.toolName.startsWith("pom_")) {
const next = structuredClone(state);
const warning = `${event.toolName} failed at ${new Date().toISOString()}`;
if (!next.warnings.includes(warning)) next.warnings.push(warning);
await runtime.set(next);
}
});
pi.on("session.compacting", async () => {
const state = runtime.get();
if (!state.initialized) return;
const spec = stage(state.currentStage);
const run = state.stageRuns[String(state.currentStage)];
return {
context: [
`POM project ${state.projectTitle} at ${state.projectRoot}`,
`Stage ${spec.id}/8 ${spec.name}; status=${run.status}; evidence=${run.evidence.length}/${spec.acceptance.length}`,
`Canonical state path: ${state.projectRoot}/00_admin/project_state.json`,
`Active hive: ${state.hive.activeMissionId ?? "none"}`,
],
preserveData: { pomState: state },
};
});
pi.on("auto_compaction_start", async (_event, ctx) => ctx.ui.setWorkingMessage("POM · preserving canon and production state…"));
pi.on("auto_compaction_end", async (_event, ctx) => ctx.ui.setWorkingMessage());
pi.on("auto_retry_start", async (event, ctx) => ctx.ui.setStatus("pom-retry", `↻ retry ${event.attempt}`));
pi.on("auto_retry_end", async (_event, ctx) => ctx.ui.setStatus("pom-retry", undefined));
pi.on("tool_approval_requested", async (event, ctx) => ctx.ui.setStatus("pom-approval", `? ${event.toolName}`));
pi.on("tool_approval_resolved", async (_event, ctx) => ctx.ui.setStatus("pom-approval", undefined));
pi.on("goal_updated", async (event, ctx) => ctx.ui.setStatus("pom-goal", event.goal ? "◎ goal active" : undefined));
pi.on("todo_reminder", async (_event, ctx) => ctx.ui.notify("POM production still has open native todos. Complete or explicitly drop them before the gate.", "warning"));
pi.on("ttsr_triggered", async (event) => {
const state = runtime.get();
if (!state.initialized) return;
const next = structuredClone(state);
const warning = `TTSR triggered: ${event.rules.map((rule) => rule.name).join(", ")}`;
if (!next.warnings.includes(warning)) next.warnings.push(warning);
await runtime.set(next);
});
pi.on("credential_disabled", async (event, ctx) => {
const state = runtime.get();
if (!state.initialized) return;
const next = structuredClone(state);
const warning = `Credential disabled: ${String(event.provider)}`;
if (!next.warnings.includes(warning)) next.warnings.push(warning);
await runtime.set(next);
ctx.ui.notify(warning, "warning");
});
pi.on("session_stop", async (_event, ctx) => {
const state = runtime.get();
if (!state.initialized || !state.activeRun) return;
const config = await runtime.refreshConfig(state.projectRoot);
if (!config.stopGuard) return;
if (state.activeRun.continuationCount < 2) {
const next = structuredClone(state);
if (next.activeRun) next.activeRun.continuationCount += 1;
await runtime.set(next);
return {
continue: true,
additionalContext: "POM stage is still active. Finish the native todo, synthesize the hive, register artifacts, record every acceptance criterion, run pom_validate, then call pom_stage pass or block with exact evidence. Do not stop ambiguously.",
};
}
const blocked = addBlocker(state, `Stage ${state.currentStage} stopped without PASS or an explicit blocker after two guarded continuations`, "Main");
await runtime.set(blocked);
await syncKnowledgeBase(blocked);
pi.sendMessage({ customType: "pom:milestone", content: "Stage blocked by stop guard", display: true, details: { stage: state.currentStage, status: "BLOCKED" } }, { triggerTurn: false });
await chrome(ctx, runtime);
});
pi.on("session_shutdown", async (_event, ctx) => clearChrome(ctx));
}

106
src/exporter.ts Normal file
View File

@@ -0,0 +1,106 @@
import { readdir } from "node:fs/promises";
import { basename, join } from "node:path";
import { atomicWrite, loadManifest, pathExists, sha256File } from "./persistence";
import { validatePom } from "./validators";
import { createZip, verifyZip, type ZipVerification } from "./zip";
import type { PomState } from "./domain";
export interface ExportResult {
zipPath: string;
verificationPath: string;
reportPath: string;
verification: ZipVerification;
}
async function collectKnowledgeFiles(state: PomState): Promise<string[]> {
const root = join(state.projectRoot, "content");
if (!(await pathExists(root))) return [];
return (await readdir(root)).filter((name) => name.endsWith(".md")).map((name) => `content/${name}`);
}
export async function exportProject(state: PomState, kind: "checkpoint" | "final"): Promise<ExportResult> {
const manifest = await loadManifest(state);
if (!manifest.length) throw new Error("Cannot export an empty artifact manifest");
const preflightScopes = kind === "final" ? (["files", "knowledge", "continuity"] as const) : (["files"] as const);
const preflightReports = [];
for (const scope of preflightScopes) preflightReports.push(await validatePom(state, scope, { writeReport: false }));
const preflightFailures = preflightReports.flatMap((report) => report.checks.filter((item) => item.status === "fail"));
if (kind === "final") {
const incomplete = Array.from({ length: 8 }, (_value, index) => index).filter((id) => state.stageRuns[String(id)]?.status !== "complete");
if (incomplete.length) throw new Error(`Final export requires stages 07 complete; incomplete: ${incomplete.join(", ")}`);
const blockers = Object.values(state.stageRuns).flatMap((run) => run.blockers).filter((blocker) => !blocker.resolvedAt);
if (blockers.length) throw new Error(`Final export blocked by ${blockers.length} unresolved blocker(s)`);
}
if (preflightFailures.length) throw new Error(`Export preflight failed with ${preflightFailures.length} error(s)`);
const preflight = {
scope: preflightScopes.join("+"),
passed: preflightFailures.length === 0,
checks: preflightReports.flatMap((report) => report.checks),
};
const exportRoot = join(state.projectRoot, "07_exports");
const base = `${state.projectSlug}-${kind}`;
const reportRelative = kind === "final" ? "07_exports/final_delivery_report.md" : `07_exports/${base}-delivery-report.md`;
const reportPath = join(state.projectRoot, reportRelative);
const checksumsRelative = "00_admin/SHA256SUMS.txt";
const checksumsPath = join(state.projectRoot, checksumsRelative);
const memberPaths = [...new Set([
...manifest.map((record) => record.path),
"00_admin/artifact_manifest.json",
"00_admin/project_state.json",
...await collectKnowledgeFiles(state),
])].sort();
const checksumLines: string[] = [];
for (const path of memberPaths) checksumLines.push(`${await sha256File(join(state.projectRoot, path))} ${path}`);
await atomicWrite(checksumsPath, `${checksumLines.join("\n")}\n`);
const report = [
"---",
`title: ${JSON.stringify(`${state.projectTitle} Delivery Report`)}`,
"type: delivery-report",
"tags: [pom, delivery]",
"publish: true",
"---",
"",
`# ${state.projectTitle} Delivery Report`,
"",
"## Contents",
"",
...memberPaths.map((path) => `- \`${path}\``),
"",
"## Validation",
"",
`Preflight scope: **${preflight.scope}**`,
`Preflight result: **${preflight.passed ? "PASS" : "FAIL"}**`,
`Checks: ${preflight.checks.length}`,
"",
"## Warnings",
"",
...(state.warnings.length ? state.warnings.map((warning) => `- ${warning}`) : ["- None recorded."]),
"",
"## Omissions",
"",
"- Archive SHA-256 and member-by-member decompression results are written to the adjacent verification sidecar after archive creation. They cannot be embedded in the archive without changing the archive hash.",
"",
"## Archive",
"",
`Planned archive: \`${base}.zip\``,
"Verification requirement: central-directory parse, safe names, supported compression, decompression, size, CRC-32, member count, and archive SHA-256.",
].join("\n");
await atomicWrite(reportPath, `${report}\n`);
const packagePaths = [...memberPaths, checksumsRelative, reportRelative];
const zipPath = join(exportRoot, `${base}.zip`);
await createZip(zipPath, packagePaths.map((path) => ({ absolutePath: join(state.projectRoot, path), archivePath: path })));
const verification = await verifyZip(zipPath);
const verificationPath = join(exportRoot, `${base}.verification.json`);
await atomicWrite(verificationPath, `${JSON.stringify({
schemaVersion: "1.0.0",
generatedAt: new Date().toISOString(),
archive: basename(zipPath),
...verification,
}, null, 2)}\n`);
return { zipPath, verificationPath, reportPath, verification };
}

43
src/git.ts Normal file
View File

@@ -0,0 +1,43 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { join } from "node:path";
import type { PomConfig, PomState } from "./domain";
import { stage } from "./domain";
import { pathExists } from "./persistence";
const execFileAsync = promisify(execFile);
async function git(root: string, args: string[]): Promise<string> {
const result = await execFileAsync("git", ["-C", root, ...args], { maxBuffer: 4 * 1024 * 1024 });
return result.stdout.trim();
}
export async function ensureGit(state: PomState, config: PomConfig): Promise<void> {
if (config.gitMode === "off") return;
if (!(await pathExists(join(state.projectRoot, ".git")))) {
await git(state.projectRoot, ["init"]);
await git(state.projectRoot, ["config", "user.name", "POM Production Kernel"]);
await git(state.projectRoot, ["config", "user.email", "pom@local.invalid"]);
}
}
export async function enterStageBranch(state: PomState, config: PomConfig): Promise<string | undefined> {
if (config.gitMode !== "branch-per-stage") return undefined;
await ensureGit(state, config);
const spec = stage(state.currentStage);
const branch = `pom/stage-${spec.id}-${spec.slug}`;
const branches = await git(state.projectRoot, ["branch", "--format=%(refname:short)"]);
if (branches.split("\n").includes(branch)) await git(state.projectRoot, ["switch", branch]);
else await git(state.projectRoot, ["switch", "-c", branch]);
return branch;
}
export async function checkpointStage(state: PomState, config: PomConfig, label: string): Promise<string | undefined> {
if (config.gitMode === "off") return undefined;
await ensureGit(state, config);
await git(state.projectRoot, ["add", "-A"]);
const status = await git(state.projectRoot, ["status", "--porcelain"]);
if (!status) return undefined;
await git(state.projectRoot, ["commit", "-m", label]);
return git(state.projectRoot, ["rev-parse", "--short", "HEAD"]);
}

50
src/index.ts Normal file
View File

@@ -0,0 +1,50 @@
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import { join } from "node:path";
import { createState, restoreLatest } from "./state";
import { loadPomConfig } from "./config";
import { DEFAULT_CONFIG, type PomConfig, type PomState } from "./domain";
import { loadState, pathExists, saveState, syncKnowledgeBase } from "./persistence";
import { registerPomCommand } from "./command";
import { registerPomEvents } from "./events";
import { registerPomTools } from "./tools";
import { registerPomRenderers } from "./renderers";
import type { PomRuntime } from "./runtime";
export default function pomExtension(pi: ExtensionAPI): void {
pi.setLabel("POM · Produce Magnum Opus");
let config: PomConfig = DEFAULT_CONFIG;
let state: PomState = createState("Untitled Magnum Opus", process.cwd(), config);
const runtime: PomRuntime = {
telemetry: { activeAgents: 0, completedAgents: 0 },
get: () => state,
getConfig: () => config,
refreshConfig: async (cwd) => {
config = await loadPomConfig(cwd);
return config;
},
set: async (next) => {
state = { ...structuredClone(next), updatedAt: new Date().toISOString() };
if (state.initialized) {
pi.appendEntry("pom-state", state);
await saveState(state);
await syncKnowledgeBase(state);
}
},
restore: async (ctx: ExtensionContext) => {
const sessionState = restoreLatest(ctx.sessionManager.getBranch());
if (sessionState) state = sessionState;
else {
const direct = join(ctx.cwd, "00_admin", "project_state.json");
if (await pathExists(direct)) state = await loadState(direct);
else state = createState("Untitled Magnum Opus", ctx.cwd, await loadPomConfig(ctx.cwd));
}
config = await loadPomConfig(state.initialized ? state.projectRoot : ctx.cwd);
return state;
},
};
registerPomTools(pi, runtime);
registerPomRenderers(pi, runtime);
registerPomEvents(pi, runtime);
registerPomCommand(pi, runtime);
}

47
src/parser.ts Normal file
View File

@@ -0,0 +1,47 @@
export interface ParsedCommand {
verb: string;
args: string[];
flags: Map<string, string | true>;
}
export function parseCommand(input: string): ParsedCommand {
const tokens: string[] = [];
let current = "";
let quote: "'" | '"' | undefined;
let escaping = false;
for (const character of input.trim()) {
if (escaping) { current += character; escaping = false; continue; }
if (character === "\\") { escaping = true; continue; }
if (quote) {
if (character === quote) quote = undefined;
else current += character;
continue;
}
if (character === "'" || character === '"') { quote = character; continue; }
if (/\s/.test(character)) {
if (current) { tokens.push(current); current = ""; }
continue;
}
current += character;
}
if (escaping) current += "\\";
if (quote) throw new Error("Unterminated quote in /pom command");
if (current) tokens.push(current);
const verb = tokens.shift() ?? "dashboard";
const args: string[] = [];
const flags = new Map<string, string | true>();
let flagsEnabled = true;
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index];
if (token === "--") { flagsEnabled = false; continue; }
if (flagsEnabled && token.startsWith("--")) {
const [name, inline] = token.slice(2).split("=", 2);
const next = tokens[index + 1];
if (inline !== undefined) flags.set(name, inline);
else if (next && !next.startsWith("--")) { flags.set(name, next); index += 1; }
else flags.set(name, true);
} else args.push(token);
}
return { verb, args, flags };
}

66
src/paths.ts Normal file
View File

@@ -0,0 +1,66 @@
import { lstat, realpath } from "node:fs/promises";
import { dirname, isAbsolute, relative, resolve } from "node:path";
function inside(root: string, target: string): boolean {
const rel = relative(root, target);
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
}
async function nearestExisting(path: string): Promise<string> {
let cursor = path;
for (;;) {
try {
await lstat(cursor);
return cursor;
} catch (error) {
const parent = dirname(cursor);
if (parent === cursor) throw error;
cursor = parent;
}
}
}
export async function canonicalRoot(root: string): Promise<string> {
return realpath(resolve(root));
}
export async function secureExistingPath(root: string, candidate: string, options: { allowDirectory?: boolean } = {}): Promise<string> {
const rootReal = await canonicalRoot(root);
const lexical = resolve(rootReal, candidate);
if (!inside(rootReal, lexical)) throw new Error(`Path escapes project root: ${candidate}`);
const stat = await lstat(lexical);
if (stat.isSymbolicLink()) throw new Error(`Symbolic links are not canonical artifacts: ${candidate}`);
if (!options.allowDirectory && !stat.isFile()) throw new Error(`Expected a regular file: ${candidate}`);
if (options.allowDirectory && !stat.isFile() && !stat.isDirectory()) throw new Error(`Unsupported filesystem object: ${candidate}`);
const targetReal = await realpath(lexical);
if (!inside(rootReal, targetReal)) throw new Error(`Canonical path escapes project root: ${candidate}`);
return targetReal;
}
export async function secureFuturePath(root: string, candidate: string): Promise<string> {
const rootReal = await canonicalRoot(root);
const lexical = resolve(rootReal, candidate);
if (!inside(rootReal, lexical)) throw new Error(`Path escapes project root: ${candidate}`);
const existing = await nearestExisting(dirname(lexical));
const parentReal = await realpath(existing);
if (!inside(rootReal, parentReal)) throw new Error(`Parent path escapes project root: ${candidate}`);
return lexical;
}
export function relativeProjectPath(root: string, absolutePath: string): string {
const rel = relative(resolve(root), resolve(absolutePath)).replaceAll("\\", "/");
if (!rel || rel.startsWith("..") || isAbsolute(rel)) throw new Error(`Path is not a project child: ${absolutePath}`);
return rel;
}
export function assertSafeArchivePath(path: string): void {
const normalized = path.replaceAll("\\", "/");
if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) {
throw new Error(`Unsafe archive member: ${path}`);
}
if (normalized.split("/").some((part) => part === ".." || part === "")) {
throw new Error(`Unsafe archive member: ${path}`);
}
}

388
src/persistence.ts Normal file
View File

@@ -0,0 +1,388 @@
import { createHash, randomUUID } from "node:crypto";
import { createReadStream } from "node:fs";
import { access, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, join, relative, resolve } from "node:path";
import { finished } from "node:stream/promises";
import {
POM_SCHEMA_VERSION,
STAGES,
stage,
type ArtifactRecord,
type HiveMission,
type PomState,
} from "./domain";
import { relativeProjectPath, secureExistingPath, secureFuturePath } from "./paths";
import { assertState, statePath } from "./state";
const writeQueues = new Map<string, Promise<void>>();
async function serialize<T>(key: string, operation: () => Promise<T>): Promise<T> {
const previous = writeQueues.get(key) ?? Promise.resolve();
let release!: () => void;
const current = new Promise<void>((resolvePromise) => { release = resolvePromise; });
const chain = previous.catch(() => undefined).then(() => current);
writeQueues.set(key, chain);
await previous.catch(() => undefined);
try {
return await operation();
} finally {
release();
if (writeQueues.get(key) === chain) writeQueues.delete(key);
}
}
export async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
async function atomicWriteUnlocked(absolute: string, content: string | Uint8Array): Promise<void> {
await mkdir(dirname(absolute), { recursive: true });
const temporary = join(dirname(absolute), `.${basename(absolute)}.${process.pid}.${randomUUID()}.tmp`);
try {
await writeFile(temporary, content);
await rename(temporary, absolute);
} finally {
await rm(temporary, { force: true }).catch(() => undefined);
}
}
export async function atomicWrite(path: string, content: string | Uint8Array): Promise<void> {
const absolute = resolve(path);
await serialize(absolute, () => atomicWriteUnlocked(absolute, content));
}
export async function readJson<T>(path: string): Promise<T> {
const text = await readFile(path, "utf8");
try {
return JSON.parse(text) as T;
} catch (error) {
throw new Error(`Invalid JSON in ${path}: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function sha256File(path: string): Promise<string> {
const hash = createHash("sha256");
const stream = createReadStream(path);
stream.on("data", (chunk) => hash.update(chunk));
await finished(stream);
return hash.digest("hex");
}
export async function saveState(state: PomState): Promise<void> {
assertState(state);
await atomicWrite(statePath(state), `${JSON.stringify(state, null, 2)}\n`);
}
export async function loadState(path: string): Promise<PomState> {
const state = await readJson<unknown>(path);
assertState(state);
return state;
}
export function manifestPath(state: PomState): string {
return join(state.projectRoot, "00_admin", "artifact_manifest.json");
}
export async function loadManifest(state: PomState): Promise<ArtifactRecord[]> {
if (!(await pathExists(manifestPath(state)))) return [];
const value = await readJson<unknown>(manifestPath(state));
if (!Array.isArray(value)) throw new Error("Artifact manifest must be an array");
const paths = new Set<string>();
return value.map((item, index) => {
if (!item || typeof item !== "object") throw new Error(`Manifest record ${index} is invalid`);
const record = item as Partial<ArtifactRecord>;
if (typeof record.path !== "string" || !record.path || record.path.startsWith("/") || record.path.includes("..")) {
throw new Error(`Manifest record ${index} has an unsafe path`);
}
if (paths.has(record.path)) throw new Error(`Duplicate manifest path: ${record.path}`);
paths.add(record.path);
if (typeof record.category !== "string" || !record.category.trim()) throw new Error(`Invalid category for ${record.path}`);
if (!Number.isInteger(record.stage) || Number(record.stage) < 0 || Number(record.stage) > 8) throw new Error(`Invalid stage for ${record.path}`);
if (typeof record.version !== "string" || !/^\d+\.\d+\.\d+$/.test(record.version)) throw new Error(`Invalid version for ${record.path}`);
if (!(["draft", "reviewed", "approved", "superseded"] as const).includes(record.status as ArtifactRecord["status"])) throw new Error(`Invalid status for ${record.path}`);
if (typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) throw new Error(`Invalid SHA-256 for ${record.path}`);
if (typeof record.bytes !== "number" || !Number.isInteger(record.bytes) || record.bytes < 1) throw new Error(`Invalid byte count for ${record.path}`);
if (typeof record.updatedAt !== "string" || Number.isNaN(Date.parse(record.updatedAt))) throw new Error(`Invalid updatedAt for ${record.path}`);
if (!(["original", "licensed", "public-domain", "reference-only", "unknown"] as const).includes(record.rightsStatus as ArtifactRecord["rightsStatus"])) throw new Error(`Invalid rights status for ${record.path}`);
if (record.source !== undefined && typeof record.source !== "string") throw new Error(`Invalid source for ${record.path}`);
if (record.notes !== undefined && typeof record.notes !== "string") throw new Error(`Invalid notes for ${record.path}`);
return record as ArtifactRecord;
});
}
export async function saveManifest(state: PomState, records: ArtifactRecord[]): Promise<void> {
const sorted = [...records].sort((left, right) => left.path.localeCompare(right.path));
await atomicWrite(manifestPath(state), `${JSON.stringify(sorted, null, 2)}\n`);
}
export async function registerArtifact(
state: PomState,
candidate: string,
metadata: {
category: string;
status: ArtifactRecord["status"];
rightsStatus: ArtifactRecord["rightsStatus"];
source?: string;
notes?: string;
stage?: number;
version?: string;
},
): Promise<ArtifactRecord> {
const absolute = await secureExistingPath(state.projectRoot, candidate);
const fileStat = await stat(absolute);
if (fileStat.size < 1) throw new Error("Empty files cannot be canonical artifacts");
const projectPath = relativeProjectPath(state.projectRoot, absolute);
const current = await loadManifest(state);
const existing = current.find((item) => item.path === projectPath);
const record: ArtifactRecord = {
path: projectPath,
category: metadata.category || existing?.category || "other",
stage: metadata.stage ?? existing?.stage ?? state.currentStage,
version: metadata.version ?? existing?.version ?? state.artifactVersion,
status: metadata.status ?? existing?.status ?? "draft",
bytes: fileStat.size,
sha256: await sha256File(absolute),
updatedAt: new Date().toISOString(),
rightsStatus: metadata.rightsStatus ?? existing?.rightsStatus ?? "unknown",
source: metadata.source ?? existing?.source,
notes: metadata.notes ?? existing?.notes,
};
await saveManifest(state, [...current.filter((item) => item.path !== projectPath), record]);
await appendJsonl(join(state.projectRoot, "00_admin", "artifact_events.jsonl"), {
type: "artifact.registered",
projectId: state.projectId,
record,
at: record.updatedAt,
});
await syncKnowledgeBase(state);
return record;
}
export async function appendJsonl(path: string, value: unknown): Promise<void> {
const absolute = resolve(path);
await serialize(absolute, async () => {
await mkdir(dirname(absolute), { recursive: true });
const previous = (await pathExists(absolute)) ? await readFile(absolute, "utf8") : "";
await atomicWriteUnlocked(absolute, `${previous}${JSON.stringify(value)}\n`);
});
}
export async function appendMarkdownLog(root: string, fileName: string, title: string, body: string): Promise<string> {
const path = await secureFuturePath(root, join("00_admin", fileName));
const stamp = new Date().toISOString();
const previous = (await pathExists(path)) ? await readFile(path, "utf8") : `---\ntags: [pom, ledger]\n---\n\n# ${title}\n`;
await atomicWrite(path, `${previous.trimEnd()}\n\n## ${stamp} · ${title}\n\n${body.trim()}\n`);
return path;
}
export async function recordHiveEvent(state: PomState, event: Record<string, unknown>): Promise<void> {
await appendJsonl(join(state.projectRoot, "00_admin", "hive_ledger.jsonl"), {
projectId: state.projectId,
stage: state.currentStage,
at: new Date().toISOString(),
...event,
});
}
export async function recordPromptUse(state: PomState, entry: Record<string, unknown>): Promise<void> {
await appendJsonl(join(state.projectRoot, "00_admin", "prompt_ledger.jsonl"), {
projectId: state.projectId,
stage: state.currentStage,
at: new Date().toISOString(),
...entry,
});
}
function frontmatter(title: string, type: string, tags: string[]): string {
return [
"---",
`title: ${JSON.stringify(title)}`,
`type: ${type}`,
`tags: [${tags.join(", ")}]`,
"publish: true",
"---",
].join("\n");
}
async function ensureStarter(path: string, content: string): Promise<void> {
if (!(await pathExists(path))) await atomicWrite(path, content);
}
export async function bootstrapProject(state: PomState): Promise<void> {
const root = state.projectRoot;
if (await pathExists(root)) {
const entries = await readdir(root);
const marker = join(root, "00_admin", "project_state.json");
if (entries.length > 0 && !(await pathExists(marker))) {
throw new Error(`Refusing to initialize inside a non-empty unrecognized directory: ${root}`);
}
}
const directories = [
"00_admin",
"01_planning",
"02_story_bible",
"03_manuscript/chapters",
"04_images/references",
"04_images/generated",
"05_research",
"06_ledgers",
"07_exports",
".omp/prompts",
"content",
];
for (const directory of directories) await mkdir(join(root, directory), { recursive: true });
await ensureStarter(join(root, ".gitignore"), ["node_modules/", ".DS_Store", "07_exports/*.tmp", "*.swp", ""].join("\n"));
await ensureStarter(join(root, ".omp", "pom.json"), `${JSON.stringify({ dashboardWidget: true, hudPlacement: "aboveEditor", showThinkingLane: true }, null, 2)}\n`);
await ensureStarter(join(root, ".omp", "config.yml"), [
"theme:",
" dark: pom-nocturne",
" light: pom-parchment",
"symbolPreset: unicode",
"statusLine:",
" preset: full",
" separator: powerline-thin",
" sessionAccent: true",
" showHookStatus: true",
"terminal:",
" showImages: true",
"images:",
" autoResize: true",
" blockImages: false",
"tools:",
" intentTracing: true",
" artifactSpillThreshold: 50",
" artifactHeadBytes: 20",
" artifactTailBytes: 20",
"task:",
" eager: preferred",
" batch: true",
" maxConcurrency: 8",
" maxRecursionDepth: 2",
" showResolvedModelBadge: true",
"async:",
" enabled: true",
"compaction:",
" strategy: snapcompact",
" midTurnEnabled: true",
" autoContinue: true",
"memory:",
" backend: local",
"steeringMode: one-at-a-time",
"followUpMode: one-at-a-time",
"interruptMode: immediate",
"defaultThinkingLevel: high",
"hideThinkingBlock: false",
"",
].join("\n"));
await ensureStarter(join(root, "AGENTS.md"), `${frontmatter("POM Agent Contract", "agent-contract", ["pom", "agents"])}\n\n# POM Agent Contract\n\nCanonical files outrank chat. Use native \`task\`, \`irc\`, \`todo\`, and POM evidence tools. Never claim completion without deterministic validation.\n`);
await ensureStarter(join(root, "POM.md"), `${frontmatter(state.projectTitle, "project", ["pom", "project"])}\n\n# ${state.projectTitle}\n\n> [!info] Production root\n> This repository is simultaneously a POM workspace, Obsidian vault, Quartz-compatible knowledge base, and Git project.\n\n- [[content/index|Open production dashboard]]\n- [[01_planning/00_project_brief|Project brief]]\n- [[02_story_bible/00_story_bible_index|Story bible]]\n- [[03_manuscript/00_master_outline|Master outline]]\n`);
await ensureStarter(join(root, "01_planning", "00_project_brief.md"), `${frontmatter("Project Brief", "planning", ["pom", "stage-0"])}\n\n# Project Brief\n\n## Vision\n\n## Audience\n\n## Constraints\n\n## Success Criteria\n\n## Open Questions\n`);
await ensureStarter(join(root, "content", "index.md"), `${frontmatter(state.projectTitle, "dashboard", ["pom", "dashboard"])}\n\n# ${state.projectTitle}\n\n> [!tip] Command center\n> Run \`/pom\` or press the POM shortcut to open the terminal command center.\n\n## Production Map\n\n- [[01-planning|Planning]]\n- [[02-story-bible|Story Bible]]\n- [[03-manuscript|Manuscript]]\n- [[04-visuals|Visuals]]\n- [[05-research|Research]]\n- [[06-ledgers|Ledgers]]\n- [[07-delivery|Delivery]]\n- [[artifact-index|Artifact Index]]\n`);
const maps = [
["01-planning.md", "Planning", "../01_planning"],
["02-story-bible.md", "Story Bible", "../02_story_bible"],
["03-manuscript.md", "Manuscript", "../03_manuscript"],
["04-visuals.md", "Visuals", "../04_images"],
["05-research.md", "Research", "../05_research"],
["06-ledgers.md", "Ledgers", "../06_ledgers"],
["07-delivery.md", "Delivery", "../07_exports"],
] as const;
for (const [file, title, target] of maps) {
await ensureStarter(join(root, "content", file), `${frontmatter(title, "map-of-content", ["pom", "moc"])}\n\n# ${title}\n\nCanonical directory: \`${target}\`\n\n## Index\n\n_Updated automatically by POM._\n`);
}
await ensureStarter(manifestPath(state), "[]\n");
await ensureStarter(join(root, "00_admin", "artifact_events.jsonl"), "");
await ensureStarter(join(root, "00_admin", "hive_ledger.jsonl"), "");
await ensureStarter(join(root, "00_admin", "prompt_ledger.jsonl"), "");
await saveState(state);
await syncKnowledgeBase(state);
}
export async function syncKnowledgeBase(state: PomState): Promise<void> {
const records = await loadManifest(state).catch(() => []);
const lines = [
frontmatter("Artifact Index", "generated-index", ["pom", "artifacts"]),
"",
"# Artifact Index",
"",
"> [!warning] Generated file",
"> Update artifact metadata through POM; this page is regenerated.",
"",
`Updated: ${new Date().toISOString()}`,
"",
];
if (!records.length) lines.push("_No canonical artifacts registered yet._");
const grouped = new Map<string, ArtifactRecord[]>();
for (const record of records) grouped.set(record.category, [...(grouped.get(record.category) ?? []), record]);
for (const [category, items] of grouped) {
lines.push("", `## ${category}`);
for (const item of items) {
lines.push(`- [[../${item.path}|${item.path}]] · ${item.status} · ${item.rightsStatus} · ${item.bytes} bytes · \`${item.sha256.slice(0, 12)}\``);
}
}
await atomicWrite(join(state.projectRoot, "content", "artifact-index.md"), `${lines.join("\n")}\n`);
const board = [
frontmatter("Production Board", "generated-board", ["pom", "dashboard"]),
"",
"# Production Board",
"",
`Project: **${state.projectTitle}**`,
"",
"## Stages",
"",
"| Stage | Status | Attempts | Evidence | Blockers |",
"|---:|---|---:|---:|---:|",
...STAGES.map((item) => {
const run = state.stageRuns[String(item.id)];
return `| ${item.id} ${item.icon} ${item.name} | ${run.status} | ${run.attempts} | ${run.evidence.length}/${item.acceptance.length} | ${run.blockers.filter((blocker) => !blocker.resolvedAt).length} |`;
}),
"",
"## Active Stage",
"",
`[[../${stage(state.currentStage).artifacts[0]?.path ?? "POM.md"}|${stage(state.currentStage).name}]]`,
];
await atomicWrite(join(state.projectRoot, "content", "production-board.md"), `${board.join("\n")}\n`);
}
export async function writeHiveMission(state: PomState, mission: HiveMission): Promise<string> {
const path = join(state.projectRoot, "06_ledgers", `hive-${mission.id}.md`);
const text = [
frontmatter(mission.title, "hive-mission", ["pom", "hive", `stage-${mission.stage}`]),
"",
`# ${mission.title}`,
"",
`Status: **${mission.status}**`,
"",
"## Tasks",
"",
...mission.tasks.flatMap((task) => [
`### ${task.name} · ${task.agent}`,
"",
task.objective,
"",
`Owns: ${task.owns.map((item) => `\`${item}\``).join(", ") || "integration only"}`,
"",
...task.acceptance.map((item) => `- [ ] ${item}`),
"",
]),
"## Synthesis",
"",
mission.synthesis ?? "_Pending._",
].join("\n");
await atomicWrite(path, `${text}\n`);
return relative(state.projectRoot, path).replaceAll("\\", "/");
}
export function schemaStamp(): string {
return POM_SCHEMA_VERSION;
}

36
src/policy.ts Normal file
View File

@@ -0,0 +1,36 @@
import { resolve } from "node:path";
function destructiveCommand(command: string): boolean {
const normalized = command.replace(/\s+/g, " ").trim();
return [
/\brm\b[^\n;&|]*(?:-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r|--recursive[^\n;&|]*--force|--force[^\n;&|]*--recursive)/i,
/\bgit\s+clean\b[^\n;&|]*-[a-z]*f/i,
/\bgit\s+reset\s+--hard\b/i,
/\bgit\s+(?:checkout|restore)\b[^\n;&|]*(?:\s--\s|\s\.\s*$)/i,
/\bfind\b[^\n;&|]*\s-delete\b/i,
/\bRemove-Item\b[^\n;&|]*(?:-Recurse[^\n;&|]*-Force|-Force[^\n;&|]*-Recurse)/i,
/\bdel\b[^\n;&|]*\/s[^\n;&|]*\/q/i,
/\brmdir\b[^\n;&|]*\/s[^\n;&|]*\/q/i,
].some((pattern) => pattern.test(normalized));
}
function inside(child: string, root: string): boolean {
const normalizedChild = resolve(child).replaceAll("\\", "/");
const normalizedRoot = resolve(root).replaceAll("\\", "/");
return normalizedChild === normalizedRoot || normalizedChild.startsWith(`${normalizedRoot}/`);
}
function likelyTargetsProject(command: string, projectRoot: string, projectSlug: string): boolean {
const normalized = command.replaceAll("\\", "/");
const root = resolve(projectRoot).replaceAll("\\", "/");
return normalized.includes(root)
|| normalized.includes(projectSlug)
|| /(?:^|\s)(?:\.|\.\/|\*|\.\*|\$PWD|%CD%|--)(?:\s|$|["'])/i.test(normalized)
|| /\b(?:git\s+(?:clean|reset|restore|checkout)|find\s+\.)\b/i.test(normalized)
|| /\bcd\s+[^;&|]+(?:&&|;).*\b(?:rm|Remove-Item|del|rmdir)\b/i.test(normalized);
}
export function shouldBlockProjectCommand(command: string, projectRoot: string, projectSlug: string, executionCwd: string): boolean {
if (!destructiveCommand(command)) return false;
return inside(executionCwd, projectRoot) || likelyTargetsProject(command, projectRoot, projectSlug);
}

118
src/prompts.ts Normal file
View File

@@ -0,0 +1,118 @@
import { createHash } from "node:crypto";
import { readFile, readdir } from "node:fs/promises";
import { basename, join } from "node:path";
import { fileURLToPath } from "node:url";
import { stage, type PomState } from "./domain";
import { pathExists, recordPromptUse } from "./persistence";
export interface PromptFragment {
id: string;
source: "bundled" | "project";
path: string;
content: string;
sha256: string;
}
export interface PromptStack {
ids: string[];
fragments: PromptFragment[];
rendered: string;
sha256: string;
}
const bundledRoot = fileURLToPath(new URL("../prompts", import.meta.url));
function hash(text: string): string {
return createHash("sha256").update(text).digest("hex");
}
function substitute(text: string, variables: Record<string, string>): string {
return text.replace(/\{\{\s*([A-Z0-9_]+)\s*\}\}/g, (_match, key: string) => variables[key] ?? `{{${key}}}`);
}
async function loadDirectory(root: string, source: PromptFragment["source"]): Promise<PromptFragment[]> {
if (!(await pathExists(root))) return [];
const files = (await readdir(root)).filter((file) => file.endsWith(".md")).sort();
return Promise.all(files.map(async (file) => {
const path = join(root, file);
const content = await readFile(path, "utf8");
return { id: basename(file, ".md"), source, path, content, sha256: hash(content) };
}));
}
export async function listPrompts(state?: PomState): Promise<PromptFragment[]> {
const bundled = await loadDirectory(bundledRoot, "bundled");
if (!state) return bundled;
const project = await loadDirectory(join(state.projectRoot, ".omp", "prompts"), "project");
const byId = new Map(bundled.map((fragment) => [fragment.id, fragment]));
for (const fragment of project) byId.set(fragment.id, fragment);
return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
}
export async function getPrompt(state: PomState | undefined, id: string): Promise<PromptFragment> {
const prompt = (await listPrompts(state)).find((fragment) => fragment.id === id);
if (!prompt) throw new Error(`Unknown POM prompt: ${id}`);
return prompt;
}
function immutableLaw(state: PomState): string {
const spec = stage(state.currentStage);
return [
"# POM Production Law",
"Canonical project files outrank chat, memory, and agent claims.",
"OMP owns execution: use native task, IRC, todo, memory, checkpoints, tools, and approvals.",
"POM owns contracts, provenance, canon, evidence, integration, validation, and delivery truth.",
"Never claim a file, metric, test, citation, image, archive, or gate result that was not actually produced and checked.",
`Active project root: ${state.projectRoot}`,
`Active stage: ${spec.id}/8 · ${spec.name}`,
].join("\n");
}
export async function composePromptStack(state: PomState, ids: string[], extraVariables: Record<string, string> = {}): Promise<PromptStack> {
const spec = stage(state.currentStage);
const variables = {
PROJECT_TITLE: state.projectTitle,
PROJECT_ROOT: state.projectRoot,
PROJECT_ID: state.projectId,
STAGE_ID: String(spec.id),
STAGE_NAME: spec.name,
STAGE_SLUG: spec.slug,
MODE: state.mode,
...extraVariables,
};
const personalityId = `personality-${state.mode}`;
const resolvedIds = [...new Set([personalityId, ...ids])];
const fragments = await Promise.all(resolvedIds.map((id) => getPrompt(state, id)));
const renderedFragments = fragments.map((fragment) => `\n\n<!-- prompt:${fragment.id} source:${fragment.source} sha256:${fragment.sha256} -->\n${substitute(fragment.content, variables).trim()}`);
const rendered = `${immutableLaw(state)}${renderedFragments.join("")}\n`;
const stack: PromptStack = { ids: resolvedIds, fragments, rendered, sha256: hash(rendered) };
await recordPromptUse(state, {
type: "prompt.stack",
ids: resolvedIds,
stackSha256: stack.sha256,
fragments: fragments.map(({ id, source, sha256 }) => ({ id, source, sha256 })),
});
return stack;
}
export async function renderStagePrompt(state: PomState, allStages = false): Promise<string> {
const spec = stage(state.currentStage);
const stack = await composePromptStack(state, [allStages ? "full-autopilot" : "stage-run"], {
STAGE_CONTRACT: JSON.stringify(spec, null, 2),
});
return [
stack.rendered,
"# Executable Stage Contract",
JSON.stringify(spec, null, 2),
"# Required Native OMP Loop",
"1. Initialize the stage todo phases with the native todo tool.",
"2. Read only the canonical dependencies required for this stage.",
"3. Call pom_hive with op=plan, then invoke native task exactly once with the returned batch.",
"4. Use native IRC for short CLAIM, QUERY, ANSWER, ALERT, HANDOFF, and DONE signals.",
"5. Integrate specialist findings into canonical files; specialists do not decide final canon independently.",
"6. Register every canonical artifact with pom_artifact.",
"7. Record one pom_stage evidence entry per acceptance criterion.",
"8. Run pom_validate scope=stage. Repair failures, then call pom_stage pass; otherwise block with exact evidence.",
allStages ? "9. After PASS, continue to the next stage until stage 8 completes or a real blocker stops production." : "",
].filter(Boolean).join("\n\n");
}

77
src/renderers.ts Normal file
View File

@@ -0,0 +1,77 @@
import { Text } from "@oh-my-pi/pi-tui";
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
import type { PomView } from "./domain";
import type { PomRuntime } from "./runtime";
import { stage } from "./domain";
function textOf(content: unknown): string {
if (typeof content === "string") return content;
try { return JSON.stringify(content, null, 2); } catch { return String(content); }
}
function canExpand(view: PomView, expanded: boolean): boolean {
return expanded && (view === "cinematic" || view === "rich");
}
function maxLines(view: PomView): number {
return view === "cinematic" ? 10 : view === "rich" ? 5 : 0;
}
export function registerPomRenderers(pi: ExtensionAPI, runtime: PomRuntime): void {
pi.registerMessageRenderer("pom:event", (message, options, theme) => {
const view = runtime.get().view;
const details = message.details as { title?: string; level?: string; lines?: string[] } | undefined;
const mark = details?.level === "error" ? theme.fg("error", "×") : details?.level === "warning" ? theme.fg("warning", "!") : theme.fg("accent", "◆");
const lines = canExpand(view, options.expanded) ? (details?.lines ?? []).slice(0, maxLines(view)) : [];
const body = lines.length ? `\n${lines.map((line) => theme.fg("dim", line)).join("\n")}` : "";
return new Text(`${mark} ${theme.bold(details?.title ?? textOf(message.content))}${body}`, view === "minimal" ? 0 : 1, 0);
});
pi.registerMessageRenderer("pom:validation", (message, options, theme) => {
const view = runtime.get().view;
const report = message.details as { passed?: boolean; scope?: string; checks?: Array<{ status: string; message: string }> } | undefined;
const failures = report?.checks?.filter((item) => item.status === "fail") ?? [];
const warnings = report?.checks?.filter((item) => item.status === "warn") ?? [];
const passed = Boolean(report?.passed);
const mark = passed ? theme.fg("success", "✓") : theme.fg("error", "×");
const limit = maxLines(view);
const lines = canExpand(view, options.expanded)
? [
...failures.slice(0, Math.max(1, limit - 2)).map((item) => `${theme.fg("error", "×")} ${item.message}`),
...warnings.slice(0, Math.min(3, limit)).map((item) => `${theme.fg("warning", "!")} ${item.message}`),
].slice(0, limit)
: [];
const summary = `${mark} ${theme.bold(`POM validation · ${report?.scope ?? "unknown"}`)} · ${failures.length} failures · ${warnings.length} warnings`;
return new Text(`${summary}${lines.length ? `\n${lines.join("\n")}` : ""}`, view === "minimal" ? 0 : 1, 0);
});
pi.registerMessageRenderer("pom:hive", (message, options, theme) => {
const view = runtime.get().view;
const mission = message.details as { id?: string; status?: string; tasks?: Array<{ name: string; agent: string }> } | undefined;
const roster = canExpand(view, options.expanded)
? `\n${(mission?.tasks ?? []).slice(0, maxLines(view)).map((task) => ` ${theme.fg("accent", "⬡")} ${task.name} · ${theme.fg("dim", task.agent)}`).join("\n")}`
: "";
return new Text(`${theme.fg("accent", "⬡")} ${theme.bold(`Hive ${mission?.id ?? "mission"}`)} · ${mission?.status ?? "planned"} · ${mission?.tasks?.length ?? 0} agents${roster}`, view === "minimal" ? 0 : 1, 0);
});
pi.registerMessageRenderer("pom:milestone", (message, options, theme) => {
const view = runtime.get().view;
const details = message.details as { stage?: number; status?: string; path?: string; commit?: string } | undefined;
const stageId = details?.stage ?? runtime.get().currentStage;
const spec = stage(stageId);
const extra = canExpand(view, options.expanded)
? [details?.path, details?.commit ? `commit ${details.commit}` : undefined].filter(Boolean).map((line) => theme.fg("dim", String(line))).join("\n")
: "";
return new Text(`${theme.fg("success", "◆")} ${theme.bold(`${spec.icon} Stage ${stageId} ${details?.status ?? "milestone"}`)}${extra ? `\n${extra}` : ""}`, view === "minimal" ? 0 : 1, 0);
});
pi.registerAssistantThinkingRenderer((context, theme) => {
const state = runtime.get();
const config = runtime.getConfig();
if (!state.initialized || !config.showThinkingLane || state.view === "minimal" || state.view === "compact") return undefined;
const spec = stage(state.currentStage);
const lane = runtime.telemetry.activeTool ? `tool:${runtime.telemetry.activeTool}` : state.hive.activeMissionId ? "hive-synthesis" : "synthesis";
const suffix = state.view === "cinematic" ? ` · ${context.text.length} chars` : "";
return new Text(theme.fg("dim", `╰─ POM lane · ${spec.slug} · ${lane}${suffix}`), 1, 0);
});
}

16
src/router.ts Normal file
View File

@@ -0,0 +1,16 @@
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import { stage, type PomState } from "./domain";
import { applyToolProfile } from "./tool-profiles";
export async function routeStage(pi: ExtensionAPI, ctx: ExtensionContext, state: PomState): Promise<{ tools: string[]; model?: string }> {
const spec = stage(state.currentStage);
const tools = state.autoToolProfiles ? await applyToolProfile(pi, spec.toolProfile) : pi.getActiveTools();
let modelName: string | undefined;
if (state.autoModelRouting) {
const model = ctx.models.resolve(spec.modelRole);
if (model && await pi.setModel(model)) modelName = `${model.provider}/${model.id}`;
}
const thinking = spec.id === 7 ? "high" : "xhigh";
try { pi.setThinkingLevel(thinking as Parameters<ExtensionAPI["setThinkingLevel"]>[0]); } catch { /* model may not support this level */ }
return { tools, model: modelName };
}

22
src/runtime.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import type { PomConfig, PomState } from "./domain";
export interface PomTelemetry {
activeTool?: string;
activeIntent?: string;
toolStartedAt?: number;
lastToolResult?: "pass" | "fail";
lastIrcSignal?: string;
activeAgents: number;
completedAgents: number;
validationFailures?: number;
}
export interface PomRuntime {
get(): PomState;
set(state: PomState): Promise<void>;
restore(ctx: ExtensionContext): Promise<PomState>;
getConfig(): PomConfig;
refreshConfig(cwd: string): Promise<PomConfig>;
telemetry: PomTelemetry;
}

217
src/state.ts Normal file
View File

@@ -0,0 +1,217 @@
import { randomUUID } from "node:crypto";
import { join, resolve } from "node:path";
import {
DEFAULT_CONFIG,
POM_SCHEMA_VERSION,
POM_VERSION,
STAGES,
freshStageRuns,
stage,
type EvidenceRecord,
type PomConfig,
type PomState,
type StageBlocker,
} from "./domain";
export function bumpPatchVersion(version: string): string {
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
if (!match) throw new Error(`Invalid semantic version: ${version}`);
return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
}
export function slugify(value: string): string {
return value
.normalize("NFKD")
.replace(/[^\x00-\x7F]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "") || "magnum-opus";
}
export function createState(title: string, cwd: string, config: PomConfig = DEFAULT_CONFIG, initialized = false): PomState {
const projectSlug = slugify(title);
const projectRoot = config.projectRootMode === "cwd" ? resolve(cwd) : resolve(cwd, projectSlug);
return {
schemaVersion: POM_SCHEMA_VERSION,
pomVersion: POM_VERSION,
projectId: randomUUID(),
projectTitle: title.trim() || "Untitled Magnum Opus",
projectSlug,
projectRoot,
initialized,
currentStage: 0,
stageRuns: freshStageRuns(),
mode: config.defaultMode,
view: config.defaultView,
motion: config.defaultMotion,
toolProfile: config.defaultToolProfile,
autoToolProfiles: config.autoToolProfiles,
autoModelRouting: config.autoModelRouting,
completedStages: [],
decisions: [],
assumptions: [],
warnings: [],
hive: { missions: {}, signalsLogged: 0 },
artifactVersion: "1.0.0",
canonVersion: "1.0.0",
updatedAt: new Date().toISOString(),
};
}
export function assertState(value: unknown): asserts value is PomState {
if (!value || typeof value !== "object") throw new Error("POM state must be an object");
const state = value as Partial<PomState>;
if (state.schemaVersion !== POM_SCHEMA_VERSION) throw new Error(`Unsupported POM state schema: ${String(state.schemaVersion)}`);
if (typeof state.projectId !== "string" || typeof state.projectRoot !== "string" || typeof state.projectTitle !== "string") {
throw new Error("POM state identity is invalid");
}
if (!Number.isInteger(state.currentStage) || Number(state.currentStage) < 0 || Number(state.currentStage) > 8) {
throw new Error("POM current stage is invalid");
}
if (!state.stageRuns || typeof state.stageRuns !== "object") throw new Error("POM stage state is missing");
for (const spec of STAGES) {
const run = state.stageRuns[String(spec.id)];
if (!run || !["queued", "active", "blocked", "complete"].includes(run.status)) {
throw new Error(`POM stage ${spec.id} state is invalid`);
}
if (!Array.isArray(run.blockers) || !Array.isArray(run.evidence)) throw new Error(`POM stage ${spec.id} records are invalid`);
}
}
export function restoreLatest(entries: readonly unknown[]): PomState | undefined {
for (let index = entries.length - 1; index >= 0; index -= 1) {
const entry = entries[index] as { type?: string; customType?: string; data?: unknown };
if (entry?.type === "custom" && entry.customType === "pom-state") {
assertState(entry.data);
return structuredClone(entry.data);
}
}
return undefined;
}
function incompletePrerequisites(state: PomState, target: number): number[] {
return STAGES.filter((item) => item.id < target && state.stageRuns[String(item.id)].status !== "complete").map((item) => item.id);
}
export function gotoStage(state: PomState, target: number): PomState {
stage(target);
const missing = incompletePrerequisites(state, target);
if (missing.length) throw new Error(`Cannot enter stage ${target}; incomplete prerequisites: ${missing.join(", ")}`);
const next = structuredClone(state);
next.currentStage = target;
return next;
}
export function startStage(state: PomState, target = state.currentStage, allStages = false): PomState {
const next = gotoStage(state, target);
const run = next.stageRuns[String(target)];
if (run.status === "complete") throw new Error(`Stage ${target} is already complete`);
const unresolved = run.blockers.filter((blocker) => !blocker.resolvedAt);
if (unresolved.length) throw new Error(`Stage ${target} has unresolved blockers`);
run.status = "active";
run.attempts += 1;
run.startedAt = new Date().toISOString();
next.activeRun = { id: randomUUID(), stage: target, startedAt: run.startedAt, continuationCount: 0, allStages };
next.currentStage = target;
return next;
}
export function addBlocker(state: PomState, reason: string, owner?: string): PomState {
const text = reason.trim();
if (!text) throw new Error("Blocker reason is required");
const next = structuredClone(state);
const run = next.stageRuns[String(next.currentStage)];
const blocker: StageBlocker = { id: randomUUID(), reason: text, owner, createdAt: new Date().toISOString() };
run.blockers.push(blocker);
run.status = "blocked";
delete next.activeRun;
return next;
}
export function resolveBlocker(state: PomState, blockerId: string, resolution: string): PomState {
const next = structuredClone(state);
const owner = Object.values(next.stageRuns).find((run) => run.blockers.some((item) => item.id === blockerId));
const blocker = owner?.blockers.find((item) => item.id === blockerId);
if (!owner || !blocker) throw new Error(`Unknown blocker: ${blockerId}`);
if (blocker.resolvedAt) throw new Error(`Blocker already resolved: ${blockerId}`);
blocker.resolvedAt = new Date().toISOString();
blocker.resolution = resolution.trim() || "Resolved";
if (owner.status === "blocked" && owner.blockers.every((item) => item.resolvedAt)) owner.status = "queued";
return next;
}
export function recordEvidence(
state: PomState,
criterionId: string,
summary: string,
paths: string[],
agent?: string,
): PomState {
const spec = stage(state.currentStage);
if (!spec.acceptance.some((criterion) => criterion.id === criterionId)) {
throw new Error(`Criterion ${criterionId} does not belong to stage ${spec.id}`);
}
if (!summary.trim()) throw new Error("Evidence summary is required");
if (!paths.length) throw new Error("Evidence must reference at least one project path");
const next = structuredClone(state);
const run = next.stageRuns[String(next.currentStage)];
const record: EvidenceRecord = {
id: randomUUID(),
criterionId,
summary: summary.trim(),
paths: [...new Set(paths.map((path) => path.replaceAll("\\", "/")))],
agent,
createdAt: new Date().toISOString(),
};
run.evidence = [...run.evidence.filter((item) => item.criterionId !== criterionId), record];
return next;
}
export function passStage(state: PomState, validationReportPath: string): PomState {
const next = structuredClone(state);
const target = next.currentStage;
const spec = stage(target);
const run = next.stageRuns[String(target)];
if (run.status !== "active" || next.activeRun?.stage !== target) throw new Error(`Stage ${target} is not actively running`);
if (!validationReportPath.trim()) throw new Error("Validation report path is required");
const unresolved = run.blockers.filter((blocker) => !blocker.resolvedAt);
if (unresolved.length) throw new Error(`Stage ${target} has unresolved blockers`);
const missingEvidence = spec.acceptance.filter((criterion) => !run.evidence.some((evidence) => evidence.criterionId === criterion.id));
if (missingEvidence.length) throw new Error(`Stage ${target} lacks evidence: ${missingEvidence.map((item) => item.id).join(", ")}`);
run.status = "complete";
run.completedAt = new Date().toISOString();
run.validationReportPath = validationReportPath;
next.completedStages = [...new Set([...next.completedStages, target])].sort((a, b) => a - b);
delete next.activeRun;
if (target < 8) next.currentStage = target + 1;
return next;
}
export function stageProgress(state: PomState): { complete: number; total: number; percent: number } {
const complete = STAGES.filter((item) => state.stageRuns[String(item.id)].status === "complete").length;
return { complete, total: STAGES.length, percent: Math.round((complete / STAGES.length) * 100) };
}
export function openBlockers(state: PomState): StageBlocker[] {
return Object.values(state.stageRuns).flatMap((run) => run.blockers).filter((item) => !item.resolvedAt);
}
export function summarizeState(state: PomState): string {
const spec = stage(state.currentStage);
const run = state.stageRuns[String(state.currentStage)];
const progress = stageProgress(state);
const evidence = new Set(run.evidence.map((item) => item.criterionId)).size;
return [
`${spec.icon} POM · ${state.projectTitle}`,
`Stage ${spec.id}/8 · ${spec.name} · ${run.status.toUpperCase()}`,
`Progress ${progress.complete}/${progress.total} · ${progress.percent}%`,
`Evidence ${evidence}/${spec.acceptance.length} · Blockers ${openBlockers(state).length}`,
`Hive ${Object.keys(state.hive.missions).length} missions · ${state.hive.signalsLogged} signals`,
`Root ${state.projectRoot}`,
].join("\n");
}
export function statePath(state: PomState): string {
return join(state.projectRoot, "00_admin", "project_state.json");
}

70
src/studio.ts Normal file
View File

@@ -0,0 +1,70 @@
import { randomUUID } from "node:crypto";
import { stage, type HiveMission, type HiveTask, type PomConfig, type PomState } from "./domain";
export interface NativeTaskBatch {
context: string;
tasks: Array<{ name: string; agent: string; task: string; isolated?: boolean }>;
}
function agentLabel(agent: string, index: number): string {
return `${agent.replace(/^pom-/, "").split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join("")}${index + 1}`.slice(0, 80);
}
export function buildHiveMission(state: PomState, config: PomConfig, preset: "lean" | "stage" | "audit" = "stage"): HiveMission {
const spec = stage(state.currentStage);
let agents = preset === "lean" ? spec.agents.slice(0, 2) : [...spec.agents];
if (preset === "audit") agents = [...new Set(["pom-continuity-auditor", "pom-rights-auditor", "pom-critic", ...agents])];
agents = agents.slice(0, config.maxStudioAgents);
const artifacts = spec.artifacts.map((artifact) => artifact.path);
const tasks: HiveTask[] = agents.map((agent, index) => ({
name: agentLabel(agent, index),
agent,
objective: `Independently investigate and produce evidence for stage ${spec.id} (${spec.name}) from the perspective of ${agent}.`,
owns: artifacts.filter((_path, artifactIndex) => artifactIndex % agents.length === index),
acceptance: spec.acceptance.filter((_criterion, criterionIndex) => criterionIndex % agents.length === index).map((criterion) => `${criterion.id}: ${criterion.description}`),
isolated: false,
}));
return {
id: `s${spec.id}-${randomUUID().slice(0, 8)}`,
stage: spec.id,
title: `${spec.icon} Stage ${spec.id} Hive · ${spec.name}`,
status: "planned",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
tasks,
};
}
export function missionToTaskBatch(state: PomState, mission: HiveMission): NativeTaskBatch {
const spec = stage(mission.stage);
const shared = [
`Project: ${state.projectTitle}`,
`Canonical root: ${state.projectRoot}`,
`Stage: ${spec.id}/8 · ${spec.name}`,
"Canonical files outrank chat. Read before changing anything.",
"Coordinate through native IRC using only short messages prefixed CLAIM, QUERY, ANSWER, ALERT, HANDOFF, or DONE.",
"Before modifying a shared path, send CLAIM to all. On collision, negotiate ownership before writing.",
"Use pom_hive op=signal for durable milestone evidence; IRC is ephemeral coordination, the Hive ledger is durable truth.",
"Never mark the stage passed. Return findings and evidence to Main; Main integrates and validates.",
].join("\n");
return {
context: shared,
tasks: mission.tasks.map((task) => ({
name: task.name,
agent: task.agent,
isolated: task.isolated,
task: [
`# Mission\n${task.objective}`,
`# Ownership\n${task.owns.length ? task.owns.map((path) => `- ${path}`).join("\n") : "- No exclusive files; inspect and advise only."}`,
`# Acceptance\n${task.acceptance.length ? task.acceptance.map((item) => `- ${item}`).join("\n") : "- Return evidence-backed findings relevant to the stage contract."}`,
"# Execution Contract",
"1. Read canonical dependencies.",
"2. Announce CLAIM for owned paths through IRC.",
"3. Work independently; do not overwrite another agent's claimed path.",
"4. Record durable evidence with pom_hive op=signal.",
"5. Send DONE or ALERT to Main through IRC.",
"6. Finish with a compact evidence report containing paths, decisions, risks, and unresolved questions.",
].join("\n\n"),
})),
};
}

83
src/swarm.ts Normal file
View File

@@ -0,0 +1,83 @@
import { join } from "node:path";
import type { HiveMission, PomState } from "./domain";
import { atomicWrite } from "./persistence";
import { secureFuturePath } from "./paths";
export type PomSwarmMode = "parallel" | "sequential" | "pipeline";
function yamlString(value: string): string {
return JSON.stringify(value);
}
function block(value: string, indent: number): string[] {
const prefix = " ".repeat(indent);
return value.replaceAll("\r\n", "\n").split("\n").map((line) => `${prefix}${line}`);
}
function key(value: string, index: number): string {
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
return `${normalized || "agent"}_${index + 1}`;
}
export async function writeSwarmDefinition(
state: PomState,
mission: HiveMission,
mode: PomSwarmMode = "parallel",
targetCount = 1,
): Promise<string> {
if (!Number.isInteger(targetCount) || targetCount < 1 || targetCount > 100) throw new Error("Swarm target count must be 1..100");
const directory = join("00_admin", "swarms");
const relativePath = join(directory, `${mission.id}.yaml`).replaceAll("\\", "/");
const absolutePath = await secureFuturePath(state.projectRoot, relativePath);
const names = mission.tasks.map((task, index) => key(task.name, index));
const lines = [
"swarm:",
` name: ${yamlString(`pom-${mission.id}`)}`,
` workspace: ${yamlString(state.projectRoot)}`,
` mode: ${mode}`,
...(mode === "pipeline" ? [` target_count: ${targetCount}`] : []),
" agents:",
];
mission.tasks.forEach((task, index) => {
const agentName = names[index];
const reportPath = `06_ledgers/swarm/${mission.id}/${agentName}.md`;
lines.push(
` ${agentName}:`,
` role: ${yamlString(task.agent)}`,
" task: |",
...block([
`Project: ${state.projectTitle}`,
`Stage: ${mission.stage}`,
`Objective: ${task.objective}`,
"Canonical project files outrank chat and unsupported assumptions.",
`Exclusive ownership: ${task.owns.join(", ") || "advisory only"}`,
`Acceptance: ${task.acceptance.join(" | ") || "evidence-backed specialist findings"}`,
`Write a durable report to ${reportPath}.`,
"Do not pass the POM stage. Report paths, decisions, evidence, risks, and unresolved blockers.",
].join("\n"), 8),
" extra_context: |",
...block("Use OMP tools autonomously. Read before editing. Never overwrite another agent's declared ownership. Durable coordination belongs in files; concise status belongs in the orchestrator log.", 8),
" reports_to:",
" - integrator",
);
});
lines.push(
" integrator:",
" role: pom-architect",
" task: |",
...block([
`Read every specialist report under 06_ledgers/swarm/${mission.id}/.`,
"Compare findings against canonical files and the active stage contract.",
`Write an integration report to 06_ledgers/swarm/${mission.id}/integration.md.`,
"Do not silently modify canon where specialists disagree; record the conflict and smallest repair.",
"Return an evidence map suitable for Main to register through POM and validate.",
].join("\n"), 8),
" waits_for:",
...names.map((name) => ` - ${name}`),
);
await atomicWrite(absolutePath, `${lines.join("\n")}\n`);
return relativePath;
}

23
src/themes.ts Normal file
View File

@@ -0,0 +1,23 @@
import { copyFile, mkdir, readdir } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { getCustomThemesDir } from "@oh-my-pi/pi-utils";
export interface InstalledTheme {
name: string;
path: string;
}
export async function installBundledThemes(): Promise<InstalledTheme[]> {
const sourceDir = fileURLToPath(new URL("../themes", import.meta.url));
const targetDir = getCustomThemesDir();
await mkdir(targetDir, { recursive: true });
const files = (await readdir(sourceDir)).filter((file) => file.endsWith(".json")).sort();
const installed: InstalledTheme[] = [];
for (const file of files) {
const target = join(targetDir, file);
await copyFile(join(sourceDir, file), target);
installed.push({ name: file.slice(0, -5), path: target });
}
return installed;
}

32
src/tool-profiles.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
import type { PomToolProfile } from "./domain";
const POM_TOOLS = ["pom_state", "pom_stage", "pom_artifact", "pom_validate", "pom_prompt", "pom_hive", "pom_export"];
const PROFILES: Record<Exclude<PomToolProfile, "auto">, string[]> = {
creative: ["read", "write", "edit", "grep", "glob", "eval", "task", "irc", "todo", "ask", "render_mermaid", "generate_image", "inspect_image"],
research: ["read", "write", "edit", "grep", "glob", "eval", "task", "irc", "todo", "ask", "web_search", "browser", "github"],
draft: ["read", "write", "edit", "grep", "glob", "eval", "task", "irc", "todo", "ask", "inspect_image"],
audit: ["read", "grep", "glob", "eval", "lsp", "task", "irc", "todo", "ask", "inspect_image", "github"],
delivery: ["read", "write", "edit", "grep", "glob", "eval", "bash", "task", "irc", "todo", "checkpoint", "github"],
};
let baselines = new WeakMap<object, string[]>();
export async function applyToolProfile(pi: ExtensionAPI, profile: PomToolProfile): Promise<string[]> {
const key = pi as unknown as object;
let baseline = baselines.get(key);
if (!baseline) {
baseline = [...pi.getActiveTools()];
baselines.set(key, baseline);
}
const all = new Set(pi.getAllTools());
const requested = profile === "auto" ? baseline : [...PROFILES[profile], ...POM_TOOLS];
const selected = [...new Set(requested)].filter((name) => all.has(name));
if (!selected.includes("read") && all.has("read")) selected.unshift("read");
await pi.setActiveTools(selected);
return selected;
}
export function resetToolBaselineForTests(): void {
baselines = new WeakMap<object, string[]>();
}

327
src/tools.ts Normal file
View File

@@ -0,0 +1,327 @@
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import { join } from "node:path";
import {
addBlocker,
bumpPatchVersion,
gotoStage,
passStage,
recordEvidence,
resolveBlocker,
startStage,
summarizeState,
} from "./state";
import { stage, type ArtifactRecord, type PomState, type ValidationScope } from "./domain";
import {
appendMarkdownLog,
recordHiveEvent,
registerArtifact,
syncKnowledgeBase,
writeHiveMission,
} from "./persistence";
import { validatePom } from "./validators";
import { getPrompt, listPrompts, renderStagePrompt } from "./prompts";
import { buildHiveMission, missionToTaskBatch } from "./studio";
import { exportProject } from "./exporter";
import { loadPomConfig } from "./config";
import { routeStage } from "./router";
import { checkpointStage, enterStageBranch } from "./git";
import { toolCallComponent, toolResultComponent, type PomTheme } from "./visual";
import type { PomRuntime } from "./runtime";
import { updateChrome } from "./ui";
function visual(runtime: PomRuntime, label: string) {
return {
renderCall(args: Record<string, unknown>, options: { spinnerFrame?: number }, theme: PomTheme) {
const action = [args.action, args.op, args.scope, args.kind, args.path].find((value) => typeof value === "string") as string | undefined;
return toolCallComponent(label, action, theme, runtime.get().motion, options.spinnerFrame);
},
renderResult(
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
options: { expanded: boolean; isPartial: boolean },
theme: PomTheme,
) {
return toolResultComponent(result, options.expanded, options.isPartial, theme, runtime.get().view);
},
};
}
function textResult(text: string, details?: unknown, isError = false) {
return { content: [{ type: "text" as const, text }], details, isError };
}
async function refresh(_pi: ExtensionAPI, runtime: PomRuntime, ctx: ExtensionContext): Promise<void> {
const state = runtime.get();
const config = await loadPomConfig(state.projectRoot);
updateChrome(ctx, state, config, runtime);
}
export function registerPomTools(pi: ExtensionAPI, runtime: PomRuntime): void {
const { z } = pi.zod;
// @ts-expect-error OMP 16.4.x + Zod v4 can exceed TypeScript instantiation depth while contextualizing this complete tool schema.
pi.registerTool({
...visual(runtime, "State"),
name: "pom_state",
label: "POM State",
description: "Read or update canonical POM configuration, decisions, assumptions, and warnings.",
defaultInactive: true,
approval: "write",
parameters: z.object({
action: z.enum(["get", "configure", "decision", "assumption", "warning"]),
value: z.string().optional(),
mode: z.enum(["autopilot", "collaborative", "strict-autopilot"]).optional(),
view: z.enum(["cinematic", "rich", "compact", "minimal"]).optional(),
motion: z.enum(["off", "subtle", "full"]).optional(),
}),
async execute(_id, params, signal, _onUpdate, ctx) {
if (signal?.aborted) return textResult("Cancelled", undefined, true);
const state = structuredClone(runtime.get());
if (params.action === "configure") {
state.mode = params.mode ?? state.mode;
state.view = params.view ?? state.view;
state.motion = params.motion ?? state.motion;
}
if (params.action === "decision" && params.value?.trim() && !state.decisions.includes(params.value.trim())) state.decisions.push(params.value.trim());
if (params.action === "assumption" && params.value?.trim() && !state.assumptions.includes(params.value.trim())) state.assumptions.push(params.value.trim());
if (params.action === "warning" && params.value?.trim() && !state.warnings.includes(params.value.trim())) state.warnings.push(params.value.trim());
if (params.action !== "get") await runtime.set(state);
await refresh(pi, runtime, ctx);
return textResult(summarizeState(runtime.get()), runtime.get());
},
});
pi.registerTool({
...visual(runtime, "Stage"),
name: "pom_stage",
label: "POM Stage",
description: "Start, inspect, evidence, block, resolve, pass, or navigate the authoritative stage machine.",
defaultInactive: true,
approval: "write",
parameters: z.object({
action: z.enum(["inspect", "start", "evidence", "block", "resolve", "pass", "goto"]),
stage: z.number().int().min(0).max(8).optional(),
criterionId: z.string().optional(),
summary: z.string().optional(),
paths: z.array(z.string()).optional(),
agent: z.string().optional(),
reason: z.string().optional(),
owner: z.string().optional(),
blockerId: z.string().optional(),
resolution: z.string().optional(),
allStages: z.boolean().optional(),
validationScope: z.enum(["quick", "stage", "canon", "continuity", "knowledge", "files", "delivery", "all"]).optional(),
}),
async execute(_id, params, signal, onUpdate, ctx) {
if (signal?.aborted) return textResult("Cancelled", undefined, true);
let state = runtime.get();
if (params.action === "start") {
state = startStage(state, params.stage ?? state.currentStage, params.allStages ?? false);
await runtime.set(state);
const config = await loadPomConfig(state.projectRoot);
await enterStageBranch(state, config);
await routeStage(pi, ctx, state);
} else if (params.action === "goto") {
if (params.stage === undefined) throw new Error("stage is required");
state = gotoStage(state, params.stage);
await runtime.set(state);
} else if (params.action === "evidence") {
if (!params.criterionId || !params.summary) throw new Error("criterionId and summary are required");
state = recordEvidence(state, params.criterionId, params.summary, params.paths ?? [], params.agent);
await runtime.set(state);
} else if (params.action === "block") {
state = addBlocker(state, params.reason ?? "Unspecified blocker", params.owner);
await runtime.set(state);
} else if (params.action === "resolve") {
if (!params.blockerId) throw new Error("blockerId is required");
state = resolveBlocker(state, params.blockerId, params.resolution ?? "Resolved");
await runtime.set(state);
} else if (params.action === "pass") {
onUpdate?.({ content: [{ type: "text", text: "Running deterministic stage gate…" }] });
const wasAllStages = state.activeRun?.allStages ?? false;
const scope = (params.validationScope ?? (state.currentStage === 8 ? "delivery" : "stage")) as ValidationScope;
const report = await validatePom(state, scope);
runtime.telemetry.validationFailures = report.checks.filter((item) => item.status === "fail").length;
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${scope}`, display: true, details: report }, { triggerTurn: false });
if (!report.passed || !report.reportPath) return textResult(`Stage gate FAIL · ${runtime.telemetry.validationFailures} failures`, report, true);
const passedStage = state.currentStage;
state = passStage(state, report.reportPath);
await runtime.set(state);
const config = await loadPomConfig(state.projectRoot);
const commit = await checkpointStage(state, config, `pom: complete stage ${passedStage} ${stage(passedStage).slug}`);
pi.sendMessage({ customType: "pom:milestone", content: `Stage ${passedStage} complete`, display: true, details: { stage: passedStage, status: "PASS", path: report.reportPath, commit } }, { triggerTurn: false });
if (wasAllStages && passedStage < 8) {
state = startStage(state, state.currentStage, true);
await runtime.set(state);
await enterStageBranch(state, config);
await routeStage(pi, ctx, state);
pi.sendUserMessage(await renderStagePrompt(state, true), { deliverAs: "followUp" });
}
}
await syncKnowledgeBase(runtime.get());
await refresh(pi, runtime, ctx);
return textResult(summarizeState(runtime.get()), runtime.get());
},
});
pi.registerTool({
...visual(runtime, "Artifact"),
name: "pom_artifact",
label: "POM Artifact",
description: "Register a non-empty canonical artifact with measured size, SHA-256, status, source, and rights metadata.",
defaultInactive: true,
approval: "write",
parameters: z.object({
path: z.string(),
category: z.string().default("other"),
status: z.enum(["draft", "reviewed", "approved", "superseded"]).default("draft"),
rightsStatus: z.enum(["original", "licensed", "public-domain", "reference-only", "unknown"]).default("unknown"),
source: z.string().optional(),
notes: z.string().optional(),
}),
async execute(_id, params, signal) {
if (signal?.aborted) return textResult("Cancelled", undefined, true);
const state = runtime.get();
const artifactVersion = bumpPatchVersion(state.artifactVersion);
const record = await registerArtifact(state, params.path, {
category: params.category,
status: params.status as ArtifactRecord["status"],
rightsStatus: params.rightsStatus as ArtifactRecord["rightsStatus"],
source: params.source,
notes: params.notes,
version: artifactVersion,
});
const next = structuredClone(state);
next.artifactVersion = artifactVersion;
if (/canon|bible|continuity|timeline|world|character/i.test(params.category)) {
next.canonVersion = bumpPatchVersion(next.canonVersion);
}
await runtime.set(next);
return textResult(`Registered ${record.path} · v${record.version} · ${record.bytes} bytes · ${record.sha256.slice(0, 12)}`, record);
},
});
pi.registerTool({
...visual(runtime, "Validate"),
name: "pom_validate",
label: "POM Validate",
description: "Run deterministic state, artifact, evidence, manifest, knowledge, continuity, and delivery checks.",
defaultInactive: true,
approval: "read",
parameters: z.object({
scope: z.enum(["quick", "stage", "canon", "continuity", "knowledge", "files", "delivery", "all"]).default("stage"),
}),
async execute(_id, params, signal, onUpdate) {
if (signal?.aborted) return textResult("Cancelled", undefined, true);
onUpdate?.({ content: [{ type: "text", text: `Validating ${params.scope}` }] });
const report = await validatePom(runtime.get(), params.scope as ValidationScope);
const failures = report.checks.filter((item) => item.status === "fail").length;
runtime.telemetry.validationFailures = failures;
pi.sendMessage({ customType: "pom:validation", content: `${report.passed ? "PASS" : "FAIL"} ${params.scope}`, display: true, details: report }, { triggerTurn: false });
return textResult(`${report.passed ? "PASS" : "FAIL"} · ${failures} failures · ${report.checks.length} checks`, report, !report.passed);
},
});
pi.registerTool({
...visual(runtime, "Prompt"),
name: "pom_prompt",
label: "POM Prompt",
description: "List or retrieve versioned POM prompt fragments, including project-local overrides and provenance hashes.",
defaultInactive: true,
approval: "read",
parameters: z.object({ action: z.enum(["list", "get"]), id: z.string().optional() }),
async execute(_id, params) {
const state = runtime.get().initialized ? runtime.get() : undefined;
if (params.action === "list") {
const prompts = await listPrompts(state);
return textResult(prompts.map((item) => `${item.id}\t${item.source}\t${item.sha256.slice(0, 12)}`).join("\n"), prompts);
}
if (!params.id) throw new Error("id is required");
const prompt = await getPrompt(state, params.id);
return textResult(prompt.content, prompt);
},
});
pi.registerTool({
...visual(runtime, "Hive"),
name: "pom_hive",
label: "POM Hive",
description: "Plan native task batches, persist inter-agent signals, and record final synthesis without replacing OMP task or IRC.",
defaultInactive: true,
approval: "write",
parameters: z.object({
op: z.enum(["plan", "signal", "synthesize", "inspect"]),
preset: z.enum(["lean", "stage", "audit"]).optional(),
missionId: z.string().optional(),
signal: z.enum(["CLAIM", "QUERY", "ANSWER", "ALERT", "HANDOFF", "DONE"]).optional(),
from: z.string().optional(),
to: z.string().optional(),
message: z.string().optional(),
paths: z.array(z.string()).optional(),
status: z.enum(["complete", "blocked"]).optional(),
synthesis: z.string().optional(),
}),
async execute(_id, params, signal, _onUpdate, ctx) {
if (signal?.aborted) return textResult("Cancelled", undefined, true);
let state = structuredClone(runtime.get());
if (params.op === "inspect") return textResult(JSON.stringify(state.hive, null, 2), state.hive);
if (params.op === "plan") {
const config = await loadPomConfig(state.projectRoot);
const mission = buildHiveMission(state, config, params.preset ?? "stage");
const batch = missionToTaskBatch(state, mission);
state.hive.activeMissionId = mission.id;
state.hive.missions[mission.id] = mission;
state.stageRuns[String(state.currentStage)].hiveMissionIds.push(mission.id);
await runtime.set(state);
const path = await writeHiveMission(state, mission);
await recordHiveEvent(state, { type: "mission.planned", mission, path });
pi.sendMessage({ customType: "pom:hive", content: mission.title, display: true, details: mission }, { triggerTurn: false });
await refresh(pi, runtime, ctx);
return textResult(`Call native task exactly once with this batch:\n${JSON.stringify(batch, null, 2)}`, { mission, batch, path });
}
const missionId = params.missionId ?? state.hive.activeMissionId;
if (!missionId || !state.hive.missions[missionId]) throw new Error("No matching hive mission");
const mission = state.hive.missions[missionId];
if (params.op === "signal") {
if (!params.signal || !params.message) throw new Error("signal and message are required");
state.hive.signalsLogged += 1;
runtime.telemetry.lastIrcSignal = `${params.signal} ${params.from ?? "agent"}: ${params.message}`;
await runtime.set(state);
await recordHiveEvent(state, { type: "hive.signal", missionId, signal: params.signal, from: params.from, to: params.to, message: params.message, paths: params.paths ?? [] });
return textResult(`${params.signal} recorded for ${missionId}`, { missionId, ...params });
}
mission.status = params.status === "blocked" ? "blocked" : "complete";
mission.synthesis = params.synthesis?.trim() || "Synthesis recorded without narrative.";
mission.updatedAt = new Date().toISOString();
if (state.hive.activeMissionId === missionId) delete state.hive.activeMissionId;
await runtime.set(state);
const path = await writeHiveMission(state, mission);
await recordHiveEvent(state, { type: "mission.synthesized", missionId, status: mission.status, synthesis: mission.synthesis, path });
pi.sendMessage({ customType: "pom:hive", content: `${mission.title} ${mission.status}`, display: true, details: mission }, { triggerTurn: false });
return textResult(`Hive ${missionId} ${mission.status}`, { mission, path });
},
});
pi.registerTool({
...visual(runtime, "Export"),
name: "pom_export",
label: "POM Export",
description: "Create a fresh delivery archive, parse its central directory, decompress every member, verify sizes and CRC-32, and hash the archive.",
defaultInactive: true,
approval: "write",
parameters: z.object({ kind: z.enum(["checkpoint", "final"]).default("checkpoint") }),
async execute(_id, params, signal, onUpdate, ctx) {
if (signal?.aborted) return textResult("Cancelled", undefined, true);
onUpdate?.({ content: [{ type: "text", text: "Building and verifying archive…" }] });
const result = await exportProject(runtime.get(), params.kind);
await appendMarkdownLog(runtime.get().projectRoot, "delivery_ledger.md", "Archive verified", [
`Archive: ${result.zipPath}`,
`SHA-256: ${result.verification.sha256}`,
`Members: ${result.verification.members.length}`,
`Verification: ${result.verificationPath}`,
].join("\n\n"));
pi.sendMessage({ customType: "pom:milestone", content: "Archive verified", display: true, details: { stage: runtime.get().currentStage, status: "ARCHIVE PASS", path: result.verificationPath } }, { triggerTurn: false });
await refresh(pi, runtime, ctx);
return textResult(`PASS · ${result.zipPath} · ${result.verification.members.length} members`, result);
},
});
}

35
src/ui.ts Normal file
View File

@@ -0,0 +1,35 @@
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
import type { PomConfig, PomState } from "./domain";
import { stage } from "./domain";
import type { PomRuntime } from "./runtime";
import { PomCommandCenter, PomHudComponent, type CommandCenterAction, type PomTheme } from "./visual";
import { openBlockers, stageProgress } from "./state";
export function updateChrome(ctx: ExtensionContext, state: PomState, config: PomConfig, runtime: PomRuntime): void {
if (!ctx.hasUI) return;
const spec = stage(state.currentStage);
const progress = stageProgress(state);
const blockers = openBlockers(state).length;
ctx.ui.setTitle(`POM · ${state.projectTitle} · ${spec.id}/8`);
ctx.ui.setStatus("pom-stage", `${spec.icon} ${spec.id}/8 ${spec.slug}`);
ctx.ui.setStatus("pom-progress", `${progress.percent}%`);
ctx.ui.setStatus("pom-gate", blockers ? `${blockers}` : `${state.stageRuns[String(spec.id)].evidence.length}/${spec.acceptance.length}`);
ctx.ui.setStatus("pom-hive", state.hive.activeMissionId ? `${state.hive.activeMissionId}` : undefined);
if (config.dashboardWidget && state.view !== "minimal") {
ctx.ui.setWidget("pom-hud", (_tui, theme) => new PomHudComponent(runtime, theme as unknown as PomTheme), { placement: config.hudPlacement });
} else {
ctx.ui.setWidget("pom-hud", undefined);
}
}
export function clearChrome(ctx: ExtensionContext): void {
if (!ctx.hasUI) return;
for (const key of ["pom-stage", "pom-progress", "pom-gate", "pom-hive", "pom-retry", "pom-approval", "pom-goal"]) ctx.ui.setStatus(key, undefined);
ctx.ui.setWidget("pom-hud", undefined);
ctx.ui.setWorkingMessage();
}
export async function openCommandCenter(ctx: ExtensionContext, state: PomState): Promise<CommandCenterAction> {
if (!ctx.hasUI) return "close";
return ctx.ui.custom<CommandCenterAction>((_tui, theme, keybindings, done) => new PomCommandCenter(state, theme as unknown as PomTheme, keybindings, done), { overlay: true });
}

220
src/validators.ts Normal file
View File

@@ -0,0 +1,220 @@
import { readdir, readFile, stat } from "node:fs/promises";
import { dirname, extname, join, relative, resolve } from "node:path";
import { stage, STAGES, type PomState, type ValidationScope } from "./domain";
import { atomicWrite, loadManifest, pathExists, sha256File } from "./persistence";
import { secureExistingPath } from "./paths";
export interface ValidationCheck {
id: string;
status: "pass" | "fail" | "warn";
message: string;
path?: string;
}
export interface ValidationReport {
schemaVersion: "1.0.0";
projectId: string;
scope: ValidationScope;
stage: number;
generatedAt: string;
passed: boolean;
checks: ValidationCheck[];
reportPath?: string;
}
function check(checks: ValidationCheck[], id: string, passed: boolean, message: string, path?: string): void {
checks.push({ id, status: passed ? "pass" : "fail", message, path });
}
function warn(checks: ValidationCheck[], id: string, message: string, path?: string): void {
checks.push({ id, status: "warn", message, path });
}
function countWords(text: string): number {
return text.trim() ? text.trim().split(/\s+/).length : 0;
}
async function validateState(state: PomState, checks: ValidationCheck[]): Promise<void> {
check(checks, "state.initialized", state.initialized, state.initialized ? "Project initialized" : "Project is not initialized");
check(checks, "state.stage-range", Number.isInteger(state.currentStage) && state.currentStage >= 0 && state.currentStage <= 8, `Current stage is ${state.currentStage}`);
const complete = new Set(state.completedStages);
const gaps = state.completedStages.filter((item) => STAGES.some((candidate) => candidate.id < item && !complete.has(candidate.id)));
check(checks, "state.sequential", gaps.length === 0, gaps.length ? `Completed-stage sequence has gaps before: ${gaps.join(", ")}` : "Completed stages are sequential");
const active = Object.entries(state.stageRuns).filter(([, run]) => run.status === "active");
check(checks, "state.single-active", active.length <= 1, active.length <= 1 ? "At most one stage is active" : `Multiple active stages: ${active.map(([id]) => id).join(", ")}`);
const open = Object.values(state.stageRuns).flatMap((run) => run.blockers).filter((blocker) => !blocker.resolvedAt);
if (open.length) warn(checks, "state.blockers", `${open.length} unresolved blocker(s) remain`);
else check(checks, "state.blockers", true, "No unresolved blockers");
}
async function validateArtifactRequirement(state: PomState, checks: ValidationCheck[], requirement: ReturnType<typeof stage>["artifacts"][number]): Promise<void> {
const path = requirement.path;
try {
const absolute = await secureExistingPath(state.projectRoot, path, { allowDirectory: requirement.kind === "directory" });
const fileStat = await stat(absolute);
if (requirement.kind === "directory") {
const entries = await readdir(absolute);
const visible = entries.filter((entry) => !entry.startsWith("."));
check(checks, `artifact.${path}.entries`, visible.length >= (requirement.minEntries ?? 1), `${path} contains ${visible.length} entries; requires ${requirement.minEntries ?? 1}`, path);
return;
}
check(checks, `artifact.${path}.bytes`, fileStat.size >= (requirement.minBytes ?? 1), `${path} is ${fileStat.size} bytes; requires ${requirement.minBytes ?? 1}`, path);
const text = await readFile(absolute, "utf8");
for (const heading of requirement.requiredHeadings ?? []) {
const found = new RegExp(`^#{1,6}\\s+.*${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*$`, "im").test(text);
check(checks, `artifact.${path}.heading.${heading}`, found, found ? `Heading found: ${heading}` : `Missing heading: ${heading}`, path);
}
for (const pattern of requirement.requiredPatterns ?? []) {
const found = new RegExp(pattern, "im").test(text);
check(checks, `artifact.${path}.pattern.${pattern}`, found, found ? `Required pattern found: ${pattern}` : `Missing required pattern: ${pattern}`, path);
}
for (const pattern of requirement.forbiddenPatterns ?? []) {
const found = new RegExp(pattern, "im").test(text);
check(checks, `artifact.${path}.forbidden.${pattern}`, !found, found ? `Forbidden placeholder found: ${pattern}` : `No forbidden placeholder: ${pattern}`, path);
}
check(checks, `artifact.${path}.words`, countWords(text) > 0, `${path} contains ${countWords(text)} words`, path);
} catch (error) {
check(checks, `artifact.${path}.exists`, false, error instanceof Error ? error.message : String(error), path);
}
}
async function validateStage(state: PomState, checks: ValidationCheck[]): Promise<void> {
const spec = stage(state.currentStage);
const run = state.stageRuns[String(spec.id)];
const missingPrerequisites = STAGES.filter((item) => item.id < spec.id && state.stageRuns[String(item.id)].status !== "complete").map((item) => item.id);
check(checks, "stage.prerequisites", missingPrerequisites.length === 0, missingPrerequisites.length ? `Incomplete prerequisite stages: ${missingPrerequisites.join(", ")}` : "All prerequisite stages are complete");
for (const artifact of spec.artifacts) await validateArtifactRequirement(state, checks, artifact);
for (const criterion of spec.acceptance) {
const evidence = run.evidence.find((item) => item.criterionId === criterion.id);
check(checks, `evidence.${criterion.id}`, Boolean(evidence), evidence ? `${criterion.label}: ${evidence.summary}` : `Missing evidence: ${criterion.label}`);
if (evidence) {
for (const evidencePath of evidence.paths) {
try {
await secureExistingPath(state.projectRoot, evidencePath, { allowDirectory: true });
check(checks, `evidence.${criterion.id}.${evidencePath}`, true, `Evidence path exists: ${evidencePath}`, evidencePath);
} catch (error) {
check(checks, `evidence.${criterion.id}.${evidencePath}`, false, error instanceof Error ? error.message : String(error), evidencePath);
}
}
}
}
const unresolved = run.blockers.filter((blocker) => !blocker.resolvedAt);
check(checks, "stage.blockers", unresolved.length === 0, unresolved.length ? `${unresolved.length} unresolved stage blocker(s)` : "No unresolved stage blockers");
}
async function validateManifest(state: PomState, checks: ValidationCheck[]): Promise<void> {
let records;
try {
records = await loadManifest(state);
} catch (error) {
check(checks, "manifest.schema", false, error instanceof Error ? error.message : String(error));
return;
}
check(checks, "manifest.nonempty", records.length > 0, records.length ? `${records.length} artifact(s) registered` : "Artifact manifest is empty");
for (const record of records) {
try {
const absolute = await secureExistingPath(state.projectRoot, record.path);
const fileStat = await stat(absolute);
check(checks, `manifest.${record.path}.bytes`, fileStat.size === record.bytes, fileStat.size === record.bytes ? `Size matches for ${record.path}` : `Size drift for ${record.path}: manifest ${record.bytes}, actual ${fileStat.size}`, record.path);
const digest = await sha256File(absolute);
check(checks, `manifest.${record.path}.sha256`, digest === record.sha256, digest === record.sha256 ? `Hash matches for ${record.path}` : `Hash drift for ${record.path}`, record.path);
if (record.status === "approved") check(checks, `manifest.${record.path}.rights`, record.rightsStatus !== "unknown", record.rightsStatus === "unknown" ? `Approved artifact has unknown rights: ${record.path}` : `Rights recorded: ${record.rightsStatus}`, record.path);
} catch (error) {
check(checks, `manifest.${record.path}.exists`, false, error instanceof Error ? error.message : String(error), record.path);
}
}
}
async function markdownFiles(root: string): Promise<string[]> {
const output: string[] = [];
async function walk(directory: string): Promise<void> {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) await walk(path);
else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") output.push(path);
}
}
if (await pathExists(root)) await walk(root);
return output;
}
async function validateKnowledge(state: PomState, checks: ValidationCheck[]): Promise<void> {
const contentRoot = join(state.projectRoot, "content");
const required = ["index.md", "production-board.md", "artifact-index.md", "01-planning.md", "02-story-bible.md", "03-manuscript.md", "04-visuals.md", "05-research.md", "06-ledgers.md", "07-delivery.md"];
for (const file of required) check(checks, `knowledge.${file}`, await pathExists(join(contentRoot, file)), `Knowledge page ${file} ${await pathExists(join(contentRoot, file)) ? "exists" : "is missing"}`, `content/${file}`);
for (const file of await markdownFiles(contentRoot)) {
const text = await readFile(file, "utf8");
check(checks, `knowledge.frontmatter.${relative(contentRoot, file)}`, text.startsWith("---\n"), `Frontmatter ${text.startsWith("---\n") ? "present" : "missing"}`, relative(state.projectRoot, file));
const links = [...text.matchAll(/\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g)].map((match) => match[1].trim());
for (const target of links) {
if (/^[a-z]+:\/\//i.test(target)) continue;
const base = target.startsWith("../") || target.startsWith("./") ? resolve(dirname(file), target) : resolve(contentRoot, target);
const candidates = [base, `${base}.md`, join(base, "index.md")];
const exists = (await Promise.all(candidates.map(pathExists))).some(Boolean);
check(checks, `knowledge.link.${relative(contentRoot, file)}.${target}`, exists, exists ? `Wikilink resolves: ${target}` : `Broken wikilink: ${target}`, relative(state.projectRoot, file));
}
}
}
async function validateContinuity(state: PomState, checks: ValidationCheck[]): Promise<void> {
const paths = ["02_story_bible/13_canon_ledger.md", "06_ledgers/continuity_ledger.md", "06_ledgers/timeline.md"];
for (const path of paths) {
const exists = await pathExists(join(state.projectRoot, path));
if (state.currentStage >= 5 || state.completedStages.includes(4)) check(checks, `continuity.${path}`, exists, exists ? `${path} exists` : `${path} is missing`, path);
else if (!exists) warn(checks, `continuity.${path}`, `${path} not required before story-bible completion`, path);
}
const ledger = join(state.projectRoot, "06_ledgers", "continuity_ledger.md");
if (await pathExists(ledger)) {
const text = await readFile(ledger, "utf8");
const openIssues = [...text.matchAll(/^- \[ \]/gm)].length;
check(checks, "continuity.open-issues", openIssues === 0, openIssues ? `${openIssues} open continuity issue(s)` : "No open continuity issues", "06_ledgers/continuity_ledger.md");
}
}
async function validateDelivery(state: PomState, checks: ValidationCheck[]): Promise<void> {
const incomplete = STAGES.filter((item) => item.id < 8 && state.stageRuns[String(item.id)].status !== "complete").map((item) => item.id);
check(checks, "delivery.prerequisites", incomplete.length === 0, incomplete.length ? `Incomplete stages: ${incomplete.join(", ")}` : "Stages 07 complete");
for (const artifact of stage(8).artifacts) await validateArtifactRequirement(state, checks, artifact);
const exportRoot = join(state.projectRoot, "07_exports");
let verificationFiles: string[] = [];
if (await pathExists(exportRoot)) verificationFiles = (await readdir(exportRoot)).filter((name) => name.endsWith(".verification.json"));
check(checks, "delivery.archive-verification", verificationFiles.length > 0, verificationFiles.length ? `Archive verification: ${verificationFiles.at(-1)}` : "No archive verification sidecar found", verificationFiles.at(-1) ? `07_exports/${verificationFiles.at(-1)}` : undefined);
}
function scopesFor(scope: ValidationScope): Set<string> {
if (scope === "quick") return new Set(["state"]);
if (scope === "stage") return new Set(["state", "stage"]);
if (scope === "canon") return new Set(["state", "stage", "continuity"]);
if (scope === "continuity") return new Set(["state", "continuity"]);
if (scope === "knowledge") return new Set(["state", "knowledge"]);
if (scope === "files") return new Set(["state", "manifest"]);
if (scope === "delivery") return new Set(["state", "manifest", "knowledge", "continuity", "delivery"]);
return new Set(["state", "stage", "manifest", "knowledge", "continuity", "delivery"]);
}
export async function validatePom(state: PomState, scope: ValidationScope, options: { writeReport?: boolean } = {}): Promise<ValidationReport> {
const checks: ValidationCheck[] = [];
const selected = scopesFor(scope);
if (selected.has("state")) await validateState(state, checks);
if (selected.has("stage")) await validateStage(state, checks);
if (selected.has("manifest")) await validateManifest(state, checks);
if (selected.has("knowledge")) await validateKnowledge(state, checks);
if (selected.has("continuity")) await validateContinuity(state, checks);
if (selected.has("delivery")) await validateDelivery(state, checks);
const report: ValidationReport = {
schemaVersion: "1.0.0",
projectId: state.projectId,
scope,
stage: state.currentStage,
generatedAt: new Date().toISOString(),
passed: checks.every((item) => item.status !== "fail"),
checks,
};
if (options.writeReport !== false && state.initialized) {
const stamp = report.generatedAt.replace(/[:.]/g, "-");
const relativePath = `00_admin/validation/stage-${state.currentStage}-${scope}-${stamp}.json`;
await atomicWrite(join(state.projectRoot, relativePath), `${JSON.stringify(report, null, 2)}\n`);
report.reportPath = relativePath;
}
return report;
}

193
src/visual.ts Normal file
View File

@@ -0,0 +1,193 @@
import { Text, truncateToWidth, visibleWidth, type Component, type KeybindingsManager } from "@oh-my-pi/pi-tui";
import type { PomMotion, PomState, PomView } from "./domain";
import { openBlockers, stageProgress } from "./state";
import { stage } from "./domain";
import type { PomRuntime } from "./runtime";
export type PomColor = "accent" | "muted" | "dim" | "success" | "warning" | "error" | "toolTitle" | "text";
export interface PomTheme {
fg(color: PomColor, text: string): string;
bold(text: string): string;
italic(text: string): string;
}
function safeLine(line: string, width: number): string {
return truncateToWidth(line.replaceAll("\t", " "), Math.max(1, width));
}
function bar(percent: number, width: number): string {
const size = Math.max(5, width);
const filled = Math.round((Math.max(0, Math.min(100, percent)) / 100) * size);
return `${"━".repeat(filled)}${"─".repeat(size - filled)}`;
}
function stageGlyph(status: string, active: boolean): string {
if (active) return "◆";
if (status === "complete") return "●";
if (status === "blocked") return "×";
if (status === "active") return "◉";
return "○";
}
function activityGlyph(state: PomState, runtime: PomRuntime): string {
if (!runtime.telemetry.activeTool) return state.stageRuns[String(state.currentStage)].status === "blocked" ? "×" : "✓";
if (state.motion === "off") return "◆";
return state.motion === "full" ? "◌" : "◐";
}
function hiveSummary(state: PomState, runtime: PomRuntime): string {
const latest = Object.values(state.hive.missions).at(-1);
const total = runtime.telemetry.activeAgents || latest?.tasks.length || 0;
const active = state.hive.activeMissionId ? `${runtime.telemetry.completedAgents}/${total}` : total ? `${runtime.telemetry.completedAgents}/${total}` : "idle";
return `Hive ${active}`;
}
export function hudLines(state: PomState, runtime: PomRuntime, theme: PomTheme, width: number): string[] {
const spec = stage(state.currentStage);
const run = state.stageRuns[String(state.currentStage)];
const progress = stageProgress(state);
const blockers = openBlockers(state).length;
const evidence = `${run.evidence.length}/${spec.acceptance.length}`;
const stageTrack = Object.entries(state.stageRuns).map(([id, item]) => stageGlyph(item.status, Number(id) === state.currentStage)).join(" ");
const tool = runtime.telemetry.activeTool
? `${activityGlyph(state, runtime)} ${runtime.telemetry.activeTool}${runtime.telemetry.activeIntent ? ` · ${runtime.telemetry.activeIntent}` : ""}`
: `${activityGlyph(state, runtime)} ${run.status}`;
const signal = runtime.telemetry.lastIrcSignal ? ` · IRC ${runtime.telemetry.lastIrcSignal}` : "";
if (state.view === "compact") {
return [safeLine(
`${theme.fg("accent", theme.bold("POM"))} ${spec.icon} ${spec.id}/8 ${spec.slug} · ${progress.percent}% · ◆ ${evidence} · ${blockers ? `${blockers}` : tool} · ${hiveSummary(state, runtime)}`,
width,
)];
}
const left = `${theme.fg("accent", theme.bold("POM"))} ${theme.fg("muted", `${spec.icon} ${spec.id}/8 ${spec.name}`)}`;
const right = `${progress.percent}% · ${evidence} evidence · ${blockers} blockers`;
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
const first = safeLine(`${left}${" ".repeat(gap)}${theme.fg(blockers ? "warning" : "dim", right)}`, width);
const activity = safeLine(`${theme.fg(runtime.telemetry.activeTool ? "warning" : "muted", tool)} · ${theme.fg("dim", hiveSummary(state, runtime))}${state.view === "cinematic" ? theme.fg("dim", signal) : ""}`, width);
if (state.view === "rich") {
return [first, safeLine(`${theme.fg("accent", bar(progress.percent, Math.max(10, Math.floor(width * 0.45))))} ${theme.fg("dim", stageTrack)} ${activity}`, width)];
}
return [
first,
safeLine(theme.fg("accent", bar(progress.percent, Math.max(10, width - 2))), width),
safeLine(`${theme.fg("dim", stageTrack)} ${activity}`, width),
];
}
export class PomHudComponent implements Component {
#cacheKey = "";
#cache: readonly string[] = [];
constructor(private readonly runtime: PomRuntime, private readonly theme: PomTheme) {}
render(width: number): readonly string[] {
const state = this.runtime.get();
const key = JSON.stringify({
width,
stage: state.currentStage,
view: state.view,
motion: state.motion,
runs: Object.values(state.stageRuns).map((run) => [run.status, run.evidence.length, run.blockers.filter((item) => !item.resolvedAt).length]),
telemetry: this.runtime.telemetry,
});
if (key === this.#cacheKey) return this.#cache;
this.#cacheKey = key;
this.#cache = hudLines(state, this.runtime, this.theme, width);
return this.#cache;
}
invalidate(): void { this.#cacheKey = ""; }
}
export type CommandCenterAction = "run" | "hive" | "validate" | "evidence" | "vault" | "export" | "theme" | "settings" | "close";
const ACTIONS: Array<{ id: CommandCenterAction; icon: string; label: string; detail: string }> = [
{ id: "run", icon: "▶", label: "Run active stage", detail: "Route model/tools and launch the authoritative stage loop" },
{ id: "hive", icon: "⬡", label: "Launch specialist hive", detail: "Prepare one native task batch with IRC ownership protocol" },
{ id: "validate", icon: "✓", label: "Validate production", detail: "Run deterministic gates and render an evidence card" },
{ id: "evidence", icon: "◆", label: "Record gate evidence", detail: "Attach a criterion to real artifact paths" },
{ id: "vault", icon: "◫", label: "Open vault dashboard", detail: "Insert the generated production board into the editor" },
{ id: "export", icon: "⇧", label: "Build verified archive", detail: "Create, decompress, CRC-check, and hash the delivery ZIP" },
{ id: "theme", icon: "◈", label: "Install POM themes", detail: "Copy Nocturne and Parchment into OMP custom themes" },
{ id: "settings", icon: "⚙", label: "Experience settings", detail: "Tune density, motion, routing, and HUD placement" },
{ id: "close", icon: "×", label: "Close", detail: "Return to the editor" },
];
export class PomCommandCenter implements Component {
#index = 0;
#cacheKey = "";
#cache: readonly string[] = [];
constructor(
private readonly state: PomState,
private readonly theme: PomTheme,
private readonly keybindings: KeybindingsManager,
private readonly done: (action: CommandCenterAction) => void,
) {}
handleInput(data: string): void {
if (this.keybindings.matches(data, "app.interrupt")) { this.done("close"); return; }
if (data === "k" || data === "\u001b[A") this.#index = (this.#index - 1 + ACTIONS.length) % ACTIONS.length;
else if (data === "j" || data === "\u001b[B") this.#index = (this.#index + 1) % ACTIONS.length;
else if (data === "\r" || data === "\n") { this.done(ACTIONS[this.#index].id); return; }
else if (data === "q" || data === "\u001b") { this.done("close"); return; }
this.invalidate();
}
render(width: number): readonly string[] {
const run = this.state.stageRuns[String(this.state.currentStage)];
const spec = stage(this.state.currentStage);
const progress = stageProgress(this.state);
const key = `${width}:${this.#index}:${this.state.updatedAt}`;
if (key === this.#cacheKey) return this.#cache;
const inner = Math.max(30, width - 4);
const top = `${"─".repeat(Math.max(1, inner))}`;
const bottom = `${"─".repeat(Math.max(1, inner))}`;
const line = (content = "") => {
const clipped = safeLine(content, inner - 2);
return `${clipped}${" ".repeat(Math.max(0, inner - 2 - visibleWidth(clipped)))}`;
};
const lines = [
top,
line(`${this.theme.fg("accent", this.theme.bold("POM COMMAND CENTER"))} ${this.theme.fg("dim", this.state.projectTitle)}`),
line(`${spec.icon} Stage ${spec.id}/8 · ${spec.name} · ${run.status.toUpperCase()}`),
line(`${this.theme.fg("accent", bar(progress.percent, Math.max(10, inner - 16)))} ${progress.percent}%`),
line(),
];
ACTIONS.forEach((action, index) => {
const selected = index === this.#index;
const cursor = selected ? this.theme.fg("accent", "") : " ";
const label = selected ? this.theme.bold(action.label) : action.label;
lines.push(line(`${cursor} ${action.icon} ${label}`));
if (selected && inner >= 60) lines.push(line(` ${this.theme.fg("dim", action.detail)}`));
});
lines.push(line(), line(this.theme.fg("dim", "↑/k ↓/j navigate · Enter select · q/Esc close")), bottom);
this.#cacheKey = key;
this.#cache = lines.map((item) => safeLine(item, width));
return this.#cache;
}
invalidate(): void { this.#cacheKey = ""; }
}
function spinnerFor(motion: PomMotion, frame = 0): string {
if (motion === "off") return "◆";
const frames = motion === "subtle" ? ["◐", "◑"] : ["◐", "◓", "◑", "◒"];
return frames[frame % frames.length];
}
export function toolCallComponent(label: string, action: string | undefined, theme: PomTheme, motion: PomMotion, spinnerFrame?: number): Component {
return new Text(`${theme.fg("accent", spinnerFor(motion, spinnerFrame))} ${theme.fg("toolTitle", theme.bold(`POM ${label}`))}${action ? ` ${theme.fg("muted", action)}` : ""}`, 0, 0);
}
export function toolResultComponent(
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
expanded: boolean,
partial: boolean,
theme: PomTheme,
view: PomView,
): Component {
if (partial) return new Text(`${theme.fg("warning", view === "minimal" ? "◌" : "◐")} ${theme.fg("muted", "Working…")}`, 0, 0);
const text = result.content.find((item) => item.type === "text")?.text ?? (result.isError ? "POM operation failed" : "Done");
const mark = result.isError ? theme.fg("error", "×") : theme.fg("success", "✓");
const allowDetails = expanded && result.details && view !== "minimal" && view !== "compact";
const detail = allowDetails ? `\n${theme.fg("dim", JSON.stringify(result.details, null, view === "cinematic" ? 2 : 0))}` : "";
return new Text(`${mark} ${text}${detail}`, 0, 0);
}

109
src/zip.ts Normal file
View File

@@ -0,0 +1,109 @@
import { createReadStream, createWriteStream } from "node:fs";
import { mkdir, stat } from "node:fs/promises";
import { dirname } from "node:path";
import { finished } from "node:stream/promises";
import CRC32 from "crc-32";
import yauzl, { type Entry, type ZipFile } from "yauzl";
import yazl from "yazl";
import { assertSafeArchivePath } from "./paths";
import { sha256File } from "./persistence";
export interface ZipMemberVerification {
path: string;
compressedBytes: number;
bytes: number;
crc32: string;
method: number;
}
export interface ZipVerification {
path: string;
bytes: number;
sha256: string;
members: ZipMemberVerification[];
}
export async function createZip(outputPath: string, members: Array<{ absolutePath: string; archivePath: string }>): Promise<void> {
if (!members.length) throw new Error("Refusing to create an empty archive");
const seen = new Set<string>();
const zip = new yazl.ZipFile();
for (const member of members) {
assertSafeArchivePath(member.archivePath);
if (seen.has(member.archivePath)) throw new Error(`Duplicate archive member: ${member.archivePath}`);
seen.add(member.archivePath);
zip.addFile(member.absolutePath, member.archivePath, { compress: true });
}
await mkdir(dirname(outputPath), { recursive: true });
const destination = createWriteStream(outputPath, { flags: "w" });
zip.outputStream.pipe(destination);
zip.end();
await finished(destination);
}
function openZip(path: string): Promise<ZipFile> {
return new Promise((resolvePromise, reject) => {
yauzl.open(path, { lazyEntries: true, decodeStrings: true, validateEntrySizes: true, strictFileNames: true }, (error, zip) => {
if (error || !zip) reject(error ?? new Error("Unable to open ZIP"));
else resolvePromise(zip);
});
});
}
function readEntry(zip: ZipFile, entry: Entry): Promise<ZipMemberVerification> {
return new Promise((resolvePromise, reject) => {
zip.openReadStream(entry, (error, stream) => {
if (error || !stream) { reject(error ?? new Error(`Unable to read ${entry.fileName}`)); return; }
let bytes = 0;
let crc = 0;
stream.on("data", (chunk: Buffer) => {
bytes += chunk.length;
crc = CRC32.buf(chunk, crc);
});
stream.once("error", reject);
stream.once("end", () => {
const unsigned = crc >>> 0;
const expected = entry.crc32 >>> 0;
if (bytes !== entry.uncompressedSize) { reject(new Error(`Size mismatch for ${entry.fileName}`)); return; }
if (unsigned !== expected) { reject(new Error(`CRC mismatch for ${entry.fileName}`)); return; }
resolvePromise({
path: entry.fileName,
compressedBytes: entry.compressedSize,
bytes,
crc32: unsigned.toString(16).padStart(8, "0"),
method: entry.compressionMethod,
});
});
});
});
}
export async function verifyZip(path: string): Promise<ZipVerification> {
const zip = await openZip(path);
const members: ZipMemberVerification[] = [];
const seen = new Set<string>();
await new Promise<void>((resolvePromise, reject) => {
zip.once("error", reject);
zip.once("end", resolvePromise);
zip.on("entry", (entry: Entry) => {
void (async () => {
try {
assertSafeArchivePath(entry.fileName.replace(/\/$/, ""));
if (/\/$/.test(entry.fileName)) { zip.readEntry(); return; }
if (seen.has(entry.fileName)) throw new Error(`Duplicate ZIP member: ${entry.fileName}`);
seen.add(entry.fileName);
if ((entry.generalPurposeBitFlag & 0x1) !== 0) throw new Error(`Encrypted ZIP member is not allowed: ${entry.fileName}`);
if (![0, 8].includes(entry.compressionMethod)) throw new Error(`Unsupported compression method ${entry.compressionMethod}: ${entry.fileName}`);
members.push(await readEntry(zip, entry));
zip.readEntry();
} catch (error) {
zip.close();
reject(error);
}
})();
});
zip.readEntry();
});
if (!members.length) throw new Error("ZIP contains no file members");
const fileStat = await stat(path);
return { path, bytes: fileStat.size, sha256: await sha256File(path), members };
}