Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a425521da | |||
| f174f2fd0a | |||
| 355594d074 | |||
| fd7609fe90 | |||
| dacecfbbb7 | |||
| 3238eaeddf | |||
| 995f273fa5 | |||
| 866154ad73 | |||
| 8efc5050cb | |||
| 3fb60ee649 | |||
| e181f67a2d |
@@ -0,0 +1,167 @@
|
||||
name: "solve-issue"
|
||||
description: "TDD-driven issue resolution for small, focused changes. Loop protection relies on engine maxRounds."
|
||||
roles:
|
||||
planner:
|
||||
description: "Analyzes issue and outputs a TDD test spec"
|
||||
goal: "You are a planning agent. You analyze Gitea issues and produce a TDD test specification that downstream roles will implement and verify."
|
||||
capabilities:
|
||||
- issue-analysis
|
||||
- planning
|
||||
procedure: |
|
||||
On first run (no previous steps):
|
||||
1. Read the issue and all comments from Gitea using `tea issues <number> -r <owner/repo>`
|
||||
2. Read CLAUDE.md (or equivalent project conventions file) to understand coding standards
|
||||
3. Assess whether the issue has enough information to produce a test spec
|
||||
4. If insufficient info: comment on the issue via `echo "..." | tea comment <number> -r <owner/repo>` (skip if you already commented), then output status=insufficient_info and terminate
|
||||
5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios
|
||||
|
||||
On subsequent runs (bounced back by tester with fix_spec):
|
||||
1. Read the tester's output from the previous step to understand what's wrong with the spec
|
||||
2. Revise the test spec accordingly
|
||||
|
||||
After producing the test spec:
|
||||
1. Store it via `uwf cas put "<markdown content>"` and capture the returned hash
|
||||
2. Put the hash in meta.plan (required when status=ready)
|
||||
output: "Output a brief summary of the test spec. Frontmatter must include: status (ready or insufficient_info) and plan (CAS hash of the test spec, required when status=ready)."
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [ready, insufficient_info]
|
||||
plan:
|
||||
type: string
|
||||
required: [status]
|
||||
developer:
|
||||
description: "TDD implementation per test spec"
|
||||
goal: "You are a developer agent. You implement code changes following TDD — write tests first, then implementation."
|
||||
capabilities:
|
||||
- coding
|
||||
procedure: |
|
||||
1. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's meta.plan)
|
||||
2. If bounced back from reviewer or tester: read the previous role's output to understand what needs fixing
|
||||
3. Write tests first based on the spec
|
||||
4. Implement the code to make tests pass
|
||||
5. Ensure `bun run build` passes with no errors
|
||||
6. Run `bun test` to verify all tests pass
|
||||
output: "List all files changed and provide a summary. Frontmatter must include: status (done or failed)."
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [done, failed]
|
||||
required: [status]
|
||||
reviewer:
|
||||
description: "Code standards compliance check"
|
||||
goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)."
|
||||
capabilities:
|
||||
- code-review
|
||||
- static-analysis
|
||||
procedure: |
|
||||
Hard checks (must all pass):
|
||||
1. `bun run build` — no build errors
|
||||
2. `bunx biome check` — no lint violations
|
||||
3. TypeScript strict mode — no type errors
|
||||
|
||||
Soft checks (review against CLAUDE.md conventions):
|
||||
- Functional-first: `function` + `type`, not `class` + `interface`
|
||||
- No optional properties (`?:`) — use `T | null`
|
||||
- Naming conventions (kebab-case files, PascalCase types, camelCase functions)
|
||||
- Module boundary discipline (folder exports via index.ts)
|
||||
- No `console.log` (use structured logger)
|
||||
- No dynamic imports in production code
|
||||
|
||||
Only review standards compliance. Do NOT test functionality.
|
||||
If rejecting, you MUST explain the specific reason in your output.
|
||||
output: "Explain your decision with specific file/line references. Frontmatter must include: approved (true or false)."
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
approved:
|
||||
type: boolean
|
||||
required: [approved]
|
||||
tester:
|
||||
description: "Functional correctness verification"
|
||||
goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec."
|
||||
capabilities:
|
||||
- testing
|
||||
procedure: |
|
||||
1. Run `bun test` for automated test verification
|
||||
2. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's meta.plan)
|
||||
3. Verify each scenario in the spec is covered and passing
|
||||
4. Determine outcome:
|
||||
- passed: all scenarios verified, tests pass
|
||||
- fix_code: tests fail or implementation doesn't match spec → send back to developer
|
||||
- fix_spec: the spec itself is wrong or incomplete → send back to planner
|
||||
output: "Report test results per scenario. Frontmatter must include: status (passed, fix_code, or fix_spec)."
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [passed, fix_code, fix_spec]
|
||||
required: [status]
|
||||
committer:
|
||||
description: "Commits and creates PR"
|
||||
goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue."
|
||||
capabilities: []
|
||||
procedure: |
|
||||
Note: You inherit the developer's worktree and branch. Do NOT create a new branch.
|
||||
1. Stage all changes: `git add -A`
|
||||
2. Commit with a descriptive message referencing the issue: `git commit -m "type: description\n\nFixes #N"`
|
||||
3. Push the branch: `git push -u origin <branch-name>`
|
||||
- If push hook fails: capture the error log in your output, mark hook_failed
|
||||
4. On push success: create a PR via `tea pr create --title "..." --description "..."`
|
||||
- PR description must follow the project template: What / Why / Changes / Ref sections, with `Fixes #N` in Ref
|
||||
output: "Include PR URL on success or error log on failure. Frontmatter must include: success (true or false)."
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
required: [success]
|
||||
conditions:
|
||||
insufficientInfo:
|
||||
description: "Planner determined there's not enough info to proceed"
|
||||
expression: "steps[-1].output.status = 'insufficient_info'"
|
||||
devFailed:
|
||||
description: "Developer failed to implement"
|
||||
expression: "steps[-1].output.status = 'failed'"
|
||||
rejected:
|
||||
description: "Reviewer rejected the implementation"
|
||||
expression: "steps[-1].output.approved = false"
|
||||
fixCode:
|
||||
description: "Tester found code issues"
|
||||
expression: "steps[-1].output.status = 'fix_code'"
|
||||
fixSpec:
|
||||
description: "Tester found spec issues"
|
||||
expression: "steps[-1].output.status = 'fix_spec'"
|
||||
hookFailed:
|
||||
description: "Push hook failed"
|
||||
expression: "steps[-1].output.success = false"
|
||||
graph:
|
||||
$START:
|
||||
- role: "planner"
|
||||
planner:
|
||||
- role: "$END"
|
||||
condition: "insufficientInfo"
|
||||
- role: "developer"
|
||||
developer:
|
||||
- role: "$END"
|
||||
condition: "devFailed"
|
||||
- role: "reviewer"
|
||||
reviewer:
|
||||
- role: "developer"
|
||||
condition: "rejected"
|
||||
- role: "tester"
|
||||
tester:
|
||||
- role: "developer"
|
||||
condition: "fixCode"
|
||||
- role: "planner"
|
||||
condition: "fixSpec"
|
||||
- role: "committer"
|
||||
committer:
|
||||
- role: "developer"
|
||||
condition: "hookFailed"
|
||||
- role: "$END"
|
||||
+13
-1
@@ -5,6 +5,8 @@
|
||||
"**",
|
||||
"!**/dist",
|
||||
"!**/node_modules",
|
||||
"!**/legacy-packages",
|
||||
"!scripts",
|
||||
"!packages/workflow/workflow",
|
||||
"!xiaoju/scripts/bundle.ts"
|
||||
]
|
||||
@@ -36,7 +38,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.d.ts"],
|
||||
"includes": ["**/*.d.ts", "**/vitest.config.*"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
@@ -44,6 +46,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/cli.ts", "**/setup.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"suspicious": {
|
||||
"noConsole": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"linter": {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { Hash, JSONSchema, Store } from "@uncaged/json-cas";
|
||||
import type { JSONSchema, Store } from "@uncaged/json-cas";
|
||||
import { bootstrap, getSchema, refs, walk } from "@uncaged/json-cas";
|
||||
import { createFsStore } from "@uncaged/json-cas-fs";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { generateCliReference as cmdSkillCli } from "@uncaged/workflow-util";
|
||||
@@ -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 CLI Reference");
|
||||
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 CLI Reference");
|
||||
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 CLI Reference");
|
||||
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", () => {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { RoleDefinition } from "@uncaged/workflow-protocol";
|
||||
import { generateCliReference } from "@uncaged/workflow-util";
|
||||
|
||||
/**
|
||||
* 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 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[] = [];
|
||||
@@ -18,6 +23,15 @@ export function buildRolePrompt(role: RoleDefinition): string {
|
||||
sections.push(`## Capabilities\n\n${list}`);
|
||||
}
|
||||
|
||||
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\n")}`);
|
||||
|
||||
if (role.procedure !== "") {
|
||||
sections.push(`## Procedure\n\n${role.procedure}`);
|
||||
}
|
||||
|
||||
@@ -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 <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 goal, capabilities, procedure, output, and meta; the moderator routes between roles.
|
||||
`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export { encodeUint64AsCrockford } from "./base32.js";
|
||||
export { generateCliReference } from "./cli-reference.js";
|
||||
export { env } from "./env.js";
|
||||
export type {
|
||||
AgentFrontmatter,
|
||||
|
||||
Reference in New Issue
Block a user