Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ CLAUDE_IMG ?= claude-code:local
CLAUDE_CTR ?= claude-$(PROJECT_NAME)
GROK_IMG ?= grok-build:local
GROK_CTR ?= grok-$(PROJECT_NAME)
CODEX_IMG ?= codex-cli:local
CODEX_CTR ?= codex-$(PROJECT_NAME)

BROKER_IMG ?= swarmforge-docker-broker:latest

Expand All @@ -25,6 +27,9 @@ CLAUDE_ARGS ?=
GROK_DATA_DIR ?= $(HOME)/.local/share/grok
GROK_HOME_DIR ?= $(GROK_DATA_DIR)/home
GROK_ARGS ?=
CODEX_DATA_DIR ?= $(HOME)/.local/share/codex
CODEX_HOME_DIR ?= $(CODEX_DATA_DIR)/home
CODEX_ARGS ?=
# Stable per-repo mount path knobs, shared by every persistent-home harness.
SWARMFORGE_REPO_SLUG ?=
SWARMFORGE_REMOTE_NAME ?= origin
Expand Down Expand Up @@ -169,7 +174,17 @@ GROK_RUN_MOUNTS = \
--tmpfs $(ANVIL_HOME)/.grok/commands \
$(SWARMFORGE_LAYER_MOUNTS)

.PHONY: opencode_network build_opencode update_opencode build_broker build_claude update_claude build_grok update_grok run_opencode stop_opencode run_claude stop_claude run_grok stop_grok run_ollama logs_ollama stop_ollama gpu_stat clean \
CODEX_RUN_ENV = \
-e SWARMFORGE_AGENT_BIN=codex \
$(SWARMFORGE_LAYER_ENV)

# Codex's native skills dir is ~/.agents/skills, masked for the reason above.
CODEX_RUN_MOUNTS = \
-v "$(CODEX_HOME_DIR)":$(ANVIL_HOME) \
--tmpfs $(ANVIL_HOME)/.agents/skills:exec \
$(SWARMFORGE_LAYER_MOUNTS)

.PHONY: opencode_network build_opencode update_opencode build_broker build_claude update_claude build_grok update_grok build_codex update_codex run_opencode stop_opencode run_claude stop_claude run_grok stop_grok run_codex stop_codex run_ollama logs_ollama stop_ollama gpu_stat clean \
run_llama_3-1-8b run_gpt-oss-20b run_gpt-oss-120b run_devstral2_small test test-skills lint

# The workspace is mounted read-write, but the paths inside its git dir that
Expand Down Expand Up @@ -317,6 +332,19 @@ build_grok:
update_grok:
$(MAKE) build_grok SWARMFORGE_HARNESS_INSTALL_BUST=$(shell date +%s)

build_codex:
docker build \
--target codex-runtime \
--build-arg AGENT=codex \
--build-arg DEBIAN_TAG=$(DEBIAN_TAG) \
--build-arg SWARMFORGE_HARNESS_INSTALL_BUST=$(SWARMFORGE_HARNESS_INSTALL_BUST) \
-f "$(SWARMFORGE_DIR)/anvil/Dockerfile" \
-t $(CODEX_IMG) "$(SWARMFORGE_DIR)"

# Rebuild only from the Codex install step onward.
update_codex:
$(MAKE) build_codex SWARMFORGE_HARNESS_INSTALL_BUST=$(shell date +%s)

run_opencode: SWARMFORGE_USER_CONFIG_DIR ?= $(HOME)/.config/opencode
run_opencode: SWARMFORGE_ORG_CONFIG_DIR ?= $(if $(strip $(SWARMFORGE_ORG_CONFIG_ROOT)),$(SWARMFORGE_ORG_CONFIG_ROOT)/.opencode,)
run_opencode: SWARMFORGE_REPO_CONFIG_DIR ?= $(OPENCODE_CONFIG_DIR)
Expand Down Expand Up @@ -365,6 +393,23 @@ run_grok: opencode_network
stop_grok:
@docker rm -f $(GROK_CTR) >/dev/null 2>&1 || true

