wogiflow 1.0.25 → 1.0.26
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/package.json +1 -1
- package/scripts/hooks/adapters/claude-code.js +58 -3
- package/scripts/hooks/core/implementation-gate.js +337 -0
- package/scripts/hooks/core/index.js +11 -1
- package/scripts/hooks/core/todowrite-gate.js +349 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +59 -7
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +87 -0
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@ const { PATHS } = require('../../flow-utils');
|
|
|
21
21
|
const HOOK_TIMEOUTS = {
|
|
22
22
|
SESSION_START: 10, // Session initialization
|
|
23
23
|
SETUP: 30, // Project setup/onboarding
|
|
24
|
+
USER_PROMPT_SUBMIT: 5, // Implementation gate check
|
|
24
25
|
PRE_TOOL_USE: 5, // Pre-edit checks (task gate, component check)
|
|
25
26
|
POST_TOOL_USE: 60, // Validation (linting, type checking)
|
|
26
27
|
STOP: 5, // Loop enforcement check
|
|
@@ -117,6 +118,8 @@ class ClaudeCodeAdapter extends BaseAdapter {
|
|
|
117
118
|
return this.transformStop(coreResult);
|
|
118
119
|
case 'SessionEnd':
|
|
119
120
|
return this.transformSessionEnd(coreResult);
|
|
121
|
+
case 'UserPromptSubmit':
|
|
122
|
+
return this.transformUserPromptSubmit(coreResult);
|
|
120
123
|
default:
|
|
121
124
|
return { continue: true };
|
|
122
125
|
}
|
|
@@ -321,6 +324,46 @@ Run: /wogi-start ${coreResult.nextTaskId}`;
|
|
|
321
324
|
};
|
|
322
325
|
}
|
|
323
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Transform UserPromptSubmit result (implementation gate)
|
|
329
|
+
*/
|
|
330
|
+
transformUserPromptSubmit(coreResult) {
|
|
331
|
+
// Blocked - prevent prompt processing with clear message
|
|
332
|
+
if (coreResult.blocked) {
|
|
333
|
+
return {
|
|
334
|
+
continue: false, // Block the prompt
|
|
335
|
+
systemMessage: coreResult.message || 'Implementation request blocked by Wogi Flow',
|
|
336
|
+
hookSpecificOutput: {
|
|
337
|
+
hookEventName: 'UserPromptSubmit',
|
|
338
|
+
decision: 'block',
|
|
339
|
+
reason: coreResult.reason,
|
|
340
|
+
suggestedAction: coreResult.suggestedAction
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Warning - allow but show message
|
|
346
|
+
if (coreResult.message && !coreResult.blocked) {
|
|
347
|
+
return {
|
|
348
|
+
continue: true,
|
|
349
|
+
systemMessage: coreResult.message,
|
|
350
|
+
hookSpecificOutput: {
|
|
351
|
+
hookEventName: 'UserPromptSubmit',
|
|
352
|
+
decision: 'warn',
|
|
353
|
+
reason: coreResult.reason
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Allowed
|
|
359
|
+
return {
|
|
360
|
+
continue: true,
|
|
361
|
+
hookSpecificOutput: {
|
|
362
|
+
hookEventName: 'UserPromptSubmit'
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
324
367
|
/**
|
|
325
368
|
* Generate Claude Code hook configuration
|
|
326
369
|
*/
|
|
@@ -350,12 +393,24 @@ Run: /wogi-start ${coreResult.nextTaskId}`;
|
|
|
350
393
|
}];
|
|
351
394
|
}
|
|
352
395
|
|
|
353
|
-
//
|
|
396
|
+
// UserPromptSubmit hook (implementation gate)
|
|
397
|
+
if (rules.implementationGate?.enabled !== false) {
|
|
398
|
+
hooks.UserPromptSubmit = [{
|
|
399
|
+
hooks: [{
|
|
400
|
+
type: 'command',
|
|
401
|
+
command: `node "${path.join(scriptsDir, 'user-prompt-submit.js')}"`,
|
|
402
|
+
timeout: HOOK_TIMEOUTS.USER_PROMPT_SUBMIT
|
|
403
|
+
}]
|
|
404
|
+
}];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// PreToolUse hooks for Edit/Write/TodoWrite
|
|
354
408
|
const preToolUseMatchers = [];
|
|
355
409
|
|
|
356
|
-
|
|
410
|
+
// Task gating for Edit/Write + TodoWrite gating
|
|
411
|
+
if (rules.taskGating?.enabled !== false || rules.todoWriteGate?.enabled !== false) {
|
|
357
412
|
preToolUseMatchers.push({
|
|
358
|
-
matcher: 'Edit|Write',
|
|
413
|
+
matcher: 'Edit|Write|TodoWrite',
|
|
359
414
|
hooks: [{
|
|
360
415
|
type: 'command',
|
|
361
416
|
command: `node "${path.join(scriptsDir, 'pre-tool-use.js')}"`,
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Implementation Gate (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* Detects implementation requests from user prompts and blocks them
|
|
7
|
+
* if no active task exists. Guides users to /wogi-story or /wogi-start.
|
|
8
|
+
*
|
|
9
|
+
* Returns a standardized result that adapters transform for specific CLIs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { getConfig } = require('../../flow-utils');
|
|
13
|
+
const { getActiveTask } = require('./task-gate');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Patterns that indicate an implementation request
|
|
17
|
+
* These should trigger the gate when no task is active
|
|
18
|
+
*/
|
|
19
|
+
// Maximum prompt length to process (prevent DoS)
|
|
20
|
+
const MAX_PROMPT_LENGTH = 10000;
|
|
21
|
+
|
|
22
|
+
const IMPLEMENTATION_PATTERNS = [
|
|
23
|
+
// Direct action verbs (bounded character classes to prevent ReDoS)
|
|
24
|
+
/\b(add|create|build|implement|make|write)\s+(a\s+)?[\w\s]{1,100}/i,
|
|
25
|
+
/\b(fix|repair|resolve|patch)\s+[\w\s]{0,50}(bug|issue|error|problem)/i,
|
|
26
|
+
/\b(fix|repair|resolve|patch)\s+(the\s+)?[\w]{1,50}/i,
|
|
27
|
+
/\b(update|modify|change|edit|refactor)\s+(the\s+)?[\w\s]{1,100}/i,
|
|
28
|
+
/\b(remove|delete|drop)\s+(the\s+)?[\w\s]{1,100}/i,
|
|
29
|
+
/\b(integrate|connect|hook\s+up)\s+[\w\s]{1,100}/i,
|
|
30
|
+
|
|
31
|
+
// Feature/component creation
|
|
32
|
+
/\b(new\s+)?(feature|component|module|service|hook|util)/i,
|
|
33
|
+
// Bounded pattern to prevent ReDoS (was: /\badd\s+.*\s+(to|into|for)\s+/i)
|
|
34
|
+
/\badd\s+[\w\s]{1,100}\s+(to|into|for)\s+/i,
|
|
35
|
+
|
|
36
|
+
// Task-like requests
|
|
37
|
+
/\bwe\s+need\s+(to\s+)?/i,
|
|
38
|
+
/\bshould\s+(add|create|implement|fix)/i,
|
|
39
|
+
/\blet'?s\s+(add|create|implement|fix|build)/i,
|
|
40
|
+
/\bcan\s+you\s+(add|create|implement|fix|build)/i,
|
|
41
|
+
/\bplease\s+(add|create|implement|fix|build)/i,
|
|
42
|
+
|
|
43
|
+
// Specific requests (bounded to prevent ReDoS - was using .*)
|
|
44
|
+
/\bmake\s+[\w\s]{1,100}\s+work/i,
|
|
45
|
+
/\bget\s+[\w\s]{1,100}\s+working/i,
|
|
46
|
+
/\bset\s+up\s+/i
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Patterns that indicate exploration/questions (NOT implementation)
|
|
51
|
+
* These should NOT trigger the gate
|
|
52
|
+
*/
|
|
53
|
+
const EXPLORATION_PATTERNS = [
|
|
54
|
+
/\bwhat\s+(does|is|are|do)\b/i,
|
|
55
|
+
/\bhow\s+(does|do|can|to|would)\b/i,
|
|
56
|
+
/\bwhy\s+(does|do|is|are)\b/i,
|
|
57
|
+
/\bwhere\s+(is|are|do|does|can)\b/i,
|
|
58
|
+
/\bshow\s+me\b/i,
|
|
59
|
+
/\bexplain\b/i,
|
|
60
|
+
/\bdescribe\b/i,
|
|
61
|
+
/\blist\s+(all|the)\b/i,
|
|
62
|
+
/\bfind\s+(all|the|where)\b/i,
|
|
63
|
+
/\bsearch\s+(for|the)\b/i,
|
|
64
|
+
/\bread\s+(the|this)\b/i,
|
|
65
|
+
/\blook\s+(at|for|into)\b/i,
|
|
66
|
+
/\bunderstand\b/i,
|
|
67
|
+
/\banalyze\b/i,
|
|
68
|
+
/\breview\s+(the|this|my)/i,
|
|
69
|
+
/\bcheck\s+(if|whether|the)/i,
|
|
70
|
+
/\bcan\s+(claude|you)\s+(access|read|see)/i
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* WogiFlow command patterns that should always be allowed
|
|
75
|
+
*/
|
|
76
|
+
const WOGI_COMMAND_PATTERNS = [
|
|
77
|
+
/^\s*\/wogi-/i,
|
|
78
|
+
/^\s*\/flow\s+/i,
|
|
79
|
+
/\brun\s+(\/)?wogi-/i
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if implementation gate should be enforced
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function isImplementationGateEnabled() {
|
|
87
|
+
const config = getConfig();
|
|
88
|
+
|
|
89
|
+
// Check hooks config first
|
|
90
|
+
if (config.hooks?.rules?.implementationGate?.enabled === false) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fall back to enforcement config
|
|
95
|
+
if (config.enforcement?.strictMode === false) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if soft mode is enabled (warn instead of block)
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function isSoftModeEnabled() {
|
|
107
|
+
const config = getConfig();
|
|
108
|
+
return config.hooks?.rules?.implementationGate?.softMode === true ||
|
|
109
|
+
config.enforcement?.softMode === true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detect if prompt is a WogiFlow command (always allowed)
|
|
114
|
+
* @param {string} prompt
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function isWogiCommand(prompt) {
|
|
118
|
+
if (!prompt || typeof prompt !== 'string') return false;
|
|
119
|
+
return WOGI_COMMAND_PATTERNS.some(pattern => pattern.test(prompt));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect if prompt is primarily exploratory (questions, reading)
|
|
124
|
+
* @param {string} prompt
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
function isExplorationRequest(prompt) {
|
|
128
|
+
if (!prompt || typeof prompt !== 'string') return false;
|
|
129
|
+
|
|
130
|
+
// Check if it matches exploration patterns
|
|
131
|
+
const matchesExploration = EXPLORATION_PATTERNS.some(pattern => pattern.test(prompt));
|
|
132
|
+
|
|
133
|
+
// Short prompts that are questions are exploratory
|
|
134
|
+
const isQuestion = prompt.trim().endsWith('?') && prompt.length < 200;
|
|
135
|
+
|
|
136
|
+
return matchesExploration || isQuestion;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Detect if prompt contains implementation intent
|
|
141
|
+
* @param {string} prompt
|
|
142
|
+
* @returns {{isImplementation: boolean, confidence: string, matches: string[]}}
|
|
143
|
+
*/
|
|
144
|
+
function detectImplementationIntent(prompt) {
|
|
145
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
146
|
+
return { isImplementation: false, confidence: 'low', matches: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const matches = [];
|
|
150
|
+
|
|
151
|
+
for (const pattern of IMPLEMENTATION_PATTERNS) {
|
|
152
|
+
const match = prompt.match(pattern);
|
|
153
|
+
if (match) {
|
|
154
|
+
matches.push(match[0]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (matches.length === 0) {
|
|
159
|
+
return { isImplementation: false, confidence: 'low', matches: [] };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Determine confidence based on number and quality of matches
|
|
163
|
+
let confidence = 'low';
|
|
164
|
+
if (matches.length >= 3) {
|
|
165
|
+
confidence = 'high';
|
|
166
|
+
} else if (matches.length >= 1) {
|
|
167
|
+
// Check for strong signals
|
|
168
|
+
const hasStrongSignal = matches.some(m =>
|
|
169
|
+
/\b(add|create|implement|fix|build)\b/i.test(m)
|
|
170
|
+
);
|
|
171
|
+
confidence = hasStrongSignal ? 'high' : 'medium';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { isImplementation: true, confidence, matches };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check implementation gate for a user prompt
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} options
|
|
181
|
+
* @param {string} options.prompt - User's input prompt
|
|
182
|
+
* @param {string} [options.source] - Source of prompt (manual, paste, etc.)
|
|
183
|
+
* @returns {Object} Result: { allowed, blocked, message, reason, confidence, suggestedAction }
|
|
184
|
+
*/
|
|
185
|
+
function checkImplementationGate(options = {}) {
|
|
186
|
+
const { prompt, source: _source } = options;
|
|
187
|
+
|
|
188
|
+
// Empty or invalid prompt - allow
|
|
189
|
+
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
190
|
+
return {
|
|
191
|
+
allowed: true,
|
|
192
|
+
blocked: false,
|
|
193
|
+
message: null,
|
|
194
|
+
reason: 'empty_prompt'
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Truncate overly long prompts to prevent DoS via regex processing
|
|
199
|
+
const processedPrompt = prompt.length > MAX_PROMPT_LENGTH
|
|
200
|
+
? prompt.slice(0, MAX_PROMPT_LENGTH)
|
|
201
|
+
: prompt;
|
|
202
|
+
|
|
203
|
+
// WogiFlow commands always allowed (check original prompt for commands)
|
|
204
|
+
if (isWogiCommand(prompt)) {
|
|
205
|
+
return {
|
|
206
|
+
allowed: true,
|
|
207
|
+
blocked: false,
|
|
208
|
+
message: null,
|
|
209
|
+
reason: 'wogi_command'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if gate is enabled
|
|
214
|
+
if (!isImplementationGateEnabled()) {
|
|
215
|
+
return {
|
|
216
|
+
allowed: true,
|
|
217
|
+
blocked: false,
|
|
218
|
+
message: null,
|
|
219
|
+
reason: 'gate_disabled'
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Exploration requests always allowed (use truncated prompt for safety)
|
|
224
|
+
if (isExplorationRequest(processedPrompt)) {
|
|
225
|
+
return {
|
|
226
|
+
allowed: true,
|
|
227
|
+
blocked: false,
|
|
228
|
+
message: null,
|
|
229
|
+
reason: 'exploration_request'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check for implementation intent (use truncated prompt for safety)
|
|
234
|
+
const { isImplementation, confidence, matches } = detectImplementationIntent(processedPrompt);
|
|
235
|
+
|
|
236
|
+
if (!isImplementation) {
|
|
237
|
+
return {
|
|
238
|
+
allowed: true,
|
|
239
|
+
blocked: false,
|
|
240
|
+
message: null,
|
|
241
|
+
reason: 'no_implementation_intent'
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Has implementation intent - check for active task
|
|
246
|
+
const activeTask = getActiveTask();
|
|
247
|
+
|
|
248
|
+
if (activeTask) {
|
|
249
|
+
return {
|
|
250
|
+
allowed: true,
|
|
251
|
+
blocked: false,
|
|
252
|
+
message: null,
|
|
253
|
+
task: activeTask,
|
|
254
|
+
reason: 'task_active',
|
|
255
|
+
confidence,
|
|
256
|
+
matches
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// No active task and implementation intent detected
|
|
261
|
+
const softMode = isSoftModeEnabled();
|
|
262
|
+
|
|
263
|
+
if (softMode) {
|
|
264
|
+
return {
|
|
265
|
+
allowed: true,
|
|
266
|
+
blocked: false,
|
|
267
|
+
message: generateWarningMessage(prompt, confidence, matches),
|
|
268
|
+
reason: 'warn_only',
|
|
269
|
+
confidence,
|
|
270
|
+
suggestedAction: 'create-story',
|
|
271
|
+
matches
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Hard block
|
|
276
|
+
return {
|
|
277
|
+
allowed: false,
|
|
278
|
+
blocked: true,
|
|
279
|
+
message: generateBlockMessage(prompt, confidence, matches),
|
|
280
|
+
reason: 'no_active_task',
|
|
281
|
+
confidence,
|
|
282
|
+
suggestedAction: 'create-story',
|
|
283
|
+
matches
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate warning message (soft mode)
|
|
289
|
+
*/
|
|
290
|
+
function generateWarningMessage(prompt, confidence, matches) {
|
|
291
|
+
const truncatedPrompt = prompt.length > 100 ? prompt.slice(0, 100) + '...' : prompt;
|
|
292
|
+
return `Warning: Implementation request detected (${confidence} confidence), but no WogiFlow task is active.
|
|
293
|
+
|
|
294
|
+
Detected: ${matches.slice(0, 3).join(', ')}
|
|
295
|
+
|
|
296
|
+
Consider using:
|
|
297
|
+
- /wogi-ready to see available tasks
|
|
298
|
+
- /wogi-start wf-XXXXXXXX to start an existing task
|
|
299
|
+
- /wogi-story "${truncatedPrompt}" to create a new task`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Generate block message (hard mode)
|
|
304
|
+
*/
|
|
305
|
+
function generateBlockMessage(prompt, confidence, matches) {
|
|
306
|
+
const truncatedPrompt = prompt.length > 80 ? prompt.slice(0, 80) + '...' : prompt;
|
|
307
|
+
|
|
308
|
+
return `BLOCKED: Implementation request detected, but no WogiFlow task is active.
|
|
309
|
+
|
|
310
|
+
Detected implementation intent (${confidence} confidence):
|
|
311
|
+
${matches.slice(0, 3).map(m => ` - "${m}"`).join('\n')}
|
|
312
|
+
|
|
313
|
+
To proceed, use the WogiFlow workflow:
|
|
314
|
+
1. /wogi-ready - see available tasks
|
|
315
|
+
2. /wogi-start wf-XXXXXXXX - start an existing task
|
|
316
|
+
3. /wogi-story "${truncatedPrompt}" - create a new task
|
|
317
|
+
|
|
318
|
+
Why? WogiFlow ensures nothing gets missed:
|
|
319
|
+
- Acceptance criteria are tracked
|
|
320
|
+
- Changes are logged
|
|
321
|
+
- Quality gates are enforced
|
|
322
|
+
|
|
323
|
+
Copy your request and use: /wogi-story "your request"`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
isImplementationGateEnabled,
|
|
328
|
+
isSoftModeEnabled,
|
|
329
|
+
isWogiCommand,
|
|
330
|
+
isExplorationRequest,
|
|
331
|
+
detectImplementationIntent,
|
|
332
|
+
checkImplementationGate,
|
|
333
|
+
generateWarningMessage,
|
|
334
|
+
generateBlockMessage,
|
|
335
|
+
IMPLEMENTATION_PATTERNS,
|
|
336
|
+
EXPLORATION_PATTERNS
|
|
337
|
+
};
|
|
@@ -13,6 +13,8 @@ const componentCheck = require('./component-check');
|
|
|
13
13
|
const sessionContext = require('./session-context');
|
|
14
14
|
const setupCheck = require('./setup-check');
|
|
15
15
|
const setupHandler = require('./setup-handler');
|
|
16
|
+
const implementationGate = require('./implementation-gate');
|
|
17
|
+
const todoWriteGate = require('./todowrite-gate');
|
|
16
18
|
|
|
17
19
|
module.exports = {
|
|
18
20
|
// Task Gating
|
|
@@ -41,5 +43,13 @@ module.exports = {
|
|
|
41
43
|
|
|
42
44
|
// Setup Handler (Claude Code 2.1.10+)
|
|
43
45
|
...setupHandler,
|
|
44
|
-
setupHandler
|
|
46
|
+
setupHandler,
|
|
47
|
+
|
|
48
|
+
// Implementation Gate (blocks implementation requests without active task)
|
|
49
|
+
...implementationGate,
|
|
50
|
+
implementationGate,
|
|
51
|
+
|
|
52
|
+
// TodoWrite Gate (blocks implementation todos without active task)
|
|
53
|
+
...todoWriteGate,
|
|
54
|
+
todoWriteGate
|
|
45
55
|
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - TodoWrite Gate (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* Distinguishes between implementation todos and workflow tracking todos.
|
|
7
|
+
* Blocks implementation todos when no active task exists.
|
|
8
|
+
*
|
|
9
|
+
* Implementation todos: "Create X component", "Add Y feature"
|
|
10
|
+
* Tracking todos: "Run tests", "Update request-log", "Commit changes"
|
|
11
|
+
*
|
|
12
|
+
* Returns a standardized result that adapters transform for specific CLIs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { getConfig } = require('../../flow-utils');
|
|
16
|
+
const { getActiveTask } = require('./task-gate');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Patterns that indicate an implementation todo (should require active task)
|
|
20
|
+
*/
|
|
21
|
+
const IMPLEMENTATION_TODO_PATTERNS = [
|
|
22
|
+
// Creation patterns
|
|
23
|
+
/^(create|add|build|implement|make|write)\s+/i,
|
|
24
|
+
/^(new|design)\s+/i,
|
|
25
|
+
|
|
26
|
+
// Modification patterns
|
|
27
|
+
/^(fix|update|modify|change|edit|refactor)\s+/i,
|
|
28
|
+
/^(remove|delete|drop)\s+/i,
|
|
29
|
+
|
|
30
|
+
// Integration patterns
|
|
31
|
+
/^(integrate|connect|hook\s+up|wire\s+up)\s+/i,
|
|
32
|
+
|
|
33
|
+
// Component-specific
|
|
34
|
+
/\b(component|module|service|hook|util|function|class|interface)\b/i,
|
|
35
|
+
|
|
36
|
+
// Feature patterns
|
|
37
|
+
/\b(feature|functionality|capability)\b/i,
|
|
38
|
+
|
|
39
|
+
// File operation patterns that imply creation
|
|
40
|
+
/\b(file|page|screen|view|route)\b.*\b(for|to)\b/i
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Patterns that indicate a workflow tracking todo (always allowed)
|
|
45
|
+
* These are WogiFlow workflow steps, not implementation
|
|
46
|
+
*/
|
|
47
|
+
const TRACKING_TODO_PATTERNS = [
|
|
48
|
+
// Testing and validation
|
|
49
|
+
/^run\s+(tests?|lint|typecheck|build|check)/i,
|
|
50
|
+
/^(test|verify|validate|check)\s+/i,
|
|
51
|
+
/^(execute|perform)\s+(tests?|validation)/i,
|
|
52
|
+
|
|
53
|
+
// Logging and documentation
|
|
54
|
+
/^update\s+(request-?log|app-?map|decision|progress)/i,
|
|
55
|
+
/^(log|record|document)\s+/i,
|
|
56
|
+
/^add\s+(to\s+)?(log|entry|record)/i,
|
|
57
|
+
|
|
58
|
+
// Version control
|
|
59
|
+
/^(commit|push|pull|merge|rebase)/i,
|
|
60
|
+
/^(stage|unstage|stash)/i,
|
|
61
|
+
/^git\s+/i,
|
|
62
|
+
|
|
63
|
+
// Review and cleanup
|
|
64
|
+
/^review\s+(changes?|code|implementation)/i,
|
|
65
|
+
/^clean\s+up/i,
|
|
66
|
+
/^(finalize|complete|finish)\s+(task|work)/i,
|
|
67
|
+
|
|
68
|
+
// WogiFlow specific
|
|
69
|
+
/^(mark|set)\s+(as\s+)?(complete|done|finished)/i,
|
|
70
|
+
/^close\s+(task|issue)/i,
|
|
71
|
+
/^(update|sync)\s+(state|status)/i,
|
|
72
|
+
|
|
73
|
+
// Reading/checking (non-modifying)
|
|
74
|
+
/^(read|check|look\s+at|review|inspect)\s+/i,
|
|
75
|
+
/^(verify|confirm|ensure)\s+/i,
|
|
76
|
+
|
|
77
|
+
// Quality gates
|
|
78
|
+
/^(pass|run)\s+(quality|lint|type)\s*(gate|check)?/i
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Explicit allowlist - these are ALWAYS allowed regardless of patterns
|
|
83
|
+
*/
|
|
84
|
+
const ALWAYS_ALLOWED_TODOS = [
|
|
85
|
+
'run tests',
|
|
86
|
+
'run lint',
|
|
87
|
+
'run typecheck',
|
|
88
|
+
'run build',
|
|
89
|
+
'run quality gates',
|
|
90
|
+
'update request-log',
|
|
91
|
+
'update request log',
|
|
92
|
+
'update app-map',
|
|
93
|
+
'update app map',
|
|
94
|
+
'update decisions',
|
|
95
|
+
'update progress',
|
|
96
|
+
'commit changes',
|
|
97
|
+
'push changes',
|
|
98
|
+
'commit and push',
|
|
99
|
+
'verify changes',
|
|
100
|
+
'review code',
|
|
101
|
+
'mark as complete',
|
|
102
|
+
'close task'
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if TodoWrite gate should be enforced
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
function isTodoWriteGateEnabled() {
|
|
110
|
+
const config = getConfig();
|
|
111
|
+
|
|
112
|
+
// Check hooks config first
|
|
113
|
+
if (config.hooks?.rules?.todoWriteGate?.enabled === false) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fall back to enforcement config
|
|
118
|
+
if (config.enforcement?.strictMode === false) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a single todo item is a tracking todo (always allowed)
|
|
127
|
+
* @param {string} content - Todo content
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
function isTrackingTodo(content) {
|
|
131
|
+
if (!content || typeof content !== 'string') return false;
|
|
132
|
+
|
|
133
|
+
const normalizedContent = content.toLowerCase().trim();
|
|
134
|
+
|
|
135
|
+
// Check explicit allowlist first
|
|
136
|
+
for (const allowed of ALWAYS_ALLOWED_TODOS) {
|
|
137
|
+
if (normalizedContent === allowed || normalizedContent.startsWith(allowed)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check tracking patterns
|
|
143
|
+
return TRACKING_TODO_PATTERNS.some(pattern => pattern.test(content));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if a single todo item is an implementation todo (requires active task)
|
|
148
|
+
* @param {string} content - Todo content
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
function isImplementationTodo(content) {
|
|
152
|
+
if (!content || typeof content !== 'string') return false;
|
|
153
|
+
|
|
154
|
+
// If it's a tracking todo, it's NOT an implementation todo
|
|
155
|
+
if (isTrackingTodo(content)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check implementation patterns
|
|
160
|
+
return IMPLEMENTATION_TODO_PATTERNS.some(pattern => pattern.test(content));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Classify a todo item
|
|
165
|
+
* @param {Object} todo - Todo object with content and status
|
|
166
|
+
* @returns {{type: string, reason: string}}
|
|
167
|
+
*/
|
|
168
|
+
function classifyTodo(todo) {
|
|
169
|
+
if (!todo || !todo.content) {
|
|
170
|
+
return { type: 'unknown', reason: 'no_content' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const content = todo.content;
|
|
174
|
+
|
|
175
|
+
// Completed todos are always allowed (just tracking completion)
|
|
176
|
+
if (todo.status === 'completed') {
|
|
177
|
+
return { type: 'tracking', reason: 'completed_status' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check if tracking
|
|
181
|
+
if (isTrackingTodo(content)) {
|
|
182
|
+
return { type: 'tracking', reason: 'tracking_pattern' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check if implementation
|
|
186
|
+
if (isImplementationTodo(content)) {
|
|
187
|
+
return { type: 'implementation', reason: 'implementation_pattern' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Default to allowed (unknown todos don't block)
|
|
191
|
+
return { type: 'unknown', reason: 'no_pattern_match' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check TodoWrite gate for a TodoWrite call
|
|
196
|
+
*
|
|
197
|
+
* @param {Object} options
|
|
198
|
+
* @param {Array} options.todos - Array of todo items [{content, status, activeForm}]
|
|
199
|
+
* @returns {Object} Result: { allowed, blocked, message, reason, implementationTodos, trackingTodos }
|
|
200
|
+
*/
|
|
201
|
+
function checkTodoWriteGate(options = {}) {
|
|
202
|
+
const { todos } = options;
|
|
203
|
+
|
|
204
|
+
// No todos - allow
|
|
205
|
+
if (!todos || !Array.isArray(todos) || todos.length === 0) {
|
|
206
|
+
return {
|
|
207
|
+
allowed: true,
|
|
208
|
+
blocked: false,
|
|
209
|
+
message: null,
|
|
210
|
+
reason: 'no_todos'
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if gate is enabled
|
|
215
|
+
if (!isTodoWriteGateEnabled()) {
|
|
216
|
+
return {
|
|
217
|
+
allowed: true,
|
|
218
|
+
blocked: false,
|
|
219
|
+
message: null,
|
|
220
|
+
reason: 'gate_disabled'
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Classify all todos
|
|
225
|
+
const implementationTodos = [];
|
|
226
|
+
const trackingTodos = [];
|
|
227
|
+
const unknownTodos = [];
|
|
228
|
+
|
|
229
|
+
for (const todo of todos) {
|
|
230
|
+
const classification = classifyTodo(todo);
|
|
231
|
+
if (classification.type === 'implementation') {
|
|
232
|
+
implementationTodos.push({ ...todo, classification });
|
|
233
|
+
} else if (classification.type === 'tracking') {
|
|
234
|
+
trackingTodos.push({ ...todo, classification });
|
|
235
|
+
} else {
|
|
236
|
+
unknownTodos.push({ ...todo, classification });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If no implementation todos, allow
|
|
241
|
+
if (implementationTodos.length === 0) {
|
|
242
|
+
return {
|
|
243
|
+
allowed: true,
|
|
244
|
+
blocked: false,
|
|
245
|
+
message: null,
|
|
246
|
+
reason: 'no_implementation_todos',
|
|
247
|
+
trackingTodos: trackingTodos.map(t => t.content),
|
|
248
|
+
unknownTodos: unknownTodos.map(t => t.content)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Has implementation todos - check for active task
|
|
253
|
+
const activeTask = getActiveTask();
|
|
254
|
+
|
|
255
|
+
if (activeTask) {
|
|
256
|
+
return {
|
|
257
|
+
allowed: true,
|
|
258
|
+
blocked: false,
|
|
259
|
+
message: null,
|
|
260
|
+
task: activeTask,
|
|
261
|
+
reason: 'task_active',
|
|
262
|
+
implementationTodos: implementationTodos.map(t => t.content),
|
|
263
|
+
trackingTodos: trackingTodos.map(t => t.content)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// No active task and has implementation todos - check if blocking is enabled
|
|
268
|
+
const config = getConfig();
|
|
269
|
+
// Default to blocking (true), only disable if explicitly set to false
|
|
270
|
+
const blockingEnabled = config.hooks?.rules?.todoWriteGate?.blockImplementationWithoutTask !== false;
|
|
271
|
+
|
|
272
|
+
if (blockingEnabled) {
|
|
273
|
+
// Hard block mode
|
|
274
|
+
return {
|
|
275
|
+
allowed: false,
|
|
276
|
+
blocked: true,
|
|
277
|
+
message: generateBlockMessage(implementationTodos, trackingTodos),
|
|
278
|
+
reason: 'no_active_task',
|
|
279
|
+
implementationTodos: implementationTodos.map(t => t.content),
|
|
280
|
+
trackingTodos: trackingTodos.map(t => t.content)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Warn-only mode (blockImplementationWithoutTask explicitly set to false)
|
|
285
|
+
return {
|
|
286
|
+
allowed: true,
|
|
287
|
+
blocked: false,
|
|
288
|
+
message: generateWarningMessage(implementationTodos, trackingTodos),
|
|
289
|
+
reason: 'warn_only',
|
|
290
|
+
implementationTodos: implementationTodos.map(t => t.content),
|
|
291
|
+
trackingTodos: trackingTodos.map(t => t.content)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Generate warning message
|
|
297
|
+
*/
|
|
298
|
+
function generateWarningMessage(implementationTodos, trackingTodos) {
|
|
299
|
+
return `Warning: TodoWrite contains implementation tasks but no WogiFlow task is active.
|
|
300
|
+
|
|
301
|
+
Implementation todos detected:
|
|
302
|
+
${implementationTodos.slice(0, 5).map(t => ` - ${t.content}`).join('\n')}
|
|
303
|
+
|
|
304
|
+
Consider using /wogi-story to create a proper task.
|
|
305
|
+
|
|
306
|
+
Tracking todos (always allowed):
|
|
307
|
+
${trackingTodos.length > 0 ? trackingTodos.slice(0, 3).map(t => ` - ${t.content}`).join('\n') : ' (none)'}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generate block message
|
|
312
|
+
*/
|
|
313
|
+
function generateBlockMessage(implementationTodos, _trackingTodos) {
|
|
314
|
+
const implList = implementationTodos.slice(0, 5).map(t => ` - ${t.content}`).join('\n');
|
|
315
|
+
|
|
316
|
+
return `BLOCKED: TodoWrite contains implementation tasks but no WogiFlow task is active.
|
|
317
|
+
|
|
318
|
+
Implementation todos detected:
|
|
319
|
+
${implList}
|
|
320
|
+
|
|
321
|
+
Use WogiFlow instead:
|
|
322
|
+
1. /wogi-ready - see available tasks
|
|
323
|
+
2. /wogi-start wf-XXXXXXXX - start an existing task
|
|
324
|
+
3. /wogi-story "description" - create a new task with these items
|
|
325
|
+
|
|
326
|
+
Tracking todos (always allowed):
|
|
327
|
+
- Run tests, Run lint, Run typecheck
|
|
328
|
+
- Update request-log, Update app-map
|
|
329
|
+
- Commit changes, Push changes
|
|
330
|
+
- Verify, Review, Mark as complete
|
|
331
|
+
|
|
332
|
+
Why? WogiFlow ensures:
|
|
333
|
+
- Acceptance criteria are tracked
|
|
334
|
+
- Changes are logged
|
|
335
|
+
- Quality gates are enforced`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = {
|
|
339
|
+
isTodoWriteGateEnabled,
|
|
340
|
+
isTrackingTodo,
|
|
341
|
+
isImplementationTodo,
|
|
342
|
+
classifyTodo,
|
|
343
|
+
checkTodoWriteGate,
|
|
344
|
+
generateWarningMessage,
|
|
345
|
+
generateBlockMessage,
|
|
346
|
+
IMPLEMENTATION_TODO_PATTERNS,
|
|
347
|
+
TRACKING_TODO_PATTERNS,
|
|
348
|
+
ALWAYS_ALLOWED_TODOS
|
|
349
|
+
};
|
|
@@ -3,23 +3,56 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Wogi Flow - Claude Code PreToolUse Hook
|
|
5
5
|
*
|
|
6
|
-
* Called before Edit/Write tool execution.
|
|
7
|
-
* Enforces task gating
|
|
6
|
+
* Called before Edit/Write/TodoWrite tool execution.
|
|
7
|
+
* Enforces task gating, component reuse checking, and TodoWrite gating.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { checkTaskGate } = require('../../core/task-gate');
|
|
11
11
|
const { checkComponentReuse } = require('../../core/component-check');
|
|
12
|
+
const { checkTodoWriteGate } = require('../../core/todowrite-gate');
|
|
12
13
|
const { claudeCodeAdapter } = require('../../adapters/claude-code');
|
|
13
14
|
|
|
15
|
+
// Maximum stdin size to prevent DoS (100KB should be enough for tool inputs)
|
|
16
|
+
const MAX_STDIN_SIZE = 100 * 1024;
|
|
17
|
+
|
|
14
18
|
async function main() {
|
|
15
19
|
try {
|
|
16
|
-
// Read input from stdin
|
|
20
|
+
// Read input from stdin with size limit
|
|
17
21
|
let inputData = '';
|
|
22
|
+
let totalSize = 0;
|
|
18
23
|
for await (const chunk of process.stdin) {
|
|
24
|
+
totalSize += chunk.length;
|
|
25
|
+
if (totalSize > MAX_STDIN_SIZE) {
|
|
26
|
+
inputData += chunk.slice(0, MAX_STDIN_SIZE - (totalSize - chunk.length));
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
19
29
|
inputData += chunk;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
// Handle empty input gracefully
|
|
33
|
+
if (!inputData || inputData.trim().length === 0) {
|
|
34
|
+
console.log(JSON.stringify({
|
|
35
|
+
continue: true,
|
|
36
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' }
|
|
37
|
+
}));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse JSON safely
|
|
43
|
+
let input;
|
|
44
|
+
try {
|
|
45
|
+
input = JSON.parse(inputData);
|
|
46
|
+
} catch (_parseErr) {
|
|
47
|
+
// Invalid JSON - allow through (graceful degradation)
|
|
48
|
+
console.log(JSON.stringify({
|
|
49
|
+
continue: true,
|
|
50
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' }
|
|
51
|
+
}));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
const parsedInput = claudeCodeAdapter.parseInput(input);
|
|
24
57
|
|
|
25
58
|
const toolName = parsedInput.toolName;
|
|
@@ -44,6 +77,20 @@ async function main() {
|
|
|
44
77
|
}
|
|
45
78
|
}
|
|
46
79
|
|
|
80
|
+
// TodoWrite gating check (for TodoWrite)
|
|
81
|
+
if (toolName === 'TodoWrite') {
|
|
82
|
+
const todos = toolInput.todos || [];
|
|
83
|
+
coreResult = checkTodoWriteGate({ todos });
|
|
84
|
+
|
|
85
|
+
// If blocked by TodoWrite gating, return early
|
|
86
|
+
if (coreResult.blocked) {
|
|
87
|
+
const output = claudeCodeAdapter.transformResult('PreToolUse', coreResult);
|
|
88
|
+
console.log(JSON.stringify(output));
|
|
89
|
+
process.exit(0);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
47
94
|
// Component reuse check (for Write only)
|
|
48
95
|
if (toolName === 'Write' && filePath) {
|
|
49
96
|
const componentResult = checkComponentReuse({
|
|
@@ -70,9 +117,14 @@ async function main() {
|
|
|
70
117
|
console.log(JSON.stringify(output));
|
|
71
118
|
process.exit(0);
|
|
72
119
|
} catch (err) {
|
|
73
|
-
// Non-blocking error - allow operation to continue
|
|
74
|
-
|
|
75
|
-
|
|
120
|
+
// Non-blocking error - allow operation to continue (graceful degradation)
|
|
121
|
+
// Log generic message to avoid leaking sensitive path information
|
|
122
|
+
if (process.env.DEBUG) {
|
|
123
|
+
console.error(`[Wogi Flow Hook Error] ${err.message}`);
|
|
124
|
+
} else {
|
|
125
|
+
console.error('[Wogi Flow Hook] Validation error occurred');
|
|
126
|
+
}
|
|
127
|
+
// Exit 0 with allow to not block on hook errors
|
|
76
128
|
console.log(JSON.stringify({
|
|
77
129
|
continue: true,
|
|
78
130
|
hookSpecificOutput: {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Claude Code UserPromptSubmit Hook
|
|
5
|
+
*
|
|
6
|
+
* Called when user submits a prompt (before processing).
|
|
7
|
+
* Enforces implementation gate - blocks implementation requests without active task.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { checkImplementationGate } = require('../../core/implementation-gate');
|
|
11
|
+
const { claudeCodeAdapter } = require('../../adapters/claude-code');
|
|
12
|
+
|
|
13
|
+
// Maximum stdin size to prevent DoS (100KB should be more than enough for prompts)
|
|
14
|
+
const MAX_STDIN_SIZE = 100 * 1024;
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
try {
|
|
18
|
+
// Read input from stdin with size limit
|
|
19
|
+
let inputData = '';
|
|
20
|
+
let totalSize = 0;
|
|
21
|
+
for await (const chunk of process.stdin) {
|
|
22
|
+
totalSize += chunk.length;
|
|
23
|
+
if (totalSize > MAX_STDIN_SIZE) {
|
|
24
|
+
// Truncate at limit to prevent memory exhaustion
|
|
25
|
+
inputData += chunk.slice(0, MAX_STDIN_SIZE - (totalSize - chunk.length));
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
inputData += chunk;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle empty input gracefully
|
|
32
|
+
if (!inputData || inputData.trim().length === 0) {
|
|
33
|
+
console.log(JSON.stringify({ continue: true, hookSpecificOutput: { hookEventName: 'UserPromptSubmit' } }));
|
|
34
|
+
process.exit(0);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse JSON safely
|
|
39
|
+
let input;
|
|
40
|
+
try {
|
|
41
|
+
input = JSON.parse(inputData);
|
|
42
|
+
} catch (_parseErr) {
|
|
43
|
+
// Invalid JSON - allow through (graceful degradation)
|
|
44
|
+
console.log(JSON.stringify({ continue: true, hookSpecificOutput: { hookEventName: 'UserPromptSubmit' } }));
|
|
45
|
+
process.exit(0);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parsedInput = claudeCodeAdapter.parseInput(input);
|
|
50
|
+
|
|
51
|
+
const prompt = parsedInput.prompt;
|
|
52
|
+
const source = parsedInput.source;
|
|
53
|
+
|
|
54
|
+
// Check implementation gate
|
|
55
|
+
const coreResult = checkImplementationGate({
|
|
56
|
+
prompt,
|
|
57
|
+
source
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Transform to Claude Code format
|
|
61
|
+
const output = claudeCodeAdapter.transformResult('UserPromptSubmit', coreResult);
|
|
62
|
+
|
|
63
|
+
// Output JSON
|
|
64
|
+
console.log(JSON.stringify(output));
|
|
65
|
+
process.exit(0);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Non-blocking error - allow prompt to continue (graceful degradation)
|
|
68
|
+
// Log generic message to avoid leaking sensitive path information
|
|
69
|
+
if (process.env.DEBUG) {
|
|
70
|
+
console.error(`[Wogi Flow Hook Error] ${err.message}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.error('[Wogi Flow Hook] Validation error occurred');
|
|
73
|
+
}
|
|
74
|
+
// Exit 0 with continue:true to not block on hook errors
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
continue: true,
|
|
77
|
+
hookSpecificOutput: {
|
|
78
|
+
hookEventName: 'UserPromptSubmit'
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handle stdin properly
|
|
86
|
+
process.stdin.setEncoding('utf8');
|
|
87
|
+
main();
|