@hank-warren/pi-plan-mode 1.1.0 → 1.2.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 +36 -0
- package/README.md +4 -0
- package/package.json +2 -2
- package/src/plan-mode.ts +81 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @hank-warren/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 358aa1f: re-read `pi-plan-mode.json` when it changes instead of only at session start
|
|
8
|
+
|
|
9
|
+
The settings file was read once per session, so a hand-edit — or a Settings save
|
|
10
|
+
from another session — did nothing until the next restart. That sat badly next to
|
|
11
|
+
the plan file, which has been hand-editable and read from disk since 1.0.
|
|
12
|
+
|
|
13
|
+
Plan mode now watches the settings file's directory (not the file: saves land
|
|
14
|
+
through a temp file and an atomic rename, which replaces the inode) and re-reads
|
|
15
|
+
on a 75 ms debounce, so one save costs one load.
|
|
16
|
+
|
|
17
|
+
An unparseable file is still reported and replaced by the defaults at session
|
|
18
|
+
start. Mid-session it is ignored and the last good settings stay in place: a
|
|
19
|
+
reload sees the file the moment an editor touches it, so an invalid read is
|
|
20
|
+
usually a partial write, and there is no context to report it through anyway.
|
|
21
|
+
Deleting the file mid-session does restore the defaults.
|
|
22
|
+
|
|
23
|
+
Session start now also honours the injected `settingsPath` dependency when
|
|
24
|
+
reading, which previously only the Settings menu used — the two could disagree
|
|
25
|
+
about which file they were looking at.
|
|
26
|
+
|
|
27
|
+
## 1.1.1
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 7d96995: Make every declared dependency accurate.
|
|
32
|
+
|
|
33
|
+
`pi-permission-selector` no longer declares a `@earendil-works/pi-coding-agent` peer dependency. It imports nothing from it — the claim was left over from 1.0.0, which removed the extension and the selector monkey patch and turned the package into a pure library.
|
|
34
|
+
|
|
35
|
+
`pi-auto-permissions` floors `@hank-warren/pi-permission-selector` at `^1.1.0` and `pi-plan-mode` floors `@narumitw/pi-tui-kit` at `^0.49.3`, in both cases the lowest version the package is actually tested against. Neither uses a feature the old floor lacked, so nothing changes at install time under caret resolution; the ranges simply no longer claim support that nothing verifies.
|
|
36
|
+
|
|
37
|
+
`scripts/validate.py` now enforces that a package's Pi `peerDependencies` are exactly the Pi packages its shipped sources import, in both directions.
|
|
38
|
+
|
|
3
39
|
## 1.1.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -72,6 +72,8 @@ Writes are atomic (temp file plus rename), so a reader never sees a partial plan
|
|
|
72
72
|
|
|
73
73
|
Open **Settings** from the `/plan` menu, or edit `$PI_CODING_AGENT_DIR/pi-plan-mode.json` (normally `~/.pi/agent/pi-plan-mode.json`). The file is optional.
|
|
74
74
|
|
|
75
|
+
The file is read at session start and **re-read whenever it changes**, so a hand-edit — or a save from another session — applies without restarting. Like the plan file itself, it is edited on disk and read from disk.
|
|
76
|
+
|
|
75
77
|
```json
|
|
76
78
|
{
|
|
77
79
|
"thinkingLevel": "inherit",
|
|
@@ -89,6 +91,8 @@ Open **Settings** from the `/plan` menu, or edit `$PI_CODING_AGENT_DIR/pi-plan-m
|
|
|
89
91
|
|
|
90
92
|
Unknown keys are preserved. Settings removed in 1.0 (`defaultPlanTools`, `bashPolicy`, `safeSubcommands`, `implementationPlanRetention`) are ignored rather than treated as errors, so an existing settings file keeps working.
|
|
91
93
|
|
|
94
|
+
A settings file that does not parse is reported at session start and the defaults are used. Mid-session it is ignored instead, leaving the last good settings in place: an edit is seen the moment your editor touches the file, so an unreadable one is usually a half-finished save rather than what you meant.
|
|
95
|
+
|
|
92
96
|
## 🔐 What Plan mode does and does not enforce
|
|
93
97
|
|
|
94
98
|
Plan mode blocks exactly three tools while planning: `edit`, `write`, and `update_plan`. That is the whole enforcement surface.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-plan-mode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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": [
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"@earendil-works/pi-tui": "*"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@narumitw/pi-tui-kit": "^0.49.
|
|
45
|
+
"@narumitw/pi-tui-kit": "^0.49.3"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/plan-mode.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { basename, dirname } from "node:path";
|
|
1
3
|
import type {
|
|
2
4
|
ExtensionAPI,
|
|
3
5
|
ExtensionCommandContext,
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
awaitPlanModeSettingsWrites,
|
|
47
49
|
configuredThinkingLevel,
|
|
48
50
|
type PlanModeSettings,
|
|
51
|
+
planModeSettingsPath,
|
|
49
52
|
readPlanModeSettings,
|
|
50
53
|
} from "./settings.js";
|
|
51
54
|
import { type PlanModeState, restorePlanModeState } from "./state.js";
|
|
@@ -58,6 +61,12 @@ const STATE_ENTRY_TYPE = "plan-mode-state";
|
|
|
58
61
|
* mutates the active tool set and never fights other extensions for it.
|
|
59
62
|
*/
|
|
60
63
|
const BLOCKED_TOOLS = new Set(["edit", "write", "update_plan"]);
|
|
64
|
+
/**
|
|
65
|
+
* One hand-edit or menu save fans out into several filesystem events (temp file
|
|
66
|
+
* created, renamed into place). Collapsing them into one re-read keeps a save
|
|
67
|
+
* to a single load.
|
|
68
|
+
*/
|
|
69
|
+
const SETTINGS_RELOAD_DEBOUNCE_MS = 75;
|
|
61
70
|
|
|
62
71
|
/**
|
|
63
72
|
* Which question tool the prompt should name this turn.
|
|
@@ -124,6 +133,8 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
124
133
|
let workflowGeneration = 0;
|
|
125
134
|
let refreshStateBeforeFirstAgentStart = false;
|
|
126
135
|
let menuController = new AbortController();
|
|
136
|
+
let settingsWatch: ReturnType<typeof watch> | undefined;
|
|
137
|
+
let settingsReloadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
127
138
|
const persistState = () => pi.appendEntry<PlanModeState>(STATE_ENTRY_TYPE, state);
|
|
128
139
|
const planExports = createPlanExportController({
|
|
129
140
|
getState: () => state,
|
|
@@ -295,6 +306,73 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
295
306
|
},
|
|
296
307
|
});
|
|
297
308
|
|
|
309
|
+
const readRuntimeSettings = () =>
|
|
310
|
+
dependencies.readSettings?.() ?? readPlanModeSettings(dependencies.settingsPath);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* `ctx` present means this is the session-start load: problems are reported
|
|
314
|
+
* and anything unusable falls back to defaults.
|
|
315
|
+
*
|
|
316
|
+
* `ctx` absent means a watch-triggered reload, which keeps the last good
|
|
317
|
+
* settings when the file no longer parses. A hand-edit is observed the moment
|
|
318
|
+
* the editor touches the file, so an invalid read is usually a half-written
|
|
319
|
+
* save rather than intent — discarding a working export path for it, with no
|
|
320
|
+
* `ctx` to explain why, would be worse than waiting for the next write. A
|
|
321
|
+
* genuinely broken file is still reported at the next session start.
|
|
322
|
+
*/
|
|
323
|
+
const loadPlanModeSettings = async (generation: number, ctx?: ExtensionContext) => {
|
|
324
|
+
const loaded = await readRuntimeSettings();
|
|
325
|
+
if (generation !== menuGeneration || menuController.signal.aborted) return;
|
|
326
|
+
if (loaded.kind === "invalid" && !ctx) return;
|
|
327
|
+
settings = loaded.kind === "loaded" ? loaded.settings : { thinkingLevel: "inherit" };
|
|
328
|
+
if (!ctx) return;
|
|
329
|
+
if (loaded.kind === "invalid") {
|
|
330
|
+
ctx.ui.notify(`pi-plan-mode settings ignored: ${loaded.reason}`, "warning");
|
|
331
|
+
}
|
|
332
|
+
if (loaded.notice) ctx.ui.notify(loaded.notice, "warning");
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const stopPlanModeSettingsWatch = () => {
|
|
336
|
+
if (settingsReloadTimer) {
|
|
337
|
+
clearTimeout(settingsReloadTimer);
|
|
338
|
+
settingsReloadTimer = undefined;
|
|
339
|
+
}
|
|
340
|
+
settingsWatch?.close();
|
|
341
|
+
settingsWatch = undefined;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Watches the settings file's directory rather than the file itself: saves go
|
|
346
|
+
* through a temp file and an atomic rename, and a watch bound to the old inode
|
|
347
|
+
* would go deaf after the first one.
|
|
348
|
+
*/
|
|
349
|
+
const startPlanModeSettingsWatch = (generation: number) => {
|
|
350
|
+
stopPlanModeSettingsWatch();
|
|
351
|
+
if (dependencies.readSettings) return;
|
|
352
|
+
const watchedPath = dependencies.settingsPath ?? planModeSettingsPath();
|
|
353
|
+
const watchedFile = basename(watchedPath);
|
|
354
|
+
try {
|
|
355
|
+
const watcher = watch(dirname(watchedPath), { persistent: false }, (event, changedFile) => {
|
|
356
|
+
if (event !== "rename" && event !== "change") return;
|
|
357
|
+
// A null filename means the platform could not name the entry; reload
|
|
358
|
+
// rather than miss the edit. The agent directory holds other churn, so
|
|
359
|
+
// a named entry that is not ours is ignored.
|
|
360
|
+
if (changedFile && changedFile.toString() !== watchedFile) return;
|
|
361
|
+
if (settingsReloadTimer) clearTimeout(settingsReloadTimer);
|
|
362
|
+
settingsReloadTimer = setTimeout(() => {
|
|
363
|
+
settingsReloadTimer = undefined;
|
|
364
|
+
void loadPlanModeSettings(generation);
|
|
365
|
+
}, SETTINGS_RELOAD_DEBOUNCE_MS);
|
|
366
|
+
});
|
|
367
|
+
watcher.on("error", stopPlanModeSettingsWatch);
|
|
368
|
+
settingsWatch = watcher;
|
|
369
|
+
} catch {
|
|
370
|
+
// An unwatchable directory only costs the live reload; settings still
|
|
371
|
+
// load at session start.
|
|
372
|
+
stopPlanModeSettingsWatch();
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
298
376
|
pi.on("session_start", async (event, ctx) => {
|
|
299
377
|
const generation = ++menuGeneration;
|
|
300
378
|
refreshStateBeforeFirstAgentStart = event.reason === "new";
|
|
@@ -305,13 +383,9 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
305
383
|
settings = { thinkingLevel: "inherit" };
|
|
306
384
|
sessionPlanPath = resolveSessionPlanPath(ctx);
|
|
307
385
|
restoreState(ctx);
|
|
308
|
-
|
|
386
|
+
await loadPlanModeSettings(generation, ctx);
|
|
309
387
|
if (generation !== menuGeneration || menuController.signal.aborted) return;
|
|
310
|
-
|
|
311
|
-
else if (loadedSettings.kind === "invalid") {
|
|
312
|
-
ctx.ui.notify(`pi-plan-mode settings ignored: ${loadedSettings.reason}`, "warning");
|
|
313
|
-
}
|
|
314
|
-
if (loadedSettings.notice) ctx.ui.notify(loadedSettings.notice, "warning");
|
|
388
|
+
startPlanModeSettingsWatch(generation);
|
|
315
389
|
const persistFlagActivation = pi.getFlag("plan") === true && !state.enabled;
|
|
316
390
|
if (persistFlagActivation) {
|
|
317
391
|
state = { ...state, enabled: true, awaitingAction: state.planPath !== undefined };
|
|
@@ -336,6 +410,7 @@ export default function planMode(pi: ExtensionAPI, dependencies: PlanModeDepende
|
|
|
336
410
|
|
|
337
411
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
338
412
|
menuGeneration += 1;
|
|
413
|
+
stopPlanModeSettingsWatch();
|
|
339
414
|
menuController.abort(new DOMException("Plan-mode session shut down", "AbortError"));
|
|
340
415
|
pendingReadyNonce = undefined;
|
|
341
416
|
latestCommandContext = undefined;
|