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.
package/.claude/settings.json
CHANGED
|
@@ -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."
|
package/lib/workspace-routing.js
CHANGED
|
@@ -221,8 +221,8 @@ After completing the task:
|
|
|
221
221
|
|
|
222
222
|
agentConfig: {
|
|
223
223
|
description: `${repoName}: ${task.substring(0, 50)}...`,
|
|
224
|
-
//
|
|
225
|
-
|
|
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
|
@@ -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' });
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|