@dreki-gg/pi-plan-mode 0.7.2 → 0.13.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 +99 -0
- package/extensions/plan-mode/__tests__/agent-end-safety.test.ts +125 -0
- package/extensions/plan-mode/__tests__/package-skills.test.ts +47 -0
- package/extensions/plan-mode/__tests__/prompts.test.ts +38 -0
- package/extensions/plan-mode/{utils.test.ts → __tests__/utils.test.ts} +1 -1
- package/extensions/plan-mode/constants.ts +34 -0
- package/extensions/plan-mode/context-filter.ts +32 -0
- package/extensions/plan-mode/index.ts +166 -411
- package/extensions/plan-mode/phase-transitions.ts +87 -0
- package/extensions/plan-mode/plan-storage.ts +80 -0
- package/extensions/plan-mode/prompts.ts +72 -0
- package/extensions/plan-mode/resume.ts +121 -0
- package/extensions/plan-mode/state.ts +47 -0
- package/extensions/plan-mode/tools/submit-plan.ts +10 -16
- package/extensions/plan-mode/tools/update-step.ts +3 -0
- package/extensions/plan-mode/types.ts +8 -2
- package/extensions/plan-mode/ui.ts +39 -0
- package/package.json +7 -2
- package/skills/technical-options/SKILL.md +89 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# @dreki-gg/pi-plan-mode
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Replace context+risks with HANDOFF.md and step summaries. submit_plan now writes a HANDOFF.md alongside plan.json. Completion message shows an actual summary of changes from step notes instead of just a checklist. Executor is prompted to always include notes summarizing what was done.
|
|
8
|
+
|
|
9
|
+
## 0.12.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Include `skills/` directory in package files so the bundled technical-options skill is published.
|
|
14
|
+
|
|
15
|
+
## 0.12.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- Bundle `technical-options` skill inside the package (installable via `pi.skills`). The planner prompt now explicitly tells the agent to generate proposals itself and only delegate voting to subagents, keeping the planner visible as the main agent.
|
|
20
|
+
|
|
21
|
+
## 0.11.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- Integrate technical-options skill into plan mode: add `subagent` to plan-phase tools and nudge the planner to use structured proposal evaluation when facing significant design decisions with multiple viable approaches.
|
|
26
|
+
|
|
27
|
+
## 0.10.1
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Fix "Execute Plan" menu option crashing with "Agent is already processing" error. The `sendUserMessage('/plan-exec')` call inside the `agent_end` handler was missing `deliverAs: 'followUp'`. Added regression test that scans all `sendUserMessage` calls inside `agent_end` handlers to ensure they always include `deliverAs`.
|
|
32
|
+
|
|
33
|
+
## 0.10.0
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- Refactor: extract plan-mode god module into domain-driven modules. index.ts goes from ~780 to ~308 lines.
|
|
38
|
+
|
|
39
|
+
New files:
|
|
40
|
+
|
|
41
|
+
- `constants.ts` — tool sets, model presets, thinking levels, model picker options
|
|
42
|
+
- `state.ts` — PlanModeState class encapsulating all mutable state with persist/restore
|
|
43
|
+
- `plan-storage.ts` — disk I/O: save/load plans, exec-pending markers, manifest updates
|
|
44
|
+
- `ui.ts` — status bar and step widget rendering
|
|
45
|
+
- `prompts.ts` — plan phase and execution phase prompt builders
|
|
46
|
+
- `context-filter.ts` — message filtering for context event
|
|
47
|
+
- `phase-transitions.ts` — enter/exit plan mode, start execution, model switching
|
|
48
|
+
- `resume.ts` — resume flow, model picker, new session handoff
|
|
49
|
+
|
|
50
|
+
No functional changes — pure structural refactor.
|
|
51
|
+
|
|
52
|
+
## 0.9.2
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- Fix plan completion not triggering immediately: update_step now returns `terminate: true` when all steps are resolved, so the agent stops and agent_end fires right away with the completion message.
|
|
57
|
+
|
|
58
|
+
## 0.9.1
|
|
59
|
+
|
|
60
|
+
### Patch Changes
|
|
61
|
+
|
|
62
|
+
- Simplify execution model picker to just two preset options: gpt-5.5 and claude-opus-4-6. No more nested registry browsing.
|
|
63
|
+
|
|
64
|
+
## 0.9.0
|
|
65
|
+
|
|
66
|
+
### Minor Changes
|
|
67
|
+
|
|
68
|
+
- Plan execution now launches in a clean session via `ctx.newSession()` for true context isolation. The executor gets a fresh context window with zero planning history, no skill references, and no system prompt pollution — fixing the root cause of rogue executor behavior.
|
|
69
|
+
|
|
70
|
+
New features:
|
|
71
|
+
|
|
72
|
+
- Model picker before execution: choose Default, Previous, or any model from registry
|
|
73
|
+
- `/plan-exec` command for direct execution handoff
|
|
74
|
+
- Removed `search_skills` from execution tools (executor follows the plan, not skills)
|
|
75
|
+
|
|
76
|
+
Breaking: Execution always creates a new session. The planning session is preserved and linked via parentSession.
|
|
77
|
+
|
|
78
|
+
## 0.8.1
|
|
79
|
+
|
|
80
|
+
### Patch Changes
|
|
81
|
+
|
|
82
|
+
- Strengthen execution context injection to prevent the agent from going rogue. The prompt now explicitly forbids running diagnostics/skills/linters unless a step asks for it, highlights the current step front and center, and demands immediate update_step calls.
|
|
83
|
+
|
|
84
|
+
## 0.8.0
|
|
85
|
+
|
|
86
|
+
### Minor Changes
|
|
87
|
+
|
|
88
|
+
- Add `/plan resume` command to pick up in-progress plans from disk. Shows a list of in-progress plans from `.plans/plans.json`, lets the user select one, and resumes execution from where it left off. Also supports re-planning from scratch.
|
|
89
|
+
|
|
90
|
+
## 0.7.4
|
|
91
|
+
|
|
92
|
+
### Patch Changes
|
|
93
|
+
|
|
94
|
+
- Simplify "Follow up" menu option: just dismiss the menu and let the user type naturally instead of opening an editor. The planner remains active with submit_plan available.
|
|
95
|
+
|
|
96
|
+
## 0.7.3
|
|
97
|
+
|
|
98
|
+
### Patch Changes
|
|
99
|
+
|
|
100
|
+
- Fix "Follow up" menu option to wrap user message with planner context, instructing the agent to revise and resubmit the plan via submit_plan.
|
|
101
|
+
|
|
3
102
|
## 0.7.2
|
|
4
103
|
|
|
5
104
|
### Patch Changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for plan-mode extension wiring.
|
|
3
|
+
*
|
|
4
|
+
* These are static analysis tests that scan source code for common mistakes
|
|
5
|
+
* that cause runtime errors in the pi extension lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const INDEX_PATH = join(import.meta.dir, '..', 'index.ts');
|
|
13
|
+
const indexSource = readFileSync(INDEX_PATH, 'utf-8');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract all `pi.on('agent_end', ...)` handler bodies from the source.
|
|
17
|
+
*
|
|
18
|
+
* The agent_end handler runs while the agent is still technically "processing",
|
|
19
|
+
* so every sendUserMessage call inside it MUST include deliverAs to avoid:
|
|
20
|
+
* "Agent is already processing. Specify streamingBehavior..."
|
|
21
|
+
*/
|
|
22
|
+
function extractAgentEndBodies(source: string): string[] {
|
|
23
|
+
const bodies: string[] = [];
|
|
24
|
+
// Match pi.on('agent_end', ...) and extract everything until the matching });
|
|
25
|
+
const pattern = /pi\.on\(\s*'agent_end'/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
28
|
+
const startIdx = match.index;
|
|
29
|
+
// Find the handler body by counting braces
|
|
30
|
+
let braceCount = 0;
|
|
31
|
+
let inHandler = false;
|
|
32
|
+
let bodyStart = startIdx;
|
|
33
|
+
let bodyEnd = startIdx;
|
|
34
|
+
|
|
35
|
+
for (let i = startIdx; i < source.length; i++) {
|
|
36
|
+
if (source[i] === '{') {
|
|
37
|
+
if (!inHandler) {
|
|
38
|
+
inHandler = true;
|
|
39
|
+
bodyStart = i;
|
|
40
|
+
}
|
|
41
|
+
braceCount++;
|
|
42
|
+
} else if (source[i] === '}') {
|
|
43
|
+
braceCount--;
|
|
44
|
+
if (inHandler && braceCount === 0) {
|
|
45
|
+
bodyEnd = i + 1;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bodies.push(source.slice(bodyStart, bodyEnd));
|
|
52
|
+
}
|
|
53
|
+
return bodies;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find all sendUserMessage calls in a code block, returning each call
|
|
58
|
+
* with its line content and whether it has deliverAs.
|
|
59
|
+
*/
|
|
60
|
+
function findSendUserMessageCalls(code: string): Array<{
|
|
61
|
+
line: string;
|
|
62
|
+
hasDeliverAs: boolean;
|
|
63
|
+
isCommand: boolean;
|
|
64
|
+
}> {
|
|
65
|
+
const results: Array<{ line: string; hasDeliverAs: boolean; isCommand: boolean }> = [];
|
|
66
|
+
const lines = code.split('\n');
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const line = lines[i].trim();
|
|
70
|
+
if (!line.includes('sendUserMessage')) continue;
|
|
71
|
+
if (line.startsWith('//') || line.startsWith('*')) continue;
|
|
72
|
+
|
|
73
|
+
// Look ahead generously — multi-line template literals can push deliverAs 12+ lines away
|
|
74
|
+
const context = lines.slice(i, Math.min(i + 15, lines.length)).join(' ');
|
|
75
|
+
const hasDeliverAs = context.includes('deliverAs');
|
|
76
|
+
|
|
77
|
+
// Check if this is sending a slash command (e.g., '/plan-exec')
|
|
78
|
+
const isCommand = /sendUserMessage\s*\(\s*['"`]\//.test(context);
|
|
79
|
+
|
|
80
|
+
results.push({ line, hasDeliverAs, isCommand });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('agent_end handler safety', () => {
|
|
87
|
+
test('all sendUserMessage calls inside agent_end must include deliverAs', () => {
|
|
88
|
+
const bodies = extractAgentEndBodies(indexSource);
|
|
89
|
+
expect(bodies.length).toBeGreaterThan(0);
|
|
90
|
+
|
|
91
|
+
const violations: string[] = [];
|
|
92
|
+
|
|
93
|
+
for (const body of bodies) {
|
|
94
|
+
const calls = findSendUserMessageCalls(body);
|
|
95
|
+
for (const call of calls) {
|
|
96
|
+
if (!call.hasDeliverAs) {
|
|
97
|
+
violations.push(call.line);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (violations.length > 0) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Found sendUserMessage calls inside agent_end without deliverAs.\n` +
|
|
105
|
+
`The agent is still "processing" during agent_end, so deliverAs is required.\n\n` +
|
|
106
|
+
`Violations:\n${violations.map((v) => ` - ${v}`).join('\n')}\n\n` +
|
|
107
|
+
`Fix: add { deliverAs: 'followUp' } to each call.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('sendUserMessage calls sending slash commands must include deliverAs', () => {
|
|
113
|
+
// Slash commands sent via sendUserMessage still go through the input pipeline
|
|
114
|
+
// and need deliverAs when called during agent processing
|
|
115
|
+
const bodies = extractAgentEndBodies(indexSource);
|
|
116
|
+
|
|
117
|
+
for (const body of bodies) {
|
|
118
|
+
const calls = findSendUserMessageCalls(body);
|
|
119
|
+
const commandCalls = calls.filter((c) => c.isCommand);
|
|
120
|
+
for (const call of commandCalls) {
|
|
121
|
+
expect(call.hasDeliverAs).toBe(true);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const PKG_ROOT = join(import.meta.dir, '..', '..', '..');
|
|
6
|
+
const PKG_JSON = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8'));
|
|
7
|
+
|
|
8
|
+
describe('package.json pi manifest', () => {
|
|
9
|
+
test('declares skills directory', () => {
|
|
10
|
+
expect(PKG_JSON.pi?.skills).toBeDefined();
|
|
11
|
+
expect(PKG_JSON.pi.skills).toContain('./skills');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('bundled technical-options skill', () => {
|
|
16
|
+
const skillDir = join(PKG_ROOT, 'skills', 'technical-options');
|
|
17
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
18
|
+
|
|
19
|
+
test('SKILL.md exists', () => {
|
|
20
|
+
expect(existsSync(skillFile)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('has valid frontmatter with name and description', () => {
|
|
24
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
25
|
+
expect(content).toMatch(/^---\n/);
|
|
26
|
+
expect(content).toMatch(/name:\s*technical-options/);
|
|
27
|
+
expect(content).toMatch(/description:/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('description does not contain overly broad triggers', () => {
|
|
31
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
32
|
+
// These were flagged during review as too broad for routing
|
|
33
|
+
expect(content).not.toMatch(/description:.*help me decide/i);
|
|
34
|
+
expect(content).not.toMatch(/description:.*what are my options/i);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('skill content mentions parallel voting agents', () => {
|
|
38
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
39
|
+
expect(content).toContain('voting');
|
|
40
|
+
expect(content).toContain('parallel');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('skill content includes adversarial framing challenge step', () => {
|
|
44
|
+
const content = readFileSync(skillFile, 'utf-8');
|
|
45
|
+
expect(content).toMatch(/challenge.*framing|framing.*challenge/i);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { buildPlanModePrompt } from '../prompts.js';
|
|
3
|
+
import { PLAN_TOOLS } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
describe('buildPlanModePrompt', () => {
|
|
6
|
+
const prompt = buildPlanModePrompt();
|
|
7
|
+
|
|
8
|
+
test('mentions technical-options skill for significant decisions', () => {
|
|
9
|
+
expect(prompt).toContain('technical-options');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('mentions handoff instead of context and risks', () => {
|
|
13
|
+
expect(prompt).toContain('handoff');
|
|
14
|
+
expect(prompt).not.toContain('- risks:');
|
|
15
|
+
expect(prompt).not.toContain('- context:');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('tells planner to do proposal generation itself, not delegate', () => {
|
|
19
|
+
// The planner should generate proposals as the main agent, only using
|
|
20
|
+
// subagents for voting/evaluation — not delegating the entire workflow
|
|
21
|
+
expect(prompt).toMatch(/you.*generat|generat.*yourself|do this yourself/i);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('mentions subagent is available for voting only', () => {
|
|
25
|
+
// Should clarify subagent is for evaluation, not for the whole workflow
|
|
26
|
+
expect(prompt).toMatch(/subagent|voting|evaluat/i);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('PLAN_TOOLS', () => {
|
|
31
|
+
test('includes subagent for voting workflows', () => {
|
|
32
|
+
expect(PLAN_TOOLS).toContain('subagent');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('includes search_skills for skill discovery', () => {
|
|
36
|
+
expect(PLAN_TOOLS).toContain('search_skills');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan mode constants — tool sets, model presets, thinking levels, and execution model options.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ── Tool sets ────────────────────────────────────────────────────────────────
|
|
6
|
+
export const PLAN_TOOLS = [
|
|
7
|
+
'read',
|
|
8
|
+
'bash',
|
|
9
|
+
'grep',
|
|
10
|
+
'find',
|
|
11
|
+
'ls',
|
|
12
|
+
'submit_plan',
|
|
13
|
+
'questionnaire',
|
|
14
|
+
'search_skills',
|
|
15
|
+
'subagent',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'update_step'];
|
|
19
|
+
|
|
20
|
+
// ── Model + thinking presets ─────────────────────────────────────────────────
|
|
21
|
+
export const PLAN_MODEL = { provider: 'anthropic', id: 'claude-opus-4-6' } as const;
|
|
22
|
+
export const PLAN_THINKING = 'medium' as const;
|
|
23
|
+
|
|
24
|
+
export const EXEC_MODEL = { provider: 'openai', id: 'gpt-5.5' } as const;
|
|
25
|
+
export const EXEC_THINKING = 'low' as const;
|
|
26
|
+
|
|
27
|
+
// ── Exec-pending marker file name ────────────────────────────────────────────
|
|
28
|
+
export const EXEC_PENDING_FILE = '.exec-pending.json';
|
|
29
|
+
|
|
30
|
+
// ── Execution model picker options ───────────────────────────────────────────
|
|
31
|
+
export const EXEC_MODEL_OPTIONS: { label: string; model: { provider: string; id: string } }[] = [
|
|
32
|
+
{ label: 'gpt-5.5', model: { provider: 'openai', id: 'gpt-5.5' } },
|
|
33
|
+
{ label: 'claude-opus-4-6', model: { provider: 'anthropic', id: 'claude-opus-4-6' } },
|
|
34
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message filtering for the context event — strips planning artifacts during execution
|
|
3
|
+
* and removes stale plan-mode injections when not in plan mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Drop everything before the execution start index. */
|
|
7
|
+
export function filterExecutionMessages<T>(
|
|
8
|
+
messages: T[],
|
|
9
|
+
executionStartIdx: number,
|
|
10
|
+
): T[] {
|
|
11
|
+
return messages.filter((_m, i) => i >= executionStartIdx);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Strip stale plan-mode injected messages when not in plan mode. */
|
|
15
|
+
export function filterStalePlanMessages<T>(messages: T[]): T[] {
|
|
16
|
+
return messages.filter((m) => {
|
|
17
|
+
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
18
|
+
if (msg.customType === 'plan-mode-context') return false;
|
|
19
|
+
if (msg.role !== 'user') return true;
|
|
20
|
+
const content = msg.content;
|
|
21
|
+
if (typeof content === 'string') {
|
|
22
|
+
return !content.includes('[PLAN MODE ACTIVE]');
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(content)) {
|
|
25
|
+
return !content.some(
|
|
26
|
+
(c: { type?: string; text?: string }) =>
|
|
27
|
+
c.type === 'text' && c.text?.includes('[PLAN MODE ACTIVE]'),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
}
|