jettypod 4.4.62 ā 4.4.64
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/apps/dashboard/lib/db.ts +1 -1
- package/apps/ws-server/package.json +13 -0
- package/apps/ws-server/server.js +101 -0
- package/claude-hooks/chore-planning-guardrails.js +174 -0
- package/claude-hooks/epic-planning-guardrails.js +177 -0
- package/claude-hooks/external-transition-guardrails.js +169 -0
- package/claude-hooks/global-guardrails.js +61 -30
- package/claude-hooks/production-mode-guardrails.js +262 -0
- package/claude-hooks/speed-mode-guardrails.js +163 -0
- package/claude-hooks/stable-mode-guardrails.js +216 -0
- package/jettypod.js +13 -3
- package/lib/work-commands/index.js +9 -7
- package/package.json +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stable Mode Guardrails Hook
|
|
4
|
+
*
|
|
5
|
+
* Blocks merge of the last stable-mode chore until all BDD scenarios pass.
|
|
6
|
+
* This ensures error handling coverage is complete before completing stable mode.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
// Test mode: allows tests to control BDD pass/fail state
|
|
14
|
+
const TEST_MODE = process.env.STABLE_GUARDRAIL_TEST_MODE === '1';
|
|
15
|
+
const TEST_BDD_PASS = process.env.STABLE_GUARDRAIL_BDD_PASS === '1';
|
|
16
|
+
|
|
17
|
+
// Read hook input from stdin
|
|
18
|
+
let input = '';
|
|
19
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
20
|
+
process.stdin.on('end', async () => {
|
|
21
|
+
try {
|
|
22
|
+
const hookInput = JSON.parse(input);
|
|
23
|
+
const { tool_name, tool_input, cwd } = hookInput;
|
|
24
|
+
|
|
25
|
+
// Only check Bash commands
|
|
26
|
+
if (tool_name !== 'Bash') {
|
|
27
|
+
allow();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const command = tool_input.command || '';
|
|
32
|
+
|
|
33
|
+
// Only check jettypod work merge commands
|
|
34
|
+
if (!/jettypod\s+work\s+merge/.test(command)) {
|
|
35
|
+
allow();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract chore ID from command
|
|
40
|
+
const choreIdMatch = command.match(/jettypod\s+work\s+merge\s+(\d+)/);
|
|
41
|
+
if (!choreIdMatch) {
|
|
42
|
+
// No explicit ID - allow (might be merging current work)
|
|
43
|
+
allow();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const choreId = parseInt(choreIdMatch[1]);
|
|
48
|
+
const result = await checkStableModeGuardrail(choreId, cwd);
|
|
49
|
+
|
|
50
|
+
if (result.allowed) {
|
|
51
|
+
allow();
|
|
52
|
+
} else {
|
|
53
|
+
deny(result.message, result.hint);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// Fail open on errors
|
|
57
|
+
allow();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if this merge should be blocked
|
|
63
|
+
*/
|
|
64
|
+
async function checkStableModeGuardrail(choreId, cwd) {
|
|
65
|
+
const dbPath = findDatabasePath(cwd);
|
|
66
|
+
if (!dbPath) {
|
|
67
|
+
return { allowed: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const sqlite3 = require('better-sqlite3');
|
|
72
|
+
const db = sqlite3(dbPath, { readonly: true });
|
|
73
|
+
|
|
74
|
+
// Get the chore and its parent feature
|
|
75
|
+
const chore = db.prepare(`
|
|
76
|
+
SELECT wi.id, wi.parent_id, parent.mode, parent.type as parent_type, parent.scenario_file
|
|
77
|
+
FROM work_items wi
|
|
78
|
+
LEFT JOIN work_items parent ON wi.parent_id = parent.id
|
|
79
|
+
WHERE wi.id = ?
|
|
80
|
+
`).get(choreId);
|
|
81
|
+
|
|
82
|
+
if (!chore) {
|
|
83
|
+
db.close();
|
|
84
|
+
return { allowed: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If no parent or parent isn't a feature, allow
|
|
88
|
+
if (!chore.parent_id || chore.parent_type !== 'feature') {
|
|
89
|
+
db.close();
|
|
90
|
+
return { allowed: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If feature mode isn't 'stable', allow
|
|
94
|
+
if (chore.mode !== 'stable') {
|
|
95
|
+
db.close();
|
|
96
|
+
return { allowed: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Feature is in stable mode - check if this is the last chore
|
|
100
|
+
const incompleteChores = db.prepare(`
|
|
101
|
+
SELECT COUNT(*) as count
|
|
102
|
+
FROM work_items
|
|
103
|
+
WHERE parent_id = ? AND type = 'chore' AND status NOT IN ('done', 'cancelled')
|
|
104
|
+
`).get(chore.parent_id);
|
|
105
|
+
|
|
106
|
+
// If there are other incomplete chores, allow this merge
|
|
107
|
+
if (incompleteChores.count > 1) {
|
|
108
|
+
db.close();
|
|
109
|
+
return { allowed: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// This is the last stable-mode chore - check if BDD tests pass
|
|
113
|
+
const scenarioFile = chore.scenario_file;
|
|
114
|
+
db.close();
|
|
115
|
+
|
|
116
|
+
// If no scenario file, allow (nothing to test)
|
|
117
|
+
if (!scenarioFile) {
|
|
118
|
+
return { allowed: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Run BDD tests to check if they pass
|
|
122
|
+
const testsPass = await runBddTests(scenarioFile, cwd);
|
|
123
|
+
|
|
124
|
+
if (testsPass) {
|
|
125
|
+
return { allowed: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Tests are failing - block!
|
|
129
|
+
return {
|
|
130
|
+
allowed: false,
|
|
131
|
+
message: 'Cannot merge last stable-mode chore: BDD scenarios must pass',
|
|
132
|
+
hint: 'Run tests and fix failures before merging. All error handling scenarios must pass.'
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// Fail open
|
|
137
|
+
return { allowed: true };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run BDD tests and return whether they pass
|
|
143
|
+
*/
|
|
144
|
+
async function runBddTests(scenarioFile, cwd) {
|
|
145
|
+
// In test mode, use environment variable to control pass/fail
|
|
146
|
+
if (TEST_MODE) {
|
|
147
|
+
return TEST_BDD_PASS;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const scenarioPath = path.resolve(cwd, scenarioFile);
|
|
152
|
+
|
|
153
|
+
// Check if scenario file exists
|
|
154
|
+
if (!fs.existsSync(scenarioPath)) {
|
|
155
|
+
// No scenario file = pass (nothing to test)
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Run cucumber-js with the scenario file
|
|
160
|
+
const result = spawnSync('npx', ['cucumber-js', scenarioPath, '--format', 'summary'], {
|
|
161
|
+
cwd: cwd,
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
timeout: 60000 // 60 second timeout
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Check exit code - 0 means all tests passed
|
|
167
|
+
return result.status === 0;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
// On error running tests, fail open (allow merge)
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Find the jettypod database path
|
|
176
|
+
*/
|
|
177
|
+
function findDatabasePath(cwd) {
|
|
178
|
+
let dir = cwd || process.cwd();
|
|
179
|
+
while (dir !== path.dirname(dir)) {
|
|
180
|
+
const dbPath = path.join(dir, '.jettypod', 'work.db');
|
|
181
|
+
if (fs.existsSync(dbPath)) {
|
|
182
|
+
return dbPath;
|
|
183
|
+
}
|
|
184
|
+
dir = path.dirname(dir);
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Allow the action
|
|
191
|
+
*/
|
|
192
|
+
function allow() {
|
|
193
|
+
console.log(JSON.stringify({
|
|
194
|
+
hookSpecificOutput: {
|
|
195
|
+
hookEventName: "PreToolUse",
|
|
196
|
+
permissionDecision: "allow"
|
|
197
|
+
}
|
|
198
|
+
}));
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Deny the action with explanation
|
|
204
|
+
*/
|
|
205
|
+
function deny(message, hint) {
|
|
206
|
+
const reason = `ā ${message}\n\nš” Hint: ${hint}`;
|
|
207
|
+
|
|
208
|
+
console.log(JSON.stringify({
|
|
209
|
+
hookSpecificOutput: {
|
|
210
|
+
hookEventName: "PreToolUse",
|
|
211
|
+
permissionDecision: "deny",
|
|
212
|
+
permissionDecisionReason: reason
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
package/jettypod.js
CHANGED
|
@@ -244,11 +244,17 @@ Status: ${currentWork.status}
|
|
|
244
244
|
<jettypod_essentials>
|
|
245
245
|
JettyPod: Structured workflow system with skills that guide complex workflows.
|
|
246
246
|
|
|
247
|
+
## ā ļø CRITICAL: All Work Requires a Work Item
|
|
248
|
+
**Claude CANNOT write code directly to main branch.**
|
|
249
|
+
- All work requires: \`work create\` ā \`work start\` ā skill workflow ā \`work merge\`
|
|
250
|
+
- If user asks to implement something without a work item, create one first and invoke the matching skill
|
|
251
|
+
- Pre-commit hooks block direct commits to main
|
|
252
|
+
|
|
247
253
|
## ā ļø CRITICAL: Skills are MANDATORY for workflows
|
|
248
254
|
Skills auto-activate and MUST complete their full workflow:
|
|
249
255
|
- epic-planning: Guides architectural decisions
|
|
250
256
|
- feature-planning: Guides UX discovery + BDD scenarios
|
|
251
|
-
- speed-mode: Implements happy path, THEN auto-
|
|
257
|
+
- speed-mode: Implements happy path, THEN auto-invokes stable-mode
|
|
252
258
|
- stable-mode: Adds error handling to speed implementation
|
|
253
259
|
- external-transition: Guides launch preparation
|
|
254
260
|
|
|
@@ -260,10 +266,14 @@ Skills auto-activate and MUST complete their full workflow:
|
|
|
260
266
|
## Basic Commands (for non-workflow operations)
|
|
261
267
|
jettypod work create epic "<title>"
|
|
262
268
|
jettypod work create feature "<title>" --parent=<id>
|
|
263
|
-
jettypod work
|
|
269
|
+
jettypod work create chore "<title>" --parent=<id>
|
|
270
|
+
jettypod work start <id> # Creates worktree branch
|
|
271
|
+
jettypod work merge # Merges worktree back to main
|
|
272
|
+
jettypod work tests <feature-id> # Create test worktree for BDD
|
|
273
|
+
jettypod work tests merge <id> # Merge tests to main
|
|
264
274
|
jettypod work status <id> cancelled
|
|
265
275
|
jettypod backlog
|
|
266
|
-
jettypod impact <file>
|
|
276
|
+
jettypod impact <file> # Show tests/features affected
|
|
267
277
|
|
|
268
278
|
## Advanced Commands
|
|
269
279
|
For mode management, decisions, project state: docs/COMMAND_REFERENCE.md
|
|
@@ -1353,11 +1353,13 @@ async function mergeWork(options = {}) {
|
|
|
1353
1353
|
console.log(`Branch: ${currentBranch}`);
|
|
1354
1354
|
|
|
1355
1355
|
// Step 1: Push feature branch to remote
|
|
1356
|
+
// NOTE: Use ['pipe', 'inherit', 'inherit'] instead of 'inherit' to avoid stealing stdin
|
|
1357
|
+
// from Claude Code's shell. Using 'inherit' for stdin breaks the shell when run interactively.
|
|
1356
1358
|
try {
|
|
1357
1359
|
console.log('Pushing feature branch to remote...');
|
|
1358
1360
|
execSync(`git push -u origin ${currentBranch}`, {
|
|
1359
1361
|
cwd: gitRoot,
|
|
1360
|
-
stdio: 'inherit'
|
|
1362
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
1361
1363
|
});
|
|
1362
1364
|
} catch (err) {
|
|
1363
1365
|
const errorMsg = err.message.toLowerCase();
|
|
@@ -1409,7 +1411,7 @@ async function mergeWork(options = {}) {
|
|
|
1409
1411
|
console.log(`Checking out ${defaultBranch}...`);
|
|
1410
1412
|
execSync(`git checkout ${defaultBranch}`, {
|
|
1411
1413
|
cwd: gitRoot,
|
|
1412
|
-
stdio: 'inherit'
|
|
1414
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
1413
1415
|
});
|
|
1414
1416
|
} catch (err) {
|
|
1415
1417
|
return Promise.reject(new Error(`Failed to checkout ${defaultBranch}: ${err.message}`));
|
|
@@ -1448,7 +1450,7 @@ async function mergeWork(options = {}) {
|
|
|
1448
1450
|
console.log(`Updating ${defaultBranch} from remote...`);
|
|
1449
1451
|
execSync(`git pull origin ${defaultBranch}`, {
|
|
1450
1452
|
cwd: gitRoot,
|
|
1451
|
-
stdio: 'inherit'
|
|
1453
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
1452
1454
|
});
|
|
1453
1455
|
} catch (err) {
|
|
1454
1456
|
const errorMsg = err.message.toLowerCase();
|
|
@@ -1485,12 +1487,12 @@ async function mergeWork(options = {}) {
|
|
|
1485
1487
|
return Promise.reject(new Error(`Failed to pull ${defaultBranch}: ${err.message}`));
|
|
1486
1488
|
}
|
|
1487
1489
|
|
|
1488
|
-
// Step
|
|
1490
|
+
// Step 5: Merge feature branch into default branch
|
|
1489
1491
|
try {
|
|
1490
1492
|
console.log(`Merging ${currentBranch} into ${defaultBranch}...`);
|
|
1491
1493
|
execSync(`git merge --no-ff ${currentBranch} -m "Merge work item #${currentWork.id}"`, {
|
|
1492
1494
|
cwd: gitRoot,
|
|
1493
|
-
stdio: 'inherit'
|
|
1495
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
1494
1496
|
});
|
|
1495
1497
|
} catch (err) {
|
|
1496
1498
|
// Check if merge failed due to conflicts
|
|
@@ -1531,12 +1533,12 @@ async function mergeWork(options = {}) {
|
|
|
1531
1533
|
return Promise.reject(new Error(`Failed to merge ${currentBranch}: ${err.message}`));
|
|
1532
1534
|
}
|
|
1533
1535
|
|
|
1534
|
-
// Step
|
|
1536
|
+
// Step 6: Push default branch to remote
|
|
1535
1537
|
try {
|
|
1536
1538
|
console.log(`Pushing ${defaultBranch} to remote...`);
|
|
1537
1539
|
execSync(`git push origin ${defaultBranch}`, {
|
|
1538
1540
|
cwd: gitRoot,
|
|
1539
|
-
stdio: 'inherit'
|
|
1541
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
1540
1542
|
});
|
|
1541
1543
|
} catch (err) {
|
|
1542
1544
|
const errorMsg = err.message.toLowerCase();
|