diff --git a/workflows/_shared/workspace-committer.ts b/workflows/_shared/workspace-committer.ts new file mode 100644 index 0000000..45bb1e5 --- /dev/null +++ b/workflows/_shared/workspace-committer.ts @@ -0,0 +1,109 @@ +import type { Role, RoleResult, StartStep } from "@uncaged/nerve-core"; +import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; +import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +export const committerMetaSchema = z.object({ + committed: z + .boolean() + .describe("true if branch created, changes committed, and pushed successfully"), +}); +export type CommitterMeta = z.infer; + +export type BuildWorkspaceCommitterDeps = { + extract: LlmExtractorConfig; + nerveRoot: string; + workflowName: string; + conventionalCommitScopeHint: string; + branchCheckoutExample: string; +}; + +function workspaceCommitterPrompt({ + threadId, + nerveRoot, + workflowName, + conventionalCommitScopeHint, + branchCheckoutExample, +}: { + threadId: string; + nerveRoot: string; + workflowName: string; + conventionalCommitScopeHint: string; + branchCheckoutExample: string; +}): string { + return `You are the **committer** agent (Hermes) for the **${workflowName}** workflow. The coder finished with a passing build; you branch, commit, and push workspace changes. + +## Context + +1. Read the workflow thread: \`nerve thread show ${threadId}\` +2. Your git repository root is: \`${nerveRoot}\` — \`cd\` there for all git commands. + +## Steps (in order) + +1. Run \`git status\`. There should be uncommitted changes from the coder. If there is nothing to commit, set **committed** to false and explain. +2. Create a short-lived branch (do not commit directly on the default branch if it would mix unrelated work): + - Prefer \`fix/\` or \`feat/\` with a lowercase hyphenated slug from the thread (planner/coder context). + - Example: \`${branchCheckoutExample}\` +3. \`git add -A\` +4. Write a **conventional commit** message summarizing what changed and why (scope may be \`${conventionalCommitScopeHint}\` or similar). +5. \`git commit -m ""\` (use multiple \`-m\` if you need a body). +6. \`git push -u origin \` + +**committed=true** only if branch was created, commit succeeded, and **push** succeeded. + +End your reply with a JSON line: +\`\`\`json +{ "committed": true } +\`\`\` +or +\`\`\`json +{ "committed": false } +\`\`\``; +} + +export function buildWorkspaceCommitterRole({ + extract, + nerveRoot, + workflowName, + conventionalCommitScopeHint, + branchCheckoutExample, +}: BuildWorkspaceCommitterDeps): Role { + const innerRole = createRole( + hermesAdapter, + async (start: StartStep) => + workspaceCommitterPrompt({ + threadId: start.meta.threadId, + nerveRoot, + workflowName, + conventionalCommitScopeHint, + branchCheckoutExample, + }), + committerMetaSchema, + extract, + ); + + return async (start, _messages): Promise> => { + if (isDryRun(start)) { + return { + content: "[dry-run] committer skipped (no git branch/commit/push)", + meta: { committed: true }, + }; + } + + const innerStart = { + ...start, + meta: { ...start.meta, workdir: nerveRoot }, + } as StartStep; + + try { + return await innerRole(innerStart, _messages); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { + content: `committer failed: ${msg}`, + meta: { committed: false }, + }; + } + }; +} diff --git a/workflows/develop-sense/build.ts b/workflows/develop-sense/build.ts index 29837b3..3ec5c35 100644 --- a/workflows/develop-sense/build.ts +++ b/workflows/develop-sense/build.ts @@ -63,6 +63,8 @@ export function buildDevelopSenseWorkflow({ extract, nerveRoot: cwd, workflowName: "develop-sense", + conventionalCommitScopeHint: "sense", + branchCheckoutExample: "git checkout -b fix/sense-export-path", }), }; diff --git a/workflows/develop-sense/roles/committer/index.ts b/workflows/develop-sense/roles/committer/index.ts index e51b119..5c6c98e 100644 --- a/workflows/develop-sense/roles/committer/index.ts +++ b/workflows/develop-sense/roles/committer/index.ts @@ -1,62 +1,6 @@ -import type { Role, RoleResult, StartStep } from "@uncaged/nerve-core"; -import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; -import { z } from "zod"; - -import { workspaceCommitterPrompt } from "./prompt.js"; - -export const committerMetaSchema = z.object({ - committed: z - .boolean() - .describe("true if branch created, changes committed, and pushed successfully"), -}); -export type CommitterMeta = z.infer; - -export type BuildWorkspaceCommitterDeps = { - extract: LlmExtractorConfig; - nerveRoot: string; - workflowName: string; -}; - -export function buildWorkspaceCommitterRole({ - extract, - nerveRoot, - workflowName, -}: BuildWorkspaceCommitterDeps): Role { - const innerRole = createRole( - hermesAdapter, - async (start: StartStep) => - workspaceCommitterPrompt({ - threadId: start.meta.threadId, - nerveRoot, - workflowName, - }), - committerMetaSchema, - extract, - ); - - return async (start, _messages): Promise> => { - if (isDryRun(start)) { - return { - content: "[dry-run] committer skipped (no git branch/commit/push)", - meta: { committed: true }, - }; - } - - const innerStart = { - ...start, - meta: { ...start.meta, workdir: nerveRoot }, - } as StartStep; - - try { - return await innerRole(innerStart, _messages); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - return { - content: `committer failed: ${msg}`, - meta: { committed: false }, - }; - } - }; -} +export { + buildWorkspaceCommitterRole, + committerMetaSchema, + type BuildWorkspaceCommitterDeps, + type CommitterMeta, +} from "../../../_shared/workspace-committer.js"; diff --git a/workflows/develop-sense/roles/committer/prompt.ts b/workflows/develop-sense/roles/committer/prompt.ts deleted file mode 100644 index 1eb77de..0000000 --- a/workflows/develop-sense/roles/committer/prompt.ts +++ /dev/null @@ -1,38 +0,0 @@ -export function workspaceCommitterPrompt({ - threadId, - nerveRoot, - workflowName, -}: { - threadId: string; - nerveRoot: string; - workflowName: string; -}): string { - return `You are the **committer** agent (Hermes) for the **${workflowName}** workflow. The coder finished with a passing build; you branch, commit, and push workspace changes. - -## Context - -1. Read the workflow thread: \`nerve thread show ${threadId}\` -2. Your git repository root is: \`${nerveRoot}\` — \`cd\` there for all git commands. - -## Steps (in order) - -1. Run \`git status\`. There should be uncommitted changes from the coder. If there is nothing to commit, set **committed** to false and explain. -2. Create a short-lived branch (do not commit directly on the default branch if it would mix unrelated work): - - Prefer \`fix/\` or \`feat/\` with a lowercase hyphenated slug from the thread (planner/coder context). - - Example: \`git checkout -b fix/sense-export-path\` -3. \`git add -A\` -4. Write a **conventional commit** message summarizing what changed and why (scope may be \`sense\` or similar). -5. \`git commit -m ""\` (use multiple \`-m\` if you need a body). -6. \`git push -u origin \` - -**committed=true** only if branch was created, commit succeeded, and **push** succeeded. - -End your reply with a JSON line: -\`\`\`json -{ "committed": true } -\`\`\` -or -\`\`\`json -{ "committed": false } -\`\`\``; -} diff --git a/workflows/develop-sense/tsconfig.json b/workflows/develop-sense/tsconfig.json index 1ae98b5..aa299d8 100644 --- a/workflows/develop-sense/tsconfig.json +++ b/workflows/develop-sense/tsconfig.json @@ -10,5 +10,5 @@ "declaration": false, "types": ["node"] }, - "include": ["./**/*.ts"] + "include": ["./**/*.ts", "../_shared/**/*.ts"] } diff --git a/workflows/develop-workflow/build.ts b/workflows/develop-workflow/build.ts index 4d065ef..8387420 100644 --- a/workflows/develop-workflow/build.ts +++ b/workflows/develop-workflow/build.ts @@ -63,6 +63,8 @@ export function buildDevelopWorkflow({ extract, nerveRoot, workflowName: "develop-workflow", + conventionalCommitScopeHint: "workflow", + branchCheckoutExample: "git checkout -b feat/workflow-new-step", }), }; diff --git a/workflows/develop-workflow/roles/committer/index.ts b/workflows/develop-workflow/roles/committer/index.ts index e51b119..5c6c98e 100644 --- a/workflows/develop-workflow/roles/committer/index.ts +++ b/workflows/develop-workflow/roles/committer/index.ts @@ -1,62 +1,6 @@ -import type { Role, RoleResult, StartStep } from "@uncaged/nerve-core"; -import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; -import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; -import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; -import { z } from "zod"; - -import { workspaceCommitterPrompt } from "./prompt.js"; - -export const committerMetaSchema = z.object({ - committed: z - .boolean() - .describe("true if branch created, changes committed, and pushed successfully"), -}); -export type CommitterMeta = z.infer; - -export type BuildWorkspaceCommitterDeps = { - extract: LlmExtractorConfig; - nerveRoot: string; - workflowName: string; -}; - -export function buildWorkspaceCommitterRole({ - extract, - nerveRoot, - workflowName, -}: BuildWorkspaceCommitterDeps): Role { - const innerRole = createRole( - hermesAdapter, - async (start: StartStep) => - workspaceCommitterPrompt({ - threadId: start.meta.threadId, - nerveRoot, - workflowName, - }), - committerMetaSchema, - extract, - ); - - return async (start, _messages): Promise> => { - if (isDryRun(start)) { - return { - content: "[dry-run] committer skipped (no git branch/commit/push)", - meta: { committed: true }, - }; - } - - const innerStart = { - ...start, - meta: { ...start.meta, workdir: nerveRoot }, - } as StartStep; - - try { - return await innerRole(innerStart, _messages); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - return { - content: `committer failed: ${msg}`, - meta: { committed: false }, - }; - } - }; -} +export { + buildWorkspaceCommitterRole, + committerMetaSchema, + type BuildWorkspaceCommitterDeps, + type CommitterMeta, +} from "../../../_shared/workspace-committer.js"; diff --git a/workflows/develop-workflow/roles/committer/prompt.ts b/workflows/develop-workflow/roles/committer/prompt.ts deleted file mode 100644 index 7c607c1..0000000 --- a/workflows/develop-workflow/roles/committer/prompt.ts +++ /dev/null @@ -1,38 +0,0 @@ -export function workspaceCommitterPrompt({ - threadId, - nerveRoot, - workflowName, -}: { - threadId: string; - nerveRoot: string; - workflowName: string; -}): string { - return `You are the **committer** agent (Hermes) for the **${workflowName}** workflow. The coder finished with a passing build; you branch, commit, and push workspace changes. - -## Context - -1. Read the workflow thread: \`nerve thread show ${threadId}\` -2. Your git repository root is: \`${nerveRoot}\` — \`cd\` there for all git commands. - -## Steps (in order) - -1. Run \`git status\`. There should be uncommitted changes from the coder. If there is nothing to commit, set **committed** to false and explain. -2. Create a short-lived branch (do not commit directly on the default branch if it would mix unrelated work): - - Prefer \`fix/\` or \`feat/\` with a lowercase hyphenated slug from the thread (planner/coder context). - - Example: \`git checkout -b feat/workflow-new-step\` -3. \`git add -A\` -4. Write a **conventional commit** message summarizing what changed and why (scope may be \`workflow\` or similar). -5. \`git commit -m ""\` (use multiple \`-m\` if you need a body). -6. \`git push -u origin \` - -**committed=true** only if branch was created, commit succeeded, and **push** succeeded. - -End your reply with a JSON line: -\`\`\`json -{ "committed": true } -\`\`\` -or -\`\`\`json -{ "committed": false } -\`\`\``; -} diff --git a/workflows/develop-workflow/tsconfig.json b/workflows/develop-workflow/tsconfig.json index fc00159..c42d321 100644 --- a/workflows/develop-workflow/tsconfig.json +++ b/workflows/develop-workflow/tsconfig.json @@ -9,5 +9,5 @@ "noEmit": true, "types": ["node"] }, - "include": ["./**/*.ts"] + "include": ["./**/*.ts", "../_shared/**/*.ts"] }