refactor: discriminated union frontmatter for solve-issue workflow

- planner: oneOf ready (plan, repoPath) | insufficient_info
- developer: single exit, plain object (branch, worktree), no $status
- reviewer: oneOf approved (branch, worktree) | rejected (comments)
- tester: oneOf passed (branch, worktree) | fix_code (report) | fix_spec (report)
- committer: oneOf committed (prUrl) | hook_failed (error)
- Edge prompts now use mustache templates with variant-specific fields
- Developer simplified from 2 exits to single exit (unit routing)

Phase 2 of #499 (closes #501)
This commit is contained in:
2026-05-25 06:34:56 +00:00
parent 7a19ceca89
commit 827ff13c4a
2 changed files with 66 additions and 51 deletions
+58 -44
View File
@@ -22,15 +22,16 @@ roles:
After producing the test spec: After producing the test spec:
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash 1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
2. Put the hash in frontmatter.plan (required when status=ready) 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. Set $status to ready (with plan hash and repoPath) or insufficient_info."
frontmatter: frontmatter:
type: object oneOf:
properties: - properties:
$status: $status: { const: "ready" }
type: string plan: { type: string }
enum: [ready, insufficient_info] repoPath: { type: string }
plan: required: [$status, plan, repoPath]
type: string - properties:
$status: { const: "insufficient_info" }
required: [$status] required: [$status]
developer: developer:
description: "TDD implementation per test spec" description: "TDD implementation per test spec"
@@ -58,14 +59,13 @@ roles:
8. Implement the code to make tests pass 8. Implement the code to make tests pass
9. Ensure `bun run build` passes with no errors 9. Ensure `bun run build` passes with no errors
10. Run `bun test` to verify all tests pass 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. Include branch name and worktree path in frontmatter."
frontmatter: frontmatter:
type: object type: object
properties: properties:
$status: branch: { type: string }
type: string worktree: { type: string }
enum: [done, failed] required: [branch, worktree]
required: [$status]
reviewer: reviewer:
description: "Code standards compliance check" description: "Code standards compliance check"
goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)." goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)."
@@ -95,14 +95,18 @@ roles:
Only review standards compliance. Do NOT test functionality. Only review standards compliance. Do NOT test functionality.
If rejecting, you MUST explain the specific reason in your output. 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. Set $status to approved (with branch/worktree) or rejected (with comments)."
frontmatter: frontmatter:
type: object oneOf:
properties: - properties:
$status: $status: { const: "approved" }
type: string branch: { type: string }
enum: [approved, rejected] worktree: { type: string }
required: [$status] required: [$status, branch, worktree]
- properties:
$status: { const: "rejected" }
comments: { type: string }
required: [$status, comments]
tester: tester:
description: "Functional correctness verification" description: "Functional correctness verification"
goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec." goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec."
@@ -118,14 +122,22 @@ roles:
- passed: all scenarios verified, tests pass - passed: all scenarios verified, tests pass
- fix_code: tests fail or implementation doesn't match spec → send back to developer - 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 - 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. Set $status to passed (with branch/worktree), fix_code (with report), or fix_spec (with report)."
frontmatter: frontmatter:
type: object oneOf:
properties: - properties:
$status: $status: { const: "passed" }
type: string branch: { type: string }
enum: [passed, fix_code, fix_spec] worktree: { type: string }
required: [$status] required: [$status, branch, worktree]
- properties:
$status: { const: "fix_code" }
report: { type: string }
required: [$status, report]
- properties:
$status: { const: "fix_spec" }
report: { type: string }
required: [$status, report]
committer: committer:
description: "Commits and creates PR" description: "Commits and creates PR"
goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue." goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue."
@@ -146,30 +158,32 @@ roles:
5. After PR creation, clean up the worktree: 5. After PR creation, clean up the worktree:
- `cd ~/repos/workflow` - `cd ~/repos/workflow`
- `git worktree remove ~/repos/workflow-worktrees/fix/<issue-number>-<slug>` - `git worktree remove ~/repos/workflow-worktrees/fix/<issue-number>-<slug>`
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. Set $status to committed (with prUrl) or hook_failed (with error)."
frontmatter: frontmatter:
type: object oneOf:
properties: - properties:
$status: $status: { const: "committed" }
type: string prUrl: { type: string }
enum: [committed, hook_failed] required: [$status, prUrl]
required: [$status] - properties:
$status: { const: "hook_failed" }
error: { type: string }
required: [$status, error]
graph: graph:
$START: $START:
_: { role: "planner", prompt: "Analyze the issue and produce an implementation plan." } _: { role: "planner", prompt: "Analyze the issue and produce an implementation plan." }
planner: planner:
insufficient_info: { role: "$END", prompt: "Insufficient information to proceed; end the workflow." } insufficient_info: { role: "$END", prompt: "Insufficient information to proceed; end the workflow." }
ready: { role: "developer", prompt: "Implement the plan from the planner." } ready: { role: "developer", prompt: "Implement the TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}." }
developer: developer:
failed: { role: "$END", prompt: "Development failed; end the workflow." } _: { role: "reviewer", prompt: "Review branch {{{branch}}} at {{{worktree}}} for code standards compliance." }
done: { role: "reviewer", prompt: "Send the implementation to the reviewer." }
reviewer: reviewer:
rejected: { role: "developer", prompt: "Reviewer rejected the implementation; fix the issues." } rejected: { role: "developer", prompt: "Reviewer rejected: {{{comments}}}. Fix the issues." }
approved: { role: "tester", prompt: "Review passed; run tests on the implementation." } approved: { role: "tester", prompt: "Review passed. Run tests on branch {{{branch}}} at {{{worktree}}}." }
tester: tester:
fix_code: { role: "developer", prompt: "Tests found code issues; return to developer." } fix_code: { role: "developer", prompt: "Tests found code issues: {{{report}}}. Fix and re-submit." }
fix_spec: { role: "planner", prompt: "Tests found spec issues; return to planner." } fix_spec: { role: "planner", prompt: "Tests found spec issues: {{{report}}}. Revise the test spec." }
passed: { role: "committer", prompt: "Tests passed; commit and push the changes." } passed: { role: "committer", prompt: "All tests passed. Commit and push branch {{{branch}}} from {{{worktree}}}." }
committer: committer:
hook_failed: { role: "developer", prompt: "Push hook failed; return to developer to fix." } hook_failed: { role: "developer", prompt: "Push hook failed: {{{error}}}. Fix and re-submit." }
committed: { role: "$END", prompt: "Commit succeeded; complete the workflow." } committed: { role: "$END", prompt: "PR created: {{{prUrl}}}. Workflow complete." }
@@ -81,17 +81,18 @@ describe("solve-issue workflow: tea pr create worktree fix", () => {
expect(workflow.roles.committer?.frontmatter).toBeDefined(); expect(workflow.roles.committer?.frontmatter).toBeDefined();
}); });
test("committer frontmatter schema should require $status field", async () => { test("committer frontmatter schema should be oneOf with $status discriminant", async () => {
const yamlContent = await readFile(workflowPath, "utf-8"); const yamlContent = await readFile(workflowPath, "utf-8");
// Parse as any to access the raw YAML structure (frontmatter is inline JSON Schema in YAML) // 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
const workflow = parse(yamlContent) as any; const workflow = parse(yamlContent) as any;
const frontmatter = workflow.roles.committer?.frontmatter; const frontmatter = workflow.roles.committer?.frontmatter;
expect(frontmatter).toBeDefined(); expect(frontmatter).toBeDefined();
expect(frontmatter?.type).toBe("object"); expect(frontmatter?.oneOf).toBeDefined();
expect(frontmatter?.properties?.["$status"]).toBeDefined(); const committedVariant = frontmatter.oneOf.find(
expect(frontmatter?.properties?.["$status"]?.enum).toContain("committed"); (v: any) => v.properties?.["$status"]?.const === "committed",
expect(frontmatter?.required).toContain("$status"); );
expect(committedVariant).toBeDefined();
expect(committedVariant.required).toContain("$status");
}); });
}); });