feat: add author skill — workflow YAML design guide
Adds 'uwf skill author' for agents/humans designing workflow definitions. Covers: YAML structure, role definition, frontmatter schema design, graph routing, edge prompts, self-testing, and common pitfalls. Refs #539
This commit is contained in:
@@ -8,6 +8,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|||||||
import {
|
import {
|
||||||
cmdSkillActor,
|
cmdSkillActor,
|
||||||
cmdSkillArchitecture,
|
cmdSkillArchitecture,
|
||||||
|
cmdSkillAuthor,
|
||||||
cmdSkillCli,
|
cmdSkillCli,
|
||||||
cmdSkillList,
|
cmdSkillList,
|
||||||
cmdSkillModerator,
|
cmdSkillModerator,
|
||||||
@@ -25,6 +26,7 @@ describe("skill commands", () => {
|
|||||||
expect(result).toContain("moderator");
|
expect(result).toContain("moderator");
|
||||||
expect(result).toContain("actor");
|
expect(result).toContain("actor");
|
||||||
expect(result).toContain("user");
|
expect(result).toContain("user");
|
||||||
|
expect(result).toContain("author");
|
||||||
for (const name of result) {
|
for (const name of result) {
|
||||||
expect(name).toMatch(/^\S+$/);
|
expect(name).toMatch(/^\S+$/);
|
||||||
}
|
}
|
||||||
@@ -84,6 +86,17 @@ describe("skill commands", () => {
|
|||||||
expect(result.length).toBeGreaterThan(500);
|
expect(result.length).toBeGreaterThan(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("skill author returns non-empty markdown string", () => {
|
||||||
|
const result = cmdSkillAuthor();
|
||||||
|
expect(typeof result).toBe("string");
|
||||||
|
expect(result).toContain("frontmatter");
|
||||||
|
expect(result).toContain("graph");
|
||||||
|
expect(result).toContain("$START");
|
||||||
|
expect(result).toContain("$END");
|
||||||
|
expect(result).toContain("$status");
|
||||||
|
expect(result.length).toBeGreaterThan(500);
|
||||||
|
});
|
||||||
|
|
||||||
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: join(__dirname, "..", ".."),
|
cwd: join(__dirname, "..", ".."),
|
||||||
@@ -97,6 +110,7 @@ describe("skill commands", () => {
|
|||||||
expect(output).toContain("moderator");
|
expect(output).toContain("moderator");
|
||||||
expect(output).toContain("actor");
|
expect(output).toContain("actor");
|
||||||
expect(output).toContain("user");
|
expect(output).toContain("user");
|
||||||
|
expect(output).toContain("author");
|
||||||
expect(output).toContain("list");
|
expect(output).toContain("list");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
|
|||||||
import {
|
import {
|
||||||
cmdSkillActor,
|
cmdSkillActor,
|
||||||
cmdSkillArchitecture,
|
cmdSkillArchitecture,
|
||||||
|
cmdSkillAuthor,
|
||||||
cmdSkillCli,
|
cmdSkillCli,
|
||||||
cmdSkillList,
|
cmdSkillList,
|
||||||
cmdSkillModerator,
|
cmdSkillModerator,
|
||||||
@@ -512,6 +513,13 @@ skill
|
|||||||
console.log(cmdSkillActor());
|
console.log(cmdSkillActor());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
skill
|
||||||
|
.command("author")
|
||||||
|
.description("Print the author reference (workflow YAML design guide)")
|
||||||
|
.action(() => {
|
||||||
|
console.log(cmdSkillAuthor());
|
||||||
|
});
|
||||||
|
|
||||||
skill
|
skill
|
||||||
.command("moderator")
|
.command("moderator")
|
||||||
.description("Print the moderator reference")
|
.description("Print the moderator reference")
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
export {
|
export {
|
||||||
generateActorReference as cmdSkillActor,
|
generateActorReference as cmdSkillActor,
|
||||||
generateArchitectureReference as cmdSkillArchitecture,
|
generateArchitectureReference as cmdSkillArchitecture,
|
||||||
|
generateAuthorReference as cmdSkillAuthor,
|
||||||
generateCliReference as cmdSkillCli,
|
generateCliReference as cmdSkillCli,
|
||||||
generateModeratorReference as cmdSkillModerator,
|
generateModeratorReference as cmdSkillModerator,
|
||||||
generateUserReference as cmdSkillUser,
|
generateUserReference as cmdSkillUser,
|
||||||
generateYamlReference as cmdSkillYaml,
|
generateYamlReference as cmdSkillYaml,
|
||||||
} from "@uncaged/workflow-util";
|
} from "@uncaged/workflow-util";
|
||||||
|
|
||||||
const SKILL_NAMES = ["cli", "architecture", "yaml", "moderator", "actor", "user"] as const;
|
const SKILL_NAMES = [
|
||||||
|
"cli",
|
||||||
|
"architecture",
|
||||||
|
"yaml",
|
||||||
|
"moderator",
|
||||||
|
"actor",
|
||||||
|
"user",
|
||||||
|
"author",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function cmdSkillList(): ReadonlyArray<string> {
|
export function cmdSkillList(): ReadonlyArray<string> {
|
||||||
return [...SKILL_NAMES];
|
return [...SKILL_NAMES];
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
export function generateAuthorReference(): string {
|
||||||
|
return `# Author Reference
|
||||||
|
|
||||||
|
Guide for designing and writing workflow YAML definitions.
|
||||||
|
|
||||||
|
## Workflow Structure
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
name: solve-issue # verb-first kebab-case
|
||||||
|
description: "..." # human-readable summary
|
||||||
|
|
||||||
|
roles: # named actors
|
||||||
|
planner:
|
||||||
|
description: "..." # short purpose
|
||||||
|
goal: "..." # system-level goal for the agent
|
||||||
|
capabilities: [...] # skill keywords the agent should load
|
||||||
|
procedure: | # step-by-step instructions
|
||||||
|
1. Do this
|
||||||
|
2. Do that
|
||||||
|
output: "..." # what the agent should produce
|
||||||
|
frontmatter: # JSON Schema for structured output
|
||||||
|
oneOf:
|
||||||
|
- properties:
|
||||||
|
$status: { const: "ready" }
|
||||||
|
plan: { type: string }
|
||||||
|
required: [$status, plan]
|
||||||
|
- properties:
|
||||||
|
$status: { const: "failed" }
|
||||||
|
error: { type: string }
|
||||||
|
required: [$status, error]
|
||||||
|
|
||||||
|
graph: # status-based routing
|
||||||
|
$START:
|
||||||
|
_: { role: planner, prompt: "Analyze the issue." }
|
||||||
|
planner:
|
||||||
|
ready: { role: developer, prompt: "Implement {{{plan}}}." }
|
||||||
|
failed: { role: $END, prompt: "Failed: {{{error}}}" }
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Role Definition
|
||||||
|
|
||||||
|
| Field | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| \`description\` | Short description for humans and moderator context |
|
||||||
|
| \`goal\` | Injected as the agent's system-level objective |
|
||||||
|
| \`capabilities\` | Keyword tags — agent loads matching skills before starting |
|
||||||
|
| \`procedure\` | Step-by-step instructions the agent follows |
|
||||||
|
| \`output\` | Describes what to produce and which \`$status\` values to use |
|
||||||
|
| \`frontmatter\` | JSON Schema defining the structured output fields |
|
||||||
|
|
||||||
|
### Role Design Principles
|
||||||
|
|
||||||
|
- **Single responsibility** — each role does one thing well
|
||||||
|
- **Minimal context** — don't overload a role with too many steps; split if needed
|
||||||
|
- **Clear status values** — each status should map to a distinct graph edge
|
||||||
|
- **Explicit output** — tell the agent exactly what \`$status\` values are valid
|
||||||
|
|
||||||
|
## Frontmatter Schema
|
||||||
|
|
||||||
|
The \`frontmatter\` field is a standard JSON Schema. It defines the structured fields the agent must output in YAML frontmatter.
|
||||||
|
|
||||||
|
### \`$status\` Field
|
||||||
|
|
||||||
|
\`$status\` is the only standard field. Its value determines which graph edge the moderator follows. Use \`const\` to constrain each variant:
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
frontmatter:
|
||||||
|
oneOf:
|
||||||
|
- properties:
|
||||||
|
$status: { const: "done" }
|
||||||
|
result: { type: string }
|
||||||
|
required: [$status, result]
|
||||||
|
- properties:
|
||||||
|
$status: { const: "failed" }
|
||||||
|
error: { type: string }
|
||||||
|
required: [$status, error]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Custom Fields
|
||||||
|
|
||||||
|
Add any fields you need for data passing between roles. These are available in edge prompts via Mustache templates.
|
||||||
|
|
||||||
|
### Flat Schema (Single Status)
|
||||||
|
|
||||||
|
When a role has only one outcome:
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
frontmatter:
|
||||||
|
properties:
|
||||||
|
$status: { const: "done" }
|
||||||
|
summary: { type: string }
|
||||||
|
required: [$status, summary]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Graph Routing
|
||||||
|
|
||||||
|
The graph maps each role's \`$status\` values to the next role:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
graph[role][$status] → { role: nextRole, prompt: edgePrompt }
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Special Nodes
|
||||||
|
|
||||||
|
| Node | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| \`$START\` | Entry point — status key is always \`_\` (unconditional) |
|
||||||
|
| \`$END\` | Terminal — thread completes and is archived |
|
||||||
|
|
||||||
|
### Edge Prompts
|
||||||
|
|
||||||
|
Use triple-brace Mustache (\`{{{field}}}\`) to pass data from the previous step's output:
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
graph:
|
||||||
|
planner:
|
||||||
|
ready: { role: developer, prompt: "Implement plan {{{plan}}} in {{{repoPath}}}." }
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
The fields referenced must exist in the source role's frontmatter schema.
|
||||||
|
|
||||||
|
### Loops and Branching
|
||||||
|
|
||||||
|
Roles can route back to previous roles (loops) or to different roles based on status (branching):
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
graph:
|
||||||
|
reviewer:
|
||||||
|
approved: { role: tester, prompt: "Run tests." }
|
||||||
|
rejected: { role: developer, prompt: "Fix: {{{comments}}}" } # loop back
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Fail Routing
|
||||||
|
|
||||||
|
Route failures to a cleanup role or \`$END\`:
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
graph:
|
||||||
|
developer:
|
||||||
|
done: { role: reviewer, prompt: "Review changes." }
|
||||||
|
failed: { role: cleanup, prompt: "Clean up: {{{error}}}" }
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Self-Testing
|
||||||
|
|
||||||
|
### Step-by-Step Verification
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Start a thread directly from YAML file (no registration needed)
|
||||||
|
uwf thread start my-workflow.yaml -p "Test prompt"
|
||||||
|
|
||||||
|
# Or register first, then start by name
|
||||||
|
uwf workflow add my-workflow.yaml
|
||||||
|
uwf thread start my-workflow -p "Test prompt"
|
||||||
|
|
||||||
|
# Execute one step at a time to verify routing
|
||||||
|
uwf thread exec <thread-id>
|
||||||
|
|
||||||
|
# Inspect step output
|
||||||
|
uwf step list <thread-id>
|
||||||
|
uwf step show <step-hash>
|
||||||
|
|
||||||
|
# Check the CAS data
|
||||||
|
uwf cas get <output-hash>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Validation Checklist
|
||||||
|
|
||||||
|
1. Every \`$status\` value in a role's frontmatter has a matching edge in the graph
|
||||||
|
2. Every field referenced in edge prompts (\`{{{field}}}\`) exists in the source role's schema
|
||||||
|
3. Every role referenced in the graph exists in \`roles\`
|
||||||
|
4. \`$START\` has exactly one edge with key \`_\`
|
||||||
|
5. At least one path leads to \`$END\`
|
||||||
|
6. No orphan roles (defined but never routed to)
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- **Missing graph edge** — if a role can produce \`$status: failed\` but the graph has no \`failed\` edge, the moderator will error
|
||||||
|
- **Mustache field mismatch** — referencing \`{{{branch}}}\` in an edge prompt but the source schema has \`branchName\` instead
|
||||||
|
- **Overly complex roles** — a role with 20 steps should be split; each role should be completable in one agent turn
|
||||||
|
- **No fail path** — always handle failure; route to cleanup or \`$END\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { generateActorReference } from "./actor-reference.js";
|
export { generateActorReference } from "./actor-reference.js";
|
||||||
export { generateArchitectureReference } from "./architecture-reference.js";
|
export { generateArchitectureReference } from "./architecture-reference.js";
|
||||||
|
export { generateAuthorReference } from "./author-reference.js";
|
||||||
export { encodeUint64AsCrockford } from "./base32.js";
|
export { encodeUint64AsCrockford } from "./base32.js";
|
||||||
export { generateCliReference } from "./cli-reference.js";
|
export { generateCliReference } from "./cli-reference.js";
|
||||||
export { env } from "./env.js";
|
export { env } from "./env.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user