@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,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Repo state detection — the contract Phase 10's `momentum doctor`
|
|
5
|
+
* and entry/exit commands depend on.
|
|
6
|
+
*
|
|
7
|
+
* A momentum-managed directory is in exactly one state. Knowing the
|
|
8
|
+
* state lets us decide what commands the user can run next.
|
|
9
|
+
*
|
|
10
|
+
* States:
|
|
11
|
+
* standalone — momentum-installed; not in any ecosystem.
|
|
12
|
+
* member — momentum-installed; pointer present; listed
|
|
13
|
+
* in a reachable ecosystem's manifest.
|
|
14
|
+
* leader — ecosystem root (has ecosystem.json); not
|
|
15
|
+
* itself a member of another ecosystem.
|
|
16
|
+
* leader-and-member — ecosystem root that is ALSO a member of a
|
|
17
|
+
* different ecosystem (rare; legal).
|
|
18
|
+
* broken-manifest — has ecosystem.json that fails validation.
|
|
19
|
+
* broken-pointer — has pointer block but is NOT registered
|
|
20
|
+
* in any reachable ecosystem.
|
|
21
|
+
* broken-orphan — registered in an ecosystem but the pointer
|
|
22
|
+
* block is missing from the primary file.
|
|
23
|
+
*
|
|
24
|
+
* `availableTransitions(state)` returns user-facing next-step commands
|
|
25
|
+
* for the doctor recipe to print as teaching output.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const lib = require('./index');
|
|
32
|
+
const pointer = require('./pointer');
|
|
33
|
+
|
|
34
|
+
const MAX_PARENT_WALK_DEFAULT = 5;
|
|
35
|
+
const MAX_PARENT_WALK_ENV = 'MOMENTUM_MAX_PARENT_WALK';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the active parent-walk limit, honoring `MOMENTUM_MAX_PARENT_WALK`
|
|
39
|
+
* env override. Negative/non-numeric values fall back to the default.
|
|
40
|
+
*/
|
|
41
|
+
function getMaxParentWalk() {
|
|
42
|
+
const raw = process.env[MAX_PARENT_WALK_ENV];
|
|
43
|
+
if (raw === undefined || raw === '') return MAX_PARENT_WALK_DEFAULT;
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
if (!Number.isFinite(n) || n < 0) return MAX_PARENT_WALK_DEFAULT;
|
|
46
|
+
return Math.floor(n);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Detect the current state of `repoPath`. Inspects four signals:
|
|
51
|
+
* - ecosystem.json presence + validity here
|
|
52
|
+
* - pointer block in primary instruction file
|
|
53
|
+
* - membership in any reachable ecosystem (walks up + scans siblings)
|
|
54
|
+
*
|
|
55
|
+
* Returns one of the seven state strings documented at the top.
|
|
56
|
+
*
|
|
57
|
+
* Note on cost: this calls `loadManifest` + `validateManifest` for any
|
|
58
|
+
* ecosystem.json in scope. For doctor-style invocations the overhead
|
|
59
|
+
* is irrelevant. State is not cached; callers cache if needed.
|
|
60
|
+
*/
|
|
61
|
+
function detectState(repoPath) {
|
|
62
|
+
const abs = path.resolve(repoPath);
|
|
63
|
+
|
|
64
|
+
// 1. Is this directory itself an ecosystem root?
|
|
65
|
+
let isLeader = false;
|
|
66
|
+
let leaderBroken = false;
|
|
67
|
+
const localManifestPath = path.join(abs, lib.MANIFEST_FILENAME);
|
|
68
|
+
if (safeIsFile(localManifestPath)) {
|
|
69
|
+
isLeader = true;
|
|
70
|
+
try {
|
|
71
|
+
const manifest = lib.loadManifest(abs);
|
|
72
|
+
const v = lib.validateManifest(manifest);
|
|
73
|
+
if (!v.ok) leaderBroken = true;
|
|
74
|
+
} catch (_err) {
|
|
75
|
+
leaderBroken = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Pointer block present?
|
|
80
|
+
const hasPtr = pointer.hasPointerBlock(abs);
|
|
81
|
+
|
|
82
|
+
// 3. Is this directory registered as a member of some reachable ecosystem?
|
|
83
|
+
const registration = findRegistration(abs);
|
|
84
|
+
const isRegistered = registration !== null;
|
|
85
|
+
|
|
86
|
+
// Combine signals.
|
|
87
|
+
if (leaderBroken) return 'broken-manifest';
|
|
88
|
+
if (isLeader && hasPtr && isRegistered) return 'leader-and-member';
|
|
89
|
+
if (isLeader) return 'leader';
|
|
90
|
+
if (hasPtr && isRegistered) return 'member';
|
|
91
|
+
if (hasPtr && !isRegistered) return 'broken-pointer';
|
|
92
|
+
if (!hasPtr && isRegistered) return 'broken-orphan';
|
|
93
|
+
return 'standalone';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Walk up looking for ecosystem.json files, scanning each manifest's
|
|
98
|
+
* members[].path to see if `repoAbs` is registered. Returns
|
|
99
|
+
* { rootPath, ecosystemName, memberId, memberRole }
|
|
100
|
+
* on first match, or null. Bounded by MAX_PARENT_WALK.
|
|
101
|
+
*
|
|
102
|
+
* The walk pattern mirrors session-append.sh: an ecosystem is a SIBLING
|
|
103
|
+
* of its members, so from a member we walk up and check each sibling
|
|
104
|
+
* for ecosystem.json (and also check `current` itself in case the caller
|
|
105
|
+
* is in the ecosystem root).
|
|
106
|
+
*/
|
|
107
|
+
function findRegistration(repoAbs) {
|
|
108
|
+
const max = getMaxParentWalk();
|
|
109
|
+
let current = repoAbs;
|
|
110
|
+
for (let depth = 0; depth <= max; depth++) {
|
|
111
|
+
// Check current dir's siblings.
|
|
112
|
+
const parent = path.dirname(current);
|
|
113
|
+
let siblings = [];
|
|
114
|
+
try {
|
|
115
|
+
siblings = fs.readdirSync(parent, { withFileTypes: true })
|
|
116
|
+
.filter((e) => e.isDirectory())
|
|
117
|
+
.map((e) => path.join(parent, e.name));
|
|
118
|
+
} catch (_err) {
|
|
119
|
+
// unreadable parent — skip siblings, keep walking
|
|
120
|
+
}
|
|
121
|
+
for (const sib of siblings) {
|
|
122
|
+
const reg = matchInManifest(sib, repoAbs);
|
|
123
|
+
if (reg) return reg;
|
|
124
|
+
}
|
|
125
|
+
// Also check `current` itself (handles leader+member case).
|
|
126
|
+
const selfReg = matchInManifest(current, repoAbs);
|
|
127
|
+
if (selfReg) return selfReg;
|
|
128
|
+
|
|
129
|
+
if (parent === current) break;
|
|
130
|
+
current = parent;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function matchInManifest(candidateRoot, repoAbs) {
|
|
136
|
+
const manifestPath = path.join(candidateRoot, lib.MANIFEST_FILENAME);
|
|
137
|
+
if (!safeIsFile(manifestPath)) return null;
|
|
138
|
+
let manifest;
|
|
139
|
+
try {
|
|
140
|
+
manifest = lib.loadManifest(candidateRoot);
|
|
141
|
+
} catch (_err) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (!lib.validateManifest(manifest).ok) return null;
|
|
145
|
+
for (const m of lib.listMembers(manifest)) {
|
|
146
|
+
if (!m || typeof m.path !== 'string') continue;
|
|
147
|
+
const memberAbs = path.resolve(candidateRoot, m.path);
|
|
148
|
+
if (samePath(memberAbs, repoAbs)) {
|
|
149
|
+
return {
|
|
150
|
+
rootPath: candidateRoot,
|
|
151
|
+
ecosystemName: manifest.name,
|
|
152
|
+
memberId: m.id,
|
|
153
|
+
memberRole: m.role,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function safeIsFile(p) {
|
|
161
|
+
try {
|
|
162
|
+
return fs.statSync(p).isFile();
|
|
163
|
+
} catch (_err) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Path equality that's symlink-aware. On macOS `/tmp/foo` and
|
|
170
|
+
* `/private/tmp/foo` are the same path but compare unequal as strings.
|
|
171
|
+
* realpathSync collapses both; if either side cannot be realpathed
|
|
172
|
+
* (missing file) we fall back to a strict string compare.
|
|
173
|
+
*/
|
|
174
|
+
function samePath(a, b) {
|
|
175
|
+
if (a === b) return true;
|
|
176
|
+
let ra = a;
|
|
177
|
+
let rb = b;
|
|
178
|
+
try {
|
|
179
|
+
ra = fs.realpathSync(a);
|
|
180
|
+
} catch (_e) { /* keep original */ }
|
|
181
|
+
try {
|
|
182
|
+
rb = fs.realpathSync(b);
|
|
183
|
+
} catch (_e) { /* keep original */ }
|
|
184
|
+
return ra === rb;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Teaching output for `momentum doctor`. Returns an array of
|
|
189
|
+
* { command, description }
|
|
190
|
+
* objects describing what the user can do from the given state.
|
|
191
|
+
* `context` carries any extra info detection produced (ecosystemName,
|
|
192
|
+
* memberId, etc.) so transitions can be rendered with concrete names.
|
|
193
|
+
*/
|
|
194
|
+
function availableTransitions(state, context = {}) {
|
|
195
|
+
switch (state) {
|
|
196
|
+
case 'standalone':
|
|
197
|
+
return [
|
|
198
|
+
{
|
|
199
|
+
command: 'momentum join <ecosystem-path>',
|
|
200
|
+
description: 'Attach this repo to an existing ecosystem (e.g. ../my-ecosystem).',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
command: 'momentum init --ecosystem <name>',
|
|
204
|
+
description: 'Create a new ecosystem in a sibling directory and register this repo as the first member.',
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
case 'member': {
|
|
208
|
+
const name = context.ecosystemName ? `"${context.ecosystemName}"` : '<ecosystem>';
|
|
209
|
+
return [
|
|
210
|
+
{
|
|
211
|
+
command: 'momentum leave',
|
|
212
|
+
description: `Detach this repo from ${name} (strips pointer + removes manifest entry).`,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
command: 'momentum ecosystem status',
|
|
216
|
+
description: `Show ${name}'s members and per-member git state.`,
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
case 'leader':
|
|
221
|
+
return [
|
|
222
|
+
{
|
|
223
|
+
command: 'momentum ecosystem add <repo-path>',
|
|
224
|
+
description: 'Register another momentum-installed repo as a member of this ecosystem.',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
command: 'momentum ecosystem status',
|
|
228
|
+
description: 'List members and per-member git state.',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
command: 'momentum ecosystem remove <member-id>',
|
|
232
|
+
description: 'Unregister a member (inverse of add).',
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
case 'leader-and-member': {
|
|
236
|
+
const name = context.ecosystemName ? `"${context.ecosystemName}"` : '<parent-ecosystem>';
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
command: 'momentum ecosystem add <repo-path>',
|
|
240
|
+
description: 'Register a member of THIS ecosystem.',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
command: 'momentum ecosystem status',
|
|
244
|
+
description: 'List members of THIS ecosystem.',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
command: 'momentum leave',
|
|
248
|
+
description: `Detach from parent ecosystem ${name} (keeps this dir as a leader).`,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
case 'broken-manifest':
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
command: '(fix or remove ecosystem.json)',
|
|
256
|
+
description: 'The manifest at this path fails validation. Repair it by hand, or delete it to revert to standalone.',
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
case 'broken-pointer':
|
|
260
|
+
return [
|
|
261
|
+
{
|
|
262
|
+
command: 'momentum leave',
|
|
263
|
+
description: 'Strip the dangling pointer block (recovers to standalone).',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
command: 'momentum join <ecosystem-path>',
|
|
267
|
+
description: 'Re-attach to an ecosystem if the previous one moved.',
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
case 'broken-orphan': {
|
|
271
|
+
const name = context.ecosystemName ? `"${context.ecosystemName}"` : '<ecosystem>';
|
|
272
|
+
return [
|
|
273
|
+
{
|
|
274
|
+
command: `momentum join ${context.rootPath ? path.relative(process.cwd(), context.rootPath) : '<ecosystem-path>'}`,
|
|
275
|
+
description: `Re-inject the missing pointer (you are listed in ${name} but the pointer block is gone).`,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
command: `momentum ecosystem remove ${context.memberId || '<id>'}`,
|
|
279
|
+
description: `Remove the orphan entry from ${name}'s manifest (run from the ecosystem root).`,
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
default:
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
MAX_PARENT_WALK_DEFAULT,
|
|
290
|
+
MAX_PARENT_WALK_ENV,
|
|
291
|
+
getMaxParentWalk,
|
|
292
|
+
detectState,
|
|
293
|
+
findRegistration,
|
|
294
|
+
availableTransitions,
|
|
295
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/LiminaLabsAI/momentum/schemas/ecosystem-v1.json",
|
|
4
|
+
"title": "Momentum Ecosystem Manifest",
|
|
5
|
+
"description": "Declares a set of related momentum-installed repos that coordinate as one product. Lives at the root of an ecosystem repo (sibling to its member repos).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["name", "version", "members"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"maxLength": 64,
|
|
14
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
15
|
+
"description": "Lowercase slug identifying the ecosystem. Used as the default folder name for `momentum ecosystem init`."
|
|
16
|
+
},
|
|
17
|
+
"version": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"const": 1,
|
|
20
|
+
"description": "Manifest schema version. Locked to 1 until a breaking change."
|
|
21
|
+
},
|
|
22
|
+
"created": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"format": "date",
|
|
25
|
+
"description": "ISO-8601 date the ecosystem manifest was created."
|
|
26
|
+
},
|
|
27
|
+
"members": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"description": "Repos belonging to this ecosystem. Each entry references a sibling directory on disk.",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"required": ["id", "path", "role"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"id": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"maxLength": 64,
|
|
40
|
+
"description": "Stable slug for this member. Used in dependency edges and session log entries."
|
|
41
|
+
},
|
|
42
|
+
"path": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"minLength": 1,
|
|
45
|
+
"description": "Filesystem path to the member repo, relative to the ecosystem root."
|
|
46
|
+
},
|
|
47
|
+
"role": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["platform", "client", "library", "infra", "bench", "other"],
|
|
50
|
+
"description": "Coarse role classification. Used by /track and consumers to render groupings."
|
|
51
|
+
},
|
|
52
|
+
"owns": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"items": {"type": "string"},
|
|
55
|
+
"description": "Optional list of named artifacts this member owns (e.g. `openapi.json`, `aws.account.XXX`). Free-form identifiers."
|
|
56
|
+
},
|
|
57
|
+
"consumes": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {"type": "string"},
|
|
60
|
+
"description": "Optional list of named artifacts this member consumes from siblings."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"description": "Directed edges between members. Used for cross-repo coordination (deploy order, contract drift).",
|
|
68
|
+
"items": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"required": ["from", "to", "kind"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"from": {"type": "string", "description": "Member id of the dependent."},
|
|
74
|
+
"to": {"type": "string", "description": "Member id of the dependency."},
|
|
75
|
+
"kind": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"enum": ["api-contract", "library", "deploy", "build-time", "other"],
|
|
78
|
+
"description": "Nature of the dependency."
|
|
79
|
+
},
|
|
80
|
+
"note": {"type": "string", "description": "Optional human-readable note."}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/LiminaLabsAI/momentum/schemas/initiative-v1.json",
|
|
4
|
+
"title": "Momentum Ecosystem Initiative Frontmatter",
|
|
5
|
+
"description": "YAML frontmatter shape for an initiative file (initiatives/NNNN-slug.md). The file body is free-form markdown; only the frontmatter block is schema-validated.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["id", "slug", "status", "started", "owner", "repos"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"id": {
|
|
11
|
+
"type": "integer",
|
|
12
|
+
"minimum": 1,
|
|
13
|
+
"description": "Monotonically-increasing initiative number. Allocated by `momentum ecosystem initiative create`."
|
|
14
|
+
},
|
|
15
|
+
"slug": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
18
|
+
"minLength": 1,
|
|
19
|
+
"maxLength": 64,
|
|
20
|
+
"description": "URL-safe slug used in the filename (NNNN-slug.md) and in session log references."
|
|
21
|
+
},
|
|
22
|
+
"title": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1,
|
|
25
|
+
"description": "Human-readable title. Defaults to slug if not provided."
|
|
26
|
+
},
|
|
27
|
+
"status": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"enum": ["in-progress", "closed", "abandoned"],
|
|
30
|
+
"description": "Lifecycle state. `closed` requires a non-empty closed date."
|
|
31
|
+
},
|
|
32
|
+
"started": {"type": "string", "format": "date"},
|
|
33
|
+
"closed": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"format": "date",
|
|
36
|
+
"description": "ISO-8601 date the initiative was closed. Required when status is `closed`."
|
|
37
|
+
},
|
|
38
|
+
"owner": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"minLength": 1,
|
|
41
|
+
"description": "Free-form identifier (GitHub handle, email, or name)."
|
|
42
|
+
},
|
|
43
|
+
"repos": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"minItems": 1,
|
|
46
|
+
"items": {"type": "string"},
|
|
47
|
+
"description": "Member ids from the ecosystem manifest. Repos contributing to this initiative."
|
|
48
|
+
},
|
|
49
|
+
"tags": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": {"type": "string"},
|
|
52
|
+
"description": "Optional free-form tags for filtering."
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"allOf": [
|
|
56
|
+
{
|
|
57
|
+
"if": {"properties": {"status": {"const": "closed"}}},
|
|
58
|
+
"then": {"required": ["closed"]}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Append an event line to today's ecosystem session log.
|
|
3
|
+
#
|
|
4
|
+
# Sourced by core/scripts/check-history-reminder.sh (via the existing
|
|
5
|
+
# PostToolUse hook installed by `momentum init`). Safe to invoke
|
|
6
|
+
# anywhere — no-ops silently when the current directory is not inside
|
|
7
|
+
# a registered ecosystem member.
|
|
8
|
+
#
|
|
9
|
+
# Inputs (positional):
|
|
10
|
+
# $1 — event kind (commit | pr | deploy | log)
|
|
11
|
+
# $2 — event summary (one line, no embedded newlines)
|
|
12
|
+
# $3 — optional context (sha, PR number, deploy tag, …)
|
|
13
|
+
#
|
|
14
|
+
# Resolves the ecosystem root by walking up from $PWD looking for a
|
|
15
|
+
# sibling directory containing ecosystem.json (bounded to 5 parents by
|
|
16
|
+
# default; override via MOMENTUM_MAX_PARENT_WALK env var — see
|
|
17
|
+
# core/ecosystem/lib/state.js for the JS counterpart).
|
|
18
|
+
# Resolves the member id by matching $PWD against the manifest's
|
|
19
|
+
# members[].path entries.
|
|
20
|
+
#
|
|
21
|
+
# Writes one line to <ecosystem-root>/sessions/$(date -u +%F).md:
|
|
22
|
+
# HH:MMZ [<member-id>] <kind> <summary> (<context>)
|
|
23
|
+
#
|
|
24
|
+
# If this is the first append today, prepends a header line naming the
|
|
25
|
+
# active initiative (if any).
|
|
26
|
+
|
|
27
|
+
set -eu
|
|
28
|
+
|
|
29
|
+
EVENT_KIND="${1:-}"
|
|
30
|
+
EVENT_SUMMARY="${2:-}"
|
|
31
|
+
EVENT_CONTEXT="${3:-}"
|
|
32
|
+
|
|
33
|
+
if [ -z "$EVENT_KIND" ] || [ -z "$EVENT_SUMMARY" ]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ── Resolve ecosystem root (bounded walk-up, looking for siblings) ──────────
|
|
38
|
+
# The ecosystem is a SIBLING of member repos. From a member repo we walk
|
|
39
|
+
# up one level and look at each sibling for ecosystem.json.
|
|
40
|
+
|
|
41
|
+
find_ecosystem_root() {
|
|
42
|
+
local start="$PWD"
|
|
43
|
+
local current="$start"
|
|
44
|
+
local depth=0
|
|
45
|
+
local max_depth="${MOMENTUM_MAX_PARENT_WALK:-5}"
|
|
46
|
+
# Guard against non-numeric / negative env value.
|
|
47
|
+
case "$max_depth" in
|
|
48
|
+
''|*[!0-9]*) max_depth=5 ;;
|
|
49
|
+
esac
|
|
50
|
+
while [ $depth -le $max_depth ]; do
|
|
51
|
+
# Same-directory check (caller might already be in ecosystem root)
|
|
52
|
+
if [ -f "$current/ecosystem.json" ]; then
|
|
53
|
+
echo "$current"
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
# Sibling check
|
|
57
|
+
local parent
|
|
58
|
+
parent=$(dirname "$current")
|
|
59
|
+
if [ "$parent" = "$current" ]; then return 1; fi
|
|
60
|
+
for sibling in "$parent"/*; do
|
|
61
|
+
if [ -d "$sibling" ] && [ -f "$sibling/ecosystem.json" ]; then
|
|
62
|
+
echo "$sibling"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
current="$parent"
|
|
67
|
+
depth=$((depth + 1))
|
|
68
|
+
done
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ROOT=$(find_ecosystem_root 2>/dev/null) || exit 0
|
|
73
|
+
[ -z "$ROOT" ] && exit 0
|
|
74
|
+
[ -f "$ROOT/ecosystem.json" ] || exit 0
|
|
75
|
+
|
|
76
|
+
# ── Resolve member id by matching $PWD against manifest.members[].path ─────
|
|
77
|
+
|
|
78
|
+
resolve_member_id() {
|
|
79
|
+
python3 - "$ROOT" "$PWD" <<'PY' 2>/dev/null || echo ""
|
|
80
|
+
import sys, json, os
|
|
81
|
+
root, pwd = sys.argv[1], os.path.realpath(sys.argv[2])
|
|
82
|
+
try:
|
|
83
|
+
with open(os.path.join(root, "ecosystem.json")) as f:
|
|
84
|
+
m = json.load(f)
|
|
85
|
+
except Exception:
|
|
86
|
+
sys.exit(0)
|
|
87
|
+
for member in m.get("members", []):
|
|
88
|
+
abs_path = os.path.realpath(os.path.join(root, member.get("path", "")))
|
|
89
|
+
# Match pwd that is == abs_path or any descendant
|
|
90
|
+
try:
|
|
91
|
+
rel = os.path.relpath(pwd, abs_path)
|
|
92
|
+
except ValueError:
|
|
93
|
+
continue
|
|
94
|
+
if rel == "." or (not rel.startswith("..") and not os.path.isabs(rel)):
|
|
95
|
+
print(member.get("id", ""))
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
PY
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
MEMBER_ID=$(resolve_member_id)
|
|
101
|
+
[ -z "$MEMBER_ID" ] && exit 0
|
|
102
|
+
|
|
103
|
+
# ── Write the line ─────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
SESSION_DIR="$ROOT/sessions"
|
|
106
|
+
mkdir -p "$SESSION_DIR"
|
|
107
|
+
TODAY=$(date -u +%F)
|
|
108
|
+
SESSION_FILE="$SESSION_DIR/$TODAY.md"
|
|
109
|
+
HHMM=$(date -u +%H:%M)
|
|
110
|
+
|
|
111
|
+
# ── BUG-004 fix: serialize concurrent writers via mkdir lock ────────────────
|
|
112
|
+
# `mkdir` is atomic on POSIX filesystems and portable across macOS/Linux
|
|
113
|
+
# without depending on flock (which is not present by default on macOS).
|
|
114
|
+
# The lock covers the "check + header-write + append" sequence so two
|
|
115
|
+
# concurrent commits in different member repos can't both write the
|
|
116
|
+
# header or interleave their data lines.
|
|
117
|
+
LOCK_DIR="$SESSION_FILE.lock"
|
|
118
|
+
acquire_session_lock() {
|
|
119
|
+
local tries=100 # ~5s total at 50ms each
|
|
120
|
+
while [ $tries -gt 0 ]; do
|
|
121
|
+
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
122
|
+
return 0
|
|
123
|
+
fi
|
|
124
|
+
sleep 0.05
|
|
125
|
+
tries=$((tries - 1))
|
|
126
|
+
done
|
|
127
|
+
return 1
|
|
128
|
+
}
|
|
129
|
+
release_session_lock() {
|
|
130
|
+
rmdir "$LOCK_DIR" 2>/dev/null || true
|
|
131
|
+
}
|
|
132
|
+
trap release_session_lock EXIT INT TERM
|
|
133
|
+
|
|
134
|
+
# If we cannot acquire the lock within the budget, drop the event silently
|
|
135
|
+
# rather than corrupt the file. Session events are advisory; momentum's
|
|
136
|
+
# correctness does not depend on every one landing.
|
|
137
|
+
if ! acquire_session_lock; then
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# First write of the day → write header (and active initiative banner).
|
|
142
|
+
if [ ! -f "$SESSION_FILE" ]; then
|
|
143
|
+
{
|
|
144
|
+
echo "# Session $TODAY"
|
|
145
|
+
if [ -f "$ROOT/.state/active-initiative" ]; then
|
|
146
|
+
ACTIVE=$(tr -d '[:space:]' < "$ROOT/.state/active-initiative")
|
|
147
|
+
if [ -n "$ACTIVE" ]; then
|
|
148
|
+
echo "Active initiative: $ACTIVE"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
echo ""
|
|
152
|
+
} > "$SESSION_FILE"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Build the line; quote context only if present.
|
|
156
|
+
if [ -n "$EVENT_CONTEXT" ]; then
|
|
157
|
+
echo "${HHMM}Z [$MEMBER_ID] ${EVENT_KIND}: ${EVENT_SUMMARY} (${EVENT_CONTEXT})" >> "$SESSION_FILE"
|
|
158
|
+
else
|
|
159
|
+
echo "${HHMM}Z [$MEMBER_ID] ${EVENT_KIND}: ${EVENT_SUMMARY}" >> "$SESSION_FILE"
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Cache last session date for cheap subsequent reads.
|
|
163
|
+
mkdir -p "$ROOT/.state"
|
|
164
|
+
echo "$TODAY" > "$ROOT/.state/last-session"
|
|
165
|
+
|
|
166
|
+
# Lock is released by the EXIT trap.
|
|
167
|
+
exit 0
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# {{NAME}} — Ecosystem (Coordination Layer)
|
|
2
|
+
|
|
3
|
+
> **You are in a momentum ecosystem coordination root — NOT a project.**
|
|
4
|
+
> This directory coordinates work across multiple sibling project repos.
|
|
5
|
+
> Auto-managed by `momentum`. The content above `## Project Extensions`
|
|
6
|
+
> may be refreshed by `momentum upgrade`.
|
|
7
|
+
|
|
8
|
+
## What this directory is
|
|
9
|
+
|
|
10
|
+
| Path | Purpose |
|
|
11
|
+
|------|---------|
|
|
12
|
+
| `ecosystem.json` | Manifest: members, roles, dependency edges |
|
|
13
|
+
| `initiatives/` | Cross-repo feature files (`NNNN-<slug>.md`) — first-class records of features that span >1 member |
|
|
14
|
+
| `sessions/` | Auto-appended daily activity log (`YYYY-MM-DD.md`); member-repo hooks write here |
|
|
15
|
+
| `.state/` | Runtime state (gitignored) — active initiative pointer, etc. |
|
|
16
|
+
| `CLAUDE.md` / `AGENTS.md` | These instructions |
|
|
17
|
+
|
|
18
|
+
**There are no `specs/` here.** Per-project specs, phases, backlog, and history stay in each member repo. The ecosystem layer is strictly additive — it never writes into a member's `specs/`.
|
|
19
|
+
|
|
20
|
+
## Cross-repo work? Write an initiative — NEVER plan implementation here
|
|
21
|
+
|
|
22
|
+
If a feature, refactor, or coordination effort touches MORE THAN ONE member repo, the entry point is an **initiative**:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
momentum ecosystem initiative create <slug> \
|
|
26
|
+
--why "<one-paragraph motivation>" \
|
|
27
|
+
--repos <member-id-1>,<member-id-2> \
|
|
28
|
+
--owner <you>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This writes `initiatives/NNNN-<slug>.md` and sets it as active. The initiative is the *only* place cross-repo scope, deploy chronology, and links to per-repo decisions live. Per-repo implementation phases stay in each member's `specs/phases/`.
|
|
32
|
+
|
|
33
|
+
**Anti-pattern:** do not create planning notes, task lists, or "phase" files directly in this coordination root. There is no `specs/phases/`. If you find yourself reaching for one, you are working in the wrong layer — switch to a member repo or create an initiative.
|
|
34
|
+
|
|
35
|
+
## Orchestration primitives (use these for multi-repo work)
|
|
36
|
+
|
|
37
|
+
These are agent commands you should invoke when working across member repos. Each has a CLI fallback under `momentum <name>`; the agent command is the LLM-synthesis door, the CLI is the keyword/scripted door.
|
|
38
|
+
|
|
39
|
+
| Primitive | When to use |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `/scout <repo>` | Read-only context fetch from another member ("what's in `<repo>/specs/status.md` right now?"). Use before planning any cross-repo work. |
|
|
42
|
+
| `/dispatch <r1> <r2> "<intent>"` | Parallel multi-repo audit/investigation ("audit how X is implemented across A, B, C"). Spawns one sub-agent per repo and synthesises. **Do NOT use `momentum dispatch` from the CLI for synthesis** — CLI mode produces keyword summaries only; use the agent command. |
|
|
43
|
+
| `/handoff <repo>` | Transfer context to an agent working in another member repo. Writes a structured handoff into the target's `.momentum/inbox/`. The receiving agent picks it up via `/continue` on session start. |
|
|
44
|
+
| `/continue` | Pick up a pending handoff from this repo's inbox (SessionStart hook surfaces these automatically). |
|
|
45
|
+
| `/initiative create <slug>` | Same as the CLI above; the agent door also wires the active initiative for follow-up `/session log` entries. |
|
|
46
|
+
| `/session log "<note>"` | Append a narrative entry to today's `sessions/YYYY-MM-DD.md`. Auto-events (commits/PRs) are logged by the PostToolUse hook in each member; use `/session log` for decisions, problems hit, checkpoint notes. |
|
|
47
|
+
|
|
48
|
+
## Discovery — how to find current state
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
momentum ecosystem status # manifest summary + per-member git state
|
|
52
|
+
cat ecosystem.json # raw manifest
|
|
53
|
+
ls initiatives/ # all initiatives, active marked in .state/active-initiative
|
|
54
|
+
tail sessions/$(date -u +%F).md # what happened today
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick member management
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
momentum ecosystem add ../<repo-name> # register a member (must already be `momentum init`-installed)
|
|
61
|
+
momentum ecosystem remove <member-id> # unregister
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`add` also injects a fenced pointer block into the member's `CLAUDE.md`/`AGENTS.md` so agents working *inside* the member know they're part of this ecosystem and can reach the orchestration primitives.
|
|
65
|
+
|
|
66
|
+
## Docs
|
|
67
|
+
|
|
68
|
+
- Full ecosystem mode: https://trymomentum.github.io/ecosystem/
|
|
69
|
+
- Orchestration primitives: https://trymomentum.github.io/orchestration/
|
|
70
|
+
- Concepts: https://trymomentum.github.io/concepts/
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Project Extensions
|
|
75
|
+
|
|
76
|
+
> Everything below this heading is preserved across `momentum upgrade`.
|
|
77
|
+
> Add ecosystem-specific notes, conventions, cross-repo references,
|
|
78
|
+
> escalation contacts, etc. here. Anything above this heading is
|
|
79
|
+
> managed by momentum and may be refreshed on upgrade.
|