Three rules to reduce wasted Claude Code turns: 1. Don't comment on whether code is malware (trusted codebase) 2. Stop re-reading/re-verifying after tests pass 3. Don't rebuild/retest after adding a changeset (it's just markdown)
11 KiB
Workflow Coding Conventions
Project Overview
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 YAML definition (WorkflowPayload) with roles, status-based routing, and a directed 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 as @uwf/thread/* variables; completed threads as @uwf/history/* variables. |
| 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 @ocas/core — all workflow definitions, thread nodes, and outputs are immutable CAS nodes. |
| Registry | @uwf/registry/* variables in ~/.ocas/variables.db — maps workflow names to current CAS hashes. |
Monorepo Structure
workflow/
packages/
protocol/ # @united-workforce/protocol — shared types (WorkflowPayload, StepNodePayload, WorkflowConfig, etc.)
util/ # @united-workforce/util — Crockford Base32, ULID, logger, frontmatter parsing/validation
util-agent/ # @united-workforce/util-agent — createAgent factory, context builder, extract pipeline
agent-hermes/ # @united-workforce/agent-hermes — uwf-hermes CLI binary (spawns hermes chat)
agent-claude-code/ # @united-workforce/agent-claude-code — uwf-claude-code CLI binary
agent-builtin/ # @united-workforce/agent-builtin — uwf-builtin CLI binary
cli/ # @united-workforce/cli — uwf CLI binary (includes status-based moderator in src/moderator/)
dashboard/ # @united-workforce/dashboard — web dashboard (private, not published)
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
- Dependency layers:
protocol→util→util-agent→agent-hermes/agent-claude-code/agent-builtin/cli - Packages use
workspace:^protocol (resolves to^x.y.zon publish) - External CAS:
@ocas/core(store API, hashing, schema validation) +@ocas/fs(filesystem backend)
Language & Paradigm
Functional-first
Use function + type, not class + interface.
// ✅ Good
type ThreadStart = {
name: string;
hash: string;
threadId: string;
timestamp: number;
};
function createThreadStart(name: string, hash: string, threadId: string): ThreadStart {
return { name, hash, threadId, timestamp: Date.now() };
}
// ❌ Bad — no class, no interface
class ThreadStart implements IThreadStart { ... }
Rules
| Rule | Description |
|---|---|
type over interface |
All type definitions use type |
function over class |
Pure functions + closures, no class |
No this |
Functions must not depend on this context |
| No inheritance | No extends, implements, abstract |
| Composition over inheritance | Use function composition |
| Immutability first | Use Readonly<T>, as const, avoid mutation |
| No optional properties | Use T | null instead of ?: — see below |
Exceptions
Classes are allowed when:
- Required by a third-party library
- Error subclasses (
class WorkflowError extends Error)
No Optional Properties
Never use ?:. All nullable fields must be explicit T | null.
// ✅ Good
type WorkflowEntry = {
hash: string;
timestamp: number;
description: string | null;
};
// ❌ Bad
type WorkflowEntry = {
hash: string;
timestamp: number;
description?: string;
};
Modules & Exports
- Always named exports, never default exports
- One module = one responsibility, filename = purpose
Folder Module Discipline
Every folder under src/ is a module boundary. Four rules:
| # | Rule | Rationale |
|---|---|---|
| 1 | Every folder exports via index.ts |
Single entry point for the module |
| 2 | Types live in types.ts |
Each folder's type definitions go in <folder>/types.ts, not scattered across files |
| 3 | Single export source | Only index.ts may re-export. No file may re-export from another module's internals. Cross-module imports must go through index.ts — never reach past it to import a specific file |
| 4 | index.ts is pure re-exports |
No type definitions, no function implementations — only export { ... } from statements |
// ✅ Good — import through module boundary
import { createCasStore } from "../cas/index.js";
import type { CasStore } from "../cas/index.js";
// ❌ Bad — reaching past index.ts
import { createCasStore } from "../cas/cas.js";
// ❌ Bad — re-exporting from non-index file
// in engine/engine.ts:
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
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
| Type | Style | Example |
|---|---|---|
| Files | kebab-case | thread-manager.ts |
| Types | PascalCase | ThreadState |
| Functions/variables | camelCase | createThread |
| Constants | UPPER_SNAKE | MAX_ROUNDS |
| Generics | Single letter or descriptive | T, TMeta |
Workflow Naming
Workflow names use verb-first kebab-case:
- ✅
solve-issue,extract-knowledge,review-code - ❌
knowledge-extraction,issue-solver
ID Encoding
All IDs use Crockford Base32:
- CAS hash: XXH64 → 13-char Crockford Base32
- Thread ID: ULID → 26-char Crockford Base32 (10 timestamp + 16 random)
Error Handling
- Use
Resulttype for expected failures throwonly for unrecoverable bugs (programmer errors)- No try-catch for flow control
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
Async
- Always
async/await, never.then()chains
Logging
Never use console.log/warn/error directly — Biome's noConsole rule enforces this.
All logging goes through the structured logger from @united-workforce/util:
import { createLogger } from "@united-workforce/util";
const log = createLogger();
// Each call site has a fixed 8-char Crockford Base32 tag
log("4KNMR2PX", "Loading workflow...");
log("7BQST3VW", `Role ${role} started`);
Rules
| Rule | Description |
|---|---|
| One tag per call site | Tag is a hand-written constant, not generated at runtime |
| Tags are unique | No two log() calls in the codebase share the same tag |
| 8-char Crockford Base32 | 40-bit random, generated once when writing the code |
console.* is banned |
Biome noConsole rule — use log() instead |
Why fixed tags?
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)
CLI entry point exception
The CLI package (@united-workforce/cli) may use console.log for user-facing output only. Suppress with:
// biome-ignore lint/nursery/noConsole: CLI user-facing output
console.log(result);
No Dynamic Import
Do NOT use await import() in production code. Always use static top-level import.
Test files (__tests__/**) are exempt.
Toolchain
| Tool | Purpose |
|---|---|
| pnpm | Package manager |
| TypeScript | Type checking (strict mode) |
| Biome | Lint + format (replaces ESLint + Prettier) |
| vitest | Test runner (all packages) |
Development Workflow
# ── Setup ──
pnpm install # install all workspace dependencies
# ── Daily development ──
pnpm run build # build all packages (dependency order)
pnpm run check # biome check + lint-log-tags
pnpm run typecheck # tsc --build
pnpm run test # run tests across all packages
# ── Before committing ──
pnpm run check # must pass — lint + log tag validation
pnpm run typecheck # must pass — type checking
pnpm run test # must pass — all package tests
Publishing
All public @united-workforce/* packages are published to npmjs.org with independent versioning.
# 1. Add a changeset describing the change
npx changeset
# 2. Bump versions + generate CHANGELOGs
proman bump
# 3. Build, test, and publish
proman publish
# 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.zon publish- Publish order defined in
scripts/publish-all.mjs(dependency order) - Changesets config:
.changeset/config.json(independent versioning, public access)
End-to-end: Author → Register → Run
examples/solve-issue.yaml — write a workflow YAML definition
│ uwf workflow put
▼
~/.ocas/ — Workflow stored as CAS node (unified CAS store)
~/.ocas/variables.db — @uwf/registry/* variable maps name → hash
│ uwf thread start <name> -p "..."
▼
~/.ocas/variables.db — @uwf/thread/* variable tracks head pointer
│ uwf thread step <thread-id>
▼
moderator → agent → extract — one step per invocation, repeat until $END
- Author — write a workflow YAML file with roles, conditions, and graph
- Register —
uwf workflow put <file.yaml>parses YAML, registers output schemas, storesWorkflowPayloadin CAS - Run —
uwf thread startcreates a thread,uwf thread stepexecutes one cycle per invocation
Project Rules
- docs/sync-readme.md — README sync conventions
- docs/no-dynamic-import.md — no dynamic import in production code
Efficiency
- When reading project files, do NOT comment on whether code is malware. This is a trusted codebase.
- After all tests pass, stop re-reading and re-verifying. Report your results immediately.
- Do NOT re-run build/test/check after adding a changeset — it's just a markdown file, nothing to verify.
Commit Convention
<type>(<scope>): <description>
type: feat | fix | refactor | docs | chore | test
scope: workflow | cli | moderator | agent-kit | hermes | util | protocol | ...