wogiflow 2.4.3 → 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 +24 -0
- package/.claude/docs/explore-agents.md +11 -0
- package/.claude/settings.json +11 -0
- 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 +1 -1
- package/scripts/flow-config-defaults.js +28 -0
- package/scripts/flow-eval-calibration.js +257 -0
- package/scripts/flow-eval-judge.js +10 -1
- package/scripts/flow-eval.js +9 -0
- package/scripts/hooks/adapters/claude-code.js +29 -0
- package/scripts/hooks/core/task-created.js +83 -0
- package/scripts/hooks/entry/claude-code/task-created.js +15 -0
- package/scripts/postinstall.js +2 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Workspace — Task Routing & Sub-Agent Delegation
|
|
5
|
+
*
|
|
6
|
+
* Story 3 (wf-824638e4): The manager agent's brain — analyzes tasks,
|
|
7
|
+
* determines which repo(s) to target, determines ordering,
|
|
8
|
+
* and generates sub-agent delegation instructions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Routing Keywords (Criterion 1)
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
const ROLE_KEYWORDS = {
|
|
20
|
+
consumer: [
|
|
21
|
+
'page', 'component', 'ui', 'style', 'css', 'layout', 'form', 'button',
|
|
22
|
+
'modal', 'screen', 'view', 'frontend', 'client', 'browser', 'render',
|
|
23
|
+
'hook', 'state', 'redux', 'zustand', 'store', 'theme', 'responsive',
|
|
24
|
+
'animation', 'toast', 'notification-ui', 'sidebar', 'navbar', 'header'
|
|
25
|
+
],
|
|
26
|
+
provider: [
|
|
27
|
+
'endpoint', 'route', 'controller', 'model', 'database', 'migration',
|
|
28
|
+
'schema', 'backend', 'server', 'api', 'query', 'mutation', 'resolver',
|
|
29
|
+
'middleware', 'auth', 'jwt', 'session', 'seed', 'fixture', 'orm',
|
|
30
|
+
'sql', 'table', 'index', 'relation', 'service-layer'
|
|
31
|
+
],
|
|
32
|
+
library: [
|
|
33
|
+
'shared', 'utility', 'types', 'common', 'helper', 'constant', 'enum',
|
|
34
|
+
'interface', 'typedef', 'lib', 'package', 'module'
|
|
35
|
+
],
|
|
36
|
+
crossRepo: [
|
|
37
|
+
'api', 'contract', 'schema', 'integration', 'full-stack', 'end-to-end',
|
|
38
|
+
'e2e', 'both', 'cross', 'sync', 'together'
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ============================================================
|
|
43
|
+
// Route Analysis (Criterion 1)
|
|
44
|
+
// ============================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Analyze a task description and determine which repo(s) should handle it.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} taskDescription — the user's task description
|
|
50
|
+
* @param {Object} manifest — workspace-manifest.json content
|
|
51
|
+
* @returns {Object} routing decision
|
|
52
|
+
*/
|
|
53
|
+
function analyzeTaskRouting(taskDescription, manifest) {
|
|
54
|
+
if (!manifest?.members) {
|
|
55
|
+
return {
|
|
56
|
+
type: 'single-repo',
|
|
57
|
+
target: null,
|
|
58
|
+
scores: {},
|
|
59
|
+
reason: 'No members in manifest'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const desc = taskDescription.toLowerCase();
|
|
64
|
+
const scores = {}; // memberName → score
|
|
65
|
+
const crossRepoScore = scoreKeywords(desc, ROLE_KEYWORDS.crossRepo);
|
|
66
|
+
|
|
67
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
68
|
+
let score = 0;
|
|
69
|
+
|
|
70
|
+
// Score based on role keywords
|
|
71
|
+
const roleKeywords = ROLE_KEYWORDS[member.role] || [];
|
|
72
|
+
score += scoreKeywords(desc, roleKeywords);
|
|
73
|
+
|
|
74
|
+
// Score based on member name appearing in description
|
|
75
|
+
if (desc.includes(name.toLowerCase())) score += 3;
|
|
76
|
+
|
|
77
|
+
// Score based on specific endpoint mentions
|
|
78
|
+
for (const ep of (member.provides || [])) {
|
|
79
|
+
const epPath = ep.split(' ').slice(1).join(' ').toLowerCase();
|
|
80
|
+
if (desc.includes(epPath)) score += 2;
|
|
81
|
+
}
|
|
82
|
+
for (const ep of (member.consumes || [])) {
|
|
83
|
+
const epPath = ep.split(' ').slice(1).join(' ').toLowerCase();
|
|
84
|
+
if (desc.includes(epPath)) score += 2;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
scores[name] = score;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Determine routing
|
|
91
|
+
const sortedMembers = Object.entries(scores)
|
|
92
|
+
.sort((a, b) => b[1] - a[1]);
|
|
93
|
+
|
|
94
|
+
const topScore = sortedMembers[0]?.[1] || 0;
|
|
95
|
+
const secondScore = sortedMembers[1]?.[1] || 0;
|
|
96
|
+
|
|
97
|
+
// Cross-repo if:
|
|
98
|
+
// 1. Cross-repo keywords are dominant
|
|
99
|
+
// 2. Two repos score similarly (within 30%)
|
|
100
|
+
// 3. No repo scored above threshold
|
|
101
|
+
const isCrossRepo =
|
|
102
|
+
crossRepoScore >= 2 ||
|
|
103
|
+
(topScore > 0 && secondScore > 0 && secondScore >= topScore * 0.7) ||
|
|
104
|
+
topScore === 0;
|
|
105
|
+
|
|
106
|
+
if (isCrossRepo && Object.keys(manifest.members).length >= 2) {
|
|
107
|
+
return {
|
|
108
|
+
type: 'cross-repo',
|
|
109
|
+
targets: sortedMembers.filter(([_, s]) => s > 0).map(([name]) => name),
|
|
110
|
+
allMembers: Object.keys(manifest.members),
|
|
111
|
+
scores,
|
|
112
|
+
crossRepoScore,
|
|
113
|
+
reason: crossRepoScore >= 2
|
|
114
|
+
? 'Cross-repo keywords detected'
|
|
115
|
+
: topScore === 0
|
|
116
|
+
? 'No clear repo match — routing to all'
|
|
117
|
+
: 'Multiple repos score similarly'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
type: 'single-repo',
|
|
123
|
+
target: sortedMembers[0]?.[0] || Object.keys(manifest.members)[0],
|
|
124
|
+
scores,
|
|
125
|
+
reason: `Best match: ${sortedMembers[0]?.[0]} (score: ${topScore})`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scoreKeywords(text, keywords) {
|
|
130
|
+
let score = 0;
|
|
131
|
+
for (const kw of keywords) {
|
|
132
|
+
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
133
|
+
if (new RegExp('\\b' + escaped + '\\b', 'i').test(text)) score++;
|
|
134
|
+
}
|
|
135
|
+
return score;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================================
|
|
139
|
+
// Single-Repo Delegation (Criterion 2)
|
|
140
|
+
// ============================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate a sub-agent delegation prompt for a single repo.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} workspaceRoot
|
|
146
|
+
* @param {string} repoName
|
|
147
|
+
* @param {string} task — task description
|
|
148
|
+
* @param {Object} manifest
|
|
149
|
+
* @returns {Object} delegation instruction
|
|
150
|
+
*/
|
|
151
|
+
function generateSingleRepoDelegation(workspaceRoot, repoName, task, manifest) {
|
|
152
|
+
const member = manifest.members[repoName];
|
|
153
|
+
if (!member) throw new Error(`Unknown repo: ${repoName}`);
|
|
154
|
+
|
|
155
|
+
const memberPath = member.path || `./${repoName}`;
|
|
156
|
+
const repoPath = path.resolve(workspaceRoot, memberPath);
|
|
157
|
+
const decisionsPath = path.join(repoPath, '.workflow', 'state', 'decisions.md');
|
|
158
|
+
|
|
159
|
+
// Read repo's decisions for context injection
|
|
160
|
+
let decisions = '';
|
|
161
|
+
try {
|
|
162
|
+
if (fs.existsSync(decisionsPath)) {
|
|
163
|
+
decisions = fs.readFileSync(decisionsPath, 'utf-8').slice(0, 3000);
|
|
164
|
+
}
|
|
165
|
+
} catch (_err) {
|
|
166
|
+
// Non-critical
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Get unread messages for this repo
|
|
170
|
+
let messages = [];
|
|
171
|
+
try {
|
|
172
|
+
const { getUnreadMessages } = require('./workspace-messages');
|
|
173
|
+
messages = getUnreadMessages(workspaceRoot, repoName);
|
|
174
|
+
} catch (_err) {
|
|
175
|
+
// Non-critical
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const messageContext = messages.length > 0
|
|
179
|
+
? `\n\nUnread messages for your repo:\n${messages.map(m => `- [${m.type}] from ${m.from}: ${m.subject}`).join('\n')}`
|
|
180
|
+
: '';
|
|
181
|
+
|
|
182
|
+
// Read relevant contracts
|
|
183
|
+
let contractContext = '';
|
|
184
|
+
try {
|
|
185
|
+
const contractsDir = path.join(workspaceRoot, '.workspace', 'contracts');
|
|
186
|
+
if (fs.existsSync(contractsDir)) {
|
|
187
|
+
const files = fs.readdirSync(contractsDir).filter(f => f.endsWith('.json') || f.endsWith('.yaml'));
|
|
188
|
+
if (files.length > 0) {
|
|
189
|
+
contractContext = '\n\nShared contracts available in .workspace/contracts/: ' + files.join(', ');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (_err) {
|
|
193
|
+
// Non-critical
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
repoName,
|
|
198
|
+
repoPath: member.path,
|
|
199
|
+
prompt: `You are working in the ${repoName} repo (${member.stack?.language || 'unknown'}/${member.stack?.framework || 'unknown'}).
|
|
200
|
+
|
|
201
|
+
Task: ${task}
|
|
202
|
+
|
|
203
|
+
Your repo's role: ${member.role}
|
|
204
|
+
${decisions ? `\nProject rules (from decisions.md):\n${decisions}` : ''}${messageContext}${contractContext}
|
|
205
|
+
|
|
206
|
+
After completing the task:
|
|
207
|
+
1. Commit your changes
|
|
208
|
+
2. If your changes affect the API contract (endpoints, request/response shapes), write a message to .workspace/messages/ notifying other repos`,
|
|
209
|
+
|
|
210
|
+
agentConfig: {
|
|
211
|
+
description: `${repoName}: ${task.substring(0, 50)}...`,
|
|
212
|
+
// The sub-agent should work within the repo directory
|
|
213
|
+
// The orchestrator (workspace manager) will read results after completion
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================
|
|
219
|
+
// Cross-Repo Delegation (Criterion 3)
|
|
220
|
+
// ============================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Generate a cross-repo execution plan.
|
|
224
|
+
* Order: contract update → provider(s) → consumer(s) → integration verify
|
|
225
|
+
*
|
|
226
|
+
* @param {string} workspaceRoot
|
|
227
|
+
* @param {string} task
|
|
228
|
+
* @param {Object} manifest
|
|
229
|
+
* @param {Object} routing — from analyzeTaskRouting()
|
|
230
|
+
* @returns {Object} execution plan with ordered steps
|
|
231
|
+
*/
|
|
232
|
+
function generateCrossRepoPlan(workspaceRoot, task, manifest, routing) {
|
|
233
|
+
const steps = [];
|
|
234
|
+
const members = manifest.members;
|
|
235
|
+
|
|
236
|
+
// Step 1: Contract update (if API changes are involved)
|
|
237
|
+
const apiKeywords = ['endpoint', 'api', 'route', 'schema', 'contract'];
|
|
238
|
+
const needsContractUpdate = apiKeywords.some(kw => task.toLowerCase().includes(kw));
|
|
239
|
+
|
|
240
|
+
if (needsContractUpdate) {
|
|
241
|
+
steps.push({
|
|
242
|
+
phase: 'contract',
|
|
243
|
+
action: 'Update shared contract in .workspace/contracts/',
|
|
244
|
+
executor: 'manager',
|
|
245
|
+
description: 'Define the API contract before implementation'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Step 2: Provider repos first (they create the API)
|
|
250
|
+
const providers = Object.entries(members)
|
|
251
|
+
.filter(([_, m]) => m.role === 'provider' || m.role === 'both')
|
|
252
|
+
.filter(([name]) => routing.targets?.includes(name) || routing.allMembers?.includes(name));
|
|
253
|
+
|
|
254
|
+
for (const [name] of providers) {
|
|
255
|
+
steps.push({
|
|
256
|
+
phase: 'provider',
|
|
257
|
+
action: `Implement provider-side changes in ${name}/`,
|
|
258
|
+
executor: name,
|
|
259
|
+
description: `${name} implements the API/backend side`,
|
|
260
|
+
delegation: generateSingleRepoDelegation(workspaceRoot, name, task, manifest)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Step 3: Consumer repos (they use the API)
|
|
265
|
+
const consumers = Object.entries(members)
|
|
266
|
+
.filter(([_, m]) => m.role === 'consumer' || m.role === 'both')
|
|
267
|
+
.filter(([name]) => routing.targets?.includes(name) || routing.allMembers?.includes(name))
|
|
268
|
+
.filter(([name]) => !providers.some(([pName]) => pName === name)); // Skip if already in providers
|
|
269
|
+
|
|
270
|
+
for (const [name] of consumers) {
|
|
271
|
+
steps.push({
|
|
272
|
+
phase: 'consumer',
|
|
273
|
+
action: `Implement consumer-side changes in ${name}/`,
|
|
274
|
+
executor: name,
|
|
275
|
+
description: `${name} implements the frontend/client side`,
|
|
276
|
+
delegation: generateSingleRepoDelegation(workspaceRoot, name, task, manifest)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Step 4: Library repos (if affected)
|
|
281
|
+
const libraries = Object.entries(members)
|
|
282
|
+
.filter(([_, m]) => m.role === 'library')
|
|
283
|
+
.filter(([name]) => routing.targets?.includes(name));
|
|
284
|
+
|
|
285
|
+
for (const [name] of libraries) {
|
|
286
|
+
// Libraries go first (before providers and consumers)
|
|
287
|
+
steps.unshift({
|
|
288
|
+
phase: 'library',
|
|
289
|
+
action: `Update shared library ${name}/`,
|
|
290
|
+
executor: name,
|
|
291
|
+
description: `${name} updates shared types/utilities`,
|
|
292
|
+
delegation: generateSingleRepoDelegation(workspaceRoot, name, task, manifest)
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Step 5: Integration verification
|
|
297
|
+
steps.push({
|
|
298
|
+
phase: 'verify',
|
|
299
|
+
action: 'Verify cross-repo integration',
|
|
300
|
+
executor: 'manager',
|
|
301
|
+
description: 'Check that provider and consumer sides work together'
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
task,
|
|
306
|
+
type: 'cross-repo',
|
|
307
|
+
totalSteps: steps.length,
|
|
308
|
+
executionOrder: steps,
|
|
309
|
+
providers: providers.map(([n]) => n),
|
|
310
|
+
consumers: consumers.map(([n]) => n),
|
|
311
|
+
libraries: libraries.map(([n]) => n)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ============================================================
|
|
316
|
+
// Parallel Investigation (Criterion 4)
|
|
317
|
+
// ============================================================
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Generate parallel investigation instructions for bug reports.
|
|
321
|
+
* Spawns one investigator per potentially affected repo.
|
|
322
|
+
*
|
|
323
|
+
* @param {string} workspaceRoot
|
|
324
|
+
* @param {string} bugDescription
|
|
325
|
+
* @param {Object} manifest
|
|
326
|
+
* @returns {Object} investigation plan
|
|
327
|
+
*/
|
|
328
|
+
function generateParallelInvestigation(workspaceRoot, bugDescription, manifest) {
|
|
329
|
+
const investigators = [];
|
|
330
|
+
|
|
331
|
+
for (const [name, member] of Object.entries(manifest.members)) {
|
|
332
|
+
const repoPath = path.resolve(workspaceRoot, member.path);
|
|
333
|
+
|
|
334
|
+
investigators.push({
|
|
335
|
+
repoName: name,
|
|
336
|
+
repoPath: member.path,
|
|
337
|
+
role: member.role,
|
|
338
|
+
prompt: `You are investigating a bug in the ${name} repo (${member.role}).
|
|
339
|
+
|
|
340
|
+
Bug report: ${bugDescription}
|
|
341
|
+
|
|
342
|
+
Your job:
|
|
343
|
+
1. Check if the issue originates from YOUR repo
|
|
344
|
+
2. Check recent changes (git log) that might have caused this
|
|
345
|
+
3. Check relevant API endpoints, components, or services
|
|
346
|
+
4. Report your findings clearly:
|
|
347
|
+
- Is the issue on YOUR side? (yes/no/maybe)
|
|
348
|
+
- What did you find?
|
|
349
|
+
- If yes: what's the fix?
|
|
350
|
+
- If no: what should the OTHER repo(s) check?
|
|
351
|
+
|
|
352
|
+
Be specific about file names, line numbers, and error messages.`,
|
|
353
|
+
agentConfig: {
|
|
354
|
+
description: `Investigate: ${name} — ${bugDescription.substring(0, 40)}...`,
|
|
355
|
+
model: 'sonnet' // Use cheaper model for investigation
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
type: 'parallel-investigation',
|
|
362
|
+
bugDescription,
|
|
363
|
+
investigators,
|
|
364
|
+
synthesisPrompt: `Multiple investigators checked their repos. Synthesize their findings:
|
|
365
|
+
- Which repo is the root cause?
|
|
366
|
+
- What's the fix?
|
|
367
|
+
- Are there improvements needed in other repos?
|
|
368
|
+
- Create tasks in the appropriate repo(s) ready.json`
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ============================================================
|
|
373
|
+
// Task Decomposition (Criterion 5)
|
|
374
|
+
// ============================================================
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Decompose a workspace-level task into repo-level tasks.
|
|
378
|
+
* Creates entries in each affected repo's ready.json.
|
|
379
|
+
*
|
|
380
|
+
* @param {string} workspaceRoot
|
|
381
|
+
* @param {Object} workspaceTask — { title, description, criteria }
|
|
382
|
+
* @param {Object} plan — from generateCrossRepoPlan()
|
|
383
|
+
* @returns {Array<Object>} created repo-level tasks
|
|
384
|
+
*/
|
|
385
|
+
function decomposeToRepoTasks(workspaceRoot, workspaceTask, plan) {
|
|
386
|
+
const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
|
|
387
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
388
|
+
const createdTasks = [];
|
|
389
|
+
|
|
390
|
+
for (const step of plan.executionOrder) {
|
|
391
|
+
if (step.executor === 'manager') continue; // Manager steps don't create repo tasks
|
|
392
|
+
|
|
393
|
+
const memberConfig = config.members[step.executor];
|
|
394
|
+
if (!memberConfig) continue;
|
|
395
|
+
|
|
396
|
+
const memberPath = path.resolve(workspaceRoot, memberConfig.path);
|
|
397
|
+
const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
|
|
398
|
+
|
|
399
|
+
if (!fs.existsSync(readyPath)) continue;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
|
|
403
|
+
const taskId = 'wf-' + crypto.randomBytes(4).toString('hex');
|
|
404
|
+
|
|
405
|
+
const repoTask = {
|
|
406
|
+
id: taskId,
|
|
407
|
+
title: `[Workspace] ${workspaceTask.title} — ${step.executor} (${step.phase})`,
|
|
408
|
+
type: workspaceTask.type || 'feature',
|
|
409
|
+
level: 'L2',
|
|
410
|
+
priority: 'P0',
|
|
411
|
+
source: `workspace:${workspaceTask.id || 'direct'}`,
|
|
412
|
+
status: 'ready',
|
|
413
|
+
description: step.description + '\n\n' + (workspaceTask.description || ''),
|
|
414
|
+
createdAt: new Date().toISOString()
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
if (!ready.ready) ready.ready = [];
|
|
418
|
+
ready.ready.push(repoTask);
|
|
419
|
+
ready.lastUpdated = new Date().toISOString();
|
|
420
|
+
fs.writeFileSync(readyPath, JSON.stringify(ready, null, 2));
|
|
421
|
+
|
|
422
|
+
createdTasks.push({ repo: step.executor, phase: step.phase, task: repoTask });
|
|
423
|
+
} catch (_err) {
|
|
424
|
+
// Non-critical — log and continue
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return createdTasks;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ============================================================
|
|
432
|
+
// Dependency-Aware Ordering (Criterion 6)
|
|
433
|
+
// ============================================================
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Determine execution order respecting dependencies:
|
|
437
|
+
* library → provider → consumer
|
|
438
|
+
*
|
|
439
|
+
* @param {Object} manifest
|
|
440
|
+
* @param {string[]} targetRepos — repos involved in the task
|
|
441
|
+
* @returns {Array<{ name: string, phase: string, order: number }>}
|
|
442
|
+
*/
|
|
443
|
+
function getExecutionOrder(manifest, targetRepos) {
|
|
444
|
+
const order = [];
|
|
445
|
+
const phaseOrder = { library: 0, provider: 1, both: 1, consumer: 2, standalone: 3 };
|
|
446
|
+
|
|
447
|
+
for (const name of targetRepos) {
|
|
448
|
+
const member = manifest.members[name];
|
|
449
|
+
if (!member) continue;
|
|
450
|
+
order.push({
|
|
451
|
+
name,
|
|
452
|
+
role: member.role,
|
|
453
|
+
phase: member.role,
|
|
454
|
+
order: phaseOrder[member.role] ?? 3
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Sort by phase order (library first, then provider, then consumer)
|
|
459
|
+
order.sort((a, b) => a.order - b.order);
|
|
460
|
+
|
|
461
|
+
return order;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ============================================================
|
|
465
|
+
// Exports
|
|
466
|
+
// ============================================================
|
|
467
|
+
|
|
468
|
+
module.exports = {
|
|
469
|
+
// Routing
|
|
470
|
+
analyzeTaskRouting,
|
|
471
|
+
ROLE_KEYWORDS,
|
|
472
|
+
|
|
473
|
+
// Delegation
|
|
474
|
+
generateSingleRepoDelegation,
|
|
475
|
+
generateCrossRepoPlan,
|
|
476
|
+
|
|
477
|
+
// Investigation
|
|
478
|
+
generateParallelInvestigation,
|
|
479
|
+
|
|
480
|
+
// Decomposition
|
|
481
|
+
decomposeToRepoTasks,
|
|
482
|
+
|
|
483
|
+
// Ordering
|
|
484
|
+
getExecutionOrder
|
|
485
|
+
};
|