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
This commit is contained in:
parent
66ce30cdfb
commit
56ce22fb1b
@ -1,4 +1,9 @@
|
|||||||
# nerve.yaml — Nerve workspace configuration
|
# nerve.yaml — Nerve workspace configuration
|
||||||
|
|
||||||
|
extract:
|
||||||
|
provider: dashscope
|
||||||
|
model: qwen-plus
|
||||||
|
|
||||||
senses:
|
senses:
|
||||||
linux-system-health:
|
linux-system-health:
|
||||||
group: system
|
group: system
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
"build": "pnpm -r build"
|
"build": "pnpm -r build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-core": "latest",
|
||||||
"@uncaged/nerve-daemon": "latest",
|
"@uncaged/nerve-daemon": "latest",
|
||||||
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils",
|
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils",
|
||||||
@ -21,6 +23,8 @@
|
|||||||
"esbuild"
|
"esbuild"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"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-daemon": "link:../repos/nerve/packages/daemon",
|
||||||
"@uncaged/nerve-core": "link:../repos/nerve/packages/core",
|
"@uncaged/nerve-core": "link:../repos/nerve/packages/core",
|
||||||
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils"
|
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils"
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@ -5,6 +5,8 @@ settings:
|
|||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
overrides:
|
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-daemon': link:../repos/nerve/packages/daemon
|
||||||
'@uncaged/nerve-core': link:../repos/nerve/packages/core
|
'@uncaged/nerve-core': link:../repos/nerve/packages/core
|
||||||
'@uncaged/nerve-workflow-utils': link:../repos/nerve/packages/workflow-utils
|
'@uncaged/nerve-workflow-utils': link:../repos/nerve/packages/workflow-utils
|
||||||
@ -13,6 +15,12 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
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':
|
'@uncaged/nerve-core':
|
||||||
specifier: link:../repos/nerve/packages/core
|
specifier: link:../repos/nerve/packages/core
|
||||||
version: link:../repos/nerve/packages/core
|
version: link:../repos/nerve/packages/core
|
||||||
@ -93,8 +101,14 @@ importers:
|
|||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
workflows/generate-sense:
|
workflows/develop-sense:
|
||||||
dependencies:
|
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':
|
'@uncaged/nerve-core':
|
||||||
specifier: link:../../../repos/nerve/packages/core
|
specifier: link:../../../repos/nerve/packages/core
|
||||||
version: link:../../../repos/nerve/packages/core
|
version: link:../../../repos/nerve/packages/core
|
||||||
@ -115,8 +129,14 @@ importers:
|
|||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
workflows/generate-workflow:
|
workflows/develop-workflow:
|
||||||
dependencies:
|
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':
|
'@uncaged/nerve-core':
|
||||||
specifier: link:../../../repos/nerve/packages/core
|
specifier: link:../../../repos/nerve/packages/core
|
||||||
version: link:../../../repos/nerve/packages/core
|
version: link:../../../repos/nerve/packages/core
|
||||||
@ -139,6 +159,12 @@ importers:
|
|||||||
|
|
||||||
workflows/solve-issue:
|
workflows/solve-issue:
|
||||||
dependencies:
|
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':
|
'@uncaged/nerve-core':
|
||||||
specifier: link:../../../repos/nerve/packages/core
|
specifier: link:../../../repos/nerve/packages/core
|
||||||
version: link:../../../repos/nerve/packages/core
|
version: link:../../../repos/nerve/packages/core
|
||||||
|
|||||||
46
workflows/_shared/cursor-ask-adapter.ts
Normal file
46
workflows/_shared/cursor-ask-adapter.ts
Normal file
@ -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<string> => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
52
workflows/_shared/rfc003-compile.ts
Normal file
52
workflows/_shared/rfc003-compile.ts
Normal file
@ -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<T>(zod: z.ZodType<T>): ZodMetaSchema<T> {
|
||||||
|
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<M extends Record<string, unknown>>(
|
||||||
|
spec: RoleSpec<M>,
|
||||||
|
deps: CompileRoleSpecDeps,
|
||||||
|
): Role<M> {
|
||||||
|
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<M>).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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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 type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
import { buildPlannerRole } from "./roles/planner/index.js";
|
|
||||||
import { buildCoderRole } from "./roles/coder/index.js";
|
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js";
|
||||||
import { buildReviewerRole } from "./roles/reviewer/index.js";
|
import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js";
|
||||||
import { buildTesterRole } from "./roles/tester/index.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 { buildCommitterRole } from "./roles/committer/index.js";
|
||||||
import { moderator } from "./moderator.js";
|
import { moderator } from "./moderator.js";
|
||||||
import type { SenseMeta } from "./moderator.js";
|
import type { SenseMeta } from "./moderator.js";
|
||||||
@ -13,17 +23,51 @@ export type BuildSenseGeneratorDeps = {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CURSOR_TIMEOUT_MS = 300_000;
|
||||||
|
|
||||||
export function buildSenseGenerator({
|
export function buildSenseGenerator({
|
||||||
provider,
|
provider,
|
||||||
cwd,
|
cwd,
|
||||||
}: BuildSenseGeneratorDeps): WorkflowDefinition<SenseMeta> {
|
}: BuildSenseGeneratorDeps): WorkflowDefinition<SenseMeta> {
|
||||||
|
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 {
|
return {
|
||||||
name: "develop-sense",
|
name: "develop-sense",
|
||||||
roles: {
|
roles: {
|
||||||
planner: buildPlannerRole({ provider, cwd }),
|
planner: compileRoleSpec(agentRoles.planner, deps),
|
||||||
coder: buildCoderRole({ provider, cwd }),
|
coder: compileRoleSpec(agentRoles.coder, deps),
|
||||||
reviewer: buildReviewerRole({ provider, nerveRoot: cwd }),
|
reviewer: compileRoleSpec(agentRoles.reviewer, deps),
|
||||||
tester: buildTesterRole({ provider, nerveRoot: cwd }),
|
tester: compileRoleSpec(agentRoles.tester, deps),
|
||||||
committer: buildCommitterRole({ nerveRoot: cwd }),
|
committer: buildCommitterRole({ nerveRoot: cwd }),
|
||||||
},
|
},
|
||||||
moderator,
|
moderator,
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const moderator: Moderator<SenseMeta> = (context) => {
|
|||||||
if (last.role === "planner") return "coder";
|
if (last.role === "planner") return "coder";
|
||||||
|
|
||||||
if (last.role === "coder") {
|
if (last.role === "coder") {
|
||||||
if (last.meta.done) return "reviewer";
|
if (last.meta.filesCreated) return "reviewer";
|
||||||
return canRetryCoder(context.steps) ? "coder" : END;
|
return canRetryCoder(context.steps) ? "coder" : END;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
"build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external"
|
"build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@uncaged/nerve-adapter-cursor": "latest",
|
||||||
|
"@uncaged/nerve-adapter-hermes": "latest",
|
||||||
"@uncaged/nerve-core": "latest",
|
"@uncaged/nerve-core": "latest",
|
||||||
"@uncaged/nerve-workflow-utils": "latest",
|
"@uncaged/nerve-workflow-utils": "latest",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
@ -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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const coderMetaSchema = z.object({
|
export const coderMetaSchema = z.object({
|
||||||
filesCreated: z.boolean().describe("true if the sense files were created"),
|
filesCreated: z.boolean().describe("true if the sense files were created"),
|
||||||
});
|
});
|
||||||
export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
||||||
|
|
||||||
export type BuildCoderDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
cwd: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildCoderRole({ provider, cwd }: BuildCoderDeps) {
|
|
||||||
return createCursorRole<CoderMeta>({
|
|
||||||
cwd,
|
|
||||||
mode: "default",
|
|
||||||
prompt: async (threadId) => coderPrompt({ threadId }),
|
|
||||||
extract: { provider, schema: coderMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -37,7 +37,13 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<Comm
|
|||||||
let success = true;
|
let success = true;
|
||||||
|
|
||||||
const run = async (cmd: string, args: string[]): Promise<boolean> => {
|
const run = async (cmd: string, args: string[]): Promise<boolean> => {
|
||||||
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) {
|
if (r.ok) {
|
||||||
lines.push(`$ ${cmd} ${args.join(" ")}`);
|
lines.push(`$ ${cmd} ${args.join(" ")}`);
|
||||||
if (r.value.stdout) lines.push(r.value.stdout);
|
if (r.value.stdout) lines.push(r.value.stdout);
|
||||||
@ -55,8 +61,10 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<Comm
|
|||||||
lines.push("timeout");
|
lines.push("timeout");
|
||||||
if (e.stdout) lines.push(e.stdout);
|
if (e.stdout) lines.push(e.stdout);
|
||||||
if (e.stderr) lines.push(e.stderr);
|
if (e.stderr) lines.push(e.stderr);
|
||||||
} else {
|
} else if (e.kind === "spawn_failed") {
|
||||||
lines.push(e.message);
|
lines.push(e.message);
|
||||||
|
} else {
|
||||||
|
lines.push(e.kind === "aborted" ? "aborted" : "error");
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -1,23 +1,6 @@
|
|||||||
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { plannerPrompt } from "./prompt.js";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const plannerMetaSchema = z.object({
|
export const plannerMetaSchema = z.object({
|
||||||
senseName: z.string().describe("kebab-case sense name from the plan"),
|
senseName: z.string().describe("kebab-case sense name from the plan"),
|
||||||
});
|
});
|
||||||
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
||||||
|
|
||||||
export type BuildPlannerDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
cwd: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildPlannerRole({ provider, cwd }: BuildPlannerDeps) {
|
|
||||||
return createCursorRole<PlannerMeta>({
|
|
||||||
cwd,
|
|
||||||
mode: "ask",
|
|
||||||
prompt: async (threadId) => plannerPrompt({ threadId }),
|
|
||||||
extract: { provider, schema: plannerMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const reviewerMetaSchema = z.object({
|
export const reviewerMetaSchema = z.object({
|
||||||
approved: z.boolean().describe("true if the diff is clean and ready for tester validation"),
|
approved: z.boolean().describe("true if the diff is clean and ready for tester validation"),
|
||||||
});
|
});
|
||||||
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
||||||
|
|
||||||
export type BuildReviewerDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
nerveRoot: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) {
|
|
||||||
return createHermesRole<ReviewerMeta>({
|
|
||||||
prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }),
|
|
||||||
extract: { provider, schema: reviewerMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const testerMetaSchema = z.object({
|
export const testerMetaSchema = z.object({
|
||||||
passed: z.boolean().describe("true if all e2e checks passed"),
|
passed: z.boolean().describe("true if all e2e checks passed"),
|
||||||
});
|
});
|
||||||
export type TesterMeta = z.infer<typeof testerMetaSchema>;
|
export type TesterMeta = z.infer<typeof testerMetaSchema>;
|
||||||
|
|
||||||
export type BuildTesterDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
nerveRoot: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildTesterRole({ provider, nerveRoot }: BuildTesterDeps) {
|
|
||||||
return createHermesRole<TesterMeta>({
|
|
||||||
prompt: async (threadId) => testerPrompt({ threadId, nerveRoot }),
|
|
||||||
extract: { provider, schema: testerMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,10 +1,19 @@
|
|||||||
import { join } from "node:path";
|
import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core";
|
||||||
import type { 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 type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
import { buildPlannerRole } from "./roles/planner/index.js";
|
|
||||||
import { buildCoderRole } from "./roles/coder/index.js";
|
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js";
|
||||||
import { buildReviewerRole } from "./roles/reviewer/index.js";
|
import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js";
|
||||||
import { buildTesterRole } from "./roles/tester/index.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 { buildCommitterRole } from "./roles/committer/index.js";
|
||||||
import { moderator } from "./moderator.js";
|
import { moderator } from "./moderator.js";
|
||||||
import type { WorkflowMeta } from "./moderator.js";
|
import type { WorkflowMeta } from "./moderator.js";
|
||||||
@ -14,17 +23,51 @@ export type BuildWorkflowGeneratorDeps = {
|
|||||||
nerveRoot: string;
|
nerveRoot: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CURSOR_TIMEOUT_MS = 300_000;
|
||||||
|
|
||||||
export function buildWorkflowGenerator({
|
export function buildWorkflowGenerator({
|
||||||
provider,
|
provider,
|
||||||
nerveRoot,
|
nerveRoot,
|
||||||
}: BuildWorkflowGeneratorDeps): WorkflowDefinition<WorkflowMeta> {
|
}: BuildWorkflowGeneratorDeps): WorkflowDefinition<WorkflowMeta> {
|
||||||
|
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 {
|
return {
|
||||||
name: "develop-workflow",
|
name: "develop-workflow",
|
||||||
roles: {
|
roles: {
|
||||||
planner: buildPlannerRole({ provider, cwd: nerveRoot }),
|
planner: compileRoleSpec(agentRoles.planner, deps),
|
||||||
coder: buildCoderRole({ provider, cwd: nerveRoot }),
|
coder: compileRoleSpec(agentRoles.coder, deps),
|
||||||
reviewer: buildReviewerRole({ provider, nerveRoot }),
|
reviewer: compileRoleSpec(agentRoles.reviewer, deps),
|
||||||
tester: buildTesterRole({ provider, nerveRoot }),
|
tester: compileRoleSpec(agentRoles.tester, deps),
|
||||||
committer: buildCommitterRole({ nerveRoot }),
|
committer: buildCommitterRole({ nerveRoot }),
|
||||||
},
|
},
|
||||||
moderator,
|
moderator,
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
"build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external"
|
"build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@uncaged/nerve-adapter-cursor": "latest",
|
||||||
|
"@uncaged/nerve-adapter-hermes": "latest",
|
||||||
"@uncaged/nerve-core": "latest",
|
"@uncaged/nerve-core": "latest",
|
||||||
"@uncaged/nerve-workflow-utils": "latest",
|
"@uncaged/nerve-workflow-utils": "latest",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
@ -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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const coderMetaSchema = z.object({
|
export const coderMetaSchema = z.object({
|
||||||
done: z.boolean().describe("true if the workflow files were created and build passes"),
|
done: z.boolean().describe("true if the workflow files were created and build passes"),
|
||||||
});
|
});
|
||||||
export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
||||||
|
|
||||||
export type BuildCoderDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
cwd: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildCoderRole({ provider, cwd }: BuildCoderDeps) {
|
|
||||||
return createCursorRole<CoderMeta>({
|
|
||||||
cwd,
|
|
||||||
mode: "default",
|
|
||||||
prompt: async (threadId) => coderPrompt({ threadId }),
|
|
||||||
extract: { provider, schema: coderMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -37,7 +37,13 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<Comm
|
|||||||
let success = true;
|
let success = true;
|
||||||
|
|
||||||
const run = async (cmd: string, args: string[]): Promise<boolean> => {
|
const run = async (cmd: string, args: string[]): Promise<boolean> => {
|
||||||
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) {
|
if (r.ok) {
|
||||||
lines.push(`$ ${cmd} ${args.join(" ")}`);
|
lines.push(`$ ${cmd} ${args.join(" ")}`);
|
||||||
if (r.value.stdout) lines.push(r.value.stdout);
|
if (r.value.stdout) lines.push(r.value.stdout);
|
||||||
@ -55,8 +61,10 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<Comm
|
|||||||
lines.push("timeout");
|
lines.push("timeout");
|
||||||
if (e.stdout) lines.push(e.stdout);
|
if (e.stdout) lines.push(e.stdout);
|
||||||
if (e.stderr) lines.push(e.stderr);
|
if (e.stderr) lines.push(e.stderr);
|
||||||
} else {
|
} else if (e.kind === "spawn_failed") {
|
||||||
lines.push(e.message);
|
lines.push(e.message);
|
||||||
|
} else {
|
||||||
|
lines.push(e.kind === "aborted" ? "aborted" : "error");
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -1,23 +1,6 @@
|
|||||||
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { plannerPrompt } from "./prompt.js";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const plannerMetaSchema = z.object({
|
export const plannerMetaSchema = z.object({
|
||||||
ready: z.boolean().describe("true if requirements are clear and a workflow can be implemented"),
|
ready: z.boolean().describe("true if requirements are clear and a workflow can be implemented"),
|
||||||
});
|
});
|
||||||
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
||||||
|
|
||||||
export type BuildPlannerDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
cwd: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildPlannerRole({ provider, cwd }: BuildPlannerDeps) {
|
|
||||||
return createCursorRole<PlannerMeta>({
|
|
||||||
cwd,
|
|
||||||
mode: "ask",
|
|
||||||
prompt: async (threadId) => plannerPrompt({ threadId }),
|
|
||||||
extract: { provider, schema: plannerMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const reviewerMetaSchema = z.object({
|
export const reviewerMetaSchema = z.object({
|
||||||
approved: z.boolean().describe("true if the diff is clean and ready for tester validation"),
|
approved: z.boolean().describe("true if the diff is clean and ready for tester validation"),
|
||||||
});
|
});
|
||||||
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
||||||
|
|
||||||
export type BuildReviewerDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
nerveRoot: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) {
|
|
||||||
return createHermesRole<ReviewerMeta>({
|
|
||||||
prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }),
|
|
||||||
extract: { provider, schema: reviewerMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const testerMetaSchema = z.object({
|
export const testerMetaSchema = z.object({
|
||||||
passed: z.boolean().describe("true if all validation checks passed"),
|
passed: z.boolean().describe("true if all validation checks passed"),
|
||||||
});
|
});
|
||||||
export type TesterMeta = z.infer<typeof testerMetaSchema>;
|
export type TesterMeta = z.infer<typeof testerMetaSchema>;
|
||||||
|
|
||||||
export type BuildTesterDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
nerveRoot: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildTesterRole({ provider, nerveRoot }: BuildTesterDeps) {
|
|
||||||
return createHermesRole<TesterMeta>({
|
|
||||||
prompt: async (threadId) => testerPrompt({ threadId, nerveRoot }),
|
|
||||||
extract: { provider, schema: testerMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
|
||||||
|
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js";
|
||||||
|
|
||||||
import { moderator } from "./moderator.js";
|
import { moderator } from "./moderator.js";
|
||||||
import type { WorkflowMeta } from "./moderator.js";
|
import type { WorkflowMeta } from "./moderator.js";
|
||||||
import { buildImplementRole } from "./roles/implement/index.js";
|
import { buildImplementRole } from "./roles/implement/index.js";
|
||||||
import { buildPlanRole } from "./roles/plan/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 { buildPublishRole } from "./roles/publish/index.js";
|
||||||
import { buildReadIssueRole } from "./roles/read-issue/index.js";
|
import { readIssueMetaSchema } from "./roles/read-issue/index.js";
|
||||||
import { buildReviewRole } from "./roles/review/index.js";
|
import { readIssuePrompt } from "./roles/read-issue/prompt.js";
|
||||||
import { buildTestRole } from "./roles/test/index.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 = {
|
export type BuildSolveIssueDeps = {
|
||||||
nerveRoot: string;
|
nerveRoot: string;
|
||||||
@ -16,15 +24,42 @@ export type BuildSolveIssueDeps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): WorkflowDefinition<WorkflowMeta> {
|
export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): WorkflowDefinition<WorkflowMeta> {
|
||||||
|
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 {
|
return {
|
||||||
name: "solve-issue",
|
name: "solve-issue",
|
||||||
roles: {
|
roles: {
|
||||||
"read-issue": buildReadIssueRole({ provider }),
|
"read-issue": compileRoleSpec(agentRoles["read-issue"], deps),
|
||||||
prepare: buildPrepareRole({ provider }),
|
prepare: compileRoleSpec(agentRoles.prepare, deps),
|
||||||
plan: buildPlanRole({ provider, nerveRoot }),
|
plan: buildPlanRole({ provider, nerveRoot }),
|
||||||
implement: buildImplementRole({ provider, nerveRoot }),
|
implement: buildImplementRole({ provider, nerveRoot }),
|
||||||
review: buildReviewRole({ provider, nerveRoot }),
|
review: compileRoleSpec(agentRoles.review, deps),
|
||||||
test: buildTestRole({ provider }),
|
test: compileRoleSpec(agentRoles.test, deps),
|
||||||
publish: buildPublishRole({ provider, nerveRoot }),
|
publish: buildPublishRole({ provider, nerveRoot }),
|
||||||
},
|
},
|
||||||
moderator,
|
moderator,
|
||||||
|
|||||||
@ -2,7 +2,12 @@ import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|||||||
import { spawnSafe } from "@uncaged/nerve-workflow-utils";
|
import { spawnSafe } from "@uncaged/nerve-workflow-utils";
|
||||||
|
|
||||||
export async function cfgGet(nerveRoot: string, key: string): Promise<string | null> {
|
export async function cfgGet(nerveRoot: string, key: string): Promise<string | null> {
|
||||||
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) {
|
if (!result.ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
"build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external"
|
"build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@uncaged/nerve-adapter-cursor": "latest",
|
||||||
|
"@uncaged/nerve-adapter-hermes": "latest",
|
||||||
"@uncaged/nerve-core": "latest",
|
"@uncaged/nerve-core": "latest",
|
||||||
"@uncaged/nerve-workflow-utils": "latest",
|
"@uncaged/nerve-workflow-utils": "latest",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
@ -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 type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js";
|
||||||
|
|
||||||
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
||||||
import { buildImplementPrompt } from "./prompt.js";
|
import { buildImplementPrompt } from "./prompt.js";
|
||||||
|
|
||||||
@ -15,7 +18,23 @@ export type BuildImplementDeps = {
|
|||||||
nerveRoot: string;
|
nerveRoot: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CURSOR_TIMEOUT_MS = 300_000;
|
||||||
|
|
||||||
export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role<ImplementMeta> {
|
export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role<ImplementMeta> {
|
||||||
|
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<ImplementMeta>;
|
||||||
|
|
||||||
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => {
|
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => {
|
||||||
const cwd = resolveRepoCwd(messages);
|
const cwd = resolveRepoCwd(messages);
|
||||||
if (cwd === null) {
|
if (cwd === null) {
|
||||||
@ -25,22 +44,18 @@ export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const runRole = createCursorRole<ImplementMeta>({
|
const run = compileRoleSpec(innerSpec, {
|
||||||
cwd,
|
provider,
|
||||||
mode: "default",
|
createContext: (s, m) => ({
|
||||||
model: "auto",
|
start: s,
|
||||||
env: {},
|
messages: m,
|
||||||
timeoutMs: 300_000,
|
workdir: cwd,
|
||||||
prompt: async () =>
|
signal: new AbortController().signal,
|
||||||
buildImplementPrompt({
|
|
||||||
threadId: (start.meta as { threadId?: string }).threadId ?? "unknown",
|
|
||||||
nerveRoot,
|
|
||||||
}),
|
}),
|
||||||
extract: { provider, schema: implementMetaSchema },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await runRole(start, messages);
|
return await run(start, messages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -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 type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
||||||
import { buildPlanPrompt } from "./prompt.js";
|
import { buildPlanPrompt } from "./prompt.js";
|
||||||
|
|
||||||
@ -15,7 +17,19 @@ export type BuildPlanDeps = {
|
|||||||
nerveRoot: string;
|
nerveRoot: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CURSOR_TIMEOUT_MS = 300_000;
|
||||||
|
|
||||||
export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<PlanMeta> {
|
export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<PlanMeta> {
|
||||||
|
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<PlanMeta>;
|
||||||
|
|
||||||
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlanMeta>> => {
|
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlanMeta>> => {
|
||||||
const cwd = resolveRepoCwd(messages);
|
const cwd = resolveRepoCwd(messages);
|
||||||
if (cwd === null) {
|
if (cwd === null) {
|
||||||
@ -25,22 +39,18 @@ export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<Plan
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const runRole = createCursorRole<PlanMeta>({
|
const run = compileRoleSpec(innerSpec, {
|
||||||
cwd,
|
provider,
|
||||||
mode: "ask",
|
createContext: (s, m) => ({
|
||||||
model: "auto",
|
start: s,
|
||||||
env: {},
|
messages: m,
|
||||||
timeoutMs: 300_000,
|
workdir: cwd,
|
||||||
prompt: async () =>
|
signal: new AbortController().signal,
|
||||||
buildPlanPrompt({
|
|
||||||
threadId: (start.meta as { threadId?: string }).threadId ?? "unknown",
|
|
||||||
nerveRoot,
|
|
||||||
}),
|
}),
|
||||||
extract: { provider, schema: planMetaSchema },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await runRole(start, messages);
|
return await run(start, messages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,20 +1,6 @@
|
|||||||
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { preparePrompt } from "./prompt.js";
|
|
||||||
|
|
||||||
export const prepareMetaSchema = z.object({
|
export const prepareMetaSchema = z.object({
|
||||||
ready: z.boolean().describe("true if repo is ready and baseline build ok"),
|
ready: z.boolean().describe("true if repo is ready and baseline build ok"),
|
||||||
});
|
});
|
||||||
export type PrepareMeta = z.infer<typeof prepareMetaSchema>;
|
export type PrepareMeta = z.infer<typeof prepareMetaSchema>;
|
||||||
|
|
||||||
export type BuildPrepareDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildPrepareRole({ provider }: BuildPrepareDeps) {
|
|
||||||
return createHermesRole<PrepareMeta>({
|
|
||||||
prompt: async (threadId) => preparePrompt({ threadId }),
|
|
||||||
extract: { provider, schema: prepareMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { mkdirSync, writeFileSync } from "node:fs";
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
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 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 { z } from "zod";
|
||||||
|
|
||||||
|
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../../../_shared/rfc003-compile.js";
|
||||||
|
|
||||||
import { buildPublishPrompt } from "./prompt.js";
|
import { buildPublishPrompt } from "./prompt.js";
|
||||||
|
|
||||||
export const publishMetaSchema = z.object({
|
export const publishMetaSchema = z.object({
|
||||||
@ -21,9 +25,15 @@ function logPath(nerveRoot: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role<PublishMeta> {
|
export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role<PublishMeta> {
|
||||||
const hermes = createHermesRole<PublishMeta>({
|
const innerSpec = {
|
||||||
prompt: async (threadId) => buildPublishPrompt({ threadId, nerveRoot }),
|
adapter: hermesAdapter,
|
||||||
extract: { provider, schema: publishMetaSchema },
|
prompt: async (start: StartStep) => buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }),
|
||||||
|
meta: zodMeta(publishMetaSchema),
|
||||||
|
} satisfies RoleSpec<PublishMeta>;
|
||||||
|
|
||||||
|
const runHermes = compileRoleSpec(innerSpec, {
|
||||||
|
provider,
|
||||||
|
createContext: defaultAgentCreateContext(nerveRoot),
|
||||||
});
|
});
|
||||||
|
|
||||||
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => {
|
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => {
|
||||||
@ -40,7 +50,7 @@ export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Rol
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await hermes(start, messages);
|
return await runHermes(start, messages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
const body = `publish failed: ${msg}\n`;
|
const body = `publish failed: ${msg}\n`;
|
||||||
|
|||||||
@ -1,20 +1,6 @@
|
|||||||
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { readIssuePrompt } from "./prompt.js";
|
|
||||||
|
|
||||||
export const readIssueMetaSchema = z.object({
|
export const readIssueMetaSchema = z.object({
|
||||||
ready: z.boolean().describe("true if issue content was fetched and markers are present"),
|
ready: z.boolean().describe("true if issue content was fetched and markers are present"),
|
||||||
});
|
});
|
||||||
export type ReadIssueMeta = z.infer<typeof readIssueMetaSchema>;
|
export type ReadIssueMeta = z.infer<typeof readIssueMetaSchema>;
|
||||||
|
|
||||||
export type BuildReadIssueDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildReadIssueRole({ provider }: BuildReadIssueDeps) {
|
|
||||||
return createHermesRole<ReadIssueMeta>({
|
|
||||||
prompt: async (threadId) => readIssuePrompt({ threadId }),
|
|
||||||
extract: { provider, schema: readIssueMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,21 +1,6 @@
|
|||||||
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { reviewPrompt } from "./prompt.js";
|
|
||||||
|
|
||||||
export const reviewMetaSchema = z.object({
|
export const reviewMetaSchema = z.object({
|
||||||
approved: z.boolean().describe("true if diff is clean and ready for tests"),
|
approved: z.boolean().describe("true if diff is clean and ready for tests"),
|
||||||
});
|
});
|
||||||
export type ReviewMeta = z.infer<typeof reviewMetaSchema>;
|
export type ReviewMeta = z.infer<typeof reviewMetaSchema>;
|
||||||
|
|
||||||
export type BuildReviewDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
nerveRoot: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildReviewRole({ provider, nerveRoot }: BuildReviewDeps) {
|
|
||||||
return createHermesRole<ReviewMeta>({
|
|
||||||
prompt: async (threadId) => reviewPrompt({ threadId, nerveRoot }),
|
|
||||||
extract: { provider, schema: reviewMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,20 +1,6 @@
|
|||||||
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { testPrompt } from "./prompt.js";
|
|
||||||
|
|
||||||
export const testMetaSchema = z.object({
|
export const testMetaSchema = z.object({
|
||||||
passed: z.boolean().describe("true if all test commands passed"),
|
passed: z.boolean().describe("true if all test commands passed"),
|
||||||
});
|
});
|
||||||
export type TestMeta = z.infer<typeof testMetaSchema>;
|
export type TestMeta = z.infer<typeof testMetaSchema>;
|
||||||
|
|
||||||
export type BuildTestDeps = {
|
|
||||||
provider: LlmProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildTestRole({ provider }: BuildTestDeps) {
|
|
||||||
return createHermesRole<TestMeta>({
|
|
||||||
prompt: async (threadId) => testPrompt({ threadId }),
|
|
||||||
extract: { provider, schema: testMetaSchema },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user