Compare commits

..

1 Commits

Author SHA1 Message Date
xiaoju f61b395727 feat(cli): add currentRole field to thread show and thread list output (#571)
CI / test (pull_request) Successful in 1m9s
Add currentRole: string | null to StepOutput and ThreadListItemWithStatus.
- idle/running: derives next role via evaluate() on workflow graph
- completed/cancelled: null
-  as next role: null

Includes 9 test cases covering all status combinations and conditional routing.
2026-05-28 01:52:09 +00:00
67 changed files with 197 additions and 2070 deletions
+6 -7
View File
@@ -7,19 +7,18 @@ on:
branches: [main]
jobs:
check:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- run: bun install
- name: Install dependencies
run: bun install
- name: Build
run: bun run build
- name: Lint
- name: Check
run: bun run check
- name: Test
-183
View File
@@ -1,183 +0,0 @@
# UWF Bootstrap Guide
This guide helps any AI agent set up `uwf` (Uncaged Workflow) from scratch — or self-check and upgrade an existing installation.
## Prerequisites
- **bun** — `uwf` is built with bun. Install: `curl -fsSL https://bun.sh/install | bash`
- **Network access** — to install npm packages
> **Already have uwf?** Jump to [Self-Check & Upgrade](#self-check--upgrade).
---
## Fresh Install
### 1. Install uwf CLI
```bash
bun install -g @uncaged/cli-workflow
```
**Check:** `uwf --version` prints a version number (e.g. `0.5.1`).
### 2. Install Agent Adapter
Install the adapter that matches your agent runtime. Pick **one**:
| Agent | Package | Binary |
|-------|---------|--------|
| Hermes | `@uncaged/workflow-agent-hermes` | `uwf-hermes` |
```bash
# Example: Hermes agent
bun install -g @uncaged/workflow-agent-hermes
```
**Check:** `uwf-hermes --version` prints a version number.
### 3. Setup
Run the interactive wizard:
```bash
uwf setup
```
Or configure non-interactively:
```bash
uwf setup \
--provider <name> \
--base-url <url> \
--api-key <key> \
--model <model-name> \
--agent hermes
```
This creates `~/.uncaged/workflow/config.yaml` with your provider, model, and default agent.
#### Config Structure
```yaml
providers:
my-provider:
baseUrl: https://api.example.com/v1
apiKey: sk-xxx
models:
default:
provider: my-provider
name: my-model
agents:
hermes:
command: uwf-hermes
args: []
defaultAgent: hermes
defaultModel: default
```
**Check:** `cat ~/.uncaged/workflow/config.yaml` shows valid provider, model, and agent config.
### 4. Verify Installation
```bash
uwf workflow list # should return empty array or existing workflows
uwf skill user # prints usage guide
uwf skill author # prints workflow authoring guide
```
**Check:** All three commands run without errors.
### 5. Add the uwf Skill
```bash
mkdir -p ~/.hermes/skills/devops/uwf
uwf skill bootstrap > ~/.hermes/skills/devops/uwf/SKILL.md
```
**Check:** `cat ~/.hermes/skills/devops/uwf/SKILL.md` shows the skill content with triggers `uwf`, `workflow`, `工作流`.
### 6. Smoke Test
```bash
# Register an example workflow
uwf workflow add examples/analyze-topic.yaml
# Start a thread
uwf thread start analyze-topic -p "Analyze the concept of technical debt"
# Execute it (one moderator → agent → extract cycle)
uwf thread exec <thread-id>
```
**Check:** Thread reaches `completed` status. Verify with `uwf thread list`.
---
## Self-Check & Upgrade
Already have uwf installed? Run through this checklist to verify and upgrade.
### Version Check
```bash
uwf --version
uwf-hermes --version # or your agent adapter
```
Compare with latest published versions:
```bash
bun pm ls -g | grep -E "cli-workflow|workflow-agent"
npm info @uncaged/cli-workflow version
npm info @uncaged/workflow-agent-hermes version
```
If local version < published version, upgrade:
```bash
bun install -g @uncaged/cli-workflow@latest
bun install -g @uncaged/workflow-agent-hermes@latest
```
**Check:** `uwf --version` matches `npm info @uncaged/cli-workflow version`.
### Config Check
```bash
cat ~/.uncaged/workflow/config.yaml
```
Verify:
- [ ] `providers` has at least one entry with valid `baseUrl` and `apiKey`
- [ ] `models.default` references an existing provider
- [ ] `agents` has your adapter configured
- [ ] `defaultAgent` and `defaultModel` are set
### Skill Check
```bash
cat ~/.hermes/skills/devops/uwf/SKILL.md
```
Verify the skill is up to date:
```bash
uwf skill bootstrap | diff - ~/.hermes/skills/devops/uwf/SKILL.md
```
If `diff` produces any output, the local skill is outdated. Update:
```bash
uwf skill bootstrap > ~/.hermes/skills/devops/uwf/SKILL.md
```
### Functional Check
```bash
uwf workflow list # should not error
uwf skill user # should print usage guide
uwf skill author # should print authoring guide
```
✅ All green? You're good to go.
+1 -1
View File
@@ -270,7 +270,7 @@ node scripts/publish-all.mjs --dry-run # preview without publishing
examples/solve-issue.yaml — write a workflow YAML definition
│ uwf workflow put
~/.uncaged/json-cas/ — Workflow stored as CAS node (unified CAS store)
~/.uncaged/workflow/cas/ — Workflow stored as CAS node
~/.uncaged/workflow/registry.yaml — name → hash mapping updated
│ uwf thread start <name> -p "..."
+1 -2
View File
@@ -39,8 +39,7 @@
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off",
"noConsole": "off"
"noExplicitAny": "off"
},
"style": {
"noNonNullAssertion": "off"
+1 -7
View File
@@ -1,14 +1,11 @@
{
"name": "@uncaged/workflow-monorepo",
"private": true,
"packageManager": "bun@1.3.14",
"workspaces": [
"packages/*"
],
"scripts": {
"uwf": "bun packages/cli-workflow/src/cli.ts",
"preinstall": "npx only-allow bun",
"prepublishOnly": "echo 'Use bun run release instead' && exit 1",
"build": "bunx tsc --build",
"check": "bunx tsc --build && biome check . && bash scripts/lint-log-tags.sh",
"typecheck": "bunx tsc --build",
@@ -26,10 +23,7 @@
"@types/node": "^25.7.0",
"@types/xxhashjs": "^0.2.4",
"@uncaged/workflow-agent-hermes": "workspace:*",
"bun-types": "^1.3.13",
"typescript": "^5.8.3",
"vitest": "^4.1.7",
"yaml": "^2.9.0"
"bun-types": "^1.3.13"
},
"repository": {
"type": "git",
+1 -10
View File
@@ -209,13 +209,4 @@ src/
| `~/.uncaged/workflow/.env` | API keys (referenced by `apiKeyEnv` in config) |
| `~/.uncaged/workflow/registry.yaml` | Workflow name → CAS hash |
| `~/.uncaged/workflow/threads.yaml` | Active thread head pointers |
| `~/.uncaged/json-cas/` | Content-addressed node storage (unified CAS store, shared with `json-cas` CLI) |
### Environment Variables
| Variable | Purpose | Default |
|----------|---------|---------|
| `UNCAGED_CAS_DIR` | Override the global CAS directory location | `~/.uncaged/json-cas` |
| `UNCAGED_WORKFLOW_STORAGE_ROOT` | Internal override for workflow metadata storage | `~/.uncaged/workflow` |
| `WORKFLOW_STORAGE_ROOT` | User override for workflow metadata storage | `~/.uncaged/workflow` |
| `~/.uncaged/workflow/cas/` | Content-addressed node storage |
+3 -4
View File
@@ -22,7 +22,6 @@
"yaml": "^2.8.4"
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "vitest run",
"test:ci": "vitest run"
},
@@ -35,12 +34,12 @@
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"url": "https://github.com/shazhou-ww/uncaged-workflow.git",
"directory": "packages/cli-workflow"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"homepage": "https://github.com/shazhou-ww/uncaged-workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"license": "MIT"
}
@@ -129,11 +129,7 @@ describe("C1: adapter JSON round-trip integration", () => {
{
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
WORKFLOW_STORAGE_ROOT: tmpDir,
UNCAGED_CAS_DIR: casDir,
},
env: { ...process.env, WORKFLOW_STORAGE_ROOT: tmpDir },
cwd: tmpDir,
timeout: 30000,
},
@@ -6,22 +6,14 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { cmdCasPutText } from "../commands/cas.js";
let storageRoot: string;
let casDir: string;
let uwfPath: string;
let originalEnv: string | undefined;
beforeEach(async () => {
storageRoot = join(
tmpdir(),
`uwf-cas-exit-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
casDir = join(storageRoot, "cas");
await mkdir(storageRoot, { recursive: true });
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR for this test
originalEnv = process.env.UNCAGED_CAS_DIR;
process.env.UNCAGED_CAS_DIR = casDir;
// Find the uwf CLI path
uwfPath = join(__dirname, "../../src/cli.ts");
@@ -29,13 +21,6 @@ beforeEach(async () => {
afterEach(async () => {
await rm(storageRoot, { recursive: true, force: true });
// Restore original environment
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
});
type ExecResult = {
@@ -47,11 +32,7 @@ type ExecResult = {
function execUwf(args: string[]): ExecResult {
try {
const stdout = execSync(`bun ${uwfPath} ${args.join(" ")}`, {
env: {
...process.env,
WORKFLOW_STORAGE_ROOT: storageRoot,
UNCAGED_CAS_DIR: casDir,
},
env: { ...process.env, WORKFLOW_STORAGE_ROOT: storageRoot },
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
@@ -206,8 +206,6 @@ async function insertStepNode(
describe("currentRole field", () => {
let tmpDir: string;
let storageRoot: string;
let casDir: string;
let originalEnv: string | undefined;
async function setup() {
tmpDir = join(
@@ -215,25 +213,13 @@ describe("currentRole field", () => {
`uwf-test-current-role-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
storageRoot = join(tmpDir, "storage");
casDir = join(tmpDir, "cas");
await mkdir(storageRoot, { recursive: true });
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR for this test
originalEnv = process.env.UNCAGED_CAS_DIR;
process.env.UNCAGED_CAS_DIR = casDir;
}
async function teardown() {
if (tmpDir) {
await rm(tmpDir, { recursive: true, force: true });
}
// Restore original environment
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
}
// T1: idle at start — currentRole = first role from graph
@@ -6,17 +6,27 @@ import { describe, expect, test } from "vitest";
const __dirname = dirname(fileURLToPath(import.meta.url));
import {
cmdSkillActor,
cmdSkillAdapter,
cmdSkillArchitecture,
cmdSkillAuthor,
cmdSkillCli,
cmdSkillDeveloper,
cmdSkillList,
cmdSkillModerator,
cmdSkillUser,
cmdSkillYaml,
} from "../commands/skill.js";
describe("skill commands", () => {
test("skill list returns all skill names", () => {
const result = cmdSkillList();
expect(result).toBeInstanceOf(Array);
expect(result).toContain("cli");
expect(result).toContain("architecture");
expect(result).toContain("yaml");
expect(result).toContain("moderator");
expect(result).toContain("actor");
expect(result).toContain("user");
expect(result).toContain("author");
expect(result).toContain("developer");
@@ -26,6 +36,50 @@ describe("skill commands", () => {
}
});
test("skill architecture returns non-empty markdown string", () => {
const result = cmdSkillArchitecture();
expect(typeof result).toBe("string");
expect(result).toContain("CAS");
expect(result).toContain("Thread");
expect(result).toContain("Workflow");
expect(result).toContain("Step");
expect(result.length).toBeGreaterThan(200);
});
test("skill yaml returns non-empty markdown string", () => {
const result = cmdSkillYaml();
expect(typeof result).toBe("string");
expect(result).toContain("roles");
expect(result).toContain("graph");
expect(result).toContain("frontmatter");
expect(result.length).toBeGreaterThan(200);
});
test("skill moderator returns non-empty markdown string", () => {
const result = cmdSkillModerator();
expect(typeof result).toBe("string");
expect(result).toContain("routing");
expect(result).toContain("status");
expect(result.length).toBeGreaterThan(200);
// Check for edge or graph
expect(result).toMatch(/edge|graph/i);
});
test("skill cli returns CLI reference markdown", () => {
const result = cmdSkillCli();
expect(typeof result).toBe("string");
expect(result).toContain("uwf");
});
test("skill actor returns non-empty markdown string", () => {
const result = cmdSkillActor();
expect(typeof result).toBe("string");
expect(result).toContain("frontmatter");
expect(result).toContain("CAS");
expect(result).toContain("status");
expect(result.length).toBeGreaterThan(200);
});
test("skill user returns non-empty markdown string", () => {
const result = cmdSkillUser();
expect(typeof result).toBe("string");
@@ -72,6 +126,11 @@ describe("skill commands", () => {
env: { ...process.env, PATH: `/opt/homebrew/bin:${process.env.PATH}` },
});
expect(output).not.toMatch(/help\s+\[command\]/i);
expect(output).toContain("cli");
expect(output).toContain("architecture");
expect(output).toContain("yaml");
expect(output).toContain("moderator");
expect(output).toContain("actor");
expect(output).toContain("user");
expect(output).toContain("author");
expect(output).toContain("developer");
@@ -98,7 +98,7 @@ describe("solve-issue workflow: tea pr create worktree fix", () => {
expect(frontmatter).toBeDefined();
expect(frontmatter?.oneOf).toBeDefined();
const committedVariant = frontmatter.oneOf.find(
(v: any) => v.properties?.$status?.const === "committed",
(v: any) => v.properties?.["$status"]?.const === "committed",
);
expect(committedVariant).toBeDefined();
expect(committedVariant.required).toContain("$status");
@@ -56,7 +56,7 @@ const VALID_OUTPUT: AdapterOutput = {
describe("spawnAgent JSON parsing", () => {
test("B1. parses valid JSON from agent stdout", () => {
const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
const stdout = JSON.stringify(VALID_OUTPUT) + "\n";
const result = parseAgentStdout(stdout);
expect(result.stepHash).toBe("0123456789ABC");
expect(result.detailHash).toBe("DEFGH12345678");
@@ -68,7 +68,7 @@ describe("spawnAgent JSON parsing", () => {
});
test("B2. extracts stepHash for head pointer", () => {
const stdout = `${JSON.stringify(VALID_OUTPUT)}\n`;
const stdout = JSON.stringify(VALID_OUTPUT) + "\n";
const result = parseAgentStdout(stdout);
expect(result.stepHash).toBe("0123456789ABC");
expect(isCasRef(result.stepHash)).toBe(true);
@@ -76,7 +76,7 @@ describe("spawnAgent JSON parsing", () => {
test("B3. handles debug lines before JSON", () => {
const debugLines = "[debug] loading context...\n[debug] running agent...\n";
const stdout = `${debugLines + JSON.stringify(VALID_OUTPUT)}\n`;
const stdout = debugLines + JSON.stringify(VALID_OUTPUT) + "\n";
const result = parseAgentStdout(stdout);
expect(result.stepHash).toBe("0123456789ABC");
});
@@ -88,13 +88,13 @@ describe("spawnAgent JSON parsing", () => {
test("B5. rejects JSON missing stepHash", () => {
const incomplete = { detailHash: "DEFGH12345678", role: "planner" };
const stdout = `${JSON.stringify(incomplete)}\n`;
const stdout = JSON.stringify(incomplete) + "\n";
expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
});
test("B6. rejects JSON with invalid stepHash", () => {
const bad = { ...VALID_OUTPUT, stepHash: "not-a-hash" };
const stdout = `${JSON.stringify(bad)}\n`;
const stdout = JSON.stringify(bad) + "\n";
expect(() => parseAgentStdout(stdout)).toThrow("missing valid stepHash");
});
});
@@ -66,21 +66,13 @@ function generateContent(size: number, prefix = "Content"): string {
// ── fixture ───────────────────────────────────────────────────────────────────
let tmpDir: string;
let originalEnv: string | undefined;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-read-test-"));
originalEnv = process.env.UNCAGED_CAS_DIR;
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
// Restore original environment
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
});
// ── step read tests ───────────────────────────────────────────────────────────
@@ -88,10 +80,7 @@ afterEach(async () => {
describe("step read", () => {
test("test 1: basic single-step read with 3 turns", async () => {
const casDir = join(tmpDir, "cas");
process.env.UNCAGED_CAS_DIR = casDir;
await mkdir(casDir, { recursive: true });
process.env.UNCAGED_CAS_DIR = casDir;
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store);
@@ -157,11 +146,10 @@ describe("step read", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
// Read step with large quota
const markdown = await cmdStepRead(tmpDir, stepHash, 10000, false);
const markdown = await cmdStepRead(tmpDir, stepHash, 10000);
// Assert structure
expect(markdown).toContain(`# Step ${stepHash}`);
@@ -177,9 +165,7 @@ describe("step read", () => {
test("test 2: quota enforcement - multiple turns", async () => {
const casDir = join(tmpDir, "cas");
process.env.UNCAGED_CAS_DIR = casDir;
await mkdir(casDir, { recursive: true });
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store);
@@ -245,11 +231,10 @@ describe("step read", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
// Read step with limited quota (700 chars)
const markdown = await cmdStepRead(tmpDir, stepHash, 700, false);
const markdown = await cmdStepRead(tmpDir, stepHash, 700);
// Assert only most recent turns fit
expect(markdown).toContain(`# Step ${stepHash}`);
@@ -263,9 +248,7 @@ describe("step read", () => {
test("test 3: minimal quota edge case - always show at least one turn", async () => {
const casDir = join(tmpDir, "cas");
process.env.UNCAGED_CAS_DIR = casDir;
await mkdir(casDir, { recursive: true });
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store);
@@ -327,11 +310,10 @@ describe("step read", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
// Read step with minimal quota (1 char)
const markdown = await cmdStepRead(tmpDir, stepHash, 1, false);
const markdown = await cmdStepRead(tmpDir, stepHash, 1);
// Assert at least one turn is always shown
expect(markdown).toContain("LongTurn");
@@ -340,9 +322,7 @@ describe("step read", () => {
test("test 4: step with no detail field", async () => {
const casDir = join(tmpDir, "cas");
process.env.UNCAGED_CAS_DIR = casDir;
await mkdir(casDir, { recursive: true });
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
@@ -385,11 +365,10 @@ describe("step read", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
// Read step - should return metadata only (no error)
const markdown = await cmdStepRead(tmpDir, stepHash, 4000, false);
const markdown = await cmdStepRead(tmpDir, stepHash, 4000);
// Assert metadata is present
expect(markdown).toContain(`# Step ${stepHash}`);
@@ -401,9 +380,7 @@ describe("step read", () => {
test("test 5: step with detail but no turns array", async () => {
const casDir = join(tmpDir, "cas");
process.env.UNCAGED_CAS_DIR = casDir;
await mkdir(casDir, { recursive: true });
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
await registerDetailSchemas(store);
@@ -464,11 +441,10 @@ describe("step read", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
// Read step - should return metadata only (no error)
const markdown = await cmdStepRead(tmpDir, stepHash, 4000, false);
const markdown = await cmdStepRead(tmpDir, stepHash, 4000);
// Assert metadata is present
expect(markdown).toContain(`# Step ${stepHash}`);
@@ -479,9 +455,7 @@ describe("step read", () => {
test("test 6: displays role and tool calls in turn body", async () => {
const casDir = join(tmpDir, "cas");
process.env.UNCAGED_CAS_DIR = casDir;
await mkdir(casDir, { recursive: true });
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store);
@@ -541,10 +515,9 @@ describe("step read", () => {
agent: "uwf-hermes",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const markdown = await cmdStepRead(tmpDir, stepHash, 4000, false);
const markdown = await cmdStepRead(tmpDir, stepHash, 4000);
expect(markdown).toContain("**Turn role:** assistant");
expect(markdown).toContain("**terminal**");
@@ -553,9 +526,7 @@ describe("step read", () => {
test("test 7: turn content with special characters", async () => {
const casDir = join(tmpDir, "cas");
process.env.UNCAGED_CAS_DIR = casDir;
await mkdir(casDir, { recursive: true });
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store);
@@ -617,11 +588,10 @@ describe("step read", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
// Read step
const markdown = await cmdStepRead(tmpDir, stepHash, 4000, false);
const markdown = await cmdStepRead(tmpDir, stepHash, 4000);
// Assert content is rendered correctly without corruption
expect(markdown).toContain("`backticks`");
@@ -116,7 +116,6 @@ async function createTestStep(
edgePrompt: "",
startedAtMs: Date.now(),
completedAtMs: Date.now() + 1000,
assembledPrompt: null,
cwd: "/tmp",
};
return store.put(schemas.stepNode, stepPayload);
@@ -125,23 +124,15 @@ async function createTestStep(
describe("cmdStepShow JSON serialization", () => {
let testDir: string;
let casDir: string;
let originalEnv: string | undefined;
beforeEach(async () => {
testDir = await mkdtemp(join(tmpdir(), "uwf-test-"));
casDir = join(testDir, "cas");
await mkdir(casDir, { recursive: true });
originalEnv = process.env.UNCAGED_CAS_DIR;
process.env.UNCAGED_CAS_DIR = casDir;
});
afterEach(async () => {
await rm(testDir, { recursive: true, force: true });
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
});
test("escapes newlines in tool call args", async () => {
@@ -63,22 +63,13 @@ async function registerDetailSchemas(store: ReturnType<typeof createFsStore>) {
// ── fixture ──────────────────────────────────────────────────────────────────
let tmpDir: string;
let originalEnv: string | undefined;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-timing-test-"));
originalEnv = process.env.UNCAGED_CAS_DIR;
process.env.UNCAGED_CAS_DIR = join(tmpDir, "cas");
await mkdir(process.env.UNCAGED_CAS_DIR, { recursive: true });
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
});
// ── 1. Protocol types (compile-time) ─────────────────────────────────────────
@@ -94,7 +85,6 @@ describe("protocol types", () => {
edgePrompt: "",
startedAtMs: 1000,
completedAtMs: 2000,
assembledPrompt: null,
cwd: "/test/path",
};
expect(record.startedAtMs).toBe(1000);
@@ -163,7 +153,6 @@ describe("StepNode JSON schema", () => {
edgePrompt: "",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
expect(hash).toBeTruthy();
});
@@ -1,224 +0,0 @@
import { mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { createUwfStore, getCasDir, getGlobalCasDir } from "../store.js";
describe("Global CAS directory", () => {
let tmpDir: string;
let originalEnv: string | undefined;
beforeEach(async () => {
tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`);
await mkdir(tmpDir, { recursive: true });
originalEnv = process.env.UNCAGED_CAS_DIR;
});
afterEach(async () => {
if (tmpDir) {
await rm(tmpDir, { recursive: true, force: true });
}
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
});
test("getGlobalCasDir returns default path when no env var set", () => {
delete process.env.UNCAGED_CAS_DIR;
const casDir = getGlobalCasDir();
// Should return ~/.uncaged/json-cas
expect(casDir).toContain(".uncaged");
expect(casDir).toContain("json-cas");
});
test("getGlobalCasDir respects UNCAGED_CAS_DIR environment variable", () => {
const customPath = join(tmpDir, "custom-cas");
process.env.UNCAGED_CAS_DIR = customPath;
const casDir = getGlobalCasDir();
expect(casDir).toBe(customPath);
});
test("getGlobalCasDir ignores empty UNCAGED_CAS_DIR", () => {
process.env.UNCAGED_CAS_DIR = "";
const casDir = getGlobalCasDir();
expect(casDir).toContain(".uncaged");
expect(casDir).toContain("json-cas");
});
test("getCasDir is deprecated but still works for backward compatibility", () => {
const storageRoot = join(tmpDir, "storage");
const casDir = getCasDir(storageRoot);
expect(casDir).toBe(join(storageRoot, "cas"));
});
test("createUwfStore uses global CAS directory", async () => {
const globalCasDir = join(tmpDir, "global-cas");
process.env.UNCAGED_CAS_DIR = globalCasDir;
const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
const uwf = await createUwfStore(storageRoot);
// Verify the store was created in the global CAS directory
expect(uwf.storageRoot).toBe(storageRoot);
expect(uwf.store).toBeDefined();
expect(uwf.schemas).toBeDefined();
// The global CAS directory should be created
const { stat } = await import("node:fs/promises");
const stats = await stat(globalCasDir);
expect(stats.isDirectory()).toBe(true);
});
test("createUwfStore creates global CAS directory if it does not exist", async () => {
const globalCasDir = join(tmpDir, "new-global-cas");
process.env.UNCAGED_CAS_DIR = globalCasDir;
const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
await createUwfStore(storageRoot);
// Verify the directory was created
const { stat } = await import("node:fs/promises");
const stats = await stat(globalCasDir);
expect(stats.isDirectory()).toBe(true);
});
test("multiple uwfStore instances share the same global CAS filesystem", async () => {
const globalCasDir = join(tmpDir, "shared-cas");
process.env.UNCAGED_CAS_DIR = globalCasDir;
const storageRoot1 = join(tmpDir, "storage1");
const storageRoot2 = join(tmpDir, "storage2");
await mkdir(storageRoot1, { recursive: true });
await mkdir(storageRoot2, { recursive: true });
const uwf1 = await createUwfStore(storageRoot1);
const uwf2 = await createUwfStore(storageRoot2);
// Both should use the same global CAS directory
expect(uwf1.store).toBeDefined();
expect(uwf2.store).toBeDefined();
// Store a node in the first store
const testData = { test: "data" };
const _hash = uwf1.store.put(uwf1.schemas.text, JSON.stringify(testData));
// Both stores share the same CAS filesystem directory
// Since schemas are registered idempotently, they should have the same hash
expect(uwf2.schemas.text).toBe(uwf1.schemas.text);
// Verify the CAS files are written to the shared directory
const { readdir } = await import("node:fs/promises");
const files = await readdir(globalCasDir);
expect(files.length).toBeGreaterThan(0);
});
test("workflow metadata remains in storageRoot, not global CAS", async () => {
const globalCasDir = join(tmpDir, "global-cas");
process.env.UNCAGED_CAS_DIR = globalCasDir;
const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
const _uwf = await createUwfStore(storageRoot);
// Write workflow registry file
const { saveWorkflowRegistry } = await import("../store.js");
await saveWorkflowRegistry(storageRoot, { "test-workflow": "ABC123" });
// Verify registry is in storageRoot, not global CAS
const { readFile } = await import("node:fs/promises");
const registryPath = join(storageRoot, "workflows.yaml");
const content = await readFile(registryPath, "utf8");
expect(content).toContain("test-workflow");
expect(content).toContain("ABC123");
// Verify registry is NOT in global CAS directory
const globalRegistryPath = join(globalCasDir, "workflows.yaml");
await expect(readFile(globalRegistryPath, "utf8")).rejects.toThrow();
});
test("thread metadata remains in storageRoot", async () => {
const globalCasDir = join(tmpDir, "global-cas");
process.env.UNCAGED_CAS_DIR = globalCasDir;
const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
await createUwfStore(storageRoot);
// Write threads index
const { saveThreadsIndex } = await import("../store.js");
await saveThreadsIndex(storageRoot, { "thread-123": "hash-456" });
// Verify threads.yaml is in storageRoot, not global CAS
const { readFile } = await import("node:fs/promises");
const threadsPath = join(storageRoot, "threads.yaml");
const content = await readFile(threadsPath, "utf8");
expect(content).toContain("thread-123");
expect(content).toContain("hash-456");
// Verify threads.yaml is NOT in global CAS directory
const globalThreadsPath = join(globalCasDir, "threads.yaml");
await expect(readFile(globalThreadsPath, "utf8")).rejects.toThrow();
});
test("history remains in storageRoot", async () => {
const globalCasDir = join(tmpDir, "global-cas");
process.env.UNCAGED_CAS_DIR = globalCasDir;
const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
await createUwfStore(storageRoot);
// Write history
const { appendThreadHistory } = await import("../store.js");
await appendThreadHistory(storageRoot, {
thread: "thread-123" as any,
workflow: "workflow-456",
head: "hash-789",
completedAt: Date.now(),
reason: "completed",
});
// Verify history.jsonl is in storageRoot, not global CAS
const { readFile } = await import("node:fs/promises");
const historyPath = join(storageRoot, "history.jsonl");
const content = await readFile(historyPath, "utf8");
expect(content).toContain("thread-123");
expect(content).toContain("workflow-456");
// Verify history.jsonl is NOT in global CAS directory
const globalHistoryPath = join(globalCasDir, "history.jsonl");
await expect(readFile(globalHistoryPath, "utf8")).rejects.toThrow();
});
test("CAS nodes are stored in global directory", async () => {
const globalCasDir = join(tmpDir, "global-cas");
process.env.UNCAGED_CAS_DIR = globalCasDir;
const storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
const uwf = await createUwfStore(storageRoot);
// Store a CAS node
const testPayload = JSON.stringify({ test: "node" });
const _hash = uwf.store.put(uwf.schemas.text, testPayload);
// Verify the node is in global CAS directory
const { readdir } = await import("node:fs/promises");
const files = await readdir(globalCasDir);
expect(files.length).toBeGreaterThan(0);
// Verify the node is NOT in the old storageRoot/cas location
const oldCasDir = join(storageRoot, "cas");
await expect(readdir(oldCasDir)).rejects.toThrow();
});
});
@@ -15,8 +15,6 @@ import { appendThreadHistory, createUwfStore, saveThreadsIndex } from "../store.
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR to use the test's CAS directory
process.env.UNCAGED_CAS_DIR = casDir;
return createUwfStore(storageRoot);
}
@@ -9,31 +9,17 @@ import { createUwfStore } from "../store.js";
describe("Thread and edge location integration", () => {
let tmpDir: string;
let storageRoot: string;
let casDir: string;
let originalEnv: string | undefined;
async function setupTestEnv() {
tmpDir = join(tmpdir(), `uwf-test-location-${Date.now()}`);
storageRoot = join(tmpDir, "storage");
casDir = join(tmpDir, "cas");
await mkdir(storageRoot, { recursive: true });
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR for this test
originalEnv = process.env.UNCAGED_CAS_DIR;
process.env.UNCAGED_CAS_DIR = casDir;
}
async function teardown() {
if (tmpDir) {
await rm(tmpDir, { recursive: true, force: true });
}
// Restore original environment
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
}
test("thread start captures cwd in StartNode", async () => {
@@ -67,22 +67,13 @@ function generateContent(size: number, prefix = "Content"): string {
// ── fixture ───────────────────────────────────────────────────────────────────
let tmpDir: string;
let originalEnv: string | undefined;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-quota-test-"));
originalEnv = process.env.UNCAGED_CAS_DIR;
process.env.UNCAGED_CAS_DIR = join(tmpDir, "cas");
await mkdir(process.env.UNCAGED_CAS_DIR, { recursive: true });
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
});
// ── thread read quota enforcement ─────────────────────────────────────────────
@@ -152,7 +143,6 @@ describe("thread read --quota flag", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
steps.push(stepHash);
}
@@ -235,7 +225,6 @@ describe("thread read --quota flag", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const step2Content = generateContent(600, "Second");
@@ -262,7 +251,6 @@ describe("thread read --quota flag", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01HX2Q3R4S5T6V7W8X9YZ1" as ThreadId;
@@ -348,7 +336,6 @@ describe("thread read --quota flag", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
steps.push(stepHash);
}
@@ -428,7 +415,6 @@ describe("thread read --quota flag", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01HX2Q3R4S5T6V7W8X9YZ4" as ThreadId;
@@ -506,7 +492,6 @@ describe("thread read --quota flag", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
steps.push(stepHash);
}
@@ -588,7 +573,6 @@ describe("thread read --quota flag", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
steps.push(stepHash);
}
@@ -53,8 +53,6 @@ const DETAIL_SCHEMA = {
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR to use the test's CAS directory
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
return { storageRoot, store, schemas };
@@ -143,7 +141,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-claude-code",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000001" as ThreadId;
@@ -221,7 +218,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-claude-code",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000002" as ThreadId;
@@ -284,7 +280,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const step2 = await uwf.store.put(uwf.schemas.stepNode, {
@@ -296,7 +291,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000003" as ThreadId;
@@ -351,7 +345,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000004" as ThreadId;
@@ -406,7 +399,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000005" as ThreadId;
@@ -461,7 +453,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000006" as ThreadId;
@@ -536,7 +527,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const step2 = await uwf.store.put(uwf.schemas.stepNode, {
@@ -548,7 +538,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const step3 = await uwf.store.put(uwf.schemas.stepNode, {
@@ -560,7 +549,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000007" as ThreadId;
@@ -641,7 +629,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
});
const threadId = "01JTEST0000000000000008" as ThreadId;
@@ -698,7 +685,6 @@ describe("thread read XML tag isolation", () => {
agent: "uwf-test",
startedAtMs: 1000000000000,
completedAtMs: 1000000005000,
assembledPrompt: null,
})) as CasRef;
steps.push(step);
prev = step;
@@ -10,31 +10,17 @@ import { createUwfStore, loadThreadsIndex } from "../store.js";
describe("thread start --cwd CLI option", () => {
let tmpDir: string;
let storageRoot: string;
let casDir: string;
let originalEnv: string | undefined;
async function setupTestEnv() {
tmpDir = join(tmpdir(), `uwf-test-cwd-cli-${Date.now()}`);
storageRoot = join(tmpDir, "storage");
casDir = join(tmpDir, "cas");
await mkdir(storageRoot, { recursive: true });
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR for this test
originalEnv = process.env.UNCAGED_CAS_DIR;
process.env.UNCAGED_CAS_DIR = casDir;
}
async function teardown() {
if (tmpDir) {
await rm(tmpDir, { recursive: true, force: true });
}
// Restore original environment
if (originalEnv === undefined) {
delete process.env.UNCAGED_CAS_DIR;
} else {
process.env.UNCAGED_CAS_DIR = originalEnv;
}
}
async function createTestWorkflow(): Promise<string> {
@@ -137,7 +123,7 @@ graph:
// Register the workflow
execFileSync("node", [uwfBin, "workflow", "add", workflowPath], {
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, UNCAGED_CAS_DIR: casDir },
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot },
encoding: "utf8",
});
@@ -146,7 +132,7 @@ graph:
"node",
[uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd],
{
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, UNCAGED_CAS_DIR: casDir },
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot },
encoding: "utf8",
},
);
@@ -58,8 +58,6 @@ const DETAIL_SCHEMA = {
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR to use the test's CAS directory
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
return { storageRoot, store, schemas };
@@ -15,8 +15,6 @@ import { loadWorkflowRegistry, saveWorkflowRegistry } from "../store.js";
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true });
// Set UNCAGED_CAS_DIR to use the test's CAS directory
process.env.UNCAGED_CAS_DIR = casDir;
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
return { storageRoot, store, schemas };
+42 -16
View File
@@ -17,12 +17,16 @@ import { cmdConfigGet, cmdConfigList, cmdConfigSet } from "./commands/config.js"
import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js";
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
import {
cmdSkillActor,
cmdSkillAdapter,
cmdSkillArchitecture,
cmdSkillAuthor,
cmdSkillBootstrap,
cmdSkillCli,
cmdSkillDeveloper,
cmdSkillList,
cmdSkillModerator,
cmdSkillUser,
cmdSkillYaml,
} from "./commands/skill.js";
import { cmdStepFork, cmdStepList, cmdStepRead, cmdStepShow } from "./commands/step.js";
import {
@@ -364,8 +368,7 @@ step
.description("Read a step's turns as human-readable markdown")
.argument("<step-hash>", "CAS hash of the StepNode")
.option("--quota <chars>", "Max output characters", "4000")
.option("--prompt", "Show the assembled prompt sent to the agent instead of turns")
.action((stepHash: string, opts: { quota: string; prompt: boolean }) => {
.action((stepHash: string, opts: { quota: string }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
const quota = Number.parseInt(opts.quota, 10);
@@ -373,12 +376,7 @@ step
process.stderr.write("invalid --quota: must be a positive integer\n");
process.exit(1);
}
const markdown = await cmdStepRead(
storageRoot,
stepHash as CasRef,
quota,
opts.prompt === true,
);
const markdown = await cmdStepRead(storageRoot, stepHash as CasRef, quota);
process.stdout.write(markdown.endsWith("\n") ? markdown : `${markdown}\n`);
});
});
@@ -495,6 +493,34 @@ For more information, see: uwf help thread list
const skill = program.command("skill").description("Built-in skill references for agents");
skill.addHelpCommand(false);
skill
.command("cli")
.description("Print a markdown reference of all uwf commands")
.action(() => {
console.log(cmdSkillCli());
});
skill
.command("architecture")
.description("Print the architecture reference")
.action(() => {
console.log(cmdSkillArchitecture());
});
skill
.command("yaml")
.description("Print the workflow YAML schema reference")
.action(() => {
console.log(cmdSkillYaml());
});
skill
.command("actor")
.description("Print the actor reference (frontmatter protocol + CAS)")
.action(() => {
console.log(cmdSkillActor());
});
skill
.command("adapter")
.description("Print the adapter reference (building agent adapters)")
@@ -516,6 +542,13 @@ skill
console.log(cmdSkillDeveloper());
});
skill
.command("moderator")
.description("Print the moderator reference")
.action(() => {
console.log(cmdSkillModerator());
});
skill
.command("user")
.description("Print the user reference (CLI guide + typical workflows)")
@@ -523,13 +556,6 @@ skill
console.log(cmdSkillUser());
});
skill
.command("bootstrap")
.description("Print the bootstrap skill YAML for Hermes agents")
.action(() => {
console.log(cmdSkillBootstrap());
});
skill
.command("list")
.description("List all available skill names")
+16 -2
View File
@@ -1,12 +1,26 @@
export {
generateActorReference as cmdSkillActor,
generateAdapterReference as cmdSkillAdapter,
generateArchitectureReference as cmdSkillArchitecture,
generateAuthorReference as cmdSkillAuthor,
generateBootstrapReference as cmdSkillBootstrap,
generateCliReference as cmdSkillCli,
generateDeveloperReference as cmdSkillDeveloper,
generateModeratorReference as cmdSkillModerator,
generateUserReference as cmdSkillUser,
generateYamlReference as cmdSkillYaml,
} from "@uncaged/workflow-util";
const SKILL_NAMES = ["user", "author", "developer", "adapter", "bootstrap"] as const;
const SKILL_NAMES = [
"cli",
"architecture",
"yaml",
"moderator",
"actor",
"user",
"author",
"developer",
"adapter",
] as const;
export function cmdSkillList(): ReadonlyArray<string> {
return [...SKILL_NAMES];
@@ -289,7 +289,6 @@ export async function cmdStepRead(
storageRoot: string,
stepHash: CasRef,
quota: number,
showPrompt: boolean,
): Promise<string> {
const uwf = await createUwfStore(storageRoot);
const node = uwf.store.get(stepHash);
@@ -301,23 +300,6 @@ export async function cmdStepRead(
}
const payload = node.payload as StepNodePayload;
// --prompt mode: show the assembled prompt that was sent to the agent
if (showPrompt) {
const promptRef = (payload as Record<string, unknown>).assembledPrompt;
if (typeof promptRef !== "string") {
return `# Step ${stepHash}\n\n_Prompt not recorded (legacy step)._`;
}
const promptNode = uwf.store.get(promptRef as CasRef);
if (promptNode === null) {
return `# Step ${stepHash}\n\n_Prompt CAS node not found: ${promptRef}_`;
}
const promptText =
typeof promptNode.payload === "string"
? promptNode.payload
: JSON.stringify(promptNode.payload);
return `# Step ${stepHash}\n\n**Role:** ${payload.role}\n**Agent:** ${payload.agent}\n\n## Prompt\n\n${promptText}`;
}
if (payload.detail === null) {
return formatStepMarkdown(stepHash, payload.role, payload.agent, [], []);
}
+1 -17
View File
@@ -70,26 +70,10 @@ export function resolveStorageRoot(): string {
return getDefaultStorageRoot();
}
/**
* Deprecated: Use `getGlobalCasDir()` instead.
* Returns the old CAS directory for backward compatibility.
*/
export function getCasDir(storageRoot: string): string {
return join(storageRoot, "cas");
}
/**
* Returns the global CAS directory shared by all uwf and json-cas tools.
* Priority: UNCAGED_CAS_DIR environment variable → default ~/.uncaged/json-cas
*/
export function getGlobalCasDir(): string {
const envPath = process.env.UNCAGED_CAS_DIR;
if (envPath !== undefined && envPath !== "") {
return envPath;
}
return join(homedir(), ".uncaged", "json-cas");
}
export function getRegistryPath(storageRoot: string): string {
return join(storageRoot, "workflows.yaml");
}
@@ -114,7 +98,7 @@ export type UwfStore = {
};
export async function createUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = getGlobalCasDir();
const casDir = getCasDir(storageRoot);
await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
-1
View File
@@ -3,6 +3,5 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
passWithNoTests: true,
},
});
+5 -6
View File
@@ -18,9 +18,8 @@
}
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "vitest run",
"test:ci": "vitest run"
"test": "bun test",
"test:ci": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
@@ -35,12 +34,12 @@
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"url": "https://github.com/shazhou-ww/uncaged-workflow.git",
"directory": "packages/workflow-agent-builtin"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"homepage": "https://github.com/shazhou-ww/uncaged-workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"license": "MIT"
}
+2 -7
View File
@@ -82,7 +82,7 @@ async function runBuiltinWithMessages(
if (loopResult.turnCount === 0) {
log("5RWTK9NB", "no turns produced, returning empty output");
return { output: "", detailHash: "", sessionId: session.sessionId, assembledPrompt: "" };
return { output: "", detailHash: "", sessionId: session.sessionId };
}
// Read jsonl → persist turns to CAS → store detail
@@ -94,12 +94,7 @@ async function runBuiltinWithMessages(
session.startedAtMs,
);
return {
output: stripPreamble(loopResult.finalText),
detailHash,
sessionId: session.sessionId,
assembledPrompt: "",
};
return { output: stripPreamble(loopResult.finalText), detailHash, sessionId: session.sessionId };
}
async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
passWithNoTests: true,
},
});
@@ -18,9 +18,8 @@
}
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "vitest run",
"test:ci": "vitest run"
"test": "bun test",
"test:ci": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
@@ -35,12 +34,12 @@
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"url": "https://github.com/shazhou-ww/uncaged-workflow.git",
"directory": "packages/workflow-agent-claude-code"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"homepage": "https://github.com/shazhou-ww/uncaged-workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"license": "MIT"
}
@@ -120,16 +120,12 @@ function spawnClaudeResume(
return spawnClaude(args);
}
async function processClaudeOutput(
stdout: string,
store: Store,
assembledPrompt: string,
): Promise<AgentRunResult> {
async function processClaudeOutput(stdout: string, store: Store): Promise<AgentRunResult> {
const parsed = parseClaudeCodeStreamOutput(stdout);
if (parsed !== null) {
const { detailHash, output, sessionId } = await storeClaudeCodeDetail(store, parsed);
return { output, detailHash, sessionId, assembledPrompt };
return { output, detailHash, sessionId };
}
throw new Error(
@@ -148,7 +144,7 @@ async function runClaudeCode(ctx: AgentContext): Promise<AgentRunResult> {
if (cachedSessionId !== null) {
try {
const { stdout } = await spawnClaudeResume(cachedSessionId, fullPrompt);
const result = await processClaudeOutput(stdout, ctx.store, fullPrompt);
const result = await processClaudeOutput(stdout, ctx.store);
if (result.sessionId !== undefined && result.sessionId !== "") {
await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId);
}
@@ -156,14 +152,16 @@ async function runClaudeCode(ctx: AgentContext): Promise<AgentRunResult> {
} catch (err) {
log(
"5VKR8N3Q",
`resume failed for session ${cachedSessionId}, falling back to fresh run: ${err}`,
"resume failed for session %s, falling back to fresh run: %s",
cachedSessionId,
err,
);
}
}
}
const { stdout } = await spawnClaudeRun(fullPrompt);
const result = await processClaudeOutput(stdout, ctx.store, fullPrompt);
const result = await processClaudeOutput(stdout, ctx.store);
if (result.sessionId !== undefined && result.sessionId !== "") {
await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId);
}
@@ -176,7 +174,7 @@ async function continueClaudeCode(
store: Store,
): Promise<AgentRunResult> {
const { stdout } = await spawnClaudeResume(sessionId, message);
return processClaudeOutput(stdout, store, "");
return processClaudeOutput(stdout, store);
}
/** Agent CLI factory: parses argv, runs Claude Code, extracts output, writes StepNode. */
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
passWithNoTests: true,
},
});
+5 -6
View File
@@ -18,9 +18,8 @@
}
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "vitest run",
"test:ci": "vitest run"
"test": "bun test",
"test:ci": "bun test __tests__/*.test.ts"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
@@ -36,12 +35,12 @@
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"url": "https://github.com/shazhou-ww/uncaged-workflow.git",
"directory": "packages/workflow-agent-hermes"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"homepage": "https://github.com/shazhou-ww/uncaged-workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"engines": {
"bun": ">= 1.0.0"
+2 -2
View File
@@ -117,7 +117,7 @@ export function createHermesAgent(): () => Promise<void> {
await setCachedSessionId(ctx.threadId, ctx.role, sessionId);
}
return { output: text, detailHash, sessionId, assembledPrompt: fullPrompt };
return { output: text, detailHash, sessionId };
}
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
@@ -148,7 +148,7 @@ export function createHermesAgent(): () => Promise<void> {
// so the agent sees the full conversation history (crucial for retries).
const { text, sessionId } = await client.prompt(message);
const { detailHash } = await storePromptResult(store, sessionId);
return { output: text, detailHash, sessionId, assembledPrompt: "" };
return { output: text, detailHash, sessionId };
}
const agentMain = createAgent({
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
passWithNoTests: true,
},
});
+1 -3
View File
@@ -5,9 +5,7 @@
"type": "module",
"scripts": {
"dev": "bun server.ts",
"build": "vite build",
"test": "vitest run",
"test:ci": "vitest run"
"build": "vite build"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
@@ -14,7 +14,7 @@ export const editNodeViewModel = define.view("editNodeView", editNodeView, (set,
function start(nodeId: string) {
const [nodes] = model.use(nodesModel);
const node = nodes.find((n) => n.id === nodeId);
if (node?.type !== "role") return;
if (!node || node.type !== "role") return;
set({ node: node as WorkNode<"role"> });
}
@@ -40,7 +40,7 @@ function traverse(
visited.add(nodeId);
const node = nodeMap.get(nodeId);
if (node?.type !== "role") return;
if (!node || node.type !== "role") return;
const roleNode = node as WorkNode<"role">;
const outEdges = outgoingEdges.get(nodeId) ?? [];
+3 -8
View File
@@ -14,11 +14,6 @@
"import": "./dist/index.js"
}
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "vitest run",
"test:ci": "vitest run"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
"@uncaged/json-cas-fs": "^0.5.3"
@@ -31,12 +26,12 @@
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"url": "https://github.com/shazhou-ww/uncaged-workflow.git",
"directory": "packages/workflow-protocol"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"homepage": "https://github.com/shazhou-ww/uncaged-workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"license": "MIT"
}
@@ -1,4 +1,4 @@
import { describe, expect, test } from "vitest";
import { describe, expect, test } from "bun:test";
import type { StartNodePayload, StepRecord, Target } from "../types.js";
describe("Protocol types for thread/edge location", () => {
@@ -25,7 +25,6 @@ describe("Protocol types for thread/edge location", () => {
edgePrompt: "Plan the implementation",
startedAtMs: Date.now(),
completedAtMs: Date.now() + 1000,
assembledPrompt: null,
cwd: "/home/user/project",
};
@@ -88,9 +88,6 @@ export const STEP_NODE_SCHEMA: JSONSchema = {
startedAtMs: { type: "integer" },
completedAtMs: { type: "integer" },
cwd: { type: "string" },
assembledPrompt: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
},
},
additionalProperties: false,
};
-2
View File
@@ -20,8 +20,6 @@ export type StepRecord = {
completedAtMs: number;
/** Working directory where the agent executed. Missing in legacy nodes → "". */
cwd: string;
/** CAS ref to the fully assembled prompt sent to the agent. null for legacy steps. */
assembledPrompt: CasRef | null;
};
// ── 4.2 Workflow 定义 ───────────────────────────────────────────────
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
passWithNoTests: true,
},
});
@@ -44,7 +44,6 @@ describe("adapter-stdout: A4 retry loop survives JSON output", () => {
body: secondAttempt!.body,
startedAtMs: 1000,
completedAtMs: 2000,
assembledPrompt: null,
};
const json = JSON.stringify(adapterOutput);
+5 -6
View File
@@ -15,9 +15,8 @@
}
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "vitest run",
"test:ci": "vitest run"
"test": "bun test",
"test:ci": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
@@ -35,12 +34,12 @@
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"url": "https://github.com/shazhou-ww/uncaged-workflow.git",
"directory": "packages/workflow-util-agent"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"homepage": "https://github.com/shazhou-ww/uncaged-workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"license": "MIT"
}
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
describe("parseArgv empty prompt error message", () => {
let stderrOutput: string;
@@ -214,7 +214,7 @@ function getConstValue(propSchema: JSONSchema): string {
function buildVariantBlock(variant: JSONSchema, discriminant: string): string {
const props = extractSchemaProperties(variant);
const value = getConstValue(
(variant.properties as Record<string, JSONSchema>)?.[discriminant] ?? {},
((variant.properties as Record<string, JSONSchema>) ?? {})[discriminant] ?? {},
);
const yamlExample = buildYamlExampleBlock(props);
const fieldList = buildFieldList(props);
@@ -131,7 +131,6 @@ async function buildHistory(
startedAtMs: step.startedAtMs,
completedAtMs: step.completedAtMs,
cwd: step.cwd ?? "",
assembledPrompt: step.assembledPrompt ?? null,
content,
});
}
-13
View File
@@ -64,7 +64,6 @@ async function writeStepNode(options: {
edgePrompt: string;
startedAtMs: number;
completedAtMs: number;
assembledPromptHash: CasRef | null;
}): Promise<CasRef> {
const payload: StepNodePayload = {
start: options.startHash,
@@ -77,7 +76,6 @@ async function writeStepNode(options: {
startedAtMs: options.startedAtMs,
completedAtMs: options.completedAtMs,
cwd: process.cwd(),
assembledPrompt: options.assembledPromptHash,
};
const hash = await options.store.put(options.schemas.stepNode, payload);
const node = options.store.get(hash);
@@ -116,7 +114,6 @@ async function persistStep(options: {
agentName: string;
startedAtMs: number;
completedAtMs: number;
assembledPromptHash: CasRef | null;
}): Promise<CasRef> {
const { store, schemas, chain, headHash } = options.ctx.meta;
return writeStepNode({
@@ -131,7 +128,6 @@ async function persistStep(options: {
edgePrompt: options.ctx.edgePrompt,
startedAtMs: options.startedAtMs,
completedAtMs: options.completedAtMs,
assembledPromptHash: options.assembledPromptHash,
});
}
@@ -186,14 +182,6 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
);
}
const completedAtMs = Date.now();
// Store the assembled prompt in CAS for later inspection via `step read --prompt`
const promptText = agentResult.assembledPrompt;
const assembledPromptHash =
promptText !== ""
? await ctx.meta.store.put(ctx.meta.schemas.text, promptText).catch(() => null)
: null;
const stepHash = await persistStep({
ctx,
outputHash: extracted.outputHash,
@@ -201,7 +189,6 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
agentName: agentLabel(options.name),
startedAtMs,
completedAtMs,
assembledPromptHash,
});
const adapterOutput: AdapterOutput = {
+2 -6
View File
@@ -6,21 +6,17 @@ export type UwfAgentSchemaHashes = {
workflow: Hash;
startNode: Hash;
stepNode: Hash;
text: Hash;
};
const TEXT_SCHEMA = { type: "string" as const };
/**
* Register Workflow, StartNode, and StepNode JSON Schemas in the CAS store.
* Idempotent: safe to call on every agent invocation.
*/
export async function registerAgentSchemas(store: Store): Promise<UwfAgentSchemaHashes> {
const [workflow, startNode, stepNode, text] = await Promise.all([
const [workflow, startNode, stepNode] = await Promise.all([
putSchema(store, WORKFLOW_SCHEMA),
putSchema(store, START_NODE_SCHEMA),
putSchema(store, STEP_NODE_SCHEMA),
putSchema(store, TEXT_SCHEMA),
]);
return { workflow, startNode, stepNode, text };
return { workflow, startNode, stepNode };
}
@@ -27,8 +27,6 @@ export type AgentRunResult = {
output: string;
detailHash: string;
sessionId: string;
/** The fully assembled prompt that was sent to the agent. */
assembledPrompt: string;
};
export type AgentContinueFn = (
+1 -1
View File
@@ -5,5 +5,5 @@
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../workflow-protocol" }, { "path": "../workflow-util" }]
"references": [{ "path": "../workflow-protocol" }]
}
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
passWithNoTests: true,
},
});
+3 -8
View File
@@ -14,11 +14,6 @@
"import": "./dist/index.js"
}
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "vitest run",
"test:ci": "vitest run"
},
"dependencies": {},
"devDependencies": {
"typescript": "^5.8.3"
@@ -28,12 +23,12 @@
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"url": "https://github.com/shazhou-ww/uncaged-workflow.git",
"directory": "packages/workflow-util"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"homepage": "https://github.com/shazhou-ww/uncaged-workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"license": "MIT"
}
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it } from "bun:test";
import { extractUlidTimestamp, generateUlid } from "../ulid.js";
describe("extractUlidTimestamp", () => {
@@ -1,39 +0,0 @@
export function generateBootstrapReference(): string {
return `---
name: uwf
description: "Uncaged Workflow (uwf) — YAML 状态机工作流引擎。任务涉及 workflow 时加载此 skill。"
tags: [workflow, uwf, uncaged]
triggers:
- uwf
- workflow
-
---
# uwf (Uncaged Workflow)
YAML workflow **uwf workflow**YAML Hermes skill \`uwf\` CLI 操作,不要混淆。
## 使
\`\`\`bash
uwf skill user # 使CLI thread
uwf skill author # workflow role graph schema
\`\`\`
##
\`\`\`bash
uwf workflow list # workflow
uwf workflow add <file.yaml> # workflow
uwf thread start <workflow> -p "prompt" # thread
uwf thread exec <thread-id> -c 10 # 10
uwf thread list # thread
\`\`\`
## workflow
\`examples/\` 目录下的 YAML 文件(analyze-topic、debate、solve-issue)。
`;
}
-1
View File
@@ -3,7 +3,6 @@ export { generateAdapterReference } from "./adapter-reference.js";
export { generateArchitectureReference } from "./architecture-reference.js";
export { generateAuthorReference } from "./author-reference.js";
export { encodeUint64AsCrockford } from "./base32.js";
export { generateBootstrapReference } from "./bootstrap-reference.js";
export { generateCliReference } from "./cli-reference.js";
export { generateDeveloperReference } from "./developer-reference.js";
export { env } from "./env.js";
@@ -121,15 +121,5 @@ uwf log clean --before <date> # delete old logs
uwf --format <json|yaml> # output format (default: json)
uwf -V, --version # print version
\`\`\`
## Other Skill References
For specific scenarios, run the corresponding \`uwf skill\` command:
| Scenario | Command | When to use |
|----------|---------|-------------|
| Writing workflow YAML | \`uwf skill author\` | Designing roles, conditions, graphs, and edge prompts |
| Contributing to the engine | \`uwf skill developer\` | Modifying the workflow engine codebase itself |
| Building a new agent adapter | \`uwf skill adapter\` | Creating a new \`uwf-<name>\` CLI adapter |
`;
}
-8
View File
@@ -1,8 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
passWithNoTests: true,
},
});
+1 -2
View File
@@ -21,7 +21,6 @@ const publishOrder = [
"workflow-util-agent",
"workflow-agent-hermes",
"workflow-agent-builtin",
"workflow-agent-claude-code",
"cli-workflow",
];
@@ -60,7 +59,7 @@ let failed = false;
for (const name of publishOrder) {
const pkgDir = join(root, "packages", name);
const tagFlag = tag ? `--tag ${tag}` : "";
const cmd = `npm publish --access public --ignore-scripts ${tagFlag}`;
const cmd = `npm publish --access public ${tagFlag}`;
console.log(`📦 ${name}...`);
-36
View File
@@ -1,36 +0,0 @@
---
name: uwf
description: "Uncaged Workflow (uwf) — YAML 状态机工作流引擎。任务涉及 workflow 时加载此 skill。"
tags: [workflow, uwf, uncaged]
triggers:
- uwf
- workflow
- 工作流
---
# uwf (Uncaged Workflow)
YAML 状态机工作流引擎。当用户提到「workflow」「工作流」时,指的是 **uwf workflow**(YAML 定义的状态机),不是 Hermes skill。用 `uwf` CLI 操作,不要混淆。
## 首次使用
运行以下命令获取完整用法:
```bash
uwf skill user # 用户使用手册(CLI 命令、thread 生命周期)
uwf skill author # workflow 编写指南(role 定义、graph 路由、schema)
```
## 快速参考
```bash
uwf workflow list # 查看已注册 workflow
uwf workflow add <file.yaml> # 注册 workflow
uwf thread start <workflow> -p "prompt" # 创建 thread
uwf thread exec <thread-id> -c 10 # 执行最多 10 步
uwf thread list # 查看所有 thread
```
## 示例 workflow
参考项目 `examples/` 目录下的 YAML 文件(analyze-topic、debate、solve-issue)。
-1
View File
@@ -23,7 +23,6 @@
{ "path": "packages/workflow-util-agent" },
{ "path": "packages/workflow-agent-hermes" },
{ "path": "packages/workflow-agent-builtin" },
{ "path": "packages/workflow-agent-claude-code" },
{ "path": "packages/cli-workflow" }
]
}
-852
View File
@@ -1,852 +0,0 @@
name: normalize-bun-monorepo
graph:
ci:
done:
role: solve-issue-workflow
prompt: CI configured. Register solve-issue workflow for repo at {{{repoPath}}}.
skipped:
role: solve-issue-workflow
prompt: "ci already configured, skipped."
failed:
role: solve-issue-workflow
prompt: CI setup failed ({{{reason}}}), but continue. Register solve-issue workflow for repo at {{{repoPath}}}.
biome:
done:
role: package-metadata
prompt: Biome configured. Standardize package metadata for repo at {{{repoPath}}}.
skipped:
role: package-metadata
prompt: "biome already configured, skipped."
failed:
role: package-metadata
prompt: Biome setup failed ({{{reason}}}), but continue. Standardize package metadata for repo at {{{repoPath}}}.
$START:
_:
role: workspace
prompt: Set up bun workspace structure for repo at {{{repoPath}}}.
release:
done:
role: testing
prompt: Release pipeline configured. Set up vitest for repo at {{{repoPath}}}.
skipped:
role: testing
prompt: "release already configured, skipped."
failed:
role: testing
prompt: Release pipeline failed ({{{reason}}}), but continue. Set up vitest for repo at {{{repoPath}}}.
testing:
done:
role: ci
prompt: Testing configured. Set up Gitea CI for repo at {{{repoPath}}}.
skipped:
role: ci
prompt: "testing already configured, skipped."
failed:
role: ci
prompt: Testing setup failed ({{{reason}}}), but continue. Set up Gitea CI for repo at {{{repoPath}}}.
committer:
failed:
role: $END
prompt: "Commit failed: {{{reason}}}."
committed:
role: $END
prompt: "Normalization committed: {{{commitHash}}}."
no_changes:
role: $END
prompt: Repo already normalized, no changes needed.
workspace:
done:
role: typescript
prompt: Workspace ready. Configure TypeScript for repo at {{{repoPath}}}.
skipped:
role: typescript
prompt: "workspace already configured, skipped."
failed:
role: typescript
prompt: Workspace setup failed ({{{reason}}}), but continue. Configure TypeScript for repo at {{{repoPath}}}.
guardrails:
done:
role: committer
prompt: All normalization complete. Commit changes in repo at {{{repoPath}}}.
skipped:
role: committer
prompt: "guardrails already configured, skipped."
failed:
role: committer
prompt: Guardrails failed ({{{reason}}}), but commit whatever was done in repo at {{{repoPath}}}.
typescript:
done:
role: biome
prompt: TypeScript configured. Set up Biome for repo at {{{repoPath}}}.
skipped:
role: biome
prompt: "typescript already configured, skipped."
failed:
role: biome
prompt: TypeScript setup failed ({{{reason}}}), but continue. Set up Biome for repo at {{{repoPath}}}.
package-metadata:
done:
role: release
prompt: Package metadata standardized. Configure release pipeline for repo at {{{repoPath}}}.
skipped:
role: release
prompt: "package-metadata already configured, skipped."
failed:
role: release
prompt: Package metadata failed ({{{reason}}}), but continue. Configure release pipeline for repo at {{{repoPath}}}.
solve-issue-workflow:
done:
role: guardrails
prompt: Solve-issue workflow placed in .workflows/. Install guardrails for repo at {{{repoPath}}}.
skipped:
role: guardrails
prompt: "solve-issue-workflow already configured, skipped."
failed:
role: guardrails
prompt: Solve-issue workflow failed ({{{reason}}}), but continue. Install guardrails for repo at {{{repoPath}}}.
roles:
ci:
goal: You configure Gitea Actions CI for build, lint, and test on push/PR.
output: Describe the CI pipeline configured. Set $status to done or failed.
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
If `.gitea/workflows/ci.yml` already exists, review it for completeness but don't overwrite unless it's missing key steps.
If `.github/workflows/` exists (GitHub Actions), keep it — add `.gitea/workflows/` alongside it.
Create `.gitea/workflows/ci.yml` (if not present):
```yaml
name: CI
on:
push:
branches: ['*']
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Build
run: bun run build
- name: Lint
run: bun run check
- name: Test
run: bun run test:ci
```
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
# 1. CI file exists
test -f .gitea/workflows/ci.yml
# 2. YAML is valid
node -e "
const fs = require('fs');
const content = fs.readFileSync('.gitea/workflows/ci.yml', 'utf8');
if (!content.includes('bun')) { console.error('Missing bun setup'); process.exit(1); }
if (!content.includes('test')) { console.error('Missing test step'); process.exit(1); }
console.log('CI YAML looks valid');
"
# 3. Required root scripts exist for CI to work
node -e "
const pkg = require('./package.json');
const required = ['build', 'check', 'test:ci'];
const missing = required.filter(s => !pkg.scripts?.[s]);
if (missing.length) { console.error('Missing scripts for CI:', missing.join(', ')); process.exit(1); }
console.log('All CI-required scripts present');
"
```
Post-condition: CI file exists, YAML references bun and test, all required scripts exist in package.json.
description: Set up Gitea CI workflow
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- ci-config
biome:
goal: You configure Biome for consistent code quality across the monorepo.
output: List what was configured and any remaining lint issues. Set $status to done or failed.
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
Be idempotent — if biome.json already exists, merge missing settings rather than overwriting.
Check and fix:
1. Install biome: add `@biomejs/biome` to root devDependencies (skip if already present)
2. Root `biome.json` must exist with at minimum:
- `files.includes`: `["**", "!**/dist", "!**/node_modules"]`
- `formatter`: indentStyle space, indentWidth 2, lineWidth 100
- `javascript.formatter`: quoteStyle double, semicolons always
- `linter.rules.nursery.noConsole: "error"` (production code)
- Override for test files (`**/__tests__/**`): `noConsole: "off"`, `noExplicitAny: "off"`
- `assist.actions.source.organizeImports: "on"`
If biome.json already exists, only add missing fields — preserve existing customizations.
3. Root scripts must include: `"check"` (should include `biome check .`), `"format": "biome format --write ."`
4. Run `bunx biome check .` — fix auto-fixable issues with `bunx biome check . --fix`
5. Remaining unfixable issues: list them but don't block
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
# 1. biome.json exists
test -f biome.json
# 2. biome is installed
bunx biome --version
# 3. check runs (exit 0 or list remaining issues)
bunx biome check . 2>&1 || true
```
Post-condition: `biome.json` exists, `bunx biome check .` runs (may have warnings but no infrastructure errors).
description: Configure Biome linter and formatter
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- linter-config
release:
goal: "You set up the complete release pipeline: changesets for version management, publish script for npm release."
output: Describe what was configured. Set $status to done or failed.
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
Be idempotent — skip steps that are already done.
## Part 1: Changesets
1. Install: add `@changesets/cli` to root devDependencies (skip if present), run `bun install`
2. If `.changeset/config.json` does not exist, run `bunx changeset init`
3. Verify `.changeset/config.json` has:
- `"access": "public"` (for @scoped packages)
- `"baseBranch": "main"`
4. Add `.changeset/README.md` if missing
## Part 2: Publish Script
5. Create `scripts/publish-all.mjs` (skip if already exists and looks correct):
- Scan `packages/` for non-private packages (skip `"private": true`)
- Determine publish order by resolving workspace dependency graph (dependees before dependents)
- For each package in order:
a. Replace `"workspace:^"` deps with actual versions from workspace
b. Run `npm publish --access public --ignore-scripts` (MUST use --ignore-scripts to bypass prepublishOnly guardrail)
c. Restore original package.json
- Support flags: `--dry-run`, `--tag <name>`
- Handle errors: if one package fails, restore and continue
CRITICAL: The publish command MUST include `--ignore-scripts` because the guardrails role adds a `prepublishOnly` script that blocks direct publishing.
## Part 3: Root Scripts
6. Root scripts must include:
- `"changeset": "bunx changeset"`
- `"version": "bunx changeset version"`
- `"release": "bun run build && bun run test && node scripts/publish-all.mjs"`
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
# 1. changeset config exists and is valid
test -f .changeset/config.json
node -e "const c = require('./.changeset/config.json'); console.log('access:', c.access, 'baseBranch:', c.baseBranch)"
# 2. changeset status works
bunx changeset status 2>&1 || true
# 3. publish script exists and uses --ignore-scripts
test -f scripts/publish-all.mjs
grep -q 'ignore-scripts' scripts/publish-all.mjs
# 4. dry run works
node scripts/publish-all.mjs --dry-run
```
Post-condition: All verification commands pass.
description: Configure changesets and publish pipeline
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- changeset-config
- release-config
testing:
goal: You set up vitest test infrastructure across the monorepo.
output: List what was configured per package. Set $status to done or failed.
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
Be idempotent — do NOT overwrite existing vitest.config.ts or test files.
Check and fix:
1. Add `vitest` to root devDependencies (skip if present), run `bun install`
2. For each package under `packages/`:
- If `vitest.config.ts` does NOT already exist, create it:
```ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
},
});
```
- If package.json has no `"test"` script, add: `"test": "vitest run --passWithNoTests"`, `"test:ci": "vitest run --passWithNoTests"`
- Create `src/__tests__/` directory if it doesn't exist
3. Root package.json scripts must include:
- `"test": "bun run --filter './packages/*' test"`
- `"test:ci": "bun run --filter './packages/*' test:ci"`
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
# 1. vitest is installed
bunx vitest --version
# 2. each package has vitest config
for d in packages/*/; do
if [ -f "$d/vitest.config.ts" ]; then echo "$d: ✅"; else echo "$d: ❌ missing vitest.config.ts"; fi
done
# 3. root test script works
bun run test 2>&1 || true
```
Post-condition: `bun run test` runs without infrastructure errors (no tests is OK, test failures are OK).
description: Configure vitest for all packages
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- test-config
committer:
goal: You commit all the changes made by previous roles in a single clean commit.
output: List files changed and commit hash. Set $status to committed or no_changes.
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
1. Review all changes: `git diff --stat` and `git status`
2. Verify key files exist from previous roles (spot-check):
```bash
echo "=== Spot-check ==="
if [ -f tsconfig.json ]; then echo "✅ tsconfig.json"; else echo "ℹ️ tsconfig.json (skipped for JS/MJS)"; fi
for f in .gitignore biome.json .changeset/config.json .gitea/workflows/ci.yml .githooks/pre-push; do
test -f "$f" && echo "✅ $f" || echo "⚠️ MISSING: $f"
done
node -e "
const p = require('./package.json');
const required = ['build', 'check', 'test', 'test:ci', 'format', 'preinstall', 'prepublishOnly'];
const missing = required.filter(s => !p.scripts?.[s]);
if (missing.length) console.log('⚠️ Missing scripts:', missing.join(', '));
else console.log('✅ All scripts present');
if (!p.packageManager) console.log('⚠️ Missing packageManager');
else console.log('✅ packageManager:', p.packageManager);
"
```
List any missing items as warnings but still commit what exists.
3. If no changes: set $status=no_changes
4. Stage all: `git add -A`
5. **Before committing, check for build artifacts that should NOT be committed:**
```bash
# Detect compiled output accidentally staged
git diff --cached --name-only | grep -E '\.(d\.ts|\.js\.map)$' | grep -v node_modules | head -20
# Also check for .js files next to .ts sources (build output in src/)
for f in $(git diff --cached --name-only | grep -E '\.js$' | grep -v node_modules | grep -v scripts/); do
ts_file="${f%.js}.ts"
if [ -f "$ts_file" ]; then echo "BUILD ARTIFACT: $f (has matching $ts_file)"; fi
done
```
If build artifacts are found:
- Unstage them: `git reset HEAD <files>`
- Add patterns to `.gitignore` if missing (e.g. `*.d.ts`, `*.js.map`, or specific output dirs)
- Re-run `git add -A` after updating `.gitignore`
6. Commit: `git commit -m "chore: normalize to bun monorepo conventions"`
7. Push: `git push`
Post-condition: Clean commit pushed, `git status` shows clean working tree. No build artifacts in the commit.
description: Commits all normalization changes
frontmatter:
oneOf:
- properties:
$status:
const: committed
commitHash:
type: string
repoPath:
type: string
- properties:
$status:
const: no_changes
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities: []
workspace:
goal: You set up the foundational bun workspace configuration for a monorepo.
output: List what was changed. Set $status to done (workspace working) or failed (with reason).
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path provided in your task prompt.
Be idempotent — check before modifying. If something is already correct, skip it.
Check and fix:
1. Root `package.json` must have `"workspaces": ["packages/*"]`
2. Root `package.json` must have `"private": true`
3. If packages exist in other locations (e.g. root src/, top-level dirs), migrate them under `packages/`
4. Each package under `packages/` must have its own `package.json` with `"name"` and `"type": "module"`
5. `.gitignore` must exist and include at minimum:
```
node_modules/
dist/
*.tsbuildinfo
```
If `.gitignore` is missing or doesn't cover these, append the missing entries (don't overwrite existing content).
6. If node_modules/ is already tracked in git, remove it: `git rm -r --cached node_modules/ */node_modules/ 2>/dev/null`
7. Run `bun install` to verify workspace resolution works
## Verification (must all pass)
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
# 1. bun install works
bun install
# 2. gitignore covers essentials
grep -q 'node_modules' .gitignore
grep -q 'dist' .gitignore
# 3. no node_modules tracked
test -z "$(git ls-files | grep node_modules)"
# 4. all packages have package.json
for d in packages/*/; do test -f "$d/package.json" || echo "MISSING: $d/package.json"; done
```
Post-condition: All verification commands pass.
description: Ensure bun workspace structure
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- workspace-setup
guardrails:
goal: You configure enforcement mechanisms that block npm/pnpm/yarn usage and direct npm publish.
output: List what guardrails were installed. Set $status to done or failed.
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
Be idempotent — check before adding.
## 1. Block wrong package manager
Add to root `package.json` (if not already present):
- `"packageManager": "bun@<version>"` — use the version from `bun --version`
- `"scripts.preinstall": "npx only-allow bun"` — blocks npm/pnpm/yarn install
## 2. Block direct npm publish
Add to root `package.json` (if not already present):
- `"scripts.prepublishOnly": "echo 'Use bun run release instead' && exit 1"`
For each non-private package under `packages/`:
- Add `"scripts.prepublishOnly": "echo 'Use bun run release from repo root' && exit 1"` to their package.json (if not present)
If `scripts/publish-all.mjs` exists, verify it uses `--ignore-scripts` in the npm publish command.
If it doesn't, add `--ignore-scripts` to the publish command.
## 3. Git hooks
Create `.githooks/pre-push` (if not already present):
```bash
#!/usr/bin/env bash
set -euo pipefail
echo "🔍 Running checks..."
bun run check
echo "🧪 Running tests..."
bun run test
echo "✅ All checks passed!"
```
Make it executable: `chmod +x .githooks/pre-push`
Configure git to use hooks dir: `git config core.hooksPath .githooks`
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
# 1. packageManager field exists
node -e "const p = require('./package.json'); if (!p.packageManager) { console.error('❌ missing packageManager'); process.exit(1); } console.log('✅ packageManager:', p.packageManager)"
# 2. preinstall guard exists
node -e "const p = require('./package.json'); if (!p.scripts?.preinstall?.includes('only-allow')) { console.error('❌ missing preinstall guard'); process.exit(1); } console.log('✅ preinstall guard')"
# 3. npm install is blocked (with timeout to prevent hang)
timeout 10 npm install 2>&1 | head -5 || true
# 4. prepublishOnly exists
node -e "const p = require('./package.json'); if (!p.scripts?.prepublishOnly) { console.error('❌ missing prepublishOnly'); process.exit(1); } console.log('✅ prepublishOnly guard')"
# 5. publish script uses --ignore-scripts
if [ -f scripts/publish-all.mjs ]; then grep -q 'ignore-scripts' scripts/publish-all.mjs && echo '✅ publish uses --ignore-scripts' || echo '❌ publish missing --ignore-scripts'; fi
# 6. git hooks configured
test -f .githooks/pre-push && echo '✅ pre-push hook' || echo '❌ missing pre-push hook'
```
Post-condition: All verification checks pass.
description: Install project guardrails to prevent wrong package manager and publish workflow
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- guardrails
typescript:
goal: You configure TypeScript for a bun monorepo with composite project references.
output: List what was configured. Set $status to done or failed.
procedure: |-
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
Be idempotent — if tsconfig.json already exists with correct settings, don't overwrite.
## Step 0: Detect if project needs TypeScript compilation
```bash
TS_FILES=$(find packages/ -name '*.ts' -o -name '*.tsx' | grep -v node_modules | grep -v dist | grep -v '.d.ts' | head -5)
```
**If there are NO .ts/.tsx source files** (pure JS/MJS project):
- Do NOT create root tsconfig.json
- Do NOT add `bunx tsc --build` as the build script
- Do NOT add typescript/bun-types to devDependencies unless already present
- Preserve the existing `build` script (e.g. vite build, esbuild, etc.)
- If no build script exists, add one based on the project's bundler (check for vite.config, esbuild, etc.)
- Set $status=done with note "JS/MJS project — skipped TypeScript setup"
- STOP HERE.
**If .ts/.tsx files exist**, continue with full TypeScript setup:
Check and fix:
1. Root `tsconfig.json` must exist with:
- `"compilerOptions"`: target ES2022, module NodeNext, moduleResolution NodeNext, strict true, composite true, declaration true, declarationMap true, sourceMap true
- `"references"`: array with `{ "path": "packages/<name>" }` for each package
- `"files": []` (root does not compile files directly)
2. Each package must have `tsconfig.json` with:
- `"extends": "../../tsconfig.json"` (inherit root config)
- `"compilerOptions": { "rootDir": "src", "outDir": "dist" }`
- `"include": ["src"]`
- `"references"` to sibling packages it depends on
3. Root scripts must include: `"build": "bunx tsc --build"`, `"typecheck": "bunx tsc --build"`
4. `devDependencies` at root: `typescript`, `bun-types`, `@types/node`
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
if [ ! -f tsconfig.json ]; then
echo "JS/MJS project — no tsconfig needed"
node -e "const p = require('./package.json'); if (!p.scripts?.build) { console.error('Missing build script'); process.exit(1); } console.log('build:', p.scripts.build)"
bun run build
exit 0
fi
test -f tsconfig.json
for d in packages/*/; do test -f "$d/tsconfig.json" || echo "MISSING: $d/tsconfig.json"; done
bunx tsc --build
```
Post-condition: For TS projects — `bunx tsc --build` succeeds. For JS/MJS projects — `bun run build` succeeds.
description: Configure TypeScript with project references
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- typescript-config
package-metadata:
goal: You ensure every package has consistent metadata for publishing and discoverability.
output: List what was standardized per package. Set $status to done or failed.
procedure: |
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
Be idempotent — skip fields that are already correctly set.
For each package under `packages/`:
1. `"type": "module"` — must be set
2. `"files": ["src", "dist", "package.json"]` — for published packages (non-private)
3. `"publishConfig": { "access": "public" }` — for @scoped public packages (non-private)
4. `"repository"` — must point to the correct git remote and directory
- Read remote URL: `git remote get-url origin`
- Set `"directory": "packages/<name>"`
5. `"exports"` — conditional exports for TypeScript packages:
```json
".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
```
Skip if package has no `src/index.ts`.
6. Private packages (server, frontend, tools) must have `"private": true` and can skip publishConfig/files
7. Each package should have a `"scripts"` section with at least `"test"` if tests exist
8. Workspace dependencies should use `"workspace:^"` protocol, not version numbers
- Check: `grep -r '"@uncaged/' packages/*/package.json | grep -v 'workspace:'`
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
# Check every non-private package has required fields
for d in packages/*/; do
pkg="$d/package.json"
test -f "$pkg" || continue
node -e "
const p = require('./$pkg');
if (p.private) { console.log('$d: private, skip'); process.exit(0); }
const issues = [];
if (p.type !== 'module') issues.push('missing type:module');
if (!p.exports) issues.push('missing exports');
if (!p.publishConfig) issues.push('missing publishConfig');
if (!p.files) issues.push('missing files');
if (issues.length) console.log('$d:', issues.join(', '));
else console.log('$d: OK');
"
done
# No hardcoded workspace deps
! grep -r '"@uncaged/' packages/*/package.json | grep -v 'workspace:' | grep -v node_modules
```
Post-condition: Verification script shows OK for all non-private packages, no hardcoded workspace versions.
description: Standardize package.json metadata across all packages
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- package-config
solve-issue-workflow:
goal: You place a solve-issue workflow YAML in .workflows/ so the project can use uwf thread start .workflows/solve-issue.yaml for issue resolution.
output: Describe the workflow registered. Set $status to done or failed.
procedure: |-
## GROUND RULES (read before doing anything)
- Only report actions you ACTUALLY performed and files you ACTUALLY created or modified.
- If everything is already correctly configured, set $status=skipped.
- NEVER fabricate command output. Run every verification command for real and paste the actual stdout/stderr.
- After writing a file, run `test -f <path> && echo EXISTS || echo MISSING` to confirm it was actually written.
cd into the repo path from your task prompt.
1. Check if `uwf` CLI is available: `which uwf`
- If not available: set $status=failed, reason="uwf CLI not installed"
2. Check if `.workflows/solve-issue.yaml` already exists:
- If it exists and looks correct (has planner/developer/reviewer/tester/committer roles), skip. Set $status=done.
3. Create `.workflows/solve-issue.yaml` adapted for this project:
- Copy the standard solve-issue workflow structure (planner -> developer -> reviewer -> tester -> committer)
- Adjust the developer role procedure to use this project's test runner and build commands
- The workflow should reference the correct repo path and build toolchain (bun)
NOTE: Place the file in `.workflows/` (dot-prefix), NOT `workflows/`. Do NOT run `uwf workflow add`. The file is used directly via `uwf thread start .workflows/solve-issue.yaml`.
## Verification
You MUST actually run each command below and include real output. Do NOT guess or fabricate results.
```bash
test -f .workflows/solve-issue.yaml
node -e "
const fs = require('fs');
const content = fs.readFileSync('.workflows/solve-issue.yaml', 'utf8');
const required = ['planner', 'developer', 'reviewer', 'tester', 'committer'];
const missing = required.filter(r => !content.includes(r + ':'));
if (missing.length) { console.error('Missing roles:', missing.join(', ')); process.exit(1); }
console.log('All roles present');
"
```
Post-condition: `.workflows/solve-issue.yaml` exists with all 5 roles.
description: Register solve-issue workflow for the project
frontmatter:
oneOf:
- properties:
$status:
const: done
repoPath:
type: string
- properties:
$status:
const: skipped
repoPath:
type: string
- properties:
$status:
const: failed
reason:
type: string
repoPath:
type: string
capabilities:
- workflow-config
description: Normalize an existing project to @uncaged bun monorepo conventions. Supports both TypeScript and JS/MJS projects. Each role handles one configuration layer. All roles allow fail.
-323
View File
@@ -1,323 +0,0 @@
name: solve-issue
description: TDD-driven issue resolution adapted for the workflow monorepo with bun + vitest
roles:
planner:
description: Analyzes issue and outputs a TDD test spec
goal: You are a planning agent. You analyze Gitea issues and produce a TDD test specification that downstream roles will implement and verify.
capabilities:
- issue-analysis
- planning
procedure: 'On first run (no previous steps):
1. Read the issue and all comments from Gitea using `tea issues <number> -r <owner/repo>`
2. Look for project conventions files (CLAUDE.md, CONTRIBUTING.md) in the repo
3. Assess whether the issue has enough information to produce a test spec
4. If insufficient info: comment on the issue via `echo "..." | tea comment <number> -r <owner/repo>` (skip if you already commented), then output $status=insufficient_info
5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios
On subsequent runs (bounced back by tester with fix_spec):
1. Read the tester''s output from the previous step to understand what''s wrong with the spec
2. Revise the test spec accordingly
After producing the test spec:
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
2. Put the hash in frontmatter.plan (required when $status=ready)
3. Set repoPath to the absolute path of the repository root
IMPORTANT: Extract the repo remote (owner/repo) from git:
```bash
git remote get-url origin | sed ''s|.*[:/]\([^/]*/[^.]*\).*|\1|''
```
Store the result as repoRemote in your frontmatter output so downstream roles can use it for tea/API calls.'
output: Output a brief summary of the test spec. Set $status to ready (with plan hash and repoPath) or insufficient_info.
frontmatter:
oneOf:
- properties:
$status:
const: ready
plan:
type: string
repoPath:
type: string
repoRemote:
type: string
required:
- $status
- plan
- repoPath
- properties:
$status:
const: insufficient_info
required:
- $status
developer:
description: TDD implementation per test spec
goal: You are a developer agent. You implement code changes following TDD — write tests first, then implementation.
capabilities:
- coding
procedure: "IMPORTANT: Always work in a git worktree, NEVER modify the main working directory directly.\nThe repo path and other details are provided in your task prompt.\n\nBefore starting any work,\
\ set up an isolated worktree:\n1. cd into the repo path provided in your task prompt\n2. `git fetch origin` to get latest refs\n3. First time (no existing branch):\n - `git worktree add .worktrees/fix/<issue-number>-<short-slug>\
\ -b fix/<issue-number>-<short-slug> origin/main`\n - `cd .worktrees/fix/<issue-number>-<short-slug> && bun install`\n4. If bounced back from reviewer or tester (branch already exists):\n - cd\
\ into the existing worktree under `.worktrees/fix/<issue-number>-<short-slug>`\n - `git fetch origin && git rebase origin/main`\n5. ALL subsequent work must happen inside the worktree directory.\n\
\nThen implement TDD:\n6. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner's output in your task prompt)\n7. If bounced back from reviewer or tester: read the\
\ previous role's feedback in your task prompt\n8. Write tests first based on the spec (use vitest)\n9. Implement the code to make tests pass\n10. Ensure `bun run build` passes with no errors\n11.\
\ Run `bun test` to verify all tests pass\n\nIf you cannot complete the implementation (e.g. the issue is too complex, blocked by external factors,\nor repeated attempts fail), set $status=failed\
\ with a reason.\n"
output: List all files changed and provide a summary. Set $status to done (with branch/worktree), or failed (with reason).
frontmatter:
oneOf:
- properties:
$status:
const: done
branch:
type: string
worktree:
type: string
repoRemote:
type: string
required:
- $status
- branch
- worktree
- properties:
$status:
const: failed
reason:
type: string
repoRemote:
type: string
required:
- $status
- reason
reviewer:
description: Code standards compliance check
goal: You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job).
capabilities:
- code-review
- static-analysis
procedure: 'The worktree path is provided in your task prompt. cd into it first.
Before reviewing, verify the git branch:
1. Run `git branch --show-current` — confirm the branch name references the issue number being worked on
2. If the branch doesn''t correspond to the issue, flag it in your output and reject
Then perform code review:
Hard checks (must all pass):
3. `bun run build` — no build errors
4. `bunx biome check` — no lint violations
5. TypeScript strict mode — no type errors
Soft checks (review against project conventions from CLAUDE.md):
- Functional-first: functions + types, no classes (except for errors or third-party requirements)
- Named exports only, no default exports
- No optional properties (use `T | null` instead of `?:`)
- Folder module discipline: index.ts only re-exports, types in types.ts
- Crockford Base32 log tags (8-char, unique per call site)
- No `console.log` in production code (use createLogger from @uncaged/workflow-util)
- No dynamic imports in production code
Only review standards compliance. Do NOT test functionality.
If rejecting, you MUST explain the specific reason in your output.
'
output: Explain your decision with specific file/line references. Set $status to approved (with branch/worktree) or rejected (with comments).
frontmatter:
oneOf:
- properties:
$status:
const: approved
branch:
type: string
worktree:
type: string
repoRemote:
type: string
required:
- $status
- branch
- worktree
- properties:
$status:
const: rejected
comments:
type: string
worktree:
type: string
repoRemote:
type: string
required:
- $status
- comments
- worktree
tester:
description: Functional correctness verification
goal: You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec.
capabilities:
- testing
procedure: "The worktree path is provided in your task prompt. cd into it first.\n\n1. Run `bun test` for automated test verification\n2. Read the test spec from CAS: `uwf cas get <plan hash>` (find\
\ the hash from the planner step in the thread history)\n3. Verify each scenario in the spec is covered and passing\n4. Determine outcome:\n - passed: all scenarios verified, tests pass\n - fix_code:\
\ tests fail or implementation doesn't match spec → send back to developer\n - fix_spec: the spec itself is wrong or incomplete → send back to planner\n"
output: Report test results per scenario. Set $status to passed (with branch/worktree), fix_code (with report), or fix_spec (with report).
frontmatter:
oneOf:
- properties:
$status:
const: passed
branch:
type: string
worktree:
type: string
repoRemote:
type: string
required:
- $status
- branch
- worktree
- properties:
$status:
const: fix_code
report:
type: string
repoRemote:
type: string
worktree:
type: string
branch:
type: string
required:
- $status
- report
- properties:
$status:
const: fix_spec
report:
type: string
repoRemote:
type: string
worktree:
type: string
branch:
type: string
required:
- $status
- report
committer:
description: Commits and creates PR
goal: You are a committer agent. You create a clean commit and push a PR linking the original issue.
capabilities: []
procedure: "The worktree path, branch name, and repo remote (owner/repo) are provided in your task prompt.\ncd into the worktree first.\n\nNote: You inherit the developer's worktree and branch. Do NOT\
\ create a new branch.\n1. Stage all changes: `git add -A`\n2. Commit with a descriptive message referencing the issue: `git commit -m \"type: description\\n\\nFixes #N\"`\n3. Push the branch: `git\
\ push -u origin <branch-name>`\n4. **Verify push succeeded** — run `git ls-remote origin <branch-name>` and confirm it prints a commit hash.\n - If no output or push failed: capture the error, mark hook_failed\n\
5. Create a PR using the Gitea API (do NOT use `tea pr create` — it fails in worktrees):\n ```bash\n GITEA_TOKEN=$(cfg get GITEA_TOKEN)\n curl -s -X POST -H \"Authorization: token $GITEA_TOKEN\" -H \"Content-Type: application/json\" \\\n\
\ \"https://git.shazhou.work/api/v1/repos/<owner>/<repo>/pulls\" \\\n -d '{\"title\":\"...\",\"body\":\"...\",\"head\":\"<branch>\",\"base\":\"main\"}'\n ```\n - The repo remote (owner/repo format, e.g. \"uncaged/workflow\") is given in your task prompt — use it directly.\n\
\ - PR body must include: What / Why / Changes / Ref sections, with `Fixes #N` in Ref\n6. **Verify PR was created** — parse the curl response JSON: it must contain a `\"number\"` field. Print the PR URL.\n\
\ - If curl returns an error or no number field: capture the response, mark hook_failed\n7. After PR creation, clean up the worktree:\n - cd to the repo root (parent of .worktrees)\n - `git worktree remove <worktree-path>`"
output: Include PR URL on success or error log on failure. Set $status to committed (with prUrl) or hook_failed (with error).
frontmatter:
oneOf:
- properties:
$status:
const: committed
prUrl:
type: string
repoRemote:
type: string
worktree:
type: string
branch:
type: string
required:
- $status
- prUrl
- properties:
$status:
const: hook_failed
error:
type: string
repoRemote:
type: string
worktree:
type: string
branch:
type: string
required:
- $status
- error
graph:
$START:
_:
role: planner
prompt: Analyze the issue and produce an implementation plan.
planner:
insufficient_info:
role: $END
prompt: Insufficient information to proceed; end the workflow.
ready:
role: developer
prompt: 'Implement the TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}. Repo remote: {{{repoRemote}}}.'
developer:
done:
role: reviewer
prompt: 'Review branch {{{branch}}} at {{{worktree}}} for code standards compliance. Repo remote: {{{repoRemote}}}.'
failed:
role: $END
prompt: 'Developer failed: {{{reason}}}. Ending workflow.'
reviewer:
rejected:
role: developer
prompt: 'Reviewer rejected: {{{comments}}}. Fix the issues in repo {{{worktree}}}. Repo remote: {{{repoRemote}}}.'
approved:
role: tester
prompt: 'Review passed. Run tests on branch {{{branch}}} at {{{worktree}}}. Repo remote: {{{repoRemote}}}.'
tester:
fix_code:
role: developer
prompt: 'Tests found code issues: {{{report}}}. Fix and re-submit. Worktree: {{{worktree}}}. Repo remote: {{{repoRemote}}}.'
fix_spec:
role: planner
prompt: 'Tests found spec issues: {{{report}}}. Revise the test spec. Repo remote: {{{repoRemote}}}.'
passed:
role: committer
prompt: 'All tests passed. Commit and push branch {{{branch}}} from {{{worktree}}}. Repo remote (owner/repo): {{{repoRemote}}}.'
committer:
hook_failed:
role: developer
prompt: 'Push hook failed: {{{error}}}. Fix and re-submit. Worktree: {{{worktree}}}. Repo remote: {{{repoRemote}}}.'
committed:
role: $END
prompt: 'PR created: {{{prUrl}}}. Workflow complete.'