Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 485bfcb0b6 | |||
| 7d3954097d | |||
| 4a925b98af | |||
| bfea771a52 | |||
| 5e411a1f19 | |||
| 21238f7825 | |||
| 6b3aa4ce35 | |||
| f042c9d640 | |||
| 66bca9ef03 | |||
| 309af39447 |
@@ -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-cursor` | `@uncaged/workflow-agent-cursor` | Cursor CLI agent (extracts workspace from ctx) |
|
||||||
| `workflow-agent-hermes` | `@uncaged/workflow-agent-hermes` | Hermes CLI agent |
|
| `workflow-agent-hermes` | `@uncaged/workflow-agent-hermes` | Hermes CLI agent |
|
||||||
| `workflow-agent-llm` | `@uncaged/workflow-agent-llm` | OpenAI-compatible LLM 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-template-develop` | `@uncaged/workflow-template-develop` | Develop workflow template (roles in `src/roles/`) |
|
||||||
| `workflow-role-coder` | `@uncaged/workflow-role-coder` | Pure data: coding prompt + schema |
|
| `workflow-template-solve-issue` | `@uncaged/workflow-template-solve-issue` | Solve-issue workflow template (roles in `src/roles/`) |
|
||||||
| `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-util-agent` | `@uncaged/workflow-util-agent` | `buildAgentPrompt` + `spawnCli` utilities |
|
| `workflow-util-agent` | `@uncaged/workflow-util-agent` | `buildAgentPrompt` + `spawnCli` utilities |
|
||||||
|
|
||||||
Monorepo with **bun workspace**, `workspace:*` protocol.
|
Monorepo with **bun workspace**, `workspace:*` protocol.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"examples"
|
"examples"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run --filter '*' build",
|
|
||||||
"check": "bunx tsc --build && biome check .",
|
"check": "bunx tsc --build && biome check .",
|
||||||
"typecheck": "bunx tsc --build",
|
"typecheck": "bunx tsc --build",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { runCli } from "../src/cli-dispatch.js";
|
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
||||||
import { formatSkillDoc } from "../src/cmd-help.js";
|
import {
|
||||||
|
formatSkillDoc,
|
||||||
|
formatSkillIndex,
|
||||||
|
formatSkillTopic,
|
||||||
|
getSkillTopics,
|
||||||
|
} from "../src/cmd-help.js";
|
||||||
|
|
||||||
const STORAGE_ROOT = "/tmp/help-test-storage";
|
const STORAGE_ROOT = "/tmp/help-test-storage";
|
||||||
|
|
||||||
@@ -10,13 +15,120 @@ describe("help command", () => {
|
|||||||
expect(code).toBe(0);
|
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"]);
|
const code = await runCli(STORAGE_ROOT, ["help", "--skill"]);
|
||||||
expect(code).toBe(0);
|
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();
|
const doc = formatSkillDoc();
|
||||||
|
|
||||||
test("contains title", () => {
|
test("contains title", () => {
|
||||||
@@ -82,3 +194,52 @@ describe("formatSkillDoc", () => {
|
|||||||
expect(doc).toContain("## Typical Workflow");
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -129,8 +129,9 @@ describe("init workspace", () => {
|
|||||||
|
|
||||||
test("usage lists init subcommands", () => {
|
test("usage lists init subcommands", () => {
|
||||||
const u = formatCliUsage();
|
const u = formatCliUsage();
|
||||||
expect(u).toContain("uncaged-workflow init workspace <name>");
|
expect(u).toContain("init workspace <name>");
|
||||||
expect(u).toContain("uncaged-workflow init template <name>");
|
expect(u).toContain("init template <name>");
|
||||||
|
expect(u).toContain("Development:");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("runCli rejects unknown init subcommand", async () => {
|
test("runCli rejects unknown init subcommand", async () => {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"yaml": "^2.8.4"
|
"yaml": "^2.8.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { cmdAdd, formatAddSuccess, parseAddArgv } from "./cmd-add.js";
|
|||||||
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "./cmd-cas.js";
|
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "./cmd-cas.js";
|
||||||
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
||||||
import { cmdGc } from "./cmd-gc.js";
|
import { cmdGc } from "./cmd-gc.js";
|
||||||
import { formatSkillDoc } from "./cmd-help.js";
|
import {
|
||||||
|
formatSkillDoc,
|
||||||
|
formatSkillIndex,
|
||||||
|
formatSkillTopic,
|
||||||
|
getSkillTopics,
|
||||||
|
} from "./cmd-help.js";
|
||||||
import { cmdHistory } from "./cmd-history.js";
|
import { cmdHistory } from "./cmd-history.js";
|
||||||
import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js";
|
import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js";
|
||||||
import { cmdKill } from "./cmd-kill.js";
|
import { cmdKill } from "./cmd-kill.js";
|
||||||
@@ -525,18 +530,68 @@ export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
|
|||||||
|
|
||||||
// ── Auto-generated CLI usage ───────────────────────────────────────────
|
// ── Auto-generated CLI usage ───────────────────────────────────────────
|
||||||
|
|
||||||
|
const USAGE_SECTION_BY_GROUP: Record<string, string> = {
|
||||||
|
workflow: "Workflow registry:",
|
||||||
|
thread: "Thread execution:",
|
||||||
|
cas: "Content-addressable storage:",
|
||||||
|
init: "Development:",
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
export function formatCliUsage(): string {
|
||||||
const groups = getCommandRegistry();
|
const groups = getCommandRegistry();
|
||||||
const lines: string[] = ["Usage:"];
|
const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""];
|
||||||
|
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
for (const cmd of group.commands) {
|
const sectionTitle = USAGE_SECTION_BY_GROUP[group.name];
|
||||||
const args = cmd.args ? ` ${cmd.args}` : "";
|
if (sectionTitle === undefined) {
|
||||||
lines.push(` uncaged-workflow ${group.name} ${cmd.name}${args}`);
|
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("");
|
||||||
}
|
}
|
||||||
lines.push(" uncaged-workflow run <name> [...] (shortcut for thread run)");
|
|
||||||
lines.push(" uncaged-workflow live <thread-id> [...] (shortcut for thread live)");
|
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("");
|
||||||
lines.push("Environment variables:");
|
lines.push("Environment variables:");
|
||||||
lines.push(
|
lines.push(
|
||||||
@@ -561,9 +616,16 @@ function dispatchGroup(
|
|||||||
argv: string[],
|
argv: string[],
|
||||||
): Promise<number> | null {
|
): Promise<number> | null {
|
||||||
const sub = argv[0];
|
const sub = argv[0];
|
||||||
if (sub === undefined) {
|
if (sub === undefined || sub === "--help" || sub === "-h") {
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: unknown ${tableName} subcommand: (none)`);
|
const entries = Object.entries(table);
|
||||||
return Promise.resolve(1);
|
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 entry = table[sub];
|
const entry = table[sub];
|
||||||
if (entry === undefined) {
|
if (entry === undefined) {
|
||||||
@@ -618,12 +680,39 @@ async function dispatchCas(storageRoot: string, argv: string[]): Promise<number>
|
|||||||
|
|
||||||
// ── Help ────────────────────────────────────────────────────────────────
|
// ── Help ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<number> {
|
||||||
if (argv.includes("--skill")) {
|
const topic = argv[0];
|
||||||
printCliLine(formatSkillDoc());
|
if (topic === undefined) {
|
||||||
} else {
|
printCliLine(formatSkillIndex());
|
||||||
printCliLine(formatCliUsage());
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,6 +725,7 @@ const COMMAND_TABLE: Record<string, DispatchFn> = {
|
|||||||
cas: dispatchCas,
|
cas: dispatchCas,
|
||||||
init: dispatchInit,
|
init: dispatchInit,
|
||||||
help: dispatchHelp,
|
help: dispatchHelp,
|
||||||
|
skill: dispatchSkill,
|
||||||
|
|
||||||
// Top-level shortcuts (no deprecation)
|
// Top-level shortcuts (no deprecation)
|
||||||
run: dispatchRun,
|
run: dispatchRun,
|
||||||
@@ -661,12 +751,12 @@ const DEPRECATED_ALIASES: Record<string, { newCmd: string; handler: DispatchFn }
|
|||||||
|
|
||||||
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
if (argv.length === 0) {
|
if (argv.length === 0) {
|
||||||
printCliError(formatCliUsage());
|
printCliLine(formatCliUsage());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const command = argv[0];
|
const command = argv[0];
|
||||||
if (command === undefined) {
|
if (command === undefined) {
|
||||||
printCliError(formatCliUsage());
|
printCliLine(formatCliUsage());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const rest = argv.slice(1);
|
const rest = argv.slice(1);
|
||||||
|
|||||||
@@ -1,6 +1,54 @@
|
|||||||
import { getCommandRegistry } from "./cli-dispatch.js";
|
import { getCommandRegistry } from "./cli-dispatch.js";
|
||||||
|
|
||||||
export function formatSkillDoc(): string {
|
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 groups = getCommandRegistry();
|
||||||
|
|
||||||
const commandSections: string[] = [];
|
const commandSections: string[] = [];
|
||||||
@@ -58,3 +106,133 @@ ${commandSections.join("\n\n")}
|
|||||||
| \`UNCAGED_WORKFLOW_STORAGE_ROOT\` | Override the default storage directory for all workflow data |
|
| \`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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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.
|
const CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only.
|
||||||
|
|
||||||
Run \`uncaged-workflow help --skill\` for full CLI reference (thread ID lookup, CAS commands, etc.).
|
Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
|
||||||
|
|
||||||
## Reading phase details
|
## Reading phase details
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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.
|
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.
|
||||||
|
|
||||||
Run \`uncaged-workflow help --skill\` for full CLI reference (thread ID lookup, CAS commands, etc.).
|
Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
|
||||||
|
|
||||||
## Storing phase details — MANDATORY
|
## Storing phase details — MANDATORY
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'TODO'",
|
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
Reference in New Issue
Block a user