Merge pull request 'feat: add author skill — workflow YAML design guide' (#547) from feat/539-skill-author into main

This commit is contained in:
2026-05-26 17:04:50 +00:00
5 changed files with 216 additions and 1 deletions
@@ -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");
}); });
}); });
+8
View File
@@ -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")
+10 -1
View File
@@ -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
View File
@@ -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";