@hank-warren/pi-plan-mode 1.5.0 → 1.7.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 CHANGED
@@ -1,5 +1,45 @@
1
1
  # @hank-warren/pi-plan-mode
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1035138: Ship the craft documents by injected path instead of as skills.
8
+
9
+ The `pi-loop` and `pi-plan-mode` skills are gone. Their bodies now ship as
10
+ `docs/loop-craft.md` and `docs/plan-craft.md`, and the mode prompts inject the
11
+ file's absolute path (resolved from the installed package) at the moments the
12
+ guidance matters: while a loop is being drafted or completed, and while Plan
13
+ Mode is active. A skill's description line sits in every system prompt of every
14
+ session with the package loaded; across ~220 sessions after these two shipped,
15
+ every read of either file was triggered by the mode's own prompt and never by
16
+ the description, so the line was a tax on the ~95% of sessions that never
17
+ entered the mode. Same document, read at the same moments, at zero cost outside
18
+ them. Hosts that referenced the skills by name in settings should drop those
19
+ entries.
20
+
21
+ ### Patch Changes
22
+
23
+ - 1035138: Report a waiting question to Herdr as `blocked`.
24
+
25
+ `ask_user_question` and Plan Mode's `plan_mode_question` now emit the same
26
+ `herdr:blocked` event pi-auto-permissions emits for an approval prompt, labelled
27
+ `question` / `plan question` and cleared in a `finally`, so Herdr's pi
28
+ integration shows a session waiting on a question as `blocked` rather than
29
+ `working`. A supervising agent in another pane can wait on that state and answer
30
+ the dialog. No-op outside Herdr.
31
+
32
+ ## 1.6.0
33
+
34
+ ### Minor Changes
35
+
36
+ - d0c46a5: Drop two legacy paths that were past their delete-by date:
37
+
38
+ - The one-shot repair of a thinking level left raised by a pre-1.3.0 session, and the "thinkingLevel is no longer used" row in the settings menu. An unknown `thinkingLevel` key in `pi-plan-mode.json` is still preserved verbatim on save.
39
+ - The `plan-mode.json` settings fallback and its "Using legacy…" / "ignored because…" notices. Only `$PI_CODING_AGENT_DIR/pi-plan-mode.json` is read now; a host still on the old filename gets defaults and should rename the file.
40
+
41
+ Internally, the settings watcher and the menu/workflow lifecycle moved into their own modules and the state transitions share one helper; nothing else about `/plan` changed. `engines.node` now states Pi's own floor, `>=22.19.0`.
42
+
3
43
  ## 1.5.0
4
44
 
5
45
  ### Minor Changes
package/README.md CHANGED
@@ -17,7 +17,7 @@ The plan is written to a **durable file** that survives compaction, survives res
17
17
  - Two ways to implement: continue in this conversation, or open a fresh session that reads the same file.
18
18
  - `/plan export [path]` copies the plan anywhere, never overwriting an existing target.
19
19
  - Hand-edit the plan file at any time; every command and both implementation paths read from disk.
20
- - A companion `pi-plan-mode` **skill**, loaded on demand, carrying the plan-crafting craft the prompt only points at.
20
+ - A shipped **plan-craft doc** the prompt points at by path, carrying the plan-crafting craft the prompt itself only names.
21
21
 
22
22
  ## 📦 Install
23
23
 
@@ -118,11 +118,11 @@ Detection is by tool name at runtime, re-evaluated every turn — there is no de
118
118
 
119
119
  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.
120
120
 
121
- ## 📚 The companion skill
121
+ ## 📚 The plan-craft doc
122
122
 
123
- The package ships a `pi-plan-mode` skill (`skills/pi-plan-mode/SKILL.md`) alongside the extension. The system prompt is the enforcement surface and stays deliberately short; the skill is the depth layer it points at — what decision-complete actually means, why exploration comes before questions, what separates a question worth asking from one the repository already answered, and what belongs in a finished plan. One line in the planning prompt names it, and the model loads the body when it judges it needs it.
123
+ The system prompt is the enforcement surface and stays deliberately short. The depth layer it points at — what decision-complete actually means, why exploration comes before questions, what separates a question worth asking from one the repository already answered, and what belongs in a finished plan — is [`docs/plan-craft.md`](docs/plan-craft.md), shipped with the package. One line in the planning prompt names it by absolute path (resolved from the installed package, so it works under any install layout), and the model reads it when Plan Mode opens.
124
124
 
