mirror of
https://github.com/wassname/pi-plan.git
synced 2026-09-26 14:10:23 +08:00
- One goals file per /goals new, planning is read-only except that file, Ready starts work. - Ready creates a session-scoped pi-scheduler prompt task with an owned marker. Each owned wake is transformed into the user's Loop statement plus the current goals read from disk. Wakes from an older Ready or another session are dropped. Pause, clear or no remaining goals remove the task. - Whole goals file is re-sent once after compaction or resume. - CompleteGoal delegates to a fresh read-only pi-subagents runtime agent. Accept with quoted checks marks [✓]. Reject, malformed output or judge failure leaves the goal open (the old version accepted on judge failure). /goals judge off records [x] self-verification. - Tests: fake Pi harness plus contract tests against real scheduler core and pi-subagents parsers. Mutation check: 7 deliberate bugs each failed at least one test. - Removes the old subprocess judge, private auto-continue timer, historical docs and scripts. Co-Authored-By: Claude <288921227+claudypoo@users.noreply.github.com>
110 lines
5.1 KiB
TypeScript
110 lines
5.1 KiB
TypeScript
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { describe, expect, it } from "vitest";
|
|
import { GOALS, LOOP, setup } from "./harness.js";
|
|
|
|
async function ready(h: ReturnType<typeof setup>) {
|
|
await h.commands.get("goals").handler("new plot the data", h.ctx);
|
|
const state = h.branch.findLast(e => e.customType === "pi-goals-single-agent").data;
|
|
writeFileSync(state.file, GOALS);
|
|
await h.tools.get("RequestPlanReview").execute();
|
|
await h.hook("agent_settled");
|
|
return state.file as string;
|
|
}
|
|
|
|
describe("planning and Ready", () => {
|
|
it("keeps user replies verbatim, blocks work tools, and starts one owned scheduler loop only on Ready", async () => {
|
|
const h = setup({ choices: ["Ready"] });
|
|
await h.commands.get("goals").handler("new plot the data", h.ctx);
|
|
const file = h.branch.findLast(e => e.customType === "pi-goals-single-agent").data.file;
|
|
expect(file).toMatch(/\.pi\/goals\/sess-[\w]+\.md$/);
|
|
writeFileSync(file, GOALS);
|
|
await h.hook("input", { source: "interactive", text: "yes, reuse judge_demos.py" });
|
|
expect(readFileSync(file, "utf8")).toContain("> yes, reuse judge_demos.py");
|
|
expect(await h.hook("tool_call", { toolName: "bash", input: { command: "python train.py" } })).toMatchObject({ block: true });
|
|
expect(await h.hook("tool_call", { toolName: "write", input: { path: file } })).toBeUndefined();
|
|
expect(h.sent.some(m => m.text.startsWith("/schedule "))).toBe(false);
|
|
|
|
await h.tools.get("RequestPlanReview").execute();
|
|
await h.hook("agent_settled");
|
|
expect(h.sent.filter(m => m.text.startsWith("/schedule ")).length).toBe(1);
|
|
expect(await h.hook("tool_call", { toolName: "bash", input: { command: "python train.py" } })).toBeUndefined();
|
|
});
|
|
|
|
it("Cancel starts nothing and keeps the file", async () => {
|
|
const h = setup({ choices: ["Cancel"] });
|
|
const file = await ready(h);
|
|
expect(h.sent.some(m => m.text.startsWith("/schedule "))).toBe(false);
|
|
expect(readFileSync(file, "utf8")).toContain("goal: make the plot");
|
|
});
|
|
});
|
|
|
|
describe("scheduled loop wake", () => {
|
|
it("injects the current loop statement and goals from disk, without Log history", async () => {
|
|
const h = setup({ choices: ["Ready"] });
|
|
const file = await ready(h);
|
|
const prompt = h.schedulerReceipt("t1");
|
|
writeFileSync(file, GOALS.replace("A plot.", "A plot with error bars."));
|
|
const out = await h.wake("t1", prompt);
|
|
expect(out.action).toBe("transform");
|
|
expect(out.text).toContain(LOOP);
|
|
expect(out.text).toContain("A plot with error bars.");
|
|
expect(out.text).toContain("goal: make the plot");
|
|
expect(out.text).not.toContain("historical detail");
|
|
});
|
|
|
|
it("drops wakes from another token and ignores ordinary text", async () => {
|
|
const h = setup({ choices: ["Ready"] });
|
|
await ready(h);
|
|
h.schedulerReceipt("t1");
|
|
expect(await h.wake("t9", "pi-goals-loop:not-ours")).toEqual({ action: "handled" });
|
|
expect(await h.hook("input", { source: "extension", text: "pi-goals-loop:x said in prose" })).toBeUndefined();
|
|
});
|
|
|
|
it("pause removes the owned task; later wakes are dropped; resume starts a new loop", async () => {
|
|
const h = setup({ choices: ["Ready"] });
|
|
await ready(h);
|
|
const prompt = h.schedulerReceipt("t1");
|
|
await h.commands.get("goals").handler("pause", h.ctx);
|
|
expect(h.sent.at(-1)?.text).toBe("/schedule-remove t1");
|
|
expect(await h.wake("t1", prompt)).toEqual({ action: "handled" });
|
|
await h.commands.get("goals").handler("resume", h.ctx);
|
|
const second = h.schedulerReceipt("t2");
|
|
expect(second).not.toBe(prompt);
|
|
expect((await h.wake("t2", second)).action).toBe("transform");
|
|
});
|
|
|
|
it("stops the loop when no goals remain", async () => {
|
|
const h = setup({ choices: ["Ready"] });
|
|
const file = await ready(h);
|
|
const prompt = h.schedulerReceipt("t1");
|
|
writeFileSync(file, GOALS.replace("[/] goal: make", "[✓] goal: make").replace("[ ] goal: write", "[-] goal: write"));
|
|
expect(await h.wake("t1", prompt)).toEqual({ action: "handled" });
|
|
expect(h.sent.at(-1)?.text).toBe("/schedule-remove t1");
|
|
});
|
|
});
|
|
|
|
describe("context after compaction and resume", () => {
|
|
it("sends the whole goals file once after compaction, including Log", async () => {
|
|
const h = setup({ choices: ["Ready"] });
|
|
await ready(h);
|
|
await h.hook("before_agent_start");
|
|
await h.hook("session_compact");
|
|
const retry = await h.hook("context", { messages: [] });
|
|
expect(retry.messages.at(-1).content[0].text).toContain("historical detail");
|
|
expect(await h.hook("context", { messages: [] })).toBeUndefined();
|
|
});
|
|
|
|
it("restores this session's goals on resume and ignores another session's state", async () => {
|
|
const h = setup({ choices: ["Ready"] });
|
|
await ready(h);
|
|
await h.hook("session_start");
|
|
expect((await h.hook("before_agent_start")).message.content).toContain(LOOP);
|
|
expect(h.ctx.widget[0]).toBe("◼ G1: make the plot");
|
|
expect(h.ctx.widget[1]).toBe(" ◦ load data");
|
|
h.branch.push({ type: "custom", customType: "pi-goals-single-agent", data: { owner: "other", phase: "working", file: "x", judge: true } });
|
|
await h.hook("session_start");
|
|
expect(await h.hook("before_agent_start")).toBeUndefined();
|
|
expect(h.ctx.widget).toBeUndefined();
|
|
});
|
|
});
|