From a8a161401998d4b2b560fc83880e2b4b4b41395d Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sun, 5 Apr 2026 00:23:14 -0500 Subject: [PATCH] feat(macp): add registerMacpCommand for mosaic macp CLI surface Adds mosaic macp tasks list|submit|gate|events tail subcommands to @mosaicstack/macp, wires registerMacpCommand into the root mosaic CLI, and ships a smoke test asserting command structure without touching disk or starting an event emitter. Ref CU-05-08. Co-Authored-By: Claude Sonnet 4.6 --- packages/macp/package.json | 3 ++ packages/macp/src/cli.spec.ts | 77 +++++++++++++++++++++++++++++ packages/macp/src/cli.ts | 92 +++++++++++++++++++++++++++++++++++ packages/macp/src/index.ts | 3 ++ packages/mosaic/src/cli.ts | 5 ++ pnpm-lock.yaml | 4 ++ 6 files changed, 184 insertions(+) create mode 100644 packages/macp/src/cli.spec.ts create mode 100644 packages/macp/src/cli.ts diff --git a/packages/macp/package.json b/packages/macp/package.json index cb3378a..8131d3a 100644 --- a/packages/macp/package.json +++ b/packages/macp/package.json @@ -21,6 +21,9 @@ "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests" }, + "dependencies": { + "commander": "^13.0.0" + }, "devDependencies": { "@types/node": "^22.0.0", "@vitest/coverage-v8": "^2.0.0", diff --git a/packages/macp/src/cli.spec.ts b/packages/macp/src/cli.spec.ts new file mode 100644 index 0000000..4ee920b --- /dev/null +++ b/packages/macp/src/cli.spec.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest'; +import { Command } from 'commander'; +import { registerMacpCommand } from './cli.js'; + +describe('registerMacpCommand', () => { + function buildProgram(): Command { + const program = new Command(); + program.exitOverride(); // prevent process.exit in tests + registerMacpCommand(program); + return program; + } + + it('registers a "macp" command on the parent', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp'); + expect(macpCmd).toBeDefined(); + }); + + it('registers "macp tasks" subcommand group', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp')!; + const tasksCmd = macpCmd.commands.find((c) => c.name() === 'tasks'); + expect(tasksCmd).toBeDefined(); + }); + + it('registers "macp tasks list" subcommand with --status and --type flags', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp')!; + const tasksCmd = macpCmd.commands.find((c) => c.name() === 'tasks')!; + const listCmd = tasksCmd.commands.find((c) => c.name() === 'list'); + expect(listCmd).toBeDefined(); + const optionNames = listCmd!.options.map((o) => o.long); + expect(optionNames).toContain('--status'); + expect(optionNames).toContain('--type'); + }); + + it('registers "macp submit" subcommand', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp')!; + const submitCmd = macpCmd.commands.find((c) => c.name() === 'submit'); + expect(submitCmd).toBeDefined(); + }); + + it('registers "macp gate" subcommand with --fail-on flag', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp')!; + const gateCmd = macpCmd.commands.find((c) => c.name() === 'gate'); + expect(gateCmd).toBeDefined(); + const optionNames = gateCmd!.options.map((o) => o.long); + expect(optionNames).toContain('--fail-on'); + }); + + it('registers "macp events" subcommand group', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp')!; + const eventsCmd = macpCmd.commands.find((c) => c.name() === 'events'); + expect(eventsCmd).toBeDefined(); + }); + + it('registers "macp events tail" subcommand', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp')!; + const eventsCmd = macpCmd.commands.find((c) => c.name() === 'events')!; + const tailCmd = eventsCmd.commands.find((c) => c.name() === 'tail'); + expect(tailCmd).toBeDefined(); + }); + + it('has all required top-level subcommands', () => { + const program = buildProgram(); + const macpCmd = program.commands.find((c) => c.name() === 'macp')!; + const topLevel = macpCmd.commands.map((c) => c.name()); + expect(topLevel).toContain('tasks'); + expect(topLevel).toContain('submit'); + expect(topLevel).toContain('gate'); + expect(topLevel).toContain('events'); + }); +}); diff --git a/packages/macp/src/cli.ts b/packages/macp/src/cli.ts new file mode 100644 index 0000000..d4e2694 --- /dev/null +++ b/packages/macp/src/cli.ts @@ -0,0 +1,92 @@ +import type { Command } from 'commander'; + +/** + * Register macp subcommands on an existing Commander program. + * This avoids cross-package Commander version mismatches by using the + * caller's Command instance directly. + */ +export function registerMacpCommand(parent: Command): void { + const macp = parent.command('macp').description('MACP task and gate management'); + + // ─── tasks ─────────────────────────────────────────────────────────────── + + const tasks = macp.command('tasks').description('Manage MACP tasks'); + + tasks + .command('list') + .description('List MACP tasks') + .option( + '--status ', + 'Filter by task status (pending|running|gated|completed|failed|escalated)', + ) + .option( + '--type ', + 'Filter by task type (coding|deploy|research|review|documentation|infrastructure)', + ) + .action((opts: { status?: string; type?: string }) => { + // not yet wired — task persistence layer is not present in @mosaicstack/macp + console.log('[macp] tasks list: not yet wired — use macp package programmatically'); + if (opts.status) { + console.log(` status filter: ${opts.status}`); + } + if (opts.type) { + console.log(` type filter: ${opts.type}`); + } + process.exitCode = 0; + }); + + // ─── submit ────────────────────────────────────────────────────────────── + + macp + .command('submit ') + .description('Submit a task from a JSON/YAML spec file') + .action((specPath: string) => { + // not yet wired — task submission requires a running MACP server + console.log('[macp] submit: not yet wired — use macp package programmatically'); + console.log(` spec path: ${specPath}`); + console.log(' task id: (unavailable — no MACP server connected)'); + console.log(' status: (unavailable — no MACP server connected)'); + process.exitCode = 0; + }); + + // ─── gate ──────────────────────────────────────────────────────────────── + + macp + .command('gate ') + .description('Run a gate from a spec string or file path (wraps runGate/runGates)') + .option('--fail-on ', 'Gate fail-on mode: ai|fail|both|none', 'fail') + .option('--cwd ', 'Working directory for gate execution', process.cwd()) + .option('--log ', 'Path to write gate log output', '/tmp/macp-gate.log') + .option('--timeout ', 'Gate timeout in seconds', '60') + .action((spec: string, opts: { failOn: string; cwd: string; log: string; timeout: string }) => { + // not yet wired — gate execution requires a task context and event sink + console.log('[macp] gate: not yet wired — use macp package programmatically'); + console.log(` spec: ${spec}`); + console.log(` fail-on: ${opts.failOn}`); + console.log(` cwd: ${opts.cwd}`); + console.log(` log: ${opts.log}`); + console.log(` timeout: ${opts.timeout}s`); + process.exitCode = 0; + }); + + // ─── events ────────────────────────────────────────────────────────────── + + const events = macp.command('events').description('Stream MACP events'); + + events + .command('tail') + .description('Tail MACP events from the event log (wraps event emitter)') + .option('--file ', 'Path to the MACP events NDJSON file') + .option('--follow', 'Follow the file for new events (like tail -f)') + .action((opts: { file?: string; follow?: boolean }) => { + // not yet wired — event streaming requires a live event source + console.log('[macp] events tail: not yet wired — use macp package programmatically'); + if (opts.file) { + console.log(` file: ${opts.file}`); + } + if (opts.follow) { + console.log(' mode: follow'); + } + process.exitCode = 0; + }); +} diff --git a/packages/macp/src/index.ts b/packages/macp/src/index.ts index 7c5283d..073c886 100644 --- a/packages/macp/src/index.ts +++ b/packages/macp/src/index.ts @@ -41,3 +41,6 @@ export type { NormalizedGate } from './gate-runner.js'; // Event emitter export { nowISO, appendEvent, emitEvent } from './event-emitter.js'; + +// CLI +export { registerMacpCommand } from './cli.js'; diff --git a/packages/mosaic/src/cli.ts b/packages/mosaic/src/cli.ts index 286dc93..860ccfb 100644 --- a/packages/mosaic/src/cli.ts +++ b/packages/mosaic/src/cli.ts @@ -3,6 +3,7 @@ import { createRequire } from 'module'; import { Command } from 'commander'; import { registerBrainCommand } from '@mosaicstack/brain'; +import { registerMacpCommand } from '@mosaicstack/macp'; import { registerMemoryCommand } from '@mosaicstack/memory'; import { registerQualityRails } from '@mosaicstack/quality-rails'; import { registerQueueCommand } from '@mosaicstack/queue'; @@ -346,6 +347,10 @@ registerMissionCommand(program); registerBrainCommand(program); +// ─── macp ──────────────────────────────────────────────────────────────── + +registerMacpCommand(program); + // ─── quality-rails ────────────────────────────────────────────────────── registerQualityRails(program); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07adbe0..20f03a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -416,6 +416,10 @@ importers: version: 2.1.9(@types/node@24.12.0)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1) packages/macp: + dependencies: + commander: + specifier: ^13.0.0 + version: 13.1.0 devDependencies: '@types/node': specifier: ^22.0.0