From 866154ad73cc03a7263ad0edba52060e7ef96909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 22 May 2026 03:20:04 +0000 Subject: [PATCH 1/2] feat: add uwf skill cli command and Prepare section in role prompt - Add 'uwf skill cli' command that prints markdown CLI reference - buildRolePrompt now generates ## Prepare section: - Always prompts agent to run 'uwf skill cli' (explicit skill) - Renders capabilities as keyword hints for implicit skill loading Fixes #369 --- packages/cli-workflow/src/cli.ts | 10 +++ packages/cli-workflow/src/commands/skill.ts | 70 +++++++++++++++++++ .../__tests__/build-role-prompt.test.ts | 16 ++++- .../src/build-role-prompt.ts | 22 +++++- 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 packages/cli-workflow/src/commands/skill.ts diff --git a/packages/cli-workflow/src/cli.ts b/packages/cli-workflow/src/cli.ts index 1a6a5c6..378c093 100755 --- a/packages/cli-workflow/src/cli.ts +++ b/packages/cli-workflow/src/cli.ts @@ -14,6 +14,7 @@ import { cmdCasWalk, } from "./commands/cas.js"; import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js"; +import { cmdSkillCli } from "./commands/skill.js"; import { cmdThreadFork, cmdThreadKill, @@ -220,6 +221,15 @@ thread }); }); +const skill = program.command("skill").description("Built-in skill references for agents"); + +skill + .command("cli") + .description("Print a markdown reference of all uwf commands") + .action(() => { + console.log(cmdSkillCli()); + }); + program .command("setup") .description("Configure provider, model, and agent") diff --git a/packages/cli-workflow/src/commands/skill.ts b/packages/cli-workflow/src/commands/skill.ts new file mode 100644 index 0000000..e86294a --- /dev/null +++ b/packages/cli-workflow/src/commands/skill.ts @@ -0,0 +1,70 @@ +export function cmdSkillCli(): string { + return `# uwf CLI Reference + +## Setup + +\`\`\` +uwf setup # interactive setup wizard +uwf setup --provider --base-url \\ + --api-key --model # non-interactive setup + [--agent ] # optional: default agent alias +\`\`\` + +## Workflow Commands + +\`\`\` +uwf workflow put # register a workflow from YAML file +uwf workflow show # show workflow by name or CAS hash +uwf workflow list # list all registered workflows +\`\`\` + +## Thread Commands + +\`\`\` +uwf thread start -p # create a thread (no execution) +uwf thread step # execute one moderator→agent→extract cycle + [--agent ] # override agent command +uwf thread show # show thread head pointer +uwf thread list # list active threads + [--all] # include archived threads +uwf thread kill # terminate and archive a thread +uwf thread steps # list all steps in a thread +uwf thread read # render thread context as markdown + [--quota ] # max output characters (default 32000) + [--before ] # load steps before this hash (exclusive) + [--start] # include start step in output +uwf thread fork # fork a thread from a specific step +uwf thread step-details # dump full detail node of a step as YAML +\`\`\` + +## CAS Commands + +\`\`\` +uwf cas get # read a CAS node (type + payload) + [--timestamp] # include timestamp in output +uwf cas put # store a node, print its hash + # : JSON file path or inline JSON string +uwf cas has # check if a hash exists +uwf cas refs # list direct CAS references from a node +uwf cas walk # recursive traversal from a node +uwf cas reindex # rebuild type index from all CAS nodes +uwf cas schema list # list all registered schemas +uwf cas schema get # show a schema by its type hash +\`\`\` + +## Global Options + +\`\`\` +uwf --format # output format: json (default) or yaml +uwf -V, --version # print version +\`\`\` + +## Key Concepts + +- **Workflow**: YAML definition with roles, conditions, and a routing graph; stored as a CAS node identified by its XXH64 hash. +- **Thread**: A single workflow execution (ULID). State is an immutable CAS chain; active threads are indexed in \`threads.yaml\`. +- **Step**: One moderator→agent→extract cycle. Run \`uwf thread step\` repeatedly until \`$END\`. +- **CAS**: Content-Addressed Storage — all nodes are immutable and identified by hash. +- **Role**: Named actor with a system prompt and JSON Schema output; the moderator routes between roles. +`; +} diff --git a/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts b/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts index 4965cc4..1a0fd0c 100644 --- a/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts +++ b/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts @@ -18,13 +18,16 @@ describe("buildRolePrompt", () => { expect(result).toContain("## Capabilities"); expect(result).toContain("- cursor-agent"); expect(result).toContain("- file-edit"); + expect(result).toContain("## Prepare"); + expect(result).toContain("uwf skill cli"); + expect(result).toContain("cursor-agent, file-edit"); expect(result).toContain("## Procedure"); expect(result).toContain("Implement the feature."); expect(result).toContain("## Output"); expect(result).toContain("Summarize changes."); }); - test("empty fields are omitted", () => { + test("empty fields are omitted but Prepare is always present", () => { const role: RoleDefinition = { description: "A reviewer", goal: "You are a code reviewer.", @@ -35,12 +38,14 @@ describe("buildRolePrompt", () => { }; const result = buildRolePrompt(role); expect(result).toContain("## Goal"); + expect(result).toContain("## Prepare"); + expect(result).toContain("uwf skill cli"); expect(result).toContain("## Procedure"); expect(result).not.toContain("## Capabilities"); expect(result).not.toContain("## Output"); }); - test("all empty returns empty string", () => { + test("all empty still includes Prepare section", () => { const role: RoleDefinition = { description: "Minimal", goal: "", @@ -50,7 +55,12 @@ describe("buildRolePrompt", () => { meta: "placeholder00000" as string, }; const result = buildRolePrompt(role); - expect(result).toBe(""); + expect(result).toContain("## Prepare"); + expect(result).toContain("uwf skill cli"); + expect(result).not.toContain("## Goal"); + expect(result).not.toContain("## Capabilities"); + expect(result).not.toContain("## Procedure"); + expect(result).not.toContain("## Output"); }); test("capabilities rendered as bullet list", () => { diff --git a/packages/workflow-agent-kit/src/build-role-prompt.ts b/packages/workflow-agent-kit/src/build-role-prompt.ts index 199d4a3..5cc7de7 100644 --- a/packages/workflow-agent-kit/src/build-role-prompt.ts +++ b/packages/workflow-agent-kit/src/build-role-prompt.ts @@ -3,8 +3,12 @@ import type { RoleDefinition } from "@uncaged/workflow-protocol"; /** * Build the role prompt from a RoleDefinition. * - * Assembles structured sections: Goal, Capabilities, Procedure, Output. + * Assembles structured sections: Goal, Capabilities, Prepare, Procedure, Output. * Empty strings and empty arrays are omitted from the output. + * + * The Prepare section always instructs the agent to run `uwf skill cli` to load + * workflow knowledge, plus renders the capabilities array as keyword hints for + * implicit skill loading. */ export function buildRolePrompt(role: RoleDefinition): string { const sections: string[] = []; @@ -18,6 +22,22 @@ export function buildRolePrompt(role: RoleDefinition): string { sections.push(`## Capabilities\n\n${list}`); } + const prepareLines: string[] = [ + "Run the following command to load workflow CLI knowledge before starting work:", + "", + "```", + "uwf skill cli", + "```", + ]; + if (role.capabilities.length > 0) { + const keywords = role.capabilities.join(", "); + prepareLines.push( + "", + `You have the following capabilities: ${keywords}. Load relevant skills matching these keywords before starting work.`, + ); + } + sections.push(`## Prepare\n\n${prepareLines.join("\n")}`); + if (role.procedure !== "") { sections.push(`## Procedure\n\n${role.procedure}`); } From 995f273fa5e7a3494c51f0b9ae8727e99a24dfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 22 May 2026 03:29:01 +0000 Subject: [PATCH 2/2] address review: move CLI reference to workflow-util, inline in prompt - Move generateCliReference() to @uncaged/workflow-util - buildRolePrompt inlines CLI reference directly (no agent tool call) - Fix Role terminology to use new field names - Add maintenance comment in cli-reference.ts - Fix test assertions --- packages/cli-workflow/src/commands/skill.ts | 71 +----------------- .../__tests__/build-role-prompt.test.ts | 6 +- .../src/build-role-prompt.ts | 18 ++--- packages/workflow-util/src/cli-reference.ts | 72 +++++++++++++++++++ packages/workflow-util/src/index.ts | 1 + 5 files changed, 83 insertions(+), 85 deletions(-) create mode 100644 packages/workflow-util/src/cli-reference.ts diff --git a/packages/cli-workflow/src/commands/skill.ts b/packages/cli-workflow/src/commands/skill.ts index e86294a..59307ca 100644 --- a/packages/cli-workflow/src/commands/skill.ts +++ b/packages/cli-workflow/src/commands/skill.ts @@ -1,70 +1 @@ -export function cmdSkillCli(): string { - return `# uwf CLI Reference - -## Setup - -\`\`\` -uwf setup # interactive setup wizard -uwf setup --provider --base-url \\ - --api-key --model # non-interactive setup - [--agent ] # optional: default agent alias -\`\`\` - -## Workflow Commands - -\`\`\` -uwf workflow put # register a workflow from YAML file -uwf workflow show # show workflow by name or CAS hash -uwf workflow list # list all registered workflows -\`\`\` - -## Thread Commands - -\`\`\` -uwf thread start -p # create a thread (no execution) -uwf thread step # execute one moderator→agent→extract cycle - [--agent ] # override agent command -uwf thread show # show thread head pointer -uwf thread list # list active threads - [--all] # include archived threads -uwf thread kill # terminate and archive a thread -uwf thread steps # list all steps in a thread -uwf thread read # render thread context as markdown - [--quota ] # max output characters (default 32000) - [--before ] # load steps before this hash (exclusive) - [--start] # include start step in output -uwf thread fork # fork a thread from a specific step -uwf thread step-details # dump full detail node of a step as YAML -\`\`\` - -## CAS Commands - -\`\`\` -uwf cas get # read a CAS node (type + payload) - [--timestamp] # include timestamp in output -uwf cas put # store a node, print its hash - # : JSON file path or inline JSON string -uwf cas has # check if a hash exists -uwf cas refs # list direct CAS references from a node -uwf cas walk # recursive traversal from a node -uwf cas reindex # rebuild type index from all CAS nodes -uwf cas schema list # list all registered schemas -uwf cas schema get # show a schema by its type hash -\`\`\` - -## Global Options - -\`\`\` -uwf --format # output format: json (default) or yaml -uwf -V, --version # print version -\`\`\` - -## Key Concepts - -- **Workflow**: YAML definition with roles, conditions, and a routing graph; stored as a CAS node identified by its XXH64 hash. -- **Thread**: A single workflow execution (ULID). State is an immutable CAS chain; active threads are indexed in \`threads.yaml\`. -- **Step**: One moderator→agent→extract cycle. Run \`uwf thread step\` repeatedly until \`$END\`. -- **CAS**: Content-Addressed Storage — all nodes are immutable and identified by hash. -- **Role**: Named actor with a system prompt and JSON Schema output; the moderator routes between roles. -`; -} +export { generateCliReference as cmdSkillCli } from "@uncaged/workflow-util"; diff --git a/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts b/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts index 1a0fd0c..a56f702 100644 --- a/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts +++ b/packages/workflow-agent-kit/__tests__/build-role-prompt.test.ts @@ -19,7 +19,7 @@ describe("buildRolePrompt", () => { expect(result).toContain("- cursor-agent"); expect(result).toContain("- file-edit"); expect(result).toContain("## Prepare"); - expect(result).toContain("uwf skill cli"); + expect(result).toContain("uwf CLI Reference"); expect(result).toContain("cursor-agent, file-edit"); expect(result).toContain("## Procedure"); expect(result).toContain("Implement the feature."); @@ -39,7 +39,7 @@ describe("buildRolePrompt", () => { const result = buildRolePrompt(role); expect(result).toContain("## Goal"); expect(result).toContain("## Prepare"); - expect(result).toContain("uwf skill cli"); + expect(result).toContain("uwf CLI Reference"); expect(result).toContain("## Procedure"); expect(result).not.toContain("## Capabilities"); expect(result).not.toContain("## Output"); @@ -56,7 +56,7 @@ describe("buildRolePrompt", () => { }; const result = buildRolePrompt(role); expect(result).toContain("## Prepare"); - expect(result).toContain("uwf skill cli"); + expect(result).toContain("uwf CLI Reference"); expect(result).not.toContain("## Goal"); expect(result).not.toContain("## Capabilities"); expect(result).not.toContain("## Procedure"); diff --git a/packages/workflow-agent-kit/src/build-role-prompt.ts b/packages/workflow-agent-kit/src/build-role-prompt.ts index 5cc7de7..d307b59 100644 --- a/packages/workflow-agent-kit/src/build-role-prompt.ts +++ b/packages/workflow-agent-kit/src/build-role-prompt.ts @@ -1,4 +1,5 @@ import type { RoleDefinition } from "@uncaged/workflow-protocol"; +import { generateCliReference } from "@uncaged/workflow-util"; /** * Build the role prompt from a RoleDefinition. @@ -6,9 +7,9 @@ import type { RoleDefinition } from "@uncaged/workflow-protocol"; * Assembles structured sections: Goal, Capabilities, Prepare, Procedure, Output. * Empty strings and empty arrays are omitted from the output. * - * The Prepare section always instructs the agent to run `uwf skill cli` to load - * workflow knowledge, plus renders the capabilities array as keyword hints for - * implicit skill loading. + * The Prepare section always inlines the uwf CLI reference so the agent has + * workflow knowledge without needing to run an external command. The capabilities + * array is rendered as keyword hints for implicit skill loading. */ export function buildRolePrompt(role: RoleDefinition): string { const sections: string[] = []; @@ -22,21 +23,14 @@ export function buildRolePrompt(role: RoleDefinition): string { sections.push(`## Capabilities\n\n${list}`); } - const prepareLines: string[] = [ - "Run the following command to load workflow CLI knowledge before starting work:", - "", - "```", - "uwf skill cli", - "```", - ]; + const prepareLines: string[] = [generateCliReference()]; if (role.capabilities.length > 0) { const keywords = role.capabilities.join(", "); prepareLines.push( - "", `You have the following capabilities: ${keywords}. Load relevant skills matching these keywords before starting work.`, ); } - sections.push(`## Prepare\n\n${prepareLines.join("\n")}`); + sections.push(`## Prepare\n\n${prepareLines.join("\n\n")}`); if (role.procedure !== "") { sections.push(`## Procedure\n\n${role.procedure}`); diff --git a/packages/workflow-util/src/cli-reference.ts b/packages/workflow-util/src/cli-reference.ts new file mode 100644 index 0000000..f2d3881 --- /dev/null +++ b/packages/workflow-util/src/cli-reference.ts @@ -0,0 +1,72 @@ +// MAINTENANCE: This string must be kept in sync with the actual uwf CLI commands. +// Update whenever commands are added, removed, or their signatures change. +export function generateCliReference(): string { + return `# uwf CLI Reference + +## Setup + +\`\`\` +uwf setup # interactive setup wizard +uwf setup --provider --base-url \\ + --api-key --model # non-interactive setup + [--agent ] # optional: default agent alias +\`\`\` + +## Workflow Commands + +\`\`\` +uwf workflow put # register a workflow from YAML file +uwf workflow show # show workflow by name or CAS hash +uwf workflow list # list all registered workflows +\`\`\` + +## Thread Commands + +\`\`\` +uwf thread start -p # create a thread (no execution) +uwf thread step # execute one moderator→agent→extract cycle + [--agent ] # override agent command +uwf thread show # show thread head pointer +uwf thread list # list active threads + [--all] # include archived threads +uwf thread kill # terminate and archive a thread +uwf thread steps # list all steps in a thread +uwf thread read # render thread context as markdown + [--quota ] # max output characters (default 32000) + [--before ] # load steps before this hash (exclusive) + [--start] # include start step in output +uwf thread fork # fork a thread from a specific step +uwf thread step-details # dump full detail node of a step as YAML +\`\`\` + +## CAS Commands + +\`\`\` +uwf cas get # read a CAS node (type + payload) + [--timestamp] # include timestamp in output +uwf cas put # store a node, print its hash + # : JSON file path or inline JSON string +uwf cas has # check if a hash exists +uwf cas refs # list direct CAS references from a node +uwf cas walk # recursive traversal from a node +uwf cas reindex # rebuild type index from all CAS nodes +uwf cas schema list # list all registered schemas +uwf cas schema get # show a schema by its type hash +\`\`\` + +## Global Options + +\`\`\` +uwf --format # output format: json (default) or yaml +uwf -V, --version # print version +\`\`\` + +## Key Concepts + +- **Workflow**: YAML definition with roles, conditions, and a routing graph; stored as a CAS node identified by its XXH64 hash. +- **Thread**: A single workflow execution (ULID). State is an immutable CAS chain; active threads are indexed in \`threads.yaml\`. +- **Step**: One moderator→agent→extract cycle. Run \`uwf thread step\` repeatedly until \`$END\`. +- **CAS**: Content-Addressed Storage — all nodes are immutable and identified by hash. +- **Role**: Named actor with goal, capabilities, procedure, output, and meta; the moderator routes between roles. +`; +} diff --git a/packages/workflow-util/src/index.ts b/packages/workflow-util/src/index.ts index bdea551..58c2fcf 100644 --- a/packages/workflow-util/src/index.ts +++ b/packages/workflow-util/src/index.ts @@ -1,4 +1,5 @@ export { encodeUint64AsCrockford } from "./base32.js"; +export { generateCliReference } from "./cli-reference.js"; export { env } from "./env.js"; export type { AgentFrontmatter,