refactor(sense-generator): use createCursorRole and slim SenseMeta

Replace hand-written planner and coder with createCursorRole from nerve-workflow-utils. Prompts instruct reading the Nerve thread via nerve thread show. Extract uses resolveDashScopeProvider. SenseMeta keeps routing-only fields; tester remains hand-written with filesystem and smoke checks.

Made-with: Cursor
This commit is contained in:
小橘 2026-04-28 02:20:01 +00:00
parent c5ea790447
commit 8ff6003a75

View File

@ -7,8 +7,7 @@ import type {
import { END } from "@uncaged/nerve-core";
import type { SpawnError } from "@uncaged/nerve-workflow-utils";
import {
cursorAgent,
llmExtract,
createCursorRole,
nerveAgentContext,
readNerveYaml,
spawnSafe,
@ -20,6 +19,7 @@ import { z } from "zod";
const HOME = process.env.HOME ?? "/home/azureuser";
const NERVE_ROOT = join(HOME, ".uncaged-nerve");
const SENSES_DIR = join(NERVE_ROOT, "senses");
const AGENT_TIMEOUT_MS = 3_600_000;
function getNerveYaml(): string {
const result = readNerveYaml({ nerveRoot: NERVE_ROOT });
@ -67,7 +67,9 @@ function formatSpawnFailure(error: SpawnError): string {
* Run the same checks the workflow used to ask Hermes to perform, but locally.
* Hermes chat often returns UI prose instead of shell output, which caused false failures.
*/
async function runSenseSmokeTest(senseName: string): Promise<{ ok: boolean; log: string; reason: string }> {
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 }> => {
@ -170,43 +172,69 @@ function buildSenseExamples(): string {
return examples.join("\n\n---\n\n");
}
function getSenseNameFromThread(messages: WorkflowMessage[]): string {
const p = messages.find((m) => m.role === "planner");
if (p === undefined || typeof p.meta !== "object" || p.meta === null) {
return "";
}
return String((p.meta as { senseName: string }).senseName);
}
function getPlanFromThread(messages: WorkflowMessage[]): string {
const p = messages.find((m) => m.role === "planner");
return p !== undefined ? p.content : "";
}
type SenseMeta = {
planner: { plan: string; senseName: string; userInput: string };
coder: { senseName: string; files: Record<string, boolean>; cursorOutput: string };
tester: { passed: boolean; senseName: string; reason: string; attempt: number };
planner: { senseName: string };
coder: { filesCreated: boolean };
tester: { passed: boolean; attempt: number };
};
const senseMetaSchema = z
const plannerMetaSchema = z
.object({
name: z.string().describe("kebab-case sense name, e.g. 'disk-usage'"),
description: z.string().describe("One-line description of what this sense monitors"),
senseName: z
.string()
.describe("kebab-case sense name from the plan, e.g. 'disk-usage'"),
})
.describe("Extract the sense name and a one-line description from the plan");
.describe("Extract the kebab-case sense name from the plan text");
const workflow: WorkflowDefinition<SenseMeta> = {
name: "sense-generator",
const coderMetaSchema = z
.object({
filesCreated: z
.boolean()
.describe("true if index.js, schema.ts, migrations/0001_init.sql exist and nerve.yaml was updated"),
})
.describe("Whether the agent completed all file work for the sense");
roles: {
async planner(
start: StartStep,
_messages: WorkflowMessage[],
): Promise<RoleResult<SenseMeta["planner"]>> {
const userInput = start.content;
async function runPlanner(
start: StartStep,
_messages: WorkflowMessage[],
): Promise<RoleResult<SenseMeta["planner"]>> {
const userInput = start.content;
const provider = await resolveDashScopeProvider();
if (provider === null) {
return {
content:
"Cannot run planner: set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL (or configure via `cfg get`), " +
"and optionally DASHSCOPE_MODEL.",
meta: { senseName: "" },
};
}
const provider = await resolveDashScopeProvider();
if (provider === null) {
return {
content:
"Cannot run planner: set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL (or configure via `cfg get`), " +
"and optionally DASHSCOPE_MODEL.",
meta: { plan: "", senseName: "", userInput },
};
}
const planPrompt = `You are planning a new Nerve sense.
const role = createCursorRole<SenseMeta["planner"]>({
cwd: NERVE_ROOT,
mode: "ask",
timeoutMs: AGENT_TIMEOUT_MS,
prompt: async (threadId) => {
return `You are planning a new Nerve sense.
${nerveAgentContext}
**Context:** Read this workflow run for background before you plan. From a shell in \`${NERVE_ROOT}\`, run:
\`nerve thread show ${threadId} --budget 50000\`
Use the thread transcript (prior user messages and rounds) when deciding the sense.
User request: ${userInput}
Pick a good kebab-case name for this sense.
@ -239,50 +267,45 @@ ${getNerveYaml()}
\`\`\`
Output ONLY the plan in markdown. Be precise and implementation-ready.`;
const planResult = await cursorAgent({
prompt: planPrompt,
mode: "ask",
cwd: NERVE_ROOT,
env: null,
timeoutMs: null,
});
if (!planResult.ok) {
return {
content: `cursor-agent failed: ${formatSpawnFailure(planResult.error)}`,
meta: { plan: "", senseName: "", userInput },
};
}
const plan = planResult.value;
const extracted = await llmExtract({
text: plan,
schema: senseMetaSchema,
provider,
});
if (!extracted.ok) {
return {
content: `${plan}\n\n[llmExtract error] ${JSON.stringify(extracted.error)}`,
meta: { plan, senseName: "", userInput },
};
}
return {
content: plan,
meta: { plan, senseName: extracted.value.name, userInput },
};
},
extract: { provider, schema: plannerMetaSchema },
});
async coder(
_start: StartStep,
messages: WorkflowMessage[],
): Promise<RoleResult<SenseMeta["coder"]>> {
const last = messages[messages.length - 1];
const { plan, senseName } = last.meta as { plan: string; senseName: string };
try {
return await role(start, _messages);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { content: message, meta: { senseName: "" } };
}
}
const codePrompt = `You are implementing a new Nerve sense called "${senseName}" in the directory ${SENSES_DIR}/${senseName}/.
async function runCoder(
_start: StartStep,
messages: WorkflowMessage[],
): Promise<RoleResult<SenseMeta["coder"]>> {
const plan = getPlanFromThread(messages);
const senseName = getSenseNameFromThread(messages);
const provider = await resolveDashScopeProvider();
if (provider === null) {
return {
content:
"Cannot run coder: set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL (or configure via `cfg get`), " +
"and optionally DASHSCOPE_MODEL.",
meta: { filesCreated: false },
};
}
Here is the plan:
const role = createCursorRole<SenseMeta["coder"]>({
cwd: NERVE_ROOT,
mode: "default",
timeoutMs: AGENT_TIMEOUT_MS,
prompt: async (threadId) => {
return `You are implementing a new Nerve sense called "${senseName}" in the directory ${SENSES_DIR}/${senseName}/.
**Context:** Read this workflow run for background before you edit files. From a shell in \`${NERVE_ROOT}\`, run:
\`nerve thread show ${threadId} --budget 50000\`
Here is the plan (from the planner step):
${plan}
@ -311,77 +334,74 @@ IMPORTANT RULES:
- 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 agentResult = await cursorAgent({
prompt: codePrompt,
mode: "default",
cwd: NERVE_ROOT,
env: null,
timeoutMs: null,
});
if (!agentResult.ok) {
const resultText = `cursor-agent failed: ${formatSpawnFailure(agentResult.error)}`;
return {
content: resultText,
meta: {
senseName,
files: { index: false, schema: false, migration: false },
cursorOutput: resultText,
},
};
}
const result = agentResult.value;
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")),
};
return {
content: result,
meta: { senseName, files, cursorOutput: result },
};
Create all files now. End with a clear statement of whether all files and updates were created successfully, or what is still missing.`;
},
extract: { provider, schema: coderMetaSchema },
});
async tester(
_start: StartStep,
messages: WorkflowMessage[],
): Promise<RoleResult<SenseMeta["tester"]>> {
const last = messages[messages.length - 1];
const { senseName, files } = last.meta as { senseName: string; files: Record<string, boolean> };
try {
return await role(_start, messages);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return { content: message, meta: { filesCreated: false } };
}
}
const attempt = messages.filter((m) => m.role === "tester").length + 1;
async function runTester(
_start: StartStep,
messages: WorkflowMessage[],
): Promise<RoleResult<SenseMeta["tester"]>> {
const senseName = getSenseNameFromThread(messages);
const senseDir = join(SENSES_DIR, senseName);
const missing = Object.entries(files).filter(([, v]) => !v).map(([k]) => k);
if (missing.length > 0) {
return {
content: `FAIL — missing files: ${missing.join(", ")}`,
meta: { passed: false, senseName, reason: `Missing files: ${missing.join(", ")}`, attempt },
};
}
const files = {
index: existsSync(join(senseDir, "index.js")),
schema: existsSync(join(senseDir, "schema.ts")),
migration: existsSync(join(senseDir, "migrations", "0001_init.sql")),
};
const smoke = await runSenseSmokeTest(senseName);
const attempt = messages.filter((m) => m.role === "tester").length + 1;
if (smoke.ok) {
return {
content: `PASS — ${smoke.reason}`,
meta: { passed: true, senseName, reason: smoke.reason, attempt },
};
}
if (!senseName) {
return {
content: "FAIL — no senseName from planner meta",
meta: { passed: false, attempt },
};
}
return {
content: `FAIL — ${smoke.reason}`,
meta: {
passed: false,
senseName,
reason: `${smoke.reason}\n\n--- smoke log ---\n${smoke.log}`,
attempt,
},
};
const missing = Object.entries(files).filter(([, v]) => !v).map(([k]) => k);
if (missing.length > 0) {
return {
content: `FAIL — missing files: ${missing.join(", ")}`,
meta: { passed: false, attempt },
};
}
const smoke = await runSenseSmokeTest(senseName);
if (smoke.ok) {
return {
content: `PASS — ${smoke.reason}`,
meta: { passed: true, attempt },
};
}
return {
content: `FAIL — ${smoke.reason}\n\n--- smoke log ---\n${smoke.log}`,
meta: {
passed: false,
attempt,
},
};
}
const workflow: WorkflowDefinition<SenseMeta> = {
name: "sense-generator",
roles: {
planner: runPlanner,
coder: runCoder,
tester: runTester,
},
moderator(context) {