Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/the-system/board-runtime.ts

Raw
import { StringEnum } from "@earendil-works/pi-ai";
import { defineTool, keyHint } from "@earendil-works/pi-coding-agent";
import { Text } from "@earendil-works/pi-tui";
import { Type } from "typebox";
import { BOARD_NOTE_LIMIT } from "./board-constants.ts";
import type { BoardServer } from "./board-server.ts";
import type { BoardProposal } from "./board-state.ts";
import {
	loadBoardServer,
	loadBoardState,
	loadOpenBrowser,
	loadProjectIndex,
	loadSystemRenderer,
} from "./loaders.js";
import { requireProjectRoot } from "./project-root.ts";
import { expandableFallback } from "./tool-rendering.ts";

const STATES = ["todo", "done", "open"] as const;
const proposalParameters = Type.Object({
	items: Type.Array(
		Type.Object({
			id: Type.String({ description: "Existing spec, plan, or issue ID." }),
			state: StringEnum(STATES, {
				description: "Proposed board state for the item type.",
			}),
			rationale: Type.String({
				minLength: 1,
				maxLength: 1000,
				description: "Terse evidence supporting this proposed state.",
			}),
		}),
		{ minItems: 0, maxItems: 100 },
	),
});
const updateParameters = Type.Object({
	items: Type.Array(
		Type.Object({
			id: Type.String({ description: "Existing spec, plan, or issue ID." }),
			state: StringEnum(STATES, {
				description: "Board state for the item type.",
			}),
			note: Type.String({
				minLength: 1,
				maxLength: BOARD_NOTE_LIMIT,
				description: "Terse verified evidence stored with the board entry.",
			}),
		}),
		{ minItems: 0, maxItems: 100 },
	),
});

interface CallTheme {
	fg(style: string, text: string): string;
	bold(text: string): string;
}

