264 lines
9.0 KiB
JavaScript
264 lines
9.0 KiB
JavaScript
#!/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 <dir>]
|
||
* 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 <text> [options]
|
||
|
||
Options:
|
||
--input <text> 用户需求描述(必填)
|
||
--output <dir> 输出目录(default: fullstack/)
|
||
--domain <name> 强制领域(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();
|
||
}
|