Compare commits

...

205 Commits

Author SHA1 Message Date
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
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
xiaoju 68246e20b1 fix: remove workflow-util dependency on workflow-protocol
Inline Result type and ok/err helpers into workflow-util to break
dependency on the now-archived workflow-protocol package.

Also add explicit @uncaged/json-cas dep to uwf-protocol (was only
available as transitive dep via json-cas-fs).

小橘 🍊(NEKO Team)
2026-05-19 07:22:15 +00:00
xiaoju d63d58ccb5 chore: reorganize repo — legacy packages to legacy-packages/, templates to examples/
- Move 15 old workflow-* packages to legacy-packages/ (inactive, preserved for reference)
- Rename templates/ → examples/ for clarity
- Rewrite docs/architecture.md to reflect current uwf architecture
- Active packages remain in packages/: cli-uwf, uwf-agent-hermes, uwf-agent-kit, uwf-moderator, uwf-protocol, workflow-util

小橘 🍊(NEKO Team)
2026-05-19 07:19:40 +00:00
xiaomo 2a3a40b9d9 Merge pull request 'feat(cli-uwf): thread read Content + step-details — #357' (#358) from feat/357-thread-read-content into main 2026-05-19 06:58:24 +00:00
xiaoju 762ecec872 feat(cli-uwf): thread read shows Content + new step-details command
- thread read: add ### Content section (last assistant message) before ### Output
- Remove --detail flag (replaced by step-details command)
- New: uwf thread step-details <step-hash> — full detail dump as yaml

Closes #357
2026-05-19 06:44:18 +00:00
xiaoju c0ac4ade09 fix(uwf-agent-hermes): consume outputFormatInstruction in prompt
buildHermesPrompt was ignoring ctx.outputFormatInstruction — the
deliverable format and scope constraint were injected into context
but never passed to the agent.

Now prepends it before systemPrompt (deliverable-first principle).

Refs #355
2026-05-19 06:23:13 +00:00
xiaomo a991393053 Merge pull request 'feat(uwf-agent-kit): frontmatter fast path + prompt injection — #355' (#356) from feat/355-uwf-frontmatter into main 2026-05-19 06:21:35 +00:00
xiaoju 892ccab8d5 feat(uwf-agent-kit): frontmatter fast path + prompt injection
Port RFC #351 frontmatter markdown to uwf-* path:
- tryFrontmatterFastPath(): parse → validate → JSON Schema check via json-cas
- Happy path skips LLM extract, fallback to existing extract()
- buildOutputFormatInstruction(): generates deliverable format from JSON Schema
- Injected into agent context before execution
- Scope reminder: 'Focus exclusively on YOUR role's deliverable'
- 14 new tests (vitest)

Closes #355
2026-05-19 06:20:15 +00:00
xiaomo 70c83c65b0 Merge pull request 'feat(workflow-util-agent): prompt restructure + scope focus — RFC #351 Phase 3' (#354) from feat/351-phase3-prompt-focus into main 2026-05-19 05:57:37 +00:00
xiaoju 8a7e756fe3 feat(workflow-util-agent): prompt restructure + scope focus — Phase 3
- buildOutputFormatInstruction(schema): auto-generates frontmatter
  format guide from Zod schema, injected at top of system prompt
- Adapter prepends deliverable format before role's systemPrompt
- buildThreadInput reordered: Task → Steps → Parent → Tools
- Scope reminder: 'Focus exclusively on YOUR role's deliverable'
- 8 tests for buildOutputFormatInstruction

Refs #351
2026-05-19 05:56:27 +00:00
xiaomo 4a4ddba9f6 Merge pull request 'feat(workflow-util-agent): two-layer frontmatter safeguard — RFC #351 Phase 2' (#353) from feat/351-phase2-adapter-frontmatter into main 2026-05-19 05:47:46 +00:00
xiaoju d5f47d1a18 feat(workflow-util-agent): two-layer frontmatter safeguard in adapter
Phase 2 of RFC #351 — adapter tries frontmatter first (zero LLM cost),
falls back to runtime.extract() when frontmatter is missing/invalid.

- tryFrontmatterMeta(): parse → validate → schema.safeParse
- Happy path stores body (no frontmatter) in CAS
- Fallback stores full raw in CAS + LLM extract
- 5 tests covering both paths

Refs #351
2026-05-19 05:46:36 +00:00
xiaoju 37c35560e9 docs: fix parseMinimalYaml JSDoc (nit from #352 review)
Refs #351
2026-05-19 05:41:18 +00:00
xiaomo f174b96028 Merge pull request 'feat(workflow-util): frontmatter markdown parser — RFC #351 Phase 1' (#352) from feat/351-frontmatter-markdown-phase1 into main 2026-05-19 04:56:58 +00:00
xiaoju 43978360ff feat(workflow-util): add frontmatter markdown parser and validator
Phase 1 of RFC #351 — define AgentFrontmatter type, parseFrontmatterMarkdown()
and validateFrontmatter() with 45 tests.

- Built-in minimal YAML parser (no new deps)
- Never throws on malformed input — degrades gracefully
- All fields use T | null (no optional properties)

Refs #351
2026-05-19 04:41:56 +00:00
xiaomo 432400ee20 Merge pull request 'feat: uwf thread read — human-readable markdown with pagination' (#350) from feat/349-thread-read into main 2026-05-19 03:45:02 +00:00
xiaoju dacebe1841 feat(thread-read): show role system prompt in each step
Each step block now includes a '### Prompt' section showing the
role's systemPrompt from the workflow definition.

Refs #349
2026-05-19 03:23:50 +00:00
xiaoju c42125946d feat(thread-read): expand detail recursively via cas_ref
--detail now uses expandDeep to recursively resolve all cas_ref
fields in the detail merkle tree, showing full turn content
instead of raw hashes.

Refs #349
2026-05-19 03:19:40 +00:00
xiaoju 4c9ce72395 feat: uwf thread read — human-readable markdown with pagination
- Outputs markdown directly (not JSON/YAML)
- --quota <chars>: character budget, loads steps backward until exceeded (default 4000)
- --before <step-hash>: load steps before this hash (exclusive), omits start
- --start: force include start section even with --before
- --detail: expand detail CAS node content for each step
- Skip hint with uwf thread read command for pagination
- Reuses walkChain/collectOrderedSteps/expandOutput

Closes #349
2026-05-19 03:15:38 +00:00
xiaomo 8b43f7993b Merge pull request 'fix: parse session_id from stderr — hermes --quiet writes it there' (#348) from fix/348-session-id-stderr into main 2026-05-18 17:10:29 +00:00
xiaoju cf9e2cd3d6 fix: parse session_id from stderr (hermes --quiet writes it there)
hermes --quiet outputs session_id to stderr and AI response to stdout.
The agent was only parsing stdout, so session_id was never found and
detail always fell back to raw output.

Now checks stderr first, then stdout as fallback.
2026-05-18 17:05:54 +00:00
xiaomo 7a99c1a9d6 Merge pull request 'fix: hermes agent empty detail — parse session_id from any line' (#347) from fix/342-parse-session-id into main 2026-05-18 16:58:24 +00:00
xiaoju 546237db85 fix: parseSessionIdFromStdout scans all lines, not just last
--quiet mode outputs session_id on the first line, not the last.
The old code only checked the last non-empty line and broke immediately
if it didn't match, causing session detail to always be empty.

Fixes the empty detail bug when hermes agent runs in quiet mode.
2026-05-18 16:57:24 +00:00
xiaomo 1ed7e32067 Merge pull request 'simplify: thread fork only takes step-hash' (#346) from fix/342-fork-simplify into main 2026-05-18 16:43:33 +00:00
xiaoju bd5e5a435b simplify: thread fork only takes step-hash
Remove thread-id argument — CAS node is self-contained, no need to
specify which thread it belongs to. Just verify the hash is a valid
StartNode or StepNode.

Refs #342
2026-05-18 16:38:55 +00:00
xiaomo 67e689ff1a Merge pull request 'feat: thread steps + thread fork' (#345) from feat/342-thread-steps-fork into main 2026-05-18 16:34:55 +00:00
xiaoju 06eb2dff3b feat: add thread steps and thread fork commands
- uwf thread steps <thread-id>: walk CAS chain, list all steps chronologically
- uwf thread fork <thread-id> <step-hash>: create new thread from history point
- New types: StartEntry, StepEntry, ThreadStepsOutput, ThreadForkOutput
- Supports both active and archived threads

Refs #342
2026-05-18 16:30:12 +00:00
xiaomo a2bd3126c8 Merge pull request 'refactor: AgentContext extends ModeratorContext, remove redundant fields' (#341) from refactor/simplify-agent-context into main 2026-05-18 16:17:16 +00:00
xiaoju 710d42d6b9 refactor(agent-kit): base AgentContext on ModeratorContext
AgentContext now extends ModeratorContext (start + steps) with threadId, role, store, and expanded workflow. Hermes and mock-agent read prompt/steps/systemPrompt from the new shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 16:14:13 +00:00
xiaomo 072d900fcb Merge pull request 'refactor: pass store via AgentContext, eliminate duplicate store instances' (#340) from refactor/pass-store-via-context into main 2026-05-18 16:05:38 +00:00
xiaoju cfebd07124 refactor(agent-kit): pass CAS store through AgentContext
Expose the store created during context build on AgentContext so agents
reuse the same in-memory cache instead of opening a second store.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 16:04:15 +00:00
xiaoju f2be6fc057 Merge pull request 'feat: hermes merkle detail — session turns as CAS tree (Phase 2 of #337)' (#339) from feat/337-agent-detail-merkle into main 2026-05-18 15:58:01 +00:00
xiaoju d392563549 feat(uwf-hermes): Phase 2 merkle detail from Hermes session JSON
Parse session_id from Hermes stdout, store hermes-turn leaves and
hermes-detail root in CAS with cas_ref turns; fall back to raw stdout
when the session file is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:56:50 +00:00
xiaoju 2af8196451 Merge pull request 'feat: agent-kit interface change — agents own their detail (Phase 1 of #337)' (#338) from feat/337-agent-detail-merkle into main 2026-05-18 15:52:56 +00:00
xiaoju ad74768630 feat(uwf-agent): Phase 1 agent returns output and detailHash
- Change AgentRunFn to return { output, detailHash } instead of raw string

- Remove agent-kit detail CAS write; agents store their own detail nodes

- Hermes stores raw output as typed hermes-raw-output CAS node

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:29:48 +00:00
xiaoju a38ca7e8db chore: upgrade json-cas deps to ^0.3.0 2026-05-18 15:27:01 +00:00
xiaomo 3d97968887 Merge pull request 'feat: add uwf cas reindex command' (#334) from feat/cas-reindex into main 2026-05-18 14:25:38 +00:00
xiaoju ade6227ffe feat: add uwf cas reindex command
Rebuilds _index from all .bin nodes. Use after upgrading to json-cas 0.2.0
on an existing CAS directory.
2026-05-18 14:24:23 +00:00
xiaomo 13789e2c66 Merge pull request 'refactor: use listByType for schema list, upgrade json-cas to 0.2.0' (#333) from refactor/use-list-by-type into main 2026-05-18 14:18:16 +00:00
xiaoju 6758adc1d5 refactor: use listByType for schema list, upgrade json-cas to 0.2.0
Replace O(n) full CAS scan with O(1) type-index lookup.

Refs #328
2026-05-18 14:16:15 +00:00
xiaomo 7c12015855 Merge pull request 'refactor: merge cas get/cat into get, default hides timestamp' (#332) from refactor/merge-cas-get-cat into main 2026-05-18 14:03:50 +00:00
xiaoju 0f6859678c refactor: merge cas get/cat into get, default hides timestamp
- Remove `cas cat` command
- `cas get` now returns {type, payload} by default
- Add `--timestamp` flag to include timestamp

Refs #328
2026-05-18 14:03:10 +00:00
xiaomo 84798510b0 Merge pull request 'refactor: remove table output format, keep json and yaml only' (#331) from refactor/remove-table-format into main 2026-05-18 13:59:12 +00:00
xiaoju 6eace09826 refactor: remove table output format, keep json and yaml only
Table format adds complexity without readability gain over yaml.

Refs #328
2026-05-18 13:57:46 +00:00
xiaomo cb39a6693a Merge pull request 'fix: table format without header row' (#330) from fix/328-table-vertical into main 2026-05-18 13:48:17 +00:00
xiaoju 36d120b745 fix: table format — horizontal for arrays, vertical for objects
Arrays: horizontal table with HEADER row
Objects: vertical KEY/VALUE table
Primitives: fall back to yaml

小橘 🍊(NEKO Team)
2026-05-18 13:43:50 +00:00
jiayi 86dd37b0c8 Merge pull request 'feat: add office-agent document workflow (template + writer + differ)' (#327) from user/jiayiyan/feat_office-agent-document-template-v2 into main
Reviewed-on: #327
2026-05-18 13:42:03 +00:00
xiaomo bb0f2ca678 Merge pull request 'feat: --format json/yaml/table for all non-interactive commands' (#329) from feat/328-format-option into main 2026-05-18 13:40:19 +00:00
xiaomo ec0bc672f6 Merge pull request 'feat: --format json/yaml/table for all non-interactive commands' (#329) from feat/328-format-option into main 2026-05-18 13:36:02 +00:00
jiayiyan f08ba6914c chore: remove .DS_Store and add to .gitignore 2026-05-18 21:35:40 +08:00
xiaoju 7dd6ab5328 feat: --format json/yaml/table for all non-interactive commands
Add program-level --format option (default: json) inherited by all
subcommands. json output unchanged, yaml via yaml package, table
renders aligned columns for arrays, falls back to yaml for objects.

Closes #328

小橘 🍊(NEKO Team)
2026-05-18 13:33:41 +00:00
jiayiyan f6dd4d59a1 docs: add office-agent document template spec and implementation plan 2026-05-18 21:26:11 +08:00
jiayiyan d8cdc8ab88 feat(agent): add workflow-agent-office runner with generate/edit and tests 2026-05-18 21:26:11 +08:00
jiayiyan 20ddc5d7aa docs(architecture): add workflow-agent-office, workflow-agent-docx-diff, workflow-template-document
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:26:11 +08:00
jiayiyan 2846311f8d feat(agent): add workflow-agent-docx-diff with docx-diff AdapterFn
Implements createDocxDiffAgent (AdapterFn), packageDescriptor, and exports in index.ts; 9 tests pass (runner 6 + agent 3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:26:11 +08:00
jiayiyan ed0043b8ac feat(agent): scaffold workflow-agent-docx-diff package
Add package.json, tsconfig.json, and placeholder src/index.ts for
@uncaged/workflow-agent-docx-diff; append reference in root tsconfig.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:26:11 +08:00
jiayiyan bee3911f3f feat(agent): add workflow-agent-office with generate/edit AdapterFn
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:26:11 +08:00
jiayiyan 4285b8b180 feat(agent): scaffold workflow-agent-office package
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:26:11 +08:00
xiaomo 7c955fa749 Merge pull request 'fix: uwf cas — JSON output + meta-schema in schema list' (#326) from fix/319-cas-json-output into main 2026-05-18 13:25:16 +00:00
jiayiyan f0b7be79fb feat(template): add workflow-template-document with writer/differ roles and moderator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:24:58 +08:00
jiayiyan d4f05adeba chore(template): scaffold workflow-template-document package
Add package.json, tsconfig.json, and placeholder src/index.ts for the
@uncaged/workflow-template-document package; register it in root tsconfig.json references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:24:58 +08:00
xiaoju c4c9f96117 fix: uwf cas commands output JSON, include meta-schema in schema list
All cas subcommands now output JSON via writeJson(), consistent with
other uwf commands. schema list includes meta-schema. Removed --json
flag and --format tree (tree is human-only, not machine-friendly).

Refs #319

小橘 🍊(NEKO Team)
2026-05-18 13:24:19 +00:00
xiaomo 633d5aeafe Merge pull request 'refactor: outputSchema only accepts inline JSON Schema' (#325) from fix/319-validate-schema-only-inline into main 2026-05-18 13:18:17 +00:00
xiaoju 17103c1ee1 refactor: outputSchema only accepts inline JSON Schema
- Remove CAS ref string support from workflow YAML outputSchema
- Simplify validate.ts: no string check for outputSchema
- Auto-set title from role name (workflow.role format)

Refs #319

小橘 🍊(NEKO Team)
2026-05-18 13:17:29 +00:00
xiaomo c8a39be9bd Merge pull request 'fix: remove cas list, add schema titles' (#324) from fix/319-schema-titles into main 2026-05-18 13:07:15 +00:00
xiaoju b304f65876 feat: auto-set outputSchema title from role name
When uwf workflow put processes inline JSON Schema for a role,
auto-inject title=roleName if not already set. Makes uwf cas schema list
show meaningful names like 'planner', 'coder' instead of (unnamed).

小橘 🍊(NEKO Team)
2026-05-18 13:05:28 +00:00
xiaoju c9010a024f fix: remove cas list, add title to schemas
- Remove uwf cas list (CAS grows unbounded, listing all hashes is useless)
- Add title to Workflow/StartNode/StepNode schemas so schema list shows names

小橘 🍊(NEKO Team)
2026-05-18 13:01:17 +00:00
xiaomo 3434e2b2be Merge pull request 'feat: built-in uwf cas commands replacing json-cas passthrough' (#323) from feat/319-uwf-cas-builtin into main 2026-05-18 12:49:18 +00:00
xiaoju 52282e1960 feat: built-in uwf cas commands replacing json-cas passthrough
- get, cat, put, has, list, refs, walk, schema list, schema get
- All commands auto-resolve store to ~/.uncaged/workflow/cas
- No external json-cas CLI dependency needed
- Agent-friendly: uwf cas --help shows all available subcommands

Refs #319, Closes #320

小橘 🍊(NEKO Team)
2026-05-18 12:40:15 +00:00
Scott Wei 7a579ee67a feat: uwf cas — passthrough to json-cas with uwf store path
uwf cas get <hash>, uwf cas list, etc. all auto-set --store to
~/.uncaged/workflow/cas so agents don't need to remember the path.

小橘 🍊(NEKO Team)
2026-05-18 20:14:59 +08:00
Scott Wei 7c230383ad improve: multi-column model list + friendly post-setup message
- Model list now renders in columns to fit terminal width
- Interactive setup ends with usage hints instead of JSON dump

小橘 🍊(NEKO Team)
2026-05-18 19:56:09 +08:00
xiaoju e604fa5f47 feat: add uwf setup command
- Interactive mode: prompts for provider, API key, model (with /models discovery)
- Non-interactive mode: --provider --base-url --api-key --model flags
- Writes config.yaml (providers, models, agents, defaults)
- Writes .env (API keys with auto-generated env var names)
- Merges into existing config non-destructively
- Includes 13 preset providers (international + China + local)

小橘 🍊(NEKO Team)
2026-05-18 11:49:42 +00:00
xiaoju 5580791686 chore: remove stale develop-entry.ts 2026-05-18 11:43:09 +00:00
xiaoju 3afd7a5319 chore: remove leftover smoke test files 2026-05-18 11:41:49 +00:00
xiaoju 3d1b2268b4 chore: bump json-cas deps to ^0.1.3 2026-05-18 10:48:06 +00:00
xiaoju 8bebe9da0f chore: bump json-cas-fs to ^0.1.2 (fix workspace:^ in published pkg) 2026-05-18 10:44:30 +00:00
xiaoju 53a7355f0b chore: fix json-cas workspace:^ refs to ^0.1.1 2026-05-18 10:30:31 +00:00
xiaoju d99c285725 chore: remove cross-repo json-cas workspace deps from root 2026-05-18 10:28:22 +00:00
xiaoju 2505dd8d6a chore: remove stale pnpm-lock.yaml 2026-05-18 10:25:45 +00:00
xiaomo 1121dfa48b Merge pull request 'feat: uwf — Stateless Workflow CLI' (#317) from feat/309-uwf-stateless into main 2026-05-18 10:07:55 +00:00
xiaoju d90e29ad05 fix: address 3 critical PR review issues
1. threads.yaml race condition: reload threads index after agent subprocess
   completes before updating head pointer (cli-uwf/commands/thread.ts)

2. evaluateJsonata not awaited: jsonata evaluate() returns Promise for async
   expressions — now properly awaited (uwf-moderator/evaluate.ts)

3. resolveWorkflowHash dead code: function always returns a value, removed
   impossible null return type and dead null-check branches at call sites
   (cli-uwf/store.ts, commands/thread.ts, commands/workflow.ts)
2026-05-18 10:05:11 +00:00
xiaoju 0727e0e8d5 fix: reload CAS store after agent spawn + share schemas via uwf-protocol
The agent subprocess writes StepNode to CAS on disk, but the parent
process had an in-memory cache from createFsStore init. Fix: re-create
store after agent spawn to pick up new nodes.

Also centralized JSON Schemas in uwf-protocol so cli-uwf and agent-kit
produce identical type hashes.

E2E smoke test passing: workflow put → thread start → 3x step → done

Refs #309
2026-05-18 09:33:52 +00:00
xiaoju ba012d98bc feat: add @uncaged/uwf-agent-hermes — Hermes agent CLI adapter
Spawns 'hermes chat' with assembled prompt from agent-kit context.
Agent-kit handles extract, StepNode write, and stdout output.

Refs #309, #316
2026-05-18 09:22:12 +00:00
xiaoju b165049a13 feat: implement thread step — moderator → agent → update head
- Walk CAS chain to build ModeratorContext with expanded output
- Call uwf-moderator evaluate() for role decision
- Agent resolution: --agent > config overrides > default
- Spawn agent CLI, capture StepNode hash
- Update threads.yaml, check done via second evaluate
- Archive on $END

Refs #309, #315
2026-05-18 09:19:37 +00:00
xiaoju 4d477c67c0 feat: add @uncaged/uwf-agent-kit — agent CLI framework
- createAgent() API for building agent CLIs
- Context builder: reads CAS chain, builds AgentContext
- Extract: LLM-based structured output extraction
- StepNode writer: writes to CAS without touching threads.yaml
- Stdout: outputs StepNode hash

Refs #309, #314
2026-05-18 09:15:25 +00:00
xiaoju 0d5678c961 feat: add thread start/show/list/kill commands
- thread start: ULID generation, StartNode to CAS, threads.yaml
- thread show: active (done:false) or archived (done:true)
- thread list: active threads, --all includes history
- thread kill: archive to history.jsonl

Refs #309, #313
2026-05-18 09:09:10 +00:00
xiaoju a8e2aa85f8 feat: add @uncaged/cli-uwf with workflow put/show/list commands
Refs #309, #312
2026-05-18 09:03:55 +00:00
xiaoju 2a4d35399b feat: add @uncaged/uwf-moderator with JSONata evaluation engine
5 tests passing:  transition, condition match, fallback,
missing role error, output expansion.

Refs #309, #311
2026-05-18 08:58:21 +00:00
xiaoju 391915411e feat: add @uncaged/uwf-protocol with all shared types
Refs #309, #310
2026-05-18 08:53:37 +00:00
scottwei 4aaf49bfc6 Merge pull request 'jshang/optimize-dashboard-ui' (#308) from jshang/optimize-dashboard-ui into main
Reviewed-on: #308
2026-05-18 08:45:46 +00:00
xiaoju 08de1ae5eb docs: fresh uwf-* packages, depend on @uncaged/json-cas, no reuse 2026-05-18 08:44:04 +00:00
xiaoju c91a3d1ec6 docs: add description to condition definitions 2026-05-18 08:41:29 +00:00
xiaoju 13d932f69c docs: config with provider/model/agent registries and alias-based overrides 2026-05-18 08:38:08 +00:00
jiashuang f705d9b8ea refactor: optimize ui for dashboard 2026-05-18 16:20:05 +08:00
xiaoju f84d327410 docs: add .env for API keys, separate from config.yaml 2026-05-18 08:19:48 +00:00
xiaoju 9c2f93629b docs: add models config (default + extract LLM) 2026-05-18 08:16:03 +00:00
xiaoju bcefcb9af7 docs: add section 4 — key data types with shared StepRecord 2026-05-18 08:13:18 +00:00
xiaoju b14dce2bc6 docs: fix inconsistencies — title, terminology, threads.yaml, JSONata context 2026-05-18 08:09:40 +00:00
xiaoju 85c572e770 docs: inline roles/moderator into Workflow, output as cas_ref, detail polymorphic 2026-05-18 08:07:20 +00:00
xiaoju 9a89885ce6 docs: rewrite CAS structure — flatten refs, named conditions, config.yaml, output naming 2026-05-18 07:55:04 +00:00
xiaoju d095ceaafa docs: agent CLI takes thread-id + role, outputs CAS hash, step owns pointer 2026-05-18 07:24:14 +00:00
xiaoju 2a0346f48b docs: simplify show to pure thread-id → head query, all output JSON 2026-05-18 07:18:29 +00:00
xiaoju b4e25ea002 docs: add done field to step output 2026-05-18 07:12:38 +00:00
xiaoju 77f2060e6b docs: step on ended thread is an error, not null head 2026-05-18 07:11:50 +00:00
xiaoju 8f9a925179 docs: simplify step output to workflow/thread/head 2026-05-18 07:10:11 +00:00
jiashuang 2f3fff3536 refactor: introduce react-router 2026-05-18 15:06:16 +08:00
xiaoju a7eb9814ae docs: fix agent invocation format in thread step 2026-05-18 07:05:35 +00:00
xiaoju a8024e6d42 docs: use full 26-char Crockford Base32 ULIDs for thread IDs 2026-05-18 07:03:40 +00:00
xiaoju 6d94d9c85a docs: fix hash format to 13-char Crockford Base32 (XXH64) 2026-05-18 07:03:02 +00:00
xiaoju 49a4d08c04 docs: add thread list --all and thread kill 2026-05-18 06:59:47 +00:00
xiaoju d5773369af docs: uwf thread subcommands, simplify start output 2026-05-18 06:58:35 +00:00
xiaoju f49e014f41 docs: update CLI design — uwf naming, simplify commands and agent protocol 2026-05-18 06:56:55 +00:00
xiaoju ab48a8169d docs: add stateless workflow CLI design
Refs #297
2026-05-18 06:37:25 +00:00
xiaomo 2b707fb44e Merge pull request 'refactor: replace extractRefs with schema casRef annotations (Phase 2)' (#290) from feat/285-phase2-remove-extractrefs into main 2026-05-16 11:50:47 +00:00
xiaoju 6306b23a9f refactor: replace extractRefs with schema casRef annotations
Migrate all templates to use .meta({ casRef: true }) on Zod schema
fields instead of manual extractRefs functions. Remove extractRefs
from RoleDefinition type entirely.

- develop: planner phases[].hash, coder completedPhase annotated
- solve-issue, smoke, init templates: extractRefs removed
- create-workflow.ts: uses collectCasRefs(schema, meta)
- RoleDefinition: extractRefs field removed (breaking)

218 tests pass, 0 fail.

Fixes #289, Refs #285
2026-05-16 10:48:45 +00:00
xiaomo 6bb8cf8315 Merge pull request 'feat: add collectCasRefs — schema-level CAS ref annotation (Phase 1)' (#288) from feat/285-cas-ref-annotation into main 2026-05-16 10:43:16 +00:00
xiaoju 93b7947d7c feat: add collectCasRefs — extract CAS refs from schema meta annotations
Replaces manual extractRefs functions with declarative schema-level
casRef annotations. Walks Zod v4 schemas recursively, collecting
string values from fields marked with .meta({ casRef: true }).

Supports: objects, arrays, discriminatedUnion, nullable/optional.

8/8 test cases pass (flat, nested, union, null, mixed).

Refs #285, addresses #286
2026-05-16 10:42:24 +00:00
xingyue 9584a86fb7 Merge pull request 'chore: fix biome cognitive complexity warnings' (#287) from chore/fix-biome-complexity-warnings into main 2026-05-16 10:40:27 +00:00
Scott Wei defc0afc27 chore: fix biome cognitive complexity warnings
Refactor dashboard graph/schema helpers and descriptor role validation
into smaller functions so bun run check passes without warnings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 18:33:00 +08:00
xiaomo 9f6633d5bf Merge pull request 'refactor(workflow-protocol): require AgentFn Opt generic' (#284) from refactor/agent-fn-required-opt into main 2026-05-16 10:27:07 +00:00
Scott Wei 7dadf874e1 refactor(workflow-protocol): require AgentFn Opt generic
Make AgentFn<Opt> always take a mandatory options argument, removing
the void conditional overload. Simplify createAgentAdapter, restore
exports needed by tests, and fix CLI test bundles to use cas.put
instead of disallowed @uncaged/* imports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 18:23:07 +08:00
xiaomo ba90214af6 Merge pull request 'chore: internalize unused exports across all packages' (#283) from chore/audit-exports-cleanup into main 2026-05-16 09:59:57 +00:00
xiaoju 5bbac3e4f7 chore: internalize unused exports across all packages
Audit public API surfaces using reachability analysis from application
entry points (Worker, CLI, Dashboard). Symbols not reachable from any
application's customization tree are removed from package index.ts files.

Source files and internal usage are untouched — only the public export
surface is narrowed.

Changes by package:
- workflow-util: -7 exports (base32 internals, logger config types)
- workflow-cas: -12 exports (merkle internals, serialization details)
- workflow-execute: -24 exports (engine internals, LLM extract details)
- workflow-reactor: -4 exports (reactor config/invocation internals)
- workflow-register: -8 exports (redundant protocol re-exports, internal YAML fns)
- workflow-runtime: curated re-export subset (stop full protocol re-export)
- workflow-util-agent: -5 exports (internal agent helpers)
- workflow-agent-cursor: -1 export (validateCursorAgentConfig)
- workflow-agent-hermes: -1 export (validateHermesAgentConfig)

Note: workflow-protocol index.ts unchanged — downstream packages still
import removed symbols via internal paths. Protocol cleanup requires
updating workflow-runtime/src/types.ts first (separate PR).

Refs #273, #274, #275, #276, #277, #278, #279, #280, #281, #282
2026-05-16 09:58:56 +00:00
xiaomo 131021b1a7 Merge pull request 'chore: remove symlink dead code' (#271) from chore/remove-symlink-dead-code into main 2026-05-16 06:22:34 +00:00
xiaoju e42555fd9c chore: remove symlink dead code
Now that bundles are fully self-contained (no external @uncaged/* imports),
the symlink mechanism is no longer needed.

- Delete ensure-uncaged-workflow-symlink.ts
- Remove ensureUncagedWorkflowSymlink from all imports/exports
- Remove ExtractBundleExportsOptions type (storageRoot param)
- Simplify extractBundleExports to single-arg signature
- Clean up stale comments
2026-05-16 06:21:34 +00:00
xiaomo 3a26eb28e5 Merge pull request 'chore: make bundle fully self-contained, no external imports' (#270) from chore/no-external-bundle into main 2026-05-16 06:16:28 +00:00
xiaoju c1a17b707c chore: make bundle fully self-contained, no external imports
- Remove uncagedWorkflowExternals() from scaffold build script
- Remove --external from Bun.build config
- Tighten bundle validator: only Node built-ins allowed, all deps must be inlined
- Update skill.ts documentation

Bundles are now deterministic — same Node/Bun version = same behavior,
no dependency resolution at runtime.
2026-05-16 05:12:49 +00:00
xiaoju 4ea1e0d8a4 chore: publish 0.5.0-alpha.4 — unified env() API 2026-05-15 10:19:38 +00:00
xiaoju b1a9d2ec3f refactor: replace requireEnv/optionalEnv with env(name, fallback)
Bundles must run without env vars — env vars are overrides, not requirements.
Single function: env(name, fallback) always returns string with a default.

- Removed requireEnv and optionalEnv
- Updated bundle entries, tests, and skill docs

小橘 🍊
2026-05-15 10:07:49 +00:00
xiaoju 2b8707a706 style: use optionalEnv fallback param instead of ?? operator
小橘 🍊
2026-05-15 09:52:07 +00:00
xiaoju 241bfbf6d9 fix: hardcode absolute paths for agent CLI defaults in bundle entry
Validator requires absolute paths — bare command names like 'cursor-agent'
fail validation. Use `which` to discover the path, write it directly.

小橘 🍊
2026-05-15 09:49:42 +00:00
xiaoju 40530d757e fix: use optionalEnv with defaults for agent CLI paths in bundle entry
requireEnv causes silent worker crash when env vars are missing —
thread shows 0 steps with no error. Use optionalEnv + sensible defaults.

Also added pitfall guidance in skill author docs.

小橘 🍊
2026-05-15 09:25:39 +00:00
xiaoju 0f3661b566 refactor: unify GATEWAY_SECRET + DASHBOARD_API_KEY into WORKFLOW_DASHBOARD_SECRET 2026-05-15 09:12:12 +00:00
xingyue 9c44c709e9 fix(dashboard): replace broken partial-order layering with longest-path
The previous computeLayers used a reachability-based relation (a « b)
with depth tiebreaker to define a < b, then grouped nodes by == into
equivalence classes. However == is NOT an equivalence relation (fails
transitivity), making the grouping order-dependent and incorrect for
graphs with parallel branches.

Replace with standard Sugiyama longest-path layering:
1. DFS to detect and remove back-edges (break cycles)
2. Kahn's topological sort on the resulting DAG
3. rank(n) = max(rank(pred) + 1) for longest-path assignment
4. Group nodes by rank into layers

Also removes the experimental dagre layout strategy that was added
for comparison — longest-path produces better results for our
workflow graphs.
2026-05-15 14:48:02 +08:00
xingyue 8892ab9978 fix(dashboard): add left/right handles to end node for skip-forward edges 2026-05-15 14:15:35 +08:00
xingyue 7ec86d82a3 fix(dashboard): sidePath supports both feedback and skip-forward edges 2026-05-15 14:11:58 +08:00
xingyue f728b36e8d fix(dashboard): route skip-forward edges to side (planner→end was hidden) 2026-05-15 14:10:25 +08:00
xingyue 3431d3070b refactor(dashboard): reachability-based partial order for graph layout
Replace linear spine walk with a proper partial order:
- a « b = a ~> b AND NOT b ~> a (strict precedence)
- a ~ b = incomparable under «
- depth tiebreaker for incomparable nodes
- Equivalent nodes (same layer) placed side-by-side horizontally
2026-05-15 14:05:53 +08:00
xingyue 576df067d4 chore: remove generated bundle from git, fix biome format 2026-05-15 09:42:33 +08:00
xingyue a46a225d04 fix(dashboard): render system prompt as markdown 2026-05-15 09:41:52 +08:00
xiaoju f74b482cc0 chore: version 0.5.0-alpha.3, add publish-all script
- scripts/publish-all.mjs: pins workspace:^ before npm publish, restores after
- Workaround for bun publish workspace:^ resolution bug in pre mode

小橘 🍊
2026-05-15 01:37:27 +00:00
xiaomo 89abfdc257 Merge pull request 'feat(dashboard): show system prompt per role' (#269) from feat/show-system-prompt into main 2026-05-15 01:28:12 +00:00
xiaoju 77e395b913 chore: version 0.5.0-alpha.1
小橘 🍊
2026-05-15 01:27:54 +00:00
xingyue b65a006d45 feat(dashboard): show system prompt per role in workflow detail
- Add systemPrompt to WorkflowRoleDescriptor (protocol)
- Propagate systemPrompt through buildDescriptor and validateWorkflowDescriptor
- Display system prompt as collapsible <details> in RoleCard
2026-05-15 09:27:03 +08:00
xiaomo 5994548f0b Merge pull request 'chore: add .env.example with all supported env vars' (#207) from chore/205-env-example into main 2026-05-15 01:25:10 +00:00
xiaoju 0871ae54ea fix: remove unused WORKFLOW_LLM_API_KEY per review
No consumers after #262 removed llmProvider from bundle entries.
WORKFLOW_CURSOR_WORKSPACE has no env var counterpart, not added.

小橘 🍊
2026-05-15 01:24:29 +00:00
xiaoju 9576d69032 chore: enter changeset pre mode (alpha), version 0.5.0-alpha.0
小橘 🍊
2026-05-15 01:22:44 +00:00
xiaoju 64dadf114d fix: align .env.example with actual env vars
- Remove non-existent: WORKFLOW_LLM_BASE_URL, WORKFLOW_LLM_MODEL, WORKFLOW_CURSOR_LLM_PROVIDER
- Add missing: WORKFLOW_CURSOR_COMMAND (required for develop workflow)

小橘 🍊
2026-05-15 01:20:38 +00:00
xiaomo baaa1d1dc8 Merge pull request 'chore: biome format fix + pre-push hook' (#268) from chore/biome-fix-and-pre-push-hook into main 2026-05-15 01:20:38 +00:00
xiaoju 3074cd5f0c chore: add .env.example with all supported env vars
Documents all environment variables used across the workflow engine:
- LLM config (base URL, API key, model)
- Cursor agent config (model, timeout, provider)
- Hermes agent config (model, timeout)
- Storage and display options

Fixes #205
2026-05-15 01:20:38 +00:00
xingyue 15edc99c72 chore: add pre-push hook (check + test) and fix lint-log-tags for macOS 2026-05-15 09:11:39 +08:00
xingyue 153178c545 fix: biome format issues (12 errors) 2026-05-15 09:10:39 +08:00
xiaomo fac215bd21 Merge pull request 'chore(dashboard): remove unused _parentRequired param' (#267) from chore/remove-parentRequired-param into main 2026-05-15 00:30:51 +00:00
xingyue 9822e68c55 chore(dashboard): remove unused _parentRequired param from flattenSchema 2026-05-15 08:25:39 +08:00
xiaomo 764b73209e Merge pull request 'feat(dashboard): workflow detail 独立子页面' (#264) from feat/workflow-detail-layout into main 2026-05-15 00:23:45 +00:00
xiaomo e7987c4cd7 Merge pull request 'fix(cli): race condition in thread rm + flaky test' (#266) from fix/265-flaky-thread-rm into main 2026-05-15 00:21:16 +00:00
xiaoju 942ff4b1a4 fix(cli): race condition in thread rm + flaky test (#265)
Two fixes:
1. cmdThreadRemove: always call both removeThreadEntry and
   removeThreadHistoryEntries regardless of resolved source,
   preventing race where thread moves from active to history
   between resolve and delete.
2. Test: add waitUntilRunningFileAbsent before thread show/rm,
   matching the pattern used by adjacent test cases.

Verified 5x consecutive runs with 0 failures.

Closes #265
2026-05-14 13:17:04 +00:00
xiaomo 71ccf8d03c Merge pull request 'chore(util-agent): remove dead createTextAdapter / TextProducerFn' (#263) from chore/252-remove-text-adapter into main 2026-05-14 12:58:22 +00:00
xingyue 510b49287a Merge pull request 'feat(dashboard): redesign workflow detail layout' (#257) from feat/workflow-detail-layout into main 2026-05-14 12:58:04 +00:00
xiaoju bb6b309efa chore(util-agent): remove dead createTextAdapter / TextProducerFn
All adapters migrated to createAgentAdapter in #261. No consumers remain.

Refs #252
2026-05-14 12:23:01 +00:00
xiaomo 56db22a908 Merge pull request 'refactor(agents): migrate LLM/Hermes/Cursor to createAgentAdapter' (#262) from feat/261-adapter-migration into main 2026-05-14 12:19:37 +00:00
xiaoju 2a1b7b0aeb refactor(agents): migrate LLM/Hermes/Cursor to createAgentAdapter
- LLM: AgentFn<{prompt}> + createAgentAdapter, chatCompletionText unchanged
- Hermes: AgentFn<{prompt}> + createAgentAdapter, config validation in extract
- Cursor: AgentFn<{prompt, workspace}> + createAgentAdapter, workspace
  extraction moved to extract fn, AgentFn itself only receives resolved options

All public API signatures preserved. createTextAdapter/TextProducerFn retained.

Closes #261, Phase 2 of #252
2026-05-14 10:22:37 +00:00
xiaomo 07f52594d1 Merge pull request 'feat(protocol): AgentFn<Opt> type + createAgentAdapter bridge' (#256) from feat/252-agent-fn into main 2026-05-14 08:53:30 +00:00
xiaoju 4582274ba4 feat(protocol): AgentFn<Opt> type + createAgentAdapter bridge
Add AgentFn<Opt = void> as the formal agent boundary type:
- Input: ThreadContext (fixed), Output: string (fixed)
- Opt: agent-specific structured options (e.g. { workspace } for Cursor)

Add createAgentAdapter<Opt>(agent, extract) → AdapterFn bridge in
workflow-util-agent, plus createSimpleAgentAdapter for Opt = void.

Also fixes: workflow-cas composite flag + cursor tsconfig reference.

Refs #252
2026-05-14 08:41:22 +00:00
481 changed files with 15806 additions and 3184 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@uncaged/workflow-util": patch
---
Replace optionalEnv/requireEnv with unified env(name, fallback) API
+5
View File
@@ -0,0 +1,5 @@
---
"@uncaged/workflow-protocol": patch
---
fix: correct internal dependency versions for prerelease
+5
View File
@@ -0,0 +1,5 @@
---
"@uncaged/workflow-util-agent": patch
---
fix: include create-agent-adapter.ts in published src
+5
View File
@@ -0,0 +1,5 @@
---
"@uncaged/workflow-protocol": patch
---
fix: use npm publish with pinned deps instead of bun publish (workspace:^ resolution bug)
+30
View File
@@ -0,0 +1,30 @@
{
"mode": "pre",
"tag": "alpha",
"initialVersions": {
"@uncaged/cli-workflow": "0.4.5",
"@uncaged/workflow-agent-cursor": "0.4.5",
"@uncaged/workflow-agent-hermes": "0.4.5",
"@uncaged/workflow-agent-llm": "0.4.5",
"@uncaged/workflow-agent-react": "0.4.5",
"@uncaged/workflow-cas": "0.4.5",
"@uncaged/workflow-dashboard": "0.1.0",
"@uncaged/workflow-execute": "0.4.5",
"@uncaged/workflow-gateway": "0.4.5",
"@uncaged/workflow-protocol": "0.4.5",
"@uncaged/workflow-reactor": "0.4.5",
"@uncaged/workflow-register": "0.4.5",
"@uncaged/workflow-runtime": "0.4.5",
"@uncaged/workflow-template-develop": "0.4.5",
"@uncaged/workflow-template-solve-issue": "0.4.5",
"@uncaged/workflow-util": "0.4.5",
"@uncaged/workflow-util-agent": "0.4.5"
},
"changesets": [
"env-api-unify",
"fix-internal-deps",
"fix-publish-src",
"fix-workspace-deps",
"rfc-252-agent-fn"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
"@uncaged/workflow-protocol": minor
---
feat: AgentFn<Opt> type boundary and createAgentAdapter bridging function (RFC #252)
+7 -3
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
# pre-push hook: typecheck + biome + lint-log-tags
set -euo pipefail
echo "🔍 pre-push: running checks..."
echo "🔍 Running check (tsc + biome + lint-log-tags)..."
bun run check
echo "✅ pre-push: all checks passed"
echo "🧪 Running tests..."
bun run test
echo "✅ All checks passed!"
+5
View File
@@ -6,3 +6,8 @@ tsconfig.tsbuildinfo
.npmrc
bunfig.toml
xiaoju/
solve-issue-entry.ts
packages/workflow-template-develop/develop.esm.js
.DS_Store
*.py
+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.
+20 -2
View File
@@ -1,7 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"files": {
"includes": ["**", "!**/dist", "!**/node_modules", "!packages/workflow/workflow"]
"includes": [
"**",
"!**/dist",
"!**/node_modules",
"!**/legacy-packages",
"!scripts",
"!packages/workflow/workflow",
"!xiaoju/scripts/bundle.ts"
]
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"formatter": {
@@ -30,7 +38,7 @@
}
},
{
"includes": ["**/*.d.ts"],
"includes": ["**/*.d.ts", "**/vitest.config.*"],
"linter": {
"rules": {
"style": {
@@ -38,6 +46,16 @@
}
}
}
},
{
"includes": ["**/cli.ts", "**/setup.ts"],
"linter": {
"rules": {
"suspicious": {
"noConsole": "off"
}
}
}
}
],
"linter": {
-15
View File
@@ -1,15 +0,0 @@
import { createCursorAgent } from "./packages/workflow-agent-cursor/src/index.js";
import { createWorkflow } from "./packages/workflow-runtime/src/create-workflow.js";
import {
buildDevelopDescriptor,
developWorkflowDefinition,
} from "./packages/workflow-template-develop/src/index.js";
const agent = createCursorAgent({
command: "/home/azureuser/.local/bin/cursor-agent",
model: "auto",
timeout: 300_000,
});
export const descriptor = buildDevelopDescriptor();
export const run = createWorkflow(developWorkflowDefinition, { adapter: agent, overrides: null });
+406 -179
View File
@@ -1,268 +1,495 @@
# Uncaged workflow — Architecture
# Workflow Engine — Architecture
**Last updated:** 2026-05-09
**Last updated:** 2026-05-19
---
## Overview
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). No daemon — processes start on demand and exit when done.
A stateless workflow engine driven by a single-step CLI. 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.
The implementation lives in **15** Bun workspace packages under `packages/`, using the `workspace:*` protocol.
The implementation lives in **6** active packages under `packages/`, plus two external CAS packages (`@uncaged/json-cas`, `@uncaged/json-cas-fs`). Legacy packages reside in `legacy-packages/` and are not part of the active stack.
## Package map
Grouped by responsibility (npm name → folder).
| Layer | Package | One-line role |
|-------|---------|----------------|
| Contract | `@uncaged/workflow-protocol``workflow-protocol` | Shared TypeScript types and `Result` helpers; peer `zod` only — no other workspace deps. |
| Author API | `@uncaged/workflow-runtime``workflow-runtime` | `createWorkflow` and re-exports of protocol workflow types for bundle authors. |
| Shared infra | `@uncaged/workflow-util``workflow-util` | Base32/ULID, logger, storage root paths, global CAS dir, ref-field helpers. |
| LLM plumbing | `@uncaged/workflow-reactor``workflow-reactor` | `createLlmFn`, `createThreadReactor`, and related tool-call types for threaded LLM invocation. |
| CAS | `@uncaged/workflow-cas``workflow-cas` | `CasStore` implementation, XXH64 hashing, Merkle helpers over CAS payloads. |
| Registry / bundles | `@uncaged/workflow-register``workflow-register` | Bundle validation & dynamic export extraction, `workflow.yaml` registry I/O, provider/model resolution. |
| Engine | `@uncaged/workflow-execute``workflow-execute` | Thread execution, worker entry path, fork/GC, extract pipeline, `workflowAsAgent`. |
| CLI | `@uncaged/cli-workflow``cli-workflow` | `uncaged-workflow` binary (depends on engine, registry, CAS, protocol, util, runtime). |
| Agent adapters | `@uncaged/workflow-agent-cursor``workflow-agent-cursor` | `AgentFn` via `cursor-agent` CLI + workspace extraction. |
| | `@uncaged/workflow-agent-hermes``workflow-agent-hermes` | `AgentFn` via `hermes chat` CLI. |
| | `@uncaged/workflow-agent-llm``workflow-agent-llm` | `AgentFn` via OpenAI-compatible HTTP (`LlmProvider` from runtime). |
| Agent shared | `@uncaged/workflow-util-agent``workflow-util-agent` | `buildAgentPrompt`, `spawnCli` for CLI-backed agents. |
| Templates | `@uncaged/workflow-template-develop``workflow-template-develop` | Develop workflow definition, roles, descriptor builder. |
| | `@uncaged/workflow-template-solve-issue``workflow-template-solve-issue` | Solve-issue workflow definition, roles, descriptor builder. |
| Dashboard | `@uncaged/workflow-dashboard``workflow-dashboard` | Private Vite + React app (`src/main.tsx`); only `react` / `react-dom` dependencies — no workspace packages. |
|-------|---------|---------------|
| 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/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. |
## Dependency graph (workspace packages)
### External dependencies
Bottom-up layering for the execution stack:
| Package | Role |
|---------|------|
| `@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 `workflow-moderator`). |
| `commander` | CLI argument parsing (used by `cli-workflow`). |
| `dotenv` | Loads `.env` files for API keys. |
| `yaml` | YAML parse/stringify. |
## Dependency graph
```mermaid
flowchart BT
subgraph External
jcas["@uncaged/json-cas"]
jcasfs["@uncaged/json-cas-fs"]
end
subgraph L0["Layer 0 — contract"]
protocol["@uncaged/workflow-protocol"]
end
subgraph L1["Layer 1 — on protocol"]
runtime["@uncaged/workflow-runtime"]
subgraph L1["Layer 1 — shared"]
util["@uncaged/workflow-util"]
reactor["@uncaged/workflow-reactor"]
moderator["@uncaged/workflow-moderator"]
end
subgraph L2["Layer 2 — protocol + util"]
cas["@uncaged/workflow-cas"]
register["@uncaged/workflow-register"]
subgraph L2["Layer 2 — agent framework"]
kit["@uncaged/workflow-agent-kit"]
end
subgraph L3["Layer 3 — engine"]
execute["@uncaged/workflow-execute"]
subgraph L3["Layer 3 — agent implementations"]
hermes["@uncaged/workflow-agent-hermes"]
end
subgraph L4["Layer 4 — CLI"]
cli["@uncaged/cli-workflow"]
end
runtime --> protocol
protocol --> jcasfs
util --> protocol
reactor --> protocol
cas --> protocol
cas --> util
register --> protocol
register --> util
execute --> protocol
execute --> runtime
execute --> util
execute --> cas
execute --> reactor
execute --> register
moderator --> protocol
kit --> protocol
kit --> util
kit --> jcas
kit --> jcasfs
hermes --> kit
hermes --> jcas
cli --> protocol
cli --> util
cli --> cas
cli --> execute
cli --> register
cli --> runtime
cli --> kit
cli --> moderator
cli --> jcas
cli --> jcasfs
```
**Adjacent consumers** (not in the main CLI stack):
## Workflow definition
- `@uncaged/workflow-util-agent``@uncaged/workflow-runtime`
- `@uncaged/workflow-agent-llm``@uncaged/workflow-runtime`
- `@uncaged/workflow-agent-cursor``@uncaged/workflow-runtime`, `@uncaged/workflow-util-agent`, `zod`
- `@uncaged/workflow-agent-hermes``@uncaged/workflow-runtime`, `@uncaged/workflow-util-agent`
- `@uncaged/workflow-template-develop``@uncaged/workflow-register`, `@uncaged/workflow-runtime`, `zod`
- `@uncaged/workflow-template-solve-issue``@uncaged/workflow-register`, `@uncaged/workflow-runtime`, `zod` (dev-only workspace deps: `@uncaged/workflow-cas`, `@uncaged/workflow-execute` for tests/tooling per `package.json`)
Workflows are **YAML files** (not ESM bundles). `uwf workflow put <file.yaml>` parses the YAML, registers output schemas as JSON Schema CAS nodes, and stores the `WorkflowPayload` as a CAS node.
## Package roles (detail)
Example (`examples/solve-issue.yaml`):
- **`workflow-protocol`** — Pure types (`WorkflowFn`, contexts, `CasStore` interface, descriptor shapes), `START` / `END`, `ok` / `err`. Depends only on peer `zod` for schema-related types in signatures.
- **`workflow-runtime`** — Workflow author surface: `createWorkflow` from `src/create-workflow.js`, re-exports protocol types/constants used when authoring bundles.
- **`workflow-util`** — Cross-cutting utilities: Crockford Base32, ULID, `createLogger`, `getDefaultWorkflowStorageRoot`, `getGlobalCasDir`, ref normalization; re-exports `ok`/`err` from protocol.
- **`workflow-cas`** — Filesystem CAS (`createCasStore`), `hashString` / `hashWorkflowBundleBytes`, Merkle node serialization and helpers (`merkle.js`).
- **`workflow-register`** — Bundle pipeline (`validateWorkflowBundle`, `extractBundleExports`, descriptor builders), registry YAML read/write, `resolveModel` / `splitProviderModelRef`.
- **`workflow-execute`** — `executeThread`, supervisor/worker wiring (`engine/`), fork/GC/pause gate, `createExtract` + LLM extract helpers (`extract/`), `workflowAsAgent`. Imports `@uncaged/workflow-reactor` for LLM-backed extract/supervisor paths (`extract-fn.ts`, `supervisor.ts`).
- **`workflow-reactor`** — `createLlmFn`, `createThreadReactor`, and thread tool-invocation types — consumed by `workflow-execute`.
- **`cli-workflow`** — CLI commands and HTTP/dashboard-related wiring (`hono`, `yaml`); composes register + execute + CAS + util.
- **`workflow-agent-*`** — Replaceable `AgentFn` implementations (Cursor / Hermes CLIs, or HTTP LLM).
- **`workflow-util-agent`** — Shared prompt assembly and subprocess spawning for CLI agents.
- **`workflow-template-*`** — Concrete `WorkflowDefinition` graphs + Zod role schemas + descriptor builders for publishing bundles.
- **`workflow-dashboard`** — Standalone React UI; no published library entry matching `src/index.ts`.
```yaml
name: "solve-issue"
description: "End-to-end issue resolution"
roles:
planner:
description: "Creates implementation plan"
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 }
steps: { type: array, items: { type: string } }
required: [plan, steps]
developer:
description: "Implements code changes"
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 } }
summary: { type: string }
required: [filesChanged, summary]
reviewer:
description: "Reviews code changes"
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 }
comments: { type: string }
required: [approved, comments]
conditions:
notApproved:
description: "Reviewer rejected the implementation"
expression: "steps[-1].output.approved = false"
graph:
$START:
- role: "planner"
condition: null
planner:
- role: "developer"
condition: null
developer:
- role: "reviewer"
condition: null
reviewer:
- role: "developer"
condition: "notApproved"
- role: "$END"
condition: null
```
Key properties:
- **`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`
- **No Zod** — all schemas are JSON Schema, validated through `@uncaged/json-cas`
## Three-phase engine loop
Each role round is implemented in `packages/workflow-runtime/src/create-workflow.ts` (`advanceOneRound`): moderator → agent → extractor, with progressive context types from `@uncaged/workflow-protocol`.
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
Context: ModeratorContext { threadId, depth, start, steps }
Action: moderator(ctx) → role name | END
Input: WorkflowPayload + ModeratorContext { start, steps[] }
Engine: JSONata conditions evaluated against the graph
│ Output: next role name | $END
│ Phase 2: AGENT
Context: AgentContext = ModeratorCtx + { currentRole: { name, systemPrompt } }
Action: agent(ctx) → raw string
Input: thread-id + role (via argv)
Engine: agent-kit builds context from CAS chain, prepends
│ output format instruction to system prompt, spawns agent
│ Output: raw string (frontmatter markdown)
│ Phase 3: EXTRACTOR
Context: ExtractContext = AgentCtx + { agentContent }
Action: runtime.extract(schema, extractPrompt, ctx) → typed meta
│ Phase 3: EXTRACT
Input: raw agent output + role's meta schema
Engine: two-layer extract (frontmatter fast path → LLM fallback)
│ Output: CasRef to structured output node
Merge: RoleStep { role, contentHash, meta, refs, timestamp }
Append to steps
└─────────────────────────────────────────────────────┘
Persist: StepNode { start, prev, role, output, detail, agent }
Update: threads.yaml head pointer
└─────────────────────────────────────────────────────────────────
```
### Context types (progressive)
### Context types
Defined in `packages/workflow-protocol/src/types.ts`:
```typescript
type ModeratorContext<M> = ThreadContext<M>;
type AgentContext<M> = ModeratorContext<M> & {
currentRole: { name: string; systemPrompt: string };
type StepContext = {
role: string;
output: unknown; // CAS node payload, expanded (not hash)
detail: CasRef;
agent: string;
};
type ModeratorContext = {
start: StartNodePayload; // { workflow: CasRef, prompt: string }
steps: StepContext[]; // chronological, oldest first
};
type AgentContext = ModeratorContext & {
threadId: ThreadId;
role: string;
store: Store;
workflow: WorkflowPayload;
outputFormatInstruction: string;
};
type ExtractContext<M> = AgentContext<M> & { agentContent: string };
```
### Key properties
- **Moderator is synchronous and pure** — no I/O, no state mutation inside `createWorkflow`’s moderator call path.
- **Agent receives `AgentContext`** — reads `ctx.currentRole.systemPrompt`; raw output becomes `agentContent` for extract.
- **Extractor is `WorkflowRuntime.extract`** — supplied by the engine from registry-resolved LLM config (`workflow-execute`); stores agent body in CAS and yields `contentHash` + `refs` on each step (`create-workflow.ts`).
- **`extractPrompt` is a call parameter** on `RoleDefinition`, not implicit context state.
- **Moderator** — pure JSONata evaluation; no LLM call, no I/O beyond CAS reads. Evaluates `workflow.graph[currentRole]` transitions in order, returns first match.
- **Agent** — receives `AgentContext` with thread history + role system prompt + output format instruction. Raw output is frontmatter markdown.
- **Extractor** — two-layer: tries frontmatter fast-path first (zero LLM cost), falls back to LLM extract if frontmatter is absent or invalid.
- **Stateless** — each `uwf thread step` is an atomic, self-contained operation. No in-memory state between steps.
## Agent information sources
## Agent CLI protocol
An agent has exactly three information sources:
Each agent is an external command invoked by `uwf thread step`:
1. **Prior knowledge** — LLM training, agent memory, agent skills
2. **Thread context**`AgentContext` (`start`, `steps`, `currentRole`)
3. **Derived information** — from 1 & 2 (e.g. tool calls, shell commands)
No hidden environment parameters. If an agent needs something (like a workspace path), it obtains it via `ExtractFn` (e.g. Cursor agent).
## Bundle contract
A workflow bundle is a single `.esm.js` file with two named exports (see `WorkflowFn` / `WorkflowDescriptor` in `packages/workflow-protocol/src/types.ts`):
```typescript
export const descriptor: WorkflowDescriptor;
export const run: WorkflowFn;
type WorkflowFn = (
thread: ThreadContext,
runtime: WorkflowRuntime,
) => AsyncGenerator<RoleOutput, WorkflowCompletion>;
```bash
<agent-cmd> <thread-id> <role>
```
`RoleOutput` carries `contentHash`, `meta`, and `refs` (agent text lives in CAS, addressed by hash).
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. `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 `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)
- Prints the new `StepNode` CAS hash to stdout
4. `uwf thread step` reads stdout, updates `threads.yaml` head pointer, re-evaluates moderator for `done`
5. Exit 0 = success, non-zero = failure
### Constraints
Agent resolution priority: `--agent` CLI override → `config.yaml` per-workflow/role override → `config.yaml` `defaultAgent`.
- Single `.esm.js` file
- No dynamic `import()` in bundles (loader exempt in engine)
- Portable bundle static imports are constrained by validation in `@uncaged/workflow-register` (`validateWorkflowBundle`)
- XXH64 hash (Crockford Base32) = version ID
## Agent output format: frontmatter markdown (RFC #351)
### Why AsyncGenerator?
Agents produce **frontmatter markdown** — YAML frontmatter for structured meta, followed by a markdown body for content:
- Each `yield` lets `workflow-execute` persist state, CAS rows, and enforce pause/abort
- `return` supplies `WorkflowCompletion`
- Fork replays historical steps into a new thread context
- Bundle does not import the engine — only protocol/runtime types at build time
```markdown
---
status: done
next: reviewer
confidence: 0.9
artifacts:
- src/auth.ts
scope: role
---
## Implementation
Fixed the login redirect by updating the auth middleware...
```
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 (`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 `meta` schema
5. Validate with `json-cas` schema validation
6. If valid → return `outputHash` (zero LLM cost)
### Layer 2: LLM extract fallback (`extract.ts`)
If the fast path returns `null` (no frontmatter, invalid, or doesn't satisfy schema):
1. Resolve extract model alias from config (`modelOverrides.extract``models.extract``defaultModel`)
2. Call OpenAI-compatible chat completion with JSON mode
3. System prompt: "Extract structured data matching this JSON Schema: ..."
4. User message: the raw agent output
5. Parse response, `store.put()`, validate
6. Return `outputHash`
## Prompt injection
`workflow-agent-kit` prepends two pieces of context to the agent's system prompt:
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.
## CAS node types
### Workflow
```yaml
type: <workflow-schema-hash>
payload:
name: "solve-issue"
description: "End-to-end issue resolution"
roles:
planner:
description: "Creates implementation plan"
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"
expression: "steps[-1].output.approved = false"
graph:
$START:
- role: "planner"
condition: null
```
### StartNode
```yaml
type: <start-node-schema-hash>
payload:
workflow: "4KNM2PXR3B1QW" # cas_ref → Workflow
prompt: "Fix the login bug..."
```
### StepNode
```yaml
type: <step-node-schema-hash>
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 meta schema)
detail: "7BQST3VW9F2MA" # cas_ref → execution detail (raw turns, session data)
agent: "uwf-hermes" # agent command used (plain string)
```
### Chain structure
```
threads.yaml: { "01J7K9...4T": "8FWKR3TN5V1QA" }
StepNode (step 3)
├── start ──→ StartNode
│ ├── workflow → Workflow (CAS)
│ └── prompt: "Fix..."
├── prev ──→ StepNode (step 2)
│ ├── prev ──→ StepNode (step 1)
│ │ └── prev: null
│ └── ...
├── role: "reviewer"
├── output → CAS({ approved: true })
├── detail → CAS(session turns)
└── agent: "uwf-hermes"
```
## Storage layout
```
~/.uncaged/workflow/
├── cas/ # Global content-addressed blobs (see getGlobalCasDir)
├── bundles/
│ ├── C9NMV6V2TQT81.esm.js # Crockford Base32 of XXH64
│ ├── C9NMV6V2TQT81.yaml # Role descriptor sidecar (when present)
│ └── C9NMV6V2TQT81/ # Per-hash bundle dir (alongside or instead of loose files)
│ ├── threads.json # Active threads: threadId → { head, start, updatedAt }
│ └── history/
│ └── 2026-05-09.jsonl # Completed threads (one JSON object per line)
├── logs/ # One folder per bundle hash
│ └── C9NMV6V2TQT81/
│ ├── 01KQXKW…YG.running # Present while worker executes this thread (optional)
│ └── 01KQXKW…YG.info.jsonl # Debug log
└── workflow.yaml # Registry
├── cas/ # json-cas filesystem store (all CAS nodes)
├── config.yaml # Provider, model, agent configuration
├── threads.yaml # Active thread head pointers: threadId → CasRef
├── history.jsonl # Archived thread records
├── registry.yaml # Workflow name → CAS hash mapping
└── .env # API keys (loaded by dotenv)
```
### Mutable state
Only three files carry mutable state:
| File | Contents |
|------|----------|
| `threads.yaml` | `Record<ThreadId, CasRef>` — maps active thread IDs to head node hash |
| `history.jsonl` | Append-only log of completed threads (`thread`, `workflow`, `head`, `completedAt`) |
| `registry.yaml` | Workflow name → current CAS hash |
Everything else is immutable CAS content.
### ID encoding: Crockford Base32
- Case-insensitive, filesystem-safe, no ambiguous chars (0/O, 1/I/L)
- Bundle hash: XXH64 → 13-char
- Thread ID: ULID → 26-char (10 timestamp + 16 random)
- CAS hash: XXH64 → 13-char Crockford Base32
- Thread ID: ULID → 26-char Crockford Base32 (10 timestamp + 16 random)
### Registry (`workflow.yaml`)
### Config (`config.yaml`)
Managed by `@uncaged/workflow-register` (`readWorkflowRegistry`, `writeWorkflowRegistry`, …). Shape includes workflow entries and a top-level `config` section used for extract/supervisor model resolution.
```yaml
providers:
openrouter:
baseUrl: "https://openrouter.ai/api/v1"
apiKeyEnv: "OPENROUTER_API_KEY"
### Thread storage (CAS + index)
models:
sonnet:
provider: "openrouter"
name: "anthropic/claude-sonnet-4"
gpt4o-mini:
provider: "openai"
name: "gpt-4o-mini"
Thread execution state is a chain of immutable CAS nodes (`StartNode`, `StateNode`, content Merkle blobs). Per bundle:
agents:
hermes:
command: "uwf-hermes"
args: []
cursor:
command: "uwf-cursor"
args: []
- **`threads.json`** — only in-flight threads (`head`, `start`, `updatedAt`).
- **`history/{YYYY-MM-DD}.jsonl`** — completed threads (`threadId`, `head`, `start`, `completedAt`).
- **CAS (`cas/`)** — payloads and refs for replay, GC, and fork sharing.
defaultAgent: "hermes"
agentOverrides:
solve-issue:
developer: "cursor"
**`.info.jsonl`** — Structured debug log via `@uncaged/workflow-util` `createLogger`:
```jsonc
{ "tag": "4KNMR2PX", "content": "Loading bundle...", "timestamp": ... }
defaultModel: "sonnet"
modelOverrides:
extract: "gpt4o-mini"
```
Tags are 8-char Crockford Base32 (40-bit random), one per call site. `grep "4KNMR2PX"` → code location.
## Execution model
- **No daemon.** `uncaged-workflow run <name>` starts a worker process (`workflow-execute` worker entry via `getWorkerHostScriptPath`)
- Threads share bundle-scoped workers as implemented in CLI/engine
- Pause/resume/abort via engine IPC and pause gate (`createThreadPauseGate`)
## CLI commands
| Priority | Command | Description |
|----------|---------|-------------|
| P1 | `add <name> <file.esm.js>` | Register a bundle |
| P1 | `list` | List registered workflows |
| P1 | `show <name>` | Show workflow details |
| P1 | `remove <name>` | Remove a workflow |
| P1 | `run <name> [--prompt] [--max-rounds]` | Start a thread |
| P1 | `threads [name]` | List threads |
| P1 | `thread <id>` | Show thread state |
| P1 | `thread rm <id>` | Delete a thread |
| P1 | `ps` | List running threads |
| P1 | `kill <thread-id>` | Terminate a running thread |
| P2 | `history <name>` | Show version history |
| P2 | `rollback <name> [hash]` | Switch to a previous version |
| P2 | `pause <thread-id>` | Pause a running thread |
| P2 | `resume <thread-id>` | Resume a paused thread |
| P3 | `fork <thread-id> [--from-role <role>]` | Fork from historical state |
Binary: `uwf`
### Thread commands
| Command | Description |
|---------|-------------|
| `uwf thread start <workflow> -p <prompt>` | Create a thread (StartNode → CAS, head → threads.yaml). No execution. |
| `uwf thread step <thread-id> [--agent <cmd>]` | Execute one moderator→agent→extract cycle. |
| `uwf thread show <thread-id>` | Show thread head pointer and done status. |
| `uwf thread list [--all]` | List active threads (`--all` includes archived). |
| `uwf thread steps <thread-id>` | List all steps in chronological order. |
| `uwf thread read <thread-id> [--quota <chars>] [--before <hash>]` | Render thread as human-readable markdown. |
| `uwf thread fork <step-hash>` | Fork a thread from a specific CAS node. |
| `uwf thread step-details <step-hash>` | Dump full detail node as YAML. |
| `uwf thread kill <thread-id>` | Terminate and archive a thread. |
### Workflow commands
| Command | Description |
|---------|-------------|
| `uwf workflow put <file.yaml>` | Register a workflow from YAML definition. |
| `uwf workflow show <id>` | Show workflow by name or CAS hash. |
| `uwf workflow list` | List registered workflows. |
### CAS commands
| Command | Description |
|---------|-------------|
| `uwf cas get <hash>` | Read a CAS node. |
| `uwf cas put <type-hash> <data>` | Store a node, print its hash. |
| `uwf cas has <hash>` | Check if a hash exists. |
| `uwf cas refs <hash>` | List direct CAS references. |
| `uwf cas walk <hash>` | Recursive traversal from a node. |
| `uwf cas reindex` | Rebuild type index from all nodes. |
| `uwf cas schema list` | List registered schemas. |
| `uwf cas schema get <hash>` | Show a schema by type hash. |
### Setup
| Command | Description |
|---------|-------------|
| `uwf setup [--provider --base-url --api-key --model --agent]` | Configure provider/model/agent (interactive if no flags). |
## Toolchain
| Tool | Purpose |
|------|---------|
| **bun** | Package manager + runtime |
| **TypeScript** | Type checking (strict mode) |
| **Biome** | Lint + format |
| **vitest** | Test runner |
## Design decisions
| Decision | Rationale |
|----------|-----------|
| **Role = pure data** | Decouples definition from execution; same role with different agents |
| **Agent bound at runtime** | `WorkflowDefinition` is reusable; agent choice is deployment concern |
| **Three-phase context** | Each phase sees only what it needs; types live in `workflow-protocol` |
| **`WorkflowRuntime.extract` + CAS `contentHash`** | Large agent bodies deduplicated globally; Merkle roots summarize threads |
| **`workflow-reactor` split** | LLM tool-calling loop isolated from filesystem/registry concerns |
| **Single-file ESM** | Hash = version, self-contained bundle |
| **No daemon** | OS handles process lifecycle |
| **Crockford Base32** | Filesystem-safe, readable, compact |
| **15-package split** | Clear boundaries: protocol ↔ runtime author API ↔ util/CAS/register ↔ execute ↔ CLI ↔ agents/templates/UI |
| **YAML workflow definitions** | Human-readable, versionable, no build step required. JSON Schema inline in YAML, registered as CAS nodes on `workflow put`. |
| **Stateless single-step CLI** | Each `uwf thread step` is atomic — no in-memory state, no daemon, no long-running process. OS handles lifecycle. |
| **CAS-backed thread state** | Immutable linked nodes enable fork, replay, and GC without copying data. Content-addressed deduplication across threads. |
| **JSONata moderator** | Declarative condition expressions evaluated against thread history. No LLM cost for routing decisions. |
| **Frontmatter markdown output** | Agents produce structured meta (YAML frontmatter) alongside free-form content (markdown body). Enables zero-cost extraction when frontmatter is well-formed. |
| **Two-layer extract** | Fast path avoids LLM calls when agents follow the format; LLM fallback handles messy output gracefully. |
| **Prompt injection for format** | Output format instruction prepended to system prompt ensures agents produce parseable output without per-agent configuration. |
| **JSON Schema (not Zod)** | Schemas are CAS-native data — storable, hashable, validatable through `json-cas`. No code generation, no runtime library dependency. |
| **Agent as external command** | Agents are independent CLI binaries (`uwf-hermes`, `uwf-cursor`). Swappable per workflow/role via config. No tight coupling to the engine. |
| **No daemon** | Process starts, does one step, exits. Simpler failure model, no connection management. |
| **Crockford Base32** | Filesystem-safe, case-insensitive, readable, compact. |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,387 @@
# 设计文档:office-agent 文档生成/编辑 Workflow 体系
**日期:** 2026-05-18
---
## 概述
在 monorepo 中新增三个包,实现通过 `office-agent` CLI 生成或编辑 Word 文档的完整 workflow 体系。
| 包 | npm name | 职责 |
|---|---|---|
| `workflow-template-document` | `@uncaged/workflow-template-document` | 纯结构:角色定义、meta schema、调度表、descriptor |
| `workflow-agent-office` | `@uncaged/workflow-agent-office` | writer 角色执行器:调用 `office-agent` CLI |
| `workflow-agent-docx-diff` | `@uncaged/workflow-agent-docx-diff` | differ 角色执行器:调用 `docx-diff` CLI |
Template 只定义结构,不含执行逻辑。执行器与 template 解耦。
---
## 一、`workflow-template-document`
### Thread 启动输入
```typescript
// src/types.ts
type DocumentStartInput = {
prompt: string; // 用户指令
inputDocx: string | null; // null = 生成模式;本机绝对路径 = 编辑模式
};
```
start.content 为 JSON `{ prompt, inputDocx }` 或纯文本(fallback:generate 模式,整段作为 prompt)。
### 角色与 Meta
`WriterMeta` 使用 discriminated union,在 schema 层区分两种模式:
```typescript
const writerMetaSchema = z.discriminatedUnion("mode", [
z.object({
mode: z.literal("generate"),
outputDocx: z.string(), // 生成产物绝对路径
sourceDocx: z.null(),
}),
z.object({
mode: z.literal("edit"),
outputDocx: z.string(), // 修改后产物:<outputDir>/modified.docx
sourceDocx: z.string(), // 原始副本:<outputDir>/original.docx
}),
]);
type WriterMeta = z.infer<typeof writerMetaSchema>;
// differ:仅编辑模式执行
const differMetaSchema = z.object({
sourceDocx: z.string(),
modifiedDocx: z.string(),
diffDocx: z.string(),
});
type DifferMeta = z.infer<typeof differMetaSchema>;
```
两个角色的 `systemPrompt` 均为 `""`
### 调度表
```
START → writer ──(mode = "edit")──→ differ → END
↘(mode = "generate")→ END
```
### 公开导出
template 导出两个对象供消费方使用:
- `documentWorkflowDefinition: WorkflowDefinition<DocumentMeta>` — 传入 `createWorkflow``def` 参数
- `buildDocumentDescriptor(): WorkflowDescriptor` — bundle 导出用
```typescript
// bundle 侧用法
export const descriptor = buildDocumentDescriptor();
export const run = createWorkflow(documentWorkflowDefinition, { adapter, overrides });
```
### 包文件结构
```
packages/workflow-template-document/
src/
types.ts # DocumentStartInput
roles/
writer.ts # writerMetaSchema, WriterMeta, writerRole
differ.ts # differMetaSchema, DifferMeta, differRole
index.ts
roles.ts # DocumentMeta, documentRoles
moderator.ts # writerIsEditMode condition + documentTable
definition.ts # documentWorkflowDefinition
descriptor.ts # buildDocumentDescriptor()
index.ts
__tests__/
moderator.test.ts
package.json
tsconfig.json
```
### 依赖
```json
{
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-runtime": "workspace:^",
"@uncaged/workflow-register": "workspace:^",
"zod": "^4.0.0"
}
```
---
## 二、`workflow-agent-office`
### office-agent CLI 接口
```bash
# 生成模式:在 CWD 生成 output.docx
office-agent create "<prompt>" -o output.docx
# 编辑模式:在 CWD 对 modified.docx 进行修改(覆写)
office-agent edit modified.docx "<instruction>"
```
- 两个命令均为阻塞调用(CLI 内部消费 SSE,退出即完成)
- 输出文件落到调用方设定的 CWD
- 退出码 0 = 成功,非零 = 失败
### 文件命名约定
| 模式 | 文件 | 路径 |
|---|---|---|
| generate | 输出 | `<outputDir>/output.docx` |
| edit | 原始副本(workflow-owned 快照) | `<outputDir>/original.docx` |
| edit | 修改后产物 | `<outputDir>/modified.docx` |
edit 模式先将 `inputDocx` 复制为 `original.docx`(不可变快照),再复制为 `modified.docx`,对 `modified.docx` 调用 CLI。agent 覆写 `modified.docx``original.docx` 保持不变。differ 对比这两个 workflow-owned 文件,不依赖用户原始路径。
### 执行流程
**生成模式(`inputDocx = null`):**
1. `mkdir -p <outputDir>``<config.outputDir>/<ctx.threadId>`
2. `const command = config.command ?? "office-agent"`
3. `spawnCli(command, ["create", prompt, "-o", "output.docx"], { cwd: outputDir, timeoutMs })`
4. 验证 `outputDir/output.docx` 存在
5. 返回 `JSON.stringify({ mode: "generate", outputDocx, sourceDocx: null })`
**编辑模式(`inputDocx ≠ null`):**
1. `mkdir -p <outputDir>`
2. `copyFile(inputDocx, <outputDir>/original.docx)`
3. `copyFile(inputDocx, <outputDir>/modified.docx)`
4. `const command = config.command ?? "office-agent"`
5. `spawnCli(command, ["edit", "modified.docx", prompt], { cwd: outputDir, timeoutMs })`
6. 验证 `outputDir/modified.docx` 存在
7. 返回 `JSON.stringify({ mode: "edit", outputDocx: modifiedPath, sourceDocx: originalPath })`
### AdapterFn 实现(直接实现,不经过 runtime.extract)
CLI 产出确定性 JSON,直接 `schema.parse(JSON.parse(raw))` 跳过 LLM extraction:
```typescript
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);
const raw = await runOfficeAgent(config, ctx.threadId, prompt, inputDocx);
const meta = schema.parse(JSON.parse(raw)) as T;
return { meta, childThread: null };
};
}
```
`_systemPrompt` 为 writer 角色的 systemPrompt(空字符串),实际指令从 `ctx.start.content` 解析。
### 配置
```typescript
type OfficeAgentConfig = {
outputDir: string; // 输出根目录,runner 在此下按 threadId 建子目录
command: string | null; // null → runner 内 resolve 为 "office-agent"
timeout: number | null; // null → 不设超时;单位 ms
};
```
### 错误处理
```typescript
if (!result.ok) {
const e = result.error;
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");
// "spawn_failed"
throw new Error(`office-agent: spawn failed: ${e.message}`);
}
if (!existsSync(expectedPath))
throw new Error(`office-agent: output file not found: ${expectedPath}`);
```
### packageDescriptor
```typescript
// src/package-descriptor.ts
export const packageDescriptor: PackageDescriptor = {
name: "@uncaged/workflow-agent-office",
version: "0.1.0",
capabilities: ["office-agent-cli", "docx-generate", "docx-edit"],
configSchema: {
type: "object",
required: ["outputDir"],
properties: {
outputDir: { type: "string", description: "Root directory for workflow outputs." },
command: { anyOf: [{ type: "string" }, { type: "null" }], description: "Path to office-agent CLI; null uses PATH." },
timeout: { anyOf: [{ type: "number" }, { type: "null" }], description: "Timeout in ms; null means no limit." },
},
additionalProperties: false,
},
};
```
### 包文件结构
```
packages/workflow-agent-office/
src/
types.ts # OfficeAgentConfig, OfficeAgentOpt
runner.ts # runOfficeAgent()(spawnCli 封装 + 文件验证)
agent.ts # createOfficeAgent(): AdapterFn
package-descriptor.ts # packageDescriptor
index.ts
__tests__/
runner.test.ts
agent.test.ts
package.json
tsconfig.json
```
### 依赖
```json
{
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util": "workspace:^",
"@uncaged/workflow-util-agent": "workspace:^"
}
```
---
## 三、`workflow-agent-docx-diff`
`differ` 角色专用执行器。从 `ctx.steps` 读取 `WriterMeta`,调用本地 `docx-diff` CLI。
### docx-diff 退出码约定
| 退出码 | 含义 | runner 处理 |
|---|---|---|
| 0 | 无差异 | 正常,验证 diffDocx 存在 |
| 1 | 有差异 | 正常(显式处理为成功),验证 diffDocx 存在 |
| 2+ | 错误 | throw |
runner 收到 `SpawnCliError { kind: "non_zero_exit", exitCode: 1 }` 时视为成功,验证文件后继续;`exitCode >= 2` 才 throw。
### 执行流程
```
1. 从 ctx.steps 找到 writer 步骤,读取 WriterMeta
2. 验证 mode === "edit"(否则 throw)
3. diffDocx = join(dirname(writer.outputDocx), "diff.docx")
4. const command = config.command ?? "docx-diff"
5. spawnCli(command,
[writer.sourceDocx, writer.outputDocx, "--output", "docx", "--out-file", diffDocx],
{ cwd: null, timeoutMs: null })
exit 0 或 1 → 验证 diffDocx 存在
exit 2+ → throw
6. 返回 JSON.stringify({ sourceDocx, modifiedDocx: writer.outputDocx, diffDocx })
```
### AdapterFn 实现(直接实现,不经过 runtime.extract)
```typescript
export function createDocxDiffAgent(config: DocxDiffAgentConfig = { command: null }): AdapterFn {
return <T>(_prompt: string, schema: z.ZodType<T>) =>
async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise<RoleResult<T>> => {
const writerStep = ctx.steps.find(s => s.role === "writer");
if (!writerStep) 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");
const raw = await runDocxDiff(config, writerMeta);
const meta = schema.parse(JSON.parse(raw)) as T;
return { meta, childThread: null };
};
}
```
### 配置
```typescript
type DocxDiffAgentConfig = {
command: string | null; // null → runner 内 resolve 为 "docx-diff"
};
```
### packageDescriptor
```typescript
export const packageDescriptor: PackageDescriptor = {
name: "@uncaged/workflow-agent-docx-diff",
version: "0.1.0",
capabilities: ["docx-diff-cli", "docx-diff-report"],
configSchema: {
type: "object",
properties: {
command: { anyOf: [{ type: "string" }, { type: "null" }], description: "Path to docx-diff CLI; null uses PATH." },
},
additionalProperties: false,
},
};
```
### 包文件结构
```
packages/workflow-agent-docx-diff/
src/
types.ts # DocxDiffAgentConfig
runner.ts # runDocxDiff()(exit 1 处理 + 文件验证)
agent.ts # createDocxDiffAgent(): AdapterFn
package-descriptor.ts # packageDescriptor
index.ts
__tests__/
runner.test.ts
agent.test.ts
package.json
tsconfig.json
```
### 依赖
```json
{
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util-agent": "workspace:^",
"@uncaged/workflow-template-document": "workspace:^"
}
```
---
## 四、外部 bundle(外部 workspace 消费)
```typescript
import { createOfficeAgent } from "@uncaged/workflow-agent-office";
import { createDocxDiffAgent } from "@uncaged/workflow-agent-docx-diff";
import {
buildDocumentDescriptor,
documentWorkflowDefinition,
} from "@uncaged/workflow-template-document";
import { createWorkflow } from "@uncaged/workflow-runtime";
import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util";
import { join } from "node:path";
const outputDir = join(getDefaultWorkflowStorageRoot(), "outputs");
export const descriptor = buildDocumentDescriptor();
export const run = createWorkflow(documentWorkflowDefinition, {
adapter: createOfficeAgent({ outputDir, command: null, timeout: null }),
overrides: { differ: createDocxDiffAgent() },
});
```
---
## 不在范围内
- 重试逻辑(失败直接 throw)
- office-agent server 的启停管理(假设 server 已在运行)
- docx-diff HTML/terminal 格式输出(仅 docx)
- 跨机器执行(`inputDocx` 须为本机有效绝对路径)
+539
View File
@@ -0,0 +1,539 @@
# `uwf` — Stateless Workflow CLI
> 将 workflow 引擎降维为无状态单步 CLI。Workflow 是纯数据(CAS 节点),执行是单步原子操作,agent 是可插拔外部命令。
---
## 1. CLI Design
### 1.1 命令总览
```
# thread 组
uwf thread start <workflow> -p <prompt> # 创建 thread,不执行
uwf thread step <thread-id> [--agent] # 单步执行
uwf thread show <thread-id> # thread-id → head 查询
uwf thread list [--all] # 列出活跃 threads(--all 含已归档)
uwf thread kill <thread-id> # 终结 thread,归档
# workflow 组
uwf workflow put <file.yaml> # 注册 workflow(YAML → CAS)
uwf workflow show <workflow-id> # 查看 workflow 定义
uwf workflow list # 列出已注册 workflows
```
两组对称,各 3-4 个子命令。CAS 操作交给 `json-cas` CLI,不在 `uwf` 中重复。
### 1.2 `uwf thread start`
```bash
uwf thread start <workflow> -p "Fix the login bug described in issue #42"
```
- `<workflow>` — workflow 名或 CAS hash
- `-p` — 用户 prompt(必填)
**输出(JSON to stdout):**
```jsonc
{
"workflow": "4KNM2PXR3B1QW", // workflow CAS hash (XXH64, 13-char Crockford Base32)
"thread": "01J7K9M2XNPQR5VWBCDF8G3H4T" // ULID
}
```
**做的事:**
1. 解析 workflow(名字查 registry → CAS hash)
2. 生成 thread ULID
3. 写 StartNode 到 CAS
4. 在 threads.yaml 中记录链头 → StartNode hash
5. 输出 JSON
### 1.3 `uwf thread step`
```bash
uwf thread step 01J7K9M2XNPQR5VWBCDF8G3H4T
uwf thread step 01J7K9M2XNPQR5VWBCDF8G3H4T --agent "bunx uwf-cursor"
```
**输出(JSON to stdout):**
```jsonc
{
"workflow": "4KNM2PXR3B1QW",
"thread": "01J7K9M2XNPQR5VWBCDF8G3H4T",
"head": "8FWKR3TN5V1QA", // 新链头 StepNode 的 CAS hash
"done": false // true = moderator 返回 END,thread 已归档
}
```
`done: true` 时 head 仍然有值(最后一个 StepNode),但 thread 已从 threads.yaml 移除。
对已结束或不存在的 thread 调用 step 会报错(非 active thread)。
详细信息通过 `uwf thread show <thread-id>``json-cas get <head>` 查看。
**做的事:**
1. 读链头 → 当前 StepNode(或 StartNode)
2. 收集 thread 历史(遍历链)
3. 调 moderator:评估 JSONata conditions → 得到下一个 role(或 END)
4. 若 END → 归档 thread,输出最后链头,退出
5. 确定 agent command(`--agent` override > config.yaml per-workflow/role > config.yaml defaultAgent)
6. 调用:`<agent-cmd> <thread-id> <role>`,捕获 stdout 得到新 StepNode hash
7. 更新链头指针
8. 再次调 moderator(基于新 StepNode)判断 done
9. 输出 JSON
### 1.4 `uwf thread show`
```bash
uwf thread show 01J7K9M2XNPQR5VWBCDF8G3H4T
```
**输出(JSON to stdout):**
```jsonc
{
"workflow": "4KNM2PXR3B1QW",
"thread": "01J7K9M2XNPQR5VWBCDF8G3H4T",
"head": "8FWKR3TN5V1QA",
"done": false
}
```
纯 thread-id → head 查询。详细内容用 `json-cas get <head>``json-cas walk <head>` 查看。
### 1.5 Agent CLI 协议
每个 agent 是一个命令,接受 thread-id 和 role 两个参数:
```bash
uwf-hermes <thread-id> <role>
```
**约定:**
- `uwf step` 负责 moderator 决策,将 role 传给 agent CLI
- 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(纯文本,一行)
- 所有配置从环境变量读(LLM model、API key、extractor config)
- exit 0 = 成功,非 0 = 失败
**stdout 输出:**
```
8FWKR3TN5V1QA
```
`uwf step` 拿到这个 hash 后更新链头指针、判断 done。
---
## 2. CAS 结构定义
### 2.1 类型层级
沿用 json-cas 的三层:bootstrap meta-schema → JSON Schema nodes → data nodes。
下面所有 CAS 节点都遵循 `{ type: cas_ref, payload: T, timestamp: number }` 的标准格式。
`cas_ref` 类型的字符串字段在 json-cas 中已内置支持,不需要额外的 `$ref` 包装。
### 2.2 数据节点
#### `Workflow`
Roles 和 moderator 内联在 Workflow 中,只有 meta 独立为 CAS 节点(方便 json-cas 校验)。
```yaml
type: <workflow-schema-hash>
payload:
name: "solve-issue"
description: "End-to-end issue resolution"
roles:
planner:
description: "Creates implementation plan"
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"
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"
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"
expression: "$exists(steps[-1].output.needsClarification)"
notApproved:
description: "Reviewer rejected the implementation"
expression: "steps[-1].output.approved = false"
graph:
$START:
- role: "planner"
condition: null # 无条件(fallback)
planner:
- role: "developer"
condition: "needsClarification"
- role: "$END"
condition: null
developer:
- role: "reviewer"
condition: null
reviewer:
- role: "developer"
condition: "notApproved"
- role: "$END"
condition: null
```
- `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
- 按数组顺序求值,第一个匹配的 transition 胜出
- 不含 agent binding — agent 配置在 `~/.uncaged/workflow/config.yaml` 中管理
JSONata 表达式的求值上下文:
```jsonc
{
"start": { // StartNode 信息
"workflow": "4KNM2PXR3B1QW",
"prompt": "Fix the login bug..."
},
"steps": [ // 所有已完成 steps,从旧到新
{ "role": "planner", "output": { "phases": [...] }, "detail": "7BQST3VW9F2MA", "agent": "uwf-hermes" },
{ "role": "developer", "output": { "filesChanged": ["src/auth.ts"], "summary": "Fixed redirect" }, "detail": "9KRVW3TN5F1QA", "agent": "uwf-cursor" },
{ "role": "reviewer", "output": { "approved": false }, "detail": "2MXBG6PN4A8JR", "agent": "uwf-hermes" }
]
}
```
注:`output` 在上下文中会被自动展开为实际的 CAS 节点内容(而非 hash),方便 JSONata 表达式直接访问字段。
#### `StartNode`(Thread 起点)
```yaml
type: <start-node-schema-hash>
payload:
workflow: "4KNM2PXR3B1QW" # cas_ref → Workflow
prompt: "Fix the login bug..."
```
- 没有 thread-id — thread-id 是索引层面的事,不进 CAS 内容
- 没有 agent binding — 运行时从 config.yaml 解析
#### `StepNode`(Thread 每一步)
```yaml
type: <step-node-schema-hash>
payload:
start: "4TNVW8KR2B3MA" # cas_ref → StartNode(每个 step 都引用)
prev: "2MXBG6PN4A8JR" # cas_ref → 前一个 StepNode,第一步为 null
role: "developer"
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 meta schema 的 CAS 节点,可用 json-cas 校验
- `detail` — cas_ref,指向执行详情。可以是原始 agent 输出(content node),也可以是子 workflow thread 的 terminal StepNode(workflowAsAgent 场景)
- `agent` — 纯字符串,不是 CAS 节点
### 2.3 链式结构
```
threads.yaml: { "01J7K9M2XNPQR5VWBCDF8G3H4T": "8FWKR3TN5V1QA" }
StepNode (step 3)
├── start ──→ StartNode
│ ├── workflow → CAS(Workflow)
│ └── prompt: "Fix..."
├── prev ──→ StepNode (step 2)
│ ├── start ──→ (same StartNode)
│ ├── prev ──→ StepNode (step 1)
│ │ ├── start ──→ (same StartNode)
│ │ ├── prev: null
│ │ ├── role: "planner"
│ │ └── ...
│ ├── role: "developer"
│ └── ...
├── role: "reviewer"
├── output → CAS({ approved: true })
├── detail → CAS(raw output | sub-workflow terminal node)
└── agent: "uwf-hermes"
```
### 2.4 可变状态
系统两个顶层 YAML 文件和一个 env 文件:
```yaml
# ~/.uncaged/workflow/config.yaml — 全局配置
providers:
openai:
baseUrl: "https://api.openai.com/v1"
apiKeyEnv: "OPENAI_API_KEY"
anthropic:
baseUrl: "https://api.anthropic.com/v1"
apiKeyEnv: "ANTHROPIC_API_KEY"
openrouter:
baseUrl: "https://openrouter.ai/api/v1"
apiKeyEnv: "OPENROUTER_API_KEY"
models:
sonnet:
provider: "openrouter"
name: "anthropic/claude-sonnet-4"
gpt4o-mini:
provider: "openai"
name: "gpt-4o-mini"
agents:
hermes:
command: "uwf-hermes"
args: []
cursor:
command: "uwf-cursor"
args: []
defaultAgent: "hermes"
agentOverrides:
solve-issue:
developer: "cursor"
defaultModel: "sonnet"
modelOverrides:
extract: "gpt4o-mini"
```
```yaml
# ~/.uncaged/workflow/threads.yaml — active thread 链头指针
01J7K9M2XNPQR5VWBCDF8G3H4T: "8FWKR3TN5V1QA"
01J8AB3QRMSTV6WKXZ2C4DF7GN: "3CNWT9KR6D2HV"
```
Thread 结束时从 threads.yaml 移除。可选:追加到 `history.jsonl` 做归档。
```bash
# ~/.uncaged/workflow/.env — 敏感信息(API keys)
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
OPENROUTER_API_KEY=sk-or-...
```
- `config.yaml` — 非敏感配置(agent 命令、model 名、provider 名)
- `.env` — 敏感信息(API keys),agent-kit 启动时自动加载
- `threads.yaml` — 运行时状态
---
## 3. 包结构
全新包,不复用现有 packages,避免命名冲突。CAS 直接依赖 `@uncaged/json-cas`
```
packages/
├── 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 — 共享类型定义
```
**外部依赖:**
- `@uncaged/json-cas` — CAS 存储、hash、schema 校验
- `@uncaged/json-cas-fs` — 文件系统 CAS 后端
**现有包全部保留不动**,新旧并存,逐步迁移。
---
## 4. 关键数据类型
JSONata 求值上下文本质上是 thread 链表的线性化表达。StepNode payload 和上下文中的 step 共享大量字段,提取为公共类型。
### 4.1 公共类型
```typescript
/** CAS hash — XXH64, 13-char Crockford Base32 */
type CasRef = string;
/** Thread ID — ULID, 26-char Crockford Base32 */
type ThreadId = string;
/** 一个 step 的核心数据,被 StepNode payload 和 JSONata 上下文共享 */
type StepRecord = {
role: string;
output: CasRef; // cas_ref → 结构化输出节点(符合 role meta schema)
detail: CasRef; // cas_ref → 执行详情(content node / 子 workflow terminal StepNode)
agent: string; // 实际使用的 agent 命令(纯字符串)
};
```
### 4.2 Workflow 定义
```typescript
type RoleDefinition = {
description: string;
goal: string;
capabilities: string[];
procedure: string;
output: string;
meta: CasRef; // cas_ref → json-cas 内置 JSON Schema 节点
};
type Transition = {
role: string; // 目标 role 名 或 "$END"
condition: string | null; // 引用 conditions 中的 key,null = fallback
};
type ConditionDefinition = {
description: string;
expression: string; // JSONata expression
};
type WorkflowPayload = {
name: string;
description: string;
roles: Record<string, RoleDefinition>;
conditions: Record<string, ConditionDefinition>;
graph: Record<string, Transition[]>; // Record<Role | "$START", Transition[]>
};
```
### 4.3 Thread 节点
```typescript
type StartNodePayload = {
workflow: CasRef; // cas_ref → Workflow
prompt: string;
};
type StepNodePayload = StepRecord & {
start: CasRef; // cas_ref → StartNode(每个 step 都引用)
prev: CasRef | null; // cas_ref → 前一个 StepNode,第一步为 null
};
```
### 4.4 JSONata 求值上下文
Thread 链表的线性化。`steps[n]` 的字段和 `StepRecord` 一致,但 `output` 被展开为实际内容。
```typescript
/** JSONata 上下文中的 step — output 被展开 */
type StepContext = Omit<StepRecord, "output"> & {
output: unknown; // 展开后的 CAS 节点内容,非 hash
};
type ModeratorContext = {
start: StartNodePayload;
steps: StepContext[]; // 从旧到新
};
```
### 4.5 CLI 输出
```typescript
/** uwf thread start */
type StartOutput = {
workflow: CasRef;
thread: ThreadId;
};
/** uwf thread step / uwf thread show */
type StepOutput = {
workflow: CasRef;
thread: ThreadId;
head: CasRef;
done: boolean;
};
/** uwf thread list */
type ThreadListItem = {
thread: ThreadId;
workflow: CasRef;
head: CasRef;
};
```
### 4.6 配置
```typescript
/** Alias types for config references */
type AgentAlias = string;
type ModelAlias = string;
type ProviderAlias = string;
type WorkflowName = string;
type RoleName = string;
type Scenario = string; // e.g. "extract"
type ProviderConfig = {
baseUrl: string;
apiKeyEnv: string; // env var name to read API key from
};
type ModelConfig = {
provider: ProviderAlias;
name: string; // e.g. "anthropic/claude-sonnet-4", "gpt-4o-mini"
};
type AgentConfig = {
command: string;
args: string[];
};
/** ~/.uncaged/workflow/config.yaml */
type WorkflowConfig = {
providers: Record<ProviderAlias, ProviderConfig>;
models: Record<ModelAlias, ModelConfig>;
agents: Record<AgentAlias, AgentConfig>;
defaultAgent: AgentAlias;
agentOverrides: Record<WorkflowName, Record<RoleName, AgentAlias>> | null;
defaultModel: ModelAlias;
modelOverrides: Record<Scenario, ModelAlias> | null;
};
/** ~/.uncaged/workflow/threads.yaml */
type ThreadsIndex = Record<ThreadId, CasRef>;
// ^ thread-id ^ head StepNode/StartNode hash
```
### 4.7 类型关系图
```
WorkflowConfig (config.yaml)
ThreadsIndex (threads.yaml) ← 唯二可变状态
│ thread-id → head hash
StepNodePayload ──extends──→ StepRecord ←──maps to──→ StepContext
│ │ │
├── start → StartNodePayload│ │ (output 展开)
├── prev → StepNodePayload │ │
│ ├── role ├── role
│ ├── output (CasRef) ├── output (展开)
│ ├── detail (CasRef) ├── detail (CasRef)
│ └── agent (string) └── agent (string)
└── start.workflow → WorkflowPayload
├── roles: Record<name, RoleDefinition>
├── conditions: Record<name, JSONata>
└── graph: Record<role, 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
+75
View File
@@ -0,0 +1,75 @@
name: "solve-issue"
description: "End-to-end issue resolution"
roles:
planner:
description: "Creates implementation plan"
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:
type: string
steps:
type: array
items:
type: string
required: [plan, steps]
developer:
description: "Implements code changes"
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:
type: array
items:
type: string
summary:
type: string
required: [filesChanged, summary]
reviewer:
description: "Reviews code changes"
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:
type: boolean
comments:
type: string
required: [approved, comments]
conditions:
notApproved:
description: "Reviewer rejected the implementation"
expression: "$last('reviewer').approved = false"
graph:
$START:
- role: "planner"
condition: null
planner:
- role: "developer"
condition: null
developer:
- role: "reviewer"
condition: null
reviewer:
- role: "developer"
condition: "notApproved"
- role: "$END"
condition: null
@@ -1,5 +1,71 @@
# @uncaged/cli-workflow
## 0.5.0-alpha.4
### Patch Changes
- Updated dependencies
- Updated dependencies [f74b482]
- Updated dependencies [f74b482]
- @uncaged/workflow-util@0.5.0-alpha.4
- @uncaged/workflow-protocol@0.5.0-alpha.4
- @uncaged/workflow-cas@0.5.0-alpha.4
- @uncaged/workflow-execute@0.5.0-alpha.4
- @uncaged/workflow-gateway@0.5.0-alpha.4
- @uncaged/workflow-register@0.5.0-alpha.4
- @uncaged/workflow-runtime@0.5.0-alpha.4
## 0.5.0-alpha.3
### Patch Changes
- Updated dependencies
- @uncaged/workflow-protocol@0.5.0-alpha.3
- @uncaged/workflow-cas@0.5.0-alpha.3
- @uncaged/workflow-execute@0.5.0-alpha.3
- @uncaged/workflow-gateway@0.5.0-alpha.3
- @uncaged/workflow-register@0.5.0-alpha.3
- @uncaged/workflow-runtime@0.5.0-alpha.3
- @uncaged/workflow-util@0.5.0-alpha.3
## 0.5.0-alpha.2
### Patch Changes
- Updated dependencies
- @uncaged/workflow-protocol@0.5.0-alpha.2
- @uncaged/workflow-cas@0.5.0-alpha.2
- @uncaged/workflow-execute@0.5.0-alpha.2
- @uncaged/workflow-gateway@0.5.0-alpha.2
- @uncaged/workflow-register@0.5.0-alpha.2
- @uncaged/workflow-runtime@0.5.0-alpha.2
- @uncaged/workflow-util@0.5.0-alpha.2
## 0.5.0-alpha.1
### Patch Changes
- @uncaged/workflow-cas@0.5.0-alpha.1
- @uncaged/workflow-execute@0.5.0-alpha.1
- @uncaged/workflow-gateway@0.5.0-alpha.1
- @uncaged/workflow-protocol@0.5.0-alpha.1
- @uncaged/workflow-register@0.5.0-alpha.1
- @uncaged/workflow-runtime@0.5.0-alpha.1
- @uncaged/workflow-util@0.5.0-alpha.1
## 0.5.0-alpha.0
### Patch Changes
- Updated dependencies
- @uncaged/workflow-protocol@0.5.0-alpha.0
- @uncaged/workflow-cas@0.5.0-alpha.0
- @uncaged/workflow-execute@0.5.0-alpha.0
- @uncaged/workflow-register@0.5.0-alpha.0
- @uncaged/workflow-runtime@0.5.0-alpha.0
- @uncaged/workflow-util@0.5.0-alpha.0
- @uncaged/workflow-gateway@0.5.0-alpha.0
## 0.4.5
### Patch Changes
@@ -20,9 +20,6 @@ import { addCliArgs } from "./bundle-fixture.js";
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {}, graph: { edges: [] } };
`;
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
`;
function casStoredForm(raw: string): string {
return serializeMerkleNode(createContentMerkleNode(raw));
}
@@ -52,12 +49,12 @@ describe("cli workflow commands", () => {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}import fs from "node:fs";
`${fixtureDescriptor}import fs from "node:fs";
export const run = async function* (input, options) {
fs.existsSync(".");
const cas = options.cas;
const h = await putContentMerkleNode(cas, input.prompt);
const h = await cas.put(input.prompt);
yield { role: "noop", contentHash: h, meta: { done: true }, refs: [h] };
return { returnCode: 0, summary: "done" };
}
@@ -155,10 +152,9 @@ export const run = async function* (input) { return { returnCode: 0, summary: in
},
graph: { edges: [] },
};
${wfPutImport}
export const run = async function* (input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, input.prompt);
const h = await cas.put( input.prompt);
yield { role: "greeter", contentHash: h, meta: { greeting: "hi" }, refs: [h] };
return { returnCode: 0, summary: "ok" };
};
@@ -197,9 +193,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -228,9 +224,9 @@ export const run = async function* (input, options) {
const dtsPath = join(bundleDir, "types.d.ts");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -261,9 +257,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -284,16 +280,16 @@ export const run = async function* (input, options) {
const bundleDir = join(storageRoot, "src");
await mkdir(bundleDir, { recursive: true });
const bundlePath = join(bundleDir, "demo.esm.js");
const v1 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v1 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v1");
const h = await cas.put( "v1");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v1" };
}
`;
const v2 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v2 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v2");
const h = await cas.put( "v2");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v2" };
}
@@ -326,16 +322,16 @@ export const run = async function* (input, options) {
const bundleDir = join(storageRoot, "src");
await mkdir(bundleDir, { recursive: true });
const bundlePath = join(bundleDir, "demo.esm.js");
const v1 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v1 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v1");
const h = await cas.put( "v1");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v1" };
}
`;
const v2 = `${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
const v2 = `${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "v2");
const h = await cas.put( "v2");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "v2" };
}
@@ -378,9 +374,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -391,9 +387,9 @@ export const run = async function* (input, options) {
expect(add1.ok).toBe(true);
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "y");
const h = await cas.put( "y");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "y" };
}
@@ -446,9 +442,9 @@ export const run = async function* (input, options) {
const bundlePath = join(bundleDir, "demo.esm.js");
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "x" };
}
@@ -463,9 +459,9 @@ export const run = async function* (input, options) {
const hash1 = add1.value.hash;
await writeFile(
bundlePath,
`${fixtureDescriptor}${wfPutImport}export const run = async function* (_input, options) {
`${fixtureDescriptor}export const run = async function* (_input, options) {
const cas = options.cas;
const h = await putContentMerkleNode(cas, "y");
const h = await cas.put( "y");
yield { role: "a", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "y" };
}
@@ -15,9 +15,7 @@ import { addCliArgs } from "./bundle-fixture.js";
import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js";
/** Three-role workflow that respects `input.steps` for fork/resume. */
const threeRoleBundleSource = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
export const descriptor = {
const threeRoleBundleSource = `export const descriptor = {
description: "fork-cli",
roles: {
planner: { description: "planner", schema: {} },
@@ -30,16 +28,16 @@ export const run = async function* (input, options) {
const cas = options.cas;
const has = (r) => input.steps.some((s) => s.role === r);
if (!has("planner")) {
const h = await putContentMerkleNode(cas, "p1");
const h = await cas.put( "p1");
yield { role: "planner", contentHash: h, meta: { k: "planner" }, refs: [h] };
}
if (!has("coder")) {
const h = await putContentMerkleNode(cas, "c1");
const h = await cas.put( "c1");
yield { role: "coder", contentHash: h, meta: { k: "coder" }, refs: [h] };
}
if (!has("reviewer")) {
const body = "rev-" + String(input.steps.length);
const h = await putContentMerkleNode(cas, body);
const h = await cas.put( body);
yield { role: "reviewer", contentHash: h, meta: { k: "reviewer" }, refs: [h] };
}
return { returnCode: 0, summary: "done" };
@@ -23,9 +23,6 @@ import { resolveThreadRecord } from "../src/thread-scan.js";
import { addCliArgs } from "./bundle-fixture.js";
import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js";
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
`;
const threadFixtureDescriptor = `export const descriptor = {
description: "thread-cli",
roles: {
@@ -41,25 +38,23 @@ const threadFixtureDescriptor = `export const descriptor = {
`;
const fastBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (input, options) {
const cas = options.cas;
let h = await putContentMerkleNode(cas, "plan");
let h = await cas.put( "plan");
yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] };
h = await putContentMerkleNode(cas, "code");
h = await cas.put( "code");
yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] };
return { returnCode: 0, summary: "done" };
};
`;
const slowPlannerBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (input, options) {
await new Promise((r) => setTimeout(r, 400));
const cas = options.cas;
let h = await putContentMerkleNode(cas, "plan");
let h = await cas.put( "plan");
yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] };
h = await putContentMerkleNode(cas, "code");
h = await cas.put( "code");
yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] };
return { returnCode: 0, summary: "done" };
};
@@ -68,37 +63,34 @@ export const run = async function* (input, options) {
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
const abortablePlannerBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (input, options) {
const cas = options.cas;
let h = await putContentMerkleNode(cas, "plan");
let h = await cas.put( "plan");
yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] };
await new Promise((r) => setTimeout(r, 10000));
h = await putContentMerkleNode(cas, "code");
h = await cas.put( "code");
yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] };
return { returnCode: 0, summary: "done" };
};
`;
const pauseResumeBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (_input, options) {
const cas = options.cas;
let h = await putContentMerkleNode(cas, "f");
let h = await cas.put( "f");
yield { role: "first", contentHash: h, meta: {}, refs: [h] };
await new Promise((r) => setTimeout(r, 1500));
h = await putContentMerkleNode(cas, "s");
h = await cas.put( "s");
yield { role: "second", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "done" };
};
`;
const delayedFirstYieldBundleSource = `${threadFixtureDescriptor}
${wfPutImport}
export const run = async function* (_input, options) {
await new Promise((r) => setTimeout(r, 900));
const cas = options.cas;
const h = await putContentMerkleNode(cas, "x");
const h = await cas.put( "x");
yield { role: "only", contentHash: h, meta: {}, refs: [h] };
return { returnCode: 0, summary: "done" };
};
@@ -180,6 +172,9 @@ describe("cli thread commands", () => {
}
expect(threads.value.some((l) => l.includes(threadId))).toBe(true);
const runningPath = join(storageRoot, "logs", added.value.hash, `${threadId}.running`);
await waitUntilRunningFileAbsent(runningPath, 120);
const shown = await cmdThreadShow(storageRoot, threadId);
expect(shown.ok).toBe(true);
if (!shown.ok) {
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@uncaged/cli-workflow",
"version": "0.5.0-alpha.4",
"files": [
"src",
"dist",
"package.json"
],
"type": "module",
"bin": {
"uncaged-workflow": "src/cli.ts"
},
"dependencies": {
"@uncaged/workflow-gateway": "workspace:^",
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util": "workspace:^",
"@uncaged/workflow-cas": "workspace:^",
"@uncaged/workflow-execute": "workspace:^",
"@uncaged/workflow-register": "workspace:^",
"@uncaged/workflow-runtime": "workspace:^",
"hono": "^4.12.18",
"yaml": "^2.8.4"
},
"scripts": {
"test": "bun test"
},
"publishConfig": {
"access": "public"
}
}
@@ -3,8 +3,8 @@ import { printCliError, printCliLine } from "./cli-output.js";
import { getCommandRegistry } from "./cli-registry.js";
import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js";
import { createCasDispatcher } from "./commands/cas/index.js";
import { createInitDispatcher } from "./commands/init/index.js";
import { dispatchConnect } from "./commands/connect/index.js";
import { createInitDispatcher } from "./commands/init/index.js";
import { dispatchSetup } from "./commands/setup/index.js";
import { createThreadDispatcher, dispatchLive, dispatchRun } from "./commands/thread/index.js";
import { createWorkflowDispatcher } from "./commands/workflow/index.js";
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bun
import { runCli } from "./cli-dispatch.js";
import { resolveWorkflowStorageRoot } from "./storage-env.js";
const argv = process.argv.slice(2);
const storageRoot = resolveWorkflowStorageRoot();
const code = await runCli(storageRoot, argv);
process.exit(code);
@@ -23,7 +23,7 @@ function requireNextArg(argv: string[], i: number, flag: string): Result<string,
function parseConnectArgv(argv: string[]): Result<ConnectOptions, string> {
let name = osHostname().split(".")[0].toLowerCase();
let gatewayUrl = DEFAULT_GATEWAY_URL;
const gatewaySecret = process.env.WORKFLOW_GATEWAY_SECRET ?? "";
const gatewaySecret = process.env.WORKFLOW_DASHBOARD_SECRET ?? "";
const stringFlags: Record<string, (v: string) => void> = {
"--name": (v) => {
name = v;
@@ -56,7 +56,7 @@ export async function dispatchConnect(storageRoot: string, argv: string[]): Prom
const options = parsed.value;
if (options.gatewaySecret === "") {
printCliLine("error: WORKFLOW_GATEWAY_SECRET is required");
printCliLine("error: WORKFLOW_DASHBOARD_SECRET is required");
return 1;
}
@@ -48,11 +48,13 @@ async function handleGatewayMessage(
const headers = new Headers(req.headers);
let resp: Response;
try {
resp = await params.appFetch(new Request(localUrl, {
method: req.method,
headers,
body: req.body === null ? undefined : req.body,
}));
resp = await params.appFetch(
new Request(localUrl, {
method: req.method,
headers,
body: req.body === null ? undefined : req.body,
}),
);
} catch (e) {
params.log("R4N7BQ3C", `app.fetch failed: ${String(e)}`);
const errBody: WsResponse = {
@@ -51,7 +51,6 @@ export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
description: "Says hello — replace with your first role.",
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
schema: greeterMetaSchema,
extractRefs: null,
};
`;
}
@@ -196,18 +196,13 @@ uncaged-workflow init workspace ${workspaceName}
function bundleTs(): string {
return [
'import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";',
'import { mkdir, readdir, writeFile } from "node:fs/promises";',
'import { join } from "node:path";',
"",
'const rootDir = join(import.meta.dir, "..");',
'const workflowsDir = join(rootDir, "workflows");',
'const distDir = join(rootDir, "dist");',
"",
"type JsonDeps = {",
" dependencies: Record<string, string> | null;",
" devDependencies: Record<string, string> | null;",
"};",
"",
"function isEntryFile(name: string): boolean {",
' return name.endsWith("-entry.ts");',
"}",
@@ -216,36 +211,6 @@ function bundleTs(): string {
' return name.slice(0, -".ts".length);',
"}",
"",
"async function uncagedWorkflowExternals(): Promise<string[]> {",
" const names = new Set<string>();",
' const paths = [join(rootDir, "package.json"), join(workflowsDir, "package.json")];',
" for (const pkgPath of paths) {",
" let raw: string;",
" try {",
' raw = await readFile(pkgPath, "utf8");',
" } catch {",
" continue;",
" }",
" const parsed = JSON.parse(raw) as JsonDeps;",
" const blocks = [parsed.dependencies, parsed.devDependencies];",
" for (const block of blocks) {",
" if (block == null) {",
" continue;",
" }",
" for (const key of Object.keys(block)) {",
' if (key.startsWith("@uncaged/workflow")) {',
" names.add(key);",
" }",
" }",
" }",
" }",
" if (names.size === 0) {",
' names.add("@uncaged/workflow-runtime");',
' names.add("@uncaged/workflow-protocol");',
" }",
" return [...names];",
"}",
"",
"async function main(): Promise<void> {",
" await mkdir(distDir, { recursive: true });",
" let files: string[];",
@@ -261,7 +226,6 @@ function bundleTs(): string {
' console.warn("bundle: no *-entry.ts files under workflows/");',
" return;",
" }",
" const external = await uncagedWorkflowExternals();",
" for (const file of entries) {",
" const stem = entryStem(file);",
" const entryPath = join(workflowsDir, file);",
@@ -272,7 +236,6 @@ function bundleTs(): string {
' target: "node",',
" splitting: false,",
' naming: { entry: "[name].esm.js" },',
" external,",
" });",
" if (!result.success) {",
" for (const log of result.logs) {",
@@ -18,13 +18,13 @@ export async function cmdThreadRemove(
return err(`thread not found: ${threadId}`);
}
if (resolved.source === "active") {
await removeThreadEntry(resolved.bundleDir, threadId);
} else {
const hist = await removeThreadHistoryEntries(resolved.bundleDir, threadId);
if (!hist.ok) {
return hist;
}
// Always clear both stores: between resolve and delete the worker may finish and
// move the thread from threads.json into history; branching only on resolved.source
// would skip history removal and leave a dangling row.
await removeThreadEntry(resolved.bundleDir, threadId);
const hist = await removeThreadHistoryEntries(resolved.bundleDir, threadId);
if (!hist.ok) {
return hist;
}
const infoPath = join(storageRoot, "logs", resolved.bundleHash, `${threadId}.info.jsonl`);
@@ -110,7 +110,7 @@ export async function cmdAdd(
return validated;
}
const extracted = await extractBundleExports(resolvedPath, { storageRoot });
const extracted = await extractBundleExports(resolvedPath);
if (!extracted.ok) {
return extracted;
}

Some files were not shown because too many files have changed in this diff Show More