refactor: remove uwf cas subcommand, use ocas CLI
CI / check (pull_request) Failing after 9m57s

- Remove entire 'uwf cas' command group from CLI
- Delete commands/cas.ts (only used by CLI + tests)
- Delete cas.test.ts and cas-exit-code.test.ts
- Update workflow YAMLs: uwf cas get/has/refs/walk → ocas
- Update e2e-walkthrough script to use ocas
- Update docs and reference files
- Keep store-global-cas.test.ts (internal CAS store tests)

CAS operations now go through 'ocas' CLI exclusively.
Agent text storage handled internally by uwf pipeline.

Closes #10
This commit is contained in:
2026-06-02 21:30:59 +08:00
parent ccc7539d52
commit 1aacf11ad9
15 changed files with 77 additions and 570 deletions
+4 -4
View File
@@ -177,10 +177,10 @@ roles:
4. `uwf thread read <threadId>` — verify non-empty output
CAS operations:
5. `uwf cas get <lastStepHash>` — verify returns a type field
6. `uwf cas has <lastStepHash>` — verify exits 0
7. `uwf cas refs <lastStepHash>` — list refs (may be empty)
8. `uwf cas walk <lastStepHash>` — verify returns non-empty array
5. `ocas get <lastStepHash>` — verify returns a type field
6. `ocas has <lastStepHash>` — verify exits 0
7. `ocas refs <lastStepHash>` — list refs (may be empty)
8. `ocas walk <lastStepHash>` — verify returns non-empty array
Report results. Pass threadId, lastStepHash, workflowName, containerName forward.
output: "Report test results. Set $status to pass (with threadId, lastStepHash, workflowName, containerName) or fail."
+4 -4
View File
@@ -20,8 +20,8 @@ roles:
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)
1. The test spec is stored in CAS automatically by the uwf pipeline (agents do not need to call `ocas put` directly)
2. Put the plan 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:
@@ -63,7 +63,7 @@ roles:
5. ALL subsequent work must happen inside the worktree directory.
Then implement TDD:
6. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner's output in your task prompt)
6. Read the test spec from CAS: `ocas get <plan hash>` (find the hash from the planner's output in your task prompt)
7. If bounced back from reviewer or tester: read the previous role's feedback in your task prompt
8. Write tests first based on the spec
9. Implement the code to make tests pass
@@ -151,7 +151,7 @@ roles:
The worktree path is provided in your task prompt. cd into it first.
1. Run `bun test` for automated test verification
2. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner step in the thread history)
2. Read the test spec from CAS: `ocas get <plan hash>` (find the hash from the planner step in the thread history)
3. Verify each scenario in the spec is covered and passing
4. Determine outcome:
- passed: all scenarios verified, tests pass
+10 -8
View File
@@ -447,16 +447,18 @@ Binary: `uwf`
### CAS commands
Use the `ocas` CLI for direct CAS operations (`~/.ocas/` store, shared with `uwf`):
| Command | Description |
|---------|-------------|
| `uwf cas get <hash>` | Read a CAS node. |
| `uwf cas put <type-hash> <data>` | Store a node, print its hash. |
| `uwf cas has <hash>` | Check if a hash exists. |
| `uwf cas refs <hash>` | List direct CAS references. |
| `uwf cas walk <hash>` | Recursive traversal from a node. |
| `uwf cas reindex` | Rebuild type index from all nodes. |
| `uwf cas schema list` | List registered schemas. |
| `uwf cas schema get <hash>` | Show a schema by type hash. |
| `ocas get <hash>` | Read a CAS node. |
| `ocas put <type-hash> <data>` | Store a node, print its hash. |
| `ocas has <hash>` | Check if a hash exists. |
| `ocas refs <hash>` | List direct CAS references. |
| `ocas walk <hash>` | Recursive traversal from a node. |
| `ocas reindex` | Rebuild type index from all nodes. |
| `ocas schema list` | List registered schemas. |
| `ocas schema get <hash>` | Show a schema by type hash. |
### Setup
+5 -5
View File
@@ -22,7 +22,7 @@ roles:
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
6. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
6. The test spec is stored in CAS automatically by the uwf pipeline (agents do not need to call `ocas put` directly)
7. Output **$status=ready** with plan hash and repoPath
**Mode B — Continue on existing PR (prompt mentions PR, branch, or review feedback):**
@@ -32,14 +32,14 @@ roles:
3. Read the existing issue for full context: `tea issues <number> -r <owner/repo>`
4. Look for project conventions files (CLAUDE.md, CONTRIBUTING.md, .cursor/rules/) in the repo
5. Produce a TDD test spec that ONLY covers the changes requested in the review — do NOT re-spec already-implemented features
6. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
6. The test spec is stored in CAS automatically by the uwf pipeline (agents do not need to call `ocas put` directly)
7. Find the existing worktree: `git worktree list` and locate the branch
8. Output **$status=continue** with plan hash, repoPath, branch name, and worktree path
**Mode C — Bounced back by tester (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
3. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
3. The test spec is stored in CAS automatically by the uwf pipeline (agents do not need to call `ocas put` directly)
4. Output **$status=ready** with plan hash and repoPath
IMPORTANT: Extract the repo remote (owner/repo) from git:
@@ -91,7 +91,7 @@ roles:
6. ALL subsequent work must happen inside the worktree directory.
Then implement TDD:
6. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner's output in your task prompt)
6. Read the test spec from CAS: `ocas get <plan hash>` (find the hash from the planner's output in your task prompt)
7. If bounced back from reviewer or tester: read the previous role's feedback in your task prompt
8. Write tests first based on the spec
9. Implement the code to make tests pass
@@ -160,7 +160,7 @@ roles:
The worktree path is provided in your task prompt. cd into it first.
1. Run `bun test` for automated test verification
2. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner step in the thread history)
2. Read the test spec from CAS: `ocas get <plan hash>` (find the hash from the planner step in the thread history)
3. Verify each scenario in the spec is covered and passing
4. Determine outcome:
- passed: all scenarios verified, tests pass
+10 -9
View File
@@ -99,17 +99,18 @@ uwf step fork 32GCDE899RRQ3
### CAS
Use the [`ocas`](https://www.npmjs.com/package/@ocas/cli) CLI for direct CAS operations (`~/.ocas/` store, shared with `uwf`):
| Command | Description |
|---------|-------------|
| `uwf cas get <hash> [--timestamp]` | Read a CAS node |
| `uwf cas put <type-hash> <data>` | Store a node, print hash |
| `uwf cas put-text <text>` | Store plain text, print hash |
| `uwf cas has <hash>` | Check existence |
| `uwf cas refs <hash>` | List direct references |
| `uwf cas walk <hash>` | Recursive traversal |
| `uwf cas reindex` | Rebuild type index |
| `uwf cas schema list` | List registered schemas |
| `uwf cas schema get <hash>` | Show a schema |
| `ocas get <hash> [--timestamp]` | Read a CAS node |
| `ocas put <type-hash> <data>` | Store a node, print hash |
| `ocas has <hash>` | Check existence |
| `ocas refs <hash>` | List direct references |
| `ocas walk <hash>` | Recursive traversal |
| `ocas reindex` | Rebuild type index |
| `ocas schema list` | List registered schemas |
| `ocas schema get <hash>` | Show a schema |
### Setup
@@ -1,171 +0,0 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execSync } from "node:child_process";
import { mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
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");
});
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 = {
stdout: string;
stderr: string;
exitCode: number;
};
function execUwf(args: string[]): ExecResult {
try {
const stdout = execSync(`bun ${uwfPath} ${args.join(" ")}`, {
env: {
...process.env,
WORKFLOW_STORAGE_ROOT: storageRoot,
UNCAGED_CAS_DIR: casDir,
},
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return { stdout, stderr: "", exitCode: 0 };
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"stdout" in error &&
"stderr" in error &&
"status" in error
) {
return {
stdout: (error.stdout as Buffer | string).toString(),
stderr: (error.stderr as Buffer | string).toString(),
exitCode: error.status as number,
};
}
throw error;
}
}
describe("uwf cas has CLI exit codes", () => {
test("exits 0 when hash exists", async () => {
// Setup: Create a temp storage root, put a text node, capture hash
const putResult = await cmdCasPutText(storageRoot, "test content");
const hash = putResult.hash;
// Execute: uwf cas has <hash>
const result = execUwf(["cas", "has", hash]);
// Assert: stdout contains {"exists":true}, exit code === 0
expect(result.stdout).toContain('"exists":true');
expect(result.exitCode).toBe(0);
});
test("exits 1 when hash does not exist", () => {
// Setup: Create a temp storage root (empty CAS store)
// Execute: uwf cas has NOSUCHHASH123
const result = execUwf(["cas", "has", "NOSUCHHASH123"]);
// Assert: stdout contains {"exists":false}, exit code === 1
expect(result.stdout).toContain('"exists":false');
expect(result.exitCode).toBe(1);
});
test("JSON output format unchanged for exists=true", async () => {
// Setup: Create store, put node
const putResult = await cmdCasPutText(storageRoot, "test");
const hash = putResult.hash;
// Execute: uwf cas has <hash>
const result = execUwf(["cas", "has", hash]);
// Assert: stdout JSON parses correctly to {exists: true}
const parsed = JSON.parse(result.stdout.trim());
expect(parsed).toEqual({ exists: true });
});
test("JSON output format unchanged for exists=false", () => {
// Setup: Create empty store
// Execute: uwf cas has INVALID
const result = execUwf(["cas", "has", "INVALID"]);
// Assert: stdout JSON parses correctly to {exists: false}
const parsed = JSON.parse(result.stdout.trim());
expect(parsed).toEqual({ exists: false });
});
test("YAML output format preserves exit code behavior for exists=true", async () => {
// Setup: Create store with node
const putResult = await cmdCasPutText(storageRoot, "test");
const hash = putResult.hash;
// Execute: uwf --format yaml cas has <hash>
const result = execUwf(["--format", "yaml", "cas", "has", hash]);
// Assert: exit code === 0, output is YAML format
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("exists:");
expect(result.stdout).toContain("true");
});
test("YAML output format preserves exit code behavior for exists=false", () => {
// Setup: Create empty store
// Execute: uwf --format yaml cas has INVALID
const result = execUwf(["--format", "yaml", "cas", "has", "INVALID"]);
// Assert: exit code === 1, output is YAML format
expect(result.exitCode).toBe(1);
expect(result.stdout).toContain("exists:");
expect(result.stdout).toContain("false");
});
});
describe("regression: other cas commands unaffected", () => {
test("uwf cas get still exits 1 on not-found with error message", () => {
// Execute: uwf cas get NOSUCHHASH
const result = execUwf(["cas", "get", "NOSUCHHASH"]);
// Assert: exit code === 1, stderr contains "Node not found"
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("Node not found");
});
test("uwf cas put-text behavior unchanged", () => {
// Execute: uwf cas put-text "hello"
const result = execUwf(["cas", "put-text", "hello"]);
// Assert: exit code === 0, returns hash
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout.trim());
expect(parsed).toHaveProperty("hash");
expect(typeof parsed.hash).toBe("string");
expect(parsed.hash.length).toBe(13); // Crockford Base32 XXH64 hash length
});
});
@@ -1,74 +0,0 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { cmdCasHas, cmdCasPutText } from "../commands/cas.js";
let storageRoot: string;
beforeEach(async () => {
storageRoot = join(tmpdir(), `uwf-cas-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(storageRoot, { recursive: true });
});
afterEach(async () => {
await rm(storageRoot, { recursive: true, force: true });
});
describe("cmdCasHas", () => {
test("returns {exists: true} for existing hash", async () => {
// Setup: Create a test store, put a node, get its hash
const putResult = await cmdCasPutText(storageRoot, "test content");
const hash = putResult.hash;
// Execute: Call cmdCasHas with the valid hash
const result = await cmdCasHas(storageRoot, hash);
// Assert: Result equals {exists: true}
expect(result).toEqual({ exists: true });
});
test("returns {exists: false} for non-existent hash", async () => {
// Setup: Create an empty test store
// (storageRoot already created in beforeEach)
// Execute: Call cmdCasHas with an invalid hash
const result = await cmdCasHas(storageRoot, "INVALIDHASH12");
// Assert: Result equals {exists: false}
expect(result).toEqual({ exists: false });
});
test("does not throw for non-existent hash", async () => {
// Setup: Create an empty test store
// Execute & Assert: Does not throw, returns {exists: false}
await expect(cmdCasHas(storageRoot, "NOSUCHHASH123")).resolves.toEqual({
exists: false,
});
});
test("handles malformed hash gracefully", async () => {
// Setup: Create a test store
// Execute: Call cmdCasHas with a too-short hash
const result = await cmdCasHas(storageRoot, "xyz");
// Assert: Returns {exists: false} (store.has() returns false)
expect(result).toEqual({ exists: false });
});
test("handles empty hash string", async () => {
// Execute: Call cmdCasHas with an empty string
const result = await cmdCasHas(storageRoot, "");
// Assert: Returns {exists: false}
expect(result).toEqual({ exists: false });
});
test("handles hash with special characters", async () => {
// Execute: Call cmdCasHas with special characters
const result = await cmdCasHas(storageRoot, "HASH!@#");
// Assert: Returns {exists: false}
expect(result).toEqual({ exists: false });
});
});
-118
View File
@@ -2,17 +2,6 @@
import type { CasRef, ThreadId, ThreadStatus } from "@united-workforce/protocol";
import { Command } from "commander";
import {
cmdCasGet,
cmdCasHas,
cmdCasPut,
cmdCasPutText,
cmdCasRefs,
cmdCasReindex,
cmdCasSchemaGet,
cmdCasSchemaList,
cmdCasWalk,
} from "./commands/cas.js";
import { cmdConfigGet, cmdConfigList, cmdConfigSet } from "./commands/config.js";
import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js";
import {
@@ -616,113 +605,6 @@ program
},
);
const cas = program.command("cas").description("Content-addressable storage operations");
cas
.command("get")
.description("Read a CAS node (type + payload; use --timestamp to include timestamp)")
.argument("<hash>", "CAS hash (13 char)")
.option("--timestamp", "Include timestamp in output")
.action((hash: string, opts: { timestamp?: boolean }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasGet(storageRoot, hash, opts));
});
});
cas
.command("put")
.description("Store a node, print its hash")
.argument("<type-hash>", "Type (schema) hash")
.argument("<data>", "JSON file path or inline JSON string")
.action((typeHash: string, data: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasPut(storageRoot, typeHash, data));
});
});
cas
.command("put-text")
.description("Store a plain text string, print its hash")
.argument("<text>", "Text content to store")
.action((text: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasPutText(storageRoot, text));
});
});
cas
.command("has")
.description("Check if a hash exists")
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdCasHas(storageRoot, hash);
writeOutput(result);
if (!result.exists) {
process.exit(1);
}
});
});
cas
.command("refs")
.description("List direct CAS references from a node")
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasRefs(storageRoot, hash));
});
});
cas
.command("walk")
.description("Recursive traversal from a node")
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasWalk(storageRoot, hash));
});
});
cas
.command("reindex")
.description("Rebuild type index from all CAS nodes")
.action(() => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasReindex(storageRoot));
});
});
const casSchema = cas.command("schema").description("CAS schema operations");
casSchema
.command("list")
.description("List all registered schemas")
.action(() => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasSchemaList(storageRoot));
});
});
casSchema
.command("get")
.description("Show a schema by its type hash")
.argument("<hash>", "Schema type hash")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasSchemaGet(storageRoot, hash));
});
});
const log = program.command("log").description("Process-level debug logs");
log
-136
View File
@@ -1,136 +0,0 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { JSONSchema, Store } from "@ocas/core";
import { bootstrap, getSchema, putSchema, refs, walk } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import { TEXT_SCHEMA } from "../schemas.js";
// ---- Helpers ----
function openStore(storageRoot: string): Store {
return createFsStore(join(storageRoot, "cas"));
}
function readJsonArg(fileOrInline: string): unknown {
try {
return JSON.parse(fileOrInline);
} catch {
try {
return JSON.parse(readFileSync(fileOrInline, "utf-8"));
} catch (e) {
throw new Error(`Cannot parse JSON from "${fileOrInline}": ${e}`);
}
}
}
// ---- Commands (all return JSON-serializable data) ----
export async function cmdCasGet(
storageRoot: string,
hash: string,
opts: { timestamp?: boolean },
): Promise<unknown> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
if (opts.timestamp) {
return node;
}
const { timestamp: _, ...rest } = node as Record<string, unknown>;
return rest;
}
export async function cmdCasPut(
storageRoot: string,
typeHash: string,
data: string,
): Promise<{ hash: string }> {
const store = openStore(storageRoot);
const payload = readJsonArg(data);
const hash = await store.put(typeHash, payload);
return { hash };
}
export async function cmdCasHas(storageRoot: string, hash: string): Promise<{ exists: boolean }> {
const store = openStore(storageRoot);
return { exists: store.has(hash) };
}
export async function cmdCasRefs(storageRoot: string, hash: string): Promise<{ refs: string[] }> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
return { refs: refs(store, node) };
}
export async function cmdCasWalk(storageRoot: string, hash: string): Promise<{ hashes: string[] }> {
const store = openStore(storageRoot);
const result: string[] = [];
walk(store, hash, (h) => {
result.push(h);
});
return { hashes: result };
}
export type SchemaListEntry = {
hash: string;
title: string;
};
export async function cmdCasSchemaList(storageRoot: string): Promise<SchemaListEntry[]> {
const store = openStore(storageRoot);
const aliases = await bootstrap(store);
const metaHash = aliases["@ocas/schema"];
if (metaHash === undefined) {
throw new Error("Meta-schema not found in bootstrap result");
}
const entries: SchemaListEntry[] = [];
// Include meta-schema itself
entries.push({ hash: metaHash, title: "(meta-schema)" });
for (const { hash } of store.listByType(metaHash)) {
if (hash === metaHash) continue;
const node = store.get(hash);
if (node !== null) {
const schema = node.payload as JSONSchema;
const title =
(schema.title as string | undefined) ??
(schema.description as string | undefined) ??
"(unnamed)";
entries.push({ hash, title });
}
}
return entries;
}
export async function cmdCasReindex(storageRoot: string): Promise<{ status: string }> {
const indexDir = join(storageRoot, "cas", "_index");
const { rmSync } = await import("node:fs");
rmSync(indexDir, { recursive: true, force: true });
// Re-open store to trigger migration rebuild
openStore(storageRoot);
return { status: "reindexed" };
}
export async function cmdCasSchemaGet(storageRoot: string, hash: string): Promise<unknown> {
const store = openStore(storageRoot);
const schema = getSchema(store, hash);
if (schema === null) {
throw new Error(`Schema not found: ${hash}`);
}
return schema;
}
export async function cmdCasPutText(storageRoot: string, text: string): Promise<{ hash: string }> {
const store = openStore(storageRoot);
const typeHash = await putSchema(store, TEXT_SCHEMA);
const hash = await store.put(typeHash, text);
return { hash };
}
+13 -12
View File
@@ -36,29 +36,30 @@ If the engine cannot parse your frontmatter, it will ask you to retry (up to 2 t
## 2. CAS (Content-Addressable Store)
Your frontmatter output is automatically stored in CAS. You can also **use CAS directly** to store intermediate artifacts, build merkle DAGs for large outputs, or reference data from previous steps.
Your frontmatter output is automatically stored in CAS. You can also **use CAS directly** via the \`ocas\` CLI to store intermediate artifacts, build merkle DAGs for large outputs, or reference data from previous steps.
### Commands
\`\`\`
uwf cas put-text <text> # store plain text, print hash
uwf cas put <type-hash> <json> # store typed JSON data, print hash
uwf cas get <hash> # read a CAS node (type + payload)
uwf cas has <hash> # check if a hash exists
uwf cas refs <hash> # list direct references from a node
uwf cas walk <hash> # recursive traversal from a node
uwf cas schema list # list registered schemas
uwf cas schema get <hash> # show a schema definition
ocas put <type-hash> <json> # store typed JSON data, print hash
ocas get <hash> # read a CAS node (type + payload)
ocas has <hash> # check if a hash exists
ocas refs <hash> # list direct references from a node
ocas walk <hash> # recursive traversal from a node
ocas schema list # list registered schemas
ocas schema get <hash> # show a schema definition
\`\`\`
Plain-text storage for agent output is handled internally by the uwf pipeline — agents do not need to call \`ocas put\` for their deliverables.
### Merkle DAG Pattern
For large outputs, store parts individually and reference their hashes:
\`\`\`bash
# Store individual sections
HASH1=$(uwf cas put-text "section 1 content")
HASH2=$(uwf cas put-text "section 2 content")
# Store individual sections (use ocas put with the appropriate type hash)
HASH1=$(ocas put <type-hash> '"section 1 content"')
HASH2=$(ocas put <type-hash> '"section 2 content"')
# Reference hashes in your frontmatter or in a parent node
\`\`\`
@@ -161,7 +161,7 @@ uwf step list <thread-id>
uwf step show <step-hash>
# Check the CAS data
uwf cas get <output-hash>
ocas get <output-hash>
\`\`\`
### Validation Checklist
+11 -11
View File
@@ -49,19 +49,19 @@ uwf step fork <step-hash> # fork a thread from a specific step
## CAS Commands
Use the \`ocas\` CLI for direct CAS operations (\`~/.ocas/\` store, shared with \`uwf\`):
\`\`\`
uwf cas get <hash> # read a CAS node (type + payload)
ocas get <hash> # read a CAS node (type + payload)
[--timestamp] # include timestamp in output
uwf cas put <type-hash> <data> # store a node, print its hash
# <data>: JSON file path or inline JSON string
uwf cas put-text <text> # store a plain text string, print its hash
# shortcut for put with the built-in text schema
uwf cas has <hash> # check if a hash exists
uwf cas refs <hash> # list direct CAS references from a node
uwf cas walk <hash> # recursive traversal from a node
uwf cas reindex # rebuild type index from all CAS nodes
uwf cas schema list # list all registered schemas
uwf cas schema get <hash> # show a schema by its type hash
ocas put <type-hash> <data> # store a node, print its hash
# <data>: JSON file path or inline JSON string
ocas has <hash> # check if a hash exists
ocas refs <hash> # list direct CAS references from a node
ocas walk <hash> # recursive traversal from a node
ocas reindex # rebuild type index from all CAS nodes
ocas schema list # list all registered schemas
ocas schema get <hash> # show a schema by its type hash
\`\`\`
## Log Commands
+10 -9
View File
@@ -91,17 +91,18 @@ Forking creates a new thread that shares history up to the fork point — useful
## CAS Commands
Use the \`ocas\` CLI for direct CAS operations (\`~/.ocas/\` store, shared with \`uwf\`):
\`\`\`
uwf cas get <hash> # read a node (type + payload)
ocas get <hash> # read a node (type + payload)
[--timestamp] # include timestamp
uwf cas put <type-hash> <data> # store typed JSON, print hash
uwf cas put-text <text> # store plain text, print hash
uwf cas has <hash> # check existence
uwf cas refs <hash> # list direct references
uwf cas walk <hash> # recursive traversal
uwf cas reindex # rebuild type index
uwf cas schema list # list schemas
uwf cas schema get <hash> # show schema definition
ocas put <type-hash> <data> # store typed JSON, print hash
ocas has <hash> # check existence
ocas refs <hash> # list direct references
ocas walk <hash> # recursive traversal
ocas reindex # rebuild type index
ocas schema list # list schemas
ocas schema get <hash> # show schema definition
\`\`\`
## Log Commands
+6 -5
View File
@@ -84,8 +84,9 @@ REAL_HOME="${6:-$HOME}"
export HOME="$REAL_HOME"
export PATH="$REAL_HOME/.bun/bin:$REAL_HOME/.hermes/hermes-agent/venv/bin:$REAL_HOME/.local/share/npm/bin:$PATH"
# Resolve uwf
# Resolve uwf and ocas
UWF="bun $REPO_DIR/packages/cli-workflow/src/cli.ts"
OCAS="ocas"
PASS=0
FAIL=0
@@ -267,14 +268,14 @@ run_test "thread read produces output" bash -c "[ -n '$OUT' ]"
# CAS operations
if [ -n "$LAST_STEP" ]; then
OUT=$(run_test "uwf cas get" bash -c "$UWF cas get $LAST_STEP")
OUT=$(run_test "ocas get" bash -c "$OCAS get $LAST_STEP")
run_test "cas get returns type" bash -c "echo '$OUT' | jq -e '.type'"
OUT=$(run_test "uwf cas has" bash -c "$UWF cas has $LAST_STEP")
OUT=$(run_test "ocas has" bash -c "$OCAS has $LAST_STEP")
OUT=$(run_test "uwf cas refs" bash -c "$UWF cas refs $LAST_STEP")
OUT=$(run_test "ocas refs" bash -c "$OCAS refs $LAST_STEP")
OUT=$(run_test "uwf cas walk" bash -c "$UWF cas walk $LAST_STEP")
OUT=$(run_test "ocas walk" bash -c "$OCAS walk $LAST_STEP")
run_test "cas walk returns nodes" bash -c "echo '$OUT' | jq -e 'length > 0'"
fi
+3 -3
View File
@@ -29,7 +29,7 @@ roles:
After producing the test spec:
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
1. The test spec is stored in CAS automatically by the uwf pipeline (agents do not need to call `ocas put` directly)
2. Put the hash in frontmatter.plan (required when $status=ready)
@@ -79,7 +79,7 @@ roles:
\ 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\
\nThen implement TDD:\n6. Read the test spec from CAS: `ocas 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"
@@ -192,7 +192,7 @@ roles:
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\
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: `ocas 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).