Skip to main content

Remote Compute

Per-worktree remote resource environments: how Daintree provisions, pauses, resumes, and tears down external compute as part of the worktree lifecycle, plus config, named environments, and auto-provision.

Updated
Reviewed

Remote Compute

Each worktree can carry its own remote resource environment: an external compute target that Daintree provisions and tears down as a side effect of worktree actions.

Remote compute is a per-worktree lifecycle hook system. You describe how to provision, pause, resume, tear down, check the status of, and connect to an external compute resource, such as a Docker container, a cloud VM, an Akash lease, or a remote sandbox, and Daintree runs those scripts in response to worktree actions. The compute lifecycle is bound to the worktree lifecycle, so creating a branch spins up its environment, and deleting the worktree tears it down.

This builds on the project-level lifecycle scripts. Lifecycle scripts cover local setup, such as installing dependencies and copying env files. Remote compute covers anything that lives off-machine.

Configuration

Resource configuration lives in .daintree/config.json. Daintree checks three locations in priority order, and the first valid file wins. There's no merging between levels.

  1. ~/.daintree/projects/<sanitized-root>/config.json: a user-level override, kept outside the repo. Useful for personal resources that shouldn't be committed.
  2. <worktreePath>/.daintree/config.json: worktree-level config, scoped to a single branch.
  3. <projectRootPath>/.daintree/config.json: repo-level config, the usual home for team-shared environments.
Note
Worktree-level config takes precedence over repo-level config. A branch that needs its own compute definition can carry a .daintree/config.json at the worktree root and override the repo default without touching shared files.

A legacy .canopy/config.json is migrated to .daintree/config.json on first load. The migration is cached per-path, so polling doesn't re-migrate on every check.

The config file has two shapes. Use the singular resource key for a single environment:

{
  "resource": {
    "provision": ["docker compose up -d"],
    "teardown": ["docker compose down -v"],
    "resume": ["docker compose start"],
    "pause": ["docker compose stop"],
    "status": "docker compose ps --format json | jq -s '{status: (if length > 0 then \"running\" else \"stopped\" end)}'",
    "connect": "docker compose exec app bash",
    "timeouts": { "provision": 300, "teardown": 300 },
    "statusInterval": 60,
    "provider": "docker"
  }
}

Or use the plural resources key for multiple named environments:

{
  "resources": {
    "docker-local": { "provision": [...], "teardown": [...], ... },
    "akash": { "provision": [...], "teardown": [...], ... }
  }
}

To resolve which environment to use, Daintree walks this order: the exact environmentId requested, then resources["default"], then the first entry in the map, then the singular resource key.

Lifecycle Phases

Six lifecycle phases map to six actions in the action palette and the worktree card context menu. Execution order is provision → (use) → pause ↔ resume → teardown, with status as a side-channel poll and connect as an interactive handoff.

PhaseActionDefault timeoutConfirmation
provisionworktree.resource.provision300sNone
teardownworktree.resource.teardown300sConfirm
resumeworktree.resource.resume120sNone
pauseworktree.resource.pause120sConfirm
statusworktree.resource.status120sNone
connectworktree.resource.connectn/a (opens a terminal panel)None

Each timeout is set per-phase in the timeouts block of the resource config. On timeout, Daintree sends SIGTERM to the process group and escalates to SIGKILL after 5 seconds on Unix, or taskkill /F /T on Windows. The last 8192 bytes of output are captured and shown in the notification.

The status command has to output JSON with at least a status field. Extra fields like endpoint and meta are parsed and stored alongside the worktree. Setting statusInterval (in seconds) turns on periodic polling. Daintree re-runs the status command at that cadence and updates the resource badge on the worktree card.

Teardown failure doesn't block worktree deletion. That's deliberate: if the remote is unreachable or already gone, you should still be able to delete the worktree locally and clean up the remote separately.

Template Variables

Lifecycle commands support two substitution syntaxes. Double-brace {{variable}} maps to fleet and runtime values; single-brace {variable} maps to filesystem-derived slugs. Every substitution is shell-escaped for you, single-quote wrapping on Unix and double-quote on Windows, except {branch-slug}, which is sanitized to [a-z0-9-] and left unquoted. Unresolved placeholders are left in place so typos are obvious. See Security > Lifecycle Command Injection for the threat model and the escaping rules in full.

SyntaxVariableValue
{{...}}{{branch}}Branch name on the worktree
{{...}}{{worktree_path}}Absolute path to the worktree directory
{{...}}{{worktree_name}}Short name of the worktree
{{...}}{{project_root}}Absolute path to the project root
{{...}}{{endpoint}}Last known endpoint from the status command
{...}{branch-slug}Branch name sanitized to lowercase [a-z0-9-]
{...}{repo-name}Repository folder name
{...}{base-folder}Base folder of the worktree path
{...}{parent-dir}Parent directory of the worktree

On top of template substitution, Daintree injects these environment variables into every lifecycle command:

  • DAINTREE_WORKTREE_PATH: absolute path to the worktree
  • DAINTREE_PROJECT_ROOT: absolute path to the project root
  • DAINTREE_WORKTREE_NAME: short name of the worktree
  • DAINTREE_BRANCH: branch name (when present)
  • DAINTREE_RESOURCE_PROVIDER: value of resource.provider from the config
  • DAINTREE_RESOURCE_ENDPOINT: endpoint returned by the last status call
  • DAINTREE_RESOURCE_STATUS: raw output of the last status command

Named Environments

Named environments let one project support several compute targets, for example a cheap local Docker setup for day-to-day dev and a remote Akash lease for longer integration runs. Define them under resources in the config, or manage them in Project Settings > Resource Environments (Worktree Setup tab).

The settings GUI handles multiple named environments through a dropdown selector, an icon picker (Server, Cloud, Container, CPU, Globe, Rocket, Database, Terminal, Box, Layers), per-environment command lists with drag-to-reorder, an in-UI variable reference, and a Default Worktree Mode selector that controls what's pre-selected in the New Worktree dialog.

Settings are stored under resourceEnvironments, activeResourceEnvironment, and defaultWorktreeMode in either ~/.daintree/projects/<sanitized-root>/settings.json (user-local) or <projectRoot>/.daintree/settings.json (in-repo mode), matching the storage mode you chose for the project.

Auto-Provision

Auto-provision fires only when a worktree is created with Remote mode selected in the New Worktree dialog. It doesn't run on app launch, on project open, or when you switch modes on an existing worktree. That keeps remote spin-up predictable: provisioning is always the result of an explicit choice at creation time.

Once the resource is provisioned, the worktree card shows a resource status badge driven by the most recent status command output. The badge color tracks the JSON status value: green for healthy or running states, amber for paused or transient states, red for unhealthy or error states.

Tip
To provision a worktree that was originally created in local mode, run the Provision action from the worktree card context menu or the action palette. Teardown, Resume, Pause, Status, and Connect work the same way. Once a resource is configured, every phase is available as an explicit action.