Compare commits

...

12 Commits

Author SHA1 Message Date
xiaomo 7a0c928a4a docs: update all docs to reference @ocas/core and ocas_ref
CI / check (pull_request) Failing after 1m2s
- README.md, docs/architecture.md, docs/wf-stateless-design.md
- docs/builtin-agent-research.md
- All package README.md files
- cas_ref → ocas_ref, @uncaged/json-cas → @ocas/core, json-cas-fs → @ocas/fs
2026-06-02 02:55:42 +00:00
xiaomo d8181e9fdf fix: config test reads source file from correct path
CI / check (pull_request) Failing after 58s
Test was reading from dist/commands/config.ts which doesn't exist
(only .js files in dist). Navigate to src/ instead.
2026-06-02 02:53:35 +00:00
xiaomo ef0174a6f1 chore: migrate @uncaged/json-cas to @ocas/core, @uncaged/json-cas-fs to @ocas/fs
CI / check (pull_request) Failing after 1m10s
- Replace all package.json dependencies
- Update all imports across 7 packages + scripts
- cas_ref → ocas_ref in schema definitions
- listByType() adapted for ListEntry[] return type
- Update CLAUDE.md references

Fixes #585
2026-06-02 02:51:21 +00:00
xiaomo 2a72dcde20 Merge pull request 'feat: !include YAML tag and folder-based workflow layout' (#584) from feat/include-and-folder-workflow into main
CI / check (push) Successful in 1m32s
2026-05-31 04:54:16 +00:00
xiaomo b1759096a2 fix: biome 2.4.16 migration, reduce scanWorkflowDir complexity, fix formatting
CI / check (pull_request) Successful in 1m23s
2026-05-31 04:52:08 +00:00
xiaomo f8c06ada64 style: fix biome lint (template literal, import sorting)
CI / check (pull_request) Failing after 2m7s
2026-05-31 04:48:16 +00:00
xiaomo 806edb2750 style: fix biome lint (import sorting, formatting)
CI / check (pull_request) Failing after 2m4s
2026-05-31 04:44:09 +00:00
xiaomo da1678ffef fix: address review feedback on !include and folder workflow
CI / check (pull_request) Failing after 1m37s
- Fix nested !include: pass customTags recursively, scoped to included file's dir
- Add path traversal guard: !include paths must resolve within base directory
- Fix discoverProjectWorkflows: scan both .workflow/ and .workflows/ (consistent with findWorkflowInDir)
- Add tests: path traversal blocking, nested !include, absolute path rejection
2026-05-31 04:26:54 +00:00
xiaomo 88c251fc14 feat: !include YAML tag and folder-based workflow layout
CI / check (pull_request) Failing after 1m58s
- Add !include custom YAML tag for referencing external files (Fixes #582)
  - .md/.txt files included as strings
  - .json files parsed as JSON objects
  - .yaml/.yml files parsed as YAML objects
  - Paths resolved relative to the workflow YAML file

- Support foo/index.yaml as alternative to foo.yaml (Fixes #583)
  - Updated discoverProjectWorkflows(), findWorkflowInDir()
  - Updated workflowNameFromPath() for index.yaml detection
  - Flat files take priority over folder layout

- Added tests for both features
2026-05-31 04:12:11 +00:00
xiaoju 9fb817a99c Merge pull request 'improve: solve-issue — replace tea pr create with Gitea API' (#581) from retrospect/fix-committer-tea into main
CI / check (push) Successful in 1m13s
2026-05-30 23:37:31 +00:00
xiaonuo 389924c3ab Merge pull request 'improve: solve-issue — fix hallucination patterns (thread 06F7FSTXQGY3D5CY5YPQFK2Y3W)' (#579) from retrospect/solve-issue-fixes into main
CI / check (push) Successful in 1m55s
2026-05-30 08:57:58 +00:00
xiaoju 0dfa20f1d7 improve: solve-issue — add mandatory verification and escalation steps
CI / check (pull_request) Successful in 1m28s
Fixes hallucination issues observed in thread 06F7FSTXQGY3D5CY5YPQFK2Y3W:

1. Developer self-verification (critical): Added step 12 requiring
   mandatory verification of branch, file existence, and git status
   before reporting done status. Prevents hallucinated completions
   without actual tool execution.

2. Reviewer hard-check enforcement (critical): Added critical warning
   and step 0 requiring cd/pwd verification before review. Prevents
   false rejections based on assumptions without actual path checks.

3. Test debugging escalation (medium): Added structured debugging
   guidance with escalation path after 3 test cycles. Prevents
   infinite retry loops by providing strategy and fail-fast guidance.

Also added 3 test cases to verify the new procedure steps exist.

Based on change plan 9EVZPDTS16PMG analyzing execution anomalies
that resulted in 58% waste (13 of 23 minutes).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-30 08:40:34 +00:00
64 changed files with 406 additions and 164 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ This monorepo implements a stateless workflow engine driven by a single-step CLI
| **Role** | A named actor within a workflow. Each role has a system prompt and a JSON Schema `outputSchema`. |
| **Moderator** | Status-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. |
| **CAS** | Content-Addressed Storage via `@ocas/core` — 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
@@ -35,7 +35,7 @@ workflow/
- Dependency layers: `workflow-protocol``workflow-util``workflow-util-agent``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)
- External CAS: `@ocas/core` (store API, hashing, schema validation) + `@ocas/fs` (filesystem backend)
## Language & Paradigm
+1 -1
View File
@@ -67,7 +67,7 @@ App (uses protocol; not in the runtime engine stack)
workflow-dashboard Web UI for visual workflow editing
```
External CAS: [`@uncaged/json-cas`](https://www.npmjs.com/package/@uncaged/json-cas) (store API, hashing, schema validation) + `@uncaged/json-cas-fs` (filesystem backend).
External CAS: [`@ocas/core`](https://www.npmjs.com/package/@ocas/core) (store API, hashing, schema validation) + `@ocas/fs` (filesystem backend).
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.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
"files": {
"includes": [
"**",
+15 -15
View File
@@ -8,13 +8,13 @@
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 **5** 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.
The implementation lives in **5** active packages under `packages/`, plus two external CAS packages (`@ocas/core`, `@ocas/fs`). Legacy packages reside in `legacy-packages/` and are not part of the active stack.
## Package map
| Layer | Package | One-line role |
|-------|---------|---------------|
| Contract | `@uncaged/workflow-protocol``workflow-protocol` | Shared TypeScript types (`WorkflowPayload`, `StepNodePayload`, `ModeratorContext`, `WorkflowConfig`, etc.). No runtime deps beyond `@uncaged/json-cas-fs`. |
| Contract | `@uncaged/workflow-protocol``workflow-protocol` | Shared TypeScript types (`WorkflowPayload`, `StepNodePayload`, `ModeratorContext`, `WorkflowConfig`, etc.). No runtime deps beyond `@ocas/fs`. |
| Shared infra | `@uncaged/workflow-util``workflow-util` | Crockford Base32, ULID generation, `createLogger`, frontmatter parsing/validation. |
| Agent framework | `@uncaged/workflow-util-agent``workflow-util-agent` | `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. |
@@ -24,8 +24,8 @@ The implementation lives in **5** active packages under `packages/`, plus two ex
| 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`. |
| `@ocas/core` | Content-addressed store API, XXH64 hashing, JSON Schema registration and validation. |
| `@ocas/fs` | Filesystem backend for `ocas`. |
| `mustache` | Template renderer for edge prompts (used by `cli-workflow` moderator). |
| `commander` | CLI argument parsing (used by `cli-workflow`). |
| `dotenv` | Loads `.env` files for API keys. |
@@ -36,8 +36,8 @@ The implementation lives in **5** active packages under `packages/`, plus two ex
```mermaid
flowchart BT
subgraph External
jcas["@uncaged/json-cas"]
jcasfs["@uncaged/json-cas-fs"]
jcas["@ocas/core"]
jcasfs["@ocas/fs"]
end
subgraph L0["Layer 0 — contract"]
protocol["@uncaged/workflow-protocol"]
@@ -146,7 +146,7 @@ Key properties:
- **`roles`** — inline role definitions; each `meta` is a JSON Schema (stored as its own CAS node on registration)
- **`graph`** — `Record<Role | "$START", Record<Status, Target>>` — status-based routing; each role maps statuses to targets
- **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`
- **No Zod** — all schemas are JSON Schema, validated through `@ocas/core`
## Three-phase engine loop
@@ -263,7 +263,7 @@ Structured output extraction uses a two-layer strategy (`workflow-util-agent`):
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
5. Validate with `ocas` schema validation
6. If valid → return `outputHash` (zero LLM cost)
### Layer 2: LLM extract fallback (`extract.ts`)
@@ -302,7 +302,7 @@ payload:
capabilities: [planning, issue-analysis]
procedure: "Analyze the issue and create a plan."
output: "Output the plan summary."
meta: "5GWKR8TN1V3JA" # cas_ref → JSON Schema node
meta: "5GWKR8TN1V3JA" # ocas_ref → JSON Schema node
conditions:
notApproved:
description: "Reviewer rejected"
@@ -318,7 +318,7 @@ payload:
```yaml
type: <start-node-schema-hash>
payload:
workflow: "4KNM2PXR3B1QW" # cas_ref → Workflow
workflow: "4KNM2PXR3B1QW" # ocas_ref → Workflow
prompt: "Fix the login bug..."
```
@@ -327,11 +327,11 @@ payload:
```yaml
type: <step-node-schema-hash>
payload:
start: "4TNVW8KR2B3MA" # cas_ref → StartNode
prev: "2MXBG6PN4A8JR" # cas_ref → previous StepNode (null for first step)
start: "4TNVW8KR2B3MA" # ocas_ref → StartNode
prev: "2MXBG6PN4A8JR" # ocas_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)
output: "9KRVW3TN5F1QA" # ocas_ref → structured output (validated against meta schema)
detail: "7BQST3VW9F2MA" # ocas_ref → execution detail (raw turns, session data)
agent: "uwf-hermes" # agent command used (plain string)
```
@@ -484,7 +484,7 @@ Binary: `uwf`
| **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. |
| **JSON Schema (not Zod)** | Schemas are CAS-native data — storable, hashable, validatable through `ocas`. 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. |
+1 -1
View File
@@ -630,7 +630,7 @@ flowchart TB
Spawn -->|"stdout: step hash"| Step
```
**新包**:`packages/workflow-agent-builtin`,bin `uwf-builtin`,仅依赖 `workflow-util-agent`、`workflow-protocol`、`workflow-util`(可选 `@uncaged/json-cas` 写 detail schema)。
**新包**:`packages/workflow-agent-builtin`,bin `uwf-builtin`,仅依赖 `workflow-util-agent`、`workflow-protocol`、`workflow-util`(可选 `@ocas/core` 写 detail schema)。
**分层**:
+25 -25
View File
@@ -22,7 +22,7 @@ uwf workflow show <workflow-id> # 查看 workflow 定义
uwf workflow list # 列出已注册 workflows
```
两组对称,各 3-4 个子命令。CAS 操作交给 `json-cas` CLI,不在 `uwf` 中重复。
两组对称,各 3-4 个子命令。CAS 操作交给 `ocas` CLI,不在 `uwf` 中重复。
### 1.2 `uwf thread start`
@@ -136,14 +136,14 @@ uwf-hermes <thread-id> <role>
沿用 json-cas 的三层:bootstrap meta-schema → JSON Schema nodes → data nodes。
下面所有 CAS 节点都遵循 `{ type: cas_ref, payload: T, timestamp: number }` 的标准格式。
`cas_ref` 类型的字符串字段在 json-cas 中已内置支持,不需要额外的 `$ref` 包装。
下面所有 CAS 节点都遵循 `{ type: ocas_ref, payload: T, timestamp: number }` 的标准格式。
`ocas_ref` 类型的字符串字段在 ocas 中已内置支持,不需要额外的 `$ref` 包装。
### 2.2 数据节点
#### `Workflow`
Roles 和 moderator 内联在 Workflow 中,只有 meta 独立为 CAS 节点(方便 json-cas 校验)。
Roles 和 moderator 内联在 Workflow 中,只有 meta 独立为 CAS 节点(方便 ocas 校验)。
```yaml
type: <workflow-schema-hash>
@@ -157,21 +157,21 @@ payload:
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 内置)
meta: "5GWKR8TN1V3JA" # ocas_ref → JSON Schema 节点(ocas 内置)
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 节点
meta: "8CNWT4KR6D1HV" # ocas_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 节点
meta: "1VPBG9SM5E7WK" # ocas_ref → JSON Schema 节点
conditions:
needsClarification:
description: "Planner requests clarification from user"
@@ -198,7 +198,7 @@ payload:
condition: null
```
- `roles` — 内联定义,每个 role 的 `meta` 是独立的 cas_ref(指向 json-cas 内置 JSON Schema 节点)
- `roles` — 内联定义,每个 role 的 `meta` 是独立的 ocas_ref(指向 ocas 内置 JSON Schema 节点)
- `graph``Record<Role | "$START", Record<Status, Target>>`,每个 Target = `{ role, prompt }`
- Status 来自上一个 role 输出的 `status` 字段,`$START``_` 作为初始 status
- Prompt 模板使用 Mustache 渲染,变量来自 lastOutput
@@ -220,7 +220,7 @@ evaluate(graph, lastRole, lastOutput) → { role, prompt }
```yaml
type: <start-node-schema-hash>
payload:
workflow: "4KNM2PXR3B1QW" # cas_ref → Workflow
workflow: "4KNM2PXR3B1QW" # ocas_ref → Workflow
prompt: "Fix the login bug..."
```
@@ -232,18 +232,18 @@ payload:
```yaml
type: <step-node-schema-hash>
payload:
start: "4TNVW8KR2B3MA" # cas_ref → StartNode(每个 step 都引用)
prev: "2MXBG6PN4A8JR" # cas_ref → 前一个 StepNode,第一步为 null
start: "4TNVW8KR2B3MA" # ocas_ref → StartNode(每个 step 都引用)
prev: "2MXBG6PN4A8JR" # ocas_ref → 前一个 StepNode,第一步为 null
role: "developer"
output: "9KRVW3TN5F1QA" # cas_ref → 结构化输出节点(符合 role 的 meta schema)
detail: "7BQST3VW9F2MA" # cas_ref → 执行详情(content node / 子 workflow terminal StepNode / ...)
output: "9KRVW3TN5F1QA" # ocas_ref → 结构化输出节点(符合 role 的 meta schema)
detail: "7BQST3VW9F2MA" # ocas_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 场景)
- `prev` — 前一个 StepNode 的 ocas_ref,第一步为 `null`(不指向 StartNode)
- `output`ocas_ref,指向符合 role meta schema 的 CAS 节点,可用 ocas 校验
- `detail`ocas_ref,指向执行详情。可以是原始 agent 输出(content node),也可以是子 workflow thread 的 terminal StepNode(workflowAsAgent 场景)
- `agent` — 纯字符串,不是 CAS 节点
### 2.3 链式结构
@@ -337,7 +337,7 @@ OPENROUTER_API_KEY=sk-or-...
## 3. 包结构
全新包,不复用现有 packages,避免命名冲突。CAS 直接依赖 `@uncaged/json-cas`
全新包,不复用现有 packages,避免命名冲突。CAS 直接依赖 `@ocas/core`
```
packages/
@@ -349,8 +349,8 @@ packages/
```
**外部依赖:**
- `@uncaged/json-cas` — CAS 存储、hash、schema 校验
- `@uncaged/json-cas-fs` — 文件系统 CAS 后端
- `@ocas/core` — CAS 存储、hash、schema 校验
- `@ocas/fs` — 文件系统 CAS 后端
**现有包全部保留不动**,新旧并存,逐步迁移。
@@ -372,8 +372,8 @@ type ThreadId = string;
/** 一个 step 的核心数据,被 StepNode payload 和 moderator 上下文共享 */
type StepRecord = {
role: string;
output: CasRef; // cas_ref → 结构化输出节点(符合 role meta schema)
detail: CasRef; // cas_ref → 执行详情(content node / 子 workflow terminal StepNode)
output: CasRef; // ocas_ref → 结构化输出节点(符合 role meta schema)
detail: CasRef; // ocas_ref → 执行详情(content node / 子 workflow terminal StepNode)
agent: string; // 实际使用的 agent 命令(纯字符串)
};
```
@@ -387,7 +387,7 @@ type RoleDefinition = {
capabilities: string[];
procedure: string;
output: string;
meta: CasRef; // cas_ref → json-cas 内置 JSON Schema 节点
meta: CasRef; // ocas_ref → ocas 内置 JSON Schema 节点
};
type Target = {
@@ -407,13 +407,13 @@ type WorkflowPayload = {
```typescript
type StartNodePayload = {
workflow: CasRef; // cas_ref → Workflow
workflow: CasRef; // ocas_ref → Workflow
prompt: string;
};
type StepNodePayload = StepRecord & {
start: CasRef; // cas_ref → StartNode(每个 step 都引用)
prev: CasRef | null; // cas_ref → 前一个 StepNode,第一步为 null
start: CasRef; // ocas_ref → StartNode(每个 step 都引用)
prev: CasRef | null; // ocas_ref → 前一个 StepNode,第一步为 null
};
```
+2 -2
View File
@@ -20,7 +20,7 @@ workflow → thread → step → turn
This package has no library `src/index.ts` — it is consumed as a CLI binary only.
**Dependencies:** `@uncaged/json-cas`, `@uncaged/json-cas-fs`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`, `commander`, `dotenv`, `mustache`, `yaml`
**Dependencies:** `@ocas/core`, `@ocas/fs`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`, `commander`, `dotenv`, `mustache`, `yaml`
## Installation
@@ -209,7 +209,7 @@ src/
| `~/.uncaged/workflow/.env` | API keys (referenced by `apiKeyEnv` in config) |
| `~/.uncaged/workflow/registry.yaml` | Workflow name → CAS hash |
| `~/.uncaged/workflow/threads.yaml` | Active thread head pointers |
| `~/.uncaged/json-cas/` | Content-addressed node storage (unified CAS store, shared with `json-cas` CLI) |
| `~/.uncaged/json-cas/` | Content-addressed node storage (unified CAS store, shared with `ocas` CLI) |
### Environment Variables
+2 -2
View File
@@ -11,8 +11,8 @@
"uwf": "./dist/cli.js"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
"@uncaged/json-cas-fs": "^0.5.3",
"@ocas/core": "^0.1.1",
"@ocas/fs": "^0.1.1",
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util": "workspace:^",
"@uncaged/workflow-util-agent": "workspace:^",
@@ -2,8 +2,8 @@ import { execFileSync } from "node:child_process";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef, StepNodePayload, ThreadId } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { registerUwfSchemas } from "../schemas.js";
@@ -720,7 +720,7 @@ defaultModel: default
describe("no legacy apiKeyEnv references", () => {
test("config.ts has no references to apiKeyEnv", () => {
const configSource = readFileSync(join(__dirname, "..", "commands", "config.ts"), "utf8");
const configSource = readFileSync(join(__dirname, "..", "..", "src", "commands", "config.ts"), "utf8");
expect(configSource).not.toContain("apiKeyEnv");
});
@@ -1,7 +1,7 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { putSchema } from "@uncaged/json-cas";
import { putSchema } from "@ocas/core";
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
import { describe, expect, test } from "vitest";
import { createMarker, deleteMarker } from "../background/index.js";
@@ -0,0 +1,84 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { parse } from "yaml";
import { createIncludeTag } from "../include.js";
let tmpDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "include-tag-test-"));
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
describe("!include tag", () => {
test("includes .md file as string", async () => {
await writeFile(join(tmpDir, "prompt.md"), "You are an analyst.");
const yaml = "system: !include prompt.md";
const result = parse(yaml, { customTags: [createIncludeTag(tmpDir)] });
expect(result.system).toBe("You are an analyst.");
});
test("includes .json file as parsed object", async () => {
await writeFile(join(tmpDir, "schema.json"), '{"type":"object","properties":{}}');
const yaml = "outputSchema: !include schema.json";
const result = parse(yaml, { customTags: [createIncludeTag(tmpDir)] });
expect(result.outputSchema).toEqual({ type: "object", properties: {} });
});
test("includes .yaml file as parsed object", async () => {
await writeFile(join(tmpDir, "config.yaml"), "key: value\nlist:\n - a\n - b");
const yaml = "config: !include config.yaml";
const result = parse(yaml, { customTags: [createIncludeTag(tmpDir)] });
expect(result.config).toEqual({ key: "value", list: ["a", "b"] });
});
test("resolves relative subdirectory paths", async () => {
const subdir = join(tmpDir, "roles");
await mkdir(subdir, { recursive: true });
await writeFile(join(subdir, "analyst.md"), "Analyze data.");
const yaml = "system: !include roles/analyst.md";
const result = parse(yaml, { customTags: [createIncludeTag(tmpDir)] });
expect(result.system).toBe("Analyze data.");
});
test("throws on missing file", () => {
const yaml = "system: !include nonexistent.md";
expect(() => parse(yaml, { customTags: [createIncludeTag(tmpDir)] })).toThrow();
});
test("includes .txt file as string", async () => {
await writeFile(join(tmpDir, "note.txt"), "Hello world");
const yaml = "note: !include note.txt";
const result = parse(yaml, { customTags: [createIncludeTag(tmpDir)] });
expect(result.note).toBe("Hello world");
});
test("blocks path traversal with ../", async () => {
const yaml = "secret: !include ../../etc/passwd";
expect(() => parse(yaml, { customTags: [createIncludeTag(tmpDir)] })).toThrow(
/path traversal blocked/,
);
});
test("blocks absolute path traversal", async () => {
const yaml = "secret: !include /etc/passwd";
expect(() => parse(yaml, { customTags: [createIncludeTag(tmpDir)] })).toThrow(
/path traversal blocked/,
);
});
test("supports nested !include in yaml files", async () => {
const subdir = join(tmpDir, "parts");
await mkdir(subdir, { recursive: true });
await writeFile(join(subdir, "inner.md"), "nested content");
await writeFile(join(tmpDir, "outer.yaml"), "value: !include parts/inner.md");
const yaml = "config: !include outer.yaml";
const result = parse(yaml, { customTags: [createIncludeTag(tmpDir)] });
expect(result.config).toEqual({ value: "nested content" });
});
});
@@ -1,8 +1,8 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { cmdStepRead } from "../commands/step.js";
@@ -40,7 +40,7 @@ const DETAIL_SCHEMA = {
turnCount: { type: "integer" as const },
turns: {
type: "array" as const,
items: { type: "string" as const, format: "cas_ref" },
items: { type: "string" as const, format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,8 +1,8 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, type Hash, type JSONSchema, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { bootstrap, type Hash, type JSONSchema, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef, StepNodePayload } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { cmdStepShow } from "../commands/step.js";
@@ -45,7 +45,7 @@ const DETAIL_SCHEMA: JSONSchema = {
properties: {
turns: {
type: "array",
items: { type: "string", format: "cas_ref" },
items: { type: "string", format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,8 +1,8 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
import { STEP_NODE_SCHEMA } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
@@ -43,7 +43,7 @@ const DETAIL_SCHEMA = {
turnCount: { type: "integer" as const },
turns: {
type: "array" as const,
items: { type: "string" as const, format: "cas_ref" },
items: { type: "string" as const, format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,8 +1,8 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { cmdThreadRead } from "../commands/thread.js";
@@ -41,7 +41,7 @@ const DETAIL_SCHEMA = {
turnCount: { type: "integer" as const },
turns: {
type: "array" as const,
items: { type: "string" as const, format: "cas_ref" },
items: { type: "string" as const, format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,8 +1,8 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { cmdThreadRead, THREAD_READ_DEFAULT_QUOTA } from "../commands/thread.js";
@@ -42,7 +42,7 @@ const DETAIL_SCHEMA = {
turnCount: { type: "integer" as const },
turns: {
type: "array" as const,
items: { type: "string" as const, format: "cas_ref" },
items: { type: "string" as const, format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,8 +1,8 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { cmdStepList, cmdStepShow } from "../commands/step.js";
@@ -47,7 +47,7 @@ const DETAIL_SCHEMA = {
turnCount: { type: "integer" as const },
turns: {
type: "array" as const,
items: { type: "string" as const, format: "cas_ref" },
items: { type: "string" as const, format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,7 +1,7 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createFsStore } from "@uncaged/json-cas-fs";
import { createFsStore } from "@ocas/fs";
import type { CasRef, WorkflowPayload } from "@uncaged/workflow-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { stringify } from "yaml";
@@ -257,6 +257,49 @@ describe("Strategy 3: Local Discovery", () => {
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
});
test("should find workflow in folder-based layout (name/index.yaml)", async () => {
await makeUwfStore(storageRoot);
const workflowDir = join(projectRoot, ".workflow", "solve-issue");
await mkdir(workflowDir, { recursive: true });
await writeFile(join(workflowDir, "index.yaml"), await createWorkflowYaml("solve-issue"));
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow);
expect(node).not.toBeNull();
if (node !== null) {
expect((node.payload as WorkflowPayload).name).toBe("solve-issue");
}
});
test("should prefer flat file over folder-based layout", async () => {
await makeUwfStore(storageRoot);
const workflowDir = join(projectRoot, ".workflow");
await mkdir(workflowDir, { recursive: true });
await writeFile(
join(workflowDir, "solve-issue.yaml"),
await createWorkflowYaml("solve-issue", "flat"),
);
const folderDir = join(workflowDir, "solve-issue");
await mkdir(folderDir, { recursive: true });
await writeFile(
join(folderDir, "index.yaml"),
await createWorkflowYaml("solve-issue", "folder"),
);
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow);
expect(node).not.toBeNull();
if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (flat)");
}
});
});
// ── Strategy 4: Global Registry Fallback ──────────────────────────────────────
+9 -5
View File
@@ -1,9 +1,9 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { JSONSchema, Store } from "@uncaged/json-cas";
import { bootstrap, getSchema, putSchema, refs, walk } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { JSONSchema, Store } from "@ocas/core";
import { bootstrap, getSchema, putSchema, refs, walk } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import { TEXT_SCHEMA } from "../schemas.js";
@@ -85,13 +85,17 @@ export type SchemaListEntry = {
export async function cmdCasSchemaList(storageRoot: string): Promise<SchemaListEntry[]> {
const store = openStore(storageRoot);
const metaHash = await bootstrap(store);
const aliases = await bootstrap(store);
const metaHash = aliases["@ocas/schema"];
if (metaHash === undefined) {
throw new Error("Meta-schema not found in bootstrap result");
}
const entries: SchemaListEntry[] = [];
// Include meta-schema itself
entries.push({ hash: metaHash, title: "(meta-schema)" });
for (const hash of store.listByType(metaHash)) {
for (const { hash } of store.listByType(metaHash)) {
if (hash === metaHash) continue;
const node = store.get(hash);
if (node !== null) {
+5 -5
View File
@@ -1,5 +1,5 @@
import type { Store as CasStore, JSONSchema } from "@uncaged/json-cas";
import { getSchema } from "@uncaged/json-cas";
import type { Store as CasStore, JSONSchema } from "@ocas/core";
import { getSchema } from "@ocas/core";
import type {
CasRef,
StartNodePayload,
@@ -88,7 +88,7 @@ function expandOutput(uwf: UwfStore, outputRef: CasRef): unknown {
}
/**
* Recursively expand all cas_ref fields in a CAS node's payload,
* Recursively expand all ocas_ref fields in a CAS node's payload,
* replacing hash strings with the referenced node's expanded payload.
*/
function expandDeep(store: CasStore, hash: CasRef, visited?: Set<string>): unknown {
@@ -120,7 +120,7 @@ function expandAnyOfField(
): unknown {
if (!Array.isArray(schema.anyOf)) return value;
for (const sub of schema.anyOf as JSONSchema[]) {
if (sub.format === "cas_ref" && typeof value === "string") {
if (sub.format === "ocas_ref" && typeof value === "string") {
return expandDeep(store, value as CasRef, visited);
}
}
@@ -163,7 +163,7 @@ function expandValue(
value: unknown,
visited: Set<string>,
): unknown {
if (schema.format === "cas_ref") return expandCasRefField(store, value, visited);
if (schema.format === "ocas_ref") return expandCasRefField(store, value, visited);
if (Array.isArray(schema.anyOf)) return expandAnyOfField(store, schema, value, visited);
if (schema.type === "array") return expandArrayField(store, schema, value, visited);
return expandObjectField(store, schema, value, visited);
+1 -1
View File
@@ -1,4 +1,4 @@
import type { BootstrapCapableStore } from "@uncaged/json-cas";
import type { BootstrapCapableStore } from "@ocas/core";
import type {
CasRef,
StartEntry,
+21 -2
View File
@@ -1,7 +1,7 @@
import { execFileSync, spawn } from "node:child_process";
import { access, readFile } from "node:fs/promises";
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
import { validate } from "@uncaged/json-cas";
import { validate } from "@ocas/core";
import type {
AgentAlias,
AgentConfig,
@@ -28,6 +28,7 @@ import { getEnvPath, loadWorkflowConfig } from "@uncaged/workflow-util-agent";
import { config as loadDotenv } from "dotenv";
import { parse } from "yaml";
import { createMarker, deleteMarker, isThreadRunning } from "../background/index.js";
import { createIncludeTag } from "../include.js";
import { evaluate } from "../moderator/index.js";
import {
appendThreadHistory,
@@ -118,6 +119,15 @@ async function findWorkflowInDir(dir: string, name: string): Promise<string | nu
return result;
}
}
for (const indexName of ["index.yaml", "index.yml"]) {
const candidate = resolvePath(dir, ".workflow", name, indexName);
try {
await access(candidate);
return candidate;
} catch {
/* not found */
}
}
// Check .workflows/ directory as fallback (legacy)
for (const ext of [".yaml", ".yml"]) {
@@ -126,6 +136,15 @@ async function findWorkflowInDir(dir: string, name: string): Promise<string | nu
return result;
}
}
for (const indexName of ["index.yaml", "index.yml"]) {
const candidate = resolvePath(dir, ".workflows", name, indexName);
try {
await access(candidate);
return candidate;
} catch {
/* not found */
}
}
return null;
}
@@ -172,7 +191,7 @@ async function materializeLocalWorkflow(uwf: UwfStore, filePath: string): Promis
let raw: unknown;
try {
raw = parse(text) as unknown;
raw = parse(text, { customTags: [createIncludeTag(dirname(filePath))] }) as unknown;
} catch (e) {
fail(`invalid YAML in ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
}
@@ -1,9 +1,11 @@
import { readFile } from "node:fs/promises";
import { dirname, resolve as resolvePath } from "node:path";
import type { JSONSchema } from "@uncaged/json-cas";
import { putSchema, validate } from "@uncaged/json-cas";
import type { JSONSchema } from "@ocas/core";
import { putSchema, validate } from "@ocas/core";
import type { CasRef, RoleDefinition, Target, WorkflowPayload } from "@uncaged/workflow-protocol";
import { parse } from "yaml";
import { createIncludeTag } from "../include.js";
import {
createUwfStore,
@@ -123,7 +125,9 @@ export async function cmdWorkflowAdd(
let raw: unknown;
try {
raw = parse(text) as unknown;
raw = parse(text, {
customTags: [createIncludeTag(dirname(resolvePath(filePath)))],
}) as unknown;
} catch (e) {
fail(`invalid YAML: ${e instanceof Error ? e.message : String(e)}`);
}
+37
View File
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { dirname, extname, resolve } from "node:path";
import { parse as parseYaml } from "yaml";
/**
* Create a YAML customTags entry for !include that resolves file paths
* relative to the given base directory.
*
* Security: resolved paths must stay within baseDir (path traversal prevention).
* Nested !include in .yaml/.yml files is supported (customTags passed recursively).
*/
export function createIncludeTag(baseDir: string) {
const resolvedBase = resolve(baseDir);
return {
tag: "!include",
resolve(str: string) {
const filePath = resolve(resolvedBase, str);
// Path traversal guard: resolved path must be inside baseDir
if (!filePath.startsWith(`${resolvedBase}/`) && filePath !== resolvedBase) {
throw new Error(
`!include path traversal blocked: "${str}" resolves outside base directory`,
);
}
const content = readFileSync(filePath, "utf8");
const ext = extname(filePath).toLowerCase();
if (ext === ".json") {
return JSON.parse(content);
}
if (ext === ".yaml" || ext === ".yml") {
// Pass customTags recursively so nested !include works,
// scoped to the included file's directory
return parseYaml(content, { customTags: [createIncludeTag(dirname(filePath))] });
}
return content;
},
};
}
+2 -2
View File
@@ -1,5 +1,5 @@
import type { Hash, Store } from "@uncaged/json-cas";
import { putSchema } from "@uncaged/json-cas";
import type { Hash, Store } from "@ocas/core";
import { putSchema } from "@ocas/core";
import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@uncaged/workflow-protocol";
export const TEXT_SCHEMA = { type: "string" as const };
+61 -16
View File
@@ -1,9 +1,10 @@
import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import type { Dirent } from "node:fs";
import { access, appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import type { BootstrapCapableStore, Hash } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { BootstrapCapableStore, Hash } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type { CasRef, ThreadId, ThreadListItem, ThreadsIndex } from "@uncaged/workflow-protocol";
import { parse, stringify } from "yaml";
@@ -19,17 +20,38 @@ export type ProjectWorkflowEntry = {
filePath: string;
};
/** Extract workflow name from a YAML filename (strip .yaml/.yml extension). */
function stemFromYaml(name: string): string {
if (name.endsWith(".yaml")) return name.slice(0, -5);
if (name.endsWith(".yml")) return name.slice(0, -4);
return name;
}
/** Check if a directory contains an index.yaml or index.yml workflow file. */
async function findIndexWorkflow(
dir: string,
dirName: string,
): Promise<ProjectWorkflowEntry | null> {
for (const indexName of ["index.yaml", "index.yml"]) {
const indexPath = join(dir, dirName, indexName);
try {
await access(indexPath);
return { name: dirName, filePath: indexPath };
} catch {
// not found, try next
}
}
return null;
}
/**
* Scan `<projectRoot>/.workflows/*.yaml` (non-recursive) and return discovered entries.
* Returns an empty array if the directory does not exist.
* Scan a single directory for workflow entries (flat YAML files + folder/index.yaml).
* Returns discovered entries. Returns empty array if directory does not exist.
*/
export async function discoverProjectWorkflows(
projectRoot: string,
): Promise<ProjectWorkflowEntry[]> {
const dir = join(projectRoot, ".workflows");
let entries: string[];
async function scanWorkflowDir(dir: string): Promise<ProjectWorkflowEntry[]> {
let dirents: Dirent[];
try {
entries = await readdir(dir);
dirents = await readdir(dir, { withFileTypes: true });
} catch (e) {
const err = e as NodeJS.ErrnoException;
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
@@ -39,16 +61,39 @@ export async function discoverProjectWorkflows(
}
const result: ProjectWorkflowEntry[] = [];
for (const entry of entries) {
if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) {
continue;
for (const entry of dirents) {
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
result.push({ name: stemFromYaml(entry.name), filePath: join(dir, entry.name) });
} else if (entry.isDirectory()) {
const found = await findIndexWorkflow(dir, entry.name);
if (found !== null) {
result.push(found);
}
}
const stem = entry.endsWith(".yaml") ? entry.slice(0, -5) : entry.slice(0, -4);
result.push({ name: stem, filePath: join(dir, entry) });
}
return result;
}
/**
* Scan `<projectRoot>/.workflow/` (preferred) and `.workflows/` (legacy) for workflow entries.
* .workflow/ takes priority: if a name is found in both, .workflow/ wins.
* Returns an empty array if neither directory exists.
*/
export async function discoverProjectWorkflows(
projectRoot: string,
): Promise<ProjectWorkflowEntry[]> {
const primary = await scanWorkflowDir(join(projectRoot, ".workflow"));
const legacy = await scanWorkflowDir(join(projectRoot, ".workflows"));
const seen = new Set(primary.map((e) => e.name));
const merged = [...primary];
for (const entry of legacy) {
if (!seen.has(entry.name)) {
merged.push(entry);
}
}
return merged;
}
/** Default filesystem root for uwf data (`~/.uncaged/workflow`). */
export function getDefaultStorageRoot(): string {
return join(homedir(), ".uncaged", "workflow");
+10 -4
View File
@@ -1,4 +1,4 @@
import { basename } from "node:path";
import { basename, dirname } from "node:path";
import type { CasRef, WorkflowPayload } from "@uncaged/workflow-protocol";
const CAS_REF_PATTERN = /^[0-9A-HJKMNP-TV-Z]{13}$/;
@@ -68,9 +68,15 @@ function isGraph(value: unknown): boolean {
*/
export function workflowNameFromPath(filePath: string): string {
const base = basename(filePath);
if (base.endsWith(".yaml")) return base.slice(0, -5);
if (base.endsWith(".yml")) return base.slice(0, -4);
return base;
const stem = base.endsWith(".yaml")
? base.slice(0, -5)
: base.endsWith(".yml")
? base.slice(0, -4)
: base;
if (stem === "index") {
return basename(dirname(filePath));
}
return stem;
}
/**
+1 -1
View File
@@ -8,7 +8,7 @@ Layer 3 agent implementation. Runs an OpenAI-compatible chat completion loop wit
Useful when you want a self-contained agent without an external CLI like Hermes or Claude Code.
**Dependencies:** `@uncaged/json-cas`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-util`
**Dependencies:** `@ocas/core`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-util`
## Installation
+1 -1
View File
@@ -23,7 +23,7 @@
"test:ci": "vitest run"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
"@ocas/core": "^0.1.1",
"@uncaged/workflow-util-agent": "workspace:^",
"@uncaged/workflow-util": "workspace:^"
},
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Store } from "@uncaged/json-cas";
import type { Store } from "@ocas/core";
import { createLogger, generateUlid } from "@uncaged/workflow-util";
import {
type AgentContext,
@@ -1,4 +1,4 @@
import { bootstrap, putSchema, type Store } from "@uncaged/json-cas";
import { bootstrap, putSchema, type Store } from "@ocas/core";
import { BUILTIN_DETAIL_SCHEMA, BUILTIN_TURN_SCHEMA } from "./schemas.js";
import { readSessionTurns } from "./session.js";
@@ -1,4 +1,4 @@
import type { JSONSchema } from "@uncaged/json-cas";
import type { JSONSchema } from "@ocas/core";
const BUILTIN_TOOL_CALL_SCHEMA: JSONSchema = {
type: "object",
@@ -38,7 +38,7 @@ export const BUILTIN_DETAIL_SCHEMA: JSONSchema = {
turnCount: { type: "integer" },
turns: {
type: "array",
items: { type: "string", format: "cas_ref" },
items: { type: "string", format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,4 +1,4 @@
import type { JSONSchema } from "@uncaged/json-cas";
import type { JSONSchema } from "@ocas/core";
export type ToolContext = {
cwd: string;
@@ -6,7 +6,7 @@
Layer 3 agent implementation. Spawns the `claude` CLI with a composed system prompt (role definition, task, prior steps, edge prompt). Parses stream or JSON stdout, caches session IDs for multi-turn continuation, and stores raw output plus structured detail in CAS.
**Dependencies:** `@uncaged/json-cas`, `@uncaged/workflow-util-agent`
**Dependencies:** `@ocas/core`, `@uncaged/workflow-util-agent`
## Installation
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { createMemoryStore, walk } from "@uncaged/json-cas";
import { createMemoryStore, walk } from "@ocas/core";
import {
parseClaudeCodeJsonOutput,
parseClaudeCodeStreamOutput,
@@ -23,7 +23,7 @@
"test:ci": "vitest run"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
"@ocas/core": "^0.1.1",
"@uncaged/workflow-util-agent": "workspace:^",
"@uncaged/workflow-util": "workspace:^"
},
@@ -1,5 +1,5 @@
import { spawn } from "node:child_process";
import type { Store } from "@uncaged/json-cas";
import type { Store } from "@ocas/core";
import { createLogger } from "@uncaged/workflow-util";
import {
type AgentContext,
@@ -1,4 +1,4 @@
import type { JSONSchema } from "@uncaged/json-cas";
import type { JSONSchema } from "@ocas/core";
export const CLAUDE_CODE_DETAIL_SCHEMA: JSONSchema = {
title: "claude-code-detail",
@@ -34,7 +34,7 @@ export const CLAUDE_CODE_DETAIL_SCHEMA: JSONSchema = {
},
turns: {
type: "array",
items: { type: "string", format: "cas_ref" },
items: { type: "string", format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -1,4 +1,4 @@
import { bootstrap, putSchema, type Store } from "@uncaged/json-cas";
import { bootstrap, putSchema, type Store } from "@ocas/core";
import {
CLAUDE_CODE_DETAIL_SCHEMA,
+1 -1
View File
@@ -8,7 +8,7 @@
On first visit to a role it sends a composed prompt (role definition, task, history, edge prompt); on continuation it resumes the cached session. Session transcripts and raw output are stored as CAS detail nodes.
**Dependencies:** `@uncaged/json-cas`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`
**Dependencies:** `@ocas/core`, `@uncaged/workflow-util-agent`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`
## Installation
@@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createMemoryStore, refs, validate, walk } from "@uncaged/json-cas";
import { createMemoryStore, refs, validate, walk } from "@ocas/core";
import {
computeDurationMs,
@@ -82,7 +82,7 @@ describe("computeDurationMs", () => {
});
describe("storeHermesSessionDetail", () => {
test("stores hermes-detail root with cas_ref turns walkable", async () => {
test("stores hermes-detail root with ocas_ref turns walkable", async () => {
const session: HermesSessionJson = {
session_id: "20260518_133159_6a84e8",
model: "claude-opus-4.6",
+1 -1
View File
@@ -23,7 +23,7 @@
"test:ci": "vitest run"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
"@ocas/core": "^0.1.1",
"@uncaged/workflow-util-agent": "workspace:^",
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util": "workspace:^"
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Store } from "@uncaged/json-cas";
import type { Store } from "@ocas/core";
import { createLogger } from "@uncaged/workflow-util";
import {
type AgentContext,
@@ -1,4 +1,4 @@
import type { JSONSchema } from "@uncaged/json-cas";
import type { JSONSchema } from "@ocas/core";
const HERMES_TOOL_CALL_SCHEMA: JSONSchema = {
type: "object",
@@ -39,7 +39,7 @@ export const HERMES_DETAIL_SCHEMA: JSONSchema = {
turnCount: { type: "integer" },
turns: {
type: "array",
items: { type: "string", format: "cas_ref" },
items: { type: "string", format: "ocas_ref" },
},
},
additionalProperties: false,
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema, type Store } from "@uncaged/json-cas";
import { bootstrap, putSchema, type Store } from "@ocas/core";
import { HERMES_DETAIL_SCHEMA, HERMES_RAW_OUTPUT_SCHEMA, HERMES_TURN_SCHEMA } from "./schemas.js";
import type {
+2 -2
View File
@@ -4,9 +4,9 @@ Shared TypeScript types and JSON Schema constants for the workflow engine.
## Overview
This is the contract layer (Layer 0). It defines `WorkflowPayload`, thread node payloads, moderator context, CLI output shapes, and configuration types used across every other package. It has no runtime logic beyond exporting schema objects from `@uncaged/json-cas`.
This is the contract layer (Layer 0). It defines `WorkflowPayload`, thread node payloads, moderator context, CLI output shapes, and configuration types used across every other package. It has no runtime logic beyond exporting schema objects from `@ocas/core`.
**Dependencies:** `@uncaged/json-cas`, `@uncaged/json-cas-fs`
**Dependencies:** `@ocas/core`, `@ocas/fs`
## Installation
+2 -2
View File
@@ -20,8 +20,8 @@
"test:ci": "vitest run"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
"@uncaged/json-cas-fs": "^0.5.3"
"@ocas/core": "^0.1.1",
"@ocas/fs": "^0.1.1"
},
"devDependencies": {
"typescript": "^5.8.3"
+8 -8
View File
@@ -1,4 +1,4 @@
import type { JSONSchema } from "@uncaged/json-cas";
import type { JSONSchema } from "@ocas/core";
const ROLE_DEFINITION: JSONSchema = {
type: "object",
@@ -9,7 +9,7 @@ const ROLE_DEFINITION: JSONSchema = {
capabilities: { type: "array", items: { type: "string" } },
procedure: { type: "string" },
output: { type: "string" },
frontmatter: { type: "string", format: "cas_ref" },
frontmatter: { type: "string", format: "ocas_ref" },
},
additionalProperties: false,
};
@@ -54,7 +54,7 @@ export const START_NODE_SCHEMA: JSONSchema = {
type: "object",
required: ["workflow", "prompt", "cwd"],
properties: {
workflow: { type: "string", format: "cas_ref" },
workflow: { type: "string", format: "ocas_ref" },
prompt: { type: "string" },
cwd: { type: "string" },
},
@@ -76,20 +76,20 @@ export const STEP_NODE_SCHEMA: JSONSchema = {
"cwd",
],
properties: {
start: { type: "string", format: "cas_ref" },
start: { type: "string", format: "ocas_ref" },
prev: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
},
role: { type: "string" },
output: { type: "string", format: "cas_ref" },
detail: { type: "string", format: "cas_ref" },
output: { type: "string", format: "ocas_ref" },
detail: { type: "string", format: "ocas_ref" },
agent: { type: "string" },
edgePrompt: { type: "string" },
startedAtMs: { type: "integer" },
completedAtMs: { type: "integer" },
cwd: { type: "string" },
assembledPrompt: {
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
},
},
additionalProperties: false,
+1 -1
View File
@@ -8,7 +8,7 @@ Layer 2 agent framework. Provides the standard entrypoint for all agent CLIs: pa
Also exports prompt builders, config/storage helpers, and session ID caching for multi-turn agents.
**Dependencies:** `@uncaged/json-cas`, `@uncaged/json-cas-fs`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`, `dotenv`, `yaml`
**Dependencies:** `@ocas/core`, `@ocas/fs`, `@uncaged/workflow-protocol`, `@uncaged/workflow-util`, `dotenv`, `yaml`
## Installation
@@ -1,4 +1,4 @@
import { createMemoryStore, putSchema } from "@uncaged/json-cas";
import { createMemoryStore, putSchema } from "@ocas/core";
import { describe, expect, test } from "vitest";
import { tryFrontmatterFastPath } from "../src/frontmatter.js";
@@ -1,4 +1,4 @@
import { createMemoryStore, putSchema } from "@uncaged/json-cas";
import { createMemoryStore, putSchema } from "@ocas/core";
import { describe, expect, test } from "vitest";
import { tryFrontmatterFastPath } from "../src/frontmatter.js";
@@ -1,4 +1,4 @@
import { createMemoryStore, putSchema } from "@uncaged/json-cas";
import { createMemoryStore, putSchema } from "@ocas/core";
import { describe, expect, test } from "vitest";
import { tryFrontmatterFastPath } from "../src/frontmatter.js";
+2 -2
View File
@@ -20,8 +20,8 @@
"test:ci": "vitest run"
},
"dependencies": {
"@uncaged/json-cas": "^0.5.3",
"@uncaged/json-cas-fs": "^0.5.3",
"@ocas/core": "^0.1.1",
"@ocas/fs": "^0.1.1",
"@uncaged/workflow-protocol": "workspace:^",
"@uncaged/workflow-util": "workspace:^",
"dotenv": "^16.6.1",
@@ -1,4 +1,4 @@
import type { JSONSchema } from "@uncaged/json-cas";
import type { JSONSchema } from "@ocas/core";
type SchemaProperty = {
name: string;
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Store } from "@uncaged/json-cas";
import type { Store } from "@ocas/core";
import type {
CasRef,
StartNodePayload,
+1 -1
View File
@@ -1,4 +1,4 @@
import { getSchema, validate } from "@uncaged/json-cas";
import { getSchema, validate } from "@ocas/core";
import type { CasRef, ModelAlias, WorkflowConfig } from "@uncaged/workflow-protocol";
import { createAgentStore, resolveStorageRoot } from "./storage.js";
@@ -1,5 +1,5 @@
import type { Store } from "@uncaged/json-cas";
import { getSchema, validate } from "@uncaged/json-cas";
import type { Store } from "@ocas/core";
import { getSchema, validate } from "@ocas/core";
import type { CasRef } from "@uncaged/workflow-protocol";
import {
type AgentFrontmatter,
+1 -1
View File
@@ -1,4 +1,4 @@
import { getSchema, validate } from "@uncaged/json-cas";
import { getSchema, validate } from "@ocas/core";
import type { CasRef, StepNodePayload, ThreadId } from "@uncaged/workflow-protocol";
import { config as loadDotenv } from "dotenv";
import { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
+2 -2
View File
@@ -1,5 +1,5 @@
import type { Hash, Store } from "@uncaged/json-cas";
import { putSchema } from "@uncaged/json-cas";
import type { Hash, Store } from "@ocas/core";
import { putSchema } from "@ocas/core";
import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@uncaged/workflow-protocol";
export type UwfAgentSchemaHashes = {
+2 -2
View File
@@ -2,8 +2,8 @@ import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Store } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { Store } from "@ocas/core";
import { createFsStore } from "@ocas/fs";
import type {
AgentAlias,
AgentConfig,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Store } from "@uncaged/json-cas";
import type { Store } from "@ocas/core";
import type { ModeratorContext, ThreadId, WorkflowPayload } from "@uncaged/workflow-protocol";
export type AgentContext = ModeratorContext & {
@@ -20,7 +20,7 @@ Dependency layers (each only imports from packages above it):
protocol util util-agent agent-hermes / agent-builtin / cli-workflow
\`\`\`
External CAS: \`@uncaged/json-cas\` (store API, hashing, schema validation) + \`@uncaged/json-cas-fs\` (filesystem backend).
External CAS: \`@ocas/core\` (store API, hashing, schema validation) + \`@ocas/fs\` (filesystem backend).
## Coding Conventions
@@ -122,7 +122,7 @@ Shared entry point for all agent CLIs. Handles:
### CAS Integration
All data is CAS-addressed via \`@uncaged/json-cas\`:
All data is CAS-addressed via \`@ocas/core\`:
- \`store.put(schemaHash, data)\` → content hash
- \`store.get(hash)\` → node
- \`validate(store, node)\` → schema check
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
// Mock agent for smoke testing
import { bootstrap, type JSONSchema, putSchema } from "@uncaged/json-cas";
import { bootstrap, type JSONSchema, putSchema } from "@ocas/core";
import { createAgent } from "../packages/uwf-agent-kit/src/index.js";
const MOCK_RAW_OUTPUT_SCHEMA: JSONSchema = {