@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,81 @@
|
|
|
1
|
+
You are a **swarm supervisor** for momentum. Your role is to drive a single repo's phase end-to-end under a conductor's coordination.
|
|
2
|
+
|
|
3
|
+
This recipe is platform-agnostic — Claude Code is the v0.20.0 carrier; Codex + Antigravity wire in via Phase 18.
|
|
4
|
+
|
|
5
|
+
## Hard invariants
|
|
6
|
+
|
|
7
|
+
1. **cwd is pinned.** You are spawned with `--cwd <repoPath>`. Never `cd` elsewhere. Reads + writes are scoped to your repo.
|
|
8
|
+
2. **You don't see other supervisors.** Other repos in the swarm are not visible to you. Communicate via the inbox (questions) or wait for the conductor to broadcast/`tell` you context.
|
|
9
|
+
3. **You don't write to the swarm manifest.** Only the conductor mutates `<eco>/swarms/<id>/manifest.json`. You write your saga step to `<repo>/.momentum/runs/dispatch-run-NNN.json`; the conductor reads it.
|
|
10
|
+
4. **You follow standard momentum discipline.** `/start-phase` → implement → `/sync-docs` → `/complete-phase`. The conductor wired your brief with swarm frontmatter; otherwise this is a normal phase loop.
|
|
11
|
+
|
|
12
|
+
## Boot sequence (first turn)
|
|
13
|
+
|
|
14
|
+
1. Read these env vars (all required):
|
|
15
|
+
- `MOMENTUM_SWARM_ID` — e.g. `0007-user-auth`
|
|
16
|
+
- `MOMENTUM_SWARM_WAVE` — your wave number
|
|
17
|
+
- `MOMENTUM_SWARM_INITIATIVE` — the initiative slug
|
|
18
|
+
- `MOMENTUM_SWARM_SESSION` — your owning session id (write into saga records)
|
|
19
|
+
- `MOMENTUM_ECOSYSTEM_ROOT` — absolute path to the ecosystem root
|
|
20
|
+
- `MOMENTUM_SWARM_CONTEXT` — absolute path to your swarm-context.md
|
|
21
|
+
2. Read your phase brief (`specs/phases/<phase-slug>/overview.md`). Its frontmatter declares your `swarm:` / `wave:` / `initiative:` / `claimed_by_session:`.
|
|
22
|
+
3. Read `MOMENTUM_SWARM_CONTEXT` if it exists — this is where the conductor appends `/swarm tell` and `/swarm broadcast` content.
|
|
23
|
+
4. Read your contract files at `<MOMENTUM_ECOSYSTEM_ROOT>/swarms/<MOMENTUM_SWARM_ID>/contracts/` — any whose `consumers` or `owner` include your repo id.
|
|
24
|
+
5. Run `/start-phase` for this repo's phase. The recipe is unchanged — it picks up the brief frontmatter automatically.
|
|
25
|
+
|
|
26
|
+
## Per-turn loop
|
|
27
|
+
|
|
28
|
+
Each conductor poll cycle:
|
|
29
|
+
|
|
30
|
+
1. Re-read `MOMENTUM_SWARM_CONTEXT` from the bottom up; integrate any new content the conductor pushed.
|
|
31
|
+
2. Implement the next task per your phase's `plan.md`.
|
|
32
|
+
3. Commit with the conventional commit style declared in `plan.md`.
|
|
33
|
+
4. Update `<repo>/.momentum/runs/dispatch-run-NNN.json` with your saga step (allocate via the existing `core/orchestration/run-artifact.js` helper; mark `primitive: "swarm-supervise"`). Required fields: `swarm_id`, `saga_id`, `wave`, `step_n`, `tasks_done`, `tasks_total`, `head_sha`, `last_commit`. Set `done: true` only after `/complete-phase` succeeds.
|
|
34
|
+
5. If you need a user decision you can't make alone, write `<eco>/swarms/<MOMENTUM_SWARM_ID>/inbox/NNNN-<slug>.md` (see core/swarm/inbox.js) and stop until it resolves.
|
|
35
|
+
|
|
36
|
+
## Inbox protocol
|
|
37
|
+
|
|
38
|
+
Write an inbox item when:
|
|
39
|
+
- A required input is ambiguous and the conductor's context plus your brief don't disambiguate.
|
|
40
|
+
- A test failure is not caused by your code (likely a contract mismatch from an earlier wave).
|
|
41
|
+
- You hit a token-budget warning at 90% of `tokens_budget`.
|
|
42
|
+
|
|
43
|
+
Do NOT write an inbox item for normal coding choices — those belong to your /sync-docs + /complete-phase loop.
|
|
44
|
+
|
|
45
|
+
## Contract enforcement
|
|
46
|
+
|
|
47
|
+
If you produce a contract surface this wave:
|
|
48
|
+
1. Author the contract artifact at `<eco>/swarms/<MOMENTUM_SWARM_ID>/contracts/<surface>.contract.json` per `core/swarm/schema/contract.schema.json`.
|
|
49
|
+
2. Compute its `content_hash` (SHA256 of canonical-JSON serialization of `shape`).
|
|
50
|
+
3. Wave checkpoint will fail if a downstream consumer's expectation doesn't match.
|
|
51
|
+
|
|
52
|
+
If you consume a contract surface:
|
|
53
|
+
1. Read the producer's contract from `contracts/`.
|
|
54
|
+
2. Generate any required client/types code per your phase plan.
|
|
55
|
+
3. If shape diverges from what your phase brief expected, raise an inbox item — don't silently adapt.
|
|
56
|
+
|
|
57
|
+
## Completion
|
|
58
|
+
|
|
59
|
+
When `/complete-phase` succeeds in this repo:
|
|
60
|
+
1. Write the final dispatch-run record with `done: true` and `exit_status: "success"`.
|
|
61
|
+
2. Append a `[SWARM]` history entry to your repo's `history.md`:
|
|
62
|
+
```
|
|
63
|
+
### [SWARM] YYYY-MM-DD — Wave <N> completed — <swarm-id>
|
|
64
|
+
Topics: swarm, wave-<N>, <initiative-slug>
|
|
65
|
+
Affects-phases: <phase-slug>
|
|
66
|
+
Affects-specs: ../swarms/<swarm-id>/manifest.json
|
|
67
|
+
Detail: Wave <N> of swarm <swarm-id> finished in this repo. <one-line summary>.
|
|
68
|
+
```
|
|
69
|
+
3. Wait for the conductor's next poll cycle to pick up your completion. Do not announce — the materialized board does that.
|
|
70
|
+
|
|
71
|
+
## Failure / cancel
|
|
72
|
+
|
|
73
|
+
If your wave is cancelled (you see `signal: cancel` propagated through `<eco>/swarms/<id>/signals/`, or your supervisor session is killed):
|
|
74
|
+
1. DO NOT force-push.
|
|
75
|
+
2. DO NOT delete the branch.
|
|
76
|
+
3. Leave your dispatch-run record with `exit_status: "cancelled"`.
|
|
77
|
+
4. Your branch remains for forensics or `/swarm resume`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
This recipe is platform-agnostic. Claude Code is the v0.20.0 carrier. Codex (Phase 18) uses an MCP cwd shim to honor "cwd is pinned"; Antigravity uses the Agent Manager workflow to launch one supervisor per cwd.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plan-graph readers for the wave engine (Phase 21c, ADR-0003 §3).
|
|
5
|
+
*
|
|
6
|
+
* Dependency annotations live in the files that already exist:
|
|
7
|
+
* - task groups: `## Group N — title (deps: G0, G1)` in tasks.md;
|
|
8
|
+
* a group whose checkboxes are all `[x]` is SATISFIED and drops out
|
|
9
|
+
* of the graph (its dependents unblock).
|
|
10
|
+
* - phases: `status:` / `deps:` frontmatter on each phase's
|
|
11
|
+
* overview.md (OKF bundle format, ADR-0005). Legacy
|
|
12
|
+
* specs/phases/index.json is read as a fallback (with an upgrade
|
|
13
|
+
* nudge) so a new CLI keeps working in not-yet-migrated projects.
|
|
14
|
+
* Deps on `complete` phases are satisfied; only non-complete phases
|
|
15
|
+
* are scheduled.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const frontmatter = require('../../lib/frontmatter');
|
|
22
|
+
|
|
23
|
+
const GROUP_HEADING = /^##\s+Group\s+([A-Za-z0-9.\-]+)\s*(?:[—–-]\s*(.*?))?\s*$/;
|
|
24
|
+
const DEPS_SUFFIX = /\(deps:\s*([^)]*)\)\s*$/i;
|
|
25
|
+
const CHECKBOX = /^\s*-\s*\[( |x|X|\/)\]/;
|
|
26
|
+
|
|
27
|
+
/** Normalize a group token: bare numbers get the G prefix ("2" → "G2"). */
|
|
28
|
+
function groupId(token) {
|
|
29
|
+
const t = String(token).trim();
|
|
30
|
+
return /^\d/.test(t) ? `G${t}` : t.toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a tasks.md body into group nodes.
|
|
35
|
+
* @returns {{id, title, deps: string[], boxes: number, done: number, satisfied: boolean}[]}
|
|
36
|
+
*/
|
|
37
|
+
function parseTaskGroups(body) {
|
|
38
|
+
const groups = [];
|
|
39
|
+
let current = null;
|
|
40
|
+
for (const line of String(body).split('\n')) {
|
|
41
|
+
const h = line.match(GROUP_HEADING);
|
|
42
|
+
if (h) {
|
|
43
|
+
let title = (h[2] || '').trim();
|
|
44
|
+
let deps = [];
|
|
45
|
+
const d = title.match(DEPS_SUFFIX) || line.match(DEPS_SUFFIX);
|
|
46
|
+
if (d) {
|
|
47
|
+
deps = d[1].split(',').map((s) => groupId(s)).filter(Boolean);
|
|
48
|
+
title = title.replace(DEPS_SUFFIX, '').trim();
|
|
49
|
+
}
|
|
50
|
+
current = { id: groupId(h[1]), title, deps, boxes: 0, done: 0 };
|
|
51
|
+
groups.push(current);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!current) continue;
|
|
55
|
+
const c = line.match(CHECKBOX);
|
|
56
|
+
if (c) {
|
|
57
|
+
current.boxes += 1;
|
|
58
|
+
if (c[1] === 'x' || c[1] === 'X') current.done += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const g of groups) {
|
|
62
|
+
g.satisfied = g.boxes > 0 && g.done === g.boxes;
|
|
63
|
+
}
|
|
64
|
+
return groups;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wave input for a tasks.md file: unsatisfied groups only; deps on
|
|
69
|
+
* satisfied (or unknown) groups are dropped.
|
|
70
|
+
* @returns {{ nodes: string[], edges: {from,to}[], groups }} or null when unreadable
|
|
71
|
+
*/
|
|
72
|
+
function taskGraph(tasksFile) {
|
|
73
|
+
let body;
|
|
74
|
+
try {
|
|
75
|
+
body = fs.readFileSync(tasksFile, 'utf8');
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const groups = parseTaskGroups(body);
|
|
80
|
+
const pending = groups.filter((g) => !g.satisfied);
|
|
81
|
+
const pendingIds = new Set(pending.map((g) => g.id));
|
|
82
|
+
const edges = [];
|
|
83
|
+
for (const g of pending) {
|
|
84
|
+
for (const dep of g.deps) {
|
|
85
|
+
if (pendingIds.has(dep)) edges.push({ from: g.id, to: dep });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { nodes: pending.map((g) => g.id), edges, groups };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Natural compare so phase-2 sorts before phase-10 (index.json had insertion order). */
|
|
92
|
+
function naturalCompare(a, b) {
|
|
93
|
+
const ax = a.split(/(\d+)/);
|
|
94
|
+
const bx = b.split(/(\d+)/);
|
|
95
|
+
for (let i = 0; i < Math.max(ax.length, bx.length); i += 1) {
|
|
96
|
+
const as = ax[i] || '';
|
|
97
|
+
const bs = bx[i] || '';
|
|
98
|
+
if (as === bs) continue;
|
|
99
|
+
const an = Number(as);
|
|
100
|
+
const bn = Number(bs);
|
|
101
|
+
if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn;
|
|
102
|
+
return as < bs ? -1 : 1;
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Phase metadata from OKF frontmatter: scans specs/phases/[star]/overview.md
|
|
109
|
+
* for `status:` (plus `deps:`, `tags:`). Returns null when NO phase dir
|
|
110
|
+
* carries status frontmatter — the caller then falls back to index.json.
|
|
111
|
+
*/
|
|
112
|
+
function phasesFromFrontmatter(repoRoot) {
|
|
113
|
+
const phasesDir = path.join(repoRoot, 'specs', 'phases');
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const phases = {};
|
|
121
|
+
for (const e of entries.filter((d) => d.isDirectory()).sort((a, b) => naturalCompare(a.name, b.name))) {
|
|
122
|
+
let content;
|
|
123
|
+
try {
|
|
124
|
+
content = fs.readFileSync(path.join(phasesDir, e.name, 'overview.md'), 'utf8');
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const { data } = frontmatter.parse(content);
|
|
129
|
+
if (!data || !data.status) continue;
|
|
130
|
+
phases[e.name] = {
|
|
131
|
+
status: String(data.status),
|
|
132
|
+
topics: Array.isArray(data.tags) ? data.tags : [],
|
|
133
|
+
deps: Array.isArray(data.deps) ? data.deps : [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return Object.keys(phases).length ? phases : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let nudged = false;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Wave input for the phase scale: non-complete phases; deps on complete
|
|
143
|
+
* phases satisfied. Prefers overview.md frontmatter (OKF); falls back to
|
|
144
|
+
* legacy specs/phases/index.json with a one-time stderr nudge.
|
|
145
|
+
* @returns {{ nodes, edges, phases, source }} or null when neither exists
|
|
146
|
+
*/
|
|
147
|
+
function phaseGraph(repoRoot) {
|
|
148
|
+
let phases = phasesFromFrontmatter(repoRoot);
|
|
149
|
+
let source = 'overview.md frontmatter';
|
|
150
|
+
if (!phases) {
|
|
151
|
+
let index;
|
|
152
|
+
try {
|
|
153
|
+
index = JSON.parse(fs.readFileSync(path.join(repoRoot, 'specs', 'phases', 'index.json'), 'utf8'));
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
phases = index.phases || {};
|
|
158
|
+
source = 'index.json (legacy)';
|
|
159
|
+
if (!nudged) {
|
|
160
|
+
nudged = true;
|
|
161
|
+
console.error('note: reading legacy specs/phases/index.json — run `momentum upgrade` to migrate specs/ to the OKF bundle format');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const pending = Object.keys(phases).filter((id) => phases[id].status !== 'complete');
|
|
165
|
+
const pendingSet = new Set(pending);
|
|
166
|
+
const edges = [];
|
|
167
|
+
for (const id of pending) {
|
|
168
|
+
for (const dep of phases[id].deps || []) {
|
|
169
|
+
if (pendingSet.has(dep)) edges.push({ from: id, to: dep });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { nodes: pending, edges, phases, source };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { parseTaskGroups, taskGraph, phaseGraph, phasesFromFrontmatter, groupId };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The recursive wave engine (Phase 21c, FEAT-028 — see ADR-0003).
|
|
5
|
+
*
|
|
6
|
+
* ONE algorithm for every scale of the plan graph (ecosystem ⊃ repos ⊃
|
|
7
|
+
* phases ⊃ tasks): Kahn layering over dependency edges. Nodes with no
|
|
8
|
+
* remaining inbound edges form the next wave; lexicographic order inside
|
|
9
|
+
* a wave keeps repeated runs identical; a stuck non-empty remainder is a
|
|
10
|
+
* cycle, reported with its participants.
|
|
11
|
+
*
|
|
12
|
+
* Extracted from core/swarm/lib/wave-ordering.js (Phase 17) — swarm is
|
|
13
|
+
* now a thin adapter over this engine. The `label` option lets adapters
|
|
14
|
+
* keep their historical error strings byte-for-byte.
|
|
15
|
+
*
|
|
16
|
+
* Edge semantics: { from: A, to: B } means A DEPENDS ON B → B runs first.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string[]} nodeIds — non-empty list of node ids
|
|
21
|
+
* @param {{from: string, to: string}[]} edges — dependency edges
|
|
22
|
+
* @param {{label?: string}} [opts] — error-message prefix (default 'computeWaves')
|
|
23
|
+
* @returns {{ index: number, nodes: string[] }[]}
|
|
24
|
+
*/
|
|
25
|
+
function computeWaveLayers(nodeIds, edges, opts = {}) {
|
|
26
|
+
const label = opts.label || 'computeWaves';
|
|
27
|
+
if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
|
|
28
|
+
throw new Error(`${label}: impactedRepos must be a non-empty array`);
|
|
29
|
+
}
|
|
30
|
+
for (const n of nodeIds) {
|
|
31
|
+
if (typeof n !== 'string' || n.length === 0) {
|
|
32
|
+
throw new TypeError(`${label}: impactedRepos must be non-empty strings (got ${JSON.stringify(n)})`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const included = new Set(nodeIds);
|
|
36
|
+
|
|
37
|
+
const inDegree = new Map();
|
|
38
|
+
const adj = new Map();
|
|
39
|
+
for (const n of included) {
|
|
40
|
+
inDegree.set(n, 0);
|
|
41
|
+
adj.set(n, []);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(edges)) {
|
|
45
|
+
for (const dep of edges) {
|
|
46
|
+
if (!dep || typeof dep !== 'object') continue;
|
|
47
|
+
const { from, to } = dep;
|
|
48
|
+
if (typeof from !== 'string' || typeof to !== 'string') continue;
|
|
49
|
+
if (!included.has(from) || !included.has(to)) continue; // outside the subgraph
|
|
50
|
+
if (from === to) {
|
|
51
|
+
throw new Error(`${label}: self-dependency on "${from}" — invalid edge`);
|
|
52
|
+
}
|
|
53
|
+
adj.get(to).push(from); // B → A: the dependency unlocks the dependent
|
|
54
|
+
inDegree.set(from, inDegree.get(from) + 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const waves = [];
|
|
59
|
+
let waveIndex = 1;
|
|
60
|
+
const remaining = new Set(included);
|
|
61
|
+
|
|
62
|
+
while (remaining.size > 0) {
|
|
63
|
+
const ready = [];
|
|
64
|
+
for (const n of remaining) {
|
|
65
|
+
if (inDegree.get(n) === 0) ready.push(n);
|
|
66
|
+
}
|
|
67
|
+
if (ready.length === 0) {
|
|
68
|
+
throw new Error(`${label}: cycle detected involving ${[...remaining].sort().join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
ready.sort();
|
|
71
|
+
waves.push({ index: waveIndex, nodes: ready });
|
|
72
|
+
waveIndex += 1;
|
|
73
|
+
for (const n of ready) {
|
|
74
|
+
remaining.delete(n);
|
|
75
|
+
for (const target of adj.get(n) || []) {
|
|
76
|
+
inDegree.set(target, inDegree.get(target) - 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return waves;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { computeWaveLayers };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@limina-labs/momentum",
|
|
3
|
+
"version": "0.30.2",
|
|
4
|
+
"description": "Agent-agnostic, specs-driven development framework — single project or multi-project ecosystem",
|
|
5
|
+
"homepage": "https://trymomentum.github.io",
|
|
6
|
+
"bin": {
|
|
7
|
+
"momentum": "bin/momentum.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"core/",
|
|
12
|
+
"adapters/**/adapter.js",
|
|
13
|
+
"adapters/**/settings.json",
|
|
14
|
+
"adapters/**/hooks.json",
|
|
15
|
+
"adapters/**/plugins/**",
|
|
16
|
+
"adapters/**/instructions/**",
|
|
17
|
+
"adapters/**/commands/**",
|
|
18
|
+
"adapters/**/agent-rules/**",
|
|
19
|
+
"adapters/**/scripts/**",
|
|
20
|
+
"adapters/**/workflows/**",
|
|
21
|
+
"adapters/**/skills/**",
|
|
22
|
+
"adapters/**/agents/**"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test --test-concurrency=1 tests/*.test.js",
|
|
26
|
+
"capture-evidence": "MOMENTUM_CAPTURE_EVIDENCE=1 node --test --test-concurrency=1 tests/swarm-e2e-scenarios.test.js tests/swarm-portability-e2e.test.js",
|
|
27
|
+
"generate-instructions": "node scripts/generate-instructions.js",
|
|
28
|
+
"prepublishOnly": "npm test"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"claude-code",
|
|
35
|
+
"codex",
|
|
36
|
+
"ai",
|
|
37
|
+
"spec-driven",
|
|
38
|
+
"momentum",
|
|
39
|
+
"workflow"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/LiminaLabsAI/momentum.git"
|
|
45
|
+
}
|
|
46
|
+
}
|