@hank-warren/pi-plan-mode 1.0.0 → 1.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 ADDED
@@ -0,0 +1,51 @@
1
+ # @hank-warren/pi-plan-mode
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c40a48d: prefer `ask_user_question` for Plan-mode decision questions when it is installed
8
+
9
+ `plan_mode_question` renders through plain `ctx.ui.select` + `ctx.ui.editor`: no
10
+ previews, no notes, no tabs, no digit hotkeys. When
11
+ `@hank-warren/pi-ask-user-question` is present, Plan mode now asks through that
12
+ tool instead, which has all of them plus checkbox multi-select.
13
+
14
+ Detection is a runtime check for a tool named `ask_user_question` in the active
15
+ tool set, re-evaluated every turn — there is no dependency between the two
16
+ packages. When it is found, `plan_mode_question` is removed from the **active**
17
+ tool set so the model never sees two overlapping question tools, and the
18
+ Plan-mode system prompt names `ask_user_question` with its own bounds (1-4
19
+ questions, 2-4 options, 2-6 when `multiSelect`) and its own decline signal. The
20
+ tool stays _registered_ either way, so a historical transcript still resolves
21
+ it.
22
+
23
+ A standalone `pi-plan-mode` install is unchanged: `plan_mode_question` remains
24
+ fully functional and the prompt reads exactly as before. It is now a legacy
25
+ fallback, slated for removal in a future major.
26
+
27
+ ## 1.0.1
28
+
29
+ ### Patch Changes
30
+
31
+ - cf12677: ship CHANGELOG.md in the published tarball
32
+
33
+ ## 1.0.0
34
+
35
+ ### Major Changes
36
+
37
+ - 88572fe: Rewrite Plan mode around a durable plan file and stop managing tool permissions.
38
+
39
+ Plan mode is now a mode of intent rather than a permission system. It blocks exactly `edit`, `write`, and `update_plan` while planning and leaves every other tool as configured, so Bash and extension-tool safety stays with your permission layer (for example `@hank-warren/pi-auto-permissions`).
40
+
41
+ **Plan mode no longer calls `setActiveTools`.** It previously rewrote the session's active tool set on every turn, which stripped tools other extensions had registered — lazily connected MCP tools and on-demand channels such as pi-subagents' supervisor tool were removed mid-session and never reactivated, and extensions that manage their own tools on `before_agent_start` fought Plan mode for control. None of that can happen now.
42
+
43
+ Completed plans are written to `<agent dir>/plans/<session-id>.md`. The plan file is the plan: session state stores only its path, the file can be hand-edited, and both implementation paths read it from disk. While a plan is active the model receives a one-line pointer to the file instead of the plan body, so plans survive compaction at negligible context cost regardless of size, replacing the previous reinjection of up to 50,000 characters.
44
+
45
+ Breaking changes:
46
+
47
+ - Removed settings `defaultPlanTools`, `bashPolicy`, `safeSubcommands`, and `implementationPlanRetention`. They are now ignored rather than rejected, so existing settings files keep loading. `thinkingLevel` and `defaultPlanExportPath` are unchanged.
48
+ - Removed the pre-start tool selector and `/plan tools`.
49
+ - Removed the separate saved-plan state and `/plan save`; there is one plan per session, and `/plan exit` deletes it.
50
+ - Removed the legacy `<proposed_plan>` XML completion path; use `plan_mode_complete`.
51
+ - The implementation handoff and fresh-session transfer now reference the plan file instead of inlining the plan text.
package/README.md CHANGED
@@ -4,14 +4,14 @@
4
4
 
5
5
  `@hank-warren/pi-plan-mode` adds a `/plan` mode to Pi for research and design. You gather information, ask questions, and land on a plan — then implement it, either in the same conversation or in a fresh one.
6
6
 
7
- **Plan mode is a mode of intent, not a permission system.** It blocks `edit`, `write`, and `update_plan` while planning and leaves every other tool exactly as you configured it. Command safety stays with your permission extension (for example [`@hank-warren/pi-auto-permissions`](../pi-auto-permissions)), which already reviews Bash. Plan mode never touches the session's active tool set, so it cannot break other extensions.
7
+ **Plan mode is a mode of intent, not a permission system.** It blocks `edit`, `write`, and `update_plan` while planning and leaves every other tool exactly as you configured it. Command safety stays with your permission extension (for example [`@hank-warren/pi-auto-permissions`](../pi-auto-permissions)), which already reviews Bash. The only tool Plan mode ever removes from the active set is its own `plan_mode_question`, and only when a better questionnaire is installed (see below), so it cannot break other extensions.
8
8
 
