diff --git a/workflows/solve-issue/build.ts b/workflows/solve-issue/build.ts index 1ad70e4..d67c257 100644 --- a/workflows/solve-issue/build.ts +++ b/workflows/solve-issue/build.ts @@ -25,7 +25,7 @@ export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): W implement: buildImplementRole({ provider, nerveRoot }), review: buildReviewRole({ provider, nerveRoot }), test: buildTestRole({ provider }), - publish: buildPublishRole({ nerveRoot }), + publish: buildPublishRole({ provider, nerveRoot }), }, moderator, }; diff --git a/workflows/solve-issue/roles/implement/index.ts b/workflows/solve-issue/roles/implement/index.ts index a30b16c..50b4da5 100644 --- a/workflows/solve-issue/roles/implement/index.ts +++ b/workflows/solve-issue/roles/implement/index.ts @@ -1,10 +1,9 @@ import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; -import { cursorAgent, isDryRun, llmExtract } 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 { resolveRepoCwd } from "../../lib/repo-context.js"; import { threadIdFromStart } from "../../lib/start-meta.js"; -import { formatSpawnFailure } from "../../lib/spawn-utils.js"; import { buildImplementPrompt } from "./prompt.js"; export const implementMetaSchema = z.object({ @@ -27,38 +26,24 @@ export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): }; } - const dry = isDryRun(start); - const prompt = buildImplementPrompt({ threadId: threadIdFromStart(start), nerveRoot }); - const run = await cursorAgent({ - prompt, + const runRole = createCursorRole({ + cwd, mode: "default", model: "auto", - cwd, - env: null, + env: {}, timeoutMs: 300_000, - dryRun: dry, + prompt: async () => buildImplementPrompt({ threadId: threadIdFromStart(start), nerveRoot }), + extract: { provider, schema: implementMetaSchema }, }); - if (!run.ok) { + try { + return await runRole(start, messages); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); return { - content: `implement cursor-agent failed: ${formatSpawnFailure(run.error)}`, + content: `implement failed: ${msg}`, meta: { done: false }, }; } - - const metaR = await llmExtract({ - text: run.value, - schema: implementMetaSchema, - provider, - dryRun: dry, - }); - if (!metaR.ok) { - return { - content: `${run.value}\n\n[meta extract failed]`, - meta: { done: false }, - }; - } - - return { content: run.value, meta: metaR.value }; }; } diff --git a/workflows/solve-issue/roles/publish/index.ts b/workflows/solve-issue/roles/publish/index.ts index fd6c48f..9b03095 100644 --- a/workflows/solve-issue/roles/publish/index.ts +++ b/workflows/solve-issue/roles/publish/index.ts @@ -1,16 +1,18 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; -import { isDryRun, spawnSafe } from "@uncaged/nerve-workflow-utils"; -import { cfgGet } from "../../lib/provider.js"; -import { lastParseFromMessages, lastRepoFromMessages, resolveRepoCwd } from "../../lib/repo-context.js"; -import { formatSpawnFailure } from "../../lib/spawn-utils.js"; +import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; +import { createHermesRole, isDryRun } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; +import { buildPublishPrompt } from "./prompt.js"; -export type PublishMeta = { - success: boolean; -}; +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 BuildPublishDeps = { + provider: LlmProvider; nerveRoot: string; }; @@ -18,17 +20,17 @@ function logPath(nerveRoot: string): string { return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`); } -export function buildPublishRole({ nerveRoot }: BuildPublishDeps): Role { +export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role { + const hermes = createHermesRole({ + prompt: async (threadId) => buildPublishPrompt({ threadId, nerveRoot }), + extract: { provider, schema: publishMetaSchema }, + }); + return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { - const dry = isDryRun(start); const file = logPath(nerveRoot); mkdirSync(join(file, ".."), { recursive: true }); - const cwd = resolveRepoCwd(messages); - const parsed = lastParseFromMessages(messages); - const repoInfo = lastRepoFromMessages(messages); - - if (dry) { + if (isDryRun(start)) { const msg = "[dry-run] publish skipped (no git push / PR)"; writeFileSync(file, `${msg}\n`, "utf-8"); return { @@ -37,102 +39,16 @@ export function buildPublishRole({ nerveRoot }: BuildPublishDeps): Role ${res.status}`); - lines.push(prBody.slice(0, 4000)); + return await hermes(start, messages); } catch (e) { - lines.push(`fetch error: ${e instanceof Error ? e.message : String(e)}`); - } - - writeFileSync(file, lines.join("\n"), "utf-8"); - - if (!prOk) { + const msg = e instanceof Error ? e.message : String(e); + const body = `publish failed: ${msg}\n`; + writeFileSync(file, body, "utf-8"); return { - content: `publish: push ok but PR API failed\nLog: ${file}`, + content: `publish failed: ${msg}\nLog: ${file}`, meta: { success: false }, }; } - - return { - content: `publish: pushed ${branch} and opened PR\nLog: ${file}`, - meta: { success: true }, - }; }; } diff --git a/workflows/solve-issue/roles/publish/prompt.ts b/workflows/solve-issue/roles/publish/prompt.ts new file mode 100644 index 0000000..cdff024 --- /dev/null +++ b/workflows/solve-issue/roles/publish/prompt.ts @@ -0,0 +1,42 @@ +export 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 + +- Read the full workflow thread: \`nerve thread show ${threadId}\` +- Nerve workspace conventions (for tone/consistency, optional): \`cat ${nerveRoot}/CONVENTIONS.md\` + +## Repo and issue (from the thread) + +Find \`---SOLVE_ISSUE_PARSE---\` and \`---SOLVE_ISSUE_REPO---\` in prior messages. You need: +- \`path\` — clone checkout directory (this is your working copy) +- \`host\`, \`owner\`, \`repo\`, \`number\` for the issue +- \`defaultBranch\` (for PR base) from SOLVE_ISSUE_REPO + +**Issue link** for the Ref section: \`https://///issues/\` + +## Steps (in order) + +1. \`cd\` to the **repo \`path\`**. Run \`git rev-parse --abbrev-ref HEAD\` to get the current branch name. +2. \`git push -u origin \` (must succeed before PR). +3. Write a **PR body** in Markdown with exactly these sections, in this order, each with a \`##\` heading (fill with concise content based on the thread: plan, implement, review, test): + - **## What** — one short paragraph: what this PR does + - **## Why** — one short paragraph: motivation / issue + - **## Changes** — bullet list of notable changes + - **## Ref** — the issue link above +4. Create the PR with **tea** (not curl/fetch to Gitea): + - \`tea pr create --repo / --base --head --title "fix: issue #" --body \` + - You may use a heredoc or a temp file for \`--body\` if the shell requires it; keep the four sections in the body. +5. Confirm the PR was created (tea prints a URL or PR number in typical setups). + +**success=true** only if both **push** and **tea** PR creation succeed. If any step fails, set **success=false** and say why. + +End your reply with a JSON line: +\`\`\`json +{ "success": true } +\`\`\` +or +\`\`\`json +{ "success": false } +\`\`\``; +}