@dreki-gg/pi-plan-mode 0.14.0 → 0.14.5
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 +35 -0
- package/extensions/plan-mode/__tests__/phase-transitions.test.ts +42 -0
- package/extensions/plan-mode/__tests__/prompts.test.ts +39 -1
- package/extensions/plan-mode/__tests__/task-storage.test.ts +8 -0
- package/extensions/plan-mode/__tests__/types.test.ts +13 -0
- package/extensions/plan-mode/html/render.ts +67 -23
- package/extensions/plan-mode/html/templates/plan.pug +12 -1
- package/extensions/plan-mode/index.ts +2 -28
- package/extensions/plan-mode/phase-transitions.ts +1 -1
- package/extensions/plan-mode/prompts.ts +13 -4
- package/extensions/plan-mode/state.ts +7 -0
- package/extensions/plan-mode/tools/submit-plan.ts +5 -3
- package/extensions/plan-mode/tools/update-task.ts +1 -1
- package/extensions/plan-mode/types.ts +2 -2
- package/extensions/plan-mode/ui.ts +6 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @dreki-gg/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 0.14.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Proper markdown rendering in plan.html — fenced code blocks, inline code, bold, italic, links, and all heading levels.
|
|
8
|
+
|
|
9
|
+
## 0.14.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Remove task list widget entirely — plan.jsonl is the source of truth.
|
|
14
|
+
|
|
15
|
+
## 0.14.3
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Only show task widget during active plan execution, not after exiting plan mode.
|
|
20
|
+
|
|
21
|
+
## 0.14.2
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Remove post-plan submission menu and auto-hide task widget when all tasks are resolved.
|
|
26
|
+
|
|
27
|
+
## 0.14.1
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Fix update_task failing after exiting plan mode; make task details optional for lightweight checklist-style plans.
|
|
32
|
+
|
|
33
|
+
- exitPlanMode now preserves plan data so update_task works outside execution mode
|
|
34
|
+
- submit_plan accepts tasks without details for self-execution workflows
|
|
35
|
+
- Plan widget shows in tracking mode after exiting plan mode
|
|
36
|
+
- Prompt guidance distinguishes delegation vs self-execution plan weights
|
|
37
|
+
|
|
3
38
|
## 0.14.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
|
@@ -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
|
+
});
|
|
@@ -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');
|
|
@@ -19,6 +19,14 @@ describe('tasks.jsonl storage', () => {
|
|
|
19
19
|
await expect(readTasksJsonl(dir)).resolves.toEqual({ meta, tasks: [task] });
|
|
20
20
|
});
|
|
21
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
|
+
|
|
22
30
|
test('missing file returns undefined', async () => {
|
|
23
31
|
await expect(readTasksJsonl(dir)).resolves.toBeUndefined();
|
|
24
32
|
});
|
|
@@ -20,6 +20,19 @@ describe('task record type guards', () => {
|
|
|
20
20
|
).toBe(true);
|
|
21
21
|
});
|
|
22
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
|
+
|
|
23
36
|
test('rejects malformed task records', () => {
|
|
24
37
|
expect(isTaskRecord({ _type: 'task', id: 't-001', status: 'pending' })).toBe(false);
|
|
25
38
|
expect(
|
|
@@ -24,39 +24,83 @@ function markdownToHtml(markdown: string): string {
|
|
|
24
24
|
const lines = markdown.split(/\r?\n/);
|
|
25
25
|
const html: string[] = [];
|
|
26
26
|
let inList = false;
|
|
27
|
+
let inCode = false;
|
|
28
|
+
let codeLang = '';
|
|
29
|
+
const codeLines: string[] = [];
|
|
27
30
|
|
|
28
31
|
for (const line of lines) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
inList = false;
|
|
32
|
+
// Fenced code blocks
|
|
33
|
+
if (line.startsWith('```')) {
|
|
34
|
+
if (!inCode) {
|
|
35
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
36
|
+
inCode = true;
|
|
37
|
+
codeLang = line.slice(3).trim();
|
|
38
|
+
codeLines.length = 0;
|
|
39
|
+
} else {
|
|
40
|
+
const langAttr = codeLang ? ` class="language-${escapeHtml(codeLang)}"` : '';
|
|
41
|
+
html.push(`<pre><code${langAttr}>${codeLines.map(escapeHtml).join('\n')}</code></pre>`);
|
|
42
|
+
inCode = false;
|
|
43
|
+
codeLang = '';
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
html.push('</ul>');
|
|
50
|
-
inList = false;
|
|
51
|
-
}
|
|
52
|
-
html.push(`<p>${escapeHtml(line)}</p>`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (inCode) {
|
|
49
|
+
codeLines.push(line);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Headings
|
|
54
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
|
|
55
|
+
if (headingMatch) {
|
|
56
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
57
|
+
const level = headingMatch[1].length;
|
|
58
|
+
html.push(`<h${level}>${inlineMarkdown(escapeHtml(headingMatch[2]))}</h${level}>`);
|
|
59
|
+
continue;
|
|
53
60
|
}
|
|
61
|
+
|
|
62
|
+
// List items (- or *)
|
|
63
|
+
const listMatch = line.match(/^\s*[-*]\s+(.*)$/);
|
|
64
|
+
if (listMatch) {
|
|
65
|
+
if (!inList) { html.push('<ul>'); inList = true; }
|
|
66
|
+
html.push(`<li>${inlineMarkdown(escapeHtml(listMatch[1]))}</li>`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Blank line
|
|
71
|
+
if (!line.trim()) {
|
|
72
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Paragraph
|
|
77
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
78
|
+
html.push(`<p>${inlineMarkdown(escapeHtml(line))}</p>`);
|
|
54
79
|
}
|
|
55
80
|
|
|
81
|
+
// Close unclosed blocks
|
|
82
|
+
if (inCode) {
|
|
83
|
+
html.push(`<pre><code>${codeLines.map(escapeHtml).join('\n')}</code></pre>`);
|
|
84
|
+
}
|
|
56
85
|
if (inList) html.push('</ul>');
|
|
57
86
|
return html.join('\n');
|
|
58
87
|
}
|
|
59
88
|
|
|
89
|
+
/** Converts inline markdown (bold, italic, inline code, links) in already-escaped HTML. */
|
|
90
|
+
function inlineMarkdown(text: string): string {
|
|
91
|
+
return text
|
|
92
|
+
// Inline code: `code`
|
|
93
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
94
|
+
// Bold: **text** or __text__
|
|
95
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
96
|
+
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
97
|
+
// Italic: *text* or _text_
|
|
98
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
99
|
+
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
100
|
+
// Links: [text](url)
|
|
101
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
102
|
+
}
|
|
103
|
+
|
|
60
104
|
function escapeHtml(value: string): string {
|
|
61
105
|
return value
|
|
62
106
|
.replaceAll('&', '&')
|
|
@@ -17,6 +17,16 @@ html(lang="en")
|
|
|
17
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
18
|
.handoff :first-child { margin-top:0; }
|
|
19
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; }
|
|
20
30
|
.tasks { display:grid; gap:12px; }
|
|
21
31
|
.task { border:1px solid var(--line); border-radius:14px; padding:16px; background:#0f1218; }
|
|
22
32
|
.task-top { display:flex; gap:10px; align-items:center; margin-bottom:8px; }
|
|
@@ -49,7 +59,8 @@ html(lang="en")
|
|
|
49
59
|
span.id= task.id
|
|
50
60
|
span.description= task.description
|
|
51
61
|
span.status= task.status
|
|
52
|
-
|
|
62
|
+
if task.details
|
|
63
|
+
p.details= task.details
|
|
53
64
|
if task.depends_on && task.depends_on.length
|
|
54
65
|
.deps Depends on #{task.depends_on.join(', ')}
|
|
55
66
|
if prototypeHtml
|
|
@@ -271,34 +271,8 @@ export default function planMode(pi: ExtensionAPI): void {
|
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
if (!state.planDir || !state.plan) return;
|
|
277
|
-
|
|
278
|
-
const choice = await ctx.ui.select('Plan ready — what next?', [
|
|
279
|
-
'Execute Plan', 'Refine Plan', 'Follow up', 'Exit plan mode',
|
|
280
|
-
]);
|
|
281
|
-
|
|
282
|
-
if (choice === 'Execute Plan') {
|
|
283
|
-
pi.sendUserMessage('/plan-exec', { deliverAs: 'followUp' });
|
|
284
|
-
} else if (choice === 'Refine Plan') {
|
|
285
|
-
pi.sendUserMessage(
|
|
286
|
-
`Review the plan you just created with an adversarial lens. Challenge assumptions, find gaps, identify risks, and look for:
|
|
287
|
-
|
|
288
|
-
- Missing edge cases or error handling
|
|
289
|
-
- Incorrect assumptions about the codebase
|
|
290
|
-
- Tasks that are too vague or could be misinterpreted
|
|
291
|
-
- Missing dependencies between tasks
|
|
292
|
-
- Simpler alternatives that were overlooked
|
|
293
|
-
|
|
294
|
-
After your review, call submit_plan again with the improved plan.`,
|
|
295
|
-
{ deliverAs: 'followUp' },
|
|
296
|
-
);
|
|
297
|
-
} else if (choice === 'Follow up') {
|
|
298
|
-
// No-op: dismiss menu, let user type naturally
|
|
299
|
-
} else if (choice === 'Exit plan mode') {
|
|
300
|
-
await exitPlanMode(state, pi, ctx);
|
|
301
|
-
}
|
|
274
|
+
// Plan submitted — user can review plan.html then /plan-exec or type naturally.
|
|
275
|
+
// No menu needed: plan.jsonl + plan.html are the source of truth.
|
|
302
276
|
});
|
|
303
277
|
|
|
304
278
|
// ── Event: session restore ────────────────────────────────────────────────
|
|
@@ -54,7 +54,7 @@ export async function exitPlanMode(
|
|
|
54
54
|
ctx: ExtensionContext,
|
|
55
55
|
): Promise<void> {
|
|
56
56
|
const { previousModel, previousThinking } = state;
|
|
57
|
-
state.
|
|
57
|
+
state.exitPreservingPlan();
|
|
58
58
|
pi.setActiveTools(EXEC_TOOLS);
|
|
59
59
|
if (previousModel) {
|
|
60
60
|
await switchModel(pi, ctx, previousModel);
|
|
@@ -24,9 +24,13 @@ When you are ready to finalize the plan, call submit_plan with:
|
|
|
24
24
|
- name: a short kebab-case name (e.g. "add-auth-middleware")
|
|
25
25
|
- title: a human-readable plan title
|
|
26
26
|
- handoff: a markdown document that explains what is changing, why it matters, approach, decisions, file paths, APIs, patterns, constraints, and gotchas
|
|
27
|
-
- tasks: an array of tasks with id (e.g. "t-001"), description (≤60 chars), details, and optional depends_on task IDs
|
|
27
|
+
- tasks: an array of tasks with id (e.g. "t-001"), description (≤60 chars), optional details, and optional depends_on task IDs
|
|
28
28
|
- prototype: optional Pug markup for a creative prototype section in the generated plan.html
|
|
29
29
|
|
|
30
|
+
Plan weight:
|
|
31
|
+
- **Delegation plans** (different agent/human executes): include full details in each task so an executor with zero context can follow them.
|
|
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.
|
|
33
|
+
|
|
30
34
|
submit_plan is finalization, not the starting point. The generated plan.html is written but not opened automatically.
|
|
31
35
|
|
|
32
36
|
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,10 +42,16 @@ export function buildExecutionPrompt(plan: PlanData): string | undefined {
|
|
|
38
42
|
if (remaining.length === 0) return undefined;
|
|
39
43
|
|
|
40
44
|
const taskList = remaining
|
|
41
|
-
.map((task) =>
|
|
45
|
+
.map((task) => {
|
|
46
|
+
const line = `${task.id}. ${task.description}`;
|
|
47
|
+
return task.details ? `${line}\n Details: ${task.details}` : line;
|
|
48
|
+
})
|
|
42
49
|
.join('\n\n');
|
|
43
50
|
|
|
44
51
|
const currentTask = remaining[0];
|
|
52
|
+
const currentDetails = currentTask.details
|
|
53
|
+
? `\nDetails: ${currentTask.details}`
|
|
54
|
+
: '';
|
|
45
55
|
|
|
46
56
|
return `[EXECUTING PLAN — FOLLOW THE PLAN EXACTLY]
|
|
47
57
|
|
|
@@ -55,8 +65,7 @@ Rules:
|
|
|
55
65
|
- Do NOT deviate from the plan — if something seems wrong, call update_task with status "blocked"
|
|
56
66
|
|
|
57
67
|
## Current task
|
|
58
|
-
${currentTask.id}: ${currentTask.description}
|
|
59
|
-
Details: ${currentTask.details}
|
|
68
|
+
${currentTask.id}: ${currentTask.description}${currentDetails}
|
|
60
69
|
|
|
61
70
|
## Handoff
|
|
62
71
|
${plan.handoff}
|
|
@@ -44,4 +44,11 @@ export class PlanModeState {
|
|
|
44
44
|
this.plan = undefined;
|
|
45
45
|
this.executionStartIdx = undefined;
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/** Exit plan/execution mode but keep plan data for update_task tracking. */
|
|
49
|
+
exitPreservingPlan(): void {
|
|
50
|
+
this.planEnabled = false;
|
|
51
|
+
this.executing = false;
|
|
52
|
+
this.executionStartIdx = undefined;
|
|
53
|
+
}
|
|
47
54
|
}
|
|
@@ -25,7 +25,9 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
|
|
|
25
25
|
promptSnippet: 'Finalize the plan with title, handoff, tasks, dependencies, and optional prototype Pug',
|
|
26
26
|
promptGuidelines: [
|
|
27
27
|
'Only call submit_plan after shared understanding has been reached with the user.',
|
|
28
|
-
'Each task needs an id like t-001, a short description,
|
|
28
|
+
'Each task needs an id like t-001, a short description, and optional depends_on task IDs.',
|
|
29
|
+
'When a different agent or human will execute the plan, include detailed implementation instructions in each task\'s details field.',
|
|
30
|
+
'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.',
|
|
29
31
|
'The handoff must be thorough enough that both a human reviewer and executor agent with zero prior context can understand the plan.',
|
|
30
32
|
],
|
|
31
33
|
parameters: Type.Object({
|
|
@@ -36,7 +38,7 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
|
|
|
36
38
|
Type.Object({
|
|
37
39
|
id: Type.String({ description: 'Stable task ID, e.g. t-001' }),
|
|
38
40
|
description: Type.String({ description: 'Short task label for progress display (≤60 chars)' }),
|
|
39
|
-
details: Type.String({ description: 'Full implementation instructions for this task' }),
|
|
41
|
+
details: Type.Optional(Type.String({ description: 'Full implementation instructions for this task. Omit for lightweight checklist-style plans when you are executing yourself.' })),
|
|
40
42
|
depends_on: Type.Optional(Type.Array(Type.String({ description: 'Dependency task ID' }))),
|
|
41
43
|
}),
|
|
42
44
|
{ minItems: 1 },
|
|
@@ -53,7 +55,7 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
|
|
|
53
55
|
_type: 'task',
|
|
54
56
|
id: task.id,
|
|
55
57
|
description: task.description.slice(0, 60),
|
|
56
|
-
details: task.details,
|
|
58
|
+
details: task.details ?? '',
|
|
57
59
|
status: 'pending',
|
|
58
60
|
depends_on: task.depends_on,
|
|
59
61
|
created_at: now,
|
|
@@ -8,7 +8,7 @@ export interface TaskRecord {
|
|
|
8
8
|
_type: 'task';
|
|
9
9
|
id: string;
|
|
10
10
|
description: string;
|
|
11
|
-
details
|
|
11
|
+
details?: string;
|
|
12
12
|
status: TaskStatus;
|
|
13
13
|
depends_on?: string[];
|
|
14
14
|
notes?: string;
|
|
@@ -62,7 +62,7 @@ export function isTaskRecord(value: unknown): value is TaskRecord {
|
|
|
62
62
|
value._type === 'task' &&
|
|
63
63
|
typeof value.id === 'string' &&
|
|
64
64
|
typeof value.description === 'string' &&
|
|
65
|
-
typeof value.details === 'string' &&
|
|
65
|
+
(value.details === undefined || typeof value.details === 'string') &&
|
|
66
66
|
typeof value.status === 'string' &&
|
|
67
67
|
TASK_STATUSES.has(value.status as TaskStatus) &&
|
|
68
68
|
(value.depends_on === undefined || isStringArray(value.depends_on)) &&
|
|
@@ -12,28 +12,16 @@ export function updateUI(state: PlanModeState, ctx: ExtensionContext): void {
|
|
|
12
12
|
const done = state.plan.tasks.filter((task) => task.status === 'done').length;
|
|
13
13
|
const total = state.plan.tasks.length;
|
|
14
14
|
ctx.ui.setStatus('plan-mode', theme.fg('accent', `📋 exec ${done}/${total}`));
|
|
15
|
+
} else if (state.plan && !state.planEnabled) {
|
|
16
|
+
const done = state.plan.tasks.filter((task) => task.status === 'done').length;
|
|
17
|
+
const total = state.plan.tasks.length;
|
|
18
|
+
ctx.ui.setStatus('plan-mode', theme.fg('muted', `📋 ${done}/${total}`));
|
|
15
19
|
} else if (state.planEnabled) {
|
|
16
20
|
ctx.ui.setStatus('plan-mode', theme.fg('warning', '📝 plan'));
|
|
17
21
|
} else {
|
|
18
22
|
ctx.ui.setStatus('plan-mode', undefined);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const label = `${task.id} ${task.description}`;
|
|
24
|
-
switch (task.status) {
|
|
25
|
-
case 'done':
|
|
26
|
-
return theme.fg('success', '✓ ') + theme.fg('muted', theme.strikethrough(label));
|
|
27
|
-
case 'skipped':
|
|
28
|
-
return theme.fg('warning', '⊘ ') + theme.fg('muted', theme.strikethrough(label));
|
|
29
|
-
case 'blocked':
|
|
30
|
-
return theme.fg('error', '✗ ') + theme.fg('error', label);
|
|
31
|
-
default:
|
|
32
|
-
return theme.fg('muted', '☐ ') + label;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
ctx.ui.setWidget('plan-todos', lines);
|
|
36
|
-
} else {
|
|
37
|
-
ctx.ui.setWidget('plan-todos', undefined);
|
|
38
|
-
}
|
|
25
|
+
// Task list lives in plan.jsonl — no widget needed.
|
|
26
|
+
ctx.ui.setWidget('plan-todos', undefined);
|
|
39
27
|
}
|
package/package.json
CHANGED