Compare commits

...

15 Commits

Author SHA1 Message Date
xingyue 9bdb18afd0 refactor(cli): merge kill/pause/resume into control.ts + extract readWorkerCtl
- Merge three near-identical files (kill.ts, pause.ts, resume.ts) into
  commands/thread/control.ts with parameterized cmdThreadControl()
- Extract readWorkerCtl() into worker-spawn.ts to eliminate duplicated
  WorkerCtl parsing logic
- Update cli-dispatch.ts and test imports
- Net reduction: ~59 lines

Refs #95
2026-05-08 08:55:25 +08:00
xiaomo 2af299f3ce Merge pull request 'refactor(cli): restructure cmd-*.ts into commands/ subdirectories' (#98) from refactor/93-phase1-directory-restructure into main 2026-05-08 00:48:30 +00:00
xiaoju d9f79c60a1 Merge pull request 'chore: remove unused build scripts' (#99) from chore/remove-build-scripts into main 2026-05-08 00:46:54 +00:00
xiaoju 485bfcb0b6 chore: remove unused build scripts
All packages are pure Bun/TS — no build step needed.
The build scripts were all placeholder `echo 'TODO'` anyway.
2026-05-08 00:46:35 +00:00
xiaoju a47ed06ea5 Merge pull request 'docs: create README.md, update architecture.md for current structure' (#89) from docs/88-readme-architecture-cleanup into main 2026-05-08 00:42:16 +00:00
xingyue 2ef004eecf refactor(cli): restructure cmd-*.ts into commands/ subdirectories
Reorganize flat cmd-*.ts files into commands/{workflow,thread,cas,init}/
subdirectories that strictly mirror the CLI subcommand hierarchy:

- workflow/: add, list, show, rm, history, rollback
- thread/: run, list, show, rm, fork, ps, kill, live, pause, resume
- cas/: get, put, list, rm, gc
- init/: workspace, template

Each group has an index.ts re-export. Split multi-command files
(cmd-cas.ts, cmd-thread.ts, cmd-init.ts) into per-subcommand files.
Rename cmd-help.ts → skill.ts to match the primary command name.

Update all import paths in cli-dispatch.ts and test files.

Pure structural change — no logic modifications.

Ref: #93, closes #94
2026-05-08 00:36:54 +08:00
xiaoju 2616259a0f Merge pull request 'feat(reviewer): enrich prompt with conventions + CLI awareness' (#92) from feat/91-reviewer-prompt into main 2026-05-07 16:27:08 +00:00
xiaoju 23b2c3b47d feat(reviewer): enrich prompt with conventions awareness + strict verdicts
- Read preparer's conventions from thread context
- Review checklist: correctness, conventions, consistency, edge cases
- No nits: every issue is blocking, approve only at zero issues
- Generic prompt, no workflow-specific concepts

Closes #91

小橘 🍊
2026-05-07 16:25:31 +00:00
xiaoju 7d3954097d docs: fix deprecated CLI commands in README
- workflow add (was: add)
- workflow list (was: list)
- thread list (was: threads)
- thread show (was: thread)

小橘 🍊
2026-05-07 16:15:29 +00:00
xiaoju 4a925b98af docs: create README.md, update architecture.md for current structure
- Create root README.md with project intro, concepts, packages, quickstart
- Remove workflow-role-* references from docs/architecture.md
- Roles now live inside template packages (src/roles/)
- Clean up untracked dist/packages/workflow-role-* remnants

Fixes #88
2026-05-07 16:10:37 +00:00
xingyue bfea771a52 Merge pull request 'fix(cli): improve usage format + fix skill index title' (#86) from fix/85-usage-format into main 2026-05-07 15:33:27 +00:00
xiaoju 5e411a1f19 fix(cli): improve usage output format + fix skill index title
- Usage: grouped sections with titles, aligned descriptions
- Header: 'uncaged-workflow — workflow engine CLI'
- Footer: 'Use <command> --help for subcommand details.'
- Fix skill index title: 'uncaged-workflow skill' (was: help --skill)
- 242 tests pass

Closes #85

小橘 🍊
2026-05-07 15:31:48 +00:00
xingyue 21238f7825 Merge pull request 'fix(cli): usage not red + skill subcommand + --help flag' (#84) from fix/83-cli-ux into main 2026-05-07 15:19:08 +00:00
xiaoju 6b3aa4ce35 fix(cli): usage not red + skill subcommand + --help flag on groups
1. No-args usage uses printCliLine (not printCliError), exit 1
2. 'skill [topic]' as first-class command (help --skill kept as compat)
3. 'workflow --help', 'thread --help' etc. show group subcommands
4. Role prompts updated: 'uncaged-workflow skill develop'

240 tests (6 new), build clean.

Closes #83

小橘 🍊
2026-05-07 15:17:20 +00:00
xingyue f042c9d640 Merge pull request 'feat(cli): help --skill <topic> for context-specific agent docs' (#82) from feat/81-skill-topics into main 2026-05-07 15:08:41 +00:00
55 changed files with 731 additions and 502 deletions
+71
View File
@@ -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.
+2 -5
View File
@@ -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.
-1
View File
@@ -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));
+91 -24
View File
@@ -1,11 +1,11 @@
import { describe, expect, test } from "bun:test";
import { runCli } from "../src/cli-dispatch.js";
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
import {
formatSkillDoc,
formatSkillIndex,
formatSkillTopic,
getSkillTopics,
} from "../src/cmd-help.js";
} from "../src/skill.js";
const STORAGE_ROOT = "/tmp/help-test-storage";
@@ -15,30 +15,66 @@ describe("help command", () => {
expect(code).toBe(0);
});
test("help --skill (no topic) returns 0 and lists topics", 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);
});
test("help --skill cli returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "cli"]);
expect(code).toBe(0);
});
test("help --skill develop returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "develop"]);
expect(code).toBe(0);
});
test("help --skill author returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "author"]);
expect(code).toBe(0);
});
test("help --skill unknown returns 1", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "unknown"]);
expect(code).toBe(1);
});
});
describe("getSkillTopics", () => {
@@ -54,10 +90,41 @@ describe("getSkillTopics", () => {
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("help --skill <topic>");
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");
});
});
@@ -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 () => {
+1 -1
View File
@@ -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));
@@ -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";
-1
View File
@@ -10,7 +10,6 @@
"yaml": "^2.8.4"
},
"scripts": {
"build": "echo 'TODO'",
"test": "bun test"
}
}
+114 -37
View File
@@ -1,25 +1,28 @@
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, formatSkillIndex, formatSkillTopic } 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";
import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "./skill.js";
type DispatchFn = (storageRoot: string, argv: string[]) => Promise<number>;
@@ -525,18 +528,68 @@ export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
// ── 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 {
const groups = getCommandRegistry();
const lines: string[] = ["Usage:"];
const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""];
for (const group of groups) {
for (const cmd of group.commands) {
const args = cmd.args ? ` ${cmd.args}` : "";
lines.push(` uncaged-workflow ${group.name} ${cmd.name}${args}`);
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(" 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("Environment variables:");
lines.push(
@@ -561,9 +614,16 @@ function dispatchGroup(
argv: string[],
): Promise<number> | null {
const sub = argv[0];
if (sub === undefined) {
printCliError(`${formatCliUsage()}\n\nerror: unknown ${tableName} subcommand: (none)`);
return Promise.resolve(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 entry = table[sub];
if (entry === undefined) {
@@ -618,13 +678,8 @@ async function dispatchCas(storageRoot: string, argv: string[]): Promise<number>
// ── Help ────────────────────────────────────────────────────────────────
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
const skillIdx = argv.indexOf("--skill");
if (skillIdx === -1) {
printCliLine(formatCliUsage());
return 0;
}
const topic = argv[skillIdx + 1];
async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<number> {
const topic = argv[0];
if (topic === undefined) {
printCliLine(formatSkillIndex());
return 0;
@@ -638,6 +693,27 @@ async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<numbe
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;
}
// ── Top-level command table (Phase 3) ──────────────────────────────────
const COMMAND_TABLE: Record<string, DispatchFn> = {
@@ -647,6 +723,7 @@ const COMMAND_TABLE: Record<string, DispatchFn> = {
cas: dispatchCas,
init: dispatchInit,
help: dispatchHelp,
skill: dispatchSkill,
// Top-level shortcuts (no deprecation)
run: dispatchRun,
@@ -672,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);
-43
View File
@@ -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);
}
-43
View File
@@ -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 },
);
}
-43
View File
@@ -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 },
);
}
-43
View File
@@ -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 });
}
@@ -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");
}
@@ -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";
@@ -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,
@@ -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,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);
@@ -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,
@@ -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);
}
@@ -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;
@@ -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";
@@ -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);
@@ -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,
@@ -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,
@@ -34,7 +34,7 @@ export function formatSkillTopic(topic: string): string | null {
export function formatSkillIndex(): string {
const rows = SKILL_TOPICS.map((t) => `| \`${t.name}\` | ${t.description} |`);
return `# uncaged-workflow help --skill
return `# uncaged-workflow skill
Available topics:
@@ -42,7 +42,7 @@ Available topics:
|-------|-------------|
${rows.join("\n")}
Usage: \`uncaged-workflow help --skill <topic>\`
Usage: \`uncaged-workflow skill <topic>\`
`;
}
+24
View File
@@ -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": {
-1
View File
@@ -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": {
@@ -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.
Run \`uncaged-workflow help --skill develop\` for thread ID lookup, CAS commands, and meta output guide.
Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
## 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.
Run \`uncaged-workflow help --skill develop\` for thread ID lookup, CAS commands, and meta output guide.
Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
## Storing phase details — MANDATORY
@@ -12,8 +12,27 @@ export const reviewerMetaSchema = z.discriminatedUnion("status", [
]);
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.`;
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.",
@@ -5,7 +5,6 @@
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "echo 'TODO'",
"test": "bun test"
},
"dependencies": {
@@ -11,7 +11,6 @@
}
},
"scripts": {
"build": "echo 'TODO'",
"test": "bun test"
},
"dependencies": {
-1
View File
@@ -5,7 +5,6 @@
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "echo 'TODO'",
"test": "bun test"
},
"dependencies": {