From 2c5d208691e681867300f497009977feaa79b3aa Mon Sep 17 00:00:00 2001 From: fargo Date: Mon, 17 Aug 2026 16:46:06 -0500 Subject: [PATCH] feat(mosaic): route prdy and mission --plan through PrdService (#1275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mosaic prdy becomes a thin adapter: --init/--update (wizard for TTY, service otherwise), --import with --accept-successor, --export - mosaic mission --plan persists the mission↔PRD linkage (mission id + version marker, PRD id + version) through the service instead of invoking the wizard as a second writer; non-TTY runs create non-interactively --- packages/mosaic/src/commands/mission.ts | 37 +++++++++++-- packages/mosaic/src/commands/prdy.ts | 71 ++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/packages/mosaic/src/commands/mission.ts b/packages/mosaic/src/commands/mission.ts index 9ee1539b..7a35fead 100644 --- a/packages/mosaic/src/commands/mission.ts +++ b/packages/mosaic/src/commands/mission.ts @@ -256,14 +256,41 @@ async function planMission( console.log(`Planning mission: ${mission.name}\n`); try { - const { runPrdWizard } = await import('@mosaicstack/prdy'); - await runPrdWizard({ + // Thin adapter: the PRD authority (create + mission↔PRD linkage) lives in + // PrdService — no second writer path. The mission's updatedAt serves as + // its version marker (the gateway exposes no numeric mission version). + const { PrdService, runPrdWizard } = await import('@mosaicstack/prdy'); + const service = new PrdService({ projectPath: process.cwd() }); + + if (process.stdout.isTTY) { + const created = await runPrdWizard({ + name: mission.name, + projectPath: process.cwd(), + interactive: true, + }); + const linked = await service.linkMission({ + prdId: created.id, + missionId: mission.id, + missionVersion: mission.updatedAt, + requirementIds: [], + }); + console.log( + `\nMission ${mission.id} linked to PRD ${linked.id} v${linked.version} (docs/prdy/).`, + ); + return; + } + + const doc = await service.planForMission({ name: mission.name, - projectPath: process.cwd(), - interactive: true, + missionId: mission.id, + missionVersion: mission.updatedAt, + requirementIds: [], }); + console.log( + `PRD created and linked: ${doc.id} v${doc.version} — mission ${mission.id} (docs/prdy/).`, + ); } catch (err) { - console.error(`PRD wizard failed: ${err instanceof Error ? err.message : String(err)}`); + console.error(`PRD planning failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } } diff --git a/packages/mosaic/src/commands/prdy.ts b/packages/mosaic/src/commands/prdy.ts index 47e55024..86a61c21 100644 --- a/packages/mosaic/src/commands/prdy.ts +++ b/packages/mosaic/src/commands/prdy.ts @@ -2,6 +2,10 @@ import type { Command } from 'commander'; import { withAuth } from './with-auth.js'; import { fetchProjects } from '../tui/gateway-api.js'; +/** + * `mosaic prdy` — thin adapter over PrdService (@mosaicstack/prdy). + * All reads/writes go through the service; there is no local writer path. + */ export function registerPrdyCommand(program: Command) { const cmd = program .command('prdy') @@ -9,12 +13,18 @@ export function registerPrdyCommand(program: Command) { .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .option('--init [name]', 'Create a new PRD') .option('--update [name]', 'Update an existing PRD') + .option('--import ', 'Import a YAML PRD document (validated, conflict-aware)') + .option('--accept-successor', 'With --import: accept a conflicted import as next version') + .option('--export [id]', 'Export a PRD as a labeled generated-view Markdown file') .option('--project ', 'Scope to project') .action( async (opts: { gateway: string; init?: string | boolean; update?: string | boolean; + import?: string; + acceptSuccessor?: boolean; + export?: string | boolean; project?: string; }) => { // Detect project context when --project flag is provided @@ -31,20 +41,69 @@ export function registerPrdyCommand(program: Command) { } } + const { PrdService, runPrdWizard } = await import('@mosaicstack/prdy'); + const service = new PrdService({ projectPath: process.cwd() }); + try { - const { runPrdWizard } = await import('@mosaicstack/prdy'); + if (opts.import !== undefined) { + const input = { filePath: opts.import }; + + if (opts.acceptSuccessor) { + const successor = await service.acceptSuccessor(input); + console.log( + `Import accepted as successor: ${successor.id} v${successor.version} (status: ${successor.status})`, + ); + return; + } + + const result = await service.importDocument(input); + console.log( + result.kind === 'created' + ? `Imported PRD ${result.document.id} v${result.document.version} (status: ${result.document.status})` + : `PRD ${result.document.id} already present with identical content — nothing to do.`, + ); + return; + } + + if (opts.export !== undefined) { + const id = + typeof opts.export === 'string' && opts.export.length > 0 ? opts.export : undefined; + const result = await service.exportMarkdown({ id }); + console.log( + `Generated view written: ${result.filePath} (source authority: YAML under docs/prdy/ — do not edit the Markdown)`, + ); + return; + } + const name = typeof opts.init === 'string' ? opts.init : typeof opts.update === 'string' ? opts.update : 'untitled'; - await runPrdWizard({ - name, - projectPath: process.cwd(), - interactive: true, - }); + + if (process.stdout.isTTY) { + await runPrdWizard({ + name, + projectPath: process.cwd(), + interactive: true, + }); + return; + } + + // Non-interactive fallback routes through the service directly. + const doc = await service.create({ name }); + console.log(`PRD created: ${doc.id} v${doc.version} (status: ${doc.status})`); } catch (err) { + if (err instanceof Error && err.name === 'PrdImportConflictError') { + const conflict = err as { proposal?: { version?: number } }; + console.error(`${err.message}`); + console.error( + `Original PRD left untouched. To accept the proposed successor (v${conflict.proposal?.version}), re-run with --accept-successor.`, + ); + process.exit(1); + } + console.error(`PRD wizard failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); }