diff --git a/packages/workflow-utils/src/__tests__/llm-extract.test.ts b/packages/workflow-utils/src/__tests__/llm-extract.test.ts index b96af35..e14e983 100644 --- a/packages/workflow-utils/src/__tests__/llm-extract.test.ts +++ b/packages/workflow-utils/src/__tests__/llm-extract.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; -import { llmExtract } from "../llm-extract.js"; +import { llmExtract } from "../shared/llm-extract.js"; describe("llmExtract", () => { afterEach(() => { diff --git a/packages/workflow-utils/src/__tests__/role-factories.test.ts b/packages/workflow-utils/src/__tests__/role-factories.test.ts index fbfc704..1eabe44 100644 --- a/packages/workflow-utils/src/__tests__/role-factories.test.ts +++ b/packages/workflow-utils/src/__tests__/role-factories.test.ts @@ -3,12 +3,10 @@ import { z } from "zod"; import { START } from "@uncaged/nerve-core"; -import { - createCursorRole, - createHermesRole, - createLlmRole, - createReActRole, -} from "../role-factories.js"; +import { createCursorRole } from "../role-cursor.js"; +import { createHermesRole } from "../role-hermes.js"; +import { createLlmRole } from "../role-llm.js"; +import { createReActRole } from "../role-react.js"; function startFrame(dryRun: boolean, threadId: string) { return { diff --git a/packages/workflow-utils/src/__tests__/schema-defaults.test.ts b/packages/workflow-utils/src/__tests__/schema-defaults.test.ts index a045571..265e3c5 100644 --- a/packages/workflow-utils/src/__tests__/schema-defaults.test.ts +++ b/packages/workflow-utils/src/__tests__/schema-defaults.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; -import { schemaDefaults } from "../schema-defaults.js"; +import { schemaDefaults } from "../shared/schema-defaults.js"; describe("schemaDefaults", () => { it("fills nested objects with primitive placeholders", () => { diff --git a/packages/workflow-utils/src/__tests__/spawn-safe.test.ts b/packages/workflow-utils/src/__tests__/spawn-safe.test.ts index 5c601ff..85ad1b2 100644 --- a/packages/workflow-utils/src/__tests__/spawn-safe.test.ts +++ b/packages/workflow-utils/src/__tests__/spawn-safe.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { spawnSafe } from "../spawn-safe.js"; +import { spawnSafe } from "../shared/spawn-safe.js"; describe("spawnSafe", () => { it("passes argv literally without shell interpretation (injection-safe)", async () => { diff --git a/packages/workflow-utils/src/hermes-options.ts b/packages/workflow-utils/src/hermes-options.ts deleted file mode 100644 index dfb866c..0000000 --- a/packages/workflow-utils/src/hermes-options.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { HermesRoleDefaults, HermesRoleRequired } from "./role-types.js"; - -const HERMES_DEFAULTS: HermesRoleDefaults = { - model: null, - provider: null, - skills: [], - quiet: true, - maxTurns: 90, - env: {}, - timeoutMs: 600_000, -}; - -export function resolveHermesOptions( - options: HermesRoleRequired & Partial, -): HermesRoleDefaults { - const d = HERMES_DEFAULTS; - return { - model: "model" in options && options.model !== undefined ? options.model : d.model, - provider: - "provider" in options && options.provider !== undefined ? options.provider : d.provider, - skills: "skills" in options && options.skills !== undefined ? options.skills : d.skills, - quiet: "quiet" in options && options.quiet !== undefined ? options.quiet : d.quiet, - maxTurns: - "maxTurns" in options && options.maxTurns !== undefined ? options.maxTurns : d.maxTurns, - env: "env" in options && options.env !== undefined ? options.env : d.env, - timeoutMs: - "timeoutMs" in options && options.timeoutMs !== undefined ? options.timeoutMs : d.timeoutMs, - }; -} diff --git a/packages/workflow-utils/src/index.ts b/packages/workflow-utils/src/index.ts index 70ca571..772a5ec 100644 --- a/packages/workflow-utils/src/index.ts +++ b/packages/workflow-utils/src/index.ts @@ -1,17 +1,15 @@ // Primary API — role factory templates -export { - createCursorRole, - createHermesRole, - createLlmRole, - createReActRole, -} from "./role-factories.js"; +export { createCursorRole } from "./role-cursor.js"; +export { createHermesRole } from "./role-hermes.js"; +export { createLlmRole } from "./role-llm.js"; +export { createReActRole } from "./role-react.js"; export { nerveAgentContext, readNerveYaml, type NerveYamlError, type ReadNerveYamlOptions, -} from "./context.js"; -export { isDryRun } from "./start-step.js"; +} from "./shared/context.js"; +export { isDryRun } from "./role-types.js"; export { nerveCommandEnv, spawnSafe, @@ -19,8 +17,8 @@ export { type SpawnError, type SpawnResult, type SpawnSafeOptions, -} from "./spawn-safe.js"; -export type { LlmError, LlmProvider } from "./llm-extract.js"; +} from "./shared/spawn-safe.js"; +export type { LlmError, LlmProvider } from "./shared/llm-extract.js"; export type { CliPromptFn, CursorRoleDefaults, @@ -35,4 +33,4 @@ export type { ReActRoleRequired, ReActTool, } from "./role-types.js"; -export type { LlmChatError } from "./llm-chat.js"; +export type { LlmChatError } from "./shared/llm-chat.js"; diff --git a/packages/workflow-utils/src/role-cursor.ts b/packages/workflow-utils/src/role-cursor.ts new file mode 100644 index 0000000..8522e58 --- /dev/null +++ b/packages/workflow-utils/src/role-cursor.ts @@ -0,0 +1,72 @@ +import type { Role } from "@uncaged/nerve-core"; + +import type { CursorRoleDefaults, CursorRoleRequired } from "./role-types.js"; +import { isDryRun } from "./role-types.js"; +import { type CursorAgentMode, cursorAgent } from "./shared/cursor-agent.js"; +import { formatLlmError } from "./shared/format-error.js"; +import { llmExtract } from "./shared/llm-extract.js"; +import type { SpawnEnv } from "./shared/spawn-safe.js"; + +const CURSOR_DEFAULTS: CursorRoleDefaults = { + mode: "default", + model: "auto", + env: {}, + timeoutMs: 300_000, +}; + +function pick(opts: Record, key: string, fallback: T): T { + if (key in opts && opts[key] !== undefined) { + return opts[key] as T; + } + return fallback; +} + +/** + * `cursor-agent` + `llmExtract` to produce `RoleResult`. CLI agent returns + * a single string; structured meta is read via a cheap follow-up `llmExtract`. + */ +export function createCursorRole( + options: CursorRoleRequired & Partial, +): Role { + return async (start, _messages) => { + const dry = isDryRun(start); + const d = CURSOR_DEFAULTS; + const mode = pick(options as Record, "mode", d.mode); + const model = pick(options as Record, "model", d.model); + const env = pick(options as Record, "env", d.env); + const timeoutMs = pick(options as Record, "timeoutMs", d.timeoutMs); + const prompt = await options.prompt(start.meta.threadId); + const run = await cursorAgent({ + prompt, + mode, + model, + cwd: options.cwd, + env: Object.keys(env).length === 0 ? null : env, + timeoutMs, + dryRun: dry, + }); + if (!run.ok) { + const e = run.error; + if (e.kind === "non_zero_exit") { + throw new Error( + `cursor-agent: exitCode=${e.exitCode} stdout=${e.stdout} stderr=${e.stderr}`, + ); + } + if (e.kind === "timeout") { + throw new Error("cursor-agent: timeout"); + } + throw new Error(`cursor-agent: ${e.message}`); + } + const text = run.value; + const metaR = await llmExtract({ + text, + schema: options.extract.schema, + provider: options.extract.provider, + dryRun: dry, + }); + if (!metaR.ok) { + throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); + } + return { content: text, meta: metaR.value }; + }; +} diff --git a/packages/workflow-utils/src/role-factories.ts b/packages/workflow-utils/src/role-factories.ts deleted file mode 100644 index 707fddd..0000000 --- a/packages/workflow-utils/src/role-factories.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { Role } from "@uncaged/nerve-core"; - -import { cursorAgent } from "./cursor-agent.js"; -import type { CursorAgentMode } from "./cursor-agent.js"; -import { hermesAgent } from "./hermes-agent.js"; -import { resolveHermesOptions } from "./hermes-options.js"; -import { type LlmChatError, chatCompletionText, reActIterativeChat } from "./llm-chat.js"; -import { type LlmError, llmExtract } from "./llm-extract.js"; -import type { - CursorRoleDefaults, - CursorRoleRequired, - HermesRoleDefaults, - HermesRoleRequired, - LlmMessage, - LlmRoleRequired, - ReActRoleDefaults, - ReActRoleRequired, -} from "./role-types.js"; -import type { SpawnEnv } from "./spawn-safe.js"; -import { isDryRun } from "./start-step.js"; - -const CURSOR_DEFAULTS: CursorRoleDefaults = { - mode: "default", - model: "auto", - env: {}, - timeoutMs: 300_000, -}; - -const REACT_DEFAULTS: ReActRoleDefaults = { - maxIterations: 10, -}; - -function mergeMode( - o: CursorRoleRequired & Partial, - d: CursorRoleDefaults, -): CursorAgentMode { - if ("mode" in o && o.mode !== undefined) { - return o.mode; - } - return d.mode; -} - -function mergeCursorModel( - o: CursorRoleRequired & Partial, - d: CursorRoleDefaults, -): string { - if ("model" in o && o.model !== undefined) { - return o.model; - } - return d.model; -} - -function mergeCursorEnv( - o: CursorRoleRequired & Partial, - d: CursorRoleDefaults, -): SpawnEnv { - if ("env" in o && o.env !== undefined) { - return o.env; - } - return d.env; -} - -function mergeCursorTimeout( - o: CursorRoleRequired & Partial, - d: CursorRoleDefaults, -): number { - if ("timeoutMs" in o && o.timeoutMs !== undefined) { - return o.timeoutMs; - } - return d.timeoutMs; -} - -function formatLlmError(e: LlmError | LlmChatError): string { - return JSON.stringify(e); -} - -/** - * `cursor-agent` + `llmExtract` to produce `RoleResult`. CLI agent returns - * a single string; structured meta is read via a cheap follow-up `llmExtract`. - */ -export function createCursorRole( - options: CursorRoleRequired & Partial, -): Role { - return async (start, _messages) => { - const dry = isDryRun(start); - const d = CURSOR_DEFAULTS; - const mode = mergeMode(options, d); - const model = mergeCursorModel(options, d); - const env = mergeCursorEnv(options, d); - const timeoutMs = mergeCursorTimeout(options, d); - const prompt = await options.prompt(start.meta.threadId); - const run = await cursorAgent({ - prompt, - mode, - model, - cwd: options.cwd, - env: Object.keys(env).length === 0 ? null : env, - timeoutMs, - dryRun: dry, - }); - if (!run.ok) { - const e = run.error; - if (e.kind === "non_zero_exit") { - throw new Error( - `cursor-agent: exitCode=${e.exitCode} stdout=${e.stdout} stderr=${e.stderr}`, - ); - } - if (e.kind === "timeout") { - throw new Error("cursor-agent: timeout"); - } - throw new Error(`cursor-agent: ${e.message}`); - } - const text = run.value; - const metaR = await llmExtract({ - text, - schema: options.extract.schema, - provider: options.extract.provider, - dryRun: dry, - }); - if (!metaR.ok) { - throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); - } - return { content: text, meta: metaR.value }; - }; -} - -export function createHermesRole( - options: HermesRoleRequired & Partial, -): Role { - return async (start, _messages) => { - const dry = isDryRun(start); - const h = resolveHermesOptions(options); - const prompt = await options.prompt(start.meta.threadId); - const run = await hermesAgent({ - prompt, - model: h.model, - provider: h.provider, - skills: h.skills, - quiet: h.quiet, - maxTurns: h.maxTurns, - env: Object.keys(h.env).length === 0 ? null : h.env, - timeoutMs: h.timeoutMs, - dryRun: dry, - }); - if (!run.ok) { - const e = run.error; - if (e.kind === "non_zero_exit") { - throw new Error(`hermes: exitCode=${e.exitCode} stdout=${e.stdout} stderr=${e.stderr}`); - } - if (e.kind === "timeout") { - throw new Error("hermes: timeout"); - } - throw new Error(`hermes: ${e.message}`); - } - const text = run.value; - const metaR = await llmExtract({ - text, - schema: options.extract.schema, - provider: options.extract.provider, - dryRun: dry, - }); - if (!metaR.ok) { - throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); - } - return { content: text, meta: metaR.value }; - }; -} - -export function createLlmRole(options: LlmRoleRequired): Role { - return async (start, _messages) => { - const dry = isDryRun(start); - const messages: LlmMessage[] = await options.prompt(start.meta.threadId); - const result = await chatCompletionText({ provider: options.provider, messages }); - if (!result.ok) { - throw new Error(`llm: ${formatLlmError(result.error)}`); - } - const text = result.value; - const metaR = await llmExtract({ - text, - schema: options.extract.schema, - provider: options.extract.provider, - dryRun: dry, - }); - if (!metaR.ok) { - throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); - } - return { content: text, meta: metaR.value }; - }; -} - -export function createReActRole( - options: ReActRoleRequired & Partial, -): Role { - return async (start, _messages) => { - const dry = isDryRun(start); - const def = REACT_DEFAULTS; - const maxIt = - "maxIterations" in options && options.maxIterations !== undefined - ? options.maxIterations - : def.maxIterations; - const messages: LlmMessage[] = await options.prompt(start.meta.threadId); - const result = await reActIterativeChat({ - provider: options.provider, - tools: options.tools, - messages, - maxIterations: maxIt, - }); - if (!result.ok) { - throw new Error(`react: ${formatLlmError(result.error)}`); - } - const text = result.value; - const metaR = await llmExtract({ - text, - schema: options.extract.schema, - provider: options.extract.provider, - dryRun: dry, - }); - if (!metaR.ok) { - throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); - } - return { content: text, meta: metaR.value }; - }; -} diff --git a/packages/workflow-utils/src/role-hermes.ts b/packages/workflow-utils/src/role-hermes.ts new file mode 100644 index 0000000..0e68ae5 --- /dev/null +++ b/packages/workflow-utils/src/role-hermes.ts @@ -0,0 +1,49 @@ +import type { Role } from "@uncaged/nerve-core"; + +import type { HermesRoleDefaults, HermesRoleRequired } from "./role-types.js"; +import { isDryRun } from "./role-types.js"; +import { formatLlmError } from "./shared/format-error.js"; +import { hermesAgent, resolveHermesOptions } from "./shared/hermes-agent.js"; +import { llmExtract } from "./shared/llm-extract.js"; + +export function createHermesRole( + options: HermesRoleRequired & Partial, +): Role { + return async (start, _messages) => { + const dry = isDryRun(start); + const h = resolveHermesOptions(options); + const prompt = await options.prompt(start.meta.threadId); + const run = await hermesAgent({ + prompt, + model: h.model, + provider: h.provider, + skills: h.skills, + quiet: h.quiet, + maxTurns: h.maxTurns, + env: Object.keys(h.env).length === 0 ? null : h.env, + timeoutMs: h.timeoutMs, + dryRun: dry, + }); + if (!run.ok) { + const e = run.error; + if (e.kind === "non_zero_exit") { + throw new Error(`hermes: exitCode=${e.exitCode} stdout=${e.stdout} stderr=${e.stderr}`); + } + if (e.kind === "timeout") { + throw new Error("hermes: timeout"); + } + throw new Error(`hermes: ${e.message}`); + } + const text = run.value; + const metaR = await llmExtract({ + text, + schema: options.extract.schema, + provider: options.extract.provider, + dryRun: dry, + }); + if (!metaR.ok) { + throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); + } + return { content: text, meta: metaR.value }; + }; +} diff --git a/packages/workflow-utils/src/role-llm.ts b/packages/workflow-utils/src/role-llm.ts new file mode 100644 index 0000000..f8a5709 --- /dev/null +++ b/packages/workflow-utils/src/role-llm.ts @@ -0,0 +1,29 @@ +import type { Role } from "@uncaged/nerve-core"; + +import type { LlmMessage, LlmRoleRequired } from "./role-types.js"; +import { isDryRun } from "./role-types.js"; +import { formatLlmError } from "./shared/format-error.js"; +import { chatCompletionText } from "./shared/llm-chat.js"; +import { llmExtract } from "./shared/llm-extract.js"; + +export function createLlmRole(options: LlmRoleRequired): Role { + return async (start, _messages) => { + const dry = isDryRun(start); + const messages: LlmMessage[] = await options.prompt(start.meta.threadId); + const result = await chatCompletionText({ provider: options.provider, messages }); + if (!result.ok) { + throw new Error(`llm: ${formatLlmError(result.error)}`); + } + const text = result.value; + const metaR = await llmExtract({ + text, + schema: options.extract.schema, + provider: options.extract.provider, + dryRun: dry, + }); + if (!metaR.ok) { + throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); + } + return { content: text, meta: metaR.value }; + }; +} diff --git a/packages/workflow-utils/src/role-react.ts b/packages/workflow-utils/src/role-react.ts new file mode 100644 index 0000000..d855217 --- /dev/null +++ b/packages/workflow-utils/src/role-react.ts @@ -0,0 +1,45 @@ +import type { Role } from "@uncaged/nerve-core"; + +import type { LlmMessage, ReActRoleDefaults, ReActRoleRequired } from "./role-types.js"; +import { isDryRun } from "./role-types.js"; +import { formatLlmError } from "./shared/format-error.js"; +import { reActIterativeChat } from "./shared/llm-chat.js"; +import { llmExtract } from "./shared/llm-extract.js"; + +const REACT_DEFAULTS: ReActRoleDefaults = { + maxIterations: 10, +}; + +export function createReActRole( + options: ReActRoleRequired & Partial, +): Role { + return async (start, _messages) => { + const dry = isDryRun(start); + const def = REACT_DEFAULTS; + const maxIt = + "maxIterations" in options && options.maxIterations !== undefined + ? options.maxIterations + : def.maxIterations; + const messages: LlmMessage[] = await options.prompt(start.meta.threadId); + const result = await reActIterativeChat({ + provider: options.provider, + tools: options.tools, + messages, + maxIterations: maxIt, + }); + if (!result.ok) { + throw new Error(`react: ${formatLlmError(result.error)}`); + } + const text = result.value; + const metaR = await llmExtract({ + text, + schema: options.extract.schema, + provider: options.extract.provider, + dryRun: dry, + }); + if (!metaR.ok) { + throw new Error(`llmExtract: ${formatLlmError(metaR.error)}`); + } + return { content: text, meta: metaR.value }; + }; +} diff --git a/packages/workflow-utils/src/role-types.ts b/packages/workflow-utils/src/role-types.ts index ca8c63a..6e97f0b 100644 --- a/packages/workflow-utils/src/role-types.ts +++ b/packages/workflow-utils/src/role-types.ts @@ -1,7 +1,13 @@ +import type { StartStep } from "@uncaged/nerve-core"; import type { z } from "zod"; -import type { LlmProvider } from "./llm-extract.js"; -import type { SpawnEnv } from "./spawn-safe.js"; +import type { LlmProvider } from "./shared/llm-extract.js"; +import type { SpawnEnv } from "./shared/spawn-safe.js"; + +/** Returns the thread-level dry-run flag from the workflow start frame. */ +export function isDryRun(start: StartStep): boolean { + return start.meta.dryRun; +} export type CliPromptFn = (threadId: string) => Promise; diff --git a/packages/workflow-utils/src/context.ts b/packages/workflow-utils/src/shared/context.ts similarity index 100% rename from packages/workflow-utils/src/context.ts rename to packages/workflow-utils/src/shared/context.ts diff --git a/packages/workflow-utils/src/cursor-agent.ts b/packages/workflow-utils/src/shared/cursor-agent.ts similarity index 100% rename from packages/workflow-utils/src/cursor-agent.ts rename to packages/workflow-utils/src/shared/cursor-agent.ts diff --git a/packages/workflow-utils/src/shared/format-error.ts b/packages/workflow-utils/src/shared/format-error.ts new file mode 100644 index 0000000..e0a35d0 --- /dev/null +++ b/packages/workflow-utils/src/shared/format-error.ts @@ -0,0 +1,6 @@ +import type { LlmChatError } from "./llm-chat.js"; +import type { LlmError } from "./llm-extract.js"; + +export function formatLlmError(e: LlmError | LlmChatError): string { + return JSON.stringify(e); +} diff --git a/packages/workflow-utils/src/hermes-agent.ts b/packages/workflow-utils/src/shared/hermes-agent.ts similarity index 58% rename from packages/workflow-utils/src/hermes-agent.ts rename to packages/workflow-utils/src/shared/hermes-agent.ts index 0d76dd6..2cf9862 100644 --- a/packages/workflow-utils/src/hermes-agent.ts +++ b/packages/workflow-utils/src/shared/hermes-agent.ts @@ -1,5 +1,6 @@ import { type Result, ok } from "@uncaged/nerve-core"; +import type { HermesRoleDefaults, HermesRoleRequired } from "../role-types.js"; import { type SpawnEnv, type SpawnError, spawnSafe } from "./spawn-safe.js"; /** @@ -64,3 +65,33 @@ export async function hermesAgent( } return ok(run.value.stdout); } + +// --- Hermes options resolution (absorbed from hermes-options.ts) --- + +const HERMES_DEFAULTS: HermesRoleDefaults = { + model: null, + provider: null, + skills: [], + quiet: true, + maxTurns: 90, + env: {}, + timeoutMs: 600_000, +}; + +export function resolveHermesOptions( + options: HermesRoleRequired & Partial, +): HermesRoleDefaults { + const d = HERMES_DEFAULTS; + return { + model: "model" in options && options.model !== undefined ? options.model : d.model, + provider: + "provider" in options && options.provider !== undefined ? options.provider : d.provider, + skills: "skills" in options && options.skills !== undefined ? options.skills : d.skills, + quiet: "quiet" in options && options.quiet !== undefined ? options.quiet : d.quiet, + maxTurns: + "maxTurns" in options && options.maxTurns !== undefined ? options.maxTurns : d.maxTurns, + env: "env" in options && options.env !== undefined ? options.env : d.env, + timeoutMs: + "timeoutMs" in options && options.timeoutMs !== undefined ? options.timeoutMs : d.timeoutMs, + }; +} diff --git a/packages/workflow-utils/src/llm-chat.ts b/packages/workflow-utils/src/shared/llm-chat.ts similarity index 99% rename from packages/workflow-utils/src/llm-chat.ts rename to packages/workflow-utils/src/shared/llm-chat.ts index e74017a..5af9888 100644 --- a/packages/workflow-utils/src/llm-chat.ts +++ b/packages/workflow-utils/src/shared/llm-chat.ts @@ -1,8 +1,8 @@ import { type Result, err, ok } from "@uncaged/nerve-core"; import { toJSONSchema } from "zod"; +import type { LlmMessage, ReActTool } from "../role-types.js"; import type { LlmProvider } from "./llm-extract.js"; -import type { LlmMessage, ReActTool } from "./role-types.js"; type OpenAiMessage = | { role: "system" | "user" | "assistant"; content: string } diff --git a/packages/workflow-utils/src/llm-extract.ts b/packages/workflow-utils/src/shared/llm-extract.ts similarity index 100% rename from packages/workflow-utils/src/llm-extract.ts rename to packages/workflow-utils/src/shared/llm-extract.ts diff --git a/packages/workflow-utils/src/schema-defaults.ts b/packages/workflow-utils/src/shared/schema-defaults.ts similarity index 100% rename from packages/workflow-utils/src/schema-defaults.ts rename to packages/workflow-utils/src/shared/schema-defaults.ts diff --git a/packages/workflow-utils/src/spawn-safe.ts b/packages/workflow-utils/src/shared/spawn-safe.ts similarity index 100% rename from packages/workflow-utils/src/spawn-safe.ts rename to packages/workflow-utils/src/shared/spawn-safe.ts diff --git a/packages/workflow-utils/src/start-step.ts b/packages/workflow-utils/src/start-step.ts deleted file mode 100644 index 2723c20..0000000 --- a/packages/workflow-utils/src/start-step.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { StartStep } from "@uncaged/nerve-core"; - -/** Returns the thread-level dry-run flag from the workflow start frame. */ -export function isDryRun(start: StartStep): boolean { - return start.meta.dryRun; -}