refactor: rename status to $status, default to _ when absent

- evaluate() reads $status instead of status, defaults to _ when missing
- Update all YAML examples and .workflows to use $status
- Update cli-workflow resolveEvaluateArgs to use $status
- 10 moderator tests pass including new default _ test
- Single-exit roles no longer need to declare status field

Phase 1 of #499 (closes #500)
This commit was merged in pull request #503.
This commit is contained in:
2026-05-25 06:22:53 +00:00
parent 298b944169
commit 7a19ceca89
8 changed files with 64 additions and 46 deletions
+15 -15
View File
@@ -22,16 +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. Frontmatter must include: $status (ready or insufficient_info) and plan (CAS hash of the test spec, required when status=ready)."
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
type: string type: string
enum: [ready, insufficient_info] enum: [ready, insufficient_info]
plan: plan:
type: string type: string
required: [status] required: [$status]
developer: developer:
description: "TDD implementation per test spec" description: "TDD implementation per test spec"
goal: "You are a developer agent. You implement code changes following TDD — write tests first, then implementation." 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 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. Frontmatter must include: $status (done or failed)."
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
type: string type: string
enum: [done, failed] enum: [done, failed]
required: [status] 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,14 @@ 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. Frontmatter must include: $status (approved or rejected)."
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
type: string type: string
enum: [approved, rejected] enum: [approved, rejected]
required: [status] required: [$status]
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 +118,14 @@ 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. Frontmatter must include: $status (passed, fix_code, or fix_spec)."
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
type: string type: string
enum: [passed, fix_code, fix_spec] enum: [passed, fix_code, fix_spec]
required: [status] required: [$status]
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,14 +146,14 @@ 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. Frontmatter must include: $status (committed or hook_failed)."
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
type: string type: string
enum: [committed, hook_failed] enum: [committed, hook_failed]
required: [status] required: [$status]
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." }
+2 -2
View File
@@ -22,7 +22,7 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
enum: ["_"] enum: ["_"]
thesis: thesis:
type: string type: string
@@ -32,7 +32,7 @@ roles:
type: string type: string
caveats: caveats:
type: string type: string
required: [status, thesis, keyPoints] required: [$status, thesis, keyPoints]
graph: graph:
$START: $START:
_: { role: "analyst", prompt: "Analyze the topic in the task and produce a structured summary with key points." } _: { role: "analyst", prompt: "Analyze the topic in the task and produce a structured summary with key points." }
+4 -4
View File
@@ -21,11 +21,11 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
enum: ["continue", "conceded"] enum: ["continue", "conceded"]
argument: argument:
type: string type: string
required: [status, argument] required: [$status, argument]
for: for:
description: "Argues for the proposition" description: "Argues for the proposition"
goal: | goal: |
@@ -46,11 +46,11 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
enum: ["continue", "conceded"] enum: ["continue", "conceded"]
argument: argument:
type: string type: string
required: [status, argument] required: [$status, argument]
graph: graph:
$START: $START:
_: { role: "against", prompt: "Present your opening argument against the proposition." } _: { role: "against", prompt: "Present your opening argument against the proposition." }
+6 -6
View File
@@ -27,13 +27,13 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
enum: ["_"] enum: ["_"]
repoPath: repoPath:
type: string type: string
plan: plan:
type: string type: string
required: [status, repoPath, plan] required: [$status, repoPath, plan]
developer: developer:
description: "Implements code changes" description: "Implements code changes"
goal: "You are a developer agent. You implement code changes according to plans." goal: "You are a developer agent. You implement code changes according to plans."
@@ -52,7 +52,7 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
enum: ["_"] enum: ["_"]
filesChanged: filesChanged:
type: array type: array
@@ -60,7 +60,7 @@ roles:
type: string type: string
summary: summary:
type: string type: string
required: [status, filesChanged, summary] required: [$status, filesChanged, summary]
reviewer: reviewer:
description: "Reviews code changes" description: "Reviews code changes"
goal: "You are a code reviewer. You review implementations for correctness and quality." goal: "You are a code reviewer. You review implementations for correctness and quality."
@@ -75,11 +75,11 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status: $status:
enum: ["approved", "rejected"] enum: ["approved", "rejected"]
comments: comments:
type: string type: string
required: [status, comments] required: [$status, comments]
graph: graph:
$START: $START:
_: { role: "planner", prompt: "Analyze the issue described in the task and produce a detailed implementation plan." } _: { role: "planner", prompt: "Analyze the issue described in the task and produce a detailed implementation plan." }
@@ -81,7 +81,7 @@ 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 require $status field", 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
@@ -90,8 +90,8 @@ describe("solve-issue workflow: tea pr create worktree fix", () => {
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?.type).toBe("object");
expect(frontmatter?.properties?.status).toBeDefined(); expect(frontmatter?.properties?.["$status"]).toBeDefined();
expect(frontmatter?.properties?.status?.enum).toContain("committed"); expect(frontmatter?.properties?.["$status"]?.enum).toContain("committed");
expect(frontmatter?.required).toContain("status"); expect(frontmatter?.required).toContain("$status");
}); });
}); });
+5 -4
View File
@@ -669,14 +669,16 @@ function formatThreadReadMarkdown(options: {
return parts.join("\n\n---\n\n"); return parts.join("\n\n---\n\n");
} }
type EvaluateLastOutput = Record<string, unknown> & { status: string }; type EvaluateLastOutput = Record<string, unknown>;
const STATUS_KEY = "$status";
function resolveEvaluateArgs( function resolveEvaluateArgs(
uwf: UwfStore, uwf: UwfStore,
chain: ChainState, chain: ChainState,
): { lastRole: string; lastOutput: EvaluateLastOutput } { ): { lastRole: string; lastOutput: EvaluateLastOutput } {
if (chain.headIsStart) { if (chain.headIsStart) {
return { lastRole: START_ROLE, lastOutput: { status: "_" } }; return { lastRole: START_ROLE, lastOutput: { [STATUS_KEY]: "_" } };
} }
const lastStep = chain.stepsNewestFirst[0]; const lastStep = chain.stepsNewestFirst[0];
@@ -689,11 +691,10 @@ function resolveEvaluateArgs(
typeof raw === "object" && raw !== null && !Array.isArray(raw) typeof raw === "object" && raw !== null && !Array.isArray(raw)
? (raw as Record<string, unknown>) ? (raw as Record<string, unknown>)
: {}; : {};
const status = typeof base.status === "string" ? base.status : "_";
return { return {
lastRole: lastStep.role, lastRole: lastStep.role,
lastOutput: { ...base, status }, lastOutput: base,
}; };
} }
@@ -21,7 +21,7 @@ const solveIssueGraph: WorkflowPayload["graph"] = {
describe("evaluate", () => { describe("evaluate", () => {
test("$START → first role (unit status _)", () => { test("$START → first role (unit status _)", () => {
const result = evaluate(solveIssueGraph, "$START", { status: "_" }); const result = evaluate(solveIssueGraph, "$START", { $status: "_" });
expect(result).toEqual({ expect(result).toEqual({
ok: true, ok: true,
value: { role: "planner", prompt: "Start planning from the issue in the task." }, 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)", () => { test("status-based routing (reviewer rejected → developer)", () => {
const result = evaluate(solveIssueGraph, "reviewer", { const result = evaluate(solveIssueGraph, "reviewer", {
status: "rejected", $status: "rejected",
comments: "missing tests", comments: "missing tests",
}); });
expect(result).toEqual({ expect(result).toEqual({
@@ -40,7 +40,7 @@ describe("evaluate", () => {
}); });
test("status-based routing (reviewer approved → $END)", () => { test("status-based routing (reviewer approved → $END)", () => {
const result = evaluate(solveIssueGraph, "reviewer", { status: "approved" }); const result = evaluate(solveIssueGraph, "reviewer", { $status: "approved" });
expect(result).toEqual({ expect(result).toEqual({
ok: true, ok: true,
value: { role: "$END", prompt: "Done." }, value: { role: "$END", prompt: "Done." },
@@ -48,7 +48,7 @@ describe("evaluate", () => {
}); });
test("missing role in graph → error", () => { 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); 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"');
@@ -56,7 +56,7 @@ describe("evaluate", () => {
}); });
test("missing status in graph → error", () => { 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); expect(result.ok).toBe(false);
if (!result.ok) { if (!result.ok) {
expect(result.error.message).toBe('no transition for role "reviewer" with status "pending"'); 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", () => { test("mustache template rendering with simple fields", () => {
const result = evaluate(solveIssueGraph, "planner", { const result = evaluate(solveIssueGraph, "planner", {
status: "_", $status: "_",
plan: "Add auth middleware", plan: "Add auth middleware",
}); });
expect(result).toEqual({ expect(result).toEqual({
@@ -76,7 +76,7 @@ describe("evaluate", () => {
test("mustache does not HTML-escape prompt content", () => { test("mustache does not HTML-escape prompt content", () => {
const result = evaluate(solveIssueGraph, "reviewer", { const result = evaluate(solveIssueGraph, "reviewer", {
status: "rejected", $status: "rejected",
comments: 'use <T> & "Result<T, E>" types', comments: 'use <T> & "Result<T, E>" types',
}); });
expect(result).toEqual({ expect(result).toEqual({
@@ -92,7 +92,7 @@ describe("evaluate", () => {
}, },
}; };
const result = evaluate(graph, "reviewer", { const result = evaluate(graph, "reviewer", {
status: "_", $status: "_",
comments: "<script>alert(1)</script>", comments: "<script>alert(1)</script>",
}); });
expect(result).toEqual({ 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", () => { test("mustache template with nested object paths", () => {
const graph: Record<string, Record<string, Target>> = { const graph: Record<string, Record<string, Target>> = {
reviewer: { reviewer: {
@@ -111,7 +121,7 @@ describe("evaluate", () => {
}, },
}; };
const result = evaluate(graph, "reviewer", { const result = evaluate(graph, "reviewer", {
status: "_", $status: "_",
review: { comments: "refactor the handler" }, review: { comments: "refactor the handler" },
}); });
expect(result).toEqual({ expect(result).toEqual({
+9 -2
View File
@@ -9,14 +9,21 @@ mustache.escape = (text: string) => text;
const START_ROLE = "$START"; const START_ROLE = "$START";
const UNIT_STATUS = "_"; const UNIT_STATUS = "_";
type LastOutput = Record<string, unknown> & { status: string }; type LastOutput = Record<string, unknown>;
const STATUS_KEY = "$status";
export function evaluate( export function evaluate(
graph: Record<string, Record<string, Target>>, graph: Record<string, Record<string, Target>>,
lastRole: string, lastRole: string,
lastOutput: LastOutput, lastOutput: LastOutput,
): Result<EvaluateResult, Error> { ): Result<EvaluateResult, Error> {
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]; const roleTargets = graph[lastRole];
if (roleTargets === undefined) { if (roleTargets === undefined) {