9
9
  The plan is written to a **durable file** that survives compaction, survives resume, and can be hand-edited.
10
10
 
11
11
  ## ✨ Features
12
12
 
13
13
  - `/plan` mode with a planning system prompt: explore first, ask decision questions, do not implement.
14
- - `plan_mode_question` for structured 1-3 question decision points with options and a free-form path.
14
+ - `plan_mode_question` for structured 1-3 question decision points with options and a free-form path — or `ask_user_question` when that is installed (see below).
15
15
  - `plan_mode_complete({ plan })` writes the plan to `<agent dir>/plans/<session-id>.md`.
16
16
  - **Pointer, not payload.** An active plan adds one line to the system prompt naming the file. The plan body is never injected into context, so a 50-page plan costs the same as a one-liner and survives compaction for free.
17
17
  - Two ways to implement: continue in this conversation, or open a fresh session that reads the same file.
@@ -95,7 +95,24 @@ Plan mode blocks exactly three tools while planning: `edit`, `write`, and `updat
95
95
 
96
96
  It deliberately does **not** police Bash, subagents, MCP tools, or any other extension tool. Those decisions belong to your permission layer, which can see the whole session and judge each call. Pair Plan mode with a permission extension such as `@hank-warren/pi-auto-permissions` if you want command review during planning.
97
97
 
98
- Plan mode also never calls `setActiveTools`. Extensions that register tools lazily (MCP connections, subagent supervision channels) keep working normally, and nothing needs to be restored when Plan mode exits.
98
+ The one exception is `plan_mode_question`, which Plan mode hides from the model when a better questionnaire is installed — see below. No other tool is ever added to or removed from the active set, so extensions that register tools lazily (MCP connections, subagent supervision channels) keep working normally and nothing needs to be restored when Plan mode exits.
99
+
100
+ ## 🤝 Better questions with `pi-ask-user-question`
101
+
102
+ With [`@hank-warren/pi-ask-user-question`](../pi-ask-user-question) installed, Plan mode asks its decision questions through that tool instead:
103
+
104
+ ```bash
105
+ pi install npm:@hank-warren/pi-ask-user-question
106
+ ```
107
+
108
+ `plan_mode_question` renders through plain `ctx.ui.select` + `ctx.ui.editor`. `ask_user_question` gives the same decision points a real dialog: markdown **previews** on options, **notes** attached to a choice, several questions as **tabs** you cycle with Tab, **digit hotkeys**, and **checkbox multi-select**. It also allows 1-4 questions instead of 1-3, and 2-6 options on a multi-select question.
109
+
110
+ Detection is by tool name at runtime, re-evaluated every turn — there is no dependency between the two packages, and installing or removing one never requires touching the other. When `ask_user_question` is present:
111
+
112
+ - `plan_mode_question` is removed from the **active** tool set, so the model never sees two overlapping question tools and cannot call the weaker one. It stays *registered*, so a historical transcript still resolves it.
113
+ - The Plan-mode system prompt names `ask_user_question` and quotes its bounds and its decline signal.
114
+
115
+ A standalone `pi-plan-mode` install loses nothing: `plan_mode_question` stays fully functional and the prompt reads exactly as it always has. It is a **legacy fallback** and is slated for removal in a future major.
99
116
 
100
117
  ## 📊 Statusline
101
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-plan-mode",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Plan mode for Pi: research and design with a durable plan file that survives compaction.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -34,7 +34,8 @@
34
34
  "src",
35
35
  "README.md",
36
36
  "NOTICE.md",
37
- "LICENSE"
37
+ "LICENSE",
38
+ "CHANGELOG.md"
38
39
  ],
39
40
  "peerDependencies": {
40
41
  "@earendil-works/pi-coding-agent": "*",
package/src/plan-mode.ts CHANGED
@@ -29,7 +29,12 @@ import {
29
29
  showStoredPlan,
30
30
  updatePlanModeUi,
31
31
  } from "./presentation.js";
32
- import { buildActivePlanPointer, buildPlanModePrompt } from "./prompt.js";
32
+ import {
33
+ ASK_USER_QUESTION_TOOL,
34
+ buildActivePlanPointer,
35
+ buildPlanModePrompt,
36
+ PLAN_MODE_QUESTION_TOOL,
37
+ } from "./prompt.js";
33
38
  import {
34
39
  answerPlanModeQuestions,
35
40
  normalizePlanModeQuestionParams,
@@ -54,6 +59,41 @@ const STATE_ENTRY_TYPE = "plan-mode-state";
54
59
  */
55
60
  const BLOCKED_TOOLS = new Set(["edit", "write", "update_plan"]);
56
61
 
62
+ /**
63
+ * Which question tool the prompt should name this turn.
64
+ *
65
+ * Detection is by tool NAME only, with no dependency on
66
+ * `@hank-warren/pi-ask-user-question`: any extension registering
67
+ * `ask_user_question` is treated as the preferred implementation. It offers
68
+ * previews, notes, question tabs, digit hotkeys and checkbox multi-select;
69
+ * `plan_mode_question` renders through plain `ctx.ui.select` + `ctx.ui.editor`
70
+ * and has none of them.
71
+ *
72
+ * Evaluated per turn rather than once at mode entry, so the prompt cannot go
73
+ * stale if the tool set changes mid-session.
74
+ */
75
+ function preferredQuestionTool(pi: ExtensionAPI): string {
76
+ return pi.getActiveTools().includes(ASK_USER_QUESTION_TOOL)
77
+ ? ASK_USER_QUESTION_TOOL
78
+ : PLAN_MODE_QUESTION_TOOL;
79
+ }
80
+
81
+ /**
82
+ * Hide `plan_mode_question` from the model whenever the better tool is present,
83
+ * so it never sees two overlapping question tools and cannot call the weaker
84
+ * one. The tool stays *registered* either way, so a historical transcript still
85
+ * resolves it, and a host without `ask_user_question` keeps it fully functional.
86
+ *
87
+ * Idempotent, and writes only when something actually changes: siblings are
88
+ * untouched, and repeated `before_agent_start` events are free.
89
+ */
90
+ function reconcileQuestionTool(pi: ExtensionAPI, preferred: string): void {
91
+ if (preferred === PLAN_MODE_QUESTION_TOOL) return;
92
+ const active = pi.getActiveTools();
93
+ if (!active.includes(PLAN_MODE_QUESTION_TOOL)) return;
94
+ pi.setActiveTools(active.filter((name) => name !== PLAN_MODE_QUESTION_TOOL));
95
+ }
96
+
57
97
  type InteractiveUi = typeof import("./interactive-ui.js");
58
98
 
59
99
  interface PlanModeDependencies {
@@ -338,8 +378,13 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
338
378
  persistState();
339
379
  updateUi(ctx);
340
380
  }
381
+ // Read fresh and write immediately, exactly like pi-ask-user-question's own
382
+ // reconciler, so the two hooks converge on the same result in either
383
+ // execution order.
384
+ const questionTool = preferredQuestionTool(pi);
385
+ reconcileQuestionTool(pi, questionTool);
341
386
  if (state.enabled) {
342
- return { systemPrompt: `${event.systemPrompt}\n\n${buildPlanModePrompt()}` };
387
+ return { systemPrompt: `${event.systemPrompt}\n\n${buildPlanModePrompt(questionTool)}` };
343
388
  }
344
389
  // Pointer, not payload: an active plan costs one line of context no matter
345
390
  // how large the plan is, and survives compaction for free.
@@ -450,7 +495,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
450
495
  return;
451
496
  }
452
497
  sendPlanModeUserMessage(
453
- "Finalize the current implementation plan now. If any material decision remains, use plan_mode_question instead. Otherwise call plan_mode_complete alone as your final action with the complete decision-ready plan.",
498
+ `Finalize the current implementation plan now. If any material decision remains, use ${preferredQuestionTool(pi)} instead. Otherwise call plan_mode_complete alone as your final action with the complete decision-ready plan.`,
454
499
  ctx,
455
500
  );
456
501
  }
package/src/prompt.ts CHANGED
@@ -1,6 +1,49 @@
1
1
  const PLAN_CONTEXT_MARKER = "[PLAN MODE ACTIVE]";
2
2
 
3
- export function buildPlanModePrompt() {
3
+ /** The built-in question tool. Used whenever nothing better is installed. */
4
+ export const PLAN_MODE_QUESTION_TOOL = "plan_mode_question";
5
+ /**
6
+ * `@hank-warren/pi-ask-user-question`'s tool. Detected by name at runtime, with
7
+ * no dependency on that package: previews, notes, tabs, digit hotkeys and
8
+ * checkbox multi-select are all things `plan_mode_question` cannot offer.
9
+ */
10
+ export const ASK_USER_QUESTION_TOOL = "ask_user_question";
11
+
12
+ /**
13
+ * The bits of prompt text that differ between the two question tools. Every
14
+ * reference to a question tool in the prompt is built from one of these, so the
15
+ * prompt can never name one tool's bounds beside the other tool's name.
16
+ */
17
+ interface QuestionToolProfile {
18
+ name: string;
19
+ /** How many questions and options the tool actually accepts. */
20
+ bounds: string;
21
+ /** What the model sees when the user does not answer. */
22
+ decline: string;
23
+ }
24
+
25
+ const QUESTION_TOOL_PROFILES: Record<string, QuestionToolProfile> = {
26
+ [PLAN_MODE_QUESTION_TOOL]: {
27
+ name: PLAN_MODE_QUESTION_TOOL,
28
+ bounds: "Ask 1-3 concise questions with 2-4 meaningful options.",
29
+ decline: `If ${PLAN_MODE_QUESTION_TOOL} returns cancelled or ui_unavailable`,
30
+ },
31
+ [ASK_USER_QUESTION_TOOL]: {
32
+ name: ASK_USER_QUESTION_TOOL,
33
+ bounds:
34
+ "Ask 1-4 concise questions with 2-4 meaningful options each, or 2-6 options when the question sets multiSelect.",
35
+ decline: `If ${ASK_USER_QUESTION_TOOL} reports that the user declined to answer`,
36
+ },
37
+ };
38
+
39
+ /**
40
+ * Build the Plan-mode prompt around whichever question tool is available.
41
+ *
42
+ * The default keeps the exported function callable with no arguments and keeps
43
+ * a standalone `pi-plan-mode` install reading exactly as it did before.
44
+ */
45
+ export function buildPlanModePrompt(questionTool: string = PLAN_MODE_QUESTION_TOOL) {
46
+ const tool = QUESTION_TOOL_PROFILES[questionTool] ?? QUESTION_TOOL_PROFILES[PLAN_MODE_QUESTION_TOOL];
4
47
  return `${PLAN_CONTEXT_MARKER}
