@astralyn/pi 0.82.2 → 0.82.4

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 (172) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +70 -634
  3. package/dist/cli/config-selector.d.ts.map +1 -1
  4. package/dist/cli/config-selector.js +3 -2
  5. package/dist/cli/config-selector.js.map +1 -1
  6. package/dist/cli/startup-ui.d.ts.map +1 -1
  7. package/dist/cli/startup-ui.js +3 -2
  8. package/dist/cli/startup-ui.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +2 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +31 -12
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/extensions/index.d.ts +1 -1
  14. package/dist/core/extensions/index.d.ts.map +1 -1
  15. package/dist/core/extensions/index.js.map +1 -1
  16. package/dist/core/extensions/types.d.ts +11 -0
  17. package/dist/core/extensions/types.d.ts.map +1 -1
  18. package/dist/core/extensions/types.js.map +1 -1
  19. package/dist/core/model-resolver.d.ts +1 -0
  20. package/dist/core/model-resolver.d.ts.map +1 -1
  21. package/dist/core/model-resolver.js +13 -3
  22. package/dist/core/model-resolver.js.map +1 -1
  23. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  24. package/dist/core/remote-catalog-provider.js +19 -1
  25. package/dist/core/remote-catalog-provider.js.map +1 -1
  26. package/dist/core/resource-loader.d.ts.map +1 -1
  27. package/dist/core/resource-loader.js +3 -0
  28. package/dist/core/resource-loader.js.map +1 -1
  29. package/dist/extensions/index.d.ts.map +1 -1
  30. package/dist/extensions/index.js +2 -0
  31. package/dist/extensions/index.js.map +1 -1
  32. package/dist/extensions/llama/provider.d.ts.map +1 -1
  33. package/dist/extensions/llama/provider.js +6 -0
  34. package/dist/extensions/llama/provider.js.map +1 -1
  35. package/dist/extensions/plan/constants.d.ts +31 -0
  36. package/dist/extensions/plan/constants.d.ts.map +1 -0
  37. package/dist/extensions/plan/constants.js +57 -0
  38. package/dist/extensions/plan/constants.js.map +1 -0
  39. package/dist/extensions/plan/index.d.ts +21 -0
  40. package/dist/extensions/plan/index.d.ts.map +1 -0
  41. package/dist/extensions/plan/index.js +291 -0
  42. package/dist/extensions/plan/index.js.map +1 -0
  43. package/dist/extensions/plan/schema.d.ts +15 -0
  44. package/dist/extensions/plan/schema.d.ts.map +1 -0
  45. package/dist/extensions/plan/schema.js +13 -0
  46. package/dist/extensions/plan/schema.js.map +1 -0
  47. package/dist/extensions/plan/state.d.ts +29 -0
  48. package/dist/extensions/plan/state.d.ts.map +1 -0
  49. package/dist/extensions/plan/state.js +74 -0
  50. package/dist/extensions/plan/state.js.map +1 -0
  51. package/dist/extensions/plan/storage.d.ts +18 -0
  52. package/dist/extensions/plan/storage.d.ts.map +1 -0
  53. package/dist/extensions/plan/storage.js +58 -0
  54. package/dist/extensions/plan/storage.js.map +1 -0
  55. package/dist/extensions/rewind/anchor.d.ts +29 -0
  56. package/dist/extensions/rewind/anchor.d.ts.map +1 -0
  57. package/dist/extensions/rewind/anchor.js +35 -0
  58. package/dist/extensions/rewind/anchor.js.map +1 -0
  59. package/dist/extensions/rewind/engine.d.ts +2 -6
  60. package/dist/extensions/rewind/engine.d.ts.map +1 -1
  61. package/dist/extensions/rewind/engine.js +41 -10
  62. package/dist/extensions/rewind/engine.js.map +1 -1
  63. package/dist/extensions/rewind/index.d.ts.map +1 -1
  64. package/dist/extensions/rewind/index.js +55 -21
  65. package/dist/extensions/rewind/index.js.map +1 -1
  66. package/dist/extensions/rewind/menu.d.ts.map +1 -1
  67. package/dist/extensions/rewind/menu.js +7 -2
  68. package/dist/extensions/rewind/menu.js.map +1 -1
  69. package/dist/extensions/rewind/restore.d.ts +26 -9
  70. package/dist/extensions/rewind/restore.d.ts.map +1 -1
  71. package/dist/extensions/rewind/restore.js +58 -16
  72. package/dist/extensions/rewind/restore.js.map +1 -1
  73. package/dist/extensions/subagent/activity.d.ts +1 -0
  74. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  75. package/dist/extensions/subagent/activity.js +20 -4
  76. package/dist/extensions/subagent/activity.js.map +1 -1
  77. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  78. package/dist/extensions/subagent/agents.js +2 -13
  79. package/dist/extensions/subagent/agents.js.map +1 -1
  80. package/dist/extensions/subagent/constants.d.ts +4 -3
  81. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  82. package/dist/extensions/subagent/constants.js +9 -3
  83. package/dist/extensions/subagent/constants.js.map +1 -1
  84. package/dist/extensions/subagent/index.d.ts.map +1 -1
  85. package/dist/extensions/subagent/index.js +12 -5
  86. package/dist/extensions/subagent/index.js.map +1 -1
  87. package/dist/extensions/subagent/render.d.ts.map +1 -1
  88. package/dist/extensions/subagent/render.js +166 -41
  89. package/dist/extensions/subagent/render.js.map +1 -1
  90. package/dist/extensions/subagent/resolve.d.ts +2 -5
  91. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  92. package/dist/extensions/subagent/resolve.js +8 -13
  93. package/dist/extensions/subagent/resolve.js.map +1 -1
  94. package/dist/extensions/subagent/runner.d.ts +3 -1
  95. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  96. package/dist/extensions/subagent/runner.js +111 -66
  97. package/dist/extensions/subagent/runner.js.map +1 -1
  98. package/dist/extensions/subagent/schema.d.ts +0 -6
  99. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  100. package/dist/extensions/subagent/schema.js +2 -3
  101. package/dist/extensions/subagent/schema.js.map +1 -1
  102. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  103. package/dist/extensions/subagent/sdk-runner.js +32 -16
  104. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  105. package/dist/extensions/subagent/types.d.ts +3 -2
  106. package/dist/extensions/subagent/types.d.ts.map +1 -1
  107. package/dist/extensions/subagent/types.js.map +1 -1
  108. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  109. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/custom-message.js +10 -2
  111. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  112. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  114. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  115. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  116. package/dist/modes/interactive/interactive-mode.js +31 -18
  117. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  118. package/dist/utils/coalescing-terminal.d.ts +62 -0
  119. package/dist/utils/coalescing-terminal.d.ts.map +1 -0
  120. package/dist/utils/coalescing-terminal.js +151 -0
  121. package/dist/utils/coalescing-terminal.js.map +1 -0
  122. package/docs/bundled/README.md +38 -0
  123. package/docs/bundled/extensions/README.md +19 -0
  124. package/docs/bundled/extensions/plan.md +31 -0
  125. package/docs/{distribution → bundled}/extensions/question.md +0 -11
  126. package/docs/{distribution → bundled}/extensions/rewind.md +19 -18
  127. package/docs/{distribution → bundled}/extensions/router.md +1 -14
  128. package/docs/{distribution → bundled}/extensions/statusline.md +1 -2
  129. package/docs/{distribution → bundled}/extensions/subagent.md +15 -22
  130. package/docs/bundled/extensions/todo.md +28 -0
  131. package/docs/{distribution → bundled}/themes.md +4 -11
  132. package/docs/{distribution → bundled}/tool-presentation.md +4 -10
  133. package/docs/compaction.md +18 -14
  134. package/docs/custom-provider.md +8 -8
  135. package/docs/docs.json +61 -9
  136. package/docs/extensions.md +14 -13
  137. package/docs/index.md +12 -7
  138. package/docs/json.md +4 -4
  139. package/docs/providers.md +1 -1
  140. package/docs/rpc.md +3 -9
  141. package/docs/session-format.md +5 -5
  142. package/docs/skills.md +1 -1
  143. package/docs/themes.md +4 -8
  144. package/docs/tui.md +1 -1
  145. package/examples/extensions/README.md +0 -1
  146. package/examples/extensions/custom-provider-anthropic/index.ts +4 -4
  147. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -2
  148. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  149. package/examples/extensions/gondolin/index.ts +4 -6
  150. package/examples/extensions/message-renderer.ts +2 -2
  151. package/examples/extensions/overlay-qa-tests.ts +2 -2
  152. package/examples/extensions/project-trust.ts +2 -2
  153. package/examples/sdk/README.md +38 -44
  154. package/npm-shrinkwrap.json +2 -2
  155. package/package.json +6 -2
  156. package/docs/development.md +0 -68
  157. package/docs/distribution/README.md +0 -34
  158. package/docs/distribution/architecture.md +0 -102
  159. package/docs/distribution/extensions/README.md +0 -17
  160. package/docs/distribution/maintenance.md +0 -121
  161. package/docs/distribution/release.md +0 -85
  162. package/examples/extensions/subagent/README.md +0 -175
  163. package/examples/extensions/subagent/agents/planner.md +0 -37
  164. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  165. package/examples/extensions/subagent/agents/scout.md +0 -50
  166. package/examples/extensions/subagent/agents/worker.md +0 -24
  167. package/examples/extensions/subagent/agents.ts +0 -126
  168. package/examples/extensions/subagent/index.ts +0 -1009
  169. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  170. package/examples/extensions/subagent/prompts/implement.md +0 -10
  171. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
  172. /package/docs/{distribution → bundled}/extensions/deepwiki.md +0 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * plan/constants.ts — tool identity, command names, and prompt copy.