125
- Skill and extension version as one artifact: a skill describing a mode the installed extension does not have is a coupling failure waiting to happen.
125
+ It used to be a skill. A skill's description line is in every system prompt, which buys exactly one thing an injected pointer cannot: the model proposing planning unprompted. Across ~220 sessions after it shipped, every read of the file happened after the Plan Mode prompt was already active, never off the description, and the model never suggested `/plan` on its own — so the line was a tax on every session that never planned (about 95% of them) that bought nothing. A hard path injected only while the mode is active is the same document at zero cost outside it.
126
126
 
127
127
  ## 📊 Statusline and widget
128
128
 
@@ -145,7 +145,7 @@ packages/pi-plan-mode/
145
145
  │ ├── plan-file.ts # Durable plan file read/write/delete
146
146
  │ ├── interactive-ui.ts # Lazily loaded interactive menus
147
147
  │ └── *.ts # Prompt, question, export, settings modules
148
- ├── skills/pi-plan-mode/ # Companion plan-crafting skill
148
+ ├── docs/plan-craft.md # Plan-crafting depth, injected by path while the mode is active
149
149
  ├── test/
150
150
  ├── README.md
151
151
  ├── NOTICE.md
@@ -153,6 +153,10 @@ packages/pi-plan-mode/
153
153
  └── package.json
