Compare commits

..

1 Commits

Author SHA1 Message Date
xiaoju c0e0e561f3 fix: setup UX improvements — adapter check, ENOENT, SQLite warning, VERSION, PATH docs
CI / check (pull_request) Failing after 2m4s
- setup validates adapter binary availability, prints install command if missing
- setup prints 'Config saved to <path> ✓' on success
- spawn ENOENT gives actionable error with which command
- SQLite ExperimentalWarning suppressed via NODE_OPTIONS
- bootstrap VERSION reads cli package.json (was reading util)
- bootstrap PATH guidance is shell-agnostic

Fixes #114
2026-06-05 15:29:41 +00:00
27 changed files with 155 additions and 286 deletions
-14
View File
@@ -1,14 +0,0 @@
---
"@united-workforce/cli": patch
---
fix: improve bootstrap docs — agent discovery, pnpm/npm parity, preset provider table (#118, #120)
- Step 1: detect installed agents (hermes/claude) before choosing adapter
- Step 1: clarify adapter versions are independent from CLI — install @latest
- Step 1: show pnpm and npm side-by-side
- Step 1: add "adapter must be installed before `uwf setup --agent`" note
- Step 1: add ACP verification step (hermes acp --help)
- Step 2: `--agent` takes adapter command name (e.g. `uwf-hermes`), not npm package
- Step 2: preset providers listed as a table with names and default base URLs
- Remove uwf-builtin from supported adapters (not ready yet)
-14
View File
@@ -1,14 +0,0 @@
---
"@united-workforce/cli": patch
"@united-workforce/agent-hermes": patch
"@united-workforce/agent-claude-code": patch
"@united-workforce/agent-builtin": patch
"@united-workforce/agent-mock": patch
---
fix: suppress ExperimentalWarning, PEP 668 pip guidance, setup help (#116)
- All CLI bins use shebang `#!/usr/bin/env -S node --disable-warning=ExperimentalWarning`
- Remove NODE_OPTIONS injection from spawn (shebang handles it)
- Bootstrap pip install guidance covers venv/pipx/source options for PEP 668 systems
- `uwf setup --help` mentions interactive wizard mode
-15
View File
@@ -1,15 +0,0 @@
---
"@united-workforce/cli": patch
"@united-workforce/util": patch
---
fix: unify $status to const-only, drop enum support (#123)
Breaking: `$status` in frontmatter now requires `const` everywhere.
`enum` is no longer accepted and will be rejected by the validator.
- Validator: `hasStatusConst()` / `getConstStatuses()` replace enum-based checks
- Error message: "must define $status as const (or oneOf with const)"
- workflow-authoring docs: all examples use `const`, enum explicitly noted as unsupported
- bootstrap hello.yaml: `$status: { const: done }`
- All test fixtures migrated from enum to const/oneOf
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@united-workforce/agent-builtin", "name": "@united-workforce/agent-builtin",
"version": "0.1.2", "version": "0.1.1",
"files": [ "files": [
"src", "src",
"dist", "dist",
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning #!/usr/bin/env node
// eslint-disable-next-line -- dynamic import for version // eslint-disable-next-line -- dynamic import for version
const pkg = await import("../package.json", { with: { type: "json" } }); const pkg = await import("../package.json", { with: { type: "json" } });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@united-workforce/agent-claude-code", "name": "@united-workforce/agent-claude-code",
"version": "0.1.2", "version": "0.1.1",
"files": [ "files": [
"src", "src",
"dist", "dist",
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning #!/usr/bin/env node
// eslint-disable-next-line -- dynamic import for version // eslint-disable-next-line -- dynamic import for version
const pkg = await import("../package.json", { with: { type: "json" } }); const pkg = await import("../package.json", { with: { type: "json" } });
@@ -15,8 +15,7 @@ describe("Issue #551 — bin entry & engines", () => {
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8")); const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
const binPath = pkg.bin["uwf-hermes"]; const binPath = pkg.bin["uwf-hermes"];
const content = readFileSync(join(PKG_ROOT, binPath), "utf-8"); const content = readFileSync(join(PKG_ROOT, binPath), "utf-8");
expect(content.startsWith("#!/usr/bin/env")).toBe(true); expect(content.startsWith("#!/usr/bin/env node")).toBe(true);
expect(content).toContain("node");
}); });
test("README.md explains uwf-hermes is an adapter", () => { test("README.md explains uwf-hermes is an adapter", () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@united-workforce/agent-hermes", "name": "@united-workforce/agent-hermes",
"version": "0.1.3", "version": "0.1.2",
"files": [ "files": [
"src", "src",
"dist", "dist",
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning #!/usr/bin/env node
// eslint-disable-next-line -- dynamic import for version // eslint-disable-next-line -- dynamic import for version
const pkg = await import("../package.json", { with: { type: "json" } }); const pkg = await import("../package.json", { with: { type: "json" } });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@united-workforce/agent-mock", "name": "@united-workforce/agent-mock",
"version": "0.1.2", "version": "0.1.1",
"files": [ "files": [
"src", "src",
"dist", "dist",
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning #!/usr/bin/env node
// eslint-disable-next-line -- dynamic import for version // eslint-disable-next-line -- dynamic import for version
const pkg = await import("../package.json", { with: { type: "json" } }); const pkg = await import("../package.json", { with: { type: "json" } });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@united-workforce/cli", "name": "@united-workforce/cli",
"version": "0.3.0", "version": "0.2.0",
"files": [ "files": [
"src", "src",
"dist", "dist",
+10 -18
View File
@@ -28,13 +28,9 @@ roles:
$status: "ready" $status: "ready"
frontmatter: frontmatter:
type: object type: object
oneOf: required: ["$status"]
- properties: properties:
$status: { const: "ready" } $status: { type: string, enum: ["ready", "not-ready"] }
required: ["$status"]
- properties:
$status: { const: "not-ready" }
required: ["$status"]
roleB: roleB:
description: Second role description: Second role
goal: Do B goal: Do B
@@ -46,7 +42,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "done" } $status: { type: string, enum: ["done"] }
graph: graph:
$START: $START:
new: new:
@@ -86,13 +82,9 @@ roles:
$status: "pass" $status: "pass"
frontmatter: frontmatter:
type: object type: object
oneOf: required: ["$status"]
- properties: properties:
$status: { const: "pass" } $status: { type: string, enum: ["pass", "fail"] }
required: ["$status"]
- properties:
$status: { const: "fail" }
required: ["$status"]
roleB: roleB:
description: Pass role description: Pass role
goal: Do B goal: Do B
@@ -104,7 +96,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "done" } $status: { type: string, enum: ["done"] }
roleC: roleC:
description: Fail role description: Fail role
goal: Do C goal: Do C
@@ -116,7 +108,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "done" } $status: { type: string, enum: ["done"] }
graph: graph:
$START: $START:
new: new:
@@ -163,7 +155,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "done" } $status: { type: string, enum: ["done"] }
graph: graph:
$START: $START:
new: new:
@@ -54,7 +54,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "ready" } $status: { type: string, enum: ["ready"] }
graph: graph:
$START: $START:
new: new:
@@ -114,7 +114,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "ready" } $status: { type: string, enum: ["ready"] }
graph: graph:
$START: $START:
new: new:
@@ -161,7 +161,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "ready" } $status: { type: string, enum: ["ready"] }
graph: graph:
$START: $START:
new: new:
@@ -31,7 +31,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "ready" } $status: { type: string, enum: ["ready"] }
graph: graph:
$START: $START:
new: new:
@@ -54,7 +54,7 @@ roles:
type: object type: object
required: ["$status"] required: ["$status"]
properties: properties:
$status: { const: "ready" } $status: { type: string, enum: ["ready"] }
graph: graph:
$START: $START:
new: new:
@@ -17,7 +17,7 @@ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
frontmatter: { frontmatter: {
type: "object", type: "object",
properties: { properties: {
$status: { const: "done" }, $status: { enum: ["done"] },
plan: { type: "string" }, plan: { type: "string" },
}, },
required: ["$status", "plan"], required: ["$status", "plan"],
@@ -85,7 +85,7 @@ describe("Suite 1: Role Reference Integrity", () => {
output: "None", output: "None",
frontmatter: { frontmatter: {
type: "object", type: "object",
properties: { $status: { const: "done" } }, properties: { $status: { enum: ["done"] } },
required: ["$status"], required: ["$status"],
} as unknown as string, } as unknown as string,
}; };
@@ -187,7 +187,7 @@ describe("Suite 2: Graph Structure", () => {
output: "Isolated", output: "Isolated",
frontmatter: { frontmatter: {
type: "object", type: "object",
properties: { $status: { const: "done" } }, properties: { $status: { enum: ["done"] } },
required: ["$status"], required: ["$status"],
} as unknown as string, } as unknown as string,
}; };
@@ -272,8 +272,8 @@ describe("Suite 3: Status-Edge Consistency", () => {
}); });
}); });
describe("Suite 3b: Enum-Based $status is Rejected", () => { describe("Suite 3b: Enum-Based Multi-Exit", () => {
test("3b.1 enum multi-exit is rejected (must use oneOf + const)", () => { test("3b.1 enum multi-exit passes with matching graph keys", () => {
const wf = makeWorkflow(); const wf = makeWorkflow();
wf.roles.reviewer = { wf.roles.reviewer = {
...wf.roles.reviewer, ...wf.roles.reviewer,
@@ -291,10 +291,52 @@ describe("Suite 3b: Enum-Based $status is Rejected", () => {
rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null }, rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
}; };
const errors = validateWorkflow(wf); const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true); expect(errors).toEqual([]);
}); });
test("3b.2 enum single-exit is rejected (must use const)", () => { test("3b.2 enum multi-exit with extra graph key", () => {
const wf = makeWorkflow();
wf.roles.reviewer = {
...wf.roles.reviewer,
frontmatter: {
type: "object",
properties: {
$status: { enum: ["approved", "rejected"] },
comments: { type: "string" },
},
required: ["$status", "comments"],
} as unknown as string,
};
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done", location: null },
rejected: { role: "writer", prompt: "Fix", location: null },
timeout: { role: "$END", prompt: "Timed out", location: null },
};
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("extra status keys: timeout"))).toBe(true);
});
test("3b.3 enum multi-exit with missing graph key", () => {
const wf = makeWorkflow();
wf.roles.reviewer = {
...wf.roles.reviewer,
frontmatter: {
type: "object",
properties: {
$status: { enum: ["approved", "rejected"] },
comments: { type: "string" },
},
required: ["$status", "comments"],
} as unknown as string,
};
wf.graph.reviewer = {
approved: { role: "$END", prompt: "Done", location: null },
};
const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("missing status keys: rejected"))).toBe(true);
});
test("3b.4 enum with single explicit value passes", () => {
const wf = makeWorkflow(); const wf = makeWorkflow();
wf.roles.writer = { wf.roles.writer = {
...wf.roles.writer, ...wf.roles.writer,
@@ -309,71 +351,28 @@ describe("Suite 3b: Enum-Based $status is Rejected", () => {
}; };
wf.graph.writer = { ready: { role: "reviewer", prompt: "Review: {{{plan}}}", location: null } }; wf.graph.writer = { ready: { role: "reviewer", prompt: "Review: {{{plan}}}", location: null } };
const errors = validateWorkflow(wf); const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true);
});
});
describe("Suite 3c: Const-Based Flat Schema", () => {
test("3c.1 flat schema with const $status passes validation", () => {
const wf = makeWorkflow();
wf.roles.writer = {
...wf.roles.writer,
frontmatter: {
type: "object",
properties: {
$status: { const: "done" },
plan: { type: "string" },
},
required: ["$status", "plan"],
} as unknown as string,
};
const errors = validateWorkflow(wf);
expect(errors).toEqual([]); expect(errors).toEqual([]);
}); });
test("3c.2 flat schema with const $status detects extra graph key", () => { test("3b.5 enum multi-exit mustache var not in frontmatter", () => {
const wf = makeWorkflow(); const wf = makeWorkflow();
wf.roles.writer = { wf.roles.reviewer = {
...wf.roles.writer, ...wf.roles.reviewer,
frontmatter: { frontmatter: {
type: "object", type: "object",
properties: { properties: {
$status: { const: "done" }, $status: { enum: ["approved", "rejected"] },
plan: { type: "string" }, comments: { type: "string" },
}, },
required: ["$status", "plan"], required: ["$status", "comments"],
} as unknown as string, } as unknown as string,
}; };
wf.graph.writer = { wf.graph.reviewer = {
done: { role: "reviewer", prompt: "Review.", location: null }, approved: { role: "$END", prompt: "Done: {{{nonexistent}}}", location: null },
extra: { role: "$END", prompt: "Nope.", location: null }, rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null },
}; };
const errors = validateWorkflow(wf); const errors = validateWorkflow(wf);
expect(errors.some((e) => e.includes("extra status keys") && e.includes("extra"))).toBe(true); expect(errors.some((e) => e.includes("nonexistent") && e.includes("not found"))).toBe(true);
});
test("3c.3 flat schema with const $status validates mustache vars", () => {
const wf = makeWorkflow();
wf.roles.writer = {
...wf.roles.writer,
frontmatter: {
type: "object",
properties: {
$status: { const: "done" },
plan: { type: "string" },
},
required: ["$status", "plan"],
} as unknown as string,
};
wf.graph.writer = {
done: { role: "reviewer", prompt: "Review: {{{nonexistent}}}", location: null },
};
const errors = validateWorkflow(wf);
expect(
errors.some(
(e) => e.includes('prompt variable "nonexistent"') && e.includes('role "writer"'),
),
).toBe(true);
}); });
}); });
@@ -481,7 +480,7 @@ describe("Suite 6: Multiple Errors Collection", () => {
output: "None", output: "None",
frontmatter: { frontmatter: {
type: "object", type: "object",
properties: { $status: { const: "done" } }, properties: { $status: { enum: ["done"] } },
required: ["$status"], required: ["$status"],
} as unknown as string, } as unknown as string,
}; };
@@ -31,7 +31,7 @@ function makeMinimalPayload(name: string, description: string): WorkflowPayload
frontmatter: { frontmatter: {
type: "object", type: "object",
properties: { properties: {
$status: { const: "done" }, $status: { type: "string", enum: ["done"] },
}, },
required: ["$status"], required: ["$status"],
} as unknown as CasRef, } as unknown as CasRef,
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning #!/usr/bin/env node
import type { CasRef, ThreadId, ThreadStatus } from "@united-workforce/protocol"; import type { CasRef, ThreadId, ThreadStatus } from "@united-workforce/protocol";
import { Command } from "commander"; import { Command } from "commander";
@@ -542,7 +542,7 @@ prompt
program program
.command("setup") .command("setup")
.description("Configure provider, model, and agent. Run without options for interactive wizard.") .description("Configure provider, model, and agent")
.option("--provider <name>", "Provider name") .option("--provider <name>", "Provider name")
.option("--base-url <url>", "OpenAI-compatible API base URL") .option("--base-url <url>", "OpenAI-compatible API base URL")
.option("--api-key <key>", "API key") .option("--api-key <key>", "API key")
+25 -114
View File
@@ -14,10 +14,7 @@ function _findCliVersion(): string {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const candidate = join(dir, "package.json"); const candidate = join(dir, "package.json");
try { try {
const pkg = JSON.parse(readFileSync(candidate, "utf-8")) as { const pkg = JSON.parse(readFileSync(candidate, "utf-8")) as { name?: string; version?: string };
name?: string;
version?: string;
};
if (pkg.name === "@united-workforce/cli") { if (pkg.name === "@united-workforce/cli") {
return pkg.version ?? "0.0.0"; return pkg.version ?? "0.0.0";
} }
@@ -78,105 +75,48 @@ npm prefix -g # global prefix; bin is <prefix>/bin
# FIX: add the missing directory to your shell's startup file # FIX: add the missing directory to your shell's startup file
# (e.g. ~/.bashrc, ~/.zshrc, ~/.profile, or fish config): # (e.g. ~/.bashrc, ~/.zshrc, ~/.profile, or fish config):
# export PATH="<global-bin-dir>:$PATH" # export PATH="<global-bin-dir>:$PATH"
# 4. (uwf-hermes only) hermes CLI
which hermes
# FIX: if hermes is in a virtualenv, add it to PATH:
# export PATH="$HOME/.hermes/hermes-agent/.venv/bin:$PATH"
# or create a symlink: ln -s ~/.hermes/hermes-agent/.venv/bin/hermes ~/.local/bin/hermes
\`\`\` \`\`\`
**All checks must pass before continuing.** If you had to modify PATH, verify the change persists by opening a new shell or sourcing your shell config. **All checks must pass before continuing.** If you had to modify PATH, verify the change persists by opening a new shell or sourcing your shell config.
### Step 1 Discover agents and install adapter ### Step 1 Install CLI and agent adapter
**First, detect which supported agents are already installed on the user's machine:**
\`\`\`bash \`\`\`bash
# Check for Hermes Agent pnpm add -g @united-workforce/cli # or: npm install -g @united-workforce/cli
which hermes 2>/dev/null && hermes --version uwf --version # should print ${CLI_VERSION}
# Check for Claude Code
which claude 2>/dev/null && claude --version # should show "X.Y.Z (Claude Code)"
\`\`\` \`\`\`
**Based on the results:** Install an agent adapter (at least one is required):
- **Only hermes found** install \`uwf-hermes\` adapter | Adapter | Install | When to use |
- **Only claude found** install \`uwf-claude-code\` adapter |---------|---------|-------------|
- **Both found** ask the user which agent they want uwf to use as default | uwf-hermes | \`pnpm add -g @united-workforce/agent-hermes\` | When your agent framework is Hermes Agent |
- **Neither found** the user must install at least one agent first: | uwf-claude-code | \`pnpm add -g @united-workforce/agent-claude-code\` | When using Claude Code CLI directly |
- Hermes Agent: https://hermes-agent.nousresearch.com/docs | uwf-builtin | \`pnpm add -g @united-workforce/agent-builtin\` | Lightweight built-in agent (no external dependency) |
- Claude Code: \`npm install -g @anthropic-ai/claude-code\`
**Install the uwf CLI and the chosen adapter** using pnpm or npm:
**uwf-hermes** also requires the Hermes ACP plugin. After installing \`hermes-agent\`, run:
\`\`\`bash \`\`\`bash
# CLI (required) pip install hermes-agent[acp] # or: pip install -e .[acp] if installed from source
pnpm add -g @united-workforce/cli # or: npm install -g @united-workforce/cli
# Adapter install the one matching the detected agent:
pnpm add -g @united-workforce/agent-hermes # or: npm i -g @united-workforce/agent-hermes
pnpm add -g @united-workforce/agent-claude-code # or: npm i -g @united-workforce/agent-claude-code
\`\`\` \`\`\`
** Adapter versions are independent from CLI versions.** Do NOT try to match adapter version to CLI version. Just install \`@latest\` (the default). Verify the adapter is installed: \`uwf-hermes --version\` (or whichever you chose).
**After installing, verify that \`uwf\` and the adapter are available in PATH:**
\`\`\`bash
uwf --version # should print ${CLI_VERSION}
uwf-hermes --version # or: uwf-claude-code --version
\`\`\`
If either command is not found, the global bin directory is not in the current shell's PATH. **You must fix this before continuing:**
1. Find where the binary was installed:
\`\`\`bash
find ~/.local ~/.hermes /usr/local -name uwf -type f 2>/dev/null
npm prefix -g # global prefix bin is <prefix>/bin
\`\`\`
2. Add the directory to PATH permanently by appending to the user's shell config (e.g. \`~/.bashrc\`, \`~/.zshrc\`, \`~/.profile\`, or fish config):
\`\`\`bash
export PATH="<global-bin-dir>:$PATH"
\`\`\`
3. Source the updated config or open a new shell, then re-verify the commands work.
**uwf-hermes** also requires the Hermes ACP plugin. Verify with \`hermes acp --help\`. If not available, install it:
\`\`\`bash
# Option A: install into hermes venv (recommended)
source ~/.hermes/hermes-agent/.venv/bin/activate && pip install hermes-agent[acp]
# Option B: pipx
pipx install 'hermes-agent[acp]'
# Option C: if installed from source
pip install -e '.[acp]'
\`\`\`
### Step 2 Configure provider and model ### Step 2 Configure provider and model
uwf needs an LLM provider to run agents. **Ask the user** for their provider, API key, and model, then run: uwf needs an LLM provider to run agents. **Ask the user** for their provider, API key, and model, then run:
\`\`\`bash \`\`\`bash
uwf setup --provider <name> --api-key <key> --model <model> --agent <adapter-command> uwf setup --provider <name> --base-url <url> --api-key <key> --model <model> [--agent <adapter>]
\`\`\` \`\`\`
**Note:** \`--agent\` takes the adapter **command name** (e.g. \`uwf-hermes\`), not the npm package name. Preset providers (base-url is auto-filled when using a preset name):
openai, xai, openrouter, venice, dashscope, deepseek, siliconflow, volcengine, kimi, glm, stepfun, minimax, ollama
**Preset providers** when using a preset name, \`--base-url\` is auto-filled and can be omitted:
| Provider | Name | Default base URL |
|----------|------|-----------------|
| OpenAI | \`openai\` | https://api.openai.com/v1 |
| xAI | \`xai\` | https://api.x.ai/v1 |
| OpenRouter | \`openrouter\` | https://openrouter.ai/api/v1 |
| Venice | \`venice\` | https://api.venice.ai/api/v1 |
| Dashscope | \`dashscope\` | https://dashscope.aliyuncs.com/compatible-mode/v1 |
| DeepSeek | \`deepseek\` | https://api.deepseek.com/v1 |
| SiliconFlow | \`siliconflow\` | https://api.siliconflow.cn/v1 |
| VolcEngine | \`volcengine\` | https://ark.cn-beijing.volces.com/api/v3 |
| Kimi (Moonshot) | \`kimi\` | https://api.moonshot.cn/v1 |
| GLM (Zhipu AI) | \`glm\` | https://open.bigmodel.cn/api/paas/v4 |
| StepFun | \`stepfun\` | https://api.stepfun.com/v1 |
| MiniMax | \`minimax\` | https://api.minimax.io/v1 |
| Ollama (local) | \`ollama\` | http://localhost:11434/v1 |
For **non-preset providers**, you must specify \`--base-url\` manually.
Example: Example:
\`\`\`bash \`\`\`bash
@@ -201,8 +141,6 @@ Each command outputs a complete SKILL.md with YAML frontmatter. Use your agent f
Verify skills are installed by listing them (e.g. \`skills_list()\`) and confirming all three appear. Verify skills are installed by listing them (e.g. \`skills_list()\`) and confirming all three appear.
** After saving all skills, start a new session** so the agent loads the updated skill content. Skills saved in the current session are not active until the next session.
### Step 4 Verify end-to-end ### Step 4 Verify end-to-end
Create a minimal workflow file to test your setup: Create a minimal workflow file to test your setup:
@@ -221,7 +159,7 @@ roles:
frontmatter: frontmatter:
type: object type: object
properties: properties:
$status: { const: done } $status: { enum: [done] }
message: { type: string } message: { type: string }
required: [$status, message] required: [$status, message]
graph: graph:
@@ -248,25 +186,11 @@ If the thread reaches \`$END\` with status \`completed\`, the setup is working.
### Step 1 Update packages ### Step 1 Update packages
\`\`\`bash \`\`\`bash
# Using pnpm pnpm add -g @united-workforce/cli@latest # or: npm install -g @united-workforce/cli@latest
pnpm add -g @united-workforce/cli@latest
# Using npm
npm install -g @united-workforce/cli@latest
\`\`\`
\`\`\`bash
uwf --version # should print ${CLI_VERSION} uwf --version # should print ${CLI_VERSION}
\`\`\`
Also update your adapter(s): # Also update your adapter(s)
\`\`\`bash
# pnpm
pnpm add -g @united-workforce/agent-hermes@latest pnpm add -g @united-workforce/agent-hermes@latest
# npm
npm install -g @united-workforce/agent-hermes@latest
\`\`\` \`\`\`
### Step 2 Regenerate skills ### Step 2 Regenerate skills
@@ -279,8 +203,6 @@ uwf prompt workflow-authoring # → update skill "uwf-workflow-authoring"
uwf prompt adapter-developing # update skill "uwf-adapter-developing" uwf prompt adapter-developing # update skill "uwf-adapter-developing"
\`\`\` \`\`\`
** After updating skills, start a new session** to load the new skill content.
### Step 3 Migrate workflow YAML files (if needed) ### Step 3 Migrate workflow YAML files (if needed)
Check the changelog for breaking changes. Known migrations: Check the changelog for breaking changes. Known migrations:
@@ -299,17 +221,6 @@ Check the changelog for breaking changes. Known migrations:
Update all \`.workflow/\` and \`.workflows/\` YAML files in your projects. \`uwf workflow add\` will reject files with the old \`_\` syntax. Update all \`.workflow/\` and \`.workflows/\` YAML files in your projects. \`uwf workflow add\` will reject files with the old \`_\` syntax.
- **v0.2.1**: \`$status: { enum: [value] }\`\`$status: { const: "value" }\`. The validator no longer accepts \`enum\` for \`$status\`. Update all workflow YAML files:
\`\`\`yaml
# Before (v0.2.0)
$status: { enum: [done] }
$status: { type: string, enum: ["ready", "failed"] }
# After (v0.2.1+)
$status: { const: "done" }
# For multi-exit, use oneOf with const (unchanged)
\`\`\`
### Step 4 Verify ### Step 4 Verify
\`\`\`bash \`\`\`bash
+2
View File
@@ -443,6 +443,7 @@ export async function cmdSetup(args: SetupArgs): Promise<Record<string, unknown>
writeFileSync(configPath, stringify(merged, { indent: 2 }), "utf8"); writeFileSync(configPath, stringify(merged, { indent: 2 }), "utf8");
// Print config path to stderr (stdout is reserved for JSON output) // Print config path to stderr (stdout is reserved for JSON output)
// biome-ignore lint/nursery/noConsole: CLI user-facing output
console.error(`Config saved to ${configPath}`); console.error(`Config saved to ${configPath}`);
// Validate model connectivity // Validate model connectivity
@@ -452,6 +453,7 @@ export async function cmdSetup(args: SetupArgs): Promise<Record<string, unknown>
const agentName = _agentNameFromBinary(args.agent ?? "hermes"); const agentName = _agentNameFromBinary(args.agent ?? "hermes");
const adapterWarnings = _checkAdapterAvailability(agentName); const adapterWarnings = _checkAdapterAvailability(agentName);
for (const w of adapterWarnings) { for (const w of adapterWarnings) {
// biome-ignore lint/nursery/noConsole: CLI user-facing output
console.error(`${w}`); console.error(`${w}`);
} }
+12
View File
@@ -1001,6 +1001,12 @@ function spawnAgent(
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 50 * 1024 * 1024, // 50 MB — stream-json output can be large maxBuffer: 50 * 1024 * 1024, // 50 MB — stream-json output can be large
cwd, cwd,
env: {
...process.env,
NODE_OPTIONS: [process.env.NODE_OPTIONS, "--disable-warning=ExperimentalWarning"]
.filter(Boolean)
.join(" "),
},
}); });
} catch (e) { } catch (e) {
const err = e as NodeJS.ErrnoException & { stderr?: Buffer | string | null }; const err = e as NodeJS.ErrnoException & { stderr?: Buffer | string | null };
@@ -1248,6 +1254,12 @@ async function cmdThreadStepBackground(
const child = spawn(scriptPath, args, { const child = spawn(scriptPath, args, {
detached: true, detached: true,
stdio: "ignore", stdio: "ignore",
env: {
...process.env,
NODE_OPTIONS: [process.env.NODE_OPTIONS, "--disable-warning=ExperimentalWarning"]
.filter(Boolean)
.join(" "),
},
}); });
child.unref(); child.unref();
+13 -13
View File
@@ -24,22 +24,22 @@ function isOneOfSchema(fm: unknown): fm is SchemaObj & { oneOf: SchemaObj[] } {
return Array.isArray(obj.oneOf); return Array.isArray(obj.oneOf);
} }
/** Check if a frontmatter schema declares "$status" as const (flat schema form). */ /** Check if a frontmatter schema declares "$status" as an enum (the required form for user roles). */
function hasStatusConst(fm: unknown): boolean { function hasStatusEnum(fm: unknown): boolean {
if (typeof fm !== "object" || fm === null) return false; if (typeof fm !== "object" || fm === null) return false;
const obj = fm as SchemaObj; const obj = fm as SchemaObj;
const props = obj.properties as Record<string, SchemaObj> | undefined; const props = obj.properties as Record<string, SchemaObj> | undefined;
if (!props?.$status) return false; if (!props?.$status) return false;
return typeof props.$status.const === "string"; return Array.isArray(props.$status.enum);
} }
/** Extract status values from a const-based $status field. */ /** Extract status values from an enum-based $status field. */
function getConstStatuses(fm: SchemaObj): string[] { function getEnumStatuses(fm: SchemaObj): string[] {
const props = fm.properties as Record<string, SchemaObj> | undefined; const props = fm.properties as Record<string, SchemaObj> | undefined;
if (!props?.$status) return []; if (!props?.$status) return [];
const statusDef = props.$status; const statusDef = props.$status;
if (typeof statusDef.const === "string") return [statusDef.const]; if (!Array.isArray(statusDef.enum)) return [];
return []; return statusDef.enum as string[];
} }
/** Get property names from a schema object. */ /** Get property names from a schema object. */
@@ -248,21 +248,21 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void
checkOneOfDiscriminant(roleName, variants, statuses, errors); checkOneOfDiscriminant(roleName, variants, statuses, errors);
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors); checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
checkMultiExitMustache(roleName, graphEntry, variants, errors); checkMultiExitMustache(roleName, graphEntry, variants, errors);
} else if (hasStatusConst(fm)) { } else if (hasStatusEnum(fm)) {
const statuses = getConstStatuses(fm as SchemaObj); const statuses = getEnumStatuses(fm as SchemaObj);
checkStatusEdges(roleName, graphKeys, new Set(statuses), errors); checkStatusEdges(roleName, graphKeys, new Set(statuses), errors);
// For const-based flat schemas, mustache vars come from the flat properties // For enum-based schemas, mustache vars come from the flat properties
checkFlatMustache(roleName, graphEntry, fm as SchemaObj, errors); checkEnumMustache(roleName, graphEntry, fm as SchemaObj, errors);
} else { } else {
errors.push( errors.push(
`role "${roleName}" must define "$status" as const (or oneOf with const) in frontmatter`, `role "${roleName}" must define "$status" as an enum (or oneOf const) in frontmatter`,
); );
} }
} }
} }
/** Check mustache vars in all edge prompts against flat schema properties. */ /** Check mustache vars in all edge prompts against flat schema properties. */
function checkFlatMustache( function checkEnumMustache(
roleName: string, roleName: string,
graphEntry: Record<string, { role: string; prompt: string }>, graphEntry: Record<string, { role: string; prompt: string }>,
fm: SchemaObj, fm: SchemaObj,
@@ -143,7 +143,7 @@ describe("buildOutputFormatInstruction", () => {
{ {
type: "object", type: "object",
properties: { properties: {
$status: { const: "approved" }, $status: { type: "string", enum: ["approved"] },
branch: { type: "string" }, branch: { type: "string" },
}, },
required: ["$status"], required: ["$status"],
@@ -151,7 +151,7 @@ describe("buildOutputFormatInstruction", () => {
{ {
type: "object", type: "object",
properties: { properties: {
$status: { const: "rejected" }, $status: { type: "string", enum: ["rejected"] },
comments: { type: "string" }, comments: { type: "string" },
}, },
required: ["$status"], required: ["$status"],
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@united-workforce/util", "name": "@united-workforce/util",
"version": "0.1.3", "version": "0.1.2",
"files": [ "files": [
"src", "src",
"dist", "dist",
@@ -28,7 +28,6 @@ roles: # named actors
2. Do that 2. Do that
output: "..." # what the agent should produce output: "..." # what the agent should produce
frontmatter: # JSON Schema for structured output frontmatter: # JSON Schema for structured output
type: object
oneOf: oneOf:
- properties: - properties:
$status: { const: "ready" } $status: { const: "ready" }
@@ -72,13 +71,10 @@ The \`frontmatter\` field is a standard JSON Schema. It defines the structured f
### \`$status\` Field ### \`$status\` Field
\`$status\` is the only standard field. Its value determines which graph edge the moderator follows. \`$status\` is the only standard field. Its value determines which graph edge the moderator follows. Use \`const\` to constrain each variant:
**Multi-exit (oneOf)** use \`const\` to constrain each variant:
\`\`\`yaml \`\`\`yaml
frontmatter: frontmatter:
type: object
oneOf: oneOf:
- properties: - properties:
$status: { const: "done" } $status: { const: "done" }
@@ -90,25 +86,26 @@ frontmatter:
required: [$status, error] required: [$status, error]
\`\`\` \`\`\`
**Single-exit (flat schema)** same syntax, just no \`oneOf\` wrapper: ### 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, use \`enum\` with a single value:
\`\`\`yaml \`\`\`yaml
frontmatter: frontmatter:
type: object type: object
properties: properties:
$status: { const: "done" } $status:
type: string
enum: [done]
summary: { type: string } summary: { type: string }
required: [$status, summary] required: [$status, summary]
\`\`\` \`\`\`
**Important rules:** Note: \`$status: { const: "done" }\` is **not** valid in flat schemas — the validator requires \`enum\` or \`oneOf\` with \`const\`. Use \`const\` only inside \`oneOf\` variants.
- \`type: object\` is **required** at the top level of frontmatter (both flat and oneOf)
- \`$status\` always uses \`const: "value"\` — simple and consistent
- \`enum\` is **not supported** for \`$status\` — the validator will reject it
### Custom Fields
Add any fields you need for data passing between roles. These are available in edge prompts via Mustache templates.
## Graph Routing ## Graph Routing