Compare commits

...

3 Commits

Author SHA1 Message Date
xiaoju 23b2c3b47d feat(reviewer): enrich prompt with conventions awareness + strict verdicts
- Read preparer's conventions from thread context
- Review checklist: correctness, conventions, consistency, edge cases
- No nits: every issue is blocking, approve only at zero issues
- Generic prompt, no workflow-specific concepts

Closes #91

小橘 🍊
2026-05-07 16:25:31 +00:00
xingyue bfea771a52 Merge pull request 'fix(cli): improve usage format + fix skill index title' (#86) from fix/85-usage-format into main 2026-05-07 15:33:27 +00:00
xiaoju 5e411a1f19 fix(cli): improve usage output format + fix skill index title
- Usage: grouped sections with titles, aligned descriptions
- Header: 'uncaged-workflow — workflow engine CLI'
- Footer: 'Use <command> --help for subcommand details.'
- Fix skill index title: 'uncaged-workflow skill' (was: help --skill)
- 242 tests pass

Closes #85

小橘 🍊
2026-05-07 15:31:48 +00:00
5 changed files with 118 additions and 15 deletions
+32 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { runCli } from "../src/cli-dispatch.js";
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
import {
formatSkillDoc,
formatSkillIndex,
@@ -90,6 +90,8 @@ describe("getSkillTopics", () => {
describe("formatSkillIndex", () => {
test("lists all topics", () => {
const idx = formatSkillIndex();
expect(idx).toContain("# uncaged-workflow skill");
expect(idx).not.toContain("# uncaged-workflow help --skill");
expect(idx).toContain("cli");
expect(idx).toContain("develop");
expect(idx).toContain("author");
@@ -97,6 +99,35 @@ describe("formatSkillIndex", () => {
});
});
describe("formatCliUsage", () => {
test("has tagline, grouped sections, help hint, and env vars", () => {
const u = formatCliUsage();
expect(u.startsWith("uncaged-workflow — workflow engine CLI")).toBe(true);
expect(u).toContain("Workflow registry:");
expect(u).toContain("Thread execution:");
expect(u).toContain("Content-addressable storage:");
expect(u).toContain("Development:");
expect(u).toContain("Shortcuts:");
expect(u).toContain("Reference:");
expect(u).toContain("skill [topic]");
expect(u).toContain("Agent-consumable docs");
expect(u).toContain("Use <command> --help for subcommand details.");
expect(u).toContain("Environment variables:");
expect(u).toContain("WORKFLOW_STORAGE_ROOT");
expect(u).toContain("UNCAGED_WORKFLOW_STORAGE_ROOT");
});
test("lists commands from registry with descriptions", () => {
const u = formatCliUsage();
expect(u).toContain("workflow add");
expect(u).toContain("Register a workflow bundle in the registry");
expect(u).toContain("thread run");
expect(u).toContain("Start a new thread executing a workflow");
expect(u).toContain("cas gc");
expect(u).toContain("Garbage-collect unreferenced CAS entries");
});
});
describe("formatSkillTopic('cli') — legacy formatSkillDoc", () => {
const doc = formatSkillDoc();
@@ -129,8 +129,9 @@ describe("init workspace", () => {
test("usage lists init subcommands", () => {
const u = formatCliUsage();
expect(u).toContain("uncaged-workflow init workspace <name>");
expect(u).toContain("uncaged-workflow init template <name>");
expect(u).toContain("init workspace <name>");
expect(u).toContain("init template <name>");
expect(u).toContain("Development:");
});
test("runCli rejects unknown init subcommand", async () => {
+61 -9
View File
@@ -3,7 +3,12 @@ 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, formatSkillIndex, formatSkillTopic } from "./cmd-help.js";
import {
formatSkillDoc,
formatSkillIndex,
formatSkillTopic,
getSkillTopics,
} from "./cmd-help.js";
import { cmdHistory } from "./cmd-history.js";
import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js";
import { cmdKill } from "./cmd-kill.js";
@@ -525,21 +530,68 @@ export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
// ── Auto-generated CLI usage ───────────────────────────────────────────
const USAGE_SECTION_BY_GROUP: Record<string, string> = {
workflow: "Workflow registry:",
thread: "Thread execution:",
cas: "Content-addressable storage:",
init: "Development:",
};
function formatUsageCommandLines(
rows: ReadonlyArray<{ prefix: string; description: string }>,
): string[] {
const maxPrefix = rows.reduce((max, row) => Math.max(max, row.prefix.length), 0);
const gap = 2;
return rows.map((row) => {
const pad = " ".repeat(maxPrefix - row.prefix.length + gap);
return ` ${row.prefix}${pad}${row.description}`;
});
}
export function formatCliUsage(): string {
const groups = getCommandRegistry();
const lines: string[] = ["Usage:"];
const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""];
for (const group of groups) {
for (const cmd of group.commands) {
const args = cmd.args ? ` ${cmd.args}` : "";
lines.push(` uncaged-workflow ${group.name} ${cmd.name}${args}`);
const sectionTitle = USAGE_SECTION_BY_GROUP[group.name];
if (sectionTitle === undefined) {
throw new Error(`BUG: missing usage section title for group "${group.name}"`);
}
lines.push(sectionTitle);
const rows = group.commands.map((cmd) => {
const args = cmd.args ? ` ${cmd.args}` : "";
return {
prefix: `${group.name} ${cmd.name}${args}`,
description: cmd.description,
};
});
lines.push(...formatUsageCommandLines(rows));
lines.push("");
}
lines.push(" uncaged-workflow run <name> [...] (shortcut for thread run)");
lines.push(" uncaged-workflow live <thread-id> [...] (shortcut for thread live)");
lines.push("Shortcuts:");
lines.push(
...formatUsageCommandLines([
{ prefix: "run <name> [...]", description: "→ thread run" },
{ prefix: "live <id> [...]", description: "→ thread live" },
]),
);
lines.push("");
lines.push(" uncaged-workflow skill [topic] agent-consumable reference docs");
lines.push(" uncaged-workflow help show this usage");
lines.push("Reference:");
const skillTopicNames = getSkillTopics()
.map((t) => t.name)
.join(", ");
lines.push(
...formatUsageCommandLines([
{
prefix: "skill [topic]",
description: `Agent-consumable docs (${skillTopicNames})`,
},
]),
);
lines.push("");
lines.push("Use <command> --help for subcommand details.");
lines.push("");
lines.push("Environment variables:");
lines.push(
+1 -1
View File
@@ -34,7 +34,7 @@ export function formatSkillTopic(topic: string): string | null {
export function formatSkillIndex(): string {
const rows = SKILL_TOPICS.map((t) => `| \`${t.name}\` | ${t.description} |`);
return `# uncaged-workflow help --skill
return `# uncaged-workflow skill
Available topics:
@@ -12,8 +12,27 @@ export const reviewerMetaSchema = z.discriminatedUnion("status", [
]);
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
const REVIEWER_SYSTEM = `You are a code reviewer. Review the current git diff. Give a clear approve or reject verdict.
Only reject for blocking issues. End with your verdict.`;
const REVIEWER_SYSTEM = `You are a code reviewer. Review the git diff for correctness, consistency, and adherence to project conventions.
## Review process
1. Read the **preparer**'s output in the thread for project conventions (coding style, naming, commit format, etc.).
2. Review the diff against these conventions.
3. For documentation changes, verify that names, paths, and references match the actual codebase.
## Review checklist
- **Correctness** — does the code do what it claims? Logic bugs, off-by-one, missing returns?
- **Conventions** — naming, imports, code style per project rules?
- **Consistency** — do docs/comments match actual code? Are references current and accurate?
- **Edge cases** — missing error handling, null checks, boundary conditions?
## Verdict
- **Approve** only if there are zero issues
- **Reject** with specific issues that must be fixed — every issue you find is blocking
Be thorough. A false approve costs more than a false reject.`;
export const reviewerRole: RoleDefinition<ReviewerMeta> = {
description: "Runs git diff checks and sets approved when the change is ready.",