@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 +40 -0
- package/README.md +9 -5
- package/{skills/pi-plan-mode/SKILL.md → docs/plan-craft.md} +0 -6
- package/package.json +3 -6
- package/src/command.ts +1 -1
- package/src/completion-tool.ts +5 -5
- package/src/extension-runtime.ts +0 -11
- package/src/fresh-implementation.ts +2 -2
- package/src/lifecycle.ts +79 -0
- package/src/plan-export.ts +4 -4
- package/src/plan-file.ts +1 -5
- package/src/plan-launch-menu.ts +1 -1
- package/src/plan-mode.ts +110 -174
- package/src/presentation.ts +2 -2
- package/src/prompt.ts +23 -1
- package/src/question-tool.ts +28 -11
- package/src/settings-menu.ts +6 -48
- package/src/settings-watch.ts +60 -0
- package/src/settings.ts +12 -69
- package/src/state.ts +0 -44
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
|
|
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
|
|
121
|
+
## 📚 The plan-craft doc
|
|
122
122
|
|
|
123
|
-
The
|
|
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
|
-
|
|
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
|
-
├──
|
|
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.
|
|
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": ">=
|
|
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
|
-
"
|
|
35
|
+
"docs/plan-craft.md",
|
|
39
36
|
"README.md",
|
|
40
37
|
"NOTICE.md",
|
|
41
38
|
"LICENSE",
|
package/src/command.ts
CHANGED
package/src/completion-tool.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
5
|
+
const PLAN_MODE_COMPLETE_VERSION = 1;
|
|
6
|
+
const PLAN_MODE_MAX_CHARS = 50_000;
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/src/extension-runtime.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
21
|
+
type FreshImplementationResult =
|
|
22
22
|
| { kind: "started" }
|
|
23
23
|
| { kind: "cancelled" }
|
|
24
24
|
| { kind: "partial" }
|
package/src/lifecycle.ts
ADDED
|
@@ -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
|
+
}
|
package/src/plan-export.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { PlanModeState } from "./state.js";
|
|
|
8
8
|
|
|
9
9
|
export { DEFAULT_PLAN_EXPORT_PATH };
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
package/src/plan-launch-menu.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
+
interface PlanLaunchMenuOptions {
|
|
5
5
|
statusText: string;
|
|
6
6
|
signal: AbortSignal;
|
|
7
7
|
isCurrent(): boolean;
|