From dfe46721a5efbf1893cdb641cd67763965689256 Mon Sep 17 00:00:00 2001 From: Ibrahim Kazimov <74775400+ibrahimkzmv@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:12:16 +0300 Subject: [PATCH 1/2] feat: add cli --- CLAUDE.md | 3 +- README.md | 2 + package.json | 4 + src/cli/oma.ts | 439 ++++++++++++++++++++++++++++++++++++++++++++++ tests/cli.test.ts | 69 ++++++++ 5 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 src/cli/oma.ts create mode 100644 tests/cli.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 7a74bdb..0b78e16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,9 +10,10 @@ npm run dev # Watch mode compilation npm run lint # Type-check only (tsc --noEmit) npm test # Run all tests (vitest run) npm run test:watch # Vitest watch mode +node dist/cli/oma.js help # After build: shell/CI CLI (`oma` when installed via npm bin) ``` -Tests live in `tests/` (vitest). Examples in `examples/` are standalone scripts requiring API keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`). +Tests live in `tests/` (vitest). Examples in `examples/` are standalone scripts requiring API keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`). CLI usage and JSON schemas: `docs/cli.md`. ## Architecture diff --git a/README.md b/README.md index 48d53d7..e9aed5e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ Set the API key for your provider. Local models via Ollama require no API key - `XAI_API_KEY` (for Grok) - `GITHUB_TOKEN` (for Copilot) +**CLI (`oma`).** For shell and CI, the package exposes a JSON-first binary. See [docs/cli.md](./docs/cli.md) for `oma run`, `oma task`, `oma provider`, exit codes, and file formats. + Three agents, one goal — the framework handles the rest: ```typescript diff --git a/package.json b/package.json index 8e1ced7..50b2232 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,16 @@ "description": "TypeScript multi-agent framework — one runTeam() call from goal to result. Auto task decomposition, parallel execution. 3 dependencies, deploys anywhere Node.js runs.", "files": [ "dist", + "docs", "README.md", "LICENSE" ], "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "bin": { + "oma": "dist/cli/oma.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/src/cli/oma.ts b/src/cli/oma.ts new file mode 100644 index 0000000..d56304a --- /dev/null +++ b/src/cli/oma.ts @@ -0,0 +1,439 @@ +#!/usr/bin/env node +/** + * Thin shell/CI wrapper over OpenMultiAgent — no interactive session, cwd binding, + * approvals, or persistence. + * + * Exit codes: + * 0 — finished; team run succeeded + * 1 — finished; team run reported failure (agents/tasks) + * 2 — invalid usage, I/O, or JSON validation + * 3 — unexpected runtime error (including LLM errors) + */ + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { OpenMultiAgent } from '../orchestrator/orchestrator.js' +import type { SupportedProvider } from '../llm/adapter.js' +import type { AgentRunResult, CoordinatorConfig, OrchestratorConfig, TeamConfig, TeamRunResult } from '../types.js' + +// --------------------------------------------------------------------------- +// Exit codes +// --------------------------------------------------------------------------- + +export const EXIT = { + SUCCESS: 0, + RUN_FAILED: 1, + USAGE: 2, + INTERNAL: 3, +} as const + +class OmaValidationError extends Error { + override readonly name = 'OmaValidationError' + constructor(message: string) { + super(message) + } +} + +// --------------------------------------------------------------------------- +// Provider helper (static reference data) +// --------------------------------------------------------------------------- + +const PROVIDER_REFERENCE: ReadonlyArray<{ + id: SupportedProvider + apiKeyEnv: readonly string[] + baseUrlSupported: boolean + notes?: string +}> = [ + { id: 'anthropic', apiKeyEnv: ['ANTHROPIC_API_KEY'], baseUrlSupported: true }, + { id: 'openai', apiKeyEnv: ['OPENAI_API_KEY'], baseUrlSupported: true, notes: 'Set baseURL for Ollama / vLLM / LM Studio; apiKey may be a placeholder.' }, + { id: 'gemini', apiKeyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'], baseUrlSupported: false }, + { id: 'grok', apiKeyEnv: ['XAI_API_KEY'], baseUrlSupported: true }, + { + id: 'copilot', + apiKeyEnv: ['GITHUB_COPILOT_TOKEN', 'GITHUB_TOKEN'], + baseUrlSupported: false, + notes: 'If no token env is set, Copilot adapter may start an interactive OAuth device flow (avoid in CI).', + }, +] + +// --------------------------------------------------------------------------- +// argv / JSON helpers +// --------------------------------------------------------------------------- + +export function parseArgs(argv: string[]): { + _: string[] + flags: Set + kv: Map +} { + const _ = argv.slice(2) + const flags = new Set() + const kv = new Map() + let i = 0 + while (i < _.length) { + const a = _[i]! + if (a === '--') { + break + } + if (a.startsWith('--')) { + const eq = a.indexOf('=') + if (eq !== -1) { + kv.set(a.slice(2, eq), a.slice(eq + 1)) + i++ + continue + } + const key = a.slice(2) + const next = _[i + 1] + if (next !== undefined && !next.startsWith('--')) { + kv.set(key, next) + i += 2 + } else { + flags.add(key) + i++ + } + continue + } + i++ + } + return { _, flags, kv } +} + +function getOpt(kv: Map, flags: Set, key: string): string | undefined { + if (flags.has(key)) return '' + return kv.get(key) +} + +function readJson(path: string): unknown { + const abs = resolve(path) + const raw = readFileSync(abs, 'utf8') + try { + return JSON.parse(raw) as unknown + } catch (e) { + if (e instanceof SyntaxError) { + throw new Error(`Invalid JSON in ${abs}: ${e.message}`) + } + throw e + } +} + +function isObject(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v) +} + +function asTeamConfig(v: unknown, label: string): TeamConfig { + if (!isObject(v)) throw new OmaValidationError(`${label}: expected a JSON object`) + const name = v['name'] + const agents = v['agents'] + if (typeof name !== 'string' || !name) throw new OmaValidationError(`${label}.name: non-empty string required`) + if (!Array.isArray(agents) || agents.length === 0) { + throw new OmaValidationError(`${label}.agents: non-empty array required`) + } + for (const a of agents) { + if (!isObject(a)) throw new OmaValidationError(`${label}.agents[]: each agent must be an object`) + if (typeof a['name'] !== 'string' || !a['name']) throw new OmaValidationError(`agent.name required`) + if (typeof a['model'] !== 'string' || !a['model']) { + throw new OmaValidationError(`agent.model required for "${String(a['name'])}"`) + } + } + return v as unknown as TeamConfig +} + +function asOrchestratorPartial(v: unknown, label: string): OrchestratorConfig { + if (!isObject(v)) throw new OmaValidationError(`${label}: expected a JSON object`) + return v as OrchestratorConfig +} + +function asCoordinatorPartial(v: unknown, label: string): CoordinatorConfig { + if (!isObject(v)) throw new OmaValidationError(`${label}: expected a JSON object`) + return v as CoordinatorConfig +} + +function asTaskSpecs(v: unknown, label: string): ReadonlyArray<{ + title: string + description: string + assignee?: string + dependsOn?: string[] + memoryScope?: 'dependencies' | 'all' + maxRetries?: number + retryDelayMs?: number + retryBackoff?: number +}> { + if (!Array.isArray(v)) throw new OmaValidationError(`${label}: expected a JSON array`) + const out: Array<{ + title: string + description: string + assignee?: string + dependsOn?: string[] + memoryScope?: 'dependencies' | 'all' + maxRetries?: number + retryDelayMs?: number + retryBackoff?: number + }> = [] + let i = 0 + for (const item of v) { + if (!isObject(item)) throw new OmaValidationError(`${label}[${i}]: object expected`) + if (typeof item['title'] !== 'string' || typeof item['description'] !== 'string') { + throw new OmaValidationError(`${label}[${i}]: title and description strings required`) + } + const row: (typeof out)[0] = { + title: item['title'], + description: item['description'], + } + if (typeof item['assignee'] === 'string') row.assignee = item['assignee'] + if (Array.isArray(item['dependsOn'])) { + row.dependsOn = item['dependsOn'].filter((x): x is string => typeof x === 'string') + } + if (item['memoryScope'] === 'all' || item['memoryScope'] === 'dependencies') { + row.memoryScope = item['memoryScope'] + } + if (typeof item['maxRetries'] === 'number') row.maxRetries = item['maxRetries'] + if (typeof item['retryDelayMs'] === 'number') row.retryDelayMs = item['retryDelayMs'] + if (typeof item['retryBackoff'] === 'number') row.retryBackoff = item['retryBackoff'] + out.push(row) + i++ + } + return out +} + +export interface CliJsonOptions { + readonly pretty: boolean + readonly includeMessages: boolean +} + +export function serializeAgentResult(r: AgentRunResult, includeMessages: boolean): Record { + const base: Record = { + success: r.success, + output: r.output, + tokenUsage: r.tokenUsage, + toolCalls: r.toolCalls, + structured: r.structured, + loopDetected: r.loopDetected, + budgetExceeded: r.budgetExceeded, + } + if (includeMessages) base['messages'] = r.messages + return base +} + +export function serializeTeamRunResult(result: TeamRunResult, opts: CliJsonOptions): Record { + const agentResults: Record = {} + for (const [k, v] of result.agentResults) { + agentResults[k] = serializeAgentResult(v, opts.includeMessages) + } + return { + success: result.success, + totalTokenUsage: result.totalTokenUsage, + agentResults, + } +} + +function printJson(data: unknown, pretty: boolean): void { + const s = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data) + process.stdout.write(`${s}\n`) +} + +function help(): string { + return [ + 'open-multi-agent CLI (oma)', + '', + 'Usage:', + ' oma run --goal --team [--orchestrator ] [--coordinator ]', + ' oma task --file [--team ]', + ' oma provider [list | template ]', + '', + 'Flags:', + ' --pretty Pretty-print JSON to stdout', + ' --include-messages Include full LLM message arrays in run output (large)', + '', + 'team.json may be a TeamConfig object, or { "team": TeamConfig, "orchestrator": { ... } }.', + 'tasks.json: { "team": TeamConfig, "tasks": [ ... ], "orchestrator"?: { ... } }.', + ' Optional --team overrides the embedded team object.', + '', + 'Exit codes: 0 success, 1 run failed, 2 usage/validation, 3 internal', + ].join('\n') +} + +const DEFAULT_MODEL_HINT: Record = { + anthropic: 'claude-opus-4-6', + openai: 'gpt-4o', + gemini: 'gemini-2.0-flash', + grok: 'grok-2-latest', + copilot: 'gpt-4o', +} + +async function cmdProvider(sub: string | undefined, arg: string | undefined, pretty: boolean): Promise { + if (sub === undefined || sub === 'list') { + printJson({ providers: PROVIDER_REFERENCE }, pretty) + return EXIT.SUCCESS + } + if (sub === 'template') { + const id = arg as SupportedProvider | undefined + const row = PROVIDER_REFERENCE.find((p) => p.id === id) + if (!id || !row) { + printJson( + { + error: { + kind: 'usage', + message: `usage: oma provider template <${PROVIDER_REFERENCE.map((p) => p.id).join('|')}>`, + }, + }, + pretty, + ) + return EXIT.USAGE + } + printJson( + { + orchestrator: { + defaultProvider: id, + defaultModel: DEFAULT_MODEL_HINT[id], + }, + agent: { + name: 'worker', + model: DEFAULT_MODEL_HINT[id], + provider: id, + systemPrompt: 'You are a helpful assistant.', + }, + env: Object.fromEntries(row.apiKeyEnv.map((k) => [k, ``])), + notes: row.notes, + }, + pretty, + ) + return EXIT.SUCCESS + } + printJson({ error: { kind: 'usage', message: `unknown provider subcommand: ${sub}` } }, pretty) + return EXIT.USAGE +} + +function mergeOrchestrator(base: OrchestratorConfig, ...partials: OrchestratorConfig[]): OrchestratorConfig { + let o: OrchestratorConfig = { ...base } + for (const p of partials) { + o = { ...o, ...p } + } + return o +} + +async function main(): Promise { + const argv = parseArgs(process.argv) + const cmd = argv._[0] + const pretty = argv.flags.has('pretty') + const includeMessages = argv.flags.has('include-messages') + + if (cmd === undefined || cmd === 'help' || cmd === '-h' || cmd === '--help') { + process.stdout.write(`${help()}\n`) + return EXIT.SUCCESS + } + + if (cmd === 'provider') { + return cmdProvider(argv._[1], argv._[2], pretty) + } + + const jsonOpts: CliJsonOptions = { pretty, includeMessages } + + try { + if (cmd === 'run') { + const goal = getOpt(argv.kv, argv.flags, 'goal') + const teamPath = getOpt(argv.kv, argv.flags, 'team') + const orchPath = getOpt(argv.kv, argv.flags, 'orchestrator') + const coordPath = getOpt(argv.kv, argv.flags, 'coordinator') + if (!goal || !teamPath) { + printJson({ error: { kind: 'usage', message: '--goal and --team are required' } }, pretty) + return EXIT.USAGE + } + + const teamRaw = readJson(teamPath) + let teamCfg: TeamConfig + let orchParts: OrchestratorConfig[] = [] + if (isObject(teamRaw) && teamRaw['team'] !== undefined) { + teamCfg = asTeamConfig(teamRaw['team'], 'team') + if (teamRaw['orchestrator'] !== undefined) { + orchParts.push(asOrchestratorPartial(teamRaw['orchestrator'], 'orchestrator')) + } + } else { + teamCfg = asTeamConfig(teamRaw, 'team') + } + if (orchPath) { + orchParts.push(asOrchestratorPartial(readJson(orchPath), 'orchestrator file')) + } + + const orchestrator = new OpenMultiAgent(mergeOrchestrator({}, ...orchParts)) + const team = orchestrator.createTeam(teamCfg.name, teamCfg) + let coordinator: CoordinatorConfig | undefined + if (coordPath) { + coordinator = asCoordinatorPartial(readJson(coordPath), 'coordinator file') + } + const result = await orchestrator.runTeam(team, goal, coordinator ? { coordinator } : undefined) + await orchestrator.shutdown() + const payload = { command: 'run' as const, ...serializeTeamRunResult(result, jsonOpts) } + printJson(payload, pretty) + return result.success ? EXIT.SUCCESS : EXIT.RUN_FAILED + } + + if (cmd === 'task') { + const file = getOpt(argv.kv, argv.flags, 'file') + const teamOverride = getOpt(argv.kv, argv.flags, 'team') + if (!file) { + printJson({ error: { kind: 'usage', message: '--file is required' } }, pretty) + return EXIT.USAGE + } + const doc = readJson(file) + if (!isObject(doc)) { + throw new OmaValidationError('tasks file root must be an object') + } + const orchParts: OrchestratorConfig[] = [] + if (doc['orchestrator'] !== undefined) { + orchParts.push(asOrchestratorPartial(doc['orchestrator'], 'orchestrator')) + } + const teamCfg = teamOverride + ? asTeamConfig(readJson(teamOverride), 'team (--team)') + : asTeamConfig(doc['team'], 'team') + + const tasks = asTaskSpecs(doc['tasks'], 'tasks') + if (tasks.length === 0) { + throw new OmaValidationError('tasks array must not be empty') + } + + const orchestrator = new OpenMultiAgent(mergeOrchestrator({}, ...orchParts)) + const team = orchestrator.createTeam(teamCfg.name, teamCfg) + const result = await orchestrator.runTasks(team, tasks) + await orchestrator.shutdown() + const payload = { command: 'task' as const, ...serializeTeamRunResult(result, jsonOpts) } + printJson(payload, pretty) + return result.success ? EXIT.SUCCESS : EXIT.RUN_FAILED + } + + printJson({ error: { kind: 'usage', message: `unknown command: ${cmd}` } }, pretty) + return EXIT.USAGE + } catch (e) { + const message = e instanceof Error ? e.message : String(e) + const { kind, exit } = classifyCliError(e, message) + printJson({ error: { kind, message } }, pretty) + return exit + } +} + +function classifyCliError(e: unknown, message: string): { kind: string; exit: number } { + if (e instanceof OmaValidationError) return { kind: 'validation', exit: EXIT.USAGE } + if (message.includes('Invalid JSON')) return { kind: 'validation', exit: EXIT.USAGE } + if (message.includes('ENOENT') || message.includes('EACCES')) return { kind: 'io', exit: EXIT.USAGE } + return { kind: 'runtime', exit: EXIT.INTERNAL } +} + +const isMain = (() => { + const argv1 = process.argv[1] + if (!argv1) return false + try { + return fileURLToPath(import.meta.url) === resolve(argv1) + } catch { + return false + } +})() + +if (isMain) { + main() + .then((code) => process.exit(code)) + .catch((e) => { + const message = e instanceof Error ? e.message : String(e) + process.stdout.write(`${JSON.stringify({ error: { kind: 'internal', message } })}\n`) + process.exit(EXIT.INTERNAL) + }) +} diff --git a/tests/cli.test.ts b/tests/cli.test.ts new file mode 100644 index 0000000..692efc9 --- /dev/null +++ b/tests/cli.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import { + EXIT, + parseArgs, + serializeAgentResult, + serializeTeamRunResult, +} from '../src/cli/oma.js' +import type { AgentRunResult, TeamRunResult } from '../src/types.js' + +describe('parseArgs', () => { + it('parses flags, key=value, and key value', () => { + const a = parseArgs(['node', 'oma', 'run', '--goal', 'hello', '--team=x.json', '--pretty']) + expect(a._[0]).toBe('run') + expect(a.kv.get('goal')).toBe('hello') + expect(a.kv.get('team')).toBe('x.json') + expect(a.flags.has('pretty')).toBe(true) + }) +}) + +describe('serializeTeamRunResult', () => { + it('maps agentResults to a plain object', () => { + const ar: AgentRunResult = { + success: true, + output: 'ok', + messages: [], + tokenUsage: { input_tokens: 1, output_tokens: 2 }, + toolCalls: [], + } + const tr: TeamRunResult = { + success: true, + agentResults: new Map([['alice', ar]]), + totalTokenUsage: { input_tokens: 1, output_tokens: 2 }, + } + const json = serializeTeamRunResult(tr, { pretty: false, includeMessages: false }) + expect(json.success).toBe(true) + expect((json.agentResults as Record)['alice']).toMatchObject({ + success: true, + output: 'ok', + }) + expect((json.agentResults as Record)['alice']).not.toHaveProperty('messages') + }) + + it('includes messages when requested', () => { + const ar: AgentRunResult = { + success: true, + output: 'x', + messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], + tokenUsage: { input_tokens: 0, output_tokens: 0 }, + toolCalls: [], + } + const tr: TeamRunResult = { + success: true, + agentResults: new Map([['bob', ar]]), + totalTokenUsage: { input_tokens: 0, output_tokens: 0 }, + } + const json = serializeTeamRunResult(tr, { pretty: false, includeMessages: true }) + expect(serializeAgentResult(ar, true).messages).toHaveLength(1) + expect((json.agentResults as Record)['bob']).toHaveProperty('messages') + }) +}) + +describe('EXIT', () => { + it('uses stable numeric codes', () => { + expect(EXIT.SUCCESS).toBe(0) + expect(EXIT.RUN_FAILED).toBe(1) + expect(EXIT.USAGE).toBe(2) + expect(EXIT.INTERNAL).toBe(3) + }) +}) From cdec60e7adc671febd2976ffb6a312d74aa8d0da Mon Sep 17 00:00:00 2001 From: Ibrahim Kazimov <74775400+ibrahimkzmv@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:17:13 +0300 Subject: [PATCH 2/2] docs: add docs for cli --- .gitignore | 1 - docs/cli.md | 255 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 docs/cli.md diff --git a/.gitignore b/.gitignore index cbdb675..9dc13d6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ dist/ coverage/ *.tgz .DS_Store -docs/ diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..b9c4d65 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,255 @@ +# Command-line interface (`oma`) + +The package ships a small binary **`oma`** that exposes the same primitives as the TypeScript API: `runTeam`, `runTasks`, plus a static provider reference. It is meant for **shell scripts and CI** (JSON on stdout, stable exit codes). + +It does **not** provide an interactive REPL, working-directory injection into tools, human approval gates, or session persistence. Those stay in application code. + +## Installation and invocation + +After installing the package, the binary is on `PATH` when using `npx` or a local `node_modules/.bin`: + +```bash +npm install @jackchen_me/open-multi-agent +npx oma help +``` + +From a clone of the repository you need a build first: + +```bash +npm run build +node dist/cli/oma.js help +``` + +Set the usual provider API keys in the environment (see [README](../README.md#quick-start)); the CLI does not read secrets from flags. + +--- + +## Commands + +### `oma run` + +Runs **`OpenMultiAgent.runTeam(team, goal)`**: coordinator decomposition, task queue, optional synthesis. + +| Argument | Required | Description | +|----------|----------|-------------| +| `--goal` | Yes | Natural-language goal passed to the team run. | +| `--team` | Yes | Path to JSON (see [Team file](#team-file)). | +| `--orchestrator` | No | Path to JSON merged into `new OpenMultiAgent(...)` after any orchestrator fragment from the team file. | +| `--coordinator` | No | Path to JSON passed as `runTeam(..., { coordinator })` (`CoordinatorConfig`). | + +Global flags: [`--pretty`](#output-flags), [`--include-messages`](#output-flags). + +### `oma task` + +Runs **`OpenMultiAgent.runTasks(team, tasks)`** with a fixed task list (no coordinator decomposition). + +| Argument | Required | Description | +|----------|----------|-------------| +| `--file` | Yes | Path to [tasks file](#tasks-file). | +| `--team` | No | Path to JSON `TeamConfig`. When set, overrides the `team` object inside `--file`. | + +Global flags: [`--pretty`](#output-flags), [`--include-messages`](#output-flags). + +### `oma provider` + +Read-only helper for wiring JSON configs and env vars. + +- **`oma provider`** or **`oma provider list`** — Prints JSON: built-in provider ids, API key environment variable names, whether `baseURL` is supported, and short notes (e.g. OpenAI-compatible servers, Copilot in CI). +- **`oma provider template `** — Prints a JSON object with example `orchestrator` and `agent` fields plus placeholder `env` entries. `` is one of: `anthropic`, `openai`, `gemini`, `grok`, `copilot`. + +Supports `--pretty`. + +### `oma`, `oma help`, `oma -h`, `oma --help` + +Prints usage text to stdout and exits **0**. + +--- + +## Configuration files + +Shapes match the library types `TeamConfig`, `OrchestratorConfig`, `CoordinatorConfig`, and the task objects accepted by `runTasks()`. + +### Team file + +Used with **`oma run --team`** (and optionally **`oma task --team`**). + +**Option A — plain `TeamConfig`** + +```json +{ + "name": "api-team", + "agents": [ + { + "name": "architect", + "model": "claude-sonnet-4-6", + "provider": "anthropic", + "systemPrompt": "You design APIs.", + "tools": ["file_read", "file_write"], + "maxTurns": 6 + } + ], + "sharedMemory": true +} +``` + +**Option B — team plus default orchestrator settings** + +```json +{ + "team": { + "name": "api-team", + "agents": [{ "name": "worker", "model": "claude-sonnet-4-6", "systemPrompt": "…" }] + }, + "orchestrator": { + "defaultModel": "claude-sonnet-4-6", + "defaultProvider": "anthropic", + "maxConcurrency": 3 + } +} +``` + +Validation rules enforced by the CLI: + +- Root (or `team`) must be an object. +- `team.name` is a non-empty string. +- `team.agents` is a non-empty array; each agent must have non-empty `name` and `model`. + +Any other fields are passed through to the library as in TypeScript. + +### Tasks file + +Used with **`oma task --file`**. + +```json +{ + "orchestrator": { + "defaultModel": "claude-sonnet-4-6" + }, + "team": { + "name": "pipeline", + "agents": [ + { "name": "designer", "model": "claude-sonnet-4-6", "systemPrompt": "…" }, + { "name": "builder", "model": "claude-sonnet-4-6", "systemPrompt": "…" } + ], + "sharedMemory": true + }, + "tasks": [ + { + "title": "Design", + "description": "Produce a short spec for the feature.", + "assignee": "designer" + }, + { + "title": "Implement", + "description": "Implement from the design.", + "assignee": "builder", + "dependsOn": ["Design"] + } + ] +} +``` + +- **`dependsOn`** — Task titles (not internal ids), same convention as the coordinator output in the library. +- Optional per-task fields: `memoryScope` (`"dependencies"` \| `"all"`), `maxRetries`, `retryDelayMs`, `retryBackoff`. +- **`tasks`** must be a non-empty array; each item needs string `title` and `description`. + +If **`--team path.json`** is passed, the file’s top-level `team` property is ignored and the external file is used instead (useful when the same team definition is shared across several pipeline files). + +### Orchestrator and coordinator JSON + +These files are arbitrary JSON objects merged into **`OrchestratorConfig`** and **`CoordinatorConfig`**. Function-valued options (`onProgress`, `onApproval`, etc.) cannot appear in JSON and are not supported by the CLI. + +--- + +## Output + +### Stdout + +Every invocation prints **one JSON document** to stdout, followed by a newline. + +**Successful `run` / `task`** + +```json +{ + "command": "run", + "success": true, + "totalTokenUsage": { "input_tokens": 0, "output_tokens": 0 }, + "agentResults": { + "architect": { + "success": true, + "output": "…", + "tokenUsage": { "input_tokens": 0, "output_tokens": 0 }, + "toolCalls": [], + "structured": null, + "loopDetected": false, + "budgetExceeded": false + } + } +} +``` + +`agentResults` keys are agent names. When an agent ran multiple tasks, the library merges results; the CLI mirrors the merged `AgentRunResult` fields. + +**Errors (usage, validation, I/O, runtime)** + +```json +{ + "error": { + "kind": "usage", + "message": "--goal and --team are required" + } +} +``` + +`kind` is one of: `usage`, `validation`, `io`, `runtime`, or `internal` (uncaught errors in the outer handler). + +### Output flags + +| Flag | Effect | +|------|--------| +| `--pretty` | Pretty-print JSON with indentation. | +| `--include-messages` | Include each agent’s full `messages` array in `agentResults`. **Very large** for long runs; default is omit. | + +There is no separate progress stream; for rich telemetry use the TypeScript API with `onProgress` / `onTrace`. + +--- + +## Exit codes + +| Code | Meaning | +|------|---------| +| **0** | Success: `run`/`task` finished with `success === true`, or help / `provider` completed normally. | +| **1** | Run finished but **`success === false`** (agent or task failure as reported by the library). | +| **2** | Usage, validation, readable JSON errors, or file access issues (e.g. missing file). | +| **3** | Unexpected error, including typical LLM/API failures surfaced as thrown errors. | + +In scripts: + +```bash +npx oma run --goal "Summarize README" --team team.json > result.json +code=$? +case $code in + 0) echo "OK" ;; + 1) echo "Run reported failure — inspect result.json" ;; + 2) echo "Bad inputs or files" ;; + 3) echo "Crash or API error" ;; +esac +``` + +--- + +## Argument parsing + +- Long options only: `--goal`, `--team`, `--file`, etc. +- Values may be attached with `=`: `--team=./team.json`. +- Boolean-style flags (`--pretty`, `--include-messages`) take no value; if the next token does not start with `--`, it is treated as the value of the previous option (standard `getopt`-style pairing). + +--- + +## Limitations (by design) + +- No TTY session, history, or `stdin` goal input. +- No built-in **`cwd`** or metadata passed into `ToolUseContext` (tools use process cwd unless the library adds other hooks later). +- No **`onApproval`** from JSON; non-interactive batch only. +- Coordinator **`runTeam`** path still requires network and API keys like any other run. +