run_codex: SWARMFORGE_USER_CONFIG_DIR ?= $(HOME)/.codex
run_codex: SWARMFORGE_ORG_CONFIG_DIR ?= $(if $(strip $(SWARMFORGE_ORG_CONFIG_ROOT)),$(SWARMFORGE_ORG_CONFIG_ROOT)/.codex,)
run_codex: SWARMFORGE_REPO_CONFIG_DIR ?= $(SWARMFORGE_DIR)/codex
run_codex: SWARMFORGE_CONFIG_RESET ?= 0
run_codex: opencode_network
@mkdir -p "$(CODEX_HOME_DIR)"
@mkdir -p "$(SWARMFORGE_USER_CONFIG_DIR)"
@mkdir -p "$(CODEX_HOME_DIR)/.swarmforge"
@mkdir -p "$(CODEX_HOME_DIR)/.swarmforge/skills"
@mkdir -p "$(CODEX_HOME_DIR)/.swarmforge/command"
@mkdir -p "$(CODEX_HOME_DIR)/.agents/skills"
@mkdir -p "$(CODEX_HOME_DIR)/.codex"
$(call run_agent_container,$(CODEX_CTR),$(CODEX_RUN_ENV),$(CODEX_RUN_MOUNTS),$(CODEX_IMG),$(CODEX_ARGS),repo-slug,codex)

stop_codex:
@docker rm -f $(CODEX_CTR) >/dev/null 2>&1 || true

run_ollama: opencode_network
@docker rm -f $(OLLAMA_CTR) >/dev/null 2>&1 || true
docker run -d --rm --name $(OLLAMA_CTR) \
Expand All @@ -386,7 +431,7 @@ stop_ollama:
gpu_stat:
nvidia-smi

clean: stop_opencode stop_claude stop_grok stop_ollama
clean: stop_opencode stop_claude stop_grok stop_codex stop_ollama
@docker network rm $(NETWORK) >/dev/null 2>&1 || true

run_llama_3-1-8b:
Expand Down
62 changes: 52 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Override the target file with `OC_RC_FILE=/path/to/rc bash ./install.sh`.
make build_opencode
make build_claude
make build_grok
make build_codex
```

To pin OpenCode to a specific release instead of latest:
Expand All @@ -34,13 +35,14 @@ make update_opencode OPENCODE_VERSION=1.4.14
```

The images share the same Debian base and toolchain (Node.js + Python; see `anvil/Dockerfile`).
Build targets pass `AGENT=opencode|claude|grok` so only the requested agent install step runs.
Build targets pass `AGENT=opencode|claude|grok|codex` so only the requested agent install step runs.

3. Run from your project directory:

- OpenCode: `oc`
- Claude Code: `make run_claude PROJECT_DIR=$(pwd)`
- Grok Build: `make run_grok PROJECT_DIR=$(pwd)`
- Codex CLI: `make run_codex PROJECT_DIR=$(pwd)`
- Pass OpenCode overrides as arguments (`oc PROFILE=work DATA_DIR=...`) or env vars (`PROFILE=work oc`).
- Override the container timezone per run (affects git commit timestamps): `oc TIMEZONE=America/New_York`.

Expand All @@ -60,10 +62,10 @@ alias ccd='make -C PATH_TO_SWARMFORGE run_claude PROJECT_DIR=$(pwd) CLAUDE_DATA_

