Merge pull request 'refactor: remove uwf cas subcommand, use ocas CLI' (#15) from refactor/remove-uwf-cas into main
CI / check (push) Failing after 9m21s

This commit was merged in pull request #15.
This commit is contained in:
2026-06-02 13:45:48 +00:00
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 4. `uwf thread read <threadId>` — verify non-empty output
CAS operations: CAS operations:
5. `uwf cas get <lastStepHash>` — verify returns a type field 5. `ocas get <lastStepHash>` — verify returns a type field
6. `uwf cas has <lastStepHash>` — verify exits 0 6. `ocas has <lastStepHash>` — verify exits 0
7. `uwf cas refs <lastStepHash>` — list refs (may be empty) 7. `ocas refs <lastStepHash>` — list refs (may be empty)
8. `uwf cas walk <lastStepHash>` — verify returns non-empty array 8. `ocas walk <lastStepHash>` — verify returns non-empty array
Report results. Pass threadId, lastStepHash, workflowName, containerName forward. Report results. Pass threadId, lastStepHash, workflowName, containerName forward.
output: "Report test results. Set $status to pass (with threadId, lastStepHash, workflowName, containerName) or fail." 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 2. Revise the test spec accordingly
After producing the test spec: 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) 2. Put the plan hash in frontmatter.plan (required when $status=ready)
3. Set repoPath to the absolute path of the repository root 3. Set repoPath to the absolute path of the repository root
IMPORTANT: Extract the repo remote (owner/repo) from git: IMPORTANT: Extract the repo remote (owner/repo) from git:
@@ -63,7 +63,7 @@ roles:
5. ALL subsequent work must happen inside the worktree directory. 5. ALL subsequent work must happen inside the worktree directory.
Then implement TDD: 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 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 8. Write tests first based on the spec
9. Implement the code to make tests pass 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. The worktree path is provided in your task prompt. cd into it first.
1. Run `bun test` for automated test verification 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 3. Verify each scenario in the spec is covered and passing
4. Determine outcome: 4. Determine outcome:
- passed: all scenarios verified, tests pass - passed: all scenarios verified, tests pass
+10 -8
View File
@@ -447,16 +447,18 @@ Binary: `uwf`
### CAS commands ### CAS commands
Use the `ocas` CLI for direct CAS operations (`~/.ocas/` store, shared with `uwf`):
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `uwf cas get <hash>` | Read a CAS node. | | `ocas get <hash>` | Read a CAS node. |
| `uwf cas put <type-hash> <data>` | Store a node, print its hash. | | `ocas put <type-hash> <data>` | Store a node, print its hash. |
| `uwf cas has <hash>` | Check if a hash exists. | | `ocas has <hash>` | Check if a hash exists. |
| `uwf cas refs <hash>` | List direct CAS references. | | `ocas refs <hash>` | List direct CAS references. |
| `uwf cas walk <hash>` | Recursive traversal from a node. | | `ocas walk <hash>` | Recursive traversal from a node. |
| `uwf cas reindex` | Rebuild type index from all nodes. | | `ocas reindex` | Rebuild type index from all nodes. |
| `uwf cas schema list` | List registered schemas. | | `ocas schema list` | List registered schemas. |
| `uwf cas schema get <hash>` | Show a schema by type hash. | | `ocas schema get <hash>` | Show a schema by type hash. |
### Setup ### Setup
+5 -5
View File
@@ -22,7 +22,7 @@ roles:
3. Assess whether the issue has enough information to produce a test spec 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 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 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 7. Output **$status=ready** with plan hash and repoPath
**Mode B — Continue on existing PR (prompt mentions PR, branch, or review feedback):** **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>` 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 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 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 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 8. Output **$status=continue** with plan hash, repoPath, branch name, and worktree path
**Mode C — Bounced back by tester (fix_spec):** **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 1. Read the tester's output from the previous step to understand what's wrong with the spec
2. Revise the test spec accordingly 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 4. Output **$status=ready** with plan hash and repoPath
IMPORTANT: Extract the repo remote (owner/repo) from git: IMPORTANT: Extract the repo remote (owner/repo) from git:
@@ -91,7 +91,7 @@ roles:
6. ALL subsequent work must happen inside the worktree directory. 6. ALL subsequent work must happen inside the worktree directory.
Then implement TDD: 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 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 8. Write tests first based on the spec
9. Implement the code to make tests pass 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. The worktree path is provided in your task prompt. cd into it first.
1. Run `bun test` for automated test verification 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 3. Verify each scenario in the spec is covered and passing
4. Determine outcome: 4. Determine outcome:
- passed: all scenarios verified, tests pass - passed: all scenarios verified, tests pass
+10 -9
View File
@@ -99,17 +99,18 @@ uwf step fork 32GCDE899RRQ3
### CAS ### CAS
Use the [`ocas`](https://www.npmjs.com/package/@ocas/cli) CLI for direct CAS operations (`~/.ocas/` store, shared with `uwf`):
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `uwf cas get <hash> [--timestamp]` | Read a CAS node | | `ocas get <hash> [--timestamp]` | Read a CAS node |
| `uwf cas put <type-hash> <data>` | Store a node, print hash | | `ocas put <type-hash> <data>` | Store a node, print hash |
| `uwf cas put-text <text>` | Store plain text, print hash | | `ocas has <hash>` | Check existence |
| `uwf cas has <hash>` | Check existence | | `ocas refs <hash>` | List direct references |
| `uwf cas refs <hash>` | List direct references | | `ocas walk <hash>` | Recursive traversal |
| `uwf cas walk <hash>` | Recursive traversal | | `ocas reindex` | Rebuild type index |
| `uwf cas reindex` | Rebuild type index | | `ocas schema list` | List registered schemas |
| `uwf cas schema list` | List registered schemas | | `ocas schema get <hash>` | Show a schema |
| `uwf cas schema get <hash>` | Show a schema |
### Setup ### 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 type { CasRef, ThreadId, ThreadStatus } from "@united-workforce/protocol";
import { Command } from "commander"; 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 { cmdConfigGet, cmdConfigList, cmdConfigSet } from "./commands/config.js";
import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js"; import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js";
import { 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"); const log = program.command("log").description("Process-level debug logs");
log 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) ## 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 ### Commands
\`\`\` \`\`\`
uwf cas put-text <text> # store plain text, print hash ocas put <type-hash> <json> # store typed JSON data, print hash
uwf cas put <type-hash> <json> # store typed JSON data, print hash ocas get <hash> # read a CAS node (type + payload)
uwf cas get <hash> # read a CAS node (type + payload) ocas has <hash> # check if a hash exists
uwf cas has <hash> # check if a hash exists ocas refs <hash> # list direct references from a node
uwf cas refs <hash> # list direct references from a node ocas walk <hash> # recursive traversal from a node
uwf cas walk <hash> # recursive traversal from a node ocas schema list # list registered schemas
uwf cas schema list # list registered schemas ocas schema get <hash> # show a schema definition
uwf cas 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 ### Merkle DAG Pattern
For large outputs, store parts individually and reference their hashes: For large outputs, store parts individually and reference their hashes:
\`\`\`bash \`\`\`bash
# Store individual sections # Store individual sections (use ocas put with the appropriate type hash)
HASH1=$(uwf cas put-text "section 1 content") HASH1=$(ocas put <type-hash> '"section 1 content"')
HASH2=$(uwf cas put-text "section 2 content") HASH2=$(ocas put <type-hash> '"section 2 content"')
# Reference hashes in your frontmatter or in a parent node # Reference hashes in your frontmatter or in a parent node
\`\`\` \`\`\`
@@ -161,7 +161,7 @@ uwf step list <thread-id>
uwf step show <step-hash> uwf step show <step-hash>
# Check the CAS data # Check the CAS data
uwf cas get <output-hash> ocas get <output-hash>
\`\`\` \`\`\`
### Validation Checklist ### Validation Checklist
+10 -10
View File
@@ -49,19 +49,19 @@ uwf step fork <step-hash> # fork a thread from a specific step
## CAS Commands ## 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 [--timestamp] # include timestamp in output
uwf cas put <type-hash> <data> # store a node, print its hash ocas put <type-hash> <data> # store a node, print its hash
# <data>: JSON file path or inline JSON string # <data>: JSON file path or inline JSON string
uwf cas put-text <text> # store a plain text string, print its hash ocas has <hash> # check if a hash exists
# shortcut for put with the built-in text schema ocas refs <hash> # list direct CAS references from a node
uwf cas has <hash> # check if a hash exists ocas walk <hash> # recursive traversal from a node
uwf cas refs <hash> # list direct CAS references from a node ocas reindex # rebuild type index from all CAS nodes
uwf cas walk <hash> # recursive traversal from a node ocas schema list # list all registered schemas
uwf cas reindex # rebuild type index from all CAS nodes ocas schema get <hash> # show a schema by its type hash
uwf cas schema list # list all registered schemas
uwf cas schema get <hash> # show a schema by its type hash
\`\`\` \`\`\`
## Log Commands ## 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 ## 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 [--timestamp] # include timestamp
uwf cas put <type-hash> <data> # store typed JSON, print hash ocas put <type-hash> <data> # store typed JSON, print hash
uwf cas put-text <text> # store plain text, print hash ocas has <hash> # check existence
uwf cas has <hash> # check existence ocas refs <hash> # list direct references
uwf cas refs <hash> # list direct references ocas walk <hash> # recursive traversal
uwf cas walk <hash> # recursive traversal ocas reindex # rebuild type index
uwf cas reindex # rebuild type index ocas schema list # list schemas
uwf cas schema list # list schemas ocas schema get <hash> # show schema definition
uwf cas schema get <hash> # show schema definition
\`\`\` \`\`\`
## Log Commands ## Log Commands
+6 -5
View File
@@ -84,8 +84,9 @@ REAL_HOME="${6:-$HOME}"
export HOME="$REAL_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" 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" UWF="bun $REPO_DIR/packages/cli-workflow/src/cli.ts"
OCAS="ocas"
PASS=0 PASS=0
FAIL=0 FAIL=0
@@ -267,14 +268,14 @@ run_test "thread read produces output" bash -c "[ -n '$OUT' ]"
# CAS operations # CAS operations
if [ -n "$LAST_STEP" ]; then 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'" 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'" run_test "cas walk returns nodes" bash -c "echo '$OUT' | jq -e 'length > 0'"
fi fi
+3 -3
View File
@@ -29,7 +29,7 @@ roles:
After producing the test spec: 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) 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>\ \ 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\ \ -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\ \ 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.\ \ 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\ \ 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" \ 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. goal: You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec.
capabilities: capabilities:
- testing - 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:\ \ 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" \ 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). output: Report test results per scenario. Set $status to passed (with branch/worktree), fix_code (with report), or fix_spec (with report).