/** * Homer control-plane extension for Pi Coding Agent. * Wraps the `@pelec/homer` CLI — does not reimplement freeze/claim/gate semantics. * * Tools: homer_version, homer_status, homer_init, homer_freeze, homer_claim, * homer_release, homer_validate, homer_gate */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { formatCliResult, runHomerCli } from "./lib/run-homer.ts"; const RootOpts = { root: Type.Optional( Type.String({ description: "Project root containing or to create .homer/ (maps to --root)", }), ), json: Type.Optional( Type.Boolean({ description: "Request JSON CLI output when supported (--json)", }), ), }; function globalFlags(params: { root?: string; json?: boolean }): string[] { const args: string[] = []; if (params.root) args.push("--root", params.root); if (params.json) args.push("--json"); return args; } async function runTool( args: string[], cwd?: string, ): Promise<{ content: [{ type: "text"; text: string }]; details: unknown }> { try { const result = await runHomerCli(args, { cwd }); return { content: [{ type: "text", text: formatCliResult(result) }], details: { exitCode: result.exitCode, argv: result.argv, stdout: result.stdout, stderr: result.stderr, }, }; } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { content: [{ type: "text", text: `homer invocation failed: ${msg}` }], details: { error: msg }, }; } } export default function homerExtension(pi: ExtensionAPI): void { pi.registerTool({ name: "homer_version", label: "Homer version", description: "Print Homer CLI version (homer version).", parameters: Type.Object({ ...RootOpts, }), async execute(_id, params) { return runTool([...globalFlags(params), "version"]); }, }); pi.registerTool({ name: "homer_status", label: "Homer status", description: "Show frozen contract version and active claims (homer status). Use after freeze/claim/gate.", parameters: Type.Object({ ...RootOpts, }), async execute(_id, params) { return runTool([...globalFlags(params), "status"]); }, }); pi.registerTool({ name: "homer_init", label: "Homer init", description: "Initialize .homer board (and optional host skill trees). Default hosts=agents. Does not replace this Pi package skills.", parameters: Type.Object({ ...RootOpts, hosts: Type.Optional( Type.String({ description: "Comma-separated hosts: agents,grok,kimi (default agents)", }), ), force: Type.Optional( Type.Boolean({ description: "Overwrite existing draft/skills when allowed" }), ), }), async execute(_id, params) { const args = [...globalFlags(params), "init"]; if (params.hosts) args.push("--hosts", params.hosts); if (params.force) args.push("--force"); return runTool(args); }, }); pi.registerTool({ name: "homer_freeze", label: "Homer freeze", description: "Freeze draft L0/L1 after human L0 approval. NEVER call until the human explicitly approved .homer/contracts/_draft/L0.md.", parameters: Type.Object({ ...RootOpts, approved_by: Type.Optional( Type.String({ description: "Recorded approver name (default CLI human)", }), ), }), async execute(_id, params) { const args = [...globalFlags(params), "freeze"]; if (params.approved_by) args.push("--approved-by", params.approved_by); return runTool(args); }, }); pi.registerTool({ name: "homer_claim", label: "Homer claim", description: "Lease path globs for an implementer (homer claim). Paths must be disjoint across concurrent active claims.", parameters: Type.Object({ ...RootOpts, paths: Type.String({ description: "Comma-separated glob paths relative to project root", }), owner: Type.Optional(Type.String({ description: "Claim owner label" })), id: Type.Optional(Type.String({ description: "Optional claim id" })), }), async execute(_id, params) { const args = [...globalFlags(params), "claim", "--paths", params.paths]; if (params.owner) args.push("--owner", params.owner); if (params.id) args.push("--id", params.id); return runTool(args); }, }); pi.registerTool({ name: "homer_release", label: "Homer release", description: "Release an active claim lease (homer release ).", parameters: Type.Object({ ...RootOpts, claim_id: Type.String({ description: "Claim id to release" }), }), async execute(_id, params) { return runTool([...globalFlags(params), "release", params.claim_id]); }, }); pi.registerTool({ name: "homer_validate", label: "Homer validate", description: "Validate board artifacts against schemas (homer validate).", parameters: Type.Object({ ...RootOpts, }), async execute(_id, params) { return runTool([...globalFlags(params), "validate"]); }, }); pi.registerTool({ name: "homer_gate", label: "Homer gate", description: "Machine-check evidence for a claim (homer gate --claim ). Requires .homer/evidence//return.json. Do not treat chat green as done.", parameters: Type.Object({ ...RootOpts, claim_id: Type.String({ description: "Claim id to gate" }), allow_open_questions: Type.Optional( Type.Boolean({ description: "Only if human explicitly accepts residual open_questions", }), ), }), async execute(_id, params) { const args = [...globalFlags(params), "gate", "--claim", params.claim_id]; if (params.allow_open_questions) args.push("--allow-open-questions"); return runTool(args); }, }); pi.registerCommand("homer-status", { description: "Show Homer board status (frozen contract + claims)", handler: async (_args, ctx) => { const result = await runHomerCli(["status"], { cwd: ctx.cwd, }).catch((e) => ({ exitCode: 1, stdout: "", stderr: e instanceof Error ? e.message : String(e), argv: ["homer", "status"], })); const text = formatCliResult(result); if (ctx.ui?.notify) { ctx.ui.notify(text.slice(0, 500), result.exitCode === 0 ? "info" : "error"); } else { console.log(text); } }, }); }