wogiflow 2.7.0 → 2.7.1

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.
@@ -36,6 +36,7 @@
36
36
  ],
37
37
  "PostToolUse": [
38
38
  {
39
+ "matcher": "Edit|Write|Bash",
39
40
  "hooks": [
40
41
  {
41
42
  "type": "command",
@@ -132,19 +133,9 @@
132
133
  }
133
134
  ]
134
135
  }
135
- ],
136
- "TaskCreated": [
137
- {
138
- "hooks": [
139
- {
140
- "type": "command",
141
- "command": "node scripts/hooks/entry/claude-code/task-created.js",
142
- "timeout": 5
143
- }
144
- ]
145
- }
146
136
  ]
147
137
  },
138
+ "_comment_dynamicHooks": "TaskCreated (2.1.84+) and PermissionDenied (2.1.88+) are added by postinstall.js when the CC version supports them. They must NOT be committed statically — CC rejects the entire settings file if it encounters an unknown hook event name.",
148
139
  "_wogiFlowManaged": true,
149
140
  "_wogiFlowVersion": "2.4.2",
150
141
  "_comment": "Shared WogiFlow hook configuration. Committed to repo for team use. User-specific overrides go in settings.local.json."
@@ -221,8 +221,8 @@ After completing the task:
221
221
 
222
222
  agentConfig: {
223
223
  description: `${repoName}: ${task.substring(0, 50)}...`,
224
- // The sub-agent should work within the repo directory
225
- // The orchestrator (workspace manager) will read results after completion
224
+ // Named subagent (2.1.88+): shows in @ mention typeahead as @repoName
225
+ name: repoName
226
226
  }
227
227
  };
228
228
  }
