fix: address 3 critical PR review issues

1. threads.yaml race condition: reload threads index after agent subprocess
   completes before updating head pointer (cli-uwf/commands/thread.ts)

2. evaluateJsonata not awaited: jsonata evaluate() returns Promise for async
   expressions — now properly awaited (uwf-moderator/evaluate.ts)

3. resolveWorkflowHash dead code: function always returns a value, removed
   impossible null return type and dead null-check branches at call sites
   (cli-uwf/store.ts, commands/thread.ts, commands/workflow.ts)
This commit is contained in:
2026-05-18 10:05:11 +00:00
parent 0727e0e8d5
commit d90e29ad05
7 changed files with 32 additions and 32 deletions
+3 -1
View File
@@ -2,7 +2,9 @@
"name": "@uncaged/workflow-monorepo", "name": "@uncaged/workflow-monorepo",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"packages/*" "packages/*",
"../json-cas/packages/json-cas",
"../json-cas/packages/json-cas-fs"
], ],
"scripts": { "scripts": {
"build": "bunx tsc --build", "build": "bunx tsc --build",
+6 -7
View File
@@ -61,9 +61,6 @@ async function resolveWorkflowCasRef(
): Promise<CasRef> { ): Promise<CasRef> {
const registry = await loadWorkflowRegistry(storageRoot); const registry = await loadWorkflowRegistry(storageRoot);
const hash = resolveWorkflowHash(registry, workflowId); const hash = resolveWorkflowHash(registry, workflowId);
if (hash === null) {
fail(`workflow not found: ${workflowId}`);
}
if (!isCasRef(hash)) { if (!isCasRef(hash)) {
fail(`workflow not found: ${workflowId}`); fail(`workflow not found: ${workflowId}`);
} }
@@ -386,7 +383,7 @@ export async function cmdThreadStep(
const workflow = loadWorkflowPayload(uwf, workflowHash); const workflow = loadWorkflowPayload(uwf, workflowHash);
const context = buildModeratorContext(uwf, chain); const context = buildModeratorContext(uwf, chain);
const nextResult = evaluate(workflow, context); const nextResult = await evaluate(workflow, context);
if (!nextResult.ok) { if (!nextResult.ok) {
fail(nextResult.error.message); fail(nextResult.error.message);
} }
@@ -415,12 +412,14 @@ export async function cmdThreadStep(
fail(`agent returned hash that is not a StepNode: ${newHead}`); fail(`agent returned hash that is not a StepNode: ${newHead}`);
} }
index[threadId] = newHead; // Reload threads index to avoid overwriting changes made by the agent subprocess
await saveThreadsIndex(storageRoot, index); const freshIndex = await loadThreadsIndex(storageRoot);
freshIndex[threadId] = newHead;
await saveThreadsIndex(storageRoot, freshIndex);
const chainAfter = walkChain(uwfAfter, newHead); const chainAfter = walkChain(uwfAfter, newHead);
const contextAfter = buildModeratorContext(uwfAfter, chainAfter); const contextAfter = buildModeratorContext(uwfAfter, chainAfter);
const afterResult = evaluate(workflow, contextAfter); const afterResult = await evaluate(workflow, contextAfter);
if (!afterResult.ok) { if (!afterResult.ok) {
fail(afterResult.error.message); fail(afterResult.error.message);
} }
@@ -132,9 +132,6 @@ export async function cmdWorkflowShow(
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const registry = await loadWorkflowRegistry(storageRoot); const registry = await loadWorkflowRegistry(storageRoot);
const hash = resolveWorkflowHash(registry, id); const hash = resolveWorkflowHash(registry, id);
if (hash === null) {
fail(`workflow not found: ${id}`);
}
const node = uwf.store.get(hash); const node = uwf.store.get(hash);
if (node === null) { if (node === null) {
+2 -5
View File
@@ -100,11 +100,8 @@ export async function saveWorkflowRegistry(
await writeFile(path, text, "utf8"); await writeFile(path, text, "utf8");
} }
export function resolveWorkflowHash(registry: WorkflowRegistry, id: string): CasRef | null { export function resolveWorkflowHash(registry: WorkflowRegistry, id: string): CasRef {
if (registry[id] !== undefined) { return registry[id] !== undefined ? registry[id] : id;
return registry[id];
}
return id;
} }
export function findRegistryName(registry: WorkflowRegistry, hash: Hash): string | null { export function findRegistryName(registry: WorkflowRegistry, hash: Hash): string | null {
@@ -58,12 +58,12 @@ function makeContext(steps: ModeratorContext["steps"]): ModeratorContext {
} }
describe("evaluate", () => { describe("evaluate", () => {
test("$START → first role (fallback)", () => { test("$START → first role (fallback)", async () => {
const result = evaluate(solveIssueWorkflow, makeContext([])); const result = await evaluate(solveIssueWorkflow, makeContext([]));
expect(result).toEqual({ ok: true, value: "planner" }); expect(result).toEqual({ ok: true, value: "planner" });
}); });
test("condition match (notApproved → developer)", () => { test("condition match (notApproved → developer)", async () => {
const context = makeContext([ const context = makeContext([
{ {
role: "reviewer", role: "reviewer",
@@ -72,11 +72,11 @@ describe("evaluate", () => {
agent: "uwf-hermes", agent: "uwf-hermes",
}, },
]); ]);
const result = evaluate(solveIssueWorkflow, context); const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "developer" }); expect(result).toEqual({ ok: true, value: "developer" });
}); });
test("fallback when condition does not match → $END", () => { test("fallback when condition does not match → $END", async () => {
const context = makeContext([ const context = makeContext([
{ {
role: "reviewer", role: "reviewer",
@@ -85,11 +85,11 @@ describe("evaluate", () => {
agent: "uwf-hermes", agent: "uwf-hermes",
}, },
]); ]);
const result = evaluate(solveIssueWorkflow, context); const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "$END" }); expect(result).toEqual({ ok: true, value: "$END" });
}); });
test("missing role in graph → error", () => { test("missing role in graph → error", async () => {
const context = makeContext([ const context = makeContext([
{ {
role: "unknown-role", role: "unknown-role",
@@ -98,14 +98,14 @@ describe("evaluate", () => {
agent: "uwf-hermes", agent: "uwf-hermes",
}, },
]); ]);
const result = evaluate(solveIssueWorkflow, context); const result = await evaluate(solveIssueWorkflow, context);
expect(result.ok).toBe(false); expect(result.ok).toBe(false);
if (!result.ok) { if (!result.ok) {
expect(result.error.message).toBe('no transitions defined for role "unknown-role"'); expect(result.error.message).toBe('no transitions defined for role "unknown-role"');
} }
}); });
test("output expansion in context works with JSONata", () => { test("output expansion in context works with JSONata", async () => {
const context = makeContext([ const context = makeContext([
{ {
role: "planner", role: "planner",
@@ -114,7 +114,7 @@ describe("evaluate", () => {
agent: "uwf-hermes", agent: "uwf-hermes",
}, },
]); ]);
const result = evaluate(solveIssueWorkflow, context); const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "developer" }); expect(result).toEqual({ ok: true, value: "developer" });
}); });
}); });
+5 -5
View File
@@ -21,9 +21,9 @@ function isTruthy(value: unknown): boolean {
return true; return true;
} }
function evaluateJsonata(expression: string, context: ModeratorContext): Result<unknown, Error> { async function evaluateJsonata(expression: string, context: ModeratorContext): Promise<Result<unknown, Error>> {
try { try {
const result = jsonata(expression).evaluate(context); const result = await jsonata(expression).evaluate(context);
return { ok: true, value: result }; return { ok: true, value: result };
} catch (error) { } catch (error) {
return { return {
@@ -40,10 +40,10 @@ function currentRole(context: ModeratorContext): string {
return context.steps[context.steps.length - 1].role; return context.steps[context.steps.length - 1].role;
} }
export function evaluate( export async function evaluate(
workflow: WorkflowPayload, workflow: WorkflowPayload,
context: ModeratorContext, context: ModeratorContext,
): Result<string, Error> { ): Promise<Result<string, Error>> {
const role = currentRole(context); const role = currentRole(context);
const transitions = workflow.graph[role]; const transitions = workflow.graph[role];
if (transitions === undefined) { if (transitions === undefined) {
@@ -66,7 +66,7 @@ export function evaluate(
}; };
} }
const evalResult = evaluateJsonata(conditionDef.expression, context); const evalResult = await evaluateJsonata(conditionDef.expression, context);
if (!evalResult.ok) { if (!evalResult.ok) {
return evalResult; return evalResult;
} }
+6 -1
View File
@@ -32,6 +32,11 @@
{ "path": "packages/workflow-agent-react" }, { "path": "packages/workflow-agent-react" },
{ "path": "packages/cli-workflow" }, { "path": "packages/cli-workflow" },
{ "path": "packages/workflow-template-solve-issue" }, { "path": "packages/workflow-template-solve-issue" },
{ "path": "packages/workflow-template-develop" } { "path": "packages/workflow-template-develop" },
{ "path": "packages/uwf-protocol" },
{ "path": "packages/uwf-moderator" },
{ "path": "packages/cli-uwf" },
{ "path": "packages/uwf-agent-kit" },
{ "path": "packages/uwf-agent-hermes" }
] ]
} }