Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bdb18afd0 | |||
| 2af299f3ce | |||
| d9f79c60a1 | |||
| 485bfcb0b6 | |||
| a47ed06ea5 | |||
| 2ef004eecf | |||
| 2616259a0f | |||
| 23b2c3b47d | |||
| 7d3954097d | |||
| 4a925b98af | |||
| bfea771a52 | |||
| 5e411a1f19 | |||
| 21238f7825 | |||
| 6b3aa4ce35 | |||
| f042c9d640 | |||
| 66bca9ef03 | |||
| 309af39447 | |||
| 86a422f7e2 | |||
| 648f0c6dec | |||
| 8456a8337b | |||
| 9c8b98a551 | |||
| c3272be760 | |||
| c44b773a86 | |||
| 2776f8e419 | |||
| 7b0e256c13 | |||
| c663ba9e9c | |||
| 71b413f20c |
@@ -0,0 +1,71 @@
|
||||
# @uncaged/workflow
|
||||
|
||||
A workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file identified by its XXH64 hash (Crockford Base32).
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Workflow** | A single-file ESM module exporting `run` (workflow function) and `descriptor` (metadata). Identified by its XXH64 hash. |
|
||||
| **Bundle** | The physical `.esm.js` file stored in `~/.uncaged/workflow/bundles/`. |
|
||||
| **Thread** | A single execution of a workflow, identified by a ULID. Persisted as `.data.jsonl` + `.info.jsonl`. |
|
||||
| **Role** | A named actor within a workflow. Each role produces output with typed `meta`. Roles live inside template packages (`src/roles/`). |
|
||||
| **Registry** | `workflow.yaml` — maps workflow names to current/historical bundle hashes. |
|
||||
| **CAS** | Content-Addressed Storage — bundles are immutable and addressed by hash. |
|
||||
|
||||
## Monorepo Packages
|
||||
|
||||
```
|
||||
packages/
|
||||
workflow/ # @uncaged/workflow — core lib (types, engine, hash, ULID, registry)
|
||||
cli-workflow/ # @uncaged/cli-workflow — CLI (`uncaged-workflow` command)
|
||||
workflow-template-develop/ # @uncaged/workflow-template-develop — develop workflow template (includes roles)
|
||||
workflow-template-solve-issue/ # @uncaged/workflow-template-solve-issue — solve-issue workflow template (includes roles)
|
||||
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — Hermes agent adapter
|
||||
workflow-agent-cursor/ # @uncaged/workflow-agent-cursor — Cursor agent adapter
|
||||
workflow-agent-llm/ # @uncaged/workflow-agent-llm — LLM agent adapter
|
||||
workflow-util-agent/ # @uncaged/workflow-util-agent — agent utilities (buildAgentPrompt, spawnCli)
|
||||
```
|
||||
|
||||
Managed with **bun workspace** using the `workspace:*` protocol.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Build all packages
|
||||
bun run build
|
||||
|
||||
# Register a workflow bundle
|
||||
uncaged-workflow workflow add solve-issue dist/packages/workflow-template-solve-issue/solve-issue.esm.js
|
||||
|
||||
# Run a workflow
|
||||
uncaged-workflow run solve-issue --prompt "Fix bug #42"
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
uncaged-workflow help # Show all commands
|
||||
uncaged-workflow workflow list # List registered workflows
|
||||
uncaged-workflow run <name> # Start a workflow thread
|
||||
uncaged-workflow thread list # List all threads
|
||||
uncaged-workflow thread show <id> # Inspect a thread
|
||||
uncaged-workflow skill # Agent-consumable reference docs
|
||||
```
|
||||
|
||||
See `uncaged-workflow help` for the full command reference.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
bun run check # Biome lint + format check
|
||||
bun run format # Auto-format with Biome
|
||||
bun test # Run tests
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
See [docs/architecture.md](docs/architecture.md) for the full design — three-phase engine loop, bundle contract, storage layout, and design decisions.
|
||||
@@ -17,11 +17,8 @@ A workflow engine that executes single-file ESM bundles. Each workflow is a self
|
||||
| `workflow-agent-cursor` | `@uncaged/workflow-agent-cursor` | Cursor CLI agent (extracts workspace from ctx) |
|
||||
| `workflow-agent-hermes` | `@uncaged/workflow-agent-hermes` | Hermes CLI agent |
|
||||
| `workflow-agent-llm` | `@uncaged/workflow-agent-llm` | OpenAI-compatible LLM agent |
|
||||
| `workflow-role-planner` | `@uncaged/workflow-role-planner` | Pure data: phased planning prompt + schema |
|
||||
| `workflow-role-coder` | `@uncaged/workflow-role-coder` | Pure data: coding prompt + schema |
|
||||
| `workflow-role-reviewer` | `@uncaged/workflow-role-reviewer` | Pure data: code review prompt + schema |
|
||||
| `workflow-role-committer` | `@uncaged/workflow-role-committer` | Pure data: git commit prompt + schema |
|
||||
| `workflow-template-solve-issue` | `@uncaged/workflow-template-solve-issue` | Composes roles + moderator into a complete workflow |
|
||||
| `workflow-template-develop` | `@uncaged/workflow-template-develop` | Develop workflow template (roles in `src/roles/`) |
|
||||
| `workflow-template-solve-issue` | `@uncaged/workflow-template-solve-issue` | Solve-issue workflow template (roles in `src/roles/`) |
|
||||
| `workflow-util-agent` | `@uncaged/workflow-util-agent` | `buildAgentPrompt` + `spawnCli` utilities |
|
||||
|
||||
Monorepo with **bun workspace**, `workspace:*` protocol.
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"examples"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bun run --filter '*' build",
|
||||
"check": "bunx tsc --build && biome check .",
|
||||
"typecheck": "bunx tsc --build",
|
||||
"format": "biome format --write .",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ParsedAddArgv } from "../src/cmd-add.js";
|
||||
import type { ParsedAddArgv } from "../src/commands/workflow/add.js";
|
||||
|
||||
export function addCliArgs(name: string, filePath: string): ParsedAddArgv {
|
||||
return { name, filePath, typesPath: null };
|
||||
|
||||
@@ -4,13 +4,16 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { getGlobalCasDir, getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow";
|
||||
import { cmdAdd } from "../src/cmd-add.js";
|
||||
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/cmd-cas.js";
|
||||
import { cmdHistory } from "../src/cmd-history.js";
|
||||
import { cmdList, formatListLines } from "../src/cmd-list.js";
|
||||
import { cmdRemove } from "../src/cmd-remove.js";
|
||||
import { cmdRollback } from "../src/cmd-rollback.js";
|
||||
import { cmdShow } from "../src/cmd-show.js";
|
||||
import { cmdCasGet } from "../src/commands/cas/get.js";
|
||||
import { cmdCasList } from "../src/commands/cas/list.js";
|
||||
import { cmdCasPut } from "../src/commands/cas/put.js";
|
||||
import { cmdCasRm } from "../src/commands/cas/rm.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/add.js";
|
||||
import { cmdHistory } from "../src/commands/workflow/history.js";
|
||||
import { cmdList, formatListLines } from "../src/commands/workflow/list.js";
|
||||
import { cmdRemove } from "../src/commands/workflow/rm.js";
|
||||
import { cmdRollback } from "../src/commands/workflow/rollback.js";
|
||||
import { cmdShow } from "../src/commands/workflow/show.js";
|
||||
import { addCliArgs } from "./bundle-fixture.js";
|
||||
|
||||
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {} };
|
||||
|
||||
@@ -3,9 +3,9 @@ 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 { cmdAdd } from "../src/cmd-add.js";
|
||||
import { cmdFork } from "../src/cmd-fork.js";
|
||||
import { cmdRun } from "../src/cmd-run.js";
|
||||
import { cmdFork } from "../src/commands/thread/fork.js";
|
||||
import { cmdRun } from "../src/commands/thread/run.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/add.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
import { addCliArgs } from "./bundle-fixture.js";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getGlobalCasDir,
|
||||
putContentMerkleNode,
|
||||
} from "@uncaged/workflow";
|
||||
import { cmdThreadRemove } from "../src/cmd-thread.js";
|
||||
import { cmdThreadRemove } from "../src/commands/thread/rm.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
|
||||
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { runCli } from "../src/cli-dispatch.js";
|
||||
import { formatSkillDoc } from "../src/cmd-help.js";
|
||||
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
||||
import {
|
||||
formatSkillDoc,
|
||||
formatSkillIndex,
|
||||
formatSkillTopic,
|
||||
getSkillTopics,
|
||||
} from "../src/skill.js";
|
||||
|
||||
const STORAGE_ROOT = "/tmp/help-test-storage";
|
||||
|
||||
@@ -10,13 +15,120 @@ describe("help command", () => {
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("help --skill returns 0", async () => {
|
||||
test("no args prints usage (not red) and returns 1", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, []);
|
||||
expect(code).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("skill command", () => {
|
||||
test("skill (no topic) lists topics and returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["skill"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("skill cli returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["skill", "cli"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("skill develop returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["skill", "develop"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("skill author returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["skill", "author"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("skill unknown returns 1", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["skill", "unknown"]);
|
||||
expect(code).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("--help flag on groups", () => {
|
||||
test("workflow --help returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["workflow", "--help"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("thread --help returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["thread", "--help"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("cas --help returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["cas", "--help"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("init --help returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["init", "--help"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy help --skill compat", () => {
|
||||
test("help --skill still works (lists topics)", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help", "--skill"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillDoc", () => {
|
||||
describe("getSkillTopics", () => {
|
||||
test("returns all topics", () => {
|
||||
const topics = getSkillTopics();
|
||||
const names = topics.map((t) => t.name);
|
||||
expect(names).toContain("cli");
|
||||
expect(names).toContain("develop");
|
||||
expect(names).toContain("author");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillIndex", () => {
|
||||
test("lists all topics", () => {
|
||||
const idx = formatSkillIndex();
|
||||
expect(idx).toContain("# uncaged-workflow skill");
|
||||
expect(idx).not.toContain("# uncaged-workflow help --skill");
|
||||
expect(idx).toContain("cli");
|
||||
expect(idx).toContain("develop");
|
||||
expect(idx).toContain("author");
|
||||
expect(idx).toContain("skill <topic>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCliUsage", () => {
|
||||
test("has tagline, grouped sections, help hint, and env vars", () => {
|
||||
const u = formatCliUsage();
|
||||
expect(u.startsWith("uncaged-workflow — workflow engine CLI")).toBe(true);
|
||||
expect(u).toContain("Workflow registry:");
|
||||
expect(u).toContain("Thread execution:");
|
||||
expect(u).toContain("Content-addressable storage:");
|
||||
expect(u).toContain("Development:");
|
||||
expect(u).toContain("Shortcuts:");
|
||||
expect(u).toContain("Reference:");
|
||||
expect(u).toContain("skill [topic]");
|
||||
expect(u).toContain("Agent-consumable docs");
|
||||
expect(u).toContain("Use <command> --help for subcommand details.");
|
||||
expect(u).toContain("Environment variables:");
|
||||
expect(u).toContain("WORKFLOW_STORAGE_ROOT");
|
||||
expect(u).toContain("UNCAGED_WORKFLOW_STORAGE_ROOT");
|
||||
});
|
||||
|
||||
test("lists commands from registry with descriptions", () => {
|
||||
const u = formatCliUsage();
|
||||
expect(u).toContain("workflow add");
|
||||
expect(u).toContain("Register a workflow bundle in the registry");
|
||||
expect(u).toContain("thread run");
|
||||
expect(u).toContain("Start a new thread executing a workflow");
|
||||
expect(u).toContain("cas gc");
|
||||
expect(u).toContain("Garbage-collect unreferenced CAS entries");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic('cli') — legacy formatSkillDoc", () => {
|
||||
const doc = formatSkillDoc();
|
||||
|
||||
test("contains title", () => {
|
||||
@@ -82,3 +194,52 @@ describe("formatSkillDoc", () => {
|
||||
expect(doc).toContain("## Typical Workflow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic('develop')", () => {
|
||||
const doc = formatSkillTopic("develop");
|
||||
|
||||
test("returns non-null", () => {
|
||||
expect(doc).not.toBeNull();
|
||||
});
|
||||
|
||||
test("contains thread ID info", () => {
|
||||
expect(doc).toContain("Thread ID");
|
||||
expect(doc).toContain("Crockford Base32");
|
||||
});
|
||||
|
||||
test("contains CAS commands", () => {
|
||||
expect(doc).toContain("cas put");
|
||||
expect(doc).toContain("cas get");
|
||||
});
|
||||
|
||||
test("contains meta output section", () => {
|
||||
expect(doc).toContain("Meta Output");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic('author')", () => {
|
||||
const doc = formatSkillTopic("author");
|
||||
|
||||
test("returns non-null", () => {
|
||||
expect(doc).not.toBeNull();
|
||||
});
|
||||
|
||||
test("contains bundle structure", () => {
|
||||
expect(doc).toContain("Bundle Structure");
|
||||
expect(doc).toContain(".esm.js");
|
||||
});
|
||||
|
||||
test("contains descriptor info", () => {
|
||||
expect(doc).toContain("WorkflowDescriptor");
|
||||
});
|
||||
|
||||
test("contains role definition", () => {
|
||||
expect(doc).toContain("Role Definition");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic unknown", () => {
|
||||
test("returns null for unknown topic", () => {
|
||||
expect(formatSkillTopic("nonexistent")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { runCli } from "../src/cli-dispatch.js";
|
||||
import { cmdInitTemplate, cmdInitWorkspace } from "../src/cmd-init.js";
|
||||
import { cmdInitTemplate } from "../src/commands/init/template.js";
|
||||
import { cmdInitWorkspace } from "../src/commands/init/workspace.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
|
||||
describe("init template", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
||||
import { cmdInitWorkspace } from "../src/cmd-init.js";
|
||||
import { cmdInitWorkspace } from "../src/commands/init/workspace.js";
|
||||
import { pathExists } from "../src/fs-utils.js";
|
||||
|
||||
describe("init workspace", () => {
|
||||
@@ -129,8 +129,9 @@ describe("init workspace", () => {
|
||||
|
||||
test("usage lists init subcommands", () => {
|
||||
const u = formatCliUsage();
|
||||
expect(u).toContain("uncaged-workflow init workspace <name>");
|
||||
expect(u).toContain("uncaged-workflow init template <name>");
|
||||
expect(u).toContain("init workspace <name>");
|
||||
expect(u).toContain("init template <name>");
|
||||
expect(u).toContain("Development:");
|
||||
});
|
||||
|
||||
test("runCli rejects unknown init subcommand", async () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
LIVE_CONTENT_MAX_LINES,
|
||||
type LiveRoleRow,
|
||||
renderLiveRoleStepLines,
|
||||
} from "../src/cmd-live.js";
|
||||
} from "../src/commands/thread/live.js";
|
||||
import { parseLiveArgv } from "../src/live-argv.js";
|
||||
|
||||
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow";
|
||||
import { resolveWorkflowStorageRoot } from "../src/storage-env.js";
|
||||
|
||||
describe("resolveWorkflowStorageRoot", () => {
|
||||
let savedInternal: string | undefined;
|
||||
let savedUser: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
savedInternal = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||
savedUser = process.env.WORKFLOW_STORAGE_ROOT;
|
||||
delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||
delete process.env.WORKFLOW_STORAGE_ROOT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedInternal === undefined) {
|
||||
delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||
} else {
|
||||
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = savedInternal;
|
||||
}
|
||||
if (savedUser === undefined) {
|
||||
delete process.env.WORKFLOW_STORAGE_ROOT;
|
||||
} else {
|
||||
process.env.WORKFLOW_STORAGE_ROOT = savedUser;
|
||||
}
|
||||
});
|
||||
|
||||
test("returns default when no env vars are set", () => {
|
||||
expect(resolveWorkflowStorageRoot()).toBe(getDefaultWorkflowStorageRoot());
|
||||
});
|
||||
|
||||
test("WORKFLOW_STORAGE_ROOT overrides default", () => {
|
||||
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/custom-storage";
|
||||
expect(resolveWorkflowStorageRoot()).toBe("/tmp/custom-storage");
|
||||
});
|
||||
|
||||
test("UNCAGED_WORKFLOW_STORAGE_ROOT takes priority over WORKFLOW_STORAGE_ROOT", () => {
|
||||
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/user-path";
|
||||
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = "/tmp/internal-path";
|
||||
expect(resolveWorkflowStorageRoot()).toBe("/tmp/internal-path");
|
||||
});
|
||||
|
||||
test("ignores empty WORKFLOW_STORAGE_ROOT", () => {
|
||||
process.env.WORKFLOW_STORAGE_ROOT = "";
|
||||
expect(resolveWorkflowStorageRoot()).toBe(getDefaultWorkflowStorageRoot());
|
||||
});
|
||||
|
||||
test("ignores empty UNCAGED_WORKFLOW_STORAGE_ROOT and falls through to WORKFLOW_STORAGE_ROOT", () => {
|
||||
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = "";
|
||||
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/user-fallback";
|
||||
expect(resolveWorkflowStorageRoot()).toBe("/tmp/user-fallback");
|
||||
});
|
||||
});
|
||||
@@ -5,15 +5,14 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getGlobalCasDir } from "@uncaged/workflow";
|
||||
import { cmdAdd } from "../src/cmd-add.js";
|
||||
import { cmdCasPut } from "../src/cmd-cas.js";
|
||||
import { cmdKill } from "../src/cmd-kill.js";
|
||||
import { cmdPause } from "../src/cmd-pause.js";
|
||||
import { cmdPs } from "../src/cmd-ps.js";
|
||||
import { cmdResume } from "../src/cmd-resume.js";
|
||||
import { cmdRun } from "../src/cmd-run.js";
|
||||
import { cmdThreadRemove, cmdThreadShow } from "../src/cmd-thread.js";
|
||||
import { cmdThreads } from "../src/cmd-threads.js";
|
||||
import { cmdCasPut } from "../src/commands/cas/put.js";
|
||||
import { cmdKill, cmdPause, cmdResume } from "../src/commands/thread/control.js";
|
||||
import { cmdThreads } from "../src/commands/thread/list.js";
|
||||
import { cmdPs } from "../src/commands/thread/ps.js";
|
||||
import { cmdThreadRemove } from "../src/commands/thread/rm.js";
|
||||
import { cmdRun } from "../src/commands/thread/run.js";
|
||||
import { cmdThreadShow } from "../src/commands/thread/show.js";
|
||||
import { cmdAdd } from "../src/commands/workflow/add.js";
|
||||
import { pathExists, readTextFileIfExists } from "../src/fs-utils.js";
|
||||
import { addCliArgs } from "./bundle-fixture.js";
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"yaml": "^2.8.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,72 @@
|
||||
import { printCliError, printCliLine, printCliWarn } from "./cli-output.js";
|
||||
import { cmdAdd, formatAddSuccess, parseAddArgv } from "./cmd-add.js";
|
||||
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "./cmd-cas.js";
|
||||
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
||||
import { cmdGc } from "./cmd-gc.js";
|
||||
import { formatSkillDoc } from "./cmd-help.js";
|
||||
import { cmdHistory } from "./cmd-history.js";
|
||||
import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js";
|
||||
import { cmdKill } from "./cmd-kill.js";
|
||||
import { cmdList, formatListLines } from "./cmd-list.js";
|
||||
import { cmdLive } from "./cmd-live.js";
|
||||
import { cmdPause } from "./cmd-pause.js";
|
||||
import { cmdPs } from "./cmd-ps.js";
|
||||
import { cmdRemove } from "./cmd-remove.js";
|
||||
import { cmdResume } from "./cmd-resume.js";
|
||||
import { cmdRollback } from "./cmd-rollback.js";
|
||||
import { cmdRun } from "./cmd-run.js";
|
||||
import { cmdShow, formatShowYaml } from "./cmd-show.js";
|
||||
import { cmdThreadRemove, cmdThreadShow } from "./cmd-thread.js";
|
||||
import { cmdThreads } from "./cmd-threads.js";
|
||||
import { cmdGc } from "./commands/cas/gc.js";
|
||||
import { cmdCasGet } from "./commands/cas/get.js";
|
||||
import { cmdCasList } from "./commands/cas/list.js";
|
||||
import { cmdCasPut } from "./commands/cas/put.js";
|
||||
import { cmdCasRm } from "./commands/cas/rm.js";
|
||||
import { cmdInitTemplate } from "./commands/init/template.js";
|
||||
import { cmdInitWorkspace } from "./commands/init/workspace.js";
|
||||
import { cmdKill, cmdPause, cmdResume } from "./commands/thread/control.js";
|
||||
import { cmdFork, parseForkArgv } from "./commands/thread/fork.js";
|
||||
import { cmdThreads } from "./commands/thread/list.js";
|
||||
import { cmdLive } from "./commands/thread/live.js";
|
||||
import { cmdPs } from "./commands/thread/ps.js";
|
||||
import { cmdThreadRemove } from "./commands/thread/rm.js";
|
||||
import { cmdRun } from "./commands/thread/run.js";
|
||||
import { cmdThreadShow } from "./commands/thread/show.js";
|
||||
import { cmdAdd, formatAddSuccess, parseAddArgv } from "./commands/workflow/add.js";
|
||||
import { cmdHistory } from "./commands/workflow/history.js";
|
||||
import { cmdList, formatListLines } from "./commands/workflow/list.js";
|
||||
import { cmdRemove } from "./commands/workflow/rm.js";
|
||||
import { cmdRollback } from "./commands/workflow/rollback.js";
|
||||
import { cmdShow, formatShowYaml } from "./commands/workflow/show.js";
|
||||
import { parseLiveArgv } from "./live-argv.js";
|
||||
import { parseRunArgv } from "./run-argv.js";
|
||||
|
||||
export function formatCliUsage(): string {
|
||||
return [
|
||||
"Usage:",
|
||||
" uncaged-workflow workflow add <name> <file.esm.js> [--types <path>]",
|
||||
" uncaged-workflow workflow list",
|
||||
" uncaged-workflow workflow show <name>",
|
||||
" uncaged-workflow workflow rm <name>",
|
||||
" uncaged-workflow workflow history <name>",
|
||||
" uncaged-workflow workflow rollback <name> [hash]",
|
||||
"",
|
||||
" uncaged-workflow thread run <name> [--prompt <text>] [--max-rounds N]",
|
||||
" uncaged-workflow thread list [name]",
|
||||
" uncaged-workflow thread show <id>",
|
||||
" uncaged-workflow thread rm <id>",
|
||||
" uncaged-workflow thread ps",
|
||||
" uncaged-workflow thread kill <thread-id>",
|
||||
" uncaged-workflow thread live <thread-id> [--debug] [--role <name>]",
|
||||
" uncaged-workflow thread live --latest [--debug] [--role <name>]",
|
||||
" uncaged-workflow thread pause <thread-id>",
|
||||
" uncaged-workflow thread resume <thread-id>",
|
||||
" uncaged-workflow thread fork <thread-id> [--from-role <role>]",
|
||||
"",
|
||||
" uncaged-workflow cas get <thread-id> <hash>",
|
||||
" uncaged-workflow cas put <thread-id> <content>",
|
||||
" uncaged-workflow cas list <thread-id>",
|
||||
" uncaged-workflow cas rm <thread-id> <hash>",
|
||||
" uncaged-workflow cas gc",
|
||||
"",
|
||||
" uncaged-workflow init workspace <name>",
|
||||
" uncaged-workflow init template <name>",
|
||||
"",
|
||||
" uncaged-workflow run <name> [...] (shortcut for thread run)",
|
||||
" uncaged-workflow live <thread-id> [...] (shortcut for thread live)",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function printDeprecation(oldCmd: string, newCmd: string): void {
|
||||
printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`);
|
||||
}
|
||||
import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "./skill.js";
|
||||
|
||||
type DispatchFn = (storageRoot: string, argv: string[]) => Promise<number>;
|
||||
|
||||
type CommandEntry = {
|
||||
handler: DispatchFn;
|
||||
args: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type CommandGroup = {
|
||||
name: string;
|
||||
commands: ReadonlyArray<{ name: string; args: string; description: string }>;
|
||||
};
|
||||
|
||||
// ── Individual dispatch functions ──────────────────────────────────────
|
||||
|
||||
async function dispatchInit(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
const sub = argv[0];
|
||||
const name = argv[1];
|
||||
if (sub === undefined || name === undefined || argv.length > 2) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: init requires workspace|template <name>`);
|
||||
async function dispatchInitWorkspace(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
const name = argv[0];
|
||||
if (name === undefined || argv.length > 1) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: init workspace requires <name>`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (sub === "workspace") {
|
||||
const result = await cmdInitWorkspace(process.cwd(), name);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(`initialized workflow workspace at ${result.value.rootPath}`);
|
||||
return 0;
|
||||
const result = await cmdInitWorkspace(process.cwd(), name);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(`initialized workflow workspace at ${result.value.rootPath}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sub === "template") {
|
||||
const result = await cmdInitTemplate(process.cwd(), name);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(`initialized template at ${result.value.templatePath}`);
|
||||
return 0;
|
||||
async function dispatchInitTemplate(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
const name = argv[0];
|
||||
if (name === undefined || argv.length > 1) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: init template requires <name>`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown init subcommand: ${sub}`);
|
||||
return 1;
|
||||
const result = await cmdInitTemplate(process.cwd(), name);
|
||||
if (!result.ok) {
|
||||
printCliError(result.error);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(`initialized template at ${result.value.templatePath}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
||||
@@ -422,49 +394,260 @@ async function dispatchCasRm(storageRoot: string, rest: string[]): Promise<numbe
|
||||
return 0;
|
||||
}
|
||||
|
||||
const CAS_SUBCOMMAND_TABLE: Record<string, DispatchFn> = {
|
||||
get: dispatchCasGet,
|
||||
put: dispatchCasPut,
|
||||
list: dispatchCasList,
|
||||
rm: dispatchCasRm,
|
||||
gc: dispatchGc,
|
||||
// ── Subcommand tables with metadata ────────────────────────────────────
|
||||
|
||||
const WORKFLOW_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
add: {
|
||||
handler: dispatchAdd,
|
||||
args: "<name> <file.esm.js> [--types <path>]",
|
||||
description: "Register a workflow bundle in the registry",
|
||||
},
|
||||
list: { handler: dispatchList, args: "", description: "List all registered workflows" },
|
||||
show: {
|
||||
handler: dispatchShow,
|
||||
args: "<name>",
|
||||
description: "Show details of a registered workflow",
|
||||
},
|
||||
rm: {
|
||||
handler: dispatchRemove,
|
||||
args: "<name>",
|
||||
description: "Remove a workflow from the registry",
|
||||
},
|
||||
history: {
|
||||
handler: dispatchHistory,
|
||||
args: "<name>",
|
||||
description: "Show version history of a workflow",
|
||||
},
|
||||
rollback: {
|
||||
handler: dispatchRollback,
|
||||
args: "<name> [hash]",
|
||||
description: "Rollback a workflow to a previous version",
|
||||
},
|
||||
};
|
||||
|
||||
async function dispatchCas(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const sub = argv[0];
|
||||
if (sub === undefined) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: (none)`);
|
||||
return 1;
|
||||
}
|
||||
const handler = CAS_SUBCOMMAND_TABLE[sub];
|
||||
if (handler === undefined) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: ${sub}`);
|
||||
return 1;
|
||||
}
|
||||
return handler(storageRoot, argv.slice(1));
|
||||
const THREAD_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
run: {
|
||||
handler: dispatchRun,
|
||||
args: "<name> [--prompt <text>] [--max-rounds N]",
|
||||
description: "Start a new thread executing a workflow",
|
||||
},
|
||||
list: {
|
||||
handler: dispatchThreadList,
|
||||
args: "[name]",
|
||||
description: "List threads, optionally filtered by workflow name",
|
||||
},
|
||||
show: { handler: dispatchThreadShow, args: "<id>", description: "Show thread details and state" },
|
||||
rm: { handler: dispatchThreadRm, args: "<id>", description: "Remove a thread" },
|
||||
fork: {
|
||||
handler: dispatchFork,
|
||||
args: "<thread-id> [--from-role <role>]",
|
||||
description: "Fork a thread, optionally from a specific role",
|
||||
},
|
||||
ps: { handler: dispatchPs, args: "", description: "List running threads" },
|
||||
kill: { handler: dispatchKill, args: "<thread-id>", description: "Kill a running thread" },
|
||||
live: {
|
||||
handler: dispatchLive,
|
||||
args: "<thread-id> | --latest [--debug] [--role <name>]",
|
||||
description: "Attach to a thread and stream output live",
|
||||
},
|
||||
pause: { handler: dispatchPause, args: "<thread-id>", description: "Pause a running thread" },
|
||||
resume: { handler: dispatchResume, args: "<thread-id>", description: "Resume a paused thread" },
|
||||
};
|
||||
|
||||
const CAS_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
get: {
|
||||
handler: dispatchCasGet,
|
||||
args: "<thread-id> <hash>",
|
||||
description: "Retrieve content by hash from a thread's CAS",
|
||||
},
|
||||
put: {
|
||||
handler: dispatchCasPut,
|
||||
args: "<thread-id> <content>",
|
||||
description: "Store content in a thread's CAS, returns hash",
|
||||
},
|
||||
list: {
|
||||
handler: dispatchCasList,
|
||||
args: "<thread-id>",
|
||||
description: "List all CAS entries for a thread",
|
||||
},
|
||||
rm: { handler: dispatchCasRm, args: "<thread-id> <hash>", description: "Remove a CAS entry" },
|
||||
gc: { handler: dispatchGc, args: "", description: "Garbage-collect unreferenced CAS entries" },
|
||||
};
|
||||
|
||||
const INIT_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||
workspace: {
|
||||
handler: dispatchInitWorkspace,
|
||||
args: "<name>",
|
||||
description: "Initialize a new workflow workspace",
|
||||
},
|
||||
template: {
|
||||
handler: dispatchInitTemplate,
|
||||
args: "<name>",
|
||||
description: "Initialize a new workflow template",
|
||||
},
|
||||
};
|
||||
|
||||
// ── Command registry ───────────────────────────────────────────────────
|
||||
|
||||
export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
|
||||
return [
|
||||
{
|
||||
name: "workflow",
|
||||
commands: Object.entries(WORKFLOW_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||
name,
|
||||
args: e.args,
|
||||
description: e.description,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "thread",
|
||||
commands: Object.entries(THREAD_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||
name,
|
||||
args: e.args,
|
||||
description: e.description,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "cas",
|
||||
commands: Object.entries(CAS_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||
name,
|
||||
args: e.args,
|
||||
description: e.description,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "init",
|
||||
commands: Object.entries(INIT_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||
name,
|
||||
args: e.args,
|
||||
description: e.description,
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ── Workflow subcommand table (Phase 1) ────────────────────────────────
|
||||
// ── Auto-generated CLI usage ───────────────────────────────────────────
|
||||
|
||||
const WORKFLOW_SUBCOMMAND_TABLE: Record<string, DispatchFn> = {
|
||||
add: dispatchAdd,
|
||||
list: dispatchList,
|
||||
show: dispatchShow,
|
||||
rm: dispatchRemove,
|
||||
history: dispatchHistory,
|
||||
rollback: dispatchRollback,
|
||||
const USAGE_SECTION_BY_GROUP: Record<string, string> = {
|
||||
workflow: "Workflow registry:",
|
||||
thread: "Thread execution:",
|
||||
cas: "Content-addressable storage:",
|
||||
init: "Development:",
|
||||
};
|
||||
|
||||
async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise<number> {
|
||||
function formatUsageCommandLines(
|
||||
rows: ReadonlyArray<{ prefix: string; description: string }>,
|
||||
): string[] {
|
||||
const maxPrefix = rows.reduce((max, row) => Math.max(max, row.prefix.length), 0);
|
||||
const gap = 2;
|
||||
return rows.map((row) => {
|
||||
const pad = " ".repeat(maxPrefix - row.prefix.length + gap);
|
||||
return ` ${row.prefix}${pad}${row.description}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCliUsage(): string {
|
||||
const groups = getCommandRegistry();
|
||||
const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""];
|
||||
|
||||
for (const group of groups) {
|
||||
const sectionTitle = USAGE_SECTION_BY_GROUP[group.name];
|
||||
if (sectionTitle === undefined) {
|
||||
throw new Error(`BUG: missing usage section title for group "${group.name}"`);
|
||||
}
|
||||
lines.push(sectionTitle);
|
||||
const rows = group.commands.map((cmd) => {
|
||||
const args = cmd.args ? ` ${cmd.args}` : "";
|
||||
return {
|
||||
prefix: `${group.name} ${cmd.name}${args}`,
|
||||
description: cmd.description,
|
||||
};
|
||||
});
|
||||
lines.push(...formatUsageCommandLines(rows));
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("Shortcuts:");
|
||||
lines.push(
|
||||
...formatUsageCommandLines([
|
||||
{ prefix: "run <name> [...]", description: "→ thread run" },
|
||||
{ prefix: "live <id> [...]", description: "→ thread live" },
|
||||
]),
|
||||
);
|
||||
lines.push("");
|
||||
|
||||
lines.push("Reference:");
|
||||
const skillTopicNames = getSkillTopics()
|
||||
.map((t) => t.name)
|
||||
.join(", ");
|
||||
lines.push(
|
||||
...formatUsageCommandLines([
|
||||
{
|
||||
prefix: "skill [topic]",
|
||||
description: `Agent-consumable docs (${skillTopicNames})`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("Use <command> --help for subcommand details.");
|
||||
lines.push("");
|
||||
lines.push("Environment variables:");
|
||||
lines.push(
|
||||
" WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)",
|
||||
);
|
||||
lines.push(
|
||||
" UNCAGED_WORKFLOW_STORAGE_ROOT Internal override (takes priority over WORKFLOW_STORAGE_ROOT)",
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function printDeprecation(oldCmd: string, newCmd: string): void {
|
||||
printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`);
|
||||
}
|
||||
|
||||
// ── Group dispatchers ──────────────────────────────────────────────────
|
||||
|
||||
function dispatchGroup(
|
||||
tableName: string,
|
||||
table: Record<string, CommandEntry>,
|
||||
storageRoot: string,
|
||||
argv: string[],
|
||||
): Promise<number> | null {
|
||||
const sub = argv[0];
|
||||
if (sub === undefined) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown workflow subcommand: (none)`);
|
||||
return 1;
|
||||
if (sub === undefined || sub === "--help" || sub === "-h") {
|
||||
const entries = Object.entries(table);
|
||||
const lines = [`${tableName} subcommands:\n`];
|
||||
for (const [name, e] of entries) {
|
||||
const args = e.args ? ` ${e.args}` : "";
|
||||
lines.push(` uncaged-workflow ${tableName} ${name}${args}`);
|
||||
lines.push(` ${e.description}\n`);
|
||||
}
|
||||
printCliLine(lines.join("\n"));
|
||||
return Promise.resolve(sub === undefined ? 1 : 0);
|
||||
}
|
||||
const handler = WORKFLOW_SUBCOMMAND_TABLE[sub];
|
||||
if (handler !== undefined) {
|
||||
return handler(storageRoot, argv.slice(1));
|
||||
const entry = table[sub];
|
||||
if (entry === undefined) {
|
||||
return null;
|
||||
}
|
||||
return entry.handler(storageRoot, argv.slice(1));
|
||||
}
|
||||
|
||||
async function dispatchInit(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("init", INIT_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
const sub = argv[0];
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown init subcommand: ${sub}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("workflow", WORKFLOW_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
const sub = argv[0];
|
||||
if (sub === "remove") {
|
||||
printDeprecation("workflow remove", "workflow rm");
|
||||
return dispatchRemove(storageRoot, argv.slice(1));
|
||||
@@ -473,43 +656,61 @@ async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise<nu
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ── Thread subcommand table (Phase 2) ──────────────────────────────────
|
||||
|
||||
const THREAD_SUBCOMMAND_TABLE: Record<string, DispatchFn> = {
|
||||
run: dispatchRun,
|
||||
list: dispatchThreadList,
|
||||
show: dispatchThreadShow,
|
||||
rm: dispatchThreadRm,
|
||||
fork: dispatchFork,
|
||||
ps: dispatchPs,
|
||||
kill: dispatchKill,
|
||||
live: dispatchLive,
|
||||
pause: dispatchPause,
|
||||
resume: dispatchResume,
|
||||
};
|
||||
|
||||
async function dispatchThread(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("thread", THREAD_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
const sub = argv[0];
|
||||
if (sub === undefined) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: (none)`);
|
||||
return 1;
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: ${sub}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
async function dispatchCas(storageRoot: string, argv: string[]): Promise<number> {
|
||||
const result = dispatchGroup("cas", CAS_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
const handler = THREAD_SUBCOMMAND_TABLE[sub];
|
||||
if (handler === undefined) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: ${sub}`);
|
||||
return 1;
|
||||
}
|
||||
return handler(storageRoot, argv.slice(1));
|
||||
const sub = argv[0];
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: ${sub}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ── Help ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
if (argv.includes("--skill")) {
|
||||
printCliLine(formatSkillDoc());
|
||||
} else {
|
||||
printCliLine(formatCliUsage());
|
||||
async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
const topic = argv[0];
|
||||
if (topic === undefined) {
|
||||
printCliLine(formatSkillIndex());
|
||||
return 0;
|
||||
}
|
||||
const doc = formatSkillTopic(topic);
|
||||
if (doc === null) {
|
||||
printCliError(`unknown skill topic: ${topic}\n\n${formatSkillIndex()}`);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(doc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
// Legacy compat: help --skill [topic] → skill [topic]
|
||||
const skillIdx = argv.indexOf("--skill");
|
||||
if (skillIdx !== -1) {
|
||||
const topic = argv[skillIdx + 1];
|
||||
if (topic === undefined) {
|
||||
printCliLine(formatSkillIndex());
|
||||
return 0;
|
||||
}
|
||||
const doc = formatSkillTopic(topic);
|
||||
if (doc === null) {
|
||||
printCliError(`unknown skill topic: ${topic}\n\n${formatSkillIndex()}`);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(doc);
|
||||
return 0;
|
||||
}
|
||||
printCliLine(formatCliUsage());
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -522,6 +723,7 @@ const COMMAND_TABLE: Record<string, DispatchFn> = {
|
||||
cas: dispatchCas,
|
||||
init: dispatchInit,
|
||||
help: dispatchHelp,
|
||||
skill: dispatchSkill,
|
||||
|
||||
// Top-level shortcuts (no deprecation)
|
||||
run: dispatchRun,
|
||||
@@ -547,12 +749,12 @@ const DEPRECATED_ALIASES: Record<string, { newCmd: string; handler: DispatchFn }
|
||||
|
||||
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
||||
if (argv.length === 0) {
|
||||
printCliError(formatCliUsage());
|
||||
printCliLine(formatCliUsage());
|
||||
return 1;
|
||||
}
|
||||
const command = argv[0];
|
||||
if (command === undefined) {
|
||||
printCliError(formatCliUsage());
|
||||
printCliLine(formatCliUsage());
|
||||
return 1;
|
||||
}
|
||||
const rest = argv.slice(1);
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { createCasStore, err, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export async function cmdCasGet(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
hash: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
const content = await cas.get(hash);
|
||||
if (content === null) {
|
||||
return err(`cas entry not found: ${hash}`);
|
||||
}
|
||||
return ok(content);
|
||||
}
|
||||
|
||||
export async function cmdCasPut(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
content: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
const hash = await cas.put(content);
|
||||
return ok(hash);
|
||||
}
|
||||
|
||||
export async function cmdCasList(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
): Promise<Result<string[], string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
const hashes = await cas.list();
|
||||
return ok(hashes);
|
||||
}
|
||||
|
||||
export async function cmdCasRm(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
hash: string,
|
||||
): Promise<Result<void, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
await cas.delete(hash);
|
||||
return ok(undefined);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
export function formatSkillDoc(): string {
|
||||
return `# uncaged-workflow CLI Reference
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Workflow** | A single-file ESM bundle (\`.esm.js\`) that exports \`run\` and \`descriptor\`. Identified by name and XXH64 hash. |
|
||||
| **Bundle** | The physical \`.esm.js\` file stored in the bundles directory. Immutable once written. |
|
||||
| **Thread** | A single execution of a workflow, identified by a ULID. Persists state as JSONL files. |
|
||||
| **CAS** | Content-Addressable Storage. Per-thread key-value store keyed by content hash. |
|
||||
| **Registry** | \`workflow.yaml\` — maps workflow names to their current and historical bundle hashes. |
|
||||
|
||||
## Commands
|
||||
|
||||
### workflow
|
||||
|
||||
| Command | Args | Description |
|
||||
|---------|------|-------------|
|
||||
| \`workflow add\` | \`<name> <file.esm.js> [--types <path>]\` | Register a workflow bundle in the registry |
|
||||
| \`workflow list\` | (none) | List all registered workflows |
|
||||
| \`workflow show\` | \`<name>\` | Show details of a registered workflow |
|
||||
| \`workflow rm\` | \`<name>\` | Remove a workflow from the registry |
|
||||
| \`workflow history\` | \`<name>\` | Show version history of a workflow |
|
||||
| \`workflow rollback\` | \`<name> [hash]\` | Rollback a workflow to a previous version |
|
||||
|
||||
### thread
|
||||
|
||||
| Command | Args | Description |
|
||||
|---------|------|-------------|
|
||||
| \`thread run\` | \`<name> [--prompt <text>] [--max-rounds N]\` | Start a new thread executing a workflow |
|
||||
| \`thread list\` | \`[name]\` | List threads, optionally filtered by workflow name |
|
||||
| \`thread show\` | \`<id>\` | Show thread details and state |
|
||||
| \`thread rm\` | \`<id>\` | Remove a thread |
|
||||
| \`thread fork\` | \`<thread-id> [--from-role <role>]\` | Fork a thread, optionally from a specific role |
|
||||
| \`thread ps\` | (none) | List running threads |
|
||||
| \`thread kill\` | \`<thread-id>\` | Kill a running thread |
|
||||
| \`thread live\` | \`<thread-id> [--debug] [--role <name>]\` or \`--latest [--debug] [--role <name>]\` | Attach to a thread and stream output live |
|
||||
| \`thread pause\` | \`<thread-id>\` | Pause a running thread |
|
||||
| \`thread resume\` | \`<thread-id>\` | Resume a paused thread |
|
||||
|
||||
### cas
|
||||
|
||||
| Command | Args | Description |
|
||||
|---------|------|-------------|
|
||||
| \`cas get\` | \`<thread-id> <hash>\` | Retrieve content by hash from a thread's CAS |
|
||||
| \`cas put\` | \`<thread-id> <content>\` | Store content in a thread's CAS, returns hash |
|
||||
| \`cas list\` | \`<thread-id>\` | List all CAS entries for a thread |
|
||||
| \`cas rm\` | \`<thread-id> <hash>\` | Remove a CAS entry |
|
||||
| \`cas gc\` | (none) | Garbage-collect unreferenced CAS entries |
|
||||
|
||||
### init
|
||||
|
||||
| Command | Args | Description |
|
||||
|---------|------|-------------|
|
||||
| \`init workspace\` | \`<name>\` | Initialize a new workflow workspace |
|
||||
| \`init template\` | \`<name>\` | Initialize a new workflow template |
|
||||
|
||||
### Top-level shortcuts
|
||||
|
||||
| Command | Equivalent | Description |
|
||||
|---------|------------|-------------|
|
||||
| \`run\` | \`thread run\` | Shortcut to start a thread |
|
||||
| \`live\` | \`thread live\` | Shortcut to attach to a thread |
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
1. \`uncaged-workflow workflow add my-wf ./my-wf.esm.js\` — register a workflow
|
||||
2. \`uncaged-workflow run my-wf --prompt "do the thing"\` — start a thread
|
||||
3. \`uncaged-workflow live --latest\` — attach and watch output
|
||||
4. \`uncaged-workflow thread show <thread-id>\` — inspect completed thread
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Error |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| \`UNCAGED_WORKFLOW_STORAGE_ROOT\` | Override the default storage directory for all workflow data |
|
||||
`;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { err, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { readTextFileIfExists } from "./fs-utils.js";
|
||||
import {
|
||||
resolveRunningHashForThread,
|
||||
sendWorkerTcpCommand,
|
||||
type WorkerCtl,
|
||||
} from "./worker-spawn.js";
|
||||
|
||||
export async function cmdKill(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<void, string>> {
|
||||
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
||||
if (!hashResult.ok) {
|
||||
return hashResult;
|
||||
}
|
||||
|
||||
const ctlPath = join(storageRoot, "workers", `${hashResult.value}.json`);
|
||||
const ctlText = await readTextFileIfExists(ctlPath);
|
||||
if (ctlText === null) {
|
||||
return err(`worker control file missing for bundle hash ${hashResult.value}`);
|
||||
}
|
||||
|
||||
let ctl: WorkerCtl;
|
||||
try {
|
||||
ctl = JSON.parse(ctlText) as WorkerCtl;
|
||||
} catch {
|
||||
return err(`corrupt worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
||||
return err(`invalid worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
return await sendWorkerTcpCommand(
|
||||
ctl.port,
|
||||
{ type: "kill", threadId },
|
||||
{ awaitResponseLine: true },
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { err, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { readTextFileIfExists } from "./fs-utils.js";
|
||||
import {
|
||||
resolveRunningHashForThread,
|
||||
sendWorkerTcpCommand,
|
||||
type WorkerCtl,
|
||||
} from "./worker-spawn.js";
|
||||
|
||||
export async function cmdPause(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<void, string>> {
|
||||
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
||||
if (!hashResult.ok) {
|
||||
return hashResult;
|
||||
}
|
||||
|
||||
const ctlPath = join(storageRoot, "workers", `${hashResult.value}.json`);
|
||||
const ctlText = await readTextFileIfExists(ctlPath);
|
||||
if (ctlText === null) {
|
||||
return err(`worker control file missing for bundle hash ${hashResult.value}`);
|
||||
}
|
||||
|
||||
let ctl: WorkerCtl;
|
||||
try {
|
||||
ctl = JSON.parse(ctlText) as WorkerCtl;
|
||||
} catch {
|
||||
return err(`corrupt worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
||||
return err(`invalid worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
return await sendWorkerTcpCommand(
|
||||
ctl.port,
|
||||
{ type: "pause", threadId },
|
||||
{ awaitResponseLine: true },
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { err, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { readTextFileIfExists } from "./fs-utils.js";
|
||||
import {
|
||||
resolveRunningHashForThread,
|
||||
sendWorkerTcpCommand,
|
||||
type WorkerCtl,
|
||||
} from "./worker-spawn.js";
|
||||
|
||||
export async function cmdResume(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<void, string>> {
|
||||
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
||||
if (!hashResult.ok) {
|
||||
return hashResult;
|
||||
}
|
||||
|
||||
const ctlPath = join(storageRoot, "workers", `${hashResult.value}.json`);
|
||||
const ctlText = await readTextFileIfExists(ctlPath);
|
||||
if (ctlText === null) {
|
||||
return err(`worker control file missing for bundle hash ${hashResult.value}`);
|
||||
}
|
||||
|
||||
let ctl: WorkerCtl;
|
||||
try {
|
||||
ctl = JSON.parse(ctlText) as WorkerCtl;
|
||||
} catch {
|
||||
return err(`corrupt worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
||||
return err(`invalid worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
return await sendWorkerTcpCommand(
|
||||
ctl.port,
|
||||
{ type: "resume", threadId },
|
||||
{ awaitResponseLine: true },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createCasStore, err, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export async function cmdCasGet(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
hash: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
const content = await cas.get(hash);
|
||||
if (content === null) {
|
||||
return err(`cas entry not found: ${hash}`);
|
||||
}
|
||||
return ok(content);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { cmdGc } from "./gc.js";
|
||||
export { cmdCasGet } from "./get.js";
|
||||
export { cmdCasList } from "./list.js";
|
||||
export { cmdCasPut } from "./put.js";
|
||||
export { cmdCasRm } from "./rm.js";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export async function cmdCasList(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
): Promise<Result<string[], string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
const hashes = await cas.list();
|
||||
return ok(hashes);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export async function cmdCasPut(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
content: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
const hash = await cas.put(content);
|
||||
return ok(hash);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
export async function cmdCasRm(
|
||||
storageRoot: string,
|
||||
_threadId: string,
|
||||
hash: string,
|
||||
): Promise<Result<void, string>> {
|
||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||
await cas.delete(hash);
|
||||
return ok(undefined);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type { CmdInitTemplateSuccess } from "./template.js";
|
||||
export { cmdInitTemplate } from "./template.js";
|
||||
export type { CmdInitWorkspaceSuccess } from "./workspace.js";
|
||||
export { cmdInitWorkspace } from "./workspace.js";
|
||||
@@ -0,0 +1,203 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { pathExists } from "../../fs-utils.js";
|
||||
|
||||
export type CmdInitTemplateSuccess = {
|
||||
templatePath: string;
|
||||
};
|
||||
|
||||
function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||
if (name.length === 0) {
|
||||
return err("workspace name must not be empty");
|
||||
}
|
||||
if (name === "." || name === "..") {
|
||||
return err("invalid workspace name");
|
||||
}
|
||||
if (name.includes("/") || name.includes("\\")) {
|
||||
return err("workspace name must not contain path separators");
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
function hasTemplatesWorkspaceGlob(workspaces: unknown): boolean {
|
||||
return Array.isArray(workspaces) && workspaces.includes("templates/*");
|
||||
}
|
||||
|
||||
async function readPackageJsonWorkspaces(dir: string): Promise<unknown | null> {
|
||||
const pkgPath = join(dir, "package.json");
|
||||
if (!(await pathExists(pkgPath))) {
|
||||
return null;
|
||||
}
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(pkgPath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || !("workspaces" in parsed)) {
|
||||
return null;
|
||||
}
|
||||
return (parsed as { workspaces: unknown }).workspaces;
|
||||
}
|
||||
|
||||
/** Resolve uncaged-workflow workspace root (package.json with `templates/*` in `workspaces`). */
|
||||
async function findWorkflowWorkspaceRoot(startDir: string): Promise<Result<string, string>> {
|
||||
let dir = resolve(startDir);
|
||||
for (;;) {
|
||||
const workspaces = await readPackageJsonWorkspaces(dir);
|
||||
if (workspaces !== null && hasTemplatesWorkspaceGlob(workspaces)) {
|
||||
return ok(dir);
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) {
|
||||
return err(
|
||||
'not inside a workflow workspace (no package.json with workspaces containing "templates/*")',
|
||||
);
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function templatePackageJson(templateName: string): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
name: `template-${templateName}`,
|
||||
version: "0.0.0",
|
||||
private: true,
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"@uncaged/workflow": "^0.1.0",
|
||||
zod: "^4.0.0",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function templateTsconfigJson(): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
extends: "../../tsconfig.json",
|
||||
compilerOptions: {
|
||||
rootDir: "src",
|
||||
outDir: "dist",
|
||||
},
|
||||
include: ["src/**/*.ts"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function templateRolesTs(): string {
|
||||
return `import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const HELLO_TEMPLATE_DESCRIPTION =
|
||||
"Minimal starter template: one greeter role, then END.";
|
||||
|
||||
export type HelloTemplateMeta = {
|
||||
greeter: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
const greeterMetaSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
|
||||
description: "Says hello — replace with your first role.",
|
||||
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
|
||||
extractPrompt: "Extract the assistant's greeting as message.",
|
||||
schema: greeterMetaSchema,
|
||||
extractRefs: null,
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
function templateModeratorTs(): string {
|
||||
return `import { END, type Moderator, type ModeratorContext } from "@uncaged/workflow";
|
||||
|
||||
import type { HelloTemplateMeta } from "./roles.js";
|
||||
|
||||
export const helloTemplateModerator: Moderator<HelloTemplateMeta> = (
|
||||
ctx: ModeratorContext<HelloTemplateMeta>,
|
||||
) => {
|
||||
if (ctx.steps.length === 0) {
|
||||
return "greeter";
|
||||
}
|
||||
return END;
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
function templateIndexTs(): string {
|
||||
return `import type { WorkflowDefinition } from "@uncaged/workflow";
|
||||
|
||||
import { helloTemplateModerator } from "./moderator.js";
|
||||
import {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
|
||||
export {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
export { helloTemplateModerator } from "./moderator.js";
|
||||
|
||||
export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = {
|
||||
description: HELLO_TEMPLATE_DESCRIPTION,
|
||||
roles: {
|
||||
greeter: greeterRole,
|
||||
},
|
||||
moderator: helloTemplateModerator,
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function cmdInitTemplate(
|
||||
startDir: string,
|
||||
templateName: string,
|
||||
): Promise<Result<CmdInitTemplateSuccess, string>> {
|
||||
const validated = validateWorkspaceSegment(templateName);
|
||||
if (!validated.ok) {
|
||||
return validated;
|
||||
}
|
||||
|
||||
const rootResult = await findWorkflowWorkspaceRoot(startDir);
|
||||
if (!rootResult.ok) {
|
||||
return rootResult;
|
||||
}
|
||||
|
||||
const workspaceRoot = rootResult.value;
|
||||
const templateDir = join(workspaceRoot, "templates", templateName);
|
||||
if (await pathExists(templateDir)) {
|
||||
return err(`template already exists: ${templateDir}`);
|
||||
}
|
||||
|
||||
await mkdir(join(templateDir, "src"), { recursive: true });
|
||||
|
||||
await Promise.all([
|
||||
writeFile(join(templateDir, "package.json"), templatePackageJson(templateName), "utf8"),
|
||||
writeFile(join(templateDir, "tsconfig.json"), templateTsconfigJson(), "utf8"),
|
||||
writeFile(join(templateDir, "src", "roles.ts"), templateRolesTs(), "utf8"),
|
||||
writeFile(join(templateDir, "src", "moderator.ts"), templateModeratorTs(), "utf8"),
|
||||
writeFile(join(templateDir, "src", "index.ts"), templateIndexTs(), "utf8"),
|
||||
]);
|
||||
|
||||
return ok({ templatePath: templateDir });
|
||||
}
|
||||
+3
-187
@@ -1,18 +1,14 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { pathExists } from "./fs-utils.js";
|
||||
import { pathExists } from "../../fs-utils.js";
|
||||
|
||||
export type CmdInitWorkspaceSuccess = {
|
||||
rootPath: string;
|
||||
};
|
||||
|
||||
export type CmdInitTemplateSuccess = {
|
||||
templatePath: string;
|
||||
};
|
||||
|
||||
function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||
if (name.length === 0) {
|
||||
return err("workspace name must not be empty");
|
||||
@@ -233,183 +229,3 @@ export async function cmdInitWorkspace(
|
||||
|
||||
return ok({ rootPath });
|
||||
}
|
||||
|
||||
function hasTemplatesWorkspaceGlob(workspaces: unknown): boolean {
|
||||
return Array.isArray(workspaces) && workspaces.includes("templates/*");
|
||||
}
|
||||
|
||||
async function readPackageJsonWorkspaces(dir: string): Promise<unknown | null> {
|
||||
const pkgPath = join(dir, "package.json");
|
||||
if (!(await pathExists(pkgPath))) {
|
||||
return null;
|
||||
}
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(pkgPath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || !("workspaces" in parsed)) {
|
||||
return null;
|
||||
}
|
||||
return (parsed as { workspaces: unknown }).workspaces;
|
||||
}
|
||||
|
||||
/** Resolve uncaged-workflow workspace root (package.json with `templates/*` in `workspaces`). */
|
||||
async function findWorkflowWorkspaceRoot(startDir: string): Promise<Result<string, string>> {
|
||||
let dir = resolve(startDir);
|
||||
for (;;) {
|
||||
const workspaces = await readPackageJsonWorkspaces(dir);
|
||||
if (workspaces !== null && hasTemplatesWorkspaceGlob(workspaces)) {
|
||||
return ok(dir);
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) {
|
||||
return err(
|
||||
'not inside a workflow workspace (no package.json with workspaces containing "templates/*")',
|
||||
);
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function templatePackageJson(templateName: string): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
name: `template-${templateName}`,
|
||||
version: "0.0.0",
|
||||
private: true,
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"@uncaged/workflow": "^0.1.0",
|
||||
zod: "^4.0.0",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function templateTsconfigJson(): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
extends: "../../tsconfig.json",
|
||||
compilerOptions: {
|
||||
rootDir: "src",
|
||||
outDir: "dist",
|
||||
},
|
||||
include: ["src/**/*.ts"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
function templateRolesTs(): string {
|
||||
return `import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const HELLO_TEMPLATE_DESCRIPTION =
|
||||
"Minimal starter template: one greeter role, then END.";
|
||||
|
||||
export type HelloTemplateMeta = {
|
||||
greeter: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
const greeterMetaSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
|
||||
description: "Says hello — replace with your first role.",
|
||||
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
|
||||
extractPrompt: "Extract the assistant's greeting as message.",
|
||||
schema: greeterMetaSchema,
|
||||
extractRefs: null,
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
function templateModeratorTs(): string {
|
||||
return `import { END, type Moderator, type ModeratorContext } from "@uncaged/workflow";
|
||||
|
||||
import type { HelloTemplateMeta } from "./roles.js";
|
||||
|
||||
export const helloTemplateModerator: Moderator<HelloTemplateMeta> = (
|
||||
ctx: ModeratorContext<HelloTemplateMeta>,
|
||||
) => {
|
||||
if (ctx.steps.length === 0) {
|
||||
return "greeter";
|
||||
}
|
||||
return END;
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
function templateIndexTs(): string {
|
||||
return `import type { WorkflowDefinition } from "@uncaged/workflow";
|
||||
|
||||
import { helloTemplateModerator } from "./moderator.js";
|
||||
import {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
|
||||
export {
|
||||
HELLO_TEMPLATE_DESCRIPTION,
|
||||
type HelloTemplateMeta,
|
||||
greeterRole,
|
||||
} from "./roles.js";
|
||||
export { helloTemplateModerator } from "./moderator.js";
|
||||
|
||||
export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = {
|
||||
description: HELLO_TEMPLATE_DESCRIPTION,
|
||||
roles: {
|
||||
greeter: greeterRole,
|
||||
},
|
||||
moderator: helloTemplateModerator,
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function cmdInitTemplate(
|
||||
startDir: string,
|
||||
templateName: string,
|
||||
): Promise<Result<CmdInitTemplateSuccess, string>> {
|
||||
const validated = validateWorkspaceSegment(templateName);
|
||||
if (!validated.ok) {
|
||||
return validated;
|
||||
}
|
||||
|
||||
const rootResult = await findWorkflowWorkspaceRoot(startDir);
|
||||
if (!rootResult.ok) {
|
||||
return rootResult;
|
||||
}
|
||||
|
||||
const workspaceRoot = rootResult.value;
|
||||
const templateDir = join(workspaceRoot, "templates", templateName);
|
||||
if (await pathExists(templateDir)) {
|
||||
return err(`template already exists: ${templateDir}`);
|
||||
}
|
||||
|
||||
await mkdir(join(templateDir, "src"), { recursive: true });
|
||||
|
||||
await Promise.all([
|
||||
writeFile(join(templateDir, "package.json"), templatePackageJson(templateName), "utf8"),
|
||||
writeFile(join(templateDir, "tsconfig.json"), templateTsconfigJson(), "utf8"),
|
||||
writeFile(join(templateDir, "src", "roles.ts"), templateRolesTs(), "utf8"),
|
||||
writeFile(join(templateDir, "src", "moderator.ts"), templateModeratorTs(), "utf8"),
|
||||
writeFile(join(templateDir, "src", "index.ts"), templateIndexTs(), "utf8"),
|
||||
]);
|
||||
|
||||
return ok({ templatePath: templateDir });
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Result } from "@uncaged/workflow";
|
||||
|
||||
import {
|
||||
readWorkerCtl,
|
||||
resolveRunningHashForThread,
|
||||
sendWorkerTcpCommand,
|
||||
} from "../../worker-spawn.js";
|
||||
|
||||
type ThreadControlAction = "kill" | "pause" | "resume";
|
||||
|
||||
async function cmdThreadControl(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
action: ThreadControlAction,
|
||||
): Promise<Result<void, string>> {
|
||||
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
||||
if (!hashResult.ok) {
|
||||
return hashResult;
|
||||
}
|
||||
|
||||
const ctlResult = await readWorkerCtl(storageRoot, hashResult.value);
|
||||
if (!ctlResult.ok) {
|
||||
return ctlResult;
|
||||
}
|
||||
|
||||
return await sendWorkerTcpCommand(
|
||||
ctlResult.value.port,
|
||||
{ type: action, threadId },
|
||||
{ awaitResponseLine: true },
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdKill(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<void, string>> {
|
||||
return cmdThreadControl(storageRoot, threadId, "kill");
|
||||
}
|
||||
|
||||
export async function cmdPause(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<void, string>> {
|
||||
return cmdThreadControl(storageRoot, threadId, "pause");
|
||||
}
|
||||
|
||||
export async function cmdResume(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<void, string>> {
|
||||
return cmdThreadControl(storageRoot, threadId, "resume");
|
||||
}
|
||||
+3
-3
@@ -2,9 +2,9 @@ import { join } from "node:path";
|
||||
|
||||
import { buildForkPlan, err, generateUlid, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { pathExists, readTextFileIfExists } from "./fs-utils.js";
|
||||
import { resolveThreadDataPath } from "./thread-scan.js";
|
||||
import { ensureWorkerForHash, sendWorkerTcpCommand } from "./worker-spawn.js";
|
||||
import { pathExists, readTextFileIfExists } from "../../fs-utils.js";
|
||||
import { resolveThreadDataPath } from "../../thread-scan.js";
|
||||
import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js";
|
||||
|
||||
export function parseForkArgv(
|
||||
argv: string[],
|
||||
@@ -0,0 +1,15 @@
|
||||
export { cmdKill, cmdPause, cmdResume } from "./control.js";
|
||||
export { cmdFork, parseForkArgv } from "./fork.js";
|
||||
export { cmdThreads } from "./list.js";
|
||||
export type { LiveRoleRow } from "./live.js";
|
||||
export {
|
||||
cmdLive,
|
||||
formatLiveDebugLine,
|
||||
formatLiveTimeLabel,
|
||||
LIVE_CONTENT_MAX_LINES,
|
||||
renderLiveRoleStepLines,
|
||||
} from "./live.js";
|
||||
export { cmdPs } from "./ps.js";
|
||||
export { cmdThreadRemove } from "./rm.js";
|
||||
export { cmdRun } from "./run.js";
|
||||
export { cmdThreadShow } from "./show.js";
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { listHistoricalThreads } from "./thread-scan.js";
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
import { listHistoricalThreads } from "../../thread-scan.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export async function cmdThreads(
|
||||
storageRoot: string,
|
||||
+4
-4
@@ -12,10 +12,10 @@ import {
|
||||
type WorkflowCompletion,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import { printCliError, printCliLine } from "./cli-output.js";
|
||||
import { pathExists } from "./fs-utils.js";
|
||||
import type { ParsedLiveArgv } from "./live-argv.js";
|
||||
import { findLatestThreadDataPath, resolveThreadDataPath } from "./thread-scan.js";
|
||||
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||
import { pathExists } from "../../fs-utils.js";
|
||||
import type { ParsedLiveArgv } from "../../live-argv.js";
|
||||
import { findLatestThreadDataPath, resolveThreadDataPath } from "../../thread-scan.js";
|
||||
|
||||
export const LIVE_CONTENT_MAX_LINES = 10;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { listRunningThreads } from "./thread-scan.js";
|
||||
import { listRunningThreads } from "../../thread-scan.js";
|
||||
|
||||
export async function cmdPs(storageRoot: string): Promise<string[]> {
|
||||
const rows = await listRunningThreads(storageRoot);
|
||||
+1
-17
@@ -3,23 +3,7 @@ import { dirname, join } from "node:path";
|
||||
|
||||
import { err, garbageCollectCas, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { readTextFileIfExists } from "./fs-utils.js";
|
||||
import { resolveThreadDataPath } from "./thread-scan.js";
|
||||
|
||||
export async function cmdThreadShow(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const dataPath = await resolveThreadDataPath(storageRoot, threadId);
|
||||
if (dataPath === null) {
|
||||
return err(`thread not found: ${threadId}`);
|
||||
}
|
||||
const text = await readTextFileIfExists(dataPath);
|
||||
if (text === null) {
|
||||
return err(`thread data missing: ${threadId}`);
|
||||
}
|
||||
return ok(text.endsWith("\n") ? text.slice(0, -1) : text);
|
||||
}
|
||||
import { resolveThreadDataPath } from "../../thread-scan.js";
|
||||
|
||||
export async function cmdThreadRemove(
|
||||
storageRoot: string,
|
||||
+2
-2
@@ -8,8 +8,8 @@ import {
|
||||
type Result,
|
||||
readWorkflowRegistry,
|
||||
} from "@uncaged/workflow";
|
||||
import { ensureWorkerForHash, sendWorkerTcpCommand } from "./worker-spawn.js";
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export async function cmdRun(
|
||||
storageRoot: string,
|
||||
@@ -0,0 +1,19 @@
|
||||
import { err, ok, type Result } from "@uncaged/workflow";
|
||||
|
||||
import { readTextFileIfExists } from "../../fs-utils.js";
|
||||
import { resolveThreadDataPath } from "../../thread-scan.js";
|
||||
|
||||
export async function cmdThreadShow(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
): Promise<Result<string, string>> {
|
||||
const dataPath = await resolveThreadDataPath(storageRoot, threadId);
|
||||
if (dataPath === null) {
|
||||
return err(`thread not found: ${threadId}`);
|
||||
}
|
||||
const text = await readTextFileIfExists(dataPath);
|
||||
if (text === null) {
|
||||
return err(`thread data missing: ${threadId}`);
|
||||
}
|
||||
return ok(text.endsWith("\n") ? text.slice(0, -1) : text);
|
||||
}
|
||||
+2
-2
@@ -14,8 +14,8 @@ import {
|
||||
writeWorkflowRegistry,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import { storeWorkflowBundleArtifacts } from "./bundle-store.js";
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
import { storeWorkflowBundleArtifacts } from "../../bundle-store.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export type ParsedAddArgv = {
|
||||
name: string;
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
readWorkflowRegistry,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export async function cmdHistory(
|
||||
storageRoot: string,
|
||||
@@ -0,0 +1,7 @@
|
||||
export type { CmdAddSuccess, ParsedAddArgv } from "./add.js";
|
||||
export { cmdAdd, formatAddSuccess, parseAddArgv } from "./add.js";
|
||||
export { cmdHistory } from "./history.js";
|
||||
export { cmdList, formatListLines } from "./list.js";
|
||||
export { cmdRemove } from "./rm.js";
|
||||
export { cmdRollback } from "./rollback.js";
|
||||
export { cmdShow, formatShowYaml } from "./show.js";
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
writeWorkflowRegistry,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export async function cmdRemove(storageRoot: string, name: string): Promise<Result<void, string>> {
|
||||
const nameOk = validateCliWorkflowName(name);
|
||||
+2
-2
@@ -10,8 +10,8 @@ import {
|
||||
writeWorkflowRegistry,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import { pathExists } from "./fs-utils.js";
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
import { pathExists } from "../../fs-utils.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export async function cmdRollback(
|
||||
storageRoot: string,
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
} from "@uncaged/workflow";
|
||||
import { stringify } from "yaml";
|
||||
|
||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
||||
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||
|
||||
export async function cmdShow(
|
||||
storageRoot: string,
|
||||
@@ -0,0 +1,238 @@
|
||||
import { getCommandRegistry } from "./cli-dispatch.js";
|
||||
|
||||
type SkillTopic = {
|
||||
name: string;
|
||||
description: string;
|
||||
format: () => string;
|
||||
};
|
||||
|
||||
const SKILL_TOPICS: ReadonlyArray<SkillTopic> = [
|
||||
{ name: "cli", description: "Full CLI command reference", format: formatSkillCli },
|
||||
{
|
||||
name: "develop",
|
||||
description: "Guide for agents executing roles inside a workflow",
|
||||
format: formatSkillDevelop,
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
description: "Guide for building and publishing workflow bundles",
|
||||
format: formatSkillAuthor,
|
||||
},
|
||||
];
|
||||
|
||||
export function getSkillTopics(): ReadonlyArray<{ name: string; description: string }> {
|
||||
return SKILL_TOPICS.map((t) => ({ name: t.name, description: t.description }));
|
||||
}
|
||||
|
||||
export function formatSkillTopic(topic: string): string | null {
|
||||
const entry = SKILL_TOPICS.find((t) => t.name === topic);
|
||||
if (entry === undefined) {
|
||||
return null;
|
||||
}
|
||||
return entry.format();
|
||||
}
|
||||
|
||||
export function formatSkillIndex(): string {
|
||||
const rows = SKILL_TOPICS.map((t) => `| \`${t.name}\` | ${t.description} |`);
|
||||
return `# uncaged-workflow skill
|
||||
|
||||
Available topics:
|
||||
|
||||
| Topic | Description |
|
||||
|-------|-------------|
|
||||
${rows.join("\n")}
|
||||
|
||||
Usage: \`uncaged-workflow skill <topic>\`
|
||||
`;
|
||||
}
|
||||
|
||||
// ── cli topic (existing full reference) ────────────────────────────────
|
||||
|
||||
function formatSkillCli(): string {
|
||||
const groups = getCommandRegistry();
|
||||
|
||||
const commandSections: string[] = [];
|
||||
for (const group of groups) {
|
||||
const rows = group.commands.map((cmd) => {
|
||||
const args = cmd.args ? `\`${cmd.args}\`` : "(none)";
|
||||
return `| \`${group.name} ${cmd.name}\` | ${args} | ${cmd.description} |`;
|
||||
});
|
||||
commandSections.push(
|
||||
`### ${group.name}\n\n| Command | Args | Description |\n|---------|------|-------------|\n${rows.join("\n")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return `# uncaged-workflow CLI Reference
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Workflow** | A single-file ESM bundle (\`.esm.js\`) that exports \`run\` and \`descriptor\`. Identified by name and XXH64 hash. |
|
||||
| **Bundle** | The physical \`.esm.js\` file stored in the bundles directory. Immutable once written. |
|
||||
| **Thread** | A single execution of a workflow, identified by a ULID. Persists state as JSONL files. |
|
||||
| **CAS** | Content-Addressable Storage. Per-thread key-value store keyed by content hash. |
|
||||
| **Registry** | \`workflow.yaml\` — maps workflow names to their current and historical bundle hashes. |
|
||||
|
||||
## Commands
|
||||
|
||||
${commandSections.join("\n\n")}
|
||||
|
||||
### Top-level shortcuts
|
||||
|
||||
| Command | Equivalent | Description |
|
||||
|---------|------------|-------------|
|
||||
| \`run\` | \`thread run\` | Shortcut to start a thread |
|
||||
| \`live\` | \`thread live\` | Shortcut to attach to a thread |
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
1. \`uncaged-workflow workflow add my-wf ./my-wf.esm.js\` — register a workflow
|
||||
2. \`uncaged-workflow run my-wf --prompt "do the thing"\` — start a thread
|
||||
3. \`uncaged-workflow live --latest\` — attach and watch output
|
||||
4. \`uncaged-workflow thread show <thread-id>\` — inspect completed thread
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Error |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| \`UNCAGED_WORKFLOW_STORAGE_ROOT\` | Override the default storage directory for all workflow data |
|
||||
`;
|
||||
}
|
||||
|
||||
// ── develop topic (for agents inside a workflow) ───────────────────────
|
||||
|
||||
function formatSkillDevelop(): string {
|
||||
return `# Workflow Role Guide
|
||||
|
||||
Reference for agents executing roles (planner, coder, reviewer, etc.) inside a running workflow thread.
|
||||
|
||||
## Thread ID
|
||||
|
||||
Every thread has a 26-character Crockford Base32 ULID (e.g. \`06F03H5V6JTMDST6P3TVH42RWM\`).
|
||||
|
||||
It appears in the **first message** of the conversation. If unsure:
|
||||
|
||||
\`\`\`
|
||||
uncaged-workflow thread list
|
||||
\`\`\`
|
||||
|
||||
## CAS (Content-Addressable Storage)
|
||||
|
||||
Store and retrieve content by hash, scoped to the current thread.
|
||||
|
||||
| Operation | Command |
|
||||
|-----------|---------|
|
||||
| **Store** | \`uncaged-workflow cas put <THREAD_ID> '<content>'\` → prints hash |
|
||||
| **Read** | \`uncaged-workflow cas get <THREAD_ID> <HASH>\` → prints content |
|
||||
| **List** | \`uncaged-workflow cas list <THREAD_ID>\` |
|
||||
|
||||
CAS is the **only** supported way to persist structured data (phase plans, review notes, etc.) within a thread. Do not use temp files.
|
||||
|
||||
## Meta Output
|
||||
|
||||
Each role must produce structured output that the moderator extracts. The exact schema depends on the role, but the pattern is:
|
||||
|
||||
1. Do your work (write code, run tests, etc.)
|
||||
2. Output a compact JSON object matching the role's schema
|
||||
3. The moderator extracts and validates it automatically
|
||||
|
||||
## Thread Context
|
||||
|
||||
The conversation history contains outputs from previous roles. Read it to understand:
|
||||
- What task was requested (from the initial prompt)
|
||||
- What previous roles produced (plans, code changes, review results)
|
||||
- What the moderator decided (which phase to work on, whether to retry)
|
||||
`;
|
||||
}
|
||||
|
||||
// ── author topic (for workflow developers) ─────────────────────────────
|
||||
|
||||
function formatSkillAuthor(): string {
|
||||
return `# Workflow Authoring Guide
|
||||
|
||||
How to build, test, and publish workflow bundles for uncaged-workflow.
|
||||
|
||||
## Bundle Structure
|
||||
|
||||
A workflow bundle is a single ESM file (\`.esm.js\`) that exports:
|
||||
|
||||
\`\`\`typescript
|
||||
// Required exports
|
||||
export const descriptor: WorkflowDescriptor;
|
||||
export const run: WorkflowRun;
|
||||
\`\`\`
|
||||
|
||||
## WorkflowDescriptor
|
||||
|
||||
Defines the workflow's metadata and role sequence:
|
||||
|
||||
\`\`\`typescript
|
||||
type WorkflowDescriptor = {
|
||||
name: string; // verb-first kebab-case, e.g. "solve-issue"
|
||||
description: string; // one-line summary
|
||||
roles: string[]; // ordered role names, e.g. ["planner", "coder", "reviewer"]
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
## WorkflowRun
|
||||
|
||||
The main function that creates and returns a moderator:
|
||||
|
||||
\`\`\`typescript
|
||||
type WorkflowRun = (ctx: WorkflowContext) => Moderator;
|
||||
\`\`\`
|
||||
|
||||
The **Moderator** controls the flow — it decides which role runs next, handles retries, and determines when the workflow is complete.
|
||||
|
||||
## Role Definition
|
||||
|
||||
Each role has:
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| \`description\` | string | What the role does |
|
||||
| \`systemPrompt\` | string | System prompt for the agent |
|
||||
| \`extractPrompt\` | string | Instruction for extracting structured meta |
|
||||
| \`schema\` | ZodSchema | Validates the extracted meta |
|
||||
| \`extractRefs\` | fn or null | Extracts CAS hashes from meta for DAG linking |
|
||||
| \`extractMode\` | "single" | Extraction mode |
|
||||
|
||||
## Development Workflow
|
||||
|
||||
\`\`\`bash
|
||||
# 1. Initialize a workspace
|
||||
uncaged-workflow init workspace my-workflow
|
||||
|
||||
# 2. Write your template (roles + moderator + descriptor)
|
||||
|
||||
# 3. Build the ESM bundle
|
||||
bun run build
|
||||
|
||||
# 4. Register locally
|
||||
uncaged-workflow workflow add my-workflow ./dist/my-workflow.esm.js
|
||||
|
||||
# 5. Test
|
||||
uncaged-workflow run my-workflow --prompt "test task"
|
||||
uncaged-workflow live --latest
|
||||
\`\`\`
|
||||
|
||||
## Versioning
|
||||
|
||||
Bundles are immutable and identified by XXH64 hash. Re-registering a workflow with a new bundle creates a new version. Use \`workflow history\` and \`workflow rollback\` to manage versions.
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Legacy compat ──────────────────────────────────────────────────────
|
||||
|
||||
/** @deprecated Use formatSkillTopic("cli") instead */
|
||||
export function formatSkillDoc(): string {
|
||||
return formatSkillCli();
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow";
|
||||
|
||||
/** Resolve storage root, honoring `UNCAGED_WORKFLOW_STORAGE_ROOT` for tests/tools. */
|
||||
/**
|
||||
* Resolve storage root with env var override support.
|
||||
*
|
||||
* Priority (highest first):
|
||||
* 1. `UNCAGED_WORKFLOW_STORAGE_ROOT` — internal/test override
|
||||
* 2. `WORKFLOW_STORAGE_ROOT` — user-facing override
|
||||
* 3. Default (`~/.uncaged/workflow`)
|
||||
*/
|
||||
export function resolveWorkflowStorageRoot(): string {
|
||||
const override = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||
if (override !== undefined && override !== "") {
|
||||
return override;
|
||||
const internal = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||
if (internal !== undefined && internal !== "") {
|
||||
return internal;
|
||||
}
|
||||
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
|
||||
if (userOverride !== undefined && userOverride !== "") {
|
||||
return userOverride;
|
||||
}
|
||||
return getDefaultWorkflowStorageRoot();
|
||||
}
|
||||
|
||||
@@ -237,6 +237,30 @@ export async function sendWorkerTcpCommand(
|
||||
});
|
||||
}
|
||||
|
||||
export async function readWorkerCtl(
|
||||
storageRoot: string,
|
||||
hash: string,
|
||||
): Promise<Result<WorkerCtl, string>> {
|
||||
const ctlPath = join(storageRoot, "workers", `${hash}.json`);
|
||||
const ctlText = await readTextFileIfExists(ctlPath);
|
||||
if (ctlText === null) {
|
||||
return err(`worker control file missing for bundle hash ${hash}`);
|
||||
}
|
||||
|
||||
let ctl: WorkerCtl;
|
||||
try {
|
||||
ctl = JSON.parse(ctlText) as WorkerCtl;
|
||||
} catch {
|
||||
return err(`corrupt worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
||||
return err(`invalid worker control file: ${ctlPath}`);
|
||||
}
|
||||
|
||||
return ok(ctl);
|
||||
}
|
||||
|
||||
export async function resolveRunningHashForThread(
|
||||
storageRoot: string,
|
||||
threadId: string,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-coder",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "echo no tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { type CoderMeta, coderMetaSchema, coderRole } from "./coder.js";
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-committer",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { type CommitterMeta, committerMetaSchema, committerRole } from "./committer.js";
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-planner",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "echo no tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export {
|
||||
type PlannerMeta,
|
||||
phaseSchema,
|
||||
plannerMetaSchema,
|
||||
plannerRole,
|
||||
} from "./planner.js";
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-preparer",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "echo no tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-reviewer",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { type ReviewerMeta, reviewerMetaSchema, reviewerRole } from "./reviewer.js";
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const reviewerMetaSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("approved"),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("rejected"),
|
||||
issues: z.array(z.string()).describe("blocking issues that must be fixed"),
|
||||
}),
|
||||
]);
|
||||
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
||||
|
||||
const REVIEWER_SYSTEM = `You are a code reviewer. Review the current git diff. Give a clear approve or reject verdict.
|
||||
Only reject for blocking issues. End with your verdict.`;
|
||||
|
||||
export const reviewerRole: RoleDefinition<ReviewerMeta> = {
|
||||
description: "Runs git diff checks and sets approved when the change is ready.",
|
||||
systemPrompt: REVIEWER_SYSTEM,
|
||||
extractPrompt:
|
||||
"Extract the review verdict: approved or rejected. If rejected, list the blocking issues.",
|
||||
schema: reviewerMetaSchema,
|
||||
extractRefs: null,
|
||||
extractMode: "single",
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-submitter",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { type SubmitterMeta, submitterMetaSchema, submitterRole } from "./submitter.js";
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-tester",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { type TesterMeta, testerMetaSchema, testerRole } from "./tester.js";
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { committerMetaSchema, committerRole } from "../src/committer.js";
|
||||
import { committerMetaSchema, committerRole } from "../src/roles/committer.js";
|
||||
|
||||
describe("committerRole", () => {
|
||||
test("committed sample validates against schema", () => {
|
||||
@@ -6,12 +6,9 @@ import {
|
||||
START,
|
||||
validateWorkflowDescriptor,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import type { CommitterMeta } from "@uncaged/workflow-role-committer";
|
||||
import type { PlannerMeta } from "@uncaged/workflow-role-planner";
|
||||
|
||||
import { buildDevelopDescriptor } from "../src/descriptor.js";
|
||||
import { developModerator } from "../src/index.js";
|
||||
import type { CommitterMeta, PlannerMeta } from "../src/roles/index.js";
|
||||
import type { DevelopMeta } from "../src/roles.js";
|
||||
|
||||
const DEFAULT_PHASES: PlannerMeta["phases"] = [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { reviewerMetaSchema, reviewerRole } from "../src/reviewer.js";
|
||||
import { reviewerMetaSchema, reviewerRole } from "../src/roles/reviewer.js";
|
||||
|
||||
describe("reviewerRole", () => {
|
||||
test("approved sample validates against schema", () => {
|
||||
@@ -5,15 +5,10 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"@uncaged/workflow-role-coder": "workspace:*",
|
||||
"@uncaged/workflow-role-committer": "workspace:*",
|
||||
"@uncaged/workflow-role-planner": "workspace:*",
|
||||
"@uncaged/workflow-role-reviewer": "workspace:*",
|
||||
"@uncaged/workflow-role-tester": "workspace:*"
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,34 +10,26 @@ import {
|
||||
import { developModerator } from "./moderator.js";
|
||||
import { DEVELOP_WORKFLOW_DESCRIPTION, type DevelopMeta, developRoles } from "./roles.js";
|
||||
|
||||
export { buildDevelopDescriptor } from "./descriptor.js";
|
||||
export { developModerator } from "./moderator.js";
|
||||
export {
|
||||
type CoderMeta,
|
||||
type CommitterMeta,
|
||||
coderMetaSchema,
|
||||
coderRole,
|
||||
} from "@uncaged/workflow-role-coder";
|
||||
export {
|
||||
type CommitterMeta,
|
||||
committerMetaSchema,
|
||||
committerRole,
|
||||
} from "@uncaged/workflow-role-committer";
|
||||
export {
|
||||
type PlannerMeta,
|
||||
phaseSchema,
|
||||
plannerMetaSchema,
|
||||
plannerRole,
|
||||
} from "@uncaged/workflow-role-planner";
|
||||
export {
|
||||
type ReviewerMeta,
|
||||
reviewerMetaSchema,
|
||||
reviewerRole,
|
||||
} from "@uncaged/workflow-role-reviewer";
|
||||
export {
|
||||
type TesterMeta,
|
||||
testerMetaSchema,
|
||||
testerRole,
|
||||
} from "@uncaged/workflow-role-tester";
|
||||
export { buildDevelopDescriptor } from "./descriptor.js";
|
||||
export { developModerator } from "./moderator.js";
|
||||
} from "./roles/index.js";
|
||||
export {
|
||||
DEVELOP_WORKFLOW_DESCRIPTION,
|
||||
type DevelopMeta,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import { type CoderMeta, coderRole } from "@uncaged/workflow-role-coder";
|
||||
import { type CommitterMeta, committerRole } from "@uncaged/workflow-role-committer";
|
||||
import { type PlannerMeta, plannerRole } from "@uncaged/workflow-role-planner";
|
||||
import { type ReviewerMeta, reviewerRole } from "@uncaged/workflow-role-reviewer";
|
||||
import { type TesterMeta, testerRole } from "@uncaged/workflow-role-tester";
|
||||
import { type CoderMeta, coderRole } from "./roles/coder.js";
|
||||
import { type CommitterMeta, committerRole } from "./roles/committer.js";
|
||||
import { type PlannerMeta, plannerRole } from "./roles/planner.js";
|
||||
import { type ReviewerMeta, reviewerRole } from "./roles/reviewer.js";
|
||||
import { type TesterMeta, testerRole } from "./roles/tester.js";
|
||||
|
||||
export const DEVELOP_WORKFLOW_DESCRIPTION =
|
||||
"Plan phases, implement incrementally, review, verify with tests/build/lint, and commit (planner → coder [repeat per phase] → reviewer → tester → committer).";
|
||||
|
||||
+3
-11
@@ -11,21 +11,13 @@ export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
||||
|
||||
const CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only.
|
||||
|
||||
## Finding the current thread ID
|
||||
|
||||
The thread ID is a 26-character Crockford Base32 string (e.g. \`06F03H5V6JTMDST6P3TVH42RWM\`). It appears in the first message of this conversation. If you are unsure, run:
|
||||
|
||||
uncaged-workflow threads
|
||||
|
||||
and use the ID of the active thread.
|
||||
Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
|
||||
|
||||
## Reading phase details
|
||||
|
||||
Each planner phase is identified by a content-hash and a title. To read a phase's full details (name, description, acceptance criteria), run:
|
||||
Each planner phase has a content-hash and title. Read full details with \`uncaged-workflow cas get <THREAD_ID> <HASH>\`.
|
||||
|
||||
uncaged-workflow cas get <THREAD_ID> <HASH>
|
||||
|
||||
Replace \`<THREAD_ID>\` with the actual thread ID and \`<HASH>\` with the phase hash from the plan.
|
||||
The thread ID (26-char Crockford Base32) appears in the first message. If unsure, run \`uncaged-workflow thread list\`.
|
||||
|
||||
## Completing a phase
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export { type CoderMeta, coderMetaSchema, coderRole } from "./coder.js";
|
||||
export { type CommitterMeta, committerMetaSchema, committerRole } from "./committer.js";
|
||||
export {
|
||||
type PlannerMeta,
|
||||
phaseSchema,
|
||||
plannerMetaSchema,
|
||||
plannerRole,
|
||||
} from "./planner.js";
|
||||
export { type ReviewerMeta, reviewerMetaSchema, reviewerRole } from "./reviewer.js";
|
||||
export { type TesterMeta, testerMetaSchema, testerRole } from "./tester.js";
|
||||
+11
-13
@@ -14,27 +14,25 @@ export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
||||
|
||||
const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time.
|
||||
|
||||
## Finding the current thread ID
|
||||
|
||||
The thread ID is a 26-character Crockford Base32 string (e.g. \`06F03H5V6JTMDST6P3TVH42RWM\`). It appears in the first message of this conversation. If you are unsure, run:
|
||||
|
||||
uncaged-workflow threads
|
||||
|
||||
and use the ID of the active thread.
|
||||
Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
|
||||
|
||||
## Storing phase details — MANDATORY
|
||||
|
||||
For each phase you MUST store its full detail text in CAS using this exact CLI command:
|
||||
For each phase, store its full detail text in CAS via \`uncaged-workflow cas put <THREAD_ID> '<content>'\`. The command prints a content-hash — use that as the phase identifier.
|
||||
|
||||
uncaged-workflow cas put <THREAD_ID> '# <name>
|
||||
The thread ID (26-char Crockford Base32) appears in the first message. If unsure, run \`uncaged-workflow thread list\`.
|
||||
|
||||
Description: <description>
|
||||
**Do NOT store phase details in any other way** — the CLI is the only supported storage mechanism.
|
||||
|
||||
Acceptance: <acceptance>'
|
||||
## Phase granularity
|
||||
|
||||
Replace \`<THREAD_ID>\` with the actual thread ID you found above. The command prints a content-hash to stdout — use that hash as the phase identifier.
|
||||
Match the number of phases to task complexity:
|
||||
- Trivial (add a config option, fix a typo, rename): 1 phase
|
||||
- Small (a new feature touching 2-3 files): 1-2 phases
|
||||
- Medium (cross-module refactor): 2-3 phases
|
||||
- Large (new subsystem, architectural change): 3-5 phases
|
||||
|
||||
**Do NOT store phase details in any other way** (no temp files, no invented paths). The CLI command is the only supported storage mechanism.
|
||||
Fewer phases is always better. Each phase must justify its existence — if two phases would be tested together anyway, merge them.
|
||||
|
||||
## Output format
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const reviewerMetaSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("approved"),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("rejected"),
|
||||
issues: z.array(z.string()).describe("blocking issues that must be fixed"),
|
||||
}),
|
||||
]);
|
||||
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
||||
|
||||
const REVIEWER_SYSTEM = `You are a code reviewer. Review the git diff for correctness, consistency, and adherence to project conventions.
|
||||
|
||||
## Review process
|
||||
|
||||
1. Read the **preparer**'s output in the thread for project conventions (coding style, naming, commit format, etc.).
|
||||
2. Review the diff against these conventions.
|
||||
3. For documentation changes, verify that names, paths, and references match the actual codebase.
|
||||
|
||||
## Review checklist
|
||||
|
||||
- **Correctness** — does the code do what it claims? Logic bugs, off-by-one, missing returns?
|
||||
- **Conventions** — naming, imports, code style per project rules?
|
||||
- **Consistency** — do docs/comments match actual code? Are references current and accurate?
|
||||
- **Edge cases** — missing error handling, null checks, boundary conditions?
|
||||
|
||||
## Verdict
|
||||
|
||||
- **Approve** only if there are zero issues
|
||||
- **Reject** with specific issues that must be fixed — every issue you find is blocking
|
||||
|
||||
Be thorough. A false approve costs more than a false reject.`;
|
||||
|
||||
export const reviewerRole: RoleDefinition<ReviewerMeta> = {
|
||||
description: "Runs git diff checks and sets approved when the change is ready.",
|
||||
systemPrompt: REVIEWER_SYSTEM,
|
||||
extractPrompt:
|
||||
"Extract the review verdict: approved or rejected. If rejected, list the blocking issues.",
|
||||
schema: reviewerMetaSchema,
|
||||
extractRefs: null,
|
||||
extractMode: "single",
|
||||
};
|
||||
@@ -6,12 +6,5 @@
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../workflow" },
|
||||
{ "path": "../workflow-role-coder" },
|
||||
{ "path": "../workflow-role-committer" },
|
||||
{ "path": "../workflow-role-planner" },
|
||||
{ "path": "../workflow-role-reviewer" },
|
||||
{ "path": "../workflow-role-tester" }
|
||||
]
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
|
||||
@@ -11,13 +11,10 @@ import {
|
||||
START,
|
||||
validateWorkflowDescriptor,
|
||||
} from "@uncaged/workflow";
|
||||
|
||||
import type { PreparerMeta } from "@uncaged/workflow-role-preparer";
|
||||
import type { SubmitterMeta } from "@uncaged/workflow-role-submitter";
|
||||
|
||||
import { buildSolveIssueDescriptor } from "../src/descriptor.js";
|
||||
import type { DeveloperMeta } from "../src/developer.js";
|
||||
import { createSolveIssueRun, solveIssueModerator } from "../src/index.js";
|
||||
import type { PreparerMeta, SubmitterMeta } from "../src/roles/index.js";
|
||||
import type { SolveIssueMeta } from "../src/roles.js";
|
||||
|
||||
function jsonResponse(payload: Record<string, unknown>): Response {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { submitterMetaSchema, submitterRole } from "../src/submitter.js";
|
||||
import { submitterMetaSchema, submitterRole } from "../src/roles/submitter.js";
|
||||
|
||||
describe("submitterRole", () => {
|
||||
test("submitted sample validates against schema", () => {
|
||||
@@ -5,13 +5,10 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"@uncaged/workflow-role-preparer": "workspace:*",
|
||||
"@uncaged/workflow-role-submitter": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,6 @@ import {
|
||||
import { solveIssueModerator } from "./moderator.js";
|
||||
import { SOLVE_ISSUE_WORKFLOW_DESCRIPTION, type SolveIssueMeta, solveIssueRoles } from "./roles.js";
|
||||
|
||||
export {
|
||||
type PreparerMeta,
|
||||
preparerMetaSchema,
|
||||
preparerRole,
|
||||
} from "@uncaged/workflow-role-preparer";
|
||||
export {
|
||||
type SubmitterMeta,
|
||||
submitterMetaSchema,
|
||||
submitterRole,
|
||||
} from "@uncaged/workflow-role-submitter";
|
||||
export { buildSolveIssueDescriptor } from "./descriptor.js";
|
||||
export {
|
||||
type DeveloperMeta,
|
||||
@@ -28,6 +18,14 @@ export {
|
||||
developerRole,
|
||||
} from "./developer.js";
|
||||
export { solveIssueModerator } from "./moderator.js";
|
||||
export {
|
||||
type PreparerMeta,
|
||||
preparerMetaSchema,
|
||||
preparerRole,
|
||||
type SubmitterMeta,
|
||||
submitterMetaSchema,
|
||||
submitterRole,
|
||||
} from "./roles/index.js";
|
||||
export {
|
||||
SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
|
||||
type SolveIssueMeta,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import { type PreparerMeta, preparerRole } from "@uncaged/workflow-role-preparer";
|
||||
import { type SubmitterMeta, submitterRole } from "@uncaged/workflow-role-submitter";
|
||||
|
||||
import { type DeveloperMeta, developerRole } from "./developer.js";
|
||||
import { type PreparerMeta, preparerRole } from "./roles/preparer.js";
|
||||
import { type SubmitterMeta, submitterRole } from "./roles/submitter.js";
|
||||
|
||||
export const SOLVE_ISSUE_WORKFLOW_DESCRIPTION =
|
||||
"Resolve an issue end-to-end by preparing the repo, delegating implementation to the develop workflow, and opening a pull request (preparer → developer → submitter).";
|
||||
|
||||
+1
@@ -3,3 +3,4 @@ export {
|
||||
preparerMetaSchema,
|
||||
preparerRole,
|
||||
} from "./preparer.js";
|
||||
export { type SubmitterMeta, submitterMetaSchema, submitterRole } from "./submitter.js";
|
||||
@@ -6,9 +6,5 @@
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../workflow" },
|
||||
{ "path": "../workflow-role-preparer" },
|
||||
{ "path": "../workflow-role-submitter" }
|
||||
]
|
||||
"references": [{ "path": "../workflow" }]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/azureuser/repos/uncaged-workflow/packages/workflow
|
||||
@@ -19,13 +19,6 @@
|
||||
"references": [
|
||||
{ "path": "packages/workflow" },
|
||||
{ "path": "packages/workflow-agent-llm" },
|
||||
{ "path": "packages/workflow-role-committer" },
|
||||
{ "path": "packages/workflow-role-coder" },
|
||||
{ "path": "packages/workflow-role-planner" },
|
||||
{ "path": "packages/workflow-role-preparer" },
|
||||
{ "path": "packages/workflow-role-reviewer" },
|
||||
{ "path": "packages/workflow-role-submitter" },
|
||||
{ "path": "packages/workflow-role-tester" },
|
||||
{ "path": "packages/workflow-agent-cursor" },
|
||||
{ "path": "packages/workflow-agent-hermes" },
|
||||
{ "path": "packages/workflow-util-agent" },
|
||||
|
||||
Reference in New Issue
Block a user