@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,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// continueHandoff — receiving-side counterpart to handoff.
|
|
4
|
+
//
|
|
5
|
+
// Reads a pending inbox file, parses the HandoffBlock, marks the file
|
|
6
|
+
// as read (move to .momentum/inbox/read/), and emits a [NOTE] in the
|
|
7
|
+
// receiving repo's active phase history. Returns the HandoffBlock so
|
|
8
|
+
// the agent or CLI can act on it.
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
|
|
13
|
+
const eventsLib = require('./events');
|
|
14
|
+
const handoffLib = require('./handoff');
|
|
15
|
+
const sessionLog = require('./session-log');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pick up a pending handoff in the receiving repo.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {string} opts.repo absolute path to receiving repo
|
|
22
|
+
* @param {string} [opts.handoffId] pick a specific id; default: oldest pending
|
|
23
|
+
* @param {object} [opts.ecosystem]
|
|
24
|
+
* @param {object} [opts.emitter]
|
|
25
|
+
* @param {boolean} [opts.silent]
|
|
26
|
+
* @returns {Promise<HandoffBlock|null>} null when no pending handoff
|
|
27
|
+
*/
|
|
28
|
+
async function continueHandoff(opts) {
|
|
29
|
+
const { repo, ecosystem } = opts;
|
|
30
|
+
if (!repo) throw new Error('orchestration/continue: opts.repo required');
|
|
31
|
+
|
|
32
|
+
const pending = handoffLib.listPending(repo);
|
|
33
|
+
if (pending.length === 0) return null;
|
|
34
|
+
|
|
35
|
+
let chosen;
|
|
36
|
+
if (opts.handoffId) {
|
|
37
|
+
const id = String(opts.handoffId).padStart(3, '0');
|
|
38
|
+
chosen = pending.find((p) => path.basename(p) === `handoff-${id}.md`);
|
|
39
|
+
if (!chosen) {
|
|
40
|
+
throw new Error(`orchestration/continue: handoff-${id}.md not found in ${repo}`);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
chosen = pending[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const emitter = opts.emitter || eventsLib.createEmitter({ primitive: 'continue', repo });
|
|
47
|
+
if (!opts.emitter && !opts.silent) {
|
|
48
|
+
emitter.on(eventsLib.createRenderer({ stream: process.stdout }));
|
|
49
|
+
}
|
|
50
|
+
if (ecosystem) {
|
|
51
|
+
emitter.on(eventsLib.createPersister({
|
|
52
|
+
ecosystemRoot: ecosystem.rootPath,
|
|
53
|
+
memberId: ecosystem.memberId,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const block = handoffLib.parseInbox(chosen);
|
|
58
|
+
emitter.emit('started', { message: `picking up ${path.basename(chosen)} from ${path.basename(block.fromRepo)}` });
|
|
59
|
+
const readPath = handoffLib.markRead(chosen);
|
|
60
|
+
emitter.emit('step', { message: `marked ${path.basename(readPath)} read` });
|
|
61
|
+
|
|
62
|
+
// Append a [NOTE] entry in the receiving repo's active phase history.
|
|
63
|
+
const activePhase = handoffLib.detectActivePhase(repo);
|
|
64
|
+
if (activePhase) {
|
|
65
|
+
appendReceivingNote({ repo, activePhase, block });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
emitter.emit('finished', { runArtifactPath: readPath });
|
|
69
|
+
return block;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* List pending handoffs (paths) in the given repo. Convenience wrapper
|
|
74
|
+
* over handoffLib.listPending so callers don't have to know about the
|
|
75
|
+
* inbox layout.
|
|
76
|
+
*/
|
|
77
|
+
function listPending(repo) {
|
|
78
|
+
return handoffLib.listPending(repo);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function appendReceivingNote({ repo, activePhase, block }) {
|
|
82
|
+
const historyPath = path.join(repo, 'specs', 'phases', activePhase, 'history.md');
|
|
83
|
+
if (!fs.existsSync(historyPath)) return;
|
|
84
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
85
|
+
const entry = [
|
|
86
|
+
'',
|
|
87
|
+
`### [NOTE] ${date} — Picked up handoff-${block.handoffId} from ${path.basename(block.fromRepo)}`,
|
|
88
|
+
`Topics: orchestration, handoff-pickup, handoff-${block.handoffId}`,
|
|
89
|
+
`Affects-phases: ${activePhase} (or "none")`,
|
|
90
|
+
`Affects-specs: ${path.relative(repo, path.join(repo, handoffLib.INBOX_DIRNAME, 'read', `handoff-${block.handoffId}.md`))}`,
|
|
91
|
+
`Detail: Received handoff from ${path.basename(block.fromRepo)}. Summary: ${block.summary}`,
|
|
92
|
+
'',
|
|
93
|
+
'---',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n');
|
|
96
|
+
fs.appendFileSync(historyPath, entry);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
continueHandoff,
|
|
101
|
+
listPending,
|
|
102
|
+
};
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// dispatch — parallel multi-repo fan-out + synthesis.
|
|
4
|
+
//
|
|
5
|
+
// In-process mode (CLI floor): runs scout against each repo. On
|
|
6
|
+
// `mode='parallel'` they run concurrently via Promise.all; on
|
|
7
|
+
// `mode='sequential'` they run one at a time and the user is told
|
|
8
|
+
// up front via a `started` event note. Either way, sub-agent failures
|
|
9
|
+
// are captured in `failures[]` — the call never throws.
|
|
10
|
+
//
|
|
11
|
+
// In-agent mode (slash command on Claude Code / Codex / Antigravity):
|
|
12
|
+
// the agent does the per-repo fan-out using its own sub-agent surface
|
|
13
|
+
// (Task tool on Claude Code, etc.), then calls record() with the
|
|
14
|
+
// structured results so the artifact + log line + tracking-contract
|
|
15
|
+
// hand-off all happen via the same code path as the CLI.
|
|
16
|
+
//
|
|
17
|
+
// Synthesis: in-process, the synthesizer concatenates per-repo
|
|
18
|
+
// summaries with a header — no LLM available from raw Node. Agent-
|
|
19
|
+
// driven dispatch produces a true synthesis in the originating agent's
|
|
20
|
+
// context, then calls record() with `synthesis` text included.
|
|
21
|
+
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
const eventsLib = require('./events');
|
|
25
|
+
const runArtifact = require('./run-artifact');
|
|
26
|
+
const sessionLog = require('./session-log');
|
|
27
|
+
const types = require('./types');
|
|
28
|
+
const routing = require('./capability-routing');
|
|
29
|
+
const scoutLib = require('./scout');
|
|
30
|
+
|
|
31
|
+
// ENH-034 — CLI / in-process dispatch degraded-mode notice. Emitted as
|
|
32
|
+
// the FIRST event before `started`, and rendered as a `> [!NOTE]`
|
|
33
|
+
// admonition inside the artifact + synthesis body. Three surfaces, one
|
|
34
|
+
// string so the message stays consistent everywhere agents/users see it.
|
|
35
|
+
const CLI_MODE_NOTICE =
|
|
36
|
+
'MODE NOTICE: CLI mode produces keyword summaries only (no LLM synthesis). ' +
|
|
37
|
+
'For full synthesis, invoke /dispatch as a slash command from an agent surface ' +
|
|
38
|
+
'(Claude Code Task tool, Codex sub-agent, etc.).';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a dispatch in-process across the given repos.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} opts
|
|
44
|
+
* @param {string[]} opts.repos absolute paths to target repos
|
|
45
|
+
* @param {string} opts.userIntent high-level prompt
|
|
46
|
+
* @param {string} [opts.originatingRepo] defaults to first repo
|
|
47
|
+
* @param {object} [opts.adapter] adapter module for capability routing
|
|
48
|
+
* @param {object} [opts.ecosystem] { rootPath, memberId }
|
|
49
|
+
* @param {boolean} [opts.forceSequential] force sequential mode (testing aid)
|
|
50
|
+
* @param {boolean} [opts.silent]
|
|
51
|
+
* @returns {Promise<DispatchResult>}
|
|
52
|
+
*/
|
|
53
|
+
async function dispatch(opts) {
|
|
54
|
+
const { repos, userIntent } = opts;
|
|
55
|
+
if (!Array.isArray(repos) || repos.length === 0) {
|
|
56
|
+
throw new Error('orchestration/dispatch: opts.repos must be a non-empty array');
|
|
57
|
+
}
|
|
58
|
+
if (!userIntent) {
|
|
59
|
+
throw new Error('orchestration/dispatch: opts.userIntent is required');
|
|
60
|
+
}
|
|
61
|
+
const originatingRepo = opts.originatingRepo || repos[0];
|
|
62
|
+
|
|
63
|
+
// Decide mode: explicit override > adapter capability > parallel default.
|
|
64
|
+
let mode = 'parallel';
|
|
65
|
+
let modeNotes = [];
|
|
66
|
+
if (opts.forceSequential) {
|
|
67
|
+
mode = 'sequential';
|
|
68
|
+
modeNotes.push('sequential mode forced via --sequential');
|
|
69
|
+
} else if (opts.adapter) {
|
|
70
|
+
const decision = routing.chooseMode(opts.adapter, 'dispatch');
|
|
71
|
+
mode = decision.mode;
|
|
72
|
+
modeNotes = decision.notes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const start = Date.now();
|
|
76
|
+
const emitter = opts.emitter || eventsLib.createEmitter({ primitive: 'dispatch', repo: originatingRepo });
|
|
77
|
+
if (!opts.emitter && !opts.silent) {
|
|
78
|
+
emitter.on(eventsLib.createRenderer({ stream: process.stdout }));
|
|
79
|
+
}
|
|
80
|
+
if (opts.ecosystem) {
|
|
81
|
+
emitter.on(eventsLib.createPersister({
|
|
82
|
+
ecosystemRoot: opts.ecosystem.rootPath,
|
|
83
|
+
memberId: opts.ecosystem.memberId,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ENH-034 — surface the in-process degraded-mode notice UPFRONT
|
|
88
|
+
// (before the `started` event) so it is the literal first user-visible
|
|
89
|
+
// output line. Without this, agents and users were silently getting
|
|
90
|
+
// keyword-grep summaries and concluding "this is the answer."
|
|
91
|
+
//
|
|
92
|
+
// The `record()` path (agent-driven dispatch from a slash command)
|
|
93
|
+
// never reaches dispatch(), so this notice only fires for CLI /
|
|
94
|
+
// in-process callers — exactly where it's needed.
|
|
95
|
+
emitter.emit('note', { message: CLI_MODE_NOTICE });
|
|
96
|
+
|
|
97
|
+
emitter.emit('started', {
|
|
98
|
+
message: `${repos.length} repo(s): ${userIntent}`,
|
|
99
|
+
});
|
|
100
|
+
for (const note of modeNotes) {
|
|
101
|
+
emitter.emit('note', { message: note });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const perRepoResults = [];
|
|
105
|
+
const failures = [];
|
|
106
|
+
|
|
107
|
+
async function runOne(repo) {
|
|
108
|
+
emitter.emit('subagent-started', { repo });
|
|
109
|
+
const perRepoPrompt = tailorPrompt(userIntent, repo);
|
|
110
|
+
const t0 = Date.now();
|
|
111
|
+
try {
|
|
112
|
+
const scoutResult = await scoutLib.scout({
|
|
113
|
+
repo,
|
|
114
|
+
prompt: perRepoPrompt,
|
|
115
|
+
originatingRepo,
|
|
116
|
+
// Skip session log on sub-scouts so the ecosystem log isn't
|
|
117
|
+
// peppered with per-repo lines; the dispatch parent emits one
|
|
118
|
+
// line summarising the whole run.
|
|
119
|
+
ecosystem: null,
|
|
120
|
+
silent: true,
|
|
121
|
+
});
|
|
122
|
+
const entry = {
|
|
123
|
+
repo,
|
|
124
|
+
prompt: perRepoPrompt,
|
|
125
|
+
summary: scoutResult.summary,
|
|
126
|
+
findings: scoutResult.findings,
|
|
127
|
+
filesRead: scoutResult.filesRead,
|
|
128
|
+
duration: Date.now() - t0,
|
|
129
|
+
};
|
|
130
|
+
perRepoResults.push(entry);
|
|
131
|
+
emitter.emit('subagent-finished', { repo, duration: entry.duration });
|
|
132
|
+
return entry;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const entry = {
|
|
135
|
+
repo,
|
|
136
|
+
prompt: perRepoPrompt,
|
|
137
|
+
error: { message: err.message },
|
|
138
|
+
duration: Date.now() - t0,
|
|
139
|
+
};
|
|
140
|
+
perRepoResults.push(entry);
|
|
141
|
+
failures.push(entry);
|
|
142
|
+
emitter.emit('subagent-failed', { repo, error: err.message });
|
|
143
|
+
return entry;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (mode === 'parallel') {
|
|
148
|
+
await Promise.all(repos.map(runOne));
|
|
149
|
+
} else {
|
|
150
|
+
for (const repo of repos) {
|
|
151
|
+
await runOne(repo);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const synthesis = synthesizeInProcess({ userIntent, perRepoResults, failures });
|
|
156
|
+
emitter.emit('synthesized', { summary: oneLineSynthesis(perRepoResults, failures) });
|
|
157
|
+
|
|
158
|
+
const duration = Date.now() - start;
|
|
159
|
+
const { runId, artifactPath } = runArtifact.writeWithAllocatedId({
|
|
160
|
+
repo: originatingRepo,
|
|
161
|
+
primitive: 'dispatch',
|
|
162
|
+
bodyFor: (id) => renderArtifact({ runId: id, repos, userIntent, mode, modeNotes, perRepoResults, failures, synthesis, duration }),
|
|
163
|
+
});
|
|
164
|
+
emitter.context.runId = runId;
|
|
165
|
+
emitter.emit('finished', { duration, runArtifactPath: artifactPath });
|
|
166
|
+
|
|
167
|
+
const result = {
|
|
168
|
+
repos,
|
|
169
|
+
userIntent,
|
|
170
|
+
mode,
|
|
171
|
+
modeNotes,
|
|
172
|
+
perRepoResults,
|
|
173
|
+
failures,
|
|
174
|
+
synthesis,
|
|
175
|
+
duration,
|
|
176
|
+
runArtifactPath: artifactPath,
|
|
177
|
+
runId,
|
|
178
|
+
};
|
|
179
|
+
return types.validateDispatchResult(result);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Record a dispatch performed by an external agent.
|
|
184
|
+
*
|
|
185
|
+
* The agent does its own sub-agent fan-out (e.g., Claude Code Task
|
|
186
|
+
* tool) and synthesis, then calls record() with the structured shape
|
|
187
|
+
* so the artifact + session log line + tracking-contract hand-off all
|
|
188
|
+
* happen via the same code path the CLI uses.
|
|
189
|
+
*/
|
|
190
|
+
function record(opts) {
|
|
191
|
+
const {
|
|
192
|
+
repos,
|
|
193
|
+
userIntent,
|
|
194
|
+
mode = 'parallel',
|
|
195
|
+
modeNotes = [],
|
|
196
|
+
perRepoResults = [],
|
|
197
|
+
failures = [],
|
|
198
|
+
synthesis,
|
|
199
|
+
originatingRepo,
|
|
200
|
+
ecosystem,
|
|
201
|
+
duration = 0,
|
|
202
|
+
} = opts;
|
|
203
|
+
if (!originatingRepo) {
|
|
204
|
+
throw new Error('orchestration/dispatch.record: originatingRepo required');
|
|
205
|
+
}
|
|
206
|
+
const { runId, artifactPath } = runArtifact.writeWithAllocatedId({
|
|
207
|
+
repo: originatingRepo,
|
|
208
|
+
primitive: 'dispatch',
|
|
209
|
+
bodyFor: (id) => renderArtifact({ runId: id, repos, userIntent, mode, modeNotes, perRepoResults, failures, synthesis, duration, isAgentDriven: true }),
|
|
210
|
+
});
|
|
211
|
+
if (ecosystem && ecosystem.rootPath && ecosystem.memberId) {
|
|
212
|
+
sessionLog.appendLine({
|
|
213
|
+
ecosystemRoot: ecosystem.rootPath,
|
|
214
|
+
memberId: ecosystem.memberId,
|
|
215
|
+
kind: 'dispatch',
|
|
216
|
+
summary: `${repos.length} repos: ${userIntent}`,
|
|
217
|
+
context: `run-${runId} → ${path.relative(originatingRepo, artifactPath)}`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// Tracking contract: each per-repo finding proposed as [DISCOVERY]
|
|
221
|
+
// in THAT repo's active phase history (only when meaningful per
|
|
222
|
+
// Rule 3). The synthesis is proposed as a [NOTE] in the
|
|
223
|
+
// ORIGINATING repo's active phase history when present.
|
|
224
|
+
const tracking = require('./tracking');
|
|
225
|
+
for (const perRepo of perRepoResults) {
|
|
226
|
+
if (!perRepo.findings) continue;
|
|
227
|
+
for (const finding of perRepo.findings) {
|
|
228
|
+
tracking.proposeDiscovery({ primitive: 'dispatch', finding, targetRepo: perRepo.repo });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (synthesis) {
|
|
232
|
+
tracking.proposeHistoryNote({
|
|
233
|
+
primitive: 'dispatch',
|
|
234
|
+
originatingRepo,
|
|
235
|
+
message: `dispatch across ${repos.length} repo(s) for: ${userIntent}`,
|
|
236
|
+
runArtifactRef: path.relative(originatingRepo, artifactPath),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const result = {
|
|
240
|
+
repos, userIntent, mode, modeNotes,
|
|
241
|
+
perRepoResults, failures, synthesis,
|
|
242
|
+
duration, runArtifactPath: artifactPath, runId,
|
|
243
|
+
};
|
|
244
|
+
return types.validateDispatchResult(result);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
function tailorPrompt(userIntent, repo) {
|
|
250
|
+
// In-process mode reuses the same prompt verbatim — the scout
|
|
251
|
+
// tokenizer is the limiting factor anyway. Agent-driven dispatch
|
|
252
|
+
// can produce repo-specific phrasing in the originating agent's
|
|
253
|
+
// context before calling record().
|
|
254
|
+
const repoLabel = path.basename(repo);
|
|
255
|
+
return `In repo ${repoLabel}: ${userIntent}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function synthesizeInProcess({ userIntent, perRepoResults, failures }) {
|
|
259
|
+
const successes = perRepoResults.filter((r) => !r.error);
|
|
260
|
+
// ENH-034 — admonition at the TOP of the synthesis block so anyone
|
|
261
|
+
// reading the in-process synthesis sees the mode caveat BEFORE the
|
|
262
|
+
// per-repo summaries (previously buried at the bottom as a paren).
|
|
263
|
+
const lines = [
|
|
264
|
+
'> [!NOTE]',
|
|
265
|
+
'> **MODE: CLI / in-process — keyword summaries only.**',
|
|
266
|
+
'> For LLM synthesis, invoke /dispatch via an agent slash command.',
|
|
267
|
+
'',
|
|
268
|
+
`Dispatch across ${perRepoResults.length} repo(s) for: ${userIntent}`,
|
|
269
|
+
'',
|
|
270
|
+
`- ${successes.length} succeeded, ${failures.length} failed.`,
|
|
271
|
+
];
|
|
272
|
+
if (successes.length) {
|
|
273
|
+
lines.push('', '## Per-repo summaries');
|
|
274
|
+
for (const r of successes) {
|
|
275
|
+
const repoLabel = path.basename(r.repo);
|
|
276
|
+
lines.push(`- **${repoLabel}** (${r.duration}ms): ${truncateForSynth(r.summary)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (failures.length) {
|
|
280
|
+
lines.push('', '## Failures');
|
|
281
|
+
for (const f of failures) {
|
|
282
|
+
const repoLabel = path.basename(f.repo);
|
|
283
|
+
lines.push(`- **${repoLabel}**: ${f.error && f.error.message ? f.error.message : 'unknown error'}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return lines.join('\n');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function oneLineSynthesis(perRepoResults, failures) {
|
|
290
|
+
const okCount = perRepoResults.length - failures.length;
|
|
291
|
+
return `${okCount}/${perRepoResults.length} succeeded${failures.length ? `, ${failures.length} failed` : ''}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function truncateForSynth(s) {
|
|
295
|
+
if (!s) return '(empty)';
|
|
296
|
+
if (s.length <= 200) return s.replace(/\n+/g, ' ');
|
|
297
|
+
return s.slice(0, 200).replace(/\n+/g, ' ') + '…';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderArtifact({ runId, repos, userIntent, mode, modeNotes, perRepoResults, failures, synthesis, duration, isAgentDriven = false }) {
|
|
301
|
+
// ENH-034 — admonition immediately under the Mode header so anyone
|
|
302
|
+
// opening the artifact sees the CLI-degradation caveat before reading
|
|
303
|
+
// the synthesis. Skip on agent-driven `record()` runs — those carry
|
|
304
|
+
// a real LLM synthesis so the caveat would mislead.
|
|
305
|
+
const modeAdmonition = isAgentDriven
|
|
306
|
+
? []
|
|
307
|
+
: [
|
|
308
|
+
'> [!NOTE]',
|
|
309
|
+
'> **CLI / in-process mode — keyword summaries only.**',
|
|
310
|
+
'> Invoke /dispatch via an agent slash command for true LLM synthesis.',
|
|
311
|
+
'',
|
|
312
|
+
];
|
|
313
|
+
const lines = [
|
|
314
|
+
`# dispatch-${runId}`,
|
|
315
|
+
'',
|
|
316
|
+
`**User intent:** ${userIntent}`,
|
|
317
|
+
`**Mode:** ${mode}${modeNotes.length ? ` (${modeNotes.join('; ')})` : ''}`,
|
|
318
|
+
`**Repos (${repos.length}):**`,
|
|
319
|
+
...repos.map((r) => ` - ${r}`),
|
|
320
|
+
`**Duration:** ${duration}ms`,
|
|
321
|
+
'',
|
|
322
|
+
...modeAdmonition,
|
|
323
|
+
'## Synthesis',
|
|
324
|
+
'',
|
|
325
|
+
synthesis || '(no synthesis recorded)',
|
|
326
|
+
'',
|
|
327
|
+
'## Per-repo results',
|
|
328
|
+
'',
|
|
329
|
+
];
|
|
330
|
+
for (const r of perRepoResults) {
|
|
331
|
+
const repoLabel = path.basename(r.repo);
|
|
332
|
+
lines.push(`### ${repoLabel}`, '');
|
|
333
|
+
lines.push(`- **Prompt:** ${r.prompt || '(n/a)'}`);
|
|
334
|
+
lines.push(`- **Duration:** ${r.duration != null ? r.duration : 'n/a'}ms`);
|
|
335
|
+
if (r.error) {
|
|
336
|
+
lines.push(`- **Error:** ${r.error.message}`);
|
|
337
|
+
} else {
|
|
338
|
+
lines.push(`- **Files read:** ${(r.filesRead || []).length}`);
|
|
339
|
+
lines.push('', '```', r.summary || '(empty)', '```');
|
|
340
|
+
}
|
|
341
|
+
if (r.findings && r.findings.length) {
|
|
342
|
+
lines.push('', 'Findings:');
|
|
343
|
+
for (const f of r.findings) {
|
|
344
|
+
lines.push(` - **${f.kind || 'note'}**: ${f.title}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
lines.push('');
|
|
348
|
+
}
|
|
349
|
+
if (failures.length) {
|
|
350
|
+
lines.push('## Failure manifest', '');
|
|
351
|
+
for (const f of failures) {
|
|
352
|
+
lines.push(`- ${path.basename(f.repo)}: ${f.error && f.error.message ? f.error.message : 'unknown'}`);
|
|
353
|
+
}
|
|
354
|
+
lines.push('');
|
|
355
|
+
}
|
|
356
|
+
return lines.join('\n');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = {
|
|
360
|
+
dispatch,
|
|
361
|
+
record,
|
|
362
|
+
CLI_MODE_NOTICE,
|
|
363
|
+
// exported for tests:
|
|
364
|
+
tailorPrompt,
|
|
365
|
+
synthesizeInProcess,
|
|
366
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Typed event emitter for orchestration primitives.
|
|
4
|
+
//
|
|
5
|
+
// Every primitive (scout, dispatch, handoff) emits structured events as
|
|
6
|
+
// it runs. Two default subscribers:
|
|
7
|
+
// - renderer: prints `▸ <message>` lines to stdout (chat surface).
|
|
8
|
+
// - persister: appends the same events to the ecosystem session log
|
|
9
|
+
// (durable audit surface).
|
|
10
|
+
//
|
|
11
|
+
// One source, two surfaces — keeps live narration and the persistent
|
|
12
|
+
// log in lockstep. New event types are listed in EVENT_KINDS; adding
|
|
13
|
+
// one requires updating that list AND any rendering / persistence code
|
|
14
|
+
// that depends on the kind.
|
|
15
|
+
|
|
16
|
+
const EVENT_KINDS = Object.freeze([
|
|
17
|
+
// Primitive lifecycle
|
|
18
|
+
'started',
|
|
19
|
+
'finished',
|
|
20
|
+
'failed',
|
|
21
|
+
|
|
22
|
+
// Step-level progress (file reads, sub-agent spawns, etc.)
|
|
23
|
+
'step',
|
|
24
|
+
'note', // free-form info that should land in chat + log
|
|
25
|
+
|
|
26
|
+
// Sub-agent lifecycle (used by dispatch)
|
|
27
|
+
'subagent-started',
|
|
28
|
+
'subagent-finished',
|
|
29
|
+
'subagent-failed',
|
|
30
|
+
|
|
31
|
+
// Result-level events
|
|
32
|
+
'finding', // emitted by scout/dispatch when a notable finding is identified
|
|
33
|
+
'synthesized', // emitted by dispatch after synthesis completes
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function isValidKind(kind) {
|
|
37
|
+
return EVENT_KINDS.includes(kind);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new event emitter for a single primitive invocation.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} opts
|
|
44
|
+
* @param {string} opts.primitive one of: 'scout' | 'dispatch' | 'handoff' | 'continue'
|
|
45
|
+
* @param {string} [opts.runId] monotonic per-primitive run id (set later by run-artifact)
|
|
46
|
+
* @param {string} [opts.repo] originating repo path
|
|
47
|
+
* @returns {{ emit: Function, on: Function, kinds: string[], context: object }}
|
|
48
|
+
*/
|
|
49
|
+
function createEmitter(opts = {}) {
|
|
50
|
+
const subscribers = [];
|
|
51
|
+
const context = { ...opts };
|
|
52
|
+
|
|
53
|
+
function emit(kind, payload = {}) {
|
|
54
|
+
if (!isValidKind(kind)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`orchestration/events: unknown event kind "${kind}". ` +
|
|
57
|
+
`Valid kinds: ${EVENT_KINDS.join(', ')}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const event = {
|
|
61
|
+
kind,
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
primitive: context.primitive,
|
|
64
|
+
runId: context.runId,
|
|
65
|
+
repo: context.repo,
|
|
66
|
+
...payload,
|
|
67
|
+
};
|
|
68
|
+
for (const subscriber of subscribers) {
|
|
69
|
+
try {
|
|
70
|
+
subscriber(event);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Subscribers MUST NOT throw — but if one does, log and continue
|
|
73
|
+
// rather than letting it break the primitive.
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`orchestration/events: subscriber error: ${err.message}\n`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return event;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function on(handler) {
|
|
83
|
+
if (typeof handler !== 'function') {
|
|
84
|
+
throw new TypeError('orchestration/events: subscriber must be a function');
|
|
85
|
+
}
|
|
86
|
+
subscribers.push(handler);
|
|
87
|
+
return () => {
|
|
88
|
+
const idx = subscribers.indexOf(handler);
|
|
89
|
+
if (idx >= 0) subscribers.splice(idx, 1);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { emit, on, kinds: EVENT_KINDS, context };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Default renderer subscriber — prints `▸ <message>` lines to a stream
|
|
98
|
+
* (default: stdout). Mostly for the CLI doors; in-agent contexts
|
|
99
|
+
* synthesize their own rendering layer.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} [opts]
|
|
102
|
+
* @param {NodeJS.WritableStream} [opts.stream] default: process.stdout
|
|
103
|
+
* @param {boolean} [opts.silent] if true, no output (useful for tests)
|
|
104
|
+
* @returns {Function} subscriber
|
|
105
|
+
*/
|
|
106
|
+
function createRenderer(opts = {}) {
|
|
107
|
+
const stream = opts.stream || process.stdout;
|
|
108
|
+
return function rendererSubscriber(event) {
|
|
109
|
+
if (opts.silent) return;
|
|
110
|
+
const line = renderEvent(event);
|
|
111
|
+
if (line) stream.write(line + '\n');
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderEvent(event) {
|
|
116
|
+
switch (event.kind) {
|
|
117
|
+
case 'started':
|
|
118
|
+
return `▸ ${event.primitive}${event.repo ? ` ${event.repo}` : ''}${
|
|
119
|
+
event.message ? `: ${event.message}` : ''
|
|
120
|
+
}`;
|
|
121
|
+
case 'finished':
|
|
122
|
+
return `▸ ${event.primitive} done${event.duration != null ? ` (${event.duration}ms)` : ''}${
|
|
123
|
+
event.runArtifactPath ? ` → ${event.runArtifactPath}` : ''
|
|
124
|
+
}`;
|
|
125
|
+
case 'failed':
|
|
126
|
+
return `▸ ${event.primitive} FAILED${event.error ? `: ${event.error}` : ''}`;
|
|
127
|
+
case 'step':
|
|
128
|
+
return ` ▸ ${event.message || ''}`;
|
|
129
|
+
case 'note':
|
|
130
|
+
return ` ▸ ${event.message || ''}`;
|
|
131
|
+
case 'subagent-started':
|
|
132
|
+
return ` ▸ ${event.repo}…`;
|
|
133
|
+
case 'subagent-finished':
|
|
134
|
+
return ` ▸ ${event.repo}… done${event.duration != null ? ` (${event.duration}ms)` : ''}`;
|
|
135
|
+
case 'subagent-failed':
|
|
136
|
+
return ` ▸ ${event.repo}… FAILED${event.error ? `: ${event.error}` : ''}`;
|
|
137
|
+
case 'finding':
|
|
138
|
+
return ` ▸ found: ${event.summary || event.message || ''}`;
|
|
139
|
+
case 'synthesized':
|
|
140
|
+
return event.message ? `▸ Synthesis\n${event.message}` : '▸ Synthesis';
|
|
141
|
+
default:
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Default persister subscriber — appends events to the ecosystem
|
|
148
|
+
* session log. Uses the shared session-log writer (lock-aware) so we
|
|
149
|
+
* don't race with PostToolUse hook commits.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} opts
|
|
152
|
+
* @param {string} opts.ecosystemRoot ecosystem root path
|
|
153
|
+
* @param {string} opts.memberId member id of the originating repo
|
|
154
|
+
* @returns {Function} subscriber
|
|
155
|
+
*/
|
|
156
|
+
function createPersister(opts) {
|
|
157
|
+
const { appendLine } = require('./session-log');
|
|
158
|
+
return function persisterSubscriber(event) {
|
|
159
|
+
if (!opts || !opts.ecosystemRoot || !opts.memberId) return;
|
|
160
|
+
const line = persistEvent(event);
|
|
161
|
+
if (!line) return;
|
|
162
|
+
appendLine({
|
|
163
|
+
ecosystemRoot: opts.ecosystemRoot,
|
|
164
|
+
memberId: opts.memberId,
|
|
165
|
+
kind: event.primitive,
|
|
166
|
+
summary: line,
|
|
167
|
+
context: event.runId ? `run-${event.runId}` : '',
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function persistEvent(event) {
|
|
173
|
+
// Only persist primitive-level lifecycle events; step-level events
|
|
174
|
+
// would flood the session log.
|
|
175
|
+
switch (event.kind) {
|
|
176
|
+
case 'started':
|
|
177
|
+
return `${event.primitive} ${event.repo || ''}${event.message ? `: ${event.message}` : ''}`.trim();
|
|
178
|
+
case 'finished':
|
|
179
|
+
return `${event.primitive} done${event.runArtifactPath ? ` → ${event.runArtifactPath}` : ''}`;
|
|
180
|
+
case 'failed':
|
|
181
|
+
return `${event.primitive} FAILED${event.error ? `: ${event.error}` : ''}`;
|
|
182
|
+
case 'synthesized':
|
|
183
|
+
return event.summary ? `${event.primitive} synthesis: ${event.summary}` : null;
|
|
184
|
+
default:
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
EVENT_KINDS,
|
|
191
|
+
isValidKind,
|
|
192
|
+
createEmitter,
|
|
193
|
+
createRenderer,
|
|
194
|
+
createPersister,
|
|
195
|
+
renderEvent, // exported for tests
|
|
196
|
+
persistEvent, // exported for tests
|
|
197
|
+
};
|