wogiflow 2.4.3 → 2.5.0
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-audit.md +26 -0
- package/.claude/commands/wogi-review.md +29 -0
- package/.claude/commands/wogi-start.md +124 -0
- package/.claude/docs/claude-code-compatibility.md +24 -0
- package/.claude/docs/explore-agents.md +19 -2
- package/.claude/settings.json +11 -0
- package/bin/flow +11 -1
- package/lib/workspace-channel-server.js +364 -0
- 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 +782 -0
- package/lib/workspace-sync.js +339 -0
- package/lib/workspace.js +1349 -0
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +28 -0
- package/scripts/flow-eval-calibration.js +257 -0
- package/scripts/flow-eval-judge.js +10 -1
- package/scripts/flow-eval.js +9 -0
- package/scripts/flow-schema-drift.js +837 -0
- package/scripts/hooks/adapters/claude-code.js +29 -0
- package/scripts/hooks/core/task-created.js +83 -0
- package/scripts/hooks/entry/claude-code/task-created.js +15 -0
- package/scripts/postinstall.js +2 -0
package/lib/workspace.js
ADDED
|
@@ -0,0 +1,1349 @@
|
|
|
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
|
+
channels: {
|
|
333
|
+
enabled: true,
|
|
334
|
+
basePort: 8801,
|
|
335
|
+
members: {}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
let port = config.channels.basePort;
|
|
340
|
+
if (port + members.length - 1 > 65535) {
|
|
341
|
+
throw new Error(`Channel port range ${port}-${port + members.length - 1} exceeds maximum port 65535. Reduce basePort or number of members.`);
|
|
342
|
+
}
|
|
343
|
+
for (const member of members) {
|
|
344
|
+
config.members[member.name] = {
|
|
345
|
+
path: `./${member.name}`,
|
|
346
|
+
role: member.role
|
|
347
|
+
};
|
|
348
|
+
config.channels.members[member.name] = { port: port++ };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return config;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Generate the workspace manifest from member metadata
|
|
356
|
+
* @param {string} workspaceName
|
|
357
|
+
* @param {Array} members — enriched member objects
|
|
358
|
+
* @returns {Object} manifest
|
|
359
|
+
*/
|
|
360
|
+
function generateManifest(workspaceName, members) {
|
|
361
|
+
const manifest = {
|
|
362
|
+
workspace: workspaceName,
|
|
363
|
+
version: '1.0.0',
|
|
364
|
+
generatedAt: new Date().toISOString(),
|
|
365
|
+
members: {},
|
|
366
|
+
integrations: {
|
|
367
|
+
matched: [],
|
|
368
|
+
orphanedConsumers: [],
|
|
369
|
+
orphanedProviders: [],
|
|
370
|
+
typeDrift: []
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
for (const member of members) {
|
|
375
|
+
manifest.members[member.name] = {
|
|
376
|
+
path: `./${member.name}`,
|
|
377
|
+
role: member.role,
|
|
378
|
+
stack: member.stack,
|
|
379
|
+
capabilities: member.capabilities,
|
|
380
|
+
provides: member.endpoints.provides,
|
|
381
|
+
consumes: member.endpoints.consumes,
|
|
382
|
+
lastSynced: new Date().toISOString()
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Cross-reference endpoints to find integration points
|
|
387
|
+
const allProviders = new Map(); // endpoint → [memberName]
|
|
388
|
+
const allConsumers = new Map(); // endpoint → [memberName]
|
|
389
|
+
|
|
390
|
+
for (const member of members) {
|
|
391
|
+
for (const ep of member.endpoints.provides) {
|
|
392
|
+
if (!allProviders.has(ep)) allProviders.set(ep, []);
|
|
393
|
+
allProviders.get(ep).push(member.name);
|
|
394
|
+
}
|
|
395
|
+
for (const ep of member.endpoints.consumes) {
|
|
396
|
+
// Normalize consumer endpoints for matching (strip base URL, query params)
|
|
397
|
+
const normalized = normalizeEndpoint(ep);
|
|
398
|
+
if (!allConsumers.has(normalized)) allConsumers.set(normalized, []);
|
|
399
|
+
allConsumers.get(normalized).push(member.name);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Find matches and orphans
|
|
404
|
+
for (const [ep, consumers] of allConsumers) {
|
|
405
|
+
// Try to match against providers (fuzzy — same method + similar path)
|
|
406
|
+
let matched = false;
|
|
407
|
+
for (const [providerEp, providers] of allProviders) {
|
|
408
|
+
if (endpointsMatch(ep, providerEp)) {
|
|
409
|
+
manifest.integrations.matched.push({
|
|
410
|
+
endpoint: ep,
|
|
411
|
+
providers,
|
|
412
|
+
consumers
|
|
413
|
+
});
|
|
414
|
+
matched = true;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (!matched) {
|
|
419
|
+
manifest.integrations.orphanedConsumers.push({
|
|
420
|
+
endpoint: ep,
|
|
421
|
+
consumers
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Find providers with no consumers
|
|
427
|
+
for (const [ep, providers] of allProviders) {
|
|
428
|
+
let hasConsumer = false;
|
|
429
|
+
for (const [consumerEp] of allConsumers) {
|
|
430
|
+
if (endpointsMatch(consumerEp, ep)) {
|
|
431
|
+
hasConsumer = true;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!hasConsumer) {
|
|
436
|
+
manifest.integrations.orphanedProviders.push({
|
|
437
|
+
endpoint: ep,
|
|
438
|
+
providers
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return manifest;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Normalize an endpoint for matching (strip query params, base URL)
|
|
448
|
+
*/
|
|
449
|
+
function normalizeEndpoint(ep) {
|
|
450
|
+
// Remove base URL if present
|
|
451
|
+
let normalized = ep.replace(/https?:\/\/[^/]+/, '');
|
|
452
|
+
// Remove query params
|
|
453
|
+
normalized = normalized.replace(/\?.*$/, '');
|
|
454
|
+
// Normalize path params: /users/123 → /users/:id
|
|
455
|
+
normalized = normalized.replace(/\/\d+/g, '/:id');
|
|
456
|
+
return normalized.trim();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Check if two endpoints match (same method + similar path)
|
|
461
|
+
*/
|
|
462
|
+
function endpointsMatch(ep1, ep2) {
|
|
463
|
+
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
464
|
+
let [method1, ...pathParts1] = ep1.split(' ');
|
|
465
|
+
let [method2, ...pathParts2] = ep2.split(' ');
|
|
466
|
+
|
|
467
|
+
// If the first token isn't a recognized HTTP method, treat the whole string as a path
|
|
468
|
+
if (!HTTP_METHODS.has(method1.toUpperCase())) {
|
|
469
|
+
pathParts1 = [ep1];
|
|
470
|
+
method1 = 'GET';
|
|
471
|
+
}
|
|
472
|
+
if (!HTTP_METHODS.has(method2.toUpperCase())) {
|
|
473
|
+
pathParts2 = [ep2];
|
|
474
|
+
method2 = 'GET';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
method1 = method1.toUpperCase();
|
|
478
|
+
method2 = method2.toUpperCase();
|
|
479
|
+
|
|
480
|
+
const path1 = pathParts1.join(' ').trim();
|
|
481
|
+
const path2 = pathParts2.join(' ').trim();
|
|
482
|
+
|
|
483
|
+
if (method1 !== method2) return false;
|
|
484
|
+
|
|
485
|
+
// Exact match
|
|
486
|
+
if (path1 === path2) return true;
|
|
487
|
+
|
|
488
|
+
// Normalize and compare
|
|
489
|
+
const norm1 = normalizeEndpoint(path1);
|
|
490
|
+
const norm2 = normalizeEndpoint(path2);
|
|
491
|
+
return norm1 === norm2;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ============================================================
|
|
495
|
+
// CLAUDE.md Generation
|
|
496
|
+
// ============================================================
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Generate workspace-level CLAUDE.md
|
|
500
|
+
* @param {Object} config — workspace config
|
|
501
|
+
* @param {Object} manifest — workspace manifest
|
|
502
|
+
* @returns {string} CLAUDE.md content
|
|
503
|
+
*/
|
|
504
|
+
function generateWorkspaceClaudeMd(config, manifest) {
|
|
505
|
+
const memberLines = Object.entries(manifest.members).map(([name, m]) => {
|
|
506
|
+
return `| ${name} | ${m.role} | ${m.stack.language}/${m.stack.framework} | ${m.provides.length} provided, ${m.consumes.length} consumed |`;
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const matchedCount = manifest.integrations.matched.length;
|
|
510
|
+
const orphanCount = manifest.integrations.orphanedConsumers.length;
|
|
511
|
+
const memberNames = Object.keys(manifest.members);
|
|
512
|
+
const providers = Object.entries(manifest.members).filter(([_, m]) => m.role === 'provider' || m.role === 'both').map(([n]) => n);
|
|
513
|
+
const consumers = Object.entries(manifest.members).filter(([_, m]) => m.role === 'consumer' || m.role === 'both').map(([n]) => n);
|
|
514
|
+
|
|
515
|
+
return `# Wogi Workspace: ${config.name}
|
|
516
|
+
|
|
517
|
+
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.
|
|
518
|
+
|
|
519
|
+
## CRITICAL RULES
|
|
520
|
+
|
|
521
|
+
1. **NEVER read source code** in member repos directly. Read \`.workflow/state/\` files (api-map.md, app-map.md, decisions.md) for context.
|
|
522
|
+
2. **ALWAYS delegate implementation** to sub-agents. You plan and coordinate — sub-agents write code.
|
|
523
|
+
3. **Provider before consumer.** When both need changes, implement the provider side first.
|
|
524
|
+
4. **Write messages after cross-repo changes.** Every change that affects another repo gets a message in \`.workspace/messages/\`.
|
|
525
|
+
|
|
526
|
+
## Member Repos
|
|
527
|
+
|
|
528
|
+
| Repo | Role | Stack | Endpoints |
|
|
529
|
+
|------|------|-------|-----------|
|
|
530
|
+
${memberLines.join('\n')}
|
|
531
|
+
|
|
532
|
+
## Session Startup Checklist
|
|
533
|
+
|
|
534
|
+
When you start a session, do this FIRST:
|
|
535
|
+
1. Read \`.workspace/state/workspace-manifest.json\` — understand the current integration map
|
|
536
|
+
2. Check \`.workspace/messages/\` for unread messages (status: "pending") — show them to the user
|
|
537
|
+
3. Read each member's \`.workflow/state/ready.json\` — know what tasks are in progress
|
|
538
|
+
|
|
539
|
+
## How to Route Tasks
|
|
540
|
+
|
|
541
|
+
### Step 1: Classify the Task
|
|
542
|
+
|
|
543
|
+
Read the user's request and determine:
|
|
544
|
+
- **Single-repo** — only affects one repo (e.g., "add a button to the login page" → ${consumers[0] || 'frontend'})
|
|
545
|
+
- **Cross-repo** — affects multiple repos (e.g., "add avatar upload" → needs endpoint + UI)
|
|
546
|
+
- **Bug investigation** — something is broken, unclear which repo
|
|
547
|
+
|
|
548
|
+
### Step 2: Routing Keywords
|
|
549
|
+
|
|
550
|
+
${Object.entries(manifest.members).map(([name, m]) => {
|
|
551
|
+
const keywords = [];
|
|
552
|
+
if (m.role === 'consumer' || m.role === 'both') keywords.push('page', 'component', 'UI', 'style', 'frontend', 'screen', 'form');
|
|
553
|
+
if (m.role === 'provider' || m.role === 'both') keywords.push('endpoint', 'model', 'database', 'migration', 'backend', 'API', 'query');
|
|
554
|
+
if (m.role === 'library') keywords.push('shared', 'utility', 'types', 'common');
|
|
555
|
+
return `- **${name}** (${m.role}): ${keywords.join(', ')}`;
|
|
556
|
+
}).join('\n')}
|
|
557
|
+
- **Both/all repos**: api contract, schema change, integration, full-stack, end-to-end
|
|
558
|
+
|
|
559
|
+
### Step 3: Dispatch via Channels
|
|
560
|
+
|
|
561
|
+
Each worker repo runs a full Claude Code session with its own CLAUDE.md, hooks, and WogiFlow pipeline. You dispatch tasks by sending HTTP requests to their channel ports. **Do NOT use the Agent tool for implementation** — use channels for full enforcement.
|
|
562
|
+
|
|
563
|
+
**Worker Channel Ports:**
|
|
564
|
+
${Object.entries(config.channels?.members || {}).map(([name, ch]) => `- **${name}**: \`http://localhost:${ch.port}\``).join('\n')}
|
|
565
|
+
|
|
566
|
+
**Check if workers are running:**
|
|
567
|
+
\`\`\`bash
|
|
568
|
+
${Object.entries(config.channels?.members || {}).map(([name, ch]) => `curl -s http://localhost:${ch.port}/health`).join('\n')}
|
|
569
|
+
\`\`\`
|
|
570
|
+
|
|
571
|
+
If a worker is down, tell the user: "Start the ${'{'}repo{'}'} worker: \`cd ${'{'}repo{'}'}/ && flow workspace start\`"
|
|
572
|
+
|
|
573
|
+
**Single-repo task:**
|
|
574
|
+
1. Create task in the repo's ready.json using \`decomposeToRepoTasks()\` or directly
|
|
575
|
+
2. Dispatch: \`curl -s -X POST http://localhost:${'{'}port{'}'} -d "/wogi-start ${'{'}taskId${'}'}"\`
|
|
576
|
+
3. The worker's full WogiFlow pipeline handles the rest (explore, spec, implement, verify)
|
|
577
|
+
4. Monitor: check \`.workspace/messages/\` for a \`task-complete\` message
|
|
578
|
+
|
|
579
|
+
**Cross-repo task:**
|
|
580
|
+
1. Read the contract from \`.workspace/contracts/\` (if exists)
|
|
581
|
+
2. If the API contract needs updating, update it first
|
|
582
|
+
3. Create tasks in each repo's ready.json (provider gets P0 priority)
|
|
583
|
+
4. Dispatch to provider first: \`curl -s -X POST http://localhost:${providers[0] ? config.channels?.members?.[providers[0]]?.port || '{port}' : '{port}'} -d "/wogi-start ${'{'}providerTaskId${'}'}"\`
|
|
584
|
+
5. Wait for provider completion message in \`.workspace/messages/\`
|
|
585
|
+
6. Then dispatch to consumer: \`curl -s -X POST http://localhost:${consumers[0] ? config.channels?.members?.[consumers[0]]?.port || '{port}' : '{port}'} -d "/wogi-start ${'{'}consumerTaskId${'}'}"\`
|
|
586
|
+
7. Wait for consumer completion, then verify integration
|
|
587
|
+
|
|
588
|
+
**Bug investigation — dispatch to all workers:**
|
|
589
|
+
\`\`\`bash
|
|
590
|
+
# Send investigation request to all workers in parallel
|
|
591
|
+
${Object.entries(config.channels?.members || {}).map(([name, ch]) =>
|
|
592
|
+
`curl -s -X POST http://localhost:${ch.port} -d "Investigate: <BUG_DESCRIPTION>. Check recent changes, error logs, relevant code. Report back via workspace message."`
|
|
593
|
+
).join('\n')}
|
|
594
|
+
\`\`\`
|
|
595
|
+
Then read responses from \`.workspace/messages/\` and synthesize findings.
|
|
596
|
+
|
|
597
|
+
## Reading Member State (What to Read, When)
|
|
598
|
+
|
|
599
|
+
| When | What to Read | Path |
|
|
600
|
+
|------|-------------|------|
|
|
601
|
+
| Before routing a task | api-map.md | \`<repo>/.workflow/state/api-map.md\` |
|
|
602
|
+
| Before spawning an agent | decisions.md | \`<repo>/.workflow/state/decisions.md\` |
|
|
603
|
+
| To understand structure | app-map.md | \`<repo>/.workflow/state/app-map.md\` |
|
|
604
|
+
| To check data models | schema-map.md | \`<repo>/.workflow/state/schema-map.md\` |
|
|
605
|
+
| To check task status | ready.json | \`<repo>/.workflow/state/ready.json\` |
|
|
606
|
+
|
|
607
|
+
**NEVER read**: source files (\`src/\`, \`lib/\`, \`app/\`, etc.) — leave that to sub-agents.
|
|
608
|
+
|
|
609
|
+
## Integration Map
|
|
610
|
+
|
|
611
|
+
- **${matchedCount}** matched endpoint pair${matchedCount !== 1 ? 's' : ''} (provider ↔ consumer)
|
|
612
|
+
${orphanCount > 0 ? `- **${orphanCount}** orphaned consumer${orphanCount !== 1 ? 's' : ''} (calling endpoints with no provider) ⚠️` : '- No orphaned consumers ✓'}
|
|
613
|
+
|
|
614
|
+
Full details: \`.workspace/state/workspace-manifest.json\`
|
|
615
|
+
Visual map: \`.workspace/state/integration-map.md\`
|
|
616
|
+
|
|
617
|
+
## Message Bus
|
|
618
|
+
|
|
619
|
+
After ANY cross-repo change, write a message to \`.workspace/messages/\`:
|
|
620
|
+
|
|
621
|
+
\`\`\`json
|
|
622
|
+
{
|
|
623
|
+
"id": "msg-<random-8-hex>",
|
|
624
|
+
"from": "<repo-that-changed>",
|
|
625
|
+
"to": "<affected-repo>",
|
|
626
|
+
"type": "contract-change",
|
|
627
|
+
"subject": "Changed POST /api/users — added email_verified field",
|
|
628
|
+
"body": "Details of what changed and why...",
|
|
629
|
+
"actionRequired": true,
|
|
630
|
+
"status": "pending",
|
|
631
|
+
"timestamp": "<ISO-8601>"
|
|
632
|
+
}
|
|
633
|
+
\`\`\`
|
|
634
|
+
|
|
635
|
+
**Message types**: \`contract-change\`, \`question\`, \`bug-report\`, \`task-complete\`, \`needs-help\`, \`heads-up\`
|
|
636
|
+
|
|
637
|
+
When spawning a sub-agent, check for pending messages to that repo and include them in the prompt context.
|
|
638
|
+
|
|
639
|
+
## Contracts
|
|
640
|
+
|
|
641
|
+
Shared API contracts: \`.workspace/contracts/\`
|
|
642
|
+
When a provider changes an endpoint, update the contract BEFORE the consumer implements.
|
|
643
|
+
|
|
644
|
+
## Peer Communication (Worker ↔ Worker)
|
|
645
|
+
|
|
646
|
+
Workers can talk directly to each other — no manager bottleneck. Each worker knows its peers' channel ports.
|
|
647
|
+
|
|
648
|
+
A worker asking its peer a question:
|
|
649
|
+
\`\`\`bash
|
|
650
|
+
curl -s -X POST http://localhost:{peer_port} -H "X-Wogi-From: {my_repo}" -d "Is the POST /users endpoint ready? What's the expected payload shape?"
|
|
651
|
+
\`\`\`
|
|
652
|
+
|
|
653
|
+
The peer receives this as a channel event, reads its codebase to answer, and can reply back the same way. This is useful when:
|
|
654
|
+
- Frontend needs to know the exact API shape before implementing
|
|
655
|
+
- Backend wants to know which fields the frontend actually uses
|
|
656
|
+
- Any repo needs real-time coordination without going through the manager
|
|
657
|
+
|
|
658
|
+
Workers also have a \`workspace_send_message\` MCP tool that handles this automatically.
|
|
659
|
+
|
|
660
|
+
## Command Routing (CRITICAL)
|
|
661
|
+
|
|
662
|
+
**You are an orchestrator — you do NOT execute code-level commands locally.** When the user invokes a WogiFlow command, route it based on this table:
|
|
663
|
+
|
|
664
|
+
| Command | Action | Why |
|
|
665
|
+
|---------|--------|-----|
|
|
666
|
+
| \`/wogi-review\` | **Dispatch to ALL workers** → aggregate findings + cross-repo analysis | Workers have the source code and full pipeline |
|
|
667
|
+
| \`/wogi-audit\` | **Dispatch to ALL workers** → aggregate audit results + cross-repo analysis | Same reason |
|
|
668
|
+
| \`/wogi-test\` | **Dispatch to ALL workers** → aggregate test results | Tests run in each repo's environment |
|
|
669
|
+
| \`/wogi-health\` | **Dispatch to ALL workers** → unified health report | Each repo has its own workflow state |
|
|
670
|
+
| \`/wogi-start "task"\` | **Analyze → decompose → dispatch** to correct worker(s) | Already designed for this |
|
|
671
|
+
| \`/wogi-status\` | Run **locally** — workspace-level overview | Reads workspace state files |
|
|
672
|
+
| \`/wogi-ready\` | Run **locally** — cross-repo task queue | Reads all repos' ready.json |
|
|
673
|
+
| \`/wogi-session-end\` | Run **locally** — end workspace session | Workspace-level action |
|
|
674
|
+
|
|
675
|
+
**How to dispatch a command to all workers:**
|
|
676
|
+
\`\`\`bash
|
|
677
|
+
${Object.entries(config.channels?.members || {}).map(([name, ch]) =>
|
|
678
|
+
`curl -s -X POST http://localhost:${ch.port} -d "/wogi-review" # → ${name}`
|
|
679
|
+
).join('\n')}
|
|
680
|
+
\`\`\`
|
|
681
|
+
|
|
682
|
+
**After all workers complete:** Run a cross-repo analysis that only you can do:
|
|
683
|
+
1. Collect findings from \`.workspace/messages/\` (workers write results there)
|
|
684
|
+
2. Check API contract alignment between provider and consumer
|
|
685
|
+
3. Check for integration gaps (orphaned consumers, drifted contracts)
|
|
686
|
+
4. Check shared decisions compliance (\`.workspace/state/decisions.md\`)
|
|
687
|
+
5. Present unified report: per-repo findings + cross-repo findings
|
|
688
|
+
|
|
689
|
+
## Workspace Commands
|
|
690
|
+
|
|
691
|
+
| Command | What it does |
|
|
692
|
+
|---------|-------------|
|
|
693
|
+
| \`flow workspace sync\` | Re-read all member state files, update manifest |
|
|
694
|
+
| \`flow workspace status\` | Show all repos, tasks, messages, contracts |
|
|
695
|
+
| \`flow workspace add <path>\` | Add a new member repo |
|
|
696
|
+
| \`flow workspace remove <name>\` | Remove a member repo |
|
|
697
|
+
| \`flow workspace start\` | Start a worker session (run from within a member repo) |
|
|
698
|
+
|
|
699
|
+
## File Reference
|
|
700
|
+
|
|
701
|
+
| File | Purpose |
|
|
702
|
+
|------|---------|
|
|
703
|
+
| \`wogi-workspace.json\` | Workspace config (members, roles, settings) |
|
|
704
|
+
| \`.workspace/state/workspace-manifest.json\` | Auto-generated member metadata + integration map |
|
|
705
|
+
| \`.workspace/state/integration-map.md\` | Human-readable integration visualization |
|
|
706
|
+
| \`.workspace/state/ready.json\` | Workspace-level cross-repo tasks |
|
|
707
|
+
| \`.workspace/state/decisions.md\` | Shared cross-repo rules |
|
|
708
|
+
| \`.workspace/contracts/\` | Shared API contracts (OpenAPI, TypeScript, etc.) |
|
|
709
|
+
| \`.workspace/messages/\` | Agent-to-agent messages |
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
Generated by Wogi Workspace v1.0.0
|
|
713
|
+
Last synced: ${new Date().toISOString()}
|
|
714
|
+
`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ============================================================
|
|
718
|
+
// Settings.json Generation
|
|
719
|
+
// ============================================================
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Generate workspace-level .claude/settings.json
|
|
723
|
+
* Minimal hooks — workspace doesn't need validation/linting hooks.
|
|
724
|
+
* @returns {Object} settings config
|
|
725
|
+
*/
|
|
726
|
+
function generateWorkspaceSettings(memberNames) {
|
|
727
|
+
// Build read patterns for member state files
|
|
728
|
+
const readPatterns = [];
|
|
729
|
+
const bashPatterns = [
|
|
730
|
+
'Bash(git status *)',
|
|
731
|
+
'Bash(git log *)',
|
|
732
|
+
'Bash(git diff *)',
|
|
733
|
+
'Bash(git show *)',
|
|
734
|
+
'Bash(git branch *)',
|
|
735
|
+
'Bash(git add *)',
|
|
736
|
+
'Bash(git commit *)',
|
|
737
|
+
'Bash(git push *)',
|
|
738
|
+
'Bash(git pull *)',
|
|
739
|
+
'Bash(git fetch *)',
|
|
740
|
+
'Bash(node --check *)',
|
|
741
|
+
'Bash(node scripts/*)',
|
|
742
|
+
'Bash(ls *)',
|
|
743
|
+
'Bash(cat *)',
|
|
744
|
+
'Bash(curl http://localhost:*)',
|
|
745
|
+
'Bash(curl http://127.0.0.1:*)',
|
|
746
|
+
'Bash(curl -s http://localhost:*)',
|
|
747
|
+
'Bash(curl -s http://127.0.0.1:*)',
|
|
748
|
+
'Bash(curl -s -X POST http://localhost:*)',
|
|
749
|
+
'Bash(curl -s -X POST http://127.0.0.1:*)'
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
for (const name of (memberNames || [])) {
|
|
753
|
+
readPatterns.push(`Read(${name}/.workflow/**)`);
|
|
754
|
+
readPatterns.push(`Read(${name}/package.json)`);
|
|
755
|
+
readPatterns.push(`Read(${name}/pyproject.toml)`);
|
|
756
|
+
readPatterns.push(`Read(${name}/go.mod)`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
permissions: {
|
|
761
|
+
allow: [
|
|
762
|
+
// Allow reading member state files
|
|
763
|
+
...readPatterns,
|
|
764
|
+
// Allow reading workspace state
|
|
765
|
+
'Read(.workspace/**)',
|
|
766
|
+
'Read(wogi-workspace.json)',
|
|
767
|
+
'Read(CLAUDE.md)',
|
|
768
|
+
// Allow writing workspace state
|
|
769
|
+
'Write(.workspace/**)',
|
|
770
|
+
'Edit(.workspace/**)',
|
|
771
|
+
// Allow git and node
|
|
772
|
+
...bashPatterns
|
|
773
|
+
]
|
|
774
|
+
},
|
|
775
|
+
hooks: {},
|
|
776
|
+
_wogiWorkspace: true,
|
|
777
|
+
_wogiFlowVersion: require('../package.json').version,
|
|
778
|
+
_comment: 'Workspace-level settings. Member repos have their own settings. Sub-agents spawned for member repos use THEIR settings, not these.'
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ============================================================
|
|
783
|
+
// Directory Structure Creation
|
|
784
|
+
// ============================================================
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Create the .workspace/ directory structure
|
|
788
|
+
* @param {string} workspaceRoot
|
|
789
|
+
*/
|
|
790
|
+
function createWorkspaceStructure(workspaceRoot) {
|
|
791
|
+
const wsDir = path.join(workspaceRoot, WORKSPACE_DIR);
|
|
792
|
+
|
|
793
|
+
// Create main dirs
|
|
794
|
+
for (const dir of WORKSPACE_DIRS) {
|
|
795
|
+
const dirPath = path.join(wsDir, dir);
|
|
796
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Create .claude/ for workspace settings
|
|
800
|
+
const claudeDir = path.join(workspaceRoot, '.claude');
|
|
801
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
802
|
+
|
|
803
|
+
// Create empty ready.json for workspace-level tasks
|
|
804
|
+
const readyPath = path.join(wsDir, 'state', 'ready.json');
|
|
805
|
+
if (!fs.existsSync(readyPath)) {
|
|
806
|
+
fs.writeFileSync(readyPath, JSON.stringify({
|
|
807
|
+
lastUpdated: new Date().toISOString(),
|
|
808
|
+
inProgress: [],
|
|
809
|
+
ready: [],
|
|
810
|
+
blocked: [],
|
|
811
|
+
recentlyCompleted: []
|
|
812
|
+
}, null, 2));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Create workspace decisions.md
|
|
816
|
+
const decisionsPath = path.join(wsDir, 'state', 'decisions.md');
|
|
817
|
+
if (!fs.existsSync(decisionsPath)) {
|
|
818
|
+
fs.writeFileSync(decisionsPath, `# Workspace Decisions
|
|
819
|
+
|
|
820
|
+
Cross-repo rules that apply to all member repositories.
|
|
821
|
+
|
|
822
|
+
## Shared Conventions
|
|
823
|
+
|
|
824
|
+
<!-- Add shared rules here, e.g.: -->
|
|
825
|
+
<!-- ### Date Format -->
|
|
826
|
+
<!-- All dates use ISO 8601 (YYYY-MM-DDTHH:mm:ssZ) across all repos. -->
|
|
827
|
+
`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Create .gitignore for workspace transient files
|
|
831
|
+
const gitignorePath = path.join(wsDir, '.gitignore');
|
|
832
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
833
|
+
fs.writeFileSync(gitignorePath, `# Wogi Workspace — transient files
|
|
834
|
+
# Messages are ephemeral (processed and resolved)
|
|
835
|
+
messages/*.json
|
|
836
|
+
# State is auto-generated from member repos
|
|
837
|
+
state/workspace-manifest.json
|
|
838
|
+
state/integration-map.md
|
|
839
|
+
state/contract-versions.json
|
|
840
|
+
`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ============================================================
|
|
845
|
+
// ============================================================
|
|
846
|
+
// Channel MCP Config Generation
|
|
847
|
+
// ============================================================
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Generate .mcp.json for each member repo with channel server config.
|
|
851
|
+
* This allows workers to receive task dispatches from the manager
|
|
852
|
+
* and communicate with peer repos via channels.
|
|
853
|
+
*
|
|
854
|
+
* @param {string} workspaceRoot
|
|
855
|
+
* @param {Object} config — workspace config (with channels section)
|
|
856
|
+
*/
|
|
857
|
+
function generateMemberMcpConfigs(workspaceRoot, config) {
|
|
858
|
+
const channelMembers = config.channels?.members || {};
|
|
859
|
+
const memberNames = Object.keys(channelMembers);
|
|
860
|
+
|
|
861
|
+
const VALID_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
862
|
+
|
|
863
|
+
for (const [name, channelConfig] of Object.entries(channelMembers)) {
|
|
864
|
+
if (!VALID_NAME.test(name)) {
|
|
865
|
+
console.error(` ✗ ${name}: invalid member name (must match [a-zA-Z0-9_-]) — skipping`);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const memberPath = path.resolve(workspaceRoot, config.members[name]?.path || `./${name}`);
|
|
870
|
+
|
|
871
|
+
// Path traversal guard — ensure member path is inside workspace root
|
|
872
|
+
if (!memberPath.startsWith(workspaceRoot + path.sep) && memberPath !== workspaceRoot) {
|
|
873
|
+
console.error(` ✗ ${name}: path escapes workspace root (${config.members[name]?.path}) — skipping`);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const mcpJsonPath = path.join(memberPath, '.mcp.json');
|
|
878
|
+
|
|
879
|
+
// Build peer list (all other members)
|
|
880
|
+
const peers = memberNames
|
|
881
|
+
.filter(n => n !== name)
|
|
882
|
+
.map(n => `${n}:${channelMembers[n].port}`)
|
|
883
|
+
.join(',');
|
|
884
|
+
|
|
885
|
+
// Resolve channel server script path
|
|
886
|
+
// Use require.resolve to find the installed wogiflow package
|
|
887
|
+
let channelServerPath;
|
|
888
|
+
try {
|
|
889
|
+
channelServerPath = require.resolve('wogiflow/lib/workspace-channel-server.js');
|
|
890
|
+
} catch (_err) {
|
|
891
|
+
// Fallback: relative path from workspace root
|
|
892
|
+
channelServerPath = path.join(__dirname, 'workspace-channel-server.js');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const mcpConfig = {
|
|
896
|
+
mcpServers: {
|
|
897
|
+
'wogi-workspace-channel': {
|
|
898
|
+
command: 'node',
|
|
899
|
+
args: [channelServerPath],
|
|
900
|
+
env: {
|
|
901
|
+
WOGI_CHANNEL_PORT: String(channelConfig.port),
|
|
902
|
+
WOGI_REPO_NAME: name,
|
|
903
|
+
WOGI_PEERS: peers,
|
|
904
|
+
WOGI_WORKSPACE_ROOT: workspaceRoot
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
// Merge with existing .mcp.json if present
|
|
911
|
+
let existingConfig = {};
|
|
912
|
+
try {
|
|
913
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
914
|
+
existingConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
|
|
915
|
+
}
|
|
916
|
+
} catch (_err) {
|
|
917
|
+
// Ignore malformed existing config
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const merged = {
|
|
921
|
+
...existingConfig,
|
|
922
|
+
mcpServers: {
|
|
923
|
+
...(existingConfig.mcpServers || {}),
|
|
924
|
+
...mcpConfig.mcpServers
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(merged, null, 2));
|
|
929
|
+
console.log(` ✓ ${name}/.mcp.json (channel port ${channelConfig.port}, peers: ${peers || 'none'})`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ============================================================
|
|
934
|
+
// Main Init Function
|
|
935
|
+
// ============================================================
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Initialize a Wogi Workspace
|
|
939
|
+
* @param {string[]} args — CLI arguments
|
|
940
|
+
*/
|
|
941
|
+
async function initWorkspace(args) {
|
|
942
|
+
const workspaceRoot = process.cwd();
|
|
943
|
+
const workspaceName = path.basename(workspaceRoot);
|
|
944
|
+
|
|
945
|
+
// Check if workspace already exists
|
|
946
|
+
if (fs.existsSync(path.join(workspaceRoot, WORKSPACE_CONFIG_FILE))) {
|
|
947
|
+
console.error(`Workspace already initialized in ${workspaceRoot}`);
|
|
948
|
+
console.error('Use `flow workspace sync` to update.');
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
console.log('🔍 Scanning for WogiFlow-enabled projects...\n');
|
|
953
|
+
|
|
954
|
+
// Discover member repos
|
|
955
|
+
const discovered = discoverMembers(workspaceRoot);
|
|
956
|
+
|
|
957
|
+
if (discovered.length === 0) {
|
|
958
|
+
console.error('No WogiFlow-enabled projects found in subdirectories.');
|
|
959
|
+
console.error('Each member repo must have a .workflow/ directory (run `flow init` in each first).');
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
console.log(`Found ${discovered.length} WogiFlow project${discovered.length !== 1 ? 's' : ''}:`);
|
|
964
|
+
for (const m of discovered) {
|
|
965
|
+
console.log(` ✓ ${m.name}/`);
|
|
966
|
+
}
|
|
967
|
+
console.log('');
|
|
968
|
+
|
|
969
|
+
// Read metadata from each member
|
|
970
|
+
console.log('── Reading project metadata ──────────────────\n');
|
|
971
|
+
const members = [];
|
|
972
|
+
|
|
973
|
+
for (const disc of discovered) {
|
|
974
|
+
const metadata = readMemberMetadata(disc.workflowPath);
|
|
975
|
+
const stack = detectStack(metadata, disc.path);
|
|
976
|
+
const capabilities = extractCapabilities(metadata);
|
|
977
|
+
const endpoints = extractEndpoints(metadata);
|
|
978
|
+
const role = autoDetectRole(endpoints);
|
|
979
|
+
|
|
980
|
+
members.push({
|
|
981
|
+
name: disc.name,
|
|
982
|
+
path: disc.path,
|
|
983
|
+
workflowPath: disc.workflowPath,
|
|
984
|
+
metadata,
|
|
985
|
+
stack,
|
|
986
|
+
capabilities,
|
|
987
|
+
endpoints,
|
|
988
|
+
role
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
const capsSummary = Object.entries(capabilities)
|
|
992
|
+
.filter(([_, v]) => v > 0)
|
|
993
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
994
|
+
.join(', ') || 'no data yet';
|
|
995
|
+
|
|
996
|
+
console.log(` ${disc.name}/ (${stack.language}/${stack.framework})`);
|
|
997
|
+
console.log(` Role: ${role} | ${capsSummary}`);
|
|
998
|
+
console.log(` Provides: ${endpoints.provides.length} endpoints | Consumes: ${endpoints.consumes.length} endpoints`);
|
|
999
|
+
console.log('');
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Create directory structure
|
|
1003
|
+
console.log('── Creating workspace structure ──────────────\n');
|
|
1004
|
+
createWorkspaceStructure(workspaceRoot);
|
|
1005
|
+
console.log(' ✓ .workspace/state/');
|
|
1006
|
+
console.log(' ✓ .workspace/contracts/');
|
|
1007
|
+
console.log(' ✓ .workspace/messages/');
|
|
1008
|
+
console.log(' ✓ .workspace/specs/');
|
|
1009
|
+
console.log('');
|
|
1010
|
+
|
|
1011
|
+
// Generate workspace config
|
|
1012
|
+
const config = generateWorkspaceConfig(workspaceName, members);
|
|
1013
|
+
fs.writeFileSync(
|
|
1014
|
+
path.join(workspaceRoot, WORKSPACE_CONFIG_FILE),
|
|
1015
|
+
JSON.stringify(config, null, 2)
|
|
1016
|
+
);
|
|
1017
|
+
console.log(` ✓ ${WORKSPACE_CONFIG_FILE}`);
|
|
1018
|
+
|
|
1019
|
+
// Generate manifest
|
|
1020
|
+
const manifest = generateManifest(workspaceName, members);
|
|
1021
|
+
fs.writeFileSync(
|
|
1022
|
+
path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'workspace-manifest.json'),
|
|
1023
|
+
JSON.stringify(manifest, null, 2)
|
|
1024
|
+
);
|
|
1025
|
+
console.log(' ✓ .workspace/state/workspace-manifest.json');
|
|
1026
|
+
|
|
1027
|
+
// Generate integration map (human-readable markdown)
|
|
1028
|
+
const integrationMap = generateIntegrationMap(manifest);
|
|
1029
|
+
fs.writeFileSync(
|
|
1030
|
+
path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'integration-map.md'),
|
|
1031
|
+
integrationMap
|
|
1032
|
+
);
|
|
1033
|
+
console.log(' ✓ .workspace/state/integration-map.md');
|
|
1034
|
+
|
|
1035
|
+
// Generate CLAUDE.md
|
|
1036
|
+
const claudeMd = generateWorkspaceClaudeMd(config, manifest);
|
|
1037
|
+
fs.writeFileSync(path.join(workspaceRoot, 'CLAUDE.md'), claudeMd);
|
|
1038
|
+
console.log(' ✓ CLAUDE.md (workspace manager instructions)');
|
|
1039
|
+
|
|
1040
|
+
// Generate settings.json
|
|
1041
|
+
const settings = generateWorkspaceSettings(members.map(m => m.name));
|
|
1042
|
+
fs.writeFileSync(
|
|
1043
|
+
path.join(workspaceRoot, '.claude', 'settings.json'),
|
|
1044
|
+
JSON.stringify(settings, null, 2)
|
|
1045
|
+
);
|
|
1046
|
+
console.log(' ✓ .claude/settings.json');
|
|
1047
|
+
|
|
1048
|
+
// Generate .mcp.json for each member repo (channel server config)
|
|
1049
|
+
if (config.channels?.enabled) {
|
|
1050
|
+
console.log('');
|
|
1051
|
+
console.log('── Setting up workspace channels ────────────\n');
|
|
1052
|
+
generateMemberMcpConfigs(workspaceRoot, config);
|
|
1053
|
+
}
|
|
1054
|
+
console.log('');
|
|
1055
|
+
|
|
1056
|
+
// Summary
|
|
1057
|
+
const matched = manifest.integrations.matched.length;
|
|
1058
|
+
const orphanedC = manifest.integrations.orphanedConsumers.length;
|
|
1059
|
+
const orphanedP = manifest.integrations.orphanedProviders.length;
|
|
1060
|
+
|
|
1061
|
+
console.log('── Integration Summary ──────────────────────\n');
|
|
1062
|
+
console.log(` ✓ ${matched} matched endpoint pair${matched !== 1 ? 's' : ''}`);
|
|
1063
|
+
if (orphanedC > 0) {
|
|
1064
|
+
console.log(` ⚠️ ${orphanedC} orphaned consumer${orphanedC !== 1 ? 's' : ''} (calling endpoints with no provider)`);
|
|
1065
|
+
for (const orphan of manifest.integrations.orphanedConsumers) {
|
|
1066
|
+
console.log(` → ${orphan.endpoint} (consumed by: ${orphan.consumers.join(', ')})`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (orphanedP > 0) {
|
|
1070
|
+
console.log(` ℹ️ ${orphanedP} endpoint${orphanedP !== 1 ? 's' : ''} with no consumer`);
|
|
1071
|
+
}
|
|
1072
|
+
console.log('');
|
|
1073
|
+
|
|
1074
|
+
console.log(`✅ Workspace "${workspaceName}" initialized with ${members.length} member${members.length !== 1 ? 's' : ''}!`);
|
|
1075
|
+
console.log('');
|
|
1076
|
+
console.log('Next steps:');
|
|
1077
|
+
console.log(" 1. Run 'claude' in this folder to start the workspace manager");
|
|
1078
|
+
console.log(" 2. Give it tasks — it will route them to the right repo(s)");
|
|
1079
|
+
console.log(" 3. Run 'flow workspace sync' after external changes");
|
|
1080
|
+
console.log('');
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Generate human-readable integration map markdown
|
|
1085
|
+
* @param {Object} manifest
|
|
1086
|
+
* @returns {string}
|
|
1087
|
+
*/
|
|
1088
|
+
function generateIntegrationMap(manifest) {
|
|
1089
|
+
const lines = ['# Integration Map\n'];
|
|
1090
|
+
lines.push(`Generated: ${manifest.generatedAt}\n`);
|
|
1091
|
+
|
|
1092
|
+
// Matched endpoints
|
|
1093
|
+
if (manifest.integrations.matched.length > 0) {
|
|
1094
|
+
lines.push('## Matched Endpoints\n');
|
|
1095
|
+
lines.push('| Endpoint | Provider(s) | Consumer(s) |');
|
|
1096
|
+
lines.push('|----------|-------------|-------------|');
|
|
1097
|
+
for (const m of manifest.integrations.matched) {
|
|
1098
|
+
lines.push(`| \`${m.endpoint}\` | ${m.providers.join(', ')} | ${m.consumers.join(', ')} |`);
|
|
1099
|
+
}
|
|
1100
|
+
lines.push('');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Orphaned consumers
|
|
1104
|
+
if (manifest.integrations.orphanedConsumers.length > 0) {
|
|
1105
|
+
lines.push('## ⚠️ Orphaned Consumers\n');
|
|
1106
|
+
lines.push('These repos call endpoints that no provider serves:\n');
|
|
1107
|
+
for (const o of manifest.integrations.orphanedConsumers) {
|
|
1108
|
+
lines.push(`- \`${o.endpoint}\` — consumed by: ${o.consumers.join(', ')}`);
|
|
1109
|
+
}
|
|
1110
|
+
lines.push('');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Orphaned providers
|
|
1114
|
+
if (manifest.integrations.orphanedProviders.length > 0) {
|
|
1115
|
+
lines.push('## ℹ️ Endpoints Without Consumers\n');
|
|
1116
|
+
lines.push('These endpoints are served but no consumer calls them:\n');
|
|
1117
|
+
for (const o of manifest.integrations.orphanedProviders) {
|
|
1118
|
+
lines.push(`- \`${o.endpoint}\` — provided by: ${o.providers.join(', ')}`);
|
|
1119
|
+
}
|
|
1120
|
+
lines.push('');
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Member summary
|
|
1124
|
+
lines.push('## Members\n');
|
|
1125
|
+
for (const [name, m] of Object.entries(manifest.members)) {
|
|
1126
|
+
lines.push(`### ${name} (${m.role})`);
|
|
1127
|
+
lines.push(`- **Stack**: ${m.stack.language} / ${m.stack.framework}`);
|
|
1128
|
+
lines.push(`- **Provides**: ${m.provides.length > 0 ? m.provides.join(', ') : 'none'}`);
|
|
1129
|
+
lines.push(`- **Consumes**: ${m.consumes.length > 0 ? m.consumes.join(', ') : 'none'}`);
|
|
1130
|
+
lines.push('');
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return lines.join('\n');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ============================================================
|
|
1137
|
+
// Worker Session Launcher
|
|
1138
|
+
// ============================================================
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Start a Claude Code worker session with the workspace channel enabled.
|
|
1142
|
+
* Must be run from within a member repo directory.
|
|
1143
|
+
*
|
|
1144
|
+
* @param {string} cwd — current working directory (should be a member repo)
|
|
1145
|
+
*/
|
|
1146
|
+
function startWorkerSession(cwd) {
|
|
1147
|
+
const { execSync } = require('node:child_process');
|
|
1148
|
+
|
|
1149
|
+
// Find workspace root by walking up
|
|
1150
|
+
let workspaceRoot = null;
|
|
1151
|
+
let dir = cwd;
|
|
1152
|
+
while (dir !== path.dirname(dir)) {
|
|
1153
|
+
if (fs.existsSync(path.join(dir, WORKSPACE_CONFIG_FILE))) {
|
|
1154
|
+
workspaceRoot = dir;
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
dir = path.dirname(dir);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (!workspaceRoot) {
|
|
1161
|
+
console.error('Error: Not inside a workspace. Could not find wogi-workspace.json in any parent directory.');
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Read workspace config
|
|
1166
|
+
let config;
|
|
1167
|
+
try {
|
|
1168
|
+
config = JSON.parse(fs.readFileSync(path.join(workspaceRoot, WORKSPACE_CONFIG_FILE), 'utf-8'));
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
console.error(`Error reading workspace config: ${err.message}`);
|
|
1171
|
+
process.exit(1);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (!config.channels?.enabled) {
|
|
1175
|
+
console.error('Error: Channels are not enabled in this workspace. Run "flow workspace init" to set up channels.');
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Determine which member we are based on cwd
|
|
1180
|
+
const relativePath = path.relative(workspaceRoot, cwd);
|
|
1181
|
+
let memberName = null;
|
|
1182
|
+
let memberChannel = null;
|
|
1183
|
+
|
|
1184
|
+
for (const [name, memberConfig] of Object.entries(config.members)) {
|
|
1185
|
+
const memberRelPath = memberConfig.path.replace(/^\.\//, '');
|
|
1186
|
+
if (relativePath === memberRelPath || relativePath.startsWith(memberRelPath + path.sep)) {
|
|
1187
|
+
memberName = name;
|
|
1188
|
+
memberChannel = config.channels.members[name];
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (!memberName || !memberChannel) {
|
|
1194
|
+
console.error(`Error: Current directory "${relativePath}" does not match any workspace member.`);
|
|
1195
|
+
console.error('Members:', Object.keys(config.members).join(', '));
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Build peer list
|
|
1200
|
+
const peers = Object.entries(config.channels.members)
|
|
1201
|
+
.filter(([n]) => n !== memberName)
|
|
1202
|
+
.map(([n, ch]) => `${n}:${ch.port}`)
|
|
1203
|
+
.join(',');
|
|
1204
|
+
|
|
1205
|
+
// Verify .mcp.json exists with channel config
|
|
1206
|
+
const mcpJsonPath = path.join(cwd, '.mcp.json');
|
|
1207
|
+
let mcpConfigValid = false;
|
|
1208
|
+
try {
|
|
1209
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
1210
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
|
|
1211
|
+
mcpConfigValid = !!mcpConfig?.mcpServers?.['wogi-workspace-channel'];
|
|
1212
|
+
}
|
|
1213
|
+
} catch (_err) {
|
|
1214
|
+
// Malformed .mcp.json
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (!mcpConfigValid) {
|
|
1218
|
+
console.error(`Error: .mcp.json missing or does not contain wogi-workspace-channel config.`);
|
|
1219
|
+
console.error('Run "flow workspace init" from the workspace root to generate channel configs.');
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
console.log(`Starting worker session for "${memberName}" (port ${memberChannel.port})`);
|
|
1224
|
+
if (peers) console.log(`Peers: ${peers}`);
|
|
1225
|
+
console.log('');
|
|
1226
|
+
|
|
1227
|
+
// Set up environment for the channel server
|
|
1228
|
+
const env = {
|
|
1229
|
+
...process.env,
|
|
1230
|
+
WOGI_CHANNEL_PORT: String(memberChannel.port),
|
|
1231
|
+
WOGI_REPO_NAME: memberName,
|
|
1232
|
+
WOGI_PEERS: peers,
|
|
1233
|
+
WOGI_WORKSPACE_ROOT: workspaceRoot
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
// Launch Claude Code with the channel
|
|
1237
|
+
try {
|
|
1238
|
+
execSync('claude --dangerously-load-development-channels server:wogi-workspace-channel', {
|
|
1239
|
+
cwd,
|
|
1240
|
+
env,
|
|
1241
|
+
stdio: 'inherit'
|
|
1242
|
+
});
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
// Exit code 0 or SIGINT from user quit — that's fine
|
|
1245
|
+
if (err.status && err.status !== 0 && err.signal !== 'SIGINT') {
|
|
1246
|
+
console.error(`Worker session exited with error (code ${err.status}): ${err.message}`);
|
|
1247
|
+
process.exit(err.status);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// ============================================================
|
|
1253
|
+
// CLI Router
|
|
1254
|
+
// ============================================================
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Handle workspace subcommands
|
|
1258
|
+
* @param {string[]} args
|
|
1259
|
+
*/
|
|
1260
|
+
async function workspace(args) {
|
|
1261
|
+
const subcommand = args[0];
|
|
1262
|
+
|
|
1263
|
+
switch (subcommand) {
|
|
1264
|
+
case 'init':
|
|
1265
|
+
await initWorkspace(args.slice(1));
|
|
1266
|
+
break;
|
|
1267
|
+
case 'sync': {
|
|
1268
|
+
const { syncWorkspace } = require('./workspace-sync');
|
|
1269
|
+
const result = syncWorkspace(process.cwd());
|
|
1270
|
+
console.log(`✓ Synced ${result.membersUpdated} member(s). ${result.changes.length} change(s) detected.`);
|
|
1271
|
+
if (result.warnings.length > 0) {
|
|
1272
|
+
for (const w of result.warnings) console.log(` ⚠️ ${w}`);
|
|
1273
|
+
}
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case 'status': {
|
|
1277
|
+
const { getWorkspaceStatus } = require('./workspace-sync');
|
|
1278
|
+
console.log(getWorkspaceStatus(process.cwd()));
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
case 'add': {
|
|
1282
|
+
const { addMember } = require('./workspace-sync');
|
|
1283
|
+
const memberPath = args[1];
|
|
1284
|
+
const role = args[2];
|
|
1285
|
+
if (!memberPath) {
|
|
1286
|
+
console.error('Usage: flow workspace add <path> [role]');
|
|
1287
|
+
process.exit(1);
|
|
1288
|
+
}
|
|
1289
|
+
const result = addMember(process.cwd(), memberPath, role);
|
|
1290
|
+
console.log(`✓ Added '${result.name}' as ${result.role}`);
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
case 'remove': {
|
|
1294
|
+
const { removeMember } = require('./workspace-sync');
|
|
1295
|
+
const name = args[1];
|
|
1296
|
+
if (!name) {
|
|
1297
|
+
console.error('Usage: flow workspace remove <name>');
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
removeMember(process.cwd(), name);
|
|
1301
|
+
console.log(`✓ Removed '${name}' from workspace`);
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
case 'start': {
|
|
1305
|
+
startWorkerSession(process.cwd());
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
default:
|
|
1309
|
+
console.log(`
|
|
1310
|
+
Wogi Workspace — Multi-Repo Orchestration
|
|
1311
|
+
|
|
1312
|
+
Usage: flow workspace <command>
|
|
1313
|
+
|
|
1314
|
+
Commands:
|
|
1315
|
+
init Initialize a workspace from member repos
|
|
1316
|
+
sync Re-sync workspace manifest from member state files
|
|
1317
|
+
status Show unified workspace status
|
|
1318
|
+
add Add a member repo to the workspace
|
|
1319
|
+
remove Remove a member repo from the workspace
|
|
1320
|
+
start Start a worker session with channel (run from a member repo)
|
|
1321
|
+
|
|
1322
|
+
Examples:
|
|
1323
|
+
flow workspace init # Create workspace from subdirectories
|
|
1324
|
+
flow workspace sync # Refresh after external changes
|
|
1325
|
+
flow workspace status # Show all repos, tasks, contracts
|
|
1326
|
+
cd frontend/ && flow workspace start # Start worker session
|
|
1327
|
+
`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
module.exports = {
|
|
1332
|
+
workspace,
|
|
1333
|
+
initWorkspace,
|
|
1334
|
+
discoverMembers,
|
|
1335
|
+
readMemberMetadata,
|
|
1336
|
+
extractCapabilities,
|
|
1337
|
+
extractEndpoints,
|
|
1338
|
+
detectStack,
|
|
1339
|
+
generateWorkspaceConfig,
|
|
1340
|
+
generateManifest,
|
|
1341
|
+
generateWorkspaceClaudeMd,
|
|
1342
|
+
generateWorkspaceSettings,
|
|
1343
|
+
createWorkspaceStructure,
|
|
1344
|
+
WORKSPACE_CONFIG_FILE,
|
|
1345
|
+
WORKSPACE_DIR,
|
|
1346
|
+
MEMBER_ROLES,
|
|
1347
|
+
generateMemberMcpConfigs,
|
|
1348
|
+
startWorkerSession
|
|
1349
|
+
};
|