@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.
|
|
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.
|
|
43
|
+
"@dreki-gg/pi-command-sandbox": "^0.3.0",
|
|
44
44
|
"pug": "^3.0.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|