Thank you for your interest in contributing to ORCH! Whether you're fixing a bug, adding a feature, writing docs, or building a new adapter — this guide will get you started quickly.
⭐ If ORCH saves you time, please star the repo to help others discover it.
New here? Look for issues labeled
good first issue— they're scoped, well-documented, and ready for a PR.
# 1. Fork & clone
git clone https://github.com/<your-username>/ORCH.git
cd ORCH
# 2. Install dependencies
npm install
# 3. Run in dev mode (no build needed)
npm run dev
# 4. Run tests — all 1694 should pass
npm test
# 5. Type-check
npm run typecheckPrerequisites: Node.js >= 20, npm (comes with Node).
| Command | What it does |
|---|---|
npm run dev |
Run via tsx (no build step) |
npm run build |
Production build (ESM + DTS via tsup) |
npm test |
Run all tests (Vitest) |
npm run typecheck |
tsc --noEmit — strict mode |
npm run coverage |
Tests with coverage report |
feat/your-feature # New feature
fix/brief-description # Bug fix
docs/what-you-changed # Documentation
refactor/what-changed # Refactoring (no behavior change)
ORCH follows layered DDD — each layer only depends on layers above it:
Domain (models, state machine, errors) ← pure logic, zero I/O
↓
Application (services, orchestrator, event bus) ← business workflows
↓
Infrastructure (adapters, storage, processes) ← file I/O, spawning
↓
CLI (Commander.js) / TUI (Ink + React) ← user interface
Key principle: Domain and Application layers have zero dependencies on Infrastructure or CLI. The @oxgeneral/orch npm package exports the full engine API — it's a runtime, not just a CLI.
src/
├── domain/ # Models, state machine, errors
│ ├── task.ts # Task model
│ ├── agent.ts # Agent model
│ ├── transitions.ts # State machine: todo → in_progress → review → done
│ └── errors.ts # OrchestryError base class
├── application/ # Services + orchestrator engine
│ ├── orchestrator.ts # Tick loop: reconcile → dispatch → collect
│ └── *-service.ts # Task, agent, run services
├── infrastructure/
│ ├── adapters/ # Claude, OpenCode, Codex, Cursor, Shell
│ ├── storage/ # YAML/JSON/JSONL file stores
│ ├── process/ # PID management, graceful kill
│ ├── template/ # LiquidJS prompt templates
│ └── workspace/ # Git worktree isolation
├── cli/ # Commander.js commands
└── tui/ # Ink + React terminal dashboard
- Strict mode —
noUncheckedIndexedAccess: true - No
any— useunknownor proper generics.as anyis not accepted. - No
@ts-ignore— fix the type instead. - ESM — all local imports use
.jsextension:// ✅ Correct import { TaskStore } from './storage/task-store.js'; // ❌ Wrong — missing .js import { TaskStore } from './storage/task-store';
| What | Convention | Example |
|---|---|---|
| Variables, functions | camelCase | getTaskById |
| Types, classes | PascalCase | TaskService |
| Files | kebab-case | task-service.ts |
| IDs | Prefixed nanoid | tsk_, agt_, run_, goal_, team_, msg_ |
- Domain errors extend
OrchestryErrorwithexitCodeand optionalhint - No
console.login production code — useeventBus.emit() - Always handle error paths — no silent catches
- Use
Promise.all()for parallel file I/O — never sequential loops for reads - Atomic writes: write to temp file, then
rename()— never write directly - Cap data structures: LRU for maps, truncation for strings (2KB)
Adapters connect ORCH to external AI tools. Each adapter implements the IAgentAdapter interface.
src/infrastructure/adapters/your-tool.ts
import type {
IAgentAdapter,
AdapterTestResult,
ExecuteParams,
AgentEvent,
ExecuteHandle,
} from './interface.js';
import type { IProcessManager } from '../process/process-manager.js';
export class YourToolAdapter implements IAgentAdapter {
readonly kind = 'your-tool'; // This is the adapter name agents use
constructor(private readonly processManager: IProcessManager) {}
async test(): Promise<AdapterTestResult> {
// Check if the tool CLI is installed and return its version
try {
// ... check tool availability
return { ok: true, version: '1.0.0' };
} catch {
return { ok: false, error: 'your-tool CLI not found' };
}
}
execute(params: ExecuteParams): ExecuteHandle {
// Spawn the tool process
const { process: proc, pid } = this.processManager.spawn(
'your-tool',
['--prompt', params.prompt],
{ cwd: params.workspace, env: { ...process.env, ...params.env } },
);
// Return pid + async generator that yields AgentEvent objects
async function* streamEvents(): AsyncGenerator<AgentEvent> {
// ... yield events as the tool produces output
yield {
type: 'output',
timestamp: new Date().toISOString(),
data: 'Tool output here',
};
yield {
type: 'done',
timestamp: new Date().toISOString(),
data: 'Completed',
};
}
return { pid, events: streamEvents() };
}
async stop(pid: number): Promise<void> {
await this.processManager.kill(pid);
}
}In src/container.ts, import and register your adapter:
import { YourToolAdapter } from './infrastructure/adapters/your-tool.js';
// Inside buildContainer():
adapterRegistry.register(new YourToolAdapter(processManager));Create test/unit/infrastructure/adapters/your-tool.test.ts with:
test()returnsok: truewhen CLI is availabletest()returnsok: falsegracefully when CLI is missingexecute()yields correct event sequencestop()kills the process
- Add the adapter to the adapters list in
readme.md - Update the test count badge if tests changed significantly
Tests mirror src/ under test/unit/:
src/domain/task.ts → test/unit/domain/task.test.ts
src/application/run-service.ts → test/unit/application/run-service.test.ts
import { describe, it, expect } from 'vitest';
describe('YourModule', () => {
it('should do one specific thing', () => {
// Arrange
// Act
// Assert — one assertion per test
});
});Rules:
- One test = one assertion (as much as possible)
- Deterministic — no
Date.now(), noMath.random(), no real file I/O - Use mock factories from
test/unit/application/helpers.ts:makeTask(),makeAgent(),makeRun()for domain objectsbuildDeps()for full orchestrator dependency injection
- Use CLI test helpers from
test/unit/cli/helpers.ts - File naming:
*.test.tsor*.test.tsx
npm test # All tests
npm test -- test/unit/domain/ # Directory
npm test -- --grep "state machine" # By pattern
npm run coverage # With coverage reportKnown flaky test: lock-concurrency — can be ignored if it fails intermittently.
Before opening a PR, verify:
- Tests pass:
npm test— all green - Types check:
npm run typecheck— zero errors - Build works:
npm run build— no warnings - No
anyor@ts-ignorein your code - No
console.log— useeventBus.emit()for logging - ESM imports use
.jsextension - New code has tests — aim for edge cases, not just happy path
- Atomic file writes — temp file + rename pattern
- Commit message describes why, not just what
## Summary
- What changed and why (1-3 bullets)
## Test Plan
- How to verify the changes
- Which tests cover it
Open an issue with:
- Steps to reproduce — exact commands you ran
- Expected vs actual behavior
- Environment — OS, Node version (
node -v), ORCH version (orch --version) - Logs — relevant output from
orch logs <run-id>or terminal
Open a Discussion in the "Feature Requests" category:
- Problem — what workflow is painful today?
- Proposed solution — how should it work?
- Alternatives considered — what else did you try?
See docs/RELEASING.md for the full process. Quick version:
./scripts/release.sh patch|minor|major
git push && git push --tagsGitHub Actions automatically publishes to npm.
By contributing, you agree that your contributions will be licensed under the MIT License.
Every contribution matters.
Bug reports, docs fixes, new adapters, test improvements — all welcome.