Colleague is a swappable coder-agent harness that turns different model backends into repo workers behind one shared task runtime.
One runtime, many minds.
The problem it solves. AI coder backends are not interchangeable — a local vLLM model, an OpenAI-compatible endpoint, and a hosted assistant each bring their own SDK, controls, and output shape. Colleague is the harness around the model: you point it at a repo task, pick a backend, and it runs the work through that backend's bounded tool-loop and hands back the same typed result every time. Swap the backend and nothing else about your workflow changes — the caller never has to care which mind ran the task.
No model and no network required — the mock backend runs the full loop
deterministically, so you can see the shape of a task before wiring up a real
model:
uv sync
uv run pytest -n auto # full suite, offline
# Run your first task through the deterministic mock backend:
uv run colleague work "add a CONTRIBUTING.md stub" --repo . --engine mock --no-pr
# Open the interactive cockpit (the session palette) at a terminal:
uv run colleagueTo drive a real model (a local vLLM server, or any OpenAI-compatible endpoint), see Running a real model (vLLM) below.
- You want to swap coder backends without changing your workflow. The same
workcommand and the sameTaskResultcome back whether a local 27B or a cloud endpoint ran it. - You want a different, independent mind on a task — a second opinion on a
diff, a fresh read of an unfamiliar area, or a small change taken off your
plate. The first-party
ask-colleagueskill is built for exactly this: diversity (a second mind), not a stronger model, is the point. - You want reproducible, auditable AI coding tasks. Every run writes a JSON result artifact with a full step trace, always-on stats, and opt-in OpenTelemetry traces — so a task's cost and behavior are inspectable after the fact.
- You want operator controls around an autonomous loop — saved command templates, lifecycle hooks, and an approval gate that gates what the harness may execute.
Colleague is not a chat assistant and not an IDE plugin — it is a headless task runtime. For an interactive pair-programmer, use Claude Code or Codex; reach for colleague when you want a backend-agnostic worker you can script, gate, and audit.
| Command | What it does |
|---|---|
colleague work "<goal>" --repo . |
Run a task autonomously through a backend. |
colleague (no args, at a terminal) |
Open the interactive cockpit / session palette. |
colleague doctor --probe |
Check the install and ping the model endpoint. |
colleague learn |
Print a prompt that teaches an agent how to drive colleague. |
colleague learn-from claude |
Absorb another agent's skills into colleague's own (writes files; add --dry-run to preview). |
learnandlearn-fromare different commands — see Two senses of "learn".
| Part | In Colleague |
|---|---|
| Mind / backend | the model/coder backend (a local vLLM model, an OpenAI-compatible endpoint, …) |
| Adapter | the code that invokes and controls one backend (colleague/engines/) |
| Task runtime | the shared task contract + lifecycle (Task → TaskResult) |
| Tool loop | the bounded agentic loop the backend works the repo through |
| Plugins | replaceable backend adapters, discovered via Python entry points |
| Run report | the JSON result artifact + step trace each run writes |
| Telemetry | opt-in OpenTelemetry traces + metrics (colleague/telemetry/) |
| Handoff | branch/commit/push + gh pr create, gated for offline/CI (colleague/handoff.py) |
| Doctor | colleague doctor — read-only configuration-readiness health check (colleague/oilcheck/) |
| Registry | colleague backends list — the backend plugins installed in this environment |
| Approval gate | colleague/policy.py — operator-declared .colleague/approvals.json that controls what the harness executes |
Colleague has graduated from v0 to v1 (see CHANGELOG.md for
the per-version history and pyproject.toml for the current
release). The runtime ships:
- A shared task contract — a typed
TaskandTaskResultthat every backend consumes and produces identically. - A bounded agentic tool-loop — the backend calls
read_file,write_file,edit_file(partial-edit; cost scales with the change, not the file),list_dir,run_command,culture(AgentCulture CLIs), andfinish, confined to the target repo, until it finishes or hits the step budget. - Two backends, both registered through the same
colleague.enginesentry-point group an out-of-tree plugin would use:mock— deterministic and networkless; the CI workhorse.vllm-openai— drives any OpenAI-compatible/v1/chat/completionsendpoint with tool calling. The built-in default model issakamakismile/Qwen3.6-27B-Text-NVFP4-MTP(an NVFP4 Qwen3 checkpoint on a vLLM server — whatdoctorchecks for); any tool-calling model works.
- Git/PR handoff — branch → commit → push →
gh pr create, gated so--no-pr(or no remote) stays a local commit and CI never pushes. - A result artifact (
.colleague/<task-id>.json) for handoff back to Guildmaster / Taskmaster / Steward. - Command templates — reusable, parameterized task recipes stored under
.colleague/commands/*.md, invoked withwork --command <name> [args…]or selected in the interactive palette. - Lifecycle hooks — operator-authored shell commands that fire at
task_start,pre_tool,post_tool, andfinishevents; apre_toolhook can allow, deny, or rewrite tool calls before the backend executes them. A per-model hooks overlay (.colleague/<model>/hooks.json) layers model-specific fixes ahead of the base hooks, giving the operator a precision tool for recurring model biases — applied only for the targeted model, a strict no-op for all others; no new runtime dep, socket, or daemon. - Interactive palette —
colleague sessionis the agent-native entry point: type a free-text goal and it routes toworkorplanon colleague's own backend automatically — no subcommand needed. Template numbers and names run directly;/opens slash commands. Override the session backend viaCOLLEAGUE_SESSION_ENGINEwithout touching the globalCOLLEAGUE_ENGINE. - Layered per-model config — AGENTS instructions
(
AGENTS.md→AGENTS.colleague.md→AGENTS.colleague.<model>.md) and skills (.colleague/skills/*.md→.colleague/<model>/skills/*.md) compose into a model-specific system prompt, with strict per-model isolation; inspect them withcolleague agents list/colleague skills list. - Telemetry: OpenTelemetry observability — opt-in traces + metrics over OTLP,
emitted identically by every backend; off by default and a strict no-op, with
the SDK as an optional
[otel]extra so the base install stays dep-free. doctor— a read-only configuration-readiness health check across identity, provider, engines, otel-readiness, and environment; emits a rubric-shaped report and exits non-zero when unhealthy.- Mesh-member integration — a work item resolves a process-level identity (the
repo's
culture.yamlnick or.colleague/identity.json) and propagates it to subcommands viaCOLLEAGUE_IDENTITY. The loop exposes one curatedculturetool (allow-list:agtag,devex) that shells out to the operator-installed CLIs with the identity injected. Operators opt into read-only ephemeral neighbour clones via.colleague/neighbours.json(defaults to empty; no new runtime dependency). - Destination — colleague's sibling to telemetry. When a task warrants it, a
backend can set a curated
devagueloop tool to open and converge a goal-frame before driving the repo, and declare the announcement on arrival. The destination (frame slug + announcement) is recorded in the JSON artifact; the curated allow-list excludesconfirm/reject(user-only) andexport(operator-only), and convergence is advisory — only human-confirmed claims are authoritative. Setting a destination is optional and backend-judged. - Approval gate — an operator-declared
.colleague/approvals.jsonthat gates what the harness executes. Approval is tamper-protection, not just a name list:approverecords a file's content checksum; if the file changes later the approval is void. Three categories are gated, each opt-in by presence of its section:run_commandCLIs by program token (allow/deny lists), lifecycle hook scripts by checksum, and command templates by checksum. Skills and AGENTS instructions load freely — they are never gated. See the Approval gate section below for the full config shape and usage. - Startup banner —
colleague workandcolleague sessiongreet an interactive terminal with an ASCII banner. It's decorative chrome: written to stderr, shown only on a TTY, and suppressed under--json, so it never pollutes the stdout result stream or agent-parsed output.
Out of scope (by design — each needs a re-spec before it lands): a
multi-backend router / routing policy, an execution
sandbox, a daemon mode, Codex/Claude/Gemini adapters, a --no-hooks escape hatch
(there is no such flag; the approval gate is the landed hook-trust increment —
a policy gate, not a sandbox), and a live MCP runtime (no mcp.json, no mcp
verb; the curated culture tool shells out to operator CLIs — no socket, no MCP
transport). The runtime package has no third-party dependencies — the vLLM
adapter speaks the OpenAI wire format over the standard library.
Each shipped feature has a focused page under docs/features/
— start at the feature index. For the per-version
list of what shipped (and when), see CHANGELOG.md.
| Feature | Doc |
|---|---|
| Work & the tool-loop | work-and-loop.md |
| Context budget / graceful degradation | graceful-degradation.md |
| Capacity standard / fill-line decision (v1) | spec |
| Backends & plugins | engines.md |
| Model & endpoint selection | model-selection.md |
| Git/PR handoff | handoff.md |
| Result artifact | artifact.md |
| Command templates | command-templates.md |
| Lifecycle hooks | hooks.md |
| Interactive palette | session.md |
| Cockpit views (tui / TAUI) | tui.md |
| Layered per-model config | layered-config.md |
| Learn skills from a peer | learn-from.md |
| Telemetry: OpenTelemetry | telemetry.md |
| Work stats & feedback (ROI) | stats-and-feedback.md |
doctor (health check) |
doctor.md |
| Agent-first CLI | agent-cli.md |
| Mesh-member integration | mesh-member.md |
| Resident promote (Culture member) | resident-promote.md |
| Destination | destination.md |
| Subagents | subagents.md |
| Parallel subagents | parallel-subagents.md |
| Auto-split (too-large assignment) | auto-split.md |
| Audit fan-out | audit-fanout.md |
| Per-model configuration | per-model-configuration.md |
| Approval gate | See Approval gate section below |
| Ask colleague (a different mind) | ask-colleague.md |
| Escalation (agtag continuation) | escalation.md |
| Memory (eidetic recall/remember) | memory.md |
Background one-shot (work --background) |
background.md |
| Resident appserver (mesh work requests) | resident.md |
| Deepthink (dual-model escalation) | deepthink.md |
| Mode profiles | mode-profiles.md |
| Adaptive backpressure | backpressure.md |
| Tier visibility | tier-visibility.md |
| Skill curation (budget-aware) | skill-curation.md |
| Rig budget (cross-process slots) | rig-budget.md |
| Task goals & acceptance | task-goals.md |
Session modes (shift-tab / /mode) |
session-modes.md |
| Plan mode | plan-mode.md |
| Subagent roles (typed workforce) | subagent-roles.md |
| Write isolation (throwaway worktrees) | write-isolation.md |
| Lint pre-finish gate | lint-gate.md |
| Test-integrity gate | test-integrity.md |
| Affected-tests gate | affected-tests.md |
| Capacity standard / fill-line | capacity-standard.md |
| Explore never wastes a run | explore-never-wastes.md |
| Continue-working (stall recovery) | continue-working.md |
| Flight (pilot a run mid-air) | flight.md |
Cleanup / reap (clean) |
cleanup-reap.md |
Config resolution (.colleague/config.json) |
config-resolution.md |
| Approval gate (detail) | approval-gate.md |
| CLI on agentfront (rendered surface) | cli-on-agentfront.md |
| ask-colleague trustworthy reflex | ask-colleague-trustworthy-reflex.md |
The detailed sections below remain the canonical reference; the feature pages add per-feature source pointers and cross-links.
Before this layer, colleague work accepted one raw instruction string
and ran the tool-loop with no operator gate and no saved recipes: run_command
and write_file executed unconditionally, and every task had to be typed from
scratch.
After, operators drop files into .colleague/ and gain three things that
work identically across every backend (the all-engines rule):
- Command templates — author a recipe once, invoke it by name with
positional arguments;
work --command <name> [args…]expands it into the sameTaskshape a rawwork "…"produces. - Lifecycle hooks —
pre_toolhooks can allow, deny (reason fed back to the model), or rewrite tool arguments before they execute;post_toolhooks run formatters or linters after;task_startandfinishhooks bracket the whole work item. Every firing is recorded in the result artifact. - Interactive palette —
colleague sessionlists discovered templates, accepts a selection (by number or name) plus optional arguments, and runs the chosen task through the same work path, loop, hooks, and artifact — no parallel code path.
This extensibility lives in the runtime (colleague/loop.py), not in any one
backend, so it binds equally to mock, vllm-openai, and any future plugin.
To go beyond the deterministic mock backend, point colleague at a real
OpenAI-compatible model server. (Discover what's installed first with
uv run colleague backends list.)
Start an OpenAI-compatible vLLM server with tool calling enabled:
vllm serve Qwen/Qwen3-32B \
--port 8001 \
--enable-auto-tool-choice \
--tool-call-parser hermesThe right --tool-call-parser depends on the model and the vLLM build:
hermes works for many models (including Qwen/Qwen3-32B above), while other
builds need a different one — e.g. an NVFP4 Qwen3 checkpoint served via vLLM may
want qwen3_coder. The backend itself is parser-agnostic — any parser that makes
the server emit OpenAI-format tool calls works.
Tip (anecdotal). With an NVFP4 Qwen3 checkpoint,
qwen3_coderhandled tool-argument escaping more reliably thanhermesin our testing: ahermesrun over-escaped the triple-quotes in a generated docstring (writing\"\"\"instead of"""), producing aSyntaxError, whereqwen3_coderwrote the same file cleanly. This is a single observation, not a benchmark — but if a parser garbles quote-heavy edits, trying the other one is worth a shot.
Then point Colleague at it (defaults already target localhost:8001):
uv run colleague work "fix the typo in the README title" \
--repo /path/to/target/repo \
--engine vllm-openai \
--base-url http://localhost:8001/v1 \
--model Qwen/Qwen3-32BConfiguration resolves in the order: explicit flag → COLLEAGUE_* env →
OPENAI_* env → default. The built-in default --model is
sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP; the example above overrides it with
--model to match the server you started. Because the adapter only touches the
OpenAI surface, pointing --base-url at any compatible server (llama.cpp, an
OpenAI proxy) needs no code change.
The opt-in live end-to-end test proves this against a real server:
COLLEAGUE_VLLM_E2E=1 uv run pytest tests/test_vllm_live.py -vOperators save reusable task recipes as Markdown files under
.colleague/commands/<name>.md (repo-level or ~/.colleague/commands/ for
user-level; repo-level shadows user-level by stem).
.colleague/commands/ is committable — it is the one part of the otherwise
gitignored .colleague/ dir that git tracks, so a team can share recipes in-repo
(run artifacts, hooks.json, and approvals.json stay local). The committed
doc-review recipe is a worked example.
A template may open with an optional --- metadata block:
---
description: Fix lint errors under a path
engine: mock
constraints: keep diffs minimal, run the formatter
arg-hint: <path>
---
Fix all lint errors under $1. Then run the formatter. $ARGUMENTSSupported metadata keys:
| Key | Meaning |
|---|---|
description |
One-line description shown in listings |
engine |
Engine to use when running this command (overridden by --engine) |
constraints |
Comma-separated constraints added to the Task |
arg-hint |
Short argument hint shown in commands list |
If no --- block is present, the entire file content is the body.
| Placeholder | Expands to |
|---|---|
$ARGUMENTS |
All arguments joined by a space |
$1, $2, … |
The N-th positional argument (empty string if not supplied) |
# One-shot via work:
uv run colleague work --command fix-lint src/ --repo /path/to/repo --engine mock --no-pr
# List all discovered templates:
uv run colleague commands list --repo .
# Surface overview:
uv run colleague commands overviewThe --command flag and a positional instruction are mutually exclusive; any
tokens after --command <name> are passed as template arguments ($1, $2,
$ARGUMENTS).
Hooks are operator-authored shell commands registered in
.colleague/hooks.json (repo-level or ~/.colleague/hooks.json for
user-level; repo-level wins).
{
"hooks": {
"pre_tool": [{ "matcher": "run_command", "command": "my-policy-gate.sh" }],
"post_tool": [{ "matcher": "write_file", "command": "black $file 2>/dev/null; true" }],
"task_start":[{ "command": "echo task starting" }],
"finish": [{ "command": "echo done" }]
}
}Each entry has:
| Field | Meaning |
|---|---|
matcher |
Regex (re.fullmatch) tested against the tool name. Absent or empty matches every tool. Ignored for task_start / finish events. |
command |
Shell command run in the target repo directory. |
| Event | When it fires | Pre/post effect |
|---|---|---|
task_start |
Before the first tool call | Observe only |
pre_tool |
Before each tool call | Can allow, deny, or rewrite |
post_tool |
After each tool call | Observe only (side-effects OK) |
finish |
After the loop ends | Observe only |
The hook receives a JSON payload on stdin:
{
"event": "pre_tool",
"tool": "run_command",
"arguments": { "command": "pytest" },
"task_id": "<uuid>",
"repo_path": "/path/to/repo"
}The hook signals its decision via exit code and optional structured stdout:
| Exit code | Stdout | Decision |
|---|---|---|
| non-zero | any | deny — stderr (fallback: stdout) is fed back to the model as the tool result |
| 0 | empty or non-JSON | allow — tool runs as-is |
| 0 | {"decision":"allow", ...} |
allow |
| 0 | {"decision":"deny", "reason":"..."} |
deny — reason fed back to model |
| 0 | {"decision":"rewrite","arguments":{...}} |
rewrite — tool runs with the supplied replacement arguments |
Any response may carry an "additionalContext" string. Every firing (event,
matched command, decision, exit code) is recorded in TaskResult.hook_firings
and appears in the result artifact JSON.
post_tool, task_start, and finish hooks are observe-only: a deny from
these events is recorded but does not halt the loop.
uv run colleague hooks list --repo .
uv run colleague hooks overviewcolleague session is the agent-native entry point to colleague: type a
free-text goal and it routes automatically to work or plan on colleague's
own backend — no subcommand needed.
uv run colleague session --repo /path/to/repo
# or, relying on COLLEAGUE_SESSION_ENGINE / COLLEAGUE_ENGINE:
colleague session --repo /path/to/repoAgent-native by default (#234). Free-text input is classified by
colleague/session_intent.py (classify_intent) and routed to work (the
default) or plan without naming a subcommand. A → work: / → plan: line
confirms the dispatch. Numbers and template names run a work template directly
and are never reclassified. The session runs on colleague's own served backend:
precedence is --engine > COLLEAGUE_SESSION_ENGINE (a session-only env-var
override, new) > COLLEAGUE_ENGINE > vllm-openai. To point the conversational
session at a different backend than bare colleague work, set
COLLEAGUE_SESSION_ENGINE — without touching COLLEAGUE_ENGINE.
Legible action feed (#233). Consecutive identical feed entries collapse into
<entry> ×N instead of stacking (e.g. four back-to-back culture calls become
[culture] agtag issues ×4); the culture/devague tools now surface as
<cli> <args> rather than a bare [culture] sentinel; and step hints are capped
at 120 characters (up from 48).
AgentFront probe reflex (#235). Before the first real use of an unfamiliar
CLI in a run, the backend is instructed to check its learn/explain/--help
surface first. This is advisory and read-only — it never installs or approves
anything.
Input is line-based. Plain text runs a work item — a number (palette
entry), a template name, or a free-text instruction (intent-routed to
work or plan). A line starting with / is a slash command:
- Introspection (surface an existing noun in the cockpit):
/help,/commands,/skills,/agents,/config(thedoctorreadiness view),/engines,/telemetry,/feedback. - Live config:
/engine <name>,/model <name>,/base <branch>,/pr(toggle push + PR) — change the session without restarting it. /quit(orq/ empty line) ends the session.
Three render tiers of the one state, chosen automatically:
- Interactive (a colour TTY) — the dynamic ANSI cockpit: redraw-in-place, and
popups on real events (an
errorpopup when a work item step fails). - Non-interactive (piped / captured) — Markdown menus (the static but
full agent-readable view), the default off a TTY.
--no-tuiforces it on a TTY. --json— stdout carries only the work itemTaskResult(one JSON object each, preserving the machine contract); the cockpit renders to stderr as chrome.
Running colleague with no arguments at a terminal opens this same cockpit
(backend resolved as above: --engine > COLLEAGUE_SESSION_ENGINE >
COLLEAGUE_ENGINE > vllm-openai, never a silent mock). By default it is a
"talk + iterate" loop — each work item commits locally but does not push or
open a PR; /pr or --pr opts in.
colleague tui exposes a headless, stdlib-only cockpit in three views of one
CockpitState: a JSON/TAUI mirror (the agent-readable, selector-addressed
source of truth — tui state), an ANSI frame (the visual render — tui render,
the default), and a Markdown view (the agent-facing readable render —
tui render --format markdown). TAUI (Textual Agentic UI) lets an agent read and
operate the UI without screen-scraping, an LLM, or any colleague import.
The cockpit is a pure reducer (event → reduce(state, event) → CockpitState) and
its state carries a popup model (skill_suggestion / confirmation / error /
progress / diff / help). tui snapshot captures a moment; tui diagnose
classifies cross-view disagreements (no LLM, no network); tui live opens the
foreground TTY cockpit.
uv run colleague tui state # the TAUI JSON mirror
uv run colleague tui render --state <file> # the ANSI frame
uv run colleague tui render --format markdown --state <file>
uv run colleague tui overviewWatching a live work item. A real work feeds the cockpit (#74):
uv run colleague work "<task>" --engine mock # auto: live cockpit on a TTY
uv run colleague work "<task>" --engine mock --no-tui # force the plain step lines
uv run colleague work "<task>" --engine mock --tui-events run.jsonl # live event stream
uv run colleague tui replay --trace .colleague/<id>.trace.jsonl # replay a finished work item- Live cockpit (A1) — on an interactive terminal a work item renders the cockpit
as it runs (conversation per step, popups on real events — e.g. an
errorpopup when a tool step fails). Auto-on a TTY;--tui/--no-tuiforce it. Off a TTY (pipes/agents/CI) it falls back to the plainstep N: <tool> [ok|err]stderr lines, byte-for-byte unchanged. - Live event stream (A3) —
--tui-events <path>appends oneWorkStepJSONL line per step as the work item runs, so an agent can follow it turn-by-turn ortui replayit. (A stream written into the driven repo is treated as harness telemetry — never swept into the work branch.) - Replay a real work item (A4) —
tui replay --trace <id>.trace.jsonlfolds a finished work item's loop-step trace into the cockpit (live and replayed steps read identically — one shared converter). - Interactive cockpit (A2) —
colleague sessionis now cockpit-rendered with slash commands; see Interactive cockpit (session).
A mid-loop failure still writes a partial artifact (status=error) with the
steps, usage, and changed files accumulated so far.
See docs/features/tui.md for the full surface.
A work item can emit OpenTelemetry traces + metrics so it's observable against an OTLP collector — not just the per-run JSON artifact. Telemetry lives in the runtime (the loop + the shared work path), so every backend emits it identically, exactly like lifecycle hooks.
It is off by default and a strict no-op when off (no spans, no SDK import, the result artifact unchanged). The OpenTelemetry SDK is an optional extra — the base install keeps zero runtime dependencies:
pip install 'colleague[otel]' # or: uv sync --extra otel
export COLLEAGUE_OTEL_ENABLED=1
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # OTLP/HTTP collector
uv run colleague work "<task>" --repo . --engine mock --no-pr
# -> stderr prints "trace: <id>"; the collector receives the spans + metricsRequested without the extra installed, colleague degrades to a no-op with a one-line stderr notice — it never fails the work item.
Signals. Spans: colleague.work (root) → colleague.tool.* (per tool
call) → colleague.handoff. Metrics: colleague.steps, colleague.tokens,
colleague.tool.latency, colleague.tool.calls, colleague.hook.denials,
colleague.work.duration.
Config (precedence: explicit > COLLEAGUE_OTEL_* > standard OTEL_* >
default): COLLEAGUE_OTEL_ENABLED, COLLEAGUE_OTEL_ENDPOINT /
OTEL_EXPORTER_OTLP_ENDPOINT, COLLEAGUE_OTEL_SERVICE_NAME /
OTEL_SERVICE_NAME. OTEL_SDK_DISABLED=true is honored as a kill-switch.
uv run colleague telemetry status # resolved config + whether the SDK is installed
uv run colleague telemetry overview # describe the surfaceTogether these let a caller compute the ROI of outsourcing a task: the stats say what a work item cost, a feedback record says how good it was.
Always-on stats. Every TaskResult carries a stats block
(WorkStats), written into the artifact on every work item — no flag, no opt-in.
It records the request, ISO start + wall-clock duration, model turns, step count,
per-tool counts, files changed, exact UTF-8 bytes_written, and
reasoning-vs-answer char/byte sizes. Exact token counts stay on usage, verbatim
from the model response (never estimated). Like hooks and telemetry, stats are
runtime-owned — identical for mock and vllm-openai.
Honest token limit. Colleague has no tokenizer (zero deps), and the served model reports no reasoning-token breakdown — so "thought vs written" is measured as chars/bytes, not tokens.
Feedback. A single record per work item (re-grading overwrites) lives beside the
artifact at .colleague/<task_id>.feedback.json; a per-repo last_work pointer
lets you grade the most recent work item without quoting its id. An ungraded work item
reads back as a clean "no feedback yet" state, never an error.
uv run colleague feedback record last --rating 4 --notes "correct but verbose"
uv run colleague feedback show last --repo .
uv run colleague feedback overviewThe agent-facing entry is the ask-colleague feedback skill verb. See
docs/features/stats-and-feedback.md.
Before you hand colleague work, colleague doctor answers "is this install
actually ready to work?" It is colleague's read-only, diagnose-only
health check (no --fix, zero new runtime deps) that emits a
rubric-shaped {healthy, checks[]} report across five ordered check-groups:
| Group | Checks (severity) |
|---|---|
| identity | prompt_file_present / backend_consistency (error), skills_present (warning) |
| provider | resolved base_url/model with redacted api_key (info); credentials + budget advisories (warning) on a non-default provider |
| engines | backends discovered + both bundled backends present + each plugin loads (error; all-engines rule) |
| otel | telemetry enabled / SDK importable / endpoint configured (info; error only when enabled but the [otel] extra is missing) |
| environment | .colleague/ config, hooks.json validity, command-template parsing, AGENTS/skills layering, git (error) + gh (warning) on PATH, CLI integrity |
Only a failed error check flips the report unhealthy; warnings and info are
advisory. doctor exits 1 when unhealthy, else 0. The diagnostic logic lives
in the runtime-level colleague/oilcheck/ package (like telemetry); the verb
is a thin renderer. Add a check-group by appending a read-only checks() callable
to CHECK_GROUPS — see colleague explain doctor and
docs/features/doctor.md.
uv run colleague doctor # human-readable rubric; exit 1 if unhealthy
uv run colleague doctor --json # structured {healthy, checks[]}
uv run colleague doctor --probe # + a live provider ping (the one networked check)--probe adds two opt-in checks that open a network connection — provider_reachable
(can colleague reach the endpoint?) and provider_model_available (is the
configured model actually served at that endpoint?) — gated behind the flag so the
default doctor stays network-free.
Colleague has two verbs with "learn" in the name. They do different things — this is the one place to keep them straight:
| Command | Direction | What it does | Who it's for |
|---|---|---|---|
colleague learn |
colleague → reader | Prints a structured self-teaching prompt (purpose, command map, exit codes, --json, the explain pointer). Read-only — nothing is written. |
An AI agent onboarding into a repo that needs to drive colleague. (Humans usually want colleague explain <path> or colleague overview instead.) |
colleague learn-from <source> |
peer → colleague | Colleague absorbs another agent's skills: it reads e.g. Claude Code's .claude/skills/<name>/SKILL.md, adapts each into colleague's own .colleague/skills/<name>.md, and (optionally) has the backend rewrite Claude-isms for colleague's tool surface. Writes files. |
An operator growing colleague's skill set from a peer mind. |
Mnemonic: learn teaches someone else how to use colleague; learn-from
teaches colleague by absorbing a peer's skills. See
docs/features/learn-from.md for the full
two-stage flow (a deterministic copy, then an optional LLM review-and-adapt
pass), the safety rules, and the honest "load, not execute" limit.
uv run colleague learn # self-teaching prompt (read-only)
uv run colleague learn-from claude --repo . # absorb Claude's skills (writes .colleague/skills/)
uv run colleague learn-from claude --copy-only --dry-run # preview the copy, write nothingColleague composes a model-specific system prompt for every work item from two layered families, resolved relative to the model currently driving. Strict per-model isolation: driving model X reads only X's overlay plus the shared base — it never even opens model Y's files (isolation is structural, built from exact paths, not filtered).
AGENTS instructions cascade from the repo root (the cross-tool standard
location — sibling agent tools read AGENTS.md there too), general → specific,
with a ~/.colleague/ user-level fallback:
AGENTS.md # shared base
AGENTS.colleague.md # colleague overlay
AGENTS.colleague.<model>.md # model overlay
Skills are markdown capability docs under .colleague/, folded into the
prompt as a compact name + one-line-summary catalog (a skill is instructional
text only — colleague loads skills as instructions, it does not execute them):
.colleague/skills/*.md # base
.colleague/<model>/skills/*.md # model overlay (shadows base by stem)
<model> is sanitized to a filename-safe token (e.g. Qwen/Qwen3-32B →
Qwen-Qwen3-32B). Inspect what resolves for a model:
uv run colleague agents list --model Qwen/Qwen3-32B --repo .
uv run colleague skills list --model Qwen/Qwen3-32B --repo .MCP layering is not built yet. Colleague does not read
mcp.jsonor connect to any MCP server today; a live MCP client needs its own spec. There is nomcpverb — don't rely on a non-existent surface.
Mid-work, a backend may delegate scoped sub-tasks via two loop tools:
subagent (a single child) or subagents (a batch that runs concurrently). Each
child runs the same bounded tool-loop as a nested in-process call, isolated in
its own throwaway git worktree on a sub/<id> branch; its result is returned to
the parent and folded into TaskResult.sub_results (omitted when empty). A
sequential merge-subagent integrates the branches afterward, surfacing (never
force-merging) unresolvable conflicts. An optional engine/model parameter lets
a child run on a different backend or model, resolved through the existing
registry.load + EngineConfig inheritance (a config-level switch, no backend
code change).
Concurrency is opt-in: COLLEAGUE_SUBAGENT_CONCURRENCY (default 1 =
byte-identical sequential behavior); with width > 1, up to
MIN(width, MAX_SUBAGENT_FANOUT-1) children run in parallel via
concurrent.futures (threads confined to colleague/subagents.py), reserving one
slot for the merge child. Delegation is backend-judged and optional (like the
devague destination tool), never a forced gate. Termination is structural:
MAX_SUBAGENT_DEPTH=2 (checked before any child work) and MAX_SUBAGENT_FANOUT=4
(per-work-item, including the merge child). Only the top-level work item hands off —
sub-drives never branch, commit, or open a PR. This is runtime-owned (the tools
fire identically for every backend) and is explicitly not the out-of-scope
multi-backend router / routing policy: there is no automatic task→backend routing.
Honest limit: real wall-clock speedup requires the served model to handle concurrent requests; on a serializing server, gain is bounded by overlapped I/O wait, not model compute.
uv run colleague explain subagent # the loop tool's contract (not a CLI verb)ask-colleague is colleague's one first-party Claude Code skill — the inverse
of the vendored skills. It lets another agent hand a scoped task to colleague: a
different backend/model (e.g. a local vLLM Qwen), not a stronger one — diversity
is the point. Four verbs over colleague work:
| Verb | What it does |
|---|---|
ask-colleague explore |
Read-only investigation of an area (worktree-isolated). |
ask-colleague review |
A diverse second opinion on the committed <base>...HEAD diff (the headline verb). |
ask-colleague write |
Delegate a small change — previews by default; --apply lands a work branch, --pr opens a PR. |
ask-colleague feedback |
Grade a finished work item (close the ROI loop). |
explore/review run in a throwaway git worktree (no working-tree side effects);
write previews in one too unless --apply/--pr. (Renamed from outsource;
"outsource this" still triggers it.) See
docs/features/ask-colleague.md.
The approval gate is an operator-declared allow-list that controls what the
harness executes — not just what it discovers. Approval is tamper-protection:
approve records the file's current content checksum; if the file changes after
approval, the checksum no longer matches and the approval is void.
{
"run_command": { "allow": ["git", "pytest", "uv"], "deny": [] },
"hooks": { ".colleague/lint.sh": "sha256:<hex>" },
"commands": { "fix-lint": "sha256:<hex>" }
}Place this file at .colleague/approvals.json in the target repo (or
~/.colleague/approvals.json for user-level defaults; repo-level wins). A
per-model overlay at .colleague/<sanitized-model>/approvals.json is composed
ahead — per-model keys replace base keys for the same section; no sibling model
is ever read.
| Category | Gated by | Absent section |
|---|---|---|
run_command |
Program token (shlex first token): allow/deny lists |
No-op (all commands allowed) |
hooks |
Content checksum of the referenced hook script file | No-op (all hooks run) |
commands |
Content checksum of the template .md file (checked at expansion) |
No-op (all templates expand) |
| Skills / AGENTS | Never gated — declarative, load freely | — |
A section is gated only when it is present in approvals.json. An absent
section is a strict no-op: byte-identical to behavior before the gate existed.
When a section is present, allow-list semantics apply: anything unlisted,
unapproved, or tampered is denied.
# Approve a command template by checksum (default: sha256):
uv run colleague commands approve fix-lint --repo .
# Approve a hook script by repo-relative path:
uv run colleague hooks approve .colleague/lint.sh --repo .
# Use md5 instead (drift detection; not recommended for integrity):
uv run colleague commands approve fix-lint --repo . --algo md5
# Both commands support --json for machine-readable output.# commands list shows: approved | drifted | unapproved | ungated
uv run colleague commands list --repo .
# hooks list shows approval status per entry + the run_command policy if present
uv run colleague hooks list --repo .
# skills list always shows: accessible (never gated)
uv run colleague skills list --repo .Status values:
| Status | Meaning |
|---|---|
approved |
Entry present, checksum matches current file content |
drifted |
Entry present, but file changed since approval — approval void |
unapproved |
Section present but no entry for this name |
ungated |
Section absent from approvals.json — gate not active |
accessible |
Skills / AGENTS — never gated, always accessible |
This is a policy gate, not a sandbox.
- The
run_commandcheck inspects the first shell token only. It is trivially bypassable bysh -c '...', shell pipelines, command substitution, shell expansion, or an absolute path to a renamed binary. The gate encodes operator intent; it does not contain a hostile process. An airtight execution sandbox is explicitly out of scope (it would need its own re-spec). md5detects accidental drift (file edited by mistake), not a deliberate attacker who can recompute a hash. Usesha256when integrity matters.- Checksum-only (current release). There is no
version-based pinning. Approvals are recorded and verified by content hash only. Version pinning is a documented follow-up that is not yet built — do not rely on it. - This is the landed increment of the tracked "per-repo hook trust gate" from
the security section below. There is still no
--no-hooksflag — that remains a future follow-up.
This is a code-execution risk. Read before driving an untrusted repo.
When you run colleague work (or colleague session) against a repo that
contains a .colleague/hooks.json, those hooks execute automatically with
your operating-system privileges. There is no confirmation prompt and no
sandboxing. Cloning a malicious repository and pointing Colleague at it will
run whatever shell commands that repository's hooks.json specifies.
This behavior is intentional under Colleague's trusted-operator-env model
(D2): the same design tradeoff Claude Code and Codex make for their .claude/
and .codex/ hook configs. You are expected to trust (or audit) the repos you
work in.
What is implemented: the approval gate lets you gate hook
scripts by checksum — an unapproved or tampered hook script is skipped (not a
hard deny of the tool call; it fires a skipped firing in the artifact). The
run_command allow/deny list gates which CLI programs the loop may invoke.
What is NOT yet implemented: a --no-hooks escape hatch or any other
mechanism to disable repo-shipped hooks without editing .colleague/hooks.json
yourself. The approval gate is a policy gate, not a sandbox — see its honest
limits above. A further hardening increment is tracked but has not shipped
in the current version. Do not rely on a non-existent flag.
Safe practices until the trust gate ships:
- Only run colleague on repos you own or have audited.
- Review
.colleague/hooks.jsonbefore runningworkin an unfamiliar repo. - Use user-level (
~/.colleague/hooks.json) hooks as an allow-list approach if you want hooks without trusting any repo's config.
| Verb | What it does |
|---|---|
work <goal> |
Work toward a goal/instruction: work autonomously through a coder backend; write the artifact; hand off. |
work --command <name> [args…] |
Expand a saved command template and run it. |
commands list |
List discovered command templates for a repo (shows approval status). |
commands approve <name> |
Record a checksum approval for a command template. |
commands overview |
Describe the commands surface. |
hooks list |
List configured hook entries for a repo (shows approval status + run_command policy). |
hooks approve <script> |
Record a checksum approval for a hook script file (repo-relative path). |
hooks overview |
Describe the hooks surface. |
agents list |
List resolved AGENTS instruction layers for a model. |
agents overview |
Describe the agents surface. |
skills list |
List resolved skill docs for a model. |
skills overview |
Describe the skills surface. |
telemetry status |
Show the resolved telemetry / OpenTelemetry config + whether the SDK is installed. |
telemetry overview |
Describe the telemetry surface. |
session |
Open a foreground interactive palette. |
backends list |
List discovered backend plugins (the registry; wheels is a deprecated alias). |
whoami |
Report nick, version, mesh backend, and the live work engine + model (the delegate an ask-colleague would actually run). |
learn |
Print a structured self-teaching prompt — teaches an agent how to drive colleague (read-only; nothing is written). |
learn-from <source> |
Absorb a peer agent's skills into colleague's own .colleague/skills/ (e.g. learn-from claude); writes files. Not to be confused with learn — see Two senses of "learn". |
explain <path> |
Markdown docs for any noun/verb path. |
overview |
Read-only descriptive snapshot of the agent. |
doctor |
Configuration-readiness health check: identity, provider, engines, otel, environment. |
cli overview |
Describe the CLI surface itself. |
Every command supports --json. Results go to stdout, errors/diagnostics to
stderr (never mixed). Exit codes: 0 success, 1 user error, 2 environment
error, 3+ reserved.
A backend is a class implementing colleague.engine.Engine (one method:
work(task, config) -> TaskResult). Advertise it under the entry-point group
and colleague backends list discovers it — no change to Colleague core:
[project.entry-points."colleague.engines"]
my-engine = "my_package.engine:MyEngine"Most backends never re-implement the loop — they delegate to
colleague.loop.run and only supply how the model is called. Because the
loop owns hook firing, a custom backend inherits the full lifecycle extensibility
layer for free.
Apache 2.0 — see LICENSE.