refactor: migrate storage paths ~/.uncaged/workflow → ~/.uwf #14
@@ -14,7 +14,7 @@ This monorepo implements a stateless workflow engine driven by a single-step CLI
|
|||||||
| **Moderator** | Status-based graph evaluator — determines the next role (or `$END`) with zero LLM cost. |
|
| **Moderator** | Status-based graph evaluator — determines the next role (or `$END`) with zero LLM cost. |
|
||||||
| **Agent** | An external CLI command (`uwf-hermes`, etc.) spawned by `uwf thread step`. Produces frontmatter markdown output. |
|
| **Agent** | An external CLI command (`uwf-hermes`, etc.) spawned by `uwf thread step`. Produces frontmatter markdown output. |
|
||||||
| **CAS** | Content-Addressed Storage via `@ocas/core` — all workflow definitions, thread nodes, and outputs are immutable CAS nodes. |
|
| **CAS** | Content-Addressed Storage via `@ocas/core` — all workflow definitions, thread nodes, and outputs are immutable CAS nodes. |
|
||||||
| **Registry** | `~/.uncaged/workflow/registry.yaml` — maps workflow names to current CAS hashes. |
|
| **Registry** | `~/.uwf/registry.yaml` — maps workflow names to current CAS hashes. |
|
||||||
|
|
||||||
### Monorepo Structure
|
### Monorepo Structure
|
||||||
|
|
||||||
@@ -273,11 +273,11 @@ node scripts/publish-all.mjs --dry-run # preview without publishing
|
|||||||
examples/solve-issue.yaml — write a workflow YAML definition
|
examples/solve-issue.yaml — write a workflow YAML definition
|
||||||
│ uwf workflow put
|
│ uwf workflow put
|
||||||
▼
|
▼
|
||||||
~/.uncaged/json-cas/ — Workflow stored as CAS node (unified CAS store)
|
~/.ocas/ — Workflow stored as CAS node (unified CAS store)
|
||||||
~/.uncaged/workflow/registry.yaml — name → hash mapping updated
|
~/.uwf/registry.yaml — name → hash mapping updated
|
||||||
│ uwf thread start <name> -p "..."
|
│ uwf thread start <name> -p "..."
|
||||||
▼
|
▼
|
||||||
~/.uncaged/workflow/threads.yaml — new thread head pointer
|
~/.uwf/threads.yaml — new thread head pointer
|
||||||
│ uwf thread step <thread-id>
|
│ uwf thread step <thread-id>
|
||||||
▼
|
▼
|
||||||
moderator → agent → extract — one step per invocation, repeat until $END
|
moderator → agent → extract — one step per invocation, repeat until $END
|
||||||
|
|||||||
@@ -6,45 +6,64 @@ import { createUwfStore, getCasDir, getGlobalCasDir } from "../store.js";
|
|||||||
|
|
||||||
describe("Global CAS directory", () => {
|
describe("Global CAS directory", () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
let originalEnv: string | undefined;
|
let originalOcasDir: string | undefined;
|
||||||
|
let originalLegacyCasDir: string | undefined;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`);
|
tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`);
|
||||||
await mkdir(tmpDir, { recursive: true });
|
await mkdir(tmpDir, { recursive: true });
|
||||||
originalEnv = process.env.UNCAGED_CAS_DIR;
|
originalOcasDir = process.env.OCAS_DIR;
|
||||||
|
originalLegacyCasDir = process.env.UNCAGED_CAS_DIR;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
if (tmpDir) {
|
if (tmpDir) {
|
||||||
await rm(tmpDir, { recursive: true, force: true });
|
await rm(tmpDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
if (originalEnv === undefined) {
|
if (originalOcasDir === undefined) {
|
||||||
|
delete process.env.OCAS_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.OCAS_DIR = originalOcasDir;
|
||||||
|
}
|
||||||
|
if (originalLegacyCasDir === undefined) {
|
||||||
delete process.env.UNCAGED_CAS_DIR;
|
delete process.env.UNCAGED_CAS_DIR;
|
||||||
} else {
|
} else {
|
||||||
process.env.UNCAGED_CAS_DIR = originalEnv;
|
process.env.UNCAGED_CAS_DIR = originalLegacyCasDir;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getGlobalCasDir returns default path when no env var set", () => {
|
test("getGlobalCasDir returns default path when no env var set", () => {
|
||||||
|
delete process.env.OCAS_DIR;
|
||||||
delete process.env.UNCAGED_CAS_DIR;
|
delete process.env.UNCAGED_CAS_DIR;
|
||||||
const casDir = getGlobalCasDir();
|
const casDir = getGlobalCasDir();
|
||||||
// Should return ~/.uncaged/json-cas
|
expect(casDir).toContain(".ocas");
|
||||||
expect(casDir).toContain(".uncaged");
|
});
|
||||||
expect(casDir).toContain("json-cas");
|
|
||||||
|
test("getGlobalCasDir respects OCAS_DIR environment variable", () => {
|
||||||
|
const customPath = join(tmpDir, "custom-cas");
|
||||||
|
process.env.OCAS_DIR = customPath;
|
||||||
|
const casDir = getGlobalCasDir();
|
||||||
|
expect(casDir).toBe(customPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getGlobalCasDir respects UNCAGED_CAS_DIR environment variable", () => {
|
test("getGlobalCasDir respects UNCAGED_CAS_DIR environment variable", () => {
|
||||||
const customPath = join(tmpDir, "custom-cas");
|
const customPath = join(tmpDir, "legacy-cas");
|
||||||
process.env.UNCAGED_CAS_DIR = customPath;
|
process.env.UNCAGED_CAS_DIR = customPath;
|
||||||
const casDir = getGlobalCasDir();
|
const casDir = getGlobalCasDir();
|
||||||
expect(casDir).toBe(customPath);
|
expect(casDir).toBe(customPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getGlobalCasDir ignores empty UNCAGED_CAS_DIR", () => {
|
test("getGlobalCasDir prefers OCAS_DIR over UNCAGED_CAS_DIR", () => {
|
||||||
process.env.UNCAGED_CAS_DIR = "";
|
process.env.OCAS_DIR = join(tmpDir, "primary-cas");
|
||||||
|
process.env.UNCAGED_CAS_DIR = join(tmpDir, "legacy-cas");
|
||||||
|
expect(getGlobalCasDir()).toBe(join(tmpDir, "primary-cas"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getGlobalCasDir ignores empty OCAS_DIR", () => {
|
||||||
|
process.env.OCAS_DIR = "";
|
||||||
|
delete process.env.UNCAGED_CAS_DIR;
|
||||||
const casDir = getGlobalCasDir();
|
const casDir = getGlobalCasDir();
|
||||||
expect(casDir).toContain(".uncaged");
|
expect(casDir).toContain(".ocas");
|
||||||
expect(casDir).toContain("json-cas");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getCasDir is deprecated but still works for backward compatibility", () => {
|
test("getCasDir is deprecated but still works for backward compatibility", () => {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { lstat, mkdir, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import {
|
||||||
|
getDefaultStorageRoot,
|
||||||
|
getGlobalCasDir,
|
||||||
|
migrateStorageIfNeeded,
|
||||||
|
resolveStorageRoot,
|
||||||
|
} from "../store.js";
|
||||||
|
|
||||||
|
describe("Storage root resolution", () => {
|
||||||
|
const envKeys = [
|
||||||
|
"UWF_STORAGE_ROOT",
|
||||||
|
"WORKFLOW_STORAGE_ROOT",
|
||||||
|
"UNCAGED_WORKFLOW_STORAGE_ROOT",
|
||||||
|
"OCAS_DIR",
|
||||||
|
"UNCAGED_CAS_DIR",
|
||||||
|
] as const;
|
||||||
|
const savedEnv: Partial<Record<(typeof envKeys)[number], string | undefined>> = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
for (const key of envKeys) {
|
||||||
|
savedEnv[key] = process.env[key];
|
||||||
|
delete process.env[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const key of envKeys) {
|
||||||
|
if (savedEnv[key] === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = savedEnv[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getDefaultStorageRoot returns ~/.uwf", () => {
|
||||||
|
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";
|
||||||
|
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = "/tmp/uwf-legacy";
|
||||||
|
expect(resolveStorageRoot()).toBe("/tmp/uwf-primary");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveStorageRoot falls back to WORKFLOW_STORAGE_ROOT", () => {
|
||||||
|
process.env.WORKFLOW_STORAGE_ROOT = "/tmp/uwf-fallback";
|
||||||
|
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = "/tmp/uwf-legacy";
|
||||||
|
expect(resolveStorageRoot()).toBe("/tmp/uwf-fallback");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveStorageRoot falls back to UNCAGED_WORKFLOW_STORAGE_ROOT", () => {
|
||||||
|
process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = "/tmp/uwf-legacy";
|
||||||
|
expect(resolveStorageRoot()).toBe("/tmp/uwf-legacy");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getGlobalCasDir returns ~/.ocas by default", () => {
|
||||||
|
const casDir = getGlobalCasDir();
|
||||||
|
expect(casDir).toBe(join(homedir(), ".ocas"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getGlobalCasDir prefers OCAS_DIR over UNCAGED_CAS_DIR", () => {
|
||||||
|
process.env.OCAS_DIR = "/tmp/ocas-primary";
|
||||||
|
process.env.UNCAGED_CAS_DIR = "/tmp/ocas-legacy";
|
||||||
|
expect(getGlobalCasDir()).toBe("/tmp/ocas-primary");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getGlobalCasDir falls back to UNCAGED_CAS_DIR", () => {
|
||||||
|
process.env.UNCAGED_CAS_DIR = "/tmp/ocas-legacy";
|
||||||
|
expect(getGlobalCasDir()).toBe("/tmp/ocas-legacy");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("migrateStorageIfNeeded", () => {
|
||||||
|
let fakeHome: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
fakeHome = join(
|
||||||
|
homedir(),
|
||||||
|
`.uwf-migrate-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
);
|
||||||
|
await mkdir(fakeHome, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(fakeHome, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates symlinks from legacy paths when new paths are missing", async () => {
|
||||||
|
const oldWorkflow = join(fakeHome, ".uncaged", "workflow");
|
||||||
|
const oldCas = join(fakeHome, ".uncaged", "json-cas");
|
||||||
|
await mkdir(oldWorkflow, { recursive: true });
|
||||||
|
await mkdir(oldCas, { recursive: true });
|
||||||
|
await writeFile(join(oldWorkflow, "config.yaml"), "defaultAgent: test\n", "utf8");
|
||||||
|
|
||||||
|
migrateStorageIfNeeded(fakeHome);
|
||||||
|
|
||||||
|
const newWorkflow = join(fakeHome, ".uwf");
|
||||||
|
const newCas = join(fakeHome, ".ocas");
|
||||||
|
const workflowStat = await lstat(newWorkflow);
|
||||||
|
const casStat = await lstat(newCas);
|
||||||
|
expect(workflowStat.isSymbolicLink()).toBe(true);
|
||||||
|
expect(casStat.isSymbolicLink()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips migration when new paths already exist", async () => {
|
||||||
|
const oldWorkflow = join(fakeHome, ".uncaged", "workflow");
|
||||||
|
const newWorkflow = join(fakeHome, ".uwf");
|
||||||
|
await mkdir(oldWorkflow, { recursive: true });
|
||||||
|
await mkdir(newWorkflow, { recursive: true });
|
||||||
|
|
||||||
|
migrateStorageIfNeeded(fakeHome);
|
||||||
|
|
||||||
|
const stat = await lstat(newWorkflow);
|
||||||
|
expect(stat.isDirectory()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
||||||
|
|
||||||
/** Marker file stored at ~/.uncaged/workflow/running/<thread-id>.json */
|
/** Marker file stored at ~/.uwf/running/<thread-id>.json */
|
||||||
export type RunningMarker = {
|
export type RunningMarker = {
|
||||||
thread: ThreadId;
|
thread: ThreadId;
|
||||||
workflow: CasRef;
|
workflow: CasRef;
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ import {
|
|||||||
import { parseTimeInput } from "./commands/thread-time-parser.js";
|
import { parseTimeInput } from "./commands/thread-time-parser.js";
|
||||||
import { cmdWorkflowAdd, cmdWorkflowList, cmdWorkflowShow } from "./commands/workflow.js";
|
import { cmdWorkflowAdd, cmdWorkflowList, cmdWorkflowShow } from "./commands/workflow.js";
|
||||||
import { formatOutput, type OutputFormat } from "./format.js";
|
import { formatOutput, type OutputFormat } from "./format.js";
|
||||||
import { resolveStorageRoot } from "./store.js";
|
import { migrateStorageIfNeeded, resolveStorageRoot } from "./store.js";
|
||||||
|
|
||||||
|
migrateStorageIfNeeded();
|
||||||
|
|
||||||
function writeOutput(data: unknown): void {
|
function writeOutput(data: unknown): void {
|
||||||
const fmt = program.opts().format as OutputFormat;
|
const fmt = program.opts().format as OutputFormat;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Dirent } from "node:fs";
|
import type { Dirent } from "node:fs";
|
||||||
|
import { existsSync, symlinkSync } from "node:fs";
|
||||||
import { access, appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
import { access, appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -105,27 +106,55 @@ export async function discoverProjectWorkflows(
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default filesystem root for uwf data (`~/.uncaged/workflow`). */
|
/** Default filesystem root for uwf data (`~/.uwf`). */
|
||||||
export function getDefaultStorageRoot(): string {
|
export function getDefaultStorageRoot(): string {
|
||||||
return join(homedir(), ".uncaged", "workflow");
|
return join(homedir(), ".uwf");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve storage root.
|
* Resolve storage root.
|
||||||
* Priority: `UNCAGED_WORKFLOW_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → default.
|
* Priority: `UWF_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → `UNCAGED_WORKFLOW_STORAGE_ROOT` (legacy) → default.
|
||||||
*/
|
*/
|
||||||
export function resolveStorageRoot(): string {
|
export function resolveStorageRoot(): string {
|
||||||
const internal = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
const primary = process.env.UWF_STORAGE_ROOT;
|
||||||
if (internal !== undefined && internal !== "") {
|
if (primary !== undefined && primary !== "") {
|
||||||
return internal;
|
return primary;
|
||||||
}
|
}
|
||||||
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
|
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
|
||||||
if (userOverride !== undefined && userOverride !== "") {
|
if (userOverride !== undefined && userOverride !== "") {
|
||||||
return userOverride;
|
return userOverride;
|
||||||
}
|
}
|
||||||
|
const legacy = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||||
|
if (legacy !== undefined && legacy !== "") {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
return getDefaultStorageRoot();
|
return getDefaultStorageRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Symlink legacy storage paths to ~/.uwf and ~/.ocas when upgrading from older installs. */
|
||||||
|
export function migrateStorageIfNeeded(home: string = homedir()): void {
|
||||||
|
const oldPath = join(home, ".uncaged", "workflow");
|
||||||
|
const newPath = join(home, ".uwf");
|
||||||
|
|
||||||
|
if (!existsSync(newPath) && existsSync(oldPath)) {
|
||||||
|
symlinkSync(oldPath, newPath);
|
||||||
|
// biome-ignore lint/suspicious/noConsole: migration notice
|
||||||
|
console.log("⚠️ Storage migrated: ~/.uwf → ~/.uncaged/workflow (symlink)");
|
||||||
|
// biome-ignore lint/suspicious/noConsole: migration notice
|
||||||
|
console.log(
|
||||||
|
" This symlink is temporary. Copy your data to ~/.uwf/ and remove the symlink in a future version.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldCas = join(home, ".uncaged", "json-cas");
|
||||||
|
const newCas = join(home, ".ocas");
|
||||||
|
if (!existsSync(newCas) && existsSync(oldCas)) {
|
||||||
|
symlinkSync(oldCas, newCas);
|
||||||
|
// biome-ignore lint/suspicious/noConsole: migration notice
|
||||||
|
console.log("⚠️ CAS storage migrated: ~/.ocas → ~/.uncaged/json-cas (symlink)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deprecated: Use `getGlobalCasDir()` instead.
|
* Deprecated: Use `getGlobalCasDir()` instead.
|
||||||
* Returns the old CAS directory for backward compatibility.
|
* Returns the old CAS directory for backward compatibility.
|
||||||
@@ -135,15 +164,19 @@ export function getCasDir(storageRoot: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the global CAS directory shared by all uwf and json-cas tools.
|
* Returns the global CAS directory shared by all uwf and ocas tools.
|
||||||
* Priority: UNCAGED_CAS_DIR environment variable → default ~/.uncaged/json-cas
|
* Priority: `OCAS_DIR` → `UNCAGED_CAS_DIR` (legacy) → default ~/.ocas
|
||||||
*/
|
*/
|
||||||
export function getGlobalCasDir(): string {
|
export function getGlobalCasDir(): string {
|
||||||
const envPath = process.env.UNCAGED_CAS_DIR;
|
const primary = process.env.OCAS_DIR;
|
||||||
if (envPath !== undefined && envPath !== "") {
|
if (primary !== undefined && primary !== "") {
|
||||||
return envPath;
|
return primary;
|
||||||
}
|
}
|
||||||
return join(homedir(), ".uncaged", "json-cas");
|
const legacy = process.env.UNCAGED_CAS_DIR;
|
||||||
|
if (legacy !== undefined && legacy !== "") {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
return join(homedir(), ".ocas");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRegistryPath(storageRoot: string): string {
|
export function getRegistryPath(storageRoot: string): string {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export type StepOutput = {
|
|||||||
background: boolean | null;
|
background: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Active thread entry in ~/.uncaged/workflow/threads.yaml */
|
/** Active thread entry in ~/.uwf/threads.yaml */
|
||||||
export type ThreadIndexEntry = {
|
export type ThreadIndexEntry = {
|
||||||
head: CasRef;
|
head: CasRef;
|
||||||
suspendedRole: string | null;
|
suspendedRole: string | null;
|
||||||
@@ -199,7 +199,7 @@ export type AgentConfig = {
|
|||||||
args: string[];
|
args: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ~/.uncaged/workflow/config.yaml */
|
/** ~/.uwf/config.yaml */
|
||||||
export type WorkflowConfig = {
|
export type WorkflowConfig = {
|
||||||
providers: Record<ProviderAlias, ProviderConfig>;
|
providers: Record<ProviderAlias, ProviderConfig>;
|
||||||
models: Record<ModelAlias, ModelConfig>;
|
models: Record<ModelAlias, ModelConfig>;
|
||||||
@@ -210,5 +210,5 @@ export type WorkflowConfig = {
|
|||||||
modelOverrides: Record<Scenario, ModelAlias> | null;
|
modelOverrides: Record<Scenario, ModelAlias> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** ~/.uncaged/workflow/threads.yaml */
|
/** ~/.uwf/threads.yaml */
|
||||||
export type ThreadsIndex = Record<ThreadId, ThreadIndexEntry>;
|
export type ThreadsIndex = Record<ThreadId, ThreadIndexEntry>;
|
||||||
|
|||||||
@@ -21,24 +21,28 @@ import { parse } from "yaml";
|
|||||||
|
|
||||||
import { registerAgentSchemas } from "./schemas.js";
|
import { registerAgentSchemas } from "./schemas.js";
|
||||||
|
|
||||||
/** Default filesystem root for uwf data (`~/.uncaged/workflow`). */
|
/** Default filesystem root for uwf data (`~/.uwf`). */
|
||||||
export function getDefaultStorageRoot(): string {
|
export function getDefaultStorageRoot(): string {
|
||||||
return join(homedir(), ".uncaged", "workflow");
|
return join(homedir(), ".uwf");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve storage root.
|
* Resolve storage root.
|
||||||
* Priority: `UNCAGED_WORKFLOW_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → default.
|
* Priority: `UWF_STORAGE_ROOT` → `WORKFLOW_STORAGE_ROOT` → `UNCAGED_WORKFLOW_STORAGE_ROOT` (legacy) → default.
|
||||||
*/
|
*/
|
||||||
export function resolveStorageRoot(): string {
|
export function resolveStorageRoot(): string {
|
||||||
const internal = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
const primary = process.env.UWF_STORAGE_ROOT;
|
||||||
if (internal !== undefined && internal !== "") {
|
if (primary !== undefined && primary !== "") {
|
||||||
return internal;
|
return primary;
|
||||||
}
|
}
|
||||||
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
|
const userOverride = process.env.WORKFLOW_STORAGE_ROOT;
|
||||||
if (userOverride !== undefined && userOverride !== "") {
|
if (userOverride !== undefined && userOverride !== "") {
|
||||||
return userOverride;
|
return userOverride;
|
||||||
}
|
}
|
||||||
|
const legacy = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT;
|
||||||
|
if (legacy !== undefined && legacy !== "") {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
return getDefaultStorageRoot();
|
return getDefaultStorageRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ The \`detailHash\` is preserved from the first \`run()\` call — retry \`contin
|
|||||||
|
|
||||||
## Registration
|
## Registration
|
||||||
|
|
||||||
Register your adapter in \`~/.uncaged/workflow/config.yaml\`:
|
Register your adapter in \`~/.uwf/config.yaml\`:
|
||||||
|
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
agents:
|
agents:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ uwf thread exec <thread-id>
|
|||||||
|
|
||||||
## Storage Layout
|
## Storage Layout
|
||||||
|
|
||||||
All data lives under \`~/.uncaged/workflow/\`:
|
All data lives under \`~/.uwf/\`:
|
||||||
- \`cas/\` — content-addressed store (XXH64-keyed)
|
- \`cas/\` — content-addressed store (XXH64-keyed)
|
||||||
- \`threads.yaml\` — active thread index
|
- \`threads.yaml\` — active thread index
|
||||||
- \`history.jsonl\` — completed thread archive
|
- \`history.jsonl\` — completed thread archive
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ export type {
|
|||||||
export { createProcessLogger } from "./process-logger/index.js";
|
export { createProcessLogger } from "./process-logger/index.js";
|
||||||
export { normalizeRefsField } from "./refs-field.js";
|
export { normalizeRefsField } from "./refs-field.js";
|
||||||
export { err, ok } from "./result.js";
|
export { err, ok } from "./result.js";
|
||||||
export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js";
|
export {
|
||||||
|
getDefaultStorageRoot,
|
||||||
|
getDefaultWorkflowStorageRoot,
|
||||||
|
getGlobalCasDir,
|
||||||
|
} from "./storage-root.js";
|
||||||
export type { LogFn, Result } from "./types.js";
|
export type { LogFn, Result } from "./types.js";
|
||||||
export { extractUlidTimestamp, generateUlid } from "./ulid.js";
|
export { extractUlidTimestamp, generateUlid } from "./ulid.js";
|
||||||
export { generateUserReference } from "./user-reference.js";
|
export { generateUserReference } from "./user-reference.js";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { appendFileSync, mkdirSync } from "node:fs";
|
import { appendFileSync, mkdirSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { getDefaultWorkflowStorageRoot } from "../storage-root.js";
|
import { getDefaultStorageRoot } from "../storage-root.js";
|
||||||
import { assertValidLogTag } from "./log-tag.js";
|
import { assertValidLogTag } from "./log-tag.js";
|
||||||
import type { CreateProcessLoggerOptions, ProcessLogger, ProcessLoggerContext } from "./types.js";
|
import type { CreateProcessLoggerOptions, ProcessLogger, ProcessLoggerContext } from "./types.js";
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ function appendEntry(filePath: string, entry: Record<string, string>): void {
|
|||||||
|
|
||||||
/** Process-scoped debug logger — append-only JSONL under `<storageRoot>/logs/YYYY-MM-DD.jsonl`. */
|
/** Process-scoped debug logger — append-only JSONL under `<storageRoot>/logs/YYYY-MM-DD.jsonl`. */
|
||||||
export function createProcessLogger(options: CreateProcessLoggerOptions): ProcessLogger {
|
export function createProcessLogger(options: CreateProcessLoggerOptions): ProcessLogger {
|
||||||
const storageRoot = options.storageRoot ?? getDefaultWorkflowStorageRoot();
|
const storageRoot = options.storageRoot ?? getDefaultStorageRoot();
|
||||||
const processId = `${Date.now()}-${process.pid}`;
|
const processId = `${Date.now()}-${process.pid}`;
|
||||||
const baseContext = options.context;
|
const baseContext = options.context;
|
||||||
const logFilePath = getProcessLogFilePath(storageRoot, new Date());
|
const logFilePath = getProcessLogFilePath(storageRoot, new Date());
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
/** Default filesystem root for workflow data (`~/.uncaged/workflow`). */
|
/** Default filesystem root for workflow data (`~/.uwf`). */
|
||||||
|
export function getDefaultStorageRoot(): string {
|
||||||
|
return join(homedir(), ".uwf");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `getDefaultStorageRoot` instead. */
|
||||||
export function getDefaultWorkflowStorageRoot(): string {
|
export function getDefaultWorkflowStorageRoot(): string {
|
||||||
return join(homedir(), ".uncaged", "workflow");
|
return getDefaultStorageRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Global content-addressed store directory under the workflow storage root (`<root>/cas`). */
|
/** Global content-addressed store directory under the workflow storage root (`<root>/cas`). */
|
||||||
export function getGlobalCasDir(storageRoot: string | undefined): string {
|
export function getGlobalCasDir(storageRoot: string | undefined): string {
|
||||||
const root = storageRoot ?? getDefaultWorkflowStorageRoot();
|
const root = storageRoot ?? getDefaultStorageRoot();
|
||||||
return join(root, "cas");
|
return join(root, "cas");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ uwf setup --provider <name> --base-url <url> \\
|
|||||||
[--agent <name>] # optional default agent
|
[--agent <name>] # optional default agent
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Config is stored at \`~/.uncaged/workflow/config.yaml\`. Override storage root with \`UNCAGED_WORKFLOW_STORAGE_ROOT\`.
|
Config is stored at \`~/.uwf/config.yaml\`. Override storage root with \`UWF_STORAGE_ROOT\` (or \`WORKFLOW_STORAGE_ROOT\`).
|
||||||
|
|
||||||
## Workflow Commands
|
## Workflow Commands
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user