chore(workflow): auto-generated commit
This commit is contained in:
parent
95587260f6
commit
d6e95f5c65
@ -25,7 +25,7 @@ export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): W
|
|||||||
implement: buildImplementRole({ provider, nerveRoot }),
|
implement: buildImplementRole({ provider, nerveRoot }),
|
||||||
review: buildReviewRole({ provider, nerveRoot }),
|
review: buildReviewRole({ provider, nerveRoot }),
|
||||||
test: buildTestRole({ provider }),
|
test: buildTestRole({ provider }),
|
||||||
publish: buildPublishRole({ nerveRoot }),
|
publish: buildPublishRole({ provider, nerveRoot }),
|
||||||
},
|
},
|
||||||
moderator,
|
moderator,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
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 type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
||||||
import { threadIdFromStart } from "../../lib/start-meta.js";
|
import { threadIdFromStart } from "../../lib/start-meta.js";
|
||||||
import { formatSpawnFailure } from "../../lib/spawn-utils.js";
|
|
||||||
import { buildImplementPrompt } from "./prompt.js";
|
import { buildImplementPrompt } from "./prompt.js";
|
||||||
|
|
||||||
export const implementMetaSchema = z.object({
|
export const implementMetaSchema = z.object({
|
||||||
@ -27,38 +26,24 @@ export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const dry = isDryRun(start);
|
const runRole = createCursorRole<ImplementMeta>({
|
||||||
const prompt = buildImplementPrompt({ threadId: threadIdFromStart(start), nerveRoot });
|
cwd,
|
||||||
const run = await cursorAgent({
|
|
||||||
prompt,
|
|
||||||
mode: "default",
|
mode: "default",
|
||||||
model: "auto",
|
model: "auto",
|
||||||
cwd,
|
env: {},
|
||||||
env: null,
|
|
||||||
timeoutMs: 300_000,
|
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 {
|
return {
|
||||||
content: `implement cursor-agent failed: ${formatSpawnFailure(run.error)}`,
|
content: `implement failed: ${msg}`,
|
||||||
meta: { done: false },
|
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 };
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import { mkdirSync, writeFileSync } from "node:fs";
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||||
import { isDryRun, spawnSafe } from "@uncaged/nerve-workflow-utils";
|
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
import { cfgGet } from "../../lib/provider.js";
|
import { createHermesRole, isDryRun } from "@uncaged/nerve-workflow-utils";
|
||||||
import { lastParseFromMessages, lastRepoFromMessages, resolveRepoCwd } from "../../lib/repo-context.js";
|
import { z } from "zod";
|
||||||
import { formatSpawnFailure } from "../../lib/spawn-utils.js";
|
import { buildPublishPrompt } from "./prompt.js";
|
||||||
|
|
||||||
export type PublishMeta = {
|
export const publishMetaSchema = z.object({
|
||||||
success: boolean;
|
success: z.boolean().describe("true if git push and tea pr create both succeeded"),
|
||||||
};
|
});
|
||||||
|
export type PublishMeta = z.infer<typeof publishMetaSchema>;
|
||||||
|
|
||||||
export type BuildPublishDeps = {
|
export type BuildPublishDeps = {
|
||||||
|
provider: LlmProvider;
|
||||||
nerveRoot: string;
|
nerveRoot: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,17 +20,17 @@ function logPath(nerveRoot: string): string {
|
|||||||
return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`);
|
return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildPublishRole({ nerveRoot }: BuildPublishDeps): Role<PublishMeta> {
|
export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role<PublishMeta> {
|
||||||
|
const hermes = createHermesRole<PublishMeta>({
|
||||||
|
prompt: async (threadId) => buildPublishPrompt({ threadId, nerveRoot }),
|
||||||
|
extract: { provider, schema: publishMetaSchema },
|
||||||
|
});
|
||||||
|
|
||||||
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => {
|
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => {
|
||||||
const dry = isDryRun(start);
|
|
||||||
const file = logPath(nerveRoot);
|
const file = logPath(nerveRoot);
|
||||||
mkdirSync(join(file, ".."), { recursive: true });
|
mkdirSync(join(file, ".."), { recursive: true });
|
||||||
|
|
||||||
const cwd = resolveRepoCwd(messages);
|
if (isDryRun(start)) {
|
||||||
const parsed = lastParseFromMessages(messages);
|
|
||||||
const repoInfo = lastRepoFromMessages(messages);
|
|
||||||
|
|
||||||
if (dry) {
|
|
||||||
const msg = "[dry-run] publish skipped (no git push / PR)";
|
const msg = "[dry-run] publish skipped (no git push / PR)";
|
||||||
writeFileSync(file, `${msg}\n`, "utf-8");
|
writeFileSync(file, `${msg}\n`, "utf-8");
|
||||||
return {
|
return {
|
||||||
@ -37,102 +39,16 @@ export function buildPublishRole({ nerveRoot }: BuildPublishDeps): Role<PublishM
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cwd === null || parsed === null) {
|
|
||||||
writeFileSync(file, "missing cwd or SOLVE_ISSUE_PARSE\n", "utf-8");
|
|
||||||
return {
|
|
||||||
content: `publish failed: missing repo path or issue markers\nLog: ${file}`,
|
|
||||||
meta: { success: false },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const branchRun = await spawnSafe("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
||||||
cwd,
|
|
||||||
env: null,
|
|
||||||
timeoutMs: 15_000,
|
|
||||||
});
|
|
||||||
if (!branchRun.ok) {
|
|
||||||
writeFileSync(file, formatSpawnFailure(branchRun.error), "utf-8");
|
|
||||||
return {
|
|
||||||
content: `publish failed: cannot read branch — ${formatSpawnFailure(branchRun.error)}`,
|
|
||||||
meta: { success: false },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const branch = branchRun.value.stdout.trim();
|
|
||||||
if (branch.length === 0) {
|
|
||||||
return { content: "publish failed: empty branch name", meta: { success: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
const pushRun = await spawnSafe("git", ["push", "-u", "origin", branch], {
|
|
||||||
cwd,
|
|
||||||
env: null,
|
|
||||||
timeoutMs: 180_000,
|
|
||||||
});
|
|
||||||
const lines: string[] = [];
|
|
||||||
if (!pushRun.ok) {
|
|
||||||
lines.push(`git push failed: ${formatSpawnFailure(pushRun.error)}`);
|
|
||||||
writeFileSync(file, lines.join("\n"), "utf-8");
|
|
||||||
return {
|
|
||||||
content: `publish: push failed\nLog: ${file}`,
|
|
||||||
meta: { success: false },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
lines.push(`$ git push -u origin ${branch}`);
|
|
||||||
lines.push(pushRun.value.stdout);
|
|
||||||
lines.push(pushRun.value.stderr);
|
|
||||||
|
|
||||||
const token = process.env.GITEA_TOKEN ?? (await cfgGet(nerveRoot, "GITEA_TOKEN"));
|
|
||||||
if (token === null || token.length === 0) {
|
|
||||||
lines.push("missing GITEA_TOKEN");
|
|
||||||
writeFileSync(file, lines.join("\n"), "utf-8");
|
|
||||||
return {
|
|
||||||
content: `publish: push ok but no GITEA_TOKEN for PR API\nLog: ${file}`,
|
|
||||||
meta: { success: false },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = repoInfo?.defaultBranch ?? "main";
|
|
||||||
const title = `fix: issue #${parsed.number}`;
|
|
||||||
const body = [`Automated PR from solve-issue workflow.`, `Issue: https://${parsed.host}/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`].join(
|
|
||||||
"\n",
|
|
||||||
);
|
|
||||||
const apiUrl = `https://${parsed.host}/api/v1/repos/${parsed.owner}/${parsed.repo}/pulls`;
|
|
||||||
|
|
||||||
let prOk = false;
|
|
||||||
let prBody = "";
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(apiUrl, {
|
return await hermes(start, messages);
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `token ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
head: branch,
|
|
||||||
base,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
prBody = await res.text();
|
|
||||||
prOk = res.ok;
|
|
||||||
lines.push(`POST ${apiUrl} -> ${res.status}`);
|
|
||||||
lines.push(prBody.slice(0, 4000));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lines.push(`fetch error: ${e instanceof Error ? e.message : String(e)}`);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
}
|
const body = `publish failed: ${msg}\n`;
|
||||||
|
writeFileSync(file, body, "utf-8");
|
||||||
writeFileSync(file, lines.join("\n"), "utf-8");
|
|
||||||
|
|
||||||
if (!prOk) {
|
|
||||||
return {
|
return {
|
||||||
content: `publish: push ok but PR API failed\nLog: ${file}`,
|
content: `publish failed: ${msg}\nLog: ${file}`,
|
||||||
meta: { success: false },
|
meta: { success: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
content: `publish: pushed ${branch} and opened PR\nLog: ${file}`,
|
|
||||||
meta: { success: true },
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
42
workflows/solve-issue/roles/publish/prompt.ts
Normal file
42
workflows/solve-issue/roles/publish/prompt.ts
Normal file
@ -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://<host>/<owner>/<repo>/issues/<number>\`
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
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>\`
|
||||||
|
- 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 }
|
||||||
|
\`\`\``;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user