refactor: align package folder names with npm package names
CI / check (pull_request) Failing after 8m30s

Rename packages/ subdirectories to match their @united-workforce/* scope:
  cli-workflow → cli
  workflow-agent-builtin → agent-builtin
  workflow-agent-claude-code → agent-claude-code
  workflow-agent-hermes → agent-hermes
  workflow-dashboard → dashboard
  workflow-protocol → protocol
  workflow-util-agent → util-agent
  workflow-util → util

Updated all tsconfig references, scripts, and active docs.
Historical docs (docs/plans/, docs/superpowers/) left as-is.

Closes #21
This commit is contained in:
2026-06-02 23:45:45 +08:00
parent e4e4288d00
commit 5970456a54
266 changed files with 207 additions and 207 deletions
+186
View File
@@ -0,0 +1,186 @@
# @united-workforce/protocol
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 `@ocas/core`.
**Dependencies:** `@ocas/core`, `@ocas/fs`
## Installation
```bash
bun add @united-workforce/protocol
```
## API
All exports come from `src/index.ts`.
### JSON Schema constants
```typescript
START_NODE_SCHEMA: JSONSchema
STEP_NODE_SCHEMA: JSONSchema
WORKFLOW_SCHEMA: JSONSchema
```
### Core identifiers
```typescript
type CasRef = string // XXH64 hash, 13-char Crockford Base32
type ThreadId = string // ULID, 26-char Crockford Base32
type WorkflowName = string
type RoleName = string
```
### Workflow definition
```typescript
type RoleDefinition = {
description: string;
goal: string;
capabilities: string[];
procedure: string;
output: string;
frontmatter: CasRef;
};
type Target = {
role: string;
prompt: string;
};
type WorkflowPayload = {
name: string;
description: string;
roles: Record<string, RoleDefinition>;
graph: Record<string, Record<string, Target>>;
};
```
### Thread nodes
```typescript
type StepRecord = {
role: string;
output: CasRef;
detail: CasRef;
agent: string;
edgePrompt: string;
};
type StartNodePayload = {
workflow: CasRef;
prompt: string;
};
type StepNodePayload = StepRecord & {
start: CasRef;
prev: CasRef | null;
};
```
### Moderator context
```typescript
type StepContext = Omit<StepRecord, "output"> & { output: unknown; content: string | null };
type ModeratorContext = {
start: StartNodePayload;
steps: StepContext[];
};
```
### Configuration
```typescript
type ProviderAlias = string;
type ModelAlias = string;
type AgentAlias = string;
type ProviderConfig = { baseUrl: string; apiKey: string };
type ModelConfig = {
provider: ProviderAlias;
name: string;
};
type AgentConfig = {
command: string;
args: string[];
};
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;
};
```
### CLI output types
```typescript
type StartOutput = { workflow: CasRef; thread: ThreadId };
type StepOutput = {
workflow: CasRef;
thread: ThreadId;
head: CasRef;
done: boolean;
};
type StepEntry = {
hash: CasRef;
role: string;
output: unknown;
detail: CasRef;
agent: string;
timestamp: number;
};
type StartEntry = {
hash: CasRef;
workflow: CasRef;
prompt: string;
timestamp: number;
};
type ThreadStepsOutput = {
thread: ThreadId;
workflow: CasRef;
steps: [StartEntry, ...StepEntry[]];
};
type ThreadForkOutput = {
thread: ThreadId;
forkedFrom: { step: CasRef };
};
type ThreadListItem = {
thread: ThreadId;
workflow: CasRef;
head: CasRef;
};
type ThreadsIndex = Record<ThreadId, CasRef>;
type Scenario = string;
```
## Internal Structure
```
src/
├── index.ts Public re-exports
├── types.ts All type definitions
└── schemas.ts START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA
```
## Configuration
This package defines `WorkflowConfig` types only. Runtime config loading lives in `@united-workforce/util-agent` (`loadWorkflowConfig`).
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@united-workforce/protocol",
"version": "0.5.0",
"files": [
"src",
"dist",
"package.json"
],
"type": "module",
"exports": {
".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1",
"test": "bun test src/__tests__/",
"test:ci": "bun test src/__tests__/"
},
"dependencies": {
"@ocas/core": "^0.1.1",
"@ocas/fs": "^0.1.1"
},
"devDependencies": {
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/workflow.git",
"directory": "packages/protocol"
},
"homepage": "https://git.shazhou.work/uncaged/workflow#readme",
"bugs": {
"url": "https://git.shazhou.work/uncaged/workflow/issues"
},
"license": "MIT"
}
@@ -0,0 +1,79 @@
import { describe, expect, test } from "bun:test";
import {
createThreadIndexEntry,
markThreadSuspended,
normalizeThreadIndexEntry,
parseThreadsIndex,
serializeThreadIndexEntry,
serializeThreadsIndex,
updateThreadHead,
} from "../thread-index.js";
describe("thread-index", () => {
test("parse legacy string head hash", () => {
const entry = normalizeThreadIndexEntry("0123456789ABC");
expect(entry).toEqual({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
});
});
test("parse suspended object entry", () => {
const entry = normalizeThreadIndexEntry({
head: "0123456789ABC",
suspendedRole: "worker",
suspendMessage: "Please clarify: Which API?",
});
expect(entry).toEqual({
head: "0123456789ABC",
suspendedRole: "worker",
suspendMessage: "Please clarify: Which API?",
});
});
test("serialize non-suspended entry as compact string", () => {
const entry = createThreadIndexEntry("0123456789ABC");
expect(serializeThreadIndexEntry(entry)).toBe("0123456789ABC");
});
test("serialize suspended entry as object", () => {
const entry = markThreadSuspended(
createThreadIndexEntry("0123456789ABC"),
"worker",
"Please clarify: Which API?",
);
expect(serializeThreadIndexEntry(entry)).toEqual({
head: "0123456789ABC",
suspendedRole: "worker",
suspendMessage: "Please clarify: Which API?",
});
});
test("updateThreadHead clears suspend metadata", () => {
const suspended = markThreadSuspended(
createThreadIndexEntry("OLDHEAD0123456"),
"worker",
"Waiting",
);
const resumed = updateThreadHead(suspended, "NEWHEAD01234567");
expect(resumed).toEqual({
head: "NEWHEAD01234567",
suspendedRole: null,
suspendMessage: null,
});
});
test("parseThreadsIndex round-trip", () => {
const raw = {
"01THREAD0000000000000001": "HEAD00000000001",
"01THREAD0000000000000002": {
head: "HEAD00000000002",
suspendedRole: "reviewer",
suspendMessage: "Need input",
},
};
const parsed = parseThreadsIndex(raw);
expect(serializeThreadsIndex(parsed)).toEqual(raw);
});
});
@@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test";
import type { StartNodePayload, StepRecord, Target } from "../types.js";
describe("Protocol types for thread/edge location", () => {
describe("StartNodePayload", () => {
test("has required cwd field", () => {
const payload: StartNodePayload = {
workflow: "0123456789ABC",
prompt: "Test prompt",
cwd: "/home/user/project",
};
expect(payload.cwd).toBe("/home/user/project");
expect(typeof payload.cwd).toBe("string");
});
});
describe("StepRecord", () => {
test("has required cwd field", () => {
const record: StepRecord = {
role: "planner",
output: "0123456789ABC",
detail: "DEF0123456789",
agent: "uwf-hermes",
edgePrompt: "Plan the implementation",
startedAtMs: Date.now(),
completedAtMs: Date.now() + 1000,
assembledPrompt: null,
cwd: "/home/user/project",
};
expect(record.cwd).toBe("/home/user/project");
expect(typeof record.cwd).toBe("string");
});
});
describe("Target", () => {
test("has location field that accepts string", () => {
const target: Target = {
role: "coder",
prompt: "Implement the code",
location: "/custom/path",
};
expect(target.location).toBe("/custom/path");
expect(typeof target.location).toBe("string");
});
test("has location field that accepts null", () => {
const target: Target = {
role: "coder",
prompt: "Implement the code",
location: null,
};
expect(target.location).toBe(null);
});
test("location supports mustache template syntax", () => {
const target: Target = {
role: "coder",
prompt: "Implement the code",
location: "{{{repoPath}}}",
};
expect(target.location).toBe("{{{repoPath}}}");
});
});
});
+49
View File
@@ -0,0 +1,49 @@
export {
START_NODE_SCHEMA,
STEP_NODE_SCHEMA,
WORKFLOW_SCHEMA,
} from "./schemas.js";
export {
createThreadIndexEntry,
markThreadSuspended,
normalizeThreadIndexEntry,
parseThreadsIndex,
serializeThreadIndexEntry,
serializeThreadsIndex,
updateThreadHead,
} from "./thread-index.js";
export type {
AgentAlias,
AgentConfig,
CasRef,
GraphPseudoRole,
ModelAlias,
ModelConfig,
ModeratorContext,
ProviderAlias,
ProviderConfig,
RoleDefinition,
RoleName,
RunningThreadItem,
RunningThreadsOutput,
Scenario,
StartEntry,
StartNodePayload,
StartOutput,
StepContext,
StepEntry,
StepNodePayload,
StepOutput,
StepRecord,
Target,
ThreadForkOutput,
ThreadId,
ThreadIndexEntry,
ThreadListItem,
ThreadStatus,
ThreadStepsOutput,
ThreadsIndex,
WorkflowConfig,
WorkflowName,
WorkflowPayload,
} from "./types.js";
+96
View File
@@ -0,0 +1,96 @@
import type { JSONSchema } from "@ocas/core";
const ROLE_DEFINITION: JSONSchema = {
type: "object",
required: ["description", "goal", "capabilities", "procedure", "output", "frontmatter"],
properties: {
description: { type: "string" },
goal: { type: "string" },
capabilities: { type: "array", items: { type: "string" } },
procedure: { type: "string" },
output: { type: "string" },
frontmatter: { type: "string", format: "ocas_ref" },
},
additionalProperties: false,
};
const TARGET: JSONSchema = {
type: "object",
required: ["role", "prompt"],
properties: {
role: { type: "string", description: "Role name or pseudo-role ($END, $SUSPEND)" },
prompt: { type: "string" },
location: {
anyOf: [{ type: "string" }, { type: "null" }],
},
},
additionalProperties: false,
};
export const WORKFLOW_SCHEMA: JSONSchema = {
title: "Workflow",
type: "object",
required: ["name", "description", "roles", "graph"],
properties: {
name: { type: "string" },
description: { type: "string" },
roles: {
type: "object",
additionalProperties: ROLE_DEFINITION,
},
graph: {
type: "object",
additionalProperties: {
type: "object",
additionalProperties: TARGET,
},
},
},
additionalProperties: false,
};
export const START_NODE_SCHEMA: JSONSchema = {
title: "StartNode",
type: "object",
required: ["workflow", "prompt", "cwd"],
properties: {
workflow: { type: "string", format: "ocas_ref" },
prompt: { type: "string" },
cwd: { type: "string" },
},
additionalProperties: false,
};
export const STEP_NODE_SCHEMA: JSONSchema = {
title: "StepNode",
type: "object",
required: [
"start",
"prev",
"role",
"output",
"detail",
"agent",
"startedAtMs",
"completedAtMs",
"cwd",
],
properties: {
start: { type: "string", format: "ocas_ref" },
prev: {
anyOf: [{ type: "string", format: "ocas_ref" }, { type: "null" }],
},
role: { type: "string" },
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: "ocas_ref" }, { type: "null" }],
},
},
additionalProperties: false,
};
+89
View File
@@ -0,0 +1,89 @@
import type { CasRef, ThreadId, ThreadIndexEntry, ThreadsIndex } from "./types.js";
/** Normalize a legacy head hash or entry object into {@link ThreadIndexEntry}. */
export function normalizeThreadIndexEntry(raw: unknown): ThreadIndexEntry | null {
if (typeof raw === "string") {
return createThreadIndexEntry(raw as CasRef);
}
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
return null;
}
const rec = raw as Record<string, unknown>;
const head = rec.head;
if (typeof head !== "string") {
return null;
}
const suspendedRole = rec.suspendedRole;
const suspendMessage = rec.suspendMessage;
return {
head: head as CasRef,
suspendedRole: typeof suspendedRole === "string" ? suspendedRole : null,
suspendMessage: typeof suspendMessage === "string" ? suspendMessage : null,
};
}
export function createThreadIndexEntry(head: CasRef): ThreadIndexEntry {
return {
head,
suspendedRole: null,
suspendMessage: null,
};
}
export function updateThreadHead(_entry: ThreadIndexEntry, head: CasRef): ThreadIndexEntry {
return {
head,
suspendedRole: null,
suspendMessage: null,
};
}
export function markThreadSuspended(
entry: ThreadIndexEntry,
suspendedRole: string,
suspendMessage: string,
): ThreadIndexEntry {
return {
head: entry.head,
suspendedRole,
suspendMessage,
};
}
/** Serialize for threads.yaml — compact string when not suspended. */
export function serializeThreadIndexEntry(
entry: ThreadIndexEntry,
): string | Record<string, string> {
if (entry.suspendedRole === null || entry.suspendMessage === null) {
return entry.head;
}
return {
head: entry.head,
suspendedRole: entry.suspendedRole,
suspendMessage: entry.suspendMessage,
};
}
export function parseThreadsIndex(raw: unknown): ThreadsIndex {
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
return {};
}
const index: ThreadsIndex = {};
for (const [threadId, value] of Object.entries(raw as Record<string, unknown>)) {
const entry = normalizeThreadIndexEntry(value);
if (entry !== null) {
index[threadId as ThreadId] = entry;
}
}
return index;
}
export function serializeThreadsIndex(
index: ThreadsIndex,
): Record<string, string | Record<string, string>> {
const out: Record<string, string | Record<string, string>> = {};
for (const [threadId, entry] of Object.entries(index)) {
out[threadId] = serializeThreadIndexEntry(entry);
}
return out;
}
+214
View File
@@ -0,0 +1,214 @@
// ── 4.1 公共类型 ────────────────────────────────────────────────────
/** CAS hash — XXH64, 13-char Crockford Base32 */
export type CasRef = string;
/** Thread ID — ULID, 26-char Crockford Base32 */
export type ThreadId = string;
/** 一个 step 的核心数据,被 StepNode payload 和 JSONata 上下文共享 */
export type StepRecord = {
role: string;
output: CasRef;
detail: CasRef;
agent: string;
/** Moderator edge prompt that led to this step. Missing in legacy nodes → "". */
edgePrompt: string;
/** Date.now() before agent spawn */
startedAtMs: number;
/** Date.now() after agent returns */
completedAtMs: number;
/** Working directory where the agent executed. Missing in legacy nodes → "". */
cwd: string;
/** CAS ref to the fully assembled prompt sent to the agent. null for legacy steps. */
assembledPrompt: CasRef | null;
};
// ── 4.2 Workflow 定义 ───────────────────────────────────────────────
export type RoleDefinition = {
description: string;
goal: string;
capabilities: string[];
procedure: string;
output: string;
frontmatter: CasRef;
};
/** Pseudo-role targets in workflow graph edges (not real roles). */
export type GraphPseudoRole = "$END" | "$SUSPEND";
export type Target = {
/** Next role name, or a graph pseudo-role such as `$END` or `$SUSPEND`. */
role: string | GraphPseudoRole;
prompt: string;
/** Optional working directory override via mustache template. */
location: string | null;
};
export type WorkflowPayload = {
name: string;
description: string;
roles: Record<string, RoleDefinition>;
graph: Record<string, Record<string, Target>>;
};
// ── 4.3 Thread 节点 ─────────────────────────────────────────────────
export type StartNodePayload = {
workflow: CasRef;
prompt: string;
/** Working directory where the thread was created. */
cwd: string;
};
export type StepNodePayload = StepRecord & {
start: CasRef;
prev: CasRef | null;
};
// ── 4.4 JSONata 求值上下文 ──────────────────────────────────────────
/** JSONata 上下文中的 step — output 被展开 */
export type StepContext = Omit<StepRecord, "output"> & {
output: unknown;
content: string | null;
};
export type ModeratorContext = {
start: StartNodePayload;
steps: StepContext[];
};
// ── 4.5 CLI 输出 ────────────────────────────────────────────────────
/** Thread status — unified status representation */
export type ThreadStatus = "idle" | "running" | "suspended" | "completed" | "cancelled";
/** uwf thread start */
export type StartOutput = {
workflow: CasRef;
thread: ThreadId;
};
/**
* Output from thread show and thread exec commands.
*
* @property status - Current thread status (idle/running/suspended/completed/cancelled)
* @property done - @deprecated Use status field instead. True if thread is completed or cancelled.
* @property background - @deprecated Use status field instead. Always null in current implementation.
*/
export type StepOutput = {
workflow: CasRef;
thread: ThreadId;
head: CasRef;
status: ThreadStatus;
/** The current or next role. Null when completed, cancelled, suspended, or next is $END. */
currentRole: string | null;
/** Role whose output triggered suspension. Null when thread is not suspended. */
suspendedRole: string | null;
/** Rendered suspend prompt for the user. Null when thread is not suspended. */
suspendMessage: string | null;
done: boolean;
background: boolean | null;
};
/** Active thread entry in ~/.uwf/threads.yaml */
export type ThreadIndexEntry = {
head: CasRef;
suspendedRole: string | null;
suspendMessage: string | null;
};
/** uwf thread steps — single step entry */
export type StepEntry = {
hash: CasRef;
role: string;
output: unknown;
detail: CasRef;
agent: string;
timestamp: number;
durationMs: number;
};
/** uwf thread steps — start entry */
export type StartEntry = {
hash: CasRef;
workflow: CasRef;
prompt: string;
timestamp: number;
};
/** uwf thread steps output */
export type ThreadStepsOutput = {
thread: ThreadId;
workflow: CasRef;
steps: [StartEntry, ...StepEntry[]];
};
/** uwf thread fork output */
export type ThreadForkOutput = {
thread: ThreadId;
forkedFrom: {
step: CasRef;
};
};
/** uwf thread list */
export type ThreadListItem = {
thread: ThreadId;
workflow: CasRef;
head: CasRef;
};
/** uwf thread running — single running thread entry */
export type RunningThreadItem = {
thread: ThreadId;
workflow: CasRef;
pid: number;
startedAt: number;
};
/** uwf thread running output */
export type RunningThreadsOutput = {
threads: RunningThreadItem[];
};
// ── 4.6 配置 ────────────────────────────────────────────────────────
/** Alias types for config references */
export type AgentAlias = string;
export type ModelAlias = string;
export type ProviderAlias = string;
export type WorkflowName = string;
export type RoleName = string;
export type Scenario = string;
export type ProviderConfig = {
baseUrl: string;
apiKey: string;
};
export type ModelConfig = {
provider: ProviderAlias;
name: string;
};
export type AgentConfig = {
command: string;
args: string[];
};
/** ~/.uwf/config.yaml */
export 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;
};
/** ~/.uwf/threads.yaml */
export type ThreadsIndex = Record<ThreadId, ThreadIndexEntry>;
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}