@underactive/pi-topping-moa-fusion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +437 -0
  4. package/agents/mf-plan.md +43 -0
  5. package/agents/moa-debater.md +37 -0
  6. package/agents/moa-explore.md +56 -0
  7. package/agents/moa-opinion.md +29 -0
  8. package/agents/moa-proposer.md +49 -0
  9. package/agents/moa-synthesizer.md +124 -0
  10. package/agents/moa-verifier.md +67 -0
  11. package/index.ts +3 -0
  12. package/package.json +61 -0
  13. package/src/activityMeter.ts +193 -0
  14. package/src/agents/authoritative.ts +91 -0
  15. package/src/agents/defaults.ts +123 -0
  16. package/src/agents/discovery.ts +119 -0
  17. package/src/config/modelCatalogue.ts +54 -0
  18. package/src/config/planName.ts +74 -0
  19. package/src/config/rosters.ts +118 -0
  20. package/src/config/settings.ts +161 -0
  21. package/src/debate/debateContract.ts +89 -0
  22. package/src/debate/debateFanout.ts +285 -0
  23. package/src/debate/debateFile.ts +38 -0
  24. package/src/debate/debateResults.ts +115 -0
  25. package/src/debate/debateRounds.ts +61 -0
  26. package/src/debate/runDebate.ts +143 -0
  27. package/src/index.ts +283 -0
  28. package/src/moa/conflictContract.ts +49 -0
  29. package/src/moa/conflicts.ts +153 -0
  30. package/src/moa/contextContract.ts +52 -0
  31. package/src/moa/fanout.ts +152 -0
  32. package/src/moa/fanoutWiring.ts +88 -0
  33. package/src/moa/implementationRetry.ts +292 -0
  34. package/src/moa/modelRuntime.ts +87 -0
  35. package/src/moa/orchestration.ts +105 -0
  36. package/src/moa/planInfo.ts +57 -0
  37. package/src/moa/planlessRetry.ts +72 -0
  38. package/src/moa/reviewLoop.ts +170 -0
  39. package/src/moa/runContext.ts +118 -0
  40. package/src/moa/synthesis.ts +420 -0
  41. package/src/moa/verdicts.ts +81 -0
  42. package/src/moa/verification.ts +791 -0
  43. package/src/moa/verificationCriteria.ts +127 -0
  44. package/src/moa/verifyGate.ts +137 -0
  45. package/src/opinion/opinionContract.ts +21 -0
  46. package/src/opinion/opinionFanout.ts +135 -0
  47. package/src/opinion/opinionFile.ts +38 -0
  48. package/src/opinion/opinionResults.ts +73 -0
  49. package/src/opinion/runOpinion.ts +156 -0
  50. package/src/planning/askUserQuestion.ts +83 -0
  51. package/src/planning/instructions.ts +146 -0
  52. package/src/planning/modeState.ts +61 -0
  53. package/src/planning/planFile.ts +273 -0
  54. package/src/planning/planMode.ts +673 -0
  55. package/src/planning/tools/enterPlanMode.ts +165 -0
  56. package/src/planning/tools/exitPlanMode.ts +159 -0
  57. package/src/planning/tools/mfPlanSubagent.ts +311 -0
  58. package/src/planning/tools/shared.ts +19 -0
  59. package/src/planning/tools/writePlan.ts +33 -0
  60. package/src/runtime/activityTracking.ts +141 -0
  61. package/src/runtime/cancelRun.ts +134 -0
  62. package/src/runtime/mutationTripwire.ts +251 -0
  63. package/src/runtime/processPool.ts +55 -0
  64. package/src/runtime/results.ts +103 -0
  65. package/src/runtime/runner.ts +538 -0
  66. package/src/runtime/wire.ts +177 -0
  67. package/src/shared/functionKeys.ts +30 -0
  68. package/src/shared/modelRefs.ts +91 -0
  69. package/src/ui/agentStatus.ts +84 -0
  70. package/src/ui/agentTranscript.ts +112 -0
  71. package/src/ui/cancelOverlay.ts +191 -0
  72. package/src/ui/chrome.ts +151 -0
  73. package/src/ui/conflictOverlay.ts +363 -0
  74. package/src/ui/debateModelPicker.ts +273 -0
  75. package/src/ui/menu.ts +679 -0
  76. package/src/ui/moaModelPicker.ts +900 -0
  77. package/src/ui/moaProgressWidget.ts +910 -0
  78. package/src/ui/moaSetupOverlay.ts +368 -0
  79. package/src/ui/modelLabel.ts +61 -0
  80. package/src/ui/observeOverlay.ts +206 -0
  81. package/src/ui/opinionModelPicker.ts +246 -0
  82. package/src/ui/planReviewOverlay.ts +315 -0
  83. package/src/ui/promptEditor.ts +87 -0
  84. package/src/ui/rosterEditor.ts +310 -0
  85. package/src/ui/shimmer.ts +77 -0
  86. package/src/ui/toolActivity.ts +35 -0
  87. package/src/ui/twoPaneModelThinking.ts +272 -0
  88. package/src/ui/verificationFindingsOverlay.ts +137 -0
