@dreki-gg/pi-plan-mode 0.15.1 → 0.16.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,16 @@
1
1
  # @dreki-gg/pi-plan-mode
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5c70b28: Reframe plan-mode HTML as a planning-phase visual aid instead of a finalization receipt.
8
+
9
+ - `submit_plan` no longer generates `plan.html`. It writes only `tasks.jsonl`, `HANDOFF.md`, and the manifest entry. The previous HTML duplicated the handoff and task list (already tracked elsewhere) and was never opened.
10
+ - Added a `preview_prototype` tool, available during planning. It renders self-contained Pug to a standalone HTML visual aid under `.plans/_prototypes/`, opens it, and notifies the path — so the user can react to a UI/component/style design _before_ the plan hardens.
11
+ - Added a bundled `visual-prototype` skill that routes UI/component/layout/style planning work to `preview_prototype` before `submit_plan`.
12
+ - Added a bundled `planning-context` skill that drives the living `context.md` deliberation discipline (intent, decisions, constraints, open questions, discarded options).
13
+
3
14
  ## 0.15.1
4
15
 
5
16
  ### Patch Changes
@@ -1,43 +1,34 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { renderPlanHtml } from '../html/render.js';
3
- import type { PlanData } from '../types.js';
4
-
5
- const plan: PlanData = {
6
- title: 'Refactor Auth',
7
- planName: 'refactor-auth',
8
- handoff: '# Handoff\nShip carefully.',
9
- tasks: [
10
- {
11
- _type: 'task',
12
- id: 't-001',
13
- description: 'Add auth middleware',
14
- details: 'Implement the middleware.',
15
- status: 'pending',
16
- created_at: '2026-05-27T12:00:00.000Z',
17
- updated_at: '2026-05-27T12:00:00.000Z',
18
- },
19
- ],
20
- };
21
-
22
- describe('renderPlanHtml', () => {
23
- test('renders title, task count, handoff, and tasks', () => {
24
- const html = renderPlanHtml(plan);
25
-
26
- expect(html).toContain('Refactor Auth');
27
- expect(html).toContain('1 task');
28
- expect(html).toContain('Ship carefully.');
29
- expect(html).toContain('t-001');
30
- expect(html).toContain('Add auth middleware');
2
+ import { renderPrototypeHtml } from '../html/render.js';
3
+
4
+ describe('renderPrototypeHtml', () => {
5
+ test('renders title and intent in a minimal header', () => {
6
+ const html = renderPrototypeHtml('Sidebar redesign', 'Left-aligned nav with icons', '.nav Nav');
7
+
8
+ expect(html).toContain('Sidebar redesign');
9
+ expect(html).toContain('Left-aligned nav with icons');
10
+ expect(html).toContain('Prototype');
11
+ });
12
+
13
+ test('renders the Pug body markup', () => {
14
+ const html = renderPrototypeHtml('Card', 'A product card', '.card Product card');
15
+
16
+ expect(html).toContain('class="card"');
17
+ expect(html).toContain('Product card');
31
18
  });
32
19
 
33
- test('omits prototypes section when no prototype is provided', () => {
34
- expect(renderPlanHtml(plan)).not.toContain('Prototype');
20
+ test('does not render tasks or handoff sections', () => {
21
+ const html = renderPrototypeHtml('Card', 'A product card', '.card Hello');
22
+
23
+ expect(html).not.toContain('Handoff');
24
+ expect(html).not.toContain('Tasks');
25
+ expect(html).not.toContain('Depends on');
35
26
  });
36
27
 
37
- test('renders optional prototype pug markup', () => {
38
- const html = renderPlanHtml(plan, '.mockup Prototype card');
28
+ test('handles an empty prototype body without throwing', () => {
29
+ const html = renderPrototypeHtml('Empty', 'Nothing yet', '');
39
30
 
40
- expect(html).toContain('Prototype');
41
- expect(html).toContain('Prototype card');
31
+ expect(html).toContain('Empty');
32
+ expect(html).toContain('Nothing yet');
42
33
  });
43
34
  });
@@ -45,3 +45,54 @@ describe('bundled technical-options skill', () => {
45
45
  expect(content).toMatch(/challenge.*framing|framing.*challenge/i);
46
46
  });
47
47
  });
48
+
49
+ describe('bundled visual-prototype skill', () => {
50
+ const skillDir = join(PKG_ROOT, 'skills', 'visual-prototype');
51
+ const skillFile = join(skillDir, 'SKILL.md');
52
+
53
+ test('SKILL.md exists', () => {
54
+ expect(existsSync(skillFile)).toBe(true);
55
+ });
56
+
57
+ test('has valid frontmatter with name and description', () => {
58
+ const content = readFileSync(skillFile, 'utf-8');
59
+ expect(content).toMatch(/^---\n/);
60
+ expect(content).toMatch(/name:\s*visual-prototype/);
61
+ expect(content).toMatch(/description:/);
62
+ });
63
+
64
+ test('routes on visual work, not backend-only', () => {
65
+ const content = readFileSync(skillFile, 'utf-8');
66
+ expect(content).toMatch(/description:.*\b(UI|component|layout|style)\b/i);
67
+ expect(content).toMatch(/not for.*backend|backend-only/i);
68
+ });
69
+
70
+ test('directs use of preview_prototype before submit_plan', () => {
71
+ const content = readFileSync(skillFile, 'utf-8');
72
+ expect(content).toContain('preview_prototype');
73
+ expect(content).toContain('submit_plan');
74
+ });
75
+ });
76
+
77
+ describe('bundled planning-context skill', () => {
78
+ const skillDir = join(PKG_ROOT, 'skills', 'planning-context');
79
+ const skillFile = join(skillDir, 'SKILL.md');
80
+
81
+ test('SKILL.md exists', () => {
82
+ expect(existsSync(skillFile)).toBe(true);
83
+ });
84
+
85
+ test('has valid frontmatter with name and description', () => {
86
+ const content = readFileSync(skillFile, 'utf-8');
87
+ expect(content).toMatch(/^---\n/);
88
+ expect(content).toMatch(/name:\s*planning-context/);
89
+ expect(content).toMatch(/description:/);
90
+ });
91
+
92
+ test('covers context.md deliberation sections', () => {
93
+ const content = readFileSync(skillFile, 'utf-8');
94
+ expect(content).toContain('context.md');
95
+ expect(content).toMatch(/discarded options/i);
96
+ expect(content).toMatch(/open questions/i);
97
+ });
98
+ });
@@ -10,6 +10,7 @@ export const PLAN_TOOLS = [
10
10
  'find',
11
11
  'ls',
12
12
  'submit_plan',
13
+ 'preview_prototype',
13
14
  'write',
14
15
  'questionnaire',
15
16
  'search_skills',
@@ -2,127 +2,23 @@ import { readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import pug from 'pug';
5
- import type { PlanData } from '../types.js';
6
5
 
7
- const templatePath = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'plan.pug');
6
+ const templatePath = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'prototype.pug');
8
7
 
9
- export function renderPlanHtml(plan: PlanData, prototype?: string): string {
8
+ /**
9
+ * Renders a standalone prototype preview: a minimal header (title + one-line
10
+ * intent) wrapping the rendered Pug body. This is a planning-phase visual aid,
11
+ * not a plan dump — no tasks, no handoff.
12
+ */
13
+ export function renderPrototypeHtml(title: string, intent: string, pugBody: string): string {
10
14
  const template = readFileSync(templatePath, 'utf8');
11
- const prototypeHtml = prototype?.trim() ? pug.render(prototype) : undefined;
15
+ const prototypeHtml = pugBody.trim() ? pug.render(pugBody) : '';
12
16
 
13
17
  return pug.render(template, {
14
18
  filename: templatePath,
15
- plan,
19
+ title,
20
+ intent,
16
21
  prototypeHtml,
17
- handoffHtml: markdownToHtml(plan.handoff),
18
- taskCountLabel: `${plan.tasks.length} ${plan.tasks.length === 1 ? 'task' : 'tasks'}`,
19
22
  generatedAt: new Date().toISOString(),
20
23
  });
21
24
  }
22
-
23
- function markdownToHtml(markdown: string): string {
24
- const lines = markdown.split(/\r?\n/);
25
- const html: string[] = [];
26
- let inList = false;
27
- let inCode = false;
28
- let codeLang = '';
29
- const codeLines: string[] = [];
30
-
31
- for (const line of lines) {
32
- // Fenced code blocks
33
- if (line.startsWith('```')) {
34
- if (!inCode) {
35
- if (inList) {
36
- html.push('</ul>');
37
- inList = false;
38
- }
39
- inCode = true;
40
- codeLang = line.slice(3).trim();
41
- codeLines.length = 0;
42
- } else {
43
- const langAttr = codeLang ? ` class="language-${escapeHtml(codeLang)}"` : '';
44
- html.push(`<pre><code${langAttr}>${codeLines.map(escapeHtml).join('\n')}</code></pre>`);
45
- inCode = false;
46
- codeLang = '';
47
- }
48
- continue;
49
- }
50
-
51
- if (inCode) {
52
- codeLines.push(line);
53
- continue;
54
- }
55
-
56
- // Headings
57
- const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
58
- if (headingMatch) {
59
- if (inList) {
60
- html.push('</ul>');
61
- inList = false;
62
- }
63
- const level = headingMatch[1].length;
64
- html.push(`<h${level}>${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);
65
- continue;
66
- }
67
-
68
- // List items (- or *)
69
- const listMatch = line.match(/^\s*[-*]\s+(.*)$/);
70
- if (listMatch) {
71
- if (!inList) {
72
- html.push('<ul>');
73
- inList = true;
74
- }
75
- html.push(`<li>${inlineMarkdown(escapeHtml(listMatch[1]))}</li>`);
76
- continue;
77
- }
78
-
79
- // Blank line
80
- if (!line.trim()) {
81
- if (inList) {
82
- html.push('</ul>');
83
- inList = false;
84
- }
85
- continue;
86
- }
87
-
88
- // Paragraph
89
- if (inList) {
90
- html.push('</ul>');
91
- inList = false;
92
- }
93
- html.push(`<p>${inlineMarkdown(escapeHtml(line))}</p>`);
94
- }
95
-
96
- // Close unclosed blocks
97
- if (inCode) {
98
- html.push(`<pre><code>${codeLines.map(escapeHtml).join('\n')}</code></pre>`);
99
- }
100
- if (inList) html.push('</ul>');
101
- return html.join('\n');
102
- }
103
-
104
- /** Converts inline markdown (bold, italic, inline code, links) in already-escaped HTML. */
105
- function inlineMarkdown(text: string): string {
106
- return (
107
- text
108
- // Inline code: `code`
109
- .replace(/`([^`]+)`/g, '<code>$1</code>')
110
- // Bold: **text** or __text__
111
- .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
112
- .replace(/__(.+?)__/g, '<strong>$1</strong>')
113
- // Italic: *text* or _text_
114
- .replace(/\*(.+?)\*/g, '<em>$1</em>')
115
- .replace(/_(.+?)_/g, '<em>$1</em>')
116
- // Links: [text](url)
117
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
118
- );
119
- }
120
-
121
- function escapeHtml(value: string): string {
122
- return value
123
- .replaceAll('&', '&amp;')
124
- .replaceAll('<', '&lt;')
125
- .replaceAll('>', '&gt;')
126
- .replaceAll('"', '&quot;')
127
- .replaceAll("'", '&#39;');
128
- }
@@ -0,0 +1,25 @@
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= title
7
+ style.
8
+ :root { color-scheme: dark; --bg:#0b0d12; --panel:#11141b; --muted:#8b93a7; --text:#eef1f7; --line:#242936; --accent:#8b5cf6; }
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 { margin-bottom:24px; }
13
+ h1 { font-size:30px; line-height:1.1; margin:0 0 8px; letter-spacing:-0.04em; }
14
+ .intent { color:var(--muted); margin:0; }
15
+ .badge { display:inline-block; border:1px solid var(--line); border-radius:999px; padding:5px 10px; background:rgba(255,255,255,0.03); color:var(--muted); font-size:12px; margin-bottom:14px; }
16
+ .prototype { background:rgba(17,20,27,0.82); border:1px solid var(--line); border-radius:18px; padding:22px; box-shadow:0 24px 80px rgba(0,0,0,0.24); }
17
+ body
18
+ main
19
+ header
20
+ span.badge Prototype · #{generatedAt}
21
+ h1= title
22
+ if intent
23
+ p.intent= intent
24
+ section.prototype
25
+ != prototypeHtml
@@ -37,6 +37,7 @@ import { filterExecutionMessages, filterStalePlanMessages } from './context-filt
37
37
  import { enterPlanMode, exitPlanMode, switchModel } from './phase-transitions.js';
38
38
  import { resumePlan, executeInNewSession } from './resume.js';
39
39
  import { registerSubmitPlanTool } from './tools/submit-plan.js';
40
+ import { registerPreviewPrototypeTool } from './tools/preview-prototype.js';
40
41
  import { registerUpdateTaskTool } from './tools/update-task.js';
41
42
  import { isSafeCommand, isPlanPath } from './utils.js';
42
43
 
@@ -59,6 +60,8 @@ export default function planMode(pi: ExtensionAPI): void {
59
60
  },
60
61
  });
61
62
 
63
+ registerPreviewPrototypeTool(pi);
64
+
62
65
  registerUpdateTaskTool(pi, {
63
66
  getPlan: () => state.plan,
64
67
  onTaskUpdated: async (taskId, status, notes) => {
@@ -335,8 +338,8 @@ export default function planMode(pi: ExtensionAPI): void {
335
338
  return;
336
339
  }
337
340
 
338
- // Plan submitted — user can review plan.html then /plan-exec or type naturally.
339
- // No menu needed: plan.jsonl + plan.html are the source of truth.
341
+ // Plan submitted — user can /plan-exec or type naturally.
342
+ // No menu needed: plan.jsonl + HANDOFF.md are the source of truth.
340
343
  });
341
344
 
342
345
  // ── Event: session restore ────────────────────────────────────────────────
@@ -18,7 +18,7 @@ Restrictions:
18
18
  Your job is to reach shared understanding before formalizing a plan:
19
19
  1. Understand the user's intent through dialogue. Push back on weak assumptions, name trade-offs, and ask clarifying questions when needed.
20
20
  2. Investigate the codebase with read-only tools. Use questionnaire when explicit choices are needed.
21
- 3. Maintain .plans/<plan-name>/context.md with the write tool as the living planning context in caveman-lite style: professional, tight, no filler. Capture intent, decisions, constraints, open questions, and discarded options.
21
+ 3. Maintain a living .plans/<plan-name>/context.md as you converge the planning-context skill covers what to capture and how.
22
22
  4. Only call submit_plan after the user and agent have converged on the approach.
23
23
 
24
24
  When you are ready to finalize the plan, call submit_plan with:
@@ -26,13 +26,14 @@ When you are ready to finalize the plan, call submit_plan with:
26
26
  - title: a human-readable plan title
27
27
  - handoff: a markdown document that explains what is changing, why it matters, approach, decisions, file paths, APIs, patterns, constraints, and gotchas
28
28
  - tasks: an array of tasks with id (e.g. "t-001"), description (≤60 chars), optional details, and optional depends_on task IDs
29
- - prototype: optional Pug markup for a creative prototype section in the generated plan.html
30
29
 
31
30
  Plan weight:
32
31
  - **Delegation plans** (different agent/human executes): include full details in each task so an executor with zero context can follow them.
33
32
  - **Self-execution plans** (you plan and execute in the same session): use lightweight checklist-style tasks — just id + description, skip details. The handoff doc carries the real context.
34
33
 
35
- submit_plan is finalization, not the starting point. The generated plan.html is written but not opened automatically.
34
+ submit_plan is finalization, not the starting point. It records tasks and the handoff — it does not generate HTML.
35
+
36
+ For visual/UI/layout/style work, build a prototype with preview_prototype DURING planning, before submit_plan, so the user can react to the visual before the plan hardens. The visual-prototype skill covers when and how.
36
37
 
37
38
  When facing a significant technical decision with multiple viable approaches (architecture, API design, implementation strategy), use the technical-options skill: you generate the competing proposals yourself, then use the subagent tool to fan out voting agents for evaluation. Do not delegate the entire workflow to a subagent — you are the planner, you drive the process.`;
38
39
  }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * preview_prototype tool — available during the plan phase.
3
+ *
4
+ * Renders a Pug prototype to a standalone HTML visual aid, writes it under
5
+ * .plans/_prototypes/, and best-effort opens it so the user can react to the
6
+ * visual BEFORE the plan is finalized.
7
+ */
8
+
9
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
10
+ import { Text } from '@earendil-works/pi-tui';
11
+ import { Type } from 'typebox';
12
+ import { mkdir, writeFile } from 'node:fs/promises';
13
+ import { spawn } from 'node:child_process';
14
+ import { join } from 'node:path';
15
+ import { renderPrototypeHtml } from '../html/render.js';
16
+ import { toKebabCase } from '../utils.js';
17
+
18
+ const PREVIEW_DIR = '.plans/_prototypes';
19
+
20
+ /** Best-effort open of a file in the OS default app. Never throws. */
21
+ function openInBrowser(filePath: string): void {
22
+ const command =
23
+ process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
24
+ try {
25
+ const child = spawn(command, [filePath], {
26
+ detached: true,
27
+ stdio: 'ignore',
28
+ shell: process.platform === 'win32',
29
+ });
30
+ child.on('error', () => {});
31
+ child.unref();
32
+ } catch {
33
+ // Opening is a convenience — ignore failures (headless, sandbox, etc.).
34
+ }
35
+ }
36
+
37
+ export function registerPreviewPrototypeTool(pi: ExtensionAPI): void {
38
+ pi.registerTool({
39
+ name: 'preview_prototype',
40
+ label: 'Preview Prototype',
41
+ description:
42
+ 'Render a Pug prototype to a standalone HTML visual aid and open it for review during planning.',
43
+ promptSnippet: 'Render a Pug UI prototype to HTML and open it for the user to review',
44
+ promptGuidelines: [
45
+ 'Use preview_prototype during planning for visual/UI/layout/style work, before submit_plan.',
46
+ 'The prototype is a convergence aid — show it so the user can react before the plan hardens.',
47
+ 'Keep the Pug self-contained; inline any styles the prototype needs.',
48
+ ],
49
+ parameters: Type.Object({
50
+ title: Type.String({ description: 'Short title for the prototype' }),
51
+ intent: Type.String({
52
+ description: 'One-line description of what this prototype is showing',
53
+ }),
54
+ pug: Type.String({ description: 'Pug markup for the prototype body' }),
55
+ }),
56
+
57
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
58
+ const slug = toKebabCase(params.title) || 'prototype';
59
+ const filePath = join(PREVIEW_DIR, `${slug}.html`);
60
+ const html = renderPrototypeHtml(params.title, params.intent, params.pug);
61
+
62
+ await mkdir(PREVIEW_DIR, { recursive: true });
63
+ await writeFile(filePath, html, 'utf-8');
64
+ openInBrowser(filePath);
65
+ ctx?.ui?.notify(`Prototype written to ${filePath} — opening for review.`, 'info');
66
+
67
+ return {
68
+ content: [
69
+ {
70
+ type: 'text' as const,
71
+ text: `Prototype "${params.title}" rendered to ${filePath} and opened. Ask the user for feedback before submitting the plan.`,
72
+ },
73
+ ],
74
+ details: { filePath, title: params.title },
75
+ };
76
+ },
77
+
78
+ renderCall(args, theme) {
79
+ const title = (args as { title?: string }).title ?? 'prototype';
80
+ let content = theme.fg('toolTitle', theme.bold('preview_prototype '));
81
+ content += theme.fg('accent', title);
82
+ return new Text(content, 0, 0);
83
+ },
84
+
85
+ renderResult(result, _options, theme) {
86
+ const filePath = (result.details as { filePath?: string } | undefined)?.filePath;
87
+ const label = filePath ? `✓ Prototype → ${filePath}` : '✓ Prototype rendered';
88
+ return new Text(theme.fg('success', label), 0, 0);
89
+ },
90
+ });
91
+ }
@@ -5,8 +5,7 @@
5
5
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
6
6
  import { Text } from '@earendil-works/pi-tui';
7
7
  import { Type } from 'typebox';
8
- import { mkdir, writeFile } from 'node:fs/promises';
9
- import { renderPlanHtml } from '../html/render.js';
8
+ import { mkdir } from 'node:fs/promises';
10
9
  import { saveHandoff } from '../storage/plan-storage.js';
11
10
  import { writeTasksJsonl } from '../storage/task-storage.js';
12
11
  import { upsertPlanEntry } from '../storage/plans-manifest.js';
@@ -22,15 +21,16 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
22
21
  name: 'submit_plan',
23
22
  label: 'Submit Plan',
24
23
  description:
25
- 'Finalize a conversational plan with task IDs, JSONL storage, HANDOFF.md, and generated plan.html.',
24
+ 'Finalize a conversational plan with task IDs, JSONL storage, and HANDOFF.md.',
26
25
  promptSnippet:
27
- 'Finalize the plan with title, handoff, tasks, dependencies, and optional prototype Pug',
26
+ 'Finalize the plan with title, handoff, tasks, and dependencies',
28
27
  promptGuidelines: [
29
28
  'Only call submit_plan after shared understanding has been reached with the user.',
30
29
  'Each task needs an id like t-001, a short description, and optional depends_on task IDs.',
31
30
  "When a different agent or human will execute the plan, include detailed implementation instructions in each task's details field.",
32
31
  'When you are planning and executing yourself (same session), use lightweight checklist-style tasks: just id + description, omit details. Put the real context in the handoff document instead.',
33
32
  'The handoff must be thorough enough that both a human reviewer and executor agent with zero prior context can understand the plan.',
33
+ 'For visual/UI work, preview a prototype with preview_prototype during planning — before submit_plan, not as part of it.',
34
34
  ],
35
35
  parameters: Type.Object({
36
36
  name: Type.String({
@@ -54,9 +54,6 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
54
54
  }),
55
55
  { minItems: 1 },
56
56
  ),
57
- prototype: Type.Optional(
58
- Type.String({ description: 'Optional Pug markup for the prototype section in plan.html' }),
59
- ),
60
57
  }),
61
58
 
62
59
  async execute(_toolCallId, params) {
@@ -84,7 +81,6 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
84
81
  await mkdir(planDir, { recursive: true });
85
82
  await writeTasksJsonl(planDir, meta, tasks);
86
83
  await saveHandoff(planDir, params.handoff);
87
- await writeFile(`${planDir}/plan.html`, renderPlanHtml(plan, params.prototype), 'utf-8');
88
84
  await upsertPlanEntry(planName, { status: 'in-progress', title: params.title });
89
85
 
90
86
  callbacks.onPlanSubmitted(planDir, plan);
@@ -93,7 +89,7 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
93
89
  content: [
94
90
  {
95
91
  type: 'text' as const,
96
- text: `Plan "${params.title}" saved with ${tasks.length} tasks. Review ${planDir}/plan.html, then execute when ready.`,
92
+ text: `Plan "${params.title}" saved with ${tasks.length} tasks in ${planDir}. Execute when ready.`,
97
93
  },
98
94
  ],
99
95
  details: { planDir, plan },
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.16.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"
@@ -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,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