feat: migrate examples to status-based routing + fix mustache HTML escape

- Migrate solve-issue.yaml, analyze-topic.yaml, debate.yaml to new format
- Add status enum field to all role frontmatter schemas
- Use {{{ }}} (triple mustache) for prompt templates with user content
- Disable mustache HTML escaping globally (prompts are plain text, not HTML)
- Add 2 new tests for HTML escape behavior
- 9 moderator tests pass

Phase 2 of #490 (closes #492)
This commit is contained in:
2026-05-25 04:52:53 +00:00
parent d00f9df2dd
commit 5a7f417899
5 changed files with 64 additions and 62 deletions
+5 -8
View File
@@ -22,6 +22,8 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status:
enum: ["_"]
thesis: thesis:
type: string type: string
keyPoints: keyPoints:
@@ -30,14 +32,9 @@ roles:
type: string type: string
caveats: caveats:
type: string type: string
required: [thesis, keyPoints] required: [status, thesis, keyPoints]
conditions: {}
graph: graph:
$START: $START:
- role: "analyst" _: { role: "analyst", prompt: "Analyze the topic in the task and produce a structured summary with key points." }
condition: null
prompt: "Analyze the topic in the task and produce a structured summary with key points."
analyst: analyst:
- role: "$END" _: { role: "$END", prompt: "Analysis complete. Finish the workflow." }
condition: null
prompt: "Analysis complete. Finish the workflow."
+15 -30
View File
@@ -16,15 +16,16 @@ roles:
3. If you find yourself genuinely convinced by the other side, you may concede. 3. If you find yourself genuinely convinced by the other side, you may concede.
output: | output: |
Provide your argument in the frontmatter. Provide your argument in the frontmatter.
Set conceded to true ONLY if you are genuinely convinced and wish to stop debating. Set status to "conceded" ONLY if you are genuinely convinced and wish to stop debating.
Otherwise set status to "continue".
frontmatter: frontmatter:
type: object type: object
properties: properties:
status:
enum: ["continue", "conceded"]
argument: argument:
type: string type: string
conceded: required: [status, argument]
type: boolean
required: [argument, conceded]
for: for:
description: "Argues for the proposition" description: "Argues for the proposition"
goal: | goal: |
@@ -40,38 +41,22 @@ roles:
3. If you find yourself genuinely convinced by the other side, you may concede. 3. If you find yourself genuinely convinced by the other side, you may concede.
output: | output: |
Provide your argument in the frontmatter. Provide your argument in the frontmatter.
Set conceded to true ONLY if you are genuinely convinced and wish to stop debating. Set status to "conceded" ONLY if you are genuinely convinced and wish to stop debating.
Otherwise set status to "continue".
frontmatter: frontmatter:
type: object type: object
properties: properties:
status:
enum: ["continue", "conceded"]
argument: argument:
type: string type: string
conceded: required: [status, argument]
type: boolean
required: [argument, conceded]
conditions:
againstConceded:
description: "The against side conceded"
expression: "$last('against').conceded = true"
forConceded:
description: "The for side conceded"
expression: "$last('for').conceded = true"
graph: graph:
$START: $START:
- role: "against" _: { role: "against", prompt: "Present your opening argument against the proposition." }
condition: null
prompt: "Present your opening argument against the proposition."
against: against:
- role: "$END" conceded: { role: "$END", prompt: "The against side conceded. Debate over." }
condition: "againstConceded" continue: { role: "for", prompt: "Counter the opposing argument: {{{argument}}}" }
prompt: "The against side conceded. Debate over."
- role: "for"
condition: null
prompt: "Counter the opposing argument. Address their points directly."
for: for:
- role: "$END" conceded: { role: "$END", prompt: "The for side conceded. Debate over." }
condition: "forConceded" continue: { role: "against", prompt: "Counter the opposing argument: {{{argument}}}" }
prompt: "The for side conceded. Debate over."
- role: "against"
condition: null
prompt: "Counter the opposing argument. Address their points directly."
+14 -24
View File
@@ -27,11 +27,13 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status:
enum: ["_"]
repoPath: repoPath:
type: string type: string
plan: plan:
type: string type: string
required: [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."
@@ -50,13 +52,15 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
status:
enum: ["_"]
filesChanged: filesChanged:
type: array type: array
items: items:
type: string type: string
summary: summary:
type: string type: string
required: [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."
@@ -71,32 +75,18 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
approved: status:
type: boolean enum: ["approved", "rejected"]
comments: comments:
type: string type: string
required: [approved, comments] required: [status, comments]
conditions:
notApproved:
description: "Reviewer rejected the implementation"
expression: "$last('reviewer').approved = false"
graph: graph:
$START: $START:
- role: "planner" _: { role: "planner", prompt: "Analyze the issue described in the task and produce a detailed implementation plan." }
condition: null
prompt: "Analyze the issue described in the task and produce a detailed implementation plan."
planner: planner:
- role: "developer" _: { role: "developer", prompt: "Implement the plan from the planner. Write code, tests, and ensure existing tests pass." }
condition: null
prompt: "Implement the plan from the planner. Write code, tests, and ensure existing tests pass."
developer: developer:
- role: "reviewer" _: { role: "reviewer", prompt: "Review the developer's implementation against the plan for correctness and quality." }
condition: null
prompt: "Review the developer's implementation against the plan for correctness and quality."
reviewer: reviewer:
- role: "developer" approved: { role: "$END", prompt: "The review passed. Complete the workflow." }
condition: "notApproved" rejected: { role: "developer", prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues: {{{comments}}}" }
prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues."
- role: "$END"
condition: null
prompt: "The review passed. Complete the workflow."
@@ -74,6 +74,33 @@ describe("evaluate", () => {
}); });
}); });
test("mustache does not HTML-escape prompt content", () => {
const result = evaluate(solveIssueGraph, "reviewer", {
status: "rejected",
comments: 'use <T> & "Result<T, E>" types',
});
expect(result).toEqual({
ok: true,
value: { role: "developer", prompt: 'Fix: use <T> & "Result<T, E>" types' },
});
});
test("triple mustache also works for unescaped output", () => {
const graph: Record<string, Record<string, Target>> = {
reviewer: {
_: { role: "developer", prompt: "Fix: {{{comments}}}" },
},
};
const result = evaluate(graph, "reviewer", {
status: "_",
comments: "<script>alert(1)</script>",
});
expect(result).toEqual({
ok: true,
value: { role: "developer", prompt: "Fix: <script>alert(1)</script>" },
});
});
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: {
@@ -3,6 +3,9 @@ import mustache from "mustache";
import type { EvaluateResult, Result } from "./types.js"; import type { EvaluateResult, Result } from "./types.js";
// Disable HTML escaping — prompts are plain text, not HTML.
mustache.escape = (text: string) => text;
const START_ROLE = "$START"; const START_ROLE = "$START";
const UNIT_STATUS = "_"; const UNIT_STATUS = "_";