🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
#!/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();
|
||||
}
|
||||
Reference in New Issue
Block a user