#!/usr/bin/env node /** * Orchestrator — V2 Protocol Stack → Factory Pipeline Bridge * * Minimal orchestrator that: * 1. Accepts user input (natural language) * 2. Generates a Task Tree + Plan record (AGENTS.md §1–2) * 3. Routes to the correct factory chain via Factory Router * 4. Captures Governance-compliant heartbeat into progress.log * 5. Returns structured result summary * * Does NOT: * - Re-implement AGENTS.md / GOVERNANCE.md rules * - Run state machines * - Replace existing CLI entry points * * Usage: * node scripts/orchestrator.mjs --input "做一个宠物管理 App" [--output ] * node scripts/orchestrator.mjs --input "做一个在线教育平台" --domain education * * @module orchestrator */ import { writeFileSync, appendFileSync, existsSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { mkdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, ".."); // ── Constants ────────────────────────────────────── const DEFAULT_OUTPUT = resolve(ROOT, "fullstack"); const PROGRESS_LOG_PATH = resolve(ROOT, "progress.log"); // ── CLI ──────────────────────────────────────────── function parseArgs() { const args = process.argv.slice(2); const opts = { input: null, output: null, domain: null, verbose: false, help: false }; for (let i = 0; i < args.length; i++) { if (args[i] === "--input" && args[i + 1]) opts.input = args[++i]; else if (args[i] === "--output" && args[i + 1]) opts.output = args[++i]; else if (args[i] === "--domain" && args[i + 1]) opts.domain = args[++i]; else if (args[i] === "--verbose") opts.verbose = true; else if (args[i] === "--help" || args[i] === "-h") opts.help = true; } return opts; } async function main() { const opts = parseArgs(); if (opts.help) { console.log(` Orchestrator — V2 Protocol Stack ↔ Factory Pipeline Usage: node scripts/orchestrator.mjs --input [options] Options: --input 用户需求描述(必填) --output 输出目录(default: fullstack/) --domain 强制领域(override auto-detect) --verbose 详细输出 --help 显示帮助 Examples: node scripts/orchestrator.mjs --input "做一个宠物管理 App" node scripts/orchestrator.mjs --input "做一个在线教育平台" --output ./my-project `); return; } if (!opts.input) { console.error("Error: --input is required. Use --help for usage."); process.exit(1); } try { const result = await run(opts.input, opts); console.log(JSON.stringify(result.summary, null, 2)); } catch (e) { console.error(`Orchestrator failed: ${e.message}`); // Write failure heartbeat writeHeartbeat({ status: "FAILED", remaining: [], risk: e.message, next_action: "investigate failure" }); process.exit(1); } } // ── Main Pipeline ───────────────────────────────── /** * Orchestrator run — the unified entry point. * * @param {string} input — User's requirement description * @param {object} opts * @param {string} [opts.output] — Output directory * @param {string} [opts.domain] — Force domain override * @returns {Promise<{summary: object, plan: object, taskTree: object, heartbeat: object}>} */ export async function run(input, opts = {}) { const startTs = new Date().toISOString(); const plan = buildPlan(input); // ── 1. Context Init & Heartbeat ── writeHeartbeat(plan.heartbeat); // ── 2. V2 Task Tree ── const taskTree = buildTaskTree(input, opts.domain); // ── 3. SF-01: Project Intake (PRD generation) ── const sf01 = await import("./project-intake-agent.mjs"); writeHeartbeat({ ...plan.heartbeat, status: "IN_PROGRESS", completed: ["T1"], remaining: plan.heartbeat.remaining.slice(1), next_action: "SF-02: generate architecture" }); const prd = sf01.generatePRD(input); if (prd.error === "EMPTY_INPUT") { throw new Error("PRD generation failed: empty input"); } if (prd.error === "EXTRACTION_FAILED") { throw new Error(prd.message); } // ── 4. SF-02: Architecture generation ── const sf02 = await import("./architecture-agent.mjs"); writeHeartbeat({ ...plan.heartbeat, status: "IN_PROGRESS", completed: ["T1", "T2"], remaining: plan.heartbeat.remaining.slice(2), next_action: "SF-03~06: fullstack compose" }); const arch = sf02.generateArchitecture(prd); if (arch.error) { throw new Error(`Architecture generation failed: ${arch.message}`); } // ── 5. Factory Router: route to fullstack composer ── const router = await import("./factory-router.mjs"); const routeResult = router.route(prd, { arch }); const factoryMod = await router.loadFactory(routeResult.factoryFn); writeHeartbeat({ ...plan.heartbeat, status: "IN_PROGRESS", completed: ["T1", "T2", "T3"], remaining: plan.heartbeat.remaining.slice(3), next_action: "writing output" }); const result = await factoryMod.composeFullstack(prd, arch); // ── 6. Write output ── const outputDir = opts.output ? resolve(opts.output) : DEFAULT_OUTPUT; factoryMod.writeFullstack(result, outputDir); // ── 7. Governance: final heartbeat ── writeHeartbeat({ ...plan.heartbeat, status: "ARCHIVED", completed: ["T1", "T2", "T3", "T4"], remaining: [], next_action: "none" }); const endTs = new Date().toISOString(); const durationMs = Date.now() - new Date(startTs).getTime(); // ── 8. Summary ── const summary = { projectName: prd.projectName, chineseName: prd.chineseName || prd.projectName, domain: prd.domain, factory: routeResult.factory, outputDir, stats: result.stats, startTs, endTs, durationMs, }; return { summary, plan, taskTree, heartbeat: plan.heartbeat }; } // ── V2 Protocol Artifacts ────────────────────────── /** * Build Task Tree per AGENTS.md §1. * * @param {string} input * @param {string} [domainOverride] * @returns {object} Task Tree */ function buildTaskTree(input, domainOverride) { return { root: { id: "T0", name: "Full-stack Product Generation", depends_on: null, description: `Generate full-stack product from: "${input.slice(0, 80)}"`, }, tasks: [ { id: "T1", name: "SF-01 Product Strategy", depends_on: ["T0"], description: "Parse requirements → PRD", parallelizable: false }, { id: "T2", name: "SF-02 Architecture Design", depends_on: ["T1"], description: "PRD → Architecture JSON", parallelizable: false }, { id: "T3", name: "SF-03~06 Full-stack Generation", depends_on: ["T2"], description: "Arch → NestJS + React code", parallelizable: false }, { id: "T4", name: "Governance Verification", depends_on: ["T3"], description: "Verify output, write audit", parallelizable: false }, ], parallelGroups: [], injectedRisks: [ "Domain match may fallback to generic → verify PRD.domain", "EXTRACTION_FAILED if input too vague → need detailed input", ], }; } /** * Build Plan per AGENTS.md §2. * * @param {string} input * @returns {object} Plan + initial heartbeat */ function buildPlan(input) { const subtasks = [ "T1: SF-01 Product Strategy — parse input, match domain, generate PRD", "T2: SF-02 Architecture — generate arch.json from PRD", "T3: SF-03~06 Full-stack — compose NestJS + React project", "T4: Write output files and finalize", ]; const heartbeat = { ts: new Date().toISOString(), status: "IN_PROGRESS", completed: [], remaining: ["T1", "T2", "T3", "T4"], risk: "", next_action: "SF-01: generate PRD", }; return { goal: `Generate a full-stack web application from: "${input.slice(0, 100)}"`, subtasks, risks: [ "Input may be too vague → EXTRACTION_FAILED", "Domain template may not fully match → generic fallback", ], verificationStrategy: { build: true, tests: true, lint: false, }, heartbeat, }; } // ── Governance Logging ───────────────────────────── /** * Append a heartbeat entry to progress.log. * * Format: JSON Lines (GOVERNANCE.md §2.4) * * @param {object} entry */ function writeHeartbeat(entry) { try { const d = dirname(PROGRESS_LOG_PATH); mkdirSync(d, { recursive: true }); appendFileSync(PROGRESS_LOG_PATH, JSON.stringify(entry) + "\n"); } catch (e) { console.error(`[orchestrator] heartbeat write failed: ${e.message}`); } } // ── CLI Bootstrap ───────────────────────────────── if (process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]))) { main(); }