Compare commits

...

13 Commits

Author SHA1 Message Date
xiaoju 09b7ddf6d0 feat: add developer skill — coding conventions + architecture guide
CI / test (pull_request) Successful in 1m26s
Adds 'uwf skill developer' for contributors to the workflow engine.
Covers: monorepo structure, dependency layers, functional-first conventions,
error handling, logging with tagged logger, development workflow,
testing, publishing, key modules (moderator, extract pipeline, createAgent).

Refs #541
2026-05-26 17:11:07 +00:00
xiaomo c4e94bbe56 Merge pull request 'feat: add author skill — workflow YAML design guide' (#547) from feat/539-skill-author into main
CI / test (push) Successful in 1m11s
2026-05-26 17:04:50 +00:00
xiaoju dbefe793f2 feat: add author skill — workflow YAML design guide
CI / test (pull_request) Successful in 1m4s
Adds 'uwf skill author' for agents/humans designing workflow definitions.
Covers: YAML structure, role definition, frontmatter schema design,
graph routing, edge prompts, self-testing, and common pitfalls.

Refs #539
2026-05-26 17:02:53 +00:00
xiaomo 6483bc4861 Merge pull request 'feat: add user skill — CLI guide with quick start' (#546) from feat/538-skill-user into main
CI / test (push) Successful in 1m40s
2026-05-26 16:27:43 +00:00
xiaoju fecb02b115 feat: add user skill — CLI guide with quick start and typical workflows
CI / test (pull_request) Successful in 1m26s
Adds 'uwf skill user' command for agents/humans using the uwf CLI.
Covers setup, workflow management, thread lifecycle, step operations,
CAS queries, logging, and global options with a Quick Start guide.

Refs #538
2026-05-26 16:24:39 +00:00
xiaomo 87938c1886 Merge pull request 'feat: add actor skill — frontmatter protocol + CAS reference' (#545) from feat/540-skill-actor into main
CI / test (push) Failing after 23s
2026-05-26 15:44:31 +00:00
xiaoju 95a130136b feat: add actor skill — frontmatter protocol + CAS reference
CI / test (pull_request) Failing after 8m9s
Adds 'uwf skill actor' command for agents executing workflow roles.
Covers the two things an actor needs to know:
1. Frontmatter output protocol (status field, schema-defined fields)
2. CAS operations (put, get, refs, walk, merkle DAG pattern)

Refs #540
2026-05-26 15:32:03 +00:00
xiaomo aba5642908 Merge pull request 'ci: use test:ci to skip integration tests in CI' (#543) from fix/ci-skip-integration-tests into main
CI / test (push) Successful in 3m32s
2026-05-26 15:26:02 +00:00
xingyue 168e604602 ci: use test:ci to skip integration tests in CI
CI / test (pull_request) Successful in 9m13s
The HermesAcpClient integration tests require a live Hermes agent
process and always timeout (3 × 120s) in CI containers, causing
every CI run to fail for ~6 minutes before reporting failure.

Switch from `bun run test` to `bun run test:ci` which was already
defined in all testable packages — workflow-agent-hermes's test:ci
runs only unit tests (__tests__/*.test.ts), skipping integration/.
2026-05-26 23:08:16 +08:00
xiaoju d50159c5a7 refactor: split e2e-walkthrough into 6 roles with dedicated cleanup
CI / test (push) Failing after 11m29s
- bootstrap: Docker + bun install + bun link + verify
- config-and-registry: config get/set/list + workflow add/show/list
- thread-ops: thread start/list/show/exec
- inspect: step list/show + thread read + CAS get/has/refs/walk
- cancel-and-fork: cancel + fork + logs
- cleanup: docker rm -f (all fail paths route here)

小橘 🍊
2026-05-26 14:47:44 +00:00
xiaoju 9a7ad34e55 chore: move e2e-walkthrough to .workflows/, fix CI, clean .plan/
CI / test (push) Failing after 11m54s
- e2e-walkthrough.yaml: examples/ → .workflows/ (project workflows, not examples)
- .gitea/workflows/ci.yml: bun test → bun run test (avoid legacy-packages)
- .plan/: removed stale test spec from #335

小橘 🍊
2026-05-26 14:37:46 +00:00
xiaoju 4193157124 refactor(hermes): clean up loadHermesSessionFromDb
CI / test (push) Failing after 11m14s
- Remove unnecessary Promise.resolve() wrappers (sync function)
- Use try/finally for db.close() instead of manual close at each exit
- Flatten nested try/catch

Follow-up to #535 review nits.

小橘 🍊
2026-05-26 14:27:31 +00:00
xiaomo 6ff1414cf0 Merge pull request 'fix(hermes): add SQLite fallback for loadHermesSession' (#536) from fix/535-sqlite-fallback into main
CI / test (push) Failing after 9m23s
Merge pull request #536: fix(hermes): add SQLite fallback for loadHermesSession
2026-05-26 14:24:42 +00:00
13 changed files with 919 additions and 332 deletions
+1 -1
View File
@@ -22,4 +22,4 @@ jobs:
run: bun run check run: bun run check
- name: Test - name: Test
run: bun test run: bun run test:ci
-83
View File
@@ -1,83 +0,0 @@
# Test Spec: uwf setup model connectivity validation (#335)
## Context
File: `packages/cli-workflow/src/commands/setup.ts`
Test file: `packages/cli-workflow/src/__tests__/setup-validate.test.ts`
After `cmdSetup` writes config, it should send a test chat completion request to verify the configured model is reachable. If validation fails, warn the user (don't abort — config is already saved).
## Implementation Notes
- Add a `validateModel(baseUrl, apiKey, model)` function that sends a minimal chat completion request (`POST /chat/completions` with `messages: [{role:"user",content:"hi"}]`, `max_tokens: 1`)
- Returns `Result<void, string>` — ok if 2xx response, error with reason string otherwise
- Use `AbortSignal.timeout(15_000)` for the request
- Both `cmdSetup` and `cmdSetupInteractive` should call it after saving config
- `cmdSetup` returns validation result in its return object: `{ ...existing, validation: { ok: true } | { ok: false, error: string } }`
- `cmdSetupInteractive` prints a warning to console if validation fails, success message if it passes
- Use the project logger (`createLogger`) — no raw `console.log` except in interactive CLI output (per CLAUDE.md)
## Test Cases (vitest)
### 1. `validateModel` — success path
- Mock `fetch` to return `{ status: 200, ok: true, json: () => ({}) }`
- Call `validateModel(baseUrl, apiKey, model)`
- Assert returns `{ ok: true, value: undefined }`
- Assert fetch was called with correct URL (`${baseUrl}/chat/completions`), correct headers (`Authorization: Bearer ${apiKey}`), correct body (model, messages, max_tokens: 1)
### 2. `validateModel` — HTTP error (401 unauthorized)
- Mock `fetch` to return `{ status: 401, ok: false, statusText: "Unauthorized" }`
- Call `validateModel(baseUrl, apiKey, model)`
- Assert returns `{ ok: false, error: <string containing "401"> }`
### 3. `validateModel` — HTTP error (404 model not found)
- Mock `fetch` to return `{ status: 404, ok: false, statusText: "Not Found" }`
- Assert returns `{ ok: false, error: <string containing "404"> }`
### 4. `validateModel` — network timeout
- Mock `fetch` to throw `DOMException` with name `AbortError`
- Assert returns `{ ok: false, error: <string containing "timeout" or "unreachable"> }`
### 5. `validateModel` — network error (DNS failure, connection refused)
- Mock `fetch` to throw `TypeError("fetch failed")`
- Assert returns `{ ok: false, error: <string mentioning connectivity> }`
### 6. `cmdSetup` — includes validation result on success
- Mock global `fetch` for `/chat/completions` to succeed
- Call `cmdSetup({ provider, baseUrl, apiKey, model, storageRoot })`
- Assert returned object has `validation: { ok: true, value: undefined }`
- Assert config files are still written (existing behavior preserved)
### 7. `cmdSetup` — includes validation result on failure (config still saved)
- Mock global `fetch` for `/chat/completions` to return 401
- Call `cmdSetup({ ... })`
- Assert returned object has `validation: { ok: false, error: ... }`
- Assert `config.yaml` and `.env` are still written (validation failure doesn't prevent saving)
### 8. `cmdSetupInteractive` — prints success message on validation pass
- Mock `fetch` for both `/models` and `/chat/completions` to succeed
- Mock stdin to provide valid selections
- Capture console output
- Assert output contains a success message like "Model verified" or "✓"
### 9. `cmdSetupInteractive` — prints warning on validation failure
- Mock `fetch`: `/models` succeeds, `/chat/completions` returns 401
- Mock stdin for valid selections
- Capture console output
- Assert output contains a warning about model not being reachable and suggests trying a different model
### 10. `validateModel` — request body correctness
- Mock `fetch` to capture the request body
- Call `validateModel(baseUrl, apiKey, "test-model")`
- Assert body is `{ model: "test-model", messages: [{role: "user", content: "hi"}], max_tokens: 1 }`
## Export Requirements
- `validateModel` must be exported (for direct unit testing)
- Signature: `async function validateModel(baseUrl: string, apiKey: string, model: string): Promise<Result<void, string>>`
- `Result` type: `{ ok: true; value: T } | { ok: false; error: E }` (project convention)
## Files to Create/Modify
- **New**: `packages/cli-workflow/src/__tests__/setup-validate.test.ts` — all test cases above
- **Modify**: `packages/cli-workflow/src/commands/setup.ts` — add `validateModel`, integrate into `cmdSetup` and `cmdSetupInteractive`
+269
View File
@@ -0,0 +1,269 @@
name: "e2e-walkthrough"
description: "End-to-end walkthrough of uwf CLI. Dogfooding: uwf tests uwf. Each role validates a phase of the CLI surface inside an isolated Docker container."
roles:
bootstrap:
description: "Start Docker container with isolated storage, verify uwf is runnable"
goal: "You are an E2E test runner. Set up an isolated Docker environment and verify basic uwf functionality."
capabilities:
- docker
- shell
procedure: |
1. Start a Docker container with isolated storage:
```
docker run -d --name uwf-e2e-$$ \
-v $HOME:$HOME \
-e HOME=$HOME \
-e UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage \
-w ~/repos/workflow \
node:22-bookworm \
sleep infinity
```
2. Inside the container, install bun, install deps, then `bun link` all packages
so that `uwf`, `uwf-hermes`, `uwf-builtin` are on PATH (from source):
```
docker exec uwf-e2e-$$ bash -c '
# Install bun
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
# Isolated storage
mkdir -p $UNCAGED_WORKFLOW_STORAGE_ROOT
# Install workspace deps
cd ~/repos/workflow && bun install --frozen-lockfile
# bun link each package that has a bin entry
cd packages/cli-workflow && bun link && cd ../..
cd packages/workflow-agent-hermes && bun link && cd ../..
cd packages/workflow-agent-builtin && bun link && cd ../..
'
```
3. Verify all three commands are available inside the container:
```
docker exec uwf-e2e-$$ bash -c 'export PATH="$HOME/.bun/bin:$PATH" && uwf --version'
docker exec uwf-e2e-$$ bash -c 'export PATH="$HOME/.bun/bin:$PATH" && uwf-hermes --help'
docker exec uwf-e2e-$$ bash -c 'export PATH="$HOME/.bun/bin:$PATH" && uwf-builtin --help'
```
4. Copy host config if it exists:
```
docker exec uwf-e2e-$$ bash -c '
if [ -f $HOME/.uncaged/workflow/config.yaml ]; then
cp $HOME/.uncaged/workflow/config.yaml $UNCAGED_WORKFLOW_STORAGE_ROOT/config.yaml
fi
'
```
Report the container name and confirm uwf + agents are working.
Set containerName to the Docker container name for subsequent roles.
output: "Report uwf version and container readiness. Set $status to pass with containerName, or fail with error."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
containerName: { type: string }
required: [$status, containerName]
- properties:
$status: { const: "fail" }
error: { type: string }
required: [$status, error]
config-and-registry:
description: "Validate uwf config commands and workflow registration"
goal: "You are an E2E test runner. Validate uwf config operations and workflow registration inside the Docker container."
capabilities:
- docker
- shell
procedure: |
Use the container from the previous step (containerName is in your prompt).
All commands run via: `docker exec <containerName> bash -c '...'`
All commands use `uwf` (installed via `bun link` inside the container).
Remember to set env vars in each exec:
export PATH="$HOME/.bun/bin:$PATH"
export UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage
Config tests:
1. `uwf config list` — verify it returns valid JSON
2. `uwf config set models.test.name test-model` — set a test key
3. `uwf config get models.test.name` — verify it returns "test-model"
Workflow registration tests:
4. `uwf workflow add ~/repos/workflow/examples/solve-issue.yaml` — register workflow
5. Verify the output contains a hash
6. `uwf workflow list` — verify non-empty array
7. Capture the workflow name from the list
8. `uwf workflow show <name>` — verify it returns roles
Report all test results with pass/fail counts.
output: "Report test results. Set $status to pass (with workflowName and containerName) or fail."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
workflowName: { type: string }
containerName: { type: string }
required: [$status, workflowName, containerName]
- properties:
$status: { const: "fail" }
error: { type: string }
containerName: { type: string }
required: [$status, error, containerName]
thread-ops:
description: "Test thread start, list, show, and exec"
goal: "You are an E2E test runner. Validate thread creation and execution inside the Docker container."
capabilities:
- docker
- shell
procedure: |
Use the container (containerName) and workflow (workflowName) from your prompt.
All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH="$HOME/.bun/bin:$PATH" UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage
1. `uwf thread start <workflowName> -p 'E2E test: what is 2+2?'` — capture thread ID from JSON output
2. `uwf thread list` — verify the thread appears in the list
3. `uwf thread show <threadId>` — verify head pointer exists
4. `uwf thread exec <threadId> --agent uwf-builtin` — execute one step
5. Verify exec returns JSON with a head field
Report results. Pass threadId and containerName forward.
output: "Report test results. Set $status to pass (with threadId, workflowName, containerName) or fail."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
threadId: { type: string }
workflowName: { type: string }
containerName: { type: string }
required: [$status, threadId, workflowName, containerName]
- properties:
$status: { const: "fail" }
error: { type: string }
containerName: { type: string }
required: [$status, error, containerName]
inspect:
description: "Test step list/show, thread read, and CAS operations"
goal: "You are an E2E test runner. Validate read and inspect operations inside the Docker container."
capabilities:
- docker
- shell
procedure: |
Use the container (containerName) and threadId from your prompt.
All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH="$HOME/.bun/bin:$PATH" UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage
Step inspection:
1. `uwf step list <threadId>` — verify steps array has length > 1
2. Capture the last step hash from the output
3. `uwf step show <lastStepHash>` — verify it returns a role field
Thread read:
4. `uwf thread read <threadId>` — verify non-empty output
CAS operations:
5. `uwf cas get <lastStepHash>` — verify returns a type field
6. `uwf cas has <lastStepHash>` — verify exits 0
7. `uwf cas refs <lastStepHash>` — list refs (may be empty)
8. `uwf cas walk <lastStepHash>` — verify returns non-empty array
Report results. Pass threadId, lastStepHash, workflowName, containerName forward.
output: "Report test results. Set $status to pass (with threadId, lastStepHash, workflowName, containerName) or fail."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
threadId: { type: string }
lastStepHash: { type: string }
workflowName: { type: string }
containerName: { type: string }
required: [$status, threadId, lastStepHash, workflowName, containerName]
- properties:
$status: { const: "fail" }
error: { type: string }
containerName: { type: string }
required: [$status, error, containerName]
cancel-and-fork:
description: "Test thread cancel, step fork, and log inspection"
goal: "You are an E2E test runner. Validate cancel, fork, and log operations inside the Docker container."
capabilities:
- docker
- shell
procedure: |
Use containerName, threadId, lastStepHash, and workflowName from your prompt.
All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH="$HOME/.bun/bin:$PATH" UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage
Cancel:
1. Start a second thread: `uwf thread start <workflowName> -p 'E2E cancel test'`
2. Cancel it: `uwf thread cancel <secondThreadId>`
3. Verify it appears in completed list: `uwf thread list --status completed`
Fork:
4. Fork from the first thread's last step: `uwf step fork <lastStepHash>`
5. Verify fork creates a new thread with a different ID
Logs:
6. `uwf log list` — verify output (may be empty)
7. `uwf log show --thread <threadId>` — verify runs without error
Report results with summary.
output: "Report test results with summary. Set $status to pass or fail."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
containerName: { type: string }
summary: { type: string }
required: [$status, containerName, summary]
- properties:
$status: { const: "fail" }
error: { type: string }
containerName: { type: string }
required: [$status, error, containerName]
cleanup:
description: "Remove Docker container"
goal: "You are an E2E test runner. Clean up the Docker container used for testing."
capabilities:
- docker
- shell
procedure: |
Remove the Docker container (containerName is in your prompt):
1. `docker rm -f <containerName>`
2. Verify the container is gone: `docker ps -a --filter name=<containerName> --format '{{.Names}}'` should return empty
Report cleanup result.
output: "Report cleanup result. Set $status to pass or fail."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
summary: { type: string }
required: [$status, summary]
- properties:
$status: { const: "fail" }
error: { type: string }
required: [$status, error]
graph:
$START:
_: { role: "bootstrap", prompt: "Set up the Docker container and verify uwf is runnable." }
bootstrap:
pass: { role: "config-and-registry", prompt: "Container {{{containerName}}} is ready. Validate config and workflow registration." }
fail: { role: "$END", prompt: "Bootstrap failed: {{{error}}}. No container was created." }
config-and-registry:
pass: { role: "thread-ops", prompt: "Config and registry OK. Workflow '{{{workflowName}}}' registered. Container: {{{containerName}}}. Now test thread operations." }
fail: { role: "cleanup", prompt: "Config/registry failed: {{{error}}}. Clean up container {{{containerName}}}." }
thread-ops:
pass: { role: "inspect", prompt: "Thread ops OK. threadId={{{threadId}}}, workflowName={{{workflowName}}}, containerName={{{containerName}}}. Now test inspect operations." }
fail: { role: "cleanup", prompt: "Thread ops failed: {{{error}}}. Clean up container {{{containerName}}}." }
inspect:
pass: { role: "cancel-and-fork", prompt: "Inspect OK. threadId={{{threadId}}}, lastStepHash={{{lastStepHash}}}, workflowName={{{workflowName}}}, containerName={{{containerName}}}. Now test cancel, fork, and logs." }
fail: { role: "cleanup", prompt: "Inspect failed: {{{error}}}. Clean up container {{{containerName}}}." }
cancel-and-fork:
pass: { role: "cleanup", prompt: "All tests passed! {{{summary}}}. Clean up container {{{containerName}}}." }
fail: { role: "cleanup", prompt: "Cancel/fork failed: {{{error}}}. Clean up container {{{containerName}}}." }
cleanup:
pass: { role: "$END", prompt: "E2E walkthrough complete. {{{summary}}}" }
fail: { role: "$END", prompt: "Cleanup failed: {{{error}}}. Manual cleanup may be needed." }
-210
View File
@@ -1,210 +0,0 @@
name: "e2e-walkthrough"
description: "End-to-end walkthrough of uwf CLI. Dogfooding: uwf tests uwf. Each role validates a phase of the CLI surface inside an isolated Docker container."
roles:
bootstrap:
description: "Start Docker container with isolated storage, verify uwf is runnable"
goal: "You are an E2E test runner. Set up an isolated Docker environment and verify basic uwf functionality."
capabilities:
- docker
- shell
procedure: |
1. Create a temp dir for this E2E run: `E2E_DIR=$(mktemp -d /tmp/uwf-e2e-XXXXXX)`
2. Start a Docker container with isolated storage:
```
docker run -d --name uwf-e2e-$$ \
-v $HOME:$HOME \
-e HOME=$HOME \
-e UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage \
-w ~/repos/workflow \
node:22-bookworm \
sleep infinity
```
3. Inside the container, install bun, install deps, then `bun link` all packages
so that `uwf`, `uwf-hermes`, `uwf-builtin` are on PATH (from source):
```
docker exec uwf-e2e-$$ bash -c '
# Install bun
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
# Isolated storage
mkdir -p $UNCAGED_WORKFLOW_STORAGE_ROOT
# Install workspace deps
cd ~/repos/workflow && bun install --frozen-lockfile
# bun link each package that has a bin entry
cd packages/cli-workflow && bun link && cd ../..
cd packages/workflow-agent-hermes && bun link && cd ../..
cd packages/workflow-agent-builtin && bun link && cd ../..
'
```
4. Verify all three commands are available inside the container:
```
docker exec uwf-e2e-$$ bash -c 'export PATH="$HOME/.bun/bin:$PATH" && uwf --version'
docker exec uwf-e2e-$$ bash -c 'export PATH="$HOME/.bun/bin:$PATH" && uwf-hermes --help'
docker exec uwf-e2e-$$ bash -c 'export PATH="$HOME/.bun/bin:$PATH" && uwf-builtin --help'
```
5. Copy host config if it exists:
```
docker exec uwf-e2e-$$ bash -c '
if [ -f $HOME/.uncaged/workflow/config.yaml ]; then
cp $HOME/.uncaged/workflow/config.yaml $UNCAGED_WORKFLOW_STORAGE_ROOT/config.yaml
fi
'
```
Report the container name and confirm uwf + agents are working.
Set containerName to the Docker container name for subsequent roles.
output: "Report uwf version and container readiness. Set $status to pass with containerName, or fail with error."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
containerName: { type: string }
required: [$status, containerName]
- properties:
$status: { const: "fail" }
error: { type: string }
required: [$status, error]
setup-and-registry:
description: "Validate uwf setup, config commands, and workflow registration"
goal: "You are an E2E test runner. Validate uwf config operations and workflow registration inside the Docker container."
capabilities:
- docker
- shell
procedure: |
Use the container from the previous step (containerName is in your prompt).
All commands run via: `docker exec <containerName> bash -c '...'`
All commands use `uwf` (installed via `bun link` inside the container).
Remember to set env vars in each exec:
export PATH="$HOME/.bun/bin:$PATH"
export UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage
Phase 2 — Config:
1. `uwf config list` — verify it returns valid JSON
2. `uwf config set models.test.name test-model` — set a test key
3. `uwf config get models.test.name` — verify it returns "test-model"
Phase 3 — Workflow registration:
4. `uwf workflow add ~/repos/workflow/examples/solve-issue.yaml` — register workflow
5. Verify the output contains a hash
6. `uwf workflow list` — verify non-empty array
7. Capture the workflow name from the list
8. `uwf workflow show <name>` — verify it returns roles
Report all test results with pass/fail counts.
output: "Report test results. Set $status to pass (with workflowName and containerName) or fail (with error and partial results)."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
workflowName: { type: string }
containerName: { type: string }
testsPassed: { type: number }
required: [$status, workflowName, containerName]
- properties:
$status: { const: "fail" }
error: { type: string }
required: [$status, error]
thread-lifecycle:
description: "Test thread start, exec, read, step list/show, and CAS operations"
goal: "You are an E2E test runner. Validate the full thread lifecycle and CAS operations."
capabilities:
- docker
- shell
procedure: |
Use the container (containerName) and workflow (workflowName) from your prompt.
All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH, UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage
Phase 4 — Thread lifecycle:
1. `uwf thread start <workflowName> -p 'E2E test: what is 2+2?'` — capture thread ID
2. `uwf thread list` — verify thread appears
3. `uwf thread show <threadId>` — verify head pointer exists
4. `uwf thread exec <threadId> --agent uwf-builtin` — execute one step
5. Verify exec returns step info with head
Phase 5 — Read & Inspect:
6. `uwf step list <threadId>` — verify steps exist (length > 1)
7. Capture last step hash
8. `uwf step show <lastStepHash>` — verify it returns role
9. `uwf thread read <threadId>` — verify non-empty output
10. `uwf cas get <lastStepHash>` — verify returns type
11. `uwf cas has <lastStepHash>` — verify exists
12. `uwf cas refs <lastStepHash>` — list refs
13. `uwf cas walk <lastStepHash>` — verify returns nodes
Report all results. Pass the threadId and lastStepHash forward.
output: "Report test results. Set $status to pass (with threadId, lastStepHash, containerName) or fail."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
threadId: { type: string }
lastStepHash: { type: string }
containerName: { type: string }
testsPassed: { type: number }
required: [$status, threadId, lastStepHash, containerName]
- properties:
$status: { const: "fail" }
error: { type: string }
required: [$status, error]
cancel-fork-and-logs:
description: "Test thread cancel, step fork, and log inspection"
goal: "You are an E2E test runner. Validate cancel, fork, and log operations."
capabilities:
- docker
- shell
procedure: |
Use containerName, threadId (first thread), lastStepHash, and workflowName from your prompt.
All commands via: `docker exec <containerName> bash -c '...'`
Set env: PATH, UNCAGED_WORKFLOW_STORAGE_ROOT=/tmp/uwf-e2e-storage
Phase 6 — Cancel & Fork:
1. Start a second thread: `uwf thread start <workflowName> -p 'E2E cancel test'`
2. Cancel it: `uwf thread cancel <secondThreadId>`
3. Verify it appears in completed list: `uwf thread list --status completed`
4. Fork from the first thread's last step: `uwf step fork <lastStepHash>`
5. Verify fork creates a new thread with different ID
Phase 7 — Logs:
6. `uwf log list` — check log files exist
7. `uwf log show --thread <threadId>` — verify log output (may be empty, that's ok)
Phase 8 — Cleanup:
8. Stop and remove the Docker container: `docker rm -f <containerName>`
Report final results with full summary of all phases.
output: "Report final test results with pass/fail counts. Set $status to pass or fail."
frontmatter:
oneOf:
- properties:
$status: { const: "pass" }
totalPassed: { type: number }
summary: { type: string }
required: [$status, totalPassed, summary]
- properties:
$status: { const: "fail" }
error: { type: string }
totalPassed: { type: number }
required: [$status, error]
graph:
$START:
_: { role: "bootstrap", prompt: "Set up the Docker container and verify uwf is runnable." }
bootstrap:
pass: { role: "setup-and-registry", prompt: "Container {{{containerName}}} is ready. Validate config and workflow registration." }
fail: { role: "$END", prompt: "Bootstrap failed: {{{error}}}" }
setup-and-registry:
pass: { role: "thread-lifecycle", prompt: "Config and registry OK. Workflow '{{{workflowName}}}' registered. Container: {{{containerName}}}. Now test thread lifecycle." }
fail: { role: "$END", prompt: "Setup/registry failed: {{{error}}}" }
thread-lifecycle:
pass: { role: "cancel-fork-and-logs", prompt: "Thread lifecycle OK. threadId={{{threadId}}}, lastStepHash={{{lastStepHash}}}, containerName={{{containerName}}}. Now test cancel, fork, logs, and cleanup." }
fail: { role: "$END", prompt: "Thread lifecycle failed: {{{error}}}" }
cancel-fork-and-logs:
pass: { role: "$END", prompt: "All E2E tests passed! {{{summary}}}" }
fail: { role: "$END", prompt: "Cancel/fork/logs phase failed: {{{error}}}. Passed: {{{totalPassed}}}" }
@@ -6,10 +6,14 @@ import { describe, expect, test } from "vitest";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
import { import {
cmdSkillActor,
cmdSkillArchitecture, cmdSkillArchitecture,
cmdSkillAuthor,
cmdSkillCli, cmdSkillCli,
cmdSkillDeveloper,
cmdSkillList, cmdSkillList,
cmdSkillModerator, cmdSkillModerator,
cmdSkillUser,
cmdSkillYaml, cmdSkillYaml,
} from "../commands/skill.js"; } from "../commands/skill.js";
@@ -21,8 +25,11 @@ describe("skill commands", () => {
expect(result).toContain("architecture"); expect(result).toContain("architecture");
expect(result).toContain("yaml"); expect(result).toContain("yaml");
expect(result).toContain("moderator"); expect(result).toContain("moderator");
expect(result).toContain("actor");
expect(result).toContain("user");
expect(result).toContain("author");
expect(result).toContain("developer");
for (const name of result) { for (const name of result) {
expect(typeof name).toBe("string");
expect(name).toMatch(/^\S+$/); expect(name).toMatch(/^\S+$/);
} }
}); });
@@ -62,6 +69,45 @@ describe("skill commands", () => {
expect(result).toContain("uwf"); expect(result).toContain("uwf");
}); });
test("skill actor returns non-empty markdown string", () => {
const result = cmdSkillActor();
expect(typeof result).toBe("string");
expect(result).toContain("frontmatter");
expect(result).toContain("CAS");
expect(result).toContain("status");
expect(result.length).toBeGreaterThan(200);
});
test("skill user returns non-empty markdown string", () => {
const result = cmdSkillUser();
expect(typeof result).toBe("string");
expect(result).toContain("uwf");
expect(result).toContain("thread");
expect(result).toContain("workflow");
expect(result).toContain("Quick Start");
expect(result.length).toBeGreaterThan(500);
});
test("skill author returns non-empty markdown string", () => {
const result = cmdSkillAuthor();
expect(typeof result).toBe("string");
expect(result).toContain("frontmatter");
expect(result).toContain("graph");
expect(result).toContain("$START");
expect(result).toContain("$END");
expect(result).toContain("$status");
expect(result.length).toBeGreaterThan(500);
});
test("skill developer returns non-empty markdown string", () => {
const result = cmdSkillDeveloper();
expect(typeof result).toBe("string");
expect(result).toContain("Monorepo");
expect(result).toContain("CAS");
expect(result).toContain("Biome");
expect(result.length).toBeGreaterThan(500);
});
test("skill help subcommand is suppressed", () => { test("skill help subcommand is suppressed", () => {
const output = execFileSync("bun", ["src/cli.ts", "skill", "--help"], { const output = execFileSync("bun", ["src/cli.ts", "skill", "--help"], {
cwd: join(__dirname, "..", ".."), cwd: join(__dirname, "..", ".."),
@@ -73,6 +119,10 @@ describe("skill commands", () => {
expect(output).toContain("architecture"); expect(output).toContain("architecture");
expect(output).toContain("yaml"); expect(output).toContain("yaml");
expect(output).toContain("moderator"); expect(output).toContain("moderator");
expect(output).toContain("actor");
expect(output).toContain("user");
expect(output).toContain("author");
expect(output).toContain("developer");
expect(output).toContain("list"); expect(output).toContain("list");
}); });
}); });
+32
View File
@@ -17,10 +17,14 @@ import { cmdConfigGet, cmdConfigList, cmdConfigSet } from "./commands/config.js"
import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js"; import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js";
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js"; import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
import { import {
cmdSkillActor,
cmdSkillArchitecture, cmdSkillArchitecture,
cmdSkillAuthor,
cmdSkillCli, cmdSkillCli,
cmdSkillDeveloper,
cmdSkillList, cmdSkillList,
cmdSkillModerator, cmdSkillModerator,
cmdSkillUser,
cmdSkillYaml, cmdSkillYaml,
} from "./commands/skill.js"; } from "./commands/skill.js";
import { cmdStepFork, cmdStepList, cmdStepRead, cmdStepShow } from "./commands/step.js"; import { cmdStepFork, cmdStepList, cmdStepRead, cmdStepShow } from "./commands/step.js";
@@ -503,6 +507,27 @@ skill
console.log(cmdSkillYaml()); console.log(cmdSkillYaml());
}); });
skill
.command("actor")
.description("Print the actor reference (frontmatter protocol + CAS)")
.action(() => {
console.log(cmdSkillActor());
});
skill
.command("author")
.description("Print the author reference (workflow YAML design guide)")
.action(() => {
console.log(cmdSkillAuthor());
});
skill
.command("developer")
.description("Print the developer reference (coding conventions + architecture)")
.action(() => {
console.log(cmdSkillDeveloper());
});
skill skill
.command("moderator") .command("moderator")
.description("Print the moderator reference") .description("Print the moderator reference")
@@ -510,6 +535,13 @@ skill
console.log(cmdSkillModerator()); console.log(cmdSkillModerator());
}); });
skill
.command("user")
.description("Print the user reference (CLI guide + typical workflows)")
.action(() => {
console.log(cmdSkillUser());
});
skill skill
.command("list") .command("list")
.description("List all available skill names") .description("List all available skill names")
+14 -1
View File
@@ -1,11 +1,24 @@
export { export {
generateActorReference as cmdSkillActor,
generateArchitectureReference as cmdSkillArchitecture, generateArchitectureReference as cmdSkillArchitecture,
generateAuthorReference as cmdSkillAuthor,
generateCliReference as cmdSkillCli, generateCliReference as cmdSkillCli,
generateDeveloperReference as cmdSkillDeveloper,
generateModeratorReference as cmdSkillModerator, generateModeratorReference as cmdSkillModerator,
generateUserReference as cmdSkillUser,
generateYamlReference as cmdSkillYaml, generateYamlReference as cmdSkillYaml,
} from "@uncaged/workflow-util"; } from "@uncaged/workflow-util";
const SKILL_NAMES = ["cli", "architecture", "yaml", "moderator"] as const; const SKILL_NAMES = [
"cli",
"architecture",
"yaml",
"moderator",
"actor",
"user",
"author",
"developer",
] as const;
export function cmdSkillList(): ReadonlyArray<string> { export function cmdSkillList(): ReadonlyArray<string> {
return [...SKILL_NAMES]; return [...SKILL_NAMES];
@@ -150,46 +150,42 @@ function dbMessageToSessionMessage(row: DbMessageRow): HermesSessionMessage {
export function loadHermesSessionFromDb( export function loadHermesSessionFromDb(
sessionId: string, sessionId: string,
dbPath: string | null = null, dbPath: string | null = null,
): Promise<HermesSessionJson | null> { ): HermesSessionJson | null {
const resolvedPath = dbPath ?? getHermesDbPath(); const resolvedPath = dbPath ?? getHermesDbPath();
let db: InstanceType<typeof Database> | null = null;
try { try {
const db = new Database(resolvedPath, { readonly: true }); db = new Database(resolvedPath, { readonly: true });
try { const session = db
const session = db .query("SELECT id, model, started_at FROM sessions WHERE id = ?")
.query("SELECT id, model, started_at FROM sessions WHERE id = ?") .get(sessionId) as DbSessionRow | null;
.get(sessionId) as DbSessionRow | null; if (session === null) {
if (session === null) { return null;
db.close();
return Promise.resolve(null);
}
const rows = db
.query(
"SELECT role, content, reasoning, tool_calls FROM messages WHERE session_id = ? ORDER BY id",
)
.all(sessionId) as DbMessageRow[];
db.close();
const messages: HermesSessionMessage[] = [];
for (const row of rows) {
const role = row.role;
if (role !== "user" && role !== "assistant" && role !== "tool") {
continue;
}
messages.push(dbMessageToSessionMessage(row));
}
return Promise.resolve({
session_id: session.id,
model: session.model,
session_start: new Date(session.started_at * 1000).toISOString(),
messages,
});
} catch {
db.close();
return Promise.resolve(null);
} }
const rows = db
.query(
"SELECT role, content, reasoning, tool_calls FROM messages WHERE session_id = ? ORDER BY id",
)
.all(sessionId) as DbMessageRow[];
const messages: HermesSessionMessage[] = [];
for (const row of rows) {
const role = row.role;
if (role !== "user" && role !== "assistant" && role !== "tool") {
continue;
}
messages.push(dbMessageToSessionMessage(row));
}
return {
session_id: session.id,
model: session.model,
session_start: new Date(session.started_at * 1000).toISOString(),
messages,
};
} catch { } catch {
return Promise.resolve(null); return null;
} finally {
db?.close();
} }
} }
@@ -0,0 +1,68 @@
export function generateActorReference(): string {
return `# Actor Reference
You are executing a workflow role. Your system prompt defines your goal, procedure, and output requirements. This reference covers two things you need to know about the workflow engine.
## 1. Frontmatter Output Protocol
Your response **MUST** begin with a YAML frontmatter block at byte position 0 — no preamble text before it.
\`\`\`
---
status: done
myField: some value
---
... markdown body (your work, explanation, notes) ...
\`\`\`
### Standard Field
| Field | Values | Default | Description |
|-------|--------|---------|-------------|
| \`status\` | \`done\`, \`needs_input\`, \`in_progress\`, \`failed\` | \`done\` | Completion signal — determines which graph edge the moderator follows next |
### Schema-Defined Fields
Your role's output schema (shown in the system prompt under "Deliverable Format") defines additional fields. Output **only** the fields listed there — do not invent extra fields.
### Body
Everything after the closing \`---\` fence is the markdown body. Use it for explanations, logs, or human-readable notes. The body is stored but not parsed by the engine.
### Retry
If the engine cannot parse your frontmatter, it will ask you to retry (up to 2 times). Just output the corrected frontmatter block — don't panic.
## 2. CAS (Content-Addressable Store)
Your frontmatter output is automatically stored in CAS. You can also **use CAS directly** to store intermediate artifacts, build merkle DAGs for large outputs, or reference data from previous steps.
### Commands
\`\`\`
uwf cas put-text <text> # store plain text, print hash
uwf cas put <type-hash> <json> # store typed JSON data, print hash
uwf cas get <hash> # read a CAS node (type + payload)
uwf cas has <hash> # check if a hash exists
uwf cas refs <hash> # list direct references from a node
uwf cas walk <hash> # recursive traversal from a node
uwf cas schema list # list registered schemas
uwf cas schema get <hash> # show a schema definition
\`\`\`
### Merkle DAG Pattern
For large outputs, store parts individually and reference their hashes:
\`\`\`bash
# Store individual sections
HASH1=$(uwf cas put-text "section 1 content")
HASH2=$(uwf cas put-text "section 2 content")
# Reference hashes in your frontmatter or in a parent node
\`\`\`
This enables progressive loading — consumers can fetch the root and resolve children on demand.
`;
}
@@ -0,0 +1,183 @@
export function generateAuthorReference(): string {
return `# Author Reference
Guide for designing and writing workflow YAML definitions.
## Workflow Structure
\`\`\`yaml
name: solve-issue # verb-first kebab-case
description: "..." # human-readable summary
roles: # named actors
planner:
description: "..." # short purpose
goal: "..." # system-level goal for the agent
capabilities: [...] # skill keywords the agent should load
procedure: | # step-by-step instructions
1. Do this
2. Do that
output: "..." # what the agent should produce
frontmatter: # JSON Schema for structured output
oneOf:
- properties:
$status: { const: "ready" }
plan: { type: string }
required: [$status, plan]
- properties:
$status: { const: "failed" }
error: { type: string }
required: [$status, error]
graph: # status-based routing
$START:
_: { role: planner, prompt: "Analyze the issue." }
planner:
ready: { role: developer, prompt: "Implement {{{plan}}}." }
failed: { role: $END, prompt: "Failed: {{{error}}}" }
\`\`\`
## Role Definition
| Field | Purpose |
|-------|---------|
| \`description\` | Short description for humans and moderator context |
| \`goal\` | Injected as the agent's system-level objective |
| \`capabilities\` | Keyword tags — agent loads matching skills before starting |
| \`procedure\` | Step-by-step instructions the agent follows |
| \`output\` | Describes what to produce and which \`$status\` values to use |
| \`frontmatter\` | JSON Schema defining the structured output fields |
### Role Design Principles
- **Single responsibility** — each role does one thing well
- **Minimal context** — don't overload a role with too many steps; split if needed
- **Clear status values** — each status should map to a distinct graph edge
- **Explicit output** — tell the agent exactly what \`$status\` values are valid
## Frontmatter Schema
The \`frontmatter\` field is a standard JSON Schema. It defines the structured fields the agent must output in YAML frontmatter.
### \`$status\` Field
\`$status\` is the only standard field. Its value determines which graph edge the moderator follows. Use \`const\` to constrain each variant:
\`\`\`yaml
frontmatter:
oneOf:
- properties:
$status: { const: "done" }
result: { type: string }
required: [$status, result]
- properties:
$status: { const: "failed" }
error: { type: string }
required: [$status, error]
\`\`\`
### Custom Fields
Add any fields you need for data passing between roles. These are available in edge prompts via Mustache templates.
### Flat Schema (Single Status)
When a role has only one outcome:
\`\`\`yaml
frontmatter:
properties:
$status: { const: "done" }
summary: { type: string }
required: [$status, summary]
\`\`\`
## Graph Routing
The graph maps each role's \`$status\` values to the next role:
\`\`\`
graph[role][$status] → { role: nextRole, prompt: edgePrompt }
\`\`\`
### Special Nodes
| Node | Purpose |
|------|---------|
| \`$START\` | Entry point — status key is always \`_\` (unconditional) |
| \`$END\` | Terminal — thread completes and is archived |
### Edge Prompts
Use triple-brace Mustache (\`{{{field}}}\`) to pass data from the previous step's output:
\`\`\`yaml
graph:
planner:
ready: { role: developer, prompt: "Implement plan {{{plan}}} in {{{repoPath}}}." }
\`\`\`
The fields referenced must exist in the source role's frontmatter schema.
### Loops and Branching
Roles can route back to previous roles (loops) or to different roles based on status (branching):
\`\`\`yaml
graph:
reviewer:
approved: { role: tester, prompt: "Run tests." }
rejected: { role: developer, prompt: "Fix: {{{comments}}}" } # loop back
\`\`\`
### Fail Routing
Route failures to a cleanup role or \`$END\`:
\`\`\`yaml
graph:
developer:
done: { role: reviewer, prompt: "Review changes." }
failed: { role: cleanup, prompt: "Clean up: {{{error}}}" }
\`\`\`
## Self-Testing
### Step-by-Step Verification
\`\`\`bash
# Start a thread directly from YAML file (no registration needed)
uwf thread start my-workflow.yaml -p "Test prompt"
# Or register first, then start by name
uwf workflow add my-workflow.yaml
uwf thread start my-workflow -p "Test prompt"
# Execute one step at a time to verify routing
uwf thread exec <thread-id>
# Inspect step output
uwf step list <thread-id>
uwf step show <step-hash>
# Check the CAS data
uwf cas get <output-hash>
\`\`\`
### Validation Checklist
1. Every \`$status\` value in a role's frontmatter has a matching edge in the graph
2. Every field referenced in edge prompts (\`{{{field}}}\`) exists in the source role's schema
3. Every role referenced in the graph exists in \`roles\`
4. \`$START\` has exactly one edge with key \`_\`
5. At least one path leads to \`$END\`
6. No orphan roles (defined but never routed to)
## Common Pitfalls
- **Missing graph edge** — if a role can produce \`$status: failed\` but the graph has no \`failed\` edge, the moderator will error
- **Mustache field mismatch** — referencing \`{{{branch}}}\` in an edge prompt but the source schema has \`branchName\` instead
- **Overly complex roles** — a role with 20 steps should be split; each role should be completable in one agent turn
- **No fail path** — always handle failure; route to cleanup or \`$END\`
`;
}
@@ -0,0 +1,140 @@
export function generateDeveloperReference(): string {
return `# Developer Reference
Guide for contributing to the workflow engine codebase.
## Monorepo Structure
\`\`\`
packages/
workflow-protocol/ # Shared types (WorkflowPayload, StepNodePayload, etc.)
workflow-util/ # Base32, ULID, logger, frontmatter parsing, skill references
workflow-util-agent/ # createAgent factory, context builder, extract pipeline
workflow-agent-hermes/ # uwf-hermes CLI (spawns Hermes chat sessions)
workflow-agent-builtin/ # uwf-builtin CLI (direct LLM calls via OpenAI API)
cli-workflow/ # uwf CLI (moderator, thread/step/cas/config commands)
\`\`\`
Dependency layers (each only imports from packages above it):
\`\`\`
protocol → util → util-agent → agent-hermes / agent-builtin / cli-workflow
\`\`\`
External CAS: \`@uncaged/json-cas\` (store API, hashing, schema validation) + \`@uncaged/json-cas-fs\` (filesystem backend).
## Coding Conventions
### Functional-first
| Rule | Description |
|------|-------------|
| \`type\` over \`interface\` | All type definitions use \`type\` |
| \`function\` over \`class\` | Pure functions + closures, no class |
| No \`this\` | Functions must not depend on \`this\` context |
| No inheritance | No \`extends\`, \`implements\`, \`abstract\` |
| No optional properties | Use \`T \\| null\` instead of \`?:\` |
| Immutability first | Use \`Readonly<T>\`, \`as const\`, avoid mutation |
Classes allowed only when required by third-party libraries or for Error subclasses.
### Error Handling
- \`Result<T, E>\` type for expected failures (\`ok\`/\`err\` constructors from \`@uncaged/workflow-util\`)
- \`throw\` only for unrecoverable bugs
- No try-catch for flow control
### Async
Always \`async/await\`, never \`.then()\` chains.
### Logging
\`console.*\` is banned (Biome \`noConsole\` rule). Use the structured logger:
\`\`\`typescript
import { createLogger } from "@uncaged/workflow-util";
const log = createLogger();
log("4KNMR2PX", "Loading workflow..."); // 8-char Crockford Base32 tag
\`\`\`
Each call site gets a unique hand-written tag. \`grep "4KNMR2PX"\` in logs → instant code location.
CLI package (\`@uncaged/cli-workflow\`) may use \`console.log\` for user-facing output with a biome-ignore comment.
### No Dynamic Import
No \`await import()\` in production code. Always static top-level \`import\`. Test files are exempt.
### Naming
- Workflow names: verb-first kebab-case (\`solve-issue\`, \`review-code\`)
- IDs: Crockford Base32 — CAS hash (XXH64, 13-char), Thread ID (ULID, 26-char)
## Development Workflow
\`\`\`bash
bun install # install all workspace deps
bun run build # tsc --build (all packages)
bun run check # tsc + biome check + lint-log-tags
bun run format # biome format --write
bun test # run all tests
\`\`\`
Before committing: \`bun run check\` + \`bun test\` must both pass.
### Testing
- \`cli-workflow\`: vitest
- Other packages: \`bun test\`
- Test files live in \`__tests__/\` directories
### Publishing
Fixed-mode versioning — all \`@uncaged/*\` packages share the same version number.
\`\`\`bash
bun changeset # describe the change
bun version # bump versions + changelogs
bun release # build + test + publish to npmjs
\`\`\`
## Key Modules
### Moderator (\`cli-workflow/src/moderator/\`)
Status-based graph evaluator. Reads \`graph[lastRole][output.$status]\` to determine the next role. Zero LLM cost.
### Extract Pipeline (\`workflow-util-agent/src/\`)
1. Agent produces frontmatter markdown
2. \`parseFrontmatterMarkdown()\` extracts YAML frontmatter
3. \`tryFrontmatterFastPath()\` validates against role's output schema
4. If fast path fails, retries up to 2 times via agent continue
5. Validated output stored as CAS node
### createAgent Factory (\`workflow-util-agent/src/run.ts\`)
Shared entry point for all agent CLIs. Handles:
- Argument parsing (\`--thread\`, \`--role\`, \`--prompt\`)
- Context building (thread history, workflow definition)
- Output extraction and CAS persistence
- Frontmatter retry loop
### CAS Integration
All data is CAS-addressed via \`@uncaged/json-cas\`:
- \`store.put(schemaHash, data)\` → content hash
- \`store.get(hash)\` → node
- \`validate(store, node)\` → schema check
- Schemas registered at workflow add time
## Commit Convention
\`\`\`
<type>(<scope>): <description>
type: feat | fix | refactor | docs | chore | test
scope: workflow | cli | moderator | agent-kit | hermes | util | protocol
\`\`\`
`;
}
+4
View File
@@ -1,6 +1,9 @@
export { generateActorReference } from "./actor-reference.js";
export { generateArchitectureReference } from "./architecture-reference.js"; export { generateArchitectureReference } from "./architecture-reference.js";
export { generateAuthorReference } from "./author-reference.js";
export { encodeUint64AsCrockford } from "./base32.js"; export { encodeUint64AsCrockford } from "./base32.js";
export { generateCliReference } from "./cli-reference.js"; export { generateCliReference } from "./cli-reference.js";
export { generateDeveloperReference } from "./developer-reference.js";
export { env } from "./env.js"; export { env } from "./env.js";
export type { export type {
AgentFrontmatter, AgentFrontmatter,
@@ -27,4 +30,5 @@ export { err, ok } from "./result.js";
export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js"; export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js";
export type { LogFn, Result } from "./types.js"; export type { LogFn, Result } from "./types.js";
export { extractUlidTimestamp, generateUlid } from "./ulid.js"; export { extractUlidTimestamp, generateUlid } from "./ulid.js";
export { generateUserReference } from "./user-reference.js";
export { generateYamlReference } from "./yaml-reference.js"; export { generateYamlReference } from "./yaml-reference.js";
@@ -0,0 +1,125 @@
export function generateUserReference(): string {
return `# User Reference
Guide for using the uwf CLI to manage workflows and threads.
## Quick Start
\`\`\`bash
# 1. Configure provider and model
uwf setup
# 2. Register a workflow
uwf workflow add my-workflow.yaml
# 3. Start a thread (creates but does not execute)
uwf thread start my-workflow -p "Build a login page"
# 4. Execute the thread (runs moderator → agent → extract cycles)
uwf thread exec <thread-id> # one step
uwf thread exec <thread-id> -c 10 # up to 10 steps
uwf thread exec <thread-id> -c 10 --background # run in background
\`\`\`
## Concepts
- **Workflow** — YAML definition with roles and a routing graph; stored as a CAS node
- **Thread** — A running instance of a workflow; a chain of step nodes in CAS
- **Step** — One moderator → agent → extract cycle; contains the role's structured output
- **CAS** — Content-addressable store; every artifact is hashed (XXH64, Crockford Base32)
## Setup
\`\`\`
uwf setup # interactive wizard
uwf setup --provider <name> --base-url <url> \\
--api-key <key> --model <name> # non-interactive
[--agent <name>] # optional default agent
\`\`\`
Config is stored at \`~/.uncaged/workflow/config.yaml\`. Override storage root with \`UNCAGED_WORKFLOW_STORAGE_ROOT\`.
## Workflow Commands
\`\`\`
uwf workflow add <file> # register from YAML file
uwf workflow show <id> # show by name or CAS hash
uwf workflow list # list all registered workflows
\`\`\`
You can also pass a file path directly to \`uwf thread start\` without registering first.
## Thread Lifecycle
\`\`\`
uwf thread start <workflow> -p <prompt> # create thread
uwf thread exec <thread-id> # execute one step
[--agent <cmd>] # override agent
[-c, --count <n>] # run n steps
[--background] # run in background
uwf thread show <thread-id> # show head pointer
uwf thread list # list all threads
[--status <filter>] # idle, running, completed, cancelled, active (comma-separated)
[--after <thread-id>] # pagination: after this thread
[--before <thread-id>] # pagination: before this thread
[--skip <n>] # skip first n results
[--take <n>] # limit results
uwf thread read <thread-id> # render context as markdown
[--quota <chars>] # max output chars (default 4000)
[--before <step-hash>] # pagination
[--start] # include start step
uwf thread stop <thread-id> # stop background execution
uwf thread cancel <thread-id> # cancel and archive thread
\`\`\`
### Typical Lifecycle
\`\`\`
start → exec (repeat) → thread reaches $END → auto-completed
→ or: cancel to abort
\`\`\`
## Step Commands
\`\`\`
uwf step list <thread-id> # list all steps
uwf step show <step-hash> # show step details
uwf step fork <step-hash> # fork thread from a step (branch)
\`\`\`
Forking creates a new thread that shares history up to the fork point — useful for retrying from a known-good state.
## CAS Commands
\`\`\`
uwf cas get <hash> # read a node (type + payload)
[--timestamp] # include timestamp
uwf cas put <type-hash> <data> # store typed JSON, print hash
uwf cas put-text <text> # store plain text, print hash
uwf cas has <hash> # check existence
uwf cas refs <hash> # list direct references
uwf cas walk <hash> # recursive traversal
uwf cas reindex # rebuild type index
uwf cas schema list # list schemas
uwf cas schema get <hash> # show schema definition
\`\`\`
## Log Commands
\`\`\`
uwf log list # list log files
uwf log show # show log entries
[--thread <id>] # filter by thread
[--process <pid>] # filter by process
[--date <YYYY-MM-DD>] # filter by date
uwf log clean --before <date> # delete old logs
\`\`\`
## Global Options
\`\`\`
uwf --format <json|yaml> # output format (default: json)
uwf -V, --version # print version
\`\`\`
`;
}