@dreki-gg/pi-plan-mode 0.6.4 → 0.10.1

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,106 @@
1
1
  # @dreki-gg/pi-plan-mode
2
2
 
3
+ ## 0.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 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`.
8
+
9
+ ## 0.10.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Refactor: extract plan-mode god module into domain-driven modules. index.ts goes from ~780 to ~308 lines.
14
+
15
+ New files:
16
+
17
+ - `constants.ts` — tool sets, model presets, thinking levels, model picker options
18
+ - `state.ts` — PlanModeState class encapsulating all mutable state with persist/restore
19
+ - `plan-storage.ts` — disk I/O: save/load plans, exec-pending markers, manifest updates
20
+ - `ui.ts` — status bar and step widget rendering
21
+ - `prompts.ts` — plan phase and execution phase prompt builders
22
+ - `context-filter.ts` — message filtering for context event
23
+ - `phase-transitions.ts` — enter/exit plan mode, start execution, model switching
24
+ - `resume.ts` — resume flow, model picker, new session handoff
25
+
26
+ No functional changes — pure structural refactor.
27
+
28
+ ## 0.9.2
29
+
30
+ ### Patch Changes
31
+
32
+ - 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.
33
+
34
+ ## 0.9.1
35
+
36
+ ### Patch Changes
37
+
38
+ - Simplify execution model picker to just two preset options: gpt-5.5 and claude-opus-4-6. No more nested registry browsing.
39
+
40
+ ## 0.9.0
41
+
42
+ ### Minor Changes
43
+
44
+ - 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.
45
+
46
+ New features:
47
+
48
+ - Model picker before execution: choose Default, Previous, or any model from registry
49
+ - `/plan-exec` command for direct execution handoff
50
+ - Removed `search_skills` from execution tools (executor follows the plan, not skills)
51
+
52
+ Breaking: Execution always creates a new session. The planning session is preserved and linked via parentSession.
53
+
54
+ ## 0.8.1
55
+
56
+ ### Patch Changes
57
+
58
+ - 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.
59
+
60
+ ## 0.8.0
61
+
62
+ ### Minor Changes
63
+
64
+ - 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.
65
+
66
+ ## 0.7.4
67
+
68
+ ### Patch Changes
69
+
70
+ - 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.
71
+
72
+ ## 0.7.3
73
+
74
+ ### Patch Changes
75
+
76
+ - Fix "Follow up" menu option to wrap user message with planner context, instructing the agent to revise and resubmit the plan via submit_plan.
77
+
78
+ ## 0.7.2
79
+
80
+ ### Patch Changes
81
+
82
+ - Drop planning conversation from executor context to prevent context window overflow when switching to a model with a smaller context window.
83
+
84
+ ## 0.7.1
85
+
86
+ ### Patch Changes
87
+
88
+ - Replace bundled workspace dependency on @dreki-gg/pi-command-sandbox with a normal registry dependency. Removes prepack/postpack scripts and bundledDependencies.
89
+
90
+ - Updated dependencies []:
91
+ - @dreki-gg/pi-command-sandbox@0.2.0
92
+
93
+ ## 0.7.0
94
+
95
+ ### Minor Changes
96
+
97
+ - Replace file-based plan handoff (PLAN.md + START-PROMPT.md) with structured tools:
98
+ - `submit_plan` tool: planner submits structured plan data (title, context, steps, risks) → writes `.plans/<name>/plan.json`
99
+ - `update_step` tool: executor marks steps as done/skipped/blocked with optional notes
100
+ - Blocked steps pause execution and prompt user for action (skip, provide instructions, re-plan, abort)
101
+ - Plan completion when all steps are done or skipped
102
+ - Removed regex-based `[DONE:n]` scanning and markdown parsing
103
+
3
104
  ## 0.6.4
4
105
 
5
106
  ### 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
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { isSafeCommand } from './utils.js';
2
+ import { isSafeCommand } from '../utils.js';
3
3
 
4
4
  describe('isSafeCommand', () => {
5
5
  // ── Commands that SHOULD be allowed ──────────────────────────────────────
@@ -12,6 +12,12 @@ describe('isSafeCommand', () => {
12
12
  expect(isSafeCommand('mkdir .plans/my-plan')).toBe(true);
13
13
  });
14
14
 
15
+ test('command with 2>/dev/null stderr redirect', () => {
16
+ expect(
17
+ isSafeCommand('cat .release-please-manifest.json 2>/dev/null; echo "---"; cat release-please-config.json 2>/dev/null'),
18
+ ).toBe(true);
19
+ });
20
+
15
21
  test('curl with 2>/dev/null pipe chain', () => {
16
22
  expect(
17
23
  isSafeCommand(
@@ -0,0 +1,33 @@
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
+ ];
16
+
17
+ export const EXEC_TOOLS = ['read', 'bash', 'edit', 'write', 'update_step'];
18
+
19
+ // ── Model + thinking presets ─────────────────────────────────────────────────
20
+ export const PLAN_MODEL = { provider: 'anthropic', id: 'claude-opus-4-6' } as const;
21
+ export const PLAN_THINKING = 'medium' as const;
22
+
23
+ export const EXEC_MODEL = { provider: 'openai', id: 'gpt-5.5' } as const;
24
+ export const EXEC_THINKING = 'low' as const;
25
+
26
+ // ── Exec-pending marker file name ────────────────────────────────────────────
27
+ export const EXEC_PENDING_FILE = '.exec-pending.json';
28
+
29
+ // ── Execution model picker options ───────────────────────────────────────────
30
+ export const EXEC_MODEL_OPTIONS: { label: string; model: { provider: string; id: string } }[] = [
31
+ { label: 'gpt-5.5', model: { provider: 'openai', id: 'gpt-5.5' } },
32
+ { label: 'claude-opus-4-6', model: { provider: 'anthropic', id: 'claude-opus-4-6' } },
33
+ ];
@@ -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
+ }