Merge pull request 'feat(cli): help --skill <topic> for context-specific agent docs' (#82) from feat/81-skill-topics into main
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { runCli } from "../src/cli-dispatch.js";
|
||||
import { formatSkillDoc } from "../src/cmd-help.js";
|
||||
import {
|
||||
formatSkillDoc,
|
||||
formatSkillIndex,
|
||||
formatSkillTopic,
|
||||
getSkillTopics,
|
||||
} from "../src/cmd-help.js";
|
||||
|
||||
const STORAGE_ROOT = "/tmp/help-test-storage";
|
||||
|
||||
@@ -10,13 +15,53 @@ describe("help command", () => {
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("help --skill returns 0", async () => {
|
||||
test("help --skill (no topic) returns 0 and lists topics", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help", "--skill"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("help --skill cli returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "cli"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("help --skill develop returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "develop"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("help --skill author returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "author"]);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
test("help --skill unknown returns 1", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "unknown"]);
|
||||
expect(code).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillDoc", () => {
|
||||
describe("getSkillTopics", () => {
|
||||
test("returns all topics", () => {
|
||||
const topics = getSkillTopics();
|
||||
const names = topics.map((t) => t.name);
|
||||
expect(names).toContain("cli");
|
||||
expect(names).toContain("develop");
|
||||
expect(names).toContain("author");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillIndex", () => {
|
||||
test("lists all topics", () => {
|
||||
const idx = formatSkillIndex();
|
||||
expect(idx).toContain("cli");
|
||||
expect(idx).toContain("develop");
|
||||
expect(idx).toContain("author");
|
||||
expect(idx).toContain("help --skill <topic>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic('cli') — legacy formatSkillDoc", () => {
|
||||
const doc = formatSkillDoc();
|
||||
|
||||
test("contains title", () => {
|
||||
@@ -82,3 +127,52 @@ describe("formatSkillDoc", () => {
|
||||
expect(doc).toContain("## Typical Workflow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic('develop')", () => {
|
||||
const doc = formatSkillTopic("develop");
|
||||
|
||||
test("returns non-null", () => {
|
||||
expect(doc).not.toBeNull();
|
||||
});
|
||||
|
||||
test("contains thread ID info", () => {
|
||||
expect(doc).toContain("Thread ID");
|
||||
expect(doc).toContain("Crockford Base32");
|
||||
});
|
||||
|
||||
test("contains CAS commands", () => {
|
||||
expect(doc).toContain("cas put");
|
||||
expect(doc).toContain("cas get");
|
||||
});
|
||||
|
||||
test("contains meta output section", () => {
|
||||
expect(doc).toContain("Meta Output");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic('author')", () => {
|
||||
const doc = formatSkillTopic("author");
|
||||
|
||||
test("returns non-null", () => {
|
||||
expect(doc).not.toBeNull();
|
||||
});
|
||||
|
||||
test("contains bundle structure", () => {
|
||||
expect(doc).toContain("Bundle Structure");
|
||||
expect(doc).toContain(".esm.js");
|
||||
});
|
||||
|
||||
test("contains descriptor info", () => {
|
||||
expect(doc).toContain("WorkflowDescriptor");
|
||||
});
|
||||
|
||||
test("contains role definition", () => {
|
||||
expect(doc).toContain("Role Definition");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSkillTopic unknown", () => {
|
||||
test("returns null for unknown topic", () => {
|
||||
expect(formatSkillTopic("nonexistent")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cmdAdd, formatAddSuccess, parseAddArgv } from "./cmd-add.js";
|
||||
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "./cmd-cas.js";
|
||||
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
||||
import { cmdGc } from "./cmd-gc.js";
|
||||
import { formatSkillDoc } from "./cmd-help.js";
|
||||
import { formatSkillDoc, formatSkillIndex, formatSkillTopic } from "./cmd-help.js";
|
||||
import { cmdHistory } from "./cmd-history.js";
|
||||
import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js";
|
||||
import { cmdKill } from "./cmd-kill.js";
|
||||
@@ -619,11 +619,22 @@ async function dispatchCas(storageRoot: string, argv: string[]): Promise<number>
|
||||
// ── Help ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
if (argv.includes("--skill")) {
|
||||
printCliLine(formatSkillDoc());
|
||||
} else {
|
||||
const skillIdx = argv.indexOf("--skill");
|
||||
if (skillIdx === -1) {
|
||||
printCliLine(formatCliUsage());
|
||||
return 0;
|
||||
}
|
||||
const topic = argv[skillIdx + 1];
|
||||
if (topic === undefined) {
|
||||
printCliLine(formatSkillIndex());
|
||||
return 0;
|
||||
}
|
||||
const doc = formatSkillTopic(topic);
|
||||
if (doc === null) {
|
||||
printCliError(`unknown skill topic: ${topic}\n\n${formatSkillIndex()}`);
|
||||
return 1;
|
||||
}
|
||||
printCliLine(doc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,54 @@
|
||||
import { getCommandRegistry } from "./cli-dispatch.js";
|
||||
|
||||
export function formatSkillDoc(): string {
|
||||
type SkillTopic = {
|
||||
name: string;
|
||||
description: string;
|
||||
format: () => string;
|
||||
};
|
||||
|
||||
const SKILL_TOPICS: ReadonlyArray<SkillTopic> = [
|
||||
{ name: "cli", description: "Full CLI command reference", format: formatSkillCli },
|
||||
{
|
||||
name: "develop",
|
||||
description: "Guide for agents executing roles inside a workflow",
|
||||
format: formatSkillDevelop,
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
description: "Guide for building and publishing workflow bundles",
|
||||
format: formatSkillAuthor,
|
||||
},
|
||||
];
|
||||
|
||||
export function getSkillTopics(): ReadonlyArray<{ name: string; description: string }> {
|
||||
return SKILL_TOPICS.map((t) => ({ name: t.name, description: t.description }));
|
||||
}
|
||||
|
||||
export function formatSkillTopic(topic: string): string | null {
|
||||
const entry = SKILL_TOPICS.find((t) => t.name === topic);
|
||||
if (entry === undefined) {
|
||||
return null;
|
||||
}
|
||||
return entry.format();
|
||||
}
|
||||
|
||||
export function formatSkillIndex(): string {
|
||||
const rows = SKILL_TOPICS.map((t) => `| \`${t.name}\` | ${t.description} |`);
|
||||
return `# uncaged-workflow help --skill
|
||||
|
||||
Available topics:
|
||||
|
||||
| Topic | Description |
|
||||
|-------|-------------|
|
||||
${rows.join("\n")}
|
||||
|
||||
Usage: \`uncaged-workflow help --skill <topic>\`
|
||||
`;
|
||||
}
|
||||
|
||||
// ── cli topic (existing full reference) ────────────────────────────────
|
||||
|
||||
function formatSkillCli(): string {
|
||||
const groups = getCommandRegistry();
|
||||
|
||||
const commandSections: string[] = [];
|
||||
@@ -58,3 +106,133 @@ ${commandSections.join("\n\n")}
|
||||
| \`UNCAGED_WORKFLOW_STORAGE_ROOT\` | Override the default storage directory for all workflow data |
|
||||
`;
|
||||
}
|
||||
|
||||
// ── develop topic (for agents inside a workflow) ───────────────────────
|
||||
|
||||
function formatSkillDevelop(): string {
|
||||
return `# Workflow Role Guide
|
||||
|
||||
Reference for agents executing roles (planner, coder, reviewer, etc.) inside a running workflow thread.
|
||||
|
||||
## Thread ID
|
||||
|
||||
Every thread has a 26-character Crockford Base32 ULID (e.g. \`06F03H5V6JTMDST6P3TVH42RWM\`).
|
||||
|
||||
It appears in the **first message** of the conversation. If unsure:
|
||||
|
||||
\`\`\`
|
||||
uncaged-workflow thread list
|
||||
\`\`\`
|
||||
|
||||
## CAS (Content-Addressable Storage)
|
||||
|
||||
Store and retrieve content by hash, scoped to the current thread.
|
||||
|
||||
| Operation | Command |
|
||||
|-----------|---------|
|
||||
| **Store** | \`uncaged-workflow cas put <THREAD_ID> '<content>'\` → prints hash |
|
||||
| **Read** | \`uncaged-workflow cas get <THREAD_ID> <HASH>\` → prints content |
|
||||
| **List** | \`uncaged-workflow cas list <THREAD_ID>\` |
|
||||
|
||||
CAS is the **only** supported way to persist structured data (phase plans, review notes, etc.) within a thread. Do not use temp files.
|
||||
|
||||
## Meta Output
|
||||
|
||||
Each role must produce structured output that the moderator extracts. The exact schema depends on the role, but the pattern is:
|
||||
|
||||
1. Do your work (write code, run tests, etc.)
|
||||
2. Output a compact JSON object matching the role's schema
|
||||
3. The moderator extracts and validates it automatically
|
||||
|
||||
## Thread Context
|
||||
|
||||
The conversation history contains outputs from previous roles. Read it to understand:
|
||||
- What task was requested (from the initial prompt)
|
||||
- What previous roles produced (plans, code changes, review results)
|
||||
- What the moderator decided (which phase to work on, whether to retry)
|
||||
`;
|
||||
}
|
||||
|
||||
// ── author topic (for workflow developers) ─────────────────────────────
|
||||
|
||||
function formatSkillAuthor(): string {
|
||||
return `# Workflow Authoring Guide
|
||||
|
||||
How to build, test, and publish workflow bundles for uncaged-workflow.
|
||||
|
||||
## Bundle Structure
|
||||
|
||||
A workflow bundle is a single ESM file (\`.esm.js\`) that exports:
|
||||
|
||||
\`\`\`typescript
|
||||
// Required exports
|
||||
export const descriptor: WorkflowDescriptor;
|
||||
export const run: WorkflowRun;
|
||||
\`\`\`
|
||||
|
||||
## WorkflowDescriptor
|
||||
|
||||
Defines the workflow's metadata and role sequence:
|
||||
|
||||
\`\`\`typescript
|
||||
type WorkflowDescriptor = {
|
||||
name: string; // verb-first kebab-case, e.g. "solve-issue"
|
||||
description: string; // one-line summary
|
||||
roles: string[]; // ordered role names, e.g. ["planner", "coder", "reviewer"]
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
## WorkflowRun
|
||||
|
||||
The main function that creates and returns a moderator:
|
||||
|
||||
\`\`\`typescript
|
||||
type WorkflowRun = (ctx: WorkflowContext) => Moderator;
|
||||
\`\`\`
|
||||
|
||||
The **Moderator** controls the flow — it decides which role runs next, handles retries, and determines when the workflow is complete.
|
||||
|
||||
## Role Definition
|
||||
|
||||
Each role has:
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| \`description\` | string | What the role does |
|
||||
| \`systemPrompt\` | string | System prompt for the agent |
|
||||
| \`extractPrompt\` | string | Instruction for extracting structured meta |
|
||||
| \`schema\` | ZodSchema | Validates the extracted meta |
|
||||
| \`extractRefs\` | fn or null | Extracts CAS hashes from meta for DAG linking |
|
||||
| \`extractMode\` | "single" | Extraction mode |
|
||||
|
||||
## Development Workflow
|
||||
|
||||
\`\`\`bash
|
||||
# 1. Initialize a workspace
|
||||
uncaged-workflow init workspace my-workflow
|
||||
|
||||
# 2. Write your template (roles + moderator + descriptor)
|
||||
|
||||
# 3. Build the ESM bundle
|
||||
bun run build
|
||||
|
||||
# 4. Register locally
|
||||
uncaged-workflow workflow add my-workflow ./dist/my-workflow.esm.js
|
||||
|
||||
# 5. Test
|
||||
uncaged-workflow run my-workflow --prompt "test task"
|
||||
uncaged-workflow live --latest
|
||||
\`\`\`
|
||||
|
||||
## Versioning
|
||||
|
||||
Bundles are immutable and identified by XXH64 hash. Re-registering a workflow with a new bundle creates a new version. Use \`workflow history\` and \`workflow rollback\` to manage versions.
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Legacy compat ──────────────────────────────────────────────────────
|
||||
|
||||
/** @deprecated Use formatSkillTopic("cli") instead */
|
||||
export function formatSkillDoc(): string {
|
||||
return formatSkillCli();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
||||
|
||||
const CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only.
|
||||
|
||||
Run \`uncaged-workflow help --skill\` for full CLI reference (thread ID lookup, CAS commands, etc.).
|
||||
Run \`uncaged-workflow help --skill develop\` for thread ID lookup, CAS commands, and meta output guide.
|
||||
|
||||
## Reading phase details
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
||||
|
||||
const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time.
|
||||
|
||||
Run \`uncaged-workflow help --skill\` for full CLI reference (thread ID lookup, CAS commands, etc.).
|
||||
Run \`uncaged-workflow help --skill develop\` for thread ID lookup, CAS commands, and meta output guide.
|
||||
|
||||
## Storing phase details — MANDATORY
|
||||
|
||||
|
||||
Reference in New Issue
Block a user