154
154
  ```
155
155
 
156
+ ## Changelog
157
+
158
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
159
+
156
160
  ## 📄 License
157
161
 
158
162
  MIT. See [`LICENSE`](./LICENSE).
@@ -1,9 +1,3 @@
1
- ---
2
- name: pi-plan-mode
3
- description: Craft decision-complete implementation plans in Plan mode — explore before asking, ask questions worth answering, and write a plan another agent can execute without guessing. Use while Plan mode is active, before calling plan_mode_complete, and when deciding whether a request needs a plan at all.
4
- license: MIT
5
- ---
6
-
7
1
  # pi-plan-mode
8
2
 
9
3
  The extension enforces the mechanics: `edit` and `write` are blocked, the plan is written to a durable file, and `plan_mode_complete` is the only way out. It cannot enforce the thing that decides whether the plan was worth making: **whether a competent implementer could execute it without asking you anything.** That is this file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-plan-mode",
3
- "version": "1.5.0",
3
+ "version": "1.7.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": [
@@ -22,20 +22,17 @@
22
22
  },
23
23
  "homepage": "https://github.com/hank-warren/pi-extensions/tree/main/packages/pi-plan-mode#readme",
24
24
  "engines": {
25
- "node": ">=18.0.0"
25
+ "node": ">=22.19.0"
26
26
  },
27
27
  "pi": {
28
28
  "extensions": [
29
29
  "./index.ts"
30
- ],
31
- "skills": [
32
- "./skills/pi-plan-mode"
33
30
  ]
34
31
  },
35
32
  "files": [
36
33
  "index.ts",
37
34
  "src",
38
- "skills",
35
+ "docs/plan-craft.md",
39
36
  "README.md",
40
37
  "NOTICE.md",
41
38
  "LICENSE",
package/src/command.ts CHANGED
@@ -1,4 +1,4 @@
1
- export interface CommandArgumentCompletion {
1
+ interface CommandArgumentCompletion {
2
2
  value: string;
3
3
  label: string;
4
4
  description?: string;
@@ -2,10 +2,10 @@ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
2
  import { Markdown } from "@earendil-works/pi-tui";
3
3
 
4
4
  export const PLAN_MODE_COMPLETE_TOOL_NAME = "plan_mode_complete";
5
- export const PLAN_MODE_COMPLETE_VERSION = 1;
6
- export const PLAN_MODE_MAX_CHARS = 50_000;
5
+ const PLAN_MODE_COMPLETE_VERSION = 1;
6
+ const PLAN_MODE_MAX_CHARS = 50_000;
7
7
 
8
- export type PlanModeCompletionDetails = {
8
+ type PlanModeCompletionDetails = {
9
9
  version: typeof PLAN_MODE_COMPLETE_VERSION;
10
10
  source: typeof PLAN_MODE_COMPLETE_TOOL_NAME;
11
11
  plan: string;
@@ -43,7 +43,7 @@ export function normalizePlanModeCompletion(input: unknown): NormalizePlanModeCo
43
43
  return { ok: true, plan };
44
44
  }
45
45
 
46
- export function planFromCompletionDetails(value: unknown) {
46
+ function planFromCompletionDetails(value: unknown) {
47
47
  if (!isRecord(value)) return undefined;
48
48
  if (
49
49
  value.version !== PLAN_MODE_COMPLETE_VERSION ||
@@ -78,7 +78,7 @@ type PlanModeCompletionRenderResult = {
78
78
  details?: unknown;
79
79
  };
80
80
 
81
- export function planModeCompletionMarkdown(result: PlanModeCompletionRenderResult) {
81
+ function planModeCompletionMarkdown(result: PlanModeCompletionRenderResult) {
82
82
  const content = result.content
83
83
  .filter((block) => block.type === "text" && typeof block.text === "string")
84
84
  .map((block) => block.text)
@@ -1,5 +1,4 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { LegacyThinkingCapture } from "./state.js";
3
2
 
4
3
  type AgentSettledHandler = (event: unknown, ctx: ExtensionContext) => unknown;
5
4
 
@@ -11,16 +10,6 @@ export function onAgentSettled(pi: ExtensionAPI, handler: AgentSettledHandler) {
11
10
  ).on("agent_settled", handler);
12
11
  }
13
12
 
14
- /**
15
- * Only the one-shot migration that undoes a pre-1.3.0 thinking-level change
16
- * still calls this. Plan mode never sets the thinking level otherwise.
17
- *
18
- * legacy: delete in 1.4.0
19
- */
20
- export function setPlanThinkingLevel(pi: ExtensionAPI, level: LegacyThinkingCapture["previous"]) {
21
- (pi.setThinkingLevel as unknown as (level: string) => void)(level);
22
- }
23
-
24
13
  export function isStaleExtensionContextError(error: unknown) {
25
14
  return (
26
15
  error instanceof Error &&
@@ -5,7 +5,7 @@ import type { PlanModeState } from "./state.js";
5
5
  type NewSessionOptions = Exclude<Parameters<ExtensionCommandContext["newSession"]>[0], undefined>;
6
6
  type ReplacementContext = Parameters<NonNullable<NewSessionOptions["withSession"]>>[0];
7
7
 
8
- export interface FreshImplementationRequest {
8
+ interface FreshImplementationRequest {
9
9
  plan: string;
10
10
  planPath: string;
11
11
  stateEntryType: string;
@@ -18,7 +18,7 @@ interface FreshImplementationFromStateOptions {
18
18
  stateEntryType: string;
19
19
  }
20
20
 
21
- export type FreshImplementationResult =
21
+ type FreshImplementationResult =
22
22
  | { kind: "started" }
23
23
  | { kind: "cancelled" }
24
24
  | { kind: "partial" }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * A scope captured when deferred work starts: the signal that work should race
3
+ * against, and the question "is what I was started for still the current
4
+ * thing?".
5
+ */
6
+ export interface LifecycleScope {
7
+ readonly signal: AbortSignal;
8
+ isCurrent(): boolean;
9
+ }
10
+
11
+ /**
12
+ * Two nested generations decide whether deferred Plan-mode work may still act.
13
+ *
14
+ * The session generation moves when Pi replaces or shuts down the session: a
15
+ * menu, a settings reload, or a question left waiting from the previous session
16
+ * must never write to the new one. The workflow generation moves on every
17
+ * enter/exit/implement, so a menu opened against one plan cannot act after the
18
+ * user has moved on — while a settings reload, which belongs to the session
19
+ * rather than to a plan, is deliberately left alone by it.
20
+ *
21
+ * The abort signal is the second half of the same rule: it stops work that is
22
+ * already blocked on the UI, where a generation check would never be reached.
23
+ */
24
+ export function createLifecycle() {
25
+ let sessionGeneration = 0;
26
+ let workflowGeneration = 0;
27
+ let controller = new AbortController();
28
+
29
+ const sessionScope = (): LifecycleScope => {
30
+ const session = sessionGeneration;
31
+ const active = controller;
32
+ return {
33
+ signal: active.signal,
34
+ isCurrent: () => session === sessionGeneration && !active.signal.aborted,
35
+ };
36
+ };
37
+
38
+ /**
39
+ * Ends the current session: everything captured before this call goes stale
40
+ * and everything waiting on the signal is aborted with `reason`. The aborted
41
+ * signal stays in place, so anything captured *after* it is stale too —
42
+ * which is what a shut-down session wants: there is no next session to be
43
+ * current for, and a menu opened in that window must refuse to run.
44
+ */
45
+ const endSession = (reason: string) => {
46
+ sessionGeneration += 1;
47
+ controller.abort(new DOMException(reason, "AbortError"));
48
+ };
49
+
50
+ return {
51
+ /** The live session signal, for composing with a caller's own. */
52
+ get signal() {
53
+ return controller.signal;
54
+ },
55
+ endSession,
56
+ /**
57
+ * Ends the current session and opens the next one, whose scope is
58
+ * returned: work started from here races against a fresh signal.
59
+ */
60
+ nextSession(reason: string): LifecycleScope {
61
+ endSession(reason);
62
+ controller = new AbortController();
63
+ return sessionScope();
64
+ },
65
+ /** Supersedes menus and prompts opened against the previous plan state. */
66
+ nextWorkflow() {
67
+ workflowGeneration += 1;
68
+ },
69
+ /** The scope for menu-scale work: stale as soon as either generation moves. */
70
+ capture(): LifecycleScope {
71
+ const session = sessionScope();
72
+ const workflow = workflowGeneration;
73
+ return {
74
+ signal: session.signal,
75
+ isCurrent: () => session.isCurrent() && workflow === workflowGeneration,
76
+ };
77
+ },
78
+ };
79
+ }
@@ -8,7 +8,7 @@ import type { PlanModeState } from "./state.js";
8
8
 
9
9
  export { DEFAULT_PLAN_EXPORT_PATH };
10
10
 
11
- export interface PlanExportResult {
11
+ interface PlanExportResult {
12
12
  path: string;
13
13
  }
14
14
 
@@ -17,7 +17,7 @@ export interface PlanExportDestination {
17
17
  resolvedPath: string;
18
18
  }
19
19
 
20
- export interface PlanExportLifecycle {
20
+ interface PlanExportLifecycle {
21
21
  signal: AbortSignal;
22
22
  isCurrent(): boolean;
23
23
  getState?(): PlanModeState;
@@ -74,7 +74,7 @@ export async function exportStoredPlan(
74
74
  return true;
75
75
  }
76
76
 
77
- export async function exportPlanToFile(
77
+ async function exportPlanToFile(
78
78
  plan: string,
79
79
  requestedPath: string | undefined,
80
80
  cwd: string,
@@ -109,7 +109,7 @@ export function planExportDestination(defaultPath: string, cwd: string): PlanExp
109
109
  };
110
110
  }
111
111
 
112
- export function resolvePlanExportPath(
112
+ function resolvePlanExportPath(
113
113
  requestedPath: string | undefined,
114
114
  cwd: string,
115
115
  defaultPath = DEFAULT_PLAN_EXPORT_PATH,
package/src/plan-file.ts CHANGED
@@ -4,7 +4,7 @@ import { mkdir, open, rename, rm, unlink, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
6
 
7
- export const PLANS_DIRECTORY = "plans";
7
+ const PLANS_DIRECTORY = "plans";
8
8
  const MAX_PLAN_BYTES = 1024 * 1024;
9
9
 
10
10
  /**
@@ -79,7 +79,3 @@ export async function readPlanFile(path: string): Promise<string | undefined> {
79
79
  export async function deletePlanFile(path: string): Promise<void> {
80
80
  await unlink(path).catch(() => undefined);
81
81
  }
82
-
83
- export async function planFileExists(path: string): Promise<boolean> {
84
- return (await readPlanFile(path)) !== undefined;
85
- }
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
3
 
4
- export interface PlanLaunchMenuOptions {
4
+ interface PlanLaunchMenuOptions {
5
5
  statusText: string;
6
6
  signal: AbortSignal;
7
7
  isCurrent(): boolean;