wogiflow 2.4.2 → 2.4.4
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/commands/wogi-start.md +124 -0
- package/.claude/docs/claude-code-compatibility.md +51 -0
- package/.claude/docs/explore-agents.md +11 -0
- package/.claude/settings.json +12 -1
- package/.workflow/models/registry.json +1 -1
- package/bin/flow +11 -1
- package/lib/workspace-contracts.js +599 -0
- package/lib/workspace-intelligence.js +600 -0
- package/lib/workspace-messages.js +441 -0
- package/lib/workspace-routing.js +485 -0
- package/lib/workspace-sync.js +339 -0
- package/lib/workspace.js +1073 -0
- package/package.json +4 -4
- package/scripts/MEMORY-ARCHITECTURE.md +1 -1
- package/scripts/base-workflow-step.js +136 -0
- package/scripts/flow-adaptive-learning.js +8 -9
- package/scripts/flow-aggregate.js +11 -6
- package/scripts/flow-api-index.js +4 -6
- package/scripts/flow-assumption-detector.js +0 -2
- package/scripts/flow-audit.js +15 -2
- package/scripts/flow-auto-context.js +8 -12
- package/scripts/flow-auto-learn.js +49 -49
- package/scripts/flow-background.js +5 -6
- package/scripts/flow-bridge-state.js +8 -10
- package/scripts/flow-bulk-loop.js +1 -3
- package/scripts/flow-bulk-orchestrator.js +1 -3
- package/scripts/flow-cascade-completion.js +0 -2
- package/scripts/flow-cascade.js +4 -4
- package/scripts/flow-checkpoint.js +10 -13
- package/scripts/flow-code-intelligence.js +10 -12
- package/scripts/flow-community-sync.js +4 -4
- package/scripts/flow-community.js +12 -20
- package/scripts/flow-config-defaults.js +28 -2
- package/scripts/flow-config-interactive.js +9 -5
- package/scripts/flow-config-loader.js +49 -92
- package/scripts/flow-config-substitution.js +0 -2
- package/scripts/flow-context-estimator.js +4 -4
- package/scripts/flow-context-init.js +10 -12
- package/scripts/flow-context-manager.js +0 -2
- package/scripts/flow-context-scoring.js +2 -2
- package/scripts/flow-contract-scan.js +6 -9
- package/scripts/flow-correct.js +29 -27
- package/scripts/flow-correction-detector.js +5 -1
- package/scripts/flow-damage-control.js +47 -54
- package/scripts/flow-decisions-merge.js +4 -14
- package/scripts/flow-diff.js +5 -8
- package/scripts/flow-done-gates.js +786 -0
- package/scripts/flow-done-report.js +123 -0
- package/scripts/flow-done.js +71 -717
- package/scripts/flow-entropy-monitor.js +1 -3
- package/scripts/flow-eval-calibration.js +257 -0
- package/scripts/flow-eval-judge.js +10 -1
- package/scripts/flow-eval.js +14 -5
- package/scripts/flow-extraction-review.js +1 -0
- package/scripts/flow-failure-categories.js +0 -2
- package/scripts/flow-figma-confirm.js +5 -9
- package/scripts/flow-figma-generate.js +8 -10
- package/scripts/flow-figma-index.js +8 -10
- package/scripts/flow-figma-match.js +3 -5
- package/scripts/flow-figma-mcp-server.js +2 -4
- package/scripts/flow-figma-orchestrator.js +2 -3
- package/scripts/flow-figma-registry.js +2 -3
- package/scripts/flow-framework-resolver.js +0 -2
- package/scripts/flow-function-index.js +4 -6
- package/scripts/flow-gate-confidence.js +2 -2
- package/scripts/flow-gitignore.js +0 -2
- package/scripts/flow-guided-edit.js +5 -6
- package/scripts/flow-health.js +5 -6
- package/scripts/flow-hook-errors.js +6 -0
- package/scripts/flow-hook-status.js +263 -0
- package/scripts/flow-hooks.js +17 -29
- package/scripts/flow-http-client.js +9 -8
- package/scripts/flow-hybrid-interactive.js +7 -12
- package/scripts/flow-hybrid-test.js +12 -13
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-io.js +21 -4
- package/scripts/flow-knowledge-router.js +9 -3
- package/scripts/flow-learning-orchestrator.js +318 -13
- package/scripts/flow-links.js +5 -7
- package/scripts/flow-long-input-association.js +275 -0
- package/scripts/flow-long-input-chunking.js +1 -0
- package/scripts/flow-long-input-cli.js +0 -2
- package/scripts/flow-long-input-complexity.js +0 -2
- package/scripts/flow-long-input-constants.js +0 -2
- package/scripts/flow-long-input-contradictions.js +351 -0
- package/scripts/flow-long-input-detection.js +0 -2
- package/scripts/flow-long-input-passes.js +885 -0
- package/scripts/flow-long-input-stories.js +1 -1
- package/scripts/flow-long-input-voice.js +0 -2
- package/scripts/flow-long-input.js +425 -3005
- package/scripts/flow-loop-retry-learning.js +2 -3
- package/scripts/flow-lsp.js +3 -3
- package/scripts/flow-mcp-docs.js +3 -4
- package/scripts/flow-memory-db.js +6 -8
- package/scripts/flow-memory-sync.js +18 -11
- package/scripts/flow-metrics.js +1 -2
- package/scripts/flow-model-adapter.js +2 -3
- package/scripts/flow-model-config.js +72 -104
- package/scripts/flow-model-router.js +2 -2
- package/scripts/flow-model-types.js +0 -2
- package/scripts/flow-multi-approach.js +5 -6
- package/scripts/flow-orchestrate-context.js +3 -7
- package/scripts/flow-orchestrate-rollback.js +3 -8
- package/scripts/flow-orchestrate-state.js +8 -14
- package/scripts/flow-orchestrate-templates.js +2 -6
- package/scripts/flow-orchestrate-validator.js +5 -9
- package/scripts/flow-orchestrate.js +126 -103
- package/scripts/flow-output.js +0 -2
- package/scripts/flow-parallel.js +1 -1
- package/scripts/flow-paths.js +23 -2
- package/scripts/flow-pattern-enforcer.js +30 -28
- package/scripts/flow-pattern-extractor.js +3 -4
- package/scripts/flow-pending.js +0 -2
- package/scripts/flow-permissions.js +2 -3
- package/scripts/flow-plugin-registry.js +10 -12
- package/scripts/flow-prd-manager.js +1 -1
- package/scripts/flow-progress.js +7 -9
- package/scripts/flow-prompt-composer.js +3 -3
- package/scripts/flow-prompt-template.js +2 -2
- package/scripts/flow-providers.js +7 -4
- package/scripts/flow-registry-manager.js +7 -12
- package/scripts/flow-regression.js +9 -11
- package/scripts/flow-roadmap.js +2 -2
- package/scripts/flow-run-trace.js +16 -15
- package/scripts/flow-safety.js +2 -5
- package/scripts/flow-scanner-base.js +5 -7
- package/scripts/flow-scenario-engine.js +1 -5
- package/scripts/flow-security.js +29 -0
- package/scripts/flow-session-end.js +32 -41
- package/scripts/flow-session-learning.js +53 -49
- package/scripts/flow-setup-hooks.js +2 -3
- package/scripts/flow-skill-create.js +7 -12
- package/scripts/flow-skill-generator.js +12 -16
- package/scripts/flow-skill-learn.js +17 -8
- package/scripts/flow-skill-matcher.js +1 -2
- package/scripts/flow-spec-generator.js +2 -4
- package/scripts/flow-stack-wizard.js +5 -7
- package/scripts/flow-standards-learner.js +35 -16
- package/scripts/flow-start.js +2 -0
- package/scripts/flow-stats-collector.js +2 -2
- package/scripts/flow-status.js +10 -10
- package/scripts/flow-statusline-setup.js +2 -2
- package/scripts/flow-step-changelog.js +2 -3
- package/scripts/flow-step-comments.js +66 -81
- package/scripts/flow-step-complexity.js +50 -70
- package/scripts/flow-step-coverage.js +3 -5
- package/scripts/flow-step-knowledge.js +2 -3
- package/scripts/flow-step-pr-tests.js +64 -74
- package/scripts/flow-step-regression.js +3 -5
- package/scripts/flow-step-review.js +86 -103
- package/scripts/flow-step-security.js +111 -121
- package/scripts/flow-step-silent-failures.js +56 -83
- package/scripts/flow-step-simplifier.js +52 -70
- package/scripts/flow-story.js +4 -7
- package/scripts/flow-strict-adherence.js +3 -4
- package/scripts/flow-task-checkpoint.js +36 -5
- package/scripts/flow-task-enforcer.js +2 -24
- package/scripts/flow-tech-debt.js +1 -1
- package/scripts/flow-template-extractor.js +1 -0
- package/scripts/flow-templates.js +11 -13
- package/scripts/flow-test-api.js +9 -13
- package/scripts/flow-test-discovery.js +1 -1
- package/scripts/flow-test-generate.js +5 -9
- package/scripts/flow-test-integrity.js +3 -7
- package/scripts/flow-test-ui.js +5 -9
- package/scripts/flow-testing-deps.js +1 -3
- package/scripts/flow-tiered-learning.js +4 -4
- package/scripts/flow-todowrite-sync.js +1 -1
- package/scripts/flow-tokens.js +0 -2
- package/scripts/flow-verification-profile.js +6 -10
- package/scripts/flow-verify.js +12 -16
- package/scripts/flow-version-check.js +4 -12
- package/scripts/flow-webmcp-generator.js +3 -5
- package/scripts/flow-workflow-steps.js +0 -2
- package/scripts/flow-workflow.js +9 -11
- package/scripts/hooks/adapters/claude-code.js +31 -0
- package/scripts/hooks/core/config-change.js +1 -0
- package/scripts/hooks/core/extension-registry.js +0 -2
- package/scripts/hooks/core/instructions-loaded.js +1 -1
- package/scripts/hooks/core/observation-capture.js +5 -5
- package/scripts/hooks/core/phase-gate.js +5 -0
- package/scripts/hooks/core/post-compact.js +1 -12
- package/scripts/hooks/core/research-gate.js +2 -12
- package/scripts/hooks/core/routing-gate.js +6 -0
- package/scripts/hooks/core/task-completed.js +12 -0
- package/scripts/hooks/core/task-created.js +83 -0
- package/scripts/hooks/core/worktree-lifecycle.js +1 -1
- package/scripts/hooks/entry/claude-code/config-change.js +6 -29
- package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
- package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
- package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
- package/scripts/hooks/entry/claude-code/session-end.js +4 -28
- package/scripts/hooks/entry/claude-code/session-start.js +205 -243
- package/scripts/hooks/entry/claude-code/setup.js +8 -49
- package/scripts/hooks/entry/claude-code/stop.js +40 -72
- package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
- package/scripts/hooks/entry/claude-code/task-created.js +15 -0
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
- package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
- package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
- package/scripts/hooks/entry/shared/hook-runner.js +99 -0
- package/scripts/hooks/entry/shared/read-stdin.js +0 -2
- package/scripts/postinstall.js +2 -0
- package/scripts/registries/api-registry.js +0 -2
- package/scripts/registries/component-registry.js +5 -9
- package/scripts/registries/contract-scanner.js +2 -9
- package/scripts/registries/function-registry.js +0 -2
- package/scripts/registries/schema-registry.js +14 -18
- package/scripts/registries/service-registry.js +23 -27
package/lib/workspace.js
ADDED
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Workspace — Multi-Repo Orchestration Layer
|
|
5
|
+
*
|
|
6
|
+
* Creates a workspace that coordinates N member repos through a manager agent.
|
|
7
|
+
* The workspace reads WogiFlow state files (not source code) from each member
|
|
8
|
+
* and generates a unified view for cross-repo task routing.
|
|
9
|
+
*
|
|
10
|
+
* Directory structure created:
|
|
11
|
+
* .workspace/
|
|
12
|
+
* ├── state/ — workspace-level state
|
|
13
|
+
* ├── contracts/ — shared API contracts
|
|
14
|
+
* ├── messages/ — agent-to-agent communication
|
|
15
|
+
* └── specs/ — cross-repo task specifications
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// Constants
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
const WORKSPACE_CONFIG_FILE = 'wogi-workspace.json';
|
|
26
|
+
const WORKSPACE_DIR = '.workspace';
|
|
27
|
+
const WORKSPACE_DIRS = [
|
|
28
|
+
'state',
|
|
29
|
+
'contracts',
|
|
30
|
+
'messages',
|
|
31
|
+
'specs'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const MEMBER_ROLES = ['consumer', 'provider', 'both', 'standalone', 'library'];
|
|
35
|
+
|
|
36
|
+
const STATE_FILES_TO_READ = [
|
|
37
|
+
{ file: 'api-map.md', key: 'apiMap', description: 'API endpoints' },
|
|
38
|
+
{ file: 'app-map.md', key: 'appMap', description: 'Components/modules' },
|
|
39
|
+
{ file: 'schema-map.md', key: 'schemaMap', description: 'Data models' },
|
|
40
|
+
{ file: 'function-map.md', key: 'functionMap', description: 'Utility functions' },
|
|
41
|
+
{ file: 'decisions.md', key: 'decisions', description: 'Coding rules' },
|
|
42
|
+
{ file: 'config.json', key: 'config', description: 'Project config', json: true }
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const INDEX_FILES_TO_READ = [
|
|
46
|
+
{ file: 'api-index.json', key: 'apiIndex' },
|
|
47
|
+
{ file: 'component-index.json', key: 'componentIndex' },
|
|
48
|
+
{ file: 'schema-index.json', key: 'schemaIndex' },
|
|
49
|
+
{ file: 'service-index.json', key: 'serviceIndex' },
|
|
50
|
+
{ file: 'registry-manifest.json', key: 'registryManifest' }
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// Discovery
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Scan for WogiFlow-enabled subdirectories
|
|
59
|
+
* @param {string} workspaceRoot — path to workspace folder
|
|
60
|
+
* @returns {Array<{name: string, path: string, workflowPath: string}>}
|
|
61
|
+
*/
|
|
62
|
+
function discoverMembers(workspaceRoot) {
|
|
63
|
+
const members = [];
|
|
64
|
+
const entries = fs.readdirSync(workspaceRoot, { withFileTypes: true });
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (!entry.isDirectory()) continue;
|
|
68
|
+
// Skip hidden dirs and node_modules
|
|
69
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
70
|
+
|
|
71
|
+
const memberPath = path.join(workspaceRoot, entry.name);
|
|
72
|
+
const workflowPath = path.join(memberPath, '.workflow');
|
|
73
|
+
|
|
74
|
+
if (fs.existsSync(workflowPath) && fs.statSync(workflowPath).isDirectory()) {
|
|
75
|
+
members.push({
|
|
76
|
+
name: entry.name,
|
|
77
|
+
path: memberPath,
|
|
78
|
+
workflowPath
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return members;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================
|
|
87
|
+
// State File Reading (metadata only — no source code)
|
|
88
|
+
// ============================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read a member repo's WogiFlow state files
|
|
92
|
+
* @param {string} workflowPath — path to member's .workflow/ directory
|
|
93
|
+
* @returns {Object} parsed metadata
|
|
94
|
+
*/
|
|
95
|
+
function readMemberMetadata(workflowPath) {
|
|
96
|
+
const statePath = path.join(workflowPath, 'state');
|
|
97
|
+
const metadata = {};
|
|
98
|
+
|
|
99
|
+
// Read markdown and JSON state files
|
|
100
|
+
for (const { file, key, json } of STATE_FILES_TO_READ) {
|
|
101
|
+
const filePath = path.join(json ? workflowPath : statePath, file);
|
|
102
|
+
try {
|
|
103
|
+
if (fs.existsSync(filePath)) {
|
|
104
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
105
|
+
metadata[key] = json ? JSON.parse(content) : content;
|
|
106
|
+
}
|
|
107
|
+
} catch (_err) {
|
|
108
|
+
// Non-critical — skip unreadable files
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Read JSON index files (machine-readable)
|
|
113
|
+
for (const { file, key } of INDEX_FILES_TO_READ) {
|
|
114
|
+
const filePath = path.join(statePath, file);
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(filePath)) {
|
|
117
|
+
metadata[key] = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
118
|
+
}
|
|
119
|
+
} catch (_err) {
|
|
120
|
+
// Non-critical
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return metadata;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extract capabilities summary from member metadata
|
|
129
|
+
* @param {Object} metadata — parsed member metadata
|
|
130
|
+
* @returns {Object} capabilities summary
|
|
131
|
+
*/
|
|
132
|
+
function extractCapabilities(metadata) {
|
|
133
|
+
const caps = {
|
|
134
|
+
endpoints: 0,
|
|
135
|
+
components: 0,
|
|
136
|
+
models: 0,
|
|
137
|
+
functions: 0,
|
|
138
|
+
services: 0
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Count from index files (most accurate)
|
|
142
|
+
if (metadata.apiIndex) {
|
|
143
|
+
const idx = metadata.apiIndex;
|
|
144
|
+
caps.endpoints = (idx.endpoints || []).length + (idx.clientFunctions || []).length;
|
|
145
|
+
}
|
|
146
|
+
if (metadata.componentIndex) {
|
|
147
|
+
const idx = metadata.componentIndex;
|
|
148
|
+
caps.components = (idx.components || []).length + (idx.hooks || []).length;
|
|
149
|
+
}
|
|
150
|
+
if (metadata.schemaIndex) {
|
|
151
|
+
const idx = metadata.schemaIndex;
|
|
152
|
+
caps.models = (idx.models || []).length;
|
|
153
|
+
}
|
|
154
|
+
if (metadata.serviceIndex) {
|
|
155
|
+
const idx = metadata.serviceIndex;
|
|
156
|
+
caps.services = (idx.services || []).length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fallback: count from markdown tables if index files missing
|
|
160
|
+
if (caps.endpoints === 0 && metadata.apiMap) {
|
|
161
|
+
caps.endpoints = (metadata.apiMap.match(/^\|[^|]+\|/gm) || []).length - countHeaderRows(metadata.apiMap);
|
|
162
|
+
}
|
|
163
|
+
if (caps.components === 0 && metadata.appMap) {
|
|
164
|
+
caps.components = (metadata.appMap.match(/^\|[^|]+\|/gm) || []).length - countHeaderRows(metadata.appMap);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return caps;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Count markdown table header rows (lines starting with |---|)
|
|
172
|
+
*/
|
|
173
|
+
function countHeaderRows(md) {
|
|
174
|
+
return (md.match(/^\|[-: |]+\|$/gm) || []).length;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Extract endpoints provided/consumed from api-map or api-index
|
|
179
|
+
* @param {Object} metadata
|
|
180
|
+
* @returns {{ provides: string[], consumes: string[] }}
|
|
181
|
+
*/
|
|
182
|
+
function extractEndpoints(metadata) {
|
|
183
|
+
const provides = [];
|
|
184
|
+
const consumes = [];
|
|
185
|
+
|
|
186
|
+
if (metadata.apiIndex) {
|
|
187
|
+
const idx = metadata.apiIndex;
|
|
188
|
+
// Server endpoints = provides
|
|
189
|
+
for (const ep of (idx.endpoints || [])) {
|
|
190
|
+
const method = (ep.method || 'GET').toUpperCase();
|
|
191
|
+
const route = ep.route || ep.path || ep.endpoint || '';
|
|
192
|
+
if (route) provides.push(`${method} ${route}`);
|
|
193
|
+
}
|
|
194
|
+
// Client functions = consumes
|
|
195
|
+
for (const fn of (idx.clientFunctions || [])) {
|
|
196
|
+
const method = (fn.method || 'GET').toUpperCase();
|
|
197
|
+
const url = fn.url || fn.endpoint || fn.path || '';
|
|
198
|
+
if (url) consumes.push(`${method} ${url}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { provides, consumes };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Detect stack from config or metadata
|
|
207
|
+
* @param {Object} metadata
|
|
208
|
+
* @param {string} memberPath
|
|
209
|
+
* @returns {Object} stack info
|
|
210
|
+
*/
|
|
211
|
+
function detectStack(metadata, memberPath) {
|
|
212
|
+
const stack = {
|
|
213
|
+
language: 'unknown',
|
|
214
|
+
framework: 'unknown'
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// From WogiFlow config
|
|
218
|
+
if (metadata.config) {
|
|
219
|
+
const c = metadata.config;
|
|
220
|
+
if (c.projectType) stack.projectType = c.projectType;
|
|
221
|
+
if (c.strictAdherence?.operational?.packageManager?.tool) {
|
|
222
|
+
stack.packageManager = c.strictAdherence.operational.packageManager.tool;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Detect from package.json
|
|
227
|
+
const pkgPath = path.join(memberPath, 'package.json');
|
|
228
|
+
if (fs.existsSync(pkgPath)) {
|
|
229
|
+
try {
|
|
230
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
231
|
+
if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript) {
|
|
232
|
+
stack.language = 'TypeScript';
|
|
233
|
+
} else {
|
|
234
|
+
stack.language = 'JavaScript';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
238
|
+
if (allDeps.react) stack.framework = 'React';
|
|
239
|
+
else if (allDeps.next) stack.framework = 'Next.js';
|
|
240
|
+
else if (allDeps.vue) stack.framework = 'Vue';
|
|
241
|
+
else if (allDeps.svelte) stack.framework = 'Svelte';
|
|
242
|
+
else if (allDeps.express) stack.framework = 'Express';
|
|
243
|
+
else if (allDeps.fastify) stack.framework = 'Fastify';
|
|
244
|
+
else if (allDeps.nestjs || allDeps['@nestjs/core']) stack.framework = 'NestJS';
|
|
245
|
+
else if (allDeps.hono) stack.framework = 'Hono';
|
|
246
|
+
} catch (_err) {
|
|
247
|
+
// Non-critical
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Detect Python
|
|
252
|
+
const pyprojectPath = path.join(memberPath, 'pyproject.toml');
|
|
253
|
+
const requirementsPath = path.join(memberPath, 'requirements.txt');
|
|
254
|
+
if (fs.existsSync(pyprojectPath) || fs.existsSync(requirementsPath)) {
|
|
255
|
+
stack.language = 'Python';
|
|
256
|
+
try {
|
|
257
|
+
const content = fs.existsSync(pyprojectPath)
|
|
258
|
+
? fs.readFileSync(pyprojectPath, 'utf-8')
|
|
259
|
+
: fs.readFileSync(requirementsPath, 'utf-8');
|
|
260
|
+
if (content.includes('fastapi')) stack.framework = 'FastAPI';
|
|
261
|
+
else if (content.includes('django')) stack.framework = 'Django';
|
|
262
|
+
else if (content.includes('flask')) stack.framework = 'Flask';
|
|
263
|
+
} catch (_err) {
|
|
264
|
+
// Non-critical
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Detect Go
|
|
269
|
+
if (fs.existsSync(path.join(memberPath, 'go.mod'))) {
|
|
270
|
+
stack.language = 'Go';
|
|
271
|
+
try {
|
|
272
|
+
const goMod = fs.readFileSync(path.join(memberPath, 'go.mod'), 'utf-8');
|
|
273
|
+
if (goMod.includes('gin-gonic')) stack.framework = 'Gin';
|
|
274
|
+
else if (goMod.includes('echo')) stack.framework = 'Echo';
|
|
275
|
+
else if (goMod.includes('fiber')) stack.framework = 'Fiber';
|
|
276
|
+
} catch (_err) {
|
|
277
|
+
// Non-critical
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return stack;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================
|
|
285
|
+
// Workspace Config & Manifest Generation
|
|
286
|
+
// ============================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Auto-detect role based on endpoints
|
|
290
|
+
* @param {{ provides: string[], consumes: string[] }} endpoints
|
|
291
|
+
* @returns {string} role
|
|
292
|
+
*/
|
|
293
|
+
function autoDetectRole(endpoints) {
|
|
294
|
+
const hasProvides = endpoints.provides.length > 0;
|
|
295
|
+
const hasConsumes = endpoints.consumes.length > 0;
|
|
296
|
+
|
|
297
|
+
if (hasProvides && hasConsumes) return 'both';
|
|
298
|
+
if (hasProvides) return 'provider';
|
|
299
|
+
if (hasConsumes) return 'consumer';
|
|
300
|
+
return 'standalone';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generate the workspace config (wogi-workspace.json)
|
|
305
|
+
* @param {string} workspaceName
|
|
306
|
+
* @param {Array} members — array of { name, role, path }
|
|
307
|
+
* @returns {Object} workspace config
|
|
308
|
+
*/
|
|
309
|
+
function generateWorkspaceConfig(workspaceName, members) {
|
|
310
|
+
const config = {
|
|
311
|
+
$schema: './workspace-config.schema.json',
|
|
312
|
+
name: workspaceName,
|
|
313
|
+
version: '1.0.0',
|
|
314
|
+
members: {},
|
|
315
|
+
routing: {
|
|
316
|
+
default: 'auto',
|
|
317
|
+
providerFirst: true
|
|
318
|
+
},
|
|
319
|
+
contracts: {
|
|
320
|
+
autoGenerate: true,
|
|
321
|
+
format: 'openapi',
|
|
322
|
+
path: '.workspace/contracts'
|
|
323
|
+
},
|
|
324
|
+
messages: {
|
|
325
|
+
autoNotify: true,
|
|
326
|
+
path: '.workspace/messages'
|
|
327
|
+
},
|
|
328
|
+
sync: {
|
|
329
|
+
autoOnSessionStart: true,
|
|
330
|
+
autoAfterTaskComplete: true
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
for (const member of members) {
|
|
335
|
+
config.members[member.name] = {
|
|
336
|
+
path: `./${member.name}`,
|
|
337
|
+
role: member.role
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return config;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Generate the workspace manifest from member metadata
|
|
346
|
+
* @param {string} workspaceName
|
|
347
|
+
* @param {Array} members — enriched member objects
|
|
348
|
+
* @returns {Object} manifest
|
|
349
|
+
*/
|
|
350
|
+
function generateManifest(workspaceName, members) {
|
|
351
|
+
const manifest = {
|
|
352
|
+
workspace: workspaceName,
|
|
353
|
+
version: '1.0.0',
|
|
354
|
+
generatedAt: new Date().toISOString(),
|
|
355
|
+
members: {},
|
|
356
|
+
integrations: {
|
|
357
|
+
matched: [],
|
|
358
|
+
orphanedConsumers: [],
|
|
359
|
+
orphanedProviders: [],
|
|
360
|
+
typeDrift: []
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
for (const member of members) {
|
|
365
|
+
manifest.members[member.name] = {
|
|
366
|
+
path: `./${member.name}`,
|
|
367
|
+
role: member.role,
|
|
368
|
+
stack: member.stack,
|
|
369
|
+
capabilities: member.capabilities,
|
|
370
|
+
provides: member.endpoints.provides,
|
|
371
|
+
consumes: member.endpoints.consumes,
|
|
372
|
+
lastSynced: new Date().toISOString()
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Cross-reference endpoints to find integration points
|
|
377
|
+
const allProviders = new Map(); // endpoint → [memberName]
|
|
378
|
+
const allConsumers = new Map(); // endpoint → [memberName]
|
|
379
|
+
|
|
380
|
+
for (const member of members) {
|
|
381
|
+
for (const ep of member.endpoints.provides) {
|
|
382
|
+
if (!allProviders.has(ep)) allProviders.set(ep, []);
|
|
383
|
+
allProviders.get(ep).push(member.name);
|
|
384
|
+
}
|
|
385
|
+
for (const ep of member.endpoints.consumes) {
|
|
386
|
+
// Normalize consumer endpoints for matching (strip base URL, query params)
|
|
387
|
+
const normalized = normalizeEndpoint(ep);
|
|
388
|
+
if (!allConsumers.has(normalized)) allConsumers.set(normalized, []);
|
|
389
|
+
allConsumers.get(normalized).push(member.name);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Find matches and orphans
|
|
394
|
+
for (const [ep, consumers] of allConsumers) {
|
|
395
|
+
// Try to match against providers (fuzzy — same method + similar path)
|
|
396
|
+
let matched = false;
|
|
397
|
+
for (const [providerEp, providers] of allProviders) {
|
|
398
|
+
if (endpointsMatch(ep, providerEp)) {
|
|
399
|
+
manifest.integrations.matched.push({
|
|
400
|
+
endpoint: ep,
|
|
401
|
+
providers,
|
|
402
|
+
consumers
|
|
403
|
+
});
|
|
404
|
+
matched = true;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (!matched) {
|
|
409
|
+
manifest.integrations.orphanedConsumers.push({
|
|
410
|
+
endpoint: ep,
|
|
411
|
+
consumers
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Find providers with no consumers
|
|
417
|
+
for (const [ep, providers] of allProviders) {
|
|
418
|
+
let hasConsumer = false;
|
|
419
|
+
for (const [consumerEp] of allConsumers) {
|
|
420
|
+
if (endpointsMatch(consumerEp, ep)) {
|
|
421
|
+
hasConsumer = true;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!hasConsumer) {
|
|
426
|
+
manifest.integrations.orphanedProviders.push({
|
|
427
|
+
endpoint: ep,
|
|
428
|
+
providers
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return manifest;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Normalize an endpoint for matching (strip query params, base URL)
|
|
438
|
+
*/
|
|
439
|
+
function normalizeEndpoint(ep) {
|
|
440
|
+
// Remove base URL if present
|
|
441
|
+
let normalized = ep.replace(/https?:\/\/[^/]+/, '');
|
|
442
|
+
// Remove query params
|
|
443
|
+
normalized = normalized.replace(/\?.*$/, '');
|
|
444
|
+
// Normalize path params: /users/123 → /users/:id
|
|
445
|
+
normalized = normalized.replace(/\/\d+/g, '/:id');
|
|
446
|
+
return normalized.trim();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Check if two endpoints match (same method + similar path)
|
|
451
|
+
*/
|
|
452
|
+
function endpointsMatch(ep1, ep2) {
|
|
453
|
+
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
454
|
+
let [method1, ...pathParts1] = ep1.split(' ');
|
|
455
|
+
let [method2, ...pathParts2] = ep2.split(' ');
|
|
456
|
+
|
|
457
|
+
// If the first token isn't a recognized HTTP method, treat the whole string as a path
|
|
458
|
+
if (!HTTP_METHODS.has(method1.toUpperCase())) {
|
|
459
|
+
pathParts1 = [ep1];
|
|
460
|
+
method1 = 'GET';
|
|
461
|
+
}
|
|
462
|
+
if (!HTTP_METHODS.has(method2.toUpperCase())) {
|
|
463
|
+
pathParts2 = [ep2];
|
|
464
|
+
method2 = 'GET';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
method1 = method1.toUpperCase();
|
|
468
|
+
method2 = method2.toUpperCase();
|
|
469
|
+
|
|
470
|
+
const path1 = pathParts1.join(' ').trim();
|
|
471
|
+
const path2 = pathParts2.join(' ').trim();
|
|
472
|
+
|
|
473
|
+
if (method1 !== method2) return false;
|
|
474
|
+
|
|
475
|
+
// Exact match
|
|
476
|
+
if (path1 === path2) return true;
|
|
477
|
+
|
|
478
|
+
// Normalize and compare
|
|
479
|
+
const norm1 = normalizeEndpoint(path1);
|
|
480
|
+
const norm2 = normalizeEndpoint(path2);
|
|
481
|
+
return norm1 === norm2;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ============================================================
|
|
485
|
+
// CLAUDE.md Generation
|
|
486
|
+
// ============================================================
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Generate workspace-level CLAUDE.md
|
|
490
|
+
* @param {Object} config — workspace config
|
|
491
|
+
* @param {Object} manifest — workspace manifest
|
|
492
|
+
* @returns {string} CLAUDE.md content
|
|
493
|
+
*/
|
|
494
|
+
function generateWorkspaceClaudeMd(config, manifest) {
|
|
495
|
+
const memberLines = Object.entries(manifest.members).map(([name, m]) => {
|
|
496
|
+
return `| ${name} | ${m.role} | ${m.stack.language}/${m.stack.framework} | ${m.provides.length} provided, ${m.consumes.length} consumed |`;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const matchedCount = manifest.integrations.matched.length;
|
|
500
|
+
const orphanCount = manifest.integrations.orphanedConsumers.length;
|
|
501
|
+
const memberNames = Object.keys(manifest.members);
|
|
502
|
+
const providers = Object.entries(manifest.members).filter(([_, m]) => m.role === 'provider' || m.role === 'both').map(([n]) => n);
|
|
503
|
+
const consumers = Object.entries(manifest.members).filter(([_, m]) => m.role === 'consumer' || m.role === 'both').map(([n]) => n);
|
|
504
|
+
|
|
505
|
+
return `# Wogi Workspace: ${config.name}
|
|
506
|
+
|
|
507
|
+
You are a **workspace manager** coordinating ${memberNames.length} repositories. You do NOT read source code directly. You read WogiFlow state files to understand each repo, then delegate implementation to repo-scoped sub-agents.
|
|
508
|
+
|
|
509
|
+
## CRITICAL RULES
|
|
510
|
+
|
|
511
|
+
1. **NEVER read source code** in member repos directly. Read \`.workflow/state/\` files (api-map.md, app-map.md, decisions.md) for context.
|
|
512
|
+
2. **ALWAYS delegate implementation** to sub-agents. You plan and coordinate — sub-agents write code.
|
|
513
|
+
3. **Provider before consumer.** When both need changes, implement the provider side first.
|
|
514
|
+
4. **Write messages after cross-repo changes.** Every change that affects another repo gets a message in \`.workspace/messages/\`.
|
|
515
|
+
|
|
516
|
+
## Member Repos
|
|
517
|
+
|
|
518
|
+
| Repo | Role | Stack | Endpoints |
|
|
519
|
+
|------|------|-------|-----------|
|
|
520
|
+
${memberLines.join('\n')}
|
|
521
|
+
|
|
522
|
+
## Session Startup Checklist
|
|
523
|
+
|
|
524
|
+
When you start a session, do this FIRST:
|
|
525
|
+
1. Read \`.workspace/state/workspace-manifest.json\` — understand the current integration map
|
|
526
|
+
2. Check \`.workspace/messages/\` for unread messages (status: "pending") — show them to the user
|
|
527
|
+
3. Read each member's \`.workflow/state/ready.json\` — know what tasks are in progress
|
|
528
|
+
|
|
529
|
+
## How to Route Tasks
|
|
530
|
+
|
|
531
|
+
### Step 1: Classify the Task
|
|
532
|
+
|
|
533
|
+
Read the user's request and determine:
|
|
534
|
+
- **Single-repo** — only affects one repo (e.g., "add a button to the login page" → ${consumers[0] || 'frontend'})
|
|
535
|
+
- **Cross-repo** — affects multiple repos (e.g., "add avatar upload" → needs endpoint + UI)
|
|
536
|
+
- **Bug investigation** — something is broken, unclear which repo
|
|
537
|
+
|
|
538
|
+
### Step 2: Routing Keywords
|
|
539
|
+
|
|
540
|
+
${Object.entries(manifest.members).map(([name, m]) => {
|
|
541
|
+
const keywords = [];
|
|
542
|
+
if (m.role === 'consumer' || m.role === 'both') keywords.push('page', 'component', 'UI', 'style', 'frontend', 'screen', 'form');
|
|
543
|
+
if (m.role === 'provider' || m.role === 'both') keywords.push('endpoint', 'model', 'database', 'migration', 'backend', 'API', 'query');
|
|
544
|
+
if (m.role === 'library') keywords.push('shared', 'utility', 'types', 'common');
|
|
545
|
+
return `- **${name}** (${m.role}): ${keywords.join(', ')}`;
|
|
546
|
+
}).join('\n')}
|
|
547
|
+
- **Both/all repos**: api contract, schema change, integration, full-stack, end-to-end
|
|
548
|
+
|
|
549
|
+
### Step 3: Execute
|
|
550
|
+
|
|
551
|
+
**Single-repo task — spawn one sub-agent:**
|
|
552
|
+
\`\`\`
|
|
553
|
+
Use the Agent tool:
|
|
554
|
+
prompt: "You are working in the <REPO> repository.
|
|
555
|
+
Stack: <STACK>
|
|
556
|
+
Task: <TASK DESCRIPTION>
|
|
557
|
+
|
|
558
|
+
Project rules (read from <REPO>/.workflow/state/decisions.md):
|
|
559
|
+
<PASTE DECISIONS CONTENT>
|
|
560
|
+
|
|
561
|
+
After completing: commit your changes with a descriptive message."
|
|
562
|
+
|
|
563
|
+
The sub-agent runs with the full codebase of that repo available.
|
|
564
|
+
It follows that repo's own WogiFlow rules, decisions, and patterns.
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
**Cross-repo task — sequential delegation:**
|
|
568
|
+
1. Read the contract from \`.workspace/contracts/\` (if exists)
|
|
569
|
+
2. If the API contract needs updating, update it first
|
|
570
|
+
3. Spawn provider sub-agent (${providers.join(' or ')}) — implement the API side
|
|
571
|
+
4. Read what the provider changed (from its git diff or commit message)
|
|
572
|
+
5. Write a message to \`.workspace/messages/\` describing the change
|
|
573
|
+
6. Spawn consumer sub-agent (${consumers.join(' or ')}) — implement the client side, include the message as context
|
|
574
|
+
7. Verify both sides work together
|
|
575
|
+
|
|
576
|
+
**Bug investigation — parallel agents:**
|
|
577
|
+
\`\`\`
|
|
578
|
+
Spawn ${memberNames.length} agents IN PARALLEL (single message with multiple Agent tool calls):
|
|
579
|
+
|
|
580
|
+
Agent 1 (${memberNames[0]}):
|
|
581
|
+
"Investigate this bug in ${memberNames[0]}: <BUG DESCRIPTION>
|
|
582
|
+
Check: recent changes, error logs, relevant code.
|
|
583
|
+
Report: Is the issue on YOUR side? What did you find?"
|
|
584
|
+
|
|
585
|
+
${memberNames.length > 1 ? `Agent 2 (${memberNames[1]}):
|
|
586
|
+
"Investigate this bug in ${memberNames[1]}: <BUG DESCRIPTION>
|
|
587
|
+
Check: recent changes, error logs, relevant code.
|
|
588
|
+
Report: Is the issue on YOUR side? What did you find?"` : ''}
|
|
589
|
+
|
|
590
|
+
Then synthesize the findings and route the fix to the correct repo.
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
## Reading Member State (What to Read, When)
|
|
594
|
+
|
|
595
|
+
| When | What to Read | Path |
|
|
596
|
+
|------|-------------|------|
|
|
597
|
+
| Before routing a task | api-map.md | \`<repo>/.workflow/state/api-map.md\` |
|
|
598
|
+
| Before spawning an agent | decisions.md | \`<repo>/.workflow/state/decisions.md\` |
|
|
599
|
+
| To understand structure | app-map.md | \`<repo>/.workflow/state/app-map.md\` |
|
|
600
|
+
| To check data models | schema-map.md | \`<repo>/.workflow/state/schema-map.md\` |
|
|
601
|
+
| To check task status | ready.json | \`<repo>/.workflow/state/ready.json\` |
|
|
602
|
+
|
|
603
|
+
**NEVER read**: source files (\`src/\`, \`lib/\`, \`app/\`, etc.) — leave that to sub-agents.
|
|
604
|
+
|
|
605
|
+
## Integration Map
|
|
606
|
+
|
|
607
|
+
- **${matchedCount}** matched endpoint pair${matchedCount !== 1 ? 's' : ''} (provider ↔ consumer)
|
|
608
|
+
${orphanCount > 0 ? `- **${orphanCount}** orphaned consumer${orphanCount !== 1 ? 's' : ''} (calling endpoints with no provider) ⚠️` : '- No orphaned consumers ✓'}
|
|
609
|
+
|
|
610
|
+
Full details: \`.workspace/state/workspace-manifest.json\`
|
|
611
|
+
Visual map: \`.workspace/state/integration-map.md\`
|
|
612
|
+
|
|
613
|
+
## Message Bus
|
|
614
|
+
|
|
615
|
+
After ANY cross-repo change, write a message to \`.workspace/messages/\`:
|
|
616
|
+
|
|
617
|
+
\`\`\`json
|
|
618
|
+
{
|
|
619
|
+
"id": "msg-<random-8-hex>",
|
|
620
|
+
"from": "<repo-that-changed>",
|
|
621
|
+
"to": "<affected-repo>",
|
|
622
|
+
"type": "contract-change",
|
|
623
|
+
"subject": "Changed POST /api/users — added email_verified field",
|
|
624
|
+
"body": "Details of what changed and why...",
|
|
625
|
+
"actionRequired": true,
|
|
626
|
+
"status": "pending",
|
|
627
|
+
"timestamp": "<ISO-8601>"
|
|
628
|
+
}
|
|
629
|
+
\`\`\`
|
|
630
|
+
|
|
631
|
+
**Message types**: \`contract-change\`, \`question\`, \`bug-report\`, \`task-complete\`, \`needs-help\`, \`heads-up\`
|
|
632
|
+
|
|
633
|
+
When spawning a sub-agent, check for pending messages to that repo and include them in the prompt context.
|
|
634
|
+
|
|
635
|
+
## Contracts
|
|
636
|
+
|
|
637
|
+
Shared API contracts: \`.workspace/contracts/\`
|
|
638
|
+
When a provider changes an endpoint, update the contract BEFORE the consumer implements.
|
|
639
|
+
|
|
640
|
+
## Workspace Commands
|
|
641
|
+
|
|
642
|
+
| Command | What it does |
|
|
643
|
+
|---------|-------------|
|
|
644
|
+
| \`flow workspace sync\` | Re-read all member state files, update manifest |
|
|
645
|
+
| \`flow workspace status\` | Show all repos, tasks, messages, contracts |
|
|
646
|
+
| \`flow workspace add <path>\` | Add a new member repo |
|
|
647
|
+
| \`flow workspace remove <name>\` | Remove a member repo |
|
|
648
|
+
|
|
649
|
+
## File Reference
|
|
650
|
+
|
|
651
|
+
| File | Purpose |
|
|
652
|
+
|------|---------|
|
|
653
|
+
| \`wogi-workspace.json\` | Workspace config (members, roles, settings) |
|
|
654
|
+
| \`.workspace/state/workspace-manifest.json\` | Auto-generated member metadata + integration map |
|
|
655
|
+
| \`.workspace/state/integration-map.md\` | Human-readable integration visualization |
|
|
656
|
+
| \`.workspace/state/ready.json\` | Workspace-level cross-repo tasks |
|
|
657
|
+
| \`.workspace/state/decisions.md\` | Shared cross-repo rules |
|
|
658
|
+
| \`.workspace/contracts/\` | Shared API contracts (OpenAPI, TypeScript, etc.) |
|
|
659
|
+
| \`.workspace/messages/\` | Agent-to-agent messages |
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
Generated by Wogi Workspace v1.0.0
|
|
663
|
+
Last synced: ${new Date().toISOString()}
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ============================================================
|
|
668
|
+
// Settings.json Generation
|
|
669
|
+
// ============================================================
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Generate workspace-level .claude/settings.json
|
|
673
|
+
* Minimal hooks — workspace doesn't need validation/linting hooks.
|
|
674
|
+
* @returns {Object} settings config
|
|
675
|
+
*/
|
|
676
|
+
function generateWorkspaceSettings(memberNames) {
|
|
677
|
+
// Build read patterns for member state files
|
|
678
|
+
const readPatterns = [];
|
|
679
|
+
const bashPatterns = [
|
|
680
|
+
'Bash(git status *)',
|
|
681
|
+
'Bash(git log *)',
|
|
682
|
+
'Bash(git diff *)',
|
|
683
|
+
'Bash(git show *)',
|
|
684
|
+
'Bash(git branch *)',
|
|
685
|
+
'Bash(git add *)',
|
|
686
|
+
'Bash(git commit *)',
|
|
687
|
+
'Bash(git push *)',
|
|
688
|
+
'Bash(git pull *)',
|
|
689
|
+
'Bash(git fetch *)',
|
|
690
|
+
'Bash(node --check *)',
|
|
691
|
+
'Bash(node scripts/*)',
|
|
692
|
+
'Bash(ls *)',
|
|
693
|
+
'Bash(cat *)'
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
for (const name of (memberNames || [])) {
|
|
697
|
+
readPatterns.push(`Read(${name}/.workflow/**)`);
|
|
698
|
+
readPatterns.push(`Read(${name}/package.json)`);
|
|
699
|
+
readPatterns.push(`Read(${name}/pyproject.toml)`);
|
|
700
|
+
readPatterns.push(`Read(${name}/go.mod)`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
permissions: {
|
|
705
|
+
allow: [
|
|
706
|
+
// Allow reading member state files
|
|
707
|
+
...readPatterns,
|
|
708
|
+
// Allow reading workspace state
|
|
709
|
+
'Read(.workspace/**)',
|
|
710
|
+
'Read(wogi-workspace.json)',
|
|
711
|
+
'Read(CLAUDE.md)',
|
|
712
|
+
// Allow writing workspace state
|
|
713
|
+
'Write(.workspace/**)',
|
|
714
|
+
'Edit(.workspace/**)',
|
|
715
|
+
// Allow git and node
|
|
716
|
+
...bashPatterns
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
hooks: {},
|
|
720
|
+
_wogiWorkspace: true,
|
|
721
|
+
_wogiFlowVersion: require('../package.json').version,
|
|
722
|
+
_comment: 'Workspace-level settings. Member repos have their own settings. Sub-agents spawned for member repos use THEIR settings, not these.'
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ============================================================
|
|
727
|
+
// Directory Structure Creation
|
|
728
|
+
// ============================================================
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Create the .workspace/ directory structure
|
|
732
|
+
* @param {string} workspaceRoot
|
|
733
|
+
*/
|
|
734
|
+
function createWorkspaceStructure(workspaceRoot) {
|
|
735
|
+
const wsDir = path.join(workspaceRoot, WORKSPACE_DIR);
|
|
736
|
+
|
|
737
|
+
// Create main dirs
|
|
738
|
+
for (const dir of WORKSPACE_DIRS) {
|
|
739
|
+
const dirPath = path.join(wsDir, dir);
|
|
740
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Create .claude/ for workspace settings
|
|
744
|
+
const claudeDir = path.join(workspaceRoot, '.claude');
|
|
745
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
746
|
+
|
|
747
|
+
// Create empty ready.json for workspace-level tasks
|
|
748
|
+
const readyPath = path.join(wsDir, 'state', 'ready.json');
|
|
749
|
+
if (!fs.existsSync(readyPath)) {
|
|
750
|
+
fs.writeFileSync(readyPath, JSON.stringify({
|
|
751
|
+
lastUpdated: new Date().toISOString(),
|
|
752
|
+
inProgress: [],
|
|
753
|
+
ready: [],
|
|
754
|
+
blocked: [],
|
|
755
|
+
recentlyCompleted: []
|
|
756
|
+
}, null, 2));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Create workspace decisions.md
|
|
760
|
+
const decisionsPath = path.join(wsDir, 'state', 'decisions.md');
|
|
761
|
+
if (!fs.existsSync(decisionsPath)) {
|
|
762
|
+
fs.writeFileSync(decisionsPath, `# Workspace Decisions
|
|
763
|
+
|
|
764
|
+
Cross-repo rules that apply to all member repositories.
|
|
765
|
+
|
|
766
|
+
## Shared Conventions
|
|
767
|
+
|
|
768
|
+
<!-- Add shared rules here, e.g.: -->
|
|
769
|
+
<!-- ### Date Format -->
|
|
770
|
+
<!-- All dates use ISO 8601 (YYYY-MM-DDTHH:mm:ssZ) across all repos. -->
|
|
771
|
+
`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Create .gitignore for workspace transient files
|
|
775
|
+
const gitignorePath = path.join(wsDir, '.gitignore');
|
|
776
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
777
|
+
fs.writeFileSync(gitignorePath, `# Wogi Workspace — transient files
|
|
778
|
+
# Messages are ephemeral (processed and resolved)
|
|
779
|
+
messages/*.json
|
|
780
|
+
# State is auto-generated from member repos
|
|
781
|
+
state/workspace-manifest.json
|
|
782
|
+
state/integration-map.md
|
|
783
|
+
state/contract-versions.json
|
|
784
|
+
`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ============================================================
|
|
789
|
+
// Main Init Function
|
|
790
|
+
// ============================================================
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Initialize a Wogi Workspace
|
|
794
|
+
* @param {string[]} args — CLI arguments
|
|
795
|
+
*/
|
|
796
|
+
async function initWorkspace(args) {
|
|
797
|
+
const workspaceRoot = process.cwd();
|
|
798
|
+
const workspaceName = path.basename(workspaceRoot);
|
|
799
|
+
|
|
800
|
+
// Check if workspace already exists
|
|
801
|
+
if (fs.existsSync(path.join(workspaceRoot, WORKSPACE_CONFIG_FILE))) {
|
|
802
|
+
console.error(`Workspace already initialized in ${workspaceRoot}`);
|
|
803
|
+
console.error('Use `flow workspace sync` to update.');
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
console.log('🔍 Scanning for WogiFlow-enabled projects...\n');
|
|
808
|
+
|
|
809
|
+
// Discover member repos
|
|
810
|
+
const discovered = discoverMembers(workspaceRoot);
|
|
811
|
+
|
|
812
|
+
if (discovered.length === 0) {
|
|
813
|
+
console.error('No WogiFlow-enabled projects found in subdirectories.');
|
|
814
|
+
console.error('Each member repo must have a .workflow/ directory (run `flow init` in each first).');
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
console.log(`Found ${discovered.length} WogiFlow project${discovered.length !== 1 ? 's' : ''}:`);
|
|
819
|
+
for (const m of discovered) {
|
|
820
|
+
console.log(` ✓ ${m.name}/`);
|
|
821
|
+
}
|
|
822
|
+
console.log('');
|
|
823
|
+
|
|
824
|
+
// Read metadata from each member
|
|
825
|
+
console.log('── Reading project metadata ──────────────────\n');
|
|
826
|
+
const members = [];
|
|
827
|
+
|
|
828
|
+
for (const disc of discovered) {
|
|
829
|
+
const metadata = readMemberMetadata(disc.workflowPath);
|
|
830
|
+
const stack = detectStack(metadata, disc.path);
|
|
831
|
+
const capabilities = extractCapabilities(metadata);
|
|
832
|
+
const endpoints = extractEndpoints(metadata);
|
|
833
|
+
const role = autoDetectRole(endpoints);
|
|
834
|
+
|
|
835
|
+
members.push({
|
|
836
|
+
name: disc.name,
|
|
837
|
+
path: disc.path,
|
|
838
|
+
workflowPath: disc.workflowPath,
|
|
839
|
+
metadata,
|
|
840
|
+
stack,
|
|
841
|
+
capabilities,
|
|
842
|
+
endpoints,
|
|
843
|
+
role
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
const capsSummary = Object.entries(capabilities)
|
|
847
|
+
.filter(([_, v]) => v > 0)
|
|
848
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
849
|
+
.join(', ') || 'no data yet';
|
|
850
|
+
|
|
851
|
+
console.log(` ${disc.name}/ (${stack.language}/${stack.framework})`);
|
|
852
|
+
console.log(` Role: ${role} | ${capsSummary}`);
|
|
853
|
+
console.log(` Provides: ${endpoints.provides.length} endpoints | Consumes: ${endpoints.consumes.length} endpoints`);
|
|
854
|
+
console.log('');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Create directory structure
|
|
858
|
+
console.log('── Creating workspace structure ──────────────\n');
|
|
859
|
+
createWorkspaceStructure(workspaceRoot);
|
|
860
|
+
console.log(' ✓ .workspace/state/');
|
|
861
|
+
console.log(' ✓ .workspace/contracts/');
|
|
862
|
+
console.log(' ✓ .workspace/messages/');
|
|
863
|
+
console.log(' ✓ .workspace/specs/');
|
|
864
|
+
console.log('');
|
|
865
|
+
|
|
866
|
+
// Generate workspace config
|
|
867
|
+
const config = generateWorkspaceConfig(workspaceName, members);
|
|
868
|
+
fs.writeFileSync(
|
|
869
|
+
path.join(workspaceRoot, WORKSPACE_CONFIG_FILE),
|
|
870
|
+
JSON.stringify(config, null, 2)
|
|
871
|
+
);
|
|
872
|
+
console.log(` ✓ ${WORKSPACE_CONFIG_FILE}`);
|
|
873
|
+
|
|
874
|
+
// Generate manifest
|
|
875
|
+
const manifest = generateManifest(workspaceName, members);
|
|
876
|
+
fs.writeFileSync(
|
|
877
|
+
path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'workspace-manifest.json'),
|
|
878
|
+
JSON.stringify(manifest, null, 2)
|
|
879
|
+
);
|
|
880
|
+
console.log(' ✓ .workspace/state/workspace-manifest.json');
|
|
881
|
+
|
|
882
|
+
// Generate integration map (human-readable markdown)
|
|
883
|
+
const integrationMap = generateIntegrationMap(manifest);
|
|
884
|
+
fs.writeFileSync(
|
|
885
|
+
path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'integration-map.md'),
|
|
886
|
+
integrationMap
|
|
887
|
+
);
|
|
888
|
+
console.log(' ✓ .workspace/state/integration-map.md');
|
|
889
|
+
|
|
890
|
+
// Generate CLAUDE.md
|
|
891
|
+
const claudeMd = generateWorkspaceClaudeMd(config, manifest);
|
|
892
|
+
fs.writeFileSync(path.join(workspaceRoot, 'CLAUDE.md'), claudeMd);
|
|
893
|
+
console.log(' ✓ CLAUDE.md (workspace manager instructions)');
|
|
894
|
+
|
|
895
|
+
// Generate settings.json
|
|
896
|
+
const settings = generateWorkspaceSettings(members.map(m => m.name));
|
|
897
|
+
fs.writeFileSync(
|
|
898
|
+
path.join(workspaceRoot, '.claude', 'settings.json'),
|
|
899
|
+
JSON.stringify(settings, null, 2)
|
|
900
|
+
);
|
|
901
|
+
console.log(' ✓ .claude/settings.json');
|
|
902
|
+
console.log('');
|
|
903
|
+
|
|
904
|
+
// Summary
|
|
905
|
+
const matched = manifest.integrations.matched.length;
|
|
906
|
+
const orphanedC = manifest.integrations.orphanedConsumers.length;
|
|
907
|
+
const orphanedP = manifest.integrations.orphanedProviders.length;
|
|
908
|
+
|
|
909
|
+
console.log('── Integration Summary ──────────────────────\n');
|
|
910
|
+
console.log(` ✓ ${matched} matched endpoint pair${matched !== 1 ? 's' : ''}`);
|
|
911
|
+
if (orphanedC > 0) {
|
|
912
|
+
console.log(` ⚠️ ${orphanedC} orphaned consumer${orphanedC !== 1 ? 's' : ''} (calling endpoints with no provider)`);
|
|
913
|
+
for (const orphan of manifest.integrations.orphanedConsumers) {
|
|
914
|
+
console.log(` → ${orphan.endpoint} (consumed by: ${orphan.consumers.join(', ')})`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (orphanedP > 0) {
|
|
918
|
+
console.log(` ℹ️ ${orphanedP} endpoint${orphanedP !== 1 ? 's' : ''} with no consumer`);
|
|
919
|
+
}
|
|
920
|
+
console.log('');
|
|
921
|
+
|
|
922
|
+
console.log(`✅ Workspace "${workspaceName}" initialized with ${members.length} member${members.length !== 1 ? 's' : ''}!`);
|
|
923
|
+
console.log('');
|
|
924
|
+
console.log('Next steps:');
|
|
925
|
+
console.log(" 1. Run 'claude' in this folder to start the workspace manager");
|
|
926
|
+
console.log(" 2. Give it tasks — it will route them to the right repo(s)");
|
|
927
|
+
console.log(" 3. Run 'flow workspace sync' after external changes");
|
|
928
|
+
console.log('');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Generate human-readable integration map markdown
|
|
933
|
+
* @param {Object} manifest
|
|
934
|
+
* @returns {string}
|
|
935
|
+
*/
|
|
936
|
+
function generateIntegrationMap(manifest) {
|
|
937
|
+
const lines = ['# Integration Map\n'];
|
|
938
|
+
lines.push(`Generated: ${manifest.generatedAt}\n`);
|
|
939
|
+
|
|
940
|
+
// Matched endpoints
|
|
941
|
+
if (manifest.integrations.matched.length > 0) {
|
|
942
|
+
lines.push('## Matched Endpoints\n');
|
|
943
|
+
lines.push('| Endpoint | Provider(s) | Consumer(s) |');
|
|
944
|
+
lines.push('|----------|-------------|-------------|');
|
|
945
|
+
for (const m of manifest.integrations.matched) {
|
|
946
|
+
lines.push(`| \`${m.endpoint}\` | ${m.providers.join(', ')} | ${m.consumers.join(', ')} |`);
|
|
947
|
+
}
|
|
948
|
+
lines.push('');
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Orphaned consumers
|
|
952
|
+
if (manifest.integrations.orphanedConsumers.length > 0) {
|
|
953
|
+
lines.push('## ⚠️ Orphaned Consumers\n');
|
|
954
|
+
lines.push('These repos call endpoints that no provider serves:\n');
|
|
955
|
+
for (const o of manifest.integrations.orphanedConsumers) {
|
|
956
|
+
lines.push(`- \`${o.endpoint}\` — consumed by: ${o.consumers.join(', ')}`);
|
|
957
|
+
}
|
|
958
|
+
lines.push('');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Orphaned providers
|
|
962
|
+
if (manifest.integrations.orphanedProviders.length > 0) {
|
|
963
|
+
lines.push('## ℹ️ Endpoints Without Consumers\n');
|
|
964
|
+
lines.push('These endpoints are served but no consumer calls them:\n');
|
|
965
|
+
for (const o of manifest.integrations.orphanedProviders) {
|
|
966
|
+
lines.push(`- \`${o.endpoint}\` — provided by: ${o.providers.join(', ')}`);
|
|
967
|
+
}
|
|
968
|
+
lines.push('');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Member summary
|
|
972
|
+
lines.push('## Members\n');
|
|
973
|
+
for (const [name, m] of Object.entries(manifest.members)) {
|
|
974
|
+
lines.push(`### ${name} (${m.role})`);
|
|
975
|
+
lines.push(`- **Stack**: ${m.stack.language} / ${m.stack.framework}`);
|
|
976
|
+
lines.push(`- **Provides**: ${m.provides.length > 0 ? m.provides.join(', ') : 'none'}`);
|
|
977
|
+
lines.push(`- **Consumes**: ${m.consumes.length > 0 ? m.consumes.join(', ') : 'none'}`);
|
|
978
|
+
lines.push('');
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return lines.join('\n');
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// ============================================================
|
|
985
|
+
// CLI Router
|
|
986
|
+
// ============================================================
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Handle workspace subcommands
|
|
990
|
+
* @param {string[]} args
|
|
991
|
+
*/
|
|
992
|
+
async function workspace(args) {
|
|
993
|
+
const subcommand = args[0];
|
|
994
|
+
|
|
995
|
+
switch (subcommand) {
|
|
996
|
+
case 'init':
|
|
997
|
+
await initWorkspace(args.slice(1));
|
|
998
|
+
break;
|
|
999
|
+
case 'sync': {
|
|
1000
|
+
const { syncWorkspace } = require('./workspace-sync');
|
|
1001
|
+
const result = syncWorkspace(process.cwd());
|
|
1002
|
+
console.log(`✓ Synced ${result.membersUpdated} member(s). ${result.changes.length} change(s) detected.`);
|
|
1003
|
+
if (result.warnings.length > 0) {
|
|
1004
|
+
for (const w of result.warnings) console.log(` ⚠️ ${w}`);
|
|
1005
|
+
}
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
case 'status': {
|
|
1009
|
+
const { getWorkspaceStatus } = require('./workspace-sync');
|
|
1010
|
+
console.log(getWorkspaceStatus(process.cwd()));
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
case 'add': {
|
|
1014
|
+
const { addMember } = require('./workspace-sync');
|
|
1015
|
+
const memberPath = args[1];
|
|
1016
|
+
const role = args[2];
|
|
1017
|
+
if (!memberPath) {
|
|
1018
|
+
console.error('Usage: flow workspace add <path> [role]');
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
const result = addMember(process.cwd(), memberPath, role);
|
|
1022
|
+
console.log(`✓ Added '${result.name}' as ${result.role}`);
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
case 'remove': {
|
|
1026
|
+
const { removeMember } = require('./workspace-sync');
|
|
1027
|
+
const name = args[1];
|
|
1028
|
+
if (!name) {
|
|
1029
|
+
console.error('Usage: flow workspace remove <name>');
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
removeMember(process.cwd(), name);
|
|
1033
|
+
console.log(`✓ Removed '${name}' from workspace`);
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
default:
|
|
1037
|
+
console.log(`
|
|
1038
|
+
Wogi Workspace — Multi-Repo Orchestration
|
|
1039
|
+
|
|
1040
|
+
Usage: flow workspace <command>
|
|
1041
|
+
|
|
1042
|
+
Commands:
|
|
1043
|
+
init Initialize a workspace from member repos
|
|
1044
|
+
sync Re-sync workspace manifest from member state files
|
|
1045
|
+
status Show unified workspace status
|
|
1046
|
+
add Add a member repo to the workspace
|
|
1047
|
+
remove Remove a member repo from the workspace
|
|
1048
|
+
|
|
1049
|
+
Examples:
|
|
1050
|
+
flow workspace init # Create workspace from subdirectories
|
|
1051
|
+
flow workspace sync # Refresh after external changes
|
|
1052
|
+
flow workspace status # Show all repos, tasks, contracts
|
|
1053
|
+
`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
module.exports = {
|
|
1058
|
+
workspace,
|
|
1059
|
+
initWorkspace,
|
|
1060
|
+
discoverMembers,
|
|
1061
|
+
readMemberMetadata,
|
|
1062
|
+
extractCapabilities,
|
|
1063
|
+
extractEndpoints,
|
|
1064
|
+
detectStack,
|
|
1065
|
+
generateWorkspaceConfig,
|
|
1066
|
+
generateManifest,
|
|
1067
|
+
generateWorkspaceClaudeMd,
|
|
1068
|
+
generateWorkspaceSettings,
|
|
1069
|
+
createWorkspaceStructure,
|
|
1070
|
+
WORKSPACE_CONFIG_FILE,
|
|
1071
|
+
WORKSPACE_DIR,
|
|
1072
|
+
MEMBER_ROLES
|
|
1073
|
+
};
|