wogiflow 2.4.2 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/wogi-start.md +124 -0
- package/.claude/docs/claude-code-compatibility.md +51 -0
- package/.claude/docs/explore-agents.md +11 -0
- package/.claude/settings.json +12 -1
- package/.workflow/models/registry.json +1 -1
- package/bin/flow +11 -1
- package/lib/workspace-contracts.js +599 -0
- package/lib/workspace-intelligence.js +600 -0
- package/lib/workspace-messages.js +441 -0
- package/lib/workspace-routing.js +485 -0
- package/lib/workspace-sync.js +339 -0
- package/lib/workspace.js +1073 -0
- package/package.json +4 -4
- package/scripts/MEMORY-ARCHITECTURE.md +1 -1
- package/scripts/base-workflow-step.js +136 -0
- package/scripts/flow-adaptive-learning.js +8 -9
- package/scripts/flow-aggregate.js +11 -6
- package/scripts/flow-api-index.js +4 -6
- package/scripts/flow-assumption-detector.js +0 -2
- package/scripts/flow-audit.js +15 -2
- package/scripts/flow-auto-context.js +8 -12
- package/scripts/flow-auto-learn.js +49 -49
- package/scripts/flow-background.js +5 -6
- package/scripts/flow-bridge-state.js +8 -10
- package/scripts/flow-bulk-loop.js +1 -3
- package/scripts/flow-bulk-orchestrator.js +1 -3
- package/scripts/flow-cascade-completion.js +0 -2
- package/scripts/flow-cascade.js +4 -4
- package/scripts/flow-checkpoint.js +10 -13
- package/scripts/flow-code-intelligence.js +10 -12
- package/scripts/flow-community-sync.js +4 -4
- package/scripts/flow-community.js +12 -20
- package/scripts/flow-config-defaults.js +28 -2
- package/scripts/flow-config-interactive.js +9 -5
- package/scripts/flow-config-loader.js +49 -92
- package/scripts/flow-config-substitution.js +0 -2
- package/scripts/flow-context-estimator.js +4 -4
- package/scripts/flow-context-init.js +10 -12
- package/scripts/flow-context-manager.js +0 -2
- package/scripts/flow-context-scoring.js +2 -2
- package/scripts/flow-contract-scan.js +6 -9
- package/scripts/flow-correct.js +29 -27
- package/scripts/flow-correction-detector.js +5 -1
- package/scripts/flow-damage-control.js +47 -54
- package/scripts/flow-decisions-merge.js +4 -14
- package/scripts/flow-diff.js +5 -8
- package/scripts/flow-done-gates.js +786 -0
- package/scripts/flow-done-report.js +123 -0
- package/scripts/flow-done.js +71 -717
- package/scripts/flow-entropy-monitor.js +1 -3
- package/scripts/flow-eval-calibration.js +257 -0
- package/scripts/flow-eval-judge.js +10 -1
- package/scripts/flow-eval.js +14 -5
- package/scripts/flow-extraction-review.js +1 -0
- package/scripts/flow-failure-categories.js +0 -2
- package/scripts/flow-figma-confirm.js +5 -9
- package/scripts/flow-figma-generate.js +8 -10
- package/scripts/flow-figma-index.js +8 -10
- package/scripts/flow-figma-match.js +3 -5
- package/scripts/flow-figma-mcp-server.js +2 -4
- package/scripts/flow-figma-orchestrator.js +2 -3
- package/scripts/flow-figma-registry.js +2 -3
- package/scripts/flow-framework-resolver.js +0 -2
- package/scripts/flow-function-index.js +4 -6
- package/scripts/flow-gate-confidence.js +2 -2
- package/scripts/flow-gitignore.js +0 -2
- package/scripts/flow-guided-edit.js +5 -6
- package/scripts/flow-health.js +5 -6
- package/scripts/flow-hook-errors.js +6 -0
- package/scripts/flow-hook-status.js +263 -0
- package/scripts/flow-hooks.js +17 -29
- package/scripts/flow-http-client.js +9 -8
- package/scripts/flow-hybrid-interactive.js +7 -12
- package/scripts/flow-hybrid-test.js +12 -13
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-io.js +21 -4
- package/scripts/flow-knowledge-router.js +9 -3
- package/scripts/flow-learning-orchestrator.js +318 -13
- package/scripts/flow-links.js +5 -7
- package/scripts/flow-long-input-association.js +275 -0
- package/scripts/flow-long-input-chunking.js +1 -0
- package/scripts/flow-long-input-cli.js +0 -2
- package/scripts/flow-long-input-complexity.js +0 -2
- package/scripts/flow-long-input-constants.js +0 -2
- package/scripts/flow-long-input-contradictions.js +351 -0
- package/scripts/flow-long-input-detection.js +0 -2
- package/scripts/flow-long-input-passes.js +885 -0
- package/scripts/flow-long-input-stories.js +1 -1
- package/scripts/flow-long-input-voice.js +0 -2
- package/scripts/flow-long-input.js +425 -3005
- package/scripts/flow-loop-retry-learning.js +2 -3
- package/scripts/flow-lsp.js +3 -3
- package/scripts/flow-mcp-docs.js +3 -4
- package/scripts/flow-memory-db.js +6 -8
- package/scripts/flow-memory-sync.js +18 -11
- package/scripts/flow-metrics.js +1 -2
- package/scripts/flow-model-adapter.js +2 -3
- package/scripts/flow-model-config.js +72 -104
- package/scripts/flow-model-router.js +2 -2
- package/scripts/flow-model-types.js +0 -2
- package/scripts/flow-multi-approach.js +5 -6
- package/scripts/flow-orchestrate-context.js +3 -7
- package/scripts/flow-orchestrate-rollback.js +3 -8
- package/scripts/flow-orchestrate-state.js +8 -14
- package/scripts/flow-orchestrate-templates.js +2 -6
- package/scripts/flow-orchestrate-validator.js +5 -9
- package/scripts/flow-orchestrate.js +126 -103
- package/scripts/flow-output.js +0 -2
- package/scripts/flow-parallel.js +1 -1
- package/scripts/flow-paths.js +23 -2
- package/scripts/flow-pattern-enforcer.js +30 -28
- package/scripts/flow-pattern-extractor.js +3 -4
- package/scripts/flow-pending.js +0 -2
- package/scripts/flow-permissions.js +2 -3
- package/scripts/flow-plugin-registry.js +10 -12
- package/scripts/flow-prd-manager.js +1 -1
- package/scripts/flow-progress.js +7 -9
- package/scripts/flow-prompt-composer.js +3 -3
- package/scripts/flow-prompt-template.js +2 -2
- package/scripts/flow-providers.js +7 -4
- package/scripts/flow-registry-manager.js +7 -12
- package/scripts/flow-regression.js +9 -11
- package/scripts/flow-roadmap.js +2 -2
- package/scripts/flow-run-trace.js +16 -15
- package/scripts/flow-safety.js +2 -5
- package/scripts/flow-scanner-base.js +5 -7
- package/scripts/flow-scenario-engine.js +1 -5
- package/scripts/flow-security.js +29 -0
- package/scripts/flow-session-end.js +32 -41
- package/scripts/flow-session-learning.js +53 -49
- package/scripts/flow-setup-hooks.js +2 -3
- package/scripts/flow-skill-create.js +7 -12
- package/scripts/flow-skill-generator.js +12 -16
- package/scripts/flow-skill-learn.js +17 -8
- package/scripts/flow-skill-matcher.js +1 -2
- package/scripts/flow-spec-generator.js +2 -4
- package/scripts/flow-stack-wizard.js +5 -7
- package/scripts/flow-standards-learner.js +35 -16
- package/scripts/flow-start.js +2 -0
- package/scripts/flow-stats-collector.js +2 -2
- package/scripts/flow-status.js +10 -10
- package/scripts/flow-statusline-setup.js +2 -2
- package/scripts/flow-step-changelog.js +2 -3
- package/scripts/flow-step-comments.js +66 -81
- package/scripts/flow-step-complexity.js +50 -70
- package/scripts/flow-step-coverage.js +3 -5
- package/scripts/flow-step-knowledge.js +2 -3
- package/scripts/flow-step-pr-tests.js +64 -74
- package/scripts/flow-step-regression.js +3 -5
- package/scripts/flow-step-review.js +86 -103
- package/scripts/flow-step-security.js +111 -121
- package/scripts/flow-step-silent-failures.js +56 -83
- package/scripts/flow-step-simplifier.js +52 -70
- package/scripts/flow-story.js +4 -7
- package/scripts/flow-strict-adherence.js +3 -4
- package/scripts/flow-task-checkpoint.js +36 -5
- package/scripts/flow-task-enforcer.js +2 -24
- package/scripts/flow-tech-debt.js +1 -1
- package/scripts/flow-template-extractor.js +1 -0
- package/scripts/flow-templates.js +11 -13
- package/scripts/flow-test-api.js +9 -13
- package/scripts/flow-test-discovery.js +1 -1
- package/scripts/flow-test-generate.js +5 -9
- package/scripts/flow-test-integrity.js +3 -7
- package/scripts/flow-test-ui.js +5 -9
- package/scripts/flow-testing-deps.js +1 -3
- package/scripts/flow-tiered-learning.js +4 -4
- package/scripts/flow-todowrite-sync.js +1 -1
- package/scripts/flow-tokens.js +0 -2
- package/scripts/flow-verification-profile.js +6 -10
- package/scripts/flow-verify.js +12 -16
- package/scripts/flow-version-check.js +4 -12
- package/scripts/flow-webmcp-generator.js +3 -5
- package/scripts/flow-workflow-steps.js +0 -2
- package/scripts/flow-workflow.js +9 -11
- package/scripts/hooks/adapters/claude-code.js +31 -0
- package/scripts/hooks/core/config-change.js +1 -0
- package/scripts/hooks/core/extension-registry.js +0 -2
- package/scripts/hooks/core/instructions-loaded.js +1 -1
- package/scripts/hooks/core/observation-capture.js +5 -5
- package/scripts/hooks/core/phase-gate.js +5 -0
- package/scripts/hooks/core/post-compact.js +1 -12
- package/scripts/hooks/core/research-gate.js +2 -12
- package/scripts/hooks/core/routing-gate.js +6 -0
- package/scripts/hooks/core/task-completed.js +12 -0
- package/scripts/hooks/core/task-created.js +83 -0
- package/scripts/hooks/core/worktree-lifecycle.js +1 -1
- package/scripts/hooks/entry/claude-code/config-change.js +6 -29
- package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
- package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
- package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
- package/scripts/hooks/entry/claude-code/session-end.js +4 -28
- package/scripts/hooks/entry/claude-code/session-start.js +205 -243
- package/scripts/hooks/entry/claude-code/setup.js +8 -49
- package/scripts/hooks/entry/claude-code/stop.js +40 -72
- package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
- package/scripts/hooks/entry/claude-code/task-created.js +15 -0
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
- package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
- package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
- package/scripts/hooks/entry/shared/hook-runner.js +99 -0
- package/scripts/hooks/entry/shared/read-stdin.js +0 -2
- package/scripts/postinstall.js +2 -0
- package/scripts/registries/api-registry.js +0 -2
- package/scripts/registries/component-registry.js +5 -9
- package/scripts/registries/contract-scanner.js +2 -9
- package/scripts/registries/function-registry.js +0 -2
- package/scripts/registries/schema-registry.js +14 -18
- package/scripts/registries/service-registry.js +23 -27
|
@@ -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
|
+
};
|