Refactor committer into Hermes agent with branch/commit/push workflow

Add solve-issue committer after implement; replace develop-sense and develop-workflow script roles with createRole(hermesAdapter). Implement prompt no longer does git; publish prompt asks for meaningful PR titles.

Refs #9

Made-with: Cursor
This commit is contained in:
小橘 🍊(NEKO Team) 2026-04-29 10:44:28 +00:00 committed by 小橘
parent 3a2b8a49a3
commit c585e0d8a8
16 changed files with 325 additions and 211 deletions

View File

@ -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<SenseMeta> {
}: BuildDevelopSenseDeps): WorkflowDefinition<SenseMeta> {
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 {

View File

@ -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,
});

View File

@ -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<string, boolean>).approved;
if (s.role === "tester") return !(s.meta as Record<string, boolean>).passed;
if (s.role === "committer") return !(s.meta as Record<string, boolean>).success;
if (s.role === "committer") return !(s.meta as Record<string, boolean>).committed;
return false;
}).length;
}
@ -57,7 +57,7 @@ export const moderator: Moderator<SenseMeta> = (context) => {
}
if (last.role === "committer") {
if (last.meta.success) return END;
if (last.meta.committed) return END;
return canRetryCoder(context.steps) ? "coder" : END;
}

View File

@ -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 = {
nerveRoot: string;
};
function logPath(nerveRoot: string): string {
return join(nerveRoot, "logs", `committer-${Date.now()}.log`);
}
function writeLog(path: string, content: string): void {
mkdirSync(join(path, ".."), { recursive: true });
writeFileSync(path, content, "utf-8");
}
export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<CommitterMeta> {
return async (start, _messages) => {
const dry = isDryRun(start);
const file = logPath(nerveRoot);
if (dry) {
writeLog(file, "[dry-run] committer skipped\n");
return {
content: `[dry-run] committer skipped — log: ${file}`,
meta: { success: true },
} satisfies RoleResult<CommitterMeta>;
}
const lines: string[] = [];
let success = true;
const run = async (cmd: string, args: string[]): Promise<boolean> => {
const r = await spawnSafe(cmd, args, {
cwd: nerveRoot,
env: null,
timeoutMs: 60_000,
dryRun: false,
abortSignal: null,
export const committerMetaSchema = z.object({
committed: z
.boolean()
.describe("true if branch created, changes committed, and pushed successfully"),
});
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;
export type CommitterMeta = z.infer<typeof committerMetaSchema>;
export type BuildWorkspaceCommitterDeps = {
extract: LlmExtractorConfig;
nerveRoot: string;
workflowName: string;
};
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;
}
export function buildWorkspaceCommitterRole({
extract,
nerveRoot,
workflowName,
}: BuildWorkspaceCommitterDeps): Role<CommitterMeta> {
const innerRole = createRole(
hermesAdapter,
async (start: StartStep) =>
workspaceCommitterPrompt({
threadId: start.meta.threadId,
nerveRoot,
workflowName,
}),
committerMetaSchema,
extract,
);
const log = lines.join("\n");
writeLog(file, log);
const summary = success ? "committed and pushed" : "commit/push failed — see log";
return async (start, _messages): Promise<RoleResult<CommitterMeta>> => {
if (isDryRun(start)) {
return {
content: `committer: ${summary}\nLog: ${file}`,
meta: { success },
} satisfies RoleResult<CommitterMeta>;
content: "[dry-run] committer skipped (no git branch/commit/push)",
meta: { committed: true },
};
}
const innerStart = {
...start,
meta: { ...start.meta, workdir: nerveRoot },
} 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 },
};
}
};
}

View File

@ -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/<short-slug>\` or \`feat/<short-slug>\` 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 "<message>"\` (use multiple \`-m\` if you need a body).
6. \`git push -u origin <branch-name>\`
**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 }
\`\`\``;
}

View File

@ -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<WorkflowMeta> {
}: BuildDevelopWorkflowDeps): WorkflowDefinition<WorkflowMeta> {
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 {

View File

@ -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,
});

View File

@ -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<string, boolean>).approved;
if (s.role === "tester") return !(s.meta as Record<string, boolean>).passed;
if (s.role === "committer") return !(s.meta as Record<string, boolean>).success;
if (s.role === "committer") return !(s.meta as Record<string, boolean>).committed;
return false;
}).length;
}
@ -59,7 +59,7 @@ export const moderator: Moderator<WorkflowMeta> = (context) => {
}
if (last.role === "committer") {
if (last.meta.success) return END;
if (last.meta.committed) return END;
return canRetryCoder(context.steps) ? "coder" : END;
}

