@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,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
|
+
};
|