@tianhai/pi-workflow-kit 0.7.0 → 0.8.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/README.md +2 -2
- package/docs/plans/completed/2026-04-15-bash-guard-design.md +39 -0
- package/docs/plans/completed/2026-04-15-bash-guard-implementation.md +229 -0
- package/extensions/workflow-guard.ts +126 -8
- package/package.json +1 -1
- package/docs/plans/2026-04-11-finalizing-merge-options-design.md +0 -33
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ brainstorm → plan → execute → finalize
|
|
|
13
13
|
**1 extension** that enforces the rules:
|
|
14
14
|
|
|
15
15
|
- During brainstorming and planning, `write` and `edit` are **hard-blocked** outside `docs/plans/`. The agent can only read code and discuss the design with you — it literally cannot modify source files.
|
|
16
|
-
- `bash`
|
|
16
|
+
- `bash` is **restricted to read-only commands** — file writes, installs, git mutations, and editors are blocked. Safe commands like `grep`, `find`, `git status`, `cat`, `curl` remain available.
|
|
17
17
|
|
|
18
18
|
No configuration required. Skills and extensions activate automatically after install.
|
|
19
19
|
|
|
@@ -34,7 +34,7 @@ You control each phase explicitly by invoking the skill:
|
|
|
34
34
|
| **Execute** | `/skill:executing-tasks` | Implement the plan task-by-task with TDD discipline and optional checkpoint review gates |
|
|
35
35
|
| **Finalize** | `/skill:finalizing` | Archive plan docs, update README/CHANGELOG, create PR |
|
|
36
36
|
|
|
37
|
-
During brainstorm and plan, the extension blocks `write`/`edit` outside `docs/plans
|
|
37
|
+
During brainstorm and plan, the extension blocks `write`/`edit` outside `docs/plans/` and restricts `bash` to read-only commands. During execute and finalize, all tools are available.
|
|
38
38
|
|
|
39
39
|
### Skills
|
|
40
40
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Bash Guard for Workflow Guard Extension
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
`workflow-guard.ts` blocks `write`/`edit` outside `docs/plans/` during brainstorm and plan phases, but ignores `bash` entirely. The LLM can mutate files via bash (e.g. `echo "..." > file.ts`, `sed -i`, `tee`, heredocs).
|
|
6
|
+
|
|
7
|
+
## Design
|
|
8
|
+
|
|
9
|
+
Adopt the `isSafeCommand()` approach from [pi-mono plan-mode example](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/extensions/plan-mode/utils.ts).
|
|
10
|
+
|
|
11
|
+
### Approach
|
|
12
|
+
|
|
13
|
+
**No changes to `pi.setActiveTools()`** — `write`/`edit` remain active so the existing path-based `tool_call` guard continues to allow writes to `docs/plans/`.
|
|
14
|
+
|
|
15
|
+
**Add a bash guard** in the existing `tool_call` handler using `isSafeCommand()`:
|
|
16
|
+
- Copy SAFE_PATTERNS and DESTRUCTIVE_PATTERNS from the example's `utils.ts` as-is
|
|
17
|
+
- Copy `isSafeCommand()` logic as-is
|
|
18
|
+
- In the `tool_call` handler, when phase is set and tool is `bash`, check `isSafeCommand(event.input.command)`
|
|
19
|
+
- Block with reason if not safe
|
|
20
|
+
- Notify via `ctx.ui.notify()` when blocking
|
|
21
|
+
|
|
22
|
+
### Changes
|
|
23
|
+
|
|
24
|
+
1. **`extensions/workflow-guard.ts`**
|
|
25
|
+
- Add `isSafeCommand()` function (copied from example, with DESTRUCTIVE_PATTERNS and SAFE_PATTERNS)
|
|
26
|
+
- Extend `tool_call` handler to also intercept `bash` when phase is set
|
|
27
|
+
- Check `event.input.command` against `isSafeCommand()`
|
|
28
|
+
- Block and notify if unsafe
|
|
29
|
+
|
|
30
|
+
2. **`tests/workflow-guard.test.ts`**
|
|
31
|
+
- Add tests for bash guard: safe commands pass, destructive commands blocked, redirects blocked
|
|
32
|
+
- Import and test `isSafeCommand` directly (it's a pure function, easy to unit test)
|
|
33
|
+
|
|
34
|
+
### What stays the same
|
|
35
|
+
|
|
36
|
+
- Phase detection via `/skill:` commands (no change)
|
|
37
|
+
- `write`/`edit` path-based blocking for `docs/plans/` (no change)
|
|
38
|
+
- `docs/plans/` write exception (no change)
|
|
39
|
+
- No `pi.setActiveTools()` usage
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Bash Guard Implementation Plan
|
|
2
|
+
|
|
3
|
+
Based on `docs/plans/2026-04-15-bash-guard-design.md`.
|
|
4
|
+
|
|
5
|
+
## Task 1 — Add `isSafeCommand()` to workflow-guard.ts [new feature, checkpoint: test]
|
|
6
|
+
|
|
7
|
+
**File:** `extensions/workflow-guard.ts`
|
|
8
|
+
|
|
9
|
+
- Add `isSafeCommand` as a named export (so tests can import it directly)
|
|
10
|
+
- Copy `DESTRUCTIVE_PATTERNS` and `SAFE_PATTERNS` from [plan-mode utils.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/extensions/plan-mode/utils.ts) as-is
|
|
11
|
+
- Copy the `isSafeCommand()` logic as-is
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// Destructive commands blocked in brainstorm/plan phases
|
|
15
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
16
|
+
/\brm\b/i,
|
|
17
|
+
/\brmdir\b/i,
|
|
18
|
+
/\bmv\b/i,
|
|
19
|
+
/\bcp\b/i,
|
|
20
|
+
/\bmkdir\b/i,
|
|
21
|
+
/\btouch\b/i,
|
|
22
|
+
/\bchmod\b/i,
|
|
23
|
+
/\bchown\b/i,
|
|
24
|
+
/\bchgrp\b/i,
|
|
25
|
+
/\bln\b/i,
|
|
26
|
+
/\btee\b/i,
|
|
27
|
+
/\btruncate\b/i,
|
|
28
|
+
/\bdd\b/i,
|
|
29
|
+
/\bshred\b/i,
|
|
30
|
+
/(^|[^<])>(?!>)/,
|
|
31
|
+
/>>/,
|
|
32
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
33
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
34
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
35
|
+
/\bpip\s+(install|uninstall)/i,
|
|
36
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
37
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
38
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
39
|
+
/\bsudo\b/i,
|
|
40
|
+
/\bsu\b/i,
|
|
41
|
+
/\bkill\b/i,
|
|
42
|
+
/\bpkill\b/i,
|
|
43
|
+
/\bkillall\b/i,
|
|
44
|
+
/\breboot\b/i,
|
|
45
|
+
/\bshutdown\b/i,
|
|
46
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
47
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
48
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const SAFE_PATTERNS = [
|
|
52
|
+
/^\s*cat\b/,
|
|
53
|
+
/^\s*head\b/,
|
|
54
|
+
/^\s*tail\b/,
|
|
55
|
+
/^\s*less\b/,
|
|
56
|
+
/^\s*more\b/,
|
|
57
|
+
/^\s*grep\b/,
|
|
58
|
+
/^\s*find\b/,
|
|
59
|
+
/^\s*ls\b/,
|
|
60
|
+
/^\s*pwd\b/,
|
|
61
|
+
/^\s*echo\b/,
|
|
62
|
+
/^\s*printf\b/,
|
|
63
|
+
/^\s*wc\b/,
|
|
64
|
+
/^\s*sort\b/,
|
|
65
|
+
/^\s*uniq\b/,
|
|
66
|
+
/^\s*diff\b/,
|
|
67
|
+
/^\s*file\b/,
|
|
68
|
+
/^\s*stat\b/,
|
|
69
|
+
/^\s*du\b/,
|
|
70
|
+
/^\s*df\b/,
|
|
71
|
+
/^\s*tree\b/,
|
|
72
|
+
/^\s*which\b/,
|
|
73
|
+
/^\s*whereis\b/,
|
|
74
|
+
/^\s*type\b/,
|
|
75
|
+
/^\s*env\b/,
|
|
76
|
+
/^\s*printenv\b/,
|
|
77
|
+
/^\s*uname\b/,
|
|
78
|
+
/^\s*whoami\b/,
|
|
79
|
+
/^\s*id\b/,
|
|
80
|
+
/^\s*date\b/,
|
|
81
|
+
/^\s*cal\b/,
|
|
82
|
+
/^\s*uptime\b/,
|
|
83
|
+
/^\s*ps\b/,
|
|
84
|
+
/^\s*top\b/,
|
|
85
|
+
/^\s*htop\b/,
|
|
86
|
+
/^\s*free\b/,
|
|
87
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
88
|
+
/^\s*git\s+ls-/i,
|
|
89
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
90
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
91
|
+
/^\s*node\s+--version/i,
|
|
92
|
+
/^\s*python\s+--version/i,
|
|
93
|
+
/^\s*curl\s/i,
|
|
94
|
+
/^\s*wget\s+-O\s*-/i,
|
|
95
|
+
/^\s*jq\b/,
|
|
96
|
+
/^\s*sed\s+-n/i,
|
|
97
|
+
/^\s*awk\b/,
|
|
98
|
+
/^\s*rg\b/,
|
|
99
|
+
/^\s*fd\b/,
|
|
100
|
+
/^\s*bat\b/,
|
|
101
|
+
/^\s*eza\b/,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
export function isSafeCommand(command: string): boolean {
|
|
105
|
+
const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
|
|
106
|
+
const isSafe = SAFE_PATTERNS.some((p) => p.test(command));
|
|
107
|
+
return !isDestructive && isSafe;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- Write failing tests in `tests/workflow-guard.test.ts` for `isSafeCommand`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { isSafeCommand } from "../extensions/workflow-guard";
|
|
115
|
+
|
|
116
|
+
describe("isSafeCommand", () => {
|
|
117
|
+
it("allows safe read-only commands", () => {
|
|
118
|
+
expect(isSafeCommand("cat file.ts")).toBe(true);
|
|
119
|
+
expect(isSafeCommand("grep -r 'foo' src/")).toBe(true);
|
|
120
|
+
expect(isSafeCommand("git status")).toBe(true);
|
|
121
|
+
expect(isSafeCommand("git log --oneline -5")).toBe(true);
|
|
122
|
+
expect(isSafeCommand("npm list")).toBe(true);
|
|
123
|
+
expect(isSafeCommand("ls -la")).toBe(true);
|
|
124
|
+
expect(isSafeCommand("curl https://example.com")).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("blocks destructive commands", () => {
|
|
128
|
+
expect(isSafeCommand("rm -rf node_modules")).toBe(false);
|
|
129
|
+
expect(isSafeCommand("touch newfile.ts")).toBe(false);
|
|
130
|
+
expect(isSafeCommand("mv old.ts new.ts")).toBe(false);
|
|
131
|
+
expect(isSafeCommand("mkdir src/components")).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("blocks file-writing bash patterns", () => {
|
|
135
|
+
expect(isSafeCommand("echo 'hello' > file.ts")).toBe(false);
|
|
136
|
+
expect(isSafeCommand("cat config > backup.txt")).toBe(false);
|
|
137
|
+
expect(isSafeCommand("echo 'log' >> output.log")).toBe(false);
|
|
138
|
+
expect(isSafeCommand("tee output.txt")).toBe(false);
|
|
139
|
+
expect(isSafeCommand("sed -i 's/old/new/g' file.ts")).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("blocks git mutations but allows read-only git", () => {
|
|
143
|
+
expect(isSafeCommand("git add .")).toBe(false);
|
|
144
|
+
expect(isSafeCommand("git commit -m 'msg'")).toBe(false);
|
|
145
|
+
expect(isSafeCommand("git push")).toBe(false);
|
|
146
|
+
expect(isSafeCommand("git checkout -b feature")).toBe(false);
|
|
147
|
+
expect(isSafeCommand("git status")).toBe(true);
|
|
148
|
+
expect(isSafeCommand("git log --oneline")).toBe(true);
|
|
149
|
+
expect(isSafeCommand("git diff")).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("blocks editors", () => {
|
|
153
|
+
expect(isSafeCommand("vim file.ts")).toBe(false);
|
|
154
|
+
expect(isSafeCommand("nano file.ts")).toBe(false);
|
|
155
|
+
expect(isSafeCommand("code .")).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("blocks sudo", () => {
|
|
159
|
+
expect(isSafeCommand("sudo apt install foo")).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("blocks npm installs but allows read-only npm", () => {
|
|
163
|
+
expect(isSafeCommand("npm install lodash")).toBe(false);
|
|
164
|
+
expect(isSafeCommand("npm list")).toBe(true);
|
|
165
|
+
expect(isSafeCommand("npm audit")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- Run: `npx vitest run tests/workflow-guard.test.ts`
|
|
171
|
+
- Verify: all new `isSafeCommand` tests fail (function doesn't exist yet)
|
|
172
|
+
- Implement `isSafeCommand()` in `extensions/workflow-guard.ts`
|
|
173
|
+
- Run: `npx vitest run tests/workflow-guard.test.ts`
|
|
174
|
+
- Verify: all tests pass
|
|
175
|
+
- `git add -A && git commit -m "feat: add isSafeCommand bash guard to workflow-guard"`
|
|
176
|
+
|
|
177
|
+
## Task 2 — Wire bash guard into tool_call handler [modifying tested code]
|
|
178
|
+
|
|
179
|
+
**File:** `extensions/workflow-guard.ts`
|
|
180
|
+
|
|
181
|
+
Replace the tool_call guard from:
|
|
182
|
+
```ts
|
|
183
|
+
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
To:
|
|
187
|
+
```ts
|
|
188
|
+
if (event.toolName === "bash") {
|
|
189
|
+
const command = (event.input as { command?: string }).command ?? "";
|
|
190
|
+
if (!isSafeCommand(command)) {
|
|
191
|
+
if (ctx.hasUI) {
|
|
192
|
+
ctx.ui.notify(
|
|
193
|
+
`Blocked bash command during ${phase} phase: ${command}`,
|
|
194
|
+
"warning",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
block: true,
|
|
199
|
+
reason: `⚠️ ${phase.toUpperCase()} PHASE: Bash command blocked (not allowlisted). Only read-only commands are permitted during brainstorming and planning.\nCommand: ${command}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- Run: `npx vitest run tests/workflow-guard.test.ts`
|
|
209
|
+
- Verify: existing tests pass (the "should allow bash" test needs updating — see Task 3)
|
|
210
|
+
- `git add -A && git commit -m "feat: wire bash guard into tool_call handler"`
|
|
211
|
+
|
|
212
|
+
## Task 3 — Update existing test for new bash behavior [modifying tested code]
|
|
213
|
+
|
|
214
|
+
**File:** `tests/workflow-guard.test.ts`
|
|
215
|
+
|
|
216
|
+
The test `"should allow bash regardless of phase"` is now incorrect — bash with unsafe commands should be blocked. Update it:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
it("should block unsafe bash during brainstorm (safe bash tested via isSafeCommand)", () => {
|
|
220
|
+
// The tool_call handler now guards bash via isSafeCommand.
|
|
221
|
+
// Direct testing of safe/unsafe commands is in the isSafeCommand describe block.
|
|
222
|
+
// This just confirms bash is no longer blanket-allowed.
|
|
223
|
+
expect(isSafeCommand("rm -rf /")).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- Run: `npx vitest run tests/workflow-guard.test.ts`
|
|
228
|
+
- Verify: all tests pass
|
|
229
|
+
- `git add -A && git commit -m "test: update bash guard test for new blocking behavior"`
|
|
@@ -1,15 +1,112 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Workflow Guard extension.
|
|
5
6
|
*
|
|
6
|
-
* Blocks write/edit outside docs/plans/ during brainstorm and plan phases.
|
|
7
|
+
* Blocks write/edit outside docs/plans/ and unsafe bash during brainstorm and plan phases.
|
|
7
8
|
* You control phases explicitly via /skill: commands — no auto-detection,
|
|
8
9
|
* no state persistence, no prompts.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
type Phase = "brainstorm" | "plan" | null;
|
|
12
13
|
|
|
14
|
+
// Destructive commands blocked in brainstorm/plan phases
|
|
15
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
16
|
+
/\brm\b/i,
|
|
17
|
+
/\brmdir\b/i,
|
|
18
|
+
/\bmv\b/i,
|
|
19
|
+
/\bcp\b/i,
|
|
20
|
+
/\bmkdir\b/i,
|
|
21
|
+
/\btouch\b/i,
|
|
22
|
+
/\bchmod\b/i,
|
|
23
|
+
/\bchown\b/i,
|
|
24
|
+
/\bchgrp\b/i,
|
|
25
|
+
/\bln\b/i,
|
|
26
|
+
/\btee\b/i,
|
|
27
|
+
/\btruncate\b/i,
|
|
28
|
+
/\bdd\b/i,
|
|
29
|
+
/\bshred\b/i,
|
|
30
|
+
/(^|[^<])>(?!>)/,
|
|
31
|
+
/>>/,
|
|
32
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
33
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
34
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
35
|
+
/\bpip\s+(install|uninstall)/i,
|
|
36
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
37
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
38
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
39
|
+
/\bsudo\b/i,
|
|
40
|
+
/\bsu\b/i,
|
|
41
|
+
/\bkill\b/i,
|
|
42
|
+
/\bpkill\b/i,
|
|
43
|
+
/\bkillall\b/i,
|
|
44
|
+
/\breboot\b/i,
|
|
45
|
+
/\bshutdown\b/i,
|
|
46
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
47
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
48
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const SAFE_PATTERNS = [
|
|
52
|
+
/^\s*cat\b/,
|
|
53
|
+
/^\s*head\b/,
|
|
54
|
+
/^\s*tail\b/,
|
|
55
|
+
/^\s*less\b/,
|
|
56
|
+
/^\s*more\b/,
|
|
57
|
+
/^\s*grep\b/,
|
|
58
|
+
/^\s*find\b/,
|
|
59
|
+
/^\s*ls\b/,
|
|
60
|
+
/^\s*pwd\b/,
|
|
61
|
+
/^\s*echo\b/,
|
|
62
|
+
/^\s*printf\b/,
|
|
63
|
+
/^\s*wc\b/,
|
|
64
|
+
/^\s*sort\b/,
|
|
65
|
+
/^\s*uniq\b/,
|
|
66
|
+
/^\s*diff\b/,
|
|
67
|
+
/^\s*file\b/,
|
|
68
|
+
/^\s*stat\b/,
|
|
69
|
+
/^\s*du\b/,
|
|
70
|
+
/^\s*df\b/,
|
|
71
|
+
/^\s*tree\b/,
|
|
72
|
+
/^\s*which\b/,
|
|
73
|
+
/^\s*whereis\b/,
|
|
74
|
+
/^\s*type\b/,
|
|
75
|
+
/^\s*env\b/,
|
|
76
|
+
/^\s*printenv\b/,
|
|
77
|
+
/^\s*uname\b/,
|
|
78
|
+
/^\s*whoami\b/,
|
|
79
|
+
/^\s*id\b/,
|
|
80
|
+
/^\s*date\b/,
|
|
81
|
+
/^\s*cal\b/,
|
|
82
|
+
/^\s*uptime\b/,
|
|
83
|
+
/^\s*ps\b/,
|
|
84
|
+
/^\s*top\b/,
|
|
85
|
+
/^\s*htop\b/,
|
|
86
|
+
/^\s*free\b/,
|
|
87
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
88
|
+
/^\s*git\s+ls-/i,
|
|
89
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
90
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
91
|
+
/^\s*node\s+--version/i,
|
|
92
|
+
/^\s*python\s+--version/i,
|
|
93
|
+
/^\s*curl\s/i,
|
|
94
|
+
/^\s*wget\s+-O\s*-/i,
|
|
95
|
+
/^\s*jq\b/,
|
|
96
|
+
/^\s*sed\s+-n/i,
|
|
97
|
+
/^\s*awk\b/,
|
|
98
|
+
/^\s*rg\b/,
|
|
99
|
+
/^\s*fd\b/,
|
|
100
|
+
/^\s*bat\b/,
|
|
101
|
+
/^\s*eza\b/,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
export function isSafeCommand(command: string): boolean {
|
|
105
|
+
const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
|
|
106
|
+
const isSafe = SAFE_PATTERNS.some((p) => p.test(command));
|
|
107
|
+
return !isDestructive && isSafe;
|
|
108
|
+
}
|
|
109
|
+
|
|
13
110
|
const SKILL_TO_PHASE: Record<string, Phase> = {
|
|
14
111
|
brainstorming: "brainstorm",
|
|
15
112
|
"writing-plans": "plan",
|
|
@@ -28,15 +125,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
28
125
|
|
|
29
126
|
pi.on("input", (event) => {
|
|
30
127
|
const text = event.text ?? "";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
128
|
+
const match = text.match(/^\/skill:([\w-]+)/);
|
|
129
|
+
if (match) {
|
|
130
|
+
const skill = match[1];
|
|
131
|
+
if (skill in SKILL_TO_PHASE) {
|
|
132
|
+
phase = SKILL_TO_PHASE[skill];
|
|
34
133
|
return;
|
|
35
134
|
}
|
|
36
135
|
}
|
|
37
136
|
if (
|
|
38
|
-
text.
|
|
39
|
-
text.
|
|
137
|
+
text.startsWith("/skill:executing-tasks") ||
|
|
138
|
+
text.startsWith("/skill:finalizing")
|
|
40
139
|
) {
|
|
41
140
|
phase = null;
|
|
42
141
|
}
|
|
@@ -45,12 +144,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
45
144
|
pi.on("tool_call", (event, ctx) => {
|
|
46
145
|
if (!phase) return;
|
|
47
146
|
|
|
147
|
+
if (event.toolName === "bash") {
|
|
148
|
+
const command = (event.input as { command?: string }).command ?? "";
|
|
149
|
+
if (!isSafeCommand(command)) {
|
|
150
|
+
if (ctx.hasUI) {
|
|
151
|
+
ctx.ui.notify(
|
|
152
|
+
`Blocked bash command during ${phase} phase: ${command}`,
|
|
153
|
+
"warning",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
block: true,
|
|
158
|
+
reason: `⚠️ ${phase.toUpperCase()} PHASE: Bash command blocked (not allowlisted). Only read-only commands are permitted during brainstorming and planning.\nCommand: ${command}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
48
164
|
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
49
165
|
|
|
50
166
|
const filePath = (event.input as { path?: string }).path ?? "";
|
|
51
167
|
if (!filePath) return;
|
|
52
168
|
|
|
53
|
-
|
|
169
|
+
const absolute = resolve(ctx.cwd, filePath);
|
|
170
|
+
const plansDir = resolve(ctx.cwd, "docs/plans");
|
|
171
|
+
if (absolute.startsWith(plansDir + "/")) return;
|
|
54
172
|
|
|
55
173
|
if (ctx.hasUI) {
|
|
56
174
|
ctx.ui.notify(
|
|
@@ -60,7 +178,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
60
178
|
}
|
|
61
179
|
|
|
62
180
|
return {
|
|
63
|
-
|
|
181
|
+
block: true,
|
|
64
182
|
reason: `⚠️ ${phase.toUpperCase()} PHASE: Cannot ${event.toolName} to ${filePath}. Only docs/plans/ is writable during brainstorming and planning.`,
|
|
65
183
|
};
|
|
66
184
|
});
|
package/package.json
CHANGED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Finalizing: Merge Strategy Options
|
|
2
|
-
|
|
3
|
-
## Problem
|
|
4
|
-
|
|
5
|
-
The finalizing skill hard-codes "Create PR" as the only shipping option. In practice, small features often don't need a PR — they can be merged directly back to the parent branch.
|
|
6
|
-
|
|
7
|
-
## Design
|
|
8
|
-
|
|
9
|
-
Add a merge strategy step after updating documentation. The human chooses one of four options:
|
|
10
|
-
|
|
11
|
-
1. **Create PR** — push and open a PR for external review via `gh pr create`
|
|
12
|
-
2. **Rebase & merge** (recommended) — rebase onto parent, fast-forward merge, push parent, delete feature branch. Preserves per-task commit history linearly.
|
|
13
|
-
3. **Squash & merge** — squash all commits into one on parent, push parent, delete feature branch. Clean single-commit history.
|
|
14
|
-
4. **Merge commit** — merge with `--no-ff`, push parent, delete feature branch. Preserves all commits and branch topology.
|
|
15
|
-
|
|
16
|
-
### Flow for options 2–4 (local merge)
|
|
17
|
-
|
|
18
|
-
1. Detect parent branch (compare `main` vs `master`, fall back to `git show-branch`)
|
|
19
|
-
2. Switch to parent branch and pull latest
|
|
20
|
-
3. Execute the chosen merge strategy:
|
|
21
|
-
- Rebase: `git rebase <parent>` on feature branch, then `git merge --ff-only <feature>` on parent
|
|
22
|
-
- Squash: `git merge --squash <feature>` on parent, then `git commit`
|
|
23
|
-
- Merge commit: `git merge --no-ff <feature>` on parent
|
|
24
|
-
4. Push parent to origin
|
|
25
|
-
5. Delete feature branch locally and remotely
|
|
26
|
-
|
|
27
|
-
### Prompting
|
|
28
|
-
|
|
29
|
-
The skill should ask the human which option they prefer, presenting rebase & merge as the default recommendation.
|
|
30
|
-
|
|
31
|
-
## Changes
|
|
32
|
-
|
|
33
|
-
- Update `skills/finalizing/SKILL.md` to replace the hard-coded PR step with the 4-option choice.
|