5
48
  # Plan Mode (Conversational)
6
49
 
@@ -29,14 +72,14 @@ You are in Plan Mode, a collaboration mode for producing a decision-complete imp
29
72
  ## Phase 3 — Implementation chat
30
73
 
31
74
  - Once intent is stable, keep asking until the spec is decision-complete: approach, interfaces, data flow, edge cases/failure modes, testing and acceptance criteria, and any migration or compatibility constraints.
32
- - Use plan_mode_question for important preferences, tradeoffs, or assumption locks that cannot be discovered by non-mutating exploration. Ask 1-3 concise questions with 2-4 meaningful options. Do not include filler options.
33
- - If plan_mode_question returns cancelled or ui_unavailable, do not jump straight to a final plan when the missing answer is high impact. Ask one concise plain-text question or proceed only with a clearly stated low-risk assumption.
75
+ - Use ${tool.name} for important preferences, tradeoffs, or assumption locks that cannot be discovered by non-mutating exploration. ${tool.bounds} Do not include filler options.
76
+ - ${tool.decline}, do not jump straight to a final plan when the missing answer is high impact. Ask one concise plain-text question or proceed only with a clearly stated low-risk assumption.
34
77
 
35
78
  ## Ending each turn
36
79
 
37
80
  Every Plan-mode turn that advances or finalizes the plan must end in exactly one of these ways:
