@dreki-gg/pi-plan-mode 0.13.0 → 0.14.2

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 (29) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/extensions/plan-mode/__tests__/atomic-write.test.ts +43 -0
  3. package/extensions/plan-mode/__tests__/html-render.test.ts +43 -0
  4. package/extensions/plan-mode/__tests__/phase-transitions.test.ts +42 -0
  5. package/extensions/plan-mode/__tests__/plans-manifest.test.ts +37 -0
  6. package/extensions/plan-mode/__tests__/prompts.test.ts +39 -1
  7. package/extensions/plan-mode/__tests__/task-storage.test.ts +52 -0
  8. package/extensions/plan-mode/__tests__/types.test.ts +68 -0
  9. package/extensions/plan-mode/constants.ts +2 -1
  10. package/extensions/plan-mode/html/render.ts +67 -0
  11. package/extensions/plan-mode/html/templates/plan.pug +59 -0
  12. package/extensions/plan-mode/index.ts +60 -72
  13. package/extensions/plan-mode/phase-transitions.ts +1 -1
  14. package/extensions/plan-mode/plan-storage.ts +1 -80
  15. package/extensions/plan-mode/prompts.ts +37 -32
  16. package/extensions/plan-mode/pug.d.ts +5 -0
  17. package/extensions/plan-mode/resume.ts +38 -37
  18. package/extensions/plan-mode/state.ts +7 -0
  19. package/extensions/plan-mode/storage/atomic-write.ts +53 -0
  20. package/extensions/plan-mode/storage/plan-storage.ts +47 -0
  21. package/extensions/plan-mode/storage/plans-manifest.ts +57 -0
  22. package/extensions/plan-mode/storage/task-storage.ts +44 -0
  23. package/extensions/plan-mode/tools/submit-plan.ts +41 -81
  24. package/extensions/plan-mode/tools/update-task.ts +82 -0
  25. package/extensions/plan-mode/types.ts +57 -4
  26. package/extensions/plan-mode/ui.ts +18 -11
  27. package/package.json +3 -2
  28. package/extensions/plan-mode/plans-json.ts +0 -48
  29. package/extensions/plan-mode/tools/update-step.ts +0 -146
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @dreki-gg/pi-plan-mode
2
2
 
3
+ ## 0.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Remove post-plan submission menu and auto-hide task widget when all tasks are resolved.
8
+
9
+ ## 0.14.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix update_task failing after exiting plan mode; make task details optional for lightweight checklist-style plans.
14
+
15
+ - exitPlanMode now preserves plan data so update_task works outside execution mode
16
+ - submit_plan accepts tasks without details for self-execution workflows
17
+ - Plan widget shows in tracking mode after exiting plan mode
18
+ - Prompt guidance distinguishes delegation vs self-execution plan weights
19
+
20
+ ## 0.14.0
21
+
22
+ ### Minor Changes
23
+
24
+ - Refactor plan-mode to conversational planning with JSONL task storage and HTML output. Replace steps with task records, add atomic writes, Pug-based plan.html generation, and migrate manifest to JSONL. Update subagent prompts.
25
+
3
26
  ## 0.13.0
4
27
 
5
28
  ### Minor Changes
