import type { WorkflowDefinition } from "@uncaged/nerve-daemon"; import { execSync } from "node:child_process"; import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; const NERVE_ROOT = join(process.env.HOME ?? "/home/azureuser", ".uncaged-nerve"); const SENSES_DIR = join(NERVE_ROOT, "senses"); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function run(cmd: string, cwd?: string): string { return execSync(cmd, { encoding: "utf-8", cwd: cwd ?? NERVE_ROOT, timeout: 300_000, env: { ...process.env, PNPM_HOME: join(process.env.HOME ?? "/home/azureuser", ".local/share/pnpm"), PATH: `${join(process.env.HOME ?? "/home/azureuser", ".local/share/pnpm")}:${process.env.PATH}`, }, }).trim(); } /** * Call a cheap LLM with tool_choice to extract structured metadata from text. * Uses DashScope (Alibaba Cloud, OpenAI-compatible) with qwen-plus. */ function llmExtract(text: string, toolName: string, toolDescription: string, parameters: Record): T { const apiKey = run("bash -c 'source ~/.profile && cfg get DASHSCOPE_API_KEY'"); const baseUrl = run("bash -c 'source ~/.profile && cfg get DASHSCOPE_BASE_URL'"); const body = JSON.stringify({ model: "qwen-plus", messages: [ { role: "system", content: "Extract the requested information from the provided text. Be precise." }, { role: "user", content: text }, ], tools: [{ type: "function" as const, function: { name: toolName, description: toolDescription, parameters }, }], tool_choice: { type: "function" as const, function: { name: toolName } }, }); const escaped = body.replace(/'/g, "'\\''"); const result = run(`curl -s '${baseUrl}/chat/completions' -H 'Authorization: Bearer ${apiKey}' -H 'Content-Type: application/json' -d '${escaped}'`); const parsed = JSON.parse(result); const toolCall = parsed.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments; if (!toolCall) throw new Error(`llmExtract failed: ${result.slice(0, 500)}`); return JSON.parse(toolCall) as T; } function cursorAgent(prompt: string, mode: "plan" | "ask" | "default", cwd: string): string { const escaped = prompt.replace(/'/g, "'\\''"); const modeFlag = mode === "plan" ? " --mode=plan" : mode === "ask" ? " --mode=ask" : ""; return run( `cursor-agent -p '${escaped}' --model auto${modeFlag} --output-format text --trust --force`, cwd, ); } function hermesRun(prompt: string): string { const escaped = prompt.replace(/'/g, "'\\''"); return run( `hermes chat -q '${escaped}' --model anthropic/claude-sonnet-4 -t terminal --yolo 2>&1 || true`, ); } // Build context string with existing sense examples 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"); } // Read current nerve.yaml function readNerveYaml(): string { return readFileSync(join(NERVE_ROOT, "nerve.yaml"), "utf-8"); } // --------------------------------------------------------------------------- // Workflow Definition // --------------------------------------------------------------------------- const workflow: WorkflowDefinition = { roles: { // ----------------------------------------------------------------------- // PLANNER: Generates a structured plan for the sense // ----------------------------------------------------------------------- planner: { async execute(prompt: unknown, ctx) { const userInput = String(prompt); ctx.log(`planner: designing sense from input: "${userInput.substring(0, 100)}"`); const planPrompt = `You are planning a new Nerve sense. User request: ${userInput} Pick a good kebab-case name for this sense. Your job is to produce a PLAN (not code) for this sense. Output a structured plan in markdown with these sections: ## Sense Design ### Name (decide a kebab-case name) ### Fields List every field the sense should collect, with name, type (integer/real/text), and description. ### Compute Logic Describe step-by-step what the compute() function should do. Be specific about which Node.js APIs or shell commands to use. ### Trigger Config - group: (suggest a group name) - interval: (decide based on the use case, e.g. 30s, 1m, 5m) - throttle: (suggest) - timeout: (suggest) Here are existing senses for reference on the format and patterns used: ${buildSenseExamples()} Current nerve.yaml: \`\`\`yaml ${readNerveYaml()} \`\`\` Output ONLY the plan in markdown. Be precise and implementation-ready.`; const plan = cursorAgent(planPrompt, "ask", NERVE_ROOT); ctx.log(`planner: plan generated (${plan.length} chars)`); // Extract sense metadata from plan using structured LLM call const meta = llmExtract<{ name: string; description: string }>( plan, "extract_sense_metadata", "Extract the sense name and a one-line description from the plan", { type: "object", properties: { name: { type: "string", description: "kebab-case sense name, e.g. 'disk-usage'" }, description: { type: "string", description: "One-line description of what this sense monitors" }, }, required: ["name", "description"], }, ); const senseName = meta.name; ctx.log(`planner: extracted sense name="${senseName}", desc="${meta.description}"`); return { type: "plan_ready", plan, senseName, userInput }; }, }, // ----------------------------------------------------------------------- // CODER: Generates sense files + updates nerve.yaml // ----------------------------------------------------------------------- coder: { async execute(prompt: unknown, ctx) { const { plan, senseName } = prompt as { plan: string; senseName: string; }; ctx.log(`coder: implementing sense "${senseName}"`); const codePrompt = `You are implementing a new Nerve sense called "${senseName}" in the directory ${SENSES_DIR}/${senseName}/. Here is the plan: ${plan} You need to create exactly 3 files: 1. \`${SENSES_DIR}/${senseName}/index.js\` — the compute() function 2. \`${SENSES_DIR}/${senseName}/schema.ts\` — Drizzle ORM schema 3. \`${SENSES_DIR}/${senseName}/migrations/0001_init.sql\` — SQLite migration And UPDATE the existing file: 4. \`${NERVE_ROOT}/nerve.yaml\` — add the new sense config and reflex entry Here are existing senses for reference — follow the EXACT same patterns: ${buildSenseExamples()} Current nerve.yaml (append to it, don't overwrite existing entries): \`\`\`yaml ${readNerveYaml()} \`\`\` IMPORTANT RULES: - index.js uses \`export async function compute(db, _peers)\` signature - index.js imports the schema table from "./schema.ts" and uses \`await db.insert(table).values({...})\` to persist - schema.ts uses drizzle-orm/sqlite-core imports - migration SQL must match schema.ts exactly - nerve.yaml: add under \`senses:\` and add a reflex under \`reflexes:\` - Use the interval specified in the plan for the reflex Create all files now.`; const result = cursorAgent(codePrompt, "default", NERVE_ROOT); ctx.log(`coder: implementation done`); // Verify files were created const senseDir = join(SENSES_DIR, senseName); const files = { index: existsSync(join(senseDir, "index.js")), schema: existsSync(join(senseDir, "schema.ts")), migration: existsSync(join(senseDir, "migrations", "0001_init.sql")), }; ctx.log(`coder: files created — index:${files.index} schema:${files.schema} migration:${files.migration}`); return { type: "code_ready", senseName, files, cursorOutput: result, }; }, }, // ----------------------------------------------------------------------- // TESTER: Triggers the sense and validates the result // ----------------------------------------------------------------------- tester: { async execute(prompt: unknown, ctx) { const { senseName, files, attempt = 1 } = prompt as { senseName: string; files: Record; attempt?: number; }; ctx.log(`tester: validating sense "${senseName}" (attempt ${attempt})`); // Check all files exist const missing = Object.entries(files).filter(([, v]) => !v).map(([k]) => k); if (missing.length > 0) { ctx.log(`tester: FAIL — missing files: ${missing.join(", ")}`); return { type: "test_failed", senseName, reason: `Missing files: ${missing.join(", ")}`, attempt, }; } // Reload daemon to pick up new sense, then trigger it const testPrompt = `You need to test a newly created Nerve sense called "${senseName}". Steps: 1. Restart the nerve daemon: run "nerve stop && nerve start" (make sure PNPM_HOME and PATH are set) 2. Wait 2 seconds 3. Check status: run "nerve status" and verify "${senseName}" appears in senses list 4. Trigger the sense: run "nerve sense trigger ${senseName}" 5. Check the result: run "nerve sense query ${senseName} --limit 1" Report whether the sense works. If there are errors, include the full error output. The nerve binary is at ~/.local/share/pnpm/nerve. Reply with either: - "PASS: " if the sense works - "FAIL: " if it doesn't`; const result = hermesRun(testPrompt); ctx.log(`tester: raw result — ${result.substring(0, 300)}`); // Use LLM to judge pass/fail instead of fragile string matching const verdict = llmExtract<{ passed: boolean; reason: string }>( `Test output for sense "${senseName}":\n\n${result.substring(0, 4000)}`, "judge_test_result", "Determine whether the test passed or failed based on the output", { type: "object", properties: { passed: { type: "boolean", description: "true if the sense was successfully triggered and returned valid data" }, reason: { type: "string", description: "Brief explanation of why it passed or failed" }, }, required: ["passed", "reason"], }, ); ctx.log(`tester: verdict — passed=${verdict.passed}, reason="${verdict.reason}"`); if (verdict.passed) { return { type: "test_passed", senseName, result: verdict.reason }; } return { type: "test_failed", senseName, reason: verdict.reason, attempt, }; }, }, }, // ------------------------------------------------------------------------- // MODERATOR: Routes the workflow through planner → coder → tester // ------------------------------------------------------------------------- moderate(thread, event) { // Initial trigger if (event.type === "thread_start") { return { role: "planner", prompt: event.triggerPayload ?? "" }; } // Plan is ready → hand to coder if (event.type === "plan_ready") { return { role: "coder", prompt: { plan: event.plan, senseName: event.senseName }, }; } // Code is ready → hand to tester if (event.type === "code_ready") { return { role: "tester", prompt: { senseName: event.senseName, files: event.files }, }; } // Test failed → retry coder (max 2 retries) if (event.type === "test_failed") { const attempt = (event.attempt as number) ?? 1; if (attempt < 3) { // Find the plan from history const planEvent = thread.events.find((e) => e.type === "plan_ready"); if (planEvent) { return { role: "coder", prompt: { plan: `${planEvent.plan}\n\n## PREVIOUS FAILURE (attempt ${attempt}):\n${event.reason}\n\nFix the issues above.`, senseName: event.senseName, }, }; } } // Give up after 3 attempts return null; } // Test passed → done if (event.type === "test_passed") { return null; } return null; }, }; export default workflow;