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
This commit is contained in:
2026-05-22 03:20:04 +00:00
parent 8efc5050cb
commit 866154ad73
4 changed files with 114 additions and 4 deletions
+10
View File
@@ -14,6 +14,7 @@ import {
cmdCasWalk, cmdCasWalk,
} from "./commands/cas.js"; } from "./commands/cas.js";
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js"; import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
import { cmdSkillCli } from "./commands/skill.js";
import { import {
cmdThreadFork, cmdThreadFork,
cmdThreadKill, 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 program
.command("setup") .command("setup")
.description("Configure provider, model, and agent") .description("Configure provider, model, and agent")
@@ -0,0 +1,70 @@
export function cmdSkillCli(): string {
return `# uwf CLI Reference
## Setup
\`\`\`
uwf setup # interactive setup wizard
uwf setup --provider <name> --base-url <url> \\
--api-key <key> --model <name> # non-interactive setup
[--agent <name>] # optional: default agent alias
\`\`\`
## Workflow Commands
\`\`\`
uwf workflow put <file> # register a workflow from YAML file
uwf workflow show <id> # show workflow by name or CAS hash
uwf workflow list # list all registered workflows
\`\`\`
## Thread Commands
\`\`\`
uwf thread start <workflow> -p <prompt> # create a thread (no execution)
uwf thread step <thread-id> # execute one moderator→agent→extract cycle
[--agent <cmd>] # override agent command
uwf thread show <thread-id> # show thread head pointer
uwf thread list # list active threads
[--all] # include archived threads
uwf thread kill <thread-id> # terminate and archive a thread
uwf thread steps <thread-id> # list all steps in a thread
uwf thread read <thread-id> # render thread context as markdown
[--quota <chars>] # max output characters (default 32000)
[--before <step-hash>] # load steps before this hash (exclusive)
[--start] # include start step in output
uwf thread fork <step-hash> # fork a thread from a specific step
uwf thread step-details <step-hash> # dump full detail node of a step as YAML
\`\`\`
## CAS Commands
\`\`\`
uwf cas get <hash> # read a CAS node (type + payload)
[--timestamp] # include timestamp in output
uwf cas put <type-hash> <data> # store a node, print its hash
# <data>: JSON file path or inline JSON string
uwf cas has <hash> # check if a hash exists
uwf cas refs <hash> # list direct CAS references from a node
uwf cas walk <hash> # 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 <hash> # show a schema by its type hash
\`\`\`
## Global Options
\`\`\`
uwf --format <fmt> # 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.
`;
}
@@ -18,13 +18,16 @@ describe("buildRolePrompt", () => {
expect(result).toContain("## Capabilities"); expect(result).toContain("## Capabilities");
expect(result).toContain("- cursor-agent"); expect(result).toContain("- cursor-agent");
expect(result).toContain("- file-edit"); 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("## Procedure");
expect(result).toContain("Implement the feature."); expect(result).toContain("Implement the feature.");
expect(result).toContain("## Output"); expect(result).toContain("## Output");
expect(result).toContain("Summarize changes."); expect(result).toContain("Summarize changes.");
}); });
test("empty fields are omitted", () => { test("empty fields are omitted but Prepare is always present", () => {
const role: RoleDefinition = { const role: RoleDefinition = {
description: "A reviewer", description: "A reviewer",
goal: "You are a code reviewer.", goal: "You are a code reviewer.",
@@ -35,12 +38,14 @@ describe("buildRolePrompt", () => {
}; };
const result = buildRolePrompt(role); const result = buildRolePrompt(role);
expect(result).toContain("## Goal"); expect(result).toContain("## Goal");
expect(result).toContain("## Prepare");
expect(result).toContain("uwf skill cli");
expect(result).toContain("## Procedure"); expect(result).toContain("## Procedure");
expect(result).not.toContain("## Capabilities"); expect(result).not.toContain("## Capabilities");
expect(result).not.toContain("## Output"); expect(result).not.toContain("## Output");
}); });
test("all empty returns empty string", () => { test("all empty still includes Prepare section", () => {
const role: RoleDefinition = { const role: RoleDefinition = {
description: "Minimal", description: "Minimal",
goal: "", goal: "",
@@ -50,7 +55,12 @@ describe("buildRolePrompt", () => {
meta: "placeholder00000" as string, meta: "placeholder00000" as string,
}; };
const result = buildRolePrompt(role); 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", () => { test("capabilities rendered as bullet list", () => {
@@ -3,8 +3,12 @@ import type { RoleDefinition } from "@uncaged/workflow-protocol";
/** /**
* Build the role prompt from a RoleDefinition. * 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. * 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 { export function buildRolePrompt(role: RoleDefinition): string {
const sections: string[] = []; const sections: string[] = [];
@@ -18,6 +22,22 @@ export function buildRolePrompt(role: RoleDefinition): string {
sections.push(`## Capabilities\n\n${list}`); 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 !== "") { if (role.procedure !== "") {
sections.push(`## Procedure\n\n${role.procedure}`); sections.push(`## Procedure\n\n${role.procedure}`);
} }