Files
2026-06-06 10:40:48 +08:00

264 lines
9.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 §12)
* 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();
}