@dreki-gg/pi-plan-mode 0.14.5 → 0.15.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.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Restrict write/edit tools to .plans/ directory only during plan phase. Add isPlanPath utility. Update prompt to document --help/man support and write restrictions.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies []:
12
+ - @dreki-gg/pi-command-sandbox@0.3.0
13
+
3
14
  ## 0.14.5
4
15
 
5
16
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { isSafeCommand } from '../utils.js';
2
+ import { isSafeCommand, isPlanPath } from '../utils.js';
3
3
 
4
4
  describe('isSafeCommand', () => {
5
5
  // ── Commands that SHOULD be allowed ──────────────────────────────────────
@@ -113,4 +113,61 @@ describe('isSafeCommand', () => {
113
113
  expect(isSafeCommand('touch newfile.ts')).toBe(false);
114
114
  });
115
115
  });
116
+
117
+ // ── Help/version commands via command-sandbox ────────────────────────────
118
+ describe('help and version commands', () => {
119
+ test('bun --help is allowed', () => {
120
+ expect(isSafeCommand('bun --help')).toBe(true);
121
+ });
122
+
123
+ test('man git is allowed', () => {
124
+ expect(isSafeCommand('man git')).toBe(true);
125
+ });
126
+
127
+ test('npm --version is allowed', () => {
128
+ expect(isSafeCommand('npm --version')).toBe(true);
129
+ });
130
+
131
+ test('rm --help is still blocked', () => {
132
+ expect(isSafeCommand('rm --help')).toBe(false);
133
+ });
134
+ });
135
+ });
136
+
137
+ describe('isPlanPath', () => {
138
+ test('relative .plans/ path', () => {
139
+ expect(isPlanPath('.plans/my-plan/context.md')).toBe(true);
140
+ });
141
+
142
+ test('absolute path containing .plans/', () => {
143
+ expect(isPlanPath('/Users/me/project/.plans/my-plan/context.md')).toBe(true);
144
+ });
145
+
146
+ test('just .plans/', () => {
147
+ expect(isPlanPath('.plans/foo')).toBe(true);
148
+ });
149
+
150
+ test('windows-style backslashes', () => {
151
+ expect(isPlanPath('.plans\\my-plan\\context.md')).toBe(true);
152
+ });
153
+
154
+ test('src/ path is blocked', () => {
155
+ expect(isPlanPath('src/index.ts')).toBe(false);
156
+ });
157
+
158
+ test('root file is blocked', () => {
159
+ expect(isPlanPath('README.md')).toBe(false);
160
+ });
161
+
162
+ test('package.json is blocked', () => {
163
+ expect(isPlanPath('package.json')).toBe(false);
164
+ });
165
+
166
+ test('path with plans but not .plans is blocked', () => {
167
+ expect(isPlanPath('src/plans/something.ts')).toBe(false);
168
+ });
169
+
170
+ test('path ending in .plans without slash is blocked', () => {
171
+ expect(isPlanPath('.plans')).toBe(false);
172
+ });
116
173
  });
@@ -31,7 +31,7 @@ import { enterPlanMode, exitPlanMode, switchModel } from './phase-transitions.js
31
31
  import { resumePlan, executeInNewSession } from './resume.js';
32
32
  import { registerSubmitPlanTool } from './tools/submit-plan.js';
33
33
  import { registerUpdateTaskTool } from './tools/update-task.js';
34
- import { isSafeCommand } from './utils.js';
34
+ import { isSafeCommand, isPlanPath } from './utils.js';
35
35
 
36
36
  export default function planMode(pi: ExtensionAPI): void {
37
37
  const state = new PlanModeState();
@@ -128,9 +128,11 @@ export default function planMode(pi: ExtensionAPI): void {
128
128
  },
129
129
  });
130
130
 
131
- // ── Event: block destructive bash in plan mode ────────────────────────────
131
+ // ── Event: block destructive bash + restrict writes in plan mode ──────────
132
132
  pi.on('tool_call', async (event) => {
133
133
  if (!state.planEnabled) return;
134
+
135
+ // Block destructive bash commands
134
136
  if (event.toolName === 'bash') {
135
137
  const command = event.input.command as string;
136
138
  if (!isSafeCommand(command)) {
@@ -140,6 +142,17 @@ export default function planMode(pi: ExtensionAPI): void {
140
142
  };
141
143
  }
142
144
  }
145
+
146
+ // Restrict write to .plans/ directory only
147
+ if (event.toolName === 'write' || event.toolName === 'edit') {
148
+ const path = event.input.path as string;
149
+ if (!isPlanPath(path)) {
150
+ return {
151
+ block: true,
152
+ reason: `Plan mode: writes are restricted to .plans/ directory only.\nPath: ${path}`,
153
+ };
154
+ }
155
+ }
143
156
  });
144
157
 
145
158
  // ── Event: filter context ─────────────────────────────────────────────────
@@ -11,7 +11,8 @@ You are in conversational plan mode — a planning dialogue with strict bash res
11
11
 
12
12
  Restrictions:
13
13
  - Available tools: ${PLAN_TOOLS.join(', ')}
14
- - Bash is restricted to read-only commands (ls, grep, git status, etc.)
14
+ - Bash is restricted to read-only commands (ls, grep, git status, etc.) and info commands (--help, -h, --version, man)
15
+ - The write tool is restricted to .plans/ directory only — no codebase file creation or modification
15
16
  - Do NOT make product code changes during planning.
16
17
 
17
18
  Your job is to reach shared understanding before formalizing a plan:
@@ -23,6 +23,16 @@ function isMkdirPlans(command: string): boolean {
23
23
  return /^\s*mkdir\s+(-p\s+)?\.plans(\/|\\|\s|$)/.test(command);
24
24
  }
25
25
 
26
+ /**
27
+ * Check if a file path is inside the .plans/ directory.
28
+ *
29
+ * Accepts both relative (.plans/foo) and absolute paths containing .plans/.
30
+ */
31
+ export function isPlanPath(filePath: string): boolean {
32
+ const normalized = filePath.replace(/\\/g, '/');
33
+ return /(?:^|\/)?\.plans\//.test(normalized);
34
+ }
35
+
26
36
  // ── Plan name utilities ─────────────────────────────────────────────────────
27
37
 
28
38
  export function toKebabCase(name: string): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-plan-mode",
3
- "version": "0.14.5",
3
+ "version": "0.15.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"
@@ -40,7 +40,7 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
- "@dreki-gg/pi-command-sandbox": "^0.2.0",
43
+ "@dreki-gg/pi-command-sandbox": "^0.3.0",
44
44
  "pug": "^3.0.3"
45
45
  },
46
46
  "devDependencies": {