@dreki-gg/pi-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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @dreki-gg/pi-plan-mode
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`c86c935`](https://github.com/dreki-gg/pi-extensions/commit/c86c9352150a5bed61602243c8164bdd5d679745) Thanks [@jalbarrang](https://github.com/jalbarrang)! - Add plans.json lifecycle tracking and CLI for cleaning completed plans
8
+
9
+ - Extension now writes `.plans/plans.json` to track plan status (`in-progress` / `done`) with timestamps and titles
10
+ - Plans are recorded as `in-progress` when created, marked `done` when all execution steps complete
11
+ - New `pi-plan-mode clean` CLI (`npx @dreki-gg/pi-plan-mode clean [--dry-run]`) removes completed plan directories while preserving in-flight plans
12
+ - Cleanup step added to publish.yml workflow to auto-clean done plans on merge to main
13
+ - Removed stale `docs/plans/` from browser-tools and subagent packages
14
+
15
+ ## 0.3.1
16
+
17
+ ### Patch Changes
18
+
19
+ - [`d133c3d`](https://github.com/dreki-gg/pi-extensions/commit/d133c3da917e7e5def568d27d6cde8ae8a6c00d2) Thanks [@jalbarrang](https://github.com/jalbarrang)! - Mark pi peer dependencies as optional so npm does not auto-install pi internals when installing extension packages.
20
+
3
21
  ## 0.3.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -67,12 +67,84 @@ When **Execute Plan** is selected:
67
67
 
68
68
  ```
69
69
  .plans/
70
+ ├── plans.json # Tracking manifest — plan status lifecycle
70
71
  └── add-auth-middleware/
71
- ├── PLAN.md # Numbered plan with context
72
- ├── START-PROMPT.md # Self-contained executor handoff prompt
73
- └── ... # Optional supporting files
72
+ ├── PLAN.md # Numbered plan with context
73
+ ├── START-PROMPT.md # Self-contained executor handoff prompt
74
+ └── ... # Optional supporting files
74
75
  ```
75
76
 
77
+ ### plans.json
78
+
79
+ The extension automatically maintains `.plans/plans.json` to track plan lifecycle:
80
+
81
+ ```json
82
+ {
83
+ "add-auth-middleware": {
84
+ "status": "in-progress",
85
+ "title": "Add Authentication Middleware with JWT Support",
86
+ "created": "2026-05-08T12:00:00.000Z",
87
+ "completed": null
88
+ },
89
+ "fix-ci-flakes": {
90
+ "status": "done",
91
+ "title": "Fix CI Flaky Tests",
92
+ "created": "2026-05-07T10:00:00.000Z",
93
+ "completed": "2026-05-07T14:30:00.000Z"
94
+ }
95
+ }
96
+ ```
97
+
98
+ Plans start as `"in-progress"` when created and are marked `"done"` when all execution steps complete. This prevents accidental deletion of in-flight plans.
99
+
100
+ ## Cleaning completed plans
101
+
102
+ Use the CLI to remove completed plan directories:
103
+
104
+ ```bash
105
+ # Preview what would be deleted
106
+ npx @dreki-gg/pi-plan-mode clean --dry-run
107
+
108
+ # Delete completed plans and update plans.json
109
+ npx @dreki-gg/pi-plan-mode clean
110
+ ```
111
+
112
+ In-flight plans (`"status": "in-progress"`) are never touched.
113
+
114
+ ### GitHub Actions
115
+
116
+ Clean done plans automatically after merge — similar to changesets:
117
+
118
+ ```yaml
119
+ name: Clean Plans
120
+
121
+ on:
122
+ push:
123
+ branches: [main]
124
+ paths: ['.plans/**']
125
+
126
+ jobs:
127
+ clean:
128
+ runs-on: ubuntu-latest
129
+ steps:
130
+ - uses: actions/checkout@v5
131
+ - uses: actions/setup-node@v4
132
+ with:
133
+ node-version: '24'
134
+ - run: npx @dreki-gg/pi-plan-mode clean
135
+ - name: Commit cleanup
136
+ run: |
137
+ git config user.name "github-actions[bot]"
138
+ git config user.email "github-actions[bot]@users.noreply.github.com"
139
+ git add .plans/
140
+ git diff --cached --quiet || git commit -m "chore: clean completed plans"
141
+ git push
142
+ ```
143
+
144
+ ### Should you gitignore `.plans/`?
145
+
146
+ **No.** Commit your plans — they provide decision history and execution context. Use the `clean` CLI to remove done plans after merge, keeping the directory lean. Plans are execution blueprints, not permanent documentation; for lasting decisions, use ADRs.
147
+
76
148
  ## Footer indicators
77
149
 
78
150
  - `📝 plan` — plan mode active (opus-4-6:medium, strict bash)
@@ -81,3 +153,14 @@ When **Execute Plan** is selected:
81
153
  ## Bash safety
82
154
 
83
155
  In plan mode, bash is restricted to read-only commands (ls, grep, git status, cat, rg, etc.). Destructive commands (rm, mv, git commit, etc.) are blocked.
156
+
157
+ ## CLI reference
158
+
159
+ ```
160
+ pi-plan-mode clean [--dry-run]
161
+ ```
162
+
163
+ | Option | Description |
164
+ | ----------- | ------------------------------------------------ |
165
+ | `clean` | Remove completed plan directories, update manifest |
166
+ | `--dry-run` | Show what would be deleted without deleting |
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI to clean completed plans from `.plans/`.
4
+ *
5
+ * Usage:
6
+ * npx @dreki-gg/pi-plan-mode clean [--dry-run]
7
+ *
8
+ * Reads `.plans/plans.json`, deletes directories for plans with status "done",
9
+ * and updates `plans.json` to remove them.
10
+ *
11
+ * Designed for use in GitHub Actions after merge — similar to changesets.
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, rmSync, readdirSync, existsSync } from 'node:fs';
15
+ import { resolve, join } from 'node:path';
16
+
17
+ const PLANS_DIR = '.plans';
18
+ const PLANS_JSON = join(PLANS_DIR, 'plans.json');
19
+
20
+ function main() {
21
+ const args = process.argv.slice(2);
22
+ const command = args[0];
23
+
24
+ if (command !== 'clean') {
25
+ console.error('Usage: pi-plan-mode clean [--dry-run]\n');
26
+ console.error('Commands:');
27
+ console.error(' clean Remove completed plan directories and update plans.json\n');
28
+ console.error('Options:');
29
+ console.error(' --dry-run Show what would be deleted without actually deleting');
30
+ process.exit(1);
31
+ }
32
+
33
+ const dryRun = args.includes('--dry-run');
34
+ const plansJsonPath = resolve(PLANS_JSON);
35
+
36
+ if (!existsSync(plansJsonPath)) {
37
+ console.log('No .plans/plans.json found — nothing to clean.');
38
+ process.exit(0);
39
+ }
40
+
41
+ /** @type {Record<string, { status: string; title: string; created: string; completed: string | null }>} */
42
+ let manifest;
43
+ try {
44
+ manifest = JSON.parse(readFileSync(plansJsonPath, 'utf-8'));
45
+ } catch (err) {
46
+ console.error(`Failed to parse ${PLANS_JSON}:`, err);
47
+ process.exit(1);
48
+ }
49
+
50
+ const donePlans = Object.entries(manifest).filter(([, entry]) => entry.status === 'done');
51
+ const inFlightPlans = Object.entries(manifest).filter(
52
+ ([, entry]) => entry.status === 'in-progress',
53
+ );
54
+
55
+ if (donePlans.length === 0) {
56
+ console.log('No completed plans to clean.');
57
+ if (inFlightPlans.length > 0) {
58
+ console.log(`\n${inFlightPlans.length} plan(s) still in progress:`);
59
+ for (const [name, entry] of inFlightPlans) {
60
+ console.log(` ○ ${name} — ${entry.title}`);
61
+ }
62
+ }
63
+ process.exit(0);
64
+ }
65
+
66
+ console.log(dryRun ? 'Dry run — would clean:\n' : 'Cleaning completed plans:\n');
67
+
68
+ let cleaned = 0;
69
+ for (const [name, entry] of donePlans) {
70
+ const planPath = resolve(join(PLANS_DIR, name));
71
+ const exists = existsSync(planPath);
72
+
73
+ if (dryRun) {
74
+ console.log(` ✓ ${name} — ${entry.title}${exists ? '' : ' (directory already missing)'}`);
75
+ } else {
76
+ if (exists) {
77
+ rmSync(planPath, { recursive: true, force: true });
78
+ console.log(` ✓ Deleted ${PLANS_DIR}/${name} — ${entry.title}`);
79
+ } else {
80
+ console.log(` ✓ ${name} — directory already missing, removing from manifest`);
81
+ }
82
+ delete manifest[name];
83
+ cleaned++;
84
+ }
85
+ }
86
+
87
+ if (!dryRun) {
88
+ const remaining = Object.keys(manifest).length;
89
+ if (remaining === 0) {
90
+ rmSync(plansJsonPath, { force: true });
91
+ // Remove .plans/ if completely empty
92
+ const plansDir = resolve(PLANS_DIR);
93
+ try {
94
+ if (existsSync(plansDir) && readdirSync(plansDir).length === 0) {
95
+ rmSync(plansDir, { recursive: true, force: true });
96
+ console.log(`\nRemoved empty ${PLANS_DIR}/`);
97
+ }
98
+ } catch {
99
+ // Directory might have other contents
100
+ }
101
+ } else {
102
+ writeFileSync(plansJsonPath, JSON.stringify(manifest, null, 2) + '\n');
103
+ }
104
+
105
+ console.log(`\nCleaned ${cleaned} plan(s).`);
106
+ if (remaining > 0) {
107
+ console.log(`${remaining} plan(s) still in progress.`);
108
+ }
109
+ } else {
110
+ console.log(`\n${donePlans.length} plan(s) would be cleaned.`);
111
+ if (inFlightPlans.length > 0) {
112
+ console.log(`${inFlightPlans.length} plan(s) still in progress (will be kept).`);
113
+ }
114
+ }
115
+ }
116
+
117
+ main();
@@ -28,6 +28,12 @@ import {
28
28
  markCompletedSteps,
29
29
  type TodoItem,
30
30
  } from './utils.js';
31
+ import {
32
+ extractPlanTitle,
33
+ readPlansJson,
34
+ serializePlansJson,
35
+ type PlansManifest,
36
+ } from './plans-json.js';
31
37
 
32
38
  // ── Tool sets ────────────────────────────────────────────────────────────────
33
39
  // Plan phase: read-only + edit/write (for .plans/ files only, enforced by prompt)
@@ -99,6 +105,29 @@ export default function planMode(pi: ExtensionAPI): void {
99
105
  });
