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 is contained in:
+15
-15
@@ -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." }
|
||||||
|
|||||||
@@ -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." }
|
||||||
|
|||||||
@@ -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." }
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user