@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.
Files changed (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/adapters/antigravity/adapter.js +196 -0
  4. package/adapters/antigravity/hooks.json +39 -0
  5. package/adapters/antigravity/instructions/AGENTS.md +638 -0
  6. package/adapters/antigravity/instructions/header.md +3 -0
  7. package/adapters/antigravity/instructions/surfaces.md +153 -0
  8. package/adapters/antigravity/instructions/vars.json +1 -0
  9. package/adapters/antigravity/scripts/antigravity-hook-adapter.sh +147 -0
  10. package/adapters/antigravity/skills/momentum-orient/SKILL.md +29 -0
  11. package/adapters/antigravity/skills/momentum-reviewer-architecture/SKILL.md +39 -0
  12. package/adapters/antigravity/skills/momentum-reviewer-qa/SKILL.md +38 -0
  13. package/adapters/antigravity/skills/momentum-reviewer-security/SKILL.md +42 -0
  14. package/adapters/antigravity/skills/swarm-supervisor/SKILL.md +75 -0
  15. package/adapters/antigravity/workflows/continue.md +39 -0
  16. package/adapters/antigravity/workflows/dispatch.md +49 -0
  17. package/adapters/antigravity/workflows/handoff.md +38 -0
  18. package/adapters/antigravity/workflows/review-code.md +84 -0
  19. package/adapters/antigravity/workflows/scout.md +42 -0
  20. package/adapters/antigravity/workflows/swarm.md +203 -0
  21. package/adapters/claude-code/adapter.js +148 -0
  22. package/adapters/claude-code/commands/continue.md +82 -0
  23. package/adapters/claude-code/commands/dispatch.md +154 -0
  24. package/adapters/claude-code/commands/handoff.md +85 -0
  25. package/adapters/claude-code/commands/review-code.md +194 -0
  26. package/adapters/claude-code/commands/scout.md +119 -0
  27. package/adapters/claude-code/commands/swarm.md +242 -0
  28. package/adapters/claude-code/instructions/header.md +3 -0
  29. package/adapters/claude-code/instructions/vars.json +1 -0
  30. package/adapters/claude-code/settings.json +36 -0
  31. package/adapters/codex/adapter.js +225 -0
  32. package/adapters/codex/agents/momentum-reviewer-architecture.toml +57 -0
  33. package/adapters/codex/agents/momentum-reviewer-qa.toml +52 -0
  34. package/adapters/codex/agents/momentum-reviewer-security.toml +57 -0
  35. package/adapters/codex/agents/swarm-supervisor.toml +76 -0
  36. package/adapters/codex/commands/continue.md +40 -0
  37. package/adapters/codex/commands/dispatch.md +111 -0
  38. package/adapters/codex/commands/handoff.md +57 -0
  39. package/adapters/codex/commands/scout.md +90 -0
  40. package/adapters/codex/commands/swarm.md +334 -0
  41. package/adapters/codex/hooks.json +36 -0
  42. package/adapters/codex/instructions/AGENTS.md +691 -0
  43. package/adapters/codex/instructions/header.md +3 -0
  44. package/adapters/codex/instructions/surfaces.md +206 -0
  45. package/adapters/codex/instructions/vars.json +1 -0
  46. package/adapters/codex/skills/momentum-orient/SKILL.md +29 -0
  47. package/adapters/opencode/adapter.js +224 -0
  48. package/adapters/opencode/agents/momentum-reviewer-architecture.md +54 -0
  49. package/adapters/opencode/agents/momentum-reviewer-qa.md +48 -0
  50. package/adapters/opencode/agents/momentum-reviewer-security.md +48 -0
  51. package/adapters/opencode/agents/swarm-supervisor.md +51 -0
  52. package/adapters/opencode/commands/continue.md +40 -0
  53. package/adapters/opencode/commands/dispatch.md +111 -0
  54. package/adapters/opencode/commands/handoff.md +57 -0
  55. package/adapters/opencode/commands/review-code.md +194 -0
  56. package/adapters/opencode/commands/scout.md +90 -0
  57. package/adapters/opencode/commands/swarm.md +334 -0
  58. package/adapters/opencode/instructions/AGENTS.md +604 -0
  59. package/adapters/opencode/instructions/header.md +3 -0
  60. package/adapters/opencode/instructions/surfaces.md +119 -0
  61. package/adapters/opencode/instructions/vars.json +1 -0
  62. package/adapters/opencode/plugins/momentum.js +219 -0
  63. package/adapters/opencode/skills/momentum-lanes/SKILL.md +30 -0
  64. package/adapters/opencode/skills/momentum-orient/SKILL.md +29 -0
  65. package/adapters/opencode/skills/momentum-track/SKILL.md +28 -0
  66. package/adapters/opencode/skills/momentum-validate/SKILL.md +31 -0
  67. package/bin/antigravity.js +112 -0
  68. package/bin/ecosystem.js +1302 -0
  69. package/bin/lanes.js +300 -0
  70. package/bin/momentum.js +1857 -0
  71. package/bin/okf.js +72 -0
  72. package/bin/orchestration-commands.js +305 -0
  73. package/bin/state-commands.js +218 -0
  74. package/bin/swarm.js +1104 -0
  75. package/bin/waves.js +147 -0
  76. package/core/adapter-capabilities.md +112 -0
  77. package/core/adapter-parity-matrix.md +170 -0
  78. package/core/commands/brainstorm-idea.md +98 -0
  79. package/core/commands/brainstorm-phase.md +129 -0
  80. package/core/commands/complete-phase.md +130 -0
  81. package/core/commands/ecosystem.md +161 -0
  82. package/core/commands/hotfix.md +64 -0
  83. package/core/commands/initiative.md +119 -0
  84. package/core/commands/lanes.md +94 -0
  85. package/core/commands/log.md +45 -0
  86. package/core/commands/migrate.md +70 -0
  87. package/core/commands/review.md +31 -0
  88. package/core/commands/session.md +79 -0
  89. package/core/commands/start-phase.md +161 -0
  90. package/core/commands/start-project.md +142 -0
  91. package/core/commands/sync-docs.md +81 -0
  92. package/core/commands/systematic-debug.md +46 -0
  93. package/core/commands/track.md +54 -0
  94. package/core/commands/validate.md +96 -0
  95. package/core/ecosystem/layout.md +128 -0
  96. package/core/ecosystem/lib/index.js +245 -0
  97. package/core/ecosystem/lib/initiative.js +258 -0
  98. package/core/ecosystem/lib/pointer.js +198 -0
  99. package/core/ecosystem/lib/state.js +295 -0
  100. package/core/ecosystem/schema/ecosystem.schema.json +85 -0
  101. package/core/ecosystem/schema/initiative.schema.json +61 -0
  102. package/core/ecosystem/scripts/session-append.sh +167 -0
  103. package/core/ecosystem/templates/ecosystem-agents.md +79 -0
  104. package/core/ecosystem/templates/ecosystem-claude.md +79 -0
  105. package/core/ecosystem/templates/ecosystem-settings-claude.json +14 -0
  106. package/core/ecosystem/templates/initiative-template.md +50 -0
  107. package/core/engines/subagent-dispatch.md +80 -0
  108. package/core/git-hooks/commit-msg +14 -0
  109. package/core/git-hooks/contract.js +176 -0
  110. package/core/git-hooks/pre-push +15 -0
  111. package/core/git-hooks/run-check.js +160 -0
  112. package/core/instructions/legacy-project-md-hashes.json +12 -0
  113. package/core/instructions/navigation.md +14 -0
  114. package/core/instructions/rules-body.md +454 -0
  115. package/core/lanes/lib/board.js +161 -0
  116. package/core/lanes/lib/land.js +301 -0
  117. package/core/lanes/lib/signals.js +207 -0
  118. package/core/lanes/lib/state.js +325 -0
  119. package/core/lib/frontmatter.js +163 -0
  120. package/core/lib/okf-migrate.js +374 -0
  121. package/core/lib/okf-types.js +61 -0
  122. package/core/lifecycle-contract.md +59 -0
  123. package/core/orchestration/capability-routing.js +120 -0
  124. package/core/orchestration/continue.js +102 -0
  125. package/core/orchestration/dispatch.js +366 -0
  126. package/core/orchestration/events.js +197 -0
  127. package/core/orchestration/handoff.js +317 -0
  128. package/core/orchestration/index.js +50 -0
  129. package/core/orchestration/run-artifact.js +93 -0
  130. package/core/orchestration/scout.js +327 -0
  131. package/core/orchestration/session-log.js +123 -0
  132. package/core/orchestration/tracking.js +180 -0
  133. package/core/orchestration/types.js +131 -0
  134. package/core/scripts/brainstorm-gate.sh +157 -0
  135. package/core/scripts/check-history-reminder.sh +110 -0
  136. package/core/scripts/sessionstart-handoff.sh +180 -0
  137. package/core/specs-templates/CLAUDE.md +484 -0
  138. package/core/specs-templates/README.md +32 -0
  139. package/core/specs-templates/specs/README.md +30 -0
  140. package/core/specs-templates/specs/adhoc/README.md +31 -0
  141. package/core/specs-templates/specs/adhoc/_TEMPLATE.md +29 -0
  142. package/core/specs-templates/specs/architecture/ecosystem.md +214 -0
  143. package/core/specs-templates/specs/backlog/backlog.md +46 -0
  144. package/core/specs-templates/specs/backlog/details/.gitkeep +0 -0
  145. package/core/specs-templates/specs/changelog/.gitkeep +0 -0
  146. package/core/specs-templates/specs/decisions/0000-template.md +33 -0
  147. package/core/specs-templates/specs/decisions/README.md +29 -0
  148. package/core/specs-templates/specs/decisions/impact-map.md +14 -0
  149. package/core/specs-templates/specs/decisions/index.md +4 -0
  150. package/core/specs-templates/specs/index.md +16 -0
  151. package/core/specs-templates/specs/phases/README.md +41 -0
  152. package/core/specs-templates/specs/phases/index.md +6 -0
  153. package/core/specs-templates/specs/planning/roadmap.md +23 -0
  154. package/core/specs-templates/specs/status.md +70 -0
  155. package/core/specs-templates/specs/vision/principles.md +16 -0
  156. package/core/specs-templates/specs/vision/project-charter.md +35 -0
  157. package/core/specs-templates/specs/vision/success-criteria.md +19 -0
  158. package/core/swarm/absorb.js +294 -0
  159. package/core/swarm/conductor.js +499 -0
  160. package/core/swarm/focus.js +151 -0
  161. package/core/swarm/inbox.js +222 -0
  162. package/core/swarm/join.js +138 -0
  163. package/core/swarm/lib/board.js +178 -0
  164. package/core/swarm/lib/brief.js +157 -0
  165. package/core/swarm/lib/git-sha-cache.js +71 -0
  166. package/core/swarm/lib/incremental-log.js +117 -0
  167. package/core/swarm/lib/manifest.js +578 -0
  168. package/core/swarm/lib/pre-merge.js +112 -0
  169. package/core/swarm/lib/saga.js +146 -0
  170. package/core/swarm/lib/sessions.js +83 -0
  171. package/core/swarm/lib/tokens.js +255 -0
  172. package/core/swarm/lib/wave-ordering.js +55 -0
  173. package/core/swarm/schema/board.schema.json +54 -0
  174. package/core/swarm/schema/contract.schema.json +56 -0
  175. package/core/swarm/schema/dispatch-run.schema.json +70 -0
  176. package/core/swarm/schema/manifest.schema.json +234 -0
  177. package/core/swarm/schema/signal.schema.json +61 -0
  178. package/core/swarm/signals.js +296 -0
  179. package/core/swarm/supervise.md +81 -0
  180. package/core/waves/lib/plan-graph.js +175 -0
  181. package/core/waves/lib/waves.js +83 -0
  182. package/package.json +46 -0
@@ -0,0 +1,374 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * OKF v0.1 bundle migration + index generation + conformance checking
5
+ * (Phase 24, ADR-0005).
6
+ *
7
+ * migrate(targetDir):
8
+ * 1. specs/phases/index.json → per-phase overview.md frontmatter
9
+ * (type/status/tags/deps), then the JSON file is deleted. A listed
10
+ * phase without an overview.md gets a minimal one — no data is lost.
11
+ * 2. specs/decisions/impact-map.json → impact-map.md (one table).
12
+ * 3. Frontmatter sweep: every frontmatter-less specs/**\/*.md gains
13
+ * `type:` (core/lib/okf-types.js taxonomy) with the body preserved
14
+ * byte-for-byte; parseable frontmatter missing `type` gets a single
15
+ * line inserted textually; unparseable frontmatter → file untouched
16
+ * (reported). Reserved index.md / log.md are never touched.
17
+ * 4. Root specs/index.md (`okf_version: "0.1"`) + generated listings
18
+ * for phases/ and decisions/.
19
+ * Idempotent: a second run reports zero changes.
20
+ *
21
+ * All writes are user-content (specs/) — callers must NOT record these
22
+ * paths in the managed-file manifest (orphan cleanup must never touch
23
+ * user specs).
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const fm = require('./frontmatter');
30
+ const { typeForPath, isReserved } = require('./okf-types');
31
+
32
+ const OKF_VERSION = '0.1';
33
+
34
+ function readIf(p) {
35
+ try {
36
+ return fs.readFileSync(p, 'utf8');
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function walkMd(dir, base = dir, out = []) {
43
+ let entries;
44
+ try {
45
+ entries = fs.readdirSync(dir, { withFileTypes: true });
46
+ } catch {
47
+ return out;
48
+ }
49
+ for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
50
+ const abs = path.join(dir, e.name);
51
+ if (e.isDirectory()) walkMd(abs, base, out);
52
+ else if (e.name.endsWith('.md')) out.push(path.relative(base, abs).split(path.sep).join('/'));
53
+ }
54
+ return out;
55
+ }
56
+
57
+ /** First `# ` heading text, or null. */
58
+ function firstHeading(content) {
59
+ const m = String(content).match(/^#\s+(.+)$/m);
60
+ return m ? m[1].trim() : null;
61
+ }
62
+
63
+ // --- step 1: distribute index.json ----------------------------------------
64
+
65
+ function distributePhaseIndex(specsRoot, actions, dryRun) {
66
+ const indexPath = path.join(specsRoot, 'phases', 'index.json');
67
+ const rawIndex = readIf(indexPath);
68
+ if (rawIndex === null) return;
69
+
70
+ let index;
71
+ try {
72
+ index = JSON.parse(rawIndex);
73
+ } catch {
74
+ actions.warnings.push('phases/index.json is unreadable JSON — left in place');
75
+ return;
76
+ }
77
+
78
+ for (const [id, entry] of Object.entries(index.phases || {})) {
79
+ const overviewAbs = path.join(specsRoot, 'phases', id, 'overview.md');
80
+ const existing = readIf(overviewAbs);
81
+ const meta = {
82
+ type: 'Phase',
83
+ status: String(entry.status || 'planned'),
84
+ };
85
+ const topics = Array.isArray(entry.topics) ? entry.topics.map(String) : [];
86
+ if (topics.length) meta.tags = topics;
87
+ const deps = Array.isArray(entry.deps) ? entry.deps.map(String) : [];
88
+ if (deps.length) meta.deps = deps;
89
+
90
+ if (existing === null) {
91
+ const body = `\n# ${id}\n\n> Recreated from specs/phases/index.json during the OKF migration —\n> the phase directory had no overview.md.\n`;
92
+ actions.writes.push({ rel: `phases/${id}/overview.md`, content: fm.compose(meta, body), why: 'index.json entry without overview.md' });
93
+ continue;
94
+ }
95
+
96
+ const parsed = fm.parse(existing);
97
+ if (parsed.data === null && fm.hasBlock(existing)) {
98
+ actions.warnings.push(`phases/${id}/overview.md has frontmatter outside the momentum subset — index.json data NOT distributed to it`);
99
+ continue;
100
+ }
101
+ if (parsed.data && parsed.data.status) continue; // already migrated
102
+
103
+ if (parsed.data === null) {
104
+ // No frontmatter at all — prepend the full block, body byte-preserved.
105
+ actions.writes.push({ rel: `phases/${id}/overview.md`, content: fm.compose(meta, `\n${existing}`), why: 'distributed from index.json' });
106
+ } else {
107
+ // Frontmatter exists (e.g. swarm brief) — merge missing keys, keep order/unknowns.
108
+ const merged = { ...meta, ...parsed.data };
109
+ if (!merged.type) merged.type = 'Phase';
110
+ actions.writes.push({ rel: `phases/${id}/overview.md`, content: fm.compose(merged, parsed.body), why: 'merged index.json data into existing frontmatter' });
111
+ }
112
+ }
113
+
114
+ actions.deletes.push({ rel: 'phases/index.json', why: 'distributed into phase overview.md frontmatter' });
115
+ if (!dryRun) {
116
+ for (const w of actions.writes) {
117
+ const abs = path.join(specsRoot, w.rel);
118
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
119
+ fs.writeFileSync(abs, w.content);
120
+ }
121
+ fs.unlinkSync(indexPath);
122
+ }
123
+ }
124
+
125
+ // --- step 2: impact-map.json → impact-map.md -------------------------------
126
+
127
+ function convertImpactMap(specsRoot, actions, dryRun) {
128
+ const jsonPath = path.join(specsRoot, 'decisions', 'impact-map.json');
129
+ const raw = readIf(jsonPath);
130
+ if (raw === null) return;
131
+
132
+ let map;
133
+ try {
134
+ map = JSON.parse(raw);
135
+ } catch {
136
+ actions.warnings.push('decisions/impact-map.json is unreadable JSON — left in place');
137
+ return;
138
+ }
139
+
140
+ const rows = [];
141
+ for (const [topic, spec] of Object.entries(map.topics || {})) {
142
+ for (const f of spec.files || []) {
143
+ rows.push(`| ${topic} | ${f.path} | ${f.section || ''} |`);
144
+ }
145
+ }
146
+ const body = [
147
+ '',
148
+ '# Impact Map',
149
+ '',
150
+ 'Maps topic keywords to the spec files/sections they affect. Used by',
151
+ '`/sync-docs` to find documents needing updates when a phase history',
152
+ 'entry carries matching `Topics:`.',
153
+ '',
154
+ '| Topic | File | Section |',
155
+ '|-------|------|---------|',
156
+ ...rows,
157
+ '',
158
+ ].join('\n');
159
+ const content = fm.compose(
160
+ {
161
+ type: 'Impact Map',
162
+ title: 'Decision Impact Map',
163
+ description: 'Topic keywords → spec files/sections, consumed by /sync-docs.',
164
+ },
165
+ body,
166
+ );
167
+
168
+ const mdRel = 'decisions/impact-map.md';
169
+ actions.converted.push({ rel: mdRel, content, why: 'converted from impact-map.json' });
170
+ actions.deletes.push({ rel: 'decisions/impact-map.json', why: 'converted to impact-map.md' });
171
+ if (!dryRun) {
172
+ fs.writeFileSync(path.join(specsRoot, mdRel), content);
173
+ fs.unlinkSync(jsonPath);
174
+ }
175
+ }
176
+
177
+ // --- step 3: frontmatter sweep ---------------------------------------------
178
+
179
+ function sweepFrontmatter(specsRoot, actions, dryRun) {
180
+ // Files the distribute step already (re)wrote carry frontmatter now; in
181
+ // dry-run they are still frontmatter-less on disk — skip to avoid
182
+ // double-counting them in the preview.
183
+ const pendingWrites = new Set(actions.writes.map((w) => w.rel));
184
+ for (const rel of walkMd(specsRoot)) {
185
+ if (isReserved(rel)) continue;
186
+ if (pendingWrites.has(rel)) continue;
187
+ const abs = path.join(specsRoot, rel);
188
+ const content = readIf(abs);
189
+ if (content === null) continue;
190
+
191
+ const type = typeForPath(rel);
192
+ if (!type) continue;
193
+
194
+ if (!fm.hasBlock(content)) {
195
+ const updated = fm.compose({ type }, `\n${content}`);
196
+ actions.injected.push(rel);
197
+ if (!dryRun) fs.writeFileSync(abs, updated);
198
+ continue;
199
+ }
200
+ const parsed = fm.parse(content);
201
+ if (parsed.data === null) {
202
+ actions.warnings.push(`${rel} has frontmatter outside the momentum subset — left untouched`);
203
+ continue;
204
+ }
205
+ if (!parsed.data.type) {
206
+ const updated = fm.insertTypeLine(content, type);
207
+ actions.injected.push(rel);
208
+ if (!dryRun) fs.writeFileSync(abs, updated);
209
+ }
210
+ }
211
+ }
212
+
213
+ // --- step 4: index generation ----------------------------------------------
214
+
215
+ function phaseListing(specsRoot) {
216
+ const phasesDir = path.join(specsRoot, 'phases');
217
+ let entries;
218
+ try {
219
+ entries = fs.readdirSync(phasesDir, { withFileTypes: true });
220
+ } catch {
221
+ return null;
222
+ }
223
+ const lines = ['# Phases', ''];
224
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
225
+ for (const id of dirs) {
226
+ const overview = readIf(path.join(phasesDir, id, 'overview.md'));
227
+ if (overview === null) continue;
228
+ const { data } = fm.parse(overview);
229
+ const status = data && data.status ? data.status : 'unknown';
230
+ lines.push(`* [${id}](/phases/${id}/overview.md) - ${status}`);
231
+ }
232
+ const readme = fs.existsSync(path.join(phasesDir, 'README.md'));
233
+ if (readme) lines.push('', '# Guides', '', '* [Phases guide](/phases/README.md) - conventions for phase directories');
234
+ return `${lines.join('\n')}\n`;
235
+ }
236
+
237
+ function decisionListing(specsRoot) {
238
+ const decDir = path.join(specsRoot, 'decisions');
239
+ let entries;
240
+ try {
241
+ entries = fs.readdirSync(decDir, { withFileTypes: true });
242
+ } catch {
243
+ return null;
244
+ }
245
+ const lines = ['# Decisions', ''];
246
+ for (const e of entries.filter((x) => x.isFile() && x.name.endsWith('.md') && x.name !== 'index.md' && x.name !== 'README.md').sort((a, b) => (a.name < b.name ? -1 : 1))) {
247
+ const content = readIf(path.join(decDir, e.name)) || '';
248
+ const { data, body } = fm.parse(content);
249
+ const title = (data && data.title) || firstHeading(body || content) || e.name;
250
+ const desc = data && data.description ? ` - ${data.description}` : '';
251
+ lines.push(`* [${title}](/decisions/${e.name})${desc}`);
252
+ }
253
+ return `${lines.join('\n')}\n`;
254
+ }
255
+
256
+ function rootIndex(specsRoot) {
257
+ const lines = [];
258
+ const entryIf = (rel, title, desc) => {
259
+ if (fs.existsSync(path.join(specsRoot, rel))) lines.push(`* [${title}](/${rel}) - ${desc}`);
260
+ };
261
+ lines.push('# Specs', '');
262
+ entryIf('status.md', 'Status', 'current project state — the first file to read');
263
+ entryIf('backlog/backlog.md', 'Backlog', 'bugs, features, tech debt, enhancements');
264
+ entryIf('planning/roadmap.md', 'Roadmap', 'phase timeline and upcoming work');
265
+ lines.push('', '# Directories', '');
266
+ // The listing files are written in this same pass — key on the source
267
+ // directories, not the listing files, so the output is run-stable.
268
+ if (fs.existsSync(path.join(specsRoot, 'phases'))) {
269
+ lines.push('* [Phases](/phases/index.md) - one directory per phase (overview, plan, tasks, history)');
270
+ }
271
+ if (fs.existsSync(path.join(specsRoot, 'decisions'))) {
272
+ lines.push('* [Decisions](/decisions/index.md) - architecture decision records + impact map');
273
+ }
274
+ if (fs.existsSync(path.join(specsRoot, 'adhoc'))) {
275
+ lines.push('* [Ad-hoc work](/adhoc/) - quick-task and spike records (Rule 14)');
276
+ }
277
+ if (fs.existsSync(path.join(specsRoot, 'changelog'))) {
278
+ lines.push('* [Changelog](/changelog/) - month-by-month change log');
279
+ }
280
+ return `---\nokf_version: ${fm.quoteScalar(OKF_VERSION)}\n---\n\n${lines.join('\n')}\n`;
281
+ }
282
+
283
+ /**
284
+ * Generate/refresh the bundle indexes (root + phases + decisions).
285
+ * Deterministic and idempotent; used by migrate() and `momentum okf index`.
286
+ */
287
+ function generateIndexes(targetDir, { dryRun = false } = {}) {
288
+ const specsRoot = path.join(targetDir, 'specs');
289
+ if (!fs.existsSync(specsRoot)) return [];
290
+ const writes = [
291
+ { rel: 'index.md', content: rootIndex(specsRoot) },
292
+ { rel: 'phases/index.md', content: phaseListing(specsRoot) },
293
+ { rel: 'decisions/index.md', content: decisionListing(specsRoot) },
294
+ ].filter((w) => w.content !== null);
295
+
296
+ const changed = [];
297
+ for (const w of writes) {
298
+ const abs = path.join(specsRoot, w.rel);
299
+ if (readIf(abs) === w.content) continue;
300
+ changed.push(w.rel);
301
+ if (!dryRun) {
302
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
303
+ fs.writeFileSync(abs, w.content);
304
+ }
305
+ }
306
+ return changed;
307
+ }
308
+
309
+ /**
310
+ * Run the full OKF migration. Returns a report:
311
+ * { applicable, writes, converted, injected, deletes, indexes, warnings }
312
+ */
313
+ function migrate(targetDir, { dryRun = false } = {}) {
314
+ const specsRoot = path.join(targetDir, 'specs');
315
+ const actions = { applicable: true, writes: [], converted: [], injected: [], deletes: [], indexes: [], warnings: [] };
316
+ if (!fs.existsSync(specsRoot)) {
317
+ actions.applicable = false;
318
+ return actions;
319
+ }
320
+ distributePhaseIndex(specsRoot, actions, dryRun);
321
+ convertImpactMap(specsRoot, actions, dryRun);
322
+ sweepFrontmatter(specsRoot, actions, dryRun);
323
+ // Index generation needs the swept files on disk; in dry-run it previews
324
+ // against the current tree instead.
325
+ actions.indexes = generateIndexes(targetDir, { dryRun });
326
+ return actions;
327
+ }
328
+
329
+ // --- conformance check -------------------------------------------------------
330
+
331
+ /**
332
+ * OKF v0.1 conformance report for targetDir/specs.
333
+ * @returns {{ applicable, files: number, violations: {rel, problem}[], warnings: string[] }}
334
+ */
335
+ function check(targetDir) {
336
+ const specsRoot = path.join(targetDir, 'specs');
337
+ const report = { applicable: true, files: 0, violations: [], warnings: [] };
338
+ if (!fs.existsSync(specsRoot)) {
339
+ report.applicable = false;
340
+ return report;
341
+ }
342
+ for (const rel of walkMd(specsRoot)) {
343
+ report.files += 1;
344
+ const content = readIf(path.join(specsRoot, rel));
345
+ if (content === null) continue;
346
+
347
+ if (isReserved(rel)) {
348
+ const base = rel.split('/').pop();
349
+ if (base === 'index.md' && rel !== 'index.md' && fm.hasBlock(content)) {
350
+ report.violations.push({ rel, problem: 'reserved index.md must not carry frontmatter (only the bundle root may)' });
351
+ }
352
+ if (base === 'log.md' && fm.hasBlock(content)) {
353
+ report.violations.push({ rel, problem: 'reserved log.md must not carry frontmatter' });
354
+ }
355
+ continue;
356
+ }
357
+
358
+ if (!fm.hasBlock(content)) {
359
+ report.violations.push({ rel, problem: 'missing YAML frontmatter' });
360
+ continue;
361
+ }
362
+ const { data } = fm.parse(content);
363
+ if (data === null) {
364
+ report.warnings.push(`${rel}: frontmatter outside the momentum subset — type not verifiable`);
365
+ continue;
366
+ }
367
+ if (!data.type || String(data.type).trim() === '') {
368
+ report.violations.push({ rel, problem: 'frontmatter lacks a non-empty `type`' });
369
+ }
370
+ }
371
+ return report;
372
+ }
373
+
374
+ module.exports = { migrate, generateIndexes, check, OKF_VERSION };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * OKF v0.1 type taxonomy for momentum spec trees (ADR-0005).
5
+ *
6
+ * Maps a path RELATIVE TO THE specs/ ROOT (forward slashes) to the
7
+ * frontmatter `type` momentum assigns that concept document. First match
8
+ * wins. Types are producer-defined per OKF §frontmatter; consumers must
9
+ * tolerate unknown types, so this vocabulary can grow freely.
10
+ *
11
+ * Single source of truth for: the upgrade migration (okf-migrate.js),
12
+ * the conformance checker (`momentum okf check`), and the templates.
13
+ */
14
+
15
+ /** Reserved OKF filenames — never concept documents (SPEC.md §reserved). */
16
+ const RESERVED = ['index.md', 'log.md'];
17
+
18
+ function isReserved(relPath) {
19
+ const base = relPath.split('/').pop();
20
+ return RESERVED.includes(base);
21
+ }
22
+
23
+ const RULES = [
24
+ [/^status\.md$/, 'Status'],
25
+ [/^backlog\/backlog\.md$/, 'Backlog'],
26
+ [/^backlog\/details\/.+\.md$/, 'Backlog Detail'],
27
+ [/^planning\/roadmap\.md$/, 'Roadmap'],
28
+ [/^planning\/.+\.md$/, 'Planning Note'],
29
+ [/^decisions\/impact-map\.md$/, 'Impact Map'],
30
+ [/^decisions\/\d{4}-.+\.md$/, 'Decision'],
31
+ [/^phases\/[^/]+\/overview\.md$/, 'Phase'],
32
+ [/^phases\/[^/]+\/plan\.md$/, 'Plan'],
33
+ [/^phases\/[^/]+\/tasks\.md$/, 'Task List'],
34
+ [/^phases\/[^/]+\/history\.md$/, 'Phase History'],
35
+ [/^phases\/[^/]+\/retrospective\.md$/, 'Retrospective'],
36
+ [/^phases\/[^/]+\/evidence\/.+\.md$/, 'Evidence'],
37
+ [/^adhoc\/[^/]+\/record\.md$/, 'Ad-hoc Record'],
38
+ [/^adhoc\/_TEMPLATE\.md$/, 'Ad-hoc Record'], // scaffold template — copies become record.md
39
+ [/^changelog\/.+\.md$/, 'Changelog'],
40
+ [/^architecture\/.+\.md$/, 'Architecture Spec'],
41
+ [/^vision\/.+\.md$/, 'Vision'],
42
+ [/(^|\/)README\.md$/, 'Guide'],
43
+ ];
44
+
45
+ const FALLBACK = 'Note';
46
+
47
+ /**
48
+ * The `type` for a specs-relative .md path, or null for reserved
49
+ * filenames (which must not carry concept frontmatter).
50
+ */
51
+ function typeForPath(relPath) {
52
+ const posix = relPath.replace(/\\/g, '/').replace(/^\.\//, '');
53
+ if (!posix.endsWith('.md')) return null;
54
+ if (isReserved(posix)) return null;
55
+ for (const [pattern, type] of RULES) {
56
+ if (pattern.test(posix)) return type;
57
+ }
58
+ return FALLBACK;
59
+ }
60
+
61
+ module.exports = { typeForPath, isReserved, RESERVED, RULES, FALLBACK };
@@ -0,0 +1,59 @@
1
+ # Lifecycle Contract (Phase 19 — Lifecycle Hardening)
2
+
3
+ > Canonical reference for momentum's git-lifecycle + ad-hoc-work enforcement.
4
+ > The single source of truth in code is [`core/git-hooks/contract.js`](git-hooks/contract.js)
5
+ > (pure functions + constants). This doc explains the *why*; that module is the *what*.
6
+
7
+ ## Principle: enforce in vendor-neutral git, document the rest
8
+
9
+ momentum is forge-agnostic (the same DIP that gives it per-agent adapters). So
10
+ **enforcement lives in plain git hooks** — pure git, no forge API — which work
11
+ on any forge (GitHub / GitLab / Bitbucket / Gitea / bare remote) and under any
12
+ agent. Forge-specific server-side protection (GitHub Rulesets, GitLab protected
13
+ branches, …) is recommended as *optional* hardening in docs only; it is never a
14
+ core dependency. Forge-specific scaffolding, if ever built, belongs behind a
15
+ future "forge adapter," not in core.
16
+
17
+ ## Hook delivery
18
+
19
+ - Hooks ship from `core/git-hooks/` and install into a tracked `.githooks/`
20
+ dir in the target repo (a dedicated install step, separate from the
21
+ `core/scripts/` → `scripts/` copy, so the shared `contract.js` lands once).
22
+ - `bin/momentum.js` wires them via `git config core.hooksPath .githooks` — no
23
+ husky, no lefthook, zero runtime dependencies (`fs`/`path`/`process` only).
24
+ - **Warn, don't clobber**: if the target already sets `core.hooksPath` or uses
25
+ husky, momentum warns and skips rather than overwriting (the BUG-008 lesson).
26
+
27
+ ## Escape hatch
28
+
29
+ Enforcement is hard-block (exit ≠ 0) — the only design that actually fixes
30
+ "advice masquerading as enforcement." Two documented escapes:
31
+
32
+ | Mechanism | Use |
33
+ |---|---|
34
+ | `MOMENTUM_SKIP_HOOKS=1` (env var) | Emergency bypass of all momentum hooks for a single command. |
35
+ | `.momentum/merge-approved` (single-use sentinel) | Authorizes ONE push to a protected branch. The agent creates it only after the user approves a merge; the `pre-push` hook consumes (deletes) it on use. Mirrors the `.momentum/brainstorm-active` pattern. |
36
+
37
+ ## Enforced controls
38
+
39
+ | Control | Hook | Rule |
40
+ |---|---|---|
41
+ | Conventional-commit subject | `commit-msg` | Rule 6 |
42
+ | Block direct push to `main`/`staging` without the sentinel | `pre-push` | Rule 6 |
43
+ | Block a release-tag push without verification evidence | `pre-push` (tag path) | Rule 12 |
44
+
45
+ Allowed commit types: `feat fix docs refactor chore infra test perf build ci style revert`.
46
+ A release tag is `vMAJOR.MINOR.PATCH…`. Verification evidence = a non-empty
47
+ `## Verification Evidence` section in the relevant `retrospective.md`.
48
+
49
+ ## Work types (Rule 14)
50
+
51
+ | Type | When | Ceremony |
52
+ |---|---|---|
53
+ | `phase` | Net-new features, cross-cutting work | Full: brainstorm → plan → groups → verify → release. |
54
+ | `quick-task` | A bounded fix/chore with a clear blast radius | An ad-hoc record (`specs/adhoc/<id>/record.md`) + the Rule 12 evidence gate. No phase scaffold. |
55
+ | `spike` | Time-boxed exploration, throwaway | Declared up front; exempt from acceptance gates; record what was learned. |
56
+
57
+ Escalation (`quick-task` → `phase`) criteria live in **Rule 14** (CLAUDE.md):
58
+ touches > N files, modifies `specs/architecture/`, needs an ADR, changes a
59
+ public contract, or displaces a planned phase.
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ // Capability-driven routing for orchestration primitives.
4
+ //
5
+ // Reads an adapter's uniform-boolean capability declarations
6
+ // (post-ENH-023 + ENH-024 cleanup in Phase 11 G0) and decides which
7
+ // mode each primitive should run in. Every degraded mode is labeled —
8
+ // the user is told up front when their adapter forces sequential mode
9
+ // or a missing hook surface.
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+
14
+ const PRIMITIVES = Object.freeze(['scout', 'dispatch', 'handoff', 'continue']);
15
+
16
+ /**
17
+ * Choose the execution mode for a primitive on a given adapter.
18
+ *
19
+ * @param {object} adapter adapter module (the export of adapters/<name>/adapter.js)
20
+ * @param {string} primitive one of: 'scout' | 'dispatch' | 'handoff' | 'continue'
21
+ * @returns {{ mode: 'parallel'|'sequential', notes: string[] }}
22
+ */
23
+ function chooseMode(adapter, primitive) {
24
+ if (!PRIMITIVES.includes(primitive)) {
25
+ throw new Error(
26
+ `orchestration/capability-routing: unknown primitive "${primitive}". ` +
27
+ `Valid primitives: ${PRIMITIVES.join(', ')}`,
28
+ );
29
+ }
30
+ const caps = (adapter && adapter.capabilities) || {};
31
+ const roadmap = (adapter && adapter.roadmap) || {};
32
+ const notes = [];
33
+
34
+ // scout, handoff, continue are inherently single-target — there's no
35
+ // parallel/sequential distinction. They run "parallel" by default
36
+ // (i.e., a single sub-agent / single in-process invocation). The
37
+ // only useful degradation note is when the adapter lacks subagents
38
+ // entirely — then CLI is the only path.
39
+ if (primitive === 'scout' || primitive === 'handoff' || primitive === 'continue') {
40
+ if (caps.subagents === false) {
41
+ notes.push(
42
+ `this adapter does not declare subagents — ${primitive} runs in-process via the CLI floor`,
43
+ );
44
+ }
45
+ return { mode: 'parallel', notes };
46
+ }
47
+
48
+ // dispatch is where parallel vs sequential matters.
49
+ if (primitive === 'dispatch') {
50
+ if (caps.parallelSubagents === true) {
51
+ return { mode: 'parallel', notes };
52
+ }
53
+ if (caps.subagents === true && caps.parallelSubagents === false) {
54
+ const reason = roadmap.parallelSubagents
55
+ ? ` (${roadmap.parallelSubagents})`
56
+ : '';
57
+ notes.push(
58
+ `this adapter does not declare parallel subagents — running sequentially${reason}`,
59
+ );
60
+ return { mode: 'sequential', notes };
61
+ }
62
+ if (caps.subagents === false) {
63
+ notes.push(
64
+ 'this adapter does not declare subagents — dispatch runs sequentially via the CLI floor',
65
+ );
66
+ return { mode: 'sequential', notes };
67
+ }
68
+ // Default conservative path: sequential with a note.
69
+ notes.push('subagent capability undeclared — running sequentially as a safe default');
70
+ return { mode: 'sequential', notes };
71
+ }
72
+
73
+ // Shouldn't reach — guarded by the early PRIMITIVES check above.
74
+ return { mode: 'parallel', notes };
75
+ }
76
+
77
+ /**
78
+ * Returns true if the adapter has a SessionStart hook surface that can
79
+ * print the handoff auto-greet banner. Otherwise, callers should fall
80
+ * back to a primary-instruction banner or rely on explicit
81
+ * `/continue` / `momentum continue`.
82
+ */
83
+ function supportsSessionStartHook(adapter) {
84
+ return Boolean(adapter && adapter.capabilities && adapter.capabilities.sessionStartHook === true);
85
+ }
86
+
87
+ /**
88
+ * Returns true if the adapter has slash commands. Antigravity is the
89
+ * notable false today — its users invoke primitives via NL inference
90
+ * or the momentum CLI.
91
+ */
92
+ function supportsSlashCommands(adapter) {
93
+ return Boolean(adapter && adapter.capabilities && adapter.capabilities.slashCommands === true);
94
+ }
95
+
96
+ /**
97
+ * Load an adapter by name from a momentum source tree.
98
+ *
99
+ * @param {string} momentumRoot absolute path to the momentum repo root
100
+ * (the directory containing `adapters/`)
101
+ * @param {string} name adapter directory name (e.g., 'claude-code')
102
+ * @returns {object} adapter module
103
+ */
104
+ function loadAdapter(momentumRoot, name) {
105
+ const adapterPath = path.join(momentumRoot, 'adapters', name, 'adapter.js');
106
+ if (!fs.existsSync(adapterPath)) {
107
+ throw new Error(
108
+ `orchestration/capability-routing: adapter "${name}" not found at ${adapterPath}`,
109
+ );
110
+ }
111
+ return require(adapterPath);
112
+ }
113
+
114
+ module.exports = {
115
+ PRIMITIVES,
116
+ chooseMode,
117
+ supportsSessionStartHook,
118
+ supportsSlashCommands,
119
+ loadAdapter,
120
+ };