wogiflow 1.4.4 → 1.4.5
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/.claude/settings.json +1 -1
- package/package.json +1 -1
- package/scripts/hooks/adapters/claude-code.js +15 -22
- package/scripts/hooks/core/implementation-gate.js +70 -154
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +38 -8
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +7 -11
- package/scripts/postinstall.js +21 -28
- package/scripts/test-hook-chain.js +184 -0
package/.claude/settings.json
CHANGED
package/package.json
CHANGED
|
@@ -324,26 +324,27 @@ Run: /wogi-start ${coreResult.nextTaskId}`;
|
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
326
|
* Transform UserPromptSubmit result (implementation gate + research gate)
|
|
327
|
+
*
|
|
328
|
+
* Claude Code UserPromptSubmit response format:
|
|
329
|
+
* Block: { decision: "block", reason: "..." } (top-level fields)
|
|
330
|
+
* Allow: {} or omit decision
|
|
331
|
+
* Context: { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: "..." } }
|
|
332
|
+
*
|
|
333
|
+
* NOTE: "continue: false" stops the entire session, NOT the individual prompt.
|
|
334
|
+
* Use "decision: block" to reject a single prompt.
|
|
327
335
|
*/
|
|
328
336
|
transformUserPromptSubmit(coreResult) {
|
|
329
|
-
// Blocked -
|
|
337
|
+
// Blocked - reject the prompt using top-level decision field
|
|
330
338
|
if (coreResult.blocked) {
|
|
331
339
|
return {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
hookSpecificOutput: {
|
|
335
|
-
hookEventName: 'UserPromptSubmit',
|
|
336
|
-
decision: 'block',
|
|
337
|
-
reason: coreResult.reason,
|
|
338
|
-
suggestedAction: coreResult.suggestedAction
|
|
339
|
-
}
|
|
340
|
+
decision: 'block',
|
|
341
|
+
reason: coreResult.message || 'Implementation request blocked by Wogi Flow'
|
|
340
342
|
};
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
// Research protocol triggered - inject protocol steps as additional context
|
|
344
346
|
if (coreResult.systemReminder) {
|
|
345
347
|
return {
|
|
346
|
-
continue: true,
|
|
347
348
|
hookSpecificOutput: {
|
|
348
349
|
hookEventName: 'UserPromptSubmit',
|
|
349
350
|
additionalContext: coreResult.systemReminder
|
|
@@ -351,26 +352,18 @@ Run: /wogi-start ${coreResult.nextTaskId}`;
|
|
|
351
352
|
};
|
|
352
353
|
}
|
|
353
354
|
|
|
354
|
-
// Warning - allow but
|
|
355
|
+
// Warning - allow but inject context with the warning message
|
|
355
356
|
if (coreResult.message && !coreResult.blocked) {
|
|
356
357
|
return {
|
|
357
|
-
continue: true,
|
|
358
|
-
systemMessage: coreResult.message,
|
|
359
358
|
hookSpecificOutput: {
|
|
360
359
|
hookEventName: 'UserPromptSubmit',
|
|
361
|
-
|
|
362
|
-
reason: coreResult.reason
|
|
360
|
+
additionalContext: coreResult.message
|
|
363
361
|
}
|
|
364
362
|
};
|
|
365
363
|
}
|
|
366
364
|
|
|
367
|
-
// Allowed
|
|
368
|
-
return {
|
|
369
|
-
continue: true,
|
|
370
|
-
hookSpecificOutput: {
|
|
371
|
-
hookEventName: 'UserPromptSubmit'
|
|
372
|
-
}
|
|
373
|
-
};
|
|
365
|
+
// Allowed - empty response means allow
|
|
366
|
+
return {};
|
|
374
367
|
}
|
|
375
368
|
|
|
376
369
|
/**
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Wogi Flow - Implementation Gate (Core Module)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Routes all user prompts through /wogi-start when no active task exists.
|
|
7
|
+
* Instead of blocking, injects context that makes Claude automatically
|
|
8
|
+
* invoke /wogi-start which handles AI-based routing (questions, bugs,
|
|
9
|
+
* features, operational tasks, etc.).
|
|
8
10
|
*
|
|
9
11
|
* Returns a standardized result that adapters transform for specific CLIs.
|
|
10
12
|
*/
|
|
@@ -51,10 +53,12 @@ const IMPLEMENTATION_PATTERNS = [
|
|
|
51
53
|
* These should NOT trigger the gate
|
|
52
54
|
*/
|
|
53
55
|
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,
|
|
56
|
+
/\bwhat\s+(does|is|are|do|did|should|would|could|can)\b/i,
|
|
57
|
+
/\bhow\s+(does|do|can|to|would|should|could|did)\b/i,
|
|
58
|
+
/\bwhy\s+(does|do|is|are|did|didn't|doesn't|don't|isn't|aren't|can't|won't|wouldn't|shouldn't|couldn't|hasn't|haven't|wasn't|weren't)\b/i,
|
|
59
|
+
/\bwhere\s+(is|are|do|does|can|did|should)\b/i,
|
|
60
|
+
/\bwho\s+(is|are|does|did|should|can)\b/i,
|
|
61
|
+
/\bwhen\s+(does|do|did|is|are|should|will)\b/i,
|
|
58
62
|
/\bshow\s+me\b/i,
|
|
59
63
|
/\bexplain\b/i,
|
|
60
64
|
/\bdescribe\b/i,
|
|
@@ -67,7 +71,9 @@ const EXPLORATION_PATTERNS = [
|
|
|
67
71
|
/\banalyze\b/i,
|
|
68
72
|
/\breview\s+(the|this|my)/i,
|
|
69
73
|
/\bcheck\s+(if|whether|the)/i,
|
|
70
|
-
/\bcan\s+(claude|you)\s+(access|read|see)/i
|
|
74
|
+
/\bcan\s+(claude|you)\s+(access|read|see)/i,
|
|
75
|
+
/\bit'?s\s+supposed\s+to\b/i,
|
|
76
|
+
/\bisn'?t\s+(it|that|this)\b/i
|
|
71
77
|
];
|
|
72
78
|
|
|
73
79
|
/**
|
|
@@ -176,14 +182,6 @@ function isImplementationGateEnabled() {
|
|
|
176
182
|
return true;
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
// NOTE: softMode is deprecated. Use hooks.rules.implementationGate.mode = 'warn' instead.
|
|
180
|
-
// Kept for backwards compatibility - maps to mode='warn'
|
|
181
|
-
function isSoftModeEnabled() {
|
|
182
|
-
const config = getConfig();
|
|
183
|
-
// Check legacy softMode, map to mode='warn'
|
|
184
|
-
return config.hooks?.rules?.implementationGate?.softMode === true ||
|
|
185
|
-
config.enforcement?.softMode === true;
|
|
186
|
-
}
|
|
187
185
|
|
|
188
186
|
/**
|
|
189
187
|
* Detect if prompt is a WogiFlow command (always allowed)
|
|
@@ -206,11 +204,12 @@ function isExplorationRequest(prompt) {
|
|
|
206
204
|
// Check if it matches exploration patterns
|
|
207
205
|
const matchesExploration = matchesAnyPattern(prompt, EXPLORATION_PATTERNS);
|
|
208
206
|
|
|
209
|
-
//
|
|
210
|
-
// Check
|
|
211
|
-
|
|
207
|
+
// Prompts containing question marks are likely exploratory
|
|
208
|
+
// Check for '?' anywhere in the prompt (not just at end - multi-sentence prompts
|
|
209
|
+
// often have the question mid-text followed by additional context)
|
|
210
|
+
const hasQuestionMark = prompt.length < 500 && prompt.includes('?');
|
|
212
211
|
|
|
213
|
-
return matchesExploration ||
|
|
212
|
+
return matchesExploration || hasQuestionMark;
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
/**
|
|
@@ -250,10 +249,19 @@ function detectImplementationIntent(prompt) {
|
|
|
250
249
|
/**
|
|
251
250
|
* Check implementation gate for a user prompt
|
|
252
251
|
*
|
|
252
|
+
* v5.2: Simplified to a binary check: active task or not.
|
|
253
|
+
* No regex classification - /wogi-start handles routing with AI understanding.
|
|
254
|
+
*
|
|
255
|
+
* Flow:
|
|
256
|
+
* 1. Active task exists → allow (Claude works on the task)
|
|
257
|
+
* 2. /wogi-* command → allow (always pass through)
|
|
258
|
+
* 3. No active task → block (user must route through /wogi-start)
|
|
259
|
+
* 4. /wogi-start handles routing: questions proceed, implementation creates tasks
|
|
260
|
+
*
|
|
253
261
|
* @param {Object} options
|
|
254
262
|
* @param {string} options.prompt - User's input prompt
|
|
255
263
|
* @param {string} [options.source] - Source of prompt (manual, paste, etc.)
|
|
256
|
-
* @returns {Object} Result: { allowed, blocked, message, reason
|
|
264
|
+
* @returns {Object} Result: { allowed, blocked, message, reason }
|
|
257
265
|
*/
|
|
258
266
|
function checkImplementationGate(options = {}) {
|
|
259
267
|
const { prompt } = options;
|
|
@@ -268,12 +276,7 @@ function checkImplementationGate(options = {}) {
|
|
|
268
276
|
};
|
|
269
277
|
}
|
|
270
278
|
|
|
271
|
-
//
|
|
272
|
-
const processedPrompt = prompt.length > MAX_PROMPT_LENGTH
|
|
273
|
-
? prompt.slice(0, MAX_PROMPT_LENGTH)
|
|
274
|
-
: prompt;
|
|
275
|
-
|
|
276
|
-
// WogiFlow commands always allowed (check original prompt for commands)
|
|
279
|
+
// WogiFlow commands always allowed (/wogi-start handles routing)
|
|
277
280
|
if (isWogiCommand(prompt)) {
|
|
278
281
|
return {
|
|
279
282
|
allowed: true,
|
|
@@ -293,29 +296,7 @@ function checkImplementationGate(options = {}) {
|
|
|
293
296
|
};
|
|
294
297
|
}
|
|
295
298
|
|
|
296
|
-
//
|
|
297
|
-
if (isExplorationRequest(processedPrompt)) {
|
|
298
|
-
return {
|
|
299
|
-
allowed: true,
|
|
300
|
-
blocked: false,
|
|
301
|
-
message: null,
|
|
302
|
-
reason: 'exploration_request'
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Check for implementation intent (use truncated prompt for safety)
|
|
307
|
-
const { isImplementation, confidence, matches } = detectImplementationIntent(processedPrompt);
|
|
308
|
-
|
|
309
|
-
if (!isImplementation) {
|
|
310
|
-
return {
|
|
311
|
-
allowed: true,
|
|
312
|
-
blocked: false,
|
|
313
|
-
message: null,
|
|
314
|
-
reason: 'no_implementation_intent'
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Has implementation intent - check for active task
|
|
299
|
+
// Check for active task
|
|
319
300
|
const activeTask = getActiveTask();
|
|
320
301
|
|
|
321
302
|
if (activeTask) {
|
|
@@ -324,117 +305,56 @@ function checkImplementationGate(options = {}) {
|
|
|
324
305
|
blocked: false,
|
|
325
306
|
message: null,
|
|
326
307
|
task: activeTask,
|
|
327
|
-
reason: 'task_active'
|
|
328
|
-
confidence,
|
|
329
|
-
matches
|
|
308
|
+
reason: 'task_active'
|
|
330
309
|
};
|
|
331
310
|
}
|
|
332
311
|
|
|
333
|
-
// No active task
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
config = {};
|
|
341
|
-
}
|
|
342
|
-
} catch (err) {
|
|
343
|
-
// Config load failed - default to warn mode for safety
|
|
344
|
-
if (process.env.DEBUG) {
|
|
345
|
-
console.error(`[Implementation Gate] Config load failed: ${err.message}`);
|
|
346
|
-
}
|
|
347
|
-
config = {};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
let mode = config.hooks?.rules?.implementationGate?.mode;
|
|
351
|
-
|
|
352
|
-
// Backward compatibility: if mode not set, check legacy softMode
|
|
353
|
-
if (!mode) {
|
|
354
|
-
const softMode = isSoftModeEnabled();
|
|
355
|
-
mode = softMode ? 'warn' : 'block';
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (mode === 'off') {
|
|
359
|
-
return {
|
|
360
|
-
allowed: true,
|
|
361
|
-
blocked: false,
|
|
362
|
-
message: null,
|
|
363
|
-
reason: 'gate_mode_off'
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (mode === 'warn') {
|
|
368
|
-
return {
|
|
369
|
-
allowed: true,
|
|
370
|
-
blocked: false,
|
|
371
|
-
message: generateRoutingMessage(prompt),
|
|
372
|
-
reason: 'route_to_wogi_start',
|
|
373
|
-
confidence,
|
|
374
|
-
suggestedAction: 'wogi-start',
|
|
375
|
-
matches
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Default: mode === 'block' - strict enforcement
|
|
312
|
+
// No active task - inject routing context so Claude automatically invokes /wogi-start
|
|
313
|
+
// /wogi-start will use AI understanding to decide:
|
|
314
|
+
// Questions/exploration → proceed directly
|
|
315
|
+
// Operational (git/npm) → execute directly
|
|
316
|
+
// Implementation → create story/task first
|
|
317
|
+
// Research → use zero-trust protocol
|
|
318
|
+
// Bugs → investigate and fix properly
|
|
380
319
|
return {
|
|
381
|
-
allowed:
|
|
382
|
-
blocked:
|
|
383
|
-
|
|
384
|
-
reason: '
|
|
385
|
-
confidence,
|
|
386
|
-
suggestedAction: 'wogi-start',
|
|
387
|
-
suggestedCommand: `/wogi-start "${truncatePrompt(prompt)}"`,
|
|
388
|
-
matches
|
|
320
|
+
allowed: true,
|
|
321
|
+
blocked: false,
|
|
322
|
+
systemReminder: generateRoutingContext(prompt),
|
|
323
|
+
reason: 'no_active_task_route'
|
|
389
324
|
};
|
|
390
325
|
}
|
|
391
326
|
|
|
392
327
|
/**
|
|
393
|
-
* Generate
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Generate routing message - instructs Claude to use /wogi-start
|
|
405
|
-
* /wogi-start will intelligently decide what to do
|
|
328
|
+
* Generate routing context injected as additionalContext when no task is active.
|
|
329
|
+
* This makes Claude automatically invoke /wogi-start via the Skill tool,
|
|
330
|
+
* so the user never needs to type any command manually.
|
|
331
|
+
*
|
|
332
|
+
* /wogi-start handles ALL routing with AI understanding:
|
|
333
|
+
* - Questions → answered directly
|
|
334
|
+
* - Research → zero-trust research protocol
|
|
335
|
+
* - Operational (git, npm) → executed directly
|
|
336
|
+
* - Implementation → creates tracked task first
|
|
337
|
+
* - Bugs → investigation + tracked fix
|
|
406
338
|
*/
|
|
407
|
-
function
|
|
408
|
-
return `
|
|
339
|
+
function generateRoutingContext(prompt) {
|
|
340
|
+
return `MANDATORY ROUTING — NO ACTIVE WOGIFLOW TASK
|
|
409
341
|
|
|
410
|
-
|
|
342
|
+
You MUST invoke the Skill tool with skill="wogi-start" and pass the user's FULL original prompt as args BEFORE doing anything else.
|
|
411
343
|
|
|
412
|
-
|
|
413
|
-
-
|
|
414
|
-
- Create
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
* @deprecated Use generateBlockingMessage instead. Kept for backwards compatibility.
|
|
419
|
-
*/
|
|
420
|
-
function generateBlockMessage(prompt) {
|
|
421
|
-
return generateBlockingMessage(prompt);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Generate blocking message (v4.2 strict enforcement)
|
|
426
|
-
* This message appears when implementation is detected without active task
|
|
427
|
-
*/
|
|
428
|
-
function generateBlockingMessage(prompt) {
|
|
429
|
-
return `Implementation request detected without active task.
|
|
344
|
+
DO NOT:
|
|
345
|
+
- Answer implementation questions directly
|
|
346
|
+
- Create, edit, or write any files
|
|
347
|
+
- Make assumptions about the codebase
|
|
348
|
+
- Research or answer questions on your own (WogiFlow has a zero-trust research protocol)
|
|
349
|
+
- Truncate or summarize the user's prompt
|
|
430
350
|
|
|
431
|
-
|
|
432
|
-
|
|
351
|
+
DO:
|
|
352
|
+
- Immediately invoke: Skill(skill="wogi-start", args="<user's full original prompt>")
|
|
353
|
+
- Let /wogi-start classify and route: questions, research, bugs, features, operational tasks
|
|
354
|
+
- Follow the workflow /wogi-start sets up
|
|
433
355
|
|
|
434
|
-
WogiFlow
|
|
435
|
-
|
|
436
|
-
- If small fix → execute + log for learning
|
|
437
|
-
- If larger task → create story/bug first`;
|
|
356
|
+
The user installed WogiFlow so ALL work goes through proper workflows with guardrails.
|
|
357
|
+
Bypassing this is a workflow violation.`;
|
|
438
358
|
}
|
|
439
359
|
|
|
440
360
|
/**
|
|
@@ -506,7 +426,7 @@ function classifyRequest(prompt) {
|
|
|
506
426
|
}
|
|
507
427
|
|
|
508
428
|
module.exports = {
|
|
509
|
-
// Classification functions
|
|
429
|
+
// Classification functions (used by /wogi-start for routing, not for blocking)
|
|
510
430
|
classifyRequest,
|
|
511
431
|
detectImplementationIntent,
|
|
512
432
|
isExplorationRequest,
|
|
@@ -514,21 +434,17 @@ module.exports = {
|
|
|
514
434
|
|
|
515
435
|
// Gate status functions
|
|
516
436
|
isImplementationGateEnabled,
|
|
517
|
-
isSoftModeEnabled,
|
|
518
437
|
isWogiCommand,
|
|
519
438
|
|
|
520
439
|
// Message generators
|
|
521
|
-
|
|
522
|
-
generateRoutingMessage,
|
|
523
|
-
generateBlockMessage, // @deprecated - use generateBlockingMessage
|
|
524
|
-
generateBlockingMessage,
|
|
440
|
+
generateRoutingContext,
|
|
525
441
|
|
|
526
442
|
// Utilities
|
|
527
443
|
truncatePrompt,
|
|
528
444
|
matchesAnyPattern,
|
|
529
445
|
calculateConfidence,
|
|
530
446
|
|
|
531
|
-
// Pattern arrays (for
|
|
447
|
+
// Pattern arrays (used by classifyRequest for /wogi-start routing)
|
|
532
448
|
IMPLEMENTATION_PATTERNS,
|
|
533
449
|
EXPLORATION_PATTERNS,
|
|
534
450
|
OPERATIONAL_PATTERNS,
|
|
@@ -15,6 +15,7 @@ const { checkComponentReuse } = require('../../core/component-check');
|
|
|
15
15
|
const { checkTodoWriteGate } = require('../../core/todowrite-gate');
|
|
16
16
|
const { claudeCodeAdapter } = require('../../adapters/claude-code');
|
|
17
17
|
const { markSkillPending } = require('../../../flow-durable-session');
|
|
18
|
+
const { safeJsonParseString } = require('../../../flow-utils');
|
|
18
19
|
|
|
19
20
|
// Lazy-load strict adherence to avoid circular deps and startup cost
|
|
20
21
|
let _strictAdherence = null;
|
|
@@ -57,12 +58,21 @@ async function main() {
|
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
// Parse JSON safely
|
|
61
|
+
// Parse JSON safely with prototype pollution protection
|
|
61
62
|
let input;
|
|
62
63
|
try {
|
|
63
|
-
input =
|
|
64
|
+
input = safeJsonParseString(inputData, null);
|
|
65
|
+
if (!input) {
|
|
66
|
+
// Invalid JSON - allow through (graceful degradation)
|
|
67
|
+
console.log(JSON.stringify({
|
|
68
|
+
continue: true,
|
|
69
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' }
|
|
70
|
+
}));
|
|
71
|
+
process.exit(0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
64
74
|
} catch (_parseErr) {
|
|
65
|
-
//
|
|
75
|
+
// Parse error - allow through (graceful degradation)
|
|
66
76
|
console.log(JSON.stringify({
|
|
67
77
|
continue: true,
|
|
68
78
|
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' }
|
|
@@ -201,19 +211,19 @@ async function main() {
|
|
|
201
211
|
console.log(JSON.stringify(output));
|
|
202
212
|
process.exit(0);
|
|
203
213
|
} catch (err) {
|
|
204
|
-
//
|
|
205
|
-
//
|
|
214
|
+
// Fail-closed: deny the tool use on hook errors to prevent untracked edits
|
|
215
|
+
// Users installed WogiFlow to enforce task tracking - failing open would bypass that
|
|
206
216
|
if (process.env.DEBUG) {
|
|
207
217
|
console.error(`[Wogi Flow Hook Error] ${err.message}`);
|
|
208
218
|
} else {
|
|
209
219
|
console.error('[Wogi Flow Hook] Validation error occurred');
|
|
210
220
|
}
|
|
211
|
-
// Exit 0 with allow to not block on hook errors
|
|
212
221
|
console.log(JSON.stringify({
|
|
213
222
|
continue: true,
|
|
214
223
|
hookSpecificOutput: {
|
|
215
224
|
hookEventName: 'PreToolUse',
|
|
216
|
-
permissionDecision: '
|
|
225
|
+
permissionDecision: 'deny',
|
|
226
|
+
permissionDecisionReason: 'WogiFlow validation error. Please check your setup or use /wogi-start.'
|
|
217
227
|
}
|
|
218
228
|
}));
|
|
219
229
|
process.exit(0);
|
|
@@ -222,4 +232,24 @@ async function main() {
|
|
|
222
232
|
|
|
223
233
|
// Handle stdin properly
|
|
224
234
|
process.stdin.setEncoding('utf8');
|
|
225
|
-
|
|
235
|
+
|
|
236
|
+
// Must await async main() to prevent race conditions
|
|
237
|
+
(async () => {
|
|
238
|
+
try {
|
|
239
|
+
await main();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
// Fail-closed: deny on unexpected errors
|
|
242
|
+
if (process.env.DEBUG) {
|
|
243
|
+
console.error(`[Wogi Flow Hook] Unexpected error: ${err.message}`);
|
|
244
|
+
}
|
|
245
|
+
console.log(JSON.stringify({
|
|
246
|
+
continue: true,
|
|
247
|
+
hookSpecificOutput: {
|
|
248
|
+
hookEventName: 'PreToolUse',
|
|
249
|
+
permissionDecision: 'deny',
|
|
250
|
+
permissionDecisionReason: 'WogiFlow hook error. Use /wogi-start to route your request.'
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
})();
|
|
@@ -154,19 +154,16 @@ async function main() {
|
|
|
154
154
|
console.log(JSON.stringify(output));
|
|
155
155
|
process.exit(0);
|
|
156
156
|
} catch (err) {
|
|
157
|
-
//
|
|
158
|
-
//
|
|
157
|
+
// Fail-closed: block the prompt on hook errors to prevent untracked implementation
|
|
158
|
+
// Users installed WogiFlow to enforce task tracking - failing open would bypass that
|
|
159
159
|
if (process.env.DEBUG) {
|
|
160
160
|
console.error(`[Wogi Flow Hook Error] ${err.message}`);
|
|
161
161
|
} else {
|
|
162
162
|
console.error('[Wogi Flow Hook] Validation error occurred');
|
|
163
163
|
}
|
|
164
|
-
// Exit 0 with continue:true to not block on hook errors
|
|
165
164
|
console.log(JSON.stringify({
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
hookEventName: 'UserPromptSubmit'
|
|
169
|
-
}
|
|
165
|
+
decision: 'block',
|
|
166
|
+
reason: 'WogiFlow validation error. Please check your WogiFlow setup or use /wogi-start to route your request.'
|
|
170
167
|
}));
|
|
171
168
|
process.exit(0);
|
|
172
169
|
}
|
|
@@ -181,14 +178,13 @@ process.stdin.setEncoding('utf8');
|
|
|
181
178
|
try {
|
|
182
179
|
await main();
|
|
183
180
|
} catch (err) {
|
|
184
|
-
//
|
|
181
|
+
// Fail-closed: block on unexpected errors to prevent untracked implementation
|
|
185
182
|
if (process.env.DEBUG) {
|
|
186
183
|
console.error(`[Wogi Flow Hook] Unexpected error: ${err.message}`);
|
|
187
184
|
}
|
|
188
|
-
// Exit gracefully - don't block on hook errors
|
|
189
185
|
console.log(JSON.stringify({
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
decision: 'block',
|
|
187
|
+
reason: 'WogiFlow hook error. Use /wogi-start to route your request.'
|
|
192
188
|
}));
|
|
193
189
|
process.exit(0);
|
|
194
190
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -158,56 +158,50 @@ function copyDir(src, dest, mergeMode = false, depth = 0) {
|
|
|
158
158
|
* Copy essential .claude/ resources from package to project
|
|
159
159
|
* This ensures commands are available immediately after npm install
|
|
160
160
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
161
|
+
* ALWAYS overwrites WogiFlow-owned files (commands, docs, rules, settings hooks)
|
|
162
|
+
* to ensure npm update actually applies changes.
|
|
163
|
+
* User-customizable files (config.json, ready.json, decisions.md) are NOT touched.
|
|
163
164
|
*/
|
|
164
165
|
function copyClaudeResources() {
|
|
165
166
|
const claudeDir = path.join(PROJECT_ROOT, '.claude');
|
|
166
167
|
fs.mkdirSync(claudeDir, { recursive: true, mode: DIR_MODE });
|
|
167
168
|
|
|
168
|
-
// Copy commands (
|
|
169
|
-
// Use merge mode to add new commands without overwriting customizations
|
|
169
|
+
// Copy commands (always overwrite - these are WogiFlow skill definitions)
|
|
170
170
|
const packageCommands = path.join(PACKAGE_ROOT, '.claude', 'commands');
|
|
171
171
|
const projectCommands = path.join(claudeDir, 'commands');
|
|
172
172
|
if (fs.existsSync(packageCommands)) {
|
|
173
|
-
|
|
174
|
-
copyDir(packageCommands, projectCommands, alreadyExists);
|
|
173
|
+
copyDir(packageCommands, projectCommands, false);
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
// Copy docs (
|
|
176
|
+
// Copy docs (always overwrite - these are WogiFlow documentation)
|
|
178
177
|
const packageDocs = path.join(PACKAGE_ROOT, '.claude', 'docs');
|
|
179
178
|
const projectDocs = path.join(claudeDir, 'docs');
|
|
180
179
|
if (fs.existsSync(packageDocs)) {
|
|
181
|
-
|
|
182
|
-
copyDir(packageDocs, projectDocs, alreadyExists);
|
|
180
|
+
copyDir(packageDocs, projectDocs, false);
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
// Copy rules (
|
|
183
|
+
// Copy rules (always overwrite - these are WogiFlow coding rules)
|
|
186
184
|
const packageRules = path.join(PACKAGE_ROOT, '.claude', 'rules');
|
|
187
185
|
const projectRules = path.join(claudeDir, 'rules');
|
|
188
186
|
if (fs.existsSync(packageRules)) {
|
|
189
|
-
|
|
190
|
-
copyDir(packageRules, projectRules, alreadyExists);
|
|
187
|
+
copyDir(packageRules, projectRules, false);
|
|
191
188
|
}
|
|
192
189
|
|
|
193
190
|
// Copy settings.json (hook configuration) - ESSENTIAL for hooks to work
|
|
194
|
-
//
|
|
191
|
+
// ALWAYS update hooks section on every install/update to ensure new hook logic applies
|
|
195
192
|
const packageSettings = path.join(PACKAGE_ROOT, '.claude', 'settings.json');
|
|
196
193
|
const projectSettings = path.join(claudeDir, 'settings.json');
|
|
197
194
|
if (fs.existsSync(packageSettings)) {
|
|
198
195
|
if (fs.existsSync(projectSettings)) {
|
|
199
|
-
//
|
|
196
|
+
// Always merge hooks from package into existing settings
|
|
200
197
|
try {
|
|
201
198
|
const existing = JSON.parse(fs.readFileSync(projectSettings, 'utf-8'));
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
fs.writeFileSync(projectSettings, JSON.stringify(existing, null, 2), { mode: FILE_MODE });
|
|
209
|
-
}
|
|
210
|
-
// If already managed, leave as-is (user may have customized hook config)
|
|
199
|
+
const ours = JSON.parse(fs.readFileSync(packageSettings, 'utf-8'));
|
|
200
|
+
// Always update hooks (core WogiFlow functionality)
|
|
201
|
+
existing.hooks = ours.hooks;
|
|
202
|
+
existing._wogiFlowManaged = true;
|
|
203
|
+
existing._wogiFlowVersion = ours._wogiFlowVersion || '1.0.0';
|
|
204
|
+
fs.writeFileSync(projectSettings, JSON.stringify(existing, null, 2), { mode: FILE_MODE });
|
|
211
205
|
} catch (err) {
|
|
212
206
|
// Parse error on existing file - overwrite with ours
|
|
213
207
|
if (process.env.DEBUG) {
|
|
@@ -231,8 +225,8 @@ function copyClaudeResources() {
|
|
|
231
225
|
* Copy scripts from package to project (for npm update scenario)
|
|
232
226
|
* This ensures scripts are updated on npm install/update
|
|
233
227
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
228
|
+
* ALWAYS overwrites WogiFlow-owned scripts to ensure npm update applies changes.
|
|
229
|
+
* Hook scripts, core modules, and adapters must stay in sync with the package version.
|
|
236
230
|
*/
|
|
237
231
|
function copyScriptsFromPackage() {
|
|
238
232
|
const packageScripts = path.join(PACKAGE_ROOT, 'scripts');
|
|
@@ -245,9 +239,8 @@ function copyScriptsFromPackage() {
|
|
|
245
239
|
return;
|
|
246
240
|
}
|
|
247
241
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
copyDir(packageScripts, projectScripts, alreadyExists);
|
|
242
|
+
// Always overwrite scripts to ensure npm update propagates hook/core changes
|
|
243
|
+
copyDir(packageScripts, projectScripts, false);
|
|
251
244
|
|
|
252
245
|
// Make flow script executable
|
|
253
246
|
const flowScript = path.join(projectScripts, 'flow');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Test the full hook chain: implementation-gate + adapter
|
|
4
|
+
*
|
|
5
|
+
* v5.3: Tests context injection (not blocking).
|
|
6
|
+
* When no active task, prompts are ALLOWED through with additionalContext
|
|
7
|
+
* that tells Claude to invoke /wogi-start automatically.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Clear module cache
|
|
11
|
+
const keysToDelete = Object.keys(require.cache).filter(k =>
|
|
12
|
+
k.includes('implementation-gate') || k.includes('task-gate') ||
|
|
13
|
+
k.includes('claude-code') || k.includes('base-adapter') ||
|
|
14
|
+
k.includes('flow-utils')
|
|
15
|
+
);
|
|
16
|
+
keysToDelete.forEach(k => delete require.cache[k]);
|
|
17
|
+
|
|
18
|
+
// Patch getReadyData before loading implementation-gate
|
|
19
|
+
const utils = require('./flow-utils');
|
|
20
|
+
let mockInProgress = [];
|
|
21
|
+
utils.getReadyData = () => ({
|
|
22
|
+
ready: [],
|
|
23
|
+
inProgress: mockInProgress,
|
|
24
|
+
blocked: [],
|
|
25
|
+
recentlyCompleted: []
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Now load the modules
|
|
29
|
+
const { checkImplementationGate } = require('./hooks/core/implementation-gate');
|
|
30
|
+
const { claudeCodeAdapter } = require('./hooks/adapters/claude-code');
|
|
31
|
+
|
|
32
|
+
let allPass = true;
|
|
33
|
+
|
|
34
|
+
function assert(condition, name, details) {
|
|
35
|
+
console.log(condition ? ' PASS' : ' FAIL', '-', name);
|
|
36
|
+
if (!condition) {
|
|
37
|
+
if (details) console.log(' ', details);
|
|
38
|
+
allPass = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ===================================================================
|
|
43
|
+
// NO ACTIVE TASK → Context injection (NOT blocking)
|
|
44
|
+
// Prompts go through with additionalContext telling Claude to invoke /wogi-start
|
|
45
|
+
// ===================================================================
|
|
46
|
+
console.log('=== NO ACTIVE TASK (should inject routing context, NOT block) ===');
|
|
47
|
+
|
|
48
|
+
function testContextInjection(name, prompt) {
|
|
49
|
+
mockInProgress = [];
|
|
50
|
+
const result = checkImplementationGate({ prompt });
|
|
51
|
+
const adapted = claudeCodeAdapter.transformResult('UserPromptSubmit', result);
|
|
52
|
+
|
|
53
|
+
const notBlocked = adapted.decision !== 'block';
|
|
54
|
+
const hasContext = adapted.hookSpecificOutput?.additionalContext?.includes('wogi-start');
|
|
55
|
+
const noContinueFalse = adapted.continue !== false;
|
|
56
|
+
|
|
57
|
+
assert(notBlocked, `${name}: NOT blocked`);
|
|
58
|
+
assert(hasContext, `${name}: has routing context mentioning wogi-start`);
|
|
59
|
+
assert(noContinueFalse, `${name}: does not kill session`);
|
|
60
|
+
|
|
61
|
+
if (!notBlocked || !hasContext || !noContinueFalse) {
|
|
62
|
+
console.log(' Adapted:', JSON.stringify(adapted).slice(0, 200));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
testContextInjection('Implementation request', 'add a logout button');
|
|
67
|
+
testContextInjection('Question', 'what does this function do?');
|
|
68
|
+
testContextInjection('Operational', 'push to github');
|
|
69
|
+
testContextInjection('Random text', 'hello world');
|
|
70
|
+
|
|
71
|
+
// ===================================================================
|
|
72
|
+
// NO ACTIVE TASK → /wogi-* commands always pass clean (no context injection)
|
|
73
|
+
// ===================================================================
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log('=== NO ACTIVE TASK (/wogi-* commands pass clean) ===');
|
|
76
|
+
|
|
77
|
+
function testCleanAllow(name, prompt) {
|
|
78
|
+
mockInProgress = [];
|
|
79
|
+
const result = checkImplementationGate({ prompt });
|
|
80
|
+
const adapted = claudeCodeAdapter.transformResult('UserPromptSubmit', result);
|
|
81
|
+
|
|
82
|
+
const notBlocked = adapted.decision !== 'block';
|
|
83
|
+
const isEmpty = Object.keys(adapted).length === 0;
|
|
84
|
+
|
|
85
|
+
assert(notBlocked, `${name}: NOT blocked`);
|
|
86
|
+
assert(isEmpty, `${name}: clean empty response (no context injection)`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
testCleanAllow('/wogi-start command', '/wogi-start add feature');
|
|
90
|
+
testCleanAllow('/wogi-review command', '/wogi-review');
|
|
91
|
+
testCleanAllow('/wogi-bug command', '/wogi-bug something broken');
|
|
92
|
+
|
|
93
|
+
// ===================================================================
|
|
94
|
+
// EMPTY/NULL PROMPTS → pass clean
|
|
95
|
+
// ===================================================================
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('=== EMPTY/NULL PROMPTS (pass clean) ===');
|
|
98
|
+
|
|
99
|
+
testCleanAllow('Empty prompt', '');
|
|
100
|
+
testCleanAllow('Null prompt', null);
|
|
101
|
+
testCleanAllow('Whitespace only', ' ');
|
|
102
|
+
|
|
103
|
+
// ===================================================================
|
|
104
|
+
// WITH ACTIVE TASK → everything passes clean
|
|
105
|
+
// ===================================================================
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log('=== WITH ACTIVE TASK (everything passes clean) ===');
|
|
108
|
+
|
|
109
|
+
function testWithTask(name, prompt) {
|
|
110
|
+
mockInProgress = [{ id: 'wf-test', title: 'Test task', status: 'in_progress' }];
|
|
111
|
+
const result = checkImplementationGate({ prompt });
|
|
112
|
+
const adapted = claudeCodeAdapter.transformResult('UserPromptSubmit', result);
|
|
113
|
+
|
|
114
|
+
const notBlocked = adapted.decision !== 'block';
|
|
115
|
+
const isEmpty = Object.keys(adapted).length === 0;
|
|
116
|
+
|
|
117
|
+
assert(notBlocked, `${name}: NOT blocked`);
|
|
118
|
+
assert(isEmpty, `${name}: clean empty response`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
testWithTask('Implementation request', 'add a logout button');
|
|
122
|
+
testWithTask('Question', 'what does this do?');
|
|
123
|
+
testWithTask('Operational', 'push to github');
|
|
124
|
+
|
|
125
|
+
// ===================================================================
|
|
126
|
+
// ADAPTER FORMAT VERIFICATION
|
|
127
|
+
// ===================================================================
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log('=== ADAPTER FORMAT - No task (context injection) ===');
|
|
130
|
+
|
|
131
|
+
mockInProgress = [];
|
|
132
|
+
const noTaskResult = checkImplementationGate({ prompt: 'add a feature' });
|
|
133
|
+
const noTaskAdapted = claudeCodeAdapter.transformResult('UserPromptSubmit', noTaskResult);
|
|
134
|
+
|
|
135
|
+
assert(noTaskAdapted.decision !== 'block',
|
|
136
|
+
'No task: does NOT have decision:"block"');
|
|
137
|
+
assert(noTaskAdapted.hookSpecificOutput?.hookEventName === 'UserPromptSubmit',
|
|
138
|
+
'No task: has hookEventName "UserPromptSubmit"');
|
|
139
|
+
assert(typeof noTaskAdapted.hookSpecificOutput?.additionalContext === 'string',
|
|
140
|
+
'No task: has additionalContext string');
|
|
141
|
+
assert(noTaskAdapted.hookSpecificOutput?.additionalContext?.includes('MANDATORY'),
|
|
142
|
+
'No task: context includes MANDATORY instruction');
|
|
143
|
+
assert(noTaskAdapted.hookSpecificOutput?.additionalContext?.includes('wogi-start'),
|
|
144
|
+
'No task: context mentions wogi-start');
|
|
145
|
+
assert(noTaskAdapted.continue !== false,
|
|
146
|
+
'No task: does NOT have continue:false (would kill session)');
|
|
147
|
+
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log('=== ADAPTER FORMAT - With task (clean allow) ===');
|
|
150
|
+
|
|
151
|
+
mockInProgress = [{ id: 'wf-test', title: 'Test', status: 'in_progress' }];
|
|
152
|
+
const withTaskResult = checkImplementationGate({ prompt: 'add a feature' });
|
|
153
|
+
const withTaskAdapted = claudeCodeAdapter.transformResult('UserPromptSubmit', withTaskResult);
|
|
154
|
+
|
|
155
|
+
assert(Object.keys(withTaskAdapted).length === 0,
|
|
156
|
+
'With task: response is empty object {}');
|
|
157
|
+
assert(withTaskAdapted.decision !== 'block',
|
|
158
|
+
'With task: no block decision');
|
|
159
|
+
assert(withTaskAdapted.continue !== false,
|
|
160
|
+
'With task: no continue:false');
|
|
161
|
+
|
|
162
|
+
// ===================================================================
|
|
163
|
+
// CORE RESULT VERIFICATION
|
|
164
|
+
// ===================================================================
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log('=== CORE RESULT SHAPE ===');
|
|
167
|
+
|
|
168
|
+
mockInProgress = [];
|
|
169
|
+
const coreNoTask = checkImplementationGate({ prompt: 'build a dashboard' });
|
|
170
|
+
assert(coreNoTask.allowed === true, 'Core no-task: allowed=true (not blocking)');
|
|
171
|
+
assert(coreNoTask.blocked === false, 'Core no-task: blocked=false');
|
|
172
|
+
assert(typeof coreNoTask.systemReminder === 'string', 'Core no-task: has systemReminder string');
|
|
173
|
+
assert(coreNoTask.reason === 'no_active_task_route', 'Core no-task: reason is no_active_task_route');
|
|
174
|
+
|
|
175
|
+
mockInProgress = [{ id: 'wf-test', title: 'Test', status: 'in_progress' }];
|
|
176
|
+
const coreWithTask = checkImplementationGate({ prompt: 'build a dashboard' });
|
|
177
|
+
assert(coreWithTask.allowed === true, 'Core with-task: allowed=true');
|
|
178
|
+
assert(coreWithTask.blocked === false, 'Core with-task: blocked=false');
|
|
179
|
+
assert(!coreWithTask.systemReminder, 'Core with-task: no systemReminder');
|
|
180
|
+
assert(coreWithTask.reason === 'task_active', 'Core with-task: reason is task_active');
|
|
181
|
+
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(allPass ? 'ALL TESTS PASSED' : 'SOME TESTS FAILED');
|
|
184
|
+
process.exit(allPass ? 0 : 1);
|