fix: correct skill references and remove hardcoded test path

- moderator-reference: use nested map graph format matching evaluate.ts
- yaml-reference: use goal/procedure/output/capabilities/frontmatter fields
  matching actual WorkflowPayload, not fabricated system/outputSchema
- skill.test.ts: replace hardcoded absolute path with __dirname-relative
- skill.test.ts: assert 'frontmatter' instead of 'outputSchema'
This commit is contained in:
2026-05-25 22:59:38 +08:00
parent d9d542c570
commit 4de13cea44
3 changed files with 84 additions and 78 deletions
@@ -1,5 +1,10 @@
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
const __dirname = dirname(fileURLToPath(import.meta.url));
import { import {
cmdSkillArchitecture, cmdSkillArchitecture,
cmdSkillCli, cmdSkillCli,
@@ -37,7 +42,7 @@ describe("skill commands", () => {
expect(typeof result).toBe("string"); expect(typeof result).toBe("string");
expect(result).toContain("roles"); expect(result).toContain("roles");
expect(result).toContain("graph"); expect(result).toContain("graph");
expect(result).toContain("outputSchema"); expect(result).toContain("frontmatter");
expect(result.length).toBeGreaterThan(200); expect(result.length).toBeGreaterThan(200);
}); });
@@ -59,7 +64,7 @@ describe("skill commands", () => {
test("skill help subcommand is suppressed", () => { test("skill help subcommand is suppressed", () => {
const output = execFileSync("bun", ["src/cli.ts", "skill", "--help"], { const output = execFileSync("bun", ["src/cli.ts", "skill", "--help"], {
cwd: "/Users/scottwei/Code/workflow/.worktrees/fix/517-expand-skill/packages/cli-workflow", cwd: join(__dirname, "..", ".."),
encoding: "utf-8", encoding: "utf-8",
env: { ...process.env, PATH: `/opt/homebrew/bin:${process.env.PATH}` }, env: { ...process.env, PATH: `/opt/homebrew/bin:${process.env.PATH}` },
}); });
@@ -7,58 +7,50 @@ The moderator is the workflow engine's routing component. It evaluates the direc
## Status-Based Routing ## Status-Based Routing
The moderator uses **status-based routing**: it inspects the previous step's extracted output (specifically the \`$status\` field and other output fields) and matches them against edge conditions in the graph. The moderator uses **status-based routing**: it inspects the previous step's extracted output (specifically the \`$status\` field) and looks up the corresponding edge in the graph.
### Routing Algorithm ### Graph Structure
1. Find all edges where \`from\` matches the current role The graph is a nested map: \`Record<Role | "$START", Record<Status, Target>>\`. Each role maps its possible \`$status\` values to a target with a \`role\` and \`prompt\`:
2. For each edge (in order), evaluate the \`when\` condition:
- If \`when\` is absent → unconditional match (always taken)
- If \`when\` is present → every key/value pair must match the step output
3. The first matching edge determines the next role
4. If no edge matches → thread stalls (error condition)
### Example
\`\`\`yaml \`\`\`yaml
graph: graph:
- from: developer $START:
to: reviewer _: { role: planner, prompt: "Analyze the issue." }
when: planner:
$status: done ready: { role: developer, prompt: "Implement the plan (CAS hash: {{{plan}}})." }
- from: developer insufficient_info: { role: $END, prompt: "Not enough info." }
to: $END developer:
when: done: { role: reviewer, prompt: "Review branch {{{branch}}} at {{{worktree}}}." }
$status: failed failed: { role: $END, prompt: "Developer failed: {{{reason}}}." }
- from: reviewer reviewer:
to: developer approved: { role: tester, prompt: "Run tests on {{{branch}}} at {{{worktree}}}." }
when: rejected: { role: developer, prompt: "Fix issues: {{{comments}}}." }
$status: needs-changes
- from: reviewer
to: $END
when:
$status: approved
\`\`\` \`\`\`
In this graph: ### Routing Algorithm
- After \`developer\` produces \`$status: done\`, the moderator routes to \`reviewer\`
- After \`reviewer\` produces \`$status: needs-changes\`, it routes back to \`developer\`
- \`$status: failed\` or \`$status: approved\` terminates the thread
## Edge Evaluation Details 1. Look up \`graph[lastRole]\` to get the status map for the current role
2. Look up \`statusMap[lastOutput.$status]\` to get the target
3. If target role is \`$END\`, mark thread as completed
4. Otherwise, render the edge prompt (Mustache templates with \`{{{field}}}\` from output) and spawn the next agent
- Edges are evaluated **in declaration order** — put specific conditions before general ones ### Edge Prompts and Mustache Templates
- \`when\` values are compared as **exact string matches**
- Multiple \`when\` fields are ANDed — all must match Edge prompts use triple-brace Mustache syntax (\`{{{field}}}\`) to interpolate values from the previous step's output into the next agent's task prompt. This passes structured data (branch names, file paths, CAS hashes) between roles without manual wiring.
- An edge without \`when\` acts as a **fallback** — place it last
## Special Nodes
- \`$START\` — entry point; uses status key \`_\` (unconditional) since there is no previous output
- \`$END\` — terminal node; thread completes when reached and is moved to history
## Integration with Steps ## Integration with Steps
Each \`uwf thread exec\` cycle: Each \`uwf thread exec\` cycle:
1. Moderator reads the thread's head step output 1. Moderator reads the thread's head step output
2. Evaluates graph edges to pick the next role 2. Looks up \`graph[lastRole][output.$status]\` to pick the next role
3. If next is \`$END\`, marks thread as completed 3. If next is \`$END\`, marks thread as completed
4. Otherwise, spawns the agent for the selected role 4. Otherwise, renders the edge prompt and spawns the agent for the selected role
5. Extract pipeline parses agent output → new step node → append to CAS chain 5. Extract pipeline parses agent output → new step node → append to CAS chain
`; `;
} }
+48 -39
View File
@@ -11,29 +11,31 @@ description: "..." # human-readable description
roles: # named actors in the workflow roles: # named actors in the workflow
planner: planner:
system: | # system prompt for the agent description: "Analyzes issue and outputs a plan"
You are a planner... goal: "You are a planning agent."
outputSchema: # JSON Schema for structured output capabilities:
type: object - issue-analysis
required: [plan, $status] - planning
properties: procedure: |
plan: 1. Read the issue
type: string 2. Produce a test spec
$status: output: "Output the plan summary. Set $status to ready or insufficient_info."
type: string frontmatter: # JSON Schema for structured output (drives routing)
enum: [ready, failed] oneOf:
- properties:
$status: { const: ready }
plan: { type: string }
required: [$status, plan]
- properties:
$status: { const: insufficient_info }
required: [$status]
graph: # status-based routing edges graph: # status-based routing (nested map)
- from: $START $START:
to: planner _: { role: planner, prompt: "Analyze the issue." }
- from: planner planner:
to: developer ready: { role: developer, prompt: "Implement plan {{{plan}}}." }
when: insufficient_info: { role: $END, prompt: "Not enough info." }
$status: ready
- from: planner
to: $END
when:
$status: failed
\`\`\` \`\`\`
## roles ## roles
@@ -42,32 +44,39 @@ Each role defines an actor in the workflow:
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------| |-------|------|-------------|
| \`system\` | string | System prompt — instructions for the agent | | \`description\` | string | Short description of the role's purpose |
| \`outputSchema\` | JSON Schema | Defines the structured output the agent must produce | | \`goal\` | string | System-level goal statement for the agent |
| \`agent\` | string (optional) | Override the default agent command for this role | | \`capabilities\` | string[] | Tags describing what the role can do |
| \`procedure\` | string | Step-by-step instructions for the agent |
| \`output\` | string | Description of expected output format |
| \`frontmatter\` | JSON Schema | Defines the structured output the agent must produce |
### outputSchema ### frontmatter
The \`outputSchema\` is a standard JSON Schema object. The extract pipeline validates agent output against it. Key conventions: The \`frontmatter\` field is a standard JSON Schema object. The extract pipeline validates agent output against it. Key conventions:
- \`$status\` field drives routing decisions in the graph - \`$status\` field drives routing decisions in the graph
- Use \`enum\` to constrain status values - Use \`const\` or \`enum\` to constrain status values
- All required fields must appear in the agent's frontmatter output - Use \`oneOf\` to define multiple valid output shapes (one per status)
- All \`required\` fields must appear in the agent's frontmatter output
## graph ## graph
The graph is an array of directed edges defining status-based routing: The graph is a nested map defining status-based routing:
| Field | Type | Description | \`\`\`
|-------|------|-------------| Record<Role | "$START", Record<Status, { role: string, prompt: string }>>
| \`from\` | string | Source role name, or \`$START\` | \`\`\`
| \`to\` | string | Target role name, or \`$END\` |
| \`when\` | object | Condition map — field/value pairs to match against previous output | | Level | Key | Value |
|-------|-----|-------|
| Outer | Role name or \`$START\` | Status map for that role |
| Inner | \`$status\` value (or \`_\` for unconditional) | Target: \`{ role, prompt }\` |
### Special Nodes ### Special Nodes
- \`$START\` — entry point, must have exactly one outgoing edge - \`$START\` — entry point; uses status key \`_\` (unconditional, no previous output)
- \`$END\` — terminal node, thread completes when reached - \`$END\` — terminal node; thread completes when reached
### Edge Evaluation ### Edge Prompts
Edges are evaluated in order. The first edge whose \`when\` condition matches the current step output is selected. If no \`when\` is specified, the edge is unconditional (always matches). Prompts use triple-brace Mustache templates (\`{{{field}}}\`) to interpolate values from the previous step's output. Example: \`"Implement plan {{{plan}}} in repo {{{repoPath}}}."\`
`; `;
} }