From 6b7636b08850500904d767d5aa1a775af012950f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 4 Jun 2026 05:12:05 +0000 Subject: [PATCH] refactor: unify env vars (UWF_HOME, OCAS_HOME) + env only in CLI (#37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking changes: - UWF_STORAGE_ROOT → UWF_HOME - WORKFLOW_STORAGE_ROOT removed (no fallback) - OCAS_DIR → OCAS_HOME (aligned with ocas CLI) Library functions no longer read process.env: - util-agent/storage.ts: resolveStorageRoot(override), getGlobalCasDir(override) - agent-hermes: isResumeDisabled(flag) pure function, CLI reads env - agent-claude-code: CLI reads CLAUDE_MODEL and passes to agent Fixes #37 --- .workflows/e2e-walkthrough.yaml | 16 +-- .../agent-builtin/__tests__/prompt.test.ts | 2 + packages/agent-builtin/src/agent.ts | 7 +- .../__tests__/claude-code.test.ts | 2 + packages/agent-claude-code/src/claude-code.ts | 53 ++++++--- packages/agent-claude-code/src/cli.ts | 3 +- .../__tests__/hermes-prompt.test.ts | 2 + packages/agent-hermes/src/cli.ts | 4 +- packages/agent-hermes/src/hermes.ts | 15 +-- packages/agent-hermes/src/session-cache.ts | 36 +++--- packages/cli/README.md | 5 +- .../__tests__/adapter-json-roundtrip.test.ts | 6 +- .../cli/src/__tests__/current-role.test.ts | 10 +- .../src/__tests__/resolve-head-hash.test.ts | 2 +- packages/cli/src/__tests__/step-read.test.ts | 36 +++--- .../cli/src/__tests__/step-show-json.test.ts | 8 +- .../cli/src/__tests__/step-timing.test.ts | 10 +- .../src/__tests__/store-global-cas.test.ts | 36 +++--- .../src/__tests__/store-storage-root.test.ts | 16 ++- .../__tests__/thread-cancel-status.test.ts | 2 +- .../src/__tests__/thread-list-filters.test.ts | 4 +- .../cli/src/__tests__/thread-location.test.ts | 10 +- .../src/__tests__/thread-read-quota.test.ts | 10 +- .../__tests__/thread-read-xml-tags.test.ts | 2 +- .../cli/src/__tests__/thread-resume.test.ts | 40 +++---- .../src/__tests__/thread-show-status.test.ts | 8 +- .../__tests__/thread-start-cwd-cli.test.ts | 14 +-- .../src/__tests__/thread-step-count.test.ts | 2 +- .../src/__tests__/thread-suspend-step.test.ts | 12 +- .../thread-suspended-display.test.ts | 24 ++-- packages/cli/src/__tests__/thread.test.ts | 2 +- .../src/__tests__/workflow-resolution.test.ts | 2 +- packages/cli/src/store.ts | 12 +- packages/util-agent/README.md | 26 ++++- .../__tests__/session-cache.test.ts | 108 ++++++++++-------- packages/util-agent/__tests__/storage.test.ts | 57 +++------ packages/util-agent/src/context.ts | 20 ++-- packages/util-agent/src/extract.ts | 8 +- packages/util-agent/src/run.ts | 20 +++- packages/util-agent/src/session-cache.ts | 26 +++-- packages/util-agent/src/storage.ts | 33 +++--- packages/util-agent/src/types.ts | 4 + packages/util/src/user-reference.ts | 2 +- scripts/check-dev-env.sh | 2 +- scripts/e2e-walkthrough.sh | 8 +- 45 files changed, 394 insertions(+), 333 deletions(-) diff --git a/.workflows/e2e-walkthrough.yaml b/.workflows/e2e-walkthrough.yaml index 73f5476..b5b9310 100644 --- a/.workflows/e2e-walkthrough.yaml +++ b/.workflows/e2e-walkthrough.yaml @@ -17,7 +17,7 @@ roles: docker run -d --name uwf-e2e-$$ \ -v "$(pwd):/workspace:ro" \ -e HOME=/root \ - -e UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage \ + -e UWF_HOME=/tmp/uwf-e2e-storage \ --add-host=host.docker.internal:host-gateway \ -w /workspace \ node:22-bookworm \ @@ -39,7 +39,7 @@ roles: export PATH="$HOME/.bun/bin:$PATH" # Isolated storage - mkdir -p $UWF_STORAGE_ROOT + mkdir -p $UWF_HOME # Install workspace deps cd /root/workflow && bun install @@ -62,9 +62,9 @@ roles: ``` docker cp ~/.uwf/config.yaml uwf-e2e-$$:/tmp/uwf-e2e-storage/config.yaml 2>/dev/null || true docker exec uwf-e2e-$$ bash -c ' - if [ -f $UWF_STORAGE_ROOT/config.yaml ]; then + if [ -f $UWF_HOME/config.yaml ]; then sed -i "s|localhost|host.docker.internal|g; s|127\.0\.0\.1|host.docker.internal|g" \ - $UWF_STORAGE_ROOT/config.yaml + $UWF_HOME/config.yaml fi ' ``` @@ -95,7 +95,7 @@ roles: All commands use `uwf` (installed via `bun link` inside the container). Remember to set env vars in each exec: export PATH="$HOME/.bun/bin:$PATH" - export UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage + export UWF_HOME=/tmp/uwf-e2e-storage Config tests: 1. `uwf config list` — verify it returns valid JSON @@ -133,7 +133,7 @@ roles: procedure: | Use the container (containerName) and workflow (workflowName) from your prompt. All commands via: `docker exec bash -c '...'` - Set env: PATH="$HOME/.bun/bin:$PATH" UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage + Set env: PATH="$HOME/.bun/bin:$PATH" UWF_HOME=/tmp/uwf-e2e-storage 1. `uwf thread start -p 'E2E test: what is 2+2?'` — capture thread ID from JSON output 2. `uwf thread list` — verify the thread appears in the list @@ -166,7 +166,7 @@ roles: procedure: | Use the container (containerName) and threadId from your prompt. All commands via: `docker exec bash -c '...'` - Set env: PATH="$HOME/.bun/bin:$PATH" UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage + Set env: PATH="$HOME/.bun/bin:$PATH" UWF_HOME=/tmp/uwf-e2e-storage Step inspection: 1. `uwf step list ` — verify steps array has length > 1 @@ -208,7 +208,7 @@ roles: procedure: | Use containerName, threadId, lastStepHash, and workflowName from your prompt. All commands via: `docker exec bash -c '...'` - Set env: PATH="$HOME/.bun/bin:$PATH" UWF_STORAGE_ROOT=/tmp/uwf-e2e-storage + Set env: PATH="$HOME/.bun/bin:$PATH" UWF_HOME=/tmp/uwf-e2e-storage Cancel: 1. Start a second thread: `uwf thread start -p 'E2E cancel test'` diff --git a/packages/agent-builtin/__tests__/prompt.test.ts b/packages/agent-builtin/__tests__/prompt.test.ts index 01d228a..32c73bc 100644 --- a/packages/agent-builtin/__tests__/prompt.test.ts +++ b/packages/agent-builtin/__tests__/prompt.test.ts @@ -29,6 +29,8 @@ function minimalContext(overrides: Partial = {}): AgentContext { outputFormatInstruction: "---\nstatus: done\n---", edgePrompt: "Implement the fix described in the plan.", isFirstVisit: true, + storageRoot: "/tmp/uwf-test", + casDir: "/tmp/ocas-test", ...overrides, }; } diff --git a/packages/agent-builtin/src/agent.ts b/packages/agent-builtin/src/agent.ts index 2eb172e..6efa2ad 100644 --- a/packages/agent-builtin/src/agent.ts +++ b/packages/agent-builtin/src/agent.ts @@ -6,7 +6,6 @@ import { createAgent, loadWorkflowConfig, resolveModel, - resolveStorageRoot, } from "@united-workforce/util-agent"; import { storeBuiltinDetail } from "./detail.js"; @@ -40,6 +39,7 @@ type SessionRecord = { model: string; startedAtMs: number; messages: ChatMessage[]; + storageRoot: string; }; const sessions = new Map(); @@ -103,7 +103,7 @@ async function runBuiltinWithMessages( } async function runBuiltin(ctx: AgentContext): Promise { - const storageRoot = resolveStorageRoot(); + const storageRoot = ctx.storageRoot; const config = await loadWorkflowConfig(storageRoot); const provider = resolveModel(config, config.defaultModel); @@ -116,6 +116,7 @@ async function runBuiltin(ctx: AgentContext): Promise { model: provider.model, startedAtMs: Date.now(), messages, + storageRoot, }; sessions.set(sessionId, session); @@ -136,7 +137,7 @@ async function continueBuiltin( store: Store, ): Promise { const session = getSession(sessionId); - const storageRoot = resolveStorageRoot(); + const storageRoot = session.storageRoot; const config = await loadWorkflowConfig(storageRoot); const provider = resolveModel(config, config.defaultModel); diff --git a/packages/agent-claude-code/__tests__/claude-code.test.ts b/packages/agent-claude-code/__tests__/claude-code.test.ts index 6243255..029b758 100644 --- a/packages/agent-claude-code/__tests__/claude-code.test.ts +++ b/packages/agent-claude-code/__tests__/claude-code.test.ts @@ -27,6 +27,8 @@ function makeCtx(overrides: Partial = {}): AgentContext { steps: [], store: {} as AgentContext["store"], outputFormatInstruction: "Use YAML frontmatter", + storageRoot: "/tmp/uwf-test", + casDir: "/tmp/ocas-test", ...overrides, }; } diff --git a/packages/agent-claude-code/src/claude-code.ts b/packages/agent-claude-code/src/claude-code.ts index dfff51b..c04b0f7 100644 --- a/packages/agent-claude-code/src/claude-code.ts +++ b/packages/agent-claude-code/src/claude-code.ts @@ -17,7 +17,6 @@ const log = createLogger({ sink: { kind: "stderr" } }); const CLAUDE_COMMAND = "claude"; const CLAUDE_MAX_TURNS = 90; -const CLAUDE_MODEL = process.env.CLAUDE_MODEL ?? null; /** Assemble system prompt, task, and prior step outputs for Claude Code. */ export function buildClaudeCodePrompt(ctx: AgentContext): string { @@ -85,6 +84,7 @@ function spawnClaude( function spawnClaudeRun( prompt: string, + model: string | null, ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { const args = [ "-p", @@ -96,8 +96,8 @@ function spawnClaudeRun( "--max-turns", String(CLAUDE_MAX_TURNS), ]; - if (CLAUDE_MODEL !== null) { - args.push("--model", CLAUDE_MODEL); + if (model !== null) { + args.push("--model", model); } return spawnClaude(args); } @@ -105,6 +105,7 @@ function spawnClaudeRun( function spawnClaudeResume( sessionId: string, message: string, + model: string | null, ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { const args = [ "-p", @@ -118,8 +119,8 @@ function spawnClaudeResume( "--max-turns", String(CLAUDE_MAX_TURNS), ]; - if (CLAUDE_MODEL !== null) { - args.push("--model", CLAUDE_MODEL); + if (model !== null) { + args.push("--model", model); } return spawnClaude(args); } @@ -157,20 +158,35 @@ async function processClaudeOutput( ); } -async function runClaudeCode(ctx: AgentContext): Promise { +async function runClaudeCode(ctx: AgentContext, model: string | null): Promise { const fullPrompt = buildClaudeCodePrompt(ctx); log("K7R2M4N8", `prompt for role=${ctx.role} (length=${fullPrompt.length}):\n${fullPrompt}`); // Try resuming a cached session for re-entry scenarios (e.g. reviewer reject → developer re-entry). if (!ctx.isFirstVisit) { - const cachedSessionId = await getCachedSessionId("claude-code", ctx.threadId, ctx.role); + const cachedSessionId = await getCachedSessionId( + "claude-code", + ctx.threadId, + ctx.role, + ctx.storageRoot, + ); if (cachedSessionId !== null) { try { - const { stdout, stderr, exitCode } = await spawnClaudeResume(cachedSessionId, fullPrompt); + const { stdout, stderr, exitCode } = await spawnClaudeResume( + cachedSessionId, + fullPrompt, + model, + ); const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt); if (result.sessionId !== undefined && result.sessionId !== "") { - await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId); + await setCachedSessionId( + "claude-code", + ctx.threadId, + ctx.role, + result.sessionId, + ctx.storageRoot, + ); } return result; } catch (err) { @@ -182,10 +198,16 @@ async function runClaudeCode(ctx: AgentContext): Promise { } } - const { stdout, stderr, exitCode } = await spawnClaudeRun(fullPrompt); + const { stdout, stderr, exitCode } = await spawnClaudeRun(fullPrompt, model); const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt); if (result.sessionId !== undefined && result.sessionId !== "") { - await setCachedSessionId("claude-code", ctx.threadId, ctx.role, result.sessionId); + await setCachedSessionId( + "claude-code", + ctx.threadId, + ctx.role, + result.sessionId, + ctx.storageRoot, + ); } return result; } @@ -194,16 +216,17 @@ async function continueClaudeCode( sessionId: string, message: string, store: Store, + model: string | null, ): Promise { - const { stdout, stderr, exitCode } = await spawnClaudeResume(sessionId, message); + const { stdout, stderr, exitCode } = await spawnClaudeResume(sessionId, message, model); return processClaudeOutput(stdout, stderr, exitCode, store, ""); } /** Agent CLI factory: parses argv, runs Claude Code, extracts output, writes StepNode. */ -export function createClaudeCodeAgent(): () => Promise { +export function createClaudeCodeAgent(model: string | null): () => Promise { return createAgent({ name: "claude-code", - run: runClaudeCode, - continue: continueClaudeCode, + run: (ctx) => runClaudeCode(ctx, model), + continue: (sessionId, message, store) => continueClaudeCode(sessionId, message, store, model), }); } diff --git a/packages/agent-claude-code/src/cli.ts b/packages/agent-claude-code/src/cli.ts index 0e26393..b944e99 100644 --- a/packages/agent-claude-code/src/cli.ts +++ b/packages/agent-claude-code/src/cli.ts @@ -2,5 +2,6 @@ import { createClaudeCodeAgent } from "./claude-code.js"; -const main = createClaudeCodeAgent(); +const model = process.env.CLAUDE_MODEL ?? null; +const main = createClaudeCodeAgent(model); void main(); diff --git a/packages/agent-hermes/__tests__/hermes-prompt.test.ts b/packages/agent-hermes/__tests__/hermes-prompt.test.ts index 979eee7..3ec32cb 100644 --- a/packages/agent-hermes/__tests__/hermes-prompt.test.ts +++ b/packages/agent-hermes/__tests__/hermes-prompt.test.ts @@ -27,6 +27,8 @@ function makeCtx(overrides: Partial = {}): AgentContext { steps: [], store: {} as AgentContext["store"], outputFormatInstruction: "Use YAML frontmatter", + storageRoot: "/tmp/uwf-test", + casDir: "/tmp/ocas-test", ...overrides, }; } diff --git a/packages/agent-hermes/src/cli.ts b/packages/agent-hermes/src/cli.ts index 8e6d26e..7e60b24 100755 --- a/packages/agent-hermes/src/cli.ts +++ b/packages/agent-hermes/src/cli.ts @@ -1,6 +1,8 @@ #!/usr/bin/env node import { createHermesAgent } from "./hermes.js"; +import { isResumeDisabled } from "./session-cache.js"; -const main = createHermesAgent(); +const resumeDisabled = isResumeDisabled(process.env.UWF_HERMES_RESUME ?? null); +const main = createHermesAgent(resumeDisabled); void main(); diff --git a/packages/agent-hermes/src/hermes.ts b/packages/agent-hermes/src/hermes.ts index b7a9e6c..8191cea 100644 --- a/packages/agent-hermes/src/hermes.ts +++ b/packages/agent-hermes/src/hermes.ts @@ -9,7 +9,7 @@ import { } from "@united-workforce/util-agent"; import { HermesAcpClient } from "./acp-client.js"; -import { getCachedSessionId, isResumeDisabled, setCachedSessionId } from "./session-cache.js"; +import { getCachedSessionId, setCachedSessionId } from "./session-cache.js"; import { loadHermesSession, storeHermesSessionDetail } from "./session-detail.js"; const log = createLogger({ sink: { kind: "stderr" } }); @@ -66,13 +66,14 @@ async function prepareSession( client: HermesAcpClient, ctx: AgentContext, cwd: string, + resumeDisabled: boolean, ): Promise { - if (ctx.isFirstVisit || isResumeDisabled()) { + if (ctx.isFirstVisit || resumeDisabled) { await client.connect(cwd); return { useContinuation: false, resumed: false }; } - const cachedSessionId = await getCachedSessionId(ctx.threadId, ctx.role); + const cachedSessionId = await getCachedSessionId(ctx.threadId, ctx.role, ctx.storageRoot); if (cachedSessionId === null) { log("6RWK3N8Q", `no cached session for ${ctx.threadId}:${ctx.role}, starting new session`); await client.connect(cwd); @@ -99,7 +100,7 @@ async function prepareSession( * frontmatter retry loops keep the same Hermes session context. The client * is closed once the agent process exits (via process.on("exit")). */ -export function createHermesAgent(): () => Promise { +export function createHermesAgent(resumeDisabled: boolean): () => Promise { const client = new HermesAcpClient(); // Ensure cleanup regardless of how the process exits. @@ -113,8 +114,8 @@ export function createHermesAgent(): () => Promise { const { text, sessionId } = await client.prompt(fullPrompt); const { detailHash } = await storePromptResult(ctx.store, sessionId); - if (!isResumeDisabled()) { - await setCachedSessionId(ctx.threadId, ctx.role, sessionId); + if (!resumeDisabled) { + await setCachedSessionId(ctx.threadId, ctx.role, sessionId, ctx.storageRoot); } return { output: text, detailHash, sessionId, assembledPrompt: fullPrompt }; @@ -122,7 +123,7 @@ export function createHermesAgent(): () => Promise { async function runHermes(ctx: AgentContext): Promise { const cwd = process.cwd(); - const attempt = await prepareSession(client, ctx, cwd); + const attempt = await prepareSession(client, ctx, cwd, resumeDisabled); try { return await runPrompt(ctx, attempt.useContinuation); diff --git a/packages/agent-hermes/src/session-cache.ts b/packages/agent-hermes/src/session-cache.ts index 65d9d1d..1cf24eb 100644 --- a/packages/agent-hermes/src/session-cache.ts +++ b/packages/agent-hermes/src/session-cache.ts @@ -6,28 +6,38 @@ import { setCachedSessionId as setCachedSessionIdBase, } from "@united-workforce/util-agent"; -export async function getCachedSessionId(threadId: ThreadId, role: string): Promise { - return getCachedSessionIdBase("hermes", threadId, role); +export async function getCachedSessionId( + threadId: ThreadId, + role: string, + storageRoot: string, +): Promise { + return getCachedSessionIdBase("hermes", threadId, role, storageRoot); } export async function setCachedSessionId( threadId: ThreadId, role: string, sessionId: string, + storageRoot: string, ): Promise { - return setCachedSessionIdBase("hermes", threadId, role, sessionId); + return setCachedSessionIdBase("hermes", threadId, role, sessionId, storageRoot); } -export function isResumeDisabled(): boolean { - // Hermes ACP session/resume is broken: _restore fails for custom providers - // because resolve_runtime_provider("custom") throws and base_url/api_mode - // are lost in the fallback path. Resume silently creates a new session - // (different sessionId, no history), causing empty-text responses. - // See: https://github.com/NousResearch/hermes-agent/issues/13489 - // Disable by default until upstream fixes the bug. Set UWF_HERMES_RESUME=1 - // to opt back in. - const enableFlag = process.env.UWF_HERMES_RESUME; - if (enableFlag === "1" || enableFlag === "true") { +/** + * Decide whether Hermes session resume is disabled, given the raw + * `UWF_HERMES_RESUME` flag (read by the CLI entry point — library code must + * not read `process.env`). + * + * Hermes ACP session/resume is broken: _restore fails for custom providers + * because resolve_runtime_provider("custom") throws and base_url/api_mode + * are lost in the fallback path. Resume silently creates a new session + * (different sessionId, no history), causing empty-text responses. + * See: https://github.com/NousResearch/hermes-agent/issues/13489 + * Disable by default until upstream fixes the bug. Set UWF_HERMES_RESUME=1 + * to opt back in. + */ +export function isResumeDisabled(resumeFlag: string | null): boolean { + if (resumeFlag === "1" || resumeFlag === "true") { return false; } return true; diff --git a/packages/cli/README.md b/packages/cli/README.md index d8bc0e9..52a54cc 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -216,7 +216,6 @@ src/ | Variable | Purpose | Default | |----------|---------|---------| -| `OCAS_DIR` | Override the global CAS directory location | `~/.ocas` | -| `UWF_STORAGE_ROOT` | Internal override for workflow metadata storage | `~/.uwf` | -| `WORKFLOW_STORAGE_ROOT` | User override for workflow metadata storage | `~/.uwf` | +| `OCAS_HOME` | Override the global CAS directory location | `~/.ocas` | +| `UWF_HOME` | Override the workflow metadata storage root | `~/.uwf` | diff --git a/packages/cli/src/__tests__/adapter-json-roundtrip.test.ts b/packages/cli/src/__tests__/adapter-json-roundtrip.test.ts index 5dbcfe7..6ac7b9d 100644 --- a/packages/cli/src/__tests__/adapter-json-roundtrip.test.ts +++ b/packages/cli/src/__tests__/adapter-json-roundtrip.test.ts @@ -68,7 +68,7 @@ describe("C1: adapter JSON round-trip integration", () => { prompt: "Test round-trip task", }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; const threadId = "01ROUNDTRIPTEST0000000000" as ThreadId; await seedThreads(tmpDir, { [threadId]: startHash }); @@ -134,8 +134,8 @@ describe("C1: adapter JSON round-trip integration", () => { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, - WORKFLOW_STORAGE_ROOT: tmpDir, - OCAS_DIR: casDir, + UWF_HOME: tmpDir, + OCAS_HOME: casDir, }, cwd: tmpDir, timeout: 30000, diff --git a/packages/cli/src/__tests__/current-role.test.ts b/packages/cli/src/__tests__/current-role.test.ts index 8b69816..e93b908 100644 --- a/packages/cli/src/__tests__/current-role.test.ts +++ b/packages/cli/src/__tests__/current-role.test.ts @@ -225,9 +225,9 @@ describe("currentRole field", () => { await mkdir(storageRoot, { recursive: true }); await mkdir(casDir, { recursive: true }); - // Set OCAS_DIR for this test - originalEnv = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + // Set OCAS_HOME for this test + originalEnv = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; } async function teardown() { @@ -236,9 +236,9 @@ describe("currentRole field", () => { } // Restore original environment if (originalEnv === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalEnv; + process.env.OCAS_HOME = originalEnv; } } diff --git a/packages/cli/src/__tests__/resolve-head-hash.test.ts b/packages/cli/src/__tests__/resolve-head-hash.test.ts index b5f68f1..4926543 100644 --- a/packages/cli/src/__tests__/resolve-head-hash.test.ts +++ b/packages/cli/src/__tests__/resolve-head-hash.test.ts @@ -12,7 +12,7 @@ beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-resolve-head-")); const casDir = join(tmpDir, "cas"); await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; }); afterEach(async () => { diff --git a/packages/cli/src/__tests__/step-read.test.ts b/packages/cli/src/__tests__/step-read.test.ts index 323f771..af06d72 100644 --- a/packages/cli/src/__tests__/step-read.test.ts +++ b/packages/cli/src/__tests__/step-read.test.ts @@ -70,16 +70,16 @@ let originalEnv: string | undefined; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-read-test-")); - originalEnv = process.env.OCAS_DIR; + originalEnv = process.env.OCAS_HOME; }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); // Restore original environment if (originalEnv === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalEnv; + process.env.OCAS_HOME = originalEnv; } }); @@ -88,10 +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.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; + process.env.OCAS_HOME = casDir; const store = await openStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -177,9 +177,9 @@ describe("step read", () => { test("test 2: quota enforcement - multiple turns", async () => { const casDir = join(tmpDir, "cas"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; const store = await openStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -263,9 +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.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; const store = await openStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -340,9 +340,9 @@ describe("step read", () => { test("test 4: step with no detail field", async () => { const casDir = join(tmpDir, "cas"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; const store = await openStore(casDir); const schemas = await registerUwfSchemas(store); @@ -401,9 +401,9 @@ describe("step read", () => { test("test 5: step with detail but no turns array", async () => { const casDir = join(tmpDir, "cas"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; const store = await openStore(casDir); const schemas = await registerUwfSchemas(store); await registerDetailSchemas(store); @@ -479,9 +479,9 @@ describe("step read", () => { test("test 6: displays role and tool calls in turn body", async () => { const casDir = join(tmpDir, "cas"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; const store = await openStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -553,9 +553,9 @@ describe("step read", () => { test("test 7: turn content with special characters", async () => { const casDir = join(tmpDir, "cas"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; const store = await openStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); diff --git a/packages/cli/src/__tests__/step-show-json.test.ts b/packages/cli/src/__tests__/step-show-json.test.ts index 63e4a35..21d446f 100644 --- a/packages/cli/src/__tests__/step-show-json.test.ts +++ b/packages/cli/src/__tests__/step-show-json.test.ts @@ -131,16 +131,16 @@ describe("cmdStepShow JSON serialization", () => { testDir = await mkdtemp(join(tmpdir(), "uwf-test-")); casDir = join(testDir, "cas"); await mkdir(casDir, { recursive: true }); - originalEnv = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + originalEnv = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; }); afterEach(async () => { await rm(testDir, { recursive: true, force: true }); if (originalEnv === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalEnv; + process.env.OCAS_HOME = originalEnv; } }); diff --git a/packages/cli/src/__tests__/step-timing.test.ts b/packages/cli/src/__tests__/step-timing.test.ts index e2a5cc7..9d84c89 100644 --- a/packages/cli/src/__tests__/step-timing.test.ts +++ b/packages/cli/src/__tests__/step-timing.test.ts @@ -67,17 +67,17 @@ let originalEnv: string | undefined; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-timing-test-")); - originalEnv = process.env.OCAS_DIR; - process.env.OCAS_DIR = join(tmpDir, "cas"); - await mkdir(process.env.OCAS_DIR, { recursive: true }); + originalEnv = process.env.OCAS_HOME; + process.env.OCAS_HOME = join(tmpDir, "cas"); + await mkdir(process.env.OCAS_HOME, { recursive: true }); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); if (originalEnv === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalEnv; + process.env.OCAS_HOME = originalEnv; } }); diff --git a/packages/cli/src/__tests__/store-global-cas.test.ts b/packages/cli/src/__tests__/store-global-cas.test.ts index 5e2a727..86e9436 100644 --- a/packages/cli/src/__tests__/store-global-cas.test.ts +++ b/packages/cli/src/__tests__/store-global-cas.test.ts @@ -20,7 +20,7 @@ describe("Global CAS directory", () => { beforeEach(async () => { tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`); await mkdir(tmpDir, { recursive: true }); - originalOcasDir = process.env.OCAS_DIR; + originalOcasDir = process.env.OCAS_HOME; }); afterEach(async () => { @@ -28,27 +28,27 @@ describe("Global CAS directory", () => { await rm(tmpDir, { recursive: true, force: true }); } if (originalOcasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalOcasDir; + process.env.OCAS_HOME = originalOcasDir; } }); test("getGlobalCasDir returns default path when no env var set", () => { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; const casDir = getGlobalCasDir(); expect(casDir).toContain(".ocas"); }); - test("getGlobalCasDir respects OCAS_DIR environment variable", () => { + test("getGlobalCasDir respects OCAS_HOME environment variable", () => { const customPath = join(tmpDir, "custom-cas"); - process.env.OCAS_DIR = customPath; + process.env.OCAS_HOME = customPath; const casDir = getGlobalCasDir(); expect(casDir).toBe(customPath); }); - test("getGlobalCasDir ignores empty OCAS_DIR", () => { - process.env.OCAS_DIR = ""; + test("getGlobalCasDir ignores empty OCAS_HOME", () => { + process.env.OCAS_HOME = ""; const casDir = getGlobalCasDir(); expect(casDir).toContain(".ocas"); }); @@ -61,7 +61,7 @@ describe("Global CAS directory", () => { test("createUwfStore uses global CAS directory", async () => { const globalCasDir = join(tmpDir, "global-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage"); await mkdir(storageRoot, { recursive: true }); @@ -82,7 +82,7 @@ describe("Global CAS directory", () => { test("createUwfStore creates global CAS directory if it does not exist", async () => { const globalCasDir = join(tmpDir, "new-global-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage"); await mkdir(storageRoot, { recursive: true }); @@ -97,7 +97,7 @@ describe("Global CAS directory", () => { test("multiple uwfStore instances share the same global CAS filesystem", async () => { const globalCasDir = join(tmpDir, "shared-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot1 = join(tmpDir, "storage1"); const storageRoot2 = join(tmpDir, "storage2"); @@ -127,7 +127,7 @@ describe("Global CAS directory", () => { test("workflow registry is stored in global CAS variable store", async () => { const globalCasDir = join(tmpDir, "global-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage"); await mkdir(storageRoot, { recursive: true }); @@ -148,7 +148,7 @@ describe("Global CAS directory", () => { test("migrates workflows.yaml to variable store and renames file", async () => { const globalCasDir = join(tmpDir, "global-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage-migrate"); await mkdir(storageRoot, { recursive: true }); @@ -173,7 +173,7 @@ describe("Global CAS directory", () => { test("migrates threads.yaml to variable store and renames file", async () => { const globalCasDir = join(tmpDir, "global-cas-threads"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage-threads-migrate"); await mkdir(storageRoot, { recursive: true }); @@ -197,7 +197,7 @@ describe("Global CAS directory", () => { test("thread metadata stored in ocas variable store", async () => { const globalCasDir = join(tmpDir, "global-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage"); await mkdir(storageRoot, { recursive: true }); @@ -218,7 +218,7 @@ describe("Global CAS directory", () => { test("history is stored in global CAS variable store", async () => { const globalCasDir = join(tmpDir, "global-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage"); await mkdir(storageRoot, { recursive: true }); @@ -249,7 +249,7 @@ describe("Global CAS directory", () => { test("migrates history.jsonl to variable store and renames file", async () => { const globalCasDir = join(tmpDir, "global-cas-history"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage-history-migrate"); await mkdir(storageRoot, { recursive: true }); @@ -292,7 +292,7 @@ describe("Global CAS directory", () => { test("CAS nodes are stored in global directory", async () => { const globalCasDir = join(tmpDir, "global-cas"); - process.env.OCAS_DIR = globalCasDir; + process.env.OCAS_HOME = globalCasDir; const storageRoot = join(tmpDir, "storage"); await mkdir(storageRoot, { recursive: true }); diff --git a/packages/cli/src/__tests__/store-storage-root.test.ts b/packages/cli/src/__tests__/store-storage-root.test.ts index 5e2dbae..68d8366 100644 --- a/packages/cli/src/__tests__/store-storage-root.test.ts +++ b/packages/cli/src/__tests__/store-storage-root.test.ts @@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { getDefaultStorageRoot, getGlobalCasDir, resolveStorageRoot } from "../store.js"; describe("Storage root resolution", () => { - const envKeys = ["UWF_STORAGE_ROOT", "WORKFLOW_STORAGE_ROOT", "OCAS_DIR"] as const; + const envKeys = ["UWF_HOME", "OCAS_HOME"] as const; const savedEnv: Partial> = {}; beforeEach(() => { @@ -28,15 +28,13 @@ describe("Storage root resolution", () => { expect(getDefaultStorageRoot()).toBe(join(homedir(), ".uwf")); }); - test("resolveStorageRoot prefers UWF_STORAGE_ROOT", () => { - process.env.UWF_STORAGE_ROOT = "/tmp/uwf-primary"; - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf-fallback"; + test("resolveStorageRoot uses UWF_HOME", () => { + process.env.UWF_HOME = "/tmp/uwf-primary"; expect(resolveStorageRoot()).toBe("/tmp/uwf-primary"); }); - test("resolveStorageRoot falls back to WORKFLOW_STORAGE_ROOT", () => { - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf-fallback"; - expect(resolveStorageRoot()).toBe("/tmp/uwf-fallback"); + test("resolveStorageRoot falls back to default when UWF_HOME unset", () => { + expect(resolveStorageRoot()).toBe(getDefaultStorageRoot()); }); test("getGlobalCasDir returns ~/.ocas by default", () => { @@ -44,8 +42,8 @@ describe("Storage root resolution", () => { expect(casDir).toBe(join(homedir(), ".ocas")); }); - test("getGlobalCasDir respects OCAS_DIR", () => { - process.env.OCAS_DIR = "/tmp/ocas-primary"; + test("getGlobalCasDir respects OCAS_HOME", () => { + process.env.OCAS_HOME = "/tmp/ocas-primary"; expect(getGlobalCasDir()).toBe("/tmp/ocas-primary"); }); }); diff --git a/packages/cli/src/__tests__/thread-cancel-status.test.ts b/packages/cli/src/__tests__/thread-cancel-status.test.ts index 5da810b..5699fce 100644 --- a/packages/cli/src/__tests__/thread-cancel-status.test.ts +++ b/packages/cli/src/__tests__/thread-cancel-status.test.ts @@ -8,7 +8,7 @@ import { addHistoryEntry, createUwfStore, loadAllHistory } from "../store.js"; async function makeUwfStore(storageRoot: string) { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; return createUwfStore(storageRoot); } diff --git a/packages/cli/src/__tests__/thread-list-filters.test.ts b/packages/cli/src/__tests__/thread-list-filters.test.ts index f7cf581..dd9344c 100644 --- a/packages/cli/src/__tests__/thread-list-filters.test.ts +++ b/packages/cli/src/__tests__/thread-list-filters.test.ts @@ -22,8 +22,8 @@ import { async function makeUwfStore(storageRoot: string): Promise { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); - // Set OCAS_DIR to use the test's CAS directory - process.env.OCAS_DIR = casDir; + // Set OCAS_HOME to use the test's CAS directory + process.env.OCAS_HOME = casDir; return createUwfStore(storageRoot); } diff --git a/packages/cli/src/__tests__/thread-location.test.ts b/packages/cli/src/__tests__/thread-location.test.ts index 17c9b1e..b0102fe 100644 --- a/packages/cli/src/__tests__/thread-location.test.ts +++ b/packages/cli/src/__tests__/thread-location.test.ts @@ -19,9 +19,9 @@ describe("Thread and edge location integration", () => { await mkdir(storageRoot, { recursive: true }); await mkdir(casDir, { recursive: true }); - // Set OCAS_DIR for this test - originalEnv = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + // Set OCAS_HOME for this test + originalEnv = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; } async function teardown() { @@ -30,9 +30,9 @@ describe("Thread and edge location integration", () => { } // Restore original environment if (originalEnv === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalEnv; + process.env.OCAS_HOME = originalEnv; } } diff --git a/packages/cli/src/__tests__/thread-read-quota.test.ts b/packages/cli/src/__tests__/thread-read-quota.test.ts index 07a9fcf..025ac6a 100644 --- a/packages/cli/src/__tests__/thread-read-quota.test.ts +++ b/packages/cli/src/__tests__/thread-read-quota.test.ts @@ -71,17 +71,17 @@ let originalEnv: string | undefined; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-quota-test-")); - originalEnv = process.env.OCAS_DIR; - process.env.OCAS_DIR = join(tmpDir, "cas"); - await mkdir(process.env.OCAS_DIR, { recursive: true }); + originalEnv = process.env.OCAS_HOME; + process.env.OCAS_HOME = join(tmpDir, "cas"); + await mkdir(process.env.OCAS_HOME, { recursive: true }); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); if (originalEnv === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalEnv; + process.env.OCAS_HOME = originalEnv; } }); diff --git a/packages/cli/src/__tests__/thread-read-xml-tags.test.ts b/packages/cli/src/__tests__/thread-read-xml-tags.test.ts index c357078..841fc9c 100644 --- a/packages/cli/src/__tests__/thread-read-xml-tags.test.ts +++ b/packages/cli/src/__tests__/thread-read-xml-tags.test.ts @@ -52,7 +52,7 @@ const DETAIL_SCHEMA = { async function makeUwfStore(storageRoot: string): Promise { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; return createUwfStore(storageRoot); } diff --git a/packages/cli/src/__tests__/thread-resume.test.ts b/packages/cli/src/__tests__/thread-resume.test.ts index 85884bd..799bd74 100644 --- a/packages/cli/src/__tests__/thread-resume.test.ts +++ b/packages/cli/src/__tests__/thread-resume.test.ts @@ -89,7 +89,7 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{ cwd: tmpDir, }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await seedThreads(tmpDir, { [THREAD_ID]: startHash }); const outputHash = await store.cas.put(outputSchemaHash, { @@ -189,8 +189,8 @@ function runUwf( stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, - WORKFLOW_STORAGE_ROOT: tmpDir, - OCAS_DIR: casDir, + UWF_HOME: tmpDir, + OCAS_HOME: casDir, }, cwd: tmpDir, timeout: 30000, @@ -242,7 +242,7 @@ describe("uwf thread resume", () => { cwd: tmpDir, }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; await seedThreads(tmpDir, { [THREAD_ID]: startHash }); const result = runUwf(["thread", "resume", THREAD_ID], casDir); @@ -251,9 +251,9 @@ describe("uwf thread resume", () => { }); test("resume suspended thread executes step and becomes idle", async () => { - const originalCasDir = process.env.OCAS_DIR; + const originalCasDir = process.env.OCAS_HOME; const { casDir, mockAgentPath } = await setupSuspendedThread("ok"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; try { const result = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir); @@ -279,17 +279,17 @@ describe("uwf thread resume", () => { expect(showResult.suspendMessage).toBeNull(); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); test("resume without -p uses suspend message as agent prompt", async () => { - const originalCasDir = process.env.OCAS_DIR; + const originalCasDir = process.env.OCAS_HOME; const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("ok"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; try { const result = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir); @@ -299,17 +299,17 @@ describe("uwf thread resume", () => { expect(capturedPrompt).toBe(SUSPEND_MESSAGE); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); test("resume with -p appends supplementary info to agent prompt", async () => { - const originalCasDir = process.env.OCAS_DIR; + const originalCasDir = process.env.OCAS_HOME; const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("ok"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; try { const supplement = "Use the REST API."; @@ -323,17 +323,17 @@ describe("uwf thread resume", () => { expect(capturedPrompt).toBe(`${SUSPEND_MESSAGE}\n\n${supplement}`); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); test("multiple suspend/resume cycles", async () => { - const originalCasDir = process.env.OCAS_DIR; + const originalCasDir = process.env.OCAS_HOME; const { casDir, mockAgentPath, promptCapturePath } = await setupSuspendedThread("suspend"); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; try { const firstResult = runUwf(["thread", "resume", THREAD_ID, "--agent", mockAgentPath], casDir); @@ -371,9 +371,9 @@ describe("uwf thread resume", () => { expect(capturedPrompt).toBe(SUSPEND_MESSAGE); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); diff --git a/packages/cli/src/__tests__/thread-show-status.test.ts b/packages/cli/src/__tests__/thread-show-status.test.ts index 8c4d0af..60d412b 100644 --- a/packages/cli/src/__tests__/thread-show-status.test.ts +++ b/packages/cli/src/__tests__/thread-show-status.test.ts @@ -305,8 +305,8 @@ describe("thread show status field", () => { await setupTestEnv(); const casDir = join(tmpDir, "cas"); await mkdir(casDir, { recursive: true }); - const originalCasDir = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + const originalCasDir = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; try { const workflowPath = join(tmpDir, "test-suspend-status.yaml"); @@ -331,9 +331,9 @@ describe("thread show status field", () => { expect(result.thread).toBe(threadId); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } await teardown(); } diff --git a/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts b/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts index 195088f..6a406dd 100644 --- a/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts +++ b/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts @@ -21,9 +21,9 @@ describe("thread start --cwd CLI option", () => { await mkdir(storageRoot, { recursive: true }); await mkdir(casDir, { recursive: true }); - // Set OCAS_DIR for this test - originalEnv = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + // Set OCAS_HOME for this test + originalEnv = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; } async function teardown() { @@ -32,9 +32,9 @@ describe("thread start --cwd CLI option", () => { } // Restore original environment if (originalEnv === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalEnv; + process.env.OCAS_HOME = originalEnv; } } @@ -139,7 +139,7 @@ graph: // Register the workflow execFileSync(process.execPath, [uwfBin, "workflow", "add", workflowPath], { - env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir }, + env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir }, encoding: "utf8", }); @@ -148,7 +148,7 @@ graph: process.execPath, [uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd], { - env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir }, + env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir }, encoding: "utf8", }, ); diff --git a/packages/cli/src/__tests__/thread-step-count.test.ts b/packages/cli/src/__tests__/thread-step-count.test.ts index c5c4aa2..4d2ca4b 100644 --- a/packages/cli/src/__tests__/thread-step-count.test.ts +++ b/packages/cli/src/__tests__/thread-step-count.test.ts @@ -9,7 +9,7 @@ function runCli(args: string[]): { stdout: string; stderr: string; exitCode: num try { const stdout = execFileSync("npx", ["tsx", CLI_PATH, ...args], { encoding: "utf8", - env: { ...process.env, WORKFLOW_STORAGE_ROOT: "/tmp/uwf-test-nonexistent" }, + env: { ...process.env, UWF_HOME: "/tmp/uwf-test-nonexistent" }, stdio: ["ignore", "pipe", "pipe"], }); return { stdout, stderr: "", exitCode: 0 }; diff --git a/packages/cli/src/__tests__/thread-suspend-step.test.ts b/packages/cli/src/__tests__/thread-suspend-step.test.ts index 77f200f..16ef4f2 100644 --- a/packages/cli/src/__tests__/thread-suspend-step.test.ts +++ b/packages/cli/src/__tests__/thread-suspend-step.test.ts @@ -35,8 +35,8 @@ describe("suspend step CAS chain and threads.yaml metadata", () => { test("thread exec records suspend step in CAS and suspend metadata in threads.yaml", async () => { const casDir = join(tmpDir, "cas"); await mkdir(casDir, { recursive: true }); - const originalCasDir = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + const originalCasDir = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; try { const store = await openStore(casDir); @@ -128,8 +128,8 @@ describe("suspend step CAS chain and threads.yaml metadata", () => { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, - WORKFLOW_STORAGE_ROOT: tmpDir, - OCAS_DIR: casDir, + UWF_HOME: tmpDir, + OCAS_HOME: casDir, }, cwd: tmpDir, timeout: 30000, @@ -170,9 +170,9 @@ describe("suspend step CAS chain and threads.yaml metadata", () => { expect(showResult.suspendedRole).toBe("worker"); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); diff --git a/packages/cli/src/__tests__/thread-suspended-display.test.ts b/packages/cli/src/__tests__/thread-suspended-display.test.ts index cc5d38f..c50d869 100644 --- a/packages/cli/src/__tests__/thread-suspended-display.test.ts +++ b/packages/cli/src/__tests__/thread-suspended-display.test.ts @@ -33,8 +33,8 @@ describe("suspended thread display", () => { test("thread list shows [suspended] marker for suspended threads", async () => { const casDir = join(tmpDir, "cas"); await mkdir(casDir, { recursive: true }); - const originalCasDir = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + const originalCasDir = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; try { const uwf = await createUwfStore(tmpDir); @@ -131,9 +131,9 @@ describe("suspended thread display", () => { expect(idleItem!.statusDisplay).toBe("idle"); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); @@ -141,8 +141,8 @@ describe("suspended thread display", () => { test("thread show displays suspend info and resume hint", async () => { const casDir = join(tmpDir, "cas"); await mkdir(casDir, { recursive: true }); - const originalCasDir = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + const originalCasDir = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; try { const uwf = await createUwfStore(tmpDir); @@ -219,9 +219,9 @@ describe("suspended thread display", () => { ); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); @@ -229,8 +229,8 @@ describe("suspended thread display", () => { test("non-suspended threads do not show suspend markers or hints", async () => { const casDir = join(tmpDir, "cas"); await mkdir(casDir, { recursive: true }); - const originalCasDir = process.env.OCAS_DIR; - process.env.OCAS_DIR = casDir; + const originalCasDir = process.env.OCAS_HOME; + process.env.OCAS_HOME = casDir; try { const uwf = await createUwfStore(tmpDir); @@ -278,9 +278,9 @@ describe("suspended thread display", () => { expect(threadItem!.statusDisplay).toBe("idle"); } finally { if (originalCasDir === undefined) { - delete process.env.OCAS_DIR; + delete process.env.OCAS_HOME; } else { - process.env.OCAS_DIR = originalCasDir; + process.env.OCAS_HOME = originalCasDir; } } }); diff --git a/packages/cli/src/__tests__/thread.test.ts b/packages/cli/src/__tests__/thread.test.ts index ff97665..959a788 100644 --- a/packages/cli/src/__tests__/thread.test.ts +++ b/packages/cli/src/__tests__/thread.test.ts @@ -57,7 +57,7 @@ const DETAIL_SCHEMA = { async function makeUwfStore(storageRoot: string): Promise { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; return createUwfStore(storageRoot); } diff --git a/packages/cli/src/__tests__/workflow-resolution.test.ts b/packages/cli/src/__tests__/workflow-resolution.test.ts index ee0ab38..a5214cc 100644 --- a/packages/cli/src/__tests__/workflow-resolution.test.ts +++ b/packages/cli/src/__tests__/workflow-resolution.test.ts @@ -13,7 +13,7 @@ import { createUwfStore, saveWorkflowRegistry } from "../store.js"; async function makeUwfStore(storageRoot: string): Promise { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); - process.env.OCAS_DIR = casDir; + process.env.OCAS_HOME = casDir; return createUwfStore(storageRoot); } diff --git a/packages/cli/src/store.ts b/packages/cli/src/store.ts index 9bc0ddb..b1126c1 100644 --- a/packages/cli/src/store.ts +++ b/packages/cli/src/store.ts @@ -118,17 +118,13 @@ export function getDefaultStorageRoot(): string { /** * Resolve storage root. - * Priority: `UWF_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → default. + * Priority: `UWF_HOME` → default. */ export function resolveStorageRoot(): string { - const primary = process.env.UWF_STORAGE_ROOT; + const primary = process.env.UWF_HOME; if (primary !== undefined && primary !== "") { return primary; } - const userOverride = process.env.WORKFLOW_STORAGE_ROOT; - if (userOverride !== undefined && userOverride !== "") { - return userOverride; - } return getDefaultStorageRoot(); } @@ -142,10 +138,10 @@ export function getCasDir(storageRoot: string): string { /** * Returns the global CAS directory shared by all uwf and ocas tools. - * Priority: `OCAS_DIR` → default ~/.ocas + * Priority: `OCAS_HOME` → default ~/.ocas */ export function getGlobalCasDir(): string { - const primary = process.env.OCAS_DIR; + const primary = process.env.OCAS_HOME; if (primary !== undefined && primary !== "") { return primary; } diff --git a/packages/util-agent/README.md b/packages/util-agent/README.md index 49fbf28..302a451 100644 --- a/packages/util-agent/README.md +++ b/packages/util-agent/README.md @@ -50,10 +50,19 @@ Agent CLIs call `createAgent(...)` and invoke the returned function as `main()`. ### Context ```typescript -function buildContext(threadId: ThreadId, role: string): Promise +function buildContext( + threadId: ThreadId, + role: string, + edgePrompt: string, + storageRoot: string, + casDir: string, +): Promise function buildContextWithMeta( threadId: ThreadId, role: string, + edgePrompt: string, + storageRoot: string, + casDir: string, ): Promise type AgentContext = ModeratorContext & { @@ -64,6 +73,8 @@ type AgentContext = ModeratorContext & { outputFormatInstruction: string; edgePrompt: string; isFirstVisit: boolean; + storageRoot: string; + casDir: string; }; type BuildContextMeta = { @@ -99,6 +110,8 @@ function extract( rawOutput: string, outputSchema: CasRef, config: WorkflowConfig, + storageRoot: string, + casDir: string, ): Promise type ResolvedLlmProvider = { baseUrl: string; apiKey: string; model: string }; @@ -120,11 +133,18 @@ type FrontmatterFastPathResult = { body: string; outputHash: CasRef }; ### Session cache ```typescript -function getCachedSessionId(threadId: ThreadId, role: string): Promise +function getCachedSessionId( + agentName: string, + threadId: ThreadId, + role: string, + storageRoot: string, +): Promise function setCachedSessionId( + agentName: string, threadId: ThreadId, role: string, sessionId: string, + storageRoot: string, ): Promise ``` @@ -133,7 +153,7 @@ function setCachedSessionId( ```typescript function getConfigPath(storageRoot: string): string function getEnvPath(storageRoot: string): string -function resolveStorageRoot(): string +function resolveStorageRoot(override: string | null): string function loadWorkflowConfig(storageRoot: string): Promise ``` diff --git a/packages/util-agent/__tests__/session-cache.test.ts b/packages/util-agent/__tests__/session-cache.test.ts index 2802177..78585ae 100644 --- a/packages/util-agent/__tests__/session-cache.test.ts +++ b/packages/util-agent/__tests__/session-cache.test.ts @@ -4,37 +4,31 @@ import type { ThreadId } from "@united-workforce/protocol"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { getCachedSessionId, getCachePath, setCachedSessionId } from "../src/session-cache.js"; -import { resolveStorageRoot } from "../src/storage.js"; +import { getDefaultStorageRoot } from "../src/storage.js"; describe("session-cache", () => { - let originalStorageRoot: string; let testStorageRoot: string; beforeEach(async () => { // Create a temporary test storage root - originalStorageRoot = resolveStorageRoot(); - testStorageRoot = join(originalStorageRoot, "test-cache", `test-${Date.now()}`); + testStorageRoot = join(getDefaultStorageRoot(), "test-cache", `test-${Date.now()}`); await mkdir(testStorageRoot, { recursive: true }); - - // Override the storage root for testing - process.env.WORKFLOW_STORAGE_ROOT = testStorageRoot; }); afterEach(async () => { // Clean up test storage root await rm(testStorageRoot, { recursive: true, force: true }); - delete process.env.WORKFLOW_STORAGE_ROOT; }); describe("getCachePath", () => { test("returns agent-specific file path", () => { - const path = getCachePath("claude-code"); + const path = getCachePath("claude-code", testStorageRoot); expect(path).toMatch(/\/cache\/claude-code-sessions\.json$/); }); test("returns different paths for different agents", () => { - const pathClaudeCode = getCachePath("claude-code"); - const pathHermes = getCachePath("hermes"); + const pathClaudeCode = getCachePath("claude-code", testStorageRoot); + const pathHermes = getCachePath("hermes", testStorageRoot); expect(pathClaudeCode).not.toBe(pathHermes); expect(pathClaudeCode).toMatch(/claude-code-sessions\.json$/); @@ -42,8 +36,8 @@ describe("session-cache", () => { }); test("handles agent names with special characters", () => { - const path1 = getCachePath("my-agent"); - const path2 = getCachePath("my_agent"); + const path1 = getCachePath("my-agent", testStorageRoot); + const path2 = getCachePath("my_agent", testStorageRoot); expect(path1).toMatch(/my-agent-sessions\.json$/); expect(path2).toMatch(/my_agent-sessions\.json$/); @@ -56,12 +50,12 @@ describe("session-cache", () => { test("sessions are isolated per agent", async () => { // Cache different session IDs for each agent - await setCachedSessionId("claude-code", threadId, role, "session-cc-001"); - await setCachedSessionId("hermes", threadId, role, "session-hermes-001"); + await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot); + await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot); // Each agent should retrieve its own session ID - const sessionCC = await getCachedSessionId("claude-code", threadId, role); - const sessionHermes = await getCachedSessionId("hermes", threadId, role); + const sessionCC = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); + const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot); expect(sessionCC).toBe("session-cc-001"); expect(sessionHermes).toBe("session-hermes-001"); @@ -69,30 +63,30 @@ describe("session-cache", () => { test("updating one agent's cache does not affect another", async () => { // Set initial sessions for both agents - await setCachedSessionId("claude-code", threadId, role, "session-cc-001"); - await setCachedSessionId("hermes", threadId, role, "session-hermes-001"); + await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot); + await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot); // Update claude-code's session - await setCachedSessionId("claude-code", threadId, role, "session-cc-002"); + await setCachedSessionId("claude-code", threadId, role, "session-cc-002", testStorageRoot); // Hermes's session should remain unchanged - const sessionHermes = await getCachedSessionId("hermes", threadId, role); + const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot); expect(sessionHermes).toBe("session-hermes-001"); // Claude-code should have the new session - const sessionCC = await getCachedSessionId("claude-code", threadId, role); + const sessionCC = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); expect(sessionCC).toBe("session-cc-002"); }); test("missing session returns null for specific agent", async () => { - const session = await getCachedSessionId("claude-code", threadId, role); + const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); expect(session).toBeNull(); }); test("empty session ID is treated as missing", async () => { - await setCachedSessionId("claude-code", threadId, role, ""); + await setCachedSessionId("claude-code", threadId, role, "", testStorageRoot); - const session = await getCachedSessionId("claude-code", threadId, role); + const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); expect(session).toBeNull(); }); }); @@ -102,14 +96,14 @@ describe("session-cache", () => { const role = "developer"; test("cache directory is created if missing", async () => { - const cachePath = getCachePath("claude-code"); + const cachePath = getCachePath("claude-code", testStorageRoot); const cacheDir = dirname(cachePath); // Ensure cache dir doesn't exist await rm(cacheDir, { recursive: true, force: true }); // Write a session - await setCachedSessionId("claude-code", threadId, role, "session-001"); + await setCachedSessionId("claude-code", threadId, role, "session-001", testStorageRoot); // Cache directory should be created const stats = await stat(cacheDir); @@ -118,12 +112,12 @@ describe("session-cache", () => { test("multiple agents create separate cache files", async () => { // Cache sessions for multiple agents - await setCachedSessionId("claude-code", threadId, role, "session-cc-001"); - await setCachedSessionId("hermes", threadId, role, "session-hermes-001"); + await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot); + await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot); // Separate cache files should exist - const pathCC = getCachePath("claude-code"); - const pathHermes = getCachePath("hermes"); + const pathCC = getCachePath("claude-code", testStorageRoot); + const pathHermes = getCachePath("hermes", testStorageRoot); const contentCC = JSON.parse(await readFile(pathCC, "utf8")) as Record; const contentHermes = JSON.parse(await readFile(pathHermes, "utf8")) as Record< @@ -137,10 +131,10 @@ describe("session-cache", () => { test("atomic writes prevent partial reads", async () => { // Write a session - await setCachedSessionId("claude-code", threadId, role, "session-001"); + await setCachedSessionId("claude-code", threadId, role, "session-001", testStorageRoot); // The final file should exist (no .tmp files left behind) - const cachePath = getCachePath("claude-code"); + const cachePath = getCachePath("claude-code", testStorageRoot); const dir = dirname(cachePath); const files = await readdir(dir); @@ -155,7 +149,7 @@ describe("session-cache", () => { test("old agent-sessions.json is ignored", async () => { // Create old agent-sessions.json file - const oldCachePath = join(resolveStorageRoot(), "cache", "agent-sessions.json"); + const oldCachePath = join(testStorageRoot, "cache", "agent-sessions.json"); await mkdir(dirname(oldCachePath), { recursive: true }); await writeFile( oldCachePath, @@ -166,7 +160,7 @@ describe("session-cache", () => { ); // Query with the new per-agent cache - const session = await getCachedSessionId("claude-code", threadId, role); + const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); // Should return null (old cache is ignored) expect(session).toBeNull(); @@ -174,7 +168,7 @@ describe("session-cache", () => { test("new per-agent cache takes precedence", async () => { // Create both old and new cache files - const oldPath = join(resolveStorageRoot(), "cache", "agent-sessions.json"); + const oldPath = join(testStorageRoot, "cache", "agent-sessions.json"); await mkdir(dirname(oldPath), { recursive: true }); await writeFile( oldPath, @@ -184,10 +178,10 @@ describe("session-cache", () => { "utf8", ); - await setCachedSessionId("claude-code", threadId, role, "new-session"); + await setCachedSessionId("claude-code", threadId, role, "new-session", testStorageRoot); // The new per-agent cache value should be returned - const session = await getCachedSessionId("claude-code", threadId, role); + const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); expect(session).toBe("new-session"); }); }); @@ -198,29 +192,29 @@ describe("session-cache", () => { test("invalid JSON in cache file returns empty cache", async () => { // Create a corrupted cache file - const cachePath = getCachePath("claude-code"); + const cachePath = getCachePath("claude-code", testStorageRoot); await mkdir(dirname(cachePath), { recursive: true }); await writeFile(cachePath, "{ invalid json }", "utf8"); // Should return null (treating corrupted cache as empty) - const session = await getCachedSessionId("claude-code", threadId, role); + const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); expect(session).toBeNull(); }); test("non-object JSON in cache file returns empty cache", async () => { // Create a cache file with non-object JSON - const cachePath = getCachePath("claude-code"); + const cachePath = getCachePath("claude-code", testStorageRoot); await mkdir(dirname(cachePath), { recursive: true }); await writeFile(cachePath, JSON.stringify(["not", "an", "object"]), "utf8"); // Should return null - const session = await getCachedSessionId("claude-code", threadId, role); + const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot); expect(session).toBeNull(); }); test("cache entries with non-string values are ignored", async () => { // Create a cache file with mixed types - const cachePath = getCachePath("claude-code"); + const cachePath = getCachePath("claude-code", testStorageRoot); const cacheData = { "thread1:role1": "valid-session", "thread2:role2": 12345, // number @@ -231,13 +225,33 @@ describe("session-cache", () => { await writeFile(cachePath, JSON.stringify(cacheData), "utf8"); // Valid string entries should be returned - const session1 = await getCachedSessionId("claude-code", "thread1" as ThreadId, "role1"); + const session1 = await getCachedSessionId( + "claude-code", + "thread1" as ThreadId, + "role1", + testStorageRoot, + ); expect(session1).toBe("valid-session"); // Invalid entries should return null - const session2 = await getCachedSessionId("claude-code", "thread2" as ThreadId, "role2"); - const session3 = await getCachedSessionId("claude-code", "thread3" as ThreadId, "role3"); - const session4 = await getCachedSessionId("claude-code", "thread4" as ThreadId, "role4"); + const session2 = await getCachedSessionId( + "claude-code", + "thread2" as ThreadId, + "role2", + testStorageRoot, + ); + const session3 = await getCachedSessionId( + "claude-code", + "thread3" as ThreadId, + "role3", + testStorageRoot, + ); + const session4 = await getCachedSessionId( + "claude-code", + "thread4" as ThreadId, + "role4", + testStorageRoot, + ); expect(session2).toBeNull(); expect(session3).toBeNull(); diff --git a/packages/util-agent/__tests__/storage.test.ts b/packages/util-agent/__tests__/storage.test.ts index 1cede7a..1a3fe2f 100644 --- a/packages/util-agent/__tests__/storage.test.ts +++ b/packages/util-agent/__tests__/storage.test.ts @@ -1,6 +1,6 @@ import { homedir } from "node:os"; import { join } from "node:path"; -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { resolveStorageRoot, getDefaultStorageRoot, @@ -26,42 +26,16 @@ describe("getDefaultStorageRoot", () => { }); describe("resolveStorageRoot", () => { - const saved: Record = {}; - - beforeEach(() => { - saved.UWF_STORAGE_ROOT = process.env.UWF_STORAGE_ROOT; - saved.WORKFLOW_STORAGE_ROOT = process.env.WORKFLOW_STORAGE_ROOT; + it("uses the override when provided", () => { + expect(resolveStorageRoot("/tmp/uwf1")).toBe("/tmp/uwf1"); }); - afterEach(() => { - for (const k of ["UWF_STORAGE_ROOT", "WORKFLOW_STORAGE_ROOT"] as const) { - if (saved[k] === undefined) delete process.env[k]; - else process.env[k] = saved[k]; - } + it("falls back to default when override is null", () => { + expect(resolveStorageRoot(null)).toBe(getDefaultStorageRoot()); }); - it("uses UWF_STORAGE_ROOT first", () => { - process.env.UWF_STORAGE_ROOT = "/tmp/uwf1"; - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2"; - expect(resolveStorageRoot()).toBe("/tmp/uwf1"); - }); - - it("falls back to WORKFLOW_STORAGE_ROOT", () => { - delete process.env.UWF_STORAGE_ROOT; - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2"; - expect(resolveStorageRoot()).toBe("/tmp/uwf2"); - }); - - it("falls back to default when both unset", () => { - delete process.env.UWF_STORAGE_ROOT; - delete process.env.WORKFLOW_STORAGE_ROOT; - expect(resolveStorageRoot()).toBe(getDefaultStorageRoot()); - }); - - it("ignores empty UWF_STORAGE_ROOT", () => { - process.env.UWF_STORAGE_ROOT = ""; - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf2"; - expect(resolveStorageRoot()).toBe("/tmp/uwf2"); + it("ignores empty override", () => { + expect(resolveStorageRoot("")).toBe(getDefaultStorageRoot()); }); }); @@ -72,21 +46,16 @@ describe("path helpers", () => { }); describe("getGlobalCasDir", () => { - const saved = { OCAS_DIR: process.env.OCAS_DIR }; - - afterEach(() => { - if (saved.OCAS_DIR === undefined) delete process.env.OCAS_DIR; - else process.env.OCAS_DIR = saved.OCAS_DIR; + it("uses the override when provided", () => { + expect(getGlobalCasDir("/tmp/ocas")).toBe("/tmp/ocas"); }); - it("uses OCAS_DIR when set", () => { - process.env.OCAS_DIR = "/tmp/ocas"; - expect(getGlobalCasDir()).toBe("/tmp/ocas"); + it("defaults to ~/.ocas when override is null", () => { + expect(getGlobalCasDir(null)).toBe(join(homedir(), ".ocas")); }); - it("defaults to ~/.ocas", () => { - delete process.env.OCAS_DIR; - expect(getGlobalCasDir()).toBe(join(homedir(), ".ocas")); + it("ignores empty override", () => { + expect(getGlobalCasDir("")).toBe(join(homedir(), ".ocas")); }); }); diff --git a/packages/util-agent/src/context.ts b/packages/util-agent/src/context.ts index 133627b..3103cdb 100644 --- a/packages/util-agent/src/context.ts +++ b/packages/util-agent/src/context.ts @@ -7,7 +7,7 @@ import type { ThreadId, } from "@united-workforce/protocol"; import type { AgentStore } from "./storage.js"; -import { createAgentStore, getActiveThreadEntry, resolveStorageRoot } from "./storage.js"; +import { createAgentStore, getActiveThreadEntry } from "./storage.js"; import type { AgentContext } from "./types.js"; type ChainState = { @@ -157,12 +157,13 @@ export async function buildContext( threadId: ThreadId, role: string, edgePrompt: string, + storageRoot: string, + casDir: string, ): Promise { - const storageRoot = resolveStorageRoot(); - const agentStore = await createAgentStore(storageRoot); + const agentStore = await createAgentStore(storageRoot, casDir); const { store, schemas } = agentStore; - const entry = await getActiveThreadEntry(storageRoot, threadId); + const entry = await getActiveThreadEntry(casDir, threadId); if (entry === null) { fail(`thread not found in active thread index: ${threadId}`); } @@ -187,6 +188,8 @@ export async function buildContext( outputFormatInstruction: "", edgePrompt, isFirstVisit, + storageRoot, + casDir, }; } @@ -205,12 +208,13 @@ export async function buildContextWithMeta( threadId: ThreadId, role: string, edgePrompt: string, + storageRoot: string, + casDir: string, ): Promise { - const storageRoot = resolveStorageRoot(); - const agentStore = await createAgentStore(storageRoot); + const agentStore = await createAgentStore(storageRoot, casDir); const { store, schemas } = agentStore; - const entry = await getActiveThreadEntry(storageRoot, threadId); + const entry = await getActiveThreadEntry(casDir, threadId); if (entry === null) { fail(`thread not found in active thread index: ${threadId}`); } @@ -235,6 +239,8 @@ export async function buildContextWithMeta( outputFormatInstruction: "", edgePrompt, isFirstVisit, + storageRoot, + casDir, meta: { storageRoot, store, schemas, headHash: entry.head, chain }, }; } diff --git a/packages/util-agent/src/extract.ts b/packages/util-agent/src/extract.ts index 4afb8d7..f3d0218 100644 --- a/packages/util-agent/src/extract.ts +++ b/packages/util-agent/src/extract.ts @@ -1,7 +1,7 @@ import { getSchema, validate } from "@ocas/core"; import type { CasRef, ModelAlias, WorkflowConfig } from "@united-workforce/protocol"; -import { createAgentStore, resolveStorageRoot } from "./storage.js"; +import { createAgentStore } from "./storage.js"; export type ResolvedLlmProvider = { baseUrl: string; @@ -135,10 +135,10 @@ export async function extract( rawOutput: string, outputSchema: CasRef, config: WorkflowConfig, + storageRoot: string, + casDir: string, ): Promise { - const storageRoot = resolveStorageRoot(); - - const { store } = await createAgentStore(storageRoot); + const { store } = await createAgentStore(storageRoot, casDir); const schema = getSchema(store, outputSchema); if (schema === null) { throw new Error(`output schema not found in CAS: ${outputSchema}`); diff --git a/packages/util-agent/src/run.ts b/packages/util-agent/src/run.ts index ae81038..0b8e951 100644 --- a/packages/util-agent/src/run.ts +++ b/packages/util-agent/src/run.ts @@ -5,7 +5,7 @@ import { buildOutputFormatInstruction } from "./build-output-format-instruction. import { buildContextWithMeta } from "./context.js"; import { tryFrontmatterFastPath } from "./frontmatter.js"; import type { AgentStore } from "./storage.js"; -import { getEnvPath, resolveStorageRoot } from "./storage.js"; +import { getEnvPath, getGlobalCasDir, resolveStorageRoot } from "./storage.js"; import type { AdapterOutput, AgentOptions } from "./types.js"; const MAX_FRONTMATTER_RETRIES = 2; @@ -135,13 +135,27 @@ async function persistStep(options: { }); } +/** + * Resolve uwf storage root + global CAS directory from the process env. + * This is the agent CLI entry point — the only place in this package allowed + * to read `process.env` for these settings. + */ +function resolveAgentDirs(): { storageRoot: string; casDir: string } { + return { + storageRoot: resolveStorageRoot(process.env.UWF_HOME ?? null), + casDir: getGlobalCasDir(process.env.OCAS_HOME ?? null), + }; +} + export function createAgent(options: AgentOptions): () => Promise { return async function main(): Promise { const { threadId, role, prompt } = parseArgv(process.argv); - const storageRoot = resolveStorageRoot(); + const { storageRoot, casDir } = resolveAgentDirs(); loadDotenv({ path: getEnvPath(storageRoot) }); - const ctx = await runWithMessage("context", () => buildContextWithMeta(threadId, role, prompt)); + const ctx = await runWithMessage("context", () => + buildContextWithMeta(threadId, role, prompt, storageRoot, casDir), + ); const roleDef = ctx.workflow.roles[role]; if (roleDef === undefined) { diff --git a/packages/util-agent/src/session-cache.ts b/packages/util-agent/src/session-cache.ts index 7dea2d3..855e8c9 100644 --- a/packages/util-agent/src/session-cache.ts +++ b/packages/util-agent/src/session-cache.ts @@ -4,12 +4,10 @@ import { dirname, join } from "node:path"; import type { ThreadId } from "@united-workforce/protocol"; -import { resolveStorageRoot } from "./storage.js"; - type SessionCache = Record; -export function getCachePath(agentName: string): string { - return join(resolveStorageRoot(), "cache", `${agentName}-sessions.json`); +export function getCachePath(agentName: string, storageRoot: string): string { + return join(storageRoot, "cache", `${agentName}-sessions.json`); } function cacheKey(threadId: ThreadId, role: string): string { @@ -20,8 +18,8 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -async function readCache(agentName: string): Promise { - const path = getCachePath(agentName); +async function readCache(agentName: string, storageRoot: string): Promise { + const path = getCachePath(agentName, storageRoot); try { const text = await readFile(path, "utf8"); const raw = JSON.parse(text) as unknown; @@ -48,8 +46,12 @@ async function readCache(agentName: string): Promise { } } -async function writeCache(agentName: string, cache: SessionCache): Promise { - const path = getCachePath(agentName); +async function writeCache( + agentName: string, + storageRoot: string, + cache: SessionCache, +): Promise { + const path = getCachePath(agentName, storageRoot); const dir = dirname(path); await mkdir(dir, { recursive: true }); // Atomic write: write to temp file then rename to avoid partial reads on concurrent access. @@ -65,8 +67,9 @@ export async function getCachedSessionId( agentName: string, threadId: ThreadId, role: string, + storageRoot: string, ): Promise { - const cache = await readCache(agentName); + const cache = await readCache(agentName, storageRoot); const sessionId = cache[cacheKey(threadId, role)]; return sessionId ?? null; } @@ -77,8 +80,9 @@ export async function setCachedSessionId( threadId: ThreadId, role: string, sessionId: string, + storageRoot: string, ): Promise { - const cache = await readCache(agentName); + const cache = await readCache(agentName, storageRoot); cache[cacheKey(threadId, role)] = sessionId; - await writeCache(agentName, cache); + await writeCache(agentName, storageRoot, cache); } diff --git a/packages/util-agent/src/storage.ts b/packages/util-agent/src/storage.ts index 8b963f1..a065668 100644 --- a/packages/util-agent/src/storage.ts +++ b/packages/util-agent/src/storage.ts @@ -28,17 +28,12 @@ export function getDefaultStorageRoot(): string { } /** - * Resolve storage root. - * Priority: `UWF_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → default. + * Resolve storage root from an explicit override (e.g. the `UWF_HOME` value + * read by the CLI entry point). Library code must not read `process.env`. */ -export function resolveStorageRoot(): string { - const primary = process.env.UWF_STORAGE_ROOT; - if (primary !== undefined && primary !== "") { - return primary; - } - const userOverride = process.env.WORKFLOW_STORAGE_ROOT; - if (userOverride !== undefined && userOverride !== "") { - return userOverride; +export function resolveStorageRoot(override: string | null): string { + if (override !== null && override !== "") { + return override; } return getDefaultStorageRoot(); } @@ -58,13 +53,13 @@ export function getEnvPath(storageRoot: string): string { const THREAD_VAR_PREFIX = "@uwf/thread/"; /** - * Global CAS directory (same as uwf CLI). - * Priority: `OCAS_DIR` → default ~/.ocas + * Resolve the global CAS directory from an explicit override (e.g. the + * `OCAS_HOME` value read by the CLI entry point). Library code must not read + * `process.env`. Defaults to `~/.ocas`. */ -export function getGlobalCasDir(): string { - const primary = process.env.OCAS_DIR; - if (primary !== undefined && primary !== "") { - return primary; +export function getGlobalCasDir(override: string | null): string { + if (override !== null && override !== "") { + return override; } return join(homedir(), ".ocas"); } @@ -75,10 +70,9 @@ function threadVarName(threadId: ThreadId): string { /** Read active thread head + suspend metadata from ocas variable store. */ export async function getActiveThreadEntry( - _storageRoot: string, + casDir: string, threadId: ThreadId, ): Promise { - const casDir = getGlobalCasDir(); const cas = createFsStore(casDir); const { var: varStore } = createSqliteVarStore(join(casDir, "vars"), cas); const vars = varStore.list({ exactName: threadVarName(threadId) }); @@ -99,8 +93,7 @@ export type AgentStore = { schemas: Awaited>; }; -export async function createAgentStore(storageRoot: string): Promise { - const casDir = getGlobalCasDir(); +export async function createAgentStore(storageRoot: string, casDir: string): Promise { const cas = createFsStore(casDir); const { var: varSub, tag } = createSqliteVarStore(join(casDir, "vars"), cas); const store: Store = { cas, var: varSub, tag }; diff --git a/packages/util-agent/src/types.ts b/packages/util-agent/src/types.ts index c3d5053..103f19d 100644 --- a/packages/util-agent/src/types.ts +++ b/packages/util-agent/src/types.ts @@ -21,6 +21,10 @@ export type AgentContext = ModeratorContext & { * True when the current role has not appeared in steps history before this invocation. */ isFirstVisit: boolean; + /** Resolved uwf storage root (from `UWF_HOME`), threaded from the CLI entry point. */ + storageRoot: string; + /** Resolved global CAS directory (from `OCAS_HOME`), threaded from the CLI entry point. */ + casDir: string; }; export type AgentRunResult = { diff --git a/packages/util/src/user-reference.ts b/packages/util/src/user-reference.ts index a4f01fa..4fa4577 100644 --- a/packages/util/src/user-reference.ts +++ b/packages/util/src/user-reference.ts @@ -37,7 +37,7 @@ uwf setup --provider --base-url \\ [--agent ] # optional default agent \`\`\` -Config is stored at \`~/.uwf/config.yaml\`. Override storage root with \`UWF_STORAGE_ROOT\` (or \`WORKFLOW_STORAGE_ROOT\`). +Config is stored at \`~/.uwf/config.yaml\`. Override storage root with \`UWF_HOME\`. ## Workflow Commands diff --git a/scripts/check-dev-env.sh b/scripts/check-dev-env.sh index 74ad3a9..f345e12 100755 --- a/scripts/check-dev-env.sh +++ b/scripts/check-dev-env.sh @@ -89,7 +89,7 @@ echo "" echo "=== Config ===" # Check workflow config exists -CONFIG_DIR="${UWF_STORAGE_ROOT:-$HOME/.shazhou/united-workforce}" +CONFIG_DIR="${UWF_HOME:-$HOME/.shazhou/united-workforce}" check "config.yaml exists" \ "[ -f '$CONFIG_DIR/config.yaml' ]" \ "Run: uwf setup" diff --git a/scripts/e2e-walkthrough.sh b/scripts/e2e-walkthrough.sh index 986ec26..d70f8b7 100755 --- a/scripts/e2e-walkthrough.sh +++ b/scripts/e2e-walkthrough.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # E2E walkthrough for shazhou/united-workforce. -# Runs inside Docker with isolated UWF_STORAGE_ROOT. +# Runs inside Docker with isolated UWF_HOME. # Exercises: setup → workflow add → thread start/exec → cancel/fork → read/inspect. # # Usage: @@ -70,8 +70,8 @@ cat > "$E2E_DIR/run.sh" << 'INNER_SCRIPT' set -euo pipefail # Isolated storage — never touches host's ~/.uwf -export UWF_STORAGE_ROOT="/tmp/uwf-e2e-storage" -mkdir -p "$UWF_STORAGE_ROOT" +export UWF_HOME="/tmp/uwf-e2e-storage" +mkdir -p "$UWF_HOME" REPO_DIR="$1" AGENT="$2" @@ -157,7 +157,7 @@ if [ -n "$PROVIDER" ] && [ -n "$MODEL" ] && [ -n "$API_KEY" ]; then else # Copy host config if available if [ -f "$HOME/.shazhou/united-workforce/config.yaml" ]; then - cp "$HOME/.shazhou/united-workforce/config.yaml" "$UWF_STORAGE_ROOT/config.yaml" + cp "$HOME/.shazhou/united-workforce/config.yaml" "$UWF_HOME/config.yaml" echo " Copied host config.yaml" >&2 fi fi -- 2.43.0