chore(workflow): auto-generated commit
This commit is contained in:
parent
0fab8a68c3
commit
495d8d1b60
@ -20,6 +20,11 @@ senses:
|
|||||||
interval: 1m
|
interval: 1m
|
||||||
throttle: 15s
|
throttle: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
|
git-workspace-status:
|
||||||
|
group: workspace
|
||||||
|
interval: 2m
|
||||||
|
throttle: 30s
|
||||||
|
timeout: 15s
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
sense-generator:
|
sense-generator:
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -33,6 +33,18 @@ importers:
|
|||||||
specifier: latest
|
specifier: latest
|
||||||
version: 0.31.10
|
version: 0.31.10
|
||||||
|
|
||||||
|
senses/git-workspace-status:
|
||||||
|
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
|
||||||
|
|
||||||
senses/hermes-gateway-health:
|
senses/hermes-gateway-health:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
|
|||||||
122
senses/git-workspace-status/index.js
Normal file
122
senses/git-workspace-status/index.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// src/index.ts
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
// src/schema.ts
|
||||||
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
var snapshots = sqliteTable("snapshots", {
|
||||||
|
ts: integer("ts").primaryKey(),
|
||||||
|
branch: text("branch").notNull(),
|
||||||
|
headShort: text("head_short").notNull(),
|
||||||
|
porcelainLines: integer("porcelain_lines").notNull(),
|
||||||
|
hasUpstream: integer("has_upstream").notNull(),
|
||||||
|
aheadCount: integer("ahead_count").notNull(),
|
||||||
|
behindCount: integer("behind_count").notNull(),
|
||||||
|
/** Empty string when the snapshot succeeded; otherwise a short error summary. */
|
||||||
|
gitError: text("git_error").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
// src/index.ts
|
||||||
|
var GIT_TIMEOUT_MS = 15e3;
|
||||||
|
function workspaceRoot() {
|
||||||
|
const raw = process.env.GIT_WORKSPACE_ROOT;
|
||||||
|
return raw ? resolve(raw) : resolve(process.cwd());
|
||||||
|
}
|
||||||
|
function gitErrorMessage(err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
const m = err.message.trim();
|
||||||
|
return m.length > 200 ? `${m.slice(0, 197)}...` : m;
|
||||||
|
}
|
||||||
|
return String(err);
|
||||||
|
}
|
||||||
|
function runGit(cwd, args) {
|
||||||
|
return execFileSync("git", args, {
|
||||||
|
cwd,
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: GIT_TIMEOUT_MS,
|
||||||
|
maxBuffer: 2 * 1024 * 1024
|
||||||
|
}).trimEnd();
|
||||||
|
}
|
||||||
|
function countPorcelainLines(output) {
|
||||||
|
if (!output) return 0;
|
||||||
|
return output.split("\n").filter((line) => line.length > 0).length;
|
||||||
|
}
|
||||||
|
async function compute(db, _peers) {
|
||||||
|
const root = workspaceRoot();
|
||||||
|
const ts = Date.now();
|
||||||
|
let branch = "";
|
||||||
|
let headShort = "";
|
||||||
|
let porcelainLines = 0;
|
||||||
|
let hasUpstream = 0;
|
||||||
|
let aheadCount = 0;
|
||||||
|
let behindCount = 0;
|
||||||
|
let gitError = "";
|
||||||
|
try {
|
||||||
|
const inside = runGit(root, ["rev-parse", "--is-inside-work-tree"]).trim();
|
||||||
|
if (inside !== "true") {
|
||||||
|
gitError = "not a git work tree";
|
||||||
|
await db.insert(snapshots).values({
|
||||||
|
ts,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
workspaceRoot: root,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream: false,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
branch = runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
|
headShort = runGit(root, ["rev-parse", "--short", "HEAD"]);
|
||||||
|
porcelainLines = countPorcelainLines(runGit(root, ["status", "--porcelain"]));
|
||||||
|
try {
|
||||||
|
runGit(root, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
|
||||||
|
hasUpstream = 1;
|
||||||
|
const lb = runGit(root, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
|
||||||
|
const parts = lb.split(/[\t\s]+/).filter(Boolean);
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
aheadCount = Number.parseInt(parts[0], 10) || 0;
|
||||||
|
behindCount = Number.parseInt(parts[1], 10) || 0;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
hasUpstream = 0;
|
||||||
|
aheadCount = 0;
|
||||||
|
behindCount = 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
gitError = gitErrorMessage(e);
|
||||||
|
}
|
||||||
|
await db.insert(snapshots).values({
|
||||||
|
ts,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
workspaceRoot: root,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream: hasUpstream === 1,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError: gitError || void 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
compute
|
||||||
|
};
|
||||||
13
senses/git-workspace-status/migrations/0001_init.sql
Normal file
13
senses/git-workspace-status/migrations/0001_init.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
-- Migration: 0001_init
|
||||||
|
-- Creates the snapshots table for git-workspace-status sense.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS snapshots (
|
||||||
|
ts INTEGER PRIMARY KEY,
|
||||||
|
branch TEXT NOT NULL,
|
||||||
|
head_short TEXT NOT NULL,
|
||||||
|
porcelain_lines INTEGER NOT NULL,
|
||||||
|
has_upstream INTEGER NOT NULL,
|
||||||
|
ahead_count INTEGER NOT NULL,
|
||||||
|
behind_count INTEGER NOT NULL,
|
||||||
|
git_error TEXT NOT NULL
|
||||||
|
);
|
||||||
14
senses/git-workspace-status/package.json
Normal file
14
senses/git-workspace-status/package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "sense-git-workspace-status",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=index.js --packages=external"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"esbuild": "^0.27.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
116
senses/git-workspace-status/src/index.ts
Normal file
116
senses/git-workspace-status/src/index.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
||||||
|
import { snapshots } from "./schema.ts";
|
||||||
|
|
||||||
|
const GIT_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
function workspaceRoot(): string {
|
||||||
|
const raw = process.env.GIT_WORKSPACE_ROOT;
|
||||||
|
return raw ? resolve(raw) : resolve(process.cwd());
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitErrorMessage(err: unknown): string {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
const m = err.message.trim();
|
||||||
|
return m.length > 200 ? `${m.slice(0, 197)}...` : m;
|
||||||
|
}
|
||||||
|
return String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGit(cwd: string, args: string[]): string {
|
||||||
|
return execFileSync("git", args, {
|
||||||
|
cwd,
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: GIT_TIMEOUT_MS,
|
||||||
|
maxBuffer: 2 * 1024 * 1024,
|
||||||
|
}).trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function countPorcelainLines(output: string): number {
|
||||||
|
if (!output) return 0;
|
||||||
|
return output.split("\n").filter((line) => line.length > 0).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compute(db: LibSQLDatabase, _peers: unknown) {
|
||||||
|
const root = workspaceRoot();
|
||||||
|
const ts = Date.now();
|
||||||
|
|
||||||
|
let branch = "";
|
||||||
|
let headShort = "";
|
||||||
|
let porcelainLines = 0;
|
||||||
|
let hasUpstream = 0;
|
||||||
|
let aheadCount = 0;
|
||||||
|
let behindCount = 0;
|
||||||
|
let gitError = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const inside = runGit(root, ["rev-parse", "--is-inside-work-tree"]).trim();
|
||||||
|
if (inside !== "true") {
|
||||||
|
gitError = "not a git work tree";
|
||||||
|
await db.insert(snapshots).values({
|
||||||
|
ts,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
workspaceRoot: root,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream: false,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
branch = runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
|
headShort = runGit(root, ["rev-parse", "--short", "HEAD"]);
|
||||||
|
porcelainLines = countPorcelainLines(runGit(root, ["status", "--porcelain"]));
|
||||||
|
|
||||||
|
try {
|
||||||
|
runGit(root, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
|
||||||
|
hasUpstream = 1;
|
||||||
|
const lb = runGit(root, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
|
||||||
|
const parts = lb.split(/[\t\s]+/).filter(Boolean);
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
aheadCount = Number.parseInt(parts[0], 10) || 0;
|
||||||
|
behindCount = Number.parseInt(parts[1], 10) || 0;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
hasUpstream = 0;
|
||||||
|
aheadCount = 0;
|
||||||
|
behindCount = 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
gitError = gitErrorMessage(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(snapshots).values({
|
||||||
|
ts,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspaceRoot: root,
|
||||||
|
branch,
|
||||||
|
headShort,
|
||||||
|
porcelainLines,
|
||||||
|
hasUpstream: hasUpstream === 1,
|
||||||
|
aheadCount,
|
||||||
|
behindCount,
|
||||||
|
gitError: gitError || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
13
senses/git-workspace-status/src/schema.ts
Normal file
13
senses/git-workspace-status/src/schema.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
export const snapshots = sqliteTable("snapshots", {
|
||||||
|
ts: integer("ts").primaryKey(),
|
||||||
|
branch: text("branch").notNull(),
|
||||||
|
headShort: text("head_short").notNull(),
|
||||||
|
porcelainLines: integer("porcelain_lines").notNull(),
|
||||||
|
hasUpstream: integer("has_upstream").notNull(),
|
||||||
|
aheadCount: integer("ahead_count").notNull(),
|
||||||
|
behindCount: integer("behind_count").notNull(),
|
||||||
|
/** Empty string when the snapshot succeeded; otherwise a short error summary. */
|
||||||
|
gitError: text("git_error").notNull(),
|
||||||
|
});
|
||||||
@ -3,6 +3,7 @@ import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
|
|||||||
import { buildPlannerRole } from "./roles/planner/index.js";
|
import { buildPlannerRole } from "./roles/planner/index.js";
|
||||||
import { buildCoderRole } from "./roles/coder/index.js";
|
import { buildCoderRole } from "./roles/coder/index.js";
|
||||||
import { buildTesterRole } from "./roles/tester/index.js";
|
import { buildTesterRole } from "./roles/tester/index.js";
|
||||||
|
import { buildCommitterRole } from "./roles/committer/index.js";
|
||||||
import { moderator } from "./moderator.js";
|
import { moderator } from "./moderator.js";
|
||||||
import type { SenseMeta } from "./moderator.js";
|
import type { SenseMeta } from "./moderator.js";
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export function buildSenseGenerator({
|
|||||||
planner: buildPlannerRole({ provider, cwd }),
|
planner: buildPlannerRole({ provider, cwd }),
|
||||||
coder: buildCoderRole({ provider, cwd }),
|
coder: buildCoderRole({ provider, cwd }),
|
||||||
tester: buildTesterRole({ provider }),
|
tester: buildTesterRole({ provider }),
|
||||||
|
committer: buildCommitterRole({ nerveRoot: cwd }),
|
||||||
},
|
},
|
||||||
moderator,
|
moderator,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,13 +3,17 @@ import type { Moderator } from "@uncaged/nerve-core";
|
|||||||
import type { PlannerMeta } from "./roles/planner/index.js";
|
import type { PlannerMeta } from "./roles/planner/index.js";
|
||||||
import type { CoderMeta } from "./roles/coder/index.js";
|
import type { CoderMeta } from "./roles/coder/index.js";
|
||||||
import type { TesterMeta } from "./roles/tester/index.js";
|
import type { TesterMeta } from "./roles/tester/index.js";
|
||||||
|
import type { CommitterMeta } from "./roles/committer/index.js";
|
||||||
|
|
||||||
export type SenseMeta = {
|
export type SenseMeta = {
|
||||||
planner: PlannerMeta;
|
planner: PlannerMeta;
|
||||||
coder: CoderMeta;
|
coder: CoderMeta;
|
||||||
tester: TesterMeta;
|
tester: TesterMeta;
|
||||||
|
committer: CommitterMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_CODER_ITERATIONS = 5;
|
||||||
|
|
||||||
function countRole(steps: { role: string }[], name: string): number {
|
function countRole(steps: { role: string }[], name: string): number {
|
||||||
return steps.filter((s) => s.role === name).length;
|
return steps.filter((s) => s.role === name).length;
|
||||||
}
|
}
|
||||||
@ -17,11 +21,19 @@ function countRole(steps: { role: string }[], name: string): number {
|
|||||||
export const moderator: Moderator<SenseMeta> = (context) => {
|
export const moderator: Moderator<SenseMeta> = (context) => {
|
||||||
if (context.steps.length === 0) return "planner";
|
if (context.steps.length === 0) return "planner";
|
||||||
const last = context.steps[context.steps.length - 1];
|
const last = context.steps[context.steps.length - 1];
|
||||||
|
const coderCount = context.steps.filter((s) => s.role === "coder").length;
|
||||||
|
|
||||||
if (last.role === "planner") return "coder";
|
if (last.role === "planner") return "coder";
|
||||||
if (last.role === "coder") return "tester";
|
if (last.role === "coder") return "tester";
|
||||||
if (last.role === "tester") {
|
if (last.role === "tester") {
|
||||||
if (last.meta.passed) return END;
|
if (last.meta.passed) return "committer";
|
||||||
return countRole(context.steps, "tester") < 3 ? "coder" : END;
|
const testerCount = countRole(context.steps, "tester");
|
||||||
|
if (testerCount < 3 && coderCount < MAX_CODER_ITERATIONS) return "coder";
|
||||||
|
return END;
|
||||||
|
}
|
||||||
|
if (last.role === "committer") {
|
||||||
|
if (last.meta.success) return END;
|
||||||
|
return coderCount < MAX_CODER_ITERATIONS ? "coder" : END;
|
||||||
}
|
}
|
||||||
return END;
|
return END;
|
||||||
};
|
};
|
||||||
|
|||||||
83
workflows/sense-generator/roles/committer/index.ts
Normal file
83
workflows/sense-generator/roles/committer/index.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export type CommitterMeta = {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 });
|
||||||
|
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 {
|
||||||
|
lines.push(e.message);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
await run("git", ["add", "-A"]);
|
||||||
|
const committed = await run("git", ["commit", "-m", "chore(sense): auto-generated commit"]);
|
||||||
|
if (!committed) {
|
||||||
|
success = false;
|
||||||
|
} else {
|
||||||
|
const pushed = await run("git", ["push"]);
|
||||||
|
if (!pushed) success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = lines.join("\n");
|
||||||
|
writeLog(file, log);
|
||||||
|
|
||||||
|
const summary = success ? "committed and pushed" : "commit/push failed — see log";
|
||||||
|
return {
|
||||||
|
content: `committer: ${summary}\nLog: ${file}`,
|
||||||
|
meta: { success },
|
||||||
|
} satisfies RoleResult<CommitterMeta>;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -5,17 +5,18 @@ Also look at existing workflows in the \`workflows/\` directory for patterns.
|
|||||||
|
|
||||||
## Your task
|
## Your task
|
||||||
|
|
||||||
Implement (or fix) the workflow the planner designed. If there is tester or committer feedback in the thread, fix the issues they identified.
|
Implement the planner's design. This may be **creating a new workflow** or **modifying an existing one**. If there is tester or committer feedback in the thread, fix the issues they identified.
|
||||||
|
|
||||||
## Multi-step approach
|
## Multi-step approach
|
||||||
|
|
||||||
You do NOT need to finish everything in one pass. You may return \`done: false\` to continue in the next iteration. For example:
|
You do NOT need to finish everything in one pass. You may return \`done: false\` to continue in the next iteration. For example:
|
||||||
1. First pass: scaffold files and basic structure
|
1. First pass: scaffold files / make structural changes
|
||||||
2. Second pass: implement role logic
|
2. Second pass: implement role logic
|
||||||
3. Third pass: fix build/lint errors
|
3. Third pass: fix build/lint errors
|
||||||
|
|
||||||
## Required files for each workflow
|
## Workflow file structure
|
||||||
|
|
||||||
|
Each workflow must have:
|
||||||
- \`workflows/<name>/index.ts\` — WorkflowDefinition default export
|
- \`workflows/<name>/index.ts\` — WorkflowDefinition default export
|
||||||
- \`workflows/<name>/build.ts\` — factory function
|
- \`workflows/<name>/build.ts\` — factory function
|
||||||
- \`workflows/<name>/moderator.ts\` — moderator + meta types
|
- \`workflows/<name>/moderator.ts\` — moderator + meta types
|
||||||
@ -23,7 +24,8 @@ You do NOT need to finish everything in one pass. You may return \`done: false\`
|
|||||||
- \`workflows/<name>/roles/<role>/prompt.ts\` — prompt pure function
|
- \`workflows/<name>/roles/<role>/prompt.ts\` — prompt pure function
|
||||||
- \`workflows/<name>/package.json\` — with esbuild build script
|
- \`workflows/<name>/package.json\` — with esbuild build script
|
||||||
- \`workflows/<name>/tsconfig.json\` — TypeScript config
|
- \`workflows/<name>/tsconfig.json\` — TypeScript config
|
||||||
- Update \`nerve.yaml\` with \`workflows.<name>\`
|
|
||||||
|
For **new workflows**, also update \`nerve.yaml\` with \`workflows.<name>\`.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
@ -36,8 +38,8 @@ You do NOT need to finish everything in one pass. You may return \`done: false\`
|
|||||||
## When to return done: true
|
## When to return done: true
|
||||||
|
|
||||||
Return \`done: true\` ONLY when ALL of the following are true:
|
Return \`done: true\` ONLY when ALL of the following are true:
|
||||||
- All required files are created
|
- All changes from the plan are implemented
|
||||||
- \`pnpm install --no-cache && pnpm build\` succeeds (run it!)
|
- \`cd workflows/<name> && pnpm install --no-cache && pnpm build\` succeeds (run it!)
|
||||||
- No lint or type errors remain
|
- No lint or type errors remain
|
||||||
|
|
||||||
Return \`done: false\` if you made progress but there is still work to do, or if build/lint has errors you plan to fix in the next iteration.`;
|
Return \`done: false\` if you made progress but there is still work to do, or if build/lint has errors you plan to fix in the next iteration.`;
|
||||||
|
|||||||
@ -63,7 +63,8 @@ export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role<Comm
|
|||||||
};
|
};
|
||||||
|
|
||||||
await run("git", ["add", "-A"]);
|
await run("git", ["add", "-A"]);
|
||||||
const committed = await run("git", ["commit", "-m", "chore: add generated workflow"]);
|
// 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) {
|
if (!committed) {
|
||||||
success = false;
|
success = false;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,24 +1,33 @@
|
|||||||
export function plannerPrompt({ threadId }: { threadId: string }): string {
|
export function plannerPrompt({ threadId }: { threadId: string }): string {
|
||||||
return `You are planning a new Nerve workflow.
|
return `You are a Nerve workflow planner. You can **create new workflows** or **modify existing ones**.
|
||||||
|
|
||||||
Read the workflow thread for the user's request: \`nerve thread ${threadId}\`
|
Read the workflow thread for the user's request: \`nerve thread ${threadId}\`
|
||||||
Read the nerve-dev skill for workflow conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
Read the nerve-dev skill for workflow conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||||
Also look at existing workflows in the \`workflows/\` directory for patterns.
|
List existing workflows: \`ls workflows/\`
|
||||||
|
|
||||||
Assess whether the requirements are clear enough to implement a workflow.
|
## Determine the task type
|
||||||
|
|
||||||
If requirements ARE clear:
|
1. If the user wants to **modify an existing workflow** — read its current code (\`cat workflows/<name>/moderator.ts\`, \`cat workflows/<name>/build.ts\`, \`ls workflows/<name>/roles/\`, etc.) and understand its current structure before planning changes.
|
||||||
- Pick a good kebab-case name for this workflow.
|
2. If the user wants to **create a new workflow** — look at existing workflows in \`workflows/\` for patterns to follow.
|
||||||
- Produce a PLAN (not code) in markdown covering:
|
|
||||||
- Workflow name
|
## Produce a PLAN (not code) in markdown
|
||||||
|
|
||||||
|
For **new workflows**:
|
||||||
|
- Workflow name (kebab-case)
|
||||||
- Roles list (name, purpose, tool)
|
- Roles list (name, purpose, tool)
|
||||||
- Flow transitions / moderator routing logic
|
- Flow transitions / moderator routing logic
|
||||||
- Validation loops design
|
- Validation loops design
|
||||||
- External dependencies
|
- External dependencies
|
||||||
- Data flow between roles
|
- Data flow between roles
|
||||||
|
|
||||||
If requirements are NOT clear:
|
For **modifications to existing workflows**:
|
||||||
- Describe what is missing or ambiguous.
|
- Workflow name (existing)
|
||||||
|
- What changes are needed and why
|
||||||
|
- Files to add/modify/delete
|
||||||
|
- Impact on moderator routing logic
|
||||||
|
- Backward compatibility considerations (if any)
|
||||||
|
|
||||||
|
If requirements are NOT clear, describe what is missing or ambiguous.
|
||||||
|
|
||||||
End your response with a JSON block:
|
End your response with a JSON block:
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export function testerPrompt({ threadId }: { threadId: string }): string {
|
export function testerPrompt({ threadId }: { threadId: string }): string {
|
||||||
return `You are testing a newly generated Nerve workflow end-to-end.
|
return `You are testing a Nerve workflow — either newly created or recently modified.
|
||||||
|
|
||||||
Read the workflow thread for context: \`nerve thread ${threadId}\`
|
Read the workflow thread for context: \`nerve thread ${threadId}\`
|
||||||
Read the nerve-dev skill for expected file structure: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
Read the nerve-dev skill for expected file structure: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user