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.
@@ -115,6 +115,6 @@
115
115
  ]
116
116
  },
117
117
  "_wogiFlowManaged": true,
118
- "_wogiFlowVersion": "1.0.0",
118
+ "_wogiFlowVersion": "1.4.5",
119
119
  "_comment": "Shared WogiFlow hook configuration. Committed to repo for team use. User-specific overrides go in settings.local.json."
120
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -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 - prevent prompt processing with clear message
337
+ // Blocked - reject the prompt using top-level decision field
330
338
  if (coreResult.blocked) {
331
339
  return {
332
- continue: false, // Block the prompt
333
- systemMessage: coreResult.message || 'Implementation request blocked by Wogi Flow',
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 show message
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
- decision: 'warn',
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
- * Detects implementation requests from user prompts and blocks them
7
- * if no active task exists. Guides users to /wogi-story or /wogi-start.
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
- // Short prompts that are questions are exploratory
210
- // Check length BEFORE calling trim() to avoid processing long strings
211
- const isQuestion = prompt.length < 200 && prompt.trim().endsWith('?');
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 || isQuestion;
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, confidence, suggestedAction }
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
- // Truncate overly long prompts to prevent DoS via regex processing
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
- // Exploration requests always allowed (use truncated prompt for safety)
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 and implementation intent detected
334
- // v4.2: Use 'mode' config as canonical control (softMode is deprecated fallback)
335
- let config;
336
- try {
337
- config = getConfig();
338
- // Defensive: ensure config is an object
339
- if (!config || typeof config !== 'object') {
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: false,
382
- blocked: true,
383
- message: generateBlockingMessage(prompt),
384
- reason: 'route_to_wogi_start',
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 warning message (soft mode)
394
- */
395
- function generateWarningMessage(prompt) {
396
- return `Warning: No active WogiFlow task.
397
-
398
- Consider: /wogi-start "${truncatePrompt(prompt)}"
399
-
400
- This will execute directly (git/npm/deploy) or create a story first (features/fixes).`;
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 generateRoutingMessage(prompt) {
408
- return `Route this request through /wogi-start.
339
+ function generateRoutingContext(prompt) {
340
+ return `MANDATORY ROUTING NO ACTIVE WOGIFLOW TASK
409
341
 
410
- Use: /wogi-start "${truncatePrompt(prompt)}"
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
- /wogi-start will intelligently decide:
413
- - Execute directly if operational (git, npm, deploy, review, commit)
414
- - Create a story first if implementation (add feature, fix bug, refactor)`;
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
- To proceed, run:
432
- /wogi-start "${truncatePrompt(prompt)}"
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 will triage and decide:
435
- - If operational (git/npm/deploy) execute directly
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
- generateWarningMessage,
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 testing and extension)
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 = JSON.parse(inputData);
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
- // Invalid JSON - allow through (graceful degradation)
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
- // Non-blocking error - allow operation to continue (graceful degradation)
205
- // Log generic message to avoid leaking sensitive path information
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: 'allow'
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
- main();
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
- // Non-blocking error - allow prompt to continue (graceful degradation)
158
- // Log generic message to avoid leaking sensitive path information
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
- continue: true,
167
- hookSpecificOutput: {
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
- // Catch any unhandled errors from main
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
- continue: true,
191
- hookSpecificOutput: { hookEventName: 'UserPromptSubmit' }
186
+ decision: 'block',
187
+ reason: 'WogiFlow hook error. Use /wogi-start to route your request.'
192
188
  }));
193
189
  process.exit(0);
194
190
  }
@@ -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
- * Uses merge mode: new commands are added, existing ones are NOT overwritten
162
- * This preserves user customizations while ensuring new commands are available
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 (essential for slash commands to work)
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
- const alreadyExists = fs.existsSync(projectCommands);
174
- copyDir(packageCommands, projectCommands, alreadyExists);
173
+ copyDir(packageCommands, projectCommands, false);
175
174
  }
176
175
 
177
- // Copy docs (knowledge base) - merge mode
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
- const alreadyExists = fs.existsSync(projectDocs);
182
- copyDir(packageDocs, projectDocs, alreadyExists);
180
+ copyDir(packageDocs, projectDocs, false);
183
181
  }
184
182
 
185
- // Copy rules (coding patterns) - merge mode
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
- const alreadyExists = fs.existsSync(projectRules);
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
- // Without this file, Claude Code has no idea hooks exist and won't run them
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
- // Merge: inject our hooks into existing settings without overwriting other config
196
+ // Always merge hooks from package into existing settings
200
197
  try {
201
198
  const existing = JSON.parse(fs.readFileSync(projectSettings, 'utf-8'));
202
- // Only merge if not already WogiFlow-managed (avoid duplicate hooks)
203
- if (!existing._wogiFlowManaged) {
204
- const ours = JSON.parse(fs.readFileSync(packageSettings, 'utf-8'));
205
- existing.hooks = ours.hooks;
206
- existing._wogiFlowManaged = true;
207
- existing._wogiFlowVersion = ours._wogiFlowVersion || '1.0.0';
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
- * Uses merge mode: new scripts are added, existing ones are NOT overwritten
235
- * This preserves user customizations while ensuring new scripts are available
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
- // Use merge mode to add new scripts without overwriting customizations
249
- const alreadyExists = fs.existsSync(projectScripts);
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);