summaryrefslogtreecommitdiff
path: root/pi
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-24 18:57:17 +0200
committerPaul Buetow <paul@buetow.org>2026-03-24 18:57:17 +0200
commit98d52f5c8eb5ed7242db390a0b760eb1b918997f (patch)
tree6e75a50ea877f54ae5123b5e40f7db7909f82a2d /pi
parentc1461b28a13758f3f9934cccaf9fb5b341716aa1 (diff)
plan-mode: prevent overwriting unrelated existing plan files
Each plan mode session now tracks an activePlanFile. On first write the target path is adopted as the active plan for that session. Any subsequent write or edit to a different, already-existing plan file in ~/.pi/plans is blocked with a clear message directing the agent to create a new file instead. Behaviour summary: - New file → always allowed; becomes the active plan. - Same file again → always allowed (updating the current plan). - Different existing file → blocked unless explicitly the active plan. - activePlanFile is persisted across session restores and reset to undefined whenever plan mode is entered fresh via /plan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'pi')
-rw-r--r--pi/agent/extensions/agent-plan-mode/index.ts31
1 files changed, 31 insertions, 0 deletions
diff --git a/pi/agent/extensions/agent-plan-mode/index.ts b/pi/agent/extensions/agent-plan-mode/index.ts
index 1724920..ebb02b8 100644
--- a/pi/agent/extensions/agent-plan-mode/index.ts
+++ b/pi/agent/extensions/agent-plan-mode/index.ts
@@ -1,3 +1,4 @@
+import { existsSync } from "node:fs";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
@@ -25,6 +26,8 @@ interface PlanModeState {
planItems: PlanItem[];
createdTaskUuids: string[];
normalTools: string[];
+ // Path of the plan file created or opened in the current plan mode session.
+ activePlanFile?: string;
}
interface WorkOnTasksArgs {
@@ -133,6 +136,8 @@ export default function agentPlanModeExtension(pi: ExtensionAPI): void {
let repeatedTaskLookups = new Set<string>();
// Stored so the mode:deactivate listener can update UI without a ctx parameter.
let lastCtx: ExtensionContext | undefined;
+ // Plan file created or opened in the current plan mode session; undefined until first write.
+ let activePlanFile: string | undefined;
pi.registerFlag("plan", {
description: "Start in plan mode (read-only exploration)",
@@ -331,6 +336,7 @@ export default function agentPlanModeExtension(pi: ExtensionAPI): void {
planItems,
createdTaskUuids,
normalTools,
+ activePlanFile,
});
}
@@ -377,6 +383,7 @@ export default function agentPlanModeExtension(pi: ExtensionAPI): void {
normalTools = pi.getActiveTools();
pi.setActiveTools(PLAN_MODE_TOOLS);
executionTaskUuid = undefined;
+ activePlanFile = undefined; // start fresh; no plan file committed to yet
repeatedTaskLookups.clear();
ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`);
} else {
@@ -656,6 +663,27 @@ Begin with the current focused task now. Do not re-check the task list immediate
reason: `Plan mode only allows writing files inside ${plansDir}.\nFile: ${filePath}\nCreate the directory with: mkdir -p ${plansDir}`,
};
}
+
+ // Guard against overwriting an unrelated existing plan file.
+ // A file that already exists on disk is only allowed if it is the
+ // active plan file for this session. New files are always allowed
+ // and become the active plan file.
+ if (existsSync(normalised)) {
+ if (activePlanFile && normalised !== activePlanFile) {
+ return {
+ block: true,
+ reason: `Plan mode blocked overwriting an unrelated existing plan file.\nFile: ${filePath}\nActive plan for this session: ${activePlanFile}\nCreate a new file with a different name if this is a separate plan.`,
+ };
+ }
+ // First write to an existing file in this session — adopt it as active.
+ activePlanFile = normalised;
+ persistState();
+ } else {
+ // New file: adopt it as the active plan for this session.
+ activePlanFile = normalised;
+ persistState();
+ }
+
return;
}
}
@@ -733,6 +761,8 @@ Rules:
- You may write or edit files only inside ${plansDir}. Create that directory first if it does not exist: mkdir -p ${plansDir}
- Write one plan markdown file there (e.g. ${plansDir}/<project>.md) describing the overall picture, goals, and task structure.
- Do NOT write any files inside the current project directory.
+- Do NOT overwrite an existing plan file that belongs to a different plan. If this is a new, unrelated plan, create a new file with a distinct name.
+- Once you write or open a plan file, it becomes the active plan for this session. Stick to that file unless explicitly asked to switch.
- For every task created with 'ask add', immediately annotate it with a reference to the plan file: 'ask annotate <uuid> "See ${plansDir}/<project>.md for overall context"'.
- Read existing started tasks first; if none, inspect the next READY tasks.
- Avoid duplicating tasks that already exist.
@@ -855,6 +885,7 @@ ${formatTaskDetails(currentTask)}`,
planItems = planStateEntry.data.planItems ?? planItems;
createdTaskUuids = planStateEntry.data.createdTaskUuids ?? createdTaskUuids;
normalTools = planStateEntry.data.normalTools?.length ? planStateEntry.data.normalTools : normalTools;
+ activePlanFile = planStateEntry.data.activePlanFile ?? activePlanFile;
} else {
normalTools = pi.getActiveTools();
}