View File

@ -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 = {
nerveRoot: string;
};
function logPath(nerveRoot: string): string {
return join(nerveRoot, "logs", `committer-${Date.now()}.log`);
}
function writeLog(path: string, content: string): void {
mkdirSync(join(path, ".."), { recursive: true });
writeFileSync(path, content, "utf-8");
}
export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<CommitterMeta> {
return async (start, _messages) => {
const dry = isDryRun(start);
const file = logPath(nerveRoot);
if (dry) {
writeLog(file, "[dry-run] committer skipped\n");
return {
content: `[dry-run] committer skipped — log: ${file}`,
meta: { success: true },
} satisfies RoleResult<CommitterMeta>;
}
const lines: string[] = [];
let success = true;
const run = async (cmd: string, args: string[]): Promise<boolean> => {
const r = await spawnSafe(cmd, args, {
cwd: nerveRoot,
env: null,
timeoutMs: 60_000,
dryRun: false,
abortSignal: null,
export const committerMetaSchema = z.object({
committed: z
.boolean()
.describe("true if branch created, changes committed, and pushed successfully"),
});
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;
export type CommitterMeta = z.infer<typeof committerMetaSchema>;
export type BuildWorkspaceCommitterDeps = {
extract: LlmExtractorConfig;
nerveRoot: string;
workflowName: string;
};
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;
}
export function buildWorkspaceCommitterRole({
extract,
nerveRoot,
workflowName,
}: BuildWorkspaceCommitterDeps): Role<CommitterMeta> {
const innerRole = createRole(
hermesAdapter,
async (start: StartStep) =>
workspaceCommitterPrompt({
threadId: start.meta.threadId,
nerveRoot,
workflowName,
}),
committerMetaSchema,
extract,
);
const log = lines.join("\n");
writeLog(file, log);
const summary = success ? "committed and pushed" : "commit/push failed — see log";
return async (start, _messages): Promise<RoleResult<CommitterMeta>> => {
if (isDryRun(start)) {
return {
content: `committer: ${summary}\nLog: ${file}`,
meta: { success },
} satisfies RoleResult<CommitterMeta>;
content: "[dry-run] committer skipped (no git branch/commit/push)",
meta: { committed: true },
};
}
const innerStart = {
...start,
meta: { ...start.meta, workdir: nerveRoot },
} 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 },
};
}
};
}

View File

@ -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=<branch-or-empty>
COMMIT=<hash-or-empty>
PUSHED=<true|false|unknown>
LOG_START
<details>
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/<short-slug>\` or \`feat/<short-slug>\` 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 "<message>"\` (use multiple \`-m\` if you need a body).
6. \`git push -u origin <branch-name>\`
**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 }
\`\`\``;
}

View File

@ -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) =>

View File

@ -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<string, boolean>).approved;
if (s.role === "test") return !(s.meta as Record<string, boolean>).passed;
if (s.role === "committer") return !(s.meta as Record<string, boolean>).committed;
if (s.role === "publish") return !(s.meta as Record<string, boolean>).success;
return false;
}).length;
@ -59,6 +62,13 @@ export const moderator: Moderator<WorkflowMeta> = (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;

View File

@ -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<typeof committerMetaSchema>;
export type BuildCommitterDeps = {
extract: LlmExtractorConfig;
nerveRoot: string;
};
export function buildCommitterRole({ extract, nerveRoot }: BuildCommitterDeps): Role<CommitterMeta> {
const innerRole = createRole(
hermesAdapter,
async (start: StartStep) =>
committerPrompt({
threadId: start.meta.threadId,
nerveRoot,
}),
committerMetaSchema,
extract,
);
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<CommitterMeta>> => {
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 },
};
}
};
}

View File

@ -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/<number>-<short-slug>\` for fixes, or \`feat/<number>-<short-slug>\` 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 "<message>"\` — use a single \`-m\` for a one-line message, or multiple \`-m\` for body paragraphs if needed.
7. \`git push -u origin <branch-name>\`
**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 }
\`\`\``;
}

View File

@ -9,10 +9,11 @@ Your cwd is the target repository.
## Requirements
1. Create a branch: \`fix/issue-<number>-<short-slug>\` (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

View File

@ -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 <that-branch>\` (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 <that-branch>\` 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 <owner>/<repo> --base <defaultBranch> --head <branch> --title "fix: issue #<number>" --body <your markdown body>\`
- \`tea pr create --repo <owner>/<repo> --base <defaultBranch> --head <branch> --title "<your meaningful title>" --body <your markdown 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).