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 --allskip pull-only stages with an info note (Skipping stage 'reference' (direction: pull).) and exit 0; bareforest pull/pull --allskip 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 bareforest lsoverview displays it next tosync_bywhen it is notboth.
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.