@@ -0,0 +1,156 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { isKeyRelease, Key, matchesKey } from "@earendil-works/pi-tui";
3
+
4
+ import { installShippedAgents, shippedAgentsDir, withAuthoritativeMoaAgents } from "../agents/authoritative.ts";
5
+ import { discoverAgents } from "../agents/discovery.ts";
6
+ import { summarizePlanPromptName } from "../config/planName.ts";
7
+ import { loadMoaConfig, saveMoaConfig } from "../config/settings.ts";
8
+ import { resolveContextWindow } from "../moa/modelRuntime.ts";
9
+ import type { CancelSession } from "../runtime/cancelRun.ts";
10
+ import { formatMutationWarning, MutationTripwire } from "../runtime/mutationTripwire.ts";
11
+ import { type SendUserMessageOptions, type ThinkingLevel } from "../shared/modelRefs.ts";
12
+ import { MoaProgressWidget } from "../ui/moaProgressWidget.ts";
13
+ import type { ObserveSession } from "../ui/observeOverlay.ts";
14
+ import { showOpinionModelPicker } from "../ui/opinionModelPicker.ts";
15
+ import { showPromptEditor } from "../ui/promptEditor.ts";
16
+ import { runOpinionFanout } from "./opinionFanout.ts";
17
+ import { repoOpinionDisplayPath, saveRepoOpinionFile } from "./opinionFile.ts";
18
+ import { collectOpinionOutcomes, formatOpinionsMarkdown } from "./opinionResults.ts";
19
+
20
+ export interface OpinionHost {
21
+ currentThinkingLevel(): ThinkingLevel;
22
+ getActiveCancelSession(): CancelSession | undefined;
23
+ setActiveCancelSession(session: CancelSession | undefined): void;
24
+ openCancelOverlayIfActive(ctx: ExtensionContext): boolean;
25
+ getActiveObserveSession(): ObserveSession | undefined;
26
+ setActiveObserveSession(session: ObserveSession | undefined): void;
27
+ setRunningProgressWidget(widget: MoaProgressWidget | undefined): void;
28
+ }
29
+
30
+ export async function runInteractiveOpinion(
31
+ pi: ExtensionAPI,
32
+ host: OpinionHost,
33
+ ctx: ExtensionContext,
34
+ initialPrompt?: string,
35
+ ): Promise<void> {
36
+ if (host.getActiveCancelSession()?.run) {
37
+ ctx.ui.notify("A plan or opinion run is already in progress.", "warning");
38
+ return;
39
+ }
40
+ if (!ctx.hasUI) {
41
+ ctx.ui.notify("/mf-opinion requires an interactive session.", "warning");
42
+ return;
43
+ }
44
+
45
+ installShippedAgents();
46
+ const session: CancelSession = { title: "Opinion agents", run: undefined, overlayOpen: false };
47
+ host.setActiveCancelSession(session);
48
+ const unsubscribeEsc = ctx.mode === "tui"
49
+ ? ctx.ui.onTerminalInput((data) => {
50
+ if (isKeyRelease(data) || !matchesKey(data, Key.escape)) return undefined;
51
+ if (host.getActiveObserveSession()?.overlayOpen) return undefined;
52
+ if (session.overlayOpen) return undefined;
53
+ if (!session.run) return undefined;
54
+ return host.openCancelOverlayIfActive(ctx) ? { consume: true } : undefined;
55
+ })
56
+ : undefined;
57
+
58
+ try {
59
+ let prefill = initialPrompt?.trim() ?? "";
60
+ let skipEditorOnce = Boolean(prefill);
61
+ while (true) {
62
+ let question: string | undefined;
63
+ if (skipEditorOnce) {
64
+ question = prefill;
65
+ skipEditorOnce = false;
66
+ } else {
67
+ question = await showPromptEditor(ctx, "Opinion — ask a question about this repo", prefill);
68
+ }
69
+ if (!question?.trim()) {
70
+ ctx.ui.notify("Opinion cancelled.");
71
+ return;
72
+ }
73
+ question = question.trim();
74
+ prefill = question;
75
+
76
+ const selection = await showOpinionModelPicker(ctx, host.currentThinkingLevel());
77
+ if (!selection) return;
78
+ const { models, thinking, thinkingSelections } = selection;
79
+ const settings = loadMoaConfig();
80
+ saveMoaConfig({
81
+ ...settings,
82
+ opinionModels: models,
83
+ thinkingOverrides: { ...settings.thinkingOverrides, ...thinkingSelections },
84
+ });
85
+
86
+ const slug = await summarizePlanPromptName(ctx, question);
87
+ try {
88
+ saveRepoOpinionFile(question, ctx.cwd, slug, "opinion-prompt");
89
+ } catch {
90
+ ctx.ui.notify("Could not save the opinion prompt artifact.", "warning");
91
+ }
92
+
93
+ const closeStacked = () => {
94
+ session.closeOverlay?.();
95
+ host.getActiveObserveSession()?.closeOverlay?.();
96
+ };
97
+ const widget = new MoaProgressWidget(
98
+ ctx,
99
+ (ref) => resolveContextWindow(ctx, ref),
100
+ {
101
+ closeStacked,
102
+ title: "MoA Opinion",
103
+ phaseLabels: { Plan: "Opinion" },
104
+ fanoutWorkingText: "analyzing & answering",
105
+ },
106
+ slug,
107
+ );
108
+ const agents = withAuthoritativeMoaAgents(
109
+ discoverAgents(ctx.cwd, "user").agents,
110
+ shippedAgentsDir(),
111
+ );
112
+ const tripwire = new MutationTripwire();
113
+ await tripwire.arm(ctx.cwd);
114
+ host.setRunningProgressWidget(widget);
115
+ let outcome;
116
+ try {
117
+ outcome = await runOpinionFanout({ host, ctx, question, models, thinking, session, widget, agents });
118
+ } finally {
119
+ host.setRunningProgressWidget(undefined);
120
+ widget.stopWidget();
121
+ const changed = await tripwire.check(ctx.cwd);
122
+ if (changed.length > 0) {
123
+ ctx.ui.notify(formatMutationWarning("MoA opinion fan-out", changed), "warning");
124
+ }
125
+ }
126
+
127
+ if (outcome.status === "cancelled") {
128
+ session.closeOverlay?.();
129
+ ctx.ui.notify("Opinion run cancelled.");
130
+ continue;
131
+ }
132
+
133
+ const outcomes = collectOpinionOutcomes(models, thinking, outcome.results);
134
+ const markdown = formatOpinionsMarkdown(question, outcomes, slug);
135
+ const displayPath = repoOpinionDisplayPath(slug, "opinions");
136
+ let saved = true;
137
+ try {
138
+ saveRepoOpinionFile(markdown, ctx.cwd, slug, "opinions");
139
+ } catch {
140
+ saved = false;
141
+ }
142
+ const appendOnly: SendUserMessageOptions = { triggerTurn: false };
143
+ await pi.sendUserMessage(markdown, appendOnly);
144
+ const completed = outcomes.filter((item) => item.status === "done").length;
145
+ const suffix = saved ? ` · saved to ${displayPath}` : " · artifact save failed";
146
+ ctx.ui.notify(
147
+ `${completed} of ${outcomes.length} opinions ready${suffix}`,
148
+ completed === 0 ? "error" : saved ? "info" : "warning",
149
+ );
150
+ return;
151
+ }
152
+ } finally {
153
+ unsubscribeEsc?.();
154
+ host.setActiveCancelSession(undefined);
155
+ }
156
+ }
@@ -0,0 +1,83 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ /**
4
+ * The only model-facing question tool mf-plan defers to. Whatever extension
5
+ * registers this name is the questionnaire; mf-plan never vendors or registers
6
+ * a competing one. Name-only detection is deliberate: attributing the
7
+ * registration to a specific package would silently disable the tool if that
8
+ * package ever ships under a different path or is vendored.
9
+ */
10
+ export const ASK_USER_QUESTION_TOOL_NAME = "ask_user_question";
11
+
12
+ /**
13
+ * The rpiv questionnaire emits `{ active: boolean }` on this channel around
14
+ * every wait, cleared in a `finally`. The channel name here duplicates the
15
+ * rpiv source rather than importing the package — it may be absent from the
16
+ * session. Channel names are immutable and payloads are append-only by that
17
+ * extension's policy, so a rename is a new channel and added fields are inert
18
+ * (the handler validates the payload and ignores anything else).
19
+ *
20
+ * Referenced source: `@juicesharp/rpiv-ask-user-question/events.ts` (channel
21
+ * names + `{ active: boolean }`) and `ask-user-question.ts` (emit sites).
22
+ */
23
+ export const ASK_USER_BLOCKED_EVENT = "rpiv:ask-user:blocked";
24
+
25
+ /** Returned by `exit_plan_mode` while a questionnaire is still awaiting answers. */
26
+ export const ASK_USER_QUESTION_PENDING_MESSAGE =
27
+ "An ask_user_question questionnaire is still waiting for the user. Wait for its result, incorporate the answers, then call exit_plan_mode by itself in a later message.";
28
+
29
+ /** True when any registered tool answers to the questionnaire name. */
30
+ export function isAskUserQuestionInstalled(pi: ExtensionAPI): boolean {
31
+ return pi.getAllTools().some((tool) => tool.name === ASK_USER_QUESTION_TOOL_NAME);
32
+ }
33
+
34
+ export interface AskUserQuestionTracker {
35
+ /** True while a questionnaire is waiting for the user (rpiv `active: true`). */
36
+ isActive(): boolean;
37
+ /** Clear the blocked flag (e.g. on session start, so a dead process cannot leave it stuck). */
38
+ reset(): void;
39
+ /**
40
+ * (Re)subscribe to the blocked event. Called on session start and on each
41
+ * plan-mode entry; the unsubscribe handle is retained so re-subscribing
42
+ * never stacks handlers.
43
+ */
44
+ ensureSubscribed(): void;
45
+ }
46
+
47
+ /**
48
+ * Tracks whether the questionnaire is actively blocking the user. The raw
49
+ * `active` payload is validated (`typeof === "boolean"`) so unknown shapes are
50
+ * ignored; the flag then only ever moves when rpiv reports a precise state.
51
+ *
52
+ * Owned by the plan-mode controller (module-singleton discipline is avoided so
53
+ * the fake-`pi` tests stay self-contained and no state leaks across sessions).
54
+ */
55
+ export function createAskUserQuestionTracker(pi: ExtensionAPI, onChange?: () => void): AskUserQuestionTracker {
56
+ let active = false;
57
+ let unsubscribe: (() => void) | undefined;
58
+
59
+ const handleBlocked = (data: unknown): void => {
60
+ const payload = data as { active?: unknown } | null;
61
+ if (typeof payload?.active !== "boolean" || payload.active === active) return;
62
+ active = payload.active;
63
+ onChange?.();
64
+ };
65
+
66
+ const ensureSubscribed = (): void => {
67
+ if (unsubscribe) return;
68
+ // `events` is typed non-optional on ExtensionAPI, but is optional at
69
+ // runtime in the fake-`pi` tests and the headless fixtures. Keep the
70
+ // optional chain so a missing bus degrades to "never blocked".
71
+ unsubscribe = pi.events?.on?.(ASK_USER_BLOCKED_EVENT, handleBlocked);
72
+ };
73
+
74
+ return {
75
+ isActive: () => active,
76
+ reset: () => {
77
+ if (!active) return;
78
+ active = false;
79
+ onChange?.();
80
+ },
81
+ ensureSubscribed,
82
+ };
83
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The 5-phase plan-mode instructions injected every turn.
3
+ * Faithful port of Claude Code's getPlanModeV2Instructions from messages.ts.
4
+ */
5
+
6
+ import { getPlanFilePath, getPlan } from "./planFile.ts";
7
+
8
+ // Agent counts — bumpable constants (Claude Code uses tier-based; we hardcode)
9
+ export const EXPLORE_AGENT_COUNT = 3;
10
+ export const PLAN_AGENT_COUNT = 1;
11
+
12
+ /**
13
+ * Build the plan-mode system reminder injected every turn.
14
+ *
15
+ * `askUserQuestionAvailable` is re-probed on each entry so the model is never
16
+ * told to call a tool that is not registered: when no extension provides
17
+ * `ask_user_question` (the tool is only active when e.g. rpiv-ask-user-question
18
+ * registers it), the instructions ask for clarification in plain text instead.
19
+ */
20
+ export function buildPlanModeInstructions(askUserQuestionAvailable: boolean): string {
21
+ const planFilePath = getPlanFilePath();
22
+ const planExists = getPlan() !== null;
23
+
24
+ const planFileInfo = planExists
25
+ ? `A plan file already exists at ${planFilePath}. You can read it with the read tool, then rewrite it with additions using the write_plan tool (which replaces the entire file, so include all content).`
26
+ : `No plan file exists yet. You should create your plan at ${planFilePath} using the write_plan tool.`;
27
+
28
+ const phase3Clarify = askUserQuestionAvailable
29
+ ? "3. Use ask_user_question to clarify any remaining questions with the user"
30
+ : "3. Ask the user directly in your reply to clarify any remaining questions";
31
+
32
+ const phase5Stop = askUserQuestionAvailable
33
+ ? "This is critical - your turn should only end with either using the ask_user_question tool OR calling exit_plan_mode. Do not stop unless it's for these 2 reasons."
34
+ : "This is critical - your turn should only end with either a plain-text clarifying question OR calling exit_plan_mode. Do not stop unless it's for these 2 reasons.";
35
+
36
+ const importantBlock = askUserQuestionAvailable
37
+ ? `**Important:** Use ask_user_question ONLY to clarify requirements or choose between approaches. Use exit_plan_mode to request plan approval. Do NOT ask about plan approval in any other way - no text questions, no ask_user_question. Phrases like "Is this plan okay?", "Should I proceed?", "How does this plan look?", "Any changes before we start?", or similar MUST use exit_plan_mode.
38
+
39
+ Call ask_user_question by itself — never in the same message as exit_plan_mode, mf_plan_subagent, write_plan, or any other tool. End your turn after calling it and wait for the answers.`
40
+ : `**Important:** Use exit_plan_mode to request plan approval. Do NOT ask about plan approval in any other way - no text questions. Phrases like "Is this plan okay?", "Should I proceed?", "How does this plan look?", "Any changes before we start?", or similar MUST use exit_plan_mode.`;
41
+
42
+ const closingNote = askUserQuestionAvailable
43
+ ? "NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications using the ask_user_question tool. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins."
44
+ : "NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications directly in your reply. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.";
45
+
46
+ return `[PLAN MODE ACTIVE]
47
+ Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
48
+
49
+ ## Plan File Info:
50
+ ${planFileInfo}
51
+ You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
52
+
53
+ ## Plan Workflow
54
+
55
+ ### Phase 1: Initial Understanding
56
+ Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions.
57
+
58
+ 1. Focus on understanding the user's request and the code associated with their request. Actively search for existing functions, utilities, and patterns that can be reused — avoid proposing new code when suitable implementations already exist.
59
+
60
+ 2. **Launch up to ${EXPLORE_AGENT_COUNT} explore agents IN PARALLEL** using the mf_plan_subagent tool (parallel mode, single message, multiple tool calls) to efficiently explore the codebase.
61
+ - Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
62
+ - Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
63
+ - Quality over quantity - ${EXPLORE_AGENT_COUNT} agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
64
+ - If using multiple agents: Provide each agent with a specific search focus or area to explore.
65
+
66
+ Example parallel launch (single message, multiple tool calls):
67
+ \`\`\`
68
+ mf_plan_subagent({ tasks: [
69
+ { agent: "moa-explore", task: "Find all authentication-related modules and their patterns" },
70
+ { agent: "moa-explore", task: "Search for existing middleware and hook patterns in the codebase" },
71
+ { agent: "moa-explore", task: "Find test files and testing patterns used in this project" }
72
+ ] })
73
+ \`\`\`
74
+
75
+ ### Phase 2: Design
76
+ Goal: Design an implementation approach.
77
+
78
+ Launch \`mf-plan\` agent(s) using the mf_plan_subagent tool to design the implementation based on the user's intent and your exploration results from Phase 1.
79
+
80
+ You can launch up to ${PLAN_AGENT_COUNT} agent(s) in parallel.
81
+
82
+ **Guidelines:**
83
+ - **Default**: Launch at least 1 plan agent for most tasks - it helps validate your understanding and consider alternatives
84
+ - **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
85
+
86
+ In the agent prompt:
87
+ - Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
88
+ - Describe requirements and constraints
89
+ - Request a detailed implementation plan
90
+
91
+ ### Phase 3: Review
92
+ Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
93
+ 1. Read the critical files identified by agents to deepen your understanding
94
+ 2. Ensure that the plans align with the user's original request
95
+ ${phase3Clarify}
96
+
97
+ ### Phase 4: Final Plan
98
+ Goal: Write your final plan to the plan file (the only file you can edit).
99
+ - Begin with a **Context** section: explain why this change is being made — the problem or need it addresses, what prompted it, and the intended outcome
100
+ - Include only your recommended approach, not all alternatives
101
+ - Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
102
+ - Include the paths of critical files to be modified
103
+ - Reference existing functions and utilities you found that should be reused, with their file paths
104
+ - Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
105
+
106
+ ### Phase 5: Call exit_plan_mode
107
+ At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call exit_plan_mode to indicate to the user that you are done planning.
108
+ ${phase5Stop}
109
+
110
+ ${importantBlock}
111
+
112
+ ${closingNote}`;
113
+ }
114
+
115
+ /** Build the re-entry reminder when a plan file already exists. */
116
+ export function buildPlanModeReentryInstructions(): string {
117
+ const planFilePath = getPlanFilePath();
118
+
119
+ return `[PLAN MODE RE-ENTRY]
120
+ ## Re-entering Plan Mode
121
+
122
+ You are returning to plan mode after having previously exited it. A plan file exists at ${planFilePath} from your previous planning session.
123
+
124
+ **Before proceeding with any new planning, you should:**
125
+ 1. Read the existing plan file to understand what was previously planned
126
+ 2. Evaluate the user's current request against that plan
127
+ 3. Decide how to proceed:
128
+ - **Different task**: If the user's request is for a different task—even if it's similar or related—start fresh by overwriting the existing plan
129
+ - **Same task, continuing**: If this is explicitly a continuation or refinement of the exact same task, modify the existing plan while cleaning up outdated or irrelevant sections
130
+ 4. Continue on with the plan process and most importantly you should always edit the plan file one way or the other before calling exit_plan_mode
131
+
132
+ Treat this as a fresh planning session. Do not assume the existing plan is relevant without evaluating it first.`;
133
+ }
134
+
135
+ /** Build the exit reminder injected once after plan mode is turned off. */
136
+ export function buildPlanModeExitInstructions(): string {
137
+ const planFilePath = getPlanFilePath();
138
+ const planExists = getPlan() !== null;
139
+ const planReference = planExists
140
+ ? ` The plan file is located at ${planFilePath} if you need to reference it.`
141
+ : "";
142
+
143
+ return `## Exited Plan Mode
144
+
145
+ You have exited plan mode. You can now make edits, run tools, and take actions.${planReference}`;
146
+ }
@@ -0,0 +1,61 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import type { MfPlanInfo } from "../moa/planInfo.ts";
4
+ import type { ImplementationHandoff } from "../moa/implementationRetry.ts";
5
+
6
+ export interface PlanModeState {
7
+ enabled: boolean;
8
+ slug: string;
9
+ repoPlanSlug?: string;
10
+ needsExitReminder?: boolean;
11
+ moaInfo?: MfPlanInfo;
12
+ implementationHandoff?: ImplementationHandoff;
13
+ }
14
+
15
+ export const MAX_PERSISTED_STATE_BYTES = 512 * 1024;
16
+
17
+ export function serializePlanModeState(state: PlanModeState): { state: PlanModeState; serialized: string } {
18
+ let persisted = state.moaInfo?.proposerPlans
19
+ ? { ...state, moaInfo: { ...state.moaInfo, proposerPlans: undefined } }
20
+ : state;
21
+ let serialized = JSON.stringify(persisted);
22
+ if (Buffer.byteLength(serialized, "utf8") > MAX_PERSISTED_STATE_BYTES && persisted.moaInfo) {
23
+ persisted = { ...persisted, moaInfo: undefined };
24
+ serialized = JSON.stringify(persisted);
25
+ }
26
+ if (Buffer.byteLength(serialized, "utf8") > MAX_PERSISTED_STATE_BYTES && persisted.implementationHandoff?.plan) {
27
+ persisted = {
28
+ ...persisted,
29
+ implementationHandoff: { ...persisted.implementationHandoff, plan: "" },
30
+ };
31
+ serialized = JSON.stringify(persisted);
32
+ }
33
+ return { state: persisted, serialized };
34
+ }
35
+
36
+ export class PlanModeStatePersistence {
37
+ private lastPersistedState: string | undefined;
38
+ private readonly pi: ExtensionAPI;
39
+
40
+ constructor(pi: ExtensionAPI) {
41
+ this.pi = pi;
42
+ }
43
+
44
+ persist(state: PlanModeState): void {
45
+ const persisted = serializePlanModeState(state);
46
+ if (persisted.serialized === this.lastPersistedState) return;
47
+ this.lastPersistedState = persisted.serialized;
48
+ this.pi.appendEntry("mf-plan", persisted.state);
49
+ }
50
+
51
+ reset(): void {
52
+ this.lastPersistedState = undefined;
53
+ }
54
+ }
55
+
56
+ export function latestPlanModeStateEntry(entries: readonly { type: string; customType?: string; data?: unknown }[]): PlanModeState | undefined {
57
+ const entry = entries
58
+ .filter((candidate) => candidate.type === "custom" && candidate.customType === "mf-plan")
59
+ .pop();
60
+ return entry?.data && typeof entry.data === "object" ? entry.data as PlanModeState : undefined;
61
+ }