Last updated 2026-05-07

Architecture

stax is three independent processes that cooperate over HTTP and a Server-Sent Events stream. None of them require the network beyond optional GitHub interactions.

Top-level layout

stax/
├── platform/
│ ├── cli-go/ Go CLI (active) — Cobra-based, single binary
│ ├── cli/ TypeScript CLI (legacy) — same commands, ts-node
│ ├── runner/ Node REST runner (Express + TypeScript)
│ ├── web/ Next.js dashboard
│ └── actions/
│ └── video-capture/ Built-in Playwright action
├── base/ General-dev container (no CUDA)
├── inference/ LLM-serving container (CUDA 12.4 + vLLM)
├── train/ Model-training container (CUDA 12.8)
├── skills/ Claude Code skills (slash commands)
├── docs/ Static landing site (HTML)
├── docs-site/ This Astro documentation site
├── connect.sh SSH helper for remote dev pods
├── install.sh One-line installer for the CLI
└── install.ps1 Windows installer

Runtime topology

┌──────────────────────────────────────────────────────────────────┐
│ Your laptop │
│ │
│ ┌──────────────┐ stdin/stdout ┌──────────────────────┐ │
│ │ stax CLI │ ─────────────────│ git, gh, your editor │ │
│ │ (Go binary) │ └──────────────────────┘ │
│ └──────┬───────┘ │
│ │ HTTP (POST /api/runs, etc.) │
│ │ SSE (GET /api/events) │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ stax runner (Node, :4800) │ │
│ │ parser → expression → scheduler → │ │
│ │ matrix → step-runner → action-resolver │ │
│ │ │ │
│ │ stores: artifacts/, secrets/, sandboxes/ │ │
│ └─────────────────────────────────────────┘ │
│ ▲ │
│ │ HTTP + SSE (same endpoints) │
│ │ │
│ ┌──────┴────────┐ │
│ │ Web dashboard │ │
│ │ (Next.js, │ │
│ │ :3000) │ │
│ └───────────────┘ │
└──────────────────────────────────────────────────────────────────┘

The CLI

platform/cli-go/ — Go 1.26 + Cobra. The CLI does two distinct jobs:

  • Stacked-diff workflow: clone, feature / fix / docs / chore, diff, sync, switch, submit, land, features, diffs, context, status, log, undo, split, config. State lives in .stax/ per-repo.
  • Runner control: runner, run, runs, workflows, secret, dashboard. These are HTTP clients that talk to the local runner.

The runner

platform/runner/ — TypeScript on Node 18+, with Express for the REST surface and a hand-rolled SSE pipe.

platform/runner/src/
├── index.ts Boots Express; wires backends; cleans stale sandboxes
├── server.ts Routes for /api/runs, /api/workflows, /api/secrets, /api/events
├── types.ts Shared TypeScript types
├── engine/
│ ├── executor.ts Lifecycle: trigger, sandbox, schedule, finalize
│ ├── parser.ts YAML loader; workflow discovery; trigger normalization
│ ├── scheduler.ts Topological-sort + matrix expansion + fail-fast + max-parallel
│ ├── matrix.ts Cartesian product, include / exclude
│ ├── expression.ts ${{ … }} parser (literals, ops, functions, contexts)
│ ├── context.ts Builds ExpressionContext + GITHUB_* env vars
│ ├── job-runner.ts Single job: services, env merge, step loop
│ ├── step-runner.ts Single step: shell vs uses, GITHUB_OUTPUT/ENV/PATH parsing
│ ├── reusable.ts Local + remote workflow_call resolution
│ └── sandbox.ts worktree | copy-on-write | rsync-copy | none
├── actions/
│ ├── resolver.ts Marketplace cache + shim dispatch
│ ├── loader.ts action.yml parsing
│ ├── composite.ts Composite step recursion
│ ├── node-runner.ts Node.js action lifecycle (pre/main/post)
│ ├── docker-runner.ts Docker build/pull/run
│ ├── commands.ts ::set-output, ::error, ::add-mask, …
│ ├── shim-framework.ts Declarative shim → composite-YAML compiler
│ └── shim-definitions.ts All language-setup shims
├── artifacts/store.ts In-memory metadata + filesystem storage under .artifacts/
└── secrets/
├── store.ts Per-workspace secrets, optional AES-256-GCM
├── backend.ts SecretsBackend interface
├── r2-backend.ts Cloudflare R2 (encrypted JSON object)
└── github-backend.ts gh CLI bridge → real repo secrets

The web dashboard

platform/web/ — Next.js App Router (this version has breaking changes from public Next.js — see platform/web/AGENTS.md). Pages live under src/app/:

src/app/
├── page.tsx Home / runs list
├── prs/ Pull-request list + detail with Monaco diff viewer
├── features/ Feature list + per-feature detail
├── diffs/ Stacked-diff list and per-diff review
├── runs/ Run list, run detail, live logs
└── api/ Server-side proxies to the runner + git/gh helpers

The dashboard never talks to GitHub directly for run data — it reads from the local runner. It does call the gh CLI for PR metadata.

Built-in actions

stax ships purpose-built actions under platform/actions/. They use the standard GitHub Actions contract (action.yml, INPUT_* env vars, $GITHUB_OUTPUT) so they run in the local runner and on cloud GitHub Actions identically.

  • video-capture — Playwright-driven walkthroughs for web, Android, and iOS.

The container images

Independent of the platform processes, base/, inference/, and train/ are Dockerfiles that publish to ghcr.io/elloloop/stax. They're meant for remote development on RunPod (or any container host); see Containerized Dev Pods.

Why these choices

  • Separate runner from CLI. A long-running runner can cache action downloads, hold a sandbox open for inspection, and stream events to multiple consumers (CLI + dashboard) at once.
  • Express + SSE. SSE is one-way and survives proxies; the dashboard, the CLI log -f, and any custom automation can subscribe.
  • Go for the CLI. Single static binary, fast startup, cross-compiles cleanly to linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, and windows/arm64.
  • Sandboxed execution. Concurrent runs each get their own working tree — your editor's file isn't trashed when CI runs npm run lint --fix.