diff --git a/CLAUDE.md b/CLAUDE.md index f7702cf..af30ad8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/workflow/cas/ — Workflow stored as CAS node +~/.uncaged/json-cas/ — Workflow stored as CAS node (unified CAS store) ~/.uncaged/workflow/registry.yaml — name → hash mapping updated │ uwf thread start -p "..." ▼ diff --git a/packages/cli-workflow/README.md b/packages/cli-workflow/README.md index 92f7eaf..2f9dc92 100644 --- a/packages/cli-workflow/README.md +++ b/packages/cli-workflow/README.md @@ -209,4 +209,13 @@ 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/workflow/cas/` | Content-addressed node storage | +| `~/.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` | + diff --git a/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts b/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts index 250241d..711ddb7 100644 --- a/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts +++ b/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts @@ -129,7 +129,11 @@ describe("C1: adapter JSON round-trip integration", () => { { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env, WORKFLOW_STORAGE_ROOT: tmpDir }, + env: { + ...process.env, + WORKFLOW_STORAGE_ROOT: tmpDir, + UNCAGED_CAS_DIR: casDir, + }, cwd: tmpDir, timeout: 30000, }, diff --git a/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts b/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts index dc53769..065eb7c 100644 --- a/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts +++ b/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts @@ -6,14 +6,22 @@ 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"); @@ -21,6 +29,13 @@ 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 = { @@ -32,7 +47,11 @@ type ExecResult = { function execUwf(args: string[]): ExecResult { try { const stdout = execSync(`bun ${uwfPath} ${args.join(" ")}`, { - env: { ...process.env, WORKFLOW_STORAGE_ROOT: storageRoot }, + env: { + ...process.env, + WORKFLOW_STORAGE_ROOT: storageRoot, + UNCAGED_CAS_DIR: casDir, + }, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); diff --git a/packages/cli-workflow/src/__tests__/current-role.test.ts b/packages/cli-workflow/src/__tests__/current-role.test.ts index 05652e2..3a4d9bd 100644 --- a/packages/cli-workflow/src/__tests__/current-role.test.ts +++ b/packages/cli-workflow/src/__tests__/current-role.test.ts @@ -206,6 +206,8 @@ async function insertStepNode( describe("currentRole field", () => { let tmpDir: string; let storageRoot: string; + let casDir: string; + let originalEnv: string | undefined; async function setup() { tmpDir = join( @@ -213,13 +215,25 @@ 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 diff --git a/packages/cli-workflow/src/__tests__/step-read.test.ts b/packages/cli-workflow/src/__tests__/step-read.test.ts index d16d45d..2e9fae9 100644 --- a/packages/cli-workflow/src/__tests__/step-read.test.ts +++ b/packages/cli-workflow/src/__tests__/step-read.test.ts @@ -66,13 +66,21 @@ 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 ─────────────────────────────────────────────────────────── @@ -80,7 +88,10 @@ 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); @@ -166,7 +177,9 @@ 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); @@ -250,7 +263,9 @@ 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); @@ -325,7 +340,9 @@ 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); @@ -384,7 +401,9 @@ 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); @@ -460,7 +479,9 @@ 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); @@ -532,7 +553,9 @@ 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); diff --git a/packages/cli-workflow/src/__tests__/step-show-json.test.ts b/packages/cli-workflow/src/__tests__/step-show-json.test.ts index feb505f..26eeaaf 100644 --- a/packages/cli-workflow/src/__tests__/step-show-json.test.ts +++ b/packages/cli-workflow/src/__tests__/step-show-json.test.ts @@ -125,15 +125,23 @@ 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 () => { diff --git a/packages/cli-workflow/src/__tests__/step-timing.test.ts b/packages/cli-workflow/src/__tests__/step-timing.test.ts index 8fe713b..24ae758 100644 --- a/packages/cli-workflow/src/__tests__/step-timing.test.ts +++ b/packages/cli-workflow/src/__tests__/step-timing.test.ts @@ -63,13 +63,22 @@ async function registerDetailSchemas(store: ReturnType) { // ── 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) ───────────────────────────────────────── diff --git a/packages/cli-workflow/src/__tests__/store-global-cas.test.ts b/packages/cli-workflow/src/__tests__/store-global-cas.test.ts new file mode 100644 index 0000000..07aacbf --- /dev/null +++ b/packages/cli-workflow/src/__tests__/store-global-cas.test.ts @@ -0,0 +1,224 @@ +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(); + }); +}); diff --git a/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts b/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts index 2d7ee2d..e69328d 100644 --- a/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts @@ -15,6 +15,8 @@ import { appendThreadHistory, createUwfStore, saveThreadsIndex } from "../store. async function makeUwfStore(storageRoot: string): Promise { 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); } diff --git a/packages/cli-workflow/src/__tests__/thread-location.test.ts b/packages/cli-workflow/src/__tests__/thread-location.test.ts index 10f5230..bf297bb 100644 --- a/packages/cli-workflow/src/__tests__/thread-location.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-location.test.ts @@ -9,17 +9,31 @@ 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 () => { diff --git a/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts b/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts index 2634863..17b8585 100644 --- a/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts @@ -67,13 +67,22 @@ 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 ───────────────────────────────────────────── @@ -143,7 +152,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } @@ -339,7 +348,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } @@ -497,7 +506,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } @@ -579,7 +588,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } diff --git a/packages/cli-workflow/src/__tests__/thread-read-xml-tags.test.ts b/packages/cli-workflow/src/__tests__/thread-read-xml-tags.test.ts index 0772ddc..f28f323 100644 --- a/packages/cli-workflow/src/__tests__/thread-read-xml-tags.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-read-xml-tags.test.ts @@ -53,6 +53,8 @@ const DETAIL_SCHEMA = { async function makeUwfStore(storageRoot: string): Promise { 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 }; @@ -696,7 +698,7 @@ describe("thread read XML tag isolation", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, })) as CasRef; steps.push(step); prev = step; diff --git a/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts b/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts index 27a35c8..2da3ff0 100644 --- a/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts @@ -10,17 +10,31 @@ 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 { @@ -123,7 +137,7 @@ graph: // Register the workflow execFileSync("node", [uwfBin, "workflow", "add", workflowPath], { - env: { ...process.env, UWF_STORAGE_ROOT: storageRoot }, + env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, UNCAGED_CAS_DIR: casDir }, encoding: "utf8", }); @@ -132,7 +146,7 @@ graph: "node", [uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd], { - env: { ...process.env, UWF_STORAGE_ROOT: storageRoot }, + env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, UNCAGED_CAS_DIR: casDir }, encoding: "utf8", }, ); diff --git a/packages/cli-workflow/src/__tests__/thread.test.ts b/packages/cli-workflow/src/__tests__/thread.test.ts index b77f27d..d93b34b 100644 --- a/packages/cli-workflow/src/__tests__/thread.test.ts +++ b/packages/cli-workflow/src/__tests__/thread.test.ts @@ -58,6 +58,8 @@ const DETAIL_SCHEMA = { async function makeUwfStore(storageRoot: string): Promise { 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 }; diff --git a/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts b/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts index d3c5d12..9c38ae0 100644 --- a/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts +++ b/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts @@ -15,6 +15,8 @@ import { loadWorkflowRegistry, saveWorkflowRegistry } from "../store.js"; async function makeUwfStore(storageRoot: string): Promise { 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 }; diff --git a/packages/cli-workflow/src/cli.ts b/packages/cli-workflow/src/cli.ts index 6ca57b2..81e1ff0 100755 --- a/packages/cli-workflow/src/cli.ts +++ b/packages/cli-workflow/src/cli.ts @@ -373,7 +373,12 @@ 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, + opts.prompt === true, + ); process.stdout.write(markdown.endsWith("\n") ? markdown : `${markdown}\n`); }); }); diff --git a/packages/cli-workflow/src/commands/skill.ts b/packages/cli-workflow/src/commands/skill.ts index 5e53485..1388ce0 100644 --- a/packages/cli-workflow/src/commands/skill.ts +++ b/packages/cli-workflow/src/commands/skill.ts @@ -1,7 +1,7 @@ export { - generateBootstrapReference as cmdSkillBootstrap, generateAdapterReference as cmdSkillAdapter, generateAuthorReference as cmdSkillAuthor, + generateBootstrapReference as cmdSkillBootstrap, generateDeveloperReference as cmdSkillDeveloper, generateUserReference as cmdSkillUser, } from "@uncaged/workflow-util"; diff --git a/packages/cli-workflow/src/commands/step.ts b/packages/cli-workflow/src/commands/step.ts index 334e2f9..c0c1ec5 100644 --- a/packages/cli-workflow/src/commands/step.ts +++ b/packages/cli-workflow/src/commands/step.ts @@ -311,7 +311,10 @@ export async function cmdStepRead( 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); + 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}`; } diff --git a/packages/cli-workflow/src/store.ts b/packages/cli-workflow/src/store.ts index 3016ab5..be4dec2 100644 --- a/packages/cli-workflow/src/store.ts +++ b/packages/cli-workflow/src/store.ts @@ -70,10 +70,26 @@ 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"); } @@ -98,7 +114,7 @@ export type UwfStore = { }; export async function createUwfStore(storageRoot: string): Promise { - const casDir = getCasDir(storageRoot); + const casDir = getGlobalCasDir(); await mkdir(casDir, { recursive: true }); const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); diff --git a/packages/workflow-agent-builtin/src/agent.ts b/packages/workflow-agent-builtin/src/agent.ts index 2486956..ee6cb85 100644 --- a/packages/workflow-agent-builtin/src/agent.ts +++ b/packages/workflow-agent-builtin/src/agent.ts @@ -94,7 +94,12 @@ async function runBuiltinWithMessages( session.startedAtMs, ); - return { output: stripPreamble(loopResult.finalText), detailHash, sessionId: session.sessionId, assembledPrompt: "" }; + return { + output: stripPreamble(loopResult.finalText), + detailHash, + sessionId: session.sessionId, + assembledPrompt: "", + }; } async function runBuiltin(ctx: AgentContext): Promise { diff --git a/packages/workflow-agent-claude-code/src/claude-code.ts b/packages/workflow-agent-claude-code/src/claude-code.ts index d8bc1f7..cdbe191 100644 --- a/packages/workflow-agent-claude-code/src/claude-code.ts +++ b/packages/workflow-agent-claude-code/src/claude-code.ts @@ -120,7 +120,11 @@ function spawnClaudeResume( return spawnClaude(args); } -async function processClaudeOutput(stdout: string, store: Store, assembledPrompt: string): Promise { +async function processClaudeOutput( + stdout: string, + store: Store, + assembledPrompt: string, +): Promise { const parsed = parseClaudeCodeStreamOutput(stdout); if (parsed !== null) { diff --git a/packages/workflow-dashboard/src/editor/model/edit-node-view.ts b/packages/workflow-dashboard/src/editor/model/edit-node-view.ts index 38859fb..18b66a0 100644 --- a/packages/workflow-dashboard/src/editor/model/edit-node-view.ts +++ b/packages/workflow-dashboard/src/editor/model/edit-node-view.ts @@ -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 || node.type !== "role") return; + if (node?.type !== "role") return; set({ node: node as WorkNode<"role"> }); } diff --git a/packages/workflow-dashboard/src/editor/trans/trans-out.ts b/packages/workflow-dashboard/src/editor/trans/trans-out.ts index 7ea613d..a0e94e4 100644 --- a/packages/workflow-dashboard/src/editor/trans/trans-out.ts +++ b/packages/workflow-dashboard/src/editor/trans/trans-out.ts @@ -40,7 +40,7 @@ function traverse( visited.add(nodeId); const node = nodeMap.get(nodeId); - if (!node || node.type !== "role") return; + if (node?.type !== "role") return; const roleNode = node as WorkNode<"role">; const outEdges = outgoingEdges.get(nodeId) ?? []; diff --git a/packages/workflow-protocol/src/__tests__/types.test.ts b/packages/workflow-protocol/src/__tests__/types.test.ts index 444ee06..ed8a1bc 100644 --- a/packages/workflow-protocol/src/__tests__/types.test.ts +++ b/packages/workflow-protocol/src/__tests__/types.test.ts @@ -25,7 +25,7 @@ describe("Protocol types for thread/edge location", () => { edgePrompt: "Plan the implementation", startedAtMs: Date.now(), completedAtMs: Date.now() + 1000, - assembledPrompt: null, + assembledPrompt: null, cwd: "/home/user/project", }; diff --git a/packages/workflow-util/src/index.ts b/packages/workflow-util/src/index.ts index 6eac6ce..55c7780 100644 --- a/packages/workflow-util/src/index.ts +++ b/packages/workflow-util/src/index.ts @@ -1,9 +1,9 @@ export { generateActorReference } from "./actor-reference.js"; -export { generateBootstrapReference } from "./bootstrap-reference.js"; 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";