mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
Co-Authored-By: PI[gpt-5.6-sol] <288921227+claudypoo@users.noreply.github.com>
180 lines
8.1 KiB
TypeScript
180 lines
8.1 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
import { Type } from "typebox";
|
|
import { goalBlock, hashGoalBlock, repositoryState, writeApproval } from "./approval.js";
|
|
import { isSupervisorReadOnlyCommand } from "./index.js";
|
|
import { GOAL_WORKER_AGENT, processWorkState } from "./worker.js";
|
|
|
|
const COMPACTED_STATE = "pi-goals-supervisor-compacted";
|
|
|
|
function result(text: string, isError = false) {
|
|
return { content: [{ type: "text" as const, text }], details: {}, isError };
|
|
}
|
|
|
|
interface GoalBindings {
|
|
compactPlanning?: boolean;
|
|
workerModel?: string | null;
|
|
}
|
|
|
|
function goalBindings(): GoalBindings {
|
|
const raw = process.env.PI_SUBAGENT_EXTENSION_BINDINGS;
|
|
if (!raw) return {};
|
|
const binding = (JSON.parse(raw) as { "pi-goals/1"?: GoalBindings })["pi-goals/1"] ?? {};
|
|
if (binding.workerModel !== undefined && binding.workerModel !== null && typeof binding.workerModel !== "string") throw new Error("pi-goals workerModel binding must be a string or null.");
|
|
return binding;
|
|
}
|
|
|
|
function messageLaunchesWorker(ctx: { sessionManager: { getBranch(): unknown[] } }): boolean {
|
|
const entry = [...ctx.sessionManager.getBranch()].reverse().find((candidate) => {
|
|
const value = candidate as { type?: unknown; message?: { role?: unknown } };
|
|
return value.type === "message" && value.message?.role === "assistant";
|
|
}) as { message?: { content?: unknown } } | undefined;
|
|
if (!Array.isArray(entry?.message?.content)) return false;
|
|
return entry.message.content.some((part) => {
|
|
const value = part as { type?: unknown; name?: unknown; arguments?: Record<string, unknown> };
|
|
return value.type === "toolCall" && value.name === "subagent" && value.arguments?.agent === GOAL_WORKER_AGENT;
|
|
});
|
|
}
|
|
|
|
export default function goalSupervisorRuntime(pi: ExtensionAPI): void {
|
|
let compacting = false;
|
|
let compactionDone = Promise.resolve();
|
|
let currentTurn = -1;
|
|
let completedWorkerTurn: number | null = null;
|
|
let workerModel: string | null = null;
|
|
const activeWorkerCalls = new Set<string>();
|
|
|
|
pi.on("session_before_compact", async (event) => {
|
|
if (!compacting) return;
|
|
const branchEntries = event.branchEntries as Array<{ id?: string; type?: string; message?: { role?: string } }>;
|
|
const latestMessage = [...branchEntries].reverse().find((entry) => entry.type === "message" && ["user", "assistant"].includes(entry.message?.role ?? ""));
|
|
return {
|
|
compaction: {
|
|
summary: "Planning is complete. The latest retained goal-supervisor task contains the current plan and approval paths; use it as the source of truth. -- PI[gpt-5.6-sol]",
|
|
firstKeptEntryId: latestMessage?.id ?? event.preparation.firstKeptEntryId,
|
|
tokensBefore: event.preparation.tokensBefore,
|
|
details: { source: "pi-goals-plan-handoff" },
|
|
},
|
|
};
|
|
});
|
|
|
|
pi.on("session_start", async (_event, ctx) => {
|
|
const entries = ctx.sessionManager.getEntries();
|
|
const bindings = goalBindings();
|
|
workerModel = bindings.workerModel ?? null;
|
|
if (bindings.compactPlanning !== true) return;
|
|
if (entries.some((entry: { type?: string; customType?: string }) => entry.type === "custom" && entry.customType === COMPACTED_STATE)) return;
|
|
compacting = true;
|
|
compactionDone = new Promise<void>((resolvePromise, reject) => {
|
|
ctx.compact({
|
|
onComplete: () => {
|
|
compacting = false;
|
|
pi.appendEntry(COMPACTED_STATE, { version: 1 });
|
|
resolvePromise();
|
|
},
|
|
onError: (error) => {
|
|
compacting = false;
|
|
reject(error);
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
pi.on("before_agent_start", async () => {
|
|
await compactionDone;
|
|
});
|
|
|
|
pi.on("turn_start", async (event) => {
|
|
activeWorkerCalls.clear();
|
|
currentTurn = event.turnIndex;
|
|
});
|
|
|
|
pi.on("tool_call", async (event) => {
|
|
if (event.toolName === "edit" || event.toolName === "write") {
|
|
return { block: true, reason: "Goal supervision is read-only. Direct project changes to the nested goal-worker." };
|
|
}
|
|
if (event.toolName === "bash" && !isSupervisorReadOnlyCommand(String((event.input as { command?: string }).command))) {
|
|
return { block: true, reason: "Goal supervision allows inspection and standard verification commands only." };
|
|
}
|
|
if (event.toolName !== "subagent") return;
|
|
const input = event.input as Record<string, unknown>;
|
|
const allowedKeys = new Set(["agent", "task", "async", "context", ...(workerModel ? ["model"] : [])]);
|
|
const unexpectedKeys = Object.keys(input).filter((key) => !allowedKeys.has(key));
|
|
const validWorker = input.agent === GOAL_WORKER_AGENT
|
|
&& typeof input.task === "string"
|
|
&& input.task.trim().length > 0
|
|
&& input.async === false
|
|
&& input.context === "fork"
|
|
&& (workerModel ? input.model === workerModel : input.model === undefined)
|
|
&& unexpectedKeys.length === 0;
|
|
if (!validWorker) {
|
|
const model = workerModel ? `, model:${JSON.stringify(workerModel)}` : "";
|
|
return { block: true, reason: `Launch only ${GOAL_WORKER_AGENT} with task, async:false, context:"fork"${model}, and no other fields.` };
|
|
}
|
|
if (activeWorkerCalls.size > 0) return { block: true, reason: "A foreground goal-worker is already running." };
|
|
activeWorkerCalls.add(event.toolCallId);
|
|
completedWorkerTurn = null;
|
|
});
|
|
|
|
pi.on("tool_result", async (event) => {
|
|
if (!activeWorkerCalls.delete(event.toolCallId)) return;
|
|
if (!event.isError) completedWorkerTurn = currentTurn;
|
|
});
|
|
|
|
pi.registerTool({
|
|
name: "ApproveGoal",
|
|
label: "Approve goal",
|
|
executionMode: "sequential",
|
|
description: "Record approval after inspecting the plan, repository, evidence, and saved verification output. Active or unknown work blocks approval.",
|
|
parameters: Type.Object({
|
|
approvalId: Type.String({ minLength: 1, description: "Exact approval ID from the latest main-coordinator direction." }),
|
|
goal: Type.String({ description: "Exact current goal text from the approved plan." }),
|
|
planPath: Type.String({ description: "Absolute path to the current plan file." }),
|
|
checkpointPath: Type.String({ description: "Exact private approval-record path supplied by the main coordinator." }),
|
|
inspectedPlan: Type.Literal(true),
|
|
inspectedRepository: Type.Literal(true),
|
|
inspectedEvidence: Type.Literal(true),
|
|
inspectedVerifyOutput: Type.Literal(true),
|
|
}),
|
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
if (messageLaunchesWorker(ctx) || activeWorkerCalls.size > 0 || completedWorkerTurn === null || completedWorkerTurn >= currentTurn) {
|
|
return result("Cannot approve in a worker-launch message or before reviewing a finished worker on a later turn.", true);
|
|
}
|
|
const processes = processWorkState(pi.events);
|
|
if (processes !== "idle") return result(`Cannot approve: processes=${processes}.`, true);
|
|
const planPath = resolve(params.planPath);
|
|
let plan: string;
|
|
let repository: ReturnType<typeof repositoryState>;
|
|
try {
|
|
plan = readFileSync(planPath, "utf8");
|
|
repository = repositoryState(ctx.cwd);
|
|
} catch (error) {
|
|
return result(`Cannot inspect approval inputs: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
}
|
|
if (!repository.cleanWorktree) return result("Cannot approve with a dirty worktree. Commit the worker changes first.", true);
|
|
const block = goalBlock(plan, params.goal);
|
|
if (!block) return result(`Cannot approve: no unique open goal matches "${params.goal}".`, true);
|
|
const path = resolve(params.checkpointPath);
|
|
const approvalRoot = resolve(ctx.cwd, ".pi", "pi-goals", "approvals");
|
|
if (!path.startsWith(`${approvalRoot}/`)) return result("Approval checkpoint must stay in private .pi/pi-goals/approvals state.", true);
|
|
writeApproval(path, {
|
|
version: 2,
|
|
verdict: "accept",
|
|
approvalId: params.approvalId,
|
|
goal: params.goal,
|
|
planPath,
|
|
goalBlockHash: hashGoalBlock(block),
|
|
repoRoot: repository.repoRoot,
|
|
head: repository.head,
|
|
tree: repository.tree,
|
|
cleanWorktree: true,
|
|
inspected: { plan: true, repository: true, evidence: true, verifyOutput: true },
|
|
supervisor: { sessionId: ctx.sessionManager.getSessionId(), runId: process.env.PI_SUBAGENT_RUN_ID ?? null },
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return result(`Approval recorded at ${path} for "${params.goal}".`);
|
|
},
|
|
});
|
|
}
|