@limina-labs/momentum 0.30.2
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/LICENSE +21 -0
- package/README.md +214 -0
- package/adapters/antigravity/adapter.js +196 -0
- package/adapters/antigravity/hooks.json +39 -0
- package/adapters/antigravity/instructions/AGENTS.md +638 -0
- package/adapters/antigravity/instructions/header.md +3 -0
- package/adapters/antigravity/instructions/surfaces.md +153 -0
- package/adapters/antigravity/instructions/vars.json +1 -0
- package/adapters/antigravity/scripts/antigravity-hook-adapter.sh +147 -0
- package/adapters/antigravity/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/antigravity/skills/momentum-reviewer-architecture/SKILL.md +39 -0
- package/adapters/antigravity/skills/momentum-reviewer-qa/SKILL.md +38 -0
- package/adapters/antigravity/skills/momentum-reviewer-security/SKILL.md +42 -0
- package/adapters/antigravity/skills/swarm-supervisor/SKILL.md +75 -0
- package/adapters/antigravity/workflows/continue.md +39 -0
- package/adapters/antigravity/workflows/dispatch.md +49 -0
- package/adapters/antigravity/workflows/handoff.md +38 -0
- package/adapters/antigravity/workflows/review-code.md +84 -0
- package/adapters/antigravity/workflows/scout.md +42 -0
- package/adapters/antigravity/workflows/swarm.md +203 -0
- package/adapters/claude-code/adapter.js +148 -0
- package/adapters/claude-code/commands/continue.md +82 -0
- package/adapters/claude-code/commands/dispatch.md +154 -0
- package/adapters/claude-code/commands/handoff.md +85 -0
- package/adapters/claude-code/commands/review-code.md +194 -0
- package/adapters/claude-code/commands/scout.md +119 -0
- package/adapters/claude-code/commands/swarm.md +242 -0
- package/adapters/claude-code/instructions/header.md +3 -0
- package/adapters/claude-code/instructions/vars.json +1 -0
- package/adapters/claude-code/settings.json +36 -0
- package/adapters/codex/adapter.js +225 -0
- package/adapters/codex/agents/momentum-reviewer-architecture.toml +57 -0
- package/adapters/codex/agents/momentum-reviewer-qa.toml +52 -0
- package/adapters/codex/agents/momentum-reviewer-security.toml +57 -0
- package/adapters/codex/agents/swarm-supervisor.toml +76 -0
- package/adapters/codex/commands/continue.md +40 -0
- package/adapters/codex/commands/dispatch.md +111 -0
- package/adapters/codex/commands/handoff.md +57 -0
- package/adapters/codex/commands/scout.md +90 -0
- package/adapters/codex/commands/swarm.md +334 -0
- package/adapters/codex/hooks.json +36 -0
- package/adapters/codex/instructions/AGENTS.md +691 -0
- package/adapters/codex/instructions/header.md +3 -0
- package/adapters/codex/instructions/surfaces.md +206 -0
- package/adapters/codex/instructions/vars.json +1 -0
- package/adapters/codex/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/opencode/adapter.js +224 -0
- package/adapters/opencode/agents/momentum-reviewer-architecture.md +54 -0
- package/adapters/opencode/agents/momentum-reviewer-qa.md +48 -0
- package/adapters/opencode/agents/momentum-reviewer-security.md +48 -0
- package/adapters/opencode/agents/swarm-supervisor.md +51 -0
- package/adapters/opencode/commands/continue.md +40 -0
- package/adapters/opencode/commands/dispatch.md +111 -0
- package/adapters/opencode/commands/handoff.md +57 -0
- package/adapters/opencode/commands/review-code.md +194 -0
- package/adapters/opencode/commands/scout.md +90 -0
- package/adapters/opencode/commands/swarm.md +334 -0
- package/adapters/opencode/instructions/AGENTS.md +604 -0
- package/adapters/opencode/instructions/header.md +3 -0
- package/adapters/opencode/instructions/surfaces.md +119 -0
- package/adapters/opencode/instructions/vars.json +1 -0
- package/adapters/opencode/plugins/momentum.js +219 -0
- package/adapters/opencode/skills/momentum-lanes/SKILL.md +30 -0
- package/adapters/opencode/skills/momentum-orient/SKILL.md +29 -0
- package/adapters/opencode/skills/momentum-track/SKILL.md +28 -0
- package/adapters/opencode/skills/momentum-validate/SKILL.md +31 -0
- package/bin/antigravity.js +112 -0
- package/bin/ecosystem.js +1302 -0
- package/bin/lanes.js +300 -0
- package/bin/momentum.js +1857 -0
- package/bin/okf.js +72 -0
- package/bin/orchestration-commands.js +305 -0
- package/bin/state-commands.js +218 -0
- package/bin/swarm.js +1104 -0
- package/bin/waves.js +147 -0
- package/core/adapter-capabilities.md +112 -0
- package/core/adapter-parity-matrix.md +170 -0
- package/core/commands/brainstorm-idea.md +98 -0
- package/core/commands/brainstorm-phase.md +129 -0
- package/core/commands/complete-phase.md +130 -0
- package/core/commands/ecosystem.md +161 -0
- package/core/commands/hotfix.md +64 -0
- package/core/commands/initiative.md +119 -0
- package/core/commands/lanes.md +94 -0
- package/core/commands/log.md +45 -0
- package/core/commands/migrate.md +70 -0
- package/core/commands/review.md +31 -0
- package/core/commands/session.md +79 -0
- package/core/commands/start-phase.md +161 -0
- package/core/commands/start-project.md +142 -0
- package/core/commands/sync-docs.md +81 -0
- package/core/commands/systematic-debug.md +46 -0
- package/core/commands/track.md +54 -0
- package/core/commands/validate.md +96 -0
- package/core/ecosystem/layout.md +128 -0
- package/core/ecosystem/lib/index.js +245 -0
- package/core/ecosystem/lib/initiative.js +258 -0
- package/core/ecosystem/lib/pointer.js +198 -0
- package/core/ecosystem/lib/state.js +295 -0
- package/core/ecosystem/schema/ecosystem.schema.json +85 -0
- package/core/ecosystem/schema/initiative.schema.json +61 -0
- package/core/ecosystem/scripts/session-append.sh +167 -0
- package/core/ecosystem/templates/ecosystem-agents.md +79 -0
- package/core/ecosystem/templates/ecosystem-claude.md +79 -0
- package/core/ecosystem/templates/ecosystem-settings-claude.json +14 -0
- package/core/ecosystem/templates/initiative-template.md +50 -0
- package/core/engines/subagent-dispatch.md +80 -0
- package/core/git-hooks/commit-msg +14 -0
- package/core/git-hooks/contract.js +176 -0
- package/core/git-hooks/pre-push +15 -0
- package/core/git-hooks/run-check.js +160 -0
- package/core/instructions/legacy-project-md-hashes.json +12 -0
- package/core/instructions/navigation.md +14 -0
- package/core/instructions/rules-body.md +454 -0
- package/core/lanes/lib/board.js +161 -0
- package/core/lanes/lib/land.js +301 -0
- package/core/lanes/lib/signals.js +207 -0
- package/core/lanes/lib/state.js +325 -0
- package/core/lib/frontmatter.js +163 -0
- package/core/lib/okf-migrate.js +374 -0
- package/core/lib/okf-types.js +61 -0
- package/core/lifecycle-contract.md +59 -0
- package/core/orchestration/capability-routing.js +120 -0
- package/core/orchestration/continue.js +102 -0
- package/core/orchestration/dispatch.js +366 -0
- package/core/orchestration/events.js +197 -0
- package/core/orchestration/handoff.js +317 -0
- package/core/orchestration/index.js +50 -0
- package/core/orchestration/run-artifact.js +93 -0
- package/core/orchestration/scout.js +327 -0
- package/core/orchestration/session-log.js +123 -0
- package/core/orchestration/tracking.js +180 -0
- package/core/orchestration/types.js +131 -0
- package/core/scripts/brainstorm-gate.sh +157 -0
- package/core/scripts/check-history-reminder.sh +110 -0
- package/core/scripts/sessionstart-handoff.sh +180 -0
- package/core/specs-templates/CLAUDE.md +484 -0
- package/core/specs-templates/README.md +32 -0
- package/core/specs-templates/specs/README.md +30 -0
- package/core/specs-templates/specs/adhoc/README.md +31 -0
- package/core/specs-templates/specs/adhoc/_TEMPLATE.md +29 -0
- package/core/specs-templates/specs/architecture/ecosystem.md +214 -0
- package/core/specs-templates/specs/backlog/backlog.md +46 -0
- package/core/specs-templates/specs/backlog/details/.gitkeep +0 -0
- package/core/specs-templates/specs/changelog/.gitkeep +0 -0
- package/core/specs-templates/specs/decisions/0000-template.md +33 -0
- package/core/specs-templates/specs/decisions/README.md +29 -0
- package/core/specs-templates/specs/decisions/impact-map.md +14 -0
- package/core/specs-templates/specs/decisions/index.md +4 -0
- package/core/specs-templates/specs/index.md +16 -0
- package/core/specs-templates/specs/phases/README.md +41 -0
- package/core/specs-templates/specs/phases/index.md +6 -0
- package/core/specs-templates/specs/planning/roadmap.md +23 -0
- package/core/specs-templates/specs/status.md +70 -0
- package/core/specs-templates/specs/vision/principles.md +16 -0
- package/core/specs-templates/specs/vision/project-charter.md +35 -0
- package/core/specs-templates/specs/vision/success-criteria.md +19 -0
- package/core/swarm/absorb.js +294 -0
- package/core/swarm/conductor.js +499 -0
- package/core/swarm/focus.js +151 -0
- package/core/swarm/inbox.js +222 -0
- package/core/swarm/join.js +138 -0
- package/core/swarm/lib/board.js +178 -0
- package/core/swarm/lib/brief.js +157 -0
- package/core/swarm/lib/git-sha-cache.js +71 -0
- package/core/swarm/lib/incremental-log.js +117 -0
- package/core/swarm/lib/manifest.js +578 -0
- package/core/swarm/lib/pre-merge.js +112 -0
- package/core/swarm/lib/saga.js +146 -0
- package/core/swarm/lib/sessions.js +83 -0
- package/core/swarm/lib/tokens.js +255 -0
- package/core/swarm/lib/wave-ordering.js +55 -0
- package/core/swarm/schema/board.schema.json +54 -0
- package/core/swarm/schema/contract.schema.json +56 -0
- package/core/swarm/schema/dispatch-run.schema.json +70 -0
- package/core/swarm/schema/manifest.schema.json +234 -0
- package/core/swarm/schema/signal.schema.json +61 -0
- package/core/swarm/signals.js +296 -0
- package/core/swarm/supervise.md +81 -0
- package/core/waves/lib/plan-graph.js +175 -0
- package/core/waves/lib/waves.js +83 -0
- package/package.json +46 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* /swarm absorb <other-id> — converge a second swarm back into this one.
|
|
5
|
+
*
|
|
6
|
+
* Phase 17.5 portability primitive — the reunion side of `focus`. The
|
|
7
|
+
* caller (`targetSwarmId`) absorbs `sourceSwarmId`:
|
|
8
|
+
*
|
|
9
|
+
* 1. Load both manifests. Both must exist.
|
|
10
|
+
* 2. Detect repo overlap. For every repo in both, look up the swarms'
|
|
11
|
+
* contracts on the same surface — if the content_hash diverges,
|
|
12
|
+
* ABORT cleanly and surface a diff. Both swarms are left untouched.
|
|
13
|
+
* 3. On clean merge:
|
|
14
|
+
* - repos: union (target wins state on overlap)
|
|
15
|
+
* - waves: recompute via the existing wave-ordering library
|
|
16
|
+
* - sessions[]: union by session_id (earliest first_seen, latest
|
|
17
|
+
* last_seen)
|
|
18
|
+
* - contracts: union; on overlap the target version is kept (we
|
|
19
|
+
* already verified compatibility above)
|
|
20
|
+
* - audit[]: concat + sort by timestamp; append `absorb` event
|
|
21
|
+
* - inbox/: source pending items copied into the target inbox
|
|
22
|
+
* with bumped ids; INDEX regenerated
|
|
23
|
+
* - tokens/ + signals/: not merged (transitional state — discarded
|
|
24
|
+
* along with the archived source dir)
|
|
25
|
+
* 4. Archive the source swarm directory to
|
|
26
|
+
* `<eco>/swarms/.absorbed/<source-id>/`. Forensics preserved.
|
|
27
|
+
* 5. Refresh the target board.
|
|
28
|
+
*
|
|
29
|
+
* Returns: {
|
|
30
|
+
* absorbed: <source-id>,
|
|
31
|
+
* into: <target-id>,
|
|
32
|
+
* reposAdded: [...],
|
|
33
|
+
* reposOverlapped: [...],
|
|
34
|
+
* inboxMoved: <count>,
|
|
35
|
+
* archivedTo: '<absolute path>',
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* On contract conflict, throws `Error` with code 'ECONTRACT' and a
|
|
39
|
+
* `.conflicts` field describing each diff.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const fs = require('fs');
|
|
43
|
+
const path = require('path');
|
|
44
|
+
|
|
45
|
+
const manifestLib = require('./lib/manifest');
|
|
46
|
+
const inboxLib = require('./inbox');
|
|
47
|
+
const boardLib = require('./lib/board');
|
|
48
|
+
const ecosystemLib = require('../ecosystem/lib/index');
|
|
49
|
+
const { computeWaves } = require('./lib/wave-ordering');
|
|
50
|
+
|
|
51
|
+
function detectContractConflicts(sourceContracts, targetContracts) {
|
|
52
|
+
const conflicts = [];
|
|
53
|
+
const tByKey = new Map();
|
|
54
|
+
for (const c of (targetContracts || [])) {
|
|
55
|
+
tByKey.set(c.surface, c);
|
|
56
|
+
}
|
|
57
|
+
for (const s of (sourceContracts || [])) {
|
|
58
|
+
const t = tByKey.get(s.surface);
|
|
59
|
+
if (!t) continue;
|
|
60
|
+
// Owner OR content_hash divergence = conflict. Version drift alone
|
|
61
|
+
// isn't a conflict if hashes are absent (legacy).
|
|
62
|
+
if (s.owner !== t.owner) {
|
|
63
|
+
conflicts.push({
|
|
64
|
+
surface: s.surface, kind: 'owner-divergence',
|
|
65
|
+
source: { owner: s.owner }, target: { owner: t.owner },
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (s.content_hash && t.content_hash && s.content_hash !== t.content_hash) {
|
|
70
|
+
conflicts.push({
|
|
71
|
+
surface: s.surface, kind: 'content-hash-divergence',
|
|
72
|
+
source: { version: s.version, content_hash: s.content_hash },
|
|
73
|
+
target: { version: t.version, content_hash: t.content_hash },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return conflicts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mergeRepos(source, target) {
|
|
81
|
+
// Target wins on overlap (kept state); only NEW repos from source are
|
|
82
|
+
// added.
|
|
83
|
+
const merged = { ...target };
|
|
84
|
+
const added = [];
|
|
85
|
+
const overlapped = [];
|
|
86
|
+
for (const [id, repoState] of Object.entries(source)) {
|
|
87
|
+
if (id in target) {
|
|
88
|
+
overlapped.push(id);
|
|
89
|
+
} else {
|
|
90
|
+
merged[id] = repoState;
|
|
91
|
+
added.push(id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { merged, added, overlapped };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mergeSessions(sourceSessions, targetSessions) {
|
|
98
|
+
const byId = new Map();
|
|
99
|
+
for (const s of (targetSessions || [])) byId.set(s.session_id, { ...s });
|
|
100
|
+
for (const s of (sourceSessions || [])) {
|
|
101
|
+
const existing = byId.get(s.session_id);
|
|
102
|
+
if (!existing) {
|
|
103
|
+
byId.set(s.session_id, { ...s });
|
|
104
|
+
} else {
|
|
105
|
+
// Keep earliest first_seen, latest last_seen
|
|
106
|
+
if (s.first_seen && (!existing.first_seen || s.first_seen < existing.first_seen)) {
|
|
107
|
+
existing.first_seen = s.first_seen;
|
|
108
|
+
}
|
|
109
|
+
if (s.last_seen && (!existing.last_seen || s.last_seen > existing.last_seen)) {
|
|
110
|
+
existing.last_seen = s.last_seen;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Sort by first_seen ascending for deterministic output
|
|
115
|
+
return Array.from(byId.values()).sort((a, b) => (a.first_seen < b.first_seen ? -1 : 1));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function mergeContracts(sourceContracts, targetContracts) {
|
|
119
|
+
const byKey = new Map();
|
|
120
|
+
for (const c of (targetContracts || [])) byKey.set(c.surface, c);
|
|
121
|
+
for (const c of (sourceContracts || [])) {
|
|
122
|
+
if (!byKey.has(c.surface)) byKey.set(c.surface, c);
|
|
123
|
+
// else: target already wins; we validated compatibility upstream
|
|
124
|
+
}
|
|
125
|
+
return Array.from(byKey.values()).sort((a, b) => (a.surface < b.surface ? -1 : 1));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeAudit(sourceAudit, targetAudit, absorbEntry) {
|
|
129
|
+
const all = [
|
|
130
|
+
...(targetAudit || []),
|
|
131
|
+
...(sourceAudit || []),
|
|
132
|
+
absorbEntry,
|
|
133
|
+
];
|
|
134
|
+
return all.sort((a, b) => (a.ts < b.ts ? -1 : 1));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function copyInboxItems(ecosystemRoot, sourceSwarmId, targetSwarmId) {
|
|
138
|
+
const srcDir = inboxLib.inboxDir(ecosystemRoot, sourceSwarmId);
|
|
139
|
+
if (!fs.existsSync(srcDir)) return 0;
|
|
140
|
+
let moved = 0;
|
|
141
|
+
for (const name of fs.readdirSync(srcDir)) {
|
|
142
|
+
const m = name.match(/^(\d{4})-([a-z][a-z0-9-]*)\.md$/);
|
|
143
|
+
if (!m) continue;
|
|
144
|
+
const [, , slug] = m;
|
|
145
|
+
// Read + re-write via the inbox library so id collision is handled
|
|
146
|
+
const raw = fs.readFileSync(path.join(srcDir, name), 'utf8');
|
|
147
|
+
// Parse repo / question from header
|
|
148
|
+
const repoMatch = raw.match(/^- Repo:\s*`([^`]+)`/m);
|
|
149
|
+
const askedMatch = raw.match(/^- Asked at:\s*(.+)$/m);
|
|
150
|
+
const repo = repoMatch ? repoMatch[1] : 'unknown';
|
|
151
|
+
const asked = askedMatch ? askedMatch[1].trim() : new Date().toISOString();
|
|
152
|
+
// Question body: everything after `## Question` until next `##` or EOF
|
|
153
|
+
const qMatch = raw.match(/## Question\s*\n+([\s\S]*?)(?=\n## |\n*$)/);
|
|
154
|
+
const question = qMatch ? qMatch[1].trim() : 'absorbed from source swarm (body unparseable)';
|
|
155
|
+
inboxLib.writeInboxItem({
|
|
156
|
+
ecosystemRoot, swarmId: targetSwarmId,
|
|
157
|
+
repo, slug: `absorbed-${slug}`,
|
|
158
|
+
question,
|
|
159
|
+
nowIso: asked,
|
|
160
|
+
});
|
|
161
|
+
moved += 1;
|
|
162
|
+
}
|
|
163
|
+
return moved;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function archiveSourceSwarm(ecosystemRoot, sourceSwarmId) {
|
|
167
|
+
const srcDir = manifestLib.swarmDir(ecosystemRoot, sourceSwarmId);
|
|
168
|
+
if (!fs.existsSync(srcDir)) return null;
|
|
169
|
+
const absorbedRoot = path.join(ecosystemRoot, manifestLib.SWARMS_DIR, '.absorbed');
|
|
170
|
+
fs.mkdirSync(absorbedRoot, { recursive: true });
|
|
171
|
+
const destDir = path.join(absorbedRoot, sourceSwarmId);
|
|
172
|
+
if (fs.existsSync(destDir)) {
|
|
173
|
+
// Already absorbed — keep both for forensics
|
|
174
|
+
const stamped = `${destDir}.${Date.now()}`;
|
|
175
|
+
fs.renameSync(srcDir, stamped);
|
|
176
|
+
return stamped;
|
|
177
|
+
}
|
|
178
|
+
fs.renameSync(srcDir, destDir);
|
|
179
|
+
return destDir;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Main entrypoint.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} args
|
|
186
|
+
* @param {string} args.ecosystemRoot
|
|
187
|
+
* @param {string} args.targetSwarmId the absorbing swarm (caller)
|
|
188
|
+
* @param {string} args.sourceSwarmId the absorbed swarm (going away)
|
|
189
|
+
* @param {string} args.sessionId caller session id
|
|
190
|
+
* @param {string} args.nowIso
|
|
191
|
+
*/
|
|
192
|
+
function absorb(args) {
|
|
193
|
+
const { ecosystemRoot, targetSwarmId, sourceSwarmId, sessionId, nowIso } = args;
|
|
194
|
+
if (!targetSwarmId || !sourceSwarmId) {
|
|
195
|
+
throw new TypeError('absorb: targetSwarmId + sourceSwarmId required');
|
|
196
|
+
}
|
|
197
|
+
if (targetSwarmId === sourceSwarmId) {
|
|
198
|
+
throw new Error('absorb: cannot absorb a swarm into itself');
|
|
199
|
+
}
|
|
200
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
201
|
+
throw new TypeError('absorb: sessionId required');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const target = manifestLib.loadManifest(ecosystemRoot, targetSwarmId);
|
|
205
|
+
if (!target) throw new Error(`absorb: target swarm ${targetSwarmId} not found`);
|
|
206
|
+
const source = manifestLib.loadManifest(ecosystemRoot, sourceSwarmId);
|
|
207
|
+
if (!source) throw new Error(`absorb: source swarm ${sourceSwarmId} not found`);
|
|
208
|
+
|
|
209
|
+
// 1. Contract conflict check
|
|
210
|
+
const conflicts = detectContractConflicts(source.contracts, target.contracts);
|
|
211
|
+
if (conflicts.length > 0) {
|
|
212
|
+
const err = new Error(`absorb: contract conflict on ${conflicts.length} surface(s)`);
|
|
213
|
+
err.code = 'ECONTRACT';
|
|
214
|
+
err.conflicts = conflicts;
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2. Recompute waves over the union of repos
|
|
219
|
+
const ecoMfst = ecosystemLib.loadManifest(ecosystemRoot);
|
|
220
|
+
const { merged: mergedRepos, added: reposAdded, overlapped: reposOverlapped } = mergeRepos(source.repos, target.repos);
|
|
221
|
+
const allRepoIds = Object.keys(mergedRepos).sort();
|
|
222
|
+
let mergedWaves;
|
|
223
|
+
try {
|
|
224
|
+
mergedWaves = computeWaves(allRepoIds, (ecoMfst && ecoMfst.dependencies) || []);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// If the union cycles, abort cleanly
|
|
227
|
+
const e = new Error(`absorb: wave recomputation failed — ${err.message}`);
|
|
228
|
+
e.code = 'EWAVE';
|
|
229
|
+
throw e;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Re-assign each repo's wave field to match new wave indices
|
|
233
|
+
const repoWaveById = new Map();
|
|
234
|
+
for (const w of mergedWaves) {
|
|
235
|
+
for (const r of w.repos) repoWaveById.set(r, w.index);
|
|
236
|
+
}
|
|
237
|
+
for (const [id, repoState] of Object.entries(mergedRepos)) {
|
|
238
|
+
repoState.wave = repoWaveById.get(id) || repoState.wave;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Merge the other sections
|
|
242
|
+
const mergedSessions = mergeSessions(source.sessions, target.sessions);
|
|
243
|
+
const mergedContracts = mergeContracts(source.contracts, target.contracts);
|
|
244
|
+
const absorbEntry = {
|
|
245
|
+
ts: nowIso, actor: sessionId, event: 'absorb',
|
|
246
|
+
detail: `absorbed ${sourceSwarmId} (added ${reposAdded.length} repo(s), overlapped ${reposOverlapped.length})`,
|
|
247
|
+
};
|
|
248
|
+
const mergedAudit = mergeAudit(source.audit, target.audit, absorbEntry);
|
|
249
|
+
|
|
250
|
+
// 4. Write merged manifest atomically
|
|
251
|
+
manifestLib.updateManifest(ecosystemRoot, targetSwarmId, (m) => {
|
|
252
|
+
m.repos = mergedRepos;
|
|
253
|
+
m.waves = mergedWaves.map((w) => ({ index: w.index, repos: w.repos, status: 'queued' }));
|
|
254
|
+
// Preserve existing wave statuses for any wave whose membership is unchanged
|
|
255
|
+
const prevByIndex = new Map((target.waves || []).map((w) => [w.index, w]));
|
|
256
|
+
m.waves = m.waves.map((w) => {
|
|
257
|
+
const prev = prevByIndex.get(w.index);
|
|
258
|
+
if (prev && JSON.stringify(prev.repos) === JSON.stringify(w.repos) && prev.status) {
|
|
259
|
+
return { ...w, status: prev.status };
|
|
260
|
+
}
|
|
261
|
+
return w;
|
|
262
|
+
});
|
|
263
|
+
m.sessions = mergedSessions;
|
|
264
|
+
if (mergedContracts.length) m.contracts = mergedContracts;
|
|
265
|
+
m.audit = mergedAudit;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// 5. Merge inbox items (additive — source items reappear in target)
|
|
269
|
+
const inboxMoved = copyInboxItems(ecosystemRoot, sourceSwarmId, targetSwarmId);
|
|
270
|
+
|
|
271
|
+
// 6. Archive source dir
|
|
272
|
+
const archivedTo = archiveSourceSwarm(ecosystemRoot, sourceSwarmId);
|
|
273
|
+
|
|
274
|
+
// 7. Refresh board
|
|
275
|
+
boardLib.refreshBoard(ecosystemRoot, targetSwarmId, nowIso);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
absorbed: sourceSwarmId,
|
|
279
|
+
into: targetSwarmId,
|
|
280
|
+
reposAdded,
|
|
281
|
+
reposOverlapped,
|
|
282
|
+
inboxMoved,
|
|
283
|
+
archivedTo,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
absorb,
|
|
289
|
+
detectContractConflicts,
|
|
290
|
+
mergeRepos,
|
|
291
|
+
mergeSessions,
|
|
292
|
+
mergeContracts,
|
|
293
|
+
mergeAudit,
|
|
294
|
+
};
|