Skip to content

Migrate to sbx CLI + publish sandbox image + expand commands + install UX overhaul#21

Merged
sagalbot merged 33 commits into
mainfrom
refactor/sbx-cli-migration
Apr 7, 2026
Merged

Migrate to sbx CLI + publish sandbox image + expand commands + install UX overhaul#21
sagalbot merged 33 commits into
mainfrom
refactor/sbx-cli-migration

Conversation

@sagalbot
Copy link
Copy Markdown
Contributor

@sagalbot sagalbot commented Apr 3, 2026

Summary

Migrates Turbo from the deprecated docker sandbox plugin to the standalone sbx CLI, publishes a pre-built sandbox image to Docker Hub, expands the command surface, overhauls turbo:install, and simplifies plugin management.

Closes #20

What changed

sbx CLI migration

  • All docker sandbox calls → sbx equivalents
  • pcntl_exec for interactive Claude sessions (Symfony Process setTty doesn't allocate a proper pty for TUIs — causes SIGKILL exit 137)
  • prepareSandbox moved from pre-session to install-time only (running sbx exec immediately before sbx run from PHP kills the agent)

Published sandbox image

  • Image: docker.io/springloadedco/turbo:latest (PHP 8.4, Composer, Node 22, Chromium)
  • CI: .github/workflows/publish-sandbox.yml builds + pushes on merge to main
  • Renovate: watches docker/sandbox-templates:claude-code for base image updates
  • turbo:install defaults to the published image — most users never build anything
  • To extend: write a FROM springloadedco/turbo:latest Dockerfile, build + push with Docker directly

Expanded command surface

New command Description
turbo:ports List / publish / unpublish sandbox ports
turbo:stop Stop sandbox (preserving state)
turbo:start Start sandbox without attaching
turbo:rm Remove sandbox and all state
turbo:exec Run arbitrary commands inside sandbox (--tty for interactive)
turbo:doctor Health check (sbx installed, sandbox exists, hosts, ports)
turbo:prepare Re-run host access setup when APP_URL changes

Removed: turbo:build (marginal wrapper over docker build --push), turbo stub command.

Install UX overhaul

  • Grouped skill selection — single flat multiselect with Group › skill labels, smart defaults per group
  • Pre-fill on re-runs — installed skills marked (installed), groups with installed skills pre-checked
  • State-aware Docker prompt — skips if sbx not installed (with install hint), skips if sandbox already exists (with doctor hint), only prompts on fresh setup

Skills curation

  • Removed github-pr-comment (ralph-loop artifact)
  • Updated github-issue and github-milestone — removed ralph-specific examples + feedback-loop placeholders
  • New feedback-loops skill — fires on completion claims, commit, PR creation. Lists project's configured verification commands via {{ $feedback_loops_checklist }}. Blocks completion until all pass.

Superpowers plugin simplified

Replaced fragile sbx run -- plugin marketplace add (which required the agent running + authenticated, and hit the 137 SIGKILL on fresh sandboxes) with a single JSON write:

// .claude/settings.json
{"enabledPlugins": {"superpowers@claude-plugins-official": true}}

Claude Code reads this on first session and auto-installs after the user authenticates via /login.

CLAUDE.md expansion

  • Authoritative sbx docs URLs
  • Capability matrix (what sbx does NOT support: --env, --add-host, --volume, --publish, --dns)
  • Secret injection model (proxy header injection, not env vars)
  • Host access patterns (host.docker.internal + policy vs. /etc/hosts)
  • pcntl_exec requirement for TUIs
  • Warning about sbx exec before sbx run from PHP

Test plan

  • composer test — 49 passing
  • composer analyse — PHPStan clean
  • composer format --test — Pint clean
  • php artisan turbo:claude opens interactive session
  • php artisan turbo:install runs full grouped flow
  • Grouped skill selection shows (installed) on re-runs
  • Docker prompt skips when sandbox exists
  • turbo:prompt with authenticated sandbox
  • turbo:ports --publish end-to-end
  • turbo:doctor on fresh vs existing sandbox
  • turbo:exec --tty bash

Recommend squash-merge — commit history has iterative discovery churn (add-then-remove cycles) that's useful PR context but doesn't belong in mainline.

🤖 Generated with Claude Code

sagalbot and others added 21 commits April 3, 2026 15:04
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass --pull-template never to sbx create so it uses the locally built
image instead of attempting to pull from Docker Hub.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ondrej/php PPA and PHP_VERSION ARG so a single Dockerfile can
produce images for PHP 8.3, 8.4, and 8.5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matrix builds springloadedco/turbo:php8.3, php8.4, php8.5 on push
to main. Uses GHA cache for fast rebuilds. php8.5 is tagged latest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Watches docker/sandbox-templates in the Dockerfile and opens PRs
when it updates. Merge triggers publish-sandbox workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Default image is now docker.io/springloadedco/turbo:php8.4. Users
extending the image override via TURBO_DOCKER_IMAGE env var.
When the default springloadedco/turbo image is selected, the install
flow skips the build step since sbx pulls it directly from Docker Hub.
Custom images still trigger turbo:build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The build process now pushes to a registry, so the integration test
requires registry auth. Renamed env var to RUN_DOCKER_PUSH_TESTS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the springloadedco/turbo Docker Hub image, available PHP
tags, and the extend-and-push pattern for custom images.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sbx uses a separate Docker daemon that only pulls from OCI registries.
Add --push to docker build so the image is available to sbx. Remove
--pull-template never since images now come from a real registry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ondrej/php PPA doesn't support Ubuntu 25.10 (questing) which is
what the sandbox base image ships. Simplify to a single image tag
using the native PHP 8.4 packages. PHP version tags can be added
later when the base image moves to an LTS release.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Symfony Process's setTty(true) does not properly allocate a pty for
fully-interactive TUIs like Claude Code — Claude starts then exits
immediately with code 137. Replace the PHP process with sbx directly
via pcntl_exec so sbx owns the terminal the same way as a direct
shell invocation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Running sbx exec via Symfony Process immediately before sbx run via
pcntl_exec causes the claude agent to be SIGKILL'd after rendering
its welcome screen (exit 137). The prep step leaves the sandbox or
sbx daemon in a state that breaks the subsequent interactive session.

Move prepareSandbox to run once at sandbox creation (via turbo:install)
instead of before every run. Add a turbo:prepare command for manual
re-runs when hosts config changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New commands:
- turbo:ports - list/publish/unpublish sandbox ports
- turbo:stop / turbo:start / turbo:rm - sandbox lifecycle
- turbo:exec - run arbitrary commands inside the sandbox
- turbo:doctor - health check (sbx installed, sandbox exists, hosts
  configured, ports published)

DockerSandbox additions:
- stopProcess(), portsProcess(), publishPortProcess(),
  unpublishPortProcess() - thin sbx wrappers
- execInteractive() - TTY-preserving exec via pcntl_exec (for bash etc.)
- execSbx() helper to DRY up pcntl_exec calls

CLAUDE.md: authoritative sbx docs references + capability matrix
documenting what sbx does NOT support (--env, --add-host, --volume,
--publish, --dns, etc.). Also documents the secret injection model,
host access patterns, and the pcntl_exec requirement for TUIs.

Removed: TurboCommand (unused stub).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
turbo:build was a thin wrapper around `docker build --push` that added
only marginal value — users building custom images already know Docker.
Worse, the 'dockerfile' config defaulted to the package's Dockerfile,
which is the wrong one for extension use cases.

Removed:
- turbo:build command (DockerBuildCommand)
- buildProcess() method from DockerSandbox service
- 'dockerfile' config option (TURBO_DOCKER_DOCKERFILE)
- getPackageDockerfilePath() helper
- Integration test that only exercised buildProcess

For custom images, users now run docker build directly:
  docker build --push -t <image> .

turbo:install still prompts for the custom image name but directs the
user to run docker build separately before continuing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sagalbot sagalbot changed the title Migrate docker sandbox CLI to sbx Migrate to sbx CLI + publish sandbox image + expand command surface Apr 5, 2026
sagalbot and others added 3 commits April 5, 2026 08:53
Three improvements to turbo:install:

1. Skill curation
   - Remove github-pr-comment (autonomous-workflow artifact)
   - Rewrite github-issue to drop ralph-specific example and
     feedback-loop placeholders (moved to new skill)
   - Update github-milestone example from ralph-specific to generic
     Stripe billing example

2. New feedback-loops skill
   - Fires on completion claims (done, complete, ready for review,
     before commit, before gh pr create)
   - Lists the project's configured feedback_loops commands via
     {{ $feedback_loops_checklist }} placeholder
   - Explicit rule: do not claim done/commit/create PR unless all
     commands passed

3. Grouped two-stage skill selection UX
   - Skills organized into groups: Laravel, Project, GitHub, Third-party
   - Stage 1: multiselect groups (laravel + project default-enabled)
   - Stage 2: optional per-skill customization with (installed) markers
   - Pre-fills groups based on already-installed skills on re-runs
   - New SkillsService::getInstalledSkillNames() helper

4. State-aware Docker sandbox prompt
   - Skips gracefully with install hint when sbx not on PATH
   - Skips with 'already exists, run turbo:doctor' when sandbox exists
   - Only prompts when there's an actual decision to make
   - Removes redundant rebuild prompt (user runs turbo:rm + turbo:install)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the two-stage group+customize flow with a single flat
multiselect that shows every skill with a group-prefix label:

  Laravel   › controllers
  Project   › feedback-loops
  GitHub    › issue
  3rd-party › agent-browser

Smart defaults: installed skills checked, plus all skills in
default-enabled groups (Laravel + Project). GitHub and 3rd-party
groups remain opt-in by default.

Fewer screens, everything visible at once, scroll: 15 for longer
lists. Planning to follow up with a custom two-pane Prompt later.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Running sbx exec (prepareSandbox → setup-sandbox) immediately before
sbx run (plugin marketplace add) triggers the same SIGKILL-on-agent
issue seen in turbo:claude (exit 137). Reorder so plugins install
before the problematic exec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sagalbot sagalbot changed the title Migrate to sbx CLI + publish sandbox image + expand command surface Migrate to sbx CLI + publish sandbox image + expand commands + install UX overhaul Apr 5, 2026
sagalbot and others added 4 commits April 6, 2026 09:30
Replace the fragile sbx-run-based plugin install with a simple JSON
write. The superpowers plugin is enabled via .claude/settings.json
(a committed project file):

  {"enabledPlugins": {"superpowers@claude-plugins-official": true}}

Claude Code reads this on first session and auto-installs the plugin
after the user authenticates via /login. No sbx run needed, no 137
SIGKILL risk, works with OAuth/Max subscriptions.

Removed: installSandboxPlugins() and all sbx run plugin commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dead methods removed from DockerSandbox:
- ttyProcess() — replaced by pcntl_exec for interactive sessions
- runPrompt(), runCommand(), runInSandbox() — were used for plugin
  install via sbx run, replaced by .claude/settings.json approach

Stale references cleaned:
- config/turbo.php: removed turbo:build mention in comment
- README.md: removed dockerfile config key from example + table,
  fixed direnv example (turbo build → turbo prompt)
- .env.example: removed TURBO_DOCKER_DOCKERFILE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Remove duplicate/stale step comments in InstallCommand::handle()
2. Fix stale docblock on prepareSandboxProcess() — no longer handles
   node_modules isolation (removed in c6042a4)
3. Add exit 137 guidance to PromptCommand — if the agent is killed
   (unauthenticated sandbox), tell the user to run turbo:claude and
   /login first

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Composer checks package keywords for "dev" and prompts the user to
re-run with --dev if they use `composer require` without it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sagalbot and others added 5 commits April 7, 2026 13:38
- Fix turbo:exec wrapping command in bash -c (was passing as single arg)
- Remove "Create Docker sandbox?" confirm (sandbox is the whole point)
- Install superpowers via npx skills instead of settings.json plugin
- Set scoped GitHub token as per-sandbox sbx secret
- Fix double sandboxExists() call in DoctorCommand
- Replace shell_exec with Process in DoctorCommand and InstallCommand
- Inline DisplaysCommands trait into PromptCommand (single consumer)
- Update "Docker sandbox" → "sandbox" in command descriptions
- Add git insteadOf rule in Dockerfile for SSH→HTTPS rewriting
- Add tests for all new commands (Exec, Doctor, Ports, Prepare, Remove, Start, Stop)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Superpowers is a multi-skill package, not a single skill. Pass '*'
as the skill name so npx skills installs all 14 skills from the source.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All skill groups (including third-party) are now checked by default on
first install. On re-runs, only previously installed skills are
preselected — defaultEnabled is ignored once any skill in the group
has been installed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README: superpowers "installed during setup" not "included", add sbx
  doc links, add superpowers to skills table, "create" not "build"
- CLAUDE.md: replace stale plugin marketplace notes with superpowers
  via npx skills, document GitHub token dual-storage, add git
  SSH→HTTPS rewriting note
- Replace last shell_exec in DockerSandbox::execSbx with Process

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sagalbot sagalbot merged commit d7be9a7 into main Apr 7, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate docker sandbox CLI to sbx

1 participant