- `GITCONFIG_FILE` points at an agent-specific git config instead of `~/.gitconfig`.
- For Claude Code, use separate `CLAUDE_DATA_DIR` roots to isolate work/personal logins and session state. `CLAUDE_HOME_DIR` defaults to `$(CLAUDE_DATA_DIR)/home`.
- Config layering uses `SWARMFORGE_USER_CONFIG_DIR`, `SWARMFORGE_ORG_CONFIG_DIR`, and `SWARMFORGE_REPO_CONFIG_DIR` (their defaults differ per harness — see OpenCode layering under [Skills](#skills) and [Claude config layering](#claude-config-layering)). Set `SWARMFORGE_ORG_CONFIG_ROOT=/path/to/org-repo` to resolve org defaults to `.opencode` (OpenCode) and `.claude` (Claude) under that root.
- Config layering uses `SWARMFORGE_USER_CONFIG_DIR`, `SWARMFORGE_ORG_CONFIG_DIR`, and `SWARMFORGE_REPO_CONFIG_DIR` (their defaults differ per harness — see OpenCode layering under [Skills](#skills) and [Claude config layering](#claude-config-layering)). Set `SWARMFORGE_ORG_CONFIG_ROOT=/path/to/org-repo` to resolve org defaults to each harness's own directory under that root (`.opencode`, `.claude`, `.grok`, `.codex`).

`SWARMFORGE_REPO_CONFIG_DIR` refers to the Swarmforge checkout (the harness repo), not the working project mounted at `/workspace`.
By default it is `$(SWARMFORGE_DIR)/opencode` for `run_opencode` and `$(SWARMFORGE_DIR)/claude` (if present) for `run_claude`.
By default each `run_*` target points it at that harness's directory in the checkout: `$(SWARMFORGE_DIR)/opencode`, and `$(SWARMFORGE_DIR)/claude`, `/grok`, `/codex` if present.
Project-local config in the working repo (for example `.opencode/`) is still handled by the agent tools themselves.

### Git repos and worktrees
Expand Down Expand Up @@ -108,8 +110,12 @@ The repo is mounted at a stable path derived from the git remote slug (with `/wo
### Shared assets (skills, commands, agents)

Every harness mounts this repo's `skills/` and `commands/` into the container, exported as `SWARMFORGE_SKILLS_DIR` and `SWARMFORGE_COMMAND_DIR`.
The entrypoint copies them into each harness's native location: the container-local config dir for Claude (see [The config directory](#the-config-directory)), the merged config dir for OpenCode (`~/.config/opencode/skills/`) and Grok (`~/.grok/skills/`).
For Claude and Grok those dirs are container-private and rebuilt each run, so per-repo assets never accumulate in the persistent home or leak into other repos' sessions.
The entrypoint copies them into each harness's native location: the container-local config dir for Claude (see [The config directory](#the-config-directory)), the merged config dir for OpenCode (`~/.config/opencode/skills/`) and Grok (`~/.grok/skills/`), and `~/.agents/skills/` for Codex, whose native user location is the `.agents` convention itself.
For Claude, Grok, and Codex those dirs are container-private and rebuilt each run, so per-repo assets never accumulate in the persistent home or leak into other repos' sessions.
Codex has no user-defined slash commands, so portable commands become
same-named skills. Translation removes command-only metadata and adapts
arguments and shell interpolation. A native skill wins over a translated
command in the same layer; normal layer precedence still applies.

Skills, commands, and agents come from four layers, lowest to highest precedence — later layers override same-named entries wholesale (never file-merged):

Expand All @@ -118,7 +124,7 @@ Skills, commands, and agents come from four layers, lowest to highest precedence
- **repo** — this checkout's `skills/`, `commands/`, and `agents/`
- **workspace** — `<workspace>/.agents/{skills,commands}` and `<workspace>/.swarmforge/agents/`

Skills and commands follow the harness-neutral `.agents/{skills,commands}` convention and are copied as-is; agents use the unified format (see [Agents](#agents)) and are translated per harness.
Skills and commands follow the harness-neutral `.agents/{skills,commands}` convention. Skills are copied as-is; commands are copied for harnesses with native commands and translated into skills for Codex. Agents use the unified format (see [Agents](#agents)) and are translated per harness.
Harness-native dirs (`<layer>/.opencode/skills/`, `<layer>/.claude/skills/`) are not consumed for skills/commands.
Override the `.agents` roots with `SWARMFORGE_USER_DOTAGENTS_DIR` / `SWARMFORGE_ORG_DOTAGENTS_DIR`.

Expand Down Expand Up @@ -176,7 +182,7 @@ Grok state persists by mounting `$(GROK_HOME_DIR)` to `/home/anvil`, keeping `~/

Grok reads the repo-root `AGENTS.md` family natively from the git root down, so it picks up this repo's instructions with no extra config.
Shared skills reach `~/.grok/skills/`, Grok's native location, through the [asset pipeline](#shared-assets-skills-commands-agents) above.
Subagent definitions are not translated for Grok; the unified-agent pipeline covers OpenCode and Claude only.
Subagent definitions are not translated for Grok; the unified-agent pipeline covers OpenCode, Claude, and Codex.
MCP tongs reach Grok as `[mcp_servers.<name>]` entries in a managed block of the merged `~/.grok/config.toml` — user-level config, so no folder-trust prompt. That file is in the persistent home, so the block is rewritten every run and stripped when a session has no MCP tongs; a server the user already defines under the same name wins over the generated entry.

Grok config layering uses the same three sources and order of trust as Claude (lowest to highest precedence):
Expand All @@ -186,6 +192,37 @@ Grok config layering uses the same three sources and order of trust as Claude (l

These merge into `~/.grok` in the container at startup, with reset disabled so credentials survive the run. Rebuild only the Grok install layer with `make update_grok`.

## Codex CLI

`make run_codex` starts an [OpenAI Codex CLI](https://developers.openai.com/codex/cli) container with the same workspace, git-worktree, and repo-slug mounting as `make run_claude`.
The image installs the official CLI via `curl -fsSL https://chatgpt.com/codex/install.sh | sh`.
That release is a package rather than a lone binary -- `bin/codex` resolves ripgrep, `bwrap`, and a bundled zsh beside itself -- so it stays whole under `/opt/codex` and the installer's symlink is what lands on `PATH`.
Codex state persists by mounting `$(CODEX_HOME_DIR)` to `/home/anvil`, keeping credentials, sessions, and the project trust levels a stable mount path keeps valid.
`CODEX_HOME_DIR` defaults to `$(CODEX_DATA_DIR)/home`; use separate `CODEX_DATA_DIR` roots to isolate work/personal logins, as with `CLAUDE_DATA_DIR`.

Codex reads the repo-root `AGENTS.md` family natively from the git root down, so it picks up this repo's instructions with no extra config.
Shared skills reach `~/.agents/skills/`, Codex's native user location, through the [asset pipeline](#shared-assets-skills-commands-agents) above. Portable commands reach the same location as translated skills.
Unified subagent definitions become temporary Codex role files under
`/run/swarmforge/codex-agents/` and are registered through the derived
`~/.codex/config.toml`. The checkout's native `.codex/agents/` is untouched.
MCP tongs reach Codex as `[mcp_servers.<name>]` entries in a managed block of the derived `~/.codex/config.toml`, rewritten from the current layers every run and yielding to a server the user already defines under that name.

Codex config layering uses the same three sources and order of trust as Claude (lowest to highest precedence):
- `SWARMFORGE_REPO_CONFIG_DIR` (default `codex/`, if present)
- `SWARMFORGE_USER_CONFIG_DIR` (default `~/.codex`)
- `SWARMFORGE_ORG_CONFIG_DIR` (optional; defaults to `$(SWARMFORGE_ORG_CONFIG_ROOT)/.codex` when that root is set)

The entrypoint builds `config.toml` from scratch in repo → user → org order,
merging by key, then copies it to Codex's native path. The canonical output
preserves values and tables, but not comments or formatting. The native file
remains writable for Codex's atomic settings updates, but the next launch
rebuilds it; put durable settings in a source layer. Rebuild only the Codex
install layer with `make update_codex`.
The merge skips `packages/` -- the host installer's release tree, which the container has no use for -- along with `sessions/`, `history.jsonl`, and `log/`, so one machine's transcripts do not follow the user config layer into the container's home.

Codex brings its own sandbox, which is redundant inside an anvil and may not initialize in one at all, since its Landlock and `bwrap` paths need kernel permissions a container is not guaranteed.
Relax it per run with `CODEX_ARGS='--dangerously-bypass-approvals-and-sandbox'`, or per install by setting `sandbox_mode` in a config layer.

## Agents

Subagent definitions live under `agents/` in a single unified format and are rewritten to each harness's native dialect by the container entrypoint (`swarmforge/agents/translate.py`).
Expand All @@ -204,6 +241,10 @@ tools:
bash: false
claude:
maxTurns: 12
codex:
model: gpt-5.3-codex
model_reasoning_effort: high
sandbox_mode: read-only
---

You are the reviewer agent...
Expand All @@ -215,8 +256,9 @@ Field handling per harness:
- `tools` uses OpenCode's lowercase tool ids mapped to booleans. For Claude Code, disabled tools become `disallowedTools` (`write: false` -> `disallowedTools: Write`); ids with no Claude equivalent are dropped.
- `model` accepts a provider-qualified id (`anthropic/claude-sonnet-4-6`, passed through to OpenCode and stripped to the bare id for Claude — non-Anthropic providers dropped) or a Claude alias (`sonnet`, `haiku`, Claude-only and dropped for OpenCode).
- `mode`, `temperature`, and other OpenCode-only fields are dropped for Claude Code.
- `claude:` / `opencode:` blocks merge verbatim into that harness's output frontmatter.
- `disable: true` passes through to OpenCode and skips the agent for Claude Code.
- For Codex, unqualified models pass through, `openai/` prefixes are stripped, and other providers are dropped. Names and `.toml` filenames are normalized to Codex's supported ASCII characters. Generic `tools` restrictions are dropped; use Codex sandbox and MCP settings instead.
- `claude:`, `codex:`, and `opencode:` blocks merge into that harness's output. Put Codex-only fields such as `model_reasoning_effort` and `sandbox_mode` in `codex:`.
- `disable: true` passes through to OpenCode and skips the agent for Claude Code and Codex.

Unified agents live in harness-neutral `.swarmforge/agents/` directories across the same four layers as shared assets (lowest to highest precedence):

Expand All @@ -225,7 +267,7 @@ Unified agents live in harness-neutral `.swarmforge/agents/` directories across
- **repo** — `agents/` in the checkout (override with `SWARMFORGE_REPO_AGENTS_DIR`, which points directly at an agents dir so the rest of the checkout is never mounted)
- **workspace** — `<workspace>/.swarmforge/agents/`

Layers mount read-only under `/tmp/swarmforge-assets/{user,org}` and `/tmp/swarmforge-assets/repo/agents` (the in-container `SWARMFORGE_ASSETS_{USER,ORG,REPO}_DIR` env vars point at the layer roots); the entrypoint translates the stacked sources into each harness's native location (`~/.config/opencode/agents/` for OpenCode, the container-private `~/.claude/agents/` for Claude). Later layers override earlier ones by filename.
Layers mount read-only under `/tmp/swarmforge-assets/{user,org}` and `/tmp/swarmforge-assets/repo/agents` (the in-container `SWARMFORGE_ASSETS_{USER,ORG,REPO}_DIR` env vars point at the layer roots). The entrypoint translates them into `~/.config/opencode/agents/` for OpenCode, the container-private `~/.claude/agents/` for Claude, and temporary registered role files for Codex. Later layers override earlier ones by filename.
Claude-native repo-local definitions (for example `<workspace>/.claude/agents/`) are still discovered by Claude directly, outside this pipeline.

The translator is covered by the unit suite; run it with `make test`.
Expand Down
7 changes: 7 additions & 0 deletions anvil/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ RUN set -eux; \
install -m 0755 "${grok_bin}" /usr/local/bin/grok; \
rm -rf /root/.grok; \
;; \
codex) \
echo "Installing Codex CLI (cache bust: ${SWARMFORGE_HARNESS_INSTALL_BUST})"; \
export CODEX_HOME=/opt/codex CODEX_INSTALL_DIR=/usr/local/bin; \
curl -fsSL https://chatgpt.com/codex/install.sh | sh; \
;; \
*) \
printf '%s\n' "Unsupported AGENT: ${AGENT}" >&2; \
exit 1; \
Expand All @@ -154,4 +159,6 @@ RUN chmod +x /usr/local/bin/swarmforge-statusline

FROM agent-runtime AS grok-runtime

FROM agent-runtime AS codex-runtime

FROM opencode-runtime AS default
Loading
Loading