Cliq SDK
Every phase in a cliq pipeline executes through an agent — a lightweight process that reads context from .cliq/, does work (via a CLI tool, an LLM API, or a human review server), writes results back, and exits. The agent handles all protocol mechanics; the implementation does the creative work.
The implementation never touches
.cliq/— only the agent wrapper does.
For most users, the built-in agents are all you need. If you want a phase powered by a different LLM or custom logic, the @cliqhub/cliq-sdk package lets you build your own agent in under 50 lines.
Built-in Agents
Section titled “Built-in Agents”Cliq ships five built-in agents. They are always available and need no configuration.
| Name | Binary | Description |
|---|---|---|
cursor | agent | Cursor IDE agent (default) |
claude-code | claude | Anthropic Claude Code |
gemini | gemini | Google Gemini CLI |
codex | codex | OpenAI Codex CLI |
hug | — | Human-in-the-loop review gate |
Install hints
Section titled “Install hints”| Agent | Install |
|---|---|
| Cursor | Install Cursor desktop app (includes the agent CLI) |
| Claude Code | npm install -g @anthropic-ai/claude-code |
| Gemini | npm install -g @anthropic-ai/gemini-cli or see Gemini CLI docs |
| Codex | npm install -g @openai/codex |
Run cliq doctor to check which agent binaries are available on your system.
Choosing an Agent
Section titled “Choosing an Agent”Global default
Section titled “Global default”Set the default agent for all projects in ~/.cliqrc/settings.json:
{ "agents": { "default": "claude-code" }}If omitted, the default is cursor.
Per-phase override
Section titled “Per-phase override”Any phase in team.yml can specify a different agent:
workflow: phases: - name: research type: standard agent: gemini
- name: implementation type: standard # uses the default (cursor or whatever agents.default is set to)CLI flag
Section titled “CLI flag”Override the default for a single run:
cliq run --agent claude-codeThis sets agents.default to claude-code for this invocation only. Phases with an explicit agent in team.yml are not affected.
Quick reference
Section titled “Quick reference”| Method | Scope | Where | Example |
|---|---|---|---|
| Global default | All projects, all phases | ~/.cliqrc/settings.json | "agents": { "default": "gemini" } |
| Per-phase | Single phase in one team | team.yml | phases: [{ name: research, agent: gemini }] |
| CLI flag | Single run, all default phases | Command line | cliq run --agent claude-code |
Custom Agents
Section titled “Custom Agents”Teams can bundle custom agent implementations — for example, an OpenAI-powered analyst or a specialized gate. Custom agents are declared in the agents section of team.yml:
agents: openai-analyst: entry: agents/analyst/index.js env: - OPENAI_API_KEY| Field | Type | Required | Description |
|---|---|---|---|
entry | string | yes | Path to the agent entry point, relative to the team directory |
env | string[] | no | Environment variable names the agent requires |
Built-in agents (cursor, claude-code, gemini, codex, hug) don’t need declarations — they’re available by name.
Users can also define or override agents without modifying a team, via ~/.cliqrc/settings.json:
{ "agents": { "custom": { "my-reviewer": { "entry": "/absolute/path/to/my_reviewer.js", "env": ["OPENAI_API_KEY"] } } }}See Settings — agents for the full reference.
Install the SDK
Section titled “Install the SDK”npm install @cliqhub/cliq-sdkRequires Node.js 18+.
If you’re adding an agent to a team package:
mkdir -p agents/my-agentcd agents/my-agentnpm init -ynpm install @cliqhub/cliq-sdkWhen to build a custom agent
Section titled “When to build a custom agent”Use the SDK when:
- You want a phase powered by a different LLM API (OpenAI, Anthropic, Google, Mistral, etc.)
- You need custom logic beyond what a CLI agent provides (multi-step reasoning, tool orchestration, database access, external API calls)
- You want to wrap an existing service or internal tool as a pipeline phase
- You need a custom gate with domain-specific review logic
For simply switching between supported CLI agents (Cursor, Claude Code, Gemini, Codex), use the agent field in team.yml — no SDK needed.
Quick Start — Standard Phase Agent
Section titled “Quick Start — Standard Phase Agent”A standard phase agent does work and produces output for downstream phases.
import { StandardPhaseAgent } from '@cliqhub/cliq-sdk';
new StandardPhaseAgent({ name: 'openai-analyst',
execute: async (ctx) => { ctx.progress('Starting analysis...');
const response = await call_your_llm(ctx.role, ctx.inputs);
for (const target of ctx.targets) { ctx.send(target, response.text); }
},}).run();Declare it in team.yml:
name: "@local/research-team"
agents: openai-analyst: entry: agents/analyst/index.ts env: - OPENAI_API_KEY
workflow: phases: - name: research type: standard
- name: analysis type: standard agent: openai-analyst depends_on: [research]
- name: review type: gate depends_on: [analysis]Run:
cliq assemble research-teamcliq runThe orchestrator writes a dispatch file, spawns your agent in a tmux pane, and the SDK handles everything else — reading context, calling your execute, writing channels and signals.
Quick Start — Gate Phase Agent
Section titled “Quick Start — Gate Phase Agent”A gate agent reviews work and renders a verdict: pass, route back, or escalate.
import { GatePhaseAgent } from '@cliqhub/cliq-sdk';
new GatePhaseAgent({ name: 'strict-reviewer',
execute: async (ctx) => { const all_pass = ctx.command_results.every(r => r.pass);
if (!all_pass) { const failed = ctx.command_results .filter(r => !r.pass) .map(r => r.name);
return { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: `Checks failed: ${failed.join(', ')}`, }; }
const review = await call_llm_for_review(ctx.role, ctx.inputs);
if (review.approved) { return { verdict: { outcome: 'PASS' }, comment: review.reason, }; }
return { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: review.reason, }; },}).run();Gate verdicts are type-safe discriminated unions:
| Verdict | Shape | Meaning |
|---|---|---|
| Pass | { outcome: 'PASS' } | All good — pipeline continues forward |
| Route | { outcome: 'ROUTE', target: '<phase>' } | Send back to a specific phase for rework |
| Escalate | { outcome: 'ESCALATE' } | Requires human intervention — pipeline pauses |
The SDK validates ROUTE targets at runtime. If the target is not in ctx.route_targets, the agent fails with a clear error.
The Execute Function
Section titled “The Execute Function”The execute function is the core of your agent. It receives a context object and returns a result. The SDK assembles the context from .cliq/ protocol files before calling your function, and writes all results back to .cliq/ after it returns.
PhaseContext (standard phases)
Section titled “PhaseContext (standard phases)”| Field | Type | Description |
|---|---|---|
phase | string | Phase name (e.g. "analysis") |
role | string | Full role brief from .cliq/roles/<phase>.md |
inputs | Record<string, string> | Input channel contents from predecessor phases |
req_spec | string | Requirements specification from .cliq/req_spec.md |
task_board | TaskBoard | Structured task board interface (see TaskBoard API below) |
design_docs | Record<string, string> | Design documents, keyed by filename |
settings | object | Resolved settings from .cliq/resolved_settings.json |
targets | string[] | Downstream phase names this phase can send handoff notes to |
send(target, message) | function | Send a handoff message to a downstream phase. Writes to disk immediately |
project_dir | string | Absolute path to the project root |
metadata.instance_id | string | Pipeline run instance ID |
metadata.team_name | string | Team package name |
metadata.req_key | string | Requirement key (e.g. Jira ticket) |
progress(msg) | function | Log a status message (visible in the tmux pane) |
GateContext (gate phases)
Section titled “GateContext (gate phases)”Gate agents receive everything in PhaseContext plus:
| Field | Type | Description |
|---|---|---|
command_results | CommandResult[] | Results of automated commands (name, pass, output, exit_code) |
route_targets | string[] | Valid phases this gate can route work back to |
review | ReviewBlock | undefined | Review configuration (present only on hug phases): reviewer, artifacts, timeout, remind_every |
Standard phase return value
Section titled “Standard phase return value”Standard phase agents return void. All output is written imperatively during execution:
- Channels —
ctx.send(target, message)writes handoff content to disk immediately - Task board —
ctx.task_board.mark_complete(),.add_note(), etc.
This means partially completed work survives agent crashes — whatever was sent before the failure is preserved on disk.
GateResult (gate phases)
Section titled “GateResult (gate phases)”| Field | Type | Required | Description |
|---|---|---|---|
verdict | VerdictOutcome | yes | { outcome: 'PASS' }, { outcome: 'ROUTE', target: '...' }, or { outcome: 'ESCALATE' } |
comment | string | yes | Explanation for the verdict |
reviewer | string | no | Who made the decision |
decided_at | string | no | ISO timestamp of the decision |
Complete Examples
Section titled “Complete Examples”OpenAI single-shot agent
Section titled “OpenAI single-shot agent”import { StandardPhaseAgent } from '@cliqhub/cliq-sdk';import OpenAI from 'openai';
const client = new OpenAI();
new StandardPhaseAgent({ name: 'openai-researcher',
execute: async (ctx) => { const input_text = Object.values(ctx.inputs).join('\n\n---\n\n');
const completion = await client.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: ctx.role }, { role: 'user', content: input_text || ctx.req_spec }, ], });
const content = completion.choices[0].message.content ?? '';
for (const target of ctx.targets) { ctx.send(target, content); } },}).run();Multi-turn agent with tool use
Section titled “Multi-turn agent with tool use”import { StandardPhaseAgent } from '@cliqhub/cliq-sdk';
new StandardPhaseAgent({ name: 'tool-agent',
execute: async (ctx) => { let context = build_initial_context(ctx.role, ctx.inputs); let iterations = 0;
while (iterations < 10) { ctx.progress(`Iteration ${iterations + 1}...`);
const response = await call_llm(context);
if (response.tool_calls) { const results = await execute_tools(response.tool_calls); context = append_tool_results(context, results); iterations++; continue; }
for (const target of ctx.targets) { ctx.send(target, response.text); }
return; }
},}).run();Anthropic gate agent
Section titled “Anthropic gate agent”import { GatePhaseAgent } from '@cliqhub/cliq-sdk';import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
new GatePhaseAgent({ name: 'claude-reviewer',
execute: async (ctx) => { const failed_commands = ctx.command_results.filter(r => !r.pass); if (failed_commands.length > 0) { return { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: `${failed_commands.length} command(s) failed: ${failed_commands.map(c => c.name).join(', ')}`, }; }
const artifacts = Object.values(ctx.inputs).join('\n\n---\n\n');
const msg = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 2048, system: ctx.role, messages: [{ role: 'user', content: `Review the following work:\n\n${artifacts}\n\nRespond with PASS if the work meets all requirements, or ROUTE if it needs changes. Explain your reasoning.`, }], });
const text = msg.content[0].type === 'text' ? msg.content[0].text : ''; const is_pass = text.toUpperCase().includes('PASS');
if (is_pass) { return { verdict: { outcome: 'PASS' }, comment: text, decided_at: new Date().toISOString(), }; }
return { verdict: { outcome: 'ROUTE', target: ctx.route_targets[0] }, comment: text, decided_at: new Date().toISOString(), }; },}).run();Agent that updates the task board
Section titled “Agent that updates the task board”The ctx.task_board object exposes structured methods — agents never
manipulate the markdown format directly.
import { StandardPhaseAgent } from '@cliqhub/cliq-sdk';
new StandardPhaseAgent({ name: 'task-tracker',
execute: async (ctx) => { ctx.progress('Working on tasks...');
const result = await do_work(ctx);
ctx.task_board.mark_complete(ctx.phase); ctx.task_board.add_note(ctx.phase, `Completed: ${result.summary}`);
for (const target of ctx.targets) { ctx.send(target, result.text); }
},}).run();See TaskBoard API for the full interface.
Environment Variables
Section titled “Environment Variables”Declare required environment variables in team.yml:
agents: openai-analyst: entry: agents/analyst/index.js env: - OPENAI_API_KEYThese must be set in the user’s shell before cliq run. The env field documents requirements but does not set values.
Shared environment variables for all agents can be set in settings:
{ "agents": { "env": { "OPENAI_API_KEY": "sk-..." } }}Testing Your Agent
Section titled “Testing Your Agent”Unit testing with vitest
Section titled “Unit testing with vitest”The SDK classes are designed for testability. Use execute_lifecycle(phase_name) in tests instead of run() (which parses process.argv and calls process.exit).
import { describe, it, expect, beforeEach, afterEach } from 'vitest';import fs from 'node:fs';import path from 'node:path';import os from 'node:os';import { StandardPhaseAgent } from '@cliqhub/cliq-sdk';
describe('my analyst agent', () => { let project_dir: string;
beforeEach(() => { project_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-')); process.env.CLIQ_PROJECT_DIR = project_dir;
const cliq = (...p: string[]) => path.join(project_dir, '.cliq', ...p); fs.mkdirSync(cliq('signals'), { recursive: true }); fs.mkdirSync(cliq('roles'), { recursive: true }); fs.mkdirSync(cliq('channels'), { recursive: true });
fs.writeFileSync(cliq('roles', 'analyst.md'), 'You are an analyst.');
fs.writeFileSync(cliq('signals', 'analyst_dispatch.json'), JSON.stringify({ phase: 'analyst', mode: 'phase', project_dir, instance_id: 'test-1', team_name: 'test', req_key: 'TEST-1', output_channels: ['analyst--reviewer'], })); });
afterEach(() => { fs.rmSync(project_dir, { recursive: true, force: true }); delete process.env.CLIQ_PROJECT_DIR; });
it('produces output on the expected channel', async () => { const agent = new StandardPhaseAgent({ name: 'openai-analyst', execute: async (ctx) => ({ output: { 'analyst--reviewer': 'Analysis results here.' }, summary: 'Done.', }), });
await agent.execute_lifecycle('analyst');
const cliq = (...p: string[]) => path.join(project_dir, '.cliq', ...p); const handoff = fs.readFileSync( cliq('channels', 'analyst--reviewer', 'handoff.md'), 'utf8', ); expect(handoff).toBe('Analysis results here.');
expect(fs.existsSync(cliq('signals', 'analyst_done'))).toBe(true); });});Manual testing
Section titled “Manual testing”Test your agent against a real .cliq/ structure:
# 1. Set up minimal protocol filesmkdir -p .cliq/signals .cliq/roles .cliq/channels/researcher--analyst .cliq/designecho "You are a research analyst." > .cliq/roles/analyst.mdecho "Research findings." > .cliq/channels/researcher--analyst/handoff.mdecho "Analyze the data." > .cliq/req_spec.mdecho '{}' > .cliq/resolved_settings.json
# 2. Write a dispatch filecat > .cliq/signals/analyst_dispatch.json << 'EOF'{ "phase": "analyst", "mode": "phase", "project_dir": "/path/to/your/project", "instance_id": "test-1", "team_name": "test", "req_key": "TEST-1", "output_channels": ["analyst--reviewer"]}EOF
# 3. Run the agentnode agents/analyst/index.js analyst
# 4. Verify resultscat .cliq/channels/analyst--reviewer/handoff.mdtest -f .cliq/signals/analyst_done && echo "Done signal written"API Reference
Section titled “API Reference”StandardPhaseAgent
Section titled “StandardPhaseAgent”import { StandardPhaseAgent } from '@cliqhub/cliq-sdk';
new StandardPhaseAgent({ name: string, execute: (ctx: PhaseContext) => Promise<void>,}).run();Creates a standard phase agent. Call .run() to start the CLI lifecycle (reads phase name from process.argv[2], runs, exits). Use .execute_lifecycle(phase_name) in tests.
GatePhaseAgent
Section titled “GatePhaseAgent”import { GatePhaseAgent } from '@cliqhub/cliq-sdk';
new GatePhaseAgent({ name: string, execute: (ctx: GateContext) => Promise<GateResult>,}).run();Creates a gate phase agent. Same lifecycle as StandardPhaseAgent, but writes a _verdict signal instead of _done.
VerdictOutcome
Section titled “VerdictOutcome”type VerdictOutcome = | { outcome: 'PASS' } | { outcome: 'ROUTE'; target: string } | { outcome: 'ESCALATE' };Type-safe discriminated union for gate verdicts. The SDK validates ROUTE targets against ctx.route_targets at runtime.
TaskBoard API
Section titled “TaskBoard API”The ctx.task_board object provides structured methods for updating the task board.
Agents never manipulate the internal markdown format directly — the SDK handles
serialization and only writes .cliq/task_board.md when mutations have been made.
| Method / Property | Description |
|---|---|
mark_complete(phase) | Toggle - [ ] phase → - [x] phase. No-op if already complete or not found. |
mark_incomplete(phase) | Toggle - [x] phase → - [ ] phase. No-op if already incomplete or not found. |
add_note(phase, note) | Insert a note line below the matching phase entry. No-op if phase not found. |
replace(content) | Replace the entire board with new content (for LLM-generated rewrites). |
content | Read-only string — current board content (pass to an LLM as context). |
dirty | Read-only boolean — true if any mutation has been made. |
execute: async (ctx) => { ctx.task_board.mark_complete(ctx.phase); ctx.task_board.add_note(ctx.phase, 'OAuth2 PKCE flow implemented');
for (const target of ctx.targets) { ctx.send(target, 'OAuth2 PKCE flow implemented.'); }},The SDK writes the task board to disk automatically after execute() returns — only when ctx.task_board.dirty is true.
Lifecycle
Section titled “Lifecycle”When .run() is called, the SDK:
- Reads the phase name from
process.argv[2] - Reads the dispatch file from
.cliq/signals/<phase>_dispatch.json - Reads all protocol files: role, channels, req_spec, task_board, design docs, settings
- Constructs the typed context (
PhaseContextorGateContext) with aTaskBoardinstance - Calls your
execute(ctx)function — channel writes happen immediately viactx.send()during execution - Writes
.cliq/task_board.mdifctx.task_boardwas mutated (dirty === true) - Standard phases: writes
.cliq/signals/<phase>_done - Gate phases: writes
.cliq/signals/<phase>_verdict(JSON) - Exits with code 0
No long-running server, no HTTP, no polling. The process starts, does its work, writes results, and exits.
Error handling
Section titled “Error handling”If your execute function throws, the SDK:
- Writes the error message to
.cliq/signals/<phase>_error - Does not write
_doneor_verdict - Exits with code 1
The orchestrator detects the missing signal and escalates. You do not need to wrap your execute() in a try/catch for protocol safety. However, you may want your own try/catch for retries or graceful degradation.
Directory Structure
Section titled “Directory Structure”A team with a custom agent:
my-team/├── team.yml├── roles/│ ├── research.md│ └── analysis.md├── agents/│ └── analyst/│ ├── index.ts│ └── package.json└── README.mdAfter cliq assemble:
.cliq/├── workflow.yml├── agents.json # generated manifest├── roles/│ ├── research.md│ └── analysis.md├── agents/│ └── analyst/│ ├── index.ts│ └── package.json├── channels/ # created at runtime└── signals/ # created at runtimeDiagnostics
Section titled “Diagnostics”cliq doctor checks agent binaries based on your configuration:
- With a project (
.cliq/exists): reads the agent manifest and checks every built-in CLI agent’s underlying binary - Without a project: checks only your default agent from
~/.cliqrc/settings.json
Checking dependencies...
tmux... ✓ (tmux 3.3a) node... ✓ (v20.11.0) git... ✓ (git version 2.43.0)
Agents: agent... ✓ (Cursor 0.45.0)Configuration Reference
Section titled “Configuration Reference”| Field | Type | Default | Description |
|---|---|---|---|
agents.default | string | "cursor" | Default agent for phases without an explicit agent key |
agents.custom | Record<string, AgentConfig> | {} | User-defined agent configurations, keyed by name |
agents.env | Record<string, string> | {} | Environment variables injected into every agent process |
See Settings for the full settings reference.
Debugging
Section titled “Debugging”Agent process exits with code 1 — Your execute() threw. Check .cliq/signals/{phase}_error for the message. Common causes: missing API key, network timeout, undefined variable.
Empty or missing channels — If you don’t call ctx.send() for each target, downstream phases won’t find the expected handoff files. Use ctx.targets to see which phases expect output. Calling ctx.send() with an invalid target throws immediately.
Dispatch file not found — The agent can’t find .cliq/signals/{phase}_dispatch.json. Check that the orchestrator wrote it (see orchestrator logs) and that CLIQ_PROJECT_DIR or the working directory is correct.
ROUTE target validation error — The GatePhaseAgent validates that ROUTE targets exist in ctx.route_targets. If your gate returns { outcome: 'ROUTE', target: 'nonexistent' }, the SDK throws and writes an error signal. Check your workflow’s depends_on graph — route targets are derived from predecessor phases.
Missing environment variable — If your custom agent requires an API key, set it in your shell before running cliq run. The env field in team.yml documents which variables are needed but does not set them.
export OPENAI_API_KEY=sk-...cliq runAgent binary not found — A built-in agent’s underlying CLI binary must be on your PATH. Run cliq doctor to see which are missing and get install hints.