Skip to content

Projects and their runtime

Register a project, bring its services online, and review agent changes into a commit.

Updated View as Markdown

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.setup command ran for it.

Two actions, both behind a confirmation:

  • Reclaim build artifacts deletes node_modules, target, dist, and .next folders 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 inside ports.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 in env (search). If it only takes a flag, pass $PORT or ${<service>.port} in command (admin, docs). Don’t hardcode the number in the app’s own config file, such as vite.config.ts, because sooner or later it stops matching port:. See How ports work.
  • dependsOn mixes services and forwards. Starting storefront starts api, postgres, redis, and clickhouse-http first and waits until each one is ready. If something on your machine already answers on a forward’s localPort, such as a local Postgres, Warpforge uses it and doesn’t start the forward.
  • ${api.port} resolves only once api has a port, so list api in dependsOn wherever you reference it.
  • Personal differences go in workspace.local.yaml, not in the shared file. That covers a different dependsOn, a forward on another localPort, 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close