@@ -0,0 +1,43 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { writeFileAtomic } from '../storage/atomic-write.js';
6
+
7
+ let dir: string;
8
+
9
+ beforeEach(async () => {
10
+ dir = await mkdtemp(join(tmpdir(), 'plan-mode-atomic-'));
11
+ });
12
+
13
+ afterEach(async () => {
14
+ await rm(dir, { recursive: true, force: true });
15
+ });
16
+
17
+ describe('writeFileAtomic', () => {
18
+ test('writes complete content to the target path', async () => {
19
+ const target = join(dir, 'data.txt');
20
+
21
+ await writeFileAtomic(target, 'hello world');
22
+
23
+ expect(await readFile(target, 'utf8')).toBe('hello world');
24
+ });
25
+
26
+ test('replaces existing content atomically from the caller perspective', async () => {
27
+ const target = join(dir, 'data.txt');
28
+ await writeFile(target, 'old');
29
+
30
+ await writeFileAtomic(target, 'new');
31
+
32
+ expect(await readFile(target, 'utf8')).toBe('new');
33
+ });
34
+
35
+ test('leaves target untouched when writing fails before rename', async () => {
36
+ const target = join(dir, 'data.txt');
37
+ await writeFile(target, 'original');
38
+
39
+ await expect(writeFileAtomic(target, 'next', { mode: 0o400 })).rejects.toThrow();
40
+
41
+ expect(await readFile(target, 'utf8')).toBe('original');
42
+ });
43
+ });
@@ -0,0 +1,43 @@
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');
31
+ });
32
+
33
+ test('omits prototypes section when no prototype is provided', () => {
34
+ expect(renderPlanHtml(plan)).not.toContain('Prototype');
35
+ });
36
+
37
+ test('renders optional prototype pug markup', () => {
38
+ const html = renderPlanHtml(plan, '.mockup Prototype card');
39
+
40
+ expect(html).toContain('Prototype');
41
+ expect(html).toContain('Prototype card');
42
+ });
43
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { PlanModeState } from '../state.js';
3
+ import type { PlanData, TaskRecord } from '../types.js';
4
+
5
+ function makePlan(overrides?: Partial<PlanData>): PlanData {
6
+ const task: TaskRecord = {
7
+ _type: 'task', id: 't-001', description: 'Do work', details: 'Details',
8
+ status: 'pending', created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z',
9
+ };
10
+ return { title: 'Test Plan', planName: 'test-plan', handoff: '# Handoff', tasks: [task], ...overrides };
11
+ }
12
+
13
+ describe('PlanModeState', () => {
14
+ describe('exitPreservingPlan', () => {
15
+ test('clears mode flags but keeps plan data when a plan was submitted', () => {
16
+ const state = new PlanModeState();
17
+ state.planEnabled = true;
18
+ state.planDir = '.plans/test-plan';
19
+ state.plan = makePlan();
20
+
21
+ state.exitPreservingPlan();
22
+
23
+ expect(state.planEnabled).toBe(false);
24
+ expect(state.executing).toBe(false);
25
+ expect(state.plan).toBeDefined();
26
+ expect(state.planDir).toBe('.plans/test-plan');
27
+ });
28
+
29
+ test('fully resets when no plan was submitted', () => {
30
+ const state = new PlanModeState();
31
+ state.planEnabled = true;
32
+ state.planDir = undefined;
33
+ state.plan = undefined;
34
+
35
+ state.exitPreservingPlan();
36
+
37
+ expect(state.planEnabled).toBe(false);
38
+ expect(state.plan).toBeUndefined();
39
+ expect(state.planDir).toBeUndefined();
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,37 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { chdir } from 'node:process';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { readPlansManifest, upsertPlanEntry, writePlansManifest } from '../storage/plans-manifest.js';
7
+
8
+ const originalCwd = process.cwd();
9
+ let dir: string;
10
+
11
+ beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'plan-mode-manifest-')); chdir(dir); });
12
+ afterEach(async () => { chdir(originalCwd); await rm(dir, { recursive: true, force: true }); });
13
+
14
+ describe('plans.jsonl manifest', () => {
15
+ test('round trips entries', async () => {
16
+ const entry = { _type: 'plan' as const, name: 'refactor', status: 'in-progress' as const, title: 'Refactor', created_at: 'now', completed_at: null };
17
+ await writePlansManifest([entry]);
18
+ await expect(readPlansManifest()).resolves.toEqual([entry]);
19
+ });
20
+
21
+ test('upserts new entries', async () => {
22
+ await upsertPlanEntry('new-plan', { status: 'in-progress', title: 'New Plan' });
23
+ const entries = await readPlansManifest();
24
+ expect(entries).toHaveLength(1);
25
+ expect(entries[0]?.name).toBe('new-plan');
26
+ expect(entries[0]?.title).toBe('New Plan');
27
+ });
28
+
29
+ test('upserts existing entries without changing created_at', async () => {
30
+ await writePlansManifest([{ _type: 'plan', name: 'p', status: 'in-progress', title: 'Old', created_at: 'created', completed_at: null }]);
31
+ await upsertPlanEntry('p', { status: 'done', title: 'New' });
32
+ const [entry] = await readPlansManifest();
33
+ expect(entry.created_at).toBe('created');
34
+ expect(entry.status).toBe('done');
35
+ expect(entry.completed_at).toBeString();
36
+ });
37
+ });
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { buildPlanModePrompt } from '../prompts.js';
2
+ import { buildPlanModePrompt, buildExecutionPrompt } from '../prompts.js';
3
3
  import { PLAN_TOOLS } from '../constants.js';
4
+ import type { PlanData, TaskRecord } from '../types.js';
5
+
6
+ const now = '2026-01-01T00:00:00Z';
7
+ function makeTask(overrides?: Partial<TaskRecord>): TaskRecord {
8
+ return { _type: 'task', id: 't-001', description: 'Do work', status: 'pending', created_at: now, updated_at: now, ...overrides };
9
+ }
4
10
 
5
11
  describe('buildPlanModePrompt', () => {
6
12
  const prompt = buildPlanModePrompt();
@@ -27,6 +33,38 @@ describe('buildPlanModePrompt', () => {
27
33
  });
28
34
  });
29
35
 
36
+ describe('buildPlanModePrompt lightweight plan guidance', () => {
37
+ const prompt = buildPlanModePrompt();
38
+
39
+ test('mentions self-execution lightweight mode', () => {
40
+ expect(prompt).toMatch(/lightweight|checklist/i);
41
+ });
42
+
43
+ test('mentions delegation plans with full details', () => {
44
+ expect(prompt).toMatch(/delegation|different agent/i);
45
+ });
46
+ });
47
+
48
+ describe('buildExecutionPrompt', () => {
49
+ test('omits Details line when task has no details', () => {
50
+ const plan: PlanData = { title: 'Test', planName: 'test', handoff: '# H', tasks: [makeTask()] };
51
+ const prompt = buildExecutionPrompt(plan)!;
52
+ expect(prompt).not.toContain('Details:');
53
+ expect(prompt).toContain('t-001: Do work');
54
+ });
55
+
56
+ test('includes Details line when task has details', () => {
57
+ const plan: PlanData = { title: 'Test', planName: 'test', handoff: '# H', tasks: [makeTask({ details: 'Full instructions here' })] };
58
+ const prompt = buildExecutionPrompt(plan)!;
59
+ expect(prompt).toContain('Details: Full instructions here');
60
+ });
61
+
62
+ test('returns undefined when no pending tasks', () => {
63
+ const plan: PlanData = { title: 'Test', planName: 'test', handoff: '# H', tasks: [makeTask({ status: 'done' })] };
64
+ expect(buildExecutionPrompt(plan)).toBeUndefined();
65
+ });
66
+ });
67
+
30
68
  describe('PLAN_TOOLS', () => {
31
69
  test('includes subagent for voting workflows', () => {
32
70
  expect(PLAN_TOOLS).toContain('subagent');
@@ -0,0 +1,52 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { readTasksJsonl, updateTask, writeTasksJsonl } from '../storage/task-storage.js';
6
+ import type { TaskMeta, TaskRecord } from '../types.js';
7
+
8
+ let dir: string;
9
+ const now = '2026-05-27T12:00:00.000Z';
10
+ const meta: TaskMeta = { _type: 'meta', title: 'Plan', plan_name: 'plan', created_at: now };
11
+ const task: TaskRecord = { _type: 'task', id: 't-001', description: 'Do work', details: 'Details', status: 'pending', created_at: now, updated_at: now };
12
+
13
+ beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'plan-mode-tasks-')); });
14
+ afterEach(async () => { await rm(dir, { recursive: true, force: true }); });
15
+
16
+ describe('tasks.jsonl storage', () => {
17
+ test('round trips meta and tasks', async () => {
18
+ await writeTasksJsonl(dir, meta, [task]);
19
+ await expect(readTasksJsonl(dir)).resolves.toEqual({ meta, tasks: [task] });
20
+ });
21
+
22
+ test('round trips tasks without details (lightweight checklist)', async () => {
23
+ const lightweight: TaskRecord = { _type: 'task', id: 't-002', description: 'Quick fix', status: 'pending', created_at: now, updated_at: now };
24
+ await writeTasksJsonl(dir, meta, [lightweight]);
25
+ const result = await readTasksJsonl(dir);
26
+ expect(result?.tasks[0]?.id).toBe('t-002');
27
+ expect(result?.tasks[0]?.details).toBeUndefined();
28
+ });
29
+
30
+ test('missing file returns undefined', async () => {
31
+ await expect(readTasksJsonl(dir)).resolves.toBeUndefined();
32
+ });
33
+
34
+ test('rejects corrupt lines', async () => {
35
+ await Bun.write(join(dir, 'tasks.jsonl'), `${JSON.stringify(meta)}\nnot-json\n`);
36
+ await expect(readTasksJsonl(dir)).rejects.toThrow(/Invalid JSONL/);
37
+ });
38
+
39
+ test('rejects empty files', async () => {
40
+ await Bun.write(join(dir, 'tasks.jsonl'), '');
41
+ await expect(readTasksJsonl(dir)).rejects.toThrow(/meta/);
42
+ });
43
+
44
+ test('updates a task by id and rewrites the snapshot', async () => {
45
+ await writeTasksJsonl(dir, meta, [task]);
46
+ const updated = await updateTask(dir, 't-001', { status: 'done', notes: 'finished' });
47
+
48
+ expect(updated.status).toBe('done');
49
+ expect(updated.notes).toBe('finished');
50
+ expect((await readTasksJsonl(dir))?.tasks[0]?.status).toBe('done');
51
+ });
52
+ });
@@ -0,0 +1,68 @@
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(isTaskMeta({ _type: 'task', title: 'Refactor', plan_name: 'refactor', created_at: now })).toBe(false);
67
+ });
68
+ });
@@ -10,12 +10,13 @@ export const PLAN_TOOLS = [
10
10
  'find',
11
11
  'ls',
12
12
  'submit_plan',
13
+ 'write',
13
14
  'questionnaire',
14
15
  'search_skills',
15
16
  'subagent',
16
17
  ];
