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.
Files changed (210) hide show
  1. package/.claude/commands/wogi-start.md +124 -0
  2. package/.claude/docs/claude-code-compatibility.md +51 -0
  3. package/.claude/docs/explore-agents.md +11 -0
  4. package/.claude/settings.json +12 -1
  5. package/.workflow/models/registry.json +1 -1
  6. package/bin/flow +11 -1
  7. package/lib/workspace-contracts.js +599 -0
  8. package/lib/workspace-intelligence.js +600 -0
  9. package/lib/workspace-messages.js +441 -0
  10. package/lib/workspace-routing.js +485 -0
  11. package/lib/workspace-sync.js +339 -0
  12. package/lib/workspace.js +1073 -0
  13. package/package.json +4 -4
  14. package/scripts/MEMORY-ARCHITECTURE.md +1 -1
  15. package/scripts/base-workflow-step.js +136 -0
  16. package/scripts/flow-adaptive-learning.js +8 -9
  17. package/scripts/flow-aggregate.js +11 -6
  18. package/scripts/flow-api-index.js +4 -6
  19. package/scripts/flow-assumption-detector.js +0 -2
  20. package/scripts/flow-audit.js +15 -2
  21. package/scripts/flow-auto-context.js +8 -12
  22. package/scripts/flow-auto-learn.js +49 -49
  23. package/scripts/flow-background.js +5 -6
  24. package/scripts/flow-bridge-state.js +8 -10
  25. package/scripts/flow-bulk-loop.js +1 -3
  26. package/scripts/flow-bulk-orchestrator.js +1 -3
  27. package/scripts/flow-cascade-completion.js +0 -2
  28. package/scripts/flow-cascade.js +4 -4
  29. package/scripts/flow-checkpoint.js +10 -13
  30. package/scripts/flow-code-intelligence.js +10 -12
  31. package/scripts/flow-community-sync.js +4 -4
  32. package/scripts/flow-community.js +12 -20
  33. package/scripts/flow-config-defaults.js +28 -2
  34. package/scripts/flow-config-interactive.js +9 -5
  35. package/scripts/flow-config-loader.js +49 -92
  36. package/scripts/flow-config-substitution.js +0 -2
  37. package/scripts/flow-context-estimator.js +4 -4
  38. package/scripts/flow-context-init.js +10 -12
  39. package/scripts/flow-context-manager.js +0 -2
  40. package/scripts/flow-context-scoring.js +2 -2
  41. package/scripts/flow-contract-scan.js +6 -9
  42. package/scripts/flow-correct.js +29 -27
  43. package/scripts/flow-correction-detector.js +5 -1
  44. package/scripts/flow-damage-control.js +47 -54
  45. package/scripts/flow-decisions-merge.js +4 -14
  46. package/scripts/flow-diff.js +5 -8
  47. package/scripts/flow-done-gates.js +786 -0
  48. package/scripts/flow-done-report.js +123 -0
  49. package/scripts/flow-done.js +71 -717
  50. package/scripts/flow-entropy-monitor.js +1 -3
  51. package/scripts/flow-eval-calibration.js +257 -0
  52. package/scripts/flow-eval-judge.js +10 -1
  53. package/scripts/flow-eval.js +14 -5
  54. package/scripts/flow-extraction-review.js +1 -0
  55. package/scripts/flow-failure-categories.js +0 -2
  56. package/scripts/flow-figma-confirm.js +5 -9
  57. package/scripts/flow-figma-generate.js +8 -10
  58. package/scripts/flow-figma-index.js +8 -10
  59. package/scripts/flow-figma-match.js +3 -5
  60. package/scripts/flow-figma-mcp-server.js +2 -4
  61. package/scripts/flow-figma-orchestrator.js +2 -3
  62. package/scripts/flow-figma-registry.js +2 -3
  63. package/scripts/flow-framework-resolver.js +0 -2
  64. package/scripts/flow-function-index.js +4 -6
  65. package/scripts/flow-gate-confidence.js +2 -2
  66. package/scripts/flow-gitignore.js +0 -2
  67. package/scripts/flow-guided-edit.js +5 -6
  68. package/scripts/flow-health.js +5 -6
  69. package/scripts/flow-hook-errors.js +6 -0
  70. package/scripts/flow-hook-status.js +263 -0
  71. package/scripts/flow-hooks.js +17 -29
  72. package/scripts/flow-http-client.js +9 -8
  73. package/scripts/flow-hybrid-interactive.js +7 -12
  74. package/scripts/flow-hybrid-test.js +12 -13
  75. package/scripts/flow-instruction-richness.js +1 -1
  76. package/scripts/flow-io.js +21 -4
  77. package/scripts/flow-knowledge-router.js +9 -3
  78. package/scripts/flow-learning-orchestrator.js +318 -13
  79. package/scripts/flow-links.js +5 -7
  80. package/scripts/flow-long-input-association.js +275 -0
  81. package/scripts/flow-long-input-chunking.js +1 -0
  82. package/scripts/flow-long-input-cli.js +0 -2
  83. package/scripts/flow-long-input-complexity.js +0 -2
  84. package/scripts/flow-long-input-constants.js +0 -2
  85. package/scripts/flow-long-input-contradictions.js +351 -0
  86. package/scripts/flow-long-input-detection.js +0 -2
  87. package/scripts/flow-long-input-passes.js +885 -0
  88. package/scripts/flow-long-input-stories.js +1 -1
  89. package/scripts/flow-long-input-voice.js +0 -2
  90. package/scripts/flow-long-input.js +425 -3005
  91. package/scripts/flow-loop-retry-learning.js +2 -3
  92. package/scripts/flow-lsp.js +3 -3
  93. package/scripts/flow-mcp-docs.js +3 -4
  94. package/scripts/flow-memory-db.js +6 -8
  95. package/scripts/flow-memory-sync.js +18 -11
  96. package/scripts/flow-metrics.js +1 -2
  97. package/scripts/flow-model-adapter.js +2 -3
  98. package/scripts/flow-model-config.js +72 -104
  99. package/scripts/flow-model-router.js +2 -2
  100. package/scripts/flow-model-types.js +0 -2
  101. package/scripts/flow-multi-approach.js +5 -6
  102. package/scripts/flow-orchestrate-context.js +3 -7
  103. package/scripts/flow-orchestrate-rollback.js +3 -8
  104. package/scripts/flow-orchestrate-state.js +8 -14
  105. package/scripts/flow-orchestrate-templates.js +2 -6
  106. package/scripts/flow-orchestrate-validator.js +5 -9
  107. package/scripts/flow-orchestrate.js +126 -103
  108. package/scripts/flow-output.js +0 -2
  109. package/scripts/flow-parallel.js +1 -1
  110. package/scripts/flow-paths.js +23 -2
  111. package/scripts/flow-pattern-enforcer.js +30 -28
  112. package/scripts/flow-pattern-extractor.js +3 -4
  113. package/scripts/flow-pending.js +0 -2
  114. package/scripts/flow-permissions.js +2 -3
  115. package/scripts/flow-plugin-registry.js +10 -12
  116. package/scripts/flow-prd-manager.js +1 -1
  117. package/scripts/flow-progress.js +7 -9
  118. package/scripts/flow-prompt-composer.js +3 -3
  119. package/scripts/flow-prompt-template.js +2 -2
  120. package/scripts/flow-providers.js +7 -4
  121. package/scripts/flow-registry-manager.js +7 -12
  122. package/scripts/flow-regression.js +9 -11
  123. package/scripts/flow-roadmap.js +2 -2
  124. package/scripts/flow-run-trace.js +16 -15
  125. package/scripts/flow-safety.js +2 -5
  126. package/scripts/flow-scanner-base.js +5 -7
  127. package/scripts/flow-scenario-engine.js +1 -5
  128. package/scripts/flow-security.js +29 -0
  129. package/scripts/flow-session-end.js +32 -41
  130. package/scripts/flow-session-learning.js +53 -49
  131. package/scripts/flow-setup-hooks.js +2 -3
  132. package/scripts/flow-skill-create.js +7 -12
  133. package/scripts/flow-skill-generator.js +12 -16
  134. package/scripts/flow-skill-learn.js +17 -8
  135. package/scripts/flow-skill-matcher.js +1 -2
  136. package/scripts/flow-spec-generator.js +2 -4
  137. package/scripts/flow-stack-wizard.js +5 -7
  138. package/scripts/flow-standards-learner.js +35 -16
  139. package/scripts/flow-start.js +2 -0
  140. package/scripts/flow-stats-collector.js +2 -2
  141. package/scripts/flow-status.js +10 -10
  142. package/scripts/flow-statusline-setup.js +2 -2
  143. package/scripts/flow-step-changelog.js +2 -3
  144. package/scripts/flow-step-comments.js +66 -81
  145. package/scripts/flow-step-complexity.js +50 -70
  146. package/scripts/flow-step-coverage.js +3 -5
  147. package/scripts/flow-step-knowledge.js +2 -3
  148. package/scripts/flow-step-pr-tests.js +64 -74
  149. package/scripts/flow-step-regression.js +3 -5
  150. package/scripts/flow-step-review.js +86 -103
  151. package/scripts/flow-step-security.js +111 -121
  152. package/scripts/flow-step-silent-failures.js +56 -83
  153. package/scripts/flow-step-simplifier.js +52 -70
  154. package/scripts/flow-story.js +4 -7
  155. package/scripts/flow-strict-adherence.js +3 -4
  156. package/scripts/flow-task-checkpoint.js +36 -5
  157. package/scripts/flow-task-enforcer.js +2 -24
  158. package/scripts/flow-tech-debt.js +1 -1
  159. package/scripts/flow-template-extractor.js +1 -0
  160. package/scripts/flow-templates.js +11 -13
  161. package/scripts/flow-test-api.js +9 -13
  162. package/scripts/flow-test-discovery.js +1 -1
  163. package/scripts/flow-test-generate.js +5 -9
  164. package/scripts/flow-test-integrity.js +3 -7
  165. package/scripts/flow-test-ui.js +5 -9
  166. package/scripts/flow-testing-deps.js +1 -3
  167. package/scripts/flow-tiered-learning.js +4 -4
  168. package/scripts/flow-todowrite-sync.js +1 -1
  169. package/scripts/flow-tokens.js +0 -2
  170. package/scripts/flow-verification-profile.js +6 -10
  171. package/scripts/flow-verify.js +12 -16
  172. package/scripts/flow-version-check.js +4 -12
  173. package/scripts/flow-webmcp-generator.js +3 -5
  174. package/scripts/flow-workflow-steps.js +0 -2
  175. package/scripts/flow-workflow.js +9 -11
  176. package/scripts/hooks/adapters/claude-code.js +31 -0
  177. package/scripts/hooks/core/config-change.js +1 -0
  178. package/scripts/hooks/core/extension-registry.js +0 -2
  179. package/scripts/hooks/core/instructions-loaded.js +1 -1
  180. package/scripts/hooks/core/observation-capture.js +5 -5
  181. package/scripts/hooks/core/phase-gate.js +5 -0
  182. package/scripts/hooks/core/post-compact.js +1 -12
  183. package/scripts/hooks/core/research-gate.js +2 -12
  184. package/scripts/hooks/core/routing-gate.js +6 -0
  185. package/scripts/hooks/core/task-completed.js +12 -0
  186. package/scripts/hooks/core/task-created.js +83 -0
  187. package/scripts/hooks/core/worktree-lifecycle.js +1 -1
  188. package/scripts/hooks/entry/claude-code/config-change.js +6 -29
  189. package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
  190. package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
  191. package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
  192. package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
  193. package/scripts/hooks/entry/claude-code/session-end.js +4 -28
  194. package/scripts/hooks/entry/claude-code/session-start.js +205 -243
  195. package/scripts/hooks/entry/claude-code/setup.js +8 -49
  196. package/scripts/hooks/entry/claude-code/stop.js +40 -72
  197. package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
  198. package/scripts/hooks/entry/claude-code/task-created.js +15 -0
  199. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
  200. package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
  201. package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
  202. package/scripts/hooks/entry/shared/hook-runner.js +99 -0
  203. package/scripts/hooks/entry/shared/read-stdin.js +0 -2
  204. package/scripts/postinstall.js +2 -0
  205. package/scripts/registries/api-registry.js +0 -2
  206. package/scripts/registries/component-registry.js +5 -9
  207. package/scripts/registries/contract-scanner.js +2 -9
  208. package/scripts/registries/function-registry.js +0 -2
  209. package/scripts/registries/schema-registry.js +14 -18
  210. package/scripts/registries/service-registry.js +23 -27
@@ -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
+ };