@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,317 @@
1
+ 'use strict';
2
+
3
+ // handoff — cross-session control transfer with a structured context block.
4
+ //
5
+ // The originating agent (or CLI) calls handoff() to write a context
6
+ // block to the receiving repo's `.momentum/inbox/handoff-NNN.md`. The
7
+ // receiving agent — when a fresh session starts there — sees a
8
+ // SessionStart banner (Claude Code + Codex) or invokes /continue
9
+ // explicitly to read and act on the block.
10
+ //
11
+ // Cheap-layer side effects:
12
+ // - inbox file written in receiving repo
13
+ // - one line appended to the ecosystem session log
14
+ //
15
+ // Curated-layer side effects (auto, since handoff IS a decision):
16
+ // - [DECISION] entry in originating repo's active phase history
17
+ // (handed off to core/orchestration/tracking once G4 lands;
18
+ // for now the entry is written here directly).
19
+
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+
23
+ const eventsLib = require('./events');
24
+ const runArtifact = require('./run-artifact');
25
+ const sessionLog = require('./session-log');
26
+ const types = require('./types');
27
+
28
+ const INBOX_DIRNAME = path.join('.momentum', 'inbox');
29
+ const READ_DIRNAME = 'read';
30
+
31
+ const SENTINELS = {
32
+ begin: '<!-- MOMENTUM_HANDOFF_BEGIN -->',
33
+ end: '<!-- MOMENTUM_HANDOFF_END -->',
34
+ };
35
+
36
+ /**
37
+ * Write a handoff context block to the receiving repo's inbox.
38
+ *
39
+ * @param {object} opts
40
+ * @param {string} opts.fromRepo originating repo (absolute path)
41
+ * @param {string} opts.toRepo receiving repo (absolute path)
42
+ * @param {string} opts.summary one-line summary
43
+ * @param {string[]} [opts.decisions] bullet list
44
+ * @param {string[]} [opts.filesTouched]
45
+ * @param {string[]} [opts.verificationCommands]
46
+ * @param {string[]} [opts.openQuestions]
47
+ * @param {string} [opts.originatingPhase] e.g. "phase-11-orchestration-handover"
48
+ * @param {object} [opts.ecosystem] { rootPath, memberId }
49
+ * @param {object} [opts.emitter]
50
+ * @param {boolean} [opts.silent]
51
+ * @returns {Promise<HandoffBlock>}
52
+ */
53
+ async function handoff(opts) {
54
+ const {
55
+ fromRepo, toRepo, summary,
56
+ decisions = [], filesTouched = [],
57
+ verificationCommands = [], openQuestions = [],
58
+ ecosystem,
59
+ } = opts;
60
+
61
+ if (!fromRepo || !toRepo || !summary) {
62
+ throw new Error('orchestration/handoff: fromRepo, toRepo, summary are required');
63
+ }
64
+ if (!fs.existsSync(toRepo)) {
65
+ throw new Error(`orchestration/handoff: toRepo does not exist: ${toRepo}`);
66
+ }
67
+
68
+ const originatingPhase = opts.originatingPhase || detectActivePhase(fromRepo);
69
+
70
+ const emitter = opts.emitter || eventsLib.createEmitter({ primitive: 'handoff', repo: fromRepo });
71
+ if (!opts.emitter && !opts.silent) {
72
+ emitter.on(eventsLib.createRenderer({ stream: process.stdout }));
73
+ }
74
+ if (ecosystem) {
75
+ emitter.on(eventsLib.createPersister({
76
+ ecosystemRoot: ecosystem.rootPath,
77
+ memberId: ecosystem.memberId,
78
+ }));
79
+ }
80
+
81
+ emitter.emit('started', { message: `${path.basename(fromRepo)} → ${path.basename(toRepo)}` });
82
+
83
+ // Allocate handoff id in the RECEIVING repo so multiple originating
84
+ // repos don't collide on the same numeric counter for the same
85
+ // target.
86
+ const handoffId = runArtifact.allocateRunId({ repo: toRepo, primitive: 'handoff' });
87
+ const inboxDir = path.join(toRepo, INBOX_DIRNAME);
88
+ fs.mkdirSync(inboxDir, { recursive: true });
89
+ const inboxPath = path.join(inboxDir, `handoff-${handoffId}.md`);
90
+ const createdAt = new Date().toISOString();
91
+
92
+ const block = {
93
+ fromRepo,
94
+ toRepo,
95
+ summary,
96
+ decisions: arrayify(decisions),
97
+ filesTouched: arrayify(filesTouched),
98
+ verificationCommands: arrayify(verificationCommands),
99
+ openQuestions: arrayify(openQuestions),
100
+ originatingPhase,
101
+ originatingHistoryRef: '', // populated below after history write
102
+ inboxPath,
103
+ handoffId,
104
+ createdAt,
105
+ };
106
+
107
+ // Write inbox file with sentinel-fenced sections.
108
+ fs.writeFileSync(inboxPath, renderInbox(block));
109
+ emitter.emit('step', { message: `wrote ${path.relative(fromRepo, inboxPath) || inboxPath}` });
110
+
111
+ // Emit [DECISION] in the originating repo's active phase history if
112
+ // a phase directory exists. G4's tracking helper will subsume this;
113
+ // for now we inline the write so the contract holds even before G4
114
+ // wires the dedicated helper.
115
+ if (originatingPhase) {
116
+ const historyRef = appendOriginatingDecision({
117
+ fromRepo,
118
+ originatingPhase,
119
+ handoffId,
120
+ toRepo,
121
+ summary,
122
+ createdAt,
123
+ });
124
+ if (historyRef) block.originatingHistoryRef = historyRef;
125
+ }
126
+
127
+ emitter.emit('finished', { runArtifactPath: inboxPath });
128
+
129
+ return types.validateHandoffBlock(block);
130
+ }
131
+
132
+ // ── render / parse the inbox block ─────────────────────────────────────────
133
+
134
+ function renderInbox(block) {
135
+ const lines = [
136
+ `# handoff-${block.handoffId}`,
137
+ '',
138
+ SENTINELS.begin,
139
+ `**fromRepo:** ${block.fromRepo}`,
140
+ `**toRepo:** ${block.toRepo}`,
141
+ `**handoffId:** ${block.handoffId}`,
142
+ `**createdAt:** ${block.createdAt}`,
143
+ `**originatingPhase:** ${block.originatingPhase || ''}`,
144
+ `**originatingHistoryRef:** ${block.originatingHistoryRef || ''}`,
145
+ '',
146
+ '## Summary',
147
+ block.summary,
148
+ '',
149
+ ];
150
+ if (block.decisions.length) {
151
+ lines.push('## Decisions');
152
+ for (const d of block.decisions) lines.push(`- ${d}`);
153
+ lines.push('');
154
+ }
155
+ if (block.filesTouched.length) {
156
+ lines.push('## Files touched');
157
+ for (const f of block.filesTouched) lines.push(`- ${f}`);
158
+ lines.push('');
159
+ }
160
+ if (block.verificationCommands.length) {
161
+ lines.push('## Verification commands');
162
+ for (const v of block.verificationCommands) lines.push(`- \`${v}\``);
163
+ lines.push('');
164
+ }
165
+ if (block.openQuestions.length) {
166
+ lines.push('## Open questions');
167
+ for (const q of block.openQuestions) lines.push(`- ${q}`);
168
+ lines.push('');
169
+ }
170
+ lines.push(SENTINELS.end);
171
+ return lines.join('\n') + '\n';
172
+ }
173
+
174
+ /**
175
+ * Parse an inbox file back into a HandoffBlock. Used by continue.js
176
+ * + the SessionStart hook.
177
+ */
178
+ function parseInbox(inboxPath) {
179
+ if (!fs.existsSync(inboxPath)) {
180
+ throw new Error(`orchestration/handoff: inbox file not found: ${inboxPath}`);
181
+ }
182
+ const text = fs.readFileSync(inboxPath, 'utf8');
183
+ const beginIdx = text.indexOf(SENTINELS.begin);
184
+ const endIdx = text.indexOf(SENTINELS.end);
185
+ if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
186
+ throw new Error(`orchestration/handoff: malformed inbox file: ${inboxPath}`);
187
+ }
188
+ // Keep the END sentinel in `inner` so the section regex can anchor
189
+ // its non-greedy capture against it for the LAST section.
190
+ const inner = text.slice(beginIdx + SENTINELS.begin.length, endIdx + SENTINELS.end.length);
191
+ const block = {
192
+ fromRepo: extract(inner, 'fromRepo'),
193
+ toRepo: extract(inner, 'toRepo'),
194
+ handoffId: extract(inner, 'handoffId'),
195
+ createdAt: extract(inner, 'createdAt'),
196
+ originatingPhase: extract(inner, 'originatingPhase'),
197
+ originatingHistoryRef: extract(inner, 'originatingHistoryRef'),
198
+ summary: extractSection(inner, 'Summary') || '',
199
+ decisions: extractBullets(inner, 'Decisions'),
200
+ filesTouched: extractBullets(inner, 'Files touched'),
201
+ verificationCommands: extractBullets(inner, 'Verification commands').map(stripBackticks),
202
+ openQuestions: extractBullets(inner, 'Open questions'),
203
+ inboxPath,
204
+ };
205
+ return types.validateHandoffBlock(block);
206
+ }
207
+
208
+ function extract(text, key) {
209
+ // `\\s*` would consume the trailing newline and pull content from
210
+ // the next line when the value is empty. Use a horizontal-whitespace
211
+ // class (` *|\\t*`) so we stop at end of the same line.
212
+ const re = new RegExp(`^\\*\\*${key}:\\*\\*[ \\t]*(.*)$`, 'm');
213
+ const m = text.match(re);
214
+ return m ? m[1].trim() : '';
215
+ }
216
+
217
+ function extractSection(text, heading) {
218
+ // Capture from "## <heading>\n" up to the NEXT "## " heading or the
219
+ // closing sentinel. Multiline flag for ^ anchors; no `$` alternative
220
+ // (a line-end inside a section would cut off bullets prematurely).
221
+ const re = new RegExp(
222
+ `^## ${heading}\\s*\\n([\\s\\S]*?)(?=^## |^${SENTINELS.end.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')})`,
223
+ 'm',
224
+ );
225
+ const m = text.match(re);
226
+ return m ? m[1].trim() : '';
227
+ }
228
+
229
+ function extractBullets(text, heading) {
230
+ const section = extractSection(text, heading);
231
+ if (!section) return [];
232
+ return section
233
+ .split('\n')
234
+ .map((l) => l.trim())
235
+ .filter((l) => l.startsWith('- '))
236
+ .map((l) => l.slice(2).trim());
237
+ }
238
+
239
+ function stripBackticks(s) {
240
+ return s.replace(/^`(.*)`$/, '$1');
241
+ }
242
+
243
+ function arrayify(v) {
244
+ if (v == null || v === false) return [];
245
+ if (Array.isArray(v)) return v;
246
+ return [String(v)];
247
+ }
248
+
249
+ // ── inbox utilities ────────────────────────────────────────────────────────
250
+
251
+ function listPending(repo) {
252
+ const dir = path.join(repo, INBOX_DIRNAME);
253
+ if (!fs.existsSync(dir)) return [];
254
+ return fs
255
+ .readdirSync(dir, { withFileTypes: true })
256
+ .filter((e) => e.isFile() && /^handoff-\d{3}\.md$/.test(e.name))
257
+ .map((e) => path.join(dir, e.name))
258
+ .sort();
259
+ }
260
+
261
+ function markRead(inboxPath) {
262
+ const dir = path.dirname(inboxPath);
263
+ const readDir = path.join(dir, READ_DIRNAME);
264
+ fs.mkdirSync(readDir, { recursive: true });
265
+ const dest = path.join(readDir, path.basename(inboxPath));
266
+ fs.renameSync(inboxPath, dest);
267
+ return dest;
268
+ }
269
+
270
+ // ── originating-repo decision write ────────────────────────────────────────
271
+
272
+ function detectActivePhase(repo) {
273
+ const phasesDir = path.join(repo, 'specs', 'phases');
274
+ if (!fs.existsSync(phasesDir)) return null;
275
+ // Try the most recently modified phase- directory.
276
+ const entries = fs
277
+ .readdirSync(phasesDir, { withFileTypes: true })
278
+ .filter((e) => e.isDirectory() && /^phase-/.test(e.name))
279
+ .map((e) => {
280
+ const p = path.join(phasesDir, e.name);
281
+ const stat = fs.statSync(p);
282
+ return { name: e.name, mtime: stat.mtimeMs };
283
+ })
284
+ .sort((a, b) => b.mtime - a.mtime);
285
+ return entries.length ? entries[0].name : null;
286
+ }
287
+
288
+ function appendOriginatingDecision({ fromRepo, originatingPhase, handoffId, toRepo, summary, createdAt }) {
289
+ const historyPath = path.join(fromRepo, 'specs', 'phases', originatingPhase, 'history.md');
290
+ if (!fs.existsSync(historyPath)) return null;
291
+ const date = createdAt.slice(0, 10);
292
+ const heading = `[DECISION] ${date} — Handoff #${handoffId} → ${path.basename(toRepo)}`;
293
+ const entry = [
294
+ '',
295
+ `### ${heading}`,
296
+ `Topics: orchestration, handoff, handoff-${handoffId}`,
297
+ `Affects-phases: ${originatingPhase} (or "none")`,
298
+ `Affects-specs: ${path.relative(fromRepo, path.join(toRepo, INBOX_DIRNAME, `handoff-${handoffId}.md`))}`,
299
+ `Detail: Handoff #${handoffId} written to ${path.basename(toRepo)}/.momentum/inbox/. Summary: ${summary}`,
300
+ '',
301
+ '---',
302
+ '',
303
+ ].join('\n');
304
+ fs.appendFileSync(historyPath, entry);
305
+ return heading;
306
+ }
307
+
308
+ module.exports = {
309
+ handoff,
310
+ renderInbox,
311
+ parseInbox,
312
+ listPending,
313
+ markRead,
314
+ detectActivePhase,
315
+ SENTINELS,
316
+ INBOX_DIRNAME,
317
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // Orchestration library entry point.
4
+ //
5
+ // One shared backend that the slash commands (Claude Code, Codex),
6
+ // natural-language inference, and CLI all dispatch to. Three primitives
7
+ // — scout, dispatch, handoff — composed by the main agent per task.
8
+ //
9
+ // Hard invariants enforced by this library:
10
+ // 1. Every primitive emits typed events via core/orchestration/events.
11
+ // Two default subscribers — a renderer (prints ▸ lines) and a
12
+ // persister (appends to ecosystem session log).
13
+ // 2. The "cheap layer" (session log + run artifact + handoff inbox)
14
+ // is always auto-written. The "curated layer" (history.md /
15
+ // backlog.md) is only written when the agent judges a finding
16
+ // meaningful, via core/orchestration/tracking.
17
+ // 3. Output shape is identical across slash / NL / CLI doors.
18
+
19
+ const events = require('./events');
20
+ const types = require('./types');
21
+ const capabilityRouting = require('./capability-routing');
22
+ const sessionLog = require('./session-log');
23
+ const runArtifact = require('./run-artifact');
24
+
25
+ module.exports = {
26
+ // Re-exports for downstream consumers.
27
+ events,
28
+ types,
29
+ capabilityRouting,
30
+ sessionLog,
31
+ runArtifact,
32
+
33
+ // Primitive entry points are added by G1/G2/G3 (scout/dispatch/handoff).
34
+ // Re-exported lazily so partial library shapes don't fail to load.
35
+ get scout() {
36
+ return require('./scout');
37
+ },
38
+ get dispatch() {
39
+ return require('./dispatch');
40
+ },
41
+ get handoff() {
42
+ return require('./handoff');
43
+ },
44
+ get continueHandoff() {
45
+ return require('./continue');
46
+ },
47
+ get tracking() {
48
+ return require('./tracking');
49
+ },
50
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ // Run artifact writer for orchestration primitives.
4
+ //
5
+ // Writes a structured Markdown artifact to <repo>/.momentum/runs/
6
+ // <primitive>-NNN.md. NNN is a monotonic counter scoped per primitive
7
+ // per repo — `scout-001`, `dispatch-001`, `handoff-001` count
8
+ // independently. Counter persisted via lightweight state file
9
+ // `.momentum/runs/.counters.json`.
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+
14
+ const PRIMITIVES = Object.freeze(['scout', 'dispatch', 'handoff']);
15
+
16
+ /**
17
+ * Allocate the next run id for the given primitive in the given repo.
18
+ *
19
+ * @param {object} opts
20
+ * @param {string} opts.repo absolute path to originating repo
21
+ * @param {string} opts.primitive one of: 'scout' | 'dispatch' | 'handoff'
22
+ * @returns {string} zero-padded id like '042'
23
+ */
24
+ function allocateRunId(opts) {
25
+ const { repo, primitive } = opts;
26
+ if (!PRIMITIVES.includes(primitive)) {
27
+ throw new Error(`orchestration/run-artifact: unknown primitive "${primitive}"`);
28
+ }
29
+ const runsDir = path.join(repo, '.momentum', 'runs');
30
+ fs.mkdirSync(runsDir, { recursive: true });
31
+ const countersPath = path.join(runsDir, '.counters.json');
32
+ let counters = {};
33
+ if (fs.existsSync(countersPath)) {
34
+ try {
35
+ counters = JSON.parse(fs.readFileSync(countersPath, 'utf8'));
36
+ } catch {
37
+ counters = {};
38
+ }
39
+ }
40
+ const next = (counters[primitive] || 0) + 1;
41
+ counters[primitive] = next;
42
+ fs.writeFileSync(countersPath, JSON.stringify(counters, null, 2) + '\n');
43
+ return String(next).padStart(3, '0');
44
+ }
45
+
46
+ /**
47
+ * Write a primitive's run artifact.
48
+ *
49
+ * @param {object} opts
50
+ * @param {string} opts.repo absolute path to originating repo
51
+ * @param {string} opts.primitive one of: 'scout' | 'dispatch' | 'handoff'
52
+ * @param {string} opts.runId pre-allocated id (use allocateRunId)
53
+ * @param {string} opts.body full Markdown body
54
+ * @returns {string} absolute path to the artifact
55
+ */
56
+ function writeRunArtifact(opts) {
57
+ const { repo, primitive, runId, body } = opts;
58
+ if (!PRIMITIVES.includes(primitive)) {
59
+ throw new Error(`orchestration/run-artifact: unknown primitive "${primitive}"`);
60
+ }
61
+ const runsDir = path.join(repo, '.momentum', 'runs');
62
+ fs.mkdirSync(runsDir, { recursive: true });
63
+ const artifactPath = path.join(runsDir, `${primitive}-${runId}.md`);
64
+ fs.writeFileSync(artifactPath, body);
65
+ return artifactPath;
66
+ }
67
+
68
+ /**
69
+ * Convenience: allocate id + write artifact + return path.
70
+ *
71
+ * @param {object} opts
72
+ * @param {string} opts.repo
73
+ * @param {string} opts.primitive
74
+ * @param {(runId: string) => string} opts.bodyFor builds body once the id is known
75
+ * @returns {{ runId: string, artifactPath: string }}
76
+ */
77
+ function writeWithAllocatedId(opts) {
78
+ const runId = allocateRunId({ repo: opts.repo, primitive: opts.primitive });
79
+ const artifactPath = writeRunArtifact({
80
+ repo: opts.repo,
81
+ primitive: opts.primitive,
82
+ runId,
83
+ body: opts.bodyFor(runId),
84
+ });
85
+ return { runId, artifactPath };
86
+ }
87
+
88
+ module.exports = {
89
+ PRIMITIVES,
90
+ allocateRunId,
91
+ writeRunArtifact,
92
+ writeWithAllocatedId,
93
+ };