3
+ *
4
+ * Name/label/command live here so `index.ts` only assembles the extension,
5
+ * matching the todo/question constants.ts pattern. Prompt copy is the
6
+ * model-facing contract; keep it stable.
7
+ */
8
+ export const EXIT_PLAN_TOOL_NAME = "exit_plan";
9
+ export const EXIT_PLAN_TOOL_LABEL = "Exit Plan";
10
+ export const PLAN_COMMAND_NAME = "plan";
11
+ export const PLAN_FLAG_NAME = "plan";
12
+ export const PLAN_ENTRY_TYPE = "plan-mode";
13
+ export const PLAN_STATUS_KEY = "plan-mode";
14
+ /**
15
+ * Tools removed while planning. edit/write/bash grant write access directly;
16
+ * subagent spawns child sessions that do not inherit the restricted tool set,
17
+ * so leaving it active would let the model write through a child agent.
18
+ * Other extension tools (question, todo, deepwiki, ...) stay available.
19
+ */
20
+ export const PLAN_BLOCKED_TOOLS = new Set(["edit", "write", "bash", "subagent"]);
21
+ /** Max characters of plan markdown echoed into the post-compaction kickoff message. */
22
+ export const PLAN_EMBED_MAX_CHARS = 24_000;
23
+ export const MENU_EXECUTE = "Start executing (keep full context)";
24
+ export const MENU_COMPACT_EXECUTE = "Compact context, then execute (best for long tasks)";
25
+ export const MENU_KEEP_PLANNING = "Keep planning";
26
+ export const EXIT_PLAN_TOOL_DESCRIPTION = "Submit the finished plan and ask the user to approve leaving plan mode. Present the complete plan as normal response text first, then call this tool with a short title and the same plan markdown; on approval the plan is saved to disk. The user chooses between executing with full context, compacting the context before executing, or continuing to plan. Only available while plan mode is active.";
27
+ export const EXIT_PLAN_PROMPT_SNIPPET = "Submit the finished plan for user approval and leave plan mode";
28
+ export const EXIT_PLAN_PROMPT_GUIDELINES = [
29
+ "Call `exit_plan` only after presenting the complete plan as response text; make it the only tool call in that step and stop after it returns.",
30
+ "`exit_plan` results are authoritative for plan-mode state: follow them even when the plan-mode system prompt block is still present later in the run.",
31
+ ];
32
+ const PLAN_MODE_PROMPT = `[PLAN MODE]
33
+ You are in plan mode: a read-only phase for exploring the codebase and agreeing on a plan with the user before any changes are made.
34
+
35
+ Restrictions:
36
+ - edit, write, bash, and subagent are unavailable; do not attempt changes or workarounds. Explore with read, grep, find, and ls instead.
37
+
38
+ Working style:
39
+ - Answer questions from the code, not from the user: explore before asking.
40
+ - When product intent, scope, or risk tolerance is unclear, interview the user with the question tool. Focus on one decision per exchange (batch only tightly coupled decisions) and always include your recommendation and the trade-offs.
41
+ - Skip the interview for small, clear tasks. Pick sensible defaults for low-risk details and state them in the plan.
42
+
43
+ Delivering the plan:
44
+ - When the plan is ready, present it in full as normal response text (goal, steps, verification criteria) so the user can read it, then call exit_plan with a short title and the same markdown as the plan parameter.
45
+ - The exit_plan tool result is authoritative for mode state: this block can outlive plan mode within a run, so if the result says plan mode has ended, it has ended.`;
46
+ /** Build the plan-mode system prompt block, listing plans already saved for this branch. */
47
+ export function buildPlanModePrompt(planFiles) {
48
+ if (!planFiles.length)
49
+ return PLAN_MODE_PROMPT;
50
+ const listing = planFiles.map((path) => `- ${path}`).join("\n");
51
+ return `${PLAN_MODE_PROMPT}
52
+
53
+ Plans already saved for this session (read them with the read tool):
54
+ ${listing}
55
+ When revising a saved plan, read the latest one first, account for work already completed, and produce a revision that states what changed and why; set the revises parameter to the file name of the plan being revised.`;
56
+ }
57
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/extensions/plan/constants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAW,CAAC;AAC/C,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAChD,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAC3C,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAEtG,uFAAuF;AACvF,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C,MAAM,CAAC,MAAM,YAAY,GAAG,qCAAqC,CAAC;AAClE,MAAM,CAAC,MAAM,oBAAoB,GAAG,qDAAqD,CAAC;AAC1F,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAElD,MAAM,CAAC,MAAM,0BAA0B,GACtC,4YAA4Y,CAAC;AAE9Y,MAAM,CAAC,MAAM,wBAAwB,GAAG,gEAAgE,CAAC;AAEzG,MAAM,CAAC,MAAM,2BAA2B,GAAG;IAC1C,+IAA+I;IAC/I,uJAAuJ;CACvJ,CAAC;AAEF,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;qKAa4I,CAAC;AAEtK,4FAA4F;AAC5F,MAAM,UAAU,mBAAmB,CAAC,SAAmB,EAAU;IAChE,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,OAAO,gBAAgB,CAAC;IAC/C,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,GAAG,gBAAgB;;;EAGzB,OAAO;0NACiN,CAAC;AAAA,CAC1N","sourcesContent":["/**\n * plan/constants.ts — tool identity, command names, and prompt copy.\n *\n * Name/label/command live here so `index.ts` only assembles the extension,\n * matching the todo/question constants.ts pattern. Prompt copy is the\n * model-facing contract; keep it stable.\n */\n\nexport const EXIT_PLAN_TOOL_NAME = \"exit_plan\";\nexport const EXIT_PLAN_TOOL_LABEL = \"Exit Plan\";\nexport const PLAN_COMMAND_NAME = \"plan\";\nexport const PLAN_FLAG_NAME = \"plan\";\nexport const PLAN_ENTRY_TYPE = \"plan-mode\";\nexport const PLAN_STATUS_KEY = \"plan-mode\";\n\n/**\n * Tools removed while planning. edit/write/bash grant write access directly;\n * subagent spawns child sessions that do not inherit the restricted tool set,\n * so leaving it active would let the model write through a child agent.\n * Other extension tools (question, todo, deepwiki, ...) stay available.\n */\nexport const PLAN_BLOCKED_TOOLS: ReadonlySet<string> = new Set([\"edit\", \"write\", \"bash\", \"subagent\"]);\n\n/** Max characters of plan markdown echoed into the post-compaction kickoff message. */\nexport const PLAN_EMBED_MAX_CHARS = 24_000;\n\nexport const MENU_EXECUTE = \"Start executing (keep full context)\";\nexport const MENU_COMPACT_EXECUTE = \"Compact context, then execute (best for long tasks)\";\nexport const MENU_KEEP_PLANNING = \"Keep planning\";\n\nexport const EXIT_PLAN_TOOL_DESCRIPTION =\n\t\"Submit the finished plan and ask the user to approve leaving plan mode. Present the complete plan as normal response text first, then call this tool with a short title and the same plan markdown; on approval the plan is saved to disk. The user chooses between executing with full context, compacting the context before executing, or continuing to plan. Only available while plan mode is active.\";\n\nexport const EXIT_PLAN_PROMPT_SNIPPET = \"Submit the finished plan for user approval and leave plan mode\";\n\nexport const EXIT_PLAN_PROMPT_GUIDELINES = [\n\t\"Call `exit_plan` only after presenting the complete plan as response text; make it the only tool call in that step and stop after it returns.\",\n\t\"`exit_plan` results are authoritative for plan-mode state: follow them even when the plan-mode system prompt block is still present later in the run.\",\n];\n\nconst PLAN_MODE_PROMPT = `[PLAN MODE]\nYou are in plan mode: a read-only phase for exploring the codebase and agreeing on a plan with the user before any changes are made.\n\nRestrictions:\n- edit, write, bash, and subagent are unavailable; do not attempt changes or workarounds. Explore with read, grep, find, and ls instead.\n\nWorking style:\n- Answer questions from the code, not from the user: explore before asking.\n- When product intent, scope, or risk tolerance is unclear, interview the user with the question tool. Focus on one decision per exchange (batch only tightly coupled decisions) and always include your recommendation and the trade-offs.\n- Skip the interview for small, clear tasks. Pick sensible defaults for low-risk details and state them in the plan.\n\nDelivering the plan:\n- When the plan is ready, present it in full as normal response text (goal, steps, verification criteria) so the user can read it, then call exit_plan with a short title and the same markdown as the plan parameter.\n- The exit_plan tool result is authoritative for mode state: this block can outlive plan mode within a run, so if the result says plan mode has ended, it has ended.`;\n\n/** Build the plan-mode system prompt block, listing plans already saved for this branch. */\nexport function buildPlanModePrompt(planFiles: string[]): string {\n\tif (!planFiles.length) return PLAN_MODE_PROMPT;\n\tconst listing = planFiles.map((path) => `- ${path}`).join(\"\\n\");\n\treturn `${PLAN_MODE_PROMPT}\n\nPlans already saved for this session (read them with the read tool):\n${listing}\nWhen revising a saved plan, read the latest one first, account for work already completed, and produce a revision that states what changed and why; set the revises parameter to the file name of the plan being revised.`;\n}\n"]}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * plan — Claude-Code-style plan mode: read-only exploration and interview,
3
+ * then a user-approved exit that saves the plan and optionally compacts.
4
+ *
5
+ * Shape:
6
+ * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/
7
+ * subagent) and activates exit_plan; a tool_call guard covers requests
8
+ * already in flight before the tool-set change lands.
9
+ * - The plan text travels as an exit_plan parameter, so the model needs no
10
+ * write access to produce the plan file.
11
+ * - "Compact, then execute" keeps plan mode active until compaction completes
12
+ * (tools are restored in onComplete) and terminates the run via the tool
13
+ * result, so there is no window where the model holds full context plus
14
+ * write tools. The kickoff message embeds the plan itself — continuation
15
+ * never depends on the compaction summary preserving a path.
16
+ * - State is conversation-backed via custom entries and branch replay (same
17
+ * pattern as todo): /reload, /tree, resume, and fork all restore.
18
+ */
19
+ import type { ExtensionAPI } from "../../core/extensions/types.ts";
20
+ export default function plan(pi: ExtensionAPI): void;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAEX,YAAY,EAGZ,MAAM,gCAAgC,CAAC;AAgDxC,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAySnD","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/\n * subagent) and activates exit_plan; a tool_call guard covers requests\n * already in flight before the tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_FLAG_NAME,\n\tPLAN_STATUS_KEY,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { savePlanFile } from \"./storage.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/** Remove blocked tools, activate exit_plan, and return what was actually removed. */\n\tfunction applyPlanTools(): string[] {\n\t\tconst active = pi.getActiveTools();\n\t\tconst removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));\n\t\tpi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);\n\t\treturn removed;\n\t}\n\n\t/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */\n\tfunction restoreTools(removedTools: string[]): void {\n\t\tconst active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);\n\t\tpi.setActiveTools([...new Set([...active, ...removedTools])]);\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"warning\", \"plan\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst removedTools = applyPlanTools();\n\t\treplacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\"Plan mode enabled: edit, write, bash, and subagent are disabled.\", \"info\");\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.removedTools);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// The fresh removal set is authoritative for the eventual restore:\n\t\t\t// the live tool set may differ from what the entry recorded.\n\t\t\tnext.removedTools = applyPlanTools();\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.removedTools : next.removedTools);\n\t\t\t}\n\t\t\tnext.removedTools = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(\n\t\t\t\t\t`Approve plan \"${title}\"?`,\n\t\t\t\t\t[MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING],\n\t\t\t\t\t{\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Toggle plan mode (read-only planning with user-approved exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.planning) {\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t} else {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName)) return;\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t};\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
@@ -0,0 +1,291 @@
1
+ import { buildPlanModePrompt, EXIT_PLAN_PROMPT_GUIDELINES, EXIT_PLAN_PROMPT_SNIPPET, EXIT_PLAN_TOOL_DESCRIPTION, EXIT_PLAN_TOOL_LABEL, EXIT_PLAN_TOOL_NAME, MENU_COMPACT_EXECUTE, MENU_EXECUTE, MENU_KEEP_PLANNING, PLAN_BLOCKED_TOOLS, PLAN_COMMAND_NAME, PLAN_EMBED_MAX_CHARS, PLAN_ENTRY_TYPE, PLAN_FLAG_NAME, PLAN_STATUS_KEY, } from "./constants.js";
2
+ import { ExitPlanParamsSchema } from "./schema.js";
3
+ import { clonePlanState, disposePlanSession, EMPTY_PLAN_STATE, getPlanState, replacePlanState, replayPlanFromBranch, setActivePlanSession, } from "./state.js";
4
+ import { savePlanFile } from "./storage.js";
5
+ function buildKickoffMessage(pending) {
6
+ const truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;
7
+ const body = truncated
8
+ ? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\n\n[Plan truncated — read the full plan at ${pending.planPath}]`
9
+ : pending.plan;
10
+ return `Execute the approved plan "${pending.title}" (saved at ${pending.planPath}).
11
+
12
+ ${body}
13
+
14
+ Start now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;
15
+ }
16
+ export default function plan(pi) {
17
+ let menuOpen = false;
18
+ let pendingCompact;
19
+ pi.registerFlag(PLAN_FLAG_NAME, {
20
+ description: "Start in plan mode (read-only planning)",
21
+ type: "boolean",
22
+ default: false,
23
+ });
24
+ /** Remove blocked tools, activate exit_plan, and return what was actually removed. */
25
+ function applyPlanTools() {
26
+ const active = pi.getActiveTools();
27
+ const removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));
28
+ pi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);
29
+ return removed;
30
+ }
31
+ /** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */
32
+ function restoreTools(removedTools) {
33
+ const active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);
34
+ pi.setActiveTools([...new Set([...active, ...removedTools])]);
35
+ }
36
+ function persist() {
37
+ pi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());
38
+ }
39
+ function updateStatus(ctx) {
40
+ const state = getPlanState();
41
+ ctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg("warning", "plan") : undefined);
42
+ }
43
+ function enterPlanMode(ctx) {
44
+ const state = getPlanState();
45
+ if (state.planning)
46
+ return;
47
+ const removedTools = applyPlanTools();
48
+ replacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });
49
+ persist();
50
+ updateStatus(ctx);
51
+ ctx.ui.notify("Plan mode enabled: edit, write, bash, and subagent are disabled.", "info");
52
+ }
53
+ function exitPlanMode(ctx, notifyText) {
54
+ const state = getPlanState();
55
+ restoreTools(state.removedTools);
56
+ replacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });
57
+ persist();
58
+ updateStatus(ctx);
59
+ if (notifyText)
60
+ ctx.ui.notify(notifyText, "info");
61
+ }
62
+ /** Save the approved plan, index it in state, and name the session if unnamed. */
63
+ function saveApprovedPlan(ctx, params, title) {
64
+ const planPath = savePlanFile({
65
+ sessionId: ctx.sessionManager.getSessionId(),
66
+ title,
67
+ plan: params.plan,
68
+ cwd: ctx.cwd,
69
+ revises: params.revises,
70
+ });
71
+ const state = getPlanState();
72
+ replacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });
73
+ if (!pi.getSessionName())
74
+ pi.setSessionName(title);
75
+ return planPath;
76
+ }
77
+ /**
78
+ * Rebuild state from the current branch and reconcile the live tool set.
79
+ * An interrupted compact-and-execute approval (restart, resume, branch
80
+ * switch) degrades to the keep-context exit so the approval is honored.
81
+ */
82
+ function syncFromBranch(ctx, options) {
83
+ setActivePlanSession(ctx.sessionManager.getSessionId());
84
+ const before = getPlanState();
85
+ const replayed = replayPlanFromBranch(ctx);
86
+ const next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);
87
+ if (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {
88
+ next.planning = true;
89
+ }
90
+ let interruptedCompact = false;
91
+ if (next.awaitingCompact) {
92
+ next.planning = false;
93
+ next.awaitingCompact = false;
94
+ interruptedCompact = true;
95
+ pendingCompact = undefined;
96
+ }
97
+ if (next.planning) {
98
+ // The fresh removal set is authoritative for the eventual restore:
99
+ // the live tool set may differ from what the entry recorded.
100
+ next.removedTools = applyPlanTools();
101
+ }
102
+ else {
103
+ if (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {
104
+ restoreTools(before.planning ? before.removedTools : next.removedTools);
105
+ }
106
+ next.removedTools = [];
107
+ }
108
+ replacePlanState(next);
109
+ if (interruptedCompact) {
110
+ persist();
111
+ ctx.ui.notify("Pending compaction was interrupted; plan mode exited with context kept.", "warning");
112
+ }
113
+ updateStatus(ctx);
114
+ }
115
+ pi.registerTool({
116
+ name: EXIT_PLAN_TOOL_NAME,
117
+ label: EXIT_PLAN_TOOL_LABEL,
118
+ description: EXIT_PLAN_TOOL_DESCRIPTION,
119
+ promptSnippet: EXIT_PLAN_PROMPT_SNIPPET,
120
+ promptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,
121
+ parameters: ExitPlanParamsSchema,
122
+ executionMode: "sequential",
123
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
124
+ setActivePlanSession(ctx.sessionManager.getSessionId());
125
+ const state = getPlanState();
126
+ const title = params.title.trim() || "plan";
127
+ if (!state.planning || state.awaitingCompact) {
128
+ return {
129
+ content: [
130
+ { type: "text", text: "Plan mode is not awaiting an exit; continue with your current instructions." },
131
+ ],
132
+ details: { decision: "cancelled", title },
133
+ };
134
+ }
135
+ if (!ctx.hasUI) {
136
+ // json/print: no way to ask, so honor the least surprising default —
137
+ // exit with context kept. Documented as a headless-mode semantic.
138
+ const planPath = saveApprovedPlan(ctx, params, title);
139
+ exitPlanMode(ctx);
140
+ return {
141
+ content: [
142
+ {
143
+ type: "text",
144
+ text: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,
145
+ },
146
+ ],
147
+ details: { decision: "execute", title, planPath },
148
+ };
149
+ }
150
+ let choice;
151
+ menuOpen = true;
152
+ try {
153
+ choice = await ctx.ui.select(`Approve plan "${title}"?`, [MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING], {
154
+ signal,
155
+ });
156
+ }
157
+ finally {
158
+ menuOpen = false;
159
+ }
160
+ if (signal?.aborted) {
161
+ // Throwing lets Pi mark the result as an error, matching the todo convention.
162
+ throw new Error("Plan approval was interrupted; plan mode is still active.");
163
+ }
164
+ if (choice === MENU_EXECUTE) {
165
+ const planPath = saveApprovedPlan(ctx, params, title);
166
+ exitPlanMode(ctx, "Plan approved: executing with full context.");
167
+ return {
168
+ content: [
169
+ {
170
+ type: "text",
171
+ text: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,
172
+ },
173
+ ],
174
+ details: { decision: "execute", title, planPath },
175
+ };
176
+ }
177
+ if (choice === MENU_COMPACT_EXECUTE) {
178
+ const planPath = saveApprovedPlan(ctx, params, title);
179
+ // Stay in plan mode (tools restricted) until compaction completes;
180
+ // terminate ends the run so agent_settled can start the compaction.
181
+ replacePlanState({ ...getPlanState(), awaitingCompact: true });
182
+ persist();
183
+ pendingCompact = { title, plan: params.plan, planPath };
184
+ return {
185
+ content: [
186
+ {
187
+ type: "text",
188
+ text: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,
189
+ },
190
+ ],
191
+ details: { decision: "compactAndExecute", title, planPath },
192
+ terminate: true,
193
+ };
194
+ }
195
+ // Explicit "Keep planning", Esc, or timeout: the only side-effect-free
196
+ // choice, so any dismissal maps here.
197
+ let feedback;
198
+ if (choice === MENU_KEEP_PLANNING) {
199
+ feedback =
200
+ ctx.mode === "tui"
201
+ ? await ctx.ui.editor("Feedback for the plan (optional):", "")
202
+ : await ctx.ui.input("Feedback for the plan (optional):");
203
+ }
204
+ const trimmedFeedback = feedback?.trim();
205
+ return {
206
+ content: [
207
+ {
208
+ type: "text",
209
+ text: trimmedFeedback
210
+ ? `The user wants to keep planning. Feedback:\n${trimmedFeedback}`
211
+ : "The user wants to keep planning. Ask what should change, or keep refining the open points.",
212
+ },
213
+ ],
214
+ details: { decision: "keepPlanning", title },
215
+ };
216
+ },
217
+ });
218
+ pi.registerCommand(PLAN_COMMAND_NAME, {
219
+ description: "Toggle plan mode (read-only planning with user-approved exit)",
220
+ handler: async (_args, ctx) => {
221
+ setActivePlanSession(ctx.sessionManager.getSessionId());
222
+ const state = getPlanState();
223
+ if (menuOpen) {
224
+ ctx.ui.notify("The exit_plan approval menu is open — answer it first.", "warning");
225
+ return;
226
+ }
227
+ if (state.awaitingCompact) {
228
+ ctx.ui.notify("Waiting for compaction to finish before leaving plan mode.", "warning");
229
+ return;
230
+ }
231
+ if (state.planning) {
232
+ exitPlanMode(ctx, "Plan mode disabled: full tool access restored.");
233
+ }
234
+ else {
235
+ enterPlanMode(ctx);
236
+ }
237
+ },
238
+ });
239
+ // Defense in depth: the active-tools change lands on the next LLM request,
240
+ // so a request already in flight when plan mode toggled can still carry
241
+ // blocked tool calls.
242
+ pi.on("tool_call", async (event, ctx) => {
243
+ setActivePlanSession(ctx.sessionManager.getSessionId());
244
+ const state = getPlanState();
245
+ if (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName))
246
+ return;
247
+ return {
248
+ block: true,
249
+ reason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,
250
+ };
251
+ });
252
+ pi.on("before_agent_start", async (event, ctx) => {
253
+ setActivePlanSession(ctx.sessionManager.getSessionId());
254
+ const state = getPlanState();
255
+ if (!state.planning)
256
+ return;
257
+ return { systemPrompt: `${event.systemPrompt}\n\n${buildPlanModePrompt(state.planFiles)}` };
258
+ });
259
+ // agent_settled fires once per fully settled run (agent_end can repeat on
260
+ // retries and queued continuations), so the approved compaction starts here.
261
+ pi.on("agent_settled", async (_event, ctx) => {
262
+ setActivePlanSession(ctx.sessionManager.getSessionId());
263
+ if (!getPlanState().awaitingCompact || !pendingCompact)
264
+ return;
265
+ const pending = pendingCompact;
266
+ pendingCompact = undefined;
267
+ ctx.compact({
268
+ customInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,
269
+ onComplete: () => {
270
+ exitPlanMode(ctx, "Context compacted; executing the plan.");
271
+ pi.sendUserMessage(buildKickoffMessage(pending));
272
+ },
273
+ onError: (error) => {
274
+ // Never leave the user stuck read-only: degrade to keep-context execution.
275
+ exitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);
276
+ pi.sendUserMessage(buildKickoffMessage(pending));
277
+ },
278
+ });
279
+ });
280
+ pi.on("session_start", async (_event, ctx) => {
281
+ syncFromBranch(ctx, { applyFlag: true });
282
+ });
283
+ pi.on("session_tree", async (_event, ctx) => {
284
+ syncFromBranch(ctx);
285
+ });
286
+ pi.on("session_shutdown", async (_event, ctx) => {
287
+ pendingCompact = undefined;
288
+ disposePlanSession(ctx.sessionManager.getSessionId());
289
+ });
290
+ }
291
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/plan/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * plan — Claude-Code-style plan mode: read-only exploration and interview,\n * then a user-approved exit that saves the plan and optionally compacts.\n *\n * Shape:\n * - Entry (/plan or --plan) removes write-capable tools (edit/write/bash/\n * subagent) and activates exit_plan; a tool_call guard covers requests\n * already in flight before the tool-set change lands.\n * - The plan text travels as an exit_plan parameter, so the model needs no\n * write access to produce the plan file.\n * - \"Compact, then execute\" keeps plan mode active until compaction completes\n * (tools are restored in onComplete) and terminates the run via the tool\n * result, so there is no window where the model holds full context plus\n * write tools. The kickoff message embeds the plan itself — continuation\n * never depends on the compaction summary preserving a path.\n * - State is conversation-backed via custom entries and branch replay (same\n * pattern as todo): /reload, /tree, resume, and fork all restore.\n */\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n\tToolExecutionMode,\n} from \"../../core/extensions/types.ts\";\nimport {\n\tbuildPlanModePrompt,\n\tEXIT_PLAN_PROMPT_GUIDELINES,\n\tEXIT_PLAN_PROMPT_SNIPPET,\n\tEXIT_PLAN_TOOL_DESCRIPTION,\n\tEXIT_PLAN_TOOL_LABEL,\n\tEXIT_PLAN_TOOL_NAME,\n\tMENU_COMPACT_EXECUTE,\n\tMENU_EXECUTE,\n\tMENU_KEEP_PLANNING,\n\tPLAN_BLOCKED_TOOLS,\n\tPLAN_COMMAND_NAME,\n\tPLAN_EMBED_MAX_CHARS,\n\tPLAN_ENTRY_TYPE,\n\tPLAN_FLAG_NAME,\n\tPLAN_STATUS_KEY,\n} from \"./constants.ts\";\nimport { type ExitPlanDetails, type ExitPlanParams, ExitPlanParamsSchema } from \"./schema.ts\";\nimport {\n\tclonePlanState,\n\tdisposePlanSession,\n\tEMPTY_PLAN_STATE,\n\tgetPlanState,\n\treplacePlanState,\n\treplayPlanFromBranch,\n\tsetActivePlanSession,\n} from \"./state.ts\";\nimport { savePlanFile } from \"./storage.ts\";\n\ninterface PendingCompact {\n\ttitle: string;\n\tplan: string;\n\tplanPath: string;\n}\n\nfunction buildKickoffMessage(pending: PendingCompact): string {\n\tconst truncated = pending.plan.length > PLAN_EMBED_MAX_CHARS;\n\tconst body = truncated\n\t\t? `${pending.plan.slice(0, PLAN_EMBED_MAX_CHARS)}\\n\\n[Plan truncated — read the full plan at ${pending.planPath}]`\n\t\t: pending.plan;\n\treturn `Execute the approved plan \"${pending.title}\" (saved at ${pending.planPath}).\n\n${body}\n\nStart now. Track multi-step work with the todo tool, and report back instead of improvising if the plan turns out to need revision.`;\n}\n\nexport default function plan(pi: ExtensionAPI): void {\n\tlet menuOpen = false;\n\tlet pendingCompact: PendingCompact | undefined;\n\n\tpi.registerFlag(PLAN_FLAG_NAME, {\n\t\tdescription: \"Start in plan mode (read-only planning)\",\n\t\ttype: \"boolean\",\n\t\tdefault: false,\n\t});\n\n\t/** Remove blocked tools, activate exit_plan, and return what was actually removed. */\n\tfunction applyPlanTools(): string[] {\n\t\tconst active = pi.getActiveTools();\n\t\tconst removed = active.filter((name) => PLAN_BLOCKED_TOOLS.has(name));\n\t\tpi.setActiveTools([...new Set([...active.filter((name) => !PLAN_BLOCKED_TOOLS.has(name)), EXIT_PLAN_TOOL_NAME])]);\n\t\treturn removed;\n\t}\n\n\t/** Inverse of applyPlanTools: drop exit_plan, re-add exactly what was removed. */\n\tfunction restoreTools(removedTools: string[]): void {\n\t\tconst active = pi.getActiveTools().filter((name) => name !== EXIT_PLAN_TOOL_NAME);\n\t\tpi.setActiveTools([...new Set([...active, ...removedTools])]);\n\t}\n\n\tfunction persist(): void {\n\t\tpi.appendEntry(PLAN_ENTRY_TYPE, getPlanState());\n\t}\n\n\tfunction updateStatus(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tctx.ui.setStatus(PLAN_STATUS_KEY, state.planning ? ctx.ui.theme.fg(\"warning\", \"plan\") : undefined);\n\t}\n\n\tfunction enterPlanMode(ctx: ExtensionContext): void {\n\t\tconst state = getPlanState();\n\t\tif (state.planning) return;\n\t\tconst removedTools = applyPlanTools();\n\t\treplacePlanState({ planning: true, removedTools, awaitingCompact: false, planFiles: state.planFiles });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tctx.ui.notify(\"Plan mode enabled: edit, write, bash, and subagent are disabled.\", \"info\");\n\t}\n\n\tfunction exitPlanMode(ctx: ExtensionContext, notifyText?: string): void {\n\t\tconst state = getPlanState();\n\t\trestoreTools(state.removedTools);\n\t\treplacePlanState({ ...state, planning: false, awaitingCompact: false, removedTools: [] });\n\t\tpersist();\n\t\tupdateStatus(ctx);\n\t\tif (notifyText) ctx.ui.notify(notifyText, \"info\");\n\t}\n\n\t/** Save the approved plan, index it in state, and name the session if unnamed. */\n\tfunction saveApprovedPlan(ctx: ExtensionContext, params: ExitPlanParams, title: string): string {\n\t\tconst planPath = savePlanFile({\n\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\ttitle,\n\t\t\tplan: params.plan,\n\t\t\tcwd: ctx.cwd,\n\t\t\trevises: params.revises,\n\t\t});\n\t\tconst state = getPlanState();\n\t\treplacePlanState({ ...state, planFiles: [...state.planFiles, planPath] });\n\t\tif (!pi.getSessionName()) pi.setSessionName(title);\n\t\treturn planPath;\n\t}\n\n\t/**\n\t * Rebuild state from the current branch and reconcile the live tool set.\n\t * An interrupted compact-and-execute approval (restart, resume, branch\n\t * switch) degrades to the keep-context exit so the approval is honored.\n\t */\n\tfunction syncFromBranch(ctx: ExtensionContext, options?: { applyFlag?: boolean }): void {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst before = getPlanState();\n\t\tconst replayed = replayPlanFromBranch(ctx);\n\t\tconst next = replayed ?? clonePlanState(EMPTY_PLAN_STATE);\n\t\tif (!replayed && options?.applyFlag && pi.getFlag(PLAN_FLAG_NAME) === true) {\n\t\t\tnext.planning = true;\n\t\t}\n\n\t\tlet interruptedCompact = false;\n\t\tif (next.awaitingCompact) {\n\t\t\tnext.planning = false;\n\t\t\tnext.awaitingCompact = false;\n\t\t\tinterruptedCompact = true;\n\t\t\tpendingCompact = undefined;\n\t\t}\n\n\t\tif (next.planning) {\n\t\t\t// The fresh removal set is authoritative for the eventual restore:\n\t\t\t// the live tool set may differ from what the entry recorded.\n\t\t\tnext.removedTools = applyPlanTools();\n\t\t} else {\n\t\t\tif (pi.getActiveTools().includes(EXIT_PLAN_TOOL_NAME)) {\n\t\t\t\trestoreTools(before.planning ? before.removedTools : next.removedTools);\n\t\t\t}\n\t\t\tnext.removedTools = [];\n\t\t}\n\n\t\treplacePlanState(next);\n\t\tif (interruptedCompact) {\n\t\t\tpersist();\n\t\t\tctx.ui.notify(\"Pending compaction was interrupted; plan mode exited with context kept.\", \"warning\");\n\t\t}\n\t\tupdateStatus(ctx);\n\t}\n\n\tpi.registerTool({\n\t\tname: EXIT_PLAN_TOOL_NAME,\n\t\tlabel: EXIT_PLAN_TOOL_LABEL,\n\t\tdescription: EXIT_PLAN_TOOL_DESCRIPTION,\n\t\tpromptSnippet: EXIT_PLAN_PROMPT_SNIPPET,\n\t\tpromptGuidelines: EXIT_PLAN_PROMPT_GUIDELINES,\n\t\tparameters: ExitPlanParamsSchema,\n\t\texecutionMode: \"sequential\" as ToolExecutionMode,\n\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<ExitPlanDetails>> {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tconst title = params.title.trim() || \"plan\";\n\n\t\t\tif (!state.planning || state.awaitingCompact) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: \"Plan mode is not awaiting an exit; continue with your current instructions.\" },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"cancelled\", title },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\t// json/print: no way to ask, so honor the least surprising default —\n\t\t\t\t// exit with context kept. Documented as a headless-mode semantic.\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan saved to ${planPath}. No interactive UI, so plan mode exited with context kept (authoritative: write tools are restored). Begin executing the plan now.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tlet choice: string | undefined;\n\t\t\tmenuOpen = true;\n\t\t\ttry {\n\t\t\t\tchoice = await ctx.ui.select(\n\t\t\t\t\t`Approve plan \"${title}\"?`,\n\t\t\t\t\t[MENU_EXECUTE, MENU_COMPACT_EXECUTE, MENU_KEEP_PLANNING],\n\t\t\t\t\t{\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tmenuOpen = false;\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\t// Throwing lets Pi mark the result as an error, matching the todo convention.\n\t\t\t\tthrow new Error(\"Plan approval was interrupted; plan mode is still active.\");\n\t\t\t}\n\n\t\t\tif (choice === MENU_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\texitPlanMode(ctx, \"Plan approved: executing with full context.\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. Plan mode has ended and write tools are restored (authoritative — overrides the plan-mode system prompt block). Begin executing the plan now; track multi-step work with the todo tool.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"execute\", title, planPath },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (choice === MENU_COMPACT_EXECUTE) {\n\t\t\t\tconst planPath = saveApprovedPlan(ctx, params, title);\n\t\t\t\t// Stay in plan mode (tools restricted) until compaction completes;\n\t\t\t\t// terminate ends the run so agent_settled can start the compaction.\n\t\t\t\treplacePlanState({ ...getPlanState(), awaitingCompact: true });\n\t\t\t\tpersist();\n\t\t\t\tpendingCompact = { title, plan: params.plan, planPath };\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Plan approved and saved to ${planPath}. The context will now be compacted; stop here — execution restarts automatically after compaction.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { decision: \"compactAndExecute\", title, planPath },\n\t\t\t\t\tterminate: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Explicit \"Keep planning\", Esc, or timeout: the only side-effect-free\n\t\t\t// choice, so any dismissal maps here.\n\t\t\tlet feedback: string | undefined;\n\t\t\tif (choice === MENU_KEEP_PLANNING) {\n\t\t\t\tfeedback =\n\t\t\t\t\tctx.mode === \"tui\"\n\t\t\t\t\t\t? await ctx.ui.editor(\"Feedback for the plan (optional):\", \"\")\n\t\t\t\t\t\t: await ctx.ui.input(\"Feedback for the plan (optional):\");\n\t\t\t}\n\t\t\tconst trimmedFeedback = feedback?.trim();\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: trimmedFeedback\n\t\t\t\t\t\t\t? `The user wants to keep planning. Feedback:\\n${trimmedFeedback}`\n\t\t\t\t\t\t\t: \"The user wants to keep planning. Ask what should change, or keep refining the open points.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { decision: \"keepPlanning\", title },\n\t\t\t};\n\t\t},\n\t});\n\n\tpi.registerCommand(PLAN_COMMAND_NAME, {\n\t\tdescription: \"Toggle plan mode (read-only planning with user-approved exit)\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\t\tconst state = getPlanState();\n\t\t\tif (menuOpen) {\n\t\t\t\tctx.ui.notify(\"The exit_plan approval menu is open — answer it first.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.awaitingCompact) {\n\t\t\t\tctx.ui.notify(\"Waiting for compaction to finish before leaving plan mode.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.planning) {\n\t\t\t\texitPlanMode(ctx, \"Plan mode disabled: full tool access restored.\");\n\t\t\t} else {\n\t\t\t\tenterPlanMode(ctx);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Defense in depth: the active-tools change lands on the next LLM request,\n\t// so a request already in flight when plan mode toggled can still carry\n\t// blocked tool calls.\n\tpi.on(\"tool_call\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning || !PLAN_BLOCKED_TOOLS.has(event.toolName)) return;\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Plan mode: ${event.toolName} is disabled. Keep exploring with read-only tools, or call ${EXIT_PLAN_TOOL_NAME} to submit the plan for approval.`,\n\t\t};\n\t});\n\n\tpi.on(\"before_agent_start\", async (event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tconst state = getPlanState();\n\t\tif (!state.planning) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildPlanModePrompt(state.planFiles)}` };\n\t});\n\n\t// agent_settled fires once per fully settled run (agent_end can repeat on\n\t// retries and queued continuations), so the approved compaction starts here.\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tsetActivePlanSession(ctx.sessionManager.getSessionId());\n\t\tif (!getPlanState().awaitingCompact || !pendingCompact) return;\n\t\tconst pending = pendingCompact;\n\t\tpendingCompact = undefined;\n\t\tctx.compact({\n\t\t\tcustomInstructions: `The user approved an implementation plan saved at ${pending.planPath}. Preserve decisions, constraints, and rejected alternatives from the planning discussion that are not already written in the plan file.`,\n\t\t\tonComplete: () => {\n\t\t\t\texitPlanMode(ctx, \"Context compacted; executing the plan.\");\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t\tonError: (error) => {\n\t\t\t\t// Never leave the user stuck read-only: degrade to keep-context execution.\n\t\t\t\texitPlanMode(ctx, `Compaction failed (${error.message}); executing with full context instead.`);\n\t\t\t\tpi.sendUserMessage(buildKickoffMessage(pending));\n\t\t\t},\n\t\t});\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx, { applyFlag: true });\n\t});\n\n\tpi.on(\"session_tree\", async (_event, ctx) => {\n\t\tsyncFromBranch(ctx);\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tpendingCompact = undefined;\n\t\tdisposePlanSession(ctx.sessionManager.getSessionId());\n\t});\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { type Static, Type } from "typebox";
2
+ export declare const ExitPlanParamsSchema: Type.TObject<{
3
+ title: Type.TString;
4
+ plan: Type.TString;
5
+ revises: Type.TOptional<Type.TString>;
6
+ }>;
7
+ export type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;
8
+ export type ExitPlanDecision = "execute" | "compactAndExecute" | "keepPlanning" | "cancelled";
9
+ export interface ExitPlanDetails {
10
+ decision: ExitPlanDecision;
11
+ title: string;
12
+ /** Absolute path of the saved plan file. Present for both execute decisions. */
13
+ planPath?: string;
14
+ }
15
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,oBAAoB;;;;EAY/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEjE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,mBAAmB,GAAG,cAAc,GAAG,WAAW,CAAC;AAE9F,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import { Type } from "typebox";
2
+ export const ExitPlanParamsSchema = Type.Object({
3
+ title: Type.String({
4
+ description: "Short plan title. Used for the plan file name and, when the session is unnamed, the session name.",
5
+ }),
6
+ plan: Type.String({
7
+ description: "The complete plan as markdown: goal, steps, and verification criteria.",
8
+ }),
9
+ revises: Type.Optional(Type.String({
10
+ description: "File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.",
11
+ })),
12
+ });
13
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/plan/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC;QAClB,WAAW,EAAE,mGAAmG;KAChH,CAAC;IACF,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;QACjB,WAAW,EAAE,wEAAwE;KACrF,CAAC;IACF,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EAAE,iFAAiF;KAC9F,CAAC,CACF;CACD,CAAC,CAAC","sourcesContent":["import { type Static, Type } from \"typebox\";\n\nexport const ExitPlanParamsSchema = Type.Object({\n\ttitle: Type.String({\n\t\tdescription: \"Short plan title. Used for the plan file name and, when the session is unnamed, the session name.\",\n\t}),\n\tplan: Type.String({\n\t\tdescription: \"The complete plan as markdown: goal, steps, and verification criteria.\",\n\t}),\n\trevises: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"File name of a previously saved plan this plan revises, e.g. `01-add-cache.md`.\",\n\t\t}),\n\t),\n});\n\nexport type ExitPlanParams = Static<typeof ExitPlanParamsSchema>;\n\nexport type ExitPlanDecision = \"execute\" | \"compactAndExecute\" | \"keepPlanning\" | \"cancelled\";\n\nexport interface ExitPlanDetails {\n\tdecision: ExitPlanDecision;\n\ttitle: string;\n\t/** Absolute path of the saved plan file. Present for both execute decisions. */\n\tplanPath?: string;\n}\n"]}
@@ -0,0 +1,29 @@
1
+ export interface PlanSessionState {
2
+ /** True from entry until the exit is fully applied (including a pending compaction). */
3
+ planning: boolean;
4
+ /** Tools actually removed on entry; restored verbatim on exit. */
5
+ removedTools: string[];
6
+ /** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */
7
+ awaitingCompact: boolean;
8
+ /** Absolute paths of plan files saved on this conversation branch. */
9
+ planFiles: string[];
10
+ }
11
+ export declare const EMPTY_PLAN_STATE: PlanSessionState;
12
+ /** Point the module at a session's state bucket. Call wherever ctx is in hand. */
13
+ export declare function setActivePlanSession(sid: string): void;
14
+ export declare function getPlanState(): PlanSessionState;
15
+ export declare function replacePlanState(next: PlanSessionState): void;
16
+ /** Drop in-memory state for a session (call from session_shutdown). */
17
+ export declare function disposePlanSession(sid: string): void;
18
+ export declare function clonePlanState(source: PlanSessionState): PlanSessionState;
19
+ export declare function isPlanState(value: unknown): value is PlanSessionState;
20
+ /**
21
+ * Latest plan-mode entry on the current branch, or undefined when the branch
22
+ * has none (lets callers distinguish "never used" from "explicitly exited").
23
+ */
24
+ export declare function replayPlanFromBranch(ctx: {
25
+ sessionManager: {
26
+ getBranch(): Iterable<unknown>;
27
+ };
28
+ }): PlanSessionState | undefined;
29
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/extensions/plan/state.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,gBAAgB;IAChC,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6FAA6F;IAC7F,eAAe,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,EAAE,gBAK9B,CAAC;AAKF,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,YAAY,IAAI,gBAAgB,CAE/C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAE7D;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAOzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CASrE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACzC,cAAc,EAAE;QAAE,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CACnD,GAAG,gBAAgB,GAAG,SAAS,CAS/B","sourcesContent":["/**\n * plan/state.ts — per-session plan-mode state, persisted as custom entries.\n *\n * State is conversation-backed like the todo extension: every transition is\n * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle\n * handlers replay the current branch tail → head to restore. Custom entries are\n * copied on fork/clone, so `planFiles` keeps pointing at the right plan files\n * even though the forked session gets a new session id.\n *\n * State is keyed per session id: one Pi process can host several sessions over\n * its lifetime (resume, /tree switches), so callers re-point the active bucket\n * wherever ctx is in hand.\n */\nimport { PLAN_ENTRY_TYPE } from \"./constants.ts\";\n\nexport interface PlanSessionState {\n\t/** True from entry until the exit is fully applied (including a pending compaction). */\n\tplanning: boolean;\n\t/** Tools actually removed on entry; restored verbatim on exit. */\n\tremovedTools: string[];\n\t/** A compact-and-execute exit was approved and is waiting for agent_settled + compaction. */\n\tawaitingCompact: boolean;\n\t/** Absolute paths of plan files saved on this conversation branch. */\n\tplanFiles: string[];\n}\n\nexport const EMPTY_PLAN_STATE: PlanSessionState = {\n\tplanning: false,\n\tremovedTools: [],\n\tawaitingCompact: false,\n\tplanFiles: [],\n};\n\nconst states = new Map<string, PlanSessionState>();\nlet activeSid = \"\";\n\n/** Point the module at a session's state bucket. Call wherever ctx is in hand. */\nexport function setActivePlanSession(sid: string): void {\n\tactiveSid = sid;\n}\n\nexport function getPlanState(): PlanSessionState {\n\treturn states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);\n}\n\nexport function replacePlanState(next: PlanSessionState): void {\n\tstates.set(activeSid, clonePlanState(next));\n}\n\n/** Drop in-memory state for a session (call from session_shutdown). */\nexport function disposePlanSession(sid: string): void {\n\tif (!sid) return;\n\tstates.delete(sid);\n\tif (activeSid === sid) activeSid = \"\";\n}\n\nexport function clonePlanState(source: PlanSessionState): PlanSessionState {\n\treturn {\n\t\tplanning: source.planning,\n\t\tremovedTools: [...source.removedTools],\n\t\tawaitingCompact: source.awaitingCompact,\n\t\tplanFiles: [...source.planFiles],\n\t};\n}\n\nexport function isPlanState(value: unknown): value is PlanSessionState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn (\n\t\ttypeof record.planning === \"boolean\" &&\n\t\ttypeof record.awaitingCompact === \"boolean\" &&\n\t\tArray.isArray(record.removedTools) &&\n\t\tArray.isArray(record.planFiles)\n\t);\n}\n\n/**\n * Latest plan-mode entry on the current branch, or undefined when the branch\n * has none (lets callers distinguish \"never used\" from \"explicitly exited\").\n */\nexport function replayPlanFromBranch(ctx: {\n\tsessionManager: { getBranch(): Iterable<unknown> };\n}): PlanSessionState | undefined {\n\tconst branch = Array.from(ctx.sessionManager.getBranch());\n\tfor (let i = branch.length - 1; i >= 0; i--) {\n\t\tconst item = branch[i] as { type?: string; customType?: string; data?: unknown };\n\t\tif (item.type !== \"custom\" || item.customType !== PLAN_ENTRY_TYPE) continue;\n\t\tif (!isPlanState(item.data)) continue;\n\t\treturn clonePlanState(item.data);\n\t}\n\treturn undefined;\n}\n"]}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * plan/state.ts — per-session plan-mode state, persisted as custom entries.
3
+ *
4
+ * State is conversation-backed like the todo extension: every transition is
5
+ * appended as a `plan-mode` custom entry (never sent to the LLM), and lifecycle
6
+ * handlers replay the current branch tail → head to restore. Custom entries are
7
+ * copied on fork/clone, so `planFiles` keeps pointing at the right plan files
8
+ * even though the forked session gets a new session id.
9
+ *
10
+ * State is keyed per session id: one Pi process can host several sessions over
11
+ * its lifetime (resume, /tree switches), so callers re-point the active bucket
12
+ * wherever ctx is in hand.
13
+ */
14
+ import { PLAN_ENTRY_TYPE } from "./constants.js";
15
+ export const EMPTY_PLAN_STATE = {
16
+ planning: false,
17
+ removedTools: [],
18
+ awaitingCompact: false,
19
+ planFiles: [],
20
+ };
21
+ const states = new Map();
22
+ let activeSid = "";
23
+ /** Point the module at a session's state bucket. Call wherever ctx is in hand. */
24
+ export function setActivePlanSession(sid) {
25
+ activeSid = sid;
26
+ }
27
+ export function getPlanState() {
28
+ return states.get(activeSid) ?? clonePlanState(EMPTY_PLAN_STATE);
29
+ }
30
+ export function replacePlanState(next) {
31
+ states.set(activeSid, clonePlanState(next));
32
+ }
33
+ /** Drop in-memory state for a session (call from session_shutdown). */
34
+ export function disposePlanSession(sid) {
35
+ if (!sid)
36
+ return;
37
+ states.delete(sid);
38
+ if (activeSid === sid)
39
+ activeSid = "";
40
+ }
41
+ export function clonePlanState(source) {
42
+ return {
43
+ planning: source.planning,
44
+ removedTools: [...source.removedTools],
45
+ awaitingCompact: source.awaitingCompact,
46
+ planFiles: [...source.planFiles],
47
+ };
48
+ }
49
+ export function isPlanState(value) {
50
+ if (!value || typeof value !== "object")
51
+ return false;
52
+ const record = value;
53
+ return (typeof record.planning === "boolean" &&
54
+ typeof record.awaitingCompact === "boolean" &&
55
+ Array.isArray(record.removedTools) &&
56
+ Array.isArray(record.planFiles));
57
+ }
58
+ /**
59
+ * Latest plan-mode entry on the current branch, or undefined when the branch
60
+ * has none (lets callers distinguish "never used" from "explicitly exited").
61
+ */
62
+ export function replayPlanFromBranch(ctx) {
63
+ const branch = Array.from(ctx.sessionManager.getBranch());
64
+ for (let i = branch.length - 1; i >= 0; i--) {
65
+ const item = branch[i];
66
+ if (item.type !== "custom" || item.customType !== PLAN_ENTRY_TYPE)
67
+ continue;
68
+ if (!isPlanState(item.data))
69
+ continue;
70
+ return clonePlanState(item.data);
71
+ }
72
+ return undefined;
73
+ }
74
+ //# sourceMappingURL=state.js.map