@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/okf.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `momentum okf` — OKF v0.1 bundle utilities (Phase 24, ADR-0005).
|
|
6
|
+
*
|
|
7
|
+
* momentum okf check [dir] conformance report for <dir>/specs
|
|
8
|
+
* momentum okf index [dir] regenerate bundle indexes (root/phases/decisions)
|
|
9
|
+
*
|
|
10
|
+
* The migration itself runs inside `momentum upgrade`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const okf = require('../core/lib/okf-migrate');
|
|
17
|
+
|
|
18
|
+
function usage() {
|
|
19
|
+
console.log(`momentum okf — Open Knowledge Format (v${okf.OKF_VERSION}) bundle utilities
|
|
20
|
+
|
|
21
|
+
momentum okf check [dir] conformance report for <dir>/specs (exit 1 on violations)
|
|
22
|
+
momentum okf index [dir] regenerate specs/index.md + phases/ + decisions/ listings
|
|
23
|
+
|
|
24
|
+
Migration of legacy JSON state (phases/index.json, decisions/impact-map.json)
|
|
25
|
+
runs as part of \`momentum upgrade\`.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runOkf(args) {
|
|
29
|
+
const sub = args[0];
|
|
30
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
31
|
+
usage();
|
|
32
|
+
return sub ? 0 : 1;
|
|
33
|
+
}
|
|
34
|
+
const target = path.resolve(args[1] || process.cwd());
|
|
35
|
+
|
|
36
|
+
if (sub === 'check') {
|
|
37
|
+
const report = okf.check(target);
|
|
38
|
+
if (!report.applicable) {
|
|
39
|
+
console.error(`✗ no specs/ directory at ${target}`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
for (const w of report.warnings) console.error(` ⚠ ${w}`);
|
|
43
|
+
if (report.violations.length) {
|
|
44
|
+
console.log(`OKF v${okf.OKF_VERSION} conformance: ${path.join(target, 'specs')}`);
|
|
45
|
+
for (const v of report.violations) console.log(` ✗ ${v.rel} — ${v.problem}`);
|
|
46
|
+
console.log(`✗ ${report.violations.length} violation(s) across ${report.files} markdown file(s)`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
console.log(`✓ specs/ is an OKF v${okf.OKF_VERSION} conformant bundle (${report.files} markdown file(s))`);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (sub === 'index') {
|
|
54
|
+
if (!fs.existsSync(path.join(target, 'specs'))) {
|
|
55
|
+
console.error(`✗ no specs/ directory at ${target}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const changed = okf.generateIndexes(target);
|
|
59
|
+
if (!changed.length) {
|
|
60
|
+
console.log('= bundle indexes up to date');
|
|
61
|
+
} else {
|
|
62
|
+
for (const rel of changed) console.log(`✓ wrote specs/${rel}`);
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.error(`Unknown okf subcommand: ${sub}`);
|
|
68
|
+
console.error('Run "momentum okf help" for usage.');
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { runOkf };
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// CLI verbs for orchestration primitives.
|
|
4
|
+
//
|
|
5
|
+
// Three doors, one library. This is the CLI door — `momentum scout`,
|
|
6
|
+
// `momentum dispatch`, `momentum handoff`, `momentum continue`. All
|
|
7
|
+
// dispatch into core/orchestration/* so output shape is identical to
|
|
8
|
+
// the slash-command and natural-language inference doors.
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
|
|
13
|
+
const MOMENTUM_ROOT = path.resolve(__dirname, '..');
|
|
14
|
+
const orchestration = require(path.join(MOMENTUM_ROOT, 'core', 'orchestration'));
|
|
15
|
+
const { findRegistration } = require(path.join(MOMENTUM_ROOT, 'core', 'ecosystem', 'lib', 'state'));
|
|
16
|
+
|
|
17
|
+
// ── shared helpers ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function resolveEcosystemFromCwd(repoPath) {
|
|
20
|
+
try {
|
|
21
|
+
const reg = findRegistration(repoPath);
|
|
22
|
+
if (reg && reg.rootPath) {
|
|
23
|
+
return { rootPath: reg.rootPath, memberId: reg.memberId };
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// ignore
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveTargetRepo(spec, ecosystem) {
|
|
32
|
+
// Allow either a manifest member id or a relative/absolute path.
|
|
33
|
+
if (!spec) return null;
|
|
34
|
+
if (path.isAbsolute(spec) && fs.existsSync(spec)) return spec;
|
|
35
|
+
|
|
36
|
+
if (ecosystem && ecosystem.rootPath) {
|
|
37
|
+
const lib = require(path.join(MOMENTUM_ROOT, 'core', 'ecosystem', 'lib', 'index'));
|
|
38
|
+
try {
|
|
39
|
+
const manifest = lib.loadManifest(ecosystem.rootPath);
|
|
40
|
+
for (const m of lib.listMembers(manifest)) {
|
|
41
|
+
if (m.id === spec) {
|
|
42
|
+
return path.resolve(ecosystem.rootPath, m.path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// fall through to path resolution
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const asRel = path.resolve(process.cwd(), spec);
|
|
51
|
+
if (fs.existsSync(asRel)) return asRel;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printOrchHelp(verb) {
|
|
56
|
+
const blocks = {
|
|
57
|
+
scout: [
|
|
58
|
+
'Usage: momentum scout <repo> "<prompt>"',
|
|
59
|
+
'',
|
|
60
|
+
'Read-only context fetch from one ecosystem member repo.',
|
|
61
|
+
'',
|
|
62
|
+
'Doors:',
|
|
63
|
+
' - slash: /scout <repo> "<prompt>" (Claude Code, Codex)',
|
|
64
|
+
' - natural: describe a single-repo read-only task in prose',
|
|
65
|
+
' - cli: momentum scout <repo> "<prompt>" (universal floor)',
|
|
66
|
+
'',
|
|
67
|
+
'Tracking contract: session log + scout-NNN.md auto every time;',
|
|
68
|
+
'[DISCOVERY] in scouted repo only if finding meets Rule 3 criteria.',
|
|
69
|
+
],
|
|
70
|
+
dispatch: [
|
|
71
|
+
'Usage: momentum dispatch <repo1> [repo2 ...] --prompt "<text>"',
|
|
72
|
+
'',
|
|
73
|
+
'Parallel multi-repo fan-out + synthesis.',
|
|
74
|
+
'',
|
|
75
|
+
'Flags:',
|
|
76
|
+
' --prompt "<text>" high-level intent (tailored per-repo at runtime)',
|
|
77
|
+
' --sequential force sequential mode (testing aid)',
|
|
78
|
+
'',
|
|
79
|
+
'Doors: /dispatch (slash) | natural-language inference | momentum dispatch (CLI)',
|
|
80
|
+
'',
|
|
81
|
+
'Tracking contract: session log + dispatch-NNN.md auto every time;',
|
|
82
|
+
'per-repo [DISCOVERY] only for meaningful findings.',
|
|
83
|
+
],
|
|
84
|
+
handoff: [
|
|
85
|
+
'Usage: momentum handoff <repo> [--summary "<text>"]',
|
|
86
|
+
'',
|
|
87
|
+
'Write a context block to <repo>/.momentum/inbox/handoff-NNN.md so',
|
|
88
|
+
'a new agent session in that repo can pick up where you left off.',
|
|
89
|
+
'',
|
|
90
|
+
'Tracking contract: writes the inbox file AND emits a [DECISION] in',
|
|
91
|
+
'the originating repo\'s active phase history (handoff IS a decision).',
|
|
92
|
+
],
|
|
93
|
+
continue: [
|
|
94
|
+
'Usage: momentum continue [--handoff <id>]',
|
|
95
|
+
'',
|
|
96
|
+
'Pick up a pending handoff in the current repo. With no --handoff,',
|
|
97
|
+
'lists pending handoffs and picks the oldest unread.',
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
for (const line of blocks[verb] || []) console.log(line);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Light-weight flag parser. `knownFlags` may be:
|
|
105
|
+
* - a string array of flag tokens (value flags — consume the next non-flag token)
|
|
106
|
+
* - an object { valueFlags: [...], boolFlags: [...] } for explicit shapes
|
|
107
|
+
*/
|
|
108
|
+
function parseFlags(args, knownFlags) {
|
|
109
|
+
let valueFlags = [];
|
|
110
|
+
let boolFlags = [];
|
|
111
|
+
if (Array.isArray(knownFlags)) {
|
|
112
|
+
valueFlags = knownFlags;
|
|
113
|
+
} else if (knownFlags && typeof knownFlags === 'object') {
|
|
114
|
+
valueFlags = knownFlags.valueFlags || [];
|
|
115
|
+
boolFlags = knownFlags.boolFlags || [];
|
|
116
|
+
}
|
|
117
|
+
const positional = [];
|
|
118
|
+
const flags = {};
|
|
119
|
+
for (let i = 0; i < args.length; i++) {
|
|
120
|
+
const a = args[i];
|
|
121
|
+
if (valueFlags.includes(a)) {
|
|
122
|
+
const next = args[i + 1];
|
|
123
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
124
|
+
flags[a.slice(2)] = next;
|
|
125
|
+
i++;
|
|
126
|
+
} else {
|
|
127
|
+
flags[a.slice(2)] = true;
|
|
128
|
+
}
|
|
129
|
+
} else if (boolFlags.includes(a)) {
|
|
130
|
+
flags[a.slice(2)] = true;
|
|
131
|
+
} else if (a === '--help' || a === '-h') {
|
|
132
|
+
flags.help = true;
|
|
133
|
+
} else {
|
|
134
|
+
positional.push(a);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { positional, flags };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── cmdScout ───────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
async function cmdScout(args) {
|
|
143
|
+
const { positional, flags } = parseFlags(args, []);
|
|
144
|
+
if (flags.help || positional.length < 2) {
|
|
145
|
+
printOrchHelp('scout');
|
|
146
|
+
process.exit(flags.help ? 0 : positional.length === 0 ? 1 : 1);
|
|
147
|
+
}
|
|
148
|
+
const repoSpec = positional[0];
|
|
149
|
+
const prompt = positional.slice(1).join(' ');
|
|
150
|
+
const originatingRepo = process.cwd();
|
|
151
|
+
const ecosystem = resolveEcosystemFromCwd(originatingRepo);
|
|
152
|
+
const targetRepo = resolveTargetRepo(repoSpec, ecosystem);
|
|
153
|
+
|
|
154
|
+
if (!targetRepo) {
|
|
155
|
+
console.error(`Error: could not resolve <repo> "${repoSpec}" — not a member id and not an existing path.`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = await orchestration.scout.scout({
|
|
160
|
+
repo: targetRepo,
|
|
161
|
+
prompt,
|
|
162
|
+
originatingRepo,
|
|
163
|
+
ecosystem: ecosystem || undefined,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Renderer already printed lifecycle lines; final summary goes to stdout.
|
|
167
|
+
process.stdout.write('\n');
|
|
168
|
+
process.stdout.write(result.summary + '\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── cmdDispatch ────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
async function cmdDispatch(args) {
|
|
174
|
+
const { positional, flags } = parseFlags(args, {
|
|
175
|
+
valueFlags: ['--prompt'],
|
|
176
|
+
boolFlags: ['--sequential'],
|
|
177
|
+
});
|
|
178
|
+
if (flags.help || positional.length === 0 || !flags.prompt) {
|
|
179
|
+
printOrchHelp('dispatch');
|
|
180
|
+
process.exit(flags.help ? 0 : 1);
|
|
181
|
+
}
|
|
182
|
+
const originatingRepo = process.cwd();
|
|
183
|
+
const ecosystem = resolveEcosystemFromCwd(originatingRepo);
|
|
184
|
+
const repos = positional
|
|
185
|
+
.map((spec) => ({ spec, abs: resolveTargetRepo(spec, ecosystem) }))
|
|
186
|
+
.filter((entry) => {
|
|
187
|
+
if (!entry.abs) {
|
|
188
|
+
console.error(`Warning: skipping unresolved repo "${entry.spec}"`);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
})
|
|
193
|
+
.map((entry) => entry.abs);
|
|
194
|
+
|
|
195
|
+
if (repos.length === 0) {
|
|
196
|
+
console.error('Error: no resolvable repos in dispatch list.');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const dispatch = orchestration.dispatch;
|
|
201
|
+
const result = await dispatch.dispatch({
|
|
202
|
+
repos,
|
|
203
|
+
userIntent: flags.prompt,
|
|
204
|
+
originatingRepo,
|
|
205
|
+
ecosystem: ecosystem || undefined,
|
|
206
|
+
forceSequential: Boolean(flags.sequential),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
process.stdout.write('\n');
|
|
210
|
+
process.stdout.write(result.synthesis + '\n');
|
|
211
|
+
if (result.failures.length) {
|
|
212
|
+
process.stdout.write(`\n${result.failures.length} repo(s) failed:\n`);
|
|
213
|
+
for (const f of result.failures) {
|
|
214
|
+
process.stdout.write(` - ${f.repo}: ${f.error && f.error.message ? f.error.message : f.error}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── cmdHandoff ─────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
async function cmdHandoff(args) {
|
|
222
|
+
const { positional, flags } = parseFlags(args, ['--summary', '--decision', '--file', '--verify', '--question']);
|
|
223
|
+
if (flags.help || positional.length === 0) {
|
|
224
|
+
printOrchHelp('handoff');
|
|
225
|
+
process.exit(flags.help ? 0 : 1);
|
|
226
|
+
}
|
|
227
|
+
const originatingRepo = process.cwd();
|
|
228
|
+
const ecosystem = resolveEcosystemFromCwd(originatingRepo);
|
|
229
|
+
const targetRepo = resolveTargetRepo(positional[0], ecosystem);
|
|
230
|
+
|
|
231
|
+
if (!targetRepo) {
|
|
232
|
+
console.error(`Error: could not resolve <repo> "${positional[0]}".`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const summary = flags.summary || `Continue work from ${path.basename(originatingRepo)}.`;
|
|
237
|
+
const block = await orchestration.handoff.handoff({
|
|
238
|
+
fromRepo: originatingRepo,
|
|
239
|
+
toRepo: targetRepo,
|
|
240
|
+
summary,
|
|
241
|
+
decisions: arrayify(flags.decision),
|
|
242
|
+
filesTouched: arrayify(flags.file),
|
|
243
|
+
verificationCommands: arrayify(flags.verify),
|
|
244
|
+
openQuestions: arrayify(flags.question),
|
|
245
|
+
ecosystem: ecosystem || undefined,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
process.stdout.write(`\n▸ handoff written: ${block.inboxPath}\n`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── cmdContinue ────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
async function cmdContinue(args) {
|
|
254
|
+
const { positional, flags } = parseFlags(args, ['--handoff']);
|
|
255
|
+
if (flags.help) {
|
|
256
|
+
printOrchHelp('continue');
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
const originatingRepo = process.cwd();
|
|
260
|
+
const ecosystem = resolveEcosystemFromCwd(originatingRepo);
|
|
261
|
+
const block = await orchestration.continueHandoff.continueHandoff({
|
|
262
|
+
repo: originatingRepo,
|
|
263
|
+
handoffId: flags.handoff || (positional.length ? positional[0] : undefined),
|
|
264
|
+
ecosystem: ecosystem || undefined,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (!block) {
|
|
268
|
+
process.stdout.write('▸ no pending handoffs in this repo\n');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
process.stdout.write(`\n▸ picking up handoff-${block.handoffId} from ${path.basename(block.fromRepo)}\n\n`);
|
|
272
|
+
process.stdout.write(`Summary: ${block.summary}\n`);
|
|
273
|
+
if (block.decisions.length) {
|
|
274
|
+
process.stdout.write('\nDecisions:\n');
|
|
275
|
+
for (const d of block.decisions) process.stdout.write(` - ${d}\n`);
|
|
276
|
+
}
|
|
277
|
+
if (block.filesTouched.length) {
|
|
278
|
+
process.stdout.write('\nFiles touched:\n');
|
|
279
|
+
for (const f of block.filesTouched) process.stdout.write(` - ${f}\n`);
|
|
280
|
+
}
|
|
281
|
+
if (block.verificationCommands.length) {
|
|
282
|
+
process.stdout.write('\nVerification:\n');
|
|
283
|
+
for (const v of block.verificationCommands) process.stdout.write(` $ ${v}\n`);
|
|
284
|
+
}
|
|
285
|
+
if (block.openQuestions.length) {
|
|
286
|
+
process.stdout.write('\nOpen questions:\n');
|
|
287
|
+
for (const q of block.openQuestions) process.stdout.write(` ? ${q}\n`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function arrayify(v) {
|
|
292
|
+
if (v == null || v === false) return [];
|
|
293
|
+
if (Array.isArray(v)) return v;
|
|
294
|
+
return [String(v)];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
cmdScout,
|
|
299
|
+
cmdDispatch,
|
|
300
|
+
cmdHandoff,
|
|
301
|
+
cmdContinue,
|
|
302
|
+
// exported for tests:
|
|
303
|
+
resolveTargetRepo,
|
|
304
|
+
resolveEcosystemFromCwd,
|
|
305
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 10 — top-level state commands: `join`, `leave`, `doctor`.
|
|
5
|
+
*
|
|
6
|
+
* These are user-facing verbs that let a repo move between Standalone,
|
|
7
|
+
* Member, and Leader states without having to know about the
|
|
8
|
+
* `momentum ecosystem` operator toolkit.
|
|
9
|
+
*
|
|
10
|
+
* Internally they compose the existing `momentum ecosystem add/remove`
|
|
11
|
+
* primitives so there is exactly one place that mutates manifests +
|
|
12
|
+
* pointer blocks. State preconditions are checked first to surface
|
|
13
|
+
* clear errors before any mutation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const stateLib = require('../core/ecosystem/lib/state');
|
|
20
|
+
const pointerLib = require('../core/ecosystem/lib/pointer');
|
|
21
|
+
const lib = require('../core/ecosystem/lib');
|
|
22
|
+
|
|
23
|
+
function cmdJoin(args) {
|
|
24
|
+
if (!args || args.length === 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'join: missing <ecosystem-path>. Try `momentum doctor` to see what is reachable from here.',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
let ecosystemPath = path.resolve(process.cwd(), args[0]);
|
|
30
|
+
if (!fs.existsSync(path.join(ecosystemPath, lib.MANIFEST_FILENAME))) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`join: ${ecosystemPath} has no ecosystem.json. Did you mean a different path?`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
// Normalize via realpath so the manifest stores a clean relative
|
|
36
|
+
// path (../project) instead of a cross-symlink monstrosity
|
|
37
|
+
// (../../../private/tmp/.../project) on macOS where /tmp → /private/tmp.
|
|
38
|
+
try {
|
|
39
|
+
ecosystemPath = fs.realpathSync(ecosystemPath);
|
|
40
|
+
} catch (_e) { /* keep resolved form */ }
|
|
41
|
+
|
|
42
|
+
const cwd = realpathOf(process.cwd());
|
|
43
|
+
lib._clearRootCache();
|
|
44
|
+
const state = stateLib.detectState(cwd);
|
|
45
|
+
|
|
46
|
+
if (state === 'leader' || state === 'leader-and-member') {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'join: this directory is itself an ecosystem root. ' +
|
|
49
|
+
'Use `momentum ecosystem add <repo-path>` to register members instead.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (state === 'member') {
|
|
54
|
+
const reg = stateLib.findRegistration(cwd);
|
|
55
|
+
if (reg && path.resolve(reg.rootPath) === ecosystemPath) {
|
|
56
|
+
console.log(
|
|
57
|
+
`Already a member of "${reg.ecosystemName}" at ${reg.rootPath}. No changes.`,
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(
|
|
62
|
+
`join: this repo is already a member of a different ecosystem ` +
|
|
63
|
+
`("${reg ? reg.ecosystemName : '?'}"). Run \`momentum leave\` first.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// standalone / broken-pointer / broken-orphan — proceed with ecosystem add.
|
|
68
|
+
const { runEcosystem } = require('./ecosystem');
|
|
69
|
+
runEcosystem(['add', cwd, '--ecosystem', ecosystemPath]);
|
|
70
|
+
|
|
71
|
+
// Re-detect post-mutation to confirm.
|
|
72
|
+
lib._clearRootCache();
|
|
73
|
+
const after = stateLib.detectState(cwd);
|
|
74
|
+
if (after === 'member' || after === 'leader-and-member') {
|
|
75
|
+
const reg = stateLib.findRegistration(cwd);
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(`✓ Joined "${reg ? reg.ecosystemName : '?'}" as member "${reg ? reg.memberId : '?'}".`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cmdLeave(args) {
|
|
82
|
+
// No required args; opt-in flags TBD.
|
|
83
|
+
void args;
|
|
84
|
+
const cwd = realpathOf(process.cwd());
|
|
85
|
+
lib._clearRootCache();
|
|
86
|
+
const state = stateLib.detectState(cwd);
|
|
87
|
+
|
|
88
|
+
if (state === 'standalone') {
|
|
89
|
+
console.log('Already standalone. No changes.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (state === 'leader') {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'leave: this directory is an ecosystem root with no membership in another ecosystem. ' +
|
|
95
|
+
'There is nothing to leave.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (state === 'broken-manifest') {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'leave: ecosystem.json in this directory fails validation. ' +
|
|
101
|
+
'Repair or delete it manually before running leave.',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (state === 'broken-pointer') {
|
|
106
|
+
// Pointer present but no registration. Just strip pointer to recover.
|
|
107
|
+
const primary = pointerLib.findPrimaryInstructionFile(cwd);
|
|
108
|
+
if (primary) {
|
|
109
|
+
pointerLib.stripPointer(path.join(cwd, primary));
|
|
110
|
+
console.log('Stripped dangling pointer. Now standalone.');
|
|
111
|
+
} else {
|
|
112
|
+
console.log('No pointer block found. No changes.');
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// member / leader-and-member / broken-orphan — find registration and remove.
|
|
118
|
+
const reg = stateLib.findRegistration(cwd);
|
|
119
|
+
if (!reg) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
'leave: could not locate this repo in any reachable ecosystem manifest. ' +
|
|
122
|
+
'Try `momentum doctor` for diagnostics.',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { runEcosystem } = require('./ecosystem');
|
|
127
|
+
runEcosystem(['remove', reg.memberId, '--ecosystem', reg.rootPath]);
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(`✓ Left "${reg.ecosystemName}". Now standalone.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function cmdDoctor(args) {
|
|
133
|
+
void args;
|
|
134
|
+
const cwd = realpathOf(process.cwd());
|
|
135
|
+
lib._clearRootCache();
|
|
136
|
+
const state = stateLib.detectState(cwd);
|
|
137
|
+
const reg = stateLib.findRegistration(cwd);
|
|
138
|
+
|
|
139
|
+
console.log(`Path: ${cwd}`);
|
|
140
|
+
console.log(`State: ${formatStateLabel(state)}`);
|
|
141
|
+
if (reg) {
|
|
142
|
+
console.log(`Ecosystem: ${reg.ecosystemName}`);
|
|
143
|
+
console.log(` root: ${reg.rootPath}`);
|
|
144
|
+
console.log(` member: ${reg.memberId} (role: ${reg.memberRole || 'other'})`);
|
|
145
|
+
} else if (state === 'leader' || state === 'leader-and-member') {
|
|
146
|
+
try {
|
|
147
|
+
const manifest = lib.loadManifest(cwd);
|
|
148
|
+
console.log(`Ecosystem: ${manifest.name}`);
|
|
149
|
+
console.log(` members: ${manifest.members.length}`);
|
|
150
|
+
} catch (_err) { /* shown via state */ }
|
|
151
|
+
}
|
|
152
|
+
console.log('');
|
|
153
|
+
|
|
154
|
+
// Phase 22b G3 — Antigravity runtime advisory. Only for projects whose
|
|
155
|
+
// momentum install targets antigravity (lock file), and only ADVISES the
|
|
156
|
+
// official installer — momentum never provisions vendor binaries (see the
|
|
157
|
+
// VAL-002 adjudication record).
|
|
158
|
+
try {
|
|
159
|
+
const lockPath = path.join(cwd, '.momentum', 'installed.json');
|
|
160
|
+
if (fs.existsSync(lockPath)) {
|
|
161
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
162
|
+
if (lock.agent === 'antigravity' || (lock.agents && lock.agents['antigravity'])) {
|
|
163
|
+
const { spawnSync } = require('child_process');
|
|
164
|
+
const agyBin = process.env.AGY_BIN || 'agy';
|
|
165
|
+
const found = agyBin.includes(path.sep)
|
|
166
|
+
? fs.existsSync(agyBin)
|
|
167
|
+
: spawnSync('which', [agyBin], { stdio: 'ignore' }).status === 0;
|
|
168
|
+
if (!found) {
|
|
169
|
+
console.log('Advisory: this project targets Antigravity but the `agy` CLI is not on PATH.');
|
|
170
|
+
console.log(' Headless features (swarm spawn, orchestration CLI floor) need it.');
|
|
171
|
+
console.log(' Official installer: curl -fsSL https://antigravity.google/cli/install.sh | bash');
|
|
172
|
+
console.log(' (The Antigravity IDE works without it — this only affects CLI/headless use.)');
|
|
173
|
+
console.log('');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (_err) { /* advisory is best-effort — never block doctor */ }
|
|
178
|
+
|
|
179
|
+
const transitions = stateLib.availableTransitions(state, reg || {});
|
|
180
|
+
if (transitions.length === 0) {
|
|
181
|
+
console.log('No suggested next steps for this state.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.log('What you can do next:');
|
|
185
|
+
for (const t of transitions) {
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(` ${t.command}`);
|
|
188
|
+
console.log(` ${t.description}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function realpathOf(p) {
|
|
193
|
+
try {
|
|
194
|
+
return fs.realpathSync(p);
|
|
195
|
+
} catch (_e) {
|
|
196
|
+
return path.resolve(p);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatStateLabel(state) {
|
|
201
|
+
const labels = {
|
|
202
|
+
standalone: 'Standalone — momentum-installed, not in any ecosystem',
|
|
203
|
+
member: 'Member — part of an ecosystem',
|
|
204
|
+
leader: 'Leader — this directory IS an ecosystem root',
|
|
205
|
+
'leader-and-member': 'Leader+Member — ecosystem root that is also a member of another ecosystem',
|
|
206
|
+
'broken-manifest': 'Broken (manifest) — ecosystem.json fails validation',
|
|
207
|
+
'broken-pointer': 'Broken (pointer) — pointer block present but no matching ecosystem',
|
|
208
|
+
'broken-orphan': 'Broken (orphan) — registered in an ecosystem but pointer block missing',
|
|
209
|
+
};
|
|
210
|
+
return labels[state] || state;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
cmdJoin,
|
|
215
|
+
cmdLeave,
|
|
216
|
+
cmdDoctor,
|
|
217
|
+
formatStateLabel,
|
|
218
|
+
};
|