jettypod 4.4.61 → 4.4.63

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,262 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Production Mode Guardrails Hook
4
+ *
5
+ * Blocks merge of the last production-mode chore until:
6
+ * 1. All BDD scenarios pass
7
+ * 2. Production standards validation passes
8
+ *
9
+ * This ensures production hardening is complete before deployment.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { spawnSync } = require('child_process');
15
+
16
+ // Test mode: allows tests to control pass/fail state
17
+ const TEST_MODE = process.env.PRODUCTION_GUARDRAIL_TEST_MODE === '1';
18
+ const TEST_BDD_PASS = process.env.PRODUCTION_GUARDRAIL_BDD_PASS === '1';
19
+ const TEST_STANDARDS_PASS = process.env.PRODUCTION_GUARDRAIL_STANDARDS_PASS === '1';
20
+ const TEST_HAS_STANDARDS = process.env.PRODUCTION_GUARDRAIL_HAS_STANDARDS === '1';
21
+
22
+ // Read hook input from stdin
23
+ let input = '';
24
+ process.stdin.on('data', chunk => input += chunk);
25
+ process.stdin.on('end', async () => {
26
+ try {
27
+ const hookInput = JSON.parse(input);
28
+ const { tool_name, tool_input, cwd } = hookInput;
29
+
30
+ // Only check Bash commands
31
+ if (tool_name !== 'Bash') {
32
+ allow();
33
+ return;
34
+ }
35
+
36
+ const command = tool_input.command || '';
37
+
38
+ // Only check jettypod work merge commands
39
+ if (!/jettypod\s+work\s+merge/.test(command)) {
40
+ allow();
41
+ return;
42
+ }
43
+
44
+ // Extract chore ID from command
45
+ const choreIdMatch = command.match(/jettypod\s+work\s+merge\s+(\d+)/);
46
+ if (!choreIdMatch) {
47
+ // No explicit ID - allow (might be merging current work)
48
+ allow();
49
+ return;
50
+ }
51
+
52
+ const choreId = parseInt(choreIdMatch[1]);
53
+ const result = await checkProductionModeGuardrail(choreId, cwd);
54
+
55
+ if (result.allowed) {
56
+ allow();
57
+ } else {
58
+ deny(result.message, result.hint);
59
+ }
60
+ } catch (err) {
61
+ // Fail open on errors
62
+ allow();
63
+ }
64
+ });
65
+
66
+ /**
67
+ * Check if this merge should be blocked
68
+ */
69
+ async function checkProductionModeGuardrail(choreId, cwd) {
70
+ const dbPath = findDatabasePath(cwd);
71
+ if (!dbPath) {
72
+ return { allowed: true };
73
+ }
74
+
75
+ try {
76
+ const sqlite3 = require('better-sqlite3');
77
+ const db = sqlite3(dbPath, { readonly: true });
78
+
79
+ // Get the chore and its parent feature
80
+ const chore = db.prepare(`
81
+ SELECT wi.id, wi.parent_id, parent.mode, parent.type as parent_type, parent.scenario_file
82
+ FROM work_items wi
83
+ LEFT JOIN work_items parent ON wi.parent_id = parent.id
84
+ WHERE wi.id = ?
85
+ `).get(choreId);
86
+
87
+ if (!chore) {
88
+ db.close();
89
+ return { allowed: true };
90
+ }
91
+
92
+ // If no parent or parent isn't a feature, allow
93
+ if (!chore.parent_id || chore.parent_type !== 'feature') {
94
+ db.close();
95
+ return { allowed: true };
96
+ }
97
+
98
+ // If feature mode isn't 'production', allow
99
+ if (chore.mode !== 'production') {
100
+ db.close();
101
+ return { allowed: true };
102
+ }
103
+
104
+ // Feature is in production mode - check if this is the last chore
105
+ const incompleteChores = db.prepare(`
106
+ SELECT COUNT(*) as count
107
+ FROM work_items
108
+ WHERE parent_id = ? AND type = 'chore' AND status NOT IN ('done', 'cancelled')
109
+ `).get(chore.parent_id);
110
+
111
+ // If there are other incomplete chores, allow this merge
112
+ if (incompleteChores.count > 1) {
113
+ db.close();
114
+ return { allowed: true };
115
+ }
116
+
117
+ // This is the last production-mode chore
118
+ const scenarioFile = chore.scenario_file;
119
+ db.close();
120
+
121
+ // Check 1: BDD tests must pass
122
+ if (scenarioFile) {
123
+ const testsPass = await runBddTests(scenarioFile, cwd);
124
+ if (!testsPass) {
125
+ return {
126
+ allowed: false,
127
+ message: 'Cannot merge last production-mode chore: BDD scenarios must pass',
128
+ hint: 'Run tests and fix failures before merging.'
129
+ };
130
+ }
131
+ }
132
+
133
+ // Check 2: Production standards validation must pass
134
+ const standardsPass = await validateProductionStandards(cwd);
135
+ if (!standardsPass) {
136
+ return {
137
+ allowed: false,
138
+ message: 'Cannot merge last production-mode chore: Production standards validation must pass',
139
+ hint: 'Run production standards validation and fix any gaps.'
140
+ };
141
+ }
142
+
143
+ // All checks pass
144
+ return { allowed: true };
145
+
146
+ } catch (err) {
147
+ // Fail open
148
+ return { allowed: true };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Run BDD tests and return whether they pass
154
+ */
155
+ async function runBddTests(scenarioFile, cwd) {
156
+ // In test mode, use environment variable to control pass/fail
157
+ if (TEST_MODE) {
158
+ return TEST_BDD_PASS;
159
+ }
160
+
161
+ try {
162
+ const scenarioPath = path.resolve(cwd, scenarioFile);
163
+
164
+ // Check if scenario file exists
165
+ if (!fs.existsSync(scenarioPath)) {
166
+ return true;
167
+ }
168
+
169
+ // Run cucumber-js with the scenario file
170
+ const result = spawnSync('npx', ['cucumber-js', scenarioPath, '--format', 'summary'], {
171
+ cwd: cwd,
172
+ encoding: 'utf-8',
173
+ timeout: 60000
174
+ });
175
+
176
+ return result.status === 0;
177
+ } catch (err) {
178
+ return true;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Validate production standards
184
+ */
185
+ async function validateProductionStandards(cwd) {
186
+ // In test mode, use environment variable to control pass/fail
187
+ if (TEST_MODE) {
188
+ // If no standards file in test mode, allow
189
+ if (!TEST_HAS_STANDARDS) {
190
+ return true;
191
+ }
192
+ return TEST_STANDARDS_PASS;
193
+ }
194
+
195
+ try {
196
+ // Check if production standards file exists
197
+ const standardsPath = path.join(cwd, '.jettypod', 'production-standards.json');
198
+ if (!fs.existsSync(standardsPath)) {
199
+ // No standards file = allow (nothing to validate)
200
+ return true;
201
+ }
202
+
203
+ // Read and validate standards
204
+ const standards = JSON.parse(fs.readFileSync(standardsPath, 'utf-8'));
205
+
206
+ // If no standards defined, allow
207
+ if (!standards.standards || standards.standards.length === 0) {
208
+ return true;
209
+ }
210
+
211
+ // Check if all standards are satisfied
212
+ // For now, we just check if the file exists and has standards
213
+ // More sophisticated validation would check each standard against implementation
214
+ return true;
215
+ } catch (err) {
216
+ return true;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Find the jettypod database path
222
+ */
223
+ function findDatabasePath(cwd) {
224
+ let dir = cwd || process.cwd();
225
+ while (dir !== path.dirname(dir)) {
226
+ const dbPath = path.join(dir, '.jettypod', 'work.db');
227
+ if (fs.existsSync(dbPath)) {
228
+ return dbPath;
229
+ }
230
+ dir = path.dirname(dir);
231
+ }
232
+ return null;
233
+ }
234
+
235
+ /**
236
+ * Allow the action
237
+ */
238
+ function allow() {
239
+ console.log(JSON.stringify({
240
+ hookSpecificOutput: {
241
+ hookEventName: "PreToolUse",
242
+ permissionDecision: "allow"
243
+ }
244
+ }));
245
+ process.exit(0);
246
+ }
247
+
248
+ /**
249
+ * Deny the action with explanation
250
+ */
251
+ function deny(message, hint) {
252
+ const reason = `āŒ ${message}\n\nšŸ’” Hint: ${hint}`;
253
+
254
+ console.log(JSON.stringify({
255
+ hookSpecificOutput: {
256
+ hookEventName: "PreToolUse",
257
+ permissionDecision: "deny",
258
+ permissionDecisionReason: reason
259
+ }
260
+ }));
261
+ process.exit(0);
262
+ }
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Speed Mode Guardrails Hook
4
+ *
5
+ * Blocks merge of the last speed-mode chore until stable-mode transition happens.
6
+ * This ensures the speed→stable handoff is not skipped.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ // Read hook input from stdin
13
+ let input = '';
14
+ process.stdin.on('data', chunk => input += chunk);
15
+ process.stdin.on('end', async () => {
16
+ try {
17
+ const hookInput = JSON.parse(input);
18
+ const { tool_name, tool_input, cwd } = hookInput;
19
+
20
+ // Only check Bash commands
21
+ if (tool_name !== 'Bash') {
22
+ allow();
23
+ return;
24
+ }
25
+
26
+ const command = tool_input.command || '';
27
+
28
+ // Only check jettypod work merge commands
29
+ if (!/jettypod\s+work\s+merge/.test(command)) {
30
+ allow();
31
+ return;
32
+ }
33
+
34
+ // Extract chore ID from command
35
+ const choreIdMatch = command.match(/jettypod\s+work\s+merge\s+(\d+)/);
36
+ if (!choreIdMatch) {
37
+ // No explicit ID - allow (might be merging current work)
38
+ allow();
39
+ return;
40
+ }
41
+
42
+ const choreId = parseInt(choreIdMatch[1]);
43
+ const result = await checkSpeedModeGuardrail(choreId, cwd);
44
+
45
+ if (result.allowed) {
46
+ allow();
47
+ } else {
48
+ deny(result.message, result.hint);
49
+ }
50
+ } catch (err) {
51
+ // Fail open on errors
52
+ allow();
53
+ }
54
+ });
55
+
56
+ /**
57
+ * Check if this merge should be blocked
58
+ */
59
+ async function checkSpeedModeGuardrail(choreId, cwd) {
60
+ const dbPath = findDatabasePath(cwd);
61
+ if (!dbPath) {
62
+ return { allowed: true };
63
+ }
64
+
65
+ try {
66
+ const sqlite3 = require('better-sqlite3');
67
+ const db = sqlite3(dbPath, { readonly: true });
68
+
69
+ // Get the chore and its parent feature
70
+ const chore = db.prepare(`
71
+ SELECT wi.id, wi.parent_id, parent.mode, parent.type as parent_type
72
+ FROM work_items wi
73
+ LEFT JOIN work_items parent ON wi.parent_id = parent.id
74
+ WHERE wi.id = ?
75
+ `).get(choreId);
76
+
77
+ if (!chore) {
78
+ db.close();
79
+ return { allowed: true };
80
+ }
81
+
82
+ // If no parent or parent isn't a feature, allow
83
+ if (!chore.parent_id || chore.parent_type !== 'feature') {
84
+ db.close();
85
+ return { allowed: true };
86
+ }
87
+
88
+ // If feature mode isn't 'speed', allow
89
+ if (chore.mode !== 'speed') {
90
+ db.close();
91
+ return { allowed: true };
92
+ }
93
+
94
+ // Feature is in speed mode - check if this is the last chore
95
+ const incompleteChores = db.prepare(`
96
+ SELECT COUNT(*) as count
97
+ FROM work_items
98
+ WHERE parent_id = ? AND type = 'chore' AND status NOT IN ('done', 'cancelled')
99
+ `).get(chore.parent_id);
100
+
101
+ db.close();
102
+
103
+ // If there are other incomplete chores, allow this merge
104
+ if (incompleteChores.count > 1) {
105
+ return { allowed: true };
106
+ }
107
+
108
+ // This is the last speed-mode chore - block!
109
+ return {
110
+ allowed: false,
111
+ message: 'Cannot merge last speed-mode chore while feature is in speed mode',
112
+ hint: 'Generate stable-mode chores first. Use the speed-mode skill to complete the handoff.'
113
+ };
114
+
115
+ } catch (err) {
116
+ // Fail open
117
+ return { allowed: true };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Find the jettypod database path
123
+ */
124
+ function findDatabasePath(cwd) {
125
+ let dir = cwd || process.cwd();
126
+ while (dir !== path.dirname(dir)) {
127
+ const dbPath = path.join(dir, '.jettypod', 'work.db');
128
+ if (fs.existsSync(dbPath)) {
129
+ return dbPath;
130
+ }
131
+ dir = path.dirname(dir);
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Allow the action
138
+ */
139
+ function allow() {
140
+ console.log(JSON.stringify({
141
+ hookSpecificOutput: {
142
+ hookEventName: "PreToolUse",
143
+ permissionDecision: "allow"
144
+ }
145
+ }));
146
+ process.exit(0);
147
+ }
148
+
149
+ /**
150
+ * Deny the action with explanation
151
+ */
152
+ function deny(message, hint) {
153
+ const reason = `āŒ ${message}\n\nšŸ’” Hint: ${hint}`;
154
+
155
+ console.log(JSON.stringify({
156
+ hookSpecificOutput: {
157
+ hookEventName: "PreToolUse",
158
+ permissionDecision: "deny",
159
+ permissionDecisionReason: reason
160
+ }
161
+ }));
162
+ process.exit(0);
163
+ }
@@ -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
+ }