@@ -364,6 +364,7 @@ Your job:
364
364
  Be specific about file names, line numbers, and error messages.`,
365
365
  agentConfig: {
366
366
  description: `Investigate: ${name} — ${bugDescription.substring(0, 40)}...`,
367
+ name: `${name}-investigator`,
367
368
  model: 'sonnet' // Use cheaper model for investigation
368
369
  }
369
370
  });
package/lib/workspace.js CHANGED
@@ -637,6 +637,14 @@ ${Object.entries(config.channels?.members || {}).map(([name, ch]) =>
637
637
  \`\`\`
638
638
  Then read responses from \`.workspace/messages/\` and synthesize findings.
639
639
 
640
+ ## Named Workspace Agents (Claude Code 2.1.88+)
641
+
642
+ Workers are named subagents — they appear in the @ mention typeahead. Users can address them directly:
643
+ ${Object.keys(config.channels?.members || {}).map(name => `- **@${name}** — the ${name} repo worker`).join('\n')}
644
+ ${Object.keys(config.channels?.members || {}).map(name => `- **@${name}-investigator** — investigation agent for ${name}`).join('\n')}
645
+
646
+ When spawning agents for delegation, always include the \`name\` field in the Agent config to enable @mention addressing.
647
+
640
648
  ## Waiting for Worker Results (CRITICAL — Automatic Return Path)
641
649
 
642
650
  Workers **automatically write results** to \`.workspace/messages/\` when they complete a task. You do NOT need to ask them to report back — the task-completed hook writes a \`task-complete\` message automatically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Claude Code PermissionDenied Hook
5
+ *
6
+ * Fires after auto-mode classifier denies a tool use.
7
+ * Available in Claude Code 2.1.88+.
8
+ *
9
+ * Handles:
10
+ * 1. Logging — tracks denied permissions for diagnostics
11
+ * 2. Workspace redirect — when a worker tries to access a file in another
12
+ * repo, redirect via the workspace message bus instead of retrying
13
+ * 3. Guidance — provides actionable hints about what to do instead
14
+ *
15
+ * Return { retry: true } to tell the model it can retry the operation.
16
+ * Return { retry: false } (or nothing) to accept the denial.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const path = require('node:path');
22
+ const { runHook } = require('../shared/hook-runner');
23
+
24
+ runHook('PermissionDenied', async ({ input, parsedInput }) => {
25
+ const toolName = parsedInput.toolName || input.tool_name || 'unknown';
26
+ const toolInput = parsedInput.toolInput || input.tool_input || {};
27
+ const filePath = toolInput.file_path || toolInput.command || '';
28
+ const reason = input.denial_reason || input.reason || '';
29
+
30
+ // ── 1. Log the denial for diagnostics ──────────────────────
31
+ try {
32
+ const fs = require('node:fs');
33
+ const { PATHS } = require('../../../flow-utils');
34
+ const logPath = path.join(PATHS.state, 'permission-denials.json');
35
+
36
+ let denials = [];
37
+ try {
38
+ if (fs.existsSync(logPath)) {
39
+ denials = JSON.parse(fs.readFileSync(logPath, 'utf-8'));
40
+ if (!Array.isArray(denials)) denials = [];
41
+ }
42
+ } catch (_err) {
43
+ denials = [];
44
+ }
45
+
46
+ denials.push({
47
+ tool: toolName,
48
+ target: typeof filePath === 'string' ? filePath.substring(0, 200) : '',
49
+ reason: typeof reason === 'string' ? reason.substring(0, 200) : '',
50
+ timestamp: new Date().toISOString()
51
+ });
52
+
53
+ // Keep last 100 denials
54
+ if (denials.length > 100) denials = denials.slice(-100);
55
+
56
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
57
+ fs.writeFileSync(logPath, JSON.stringify(denials, null, 2));
58
+ } catch (_err) {
59
+ // Non-critical — don't let logging failure break the hook
60
+ }
61
+
62
+ // ── 2. Workspace redirect — cross-repo file access ─────────
63
+ // When a worker tries to read/write a file that belongs to another repo,
64
+ // redirect them to use the workspace message bus instead.
65
+ const isWorkspace = !!process.env.WOGI_WORKSPACE_ROOT;
66
+ const repoName = process.env.WOGI_REPO_NAME || '';
67
+
68
+ if (isWorkspace && typeof filePath === 'string' && filePath.length > 0) {
69
+ const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
70
+
71
+ // Check if the denied path is inside the workspace but outside this repo
72
+ try {
73
+ const resolvedFile = path.resolve(filePath);
74
+ const resolvedRoot = path.resolve(workspaceRoot);
75
+ const isInsideWorkspace = resolvedFile.startsWith(resolvedRoot + path.sep);
76
+ const cwd = process.cwd();
77
+ const isOutsideOwnRepo = !resolvedFile.startsWith(cwd + path.sep) && resolvedFile !== cwd;
78
+
79
+ if (isInsideWorkspace && isOutsideOwnRepo) {
80
+ // This is a cross-repo access attempt — redirect to message bus
81
+ // Extract the target repo name from the path
82
+ const relPath = resolvedFile.substring(resolvedRoot.length + 1);
83
+ const targetRepo = relPath.split(path.sep)[0] || 'unknown';
84
+
85
+ const guidance = [
86
+ `Cross-repo file access denied: ${toolName} on ${path.basename(filePath)}`,
87
+ `Target repo: ${targetRepo} (you are: ${repoName})`,
88
+ '',
89
+ 'In workspace mode, repos cannot directly access each other\'s files.',
90
+ `Use workspace_send_message(to: "${targetRepo}", message: "...") to ask the other repo\'s worker.`,
91
+ `Or use workspace_send_message(to: "manager", message: "...") to escalate to the manager.`
92
+ ].join('\n');
93
+
94
+ return {
95
+ __raw: true,
96
+ retry: false,
97
+ message: guidance
98
+ };
99
+ }
100
+ } catch (_err) {
101
+ // Path resolution failed — fall through to default handling
102
+ }
103
+ }
104
+
105
+ // ── 3. Default handling — accept denial with guidance ──────
106
+ // For non-workspace denials, just accept and let the model know
107
+ return {
108
+ __raw: true,
109
+ retry: false
110
+ };
111
+ }, { failMode: 'silent' });
@@ -269,6 +269,8 @@ const HOOK_VERSION_MAP = {
269
269
  PostCompact: { major: 2, minor: 1, patch: 76 },
270
270
  // Hooks added in 2.1.84+
271
271
  TaskCreated: { major: 2, minor: 1, patch: 84 },
272
+ // Hooks added in 2.1.88+
273
+ PermissionDenied: { major: 2, minor: 1, patch: 88 },
272
274
  };
273
275
 
274
276
  /**
@@ -310,7 +312,26 @@ function versionMeetsMinimum(version, minimum) {
310
312
  * @returns {string[]} List of removed hook names
311
313
  */
