diff --git a/.cursor/rules/no-dynamic-import.mdc b/.cursor/rules/no-dynamic-import.mdc index 5378994..1cf0b50 100644 --- a/.cursor/rules/no-dynamic-import.mdc +++ b/.cursor/rules/no-dynamic-import.mdc @@ -20,7 +20,7 @@ Always use static top-level `import` statements. ## Exceptions (must include a comment explaining why) 1. **`sense-runtime.ts`** — loads user-authored sense modules whose paths are only known at runtime -2. **`workflow-worker.ts`** — loads user-authored workflow modules whose paths are only known at runtime +2. **`packages/workflow/src/worker.ts`** — loads user-authored workflow modules whose paths are only known at runtime When suppressing, add a comment directly above: diff --git a/docs/plans/2026-05-05-extract-workflow-package.md b/docs/plans/2026-05-05-extract-workflow-package.md new file mode 100644 index 0000000..c7727e2 --- /dev/null +++ b/docs/plans/2026-05-05-extract-workflow-package.md @@ -0,0 +1,496 @@ +# Extract Workflow Engine into `@uncaged/workflow` — Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Extract the workflow engine (types, runtime, IPC, manager) from nerve-core and nerve-daemon into a standalone `@uncaged/workflow` package. + +**Architecture:** Create `packages/workflow/` as a new pnpm workspace package. Move workflow types from core and workflow runtime from daemon into it. The daemon becomes a consumer of `@uncaged/workflow`. No backward-compat re-exports — breaking change, update all consumers in one shot. + +**Tech Stack:** TypeScript, pnpm workspace, rslib (bundler, same as other packages), Biome + +**Ref:** Fixes #320 + +--- + +## Phase 1: Scaffold `packages/workflow/` + +### Task 1: Create package skeleton + +**Objective:** Create the `@uncaged/workflow` package with package.json, tsconfig, rslib config. + +**Files:** +- Create: `packages/workflow/package.json` +- Create: `packages/workflow/tsconfig.json` +- Create: `packages/workflow/rslib.config.ts` +- Create: `packages/workflow/src/index.ts` (empty barrel) + +**Steps:** + +1. Copy `packages/workflow-utils/package.json` as template, change name to `@uncaged/workflow`, remove all dependencies except dev deps (typescript, rslib, etc.). No runtime deps initially. + +2. Copy `packages/workflow-utils/tsconfig.json` and `rslib.config.ts` as-is (same monorepo conventions). + +3. Create empty `packages/workflow/src/index.ts`: +```typescript +// @uncaged/workflow — standalone workflow orchestration engine +``` + +4. Verify: +```bash +cd packages/workflow && pnpm install && pnpm run build +``` + +5. Commit: +```bash +git add packages/workflow/ +git commit -m "chore(workflow): scaffold @uncaged/workflow package + +Refs #320" +``` + +--- + +## Phase 2: Move Types from Core + +### Task 2: Move `workflow.ts` types to `@uncaged/workflow` + +**Objective:** Move all workflow types and constants from `packages/core/src/workflow.ts` → `packages/workflow/src/types.ts`. + +**Files:** +- Create: `packages/workflow/src/types.ts` +- Modify: `packages/workflow/src/index.ts` + +**Steps:** + +1. Copy `packages/core/src/workflow.ts` → `packages/workflow/src/types.ts` verbatim (all 83 lines — `START`, `END`, `DEFAULT_ENGINE_MAX_ROUNDS`, all types). + +2. Export everything from `packages/workflow/src/index.ts`: +```typescript +export { + START, + END, + DEFAULT_ENGINE_MAX_ROUNDS, +} from "./types.js"; +export type { + WorkflowMessage, + RoleResult, + Role, + RoleMeta, + StartStep, + ThreadContext, + WorkflowContext, + AgentFn, + RoleStep, + ModeratorContext, + Moderator, + WorkflowDefinition, +} from "./types.js"; +``` + +3. Build to verify types compile: +```bash +cd packages/workflow && pnpm run build +``` + +4. Commit: +```bash +git commit -am "refactor(workflow): move workflow types from core to @uncaged/workflow + +Refs #320" +``` + +### Task 3: Move `WorkflowConfig` to `@uncaged/workflow` + +**Objective:** Move the `WorkflowConfig` type (and its constituent types `DropOverflowConfig`, `QueueOverflowConfig`) from core/config.ts to the workflow package. + +**Files:** +- Create: `packages/workflow/src/config.ts` +- Modify: `packages/workflow/src/index.ts` +- Modify: `packages/core/src/config.ts` — remove `WorkflowConfig`, `DropOverflowConfig`, `QueueOverflowConfig`; import from `@uncaged/workflow` instead + +**Steps:** + +1. Create `packages/workflow/src/config.ts` with the three types: +```typescript +export type DropOverflowConfig = { + concurrency: number; + overflow: "drop"; +}; + +export type QueueOverflowConfig = { + concurrency: number; + overflow: "queue"; + maxQueue: number; +}; + +export type WorkflowConfig = DropOverflowConfig | QueueOverflowConfig; +``` + +2. Re-export from `packages/workflow/src/index.ts`. + +3. In `packages/core/package.json`, add dependency: `"@uncaged/workflow": "workspace:*"`. + +4. In `packages/core/src/config.ts`: + - Remove the three type definitions + - Add `import type { WorkflowConfig, DropOverflowConfig, QueueOverflowConfig } from "@uncaged/workflow";` + - Keep the re-export from `packages/core/src/index.ts` pointing to config.ts (which now re-exports from workflow) + +5. Build entire workspace: +```bash +pnpm run build +``` + +6. Commit: +```bash +git commit -am "refactor(workflow): move WorkflowConfig types to @uncaged/workflow + +Refs #320" +``` + +### Task 4: Remove workflow types from core, update core exports + +**Objective:** Delete `packages/core/src/workflow.ts` entirely. Core re-exports workflow types from `@uncaged/workflow`. + +**Files:** +- Delete: `packages/core/src/workflow.ts` +- Modify: `packages/core/src/index.ts` — change workflow exports to re-export from `@uncaged/workflow` +- Modify: `packages/core/src/config.ts` — remove import of `DEFAULT_ENGINE_MAX_ROUNDS` from deleted file + +**Steps:** + +1. In `packages/core/src/config.ts`, replace: + ```typescript + import { DEFAULT_ENGINE_MAX_ROUNDS } from "./workflow.js"; + ``` + with: + ```typescript + import { DEFAULT_ENGINE_MAX_ROUNDS } from "@uncaged/workflow"; + ``` + +2. In `packages/core/src/index.ts`, replace all workflow.js imports with re-exports from `@uncaged/workflow`: + ```typescript + // Workflow types — re-exported from @uncaged/workflow + export { + START, END, DEFAULT_ENGINE_MAX_ROUNDS, + } from "@uncaged/workflow"; + export type { + WorkflowMessage, RoleResult, Role, RoleMeta, StartStep, + ThreadContext, WorkflowContext, AgentFn, RoleStep, + ModeratorContext, Moderator, WorkflowDefinition, + } from "@uncaged/workflow"; + ``` + +3. Delete `packages/core/src/workflow.ts`. + +4. Full build + test: +```bash +pnpm run build && pnpm test +``` + +5. Commit: +```bash +git commit -am "refactor(core): remove workflow.ts, re-export from @uncaged/workflow + +Refs #320" +``` + +--- + +## Phase 3: Move Workflow IPC Messages + +### Task 5: Extract workflow IPC types to `@uncaged/workflow` + +**Objective:** Move workflow-related IPC message types from `packages/daemon/src/ipc.ts` to `packages/workflow/src/ipc.ts`. Sense IPC stays in daemon. + +**Files:** +- Create: `packages/workflow/src/ipc.ts` +- Modify: `packages/workflow/src/index.ts` +- Modify: `packages/daemon/src/ipc.ts` — remove workflow IPC types, import from `@uncaged/workflow` + +**Steps:** + +1. Extract to `packages/workflow/src/ipc.ts`: + - `StartThreadMessage`, `ResumeThreadMessage`, `KillThreadMessage` + - `ThreadLifecycleEvent`, `ThreadEventMessage`, `WorkflowErrorMessage`, `ThreadWorkflowMessage` + - Workflow-related validation logic from `parseParentToWorkerMessage` + - Union type for workflow parent→worker messages + +2. Keep in daemon `ipc.ts`: + - `ComputeMessage`, `ShutdownMessage`, `HealthRequestMessage` + - Sense-related worker→parent messages + - The combined `ParentToWorkerMessage` union (imports workflow types from `@uncaged/workflow`) + +3. Add `@uncaged/workflow` as dependency to `packages/daemon/package.json`. + +4. Build + test: +```bash +pnpm run build && pnpm test +``` + +5. Commit: +```bash +git commit -am "refactor(workflow): move workflow IPC types to @uncaged/workflow + +Refs #320" +``` + +--- + +## Phase 4: Move Workflow Runtime + +### Task 6: Move `workflow-worker.ts` to `@uncaged/workflow` + +**Objective:** Move workflow execution runtime (the worker that runs inside a child process) from daemon to the workflow package. + +**Files:** +- Move: `packages/daemon/src/workflow-worker.ts` → `packages/workflow/src/worker.ts` +- Modify: `packages/workflow/src/index.ts` +- Modify: `packages/daemon/` — update worker spawn path + +**Steps:** + +1. Copy `workflow-worker.ts` to `packages/workflow/src/worker.ts`. + +2. Update imports: replace `@uncaged/nerve-core` with local imports from `./types.js`, `./ipc.js`. + +3. Export the worker entry point or the worker file path from `@uncaged/workflow` so daemon can spawn it. + +4. In daemon, update worker spawn to reference `@uncaged/workflow`'s worker. + +5. Delete `packages/daemon/src/workflow-worker.ts`. + +6. Build + test: +```bash +pnpm run build && pnpm test +``` + +7. Commit: +```bash +git commit -am "refactor(workflow): move workflow-worker runtime to @uncaged/workflow + +Refs #320" +``` + +### Task 7: Move `workflow-manager.ts` and `workflow-manager-support.ts` to `@uncaged/workflow` + +**Objective:** Move workflow process management from daemon to workflow package. + +**Files:** +- Move: `packages/daemon/src/workflow-manager.ts` → `packages/workflow/src/manager.ts` +- Move: `packages/daemon/src/workflow-manager-support.ts` → `packages/workflow/src/manager-support.ts` +- Modify: `packages/daemon/src/kernel.ts` — import from `@uncaged/workflow` + +**Steps:** + +1. Copy both files to `packages/workflow/src/`. + +2. Update imports to use local paths within the workflow package. + +3. Export manager creation function from `packages/workflow/src/index.ts`. + +4. Update `packages/daemon/src/kernel.ts` to import workflow manager from `@uncaged/workflow`. + +5. Delete the original files from daemon. + +6. Build + test: +```bash +pnpm run build && pnpm test +``` + +7. Commit: +```bash +git commit -am "refactor(workflow): move workflow-manager to @uncaged/workflow + +Refs #320" +``` + +--- + +## Phase 5: Update All Consumers + +### Task 8: Update `workflow-utils` to depend on `@uncaged/workflow` + +**Objective:** Change `@uncaged/nerve-workflow-utils` to import workflow types from `@uncaged/workflow` instead of `@uncaged/nerve-core`. + +**Files:** +- Modify: `packages/workflow-utils/package.json` — replace `@uncaged/nerve-core` dep with `@uncaged/workflow` +- Modify: all `packages/workflow-utils/src/*.ts` — update import paths + +**Steps:** + +1. In `package.json`, replace `"@uncaged/nerve-core": "workspace:*"` with `"@uncaged/workflow": "workspace:*"`. + +2. Find-and-replace all `from "@uncaged/nerve-core"` → `from "@uncaged/workflow"` in `packages/workflow-utils/src/`. + +3. Build + test: +```bash +pnpm run build && pnpm test +``` + +4. Commit: +```bash +git commit -am "refactor(workflow-utils): import from @uncaged/workflow + +Refs #320" +``` + +### Task 9: Update `workflow-meta` to depend on `@uncaged/workflow` + +**Objective:** Same as Task 8 but for `packages/workflow-meta/`. + +**Files:** +- Modify: `packages/workflow-meta/package.json` +- Modify: all `packages/workflow-meta/src/**/*.ts` + +**Steps:** Same pattern as Task 8. + +### Task 10: Update adapter packages + +**Objective:** Update `adapter-cursor` and `adapter-hermes` to import workflow types from `@uncaged/workflow`. + +**Files:** +- Modify: `packages/adapter-cursor/src/index.ts` +- Modify: `packages/adapter-cursor/package.json` +- Modify: `packages/adapter-hermes/src/index.ts` +- Modify: `packages/adapter-hermes/package.json` + +**Steps:** Same pattern — add `@uncaged/workflow` dep, update imports. + +### Task 11: Update CLI package + +**Objective:** Update CLI to import workflow types from `@uncaged/workflow`. + +**Files:** +- Modify: `packages/cli/package.json` +- Modify: `packages/cli/src/commands/workflow.ts` +- Modify: `packages/cli/src/commands/thread.ts` +- Modify: `packages/cli/src/commands/create.ts` +- Modify: `packages/cli/src/workflow-agent-validation.ts` +- Modify: other files that import workflow types from nerve-core + +**Steps:** + +1. Add `"@uncaged/workflow": "workspace:*"` to `packages/cli/package.json`. + +2. In each file, change workflow-related imports from `@uncaged/nerve-core` to `@uncaged/workflow`. + +3. Build + test: +```bash +pnpm run build && pnpm test +``` + +4. Commit: +```bash +git commit -am "refactor(cli): import workflow types from @uncaged/workflow + +Refs #320" +``` + +--- + +## Phase 6: Clean Up Core Re-exports + +### Task 12: Remove workflow re-exports from `@uncaged/nerve-core` + +**Objective:** Core no longer re-exports any workflow types. All consumers import directly from `@uncaged/workflow`. + +**Files:** +- Modify: `packages/core/src/index.ts` — remove all workflow re-exports +- Modify: `packages/core/package.json` — remove `@uncaged/workflow` dependency (core no longer needs it) + +**Steps:** + +1. Remove all workflow-related `export` lines from `packages/core/src/index.ts`. + +2. Remove `@uncaged/workflow` from core's dependencies. + +3. Full build + full test: +```bash +pnpm run build && pnpm test +``` + +4. Run biome check: +```bash +pnpm run check +``` + +5. Commit: +```bash +git commit -am "refactor(core): remove workflow re-exports, clean break + +Fixes #320" +``` + +--- + +## Phase 7: Verify & PR + +### Task 13: Final verification + +**Objective:** Full workspace build, all tests pass, biome clean. + +**Steps:** + +1. Clean build: +```bash +pnpm run build +``` + +2. Full tests: +```bash +pnpm test +``` + +3. Biome: +```bash +pnpm run check +``` + +4. Verify monorepo structure matches issue spec: +``` +packages/ + core/ # @uncaged/nerve-core — sense types, config (no workflow) + workflow/ # @uncaged/workflow — standalone orchestration engine + workflow-utils/ # helper roles, extract layer + workflow-meta/ # meta-workflows + daemon/ # @uncaged/nerve-daemon — sense engine + workflow integration + cli/ # @uncaged/nerve-cli +``` + +5. Create PR: +```bash +tea pr create --title "refactor: extract workflow engine into @uncaged/workflow" \ + --description "## What +Extract workflow engine into standalone @uncaged/workflow package. + +## Why +Workflow engine is now independent of sense observation — can be used standalone. + +## Changes +- packages/workflow/ — new package with types, IPC, worker, manager +- packages/core/ — removed workflow types, no longer re-exports them +- packages/daemon/ — imports workflow runtime from @uncaged/workflow +- packages/cli/ — imports workflow types from @uncaged/workflow +- packages/workflow-utils/ — depends on @uncaged/workflow +- packages/workflow-meta/ — depends on @uncaged/workflow +- packages/adapter-*/ — depends on @uncaged/workflow + +## Ref +Fixes #320" +``` + +--- + +## Pitfalls & Notes + +1. **Worker spawn path**: `workflow-worker.ts` is spawned as a child process via `node`. After moving to `@uncaged/workflow`, the daemon needs to resolve the worker entry point from the package (e.g. `require.resolve("@uncaged/workflow/worker")`). This likely requires a separate export in `package.json` exports map. + +2. **Dynamic import exception**: `workflow-worker.ts` uses dynamic `import()` for user workflow modules — this exception carries over to the new package. Add comment per CLAUDE.md conventions. + +3. **IPC split**: The `ipc.ts` in daemon has both sense and workflow messages in one file with shared validation. The split needs careful handling of the `ParentToWorkerMessage` union type and `parseParentToWorkerMessage` function. + +4. **No backward compat**: Per user preference, no deprecated re-exports — straight breaking change. Phase 6 removes re-exports from core entirely. + +5. **`workflow-utils` may still need `nerve-core`**: If workflow-utils imports non-workflow types (like `Schema`, `ExtractFn`, `ExtractError`) from core, it will need both deps. Check carefully. + +6. **Test files**: Many test files in daemon import workflow types. They need updating in Phase 5 alongside the source files. diff --git a/package.json b/package.json index 064f41b..15c974a 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,15 @@ "engines": { "node": ">=22.5.0" }, + "pnpm": { + "overrides": { + "@uncaged/nerve-core": "workspace:*", + "@uncaged/nerve-store": "workspace:*" + } + }, "scripts": { "prepare": "husky", - "build": "pnpm -r run build", + "build": "pnpm --filter @uncaged/workflow run build:public-types && pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-store run build && pnpm --filter @uncaged/workflow run build && pnpm -r --filter '!@uncaged/nerve-core' --filter '!@uncaged/nerve-store' --filter '!@uncaged/workflow' run build", "test": "pnpm -r test", "check": "biome check .", "format": "biome format --write .", diff --git a/packages/adapter-cursor/package.json b/packages/adapter-cursor/package.json index b42e498..dd77846 100644 --- a/packages/adapter-cursor/package.json +++ b/packages/adapter-cursor/package.json @@ -14,7 +14,8 @@ "test": "vitest run --passWithNoTests" }, "dependencies": { - "@uncaged/nerve-core": "workspace:*" + "@uncaged/nerve-core": "workspace:*", + "@uncaged/workflow": "workspace:*" }, "devDependencies": { "@rslib/core": "^0.21.3", diff --git a/packages/adapter-cursor/src/index.ts b/packages/adapter-cursor/src/index.ts index a9bf382..778f154 100644 --- a/packages/adapter-cursor/src/index.ts +++ b/packages/adapter-cursor/src/index.ts @@ -1,5 +1,6 @@ -import type { AgentConfig, AgentFn, ThreadContext } from "@uncaged/nerve-core"; +import type { AgentConfig } from "@uncaged/nerve-core"; import { type Result, type SpawnEnv, type SpawnError, ok, spawnSafe } from "@uncaged/nerve-core"; +import type { AgentFn, ThreadContext } from "@uncaged/workflow"; export type CursorAgentMode = "plan" | "ask" | "default"; diff --git a/packages/adapter-hermes/package.json b/packages/adapter-hermes/package.json index f7cda3e..f48867a 100644 --- a/packages/adapter-hermes/package.json +++ b/packages/adapter-hermes/package.json @@ -14,7 +14,8 @@ "test": "vitest run --passWithNoTests" }, "dependencies": { - "@uncaged/nerve-core": "workspace:*" + "@uncaged/nerve-core": "workspace:*", + "@uncaged/workflow": "workspace:*" }, "devDependencies": { "@rslib/core": "^0.21.3", diff --git a/packages/adapter-hermes/src/index.ts b/packages/adapter-hermes/src/index.ts index 085c85a..2ca9e1d 100644 --- a/packages/adapter-hermes/src/index.ts +++ b/packages/adapter-hermes/src/index.ts @@ -1,5 +1,6 @@ -import type { AgentConfig, AgentFn, ThreadContext } from "@uncaged/nerve-core"; +import type { AgentConfig } from "@uncaged/nerve-core"; import { type Result, type SpawnEnv, type SpawnError, ok, spawnSafe } from "@uncaged/nerve-core"; +import type { AgentFn, ThreadContext } from "@uncaged/workflow"; /** * Spawns a non-interactive `hermes chat` invocation with YOLO enabled, argv-only diff --git a/packages/cli/package.json b/packages/cli/package.json index 60edfe2..4585f1e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,12 +17,13 @@ "scripts": { "prepublishOnly": "bash ../../scripts/prepublish-check.sh", "build": "rslib build", - "pretest": "pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-daemon run build", + "pretest": "pnpm --filter @uncaged/workflow run build:public-types && pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-store run build && pnpm --filter @uncaged/workflow run build && pnpm --filter @uncaged/nerve-daemon run build", "test": "vitest run" }, "dependencies": { "@uncaged/nerve-core": "workspace:*", "@uncaged/nerve-store": "workspace:*", + "@uncaged/workflow": "workspace:*", "citty": "^0.1.6", "picomatch": "^4.0.2", "yaml": "^2.8.3" diff --git a/packages/cli/src/__tests__/create-workflow.test.ts b/packages/cli/src/__tests__/create-workflow.test.ts index e64fc07..7d2721a 100644 --- a/packages/cli/src/__tests__/create-workflow.test.ts +++ b/packages/cli/src/__tests__/create-workflow.test.ts @@ -27,10 +27,10 @@ describe("buildWorkflowScaffold", () => { expect(roleMainIndexTs).toContain("my-workflow started"); }); - it("root index contains WorkflowDefinition import from nerve-core", () => { + it("root index contains WorkflowDefinition import from @uncaged/workflow", () => { const { indexTs } = buildWorkflowScaffold("test"); expect(indexTs).toContain("WorkflowDefinition"); - expect(indexTs).toContain("@uncaged/nerve-core"); + expect(indexTs).toContain("@uncaged/workflow"); }); it("root index wires moderator with ThreadContext and END", () => { diff --git a/packages/cli/src/__tests__/e2e-create.test.ts b/packages/cli/src/__tests__/e2e-create.test.ts index e82de11..705fc66 100644 --- a/packages/cli/src/__tests__/e2e-create.test.ts +++ b/packages/cli/src/__tests__/e2e-create.test.ts @@ -2,9 +2,12 @@ * E2E-style tests for `nerve create workflow` and `nerve create sense`. */ +import { execFile } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; import { defineCommand, runCommand } from "citty"; import { afterEach, describe, expect, it } from "vitest"; @@ -12,6 +15,23 @@ import { afterEach, describe, expect, it } from "vitest"; import { createCommand } from "../commands/create.js"; import { initCommand } from "../commands/init.js"; +const execFileAsync = promisify(execFile); +const requireFromHere = createRequire(import.meta.url); + +/** + * Default init pins `@uncaged/workflow` to npm `latest`, but that package is not published yet. + * Install from the monorepo workspace so `pnpm run build` can resolve workflow types. + */ +async function installWorkspaceWithLocalWorkflow(nerveRoot: string): Promise { + const pkgPath = join(nerveRoot, "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { dependencies: Record }; + const wfRoot = dirname(requireFromHere.resolve("@uncaged/workflow/package.json")); + pkg.dependencies["@uncaged/workflow"] = `file:${wfRoot}`; + writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); + await execFileAsync("pnpm", ["install", "--no-frozen-lockfile"], { cwd: nerveRoot }); + await execFileAsync("pnpm", ["run", "build"], { cwd: nerveRoot }); +} + const testRootCommand = defineCommand({ meta: { name: "nerve", description: "e2e-create" }, subCommands: { @@ -128,7 +148,8 @@ describe("e2e create", () => { fakeHome = mkdtempSync(join(tmpdir(), "nerve-create-e2e-")); const nerveRoot = join(fakeHome, ".uncaged-nerve"); - await runTestCli(fakeHome, ["init", "--force"]); + await runTestCli(fakeHome, ["init", "--force", "--skip-install"]); + await installWorkspaceWithLocalWorkflow(nerveRoot); const wf = await runTestCli(fakeHome, ["create", "workflow", "e2e-flow"]); expect(wf.exitCode).toBe(0); @@ -153,7 +174,8 @@ describe("e2e create", () => { fakeHome = mkdtempSync(join(tmpdir(), "nerve-create-e2e-")); const nerveRoot = join(fakeHome, ".uncaged-nerve"); - await runTestCli(fakeHome, ["init", "--force"]); + await runTestCli(fakeHome, ["init", "--force", "--skip-install"]); + await installWorkspaceWithLocalWorkflow(nerveRoot); const sense = await runTestCli(fakeHome, ["create", "sense", "e2e-sense"]); expect(sense.exitCode).toBe(0); diff --git a/packages/cli/src/__tests__/e2e-harness.ts b/packages/cli/src/__tests__/e2e-harness.ts index 67355b7..1f39c0a 100644 --- a/packages/cli/src/__tests__/e2e-harness.ts +++ b/packages/cli/src/__tests__/e2e-harness.ts @@ -51,8 +51,9 @@ import { workflowCommand } from "../commands/workflow.js"; const require = createRequire(import.meta.url); const nerveDaemonRoot = dirname(require.resolve("@uncaged/nerve-daemon/package.json")); +const nerveWorkflowRoot = dirname(require.resolve("@uncaged/workflow/package.json")); const senseWorkerScript = join(nerveDaemonRoot, "dist", "sense-worker.js"); -const workflowWorkerScript = join(nerveDaemonRoot, "dist", "workflow-worker.js"); +const workflowWorkerScript = join(nerveWorkflowRoot, "dist", "worker.js"); const nerveYamlTemplate = `senses: counter: @@ -274,7 +275,7 @@ export async function startTestDaemon( } if (!existsSync(workflowWorkerScript)) { throw new Error( - `Missing "${workflowWorkerScript}". Run \`pnpm --filter @uncaged/nerve-daemon build\`.`, + `Missing "${workflowWorkerScript}". Run \`pnpm --filter @uncaged/workflow build\`.`, ); } diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index dde6d82..fb2b46a 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -31,8 +31,8 @@ export function buildWorkflowScaffold(name: string): WorkflowScaffoldFiles { } function buildWorkflowIndexTs(name: string): string { - return `import type { ThreadContext, WorkflowDefinition } from "@uncaged/nerve-core"; -import { END } from "@uncaged/nerve-core"; + return `import type { ThreadContext, WorkflowDefinition } from "@uncaged/workflow"; +import { END } from "@uncaged/workflow"; import { mainRole } from "./roles/main/index.js"; @@ -56,7 +56,7 @@ export default workflow; } function buildWorkflowMainRoleIndexTs(name: string): string { - return `import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + return `import type { RoleResult, ThreadContext } from "@uncaged/workflow"; /** * Main role — implement LLM calls, scripts, HTTP, etc. diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 48c530d..dd703f7 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -54,6 +54,7 @@ const PACKAGE_JSON = `${JSON.stringify( dependencies: { "@uncaged/nerve-core": "latest", "@uncaged/nerve-daemon": "latest", + "@uncaged/workflow": "latest", zod: "^4.3.6", }, devDependencies: { diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index 4c7cc31..f218709 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -1,7 +1,8 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { DEFAULT_ENGINE_MAX_ROUNDS, isPlainRecord, parseNerveConfig } from "@uncaged/nerve-core"; +import { isPlainRecord, parseNerveConfig } from "@uncaged/nerve-core"; +import { DEFAULT_ENGINE_MAX_ROUNDS } from "@uncaged/workflow"; import { defineCommand } from "citty"; import { stringify } from "yaml"; diff --git a/packages/cli/src/daemon-client.ts b/packages/cli/src/daemon-client.ts index 052dcfa..a81f936 100644 --- a/packages/cli/src/daemon-client.ts +++ b/packages/cli/src/daemon-client.ts @@ -20,12 +20,8 @@ import type { SenseInfo, WorkflowStatus, } from "@uncaged/nerve-core"; -import { - DEFAULT_ENGINE_MAX_ROUNDS, - isPlainRecord, - isSenseInfo, - isWorkflowStatus, -} from "@uncaged/nerve-core"; +import { isPlainRecord, isSenseInfo, isWorkflowStatus } from "@uncaged/nerve-core"; +import { DEFAULT_ENGINE_MAX_ROUNDS } from "@uncaged/workflow"; import { getCliDaemonApiToken, getCliDaemonHost } from "./cli-global.js"; import { HttpTransport } from "./http-transport.js"; diff --git a/packages/cli/src/http-transport.ts b/packages/cli/src/http-transport.ts index fa05511..5acdb52 100644 --- a/packages/cli/src/http-transport.ts +++ b/packages/cli/src/http-transport.ts @@ -6,12 +6,8 @@ import type { SenseInfo, WorkflowStatus, } from "@uncaged/nerve-core"; -import { - DEFAULT_ENGINE_MAX_ROUNDS, - isPlainRecord, - isSenseInfo, - isWorkflowStatus, -} from "@uncaged/nerve-core"; +import { isPlainRecord, isSenseInfo, isWorkflowStatus } from "@uncaged/nerve-core"; +import { DEFAULT_ENGINE_MAX_ROUNDS } from "@uncaged/workflow"; function normalizeBaseUrl(host: string): string { const t = host.trim(); diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index fb835d7..b33b7f6 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1,19 +1,11 @@ import { parse } from "yaml"; import { type Result, err, isPlainRecord, ok, parseDurationStringToMs } from "./util.js"; -import { DEFAULT_ENGINE_MAX_ROUNDS } from "./workflow.js"; - -export type SenseConfig = { - group: string; - throttle: number | null; - timeout: number | null; - gracePeriod: number | null; - /** Polling interval (ms). When set, the sense is triggered periodically. */ - interval: number | null; - /** Other sense names whose successful computes schedule this sense (kernel reverse-index). */ - on: string[]; -}; +/** + * Workflow queue/runtime limits parsed from nerve.yaml. + * Shapes match the standalone workflow package — core must not depend on it (#320). + */ export type DropOverflowConfig = { concurrency: number; overflow: "drop"; @@ -27,6 +19,20 @@ export type QueueOverflowConfig = { export type WorkflowConfig = DropOverflowConfig | QueueOverflowConfig; +/** Engine-wide fallback when nerve.yaml omits max_rounds (keep in sync with workflow package default). */ +export const DEFAULT_ENGINE_MAX_ROUNDS = 100; + +export type SenseConfig = { + group: string; + throttle: number | null; + timeout: number | null; + gracePeriod: number | null; + /** Polling interval (ms). When set, the sense is triggered periodically. */ + interval: number | null; + /** Other sense names whose successful computes schedule this sense (kernel reverse-index). */ + on: string[]; +}; + /** Optional HTTP control plane. When `port` is null, the HTTP server is not started. */ export type NerveApiConfig = { port: number | null; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 18f7135..90da4bf 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,8 +1,5 @@ export type { SenseConfig, - DropOverflowConfig, - QueueOverflowConfig, - WorkflowConfig, NerveApiConfig, AgentConfig, ExtractConfig, @@ -12,21 +9,6 @@ export type { export type { SenseInfo } from "./sense.js"; export type { SenseComputeFn, SenseModule } from "./sense.js"; export { senseTriggerLabels } from "./sense.js"; -export type { - WorkflowMessage, - RoleResult, - Role, - RoleMeta, - StartStep, - ThreadContext, - WorkflowContext, - AgentFn, - RoleStep, - ModeratorContext, - Moderator, - WorkflowDefinition, -} from "./workflow.js"; -export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "./workflow.js"; export type { Schema, ExtractFn } from "./agent.js"; export { ExtractError } from "./agent.js"; export type { Result } from "./util.js"; diff --git a/packages/daemon/package.json b/packages/daemon/package.json index e205d1d..73dcc97 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -22,10 +22,11 @@ "scripts": { "prepublishOnly": "bash ../../scripts/prepublish-check.sh", "build": "rslib build", - "pretest": "pnpm --filter @uncaged/nerve-core run build", + "pretest": "pnpm --filter @uncaged/workflow run build:public-types && pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-store run build && pnpm --filter @uncaged/workflow run build", "test": "vitest run" }, "dependencies": { + "@uncaged/workflow": "workspace:*", "@uncaged/nerve-core": "workspace:*", "@uncaged/nerve-store": "workspace:*", "yaml": "^2.8.3" diff --git a/packages/daemon/rslib.config.ts b/packages/daemon/rslib.config.ts index ee4c746..ef3ab7c 100644 --- a/packages/daemon/rslib.config.ts +++ b/packages/daemon/rslib.config.ts @@ -11,7 +11,6 @@ export default defineConfig({ entry: { index: "src/index.ts", "sense-worker": "src/sense-worker.ts", - "workflow-worker": "src/workflow-worker.ts", "experimental-warning-suppression": "src/experimental-warning-suppression.ts", }, }, diff --git a/packages/daemon/src/__tests__/crash-recovery.test.ts b/packages/daemon/src/__tests__/crash-recovery.test.ts index 4891997..252aba5 100644 --- a/packages/daemon/src/__tests__/crash-recovery.test.ts +++ b/packages/daemon/src/__tests__/crash-recovery.test.ts @@ -10,7 +10,8 @@ import { EventEmitter } from "node:events"; -import type { NerveConfig, WorkflowConfig } from "@uncaged/nerve-core"; +import type { NerveConfig } from "@uncaged/nerve-core"; +import type { WorkflowConfig } from "@uncaged/workflow"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; type MockChild = EventEmitter & { @@ -62,7 +63,7 @@ vi.mock("node:child_process", () => ({ }), })); -const { createWorkflowManager } = await import("../workflow-manager.js"); +const { createWorkflowManager } = await import("@uncaged/workflow"); function makeConfig(workflows: Record = {}): NerveConfig { return { diff --git a/packages/daemon/src/__tests__/hot-reload.test.ts b/packages/daemon/src/__tests__/hot-reload.test.ts index 678affe..13eddde 100644 --- a/packages/daemon/src/__tests__/hot-reload.test.ts +++ b/packages/daemon/src/__tests__/hot-reload.test.ts @@ -15,7 +15,8 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { NerveConfig, WorkflowConfig } from "@uncaged/nerve-core"; +import type { NerveConfig } from "@uncaged/nerve-core"; +import type { WorkflowConfig } from "@uncaged/workflow"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; type MockChild = EventEmitter & { @@ -67,7 +68,7 @@ vi.mock("node:child_process", () => ({ }), })); -const { createWorkflowManager } = await import("../workflow-manager.js"); +const { createWorkflowManager } = await import("@uncaged/workflow"); const { createKernel } = await import("../kernel.js"); function makeWfConfig(workflows: Record = {}): NerveConfig { diff --git a/packages/daemon/src/__tests__/worker-runtime.test.ts b/packages/daemon/src/__tests__/worker-runtime.test.ts index 642a33e..ed83756 100644 --- a/packages/daemon/src/__tests__/worker-runtime.test.ts +++ b/packages/daemon/src/__tests__/worker-runtime.test.ts @@ -1,7 +1,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { createWorkerRuntime } from "@uncaged/workflow"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { createWorkerRuntime } from "../worker-runtime.js"; const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), "fixtures"); const echoWorkerPath = join(fixturesDir, "echo-worker.js"); diff --git a/packages/daemon/src/__tests__/workflow-manager.test.ts b/packages/daemon/src/__tests__/workflow-manager.test.ts index 280ad41..ed8b0ee 100644 --- a/packages/daemon/src/__tests__/workflow-manager.test.ts +++ b/packages/daemon/src/__tests__/workflow-manager.test.ts @@ -61,7 +61,7 @@ vi.mock("node:child_process", () => ({ })); // Import after mock is set up -const { createWorkflowManager } = await import("../workflow-manager.js"); +const { createWorkflowManager } = await import("@uncaged/workflow"); // --------------------------------------------------------------------------- // Helpers diff --git a/packages/daemon/src/daemon-handlers.ts b/packages/daemon/src/daemon-handlers.ts index bd97d5e..6f34644 100644 --- a/packages/daemon/src/daemon-handlers.ts +++ b/packages/daemon/src/daemon-handlers.ts @@ -1,6 +1,6 @@ import type { HealthInfo, SenseInfo, WorkflowStatus } from "@uncaged/nerve-core"; -import type { WorkflowManager } from "./workflow-manager.js"; +import type { WorkflowManager } from "@uncaged/workflow"; export type DaemonHandlerBundle = { health: () => HealthInfo; diff --git a/packages/daemon/src/index.ts b/packages/daemon/src/index.ts index 0d88a47..64f844b 100644 --- a/packages/daemon/src/index.ts +++ b/packages/daemon/src/index.ts @@ -12,7 +12,7 @@ export type { ResumeThreadMessage, ThreadEventMessage, WorkflowErrorMessage, - ThreadWorkflowMessageMessage, + ThreadWorkflowMessage, } from "./ipc.js"; export { loadSenseModule, executeCompute, readState, writeState } from "./sense-runtime.js"; @@ -45,5 +45,5 @@ export type { WorkflowRunStatus, } from "@uncaged/nerve-store"; -export { createWorkflowManager } from "./workflow-manager.js"; -export type { WorkflowManager } from "./workflow-manager.js"; +export { createWorkflowManager } from "@uncaged/workflow"; +export type { WorkflowManager } from "@uncaged/workflow"; diff --git a/packages/daemon/src/ipc.ts b/packages/daemon/src/ipc.ts index e23f692..d38a217 100644 --- a/packages/daemon/src/ipc.ts +++ b/packages/daemon/src/ipc.ts @@ -1,10 +1,30 @@ /** * IPC message types for parent (kernel) ↔ sense worker communication. * Protocol per RFC §5.2: hub-and-spoke, all messages through engine. + * + * Workflow worker IPC types and parsers live in `@uncaged/workflow`. */ import type { Result, SenseTrigger } from "@uncaged/nerve-core"; import { err, isPlainRecord, ok, parseSenseTrigger } from "@uncaged/nerve-core"; +import type { + KillThreadMessage, + ResumeThreadMessage, + StartThreadMessage, + ThreadEventMessage, + ThreadWorkflowMessage, + WorkflowErrorMessage, +} from "@uncaged/workflow"; +import { parseWorkflowParentMessage, parseWorkflowWorkerToParentMessage } from "@uncaged/workflow"; + +export type { + KillThreadMessage, + ResumeThreadMessage, + StartThreadMessage, + ThreadEventMessage, + ThreadWorkflowMessage, + WorkflowErrorMessage, +} from "@uncaged/workflow"; /** Parent → Worker: trigger one compute cycle for a sense */ export type ComputeMessage = { @@ -22,40 +42,6 @@ export type HealthRequestMessage = { type: "health-request"; }; -// --------------------------------------------------------------------------- -// Workflow IPC messages (RFC-002 §5.2) -// --------------------------------------------------------------------------- - -/** Parent → Workflow Worker: start a new thread */ -export type StartThreadMessage = { - type: "start-thread"; - runId: string; - workflow: string; - prompt: string; - /** Safety-valve: max moderator rounds for this thread (engine launch parameter). */ - maxRounds: number; - /** When true, roles may skip side effects (thread-level hint on the start frame). */ - dryRun: boolean; -}; - -/** Parent → Workflow Worker: resume an existing thread after crash recovery */ -export type ResumeThreadMessage = { - type: "resume-thread"; - runId: string; - /** Serialised WorkflowMessage history to rebuild chain (must begin with `__start__`). */ - messages: Array<{ role: string; content: string; meta: unknown; timestamp: number }>; - /** Safety-valve: max moderator rounds for this thread. */ - maxRounds: number; - /** Thread-level dry-run hint (aligns with persisted `__start__` meta when replaying). */ - dryRun: boolean; -}; - -/** Parent → Workflow Worker: kill a specific running thread */ -export type KillThreadMessage = { - type: "kill-thread"; - runId: string; -}; - /** Union of all messages the parent sends to a worker */ export type ParentToWorkerMessage = | ComputeMessage @@ -92,46 +78,6 @@ export type HealthResponseMessage = { inFlightCount: number; }; -// --------------------------------------------------------------------------- -// Workflow Worker → Parent messages (RFC-002 §5.2) -// --------------------------------------------------------------------------- - -/** Valid lifecycle event types for a workflow thread. */ -export type ThreadEventType = - | "queued" - | "started" - | "step_complete" - | "completed" - | "failed" - | "killed"; - -/** - * Workflow Worker → Parent: a thread lifecycle event. - */ -export type ThreadEventMessage = { - type: "thread-event"; - runId: string; - eventType: ThreadEventType; - payload: unknown; -}; - -/** Workflow Worker → Parent: a thread encountered an unrecoverable error. */ -export type WorkflowErrorMessage = { - type: "workflow-error"; - runId: string; - error: string; - /** Exit code conveying the failure reason (1=role error, 2=maxRounds exhausted). */ - exitCode: number; -}; - -/** Workflow Worker → Parent: a WorkflowMessage produced by a role (for crash recovery). */ -export type ThreadWorkflowMessageMessage = { - type: "thread-workflow-message"; - runId: string; - /** The WorkflowMessage produced by the role — persisted for crash recovery. */ - message: { role: string; content: string; meta: unknown; timestamp: number }; -}; - /** Union of all messages a worker sends to the parent */ export type WorkerToParentMessage = | ComputeResultMessage @@ -140,7 +86,7 @@ export type WorkerToParentMessage = | HealthResponseMessage | ThreadEventMessage | WorkflowErrorMessage - | ThreadWorkflowMessageMessage; + | ThreadWorkflowMessage; const PARENT_MSG_TYPES = new Set([ "compute", @@ -151,24 +97,6 @@ const PARENT_MSG_TYPES = new Set([ "kill-thread", ]); -function validateStartThreadMsg(obj: Record): string | null { - if (typeof obj.runId !== "string") return "'start-thread' message missing string 'runId'"; - if (typeof obj.workflow !== "string") return "'start-thread' message missing string 'workflow'"; - if (typeof obj.prompt !== "string") return "'start-thread' message missing string 'prompt'"; - if (typeof obj.maxRounds !== "number") return "'start-thread' message missing number 'maxRounds'"; - if (typeof obj.dryRun !== "boolean") return "'start-thread' message missing boolean 'dryRun'"; - return null; -} - -function validateResumeThreadMsg(obj: Record): string | null { - if (typeof obj.runId !== "string") return "'resume-thread' message missing string 'runId'"; - if (!Array.isArray(obj.messages)) return "'resume-thread' message missing 'messages' array"; - if (typeof obj.maxRounds !== "number") - return "'resume-thread' message missing number 'maxRounds'"; - if (typeof obj.dryRun !== "boolean") return "'resume-thread' message missing boolean 'dryRun'"; - return null; -} - function parseParentCompute(obj: Record): Result { if (typeof obj.sense !== "string") { return err(new Error("IPC 'compute' message missing string 'sense' field")); @@ -176,40 +104,6 @@ function parseParentCompute(obj: Record): Result): Result { - const errMsg = validateStartThreadMsg(obj); - if (errMsg !== null) return err(new Error(errMsg)); - // Field types are validated above; `Record` values stay `unknown` to TypeScript. - return ok({ - type: "start-thread", - runId: obj.runId, - workflow: obj.workflow, - prompt: obj.prompt, - maxRounds: obj.maxRounds, - dryRun: obj.dryRun, - } as StartThreadMessage); -} - -function parseParentResumeThread(obj: Record): Result { - const errMsg = validateResumeThreadMsg(obj); - if (errMsg !== null) return err(new Error(errMsg)); - // Elements are validated as plain objects by the kernel; trust the wire shape here. - return ok({ - type: "resume-thread", - runId: obj.runId, - messages: obj.messages as ResumeThreadMessage["messages"], - maxRounds: obj.maxRounds, - dryRun: obj.dryRun, - } as ResumeThreadMessage); -} - -function parseParentKillThread(obj: Record): Result { - if (typeof obj.runId !== "string") { - return err(new Error("'kill-thread' message missing string 'runId'")); - } - return ok({ type: "kill-thread", runId: obj.runId } as KillThreadMessage); -} - /** Validate and parse an unknown IPC message received from the parent process. */ export function parseParentMessage(raw: unknown): Result { if (!isPlainRecord(raw)) { @@ -230,11 +124,14 @@ export function parseParentMessage(raw: unknown): Result case "health-request": return ok({ type: "health-request" }); case "start-thread": - return parseParentStartThread(obj); case "resume-thread": - return parseParentResumeThread(obj); - case "kill-thread": - return parseParentKillThread(obj); + case "kill-thread": { + const wf = parseWorkflowParentMessage(raw); + if (!wf.ok) { + return wf; + } + return ok(wf.value as ParentToWorkerMessage); + } default: return err(new Error(`Unhandled IPC message type: "${obj.type}"`)); } @@ -299,55 +196,11 @@ function parseHealthResponseMsg(obj: Record): Result): Result { - if (typeof obj.runId !== "string") { - return err(new Error("Worker 'thread-event' message missing string 'runId' field")); - } - if (typeof obj.eventType !== "string" || !isThreadEventType(obj.eventType)) { - return err( - new Error(`Worker 'thread-event' message has invalid 'eventType': "${obj.eventType}"`), - ); - } - if (!("payload" in obj)) { - return err(new Error("Worker 'thread-event' message missing 'payload' field")); - } - return ok({ - type: "thread-event", - runId: obj.runId, - eventType: obj.eventType, - payload: obj.payload, - }); -} - -function parseWorkflowErrorMsg(obj: Record): Result { - if (typeof obj.runId !== "string") { - return err(new Error("Worker 'workflow-error' message missing string 'runId' field")); - } - if (typeof obj.error !== "string") { - return err(new Error("Worker 'workflow-error' message missing string 'error' field")); - } - const exitCode = typeof obj.exitCode === "number" ? obj.exitCode : 1; - return ok({ - type: "workflow-error", - runId: obj.runId, - error: obj.error, - exitCode, - }); -} +const WORKFLOW_WORKER_MSG_TYPES = new Set([ + "thread-event", + "workflow-error", + "thread-workflow-message", +]); const WORKER_MSG_TYPES = new Set([ "compute-result", @@ -359,41 +212,6 @@ const WORKER_MSG_TYPES = new Set([ "thread-workflow-message", ]); -function parseThreadWorkflowMessageMsg( - obj: Record, -): Result { - if (typeof obj.runId !== "string") { - return err(new Error("Worker 'thread-workflow-message' missing string 'runId' field")); - } - if (!isPlainRecord(obj.message)) { - return err(new Error("Worker 'thread-workflow-message' missing object 'message' field")); - } - const msg = obj.message; - if (typeof msg.role !== "string") { - return err(new Error("Worker 'thread-workflow-message' message missing string 'role' field")); - } - if (typeof msg.content !== "string") { - return err( - new Error("Worker 'thread-workflow-message' message missing string 'content' field"), - ); - } - if (typeof msg.timestamp !== "number") { - return err( - new Error("Worker 'thread-workflow-message' message missing number 'timestamp' field"), - ); - } - return ok({ - type: "thread-workflow-message", - runId: obj.runId, - message: { - role: msg.role, - content: msg.content, - meta: "meta" in msg ? msg.meta : undefined, - timestamp: msg.timestamp, - }, - }); -} - /** Validate and parse an unknown IPC message received from a worker process. */ export function parseWorkerMessage(raw: unknown): Result { if (!isPlainRecord(raw)) { @@ -406,11 +224,15 @@ export function parseWorkerMessage(raw: unknown): Result if (!WORKER_MSG_TYPES.has(obj.type)) { return err(new Error(`Unknown worker IPC message type: "${obj.type}"`)); } + if (WORKFLOW_WORKER_MSG_TYPES.has(obj.type)) { + const wf = parseWorkflowWorkerToParentMessage(raw); + if (!wf.ok) { + return wf; + } + return ok(wf.value as WorkerToParentMessage); + } if (obj.type === "compute-result") return parseComputeResultMsg(obj); if (obj.type === "error") return parseErrorMsg(obj); if (obj.type === "health-response") return parseHealthResponseMsg(obj); - if (obj.type === "thread-event") return parseThreadEventMsg(obj); - if (obj.type === "workflow-error") return parseWorkflowErrorMsg(obj); - if (obj.type === "thread-workflow-message") return parseThreadWorkflowMessageMsg(obj); return ok({ type: "ready" }); } diff --git a/packages/daemon/src/kernel-file-watch.ts b/packages/daemon/src/kernel-file-watch.ts index f0b353e..aec5c2e 100644 --- a/packages/daemon/src/kernel-file-watch.ts +++ b/packages/daemon/src/kernel-file-watch.ts @@ -9,7 +9,7 @@ import type { NerveConfig } from "@uncaged/nerve-core"; import { parseNerveConfig } from "@uncaged/nerve-core"; import type { LogStore } from "@uncaged/nerve-store"; -import type { WorkflowManager } from "./workflow-manager.js"; +import type { WorkflowManager } from "@uncaged/workflow"; export type KernelFileWatchDeps = { nerveRoot: string; diff --git a/packages/daemon/src/kernel.ts b/packages/daemon/src/kernel.ts index d411a84..68d4b52 100644 --- a/packages/daemon/src/kernel.ts +++ b/packages/daemon/src/kernel.ts @@ -18,6 +18,8 @@ import { import { createLogStore } from "@uncaged/nerve-store"; import type { LogStore } from "@uncaged/nerve-store"; +import { createWorkflowManager } from "@uncaged/workflow"; +import type { WorkflowManager } from "@uncaged/workflow"; import { createDaemonHandlers } from "./daemon-handlers.js"; import { createDaemonIpcServer } from "./daemon-ipc.js"; import type { DaemonIpcServer } from "./daemon-ipc.js"; @@ -36,8 +38,6 @@ import { import { createSenseScheduler } from "./sense-scheduler.js"; import type { SenseScheduler } from "./sense-scheduler.js"; import { createSenseWorkerPool, resolveWorkerScript } from "./worker-pool.js"; -import { createWorkflowManager } from "./workflow-manager.js"; -import type { WorkflowManager } from "./workflow-manager.js"; export type KernelHealth = { uptime: number; diff --git a/packages/daemon/src/worker-pool.ts b/packages/daemon/src/worker-pool.ts index d93560f..165e8ff 100644 --- a/packages/daemon/src/worker-pool.ts +++ b/packages/daemon/src/worker-pool.ts @@ -5,12 +5,12 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import type { ComputeMessage } from "./ipc.js"; import { createWorkerRuntime, formatCapturedStderrTail, formatChildExitSummary, -} from "./worker-runtime.js"; +} from "@uncaged/workflow"; +import type { ComputeMessage } from "./ipc.js"; export function resolveWorkerScript(): string { const __filename = fileURLToPath(import.meta.url); diff --git a/packages/role-committer/package.json b/packages/role-committer/package.json index a7221d4..3869790 100644 --- a/packages/role-committer/package.json +++ b/packages/role-committer/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@uncaged/nerve-core": "workspace:*", + "@uncaged/workflow": "workspace:*", "@uncaged/nerve-workflow-utils": "workspace:*", "zod": "^4.3.6" }, diff --git a/packages/role-committer/src/index.ts b/packages/role-committer/src/index.ts index 6c39e84..6c16824 100644 --- a/packages/role-committer/src/index.ts +++ b/packages/role-committer/src/index.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole, decorateRole, onFail, withDryRun } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const committerMetaSchema = z.object({ diff --git a/packages/role-reviewer/package.json b/packages/role-reviewer/package.json index 211f413..a5d123c 100644 --- a/packages/role-reviewer/package.json +++ b/packages/role-reviewer/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@uncaged/nerve-core": "workspace:*", + "@uncaged/workflow": "workspace:*", "@uncaged/nerve-workflow-utils": "workspace:*", "zod": "^4.3.6" }, diff --git a/packages/role-reviewer/src/reviewer.ts b/packages/role-reviewer/src/reviewer.ts index 449c6a4..4ee3cb1 100644 --- a/packages/role-reviewer/src/reviewer.ts +++ b/packages/role-reviewer/src/reviewer.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const reviewerMetaSchema = z.object({ diff --git a/packages/workflow-meta/package.json b/packages/workflow-meta/package.json index 987c79a..adbb459 100644 --- a/packages/workflow-meta/package.json +++ b/packages/workflow-meta/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@uncaged/nerve-core": "workspace:*", + "@uncaged/workflow": "workspace:*", "@uncaged/nerve-role-committer": "workspace:*", "@uncaged/nerve-role-reviewer": "workspace:*", "@uncaged/nerve-workflow-utils": "workspace:*", diff --git a/packages/workflow-meta/src/develop-sense/build.ts b/packages/workflow-meta/src/develop-sense/build.ts index 526c6a7..30d6166 100644 --- a/packages/workflow-meta/src/develop-sense/build.ts +++ b/packages/workflow-meta/src/develop-sense/build.ts @@ -1,7 +1,7 @@ -import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core"; import { createCommitterRole } from "@uncaged/nerve-role-committer"; import { createReviewerRole } from "@uncaged/nerve-role-reviewer"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, WorkflowDefinition } from "@uncaged/workflow"; import { moderator } from "./moderator.js"; import type { SenseMeta } from "./moderator.js"; diff --git a/packages/workflow-meta/src/develop-sense/moderator.ts b/packages/workflow-meta/src/develop-sense/moderator.ts index f24d46e..bde617a 100644 --- a/packages/workflow-meta/src/develop-sense/moderator.ts +++ b/packages/workflow-meta/src/develop-sense/moderator.ts @@ -1,7 +1,7 @@ -import { END } from "@uncaged/nerve-core"; -import type { Moderator } from "@uncaged/nerve-core"; import type { CommitterMeta } from "@uncaged/nerve-role-committer"; import type { ReviewerMeta } from "@uncaged/nerve-role-reviewer"; +import { END } from "@uncaged/workflow"; +import type { Moderator } from "@uncaged/workflow"; import type { CoderMeta } from "./roles/coder.js"; import type { PlannerMeta } from "./roles/planner.js"; import type { TesterMeta } from "./roles/tester.js"; diff --git a/packages/workflow-meta/src/develop-sense/roles/coder.ts b/packages/workflow-meta/src/develop-sense/roles/coder.ts index 3254414..ef39cb3 100644 --- a/packages/workflow-meta/src/develop-sense/roles/coder.ts +++ b/packages/workflow-meta/src/develop-sense/roles/coder.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const coderMetaSchema = z.object({ diff --git a/packages/workflow-meta/src/develop-sense/roles/planner.ts b/packages/workflow-meta/src/develop-sense/roles/planner.ts index bf75bac..1fdba57 100644 --- a/packages/workflow-meta/src/develop-sense/roles/planner.ts +++ b/packages/workflow-meta/src/develop-sense/roles/planner.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const plannerMetaSchema = z.object({ diff --git a/packages/workflow-meta/src/develop-sense/roles/tester.ts b/packages/workflow-meta/src/develop-sense/roles/tester.ts index c96ab0e..2fe10bc 100644 --- a/packages/workflow-meta/src/develop-sense/roles/tester.ts +++ b/packages/workflow-meta/src/develop-sense/roles/tester.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const testerMetaSchema = z.object({ diff --git a/packages/workflow-meta/src/develop-workflow/build.ts b/packages/workflow-meta/src/develop-workflow/build.ts index 3659368..332180e 100644 --- a/packages/workflow-meta/src/develop-workflow/build.ts +++ b/packages/workflow-meta/src/develop-workflow/build.ts @@ -1,7 +1,7 @@ -import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core"; import { createCommitterRole } from "@uncaged/nerve-role-committer"; import { createReviewerRole } from "@uncaged/nerve-role-reviewer"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, WorkflowDefinition } from "@uncaged/workflow"; import { moderator } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js"; diff --git a/packages/workflow-meta/src/develop-workflow/moderator.ts b/packages/workflow-meta/src/develop-workflow/moderator.ts index a55fa15..f28a2b7 100644 --- a/packages/workflow-meta/src/develop-workflow/moderator.ts +++ b/packages/workflow-meta/src/develop-workflow/moderator.ts @@ -1,7 +1,7 @@ -import { END } from "@uncaged/nerve-core"; -import type { Moderator } from "@uncaged/nerve-core"; import type { CommitterMeta } from "@uncaged/nerve-role-committer"; import type { ReviewerMeta } from "@uncaged/nerve-role-reviewer"; +import { END } from "@uncaged/workflow"; +import type { Moderator } from "@uncaged/workflow"; import type { CoderMeta } from "./roles/coder.js"; import type { PlannerMeta } from "./roles/planner.js"; import type { TesterMeta } from "./roles/tester.js"; diff --git a/packages/workflow-meta/src/develop-workflow/roles/coder.ts b/packages/workflow-meta/src/develop-workflow/roles/coder.ts index ec746c2..7a60bb9 100644 --- a/packages/workflow-meta/src/develop-workflow/roles/coder.ts +++ b/packages/workflow-meta/src/develop-workflow/roles/coder.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const coderMetaSchema = z.object({ diff --git a/packages/workflow-meta/src/develop-workflow/roles/planner.ts b/packages/workflow-meta/src/develop-workflow/roles/planner.ts index eadc893..00edbf5 100644 --- a/packages/workflow-meta/src/develop-workflow/roles/planner.ts +++ b/packages/workflow-meta/src/develop-workflow/roles/planner.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const plannerMetaSchema = z.object({ diff --git a/packages/workflow-meta/src/develop-workflow/roles/tester.ts b/packages/workflow-meta/src/develop-workflow/roles/tester.ts index 8902732..27af4a0 100644 --- a/packages/workflow-meta/src/develop-workflow/roles/tester.ts +++ b/packages/workflow-meta/src/develop-workflow/roles/tester.ts @@ -1,6 +1,6 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import { z } from "zod"; export const testerMetaSchema = z.object({ diff --git a/packages/workflow-utils/package.json b/packages/workflow-utils/package.json index dca8c8c..faa7e64 100644 --- a/packages/workflow-utils/package.json +++ b/packages/workflow-utils/package.json @@ -24,6 +24,7 @@ "@uncaged/nerve-adapter-cursor": "workspace:*", "@uncaged/nerve-adapter-hermes": "workspace:*", "@uncaged/nerve-core": "workspace:*", + "@uncaged/workflow": "workspace:*", "zod": "^4.3.6" }, "devDependencies": { diff --git a/packages/workflow-utils/src/__tests__/create-llm-adapter.test.ts b/packages/workflow-utils/src/__tests__/create-llm-adapter.test.ts index f0d3a90..4fdbd15 100644 --- a/packages/workflow-utils/src/__tests__/create-llm-adapter.test.ts +++ b/packages/workflow-utils/src/__tests__/create-llm-adapter.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { START, type ThreadContext } from "@uncaged/nerve-core"; +import { START, type ThreadContext } from "@uncaged/workflow"; import { createLlmAdapter } from "../create-llm-adapter.js"; diff --git a/packages/workflow-utils/src/__tests__/create-role.test.ts b/packages/workflow-utils/src/__tests__/create-role.test.ts index c2aca98..06811fc 100644 --- a/packages/workflow-utils/src/__tests__/create-role.test.ts +++ b/packages/workflow-utils/src/__tests__/create-role.test.ts @@ -4,8 +4,8 @@ import type { RoleMeta, ThreadContext, WorkflowDefinition, -} from "@uncaged/nerve-core"; -import { END, START } from "@uncaged/nerve-core"; +} from "@uncaged/workflow"; +import { END, START } from "@uncaged/workflow"; import { afterEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; diff --git a/packages/workflow-utils/src/__tests__/role-decorators.test.ts b/packages/workflow-utils/src/__tests__/role-decorators.test.ts index 26aa03e..1e3a38f 100644 --- a/packages/workflow-utils/src/__tests__/role-decorators.test.ts +++ b/packages/workflow-utils/src/__tests__/role-decorators.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import type { Role, ThreadContext } from "@uncaged/nerve-core"; +import type { Role, ThreadContext } from "@uncaged/workflow"; -import { START } from "@uncaged/nerve-core"; +import { START } from "@uncaged/workflow"; import { decorateRole, onFail, withDryRun } from "../role-decorators.js"; type TestMeta = Record & { ok: boolean }; diff --git a/packages/workflow-utils/src/__tests__/role-factories.test.ts b/packages/workflow-utils/src/__tests__/role-factories.test.ts index 6494ca8..f601837 100644 --- a/packages/workflow-utils/src/__tests__/role-factories.test.ts +++ b/packages/workflow-utils/src/__tests__/role-factories.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; -import { START, type ThreadContext } from "@uncaged/nerve-core"; +import { START, type ThreadContext } from "@uncaged/workflow"; import { createCursorRole } from "../role-cursor.js"; import { createHermesRole } from "../role-hermes.js"; diff --git a/packages/workflow-utils/src/create-llm-adapter.ts b/packages/workflow-utils/src/create-llm-adapter.ts index bcab534..0e3c771 100644 --- a/packages/workflow-utils/src/create-llm-adapter.ts +++ b/packages/workflow-utils/src/create-llm-adapter.ts @@ -1,4 +1,4 @@ -import type { AgentFn, ThreadContext } from "@uncaged/nerve-core"; +import type { AgentFn, ThreadContext } from "@uncaged/workflow"; import { formatLlmError } from "./shared/format-error.js"; import { chatCompletionText } from "./shared/llm-chat.js"; diff --git a/packages/workflow-utils/src/create-role.ts b/packages/workflow-utils/src/create-role.ts index c3768a7..18648b3 100644 --- a/packages/workflow-utils/src/create-role.ts +++ b/packages/workflow-utils/src/create-role.ts @@ -1,4 +1,4 @@ -import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core"; +import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow"; import type { z } from "zod"; import { extractMetaOrThrow } from "./shared/extract-fn.js"; diff --git a/packages/workflow-utils/src/role-cursor.ts b/packages/workflow-utils/src/role-cursor.ts index 72673c5..33da265 100644 --- a/packages/workflow-utils/src/role-cursor.ts +++ b/packages/workflow-utils/src/role-cursor.ts @@ -1,5 +1,6 @@ import { type CursorAgentMode, cursorAgent } from "@uncaged/nerve-adapter-cursor"; -import type { Role, SpawnEnv } from "@uncaged/nerve-core"; +import type { SpawnEnv } from "@uncaged/nerve-core"; +import type { Role } from "@uncaged/workflow"; import type { CursorRoleDefaults, CursorRoleRequired } from "./role-types.js"; import { formatLlmError } from "./shared/format-error.js"; diff --git a/packages/workflow-utils/src/role-decorators.ts b/packages/workflow-utils/src/role-decorators.ts index b8155a0..3b2bfea 100644 --- a/packages/workflow-utils/src/role-decorators.ts +++ b/packages/workflow-utils/src/role-decorators.ts @@ -1,4 +1,4 @@ -import type { Role, ThreadContext } from "@uncaged/nerve-core"; +import type { Role, ThreadContext } from "@uncaged/workflow"; // --------------------------------------------------------------------------- // Decorator types diff --git a/packages/workflow-utils/src/role-hermes.ts b/packages/workflow-utils/src/role-hermes.ts index d23ee80..c2a4cd6 100644 --- a/packages/workflow-utils/src/role-hermes.ts +++ b/packages/workflow-utils/src/role-hermes.ts @@ -1,4 +1,4 @@ -import type { Role } from "@uncaged/nerve-core"; +import type { Role } from "@uncaged/workflow"; import type { HermesRoleDefaults, HermesRoleRequired } from "./role-types.js"; import { formatLlmError } from "./shared/format-error.js"; diff --git a/packages/workflow-utils/src/role-llm.ts b/packages/workflow-utils/src/role-llm.ts index 9ebb54e..cf66882 100644 --- a/packages/workflow-utils/src/role-llm.ts +++ b/packages/workflow-utils/src/role-llm.ts @@ -1,4 +1,4 @@ -import type { Role } from "@uncaged/nerve-core"; +import type { Role } from "@uncaged/workflow"; import type { LlmMessage, LlmRoleDefaults, LlmRoleRequired } from "./role-types.js"; import { formatLlmError } from "./shared/format-error.js"; diff --git a/packages/workflow-utils/src/role-react.ts b/packages/workflow-utils/src/role-react.ts index 579a90d..75ba8b1 100644 --- a/packages/workflow-utils/src/role-react.ts +++ b/packages/workflow-utils/src/role-react.ts @@ -1,4 +1,4 @@ -import type { Role } from "@uncaged/nerve-core"; +import type { Role } from "@uncaged/workflow"; import type { LlmMessage, ReActRoleDefaults, ReActRoleRequired } from "./role-types.js"; import { formatLlmError } from "./shared/format-error.js"; diff --git a/packages/workflow-utils/src/role-types.ts b/packages/workflow-utils/src/role-types.ts index be540c8..a060aba 100644 --- a/packages/workflow-utils/src/role-types.ts +++ b/packages/workflow-utils/src/role-types.ts @@ -1,4 +1,5 @@ -import type { SpawnEnv, ThreadContext } from "@uncaged/nerve-core"; +import type { SpawnEnv } from "@uncaged/nerve-core"; +import type { ThreadContext } from "@uncaged/workflow"; import type { z } from "zod"; import type { LlmProvider } from "./shared/llm-extract.js"; diff --git a/packages/workflow/package.json b/packages/workflow/package.json new file mode 100644 index 0000000..531e20a --- /dev/null +++ b/packages/workflow/package.json @@ -0,0 +1,35 @@ +{ + "name": "@uncaged/workflow", + "version": "0.5.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./public-types": { + "types": "./dist/public-types.d.ts", + "default": "./dist/public-types.js" + }, + "./package.json": "./package.json" + }, + "files": ["dist"], + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepublishOnly": "bash ../../scripts/prepublish-check.sh", + "build": "rslib build && pnpm run build:public-types", + "build:public-types": "tsc -p tsconfig.public-types.json" + }, + "dependencies": { + "@uncaged/nerve-core": "^0.5.0", + "@uncaged/nerve-store": "^0.5.0" + }, + "devDependencies": { + "@rslib/core": "^0.21.3", + "@types/node": "^22.0.0" + } +} diff --git a/packages/workflow/rslib.config.ts b/packages/workflow/rslib.config.ts new file mode 100644 index 0000000..6894607 --- /dev/null +++ b/packages/workflow/rslib.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "@rslib/core"; + +export default defineConfig({ + lib: [ + { + format: "esm", + dts: true, + }, + ], + source: { + entry: { + index: "src/index.ts", + worker: "src/worker.ts", + }, + }, + output: { + target: "node", + cleanDistPath: true, + }, +}); diff --git a/packages/workflow/src/config.ts b/packages/workflow/src/config.ts new file mode 100644 index 0000000..5fa1200 --- /dev/null +++ b/packages/workflow/src/config.ts @@ -0,0 +1,12 @@ +export type DropOverflowConfig = { + concurrency: number; + overflow: "drop"; +}; + +export type QueueOverflowConfig = { + concurrency: number; + overflow: "queue"; + maxQueue: number; +}; + +export type WorkflowConfig = DropOverflowConfig | QueueOverflowConfig; diff --git a/packages/workflow/src/experimental-warning-suppression.ts b/packages/workflow/src/experimental-warning-suppression.ts new file mode 100644 index 0000000..f33086d --- /dev/null +++ b/packages/workflow/src/experimental-warning-suppression.ts @@ -0,0 +1,23 @@ +/** + * Patches `process.emit` so `ExperimentalWarning` (e.g. from `node:sqlite`) is not + * forwarded to the default handler. Other warning types are unchanged. + * + * Import this module before any code that loads `node:sqlite`. + */ + +const WARNING_EVENT = "warning"; +const EXPERIMENTAL_WARNING_NAME = "ExperimentalWarning"; + +type EmitFn = typeof process.emit; + +const originalEmit = process.emit.bind(process) as EmitFn; + +process.emit = ((event: string | symbol, ...args: unknown[]): boolean => { + if (event === WARNING_EVENT) { + const w = args[0]; + if (w instanceof Error && w.name === EXPERIMENTAL_WARNING_NAME) { + return false; + } + } + return Reflect.apply(originalEmit, process, [event, ...args]) as boolean; +}) as EmitFn; diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts new file mode 100644 index 0000000..e2193fe --- /dev/null +++ b/packages/workflow/src/index.ts @@ -0,0 +1,53 @@ +// @uncaged/workflow — standalone workflow orchestration engine + +export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "./types.js"; +export type { + WorkflowMessage, + RoleResult, + Role, + RoleMeta, + StartStep, + ThreadContext, + WorkflowContext, + AgentFn, + RoleStep, + ModeratorContext, + Moderator, + WorkflowDefinition, +} from "./types.js"; + +export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig } from "./config.js"; + +export type { + KillThreadMessage, + ResumeThreadMessage, + StartThreadMessage, + WorkflowParentToWorkerMessage, + WorkflowWorkerShutdownMessage, + ThreadEventType, + ThreadLifecycleEvent, + ThreadEventMessage, + WorkflowErrorMessage, + ThreadWorkflowMessage, + WorkflowWorkerToParentMessage, + WorkflowWorkerReadyMessage, + WorkflowChildToParentMessage, + WorkflowWorkerOutboundMessage, +} from "./ipc.js"; +export { + parseWorkflowParentMessage, + parseWorkflowWorkerToParentMessage, + parseWorkflowChildMessage, +} from "./ipc.js"; + +export { WORKFLOW_WORKER_PATH } from "./paths.js"; + +export { + createWorkerRuntime, + formatCapturedStderrTail, + formatChildExitSummary, +} from "./worker-runtime.js"; +export type { WorkerDrainOpts, WorkerRuntime, WorkerRuntimeConfig } from "./worker-runtime.js"; + +export { createWorkflowManager } from "./manager.js"; +export type { WorkflowLaunchParams, WorkflowManager } from "./manager.js"; diff --git a/packages/workflow/src/ipc.ts b/packages/workflow/src/ipc.ts new file mode 100644 index 0000000..a216589 --- /dev/null +++ b/packages/workflow/src/ipc.ts @@ -0,0 +1,314 @@ +/** + * IPC message types for parent (kernel) ↔ workflow worker communication. + * Protocol per RFC-002 §5.2. + */ + +import type { Result } from "@uncaged/nerve-core"; +import { err, isPlainRecord, ok } from "@uncaged/nerve-core"; + +/** Parent → Workflow Worker: start a new thread */ +export type StartThreadMessage = { + type: "start-thread"; + runId: string; + workflow: string; + prompt: string; + /** Safety-valve: max moderator rounds for this thread (engine launch parameter). */ + maxRounds: number; + /** When true, roles may skip side effects (thread-level hint on the start frame). */ + dryRun: boolean; +}; + +/** Parent → Workflow Worker: resume an existing thread after crash recovery */ +export type ResumeThreadMessage = { + type: "resume-thread"; + runId: string; + /** Serialised WorkflowMessage history to rebuild chain (must begin with `__start__`). */ + messages: Array<{ role: string; content: string; meta: unknown; timestamp: number }>; + /** Safety-valve: max moderator rounds for this thread. */ + maxRounds: number; + /** Thread-level dry-run hint (aligns with persisted `__start__` meta when replaying). */ + dryRun: boolean; +}; + +/** Parent → Workflow Worker: kill a specific running thread */ +export type KillThreadMessage = { + type: "kill-thread"; + runId: string; +}; + +/** Parent → Workflow Worker: graceful shutdown (same wire shape as sense worker). */ +export type WorkflowWorkerShutdownMessage = { + type: "shutdown"; +}; + +/** Messages the parent sends to a workflow worker process */ +export type WorkflowParentToWorkerMessage = + | StartThreadMessage + | ResumeThreadMessage + | KillThreadMessage + | WorkflowWorkerShutdownMessage; + +/** Valid lifecycle event types for a workflow thread. */ +export type ThreadEventType = + | "queued" + | "started" + | "step_complete" + | "completed" + | "failed" + | "killed"; + +/** Alias — lifecycle channel uses `ThreadEventType` values. */ +export type ThreadLifecycleEvent = ThreadEventType; + +/** + * Workflow Worker → Parent: a thread lifecycle event. + */ +export type ThreadEventMessage = { + type: "thread-event"; + runId: string; + eventType: ThreadEventType; + payload: unknown; +}; + +/** Workflow Worker → Parent: a thread encountered an unrecoverable error. */ +export type WorkflowErrorMessage = { + type: "workflow-error"; + runId: string; + error: string; + /** Exit code conveying the failure reason (1=role error, 2=maxRounds exhausted). */ + exitCode: number; +}; + +/** Workflow Worker → Parent: a WorkflowMessage produced by a role (for crash recovery). */ +export type ThreadWorkflowMessage = { + type: "thread-workflow-message"; + runId: string; + /** The WorkflowMessage produced by the role — persisted for crash recovery. */ + message: { role: string; content: string; meta: unknown; timestamp: number }; +}; + +/** Messages a workflow worker sends to the parent (subset of full worker IPC). */ +export type WorkflowWorkerToParentMessage = + | ThreadEventMessage + | WorkflowErrorMessage + | ThreadWorkflowMessage; + +export type WorkflowWorkerReadyMessage = { type: "ready" }; + +/** Messages a workflow child may emit on IPC (including bootstrap ready). */ +export type WorkflowChildToParentMessage = + | WorkflowWorkerReadyMessage + | WorkflowWorkerToParentMessage; + +/** Messages a workflow worker process may send upstream (ready + persisted workflow IPC). */ +export type WorkflowWorkerOutboundMessage = + | WorkflowWorkerReadyMessage + | WorkflowWorkerToParentMessage; + +const WORKFLOW_PARENT_MSG_TYPES = new Set([ + "start-thread", + "resume-thread", + "kill-thread", + "shutdown", +]); + +function validateStartThreadMsg(obj: Record): string | null { + if (typeof obj.runId !== "string") return "'start-thread' message missing string 'runId'"; + if (typeof obj.workflow !== "string") return "'start-thread' message missing string 'workflow'"; + if (typeof obj.prompt !== "string") return "'start-thread' message missing string 'prompt'"; + if (typeof obj.maxRounds !== "number") return "'start-thread' message missing number 'maxRounds'"; + if (typeof obj.dryRun !== "boolean") return "'start-thread' message missing boolean 'dryRun'"; + return null; +} + +function validateResumeThreadMsg(obj: Record): string | null { + if (typeof obj.runId !== "string") return "'resume-thread' message missing string 'runId'"; + if (!Array.isArray(obj.messages)) return "'resume-thread' message missing 'messages' array"; + if (typeof obj.maxRounds !== "number") + return "'resume-thread' message missing number 'maxRounds'"; + if (typeof obj.dryRun !== "boolean") return "'resume-thread' message missing boolean 'dryRun'"; + return null; +} + +function parseParentStartThread(obj: Record): Result { + const errMsg = validateStartThreadMsg(obj); + if (errMsg !== null) return err(new Error(errMsg)); + return ok({ + type: "start-thread", + runId: obj.runId, + workflow: obj.workflow, + prompt: obj.prompt, + maxRounds: obj.maxRounds, + dryRun: obj.dryRun, + } as StartThreadMessage); +} + +function parseParentResumeThread(obj: Record): Result { + const errMsg = validateResumeThreadMsg(obj); + if (errMsg !== null) return err(new Error(errMsg)); + return ok({ + type: "resume-thread", + runId: obj.runId, + messages: obj.messages as ResumeThreadMessage["messages"], + maxRounds: obj.maxRounds, + dryRun: obj.dryRun, + } as ResumeThreadMessage); +} + +function parseParentKillThread(obj: Record): Result { + if (typeof obj.runId !== "string") { + return err(new Error("'kill-thread' message missing string 'runId'")); + } + return ok({ type: "kill-thread", runId: obj.runId } as KillThreadMessage); +} + +/** Validate and parse an unknown IPC message for a workflow worker process. */ +export function parseWorkflowParentMessage(raw: unknown): Result { + if (!isPlainRecord(raw)) { + return err(new Error("IPC message is not an object")); + } + const obj = raw; + if (typeof obj.type !== "string") { + return err(new Error("IPC message missing string 'type' field")); + } + if (!WORKFLOW_PARENT_MSG_TYPES.has(obj.type)) { + return err(new Error(`Unknown workflow IPC message type: "${obj.type}"`)); + } + switch (obj.type) { + case "shutdown": + return ok({ type: "shutdown" }); + case "start-thread": + return parseParentStartThread(obj); + case "resume-thread": + return parseParentResumeThread(obj); + case "kill-thread": + return parseParentKillThread(obj); + default: + return err(new Error(`Unhandled workflow IPC message type: "${obj.type}"`)); + } +} + +function isThreadEventType(value: string): value is ThreadEventType { + switch (value) { + case "queued": + case "started": + case "step_complete": + case "completed": + case "failed": + case "killed": + return true; + default: + return false; + } +} + +function parseThreadEventMsg(obj: Record): Result { + if (typeof obj.runId !== "string") { + return err(new Error("Worker 'thread-event' message missing string 'runId' field")); + } + if (typeof obj.eventType !== "string" || !isThreadEventType(obj.eventType)) { + return err( + new Error(`Worker 'thread-event' message has invalid 'eventType': "${obj.eventType}"`), + ); + } + if (!("payload" in obj)) { + return err(new Error("Worker 'thread-event' message missing 'payload' field")); + } + return ok({ + type: "thread-event", + runId: obj.runId, + eventType: obj.eventType, + payload: obj.payload, + }); +} + +function parseWorkflowErrorMsg(obj: Record): Result { + if (typeof obj.runId !== "string") { + return err(new Error("Worker 'workflow-error' message missing string 'runId' field")); + } + if (typeof obj.error !== "string") { + return err(new Error("Worker 'workflow-error' message missing string 'error' field")); + } + const exitCode = typeof obj.exitCode === "number" ? obj.exitCode : 1; + return ok({ + type: "workflow-error", + runId: obj.runId, + error: obj.error, + exitCode, + }); +} + +function parseThreadWorkflowMessageMsg( + obj: Record, +): Result { + if (typeof obj.runId !== "string") { + return err(new Error("Worker 'thread-workflow-message' missing string 'runId' field")); + } + if (!isPlainRecord(obj.message)) { + return err(new Error("Worker 'thread-workflow-message' missing object 'message' field")); + } + const msg = obj.message; + if (typeof msg.role !== "string") { + return err(new Error("Worker 'thread-workflow-message' message missing string 'role' field")); + } + if (typeof msg.content !== "string") { + return err( + new Error("Worker 'thread-workflow-message' message missing string 'content' field"), + ); + } + if (typeof msg.timestamp !== "number") { + return err( + new Error("Worker 'thread-workflow-message' message missing number 'timestamp' field"), + ); + } + return ok({ + type: "thread-workflow-message", + runId: obj.runId, + message: { + role: msg.role, + content: msg.content, + meta: "meta" in msg ? msg.meta : null, + timestamp: msg.timestamp, + }, + }); +} + +const WORKFLOW_WORKER_MSG_TYPES = new Set([ + "thread-event", + "workflow-error", + "thread-workflow-message", +]); + +/** Validate and parse workflow worker → parent IPC messages. */ +export function parseWorkflowWorkerToParentMessage( + raw: unknown, +): Result { + if (!isPlainRecord(raw)) { + return err(new Error("Worker IPC message is not an object")); + } + const obj = raw; + if (typeof obj.type !== "string") { + return err(new Error("Worker IPC message missing string 'type' field")); + } + if (!WORKFLOW_WORKER_MSG_TYPES.has(obj.type)) { + return err(new Error(`Unknown workflow worker IPC message type: "${obj.type}"`)); + } + if (obj.type === "thread-event") return parseThreadEventMsg(obj); + if (obj.type === "workflow-error") return parseWorkflowErrorMsg(obj); + return parseThreadWorkflowMessageMsg(obj); +} + +/** Parse messages from a workflow worker child (thread IPC + optional `ready`). */ +export function parseWorkflowChildMessage(raw: unknown): Result { + if (!isPlainRecord(raw)) { + return err(new Error("Worker IPC message is not an object")); + } + const obj = raw; + if (typeof obj.type !== "string") { + return err(new Error("Worker IPC message missing string 'type' field")); + } + if (obj.type === "ready") { + return ok({ type: "ready" }); + } + return parseWorkflowWorkerToParentMessage(raw); +} diff --git a/packages/daemon/src/workflow-manager-support.ts b/packages/workflow/src/manager-support.ts similarity index 90% rename from packages/daemon/src/workflow-manager-support.ts rename to packages/workflow/src/manager-support.ts index 800030c..bde8ef7 100644 --- a/packages/daemon/src/workflow-manager-support.ts +++ b/packages/workflow/src/manager-support.ts @@ -2,15 +2,13 @@ * Pure helpers and IPC branching for workflow-manager (keeps workflow-manager.ts lean). */ -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -import type { WorkflowMessage } from "@uncaged/nerve-core"; -import { START, isPlainRecord } from "@uncaged/nerve-core"; +import { isPlainRecord } from "@uncaged/nerve-core"; import type { LogStore, WorkflowRunStatus } from "@uncaged/nerve-store"; import type { ResumeThreadMessage, ThreadEventMessage } from "./ipc.js"; -import type { WorkerToParentMessage } from "./ipc.js"; +import type { WorkflowChildToParentMessage } from "./ipc.js"; +import type { WorkflowMessage } from "./types.js"; +import { START } from "./types.js"; export type PendingThread = { runId: string; @@ -33,7 +31,7 @@ export const WORKFLOW_WORKER_RESPAWN = { } as const; /** - * Worker shutdown timeout — must stay in sync with SHUTDOWN_TIMEOUT_MS in workflow-worker.ts. + * Worker shutdown timeout — must stay in sync with shutdown handling in `worker.ts`. * The drain timeout passed to drainAndRespawn must be >= this value so the worker has * enough time to finish in-flight threads before the parent force-kills it. */ @@ -79,12 +77,6 @@ export function ensureThreadMessagesWithStart( return [start, ...mapped]; } -export function resolveWorkflowWorkerScript(): string { - const __filename = fileURLToPath(import.meta.url); - const __dir = dirname(__filename); - return join(__dir, "workflow-worker.js"); -} - export function mapWorkflowRunStatus(eventType: string): WorkflowRunStatus | null { const map: Record = { started: "started", @@ -223,9 +215,13 @@ export type WorkflowManagerMessageDeps = { export function dispatchWorkflowWorkerMessage( workflowName: string, - msg: WorkerToParentMessage, + msg: WorkflowChildToParentMessage, deps: WorkflowManagerMessageDeps, ): void { + if (msg.type === "ready") { + return; + } + if (msg.type === "thread-event") { deps.handleThreadEvent(workflowName, msg); return; @@ -247,10 +243,5 @@ export function dispatchWorkflowWorkerMessage( `[workflow-manager] workflow-error for runId "${msg.runId}" in "${workflowName}": ${msg.error}\n`, ); deps.onWorkflowRoleError(workflowName, msg.runId, msg.error, msg.exitCode); - return; - } - - if (msg.type === "error") { - process.stderr.write(`[workflow-manager] error from "${workflowName}" worker: ${msg.error}\n`); } } diff --git a/packages/daemon/src/workflow-manager.ts b/packages/workflow/src/manager.ts similarity index 97% rename from packages/daemon/src/workflow-manager.ts rename to packages/workflow/src/manager.ts index 9048097..1f22044 100644 --- a/packages/daemon/src/workflow-manager.ts +++ b/packages/workflow/src/manager.ts @@ -6,16 +6,13 @@ * Concurrency and overflow (drop/queue) are enforced here in the parent process. */ -import type { NerveConfig, WorkflowConfig, WorkflowStatus } from "@uncaged/nerve-core"; +import type { NerveConfig, WorkflowStatus } from "@uncaged/nerve-core"; + +import type { WorkflowConfig } from "./config.js"; import type { LogStore } from "@uncaged/nerve-store"; import type { KillThreadMessage, StartThreadMessage, ThreadEventMessage } from "./ipc.js"; -import { parseWorkerMessage } from "./ipc.js"; -import { - createWorkerRuntime, - formatCapturedStderrTail, - formatChildExitSummary, -} from "./worker-runtime.js"; +import { parseWorkflowChildMessage } from "./ipc.js"; import { DEFAULT_MAX_QUEUE, WORKER_SHUTDOWN_TIMEOUT_MS, @@ -25,8 +22,13 @@ import { dispatchWorkflowWorkerMessage, extractExitCodeFromPayload, recoverThreadsFromStore, - resolveWorkflowWorkerScript, -} from "./workflow-manager-support.js"; +} from "./manager-support.js"; +import { WORKFLOW_WORKER_PATH } from "./paths.js"; +import { + createWorkerRuntime, + formatCapturedStderrTail, + formatChildExitSummary, +} from "./worker-runtime.js"; export type WorkflowLaunchParams = { prompt: string; @@ -73,7 +75,7 @@ export function createWorkflowManager( initialConfig: NerveConfig, logStore: LogStore, ): WorkflowManager { - const workerScript = resolveWorkflowWorkerScript(); + const workerScript = WORKFLOW_WORKER_PATH; /** * Default drain timeout must be at least WORKER_SHUTDOWN_TIMEOUT_MS so the worker @@ -309,7 +311,7 @@ export function createWorkflowManager( } function handleWorkerMessage(workflowName: string, raw: unknown): void { - const result = parseWorkerMessage(raw); + const result = parseWorkflowChildMessage(raw); if (!result.ok) { process.stderr.write( `[workflow-manager] invalid message from "${workflowName}" worker: ${result.error.message}\n`, diff --git a/packages/workflow/src/paths.ts b/packages/workflow/src/paths.ts new file mode 100644 index 0000000..4bc2a95 --- /dev/null +++ b/packages/workflow/src/paths.ts @@ -0,0 +1,5 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Resolved path to the workflow child-process entry (`worker.js` adjacent to this module in dist). */ +export const WORKFLOW_WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), "worker.js"); diff --git a/packages/workflow/src/public-types.ts b/packages/workflow/src/public-types.ts new file mode 100644 index 0000000..0e39bff --- /dev/null +++ b/packages/workflow/src/public-types.ts @@ -0,0 +1,22 @@ +/** + * Narrow surface for `@uncaged/nerve-core` — workflow automaton types + config only. + * Keeps core's declaration graph free of IPC / manager / store. + */ + +export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "./types.js"; +export type { + WorkflowMessage, + RoleResult, + Role, + RoleMeta, + StartStep, + ThreadContext, + WorkflowContext, + AgentFn, + RoleStep, + ModeratorContext, + Moderator, + WorkflowDefinition, +} from "./types.js"; + +export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig } from "./config.js"; diff --git a/packages/core/src/workflow.ts b/packages/workflow/src/types.ts similarity index 100% rename from packages/core/src/workflow.ts rename to packages/workflow/src/types.ts diff --git a/packages/daemon/src/worker-runtime.ts b/packages/workflow/src/worker-runtime.ts similarity index 100% rename from packages/daemon/src/worker-runtime.ts rename to packages/workflow/src/worker-runtime.ts diff --git a/packages/workflow/src/worker-signals.ts b/packages/workflow/src/worker-signals.ts new file mode 100644 index 0000000..b9b20ef --- /dev/null +++ b/packages/workflow/src/worker-signals.ts @@ -0,0 +1,17 @@ +/** + * Worker-process signal handling (fork IPC children only). + * Worker entrypoints import this module — not worker-runtime.ts (parent/kernel code). + */ + +/** + * Forked workers inherit the parent's process group. In foreground `nerve dev`, + * terminal-driven SIGINT/SIGTERM is delivered to the whole group, so workers can exit + * on the default handler before the kernel sends `{ type: "shutdown" }` over IPC. + * Swallow these in worker processes so the parent coordinates shutdown (issue #55). + * Only call when `process.send` is defined (fork IPC); standalone `node …-worker.js` keeps default Ctrl+C behaviour. + */ +export function ignoreSessionBroadcastSignals(): void { + const swallow = (): void => {}; + process.on("SIGINT", swallow); + process.on("SIGTERM", swallow); +} diff --git a/packages/daemon/src/workflow-worker.ts b/packages/workflow/src/worker.ts similarity index 97% rename from packages/daemon/src/workflow-worker.ts rename to packages/workflow/src/worker.ts index 29c36e5..c2ab882 100644 --- a/packages/daemon/src/workflow-worker.ts +++ b/packages/workflow/src/worker.ts @@ -14,6 +14,14 @@ import "./experimental-warning-suppression.js"; import { existsSync } from "node:fs"; import { join, resolve } from "node:path"; +import { isPlainRecord } from "@uncaged/nerve-core"; + +import type { + ThreadEventType, + ThreadWorkflowMessage, + WorkflowWorkerOutboundMessage, +} from "./ipc.js"; +import { parseWorkflowParentMessage } from "./ipc.js"; import type { RoleMeta, RoleStep, @@ -21,22 +29,15 @@ import type { ThreadContext, WorkflowDefinition, WorkflowMessage, -} from "@uncaged/nerve-core"; -import { END, START, isPlainRecord } from "@uncaged/nerve-core"; - -import type { - ThreadEventType, - ThreadWorkflowMessageMessage, - WorkerToParentMessage, -} from "./ipc.js"; -import { parseParentMessage } from "./ipc.js"; +} from "./types.js"; +import { END, START } from "./types.js"; import { ignoreSessionBroadcastSignals } from "./worker-signals.js"; // --------------------------------------------------------------------------- // IPC helpers // --------------------------------------------------------------------------- -function send(msg: WorkerToParentMessage): void { +function send(msg: WorkflowWorkerOutboundMessage): void { if (process.send) { process.send(msg); } @@ -55,7 +56,7 @@ function sendWorkflowError(runId: string, error: string, exitCode = 1): void { } function sendWorkflowMessage(runId: string, message: WorkflowMessage): void { - const msg: ThreadWorkflowMessageMessage = { + const msg: ThreadWorkflowMessage = { type: "thread-workflow-message", runId, message: { @@ -334,7 +335,7 @@ function handleMessage( killFlags: Map, shuttingDown: { value: boolean }, ): void { - const parseResult = parseParentMessage(raw); + const parseResult = parseWorkflowParentMessage(raw); if (!parseResult.ok) { process.stderr.write(`[workflow-worker] Invalid IPC message: ${parseResult.error.message}\n`); return; diff --git a/packages/workflow/tsconfig.json b/packages/workflow/tsconfig.json new file mode 100644 index 0000000..9036088 --- /dev/null +++ b/packages/workflow/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": false + }, + "include": ["src"] +} diff --git a/packages/workflow/tsconfig.public-types.json b/packages/workflow/tsconfig.public-types.json new file mode 100644 index 0000000..6972ed5 --- /dev/null +++ b/packages/workflow/tsconfig.public-types.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "composite": false, + "noEmit": false + }, + "include": ["src/types.ts", "src/config.ts", "src/public-types.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f48b7d..17d5c24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@uncaged/nerve-core': workspace:* + '@uncaged/nerve-store': workspace:* + importers: .: @@ -26,6 +30,9 @@ importers: '@uncaged/nerve-core': specifier: workspace:* version: link:../core + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow devDependencies: '@rslib/core': specifier: ^0.21.3 @@ -42,6 +49,9 @@ importers: '@uncaged/nerve-core': specifier: workspace:* version: link:../core + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow devDependencies: '@rslib/core': specifier: ^0.21.3 @@ -61,6 +71,9 @@ importers: '@uncaged/nerve-store': specifier: workspace:* version: link:../store + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow citty: specifier: ^0.1.6 version: 0.1.6 @@ -108,6 +121,9 @@ importers: '@uncaged/nerve-store': specifier: workspace:* version: link:../store + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow yaml: specifier: ^2.8.3 version: 2.8.3 @@ -161,6 +177,9 @@ importers: '@uncaged/nerve-workflow-utils': specifier: workspace:* version: link:../workflow-utils + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow zod: specifier: ^4.3.6 version: 4.3.6 @@ -183,6 +202,9 @@ importers: '@uncaged/nerve-workflow-utils': specifier: workspace:* version: link:../workflow-utils + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow zod: specifier: ^4.3.6 version: 4.3.6 @@ -215,6 +237,22 @@ importers: specifier: ^4.1.5 version: 4.1.5(@types/node@22.19.17)(vite@8.0.9(@types/node@22.19.17)(esbuild@0.27.7)(yaml@2.8.3)) + packages/workflow: + dependencies: + '@uncaged/nerve-core': + specifier: workspace:* + version: link:../core + '@uncaged/nerve-store': + specifier: workspace:* + version: link:../store + devDependencies: + '@rslib/core': + specifier: ^0.21.3 + version: 0.21.3(typescript@5.9.3) + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + packages/workflow-meta: dependencies: '@uncaged/nerve-core': @@ -229,6 +267,9 @@ importers: '@uncaged/nerve-workflow-utils': specifier: workspace:* version: link:../workflow-utils + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow zod: specifier: ^4.3.6 version: 4.3.6 @@ -254,6 +295,9 @@ importers: '@uncaged/nerve-core': specifier: workspace:* version: link:../core + '@uncaged/workflow': + specifier: workspace:* + version: link:../workflow zod: specifier: ^4.3.6 version: 4.3.6