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,339 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Workspace — Sync & Lifecycle Management
5
+ *
6
+ * Story 6 (wf-a3a13d95): Re-reads member repo state files, updates manifest,
7
+ * detects changes, and provides unified workspace status.
8
+ */
9
+
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+
13
+ const { readMemberMetadata, extractCapabilities, extractEndpoints, detectStack, generateManifest, WORKSPACE_CONFIG_FILE, WORKSPACE_DIR } = require('./workspace');
14
+ const { buildIntegrationMap, detectTypeDrift } = require('./workspace-contracts');
15
+ const { getUnreadMessages, formatMessagesForDisplay } = require('./workspace-messages');
16
+
17
+ // ============================================================
18
+ // Workspace Sync (Criterion 1)
19
+ // ============================================================
20
+
21
+ /**
22
+ * Re-read all member repo state files and update the workspace manifest.
23
+ * @param {string} workspaceRoot
24
+ * @param {Object} [options] — { silent: boolean }
25
+ * @returns {Object} sync result
26
+ */
27
+ function syncWorkspace(workspaceRoot, options = {}) {
28
+ const { silent = false } = options;
29
+ const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
30
+
31
+ if (!fs.existsSync(configPath)) {
32
+ throw new Error('No wogi-workspace.json found. Run `flow workspace init` first.');
33
+ }
34
+
35
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
36
+ const result = {
37
+ success: true,
38
+ membersUpdated: 0,
39
+ changes: [],
40
+ warnings: []
41
+ };
42
+
43
+ // Read old manifest for diff
44
+ const manifestPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'workspace-manifest.json');
45
+ let oldManifest = null;
46
+ try {
47
+ if (fs.existsSync(manifestPath)) {
48
+ oldManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
49
+ }
50
+ } catch (_err) {
51
+ // Will regenerate from scratch
52
+ }
53
+
54
+ // Re-read each member
55
+ const members = [];
56
+ for (const [name, memberConfig] of Object.entries(config.members)) {
57
+ const memberPath = path.resolve(workspaceRoot, memberConfig.path);
58
+ const workflowPath = path.join(memberPath, '.workflow');
59
+
60
+ if (!fs.existsSync(workflowPath)) {
61
+ result.warnings.push(`Member '${name}' has no .workflow/ directory — skipping`);
62
+ continue;
63
+ }
64
+
65
+ const metadata = readMemberMetadata(workflowPath);
66
+ const stack = detectStack(metadata, memberPath);
67
+ const capabilities = extractCapabilities(metadata);
68
+ const endpoints = extractEndpoints(metadata);
69
+ const role = memberConfig.role || 'standalone';
70
+
71
+ members.push({ name, path: memberPath, workflowPath, metadata, stack, capabilities, endpoints, role });
72
+
73
+ // Detect changes vs old manifest
74
+ if (oldManifest && oldManifest.members[name]) {
75
+ const oldMember = oldManifest.members[name];
76
+ const oldProvides = new Set(oldMember.provides || []);
77
+ const oldConsumes = new Set(oldMember.consumes || []);
78
+
79
+ const newProvides = endpoints.provides.filter(ep => !oldProvides.has(ep));
80
+ const removedProvides = [...oldProvides].filter(ep => !endpoints.provides.includes(ep));
81
+ const newConsumes = endpoints.consumes.filter(ep => !oldConsumes.has(ep));
82
+ const removedConsumes = [...oldConsumes].filter(ep => !endpoints.consumes.includes(ep));
83
+
84
+ if (newProvides.length > 0) result.changes.push({ member: name, type: 'new-provides', endpoints: newProvides });
85
+ if (removedProvides.length > 0) result.changes.push({ member: name, type: 'removed-provides', endpoints: removedProvides });
86
+ if (newConsumes.length > 0) result.changes.push({ member: name, type: 'new-consumes', endpoints: newConsumes });
87
+ if (removedConsumes.length > 0) result.changes.push({ member: name, type: 'removed-consumes', endpoints: removedConsumes });
88
+ }
89
+
90
+ result.membersUpdated++;
91
+ }
92
+
93
+ // Generate new manifest
94
+ const newManifest = generateManifest(config.name, members);
95
+
96
+ // Detect type drift
97
+ const memberMetadata = {};
98
+ for (const m of members) memberMetadata[m.name] = m.metadata;
99
+ const drifts = detectTypeDrift(newManifest, memberMetadata);
100
+ if (drifts.length > 0) {
101
+ newManifest.integrations.typeDrift = drifts;
102
+ }
103
+
104
+ // Write updated manifest
105
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
106
+ fs.writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2));
107
+
108
+ // Write updated integration map
109
+ const integrationMap = buildIntegrationMap(newManifest);
110
+ const mapLines = ['# Integration Map\n', `Generated: ${integrationMap.generatedAt}`, `Match rate: ${integrationMap.stats.matchRate}%\n`];
111
+
112
+ if (integrationMap.matched.length > 0) {
113
+ mapLines.push('## Matched Endpoints\n');
114
+ mapLines.push('| Endpoint | Provider(s) | Consumer(s) | Score |');
115
+ mapLines.push('|----------|-------------|-------------|-------|');
116
+ for (const m of integrationMap.matched) {
117
+ mapLines.push(`| \`${m.endpoint}\` | ${m.providers.join(', ')} | ${m.consumers.join(', ')} | ${(m.matchScore * 100).toFixed(0)}% |`);
118
+ }
119
+ mapLines.push('');
120
+ }
121
+
122
+ if (integrationMap.orphanedConsumers.length > 0) {
123
+ mapLines.push('## Orphaned Consumers\n');
124
+ for (const o of integrationMap.orphanedConsumers) {
125
+ mapLines.push(`- \`${o.endpoint}\` — consumed by: ${o.consumers.join(', ')}`);
126
+ }
127
+ mapLines.push('');
128
+ }
129
+
130
+ fs.writeFileSync(
131
+ path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'integration-map.md'),
132
+ mapLines.join('\n')
133
+ );
134
+
135
+ if (!silent && result.changes.length > 0) {
136
+ console.log(`\n── Changes detected ──────────────────\n`);
137
+ for (const change of result.changes) {
138
+ const icon = change.type.includes('new') ? '✚' : '✗';
139
+ console.log(` ${icon} ${change.member}: ${change.type} — ${change.endpoints.join(', ')}`);
140
+ }
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ // ============================================================
147
+ // Workspace Status (Criterion 5)
148
+ // ============================================================
149
+
150
+ /**
151
+ * Generate a unified workspace status report
152
+ * @param {string} workspaceRoot
153
+ * @returns {string} formatted status report
154
+ */
155
+ function getWorkspaceStatus(workspaceRoot) {
156
+ const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
157
+ if (!fs.existsSync(configPath)) return 'No workspace found. Run `flow workspace init` first.';
158
+
159
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
160
+ const manifestPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'workspace-manifest.json');
161
+ const manifest = fs.existsSync(manifestPath)
162
+ ? JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
163
+ : null;
164
+
165
+ const lines = [];
166
+ lines.push(`đŸ—ī¸ Wogi Workspace: ${config.name}`);
167
+ lines.push('━'.repeat(40));
168
+ lines.push('');
169
+
170
+ // Members
171
+ lines.push('Members:');
172
+ for (const [name, memberConfig] of Object.entries(config.members)) {
173
+ const memberPath = path.resolve(workspaceRoot, memberConfig.path);
174
+ const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
175
+
176
+ let taskSummary = 'no .workflow/';
177
+ try {
178
+ if (fs.existsSync(readyPath)) {
179
+ const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
180
+ const inProgress = (ready.inProgress || []).length;
181
+ const readyCount = (ready.ready || []).length;
182
+ taskSummary = `${inProgress} in progress, ${readyCount} ready`;
183
+ }
184
+ } catch (_err) {
185
+ taskSummary = 'error reading';
186
+ }
187
+
188
+ const memberManifest = manifest?.members?.[name];
189
+ const stack = memberManifest ? `${memberManifest.stack.language}/${memberManifest.stack.framework}` : 'unknown';
190
+
191
+ lines.push(` đŸ“Ļ ${name} (${stack}) — ${taskSummary}`);
192
+ }
193
+ lines.push('');
194
+
195
+ // Integration summary
196
+ if (manifest) {
197
+ const matched = manifest.integrations.matched?.length ?? 0;
198
+ const orphanedC = manifest.integrations.orphanedConsumers?.length ?? 0;
199
+ const orphanedP = manifest.integrations.orphanedProviders?.length ?? 0;
200
+ const drifts = manifest.integrations.typeDrift?.length ?? 0;
201
+
202
+ lines.push('Integrations:');
203
+ lines.push(` 🔗 ${matched} matched endpoints`);
204
+ if (orphanedC > 0) lines.push(` âš ī¸ ${orphanedC} orphaned consumer${orphanedC !== 1 ? 's' : ''}`);
205
+ if (orphanedP > 0) lines.push(` â„šī¸ ${orphanedP} endpoint${orphanedP !== 1 ? 's' : ''} without consumers`);
206
+ if (drifts > 0) lines.push(` âš ī¸ ${drifts} type drift${drifts !== 1 ? 's' : ''} detected`);
207
+ lines.push('');
208
+ }
209
+
210
+ // Messages
211
+ const unread = getUnreadMessages(workspaceRoot, 'all');
212
+ if (unread.length > 0) {
213
+ lines.push(`Messages (${unread.length} unread):`);
214
+ lines.push(formatMessagesForDisplay(unread, 5));
215
+ lines.push('');
216
+ }
217
+
218
+ // Contracts
219
+ const contractsDir = path.join(workspaceRoot, WORKSPACE_DIR, 'contracts');
220
+ if (fs.existsSync(contractsDir)) {
221
+ const contracts = fs.readdirSync(contractsDir).filter(f => !f.startsWith('.'));
222
+ if (contracts.length > 0) {
223
+ lines.push(`Contracts: ${contracts.length}`);
224
+ for (const c of contracts) {
225
+ const stat = fs.statSync(path.join(contractsDir, c));
226
+ const age = formatAge(stat.mtime);
227
+ lines.push(` 📋 ${c} (updated ${age})`);
228
+ }
229
+ lines.push('');
230
+ }
231
+ }
232
+
233
+ // Workspace-level tasks
234
+ const wsReadyPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'ready.json');
235
+ if (fs.existsSync(wsReadyPath)) {
236
+ try {
237
+ const wsReady = JSON.parse(fs.readFileSync(wsReadyPath, 'utf-8'));
238
+ const wsInProgress = (wsReady.inProgress || []).length;
239
+ const wsReadyCount = (wsReady.ready || []).length;
240
+ if (wsInProgress > 0 || wsReadyCount > 0) {
241
+ lines.push(`Workspace tasks: ${wsInProgress} in progress, ${wsReadyCount} ready`);
242
+ lines.push('');
243
+ }
244
+ } catch (_err) {
245
+ // Non-critical
246
+ }
247
+ }
248
+
249
+ return lines.join('\n');
250
+ }
251
+
252
+ function formatAge(date) {
253
+ const ms = Date.now() - new Date(date).getTime();
254
+ const minutes = Math.floor(ms / 60000);
255
+ if (minutes < 60) return `${minutes}m ago`;
256
+ const hours = Math.floor(minutes / 60);
257
+ if (hours < 24) return `${hours}h ago`;
258
+ const days = Math.floor(hours / 24);
259
+ return `${days}d ago`;
260
+ }
261
+
262
+ // ============================================================
263
+ // Add/Remove Members (Criterion 4)
264
+ // ============================================================
265
+
266
+ /**
267
+ * Add a member repo to the workspace
268
+ * @param {string} workspaceRoot
269
+ * @param {string} memberPath — path to the repo
270
+ * @param {string} [role] — optional role override
271
+ * @returns {Object} result
272
+ */
273
+ function addMember(workspaceRoot, memberPath, role) {
274
+ const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
275
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
276
+
277
+ const absPath = path.resolve(workspaceRoot, memberPath);
278
+ const name = path.basename(absPath);
279
+ const workflowPath = path.join(absPath, '.workflow');
280
+
281
+ if (!fs.existsSync(workflowPath)) {
282
+ throw new Error(`${memberPath} does not have a .workflow/ directory. Run 'flow init' there first.`);
283
+ }
284
+
285
+ if (config.members[name]) {
286
+ throw new Error(`Member '${name}' already exists in workspace.`);
287
+ }
288
+
289
+ // Read metadata to auto-detect role
290
+ const metadata = readMemberMetadata(workflowPath);
291
+ const endpoints = extractEndpoints(metadata);
292
+ const detectedRole = role || require('./workspace').autoDetectRole(endpoints);
293
+
294
+ config.members[name] = {
295
+ path: `./${name}`,
296
+ role: detectedRole
297
+ };
298
+
299
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
300
+
301
+ // Re-sync to update manifest
302
+ syncWorkspace(workspaceRoot, { silent: true });
303
+
304
+ return { name, role: detectedRole, path: memberPath };
305
+ }
306
+
307
+ /**
308
+ * Remove a member repo from the workspace
309
+ * @param {string} workspaceRoot
310
+ * @param {string} memberName
311
+ * @returns {boolean} success
312
+ */
313
+ function removeMember(workspaceRoot, memberName) {
314
+ const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
315
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
316
+
317
+ if (!config.members[memberName]) {
318
+ throw new Error(`Member '${memberName}' not found in workspace.`);
319
+ }
320
+
321
+ delete config.members[memberName];
322
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
323
+
324
+ // Re-sync to update manifest
325
+ syncWorkspace(workspaceRoot, { silent: true });
326
+
327
+ return true;
328
+ }
329
+
330
+ // ============================================================
331
+ // Exports
332
+ // ============================================================
333
+
334
+ module.exports = {
335
+ syncWorkspace,
336
+ getWorkspaceStatus,
337
+ addMember,
338
+ removeMember
339
+ };