chore: initialize POM 2 repo (docs guards green; release gates recorded NOT RUN pending install)
This commit is contained in:
327
src/tools.ts
Normal file
327
src/tools.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user