diff --git a/workflows/develop-sense/build.ts b/workflows/develop-sense/build.ts index 174c58b..29837b3 100644 --- a/workflows/develop-sense/build.ts +++ b/workflows/develop-sense/build.ts @@ -12,21 +12,21 @@ 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 { buildWorkspaceCommitterRole } from "./roles/committer/index.js"; import { moderator } from "./moderator.js"; import type { SenseMeta } from "./moderator.js"; -export type BuildSenseGeneratorDeps = { +export type BuildDevelopSenseDeps = { extract: LlmExtractorConfig; cwd: string; }; const CURSOR_TIMEOUT_MS = 300_000; -export function buildSenseGenerator({ +export function buildDevelopSenseWorkflow({ extract, cwd, -}: BuildSenseGeneratorDeps): WorkflowDefinition { +}: BuildDevelopSenseDeps): WorkflowDefinition { const roles = { planner: createRole( createCursorAdapter({ @@ -59,7 +59,11 @@ export function buildSenseGenerator({ testerMetaSchema, extract, ), - committer: buildCommitterRole({ nerveRoot: cwd }), + committer: buildWorkspaceCommitterRole({ + extract, + nerveRoot: cwd, + workflowName: "develop-sense", + }), }; return { diff --git a/workflows/develop-sense/index.ts b/workflows/develop-sense/index.ts index 47363b6..ee9d343 100644 --- a/workflows/develop-sense/index.ts +++ b/workflows/develop-sense/index.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import { buildSenseGenerator } from "./build.js"; +import { buildDevelopSenseWorkflow } from "./build.js"; const HOME = process.env.HOME ?? "/home/azureuser"; const NERVE_ROOT = join(HOME, ".uncaged-nerve"); @@ -11,7 +11,7 @@ if (!apiKey || !baseUrl) { throw new Error("Set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL"); } -const workflow = buildSenseGenerator({ +const workflow = buildDevelopSenseWorkflow({ extract: { provider: { apiKey, baseUrl, model } }, cwd: NERVE_ROOT, }); diff --git a/workflows/develop-sense/moderator.ts b/workflows/develop-sense/moderator.ts index f3a6caa..87fea65 100644 --- a/workflows/develop-sense/moderator.ts +++ b/workflows/develop-sense/moderator.ts @@ -25,7 +25,7 @@ function totalRejections(steps: { role: string; meta: unknown }[]): number { return steps.filter((s) => { if (s.role === "reviewer") return !(s.meta as Record).approved; if (s.role === "tester") return !(s.meta as Record).passed; - if (s.role === "committer") return !(s.meta as Record).success; + if (s.role === "committer") return !(s.meta as Record).committed; return false; }).length; } @@ -57,7 +57,7 @@ export const moderator: Moderator = (context) => { } if (last.role === "committer") { - if (last.meta.success) return END; + if (last.meta.committed) return END; return canRetryCoder(context.steps) ? "coder" : END; } diff --git a/workflows/develop-sense/roles/committer/index.ts b/workflows/develop-sense/roles/committer/index.ts index 799b277..e51b119 100644 --- a/workflows/develop-sense/roles/committer/index.ts +++ b/workflows/develop-sense/roles/committer/index.ts @@ -1,91 +1,62 @@ -import { writeFileSync, mkdirSync } from "node:fs"; -import { join } from "node:path"; -import type { Role, RoleResult } from "@uncaged/nerve-core"; -import { isDryRun, spawnSafe } from "@uncaged/nerve-workflow-utils"; +import type { Role, RoleResult, StartStep } from "@uncaged/nerve-core"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; -export type CommitterMeta = { - success: boolean; -}; +import { workspaceCommitterPrompt } from "./prompt.js"; -export type BuildCommitterDeps = { +export const committerMetaSchema = z.object({ + committed: z + .boolean() + .describe("true if branch created, changes committed, and pushed successfully"), +}); +export type CommitterMeta = z.infer; + +export type BuildWorkspaceCommitterDeps = { + extract: LlmExtractorConfig; nerveRoot: string; + workflowName: string; }; -function logPath(nerveRoot: string): string { - return join(nerveRoot, "logs", `committer-${Date.now()}.log`); -} +export function buildWorkspaceCommitterRole({ + extract, + nerveRoot, + workflowName, +}: BuildWorkspaceCommitterDeps): Role { + const innerRole = createRole( + hermesAdapter, + async (start: StartStep) => + workspaceCommitterPrompt({ + threadId: start.meta.threadId, + nerveRoot, + workflowName, + }), + committerMetaSchema, + extract, + ); -function writeLog(path: string, content: string): void { - mkdirSync(join(path, ".."), { recursive: true }); - writeFileSync(path, content, "utf-8"); -} - -export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role { - return async (start, _messages) => { - const dry = isDryRun(start); - const file = logPath(nerveRoot); - - if (dry) { - writeLog(file, "[dry-run] committer skipped\n"); + return async (start, _messages): Promise> => { + if (isDryRun(start)) { return { - content: `[dry-run] committer skipped — log: ${file}`, - meta: { success: true }, - } satisfies RoleResult; + content: "[dry-run] committer skipped (no git branch/commit/push)", + meta: { committed: true }, + }; } - const lines: string[] = []; - let success = true; + const innerStart = { + ...start, + meta: { ...start.meta, workdir: nerveRoot }, + } as StartStep; - const run = async (cmd: string, args: string[]): Promise => { - 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); - if (r.value.stderr) lines.push(r.value.stderr); - lines.push(""); - return true; - } - const e = r.error; - lines.push(`$ ${cmd} ${args.join(" ")} — FAILED`); - if (e.kind === "non_zero_exit") { - lines.push(`exit ${e.exitCode}`); - if (e.stdout) lines.push(e.stdout); - if (e.stderr) lines.push(e.stderr); - } else if (e.kind === "timeout") { - lines.push("timeout"); - if (e.stdout) lines.push(e.stdout); - if (e.stderr) lines.push(e.stderr); - } else if (e.kind === "spawn_failed") { - lines.push(e.message); - } else { - lines.push(e.kind === "aborted" ? "aborted" : "error"); - } - lines.push(""); - return false; - }; - - await run("git", ["add", "-A"]); - const committed = await run("git", ["commit", "-m", "chore(sense): auto-generated commit"]); - if (!committed) { - success = false; - } else { - const pushed = await run("git", ["push"]); - if (!pushed) success = false; + try { + return await innerRole(innerStart, _messages); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { + content: `committer failed: ${msg}`, + meta: { committed: false }, + }; } - - const log = lines.join("\n"); - writeLog(file, log); - - const summary = success ? "committed and pushed" : "commit/push failed — see log"; - return { - content: `committer: ${summary}\nLog: ${file}`, - meta: { success }, - } satisfies RoleResult; }; } diff --git a/workflows/develop-sense/roles/committer/prompt.ts b/workflows/develop-sense/roles/committer/prompt.ts new file mode 100644 index 0000000..1eb77de --- /dev/null +++ b/workflows/develop-sense/roles/committer/prompt.ts @@ -0,0 +1,38 @@ +export function workspaceCommitterPrompt({ + threadId, + nerveRoot, + workflowName, +}: { + threadId: string; + nerveRoot: string; + workflowName: string; +}): string { + return `You are the **committer** agent (Hermes) for the **${workflowName}** workflow. The coder finished with a passing build; you branch, commit, and push workspace changes. + +## Context + +1. Read the workflow thread: \`nerve thread show ${threadId}\` +2. Your git repository root is: \`${nerveRoot}\` — \`cd\` there for all git commands. + +## Steps (in order) + +1. Run \`git status\`. There should be uncommitted changes from the coder. If there is nothing to commit, set **committed** to false and explain. +2. Create a short-lived branch (do not commit directly on the default branch if it would mix unrelated work): + - Prefer \`fix/\` or \`feat/\` with a lowercase hyphenated slug from the thread (planner/coder context). + - Example: \`git checkout -b fix/sense-export-path\` +3. \`git add -A\` +4. Write a **conventional commit** message summarizing what changed and why (scope may be \`sense\` or similar). +5. \`git commit -m ""\` (use multiple \`-m\` if you need a body). +6. \`git push -u origin \` + +**committed=true** only if branch was created, commit succeeded, and **push** succeeded. + +End your reply with a JSON line: +\`\`\`json +{ "committed": true } +\`\`\` +or +\`\`\`json +{ "committed": false } +\`\`\``; +} diff --git a/workflows/develop-workflow/build.ts b/workflows/develop-workflow/build.ts index dbc5d6a..4d065ef 100644 --- a/workflows/develop-workflow/build.ts +++ b/workflows/develop-workflow/build.ts @@ -12,21 +12,21 @@ 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 { buildWorkspaceCommitterRole } from "./roles/committer/index.js"; import { moderator } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js"; -export type BuildWorkflowGeneratorDeps = { +export type BuildDevelopWorkflowDeps = { extract: LlmExtractorConfig; nerveRoot: string; }; const CURSOR_TIMEOUT_MS = 300_000; -export function buildWorkflowGenerator({ +export function buildDevelopWorkflow({ extract, nerveRoot, -}: BuildWorkflowGeneratorDeps): WorkflowDefinition { +}: BuildDevelopWorkflowDeps): WorkflowDefinition { const roles = { planner: createRole( createCursorAdapter({ @@ -59,7 +59,11 @@ export function buildWorkflowGenerator({ testerMetaSchema, extract, ), - committer: buildCommitterRole({ nerveRoot }), + committer: buildWorkspaceCommitterRole({ + extract, + nerveRoot, + workflowName: "develop-workflow", + }), }; return { diff --git a/workflows/develop-workflow/index.ts b/workflows/develop-workflow/index.ts index 7e9c0de..d9c2cc4 100644 --- a/workflows/develop-workflow/index.ts +++ b/workflows/develop-workflow/index.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import { buildWorkflowGenerator } from "./build.js"; +import { buildDevelopWorkflow } from "./build.js"; const HOME = process.env.HOME ?? "/home/azureuser"; const NERVE_ROOT = join(HOME, ".uncaged-nerve"); @@ -12,7 +12,7 @@ if (!apiKey || !baseUrl) { throw new Error("Set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL"); } -const workflow = buildWorkflowGenerator({ +const workflow = buildDevelopWorkflow({ extract: { provider: { apiKey, baseUrl, model } }, nerveRoot: NERVE_ROOT, }); diff --git a/workflows/develop-workflow/moderator.ts b/workflows/develop-workflow/moderator.ts index dd5aa27..156f1fa 100644 --- a/workflows/develop-workflow/moderator.ts +++ b/workflows/develop-workflow/moderator.ts @@ -25,7 +25,7 @@ function totalRejections(steps: { role: string; meta: unknown }[]): number { return steps.filter((s) => { if (s.role === "reviewer") return !(s.meta as Record).approved; if (s.role === "tester") return !(s.meta as Record).passed; - if (s.role === "committer") return !(s.meta as Record).success; + if (s.role === "committer") return !(s.meta as Record).committed; return false; }).length; } @@ -59,7 +59,7 @@ export const moderator: Moderator = (context) => { } if (last.role === "committer") { - if (last.meta.success) return END; + if (last.meta.committed) return END; return canRetryCoder(context.steps) ? "coder" : END; } diff --git a/workflows/develop-workflow/roles/committer/index.ts b/workflows/develop-workflow/roles/committer/index.ts index adb10cf..e51b119 100644 --- a/workflows/develop-workflow/roles/committer/index.ts +++ b/workflows/develop-workflow/roles/committer/index.ts @@ -1,92 +1,62 @@ -import { writeFileSync, mkdirSync } from "node:fs"; -import { join } from "node:path"; -import type { Role, RoleResult } from "@uncaged/nerve-core"; -import { isDryRun, spawnSafe } from "@uncaged/nerve-workflow-utils"; +import type { Role, RoleResult, StartStep } from "@uncaged/nerve-core"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; -export type CommitterMeta = { - success: boolean; -}; +import { workspaceCommitterPrompt } from "./prompt.js"; -export type BuildCommitterDeps = { +export const committerMetaSchema = z.object({ + committed: z + .boolean() + .describe("true if branch created, changes committed, and pushed successfully"), +}); +export type CommitterMeta = z.infer; + +export type BuildWorkspaceCommitterDeps = { + extract: LlmExtractorConfig; nerveRoot: string; + workflowName: string; }; -function logPath(nerveRoot: string): string { - return join(nerveRoot, "logs", `committer-${Date.now()}.log`); -} +export function buildWorkspaceCommitterRole({ + extract, + nerveRoot, + workflowName, +}: BuildWorkspaceCommitterDeps): Role { + const innerRole = createRole( + hermesAdapter, + async (start: StartStep) => + workspaceCommitterPrompt({ + threadId: start.meta.threadId, + nerveRoot, + workflowName, + }), + committerMetaSchema, + extract, + ); -function writeLog(path: string, content: string): void { - mkdirSync(join(path, ".."), { recursive: true }); - writeFileSync(path, content, "utf-8"); -} - -export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role { - return async (start, _messages) => { - const dry = isDryRun(start); - const file = logPath(nerveRoot); - - if (dry) { - writeLog(file, "[dry-run] committer skipped\n"); + return async (start, _messages): Promise> => { + if (isDryRun(start)) { return { - content: `[dry-run] committer skipped — log: ${file}`, - meta: { success: true }, - } satisfies RoleResult; + content: "[dry-run] committer skipped (no git branch/commit/push)", + meta: { committed: true }, + }; } - const lines: string[] = []; - let success = true; + const innerStart = { + ...start, + meta: { ...start.meta, workdir: nerveRoot }, + } as StartStep; - const run = async (cmd: string, args: string[]): Promise => { - 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); - if (r.value.stderr) lines.push(r.value.stderr); - lines.push(""); - return true; - } - const e = r.error; - lines.push(`$ ${cmd} ${args.join(" ")} — FAILED`); - if (e.kind === "non_zero_exit") { - lines.push(`exit ${e.exitCode}`); - if (e.stdout) lines.push(e.stdout); - if (e.stderr) lines.push(e.stderr); - } else if (e.kind === "timeout") { - lines.push("timeout"); - if (e.stdout) lines.push(e.stdout); - if (e.stderr) lines.push(e.stderr); - } else if (e.kind === "spawn_failed") { - lines.push(e.message); - } else { - lines.push(e.kind === "aborted" ? "aborted" : "error"); - } - lines.push(""); - return false; - }; - - await run("git", ["add", "-A"]); - // Use a generic message; git diff will show what actually changed - const committed = await run("git", ["commit", "-m", "chore(workflow): auto-generated commit"]); - if (!committed) { - success = false; - } else { - const pushed = await run("git", ["push"]); - if (!pushed) success = false; + try { + return await innerRole(innerStart, _messages); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { + content: `committer failed: ${msg}`, + meta: { committed: false }, + }; } - - const log = lines.join("\n"); - writeLog(file, log); - - const summary = success ? "committed and pushed" : "commit/push failed — see log"; - return { - content: `committer: ${summary}\nLog: ${file}`, - meta: { success }, - } satisfies RoleResult; }; } diff --git a/workflows/develop-workflow/roles/committer/prompt.ts b/workflows/develop-workflow/roles/committer/prompt.ts index fad7b8b..7c607c1 100644 --- a/workflows/develop-workflow/roles/committer/prompt.ts +++ b/workflows/develop-workflow/roles/committer/prompt.ts @@ -1,35 +1,38 @@ -export type CommitterPromptParams = { - nerveRoot: string; - workflowName: string; - userPrompt: string; - testerReason: string; -}; - -export function committerPrompt({ +export function workspaceCommitterPrompt({ + threadId, nerveRoot, workflowName, - userPrompt, - testerReason, -}: CommitterPromptParams): string { - return `You are a git committer subagent for Nerve workflow generation. -Repository root: ${nerveRoot} +}: { + threadId: string; + nerveRoot: string; + workflowName: string; +}): string { + return `You are the **committer** agent (Hermes) for the **${workflowName}** workflow. The coder finished with a passing build; you branch, commit, and push workspace changes. -Goal: -- Commit and push generated workflow "${workflowName}". -- Handle dirty worktree safely (do not discard unrelated user edits). -- Detect default branch automatically. -- Create a focused branch for this workflow update. -- Stage only workflow files and required config updates. +## Context -Context: -- User prompt summary: ${userPrompt.slice(0, 500)} -- Tester result: ${testerReason} +1. Read the workflow thread: \`nerve thread show ${threadId}\` +2. Your git repository root is: \`${nerveRoot}\` — \`cd\` there for all git commands. -Expected output format: -BRANCH= -COMMIT= -PUSHED= -LOG_START -
-LOG_END`; +## Steps (in order) + +1. Run \`git status\`. There should be uncommitted changes from the coder. If there is nothing to commit, set **committed** to false and explain. +2. Create a short-lived branch (do not commit directly on the default branch if it would mix unrelated work): + - Prefer \`fix/\` or \`feat/\` with a lowercase hyphenated slug from the thread (planner/coder context). + - Example: \`git checkout -b feat/workflow-new-step\` +3. \`git add -A\` +4. Write a **conventional commit** message summarizing what changed and why (scope may be \`workflow\` or similar). +5. \`git commit -m ""\` (use multiple \`-m\` if you need a body). +6. \`git push -u origin \` + +**committed=true** only if branch was created, commit succeeded, and **push** succeeded. + +End your reply with a JSON line: +\`\`\`json +{ "committed": true } +\`\`\` +or +\`\`\`json +{ "committed": false } +\`\`\``; } diff --git a/workflows/solve-issue/build.ts b/workflows/solve-issue/build.ts index 0c67ec3..2fa7b04 100644 --- a/workflows/solve-issue/build.ts +++ b/workflows/solve-issue/build.ts @@ -5,6 +5,7 @@ import { createRole } from "@uncaged/nerve-workflow-utils"; import { moderator } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js"; +import { buildCommitterRole } from "./roles/committer/index.js"; import { buildImplementRole } from "./roles/implement/index.js"; import { buildPlanRole } from "./roles/plan/index.js"; import { prepareMetaSchema } from "./roles/prepare/index.js"; @@ -43,6 +44,7 @@ export function buildSolveIssue({ ), plan: buildPlanRole({ extract, nerveRoot }), implement: buildImplementRole({ extract, nerveRoot }), + committer: buildCommitterRole({ extract, nerveRoot }), review: createRole( hermesAdapter, async (start: StartStep) => diff --git a/workflows/solve-issue/moderator.ts b/workflows/solve-issue/moderator.ts index 02714f3..a79cf65 100644 --- a/workflows/solve-issue/moderator.ts +++ b/workflows/solve-issue/moderator.ts @@ -4,6 +4,7 @@ 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"; @@ -13,6 +14,7 @@ export type WorkflowMeta = { prepare: PrepareMeta; plan: PlanMeta; implement: ImplementMeta; + committer: CommitterMeta; review: ReviewMeta; test: TestMeta; publish: PublishMeta; @@ -29,6 +31,7 @@ function totalRejections(steps: { role: string; meta: unknown }[]): number { return steps.filter((s) => { if (s.role === "review") return !(s.meta as Record).approved; if (s.role === "test") return !(s.meta as Record).passed; + if (s.role === "committer") return !(s.meta as Record).committed; if (s.role === "publish") return !(s.meta as Record).success; return false; }).length; @@ -59,6 +62,13 @@ export const moderator: Moderator = (context) => { if (last.role === "implement") { if (last.meta.done) { + return "committer"; + } + return canRetryImplement(context.steps) ? "implement" : END; + } + + if (last.role === "committer") { + if (last.meta.committed) { return "review"; } return canRetryImplement(context.steps) ? "implement" : END; diff --git a/workflows/solve-issue/roles/committer/index.ts b/workflows/solve-issue/roles/committer/index.ts new file mode 100644 index 0000000..fcd95e3 --- /dev/null +++ b/workflows/solve-issue/roles/committer/index.ts @@ -0,0 +1,65 @@ +import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +import { resolveRepoCwd } from "../../lib/repo-context.js"; +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 type BuildCommitterDeps = { + extract: LlmExtractorConfig; + nerveRoot: string; +}; + +export function buildCommitterRole({ extract, nerveRoot }: BuildCommitterDeps): Role { + const innerRole = createRole( + hermesAdapter, + async (start: StartStep) => + committerPrompt({ + threadId: start.meta.threadId, + nerveRoot, + }), + committerMetaSchema, + extract, + ); + + return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { + const cwd = resolveRepoCwd(messages); + if (cwd === null) { + return { + content: "committer cannot run: missing repo path in thread markers", + meta: { committed: false }, + }; + } + + if (isDryRun(start)) { + return { + content: "[dry-run] committer skipped (no git branch/commit/push)", + meta: { committed: true }, + }; + } + + const innerStart = { + ...start, + meta: { ...start.meta, workdir: cwd }, + } as StartStep; + + try { + return await innerRole(innerStart, messages); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { + content: `committer failed: ${msg}`, + meta: { committed: false }, + }; + } + }; +} diff --git a/workflows/solve-issue/roles/committer/prompt.ts b/workflows/solve-issue/roles/committer/prompt.ts new file mode 100644 index 0000000..dd703ee --- /dev/null +++ b/workflows/solve-issue/roles/committer/prompt.ts @@ -0,0 +1,46 @@ +export function committerPrompt({ + threadId, + nerveRoot, +}: { + threadId: string; + nerveRoot: string; +}): string { + return `You are the **committer** agent (Hermes). The **implement** step finished with a passing build; your job is to put those changes on a feature branch and push to **origin**. + +## Context + +- Read the full workflow thread: \`nerve thread show ${threadId}\` +- Optional workspace tone: \`cat ${nerveRoot}/CONVENTIONS.md\` + +## Find repo and issue markers + +In the thread, locate \`---SOLVE_ISSUE_PARSE---\` and \`---SOLVE_ISSUE_REPO---\`. From them you need: + +- Issue **number** and **title** (from PARSE; title for the branch slug) +- Repo checkout **path** (from REPO \`path\`) — this is your working copy; your shell cwd should be this directory. + +## Steps (in order) + +1. \`cd\` to the repo **path** from SOLVE_ISSUE_REPO. +2. Run \`git status\`. There should be **uncommitted** changes from implement. If the tree is clean with nothing to commit, set **committed** to false and explain. +3. Create a feature branch (do not work on the default branch directly): + - Name: \`fix/-\` for fixes, or \`feat/-\` if the issue is clearly a feature. + - **number** from SOLVE_ISSUE_PARSE. + - **slug**: lowercase, hyphens only, short (from issue title words). + - Example: \`git checkout -b fix/42-auth-timeout\` +4. \`git add -A\` +5. Write a **conventional commit** message (e.g. \`fix(scope): summary\` or \`feat(scope): summary\`) describing **what** changed and **why**, using the thread (plan + implement context). +6. \`git commit -m ""\` — use a single \`-m\` for a one-line message, or multiple \`-m\` for body paragraphs if needed. +7. \`git push -u origin \` + +**committed=true** only if you created the branch, committed successfully, and **push** succeeded. + +End your reply with a JSON line: +\`\`\`json +{ "committed": true } +\`\`\` +or +\`\`\`json +{ "committed": false } +\`\`\``; +} diff --git a/workflows/solve-issue/roles/implement/prompt.ts b/workflows/solve-issue/roles/implement/prompt.ts index 5e1dc77..8fee642 100644 --- a/workflows/solve-issue/roles/implement/prompt.ts +++ b/workflows/solve-issue/roles/implement/prompt.ts @@ -9,10 +9,11 @@ Your cwd is the target repository. ## Requirements -1. Create a branch: \`fix/issue--\` (use \`feat/\` if the issue is clearly a feature). Use a slug from the issue title (lowercase, hyphens). -2. Implement the planned changes; address reviewer/tester feedback from the thread if any. -3. Run the project **build** (\`pnpm build\`, \`npm run build\`, etc.) and fix issues until build passes. -4. Multi-step: if you cannot finish this round, explain why and set **done** to false. +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\`. Branching and commits are handled by the **committer** step after you finish. Then close with JSON: \`\`\`json diff --git a/workflows/solve-issue/roles/publish/prompt.ts b/workflows/solve-issue/roles/publish/prompt.ts index cdff024..df353f2 100644 --- a/workflows/solve-issue/roles/publish/prompt.ts +++ b/workflows/solve-issue/roles/publish/prompt.ts @@ -17,15 +17,15 @@ Find \`---SOLVE_ISSUE_PARSE---\` and \`---SOLVE_ISSUE_REPO---\` in prior message ## 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). +1. \`cd\` to the **repo \`path\`**. Run \`git rev-parse --abbrev-ref HEAD\` to get the current branch name. The **committer** step should already have pushed this branch; run \`git push -u origin \` only if the branch is not yet on the remote. +2. Choose a **PR title** that reflects the real change (not a generic \`fix: issue #N\`): derive it from the issue title, plan, and thread summary (keep it concise; Conventional Commits style is fine, e.g. \`fix(auth): handle session expiry\`). 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 \` + - \`tea pr create --repo / --base --head --title "" --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).