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:
wassname
2026-08-05 12:08:16 +08:00
co-authored by Claudypoo
parent 16827de45d
commit 07510d96bd
3 changed files with 272 additions and 81 deletions
+131 -64
View File
@@ -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
View File
@@ -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
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { foldPlan, openSubtasks } from "../src/index.js";
const plan = `# Plan
## User voice
- > "keep it under 50 lines"
## Goals
1. [/] goal: Implement the cache layer
- discriminator: hit-rate > 0.8 in load-test.log
- tasks:
1. [x] wire client
2. [/] eviction policy
3. [ ] bench p95
2. [ ] goal: Ship the docs
- tasks:
1. [ ] write the readme
## Log
- 2026-08-05 12:00 wired the client
## Learnings
- the tokenizer pads left, which silently shifted every offset
## Appendix (context, not approved)
${"filler line\n".repeat(200)}`;
describe("foldPlan (the working set is what gets re-sent; below ## Log is durable memory)", () => {
it("keeps the title, user voice and goals", () => {
const folded = foldPlan(plan);
expect(folded).toContain("keep it under 50 lines");
expect(folded).toContain("goal: Implement the cache layer");
expect(folded).toContain("discriminator: hit-rate > 0.8");
});
it("drops the log, the learnings and the unlimited appendix", () => {
const folded = foldPlan(plan);
expect(folded).not.toContain("wired the client");
expect(folded).not.toContain("tokenizer pads left");
expect(folded).not.toContain("filler line");
expect(folded.length).toBeLessThan(plan.length / 4);
});
it("returns the whole plan when there is no ## Log yet (a fresh draft)", () => {
const draft = "# Plan\n\n## Goals\n\n1. [ ] goal: do the thing\n";
expect(foldPlan(draft)).toBe(draft.trimEnd());
});
});
describe("openSubtasks (the widget shows the next action, so the plan IS the task list)", () => {
const active = plan.split("\n").findIndex((l) => l.includes("goal: Implement the cache layer"));
it("lists the active goal's open and in-progress subtasks, stopping at the next goal", () => {
expect(openSubtasks(plan, active)).toEqual(["eviction policy", "bench p95"]);
});
it("does not leak subtasks from the goal below", () => {
expect(openSubtasks(plan, active)).not.toContain("write the readme");
});
});