@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
package/bin/swarm.js
ADDED
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `momentum swarm` subcommand dispatcher.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands (v0.20.0):
|
|
7
|
+
* start <slug> --initiative <slug> --repos r1,r2,... --phase <phase-slug> [--mode autopilot|checkpoint]
|
|
8
|
+
* status <swarm-id> — print board.json as JSON or rendered table
|
|
9
|
+
* tell <swarm-id> <repo> "<text>"
|
|
10
|
+
* broadcast <swarm-id> "<text>"
|
|
11
|
+
* verify <swarm-id> — contract verifier + drift check
|
|
12
|
+
* complete <swarm-id> — synthesize retrospective + changes/<id>.md
|
|
13
|
+
* resume <swarm-id> — reattach this session to a swarm
|
|
14
|
+
* cancel <swarm-id> — graceful halt
|
|
15
|
+
* budget <swarm-id> <repo> +N
|
|
16
|
+
*
|
|
17
|
+
* Lower-level CLI floor for the three doors: slash recipes wrap this,
|
|
18
|
+
* NL inference wraps this, direct CLI is this. All three produce the
|
|
19
|
+
* same on-disk artifacts.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
|
|
26
|
+
const MOMENTUM_ROOT = path.resolve(__dirname, '..');
|
|
27
|
+
const conductor = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'conductor'));
|
|
28
|
+
const manifestLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'lib', 'manifest'));
|
|
29
|
+
const boardLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'lib', 'board'));
|
|
30
|
+
const briefLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'lib', 'brief'));
|
|
31
|
+
const inboxLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'inbox'));
|
|
32
|
+
const signalsLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'signals'));
|
|
33
|
+
const focusLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'focus'));
|
|
34
|
+
const joinLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'join'));
|
|
35
|
+
const absorbLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'absorb'));
|
|
36
|
+
const preMergeLib = require(path.join(MOMENTUM_ROOT, 'core', 'swarm', 'lib', 'pre-merge'));
|
|
37
|
+
const ecosystemLib = require(path.join(MOMENTUM_ROOT, 'core', 'ecosystem', 'lib', 'index'));
|
|
38
|
+
const { findRegistration } = require(path.join(MOMENTUM_ROOT, 'core', 'ecosystem', 'lib', 'state'));
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Argv parsing
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function parseFlags(args, defs) {
|
|
45
|
+
const out = { positional: [] };
|
|
46
|
+
for (const key of Object.keys(defs)) out[key] = defs[key].default;
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < args.length) {
|
|
49
|
+
const a = args[i];
|
|
50
|
+
let consumed = false;
|
|
51
|
+
for (const key of Object.keys(defs)) {
|
|
52
|
+
const def = defs[key];
|
|
53
|
+
if (a === def.flag || (def.aliases && def.aliases.includes(a))) {
|
|
54
|
+
if (def.type === 'bool') {
|
|
55
|
+
out[key] = true;
|
|
56
|
+
i += 1;
|
|
57
|
+
} else if (def.type === 'list') {
|
|
58
|
+
out[key] = (args[i + 1] || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
59
|
+
i += 2;
|
|
60
|
+
} else {
|
|
61
|
+
out[key] = args[i + 1];
|
|
62
|
+
i += 2;
|
|
63
|
+
}
|
|
64
|
+
consumed = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!consumed) {
|
|
69
|
+
out.positional.push(a);
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Ecosystem resolution
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function resolveEcosystemRoot(explicit) {
|
|
81
|
+
if (explicit && fs.existsSync(path.join(explicit, 'ecosystem.json'))) {
|
|
82
|
+
return path.resolve(explicit);
|
|
83
|
+
}
|
|
84
|
+
const fromCwd = ecosystemLib.findRoot(process.cwd());
|
|
85
|
+
if (fromCwd) return fromCwd;
|
|
86
|
+
const reg = findRegistration(process.cwd());
|
|
87
|
+
if (reg && reg.rootPath) return reg.rootPath;
|
|
88
|
+
throw new Error(
|
|
89
|
+
'swarm: cannot locate ecosystem root. Run from inside an ecosystem ' +
|
|
90
|
+
'or pass --ecosystem <path>.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function nowIso() {
|
|
95
|
+
return new Date().toISOString().replace(/\.\d+Z$/, 'Z');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function generateSessionId() {
|
|
99
|
+
return 'sess_' + crypto.randomBytes(8).toString('hex');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the calling session id. Order:
|
|
104
|
+
* 1. explicit --session flag
|
|
105
|
+
* 2. MOMENTUM_SESSION_ID env (set by conductor recipe)
|
|
106
|
+
* 3. generate a fresh one
|
|
107
|
+
*/
|
|
108
|
+
function resolveSessionId(explicit) {
|
|
109
|
+
if (explicit) return explicit;
|
|
110
|
+
if (process.env.MOMENTUM_SESSION_ID) return process.env.MOMENTUM_SESSION_ID;
|
|
111
|
+
return generateSessionId();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add `hours` to an ISO timestamp (returns ISO).
|
|
116
|
+
*/
|
|
117
|
+
function plusHours(iso, hours) {
|
|
118
|
+
const ms = Date.parse(iso) + Number(hours) * 60 * 60 * 1000;
|
|
119
|
+
return new Date(ms).toISOString().replace(/\.\d+Z$/, 'Z');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Slugify a session id for use in signal slugs (lowercase, kebab-safe).
|
|
124
|
+
*/
|
|
125
|
+
function sessionSlug(sessionId) {
|
|
126
|
+
return String(sessionId).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || 'unknown';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
// Subcommands
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function cmdStart(args) {
|
|
134
|
+
const opts = parseFlags(args, {
|
|
135
|
+
initiative: { flag: '--initiative' },
|
|
136
|
+
repos: { flag: '--repos', type: 'list', default: [] },
|
|
137
|
+
phase: { flag: '--phase' },
|
|
138
|
+
mode: { flag: '--mode', default: 'checkpoint' },
|
|
139
|
+
sessionId: { flag: '--session', default: null },
|
|
140
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
141
|
+
spawn: { flag: '--spawn', type: 'bool', default: false },
|
|
142
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
143
|
+
});
|
|
144
|
+
const [slug] = opts.positional;
|
|
145
|
+
if (!slug) throw new Error('swarm start: <slug> required');
|
|
146
|
+
if (!opts.initiative) throw new Error('swarm start: --initiative <slug> required');
|
|
147
|
+
if (!opts.repos || opts.repos.length === 0) throw new Error('swarm start: --repos r1,r2,... required');
|
|
148
|
+
if (!opts.phase) throw new Error('swarm start: --phase <phase-slug> required');
|
|
149
|
+
|
|
150
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
151
|
+
const swarmId = manifestLib.nextSwarmId(ecosystemRoot, slug);
|
|
152
|
+
const sessionId = opts.sessionId || generateSessionId();
|
|
153
|
+
const ts = nowIso();
|
|
154
|
+
|
|
155
|
+
const manifest = conductor.planSwarm({
|
|
156
|
+
ecosystemRoot,
|
|
157
|
+
swarmId,
|
|
158
|
+
initiative: opts.initiative,
|
|
159
|
+
impactedRepos: opts.repos,
|
|
160
|
+
phaseSlug: opts.phase,
|
|
161
|
+
sessionId,
|
|
162
|
+
mode: opts.mode,
|
|
163
|
+
nowIso: ts,
|
|
164
|
+
});
|
|
165
|
+
manifestLib.writeManifest(ecosystemRoot, swarmId, manifest);
|
|
166
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, ts);
|
|
167
|
+
|
|
168
|
+
// Build (and optionally execute) Wave 1 spawn directives. Default is
|
|
169
|
+
// dry-run — the platform layer or the conductor recipe handles the
|
|
170
|
+
// actual spawn so the CLI floor stays platform-agnostic.
|
|
171
|
+
const directives = conductor.buildSpawnDirectives({
|
|
172
|
+
ecosystemRoot, swarmId, waveIndex: 1, platform: 'claude-code',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
let spawnResults = null;
|
|
176
|
+
if (opts.spawn) {
|
|
177
|
+
spawnResults = spawnSupervisors(directives);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (opts.json) {
|
|
181
|
+
process.stdout.write(JSON.stringify({
|
|
182
|
+
swarmId, sessionId, manifestPath: manifestLib.manifestPath(ecosystemRoot, swarmId),
|
|
183
|
+
directives, spawned: spawnResults,
|
|
184
|
+
}, null, 2) + '\n');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`▸ Swarm started: ${swarmId}`);
|
|
189
|
+
console.log(` Initiative: ${opts.initiative}`);
|
|
190
|
+
console.log(` Mode: ${opts.mode}`);
|
|
191
|
+
console.log(` Session: ${sessionId}`);
|
|
192
|
+
console.log(` Waves: ${manifest.waves.length}`);
|
|
193
|
+
for (const w of manifest.waves) {
|
|
194
|
+
console.log(` Wave ${w.index}: ${w.repos.join(', ')}`);
|
|
195
|
+
}
|
|
196
|
+
console.log('');
|
|
197
|
+
console.log(`▸ Wave 1 spawn directives (${directives.length}):`);
|
|
198
|
+
for (const d of directives) {
|
|
199
|
+
console.log(` - ${d.repoId}: ${d.repoPath}`);
|
|
200
|
+
}
|
|
201
|
+
if (opts.spawn && spawnResults) {
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log('▸ Spawn results:');
|
|
204
|
+
for (const r of spawnResults) {
|
|
205
|
+
console.log(` - ${r.repoId}: ${r.status === 0 ? 'ok' : 'failed'}`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log(' (dry-run — pass --spawn to launch Claude Code supervisors)');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cmdStatus(args) {
|
|
214
|
+
const opts = parseFlags(args, {
|
|
215
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
216
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
217
|
+
});
|
|
218
|
+
const [swarmId] = opts.positional;
|
|
219
|
+
if (!swarmId) throw new Error('swarm status: <swarm-id> required');
|
|
220
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
221
|
+
const board = boardLib.loadBoard(ecosystemRoot, swarmId);
|
|
222
|
+
if (!board) throw new Error(`swarm status: no board for ${swarmId}`);
|
|
223
|
+
if (opts.json) {
|
|
224
|
+
process.stdout.write(JSON.stringify(board, null, 2) + '\n');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
renderBoardTable(board);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cmdTell(args) {
|
|
231
|
+
const opts = parseFlags(args, {
|
|
232
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
233
|
+
});
|
|
234
|
+
const [swarmId, repoId, ...rest] = opts.positional;
|
|
235
|
+
const text = rest.join(' ').trim();
|
|
236
|
+
if (!swarmId || !repoId || !text) {
|
|
237
|
+
throw new Error('swarm tell: usage — momentum swarm tell <swarm-id> <repo> "<text>"');
|
|
238
|
+
}
|
|
239
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
240
|
+
const manifest = manifestLib.loadManifest(ecosystemRoot, swarmId);
|
|
241
|
+
if (!manifest) throw new Error(`swarm tell: no manifest for ${swarmId}`);
|
|
242
|
+
const repo = manifest.repos[repoId];
|
|
243
|
+
if (!repo) throw new Error(`swarm tell: ${repoId} is not a member of swarm ${swarmId}`);
|
|
244
|
+
|
|
245
|
+
const repoPath = conductor.resolveMemberPath(ecosystemRoot, repoId);
|
|
246
|
+
if (!repoPath) throw new Error(`swarm tell: cannot locate repo ${repoId}`);
|
|
247
|
+
const contextPath = path.join(repoPath, 'specs', 'phases', repo.phase_slug, 'swarm-context.md');
|
|
248
|
+
fs.mkdirSync(path.dirname(contextPath), { recursive: true });
|
|
249
|
+
const ts = nowIso();
|
|
250
|
+
const block = `\n\n## tell @ ${ts}\n\n${text}\n`;
|
|
251
|
+
fs.appendFileSync(contextPath, block, 'utf8');
|
|
252
|
+
|
|
253
|
+
manifestLib.appendAudit(ecosystemRoot, swarmId, {
|
|
254
|
+
ts, actor: 'conductor', event: 'tell', repo: repoId, detail: text.slice(0, 200),
|
|
255
|
+
});
|
|
256
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, ts);
|
|
257
|
+
console.log(`▸ tell delivered to ${repoId}: ${path.relative(ecosystemRoot, contextPath)}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function cmdBroadcast(args) {
|
|
261
|
+
const opts = parseFlags(args, {
|
|
262
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
263
|
+
});
|
|
264
|
+
const [swarmId, ...rest] = opts.positional;
|
|
265
|
+
const text = rest.join(' ').trim();
|
|
266
|
+
if (!swarmId || !text) {
|
|
267
|
+
throw new Error('swarm broadcast: usage — momentum swarm broadcast <swarm-id> "<text>"');
|
|
268
|
+
}
|
|
269
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
270
|
+
const manifest = manifestLib.loadManifest(ecosystemRoot, swarmId);
|
|
271
|
+
if (!manifest) throw new Error(`swarm broadcast: no manifest for ${swarmId}`);
|
|
272
|
+
const ts = nowIso();
|
|
273
|
+
for (const [repoId, repo] of Object.entries(manifest.repos)) {
|
|
274
|
+
const repoPath = conductor.resolveMemberPath(ecosystemRoot, repoId);
|
|
275
|
+
if (!repoPath) continue;
|
|
276
|
+
const contextPath = path.join(repoPath, 'specs', 'phases', repo.phase_slug, 'swarm-context.md');
|
|
277
|
+
fs.mkdirSync(path.dirname(contextPath), { recursive: true });
|
|
278
|
+
const block = `\n\n## broadcast @ ${ts}\n\n${text}\n`;
|
|
279
|
+
fs.appendFileSync(contextPath, block, 'utf8');
|
|
280
|
+
}
|
|
281
|
+
manifestLib.appendAudit(ecosystemRoot, swarmId, {
|
|
282
|
+
ts, actor: 'conductor', event: 'broadcast', detail: text.slice(0, 200),
|
|
283
|
+
});
|
|
284
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, ts);
|
|
285
|
+
console.log(`▸ broadcast sent to ${Object.keys(manifest.repos).length} repos`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function cmdVerify(args) {
|
|
289
|
+
const opts = parseFlags(args, {
|
|
290
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
291
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
292
|
+
});
|
|
293
|
+
const [swarmId] = opts.positional;
|
|
294
|
+
if (!swarmId) throw new Error('swarm verify: <swarm-id> required');
|
|
295
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
296
|
+
const result = verifySwarm(ecosystemRoot, swarmId);
|
|
297
|
+
if (opts.json) {
|
|
298
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`▸ Verify ${swarmId}: ${result.ok ? 'OK' : 'FAIL'}`);
|
|
301
|
+
for (const issue of result.issues) {
|
|
302
|
+
console.log(` ✗ ${issue}`);
|
|
303
|
+
}
|
|
304
|
+
for (const note of result.notes) {
|
|
305
|
+
console.log(` · ${note}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!result.ok) process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function cmdComplete(args) {
|
|
312
|
+
const opts = parseFlags(args, {
|
|
313
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
314
|
+
});
|
|
315
|
+
const [swarmId] = opts.positional;
|
|
316
|
+
if (!swarmId) throw new Error('swarm complete: <swarm-id> required');
|
|
317
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
318
|
+
const ts = nowIso();
|
|
319
|
+
const result = completeSwarm(ecosystemRoot, swarmId, ts);
|
|
320
|
+
console.log(`▸ Swarm ${swarmId} complete`);
|
|
321
|
+
console.log(` Changeset: ${result.changesetPath}`);
|
|
322
|
+
console.log(` Repos done: ${result.reposComplete}/${result.reposTotal}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function cmdResume(args) {
|
|
326
|
+
const opts = parseFlags(args, {
|
|
327
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
328
|
+
sessionId: { flag: '--session', default: null },
|
|
329
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
330
|
+
});
|
|
331
|
+
const [swarmId] = opts.positional;
|
|
332
|
+
if (!swarmId) throw new Error('swarm resume: <swarm-id> required');
|
|
333
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
334
|
+
const sessionId = opts.sessionId || generateSessionId();
|
|
335
|
+
const ts = nowIso();
|
|
336
|
+
const manifest = conductor.resumeSwarm(ecosystemRoot, swarmId, sessionId, ts);
|
|
337
|
+
|
|
338
|
+
// Disk-only reconstitution: pull the current materialized board so
|
|
339
|
+
// the recipe can render it without recomputing.
|
|
340
|
+
const board = boardLib.loadBoard(ecosystemRoot, swarmId);
|
|
341
|
+
|
|
342
|
+
// For every owned repo, refresh the lease so audit shows we are live.
|
|
343
|
+
conductor.renewLeases(ecosystemRoot, swarmId, sessionId, ts);
|
|
344
|
+
|
|
345
|
+
if (opts.json) {
|
|
346
|
+
process.stdout.write(JSON.stringify({
|
|
347
|
+
swarmId, sessionId, status: manifest.status, board,
|
|
348
|
+
}, null, 2) + '\n');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
console.log(`▸ Resumed swarm ${swarmId} as session ${sessionId}`);
|
|
352
|
+
console.log(` Status: ${manifest.status}`);
|
|
353
|
+
for (const w of manifest.waves) {
|
|
354
|
+
console.log(` Wave ${w.index}: ${w.status || 'queued'} — ${w.repos.join(', ')}`);
|
|
355
|
+
}
|
|
356
|
+
if (board && board.inbox_count > 0) {
|
|
357
|
+
console.log(` ▸ ${board.inbox_count} pending inbox item(s) — surface with /swarm inbox list ${swarmId}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function cmdCancel(args) {
|
|
362
|
+
const opts = parseFlags(args, {
|
|
363
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
364
|
+
reason: { flag: '--reason', default: 'cancelled by user' },
|
|
365
|
+
});
|
|
366
|
+
const [swarmId] = opts.positional;
|
|
367
|
+
if (!swarmId) throw new Error('swarm cancel: <swarm-id> required');
|
|
368
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
369
|
+
const ts = nowIso();
|
|
370
|
+
conductor.cancelSwarm(ecosystemRoot, swarmId, opts.reason, ts);
|
|
371
|
+
console.log(`▸ Swarm ${swarmId} cancelled (${opts.reason})`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
375
|
+
// Phase 17.5 — claim / release
|
|
376
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
function cmdClaim(args) {
|
|
379
|
+
const opts = parseFlags(args, {
|
|
380
|
+
sessionId: { flag: '--session', default: null },
|
|
381
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
382
|
+
leaseHours: { flag: '--lease-hours', default: '24' },
|
|
383
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
384
|
+
});
|
|
385
|
+
const [swarmId, repoId] = opts.positional;
|
|
386
|
+
if (!swarmId || !repoId) {
|
|
387
|
+
throw new Error('swarm claim: usage — momentum swarm claim <swarm-id> <repo> [--session <id>]');
|
|
388
|
+
}
|
|
389
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
390
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
391
|
+
const ts = nowIso();
|
|
392
|
+
const expiresAt = plusHours(ts, opts.leaseHours);
|
|
393
|
+
try {
|
|
394
|
+
manifestLib.updateManifestAsOwner({
|
|
395
|
+
ecosystemRoot, swarmId, sessionId, repo: repoId, nowIso: ts,
|
|
396
|
+
mutate: (m, decision) => {
|
|
397
|
+
const previousOwner = m.repos[repoId].owner;
|
|
398
|
+
m.repos[repoId].owner = sessionId;
|
|
399
|
+
m.repos[repoId].claimed_by_session = sessionId;
|
|
400
|
+
m.repos[repoId].lease_renewed_at = ts;
|
|
401
|
+
m.repos[repoId].lease_expires_at = expiresAt;
|
|
402
|
+
if (!Array.isArray(m.audit)) m.audit = [];
|
|
403
|
+
m.audit.push({
|
|
404
|
+
ts, actor: sessionId, event: 'claim', repo: repoId,
|
|
405
|
+
detail: `${previousOwner} → ${sessionId}${decision.expired ? ' (after lease expiry)' : ''}`,
|
|
406
|
+
});
|
|
407
|
+
if (decision.expired) {
|
|
408
|
+
m.audit.push({
|
|
409
|
+
ts, actor: sessionId, event: 'lease-takeover', repo: repoId,
|
|
410
|
+
detail: `previous owner ${previousOwner} lease expired`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, ts);
|
|
416
|
+
if (opts.json) {
|
|
417
|
+
process.stdout.write(JSON.stringify({
|
|
418
|
+
swarmId, repo: repoId, owner: sessionId, lease_expires_at: expiresAt,
|
|
419
|
+
}, null, 2) + '\n');
|
|
420
|
+
} else {
|
|
421
|
+
console.log(`▸ claim ${swarmId}/${repoId} → ${sessionId}`);
|
|
422
|
+
console.log(` lease until ${expiresAt}`);
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (err.code === 'EOWNERSHIP') {
|
|
426
|
+
// Write a claim-request signal so the existing owner sees it next poll.
|
|
427
|
+
const sig = signalsLib.writeSignal({
|
|
428
|
+
ecosystemRoot, swarmId,
|
|
429
|
+
type: 'claim-request', slug: `${repoId}-by-${sessionSlug(sessionId)}`,
|
|
430
|
+
fromSession: sessionId, repo: repoId,
|
|
431
|
+
nowIso: ts, detail: `claim rejected: ${err.decision.reason}`,
|
|
432
|
+
});
|
|
433
|
+
console.error(`✗ claim ${swarmId}/${repoId} rejected: ${err.decision.reason}`);
|
|
434
|
+
console.error(` claim-request signal written (${sig.signal_id}); owner sees it next poll`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
throw err;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function cmdAbsorb(args) {
|
|
442
|
+
const opts = parseFlags(args, {
|
|
443
|
+
sessionId: { flag: '--session', default: null },
|
|
444
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
445
|
+
yes: { flag: '--yes', type: 'bool', default: false, aliases: ['-y'] },
|
|
446
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
447
|
+
});
|
|
448
|
+
const [targetSwarmId, sourceSwarmId] = opts.positional;
|
|
449
|
+
if (!targetSwarmId || !sourceSwarmId) {
|
|
450
|
+
throw new Error('swarm absorb: usage — momentum swarm absorb <target-swarm-id> <source-swarm-id> [--session <id>] [--yes]');
|
|
451
|
+
}
|
|
452
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
453
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
454
|
+
const ts = nowIso();
|
|
455
|
+
|
|
456
|
+
// Confirmation prompt (skipped with --yes)
|
|
457
|
+
if (!opts.yes && !opts.json) {
|
|
458
|
+
const target = manifestLib.loadManifest(ecosystemRoot, targetSwarmId);
|
|
459
|
+
const source = manifestLib.loadManifest(ecosystemRoot, sourceSwarmId);
|
|
460
|
+
if (!target || !source) {
|
|
461
|
+
// Let absorb() surface the canonical error
|
|
462
|
+
} else {
|
|
463
|
+
const conflicts = absorbLib.detectContractConflicts(source.contracts, target.contracts);
|
|
464
|
+
const overlap = Object.keys(source.repos).filter((id) => id in target.repos);
|
|
465
|
+
const newRepos = Object.keys(source.repos).filter((id) => !(id in target.repos));
|
|
466
|
+
console.log(`▸ absorb plan: ${sourceSwarmId} → ${targetSwarmId}`);
|
|
467
|
+
console.log(` Repos to add: ${newRepos.length ? newRepos.join(', ') : '(none)'}`);
|
|
468
|
+
console.log(` Repos overlap: ${overlap.length ? overlap.join(', ') : '(none)'}`);
|
|
469
|
+
console.log(` Contracts diff: ${conflicts.length ? conflicts.length + ' conflict(s)' : 'clean'}`);
|
|
470
|
+
if (conflicts.length) {
|
|
471
|
+
for (const c of conflicts) {
|
|
472
|
+
console.log(` ✗ ${c.surface} — ${c.kind}`);
|
|
473
|
+
}
|
|
474
|
+
console.error('');
|
|
475
|
+
console.error('Aborting — resolve contract conflicts first (bump versions on both sides, or use --yes to force).');
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
console.log('');
|
|
479
|
+
console.log(' Pass --yes to skip this confirmation. Re-run with --yes to proceed.');
|
|
480
|
+
process.exit(0);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const r = absorbLib.absorb({
|
|
486
|
+
ecosystemRoot, targetSwarmId, sourceSwarmId, sessionId, nowIso: ts,
|
|
487
|
+
});
|
|
488
|
+
if (opts.json) {
|
|
489
|
+
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
console.log(`▸ absorbed ${r.absorbed} → ${r.into}`);
|
|
493
|
+
console.log(` Repos added: ${r.reposAdded.length ? r.reposAdded.join(', ') : '(none)'}`);
|
|
494
|
+
console.log(` Repos overlap: ${r.reposOverlapped.length ? r.reposOverlapped.join(', ') : '(none)'}`);
|
|
495
|
+
console.log(` Inbox moved: ${r.inboxMoved}`);
|
|
496
|
+
console.log(` Archived to: ${path.relative(ecosystemRoot, r.archivedTo)}`);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
if (err.code === 'ECONTRACT') {
|
|
499
|
+
console.error(`✗ absorb ${sourceSwarmId} → ${targetSwarmId}: contract conflict on ${err.conflicts.length} surface(s)`);
|
|
500
|
+
for (const c of err.conflicts) {
|
|
501
|
+
console.error(` ${c.surface} — ${c.kind}`);
|
|
502
|
+
console.error(` source: ${JSON.stringify(c.source)}`);
|
|
503
|
+
console.error(` target: ${JSON.stringify(c.target)}`);
|
|
504
|
+
}
|
|
505
|
+
console.error('');
|
|
506
|
+
console.error('Both swarms untouched. Resolve contract divergence then retry.');
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function cmdJoin(args) {
|
|
514
|
+
const opts = parseFlags(args, {
|
|
515
|
+
sessionId: { flag: '--session', default: null },
|
|
516
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
517
|
+
token: { flag: '--token', default: null },
|
|
518
|
+
claim: { flag: '--claim', default: null },
|
|
519
|
+
leaseHours: { flag: '--lease-hours', default: '24' },
|
|
520
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
521
|
+
});
|
|
522
|
+
const [swarmId] = opts.positional;
|
|
523
|
+
if (!swarmId) {
|
|
524
|
+
throw new Error('swarm join: usage — momentum swarm join <swarm-id> [--token <token>] [--claim <repo>] [--session <id>]');
|
|
525
|
+
}
|
|
526
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
527
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
528
|
+
const ts = nowIso();
|
|
529
|
+
const leaseMs = Number(opts.leaseHours) * 60 * 60 * 1000;
|
|
530
|
+
try {
|
|
531
|
+
const r = joinLib.join({
|
|
532
|
+
ecosystemRoot, swarmId, sessionId, nowIso: ts,
|
|
533
|
+
token: opts.token, claim: opts.claim,
|
|
534
|
+
leaseMs,
|
|
535
|
+
});
|
|
536
|
+
if (opts.json) {
|
|
537
|
+
process.stdout.write(JSON.stringify({
|
|
538
|
+
swarmId: r.swarmId,
|
|
539
|
+
sessionId: r.sessionId,
|
|
540
|
+
sessions: r.sessions,
|
|
541
|
+
claimed: r.claimed,
|
|
542
|
+
token: r.token ? { kind: r.token.kind, target_repo: r.token.target_repo, swarm_id: r.token.swarm_id } : null,
|
|
543
|
+
}, null, 2) + '\n');
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
console.log(`▸ join ${swarmId} as session ${sessionId}`);
|
|
547
|
+
console.log(` Sessions in this swarm: ${r.sessions.length}`);
|
|
548
|
+
if (r.token) {
|
|
549
|
+
console.log(` Token consumed: kind=${r.token.kind}${r.token.target_repo ? ` target=${r.token.target_repo}` : ''}`);
|
|
550
|
+
}
|
|
551
|
+
if (r.claimed) {
|
|
552
|
+
console.log(` Claimed: ${r.claimed.repo} → ${r.claimed.owner} (lease until ${r.claimed.lease_expires_at})`);
|
|
553
|
+
} else {
|
|
554
|
+
console.log(` (registration only — use --claim <repo> to take ownership)`);
|
|
555
|
+
}
|
|
556
|
+
} catch (err) {
|
|
557
|
+
if (err.code === 'EOWNERSHIP') {
|
|
558
|
+
console.error(`✗ join ${swarmId} --claim rejected: ${err.decision.reason}`);
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
if (/expired/.test(err.message) || /not found/.test(err.message)) {
|
|
562
|
+
console.error(`✗ join ${swarmId}: ${err.message}`);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function cmdFocus(args) {
|
|
570
|
+
const opts = parseFlags(args, {
|
|
571
|
+
sessionId: { flag: '--session', default: null },
|
|
572
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
573
|
+
expiresMin: { flag: '--expires-min', default: '60' },
|
|
574
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
575
|
+
});
|
|
576
|
+
const [swarmId, repoId] = opts.positional;
|
|
577
|
+
if (!swarmId || !repoId) {
|
|
578
|
+
throw new Error('swarm focus: usage — momentum swarm focus <swarm-id> <repo> [--session <id>] [--expires-min 60]');
|
|
579
|
+
}
|
|
580
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
581
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
582
|
+
const ts = nowIso();
|
|
583
|
+
const expiresInMs = Number(opts.expiresMin) * 60 * 1000;
|
|
584
|
+
try {
|
|
585
|
+
const r = focusLib.focus({
|
|
586
|
+
ecosystemRoot, swarmId, repo: repoId, sessionId, nowIso: ts,
|
|
587
|
+
expiresInMs,
|
|
588
|
+
});
|
|
589
|
+
if (opts.json) {
|
|
590
|
+
process.stdout.write(JSON.stringify({
|
|
591
|
+
swarmId, repo: repoId, token: r.token.token,
|
|
592
|
+
expires_at: r.token.expires_at, signal_id: r.signal.signal_id,
|
|
593
|
+
directive: r.directive,
|
|
594
|
+
}, null, 2) + '\n');
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
console.log(`▸ focus ${swarmId}/${repoId} — token issued`);
|
|
598
|
+
console.log(` Token: ${r.token.token}`);
|
|
599
|
+
console.log(` Expires: ${r.token.expires_at}`);
|
|
600
|
+
console.log(` Owner: → ${'_focusing'} (transitional)`);
|
|
601
|
+
console.log(` Signal: ${r.signal.signal_id}`);
|
|
602
|
+
console.log('');
|
|
603
|
+
console.log(`▸ Spawn directive — run in a second terminal:`);
|
|
604
|
+
console.log(` ${r.directive.command} ${r.directive.args.join(' ')}`);
|
|
605
|
+
console.log(` Then issue inside that session:`);
|
|
606
|
+
console.log(` momentum swarm join ${swarmId} --token ${r.token.token}`);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
if (err.code === 'EOWNERSHIP') {
|
|
609
|
+
console.error(`✗ focus ${swarmId}/${repoId} rejected: ${err.decision.reason}`);
|
|
610
|
+
console.error(` Only the current owner may focus a repo. Try /swarm claim first.`);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
throw err;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function cmdRelease(args) {
|
|
618
|
+
const opts = parseFlags(args, {
|
|
619
|
+
sessionId: { flag: '--session', default: null },
|
|
620
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
621
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
622
|
+
});
|
|
623
|
+
const [swarmId, repoId] = opts.positional;
|
|
624
|
+
if (!swarmId || !repoId) {
|
|
625
|
+
throw new Error('swarm release: usage — momentum swarm release <swarm-id> <repo> [--session <id>]');
|
|
626
|
+
}
|
|
627
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
628
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
629
|
+
const ts = nowIso();
|
|
630
|
+
// Idempotent fast-path: if already UNCLAIMED, no-op
|
|
631
|
+
const manifest = manifestLib.loadManifest(ecosystemRoot, swarmId);
|
|
632
|
+
if (!manifest) throw new Error(`swarm release: no manifest for ${swarmId}`);
|
|
633
|
+
if (!manifest.repos[repoId]) throw new Error(`swarm release: ${repoId} not in swarm ${swarmId}`);
|
|
634
|
+
if (manifest.repos[repoId].owner === manifestLib.UNCLAIMED) {
|
|
635
|
+
if (opts.json) {
|
|
636
|
+
process.stdout.write(JSON.stringify({
|
|
637
|
+
swarmId, repo: repoId, owner: manifestLib.UNCLAIMED, noop: true,
|
|
638
|
+
}, null, 2) + '\n');
|
|
639
|
+
} else {
|
|
640
|
+
console.log(`▸ release ${swarmId}/${repoId}: already unclaimed (no-op)`);
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
manifestLib.updateManifestAsOwner({
|
|
646
|
+
ecosystemRoot, swarmId, sessionId, repo: repoId, nowIso: ts,
|
|
647
|
+
mutate: (m) => {
|
|
648
|
+
const previousOwner = m.repos[repoId].owner;
|
|
649
|
+
m.repos[repoId].owner = manifestLib.UNCLAIMED;
|
|
650
|
+
// Drop the lease — UNCLAIMED owner means anyone may claim
|
|
651
|
+
delete m.repos[repoId].lease_expires_at;
|
|
652
|
+
delete m.repos[repoId].lease_renewed_at;
|
|
653
|
+
delete m.repos[repoId].claimed_by_session;
|
|
654
|
+
if (!Array.isArray(m.audit)) m.audit = [];
|
|
655
|
+
m.audit.push({
|
|
656
|
+
ts, actor: sessionId, event: 'release', repo: repoId,
|
|
657
|
+
detail: `${previousOwner} → ${manifestLib.UNCLAIMED}`,
|
|
658
|
+
});
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, ts);
|
|
662
|
+
if (opts.json) {
|
|
663
|
+
process.stdout.write(JSON.stringify({
|
|
664
|
+
swarmId, repo: repoId, owner: manifestLib.UNCLAIMED,
|
|
665
|
+
}, null, 2) + '\n');
|
|
666
|
+
} else {
|
|
667
|
+
console.log(`▸ release ${swarmId}/${repoId} → unclaimed`);
|
|
668
|
+
}
|
|
669
|
+
} catch (err) {
|
|
670
|
+
if (err.code === 'EOWNERSHIP') {
|
|
671
|
+
console.error(`✗ release ${swarmId}/${repoId} rejected: ${err.decision.reason}`);
|
|
672
|
+
console.error(` Only the current owner may release a repo.`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
throw err;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function cmdBudget(args) {
|
|
680
|
+
const opts = parseFlags(args, {
|
|
681
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
682
|
+
});
|
|
683
|
+
const [swarmId, repoId, deltaStr] = opts.positional;
|
|
684
|
+
if (!swarmId || !repoId || !deltaStr) {
|
|
685
|
+
throw new Error('swarm budget: usage — momentum swarm budget <swarm-id> <repo> +N');
|
|
686
|
+
}
|
|
687
|
+
const sign = deltaStr.startsWith('+') ? 1 : (deltaStr.startsWith('-') ? -1 : 1);
|
|
688
|
+
const delta = sign * parseInt(deltaStr.replace(/^[+-]/, ''), 10);
|
|
689
|
+
if (!Number.isFinite(delta)) throw new Error(`swarm budget: invalid delta ${deltaStr}`);
|
|
690
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
691
|
+
const ts = nowIso();
|
|
692
|
+
manifestLib.updateManifest(ecosystemRoot, swarmId, (m) => {
|
|
693
|
+
if (!m.repos[repoId]) throw new Error(`swarm budget: ${repoId} not a member of ${swarmId}`);
|
|
694
|
+
const cur = m.repos[repoId].tokens_budget || conductor.DEFAULT_TOKEN_BUDGET;
|
|
695
|
+
const next = Math.max(1, cur + delta);
|
|
696
|
+
m.repos[repoId].tokens_budget = next;
|
|
697
|
+
if (!Array.isArray(m.audit)) m.audit = [];
|
|
698
|
+
m.audit.push({
|
|
699
|
+
ts, actor: 'conductor', event: 'budget', repo: repoId,
|
|
700
|
+
detail: `${cur} → ${next}`,
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, ts);
|
|
704
|
+
console.log(`▸ budget(${repoId}) adjusted by ${delta}`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
708
|
+
// Verifier + completion
|
|
709
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
function verifySwarm(ecosystemRoot, swarmId) {
|
|
712
|
+
const issues = [];
|
|
713
|
+
const notes = [];
|
|
714
|
+
|
|
715
|
+
const manifest = manifestLib.loadManifest(ecosystemRoot, swarmId);
|
|
716
|
+
if (!manifest) {
|
|
717
|
+
return { ok: false, issues: [`no manifest for ${swarmId}`], notes };
|
|
718
|
+
}
|
|
719
|
+
const mv = manifestLib.validateManifest(manifest);
|
|
720
|
+
if (!mv.ok) {
|
|
721
|
+
for (const e of mv.errors) issues.push(`manifest ${e.path}: ${e.message}`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Initiative back-reference
|
|
725
|
+
const initiativeDir = path.join(ecosystemRoot, 'initiatives');
|
|
726
|
+
const initiativeOk = fs.existsSync(initiativeDir) &&
|
|
727
|
+
fs.readdirSync(initiativeDir).some((n) => n.endsWith(`-${manifest.initiative}.md`));
|
|
728
|
+
if (!initiativeOk) {
|
|
729
|
+
issues.push(`initiative "${manifest.initiative}" not found at ${initiativeDir}`);
|
|
730
|
+
} else {
|
|
731
|
+
notes.push(`initiative ${manifest.initiative} resolved`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Brief back-references
|
|
735
|
+
const ecoMfst = ecosystemLib.loadManifest(ecosystemRoot);
|
|
736
|
+
for (const [repoId, repo] of Object.entries(manifest.repos || {})) {
|
|
737
|
+
const repoPath = conductor.resolveMemberPath(ecosystemRoot, repoId, ecoMfst);
|
|
738
|
+
if (!repoPath) {
|
|
739
|
+
issues.push(`repo "${repoId}" not in ecosystem manifest`);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const briefPath = path.join(repoPath, 'specs', 'phases', repo.phase_slug, 'overview.md');
|
|
743
|
+
if (!fs.existsSync(briefPath)) {
|
|
744
|
+
issues.push(`brief missing: ${briefPath}`);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const { frontmatter } = briefLib.parseSwarmFrontmatter(fs.readFileSync(briefPath, 'utf8'));
|
|
748
|
+
if (!frontmatter || !frontmatter.swarm) {
|
|
749
|
+
issues.push(`brief ${briefPath} has no swarm frontmatter`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (frontmatter.swarm !== swarmId) {
|
|
753
|
+
issues.push(`brief ${briefPath} swarm=${frontmatter.swarm} mismatches ${swarmId}`);
|
|
754
|
+
}
|
|
755
|
+
if (frontmatter.wave !== repo.wave) {
|
|
756
|
+
issues.push(`brief ${briefPath} wave=${frontmatter.wave} mismatches manifest wave ${repo.wave}`);
|
|
757
|
+
}
|
|
758
|
+
if (frontmatter.initiative !== manifest.initiative) {
|
|
759
|
+
issues.push(`brief ${briefPath} initiative=${frontmatter.initiative} mismatches ${manifest.initiative}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Contract shape — every contract artifact must validate against schema basics
|
|
764
|
+
const contractsDir = path.join(manifestLib.swarmDir(ecosystemRoot, swarmId), 'contracts');
|
|
765
|
+
if (fs.existsSync(contractsDir)) {
|
|
766
|
+
for (const file of fs.readdirSync(contractsDir)) {
|
|
767
|
+
if (!file.endsWith('.contract.json')) continue;
|
|
768
|
+
const filePath = path.join(contractsDir, file);
|
|
769
|
+
try {
|
|
770
|
+
const c = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
771
|
+
if (typeof c.surface !== 'string') issues.push(`${file}: missing surface`);
|
|
772
|
+
if (typeof c.owner !== 'string') issues.push(`${file}: missing owner`);
|
|
773
|
+
if (!Array.isArray(c.consumers)) issues.push(`${file}: consumers must be array`);
|
|
774
|
+
if (!Number.isInteger(c.version) || c.version < 1) issues.push(`${file}: invalid version`);
|
|
775
|
+
if (typeof c.content_hash !== 'string') issues.push(`${file}: missing content_hash`);
|
|
776
|
+
} catch (e) {
|
|
777
|
+
issues.push(`${file}: invalid JSON — ${e.message}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Inbox — any pending items are advisories, not failures
|
|
783
|
+
const inboxDir = path.join(manifestLib.swarmDir(ecosystemRoot, swarmId), 'inbox');
|
|
784
|
+
if (fs.existsSync(inboxDir)) {
|
|
785
|
+
const pending = fs.readdirSync(inboxDir).filter((n) => /^\d{4}-[a-z][a-z0-9-]*\.md$/.test(n));
|
|
786
|
+
if (pending.length) notes.push(`${pending.length} pending inbox item(s)`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return { ok: issues.length === 0, issues, notes };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function completeSwarm(ecosystemRoot, swarmId, ts) {
|
|
793
|
+
const manifest = manifestLib.loadManifest(ecosystemRoot, swarmId);
|
|
794
|
+
if (!manifest) throw new Error(`completeSwarm: no manifest for ${swarmId}`);
|
|
795
|
+
|
|
796
|
+
const repos = Object.entries(manifest.repos || {});
|
|
797
|
+
const reposComplete = repos.filter(([, r]) => r.status === 'complete').length;
|
|
798
|
+
const reposTotal = repos.length;
|
|
799
|
+
|
|
800
|
+
// Write cross-repo changeset to <eco>/changes/<id>.md
|
|
801
|
+
const changesDir = path.join(ecosystemRoot, 'changes');
|
|
802
|
+
fs.mkdirSync(changesDir, { recursive: true });
|
|
803
|
+
const changesetPath = path.join(changesDir, `${swarmId}.md`);
|
|
804
|
+
const lines = [
|
|
805
|
+
`# Swarm ${swarmId} — cross-repo changeset`,
|
|
806
|
+
'',
|
|
807
|
+
`- Initiative: ${manifest.initiative}`,
|
|
808
|
+
`- Mode: ${manifest.mode}`,
|
|
809
|
+
`- Status: ${manifest.status || 'running'}`,
|
|
810
|
+
`- Repos: ${reposComplete}/${reposTotal} complete`,
|
|
811
|
+
`- Completed at: ${ts}`,
|
|
812
|
+
'',
|
|
813
|
+
'## Per-repo contributions',
|
|
814
|
+
'',
|
|
815
|
+
];
|
|
816
|
+
for (const [repoId, repo] of repos) {
|
|
817
|
+
lines.push(`### ${repoId}`);
|
|
818
|
+
lines.push(`- Wave: ${repo.wave}`);
|
|
819
|
+
lines.push(`- Branch: \`${repo.branch}\``);
|
|
820
|
+
lines.push(`- Phase: \`${repo.phase_slug}\``);
|
|
821
|
+
lines.push(`- Tasks: ${repo.tasks_done || 0}/${repo.tasks_total || 0}`);
|
|
822
|
+
lines.push(`- Commits: ${repo.commits || 0}`);
|
|
823
|
+
lines.push('');
|
|
824
|
+
}
|
|
825
|
+
fs.writeFileSync(changesetPath, lines.join('\n'), 'utf8');
|
|
826
|
+
|
|
827
|
+
manifestLib.updateManifest(ecosystemRoot, swarmId, (m) => {
|
|
828
|
+
m.status = reposComplete === reposTotal ? 'complete' : (m.status || 'running');
|
|
829
|
+
if (!Array.isArray(m.audit)) m.audit = [];
|
|
830
|
+
m.audit.push({
|
|
831
|
+
ts, actor: 'conductor', event: 'complete',
|
|
832
|
+
detail: `${reposComplete}/${reposTotal} repos complete`,
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, ts);
|
|
836
|
+
|
|
837
|
+
return { changesetPath, reposComplete, reposTotal };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
841
|
+
// Adapter spawn dispatch (Phase 18)
|
|
842
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Resolve the adapter module for a given platform id. Used by spawn
|
|
846
|
+
* dispatch so the CLI floor stays platform-agnostic — `bin/swarm.js`
|
|
847
|
+
* never imports any adapter directly.
|
|
848
|
+
*/
|
|
849
|
+
function loadAdapterForPlatform(platform) {
|
|
850
|
+
const adapterPath = path.join(MOMENTUM_ROOT, 'adapters', platform, 'adapter.js');
|
|
851
|
+
if (!fs.existsSync(adapterPath)) {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
// eslint-disable-next-line global-require
|
|
855
|
+
return require(adapterPath);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Dispatch each directive to `adapter.spawn(directive)`. The directive's
|
|
860
|
+
* `platform` field selects the adapter; an unknown platform or missing
|
|
861
|
+
* `spawn` function yields a canonical `status: -1` entry so the wave
|
|
862
|
+
* stays robust to per-repo dispatch failures. See `bin/momentum.js` for
|
|
863
|
+
* the directive contract.
|
|
864
|
+
*/
|
|
865
|
+
function spawnSupervisors(directives) {
|
|
866
|
+
const results = [];
|
|
867
|
+
for (const d of directives) {
|
|
868
|
+
const adapter = loadAdapterForPlatform(d.platform);
|
|
869
|
+
if (!adapter) {
|
|
870
|
+
results.push({
|
|
871
|
+
repoId: d.repoId, status: -1,
|
|
872
|
+
detail: `unknown platform: ${d.platform}`,
|
|
873
|
+
});
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (typeof adapter.spawn !== 'function') {
|
|
877
|
+
results.push({
|
|
878
|
+
repoId: d.repoId, status: -1,
|
|
879
|
+
detail: `adapter ${d.platform} does not export spawn()`,
|
|
880
|
+
});
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
results.push(adapter.spawn(d));
|
|
884
|
+
}
|
|
885
|
+
return results;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
889
|
+
// Renderer
|
|
890
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
891
|
+
|
|
892
|
+
function renderBoardTable(board) {
|
|
893
|
+
console.log(`▸ Swarm ${board.swarm_id} — mode=${board.mode} status=${board.status} wave=${board.active_wave}`);
|
|
894
|
+
console.log('');
|
|
895
|
+
const header = ['repo', 'wave', 'status', 'tasks', 'tokens', 'commits'];
|
|
896
|
+
const widths = header.map((h) => h.length);
|
|
897
|
+
const rows = [];
|
|
898
|
+
for (const r of board.repos) {
|
|
899
|
+
const row = [
|
|
900
|
+
r.name, String(r.wave), r.status,
|
|
901
|
+
r.tasks || '', r.tokens || '', String(r.commits || 0),
|
|
902
|
+
];
|
|
903
|
+
rows.push(row);
|
|
904
|
+
for (let i = 0; i < row.length; i++) widths[i] = Math.max(widths[i], row[i].length);
|
|
905
|
+
}
|
|
906
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ');
|
|
907
|
+
console.log(' ' + fmt(header));
|
|
908
|
+
console.log(' ' + widths.map((w) => '-'.repeat(w)).join(' '));
|
|
909
|
+
for (const row of rows) console.log(' ' + fmt(row));
|
|
910
|
+
if (board.recent_activity && board.recent_activity.length) {
|
|
911
|
+
console.log('');
|
|
912
|
+
console.log(' Recent activity:');
|
|
913
|
+
for (const a of board.recent_activity) {
|
|
914
|
+
console.log(` ${a.ts} ${a.repo ? a.repo + ' ' : ''}${a.msg}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (board.inbox_count > 0) {
|
|
918
|
+
console.log('');
|
|
919
|
+
console.log(` ▸ ${board.inbox_count} pending inbox item(s)`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
924
|
+
// Entrypoint
|
|
925
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
926
|
+
|
|
927
|
+
function printUsage() {
|
|
928
|
+
console.log(`momentum swarm — cross-repo work units (Phase 17 v0.20.0, Claude Code only)
|
|
929
|
+
|
|
930
|
+
Usage:
|
|
931
|
+
momentum swarm start <slug> --initiative <slug> --repos r1,r2,... --phase <phase-slug> [--mode autopilot|checkpoint] [--spawn]
|
|
932
|
+
momentum swarm status <swarm-id> [--json]
|
|
933
|
+
momentum swarm tell <swarm-id> <repo> "<text>"
|
|
934
|
+
momentum swarm broadcast <swarm-id> "<text>"
|
|
935
|
+
momentum swarm verify <swarm-id> [--json]
|
|
936
|
+
momentum swarm complete <swarm-id>
|
|
937
|
+
momentum swarm resume <swarm-id> [--session <id>]
|
|
938
|
+
momentum swarm cancel <swarm-id> [--reason "<text>"]
|
|
939
|
+
momentum swarm budget <swarm-id> <repo> +N | -N
|
|
940
|
+
momentum swarm claim <swarm-id> <repo> [--session <id>] [--lease-hours 24]
|
|
941
|
+
momentum swarm release <swarm-id> <repo> [--session <id>]
|
|
942
|
+
momentum swarm focus <swarm-id> <repo> [--session <id>] [--expires-min 60]
|
|
943
|
+
momentum swarm join <swarm-id> [--token <token>] [--claim <repo>] [--session <id>]
|
|
944
|
+
momentum swarm absorb <target-swarm-id> <source-swarm-id> [--yes] [--session <id>]
|
|
945
|
+
momentum swarm inbox list <swarm-id>
|
|
946
|
+
momentum swarm inbox write <swarm-id> --repo <r> --slug <s> --question "<text>"
|
|
947
|
+
momentum swarm inbox resolve <swarm-id> <id> --answer "<text>"
|
|
948
|
+
momentum swarm preview-merge <swarm-id> [--json]
|
|
949
|
+
|
|
950
|
+
Global flags:
|
|
951
|
+
--ecosystem <path> Override ecosystem root resolution
|
|
952
|
+
`);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function cmdInboxList(args) {
|
|
956
|
+
const opts = parseFlags(args, {
|
|
957
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
958
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
959
|
+
});
|
|
960
|
+
const [swarmId] = opts.positional;
|
|
961
|
+
if (!swarmId) throw new Error('swarm inbox list: <swarm-id> required');
|
|
962
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
963
|
+
const items = inboxLib.listPendingInboxItems(ecosystemRoot, swarmId);
|
|
964
|
+
if (opts.json) {
|
|
965
|
+
process.stdout.write(JSON.stringify(items, null, 2) + '\n');
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (items.length === 0) {
|
|
969
|
+
console.log(`▸ inbox(${swarmId}): no pending items`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
console.log(`▸ inbox(${swarmId}): ${items.length} pending`);
|
|
973
|
+
for (const it of items) {
|
|
974
|
+
console.log(` ${it.id} ${it.repo} ${it.slug} (${it.asked})`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function cmdInboxWrite(args) {
|
|
979
|
+
const opts = parseFlags(args, {
|
|
980
|
+
repo: { flag: '--repo' },
|
|
981
|
+
slug: { flag: '--slug' },
|
|
982
|
+
question: { flag: '--question' },
|
|
983
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
984
|
+
});
|
|
985
|
+
const [swarmId] = opts.positional;
|
|
986
|
+
if (!swarmId || !opts.repo || !opts.slug || !opts.question) {
|
|
987
|
+
throw new Error('swarm inbox write: usage — momentum swarm inbox write <swarm-id> --repo <r> --slug <s> --question "<text>"');
|
|
988
|
+
}
|
|
989
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
990
|
+
const r = inboxLib.writeInboxItem({
|
|
991
|
+
ecosystemRoot, swarmId,
|
|
992
|
+
repo: opts.repo, slug: opts.slug, question: opts.question,
|
|
993
|
+
nowIso: nowIso(),
|
|
994
|
+
});
|
|
995
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, nowIso());
|
|
996
|
+
console.log(`▸ inbox(${swarmId}) ${r.id}-${r.slug}.md written`);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function cmdInboxResolve(args) {
|
|
1000
|
+
const opts = parseFlags(args, {
|
|
1001
|
+
answer: { flag: '--answer' },
|
|
1002
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
1003
|
+
});
|
|
1004
|
+
const [swarmId, id] = opts.positional;
|
|
1005
|
+
if (!swarmId || !id || !opts.answer) {
|
|
1006
|
+
throw new Error('swarm inbox resolve: usage — momentum swarm inbox resolve <swarm-id> <id> --answer "<text>"');
|
|
1007
|
+
}
|
|
1008
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
1009
|
+
const r = inboxLib.resolveInboxItem({
|
|
1010
|
+
ecosystemRoot, swarmId, id, answer: opts.answer, nowIso: nowIso(),
|
|
1011
|
+
});
|
|
1012
|
+
boardLib.refreshBoard(ecosystemRoot, swarmId, nowIso());
|
|
1013
|
+
console.log(`▸ inbox(${swarmId}) ${r.id} resolved → ${path.relative(ecosystemRoot, r.resolvedPath)}`);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function cmdInbox(args) {
|
|
1017
|
+
if (!args || args.length === 0 || args[0] === '--help') {
|
|
1018
|
+
console.log('momentum swarm inbox — supervisor questions to the conductor');
|
|
1019
|
+
console.log('Usage:');
|
|
1020
|
+
console.log(' momentum swarm inbox list <swarm-id>');
|
|
1021
|
+
console.log(' momentum swarm inbox write <swarm-id> --repo <r> --slug <s> --question "<text>"');
|
|
1022
|
+
console.log(' momentum swarm inbox resolve <swarm-id> <id> --answer "<text>"');
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const sub = args[0];
|
|
1026
|
+
const rest = args.slice(1);
|
|
1027
|
+
switch (sub) {
|
|
1028
|
+
case 'list': return cmdInboxList(rest);
|
|
1029
|
+
case 'write': return cmdInboxWrite(rest);
|
|
1030
|
+
case 'resolve': return cmdInboxResolve(rest);
|
|
1031
|
+
default:
|
|
1032
|
+
throw new Error(`swarm inbox: unknown subcommand "${sub}". Try --help.`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function cmdPreviewMerge(args) {
|
|
1037
|
+
const opts = parseFlags(args, {
|
|
1038
|
+
ecosystem: { flag: '--ecosystem', default: null },
|
|
1039
|
+
json: { flag: '--json', type: 'bool', default: false },
|
|
1040
|
+
});
|
|
1041
|
+
const [swarmId] = opts.positional;
|
|
1042
|
+
if (!swarmId) throw new Error('swarm preview-merge: <swarm-id> required');
|
|
1043
|
+
const ecosystemRoot = resolveEcosystemRoot(opts.ecosystem);
|
|
1044
|
+
const manifest = manifestLib.loadManifest(ecosystemRoot, swarmId);
|
|
1045
|
+
if (!manifest) throw new Error(`swarm preview-merge: no manifest for ${swarmId}`);
|
|
1046
|
+
|
|
1047
|
+
const entries = [];
|
|
1048
|
+
for (const [repoId, repo] of Object.entries(manifest.repos || {})) {
|
|
1049
|
+
const repoPath = conductor.resolveMemberPath(ecosystemRoot, repoId);
|
|
1050
|
+
if (!repoPath) continue;
|
|
1051
|
+
entries.push({
|
|
1052
|
+
repo: repoId, repoPath,
|
|
1053
|
+
branch: repo.branch,
|
|
1054
|
+
intoBranch: 'main',
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
const results = preMergeLib.previewMergeBatch(entries);
|
|
1058
|
+
if (opts.json) {
|
|
1059
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
for (const r of results) {
|
|
1063
|
+
console.log(` ${r.repo}: ${r.result.ok ? 'CLEAN' : 'CONFLICTS'} — ${r.result.summary}`);
|
|
1064
|
+
for (const f of r.result.conflictedFiles) console.log(` ✗ ${f}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function runSwarm(args) {
|
|
1069
|
+
if (!args || args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
1070
|
+
return printUsage();
|
|
1071
|
+
}
|
|
1072
|
+
const sub = args[0];
|
|
1073
|
+
const rest = args.slice(1);
|
|
1074
|
+
switch (sub) {
|
|
1075
|
+
case 'start': return cmdStart(rest);
|
|
1076
|
+
case 'status': return cmdStatus(rest);
|
|
1077
|
+
case 'tell': return cmdTell(rest);
|
|
1078
|
+
case 'broadcast': return cmdBroadcast(rest);
|
|
1079
|
+
case 'verify': return cmdVerify(rest);
|
|
1080
|
+
case 'complete': return cmdComplete(rest);
|
|
1081
|
+
case 'resume': return cmdResume(rest);
|
|
1082
|
+
case 'cancel': return cmdCancel(rest);
|
|
1083
|
+
case 'budget': return cmdBudget(rest);
|
|
1084
|
+
case 'claim': return cmdClaim(rest);
|
|
1085
|
+
case 'release': return cmdRelease(rest);
|
|
1086
|
+
case 'focus': return cmdFocus(rest);
|
|
1087
|
+
case 'join': return cmdJoin(rest);
|
|
1088
|
+
case 'absorb': return cmdAbsorb(rest);
|
|
1089
|
+
case 'inbox': return cmdInbox(rest);
|
|
1090
|
+
case 'preview-merge': return cmdPreviewMerge(rest);
|
|
1091
|
+
default:
|
|
1092
|
+
throw new Error(`swarm: unknown subcommand "${sub}". Try --help.`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
module.exports = {
|
|
1097
|
+
runSwarm,
|
|
1098
|
+
// Exposed for tests
|
|
1099
|
+
verifySwarm,
|
|
1100
|
+
completeSwarm,
|
|
1101
|
+
spawnSupervisors,
|
|
1102
|
+
loadAdapterForPlatform,
|
|
1103
|
+
resolveEcosystemRoot,
|
|
1104
|
+
};
|