From 2852432d449eef4006c64a4fa639ab2a65880937 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:43:00 +0800 Subject: [PATCH] fix: package goal worker for nested discovery Co-Authored-By: Pi/Codex <288921227+claudypoo@users.noreply.github.com> --- README.md | 2 +- agents/goal-worker.md | 21 +++++++++++++++++++++ package.json | 6 ++++++ src/supervisor-runtime.ts | 15 +-------------- src/worker.ts | 31 ------------------------------- test/package-agent.test.ts | 23 +++++++++++++++++++++++ test/prompts.test.ts | 8 +++++--- test/supervisor-runtime.test.ts | 13 ++----------- test/worker.test.ts | 21 --------------------- 9 files changed, 59 insertions(+), 81 deletions(-) create mode 100644 agents/goal-worker.md create mode 100644 test/package-agent.test.ts diff --git a/README.md b/README.md index 8669191..8d63dd3 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ continue until all goals close, the human uses `auto off`, or the plan is cleare ## Prompts -Planning and coordinator sign-off prompts live in [`src/prompts.ts`](src/prompts.ts). Runtime-agent registration and RPC calls live in [`src/worker.ts`](src/worker.ts). The supervisor-only nested-worker registration and approval tool live in [`src/supervisor-runtime.ts`](src/supervisor-runtime.ts). +Planning and coordinator sign-off prompts live in [`src/prompts.ts`](src/prompts.ts). Supervisor registration and RPC calls live in [`src/worker.ts`](src/worker.ts). The packaged worker definition lives in [`agents/goal-worker.md`](agents/goal-worker.md), and the supervisor-only approval tool lives in [`src/supervisor-runtime.ts`](src/supervisor-runtime.ts). ## Manual check diff --git a/agents/goal-worker.md b/agents/goal-worker.md new file mode 100644 index 0000000..59f22fd --- /dev/null +++ b/agents/goal-worker.md @@ -0,0 +1,21 @@ +--- +name: goal-worker +description: Implementation worker directed by the retained goal supervisor +thinking: high +systemPromptMode: replace +inheritProjectContext: true +inheritGlobalContext: true +inheritSkills: true +tools: read, grep, find, ls, bash, edit, write, contact_supervisor +defaultContext: fork +async: true +defaultProgress: true +--- + +You are the retained implementation worker for one goal supervisor. + +Work autonomously from the approved plan. Keep the plan current, run the real checks, commit the implementation, and leave specific evidence in its Log. The human's latest message outranks the plan; update affected goals instead of defending an obsolete decision. The main Pi agent is the research supervisor and owns direction and goal sign-off. + +Send `contact_supervisor` progress updates when evidence changes the research direction, when an hourly check asks for one, or when you need a decision. Do not claim a goal is complete; report the evidence and let the supervisor decide. Continue until the plan is complete or the human stops the session. + +-- Pi/Codex diff --git a/package.json b/package.json index e128dfa..177688a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "files": [ "src", + "agents", "README.md" ], "publishConfig": { @@ -54,6 +55,11 @@ "extensions": [ "./src/index.ts" ], + "subagents": { + "agents": [ + "./agents" + ] + }, "image": "https://cdn.jsdelivr.net/gh/wassname/pi-goals@main/media/screenshot.png" } } diff --git a/src/supervisor-runtime.ts b/src/supervisor-runtime.ts index 8ba10e3..b17f12f 100644 --- a/src/supervisor-runtime.ts +++ b/src/supervisor-runtime.ts @@ -4,7 +4,7 @@ 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 { registerGoalWorker, subagentWorkState } from "./worker.js"; +import { subagentWorkState } from "./worker.js"; const APPROVE_GOAL = "ApproveGoal"; @@ -13,19 +13,6 @@ function result(text: string, isError = false) { } export default function goalSupervisorRuntime(pi: ExtensionAPI): void { - let workerRegistration: { dispose(): void } | null = null; - - pi.on("session_start", async (_event, ctx) => { - workerRegistration?.dispose(); - workerRegistration = registerGoalWorker(pi.events, null); - ctx.ui.notify("Goal supervisor can now launch its retained worker.", "info"); - }); - - pi.on("session_shutdown", async () => { - workerRegistration?.dispose(); - workerRegistration = null; - }); - 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." }; diff --git a/src/worker.ts b/src/worker.ts index c3df1c0..a92c300 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -7,7 +7,6 @@ const RPC_REPLY_PREFIX = "subagents:rpc:v1:reply:"; const RPC_VERSION = 1; const RPC_TIMEOUT_MS = 15_000; export const SUPERVISOR_AGENT = "goal-supervisor"; -export const WORKER_AGENT = "goal-worker"; interface EventBus { on(event: string, handler: (data: unknown) => void): () => void; @@ -49,12 +48,6 @@ is clean, and you have explicitly inspected the plan, repository, evidence, and Otherwise return continue or redirect the worker. Do not claim acceptance in prose: only ApproveGoal creates the durable approval checkpoint. -- Pi/Codex`; -export const workerSystemPrompt = `You are the retained implementation worker for one goal supervisor. -Work autonomously from the approved plan. The latest human message outranks the plan. Keep the plan current, run the real checks, commit the implementation, -and leave specific evidence in its Log. Your goal supervisor owns direction and approval. Send contact_supervisor -progress updates when evidence changes the research direction, when an hourly check asks for one, or when you need a -decision. Do not claim a goal is complete; report the evidence and let the supervisor decide. -- Pi/Codex`; - export function registerGoalSupervisor(events: EventBus, model: string | null): Registration { const supervisorRuntime = fileURLToPath(new URL("./supervisor-runtime.ts", import.meta.url)); const request: Record = { @@ -82,30 +75,6 @@ export function registerGoalSupervisor(events: EventBus, model: string | null): return result.registration; } -export function registerGoalWorker(events: EventBus, model: string | null): Registration { - const request: Record = { - version: 1, - name: WORKER_AGENT, - definition: { - description: "Implementation worker directed by the retained goal supervisor.", - systemPrompt: workerSystemPrompt, - ...(model ? { model } : {}), - systemPromptMode: "replace", - inheritProjectContext: true, - inheritGlobalContext: true, - inheritSkills: true, - defaultContext: "fork", - defaultAsync: true, - defaultProgress: true, - }, - }; - events.emit(REGISTER_EVENT, request); - const result = request.result as { ok?: boolean; registration?: Registration; error?: Error } | undefined; - if (!result) throw new Error("pi-subagents is not installed or not ready."); - if (!result.ok || !result.registration) throw result.error ?? new Error("pi-subagents rejected the goal-worker agent."); - return result.registration; -} - async function rpc(events: EventBus, method: "spawn" | "resume" | "steer" | "status", params: Record, signal?: AbortSignal): Promise { if (signal?.aborted) throw new Error("Goal-worker request aborted."); const requestId = randomUUID(); diff --git a/test/package-agent.test.ts b/test/package-agent.test.ts new file mode 100644 index 0000000..852aec0 --- /dev/null +++ b/test/package-agent.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +interface PackageManifest { + files: string[]; + pi: { subagents: { agents: string[] } }; +} + +describe("packaged goal worker", () => { + it("exposes goal-worker through pi-subagents package discovery", () => { + const root = resolve(import.meta.dirname, ".."); + const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")) as PackageManifest; + expect(manifest.files).toContain("agents"); + expect(manifest.pi.subagents.agents).toEqual(["./agents"]); + + const definition = readFileSync(resolve(root, "agents", "goal-worker.md"), "utf8"); + expect(definition).toMatch(/^---\nname: goal-worker\n/); + expect(definition).toContain("tools: read, grep, find, ls, bash, edit, write, contact_supervisor"); + expect(definition).toContain("defaultContext: fork"); + expect(definition).toContain("retained implementation worker"); + }); +}); diff --git a/test/prompts.test.ts b/test/prompts.test.ts index 27708c5..26cedd1 100644 --- a/test/prompts.test.ts +++ b/test/prompts.test.ts @@ -1,6 +1,8 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { completeGoalDescription, planDrafting, planningState, resync } from "../src/prompts.js"; -import { workerSystemPrompt } from "../src/worker.js"; + +const workerSystemPrompt = readFileSync(new URL("../agents/goal-worker.md", import.meta.url), "utf8"); describe("planning prompt", () => { it("requires fact finding or a focused question before a goal", () => { @@ -24,8 +26,8 @@ describe("planning prompt", () => { expect(planDrafting).toContain("Take it from the original request, not from your implementation plan"); expect(planDrafting).toContain("Future work may not defer any artifact or action named there"); expect(resync("plan", ".pi/plan/test.md", "Compacted.")).toContain("amend the plan rather than preserving an obsolete decision"); - expect(workerSystemPrompt).toContain("latest human message outranks the plan"); - expect(workerSystemPrompt).toContain("goal supervisor owns direction and approval"); + expect(workerSystemPrompt).toContain("human's latest message outranks the plan"); + expect(workerSystemPrompt).toContain("main Pi agent is the research supervisor"); expect(completeGoalDescription).toContain("approval checkpoint only after it inspected"); }); }); diff --git a/test/supervisor-runtime.test.ts b/test/supervisor-runtime.test.ts index 18dad4a..3706496 100644 --- a/test/supervisor-runtime.test.ts +++ b/test/supervisor-runtime.test.ts @@ -30,12 +30,6 @@ function setup() { const hooks = new Map(); const tools = new Map(); const events = new Events(); - let workerDefinition: Record | undefined; - events.on("pi-subagents:runtime-agent-register:v1", (raw) => { - const request = raw as { definition: Record; result?: unknown }; - workerDefinition = request.definition; - request.result = { ok: true, registration: { dispose() {} } }; - }); events.on("subagents:rpc:v1:request", (raw) => { const request = raw as any; events.emit(`subagents:rpc:v1:reply:${request.requestId}`, { @@ -54,15 +48,13 @@ function setup() { registerTool: (tool: any) => tools.set(tool.name, tool), }; supervisorRuntime(pi as any); - return { cwd, ctx, hooks, tools, workerDefinition: () => workerDefinition }; + return { cwd, ctx, hooks, tools }; } describe("supervisor-only runtime", () => { - it("registers the nested worker and blocks direct supervisor writes", async () => { + it("blocks direct supervisor writes", async () => { const runtime = setup(); try { - await runtime.hooks.get("session_start")({}, runtime.ctx); - expect(runtime.workerDefinition()?.description).toContain("Implementation worker"); expect((await runtime.hooks.get("tool_call")({ toolName: "edit", input: { path: "README.md" } }, runtime.ctx))?.block).toBe(true); expect((await runtime.hooks.get("tool_call")({ toolName: "bash", input: { command: "git status && npm test" } }, runtime.ctx))).toBeUndefined(); } finally { @@ -73,7 +65,6 @@ describe("supervisor-only runtime", () => { it("writes an approval only after inspecting the plan and confirming a clean worktree at a commit", async () => { const runtime = setup(); try { - await runtime.hooks.get("session_start")({}, runtime.ctx); const planPath = join(runtime.cwd, ".pi/plan/session-a-v1.md"); mkdirSync(join(runtime.cwd, ".pi/plan"), { recursive: true }); writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: ship it\n - evidence: verify.log: PASS\n"); diff --git a/test/worker.test.ts b/test/worker.test.ts index 6e160e8..d6c4305 100644 --- a/test/worker.test.ts +++ b/test/worker.test.ts @@ -2,13 +2,11 @@ import { describe, expect, it } from "vitest"; import { processWorkState, registerGoalSupervisor, - registerGoalWorker, resumeGoalSupervisor, startGoalSupervisor, steerGoalSupervisor, subagentWorkState, supervisorSystemPrompt, - workerSystemPrompt, } from "../src/worker.js"; class Events { @@ -52,25 +50,6 @@ describe("goal hierarchy registration", () => { expect(definition?.subagentOnlyExtensions).toEqual([expect.stringContaining("supervisor-runtime.ts")]); expect(supervisorSystemPrompt).toContain("nested goal-worker"); expect(supervisorSystemPrompt).toContain("ApproveGoal"); - expect(workerSystemPrompt).toContain("retained implementation worker"); - }); - - it("registers the implementation worker without nested supervisor tools", () => { - const events = new Events(); - let definition: Record | undefined; - events.on("pi-subagents:runtime-agent-register:v1", (raw) => { - const request = raw as { definition: Record; result?: unknown }; - definition = request.definition; - request.result = { ok: true, registration: { dispose() {} } }; - }); - - registerGoalWorker(events, null); - expect(definition?.allowNestedSubagents).toBeUndefined(); - expect(definition).not.toHaveProperty("subagentOnlyExtensions"); - }); - - it("fails clearly when pi-subagents is absent", () => { - expect(() => registerGoalWorker(new Events(), null)).toThrow("pi-subagents is not installed or not ready"); }); });