@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.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +437 -0
- package/agents/mf-plan.md +43 -0
- package/agents/moa-debater.md +37 -0
- package/agents/moa-explore.md +56 -0
- package/agents/moa-opinion.md +29 -0
- package/agents/moa-proposer.md +49 -0
- package/agents/moa-synthesizer.md +124 -0
- package/agents/moa-verifier.md +67 -0
- package/index.ts +3 -0
- package/package.json +61 -0
- package/src/activityMeter.ts +193 -0
- package/src/agents/authoritative.ts +91 -0
- package/src/agents/defaults.ts +123 -0
- package/src/agents/discovery.ts +119 -0
- package/src/config/modelCatalogue.ts +54 -0
- package/src/config/planName.ts +74 -0
- package/src/config/rosters.ts +118 -0
- package/src/config/settings.ts +161 -0
- package/src/debate/debateContract.ts +89 -0
- package/src/debate/debateFanout.ts +285 -0
- package/src/debate/debateFile.ts +38 -0
- package/src/debate/debateResults.ts +115 -0
- package/src/debate/debateRounds.ts +61 -0
- package/src/debate/runDebate.ts +143 -0
- package/src/index.ts +283 -0
- package/src/moa/conflictContract.ts +49 -0
- package/src/moa/conflicts.ts +153 -0
- package/src/moa/contextContract.ts +52 -0
- package/src/moa/fanout.ts +152 -0
- package/src/moa/fanoutWiring.ts +88 -0
- package/src/moa/implementationRetry.ts +292 -0
- package/src/moa/modelRuntime.ts +87 -0
- package/src/moa/orchestration.ts +105 -0
- package/src/moa/planInfo.ts +57 -0
- package/src/moa/planlessRetry.ts +72 -0
- package/src/moa/reviewLoop.ts +170 -0
- package/src/moa/runContext.ts +118 -0
- package/src/moa/synthesis.ts +420 -0
- package/src/moa/verdicts.ts +81 -0
- package/src/moa/verification.ts +791 -0
- package/src/moa/verificationCriteria.ts +127 -0
- package/src/moa/verifyGate.ts +137 -0
- package/src/opinion/opinionContract.ts +21 -0
- package/src/opinion/opinionFanout.ts +135 -0
- package/src/opinion/opinionFile.ts +38 -0
- package/src/opinion/opinionResults.ts +73 -0
- package/src/opinion/runOpinion.ts +156 -0
- package/src/planning/askUserQuestion.ts +83 -0
- package/src/planning/instructions.ts +146 -0
- package/src/planning/modeState.ts +61 -0
- package/src/planning/planFile.ts +273 -0
- package/src/planning/planMode.ts +673 -0
- package/src/planning/tools/enterPlanMode.ts +165 -0
- package/src/planning/tools/exitPlanMode.ts +159 -0
- package/src/planning/tools/mfPlanSubagent.ts +311 -0
- package/src/planning/tools/shared.ts +19 -0
- package/src/planning/tools/writePlan.ts +33 -0
- package/src/runtime/activityTracking.ts +141 -0
- package/src/runtime/cancelRun.ts +134 -0
- package/src/runtime/mutationTripwire.ts +251 -0
- package/src/runtime/processPool.ts +55 -0
- package/src/runtime/results.ts +103 -0
- package/src/runtime/runner.ts +538 -0
- package/src/runtime/wire.ts +177 -0
- package/src/shared/functionKeys.ts +30 -0
- package/src/shared/modelRefs.ts +91 -0
- package/src/ui/agentStatus.ts +84 -0
- package/src/ui/agentTranscript.ts +112 -0
- package/src/ui/cancelOverlay.ts +191 -0
- package/src/ui/chrome.ts +151 -0
- package/src/ui/conflictOverlay.ts +363 -0
- package/src/ui/debateModelPicker.ts +273 -0
- package/src/ui/menu.ts +679 -0
- package/src/ui/moaModelPicker.ts +900 -0
- package/src/ui/moaProgressWidget.ts +910 -0
- package/src/ui/moaSetupOverlay.ts +368 -0
- package/src/ui/modelLabel.ts +61 -0
- package/src/ui/observeOverlay.ts +206 -0
- package/src/ui/opinionModelPicker.ts +246 -0
- package/src/ui/planReviewOverlay.ts +315 -0
- package/src/ui/promptEditor.ts +87 -0
- package/src/ui/rosterEditor.ts +310 -0
- package/src/ui/shimmer.ts +77 -0
- package/src/ui/toolActivity.ts +35 -0
- package/src/ui/twoPaneModelThinking.ts +272 -0
- 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
|
+
}
|