cli-error 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. cli_error-0.1.0/.claude/scheduled_tasks.lock +1 -0
  2. cli_error-0.1.0/.flake8 +3 -0
  3. cli_error-0.1.0/.github/workflows/ci.yml +38 -0
  4. cli_error-0.1.0/.github/workflows/release.yml +28 -0
  5. cli_error-0.1.0/.gitignore +26 -0
  6. cli_error-0.1.0/.mcp.json +12 -0
  7. cli_error-0.1.0/.sandbox/.env.example +20 -0
  8. cli_error-0.1.0/.sandbox/README.md +114 -0
  9. cli_error-0.1.0/.sandbox/base/Dockerfile +42 -0
  10. cli_error-0.1.0/.sandbox/base/tmux.conf +112 -0
  11. cli_error-0.1.0/.sandbox/base/zshrc +9 -0
  12. cli_error-0.1.0/.sandbox/docker-compose.yml +28 -0
  13. cli_error-0.1.0/.sandbox/run.sh +42 -0
  14. cli_error-0.1.0/.sandbox/scripts/_lib.sh +23 -0
  15. cli_error-0.1.0/.sandbox/scripts/attach.sh +6 -0
  16. cli_error-0.1.0/.sandbox/scripts/build.sh +44 -0
  17. cli_error-0.1.0/.sandbox/scripts/claude.sh +6 -0
  18. cli_error-0.1.0/.sandbox/scripts/shell.sh +31 -0
  19. cli_error-0.1.0/.sandbox/variants/python/Dockerfile +30 -0
  20. cli_error-0.1.0/.sandbox/variants/python/compose.override.yml +16 -0
  21. cli_error-0.1.0/.sandbox/worktree_run.sh +4 -0
  22. cli_error-0.1.0/.sandbox/worktree_shell.sh +4 -0
  23. cli_error-0.1.0/.tasker/.closed +16 -0
  24. cli_error-0.1.0/.tasker/.gitignore +4 -0
  25. cli_error-0.1.0/.tasker/.recent +1 -0
  26. cli_error-0.1.0/.tasker/archive/.gitkeep +0 -0
  27. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/README.md +61 -0
  28. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t01-rename-package-structure-clierror-cliexit.md +43 -0
  29. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t02-fluent-builder-for-clierror-context.md +41 -0
  30. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t03-render-formatted-error.md +42 -0
  31. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t04-error-handler-errorreporter-context-manager.md +38 -0
  32. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t05-cause-chain-rendering.md +35 -0
  33. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t06-debug-traceback.md +36 -0
  34. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t07-console-creation-with-style-overrides.md +40 -0
  35. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t08-library-documentation-readme.md +36 -0
  36. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t09-deployment-setup-ci-release-workflows.md +36 -0
  37. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t10-unify-markup-construction-template-args.md +47 -0
  38. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t11-refine-detail-single-last-wins.md +44 -0
  39. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t12-revisit-props-markup-first-prop.md +62 -0
  40. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t13-store-detail-as-ready-to.md +33 -0
  41. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t14-handle-empty-message-cause-in.md +53 -0
  42. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t15-reconsider-derive-debug-stderr-console.md +42 -0
  43. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t16-optionally-show-locals-in-debug.md +41 -0
  44. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t17-test-debug-traceback-includes-the.md +32 -0
  45. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t18-add-debug-gated-diagnostic-helpers.md +49 -0
  46. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t19-consolidate-shared-test-helpers-across.md +47 -0
  47. cli_error-0.1.0/.tasker/s01-extract-repo-skills-error-reporting/s01t20-add-a-safe-post-construction.md +31 -0
  48. cli_error-0.1.0/CLAUDE.md +24 -0
  49. cli_error-0.1.0/CONTEXT.md +15 -0
  50. cli_error-0.1.0/PKG-INFO +6 -0
  51. cli_error-0.1.0/README.md +287 -0
  52. cli_error-0.1.0/docs/adr/0001-error-formatting-and-theming.md +72 -0
  53. cli_error-0.1.0/docs/adr/0002-cli-reporter-output-facade.md +72 -0
  54. cli_error-0.1.0/docs/agents/dev-loop.md +50 -0
  55. cli_error-0.1.0/docs/agents/task-tracker.md +53 -0
  56. cli_error-0.1.0/pyproject.toml +34 -0
  57. cli_error-0.1.0/src/cli_error/__init__.py +21 -0
  58. cli_error-0.1.0/src/cli_error/_console.py +40 -0
  59. cli_error-0.1.0/src/cli_error/_errors.py +92 -0
  60. cli_error-0.1.0/src/cli_error/_render.py +41 -0
  61. cli_error-0.1.0/src/cli_error/_reporter.py +108 -0
  62. cli_error-0.1.0/src/cli_error/py.typed +0 -0
  63. cli_error-0.1.0/tests/__init__.py +0 -0
  64. cli_error-0.1.0/tests/test_console.py +144 -0
  65. cli_error-0.1.0/tests/test_errors.py +279 -0
  66. cli_error-0.1.0/tests/test_render.py +292 -0
  67. cli_error-0.1.0/tests/test_reporter.py +355 -0
  68. cli_error-0.1.0/tox.ini +22 -0
  69. cli_error-0.1.0/uv.lock +589 -0
