diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e93bf98 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + python-version: "3.13" + - run: cd app/cli && uv sync && uv run ruff check . + + test-cli: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + python-version: "3.13" + - run: cd app/cli && uv sync && uv run pytest -v + + test-entrypoint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install ShellSpec + run: curl -fsSL https://git.io/shellspec | sh -s -- -y + - run: cd config && shellspec --shell bash + + dockerfile-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: config/Dockerfile + failure-threshold: error + - uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: config/Dockerfile.wolfi + failure-threshold: error + + docker-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build Alpine image (builder stage only — runtime requires glibc for Claude CLI) + run: docker build --target=builder -f config/Dockerfile -t stackai:ci-test config/ diff --git a/.gitignore b/.gitignore index b666de0..555c417 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,19 @@ +# Python **/__pycache__/* +*.egg-info/ +dist/ +build/ +.venv/ + +# OS +.DS_Store +Thumbs.db + +# Editors +.vscode/ +.idea/ +*.swp +*.swo + +# Claude Code eval workspace .claude/skills/spawn-agent-workspace/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b50d6be --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2026 stackai contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/cli/pyproject.toml b/app/cli/pyproject.toml index 236a363..36b073f 100644 --- a/app/cli/pyproject.toml +++ b/app/cli/pyproject.toml @@ -17,4 +17,5 @@ packages = ["src/container_cli"] [dependency-groups] dev = [ "pytest>=9.0.2", + "ruff>=0.9", ] diff --git a/app/cli/src/container_cli/utils.py b/app/cli/src/container_cli/utils.py index 7436a8a..e4ff460 100644 --- a/app/cli/src/container_cli/utils.py +++ b/app/cli/src/container_cli/utils.py @@ -1,6 +1,5 @@ import os import subprocess -import sys from pathlib import Path import typer diff --git a/app/cli/tests/conftest.py b/app/cli/tests/conftest.py index a4b7a54..6746d76 100644 --- a/app/cli/tests/conftest.py +++ b/app/cli/tests/conftest.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest diff --git a/app/cli/tests/test_agents.py b/app/cli/tests/test_agents.py index 14294ad..8c20d86 100644 --- a/app/cli/tests/test_agents.py +++ b/app/cli/tests/test_agents.py @@ -1,7 +1,5 @@ from __future__ import annotations -import pytest - from container_cli.commands.agents import follow, list_agents, logs, spawn, stop diff --git a/app/cli/tests/test_main.py b/app/cli/tests/test_main.py index 485d801..ad7bc61 100644 --- a/app/cli/tests/test_main.py +++ b/app/cli/tests/test_main.py @@ -1,7 +1,5 @@ from __future__ import annotations -from unittest.mock import patch - from typer.testing import CliRunner from container_cli.main import app diff --git a/config/Dockerfile b/config/Dockerfile index 72cf0d0..0ba9009 100644 --- a/config/Dockerfile +++ b/config/Dockerfile @@ -122,19 +122,19 @@ RUN curl -fsSL https://opencode.ai/install | bash \ # Definidos en /etc/profile.d/ para shells interactivas # Para uso programático por Claude, complementar con system prompt # ----------------------------------------------------------------------------- -RUN cat > /etc/profile.d/rust-aliases.sh << 'EOF' -# Reemplazos Rust -alias grep='rg --smart-case --follow' -alias find='fd --follow' -alias cat='bat --paging=never' -alias ls='eza' -alias ll='eza -la --git' -alias la='eza -la' -alias lt='eza --tree --level=2' -alias du='dust' -alias ps='procs' -alias top='btm' -EOF +RUN printf '%s\n' \ + '# Rust replacements' \ + "alias grep='rg --smart-case --follow'" \ + "alias find='fd --follow'" \ + "alias cat='bat --paging=never'" \ + "alias ls='eza'" \ + "alias ll='eza -la --git'" \ + "alias la='eza -la'" \ + "alias lt='eza --tree --level=2'" \ + "alias du='dust'" \ + "alias ps='procs'" \ + "alias top='btm'" \ + > /etc/profile.d/rust-aliases.sh RUN echo 'source /etc/profile.d/rust-aliases.sh' >> /root/.bashrc \ && echo 'source /etc/profile.d/rust-aliases.sh' >> /root/.profile diff --git a/config/Dockerfile.wolfi b/config/Dockerfile.wolfi index 2c31d21..7a17aa1 100644 --- a/config/Dockerfile.wolfi +++ b/config/Dockerfile.wolfi @@ -94,18 +94,18 @@ RUN curl -fsSL https://opencode.ai/install | bash \ RUN npm install -g @fission-ai/openspec@latest # Aliases Rust -RUN cat > /etc/profile.d/rust-aliases.sh << 'EOF' -alias grep='rg --smart-case --follow' -alias find='fd --follow' -alias cat='bat --paging=never' -alias ls='eza' -alias ll='eza -la --git' -alias la='eza -la' -alias lt='eza --tree --level=2' -alias du='dust' -alias ps='procs' -alias top='btm' -EOF +RUN printf '%s\n' \ + "alias grep='rg --smart-case --follow'" \ + "alias find='fd --follow'" \ + "alias cat='bat --paging=never'" \ + "alias ls='eza'" \ + "alias ll='eza -la --git'" \ + "alias la='eza -la'" \ + "alias lt='eza --tree --level=2'" \ + "alias du='dust'" \ + "alias ps='procs'" \ + "alias top='btm'" \ + > /etc/profile.d/rust-aliases.sh RUN echo 'source /etc/profile.d/rust-aliases.sh' >> /root/.bashrc \ && echo 'source /etc/profile.d/rust-aliases.sh' >> /root/.profile diff --git a/config/Makefile b/config/Makefile index 7e87295..4cd8e9b 100644 --- a/config/Makefile +++ b/config/Makefile @@ -105,9 +105,9 @@ shell: run spawn: network _check-token @mkdir -p "$(WORKTREES_DIR)" - @echo "[spawn] Lanzando agente: $(PROJECT_NAME)-$(CONTAINER_BRANCH)" + @echo "[spawn] Launching agent: $(PROJECT_NAME)-$(CONTAINER_BRANCH)" @echo "[spawn] Worktree: $(WORKTREES_DIR)/$(BRANCH)" - @echo "[spawn] Tarea: $(TASK)" + @echo "[spawn] Task: $(TASK)" container run -d --rm \ --name $(PROJECT_NAME)-$(CONTAINER_BRANCH) \ --network $(NETWORK) \ @@ -122,16 +122,16 @@ spawn: network _check-token -e CLAUDE_CODE_OAUTH_TOKEN=$${$(HOST_TOKEN_VAR)} \ $(IMAGE) \ --worktree "$(BRANCH)" --task "$(TASK)" - @echo "[spawn] Agente iniciado. Ver logs: make logs-agent BRANCH=$(BRANCH)" + @echo "[spawn] Agent started. View logs: make logs-agent BRANCH=$(BRANCH)" # ── Agent monitoring ────────────────────────────────────────────────────────── list-agents: - @echo "[agents] Contenedores activos para proyecto '$(PROJECT_NAME)':" - @container list 2>/dev/null | grep "$(PROJECT_NAME)" || echo " (ninguno)" + @echo "[agents] Active containers for project '$(PROJECT_NAME)':" + @container list 2>/dev/null | grep "$(PROJECT_NAME)" || echo " (none)" @echo "" - @echo "[agents] Worktrees en $(WORKTREES_DIR):" - @ls -la "$(WORKTREES_DIR)" 2>/dev/null || echo " (ninguno aún)" + @echo "[agents] Worktrees in $(WORKTREES_DIR):" + @ls -la "$(WORKTREES_DIR)" 2>/dev/null || echo " (none yet)" logs-agent: container logs $(PROJECT_NAME)-$(CONTAINER_BRANCH) @@ -141,8 +141,8 @@ follow-agent: stop-agent: @container stop $(PROJECT_NAME)-$(CONTAINER_BRANCH) 2>/dev/null \ - && echo "[stop] Agente $(PROJECT_NAME)-$(CONTAINER_BRANCH) detenido." \ - || echo "[stop] Agente $(PROJECT_NAME)-$(CONTAINER_BRANCH) no encontrado o ya detenido." + && echo "[stop] Agent $(PROJECT_NAME)-$(CONTAINER_BRANCH) stopped." \ + || echo "[stop] Agent $(PROJECT_NAME)-$(CONTAINER_BRANCH) not found or already stopped." # ── Cleanup ─────────────────────────────────────────────────────────────────── diff --git a/config/entrypoint.sh b/config/entrypoint.sh index 02aeab4..5b01d67 100644 --- a/config/entrypoint.sh +++ b/config/entrypoint.sh @@ -1,18 +1,18 @@ #!/bin/bash -# Entrypoint — copia credenciales y soporta modo agente headless +# Entrypoint — copies credentials and supports headless agent mode # -# Modo interactivo (default): -# entrypoint.sh → bash interactivo +# Interactive mode (default): +# entrypoint.sh → interactive bash # entrypoint.sh [args...] → exec [args...] # -# Modo agente headless: +# Headless agent mode: # entrypoint.sh --worktree --task "" # entrypoint.sh --worktree --task "" --project # -# Volúmenes esperados: -# -v :/workspace → repo principal (read/write) -# -v /.worktrees:/worktrees → directorio de worktrees -# -v ~/.claude:/root/.claudenew:ro → credenciales host +# Expected volumes: +# -v :/workspace → main repository (read/write) +# -v /.worktrees:/worktrees → worktrees directory +# -v ~/.claude:/root/.claudenew:ro → host credentials # -v ~/.claude.json:/root/.claudenew.json:ro set -euo pipefail @@ -22,7 +22,7 @@ AGENT_TASK="" PROJECT_NAME="" PASSTHROUGH_ARGS=() -# ── Parsear flags del modo agente ────────────────────────────────────────────── +# ── Parse agent mode flags ──────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --worktree) WORKTREE_BRANCH="$2"; shift 2 ;; @@ -32,38 +32,38 @@ while [[ $# -gt 0 ]]; do esac done -# ── Copiar credenciales desde mounts del host ────────────────────────────────── -echo "[entrypoint] Copiando credenciales..." +# ── Copy credentials from host mounts ───────────────────────────────────────── +echo "[entrypoint] Copying credentials..." cp /root/.claudenew.json /root/.claude.json mkdir -p /root/.claude cp -r /root/.claudenew/. /root/.claude/ -echo "[entrypoint] Credenciales listas." +echo "[entrypoint] Credentials ready." -# ── Modo agente: worktree + headless ────────────────────────────────────────── +# ── Agent mode: worktree + headless ─────────────────────────────────────────── if [[ -n "$WORKTREE_BRANCH" ]]; then WORKTREE_PATH="/worktrees/${WORKTREE_BRANCH}" - echo "[entrypoint] Creando worktree: ${WORKTREE_BRANCH} → ${WORKTREE_PATH}" + echo "[entrypoint] Creating worktree: ${WORKTREE_BRANCH} → ${WORKTREE_PATH}" - # Crear directorio de destino si no existe + # Create destination directory if it doesn't exist mkdir -p "$(dirname "$WORKTREE_PATH")" - # Añadir worktree (idempotente: si ya existe la rama, simplemente la usa) + # Add worktree (idempotent: if branch already exists, reuses it) if git -C /workspace worktree add "$WORKTREE_PATH" -b "$WORKTREE_BRANCH" 2>/dev/null; then - echo "[entrypoint] Worktree creado en rama nueva: ${WORKTREE_BRANCH}" + echo "[entrypoint] Worktree created on new branch: ${WORKTREE_BRANCH}" elif git -C /workspace worktree add "$WORKTREE_PATH" "$WORKTREE_BRANCH" 2>/dev/null; then - echo "[entrypoint] Worktree creado sobre rama existente: ${WORKTREE_BRANCH}" + echo "[entrypoint] Worktree created on existing branch: ${WORKTREE_BRANCH}" else - echo "[entrypoint] ERROR: no se pudo crear el worktree para '${WORKTREE_BRANCH}'" >&2 + echo "[entrypoint] ERROR: could not create worktree for '${WORKTREE_BRANCH}'" >&2 exit 1 fi cd "$WORKTREE_PATH" - echo "[entrypoint] Directorio de trabajo: $(pwd)" + echo "[entrypoint] Working directory: $(pwd)" if [[ -n "$AGENT_TASK" ]]; then - echo "[entrypoint] Iniciando agente Claude (headless)..." - echo "[entrypoint] Tarea: ${AGENT_TASK}" + echo "[entrypoint] Starting Claude agent (headless)..." + echo "[entrypoint] Task: ${AGENT_TASK}" echo "---" # Make claude's install path traversable for non-root users (installed under /root/) # go+x required: agent is in group root (gid=0), so group bits apply, not others bits @@ -77,12 +77,12 @@ if [[ -n "$WORKTREE_BRANCH" ]]; then chown -R agent:agent "$WORKTREE_PATH" exec su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p "$AGENT_TASK" else - # Worktree listo pero sin tarea: shell interactivo en el worktree + # Worktree ready but no task: interactive shell in the worktree exec /bin/bash --login fi fi -# ── Modo interactivo (comportamiento original) ───────────────────────────────── +# ── Interactive mode (original behavior) ────────────────────────────────────── if [[ ${#PASSTHROUGH_ARGS[@]} -eq 0 ]]; then exec /bin/bash --login else diff --git a/config/spec/entrypoint_spec.sh b/config/spec/entrypoint_spec.sh index 31c5b53..29e243c 100644 --- a/config/spec/entrypoint_spec.sh +++ b/config/spec/entrypoint_spec.sh @@ -71,19 +71,19 @@ WRAPPER_EOF Describe "Argument Parsing" It "sets WORKTREE_BRANCH with --worktree flag" When run run_entrypoint --worktree my-branch - The output should include "Creando worktree: my-branch" + The output should include "Creating worktree: my-branch" The status should equal 0 End It "sets AGENT_TASK with --task flag" When run run_entrypoint --worktree test-branch --task "fix the bug" - The output should include "Tarea: fix the bug" + The output should include "Task: fix the bug" The status should equal 0 End It "sets PROJECT_NAME with --project flag" When run run_entrypoint --worktree test-branch --task "do stuff" --project my-project - The output should include "Tarea: do stuff" + The output should include "Task: do stuff" The status should equal 0 End @@ -101,21 +101,21 @@ WRAPPER_EOF It "handles all three flags together" When run run_entrypoint --worktree feat/xyz --task "implement feature" --project acme - The output should include "Creando worktree: feat/xyz" - The output should include "Tarea: implement feature" + The output should include "Creating worktree: feat/xyz" + The output should include "Task: implement feature" The status should equal 0 End It "handles empty string values for --worktree" When run run_entrypoint --worktree "" - The output should not include "Creando worktree" + The output should not include "Creating worktree" The output should include "[EXEC] exec /bin/bash --login" The status should equal 0 End It "handles mixed flags and passthrough args" When run run_entrypoint --worktree my-branch --task "hello" extra-arg - The output should include "Creando worktree: my-branch" + The output should include "Creating worktree: my-branch" The status should equal 0 End End @@ -144,13 +144,13 @@ WRAPPER_EOF It "prints start message" When run run_entrypoint - The output should include "[entrypoint] Copiando credenciales..." + The output should include "[entrypoint] Copying credentials..." The status should equal 0 End It "prints completion message" When run run_entrypoint - The output should include "[entrypoint] Credenciales listas." + The output should include "[entrypoint] Credentials ready." The status should equal 0 End End @@ -179,7 +179,7 @@ WRAPPER_EOF It "does not trigger worktree mode without --worktree" When run run_entrypoint some-command - The output should not include "Creando worktree" + The output should not include "Creating worktree" The output should include "[EXEC] exec some-command" The status should equal 0 End @@ -204,7 +204,7 @@ WRAPPER_EOF It "creates worktree on new branch via git worktree add -b" When run run_entrypoint --worktree feat/new-feature The output should include "[MOCK] git -C /workspace worktree add /worktrees/feat/new-feature -b feat/new-feature" - The output should include "Worktree creado en rama nueva: feat/new-feature" + The output should include "Worktree created on new branch: feat/new-feature" The status should equal 0 End @@ -212,7 +212,7 @@ WRAPPER_EOF export GIT_WORKTREE_NEW_BRANCH_SUCCEEDS="false" When run run_entrypoint --worktree feat/existing The output should include "[MOCK] git -C /workspace worktree add /worktrees/feat/existing feat/existing" - The output should include "Worktree creado sobre rama existente: feat/existing" + The output should include "Worktree created on existing branch: feat/existing" The status should equal 0 End @@ -220,7 +220,7 @@ WRAPPER_EOF export GIT_WORKTREE_NEW_BRANCH_SUCCEEDS="false" export GIT_WORKTREE_EXISTING_BRANCH_SUCCEEDS="false" When run run_entrypoint --worktree feat/broken - The output should include "Creando worktree: feat/broken" + The output should include "Creating worktree: feat/broken" The stderr should include "ERROR" The stderr should include "feat/broken" The status should equal 1 @@ -246,7 +246,7 @@ WRAPPER_EOF It "handles nested branch names with slashes" When run run_entrypoint --worktree feature/team/ticket-123 - The output should include "Creando worktree: feature/team/ticket-123" + The output should include "Creating worktree: feature/team/ticket-123" The output should include "/worktrees/feature/team/ticket-123" The status should equal 0 End @@ -259,7 +259,7 @@ WRAPPER_EOF It "prints working directory after cd" When run run_entrypoint --worktree my-branch - The output should include "[entrypoint] Directorio de trabajo:" + The output should include "[entrypoint] Working directory:" The status should equal 0 End End @@ -313,8 +313,8 @@ WRAPPER_EOF It "prints agent initialization messages" When run run_entrypoint --worktree agent-br --task "implement feature" - The output should include "[entrypoint] Iniciando agente Claude (headless)..." - The output should include "[entrypoint] Tarea: implement feature" + The output should include "[entrypoint] Starting Claude agent (headless)..." + The output should include "[entrypoint] Task: implement feature" The output should include "---" The status should equal 0 End @@ -327,7 +327,7 @@ WRAPPER_EOF It "passes task with special characters" When run run_entrypoint --worktree agent-br --task "fix bug #123 & deploy" - The output should include "Tarea: fix bug #123 & deploy" + The output should include "Task: fix bug #123 & deploy" The status should equal 0 End End @@ -376,40 +376,40 @@ WRAPPER_EOF It "treats empty WORKTREE_BRANCH as unset (interactive mode)" When run run_entrypoint --worktree "" - The output should not include "Creando worktree" + The output should not include "Creating worktree" The output should include "[EXEC] exec /bin/bash --login" The status should equal 0 End It "handles task with double quotes" When run run_entrypoint --worktree br --task 'say "hello world"' - The output should include 'Tarea: say "hello world"' + The output should include 'Task: say "hello world"' The status should equal 0 End It "handles task with newline-like content" When run run_entrypoint --worktree br --task "line1 line2" - The output should include "Tarea: line1 line2" + The output should include "Task: line1 line2" The status should equal 0 End It "handles branch name with dots" When run run_entrypoint --worktree release/v1.2.3 - The output should include "Creando worktree: release/v1.2.3" + The output should include "Creating worktree: release/v1.2.3" The status should equal 0 End It "handles branch name with hyphens and underscores" When run run_entrypoint --worktree fix/my_feature-branch - The output should include "Creando worktree: fix/my_feature-branch" + The output should include "Creating worktree: fix/my_feature-branch" The status should equal 0 End It "credential copy runs before worktree mode" When run run_entrypoint --worktree my-branch --task "work" - The output should include "Copiando credenciales" - The output should include "Credenciales listas" - The output should include "Creando worktree" + The output should include "Copying credentials" + The output should include "Credentials ready" + The output should include "Creating worktree" The status should equal 0 End End diff --git a/docs/agents/container-agent.md b/docs/agents/container-agent.md index 00f79fe..83e24ed 100644 --- a/docs/agents/container-agent.md +++ b/docs/agents/container-agent.md @@ -1,76 +1,73 @@ -# Imagen claude-agent:wolfi — Dockerfile y Makefile +# claude-agent:wolfi Image — Dockerfile and Makefile -## Visión general +## Overview -`claude-agent:wolfi` es una imagen ARM64 (Apple Silicon M4) construida sobre Chainguard Wolfi. Está diseñada específicamente para correr instancias headless de Claude Code en contenedores Apple Container, con soporte para operación multi-agente en paralelo. - -**Por qué Wolfi y no Alpine:** -Alpine usa la librería `musl`, y el binario de Claude ≥ 2.1.63 requiere `posix_getdents`, símbolo exclusivo de `glibc`. Wolfi es glibc-based con un footprint comparable a Alpine (~5 MB base), sin las incompatibilidades de musl. +`claude-agent:wolfi` is an ARM64 (Apple Silicon) image built on Chainguard Wolfi. It is specifically designed to run headless instances of Claude Code in Apple Containers, with support for parallel multi-agent operation. --- -## Dockerfile.wolfi — Explicación por etapas +## Dockerfile.wolfi — Stage-by-stage explanation -### Stage 1: `builder` (compilación de herramientas Rust) +### Stage 1: `builder` (Rust tool compilation) ```dockerfile FROM --platform=linux/arm64 cgr.dev/chainguard/rust:latest-dev AS builder ``` -Compila en Rust las herramientas de productividad CLI: +Compiles CLI productivity tools written in Rust: -| Herramienta | Propósito | Reemplaza | +| Tool | Purpose | Replaces | |---|---|---| -| `rg` (ripgrep) | Búsqueda de texto ultra-rápida | `grep` | -| `fd` (fd-find) | Búsqueda de archivos | `find` | -| `bat` | Cat con syntax highlighting | `cat` | -| `eza` | Listado de archivos moderno | `ls` | -| `dust` | Visualización de uso de disco | `du` | -| `procs` | Listado de procesos moderno | `ps` | -| `btm` (bottom) | Monitor de sistema | `top` | +| `rg` (ripgrep) | Ultra-fast text search | `grep` | +| `fd` (fd-find) | File search | `find` | +| `bat` | Cat with syntax highlighting | `cat` | +| `eza` | Modern file listing | `ls` | +| `dust` | Disk usage visualization | `du` | +| `procs` | Modern process listing | `ps` | +| `btm` (bottom) | System monitor | `top` | -Los binarios compilados se copian al stage 2, manteniendo la imagen final limpia. +The compiled binaries are copied to stage 2, keeping the final image clean. -### Stage 2: `runtime` (imagen final) +### Stage 2: `runtime` (final image) ```dockerfile FROM --platform=linux/arm64 cgr.dev/chainguard/wolfi-base:latest AS runtime ``` -**Paquetes del sistema instalados:** +**Installed system packages:** ``` -bash, busybox, curl, wget ← utilidades base -git, git-lfs ← control de versiones -openssh-client, ca-certificates ← conectividad segura -jq, unzip, gzip ← procesamiento de datos -tmux ← multiplexor de terminal +bash, busybox, curl, wget ← base utilities +git, git-lfs ← version control +openssh-client, ca-certificates ← secure connectivity +jq, unzip, gzip ← data processing +tmux ← terminal multiplexer gh ← GitHub CLI -nodejs-22, npm ← runtime JavaScript -python-3.13, py3-pip ← runtime Python +nodejs-22, npm ← JavaScript runtime +python-3.13, py3-pip ← Python runtime ``` -**Herramientas adicionales instaladas:** +**Additional tools installed:** ``` -claude ← Claude Code CLI (via install.sh oficial) +claude ← Claude Code CLI (via official install.sh) opencode ← OpenCode AI CLI -openspec ← @fission-ai/openspec (npm global) +openspec ← @fission-ai/openspec (global npm) ``` -**Variables de entorno configuradas:** +**Configured environment variables:** ```dockerfile LANG=C.UTF-8 LC_ALL=C.UTF-8 TERM=xterm-256color PATH=/root/.local/bin:/usr/local/bin:$PATH -CLAUDE_CODE_DISABLE_AUTOUPDATE=1 ← evita auto-updates en contenedores +CLAUDE_CODE_DISABLE_AUTOUPDATE=1 ← prevents auto-updates in containers BAT_PAGER="" BAT_STYLE="numbers,changes,header" ``` -**Aliases Rust** (configurados en `/etc/profile.d/rust-aliases.sh`): +**Rust aliases** (configured in `/etc/profile.d/rust-aliases.sh`): ```bash alias grep='rg --smart-case --follow' @@ -83,33 +80,33 @@ alias ps='procs' alias top='btm' ``` -**Configuración git global:** +**Global git configuration:** ```bash git config --global init.defaultBranch main -git config --global core.editor "true" # editor no-op (headless) +git config --global core.editor "true" # no-op editor (headless) git config --global advice.detachedHead false ``` --- -## entrypoint.sh — Modos de operación +## entrypoint.sh — Operating modes -El entrypoint soporta dos modos, seleccionados por los argumentos pasados al contenedor. +The entrypoint supports two modes, selected by the arguments passed to the container. -### Modo interactivo (default) +### Interactive mode (default) ```bash container run -it claude-agent:wolfi -# o +# or container run -it claude-agent:wolfi /bin/bash --login ``` -**Flujo:** -1. Copia credenciales: `~/.claudenew.json` → `~/.claude.json` y `~/.claudenew/` → `~/.claude/` -2. Inicia shell bash interactiva con el perfil completo cargado +**Flow:** +1. Copies credentials: `~/.claudenew.json` → `~/.claude.json` and `~/.claudenew/` → `~/.claude/` +2. Starts an interactive bash shell with the full profile loaded -### Modo agente headless +### Headless agent mode ```bash container run -d --rm claude-agent:wolfi \ @@ -117,47 +114,47 @@ container run -d --rm claude-agent:wolfi \ --task "Implement OAuth2 with JWT tokens..." ``` -**Argumentos del entrypoint:** +**Entrypoint arguments:** -| Argumento | Descripción | +| Argument | Description | |---|---| -| `--worktree ` | Nombre de la rama/worktree a crear | -| `--task ""` | Prompt para Claude en modo headless | -| `--project ` | (opcional) Nombre del proyecto | +| `--worktree ` | Name of the branch/worktree to create | +| `--task ""` | Prompt for Claude in headless mode | +| `--project ` | (optional) Project name | -**Flujo:** -1. Copia credenciales desde mounts del host +**Flow:** +1. Copies credentials from host mounts 2. `git -C /workspace worktree add /worktrees/ -b ` - - Si la rama ya existe: `git worktree add /worktrees/ ` + - If the branch already exists: `git worktree add /worktrees/ ` 3. `cd /worktrees/` 4. `claude --dangerously-skip-permissions -p ""` -**Por qué `--dangerously-skip-permissions`:** En modo headless no hay usuario interactivo para aprobar permisos. El contenedor es un entorno sandboxed con acceso solo al worktree montado, por lo que es seguro saltarse las confirmaciones. +**Why `--dangerously-skip-permissions`:** In headless mode there is no interactive user to approve permissions. The container is a sandboxed environment with access only to the mounted worktree, so it is safe to skip confirmations. -**Por qué correr como `agent` (non-root):** Claude CLI bloquea `--dangerously-skip-permissions` cuando el proceso corre como `root` (uid 0). El entrypoint usa `su-exec` para hacer drop al usuario `agent` antes de ejecutar Claude. +**Why run as `agent` (non-root):** Claude CLI blocks `--dangerously-skip-permissions` when the process runs as `root` (uid 0). The entrypoint uses `su-exec` to drop to the `agent` user before executing Claude. -IMPORTANTE: **Por qué el worktree se crea dentro del contenedor:** Git necesita acceso al repositorio para registrar el worktree en `.git/worktrees/`. Como el repo está montado en `/workspace` dentro del contenedor, el worktree debe crearse desde allí. Si se creara desde el host directamente, el path registrado en git sería el path del host (`/Users/...`), que no existiría dentro del contenedor. +IMPORTANT: **Why the worktree is created inside the container:** Git needs access to the repository to register the worktree in `.git/worktrees/`. Since the repo is mounted at `/workspace` inside the container, the worktree must be created from there. If it were created directly from the host, the path registered in git would be the host path (`/Users/...`), which would not exist inside the container. --- -## Makefile — Referencia de targets +## Makefile — Target reference -### Variables configurables +### Configurable variables -| Variable | Default | Descripción | +| Variable | Default | Description | |---|---|---| -| `IMAGE` | `claude-agent:wolfi` | Nombre de la imagen Docker | -| `DOCKERFILE` | `Dockerfile.wolfi` | Dockerfile a usar | -| `NAME` | `qubits-team` | Nombre base para el contenedor interactivo | -| `NETWORK` | `claude-agent-net` | Red bridge de los agentes | -| `SUBNET` | `192.168.100.0/24` | CIDR de la red | -| `CPUS` | `8` | CPUs asignadas a cada contenedor | -| `MEMORY` | `12G` | RAM asignada a cada contenedor | -| `BRANCH` | `agent-` | Rama del agente a spawnear | -| `TASK` | `Explore the codebase...` | Tarea del agente | -| `AGENTS_HOME` | `/.worktrees` | Fallback si no está en env | - -**Variables derivadas automáticamente:** +| `IMAGE` | `claude-agent:wolfi` | Docker image name | +| `DOCKERFILE` | `Dockerfile.wolfi` | Dockerfile to use | +| `NAME` | `qubits-team` | Base name for the interactive container | +| `NETWORK` | `claude-agent-net` | Agent bridge network | +| `SUBNET` | `192.168.100.0/24` | Network CIDR | +| `CPUS` | `8` | CPUs allocated to each container | +| `MEMORY` | `12G` | RAM allocated to each container | +| `BRANCH` | `agent-` | Agent branch to spawn | +| `TASK` | `Explore the codebase...` | Agent task | +| `AGENTS_HOME` | `/.worktrees` | Fallback if not in env | + +**Automatically derived variables:** ```makefile GIT_ROOT := $(shell git -C $(CURDIR) rev-parse --show-toplevel) @@ -170,99 +167,99 @@ CONTAINER_BRANCH := $(shell echo "$(BRANCH)" | tr '/_ ' '-' | tr '[:upper:]' '[: ### Targets #### `make build` -Construye la imagen sin caché. +Builds the image without cache. ```bash make build -# equivale a: container build --no-cache -f Dockerfile.wolfi -t claude-agent:wolfi . +# equivalent to: container build --no-cache -f Dockerfile.wolfi -t claude-agent:wolfi . ``` #### `make network` -Crea la red bridge `claude-agent-net` si no existe. Requiere macOS 26+. +Creates the bridge network `claude-agent-net` if it does not exist. Requires macOS 26+. ```bash make network ``` #### `make run` / `make shell` -Lanza el contenedor en modo interactivo (coordinador o sesión de desarrollo). +Launches the container in interactive mode (coordinator or development session). ```bash make run -make run NAME=mi-agente CPUS=4 MEMORY=8G +make run NAME=my-agent CPUS=4 MEMORY=8G ``` -Requiere `CLAUDE_CONTAINER_OAUTH_TOKEN` exportado. +Requires `CLAUDE_CONTAINER_OAUTH_TOKEN` to be exported. #### `make spawn` -Lanza un agente virtual en modo detached (headless). **El target principal para multi-agente.** +Launches a virtual agent in detached (headless) mode. **The main target for multi-agent.** ```bash make spawn BRANCH=feat/oauth2 TASK="Implement OAuth2 with JWT" make spawn BRANCH=test/auth TASK="Write unit tests for auth module" make spawn BRANCH=mutation/payments TASK="Run mutation testing on payment service" ``` -- Crea `$AGENTS_HOME` si no existe -- Lanza contenedor con nombre `${PROJECT_NAME}-${CONTAINER_BRANCH}` -- Muestra cómo ver los logs al terminar +- Creates `$AGENTS_HOME` if it does not exist +- Launches container named `${PROJECT_NAME}-${CONTAINER_BRANCH}` +- Shows how to view logs upon completion #### `make list-agents` -Lista contenedores activos del proyecto y worktrees en disco. +Lists active project containers and worktrees on disk. ```bash make list-agents ``` -#### `make logs-agent BRANCH=` -Muestra los logs del agente (snapshot). +#### `make logs-agent BRANCH=` +Shows agent logs (snapshot). ```bash make logs-agent BRANCH=feat/oauth2 ``` -#### `make follow-agent BRANCH=` -Sigue los logs del agente en tiempo real. +#### `make follow-agent BRANCH=` +Follows agent logs in real time. ```bash make follow-agent BRANCH=feat/oauth2 ``` -#### `make stop-agent BRANCH=` -Detiene el agente. +#### `make stop-agent BRANCH=` +Stops the agent. ```bash make stop-agent BRANCH=feat/oauth2 ``` #### `make clean` -Elimina el contenedor y la imagen. No afecta los worktrees. +Removes the container and the image. Does not affect worktrees. ```bash make clean ``` #### `make clean-network` -Elimina la red bridge. +Removes the bridge network. ```bash make clean-network ``` #### `make clean-all` -Elimina imagen y red. +Removes image and network. ```bash make clean-all ``` --- -## Requisito de variable de entorno del host +## Host environment variable requirement ```bash -# Obligatorio para usar make run, make spawn -export CLAUDE_CONTAINER_OAUTH_TOKEN= +# Required for make run, make spawn +export CLAUDE_CONTAINER_OAUTH_TOKEN= -# Recomendado (fallback si no está seteado: dirname(GIT_ROOT)/.worktrees) +# Recommended (fallback if not set: dirname(GIT_ROOT)/.worktrees) export AGENTS_HOME=~/agents ``` -**Por qué `CLAUDE_CONTAINER_OAUTH_TOKEN` y no `CLAUDE_CODE_OAUTH_TOKEN`:** -El Makefile mapea `CLAUDE_CONTAINER_OAUTH_TOKEN` del host a `CLAUDE_CODE_OAUTH_TOKEN` dentro del contenedor. Esto evita que el contenedor lea el token de la sesión host, manteniendo sesiones aisladas. +**Why `CLAUDE_CONTAINER_OAUTH_TOKEN` and not `CLAUDE_CODE_OAUTH_TOKEN`:** +The Makefile maps `CLAUDE_CONTAINER_OAUTH_TOKEN` from the host to `CLAUDE_CODE_OAUTH_TOKEN` inside the container. This prevents the container from reading the host session token, keeping sessions isolated. --- -## Instrucciones de build +## Build instructions -### Build estándar +### Standard build ```bash cd /path/to/project/config @@ -270,17 +267,17 @@ export CLAUDE_CONTAINER_OAUTH_TOKEN= make build ``` -El build puede tomar varios minutos la primera vez (compila 7 crates de Rust). +The build may take several minutes the first time (compiles 7 Rust crates). -### Build rápido (reutilizar caché) +### Fast build (reuse cache) -Editar el Makefile y cambiar `--no-cache`: +Edit the Makefile and remove `--no-cache`: ```makefile build: - container build -f $(DOCKERFILE) -t $(IMAGE) . # sin --no-cache + container build -f $(DOCKERFILE) -t $(IMAGE) . # without --no-cache ``` -### Verificar imagen +### Verify image ```bash container image list | grep "claude-agent.*wolfi" @@ -289,93 +286,145 @@ container run --rm claude-agent:wolfi claude --version --- -## Red bridge (macOS 26+) +## Bridge network (macOS 26+) -La red `claude-agent-net` (CIDR `192.168.100.0/24`) permite que los contenedores se comuniquen entre sí y accedan a internet vía DNS `1.1.1.1` (Cloudflare). +The `claude-agent-net` network (CIDR `192.168.100.0/24`) allows containers to communicate with each other and access the internet via DNS `1.1.1.1` (Cloudflare). ```bash -# Crear +# Create container network create --subnet 192.168.100.0/24 claude-agent-net -# Listar +# List container network list -# Inspeccionar +# Inspect container network inspect claude-agent-net -# Eliminar +# Remove container network delete claude-agent-net ``` -`make network` hace el create de forma idempotente (no falla si ya existe). +`make network` performs the create idempotently (does not fail if it already exists). --- -## Flujo de credenciales - -``` -Host Contenedor -──────────────────────────────────────────────────────── -~/.claude/ ──(ro mount)──→ /root/.claudenew/ -~/.claude.json ──(ro mount)──→ /root/.claudenew.json - │ - entrypoint.sh - cp -r .claudenew/ → .claude/ - cp .claudenew.json → .claude.json - │ - Claude Code usa /root/.claude/ - (lectura/escritura dentro del contenedor) - -CLAUDE_CONTAINER_OAUTH_TOKEN ──(env var)──→ CLAUDE_CODE_OAUTH_TOKEN -``` - -Los mounts son **read-only** desde el host para evitar que el contenedor modifique las credenciales originales. El entrypoint hace una copia local para que Claude pueda escribir en su directorio de configuración sin afectar el host. +## Credential flow + +```mermaid +flowchart LR + subgraph Host + A["~/.claude/"] + B["~/.claude.json"] + C["CLAUDE_CONTAINER_OAUTH_TOKEN"] + end + + subgraph Container + subgraph "entrypoint.sh" + D["/root/.claudenew/ (ro)"] + E["/root/.claudenew.json (ro)"] + F["cp -r → /root/.claude/"] + G["cp → /root/.claude.json"] + end + H["Claude Code reads /root/.claude/ (rw)"] + I["CLAUDE_CODE_OAUTH_TOKEN"] + end + + A -- "ro mount" --> D + B -- "ro mount" --> E + D --> F + E --> G + F --> H + G --> H + C -- "-e flag" --> I + I --> H +``` + +The mounts are **read-only** from the host to prevent the container from modifying the original credentials. The entrypoint makes a local copy so Claude can write to its configuration directory without affecting the host. --- -## Usuario no-root para modo headless +## Non-root user for headless mode -Claude CLI bloquea `--dangerously-skip-permissions` cuando el proceso corre como `root` (uid 0). La imagen incluye un usuario `agent` (no-root) para el modo headless. +Claude CLI blocks `--dangerously-skip-permissions` when the process runs as `root` (uid 0). The image includes an `agent` user (non-root) for headless mode. -### Cambios en la imagen +### Image changes ```dockerfile -# su-exec: drop de privilegios con semántica exec (estándar Docker) +# su-exec: privilege drop with exec semantics (Docker standard) RUN apk add --no-cache su-exec -# Usuario agent (non-root) +# agent user (non-root) RUN addgroup -S agent \ && adduser -S -G agent -h /home/agent -s /bin/bash agent \ && ln -sf /root/.local/bin/claude /usr/local/bin/claude ``` -### Flujo de credenciales para modo headless +### Credential flow for headless mode ``` -/root/.claude/ (copiado por entrypoint desde mount host) +/root/.claude/ (copied by entrypoint from host mount) │ - └─► /home/agent/.claude/ (copiado + chown → agent) + └─► /home/agent/.claude/ (copied + chown → agent) │ su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p "..." ``` -El entrypoint: -1. Copia credenciales a `/root/.claude/` (como siempre) -2. Las copia también a `/home/agent/.claude/` con `chown agent` -3. Hace `chown agent` en el worktree -4. Ejecuta `su-exec agent` para hacer drop a uid no-root antes de llamar a Claude +The entrypoint: +1. Copies credentials to `/root/.claude/` (as usual) +2. Also copies them to `/home/agent/.claude/` with `chown agent` +3. Runs `chown agent` on the worktree +4. Executes `su-exec agent` to drop to non-root uid before calling Claude -### Por qué `su-exec` y no `su` o `runuser` +### Why `su-exec` and not `su` or `runuser` -`su-exec` hace un `execvp` directo (reemplaza el proceso, no crea un subshell). Esto preserva las señales, el PID, y evita el overhead de un shell adicional. Es el estándar para entrypoints de contenedores Docker. +`su-exec` performs a direct `execvp` (replaces the process, does not create a subshell). This preserves signals, the PID, and avoids the overhead of an additional shell. It is the standard for Docker container entrypoints. --- -## Notas de seguridad +## Security notes + +- Containers run with `--rm` (ephemeral) — they do not persist state outside the worktree +- Credentials are mounted read-only from the host +- `CLAUDE_CODE_DISABLE_AUTOUPDATE=1` prevents Claude from downloading updates inside the container +- Each container has access only to the repo mounted at `/workspace` and `$AGENTS_HOME` at `/worktrees` +- `--dangerously-skip-permissions` is safe in this context because the accessible filesystem is limited to the mounted volumes +- Headless mode runs as the `agent` user (non-root) by design + +--- + +## Container Strategy: Production vs CI + +This project uses two different container strategies depending on the environment: -- Los contenedores corren con `--rm` (efímeros) — no persisten estado fuera del worktree -- Las credenciales se montan read-only desde el host -- `CLAUDE_CODE_DISABLE_AUTOUPDATE=1` evita que Claude descargue actualizaciones dentro del contenedor -- Cada contenedor tiene acceso solo al repo montado en `/workspace` y a `$AGENTS_HOME` en `/worktrees` -- `--dangerously-skip-permissions` es seguro en este contexto porque el filesystem accesible está limitado a los volúmenes montados -- El modo headless corre como usuario `agent` (non-root) por diseño +### Production (local development) + +- **Runtime:** Apple Container CLI +- **Dockerfile:** `Dockerfile.wolfi` (ARM64, Chainguard Wolfi) +- **Architecture:** `linux/arm64` (Apple Silicon) +- **C library:** `glibc` + +The production image is built with Chainguard Wolfi, a hardened, minimal Linux distribution that uses `glibc`. This is required because Claude Code >= 2.1.63 depends on `posix_getdents`, a POSIX syscall wrapper that `musl` (the C library used by Alpine) does not provide. Wolfi provides `glibc` compatibility with a footprint comparable to Alpine, making it the ideal base for running Claude Code in containers. + +Apple Container CLI is used locally to run containers natively on Apple Silicon (ARM64) with low overhead and tight macOS integration (shared networking, volume mounts, bridge networks on macOS 26+). + +### CI (GitHub Actions) + +- **Runtime:** Docker +- **Dockerfile:** `Dockerfile` (Alpine-based) +- **Architecture:** `linux/amd64` +- **Purpose:** Build validation and testing only + +GitHub Actions runners are `amd64` Linux machines. Apple Container CLI is not available in this environment — it is a macOS-only tool tied to the Apple Virtualization framework. Therefore, CI uses standard Docker with the Alpine-based `Dockerfile` for build validation. + +The Alpine CI image is sufficient for verifying that the Dockerfile syntax is correct, dependencies resolve, and the build completes successfully. It is **not** used to run Claude Code headless agents — that is exclusively done via the Wolfi image on local Apple Silicon machines. + +### Summary + +| Aspect | Production (local) | CI (GitHub Actions) | +|---|---|---| +| Container runtime | Apple Container CLI | Docker | +| Dockerfile | `Dockerfile.wolfi` | `Dockerfile` (Alpine) | +| Architecture | `linux/arm64` | `linux/amd64` | +| Base image | Chainguard Wolfi (glibc) | Alpine (musl) | +| Purpose | Run headless Claude agents | Build validation | +| Claude Code compatible | Yes (`glibc` + `posix_getdents`) | No (musl lacks `posix_getdents`) | diff --git a/docs/agents/evals.md b/docs/agents/evals.md index d963554..c2c3492 100644 --- a/docs/agents/evals.md +++ b/docs/agents/evals.md @@ -1,84 +1,84 @@ # Evals — spawn-agent skill -## Qué son los evals +## What are evals -Los evals son casos de prueba automatizados para el skill `spawn-agent`. Miden si Claude, al tener el skill activo, produce las respuestas correctas (comandos, nombres de contenedores, prompts) ante distintos escenarios de uso. +Evals are automated test cases for the `spawn-agent` skill. They measure whether Claude, with the skill active, produces the correct responses (commands, container names, prompts) for different usage scenarios. -Se comparan dos configuraciones: -- **with_skill**: Claude tiene acceso a las instrucciones del skill -- **without_skill**: Claude responde sin el skill (baseline) +Two configurations are compared: +- **with_skill**: Claude has access to the skill instructions +- **without_skill**: Claude responds without the skill (baseline) -El objetivo es cuantificar el valor que agrega el skill y detectar regresiones entre iteraciones. +The goal is to quantify the value the skill adds and detect regressions between iterations. --- -## Escenarios de prueba +## Test scenarios ### Eval 1 — `spawn-feature` -**Prompt de prueba:** +**Test prompt:** > "I'm working on my stackai project and need you to spawn a virtual agent to implement OAuth2 authentication with JWT tokens in the API. Use branch feat/oauth2." **Assertions (7):** -1. Genera `container run -d` (detached, no `-it`) -2. Usa `--worktree feat/oauth2` en el comando -3. Sanitiza la rama correctamente: `feat-oauth2` (un guión, no dos) -4. Monta worktrees con `-v $AGENTS_HOME:/worktrees` (no named volume) -5. Pasa `CLAUDE_CODE_OAUTH_TOKEN` como variable de entorno -6. El prompt del agente es tipo **feature** (menciona "senior software engineer") -7. Incluye comando para seguir logs (`container logs -f`) +1. Generates `container run -d` (detached, not `-it`) +2. Uses `--worktree feat/oauth2` in the command +3. Sanitizes the branch correctly: `feat-oauth2` (one hyphen, not two) +4. Mounts worktrees with `-v $AGENTS_HOME:/worktrees` (not a named volume) +5. Passes `CLAUDE_CODE_OAUTH_TOKEN` as an environment variable +6. The agent prompt is **feature** type (mentions "senior software engineer") +7. Includes a command to follow logs (`container logs -f`) ### Eval 2 — `spawn-test` -**Prompt de prueba:** +**Test prompt:** > "Spawn an agent to write unit tests for the payment service module. Branch: test/payment-service" **Assertions (5):** -1. Genera `container run -d` (detached) -2. Usa `--worktree test/payment-service` -3. Prompt tipo **test** (menciona QA engineer, coverage, edge cases) -4. No usa prompt de tipo feature ni mutation -5. Nombre de contenedor sanitizado: `test-payment-service` (un guión) +1. Generates `container run -d` (detached) +2. Uses `--worktree test/payment-service` +3. Prompt is **test** type (mentions QA engineer, coverage, edge cases) +4. Does not use a feature or mutation type prompt +5. Sanitized container name: `test-payment-service` (one hyphen) ### Eval 3 — `list-agents` -**Prompt de prueba:** +**Test prompt:** > "Show me what agents are currently running. Also list the worktrees that exist." **Assertions (4):** -1. Ejecuta `container list` (no `docker ps` ni `ps aux`) -2. Filtra por prefijo del proyecto (`grep PROJECT_NAME`) -3. Muestra worktrees en disco (`ls -la $AGENTS_HOME`) -4. No intenta lanzar un nuevo agente +1. Runs `container list` (not `docker ps` or `ps aux`) +2. Filters by project prefix (`grep PROJECT_NAME`) +3. Shows worktrees on disk (`ls -la $AGENTS_HOME`) +4. Does not attempt to launch a new agent ### Eval 4 — `monitor-agent` -**Prompt de prueba:** +**Test prompt:** > "Check what the feat/oauth2 agent is doing right now. Give me a summary of its progress." **Assertions (4):** -1. Usa `container logs` (no `container run` ni `container list`) -2. Nombre de contenedor correcto: incluye `feat-oauth2` (sanitización correcta) -3. Resume los logs en lenguaje natural (no dump raw) -4. No lanza un nuevo contenedor +1. Uses `container logs` (not `container run` or `container list`) +2. Correct container name: includes `feat-oauth2` (proper sanitization) +3. Summarizes logs in natural language (no raw dump) +4. Does not launch a new container --- -## Estructura de archivos +## File structure ``` ~/.claude/skills/spawn-agent/ ├── SKILL.md └── evals/ - ├── evals.json ← definición formal de los 4 evals - ├── spawn_feature.md ← descripción narrativa del escenario + ├── evals.json ← formal definition of the 4 evals + ├── spawn_feature.md ← narrative description of the scenario ├── spawn_test.md ├── list_and_monitor.md ├── stop_agent.md └── multi_agent.md ~/.claude/skills/spawn-agent-workspace/ -├── iteration-1/ ← primera iteración del skill +├── iteration-1/ ← first iteration of the skill │ ├── spawn-feature/ │ │ ├── with_skill/outputs/response.md │ │ ├── with_skill/grading.json @@ -88,7 +88,7 @@ El objetivo es cuantificar el valor que agrega el skill y detectar regresiones e │ ├── list-agents/ │ ├── monitor-agent/ │ └── benchmark.json -└── iteration-2/ ← skill mejorado (versión actual) +└── iteration-2/ ← improved skill (current version) ├── spawn-feature/ ├── spawn-test/ ├── list-agents/ @@ -98,23 +98,23 @@ El objetivo es cuantificar el valor que agrega el skill y detectar regresiones e --- -## Resultados +## Results -### Iteración 1 — skill inicial +### Iteration 1 — initial skill -| Eval | with_skill | without_skill | Bug encontrado | +| Eval | with_skill | without_skill | Bug found | |---|---|---|---| -| spawn-feature | 85.7% | 0% | `feat--oauth2` doble guión | -| spawn-test | 80% | 20% | `test--payment-service` doble guión | -| list-agents | 25% | 50%* | Bash bloqueado en eval | -| monitor-agent | 50% | 50%* | Bash bloqueado en eval | -| **Media** | **60.7%** | **30%** | | +| spawn-feature | 85.7% | 0% | `feat--oauth2` double hyphen | +| spawn-test | 80% | 20% | `test--payment-service` double hyphen | +| list-agents | 25% | 50%* | Bash blocked in eval | +| monitor-agent | 50% | 50%* | Bash blocked in eval | +| **Average** | **60.7%** | **30%** | | -*El entorno de eval bloqueó Bash — los evals de list/monitor reflejan conocimiento del skill, no ejecución real. +*The eval environment blocked Bash — the list/monitor evals reflect skill knowledge, not actual execution. -**Bug crítico identificado:** `tr '/_ ' '---'` era ambiguo — los agentes interpretaban `'---'` como "triple guión" produciendo `feat--oauth2`. Debe ser `tr '/_ ' '-'`. +**Critical bug identified:** `tr '/_ ' '---'` was ambiguous — agents interpreted `'---'` as "triple hyphen" producing `feat--oauth2`. It should be `tr '/_ ' '-'`. -### Iteración 2 — skill corregido (actual) +### Iteration 2 — corrected skill (current) | Eval | with_skill | without_skill | Delta | |---|---|---|---| @@ -122,46 +122,46 @@ El objetivo es cuantificar el valor que agrega el skill y detectar regresiones e | spawn-test | **100%** | 20% | **+80%** | | list-agents | **100%** | 50% | **+50%** | | monitor-agent | **100%** | 50% | **+50%** | -| **Media** | **100%** | **30%** | **+70%** | +| **Average** | **100%** | **30%** | **+70%** | -**Cambios que corrigieron al 100%:** -1. `tr '/_ ' '-'` — reemplazo inequívoco, un guión siempre -2. `AGENTS_HOME` — variable de entorno reemplaza paths hardcodeados -3. `PROJECT_NAME=$(basename "$GIT_ROOT")` — nombre dinámico del proyecto -4. `container network list --format json` — parsing fiable de redes -5. Docs de Apple Container CLI incluidas en el skill +**Changes that achieved 100%:** +1. `tr '/_ ' '-'` — unambiguous replacement, always one hyphen +2. `AGENTS_HOME` — environment variable replaces hardcoded paths +3. `PROJECT_NAME=$(basename "$GIT_ROOT")` — dynamic project name +4. `container network list --format json` — reliable network parsing +5. Apple Container CLI docs included in the skill --- -## Cómo ejecutar los evals +## How to run evals -### Prerrequisitos +### Prerequisites ```bash -# Instalar el plugin skill-creator -/plugin skill-creator # desde Claude Code +# Install the skill-creator plugin +/plugin skill-creator # from Claude Code /reload-plugins ``` -### Correr evals con skill-creator +### Run evals with skill-creator ``` /skill-creator:skill-creator run evals for the spawn-agent skill at ~/.claude/skills/spawn-agent/ ``` -El proceso: -1. Lee `evals/evals.json` -2. Lanza runs en paralelo (with_skill + without_skill) -3. Genera `benchmark.json` y abre el viewer HTML -4. Tú revisas outputs y dejas feedback -5. El skill se mejora y se repite +The process: +1. Reads `evals/evals.json` +2. Launches runs in parallel (with_skill + without_skill) +3. Generates `benchmark.json` and opens the HTML viewer +4. You review outputs and leave feedback +5. The skill is improved and the cycle repeats -### Ejecutar directamente +### Run directly ```bash SKILL_CREATOR=~/.claude/plugins/cache/claude-plugins-official/skill-creator/d5c15b861cd2/skills/skill-creator -# Generar viewer estático +# Generate static viewer python3.13 "$SKILL_CREATOR/eval-viewer/generate_review.py" \ ~/.claude/skills/spawn-agent-workspace/iteration-2 \ --skill-name "spawn-agent" \ @@ -171,13 +171,13 @@ python3.13 "$SKILL_CREATOR/eval-viewer/generate_review.py" \ open /tmp/spawn-agent-review.html ``` -> **Python requerido:** Python 3.10+ (el sistema puede tener 3.9). Usar `~/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/bin/python3.13` +> **Python required:** Python 3.10+ (the system may have 3.9). Use `~/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/bin/python3.13` --- -## Cómo añadir nuevos evals +## How to add new evals -### 1. Agregar a `evals.json` +### 1. Add to `evals.json` ```json { @@ -193,13 +193,13 @@ open /tmp/spawn-agent-review.html } ``` -### 2. Crear archivo de descripción (opcional) +### 2. Create a description file (optional) ``` ~/.claude/skills/spawn-agent/evals/stop_agent.md ``` -### 3. Correr la nueva iteración +### 3. Run the new iteration ``` /skill-creator:skill-creator run evals for spawn-agent, iterate from iteration-2 @@ -207,7 +207,7 @@ open /tmp/spawn-agent-review.html --- -## Interpretación del benchmark.json +## Interpreting benchmark.json ```json { @@ -219,7 +219,61 @@ open /tmp/spawn-agent-review.html } ``` -- **pass_rate mean > 0.8** con skill → skill funcionando bien -- **delta > 0.5** → skill aporta valor significativo -- **stddev alto** → eval posiblemente flaky o dependiente del entorno -- **with_skill ≈ without_skill** → assertion no discrimina (revisar) +- **pass_rate mean > 0.8** with skill → skill is working well +- **delta > 0.5** → skill adds significant value +- **high stddev** → eval is possibly flaky or environment-dependent +- **with_skill ≈ without_skill** → assertion is not discriminating (review it) + +--- + +## Running Evals: Local Only + +Evals for the `spawn-agent` skill **cannot run in CI/CD pipelines**. They must be executed locally on a developer machine due to the following hard requirements: + +### Requirements + +1. **Claude Code CLI in headless mode** — Evals are driven by Claude Code, which must be installed and available as `claude` in your PATH. The eval runner invokes it in headless (non-interactive) mode to capture responses programmatically. + +2. **Apple Container CLI (macOS 26+)** — The spawn-agent skill generates `container run`, `container list`, and `container logs` commands targeting Apple's native container runtime. This CLI is only available on macOS 26 (Tahoe) or later. Linux and older macOS versions are not supported. + +3. **A valid `CLAUDE_CONTAINER_OAUTH_TOKEN`** — The containerized agent authenticates via an OAuth token passed as an environment variable. Without a valid token, spawned containers cannot execute Claude Code inside the container. This token cannot be safely stored in CI secrets due to rotation and scope constraints. + +4. **Mounted git worktrees** — The skill mounts the host's `$AGENTS_HOME` directory (typically `~/agents`) into containers at `/worktrees`. The eval environment must have a real git repository with worktree support. CI runners typically lack the necessary filesystem layout. + +### Step-by-step local execution + +```bash +# 1. Verify prerequisites +claude --version # Claude Code CLI is installed +container --version # Apple Container CLI is available (macOS 26+) +echo $CLAUDE_CONTAINER_OAUTH_TOKEN # Token is set and non-empty + +# 2. Ensure the skill is installed +ls ~/.claude/skills/spawn-agent/SKILL.md + +# 3. Ensure the eval definitions exist +ls ~/.claude/skills/spawn-agent/evals/evals.json + +# 4. Set up the agents home directory if it doesn't exist +export AGENTS_HOME="${AGENTS_HOME:-$HOME/agents}" +mkdir -p "$AGENTS_HOME" + +# 5. Run evals via skill-creator (recommended) +# Open Claude Code and run: +/plugin skill-creator +/reload-plugins +/skill-creator:skill-creator run evals for the spawn-agent skill at ~/.claude/skills/spawn-agent/ + +# 6. Or run the static viewer directly +SKILL_CREATOR=~/.claude/plugins/cache/claude-plugins-official/skill-creator/d5c15b861cd2/skills/skill-creator + +python3.13 "$SKILL_CREATOR/eval-viewer/generate_review.py" \ + ~/.claude/skills/spawn-agent-workspace/iteration-2 \ + --skill-name "spawn-agent" \ + --benchmark ~/.claude/skills/spawn-agent-workspace/iteration-2/benchmark.json \ + --static /tmp/spawn-agent-review.html + +open /tmp/spawn-agent-review.html +``` + +> **Note:** If evals appear to hang or produce empty results, verify that your `CLAUDE_CONTAINER_OAUTH_TOKEN` has not expired and that the Apple Container daemon is running (`container system info`). diff --git a/docs/agents/setup.md b/docs/agents/setup.md new file mode 100644 index 0000000..a71018a --- /dev/null +++ b/docs/agents/setup.md @@ -0,0 +1,110 @@ +# Setup and Authentication Guide + +## Why Claude Pro/Max is Required + +Virtual agents run `claude -p` (headless mode) inside Apple Containers. This mode requires an **OAuth token** that is only available with an active **Claude Pro** or **Claude Max** subscription on [claude.ai](https://claude.ai). + +Without an active subscription, the CLI cannot authenticate the headless session and the agent will fail immediately with an authentication error. + +--- + +## How to Obtain Your Token + +### Step 1 — Active subscription + +Make sure you have an active **Pro** or **Max** subscription on [claude.ai](https://claude.ai). You can verify your plan under **Settings → Subscription**. + +### Step 2 — Login from Claude Code CLI + +```bash +claude login +``` + +This command opens an OAuth flow in your default browser. Authorize access when prompted. + +### Step 3 — Verify the stored token + +Once the OAuth flow is complete, the token is automatically stored in `~/.claude/`. You can verify it exists: + +```bash +ls ~/.claude/ +``` + +You should see the credential files generated by the CLI. + +--- + +## Dual-Token Architecture + +The system uses **two distinct environment variables** for the OAuth token, one on the host and one inside the container: + +| Context | Variable | Purpose | +|---|---|---| +| Host | `CLAUDE_CONTAINER_OAUTH_TOKEN` | Token stored as an environment variable in your shell | +| Container | `CLAUDE_CODE_OAUTH_TOKEN` | Token injected into the container, consumed by `claude -p` | + +### How they connect + +In the `Makefile`, the host variable is defined as: + +```makefile +HOST_TOKEN_VAR := CLAUDE_CONTAINER_OAUTH_TOKEN +``` + +And mapped to the container via the `-e` flag: + +```makefile +-e CLAUDE_CODE_OAUTH_TOKEN=$${$(HOST_TOKEN_VAR)} +``` + +This takes the value of `CLAUDE_CONTAINER_OAUTH_TOKEN` on the host and injects it as `CLAUDE_CODE_OAUTH_TOKEN` inside the container. The Claude CLI reads this variable automatically on startup. + +### Why two separate variables + +- **Session isolation:** The container session is independent from the host session. If an agent fails or its token expires, your local Claude Code session is not affected. +- **Credential collision prevention:** Prevents the container from overwriting or interfering with the credential files in `~/.claude/` on the host. + +--- + +## Environment Setup + +Add the following variables to your `~/.zshrc` or `~/.bashrc`: + +```bash +# OAuth token for agent authentication in containers +export CLAUDE_CONTAINER_OAUTH_TOKEN= + +# Directory where agent worktrees will be stored +export AGENTS_HOME=~/agents +``` + +Apply the changes: + +```bash +source ~/.zshrc # or source ~/.bashrc +``` + +Verify the variables are set: + +```bash +echo $CLAUDE_CONTAINER_OAUTH_TOKEN +echo $AGENTS_HOME +``` + +--- + +## How to Obtain `CLAUDE_CODE_OAUTH_TOKEN` + +Run the following command in your terminal: + +```bash +claude setup-token +``` + +This generates the OAuth token and displays it in the output. Copy it and export it: + +```bash +export CLAUDE_CONTAINER_OAUTH_TOKEN= +``` + +To make it persistent, add the line above to your `~/.zshrc` or `~/.bashrc`. diff --git a/docs/agents/spawn-agent-skill.md b/docs/agents/spawn-agent-skill.md index b7f79e2..4bb3860 100644 --- a/docs/agents/spawn-agent-skill.md +++ b/docs/agents/spawn-agent-skill.md @@ -1,12 +1,12 @@ -# spawn-agent — Coordinación de Agentes Virtuales +# spawn-agent — Virtual Agent Coordination -## Visión general +## Overview -`spawn-agent` es un skill de Claude Code que convierte al host en un **coordinador de agentes virtuales**. Cada agente virtual es un contenedor Apple Container que corre Claude en modo headless (`claude -p`) dentro de un git worktree aislado, y reporta su progreso a través de `container logs`. +`spawn-agent` is a Claude Code skill that turns the host into a **virtual agent coordinator**. Each virtual agent is an Apple Container running Claude in headless mode (`claude -p`) inside an isolated git worktree, and reports its progress through `container logs`. ``` ┌─────────────────────────────────────────────────────────────┐ -│ Host (coordinador) │ +│ Host (coordinator) │ │ Claude Code + spawn-agent skill │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ @@ -19,122 +19,122 @@ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ ↑ ↑ ↑ │ │ └──────────────────┴──────────────────┘ │ -│ container logs (contexto) │ +│ container logs (context) │ │ │ │ $AGENTS_HOME/ │ -│ ├── feat/oauth2/ ← worktree persiste post-container │ +│ ├── feat/oauth2/ ← worktree persists post-container │ │ ├── test/payment-service/ │ │ └── mutation/api/ │ └─────────────────────────────────────────────────────────────┘ ``` -### Por qué worktrees dentro del contenedor +### Why worktrees inside the container -Git worktrees deben crearse desde dentro de un contexto donde el repositorio sea accesible. El contenedor monta el repo principal en `/workspace` y los worktrees en `/worktrees`. El `entrypoint.sh` ejecuta `git -C /workspace worktree add /worktrees/` antes de lanzar Claude, garantizando aislamiento completo entre agentes. +Git worktrees must be created from within a context where the repository is accessible. The container mounts the main repo at `/workspace` and the worktrees at `/worktrees`. The `entrypoint.sh` runs `git -C /workspace worktree add /worktrees/` before launching Claude, ensuring complete isolation between agents. --- -## Prerrequisitos +## Prerequisites -### 1. Imagen Docker +### 1. Docker Image ```bash cd /path/to/project/config make build ``` -Verifica que exista: +Verify it exists: ```bash container image list | grep "claude-agent.*wolfi" ``` -### 2. Variables de entorno (una vez en `~/.zshrc` o `~/.bashrc`) +### 2. Environment variables (once in `~/.zshrc` or `~/.bashrc`) ```bash -# Directorio donde se almacenarán los worktrees de los agentes -export AGENTS_HOME=~/agents # o cualquier ruta persistente +# Directory where agent worktrees will be stored +export AGENTS_HOME=~/agents # or any persistent path -# Token OAuth para que Claude autentique dentro del contenedor -# ⚠️ Distinto del token de tu sesión host — evita colisiones -export CLAUDE_CONTAINER_OAUTH_TOKEN= +# OAuth token for Claude to authenticate inside the container +# ⚠️ Different from your host session token — avoids collisions +export CLAUDE_CONTAINER_OAUTH_TOKEN= ``` -> **Por qué dos tokens?** Claude Code usa `~/.claude/` del host para la sesión interactiva. Los contenedores reciben el token vía variable de entorno, evitando que dos instancias de Claude compitan por el mismo estado de sesión. +> **Why two tokens?** Claude Code uses the host's `~/.claude/` for the interactive session. Containers receive the token via environment variable, preventing two Claude instances from competing for the same session state. --- -## Flujo principal +## Main Flow ``` -1. Usuario le pide a Claude una tarea → skill se activa automáticamente +1. User asks Claude for a task → skill activates automatically │ ▼ -2. Claude verifica AGENTS_HOME y CLAUDE_CONTAINER_OAUTH_TOKEN - │ (si faltan → muestra qué exportar) +2. Claude verifies AGENTS_HOME and CLAUDE_CONTAINER_OAUTH_TOKEN + │ (if missing → shows what to export) ▼ -3. Claude determina el tipo de agente (feature / test / mutation / explore) - y construye el prompt adecuado +3. Claude determines the agent type (feature / test / mutation / explore) + and builds the appropriate prompt │ ▼ -4. Claude calcula variables de ruta: +4. Claude computes path variables: GIT_ROOT = git rev-parse --show-toplevel PROJECT_NAME = basename $GIT_ROOT CONTAINER_NAME = ${PROJECT_NAME}-$(echo $BRANCH | tr '/_ ' '-' | tr A-Z a-z) │ ▼ -5. container run -d --rm ← detached (no bloquea) - • -v $GIT_ROOT:/workspace ← repo completo (read/write) - • -v $AGENTS_HOME:/worktrees ← destino de worktrees - • --worktree $BRANCH → entrypoint crea el worktree - • --task "$TASK" → claude -p "$TASK" en el worktree +5. container run -d --rm ← detached (non-blocking) + • -v $GIT_ROOT:/workspace ← full repo (read/write) + • -v $AGENTS_HOME:/worktrees ← worktree destination + • --worktree $BRANCH → entrypoint creates the worktree + • --task "$TASK" → claude -p "$TASK" in the worktree │ ▼ -6. Dentro del contenedor (entrypoint.sh): - a) Copia credenciales desde mounts host → /root/.claude/ +6. Inside the container (entrypoint.sh): + a) Copies credentials from host mounts → /root/.claude/ b) git -C /workspace worktree add /worktrees/$BRANCH -b $BRANCH c) cd /worktrees/$BRANCH - d) Copia credenciales a /home/agent/.claude/ + chown agent + d) Copies credentials to /home/agent/.claude/ + chown agent e) su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p "$TASK" - (Claude requiere uid != 0 para usar --dangerously-skip-permissions) + (Claude requires uid != 0 to use --dangerously-skip-permissions) │ ▼ -7. Claude en el agente trabaja autónomamente: - lee codebase → implementa → commitea → sale +7. Claude in the agent works autonomously: + reads codebase → implements → commits → exits │ ▼ -8. Coordinador puede leer progreso en tiempo real: +8. Coordinator can read progress in real time: container logs -f ${CONTAINER_NAME} │ ▼ -9. Al terminar: contenedor se elimina (--rm), worktree persiste en AGENTS_HOME +9. On completion: container is removed (--rm), worktree persists in AGENTS_HOME ``` --- -## Tipos de agente y prompts automáticos +## Agent Types and Automatic Prompts -El skill construye el prompt según el tipo detectado de la petición del usuario: +The skill builds the prompt based on the type detected from the user's request: -### `feature` — nueva funcionalidad +### `feature` — new functionality -**Cuándo**: el usuario pide implementar algo nuevo. +**When**: the user asks to implement something new. ``` You are a senior software engineer. Implement the following in this codebase: - + Requirements: - Write clean, tested, production-ready code - Follow existing conventions (read the codebase first) - Create a git commit when done with a descriptive message ``` -### `test` — pruebas unitarias +### `test` — unit tests -**Cuándo**: el usuario pide escribir o mejorar tests. +**When**: the user asks to write or improve tests. ``` You are a senior QA engineer. Your task: - + Requirements: - Identify untested or poorly tested code - Write comprehensive unit tests @@ -145,11 +145,11 @@ Requirements: ### `mutation` — mutation testing -**Cuándo**: el usuario pide mutation testing o análisis de cobertura de tests. +**When**: the user asks for mutation testing or test coverage analysis. ``` You are a mutation testing expert. Your task: - + Requirements: - Analyze existing tests for weak assertions - Introduce mutations and verify tests catch them @@ -160,24 +160,24 @@ Requirements: ### `explore` / general -**Cuándo**: cualquier otra tarea de código. +**When**: any other code task. ``` You are a senior software engineer. Your task: - + Work autonomously, read the codebase as needed, and commit any changes. ``` --- -## Nomenclatura de contenedores +## Container Naming -El nombre del contenedor se deriva automáticamente del proyecto y la rama: +The container name is automatically derived from the project and branch: ``` CONTAINER_NAME = - -donde: +where: PROJECT_NAME = basename $(git rev-parse --show-toplevel) CONTAINER_BRANCH = echo $BRANCH | tr '/_ ' '-' | tr '[:upper:]' '[:lower:]' ``` @@ -188,27 +188,27 @@ donde: | `test/payment-service` | `stackai` | `stackai-test-payment-service` | | `mutation/API_v2` | `stackai` | `stackai-mutation-api-v2` | -> **Regla de sanitización**: cada `/`, `_` o espacio se convierte en un único `-`, y se pasa a minúsculas. Se usa `tr '/_ ' '-'` (no `'---'`) para garantizar reemplazo 1:1. +> **Sanitization rule**: every `/`, `_`, or space is converted to a single `-`, and the result is lowercased. `tr '/_ ' '-'` is used (not `'---'`) to ensure a 1:1 replacement. --- -## Ejemplo completo — feature agent +## Full Example — Feature Agent -### Escenario -Queremos implementar OAuth2 con JWT en la API, en rama `feat/oauth2`, sin tocar el branch `main`. +### Scenario +We want to implement OAuth2 with JWT in the API, on branch `feat/oauth2`, without touching the `main` branch. -### 1. Invocar al coordinador +### 1. Invoke the coordinator ``` "Spawn an agent to implement OAuth2 authentication with JWT tokens. Branch: feat/oauth2" ``` -El skill se activa automáticamente. +The skill activates automatically. -### 2. Lo que Claude ejecuta +### 2. What Claude executes ```bash -# Verificación de vars +# Variable verification test -n "$CLAUDE_CONTAINER_OAUTH_TOKEN" || echo "ERROR: export CLAUDE_CONTAINER_OAUTH_TOKEN=" test -n "$AGENTS_HOME" || echo "ERROR: export AGENTS_HOME=" @@ -221,14 +221,14 @@ CONTAINER_BRANCH=$(echo "$BRANCH" | tr '/_ ' '-' | tr '[:upper:]' '[:lower:]') CONTAINER_NAME="${PROJECT_NAME}-${CONTAINER_BRANCH}" # => stackai-feat-oauth2 -# Red (macOS 26+) +# Network (macOS 26+) container network list --format json 2>/dev/null | grep -q '"claude-agent-net"' \ || container network create --subnet 192.168.100.0/24 claude-agent-net -# Directorio de worktrees +# Worktrees directory mkdir -p "${AGENTS_HOME}" -# Lanzar agente +# Launch agent TASK="You are a senior software engineer. Implement the following in this codebase: Implement OAuth2 authentication with JWT tokens in the API. Requirements: @@ -249,97 +249,97 @@ container run -d --rm \ claude-agent:wolfi \ --worktree "feat/oauth2" --task "${TASK}" -# Confirmar +# Confirm container list | grep "stackai-feat-oauth2" ``` -### 3. Monitorear progreso +### 3. Monitor progress ```bash -# Últimas 100 líneas (snapshot) +# Last 100 lines (snapshot) container logs -n 100 stackai-feat-oauth2 -# En tiempo real +# Real time container logs -f stackai-feat-oauth2 ``` -Claude resume los logs y te explica en qué paso está el agente. +Claude summarizes the logs and explains what step the agent is at. -### 4. Resultado +### 4. Result -Al terminar, el agente habrá: -- Creado la rama `feat/oauth2` -- Implementado OAuth2 + JWT en la rama -- Hecho commit con mensaje descriptivo -- Salido (contenedor eliminado automáticamente) +On completion, the agent will have: +- Created the `feat/oauth2` branch +- Implemented OAuth2 + JWT on the branch +- Made a commit with a descriptive message +- Exited (container automatically removed) -El worktree persiste en `$AGENTS_HOME/feat/oauth2/` para que puedas revisar el código. +The worktree persists in `$AGENTS_HOME/feat/oauth2/` so you can review the code. -### 5. Revisar y mergear +### 5. Review and merge ```bash -# Ver los commits del agente +# View the agent's commits git -C "$AGENTS_HOME/feat/oauth2" log --oneline -10 -# Diff contra main +# Diff against main git -C "$GIT_ROOT" diff main..feat/oauth2 --stat -# Mergear si estás satisfecho +# Merge if satisfied git -C "$GIT_ROOT" merge feat/oauth2 -# Limpiar worktree +# Clean up worktree git -C "$GIT_ROOT" worktree remove --force "$AGENTS_HOME/feat/oauth2" rm -rf "$AGENTS_HOME/feat/oauth2" ``` --- -## Referencia de operaciones +## Operations Reference -### Listar agentes activos +### List active agents ``` "Show me what agents are currently running" ``` -Claude ejecuta: +Claude executes: ```bash container list | grep "${PROJECT_NAME}" ls -la "${AGENTS_HOME}" ``` -### Monitorear un agente específico +### Monitor a specific agent ``` "What is the feat/oauth2 agent doing?" ``` -Claude ejecuta `container logs -n 100 stackai-feat-oauth2` y te da un resumen en lenguaje natural. +Claude executes `container logs -n 100 stackai-feat-oauth2` and gives you a natural language summary. -### Detener un agente +### Stop an agent ``` "Stop the feat/oauth2 agent" ``` -Claude ejecuta: +Claude executes: ```bash container stop stackai-feat-oauth2 ``` -Opcionalmente limpia el worktree si lo pides. +Optionally cleans up the worktree if you ask. -### Lanzar múltiples agentes en paralelo +### Launch multiple agents in parallel ``` "Spawn three agents: one for OAuth, one for tests on auth, one for mutation testing on payments" ``` -Claude lanza los tres contenedores en secuencia (detached), cada uno con su propia rama y prompt. +Claude launches the three containers in sequence (detached), each with its own branch and prompt. --- -## Referencia Apple Container CLI +## Apple Container CLI Reference ``` container run -d --rm --name --network --cpus --memory G @@ -352,18 +352,18 @@ container network list [--format json|table] container network create --subnet ``` -Docs completos: https://github.com/apple/container/blob/main/docs/command-reference.md +Full docs: https://github.com/apple/container/blob/main/docs/command-reference.md --- -## Solución de problemas +## Troubleshooting -| Problema | Causa | Solución | +| Problem | Cause | Solution | |---|---|---| -| `ERROR: export AGENTS_HOME` | Variable no seteada | `export AGENTS_HOME=~/agents` en `~/.zshrc` | -| `ERROR: export CLAUDE_CONTAINER_OAUTH_TOKEN` | Token no seteado | `export CLAUDE_CONTAINER_OAUTH_TOKEN=` | -| `Image not found: claude-agent:wolfi` | Imagen no construida | `cd config && make build` | -| `--dangerously-skip-permissions cannot be used with root` | Imagen vieja sin usuario `agent` | `cd config && make build` para reconstruir | -| Worktree creation failed (branch + dir ya existen) | Intento anterior dejó restos | `git worktree prune && git branch -D && rm -rf $AGENTS_HOME/` | -| Container exits immediately | Error en entrypoint | `container logs ` para ver el error (sin `--rm` para preservar logs) | -| Nombre de contenedor duplicado | Agente ya corriendo | `container list` para verificar; `container stop ` para liberarlo | +| `ERROR: export AGENTS_HOME` | Variable not set | `export AGENTS_HOME=~/agents` in `~/.zshrc` | +| `ERROR: export CLAUDE_CONTAINER_OAUTH_TOKEN` | Token not set | `export CLAUDE_CONTAINER_OAUTH_TOKEN=` | +| `Image not found: claude-agent:wolfi` | Image not built | `cd config && make build` | +| `--dangerously-skip-permissions cannot be used with root` | Old image without `agent` user | `cd config && make build` to rebuild | +| Worktree creation failed (branch + dir already exist) | Previous attempt left remnants | `git worktree prune && git branch -D && rm -rf $AGENTS_HOME/` | +| Container exits immediately | Error in entrypoint | `container logs ` to see the error (without `--rm` to preserve logs) | +| Duplicate container name | Agent already running | `container list` to verify; `container stop ` to free it |