function renderBoardItems(
	name: string,
	args: { items?: unknown },
	theme: CallTheme,
	context: { expanded: boolean; lastComponent?: unknown },
): Text {
	const text =
		(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
	const items = Array.isArray(args.items)
		? (args.items as Record<string, unknown>[])
		: [];
	const count = items.length;
	let output = `${theme.fg("toolTitle", theme.bold(name))} ${theme.fg("toolOutput", `${count} item${count === 1 ? "" : "s"}`)}`;
	if (!context.expanded && count > 0) {
		output += theme.fg("dim", ` (${keyHint("app.tools.expand", "to expand")})`);
	} else {
		for (const item of items) {
			output += `\n${theme.fg("accent", String(item.state).toUpperCase())} ${theme.fg("toolOutput", String(item.id))}`;
			const evidence = item.rationale ?? item.note;
			if (evidence) output += `\n${theme.fg("dim", String(evidence))}`;
		}
	}
	text.setText(output);
	return text;
}

const renderBoardToolResult = (
	result: Readonly<{
		content: Parameters<typeof expandableFallback>[0];
		details?: unknown;
	}>,
	expanded: boolean,
	theme: CallTheme,
	isError: boolean,
	lastComponent: unknown,
	messages: Readonly<{ empty: string; summary: (count: number) => string }>,
): Text => {
	const text = (lastComponent as Text | undefined) ?? new Text("", 0, 0);
	const items = (result.details as { items?: BoardProposal[] } | undefined)
		?.items;
	if (!items) {
		text.setText(
			expandableFallback(
				result.content,
				expanded,
				theme,
				isError ? "error" : "muted",
			),
		);
		return text;
	}
	text.setText(
		theme.fg(
			"success",
			items.length === 0 ? messages.empty : messages.summary(items.length),
		),
	);
	return text;
};

export function createBoardRuntime() {
	let proposalRoot: string | undefined;
	let proposalFinished = false;
	let updateRoot: string | undefined;
	let updateFinished = false;
	const proposals = new Map<string, Map<string, BoardProposal>>();
	const servers = new Set<BoardServer>();
	const loadBoard = async (cwd: string) => {
		const root = requireProjectRoot(cwd);
		const [projectIndex, boardState] = await Promise.all([
			loadProjectIndex(),
			loadBoardState(),
		]);
		const index = projectIndex.loadProjectIndex(root);
		const state = boardState.readBoardState(root, index.prefix);
		return { boardState, index, root, state };
	};
	const tool = defineTool({
		name: "system_board_propose",
		label: "system_board_propose",
		description:
			"Record evidence-backed proposed board states for human review. Accepts existing spec, plan, or issue IDs and does not modify .system/board.json.",
		promptSnippet: "Propose System board states for human review",
		promptGuidelines: [
			"During /system assess, call system_board_propose exactly once with evidence-backed state changes; use an empty items list when no changes are justified.",
		],
		parameters: proposalParameters,
		renderCall(args, theme, context) {
			return renderBoardItems("system_board_propose", args, theme, context);
		},
		renderResult(result, { expanded }, theme, context) {
			return renderBoardToolResult(
				result,
				expanded,
				theme,
				Boolean(context.isError),
				context.lastComponent,
				{
					empty: "✓ no changes proposed",
					summary: (count) =>
						`✓ ${count} change${count === 1 ? "" : "s"} proposed`,
				},
			);
		},
		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			const root = requireProjectRoot(ctx.cwd);
			if (root !== proposalRoot)
				throw new Error(
					"system_board_propose is only available during /system assess or /system exec",
				);
			const { index } = await loadBoard(root);
			if (root !== proposalRoot)
				throw new Error(
					"system_board_propose is only available during /system assess or /system exec",
				);
			const byId = new Map(
				index.documents.map((document) => [document.metadata.id, document]),
			);
			const proposed = proposals.get(root) ?? new Map<string, BoardProposal>();
			for (const raw of params.items) {
				const document = byId.get(raw.id);
				if (!document) throw new Error(`No System board item: ${raw.id}`);
				const valid =
					document.metadata.type === "issue"
						? raw.state === "open" || raw.state === "done"
						: (document.metadata.type === "spec" ||
								document.metadata.type === "plan") &&
							(raw.state === "todo" || raw.state === "done");
				if (!valid)
					throw new Error(
						`Invalid ${document.metadata.type} board state ${raw.state}: ${raw.id}`,
					);
				proposed.set(raw.id, {
					id: raw.id,
					state: raw.state,
					rationale: raw.rationale.trim(),
				});
			}
			proposals.set(root, proposed);
			proposalFinished = true;
			const items = [...proposed.values()].sort((left, right) =>
				left.id.localeCompare(right.id),
			);
			return {
				content: [
					{
						type: "text" as const,
						text: `${params.items.length} board state proposal(s) recorded for human review. Run /system board to inspect and apply them.`,
					},
				],
				details: { items },
			};
		},
	});

	const updateTool = defineTool({
		name: "system_board_update",
		label: "system_board_update",
		description:
			"Apply verified board states to .system/board.json and record their source as agent. Accepts existing spec, plan, or issue IDs and rejects states that violate board invariants.",
		promptSnippet: "Update System board states from verified work",
		promptGuidelines: [
			"After fresh verification in /system exec, call system_board_update exactly once with the states your work proves; use an empty items list when no change is justified.",
		],
		parameters: updateParameters,
		renderCall(args, theme, context) {
			return renderBoardItems("system_board_update", args, theme, context);
		},
		renderResult(result, { expanded }, theme, context) {
			return renderBoardToolResult(
				result,
				expanded,
				theme,
				Boolean(context.isError),
				context.lastComponent,
				{
					empty: "✓ no board changes",
					summary: (count) =>
						`✓ ${count} board change${count === 1 ? "" : "s"} applied`,
				},
			);
		},
		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			const root = requireProjectRoot(ctx.cwd);
			if (root !== updateRoot)
				throw new Error(
					"system_board_update is only available during /system exec",
				);
			const loaded = await loadBoard(root);
			if (root !== updateRoot)
				throw new Error(
					"system_board_update is only available during /system exec",
				);
			let state = loaded.state;
			const updatedAt = new Date().toISOString();
			const applied: BoardProposal[] = [];
			for (const raw of params.items) {
				state = loaded.boardState.updateBoardState(
					state,
					loaded.index.documents,
					raw.id,
					raw.state,
					"agent",
					updatedAt,
					raw.note,
				);
				applied.push({
					id: raw.id,
					state: raw.state,
					rationale: raw.note.trim(),
				});
			}
			if (applied.length > 0) loaded.boardState.writeBoardState(root, state);
			updateFinished = true;
			const items = [...applied].sort((left, right) =>
				left.id.localeCompare(right.id),
			);
			return {
				content: [
					{
						type: "text" as const,
						text:
							applied.length === 0
								? "No board states changed."
								: `${applied.length} board state(s) applied. Run /system board to review or override them.`,
					},
				],
				details: { items },
			};
		},
	});

	return {
		tool,
		updateTool,
		async beginUpdate(cwd: string) {
			const { root } = await loadBoard(cwd);
			updateRoot = root;
			updateFinished = false;
		},
		updateComplete() {
			return updateRoot !== undefined && updateFinished;
		},
		endUpdate() {
			updateRoot = undefined;
			updateFinished = false;
		},
		async beginProposal(cwd: string) {
			const { root } = await loadBoard(cwd);
			proposalRoot = root;
			proposalFinished = false;
			proposals.set(root, new Map());
		},
		proposalComplete() {
			return proposalRoot !== undefined && proposalFinished;
		},
		endProposal() {
			proposalRoot = undefined;
			proposalFinished = false;
		},
		async open(cwd: string): Promise<string> {
			const { index } = await loadBoard(cwd);
			const [{ renderSystemBoardSite }, { startBoardServer }, { openUrl }] =
				await Promise.all([
					loadSystemRenderer(),
					loadBoardServer(),
					loadOpenBrowser(),
				]);
			const current = proposals.get(index.root) ?? new Map();
			proposals.set(index.root, current);
			const server = await startBoardServer(
				index.root,
				current,
				(site, state, pending, revision) =>
					renderSystemBoardSite(site, state, pending, revision),
			);
			servers.add(server);
			const opened = await openUrl(server.url);
			return opened.ok
				? `System board opened: ${server.url}\nBrowser: ${opened.command}`
				: `System board running: ${server.url}\nBrowser: ${opened.error}`;
		},
		shutdown() {
			proposalRoot = undefined;
			proposalFinished = false;
			updateRoot = undefined;
			updateFinished = false;
			for (const server of servers) server.close();
			servers.clear();
		},
	};
}