@@ -0,0 +1 @@
1
+ {"sessionId":"d6c578d2-0e67-41a3-9ab3-6eb663d5b9d5","pid":25,"procStart":"26355062","acquiredAt":1783286663093}
@@ -0,0 +1,3 @@
1
+ [flake8]
2
+ max-line-length = 88
3
+ extend-ignore = E203, W503
@@ -0,0 +1,38 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ name: tox
15
+ runs-on: ubuntu-latest
16
+ env:
17
+ UV_PYTHON: ${{ matrix.python-version }}
18
+ strategy:
19
+ matrix:
20
+ python-version: ["3.12", "3.13", "3.14"]
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up uv
26
+ uses: astral-sh/setup-uv@v5
27
+ with:
28
+ enable-cache: true
29
+ cache-dependency-glob: uv.lock
30
+
31
+ - name: Install Python ${{ matrix.python-version }}
32
+ run: uv python install ${{ matrix.python-version }}
33
+
34
+ - name: Sync dev dependencies
35
+ run: uv sync --group dev
36
+
37
+ - name: Run tox
38
+ run: uv run tox
@@ -0,0 +1,28 @@
1
+ name: release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ name: Publish to PyPI
10
+ runs-on: ubuntu-latest
11
+ environment: Production
12
+ permissions:
13
+ id-token: write
14
+ contents: read
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up uv
19
+ uses: astral-sh/setup-uv@v5
20
+ with:
21
+ enable-cache: true
22
+ cache-dependency-glob: uv.lock
23
+
24
+ - name: Build package
25
+ run: uv build
26
+
27
+ - name: Publish to PyPI
28
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+
9
+ # Tools
10
+ .mypy_cache/
11
+ .pytest_cache/
12
+
13
+ # tox
14
+ .tox/
15
+
16
+ # local configs
17
+ .env
18
+ *.local.json
19
+ .worktrees
20
+
21
+ # packaging / testing extras
22
+ *$py.class
23
+ *.egg
24
+ .eggs/
25
+ .coverage
26
+ htmlcov/
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "tasker": {
4
+ "type": "stdio",
5
+ "command": "tasker",
6
+ "args": [
7
+ "mcp"
8
+ ],
9
+ "env": {}
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,20 @@
1
+ # Host identity — container files will be owned by this UID/GID.
2
+ # Populate with: id -u, id -g, echo $USER
3
+ HOST_UID=1000
4
+ HOST_GID=1000
5
+ USER=yourname
6
+
7
+ # Compose project namespace. Controls per-sandbox variant/volume naming:
8
+ # <COMPOSE_PROJECT_NAME>-<VARIANT>:latest
9
+ COMPOSE_PROJECT_NAME=app-error-sandbox
10
+
11
+ # Base image name. Shared across all sandboxes (independent of
12
+ # COMPOSE_PROJECT_NAME), so rebuilding it once updates every sandbox.
13
+ # Leave this alone unless you deliberately want a private, unshared base.
14
+ # BASE_IMAGE=agent-sandbox-base:latest
15
+
16
+ # Active variant — uncomment exactly ONE.
17
+ VARIANT=python
18
+
19
+ # Language versions (used only by the matching variant).
20
+ PYTHON_VERSION="3.10 3.11 3.12"
@@ -0,0 +1,114 @@
1
+ # Agent Sandbox
2
+
3
+ A Docker-based environment for running Claude Code with persistent authentication,
4
+ a shared project workspace, and a pluggable variant system for different language stacks.
5
+
6
+ See the top-level [`README.md`](../README.md) for WSL setup and a variants overview.
7
+
8
+ The shared base image ships `uv` and the `mcp-tasker` MCP server, so they're
9
+ available in every variant regardless of language stack.
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ sandbox/
15
+ .env.example template for .env (copy and edit)
16
+ docker-compose.yml base compose file (variant-agnostic)
17
+ run.sh one-shot: build (if needed) → up → claude
18
+ base/
19
+ Dockerfile generic dev image (ubuntu + tmux/zsh/nvim/node/claude + uv/mcp-tasker)
20
+ zshrc, tmux.conf mounted read-only into the container
21
+ variants/
22
+ python/ Dockerfile + compose.override.yml (uv + poetry)
23
+ typescript/ Dockerfile + compose.override.yml (node TS toolchain)
24
+ php/ Dockerfile + compose.override.yml
25
+ minimal/ Dockerfile + compose.override.yml (no language stack)
26
+ scripts/
27
+ build.sh builds base image, then the active variant
28
+ attach.sh `tmux attach -t main`
29
+ shell.sh plain zsh shell
30
+ claude.sh `claude --dangerously-skip-permissions`
31
+ _lib.sh sourced helper (loads .env, sets COMPOSE_FILE)
32
+ ```
33
+
34
+ ## First-time setup
35
+
36
+ ```bash
37
+ cd sandbox
38
+ cp .env.example .env
39
+ # Edit .env: set HOST_UID, HOST_GID, USER, COMPOSE_PROJECT_NAME, VARIANT
40
+ ./run.sh
41
+ ```
42
+
43
+ `run.sh` builds the base image + active variant on first invocation, starts
44
+ the container, and drops you into Claude Code. Subsequent runs skip the
45
+ build when the images already exist.
46
+
47
+ Claude credentials live in `~/.claude` / `~/.claude.json` on the host (bind-mounted
48
+ into the container), so authentication persists across container rebuilds.
49
+
50
+ The host's `~/repo-skills` is bind-mounted **read-only** at `~/repo-skills` inside
51
+ the container as a reference source, so the sandbox can consult it without
52
+ modifying it or leaking container-generated files back onto the host.
53
+
54
+ ## Daily usage
55
+
56
+ ```bash
57
+ cd sandbox
58
+ ./run.sh # ensure running, then run claude
59
+ ./scripts/attach.sh # attach to the tmux session inside the container
60
+ ./scripts/shell.sh # plain zsh shell inside the container
61
+ ./scripts/claude.sh # another claude instance in the running container
62
+ ```
63
+
64
+ The project directory (`..` relative to `sandbox/`) is mounted at `/work` inside
65
+ the container and reflects host changes in real time.
66
+
67
+ Press `Ctrl+B, D` to detach from tmux without stopping the container.
68
+
69
+ ## Switching variants
70
+
71
+ ```bash
72
+ # Edit VARIANT in .env, then:
73
+ docker compose down # stop the old variant
74
+ ./run.sh # build+start the new one
75
+ ```
76
+
77
+ Each variant's image is tagged `<COMPOSE_PROJECT_NAME>-<variant>:latest`, so
78
+ switching doesn't rebuild unnecessarily. The base image (`BASE_IMAGE`, default
79
+ `agent-sandbox-base:latest`) is *shared* across all sandboxes regardless of
80
+ `COMPOSE_PROJECT_NAME`, so it's built once and reused everywhere.
81
+
82
+ ## Stopping
83
+
84
+ ```bash
85
+ docker compose stop # pause, keep state
86
+ docker compose down # remove containers, keep volumes (auth + caches)
87
+ docker compose down -v # nuke everything including caches and venvs
88
+ ```
89
+
90
+ Named volumes owned by a variant (e.g. `venv`, `tox`, `uv-cache`, `uv-data`,
91
+ `poetry-cache`, `node-modules`, `vendor`, `composer-cache`) live under the
92
+ compose project namespace — you can list them
93
+ with `docker volume ls | grep "$COMPOSE_PROJECT_NAME"`.
94
+
95
+ ## Rebuilding
96
+
97
+ After changing a `Dockerfile` or `claude_install.sh`:
98
+
99
+ ```bash
100
+ ./scripts/build.sh
101
+ docker compose up -d --force-recreate
102
+ ```
103
+
104
+ `build.sh` builds the shared base image only when it's missing, then builds the
105
+ active variant. To refresh the shared base itself (a no-cache rebuild that
106
+ updates *every* sandbox using it), pass `--force`:
107
+
108
+ ```bash
109
+ ./scripts/build.sh --force
110
+ ```
111
+
112
+ ## Adding a variant
113
+
114
+ See the top-level README's "Adding a new variant" section.
@@ -0,0 +1,42 @@
1
+ FROM ubuntu:26.04
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ curl ca-certificates git zsh neovim tmux ncurses-term \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ ARG UID=1000
10
+ ARG GID=1000
11
+ ARG USERNAME=agent
12
+
13
+ # Ubuntu 26.04 ships a pre-created `ubuntu` user at 1000:1000. Remove it so the
14
+ # host's UID/GID can be reused without collision.
15
+ RUN (userdel -r ubuntu && groupdel ubuntu) 2>/dev/null || true \
16
+ && groupadd -g ${GID} ${USERNAME} \
17
+ && useradd -m -u ${UID} -g ${GID} -s /bin/zsh ${USERNAME}
18
+
19
+ RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
20
+ && apt-get install -y --no-install-recommends nodejs \
21
+ && rm -rf /var/lib/apt/lists/*
22
+
23
+ USER ${USERNAME}
24
+ ENV USERNAME=${USERNAME}
25
+ ENV PATH="/home/${USERNAME}/.local/bin:${PATH}"
26
+
27
+ RUN curl -fsSL https://claude.ai/install.sh | bash
28
+
29
+ # uv is shared across all variants — it manages Python interpreters and is also
30
+ # the installer for Python-based tooling (e.g. mcp-tasker), so it belongs in the
31
+ # base image rather than any single variant.
32
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh
33
+
34
+ # mcp-tasker is the task-tracker MCP server; make it available everywhere.
35
+ RUN uv tool install mcp-tasker
36
+
37
+ RUN git config --global user.name "Claude Agent" \
38
+ && git config --global user.email "agent@claude.ai"
39
+
40
+ WORKDIR /work
41
+
42
+ CMD ["tmux", "new-session", "-A", "-s", "main"]
@@ -0,0 +1,112 @@
1
+ # link: https://hamvocke.com/blog/a-guide-to-customizing-your-tmux-conf/
2
+
3
+ # command shortcuts:
4
+ # set-option -> set
5
+ # bind-key -> bind
6
+ # set-window-option -> setw "set -w"
7
+ # list-sessions -> ls
8
+
9
+ # useful shortcuts:
10
+ # C-q - prefix (default: 'C-b')
11
+ # r - reload tmux config
12
+ # windows:
13
+ # c - create window
14
+ # n - switch to next window
15
+ # R - rename window title (default: ',')
16
+ # panes:
17
+ # C-o - switch panes
18
+ # - - split horizontally (default: '"')
19
+ # \ or | - split vertically (default: '%')
20
+ # window content and history
21
+ # [ - enter meta mode (Shift-A will exit this mode)
22
+ # v - start selection
23
+ # y - copy selected
24
+
25
+ # add more accessible prefix 'Ctrl+Q' but keep the old one as fallback
26
+ set -g prefix C-b
27
+ bind C-b send-prefix
28
+
29
+ set -g prefix2 C-q
30
+ bind C-q send-prefix -2
31
+
32
+ # split panes using | and -
33
+ bind | split-window -h
34
+ bind "\\" split-window -h # don't need to press `Shift`
35
+ bind - split-window -v
36
+
37
+ # config reload
38
+ bind r source-file ~/.config/tmux/tmux.conf
39
+
40
+ # start numbering from 1
41
+ set -g base-index 1
42
+
43
+ # switch panes using Alt-arrow without prefix
44
+ bind -n M-Left select-pane -L
45
+ bind -n M-Right select-pane -R
46
+ bind -n M-Up select-pane -U
47
+ bind -n M-Down select-pane -D
48
+
49
+ # allow to run vim over pane content
50
+ setw -g mode-keys vi
51
+ bind -T copy-mode-vi 'v' send-keys -X begin-selection
52
+ bind -T copy-mode-vi 'y' send-keys -X copy-selection
53
+
54
+ # enable mouse control
55
+ set -g mouse on
56
+
57
+ # NVIM advices:
58
+ set -g focus-events on
59
+ # enable truecolor feature (required by NVIM's `vim.opt.termguicolors` option)
60
+ set -g default-terminal "xterm-256color"
61
+ set -as terminal-features '*:RGB'
62
+
63
+ # don't rename windows automatically (use "C-b ," or "C-q R" to rename window title)
64
+ set -g allow-rename off
65
+ bind R command-prompt -I "#W" "rename-window '%%'"
66
+
67
+ # include scrollback history
68
+ set -g history-limit 3000
69
+
70
+
71
+ # DESIGN TWEAKS
72
+
73
+ # don't do anything when a 'bell' rings
74
+ set -g visual-activity off
75
+ set -g visual-bell off
76
+ set -g visual-silence off
77
+ setw -g monitor-activity off
78
+ set -g bell-action none
79
+
80
+ # clock mode
81
+ setw -g clock-mode-colour yellow
82
+
83
+ # copy mode
84
+ setw -g mode-style 'fg=black bg=red bold'
85
+
86
+ # panes
87
+ set -g pane-border-style 'fg=red'
88
+ set -g pane-active-border-style 'fg=yellow'
89
+
90
+ # statusbar
91
+ set -g status-position bottom
92
+ set -g status-justify left
93
+ set -g status-style 'fg=red'
94
+
95
+ set -g status-left ''
96
+ set -g status-left-length 10
97
+
98
+ set -g status-right-style 'fg=black bg=yellow'
99
+ set -g status-right '%Y-%m-%d %H:%M '
100
+ set -g status-right-length 50
101
+
102
+ setw -g window-status-current-style 'fg=black bg=red'
103
+ setw -g window-status-current-format ' #I #W #F '
104
+
105
+ setw -g window-status-style 'fg=red bg=black'
106
+ setw -g window-status-format ' #I #[fg=white]#W #[fg=yellow]#F '
107
+
108
+ setw -g window-status-bell-style 'fg=yellow bg=red bold'
109
+
110
+ # messages
111
+ set -g message-style 'fg=yellow bg=red bold'
112
+
@@ -0,0 +1,9 @@
1
+ # PATH
2
+ export PATH="$HOME/.local/bin:$PATH"
3
+
4
+ # Pretty prompt: user@host:dir (branch)$
5
+ autoload -Uz vcs_info
6
+ precmd() { vcs_info }
7
+ zstyle ':vcs_info:git:*' formats ' (%b)'
8
+ setopt PROMPT_SUBST
9
+ PROMPT='%F{green}%n@%m%f:%F{blue}%~%f%F{yellow}${vcs_info_msg_0_}%f%# '
@@ -0,0 +1,28 @@
1
+ services:
2
+ claude-code:
3
+ build:
4
+ context: variants/${VARIANT}
5
+ args:
6
+ - BASE_IMAGE=${BASE_IMAGE}
7
+ - USERNAME=${USER}
8
+ - UID=${HOST_UID:-1000}
9
+ - GID=${HOST_GID:-1000}
10
+ - PYTHON_VERSION=${PYTHON_VERSION:-3.10 3.11 3.12}
11
+ - PHP_VERSION=${PHP_VERSION:-8.3}
12
+ image: ${COMPOSE_PROJECT_NAME}-${VARIANT}:latest
13
+ stdin_open: true
14
+ tty: true
15
+ volumes:
16
+ - ..:/work
17
+ - npm-cache:/home/${USER}/.npm
18
+ - ./base/tmux.conf:/home/${USER}/.tmux.conf:ro
19
+ - ./base/zshrc:/home/${USER}/.zshrc:ro
20
+ - ~/.claude:/home/${USER}/.claude
21
+ - ~/.claude.json:/home/${USER}/.claude.json
22
+ - ~/repo-skills:/home/${USER}/repo-skills:ro
23
+ working_dir: /work
24
+ environment:
25
+ - TERM=xterm-256color
26
+
27
+ volumes:
28
+ npm-cache:
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # One-shot: ensure images are built, container is up, then drop into claude.
3
+ set -euo pipefail
4
+
5
+ cd "$(dirname "$0")"
6
+ # shellcheck source=scripts/_lib.sh
7
+ source scripts/_lib.sh
8
+
9
+ WORKDIR=""
10
+ ARGS=()
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --workdir)
14
+ WORKDIR="$2"
15
+ shift 2
16
+ ;;
17
+ --workdir=*)
18
+ WORKDIR="${1#*=}"
19
+ shift
20
+ ;;
21
+ *)
22
+ ARGS+=("$1")
23
+ shift
24
+ ;;
25
+ esac
26
+ done
27
+
28
+ WORKDIR_ARGS=()
29
+ if [[ -n "$WORKDIR" ]]; then
30
+ WORKDIR_ARGS=(--workdir "$WORKDIR")
31
+ fi
32
+
33
+ VARIANT_IMAGE="${COMPOSE_PROJECT_NAME}-${VARIANT}:latest"
34
+
35
+ if ! docker image inspect "${BASE_IMAGE}" >/dev/null 2>&1 \
36
+ || ! docker image inspect "${VARIANT_IMAGE}" >/dev/null 2>&1; then
37
+ scripts/build.sh
38
+ fi
39
+
40
+ docker compose up -d
41
+
42
+ exec docker compose exec "${WORKDIR_ARGS[@]}" claude-code claude --dangerously-skip-permissions "${ARGS[@]}"
@@ -0,0 +1,23 @@
1
+ # Sourced by other scripts — loads .env and exports COMPOSE_FILE.
2
+ # Assumes the caller has already cd'd to sandbox/.
3
+
4
+ if [[ ! -f .env ]]; then
5
+ echo "Error: sandbox/.env not found. Copy .env.example to .env and edit it." >&2
6
+ exit 1
7
+ fi
8
+
9
+ set -a
10
+ # shellcheck disable=SC1091
11
+ source .env
12
+ set +a
13
+
14
+ : "${COMPOSE_PROJECT_NAME:?Must set COMPOSE_PROJECT_NAME in .env}"
15
+ : "${VARIANT:?Must set VARIANT in .env}"
16
+ : "${USER:?Must set USER in .env}"
17
+
18
+ # Base image name is shared across all sandboxes (independent of
19
+ # COMPOSE_PROJECT_NAME), so rebuilding it once updates every sandbox.
20
+ : "${BASE_IMAGE:=agent-sandbox-base:latest}"
21
+ export BASE_IMAGE
22
+
23
+ export COMPOSE_FILE="docker-compose.yml:variants/${VARIANT}/compose.override.yml"
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ cd "$(dirname "$0")/.."
4
+ # shellcheck source=_lib.sh
5
+ source scripts/_lib.sh
6
+ exec docker compose exec claude-code tmux attach -t main
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd "$(dirname "$0")/.."
5
+ # shellcheck source=_lib.sh
6
+ source scripts/_lib.sh
7
+
8
+ FORCE=0
9
+ for arg in "$@"; do
10
+ case "$arg" in
11
+ --force) FORCE=1 ;;
12
+ *)
13
+ echo "Usage: build.sh [--force]" >&2
14
+ echo " --force rebuild the shared base image from scratch (--no-cache)" >&2
15
+ exit 1
16
+ ;;
17
+ esac
18
+ done
19
+
20
+ # The base image is shared across all sandboxes, so leave it untouched when it
21
+ # already exists unless --force is given (which rebuilds it for everyone).
22
+ if [[ "${FORCE}" -eq 1 ]]; then
23
+ echo ">>> Forcing rebuild of base image: ${BASE_IMAGE}"
24
+ docker build \
25
+ --no-cache \
26
+ --build-arg UID="${HOST_UID:-1000}" \
27
+ --build-arg GID="${HOST_GID:-1000}" \
28
+ --build-arg USERNAME="${USER}" \
29
+ -t "${BASE_IMAGE}" \
30
+ base/
31
+ elif docker image inspect "${BASE_IMAGE}" >/dev/null 2>&1; then
32
+ echo ">>> Base image already present, skipping: ${BASE_IMAGE} (use --force to rebuild)"
33
+ else
34
+ echo ">>> Building base image: ${BASE_IMAGE}"
35
+ docker build \
36
+ --build-arg UID="${HOST_UID:-1000}" \
37
+ --build-arg GID="${HOST_GID:-1000}" \
38
+ --build-arg USERNAME="${USER}" \
39
+ -t "${BASE_IMAGE}" \
40
+ base/
41
+ fi
42
+
43
+ echo ">>> Building variant: ${VARIANT}"
44
+ docker compose build
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ cd "$(dirname "$0")/.."
4
+ # shellcheck source=_lib.sh
5
+ source scripts/_lib.sh
6
+ exec docker compose exec claude-code claude --dangerously-skip-permissions
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ cd "$(dirname "$0")/.."
4
+ # shellcheck source=_lib.sh
5
+ source scripts/_lib.sh
6
+
7
+ WORKDIR=""
8
+ ARGS=()
9
+ while [[ $# -gt 0 ]]; do
10
+ case "$1" in
11
+ --workdir)
12
+ WORKDIR="$2"
13
+ shift 2
14
+ ;;
15
+ --workdir=*)
16
+ WORKDIR="${1#*=}"
17
+ shift
18
+ ;;
19
+ *)
20
+ ARGS+=("$1")
21
+ shift
22
+ ;;
23
+ esac
24
+ done
25
+
26
+ WORKDIR_ARGS=()
27
+ if [[ -n "$WORKDIR" ]]; then
28
+ WORKDIR_ARGS=(--workdir "$WORKDIR")
29
+ fi
30
+
31
+ exec docker compose exec "${WORKDIR_ARGS[@]}" claude-code zsh "${ARGS[@]}"
@@ -0,0 +1,30 @@
1
+ # check=skip=InvalidDefaultArgInFrom
2
+ ARG BASE_IMAGE
3
+ FROM ${BASE_IMAGE}
4
+
5
+ ARG PYTHON_VERSION="3.10 3.11 3.12"
6
+
7
+ # uv ships in the base image; use it to install the requested interpreters.
8
+ RUN uv python install ${PYTHON_VERSION}
9
+
10
+ # Both uv- and poetry-based projects are supported, so install both package
11
+ # managers alongside the shared dev tooling.
12
+ RUN uv tool install poetry \
13
+ && uv tool install tox \
14
+ && uv tool install pytest \
15
+ && uv tool install mypy \
16
+ && uv tool install black \
17
+ && uv tool install isort \
18
+ && uv tool install flake8
19
+
20
+ USER root
21
+ RUN npm install -g pyright && rm -rf /var/lib/apt/lists/*
22
+ USER ${USERNAME}
23
+
24
+ # Pre-create volume mount points so named-volume init inherits user ownership
25
+ # instead of being created as root by the Docker daemon.
26
+ RUN mkdir -p /work/.venv /work/.tox
27
+
28
+ # Keep poetry's venv inside the project dir so the .venv volume-shadow works.
29
+ ENV POETRY_VIRTUALENVS_IN_PROJECT=true
30
+ ENV PATH="/work/.venv/bin:${PATH}"
@@ -0,0 +1,16 @@
1
+ services:
2
+ claude-code:
3
+ volumes:
4
+ # Shadow project-local paths so container writes stay out of the host bind mount.
5
+ - venv:/work/.venv
6
+ - tox:/work/.tox
7
+ - uv-cache:/home/${USER}/.cache/uv
8
+ - uv-data:/home/${USER}/.local/share/uv
9
+ - poetry-cache:/home/${USER}/.cache/pypoetry
10
+
11
+ volumes:
12
+ venv:
13
+ tox:
14
+ uv-cache:
15
+ uv-data:
16
+ poetry-cache:
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
3
+
4
+ exec "$SCRIPT_DIR/run.sh" --workdir /work/.worktrees/work "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
3
+
4
+ exec "$SCRIPT_DIR/scripts/shell.sh" --workdir /work/.worktrees/work "$@"
@@ -0,0 +1,16 @@
1
+ s01t01
2
+ s01t02
3
+ s01t10
4
+ s01t11
5
+ s01t12
6
+ s01t03
7
+ s01t13
8
+ s01t04
9
+ s01t05
10
+ s01t14
11
+ s01t06
12
+ s01t07
13
+ s01t20
14
+ s01t18
15
+ s01t08
16
+ s01t09