Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09b7ddf6d0 | |||
| c4e94bbe56 | |||
| dbefe793f2 | |||
| 6483bc4861 | |||
| fecb02b115 | |||
| 87938c1886 | |||
| 95a130136b | |||
| aba5642908 | |||
| 168e604602 | |||
| d50159c5a7 | |||
| 9a7ad34e55 | |||
| 4193157124 | |||
| 6ff1414cf0 |
@@ -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
|
||||||
|
|||||||
@@ -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`
|
|
||||||
@@ -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." }
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user