diff --git a/AGENTS.md b/AGENTS.md index ef03a5d..43a4b82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # pi-goals contributor notes -Keep the design small: one agent, one goals file, the stock scheduler for wakes, pi-subagents for the judge. Do not add a private timer, task store or second agent runtime. +Keep the design small and close to how models are trained (see README, Design intent): one agent with its normal tools, one goals file, the stock scheduler for wakes, pi-subagents for the judge. Do not add a private timer, task store or second agent runtime. Keep the user's loop statement wording. All model-facing text is in `src/prompts.ts`, in conversation order. diff --git a/README.md b/README.md index 9d3e290..9eaaa70 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ One Pi agent works from one goals file you approve. A scheduled loop reminds it The supervisor/worker/Herdr design is on the `supervisor-worker-herdr` branch. +## Design intent + +> hmm one idea is that we want to get as close to training as possible. and as ligth as possible. having pi-goals which just repeats a user written paragraph and one agent works seems the lightest. + +— wassname, 2026-09-24. The loop statement is meant to act like an assistance game (CIRL): the user knows the goal, the agent starts uncertain and keeps reducing that uncertainty while it works. + +So: one agent with its normal tools, a user-written paragraph repeated on a schedule, a goals file, and an optional stateless judge. Planning is an explore-and-ask phase; only edits outside the goals file are blocked. Add machinery only when a real run shows it is needed. + ## Use ``` @@ -50,12 +58,16 @@ Goal marks: `[ ]` open, `[/]` active, `[x]` self-verified (judge off), `[✓]` a ## What happens when -| When | The model receives | -|---|---| -| Ready | a short prompt to start work, and `/schedule prompt every 1h` creates the loop task | -| Each scheduled wake | the Loop statement and everything above `## Log`, read from disk at that moment | -| After compaction or resume | the whole goals file once, including Log and Interview | -| `CompleteGoal` | the judge result; accept marks `[✓]`, reject or judge failure leaves the goal open | +| When | The agent receives | You see | +|---|---|---| +| `/goals new` | drafting rules and the default loop statement, as a user message | the same message | +| Review | the whole file | the whole file, then Ready / Refine / Edit / Cancel | +| Ready | a short start prompt; `/schedule prompt every 1h` creates the loop task | both | +| Each scheduled wake | the Loop statement and everything above `## Log`, read from disk then | the same text in chat | +| After compaction or resume | the whole goals file once, including Log and Interview | nothing (hidden message) | +| `CompleteGoal` | the judge result; accept marks `[✓]`, reject or judge failure leaves the goal open | the tool result | + +You can edit the goals file, including the loop statement, at any time; the next wake uses the new text. The loop uses the stock [@jl1990/pi-scheduler](https://www.npmjs.com/package/@jl1990/pi-scheduler) session-scoped task. Change its interval with the scheduler's own commands. Wakes from an older Ready or another session are dropped. The loop is removed on pause, clear, or when no unfinished goals remain. diff --git a/src/index.ts b/src/index.ts index ddb3622..ab9811d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,6 @@ interface State { model?: string; } const initial = (owner: string): State => ({ owner, phase: null, judge: true }); -const PLANNING_TOOLS = new Set(["read", "grep", "find", "ls", "ffgrep", "fffind", "web_search", "fetch_content", "get_search_content", "source_check", "copilot_search", "RequestPlanReview"]); const result = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} }); const handled = { action: "handled" as const }; @@ -181,8 +180,9 @@ export default function piGoals(pi: ExtensionAPI): void { pi.on("session_compact", async () => { resyncDue = true; }); pi.on("turn_end", async (_event, ctx) => { refresh(ctx); }); pi.on("tool_call", async (event, ctx) => { - if (state.phase !== "planning" || PLANNING_TOOLS.has(event.toolName)) return; - if (["write", "edit"].includes(event.toolName) && resolve(ctx.cwd, String((event.input as { path?: string }).path)) === state.file) return; + // Planning allows exploration; only file edits outside the goals file and completion are blocked. + if (state.phase !== "planning" || !["write", "edit", "CompleteGoal"].includes(event.toolName)) return; + if (event.toolName !== "CompleteGoal" && resolve(ctx.cwd, String((event.input as { path?: string }).path)) === state.file) return; return { block: true, reason: prompts.planningState(state.file!) }; }); pi.on("session_start", async (_event, ctx) => { diff --git a/src/prompts.ts b/src/prompts.ts index 8399ade..8811a93 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -1,19 +1,16 @@ // PI/OpenAI: all model-facing text, in conversation order: // planning -> Ready -> scheduled loop wake -> compaction/resume resync -> CompleteGoal -> judge. -// The user edits this during planning; the saved copy in the goals file is what the loop sends. +// wassname's default (2026-09-24, spelling fixed by Claude). The user edits it during planning; +// the saved copy in the goals file is what the loop sends. Aim: assistance-game (CIRL) behaviour. export const DEFAULT_LOOP_STATEMENT = `\ -Task: help the user achieve the goals in this goals file. The user knows what they want better than you do. Your job is to understand it, advance it, and show them results they can easily verify. - -Check whether your recent work still advances the highest-priority open goal and its discriminator. If it has drifted, get back on track. Keep working autonomously. When a decision genuinely needs the user, record the question and continue with what is clear. - -Then briefly tell the busy user, in plain language: what changed toward that goal since you last spoke, what you will do next, and anything you need from them.`; +You are an autonomous agent. Your task is to understand and advance the user's goals, and show them in an easy to understand and easy to verify way that you have done that. Your job is to get back on track, keep moving towards the goals, and keep refining and reducing uncertainty in the user's goals. This is a reminder: your immediate task now is to reread your goals file and get back on track. As a result of this, briefly update the busy user (in plain language, with reminded context) on what you have done since they last talked with respect to their highest goal, what you will do next, and anything you need from them.`; // 1. Planning: sent once with the seed, and again after compaction during planning. export const planDrafting = `\ -You are in plan mode. Write a short goals file that captures what the user actually wants, then test it with them. +You are in plan mode. The user knows what they want; you start uncertain. Reduce that uncertainty: explore, then ask, then write a short goals file that captures what they actually want. -Read the supplied resources, code and data first. Resolve facts with read-only tools or web search. Do not implement, run experiments or launch work in this mode; only the goals file may be written. +Explore first as needed: read the supplied resources, code and data, run quick read-only commands, search the web, or send scouts. Do not implement, run experiments or change files in this mode; only the goals file may be written. Use the grilling approach for consequential gaps: one round of short, self-contained questions with your recommended answers. Ask about decisions the user owns, such as the outcome, scope, evaluation, spending and publication. Resolve routine choices yourself. Respect requests to skip questions. @@ -65,7 +62,7 @@ Conventions: Start with an explicit provisional draft. Discuss and revise it before offering acceptance. When consequential questions are settled, call RequestPlanReview. Only the user's Ready selection authorizes work; saving a draft or answering interview questions does not.`; export function planningState(path: string): string { - return `[pi-goals: planning] Only ${path} may be written. Resolve facts with read-only tools, ask the user about consequential choices, and do not start work. The plan is not approved until the user chooses Ready.`; + return `[pi-goals: planning] Only ${path} may be written. Explore read-only, ask the user about consequential choices, and do not start work. The plan is not approved until the user chooses Ready.`; } // 2. Ready. diff --git a/test/goals-loop.test.ts b/test/goals-loop.test.ts index ec99ae4..3ad92ac 100644 --- a/test/goals-loop.test.ts +++ b/test/goals-loop.test.ts @@ -20,14 +20,15 @@ describe("planning and Ready", () => { 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: "train.py" } })).toMatchObject({ block: true }); + expect(await h.hook("tool_call", { toolName: "bash", input: { command: "head data.csv" } })).toBeUndefined(); 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(); + expect(await h.hook("tool_call", { toolName: "write", input: { path: "train.py" } })).toBeUndefined(); }); it("Cancel starts nothing and keeps the file", async () => {