From 2f78c72e4e6074344c0e97e1d7daeec86062db15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 30 Apr 2026 13:04:36 +0000 Subject: [PATCH] refactor(solve-issue): flatten role folders to single files Refs uncaged/nerve#284 --- workflows/solve-issue/build.ts | 16 ++--- workflows/solve-issue/moderator.ts | 16 ++--- .../{committer/prompt.ts => committer.ts} | 31 +++++++- .../solve-issue/roles/committer/index.ts | 30 -------- .../{implement/index.ts => implement.ts} | 29 +++++++- .../solve-issue/roles/implement/prompt.ts | 25 ------- .../roles/{plan/index.ts => plan.ts} | 31 +++++++- workflows/solve-issue/roles/plan/prompt.ts | 27 ------- .../roles/{prepare/prompt.ts => prepare.ts} | 21 +++++- workflows/solve-issue/roles/prepare/index.ts | 20 ------ .../roles/{publish/prompt.ts => publish.ts} | 70 ++++++++++++++++++- workflows/solve-issue/roles/publish/index.ts | 69 ------------------ .../{read-issue/prompt.ts => read-issue.ts} | 21 +++++- .../solve-issue/roles/read-issue/index.ts | 20 ------ .../roles/{review/prompt.ts => review.ts} | 26 ++++++- workflows/solve-issue/roles/review/index.ts | 25 ------- .../roles/{test/prompt.ts => test.ts} | 21 +++++- workflows/solve-issue/roles/test/index.ts | 20 ------ 18 files changed, 256 insertions(+), 262 deletions(-) rename workflows/solve-issue/roles/{committer/prompt.ts => committer.ts} (62%) delete mode 100644 workflows/solve-issue/roles/committer/index.ts rename workflows/solve-issue/roles/{implement/index.ts => implement.ts} (59%) delete mode 100644 workflows/solve-issue/roles/implement/prompt.ts rename workflows/solve-issue/roles/{plan/index.ts => plan.ts} (61%) delete mode 100644 workflows/solve-issue/roles/plan/prompt.ts rename workflows/solve-issue/roles/{prepare/prompt.ts => prepare.ts} (74%) delete mode 100644 workflows/solve-issue/roles/prepare/index.ts rename workflows/solve-issue/roles/{publish/prompt.ts => publish.ts} (53%) delete mode 100644 workflows/solve-issue/roles/publish/index.ts rename workflows/solve-issue/roles/{read-issue/prompt.ts => read-issue.ts} (56%) delete mode 100644 workflows/solve-issue/roles/read-issue/index.ts rename workflows/solve-issue/roles/{review/prompt.ts => review.ts} (50%) delete mode 100644 workflows/solve-issue/roles/review/index.ts rename workflows/solve-issue/roles/{test/prompt.ts => test.ts} (50%) delete mode 100644 workflows/solve-issue/roles/test/index.ts diff --git a/workflows/solve-issue/build.ts b/workflows/solve-issue/build.ts index 3dbfecd..1b879bc 100644 --- a/workflows/solve-issue/build.ts +++ b/workflows/solve-issue/build.ts @@ -3,14 +3,14 @@ import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { moderator } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js"; -import { createCommitterRole } from "./roles/committer/index.js"; -import { createImplementRole } from "./roles/implement/index.js"; -import { createPlanRole } from "./roles/plan/index.js"; -import { createPrepareRole } from "./roles/prepare/index.js"; -import { createPublishRole } from "./roles/publish/index.js"; -import { createReadIssueRole } from "./roles/read-issue/index.js"; -import { createReviewRole } from "./roles/review/index.js"; -import { createTestRole } from "./roles/test/index.js"; +import { createCommitterRole } from "./roles/committer.js"; +import { createImplementRole } from "./roles/implement.js"; +import { createPlanRole } from "./roles/plan.js"; +import { createPrepareRole } from "./roles/prepare.js"; +import { createPublishRole } from "./roles/publish.js"; +import { createReadIssueRole } from "./roles/read-issue.js"; +import { createReviewRole } from "./roles/review.js"; +import { createTestRole } from "./roles/test.js"; export type CreateSolveIssueDeps = { defaultAdapter: AgentFn; diff --git a/workflows/solve-issue/moderator.ts b/workflows/solve-issue/moderator.ts index a79cf65..14809b4 100644 --- a/workflows/solve-issue/moderator.ts +++ b/workflows/solve-issue/moderator.ts @@ -1,13 +1,13 @@ import { END } from "@uncaged/nerve-core"; import type { Moderator } from "@uncaged/nerve-core"; -import type { ReadIssueMeta } from "./roles/read-issue/index.js"; -import type { PrepareMeta } from "./roles/prepare/index.js"; -import type { PlanMeta } from "./roles/plan/index.js"; -import type { ImplementMeta } from "./roles/implement/index.js"; -import type { CommitterMeta } from "./roles/committer/index.js"; -import type { ReviewMeta } from "./roles/review/index.js"; -import type { TestMeta } from "./roles/test/index.js"; -import type { PublishMeta } from "./roles/publish/index.js"; +import type { ReadIssueMeta } from "./roles/read-issue.js"; +import type { PrepareMeta } from "./roles/prepare.js"; +import type { PlanMeta } from "./roles/plan.js"; +import type { ImplementMeta } from "./roles/implement.js"; +import type { CommitterMeta } from "./roles/committer.js"; +import type { ReviewMeta } from "./roles/review.js"; +import type { TestMeta } from "./roles/test.js"; +import type { PublishMeta } from "./roles/publish.js"; export type WorkflowMeta = { "read-issue": ReadIssueMeta; diff --git a/workflows/solve-issue/roles/committer/prompt.ts b/workflows/solve-issue/roles/committer.ts similarity index 62% rename from workflows/solve-issue/roles/committer/prompt.ts rename to workflows/solve-issue/roles/committer.ts index 474e2f7..7507469 100644 --- a/workflows/solve-issue/roles/committer/prompt.ts +++ b/workflows/solve-issue/roles/committer.ts @@ -1,4 +1,9 @@ -export function committerPrompt({ threadId }: { threadId: string }): string { +import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole, decorateRole, withDryRun, onFail } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +function committerPrompt({ threadId }: { threadId: string }): string { return `You are the committer agent. The **implement** step finished with a passing build; your job is to branch, commit, and push. 1. Read the workflow thread: \`nerve thread show ${threadId}\` — understand what was planned, implemented, and reviewed. @@ -26,3 +31,27 @@ or { "committed": false } \`\`\``; } + +export const committerMetaSchema = z.object({ + committed: z + .boolean() + .describe("true if branch created, changes committed, and pushed successfully"), +}); +export type CommitterMeta = z.infer; + +export function createCommitterRole( + adapter: AgentFn, + extract: LlmExtractorConfig, +): Role { + const inner = createRole( + adapter, + async (ctx: ThreadContext) => committerPrompt({ threadId: ctx.start.meta.threadId }), + committerMetaSchema, + extract, + ); + + return decorateRole(inner, [ + withDryRun({ label: "committer", meta: { committed: true } as CommitterMeta }), + onFail({ label: "committer", meta: { committed: false } as CommitterMeta }), + ]) as Role; +} diff --git a/workflows/solve-issue/roles/committer/index.ts b/workflows/solve-issue/roles/committer/index.ts deleted file mode 100644 index 44ec142..0000000 --- a/workflows/solve-issue/roles/committer/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole, decorateRole, withDryRun, onFail } from "@uncaged/nerve-workflow-utils"; -import { z } from "zod"; - -import { committerPrompt } from "./prompt.js"; - -export const committerMetaSchema = z.object({ - committed: z - .boolean() - .describe("true if branch created, changes committed, and pushed successfully"), -}); -export type CommitterMeta = z.infer; - -export function createCommitterRole( - adapter: AgentFn, - extract: LlmExtractorConfig, -): Role { - const inner = createRole( - adapter, - async (ctx: ThreadContext) => committerPrompt({ threadId: ctx.start.meta.threadId }), - committerMetaSchema, - extract, - ); - - return decorateRole(inner, [ - withDryRun({ label: "committer", meta: { committed: true } as CommitterMeta }), - onFail({ label: "committer", meta: { committed: false } as CommitterMeta }), - ]) as Role; -} diff --git a/workflows/solve-issue/roles/implement/index.ts b/workflows/solve-issue/roles/implement.ts similarity index 59% rename from workflows/solve-issue/roles/implement/index.ts rename to workflows/solve-issue/roles/implement.ts index 6839dac..59efb05 100644 --- a/workflows/solve-issue/roles/implement/index.ts +++ b/workflows/solve-issue/roles/implement.ts @@ -3,8 +3,33 @@ import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; -import { resolveRepoCwd } from "../../lib/repo-context.js"; -import { buildImplementPrompt } from "./prompt.js"; +import { resolveRepoCwd } from "../lib/repo-context.js"; + +function buildImplementPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { + return `You are the **implement** agent. You apply code changes for the issue. + +Read workflow context (plan, reviewer/test feedback): \`nerve thread show ${threadId}\` + +Read Nerve workspace conventions: \`cat ${nerveRoot}/CONVENTIONS.md\` + +Your cwd is the target repository. + +## Requirements + +1. Implement the planned changes; address reviewer/tester feedback from the thread if any. +2. Run the project **build** (\`pnpm build\`, \`npm run build\`, etc.) and fix issues until build passes. +3. Multi-step: if you cannot finish this round, explain why and set **done** to false. + +Do **not** run \`git checkout -b\`, \`git add\`, \`git commit\`, or \`git push\`. **Never** create commits on any branch — branching and commits are handled by the **committer** step after you finish. + +Then close with JSON: +\`\`\`json +{ "done": true } +\`\`\` +or \`{ "done": false }\` matching whether implementation is complete. + +**done=true** only when changes are complete **and** build passes in this round.`; +} export const implementMetaSchema = z.object({ done: z.boolean().describe("true when changes are complete and build passes this round"), diff --git a/workflows/solve-issue/roles/implement/prompt.ts b/workflows/solve-issue/roles/implement/prompt.ts deleted file mode 100644 index 4260a12..0000000 --- a/workflows/solve-issue/roles/implement/prompt.ts +++ /dev/null @@ -1,25 +0,0 @@ -export function buildImplementPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { - return `You are the **implement** agent. You apply code changes for the issue. - -Read workflow context (plan, reviewer/test feedback): \`nerve thread show ${threadId}\` - -Read Nerve workspace conventions: \`cat ${nerveRoot}/CONVENTIONS.md\` - -Your cwd is the target repository. - -## Requirements - -1. Implement the planned changes; address reviewer/tester feedback from the thread if any. -2. Run the project **build** (\`pnpm build\`, \`npm run build\`, etc.) and fix issues until build passes. -3. Multi-step: if you cannot finish this round, explain why and set **done** to false. - -Do **not** run \`git checkout -b\`, \`git add\`, \`git commit\`, or \`git push\`. **Never** create commits on any branch — branching and commits are handled by the **committer** step after you finish. - -Then close with JSON: -\`\`\`json -{ "done": true } -\`\`\` -or \`{ "done": false }\` matching whether implementation is complete. - -**done=true** only when changes are complete **and** build passes in this round.`; -} diff --git a/workflows/solve-issue/roles/plan/index.ts b/workflows/solve-issue/roles/plan.ts similarity index 61% rename from workflows/solve-issue/roles/plan/index.ts rename to workflows/solve-issue/roles/plan.ts index b99b0f4..dfe2fa3 100644 --- a/workflows/solve-issue/roles/plan/index.ts +++ b/workflows/solve-issue/roles/plan.ts @@ -3,8 +3,35 @@ import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; -import { resolveRepoCwd } from "../../lib/repo-context.js"; -import { buildPlanPrompt } from "./prompt.js"; +import { resolveRepoCwd } from "../lib/repo-context.js"; + +function buildPlanPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { + return `You are the **plan** agent (analysis only — ask mode). You produce an implementation plan for fixing the issue. + +Read workflow context: \`nerve thread show ${threadId}\` + +Read Nerve workspace conventions (coding rules for agents): \`cat ${nerveRoot}/CONVENTIONS.md\` + +In the **target repository** (your cwd), skim relevant files and read \`CONVENTIONS.md\` **if it exists** there. + +## Output + +Write an implementation plan in **markdown** with: + +1. Problem understanding +2. Change strategy +3. Target files (paths) +4. **Test commands** to run (explicit shell commands, e.g. \`pnpm test\`, \`pnpm vitest run\`) +5. Risks + +End your reply with a JSON code block (meta signal): +\`\`\`json +{ "ready": true } +\`\`\` +Use \`{ "ready": false }\` if the plan cannot be made actionable. + +**ready=true** only when the plan is clear and actionable.`; +} export const planMetaSchema = z.object({ ready: z.boolean().describe("true if plan is clear and actionable"), diff --git a/workflows/solve-issue/roles/plan/prompt.ts b/workflows/solve-issue/roles/plan/prompt.ts deleted file mode 100644 index 3647daf..0000000 --- a/workflows/solve-issue/roles/plan/prompt.ts +++ /dev/null @@ -1,27 +0,0 @@ -export function buildPlanPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { - return `You are the **plan** agent (analysis only — ask mode). You produce an implementation plan for fixing the issue. - -Read workflow context: \`nerve thread show ${threadId}\` - -Read Nerve workspace conventions (coding rules for agents): \`cat ${nerveRoot}/CONVENTIONS.md\` - -In the **target repository** (your cwd), skim relevant files and read \`CONVENTIONS.md\` **if it exists** there. - -## Output - -Write an implementation plan in **markdown** with: - -1. Problem understanding -2. Change strategy -3. Target files (paths) -4. **Test commands** to run (explicit shell commands, e.g. \`pnpm test\`, \`pnpm vitest run\`) -5. Risks - -End your reply with a JSON code block (meta signal): -\`\`\`json -{ "ready": true } -\`\`\` -Use \`{ "ready": false }\` if the plan cannot be made actionable. - -**ready=true** only when the plan is clear and actionable.`; -} diff --git a/workflows/solve-issue/roles/prepare/prompt.ts b/workflows/solve-issue/roles/prepare.ts similarity index 74% rename from workflows/solve-issue/roles/prepare/prompt.ts rename to workflows/solve-issue/roles/prepare.ts index 057c6ac..ffc8f90 100644 --- a/workflows/solve-issue/roles/prepare/prompt.ts +++ b/workflows/solve-issue/roles/prepare.ts @@ -1,4 +1,9 @@ -export function preparePrompt({ threadId }: { threadId: string }): string { +import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +function preparePrompt({ threadId }: { threadId: string }): string { return `You are the **prepare** agent. You ensure the target repository is ready for work. Read prior messages / thread for issue markers: \`nerve thread show ${threadId}\` @@ -52,3 +57,17 @@ or \`{ "ready": false }\` if the repo is invalid, or install/build baseline fail **ready=true** only when the repo exists at \`path\`, is clean, dependencies installed, and baseline build succeeded (or no build script).`; } + +export const prepareMetaSchema = z.object({ + ready: z.boolean().describe("true if repo is ready and baseline build ok"), +}); +export type PrepareMeta = z.infer; + +export function createPrepareRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { + return createRole( + adapter, + async (ctx: ThreadContext) => preparePrompt({ threadId: ctx.start.meta.threadId }), + prepareMetaSchema, + extract, + ); +} diff --git a/workflows/solve-issue/roles/prepare/index.ts b/workflows/solve-issue/roles/prepare/index.ts deleted file mode 100644 index e18eed7..0000000 --- a/workflows/solve-issue/roles/prepare/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole } 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 function createPrepareRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { - return createRole( - adapter, - async (ctx: ThreadContext) => preparePrompt({ threadId: ctx.start.meta.threadId }), - prepareMetaSchema, - extract, - ); -} diff --git a/workflows/solve-issue/roles/publish/prompt.ts b/workflows/solve-issue/roles/publish.ts similarity index 53% rename from workflows/solve-issue/roles/publish/prompt.ts rename to workflows/solve-issue/roles/publish.ts index a610e8c..17c99f3 100644 --- a/workflows/solve-issue/roles/publish/prompt.ts +++ b/workflows/solve-issue/roles/publish.ts @@ -1,4 +1,11 @@ -export function buildPublishPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import type { AgentFn, Role, RoleResult, ThreadContext } from "@uncaged/nerve-core"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +function buildPublishPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { return `You are the **publish** agent (Hermes). Test has passed. Open a pull request for the current branch using the **tea** CLI. ## Context @@ -40,3 +47,64 @@ or { "success": false } \`\`\``; } + +export const publishMetaSchema = z.object({ + success: z.boolean().describe("true if git push and tea pr create both succeeded"), +}); +export type PublishMeta = z.infer; + +export type CreatePublishRoleDeps = { + extract: LlmExtractorConfig; + nerveRoot: string; +}; + +function logPath(nerveRoot: string): string { + return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`); +} + +export function createPublishRole( + adapter: AgentFn, + { extract, nerveRoot }: CreatePublishRoleDeps, +): Role { + const innerRole = createRole( + adapter, + async (ctx: ThreadContext) => + buildPublishPrompt({ threadId: ctx.start.meta.threadId, nerveRoot }), + publishMetaSchema, + extract, + ); + + return async (ctx: ThreadContext): Promise> => { + const file = logPath(nerveRoot); + mkdirSync(join(file, ".."), { recursive: true }); + + if (isDryRun(ctx.start)) { + const msg = "[dry-run] publish skipped (no git push / PR)"; + writeFileSync(file, `${msg}\n`, "utf-8"); + return { + content: `[dry-run] publish skipped — log: ${file}`, + meta: { success: true }, + }; + } + + const innerCtx: ThreadContext = { + ...ctx, + start: { + ...ctx.start, + meta: { ...ctx.start.meta, workdir: nerveRoot }, + }, + }; + + try { + return await innerRole(innerCtx); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + const body = `publish failed: ${msg}\n`; + writeFileSync(file, body, "utf-8"); + return { + content: `publish failed: ${msg}\nLog: ${file}`, + meta: { success: false }, + }; + } + }; +} diff --git a/workflows/solve-issue/roles/publish/index.ts b/workflows/solve-issue/roles/publish/index.ts deleted file mode 100644 index 33053e6..0000000 --- a/workflows/solve-issue/roles/publish/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import type { AgentFn, Role, RoleResult, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; -import { z } from "zod"; - -import { buildPublishPrompt } from "./prompt.js"; - -export const publishMetaSchema = z.object({ - success: z.boolean().describe("true if git push and tea pr create both succeeded"), -}); -export type PublishMeta = z.infer; - -export type CreatePublishRoleDeps = { - extract: LlmExtractorConfig; - nerveRoot: string; -}; - -function logPath(nerveRoot: string): string { - return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`); -} - -export function createPublishRole( - adapter: AgentFn, - { extract, nerveRoot }: CreatePublishRoleDeps, -): Role { - const innerRole = createRole( - adapter, - async (ctx: ThreadContext) => - buildPublishPrompt({ threadId: ctx.start.meta.threadId, nerveRoot }), - publishMetaSchema, - extract, - ); - - return async (ctx: ThreadContext): Promise> => { - const file = logPath(nerveRoot); - mkdirSync(join(file, ".."), { recursive: true }); - - if (isDryRun(ctx.start)) { - const msg = "[dry-run] publish skipped (no git push / PR)"; - writeFileSync(file, `${msg}\n`, "utf-8"); - return { - content: `[dry-run] publish skipped — log: ${file}`, - meta: { success: true }, - }; - } - - const innerCtx: ThreadContext = { - ...ctx, - start: { - ...ctx.start, - meta: { ...ctx.start.meta, workdir: nerveRoot }, - }, - }; - - try { - return await innerRole(innerCtx); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - const body = `publish failed: ${msg}\n`; - writeFileSync(file, body, "utf-8"); - return { - content: `publish failed: ${msg}\nLog: ${file}`, - meta: { success: false }, - }; - } - }; -} diff --git a/workflows/solve-issue/roles/read-issue/prompt.ts b/workflows/solve-issue/roles/read-issue.ts similarity index 56% rename from workflows/solve-issue/roles/read-issue/prompt.ts rename to workflows/solve-issue/roles/read-issue.ts index 9a6ea15..312fb13 100644 --- a/workflows/solve-issue/roles/read-issue/prompt.ts +++ b/workflows/solve-issue/roles/read-issue.ts @@ -1,4 +1,9 @@ -export function readIssuePrompt({ threadId }: { threadId: string }): string { +import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +function readIssuePrompt({ threadId }: { threadId: string }): string { return `You are the **read-issue** agent. You fetch Gitea issue content via the \`tea\` CLI. Read the workflow thread start prompt for the issue URL (same run): \`nerve thread show ${threadId}\` @@ -32,3 +37,17 @@ Use \`{ "ready": false }\` if you could not fetch or parse the issue. **ready=true** only if the issue was fetched successfully and the marker block is correct.`; } + +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 function createReadIssueRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { + return createRole( + adapter, + async (ctx: ThreadContext) => readIssuePrompt({ threadId: ctx.start.meta.threadId }), + readIssueMetaSchema, + extract, + ); +} diff --git a/workflows/solve-issue/roles/read-issue/index.ts b/workflows/solve-issue/roles/read-issue/index.ts deleted file mode 100644 index 46377f9..0000000 --- a/workflows/solve-issue/roles/read-issue/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole } 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 function createReadIssueRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { - return createRole( - adapter, - async (ctx: ThreadContext) => readIssuePrompt({ threadId: ctx.start.meta.threadId }), - readIssueMetaSchema, - extract, - ); -} diff --git a/workflows/solve-issue/roles/review/prompt.ts b/workflows/solve-issue/roles/review.ts similarity index 50% rename from workflows/solve-issue/roles/review/prompt.ts rename to workflows/solve-issue/roles/review.ts index 25d108c..ff2b4bf 100644 --- a/workflows/solve-issue/roles/review/prompt.ts +++ b/workflows/solve-issue/roles/review.ts @@ -1,4 +1,9 @@ -export function reviewPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { +import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +function reviewPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { return `You are a **code reviewer** (Hermes). You run after implement and before test. Read Nerve workspace conventions: \`cat ${nerveRoot}/CONVENTIONS.md\` @@ -33,3 +38,22 @@ or { "approved": false } \`\`\``; } + +export const reviewMetaSchema = z.object({ + approved: z.boolean().describe("true if diff is clean and ready for tests"), +}); +export type ReviewMeta = z.infer; + +export function createReviewRole( + adapter: AgentFn, + extract: LlmExtractorConfig, + nerveRoot: string, +): Role { + return createRole( + adapter, + async (ctx: ThreadContext) => + reviewPrompt({ threadId: ctx.start.meta.threadId, nerveRoot }), + reviewMetaSchema, + extract, + ); +} diff --git a/workflows/solve-issue/roles/review/index.ts b/workflows/solve-issue/roles/review/index.ts deleted file mode 100644 index 9f3cc42..0000000 --- a/workflows/solve-issue/roles/review/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole } 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 function createReviewRole( - adapter: AgentFn, - extract: LlmExtractorConfig, - nerveRoot: string, -): Role { - return createRole( - adapter, - async (ctx: ThreadContext) => - reviewPrompt({ threadId: ctx.start.meta.threadId, nerveRoot }), - reviewMetaSchema, - extract, - ); -} diff --git a/workflows/solve-issue/roles/test/prompt.ts b/workflows/solve-issue/roles/test.ts similarity index 50% rename from workflows/solve-issue/roles/test/prompt.ts rename to workflows/solve-issue/roles/test.ts index bb4256d..8e8eadd 100644 --- a/workflows/solve-issue/roles/test/prompt.ts +++ b/workflows/solve-issue/roles/test.ts @@ -1,4 +1,9 @@ -export function testPrompt({ threadId }: { threadId: string }): string { +import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +function testPrompt({ threadId }: { threadId: string }): string { return `You are the **test** agent (Hermes). You execute automated tests for the change. Read workflow context: \`nerve thread show ${threadId}\` @@ -19,3 +24,17 @@ or \`{ "passed": false }\` **passed=true** only if every executed command exited 0 (or skip was justified with no failing command).`; } + +export const testMetaSchema = z.object({ + passed: z.boolean().describe("true if all test commands passed"), +}); +export type TestMeta = z.infer; + +export function createTestRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { + return createRole( + adapter, + async (ctx: ThreadContext) => testPrompt({ threadId: ctx.start.meta.threadId }), + testMetaSchema, + extract, + ); +} diff --git a/workflows/solve-issue/roles/test/index.ts b/workflows/solve-issue/roles/test/index.ts deleted file mode 100644 index 802de1e..0000000 --- a/workflows/solve-issue/roles/test/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole } 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 function createTestRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { - return createRole( - adapter, - async (ctx: ThreadContext) => testPrompt({ threadId: ctx.start.meta.threadId }), - testMetaSchema, - extract, - ); -}