pi-plan -> pi-goals: rename package, command, and file to goals.md

Distinguishes this from the other pi-plan extensions by foregrounding what's
different (goals tracked to verified completion). Mechanical rename only, no
behavior change:
- package @wassname2/pi-plan -> @wassname2/pi-goals (+ repo url)
- plan.md -> goals.md (the canonical file)
- command /plan -> /goals
- file H1 marker "# Plan:" -> "# Goals:", widget/session labels likewise
- internal state keys pi-plan-* -> pi-goals-*

Internal source filename (plan-file.ts) and identifiers (planDrafting, PlanDoc,
setGoalStatus) keep "plan"; they're not user-visible. External burneikis/pi-plan
references are left intact.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-06-16 05:53:22 +08:00
parent bb00314932
commit a65c822bf9
6 changed files with 70 additions and 70 deletions
+14 -14
View File
@@ -1,14 +1,14 @@
# pi-plan
# pi-goals
A [pi](https://github.com/badlogic/pi-mono) extension for plan-driven, goal-tracked work in one
`plan.md`. Set up goals (with evidence and failure modes) in plan mode, work them, and sign a goal
`goals.md`. Set up goals (with evidence and failure modes) in plan mode, work them, and sign a goal
off only when a read-only subagent has checked the evidence.
Successor to [pi-lgtm](https://github.com/wassname/pi-lgtm), kept deliberately small: about
[burneikis/pi-plan](https://github.com/burneikis/pi-plan) plus the additions, goals with evidence,
a sign-off check, a widget, and a reminder.
The form guides; it does not gate. The agent edits `plan.md` with its normal Edit tool. The one
The form guides; it does not gate. The agent edits `goals.md` with its normal Edit tool. The one
blessed tool is `CompleteGoal`, which runs the sign-off check and records the result. The reminder,
the injected plan summary, and git/widget visibility carry the process. It trusts the agent's
judgement rather than guarding it.
@@ -16,39 +16,39 @@ judgement rather than guarding it.
## Install
```bash
pi install npm:@wassname2/pi-plan
pi install npm:@wassname2/pi-goals
```
Or run without installing:
```bash
pi -e npm:@wassname2/pi-plan
pi -e npm:@wassname2/pi-goals
```
## Use
```
/plan add CSV export to the report view
/goals add CSV export to the report view
```
1. Plan. The agent explores read-only and writes goals into `plan.md` (see format below).
1. Plan. The agent explores read-only and writes goals into `goals.md` (see format below).
2. Review. You get a menu: Ready, Edit (ask the agent to revise), Open in `$EDITOR`, or Cancel.
On Ready you choose whether to keep the current context or start fresh and compacted.
3. Work. Each turn the active goal is injected (so it survives compaction) and a reminder nudges
the agent to keep `plan.md` current and work autonomously. When a goal's `done_when` is met the
the agent to keep `goals.md` current and work autonomously. When a goal's `done_when` is met the
agent calls `CompleteGoal`, which runs `verify` and a read-only judge and, on accept, marks it
done and logs it.
Other commands: `/plan` (print the goals), `/plan clear` (empty `plan.md`, history kept in git),
`/plan judge <model-ref>` (use a specific model for the sign-off judge; default is your current
Other commands: `/goals` (print the goals), `/goals clear` (empty `goals.md`, history kept in git),
`/goals judge <model-ref>` (use a specific model for the sign-off judge; default is your current
model).
## plan.md format
## goals.md format
One file holds the objective, the goals, and a short append-only log.
```markdown
# Plan: ship the cache layer
# Goals: ship the cache layer
## Goal: [/] Implement cache layer
<!-- id: cache-layer-1 -->
@@ -81,7 +81,7 @@ evidence:
## The sign-off check (`CompleteGoal`)
`CompleteGoal(goal_id)` is the one blessed completion path. It reads the goal's `evidence:` block
from plan.md (so the proof is git-tracked and human-reviewable before sign-off, not buried in a tool
from goals.md (so the proof is git-tracked and human-reviewable before sign-off, not buried in a tool
call):
1. If the goal has a `verify:` command, it is run. A non-zero exit rejects immediately, with no model
@@ -94,7 +94,7 @@ call):
the goal stays open and the agent is told what is missing.
The judge defaults to your current model (guaranteed authorized and capable). Set a different one
with `/plan judge <provider/model>` for an independent cross-family check.
with `/goals judge <provider/model>` for an independent cross-family check.
## Prompts
+3 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@wassname2/pi-plan",
"name": "@wassname2/pi-goals",
"version": "0.0.1",
"description": "One plan.md: set goals via plan mode, work them, sign off only when a read-only check passes. Successor to pi-lgtm.",
"description": "One goals.md: set goals in plan mode, work them, sign off only when a read-only check passes. Successor to pi-lgtm.",
"author": "wassname",
"license": "MIT",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/wassname/pi-plan.git"
"url": "https://github.com/wassname/pi-goals.git"
},
"keywords": [
"pi-package",
+30 -30
View File
@@ -1,15 +1,15 @@
/**
* pi-plan — plan mode that sets up goals with evidence, tracked in one plan.md, signed off by a
* pi-goals — plan mode that sets up goals with evidence, tracked in one goals.md, signed off by a
* read-only subagent check. A successor to pi-lgtm, kept deliberately small (≈ burneikis/pi-plan
* plus the additions: goals + failure_modes + subtasks, a sign-off check, a widget, a reminder).
*
* Philosophy (spec D3): the form guides, it does not gate. The agent edits plan.md with its normal
* Philosophy (spec D3): the form guides, it does not gate. The agent edits goals.md with its normal
* Edit tool. The one blessed tool is CompleteGoal, which runs the sign-off check and records it. The
* reminder + the injected plan + git/widget visibility carry the process; we trust the agent's
* judgement rather than guarding it.
*
* Flow:
* /plan <objective> -> plan mode: agent explores, drafts goals into plan.md (planDrafting guides)
* /goals <objective> -> plan mode: agent explores, drafts goals into goals.md (planDrafting guides)
* agent_end -> review menu (Ready / Edit / $EDITOR / Cancel); Ready offers compaction
* execution -> each turn, inject the plan summary (survives compaction) + a reminder;
* agent works goals, ticks subtasks, appends ## Log, calls CompleteGoal
@@ -27,10 +27,10 @@ import { Type } from "@sinclair/typebox";
import { counts, findGoal, type Goal, type PlanDoc, parse, recordSignOff, type SignOff } from "./plan-file.js";
import { evidenceJudgeSystem, evidenceJudgeUser, planDrafting, planInjection, reminder } from "./prompts.js";
const STATE = "pi-plan-state";
const PLAN_CONTEXT = "pi-plan-context"; // injected plan-mode guidance, stripped from history later
const STATUS_KEY = "pi-plan";
const WIDGET_KEY = "pi-plan-widget";
const STATE = "pi-goals-state";
const PLAN_CONTEXT = "pi-goals-context"; // injected plan-mode guidance, stripped from history later
const STATUS_KEY = "pi-goals";
const WIDGET_KEY = "pi-goals-widget";
const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", "bash"];
interface PlanState {
@@ -42,12 +42,12 @@ interface PlanState {
export default function piPlanExtension(pi: ExtensionAPI): void {
let state: PlanState = { isPlanMode: false, objective: null, judgeModel: null };
// Reminder cadence: fire when an active goal exists but plan.md was not touched since last turn.
// Reminder cadence: fire when an active goal exists but goals.md was not touched since last turn.
let lastInjectedPlan = "";
// newSession is only on the command-handler context; agent_end's ctx lacks it. Save it from /plan.
// newSession is only on the command-handler context; agent_end's ctx lacks it. Save it from /goals.
let savedCmdCtx: ExtensionCommandContext | null = null;
const planPath = (ctx: ExtensionContext) => join(ctx.cwd, "plan.md");
const planPath = (ctx: ExtensionContext) => join(ctx.cwd, "goals.md");
const readPlan = (ctx: ExtensionContext): string => (existsSync(planPath(ctx)) ? readFileSync(planPath(ctx), "utf-8") : "");
function persist(): void {
@@ -57,7 +57,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
function updateWidget(ctx: ExtensionContext): void {
if (state.isPlanMode) {
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("warning", "planning"));
ctx.ui.setWidget(WIDGET_KEY, ["pi-plan: drafting goals", "Write goals to plan.md, then review."]);
ctx.ui.setWidget(WIDGET_KEY, ["pi-goals: drafting goals", "Write goals to goals.md, then review."]);
return;
}
const doc = parse(readPlan(ctx));
@@ -73,7 +73,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
function goalWidgetLines(doc: PlanDoc): string[] {
const mark: Record<Goal["status"], string> = { done: "✔", active: "▸", open: "◻", cancelled: "✗" };
const lines = [`Plan: ${doc.objective || "(untitled)"}`];
const lines = [`Goals: ${doc.objective || "(untitled)"}`];
for (const g of doc.goals) {
// Show every goal with its status glyph (✔ done, ▸ active, ◻ open, ✗ cancelled) so finished
// goals read as checked off rather than vanishing. Plans are small, so this stays readable.
@@ -86,8 +86,8 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
// --- plan mode: setup -------------------------------------------------------------------------
pi.registerCommand("plan", {
description: "Plan mode: set up goals (with evidence) in plan.md, then work them. /plan <objective>",
pi.registerCommand("goals", {
description: "Plan mode: set up goals (with evidence) in goals.md, then work them. /goals <objective>",
handler: async (args, ctx) => {
savedCmdCtx = ctx; // ctx here is an ExtensionCommandContext (has newSession); keep it for later
const arg = args.trim();
@@ -99,7 +99,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
setJudge(arg.slice("judge".length).trim(), ctx);
return;
}
// Bare `/plan` enters plan mode by prompting for the objective (the common expectation).
// Bare `/goals` enters plan mode by prompting for the objective (the common expectation).
// If the user cancels with no objective, fall back to showing the current plan.
let objective = arg;
if (!objective) {
@@ -128,24 +128,24 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
async function clearPlan(ctx: ExtensionContext): Promise<void> {
if (!existsSync(planPath(ctx))) {
ctx.ui.notify("No plan.md to clear.", "info");
ctx.ui.notify("No goals.md to clear.", "info");
return;
}
if (ctx.hasUI) {
const ok = await ctx.ui.select("Clear plan.md? (it stays in git history)", ["Cancel", "Clear plan.md"]);
if (ok !== "Clear plan.md") return;
const ok = await ctx.ui.select("Clear goals.md? (it stays in git history)", ["Cancel", "Clear goals.md"]);
if (ok !== "Clear goals.md") return;
}
writeFileSync(planPath(ctx), "");
state = { ...state, isPlanMode: false, objective: null };
persist();
updateWidget(ctx);
ctx.ui.notify("Cleared plan.md.", "info");
ctx.ui.notify("Cleared goals.md.", "info");
}
function showPlan(ctx: ExtensionContext): void {
const content = readPlan(ctx);
if (!content.trim()) {
ctx.ui.notify("No plan yet. Use /plan <objective> to start.", "info");
ctx.ui.notify("No goals yet. Use /goals <objective> to start.", "info");
return;
}
ctx.ui.notify(content, "info");
@@ -156,7 +156,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
async function reviewLoop(ctx: ExtensionContext): Promise<void> {
while (true) {
const doc = parse(readPlan(ctx));
const choice = await ctx.ui.select(`Plan: ${doc.goals.length} goal(s). What next?`, [
const choice = await ctx.ui.select(`Goals: ${doc.goals.length} goal(s). What next?`, [
"Ready — start working the plan",
"Edit — ask the agent to revise",
"Open in $EDITOR",
@@ -164,7 +164,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
]);
if (!choice || choice.startsWith("Cancel")) {
exitPlanMode(ctx);
ctx.ui.notify("Left plan mode. plan.md kept.", "info");
ctx.ui.notify("Left plan mode. goals.md kept.", "info");
return;
}
if (choice.startsWith("Ready")) return startExecution(ctx);
@@ -203,7 +203,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
const planFile = planPath(ctx);
const planContent = readPlan(ctx); // captured now: ctx is stale after newSession below
const parentSession = ctx.sessionManager.getSessionFile();
const startMsg = `Work the plan in ${planFile}. Pick an open goal, mark it active (set its header to [/]), work its subtasks, and when its done_when is met fill the goal's evidence: block then call CompleteGoal with the goal_id. Keep plan.md current as you go.`;
const startMsg = `Work the goals in ${planFile}. Pick an open goal, mark it active (set its header to [/]), work its subtasks, and when its done_when is met fill the goal's evidence: block then call CompleteGoal with the goal_id. Keep goals.md current as you go.`;
exitPlanMode(ctx);
if (fresh && savedCmdCtx) {
@@ -223,7 +223,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
}
return;
}
if (doc.objective) pi.setSessionName(`Plan: ${doc.objective}`);
if (doc.objective) pi.setSessionName(`Goals: ${doc.objective}`);
ctx.ui.notify(planContent, "info");
pi.sendUserMessage(startMsg, { deliverAs: "followUp" });
}
@@ -234,20 +234,20 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
name: "CompleteGoal",
label: "Complete goal",
description:
"Sign off a goal once its done_when is met. First fill the goal's evidence: block in plan.md " +
"Sign off a goal once its done_when is met. First fill the goal's evidence: block in goals.md " +
"(a '- ' list pointing at durable artifacts: saved logs, committed diffs, files, not claims), then " +
"call this with the goal_id. Runs the goal's verify command (if any) then a read-only subagent that " +
"inspects that evidence against the repo. On accept, the goal is marked done and logged; on reject, " +
"it stays open and you get what is missing.",
parameters: Type.Object({
goal_id: Type.String({ description: "The goal's <!-- id --> from plan.md" }),
goal_id: Type.String({ description: "The goal's <!-- id --> from goals.md" }),
}),
async execute(_id, params, signal, _onUpdate, ctx) {
const content = readPlan(ctx);
const goal = findGoal(parse(content), params.goal_id);
if (!goal) return text(`No goal #${params.goal_id} in plan.md.`, true);
if (!goal) return text(`No goal #${params.goal_id} in goals.md.`, true);
if (goal.evidence.length === 0) {
return text(`Goal #${goal.id} has no evidence: block. Add a "- " evidence list to the goal in plan.md (what shows done_when is met, and where to verify it), then call CompleteGoal.`, true);
return text(`Goal #${goal.id} has no evidence: block. Add a "- " evidence list to the goal in goals.md (what shows done_when is met, and where to verify it), then call CompleteGoal.`, true);
}
// Decide the outcome (the I/O); recordSignOff applies it to the file (the pure write).
@@ -279,7 +279,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
lastLogLine: doc.log.at(-1) ?? null,
counts: { done: c.done, open: c.open + c.active },
});
// Reminder fires when there is an active goal but plan.md was untouched since the last turn.
// Reminder fires when there is an active goal but goals.md was untouched since the last turn.
const planNow = readPlan(ctx);
if (active && planNow === lastInjectedPlan) body += `\n\n${reminder}`;
lastInjectedPlan = planNow;
@@ -290,7 +290,7 @@ export default function piPlanExtension(pi: ExtensionAPI): void {
if (!state.isPlanMode || !ctx.hasUI) return;
const doc = parse(readPlan(ctx));
if (doc.goals.length === 0) {
ctx.ui.notify("No goals found in plan.md yet — ask the agent to draft them.", "warning");
ctx.ui.notify("No goals found in goals.md yet — ask the agent to draft them.", "warning");
return;
}
await reviewLoop(ctx);
+6 -6
View File
@@ -1,5 +1,5 @@
/**
* plan-file.ts — read plan.md, and the two writes CompleteGoal needs. That is all.
* plan-file.ts — read goals.md, and the two writes CompleteGoal needs. That is all.
*
* Pure module, no pi deps, so it unit-tests without a runtime. The file is the canonical store and
* the agent edits it with its normal Edit tool (create goals, tick subtasks, append log), guided by
@@ -14,7 +14,7 @@
*
* Format:
*
* # Plan: <objective>
* # Goals: <objective>
*
* ## Goal: [ ] <subject>
* <!-- id: <slug> -->
@@ -87,7 +87,7 @@ export function parse(text: string): PlanDoc {
};
for (const line of lines) {
const objMatch = /^#\s+Plan:\s*(.*)$/.exec(line);
const objMatch = /^#\s+Goals:\s*(.*)$/.exec(line);
if (objMatch) {
objective = objMatch[1].trim();
continue;
@@ -201,7 +201,7 @@ export type SignOff =
| { kind: "rejected"; missing: string }
| { kind: "accepted" };
/** Apply a sign-off outcome to plan.md text: accept flips the header checkbox to [x] + logs; reject only logs. Pure. */
/** Apply a sign-off outcome to goals.md text: accept flips the header checkbox to [x] + logs; reject only logs. Pure. */
export function recordSignOff(
text: string,
goalId: string,
@@ -209,7 +209,7 @@ export function recordSignOff(
outcome: SignOff,
): { content: string; message: string; isError: boolean } {
const goal = findGoal(parse(text), goalId);
if (!goal) return { content: text, message: `No goal #${goalId} in plan.md.`, isError: true };
if (!goal) return { content: text, message: `No goal #${goalId} in goals.md.`, isError: true };
if (outcome.kind === "verify_failed") {
const content = appendLog(text, `${when} reject #${goalId}: verify exit ${outcome.exitCode}`);
@@ -222,7 +222,7 @@ export function recordSignOff(
}
const flipped = setGoalStatus(text, goalId, "done");
const content = appendLog(flipped, `${when} signed off #${goalId}: ${goal.subject} (oracle accept)`);
return { content, message: `Signed off #${goalId}: ${goal.subject}. Marked done in plan.md.`, isError: false };
return { content, message: `Signed off #${goalId}: ${goal.subject}. Marked done in goals.md.`, isError: false };
}
/** Append one verbatim line to ## Log (creating the section if absent). The other CompleteGoal write. */
+12 -12
View File
@@ -1,8 +1,8 @@
/**
* pi-plan — all model-facing text, in flow order.
* pi-goals — all model-facing text, in flow order.
*
* Philosophy: the form guides a process; it does not police one. The agent can
* edit plan.md freely. These prompts + the plan.md structure make the right path
* edit goals.md freely. These prompts + the goals.md structure make the right path
* the easy path. The only step that is genuinely rigorous is the evidence judge
* (6), and even that is reached by guiding the agent to call CompleteGoal, not by
* trapping it. Bypasses stay visible in the git diff and the widget.
@@ -29,12 +29,12 @@
*
* System guidance for the plan-phase agent. Runs on the plan model (may differ
* from the execution model; the choice is sticky — see oracle.json-style config).
* This phase is read-only: explore, then draft goals into plan.md. No code yet.
* This phase is read-only: explore, then draft goals into goals.md. No code yet.
* The field requirements here are the whole "elicitation" — get them agreed up
* front, because the human reviews this output before any execution.
* ──────────────────────────────────────────────────────────────────────── */
export const planDrafting = `\
You are in plan mode. Explore the repository read-only, then draft goals into plan.md.
You are in plan mode. Explore the repository read-only, then draft goals into goals.md.
Do not write or run code in this phase. Produce a plan the human will review and approve.
Right-size it, don't force structure that isn't there:
@@ -47,7 +47,7 @@ Right-size it, don't force structure that isn't there:
Write the whole file in this shape:
# Plan: <the objective>
# Goals: <the objective>
## Goal: [ ] <one short imperative line>
<!-- id: <kebab-case-slug, unique> -->
@@ -71,7 +71,7 @@ Keep it lean:
- done_when is ONE concrete, checkable condition, not a paragraph, no "if wrong" clause.
The symptom of failure goes in failure_modes, not here.
- done_when names a real artifact: a file, a test result, a committed diff, a program's output.
Never write it about plan.md's own checkbox or ## Log: CompleteGoal writes those when it accepts,
Never write it about goals.md's own checkbox or ## Log: CompleteGoal writes those when it accepts,
so a done_when about them is circular and the sign-off can never pass.
- failure_modes: 0-2 terse items, only the non-obvious ways a "done" could be wrong (a
pre-mortem). If you add a verify command, one mode can be "verify passes on a gamed file".
@@ -95,13 +95,13 @@ export function planInjection(p: {
counts: { done: number; open: number };
}): string {
if (!p.activeGoal) {
return `Plan (plan.md): ${p.objective}\nNo active goal. ${p.counts.open} open, ${p.counts.done} done. Pick the next goal (set its header to [/]) or run /plan.`;
return `Goals (goals.md): ${p.objective}\nNo active goal. ${p.counts.open} open, ${p.counts.done} done. Pick the next goal (set its header to [/]) or run /goals.`;
}
const subtasks = p.activeGoal.openSubtasks.length
? p.activeGoal.openSubtasks.map((s) => ` - [ ] ${s}`).join("\n")
: " (no open subtasks)";
return `\
Plan (plan.md): ${p.objective}
Goals (goals.md): ${p.objective}
Active goal: ${p.activeGoal.subject}
done_when: ${p.activeGoal.done_when}
Open subtasks:
@@ -115,15 +115,15 @@ Progress: ${p.counts.done} done, ${p.counts.open} open.`;
*
* The typed nudge. This is both the housekeeping and the autonomy engine — it is
* what makes the process get followed without a hard gate. Fires after N
* file-modifying turns since the last plan.md update while a goal is active.
* file-modifying turns since the last goals.md update while a goal is active.
* Keep the wording stable so it doesn't thrash the cache.
* ──────────────────────────────────────────────────────────────────────── */
export const reminder = `\
<system-reminder>
Keep plan.md current as you work:
Keep goals.md current as you work:
- tasks: tick the subtasks you've finished; add any new ones you've discovered.
- log: append ONE short line to ## Log (append, don't rewrite earlier lines).
- goal: when the active goal's done_when is met, fill its evidence: block in plan.md (a "- " list
- goal: when the active goal's done_when is met, fill its evidence: block in goals.md (a "- " list
pointing at durable artifacts), then call CompleteGoal with the goal_id. Don't tick the goal's
header [x] by hand; CompleteGoal reads the evidence, runs the check, and writes [x].
- otherwise: keep working toward the active goal. Don't stop to ask unless you're genuinely
@@ -137,7 +137,7 @@ Keep plan.md current as you work:
* continue. Does not mutate the system prompt, so the cache holds.
* ──────────────────────────────────────────────────────────────────────── */
export const continuation = `\
Continue toward the active goal in plan.md. If it now meets its done_when, fill the goal's
Continue toward the active goal in goals.md. If it now meets its done_when, fill the goal's
evidence: block (durable artifacts: saved logs, committed diffs, files, not just claims) and then
call CompleteGoal with the goal_id. If you're blocked, state what's blocking it.`;
+5 -5
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { appendLog, counts, findGoal, parse, recordSignOff, setGoalStatus } from "../src/plan-file.js";
const SAMPLE = `# Plan: ship the cache layer
const SAMPLE = `# Goals: ship the cache layer
## Goal: [/] Implement cache layer
<!-- id: cache-layer-1 -->
@@ -91,7 +91,7 @@ describe("parse", () => {
describe("failure_modes vs subtask disambiguation", () => {
it("a column-0 checkbox right after failure_modes: is a SUBTASK", () => {
const doc = parse(
`# Plan: x\n\n## Goal: [ ] G\n<!-- id: g-1 -->\ndone_when: z\nfailure_modes:\n- [ ] first subtask\n- [x] second subtask\n`,
`# Goals: x\n\n## Goal: [ ] G\n<!-- id: g-1 -->\ndone_when: z\nfailure_modes:\n- [ ] first subtask\n- [x] second subtask\n`,
);
const g = findGoal(doc, "g-1");
expect(g?.failure_modes).toEqual([]);
@@ -103,7 +103,7 @@ describe("failure_modes vs subtask disambiguation", () => {
it("an indented checkbox-shaped item inside failure_modes is a FAILURE MODE", () => {
const doc = parse(
`# Plan: x\n\n## Goal: [ ] G\n<!-- id: g-2 -->\ndone_when: z\nfailure_modes:\n - [ ] prose that looks like a checkbox\n- [ ] real subtask\n`,
`# Goals: x\n\n## Goal: [ ] G\n<!-- id: g-2 -->\ndone_when: z\nfailure_modes:\n - [ ] prose that looks like a checkbox\n- [ ] real subtask\n`,
);
const g = findGoal(doc, "g-2");
expect(g?.failure_modes).toEqual(["[ ] prose that looks like a checkbox"]);
@@ -111,7 +111,7 @@ describe("failure_modes vs subtask disambiguation", () => {
});
it("a goal with no failure_modes keeps its subtasks", () => {
const doc = parse(`# Plan: x\n\n## Goal: [ ] G\n<!-- id: g-3 -->\ndone_when: z\n- [ ] only subtask\n`);
const doc = parse(`# Goals: x\n\n## Goal: [ ] G\n<!-- id: g-3 -->\ndone_when: z\n- [ ] only subtask\n`);
const g = findGoal(doc, "g-3");
expect(g?.failure_modes).toEqual([]);
expect(g?.subtasks).toEqual([{ text: "only subtask", done: false }]);
@@ -147,7 +147,7 @@ describe("the two CompleteGoal writes (minimal diff)", () => {
});
it("appendLog creates the section when absent", () => {
const noLog = "# Plan: x\n\n## Goal: [ ] y\n<!-- id: y-1 -->\ndone_when: z\n";
const noLog = "# Goals: x\n\n## Goal: [ ] y\n<!-- id: y-1 -->\ndone_when: z\n";
expect(parse(appendLog(noLog, "first entry")).log).toEqual(["- first entry"]);
});
});