Skip to content

Plugins

Coxswain's core repository names no vendor. Anything that talks to a particular tracker, forge, model vendor or hosted service is a plugin: a Python package that registers under an entry-point group, which core looks up by name. The public coxswain-plugins repository holds the ones Coxswain ships.

The groups

Group Loaded by Chosen by Built in
coxswain.sources cox route pull --source <name>, configured under the profile's sources.<name>; a bare pull reads every configured source github (issues labelled intake and PRs labelled review, through gh) and folder
coxswain.forges cox runs land the profile's forge: key local (plain git, the default), github, forgejo, and auto (each repository's forge, from its origin)
coxswain.trackers cox route sync the profile's tracker: key none (the default) and github-projects
coxswain.system_one the harness's fast path the provider profile's system_one.backend model-tier
coxswain.runners the harness the provider profile's runner: key claude-code and anthropic
coxswain.storage the harness's trace store the URL's scheme file and s3 (built in)

A name that is neither built in nor registered is refused with a line naming the group, except in system one: an unregistered backend turns system one off for that run, and the run goes on without it.

Built-in sources

Both run locally: github through your own gh login, folder on the filesystem. Name them in the routing profile, and map each listed repo or folder to the repository its tickets target:

sources: {"github": {"repos": ["owner/repo"]}, "folder": {"repos": ["~/intake"]}}
repo_map: {"owner/repo": "/path/to/repo", "~/intake": "/path/to/repo"}
  • github files each issue labelled intake (filter: label:<name> picks another label), then swaps the label for intake:taken and comments the intake path. An open PR labelled review is reviewed instead, and relabelled review:taken.
  • folder files each *.md directly in the folder: its first # heading is the title (else the file name), the rest the body. A filed file moves into the folder's done/, so the folder holds only what is waiting. The same name with new text files again.

The autonomous chair runs a bare cox route pull when it has free lanes and nothing else queued, so it reads every configured source.

Installing

cox loads sources, forges and trackers from its own environment. cox is installed editable from the coxswain checkout, so add the plugins to that install:

uv tool install --force --editable ~/.local/share/coxswain/installs/coxswain \
  --with "coxswain-plugins @ git+https://github.com/ppfenning/coxswain-plugins"

The harness loads system-one backends and runners from the checkout's venv:

uv pip install --python ~/.local/share/coxswain/installs/coxswain/.venv/bin/python \
  "coxswain-plugins[jev] @ git+https://github.com/ppfenning/coxswain-plugins"

cox upgrade reinstalls cox with only the extras it had, which drops the --with package, so run the first command again after an upgrade. Its venv sync keeps packages the lock does not name, so the second survives.

A plugin that needs a vendor's client library declares it as an extra, as jev does. Installing one plugin never pulls in another's dependencies. cox setup doctor lists what each side can load in its plugins row.

Writing one

Register the entry point in your package's pyproject.toml:

[project.entry-points."coxswain.forges"]
myforge = "my_package.forge"

Each group expects a different shape:

  • Source (a module): candidates(config, listing) -> tuple[Ref, ...], read(raw) -> Candidate, taken(link, intake_links) -> bool and mark_argv(ref, intake_path) -> list[str]. The profile's sources.<name> block gives repos, filter and token_env. An adapter reads a tracker and files intake tickets. It never moves the tracker's state.
  • Forge (a module): find_open_prs(repo, branch), push(repo, branch), open_pr(repo, title, body, *, head=None, base=None), wait_checks(repo, timeout_s, *, ref="HEAD") and merge(repo, step). Each except the first returns (ok, detail). merge updates the local default branch without checking anything out.
  • Tracker (a module): sync(workspace, items, *, dry_run) -> (ok, lines). cox route sync reads the work store's items, applies --item if given, and hands them over. items are the work store's initiatives and tasks: each has an id, title, body, repo, state, phase, run, cost, gate, issue and parent. Return ok and the lines to print, and write nothing when dry_run is true. An exception is reported as the tracker failing, and route sync exits 1.
  • System-one backend (a callable): make(model, api_key, block) returns a decider whose decide(question, state) returns an answer with a confidence. Set make.needs_key = True for a hosted service. Its key then comes only from the environment variable named in system_one.key_env, never from the provider's own key. Set make.wants_runner = True to be handed the run's runner as runner=. A backend runs in shadow until cox stats system-one says it has graduated.
  • Runner (a callable): factory(profile, *, role_skills, workdir, repo) returns a runner whose run(*, role, tier, schema, prompt, ...) returns the node's structured result.