import type { RoleResult, StartStep, WorkflowDefinition, WorkflowMessage, } from "@uncaged/nerve-core"; import { END } from "@uncaged/nerve-core"; import { createCursorRole, spawnSafe } from "@uncaged/nerve-workflow-utils"; import type { SpawnError } from "@uncaged/nerve-workflow-utils"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { z } from "zod"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const HOME = process.env.HOME ?? "/home/azureuser"; const NERVE_ROOT = join(HOME, ".uncaged-nerve"); const SENSES_DIR = join(NERVE_ROOT, "senses"); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function formatSpawnFailure(error: SpawnError): string { if (error.kind === "spawn_failed") return error.message; if (error.kind === "timeout") return `timeout (stdout=${error.stdout.slice(0, 200)})`; return `exit ${error.exitCode} stderr=${error.stderr.slice(0, 400)}`; } async function cfgGet(key: string): Promise { const result = await spawnSafe("cfg", ["get", key], { cwd: NERVE_ROOT, env: null, timeoutMs: 10_000, }); if (!result.ok) return null; return result.value.stdout.trim() || null; } async function resolveDashScopeProvider(): Promise<{ baseUrl: string; apiKey: string; model: string; } | null> { const apiKey = process.env.DASHSCOPE_API_KEY ?? (await cfgGet("DASHSCOPE_API_KEY")); const baseUrl = process.env.DASHSCOPE_BASE_URL ?? (await cfgGet("DASHSCOPE_BASE_URL")); const model = process.env.DASHSCOPE_MODEL ?? (await cfgGet("DASHSCOPE_MODEL")) ?? "qwen-plus"; if (!apiKey || !baseUrl) return null; return { apiKey, baseUrl, model }; } function getNerveYaml(): string { try { return readFileSync(join(NERVE_ROOT, "nerve.yaml"), "utf-8"); } catch { return "# nerve.yaml unavailable"; } } function buildSenseExamples(): string { const examples: string[] = []; for (const name of ["cpu-usage", "linux-system-health"]) { const dir = join(SENSES_DIR, name); if (!existsSync(dir)) continue; const indexFile = existsSync(join(dir, "index.js")) ? readFileSync(join(dir, "index.js"), "utf-8") : ""; const schema = existsSync(join(dir, "schema.ts")) ? readFileSync(join(dir, "schema.ts"), "utf-8") : ""; const migrationDir = join(dir, "migrations"); let migration = ""; if (existsSync(join(migrationDir, "0001_init.sql"))) { migration = readFileSync(join(migrationDir, "0001_init.sql"), "utf-8"); } examples.push( `### Example sense: ${name}\n\n` + `**index.js:**\n\`\`\`js\n${indexFile}\n\`\`\`\n\n` + `**schema.ts:**\n\`\`\`ts\n${schema}\n\`\`\`\n\n` + `**migrations/0001_init.sql:**\n\`\`\`sql\n${migration}\n\`\`\``, ); } return examples.join("\n\n---\n\n"); } async function runSenseSmokeTest( senseName: string, ): Promise<{ ok: boolean; log: string; reason: string }> { const logParts: string[] = []; const runNerve = async ( args: string[], ): Promise<{ ok: true; out: string } | { ok: false; err: string }> => { const result = await spawnSafe("nerve", args, { cwd: NERVE_ROOT, env: null, timeoutMs: 300_000, }); if (!result.ok) return { ok: false, err: formatSpawnFailure(result.error) }; return { ok: true, out: result.value.stdout }; }; const statusRun = await runNerve(["status"]); if (!statusRun.ok) { return { ok: false, log: `=== nerve status ===\nERROR: ${statusRun.err}`, reason: `Smoke test command failed: ${statusRun.err}`, }; } logParts.push("=== nerve status ===\n" + statusRun.out); if (!statusRun.out.includes(senseName)) { return { ok: false, log: logParts.join("\n\n"), reason: `Sense "${senseName}" not listed in \`nerve status\` output`, }; } const triggerRun = await runNerve(["sense", "trigger", senseName]); if (!triggerRun.ok) { logParts.push(`=== nerve sense trigger ===\nERROR: ${triggerRun.err}`); return { ok: false, log: logParts.join("\n\n"), reason: `Trigger failed: ${triggerRun.err}` }; } logParts.push("=== nerve sense trigger ===\n" + triggerRun.out); let lastQuery = ""; for (let i = 0; i < 25; i++) { await new Promise((r) => setTimeout(r, 1000)); const queryRun = await runNerve(["sense", "query", senseName]); if (!queryRun.ok) { logParts.push(`=== query attempt ${i + 1} ===\nERROR: ${queryRun.err}`); } else { lastQuery = queryRun.out; logParts.push(`=== query attempt ${i + 1} ===\n${lastQuery}`); if (!lastQuery.includes("(0 rows)")) { return { ok: true, log: logParts.join("\n\n"), reason: "Trigger succeeded and query returned at least one row", }; } } } return { ok: false, log: logParts.join("\n\n"), reason: lastQuery.includes("(0 rows)") ? "Query still returned 0 rows after trigger" : "Timed out waiting for successful sense query", }; } // --------------------------------------------------------------------------- // Meta — routing-only signals for the moderator // --------------------------------------------------------------------------- type SenseMeta = { planner: { senseName: string }; coder: { filesCreated: boolean }; tester: { passed: boolean; attempt: number }; }; // --------------------------------------------------------------------------- // Bake static context (read once at module load, not per-call) // --------------------------------------------------------------------------- const senseExamples = buildSenseExamples(); const nerveYaml = getNerveYaml(); // --------------------------------------------------------------------------- // Roles // --------------------------------------------------------------------------- async function buildPlannerRole() { const provider = await resolveDashScopeProvider(); if (provider === null) { throw new Error("Cannot create planner: set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL"); } return createCursorRole({ cwd: NERVE_ROOT, mode: "ask", prompt: async (threadId) => `You are planning a new Nerve sense. Read the workflow thread for the user's request: \`nerve thread ${threadId}\` Pick a good kebab-case name for this sense. Produce a PLAN (not code) in markdown: ## Sense Design ### Name — kebab-case ### Fields — name, type (integer/real/text), description ### Compute Logic — step-by-step, specific Node.js APIs or shell commands ### Trigger Config — group, interval, throttle, timeout Reference senses: ${senseExamples} Current nerve.yaml: \`\`\`yaml ${nerveYaml} \`\`\` Output ONLY the plan. Be precise and implementation-ready.`, extract: { provider, schema: z.object({ senseName: z.string().describe("kebab-case sense name from the plan"), }), }, }); } async function buildCoderRole() { const provider = await resolveDashScopeProvider(); if (provider === null) { throw new Error("Cannot create coder: set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL"); } return createCursorRole({ cwd: NERVE_ROOT, mode: "default", prompt: async (threadId) => `Read the workflow thread for the planner's sense design: \`nerve thread ${threadId}\` Implement the sense. Create exactly: 1. The sense directory under ${SENSES_DIR}// 2. index.js — export async function compute(db, _peers), import schema from "./schema.ts" 3. schema.ts — drizzle-orm/sqlite-core 4. migrations/0001_init.sql — must match schema.ts 5. Update ${NERVE_ROOT}/nerve.yaml — add sense config + reflex entry Follow the patterns from existing senses. Create all files now.`, extract: { provider, schema: z.object({ filesCreated: z.boolean().describe("true if the sense files were created"), }), }, }); } // Tester: pure CLI logic — stays hand-written async function tester( _start: StartStep, messages: WorkflowMessage[], ): Promise> { const attempt = messages.filter((m) => m.role === "tester").length + 1; // Get senseName from planner meta const plannerStep = messages.find((m) => m.role === "planner"); const senseName = plannerStep ? (plannerStep.meta as SenseMeta["planner"]).senseName : ""; if (senseName.length === 0) { return { content: "FAIL — no senseName from planner", meta: { passed: false, attempt }, }; } // Check files exist const senseDir = join(SENSES_DIR, senseName); const missing = [ existsSync(join(senseDir, "index.js")) ? null : "index.js", existsSync(join(senseDir, "schema.ts")) ? null : "schema.ts", existsSync(join(senseDir, "migrations", "0001_init.sql")) ? null : "migrations/0001_init.sql", ].filter((x) => x !== null); if (missing.length > 0) { return { content: `FAIL — missing files: ${missing.join(", ")}`, meta: { passed: false, attempt }, }; } // Smoke test const smoke = await runSenseSmokeTest(senseName); return { content: `${smoke.ok ? "PASS" : "FAIL"} — ${smoke.reason}`, meta: { passed: smoke.ok, attempt }, }; } // --------------------------------------------------------------------------- // Workflow definition // --------------------------------------------------------------------------- async function buildWorkflow(): Promise> { const plannerRole = await buildPlannerRole(); const coderRole = await buildCoderRole(); return { name: "sense-generator", roles: { planner: plannerRole, coder: coderRole, tester, }, moderator(context) { if (context.steps.length === 0) return "planner"; const last = context.steps[context.steps.length - 1]; if (last.role === "planner") return "coder"; if (last.role === "coder") return "tester"; if (last.role === "tester") { if (last.meta.passed) return END; return last.meta.attempt < 3 ? "coder" : END; } return END; }, }; } const workflow = await buildWorkflow(); export default workflow;