import type { ModeratorContext, RoleResult, StartStep, WorkflowDefinition, WorkflowMessage, } from "@uncaged/nerve-core"; import { END } from "@uncaged/nerve-core"; import type { SpawnError } from "@uncaged/nerve-workflow-utils"; import { cursorAgent, llmExtract, nerveAgentContext, readNerveYaml, spawnSafe } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; const NERVE_ROOT = "/home/azureuser/.uncaged-nerve"; const ISSUE_READER_MAX_ATTEMPTS = 3; const TESTER_MAX_ATTEMPTS = 3; const PUBLISHER_MAX_ATTEMPTS = 3; type IssueData = { id: number; number: number; title: string; body: string; state: string; labels: string[]; comments: Array<{ author: string; body: string; createdAt: string; }>; url: string; }; type TestCommandResult = { command: string; ok: boolean; stdoutPreview: string; stderrPreview: string; }; type WorkflowMeta = { intake: { issueUrl: string; host: string | null; owner: string | null; repo: string | null; issueNumber: number | null; valid: boolean; failureKind: "none" | "invalid_input"; failureReason: string | null; }; "issue-reader": { issue: IssueData | null; fetchOk: boolean; transientError: boolean; attempt: number; failureKind: "none" | "transient" | "permanent"; failureReason: string | null; }; planner: { plan: string | null; targetFiles: string[] | null; testCommands: string[] | null; riskNotes: string[] | null; planningOk: boolean; failureKind: "none" | "planning_failed"; failureReason: string | null; }; implementer: { branchName: string | null; changedFiles: string[] | null; implementationOk: boolean; attempt: number; failureKind: "none" | "branch_failed" | "agent_failed" | "no_diff"; failureReason: string | null; implementationLog: string | null; }; tester: { passed: boolean; attempt: number; failureReason: string | null; testCommandResults: TestCommandResult[] | null; }; "pr-publisher": { prUrl: string | null; prNumber: number | null; linkedIssue: string | null; published: boolean; attempt: number; failureKind: "none" | "auth_or_network" | "permanent"; failureReason: string | null; }; }; type Provider = { baseUrl: string; apiKey: string; model: string; }; const issueSchema = z.object({ id: z.number().int().default(0), number: z.number().int().default(0), title: z.string().default(""), body: z.string().default(""), state: z.string().default("unknown"), labels: z.array(z.string().default("")).default([]), comments: z .array( z.object({ author: z.string().default("unknown"), body: z.string().default(""), createdAt: z.string().default(""), }), ) .default([]), url: z.string().default(""), }); const planSchema = z.object({ targetFiles: z.array(z.string().default("")).default([]), testCommands: z.array(z.string().default("")).default([]), riskNotes: z.array(z.string().default("")).default([]), }); function lastMetaForRole(messages: WorkflowMessage[], role: string): M | null { for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === role) { return messages[i].meta as M; } } return null; } function formatSpawnFailure(error: SpawnError): string { if (error.kind === "spawn_failed") { return `spawn_failed: ${error.message}`; } if (error.kind === "timeout") { return `timeout: stdout=${error.stdout.slice(0, 200)} stderr=${error.stderr.slice(0, 200)}`; } return `non_zero_exit(${error.exitCode}): stderr=${error.stderr.slice(0, 400)}`; } function errorText(error: SpawnError): string { if (error.kind === "spawn_failed") { return error.message.toLowerCase(); } if (error.kind === "timeout") { return `${error.stdout} ${error.stderr}`.toLowerCase(); } return `${error.stdout} ${error.stderr}`.toLowerCase(); } function classifyIssueReaderFailure(error: SpawnError): { transientError: boolean; failureKind: "transient" | "permanent" } { if (error.kind === "timeout" || error.kind === "spawn_failed") { return { transientError: true, failureKind: "transient" }; } const text = errorText(error); if ( text.includes("network") || text.includes("timed out") || text.includes("timeout") || text.includes("connection reset") || text.includes("connection refused") || text.includes("429") || text.includes("500") || text.includes("502") || text.includes("503") || text.includes("504") ) { return { transientError: true, failureKind: "transient" }; } return { transientError: false, failureKind: "permanent" }; } function classifyPublisherFailure(error: SpawnError): "auth_or_network" | "permanent" { if (error.kind === "timeout" || error.kind === "spawn_failed") { return "auth_or_network"; } const text = errorText(error); if ( text.includes("401") || text.includes("403") || text.includes("auth") || text.includes("token") || text.includes("permission") || text.includes("network") || text.includes("connection") || text.includes("timeout") ) { return "auth_or_network"; } return "permanent"; } function parseIssueUrl(raw: string): WorkflowMeta["intake"] { const issueUrl = raw.trim(); const match = issueUrl.match(/^https?:\/\/([^/\s]+)\/([^/\s]+)\/([^/\s]+)\/issues\/(\d+)(?:[/?#].*)?$/i); if (match === null) { return { issueUrl, host: null, owner: null, repo: null, issueNumber: null, valid: false, failureKind: "invalid_input", failureReason: `invalid issue URL: ${issueUrl}`, }; } const issueNumber = Number(match[4]); if (!Number.isInteger(issueNumber) || issueNumber <= 0) { return { issueUrl, host: null, owner: null, repo: null, issueNumber: null, valid: false, failureKind: "invalid_input", failureReason: `invalid issue number: ${match[4]}`, }; } return { issueUrl, host: match[1], owner: match[2], repo: match[3], issueNumber, valid: true, failureKind: "none", failureReason: null, }; } function slugify(input: string): string { const normalized = input .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return normalized.length > 0 ? normalized.slice(0, 40) : "update"; } function readNerveConfigText(): string { const result = readNerveYaml({ nerveRoot: NERVE_ROOT }); return result.ok ? result.value : "# nerve.yaml unavailable"; } async function cfgGet(key: string): Promise { const result = await spawnSafe("cfg", ["get", key], { cwd: NERVE_ROOT, env: null, timeoutMs: 10_000 }); if (!result.ok) { return null; } const value = result.value.stdout.trim(); return value.length > 0 ? value : null; } async function resolveDashScopeProvider(): Promise { const apiKey = process.env.DASHSCOPE_API_KEY ?? (await cfgGet("DASHSCOPE_API_KEY")); const baseUrl = process.env.DASHSCOPE_BASE_URL ?? (await cfgGet("DASHSCOPE_BASE_URL")); const model = process.env.DASHSCOPE_MODEL ?? (await cfgGet("DASHSCOPE_MODEL")) ?? "qwen-plus"; if (!apiKey || !baseUrl) { return null; } return { apiKey, baseUrl, model }; } function toIssueFromJson(raw: unknown): IssueData | null { if (typeof raw !== "object" || raw === null) { return null; } const obj = raw as Record; const labels = (Array.isArray(obj.labels) ? obj.labels : []) .map((label) => { if (typeof label === "string") { return label; } if (typeof label === "object" && label !== null && typeof (label as Record).name === "string") { return String((label as Record).name); } return ""; }) .filter((label) => label.length > 0); const comments = (Array.isArray(obj.comments) ? obj.comments : []).map((comment) => { const item = (comment ?? {}) as Record; const userObj = (item.user ?? {}) as Record; return { author: typeof item.author === "string" ? item.author : typeof userObj.login === "string" ? userObj.login : "unknown", body: typeof item.body === "string" ? item.body : "", createdAt: typeof item.created_at === "string" ? item.created_at : typeof item.createdAt === "string" ? item.createdAt : "", }; }); const parsed = issueSchema.parse({ id: Number(obj.id ?? 0), number: Number(obj.number ?? 0), title: typeof obj.title === "string" ? obj.title : "", body: typeof obj.body === "string" ? obj.body : "", state: typeof obj.state === "string" ? obj.state : "unknown", labels, comments, url: typeof obj.url === "string" ? obj.url : "", }); return parsed.number > 0 ? parsed : null; } async function parseIssueFromText(text: string, provider: Provider | null): Promise { if (provider === null) { return null; } const extracted = await llmExtract({ text, schema: issueSchema, provider }); if (!extracted.ok) { return null; } return extracted.value.number > 0 ? extracted.value : null; } async function runWithRetries( run: () => Promise< | { ok: true; stdout: string; stderr: string } | { ok: false; error: SpawnError; lastStdout: string; lastStderr: string } >, maxAttempts: number, ): Promise< | { ok: true; stdout: string; stderr: string; attemptsUsed: number } | { ok: false; error: SpawnError; attemptsUsed: number; lastStdout: string; lastStderr: string } > { let attemptsUsed = 0; let lastError: SpawnError | null = null; let lastStdout = ""; let lastStderr = ""; while (attemptsUsed < maxAttempts) { attemptsUsed += 1; const current = await run(); if (current.ok) { return { ok: true, stdout: current.stdout, stderr: current.stderr, attemptsUsed }; } lastError = current.error; lastStdout = current.lastStdout; lastStderr = current.lastStderr; if (attemptsUsed < maxAttempts) { const backoffMs = Math.min(1000 * 2 ** (attemptsUsed - 1), 4000); await new Promise((resolve) => setTimeout(resolve, backoffMs)); } } if (lastError === null) { return { ok: false, error: { kind: "spawn_failed", message: "unknown retry failure" }, attemptsUsed, lastStdout, lastStderr, }; } return { ok: false, error: lastError, attemptsUsed, lastStdout, lastStderr }; } function extractPrInfo(text: string): { prUrl: string | null; prNumber: number | null } { const url = text.match(/https?:\/\/\S+/)?.[0] ?? null; const num = text.match(/(?:pulls|pull|pr)\/(\d+)/i)?.[1] ?? text.match(/#(\d+)/)?.[1] ?? null; return { prUrl: url, prNumber: num === null ? null : Number(num) }; } async function runTestCommand(command: string): Promise<{ ok: boolean; stdoutPreview: string; stderrPreview: string; reason: string | null; }> { const result = await spawnSafe("bash", ["-lc", command], { cwd: NERVE_ROOT, env: null, timeoutMs: 300_000, }); if (result.ok) { return { ok: true, stdoutPreview: result.value.stdout.slice(0, 600), stderrPreview: result.value.stderr.slice(0, 400), reason: null, }; } return { ok: false, stdoutPreview: (result.error.kind === "spawn_failed" ? "" : result.error.stdout).slice(0, 600), stderrPreview: (result.error.kind === "spawn_failed" ? result.error.message : result.error.stderr).slice(0, 400), reason: formatSpawnFailure(result.error), }; } async function intake(start: StartStep, _messages: WorkflowMessage[]): Promise> { const parsed = parseIssueUrl(start.content); if (!parsed.valid) { return { content: parsed.failureReason ?? "invalid issue URL", meta: parsed }; } return { content: `parsed issue URL: ${parsed.owner}/${parsed.repo}#${parsed.issueNumber} @ ${parsed.host}`, meta: parsed, }; } async function issueReader( _start: StartStep, messages: WorkflowMessage[], ): Promise> { const intakeMeta = lastMetaForRole(messages, "intake"); const attempt = messages.filter((message) => message.role === "issue-reader").length + 1; if ( intakeMeta === null || !intakeMeta.valid || intakeMeta.owner === null || intakeMeta.repo === null || intakeMeta.issueNumber === null ) { return { content: "issue-reader cannot continue: missing valid intake meta", meta: { issue: null, fetchOk: false, transientError: false, attempt, failureKind: "permanent", failureReason: "missing valid intake meta", }, }; } const repoSpec = `${intakeMeta.owner}/${intakeMeta.repo}`; const jsonRun = await runWithRetries(async () => { const run = await spawnSafe( "tea", [ "issue", "show", String(intakeMeta.issueNumber), "--repo", repoSpec, "--comments", "--json", "id,number,title,body,state,labels,comments,url", ], { cwd: NERVE_ROOT, env: null, timeoutMs: 60_000, }, ); if (run.ok) { return { ok: true, stdout: run.value.stdout, stderr: run.value.stderr }; } return { ok: false, error: run.error, lastStdout: run.error.kind === "spawn_failed" ? "" : run.error.stdout, lastStderr: run.error.kind === "spawn_failed" ? run.error.message : run.error.stderr, }; }, ISSUE_READER_MAX_ATTEMPTS); if (jsonRun.ok) { try { const issue = toIssueFromJson(JSON.parse(jsonRun.stdout) as unknown); if (issue !== null) { return { content: `issue fetched: #${issue.number} ${issue.title}`, meta: { issue, fetchOk: true, transientError: false, attempt, failureKind: "none", failureReason: null, }, }; } } catch { // fallback to text parsing } } const textRun = await runWithRetries(async () => { const run = await spawnSafe( "tea", ["issue", "show", String(intakeMeta.issueNumber), "--repo", repoSpec, "--comments"], { cwd: NERVE_ROOT, env: null, timeoutMs: 60_000, }, ); if (run.ok) { return { ok: true, stdout: run.value.stdout, stderr: run.value.stderr }; } return { ok: false, error: run.error, lastStdout: run.error.kind === "spawn_failed" ? "" : run.error.stdout, lastStderr: run.error.kind === "spawn_failed" ? run.error.message : run.error.stderr, }; }, ISSUE_READER_MAX_ATTEMPTS); if (textRun.ok) { const provider = await resolveDashScopeProvider(); const issue = await parseIssueFromText(textRun.stdout, provider); if (issue !== null) { return { content: `issue fetched (text+extract): #${issue.number} ${issue.title}`, meta: { issue, fetchOk: true, transientError: false, attempt, failureKind: "none", failureReason: null, }, }; } return { content: "tea issue output received, but could not parse structured fields.", meta: { issue: null, fetchOk: false, transientError: false, attempt, failureKind: "permanent", failureReason: "unable to parse tea issue output", }, }; } const classified = classifyIssueReaderFailure(textRun.error); return { content: `issue read failed after retries: ${formatSpawnFailure(textRun.error)}`, meta: { issue: null, fetchOk: false, transientError: classified.transientError, attempt, failureKind: classified.failureKind, failureReason: formatSpawnFailure(textRun.error), }, }; } async function planner(_start: StartStep, messages: WorkflowMessage[]): Promise> { const issueMeta = lastMetaForRole(messages, "issue-reader"); if (issueMeta === null || !issueMeta.fetchOk || issueMeta.issue === null) { return { content: "planner cannot continue: issue-reader has no issue data", meta: { plan: null, targetFiles: null, testCommands: null, riskNotes: null, planningOk: false, failureKind: "planning_failed", failureReason: "missing issue data", }, }; } const prompt = `You are planning a fix for a Gitea issue in this repository. ${nerveAgentContext} Issue URL: ${issueMeta.issue.url} Issue title: ${issueMeta.issue.title} Issue body: ${issueMeta.issue.body} Issue comments: ${issueMeta.issue.comments.map((c) => `- ${c.author} (${c.createdAt}): ${c.body}`).join("\n") || "(none)"} Current nerve.yaml: \`\`\`yaml ${readNerveConfigText()} \`\`\` Output implementation-ready markdown with sections: 1) Problem understanding 2) Change strategy 3) Test strategy (commands) 4) Risks`; const result = await cursorAgent({ prompt, mode: "ask", cwd: NERVE_ROOT, env: null, timeoutMs: null, }); if (!result.ok) { return { content: `planner failed: ${formatSpawnFailure(result.error)}`, meta: { plan: null, targetFiles: null, testCommands: null, riskNotes: null, planningOk: false, failureKind: "planning_failed", failureReason: formatSpawnFailure(result.error), }, }; } const plan = result.value; const provider = await resolveDashScopeProvider(); if (provider === null) { return { content: plan, meta: { plan, targetFiles: null, testCommands: null, riskNotes: null, planningOk: true, failureKind: "none", failureReason: null, }, }; } const structured = await llmExtract({ text: plan, schema: planSchema, provider }); if (!structured.ok) { return { content: `${plan}\n\n[llmExtract error] ${JSON.stringify(structured.error)}`, meta: { plan, targetFiles: null, testCommands: null, riskNotes: null, planningOk: true, failureKind: "none", failureReason: null, }, }; } return { content: plan, meta: { plan, targetFiles: structured.value.targetFiles, testCommands: structured.value.testCommands, riskNotes: structured.value.riskNotes, planningOk: true, failureKind: "none", failureReason: null, }, }; } async function implementer( _start: StartStep, messages: WorkflowMessage[], ): Promise> { const plannerMeta = lastMetaForRole(messages, "planner"); const intakeMeta = lastMetaForRole(messages, "intake"); const issueMeta = lastMetaForRole(messages, "issue-reader"); const attempt = messages.filter((message) => message.role === "implementer").length + 1; if ( plannerMeta === null || !plannerMeta.planningOk || plannerMeta.plan === null || intakeMeta === null || intakeMeta.issueNumber === null ) { return { content: "implementer cannot continue: missing planner or intake context", meta: { branchName: null, changedFiles: null, implementationOk: false, attempt, failureKind: "agent_failed", failureReason: "missing planner/intake context", implementationLog: null, }, }; } const slugSource = issueMeta?.issue?.title ?? plannerMeta.plan.split("\n")[0] ?? "fix"; const branchName = `fix/issue-${intakeMeta.issueNumber}-${slugify(slugSource)}`; const existsResult = await spawnSafe("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], { cwd: NERVE_ROOT, env: null, timeoutMs: 10_000, }); const checkoutResult = existsResult.ok ? await spawnSafe("git", ["checkout", branchName], { cwd: NERVE_ROOT, env: null, timeoutMs: 20_000 }) : await spawnSafe("git", ["checkout", "-b", branchName], { cwd: NERVE_ROOT, env: null, timeoutMs: 20_000 }); if (!checkoutResult.ok) { return { content: `branch setup failed: ${formatSpawnFailure(checkoutResult.error)}`, meta: { branchName, changedFiles: null, implementationOk: false, attempt, failureKind: "branch_failed", failureReason: formatSpawnFailure(checkoutResult.error), implementationLog: null, }, }; } const prompt = `Implement the planned fix in this repository. Issue #${intakeMeta.issueNumber} Plan: ${plannerMeta.plan} Target files: ${plannerMeta.targetFiles?.join("\n") ?? "(not specified)"} Test commands: ${plannerMeta.testCommands?.join("\n") ?? "(not specified)"} Apply the code changes now with minimal, focused edits.`; const agentResult = await cursorAgent({ prompt, mode: "default", cwd: NERVE_ROOT, env: null, timeoutMs: null, }); if (!agentResult.ok) { return { content: `implementer agent failed: ${formatSpawnFailure(agentResult.error)}`, meta: { branchName, changedFiles: null, implementationOk: false, attempt, failureKind: "agent_failed", failureReason: formatSpawnFailure(agentResult.error), implementationLog: null, }, }; } const diffResult = await spawnSafe("git", ["diff", "--name-only"], { cwd: NERVE_ROOT, env: null, timeoutMs: 15_000, }); if (!diffResult.ok) { return { content: `implementation finished but diff check failed: ${formatSpawnFailure(diffResult.error)}`, meta: { branchName, changedFiles: null, implementationOk: false, attempt, failureKind: "no_diff", failureReason: formatSpawnFailure(diffResult.error), implementationLog: agentResult.value, }, }; } const changedFiles = diffResult.value.stdout .split("\n") .map((file) => file.trim()) .filter((file) => file.length > 0); if (changedFiles.length === 0) { return { content: "implementer made no local diff", meta: { branchName, changedFiles: [], implementationOk: false, attempt, failureKind: "no_diff", failureReason: "git diff --name-only is empty", implementationLog: agentResult.value, }, }; } return { content: `branch ready: ${branchName}\nchanged files:\n${changedFiles.join("\n")}`, meta: { branchName, changedFiles, implementationOk: true, attempt, failureKind: "none", failureReason: null, implementationLog: agentResult.value, }, }; } async function tester(_start: StartStep, messages: WorkflowMessage[]): Promise> { const plannerMeta = lastMetaForRole(messages, "planner"); const implementerMeta = lastMetaForRole(messages, "implementer"); const attempt = messages.filter((message) => message.role === "tester").length + 1; if (implementerMeta === null || !implementerMeta.implementationOk || implementerMeta.branchName === null) { return { content: "tester cannot continue: no successful implementer output", meta: { passed: false, attempt, failureReason: "no successful implementer output", testCommandResults: null, }, }; } const commands = plannerMeta?.testCommands !== null && plannerMeta?.testCommands !== undefined && plannerMeta.testCommands.length > 0 ? plannerMeta.testCommands : ["pnpm test"]; const testCommandResults: TestCommandResult[] = []; for (const command of commands) { const run = await runTestCommand(command); testCommandResults.push({ command, ok: run.ok, stdoutPreview: run.stdoutPreview, stderrPreview: run.stderrPreview, }); if (!run.ok) { return { content: `test failed: ${command}\n${run.reason ?? "unknown error"}`, meta: { passed: false, attempt, failureReason: `command failed: ${command} (${run.reason ?? "unknown error"})`, testCommandResults, }, }; } } return { content: `all tests passed on branch ${implementerMeta.branchName}`, meta: { passed: true, attempt, failureReason: null, testCommandResults, }, }; } async function prPublisher( _start: StartStep, messages: WorkflowMessage[], ): Promise> { const attempt = messages.filter((message) => message.role === "pr-publisher").length + 1; const intakeMeta = lastMetaForRole(messages, "intake"); const issueMeta = lastMetaForRole(messages, "issue-reader"); const plannerMeta = lastMetaForRole(messages, "planner"); const implementerMeta = lastMetaForRole(messages, "implementer"); const testerMeta = lastMetaForRole(messages, "tester"); if (testerMeta === null || !testerMeta.passed) { return { content: "skip PR publishing: tester.passed is not true", meta: { prUrl: null, prNumber: null, linkedIssue: intakeMeta?.issueUrl ?? null, published: false, attempt, failureKind: "permanent", failureReason: "tester did not pass", }, }; } if ( intakeMeta === null || intakeMeta.owner === null || intakeMeta.repo === null || implementerMeta === null || implementerMeta.branchName === null ) { return { content: "pr-publisher cannot continue: missing required context", meta: { prUrl: null, prNumber: null, linkedIssue: intakeMeta?.issueUrl ?? null, published: false, attempt, failureKind: "permanent", failureReason: "missing context", }, }; } const pushRun = await runWithRetries(async () => { const run = await spawnSafe("git", ["push", "-u", "origin", implementerMeta.branchName as string], { cwd: NERVE_ROOT, env: null, timeoutMs: 180_000, }); if (run.ok) { return { ok: true, stdout: run.value.stdout, stderr: run.value.stderr }; } return { ok: false, error: run.error, lastStdout: run.error.kind === "spawn_failed" ? "" : run.error.stdout, lastStderr: run.error.kind === "spawn_failed" ? run.error.message : run.error.stderr, }; }, PUBLISHER_MAX_ATTEMPTS); if (!pushRun.ok) { const failureKind = classifyPublisherFailure(pushRun.error); return { content: `failed to push branch before PR: ${formatSpawnFailure(pushRun.error)}`, meta: { prUrl: null, prNumber: null, linkedIssue: intakeMeta.issueUrl, published: false, attempt, failureKind, failureReason: formatSpawnFailure(pushRun.error), }, }; } const shortTitle = (issueMeta?.issue?.title ?? "issue fix").slice(0, 72); const title = `fix: ${shortTitle}`; const body = [ `Issue: ${intakeMeta.issueUrl}`, "", "## Summary", ...(implementerMeta.changedFiles ?? []).map((file) => `- updated \`${file}\``), "", "## Plan", plannerMeta?.plan ?? "(no planner output)", "", "## Test Results", testerMeta.testCommandResults?.map((r) => `- ${r.ok ? "PASS" : "FAIL"} \`${r.command}\``).join("\n") ?? "- no test logs", ].join("\n"); const repoSpec = `${intakeMeta.owner}/${intakeMeta.repo}`; const prRun = await runWithRetries(async () => { const run = await spawnSafe( "tea", ["pr", "create", "--repo", repoSpec, "--title", title, "--body", body, "--head", implementerMeta.branchName as string], { cwd: NERVE_ROOT, env: null, timeoutMs: 120_000, }, ); if (run.ok) { return { ok: true, stdout: run.value.stdout, stderr: run.value.stderr }; } return { ok: false, error: run.error, lastStdout: run.error.kind === "spawn_failed" ? "" : run.error.stdout, lastStderr: run.error.kind === "spawn_failed" ? run.error.message : run.error.stderr, }; }, PUBLISHER_MAX_ATTEMPTS); if (!prRun.ok) { const failureKind = classifyPublisherFailure(prRun.error); return { content: `PR creation failed: ${formatSpawnFailure(prRun.error)}`, meta: { prUrl: null, prNumber: null, linkedIssue: intakeMeta.issueUrl, published: false, attempt, failureKind, failureReason: formatSpawnFailure(prRun.error), }, }; } const info = extractPrInfo(`${prRun.stdout}\n${prRun.stderr}`); return { content: `PR created: ${title}\n${prRun.stdout}`, meta: { prUrl: info.prUrl, prNumber: info.prNumber, linkedIssue: intakeMeta.issueUrl, published: true, attempt, failureKind: "none", failureReason: null, }, }; } const workflow: WorkflowDefinition = { name: "gitea-issue-solver", roles: { intake, "issue-reader": issueReader, planner, implementer, tester, "pr-publisher": prPublisher, }, moderator(context: ModeratorContext) { if (context.steps.length === 0) { return "intake"; } const last = context.steps[context.steps.length - 1]; if (last.role === "intake") { const meta = last.meta as WorkflowMeta["intake"]; return meta.valid ? "issue-reader" : END; } if (last.role === "issue-reader") { const meta = last.meta as WorkflowMeta["issue-reader"]; if (meta.fetchOk) { return "planner"; } return END; } if (last.role === "planner") { const meta = last.meta as WorkflowMeta["planner"]; return meta.planningOk ? "implementer" : END; } if (last.role === "implementer") { return "tester"; } if (last.role === "tester") { const meta = last.meta as WorkflowMeta["tester"]; if (meta.passed) { return "pr-publisher"; } return meta.attempt < TESTER_MAX_ATTEMPTS ? "implementer" : END; } if (last.role === "pr-publisher") { return END; } return END; }, }; export default workflow;