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.
@@ -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-generates stable chores
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 start <id>
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> # Show tests/features affected by changing a 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 4: Merge feature branch into default branch
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 5: Push default branch to remote
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.62",
3
+ "version": "4.4.64",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {