From bf50d9bbc8274786eef49561c67a366baa5191ba Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:17:27 +0800 Subject: [PATCH] Use TUI-style goals subcommands Stop steward checkpoints after all goals close. Co-Authored-By: Pi Codex <288921227+claudypoo@users.noreply.github.com> --- README.md | 8 ++++---- src/index.ts | 16 ++++++++-------- test/goals-flow.test.ts | 27 ++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fb669aa..97f41e8 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,11 @@ pi -e npm:pi-subagents -e ./src/index.ts turns without a change above `## Log`, the worker gets a reminder and the steward gets a progress checkpoint. -Other commands: `/goals --clear` disconnects this session from its active plan, preserving the -versioned file on disk; `/goals --auto [minutes|off]` continues active goals after the agent settles +Other commands: `/goals clear` disconnects this session from its active plan, preserving the +versioned file on disk; `/goals auto [minutes|off]` continues active goals after the agent settles and then on that interval. It pauses after two automatic wakes with no working-plan change; `/goals ---steward-model ` picks the steward model (default: the pi-subagents agent model). The `--` prefix -keeps ordinary objectives such as `judge model quality` from being parsed as commands. +model ` picks the steward model (default: the pi-subagents agent model). These are TUI +subcommands, not CLI flags. ## Prompts diff --git a/src/index.ts b/src/index.ts index e366934..e0c76b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -280,10 +280,10 @@ export default function piGoalsExtension(pi: ExtensionAPI): void { // --- /goals: enter plan mode (or clear / configure the steward) — Pi/Codex --------------------- pi.registerCommand("goals", { - description: `Plan mode: draft goals into ${PLAN_SHAPE}, review, then work them. /goals | /goals --clear | /goals --auto [minutes|off] | /goals --steward-model `, + description: `Plan mode: draft goals into ${PLAN_SHAPE}, review, then work them. /goals | /goals clear | /goals auto [minutes|off] | /goals model `, handler: async (args, ctx) => { const arg = args.trim(); - if (arg === "--clear") { + if (arg === "clear") { if (state.planVersion === null) { ctx.ui.notify("No active plan to disconnect.", "info"); return; @@ -296,8 +296,8 @@ export default function piGoalsExtension(pi: ExtensionAPI): void { ctx.ui.notify(`Disconnected from ${currentPlan}; the file remains on disk.`, "info"); return; } - if (arg === "--auto" || arg.startsWith("--auto ")) { - const value = arg.slice("--auto".length).trim(); + if (arg === "auto" || arg.startsWith("auto ")) { + const value = arg.slice("auto".length).trim(); if (value === "off") { clearAutoTimer(); state = { ...state, autoIntervalMs: null, autoPaused: false }; @@ -312,7 +312,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void { } const minutes = value ? Number(value) : AUTO_DEFAULT_INTERVAL_MS / 60_000; if (!Number.isInteger(minutes) || minutes < 1) { - ctx.ui.notify("Use /goals --auto [whole minutes], or /goals --auto off.", "warning"); + ctx.ui.notify("Use /goals auto [whole minutes], or /goals auto off.", "warning"); return; } autoWakeInFlight = false; @@ -325,8 +325,8 @@ export default function piGoalsExtension(pi: ExtensionAPI): void { ctx.ui.notify(`Goal auto-continue enabled every ${minutes}m.`, "info"); return; } - if (arg === "--steward-model" || arg.startsWith("--steward-model ")) { - const ref = arg.slice("--steward-model".length).trim(); + if (arg === "model" || arg.startsWith("model ")) { + const ref = arg.slice("model".length).trim(); state = { ...state, stewardModel: ref || null, stewardRunId: null, stewardPending: false }; persist(); setupSteward(ctx); @@ -448,7 +448,7 @@ export default function piGoalsExtension(pi: ExtensionAPI): void { // PI: Print after Pi settles. agent_end is still streaming, so its message queues behind the menu. pi.on("agent_settled", async (_event, ctx) => { if (state.phase === "working") { - if (turnsStale >= STALE_TURNS) await reviewInBackground(ctx, checkpointReview(planRel(ctx), turnsStale)); + if (turnsStale >= STALE_TURNS && activeGoals(ctx)) await reviewInBackground(ctx, checkpointReview(planRel(ctx), turnsStale)); settleAuto(ctx); return; } diff --git a/test/goals-flow.test.ts b/test/goals-flow.test.ts index b3ad57d..b040b57 100644 --- a/test/goals-flow.test.ts +++ b/test/goals-flow.test.ts @@ -124,7 +124,7 @@ describe("/goals draft flow", () => { const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md"); writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [ ] goal: preserve this\n"); - await flow.commands.get("goals").handler("--clear", flow.ctx); + await flow.commands.get("goals").handler("clear", flow.ctx); expect(readFileSync(planPath, "utf-8")).toContain("goal: preserve this"); expect(flow.entries.at(-1)?.data).toMatchObject({ phase: null, planVersion: null }); @@ -232,7 +232,7 @@ describe("/goals draft flow", () => { const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md"); writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n"); await flow.hooks.get("agent_settled")({}, flow.ctx); - await flow.commands.get("goals").handler("--auto 1", flow.ctx); + await flow.commands.get("goals").handler("auto 1", flow.ctx); await flow.hooks.get("agent_settled")({}, flow.ctx); await vi.advanceTimersByTimeAsync(0); @@ -259,7 +259,7 @@ describe("/goals draft flow", () => { const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md"); writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: make the output\n"); await flow.hooks.get("agent_settled")({}, flow.ctx); - await flow.commands.get("goals").handler("--auto 1", flow.ctx); + await flow.commands.get("goals").handler("auto 1", flow.ctx); await flow.hooks.get("agent_start")({}, flow.ctx); await flow.hooks.get("tool_call")({ toolName: "process", input: { action: "start" } }, flow.ctx); await flow.hooks.get("agent_settled")({}, flow.ctx); @@ -274,6 +274,27 @@ describe("/goals draft flow", () => { } }); + it("does not checkpoint after every goal is closed", async () => { + const flow = setup(["Ready"]); + try { + await flow.hooks.get("session_start")({}, flow.ctx); + await flow.commands.get("goals").handler("objective", flow.ctx); + const planPath = join(flow.cwd, ".pi/plan/session-a-v1.md"); + writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [/] goal: produce report\n"); + await flow.hooks.get("agent_settled")({}, flow.ctx); + flow.eventBus.emit("subagent:async-complete", { runId: "steward-1" }); + + writeFileSync(planPath, "# Plan\n\n## Goals\n\n1. [x] goal: produce report\n"); + await flow.hooks.get("turn_end")({}, flow.ctx); + for (let turn = 0; turn < 8; turn++) await flow.hooks.get("turn_end")({}, flow.ctx); + await flow.hooks.get("agent_settled")({}, flow.ctx); + + expect(flow.rpcRequests).toHaveLength(1); + } finally { + rmSync(flow.cwd, { recursive: true, force: true }); + } + }); + it("resumes the same steward lineage for sign-off and persists the latest run", async () => { const flow = setup(["Ready"]); try {