@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 +18 -0
- package/README.md +86 -3
- package/bin/clean-plans.js +117 -0
- package/extensions/plan-mode/index.ts +61 -0
- package/extensions/plan-mode/plans-json.ts +50 -0
- package/package.json +16 -4
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
|
|
72
|
-
├── START-PROMPT.md
|
|
73
|
-
└── ...
|
|
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
|
+
"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
|
}
|