100
106
  }
101
107
 
108
+ // ── plans.json tracking ───────────────────────────────────────────────────
109
+ async function updatePlansManifest(
110
+ planName: string,
111
+ status: 'in-progress' | 'done',
112
+ title?: string,
113
+ ): Promise<void> {
114
+ const manifest = await readPlansJson((cmd, args) => pi.exec(cmd, args));
115
+ const existing = manifest[planName];
116
+ const now = new Date().toISOString();
117
+
118
+ manifest[planName] = {
119
+ status,
120
+ title: title ?? existing?.title ?? 'Untitled plan',
121
+ created: existing?.created ?? now,
122
+ completed: status === 'done' ? now : null,
123
+ };
124
+
125
+ await pi.exec('mkdir', ['-p', '.plans']);
126
+ const content = serializePlansJson(manifest);
127
+ // Write via a temp approach — use bash echo to avoid needing the write tool
128
+ await pi.exec('bash', ['-c', `cat > .plans/plans.json << 'PLANS_EOF'\n${content}PLANS_EOF`]);
129
+ }
130
+
102
131
  // ── UI updates ────────────────────────────────────────────────────────────
103
132
  function updateUI(ctx: ExtensionContext): void {
104
133
  const { theme } = ctx.ui;
@@ -384,7 +413,33 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
384
413
  const match = path.match(/\.plans\/([^/]+)\//);
385
414
  if (match && !planDir) {
386
415
  planDir = `.plans/${match[1]}`;
416
+ const planName = match[1];
417
+
418
+ // Read PLAN.md to extract the title for plans.json
419
+ let title = 'Untitled plan';
420
+ if (path.endsWith('PLAN.md')) {
421
+ try {
422
+ const result = await pi.exec('cat', [path]);
423
+ if (result.code === 0) {
424
+ title = extractPlanTitle(result.stdout);
425
+ }
426
+ } catch {
427
+ // Fall through
428
+ }
429
+ }
430
+ await updatePlansManifest(planName, 'in-progress', title);
387
431
  persist();
432
+ } else if (match && planDir && path.endsWith('PLAN.md')) {
433
+ // planDir already set but PLAN.md just written — update title
434
+ try {
435
+ const result = await pi.exec('cat', [path]);
436
+ if (result.code === 0) {
437
+ const title = extractPlanTitle(result.stdout);
438
+ await updatePlansManifest(match[1], 'in-progress', title);
439
+ }
440
+ } catch {
441
+ // Fall through
442
+ }
388
443
  }
389
444
  });
