jettypod 4.4.53 → 4.4.55
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/claude-hooks/global-guardrails.js +281 -0
- package/jettypod.js +20 -3
- package/package.json +1 -1
- package/skills-templates/feature-planning/SKILL.md +11 -4
- package/skills-templates/speed-mode/SKILL.md +8 -7
- package/skills-templates/stable-mode/SKILL.md +4 -4
- package/docs/command-whitelist-feature-planning.md +0 -120
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Global Guardrails Hook
|
|
4
|
+
*
|
|
5
|
+
* Enforces actions that are ALWAYS allowed and ALWAYS blocked regardless of
|
|
6
|
+
* skill or context. Runs before any contextual/skill-specific validators.
|
|
7
|
+
*
|
|
8
|
+
* Claude Code PreToolUse Hook
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Read hook input from stdin
|
|
15
|
+
let input = '';
|
|
16
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
17
|
+
process.stdin.on('end', () => {
|
|
18
|
+
try {
|
|
19
|
+
const hookInput = JSON.parse(input);
|
|
20
|
+
const result = evaluateRequest(hookInput);
|
|
21
|
+
|
|
22
|
+
if (result.allowed) {
|
|
23
|
+
allow();
|
|
24
|
+
} else {
|
|
25
|
+
deny(result.message, result.hint);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// On parse error, allow (fail open)
|
|
29
|
+
console.error('Hook error:', err.message);
|
|
30
|
+
allow();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate the request and return allow/deny decision
|
|
36
|
+
*/
|
|
37
|
+
function evaluateRequest(hookInput) {
|
|
38
|
+
const { tool_name, tool_input, cwd, active_worktree_path, current_branch } = hookInput;
|
|
39
|
+
|
|
40
|
+
// ALWAYS ALLOW: Read operations
|
|
41
|
+
if (tool_name === 'Read' || tool_name === 'Glob' || tool_name === 'Grep') {
|
|
42
|
+
return { allowed: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle Bash commands
|
|
46
|
+
if (tool_name === 'Bash') {
|
|
47
|
+
return evaluateBashCommand(tool_input.command || '', current_branch, cwd);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle Write/Edit operations
|
|
51
|
+
if (tool_name === 'Write' || tool_name === 'Edit') {
|
|
52
|
+
return evaluateWriteOperation(tool_input.file_path || '', active_worktree_path, cwd);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default: allow unknown tools
|
|
56
|
+
return { allowed: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate Bash command against global rules
|
|
61
|
+
*/
|
|
62
|
+
function evaluateBashCommand(command, currentBranch, cwd) {
|
|
63
|
+
// BLOCKED: Force push
|
|
64
|
+
if (/git\s+push\s+.*--force/.test(command) || /git\s+push\s+-f\b/.test(command)) {
|
|
65
|
+
return {
|
|
66
|
+
allowed: false,
|
|
67
|
+
message: 'Force push is blocked',
|
|
68
|
+
hint: 'Force pushing can destroy history. Use regular push or create a PR.'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// BLOCKED: Direct commit to main
|
|
73
|
+
if (/git\s+commit\b/.test(command) && currentBranch === 'main') {
|
|
74
|
+
return {
|
|
75
|
+
allowed: false,
|
|
76
|
+
message: 'Direct commits to main are blocked',
|
|
77
|
+
hint: 'Use jettypod work start to create a feature branch first.'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// BLOCKED: Manual worktree creation
|
|
82
|
+
if (/git\s+worktree\s+add\b/.test(command)) {
|
|
83
|
+
return {
|
|
84
|
+
allowed: false,
|
|
85
|
+
message: 'Manual worktree creation is blocked',
|
|
86
|
+
hint: 'Use jettypod work start to create worktrees.'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// BLOCKED: Manual branch creation
|
|
91
|
+
if (/git\s+checkout\s+-b\b/.test(command) || /git\s+branch\s+(?!-d|-D)/.test(command)) {
|
|
92
|
+
return {
|
|
93
|
+
allowed: false,
|
|
94
|
+
message: 'Manual branch creation is blocked',
|
|
95
|
+
hint: 'Use jettypod work start to create branches.'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// BLOCKED: Direct SQL mutation to work.db
|
|
100
|
+
if (/sqlite3\s+.*work\.db/.test(command)) {
|
|
101
|
+
const sqlCommand = command.toLowerCase();
|
|
102
|
+
// Allow SELECT, block mutations
|
|
103
|
+
if (/\b(update|insert|delete|drop|alter|create)\b/i.test(sqlCommand)) {
|
|
104
|
+
return {
|
|
105
|
+
allowed: false,
|
|
106
|
+
message: 'Direct database mutations are blocked',
|
|
107
|
+
hint: 'Use jettypod CLI commands to modify work items.'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// BLOCKED: Merge from inside worktree (causes CWD corruption)
|
|
113
|
+
if (/jettypod\s+(work|tests)\s+merge/.test(command)) {
|
|
114
|
+
if (cwd && /\.jettypod-work\//.test(cwd)) {
|
|
115
|
+
return {
|
|
116
|
+
allowed: false,
|
|
117
|
+
message: 'Cannot merge from inside a worktree. CWD would be deleted.',
|
|
118
|
+
hint: 'Run: cd /Users/erikspangenberg/jettypod-source && jettypod work merge <id>'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ALLOWED: Git read-only commands
|
|
124
|
+
if (/git\s+(status|log|diff|show|branch\s*$|remote|fetch)\b/.test(command)) {
|
|
125
|
+
return { allowed: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ALLOWED: Jettypod read-only commands
|
|
129
|
+
if (/jettypod\s+(backlog|status|work\s+status|impact)\b/.test(command)) {
|
|
130
|
+
return { allowed: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Default: allow other commands
|
|
134
|
+
return { allowed: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Find the jettypod database path
|
|
139
|
+
* @param {string} cwd - Starting directory
|
|
140
|
+
* @returns {string|null} Database path or null
|
|
141
|
+
*/
|
|
142
|
+
function findDatabasePath(cwd) {
|
|
143
|
+
let dir = cwd;
|
|
144
|
+
while (dir !== path.dirname(dir)) {
|
|
145
|
+
const dbPath = path.join(dir, '.jettypod', 'work.db');
|
|
146
|
+
if (fs.existsSync(dbPath)) {
|
|
147
|
+
return dbPath;
|
|
148
|
+
}
|
|
149
|
+
dir = path.dirname(dir);
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the active worktree path from database
|
|
156
|
+
* @param {string} cwd - Current working directory
|
|
157
|
+
* @returns {string|null} Active worktree path or null
|
|
158
|
+
*/
|
|
159
|
+
function getActiveWorktreePathFromDB(cwd) {
|
|
160
|
+
const dbPath = findDatabasePath(cwd);
|
|
161
|
+
if (!dbPath) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// Try better-sqlite3 first
|
|
167
|
+
const sqlite3 = require('better-sqlite3');
|
|
168
|
+
const db = sqlite3(dbPath, { readonly: true });
|
|
169
|
+
const row = db.prepare(
|
|
170
|
+
`SELECT worktree_path FROM worktrees WHERE status = 'active' LIMIT 1`
|
|
171
|
+
).get();
|
|
172
|
+
db.close();
|
|
173
|
+
return row ? row.worktree_path : null;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// Fall back to CLI
|
|
176
|
+
const { spawnSync } = require('child_process');
|
|
177
|
+
const result = spawnSync('sqlite3', [
|
|
178
|
+
dbPath,
|
|
179
|
+
`SELECT worktree_path FROM worktrees WHERE status = 'active' LIMIT 1`
|
|
180
|
+
], { encoding: 'utf-8' });
|
|
181
|
+
|
|
182
|
+
if (result.error || result.status !== 0) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return result.stdout.trim() || null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Evaluate Write/Edit operation against global rules
|
|
191
|
+
*/
|
|
192
|
+
function evaluateWriteOperation(filePath, inputWorktreePath, cwd) {
|
|
193
|
+
// Query database for active worktree, fall back to input if provided
|
|
194
|
+
const activeWorktreePath = getActiveWorktreePathFromDB(cwd) || inputWorktreePath;
|
|
195
|
+
|
|
196
|
+
// Normalize paths
|
|
197
|
+
const normalizedPath = path.resolve(cwd || '.', filePath);
|
|
198
|
+
const normalizedWorktree = activeWorktreePath ? path.resolve(cwd || '.', activeWorktreePath) : null;
|
|
199
|
+
|
|
200
|
+
// BLOCKED: Protected files (skills, hooks)
|
|
201
|
+
const protectedPatterns = [
|
|
202
|
+
/\.claude\/skills\//i,
|
|
203
|
+
/claude-hooks\//i,
|
|
204
|
+
/\.jettypod\/hooks\//i
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
for (const pattern of protectedPatterns) {
|
|
208
|
+
if (pattern.test(normalizedPath) || pattern.test(filePath)) {
|
|
209
|
+
return {
|
|
210
|
+
allowed: false,
|
|
211
|
+
message: 'Protected file - cannot modify',
|
|
212
|
+
hint: 'Skill and hook files are protected. Modify them through proper channels.'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if path is in a worktree
|
|
218
|
+
const isInWorktree = /\.jettypod-work\//.test(filePath) || /\.jettypod-work\//.test(normalizedPath);
|
|
219
|
+
|
|
220
|
+
if (isInWorktree) {
|
|
221
|
+
// If we have an active worktree, check if this write is to it
|
|
222
|
+
if (activeWorktreePath) {
|
|
223
|
+
// SECURITY: Only use normalized path comparison to prevent traversal attacks
|
|
224
|
+
// The filePath.includes() check was vulnerable to "../" escapes
|
|
225
|
+
const isActiveWorktree = normalizedPath.startsWith(normalizedWorktree + path.sep) ||
|
|
226
|
+
normalizedPath === normalizedWorktree;
|
|
227
|
+
|
|
228
|
+
if (isActiveWorktree) {
|
|
229
|
+
return { allowed: true };
|
|
230
|
+
} else {
|
|
231
|
+
return {
|
|
232
|
+
allowed: false,
|
|
233
|
+
message: 'Not your active worktree',
|
|
234
|
+
hint: 'You can only write to your currently active worktree.'
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// No active worktree tracked but path looks like worktree - block
|
|
239
|
+
return {
|
|
240
|
+
allowed: false,
|
|
241
|
+
message: 'Not your active worktree',
|
|
242
|
+
hint: 'You can only write to your currently active worktree.'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// BLOCKED: Write to main repo (not in worktree)
|
|
247
|
+
return {
|
|
248
|
+
allowed: false,
|
|
249
|
+
message: 'Cannot write to main repository',
|
|
250
|
+
hint: 'Use jettypod work start to create a worktree first.'
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Allow the action
|
|
256
|
+
*/
|
|
257
|
+
function allow() {
|
|
258
|
+
console.log(JSON.stringify({
|
|
259
|
+
hookSpecificOutput: {
|
|
260
|
+
hookEventName: "PreToolUse",
|
|
261
|
+
permissionDecision: "allow"
|
|
262
|
+
}
|
|
263
|
+
}));
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Deny the action with explanation
|
|
269
|
+
*/
|
|
270
|
+
function deny(message, hint) {
|
|
271
|
+
const reason = `❌ ${message}\n\n💡 Hint: ${hint || 'Check your action.'}`;
|
|
272
|
+
|
|
273
|
+
console.log(JSON.stringify({
|
|
274
|
+
hookSpecificOutput: {
|
|
275
|
+
hookEventName: "PreToolUse",
|
|
276
|
+
permissionDecision: "deny",
|
|
277
|
+
permissionDecisionReason: reason
|
|
278
|
+
}
|
|
279
|
+
}));
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
package/jettypod.js
CHANGED
|
@@ -785,6 +785,14 @@ async function initializeProject() {
|
|
|
785
785
|
fs.chmodSync(enforceHookDest, 0o755);
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
// Install global-guardrails hook
|
|
789
|
+
const guardrailsHookSource = path.join(__dirname, 'claude-hooks', 'global-guardrails.js');
|
|
790
|
+
const guardrailsHookDest = path.join('.jettypod', 'hooks', 'global-guardrails.js');
|
|
791
|
+
if (fs.existsSync(guardrailsHookSource)) {
|
|
792
|
+
fs.copyFileSync(guardrailsHookSource, guardrailsHookDest);
|
|
793
|
+
fs.chmodSync(guardrailsHookDest, 0o755);
|
|
794
|
+
}
|
|
795
|
+
|
|
788
796
|
// Create Claude Code settings
|
|
789
797
|
if (!fs.existsSync('.claude')) {
|
|
790
798
|
fs.mkdirSync('.claude', { recursive: true });
|
|
@@ -800,15 +808,24 @@ async function initializeProject() {
|
|
|
800
808
|
PreToolUse: [
|
|
801
809
|
{
|
|
802
810
|
matcher: 'Edit',
|
|
803
|
-
hooks: [
|
|
811
|
+
hooks: [
|
|
812
|
+
{ type: 'command', command: '.jettypod/hooks/global-guardrails.js' },
|
|
813
|
+
{ type: 'command', command: '.jettypod/hooks/protect-claude-md.js' }
|
|
814
|
+
]
|
|
804
815
|
},
|
|
805
816
|
{
|
|
806
817
|
matcher: 'Write',
|
|
807
|
-
hooks: [
|
|
818
|
+
hooks: [
|
|
819
|
+
{ type: 'command', command: '.jettypod/hooks/global-guardrails.js' },
|
|
820
|
+
{ type: 'command', command: '.jettypod/hooks/protect-claude-md.js' }
|
|
821
|
+
]
|
|
808
822
|
},
|
|
809
823
|
{
|
|
810
824
|
matcher: 'Bash',
|
|
811
|
-
hooks: [
|
|
825
|
+
hooks: [
|
|
826
|
+
{ type: 'command', command: '.jettypod/hooks/global-guardrails.js' },
|
|
827
|
+
{ type: 'command', command: '.jettypod/hooks/enforce-skill-activation.js' }
|
|
828
|
+
]
|
|
812
829
|
}
|
|
813
830
|
]
|
|
814
831
|
}
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ When this skill is activated, you are helping discover the best approach for a f
|
|
|
19
19
|
|---------|----------|------|-------|
|
|
20
20
|
| `work implement <feature-id>` | Transition feature to implementation phase | After chores created (Step 8C) | Feature Planning |
|
|
21
21
|
| `work tests start <feature-id>` | Create worktree for test authoring | After transition (Step 8D) | Feature Planning |
|
|
22
|
-
| `work tests merge <feature-id>` | Merge tests to main, cleanup worktree | After tests validated (Step 8D) | Feature Planning |
|
|
22
|
+
| `cd <main-repo> && work tests merge <feature-id>` | Merge tests to main, cleanup worktree | After tests validated (Step 8D) | Feature Planning |
|
|
23
23
|
| `work start <chore-id>` | Start implementing a specific chore | After tests merged (Step 8E) | Speed Mode |
|
|
24
24
|
|
|
25
25
|
**CRITICAL:** All commands are run by **Claude**, not the user. The distinction is:
|
|
@@ -653,7 +653,8 @@ Write your BDD files to:
|
|
|
653
653
|
<worktree>/features/email-login.feature
|
|
654
654
|
<worktree>/features/step_definitions/email-login.steps.js
|
|
655
655
|
|
|
656
|
-
When done,
|
|
656
|
+
When done, cd to main repo and merge:
|
|
657
|
+
cd /path/to/main/repo && jettypod work tests merge 42
|
|
657
658
|
```
|
|
658
659
|
|
|
659
660
|
**🛑 STOP AND CHECK:** Verify worktree was created successfully. If you see an error, investigate before continuing.
|
|
@@ -735,10 +736,16 @@ sqlite3 .jettypod/work.db "UPDATE work_items SET scenario_file = 'features/${FEA
|
|
|
735
736
|
|
|
736
737
|
**Sub-step 5: Merge tests to main**
|
|
737
738
|
|
|
739
|
+
**⚠️ CRITICAL: The merge will delete the worktree.** Your shell is currently inside it. Chain commands to ensure shell is in main repo BEFORE deletion:
|
|
740
|
+
|
|
738
741
|
```bash
|
|
739
|
-
|
|
742
|
+
# CRITICAL: cd to main repo AND merge in SAME command
|
|
743
|
+
# This ensures shell is in main repo BEFORE worktree deletion
|
|
744
|
+
cd <main-repo-path> && jettypod work tests merge ${FEATURE_ID}
|
|
740
745
|
```
|
|
741
746
|
|
|
747
|
+
If you run the merge while your shell is in the worktree, subsequent commands will fail with "No such file or directory".
|
|
748
|
+
|
|
742
749
|
This will:
|
|
743
750
|
- Commit changes in the worktree
|
|
744
751
|
- Merge to main
|
|
@@ -913,6 +920,6 @@ Before completing feature planning, ensure:
|
|
|
913
920
|
- [ ] **Test worktree created** with `work tests start` (Step 8D)
|
|
914
921
|
- [ ] **BDD files written** in worktree (Step 8D)
|
|
915
922
|
- [ ] **BDD infrastructure validated** with dry-run (Step 8D)
|
|
916
|
-
- [ ] **Tests merged to main** with `work tests merge` (Step 8D)
|
|
923
|
+
- [ ] **Tests merged to main** with `cd <main-repo> && work tests merge` (Step 8D)
|
|
917
924
|
- [ ] First chore started with `work start [chore-id]` (Step 8E)
|
|
918
925
|
- [ ] **Speed-mode skill invoked using Skill tool** (Step 8E)
|
|
@@ -827,7 +827,7 @@ Repeat for each confirmed chore. **Do NOT use `--mode` flag** - chores inherit m
|
|
|
827
827
|
**3. Release merge lock:**
|
|
828
828
|
|
|
829
829
|
```bash
|
|
830
|
-
jettypod work merge --release-lock
|
|
830
|
+
cd <main-repo> && jettypod work merge --release-lock
|
|
831
831
|
```
|
|
832
832
|
|
|
833
833
|
**🔄 WORKFLOW COMPLETE: Speed mode finished**
|
|
@@ -926,12 +926,12 @@ Before ending speed-mode skill, ensure:
|
|
|
926
926
|
|
|
927
927
|
## Command Reference
|
|
928
928
|
|
|
929
|
-
**Complete chores (
|
|
929
|
+
**Complete chores (CRITICAL: cd to main repo BEFORE merge - worktree will be deleted):**
|
|
930
930
|
```bash
|
|
931
|
-
jettypod work merge <id>
|
|
932
|
-
jettypod work merge
|
|
933
|
-
jettypod work merge --with-transition # Hold lock for transition
|
|
934
|
-
jettypod work merge --release-lock # Release held lock
|
|
931
|
+
cd <main-repo> && jettypod work merge <id> # Merge chore by ID (recommended)
|
|
932
|
+
cd <main-repo> && jettypod work merge # Merge current chore
|
|
933
|
+
cd <main-repo> && jettypod work merge --with-transition # Hold lock for transition
|
|
934
|
+
cd <main-repo> && jettypod work merge --release-lock # Release held lock
|
|
935
935
|
```
|
|
936
936
|
|
|
937
937
|
**Start chores:**
|
|
@@ -942,7 +942,8 @@ jettypod work start <chore-id> # Create worktree and start chore
|
|
|
942
942
|
**Create test worktree (for writing BDD scenarios):**
|
|
943
943
|
```bash
|
|
944
944
|
jettypod work tests <feature-id> # Create worktree for writing tests
|
|
945
|
-
|
|
945
|
+
# CRITICAL: cd to main repo BEFORE merge (worktree will be deleted)
|
|
946
|
+
cd <main-repo> && jettypod work tests merge <feature-id>
|
|
946
947
|
```
|
|
947
948
|
|
|
948
949
|
**Set feature mode (BEFORE creating chores):**
|
|
@@ -743,10 +743,10 @@ Before ending stable-mode skill, ensure:
|
|
|
743
743
|
|
|
744
744
|
## Command Reference
|
|
745
745
|
|
|
746
|
-
**Complete chores (
|
|
746
|
+
**Complete chores (CRITICAL: cd to main repo BEFORE merge - worktree will be deleted):**
|
|
747
747
|
```bash
|
|
748
|
-
jettypod work merge <id>
|
|
749
|
-
jettypod work merge
|
|
748
|
+
cd <main-repo> && jettypod work merge <id> # Merge chore by ID (recommended)
|
|
749
|
+
cd <main-repo> && jettypod work merge # Merge current chore
|
|
750
750
|
```
|
|
751
751
|
|
|
752
752
|
**Start chores:**
|
|
@@ -776,7 +776,7 @@ jettypod work set-mode <feature-id> production # Set feature to production mode
|
|
|
776
776
|
- Parallel worktrees branch from main independently
|
|
777
777
|
|
|
778
778
|
**Process:**
|
|
779
|
-
1. Complete chore → `jettypod work merge`
|
|
779
|
+
1. Complete chore → `cd <main-repo> && jettypod work merge` (CRITICAL: cd first!)
|
|
780
780
|
2. Start next chore → `jettypod work start <next-id>`
|
|
781
781
|
3. Repeat
|
|
782
782
|
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# Command Whitelist Matrix: Feature Planning
|
|
2
|
-
|
|
3
|
-
This document defines what commands/actions are allowed at each step of the feature-planning skill. Hooks should enforce these rules and redirect the agent when violations occur.
|
|
4
|
-
|
|
5
|
-
## Matrix
|
|
6
|
-
|
|
7
|
-
| Step | Description | Allowed Commands | Blocked Commands | Redirect Message |
|
|
8
|
-
|------|-------------|------------------|------------------|------------------|
|
|
9
|
-
| **1** | Get feature context | `work show`, `workflow start`, `backlog` | Any file writes, `work create`, `work start`, `work implement` | "You're in Step 1 - get context first with `work show <id>`" |
|
|
10
|
-
| **2** | Check epic decisions | `decisions --epic=X` | File writes, `work create`, `work start` | "Check for epic decisions before suggesting approaches" |
|
|
11
|
-
| **3** | Suggest 3 UX approaches | Read-only (Read, Glob, Grep) | File writes, `work create`, `work start` | "Wait for user to pick an approach - no changes yet" |
|
|
12
|
-
| **4** | Optional prototyping | Write to `prototypes/` only | Writes to `features/`, `src/`, `work create`, `work start` | "Prototypes go in prototypes/ - no production code yet" |
|
|
13
|
-
| **5** | Choose winner | `workflow checkpoint` | File writes, `work create`, `work start` | "Wait for user to confirm winner" |
|
|
14
|
-
| **6A** | Define integration contract | Read-only | File writes | "Define how users reach the feature first" |
|
|
15
|
-
| **6B** | Propose BDD scenarios | Read-only, `workflow checkpoint` | **Write to `features/`**, `work create`, `work start` | "**BDD files are written in Step 8D in a worktree, not now**" |
|
|
16
|
-
| **7** | Propose chores | Read-only (analyze codebase) | `work create`, `work start`, file writes | "Propose chores to user first - don't create yet" |
|
|
17
|
-
| **8B** | Create chores | `work create chore` | `work start`, writes to `features/` | "Create all chores before transitioning" |
|
|
18
|
-
| **8C** | Execute transition | `work implement`, `workflow checkpoint` | `work start` | "Transition the feature before starting chores" |
|
|
19
|
-
| **8D** | Write tests in worktree | `work tests start`, `work tests merge`, Write to **worktree path only**, `cucumber-js --dry-run` | Write to main repo paths, `work start` | "Write tests in the worktree, not main repo" |
|
|
20
|
-
| **8E** | Start first chore | `work start`, `workflow complete` | - | "Start the chore, then invoke speed-mode" |
|
|
21
|
-
|
|
22
|
-
## Key Enforcement Points
|
|
23
|
-
|
|
24
|
-
### 0. Allowlist-First Enforcement
|
|
25
|
-
|
|
26
|
-
**Principle:** Each step defines what IS allowed, not what's blocked. Anything not explicitly allowed is rejected.
|
|
27
|
-
|
|
28
|
-
This is simpler to reason about, safer by default, and guides the agent toward correct behavior rather than away from incorrect behavior.
|
|
29
|
-
|
|
30
|
-
**Enforcement logic:**
|
|
31
|
-
1. Check if command/action is in the step's allowlist → allow
|
|
32
|
-
2. Check if it matches a global bypass pattern (see below) → block with specific message
|
|
33
|
-
3. Otherwise → block with generic "not allowed at this step" + list what IS allowed
|
|
34
|
-
|
|
35
|
-
### 1. Common Bypass Patterns (Global Blocks)
|
|
36
|
-
|
|
37
|
-
Agents sometimes try shortcuts that bypass CLI commands entirely. Catch the common ones:
|
|
38
|
-
|
|
39
|
-
- **Direct SQL:** `sqlite3` commands, raw SQL (`INSERT`, `UPDATE`, `DELETE`) in bash
|
|
40
|
-
- **Inline Node execution:** `node -e` with database operations
|
|
41
|
-
|
|
42
|
-
**Redirect Message:** "Use CLI commands to modify work items, not direct SQL. Run `jettypod help` to see available commands."
|
|
43
|
-
|
|
44
|
-
We don't need to catch every possible bypass - these cover ~95% of cases. The redirect message does the real work.
|
|
45
|
-
|
|
46
|
-
### 2. Step 6B is the Critical Trap
|
|
47
|
-
|
|
48
|
-
The agent often tries to write `.feature` files in Step 6B after proposing scenarios. This must be blocked.
|
|
49
|
-
|
|
50
|
-
**Rule:** Block all writes to `features/**` until Step 8D, and only then to the worktree path.
|
|
51
|
-
|
|
52
|
-
### 3. Worktree Path Validation
|
|
53
|
-
|
|
54
|
-
In Step 8D, writes are only allowed to the active worktree path (`.jettypod-work/tests-*`), not anywhere in the main repo.
|
|
55
|
-
|
|
56
|
-
**Rule:** If a write targets a path that doesn't start with the active worktree path, block it.
|
|
57
|
-
|
|
58
|
-
### 4. Order Enforcement
|
|
59
|
-
|
|
60
|
-
The following commands have strict ordering:
|
|
61
|
-
1. `work create chore` - Only in Step 8B (after user confirms chores)
|
|
62
|
-
2. `work implement` - Only in Step 8C (after chores created)
|
|
63
|
-
3. `work tests start` - Only in Step 8D (after implement)
|
|
64
|
-
4. `work tests merge` - Only in Step 8D (after tests written)
|
|
65
|
-
5. `work start` - Only in Step 8E (after tests merged)
|
|
66
|
-
|
|
67
|
-
**Rule:** Each command should validate the previous step completed.
|
|
68
|
-
|
|
69
|
-
## Context Required for Enforcement
|
|
70
|
-
|
|
71
|
-
Hooks need access to:
|
|
72
|
-
- **Current skill:** `feature-planning`
|
|
73
|
-
- **Current step:** 1-8E (from workflow checkpoint)
|
|
74
|
-
- **Feature ID:** The work item being planned
|
|
75
|
-
- **Worktree path:** For Step 8D validation (from `worktrees` table)
|
|
76
|
-
|
|
77
|
-
## Example Hook Logic
|
|
78
|
-
|
|
79
|
-
```javascript
|
|
80
|
-
// Pseudocode for pre-command hook
|
|
81
|
-
function validateCommand(command, context) {
|
|
82
|
-
const { skill, step, featureId, worktreePath } = context;
|
|
83
|
-
|
|
84
|
-
if (skill !== 'feature-planning') return { allowed: true };
|
|
85
|
-
|
|
86
|
-
// Step 6B: Block writes to features/
|
|
87
|
-
if (step === '6B' && command.type === 'write' && command.path.includes('features/')) {
|
|
88
|
-
return {
|
|
89
|
-
allowed: false,
|
|
90
|
-
message: "BDD files are written in Step 8D in a worktree, not now. You're proposing scenarios - wait for user confirmation."
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Step 8D: Only allow writes to worktree
|
|
95
|
-
if (step === '8D' && command.type === 'write') {
|
|
96
|
-
if (!command.path.startsWith(worktreePath)) {
|
|
97
|
-
return {
|
|
98
|
-
allowed: false,
|
|
99
|
-
message: `Write tests in the worktree (${worktreePath}), not the main repo.`
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Block work start until Step 8E
|
|
105
|
-
if (command.name === 'work start' && step !== '8E') {
|
|
106
|
-
return {
|
|
107
|
-
allowed: false,
|
|
108
|
-
message: `Cannot start chores yet. Complete steps through 8D first (tests must be merged to main).`
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return { allowed: true };
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Open Questions
|
|
117
|
-
|
|
118
|
-
1. **Step granularity:** Should we track sub-steps like 8B, 8C, 8D, 8E separately, or group them as "Step 8"?
|
|
119
|
-
2. **Read operations:** Should we restrict what files can be read at certain steps, or only writes?
|
|
120
|
-
3. **Skill transitions:** How do we handle the handoff to speed-mode at Step 8E?
|