Forgejo 13 changed the Actions log endpoint to require an attempt segment (/attempt/1/logs). Falls back to the old URL format for older versions. |
||
|---|---|---|
| .claude/commands | ||
| .forgejo/workflows | ||
| apps | ||
| bin | ||
| docs | ||
| scripts | ||
| .envrc | ||
| .gitignore | ||
| .licenserc.yaml | ||
| AGENTS.md | ||
| check-license-headers | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| default.do | ||
| LICENSE | ||
| README.md | ||
| README.talk.md | ||
Get Started · Examples · Try the Demo · Docs · Discord
Bind any git worktree to a bag of state — its own postgres, redis, ports, config — and run your whole product. Detach, reattach different code. No Docker, no VMs. Just processes.
Motivation
In the old days we worked on one branch at a time. You checked out main, start your dev servers where everything binds to the usual ports. There is a .env file with a DATABASE_URL pointed at the one Postgres running on your laptop. The frontend listens on 3000, and you move on to writing code.
Then the world changes. You're running a coding agent on a feature branch while reviewing a PR, and you want to see both of them running, hot-reloading, side by side. But both want port 3000, both want the same Postgres, and both assume they're the only copy of your app on this machine.
The root cause is that nobody treats port assignments, database locations, or Redis URLs as state. They get hardcoded in a .env file or baked into a config.toml and everyone moves on. When there's only one checkout that's fine, but when there are two and more it gets tedious.
The fix I'm proposing sounds strange at first so think about it a little. Stop running one Postgres on your laptop and start running several. Give each branch its own Postgres data directory, its own port, its own Redis, its own config. The same way you'd give each branch its own code checkout, give it its own state.
The Docker crowd got really close, but it's not lightweight. On macOS and Windows it runs a Linux VM underneath, and it's a greedy guest on a machine that's already running your browser, your editor, and Slack. But your laptop can handle a few extra Postgres processes without breaking a sweat.
/rant
Large companies tell us the answer to local dev getting complicated is to take it off your machine entirely. Remote dev environments give you a whole computer in the cloud, and they're making a profit off you every second you're using it. You can SSH in, sure, but your tools, your dotfiles, your workflow are all somewhere else. You're renting a machine to solve a problem your laptop can already handle.
It gets worse with managed agent sandboxes. They copy your code into an environment you often don't even get SSH access to. When the code doesn't work, or a state transition didn't happen the way you expected, you can't just pop in and look at your sqlite database. You sit and watch Anthropic spend your money try to figure out their mistakes. And if you don't like it, maybe you'd like to purchase an Ultraplan for $5 or a Ultrareview for $25. Yes Anthropic, when I go out to eat I love it when the waiter takes a shit in my food right at the table and then extoles the virtues of their Ultrafood upgrade to a (potentially) shit-free dining experience.
Remember underlying problem is a port conflict and the name of a database in an environment variable.
I think it's absolute fucking bonkers crazy to be renting computers, burning tokens, with the fuck you cherry on top of active internet connection.
The problem isn't code isolation (git worktrees). It's state isolation. And state isolation doesn't need containers. It needs directories, environment variables, and bookkeeping.
So I built thinslice.
How It Works
thinslice separates your repository into two halves: code and state.
Code lives in git worktrees — main/, dev/ for shared branches, local/<name> for your feature work, pr/<number> for reviews, tmp/<name> for throwaway experiments and agents.
State lives in state directories under +state+/. Each one is a self-contained bag of runtime state: a postgres data directory, redis dumps, config files, log files, pid files. A state directory has everything your processes need that isn't source code.
A slice.toml manifest in each state directory declares the environment variables and processes for that state:
[env]
DATABASE_URL = "postgres://localhost:5433/myapp"
PGDATA = "$SLICE_STATE_DIR/postgres"
PORT = "3001"
[programs.postgres]
start = "postgres -D $SLICE_STATE_DIR/postgres"
ports = [5433]
[programs.server]
start = "npm run dev"
ports = [3001]
The binding between a worktree and a state directory is explicit and exclusive — each state can be bound to at most one worktree at a time. slice use binds, slice detach unbinds. When you cd into a worktree, direnv loads the bound state's environment variables automatically.
Ports are allocated by a machine-wide registry (slice give-me-a-port) so nothing collides across projects. A thinslice.new-state script checked into your repo generates new state directories with fresh ports — it's just a shell script that prints TOML to stdout.
Getting Started
slice setup
slice setup converts an existing git checkout into a thinslice workspace — bare repo, state directories, worktree namespaces. It's idempotent; safe to re-run. See the docs for the full guide.
Contributing
See CONTRIBUTING.md.
