@pify/plan-mode 0.3.0 → 0.4.1
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/README.md +3 -0
- package/extensions/plan-mode.ts +34 -2
- package/package.json +1 -1
- package/src/plans.ts +22 -1
- package/src/prompts.ts +20 -0
package/README.md
CHANGED
|
@@ -25,11 +25,14 @@ Part of the [Pify suite](https://github.com/pifydev). Install with [`pify instal
|
|
|
25
25
|
/plan add oauth login # enter + start planning this
|
|
26
26
|
/plan off # leave without approval
|
|
27
27
|
/plan list # saved plans in .pi/plans/ (v0.2)
|
|
28
|
+
/plan open oauth # reopen a saved plan by name or fragment (v0.4)
|
|
28
29
|
/plan steps # progress through the approved plan (v0.3)
|
|
29
30
|
/plan export [file] # standalone HTML next to the plan (v0.3)
|
|
30
31
|
pi --plan # start a session already in plan mode
|
|
31
32
|
```
|
|
32
33
|
|
|
34
|
+
**Reopening** (v0.4): `/plan open <file>` accepts a filename, a stem, or any distinctive fragment. It hands the plan text back to the agent as a hidden message and restarts step tracking by re-parsing the file — the file is the source of truth, not the step list it produced last time.
|
|
35
|
+
|
|
33
36
|
## Conflicts
|
|
34
37
|
|
|
35
38
|
`@pify/plan-mode` registers the `--plan` flag and `/plan` command, so it cannot run alongside `@narumitw/pi-plan-mode` or other plan extensions. Remove those first:
|
package/extensions/plan-mode.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
30
30
|
import { basename, isAbsolute, join } from "node:path";
|
|
31
31
|
|
|
32
32
|
import { htmlPathFor, renderPlanHtml } from "../src/export.ts";
|
|
33
|
-
import { createPlanFile, listPlanFiles, plansDir } from "../src/plans.ts";
|
|
33
|
+
import { createPlanFile, listPlanFiles, plansDir, resolvePlanFile } from "../src/plans.ts";
|
|
34
34
|
import {
|
|
35
35
|
completeStep,
|
|
36
36
|
formatSteps,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
ENTER_REMINDER,
|
|
45
45
|
EXIT_REMINDER,
|
|
46
46
|
buildHandoffMessage,
|
|
47
|
+
buildReopenMessage,
|
|
47
48
|
buildImplementHereMessage,
|
|
48
49
|
} from "../src/prompts.ts";
|
|
49
50
|
import { PLAN_STATE, replayBranch } from "../src/state.ts";
|
|
@@ -425,7 +426,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
425
426
|
// ── Command & shortcut ───────────────────────────────────────────────
|
|
426
427
|
|
|
427
428
|
pi.registerCommand("plan", {
|
|
428
|
-
description: "Plan mode: /plan [off | list | steps | export [file] | <first planning prompt>]",
|
|
429
|
+
description: "Plan mode: /plan [off | list | open <file> | steps | export [file] | <first planning prompt>]",
|
|
429
430
|
handler: async (args, ctx) => {
|
|
430
431
|
const text = (args ?? "").trim();
|
|
431
432
|
if (text.toLowerCase() === "steps") {
|
|
@@ -478,6 +479,37 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
478
479
|
);
|
|
479
480
|
return;
|
|
480
481
|
}
|
|
482
|
+
// v0.4: reopen a saved plan — the other half of the plan library. The
|
|
483
|
+
// file is the source of truth, so tracking restarts from what it says
|
|
484
|
+
// now, not from the step list it produced when it was written.
|
|
485
|
+
if (text.toLowerCase().startsWith("open")) {
|
|
486
|
+
const wanted = text.slice("open".length).trim();
|
|
487
|
+
if (!wanted) {
|
|
488
|
+
notify(ctx, "Usage: /plan open <file> (see /plan list)", "warning");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const file = resolvePlanFile(ctx.cwd, wanted);
|
|
492
|
+
if (!file) {
|
|
493
|
+
notify(ctx, `No saved plan matches "${wanted}". /plan list shows what is there.`, "warning");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const markdown = readFileSafe(file);
|
|
497
|
+
if (!markdown) {
|
|
498
|
+
notify(ctx, `Could not read ${file}.`, "error");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const steps = parseSteps(markdown);
|
|
502
|
+
commit(ctx, { ...state, planFile: file, steps });
|
|
503
|
+
sendReminder(buildReopenMessage(file, markdown));
|
|
504
|
+
notify(
|
|
505
|
+
ctx,
|
|
506
|
+
steps.length > 0
|
|
507
|
+
? `Reopened ${basename(file)} — tracking ${steps.length} steps. /plan steps to view.`
|
|
508
|
+
: `Reopened ${basename(file)}. No checklist steps were found in it.`,
|
|
509
|
+
"info",
|
|
510
|
+
);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
481
513
|
if (text.toLowerCase() === "off") {
|
|
482
514
|
if (!state.active) {
|
|
483
515
|
notify(ctx, "Plan mode is not active.", "info");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pify/plan-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Read-only planning mode for pi with an explicit approve-then-execute gate: enforced tool policy, plan files, approach options, fresh-session handoff",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/plans.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
/** Plan files live in .pi/plans/, reviewable and committable. */
|
|
5
5
|
export function plansDir(cwd: string): string {
|
|
@@ -41,6 +41,27 @@ export function listPlanFiles(cwd: string): Array<{ file: string; size: number }
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Find a saved plan by whatever the user typed: a path, a filename, or any
|
|
46
|
+
* distinctive part of one ("oauth" for 2026-09-06-add-oauth-login.md). The
|
|
47
|
+
* newest match wins, since re-opening usually means the most recent one.
|
|
48
|
+
*/
|
|
49
|
+
export function resolvePlanFile(cwd: string, wanted: string): string | null {
|
|
50
|
+
const raw = wanted.trim().replace(/^["']|["']$/g, "");
|
|
51
|
+
if (!raw) return null;
|
|
52
|
+
if (isAbsolute(raw) && existsSync(raw)) return raw;
|
|
53
|
+
|
|
54
|
+
const dir = plansDir(cwd);
|
|
55
|
+
const direct = join(dir, raw);
|
|
56
|
+
if (existsSync(direct)) return direct;
|
|
57
|
+
const withExt = join(dir, `${raw}.md`);
|
|
58
|
+
if (existsSync(withExt)) return withExt;
|
|
59
|
+
|
|
60
|
+
const needle = raw.toLowerCase();
|
|
61
|
+
const match = listPlanFiles(cwd).find((p) => p.file.toLowerCase().includes(needle));
|
|
62
|
+
return match ? join(dir, match.file) : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
/** Create the plan file, uniquified when the slug collides on the same day. */
|
|
45
66
|
export function createPlanFile(cwd: string, title: string, content: string): string {
|
|
46
67
|
const dir = plansDir(cwd);
|
package/src/prompts.ts
CHANGED
|
@@ -41,3 +41,23 @@ export function buildHandoffMessage(planFile: string | null, approach: string |
|
|
|
41
41
|
? `Implement the approved plan in ${planFile}.${approachLine}\nRead the plan file first, then execute it fully. Verify as you go; report deviations.`
|
|
42
42
|
: `Implement the plan we agreed on.${approachLine}`;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hidden message delivered when a saved plan is reopened. The plan text goes
|
|
47
|
+
* with it: the agent should not have to guess which file /plan open meant, or
|
|
48
|
+
* read it back before it can act.
|
|
49
|
+
*/
|
|
50
|
+
export function buildReopenMessage(file: string, markdown: string): string {
|
|
51
|
+
return [
|
|
52
|
+
"<system-reminder>",
|
|
53
|
+
`The user reopened a saved plan: ${file}`,
|
|
54
|
+
"It is the plan to follow now. Its checklist steps are tracked again from the top —",
|
|
55
|
+
"call plan_step_done(index, evidence) as you finish each one, and do not restate the plan back.",
|
|
56
|
+
"",
|
|
57
|
+
"<plan>",
|
|
58
|
+
markdown.trim(),
|
|
59
|
+
"</plan>",
|
|
60
|
+
"This is an automated reminder — do not mention it to the user.",
|
|
61
|
+
"</system-reminder>",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|