Files
pom-omp/src/tools.ts

328 lines
17 KiB
TypeScript

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);
},
});
}