390
445
 
@@ -393,6 +448,12 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
393
448
  // Check execution completion
394
449
  if (executing && todos.length > 0) {
395
450
  if (todos.every((t) => t.completed)) {
451
+ // Mark plan as done in plans.json
452
+ if (planDir) {
453
+ const planName = planDir.replace(/^\.plans\//, '');
454
+ await updatePlansManifest(planName, 'done');
455
+ }
456
+
396
457
  const list = todos.map((t) => `~~${t.text}~~`).join('\n');
397
458
  pi.sendMessage(
398
459
  {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Reads and writes `.plans/plans.json` — the tracking manifest for plan lifecycle.
3
+ *
4
+ * Schema:
5
+ * ```json
6
+ * {
7
+ * "<plan-name>": {
8
+ * "status": "in-progress" | "done",
9
+ * "title": "Human-readable plan title",
10
+ * "created": "2026-05-08T12:00:00.000Z",
11
+ * "completed": "2026-05-08T13:00:00.000Z" | null
12
+ * }
13
+ * }
14
+ * ```
15
+ */
16
+
17
+ export interface PlanEntry {
18
+ status: 'in-progress' | 'done';
19
+ title: string;
20
+ created: string;
21
+ completed: string | null;
22
+ }
23
+
24
+ export type PlansManifest = Record<string, PlanEntry>;
25
+
26
+ const PLANS_JSON = '.plans/plans.json';
27
+
28
+ /** Read plans.json via pi.exec, returning current manifest (empty object if missing). */
29
+ export async function readPlansJson(exec: (cmd: string, args: string[]) => Promise<{ code: number; stdout: string }>): Promise<PlansManifest> {
30
+ try {
31
+ const result = await exec('cat', [PLANS_JSON]);
32
+ if (result.code === 0 && result.stdout.trim()) {
33
+ return JSON.parse(result.stdout) as PlansManifest;
34
+ }
35
+ } catch {
36
+ // File doesn't exist or isn't valid JSON
37
+ }
38
+ return {};
39
+ }
40
+
41
+ /** Serialize the manifest to a formatted JSON string. */
42
+ export function serializePlansJson(manifest: PlansManifest): string {
43
+ return `${JSON.stringify(manifest, null, 2)}\n`;
44
+ }
45
+
46
+ /** Extract the plan title from the first `# ...` heading in PLAN.md content. */
47
+ export function extractPlanTitle(planContent: string): string {
48
+ const match = planContent.match(/^#\s+(.+)$/m);
49
+ return match ? match[1].trim() : 'Untitled plan';
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-plan-mode",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Two-phase planning workflow for pi — plan with claude-opus-4-6:medium, execute with gpt-5.5:low, with .plans/ file-based handoff",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -13,17 +13,21 @@
13
13
  "directory": "packages/plan-mode"
14
14
  },
15
15
  "type": "module",
16
+ "bin": {
17
+ "pi-plan-mode": "./bin/clean-plans.js"
18
+ },
16
19
  "files": [
17
20
  "extensions",
21
+ "bin",
18
22
  "README.md",
19
23
  "CHANGELOG.md",
20
24
  "package.json"
21
25
  ],
22
26
  "scripts": {
23
27
  "typecheck": "tsc --noEmit",
24
- "lint": "oxlint extensions",
25
- "format": "oxfmt --write extensions",
26
- "format:check": "oxfmt --check extensions"
28
+ "lint": "oxlint extensions bin",
29
+ "format": "oxfmt --write extensions bin",
30
+ "format:check": "oxfmt --check extensions bin"
27
31
  },
28
32
  "pi": {
29
33
  "extensions": [
@@ -39,5 +43,13 @@
39
43
  "peerDependencies": {
40
44
  "@earendil-works/pi-coding-agent": "*",
41
45
  "@earendil-works/pi-tui": "*"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@earendil-works/pi-coding-agent": {
49
+ "optional": true
50
+ },
51
+ "@earendil-works/pi-tui": {
52
+ "optional": true
53
+ }
42
54
  }
43
55
  }