fix(cli): align workflow list with thread start via parent traversal
CI / check (pull_request) Successful in 3m12s
CI / check (pull_request) Successful in 3m12s
discoverProjectWorkflows() now searches .workflow/ in ancestor directories, matching findWorkflowInParents() behavior used by `uwf thread start`. Also documents project-local .workflow/ auto-discovery in usage, cli, and workflow-authoring references. Fixes #162
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
"@united-workforce/cli": patch
|
||||||
|
"@united-workforce/util": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(cli): align `uwf workflow list` with `uwf thread start` parent traversal; document `.workflow/` auto-discovery (#162)
|
||||||
|
|
||||||
|
`discoverProjectWorkflows()` now walks from `cwd` up through parent directories
|
||||||
|
looking for the nearest `.workflow/` (or legacy `.workflows/`), mirroring
|
||||||
|
`findWorkflowInParents()` used by `uwf thread start`. Previously, `uwf workflow
|
||||||
|
list` only inspected the exact `cwd` and returned `[]` when run from any
|
||||||
|
subdirectory, even though `uwf thread start <name>` succeeded from the same
|
||||||
|
location. The two commands now agree on what is discoverable.
|
||||||
|
|
||||||
|
The `@united-workforce/util` reference strings (`generateUsageReference`,
|
||||||
|
`generateCliReference`, `generateWorkflowAuthoringReference`) are updated to
|
||||||
|
document project-local `.workflow/` auto-discovery and recommend it as the
|
||||||
|
primary placement strategy — `uwf workflow add` registration is only needed for
|
||||||
|
global, cwd-independent workflows.
|
||||||
@@ -5,6 +5,7 @@ import { describe, expect, test } from "vitest";
|
|||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
import { generateCliReference } from "@united-workforce/util";
|
||||||
import {
|
import {
|
||||||
cmdPromptAdapterDeveloping,
|
cmdPromptAdapterDeveloping,
|
||||||
cmdPromptBootstrap,
|
cmdPromptBootstrap,
|
||||||
@@ -42,6 +43,24 @@ describe("prompt commands", () => {
|
|||||||
expect(result.length).toBeGreaterThan(500);
|
expect(result.length).toBeGreaterThan(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prompt usage describes .workflow/ auto-discovery", () => {
|
||||||
|
const result = cmdPromptUsage();
|
||||||
|
expect(result).toContain(".workflow/");
|
||||||
|
expect(result).toContain("uwf thread start solve-issue");
|
||||||
|
expect(result.toLowerCase()).toContain("auto-discover");
|
||||||
|
expect(result.toLowerCase()).toContain("recommended");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prompt cli-reference describes .workflow/ auto-discovery", () => {
|
||||||
|
const ref = generateCliReference();
|
||||||
|
expect(ref).toContain(".workflow/");
|
||||||
|
expect(ref.toLowerCase()).toContain("cwd upward");
|
||||||
|
expect(ref).toContain("workflow list");
|
||||||
|
expect(ref).toMatch(/CAS hash/i);
|
||||||
|
expect(ref).toMatch(/file path/i);
|
||||||
|
expect(ref).toMatch(/registry/i);
|
||||||
|
});
|
||||||
|
|
||||||
test("prompt workflow-authoring returns non-empty markdown string with frontmatter", () => {
|
test("prompt workflow-authoring returns non-empty markdown string with frontmatter", () => {
|
||||||
const result = cmdPromptWorkflowAuthoring();
|
const result = cmdPromptWorkflowAuthoring();
|
||||||
expect(typeof result).toBe("string");
|
expect(typeof result).toBe("string");
|
||||||
@@ -56,6 +75,17 @@ describe("prompt commands", () => {
|
|||||||
expect(result.length).toBeGreaterThan(500);
|
expect(result.length).toBeGreaterThan(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prompt workflow-authoring documents .workflow/ Placement section", () => {
|
||||||
|
const result = cmdPromptWorkflowAuthoring();
|
||||||
|
expect(result).toContain("## Placement");
|
||||||
|
expect(result).toContain(".workflow/");
|
||||||
|
expect(result).toContain("solve-issue.yaml");
|
||||||
|
expect(result.toLowerCase()).toContain("auto-discover");
|
||||||
|
expect(result.toLowerCase()).toContain("no workflow add");
|
||||||
|
// Placement must appear before Self-Testing
|
||||||
|
expect(result.indexOf("## Placement")).toBeLessThan(result.indexOf("## Self-Testing"));
|
||||||
|
});
|
||||||
|
|
||||||
test("prompt adapter-developing returns non-empty markdown string with frontmatter", () => {
|
test("prompt adapter-developing returns non-empty markdown string with frontmatter", () => {
|
||||||
const result = cmdPromptAdapterDeveloping();
|
const result = cmdPromptAdapterDeveloping();
|
||||||
expect(typeof result).toBe("string");
|
expect(typeof result).toBe("string");
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import type { CasRef, WorkflowPayload } from "@united-workforce/protocol";
|
||||||
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||||
|
import { stringify } from "yaml";
|
||||||
|
import { cmdThreadStart } from "../commands/thread.js";
|
||||||
|
import { cmdWorkflowList } from "../commands/workflow.js";
|
||||||
|
import type { UwfStore } from "../store.js";
|
||||||
|
import { createUwfStore, discoverProjectWorkflows } from "../store.js";
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
|
||||||
|
const casDir = join(storageRoot, "cas");
|
||||||
|
await mkdir(casDir, { recursive: true });
|
||||||
|
process.env.OCAS_HOME = casDir;
|
||||||
|
return createUwfStore(storageRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMinimalPayload(name: string, description: string): WorkflowPayload {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
roles: {
|
||||||
|
worker: {
|
||||||
|
description: "worker role",
|
||||||
|
goal: "do work",
|
||||||
|
capabilities: [],
|
||||||
|
procedure: "",
|
||||||
|
output: "",
|
||||||
|
frontmatter: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
$status: { const: "done" },
|
||||||
|
},
|
||||||
|
required: ["$status"],
|
||||||
|
} as unknown as CasRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
$START: {
|
||||||
|
new: { role: "worker", prompt: "start working", location: null },
|
||||||
|
resume: { role: "worker", prompt: "resume working", location: null },
|
||||||
|
},
|
||||||
|
worker: { done: { role: "$END", prompt: "done", location: null } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWorkflowYaml(name: string, version: string | null = null): Promise<string> {
|
||||||
|
const payload = makeMinimalPayload(
|
||||||
|
name,
|
||||||
|
version !== null ? `Test workflow (${version})` : "Test workflow",
|
||||||
|
);
|
||||||
|
return stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── fixture ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let tmpDir: string;
|
||||||
|
let storageRoot: string;
|
||||||
|
let projectRoot: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tmpDir = await mkdtemp(join(tmpdir(), "uwf-wf-list-recursive-"));
|
||||||
|
storageRoot = join(tmpDir, "storage");
|
||||||
|
projectRoot = join(tmpDir, "project");
|
||||||
|
await mkdir(storageRoot, { recursive: true });
|
||||||
|
await mkdir(projectRoot, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── discoverProjectWorkflows — parent traversal ───────────────────────────────
|
||||||
|
|
||||||
|
describe("discoverProjectWorkflows — parent traversal", () => {
|
||||||
|
test("B1: finds workflows in cwd's .workflow/", async () => {
|
||||||
|
const wfDir = join(projectRoot, ".workflow");
|
||||||
|
await mkdir(wfDir, { recursive: true });
|
||||||
|
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
||||||
|
|
||||||
|
const entries = await discoverProjectWorkflows(projectRoot);
|
||||||
|
|
||||||
|
expect(entries.map((e) => e.name)).toContain("solve-issue");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("B2: finds workflows in ancestor's .workflow/ when called from subdirectory", async () => {
|
||||||
|
const wfDir = join(projectRoot, ".workflow");
|
||||||
|
await mkdir(wfDir, { recursive: true });
|
||||||
|
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
||||||
|
|
||||||
|
const subdir = join(projectRoot, "packages", "cli", "src");
|
||||||
|
await mkdir(subdir, { recursive: true });
|
||||||
|
|
||||||
|
const entries = await discoverProjectWorkflows(subdir);
|
||||||
|
|
||||||
|
expect(entries.map((e) => e.name)).toContain("solve-issue");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("B3: returns [] when no .workflow/ exists in any ancestor", async () => {
|
||||||
|
// Use a deep path under tmpDir that has no .workflow/ on the way up.
|
||||||
|
// (Traversal will stop at filesystem root and find nothing.)
|
||||||
|
const deepPath = join(tmpDir, "isolated", "no", "workflow", "here");
|
||||||
|
await mkdir(deepPath, { recursive: true });
|
||||||
|
|
||||||
|
const entries = await discoverProjectWorkflows(deepPath);
|
||||||
|
|
||||||
|
expect(entries).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("B4: .workflow/ entries win over .workflows/ within the same directory", async () => {
|
||||||
|
const wfDir = join(projectRoot, ".workflow");
|
||||||
|
const legacyDir = join(projectRoot, ".workflows");
|
||||||
|
await mkdir(wfDir, { recursive: true });
|
||||||
|
await mkdir(legacyDir, { recursive: true });
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
join(wfDir, "solve-issue.yaml"),
|
||||||
|
await createWorkflowYaml("solve-issue", "new"),
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(legacyDir, "solve-issue.yaml"),
|
||||||
|
await createWorkflowYaml("solve-issue", "legacy"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const entries = await discoverProjectWorkflows(projectRoot);
|
||||||
|
|
||||||
|
const match = entries.find((e) => e.name === "solve-issue");
|
||||||
|
expect(match).toBeDefined();
|
||||||
|
expect(match?.filePath).toBe(join(wfDir, "solve-issue.yaml"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("B5: nearest .workflow/ wins over ancestor's .workflow/", async () => {
|
||||||
|
const ancestorWf = join(projectRoot, ".workflow");
|
||||||
|
await mkdir(ancestorWf, { recursive: true });
|
||||||
|
await writeFile(join(ancestorWf, "foo.yaml"), await createWorkflowYaml("foo", "ancestor"));
|
||||||
|
|
||||||
|
const nearDir = join(projectRoot, "pkg");
|
||||||
|
const nearWf = join(nearDir, ".workflow");
|
||||||
|
await mkdir(nearWf, { recursive: true });
|
||||||
|
await writeFile(join(nearWf, "foo.yaml"), await createWorkflowYaml("foo", "near"));
|
||||||
|
|
||||||
|
const entries = await discoverProjectWorkflows(nearDir);
|
||||||
|
|
||||||
|
const match = entries.find((e) => e.name === "foo");
|
||||||
|
expect(match).toBeDefined();
|
||||||
|
expect(match?.filePath).toBe(join(nearWf, "foo.yaml"));
|
||||||
|
// Should not include duplicates from ancestor
|
||||||
|
expect(entries.filter((e) => e.name === "foo")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("B6: returns all entries from the nearest .workflow/ when called from a deep subdir", async () => {
|
||||||
|
const wfDir = join(projectRoot, ".workflow");
|
||||||
|
await mkdir(wfDir, { recursive: true });
|
||||||
|
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
||||||
|
await writeFile(join(wfDir, "review-code.yaml"), await createWorkflowYaml("review-code"));
|
||||||
|
|
||||||
|
const deep = join(projectRoot, "a", "b", "c", "d");
|
||||||
|
await mkdir(deep, { recursive: true });
|
||||||
|
|
||||||
|
const entries = await discoverProjectWorkflows(deep);
|
||||||
|
|
||||||
|
const names = entries.map((e) => e.name).sort();
|
||||||
|
expect(names).toEqual(["review-code", "solve-issue"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("B7: discovers folder-based layout (name/index.yaml) via parent traversal", async () => {
|
||||||
|
const folderDir = join(projectRoot, ".workflow", "solve-issue");
|
||||||
|
await mkdir(folderDir, { recursive: true });
|
||||||
|
await writeFile(join(folderDir, "index.yaml"), await createWorkflowYaml("solve-issue"));
|
||||||
|
|
||||||
|
const subdir = join(projectRoot, "deep", "sub");
|
||||||
|
await mkdir(subdir, { recursive: true });
|
||||||
|
|
||||||
|
const entries = await discoverProjectWorkflows(subdir);
|
||||||
|
|
||||||
|
const match = entries.find((e) => e.name === "solve-issue");
|
||||||
|
expect(match).toBeDefined();
|
||||||
|
expect(match?.filePath).toBe(join(folderDir, "index.yaml"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── cmdWorkflowList — parent traversal ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe("cmdWorkflowList — parent traversal", () => {
|
||||||
|
test("B9: lists local workflows discovered from a subdirectory", async () => {
|
||||||
|
await makeUwfStore(storageRoot);
|
||||||
|
const wfDir = join(projectRoot, ".workflow");
|
||||||
|
await mkdir(wfDir, { recursive: true });
|
||||||
|
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
||||||
|
|
||||||
|
const subdir = join(projectRoot, "packages", "foo", "src");
|
||||||
|
await mkdir(subdir, { recursive: true });
|
||||||
|
|
||||||
|
const result = await cmdWorkflowList(storageRoot, subdir);
|
||||||
|
|
||||||
|
const match = result.find((e) => e.name === "solve-issue");
|
||||||
|
expect(match).toBeDefined();
|
||||||
|
expect(match?.hash).toBe("(local)");
|
||||||
|
expect(match?.origin).toBe("local");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("aligns with cmdThreadStart discovery from same subdirectory", async () => {
|
||||||
|
await makeUwfStore(storageRoot);
|
||||||
|
const wfDir = join(projectRoot, ".workflow");
|
||||||
|
await mkdir(wfDir, { recursive: true });
|
||||||
|
await writeFile(join(wfDir, "foo.yaml"), await createWorkflowYaml("foo"));
|
||||||
|
|
||||||
|
const subdir = join(projectRoot, "packages", "foo", "src");
|
||||||
|
await mkdir(subdir, { recursive: true });
|
||||||
|
|
||||||
|
// cmdThreadStart already resolves foo successfully from subdir (existing behavior)
|
||||||
|
const startResult = await cmdThreadStart(storageRoot, "foo", "prompt", subdir);
|
||||||
|
expect(startResult.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
|
|
||||||
|
// cmdWorkflowList must ALSO include foo (newly aligned behavior)
|
||||||
|
const listResult = await cmdWorkflowList(storageRoot, subdir);
|
||||||
|
const match = listResult.find((e) => e.name === "foo");
|
||||||
|
expect(match).toBeDefined();
|
||||||
|
expect(match?.origin).toBe("local");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import type { Dirent } from "node:fs";
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { access, mkdir, readdir, readFile, rename } from "node:fs/promises";
|
import { access, mkdir, readdir, readFile, rename } from "node:fs/promises";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { dirname, join, resolve as resolvePath } from "node:path";
|
||||||
|
|
||||||
import { bootstrap, type Hash, type Store, type VarStore } from "@ocas/core";
|
import { bootstrap, type Hash, type Store, type VarStore } from "@ocas/core";
|
||||||
import { createFsStore, createSqliteVarStore } from "@ocas/fs";
|
import { createFsStore, createSqliteVarStore } from "@ocas/fs";
|
||||||
@@ -83,15 +83,31 @@ async function scanWorkflowDir(dir: string): Promise<ProjectWorkflowEntry[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scan `<projectRoot>/.workflow/` (preferred) and `.workflows/` (legacy) for workflow entries.
|
* Discover project-local workflows by walking from `startDir` up through parent
|
||||||
* .workflow/ takes priority: if a name is found in both, .workflow/ wins.
|
* directories. The nearest directory that contains a `.workflow/` or `.workflows/`
|
||||||
* Returns an empty array if neither directory exists.
|
* directory wins — once a match is found, traversal stops (entries from more
|
||||||
|
* distant ancestors are NOT merged in).
|
||||||
|
*
|
||||||
|
* Within the winning directory:
|
||||||
|
* - `.workflow/` (preferred) takes priority over `.workflows/` (legacy).
|
||||||
|
* - If both exist in that directory, `.workflow/` entries win when names collide.
|
||||||
|
*
|
||||||
|
* This matches the resolution strategy of `findWorkflowInParents` used by
|
||||||
|
* `uwf thread start`, so `uwf workflow list` and `uwf thread start` agree on
|
||||||
|
* what's discoverable from any given subdirectory.
|
||||||
|
*
|
||||||
|
* Returns an empty array if no `.workflow/` or `.workflows/` directory exists
|
||||||
|
* anywhere from `startDir` up to the filesystem root.
|
||||||
*/
|
*/
|
||||||
export async function discoverProjectWorkflows(
|
export async function discoverProjectWorkflows(startDir: string): Promise<ProjectWorkflowEntry[]> {
|
||||||
projectRoot: string,
|
let currentDir = resolvePath(startDir);
|
||||||
): Promise<ProjectWorkflowEntry[]> {
|
const root = resolvePath("/");
|
||||||
const primary = await scanWorkflowDir(join(projectRoot, ".workflow"));
|
|
||||||
const legacy = await scanWorkflowDir(join(projectRoot, ".workflows"));
|
while (true) {
|
||||||
|
const primary = await scanWorkflowDir(join(currentDir, ".workflow"));
|
||||||
|
const legacy = await scanWorkflowDir(join(currentDir, ".workflows"));
|
||||||
|
|
||||||
|
if (primary.length > 0 || legacy.length > 0) {
|
||||||
const seen = new Set(primary.map((e) => e.name));
|
const seen = new Set(primary.map((e) => e.name));
|
||||||
const merged = [...primary];
|
const merged = [...primary];
|
||||||
for (const entry of legacy) {
|
for (const entry of legacy) {
|
||||||
@@ -100,6 +116,18 @@ export async function discoverProjectWorkflows(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop at filesystem root
|
||||||
|
if (currentDir === root) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parentDir = dirname(currentDir);
|
||||||
|
if (parentDir === currentDir) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
currentDir = parentDir;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default filesystem root for uwf data (`~/.uwf`). */
|
/** Default filesystem root for uwf data (`~/.uwf`). */
|
||||||
|
|||||||
@@ -17,9 +17,24 @@ uwf setup --provider <name> --base-url <url> \\
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
uwf workflow add <file> # register a workflow from YAML file
|
uwf workflow add <file> # register a workflow from YAML file
|
||||||
uwf workflow show <id> # show workflow by name or CAS hash
|
uwf workflow show <id> # show workflow by name or CAS hash
|
||||||
uwf workflow list # list all registered workflows
|
uwf workflow list # list workflows (auto-discovers .workflow/ from cwd upward + global registry)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
### Workflow Resolution
|
||||||
|
|
||||||
|
\`uwf thread start <workflow>\` and \`uwf workflow list\` both resolve the workflow
|
||||||
|
argument by searching from cwd upward. Strategies are tried in priority order:
|
||||||
|
|
||||||
|
1. **CAS hash** — a 13-char Crockford Base32 string is loaded directly from CAS.
|
||||||
|
2. **File path** — a relative or absolute \`.yaml\`/\`.yml\` path is materialized on the fly.
|
||||||
|
3. **Local \`.workflow/\` (cwd upward)** — \`uwf\` searches from cwd upward for the nearest
|
||||||
|
directory containing \`.workflow/<name>.yaml\`, \`.workflow/<name>.yml\`,
|
||||||
|
\`.workflow/<name>/index.yaml\`, or the legacy \`.workflows/\` variants. \`workflow list\`
|
||||||
|
uses the same cwd upward parent traversal so its output matches what \`thread start\`
|
||||||
|
can resolve.
|
||||||
|
4. **Global registry** — \`uwf workflow add\` stores the workflow under
|
||||||
|
\`@uwf/registry/<name>\` for system-wide resolution independent of cwd.
|
||||||
|
|
||||||
## Thread Commands
|
## Thread Commands
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ Guide for using the uwf CLI to manage workflows and threads.
|
|||||||
# 1. Configure provider and model
|
# 1. Configure provider and model
|
||||||
uwf setup
|
uwf setup
|
||||||
|
|
||||||
# 2. Register a workflow
|
# 2. Place a workflow under .workflow/ in your project (recommended)
|
||||||
uwf workflow add my-workflow.yaml
|
# uwf thread start auto-discovers from .workflow/ by walking from cwd upward.
|
||||||
|
# No workflow add registration needed.
|
||||||
|
mkdir -p .workflow
|
||||||
|
cp my-workflow.yaml .workflow/solve-issue.yaml
|
||||||
|
|
||||||
# 3. Start a thread (creates but does not execute)
|
# 3. Start a thread by bare name (no file path)
|
||||||
uwf thread start my-workflow -p "Build a login page"
|
uwf thread start solve-issue -p "Build a login page"
|
||||||
|
|
||||||
# 4. Execute the thread (runs moderator → agent → extract cycles)
|
# 4. Execute the thread (runs moderator → agent → extract cycles)
|
||||||
uwf thread exec <thread-id> # one step
|
uwf thread exec <thread-id> # one step
|
||||||
@@ -51,12 +54,16 @@ Config is stored at \`~/.uwf/config.yaml\`. Override storage root with \`UWF_HOM
|
|||||||
## Workflow Commands
|
## Workflow Commands
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
uwf workflow add <file> # register from YAML file
|
uwf workflow add <file> # register from YAML file (optional)
|
||||||
uwf workflow show <id> # show by name or CAS hash
|
uwf workflow show <id> # show by name or CAS hash
|
||||||
uwf workflow list # list all registered workflows
|
uwf workflow list # list workflows (auto-discovers .workflow/ from cwd upward + global registry)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
You can also pass a file path directly to \`uwf thread start\` without registering first.
|
Three placement strategies, in priority order:
|
||||||
|
|
||||||
|
1. **Project-local \`.workflow/\` (recommended)** — drop \`<name>.yaml\` (or \`<name>/index.yaml\`) under \`<repo>/.workflow/\`. \`uwf thread start <name>\` and \`uwf workflow list\` both auto-discover by walking from cwd upward. No registration step is needed.
|
||||||
|
2. **Explicit file path** — pass a relative or absolute \`.yaml\` path to \`uwf thread start ./path/to/workflow.yaml\`. Useful for one-off runs and testing.
|
||||||
|
3. **Global registry** — \`uwf workflow add <file>\` stores the workflow hash under \`@uwf/registry/<name>\` so it is available system-wide, independent of cwd.
|
||||||
|
|
||||||
## Thread Lifecycle
|
## Thread Lifecycle
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,28 @@ graph:
|
|||||||
failed: { role: cleanup, prompt: "Clean up: {{{error}}}" }
|
failed: { role: cleanup, prompt: "Clean up: {{{error}}}" }
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
## Placement
|
||||||
|
|
||||||
|
Drop your workflow YAML under a project-local \`.workflow/\` directory at (or above)
|
||||||
|
your repo root:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
my-project/
|
||||||
|
.workflow/
|
||||||
|
solve-issue.yaml
|
||||||
|
review-code.yaml
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
\`uwf thread start solve-issue\` will auto-discover \`.workflow/solve-issue.yaml\` by
|
||||||
|
searching from cwd upward — you can run the command from any subdirectory of the
|
||||||
|
project. \`uwf workflow list\` uses the same parent traversal, so its output
|
||||||
|
matches what \`thread start\` can resolve. No workflow add registration needed —
|
||||||
|
\`uwf workflow add\` is only required for global, cwd-independent registration.
|
||||||
|
|
||||||
|
Folder-based layouts also work — \`.workflow/<name>/index.yaml\` (or \`index.yml\`) is
|
||||||
|
discovered as workflow \`<name>\`. The legacy \`.workflows/\` directory remains
|
||||||
|
supported as a fallback when \`.workflow/\` is absent.
|
||||||
|
|
||||||
## Self-Testing
|
## Self-Testing
|
||||||
|
|
||||||
### Step-by-Step Verification
|
### Step-by-Step Verification
|
||||||
|
|||||||
Reference in New Issue
Block a user