17
18
 
18
- export const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'update_step'];
19
+ export const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'update_task'];
19
20
 
20
21
  // ── Model + thinking presets ─────────────────────────────────────────────────
21
22
  export const PLAN_MODEL = { provider: 'anthropic', id: 'claude-opus-4-6' } as const;
@@ -0,0 +1,67 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import pug from 'pug';
5
+ import type { PlanData } from '../types.js';
6
+
7
+ const templatePath = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'plan.pug');
8
+
9
+ export function renderPlanHtml(plan: PlanData, prototype?: string): string {
10
+ const template = readFileSync(templatePath, 'utf8');
11
+ const prototypeHtml = prototype?.trim() ? pug.render(prototype) : undefined;
12
+
13
+ return pug.render(template, {
14
+ filename: templatePath,
15
+ plan,
16
+ prototypeHtml,
17
+ handoffHtml: markdownToHtml(plan.handoff),
18
+ taskCountLabel: `${plan.tasks.length} ${plan.tasks.length === 1 ? 'task' : 'tasks'}`,
19
+ generatedAt: new Date().toISOString(),
20
+ });
21
+ }
22
+
23
+ function markdownToHtml(markdown: string): string {
24
+ const lines = markdown.split(/\r?\n/);
25
+ const html: string[] = [];
26
+ let inList = false;
27
+
28
+ for (const line of lines) {
29
+ if (line.startsWith('# ')) {
30
+ if (inList) {
31
+ html.push('</ul>');
32
+ inList = false;
33
+ }
34
+ html.push(`<h1>${escapeHtml(line.slice(2))}</h1>`);
35
+ } else if (line.startsWith('## ')) {
36
+ if (inList) {
37
+ html.push('</ul>');
38
+ inList = false;
39
+ }
40
+ html.push(`<h2>${escapeHtml(line.slice(3))}</h2>`);
41
+ } else if (line.startsWith('- ')) {
42
+ if (!inList) {
43
+ html.push('<ul>');
44
+ inList = true;
45
+ }
46
+ html.push(`<li>${escapeHtml(line.slice(2))}</li>`);
47
+ } else if (line.trim()) {
48
+ if (inList) {
49
+ html.push('</ul>');
50
+ inList = false;
51
+ }
52
+ html.push(`<p>${escapeHtml(line)}</p>`);
53
+ }
54
+ }
55
+
56
+ if (inList) html.push('</ul>');
57
+ return html.join('\n');
58
+ }
59
+
60
+ function escapeHtml(value: string): string {
61
+ return value
62
+ .replaceAll('&', '&amp;')
63
+ .replaceAll('<', '&lt;')
64
+ .replaceAll('>', '&gt;')
65
+ .replaceAll('"', '&quot;')
66
+ .replaceAll("'", '&#39;');
67
+ }
@@ -0,0 +1,59 @@
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
+ .tasks { display:grid; gap:12px; }
21
+ .task { border:1px solid var(--line); border-radius:14px; padding:16px; background:#0f1218; }
22
+ .task-top { display:flex; gap:10px; align-items:center; margin-bottom:8px; }
23
+ .id { color:var(--accent); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
24
+ .status { margin-left:auto; color:var(--muted); }
25
+ .task.done .status { color:var(--done); }
26
+ .task.blocked .status { color:var(--blocked); }
27
+ .description { font-weight:650; }
28
+ .details { color:#c7ccda; margin:0; white-space:pre-wrap; }
29
+ .deps { color:var(--muted); margin-top:10px; font-size:12px; }
30
+ body
31
+ main
32
+ header
33
+ div
34
+ h1= plan.title
35
+ .meta
36
+ span.pill= plan.planName
37
+ span.pill= taskCountLabel
38
+ .meta
39
+ span.pill Generated #{generatedAt}
40
+ section.handoff
41
+ h2 Handoff
42
+ != handoffHtml
43
+ section
44
+ h2 Tasks
45
+ .tasks
46
+ each task in plan.tasks
47
+ article.task(class=task.status)
48
+ .task-top
49
+ span.id= task.id
50
+ span.description= task.description
51
+ span.status= task.status
52
+ if task.details
53
+ p.details= task.details
54
+ if task.depends_on && task.depends_on.length
55
+ .deps Depends on #{task.depends_on.join(', ')}
56
+ if prototypeHtml
57
+ section.prototype
58
+ h2 Prototype
59
+ != prototypeHtml