From dbefe793f2ef7571030adce8b54915ee2cf564fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 26 May 2026 16:48:52 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20author=20skill=20=E2=80=94=20work?= =?UTF-8?q?flow=20YAML=20design=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../cli-workflow/src/__tests__/skill.test.ts | 14 ++ packages/cli-workflow/src/cli.ts | 8 + packages/cli-workflow/src/commands/skill.ts | 11 +- .../workflow-util/src/author-reference.ts | 183 ++++++++++++++++++ packages/workflow-util/src/index.ts | 1 + 5 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 packages/workflow-util/src/author-reference.ts diff --git a/packages/cli-workflow/src/__tests__/skill.test.ts b/packages/cli-workflow/src/__tests__/skill.test.ts index 5ec32ea..9a5244e 100644 --- a/packages/cli-workflow/src/__tests__/skill.test.ts +++ b/packages/cli-workflow/src/__tests__/skill.test.ts @@ -8,6 +8,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); import { cmdSkillActor, cmdSkillArchitecture, + cmdSkillAuthor, cmdSkillCli, cmdSkillList, cmdSkillModerator, @@ -25,6 +26,7 @@ describe("skill commands", () => { expect(result).toContain("moderator"); expect(result).toContain("actor"); expect(result).toContain("user"); + expect(result).toContain("author"); for (const name of result) { expect(name).toMatch(/^\S+$/); } @@ -84,6 +86,17 @@ describe("skill commands", () => { 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", () => { const output = execFileSync("bun", ["src/cli.ts", "skill", "--help"], { cwd: join(__dirname, "..", ".."), @@ -97,6 +110,7 @@ describe("skill commands", () => { expect(output).toContain("moderator"); expect(output).toContain("actor"); expect(output).toContain("user"); + expect(output).toContain("author"); expect(output).toContain("list"); }); }); diff --git a/packages/cli-workflow/src/cli.ts b/packages/cli-workflow/src/cli.ts index 6c1f11f..6876abf 100755 --- a/packages/cli-workflow/src/cli.ts +++ b/packages/cli-workflow/src/cli.ts @@ -19,6 +19,7 @@ import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js"; import { cmdSkillActor, cmdSkillArchitecture, + cmdSkillAuthor, cmdSkillCli, cmdSkillList, cmdSkillModerator, @@ -512,6 +513,13 @@ skill console.log(cmdSkillActor()); }); +skill + .command("author") + .description("Print the author reference (workflow YAML design guide)") + .action(() => { + console.log(cmdSkillAuthor()); + }); + skill .command("moderator") .description("Print the moderator reference") diff --git a/packages/cli-workflow/src/commands/skill.ts b/packages/cli-workflow/src/commands/skill.ts index 4247d03..9c06808 100644 --- a/packages/cli-workflow/src/commands/skill.ts +++ b/packages/cli-workflow/src/commands/skill.ts @@ -1,13 +1,22 @@ export { generateActorReference as cmdSkillActor, generateArchitectureReference as cmdSkillArchitecture, + generateAuthorReference as cmdSkillAuthor, generateCliReference as cmdSkillCli, generateModeratorReference as cmdSkillModerator, generateUserReference as cmdSkillUser, generateYamlReference as cmdSkillYaml, } 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 { return [...SKILL_NAMES]; diff --git a/packages/workflow-util/src/author-reference.ts b/packages/workflow-util/src/author-reference.ts new file mode 100644 index 0000000..897dd5e --- /dev/null +++ b/packages/workflow-util/src/author-reference.ts @@ -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 + +# Inspect step output +uwf step list +uwf step show + +# Check the CAS data +uwf cas get +\`\`\` + +### 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\` +`; +} diff --git a/packages/workflow-util/src/index.ts b/packages/workflow-util/src/index.ts index ebb6753..770177a 100644 --- a/packages/workflow-util/src/index.ts +++ b/packages/workflow-util/src/index.ts @@ -1,5 +1,6 @@ export { generateActorReference } from "./actor-reference.js"; export { generateArchitectureReference } from "./architecture-reference.js"; +export { generateAuthorReference } from "./author-reference.js"; export { encodeUint64AsCrockford } from "./base32.js"; export { generateCliReference } from "./cli-reference.js"; export { env } from "./env.js";