@dreki-gg/pi-plan-mode 0.15.1 → 0.17.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 +19 -0
- package/extensions/plan-mode/__tests__/add-task.test.ts +75 -0
- package/extensions/plan-mode/__tests__/atomic-write.test.ts +6 -4
- package/extensions/plan-mode/__tests__/html-render.test.ts +26 -35
- package/extensions/plan-mode/__tests__/package-skills.test.ts +51 -0
- package/extensions/plan-mode/__tests__/plan-storage.test.ts +57 -0
- package/extensions/plan-mode/__tests__/plans-manifest.test.ts +27 -16
- package/extensions/plan-mode/__tests__/schema.test.ts +212 -0
- package/extensions/plan-mode/__tests__/task-status.test.ts +74 -0
- package/extensions/plan-mode/__tests__/task-storage.test.ts +35 -12
- package/extensions/plan-mode/__tests__/utils.test.ts +19 -1
- package/extensions/plan-mode/constants.ts +2 -1
- package/extensions/plan-mode/effects/filesystem.ts +65 -0
- package/extensions/plan-mode/effects/runtime.ts +24 -0
- package/extensions/plan-mode/errors.ts +105 -0
- package/extensions/plan-mode/html/render.ts +10 -114
- package/extensions/plan-mode/html/templates/prototype.pug +25 -0
- package/extensions/plan-mode/index.ts +151 -48
- package/extensions/plan-mode/prompts.ts +5 -3
- package/extensions/plan-mode/resume.ts +19 -15
- package/extensions/plan-mode/schema.ts +57 -0
- package/extensions/plan-mode/storage/atomic-write.ts +19 -1
- package/extensions/plan-mode/storage/plan-storage.ts +57 -29
- package/extensions/plan-mode/storage/plans-manifest.ts +64 -57
- package/extensions/plan-mode/storage/task-storage.ts +83 -45
- package/extensions/plan-mode/task-status.ts +46 -0
- package/extensions/plan-mode/tools/add-task.ts +95 -0
- package/extensions/plan-mode/tools/preview-prototype.ts +98 -0
- package/extensions/plan-mode/tools/submit-plan.ts +18 -16
- package/extensions/plan-mode/types.ts +8 -38
- package/extensions/plan-mode/utils.ts +20 -0
- package/package.json +2 -1
- package/skills/planning-context/SKILL.md +42 -0
- package/skills/visual-prototype/SKILL.md +45 -0
- package/extensions/plan-mode/__tests__/types.test.ts +0 -70
- package/extensions/plan-mode/html/templates/plan.pug +0 -69
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Shared types for plan mode.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type TaskStatus = 'pending' | 'done' | 'skipped' | 'blocked';
|
|
5
|
+
export type TaskStatus = 'pending' | 'done' | 'skipped' | 'blocked' | 'deferred';
|
|
6
|
+
|
|
7
|
+
/** Where a task came from: the original submitted plan, or discovered during execution. */
|
|
8
|
+
export type TaskOrigin = 'plan' | 'discovered';
|
|
6
9
|
|
|
7
10
|
export interface TaskRecord {
|
|
8
11
|
_type: 'task';
|
|
@@ -10,6 +13,8 @@ export interface TaskRecord {
|
|
|
10
13
|
description: string;
|
|
11
14
|
details?: string;
|
|
12
15
|
status: TaskStatus;
|
|
16
|
+
/** Defaults to 'plan' when absent (back-compat with older tasks.jsonl files). */
|
|
17
|
+
origin?: TaskOrigin;
|
|
13
18
|
depends_on?: string[];
|
|
14
19
|
notes?: string;
|
|
15
20
|
created_at: string;
|
|
@@ -45,40 +50,5 @@ export interface PersistedState {
|
|
|
45
50
|
executionStartIdx: number | undefined;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
-
return typeof value === 'object' && value !== null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function isStringArray(value: unknown): value is string[] {
|
|
55
|
-
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function isTaskRecord(value: unknown): value is TaskRecord {
|
|
59
|
-
if (!isRecord(value)) return false;
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
value._type === 'task' &&
|
|
63
|
-
typeof value.id === 'string' &&
|
|
64
|
-
typeof value.description === 'string' &&
|
|
65
|
-
(value.details === undefined || typeof value.details === 'string') &&
|
|
66
|
-
typeof value.status === 'string' &&
|
|
67
|
-
TASK_STATUSES.has(value.status as TaskStatus) &&
|
|
68
|
-
(value.depends_on === undefined || isStringArray(value.depends_on)) &&
|
|
69
|
-
(value.notes === undefined || typeof value.notes === 'string') &&
|
|
70
|
-
typeof value.created_at === 'string' &&
|
|
71
|
-
typeof value.updated_at === 'string'
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function isTaskMeta(value: unknown): value is TaskMeta {
|
|
76
|
-
if (!isRecord(value)) return false;
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
value._type === 'meta' &&
|
|
80
|
-
typeof value.title === 'string' &&
|
|
81
|
-
typeof value.plan_name === 'string' &&
|
|
82
|
-
typeof value.created_at === 'string'
|
|
83
|
-
);
|
|
84
|
-
}
|
|
53
|
+
// Record validation lives in `schema.ts` (Effect Schema). The interfaces above
|
|
54
|
+
// remain the mutable shapes used by the imperative orchestration code.
|
|
@@ -42,3 +42,23 @@ export function toKebabCase(name: string): string {
|
|
|
42
42
|
.replace(/^-+|-+$/g, '')
|
|
43
43
|
.slice(0, 60);
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate the next sequential task id (`t-NNN`) given existing ids.
|
|
48
|
+
*
|
|
49
|
+
* Uses the max numeric suffix of `t-<digits>` ids + 1, zero-padded to 3.
|
|
50
|
+
* Falls back to `t-<count+1>` when no ids match the pattern.
|
|
51
|
+
*/
|
|
52
|
+
export function nextTaskId(existingIds: readonly string[]): string {
|
|
53
|
+
let max = 0;
|
|
54
|
+
let matched = false;
|
|
55
|
+
for (const id of existingIds) {
|
|
56
|
+
const m = /^t-(\d+)$/.exec(id);
|
|
57
|
+
if (!m) continue;
|
|
58
|
+
matched = true;
|
|
59
|
+
const n = Number.parseInt(m[1], 10);
|
|
60
|
+
if (n > max) max = n;
|
|
61
|
+
}
|
|
62
|
+
const next = matched ? max + 1 : existingIds.length + 1;
|
|
63
|
+
return `t-${String(next).padStart(3, '0')}`;
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreki-gg/pi-plan-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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"
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@dreki-gg/pi-command-sandbox": "^0.3.0",
|
|
44
|
+
"effect": "^3.21.2",
|
|
44
45
|
"pug": "^3.0.3"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planning-context
|
|
3
|
+
description: Maintain a living context.md during planning to capture intent, decisions, constraints, open questions, and discarded options before finalizing a plan. Use whenever you are in plan mode and converging on an approach, especially before calling submit_plan.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Planning Context
|
|
7
|
+
|
|
8
|
+
`context.md` is the living written record of a planning conversation. It exists to slow the jump from "read the codebase" to "submit the plan" — the moment where reasoning usually gets lost. Write it as you think, not as an afterthought.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Use this throughout any planning session, from the first real decision onward. If you have read code and formed an opinion but have not written anything down, that is the signal to update `context.md`.
|
|
13
|
+
|
|
14
|
+
## What it is
|
|
15
|
+
|
|
16
|
+
`.plans/<plan-name>/context.md` — a deliberation document, not a plan. It captures the *why* and the *roads not taken*, which `HANDOFF.md` and the task list deliberately omit.
|
|
17
|
+
|
|
18
|
+
## Process
|
|
19
|
+
|
|
20
|
+
### 1. Create it early
|
|
21
|
+
|
|
22
|
+
As soon as you understand the intent, write `.plans/<plan-name>/context.md` with the `write` tool. Do not wait until you are ready to submit.
|
|
23
|
+
|
|
24
|
+
### 2. Keep these sections current
|
|
25
|
+
|
|
26
|
+
- **Intent** — what the user actually wants, in their terms
|
|
27
|
+
- **Decisions** — choices made and the reasoning behind each
|
|
28
|
+
- **Constraints** — technical, product, or process limits that shape the work
|
|
29
|
+
- **Open questions** — anything unresolved; do not submit a plan with silent unknowns
|
|
30
|
+
- **Discarded options** — approaches considered and rejected, with why. This is the highest-value section and the one most often skipped.
|
|
31
|
+
|
|
32
|
+
### 3. Style
|
|
33
|
+
|
|
34
|
+
Caveman-lite: professional, tight, no filler. Bullet points over prose. Update in place as understanding shifts — do not append a changelog.
|
|
35
|
+
|
|
36
|
+
### 4. Use it before submitting
|
|
37
|
+
|
|
38
|
+
`context.md` is the input to `submit_plan`, not a duplicate of it. Before finalizing, re-read it: every open question resolved, every decision justified. The handoff is the distilled conclusion; `context.md` is the reasoning that earned it.
|
|
39
|
+
|
|
40
|
+
## Relationship to prototypes
|
|
41
|
+
|
|
42
|
+
For visual/UI work, a prototype is the visual sibling of `context.md` — see the `visual-prototype` skill. Both are deliberation artifacts. Written reasoning lives here; visual reasoning lives in the prototype.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: visual-prototype
|
|
3
|
+
description: Build a visual HTML prototype during planning for UI, component, layout, or style changes, so the user can react to the design before the plan is finalized. Use when a plan touches frontend appearance or interaction. Not for backend-only or non-visual work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Visual Prototype
|
|
7
|
+
|
|
8
|
+
A prototype is a **convergence artifact**, not a deliverable. When a plan changes how something looks or behaves visually, a static markdown plan cannot show the user what they are agreeing to. Build the prototype while the plan is still soft, show it, and let the user redirect you before anything is committed to `submit_plan`.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Use this when the plan involves any of:
|
|
13
|
+
|
|
14
|
+
- New or restyled UI components
|
|
15
|
+
- Layout, spacing, or visual hierarchy changes
|
|
16
|
+
- Color, typography, or theming changes
|
|
17
|
+
- Interaction states (hover, active, empty, loading, error)
|
|
18
|
+
|
|
19
|
+
Do **not** use it for backend-only, refactor-only, or otherwise non-visual work.
|
|
20
|
+
|
|
21
|
+
## Process
|
|
22
|
+
|
|
23
|
+
### 1. Decide there is something to see
|
|
24
|
+
|
|
25
|
+
If you cannot picture a screen or component changing, skip the prototype. A prototype for invisible work is noise.
|
|
26
|
+
|
|
27
|
+
### 2. Build it with `preview_prototype`
|
|
28
|
+
|
|
29
|
+
Call `preview_prototype` with:
|
|
30
|
+
|
|
31
|
+
- `title` — short name for the prototype
|
|
32
|
+
- `intent` — one line describing what it shows
|
|
33
|
+
- `pug` — self-contained Pug markup for the prototype body. Inline any styles the prototype needs; assume nothing about the host page.
|
|
34
|
+
|
|
35
|
+
The tool renders the Pug, writes `.plans/_prototypes/<slug>.html`, and opens it for review.
|
|
36
|
+
|
|
37
|
+
### 3. Get a reaction before submitting
|
|
38
|
+
|
|
39
|
+
Stop and ask the user what they think. Iterate on the prototype — call `preview_prototype` again with revisions — until the visual direction is agreed. Only then move toward `submit_plan`.
|
|
40
|
+
|
|
41
|
+
`submit_plan` never generates HTML. The prototype lives entirely in the planning phase; its job is done once the user has reacted.
|
|
42
|
+
|
|
43
|
+
## Relationship to context.md
|
|
44
|
+
|
|
45
|
+
The prototype is the visual sibling of `context.md`. Both are deliberation artifacts that exist to slow the jump from "read the codebase" to "submit the plan." Keep `context.md` current as the living written record of intent, decisions, and open questions; use a prototype whenever the decision is visual. Resist the urge to skip straight to `submit_plan` on visual work.
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { isTaskMeta, isTaskRecord } from '../types.js';
|
|
3
|
-
|
|
4
|
-
const now = '2026-05-27T12:00:00.000Z';
|
|
5
|
-
|
|
6
|
-
describe('task record type guards', () => {
|
|
7
|
-
test('accepts valid task records', () => {
|
|
8
|
-
expect(
|
|
9
|
-
isTaskRecord({
|
|
10
|
-
_type: 'task',
|
|
11
|
-
id: 't-001',
|
|
12
|
-
description: 'Do work',
|
|
13
|
-
details: 'Full instructions',
|
|
14
|
-
status: 'pending',
|
|
15
|
-
depends_on: ['t-000'],
|
|
16
|
-
notes: 'note',
|
|
17
|
-
created_at: now,
|
|
18
|
-
updated_at: now,
|
|
19
|
-
}),
|
|
20
|
-
).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('accepts task records without details (lightweight checklist)', () => {
|
|
24
|
-
expect(
|
|
25
|
-
isTaskRecord({
|
|
26
|
-
_type: 'task',
|
|
27
|
-
id: 't-001',
|
|
28
|
-
description: 'Do work',
|
|
29
|
-
status: 'pending',
|
|
30
|
-
created_at: now,
|
|
31
|
-
updated_at: now,
|
|
32
|
-
}),
|
|
33
|
-
).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('rejects malformed task records', () => {
|
|
37
|
-
expect(isTaskRecord({ _type: 'task', id: 't-001', status: 'pending' })).toBe(false);
|
|
38
|
-
expect(
|
|
39
|
-
isTaskRecord({
|
|
40
|
-
_type: 'task',
|
|
41
|
-
id: 't-001',
|
|
42
|
-
description: 'Do work',
|
|
43
|
-
details: 'Full instructions',
|
|
44
|
-
status: 'unknown',
|
|
45
|
-
created_at: now,
|
|
46
|
-
updated_at: now,
|
|
47
|
-
}),
|
|
48
|
-
).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('task meta type guard', () => {
|
|
53
|
-
test('accepts valid meta records', () => {
|
|
54
|
-
expect(
|
|
55
|
-
isTaskMeta({
|
|
56
|
-
_type: 'meta',
|
|
57
|
-
title: 'Refactor',
|
|
58
|
-
plan_name: 'refactor',
|
|
59
|
-
created_at: now,
|
|
60
|
-
}),
|
|
61
|
-
).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('rejects malformed meta records', () => {
|
|
65
|
-
expect(isTaskMeta({ _type: 'meta', title: 'Refactor' })).toBe(false);
|
|
66
|
-
expect(
|
|
67
|
-
isTaskMeta({ _type: 'task', title: 'Refactor', plan_name: 'refactor', created_at: now }),
|
|
68
|
-
).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
doctype html
|
|
2
|
-
html(lang="en")
|
|
3
|
-
head
|
|
4
|
-
meta(charset="utf-8")
|
|
5
|
-
meta(name="viewport" content="width=device-width, initial-scale=1")
|
|
6
|
-
title= plan.title
|
|
7
|
-
style.
|
|
8
|
-
:root { color-scheme: dark; --bg:#0b0d12; --panel:#11141b; --muted:#8b93a7; --text:#eef1f7; --line:#242936; --accent:#8b5cf6; --done:#33d69f; --blocked:#ff6b6b; }
|
|
9
|
-
* { box-sizing: border-box; }
|
|
10
|
-
body { margin:0; background:radial-gradient(circle at top left,#1c1730,transparent 34rem),var(--bg); color:var(--text); font:14px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
11
|
-
main { width:min(1040px, calc(100% - 48px)); margin:48px auto; }
|
|
12
|
-
header { display:flex; justify-content:space-between; gap:24px; align-items:flex-start; margin-bottom:28px; }
|
|
13
|
-
h1 { font-size:34px; line-height:1.08; margin:0 0 10px; letter-spacing:-0.04em; }
|
|
14
|
-
h2 { font-size:15px; text-transform:uppercase; color:var(--muted); letter-spacing:0.14em; margin:0 0 14px; }
|
|
15
|
-
.meta { color:var(--muted); display:flex; gap:12px; flex-wrap:wrap; }
|
|
16
|
-
.pill { border:1px solid var(--line); border-radius:999px; padding:6px 10px; background:rgba(255,255,255,0.03); }
|
|
17
|
-
section { background:rgba(17,20,27,0.82); border:1px solid var(--line); border-radius:18px; padding:22px; margin:18px 0; box-shadow:0 24px 80px rgba(0,0,0,0.24); }
|
|
18
|
-
.handoff :first-child { margin-top:0; }
|
|
19
|
-
.handoff :last-child { margin-bottom:0; }
|
|
20
|
-
.handoff pre { background:#161922; border:1px solid var(--line); border-radius:10px; padding:14px 16px; overflow-x:auto; margin:12px 0; }
|
|
21
|
-
.handoff pre code { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:13px; color:#c7ccda; background:none; padding:0; border:none; border-radius:0; }
|
|
22
|
-
.handoff code { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:13px; background:rgba(139,92,246,0.12); color:var(--accent); padding:2px 6px; border-radius:5px; }
|
|
23
|
-
.handoff strong { font-weight:650; }
|
|
24
|
-
.handoff a { color:var(--accent); text-decoration:none; }
|
|
25
|
-
.handoff a:hover { text-decoration:underline; }
|
|
26
|
-
.handoff h1, .handoff h3, .handoff h4 { font-size:15px; text-transform:uppercase; color:var(--muted); letter-spacing:0.14em; margin:20px 0 10px; }
|
|
27
|
-
.handoff p { margin:8px 0; }
|
|
28
|
-
.handoff ul { margin:8px 0; padding-left:20px; }
|
|
29
|
-
.handoff li { margin:4px 0; }
|
|
30
|
-
.tasks { display:grid; gap:12px; }
|
|
31
|
-
.task { border:1px solid var(--line); border-radius:14px; padding:16px; background:#0f1218; }
|
|
32
|
-
.task-top { display:flex; gap:10px; align-items:center; margin-bottom:8px; }
|
|
33
|
-
.id { color:var(--accent); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
34
|
-
.status { margin-left:auto; color:var(--muted); }
|
|
35
|
-
.task.done .status { color:var(--done); }
|
|
36
|
-
.task.blocked .status { color:var(--blocked); }
|
|
37
|
-
.description { font-weight:650; }
|
|
38
|
-
.details { color:#c7ccda; margin:0; white-space:pre-wrap; }
|
|
39
|
-
.deps { color:var(--muted); margin-top:10px; font-size:12px; }
|
|
40
|
-
body
|
|
41
|
-
main
|
|
42
|
-
header
|
|
43
|
-
div
|
|
44
|
-
h1= plan.title
|
|
45
|
-
.meta
|
|
46
|
-
span.pill= plan.planName
|
|
47
|
-
span.pill= taskCountLabel
|
|
48
|
-
.meta
|
|
49
|
-
span.pill Generated #{generatedAt}
|
|
50
|
-
section.handoff
|
|
51
|
-
h2 Handoff
|
|
52
|
-
!= handoffHtml
|
|
53
|
-
section
|
|
54
|
-
h2 Tasks
|
|
55
|
-
.tasks
|
|
56
|
-
each task in plan.tasks
|
|
57
|
-
article.task(class=task.status)
|
|
58
|
-
.task-top
|
|
59
|
-
span.id= task.id
|
|
60
|
-
span.description= task.description
|
|
61
|
-
span.status= task.status
|
|
62
|
-
if task.details
|
|
63
|
-
p.details= task.details
|
|
64
|
-
if task.depends_on && task.depends_on.length
|
|
65
|
-
.deps Depends on #{task.depends_on.join(', ')}
|
|
66
|
-
if prototypeHtml
|
|
67
|
-
section.prototype
|
|
68
|
-
h2 Prototype
|
|
69
|
-
!= prototypeHtml
|