312
314
  function stripUnsupportedHooks(settings, ccVersion) {
313
- if (!settings || !settings.hooks || !ccVersion) return [];
315
+ if (!settings || !settings.hooks) return [];
316
+
317
+ // CRITICAL: When CC version is unknown, strip ALL hooks added after the base set (2.1.23).
318
+ // CC rejects the ENTIRE settings file if it encounters an unknown hook event name —
319
+ // this means one unsupported hook kills ALL hooks. Fail-safe: keep only the base set.
320
+ const BASE_HOOKS = new Set([
321
+ 'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse',
322
+ 'Stop', 'SessionEnd', 'TaskCompleted'
323
+ ]);
324
+
325
+ if (!ccVersion) {
326
+ const removed = [];
327
+ for (const hookName of Object.keys(settings.hooks)) {
328
+ if (!BASE_HOOKS.has(hookName)) {
329
+ delete settings.hooks[hookName];
330
+ removed.push(`${hookName} (CC version unknown — keeping base hooks only)`);
331
+ }
332
+ }
333
+ return removed;
334
+ }
314
335
 
315
336
  const removed = [];
316
337
  for (const hookName of Object.keys(settings.hooks)) {
@@ -323,6 +344,39 @@ function stripUnsupportedHooks(settings, ccVersion) {
323
344
  return removed;
324
345
  }
325
346
 
347
+ /**
348
+ * Dynamically inject hooks that are NOT in the base settings.json but are
349
+ * supported by the detected Claude Code version. These hooks are intentionally
350
+ * excluded from the committed settings.json because CC rejects the ENTIRE file
351
+ * when it encounters an unknown hook event name.
352
+ *
353
+ * @param {Object} settings - Parsed settings object (mutated in place)
354
+ * @param {{ major: number, minor: number, patch: number }} ccVersion
355
+ */
356
+ function injectDynamicHooks(settings, ccVersion) {
357
+ if (!settings || !settings.hooks) return;
358
+
359
+ // Map of hooks to inject with their minimum version and command
360
+ const DYNAMIC_HOOKS = [
361
+ {
362
+ name: 'TaskCreated',
363
+ minVersion: { major: 2, minor: 1, patch: 84 },
364
+ config: [{ hooks: [{ type: 'command', command: 'node scripts/hooks/entry/claude-code/task-created.js', timeout: 5 }] }]
365
+ },
366
+ {
367
+ name: 'PermissionDenied',
368
+ minVersion: { major: 2, minor: 1, patch: 88 },
369
+ config: [{ hooks: [{ type: 'command', command: 'node scripts/hooks/entry/claude-code/permission-denied.js', timeout: 5 }] }]
370
+ }
371
+ ];
372
+
373
+ for (const hook of DYNAMIC_HOOKS) {
374
+ if (!settings.hooks[hook.name] && versionMeetsMinimum(ccVersion, hook.minVersion)) {
375
+ settings.hooks[hook.name] = hook.config;
376
+ }
377
+ }
378
+ }
379
+
326
380
  /**
327
381
  * Rewrite hook command paths from local dev paths to package paths.
328
382
  * The package's settings.json uses local paths (node scripts/hooks/...)
@@ -404,12 +458,20 @@ function copyClaudeResources() {
404
458
  const ccVersion = detectClaudeCodeVersion();
405
459
  const removedHooks = stripUnsupportedHooks(ours, ccVersion);
406
460
  if (removedHooks.length > 0) {
407
- console.log(`[wogiflow] Claude Code ${ccVersion.major}.${ccVersion.minor}.${ccVersion.patch} detected. Excluded unsupported hooks:`);
461
+ const versionStr = ccVersion ? `${ccVersion.major}.${ccVersion.minor}.${ccVersion.patch}` : 'unknown';
462
+ console.log(`[wogiflow] Claude Code ${versionStr} detected. Excluded unsupported hooks:`);
408
463
  for (const h of removedHooks) {
409
464
  console.log(` - ${h}`);
410
465
  }
411
466
  console.log('[wogiflow] Update Claude Code for full functionality: npm i -g @anthropic-ai/claude-code@latest');
412
467
  }
468
+
469
+ // Dynamically inject hooks supported by this CC version but not in the base settings.json.
470
+ // These hooks are NOT committed to the repo to avoid breaking older CC versions.
471
+ if (ccVersion) {
472
+ injectDynamicHooks(ours, ccVersion);
473
+ }
474
+
413
475
  // Always update hooks (core WogiFlow functionality)
414
476
  existing.hooks = ours.hooks;
415
477
  existing._wogiFlowManaged = true;