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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -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
- // PreToolUse hooks for Edit/Write
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
- if (rules.taskGating?.enabled !== false) {
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 and component reuse checking.
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
- const input = inputData ? JSON.parse(inputData) : {};
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
- console.error(`[Wogi Flow Hook Error] ${err.message}`);
75
- // Exit 0 with allow to not block on hook errors (graceful degradation)
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();