diff --git a/.workflows/solve-issue.yaml b/.workflows/solve-issue.yaml index 1f9fb94..2f1de54 100644 --- a/.workflows/solve-issue.yaml +++ b/.workflows/solve-issue.yaml @@ -22,16 +22,16 @@ roles: After producing the test spec: 1. Store it via `uwf cas put-text ""` and capture the returned hash 2. Put the hash in frontmatter.plan (required when status=ready) - output: "Output a brief summary of the test spec. Frontmatter must include: status (ready or insufficient_info) and plan (CAS hash of the test spec, required when status=ready)." + output: "Output a brief summary of the test spec. Frontmatter must include: $status (ready or insufficient_info) and plan (CAS hash of the test spec, required when status=ready)." frontmatter: type: object properties: - status: + $status: type: string enum: [ready, insufficient_info] plan: type: string - required: [status] + required: [$status] developer: description: "TDD implementation per test spec" goal: "You are a developer agent. You implement code changes following TDD — write tests first, then implementation." @@ -58,14 +58,14 @@ roles: 8. Implement the code to make tests pass 9. Ensure `bun run build` passes with no errors 10. Run `bun test` to verify all tests pass - output: "List all files changed and provide a summary. Frontmatter must include: status (done or failed)." + output: "List all files changed and provide a summary. Frontmatter must include: $status (done or failed)." frontmatter: type: object properties: - status: + $status: type: string enum: [done, failed] - required: [status] + required: [$status] reviewer: description: "Code standards compliance check" goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)." @@ -95,14 +95,14 @@ roles: Only review standards compliance. Do NOT test functionality. If rejecting, you MUST explain the specific reason in your output. - output: "Explain your decision with specific file/line references. Frontmatter must include: status (approved or rejected)." + output: "Explain your decision with specific file/line references. Frontmatter must include: $status (approved or rejected)." frontmatter: type: object properties: - status: + $status: type: string enum: [approved, rejected] - required: [status] + required: [$status] tester: description: "Functional correctness verification" goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec." @@ -118,14 +118,14 @@ roles: - passed: all scenarios verified, tests pass - fix_code: tests fail or implementation doesn't match spec → send back to developer - fix_spec: the spec itself is wrong or incomplete → send back to planner - output: "Report test results per scenario. Frontmatter must include: status (passed, fix_code, or fix_spec)." + output: "Report test results per scenario. Frontmatter must include: $status (passed, fix_code, or fix_spec)." frontmatter: type: object properties: - status: + $status: type: string enum: [passed, fix_code, fix_spec] - required: [status] + required: [$status] committer: description: "Commits and creates PR" goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue." @@ -146,14 +146,14 @@ roles: 5. After PR creation, clean up the worktree: - `cd ~/repos/workflow` - `git worktree remove ~/repos/workflow-worktrees/fix/-` - output: "Include PR URL on success or error log on failure. Frontmatter must include: status (committed or hook_failed)." + output: "Include PR URL on success or error log on failure. Frontmatter must include: $status (committed or hook_failed)." frontmatter: type: object properties: - status: + $status: type: string enum: [committed, hook_failed] - required: [status] + required: [$status] graph: $START: _: { role: "planner", prompt: "Analyze the issue and produce an implementation plan." } diff --git a/examples/analyze-topic.yaml b/examples/analyze-topic.yaml index abedb78..3a01f6a 100644 --- a/examples/analyze-topic.yaml +++ b/examples/analyze-topic.yaml @@ -22,7 +22,7 @@ roles: frontmatter: type: object properties: - status: + $status: enum: ["_"] thesis: type: string @@ -32,7 +32,7 @@ roles: type: string caveats: type: string - required: [status, thesis, keyPoints] + required: [$status, thesis, keyPoints] graph: $START: _: { role: "analyst", prompt: "Analyze the topic in the task and produce a structured summary with key points." } diff --git a/examples/debate.yaml b/examples/debate.yaml index ce5b529..b28749f 100644 --- a/examples/debate.yaml +++ b/examples/debate.yaml @@ -21,11 +21,11 @@ roles: frontmatter: type: object properties: - status: + $status: enum: ["continue", "conceded"] argument: type: string - required: [status, argument] + required: [$status, argument] for: description: "Argues for the proposition" goal: | @@ -46,11 +46,11 @@ roles: frontmatter: type: object properties: - status: + $status: enum: ["continue", "conceded"] argument: type: string - required: [status, argument] + required: [$status, argument] graph: $START: _: { role: "against", prompt: "Present your opening argument against the proposition." } diff --git a/examples/solve-issue.yaml b/examples/solve-issue.yaml index 8e6c8ef..3930679 100644 --- a/examples/solve-issue.yaml +++ b/examples/solve-issue.yaml @@ -27,13 +27,13 @@ roles: frontmatter: type: object properties: - status: + $status: enum: ["_"] repoPath: type: string plan: type: string - required: [status, repoPath, plan] + required: [$status, repoPath, plan] developer: description: "Implements code changes" goal: "You are a developer agent. You implement code changes according to plans." @@ -52,7 +52,7 @@ roles: frontmatter: type: object properties: - status: + $status: enum: ["_"] filesChanged: type: array @@ -60,7 +60,7 @@ roles: type: string summary: type: string - required: [status, filesChanged, summary] + required: [$status, filesChanged, summary] reviewer: description: "Reviews code changes" goal: "You are a code reviewer. You review implementations for correctness and quality." @@ -75,11 +75,11 @@ roles: frontmatter: type: object properties: - status: + $status: enum: ["approved", "rejected"] comments: type: string - required: [status, comments] + required: [$status, comments] graph: $START: _: { role: "planner", prompt: "Analyze the issue described in the task and produce a detailed implementation plan." } diff --git a/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts b/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts index d9f22a8..7d56f95 100644 --- a/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts +++ b/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts @@ -81,7 +81,7 @@ describe("solve-issue workflow: tea pr create worktree fix", () => { expect(workflow.roles.committer?.frontmatter).toBeDefined(); }); - test("committer frontmatter schema should require status field", async () => { + test("committer frontmatter schema should require $status field", async () => { const yamlContent = await readFile(workflowPath, "utf-8"); // Parse as any to access the raw YAML structure (frontmatter is inline JSON Schema in YAML) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -90,8 +90,8 @@ describe("solve-issue workflow: tea pr create worktree fix", () => { const frontmatter = workflow.roles.committer?.frontmatter; expect(frontmatter).toBeDefined(); expect(frontmatter?.type).toBe("object"); - expect(frontmatter?.properties?.status).toBeDefined(); - expect(frontmatter?.properties?.status?.enum).toContain("committed"); - expect(frontmatter?.required).toContain("status"); + expect(frontmatter?.properties?.["$status"]).toBeDefined(); + expect(frontmatter?.properties?.["$status"]?.enum).toContain("committed"); + expect(frontmatter?.required).toContain("$status"); }); }); diff --git a/packages/cli-workflow/src/commands/thread.ts b/packages/cli-workflow/src/commands/thread.ts index 5850bb0..fab2140 100644 --- a/packages/cli-workflow/src/commands/thread.ts +++ b/packages/cli-workflow/src/commands/thread.ts @@ -669,14 +669,16 @@ function formatThreadReadMarkdown(options: { return parts.join("\n\n---\n\n"); } -type EvaluateLastOutput = Record & { status: string }; +type EvaluateLastOutput = Record; + +const STATUS_KEY = "$status"; function resolveEvaluateArgs( uwf: UwfStore, chain: ChainState, ): { lastRole: string; lastOutput: EvaluateLastOutput } { if (chain.headIsStart) { - return { lastRole: START_ROLE, lastOutput: { status: "_" } }; + return { lastRole: START_ROLE, lastOutput: { [STATUS_KEY]: "_" } }; } const lastStep = chain.stepsNewestFirst[0]; @@ -689,11 +691,10 @@ function resolveEvaluateArgs( typeof raw === "object" && raw !== null && !Array.isArray(raw) ? (raw as Record) : {}; - const status = typeof base.status === "string" ? base.status : "_"; return { lastRole: lastStep.role, - lastOutput: { ...base, status }, + lastOutput: base, }; } diff --git a/packages/workflow-moderator/__tests__/evaluate.test.ts b/packages/workflow-moderator/__tests__/evaluate.test.ts index bf0800c..2ff057f 100644 --- a/packages/workflow-moderator/__tests__/evaluate.test.ts +++ b/packages/workflow-moderator/__tests__/evaluate.test.ts @@ -21,7 +21,7 @@ const solveIssueGraph: WorkflowPayload["graph"] = { describe("evaluate", () => { test("$START → first role (unit status _)", () => { - const result = evaluate(solveIssueGraph, "$START", { status: "_" }); + const result = evaluate(solveIssueGraph, "$START", { $status: "_" }); expect(result).toEqual({ ok: true, value: { role: "planner", prompt: "Start planning from the issue in the task." }, @@ -30,7 +30,7 @@ describe("evaluate", () => { test("status-based routing (reviewer rejected → developer)", () => { const result = evaluate(solveIssueGraph, "reviewer", { - status: "rejected", + $status: "rejected", comments: "missing tests", }); expect(result).toEqual({ @@ -40,7 +40,7 @@ describe("evaluate", () => { }); test("status-based routing (reviewer approved → $END)", () => { - const result = evaluate(solveIssueGraph, "reviewer", { status: "approved" }); + const result = evaluate(solveIssueGraph, "reviewer", { $status: "approved" }); expect(result).toEqual({ ok: true, value: { role: "$END", prompt: "Done." }, @@ -48,7 +48,7 @@ describe("evaluate", () => { }); test("missing role in graph → error", () => { - const result = evaluate(solveIssueGraph, "unknown-role", { status: "_" }); + const result = evaluate(solveIssueGraph, "unknown-role", { $status: "_" }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.message).toBe('no transitions defined for role "unknown-role"'); @@ -56,7 +56,7 @@ describe("evaluate", () => { }); test("missing status in graph → error", () => { - const result = evaluate(solveIssueGraph, "reviewer", { status: "pending" }); + const result = evaluate(solveIssueGraph, "reviewer", { $status: "pending" }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.message).toBe('no transition for role "reviewer" with status "pending"'); @@ -65,7 +65,7 @@ describe("evaluate", () => { test("mustache template rendering with simple fields", () => { const result = evaluate(solveIssueGraph, "planner", { - status: "_", + $status: "_", plan: "Add auth middleware", }); expect(result).toEqual({ @@ -76,7 +76,7 @@ describe("evaluate", () => { test("mustache does not HTML-escape prompt content", () => { const result = evaluate(solveIssueGraph, "reviewer", { - status: "rejected", + $status: "rejected", comments: 'use & "Result" types', }); expect(result).toEqual({ @@ -92,7 +92,7 @@ describe("evaluate", () => { }, }; const result = evaluate(graph, "reviewer", { - status: "_", + $status: "_", comments: "", }); expect(result).toEqual({ @@ -101,6 +101,16 @@ describe("evaluate", () => { }); }); + test("missing $status defaults to _ (unit routing)", () => { + const result = evaluate(solveIssueGraph, "planner", { + plan: "Add auth middleware", + }); + expect(result).toEqual({ + ok: true, + value: { role: "developer", prompt: "Implement the plan: Add auth middleware" }, + }); + }); + test("mustache template with nested object paths", () => { const graph: Record> = { reviewer: { @@ -111,7 +121,7 @@ describe("evaluate", () => { }, }; const result = evaluate(graph, "reviewer", { - status: "_", + $status: "_", review: { comments: "refactor the handler" }, }); expect(result).toEqual({ diff --git a/packages/workflow-moderator/src/evaluate.ts b/packages/workflow-moderator/src/evaluate.ts index 69aa469..ae82e4a 100644 --- a/packages/workflow-moderator/src/evaluate.ts +++ b/packages/workflow-moderator/src/evaluate.ts @@ -9,14 +9,21 @@ mustache.escape = (text: string) => text; const START_ROLE = "$START"; const UNIT_STATUS = "_"; -type LastOutput = Record & { status: string }; +type LastOutput = Record; + +const STATUS_KEY = "$status"; export function evaluate( graph: Record>, lastRole: string, lastOutput: LastOutput, ): Result { - const status = lastRole === START_ROLE ? UNIT_STATUS : lastOutput.status; + const status = + lastRole === START_ROLE + ? UNIT_STATUS + : typeof lastOutput[STATUS_KEY] === "string" + ? (lastOutput[STATUS_KEY] as string) + : UNIT_STATUS; const roleTargets = graph[lastRole]; if (roleTargets === undefined) {