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,441 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Workspace — Agent Communication (Message Bus)
5
+ *
6
+ * Story 4 (wf-0206b2b5): File-based message bus for agent-to-agent
7
+ * communication across repos. Supports structured message types,
8
+ * lifecycle management, and suggested task auto-creation.
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const crypto = require('node:crypto');
14
+
15
+ // ============================================================
16
+ // Constants
17
+ // ============================================================
18
+
19
+ const MESSAGE_TYPES = [
20
+ 'contract-change', // "I changed an API endpoint"
21
+ 'question', // "Does your side handle X?"
22
+ 'bug-report', // "Your endpoint returns 500 when I send Y"
23
+ 'task-complete', // "I finished my side of feature Z"
24
+ 'needs-help', // "I'm stuck, can you check X on your side?"
25
+ 'heads-up' // "I'm about to change Y, just FYI"
26
+ ];
27
+
28
+ const MESSAGE_STATUSES = ['pending', 'acknowledged', 'task-created', 'resolved'];
29
+
30
+ const MESSAGE_ID_PATTERN = /^msg-[a-f0-9]{8}$/;
31
+
32
+ // ============================================================
33
+ // Message Creation (Criterion 1)
34
+ // ============================================================
35
+
36
+ /**
37
+ * Generate a unique message ID
38
+ * @returns {string} msg-XXXXXXXX
39
+ */
40
+ function generateMessageId() {
41
+ return 'msg-' + crypto.randomBytes(4).toString('hex');
42
+ }
43
+
44
+ /**
45
+ * Create a structured message
46
+ * @param {Object} params
47
+ * @param {string} params.from — sender repo name
48
+ * @param {string} params.to — receiver repo name, "manager", or "all"
49
+ * @param {string} params.type — one of MESSAGE_TYPES
50
+ * @param {string} params.subject — short description
51
+ * @param {string} params.body — detailed description
52
+ * @param {string} [params.priority] — "low", "medium", "high", "critical"
53
+ * @param {string} [params.diff] — git diff or contract diff
54
+ * @param {Object} [params.suggestedTask] — auto-create task in target repo
55
+ * @param {boolean} [params.actionRequired] — does the receiver need to act?
56
+ * @returns {Object} message object
57
+ */
58
+ function createMessage({ from, to, type, subject, body, priority, diff, suggestedTask, actionRequired }) {
59
+ if (typeof from !== 'string' || !from.trim()) {
60
+ throw new Error('Message "from" must be a non-empty string');
61
+ }
62
+ if (typeof subject !== 'string' || !subject.trim()) {
63
+ throw new Error('Message "subject" must be a non-empty string');
64
+ }
65
+ if (!MESSAGE_TYPES.includes(type)) {
66
+ throw new Error(`Invalid message type: ${type}. Must be one of: ${MESSAGE_TYPES.join(', ')}`);
67
+ }
68
+
69
+ return {
70
+ id: generateMessageId(),
71
+ from,
72
+ to: to || 'all',
73
+ type,
74
+ priority: priority || 'medium',
75
+ timestamp: new Date().toISOString(),
76
+ subject,
77
+ body,
78
+ ...(diff && { diff }),
79
+ ...(suggestedTask && { suggestedTask }),
80
+ actionRequired: actionRequired ?? (type === 'contract-change' || type === 'bug-report'),
81
+ status: 'pending'
82
+ };
83
+ }
84
+
85
+ // ============================================================
86
+ // Message Persistence (Criterion 2 — lifecycle)
87
+ // ============================================================
88
+
89
+ /**
90
+ * Save a message to the workspace message bus
91
+ * @param {string} workspaceRoot
92
+ * @param {Object} message
93
+ * @returns {string} message file path
94
+ */
95
+ function saveMessage(workspaceRoot, message) {
96
+ const messagesDir = path.join(workspaceRoot, '.workspace', 'messages');
97
+ fs.mkdirSync(messagesDir, { recursive: true });
98
+
99
+ const filePath = path.join(messagesDir, `${message.id}.json`);
100
+ fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
101
+ return filePath;
102
+ }
103
+
104
+ /**
105
+ * Read all messages from the workspace
106
+ * @param {string} workspaceRoot
107
+ * @param {Object} [filter] — { status, from, to, type }
108
+ * @returns {Array<Object>} messages sorted by timestamp (newest first)
109
+ */
110
+ function readMessages(workspaceRoot, filter = {}) {
111
+ const messagesDir = path.join(workspaceRoot, '.workspace', 'messages');
112
+ if (!fs.existsSync(messagesDir)) return [];
113
+
114
+ const messages = [];
115
+ const files = fs.readdirSync(messagesDir).filter(f => f.endsWith('.json'));
116
+
117
+ for (const file of files) {
118
+ try {
119
+ const content = JSON.parse(fs.readFileSync(path.join(messagesDir, file), 'utf-8'));
120
+ if (!content.id) continue;
121
+
122
+ // Apply filters
123
+ if (filter.status && content.status !== filter.status) continue;
124
+ if (filter.from && content.from !== filter.from) continue;
125
+ if (filter.to && content.to !== filter.to && content.to !== 'all') continue;
126
+ if (filter.type && content.type !== filter.type) continue;
127
+
128
+ messages.push(content);
129
+ } catch (_err) {
130
+ // Skip malformed messages
131
+ }
132
+ }
133
+
134
+ // Sort newest first
135
+ messages.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
136
+ return messages;
137
+ }
138
+
139
+ /**
140
+ * Update a message's status
141
+ * @param {string} workspaceRoot
142
+ * @param {string} messageId
143
+ * @param {string} newStatus
144
+ * @param {Object} [extra] — additional fields to merge
145
+ * @returns {Object|null} updated message or null if not found
146
+ */
147
+ function updateMessageStatus(workspaceRoot, messageId, newStatus, extra = {}) {
148
+ if (!MESSAGE_ID_PATTERN.test(messageId)) {
149
+ throw new Error(`Invalid messageId: ${messageId}. Must match msg-[a-f0-9]{8}`);
150
+ }
151
+ if (!MESSAGE_STATUSES.includes(newStatus)) {
152
+ throw new Error(`Invalid status: ${newStatus}. Must be one of: ${MESSAGE_STATUSES.join(', ')}`);
153
+ }
154
+
155
+ const filePath = path.join(workspaceRoot, '.workspace', 'messages', `${messageId}.json`);
156
+ if (!fs.existsSync(filePath)) return null;
157
+
158
+ try {
159
+ const message = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
160
+ message.status = newStatus;
161
+ message.updatedAt = new Date().toISOString();
162
+ Object.assign(message, extra);
163
+ fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
164
+ return message;
165
+ } catch (_err) {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Get unread (pending) messages for a specific repo
172
+ * @param {string} workspaceRoot
173
+ * @param {string} repoName
174
+ * @returns {Array<Object>} unread messages
175
+ */
176
+ function getUnreadMessages(workspaceRoot, repoName) {
177
+ return readMessages(workspaceRoot, { status: 'pending', to: repoName });
178
+ }
179
+
180
+ // ============================================================
181
+ // Change Notifications (Criterion 3)
182
+ // ============================================================
183
+
184
+ /**
185
+ * Generate a change notification message when a repo changes a contract endpoint
186
+ * @param {string} repoName — which repo made the change
187
+ * @param {Object} change — { endpoint, method, action, details }
188
+ * @param {string[]} affectedRepos — repos that consume this endpoint
189
+ * @returns {Array<Object>} messages to send (one per affected repo)
190
+ */
191
+ function generateChangeNotifications(repoName, change, affectedRepos) {
192
+ const messages = [];
193
+
194
+ for (const targetRepo of affectedRepos) {
195
+ if (targetRepo === repoName) continue; // Don't notify self
196
+
197
+ const msg = createMessage({
198
+ from: repoName,
199
+ to: targetRepo,
200
+ type: 'contract-change',
201
+ priority: change.action === 'removed' ? 'critical' : 'high',
202
+ subject: `${change.action}: ${change.method} ${change.endpoint}`,
203
+ body: change.details || `The endpoint ${change.method} ${change.endpoint} was ${change.action} by ${repoName}.`,
204
+ diff: change.diff,
205
+ actionRequired: true,
206
+ suggestedTask: {
207
+ title: `Update ${targetRepo} for contract change: ${change.method} ${change.endpoint}`,
208
+ type: 'fix',
209
+ criteria: [`Handle ${change.action} of ${change.method} ${change.endpoint} from ${repoName}`]
210
+ }
211
+ });
212
+
213
+ messages.push(msg);
214
+ }
215
+
216
+ return messages;
217
+ }
218
+
219
+ // ============================================================
220
+ // Suggested Task Auto-Creation (Criterion 4)
221
+ // ============================================================
222
+
223
+ /**
224
+ * Process suggested tasks from messages — create tasks in target repo's ready.json
225
+ * @param {string} workspaceRoot
226
+ * @param {Object} message — message with suggestedTask
227
+ * @returns {Object|null} created task entry, or null if no suggestion
228
+ */
229
+ function processSuggestedTask(workspaceRoot, message) {
230
+ if (!message.suggestedTask) return null;
231
+
232
+ const targetRepo = message.to;
233
+ if (targetRepo === 'all' || targetRepo === 'manager') return null;
234
+
235
+ // Find the target repo's ready.json
236
+ const config = readWorkspaceConfig(workspaceRoot);
237
+ if (!config || !config.members[targetRepo]) return null;
238
+
239
+ const memberPath = path.resolve(workspaceRoot, config.members[targetRepo].path);
240
+ const resolvedRoot = path.resolve(workspaceRoot);
241
+ if (!memberPath.startsWith(resolvedRoot + path.sep) && memberPath !== resolvedRoot) {
242
+ return null; // Path traversal attempt — member path escapes workspace root
243
+ }
244
+ const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
245
+
246
+ if (!fs.existsSync(readyPath)) return null;
247
+
248
+ try {
249
+ const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
250
+ const taskId = 'wf-' + crypto.randomBytes(4).toString('hex');
251
+
252
+ const task = {
253
+ id: taskId,
254
+ title: message.suggestedTask.title || `From ${message.from}: ${message.subject}`,
255
+ type: message.suggestedTask.type || 'fix',
256
+ level: 'L2',
257
+ priority: message.priority === 'critical' ? 'P0' : 'P1',
258
+ source: `workspace-message:${message.id}`,
259
+ status: 'ready',
260
+ description: message.body,
261
+ createdAt: new Date().toISOString()
262
+ };
263
+
264
+ if (!ready.ready) ready.ready = [];
265
+ ready.ready.push(task);
266
+ ready.lastUpdated = new Date().toISOString();
267
+ fs.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
268
+
269
+ // Update message status
270
+ updateMessageStatus(workspaceRoot, message.id, 'task-created', { createdTaskId: taskId });
271
+
272
+ return task;
273
+ } catch (_err) {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ // ============================================================
279
+ // Message Display (Criterion 5)
280
+ // ============================================================
281
+
282
+ /**
283
+ * Format messages for session display
284
+ * @param {Array<Object>} messages
285
+ * @param {number} [maxMessages=10]
286
+ * @returns {string} formatted text
287
+ */
288
+ function formatMessagesForDisplay(messages, maxMessages = 10) {
289
+ if (messages.length === 0) return 'No messages.';
290
+
291
+ const lines = [];
292
+ const displayed = messages.slice(0, maxMessages);
293
+
294
+ for (const msg of displayed) {
295
+ const icon = getMessageIcon(msg.type);
296
+ const priority = msg.priority === 'critical' ? ' 🔴' : msg.priority === 'high' ? ' 🟡' : '';
297
+ const action = msg.actionRequired ? ' [ACTION REQUIRED]' : '';
298
+ const age = formatAge(msg.timestamp);
299
+
300
+ lines.push(`${icon} ${msg.from}→${msg.to}: ${msg.subject}${priority}${action} (${age})`);
301
+ }
302
+
303
+ if (messages.length > maxMessages) {
304
+ lines.push(` ... and ${messages.length - maxMessages} more`);
305
+ }
306
+
307
+ return lines.join('\n');
308
+ }
309
+
310
+ function getMessageIcon(type) {
311
+ switch (type) {
312
+ case 'contract-change': return '📋';
313
+ case 'question': return '❓';
314
+ case 'bug-report': return '🐛';
315
+ case 'task-complete': return '✅';
316
+ case 'needs-help': return '🆘';
317
+ case 'heads-up': return '👀';
318
+ default: return '💬';
319
+ }
320
+ }
321
+
322
+ function formatAge(timestamp) {
323
+ const ms = Date.now() - new Date(timestamp).getTime();
324
+ const minutes = Math.floor(ms / 60000);
325
+ if (minutes < 60) return `${minutes}m ago`;
326
+ const hours = Math.floor(minutes / 60);
327
+ if (hours < 24) return `${hours}h ago`;
328
+ const days = Math.floor(hours / 24);
329
+ return `${days}d ago`;
330
+ }
331
+
332
+ // ============================================================
333
+ // Agent-to-Agent Questions (Criterion 6)
334
+ // ============================================================
335
+
336
+ /**
337
+ * Create a question from one repo agent to another
338
+ * @param {string} fromRepo
339
+ * @param {string} toRepo
340
+ * @param {string} question
341
+ * @param {Object} [context] — optional context (file paths, error messages, etc.)
342
+ * @returns {Object} question message
343
+ */
344
+ function askQuestion(fromRepo, toRepo, question, context = {}) {
345
+ return createMessage({
346
+ from: fromRepo,
347
+ to: toRepo,
348
+ type: 'question',
349
+ subject: question.length > 80 ? question.substring(0, 77) + '...' : question,
350
+ body: question,
351
+ priority: 'medium',
352
+ actionRequired: true,
353
+ ...(context.diff && { diff: context.diff })
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Create a response to a question
359
+ * @param {string} workspaceRoot
360
+ * @param {string} originalMessageId — the question being answered
361
+ * @param {string} fromRepo — who is answering
362
+ * @param {string} answer
363
+ * @returns {Object} response message
364
+ */
365
+ function answerQuestion(workspaceRoot, originalMessageId, fromRepo, answer) {
366
+ if (!MESSAGE_ID_PATTERN.test(originalMessageId)) {
367
+ throw new Error(`Invalid messageId: ${originalMessageId}. Must match msg-[a-f0-9]{8}`);
368
+ }
369
+
370
+ // Mark original question as resolved
371
+ updateMessageStatus(workspaceRoot, originalMessageId, 'resolved');
372
+
373
+ // Read original to get the sender
374
+ const filePath = path.join(workspaceRoot, '.workspace', 'messages', `${originalMessageId}.json`);
375
+ let originalFrom = 'unknown';
376
+ try {
377
+ const original = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
378
+ originalFrom = original.from;
379
+ } catch (_err) {
380
+ // Non-critical
381
+ }
382
+
383
+ return createMessage({
384
+ from: fromRepo,
385
+ to: originalFrom,
386
+ type: 'heads-up',
387
+ subject: `Re: ${originalMessageId}`,
388
+ body: answer,
389
+ priority: 'medium',
390
+ actionRequired: false
391
+ });
392
+ }
393
+
394
+ // ============================================================
395
+ // Helpers
396
+ // ============================================================
397
+
398
+ /**
399
+ * Read workspace config (wogi-workspace.json)
400
+ * @param {string} workspaceRoot
401
+ * @returns {Object|null}
402
+ */
403
+ function readWorkspaceConfig(workspaceRoot) {
404
+ const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
405
+ try {
406
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
407
+ } catch (_err) {
408
+ return null;
409
+ }
410
+ }
411
+
412
+ // ============================================================
413
+ // Exports
414
+ // ============================================================
415
+
416
+ module.exports = {
417
+ // Message creation
418
+ createMessage,
419
+ generateMessageId,
420
+ MESSAGE_TYPES,
421
+ MESSAGE_STATUSES,
422
+
423
+ // Persistence
424
+ saveMessage,
425
+ readMessages,
426
+ updateMessageStatus,
427
+ getUnreadMessages,
428
+
429
+ // Change notifications
430
+ generateChangeNotifications,
431
+
432
+ // Suggested tasks
433
+ processSuggestedTask,
434
+
435
+ // Display
436
+ formatMessagesForDisplay,
437
+
438
+ // Agent questions
439
+ askQuestion,
440
+ answerQuestion
441
+ };