@ijfw/memory-server 1.3.0 → 1.4.0

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 (64) hide show
  1. package/fixtures/team/book.json +47 -0
  2. package/fixtures/team/business.json +47 -0
  3. package/fixtures/team/content.json +47 -0
  4. package/fixtures/team/design.json +47 -0
  5. package/fixtures/team/mixed.json +59 -0
  6. package/fixtures/team/research.json +47 -0
  7. package/fixtures/team/software.json +47 -0
  8. package/package.json +1 -9
  9. package/src/active-extension-writer.js +116 -0
  10. package/src/blackboard.js +360 -0
  11. package/src/cli-run.js +91 -0
  12. package/src/codex-agents.js +177 -0
  13. package/src/compute/extract.js +3 -0
  14. package/src/compute/fts5.js +4 -4
  15. package/src/compute/graph-lock.js +0 -2
  16. package/src/compute/migrations/003-tier-semantic.js +3 -3
  17. package/src/compute/runner.js +44 -15
  18. package/src/compute/schema.sql +1 -1
  19. package/src/cross-orchestrator-cli.js +974 -13
  20. package/src/cross-orchestrator.js +9 -1
  21. package/src/dashboard-client.html +144 -1
  22. package/src/dashboard-server.js +75 -2
  23. package/src/design-intelligence.js +721 -0
  24. package/src/dispatch/colon-syntax.js +31 -3
  25. package/src/dispatch/domain-manifest.js +251 -0
  26. package/src/dispatch/extension.js +404 -0
  27. package/src/dispatch/override.js +221 -0
  28. package/src/dispatch-planner.js +1 -0
  29. package/src/dream/runner.mjs +3 -3
  30. package/src/extension-installer.js +1230 -0
  31. package/src/extension-manifest-schema.js +301 -0
  32. package/src/extension-signer.js +740 -0
  33. package/src/gate-result-formatter.js +95 -0
  34. package/src/gate-result-schema.js +274 -0
  35. package/src/gate-result.js +195 -0
  36. package/src/intent-router.js +2 -0
  37. package/src/lib/npm-view.js +1 -0
  38. package/src/memory/fts5.js +3 -3
  39. package/src/memory/migrations/002-tier-semantic.js +2 -2
  40. package/src/memory/staleness.js +1 -1
  41. package/src/memory/tier-promotion.js +6 -6
  42. package/src/memory/tokenize.js +1 -1
  43. package/src/memory-feedback.js +188 -0
  44. package/src/override-manifest-schema.js +146 -0
  45. package/src/override-resolver.js +699 -0
  46. package/src/override-use-registry.js +307 -0
  47. package/src/overrides/presets/academic.md +101 -0
  48. package/src/overrides/presets/book.md +87 -0
  49. package/src/overrides/presets/campaign.md +95 -0
  50. package/src/overrides/presets/screenplay.md +99 -0
  51. package/src/recovery/checkpoint.js +191 -0
  52. package/src/redactor.js +2 -0
  53. package/src/runtime-mediator.js +178 -0
  54. package/src/sandbox.js +17 -3
  55. package/src/server.js +94 -2
  56. package/src/swarm/dispatch-prompt.js +154 -0
  57. package/src/swarm/planner.js +399 -0
  58. package/src/swarm/review.js +136 -0
  59. package/src/swarm/worktree.js +239 -0
  60. package/src/team/generator.js +119 -0
  61. package/src/team/schemas.js +341 -0
  62. package/src/trident/dispatch.js +47 -0
  63. package/src/update-check.js +1 -1
  64. package/src/vectors.js +7 -8
