mirror of
https://github.com/wassname/pi-plan.git
synced 2026-08-11 11:23:31 +08:00
fold the plan at ## Log: re-send the working set on a stale cadence, the whole file only on resync
v2 injected the entire plan.md on every turn. pi-tasks tried that and deleted it -- "wallpaper noise that trains the model to ignore the task block" (CHANGELOG.md:149) -- so follow them: one transient user message via the context hook, never persisted, and only when the plan went untouched for 2 turns. Editing the plan resets the clock, the way a task tool call resets theirs. Session start and session_compact push the WHOLE file back instead, which is where the settled context is actually needed (pi-goal-x does the same with its post-compaction resync). That makes an unlimited appendix free: everything under ## Log is durable memory, not working set. Also: the drafting prompt is sent once with the /goals seed instead of every turn (that re-arming is why plan mode read as never-ending), the review menu gains "Open in $EDITOR" and loops like pi-plan's, and the widget shows the active goal's open subtasks so the plan is visibly the task list. Drops the stale-copy stripping hook, which a non-persisted injection doesn't need. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
+131
-64
@@ -6,8 +6,12 @@
|
||||
* The v1 lesson: the parser existed so TypeScript could read plan.md, but almost every reader is a
|
||||
* model. So v2 has NO parser and no schema. The harness does exactly three things for a
|
||||
* cooperative-but-confused model:
|
||||
* 1. memory — inject plan.md verbatim every turn (survives compaction; byte-identical when
|
||||
* unchanged so the KV cache holds; stale copies stripped by the context hook)
|
||||
* 1. memory — a transient re-send of the plan, never persisted, on two triggers: the plan went
|
||||
* stale for STALE_TURNS turns (send the working set above ## Log), or the session
|
||||
* started / compacted (send the whole file, appendix included). v2 sent the whole
|
||||
* file every turn; pi-tasks tried that and deleted it as "wallpaper noise that
|
||||
* trains the model to ignore the task block" (tintinweb/pi-tasks CHANGELOG.md:149),
|
||||
* and the always-present CompleteGoal description carries the contract instead.
|
||||
* 2. format — a skeleton convention taught in planDrafting (prompts.ts), not validated
|
||||
* 3. eyes — CompleteGoal spawns a strictly read-only pi subprocess (--no-session, no bash)
|
||||
* that gets the whole plan file plus the claimed goal, finds the goal itself
|
||||
@@ -28,15 +32,14 @@
|
||||
* All model-facing text lives in prompts.ts, in flow order.
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { completeGoalDescription, completeGoalParamDescription, judgeSystem, judgeUser, planDrafting, reminder } from "./prompts.js";
|
||||
import { completeGoalDescription, completeGoalParamDescription, judgeSystem, judgeUser, planDrafting, reminder, resync } from "./prompts.js";
|
||||
|
||||
const STATE = "pi-goals-state";
|
||||
const PLAN_CONTEXT = "pi-goals-context"; // injected plan/guidance, stale copies stripped by the context hook
|
||||
const STATUS_KEY = "pi-goals";
|
||||
const WIDGET_KEY = "pi-goals-widget";
|
||||
const PLAN_REL = ".pi/plan.md";
|
||||
@@ -49,22 +52,49 @@ const JUDGE_TIMEOUT_MS = 600_000;
|
||||
// Plan mode is read-only by convention AND a light gate: edit/write are blocked (except plan.md,
|
||||
// the deliverable). bash stays open — the prompt says don't mutate; guide, don't gate (spec D3).
|
||||
const PLAN_MODE_BLOCKED_TOOLS = ["edit", "write"];
|
||||
// Turns the plan may go untouched before it is re-sent. pi-tasks uses 4, or 2 while something is in
|
||||
// progress; here every goal is "in progress", so 2.
|
||||
const STALE_TURNS = 2;
|
||||
|
||||
// The one regex in the whole extension: a checkbox line beginning "goal:", for the widget and the
|
||||
// "any goals open?" reminder condition. Everything else reads the file as prose.
|
||||
// A checkbox line beginning "goal:", for the widget and the "any goals open?" reminder condition.
|
||||
// Everything else reads the file as prose.
|
||||
const GOAL_LINE = /^\s*(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*goal:\s*(.*)$/i;
|
||||
// An indented checkbox line that isn't a goal: a subtask. Only the widget reads these, so the human
|
||||
// sees the next action and not just the goal -- this file IS the task list.
|
||||
const SUBTASK_LINE = /^\s+(?:\d+\.|[-*])\s*\[([ xX/-])\]\s*(.*)$/;
|
||||
// The fold. Above it: the working set that gets re-sent. Below it: durable memory.
|
||||
const FOLD_LINE = /^##\s+Log\s*$/im;
|
||||
type GoalStatus = "open" | "active" | "done" | "cancelled";
|
||||
const CHAR_TO_STATUS: Record<string, GoalStatus> = { " ": "open", "/": "active", x: "done", "-": "cancelled" };
|
||||
|
||||
function scanGoals(plan: string): Array<{ status: GoalStatus; subject: string }> {
|
||||
const goals: Array<{ status: GoalStatus; subject: string }> = [];
|
||||
for (const line of plan.split("\n")) {
|
||||
function scanGoals(plan: string): Array<{ status: GoalStatus; subject: string; line: number }> {
|
||||
const goals: Array<{ status: GoalStatus; subject: string; line: number }> = [];
|
||||
plan.split("\n").forEach((line, i) => {
|
||||
const m = GOAL_LINE.exec(line);
|
||||
if (m) goals.push({ status: CHAR_TO_STATUS[m[1].toLowerCase()] ?? "open", subject: m[2].trim() });
|
||||
}
|
||||
if (m) goals.push({ status: CHAR_TO_STATUS[m[1].toLowerCase()] ?? "open", subject: m[2].trim(), line: i });
|
||||
});
|
||||
return goals;
|
||||
}
|
||||
|
||||
/** The working set: everything above "## Log". Log, Learnings and Appendix below it are durable
|
||||
* memory -- unlimited, read on demand, pushed back only by a resync. Exported for the unit test. */
|
||||
export function foldPlan(plan: string): string {
|
||||
const m = FOLD_LINE.exec(plan);
|
||||
return (m ? plan.slice(0, m.index) : plan).trimEnd();
|
||||
}
|
||||
|
||||
/** Open subtasks under the goal on line `goalLine`, up to the next goal line. */
|
||||
export function openSubtasks(plan: string, goalLine: number): string[] {
|
||||
const lines = plan.split("\n");
|
||||
const out: string[] = [];
|
||||
for (let i = goalLine + 1; i < lines.length; i++) {
|
||||
if (GOAL_LINE.test(lines[i])) break;
|
||||
const m = SUBTASK_LINE.exec(lines[i]);
|
||||
if (m && (m[1] === " " || m[1] === "/")) out.push(m[2].trim());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
interface PlanState {
|
||||
isPlanMode: boolean;
|
||||
/** Optional model ref for the sign-off judge; unset => current session model, else pi's default. */
|
||||
@@ -73,8 +103,14 @@ interface PlanState {
|
||||
|
||||
export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
let state: PlanState = { isPlanMode: false, judgeModel: null };
|
||||
// Reminder cadence: fire when goals are open but plan.md was untouched since the last turn.
|
||||
let lastInjectedPlan = "";
|
||||
// Reminder cadence (pi-tasks style): the plan is re-sent only after it has gone untouched for
|
||||
// STALE_TURNS turns, and editing it resets the clock -- an agent that is maintaining the file
|
||||
// doesn't need to be told to. In-memory, like pi-tasks: a new session starts fresh.
|
||||
let turnsStale = 0;
|
||||
let lastSeenPlan = "";
|
||||
// Set on session start and after a compaction; drained by the next LLM call, which then carries
|
||||
// the WHOLE file (appendix included) instead of just the working set.
|
||||
let resyncReason: string | null = "New session.";
|
||||
|
||||
const planPath = (ctx: ExtensionContext) => join(ctx.cwd, ".pi", "plan.md");
|
||||
const readPlan = (ctx: ExtensionContext): string => (existsSync(planPath(ctx)) ? readFileSync(planPath(ctx), "utf-8") : "");
|
||||
@@ -102,9 +138,15 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
const done = goals.filter((g) => g.status === "done").length;
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("accent", `◷ ${done}/${goals.length} goals`));
|
||||
const mark: Record<GoalStatus, string> = { done: "✔", active: "▸", open: "◻", cancelled: "✗" };
|
||||
// Only live goals get lines so finished work never pushes current work off screen.
|
||||
const live = goals.filter((g) => g.status === "active" || g.status === "open");
|
||||
ctx.ui.setWidget(WIDGET_KEY, [ctx.ui.theme.fg("muted", PLAN_REL), ...live.map((g) => `${mark[g.status]} ${g.subject}`)]);
|
||||
// Only live goals get lines so finished work never pushes current work off screen. The active
|
||||
// goal also shows its open subtasks: this file is the task list, so the widget is the task list.
|
||||
const plan = readPlan(ctx);
|
||||
const lines = [ctx.ui.theme.fg("muted", PLAN_REL)];
|
||||
for (const g of goals.filter((g) => g.status === "active" || g.status === "open")) {
|
||||
lines.push(`${mark[g.status]} ${g.subject}`);
|
||||
if (g.status === "active") lines.push(...openSubtasks(plan, g.line).slice(0, 3).map((s) => ctx.ui.theme.fg("muted", ` ◦ ${s}`)));
|
||||
}
|
||||
ctx.ui.setWidget(WIDGET_KEY, lines);
|
||||
}
|
||||
|
||||
// --- /goals: enter plan mode (or clear / set judge) --------------------------------------------
|
||||
@@ -131,40 +173,67 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
state = { ...state, isPlanMode: true };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
// The drafting rules are sent ONCE, with the seed. v2 re-injected them every turn, which is
|
||||
// why plan mode read as never-ending: every reply re-armed it. They come back only on a
|
||||
// resync (session start / compaction), when the model has genuinely lost them.
|
||||
const seed = arg
|
||||
? `We're in plan mode. Objective: ${arg}\n\nExplore the repo read-only and ask me anything unclear. When the objective is nailed down, draft (or replace) the plan in ${planPath(ctx)}, then stop for review.`
|
||||
: `We're in plan mode. Tell me what you want to plan. Explore read-only and ask questions as needed; when the objective is clear, draft the plan in ${planPath(ctx)} and stop for review.`;
|
||||
? `We're in plan mode. Objective: ${arg}\n\n${planDrafting}\n\nWrite the plan to ${planPath(ctx)}.`
|
||||
: `We're in plan mode. Tell me what you want to plan.\n\n${planDrafting}\n\nWrite the plan to ${planPath(ctx)}.`;
|
||||
pi.sendUserMessage(seed, { deliverAs: "followUp" });
|
||||
},
|
||||
});
|
||||
|
||||
// --- hooks --------------------------------------------------------------------------------------
|
||||
|
||||
pi.on("before_agent_start", async (_event, ctx) => {
|
||||
/** What this LLM call should carry, if anything: a one-shot resync, or a staleness reminder. */
|
||||
function dueInjection(ctx: ExtensionContext, plan: string): string | null {
|
||||
const drainResync = (): string | null => {
|
||||
const why = resyncReason;
|
||||
resyncReason = null;
|
||||
return why;
|
||||
};
|
||||
if (state.isPlanMode) {
|
||||
return { message: { customType: PLAN_CONTEXT, content: `${planDrafting}\n\nWrite the plan to ${planPath(ctx)}.`, display: false } };
|
||||
const why = drainResync();
|
||||
return why ? `<system-reminder>\n${why} You are still in plan mode.\n\n${planDrafting}\n\nWrite the plan to ${planPath(ctx)}.\n</system-reminder>` : null;
|
||||
}
|
||||
const plan = readPlan(ctx);
|
||||
if (!plan.trim()) return null;
|
||||
const why = drainResync();
|
||||
if (why) return resync(plan, PLAN_REL, why);
|
||||
if (turnsStale < STALE_TURNS) return null;
|
||||
const goals = scanGoals(plan);
|
||||
if (goals.length === 0) {
|
||||
// Non-empty plan but no recognizable goal line: the harness would go silently inert (no
|
||||
// widget, no injection, no reminders). Say so once instead -- cooperative but confused.
|
||||
if (!plan.trim()) return;
|
||||
return {
|
||||
message: {
|
||||
customType: PLAN_CONTEXT,
|
||||
content: `${PLAN_REL} exists but has no goal line pi-goals recognizes. A goal is a checkbox list line starting "goal:", e.g. "1. [ ] goal: <imperative>" ([ ] open, [/] active, [x] done, [-] cancelled). Reformat if it's meant to be the plan.`,
|
||||
display: false,
|
||||
},
|
||||
};
|
||||
// widget, no injection, no reminders). Say so instead -- cooperative but confused.
|
||||
return `<system-reminder>\n${PLAN_REL} exists but has no goal line pi-goals recognizes. A goal is a checkbox list line starting "goal:", e.g. "1. [ ] goal: <imperative>" ([ ] open, [/] active, [x] done, [-] cancelled). Reformat it if it's meant to be the plan.\n</system-reminder>`;
|
||||
}
|
||||
// The plan file itself IS the injection: no parsing, no summarizing, the model sees the
|
||||
// literal file it edits. Byte-identical when unchanged, so the prefix cache holds.
|
||||
let body = `Current plan (${PLAN_REL}; keep it updated with your edit tool):\n\n${plan}`;
|
||||
const live = goals.some((g) => g.status === "active" || g.status === "open");
|
||||
if (live && plan === lastInjectedPlan) body += `\n\n${reminder}`;
|
||||
lastInjectedPlan = plan;
|
||||
return { message: { customType: PLAN_CONTEXT, content: body, display: false } };
|
||||
if (!goals.some((g) => g.status === "active" || g.status === "open")) return null;
|
||||
return reminder(foldPlan(plan), PLAN_REL);
|
||||
}
|
||||
|
||||
// The one injection point: a transient user message on this LLM call only, never persisted. So
|
||||
// there are no stale copies to strip, and an untouched turn costs nothing.
|
||||
pi.on("context", async (event, ctx) => {
|
||||
const text = dueInjection(ctx, readPlan(ctx));
|
||||
if (!text) return;
|
||||
turnsStale = 0;
|
||||
return { messages: [...event.messages, { role: "user" as const, content: [{ type: "text" as const, text }], timestamp: Date.now() }] };
|
||||
});
|
||||
|
||||
// The staleness clock: editing the plan resets it, the way a task tool call resets pi-tasks'.
|
||||
pi.on("turn_end", async (_event, ctx) => {
|
||||
const plan = readPlan(ctx);
|
||||
if (plan === lastSeenPlan) {
|
||||
turnsStale++;
|
||||
return;
|
||||
}
|
||||
lastSeenPlan = plan;
|
||||
turnsStale = 0;
|
||||
updateWidget(ctx);
|
||||
});
|
||||
|
||||
// A compaction is exactly when the settled context is gone, so push the whole file back once.
|
||||
pi.on("session_compact", async () => {
|
||||
resyncReason = "The session was just compacted.";
|
||||
});
|
||||
|
||||
// Plan mode gate: block edit/write except on plan.md itself. bash stays open (guide, not gate).
|
||||
@@ -177,34 +246,30 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
}
|
||||
});
|
||||
|
||||
// After a plan-mode turn: if goals were drafted, offer Ready. No edit menus, no fresh-session
|
||||
// dance — the human reads the file and says go (or keeps talking to revise it).
|
||||
// After a plan-mode turn: if goals were drafted, offer Ready. The human reads the file and says
|
||||
// go, edits it in $EDITOR, or keeps talking to revise it (menu shape borrowed from pi-plan).
|
||||
pi.on("agent_end", async (_event, ctx) => {
|
||||
if (!state.isPlanMode || !ctx.hasUI) return;
|
||||
if (scanGoals(readPlan(ctx)).length === 0) return; // still exploring/asking; nothing to review yet
|
||||
const choice = await ctx.ui.select(`Plan drafted in ${PLAN_REL}. Ready?`, [
|
||||
"Ready — start working the plan",
|
||||
"Keep planning (reply to revise)",
|
||||
]);
|
||||
if (!choice?.startsWith("Ready")) return;
|
||||
state = { ...state, isPlanMode: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
pi.sendUserMessage(
|
||||
`Work the goals in ${planPath(ctx)}. Pick an open goal, mark it active ([/]), work its subtasks, and when its discriminator is satisfied fill its evidence: list, then call CompleteGoal with the goal's text. Keep the plan file current as you go.`,
|
||||
{ deliverAs: "followUp" },
|
||||
);
|
||||
});
|
||||
|
||||
// Keep only the freshest injected plan; strip stale ones so history doesn't bloat and the model
|
||||
// never sees an out-of-date plan.
|
||||
pi.on("context", async (event) => {
|
||||
const isCtx = (m: unknown) => (m as { customType?: string }).customType === PLAN_CONTEXT;
|
||||
let lastIdx = -1;
|
||||
event.messages.forEach((m, i) => {
|
||||
if (isCtx(m)) lastIdx = i;
|
||||
});
|
||||
return { messages: event.messages.filter((m, i) => !isCtx(m) || i === lastIdx) };
|
||||
while (scanGoals(readPlan(ctx)).length > 0) {
|
||||
const choice = await ctx.ui.select(`Plan drafted in ${PLAN_REL}. Ready?`, [
|
||||
"Ready — start working the plan",
|
||||
"Open in $EDITOR — edit it myself",
|
||||
"Keep planning (reply to revise)",
|
||||
]);
|
||||
if (choice?.startsWith("Open")) {
|
||||
spawnSync(process.env.EDITOR || process.env.VISUAL || "vi", [planPath(ctx)], { stdio: "inherit" });
|
||||
continue;
|
||||
}
|
||||
if (!choice?.startsWith("Ready")) return;
|
||||
state = { ...state, isPlanMode: false };
|
||||
persist();
|
||||
updateWidget(ctx);
|
||||
pi.sendUserMessage(
|
||||
`Work the goals in ${planPath(ctx)}. Pick an open goal, mark it active ([/]), work its subtasks, and when its discriminator is satisfied fill its evidence: list, then call CompleteGoal with the goal's text. Keep the plan file current as you go.`,
|
||||
{ deliverAs: "followUp" },
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
@@ -220,6 +285,8 @@ export default function piGoalsExtension(pi: ExtensionAPI): void {
|
||||
.filter((e: { type?: string; customType?: string }) => e.type === "custom" && e.customType === STATE)
|
||||
.pop() as { data?: PlanState } | undefined;
|
||||
if (last?.data) state = { ...state, ...last.data };
|
||||
lastSeenPlan = readPlan(ctx);
|
||||
resyncReason = "New session.";
|
||||
updateWidget(ctx);
|
||||
});
|
||||
|
||||
|
||||
+78
-17
@@ -4,13 +4,18 @@
|
||||
* Design: plan.md is for LLMs and the human, not for TypeScript. There is no parser and no schema;
|
||||
* the skeleton below is a convention the drafting prompt teaches, the working agent maintains with
|
||||
* its normal Edit tool, and the judge reads natively. The harness does three things for a
|
||||
* cooperative-but-confused model: memory (inject the file verbatim every turn), format guidance
|
||||
* (the skeleton), and fresh eyes (the read-only judge in CompleteGoal).
|
||||
* cooperative-but-confused model: memory (a transient re-send of the plan when it goes stale),
|
||||
* format guidance (the skeleton), and fresh eyes (the read-only judge in CompleteGoal).
|
||||
*
|
||||
* THE FOLD: everything above "## Log" is the working set (title, user voice, goals,
|
||||
* discriminators) and is what gets re-sent on the reminder cadence. Everything below it (Log,
|
||||
* Learnings, Appendix) is durable memory: unlimited, read on demand, and re-sent in full only at
|
||||
* session start and after a compaction, which is where the settled context is actually needed.
|
||||
*
|
||||
* Flow:
|
||||
* SETUP (plan mode) 1. planDrafting — draft goals into plan.md (read-only phase)
|
||||
* EXEC, each turn start 2. (the plan.md file itself, injected verbatim by index.ts)
|
||||
* EXEC, periodic 3. reminder — upkeep + autonomy nudge when plan.md went untouched
|
||||
* SETUP (plan mode) 1. planDrafting — draft goals into plan.md (read-only phase), sent once
|
||||
* EXEC, on cadence 2. reminder — the folded plan + upkeep nudge when plan.md went stale
|
||||
* EXEC, after compact 3. resync — the WHOLE file back, once
|
||||
* SIGN-OFF, agent-side 4. completeGoal* — the one blessed tool's description
|
||||
* SIGN-OFF, judge-side 5. judgeSystem/judgeUser — the one rigorous check
|
||||
*
|
||||
@@ -26,20 +31,37 @@ You are in plan mode. The objective may arrive through conversation, not as one
|
||||
Explore the repository read-only first: resolve discoverable facts by looking them up, and only ask
|
||||
the human when the answer is a genuine intent or preference choice. Do not write or run code in this
|
||||
phase (edit/write are blocked except for the plan file; don't mutate state via bash either). When
|
||||
the objective is clear, draft the plan file and stop for review.
|
||||
the objective is clear, draft the plan file and present it.
|
||||
|
||||
How this mode ends: after each of your turns the human gets a menu (Ready / open in $EDITOR / keep
|
||||
planning). Plan mode ends when they pick Ready. So close every draft with one line -- the plan is
|
||||
final, pick Ready to start or reply to revise -- and do not redraft in silence. When a new
|
||||
requirement arrives, fold it in, say what changed, and say the plan is final again. Detail that
|
||||
doesn't change a goal or a discriminator belongs in the appendix, not in the goals.
|
||||
|
||||
Right-size it:
|
||||
- Default to ONE goal. Add another only when it's a genuinely separate checkpoint that can pass or
|
||||
fail on its own. Most objectives are 1-2 goals.
|
||||
- Subtasks are the steps inside a goal; add them when a goal has 3+ distinct steps, skip otherwise.
|
||||
- Don't invent goals to look thorough. When in doubt, merge.
|
||||
- Everything above "## Log" is the part the model carries while it works. Keep it under 50 lines,
|
||||
reviewable in one pass. Everything below "## Log" is unlimited.
|
||||
|
||||
Style: ASD-STE100 Simplified Technical English. Active voice, one idea per sentence, common words,
|
||||
the same word for the same thing, and define a new term at first use. This covers the context
|
||||
paragraph and the appendix too, not just the checklist. No all-caps headers and no bold spam; the
|
||||
checklist is already the structure.
|
||||
|
||||
Write the plan file in roughly this shape (it's a convention, not a schema -- the file is read
|
||||
directly by the human and a judge model, so clarity beats conformance; small deviations are fine):
|
||||
|
||||
# <short plan title>
|
||||
|
||||
<context: restate the user's ask, their stated preferences, and any decisions you've agreed on>
|
||||
<context: one short paragraph. What the human wants and why.>
|
||||
|
||||
## User voice
|
||||
|
||||
- > "<the human's requirement, quoted word for word>"
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -52,10 +74,16 @@ directly by the human and a judge model, so clarity beats conformance; small dev
|
||||
1. [ ] <subtask>
|
||||
- evidence: (empty until sign-off)
|
||||
|
||||
# Future work / out of scope
|
||||
## Future work / out of scope
|
||||
|
||||
<-- the fold: everything below here is durable memory, not the working set -->
|
||||
|
||||
## Log
|
||||
|
||||
## Learnings
|
||||
|
||||
## Appendix (context, not approved)
|
||||
|
||||
Conventions:
|
||||
- A goal is a checkbox line beginning "goal:". Checkbox state: [ ] open, [/] active, [x] done,
|
||||
[-] cancelled. Leave goals [ ] at planning.
|
||||
@@ -69,24 +97,57 @@ Conventions:
|
||||
- evidence stays empty at planning; you fill it at sign-off and a fresh read-only judge checks it.
|
||||
Cite durable artifacts a future reader can open: committed files, test names, git diffs. .pi/ is
|
||||
usually gitignored, so files there prove things only at judge time, not in history.
|
||||
- User voice: quote the human word for word, one line per requirement, as they say it. Never
|
||||
paraphrase there -- a paraphrase drifts, and then the goals churn on the next reply.
|
||||
- Rejected options stay visible: ~~struck through~~ with who rejected them and why, so nobody
|
||||
relitigates them.
|
||||
- Learnings: one line per gotcha that a future reader would otherwise rediscover. Write down what
|
||||
you saw from a source that does not persist (a browser page, an image, a long log tail) before
|
||||
you do anything else with it.
|
||||
- Appendix: unlimited and unverified. Alternatives, links, dead ends, and the settled detail that
|
||||
is not part of the approved goals. Nothing here is approved and nothing here is checked.
|
||||
|
||||
When the goals are drafted, present them and stop for review. Do not begin execution.`;
|
||||
When the goals are drafted, present them and say the plan is final. Do not begin execution.`;
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 3. reminder — EXEC, appended to the injected plan when plan.md went
|
||||
* untouched for a whole turn while goals are open. Wording stable (cache).
|
||||
* 3. reminder — EXEC. Transient, never persisted, and only when the plan went stale for a couple of
|
||||
* turns. pi-tasks tried a per-turn injection and deleted it: "wallpaper noise that trains the
|
||||
* model to ignore the task block" (tintinweb/pi-tasks CHANGELOG.md:149). Carries the folded plan
|
||||
* (above ## Log), because a nudge with no plan in it makes the model go read the file anyway.
|
||||
* ──────────────────────────────────────────────────────────────────────── */
|
||||
export const reminder = `\
|
||||
export function reminder(foldedPlan: string, planRel: string): string {
|
||||
return `\
|
||||
<system-reminder>
|
||||
Keep the plan file current as you work (edit it directly):
|
||||
Your plan (${planRel}, above the fold; the log, learnings and appendix are in the file):
|
||||
|
||||
${foldedPlan}
|
||||
|
||||
Keep it current as you work, with your normal edit tool:
|
||||
- tick finished subtasks ([/] in progress), add discovered ones
|
||||
- append ONE short line to ## Log (append, don't rewrite earlier lines)
|
||||
- append ONE short line to ## Log, and a line to ## Learnings for a gotcha worth keeping
|
||||
- when the active goal's discriminator is satisfied, fill its evidence: list (each item = a durable
|
||||
artifact + a verbatim quote you actually observed + a short read of it), then call CompleteGoal. Don't tick a goal [x] before CompleteGoal
|
||||
accepts; the sign-off log line is the audit trail.
|
||||
- if the file has grown long, prune finished goals (their evidence lives in git history and ## Log)
|
||||
artifact + a verbatim quote you actually observed + a short read of it), then call CompleteGoal.
|
||||
Don't tick a goal [x] before CompleteGoal accepts; the sign-off log line is the audit trail.
|
||||
- if the working set has grown long, prune finished goals (their evidence lives in git history and
|
||||
## Log) and move settled detail down to ## Appendix, which is unlimited
|
||||
- otherwise keep working toward the active goal; don't stop to ask unless genuinely blocked
|
||||
</system-reminder>`;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 3b. resync — EXEC, one-shot at session start and after a compaction: the WHOLE file back,
|
||||
* appendix included. Modelled on pi-goal-x's [POST-COMPACTION RESYNC] one-shot. This is the
|
||||
* only place the below-the-fold sections are pushed; otherwise the agent reads them on demand.
|
||||
* ──────────────────────────────────────────────────────────────────────── */
|
||||
export function resync(plan: string, planRel: string, why: string): string {
|
||||
return `\
|
||||
<system-reminder>
|
||||
${why} This is the whole plan file (${planRel}), appendix included, so you don't re-litigate what
|
||||
was already settled. Keep working the active goal; edit the file directly as you go.
|
||||
|
||||
${plan}
|
||||
</system-reminder>`;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* 4. completeGoal — SIGN-OFF, agent-side: the one blessed tool
|
||||
|
||||
Reference in New Issue
Block a user