chore(workflow): auto-generated commit
This commit is contained in:
parent
57c740cdde
commit
95587260f6
@ -36,3 +36,6 @@ workflows:
|
|||||||
gitea-issue-solver:
|
gitea-issue-solver:
|
||||||
concurrency: 1
|
concurrency: 1
|
||||||
overflow: drop
|
overflow: drop
|
||||||
|
solve-issue:
|
||||||
|
concurrency: 1
|
||||||
|
overflow: drop
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -137,6 +137,28 @@ importers:
|
|||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
|
workflows/solve-issue:
|
||||||
|
dependencies:
|
||||||
|
'@uncaged/nerve-core':
|
||||||
|
specifier: link:../../../repos/nerve/packages/core
|
||||||
|
version: link:../../../repos/nerve/packages/core
|
||||||
|
'@uncaged/nerve-workflow-utils':
|
||||||
|
specifier: link:../../../repos/nerve/packages/workflow-utils
|
||||||
|
version: link:../../../repos/nerve/packages/workflow-utils
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.6
|
||||||
|
version: 4.3.6
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.0.0
|
||||||
|
version: 22.19.17
|
||||||
|
esbuild:
|
||||||
|
specifier: ^0.27.0
|
||||||
|
version: 0.27.7
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.7.0
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
workflows/workflow-generator:
|
workflows/workflow-generator:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@uncaged/nerve-core':
|
'@uncaged/nerve-core':
|
||||||
|
|||||||
1
workflows/solve-issue/.gitignore
vendored
Normal file
1
workflows/solve-issue/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist/
|
||||||
32
workflows/solve-issue/build.ts
Normal file
32
workflows/solve-issue/build.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { WorkflowDefinition } from "@uncaged/nerve-core";
|
||||||
|
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { moderator } from "./moderator.js";
|
||||||
|
import type { WorkflowMeta } from "./moderator.js";
|
||||||
|
import { buildImplementRole } from "./roles/implement/index.js";
|
||||||
|
import { buildPlanRole } from "./roles/plan/index.js";
|
||||||
|
import { buildPrepareRole } from "./roles/prepare/index.js";
|
||||||
|
import { buildPublishRole } from "./roles/publish/index.js";
|
||||||
|
import { buildReadIssueRole } from "./roles/read-issue/index.js";
|
||||||
|
import { buildReviewRole } from "./roles/review/index.js";
|
||||||
|
import { buildTestRole } from "./roles/test/index.js";
|
||||||
|
|
||||||
|
export type BuildSolveIssueDeps = {
|
||||||
|
nerveRoot: string;
|
||||||
|
provider: LlmProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): WorkflowDefinition<WorkflowMeta> {
|
||||||
|
return {
|
||||||
|
name: "solve-issue",
|
||||||
|
roles: {
|
||||||
|
"read-issue": buildReadIssueRole({ provider }),
|
||||||
|
prepare: buildPrepareRole({ provider }),
|
||||||
|
plan: buildPlanRole({ provider, nerveRoot }),
|
||||||
|
implement: buildImplementRole({ provider, nerveRoot }),
|
||||||
|
review: buildReviewRole({ provider, nerveRoot }),
|
||||||
|
test: buildTestRole({ provider }),
|
||||||
|
publish: buildPublishRole({ nerveRoot }),
|
||||||
|
},
|
||||||
|
moderator,
|
||||||
|
};
|
||||||
|
}
|
||||||
16
workflows/solve-issue/index.ts
Normal file
16
workflows/solve-issue/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import { buildSolveIssue } from "./build.js";
|
||||||
|
import { resolveDashScopeProvider } from "./lib/provider.js";
|
||||||
|
|
||||||
|
const HOME = process.env.HOME ?? "/home/azureuser";
|
||||||
|
const NERVE_ROOT = join(HOME, ".uncaged-nerve");
|
||||||
|
|
||||||
|
const provider = await resolveDashScopeProvider(NERVE_ROOT);
|
||||||
|
|
||||||
|
if (provider === null) {
|
||||||
|
throw new Error("Set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL (or cfg get equivalents)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflow = buildSolveIssue({ nerveRoot: NERVE_ROOT, provider });
|
||||||
|
|
||||||
|
export default workflow;
|
||||||
21
workflows/solve-issue/lib/provider.ts
Normal file
21
workflows/solve-issue/lib/provider.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { spawnSafe } from "@uncaged/nerve-workflow-utils";
|
||||||
|
|
||||||
|
export async function cfgGet(nerveRoot: string, key: string): Promise<string | null> {
|
||||||
|
const result = await spawnSafe("cfg", ["get", key], { cwd: nerveRoot, env: null, timeoutMs: 10_000 });
|
||||||
|
if (!result.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const value = result.value.stdout.trim();
|
||||||
|
return value.length > 0 ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveDashScopeProvider(nerveRoot: string): Promise<LlmProvider | null> {
|
||||||
|
const apiKey = process.env.DASHSCOPE_API_KEY ?? (await cfgGet(nerveRoot, "DASHSCOPE_API_KEY"));
|
||||||
|
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? (await cfgGet(nerveRoot, "DASHSCOPE_BASE_URL"));
|
||||||
|
const model = process.env.DASHSCOPE_MODEL ?? (await cfgGet(nerveRoot, "DASHSCOPE_MODEL")) ?? "qwen-plus";
|
||||||
|
if (!apiKey || !baseUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { apiKey, baseUrl, model };
|
||||||
|
}
|
||||||
110
workflows/solve-issue/lib/repo-context.ts
Normal file
110
workflows/solve-issue/lib/repo-context.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import type { WorkflowMessage } from "@uncaged/nerve-core";
|
||||||
|
|
||||||
|
export type SolveIssueParse = {
|
||||||
|
host: string;
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SolveIssueRepo = {
|
||||||
|
path: string;
|
||||||
|
defaultBranch: string;
|
||||||
|
packageManager: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HOME = process.env.HOME ?? "/home/azureuser";
|
||||||
|
|
||||||
|
function extractMarkedSection(text: string, marker: string): Record<string, string> | null {
|
||||||
|
const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const re = new RegExp(`---${escaped}---\\s*([\\s\\S]*?)(?:\\n---|$)`);
|
||||||
|
const m = text.match(re);
|
||||||
|
if (m === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rec: Record<string, string> = {};
|
||||||
|
for (const line of m[1].split("\n")) {
|
||||||
|
const kv = line.match(/^([a-zA-Z]+):\s*(.+)$/);
|
||||||
|
if (kv !== null) {
|
||||||
|
rec[kv[1]] = kv[2].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.keys(rec).length > 0 ? rec : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSolveIssueParse(text: string): SolveIssueParse | null {
|
||||||
|
const rec = extractMarkedSection(text, "SOLVE_ISSUE_PARSE");
|
||||||
|
if (rec === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const host = rec.host ?? "";
|
||||||
|
const owner = rec.owner ?? "";
|
||||||
|
const repo = rec.repo ?? "";
|
||||||
|
const num = Number(rec.number ?? "");
|
||||||
|
if (host.length === 0 || owner.length === 0 || repo.length === 0 || !Number.isFinite(num) || num <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { host, owner, repo, number: num };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSolveIssueRepo(text: string): SolveIssueRepo | null {
|
||||||
|
const rec = extractMarkedSection(text, "SOLVE_ISSUE_REPO");
|
||||||
|
if (rec === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const path = rec.path ?? "";
|
||||||
|
if (path.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
defaultBranch: rec.defaultBranch ?? "main",
|
||||||
|
packageManager: rec.packageManager ?? "pnpm",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefer explicit prepare marker; else ~/Code/<owner>/<repo> from read-issue parse block. */
|
||||||
|
export function resolveRepoCwd(messages: WorkflowMessage[]): string | null {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
if (messages[i].role === "prepare") {
|
||||||
|
const repo = parseSolveIssueRepo(messages[i].content);
|
||||||
|
if (repo !== null) {
|
||||||
|
return repo.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
if (messages[i].role === "read-issue") {
|
||||||
|
const parsed = parseSolveIssueParse(messages[i].content);
|
||||||
|
if (parsed !== null) {
|
||||||
|
return join(HOME, "Code", parsed.owner, parsed.repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lastParseFromMessages(messages: WorkflowMessage[]): SolveIssueParse | null {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
if (messages[i].role === "read-issue") {
|
||||||
|
const parsed = parseSolveIssueParse(messages[i].content);
|
||||||
|
if (parsed !== null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lastRepoFromMessages(messages: WorkflowMessage[]): SolveIssueRepo | null {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
if (messages[i].role === "prepare") {
|
||||||
|
const repo = parseSolveIssueRepo(messages[i].content);
|
||||||
|
if (repo !== null) {
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
11
workflows/solve-issue/lib/spawn-utils.ts
Normal file
11
workflows/solve-issue/lib/spawn-utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { SpawnError } from "@uncaged/nerve-workflow-utils";
|
||||||
|
|
||||||
|
export 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)}`;
|
||||||
|
}
|
||||||
7
workflows/solve-issue/lib/start-meta.ts
Normal file
7
workflows/solve-issue/lib/start-meta.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { StartStep } from "@uncaged/nerve-core";
|
||||||
|
|
||||||
|
/** Runtime may include threadId before types catch up in published nerve-core. */
|
||||||
|
export function threadIdFromStart(start: StartStep): string {
|
||||||
|
const m = start.meta as { threadId?: string };
|
||||||
|
return typeof m.threadId === "string" && m.threadId.length > 0 ? m.threadId : "unknown";
|
||||||
|
}
|
||||||
89
workflows/solve-issue/moderator.ts
Normal file
89
workflows/solve-issue/moderator.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { END } from "@uncaged/nerve-core";
|
||||||
|
import type { Moderator } from "@uncaged/nerve-core";
|
||||||
|
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 { ReviewMeta } from "./roles/review/index.js";
|
||||||
|
import type { TestMeta } from "./roles/test/index.js";
|
||||||
|
import type { PublishMeta } from "./roles/publish/index.js";
|
||||||
|
|
||||||
|
export type WorkflowMeta = {
|
||||||
|
"read-issue": ReadIssueMeta;
|
||||||
|
prepare: PrepareMeta;
|
||||||
|
plan: PlanMeta;
|
||||||
|
implement: ImplementMeta;
|
||||||
|
review: ReviewMeta;
|
||||||
|
test: TestMeta;
|
||||||
|
publish: PublishMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_IMPLEMENT_ROUNDS = 20;
|
||||||
|
const MAX_TOTAL_REJECTIONS = 10;
|
||||||
|
|
||||||
|
function implementRounds(steps: { role: string }[]): number {
|
||||||
|
return steps.filter((s) => s.role === "implement").length;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === "publish") return !(s.meta as Record<string, boolean>).success;
|
||||||
|
return false;
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canRetryImplement(steps: { role: string; meta: unknown }[]): boolean {
|
||||||
|
return implementRounds(steps) < MAX_IMPLEMENT_ROUNDS && totalRejections(steps) < MAX_TOTAL_REJECTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moderator: Moderator<WorkflowMeta> = (context) => {
|
||||||
|
if (context.steps.length === 0) {
|
||||||
|
return "read-issue";
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = context.steps[context.steps.length - 1];
|
||||||
|
|
||||||
|
if (last.role === "read-issue") {
|
||||||
|
return last.meta.ready ? "prepare" : END;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.role === "prepare") {
|
||||||
|
return last.meta.ready ? "plan" : END;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.role === "plan") {
|
||||||
|
return last.meta.ready ? "implement" : END;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.role === "implement") {
|
||||||
|
if (last.meta.done) {
|
||||||
|
return "review";
|
||||||
|
}
|
||||||
|
return canRetryImplement(context.steps) ? "implement" : END;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.role === "review") {
|
||||||
|
if (last.meta.approved) {
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
return canRetryImplement(context.steps) ? "implement" : END;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.role === "test") {
|
||||||
|
if (last.meta.passed) {
|
||||||
|
return "publish";
|
||||||
|
}
|
||||||
|
return canRetryImplement(context.steps) ? "implement" : END;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last.role === "publish") {
|
||||||
|
if (last.meta.success) {
|
||||||
|
return END;
|
||||||
|
}
|
||||||
|
return canRetryImplement(context.steps) ? "implement" : END;
|
||||||
|
}
|
||||||
|
|
||||||
|
return END;
|
||||||
|
};
|
||||||
19
workflows/solve-issue/package.json
Normal file
19
workflows/solve-issue/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "solve-issue-workflow",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/nerve-core": "latest",
|
||||||
|
"@uncaged/nerve-workflow-utils": "latest",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"esbuild": "^0.27.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
workflows/solve-issue/roles/implement/index.ts
Normal file
64
workflows/solve-issue/roles/implement/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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 { z } from "zod";
|
||||||
|
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
||||||
|
import { threadIdFromStart } from "../../lib/start-meta.js";
|
||||||
|
import { formatSpawnFailure } from "../../lib/spawn-utils.js";
|
||||||
|
import { buildImplementPrompt } from "./prompt.js";
|
||||||
|
|
||||||
|
export const implementMetaSchema = z.object({
|
||||||
|
done: z.boolean().describe("true when changes are complete and build passes this round"),
|
||||||
|
});
|
||||||
|
export type ImplementMeta = z.infer<typeof implementMetaSchema>;
|
||||||
|
|
||||||
|
export type BuildImplementDeps = {
|
||||||
|
provider: LlmProvider;
|
||||||
|
nerveRoot: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role<ImplementMeta> {
|
||||||
|
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => {
|
||||||
|
const cwd = resolveRepoCwd(messages);
|
||||||
|
if (cwd === null) {
|
||||||
|
return {
|
||||||
|
content: "implement cannot run: missing repo path in thread markers",
|
||||||
|
meta: { done: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dry = isDryRun(start);
|
||||||
|
const prompt = buildImplementPrompt({ threadId: threadIdFromStart(start), nerveRoot });
|
||||||
|
const run = await cursorAgent({
|
||||||
|
prompt,
|
||||||
|
mode: "default",
|
||||||
|
model: "auto",
|
||||||
|
cwd,
|
||||||
|
env: null,
|
||||||
|
timeoutMs: 300_000,
|
||||||
|
dryRun: dry,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!run.ok) {
|
||||||
|
return {
|
||||||
|
content: `implement cursor-agent failed: ${formatSpawnFailure(run.error)}`,
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
}
|
||||||
24
workflows/solve-issue/roles/implement/prompt.ts
Normal file
24
workflows/solve-issue/roles/implement/prompt.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export function buildImplementPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
|
||||||
|
return `You are the **implement** agent. You apply code changes for the issue.
|
||||||
|
|
||||||
|
Read workflow context (plan, reviewer/test feedback): \`nerve thread show ${threadId}\`
|
||||||
|
|
||||||
|
Read Nerve workspace conventions: \`cat ${nerveRoot}/CONVENTIONS.md\`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Then close with JSON:
|
||||||
|
\`\`\`json
|
||||||
|
{ "done": true }
|
||||||
|
\`\`\`
|
||||||
|
or \`{ "done": false }\` matching whether implementation is complete.
|
||||||
|
|
||||||
|
**done=true** only when changes are complete **and** build passes in this round.`;
|
||||||
|
}
|
||||||
64
workflows/solve-issue/roles/plan/index.ts
Normal file
64
workflows/solve-issue/roles/plan/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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 { z } from "zod";
|
||||||
|
import { resolveRepoCwd } from "../../lib/repo-context.js";
|
||||||
|
import { threadIdFromStart } from "../../lib/start-meta.js";
|
||||||
|
import { formatSpawnFailure } from "../../lib/spawn-utils.js";
|
||||||
|
import { buildPlanPrompt } from "./prompt.js";
|
||||||
|
|
||||||
|
export const planMetaSchema = z.object({
|
||||||
|
ready: z.boolean().describe("true if plan is clear and actionable"),
|
||||||
|
});
|
||||||
|
export type PlanMeta = z.infer<typeof planMetaSchema>;
|
||||||
|
|
||||||
|
export type BuildPlanDeps = {
|
||||||
|
provider: LlmProvider;
|
||||||
|
nerveRoot: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<PlanMeta> {
|
||||||
|
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlanMeta>> => {
|
||||||
|
const cwd = resolveRepoCwd(messages);
|
||||||
|
if (cwd === null) {
|
||||||
|
return {
|
||||||
|
content: "plan cannot run: missing ---SOLVE_ISSUE_REPO--- or ---SOLVE_ISSUE_PARSE--- in thread",
|
||||||
|
meta: { ready: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dry = isDryRun(start);
|
||||||
|
const prompt = buildPlanPrompt({ threadId: threadIdFromStart(start), nerveRoot });
|
||||||
|
const run = await cursorAgent({
|
||||||
|
prompt,
|
||||||
|
mode: "ask",
|
||||||
|
model: "auto",
|
||||||
|
cwd,
|
||||||
|
env: null,
|
||||||
|
timeoutMs: 300_000,
|
||||||
|
dryRun: dry,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!run.ok) {
|
||||||
|
return {
|
||||||
|
content: `plan cursor-agent failed: ${formatSpawnFailure(run.error)}`,
|
||||||
|
meta: { ready: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaR = await llmExtract({
|
||||||
|
text: run.value,
|
||||||
|
schema: planMetaSchema,
|
||||||
|
provider,
|
||||||
|
dryRun: dry,
|
||||||
|
});
|
||||||
|
if (!metaR.ok) {
|
||||||
|
return {
|
||||||
|
content: `${run.value}\n\n[meta extract failed]`,
|
||||||
|
meta: { ready: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content: run.value, meta: metaR.value };
|
||||||
|
};
|
||||||
|
}
|
||||||
27
workflows/solve-issue/roles/plan/prompt.ts
Normal file
27
workflows/solve-issue/roles/plan/prompt.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export function buildPlanPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
|
||||||
|
return `You are the **plan** agent (analysis only — ask mode). You produce an implementation plan for fixing the issue.
|
||||||
|
|
||||||
|
Read workflow context: \`nerve thread show ${threadId}\`
|
||||||
|
|
||||||
|
Read Nerve workspace conventions (coding rules for agents): \`cat ${nerveRoot}/CONVENTIONS.md\`
|
||||||
|
|
||||||
|
In the **target repository** (your cwd), skim relevant files and read \`CONVENTIONS.md\` **if it exists** there.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Write an implementation plan in **markdown** with:
|
||||||
|
|
||||||
|
1. Problem understanding
|
||||||
|
2. Change strategy
|
||||||
|
3. Target files (paths)
|
||||||
|
4. **Test commands** to run (explicit shell commands, e.g. \`pnpm test\`, \`pnpm vitest run\`)
|
||||||
|
5. Risks
|
||||||
|
|
||||||
|
End your reply with a JSON code block (meta signal):
|
||||||
|
\`\`\`json
|
||||||
|
{ "ready": true }
|
||||||
|
\`\`\`
|
||||||
|
Use \`{ "ready": false }\` if the plan cannot be made actionable.
|
||||||
|
|
||||||
|
**ready=true** only when the plan is clear and actionable.`;
|
||||||
|
}
|
||||||
20
workflows/solve-issue/roles/prepare/index.ts
Normal file
20
workflows/solve-issue/roles/prepare/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { preparePrompt } from "./prompt.js";
|
||||||
|
|
||||||
|
export const prepareMetaSchema = z.object({
|
||||||
|
ready: z.boolean().describe("true if repo is ready and baseline build ok"),
|
||||||
|
});
|
||||||
|
export type PrepareMeta = z.infer<typeof prepareMetaSchema>;
|
||||||
|
|
||||||
|
export type BuildPrepareDeps = {
|
||||||
|
provider: LlmProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildPrepareRole({ provider }: BuildPrepareDeps) {
|
||||||
|
return createHermesRole<PrepareMeta>({
|
||||||
|
prompt: async (threadId) => preparePrompt({ threadId }),
|
||||||
|
extract: { provider, schema: prepareMetaSchema },
|
||||||
|
});
|
||||||
|
}
|
||||||
40
workflows/solve-issue/roles/prepare/prompt.ts
Normal file
40
workflows/solve-issue/roles/prepare/prompt.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export function preparePrompt({ threadId }: { threadId: string }): string {
|
||||||
|
return `You are the **prepare** agent. You clone or update the target repository and verify a clean baseline build.
|
||||||
|
|
||||||
|
Read prior messages / thread for issue markers: \`nerve thread show ${threadId}\`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Find **owner** and **repo** from \`---SOLVE_ISSUE_PARSE---\` in the thread (from read-issue).
|
||||||
|
|
||||||
|
Let \`REPOPATH=$HOME/Code/<owner>/<repo>\` (expand \`$HOME\`).
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. \`mkdir -p "$HOME/Code/<owner>"\`
|
||||||
|
2. If \`REPOPATH/.git\` is missing: \`git clone https://<host>/<owner>/<repo>.git "$REPOPATH"\` (use host from markers; adjust scheme if needed).
|
||||||
|
Else: \`cd "$REPOPATH" && git fetch --all && git pull --ff-only\`
|
||||||
|
3. \`cd "$REPOPATH"\` — ensure working tree clean: if \`git status --porcelain\` is non-empty, \`git stash push -u -m "solve-issue stash"\`
|
||||||
|
4. Detect default branch (\`main\` or \`master\`) and \`git checkout <default>\`
|
||||||
|
5. Detect package manager: \`pnpm-lock.yaml\` → pnpm, \`yarn.lock\` → yarn, \`package-lock.json\` → npm; run install (\`pnpm install --no-frozen-lockfile\` / \`npm ci\` or \`npm install\` / \`yarn\` as appropriate).
|
||||||
|
6. If \`package.json\` has a \`build\` script, run the build (\`pnpm build\`, etc.) and fix nothing — only verify baseline passes.
|
||||||
|
|
||||||
|
## Required marker block
|
||||||
|
|
||||||
|
Emit **exactly**:
|
||||||
|
\`\`\`
|
||||||
|
---SOLVE_ISSUE_REPO---
|
||||||
|
path: <absolute path to REPOPATH>
|
||||||
|
defaultBranch: <main or master>
|
||||||
|
packageManager: <pnpm|npm|yarn>
|
||||||
|
---
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
End with:
|
||||||
|
\`\`\`json
|
||||||
|
{ "ready": true }
|
||||||
|
\`\`\`
|
||||||
|
or \`{ "ready": false }\` if clone/install/build baseline failed.
|
||||||
|
|
||||||
|
**ready=true** only when the repo exists at \`path\`, is clean, dependencies installed, and baseline build succeeded (or no build script).`;
|
||||||
|
}
|
||||||
138
workflows/solve-issue/roles/publish/index.ts
Normal file
138
workflows/solve-issue/roles/publish/index.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||||
|
import { isDryRun, spawnSafe } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { cfgGet } from "../../lib/provider.js";
|
||||||
|
import { lastParseFromMessages, lastRepoFromMessages, resolveRepoCwd } from "../../lib/repo-context.js";
|
||||||
|
import { formatSpawnFailure } from "../../lib/spawn-utils.js";
|
||||||
|
|
||||||
|
export type PublishMeta = {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BuildPublishDeps = {
|
||||||
|
nerveRoot: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function logPath(nerveRoot: string): string {
|
||||||
|
return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPublishRole({ nerveRoot }: BuildPublishDeps): Role<PublishMeta> {
|
||||||
|
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => {
|
||||||
|
const dry = isDryRun(start);
|
||||||
|
const file = logPath(nerveRoot);
|
||||||
|
mkdirSync(join(file, ".."), { recursive: true });
|
||||||
|
|
||||||
|
const cwd = resolveRepoCwd(messages);
|
||||||
|
const parsed = lastParseFromMessages(messages);
|
||||||
|
const repoInfo = lastRepoFromMessages(messages);
|
||||||
|
|
||||||
|
if (dry) {
|
||||||
|
const msg = "[dry-run] publish skipped (no git push / PR)";
|
||||||
|
writeFileSync(file, `${msg}\n`, "utf-8");
|
||||||
|
return {
|
||||||
|
content: `[dry-run] publish skipped — log: ${file}`,
|
||||||
|
meta: { success: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const res = await fetch(apiUrl, {
|
||||||
|
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) {
|
||||||
|
lines.push(`fetch error: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(file, lines.join("\n"), "utf-8");
|
||||||
|
|
||||||
|
if (!prOk) {
|
||||||
|
return {
|
||||||
|
content: `publish: push ok but PR API failed\nLog: ${file}`,
|
||||||
|
meta: { success: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: `publish: pushed ${branch} and opened PR\nLog: ${file}`,
|
||||||
|
meta: { success: true },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
20
workflows/solve-issue/roles/read-issue/index.ts
Normal file
20
workflows/solve-issue/roles/read-issue/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { readIssuePrompt } from "./prompt.js";
|
||||||
|
|
||||||
|
export const readIssueMetaSchema = z.object({
|
||||||
|
ready: z.boolean().describe("true if issue content was fetched and markers are present"),
|
||||||
|
});
|
||||||
|
export type ReadIssueMeta = z.infer<typeof readIssueMetaSchema>;
|
||||||
|
|
||||||
|
export type BuildReadIssueDeps = {
|
||||||
|
provider: LlmProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildReadIssueRole({ provider }: BuildReadIssueDeps) {
|
||||||
|
return createHermesRole<ReadIssueMeta>({
|
||||||
|
prompt: async (threadId) => readIssuePrompt({ threadId }),
|
||||||
|
extract: { provider, schema: readIssueMetaSchema },
|
||||||
|
});
|
||||||
|
}
|
||||||
34
workflows/solve-issue/roles/read-issue/prompt.ts
Normal file
34
workflows/solve-issue/roles/read-issue/prompt.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export function readIssuePrompt({ threadId }: { threadId: string }): string {
|
||||||
|
return `You are the **read-issue** agent. You fetch Gitea issue content via the \`tea\` CLI.
|
||||||
|
|
||||||
|
Read the workflow thread start prompt for the issue URL (same run): \`nerve thread show ${threadId}\`
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. From the **initial user prompt** (issue URL), extract **host**, **owner**, **repo**, and **issue number**. Supported shape:
|
||||||
|
\`https://<host>/<owner>/<repo>/issues/<number>\`
|
||||||
|
|
||||||
|
2. Run:
|
||||||
|
\`tea issue show <number> --repo <owner>/<repo> --comments\`
|
||||||
|
(Add \`--json\` if helpful for parsing.)
|
||||||
|
|
||||||
|
3. In your reply, include **structured issue text**: title, body, labels, and each comment (author + body + time).
|
||||||
|
|
||||||
|
4. You **must** emit this marker block **exactly** (fill in real values):
|
||||||
|
\`\`\`
|
||||||
|
---SOLVE_ISSUE_PARSE---
|
||||||
|
host: <host>
|
||||||
|
owner: <owner>
|
||||||
|
repo: <repo>
|
||||||
|
number: <number>
|
||||||
|
---
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
5. End with JSON meta (verbatim block):
|
||||||
|
\`\`\`json
|
||||||
|
{ "ready": true }
|
||||||
|
\`\`\`
|
||||||
|
Use \`{ "ready": false }\` if you could not fetch or parse the issue.
|
||||||
|
|
||||||
|
**ready=true** only if the issue was fetched successfully and the marker block is correct.`;
|
||||||
|
}
|
||||||
21
workflows/solve-issue/roles/review/index.ts
Normal file
21
workflows/solve-issue/roles/review/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { reviewPrompt } from "./prompt.js";
|
||||||
|
|
||||||
|
export const reviewMetaSchema = z.object({
|
||||||
|
approved: z.boolean().describe("true if diff is clean and ready for tests"),
|
||||||
|
});
|
||||||
|
export type ReviewMeta = z.infer<typeof reviewMetaSchema>;
|
||||||
|
|
||||||
|
export type BuildReviewDeps = {
|
||||||
|
provider: LlmProvider;
|
||||||
|
nerveRoot: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildReviewRole({ provider, nerveRoot }: BuildReviewDeps) {
|
||||||
|
return createHermesRole<ReviewMeta>({
|
||||||
|
prompt: async (threadId) => reviewPrompt({ threadId, nerveRoot }),
|
||||||
|
extract: { provider, schema: reviewMetaSchema },
|
||||||
|
});
|
||||||
|
}
|
||||||
35
workflows/solve-issue/roles/review/prompt.ts
Normal file
35
workflows/solve-issue/roles/review/prompt.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export function reviewPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
|
||||||
|
return `You are a **code reviewer** (Hermes). You run after implement and before test.
|
||||||
|
|
||||||
|
Read Nerve workspace conventions: \`cat ${nerveRoot}/CONVENTIONS.md\`
|
||||||
|
|
||||||
|
Read workflow context: \`nerve thread show ${threadId}\`
|
||||||
|
|
||||||
|
Find **repo path** from \`---SOLVE_ISSUE_REPO--- path:\` in the thread (prepare step). \`cd\` there before any git commands.
|
||||||
|
|
||||||
|
## Static analysis
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
1. \`cd <repo-path> && git diff --stat\`
|
||||||
|
2. \`cd <repo-path> && git diff\`
|
||||||
|
3. \`cd <repo-path> && git status --short\`
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
Reject (**approved: false**) if you find:
|
||||||
|
|
||||||
|
- Garbage files, secrets/credentials, unrelated changes
|
||||||
|
- Violations of CONVENTIONS.md (e.g. \`interface\` vs \`type\`, dynamic \`import()\`)
|
||||||
|
|
||||||
|
Approve (**approved: true**) if the diff is clean and focused.
|
||||||
|
|
||||||
|
End with:
|
||||||
|
\`\`\`json
|
||||||
|
{ "approved": true }
|
||||||
|
\`\`\`
|
||||||
|
or
|
||||||
|
\`\`\`json
|
||||||
|
{ "approved": false }
|
||||||
|
\`\`\``;
|
||||||
|
}
|
||||||
20
workflows/solve-issue/roles/test/index.ts
Normal file
20
workflows/solve-issue/roles/test/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { testPrompt } from "./prompt.js";
|
||||||
|
|
||||||
|
export const testMetaSchema = z.object({
|
||||||
|
passed: z.boolean().describe("true if all test commands passed"),
|
||||||
|
});
|
||||||
|
export type TestMeta = z.infer<typeof testMetaSchema>;
|
||||||
|
|
||||||
|
export type BuildTestDeps = {
|
||||||
|
provider: LlmProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildTestRole({ provider }: BuildTestDeps) {
|
||||||
|
return createHermesRole<TestMeta>({
|
||||||
|
prompt: async (threadId) => testPrompt({ threadId }),
|
||||||
|
extract: { provider, schema: testMetaSchema },
|
||||||
|
});
|
||||||
|
}
|
||||||
21
workflows/solve-issue/roles/test/prompt.ts
Normal file
21
workflows/solve-issue/roles/test/prompt.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export function testPrompt({ threadId }: { threadId: string }): string {
|
||||||
|
return `You are the **test** agent (Hermes). You execute automated tests for the change.
|
||||||
|
|
||||||
|
Read workflow context: \`nerve thread show ${threadId}\`
|
||||||
|
|
||||||
|
Find **repo path** from \`---SOLVE_ISSUE_REPO--- path:\` in the thread.
|
||||||
|
|
||||||
|
From the **plan** step output, locate **Test commands** (explicit shell commands). Run each command with cwd = repo path, in order.
|
||||||
|
|
||||||
|
If the plan lists **no** test commands, try **pnpm test**, then **npm test** if pnpm is unavailable; if neither applies, explain skip.
|
||||||
|
|
||||||
|
Collect stdout/stderr snippets on failure.
|
||||||
|
|
||||||
|
End with JSON only:
|
||||||
|
\`\`\`json
|
||||||
|
{ "passed": true }
|
||||||
|
\`\`\`
|
||||||
|
or \`{ "passed": false }\`
|
||||||
|
|
||||||
|
**passed=true** only if every executed command exited 0 (or skip was justified with no failing command).`;
|
||||||
|
}
|
||||||
13
workflows/solve-issue/tsconfig.json
Normal file
13
workflows/solve-issue/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["./**/*.ts"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user