feat(execute): create @uncaged/workflow-execute + CLI migration

Phase 7: Engine + extract + workflow-as-agent merged into execute package.
All CLI imports migrated from @uncaged/workflow to specific packages.
105 CLI tests pass, 0 failures.

Changes:
- New @uncaged/workflow-execute package (engine/, extract/, workflow-as-agent)
- CLI src/ and __tests__/ rewritten to import from split packages
- bundle-validator updated to allow @uncaged/workflow-cas imports
- ensure-uncaged-workflow-symlink creates symlinks for all new packages

Ref: #143, closes #150
This commit is contained in:
2026-05-09 11:35:03 +08:00
parent b07f8cf166
commit 9bbdfc41bd
63 changed files with 2175 additions and 107 deletions
@@ -3,13 +3,9 @@ import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promise
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
createContentMerkleNode,
getGlobalCasDir,
getRegisteredWorkflow,
readWorkflowRegistry,
serializeMerkleNode,
} from "@uncaged/workflow";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow-cas";
import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register";
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/commands/cas/index.js";
import {
cmdAdd,
@@ -25,7 +21,7 @@ import { addCliArgs } from "./bundle-fixture.js";
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {} };
`;
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow";
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
`;
function casStoredForm(raw: string): string {
@@ -2,7 +2,8 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createCasStore, getContentMerklePayload, getGlobalCasDir } from "@uncaged/workflow";
import { createCasStore, getContentMerklePayload } from "@uncaged/workflow-cas";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { cmdFork, cmdRun } from "../src/commands/thread/index.js";
import { cmdAdd } from "../src/commands/workflow/index.js";
import { pathExists } from "../src/fs-utils.js";
@@ -10,7 +11,7 @@ import { addCliArgs } from "./bundle-fixture.js";
import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js";
/** Three-role workflow that respects `input.steps` for fork/resume. */
const threeRoleBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow";
const threeRoleBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
export const descriptor = {
description: "fork-cli",
@@ -4,12 +4,9 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import {
createCasStore,
garbageCollectCas,
getGlobalCasDir,
putContentMerkleNode,
} from "@uncaged/workflow";
import { createCasStore, putContentMerkleNode } from "@uncaged/workflow-cas";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { garbageCollectCas } from "@uncaged/workflow-execute";
import { cmdThreadRemove } from "../src/commands/thread/index.js";
import { pathExists } from "../src/fs-utils.js";
@@ -50,7 +50,6 @@ describe("init template", () => {
dependencies: Record<string, string>;
};
expect(pkg.type).toBe("module");
expect(pkg.dependencies["@uncaged/workflow"]).toBeDefined();
expect(pkg.dependencies["@uncaged/workflow-runtime"]).toBeDefined();
expect(pkg.dependencies.zod).toBeDefined();
expect(pkg.name).toContain("review-pr");
@@ -46,7 +46,7 @@ describe("init workspace", () => {
dependencies: Record<string, string>;
};
expect(wfPkg.type).toBe("module");
expect(wfPkg.dependencies["@uncaged/workflow"]).toBeDefined();
expect(wfPkg.dependencies["@uncaged/workflow-runtime"]).toBeDefined();
expect(wfPkg.dependencies.zod).toBeDefined();
const tsconfig = JSON.parse(await readFile(join(root, "tsconfig.json"), "utf8")) as {
+2 -1
View File
@@ -5,7 +5,8 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { createCasStore, getGlobalCasDir, putContentMerkleNode } from "@uncaged/workflow";
import { createCasStore, putContentMerkleNode } from "@uncaged/workflow-cas";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import {
formatLiveDebugLine,
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow";
import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow-cas";
import { createApp } from "../src/commands/serve/app.js";
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow";
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util";
import { resolveWorkflowStorageRoot } from "../src/storage-env.js";
describe("resolveWorkflowStorageRoot", () => {
@@ -4,7 +4,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { getGlobalCasDir } from "@uncaged/workflow";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { cmdCasPut } from "../src/commands/cas/index.js";
import {
cmdKill,
@@ -21,7 +21,7 @@ import { pathExists, readTextFileIfExists } from "../src/fs-utils.js";
import { addCliArgs } from "./bundle-fixture.js";
import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js";
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow";
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
`;
const threadFixtureDescriptor = `export const descriptor = {
+5 -1
View File
@@ -6,8 +6,12 @@
"uncaged-workflow": "src/cli.ts"
},
"dependencies": {
"@uncaged/workflow-protocol": "workspace:*",
"@uncaged/workflow-util": "workspace:*",
"@uncaged/workflow-cas": "workspace:*",
"@uncaged/workflow-execute": "workspace:*",
"@uncaged/workflow-register": "workspace:*",
"@uncaged/workflow-runtime": "workspace:*",
"@uncaged/workflow": "workspace:*",
"hono": "^4.12.18",
"yaml": "^2.8.4"
},
+1 -1
View File
@@ -1,7 +1,7 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { pathExists } from "./fs-utils.js";
+2 -1
View File
@@ -1,4 +1,5 @@
import { type GcResult, garbageCollectCas, type Result } from "@uncaged/workflow";
import type { Result } from "@uncaged/workflow-protocol";
import { type GcResult, garbageCollectCas } from "@uncaged/workflow-execute";
export async function cmdGc(storageRoot: string): Promise<Result<GcResult, string>> {
return garbageCollectCas(storageRoot);
@@ -1,4 +1,6 @@
import { createCasStore, err, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { createCasStore } from "@uncaged/workflow-cas";
export async function cmdCasGet(
storageRoot: string,
@@ -1,4 +1,6 @@
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
import { ok, type Result } from "@uncaged/workflow-protocol";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { createCasStore } from "@uncaged/workflow-cas";
export async function cmdCasList(storageRoot: string): Promise<Result<string[], string>> {
const cas = createCasStore(getGlobalCasDir(storageRoot));
@@ -1,4 +1,6 @@
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
import { ok, type Result } from "@uncaged/workflow-protocol";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { createCasStore } from "@uncaged/workflow-cas";
export async function cmdCasPut(
storageRoot: string,
+3 -1
View File
@@ -1,4 +1,6 @@
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
import { ok, type Result } from "@uncaged/workflow-protocol";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { createCasStore } from "@uncaged/workflow-cas";
export async function cmdCasRm(storageRoot: string, hash: string): Promise<Result<void, string>> {
const cas = createCasStore(getGlobalCasDir(storageRoot));
@@ -1,7 +1,7 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { pathExists } from "../../fs-utils.js";
@@ -6,7 +6,6 @@ export function templatePackageJson(templateName: string): string {
private: true,
type: "module",
dependencies: {
"@uncaged/workflow": "^0.1.0",
"@uncaged/workflow-runtime": "^0.1.0",
zod: "^4.0.0",
},
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
/** Validates a single path segment for workspace / template names (no separators, not `.` / `..`). */
export function validateWorkspaceSegment(name: string): Result<void, string> {
@@ -1,7 +1,7 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { pathExists } from "../../fs-utils.js";
import type { CmdInitWorkspaceSuccess } from "./types.js";
@@ -28,7 +28,7 @@ function workflowsPackageJson(): string {
private: true,
type: "module",
dependencies: {
"@uncaged/workflow": "^0.1.0",
"@uncaged/workflow-runtime": "^0.1.0",
zod: "^4.0.0",
},
},
@@ -1,4 +1,6 @@
import { createCasStore, garbageCollectCas, getGlobalCasDir } from "@uncaged/workflow";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { createCasStore } from "@uncaged/workflow-cas";
import { garbageCollectCas } from "@uncaged/workflow-execute";
import { Hono } from "hono";
export function createCasRoutes(storageRoot: string): Hono {
@@ -2,7 +2,7 @@ import {
getRegisteredWorkflow,
listRegisteredWorkflowNames,
readWorkflowRegistry,
} from "@uncaged/workflow";
} from "@uncaged/workflow-register";
import { Hono } from "hono";
export function createWorkflowRoutes(storageRoot: string): Hono {
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { serve } from "bun";
import { printCliLine } from "../../cli-output.js";
@@ -1,4 +1,4 @@
import type { Result } from "@uncaged/workflow";
import type { Result } from "@uncaged/workflow-protocol";
import {
readWorkerCtl,
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import type { ParsedForkArgv } from "./types.js";
@@ -1,6 +1,8 @@
import { join } from "node:path";
import { buildForkPlan, err, generateUlid, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { generateUlid } from "@uncaged/workflow-util";
import { buildForkPlan } from "@uncaged/workflow-execute";
import { pathExists, readTextFileIfExists } from "../../fs-utils.js";
import { resolveThreadDataPath } from "../../thread-scan.js";
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { listHistoricalThreads } from "../../thread-scan.js";
import { validateCliWorkflowName } from "../../workflow-name.js";
@@ -2,15 +2,10 @@ import { watch } from "node:fs";
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import {
type CasStore,
createCasStore,
getContentMerklePayload,
getGlobalCasDir,
tryParseRoleStepRecord,
tryParseWorkflowResultRecord,
} from "@uncaged/workflow";
import type { WorkflowCompletion } from "@uncaged/workflow-runtime";
import type { CasStore, WorkflowCompletion } from "@uncaged/workflow-protocol";
import { getGlobalCasDir } from "@uncaged/workflow-util";
import { createCasStore, getContentMerklePayload } from "@uncaged/workflow-cas";
import { tryParseRoleStepRecord, tryParseWorkflowResultRecord } from "@uncaged/workflow-execute";
import { dimGreyLine, highlightLiveRole } from "../../cli-color.js";
import { printCliError, printCliLine } from "../../cli-output.js";
@@ -1,7 +1,8 @@
import { unlink } from "node:fs/promises";
import { dirname, join } from "node:path";
import { err, garbageCollectCas, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { garbageCollectCas } from "@uncaged/workflow-execute";
import { resolveThreadDataPath } from "../../thread-scan.js";
@@ -1,13 +1,8 @@
import { join } from "node:path";
import {
err,
generateUlid,
getRegisteredWorkflow,
ok,
type Result,
readWorkflowRegistry,
} from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { generateUlid } from "@uncaged/workflow-util";
import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register";
import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js";
import { validateCliWorkflowName } from "../../workflow-name.js";
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { readTextFileIfExists } from "../../fs-utils.js";
import { resolveThreadDataPath } from "../../thread-scan.js";
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import type { ParsedAddArgv } from "./types.js";
@@ -1,18 +1,16 @@
import { readFile, stat } from "node:fs/promises";
import { basename, resolve } from "node:path";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { hashWorkflowBundleBytes } from "@uncaged/workflow-cas";
import {
err,
extractBundleExports,
hashWorkflowBundleBytes,
ok,
type Result,
readWorkflowRegistry,
registerWorkflowVersion,
stringifyWorkflowDescriptor,
validateWorkflowBundle,
writeWorkflowRegistry,
} from "@uncaged/workflow";
} from "@uncaged/workflow-register";
import { storeWorkflowBundleArtifacts } from "../../bundle-store.js";
import { validateCliWorkflowName } from "../../workflow-name.js";
@@ -1,10 +1,5 @@
import {
err,
getRegisteredWorkflow,
ok,
type Result,
readWorkflowRegistry,
} from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register";
import { validateCliWorkflowName } from "../../workflow-name.js";
@@ -1,11 +1,9 @@
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import {
err,
listRegisteredWorkflowNames,
ok,
type Result,
readWorkflowRegistry,
type WorkflowRegistryFile,
} from "@uncaged/workflow";
} from "@uncaged/workflow-register";
export async function cmdList(storageRoot: string): Promise<Result<WorkflowRegistryFile, string>> {
const reg = await readWorkflowRegistry(storageRoot);
@@ -1,11 +1,9 @@
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import {
err,
ok,
type Result,
readWorkflowRegistry,
unregisterWorkflow,
writeWorkflowRegistry,
} from "@uncaged/workflow";
} from "@uncaged/workflow-register";
import { validateCliWorkflowName } from "../../workflow-name.js";
@@ -1,14 +1,12 @@
import { join } from "node:path";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import {
err,
getRegisteredWorkflow,
ok,
type Result,
readWorkflowRegistry,
rollbackWorkflowToHistoryHash,
writeWorkflowRegistry,
} from "@uncaged/workflow";
} from "@uncaged/workflow-register";
import { pathExists } from "../../fs-utils.js";
import { validateCliWorkflowName } from "../../workflow-name.js";
@@ -1,11 +1,9 @@
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import {
err,
getRegisteredWorkflow,
ok,
type Result,
readWorkflowRegistry,
type WorkflowRegistryEntry,
} from "@uncaged/workflow";
} from "@uncaged/workflow-register";
import { stringify } from "yaml";
import { validateCliWorkflowName } from "../../workflow-name.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
export type ParsedLiveArgv = {
threadId: string | null;
+1 -1
View File
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
export type ParsedRunArgv = {
name: string;
+1 -1
View File
@@ -1,4 +1,4 @@
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow";
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util";
/**
* Resolve storage root with env var override support.
+2 -1
View File
@@ -3,7 +3,8 @@ import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
import { createConnection } from "node:net";
import { join } from "node:path";
import { err, getWorkerHostScriptPath, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { getWorkerHostScriptPath } from "@uncaged/workflow-execute";
import { pathExists, readTextFileIfExists } from "./fs-utils.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { err, ok, type Result } from "@uncaged/workflow";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
const WORKFLOW_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;