@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/extensions/plan-mode/__tests__/add-task.test.ts +75 -0
  3. package/extensions/plan-mode/__tests__/atomic-write.test.ts +6 -4
  4. package/extensions/plan-mode/__tests__/html-render.test.ts +26 -35
  5. package/extensions/plan-mode/__tests__/package-skills.test.ts +51 -0
  6. package/extensions/plan-mode/__tests__/plan-storage.test.ts +57 -0
  7. package/extensions/plan-mode/__tests__/plans-manifest.test.ts +27 -16
  8. package/extensions/plan-mode/__tests__/schema.test.ts +212 -0
  9. package/extensions/plan-mode/__tests__/task-status.test.ts +74 -0
  10. package/extensions/plan-mode/__tests__/task-storage.test.ts +35 -12
  11. package/extensions/plan-mode/__tests__/utils.test.ts +19 -1
  12. package/extensions/plan-mode/constants.ts +2 -1
  13. package/extensions/plan-mode/effects/filesystem.ts +65 -0
  14. package/extensions/plan-mode/effects/runtime.ts +24 -0
  15. package/extensions/plan-mode/errors.ts +105 -0
  16. package/extensions/plan-mode/html/render.ts +10 -114
  17. package/extensions/plan-mode/html/templates/prototype.pug +25 -0
  18. package/extensions/plan-mode/index.ts +151 -48
  19. package/extensions/plan-mode/prompts.ts +5 -3
  20. package/extensions/plan-mode/resume.ts +19 -15
  21. package/extensions/plan-mode/schema.ts +57 -0
  22. package/extensions/plan-mode/storage/atomic-write.ts +19 -1
  23. package/extensions/plan-mode/storage/plan-storage.ts +57 -29
  24. package/extensions/plan-mode/storage/plans-manifest.ts +64 -57
  25. package/extensions/plan-mode/storage/task-storage.ts +83 -45
  26. package/extensions/plan-mode/task-status.ts +46 -0
  27. package/extensions/plan-mode/tools/add-task.ts +95 -0
  28. package/extensions/plan-mode/tools/preview-prototype.ts +98 -0
  29. package/extensions/plan-mode/tools/submit-plan.ts +18 -16
  30. package/extensions/plan-mode/types.ts +8 -38
  31. package/extensions/plan-mode/utils.ts +20 -0
  32. package/package.json +2 -1
  33. package/skills/planning-context/SKILL.md +42 -0
  34. package/skills/visual-prototype/SKILL.md +45 -0
  35. package/extensions/plan-mode/__tests__/types.test.ts +0 -70
  36. 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
- const TASK_STATUSES = new Set<TaskStatus>(['pending', 'done', 'skipped', 'blocked']);
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.15.1",
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