38
81
 
39
- - If a material decision remains, use plan_mode_question. If interactive UI is unavailable, ask one concise plain-text question instead.
82
+ - If a material decision remains, use ${tool.name}. If interactive UI is unavailable, ask one concise plain-text question instead.
40
83
  - If the implementation plan is decision-complete, call plan_mode_complete alone as your final action. Do not call other tools in the same batch and do not emit a normal assistant response after it.
41
84
 
42
85
  If a follow-up asks only for clarification and does not change or challenge the plan, answer it directly, then call plan_mode_complete alone as the final action with the complete unchanged plan so it remains available for implementation.
@@ -57,7 +100,7 @@ Keep the plan concise, human and agent digestible, and free of open decisions. P
57
100
 
58
101
  The plan is saved to a durable file, so it survives compaction and can be re-read at any time.
59
102
 
60
- If the user requests revisions after a completed plan, the next plan_mode_complete call must contain a complete replacement, not a delta. If there is not enough information for a complete replacement, continue planning with plan_mode_question instead of calling plan_mode_complete.`;
103
+ If the user requests revisions after a completed plan, the next plan_mode_complete call must contain a complete replacement, not a delta. If there is not enough information for a complete replacement, continue planning with ${tool.name} instead of calling plan_mode_complete.`;
61
104
  }
62
105
 
63
106
  /**