Compare commits

..

52 Commits

Author SHA1 Message Date
xiaoju 76fab22827 fix: explicitly forbid extra frontmatter fields in output format instruction
buildOutputFormatInstruction now includes explicit language telling agents to
output ONLY schema-defined fields and to focus on their role's deliverable.

Fixes #394
2026-05-22 10:49:04 +00:00
xiaomo 669875fb46 Merge pull request 'feat: validate model connectivity during uwf setup' (#392) from feat/335-setup-validate-model into main 2026-05-22 10:32:01 +00:00
xiaoju 6d94be34a9 feat: validate model connectivity during uwf setup
Send a test completion request after configuration to verify the model
is reachable. If validation fails, warn the user and suggest trying a
different model or checking their settings.

Fixes #335
2026-05-22 10:30:39 +00:00
xiaomo d95fe45a3d Merge pull request 'feat: add --count/-c flag to uwf thread step' (#390) from feat/373-thread-step-count into main 2026-05-22 10:11:13 +00:00
xiaoju b9252b5ce2 fix: dynamic frontmatter instruction from role schema (closes #389) 2026-05-22 10:03:56 +00:00
xiaoju 4d47effd39 fix: generate frontmatter instruction dynamically from role schema
Replace hardcoded 5-field example with schema-driven generation.
Now shows actual enum values, types, and required markers for
each role's frontmatter schema.

Fixes #389

小橘 <xiaoju@shazhou.work>
2026-05-22 10:03:45 +00:00
xiaoju 7b93ce8f3e fix: dynamic frontmatter field extraction from role schema (closes #388) 2026-05-22 09:57:45 +00:00
xiaoju 67870392ab fix: dynamic frontmatter field extraction from role schema
Replace hardcoded 5-field candidate with schema-driven extraction.
Now reads outputSchema properties and picks matching fields from
parsed frontmatter, supporting role-specific fields like plan,
approved, success.

Falls back to standard 5 fields when schema has no properties.

Fixes #388

小橘 <xiaoju@shazhou.work>
2026-05-22 09:57:30 +00:00
xiaomo 6b9ff9781d Merge pull request 'fix: revert unnecessary output protocol changes from #385' (#386) from fix/385-revert-output-protocol into main 2026-05-22 09:40:33 +00:00
xiaoju 487c48effa fix: revert output protocol changes from #385
Agent CLI outputs plain CAS hash (not JSON), engine parses plain hash.
StepOutput no longer carries sessionId — session info is already in CAS detail.
Keeps the valuable parts of #385: sessionId in AgentRunResult (process-internal),
continue support, and frontmatter retry loop.
2026-05-22 09:39:36 +00:00
xiaomo 4eca2d533c Merge pull request 'feat: agent session protocol — sessionId, continue, frontmatter retry' (#385) from feat/384-agent-session-protocol into main 2026-05-22 09:20:35 +00:00
xiaoju f0f840e6e0 fix: StepOutput.sessionId → string | null, legacy fallback → null 2026-05-22 09:16:13 +00:00
xiaoju 7ff90cef4f feat: agent session protocol — sessionId in result, continue support, frontmatter retry
Breaking changes:
- AgentRunResult now requires sessionId field
- AgentOptions now requires continue function
- Agent CLI outputs JSON {stepHash, sessionId} instead of plain CAS hash
- Engine parses JSON output (with legacy CAS hash fallback)

New features:
- Frontmatter validation retry: if agent output lacks valid frontmatter,
  engine calls agent.continue() up to 2 times with correction message
- Session tracking: sessionId flows from agent → engine → StepOutput
- Hermes agent: session parse failure is now a hard error (no raw text fallback)
- Hermes agent: supports --resume for continue sessions

Closes #384
2026-05-22 09:13:05 +00:00
xiaoju e62d51d845 Merge remote-tracking branch 'origin/feat/remove-llm-extract' into feat/384-agent-session-protocol 2026-05-22 09:06:24 +00:00
xiaoju a803fcb4fc fix: solve-issue.yaml meta.plan → frontmatter.plan
Follows #375 rename.
2026-05-22 09:04:34 +00:00
xiaomo d00c93fc19 Merge pull request 'feat: uwf cas put-text for storing plain text in CAS' (#382) from feat/cas-put-text into main 2026-05-22 09:02:09 +00:00
xiaoju 99a2890be2 feat: remove LLM extract fallback, require YAML frontmatter
Agent output must contain valid YAML frontmatter matching the role schema.
If frontmatter parsing fails, the step fails immediately with a clear error
instead of falling back to an LLM extraction that can fabricate values.

The extract module remains as a public API export but is no longer used
in the agent run loop.

Breaking change: agents that relied on LLM extraction to produce valid
output will now fail. They must output proper frontmatter.
2026-05-22 08:58:01 +00:00
xiaoju 3b7d0564bb feat: uwf cas put-text for storing plain text in CAS
- Register built-in text schema ({type: 'string'}) alongside workflow schemas
- Add cmdCasPutText command: uwf cas put-text <text>
- Update CLI reference in workflow-util
- Update solve-issue.yaml procedure to use put-text

Refs #380
2026-05-22 08:53:27 +00:00
xiaoju 45dacf540b feat: thread step --count/-c <number> to run multiple steps
Add --count/-c flag to 'uwf thread step' for running N steps in one
invocation, stopping early if $END is reached.

- cmdThreadStep now loops up to count times, delegates to cmdThreadStepOnce
- CLI parses -c/--count, defaults to 1 (backward compatible single output)
- Validation rejects 0, negative, and non-integer counts
- 7 new tests covering CLI parsing and count validation

Fixes #373

Co-authored-by: uwf-hermes (solve-issue workflow)
2026-05-22 08:06:26 +00:00
xiaomo 2eb5ee0666 Merge pull request 'fix: accept omitted condition in fallback transitions' (#378) from fix/fallback-transition-validation into main 2026-05-22 07:56:18 +00:00
xiaoju e67932c83c fix: accept omitted condition in fallback transitions
Fallback transitions (last entry in graph node) omit the condition
field in YAML, resulting in undefined instead of null. The validator
and materializer now handle this:

- validate.ts: accept undefined as valid condition value
- workflow.ts: normalizeGraph() coerces undefined → null before CAS put

This was broken by the graph fallback pattern introduced in #370.
2026-05-22 07:38:24 +00:00
xiaomo 04a12231c3 Merge pull request 'feat: register $first/$last JSONata functions in moderator' (#377) from feat/376-first-last-jsonata into main 2026-05-22 07:32:17 +00:00
xiaoju e5ae9a134c feat: register $first/$last JSONata functions in moderator
Register custom $first(role) and $last(role) functions in the JSONata
evaluator. These search the steps array and return the matching role's
frontmatter (output) directly, replacing verbose steps[-1].output.x
expressions with semantic $last('role').field syntax.

- workflow-moderator: register functions via expr.registerFunction()
- Updated all condition expressions in .workflows/ and examples/
- Added tests for $last, $first, and unmatched role (undefined)

Fixes #376
2026-05-22 06:29:56 +00:00
xiaomo bdafaf3aa1 Merge pull request 'refactor!: rename RoleDefinition.meta → frontmatter' (#375) from refactor/374-meta-to-frontmatter into main 2026-05-22 06:06:06 +00:00
xiaoju 02f7f0b708 refactor!: rename RoleDefinition.meta → frontmatter
BREAKING CHANGE: All workflow YAML files must use 'frontmatter' instead of 'meta'.

- workflow-protocol: RoleDefinition.meta → frontmatter, schema updated
- cli-workflow: validate.ts, workflow.ts — resolveMetaRef → resolveFrontmatterRef
- workflow-agent-kit: run.ts — metaSchema → frontmatterSchema
- All YAML files updated (examples/, .workflows/)

Fixes #374
2026-05-22 06:05:07 +00:00
xiaoju 8ea554bb5e Merge pull request 'feat: create .workflows/solve-issue.yaml' (#372) from feat/370-solve-issue-workflow into main 2026-05-22 06:02:15 +00:00
xiaoju 8a425521da fix: output instructions now specify required frontmatter meta fields 2026-05-22 05:42:17 +00:00
xiaoju f174f2fd0a fix: remove redundant condition null from $START 2026-05-22 05:33:39 +00:00
xiaoju 355594d074 refactor: graph fallback pattern + positive condition names
- Last transition in each graph node is now the fallback (no condition)
- Remove redundant positive conditions (ready, devDone, approved, passed, pushSuccess)
- notApproved → rejected (positive naming)
2026-05-22 05:31:43 +00:00
xiaoju fd7609fe90 fix: address review feedback from xingyue
1. npm/npx → bun/bunx (project standard)
2. Fix tea CLI usage (tea comment + -r flag)
3. cursor-agent → coding (abstract capability)
4. Clarify committer inherits developer's worktree
5. Mark meta.plan required when status=ready
6. PR description must follow What/Why/Changes/Ref template
7. Note maxRounds loop protection in description
2026-05-22 05:27:21 +00:00
xiaoju dacecfbbb7 feat: create .workflows/solve-issue.yaml
TDD-driven issue resolution workflow with 5 roles:
- planner: analyzes issue, outputs TDD test spec (stored in CAS)
- developer: implements code following TDD
- reviewer: code standards compliance check (not functionality)
- tester: functional correctness verification
- committer: commits and creates PR

Graph handles bounce-backs: reviewer→developer, tester→developer,
tester→planner (fix_spec), committer→developer (hook_failed).

Refs #370
2026-05-22 05:21:19 +00:00
xiaomo 3238eaeddf Merge pull request 'feat: add uwf skill cli command and Prepare section' (#371) from feat/369-uwf-skill-cli into main 2026-05-22 04:50:12 +00:00
xiaoju 995f273fa5 address review: move CLI reference to workflow-util, inline in prompt
- Move generateCliReference() to @uncaged/workflow-util
- buildRolePrompt inlines CLI reference directly (no agent tool call)
- Fix Role terminology to use new field names
- Add maintenance comment in cli-reference.ts
- Fix test assertions
2026-05-22 03:29:01 +00:00
xiaoju 866154ad73 feat: add uwf skill cli command and Prepare section in role prompt
- Add 'uwf skill cli' command that prints markdown CLI reference
- buildRolePrompt now generates ## Prepare section:
  - Always prompts agent to run 'uwf skill cli' (explicit skill)
  - Renders capabilities as keyword hints for implicit skill loading

Fixes #369
2026-05-22 03:20:04 +00:00
xiaomo 8efc5050cb Merge pull request 'chore: exclude legacy code from biome check' (#368) from chore/ignore-legacy-biome into main 2026-05-22 02:10:20 +00:00
xiaoju 3fb60ee649 chore: exclude legacy-packages and scripts from biome check
- Add legacy-packages/ and scripts/ to biome ignore
- Allow noDefaultExport in vitest.config.* and .d.ts
- Allow console in cli.ts and setup.ts (CLI user output)
- Fix unused imports in cas.ts and setup.ts
2026-05-22 02:09:18 +00:00
xiaomo e181f67a2d Merge pull request 'feat: support project-local workflow discovery' (#367) from feat/365-project-local-workflows into main 2026-05-22 02:07:33 +00:00
xiaoju a3114bf840 chore: apply biome formatting across codebase 2026-05-22 02:06:05 +00:00
xiaoju e59ae9aca1 feat: support project-local workflow discovery
- Add .workflows/*.yaml scanning from project root (cwd)
- Resolution: project-local first, then global registry
- On-the-fly CAS materialization for local workflows
- Filename/name consistency check
- uwf workflow list shows origin (local/global)

Fixes #365
2026-05-22 01:01:45 +00:00
xiaomo c050a38f38 Merge pull request 'refactor: rename RoleDefinition fields for clarity' (#366) from refactor/364-rename-role-fields into main 2026-05-22 00:48:23 +00:00
xiaoju c60c310074 refactor: rename RoleDefinition fields for clarity
- identity → goal
- prepare → capabilities (string[])
- execute → procedure
- report → output
- outputSchema → meta

Fixes #364
2026-05-22 00:46:06 +00:00
xiaomo fe035c065d Merge pull request 'feat: Role 四段式描述 (identity/prepare/execute/report)' (#361) from feat/359-role-four-phase into main 2026-05-21 03:11:00 +00:00
xiaoju 192ad656a4 refactor: remove systemPrompt, make four-phase fields required
Breaking change per review:
- Remove systemPrompt from RoleDefinition entirely
- identity/prepare/execute/report are now required (string, not nullable)
- Remove all legacy fallback logic in buildRolePrompt
- Simplify validate.ts, workflow.ts materialize
- Migrate all test fixtures and example workflows

Refs #359
2026-05-21 03:07:56 +00:00
xiaoju c0c8d6499e feat: add four-phase example workflow (analyze-topic)
Refs #359, #363
2026-05-21 02:56:11 +00:00
xiaoju 505f85e3c4 feat: add buildRolePrompt in agent-kit, integrate with uwf-hermes
- New buildRolePrompt() in workflow-agent-kit: four-phase prompt assembly
  with fallback to systemPrompt
- Export from agent-kit index
- Update uwf-hermes to use buildRolePrompt instead of raw systemPrompt
- Add tests for all modes: four-phase, legacy, mixed

Refs #359, #362
2026-05-21 02:31:56 +00:00
xiaoju fc7d482b4f feat: add four-phase role description (identity/prepare/execute/report)
- Extend RoleDefinition with identity, prepare, execute, report fields
- Make systemPrompt optional (nullable) for four-phase workflows
- Update ROLE_DEFINITION JSON Schema (all new fields optional)
- Update validate.ts to accept new fields
- Update workflow.ts to strip null fields before CAS storage
- Update thread read to prefer identity over systemPrompt
- Add --version flag to uwf CLI
- Bump all packages to 0.5.0

Refs #359
2026-05-21 01:41:20 +00:00
xiaoju f9979c3c89 chore: upgrade json-cas to 0.4.x, fix Store → BootstrapCapableStore
- @uncaged/json-cas ^0.3.0 → ^0.4.0
- @uncaged/json-cas-fs ^0.3.0 → ^0.4.0 (now publishes .d.ts + .js)
- UwfStore.store typed as BootstrapCapableStore
- tsc --build now clean (no more node_modules type errors)

小橘 🍊(NEKO Team)
2026-05-19 10:29:57 +00:00
xiaoju 46def2945a chore: update dev workflow — fix publish script, remove deploy.sh, update CLAUDE.md
- scripts/publish-all.mjs: update to 6 active packages only
- scripts/deploy.sh: removed (dashboard/gateway in legacy)
- package.json: release script uses publish-all.mjs directly
- CLAUDE.md: add complete dev workflow section (setup, build, check, test, publish)

小橘 🍊(NEKO Team)
2026-05-19 08:07:45 +00:00
xiaoju 4e89508246 docs: rewrite README.md and CLAUDE.md for current architecture
Remove all references to ESM bundles, old packages, old CLI name.
Update to reflect YAML workflow definitions, uwf CLI, 6 active packages,
frontmatter markdown output format, and stateless single-step execution.

小橘 🍊(NEKO Team)
2026-05-19 08:03:13 +00:00
xiaoju 77d799d458 chore: remove obsolete .env.example, config via uwf setup
小橘 🍊(NEKO Team)
2026-05-19 07:58:50 +00:00
xiaoju 6c14259184 chore: remove pnpm-lock.yaml files, bun only
小橘 🍊(NEKO Team)
2026-05-19 07:58:24 +00:00
xiaoju 7b9cb6a9c8 chore: rename uwf-* → workflow-*, cli-uwf → cli-workflow
Reclaim the workflow-* package names now that legacy packages are archived.

Package renames:
- @uncaged/uwf-protocol → @uncaged/workflow-protocol
- @uncaged/uwf-moderator → @uncaged/workflow-moderator
- @uncaged/uwf-agent-kit → @uncaged/workflow-agent-kit
- @uncaged/uwf-agent-hermes → @uncaged/workflow-agent-hermes
- @uncaged/cli-uwf → @uncaged/cli-workflow

All internal imports, tsconfig references, and docs updated.
CLI binary name 'uwf' unchanged.

小橘 🍊(NEKO Team)
2026-05-19 07:52:16 +00:00
91 changed files with 2581 additions and 1941 deletions
-40
View File
@@ -1,40 +0,0 @@
# ──────────────────────────────────────────────
# Workflow Engine — Environment Variables
# ──────────────────────────────────────────────
# Copy this file to .env and fill in the values.
# ── Cursor Agent ──
# CLI command to invoke the Cursor agent (required for develop workflow)
WORKFLOW_CURSOR_COMMAND=
# Model override for Cursor agent
WORKFLOW_CURSOR_MODEL=
# Timeout in milliseconds for Cursor agent operations
WORKFLOW_CURSOR_TIMEOUT=
# ── Hermes Agent (used by develop tester/committer + solve-issue) ──
# CLI command to invoke the Hermes agent (absolute path required)
WORKFLOW_HERMES_COMMAND=
# Model override for Hermes agent
WORKFLOW_HERMES_MODEL=
# Timeout in milliseconds for Hermes agent operations
WORKFLOW_HERMES_TIMEOUT=
# ── Storage ──
# Override the workflow storage root directory
# Default: ~/.uncaged/workflow
WORKFLOW_STORAGE_ROOT=
# Gateway secret for the serve command
WORKFLOW_DASHBOARD_SECRET=
# ── Display ──
# Set to any value to disable colored output
# NO_COLOR=1
+83
View File
@@ -0,0 +1,83 @@
# 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`
+167
View File
@@ -0,0 +1,167 @@
name: "solve-issue"
description: "TDD-driven issue resolution for small, focused changes. Loop protection relies on engine maxRounds."
roles:
planner:
description: "Analyzes issue and outputs a TDD test spec"
goal: "You are a planning agent. You analyze Gitea issues and produce a TDD test specification that downstream roles will implement and verify."
capabilities:
- issue-analysis
- planning
procedure: |
On first run (no previous steps):
1. Read the issue and all comments from Gitea using `tea issues <number> -r <owner/repo>`
2. Read CLAUDE.md (or equivalent project conventions file) to understand coding standards
3. Assess whether the issue has enough information to produce a test spec
4. If insufficient info: comment on the issue via `echo "..." | tea comment <number> -r <owner/repo>` (skip if you already commented), then output status=insufficient_info and terminate
5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios
On subsequent runs (bounced back by tester with fix_spec):
1. Read the tester's output from the previous step to understand what's wrong with the spec
2. Revise the test spec accordingly
After producing the test spec:
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
2. Put the hash in frontmatter.plan (required when status=ready)
output: "Output a brief summary of the test spec. Frontmatter must include: status (ready or insufficient_info) and plan (CAS hash of the test spec, required when status=ready)."
frontmatter:
type: object
properties:
status:
type: string
enum: [ready, insufficient_info]
plan:
type: string
required: [status]
developer:
description: "TDD implementation per test spec"
goal: "You are a developer agent. You implement code changes following TDD — write tests first, then implementation."
capabilities:
- coding
procedure: |
1. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's meta.plan)
2. If bounced back from reviewer or tester: read the previous role's output to understand what needs fixing
3. Write tests first based on the spec
4. Implement the code to make tests pass
5. Ensure `bun run build` passes with no errors
6. Run `bun test` to verify all tests pass
output: "List all files changed and provide a summary. Frontmatter must include: status (done or failed)."
frontmatter:
type: object
properties:
status:
type: string
enum: [done, failed]
required: [status]
reviewer:
description: "Code standards compliance check"
goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)."
capabilities:
- code-review
- static-analysis
procedure: |
Hard checks (must all pass):
1. `bun run build` — no build errors
2. `bunx biome check` — no lint violations
3. TypeScript strict mode — no type errors
Soft checks (review against CLAUDE.md conventions):
- Functional-first: `function` + `type`, not `class` + `interface`
- No optional properties (`?:`) — use `T | null`
- Naming conventions (kebab-case files, PascalCase types, camelCase functions)
- Module boundary discipline (folder exports via index.ts)
- No `console.log` (use structured logger)
- No dynamic imports in production code
Only review standards compliance. Do NOT test functionality.
If rejecting, you MUST explain the specific reason in your output.
output: "Explain your decision with specific file/line references. Frontmatter must include: approved (true or false)."
frontmatter:
type: object
properties:
approved:
type: boolean
required: [approved]
tester:
description: "Functional correctness verification"
goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec."
capabilities:
- testing
procedure: |
1. Run `bun test` for automated test verification
2. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's meta.plan)
3. Verify each scenario in the spec is covered and passing
4. Determine outcome:
- passed: all scenarios verified, tests pass
- fix_code: tests fail or implementation doesn't match spec → send back to developer
- fix_spec: the spec itself is wrong or incomplete → send back to planner
output: "Report test results per scenario. Frontmatter must include: status (passed, fix_code, or fix_spec)."
frontmatter:
type: object
properties:
status:
type: string
enum: [passed, fix_code, fix_spec]
required: [status]
committer:
description: "Commits and creates PR"
goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue."
capabilities: []
procedure: |
Note: You inherit the developer's worktree and branch. Do NOT create a new branch.
1. Stage all changes: `git add -A`
2. Commit with a descriptive message referencing the issue: `git commit -m "type: description\n\nFixes #N"`
3. Push the branch: `git push -u origin <branch-name>`
- If push hook fails: capture the error log in your output, mark hook_failed
4. On push success: create a PR via `tea pr create --title "..." --description "..."`
- PR description must follow the project template: What / Why / Changes / Ref sections, with `Fixes #N` in Ref
output: "Include PR URL on success or error log on failure. Frontmatter must include: success (true or false)."
frontmatter:
type: object
properties:
success:
type: boolean
required: [success]
conditions:
insufficientInfo:
description: "Planner determined there's not enough info to proceed"
expression: "$last('planner').status = 'insufficient_info'"
devFailed:
description: "Developer failed to implement"
expression: "$last('developer').status = 'failed'"
rejected:
description: "Reviewer rejected the implementation"
expression: "$last('reviewer').approved = false"
fixCode:
description: "Tester found code issues"
expression: "$last('tester').status = 'fix_code'"
fixSpec:
description: "Tester found spec issues"
expression: "$last('tester').status = 'fix_spec'"
hookFailed:
description: "Push hook failed"
expression: "$last('committer').success = false"
graph:
$START:
- role: "planner"
planner:
- role: "$END"
condition: "insufficientInfo"
- role: "developer"
developer:
- role: "$END"
condition: "devFailed"
- role: "reviewer"
reviewer:
- role: "developer"
condition: "rejected"
- role: "tester"
tester:
- role: "developer"
condition: "fixCode"
- role: "planner"
condition: "fixSpec"
- role: "committer"
committer:
- role: "developer"
condition: "hookFailed"
- role: "$END"
+64 -69
View File
@@ -2,46 +2,41 @@
## Project Overview
This monorepo implements a workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file with an XXH64 hash as its version identifier. Shared types live in `@uncaged/workflow-protocol`; bundle authors typically depend on `@uncaged/workflow-runtime`.
This monorepo implements a stateless workflow engine driven by a single-step CLI (`uwf`). Workflows are **YAML definitions** stored as CAS nodes; threads are immutable chains of CAS-linked step nodes. No daemon — each `uwf thread step` invocation runs one moderator→agent→extract cycle and exits.
### Key Terms
| Concept | What it is |
|---------|-----------|
| **Workflow** | A single-file ESM module that exports `run` (workflow function) and `descriptor` (metadata). Identified by its XXH64 hash (Crockford Base32). |
| **Bundle** | The physical `.esm.js` file stored in `~/.uncaged/workflow/bundles/`. |
| **Thread** | A single execution of a workflow, identified by a ULID. State lives in CAS (linked nodes); active threads indexed in `threads.json`; completed rows in `history/*.jsonl`. Debug logs use `.info.jsonl`. |
| **Role** | A named actor within a workflow. Each role produces output with typed `meta`. |
| **Registry** | `workflow.yaml` — maps workflow names to current/historical bundle hashes. |
| **Workflow** | A YAML definition (`WorkflowPayload`) with roles, conditions, and a routing graph. Stored as a CAS node, identified by its XXH64 hash. |
| **Thread** | A single execution of a workflow, identified by a ULID. State is an immutable CAS chain; active threads indexed in `threads.yaml`; completed threads in `history.jsonl`. |
| **Role** | A named actor within a workflow. Each role has a system prompt and a JSON Schema `outputSchema`. |
| **Moderator** | JSONata-based graph evaluator — determines the next role (or `$END`) with zero LLM cost. |
| **Agent** | An external CLI command (`uwf-hermes`, etc.) spawned by `uwf thread step`. Produces frontmatter markdown output. |
| **CAS** | Content-Addressed Storage via `@uncaged/json-cas` — all workflow definitions, thread nodes, and outputs are immutable CAS nodes. |
| **Registry** | `~/.uncaged/workflow/registry.yaml` — maps workflow names to current CAS hashes. |
### Monorepo Structure
```
workflow/
packages/
workflow-protocol/ # @uncaged/workflow-protocol — shared types + Result
workflow-runtime/ # @uncaged/workflow-runtime — createWorkflow, type re-exports
workflow-util/ # @uncaged/workflow-util — Base32, ULID, logger, storage paths, refs helpers
workflow-reactor/ # @uncaged/workflow-reactor — LLM fn + thread reactor (tool calls)
workflow-cas/ # @uncaged/workflow-cas — CAS store, hash, Merkle
workflow-register/ # @uncaged/workflow-register — bundle validation, registry YAML, model resolution
workflow-execute/ # @uncaged/workflow-execute — engine, extract, fork, GC, workflowAsAgent
cli-workflow/ # @uncaged/cli-workflow — uncaged-workflow CLI
workflow-agent-cursor/ # @uncaged/workflow-agent-cursor
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes
workflow-agent-llm/ # @uncaged/workflow-agent-llm
workflow-agent-react/ # @uncaged/workflow-agent-react
workflow-util-agent/ # @uncaged/workflow-util-agent — buildAgentPrompt, spawnCli
workflow-template-develop/ # @uncaged/workflow-template-develop
workflow-template-solve-issue/ # @uncaged/workflow-template-solve-issue
workflow-dashboard/ # @uncaged/workflow-dashboard — React dashboard (private app)
docs/ # RFCs, conventions
biome.json # root Biome config
tsconfig.json # root TypeScript config
workflow-protocol/ # @uncaged/workflow-protocol — shared types (WorkflowPayload, StepNodePayload, WorkflowConfig, etc.)
workflow-util/ # @uncaged/workflow-util — Crockford Base32, ULID, logger, frontmatter parsing/validation
workflow-moderator/ # @uncaged/workflow-moderator — JSONata graph evaluator
workflow-agent-kit/ # @uncaged/workflow-agent-kit — createAgent factory, context builder, extract pipeline
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — uwf-hermes CLI binary (spawns hermes chat)
cli-workflow/ # @uncaged/cli-workflow — uwf CLI binary
legacy-packages/ # Archived packages (preserved for reference, not active)
examples/ # Workflow YAML examples (solve-issue.yaml)
docs/ # Architecture docs
biome.json # root Biome config
tsconfig.json # root TypeScript config
```
- Execution stack layers: `workflow-protocol` → (`workflow-runtime`, `workflow-util`, `workflow-reactor`) → (`workflow-cas`, `workflow-register`)`workflow-execute` `cli-workflow`
- Dependency layers: `workflow-protocol` → (`workflow-util`, `workflow-moderator`) → `workflow-agent-kit``workflow-agent-hermes` / `cli-workflow`
- Packages use `workspace:^` protocol (resolves to `^x.y.z` on publish)
- External CAS: `@uncaged/json-cas` (store API, hashing, schema validation) + `@uncaged/json-cas-fs` (filesystem backend)
## Language & Paradigm
@@ -109,8 +104,6 @@ type WorkflowEntry = {
- Always named exports, never default exports
- One module = one responsibility, filename = purpose
Workflow bundles (`.esm.js`) follow the same rule: export `const run` and `const descriptor`, not `export default`.
### Folder Module Discipline
Every folder under `src/` is a **module boundary**. Four rules:
@@ -136,10 +129,10 @@ export { createCasStore } from "../cas/cas.js";
// ❌ Bad — types defined in index.ts
// in cas/index.ts:
export type CasStore = { ... }; // should be in cas/types.ts
export type CasStore = { ... }; // should be in cas/types.ts
```
**Exception**: The package-level `src/index.ts` is the public API surface and re-exports from folder `index.ts` files. Files that remain at `src/` root (e.g. `types.ts`, `workflow-as-agent.ts`) are not inside a folder module and follow normal rules.
**Exception**: The package-level `src/index.ts` is the public API surface and re-exports from folder `index.ts` files. Files that remain at `src/` root (e.g. `types.ts`) are not inside a folder module and follow normal rules.
## Naming
@@ -160,7 +153,7 @@ Workflow names use **verb-first** kebab-case:
### ID Encoding
All IDs use **Crockford Base32**:
- Bundle hash: XXH64 → 13-char Crockford Base32
- CAS hash: XXH64 → 13-char Crockford Base32
- Thread ID: ULID → 26-char Crockford Base32 (10 timestamp + 16 random)
## Error Handling
@@ -189,7 +182,7 @@ import { createLogger } from "@uncaged/workflow-util";
const log = createLogger();
// Each call site has a fixed 8-char Crockford Base32 tag
log("4KNMR2PX", "Loading workflow bundle...");
log("4KNMR2PX", "Loading workflow...");
log("7BQST3VW", `Role ${role} started`);
```
@@ -204,7 +197,7 @@ log("7BQST3VW", `Role ${role} started`);
### Why fixed tags?
- `grep "4KNMR2PX"` in `.info.jsonl` → instant code location
- `grep "4KNMR2PX"` in logs → instant code location
- No need for file/line info in the log — tag is the locator
- Survives refactoring (tag stays the same when code moves)
@@ -221,74 +214,76 @@ console.log(result);
Do NOT use `await import()` in production code. Always use static top-level `import`.
**Exception**: The bundle loader and `extractBundleExports` dynamically import user workflow files at runtime.
```ts
// Dynamic import required: user bundle path resolved at runtime
const mod = await import(bundlePath);
```
Test files (`__tests__/**`) are exempt.
## Toolchain
| Tool | Purpose |
|------|---------|
| **bun** | Package manager + runtime + test runner |
| **bun** | Package manager + runtime |
| **TypeScript** | Type checking (strict mode) |
| **Biome** | Lint + format (replaces ESLint + Prettier) |
| **vitest** | Test runner (`cli-workflow` uses vitest; other packages use `bun test`) |
### Commands
### Development Workflow
```bash
bun run check # tsc --build + biome check
bun run format # biome format --write
bun test # run tests
# ── Setup ──
bun install # install all workspace dependencies
# ── Daily development ──
bun run build # tsc --build (all packages, dependency order)
bun run check # tsc --build + biome check + lint-log-tags
bun run format # biome format --write
bun test # run tests across all packages
# ── Before committing ──
bun run check # must pass — typecheck + lint + log tag validation
bun test # must pass — all package tests
```
### Version Management & Publishing
### Publishing
All public `@uncaged/*` packages are published to **npmjs.org** via `@changesets/cli` with **fixed mode** (all packages share the same version number). `workflow-dashboard` is private and excluded.
All public `@uncaged/*` packages are published to **npmjs.org** with **fixed mode** (all packages share the same version number).
```bash
# 1. After making changes, add a changeset describing the change
# 1. Add a changeset describing the change
bun changeset
# 2. Before release, bump all package versions + generate CHANGELOGs
# 2. Bump all package versions + generate CHANGELOGs
bun version
# 3. Build, test, and publish to npmjs
# 3. Build, test, and publish (runs scripts/publish-all.mjs)
bun release
# Or publish manually with a tag:
node scripts/publish-all.mjs --tag alpha
node scripts/publish-all.mjs --dry-run # preview without publishing
```
- `workspace:^` dependencies resolve to `^x.y.z` on publish
- Publish order defined in `scripts/publish-all.mjs` (dependency order)
- Changesets config: `.changeset/config.json` (fixed mode, public access)
- Each package has auto-generated `CHANGELOG.md`
### Consuming @uncaged/* Packages
External workflow repos just `bun install` — packages come from npmjs like any other dependency. No special registry config needed.
### End-to-end: Monorepo → Registry → Workspace → Bundle
### End-to-end: Author → Register → Run
```
workflow/ (monorepo) — engine, runtime, templates, agents
bun release — build + test + changeset publish
examples/solve-issue.yaml — write a workflow YAML definition
uwf workflow put
npmjs.org — @uncaged/* scoped packages (public)
│ bun install
~/.uncaged/workflow/cas/ — Workflow stored as CAS node
~/.uncaged/workflow/registry.yaml — name → hash mapping updated
│ uwf thread start <name> -p "..."
my-workflows/ (workspace) — normal package.json
bun run build:develop — bun build → single .esm.js
~/.uncaged/workflow/threads.yaml — new thread head pointer
uwf thread step <thread-id>
uncaged-workflow workflow add — register bundle locally
uncaged-workflow run — execute workflow
moderator → agent → extract — one step per invocation, repeat until $END
```
1. **Monorepo changes**`bun changeset` (describe change) → `bun version` (bump) → `bun release` (publish)
2. **Workspace** `bun install` fetches latest from npmjs
3. **Build** → produces single-file ESM bundle with `@uncaged/*` as externals
4. **Register & Run**`uncaged-workflow workflow add <name> <bundle>` then `uncaged-workflow run <name>`
1. **Author** — write a workflow YAML file with roles, conditions, and graph
2. **Register** `uwf workflow put <file.yaml>` parses YAML, registers output schemas, stores `WorkflowPayload` in CAS
3. **Run** `uwf thread start` creates a thread, `uwf thread step` executes one cycle per invocation
## Commit Convention
@@ -296,5 +291,5 @@ uncaged-workflow run — execute workflow
<type>(<scope>): <description>
type: feat | fix | refactor | docs | chore | test
scope: workflow | cli | rfc-001 | ...
scope: workflow | cli | moderator | agent-kit | hermes | util | protocol | ...
```
+69 -47
View File
@@ -1,71 +1,93 @@
# @uncaged/workflow
A workflow engine that executes single-file ESM bundles. Each workflow is a self-contained `.esm.js` file identified by its XXH64 hash (Crockford Base32).
A stateless workflow engine driven by a single-step CLI. Workflows are YAML definitions with roles, JSONata routing conditions, and a directed graph. Threads are immutable CAS-linked chains — each `uwf thread step` runs one moderator→agent→extract cycle and exits.
## Core Concepts
## Package Map
| Concept | Description |
|---------|-------------|
| **Workflow** | A single-file ESM module exporting `run` (workflow function) and `descriptor` (metadata). Identified by its XXH64 hash. |
| **Bundle** | The physical `.esm.js` file stored in `~/.uncaged/workflow/bundles/`. |
| **Thread** | A single execution of a workflow, identified by a ULID. CAS-backed chain plus `threads.json` / `history/*.jsonl`; `.info.jsonl` for debug logs. |
| **Role** | A named actor within a workflow. Each role produces output with typed `meta`. Roles live inside template packages (`src/roles/`). |
| **Registry** | `workflow.yaml` — maps workflow names to current/historical bundle hashes. |
| **CAS** | Content-Addressed Storage — bundles are immutable and addressed by hash. |
| Package | npm | Role |
|---------|-----|------|
| `cli-workflow` | `@uncaged/cli-workflow` | `uwf` CLI binary — thread lifecycle, workflow registry, CAS inspection, setup |
| `workflow-protocol` | `@uncaged/workflow-protocol` | Shared TypeScript types (`WorkflowPayload`, `StepNodePayload`, `WorkflowConfig`, etc.) |
| `workflow-moderator` | `@uncaged/workflow-moderator` | JSONata graph evaluator — determines next role or `$END` |
| `workflow-agent-kit` | `@uncaged/workflow-agent-kit` | `createAgent` factory, context builder, two-layer extract pipeline |
| `workflow-agent-hermes` | `@uncaged/workflow-agent-hermes` | `uwf-hermes` agent — spawns Hermes chat, captures session |
| `workflow-util` | `@uncaged/workflow-util` | Crockford Base32, ULID, logger, frontmatter parsing |
## Monorepo Packages
```
packages/
workflow/ # @uncaged/workflow — core lib (types, engine, hash, ULID, registry)
cli-workflow/ # @uncaged/cli-workflow — CLI (`uncaged-workflow` command)
workflow-template-develop/ # @uncaged/workflow-template-develop — develop workflow template (includes roles)
workflow-template-solve-issue/ # @uncaged/workflow-template-solve-issue — solve-issue workflow template (includes roles)
workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — Hermes agent adapter
workflow-agent-cursor/ # @uncaged/workflow-agent-cursor — Cursor agent adapter
workflow-agent-llm/ # @uncaged/workflow-agent-llm — LLM agent adapter
workflow-util-agent/ # @uncaged/workflow-util-agent — agent utilities (buildAgentPrompt, spawnCli)
```
Managed with **bun workspace** using the `workspace:*` protocol.
External: [`@uncaged/json-cas`](https://www.npmjs.com/package/@uncaged/json-cas) (CAS store + JSON Schema validation) + `@uncaged/json-cas-fs` (filesystem backend).
## Quick Start
```bash
# Install dependencies
bun install
# 1. Configure provider and model
uwf setup
# Build all packages
bun run build
# 2. Register a workflow from YAML
uwf workflow put examples/solve-issue.yaml
# Register a workflow bundle
uncaged-workflow workflow add solve-issue dist/packages/workflow-template-solve-issue/solve-issue.esm.js
# 3. Start a thread
uwf thread start solve-issue -p "Fix the login redirect bug"
# Run a workflow
uncaged-workflow run solve-issue --prompt "Fix bug #42"
# 4. Execute steps (one at a time, until done)
uwf thread step <thread-id>
```
## CLI Usage
## CLI Commands
```bash
uncaged-workflow # Print full command usage (exits with status 1)
uncaged-workflow workflow list # List registered workflows
uncaged-workflow run <name> # Start a workflow thread
uncaged-workflow thread list # List all threads
uncaged-workflow thread show <id> # Inspect a thread
uncaged-workflow skill # Agent-consumable reference docs
```
### Thread
Run `uncaged-workflow` with no arguments to print usage, or `uncaged-workflow skill cli` for the full CLI skill reference.
| Command | Description |
|---------|-------------|
| `uwf thread start <workflow> -p <prompt>` | Create a thread (no execution) |
| `uwf thread step <thread-id> [--agent <cmd>]` | Execute one moderator→agent→extract cycle |
| `uwf thread show <thread-id>` | Show head pointer and done status |
| `uwf thread list [--all]` | List threads (`--all` includes archived) |
| `uwf thread steps <thread-id>` | List all steps chronologically |
| `uwf thread read <thread-id> [--quota N]` | Render thread as readable markdown |
| `uwf thread fork <step-hash>` | Fork from a specific step |
| `uwf thread step-details <step-hash>` | Dump full detail node |
| `uwf thread kill <thread-id>` | Terminate and archive |
### Workflow
| Command | Description |
|---------|-------------|
| `uwf workflow put <file.yaml>` | Register a workflow from YAML |
| `uwf workflow show <name-or-hash>` | Show workflow definition |
| `uwf workflow list` | List registered workflows |
### CAS
| Command | Description |
|---------|-------------|
| `uwf cas get <hash>` | Read a CAS node |
| `uwf cas put <type-hash> <data>` | Store a node |
| `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 a schema |
### Setup
| Command | Description |
|---------|-------------|
| `uwf setup` | Interactive provider/model/agent configuration |
| `uwf setup --provider ... --base-url ... --api-key ... --model ...` | Non-interactive setup |
Config stored in `~/.uncaged/workflow/config.yaml`. API keys in `~/.uncaged/workflow/.env`.
## Development
```bash
bun run check # Biome lint + format check
bun run format # Auto-format with Biome
bun test # Run tests
bun install --no-cache # Install dependencies
bun run check # tsc + biome + lint-log-tags
bun run format # Auto-format with Biome
bun test # Run all tests
```
Managed with **bun workspace**. See [CLAUDE.md](CLAUDE.md) for coding conventions.
## Architecture
See [docs/architecture.md](docs/architecture.md) for the full design — three-phase engine loop, bundle contract, storage layout, and design decisions.
See [docs/architecture.md](docs/architecture.md) for the full design — three-phase engine loop, CAS node types, storage layout, agent CLI protocol, and design decisions.
+13 -1
View File
@@ -5,6 +5,8 @@
"**",
"!**/dist",
"!**/node_modules",
"!**/legacy-packages",
"!scripts",
"!packages/workflow/workflow",
"!xiaoju/scripts/bundle.ts"
]
@@ -36,7 +38,7 @@
}
},
{
"includes": ["**/*.d.ts"],
"includes": ["**/*.d.ts", "**/vitest.config.*"],
"linter": {
"rules": {
"style": {
@@ -44,6 +46,16 @@
}
}
}
},
{
"includes": ["**/cli.ts", "**/setup.ts"],
"linter": {
"rules": {
"suspicious": {
"noConsole": "off"
}
}
}
}
],
"linter": {
+50 -33
View File
@@ -1,4 +1,4 @@
# uwf — Architecture
# Workflow Engine — Architecture
**Last updated:** 2026-05-19
@@ -14,12 +14,12 @@ The implementation lives in **6** active packages under `packages/`, plus two ex
| Layer | Package | One-line role |
|-------|---------|---------------|
| Contract | `@uncaged/uwf-protocol``uwf-protocol` | Shared TypeScript types (`WorkflowPayload`, `StepNodePayload`, `ModeratorContext`, `WorkflowConfig`, etc.). No runtime deps beyond `@uncaged/json-cas-fs`. |
| Contract | `@uncaged/workflow-protocol``workflow-protocol` | Shared TypeScript types (`WorkflowPayload`, `StepNodePayload`, `ModeratorContext`, `WorkflowConfig`, etc.). No runtime deps beyond `@uncaged/json-cas-fs`. |
| Shared infra | `@uncaged/workflow-util``workflow-util` | Crockford Base32, ULID generation, `createLogger`, frontmatter parsing/validation. |
| Moderator | `@uncaged/uwf-moderator``uwf-moderator` | JSONata-based graph evaluator: given a `WorkflowPayload` and `ModeratorContext`, returns the next role or `$END`. |
| Agent framework | `@uncaged/uwf-agent-kit``uwf-agent-kit` | `createAgent` entrypoint factory, context builder, frontmatter fast-path extractor, LLM extract fallback, output format instruction builder. |
| Agent: Hermes | `@uncaged/uwf-agent-hermes``uwf-agent-hermes` | `uwf-hermes` CLI binary — spawns `hermes chat`, pipes prompt, captures session detail. |
| CLI | `@uncaged/cli-uwf``cli-uwf` | `uwf` binary — thread lifecycle, workflow registry, CAS inspection, setup. |
| Moderator | `@uncaged/workflow-moderator``workflow-moderator` | JSONata-based graph evaluator: given a `WorkflowPayload` and `ModeratorContext`, returns the next role or `$END`. |
| Agent framework | `@uncaged/workflow-agent-kit``workflow-agent-kit` | `createAgent` entrypoint factory, context builder, frontmatter fast-path extractor, LLM extract fallback, output format instruction builder. |
| Agent: Hermes | `@uncaged/workflow-agent-hermes``workflow-agent-hermes` | `uwf-hermes` CLI binary — spawns `hermes chat`, pipes prompt, captures session detail. |
| CLI | `@uncaged/cli-workflow``cli-workflow` | `uwf` binary — thread lifecycle, workflow registry, CAS inspection, setup. |
### External dependencies
@@ -27,8 +27,8 @@ The implementation lives in **6** active packages under `packages/`, plus two ex
|---------|------|
| `@uncaged/json-cas` | Content-addressed store API, XXH64 hashing, JSON Schema registration and validation. |
| `@uncaged/json-cas-fs` | Filesystem backend for `json-cas`. |
| `jsonata` | JSONata expression evaluator (used by `uwf-moderator`). |
| `commander` | CLI argument parsing (used by `cli-uwf`). |
| `jsonata` | JSONata expression evaluator (used by `workflow-moderator`). |
| `commander` | CLI argument parsing (used by `cli-workflow`). |
| `dotenv` | Loads `.env` files for API keys. |
| `yaml` | YAML parse/stringify. |
@@ -41,20 +41,20 @@ flowchart BT
jcasfs["@uncaged/json-cas-fs"]
end
subgraph L0["Layer 0 — contract"]
protocol["@uncaged/uwf-protocol"]
protocol["@uncaged/workflow-protocol"]
end
subgraph L1["Layer 1 — shared"]
util["@uncaged/workflow-util"]
moderator["@uncaged/uwf-moderator"]
moderator["@uncaged/workflow-moderator"]
end
subgraph L2["Layer 2 — agent framework"]
kit["@uncaged/uwf-agent-kit"]
kit["@uncaged/workflow-agent-kit"]
end
subgraph L3["Layer 3 — agent implementations"]
hermes["@uncaged/uwf-agent-hermes"]
hermes["@uncaged/workflow-agent-hermes"]
end
subgraph L4["Layer 4 — CLI"]
cli["@uncaged/cli-uwf"]
cli["@uncaged/cli-workflow"]
end
protocol --> jcasfs
util --> protocol
@@ -85,8 +85,13 @@ description: "End-to-end issue resolution"
roles:
planner:
description: "Creates implementation plan"
systemPrompt: "You are a planning agent. Analyze the issue and create a step-by-step plan."
outputSchema:
goal: "You are a planning agent. Analyze the issue and create a step-by-step plan."
capabilities:
- issue-analysis
- planning
procedure: "Analyze the issue and create a detailed, actionable implementation plan."
output: "Output the plan summary and list of concrete steps."
meta:
type: object
properties:
plan: { type: string }
@@ -94,8 +99,13 @@ roles:
required: [plan, steps]
developer:
description: "Implements code changes"
systemPrompt: "You are a developer agent. Implement the plan."
outputSchema:
goal: "You are a developer agent. Implement the plan."
capabilities:
- file-edit
- shell
procedure: "Implement the plan. Write code, tests, and ensure existing tests pass."
output: "List all files changed and provide a summary of the implementation."
meta:
type: object
properties:
filesChanged: { type: array, items: { type: string } }
@@ -103,8 +113,12 @@ roles:
required: [filesChanged, summary]
reviewer:
description: "Reviews code changes"
systemPrompt: "You are a code reviewer. Review the implementation."
outputSchema:
goal: "You are a code reviewer. Review the implementation."
capabilities:
- code-review
procedure: "Review the implementation against the plan."
output: "Approve or reject with detailed comments."
meta:
type: object
properties:
approved: { type: boolean }
@@ -133,7 +147,7 @@ graph:
Key properties:
- **`roles`** — inline role definitions; each `outputSchema` is a JSON Schema (stored as its own CAS node on registration)
- **`roles`** — inline role definitions; each `meta` is a JSON Schema (stored as its own CAS node on registration)
- **`conditions`** — named JSONata expressions evaluated against the `ModeratorContext`
- **`graph`** — `Record<Role | "$START", Transition[]>` — first matching transition wins; `condition: null` = fallback
- **No agent binding** — agent selection is a deployment concern, configured in `config.yaml`
@@ -141,7 +155,7 @@ Key properties:
## Three-phase engine loop
Each `uwf thread step` runs exactly one cycle: moderator → agent → extract. The CLI orchestrates this in `packages/cli-uwf/src/commands/thread.ts` (`cmdThreadStep`).
Each `uwf thread step` runs exactly one cycle: moderator → agent → extract. The CLI orchestrates this in `packages/cli-workflow/src/commands/thread.ts` (`cmdThreadStep`).
```
┌─→ Phase 1: MODERATOR
@@ -156,7 +170,7 @@ Each `uwf thread step` runs exactly one cycle: moderator → agent → extract.
│ Output: raw string (frontmatter markdown)
│ Phase 3: EXTRACT
│ Input: raw agent output + role's outputSchema
│ Input: raw agent output + role's meta schema
│ Engine: two-layer extract (frontmatter fast path → LLM fallback)
│ Output: CasRef to structured output node
@@ -167,7 +181,7 @@ Each `uwf thread step` runs exactly one cycle: moderator → agent → extract.
### Context types
Defined in `packages/uwf-protocol/src/types.ts`:
Defined in `packages/workflow-protocol/src/types.ts`:
```typescript
type StepContext = {
@@ -209,11 +223,11 @@ Each agent is an external command invoked by `uwf thread step`:
Contract:
1. `uwf thread step` determines the next role via the moderator
2. Agent CLI is spawned with `(thread-id, role)` as positional args
3. `uwf-agent-kit` (`createAgent`) handles the boilerplate:
3. `workflow-agent-kit` (`createAgent`) handles the boilerplate:
- Parses argv
- Loads `.env` from storage root
- Builds `AgentContext` by walking the CAS chain from `threads.yaml` head
- Resolves the role's `outputSchema` and builds `outputFormatInstruction`
- Resolves the role's `meta` schema and builds `outputFormatInstruction`
- Calls the agent's `run` function
- Runs two-layer extract on the raw output
- Writes `StepNode` to CAS (output + detail + prev link)
@@ -242,18 +256,18 @@ scope: role
Fixed the login redirect by updating the auth middleware...
```
The `outputFormatInstruction` (built by `buildOutputFormatInstruction` in `uwf-agent-kit`) is prepended to the role's system prompt, so the deliverable format is the first thing the agent sees. It lists the expected frontmatter fields derived from the role's JSON Schema.
The `outputFormatInstruction` (built by `buildOutputFormatInstruction` in `workflow-agent-kit`) is prepended to the role's system prompt, so the deliverable format is the first thing the agent sees. It lists the expected frontmatter fields derived from the role's `meta` JSON Schema.
## Two-layer extract
Structured output extraction uses a two-layer strategy (`uwf-agent-kit`):
Structured output extraction uses a two-layer strategy (`workflow-agent-kit`):
### Layer 1: frontmatter fast path (`frontmatter.ts`)
1. Parse YAML frontmatter from raw agent output (`parseFrontmatterMarkdown`)
2. Validate required fields (`validateFrontmatter`)
3. Build a candidate object from frontmatter fields (`status`, `next`, `confidence`, `artifacts`, `scope`)
4. `store.put()` the candidate against the role's `outputSchema`
4. `store.put()` the candidate against the role's `meta` schema
5. Validate with `json-cas` schema validation
6. If valid → return `outputHash` (zero LLM cost)
@@ -270,9 +284,9 @@ If the fast path returns `null` (no frontmatter, invalid, or doesn't satisfy sch
## Prompt injection
`uwf-agent-kit` prepends two pieces of context to the agent's system prompt:
`workflow-agent-kit` prepends two pieces of context to the agent's system prompt:
1. **Deliverable format instruction** — generated from the role's `outputSchema`, tells the agent exactly what frontmatter fields to produce and the expected format
1. **Deliverable format instruction** — generated from the role's `meta` schema, tells the agent exactly what frontmatter fields to produce and the expected format
2. **Scope constraint** — "Focus exclusively on YOUR role's deliverable. Do not perform actions outside your role's scope."
This ensures agents produce parseable frontmatter output without requiring per-agent format knowledge.
@@ -289,8 +303,11 @@ payload:
roles:
planner:
description: "Creates implementation plan"
systemPrompt: "You are a planning agent..."
outputSchema: "5GWKR8TN1V3JA" # cas_ref → JSON Schema node
goal: "You are a planning agent..."
capabilities: [planning, issue-analysis]
procedure: "Analyze the issue and create a plan."
output: "Output the plan summary."
meta: "5GWKR8TN1V3JA" # cas_ref → JSON Schema node
conditions:
notApproved:
description: "Reviewer rejected"
@@ -318,7 +335,7 @@ payload:
start: "4TNVW8KR2B3MA" # cas_ref → StartNode
prev: "2MXBG6PN4A8JR" # cas_ref → previous StepNode (null for first step)
role: "developer"
output: "9KRVW3TN5F1QA" # cas_ref → structured output (validated against outputSchema)
output: "9KRVW3TN5F1QA" # cas_ref → structured output (validated against meta schema)
detail: "7BQST3VW9F2MA" # cas_ref → execution detail (raw turns, session data)
agent: "uwf-hermes" # agent command used (plain string)
```
+33 -21
View File
@@ -112,8 +112,8 @@ uwf-hermes <thread-id> <role>
**约定:**
- `uwf step` 负责 moderator 决策,将 role 传给 agent CLI
- agent-kit 根据 thread + role 从 CAS 读 systemPrompt / outputSchema
- agent-kit 组装完整 prompt(role systemPrompt + thread context + user prompt from StartNode)
- agent-kit 根据 thread + role 从 CAS 读 goal / capabilities / procedure / output / meta
- agent-kit 组装完整 prompt(role goal/capabilities/procedure/output + thread context + user prompt from StartNode)
- agent 执行实际逻辑,agent-kit 负责 extract
- agent 将 StepNode 写入 CAS(含 output、detail、agent、prev),但**不挪链头指针**
- stdout 输出新 StepNode 的 CAS hash(纯文本,一行)
@@ -143,7 +143,7 @@ uwf-hermes <thread-id> <role>
#### `Workflow`
Roles 和 moderator 内联在 Workflow 中,只有 outputSchema 独立为 CAS 节点(方便 json-cas 校验)。
Roles 和 moderator 内联在 Workflow 中,只有 meta 独立为 CAS 节点(方便 json-cas 校验)。
```yaml
type: <workflow-schema-hash>
@@ -153,16 +153,25 @@ payload:
roles:
planner:
description: "Creates implementation plan"
systemPrompt: "You are a planning agent..."
outputSchema: "5GWKR8TN1V3JA" # cas_ref → JSON Schema 节点(json-cas 内置)
goal: "You are a planning agent..."
capabilities: [planning, issue-analysis]
procedure: "Analyze the issue and create a plan."
output: "Output the plan summary."
meta: "5GWKR8TN1V3JA" # cas_ref → JSON Schema 节点(json-cas 内置)
developer:
description: "Implements code changes"
systemPrompt: "You are a developer agent..."
outputSchema: "8CNWT4KR6D1HV" # cas_ref → JSON Schema 节点
goal: "You are a developer agent..."
capabilities: [file-edit, shell]
procedure: "Implement the plan."
output: "List all files changed."
meta: "8CNWT4KR6D1HV" # cas_ref → JSON Schema 节点
reviewer:
description: "Reviews code changes"
systemPrompt: "You are a code reviewer..."
outputSchema: "1VPBG9SM5E7WK" # cas_ref → JSON Schema 节点
goal: "You are a code reviewer..."
capabilities: [code-review]
procedure: "Review the implementation."
output: "Approve or reject with comments."
meta: "1VPBG9SM5E7WK" # cas_ref → JSON Schema 节点
conditions:
needsClarification:
description: "Planner requests clarification from user"
@@ -189,7 +198,7 @@ payload:
condition: null
```
- `roles` — 内联定义,每个 role 的 `outputSchema` 是独立的 cas_ref(指向 json-cas 内置 JSON Schema 节点)
- `roles` — 内联定义,每个 role 的 `meta` 是独立的 cas_ref(指向 json-cas 内置 JSON Schema 节点)
- `conditions``Record<Name, JSONata>`,命名条件,方便画图描述
- `graph``Record<Role | "$START", Transition[]>`,每个 Transition = `{ role, condition }`
- `condition` 引用 conditions 中的 key,`null` = fallback
@@ -234,14 +243,14 @@ payload:
start: "4TNVW8KR2B3MA" # cas_ref → StartNode(每个 step 都引用)
prev: "2MXBG6PN4A8JR" # cas_ref → 前一个 StepNode,第一步为 null
role: "developer"
output: "9KRVW3TN5F1QA" # cas_ref → 结构化输出节点(符合 role 的 outputSchema)
output: "9KRVW3TN5F1QA" # cas_ref → 结构化输出节点(符合 role 的 meta schema)
detail: "7BQST3VW9F2MA" # cas_ref → 执行详情(content node / 子 workflow terminal StepNode / ...)
agent: "uwf-cursor" # 实际使用的 agent 命令(纯字符串)
```
- `start` — 每个 StepNode 都直接引用 StartNode,方便随机访问
- `prev` — 前一个 StepNode 的 cas_ref,第一步为 `null`(不指向 StartNode)
- `output` — cas_ref,指向符合 role outputSchema 的 CAS 节点,可用 json-cas 校验
- `output` — cas_ref,指向符合 role meta schema 的 CAS 节点,可用 json-cas 校验
- `detail` — cas_ref,指向执行详情。可以是原始 agent 输出(content node),也可以是子 workflow thread 的 terminal StepNode(workflowAsAgent 场景)
- `agent` — 纯字符串,不是 CAS 节点
@@ -340,12 +349,12 @@ OPENROUTER_API_KEY=sk-or-...
```
packages/
├── cli-uwf/ # @uncaged/cli-uwf — uwf CLI(thread/workflow 命令)
├── uwf-moderator/ # @uncaged/uwf-moderator — JSONata moderator 引擎
├── uwf-agent-kit/ # @uncaged/uwf-agent-kit — Agent CLI 框架(含 extractor)
├── uwf-agent-hermes/ # @uncaged/uwf-agent-hermes — uwf-hermes CLI
├── uwf-agent-cursor/ # @uncaged/uwf-agent-cursor — uwf-cursor CLI
└── uwf-protocol/ # @uncaged/uwf-protocol — 共享类型定义
├── cli-workflow/ # @uncaged/cli-workflow — uwf CLI(thread/workflow 命令)
├── workflow-moderator/ # @uncaged/workflow-moderator — JSONata moderator 引擎
├── workflow-agent-kit/ # @uncaged/workflow-agent-kit — Agent CLI 框架(含 extractor)
├── workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — uwf-hermes CLI
├── workflow-agent-cursor/ # @uncaged/workflow-agent-cursor — uwf-cursor CLI
└── workflow-protocol/ # @uncaged/workflow-protocol — 共享类型定义
```
**外部依赖:**
@@ -372,7 +381,7 @@ type ThreadId = string;
/** 一个 step 的核心数据,被 StepNode payload 和 JSONata 上下文共享 */
type StepRecord = {
role: string;
output: CasRef; // cas_ref → 结构化输出节点(符合 role outputSchema)
output: CasRef; // cas_ref → 结构化输出节点(符合 role meta schema)
detail: CasRef; // cas_ref → 执行详情(content node / 子 workflow terminal StepNode)
agent: string; // 实际使用的 agent 命令(纯字符串)
};
@@ -383,8 +392,11 @@ type StepRecord = {
```typescript
type RoleDefinition = {
description: string;
systemPrompt: string;
outputSchema: CasRef; // cas_ref → json-cas 内置 JSON Schema 节点
goal: string;
capabilities: string[];
procedure: string;
output: string;
meta: CasRef; // cas_ref → json-cas 内置 JSON Schema 节点
};
type Transition = {
+41
View File
@@ -0,0 +1,41 @@
name: "analyze-topic"
description: "Single-role topic analysis using four-phase role description"
roles:
analyst:
description: "Analyzes a given topic and produces a structured summary"
goal: |
You are a research analyst with expertise in breaking down complex topics
into clear, structured summaries. You think critically and cite key points.
capabilities:
- research
- critical-thinking
- structured-writing
procedure: |
Analyze the topic by:
1. Identifying the main thesis or question
2. Listing 3-5 key points with brief explanations
3. Noting any counterarguments or caveats
Keep your analysis concise (under 500 words).
output: |
Provide your analysis as markdown under the frontmatter.
The frontmatter must include your structured findings.
frontmatter:
type: object
properties:
thesis:
type: string
keyPoints:
type: array
items:
type: string
caveats:
type: string
required: [thesis, keyPoints]
conditions: {}
graph:
$START:
- role: "analyst"
condition: null
analyst:
- role: "$END"
condition: null
+23 -7
View File
@@ -3,8 +3,13 @@ description: "End-to-end issue resolution"
roles:
planner:
description: "Creates implementation plan"
systemPrompt: "You are a planning agent. Analyze the issue and create a step-by-step plan."
outputSchema:
goal: "You are a planning agent. You analyze issues and create step-by-step plans."
capabilities:
- issue-analysis
- planning
procedure: "Analyze the issue and create a detailed, actionable implementation plan."
output: "Output the plan summary and list of concrete steps."
frontmatter:
type: object
properties:
plan:
@@ -16,8 +21,14 @@ roles:
required: [plan, steps]
developer:
description: "Implements code changes"
systemPrompt: "You are a developer agent. Implement the plan."
outputSchema:
goal: "You are a developer agent. You implement code changes according to plans."
capabilities:
- file-edit
- shell
- testing
procedure: "Implement the plan. Write code, tests, and ensure existing tests pass."
output: "List all files changed and provide a summary of the implementation."
frontmatter:
type: object
properties:
filesChanged:
@@ -29,8 +40,13 @@ roles:
required: [filesChanged, summary]
reviewer:
description: "Reviews code changes"
systemPrompt: "You are a code reviewer. Review the implementation."
outputSchema:
goal: "You are a code reviewer. You review implementations for correctness and quality."
capabilities:
- code-review
- static-analysis
procedure: "Review the implementation against the plan. Check for bugs, edge cases, and style."
output: "Approve or reject with detailed comments explaining your decision."
frontmatter:
type: object
properties:
approved:
@@ -41,7 +57,7 @@ roles:
conditions:
notApproved:
description: "Reviewer rejected the implementation"
expression: "steps[-1].output.approved = false"
expression: "$last('reviewer').approved = false"
graph:
$START:
- role: "planner"
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { packageDescriptor } from "../src/package-descriptor.js";
import { createDocxDiffAgent } from "../src/agent.js";
import { packageDescriptor } from "../src/package-descriptor.js";
describe("createDocxDiffAgent", () => {
test("returns an AdapterFn (function)", () => {
@@ -1,8 +1,8 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, mock, test } from "bun:test";
import { ok, err } from "@uncaged/workflow-util";
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { err, ok } from "@uncaged/workflow-util";
import type { SpawnCliConfig } from "@uncaged/workflow-util-agent";
import { runDocxDiff } from "../src/runner.js";
@@ -74,7 +74,12 @@ describe("runDocxDiff", () => {
test("exit 2: throws error", async () => {
const dir = tempDir();
const spawnFn = makeSpawn(
err({ kind: "non_zero_exit", exitCode: 2, stdout: "", stderr: "fatal error" }) as MockSpawnResult,
err({
kind: "non_zero_exit",
exitCode: 2,
stdout: "",
stderr: "fatal error",
}) as MockSpawnResult,
);
await expect(
@@ -1,7 +1,11 @@
{
"name": "@uncaged/workflow-agent-docx-diff",
"version": "0.1.0",
"files": ["src", "dist", "package.json"],
"files": [
"src",
"dist",
"package.json"
],
"type": "module",
"types": "src/index.ts",
"exports": {
@@ -1,7 +1,12 @@
import * as z from "zod/v4";
import { dirname, join } from "node:path";
import type { AdapterFn, RoleResult, ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime";
import type {
AdapterFn,
RoleResult,
ThreadContext,
WorkflowRuntime,
} from "@uncaged/workflow-runtime";
import type { WriterMeta } from "@uncaged/workflow-template-document";
import type * as z from "zod/v4";
import { runDocxDiff } from "./runner.js";
import type { DocxDiffAgentConfig } from "./types.js";
@@ -12,16 +17,10 @@ export function createDocxDiffAgent(config: DocxDiffAgentConfig): AdapterFn {
if (writerStep === undefined) throw new Error("differ: no writer step found");
const writerMeta = writerStep.meta as WriterMeta;
if (writerMeta.mode !== "edit")
throw new Error("differ: writer did not run in edit mode");
if (writerMeta.mode !== "edit") throw new Error("differ: writer did not run in edit mode");
const diffDocx = join(dirname(writerMeta.outputDocx), "diff.docx");
const raw = await runDocxDiff(
config,
writerMeta.sourceDocx,
writerMeta.outputDocx,
diffDocx,
);
const raw = await runDocxDiff(config, writerMeta.sourceDocx, writerMeta.outputDocx, diffDocx);
const meta = schema.parse(JSON.parse(raw)) as T;
return { meta, childThread: null };
@@ -1,6 +1,6 @@
import { stat } from "node:fs/promises";
import { spawnCli } from "@uncaged/workflow-util-agent";
import type { SpawnCliError } from "@uncaged/workflow-util-agent";
import { spawnCli } from "@uncaged/workflow-util-agent";
import type { DocxDiffAgentConfig } from "./types.js";
type SpawnCliFn = typeof spawnCli;
@@ -8,8 +8,7 @@ type SpawnCliFn = typeof spawnCli;
function throwSpawnError(e: SpawnCliError): never {
if (e.kind === "non_zero_exit")
throw new Error(`docx-diff failed (exit ${e.exitCode}): ${e.stderr}`);
if (e.kind === "timeout")
throw new Error("docx-diff: timed out");
if (e.kind === "timeout") throw new Error("docx-diff: timed out");
throw new Error(`docx-diff: spawn failed: ${e.message}`);
}
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { packageDescriptor } from "../src/package-descriptor.js";
import { createOfficeAgent } from "../src/agent.js";
import { packageDescriptor } from "../src/package-descriptor.js";
describe("createOfficeAgent", () => {
test("returns an AdapterFn (function)", () => {
@@ -1,8 +1,8 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, mock, test } from "bun:test";
import { ok, err } from "@uncaged/workflow-util";
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { err, ok } from "@uncaged/workflow-util";
import type { SpawnCliConfig } from "@uncaged/workflow-util-agent";
import { editDocument, generateDocument } from "../src/runner.js";
@@ -123,7 +123,13 @@ describe("editDocument", () => {
);
await expect(
editDocument({ outputDir: base, command: null, timeout: null }, "te2", "edit", inputFile, spawnFn),
editDocument(
{ outputDir: base, command: null, timeout: null },
"te2",
"edit",
inputFile,
spawnFn,
),
).rejects.toThrow("spawn failed");
});
});
@@ -1,7 +1,11 @@
{
"name": "@uncaged/workflow-agent-office",
"version": "0.1.0",
"files": ["src", "dist", "package.json"],
"files": [
"src",
"dist",
"package.json"
],
"type": "module",
"types": "src/index.ts",
"exports": {
@@ -1,6 +1,11 @@
import * as z from "zod/v4";
import type { AdapterFn, RoleResult, ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime";
import type {
AdapterFn,
RoleResult,
ThreadContext,
WorkflowRuntime,
} from "@uncaged/workflow-runtime";
import { createLogger } from "@uncaged/workflow-util";
import type * as z from "zod/v4";
import { editDocument, generateDocument } from "./runner.js";
import type { OfficeAgentConfig } from "./types.js";
@@ -27,7 +32,10 @@ export function createOfficeAgent(config: OfficeAgentConfig): AdapterFn {
return <T>(_systemPrompt: string, schema: z.ZodType<T>) =>
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
const { prompt, inputDocx } = parseStartInput(ctx.start.content);
log("8FQKP3NV", `office-agent: mode=${inputDocx === null ? "generate" : "edit"} thread=${ctx.threadId}`);
log(
"8FQKP3NV",
`office-agent: mode=${inputDocx === null ? "generate" : "edit"} thread=${ctx.threadId}`,
);
let raw: string;
if (inputDocx === null) {
@@ -35,7 +43,11 @@ export function createOfficeAgent(config: OfficeAgentConfig): AdapterFn {
raw = JSON.stringify({ mode: "generate", outputDocx: result.outputDocx, sourceDocx: null });
} else {
const result = await editDocument(config, ctx.threadId, prompt, inputDocx);
raw = JSON.stringify({ mode: "edit", outputDocx: result.outputDocx, sourceDocx: result.sourceDocx });
raw = JSON.stringify({
mode: "edit",
outputDocx: result.outputDocx,
sourceDocx: result.sourceDocx,
});
}
const meta = schema.parse(JSON.parse(raw)) as T;
@@ -1,7 +1,7 @@
import { copyFile, mkdir, stat } from "node:fs/promises";
import { join } from "node:path";
import { spawnCli } from "@uncaged/workflow-util-agent";
import type { SpawnCliError } from "@uncaged/workflow-util-agent";
import { spawnCli } from "@uncaged/workflow-util-agent";
import type { OfficeAgentConfig } from "./types.js";
type SpawnCliFn = typeof spawnCli;
@@ -9,8 +9,7 @@ type SpawnCliFn = typeof spawnCli;
function throwSpawnError(e: SpawnCliError): never {
if (e.kind === "non_zero_exit")
throw new Error(`office-agent failed (exit ${e.exitCode}): ${e.stderr}`);
if (e.kind === "timeout")
throw new Error("office-agent: timed out");
if (e.kind === "timeout") throw new Error("office-agent: timed out");
throw new Error(`office-agent: spawn failed: ${e.message}`);
}
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Workflow Dashboard</title>
<script>
(function () {
(() => {
var t = localStorage.getItem("theme");
if (t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches)) {
document.documentElement.classList.add("dark");
@@ -54,10 +54,14 @@ type CallExpression = {
arguments: Array<AstExpression>;
};
type AstExpression = Identifier | MemberExpression | CallExpression | {
type: string;
[key: string]: unknown;
};
type AstExpression =
| Identifier
| MemberExpression
| CallExpression
| {
type: string;
[key: string]: unknown;
};
type VariableDeclarator = {
id: Identifier | null;
@@ -258,15 +262,21 @@ function createLimitResolver(options: LimitLineOptions): (id: string) => Resolve
}
function shouldProcess(id: string, options: LimitLineOptions): boolean {
return options.include.test(id) && !id.includes("node_modules") && (options.exclude === null || !options.exclude.test(id));
return (
options.include.test(id) &&
!id.includes("node_modules") &&
(options.exclude === null || !options.exclude.test(id))
);
}
// --- Plugin ---
function viteLimitLinePlugin(
userOptions: Partial<LimitLineOptions> = {},
): Array<Plugin> {
const options: LimitLineOptions = { ...DEFAULT_OPTIONS, ...userOptions, overrides: userOptions.overrides ?? [] };
function viteLimitLinePlugin(userOptions: Partial<LimitLineOptions> = {}): Array<Plugin> {
const options: LimitLineOptions = {
...DEFAULT_OPTIONS,
...userOptions,
overrides: userOptions.overrides ?? [],
};
const resolve = createLimitResolver(options);
const rawCodeCache = new Map<string, string>();
@@ -358,5 +368,5 @@ function viteLimitLinePlugin(
];
}
export { viteLimitLinePlugin };
export type { LimitLineOptions, LimitLineOverride };
export { viteLimitLinePlugin };
@@ -55,10 +55,7 @@ export function ResizablePanel({
}, []);
return (
<div
className={cn("relative shrink-0", className)}
style={{ ...style, width }}
>
<div className={cn("relative shrink-0", className)} style={{ ...style, width }}>
{children}
<div
className="absolute top-0 -right-1 w-2 h-full cursor-col-resize z-10 group"
@@ -9,9 +9,7 @@ import type { DocumentMeta } from "../src/roles.js";
const documentModerator = tableToModerator(documentTable);
function makeCtx(
steps: ModeratorContext<DocumentMeta>["steps"],
): ModeratorContext<DocumentMeta> {
function makeCtx(steps: ModeratorContext<DocumentMeta>["steps"]): ModeratorContext<DocumentMeta> {
return {
threadId: "01TEST000000000000000000TR",
depth: 0,
@@ -25,7 +23,11 @@ function writerGenerateStep(): RoleStep<DocumentMeta> {
return {
role: "writer",
contentHash: "STUBHASHWRITER001",
meta: { mode: "generate", outputDocx: "/out/output.docx", sourceDocx: null } satisfies WriterMeta,
meta: {
mode: "generate",
outputDocx: "/out/output.docx",
sourceDocx: null,
} satisfies WriterMeta,
refs: [],
timestamp: 1,
};
@@ -35,7 +37,11 @@ function writerEditStep(): RoleStep<DocumentMeta> {
return {
role: "writer",
contentHash: "STUBHASHWRITER002",
meta: { mode: "edit", outputDocx: "/out/modified.docx", sourceDocx: "/out/original.docx" } satisfies WriterMeta,
meta: {
mode: "edit",
outputDocx: "/out/modified.docx",
sourceDocx: "/out/original.docx",
} satisfies WriterMeta,
refs: [],
timestamp: 1,
};
@@ -1,7 +1,11 @@
{
"name": "@uncaged/workflow-template-document",
"version": "0.1.0",
"files": ["src", "dist", "package.json"],
"files": [
"src",
"dist",
"package.json"
],
"type": "module",
"types": "src/index.ts",
"exports": {
+1 -1
View File
@@ -12,7 +12,7 @@
"test": "bun run --filter '*' test",
"changeset": "bunx changeset",
"version": "bunx changeset version",
"release": "bun run build && bun test && npx changeset publish --no-git-tag"
"release": "bun run build && bun test && node scripts/publish-all.mjs"
},
"devDependencies": {
"@biomejs/biome": "^2.4.14",
@@ -1,6 +1,6 @@
{
"name": "@uncaged/cli-uwf",
"version": "0.1.0",
"name": "@uncaged/cli-workflow",
"version": "0.5.0",
"files": [
"src",
"dist",
@@ -11,11 +11,11 @@
"uwf": "./src/cli.ts"
},
"dependencies": {
"@uncaged/json-cas": "^0.3.0",
"@uncaged/json-cas-fs": "^0.3.0",
"@uncaged/uwf-agent-kit": "workspace:^",
"@uncaged/uwf-moderator": "workspace:^",
"@uncaged/uwf-protocol": "workspace:^",
"@uncaged/json-cas": "^0.4.0",
"@uncaged/json-cas-fs": "^0.4.0",
"@uncaged/workflow-agent-kit": "workspace:^",
"@uncaged/workflow-moderator": "workspace:^",
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util": "workspace:^",
"commander": "^14.0.3",
"dotenv": "^16.6.1",
@@ -0,0 +1,150 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { cmdSetup, validateModel } from "../commands/setup.js";
describe("validateModel", () => {
const BASE_URL = "https://api.example.com/v1";
const API_KEY = "sk-test-key";
const MODEL = "test-model";
afterEach(() => {
vi.restoreAllMocks();
});
test("success path — returns ok on 200", async () => {
const mockFetch = vi
.spyOn(globalThis, "fetch")
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
const result = await validateModel(BASE_URL, API_KEY, MODEL);
expect(result).toEqual({ ok: true, value: undefined });
expect(mockFetch).toHaveBeenCalledOnce();
const [url, opts] = mockFetch.mock.calls[0]!;
expect(url).toBe(`${BASE_URL}/chat/completions`);
expect((opts as RequestInit).headers).toEqual(
expect.objectContaining({ Authorization: `Bearer ${API_KEY}` }),
);
const body = JSON.parse((opts as RequestInit).body as string);
expect(body).toEqual({
model: MODEL,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
});
});
test("HTTP 401 — returns error containing 401", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
);
const result = await validateModel(BASE_URL, API_KEY, MODEL);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain("401");
}
});
test("HTTP 404 — returns error containing 404", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("Not Found", { status: 404, statusText: "Not Found" }),
);
const result = await validateModel(BASE_URL, API_KEY, MODEL);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain("404");
}
});
test("network timeout — returns error mentioning timeout", async () => {
const err = new DOMException("signal timed out", "AbortError");
vi.spyOn(globalThis, "fetch").mockRejectedValue(err);
const result = await validateModel(BASE_URL, API_KEY, MODEL);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.toLowerCase()).toMatch(/timeout|timed out/);
}
});
test("network error (DNS/connection) — returns error mentioning connectivity", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed"));
const result = await validateModel(BASE_URL, API_KEY, MODEL);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.toLowerCase()).toMatch(/connect|reach|network/);
}
});
test("request body correctness", async () => {
const mockFetch = vi
.spyOn(globalThis, "fetch")
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
await validateModel(BASE_URL, API_KEY, "my-special-model");
const body = JSON.parse((mockFetch.mock.calls[0]![1] as RequestInit).body as string);
expect(body).toEqual({
model: "my-special-model",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
});
});
});
describe("cmdSetup with validation", () => {
let storageRoot: string;
beforeEach(async () => {
storageRoot = await mkdtemp(join(tmpdir(), "uwf-setup-validate-"));
});
afterEach(async () => {
vi.restoreAllMocks();
await rm(storageRoot, { recursive: true, force: true });
});
const setupArgs = () => ({
provider: "testprovider",
baseUrl: "https://api.test.com/v1",
apiKey: "sk-test",
model: "test-model",
storageRoot,
});
test("includes validation result on success", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({}), { status: 200 }),
);
const result = await cmdSetup(setupArgs());
expect(result.validation).toEqual({ ok: true, value: undefined });
// Config files should still be written
expect(result.configPath).toBeTruthy();
expect(result.envPath).toBeTruthy();
});
test("includes validation failure — config still saved", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
);
const result = await cmdSetup(setupArgs());
expect(result.validation).toBeDefined();
expect((result.validation as { ok: boolean }).ok).toBe(false);
// Config files should still be written despite validation failure
expect(result.configPath).toBeTruthy();
expect(result.envPath).toBeTruthy();
});
});
@@ -0,0 +1,71 @@
import { execFileSync } from "node:child_process";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
const CLI_PATH = join(import.meta.dirname, "..", "cli.js");
function runCli(args: string[]): { stdout: string; stderr: string; exitCode: number } {
try {
const stdout = execFileSync("bun", ["run", CLI_PATH, ...args], {
encoding: "utf8",
env: { ...process.env, WORKFLOW_STORAGE_ROOT: "/tmp/uwf-test-nonexistent" },
stdio: ["ignore", "pipe", "pipe"],
});
return { stdout, stderr: "", exitCode: 0 };
} catch (e: unknown) {
const err = e as NodeJS.ErrnoException & { stdout?: string; stderr?: string; status?: number };
return {
stdout: err.stdout ?? "",
stderr: err.stderr ?? "",
exitCode: err.status ?? 1,
};
}
}
describe("thread step --count CLI parsing", () => {
test("--help shows -c/--count option", () => {
const result = runCli(["thread", "step", "--help"]);
expect(result.stdout).toContain("--count");
expect(result.stdout).toContain("-c");
});
test("description says 'one or more steps'", () => {
const result = runCli(["thread", "step", "--help"]);
expect(result.stdout).toContain("one or more steps");
});
});
describe("cmdThreadStep count logic", () => {
test("count=0 fails with validation error", () => {
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "0"]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("positive integer");
});
test("negative count fails with validation error", () => {
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "-1"]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("positive integer");
});
test("non-integer count fails with validation error", () => {
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "1.5"]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("positive integer");
});
test("count=1 is the default (no -c flag)", () => {
// Without -c, it should attempt to run 1 step (failing on missing thread, not on count validation)
const result = runCli(["thread", "step", "FAKE_THREAD_ID"]);
expect(result.exitCode).not.toBe(0);
// Should NOT contain "positive integer" error — should fail on thread lookup instead
expect(result.stderr).not.toContain("positive integer");
});
test("count=3 passes validation (fails on thread lookup)", () => {
const result = runCli(["thread", "step", "FAKE_THREAD_ID", "-c", "3"]);
expect(result.exitCode).not.toBe(0);
// Should NOT contain "positive integer" error — should fail on thread/storage lookup
expect(result.stderr).not.toContain("positive integer");
});
});
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { CasRef, ThreadId } from "@uncaged/uwf-protocol";
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
cmdThreadRead,
@@ -211,8 +211,11 @@ describe("cmdThreadRead ### Content section", () => {
roles: {
writer: {
description: "Write",
systemPrompt: "You are a writer.",
outputSchema: "placeholder00" as CasRef,
goal: "You are a writer.",
capabilities: [],
procedure: "Write content as requested.",
output: "Summarize what was written.",
meta: "placeholder00" as CasRef,
},
},
conditions: {},
@@ -1,12 +1,13 @@
#!/usr/bin/env bun
import type { ThreadId } from "@uncaged/uwf-protocol";
import type { ThreadId } from "@uncaged/workflow-protocol";
import { Command } from "commander";
import { stringify as yamlStringify } from "yaml";
import {
cmdCasGet,
cmdCasHas,
cmdCasPut,
cmdCasPutText,
cmdCasRefs,
cmdCasReindex,
cmdCasSchemaGet,
@@ -14,6 +15,7 @@ import {
cmdCasWalk,
} from "./commands/cas.js";
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
import { cmdSkillCli } from "./commands/skill.js";
import {
cmdThreadFork,
cmdThreadKill,
@@ -45,7 +47,12 @@ function runAction(action: () => Promise<void>): void {
const program = new Command();
program.name("uwf").description("Stateless workflow CLI");
// eslint-disable-next-line -- dynamic import for version
const pkg = await import("../package.json", { with: { type: "json" } });
program
.name("uwf")
.description("Stateless workflow CLI")
.version(pkg.default.version, "-V, --version");
program.option("--format <fmt>", "Output format: json or yaml", "json");
const workflow = program.command("workflow").description("Workflow registry and CAS");
@@ -80,7 +87,7 @@ workflow
.action(() => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdWorkflowList(storageRoot);
const result = await cmdWorkflowList(storageRoot, process.cwd());
writeOutput(result);
});
});
@@ -95,22 +102,28 @@ thread
.action((workflow: string, opts: { prompt: string }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadStart(storageRoot, workflow, opts.prompt);
const result = await cmdThreadStart(storageRoot, workflow, opts.prompt, process.cwd());
writeOutput(result);
});
});
thread
.command("step")
.description("Execute one step")
.description("Execute one or more steps")
.argument("<thread-id>", "Thread ULID")
.option("--agent <cmd>", "Override agent command")
.action((threadId: string, opts: { agent: string | undefined }) => {
.option("-c, --count <number>", "Number of steps to run (default: 1)")
.action((threadId: string, opts: { agent: string | undefined; count: string | undefined }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
const agentOverride = opts.agent ?? null;
const result = await cmdThreadStep(storageRoot, threadId, agentOverride);
writeOutput(result);
const count = opts.count !== undefined ? Number(opts.count) : 1;
const results = await cmdThreadStep(storageRoot, threadId, agentOverride, count);
if (results.length === 1) {
writeOutput(results[0]);
} else {
writeOutput(results);
}
});
});
@@ -215,6 +228,15 @@ thread
});
});
const skill = program.command("skill").description("Built-in skill references for agents");
skill
.command("cli")
.description("Print a markdown reference of all uwf commands")
.action(() => {
console.log(cmdSkillCli());
});
program
.command("setup")
.description("Configure provider, model, and agent")
@@ -280,6 +302,17 @@ cas
});
});
cas
.command("put-text")
.description("Store a plain text string, print its hash")
.argument("<text>", "Text content to store")
.action((text: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasPutText(storageRoot, text));
});
});
cas
.command("has")
.description("Check if a hash exists")
@@ -1,10 +1,12 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { Hash, JSONSchema, Store } from "@uncaged/json-cas";
import { bootstrap, getSchema, refs, walk } from "@uncaged/json-cas";
import type { JSONSchema, Store } from "@uncaged/json-cas";
import { bootstrap, getSchema, putSchema, refs, walk } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { TEXT_SCHEMA } from "../schemas.js";
// ---- Helpers ----
function openStore(storageRoot: string): Store {
@@ -53,18 +55,12 @@ export async function cmdCasPut(
return { hash };
}
export async function cmdCasHas(
storageRoot: string,
hash: string,
): Promise<{ exists: boolean }> {
export async function cmdCasHas(storageRoot: string, hash: string): Promise<{ exists: boolean }> {
const store = openStore(storageRoot);
return { exists: store.has(hash) };
}
export async function cmdCasRefs(
storageRoot: string,
hash: string,
): Promise<{ refs: string[] }> {
export async function cmdCasRefs(storageRoot: string, hash: string): Promise<{ refs: string[] }> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
@@ -73,10 +69,7 @@ export async function cmdCasRefs(
return { refs: refs(store, node) };
}
export async function cmdCasWalk(
storageRoot: string,
hash: string,
): Promise<{ hashes: string[] }> {
export async function cmdCasWalk(storageRoot: string, hash: string): Promise<{ hashes: string[] }> {
const store = openStore(storageRoot);
const result: string[] = [];
walk(store, hash, (h) => {
@@ -90,9 +83,7 @@ export type SchemaListEntry = {
title: string;
};
export async function cmdCasSchemaList(
storageRoot: string,
): Promise<SchemaListEntry[]> {
export async function cmdCasSchemaList(storageRoot: string): Promise<SchemaListEntry[]> {
const store = openStore(storageRoot);
const metaHash = await bootstrap(store);
const entries: SchemaListEntry[] = [];
@@ -115,9 +106,7 @@ export async function cmdCasSchemaList(
return entries;
}
export async function cmdCasReindex(
storageRoot: string,
): Promise<{ status: string }> {
export async function cmdCasReindex(storageRoot: string): Promise<{ status: string }> {
const indexDir = join(storageRoot, "cas", "_index");
const { rmSync } = await import("node:fs");
rmSync(indexDir, { recursive: true, force: true });
@@ -126,10 +115,7 @@ export async function cmdCasReindex(
return { status: "reindexed" };
}
export async function cmdCasSchemaGet(
storageRoot: string,
hash: string,
): Promise<unknown> {
export async function cmdCasSchemaGet(storageRoot: string, hash: string): Promise<unknown> {
const store = openStore(storageRoot);
const schema = getSchema(store, hash);
if (schema === null) {
@@ -137,3 +123,10 @@ export async function cmdCasSchemaGet(
}
return schema;
}
export async function cmdCasPutText(storageRoot: string, text: string): Promise<{ hash: string }> {
const store = openStore(storageRoot);
const typeHash = await putSchema(store, TEXT_SCHEMA);
const hash = await store.put(typeHash, text);
return { hash };
}
@@ -1,10 +1,45 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { createInterface } from "node:readline/promises";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";
import type { Result } from "@uncaged/workflow-util";
import { parse, stringify } from "yaml";
import { stringify, parse } from "yaml";
/**
* Send a minimal chat completion request to verify the model is reachable.
* Returns ok on 2xx, error with reason string otherwise.
*/
export async function validateModel(
baseUrl: string,
apiKey: string,
model: string,
): Promise<Result<void, string>> {
try {
const url = `${baseUrl.replace(/\/+$/, "")}/chat/completions`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
}),
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) {
return { ok: false, error: `HTTP ${res.status} ${res.statusText}` };
}
return { ok: true, value: undefined };
} catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") {
return { ok: false, error: "Request timed out — model endpoint unreachable" };
}
return { ok: false, error: `Network error — could not reach endpoint (${String(err)})` };
}
}
/**
* Preset provider list embedded to avoid runtime YAML loading dependency.
@@ -17,10 +52,18 @@ const PRESET_PROVIDERS = [
{ name: "openrouter", label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1" },
{ name: "venice", label: "Venice", baseUrl: "https://api.venice.ai/api/v1" },
// China
{ name: "dashscope", label: "DashScope (Alibaba)", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
{
name: "dashscope",
label: "DashScope (Alibaba)",
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
{ name: "deepseek", label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1" },
{ name: "siliconflow", label: "SiliconFlow", baseUrl: "https://api.siliconflow.cn/v1" },
{ name: "volcengine", label: "Volcengine (ByteDance)", baseUrl: "https://ark.cn-beijing.volces.com/api/v3" },
{
name: "volcengine",
label: "Volcengine (ByteDance)",
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
},
{ name: "kimi", label: "Kimi (Moonshot)", baseUrl: "https://api.moonshot.cn/v1" },
{ name: "glm", label: "GLM (Zhipu AI)", baseUrl: "https://open.bigmodel.cn/api/paas/v4" },
{ name: "stepfun", label: "StepFun", baseUrl: "https://api.stepfun.com/v1" },
@@ -98,21 +141,27 @@ function apiKeyEnvName(providerName: string): string {
* Merge setup args into config.yaml structure. Non-destructive preserves existing entries.
*/
function mergeConfig(existing: Record<string, unknown>, args: SetupArgs): Record<string, unknown> {
const providers = (typeof existing.providers === "object" && existing.providers !== null
? { ...(existing.providers as Record<string, unknown>) }
: {}) as Record<string, unknown>;
const providers = (
typeof existing.providers === "object" && existing.providers !== null
? { ...(existing.providers as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
const envName = apiKeyEnvName(args.provider);
providers[args.provider] = { baseUrl: args.baseUrl, apiKeyEnv: envName };
const models = (typeof existing.models === "object" && existing.models !== null
? { ...(existing.models as Record<string, unknown>) }
: {}) as Record<string, unknown>;
const models = (
typeof existing.models === "object" && existing.models !== null
? { ...(existing.models as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
models.default = { provider: args.provider, name: args.model };
const agents = (typeof existing.agents === "object" && existing.agents !== null
? { ...(existing.agents as Record<string, unknown>) }
: {}) as Record<string, unknown>;
const agents = (
typeof existing.agents === "object" && existing.agents !== null
? { ...(existing.agents as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
const agentName = args.agent ?? "hermes";
if (Object.keys(agents).length === 0) {
@@ -150,12 +199,16 @@ export async function cmdSetup(args: SetupArgs): Promise<Record<string, unknown>
envData[envName] = args.apiKey;
saveEnvFile(envPath, envData);
// Validate model connectivity
const validation = await validateModel(args.baseUrl, args.apiKey, args.model);
return {
configPath,
envPath,
provider: args.provider,
model: args.model,
defaultAgent: merged.defaultAgent,
validation,
};
}
@@ -211,8 +264,12 @@ async function fetchModels(baseUrl: string, apiKey: string): Promise<string[]> {
if (!res.ok) return [];
const body = (await res.json()) as { data?: { id: string }[] };
if (!Array.isArray(body.data)) return [];
const NON_CHAT = /speech|embed|image|video|audio|ocr|rerank|tts|asr|paraformer|sambert|cosyvoice|wordart|wanx|wan2|flux|stable-diffusion|gui-/i;
return body.data.map((m) => m.id).filter((id) => !NON_CHAT.test(id)).sort();
const NON_CHAT =
/speech|embed|image|video|audio|ocr|rerank|tts|asr|paraformer|sambert|cosyvoice|wordart|wanx|wan2|flux|stable-diffusion|gui-/i;
return body.data
.map((m) => m.id)
.filter((id) => !NON_CHAT.test(id))
.sort();
} catch {
return [];
}
@@ -311,7 +368,7 @@ export async function cmdSetupInteractive(storageRoot: string): Promise<Record<s
console.log(`${providerName}/${model}\n`);
await cmdSetup({
const setupResult = await cmdSetup({
provider: providerName,
baseUrl,
apiKey,
@@ -319,6 +376,19 @@ export async function cmdSetupInteractive(storageRoot: string): Promise<Record<s
storageRoot,
});
// Show validation result
if (setupResult.validation && typeof setupResult.validation === "object") {
const v = setupResult.validation as { ok: boolean; error?: string };
if (v.ok) {
console.log("✓ Model verified — connection successful.\n");
} else {
console.log(`\n⚠ Warning: Could not reach model — ${v.error}`);
console.log(
" Config saved, but you may want to try a different model or check your API key.\n",
);
}
}
console.log("Setup complete! Get started:\n");
console.log(" uwf workflow put <workflow.yaml> Register a workflow");
console.log(' uwf thread start <name> -p "..." Start a thread');
@@ -0,0 +1 @@
export { generateCliReference as cmdSkillCli } from "@uncaged/workflow-util";
@@ -1,8 +1,9 @@
import { execFileSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import type { Store as CasStore, JSONSchema } from "@uncaged/json-cas";
import { getSchema, validate } from "@uncaged/json-cas";
import { getEnvPath, loadWorkflowConfig } from "@uncaged/uwf-agent-kit";
import { evaluate } from "@uncaged/uwf-moderator";
import { getEnvPath, loadWorkflowConfig } from "@uncaged/workflow-agent-kit";
import { evaluate } from "@uncaged/workflow-moderator";
import type {
AgentAlias,
AgentConfig,
@@ -21,24 +22,27 @@ import type {
ThreadStepsOutput,
WorkflowConfig,
WorkflowPayload,
} from "@uncaged/uwf-protocol";
} from "@uncaged/workflow-protocol";
import { generateUlid } from "@uncaged/workflow-util";
import { config as loadDotenv } from "dotenv";
import { stringify } from "yaml";
import { parse, stringify } from "yaml";
import {
appendThreadHistory,
createUwfStore,
discoverProjectWorkflows,
findThreadInHistory,
loadThreadHistory,
loadThreadsIndex,
loadWorkflowRegistry,
resolveProjectWorkflowFile,
resolveWorkflowHash,
saveThreadsIndex,
type ThreadHistoryLine,
type UwfStore,
} from "../store.js";
import { isCasRef } from "../validate.js";
import { checkWorkflowFilenameConsistency, isCasRef, parseWorkflowPayload } from "../validate.js";
import { materializeWorkflowPayload } from "./workflow.js";
const END_ROLE = "$END";
export const THREAD_READ_DEFAULT_QUOTA = 4000;
@@ -66,11 +70,55 @@ function fail(message: string): never {
process.exit(1);
}
async function materializeLocalWorkflow(uwf: UwfStore, filePath: string): Promise<CasRef> {
let text: string;
try {
text = await readFile(filePath, "utf8");
} catch {
fail(`project workflow file not found: ${filePath}`);
}
let raw: unknown;
try {
raw = parse(text) as unknown;
} catch (e) {
fail(`invalid YAML in ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
}
const payload = parseWorkflowPayload(raw);
if (payload === null) {
fail(`invalid workflow YAML in ${filePath}: expected WorkflowPayload shape`);
}
const filenameError = checkWorkflowFilenameConsistency(filePath, payload);
if (filenameError !== null) {
fail(filenameError);
}
const materialized = await materializeWorkflowPayload(uwf, payload);
const hash = await uwf.store.put(uwf.schemas.workflow, materialized);
const stored = uwf.store.get(hash);
if (stored === null || !validate(uwf.store, stored)) {
fail("stored local workflow failed schema validation");
}
return hash;
}
async function resolveWorkflowCasRef(
uwf: UwfStore,
storageRoot: string,
workflowId: string,
projectRoot: string,
): Promise<CasRef> {
// Project-local resolution: check .workflows/<workflowId>.yaml first
const localEntries = await discoverProjectWorkflows(projectRoot);
const localFile = resolveProjectWorkflowFile(localEntries, workflowId);
if (localFile !== null) {
return materializeLocalWorkflow(uwf, localFile);
}
// Global registry fallback
const registry = await loadWorkflowRegistry(storageRoot);
const hash = resolveWorkflowHash(registry, workflowId);
if (!isCasRef(hash)) {
@@ -114,9 +162,10 @@ export async function cmdThreadStart(
storageRoot: string,
workflowId: string,
prompt: string,
projectRoot: string,
): Promise<StartOutput> {
const uwf = await createUwfStore(storageRoot);
const workflowHash = await resolveWorkflowCasRef(uwf, storageRoot, workflowId);
const workflowHash = await resolveWorkflowCasRef(uwf, storageRoot, workflowId, projectRoot);
const threadId = generateUlid(Date.now()) as ThreadId;
const startPayload: StartNodePayload = {
@@ -500,7 +549,8 @@ function formatThreadReadMarkdown(options: {
];
const roleDef = workflow.roles[item.payload.role];
if (roleDef) {
stepLines.push("", "### Prompt", "", roleDef.systemPrompt);
const prompt = roleDef.goal;
stepLines.push("", "### Prompt", "", prompt);
}
if (item.payload.detail) {
const content = extractLastAssistantContent(uwf, item.payload.detail);
@@ -623,6 +673,27 @@ export async function cmdThreadStep(
storageRoot: string,
threadId: ThreadId,
agentOverride: string | null,
count: number,
): Promise<StepOutput[]> {
if (count < 1 || !Number.isInteger(count)) {
fail(`--count must be a positive integer, got: ${count}`);
}
const results: StepOutput[] = [];
for (let i = 0; i < count; i++) {
const result = await cmdThreadStepOnce(storageRoot, threadId, agentOverride);
results.push(result);
if (result.done) {
break;
}
}
return results;
}
async function cmdThreadStepOnce(
storageRoot: string,
threadId: ThreadId,
agentOverride: string | null,
): Promise<StepOutput> {
const index = await loadThreadsIndex(storageRoot);
const headHash = index[threadId];
@@ -2,22 +2,31 @@ import { readFile } from "node:fs/promises";
import type { JSONSchema } from "@uncaged/json-cas";
import { putSchema, validate } from "@uncaged/json-cas";
import type { CasRef, RoleDefinition, WorkflowPayload } from "@uncaged/uwf-protocol";
import type {
CasRef,
RoleDefinition,
Transition,
WorkflowPayload,
} from "@uncaged/workflow-protocol";
import { parse } from "yaml";
import {
createUwfStore,
discoverProjectWorkflows,
findRegistryName,
loadWorkflowRegistry,
resolveWorkflowHash,
saveWorkflowRegistry,
type UwfStore,
} from "../store.js";
import { parseWorkflowPayload } from "../validate.js";
import { checkWorkflowFilenameConsistency, parseWorkflowPayload } from "../validate.js";
export type WorkflowOrigin = "local" | "global";
export type WorkflowListEntry = {
name: string;
hash: CasRef;
origin: WorkflowOrigin;
};
export type WorkflowPutOutput = {
@@ -42,35 +51,49 @@ function isJsonSchema(value: unknown): value is JSONSchema {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function resolveOutputSchemaRef(
/** Normalize graph transitions: ensure condition is null (not undefined) for fallback entries. */
function normalizeGraph(graph: Record<string, Transition[]>): Record<string, Transition[]> {
const result: Record<string, Transition[]> = {};
for (const [node, transitions] of Object.entries(graph)) {
result[node] = transitions.map((t) => ({
role: t.role,
condition: t.condition ?? null,
}));
}
return result;
}
async function resolveFrontmatterRef(
uwf: UwfStore,
roleName: string,
outputSchema: unknown,
frontmatter: unknown,
): Promise<CasRef> {
if (!isJsonSchema(outputSchema)) {
fail(`role "${roleName}": outputSchema must be a JSON Schema object`);
if (!isJsonSchema(frontmatter)) {
fail(`role "${roleName}": frontmatter must be a JSON Schema object`);
}
const schema: JSONSchema = outputSchema.title === undefined
? { ...outputSchema, title: roleName }
: outputSchema;
const schema: JSONSchema =
frontmatter.title === undefined ? { ...frontmatter, title: roleName } : frontmatter;
return putSchema(uwf.store, schema);
}
async function materializeWorkflowPayload(
export async function materializeWorkflowPayload(
uwf: UwfStore,
raw: WorkflowPayload,
): Promise<WorkflowPayload> {
const roles: Record<string, RoleDefinition> = {};
for (const [roleName, role] of Object.entries(raw.roles)) {
const outputSchema = await resolveOutputSchemaRef(
const frontmatter = await resolveFrontmatterRef(
uwf,
`${raw.name}.${roleName}`,
role.outputSchema,
role.frontmatter,
);
roles[roleName] = {
description: role.description,
systemPrompt: role.systemPrompt,
outputSchema,
goal: role.goal,
capabilities: role.capabilities,
procedure: role.procedure,
output: role.output,
frontmatter,
};
}
return {
@@ -78,7 +101,7 @@ async function materializeWorkflowPayload(
description: raw.description,
roles,
conditions: raw.conditions,
graph: raw.graph,
graph: normalizeGraph(raw.graph),
};
}
@@ -105,6 +128,11 @@ export async function cmdWorkflowPut(
fail("invalid workflow YAML: expected WorkflowPayload shape");
}
const filenameError = checkWorkflowFilenameConsistency(filePath, payload);
if (filenameError !== null) {
fail(filenameError);
}
const uwf = await createUwfStore(storageRoot);
const materialized = await materializeWorkflowPayload(uwf, payload);
@@ -147,7 +175,26 @@ export async function cmdWorkflowShow(
};
}
export async function cmdWorkflowList(storageRoot: string): Promise<WorkflowListEntry[]> {
export async function cmdWorkflowList(
storageRoot: string,
projectRoot: string,
): Promise<WorkflowListEntry[]> {
const localEntries = await discoverProjectWorkflows(projectRoot);
const registry = await loadWorkflowRegistry(storageRoot);
return Object.entries(registry).map(([name, hash]) => ({ name, hash }));
const result: WorkflowListEntry[] = [];
const localNames = new Set<string>();
for (const entry of localEntries) {
localNames.add(entry.name);
result.push({ name: entry.name, hash: "(local)", origin: "local" });
}
for (const [name, hash] of Object.entries(registry)) {
if (!localNames.has(name)) {
result.push({ name, hash, origin: "global" });
}
}
return result;
}
@@ -1,15 +1,14 @@
import type { Hash, Store } from "@uncaged/json-cas";
import { putSchema } from "@uncaged/json-cas";
import {
START_NODE_SCHEMA,
STEP_NODE_SCHEMA,
WORKFLOW_SCHEMA,
} from "@uncaged/uwf-protocol";
import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@uncaged/workflow-protocol";
export const TEXT_SCHEMA = { type: "string" as const };
export type UwfSchemaHashes = {
workflow: Hash;
startNode: Hash;
stepNode: Hash;
text: Hash;
};
/**
@@ -17,10 +16,11 @@ export type UwfSchemaHashes = {
* Idempotent: safe to call on every CLI invocation.
*/
export async function registerUwfSchemas(store: Store): Promise<UwfSchemaHashes> {
const [workflow, startNode, stepNode] = await Promise.all([
const [workflow, startNode, stepNode, text] = await Promise.all([
putSchema(store, WORKFLOW_SCHEMA),
putSchema(store, START_NODE_SCHEMA),
putSchema(store, STEP_NODE_SCHEMA),
putSchema(store, TEXT_SCHEMA),
]);
return { workflow, startNode, stepNode };
return { workflow, startNode, stepNode, text };
}
@@ -1,16 +1,54 @@
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Hash, Store } from "@uncaged/json-cas";
import type { BootstrapCapableStore, Hash } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { CasRef, ThreadId, ThreadListItem, ThreadsIndex } from "@uncaged/uwf-protocol";
import type { CasRef, ThreadId, ThreadListItem, ThreadsIndex } from "@uncaged/workflow-protocol";
import { parse, stringify } from "yaml";
import { registerUwfSchemas, type UwfSchemaHashes } from "./schemas.js";
export type WorkflowRegistry = Record<string, CasRef>;
/** A workflow entry discovered from the project-local .workflows/ directory. */
export type ProjectWorkflowEntry = {
/** Workflow name (from YAML `name` field, equals filename stem). */
name: string;
/** Absolute path to the YAML file. */
filePath: string;
};
/**
* Scan `<projectRoot>/.workflows/*.yaml` (non-recursive) and return discovered entries.
* Returns an empty array if the directory does not exist.
*/
export async function discoverProjectWorkflows(
projectRoot: string,
): Promise<ProjectWorkflowEntry[]> {
const dir = join(projectRoot, ".workflows");
let entries: string[];
try {
entries = await readdir(dir);
} catch (e) {
const err = e as NodeJS.ErrnoException;
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
return [];
}
throw e;
}
const result: ProjectWorkflowEntry[] = [];
for (const entry of entries) {
if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) {
continue;
}
const stem = entry.endsWith(".yaml") ? entry.slice(0, -5) : entry.slice(0, -4);
result.push({ name: stem, filePath: join(dir, entry) });
}
return result;
}
/** Default filesystem root for uwf data (`~/.uncaged/workflow`). */
export function getDefaultStorageRoot(): string {
return join(homedir(), ".uncaged", "workflow");
@@ -54,7 +92,7 @@ export type ThreadHistoryLine = ThreadListItem & {
export type UwfStore = {
storageRoot: string;
store: Store;
store: BootstrapCapableStore;
schemas: UwfSchemaHashes;
};
@@ -104,6 +142,22 @@ export function resolveWorkflowHash(registry: WorkflowRegistry, id: string): Cas
return registry[id] !== undefined ? registry[id] : id;
}
/**
* Resolve a workflow name to a project-local YAML file path.
* Returns null if the name is not found in the local entries.
*/
export function resolveProjectWorkflowFile(
localEntries: ProjectWorkflowEntry[],
name: string,
): string | null {
for (const entry of localEntries) {
if (entry.name === name) {
return entry.filePath;
}
}
return null;
}
export function findRegistryName(registry: WorkflowRegistry, hash: Hash): string | null {
for (const [name, h] of Object.entries(registry)) {
if (h === hash) {
@@ -1,4 +1,5 @@
import type { CasRef, WorkflowPayload } from "@uncaged/uwf-protocol";
import { basename } from "node:path";
import type { CasRef, WorkflowPayload } from "@uncaged/workflow-protocol";
const CAS_REF_PATTERN = /^[0-9A-HJKMNP-TV-Z]{13}$/;
@@ -14,10 +15,18 @@ function isRoleDefinition(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
const outputSchema = value.outputSchema;
const schemaOk = isRecord(outputSchema) && typeof outputSchema.type === "string";
const frontmatter = value.frontmatter;
const frontmatterOk = isRecord(frontmatter) && typeof frontmatter.type === "string";
const capabilities = value.capabilities;
const capabilitiesOk =
Array.isArray(capabilities) && capabilities.every((c) => typeof c === "string");
return (
typeof value.description === "string" && typeof value.systemPrompt === "string" && schemaOk
typeof value.description === "string" &&
typeof value.goal === "string" &&
capabilitiesOk &&
typeof value.procedure === "string" &&
typeof value.output === "string" &&
frontmatterOk
);
}
@@ -33,7 +42,10 @@ function isTransition(value: unknown): boolean {
return false;
}
const condition = value.condition;
return typeof value.role === "string" && (condition === null || typeof condition === "string");
return (
typeof value.role === "string" &&
(condition === null || condition === undefined || typeof condition === "string")
);
}
function isStringRecord(value: unknown, itemCheck: (item: unknown) => boolean): boolean {
@@ -52,6 +64,33 @@ function isGraph(value: unknown): boolean {
);
}
/**
* Derive the expected workflow name from a file path (stem without extension).
* Returns the stem for `.yaml` / `.yml` files.
*/
export function workflowNameFromPath(filePath: string): string {
const base = basename(filePath);
if (base.endsWith(".yaml")) return base.slice(0, -5);
if (base.endsWith(".yml")) return base.slice(0, -4);
return base;
}
/**
* Check that the `name` field in a parsed payload matches the expected name
* derived from the file path. Returns an error message string on mismatch,
* or null when the names are consistent.
*/
export function checkWorkflowFilenameConsistency(
filePath: string,
payload: WorkflowPayload,
): string | null {
const expected = workflowNameFromPath(filePath);
if (payload.name !== expected) {
return `workflow name mismatch: file "${basename(filePath)}" implies name "${expected}" but YAML declares name "${payload.name}"`;
}
return null;
}
/** Validate YAML-parsed workflow document shape (outputSchema may be inline JSON Schema). */
export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null {
if (!isRecord(raw)) {
@@ -6,8 +6,8 @@
},
"include": ["src"],
"references": [
{ "path": "../uwf-protocol" },
{ "path": "../uwf-moderator" },
{ "path": "../uwf-agent-kit" }
{ "path": "../workflow-protocol" },
{ "path": "../workflow-moderator" },
{ "path": "../workflow-agent-kit" }
]
}
@@ -1,89 +0,0 @@
import { describe, expect, test } from "vitest";
import { buildOutputFormatInstruction } from "../src/build-output-format-instruction.js";
describe("buildOutputFormatInstruction", () => {
test("always includes the frontmatter example block", () => {
const result = buildOutputFormatInstruction({});
expect(result).toContain("---");
expect(result).toContain("status: done");
expect(result).toContain("confidence:");
expect(result).toContain("scope: role");
});
test("always marks frontmatter as the primary deliverable", () => {
const result = buildOutputFormatInstruction({});
expect(result).toContain("primary deliverable");
});
test("lists fields from a flat object schema", () => {
const schema = {
type: "object",
properties: {
status: { type: "string" },
confidence: { type: "number" },
},
};
const result = buildOutputFormatInstruction(schema);
expect(result).toContain("`status`");
expect(result).toContain("`confidence`");
});
test("lists union of fields from an anyOf schema", () => {
const schema = {
anyOf: [
{
type: "object",
properties: { alpha: { type: "string" } },
},
{
type: "object",
properties: { beta: { type: "number" } },
},
],
};
const result = buildOutputFormatInstruction(schema);
expect(result).toContain("`alpha`");
expect(result).toContain("`beta`");
});
test("lists union of fields from a oneOf schema", () => {
const schema = {
oneOf: [
{
type: "object",
properties: { foo: { type: "string" } },
},
{
type: "object",
properties: { bar: { type: "boolean" } },
},
],
};
const result = buildOutputFormatInstruction(schema);
expect(result).toContain("`foo`");
expect(result).toContain("`bar`");
});
test("falls back gracefully for a non-object schema with no properties", () => {
const result = buildOutputFormatInstruction({ type: "string" });
expect(result).toContain("schema fields will be extracted automatically");
});
test("does not list a field more than once for a union with overlapping keys", () => {
const schema = {
anyOf: [
{ type: "object", properties: { shared: { type: "string" } } },
{ type: "object", properties: { shared: { type: "number" } } },
],
};
const result = buildOutputFormatInstruction(schema);
const matches = [...result.matchAll(/`shared`/g)];
expect(matches.length).toBe(1);
});
test("includes focus reminder about role scope", () => {
const result = buildOutputFormatInstruction({});
expect(result).toContain("Focus exclusively on YOUR role");
});
});
@@ -1,75 +0,0 @@
import type { JSONSchema } from "@uncaged/json-cas";
/**
* Extract top-level property names from a JSON Schema object.
*
* Handles:
* - Object schemas with a `properties` key
* - Union schemas via `anyOf` / `oneOf` — union of all variant property names
*
* Returns an empty array for schemas with no inspectable property definitions.
*/
function extractSchemaFields(schema: JSONSchema): string[] {
if (typeof schema.properties === "object" && schema.properties !== null) {
return Object.keys(schema.properties as Record<string, unknown>);
}
const unionKey = Array.isArray(schema.anyOf)
? "anyOf"
: Array.isArray(schema.oneOf)
? "oneOf"
: null;
if (unionKey !== null) {
const variants = schema[unionKey] as JSONSchema[];
const fieldSet = new Set<string>();
for (const variant of variants) {
for (const field of extractSchemaFields(variant)) {
fieldSet.add(field);
}
}
return [...fieldSet];
}
return [];
}
/**
* Build a concise output format instruction block for an agent role.
*
* The instruction describes the expected frontmatter markdown format and lists
* the meta fields derived from the JSON Schema. It is prepended to the agent's
* system prompt so the deliverable format is the first thing the agent sees.
*/
export function buildOutputFormatInstruction(schema: JSONSchema): string {
const fields = extractSchemaFields(schema);
const fieldList =
fields.length > 0
? fields.map((f) => ` - \`${f}\``).join("\n")
: " (schema fields will be extracted automatically)";
return `## Deliverable Format
Your response MUST begin with a YAML frontmatter block followed by your markdown work:
\`\`\`
---
status: done # done | needs_input | in_progress | failed
next: <role-name> # suggested next role, or omit
confidence: 0.9 # 0.0–1.0, your self-assessed confidence
artifacts: # list of file paths or CAS hashes you produced
- path/to/file.ts
scope: role # role | thread
---
... your markdown work here ...
\`\`\`
The frontmatter is the **primary deliverable** — the engine reads it directly.
Your meta output must satisfy these fields:
${fieldList}
Focus exclusively on YOUR role's deliverable. Do not perform actions outside your role's scope.`;
}
-61
View File
@@ -1,61 +0,0 @@
import { validate } from "@uncaged/json-cas";
import type { Store } from "@uncaged/json-cas";
import type { CasRef } from "@uncaged/uwf-protocol";
import { parseFrontmatterMarkdown, validateFrontmatter } from "@uncaged/workflow-util";
export type FrontmatterFastPathResult = {
body: string;
outputHash: CasRef;
};
/**
* Try to satisfy `outputSchema` from frontmatter fields alone.
*
* Returns a result containing the stored CAS hash and stripped body on success,
* or `null` when frontmatter is absent, invalid, or does not satisfy the schema.
* Never throws.
*
* The candidate object is put into the real CAS store (idempotent content-addressed
* write) and validated against the output schema. If validation fails the node
* is orphaned — it will be GC'd on the next collection pass.
*/
export async function tryFrontmatterFastPath(
raw: string,
outputSchema: CasRef,
store: Store,
): Promise<FrontmatterFastPathResult | null> {
const { frontmatter, body } = parseFrontmatterMarkdown(raw);
if (frontmatter === null) {
return null;
}
const validationErrors = validateFrontmatter(frontmatter);
if (validationErrors.length > 0) {
return null;
}
const candidate: Record<string, unknown> = {
status: frontmatter.status,
next: frontmatter.next,
confidence: frontmatter.confidence,
artifacts: [...frontmatter.artifacts],
scope: frontmatter.scope,
};
let outputHash: CasRef;
let node: ReturnType<Store["get"]>;
try {
outputHash = await store.put(outputSchema, candidate);
node = store.get(outputHash);
} catch {
return null;
}
if (node === null || !validate(store, node)) {
return null;
}
return { body, outputHash };
}
@@ -1,120 +0,0 @@
import { describe, expect, test } from "bun:test";
import type { ModeratorContext, WorkflowPayload } from "@uncaged/uwf-protocol";
import { evaluate } from "../src/evaluate.js";
const solveIssueWorkflow: WorkflowPayload = {
name: "solve-issue",
description: "End-to-end issue resolution",
roles: {
planner: {
description: "Creates implementation plan",
systemPrompt: "You are a planning agent...",
outputSchema: "5GWKR8TN1V3JA",
},
developer: {
description: "Implements code changes",
systemPrompt: "You are a developer agent...",
outputSchema: "8CNWT4KR6D1HV",
},
reviewer: {
description: "Reviews code changes",
systemPrompt: "You are a code reviewer...",
outputSchema: "1VPBG9SM5E7WK",
},
},
conditions: {
needsClarification: {
description: "Planner requests clarification from user",
expression: "$exists(steps[-1].output.needsClarification)",
},
notApproved: {
description: "Reviewer rejected the implementation",
expression: "steps[-1].output.approved = false",
},
},
graph: {
$START: [{ role: "planner", condition: null }],
planner: [
{ role: "developer", condition: "needsClarification" },
{ role: "$END", condition: null },
],
developer: [{ role: "reviewer", condition: null }],
reviewer: [
{ role: "developer", condition: "notApproved" },
{ role: "$END", condition: null },
],
},
};
function makeContext(steps: ModeratorContext["steps"]): ModeratorContext {
return {
start: {
workflow: "4KNM2PXR3B1QW",
prompt: "Fix the login bug",
},
steps,
};
}
describe("evaluate", () => {
test("$START → first role (fallback)", async () => {
const result = await evaluate(solveIssueWorkflow, makeContext([]));
expect(result).toEqual({ ok: true, value: "planner" });
});
test("condition match (notApproved → developer)", async () => {
const context = makeContext([
{
role: "reviewer",
output: { approved: false },
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "developer" });
});
test("fallback when condition does not match → $END", async () => {
const context = makeContext([
{
role: "reviewer",
output: { approved: true },
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "$END" });
});
test("missing role in graph → error", async () => {
const context = makeContext([
{
role: "unknown-role",
output: {},
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toBe('no transitions defined for role "unknown-role"');
}
});
test("output expansion in context works with JSONata", async () => {
const context = makeContext([
{
role: "planner",
output: { needsClarification: true },
detail: "7BQST3VW9F2MA",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "developer" });
});
});
-68
View File
@@ -1,68 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/json-cas':
specifier: ^0.3.0
version: 0.3.0
packages:
'@uncaged/json-cas@0.3.0':
resolution: {integrity: sha512-LR8Uow7cBdvH+6y9mh9Fd7zDs8fWhfhpVZVsexfdK1KKnGaR7WvukuhBj6r0FbOZ78j7jhjeEfzsUXR2cHELwQ==}
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
cborg@4.5.8:
resolution: {integrity: sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==}
hasBin: true
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
snapshots:
'@uncaged/json-cas@0.3.0':
dependencies:
ajv: 8.20.0
cborg: 4.5.8
xxhash-wasm: 1.1.0
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.2
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
cborg@4.5.8: {}
fast-deep-equal@3.1.3: {}
fast-uri@3.1.2: {}
json-schema-traverse@1.0.0: {}
require-from-string@2.0.2: {}
xxhash-wasm@1.1.0: {}
@@ -1,6 +1,6 @@
{
"name": "@uncaged/uwf-agent-hermes",
"version": "0.1.0",
"name": "@uncaged/workflow-agent-hermes",
"version": "0.5.0",
"files": [
"src",
"dist",
@@ -21,8 +21,8 @@
"test": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "^0.3.0",
"@uncaged/uwf-agent-kit": "workspace:^"
"@uncaged/json-cas": "^0.4.0",
"@uncaged/workflow-agent-kit": "workspace:^"
},
"devDependencies": {
"typescript": "^5.8.3"
@@ -1,11 +1,16 @@
import { spawn } from "node:child_process";
import type { Store } from "@uncaged/json-cas";
import { type AgentContext, type AgentRunResult, createAgent } from "@uncaged/uwf-agent-kit";
import {
type AgentContext,
type AgentRunResult,
buildRolePrompt,
createAgent,
} from "@uncaged/workflow-agent-kit";
import {
loadHermesSession,
parseSessionIdFromStdout,
storeHermesRawOutput,
storeHermesSessionDetail,
} from "./session-detail.js";
@@ -34,12 +39,12 @@ function buildHistorySummary(steps: AgentContext["steps"]): string {
/** Assemble system prompt, task, and prior step outputs for Hermes. */
export function buildHermesPrompt(ctx: AgentContext): string {
const roleDef = ctx.workflow.roles[ctx.role];
const systemPrompt = roleDef?.systemPrompt ?? "";
const rolePrompt = roleDef !== undefined ? buildRolePrompt(roleDef) : "";
const parts: string[] = [];
if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") {
parts.push(ctx.outputFormatInstruction, "");
}
parts.push(systemPrompt, "", "## Task", ctx.start.prompt);
parts.push(rolePrompt, "", "## Task", ctx.start.prompt);
const historyBlock = buildHistorySummary(ctx.steps);
if (historyBlock !== "") {
parts.push("", historyBlock);
@@ -47,17 +52,8 @@ export function buildHermesPrompt(ctx: AgentContext): string {
return parts.join("\n");
}
function spawnHermesChat(prompt: string): Promise<{ stdout: string; stderr: string }> {
function spawnHermes(args: string[]): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const args = [
"chat",
"-q",
prompt,
"--yolo",
"--max-turns",
String(HERMES_MAX_TURNS),
"--quiet",
];
const child = spawn(HERMES_COMMAND, args, {
env: process.env,
shell: false,
@@ -89,23 +85,73 @@ function spawnHermesChat(prompt: string): Promise<{ stdout: string; stderr: stri
});
}
function spawnHermesChat(prompt: string): Promise<{ stdout: string; stderr: string }> {
return spawnHermes([
"chat",
"-q",
prompt,
"--yolo",
"--max-turns",
String(HERMES_MAX_TURNS),
"--quiet",
]);
}
function spawnHermesResume(
sessionId: string,
message: string,
): Promise<{ stdout: string; stderr: string }> {
return spawnHermes([
"chat",
"--resume",
sessionId,
"-q",
message,
"--yolo",
"--max-turns",
String(HERMES_MAX_TURNS),
"--quiet",
]);
}
function parseSessionId(stdout: string, stderr: string): string {
const sessionId = parseSessionIdFromStdout(stderr) ?? parseSessionIdFromStdout(stdout);
if (sessionId === null) {
throw new Error(
"Failed to parse session_id from hermes output.\n" +
`stderr (first 200 chars): ${stderr.slice(0, 200)}\n` +
`stdout (first 200 chars): ${stdout.slice(0, 200)}`,
);
}
return sessionId;
}
async function buildResultFromSession(sessionId: string, store: Store): Promise<AgentRunResult> {
const session = await loadHermesSession(sessionId);
if (session === null) {
throw new Error(`Failed to load hermes session file for session_id: ${sessionId}`);
}
const { detailHash, output } = await storeHermesSessionDetail(store, session);
return { output, detailHash, sessionId };
}
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
const fullPrompt = buildHermesPrompt(ctx);
const { stdout, stderr } = await spawnHermesChat(fullPrompt);
const { store } = ctx;
const sessionId = parseSessionId(stdout, stderr);
return buildResultFromSession(sessionId, ctx.store);
}
// --quiet mode: session_id may be on stdout or stderr
const sessionId = parseSessionIdFromStdout(stderr) ?? parseSessionIdFromStdout(stdout);
if (sessionId !== null) {
const session = await loadHermesSession(sessionId);
if (session !== null) {
const { detailHash, output } = await storeHermesSessionDetail(store, session);
return { output, detailHash };
}
}
const detailHash = await storeHermesRawOutput(store, stdout);
return { output: stdout, detailHash };
async function continueHermes(
sessionId: string,
message: string,
store: Store,
): Promise<AgentRunResult> {
const { stdout, stderr } = await spawnHermesResume(sessionId, message);
// Resume may return a new session_id
const newSessionId = parseSessionIdFromStdout(stderr) ?? parseSessionIdFromStdout(stdout);
const resolvedId = newSessionId ?? sessionId;
return buildResultFromSession(resolvedId, store);
}
/** Agent CLI factory: parses argv, runs Hermes, extracts output, writes StepNode. */
@@ -113,5 +159,6 @@ export function createHermesAgent(): () => Promise<void> {
return createAgent({
name: "hermes",
run: runHermes,
continue: continueHermes,
});
}
@@ -5,5 +5,5 @@
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../uwf-protocol" }]
"references": [{ "path": "../workflow-agent-kit" }]
}
@@ -0,0 +1,170 @@
import { describe, expect, test } from "vitest";
import { buildOutputFormatInstruction } from "../src/build-output-format-instruction.js";
const PLANNER_SCHEMA = {
type: "object",
properties: {
status: { type: "string", enum: ["ready", "insufficient_info"] },
plan: { type: "string" },
},
required: ["status"],
additionalProperties: false,
};
const REVIEWER_SCHEMA = {
type: "object",
properties: {
approved: { type: "boolean" },
},
required: ["approved"],
additionalProperties: false,
};
describe("buildOutputFormatInstruction", () => {
test("always includes the frontmatter example block", () => {
const result = buildOutputFormatInstruction({});
expect(result).toContain("---");
expect(result).not.toContain("status: done");
expect(result).not.toContain("confidence:");
expect(result).not.toContain("scope: role");
});
test("always marks frontmatter as the primary deliverable", () => {
const result = buildOutputFormatInstruction({});
expect(result).toContain("primary deliverable");
});
test("generates planner-specific YAML example from schema", () => {
const result = buildOutputFormatInstruction(PLANNER_SCHEMA);
expect(result).toContain("status: ready # required | ready | insufficient_info");
expect(result).toContain("plan: <string>");
expect(result).not.toContain("status: done");
expect(result).not.toContain("confidence:");
expect(result).not.toContain("artifacts:");
});
test("generates reviewer-specific YAML example from schema", () => {
const result = buildOutputFormatInstruction(REVIEWER_SCHEMA);
expect(result).toContain("approved: true # required | true | false");
expect(result).not.toContain("status:");
});
test("lists fields from a flat object schema with required marker", () => {
const schema = {
type: "object",
properties: {
status: { type: "string" },
confidence: { type: "number" },
},
required: ["status"],
};
const result = buildOutputFormatInstruction(schema);
expect(result).toContain("`status` (required)");
expect(result).toContain("`confidence`");
expect(result).not.toContain("`confidence` (required)");
expect(result).toContain("status: <string> # required");
expect(result).toContain("confidence: <number>");
});
test("lists union of fields from an anyOf schema", () => {
const schema = {
anyOf: [
{
type: "object",
properties: { alpha: { type: "string" } },
},
{
type: "object",
properties: { beta: { type: "number" } },
},
],
};
const result = buildOutputFormatInstruction(schema);
expect(result).toContain("`alpha`");
expect(result).toContain("`beta`");
expect(result).toContain("alpha: <string>");
expect(result).toContain("beta: <number>");
});
test("lists union of fields from a oneOf schema", () => {
const schema = {
oneOf: [
{
type: "object",
properties: { foo: { type: "string" } },
},
{
type: "object",
properties: { bar: { type: "boolean" } },
},
],
};
const result = buildOutputFormatInstruction(schema);
expect(result).toContain("`foo`");
expect(result).toContain("`bar`");
expect(result).toContain("foo: <string>");
expect(result).toContain("bar: true # true | false");
});
test("falls back gracefully for a non-object schema with no properties", () => {
const result = buildOutputFormatInstruction({ type: "string" });
expect(result).toContain("schema fields will be extracted automatically");
});
test("does not list a field more than once for a union with overlapping keys", () => {
const schema = {
anyOf: [
{ type: "object", properties: { shared: { type: "string" } } },
{ type: "object", properties: { shared: { type: "number" } } },
],
};
const result = buildOutputFormatInstruction(schema);
const matches = [...result.matchAll(/`shared`/g)];
expect(matches.length).toBe(1);
expect(result).toContain("shared: <string>");
});
test("marks required when any union variant requires the field", () => {
const schema = {
anyOf: [
{
type: "object",
properties: { shared: { type: "string" } },
required: ["shared"],
},
{ type: "object", properties: { shared: { type: "number" } } },
],
};
const result = buildOutputFormatInstruction(schema);
expect(result).toContain("`shared` (required)");
expect(result).toContain("shared: <string> # required");
});
test("explicitly forbids extra frontmatter fields", () => {
const result = buildOutputFormatInstruction(PLANNER_SCHEMA);
expect(result).toMatch(/\b(only|exclusively)\b.*fields/i);
expect(result).toMatch(/do not add (extra|additional|other) fields/i);
});
test("forbids extra fields even for empty schema", () => {
const result = buildOutputFormatInstruction({});
expect(result).toMatch(/do not add (extra|additional|other) fields/i);
});
test("forbids extra fields for anyOf/oneOf schemas", () => {
const schema = {
anyOf: [
{ type: "object", properties: { alpha: { type: "string" } } },
{ type: "object", properties: { beta: { type: "number" } } },
],
};
const result = buildOutputFormatInstruction(schema);
expect(result).toMatch(/do not add (extra|additional|other) fields/i);
});
test("includes focus reminder about role scope", () => {
const result = buildOutputFormatInstruction({});
expect(result).toContain("Focus exclusively on YOUR role");
});
});
@@ -0,0 +1,81 @@
import type { RoleDefinition } from "@uncaged/workflow-protocol";
import { describe, expect, test } from "vitest";
import { buildRolePrompt } from "../src/build-role-prompt.js";
describe("buildRolePrompt", () => {
test("all fields present", () => {
const role: RoleDefinition = {
description: "A coder",
goal: "You are a senior developer.",
capabilities: ["cursor-agent", "file-edit"],
procedure: "Implement the feature.",
output: "Summarize changes.",
meta: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toContain("## Goal");
expect(result).toContain("You are a senior developer.");
expect(result).toContain("## Capabilities");
expect(result).toContain("- cursor-agent");
expect(result).toContain("- file-edit");
expect(result).toContain("## Prepare");
expect(result).toContain("uwf CLI Reference");
expect(result).toContain("cursor-agent, file-edit");
expect(result).toContain("## Procedure");
expect(result).toContain("Implement the feature.");
expect(result).toContain("## Output");
expect(result).toContain("Summarize changes.");
});
test("empty fields are omitted but Prepare is always present", () => {
const role: RoleDefinition = {
description: "A reviewer",
goal: "You are a code reviewer.",
capabilities: [],
procedure: "Review the PR diff carefully.",
output: "",
meta: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toContain("## Goal");
expect(result).toContain("## Prepare");
expect(result).toContain("uwf CLI Reference");
expect(result).toContain("## Procedure");
expect(result).not.toContain("## Capabilities");
expect(result).not.toContain("## Output");
});
test("all empty still includes Prepare section", () => {
const role: RoleDefinition = {
description: "Minimal",
goal: "",
capabilities: [],
procedure: "",
output: "",
meta: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toContain("## Prepare");
expect(result).toContain("uwf CLI Reference");
expect(result).not.toContain("## Goal");
expect(result).not.toContain("## Capabilities");
expect(result).not.toContain("## Procedure");
expect(result).not.toContain("## Output");
});
test("capabilities rendered as bullet list", () => {
const role: RoleDefinition = {
description: "Agent",
goal: "",
capabilities: ["search", "code", "browse"],
procedure: "",
output: "",
meta: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toContain("## Capabilities");
expect(result).toContain("- search");
expect(result).toContain("- code");
expect(result).toContain("- browse");
});
});
@@ -1,6 +1,5 @@
import { describe, expect, test } from "vitest";
import { createMemoryStore, putSchema } from "@uncaged/json-cas";
import { describe, expect, test } from "vitest";
import { tryFrontmatterFastPath } from "../src/frontmatter.js";
@@ -30,6 +29,27 @@ const STRICT_SCHEMA = {
additionalProperties: false,
};
/** Role-specific schema (reviewer) — only approved, no standard agent fields. */
const REVIEWER_SCHEMA = {
type: "object",
properties: {
approved: { type: "boolean" },
},
required: ["approved"],
additionalProperties: false,
};
/** Role-specific schema (planner) — custom status enum + plan hash. */
const PLANNER_SCHEMA = {
type: "object",
properties: {
status: { type: "string", enum: ["ready", "insufficient_info"] },
plan: { type: "string" },
},
required: ["status"],
additionalProperties: false,
};
async function makeStoreWithSchema(schema: Record<string, unknown>) {
const store = createMemoryStore();
const schemaHash = await putSchema(store, schema);
@@ -68,7 +88,8 @@ describe("tryFrontmatterFastPath — happy path", () => {
test("stored CAS node payload matches frontmatter fields", async () => {
const { store, schemaHash } = await makeStoreWithSchema(FRONTMATTER_SCHEMA);
const raw = "---\nstatus: done\nnext: null\nconfidence: null\nartifacts: []\nscope: role\n---\n\nBody.";
const raw =
"---\nstatus: done\nnext: null\nconfidence: null\nartifacts: []\nscope: role\n---\n\nBody.";
const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull();
@@ -134,3 +155,48 @@ describe("tryFrontmatterFastPath — fallback: schema mismatch", () => {
expect(result).toBeNull();
});
});
// ── Role-specific schema fields ───────────────────────────────────────────────
describe("tryFrontmatterFastPath — role-specific fields", () => {
test("extracts approved only for reviewer schema (no extra standard fields)", async () => {
const { store, schemaHash } = await makeStoreWithSchema(REVIEWER_SCHEMA);
const raw = "---\napproved: true\n---\n\nReview passed.";
const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull();
const node = store.get(result!.outputHash);
expect(node).not.toBeNull();
const payload = node!.payload as Record<string, unknown>;
expect(payload).toEqual({ approved: true });
expect(payload.status).toBeUndefined();
expect(payload.scope).toBeUndefined();
});
test("extracts plan and role-specific status for planner schema", async () => {
const { store, schemaHash } = await makeStoreWithSchema(PLANNER_SCHEMA);
const raw = "---\nstatus: ready\nplan: 01HASHPLANNER0001\n---\n\nSpec summary.";
const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull();
const node = store.get(result!.outputHash);
expect(node).not.toBeNull();
const payload = node!.payload as Record<string, unknown>;
expect(payload.status).toBe("ready");
expect(payload.plan).toBe("01HASHPLANNER0001");
expect(payload.scope).toBeUndefined();
});
test("returns null when required role-specific field is missing", async () => {
const { store, schemaHash } = await makeStoreWithSchema(REVIEWER_SCHEMA);
const raw = "---\nstatus: done\nscope: role\n---\n\nBody.";
const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).toBeNull();
});
});
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import type { WorkflowConfig } from "@uncaged/uwf-protocol";
import type { WorkflowConfig } from "@uncaged/workflow-protocol";
import { resolveExtractModelAlias } from "../src/extract.js";
function baseConfig(overrides: Partial<WorkflowConfig> = {}): WorkflowConfig {
@@ -1,6 +1,6 @@
{
"name": "@uncaged/uwf-agent-kit",
"version": "0.1.0",
"name": "@uncaged/workflow-agent-kit",
"version": "0.5.0",
"files": [
"src",
"dist",
@@ -18,9 +18,9 @@
"test": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "^0.3.0",
"@uncaged/json-cas-fs": "^0.3.0",
"@uncaged/uwf-protocol": "workspace:^",
"@uncaged/json-cas": "^0.4.0",
"@uncaged/json-cas-fs": "^0.4.0",
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util": "workspace:^",
"dotenv": "^16.6.1",
"yaml": "^2.8.4"
@@ -0,0 +1,197 @@
import type { JSONSchema } from "@uncaged/json-cas";
type SchemaProperty = {
name: string;
schema: JSONSchema;
required: boolean;
};
/**
* Extract top-level property names from a JSON Schema object.
*
* Handles:
* - Object schemas with a `properties` key
* - Union schemas via `anyOf` / `oneOf` — union of all variant property names
*
* Returns an empty array for schemas with no inspectable property definitions.
*/
export function extractSchemaFields(schema: JSONSchema): string[] {
return extractSchemaProperties(schema).map((p) => p.name);
}
function extractSchemaProperties(schema: JSONSchema): SchemaProperty[] {
const objectSchemas = collectObjectSchemas(schema);
if (objectSchemas.length === 0) {
return [];
}
const byName = new Map<string, SchemaProperty>();
for (const objectSchema of objectSchemas) {
const requiredSet = new Set(
Array.isArray(objectSchema.required) ? (objectSchema.required as string[]) : [],
);
const properties = objectSchema.properties as Record<string, JSONSchema> | null | undefined;
if (typeof properties !== "object" || properties === null) {
continue;
}
for (const [name, propSchema] of Object.entries(properties)) {
const required = requiredSet.has(name);
const existing = byName.get(name);
if (existing === undefined) {
byName.set(name, { name, schema: propSchema, required });
} else if (required) {
byName.set(name, { ...existing, required: true });
}
}
}
return [...byName.values()];
}
function collectObjectSchemas(schema: JSONSchema): JSONSchema[] {
if (typeof schema.properties === "object" && schema.properties !== null) {
return [schema];
}
const unionKey = Array.isArray(schema.anyOf)
? "anyOf"
: Array.isArray(schema.oneOf)
? "oneOf"
: null;
if (unionKey === null) {
return [];
}
const variants = schema[unionKey] as JSONSchema[];
const result: JSONSchema[] = [];
for (const variant of variants) {
result.push(...collectObjectSchemas(variant));
}
return result;
}
function resolvePropertySchema(prop: JSONSchema): JSONSchema {
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
return prop;
}
const unionKey = Array.isArray(prop.anyOf) ? "anyOf" : Array.isArray(prop.oneOf) ? "oneOf" : null;
if (unionKey !== null) {
const variants = prop[unionKey] as JSONSchema[];
const nonNull = variants.filter((v) => v.type !== "null");
if (nonNull.length === 1) {
return nonNull[0];
}
}
return prop;
}
function formatYamlScalar(value: unknown): string {
if (typeof value === "boolean") {
return String(value);
}
if (typeof value === "number") {
return String(value);
}
return String(value);
}
function buildPropertyComment(parts: string[]): string {
const filtered = parts.filter((p) => p.length > 0);
return filtered.length > 0 ? ` # ${filtered.join(" | ")}` : "";
}
function buildPropertyExampleLine(prop: SchemaProperty): string {
const resolved = resolvePropertySchema(prop.schema);
const commentParts: string[] = [];
if (prop.required) {
commentParts.push("required");
}
if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
const enumValues = resolved.enum.map((v) => String(v));
commentParts.push(...enumValues);
const first = resolved.enum[0];
return `${prop.name}: ${formatYamlScalar(first)}${buildPropertyComment(commentParts)}`;
}
if (resolved.type === "boolean") {
commentParts.push("true", "false");
return `${prop.name}: true${buildPropertyComment(commentParts)}`;
}
if (resolved.type === "string") {
return `${prop.name}: <string>${buildPropertyComment(commentParts)}`;
}
if (resolved.type === "number" || resolved.type === "integer") {
return `${prop.name}: <number>${buildPropertyComment(commentParts)}`;
}
if (resolved.type === "array") {
return `${prop.name}:\n - <item>${buildPropertyComment(commentParts)}`;
}
if (resolved.type === "object") {
return `${prop.name}: <object>${buildPropertyComment(commentParts)}`;
}
return `${prop.name}: <value>${buildPropertyComment(commentParts)}`;
}
function buildYamlExampleBlock(properties: SchemaProperty[]): string {
if (properties.length === 0) {
return "---\n\n... your markdown work here ...";
}
const lines = properties.map((p) => buildPropertyExampleLine(p));
return `---\n${lines.join("\n")}\n---\n\n... your markdown work here ...`;
}
function buildFieldList(properties: SchemaProperty[]): string {
if (properties.length === 0) {
return " (schema fields will be extracted automatically)";
}
return properties
.map((p) => {
const suffix = p.required ? " (required)" : "";
return ` - \`${p.name}\`${suffix}`;
})
.join("\n");
}
/**
* Build a concise output format instruction block for an agent role.
*
* The instruction describes the expected frontmatter markdown format and lists
* the meta fields derived from the JSON Schema. It is prepended to the agent's
* system prompt so the deliverable format is the first thing the agent sees.
*/
export function buildOutputFormatInstruction(schema: JSONSchema): string {
const properties = extractSchemaProperties(schema);
const yamlExample = buildYamlExampleBlock(properties);
const fieldList = buildFieldList(properties);
return `## Deliverable Format
Your response MUST begin with a YAML frontmatter block followed by your markdown work:
\`\`\`
${yamlExample}
\`\`\`
The frontmatter is the **primary deliverable** — the engine reads it directly.
Your meta output must satisfy these fields:
${fieldList}
Output ONLY the fields listed above. Do not add extra fields that are not specified in the schema.
Focus exclusively on YOUR role's deliverable. Do not perform actions outside your role's scope.`;
}
@@ -0,0 +1,44 @@
import type { RoleDefinition } from "@uncaged/workflow-protocol";
import { generateCliReference } from "@uncaged/workflow-util";
/**
* Build the role prompt from a RoleDefinition.
*
* Assembles structured sections: Goal, Capabilities, Prepare, Procedure, Output.
* Empty strings and empty arrays are omitted from the output.
*
* The Prepare section always inlines the uwf CLI reference so the agent has
* workflow knowledge without needing to run an external command. The capabilities
* array is rendered as keyword hints for implicit skill loading.
*/
export function buildRolePrompt(role: RoleDefinition): string {
const sections: string[] = [];
if (role.goal !== "") {
sections.push(`## Goal\n\n${role.goal}`);
}
if (role.capabilities.length > 0) {
const list = role.capabilities.map((c) => `- ${c}`).join("\n");
sections.push(`## Capabilities\n\n${list}`);
}
const prepareLines: string[] = [generateCliReference()];
if (role.capabilities.length > 0) {
const keywords = role.capabilities.join(", ");
prepareLines.push(
`You have the following capabilities: ${keywords}. Load relevant skills matching these keywords before starting work.`,
);
}
sections.push(`## Prepare\n\n${prepareLines.join("\n\n")}`);
if (role.procedure !== "") {
sections.push(`## Procedure\n\n${role.procedure}`);
}
if (role.output !== "") {
sections.push(`## Output\n\n${role.output}`);
}
return sections.join("\n\n");
}
@@ -5,9 +5,9 @@ import type {
StepContext,
StepNodePayload,
ThreadId,
} from "@uncaged/uwf-protocol";
import { createAgentStore, loadThreadsIndex, resolveStorageRoot } from "./storage.js";
} from "@uncaged/workflow-protocol";
import type { AgentStore } from "./storage.js";
import { createAgentStore, loadThreadsIndex, resolveStorageRoot } from "./storage.js";
import type { AgentContext } from "./types.js";
type ChainState = {
@@ -21,11 +21,7 @@ function fail(message: string): never {
throw new Error(message);
}
function walkChain(
store: Store,
schemas: AgentStore["schemas"],
headHash: CasRef,
): ChainState {
function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRef): ChainState {
const headNode = store.get(headHash);
if (headNode === null) {
fail(`CAS node not found: ${headHash}`);
@@ -78,10 +74,7 @@ function walkChain(
};
}
function expandOutput(
store: Store,
outputRef: CasRef,
): unknown {
function expandOutput(store: Store, outputRef: CasRef): unknown {
const node = store.get(outputRef);
if (node === null) {
return {};
@@ -106,11 +99,7 @@ async function buildHistory(
return history;
}
async function loadWorkflow(
store: Store,
schemas: AgentStore["schemas"],
workflowRef: CasRef,
) {
async function loadWorkflow(store: Store, schemas: AgentStore["schemas"], workflowRef: CasRef) {
const node = store.get(workflowRef);
if (node === null) {
fail(`workflow CAS node not found: ${workflowRef}`);
@@ -1,6 +1,6 @@
import { getSchema, validate } from "@uncaged/json-cas";
import type { CasRef, ModelAlias, WorkflowConfig } from "@uncaged/uwf-protocol";
import type { CasRef, ModelAlias, WorkflowConfig } from "@uncaged/workflow-protocol";
import { config as loadDotenv } from "dotenv";
import { createAgentStore, getEnvPath, resolveStorageRoot } from "./storage.js";
@@ -0,0 +1,195 @@
import type { Store } from "@uncaged/json-cas";
import { getSchema, validate } from "@uncaged/json-cas";
import type { CasRef } from "@uncaged/workflow-protocol";
import {
type AgentFrontmatter,
createLogger,
parseFrontmatterMarkdown,
validateFrontmatter,
} from "@uncaged/workflow-util";
import { parse as parseYaml } from "yaml";
import { extractSchemaFields } from "./build-output-format-instruction.js";
const log = createLogger({ sink: { kind: "stderr" } });
const STANDARD_KEYS = ["status", "next", "confidence", "artifacts", "scope"] as const;
type StandardKey = (typeof STANDARD_KEYS)[number];
export type FrontmatterFastPathResult = {
body: string;
outputHash: CasRef;
};
function extractYamlBlock(raw: string): string | null {
const fence = "---";
if (!raw.startsWith(fence)) {
return null;
}
const rest = raw.slice(fence.length);
if (rest.length > 0 && rest[0] !== "\n" && rest[0] !== "\r") {
return null;
}
const afterOpen = rest.startsWith("\n") ? rest.slice(1) : rest;
const closeIndex = afterOpen.indexOf(`\n${fence}`);
if (closeIndex === -1) {
return null;
}
return afterOpen.slice(0, closeIndex);
}
function parseRawFrontmatterFields(raw: string): Record<string, unknown> {
const yamlText = extractYamlBlock(raw);
if (yamlText === null) {
return {};
}
try {
const parsed = parseYaml(yamlText);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
return {};
}
return parsed as Record<string, unknown>;
} catch {
return {};
}
}
function defaultCandidate(frontmatter: AgentFrontmatter): Record<string, unknown> {
return {
status: frontmatter.status,
next: frontmatter.next,
confidence: frontmatter.confidence,
artifacts: [...frontmatter.artifacts],
scope: frontmatter.scope,
};
}
function pickStandardField(frontmatter: AgentFrontmatter, key: StandardKey): unknown {
switch (key) {
case "status":
return frontmatter.status;
case "next":
return frontmatter.next;
case "confidence":
return frontmatter.confidence;
case "artifacts":
return [...frontmatter.artifacts];
case "scope":
return frontmatter.scope;
}
}
function isStandardKey(key: string): key is StandardKey {
return (STANDARD_KEYS as readonly string[]).includes(key);
}
function pickFieldValue(
field: string,
frontmatter: AgentFrontmatter,
rawFields: Record<string, unknown>,
): unknown | undefined {
if (!isStandardKey(field)) {
return Object.hasOwn(rawFields, field) ? rawFields[field] : undefined;
}
const coerced = pickStandardField(frontmatter, field);
if (field === "artifacts" || field === "scope") {
return coerced;
}
if (coerced !== null) {
return coerced;
}
return Object.hasOwn(rawFields, field) ? rawFields[field] : coerced;
}
/**
* Build a CAS candidate object from schema property keys and parsed frontmatter.
*
* When the schema has no inspectable properties, falls back to the five standard
* agent frontmatter fields for backward compatibility.
*/
function buildCandidate(
frontmatter: AgentFrontmatter,
rawFields: Record<string, unknown>,
schemaFields: string[],
): Record<string, unknown> {
if (schemaFields.length === 0) {
return defaultCandidate(frontmatter);
}
const candidate: Record<string, unknown> = {};
for (const field of schemaFields) {
const value = pickFieldValue(field, frontmatter, rawFields);
if (value !== undefined) {
candidate[field] = value;
}
}
return candidate;
}
/**
* Try to satisfy `outputSchema` from frontmatter fields alone.
*
* Returns a result containing the stored CAS hash and stripped body on success,
* or `null` when frontmatter is absent, invalid, or does not satisfy the schema.
* Never throws.
*
* The candidate object is put into the real CAS store (idempotent content-addressed
* write) and validated against the output schema. If validation fails the node
* is orphaned — it will be GC'd on the next collection pass.
*/
export async function tryFrontmatterFastPath(
raw: string,
outputSchema: CasRef,
store: Store,
): Promise<FrontmatterFastPathResult | null> {
const { frontmatter, body } = parseFrontmatterMarkdown(raw);
if (frontmatter === null) {
return null;
}
const validationErrors = validateFrontmatter(frontmatter);
if (validationErrors.length > 0) {
log(
"9GNPS4WY",
`frontmatter validation errors: ${validationErrors.map((e) => e.message).join("; ")}`,
);
return null;
}
const schema = getSchema(store, outputSchema);
if (schema === null) {
log("8FHMR2QX", `output schema not found in CAS: ${outputSchema}`);
return null;
}
const schemaFields = extractSchemaFields(schema);
const rawFields = parseRawFrontmatterFields(raw);
const candidate = buildCandidate(frontmatter, rawFields, schemaFields);
let outputHash: CasRef;
let node: ReturnType<Store["get"]>;
try {
outputHash = await store.put(outputSchema, candidate);
node = store.get(outputHash);
} catch {
log("2KMQT7NR", "failed to store frontmatter candidate in CAS");
return null;
}
if (node === null || !validate(store, node)) {
log("2KMQT7NR", "stored frontmatter candidate failed schema validation");
return null;
}
return { body, outputHash };
}
@@ -1,3 +1,5 @@
export { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
export { buildRolePrompt } from "./build-role-prompt.js";
export type { BuildContextMeta } from "./context.js";
export { buildContext, buildContextWithMeta } from "./context.js";
export type { ExtractResult, ResolvedLlmProvider } from "./extract.js";
@@ -6,9 +8,14 @@ export {
resolveExtractModelAlias,
resolveModel,
} from "./extract.js";
export { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
export type { FrontmatterFastPathResult } from "./frontmatter.js";
export { tryFrontmatterFastPath } from "./frontmatter.js";
export { createAgent } from "./run.js";
export { getConfigPath, getEnvPath, loadWorkflowConfig } from "./storage.js";
export type { AgentContext, AgentOptions, AgentRunFn, AgentRunResult } from "./types.js";
export type {
AgentContext,
AgentContinueFn,
AgentOptions,
AgentRunFn,
AgentRunResult,
} from "./types.js";
@@ -1,14 +1,14 @@
import { getSchema, validate } from "@uncaged/json-cas";
import type { CasRef, StepNodePayload, ThreadId } from "@uncaged/uwf-protocol";
import type { CasRef, StepNodePayload, ThreadId } from "@uncaged/workflow-protocol";
import { config as loadDotenv } from "dotenv";
import { buildContextWithMeta } from "./context.js";
import { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
import { extract } from "./extract.js";
import { buildContextWithMeta } from "./context.js";
import { tryFrontmatterFastPath } from "./frontmatter.js";
import type { AgentStore } from "./storage.js";
import { getEnvPath, loadWorkflowConfig, resolveStorageRoot } from "./storage.js";
import type { AgentContext, AgentOptions, AgentRunResult } from "./types.js";
import { getEnvPath, resolveStorageRoot } from "./storage.js";
import type { AgentOptions } from "./types.js";
const MAX_FRONTMATTER_RETRIES = 2;
function fail(message: string): never {
process.stderr.write(`${message}\n`);
@@ -67,31 +67,16 @@ async function writeStepNode(options: {
return hash;
}
async function runAgent(options: AgentOptions, ctx: AgentContext): Promise<AgentRunResult> {
return runWithMessage("agent run failed", () => options.run(ctx));
}
async function extractOutput(
async function tryExtractOutput(
rawOutput: string,
outputSchema: CasRef,
storageRoot: string,
ctx: Awaited<ReturnType<typeof buildContextWithMeta>>,
): Promise<CasRef> {
const fastPath = await runWithMessage("frontmatter fast path", () =>
tryFrontmatterFastPath(rawOutput, outputSchema, ctx.meta.store),
).catch(() => null);
): Promise<CasRef | null> {
const fastPath = await tryFrontmatterFastPath(rawOutput, outputSchema, ctx.meta.store);
if (fastPath !== null) {
return fastPath.outputHash;
}
const config = await runWithMessage("failed to load config", () =>
loadWorkflowConfig(storageRoot),
);
const extracted = await runWithMessage("extract failed", () =>
extract(rawOutput, outputSchema, config),
);
return extracted.hash;
return null;
}
async function persistStep(options: {
@@ -113,11 +98,6 @@ async function persistStep(options: {
});
}
/**
* Create an agent CLI entrypoint.
* Parses argv (`<thread-id> <role>`), runs the agent, extracts structured output,
* writes StepNode to CAS, and prints the new node hash to stdout.
*/
export function createAgent(options: AgentOptions): () => Promise<void> {
return async function main(): Promise<void> {
const { threadId, role } = parseArgv(process.argv);
@@ -131,13 +111,36 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
fail(`unknown role: ${role}`);
}
const outputSchema = getSchema(ctx.meta.store, roleDef.outputSchema);
if (outputSchema !== null) {
ctx.outputFormatInstruction = buildOutputFormatInstruction(outputSchema);
const frontmatterSchema = getSchema(ctx.meta.store, roleDef.frontmatter);
if (frontmatterSchema !== null) {
ctx.outputFormatInstruction = buildOutputFormatInstruction(frontmatterSchema);
}
let agentResult = await runWithMessage("agent run failed", () => options.run(ctx));
// Try to extract frontmatter; retry via continue if it fails
let outputHash = await tryExtractOutput(agentResult.output, roleDef.frontmatter, ctx);
for (let retry = 0; retry < MAX_FRONTMATTER_RETRIES && outputHash === null; retry++) {
const correctionMessage =
"Your previous response did not contain valid YAML frontmatter matching the role schema.\n" +
"You MUST begin your response with a YAML frontmatter block (--- delimited).\n" +
"Please output ONLY the corrected frontmatter block followed by your work.";
agentResult = await runWithMessage("agent continue failed", () =>
options.continue(agentResult.sessionId, correctionMessage, ctx.meta.store),
);
outputHash = await tryExtractOutput(agentResult.output, roleDef.frontmatter, ctx);
}
if (outputHash === null) {
fail(
"Agent output does not contain valid YAML frontmatter matching the role schema " +
`after ${MAX_FRONTMATTER_RETRIES} retries.\n` +
`Raw output (first 500 chars): ${agentResult.output.slice(0, 500)}`,
);
}
const agentResult = await runAgent(options, ctx);
const outputHash = await extractOutput(agentResult.output, roleDef.outputSchema, storageRoot, ctx);
const stepHash = await persistStep({
ctx,
outputHash,
@@ -1,6 +1,6 @@
import type { Hash, Store } from "@uncaged/json-cas";
import { putSchema } from "@uncaged/json-cas";
import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@uncaged/uwf-protocol";
import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@uncaged/workflow-protocol";
export type UwfAgentSchemaHashes = {
workflow: Hash;
@@ -16,7 +16,7 @@ import type {
ThreadsIndex,
WorkflowConfig,
WorkflowName,
} from "@uncaged/uwf-protocol";
} from "@uncaged/workflow-protocol";
import { parse } from "yaml";
import { registerAgentSchemas } from "./schemas.js";
@@ -1,5 +1,5 @@
import type { Store } from "@uncaged/json-cas";
import type { ModeratorContext, ThreadId, WorkflowPayload } from "@uncaged/uwf-protocol";
import type { ModeratorContext, ThreadId, WorkflowPayload } from "@uncaged/workflow-protocol";
export type AgentContext = ModeratorContext & {
threadId: ThreadId;
@@ -7,7 +7,7 @@ export type AgentContext = ModeratorContext & {
store: Store;
workflow: WorkflowPayload;
/**
* Prepend to the role's systemPrompt when building the agent prompt.
* Prepend to the role's prompt when building the agent prompt.
* Contains the frontmatter deliverable format instruction derived from the
* role's output schema. Populated by `createAgent` at run time.
*/
@@ -17,11 +17,19 @@ export type AgentContext = ModeratorContext & {
export type AgentRunResult = {
output: string;
detailHash: string;
sessionId: string;
};
export type AgentContinueFn = (
sessionId: string,
message: string,
store: AgentContext["store"],
) => Promise<AgentRunResult>;
export type AgentRunFn = (ctx: AgentContext) => Promise<AgentRunResult>;
export type AgentOptions = {
name: string;
run: AgentRunFn;
continue: AgentContinueFn;
};
@@ -5,5 +5,5 @@
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../uwf-agent-kit" }]
"references": [{ "path": "../workflow-protocol" }]
}
@@ -0,0 +1,241 @@
import { describe, expect, test } from "bun:test";
import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol";
import { evaluate } from "../src/evaluate.js";
const solveIssueWorkflow: WorkflowPayload = {
name: "solve-issue",
description: "End-to-end issue resolution",
roles: {
planner: {
description: "Creates implementation plan",
identity: "You are a planning agent.",
prepare: "Review the issue context.",
execute: "Create a step-by-step plan.",
report: "Output the plan and steps.",
outputSchema: "5GWKR8TN1V3JA",
},
developer: {
description: "Implements code changes",
identity: "You are a developer agent.",
prepare: "Load coding tools.",
execute: "Implement the plan.",
report: "List files changed and summary.",
outputSchema: "8CNWT4KR6D1HV",
},
reviewer: {
description: "Reviews code changes",
identity: "You are a code reviewer.",
prepare: "Review project conventions.",
execute: "Review the implementation.",
report: "Approve or reject with comments.",
outputSchema: "1VPBG9SM5E7WK",
},
},
conditions: {
needsClarification: {
description: "Planner requests clarification from user",
expression: "$exists($last('planner').needsClarification)",
},
rejected: {
description: "Reviewer rejected the implementation",
expression: "$last('reviewer').approved = false",
},
},
graph: {
$START: [{ role: "planner", condition: null }],
planner: [
{ role: "developer", condition: "needsClarification" },
{ role: "$END", condition: null },
],
developer: [{ role: "reviewer", condition: null }],
reviewer: [
{ role: "developer", condition: "rejected" },
{ role: "$END", condition: null },
],
},
};
function makeContext(steps: ModeratorContext["steps"]): ModeratorContext {
return {
start: {
workflow: "4KNM2PXR3B1QW",
prompt: "Fix the login bug",
},
steps,
};
}
describe("evaluate", () => {
test("$START → first role (fallback)", async () => {
const result = await evaluate(solveIssueWorkflow, makeContext([]));
expect(result).toEqual({ ok: true, value: "planner" });
});
test("condition match (rejected → developer)", async () => {
const context = makeContext([
{
role: "reviewer",
output: { approved: false },
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "developer" });
});
test("fallback when condition does not match → $END", async () => {
const context = makeContext([
{
role: "reviewer",
output: { approved: true },
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "$END" });
});
test("missing role in graph → error", async () => {
const context = makeContext([
{
role: "unknown-role",
output: {},
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toBe('no transitions defined for role "unknown-role"');
}
});
test("output expansion in context works with JSONata", async () => {
const context = makeContext([
{
role: "planner",
output: { needsClarification: true },
detail: "7BQST3VW9F2MA",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ ok: true, value: "developer" });
});
test("$last returns most recent matching role's frontmatter", async () => {
const workflow: WorkflowPayload = {
...solveIssueWorkflow,
conditions: {
devFailed: {
description: "Developer failed",
expression: "$last('developer').status = 'failed'",
},
},
graph: {
$START: [{ role: "developer", condition: null }],
developer: [
{ role: "$END", condition: "devFailed" },
{ role: "reviewer", condition: null },
],
},
};
const context = makeContext([
{
role: "developer",
output: { status: "done" },
detail: "1VPBG9SM5E7WK",
agent: "uwf-hermes",
},
{
role: "reviewer",
output: { approved: false },
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
{
role: "developer",
output: { status: "failed" },
detail: "3QNTH7WK8D2PA",
agent: "uwf-hermes",
},
]);
const result = await evaluate(workflow, context);
expect(result).toEqual({ ok: true, value: "$END" });
});
test("$first returns earliest matching role's frontmatter", async () => {
const workflow: WorkflowPayload = {
...solveIssueWorkflow,
conditions: {
firstPlanReady: {
description: "First planner run was ready",
expression: "$first('planner').status = 'ready'",
},
},
graph: {
$START: [{ role: "planner", condition: null }],
planner: [
{ role: "$END", condition: "firstPlanReady" },
{ role: "developer", condition: null },
],
},
};
const context = makeContext([
{
role: "planner",
output: { status: "ready", plan: "ABC123" },
detail: "7BQST3VW9F2MA",
agent: "uwf-hermes",
},
{
role: "developer",
output: { status: "done" },
detail: "1VPBG9SM5E7WK",
agent: "uwf-hermes",
},
{
role: "planner",
output: { status: "revised", plan: "DEF456" },
detail: "4RNMK6PX8B3WQ",
agent: "uwf-hermes",
},
]);
const result = await evaluate(workflow, context);
expect(result).toEqual({ ok: true, value: "$END" });
});
test("$last returns undefined for unmatched role", async () => {
const workflow: WorkflowPayload = {
...solveIssueWorkflow,
conditions: {
hasReviewer: {
description: "Reviewer has run",
expression: "$exists($last('reviewer'))",
},
},
graph: {
$START: [{ role: "planner", condition: null }],
planner: [
{ role: "$END", condition: "hasReviewer" },
{ role: "developer", condition: null },
],
},
};
const context = makeContext([
{
role: "planner",
output: { status: "ready" },
detail: "7BQST3VW9F2MA",
agent: "uwf-hermes",
},
]);
const result = await evaluate(workflow, context);
// no reviewer step → $exists returns false → fallback to developer
expect(result).toEqual({ ok: true, value: "developer" });
});
});
@@ -1,6 +1,6 @@
{
"name": "@uncaged/uwf-moderator",
"version": "0.1.0",
"name": "@uncaged/workflow-moderator",
"version": "0.5.0",
"files": [
"src",
"dist",
@@ -18,7 +18,7 @@
"test": "bun test"
},
"dependencies": {
"@uncaged/uwf-protocol": "workspace:^",
"@uncaged/workflow-protocol": "workspace:^",
"jsonata": "^1.8.7"
},
"devDependencies": {
@@ -1,4 +1,4 @@
import type { ModeratorContext, WorkflowPayload } from "@uncaged/uwf-protocol";
import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol";
import jsonata from "jsonata";
import type { Result } from "./types.js";
@@ -21,9 +21,44 @@ function isTruthy(value: unknown): boolean {
return true;
}
async function evaluateJsonata(expression: string, context: ModeratorContext): Promise<Result<unknown, Error>> {
function findByRole(
steps: ModeratorContext["steps"],
role: string,
direction: "first" | "last",
): unknown {
if (direction === "last") {
for (let i = steps.length - 1; i >= 0; i--) {
if (steps[i].role === role) {
return steps[i].output;
}
}
} else {
for (const step of steps) {
if (step.role === role) {
return step.output;
}
}
}
return undefined;
}
async function evaluateJsonata(
expression: string,
context: ModeratorContext,
): Promise<Result<unknown, Error>> {
try {
const result = await jsonata(expression).evaluate(context);
const expr = jsonata(expression);
expr.registerFunction(
"first",
(role: string) => findByRole(context.steps, role, "first"),
"<s:x>",
);
expr.registerFunction(
"last",
(role: string) => findByRole(context.steps, role, "last"),
"<s:x>",
);
const result = await expr.evaluate(context);
return { ok: true, value: result };
} catch (error) {
return {
@@ -5,5 +5,5 @@
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../uwf-protocol" }]
"references": [{ "path": "../workflow-protocol" }]
}
@@ -1,6 +1,6 @@
{
"name": "@uncaged/uwf-protocol",
"version": "0.1.0",
"name": "@uncaged/workflow-protocol",
"version": "0.5.0",
"files": [
"src",
"dist",
@@ -15,8 +15,8 @@
}
},
"dependencies": {
"@uncaged/json-cas": "^0.3.0",
"@uncaged/json-cas-fs": "^0.3.0"
"@uncaged/json-cas": "^0.4.0",
"@uncaged/json-cas-fs": "^0.4.0"
},
"devDependencies": {
"typescript": "^5.8.3"
@@ -2,11 +2,14 @@ import type { JSONSchema } from "@uncaged/json-cas";
const ROLE_DEFINITION: JSONSchema = {
type: "object",
required: ["description", "systemPrompt", "outputSchema"],
required: ["description", "goal", "capabilities", "procedure", "output", "frontmatter"],
properties: {
description: { type: "string" },
systemPrompt: { type: "string" },
outputSchema: { type: "string", format: "cas_ref" },
goal: { type: "string" },
capabilities: { type: "array", items: { type: "string" } },
procedure: { type: "string" },
output: { type: "string" },
frontmatter: { type: "string", format: "cas_ref" },
},
additionalProperties: false,
};
@@ -18,8 +18,11 @@ export type StepRecord = {
export type RoleDefinition = {
description: string;
systemPrompt: string;
outputSchema: CasRef;
goal: string;
capabilities: string[];
procedure: string;
output: string;
frontmatter: CasRef;
};
export type Transition = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util",
"version": "0.5.0-alpha.4",
"version": "0.5.0",
"files": [
"src",
"dist",
-28
View File
@@ -1,28 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@uncaged/workflow-protocol':
specifier: workspace:*
version: link:../workflow-protocol
devDependencies:
typescript:
specifier: ^5.8.3
version: 5.9.3
packages:
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
snapshots:
typescript@5.9.3: {}
@@ -0,0 +1,74 @@
// MAINTENANCE: This string must be kept in sync with the actual uwf CLI commands.
// Update whenever commands are added, removed, or their signatures change.
export function generateCliReference(): string {
return `# uwf CLI Reference
## Setup
\`\`\`
uwf setup # interactive setup wizard
uwf setup --provider <name> --base-url <url> \\
--api-key <key> --model <name> # non-interactive setup
[--agent <name>] # optional: default agent alias
\`\`\`
## Workflow Commands
\`\`\`
uwf workflow put <file> # register a workflow from YAML file
uwf workflow show <id> # show workflow by name or CAS hash
uwf workflow list # list all registered workflows
\`\`\`
## Thread Commands
\`\`\`
uwf thread start <workflow> -p <prompt> # create a thread (no execution)
uwf thread step <thread-id> # execute one moderator→agent→extract cycle
[--agent <cmd>] # override agent command
uwf thread show <thread-id> # show thread head pointer
uwf thread list # list active threads
[--all] # include archived threads
uwf thread kill <thread-id> # terminate and archive a thread
uwf thread steps <thread-id> # list all steps in a thread
uwf thread read <thread-id> # render thread context as markdown
[--quota <chars>] # max output characters (default 32000)
[--before <step-hash>] # load steps before this hash (exclusive)
[--start] # include start step in output
uwf thread fork <step-hash> # fork a thread from a specific step
uwf thread step-details <step-hash> # dump full detail node of a step as YAML
\`\`\`
## CAS Commands
\`\`\`
uwf cas get <hash> # read a CAS node (type + payload)
[--timestamp] # include timestamp in output
uwf cas put <type-hash> <data> # store a node, print its hash
# <data>: JSON file path or inline JSON string
uwf cas put-text <text> # store a plain text string, print its hash
# shortcut for put with the built-in text schema
uwf cas has <hash> # check if a hash exists
uwf cas refs <hash> # list direct CAS references from a node
uwf cas walk <hash> # recursive traversal from a node
uwf cas reindex # rebuild type index from all CAS nodes
uwf cas schema list # list all registered schemas
uwf cas schema get <hash> # show a schema by its type hash
\`\`\`
## Global Options
\`\`\`
uwf --format <fmt> # output format: json (default) or yaml
uwf -V, --version # print version
\`\`\`
## Key Concepts
- **Workflow**: YAML definition with roles, conditions, and a routing graph; stored as a CAS node identified by its XXH64 hash.
- **Thread**: A single workflow execution (ULID). State is an immutable CAS chain; active threads are indexed in \`threads.yaml\`.
- **Step**: One moderator→agent→extract cycle. Run \`uwf thread step\` repeatedly until \`$END\`.
- **CAS**: Content-Addressed Storage — all nodes are immutable and identified by hash.
- **Role**: Named actor with goal, capabilities, procedure, output, and meta; the moderator routes between roles.
`;
}
+6 -5
View File
@@ -1,10 +1,6 @@
export { err, ok } from "./result.js";
export { encodeUint64AsCrockford } from "./base32.js";
export { generateCliReference } from "./cli-reference.js";
export { env } from "./env.js";
export {
parseFrontmatterMarkdown,
validateFrontmatter,
} from "./frontmatter-markdown/index.js";
export type {
AgentFrontmatter,
FrontmatterScope,
@@ -12,8 +8,13 @@ export type {
FrontmatterValidationError,
ParsedFrontmatterMarkdown,
} from "./frontmatter-markdown/index.js";
export {
parseFrontmatterMarkdown,
validateFrontmatter,
} from "./frontmatter-markdown/index.js";
export { createLogger } from "./logger.js";
export { normalizeRefsField } from "./refs-field.js";
export { err, ok } from "./result.js";
export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js";
export type { LogFn, Result } from "./types.js";
export { generateUlid } from "./ulid.js";
-937
View File
@@ -1,937 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@biomejs/biome':
specifier: ^2.4.14
version: 2.4.15
'@changesets/cli':
specifier: ^2.31.0
version: 2.31.0(@types/node@25.8.0)
'@types/node':
specifier: ^25.7.0
version: 25.8.0
'@types/xxhashjs':
specifier: ^0.2.4
version: 0.2.4
bun-types:
specifier: ^1.3.13
version: 1.3.14
packages:
'@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
'@biomejs/biome@2.4.15':
resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.4.15':
resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.4.15':
resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.4.15':
resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@2.4.15':
resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.15':
resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@2.4.15':
resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.15':
resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.4.15':
resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@changesets/apply-release-plan@7.1.1':
resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==}
'@changesets/assemble-release-plan@6.0.10':
resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==}
'@changesets/changelog-git@0.2.1':
resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
'@changesets/cli@2.31.0':
resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==}
hasBin: true
'@changesets/config@3.1.4':
resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==}
'@changesets/errors@0.2.0':
resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==}
'@changesets/get-dependents-graph@2.1.4':
resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==}
'@changesets/get-release-plan@4.0.16':
resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==}
'@changesets/get-version-range-type@0.4.0':
resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==}
'@changesets/git@3.0.4':
resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==}
'@changesets/logger@0.1.1':
resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==}
'@changesets/parse@0.4.3':
resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==}
'@changesets/pre@2.0.2':
resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==}
'@changesets/read@0.6.7':
resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==}
'@changesets/should-skip-package@0.1.2':
resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==}
'@changesets/types@4.1.0':
resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==}
'@changesets/types@6.1.0':
resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
'@changesets/write@0.4.0':
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
'@inquirer/external-editor@1.0.3':
resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@manypkg/find-root@1.1.0':
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
'@types/node@25.8.0':
resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==}
'@types/xxhashjs@0.2.4':
resolution: {integrity: sha512-E2+ZoJY2JjmVPN0iQM5gJvZkk98O2PYXSi6HrciEk3EKF34+mauEk/HgwTeCz+2r8HXHMKpucrwy4qTT12OPaQ==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
bun-types@1.3.14:
resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==}
chardet@2.1.1:
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
detect-indent@6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
enquirer@2.4.1:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
extendable-error@0.1.7:
resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
fs-extra@8.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
globby@11.1.0:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
engines: {node: '>=10'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
human-id@4.1.3:
resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==}
hasBin: true
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-subdir@1.2.0:
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
engines: {node: '>=4'}
is-windows@1.0.2:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
lodash.startcase@4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
outdent@0.5.0:
resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
p-filter@2.1.0:
resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
engines: {node: '>=8'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-map@2.1.0:
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
engines: {node: '>=6'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
package-manager-detector@0.2.11:
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.2:
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'}
pify@4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
prettier@2.8.8:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=10.13.0'}
hasBin: true
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
read-yaml-file@1.1.0:
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
engines: {node: '>=6'}
resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
semver@7.8.0:
resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==}
engines: {node: '>=10'}
hasBin: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
spawndamnit@3.0.1:
resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
term-size@2.2.1:
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
engines: {node: '>=8'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
snapshots:
'@babel/runtime@7.29.2': {}
'@biomejs/biome@2.4.15':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.4.15
'@biomejs/cli-darwin-x64': 2.4.15
'@biomejs/cli-linux-arm64': 2.4.15
'@biomejs/cli-linux-arm64-musl': 2.4.15
'@biomejs/cli-linux-x64': 2.4.15
'@biomejs/cli-linux-x64-musl': 2.4.15
'@biomejs/cli-win32-arm64': 2.4.15
'@biomejs/cli-win32-x64': 2.4.15
'@biomejs/cli-darwin-arm64@2.4.15':
optional: true
'@biomejs/cli-darwin-x64@2.4.15':
optional: true
'@biomejs/cli-linux-arm64-musl@2.4.15':
optional: true
'@biomejs/cli-linux-arm64@2.4.15':
optional: true
'@biomejs/cli-linux-x64-musl@2.4.15':
optional: true
'@biomejs/cli-linux-x64@2.4.15':
optional: true
'@biomejs/cli-win32-arm64@2.4.15':
optional: true
'@biomejs/cli-win32-x64@2.4.15':
optional: true
'@changesets/apply-release-plan@7.1.1':
dependencies:
'@changesets/config': 3.1.4
'@changesets/get-version-range-type': 0.4.0
'@changesets/git': 3.0.4
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
detect-indent: 6.1.0
fs-extra: 7.0.1
lodash.startcase: 4.4.0
outdent: 0.5.0
prettier: 2.8.8
resolve-from: 5.0.0
semver: 7.8.0
'@changesets/assemble-release-plan@6.0.10':
dependencies:
'@changesets/errors': 0.2.0
'@changesets/get-dependents-graph': 2.1.4
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
semver: 7.8.0
'@changesets/changelog-git@0.2.1':
dependencies:
'@changesets/types': 6.1.0
'@changesets/cli@2.31.0(@types/node@25.8.0)':
dependencies:
'@changesets/apply-release-plan': 7.1.1
'@changesets/assemble-release-plan': 6.0.10
'@changesets/changelog-git': 0.2.1
'@changesets/config': 3.1.4
'@changesets/errors': 0.2.0
'@changesets/get-dependents-graph': 2.1.4
'@changesets/get-release-plan': 4.0.16
'@changesets/git': 3.0.4
'@changesets/logger': 0.1.1
'@changesets/pre': 2.0.2
'@changesets/read': 0.6.7
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@changesets/write': 0.4.0
'@inquirer/external-editor': 1.0.3(@types/node@25.8.0)
'@manypkg/get-packages': 1.1.3
ansi-colors: 4.1.3
enquirer: 2.4.1
fs-extra: 7.0.1
mri: 1.2.0
package-manager-detector: 0.2.11
picocolors: 1.1.1
resolve-from: 5.0.0
semver: 7.8.0
spawndamnit: 3.0.1
term-size: 2.2.1
transitivePeerDependencies:
- '@types/node'
'@changesets/config@3.1.4':
dependencies:
'@changesets/errors': 0.2.0
'@changesets/get-dependents-graph': 2.1.4
'@changesets/logger': 0.1.1
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
fs-extra: 7.0.1
micromatch: 4.0.8
'@changesets/errors@0.2.0':
dependencies:
extendable-error: 0.1.7
'@changesets/get-dependents-graph@2.1.4':
dependencies:
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
picocolors: 1.1.1
semver: 7.8.0
'@changesets/get-release-plan@4.0.16':
dependencies:
'@changesets/assemble-release-plan': 6.0.10
'@changesets/config': 3.1.4
'@changesets/pre': 2.0.2
'@changesets/read': 0.6.7
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
'@changesets/get-version-range-type@0.4.0': {}
'@changesets/git@3.0.4':
dependencies:
'@changesets/errors': 0.2.0
'@manypkg/get-packages': 1.1.3
is-subdir: 1.2.0
micromatch: 4.0.8
spawndamnit: 3.0.1
'@changesets/logger@0.1.1':
dependencies:
picocolors: 1.1.1
'@changesets/parse@0.4.3':
dependencies:
'@changesets/types': 6.1.0
js-yaml: 4.1.1
'@changesets/pre@2.0.2':
dependencies:
'@changesets/errors': 0.2.0
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
fs-extra: 7.0.1
'@changesets/read@0.6.7':
dependencies:
'@changesets/git': 3.0.4
'@changesets/logger': 0.1.1
'@changesets/parse': 0.4.3
'@changesets/types': 6.1.0
fs-extra: 7.0.1
p-filter: 2.1.0
picocolors: 1.1.1
'@changesets/should-skip-package@0.1.2':
dependencies:
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
'@changesets/types@4.1.0': {}
'@changesets/types@6.1.0': {}
'@changesets/write@0.4.0':
dependencies:
'@changesets/types': 6.1.0
fs-extra: 7.0.1
human-id: 4.1.3
prettier: 2.8.8
'@inquirer/external-editor@1.0.3(@types/node@25.8.0)':
dependencies:
chardet: 2.1.1
iconv-lite: 0.7.2
optionalDependencies:
'@types/node': 25.8.0
'@manypkg/find-root@1.1.0':
dependencies:
'@babel/runtime': 7.29.2
'@types/node': 12.20.55
find-up: 4.1.0
fs-extra: 8.1.0
'@manypkg/get-packages@1.1.3':
dependencies:
'@babel/runtime': 7.29.2
'@changesets/types': 4.1.0
'@manypkg/find-root': 1.1.0
fs-extra: 8.1.0
globby: 11.1.0
read-yaml-file: 1.1.0
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@types/node@12.20.55': {}
'@types/node@25.8.0':
dependencies:
undici-types: 7.24.6
'@types/xxhashjs@0.2.4':
dependencies:
'@types/node': 25.8.0
ansi-colors@4.1.3: {}
ansi-regex@5.0.1: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
argparse@2.0.1: {}
array-union@2.1.0: {}
better-path-resolve@1.0.0:
dependencies:
is-windows: 1.0.2
braces@3.0.3:
dependencies:
fill-range: 7.1.1
bun-types@1.3.14:
dependencies:
'@types/node': 25.8.0
chardet@2.1.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
detect-indent@6.1.0: {}
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
enquirer@2.4.1:
dependencies:
ansi-colors: 4.1.3
strip-ansi: 6.0.1
esprima@4.0.1: {}
extendable-error@0.1.7: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fastq@1.20.1:
dependencies:
reusify: 1.1.0
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
fs-extra@8.1.0:
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
globby@11.1.0:
dependencies:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.3.3
ignore: 5.3.2
merge2: 1.4.1
slash: 3.0.0
graceful-fs@4.2.11: {}
human-id@4.1.3: {}
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
ignore@5.3.2: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
is-subdir@1.2.0:
dependencies:
better-path-resolve: 1.0.0
is-windows@1.0.2: {}
isexe@2.0.0: {}
js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
jsonfile@4.0.0:
optionalDependencies:
graceful-fs: 4.2.11
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
lodash.startcase@4.4.0: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.2
mri@1.2.0: {}
outdent@0.5.0: {}
p-filter@2.1.0:
dependencies:
p-map: 2.1.0
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-map@2.1.0: {}
p-try@2.2.0: {}
package-manager-detector@0.2.11:
dependencies:
quansync: 0.2.11
path-exists@4.0.0: {}
path-key@3.1.1: {}
path-type@4.0.0: {}
picocolors@1.1.1: {}
picomatch@2.3.2: {}
pify@4.0.1: {}
prettier@2.8.8: {}
quansync@0.2.11: {}
queue-microtask@1.2.3: {}
read-yaml-file@1.1.0:
dependencies:
graceful-fs: 4.2.11
js-yaml: 3.14.2
pify: 4.0.1
strip-bom: 3.0.0
resolve-from@5.0.0: {}
reusify@1.1.0: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
safer-buffer@2.1.2: {}
semver@7.8.0: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
slash@3.0.0: {}
spawndamnit@3.0.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
sprintf-js@1.0.3: {}
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-bom@3.0.0: {}
term-size@2.2.1: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
undici-types@7.24.6: {}
universalify@0.1.2: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env bash
# deploy.sh — Build & deploy dashboard + gateway to Cloudflare
#
# Usage:
# ./scripts/deploy.sh # deploy both
# ./scripts/deploy.sh dashboard # dashboard only
# ./scripts/deploy.sh gateway # gateway only
#
# Env (via `cfg` or export):
# CLOUDFLARE_API_TOKEN — Cloudflare API token
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN is required}"
export CLOUDFLARE_API_TOKEN
TARGET="${1:-all}"
deploy_dashboard() {
echo "🌐 Building dashboard..."
(cd packages/workflow-dashboard && npm run build)
echo "🚀 Deploying dashboard to Cloudflare Pages..."
(cd packages/workflow-gateway && npx wrangler pages deploy \
../workflow-dashboard/dist \
--project-name workflow-dashboard)
echo " ✅ Dashboard → workflow.shazhou.work"
}
deploy_gateway() {
echo "🚀 Deploying gateway Worker..."
(cd packages/workflow-gateway && npx wrangler deploy)
echo " ✅ Gateway → workflow-gateway.shazhou.workers.dev"
}
case "$TARGET" in
dashboard) deploy_dashboard ;;
gateway) deploy_gateway ;;
all) deploy_dashboard; deploy_gateway ;;
*) echo "Usage: deploy.sh [dashboard|gateway|all]"; exit 1 ;;
esac
echo "✅ Deploy complete"
+10 -14
View File
@@ -18,19 +18,9 @@ const dryRun = args.includes("--dry-run");
const publishOrder = [
"workflow-protocol",
"workflow-util",
"workflow-runtime",
"workflow-cas",
"workflow-reactor",
"workflow-register",
"workflow-execute",
"workflow-util-agent",
"workflow-agent-cursor",
"workflow-moderator",
"workflow-agent-kit",
"workflow-agent-hermes",
"workflow-agent-llm",
"workflow-agent-react",
"workflow-template-develop",
"workflow-template-solve-issue",
"workflow-gateway",
"cli-workflow",
];
@@ -71,14 +61,18 @@ for (const name of publishOrder) {
const tagFlag = tag ? `--tag ${tag}` : "";
const cmd = `npm publish --access public ${tagFlag}`;
console.log(`📦 ${name}...`);
if (dryRun) {
console.log(` (dry-run) ${cmd}`);
continue;
}
try {
const out = execSync(cmd, { cwd: pkgDir, stdio: "pipe" }).toString().trim();
const _lastLine = out.split("\n").pop();
} catch (_err) {
console.log(` ✅ published`);
} catch (err) {
console.error(` ❌ failed: ${err.message}`);
failed = true;
break;
}
@@ -92,3 +86,5 @@ for (const [pkgPath, raw] of originals) {
if (failed) {
process.exit(1);
}
console.log(dryRun ? "\n✅ Dry run complete" : "\n✅ All packages published");
+5 -5
View File
@@ -19,10 +19,10 @@
},
"references": [
{ "path": "packages/workflow-util" },
{ "path": "packages/uwf-protocol" },
{ "path": "packages/uwf-moderator" },
{ "path": "packages/uwf-agent-kit" },
{ "path": "packages/uwf-agent-hermes" },
{ "path": "packages/cli-uwf" }
{ "path": "packages/workflow-protocol" },
{ "path": "packages/workflow-moderator" },
{ "path": "packages/workflow-agent-kit" },
{ "path": "packages/workflow-agent-hermes" },
{ "path": "packages/cli-workflow" }
]
}