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
+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