wogiflow 2.6.4 → 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 +1 -11
- package/lib/workspace-changelog.js +182 -0
- package/lib/workspace-channel-server.js +75 -2
- package/lib/workspace-contracts.js +151 -1
- package/lib/workspace-events.js +383 -0
- package/lib/workspace-gates.js +740 -0
- package/lib/workspace-integration-tests.js +299 -0
- package/lib/workspace-intelligence.js +486 -1
- package/lib/workspace-locks.js +371 -0
- package/lib/workspace-messages.js +203 -3
- package/lib/workspace-routing.js +147 -2
- package/lib/workspace.js +8 -0
- package/package.json +1 -1
- package/scripts/flow-done-gates.js +70 -0
- package/scripts/hooks/entry/claude-code/permission-denied.js +111 -0
- package/scripts/postinstall.js +64 -2
- package/.claude/rules/_internal/README.md +0 -64
- package/.claude/rules/_internal/document-structure.md +0 -77
- package/.claude/rules/_internal/dual-repo-management.md +0 -174
- package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
- package/.claude/rules/_internal/github-releases.md +0 -71
- package/.claude/rules/_internal/model-management.md +0 -35
- package/.claude/rules/_internal/self-maintenance.md +0 -87
- package/.claude/rules/architecture/component-reuse.md +0 -38
- package/.claude/rules/code-style/naming-conventions.md +0 -107
- package/.claude/rules/operations/git-workflows.md +0 -92
- package/.claude/rules/operations/scratch-directory.md +0 -54
- package/.claude/rules/security/security-patterns.md +0 -176
- package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
- package/.workflow/specs/architecture.md.template +0 -24
- package/.workflow/specs/stack.md.template +0 -33
- package/.workflow/specs/testing.md.template +0 -36
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
|
});
|
|
@@ -479,6 +480,146 @@ function getExecutionOrder(manifest, targetRepos) {
|
|
|
479
480
|
return order;
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
// ============================================================
|
|
484
|
+
// Cross-Repo Dependency-Aware Task Blocking
|
|
485
|
+
// ============================================================
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Block consumer-side tasks until their provider dependencies are complete.
|
|
489
|
+
* Reads workspace-level ready.json and each member's ready.json to find
|
|
490
|
+
* cross-repo dependencies.
|
|
491
|
+
*
|
|
492
|
+
* @param {string} workspaceRoot
|
|
493
|
+
* @param {Object} manifest
|
|
494
|
+
* @returns {{ blockedTasks: Array<Object>, unblockedTasks: Array<Object> }}
|
|
495
|
+
*/
|
|
496
|
+
function updateCrossRepoBlocking(workspaceRoot, manifest) {
|
|
497
|
+
const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
|
|
498
|
+
let config;
|
|
499
|
+
try {
|
|
500
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
501
|
+
} catch (_err) {
|
|
502
|
+
return { blockedTasks: [], unblockedTasks: [] };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const blockedTasks = [];
|
|
506
|
+
const unblockedTasks = [];
|
|
507
|
+
const memberTasks = {};
|
|
508
|
+
|
|
509
|
+
// Collect all tasks from all member repos
|
|
510
|
+
for (const [name, memberConfig] of Object.entries(config.members || {})) {
|
|
511
|
+
const memberPath = path.resolve(workspaceRoot, memberConfig.path);
|
|
512
|
+
const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
|
|
513
|
+
try {
|
|
514
|
+
if (fs.existsSync(readyPath)) {
|
|
515
|
+
const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
|
|
516
|
+
memberTasks[name] = {
|
|
517
|
+
path: memberPath,
|
|
518
|
+
readyPath,
|
|
519
|
+
ready,
|
|
520
|
+
inProgress: ready.inProgress || [],
|
|
521
|
+
readyItems: ready.ready || [],
|
|
522
|
+
completed: ready.recentlyCompleted || []
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
} catch (_err) {
|
|
526
|
+
// Skip
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// For each member's ready tasks, check if they depend on workspace tasks
|
|
531
|
+
const executionOrder = getExecutionOrder(manifest, Object.keys(config.members));
|
|
532
|
+
|
|
533
|
+
for (const [name, data] of Object.entries(memberTasks)) {
|
|
534
|
+
const memberPhase = executionOrder.find(o => o.name === name);
|
|
535
|
+
if (!memberPhase) continue;
|
|
536
|
+
|
|
537
|
+
for (const task of data.readyItems) {
|
|
538
|
+
// Check if this task has workspace source and blockedBy
|
|
539
|
+
if (!task.source?.startsWith('workspace:')) continue;
|
|
540
|
+
|
|
541
|
+
const blockedBy = task.blockedBy || [];
|
|
542
|
+
let isBlocked = false;
|
|
543
|
+
|
|
544
|
+
for (const depId of blockedBy) {
|
|
545
|
+
// Check if the blocking task is completed in any member
|
|
546
|
+
let depCompleted = false;
|
|
547
|
+
for (const [depName, depData] of Object.entries(memberTasks)) {
|
|
548
|
+
if (depData.completed.some(t => t.id === depId)) {
|
|
549
|
+
depCompleted = true;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!depCompleted) {
|
|
555
|
+
isBlocked = true;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (isBlocked) {
|
|
561
|
+
blockedTasks.push({ repo: name, task, blockedBy });
|
|
562
|
+
} else if (blockedBy.length > 0) {
|
|
563
|
+
unblockedTasks.push({ repo: name, task });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return { blockedTasks, unblockedTasks };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Build a visual dependency tree for workspace tasks.
|
|
573
|
+
*
|
|
574
|
+
* @param {string} workspaceRoot
|
|
575
|
+
* @param {Object} manifest
|
|
576
|
+
* @returns {string} formatted tree
|
|
577
|
+
*/
|
|
578
|
+
function formatDependencyTree(workspaceRoot, manifest) {
|
|
579
|
+
const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
|
|
580
|
+
let config;
|
|
581
|
+
try {
|
|
582
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
583
|
+
} catch (_err) {
|
|
584
|
+
return 'No workspace config found.';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const lines = ['Workspace Task Dependencies:'];
|
|
588
|
+
const order = getExecutionOrder(manifest, Object.keys(config.members || {}));
|
|
589
|
+
|
|
590
|
+
for (const entry of order) {
|
|
591
|
+
const memberConfig = config.members[entry.name];
|
|
592
|
+
if (!memberConfig) continue;
|
|
593
|
+
|
|
594
|
+
const memberPath = path.resolve(workspaceRoot, memberConfig.path);
|
|
595
|
+
const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
if (!fs.existsSync(readyPath)) continue;
|
|
599
|
+
const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
|
|
600
|
+
const wsTasks = [...(ready.inProgress || []), ...(ready.ready || []), ...(ready.blocked || [])]
|
|
601
|
+
.filter(t => t.source?.startsWith('workspace:'));
|
|
602
|
+
|
|
603
|
+
if (wsTasks.length === 0) continue;
|
|
604
|
+
|
|
605
|
+
lines.push(`\n ${entry.name} (${entry.role}, phase ${entry.order}):`);
|
|
606
|
+
for (const task of wsTasks) {
|
|
607
|
+
const status = task.status === 'completed' ? '\u2713' :
|
|
608
|
+
(ready.inProgress || []).some(t => t.id === task.id) ? '\u25B6' :
|
|
609
|
+
task.blockedBy?.length ? '\u2718' : '\u25CB';
|
|
610
|
+
const blockedNote = task.blockedBy?.length
|
|
611
|
+
? ` [blocked by: ${task.blockedBy.join(', ')}]`
|
|
612
|
+
: '';
|
|
613
|
+
lines.push(` ${status} ${task.id} — ${task.title}${blockedNote}`);
|
|
614
|
+
}
|
|
615
|
+
} catch (_err) {
|
|
616
|
+
// Skip
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return lines.join('\n');
|
|
621
|
+
}
|
|
622
|
+
|
|
482
623
|
// ============================================================
|
|
483
624
|
// Channel-Based Dispatch (wf-d4b98f60)
|
|
484
625
|
// ============================================================
|
|
@@ -772,6 +913,10 @@ module.exports = {
|
|
|
772
913
|
// Ordering
|
|
773
914
|
getExecutionOrder,
|
|
774
915
|
|
|
916
|
+
// Cross-repo blocking
|
|
917
|
+
updateCrossRepoBlocking,
|
|
918
|
+
formatDependencyTree,
|
|
919
|
+
|
|
775
920
|
// Channel dispatch
|
|
776
921
|
dispatchToChannel,
|
|
777
922
|
dispatchCrossRepoPlan,
|
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
|
@@ -57,6 +57,19 @@ function getRegistryManager() {
|
|
|
57
57
|
return _registryManagerModule;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Workspace gates — lazy-loaded (only active when workspace mode is detected)
|
|
61
|
+
let _workspaceGatesModule = undefined;
|
|
62
|
+
function getWorkspaceGates() {
|
|
63
|
+
if (_workspaceGatesModule === undefined) {
|
|
64
|
+
try {
|
|
65
|
+
_workspaceGatesModule = require('../lib/workspace-gates');
|
|
66
|
+
} catch (_err) {
|
|
67
|
+
_workspaceGatesModule = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return _workspaceGatesModule;
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
// ============================================================
|
|
61
74
|
// Gate Handlers
|
|
62
75
|
// ============================================================
|
|
@@ -717,6 +730,60 @@ function unknownGate(ctx, gateName) {
|
|
|
717
730
|
// Gate Registry
|
|
718
731
|
// ============================================================
|
|
719
732
|
|
|
733
|
+
// ============================================================
|
|
734
|
+
// Workspace Quality Gates (conditional — only when workspace active)
|
|
735
|
+
// ============================================================
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Workspace gate handler. Delegates to workspace-gates.js for each
|
|
739
|
+
* sub-gate (crossRepoImpactCheck, contractCompliance, peerNotification,
|
|
740
|
+
* cascadeVerification, integrationMapFreshness).
|
|
741
|
+
*
|
|
742
|
+
* This is a single gate entry in GATE_REGISTRY that runs all applicable
|
|
743
|
+
* workspace sub-gates based on task type.
|
|
744
|
+
*/
|
|
745
|
+
function workspaceGate(ctx, gateName) {
|
|
746
|
+
const wsGates = getWorkspaceGates();
|
|
747
|
+
if (!wsGates) {
|
|
748
|
+
return { passed: true, skipped: true };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const ws = wsGates.workspaceActive();
|
|
752
|
+
if (!ws.active) {
|
|
753
|
+
return { passed: true, skipped: true };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const context = wsGates.loadWorkspaceContext(ws.root);
|
|
757
|
+
const taskMeta = {
|
|
758
|
+
taskId: ctx.taskId,
|
|
759
|
+
taskTitle: ctx.taskTitle || '',
|
|
760
|
+
taskType: ctx.normalizedType || 'feature',
|
|
761
|
+
impactAssessed: ctx.impactAssessed || false
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const results = wsGates.runAllWorkspaceGates(ws.root, context, taskMeta);
|
|
765
|
+
|
|
766
|
+
// Display results
|
|
767
|
+
for (const r of results.results) {
|
|
768
|
+
if (r.passed) {
|
|
769
|
+
ctx.success(`workspace/${r.gate}: ${r.message}`);
|
|
770
|
+
} else if (r.severity === 'warning') {
|
|
771
|
+
console.log(` ${ctx.color('yellow', '\u25CB')} workspace/${r.gate}: ${r.message}`);
|
|
772
|
+
} else {
|
|
773
|
+
ctx.error(`workspace/${r.gate}: ${r.message}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (!results.passed) {
|
|
778
|
+
return {
|
|
779
|
+
passed: false,
|
|
780
|
+
errorOutput: `${results.errors} workspace gate(s) failed, ${results.warnings} warning(s)`
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return { passed: true };
|
|
785
|
+
}
|
|
786
|
+
|
|
720
787
|
const GATE_REGISTRY = {
|
|
721
788
|
tests: testsGate,
|
|
722
789
|
lint: lintGate,
|
|
@@ -737,6 +804,8 @@ const GATE_REGISTRY = {
|
|
|
737
804
|
uiVerification: verificationGate,
|
|
738
805
|
apiVerification: verificationGate,
|
|
739
806
|
testDiscovery: testDiscoveryGate,
|
|
807
|
+
// Workspace gates (conditional — auto-skip when not in workspace)
|
|
808
|
+
workspaceCompliance: workspaceGate,
|
|
740
809
|
};
|
|
741
810
|
|
|
742
811
|
/**
|
|
@@ -782,5 +851,6 @@ module.exports = {
|
|
|
782
851
|
generatedTestsPassGate,
|
|
783
852
|
verificationGate,
|
|
784
853
|
testDiscoveryGate,
|
|
854
|
+
workspaceGate,
|
|
785
855
|
unknownGate,
|
|
786
856
|
};
|
|
@@ -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;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
alwaysApply: false
|
|
3
|
-
description: "Meta-documentation about how project rules are organized"
|
|
4
|
-
---
|
|
5
|
-
# Project Rules
|
|
6
|
-
|
|
7
|
-
This directory contains coding rules and patterns for this project, organized by category.
|
|
8
|
-
|
|
9
|
-
## Structure
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
.claude/rules/
|
|
13
|
-
├── code-style/ # Naming conventions, formatting
|
|
14
|
-
│ └── naming-conventions.md
|
|
15
|
-
├── security/ # Security patterns and practices
|
|
16
|
-
│ └── security-patterns.md
|
|
17
|
-
├── architecture/ # Design decisions and patterns
|
|
18
|
-
│ ├── component-reuse.md
|
|
19
|
-
│ └── model-management.md
|
|
20
|
-
└── README.md
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## How Rules Work
|
|
24
|
-
|
|
25
|
-
Rules are automatically loaded by Claude Code based on:
|
|
26
|
-
- **alwaysApply: true** - Rule is always loaded
|
|
27
|
-
- **alwaysApply: false** - Rule is loaded based on `globs` or `description` relevance
|
|
28
|
-
- **globs** - File patterns that trigger rule loading
|
|
29
|
-
|
|
30
|
-
## Adding New Rules
|
|
31
|
-
|
|
32
|
-
1. Choose the appropriate category subdirectory
|
|
33
|
-
2. Create a `.md` file with frontmatter:
|
|
34
|
-
|
|
35
|
-
```yaml
|
|
36
|
-
---
|
|
37
|
-
alwaysApply: false
|
|
38
|
-
description: "Brief description for relevance matching"
|
|
39
|
-
globs: src/**/*.ts # Optional: only load for these files
|
|
40
|
-
---
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
3. Write the rule content in markdown
|
|
44
|
-
|
|
45
|
-
## Categories
|
|
46
|
-
|
|
47
|
-
| Category | Purpose |
|
|
48
|
-
|----------|---------|
|
|
49
|
-
| code-style | Naming conventions, formatting, file structure |
|
|
50
|
-
| security | Security patterns, input validation, safe practices |
|
|
51
|
-
| architecture | Design decisions, component patterns, system organization |
|
|
52
|
-
|
|
53
|
-
## Auto-Generation
|
|
54
|
-
|
|
55
|
-
Some rules can be auto-generated from `.workflow/state/decisions.md`:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
node scripts/flow-rules-sync.js
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
The sync script will route rules to appropriate category subdirectories.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
Last updated: 2026-01-12
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
alwaysApply: false
|
|
3
|
-
description: "All AI-context documents must use PIN markers for targeted context loading"
|
|
4
|
-
globs: ".workflow/**/*.md"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Document Structure for AI Context
|
|
8
|
-
|
|
9
|
-
All documents in `.workflow/` that are used as AI context MUST follow the PIN standard.
|
|
10
|
-
|
|
11
|
-
## Required Structure
|
|
12
|
-
|
|
13
|
-
### 1. Header with PIN List
|
|
14
|
-
Every document starts with a comment listing all pins in the document:
|
|
15
|
-
```markdown
|
|
16
|
-
<!-- PINS: pin1, pin2, pin3 -->
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### 2. Section PIN Markers
|
|
20
|
-
Each major section has a PIN marker comment:
|
|
21
|
-
```markdown
|
|
22
|
-
### Section Title
|
|
23
|
-
<!-- PIN: section-specific-pin -->
|
|
24
|
-
[Content]
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### 3. PIN Naming Convention
|
|
28
|
-
- Use kebab-case: `user-authentication`, not `userAuthentication`
|
|
29
|
-
- Use semantic names: `error-handling`, not `eh`
|
|
30
|
-
- Use compound names for specificity: `json-parse-safety`
|
|
31
|
-
|
|
32
|
-
## Why PINs Matter
|
|
33
|
-
|
|
34
|
-
The PIN system enables:
|
|
35
|
-
1. **Targeted context loading**: Only load sections relevant to current task
|
|
36
|
-
2. **Cheaper model routing**: Haiku can fetch only relevant sections for Opus
|
|
37
|
-
3. **Change detection**: Hash sections independently for smart invalidation
|
|
38
|
-
4. **Cross-reference**: Link sections by PIN across documents
|
|
39
|
-
|
|
40
|
-
## Example Document
|
|
41
|
-
|
|
42
|
-
```markdown
|
|
43
|
-
# Config Reference
|
|
44
|
-
|
|
45
|
-
<!-- PINS: database, authentication, api-keys, environment -->
|
|
46
|
-
|
|
47
|
-
## Database Settings
|
|
48
|
-
<!-- PIN: database -->
|
|
49
|
-
| Setting | Default | Description |
|
|
50
|
-
|---------|---------|-------------|
|
|
51
|
-
|
|
52
|
-
## Authentication
|
|
53
|
-
<!-- PIN: authentication -->
|
|
54
|
-
| Setting | Default | Description |
|
|
55
|
-
|---------|---------|-------------|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Parsing
|
|
59
|
-
|
|
60
|
-
The PIN system automatically parses documents with:
|
|
61
|
-
- `flow-section-index.js` - Generates section index with pins
|
|
62
|
-
- `flow-section-resolver.js` - Resolves sections by PIN lookup
|
|
63
|
-
- `getSectionsByPins(['auth', 'security'])` - Fetch only relevant sections
|
|
64
|
-
|
|
65
|
-
## Files That Must Have PINs
|
|
66
|
-
|
|
67
|
-
| File | Required PINs |
|
|
68
|
-
|------|---------------|
|
|
69
|
-
| `decisions.md` | Per coding rule/pattern |
|
|
70
|
-
| `app-map.md` | Per component/screen |
|
|
71
|
-
| `product.md` | Per product section |
|
|
72
|
-
| `stack.md` | Per technology |
|
|
73
|
-
|
|
74
|
-
## Validation
|
|
75
|
-
|
|
76
|
-
Run `node scripts/flow-section-index.js --force` to regenerate the index.
|
|
77
|
-
Check `.workflow/state/section-index.json` for indexed sections and their pins.
|