Skip to content

Give each stage a transfer direction policy

Date: 2026-07-07

A stage binding is one local path used for both transfer directions, and nothing in config distinguished a read-only reference mirror from an instrument-output drop zone. The only guard was operator discipline: a bare forest pull would happily overwrite a push-only stage's local files (subject only to the ADR 0018 dirty-file guard), and a bare forest push would upload from a stage meant to be a read-only mirror.

Stages now carry a direction policy in the shared checkout config (forest.yaml), next to sync_by:

stages:
  raw:
    remote_path: demo/raw
    sync_by: subdirectory
    direction: push        # push | pull | both (default: both)
  reference:
    remote_path: demo/reference
    sync_by: directory
    direction: pull

Direction is unit identity/policy shared by every user of the checkout, so it belongs in forest.yaml (the ADR 0005 split), not local.yaml. It is omitted from serialized config at its default, so existing forest.yaml files are untouched and direction: both behaves exactly like an absent key. forest add --direction sets it at stage creation, and adopt verification rejects a direction change the same way it rejects a sync_by change.

Enforcement follows the ADR 0016 bare-vs-explicit split:

  • Bare forest push / push --all skip pull-only stages with an info note (Skipping stage 'reference' (direction: pull).) and exit 0; bare forest pull / pull --all skip push-only stages the same way. A skipped stage is dropped before its bind check, so a wrong-direction unbound stage never forces a binding it would never use.
  • Explicit selection (--stage, unit IDs, --id, including --all --stage) that resolves into a wrong-direction stage is a hard error, exit 1: Error: stage 'reference' is pull-only; push not allowed. Set direction: both in forest.yaml to change this.
  • Read-only commands (status, ls, diff, tui, flow) ignore direction; the bare forest ls overview displays it next to sync_by when it is not both.

There is deliberately no --force override: direction is policy, not a lease. Changing it is a deliberate forest.yaml edit visible in version control, not a flag on one invocation.

Non-goals

  • Separate push/pull local paths for one stage (dual binding) — a different feature that touches sync-state checksum semantics; use a second checkout tree if both directions with different paths are needed.
  • Remote-side enforcement — bucket policy is the real read-only guard.
  • Per-remote or per-unit direction.

Consequences

  • A pull-only stage can never be pushed from, and a push-only stage can never be pulled into, without an explicit shared-config change.
  • Bare verbs stay usable in mixed checkouts: wrong-direction stages skip with a note instead of failing the whole command.
  • One more stage key participates in the asymmetric YAML serialization contract (emitted only when not both) and in adopt verification.