Register a project once. In Projects → Add Project, pick a folder. Warpforge keeps a local registry and reads or creates .warpforge/workspace.yaml — optionally prefilled from a package.json dev script or a Docker Compose file, or generated interactively by an agent in the bootstrap wizard. Existing .warpforge.yaml, .wf.yaml, and .workspace.yaml files remain supported. Removing a project only unregisters it; the directory and its config stay untouched.
A project opens into tabs, the same way a task does: Backlog, Pull Requests, Explorer, Runtime, Terminal, and Worktrees. Explorer browses the project’s own checkout without starting a task — pick anything in the tree and it opens in a syntax-highlighted preview, with several files open at once across a tab strip. Each project remembers the tab you left it on, and its name, path, and port range stay pinned above them all.
Bring the real runtime online. Services start in dependency order with captured logs, interpolated environment variables, and readiness detection: a service waits until the services it depends on are ready, and one that never comes up is marked failed with the reason instead of starting forever. Kubernetes port-forwards run alongside local processes and are watched and retried with backoff when they drop. Runtime shows services and port-forwards in one place with their live logs and the http://localhost:… address of anything that is up; a service that is up but ignores the port it was given gets a warning on its row — for example “Listening on 4321, not 4400”, naming only ports that really answer, even when the service prints several URLs — because tools like astro dev and vite only listen where you tell them to (--port $PORT); start, restart, and stop appear on a row when you point at it, and starting or stopping everything at once is a single click in the section heading. A service or port-forward that your own uncommitted workspace.local.yaml added or changed carries a small local badge, and hovering it lists the fields that came from your file. Each project also has interactive terminals inside the app, so a quick git log or one-off script does not need another window.
Search without leaving the task. Press ⌘⇧F for Find in Files: every matching line grouped by file, with a live peek at the surrounding code, and Enter opens the file at that line with the cursor already there. The quick-open palette (double ⇧ or ⌘P) finds text too. Searches stay on files that belong to the project — build output and dependencies don’t bury the results.
Your work is kept; running services are not. A local Rust daemon owns projects, services, sessions, and task state behind a WebSocket API; the Tauri app is a thin client. Quitting stops services, port-forwards, and agent sessions — after one confirmation that lists everything running — while task history and agent configuration persist in ~/.warpforge/warpforge.db and the project registry in ~/.warpforge/projects.json, so every conversation is exactly where you left it.
Review, commit, ship. Browse changed files, read unified or split diffs, revert individual hunks, edit files inline, draft a commit message, commit or amend, update the branch, and push with --force-with-lease — all through git directly. Open pull request additionally needs the GitHub CLI (gh) installed and authenticated; see Install → Requirements.
The Worktrees tab
Every task that runs isolated leaves a checkout on disk, and node_modules and target folders make them large. Worktrees lists the ones Warpforge manages for the project, so you can see what they cost and clear out the ones you no longer need. Checkouts you made yourself elsewhere are not listed.
Each row shows:
- The branch, and the task that owns the worktree. A worktree no task owns — left behind by a deleted task, say — is marked orphan.
- Its size on disk, or a dash when it could not be measured in time.
- Setup log, when a
worktree.setupcommand ran for it.
Two actions, both behind a confirmation:
- Reclaim build artifacts deletes
node_modules,target,dist, and.nextfolders inside that one worktree and tells you how much space came back. Source files stay and the next build recreates them. Folders that hold tracked files are left alone, and so is anything inside a nested repository. - Remove deletes the checkout. For a task’s worktree it also archives the task, and it deletes the task branch Warpforge created — a branch you chose yourself is kept.
Both refuse while the task’s agent is mid-turn. Remove also refuses while the worktree has uncommitted changes or commits that exist nowhere else — push them first.
No port roulette
Every registered project gets a predictable 100-port range beginning at 4000 — assigned once and kept, or pinned to an explicit range your team commits. That is not a registry detail — it is what makes parallel work practical: multiple projects, and multiple agent-built previews inside them, stay online together without fighting over 3000, 5173, or other common defaults.
For services with a configured port, the behaviour depends on whether the project declares a port range in its config. When it does, the declared port is a hard pin — the service binds exactly that port and fails rather than moving if it is taken. When it doesn’t, Warpforge selects an available value in the project’s range, sets PORT, and expands references such as ${app.port} in environment variables. Either way, the resolved URLs become part of the live context handed to new agent sessions. You can switch projects and come back without stopping unrelated services, rewriting configuration, or chasing address already in use. The full picture — including the trap of an app that ignores PORT — is in How ports work.
A complete example
This monorepo has five apps, gets its databases from a shared Kubernetes dev cluster, and commits a port range for the whole team. Each service gets its port a different way.
# .warpforge/workspace.yaml
name: acme-shop
# Committed and identical on every machine, so the ports below can go into
# .env files, Postman collections, and OAuth redirect URIs.
ports:
range: "4200-4299"
services:
# Reads PORT, the default path. Waits for the API and the database forward.
storefront:
command: cd apps/storefront && bun run dev
port: 4200
readyPattern: "Ready in"
dependsOn: [api, postgres]
env:
API_URL: http://localhost:${api.port}
# Reads PORT. Its dependencies are all port-forwards.
api:
command: cd apps/api && bun run dev
port: 4210
readyPattern: "api: listening"
dependsOn: [postgres, redis, clickhouse-http]
env:
DATABASE_URL: postgres://dev@localhost:5432/shop
REDIS_URL: redis://localhost:6379
# Reads its own variable instead of PORT, so hand it the port by name.
search:
command: cd apps/search && go run ./cmd/server
port: 4220
readyPattern: "http server listening"
dependsOn: [clickhouse-native, object-store]
env:
HTTP_PORT: "${search.port}"
# Vite ignores PORT, so pass the port as a flag.
admin:
command: cd apps/admin && bun run dev --port $PORT
port: 4230
readyPattern: "ready in"
dependsOn: [api]
# Same for Astro. ${docs.port} works in a command as well as $PORT.
docs:
command: cd apps/docs && bun run dev --port ${docs.port}
port: 4240
readyPattern: "ready in"
portforwards:
- name: postgres
namespace: postgres
pod: postgres-pooler
localPort: 5432
remotePort: 5432
- name: redis
namespace: redis
pod: redis
localPort: 6379
remotePort: 6379
# One pod, two protocols: one forward each.
- name: clickhouse-http
namespace: clickhouse
pod: clickhouse-0
localPort: 8123
remotePort: 8123
# Native port moved to 9100 locally, since object-store already takes 9000.
- name: clickhouse-native
namespace: clickhouse
pod: clickhouse-0
localPort: 9100
remotePort: 9000
- name: object-store
namespace: minio
pod: minio
localPort: 9000
remotePort: 9000- Every service
port:sits insideports.range. With a range declared, each one is pinned, and a service whose port is taken fails instead of moving. Give every service its own number, or one of the two will never start. - Port-forwards sit outside the range. The range doesn’t apply to
localPort. Use the port your apps already expect (5432,6379) so their connection strings work unchanged. - Find out how each app picks its port before you pin it. If it reads
PORT, you’re done (storefront,api). If it reads another variable, set that variable inenv(search). If it only takes a flag, pass$PORTor${<service>.port}incommand(admin,docs). Don’t hardcode the number in the app’s own config file, such asvite.config.ts, because sooner or later it stops matchingport:. See How ports work. dependsOnmixes services and forwards. Startingstorefrontstartsapi,postgres,redis, andclickhouse-httpfirst and waits until each one is ready. If something on your machine already answers on a forward’slocalPort, such as a local Postgres, Warpforge uses it and doesn’t start the forward.${api.port}resolves only onceapihas a port, so listapiindependsOnwherever you reference it.- Personal differences go in
workspace.local.yaml, not in the shared file. That covers a differentdependsOn, a forward on anotherlocalPort, or a service only you run.
Agents see the runtime too
The same services, logs, and controls are available to agents as tools — list_runtime to discover what’s up and on which port, read_service_logs to watch a restart, service_restart to bring something back. An agent that can read the failure it caused fixes it before handing you a diff.
See the configuration reference for the full .warpforge/workspace.yaml schema, and Tasks and backlog for where the work comes from.