@@ -0,0 +1,154 @@
1
+ // Prompt scaffolding for prepared swarm tasks.
2
+ //
3
+ // This helper is intentionally pure and dependency-free. It renders the task
4
+ // record already prepared on the blackboard; command integration lives in the
5
+ // CLI layer.
6
+
7
+ export function renderSwarmDispatchPrompt(task, options = {}) {
8
+ const normalized = normalizeTask(task);
9
+ const projectRoot = stringValue(options.projectRoot);
10
+ const lead = stringValue(options.lead) || 'main agent';
11
+ const codex = Boolean(options.codex);
12
+
13
+ const lines = [
14
+ `# IJFW Swarm Worker Prompt: ${normalized.id}`,
15
+ '',
16
+ 'You are working inside an IJFW prepared swarm task. Stay inside the task scope, coordinate through the blackboard, and report blockers early.',
17
+ '',
18
+ '## Task',
19
+ field('Task id', normalized.id),
20
+ field('Title', normalized.title),
21
+ field('Owner', normalized.owner),
22
+ field('Status', normalized.status),
23
+ field('Wave', normalized.wave_id),
24
+ field('Wave mode', normalized.wave_mode),
25
+ ];
26
+
27
+ if (projectRoot) lines.push(field('Project root', projectRoot));
28
+
29
+ if (codex) {
30
+ lines.push(
31
+ '',
32
+ '## Codex Dispatch',
33
+ `- Preferred custom agent: ${codexAgentName(normalized.owner)}`,
34
+ '- If this Codex runtime cannot invoke named `.codex/agents/*.toml` agents, spawn a generic `worker` for implementation/review tasks or `explorer` for read-only discovery, then paste this prompt into it.',
35
+ '- Keep `fork_context` false unless the worker needs the full parent conversation; pass only the task scope and relevant files when possible.',
36
+ );
37
+ }
38
+
39
+ lines.push(
40
+ '',
41
+ '## Artifact Scope',
42
+ listBlock('Artifact ids', normalized.artifact_ids),
43
+ listBlock('Allowed paths', normalized.paths, 'No path scope was provided. Coordinate by artifact id, references, and the blackboard before editing project artifacts.'),
44
+ listBlock('Allowed refs', normalized.refs, 'No external or internal refs were provided.'),
45
+ );
46
+
47
+ lines.push(
48
+ '',
49
+ '## Dependencies',
50
+ listBlock('Depends on', normalized.depends_on, 'No prepared task dependencies.'),
51
+ );
52
+
53
+ if (normalized.blocked_by.length) {
54
+ lines.push(listBlock('Blocked by', normalized.blocked_by.map(formatBlocker)));
55
+ }
56
+
57
+ if (normalized.blocker) {
58
+ lines.push(listBlock('Current blocker', [normalized.blocker]));
59
+ }
60
+
61
+ lines.push(
62
+ '',
63
+ '## Verification',
64
+ listBlock('Required checks or review gates', normalized.verification, 'No verification was specified. Define an appropriate check for this artifact type and include the result in your completion note.'),
65
+ );
66
+
67
+ if (normalized.review_criteria.length) {
68
+ lines.push(
69
+ '',
70
+ '## Review Criteria',
71
+ listBlock('Criteria', normalized.review_criteria),
72
+ );
73
+ }
74
+
75
+ lines.push(
76
+ '',
77
+ '## Blackboard Commands',
78
+ `- Start when you begin: \`ijfw swarm start ${normalized.id}\``,
79
+ `- Complete when finished: \`ijfw swarm complete ${normalized.id} "<summary and verification result>"\``,
80
+ `- Block if stuck: \`ijfw swarm block ${normalized.id} "<blocker and requested help>"\``,
81
+ );
82
+
83
+ if (normalized.status === 'blocked') {
84
+ lines.push('', 'This task is currently blocked. Do not start implementation/review work until the blocker is resolved or the lead marks the task ready.');
85
+ } else if (normalized.status !== 'ready') {
86
+ lines.push('', `This task is currently \`${normalized.status}\`. Confirm with ${lead} before changing its lifecycle state.`);
87
+ }
88
+
89
+ lines.push(
90
+ '',
91
+ '## Operating Constraints',
92
+ '- Do not revert user changes or edits by other agents.',
93
+ '- Touch only the allowed artifact scope unless the lead expands it.',
94
+ '- Preserve project-agnostic output: this task may be code, design, writing, research, business, operations, or another artifact type.',
95
+ '- Keep handoff notes concise and include changed artifacts, verification performed, and any residual risk.',
96
+ );
97
+
98
+ return lines.filter((line) => line !== null && line !== undefined).join('\n').trimEnd();
99
+ }
100
+
101
+ function normalizeTask(task) {
102
+ const source = task && typeof task === 'object' ? task : {};
103
+ return {
104
+ id: stringValue(source.id) || 'unknown-task',
105
+ title: stringValue(source.title) || 'Untitled prepared swarm task',
106
+ owner: stringValue(source.owner) || 'unassigned',
107
+ status: stringValue(source.status) || 'unknown',
108
+ wave_id: stringValue(source.wave_id) || 'unknown',
109
+ wave_mode: stringValue(source.wave_mode) || 'unknown',
110
+ artifact_ids: stringArray(source.artifact_ids),
111
+ paths: stringArray(source.paths),
112
+ refs: stringArray(source.refs),
113
+ depends_on: stringArray(source.depends_on),
114
+ verification: stringArray(source.verification),
115
+ review_criteria: stringArray(source.review_criteria),
116
+ blocked_by: Array.isArray(source.blocked_by) ? source.blocked_by : [],
117
+ blocker: stringValue(source.blocker),
118
+ };
119
+ }
120
+
121
+ function field(label, value) {
122
+ return `- ${label}: ${value || 'none'}`;
123
+ }
124
+
125
+ function listBlock(label, values, empty = 'None.') {
126
+ const items = stringArray(values);
127
+ if (!items.length) return `- ${label}: ${empty}`;
128
+ return [`- ${label}:`, ...items.map((value) => ` - ${value}`)].join('\n');
129
+ }
130
+
131
+ function formatBlocker(blocker) {
132
+ if (!blocker || typeof blocker !== 'object') return stringValue(blocker) || 'unknown blocker';
133
+ const parts = [];
134
+ if (blocker.task_id) parts.push(`task ${blocker.task_id}`);
135
+ if (blocker.status) parts.push(`status ${blocker.status}`);
136
+ if (blocker.artifact_id) parts.push(`artifact ${blocker.artifact_id}`);
137
+ if (blocker.agent) parts.push(`agent ${blocker.agent}`);
138
+ if (Array.isArray(blocker.paths) && blocker.paths.length) parts.push(`paths ${blocker.paths.join(', ')}`);
139
+ return parts.join('; ') || JSON.stringify(blocker);
140
+ }
141
+
142
+ function stringArray(value) {
143
+ if (!Array.isArray(value)) return [];
144
+ return value.map((item) => stringValue(item)).filter(Boolean);
145
+ }
146
+
147
+ function stringValue(value) {
148
+ if (value === null || value === undefined) return '';
149
+ return String(value).trim();
150
+ }
151
+
152
+ function codexAgentName(value) {
153
+ return stringValue(value).toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '') || 'ijfw_agent';
154
+ }
@@ -0,0 +1,399 @@
1
+ // Artifact-aware swarm planner.
2
+ //
3
+ // Reads Team Assembly 2.0 charter/workflow plus blackboard claims and returns
4
+ // an execution plan. This module is intentionally read-only: it explains what
5
+ // can run, what is blocked, and why. Execution/worktrees come later.
6
+
7
+ import { readTeamAssembly } from '../team/generator.js';
8
+ import {
9
+ addBlackboardNote,
10
+ appendBlackboardEvent,
11
+ blackboardStatus,
12
+ claimArtifact,
13
+ listBlackboardTasks,
14
+ releaseClaim,
15
+ updateBlackboardTask,
16
+ writeBlackboardTasks,
17
+ } from '../blackboard.js';
18
+ import { deriveReviewTasks } from './review.js';
19
+
20
+ export function buildSwarmPlan(projectRoot = process.cwd(), options = {}) {
21
+ const team = options.team || readTeamAssembly(projectRoot);
22
+ if (!team.ok) {
23
+ return {
24
+ ok: false,
25
+ error: 'missing-team-assembly',
26
+ message: 'No complete team assembly found. Run: ijfw team init',
27
+ validation: team.validation,
28
+ };
29
+ }
30
+
31
+ const blackboard = options.blackboard || blackboardStatus(projectRoot);
32
+ const workflow = team.workflow;
33
+ const charter = team.charter;
34
+ const artifactsById = new Map(workflow.artifacts.map((artifact) => [artifact.id, artifact]));
35
+ const rolesByName = new Map(charter.roles.map((role) => [role.name, role]));
36
+ const activeClaims = blackboard.claims.active_items || [];
37
+
38
+ const waves = workflow.waves.map((wave) => {
39
+ const tasks = wave.artifact_ids.map((artifactId) => {
40
+ const artifact = artifactsById.get(artifactId);
41
+ return buildTask(artifactId, artifact, rolesByName, activeClaims);
42
+ });
43
+ const blocked = tasks.filter((task) => task.blocked);
44
+ const mode = normalizeMode(wave.mode, tasks);
45
+ return {
46
+ id: wave.id,
47
+ requested_mode: wave.mode,
48
+ mode,
49
+ artifact_ids: wave.artifact_ids.slice(),
50
+ tasks,
51
+ blocked: blocked.length > 0,
52
+ reason: explainWave(wave, tasks, mode),
53
+ };
54
+ });
55
+
56
+ return {
57
+ ok: true,
58
+ team_name: charter.team_name,
59
+ project_archetypes: workflow.project_archetypes,
60
+ waves,
61
+ summary: summarizePlan(waves),
62
+ };
63
+ }
64
+
65
+ export function swarmPlanSummary(plan) {
66
+ if (!plan.ok) return plan.message || plan.error;
67
+ const lines = [
68
+ `Swarm plan for ${plan.team_name}`,
69
+ `Archetypes: ${plan.project_archetypes.join(', ')}`,
70
+ plan.summary,
71
+ ];
72
+ for (const wave of plan.waves) {
73
+ lines.push(`Wave ${wave.id}: ${wave.mode}${wave.blocked ? ' (blocked)' : ''} -- ${wave.reason}`);
74
+ for (const task of wave.tasks) {
75
+ const state = task.blocked ? `blocked by ${task.blocked_by.map((c) => `${c.artifact_id}:${c.agent}`).join(', ')}` : 'ready';
76
+ lines.push(` ${task.artifact_id} -> ${task.owner} [${state}]`);
77
+ if (task.verification.length) lines.push(` verify: ${task.verification.join(' | ')}`);
78
+ }
79
+ }
80
+ return lines.join('\n');
81
+ }
82
+
83
+ export function prepareSwarmTasks(projectRoot = process.cwd(), options = {}) {
84
+ const plan = options.plan || buildSwarmPlan(projectRoot, options);
85
+ if (!plan.ok) return { ok: false, error: plan.error, message: plan.message, plan };
86
+
87
+ const tasks = [];
88
+ for (const wave of plan.waves) {
89
+ for (const task of wave.tasks) {
90
+ tasks.push({
91
+ id: `swarm:${wave.id}:${task.artifact_id}`,
92
+ title: `${task.owner}: ${task.artifact_id}`,
93
+ status: task.blocked ? 'blocked' : 'ready',
94
+ wave_id: wave.id,
95
+ wave_mode: wave.mode,
96
+ artifact_ids: [task.artifact_id],
97
+ owner: task.owner,
98
+ reviewers: task.reviewers,
99
+ paths: task.paths,
100
+ refs: task.refs,
101
+ depends_on: task.depends_on.map((id) => `swarm:${findWaveForArtifact(plan, id) || wave.id}:${id}`),
102
+ verification: task.verification,
103
+ blocked_by: task.blocked_by.map((claim) => ({
104
+ artifact_id: claim.artifact_id,
105
+ agent: claim.agent,
106
+ paths: claim.paths || [],
107
+ })),
108
+ });
109
+ }
110
+ }
111
+
112
+ if (options.includeReviews) {
113
+ tasks.push(...deriveReviewTasks({
114
+ plan,
115
+ tasks,
116
+ charter: (options.team || readTeamAssembly(projectRoot)).charter,
117
+ }));
118
+ }
119
+
120
+ const write = writeBlackboardTasks(projectRoot, tasks, { replace: options.replace !== false });
121
+ appendBlackboardEvent(projectRoot, {
122
+ type: 'swarm.prepared',
123
+ actor: 'ijfw',
124
+ message: `Prepared ${tasks.length} swarm task(s)`,
125
+ data: { includeReviews: Boolean(options.includeReviews), replace: options.replace !== false },
126
+ });
127
+ return {
128
+ ok: write.ok,
129
+ written: write.written || 0,
130
+ total: write.total || 0,
131
+ tasks,
132
+ plan,
133
+ error: write.error,
134
+ };
135
+ }
136
+
137
+ export function listSwarmTasks(projectRoot = process.cwd()) {
138
+ const listed = listBlackboardTasks(projectRoot);
139
+ const tasks = (listed.tasks || []).filter((task) => String(task.id || '').startsWith('swarm:') || String(task.id || '').startsWith('review:'));
140
+ return { ok: listed.ok, tasks, error: listed.error };
141
+ }
142
+
143
+ export function startSwarmTask(projectRoot, taskId, options = {}) {
144
+ const task = findTask(projectRoot, taskId);
145
+ if (!task.ok) return task;
146
+ if (task.task.status !== 'ready') return { ok: false, error: 'task-not-ready', task: task.task };
147
+ const blockedDependency = firstBlockedDependency(projectRoot, task.task);
148
+ if (blockedDependency) return { ok: false, error: 'dependency-not-done', dependency: blockedDependency, task: task.task };
149
+
150
+ const owner = options.owner || task.task.owner;
151
+ const claimResults = [];
152
+ for (const artifactId of task.task.artifact_ids || []) {
153
+ const result = claimArtifact(projectRoot, {
154
+ artifact: artifactId,
155
+ owner,
156
+ paths: task.task.paths || [],
157
+ note: `swarm task ${taskId}`,
158
+ });
159
+ if (!result.ok) return { ok: false, error: 'claim-failed', claim: result, task: task.task };
160
+ claimResults.push(result.claim);
161
+ }
162
+
163
+ const updated = updateBlackboardTask(projectRoot, taskId, {
164
+ status: 'in_progress',
165
+ started_at: new Date().toISOString(),
166
+ active_owner: owner,
167
+ });
168
+ if (updated.ok) {
169
+ appendBlackboardEvent(projectRoot, {
170
+ type: 'task.started',
171
+ actor: owner,
172
+ task_id: taskId,
173
+ artifact_ids: task.task.artifact_ids || [],
174
+ message: `Started ${taskId}`,
175
+ });
176
+ }
177
+ return { ok: updated.ok, task: updated.task, claims: claimResults, error: updated.error };
178
+ }
179
+
180
+ export function completeSwarmTask(projectRoot, taskId, options = {}) {
181
+ const task = findTask(projectRoot, taskId);
182
+ if (!task.ok) return task;
183
+ if (!['in_progress', 'review'].includes(task.task.status)) {
184
+ return { ok: false, error: 'task-not-in-progress', task: task.task };
185
+ }
186
+ const owner = options.owner || task.task.active_owner || task.task.owner;
187
+ const releases = [];
188
+ for (const artifactId of task.task.artifact_ids || []) {
189
+ releases.push(releaseClaim(projectRoot, { artifact: artifactId, owner }));
190
+ }
191
+ const updated = updateBlackboardTask(projectRoot, taskId, {
192
+ status: 'done',
193
+ completed_at: new Date().toISOString(),
194
+ completion_note: options.message || undefined,
195
+ });
196
+ if (updated.ok) {
197
+ appendBlackboardEvent(projectRoot, {
198
+ type: 'task.completed',
199
+ actor: owner,
200
+ task_id: taskId,
201
+ artifact_ids: task.task.artifact_ids || [],
202
+ message: options.message || `Completed ${taskId}`,
203
+ });
204
+ }
205
+ return { ok: updated.ok, task: updated.task, releases, error: updated.error };
206
+ }
207
+
208
+ export function blockSwarmTask(projectRoot, taskId, options = {}) {
209
+ const task = findTask(projectRoot, taskId);
210
+ if (!task.ok) return task;
211
+ const message = String(options.message || '').trim();
212
+ if (!message) return { ok: false, error: 'message-required', task: task.task };
213
+ const updated = updateBlackboardTask(projectRoot, taskId, {
214
+ status: 'blocked',
215
+ blocked_at: new Date().toISOString(),
216
+ blocker: message,
217
+ });
218
+ if (updated.ok) {
219
+ appendBlackboardEvent(projectRoot, {
220
+ type: 'task.blocked',
221
+ actor: options.owner || task.task.owner || 'swarm',
222
+ task_id: taskId,
223
+ artifact_ids: task.task.artifact_ids || [],
224
+ message,
225
+ });
226
+ }
227
+ addBlackboardNote(projectRoot, {
228
+ kind: 'blocker',
229
+ author: options.owner || task.task.owner || 'swarm',
230
+ artifact: (task.task.artifact_ids || []).join(','),
231
+ message: `${taskId}: ${message}`,
232
+ });
233
+ return { ok: updated.ok, task: updated.task, error: updated.error };
234
+ }
235
+
236
+ export function readySwarmTask(projectRoot, taskId) {
237
+ const task = findTask(projectRoot, taskId);
238
+ if (!task.ok) return task;
239
+ if (task.task.status !== 'blocked') return { ok: false, error: 'task-not-blocked', task: task.task };
240
+ const updated = updateBlackboardTask(projectRoot, taskId, {
241
+ status: 'ready',
242
+ blocker: null,
243
+ unblocked_at: new Date().toISOString(),
244
+ });
245
+ if (updated.ok) {
246
+ appendBlackboardEvent(projectRoot, {
247
+ type: 'task.ready',
248
+ actor: 'ijfw',
249
+ task_id: taskId,
250
+ artifact_ids: task.task.artifact_ids || [],
251
+ message: `Ready ${taskId}`,
252
+ });
253
+ }
254
+ return { ok: updated.ok, task: updated.task, error: updated.error };
255
+ }
256
+
257
+ function buildTask(artifactId, artifact, rolesByName, activeClaims) {
258
+ if (!artifact) {
259
+ return {
260
+ artifact_id: artifactId,
261
+ owner: null,
262
+ reviewers: [],
263
+ paths: [],
264
+ refs: [],
265
+ verification: [],
266
+ blocked: true,
267
+ blocked_by: [],
268
+ reason: 'unknown-artifact',
269
+ };
270
+ }
271
+ const owner = rolesByName.get(artifact.owner);
272
+ const claimConflicts = activeClaims.filter((claim) => claimBlocksArtifact(claim, artifact));
273
+ const allowed_paths = artifact.paths || [];
274
+ const refs = artifact.refs || [];
275
+ return {
276
+ artifact_id: artifact.id,
277
+ artifact_type: artifact.type,
278
+ owner: artifact.owner,
279
+ owner_role_type: owner?.role_type || null,
280
+ reviewers: artifact.reviewers || [],
281
+ paths: allowed_paths,
282
+ refs,
283
+ verification: artifact.verification || [],
284
+ depends_on: artifact.depends_on || [],
285
+ claim_required: owner?.coordination?.claim_required !== false,
286
+ blocked: claimConflicts.length > 0,
287
+ blocked_by: claimConflicts,
288
+ reason: claimConflicts.length ? 'active-claim-conflict' : 'ready',
289
+ };
290
+ }
291
+
292
+ function normalizeMode(requested, tasks) {
293
+ if (tasks.some((task) => task.blocked)) return 'blocked';
294
+ if (requested === 'review') return 'review';
295
+ if (requested === 'parallel' && tasks.length > 1 && !tasksOverlap(tasks)) return 'parallel';
296
+ if (requested === 'parallel' && tasks.length <= 1) return 'parallel';
297
+ return 'sequential';
298
+ }
299
+
300
+ function explainWave(wave, tasks, mode) {
301
+ if (mode === 'blocked') return 'one or more artifacts already have active blackboard claims';
302
+ if (mode === 'review') return 'review wave from workflow manifest';
303
+ if (mode === 'parallel') return 'artifacts have no dependency or path overlap inside this wave';
304
+ if (wave.mode === 'parallel' && tasksOverlap(tasks)) return 'requested parallel, but artifact paths overlap';
305
+ return 'workflow requested sequential execution or contains dependent artifacts';
306
+ }
307
+
308
+ function summarizePlan(waves) {
309
+ const total = waves.reduce((n, wave) => n + wave.tasks.length, 0);
310
+ const blocked = waves.reduce((n, wave) => n + wave.tasks.filter((task) => task.blocked).length, 0);
311
+ const parallel = waves.filter((wave) => wave.mode === 'parallel').length;
312
+ const review = waves.filter((wave) => wave.mode === 'review').length;
313
+ return `${waves.length} wave(s), ${total} artifact task(s), ${parallel} parallel wave(s), ${review} review wave(s), ${blocked} blocked task(s).`;
314
+ }
315
+
316
+ function findWaveForArtifact(plan, artifactId) {
317
+ for (const wave of plan.waves) {
318
+ if (wave.artifact_ids.includes(artifactId)) return wave.id;
319
+ }
320
+ return null;
321
+ }
322
+
323
+ function findTask(projectRoot, taskId) {
324
+ const listed = listSwarmTasks(projectRoot);
325
+ const task = (listed.tasks || []).find((item) => item.id === taskId);
326
+ if (!task) return { ok: false, error: 'task-not-found' };
327
+ return { ok: true, task };
328
+ }
329
+
330
+ function firstBlockedDependency(projectRoot, task) {
331
+ const listed = listSwarmTasks(projectRoot);
332
+ const byId = new Map((listed.tasks || []).map((item) => [item.id, item]));
333
+ for (const depId of task.depends_on || []) {
334
+ const dep = byId.get(depId);
335
+ if (dep && dep.status !== 'done') return dep;
336
+ }
337
+ return null;
338
+ }
339
+
340
+ function tasksOverlap(tasks) {
341
+ for (let i = 0; i < tasks.length; i++) {
342
+ for (let j = i + 1; j < tasks.length; j++) {
343
+ if (pathsOverlap(tasks[i].paths, tasks[j].paths)) return true;
344
+ if ((tasks[i].depends_on || []).includes(tasks[j].artifact_id)) return true;
345
+ if ((tasks[j].depends_on || []).includes(tasks[i].artifact_id)) return true;
346
+ }
347
+ }
348
+ return false;
349
+ }
350
+
351
+ function claimBlocksArtifact(claim, artifact) {
352
+ if (claim.agent === artifact.owner) return false;
353
+ if (claim.artifact_id === artifact.id) return true;
354
+ return pathsOverlap(claim.paths || [], artifact.paths || []);
355
+ }
356
+
357
+ function pathsOverlap(a, b) {
358
+ if (!a?.length || !b?.length) return false;
359
+ for (const left of a) {
360
+ for (const right of b) {
361
+ if (left === right) return true;
362
+ const lp = prefixBeforeGlob(left);
363
+ const rp = prefixBeforeGlob(right);
364
+ if (lp && right.startsWith(lp)) return true;
365
+ if (rp && left.startsWith(rp)) return true;
366
+ if (globCouldMatch(left, right) || globCouldMatch(right, left)) return true;
367
+ }
368
+ }
369
+ return false;
370
+ }
371
+
372
+ function prefixBeforeGlob(pattern) {
373
+ const idx = pattern.search(/[*?[\]{}]/);
374
+ return idx === -1 ? pattern : pattern.slice(0, idx);
375
+ }
376
+
377
+ function globCouldMatch(glob, literal) {
378
+ if (!/[*?]/.test(glob)) return false;
379
+ const re = new RegExp(`^${globToRegex(glob)}$`);
380
+ return re.test(literal);
381
+ }
382
+
383
+ function globToRegex(glob) {
384
+ let out = '';
385
+ for (let i = 0; i < glob.length; i++) {
386
+ const c = glob[i];
387
+ if (c === '*') {
388
+ if (glob[i + 1] === '*') { out += '.*'; i++; }
389
+ else out += '[^/]*';
390
+ } else if (c === '?') {
391
+ out += '[^/]';
392
+ } else if ('.+^$|()[]{}\\'.includes(c)) {
393
+ out += `\\${c}`;
394
+ } else {
395
+ out += c;
396
+ }
397
+ }
398
+ return out;
399
+ }
@@ -0,0 +1,136 @@
1
+ // Review task materialization for swarm lifecycle.
2
+ //
3
+ // This sidecar is dependency-free and intentionally does not write to the
4
+ // blackboard. CLI integration can decide when to persist these task records.
5
+
6
+ const DONE_STATUSES = new Set(['done']);
7
+
8
+ export function deriveReviewTasks(source = {}, options = {}) {
9
+ const normalized = normalizeSource(source, options);
10
+ const tasks = [];
11
+
12
+ for (const wave of normalized.waves) {
13
+ for (const artifactId of wave.artifact_ids || []) {
14
+ const artifact = normalized.artifactsById.get(artifactId);
15
+ if (!artifact) continue;
16
+
17
+ const reviewers = Array.isArray(artifact.reviewers) ? artifact.reviewers : [];
18
+ for (const reviewer of reviewers) {
19
+ const implementationTaskId = swarmTaskId(wave.id, artifact.id);
20
+ const implementationTask = normalized.implementationTasksById.get(implementationTaskId);
21
+ const dependencyKnown = implementationTask != null;
22
+ const dependencyDone = !dependencyKnown || DONE_STATUSES.has(implementationTask.status);
23
+ const reviewCriteria = criteriaForReviewer(normalized.rolesByName.get(reviewer), artifact);
24
+
25
+ tasks.push({
26
+ id: `review:${wave.id}:${artifact.id}:${reviewer}`,
27
+ title: `${reviewer}: review ${artifact.id}`,
28
+ status: dependencyDone ? 'ready' : 'blocked',
29
+ wave_id: wave.id,
30
+ wave_mode: 'review',
31
+ artifact_ids: [artifact.id],
32
+ owner: reviewer,
33
+ reviewers: [],
34
+ paths: artifact.paths || [],
35
+ refs: artifact.refs || [],
36
+ depends_on: [implementationTaskId],
37
+ verification: artifact.verification || [],
38
+ review_criteria: reviewCriteria,
39
+ blocked_by: dependencyDone ? [] : [{
40
+ task_id: implementationTaskId,
41
+ status: implementationTask.status,
42
+ }],
43
+ });
44
+ }
45
+ }
46
+ }
47
+
48
+ return tasks;
49
+ }
50
+
51
+ function normalizeSource(source, options) {
52
+ const sourceTasks = Array.isArray(source) ? source : source.tasks;
53
+ const plan = source.plan || options.plan || (Array.isArray(source.waves) ? source : null);
54
+ const implementationTasks = sourceTasks || options.tasks || [];
55
+ const workflow = source.workflow || options.workflow || planToWorkflow(plan) || tasksToWorkflow(implementationTasks);
56
+ const charter = source.charter || options.charter || null;
57
+
58
+ return {
59
+ waves: workflow?.waves || [],
60
+ artifactsById: new Map((workflow?.artifacts || []).map((artifact) => [artifact.id, artifact])),
61
+ rolesByName: new Map((charter?.roles || []).map((role) => [role.name, role])),
62
+ implementationTasksById: new Map(implementationTasks.map((task) => [task.id, task])),
63
+ };
64
+ }
65
+
66
+ function planToWorkflow(plan) {
67
+ if (!plan?.ok || !Array.isArray(plan.waves)) return null;
68
+
69
+ const artifacts = [];
70
+ const waves = plan.waves.map((wave) => {
71
+ for (const task of wave.tasks || []) {
72
+ artifacts.push({
73
+ id: task.artifact_id,
74
+ type: task.artifact_type,
75
+ paths: task.paths || [],
76
+ refs: task.refs || [],
77
+ owner: task.owner,
78
+ reviewers: task.reviewers || [],
79
+ depends_on: task.depends_on || [],
80
+ verification: task.verification || [],
81
+ });
82
+ }
83
+ return {
84
+ id: wave.id,
85
+ mode: wave.mode,
86
+ artifact_ids: wave.artifact_ids || [],
87
+ };
88
+ });
89
+
90
+ return { artifacts, waves };
91
+ }
92
+
93
+ function tasksToWorkflow(tasks) {
94
+ if (!Array.isArray(tasks) || tasks.length === 0) return null;
95
+
96
+ const artifacts = [];
97
+ const wavesById = new Map();
98
+ for (const task of tasks) {
99
+ const waveId = task.wave_id;
100
+ if (!waveId || !Array.isArray(task.artifact_ids)) continue;
101
+ if (!wavesById.has(waveId)) {
102
+ wavesById.set(waveId, {
103
+ id: waveId,
104
+ mode: task.wave_mode || 'parallel',
105
+ artifact_ids: [],
106
+ });
107
+ }
108
+
109
+ const wave = wavesById.get(waveId);
110
+ for (const artifactId of task.artifact_ids) {
111
+ if (!wave.artifact_ids.includes(artifactId)) wave.artifact_ids.push(artifactId);
112
+ artifacts.push({
113
+ id: artifactId,
114
+ type: task.artifact_type,
115
+ paths: task.paths || [],
116
+ refs: task.refs || [],
117
+ owner: task.owner,
118
+ reviewers: task.reviewers || [],
119
+ depends_on: task.depends_on || [],
120
+ verification: task.verification || [],
121
+ });
122
+ }
123
+ }
124
+
125
+ return { artifacts, waves: Array.from(wavesById.values()) };
126
+ }
127
+
128
+ function criteriaForReviewer(role, artifact) {
129
+ if (!role || !Array.isArray(role.reviews)) return [];
130
+ const match = role.reviews.find((review) => review.artifact_type === artifact.type);
131
+ return match?.criteria || [];
132
+ }
133
+
134
+ function swarmTaskId(waveId, artifactId) {
135
+ return `swarm:${waveId}:${artifactId}`;
136
+ }