From 56ce22fb1bd83057bac5c8d9bf37163a33f6c703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98=20=F0=9F=8D=8A=EF=BC=88NEKO=20Team?= =?UTF-8?q?=EF=BC=89?= Date: Wed, 29 Apr 2026 09:23:55 +0000 Subject: [PATCH] Migrate workflows to WorkflowSpec-style roles (RFC-003) Replace createCursorRole/createHermesRole with adapter + prompt + zod meta. Add shared compileRoleSpec, cursor ask adapter, nerve.yaml extract defaults. Refs #248 Made-with: Cursor --- nerve.yaml | 5 ++ package.json | 4 ++ pnpm-lock.yaml | 30 ++++++++- workflows/_shared/cursor-ask-adapter.ts | 46 ++++++++++++++ workflows/_shared/rfc003-compile.ts | 52 +++++++++++++++ workflows/develop-sense/build.ts | 62 +++++++++++++++--- workflows/develop-sense/moderator.ts | 2 +- workflows/develop-sense/package.json | 2 + workflows/develop-sense/roles/coder/index.ts | 17 ----- .../develop-sense/roles/committer/index.ts | 12 +++- .../develop-sense/roles/planner/index.ts | 17 ----- .../develop-sense/roles/reviewer/index.ts | 15 ----- workflows/develop-sense/roles/tester/index.ts | 15 ----- workflows/develop-workflow/build.ts | 63 ++++++++++++++++--- workflows/develop-workflow/package.json | 2 + .../develop-workflow/roles/coder/index.ts | 17 ----- .../develop-workflow/roles/committer/index.ts | 12 +++- .../develop-workflow/roles/planner/index.ts | 17 ----- .../develop-workflow/roles/reviewer/index.ts | 15 ----- .../develop-workflow/roles/tester/index.ts | 15 ----- workflows/solve-issue/build.ts | 53 +++++++++++++--- workflows/solve-issue/lib/provider.ts | 7 ++- workflows/solve-issue/package.json | 2 + .../solve-issue/roles/implement/index.ts | 45 ++++++++----- workflows/solve-issue/roles/plan/index.ts | 40 +++++++----- workflows/solve-issue/roles/prepare/index.ts | 14 ----- workflows/solve-issue/roles/publish/index.ts | 22 +++++-- .../solve-issue/roles/read-issue/index.ts | 14 ----- workflows/solve-issue/roles/review/index.ts | 15 ----- workflows/solve-issue/roles/test/index.ts | 14 ----- 30 files changed, 389 insertions(+), 257 deletions(-) create mode 100644 workflows/_shared/cursor-ask-adapter.ts create mode 100644 workflows/_shared/rfc003-compile.ts diff --git a/nerve.yaml b/nerve.yaml index 86c0c9b..fafca2d 100644 --- a/nerve.yaml +++ b/nerve.yaml @@ -1,4 +1,9 @@ # nerve.yaml — Nerve workspace configuration + +extract: + provider: dashscope + model: qwen-plus + senses: linux-system-health: group: system diff --git a/package.json b/package.json index fba092c..44cb05f 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "build": "pnpm -r build" }, "dependencies": { + "@uncaged/nerve-adapter-cursor": "link:../repos/nerve/packages/adapter-cursor", + "@uncaged/nerve-adapter-hermes": "link:../repos/nerve/packages/adapter-hermes", "@uncaged/nerve-core": "latest", "@uncaged/nerve-daemon": "latest", "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils", @@ -21,6 +23,8 @@ "esbuild" ], "overrides": { + "@uncaged/nerve-adapter-cursor": "link:../repos/nerve/packages/adapter-cursor", + "@uncaged/nerve-adapter-hermes": "link:../repos/nerve/packages/adapter-hermes", "@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon", "@uncaged/nerve-core": "link:../repos/nerve/packages/core", "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad325e1..62f66df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,8 @@ settings: excludeLinksFromLockfile: false overrides: + '@uncaged/nerve-adapter-cursor': link:../repos/nerve/packages/adapter-cursor + '@uncaged/nerve-adapter-hermes': link:../repos/nerve/packages/adapter-hermes '@uncaged/nerve-daemon': link:../repos/nerve/packages/daemon '@uncaged/nerve-core': link:../repos/nerve/packages/core '@uncaged/nerve-workflow-utils': link:../repos/nerve/packages/workflow-utils @@ -13,6 +15,12 @@ importers: .: dependencies: + '@uncaged/nerve-adapter-cursor': + specifier: link:../repos/nerve/packages/adapter-cursor + version: link:../repos/nerve/packages/adapter-cursor + '@uncaged/nerve-adapter-hermes': + specifier: link:../repos/nerve/packages/adapter-hermes + version: link:../repos/nerve/packages/adapter-hermes '@uncaged/nerve-core': specifier: link:../repos/nerve/packages/core version: link:../repos/nerve/packages/core @@ -93,8 +101,14 @@ importers: specifier: ^5.7.0 version: 5.9.3 - workflows/generate-sense: + workflows/develop-sense: dependencies: + '@uncaged/nerve-adapter-cursor': + specifier: link:../../../repos/nerve/packages/adapter-cursor + version: link:../../../repos/nerve/packages/adapter-cursor + '@uncaged/nerve-adapter-hermes': + specifier: link:../../../repos/nerve/packages/adapter-hermes + version: link:../../../repos/nerve/packages/adapter-hermes '@uncaged/nerve-core': specifier: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core @@ -115,8 +129,14 @@ importers: specifier: ^5.7.0 version: 5.9.3 - workflows/generate-workflow: + workflows/develop-workflow: dependencies: + '@uncaged/nerve-adapter-cursor': + specifier: link:../../../repos/nerve/packages/adapter-cursor + version: link:../../../repos/nerve/packages/adapter-cursor + '@uncaged/nerve-adapter-hermes': + specifier: link:../../../repos/nerve/packages/adapter-hermes + version: link:../../../repos/nerve/packages/adapter-hermes '@uncaged/nerve-core': specifier: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core @@ -139,6 +159,12 @@ importers: workflows/solve-issue: dependencies: + '@uncaged/nerve-adapter-cursor': + specifier: link:../../../repos/nerve/packages/adapter-cursor + version: link:../../../repos/nerve/packages/adapter-cursor + '@uncaged/nerve-adapter-hermes': + specifier: link:../../../repos/nerve/packages/adapter-hermes + version: link:../../../repos/nerve/packages/adapter-hermes '@uncaged/nerve-core': specifier: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core diff --git a/workflows/_shared/cursor-ask-adapter.ts b/workflows/_shared/cursor-ask-adapter.ts new file mode 100644 index 0000000..88cf3fb --- /dev/null +++ b/workflows/_shared/cursor-ask-adapter.ts @@ -0,0 +1,46 @@ +import { cursorAgent } from "@uncaged/nerve-adapter-cursor"; +import type { AgentFn, WorkflowContext } from "@uncaged/nerve-core"; + +const DEFAULT_MS = 300_000; + +function throwCursorError(error: { + kind: string; + exitCode?: number; + stdout?: string; + stderr?: string; + message?: string; +}): never { + if (error.kind === "non_zero_exit") { + throw new Error( + `cursor-agent: exitCode=${error.exitCode} stdout=${error.stdout} stderr=${error.stderr}`, + ); + } + if (error.kind === "timeout") { + throw new Error("cursor-agent: timeout"); + } + if (error.kind === "aborted") { + throw new Error("cursor-agent: aborted"); + } + throw new Error(`cursor-agent: ${error.message ?? error.kind}`); +} + +/** Cursor CLI in `--mode=ask` (planner-style roles). */ +export function createAskCursorAdapter(config: { model: string; timeout: number | null }): AgentFn { + const timeoutMs = config.timeout ?? DEFAULT_MS; + return async (prompt: string, context: WorkflowContext): Promise => { + const run = await cursorAgent({ + prompt, + mode: "ask", + model: config.model, + cwd: context.workdir, + env: null, + timeoutMs, + dryRun: context.start.meta.dryRun, + abortSignal: context.signal, + }); + if (!run.ok) { + throwCursorError(run.error); + } + return run.value; + }; +} diff --git a/workflows/_shared/rfc003-compile.ts b/workflows/_shared/rfc003-compile.ts new file mode 100644 index 0000000..c6962b8 --- /dev/null +++ b/workflows/_shared/rfc003-compile.ts @@ -0,0 +1,52 @@ +import type { + Role, + RoleSpec, + StartStep, + WorkflowContext, + WorkflowMessage, +} from "@uncaged/nerve-core"; +import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; +import { extractMetaOrThrow, type ZodMetaSchema } from "@uncaged/nerve-workflow-utils"; +import type { z } from "zod"; + +export function zodMeta(zod: z.ZodType): ZodMetaSchema { + return { witness: null, zod }; +} + +export type CompileRoleSpecDeps = { + provider: LlmProvider; + createContext: (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext; +}; + +/** + * RFC-003 RoleSpec → runtime Role (extract uses global nerve.yaml provider via `provider`; + * dryRun follows each invocation's start frame). + */ +export function compileRoleSpec>( + spec: RoleSpec, + deps: CompileRoleSpecDeps, +): Role { + return async (start, messages) => { + const ctx = deps.createContext(start, messages); + const promptText = + typeof spec.prompt === "string" ? spec.prompt : await spec.prompt(start, messages); + const raw = await spec.adapter(promptText, ctx); + const zod = (spec.meta as ZodMetaSchema).zod; + const meta = await extractMetaOrThrow(raw, zod, { + provider: deps.provider, + dryRun: start.meta.dryRun, + }); + return { content: raw, meta }; + }; +} + +export function defaultAgentCreateContext( + workdir: string, +): (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext { + return (start, messages) => ({ + start, + messages, + workdir, + signal: new AbortController().signal, + }); +} diff --git a/workflows/develop-sense/build.ts b/workflows/develop-sense/build.ts index bb61f9b..d8c26cf 100644 --- a/workflows/develop-sense/build.ts +++ b/workflows/develop-sense/build.ts @@ -1,9 +1,19 @@ -import type { WorkflowDefinition } from "@uncaged/nerve-core"; +import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core"; +import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { buildPlannerRole } from "./roles/planner/index.js"; -import { buildCoderRole } from "./roles/coder/index.js"; -import { buildReviewerRole } from "./roles/reviewer/index.js"; -import { buildTesterRole } from "./roles/tester/index.js"; + +import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js"; +import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js"; + +import { coderPrompt } from "./roles/coder/prompt.js"; +import { coderMetaSchema } from "./roles/coder/index.js"; +import { plannerPrompt } from "./roles/planner/prompt.js"; +import { plannerMetaSchema } from "./roles/planner/index.js"; +import { reviewerPrompt } from "./roles/reviewer/prompt.js"; +import { reviewerMetaSchema } from "./roles/reviewer/index.js"; +import { testerPrompt } from "./roles/tester/prompt.js"; +import { testerMetaSchema } from "./roles/tester/index.js"; import { buildCommitterRole } from "./roles/committer/index.js"; import { moderator } from "./moderator.js"; import type { SenseMeta } from "./moderator.js"; @@ -13,17 +23,51 @@ export type BuildSenseGeneratorDeps = { cwd: string; }; +const CURSOR_TIMEOUT_MS = 300_000; + export function buildSenseGenerator({ provider, cwd, }: BuildSenseGeneratorDeps): WorkflowDefinition { + const createContext = defaultAgentCreateContext(cwd); + const deps = { provider, createContext }; + + const agentRoles = { + planner: { + adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }), + prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }), + meta: zodMeta(plannerMetaSchema), + }, + coder: { + adapter: createCursorAdapter({ + type: "cursor", + model: "auto", + timeout: CURSOR_TIMEOUT_MS, + }), + prompt: async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }), + meta: zodMeta(coderMetaSchema), + }, + reviewer: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => + reviewerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }), + meta: zodMeta(reviewerMetaSchema), + }, + tester: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => + testerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }), + meta: zodMeta(testerMetaSchema), + }, + }; + return { name: "develop-sense", roles: { - planner: buildPlannerRole({ provider, cwd }), - coder: buildCoderRole({ provider, cwd }), - reviewer: buildReviewerRole({ provider, nerveRoot: cwd }), - tester: buildTesterRole({ provider, nerveRoot: cwd }), + planner: compileRoleSpec(agentRoles.planner, deps), + coder: compileRoleSpec(agentRoles.coder, deps), + reviewer: compileRoleSpec(agentRoles.reviewer, deps), + tester: compileRoleSpec(agentRoles.tester, deps), committer: buildCommitterRole({ nerveRoot: cwd }), }, moderator, diff --git a/workflows/develop-sense/moderator.ts b/workflows/develop-sense/moderator.ts index c9d49e0..f3a6caa 100644 --- a/workflows/develop-sense/moderator.ts +++ b/workflows/develop-sense/moderator.ts @@ -42,7 +42,7 @@ export const moderator: Moderator = (context) => { if (last.role === "planner") return "coder"; if (last.role === "coder") { - if (last.meta.done) return "reviewer"; + if (last.meta.filesCreated) return "reviewer"; return canRetryCoder(context.steps) ? "coder" : END; } diff --git a/workflows/develop-sense/package.json b/workflows/develop-sense/package.json index c9a64ac..dff8549 100644 --- a/workflows/develop-sense/package.json +++ b/workflows/develop-sense/package.json @@ -7,6 +7,8 @@ "build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external" }, "dependencies": { + "@uncaged/nerve-adapter-cursor": "latest", + "@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-core": "latest", "@uncaged/nerve-workflow-utils": "latest", "zod": "^4.3.6" diff --git a/workflows/develop-sense/roles/coder/index.ts b/workflows/develop-sense/roles/coder/index.ts index 6464fba..caf3cb5 100644 --- a/workflows/develop-sense/roles/coder/index.ts +++ b/workflows/develop-sense/roles/coder/index.ts @@ -1,23 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createCursorRole } from "@uncaged/nerve-workflow-utils"; -import { coderPrompt } from "./prompt.js"; import { z } from "zod"; export const coderMetaSchema = z.object({ filesCreated: z.boolean().describe("true if the sense files were created"), }); export type CoderMeta = z.infer; - -export type BuildCoderDeps = { - provider: LlmProvider; - cwd: string; -}; - -export function buildCoderRole({ provider, cwd }: BuildCoderDeps) { - return createCursorRole({ - cwd, - mode: "default", - prompt: async (threadId) => coderPrompt({ threadId }), - extract: { provider, schema: coderMetaSchema }, - }); -} diff --git a/workflows/develop-sense/roles/committer/index.ts b/workflows/develop-sense/roles/committer/index.ts index ee8dc91..799b277 100644 --- a/workflows/develop-sense/roles/committer/index.ts +++ b/workflows/develop-sense/roles/committer/index.ts @@ -37,7 +37,13 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role => { - const r = await spawnSafe(cmd, args, { cwd: nerveRoot, env: null, timeoutMs: 60_000, dryRun: false }); + const r = await spawnSafe(cmd, args, { + cwd: nerveRoot, + env: null, + timeoutMs: 60_000, + dryRun: false, + abortSignal: null, + }); if (r.ok) { lines.push(`$ ${cmd} ${args.join(" ")}`); if (r.value.stdout) lines.push(r.value.stdout); @@ -55,8 +61,10 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role; - -export type BuildPlannerDeps = { - provider: LlmProvider; - cwd: string; -}; - -export function buildPlannerRole({ provider, cwd }: BuildPlannerDeps) { - return createCursorRole({ - cwd, - mode: "ask", - prompt: async (threadId) => plannerPrompt({ threadId }), - extract: { provider, schema: plannerMetaSchema }, - }); -} diff --git a/workflows/develop-sense/roles/reviewer/index.ts b/workflows/develop-sense/roles/reviewer/index.ts index 9148076..b34a4a0 100644 --- a/workflows/develop-sense/roles/reviewer/index.ts +++ b/workflows/develop-sense/roles/reviewer/index.ts @@ -1,21 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; -import { reviewerPrompt } from "./prompt.js"; import { z } from "zod"; export const reviewerMetaSchema = z.object({ approved: z.boolean().describe("true if the diff is clean and ready for tester validation"), }); export type ReviewerMeta = z.infer; - -export type BuildReviewerDeps = { - provider: LlmProvider; - nerveRoot: string; -}; - -export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) { - return createHermesRole({ - prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }), - extract: { provider, schema: reviewerMetaSchema }, - }); -} diff --git a/workflows/develop-sense/roles/tester/index.ts b/workflows/develop-sense/roles/tester/index.ts index 2519b93..ffed0ce 100644 --- a/workflows/develop-sense/roles/tester/index.ts +++ b/workflows/develop-sense/roles/tester/index.ts @@ -1,21 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; -import { testerPrompt } from "./prompt.js"; import { z } from "zod"; export const testerMetaSchema = z.object({ passed: z.boolean().describe("true if all e2e checks passed"), }); export type TesterMeta = z.infer; - -export type BuildTesterDeps = { - provider: LlmProvider; - nerveRoot: string; -}; - -export function buildTesterRole({ provider, nerveRoot }: BuildTesterDeps) { - return createHermesRole({ - prompt: async (threadId) => testerPrompt({ threadId, nerveRoot }), - extract: { provider, schema: testerMetaSchema }, - }); -} diff --git a/workflows/develop-workflow/build.ts b/workflows/develop-workflow/build.ts index ad3c3dc..4716460 100644 --- a/workflows/develop-workflow/build.ts +++ b/workflows/develop-workflow/build.ts @@ -1,10 +1,19 @@ -import { join } from "node:path"; -import type { WorkflowDefinition } from "@uncaged/nerve-core"; +import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core"; +import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { buildPlannerRole } from "./roles/planner/index.js"; -import { buildCoderRole } from "./roles/coder/index.js"; -import { buildReviewerRole } from "./roles/reviewer/index.js"; -import { buildTesterRole } from "./roles/tester/index.js"; + +import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js"; +import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js"; + +import { coderPrompt } from "./roles/coder/prompt.js"; +import { coderMetaSchema } from "./roles/coder/index.js"; +import { plannerPrompt } from "./roles/planner/prompt.js"; +import { plannerMetaSchema } from "./roles/planner/index.js"; +import { reviewerPrompt } from "./roles/reviewer/prompt.js"; +import { reviewerMetaSchema } from "./roles/reviewer/index.js"; +import { testerPrompt } from "./roles/tester/prompt.js"; +import { testerMetaSchema } from "./roles/tester/index.js"; import { buildCommitterRole } from "./roles/committer/index.js"; import { moderator } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js"; @@ -14,17 +23,51 @@ export type BuildWorkflowGeneratorDeps = { nerveRoot: string; }; +const CURSOR_TIMEOUT_MS = 300_000; + export function buildWorkflowGenerator({ provider, nerveRoot, }: BuildWorkflowGeneratorDeps): WorkflowDefinition { + const createContext = defaultAgentCreateContext(nerveRoot); + const deps = { provider, createContext }; + + const agentRoles = { + planner: { + adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }), + prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }), + meta: zodMeta(plannerMetaSchema), + }, + coder: { + adapter: createCursorAdapter({ + type: "cursor", + model: "auto", + timeout: CURSOR_TIMEOUT_MS, + }), + prompt: async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }), + meta: zodMeta(coderMetaSchema), + }, + reviewer: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => + reviewerPrompt({ threadId: start.meta.threadId, nerveRoot }), + meta: zodMeta(reviewerMetaSchema), + }, + tester: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => + testerPrompt({ threadId: start.meta.threadId, nerveRoot }), + meta: zodMeta(testerMetaSchema), + }, + }; + return { name: "develop-workflow", roles: { - planner: buildPlannerRole({ provider, cwd: nerveRoot }), - coder: buildCoderRole({ provider, cwd: nerveRoot }), - reviewer: buildReviewerRole({ provider, nerveRoot }), - tester: buildTesterRole({ provider, nerveRoot }), + planner: compileRoleSpec(agentRoles.planner, deps), + coder: compileRoleSpec(agentRoles.coder, deps), + reviewer: compileRoleSpec(agentRoles.reviewer, deps), + tester: compileRoleSpec(agentRoles.tester, deps), committer: buildCommitterRole({ nerveRoot }), }, moderator, diff --git a/workflows/develop-workflow/package.json b/workflows/develop-workflow/package.json index 166f103..336bcf0 100644 --- a/workflows/develop-workflow/package.json +++ b/workflows/develop-workflow/package.json @@ -7,6 +7,8 @@ "build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external" }, "dependencies": { + "@uncaged/nerve-adapter-cursor": "latest", + "@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-core": "latest", "@uncaged/nerve-workflow-utils": "latest", "zod": "^4.3.6" diff --git a/workflows/develop-workflow/roles/coder/index.ts b/workflows/develop-workflow/roles/coder/index.ts index 4deac31..bf527b9 100644 --- a/workflows/develop-workflow/roles/coder/index.ts +++ b/workflows/develop-workflow/roles/coder/index.ts @@ -1,23 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createCursorRole } from "@uncaged/nerve-workflow-utils"; -import { coderPrompt } from "./prompt.js"; import { z } from "zod"; export const coderMetaSchema = z.object({ done: z.boolean().describe("true if the workflow files were created and build passes"), }); export type CoderMeta = z.infer; - -export type BuildCoderDeps = { - provider: LlmProvider; - cwd: string; -}; - -export function buildCoderRole({ provider, cwd }: BuildCoderDeps) { - return createCursorRole({ - cwd, - mode: "default", - prompt: async (threadId) => coderPrompt({ threadId }), - extract: { provider, schema: coderMetaSchema }, - }); -} diff --git a/workflows/develop-workflow/roles/committer/index.ts b/workflows/develop-workflow/roles/committer/index.ts index 1d6a989..adb10cf 100644 --- a/workflows/develop-workflow/roles/committer/index.ts +++ b/workflows/develop-workflow/roles/committer/index.ts @@ -37,7 +37,13 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role => { - const r = await spawnSafe(cmd, args, { cwd: nerveRoot, env: null, timeoutMs: 60_000, dryRun: false }); + const r = await spawnSafe(cmd, args, { + cwd: nerveRoot, + env: null, + timeoutMs: 60_000, + dryRun: false, + abortSignal: null, + }); if (r.ok) { lines.push(`$ ${cmd} ${args.join(" ")}`); if (r.value.stdout) lines.push(r.value.stdout); @@ -55,8 +61,10 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role; - -export type BuildPlannerDeps = { - provider: LlmProvider; - cwd: string; -}; - -export function buildPlannerRole({ provider, cwd }: BuildPlannerDeps) { - return createCursorRole({ - cwd, - mode: "ask", - prompt: async (threadId) => plannerPrompt({ threadId }), - extract: { provider, schema: plannerMetaSchema }, - }); -} diff --git a/workflows/develop-workflow/roles/reviewer/index.ts b/workflows/develop-workflow/roles/reviewer/index.ts index 9148076..b34a4a0 100644 --- a/workflows/develop-workflow/roles/reviewer/index.ts +++ b/workflows/develop-workflow/roles/reviewer/index.ts @@ -1,21 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; -import { reviewerPrompt } from "./prompt.js"; import { z } from "zod"; export const reviewerMetaSchema = z.object({ approved: z.boolean().describe("true if the diff is clean and ready for tester validation"), }); export type ReviewerMeta = z.infer; - -export type BuildReviewerDeps = { - provider: LlmProvider; - nerveRoot: string; -}; - -export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) { - return createHermesRole({ - prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }), - extract: { provider, schema: reviewerMetaSchema }, - }); -} diff --git a/workflows/develop-workflow/roles/tester/index.ts b/workflows/develop-workflow/roles/tester/index.ts index 52117b6..ed17c3b 100644 --- a/workflows/develop-workflow/roles/tester/index.ts +++ b/workflows/develop-workflow/roles/tester/index.ts @@ -1,21 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; -import { testerPrompt } from "./prompt.js"; import { z } from "zod"; export const testerMetaSchema = z.object({ passed: z.boolean().describe("true if all validation checks passed"), }); export type TesterMeta = z.infer; - -export type BuildTesterDeps = { - provider: LlmProvider; - nerveRoot: string; -}; - -export function buildTesterRole({ provider, nerveRoot }: BuildTesterDeps) { - return createHermesRole({ - prompt: async (threadId) => testerPrompt({ threadId, nerveRoot }), - extract: { provider, schema: testerMetaSchema }, - }); -} diff --git a/workflows/solve-issue/build.ts b/workflows/solve-issue/build.ts index d67c257..cda118d 100644 --- a/workflows/solve-issue/build.ts +++ b/workflows/solve-issue/build.ts @@ -1,14 +1,22 @@ -import type { WorkflowDefinition } from "@uncaged/nerve-core"; +import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; + +import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js"; + import { moderator } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js"; import { buildImplementRole } from "./roles/implement/index.js"; import { buildPlanRole } from "./roles/plan/index.js"; -import { buildPrepareRole } from "./roles/prepare/index.js"; +import { prepareMetaSchema } from "./roles/prepare/index.js"; +import { preparePrompt } from "./roles/prepare/prompt.js"; import { buildPublishRole } from "./roles/publish/index.js"; -import { buildReadIssueRole } from "./roles/read-issue/index.js"; -import { buildReviewRole } from "./roles/review/index.js"; -import { buildTestRole } from "./roles/test/index.js"; +import { readIssueMetaSchema } from "./roles/read-issue/index.js"; +import { readIssuePrompt } from "./roles/read-issue/prompt.js"; +import { reviewMetaSchema } from "./roles/review/index.js"; +import { reviewPrompt } from "./roles/review/prompt.js"; +import { testMetaSchema } from "./roles/test/index.js"; +import { testPrompt } from "./roles/test/prompt.js"; export type BuildSolveIssueDeps = { nerveRoot: string; @@ -16,15 +24,42 @@ export type BuildSolveIssueDeps = { }; export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): WorkflowDefinition { + const createContext = defaultAgentCreateContext(nerveRoot); + const deps = { provider, createContext }; + + const agentRoles = { + "read-issue": { + adapter: hermesAdapter, + prompt: async (start: StartStep) => readIssuePrompt({ threadId: start.meta.threadId }), + meta: zodMeta(readIssueMetaSchema), + }, + prepare: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => preparePrompt({ threadId: start.meta.threadId }), + meta: zodMeta(prepareMetaSchema), + }, + review: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => + reviewPrompt({ threadId: start.meta.threadId, nerveRoot }), + meta: zodMeta(reviewMetaSchema), + }, + test: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => testPrompt({ threadId: start.meta.threadId }), + meta: zodMeta(testMetaSchema), + }, + }; + return { name: "solve-issue", roles: { - "read-issue": buildReadIssueRole({ provider }), - prepare: buildPrepareRole({ provider }), + "read-issue": compileRoleSpec(agentRoles["read-issue"], deps), + prepare: compileRoleSpec(agentRoles.prepare, deps), plan: buildPlanRole({ provider, nerveRoot }), implement: buildImplementRole({ provider, nerveRoot }), - review: buildReviewRole({ provider, nerveRoot }), - test: buildTestRole({ provider }), + review: compileRoleSpec(agentRoles.review, deps), + test: compileRoleSpec(agentRoles.test, deps), publish: buildPublishRole({ provider, nerveRoot }), }, moderator, diff --git a/workflows/solve-issue/lib/provider.ts b/workflows/solve-issue/lib/provider.ts index 22b88a2..a1bde83 100644 --- a/workflows/solve-issue/lib/provider.ts +++ b/workflows/solve-issue/lib/provider.ts @@ -2,7 +2,12 @@ import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import { spawnSafe } from "@uncaged/nerve-workflow-utils"; export async function cfgGet(nerveRoot: string, key: string): Promise { - const result = await spawnSafe("cfg", ["get", key], { cwd: nerveRoot, env: null, timeoutMs: 10_000 }); + const result = await spawnSafe("cfg", ["get", key], { + cwd: nerveRoot, + env: null, + timeoutMs: 10_000, + abortSignal: null, + }); if (!result.ok) { return null; } diff --git a/workflows/solve-issue/package.json b/workflows/solve-issue/package.json index dee9b0d..ce6e1eb 100644 --- a/workflows/solve-issue/package.json +++ b/workflows/solve-issue/package.json @@ -7,6 +7,8 @@ "build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external" }, "dependencies": { + "@uncaged/nerve-adapter-cursor": "latest", + "@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-core": "latest", "@uncaged/nerve-workflow-utils": "latest", "zod": "^4.3.6" diff --git a/workflows/solve-issue/roles/implement/index.ts b/workflows/solve-issue/roles/implement/index.ts index f801560..51c29be 100644 --- a/workflows/solve-issue/roles/implement/index.ts +++ b/workflows/solve-issue/roles/implement/index.ts @@ -1,7 +1,10 @@ -import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; +import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; +import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createCursorRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; + +import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js"; + import { resolveRepoCwd } from "../../lib/repo-context.js"; import { buildImplementPrompt } from "./prompt.js"; @@ -15,7 +18,23 @@ export type BuildImplementDeps = { nerveRoot: string; }; +const CURSOR_TIMEOUT_MS = 300_000; + export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role { + const innerSpec = { + adapter: createCursorAdapter({ + type: "cursor", + model: "auto", + timeout: CURSOR_TIMEOUT_MS, + }), + prompt: async (start: StartStep) => + buildImplementPrompt({ + threadId: start.meta.threadId, + nerveRoot, + }), + meta: zodMeta(implementMetaSchema), + } satisfies RoleSpec; + return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { const cwd = resolveRepoCwd(messages); if (cwd === null) { @@ -25,22 +44,18 @@ export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): }; } - const runRole = createCursorRole({ - cwd, - mode: "default", - model: "auto", - env: {}, - timeoutMs: 300_000, - prompt: async () => - buildImplementPrompt({ - threadId: (start.meta as { threadId?: string }).threadId ?? "unknown", - nerveRoot, - }), - extract: { provider, schema: implementMetaSchema }, + const run = compileRoleSpec(innerSpec, { + provider, + createContext: (s, m) => ({ + start: s, + messages: m, + workdir: cwd, + signal: new AbortController().signal, + }), }); try { - return await runRole(start, messages); + return await run(start, messages); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { diff --git a/workflows/solve-issue/roles/plan/index.ts b/workflows/solve-issue/roles/plan/index.ts index 349d99e..827dbb6 100644 --- a/workflows/solve-issue/roles/plan/index.ts +++ b/workflows/solve-issue/roles/plan/index.ts @@ -1,7 +1,9 @@ -import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; +import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; +import { createAskCursorAdapter } from "../../../_shared/cursor-ask-adapter.js"; +import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createCursorRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; + import { resolveRepoCwd } from "../../lib/repo-context.js"; import { buildPlanPrompt } from "./prompt.js"; @@ -15,7 +17,19 @@ export type BuildPlanDeps = { nerveRoot: string; }; +const CURSOR_TIMEOUT_MS = 300_000; + export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role { + const innerSpec = { + adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }), + prompt: async (start: StartStep) => + buildPlanPrompt({ + threadId: start.meta.threadId, + nerveRoot, + }), + meta: zodMeta(planMetaSchema), + } satisfies RoleSpec; + return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { const cwd = resolveRepoCwd(messages); if (cwd === null) { @@ -25,22 +39,18 @@ export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role({ - cwd, - mode: "ask", - model: "auto", - env: {}, - timeoutMs: 300_000, - prompt: async () => - buildPlanPrompt({ - threadId: (start.meta as { threadId?: string }).threadId ?? "unknown", - nerveRoot, - }), - extract: { provider, schema: planMetaSchema }, + const run = compileRoleSpec(innerSpec, { + provider, + createContext: (s, m) => ({ + start: s, + messages: m, + workdir: cwd, + signal: new AbortController().signal, + }), }); try { - return await runRole(start, messages); + return await run(start, messages); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { diff --git a/workflows/solve-issue/roles/prepare/index.ts b/workflows/solve-issue/roles/prepare/index.ts index 490393b..5301e0b 100644 --- a/workflows/solve-issue/roles/prepare/index.ts +++ b/workflows/solve-issue/roles/prepare/index.ts @@ -1,20 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; -import { preparePrompt } from "./prompt.js"; export const prepareMetaSchema = z.object({ ready: z.boolean().describe("true if repo is ready and baseline build ok"), }); export type PrepareMeta = z.infer; - -export type BuildPrepareDeps = { - provider: LlmProvider; -}; - -export function buildPrepareRole({ provider }: BuildPrepareDeps) { - return createHermesRole({ - prompt: async (threadId) => preparePrompt({ threadId }), - extract: { provider, schema: prepareMetaSchema }, - }); -} diff --git a/workflows/solve-issue/roles/publish/index.ts b/workflows/solve-issue/roles/publish/index.ts index 9b03095..762b167 100644 --- a/workflows/solve-issue/roles/publish/index.ts +++ b/workflows/solve-issue/roles/publish/index.ts @@ -1,9 +1,13 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; +import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole, isDryRun } from "@uncaged/nerve-workflow-utils"; +import { isDryRun } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; + +import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../../../_shared/rfc003-compile.js"; + import { buildPublishPrompt } from "./prompt.js"; export const publishMetaSchema = z.object({ @@ -21,9 +25,15 @@ function logPath(nerveRoot: string): string { } export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role { - const hermes = createHermesRole({ - prompt: async (threadId) => buildPublishPrompt({ threadId, nerveRoot }), - extract: { provider, schema: publishMetaSchema }, + const innerSpec = { + adapter: hermesAdapter, + prompt: async (start: StartStep) => buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }), + meta: zodMeta(publishMetaSchema), + } satisfies RoleSpec; + + const runHermes = compileRoleSpec(innerSpec, { + provider, + createContext: defaultAgentCreateContext(nerveRoot), }); return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { @@ -40,7 +50,7 @@ export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Rol } try { - return await hermes(start, messages); + return await runHermes(start, messages); } catch (e) { const msg = e instanceof Error ? e.message : String(e); const body = `publish failed: ${msg}\n`; diff --git a/workflows/solve-issue/roles/read-issue/index.ts b/workflows/solve-issue/roles/read-issue/index.ts index c856e46..0b029e7 100644 --- a/workflows/solve-issue/roles/read-issue/index.ts +++ b/workflows/solve-issue/roles/read-issue/index.ts @@ -1,20 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; -import { readIssuePrompt } from "./prompt.js"; export const readIssueMetaSchema = z.object({ ready: z.boolean().describe("true if issue content was fetched and markers are present"), }); export type ReadIssueMeta = z.infer; - -export type BuildReadIssueDeps = { - provider: LlmProvider; -}; - -export function buildReadIssueRole({ provider }: BuildReadIssueDeps) { - return createHermesRole({ - prompt: async (threadId) => readIssuePrompt({ threadId }), - extract: { provider, schema: readIssueMetaSchema }, - }); -} diff --git a/workflows/solve-issue/roles/review/index.ts b/workflows/solve-issue/roles/review/index.ts index dbe6900..c7112ba 100644 --- a/workflows/solve-issue/roles/review/index.ts +++ b/workflows/solve-issue/roles/review/index.ts @@ -1,21 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; -import { reviewPrompt } from "./prompt.js"; export const reviewMetaSchema = z.object({ approved: z.boolean().describe("true if diff is clean and ready for tests"), }); export type ReviewMeta = z.infer; - -export type BuildReviewDeps = { - provider: LlmProvider; - nerveRoot: string; -}; - -export function buildReviewRole({ provider, nerveRoot }: BuildReviewDeps) { - return createHermesRole({ - prompt: async (threadId) => reviewPrompt({ threadId, nerveRoot }), - extract: { provider, schema: reviewMetaSchema }, - }); -} diff --git a/workflows/solve-issue/roles/test/index.ts b/workflows/solve-issue/roles/test/index.ts index aa95899..ef0467c 100644 --- a/workflows/solve-issue/roles/test/index.ts +++ b/workflows/solve-issue/roles/test/index.ts @@ -1,20 +1,6 @@ -import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; -import { createHermesRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; -import { testPrompt } from "./prompt.js"; export const testMetaSchema = z.object({ passed: z.boolean().describe("true if all test commands passed"), }); export type TestMeta = z.infer; - -export type BuildTestDeps = { - provider: LlmProvider; -}; - -export function buildTestRole({ provider }: BuildTestDeps) { - return createHermesRole({ - prompt: async (threadId) => testPrompt({ threadId }), - extract: { provider, schema: testMetaSchema }, - }); -}