Compare commits

..

2 Commits

Author SHA1 Message Date
xiaoju e8dd398f28 fix: add workflow-agent-claude-code to publish order
小橘 <xiaoju@shazhou.work>
2026-05-27 00:00:09 +00:00
xiaoju 61d95cc47f chore: release v0.5.1
- Add 5 persona-based skills (actor, user, author, developer, adapter)
- Fix skill CLI description truncation (#549)

小橘 <xiaoju@shazhou.work>
2026-05-26 17:30:00 +00:00
39 changed files with 137 additions and 948 deletions
-30
View File
@@ -1,30 +0,0 @@
{
"mode": "exit",
"tag": "alpha",
"initialVersions": {
"@uncaged/cli-workflow": "0.4.5",
"@uncaged/workflow-agent-cursor": "0.4.5",
"@uncaged/workflow-agent-hermes": "0.4.5",
"@uncaged/workflow-agent-llm": "0.4.5",
"@uncaged/workflow-agent-react": "0.4.5",
"@uncaged/workflow-cas": "0.4.5",
"@uncaged/workflow-dashboard": "0.1.0",
"@uncaged/workflow-execute": "0.4.5",
"@uncaged/workflow-gateway": "0.4.5",
"@uncaged/workflow-protocol": "0.4.5",
"@uncaged/workflow-reactor": "0.4.5",
"@uncaged/workflow-register": "0.4.5",
"@uncaged/workflow-runtime": "0.4.5",
"@uncaged/workflow-template-develop": "0.4.5",
"@uncaged/workflow-template-solve-issue": "0.4.5",
"@uncaged/workflow-util": "0.4.5",
"@uncaged/workflow-util-agent": "0.4.5"
},
"changesets": [
"env-api-unify",
"fix-internal-deps",
"fix-publish-src",
"fix-workspace-deps",
"rfc-252-agent-fn"
]
}
+11
View File
@@ -0,0 +1,11 @@
# @uncaged/cli-workflow
## 0.5.1
### Patch Changes
- Add 5 persona-based skills (actor, user, author, developer, adapter) and fix skill CLI description truncation
- Updated dependencies
- @uncaged/workflow-util@0.5.1
- @uncaged/workflow-protocol@0.5.1
- @uncaged/workflow-util-agent@0.5.1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/cli-workflow",
"version": "0.5.0",
"version": "0.5.1",
"files": [
"src",
"dist",
@@ -143,44 +143,6 @@ defaultModel: default
const masked = maskApiKeys(config);
expect(masked).toEqual(config);
});
test("does not mask non-provider apiKey fields", () => {
const config = {
apiKey: "root-level-key",
providers: {
dashscope: { apiKey: "sk-secret" },
},
models: {
default: { provider: "dashscope" },
},
};
const masked = maskApiKeys(config);
// Root-level apiKey should NOT be masked
expect(masked.apiKey).toBe("root-level-key");
// Provider apiKey SHOULD be masked
const providers = masked.providers as Record<string, Record<string, unknown>>;
expect(providers.dashscope.apiKey).toBe("***MASKED***");
});
test("handles empty provider object", () => {
const config = {
providers: { dashscope: {} },
};
const masked = maskApiKeys(config);
expect(masked).toEqual({ providers: { dashscope: {} } });
});
test("handles provider with null apiKey", () => {
const config = {
providers: {
dashscope: { apiKey: null, baseUrl: "https://example.com" },
},
};
const masked = maskApiKeys(config);
const providers = masked.providers as Record<string, Record<string, unknown>>;
expect(providers.dashscope.apiKey).toBe("***MASKED***");
expect(providers.dashscope.baseUrl).toBe("https://example.com");
});
});
});
@@ -656,82 +618,5 @@ defaultModel: default
rmSync(tempDir, { recursive: true, force: true });
}
});
test("agentOverrides — accepts valid 3-segment path", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
try {
createTestConfig(tempDir, sampleConfig);
await cmdConfigSet(tempDir, "agentOverrides.solve-issue.planner", "claude-code");
const value = await cmdConfigGet(tempDir, "agentOverrides.solve-issue.planner");
expect(value).toBe("claude-code");
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test("agentOverrides — rejects incomplete path (2 segments)", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
try {
createTestConfig(tempDir, sampleConfig);
await expect(cmdConfigSet(tempDir, "agentOverrides.solve-issue", "hermes")).rejects.toThrow(
/incomplete path|must specify a field/i,
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test("modelOverrides — accepts valid 2-segment path", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
try {
createTestConfig(tempDir, sampleConfig);
await cmdConfigSet(tempDir, "modelOverrides.extract", "gpt4");
const value = await cmdConfigGet(tempDir, "modelOverrides.extract");
expect(value).toBe("gpt4");
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test("modelOverrides — rejects incomplete path (1 segment only)", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
try {
createTestConfig(tempDir, sampleConfig);
await expect(cmdConfigSet(tempDir, "modelOverrides", "gpt4")).rejects.toThrow(
/incomplete path|must specify a field/i,
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
test("rejects unknown top-level key (regression)", async () => {
const tempDir = mkdtempSync(join(tmpdir(), "test-config-"));
try {
createTestConfig(tempDir, sampleConfig);
await expect(cmdConfigSet(tempDir, "randomKey", "value")).rejects.toThrow(
/Unknown config key/,
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
describe("no legacy apiKeyEnv references", () => {
test("config.ts has no references to apiKeyEnv", () => {
const configSource = readFileSync(join(__dirname, "..", "commands", "config.ts"), "utf8");
expect(configSource).not.toContain("apiKeyEnv");
});
test("config.test.ts has no references to apiKeyEnv (except this test)", () => {
const testSource = readFileSync(__filename, "utf8");
// Remove this test block's own mentions before checking
const withoutThisTest = testSource.replace(
/describe\("no legacy apiKeyEnv references"[\s\S]*$/,
"",
);
expect(withoutThisTest).not.toContain("apiKeyEnv");
});
});
});
@@ -5,17 +5,17 @@ import { evaluate } from "../moderator/evaluate.js";
const solveIssueGraph: WorkflowPayload["graph"] = {
$START: {
_: { role: "planner", prompt: "Start planning from the issue in the task.", location: null },
_: { role: "planner", prompt: "Start planning from the issue in the task." },
},
planner: {
_: { role: "developer", prompt: "Implement the plan: {{plan}}", location: null },
_: { role: "developer", prompt: "Implement the plan: {{plan}}" },
},
developer: {
_: { role: "reviewer", prompt: "Review the changes: {{summary}}", location: null },
_: { role: "reviewer", prompt: "Review the changes: {{summary}}" },
},
reviewer: {
approved: { role: "$END", prompt: "Done.", location: null },
rejected: { role: "developer", prompt: "Fix: {{comments}}", location: null },
approved: { role: "$END", prompt: "Done." },
rejected: { role: "developer", prompt: "Fix: {{comments}}" },
},
};
@@ -24,11 +24,7 @@ describe("evaluate", () => {
const result = evaluate(solveIssueGraph, "$START", { $status: "_" });
expect(result).toEqual({
ok: true,
value: {
role: "planner",
prompt: "Start planning from the issue in the task.",
location: null,
},
value: { role: "planner", prompt: "Start planning from the issue in the task." },
});
});
@@ -39,7 +35,7 @@ describe("evaluate", () => {
});
expect(result).toEqual({
ok: true,
value: { role: "developer", prompt: "Fix: missing tests", location: null },
value: { role: "developer", prompt: "Fix: missing tests" },
});
});
@@ -47,7 +43,7 @@ describe("evaluate", () => {
const result = evaluate(solveIssueGraph, "reviewer", { $status: "approved" });
expect(result).toEqual({
ok: true,
value: { role: "$END", prompt: "Done.", location: null },
value: { role: "$END", prompt: "Done." },
});
});
@@ -74,11 +70,7 @@ describe("evaluate", () => {
});
expect(result).toEqual({
ok: true,
value: {
role: "developer",
prompt: "Implement the plan: Add auth middleware",
location: null,
},
value: { role: "developer", prompt: "Implement the plan: Add auth middleware" },
});
});
@@ -89,14 +81,14 @@ describe("evaluate", () => {
});
expect(result).toEqual({
ok: true,
value: { role: "developer", prompt: 'Fix: use <T> & "Result<T, E>" types', location: null },
value: { role: "developer", prompt: 'Fix: use <T> & "Result<T, E>" types' },
});
});
test("triple mustache also works for unescaped output", () => {
const graph: Record<string, Record<string, Target>> = {
reviewer: {
_: { role: "developer", prompt: "Fix: {{{comments}}}", location: null },
_: { role: "developer", prompt: "Fix: {{{comments}}}" },
},
};
const result = evaluate(graph, "reviewer", {
@@ -105,7 +97,7 @@ describe("evaluate", () => {
});
expect(result).toEqual({
ok: true,
value: { role: "developer", prompt: "Fix: <script>alert(1)</script>", location: null },
value: { role: "developer", prompt: "Fix: <script>alert(1)</script>" },
});
});
@@ -115,11 +107,7 @@ describe("evaluate", () => {
});
expect(result).toEqual({
ok: true,
value: {
role: "developer",
prompt: "Implement the plan: Add auth middleware",
location: null,
},
value: { role: "developer", prompt: "Implement the plan: Add auth middleware" },
});
});
@@ -129,7 +117,6 @@ describe("evaluate", () => {
_: {
role: "developer",
prompt: "Address: {{review.comments}}",
location: null,
},
},
};
@@ -139,7 +126,7 @@ describe("evaluate", () => {
});
expect(result).toEqual({
ok: true,
value: { role: "developer", prompt: "Address: refactor the handler", location: null },
value: { role: "developer", prompt: "Address: refactor the handler" },
});
});
});
@@ -85,7 +85,6 @@ describe("protocol types", () => {
edgePrompt: "",
startedAtMs: 1000,
completedAtMs: 2000,
cwd: "/test/path",
};
expect(record.startedAtMs).toBe(1000);
expect(record.completedAtMs).toBe(2000);
@@ -240,8 +239,8 @@ describe("thread read timing", () => {
},
},
graph: {
$START: { _: { role: "worker", prompt: "go", location: null } },
worker: { _: { role: "$END", prompt: "", location: null } },
$START: { _: { role: "worker", prompt: "go" } },
worker: { _: { role: "$END", prompt: "" } },
},
});
@@ -306,8 +305,8 @@ describe("thread read timing", () => {
},
},
graph: {
$START: { _: { role: "worker", prompt: "go", location: null } },
worker: { _: { role: "$END", prompt: "", location: null } },
$START: { _: { role: "worker", prompt: "go" } },
worker: { _: { role: "$END", prompt: "" } },
},
});
@@ -1,174 +0,0 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { CasRef, StartNodePayload, ThreadId } from "@uncaged/workflow-protocol";
import { describe, expect, test } from "vitest";
import { cmdThreadStart } from "../commands/thread.js";
import { createUwfStore } from "../store.js";
describe("Thread and edge location integration", () => {
let tmpDir: string;
let storageRoot: string;
async function setupTestEnv() {
tmpDir = join(tmpdir(), `uwf-test-location-${Date.now()}`);
storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
}
async function teardown() {
if (tmpDir) {
await rm(tmpDir, { recursive: true, force: true });
}
}
test("thread start captures cwd in StartNode", async () => {
await setupTestEnv();
const workflowYaml = `
name: test-location
description: Test workflow for location feature
roles:
planner:
description: Plans the work
goal: Plan implementation
capabilities: ["planning"]
procedure: Plan
output: |
$status: "ready"
frontmatter:
type: object
required: ["$status"]
properties:
$status: { type: string }
graph:
$START:
_:
role: planner
prompt: "Plan the work"
location: null
planner:
_:
role: $END
prompt: "Done"
location: null
`;
const workflowPath = join(tmpDir, "test-location.yaml");
await writeFile(workflowPath, workflowYaml, "utf8");
const testCwd = "/test/project/path";
const result = await cmdThreadStart(storageRoot, workflowPath, "test prompt", tmpDir, testCwd);
expect(result.thread).toBeDefined();
expect(result.workflow).toBeDefined();
// Verify StartNode has the cwd field
const uwf = await createUwfStore(storageRoot);
const index = await import("../store.js").then((m) => m.loadThreadsIndex(storageRoot));
const headHash = index[result.thread as ThreadId];
expect(headHash).toBeDefined();
const startNode = uwf.store.get(headHash as CasRef);
expect(startNode).not.toBe(null);
expect(startNode?.type).toBe(uwf.schemas.startNode);
const startPayload = startNode?.payload as StartNodePayload;
expect(startPayload.cwd).toBe(testCwd);
await teardown();
});
test("thread start validates cwd is absolute path", async () => {
await setupTestEnv();
const workflowYaml = `
name: test-location
description: Test workflow
roles:
planner:
description: Plans
goal: Plan
capabilities: ["planning"]
procedure: Plan
output: |
$status: "ready"
frontmatter:
type: object
required: ["$status"]
properties:
$status: { type: string }
graph:
$START:
_:
role: planner
prompt: "Plan"
location: null
planner:
_:
role: $END
prompt: "Done"
location: null
`;
const workflowPath = join(tmpDir, "test-location.yaml");
await writeFile(workflowPath, workflowYaml, "utf8");
// Relative path should fail (process.exit is wrapped by vitest)
await expect(
cmdThreadStart(storageRoot, workflowPath, "test", tmpDir, "relative/path"),
).rejects.toThrow();
await teardown();
});
test("thread start uses process.cwd() as default", async () => {
await setupTestEnv();
const workflowYaml = `
name: test-default-cwd
description: Test default cwd
roles:
planner:
description: Plans
goal: Plan
capabilities: ["planning"]
procedure: Plan
output: |
$status: "ready"
frontmatter:
type: object
required: ["$status"]
properties:
$status: { type: string }
graph:
$START:
_:
role: planner
prompt: "Plan"
location: null
planner:
_:
role: $END
prompt: "Done"
location: null
`;
const workflowPath = join(tmpDir, "test-default-cwd.yaml");
await writeFile(workflowPath, workflowYaml, "utf8");
const result = await cmdThreadStart(storageRoot, workflowPath, "test", tmpDir);
const uwf = await createUwfStore(storageRoot);
const index = await import("../store.js").then((m) => m.loadThreadsIndex(storageRoot));
const headHash = index[result.thread as ThreadId];
const startNode = uwf.store.get(headHash as CasRef);
const startPayload = startNode?.payload as StartNodePayload;
// Should default to process.cwd()
expect(startPayload.cwd).toBe(process.cwd());
await teardown();
});
});
@@ -1,227 +0,0 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ThreadId } from "@uncaged/workflow-protocol";
import { describe, expect, test } from "vitest";
import { createMarker, deleteMarker } from "../background/index.js";
import { cmdThreadShow, cmdThreadStart } from "../commands/thread.js";
import { appendThreadHistory, loadThreadsIndex } from "../store.js";
const TEST_WORKFLOW_YAML = `
name: test-status
description: Test workflow for status field
roles:
planner:
description: Plans the work
goal: Plan implementation
capabilities: ["planning"]
procedure: Plan
output: |
$status: "ready"
frontmatter:
type: object
required: ["$status"]
properties:
$status: { type: string }
graph:
$START:
_:
role: planner
prompt: "Plan the work"
location: null
planner:
_:
role: $END
prompt: "Done"
location: null
`;
describe("thread show status field", () => {
let tmpDir: string;
let storageRoot: string;
async function setupTestEnv() {
tmpDir = join(tmpdir(), `uwf-test-status-${Date.now()}`);
storageRoot = join(tmpDir, "storage");
await mkdir(storageRoot, { recursive: true });
}
async function teardown() {
if (tmpDir) {
await rm(tmpDir, { recursive: true, force: true });
}
}
test("active idle thread shows status 'idle'", async () => {
await setupTestEnv();
const workflowPath = join(tmpDir, "test-status.yaml");
await writeFile(workflowPath, TEST_WORKFLOW_YAML, "utf8");
// Create a thread
const startResult = await cmdThreadStart(storageRoot, workflowPath, "test prompt", tmpDir);
const threadId = startResult.thread as ThreadId;
// Show the thread (should be idle)
const result = await cmdThreadShow(storageRoot, threadId);
expect(result.status).toBe("idle");
expect(result.done).toBe(false);
expect(result.background).toBe(null);
expect(result.thread).toBe(threadId);
await teardown();
});
test("active running thread shows status 'running'", async () => {
await setupTestEnv();
const workflowPath = join(tmpDir, "test-status.yaml");
await writeFile(workflowPath, TEST_WORKFLOW_YAML, "utf8");
// Create a thread
const startResult = await cmdThreadStart(storageRoot, workflowPath, "test prompt", tmpDir);
const threadId = startResult.thread as ThreadId;
const workflow = startResult.workflow;
// Create a running marker
await createMarker(storageRoot, {
thread: threadId,
workflow,
pid: process.pid,
startedAt: Date.now(),
});
try {
const result = await cmdThreadShow(storageRoot, threadId);
expect(result.status).toBe("running");
expect(result.done).toBe(false);
expect(result.background).toBe(null);
expect(result.thread).toBe(threadId);
} finally {
// Cleanup: delete marker
await deleteMarker(storageRoot, threadId);
await teardown();
}
});
test("completed thread shows status 'completed'", async () => {
await setupTestEnv();
const workflowPath = join(tmpDir, "test-status.yaml");
await writeFile(workflowPath, TEST_WORKFLOW_YAML, "utf8");
// Create a thread
const startResult = await cmdThreadStart(storageRoot, workflowPath, "test prompt", tmpDir);
const threadId = startResult.thread as ThreadId;
const workflow = startResult.workflow;
// Get the head hash before moving to history
const index = await loadThreadsIndex(storageRoot);
const head = index[threadId];
if (!head) throw new Error("Thread not found in index");
// Move thread to history with reason 'completed'
const { saveThreadsIndex } = await import("../store.js");
const newIndex = { ...index };
delete newIndex[threadId];
await saveThreadsIndex(storageRoot, newIndex);
await appendThreadHistory(storageRoot, {
thread: threadId,
workflow,
head,
completedAt: Date.now(),
reason: "completed",
});
const result = await cmdThreadShow(storageRoot, threadId);
expect(result.status).toBe("completed");
expect(result.done).toBe(true);
expect(result.background).toBe(null);
expect(result.thread).toBe(threadId);
await teardown();
});
test("cancelled thread shows status 'cancelled'", async () => {
await setupTestEnv();
const workflowPath = join(tmpDir, "test-status.yaml");
await writeFile(workflowPath, TEST_WORKFLOW_YAML, "utf8");
// Create a thread
const startResult = await cmdThreadStart(storageRoot, workflowPath, "test prompt", tmpDir);
const threadId = startResult.thread as ThreadId;
const workflow = startResult.workflow;
// Get the head hash before moving to history
const index = await loadThreadsIndex(storageRoot);
const head = index[threadId];
if (!head) throw new Error("Thread not found in index");
// Move thread to history with reason 'cancelled'
const { saveThreadsIndex } = await import("../store.js");
const newIndex = { ...index };
delete newIndex[threadId];
await saveThreadsIndex(storageRoot, newIndex);
await appendThreadHistory(storageRoot, {
thread: threadId,
workflow,
head,
completedAt: Date.now(),
reason: "cancelled",
});
const result = await cmdThreadShow(storageRoot, threadId);
expect(result.status).toBe("cancelled");
expect(result.done).toBe(true);
expect(result.background).toBe(null);
expect(result.thread).toBe(threadId);
await teardown();
});
test("legacy completed thread without reason shows status 'completed'", async () => {
await setupTestEnv();
const workflowPath = join(tmpDir, "test-status.yaml");
await writeFile(workflowPath, TEST_WORKFLOW_YAML, "utf8");
// Create a thread
const startResult = await cmdThreadStart(storageRoot, workflowPath, "test prompt", tmpDir);
const threadId = startResult.thread as ThreadId;
const workflow = startResult.workflow;
// Get the head hash before moving to history
const index = await loadThreadsIndex(storageRoot);
const head = index[threadId];
if (!head) throw new Error("Thread not found in index");
// Move thread to history with reason null (legacy format)
const { saveThreadsIndex } = await import("../store.js");
const newIndex = { ...index };
delete newIndex[threadId];
await saveThreadsIndex(storageRoot, newIndex);
await appendThreadHistory(storageRoot, {
thread: threadId,
workflow,
head,
completedAt: Date.now(),
reason: null,
});
const result = await cmdThreadShow(storageRoot, threadId);
expect(result.status).toBe("completed");
expect(result.done).toBe(true);
expect(result.background).toBe(null);
await teardown();
});
});
@@ -51,11 +51,11 @@ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
},
},
graph: {
$START: { _: { role: "writer", prompt: "Begin writing", location: null } },
writer: { _: { role: "reviewer", prompt: "Review this: {{{plan}}}", location: null } },
$START: { _: { role: "writer", prompt: "Begin writing" } },
writer: { _: { role: "reviewer", prompt: "Review this: {{{plan}}}" } },
reviewer: {
approved: { role: "$END", prompt: "Done: {{{summary}}}", location: null },
rejected: { role: "writer", prompt: "Fix: {{{reason}}}", location: null },
approved: { role: "$END", prompt: "Done: {{{summary}}}" },
rejected: { role: "writer", prompt: "Fix: {{{reason}}}" },
},
},
};
@@ -67,7 +67,7 @@ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
describe("Suite 1: Role Reference Integrity", () => {
test("1.1 graph references unknown role", () => {
const wf = makeWorkflow();
wf.graph.nonexistent = { _: { role: "$END", prompt: "done", location: null } };
wf.graph.nonexistent = { _: { role: "$END", prompt: "done" } };
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes('unknown role "nonexistent"'))).toBe(true);
});
@@ -138,8 +138,8 @@ describe("Suite 2: Graph Structure", () => {
test("2.2 $START has multiple status keys", () => {
const wf = makeWorkflow();
wf.graph.$START = {
_: { role: "writer", prompt: "Begin", location: null },
other: { role: "reviewer", prompt: "Also", location: null },
_: { role: "writer", prompt: "Begin" },
other: { role: "reviewer", prompt: "Also" },
};
const errors = validateWorkflow(wf);
expect(
@@ -149,7 +149,7 @@ describe("Suite 2: Graph Structure", () => {
test("2.3 $START edge uses non-_ status", () => {
const wf = makeWorkflow();
wf.graph.$START = { ready: { role: "writer", prompt: "Begin", location: null } };
wf.graph.$START = { ready: { role: "writer", prompt: "Begin" } };
const errors = validateWorkflow(wf);
expect(
errors.some((e) => e.includes('$START must have exactly one edge with status "_"')),
@@ -158,7 +158,7 @@ describe("Suite 2: Graph Structure", () => {
test("2.4 $END has outgoing edges", () => {
const wf = makeWorkflow();
wf.graph.$END = { _: { role: "writer", prompt: "Loop", location: null } };
wf.graph.$END = { _: { role: "writer", prompt: "Loop" } };
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("$END must not have outgoing edges"))).toBe(true);
});
@@ -177,7 +177,7 @@ describe("Suite 2: Graph Structure", () => {
required: ["$status"],
} as unknown as string,
};
wf.graph.isolated = { _: { role: "$END", prompt: "done", location: null } };
wf.graph.isolated = { _: { role: "$END", prompt: "done" } };
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes('role "isolated" is not reachable from $START'))).toBe(
true,
@@ -186,7 +186,7 @@ describe("Suite 2: Graph Structure", () => {
test("2.6 edge target references invalid role", () => {
const wf = makeWorkflow();
wf.graph.writer = { _: { role: "ghost", prompt: "Go to ghost", location: null } };
wf.graph.writer = { _: { role: "ghost", prompt: "Go to ghost" } };
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes('unknown target role "ghost"'))).toBe(true);
});
@@ -196,8 +196,8 @@ describe("Suite 3: Status-Edge Consistency", () => {
test("3.1 single-exit role with multiple graph keys", () => {
const wf = makeWorkflow();
wf.graph.writer = {
_: { role: "reviewer", prompt: "Review", location: null },
extra: { role: "$END", prompt: "Done", location: null },
_: { role: "reviewer", prompt: "Review" },
extra: { role: "$END", prompt: "Done" },
};
const errors = validateWorkflow(wf);
expect(
@@ -209,7 +209,7 @@ describe("Suite 3: Status-Edge Consistency", () => {
test("3.2 single-exit role missing _ key", () => {
const wf = makeWorkflow();
wf.graph.writer = { done: { role: "reviewer", prompt: "Review", location: null } };
wf.graph.writer = { done: { role: "reviewer", prompt: "Review" } };
const errors = validateWorkflow(wf);
expect(
errors.some((e) => e.includes('role "writer" is single-exit but graph has no "_" key')),
@@ -219,9 +219,9 @@ describe("Suite 3: Status-Edge Consistency", () => {
test("3.3 multi-exit role with extra statuses", () => {
const wf = makeWorkflow();
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done", location: null },
rejected: { role: "writer", prompt: "Fix", location: null },
timeout: { role: "$END", prompt: "Timed out", location: null },
approved: { role: "$END", prompt: "Done" },
rejected: { role: "writer", prompt: "Fix" },
timeout: { role: "$END", prompt: "Timed out" },
};
const errors = validateWorkflow(wf);
expect(
@@ -232,7 +232,7 @@ describe("Suite 3: Status-Edge Consistency", () => {
test("3.4 multi-exit role missing a status", () => {
const wf = makeWorkflow();
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done", location: null },
approved: { role: "$END", prompt: "Done" },
};
const errors = validateWorkflow(wf);
expect(
@@ -242,7 +242,7 @@ describe("Suite 3: Status-Edge Consistency", () => {
test("3.5 multi-exit role with _ key", () => {
const wf = makeWorkflow();
wf.graph.reviewer = { _: { role: "$END", prompt: "Done", location: null } };
wf.graph.reviewer = { _: { role: "$END", prompt: "Done" } };
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes('role "reviewer" is multi-exit but graph uses "_"'))).toBe(
true,
@@ -265,8 +265,8 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
} as unknown as string,
};
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done", location: null },
rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
approved: { role: "$END", prompt: "Done" },
rejected: { role: "writer", prompt: "Fix: {{{comments}}}" },
};
const errors = validateWorkflow(wf);
expect(errors).toEqual([]);
@@ -286,9 +286,9 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
} as unknown as string,
};
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done", location: null },
rejected: { role: "writer", prompt: "Fix", location: null },
timeout: { role: "$END", prompt: "Timed out", location: null },
approved: { role: "$END", prompt: "Done" },
rejected: { role: "writer", prompt: "Fix" },
timeout: { role: "$END", prompt: "Timed out" },
};
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("extra status keys: timeout"))).toBe(true);
@@ -308,7 +308,7 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
} as unknown as string,
};
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done", location: null },
approved: { role: "$END", prompt: "Done" },
};
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("missing status keys: rejected"))).toBe(true);
@@ -327,7 +327,7 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
required: ["$status", "plan"],
} as unknown as string,
};
wf.graph.writer = { _: { role: "reviewer", prompt: "Review: {{{plan}}}", location: null } };
wf.graph.writer = { _: { role: "reviewer", prompt: "Review: {{{plan}}}" } };
const errors = validateWorkflow(wf);
expect(errors).toEqual([]);
});
@@ -346,8 +346,8 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
} as unknown as string,
};
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done: {{{nonexistent}}}", location: null },
rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
approved: { role: "$END", prompt: "Done: {{{nonexistent}}}" },
rejected: { role: "writer", prompt: "Fix: {{{comments}}}" },
};
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("nonexistent") && e.includes("not found"))).toBe(true);
@@ -357,7 +357,7 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => {
describe("Suite 4: Mustache Template Variable Existence", () => {
test("4.1 prompt references nonexistent variable (single-exit)", () => {
const wf = makeWorkflow();
wf.graph.writer = { _: { role: "reviewer", prompt: "Review: {{{branch}}}", location: null } };
wf.graph.writer = { _: { role: "reviewer", prompt: "Review: {{{branch}}}" } };
const errors = validateWorkflow(wf);
expect(
errors.some((e) =>
@@ -369,8 +369,8 @@ describe("Suite 4: Mustache Template Variable Existence", () => {
test("4.2 prompt references nonexistent variable (multi-exit)", () => {
const wf = makeWorkflow();
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done: {{{branch}}}", location: null },
rejected: { role: "writer", prompt: "Fix: {{{reason}}}", location: null },
approved: { role: "$END", prompt: "Done: {{{branch}}}" },
rejected: { role: "writer", prompt: "Fix: {{{reason}}}" },
};
const errors = validateWorkflow(wf);
expect(
@@ -388,7 +388,7 @@ describe("Suite 4: Mustache Template Variable Existence", () => {
test("4.4 $status variable is always valid", () => {
const wf = makeWorkflow();
wf.graph.writer = { _: { role: "reviewer", prompt: "Status: {{$status}}", location: null } };
wf.graph.writer = { _: { role: "reviewer", prompt: "Status: {{$status}}" } };
const errors = validateWorkflow(wf);
expect(errors).toEqual([]);
});
@@ -461,9 +461,9 @@ describe("Suite 6: Multiple Errors Collection", () => {
} as unknown as string,
};
// unknown graph reference
wf.graph.nonexistent = { _: { role: "$END", prompt: "done", location: null } };
wf.graph.nonexistent = { _: { role: "$END", prompt: "done" } };
// bad mustache var
wf.graph.writer = { _: { role: "reviewer", prompt: "{{{badvar}}}", location: null } };
wf.graph.writer = { _: { role: "reviewer", prompt: "{{{badvar}}}" } };
const errors = validateWorkflow(wf);
expect(errors.length).toBeGreaterThanOrEqual(3);
});
@@ -41,8 +41,8 @@ function makeMinimalPayload(name: string, description: string): WorkflowPayload
},
},
graph: {
$START: { _: { role: "worker", prompt: "start working", location: null } },
worker: { _: { role: "$END", prompt: "done", location: null } },
$START: { _: { role: "worker", prompt: "start working" } },
worker: { _: { role: "$END", prompt: "done" } },
},
};
}
+3 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import type { CasRef, ThreadId, ThreadStatus } from "@uncaged/workflow-protocol";
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
import { Command } from "commander";
import {
cmdCasGet,
@@ -38,6 +38,7 @@ import {
cmdThreadStart,
cmdThreadStop,
THREAD_READ_DEFAULT_QUOTA,
type ThreadStatus,
} from "./commands/thread.js";
import { parseTimeInput } from "./commands/thread-time-parser.js";
import { cmdWorkflowAdd, cmdWorkflowList, cmdWorkflowShow } from "./commands/workflow.js";
@@ -563,7 +564,7 @@ program
.option("--base-url <url>", "OpenAI-compatible API base URL")
.option("--api-key <key>", "API key")
.option("--model <name>", "Default model name")
.option("--agent <name>", "Default agent adapter (e.g. hermes → uwf-hermes)")
.option("--agent <name>", "Default agent alias")
.action(
(opts: {
provider?: string;
+3 -18
View File
@@ -5,10 +5,7 @@ import { parse, stringify } from "yaml";
/**
* Valid configuration key schema
*/
const VALID_CONFIG_KEYS: Record<
string,
{ nested: boolean; knownFields?: string[]; minDepth?: number }
> = {
const VALID_CONFIG_KEYS: Record<string, { nested: boolean; knownFields?: string[] }> = {
providers: {
nested: true,
knownFields: ["baseUrl", "apiKey"],
@@ -21,17 +18,6 @@ const VALID_CONFIG_KEYS: Record<
nested: true,
knownFields: ["command", "args"],
},
agentOverrides: {
nested: true,
// agentOverrides.<workflowName>.<roleName> = agentAlias (string value)
// No knownFields — workflow/role names are user-defined
},
modelOverrides: {
nested: true,
minDepth: 2,
// modelOverrides.<scenario> = modelAlias (string value)
// No knownFields — scenarios are user-defined
},
defaultAgent: { nested: false },
defaultModel: { nested: false },
};
@@ -57,9 +43,8 @@ function validateConfigKey(path: string[]): void {
throw new Error(`${topLevel} is a scalar key and cannot have nested properties`);
}
// Nested keys must have at least minDepth segments (default 3)
const minDepth = schema.minDepth ?? 3;
if (schema.nested && path.length < minDepth) {
// Nested keys must have at least 3 segments (e.g., providers.myProvider.baseUrl)
if (schema.nested && path.length < 3) {
const fields = schema.knownFields?.join(", ") ?? "";
throw new Error(
`Incomplete path for ${topLevel}. Must specify a field (e.g., ${topLevel}.<name>.<field>). Valid fields: ${fields}`,
+3 -31
View File
@@ -12,7 +12,6 @@ import type {
StepOutput,
ThreadId,
ThreadListItem,
ThreadStatus,
ThreadsIndex,
WorkflowConfig,
WorkflowPayload,
@@ -267,13 +266,7 @@ export async function cmdThreadStart(
workflowId: string,
prompt: string,
projectRoot: string,
cwd: string = process.cwd(),
): Promise<StartOutput> {
// Validate cwd is an absolute path
if (!isAbsolute(cwd)) {
fail("cwd must be an absolute path");
}
const uwf = await createUwfStore(storageRoot);
const workflowHash = await resolveWorkflowCasRef(uwf, storageRoot, workflowId, projectRoot);
@@ -285,7 +278,6 @@ export async function cmdThreadStart(
const startPayload: StartNodePayload = {
workflow: workflowHash,
prompt,
cwd,
};
const headHash = await uwf.store.put(uwf.schemas.startNode, startPayload);
@@ -316,16 +308,10 @@ export async function cmdThreadShow(storageRoot: string, threadId: ThreadId): Pr
if (workflow === null) {
fail(`failed to resolve workflow from head: ${activeHead}`);
}
// Check if thread is running
const runningMarker = await isThreadRunning(storageRoot, threadId);
const status: ThreadStatus = runningMarker !== null ? "running" : "idle";
return {
workflow,
thread: threadId,
head: activeHead,
status,
done: false,
background: null,
};
@@ -333,13 +319,10 @@ export async function cmdThreadShow(storageRoot: string, threadId: ThreadId): Pr
const hist = await findThreadInHistory(storageRoot, threadId);
if (hist !== null) {
const status: ThreadStatus = hist.reason === "cancelled" ? "cancelled" : "completed";
return {
workflow: hist.workflow,
thread: threadId,
head: hist.head,
status,
done: true,
background: null,
};
@@ -348,6 +331,8 @@ export async function cmdThreadShow(storageRoot: string, threadId: ThreadId): Pr
fail(`thread not found: ${threadId}`);
}
export type ThreadStatus = "idle" | "running" | "completed" | "cancelled";
export type ThreadListItemWithStatus = ThreadListItem & {
status: ThreadStatus;
};
@@ -787,7 +772,6 @@ function spawnAgent(
threadId: ThreadId,
role: string,
edgePrompt: string,
cwd: string,
): CasRef {
const argv = [...agent.args, "--thread", threadId, "--role", role, "--prompt", edgePrompt];
let stdout: string;
@@ -796,7 +780,6 @@ function spawnAgent(
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 50 * 1024 * 1024, // 50 MB — stream-json output can be large
cwd,
});
} catch (e) {
const err = e as NodeJS.ErrnoException & { stderr?: Buffer | string | null };
@@ -955,7 +938,6 @@ async function cmdThreadStepBackground(
workflow: workflowHash,
thread: threadId,
head: headHash,
status: "running",
done: false,
background: true,
},
@@ -998,7 +980,6 @@ async function cmdThreadStepOnce(
workflow: workflowHash,
thread: threadId,
head: headHash,
status: "completed",
done: true,
background: null,
};
@@ -1006,11 +987,6 @@ async function cmdThreadStepOnce(
const role = nextResult.value.role;
const edgePrompt = nextResult.value.prompt;
// Resolve cwd: use edge location if provided, otherwise inherit thread.cwd
const threadCwd = chain.start.cwd;
const effectiveCwd = nextResult.value.location !== null ? nextResult.value.location : threadCwd;
const config = await loadWorkflowConfig(storageRoot);
const agent = resolveAgentConfig(config, workflow, role, agentOverride);
@@ -1019,7 +995,7 @@ async function cmdThreadStepOnce(
});
loadDotenv({ path: getEnvPath(storageRoot) });
const newHead = spawnAgent(plog, agent, threadId, role, edgePrompt, effectiveCwd);
const newHead = spawnAgent(plog, agent, threadId, role, edgePrompt);
plog.log(PL_AGENT_DONE, `agent returned head=${newHead}`, null);
@@ -1051,14 +1027,10 @@ async function cmdThreadStepOnce(
await archiveThread(storageRoot, threadId, workflowHash, newHead);
}
// Determine status based on whether thread is done and running state
const status: ThreadStatus = done ? "completed" : "idle";
return {
workflow: workflowHash,
thread: threadId,
head: newHead,
status,
done,
background: null,
};
@@ -61,7 +61,6 @@ function normalizeGraph(
normalized[status] = {
role: target.role,
prompt: target.prompt,
location: target.location ?? null,
};
}
result[node] = normalized;
@@ -1,107 +0,0 @@
import { describe, expect, test } from "vitest";
import { evaluate } from "../evaluate.js";
describe("Moderator location resolution", () => {
test("returns null location when edge has no location field", () => {
const graph = {
planner: {
ready: {
role: "coder",
prompt: "Implement the code",
location: null,
},
},
};
const result = evaluate(graph, "planner", { $status: "ready" });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.location).toBe(null);
}
});
test("resolves static location string", () => {
const graph = {
planner: {
ready: {
role: "coder",
prompt: "Implement the code",
location: "/static/path",
},
},
};
const result = evaluate(graph, "planner", { $status: "ready" });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.location).toBe("/static/path");
}
});
test("resolves mustache template location", () => {
const graph = {
planner: {
ready: {
role: "coder",
prompt: "Implement the code",
location: "{{{repoPath}}}",
},
},
};
const result = evaluate(graph, "planner", {
$status: "ready",
repoPath: "/home/user/repo",
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.location).toBe("/home/user/repo");
}
});
test("resolves mustache template with multiple variables", () => {
const graph = {
planner: {
ready: {
role: "coder",
prompt: "Implement the code",
location: "{{{basePath}}}/{{{projectName}}}",
},
},
};
const result = evaluate(graph, "planner", {
$status: "ready",
basePath: "/home/user",
projectName: "myproject",
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.location).toBe("/home/user/myproject");
}
});
test("handles missing template variable gracefully", () => {
const graph = {
planner: {
ready: {
role: "coder",
prompt: "Implement the code",
location: "{{{repoPath}}}",
},
},
};
const result = evaluate(graph, "planner", { $status: "ready" });
expect(result.ok).toBe(true);
if (result.ok) {
// Mustache renders missing variables as empty string
expect(result.value.location).toBe("");
}
});
});
@@ -43,8 +43,7 @@ export function evaluate(
try {
const prompt = mustache.render(target.prompt, lastOutput);
const location = target.location !== null ? mustache.render(target.location, lastOutput) : null;
return { ok: true, value: { role: target.role, prompt, location } };
return { ok: true, value: { role: target.role, prompt } };
} catch (error) {
return {
ok: false,
@@ -4,6 +4,4 @@ export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
export type EvaluateResult = {
role: string;
prompt: string;
/** Resolved working directory from edge location field (null = inherit thread cwd). */
location: string | null;
};
+2 -24
View File
@@ -36,13 +36,8 @@ function isTarget(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
const hasValidLocation =
value.location === undefined || value.location === null || typeof value.location === "string";
return (
typeof value.role === "string" &&
typeof value.prompt === "string" &&
value.prompt.trim() !== "" &&
hasValidLocation
typeof value.role === "string" && typeof value.prompt === "string" && value.prompt.trim() !== ""
);
}
@@ -100,22 +95,5 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) {
return null;
}
// Normalize location field: undefined → null
const normalized = { ...raw } as WorkflowPayload;
for (const roleName of Object.keys(normalized.graph)) {
const statusMap = normalized.graph[roleName];
if (statusMap !== undefined) {
for (const status of Object.keys(statusMap)) {
const target = statusMap[status];
if (target !== undefined) {
if (target.location === undefined) {
target.location = null;
}
}
}
}
}
return normalized;
return raw as WorkflowPayload;
}
@@ -0,0 +1,9 @@
# @uncaged/workflow-agent-builtin
## 0.5.1
### Patch Changes
- Updated dependencies
- @uncaged/workflow-util@0.5.1
- @uncaged/workflow-util-agent@0.5.1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-builtin",
"version": "0.5.0",
"version": "0.5.1",
"files": [
"src",
"dist",
@@ -0,0 +1,9 @@
# @uncaged/workflow-agent-claude-code
## 0.5.1
### Patch Changes
- Updated dependencies
- @uncaged/workflow-util@0.5.1
- @uncaged/workflow-util-agent@0.5.1
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-claude-code",
"version": "0.1.0",
"version": "0.5.1",
"files": [
"src",
"dist",
@@ -0,0 +1,10 @@
# @uncaged/workflow-agent-hermes
## 0.5.1
### Patch Changes
- Updated dependencies
- @uncaged/workflow-util@0.5.1
- @uncaged/workflow-protocol@0.5.1
- @uncaged/workflow-util-agent@0.5.1
+2 -4
View File
@@ -1,12 +1,10 @@
# @uncaged/workflow-agent-hermes
`uwf-hermes` — an **agent adapter** that bridges the `uwf` workflow engine and the Hermes CLI.
`uwf-hermes` agent — spawns Hermes chat via ACP and captures session detail.
## Overview
`uwf-hermes` is an adapter (not the Hermes CLI itself). The `uwf` engine speaks a generic agent protocol (stdin/stdout frontmatter contract); `uwf-hermes` translates that protocol into Hermes ACP (Agent Client Protocol) calls. Other adapters (e.g. `uwf-claude-code`, `uwf-cursor`) do the same for their respective CLIs.
On first visit to a role it sends a composed prompt (role definition, task, history, edge prompt); on continuation it resumes the cached session. Session transcripts and raw output are stored as CAS detail nodes.
Layer 3 agent implementation. Wraps the Hermes CLI using the Agent Client Protocol (ACP). On first visit to a role it sends a composed prompt (role definition, task, history, edge prompt); on continuation it resumes the cached session. Session transcripts and raw output are stored as CAS detail nodes.
**Dependencies:** `@uncaged/json-cas`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`
@@ -1,28 +0,0 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const PKG_ROOT = join(import.meta.dir, "..");
describe("Issue #551 — bin entry & engines", () => {
test("package.json declares bun in engines", () => {
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
expect(pkg.engines).toBeDefined();
expect(pkg.engines.bun).toBeDefined();
expect(pkg.engines.bun).toMatch(/^>=?\s*[\d.]+/);
});
test("bin entry file has bun shebang", () => {
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
const binPath = pkg.bin["uwf-hermes"];
const content = readFileSync(join(PKG_ROOT, binPath), "utf-8");
expect(content.startsWith("#!/usr/bin/env bun")).toBe(true);
});
test("README.md explains uwf-hermes is an adapter", () => {
const readme = readFileSync(join(PKG_ROOT, "README.md"), "utf-8");
expect(readme.toLowerCase()).toContain("adapter");
expect(readme).toMatch(/uwf-hermes/);
expect(readme).toMatch(/hermes/);
});
});
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-hermes",
"version": "0.5.0",
"version": "0.5.1",
"files": [
"src",
"dist",
@@ -42,8 +42,5 @@
"bugs": {
"url": "https://github.com/shazhou-ww/uncaged-workflow/issues"
},
"engines": {
"bun": ">= 1.0.0"
},
"license": "MIT"
}
+3
View File
@@ -0,0 +1,3 @@
# @uncaged/workflow-protocol
## 0.5.1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-protocol",
"version": "0.5.0",
"version": "0.5.1",
"files": [
"src",
"dist",
@@ -1,68 +0,0 @@
import { describe, expect, test } from "bun:test";
import type { StartNodePayload, StepRecord, Target } from "../types.js";
describe("Protocol types for thread/edge location", () => {
describe("StartNodePayload", () => {
test("has required cwd field", () => {
const payload: StartNodePayload = {
workflow: "0123456789ABC",
prompt: "Test prompt",
cwd: "/home/user/project",
};
expect(payload.cwd).toBe("/home/user/project");
expect(typeof payload.cwd).toBe("string");
});
});
describe("StepRecord", () => {
test("has required cwd field", () => {
const record: StepRecord = {
role: "planner",
output: "0123456789ABC",
detail: "DEF0123456789",
agent: "uwf-hermes",
edgePrompt: "Plan the implementation",
startedAtMs: Date.now(),
completedAtMs: Date.now() + 1000,
cwd: "/home/user/project",
};
expect(record.cwd).toBe("/home/user/project");
expect(typeof record.cwd).toBe("string");
});
});
describe("Target", () => {
test("has location field that accepts string", () => {
const target: Target = {
role: "coder",
prompt: "Implement the code",
location: "/custom/path",
};
expect(target.location).toBe("/custom/path");
expect(typeof target.location).toBe("string");
});
test("has location field that accepts null", () => {
const target: Target = {
role: "coder",
prompt: "Implement the code",
location: null,
};
expect(target.location).toBe(null);
});
test("location supports mustache template syntax", () => {
const target: Target = {
role: "coder",
prompt: "Implement the code",
location: "{{{repoPath}}}",
};
expect(target.location).toBe("{{{repoPath}}}");
});
});
});
-1
View File
@@ -29,7 +29,6 @@ export type {
ThreadForkOutput,
ThreadId,
ThreadListItem,
ThreadStatus,
ThreadStepsOutput,
ThreadsIndex,
WorkflowConfig,
+2 -17
View File
@@ -20,9 +20,6 @@ const TARGET: JSONSchema = {
properties: {
role: { type: "string" },
prompt: { type: "string" },
location: {
anyOf: [{ type: "string" }, { type: "null" }],
},
},
additionalProperties: false,
};
@@ -52,11 +49,10 @@ export const WORKFLOW_SCHEMA: JSONSchema = {
export const START_NODE_SCHEMA: JSONSchema = {
title: "StartNode",
type: "object",
required: ["workflow", "prompt", "cwd"],
required: ["workflow", "prompt"],
properties: {
workflow: { type: "string", format: "cas_ref" },
prompt: { type: "string" },
cwd: { type: "string" },
},
additionalProperties: false,
};
@@ -64,17 +60,7 @@ export const START_NODE_SCHEMA: JSONSchema = {
export const STEP_NODE_SCHEMA: JSONSchema = {
title: "StepNode",
type: "object",
required: [
"start",
"prev",
"role",
"output",
"detail",
"agent",
"startedAtMs",
"completedAtMs",
"cwd",
],
required: ["start", "prev", "role", "output", "detail", "agent", "startedAtMs", "completedAtMs"],
properties: {
start: { type: "string", format: "cas_ref" },
prev: {
@@ -87,7 +73,6 @@ export const STEP_NODE_SCHEMA: JSONSchema = {
edgePrompt: { type: "string" },
startedAtMs: { type: "integer" },
completedAtMs: { type: "integer" },
cwd: { type: "string" },
},
additionalProperties: false,
};
+1 -17
View File
@@ -18,8 +18,6 @@ export type StepRecord = {
startedAtMs: number;
/** Date.now() after agent returns */
completedAtMs: number;
/** Working directory where the agent executed. Missing in legacy nodes → "". */
cwd: string;
};
// ── 4.2 Workflow 定义 ───────────────────────────────────────────────
@@ -36,8 +34,6 @@ export type RoleDefinition = {
export type Target = {
role: string;
prompt: string;
/** Optional working directory override via mustache template. */
location: string | null;
};
export type WorkflowPayload = {
@@ -52,8 +48,6 @@ export type WorkflowPayload = {
export type StartNodePayload = {
workflow: CasRef;
prompt: string;
/** Working directory where the thread was created. */
cwd: string;
};
export type StepNodePayload = StepRecord & {
@@ -76,27 +70,17 @@ export type ModeratorContext = {
// ── 4.5 CLI 输出 ────────────────────────────────────────────────────
/** Thread status — unified status representation */
export type ThreadStatus = "idle" | "running" | "completed" | "cancelled";
/** uwf thread start */
export type StartOutput = {
workflow: CasRef;
thread: ThreadId;
};
/**
* Output from thread show and thread exec commands.
*
* @property status - Current thread status (idle/running/completed/cancelled)
* @property done - @deprecated Use status field instead. True if thread is completed or cancelled.
* @property background - @deprecated Use status field instead. Always null in current implementation.
*/
/** uwf thread step / uwf thread show */
export type StepOutput = {
workflow: CasRef;
thread: ThreadId;
head: CasRef;
status: ThreadStatus;
done: boolean;
background: boolean | null;
};
@@ -0,0 +1,9 @@
# @uncaged/workflow-util-agent
## 0.5.1
### Patch Changes
- Updated dependencies
- @uncaged/workflow-util@0.5.1
- @uncaged/workflow-protocol@0.5.1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util-agent",
"version": "0.5.0",
"version": "0.5.1",
"files": [
"src",
"dist",
@@ -130,7 +130,6 @@ async function buildHistory(
edgePrompt: step.edgePrompt ?? "",
startedAtMs: step.startedAtMs,
completedAtMs: step.completedAtMs,
cwd: step.cwd ?? "",
content,
});
}
-1
View File
@@ -72,7 +72,6 @@ async function writeStepNode(options: {
edgePrompt: options.edgePrompt,
startedAtMs: options.startedAtMs,
completedAtMs: options.completedAtMs,
cwd: process.cwd(),
};
const hash = await options.store.put(options.schemas.stepNode, payload);
const node = options.store.get(hash);
+7
View File
@@ -0,0 +1,7 @@
# @uncaged/workflow-util
## 0.5.1
### Patch Changes
- Add 5 persona-based skills (actor, user, author, developer, adapter) and fix skill CLI description truncation
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util",
"version": "0.5.0",
"version": "0.5.1",
"files": [
"src",
"dist",
+1
View File
@@ -21,6 +21,7 @@ const publishOrder = [
"workflow-util-agent",
"workflow-agent-hermes",
"workflow-agent-builtin",
"workflow-agent-claude-code",
"cli-workflow",
];