@pify/plan-mode 0.3.0 → 0.4.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/README.md CHANGED
@@ -25,6 +25,7 @@ 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
+ - **Reopen a saved plan** (v0.4): `/plan open <file>` takes a filename, a stem, or any distinctive fragment (`/plan open oauth`), hands the plan text back to the agent as a hidden message, and restarts step tracking from what the file says **now** — the file is the source of truth, not the step list it produced last time.
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
@@ -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.0",
3
+ "version": "0.4.0",
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
+ }