Caravan is an agent-in-the-loop merge queue for GitHub pull requests. The cara
CLI represents a queue as one or more labelled PR chains, performs deterministic
compatibility and CI checks, and returns typed decision points when semantic
judgment or code repair is required.
The normative behavior is in SPEC.md. Visit the public project site at
a.skh.am/caravan/ for a visual guide to the
queue model, agent loop, safety evidence, commands, and installation.
The GitHub Pages site is deterministic static source in site/, with
no external runtime assets or tracking. All project-local URLs use the
/caravan/ Pages base path. Run its dependency-free link, metadata, and asset
smoke check with:
python3 scripts/check-site.pyPushes that touch the site deploy through the Pages workflow after the same check passes.
For the full decision tree — how a PR becomes a candidate, what blocks the admission front and what does not, when a root is flattened rather than replayed, the four conditions for merging a root, and how failures are typed by who can resolve them — see docs/lifecycle.md. It also lists the currently known gaps between that intended behaviour and what Cara does today.
- A caravan head targets the repository default branch.
- Each later PR targets its predecessor's branch without routinely rewriting history.
- Every member has the
caravanlabel. - Only the head has squash auto-merge enabled.
- The current head PR number is the caravan ID.
cara syncis idempotent: pending CI waits, failed CI returns bounded run/job/failed-step and exact selected-lineage receipts, and--rerun-failedreruns only current-generation infrastructure failures. Every successful tick includes a versionedscheduler_statuswith exact default/root/tail/member generations and ahealthy,waiting_ci,held,retry_tick, oroperator_actiondisposition. A successful tick is neverhealthywhile a member's required contexts have no reporting run on its exact current head: those members appear inscheduler_status.missing_required_runswith the exact PR, head, and contexts. A successful tick also names anyhead_of_linestall: the exact blocking PR, its one-based queue position, the members waiting behind it, the block class (conflict, CI failure, missing required runs, invalid graph, or a blocking admission rejection), ordered repair/reshape/evict remedies, and a stable fingerprint for counting no-progress passes. A stalled front is an external decision, never healthy or idle: syncing more often cannot resolve a conflict, and work must be selected by queue position rather than by whichever member is cheapest to fix. Failed ticks classifywake_classasretry_tick,external_decision, oroperator_action; only an external decision emits a repair-wake failure event. Stale provider preconditions are routine retry ticks, not merger work. Deterministic unsupported range shapes such asrebase_nonlinear_range, ambiguous/empty ranges, or rewritten target history are non-retryable external decisions with a stable evidence fingerprint and explicit repair/reshape/strategy continuations. Stale synthetic generations require a fresh candidate trigger; raw logs and unrelated log text are never retained or exposed.caravan-forceis durable PR-scoped operator intent: when that PR reaches caravan root, if it is mechanically mergeable, Cara ignores every CI state (successful, pending, running, failed, mixed, unknown, or empty) and performs the administrator squash immediately.cara force --pr N --actor A --reason Rmay arm any active member after exact current-edge, selected-Caravan hold/graph, permission, branch, and PR preflight. The label follows the PR through Cara-owned history rewrites, membership/base changes, and position changes; unrelated admission candidates cannot block it. At root, Cara freshly proves the exact root/default compatibility, provider head/base, repository policy, ADMIN permission, and lease before merging. Explicit revoke, eviction, or a successful merge consumes intent. Controller-reviewed transitions additionally usecara --json force-intent preview|apply|revokewith exact transition evidence; that authorization is generation-bound, but the resulting PR intent is durable. Matching MCP tools expose the same receipts.- Optional sync-owned auto-admission is strictly opt-in. After the existing
fleet converges,
sync --allconsiders unlabelled PRs in configured-priority then immutable-FIFO order and greedily joins the first compatible live tail.sync.max_caravansdefaults to1and bounds active, non-parked caravans: joining an existing chain remains possible, but creating another returns a typed zero-write capacity refusal. Parked chains consume no slot, and lowering the bound never deletes or reshapes excess existing chains. The cap is never a convergence gate: every already-enrolled caravan continues check/merge/ collapse, and a recovered parked caravan may reactivate even when that makes the retained active fleet exceed the current bound. Automatic and explicit unpark remove an existing quarantine; they do not admit a new caravan. Incompatible generations receivecaravan-join-skippedplus a durable exact evidence receipt; unchanged generations are not retried, while candidate, default, tail, config, or heuristic changes invalidate the skip automatically. - Trusted sync terminalizes provider
CLOSEDrows with no merge timestamp before any active queue work: after a fresh authoritative eligibility read, one exact complete-label write addscaravan-closedwhile removingcaravanandcaravan-parked, preserves branches and provider history, then returns after authoritative rediscovery. Open, unknown, stale, reopened, merged, or drifted plans refuse cleanup before writing. Independently discovered reopened or merged rows lose only the stale terminal marker. Provider reads and writes carry exact before/after evidence; duplicate syncs issue no provider write. Status reports terminal-closed rows separately and they never consumemax_caravans. - Every non-clean attachment check also returns exact squash-equivalence
evidence. A landed member reaches the default branch as one squash commit
whose content matches that member's cumulative content but whose commit
identity is unrelated to the pre-squash commits later members still carry, so
replaying that stacked history can conflict against content identical to what
it introduces. Cara reports whether an ancestor-closed linear prefix of the
candidate-only range is already held by the target byte for byte — identical
blob objects and file modes on every path that prefix's cumulative diff
changes — and whether replaying only the retained commits from that proven
boundary is independently clean. Commit messages, subjects, and patch text are
never proof, an identical patch with a different resulting blob is not
equivalence, and an untouched file that happens to match proves nothing. A
represented prefix whose retained commits still diverge reports
residual_conflict; ordinary three-way divergence after the equality point reportsno_equivalence. Neither drops a commit, and nothing is ever resolved by taking either side. Detection is evidence, not authority: reconciliation applies only under an explicitly authorized rewrite, which reverifies the replayed head tree against the proven cumulative tree and the rebuilt commit count against the proven retained set before any push. Receipts list dropped and retained commits, the proven boundary and its tree, the represented paths with blobs and modes, and the cumulative tree before and after reconciliation.
Caravan develops Caravan through its own checked-in physical-chain, automatic
admission, force, CI, and repair policies. Cacophony worker reintegration is
being moved to the transactional pr_cara_join handoff so every feature lands
through the same path operators use. See docs/dogfood.md
for rollout gates, daily operation, evidence capture, and rollback.
Every bounded queue operation is implemented by one shared typed library path
used by the human CLI, stable --json envelopes, and MCP. The foreground loop
is intentionally CLI-only; MCP coordinators schedule bounded sync --all
calls instead. See docs/v1-parity.md for the checked
SPEC-to-surface matrix.
Development surfaces:
nix develop
./scripts/check-workflows.sh # pinned actionlint + shellcheck
cargo test
cargo run -- help
cargo run -- mcp tools
cargo run -- mcp stdio
cargo run -- self-update status
cargo run -- feedback status
# Explicit only: runs the process/load-sensitive hook contract.
cargo test --features environmental-hook-acceptance --test hook_exampleThe default development shell pins actionlint and shellcheck; the same
scripts/check-workflows.sh command is a Nix flake check. Custom self-hosted
runner labels live in .github/actionlint.yaml, so workflow validation needs no
ad hoc nixpkgs fetch or machine-global linter installation.
tests/hook_example.rs is deliberately guarded by the
environmental-hook-acceptance feature. Ordinary cargo test, Nix package
checks/installations, pull-request CI, and release contract builds retain every
deterministic unit/build/schema/version gate but do not build or execute that
shell/process integration target. .github/workflows/hook-acceptance.yml is the
only hosted owner: it runs daily or by workflow_dispatch, never on
push/pull_request, and executes all assertions in the target unchanged.
The environmental lane requires the repository secrets
CARAVAN_FEEDBACK_WEBHOOK_URL and CARAVAN_FEEDBACK_WEBHOOK_TOKEN.
scripts/run-hook-acceptance.sh always runs the assertions; on failure it
refuses a silent stderr fallback and requires an authenticated webhook before
emitting exactly one bounded, credential-redacted feedback event.
The event names both load-bearing tests, architecture, runner environment,
source revision, failure phase, output digest, and a bounded output tail. Its
stable caravan-hook-acceptance-v1:... fingerprint lets an identical rerun
refresh one receiver-side record; canonical webhook fingerprint idempotency is
owned by Cacophony bd-52cf42, not by a Caravan-local dedupe workaround. A
passing run emits no feedback event. Manual runs may set force_failure=true
to exercise the same visible failure/reporting path without weakening either
test assertion.
Domain surface:
cara init
cara config check [--config PATH] # strict, read-only pin/config compatibility
cara status
cara web --repo PATH [--repo PATH ...] [--read-only]
cara check [--pr N] [--tail-pr N | --head-pr N]
cara new [--pr N | --create-pr]
cara join [--pr N] [--tail-pr N | --head-pr N] [--create-pr]
cara renew [--pr N | --create-pr] | cara rejoin [--pr N]
cara priority set --pr N --label caravan-priority:high --actor A --reason R
cara priority clear --pr N --actor A --reason R
cara show | cara next | cara prev
cara force --pr N --actor A --reason R
cara force revoke --pr N --actor A --reason R
cara --json force-intent preview|apply|revoke --pr N --head OID \
--membership-generation G --failure-fingerprint F --reason R \
--expires-at-ms T --auto-merge squash
cara plan sync [--all] [--rerun-failed]
cara plan concat --source-head-pr S --target-tail-pr T --actor A --reason R
cara concat --source-head-pr S --target-tail-pr T --actor A --reason R --expected-plan-hash H
cara sync [--all] [--rerun-failed] | cara loop [--once] [--manual --shell COMMAND]
cara repair start --pr N [--target-pr T]
cara repair authorize-agent-edits --session ID --actor A --reason R
cara repair grant --session ID --path P --source-revision SHA --actor A --reason R
cara repair revoke-grant --session ID --path P --actor A --reason R
cara repair status --session ID | cara repair continue --session ID
cara repair abort --session ID --confirm
cara evict [--pr N] --reason TEXT
cara split [--pr N]
cara van list | cara van next | cara van prev
cara lock status | cara lock recover --token TOKEN --confirm
cara mcp tools | cara mcp stdio
cara self-update status | check | run
cara feedback status | report
cara concat is the atomic recovery operation for appending one complete live caravan after another. Always run cara plan concat first and review its immutable plan hash, exact old/new ordering, complete source rewrite scope, and rollback heads. Execution atomically rewrites every source branch under exact leases, then commits one source-root base edge; it never sequences client-side evict+rejoin. A failed commit restores every original source head, final verification failure restores the membership edge and physical heads, and exact retry returns the durable original journal receipt. Cycles, forks, holds, incomplete topology, stale plan hashes, conflicts, and ambiguous rollback all fail closed.
cara next-candidate selects the canonical priority/FIFO attempt. Preflight that exact provider candidate with cara check --pr N (and optionally --tail-pr T or --head-pr H) without checking out or mutating a branch, base, label, or auto-merge state. A targetless check first offers the one visible unheld caravan: when that attachment is clean, the complete receipt coherently reports join mode, caravan, target, compatibility, and intent; an ineligible attachment falls back to new. Zero or multiple visible caravans retain new evaluation because a later targetless join could not resolve the same target unambiguously. This inference belongs only to check recommendation; an explicit cara new still preflights the requested new-caravan operation. The receipt includes the canonical provider candidate identity/freshness evidence, exact head/base repository and OIDs, draft/labels/auto-merge facts, enrollment and canonical-order state, pairwise conflict paths, and a mechanical new, join, repair, wait, or reject continuation. Provider/ref races fail closed, and automatic selection never silently leapfrogs a rejected first eligible attempt. Every remote --pr receipt records non-blocking admission_note ordering evidence plus a typed admission_intent decision, and an explicit --pr request proceeds on the recommended or explicitly targeted action using that candidate's own eligibility rather than its queue position, so an unadmitted earlier PR cannot wedge other owners. Structurally ineligible PRs (draft, fork-only, externally enabled auto-merge, superseded/ambiguous/invalid generation) are reported with exact reasons and excluded from ordering instead of wedging the queue, so a fleet at zero caravans still forms a new root from the first eligible candidate. Cacophony-generated PRs also expose structured generation integrity: Cara groups only the same agent, overlapping bead stream, and stack slot; exact containment or a current reviewed canonical-link audit excludes older superseded_generation PRs without blocking their successor, while divergent/invalid siblings stop for owner choice. Membership revalidates the complete open generation set immediately before mutation and never auto-closes stale PRs. Use audited cara priority set|clear to change one unenrolled PR's persistent automatic-order metadata; priority never authorizes membership or bypasses compatibility.
Priority/FIFO order is the automatic selection contract, so Cara resolves who selected a candidate and what it asked for before rejecting a non-canonical attempt. These are separate axes:
- Automatic selection (
cara next-candidate, sync auto-admission) is bound by priority/FIFO fornewandjoinintent alike, without exception. - Explicit owner selection — naming one exact remote PR with
cara check --pr N, with or without--tail-pr/--head-pr— is deliberate admission intent fornewandjoin. It may attach ahead of earlier ordered rows while every bypassed row is an unrelated unjoined first-admission attempt; those rows keep their canonical order and are still admitted in turn. - Checked-out owner operations (local
cara check, every membership operation includingrenew/rejoin) report canonical position as evidence only, exactly as they always have.
The relaxation never passes an active caravan member, a PR on the candidate's
exact base chain, or a rank-indeterminate row; it never applies to a candidate
that is not itself a current ordered admission attempt; it never guesses an
ambiguous or missing target; and it never substitutes for compatibility,
freshness, generation, policy, or provider success. Every check and membership
receipt carries a typed admission_intent decision naming the selection, the
intent, the resolved target, each ordered row ahead with its disposition,
compatibility, provider mutation, and idempotency. The human admission_note is
derived from that same decision, so the CLI note, the decision, and the mutation
behaviour cannot disagree.
Human CLI output is terminal-aware: concise sectioned layouts use color when
stdout is a TTY, honor NO_COLOR, and render PR numbers/titles as OSC-8 GitHub
links where supported. JSON/MCP remains unstyled and stable for jq and agents.
For cara new and local cara join, --create-pr remains the deterministic
noninteractive contract. In an interactive terminal, a missing PR now offers to
publish the current topic branch and create a commit-derived PR automatically.
If invoked from the default branch with changes, Cara shows the exact changed
paths and, after confirmation, creates a named topic branch, stages and commits
the listed changes, publishes it, creates the PR, and resumes membership. On a
clean default branch it creates the requested topic branch and tells the user to
make a commit before rerunning; no empty PR is invented. Advanced same-repository
branches with one merged historical PR are safe fresh-generation ancestry for
an explicit create, while ambiguous reuse, unchanged old heads, unpushed heads,
forks, and provider races still fail closed.
With rebase_on_join: true, all successful membership operations expose one exact join_receipt. For root new/renew, predecessor PR 0 denotes the live default branch and its exact branch/OID; root ancestry, physical rebase, durable membership, provider receipts, force absence, configuration fingerprint, and receipt hash are validated just like tail join/rejoin (bd-d15ba3).
Cara first resolves the current Git worktree root, so every command behaves the
same from the root or any nested directory. Default .caravan/config.yaml,
locks, journal, repair state, and init writes are rooted there. A relative
explicit --config remains relative to the invocation directory and is stored
as an absolute identity; outside a non-bare worktree fails without writing.
First use is always explicit: run cara status, then cara init. Init atomically
creates .caravan/config.yaml only when absent, verifies repository permission,
default-branch protection, and squash auto-merge policy, and creates only the
four fixed control labels (caravan, caravan-evicted, caravan-force, and
caravan-closed), the opt-in caravan-join-skipped label when greedy admission is enabled, and
configured priority labels. It never overwrites an existing config or label and
never mutates a pull request. Repeated init calls are verification-only no-ops. Before creating any missing
label, init reads the REST core rate-limit resource and requires a conservative
budget for every bounded create/reread step. An exhausted or near-threshold
budget returns github_rest_rate_limit_wait with exact core/GraphQL evidence,
reset time/delay, completed/pending labels, and mutation=false; wait for reset
rather than hot-looping. A fully initialized repository performs no rate probe.
If label metadata differs, reconcile it manually and retry. The legacy active
label 1D76DB / Active member of a Caravan merge chain is explicitly
compatible and preserved. See docs/first-use.md.
cara van next on the default branch enters the first caravan head; ordinary
cara next remains chain-local and requires the current branch to map to an
open caravan PR. Navigation follows the exact provider generation after a Cara
physical rewrite. If a clean, non-current destination branch is stale locally,
Cara atomically retains its old OID under refs/cara-backup/navigation/*,
advances the named branch to the reverified provider OID, and reports the backup
ref before checkout. It never resets the current branch or a branch checked out
in another worktree, and never discards a local generation.
For human flow testing, cara loop --manual [--shell 'zsh -i'] runs normal
sync-all ticks and opens a real inherited-TTY shell only at an
external_decision. Cara writes the complete bounded decision to a private
file and exports CARA_DECISION_FILE, CARA_DECISION_CODE, and
CARA_REPOSITORY_PATH; the shell starts in a safe affected/repair workspace
when available. Exiting zero never claims success—it causes exact provider
rediscovery and another tick. Nonzero exits preserve evidence and stop. Manual
mode is refused for JSON/MCP/non-TTY use; production hooks remain noninteractive
machine orchestration.
Use cara help for the agent operating loop and recovery rules. Use --json
for stable mcp-cli envelopes. Operation-lock owner files stay below 16 KiB:
large syncs retain schema-versioned counts, deterministic hashes, and bounded
first/last samples instead of copying full plan/receipt/event histories into the
lock; GitHub rediscovery remains recovery authority.
cara web serves the primary visual operations surface directly from the Cara
binary; HTML, CSS, and JavaScript are embedded at compile time, with no CDN or
separate deployment:
cara web --repo /path/to/repository
cara web --repo ~/src/a --repo ~/src/b --poll-seconds 30 --read-only
cara web --repo . --listen 127.0.0.1:4774 --open
CARA_GITHUB_WEBHOOK_SECRET=... cara web --repo . \
--github-webhook-secret-env CARA_GITHUB_WEBHOOK_SECRET \
--github-installation-id 12345 --webhook-sync
CARA_GITHUB_WEBHOOK_SECRET=... cara web --repo /srv/a --repo /srv/b \
--github-webhook-secret-env CARA_GITHUB_WEBHOOK_SECRET \
--github-installation-id 12345 --webhook-sync --hosted--hosted is the optional deployment contract for pre-provisioned checkouts: it
requires signed webhooks, one exact installation, --webhook-sync, and per-repo
app_installation auth plus remote_fenced writer with an exact slug. Ambient
auth, local_only, mixed installations, or --read-only fail closed at startup.
Hosted workers mutate only from verified webhook deliveries; interactive
dashboard mutations are refused, because the same-origin CSRF token is not
authentication. It provisions no clones and performs no failover; see
docs/github-app.md. Default cara web is unchanged.
Repository inputs are explicit filesystem paths rather than slugs because every
read and mutation remains bound to that exact worktree, config, operation lock,
and provider identity. The initial server refuses non-loopback listeners,
canonicalizes and deduplicates paths, periodically returns the same typed status
used by CLI/JSON/MCP, and applies strict CSP/anti-frame/no-store headers. The
responsive one-page dashboard renders trail-linked caravans, linked GitHub PRs,
exact generations and check failures. Repositories and attention decisions live
in independently collapsible left/right sidebars, leaving the center for repo
metadata, compact active topology, and the Saloon. The Saloon
deterministically groups unenrolled PRs as Ready to Roll, Saddling Up, Other,
then Bounty List; each group is independently collapsible and its per-repository
open/closed state survives polling renders. It evaluates a bounded set of exact
current destinations for each candidate and shows Ready (main, PR #tail...),
Conflicting (main, PR #tail...), or checking/unknown evidence independently
from priority/FIFO position. Mixed PRs show both compatible and conflicting
targets; stale, truncated, or unevaluated targets are never called Ready. Fresh
candidate evidence moves fixed unjoined PRs back to Ready, while draft/incomplete
and skipped/evicted PRs remain Saddling Up and Bounty List. Plan sync runs the
same fresh physical/conflict/lease and
first auto-admission selection preflight without provider writes, then renders
ordered exact actions and rediscovery barriers before Apply. Bounded inner lists keep fleet topology and attention queues
visible without an unbounded page. The Evidence drawer retains the latest typed
action receipt, CI lineage diagnostics, canonical events, and hook delivery
outcomes; the Config drawer shows the effective parsed policy with hook commands
redacted. Eligible active heads expose audited Force/Unforce controls, while
unenrolled Saloon PRs expose configured priority/FIFO controls; both require an
actor and reason and call the same typed force/priority APIs as CLI/MCP rather
than editing labels directly. Mutating requests are accepted as bounded per-repository action jobs. Acceptance
binds both the displayed refresh sequence and a deterministic mutation-authority
fingerprint over exact config/default/PR/check/topology/pause facts. Poll or
webhook refreshes coalesce behind queued/running actions; harmless sequence drift
with the same fingerprint may proceed after locks, while real provider/config
drift returns expected/actual fingerprints and mutated=false. The browser can
reconnect and poll durable operation-lock checkpoints while a long sync, split,
evict, join, or repair continues. Evidence includes a bounded
Cara event/hook journal and terminal typed receipt; concurrent actions against
the same repository are refused. --read-only leaves read-only preflight available but disables every
mutating control. Interactive actions use same-origin CSRF, exact snapshot
sequences, and existing typed Cara operations and receipts; they never execute
arbitrary shell input.
An optional POST /api/v1/webhooks/github endpoint accepts GitHub App webhooks
only when an HMAC secret environment variable and exact installation ID are
configured. It verifies X-Hub-Signature-256, installation, explicit repository,
and bounded delivery/event IDs; delivery IDs are durably deduplicated under
common Git state. Default-branch pushes, PR lifecycle changes, and check/workflow
updates coalesce into a fresh status refresh or one bounded sync --all action
with --webhook-sync. Payloads are wake hints only: Cara always rediscovers
provider truth under its normal lock/budgets. Polling remains a low-frequency
reconciliation fallback; webhook counters are secret-free dashboard state. Use a
TLS reverse proxy/tunnel to the loopback listener and never store the webhook
secret in .caravan/config.yaml. Configure the GitHub App for JSON delivery and
subscribe to Push, Pull request, Pull request review, Check run,
Check suite, Status, and Workflow run events; read-only Metadata,
Contents, Pull requests, Checks, Commit statuses, and Actions permissions are
sufficient for delivery. The ordinary Cara/GitHub credential—not the webhook
payload—still authorizes any --webhook-sync provider mutation.
When sync returns a typed head or link conflict, do not create a nested worktree, update a local PR ref, or hand-push a guessed generation. Start an exact Cara-owned session instead:
cara repair start --pr 1962 # merge current default
cara repair start --pr 1959 --target-pr 1962 # merge exact predecessor
cara repair status --session pr-1962-<generation>
# For a typed semantic/CI decision, authorize one exact agent identity
cara repair authorize-agent-edits --session pr-1962-<generation> \
--actor caco-merger --reason <reviewed-decision>
# Optional narrower deterministic restoration from one reviewed source commit
cara repair grant --session pr-1962-<generation> --path README.md \
--source-revision <full-sha> --actor <actor> --reason <reviewed-contract>
# Resolve/stage reviewed edits; broad edits must use the authorized actor
cara repair continue --session pr-1962-<generation> --actor caco-mergerA session-level agent-edit authorization is bound to exact repository, PR, provider head, target, config, session manifest, actor, reason, and expiry. It lets that agent add, modify, rename, or delete ordinary repository files in the isolated workspace after a typed semantic/CI decision. Continue still rejects unstaged/untracked files, unresolved conflicts, Git internals, symlinks/gitlinks, secret-like paths, identity drift, excess path/diff bounds, and actor mismatch. Before commit it records the complete bounded path list plus path, staged-index, and binary-diff fingerprints; publication is non-force and explicitly requires fresh CI.
A semantic grant is distinct from a mechanical conflict: it is bounded, expiring, and bound to session/repository/head/target, actor/reason, exact one-parent source commit, source/base blobs, source patch fingerprint, original index blob, and expected merged result blob. Cara three-way applies and stages that reviewed source change; continue rejects any later byte drift or ungranted path. Before continue, the same granting actor may revoke exact paths; Cara preflights the whole set, restores and stages each pre-grant blob, and records durable bounded revocation receipts without provider mutation. Grant and revoke reconcile exact receipt/index states after interruption, including a staged result or restored baseline whose final manifest publication did not complete.
Repair uses an independent clone below Git's common Caravan metadata. It seeds
content-addressed objects from the current canonical checkout, binds a separate
explicit provider origin, and minimally fetches the exact recorded head and
target with blob filtering. A dirty caller worktree, daemon-internal origin,
and locally diverged PR refs are irrelevant and remain untouched; transport
resume reuses a valid partial repository instead of retransferring it. The manifest persists the
exact provider head and target, allowed conflict paths, mechanically staged
baseline, config path, lifecycle state, and publication receipt. Continue
rejects unstaged/untracked files, unresolved markers, edits outside the typed
conflict scope, changed parents, and moved provider heads. It creates the exact
merge commit itself, publishes only by ordinary non-force fast-forward, verifies
the provider ref, and resumes sync --all from the clean managed workspace.
Interrupted committed or published sessions resume idempotently; nonterminal
workspaces are preserved rather than deleted. After inspecting status, an
operator can explicitly remove abandoned local state with repair abort --session ID --confirm; abort never changes provider state.
The reviewed least-privilege App permissions, broker schema, branch rules,
webhook setup, attribution limits, and single-writer contract are in
docs/github-app.md. The cross-host CAS lease protocol,
activated by writer.mode: remote_fenced, is documented in
docs/remote-writer-lease.md; the exact machine-checked baseline is
docs/github-app-policy.json.
Cara's gh subprocesses are authenticated. An explicit GH_TOKEN or
GITHUB_TOKEN is used directly; otherwise Cara selects a repository-accessible
account from gh auth and injects its token without printing it. Explicit
ambient tokens are validated by the first real provider request rather than a
redundant per-process REST probe.
Opt-in local GitHub App mode first requires repository policy
github_auth.mode: app_installation with exact non-secret app_slug and
installation_id. Missing policy defaults to ambient. Production startup then
requires matching CARA_GITHUB_AUTH_MODE=app_installation, one executable
CARA_GITHUB_APP_CREDENTIAL_COMMAND, CARA_GITHUB_APP_SLUG, and
CARA_GITHUB_APP_INSTALLATION_ID. A broker path alone never activates App mode;
policy/runtime mismatch, unknown mode, or incomplete settings fail closed
without falling back. Cara passes
exact repository/host to the broker in secret-free environment and accepts one
JSON object containing token, app_slug, installation_id, repository, and
expires_unix_secs. Repository and expected identity must match; expired or
near-expiry responses fail closed. Valid credentials are process-cached with a
60-second refresh margin and concurrent refresh is single-flight. Broker stdout
is parsed but never rendered.
The same cached installation principal authenticates remote Git operations over an exact HTTPS repository. Cara installs a process-local Git credential helper through secret environment configuration: the helper program text contains no secret and reads the token only from environment; token-bearing scripts, argv, remote URLs, and persisted Git config are never created. Existing credential helpers and interactive prompting are disabled for that child. SSH, plaintext HTTP, non-GitHub/local remotes, explicit repository mismatch, and credential-bearing URLs fail before the remote command. One authentication failure refreshes and retries under the same deadline; a second failure stops. Ambient mode does not install or invoke this helper.
Status, check, sync, loop, JSON, and MCP receipts expose secret-free provider
telemetry: authenticated source class, total/GraphQL/REST/gh-CLI call counts,
App slug/installation/expiry plus exact Git transport/repository when selected,
and the latest GraphQL cost/remaining/reset evidence. The merge-candidate query
collects rateLimit in-band, so observing budget costs no extra request. Set
CARA_GITHUB_AUTH_KIND=github_app_installation beside an installation token to
make that non-secret identity explicit in receipts.
For automation, prefer one event-driven Cara loop/controller over independent
agent polling. A GitHub App installation token gives a separate least-privilege
installation bucket and webhook identity, but does not replace query batching,
generation-aware caching, adaptive backoff, or exact provider re-reads before
mutation. In GitHub Actions, mint the short-lived token with the repository's
approved GitHub App token action, export it as GH_TOKEN, and run bounded
cara loop --once ticks from PR/check/default-branch events plus a schedule.
Never store app private keys or access tokens in .caravan/config.yaml.
Version tags publish update-compatible cara binaries for x86_64-linux,
aarch64-linux, and aarch64-darwin. Each target produces:
cara-<version>-<target>.tar.gz
cara-<version>-<target>.sha256
The tagged release workflow builds x86_64-linux and aarch64-darwin on the
matching registered runners. Every job uses a unique RUNNER_TEMP-scoped Git
configuration, so anonymous HTTPS rewrites never mutate operator-global state
on persistent runners. After a terminal platform failure and a reviewed workflow
fix on green main, workflow_dispatch accepts the immutable existing tag
plus an exact target; the generated matrix contains only that target, so a
Darwin backfill cannot rebuild or overwrite an already-published Linux asset.
aarch64-linux remains on the bounded just release-backfill-target path until
more than one suitable runner can accept it.
When a repository pins Cara through a flake, flake.lock is the upgrade path
of record and nix flake update is the normal way to move versions. Self-update
remains available as a deliberate override for pulling a GitHub release anyway:
it installs in place for a user-managed binary, and for a Nix- or
Homebrew-managed binary it installs into a user-owned directory instead
(CARA_SELF_UPDATE_INSTALL_DIR, else ~/.local/bin). It never writes into a
store path or cellar, and it refuses when the chosen directory does not precede
the managed binary on PATH, because an install that PATH would never resolve
is worse than a refusal. cara status reports the resolved executable, its
hash, and whether it came from a store path, so a shadowing install is provable
rather than inferred.
The archive contains cara-<version>-<target>/cara, matching
updatable-cli's default asset strategy. The release workflow tests the tagged
source, packages each binary, verifies native archives by executing --version,
and uploads both assets to the GitHub Release. Public ecosystem source inputs
are fetched anonymously over HTTPS; the workflow needs no SSH deploy secret.
Use the bounded helper to keep Cargo.toml, Cargo.lock, and flake.nix
aligned and create the tag:
./scripts/release.sh patch --dry-run # inspect the next version
./scripts/release.sh patch --no-push # commit + tag for local review
./scripts/release.sh patch # atomically push branch + tagAn explicit version such as ./scripts/release.sh 0.2.0 is also accepted.
That one-shot path commits, tags, and pushes together. It deliberately ignores
the checkout's origin: managed Cacophony worktrees point origin at a local
daemon mirror. scripts/release-remote.sh resolves the canonical GitHub
repository from CARA_RELEASE_REPO (or Cargo.toml's package repository), uses
CARA_RELEASE_REMOTE only when it names the same github.com repository, and
defaults to its explicit GitHub SSH URL. Publication requires a clean local
main exactly equal to canonical GitHub main, atomically pushes main plus the
new tag there, then re-reads both remote refs before claiming the workflow will
run.
Caravan's reviewed flow instead lands the release: vX.Y.Z bump through the
ordinary agent and reintegration lifecycle, so by tagging time the bump is
already on main and release.sh refuses with "already current". Use this
sequence instead, and prefer just release-tag over a hand-rolled git tag:
it fetches canonical GitHub main (never daemon-mirror origin), verifies the
commit is contained there and that Cargo.toml, Cargo.lock, and flake.nix
all declare the exact version, refuses an existing local or true-GitHub tag,
and verifies the peeled remote tag after push. It never moves or force-pushes a
tag.
./scripts/release.sh 0.0.75 --no-push # bump Cargo.toml/Cargo.lock/flake.nix
git tag -d v0.0.75 # drop the provisional local tag
just validate # green on the versioned commit
just stress # timing-sensitive changes: shake out races
# land the release commit through the normal reintegration flow, then:
just release-tag v0.0.75 # fail-closed tag at exact landed mainA tag is immutable once pushed. If tagged source is wrong, supersede it with the
next patch version rather than moving or reusing the tag. If green immutable
source is only missing a platform asset because release infrastructure failed,
land the workflow repair on green main, wait for the original run to become
terminal, then dispatch that existing tag for the missing target only; never
overwrite already-published assets. A bare tag with no release object is not
returned by the releases API, so self-update ignores it.
Run ./tests/release_contract.sh target/debug/cara after building to exercise
the asset/checksum layout and cara self-update status with an isolated home;
it never stages or installs an update over the developer binary.
When a runner for one platform is down or exhausted, just release-backfill-all <tag> (or just release-backfill-target <tag> <target>) builds the exact
tagged source in a detached worktree and uploads the same assets to the existing
tag. It never creates, moves, or force-pushes a tag.
Downstream consumers that record a reviewed digest per released platform derive
those rows with just release-pin-rows <tag>. It re-verifies every published
.sha256 against the archive it downloaded and fails closed when a platform is
unpublished, so a partially published release can never be pinned as if it were
complete. docs/release/v0.0.9-rollout.md is
the worked example, including the read-only rollout proof and rollback pin.
Self-update is bound to the exact running executable, not a hard-coded default.
status, check, and run require that executable to be the first executable
cara on PATH and place cara_next beside it. Existing ~/.cargo/bin/cara
and ~/.local/bin/cara installations migrate automatically by updating in
place. A shadowed binary, a renamed/test binary, Cargo target/debug or
target/release binary, and package-manager location fail closed. For another
intentional user-managed directory, set CARA_SELF_UPDATE_INSTALL_DIR to its
absolute parent; it must still be the active first PATH entry. Startup staged
promotion is likewise skipped for unmanaged/development binaries.
The binary uses:
mcp-clifor shared CLI/MCP typed tools and structured envelopes;updatable-clifor GitHub release self-update tools;feedback-clifor configurable structured reporting.
The Nix flake pins Harry's current FlakeHub nixpkgs revision. The three public
ecosystem repositories are also pinned as flake = false source inputs and
patched into Cargo, so sandbox builds neither evaluate their independent flakes
nor fetch Git dependencies at build time.
Repository policy and hooks live at .caravan/config.yaml. stack_type defaults
to caravan, preserving the existing implementation without probing GitHub's
native Stack API. The explicit github value plus a reviewed
stack_rollout.mutations_opt_in enables exact Stack membership, reshape, and
lock-fenced landing. Capability, complete inventory, unique mapping, exact
generation, holds, compatibility, CI, and unsupported force intent all fail
closed before provider mutation. Joining the second member writes a sealed local
native-membership continuation before Stack creation; successful create clears
it, while a provider failure returns the exact checkpoint instead of telling a
scheduler to re-admit an already-labelled PR. A legacy or checkpointed
multi-member caravan with zero mappings is repaired only through
cara native-stack recovery-preview followed by recovery-apply with the exact
plan hash. Apply independently revalidates ordered membership, immutable
heads/bases, current CLEAN/green evidence, rollout capability, and complete
mapping truth. A legacy first-child gap permits zero mappings; a checkpointed
append permits only its exact provider prefix; a response-loss retry permits one
exact desired mapping. Apply then issues at most one idempotent Stack create/add.
It never changes a source head, base, label, comment, or merge state;
multiple/partial mappings and singleton inputs fail closed. The installed
gh stack CLI does not merge Stacks; GitHub's web merge uses the top-SHA-only
async REST endpoint. A disposable sandbox proved an unlocked lower rewind can
merge at a changed generation. A follow-up sandbox proved that an exact-ref
ruleset fences external writers, but also exposed GitHub's dangerous partial
merge postcondition: the provider rebases an unselected suffix and thereby
replaces its source head and starts CI again. Cara therefore locks every source
ref in the complete Stack generation, never only the ready prefix. Native sync
submits only a fully ready Stack; a ready prefix with a blocked suffix returns
github_stack_partial_prefix_requires_tail_eviction before lock/submission and
requires typed evict/split first. Readiness binds each immutable source head to
its exact fresh two-parent synthetic merge candidate (current main for the root,
predecessor candidate base for descendants), so provider candidate regeneration
rather than scheduler force-pushes drives CI. The adapter acquires, verifies,
checkpoints, and exactly releases the complete lock. The ruleset path requires
the explicit, conditional Administration(write) App permission documented
below; default Caravan mode never needs it.
GitHub exposes no arbitrary Stack remove/reorder, so evict and split are a
phased unstack/rebuild transaction whose sealed checkpoints record
preflighted, unstacked, reshape_applied, rebuilding, rebuilt, and
verified and always persist provider_atomic: false. Retries resume from
provider truth without repeating a proven unstack or replacement creation.
max_caravan_length bounds one caravan as a merge batch within GitHub's
2..=100 Stack range. It is absent by default, preserving the existing dynamic
capacity model, and defaults to 8 only under stack_type: github. A full batch
is never extended: admission uses another compatible caravan or opens a new one,
while sync never waits for occupancy. Native sync lands a fully ready batch; a
partial ready prefix first requires typed suffix reshape so GitHub cannot rewrite
an unselected source head. A one-member caravan is valid but has no representable
GitHub Stack object created by Cara. Native-mode sync nevertheless performs a
fresh complete inventory read and rebinds every provider Stack intersecting any
candidate member to its exact current ordered PR/base/head/state generation
before root-first landing, promotion, auto-merge disarm, force handling, or
ordinary merge. Only an authoritative singleton with proven zero intersections
uses Cara's ordinary root validation/merge path. One exact singleton or
multi-entry intersection uses the lock-fenced asynchronous Stack backend. When
GitHub retains a merged prefix and retargets the first remaining open member to
the Stack base, Cara proves every predecessor is freshly merged, its merge
commit and recorded base are contained by current main, the historical prefix
order is exact, and the current open suffix exactly equals the Cara caravan.
That typed merged_predecessor_base_collapsed frontier stays on the native
backend; planning skips the proven merged prefix and selects from the first
remaining open entry. Appending a join may let GitHub rebase exactly the new
tail head while committing Stack membership; Cara accepts that converged
postcondition only when every previous entry remains byte-for-byte exact, the
new tail retains its PR/branch identity, and its base is the sealed predecessor
head. Exact retries recognize the same converged tail without another write.
A closed Stack whose complete membership is freshly merged is terminal audit
history, not github_stack_orphaned or base drift merely because GitHub
collapsed merged bases. Partial, cross-mapped, stale, truncated, unreadable, or
multiple intersections fail as non-retryable external decisions without a
landing write. If a Stack
races into existence after the absence proof and GitHub refuses the ordinary
merge, Cara returns github_stack_membership_detected_during_owned_merge
instead of retrying the same synchronous call. Zero intersections for 2+
members still fail closed. Native mode is enabled per repository only with a
reviewed allowlist:
min_cara_version: "0.0.65"
stack_type: github
max_caravan_length: 8
stack_rollout:
mutations_opt_in: true
reviewed_by: "operator/change-ticket"
sync:
head_merge_actor: caravan
rebase_on_join: falseThe optional rebase_on_join: true mode
physically rebases each owned PR's candidate-only, linear commit range onto its
exact predecessor under an exact force-with-lease; it is disabled by default and
never mutates the caller's worktree:
version: 1
repository: owner/name # optional; required only when git remotes cannot name it
force_merge: false
stack_type: caravan
github_auth:
mode: ambient
writer:
mode: local_only # or read_only / remote_fenced; see remote-writer-lease.md
rebase_on_join: false
command_timeout_secs: 30
repair:
materialization_timeout_secs: 180
sync:
allow_fetch: true # default: sync policy/hooks come from fetched origin default
actions:
join_unlabelled_prs: false
terminal_red:
action: block # default; opt in to `park` for strict queue liveness
max_candidates_per_tick: 8
max_mutations_per_tick: 64
max_github_requests_per_tick: 256
max_duration_secs: 120
missing_required_runs_grace_secs: 300
retrigger_missing_required_runs: true
loop:
interval_secs: 60
journal:
max_bytes: 8388608
max_archives: 3
hooks:
sync_failed:
command: ./scripts/on-caravan-sync-failed
timeout_secs: 30
blocking: false
conflict_detected:
command: ./scripts/request-caravan-conflict-repair
timeout_secs: 30
blocking: falsecara sync, cara plan sync, and loop ticks are safe to invoke from any
branch. Unless an explicit --config or reviewed sync.allow_fetch: false
opts out, Cara fetches the exact origin/HEAD branch, pins its commit, and runs
against a detached temporary worktree sharing the repository's common Git
metadata. Policy and repository-relative hooks therefore come from one
fetched default-branch snapshot while the caller's branch, HEAD, index, tracked
changes, untracked files, and in-progress Git state remain untouched. The
remote-tracking default ref is rechecked after provider discovery and before the
first possible provider mutation; movement returns a typed retry instead of
mixing policy generations. CARA_ALLOW_FETCH=true|false is the strict
per-invocation override.
conflict_detected is a versioned, secret-free notification for one exact open
PR generation with a mechanically proven conflict. It includes immutable
head/base/default/target identities, conflict class, bounded paths, operation
ID, and stable repository+PR+head dedupe key. Unknown or stale provider state
emits nothing. Candidate conflicts remain advisory, repeated ticks may redeliver,
and the delivery is forcibly best-effort even if configured as blocking. The
hook may request a first-party repair; it is not itself authorized to merge,
rebase, push, comment, or mutate topology.
sync.terminal_red.action configures deterministic latest-verdict liveness.
block is the backward-compatible default: terminal red stops the tick. park
adds caravan-parked to the exact caravan head, disables its auto-merge,
preserves every member/base/head, excludes it from active convergence/tail
capacity, and allows independent green candidates to advance. Pending/running
and superseded historical red never park. Once parked, queued, expected,
running, unknown, or absent current evidence preserves the label without a
provider write. Only a nonempty, fully green latest verdict for every member,
with every protection-declared context proven satisfied, removes it and
re-enters the caravan at its original FIFO age. A check-sensitive provider
reread fences either label transition, so a new same-head check generation
refuses before mutation. Hooks may repair parked work but are never required for
unrelated throughput.
Run cara init after enabling park so the fixed label exists.
If the same immutable parked head later becomes green but no normal sync tick can
release it, use the reviewed cara unpark surface—not resume, rejoin, or a
raw label edit. It requires exact repository/PR/head/base, current Caravan
membership generation, the durable caravan_parked event fingerprint,
--provider-state open_parked, actor, and reason. Cara freshly verifies one
open enrolled non-evicted parked head, no explicit pause/recovery fence, and the
newest authoritative green check generation under the normal writer guard and
provider lease. It removes only caravan-parked, reruns fleet analysis, and
returns a durable idempotent receipt with old/new labels and evidence. Use
cara --json unpark --help (or the MCP unpark tool) to construct the reviewed
request.
If a stale closed-row cleanup stripped both caravan and caravan-parked from
that same open generation, use cara restore-parked (or MCP restore_parked)
after deploying a fixed Cara build. The request binds exact repository, PR,
head/base, prior membership generation, latest durable caravan_parked
fingerprint, --provider-state open_labels_missing, actor, and reason. Cara
requires the parking event's provider receipt to prove both prior labels on the
same generation, requires auto-merge to remain disabled, and restores both
labels in one complete-label write before rediscovering the identical parked
topology. Partial labels, an unpark event, close/merge/reopen drift, terminal or
evicted labels, missing provenance, capacity substitution, and replacement PRs
all fail closed.
sync.missing_required_runs_grace_secs is the bounded wait before a required
context with zero reporting run or check-suite lineage on the exact current head
is reported as missing_required_runs instead of pending. GitHub sometimes
never starts a run for a freshly rebased head; the pull request then sits
MERGEABLE/BLOCKED with nothing pending and nothing failed, so waiting is
futile. Cara reports that member in scheduler_status.missing_required_runs
with the exact PR, head, and contexts, degrades the disposition to
operator_action, and — when retrigger_missing_required_runs is enabled and a
rerequestable check suite exists on the unchanged head — issues exactly one
auditable rerequest and rediscovers once. Cara never pushes an empty commit,
closes and reopens the PR, force-pushes, or broadly reruns another generation to
work around it; head, base, branch, and membership are always preserved. A
partial provider read is reported as unknown_provider_state and retried rather
than being mistaken for an absence.
For a complete, fast, agent-dispatching setup, see
examples/hooks/: a caco cron entry runs one
bounded cara loop --once tick, and an idempotent hook files exactly one
deduplicated bead per canonical event, which normal controller dispatch routes
to an agent. Hooks stay blocking: false so a delivery failure can never roll
back completed provider work.
When enabling cumulative mode, set sync.max_duration_secs high enough for
physical planning plus the typed apply reserve. The validated repository value
is the one deadline for discovery, root-first evaluation, planning, mutation,
reconciliation, serialization, and hooks; there is no hidden deployment-specific
binary ceiling. Configured hook timeouts may shorten their child lifetime but
can never extend the remaining sync deadline. Before physical planning or
whole-member CI analysis, a Cara-owned tick evaluates already-admitted exact-
green roots and lands at most one; successful root landing
returns retry_tick immediately and defers unrelated inventory to the next
GitHub-cursor tick. cara status reports the required and retained reserve, the
processable prefix, the deferred members, the maximum admissible chain size,
and a safe next action for every caravan before any refusal, and
cara plan sync --all reports the exact prefix a tick would admit without
mutation.
Cumulative mode rejects fork heads, stale leases, and ambiguous ranges. Before
join creates/updates a PR, rewrites a branch, or changes membership, the
selected caravan root must target the exact current default generation; a stale
root returns join_root_stale_default and requires sync --all. The source
branch is separately bound to exact repository/branch/head, one merge-base
parent, source tree, binary-patch fingerprint/title provenance, selected tail,
and independently computed result tree. Stable git cherry patch identity plus
an effective merge-tree/diff against exact current main removes equivalent
patches already landed under different commit OIDs; mixed ranges replay only
the genuinely unique commits. Main changes outside the effective source range
are never replayed into the child. An empty effective source patch returns join_empty_source_noop with the complete receipt and zero
provider/branch mutation.
It preserves bounded owned two-parent candidate topology with
rebase-merges=rebase-cousins, so a stacked child is rooted on the selected
parent generation instead of preserving a stale cousin root. It independently
proves the exact clean merge-tree result and retains old/new commit-parent
mapping in the plan/receipt. A redundant merge of target history may be elided
only when every external parent was already target ancestry and the final tree
still equals that independent proof; the receipt names every elided merge.
Cara-created commits are also checked before push for parent-monotonic author
and committer dates, and a reconstructed merge directly involving the selected
target must name its actual branch. Octopus roots, unowned external parents,
unsafe topology drift, misleading merge provenance, timestamp reversal, or tree
mismatch stop before any write. When any other Git replay changes the commit
count, rebase_topology_changed reports source/rebuilt/dropped counts and OIDs,
likely already-present/empty-patch causes, and a safe source-rebase or
reviewed-repair next action. Every exact branch generation that Cara actually
rewrites receives one GitHub-visible line naming the typed reason (default
advanced, parent advanced, join, eviction, reshape, or reviewed repair) and the
short old/new head OIDs. A same-line hidden marker deduplicates provider retries;
dry-runs, failed pushes, and already-satisfied generations post nothing.
Membership rewrites one candidate. After the mandatory provider rediscovery,
join/rejoin require the exact live tail from
the rebase receipt, while new/renew require the exact current default and no
inferred candidate membership; a new caravan correctly has no join tail.
Candidate-head/default/tail drift stops before membership writes with an
operation-specific resumable receipt. cara sync --all builds every selected
caravan head-to-tail, feeding each retained planned
head into its child. It materializes each generation exactly once in a retained
detached worktree, then verifies every conflict, PR precondition, remote head,
dry-run permission, and exact lease across the complete plan before the first
write. Planning and final no-write verification stop at a precommit deadline
which preserves the apply reserve inside the one configured operation deadline.
That reserve is derived from the operations the tick actually runs: a member
whose exact ancestry already holds costs no push or auto-merge drop, and durable
force labels add no rewrite control mutation, so a completed prefix makes every
later tick cheaper. When
the complete reserve cannot remain but the irreversible part can, the tick still
plans and verifies the complete graph and applies the largest exact
root-to-descendant prefix that fits, checkpoints its receipts, and succeeds with
a retry_tick disposition plus the admitted/deferred member lists; the resumed
tick never replays a completed provider mutation. Growing a caravan past the
size the configured deadline can guarantee to drain is refused up front with
caravan_budget_capacity_exhausted while the admitted prefix keeps draining.
That bound is priced by the same actual-work reserve as required_ms, so
raising a proven-safe command_timeout_secs never closes admission. A bound
below two members is impossible to emit: a one-member caravan is never reported
at capacity, and an arithmetic result below that floor fails loudly as
caravan_budget_capacity_defect with the deadline that repairs it instead of a
drain suggestion that cannot.
physical_sync_budget_insufficient remains for the case where not even one
pending member fits, and reports required/remaining time, configured deadline,
maximum admissible chain size, processable prefix,
partial-or-complete plan count/hash, zero writes, and configuration guidance.
Only independent, disjoint caravans apply concurrently (at most two); each
caravan remains strictly parent-to-descendant. Confirmed control mutations are
checkpointed before branch apply. Apply revalidates the source/range branch,
current default, selected tail, result tree, and root provider precondition,
then lets the exact force-with-lease push detect source-head movement without a
redundant post-mutation dry-run. A child provider base OID that lags an
already-advanced parent branch is accepted only as an explicit retained
ancestor range. Successful join receipts and durable join_failed journal
events retain source/target/result evidence and every completed provider
mutation. Pushes use only
--force-with-lease=refs/heads/<branch>:<old-oid>. A mandatory midpoint GitHub
rediscovery verifies every new head and replaces stale CI facts before normal
sync convergence. Errors preserve exact plan, completed-prefix, provider, and
lease receipts and never force-rollback rewritten branches.
GitHub Actions must also be configured to run for non-default PR bases.
pull_request.branches filters match the PR's base branch; a workflow restricted
to main will not run for B targeting A. Cumulative mode therefore requires a global pull_request trigger with no
branches or branches-ignore filter and activity types opened,
synchronize, reopened, edited, and labeled (usually also unlabeled). A
dedicated stack/full job can then skip unless base_ref == 'main' or the PR has
the caravan label. The labeled event closes the race where the base-edit
edited event occurs before Caravan adds its label. Physical ancestry cannot
override provider workflow filters; opt-in membership and whole-chain sync fail
with rebase_ci_trigger_missing when this trigger proof is absent.
For an existing repository, enable this first on a disposable or paused
caravan: commit rebase_on_join: true, run cara status, cara check, and then
one cara sync --all; verify the returned plans, leases, rewritten heads, and
fresh pending CI before widening the rollout. Roll back by reverting the config
to rebase_on_join: false. Do not force-push branches back: any successfully
applied prefix is authoritative and the resumable recovery is rediscovery plus
the same idempotent sync.
With sync.actions.join_unlabelled_prs: true, only sync --all and the
identical loop/loop --once path grow the fleet. Existing caravans always
reconcile first; a real graph, provider, compatibility, CI, or operator decision
stops before admission. The priority_fifo_greedy_v1 heuristic considers
caravans in their existing deterministic order and candidates in canonical
priority/FIFO order. It is best-effort rather than globally optimal: the first
compatible tail wins. When no tail works (or the empty fleet candidate cannot
form a head), Cara records exact candidate head/base, default, all tested tail
generations, compatibility reasons, config fingerprint, actor/time, and
heuristic version in a GitHub comment and adds caravan-join-skipped. Manual
new/join/rejoin consumes that advisory label. One tick is bounded by the
configured wall-clock, authenticated gh request, candidate, and mutation
limits. Cara will not start another automatic candidate when less than a real
bounded exact-Git reserve remains; the receipt exposes reserved/remaining
milliseconds and leaves that candidate for a later tick. Once selected,
sync-owned membership reuses the already-discovered fleet snapshot and receives
a fresh exact-candidate deadline instead of re-running unrelated fleet
compatibility. The result returns exact joins, skips, remaining candidates, and
continuation. Hosted automation should run bounded cara loop --once ticks from PR/check,
workflow, default-branch, and scheduled events rather than one unbounded job.
This proves cumulative tree content, not stable GitHub check identity. Because Caravan heads currently squash-merge, retargeting a child after its parent lands can change GitHub's merge ref and trigger CI again even when that cumulative tree was already tested. Instant no-rerun landing requires an ancestry-preserving merge mode or an audited exact-tree/check receipt policy, neither of which is currently implemented.
command_timeout_secs is the hard ceiling for lightweight git or gh
children and the complete operator-safe budget for cara status (30 seconds by
default). Stdout and stderr have independent hard capture bounds; exceeding one
returns command_output_limit with exact total/limit bytes and separate bounded
prefix/suffix evidence before JSON decoding, never a misleading malformed-JSON
error. Network-heavy repair clone/fetch/checkout uses the separately bounded
repair.materialization_timeout_secs (180 seconds by default), and persisted
repair status reports its exact phase, budget, process group, partial path, last
error, and resume/abort path. Exact retries reuse verified partial objects after
a sideband disconnect; changed repository/head/target/provider/config facts
still refuse rather than trusting the cache.
Human CLI errors render concise colored operational evidence rather than dumping
unbounded provider snapshots. Root-join drift lists only changed mutation facts
(head/base/labels/state/auto-merge), topology refusals show compact commit counts
and OIDs, and physical-budget decisions show required/remaining time. Evidence
over 4 KiB is summarized with a --json hint; JSON/MCP always retain the full
structured continuation. CI/check state transitions are deliberately excluded from topology, base,
label, comment, and auto-merge mutation identity, so queued→running progress
does not invalidate an otherwise exact sync/join retry. CI diagnostics and
rerun operations retain strict check/run/head identity.
Status propagates one absolute deadline through discovery, label inventory,
compatibility, and provider identity; every child receives only the remaining
budget. The 58-second read-only surface reserves 2 seconds for serialization and
5 seconds after compatibility for provider-backed/final projection, leaving
about 30 seconds for active proofs even after the measured 20.9-second,
117-candidate discovery. Root/default, cumulative-tree, and every adjacent proof
for active caravans are ordered before the bounded leading-candidate work; a
100+-candidate fixture pins that non-starvation rule. It performs the minimal
provider inventory before local graph work. Compatibility therefore yields with
a successful, unhealthy status_partial built from current bounded evidence —
even when no historical snapshot exists — rather than consuming the whole
deadline before a provider call. The receipt exposes candidate/caravan/
branch counts, planned/completed proofs, bounded skipped/deferred proof names,
phase timings, completion reserve, provider calls, explicit unknown fields, and
whether evidence is current_bounded_evidence or historical_last_good. Zero
provider calls omit the authentication verdict instead of diagnosing
authenticated: false. A separate 59-second CLI watchdog launches status in a
subprocess (one second inside Cacophony's collection bound) and atomically
checkpoints provider/structural evidence before
compatibility. Worker stdout/stderr go to parent-owned bounded files, never
pipes whose EOF an orphan can retain. On Unix the worker also owns a dedicated
session, so provider groups remain identifiable after an intermediate worker
exits and reparents them. If the executor wedges, the parent preserves the
original PID/group inventory through TERM and KILL, binds Linux identities with
pidfds, adopts orphaned providers as a child subreaper, adds any remaining
session members, and performs only a bounded exact-child reap poll before returning the checkpoint
with phase command_boundary_watchdog, still inside Cacophony's 60-second
adapter window. Envelope emission is never behind an unbounded wait. Other
timeouts terminate and reap
the child process group and return stable github_discovery_timeout evidence
with the exact phase, operation elapsed_ms/deadline_ms, retryability, bounded
output, and a mutation-free safe next action.
Discovery performs one bounded all-open PR query containing current check
rollups, derives the current PR and caravan-labelled members from that snapshot,
and uses a separate bounded branch-history query that deliberately omits check
rollups. If the bounded open rollup omitted a reused branch, one unique exact
OPEN same-repository PR may take precedence over older unlabelled history only
when its local, remote-ref, and provider head OIDs all match. Multiple open
reuses, forks, OID mismatch, or older caravan/caravan-evicted membership
history still fail closed. Provider command count therefore remains constant as open PR count
grows; compatibility subprocesses share the same whole-status deadline. Explicit
remote check/new/join/rejoin --pr first completes that bounded fleet discovery,
then re-reads and binds only the selected PR under a fresh
command_timeout_secs deadline. Its status timing adds
exact_candidate_provider_refetch and exact_candidate_merge_identity; later
compatibility, physical Git, provider mutation, and post-rewrite rediscovery use
the independently reserved exact deadline and still refuse any head/base drift.
Hooks receive one versioned CaravanEvent JSON object on stdin plus non-secret
CARA_EVENT, CARA_EVENT_ID, CARA_OPERATION_ID, CARA_REPOSITORY,
CARA_CARAVAN_ID, and CARA_PRS context. Delivery output reports only bounded
state/exit/byte counts, never hook output content. Best-effort failures remain in
the command output; blocking failures return typed hook_failure and never roll
back provider mutations already recorded by the event.
Every canonical event is first appended under the repository's common Git
metadata (caravan/events-v1.jsonl), so linked worktrees share one journal.
Secret-free hook delivery receipts are appended afterward; hook stdout/stderr
content is never stored. Appends and reads use a repository lock, exact event IDs
are deduplicated, torn final records recover safely, and the configured size and
archive count bound retention.
cara log # newest 100 event/delivery records
cara log --kind ci_failed --pr 42 --limit 20
cara log --json # stable bounded JSON envelope
cara log -f # existing tail, then new records until signal
cara log --json -f # newline-delimited streaming recordsOnly the bounded log snapshot is exposed over MCP; follow is deliberately
CLI-only and creates no queue cursor or authority.
Caravan intentionally owns no cross-process hook dedupe. A long-running hook can use an external lock and return success while that lock exists; repeated ticks then become visible no-ops rather than duplicate coordination.
unpark is deliberately separate from incident-hold resume. After deploying
a Cara build containing this transition, a live recovery must first capture
fresh cara status --json and cara log --kind caravan_parked --pr N --json
evidence, then invoke the exact reviewed request once and retain its JSON
receipt. Never test deployment by mutating the parked PR with an older binary.
For the motivating PR #2452, preserve head
8cbd01d9674e27a502f253e51e171d1b13822a53 and base
db3ceb84eea033a381aef3141988396ceb6d6ca4 as comparison evidence, but use the
freshly re-read values and membership/provenance fingerprints at execution time;
any drift must refuse.
Freeze one caravan without invalidating or blocking independent caravans:
cara pause --head-pr 42 --actor oncall --reason "incident INC-123"
cara status # active, expired, stale, or retired hold evidence
cara sync --all # skips #42 without mutation; continues the fleet
# After CI/operator recovery:
cara resume --head-pr 42 --actor oncall
cara syncPause changes only the exact head's auto-merge state and preserves labels,
branches, bases, children, and all PR heads. Optional --expires-unix-secs and
--external-reference metadata are bounded and non-secret. Expiry only changes
the status warning: no background loop can resume a hold. Resume is an explicit
audited action and fails closed if the head, base, labels, topology, compatibility,
or safe terminal check state no longer matches. When provider truth shows the exact
recorded head merged or closed, the hold is retired: it stays as history, never as an
active card, and never as an auto-merge repair request.
An audited external owner that must change one paused PR's base and reviewed one-commit head uses the separate versioned phase machine:
cara pause-recovery prepare --help
# Repeat the exact v1 identity on each call:
cara --json pause-recovery prepare ...
cara --json pause-recovery checkpoint-base ...
cara --json pause-recovery checkpoint-head ...
cara --json pause-recovery finalize ...
# Or, only after reverse-lease restoration to the exact old state:
cara --json pause-recovery rollback ...Every phase repeats the exact operation/idempotency/actor/owner generation,
repository slug (--repository-slug, distinct from global checkout path
--repository), pause and Caravan identity, target, old and desired base/head,
replacement tree, and reason. Finalize additionally requires virtual-merge
parents/tree and the replacement head's exact CheckRun/StatusContext counts.
Cara performs no provider mutation in this surface; provider_mutated is always
false. It independently rediscovers each checkpoint, keeps recovering and
recovery_drift pauses effective against concurrent sync, blocks ordinary
resume, and releases only after verified finalize or exact old-state rollback.
Exact terminal retries return the original durable receipt. The five matching
MCP tools are pause_recovery_prepare, pause_recovery_checkpoint_base,
pause_recovery_checkpoint_head, pause_recovery_finalize, and
pause_recovery_rollback; results are flat fields under the standard JSON/MCP
data envelope. See SPEC.md for the complete contract.
cara loop --once --json # one bounded sync --all tick for agents/schedulers
cara loop # foreground human stream until SIGINT/SIGTERMThe unbounded loop is deliberately not an MCP tool. Every tick starts from fresh GitHub state, and a decision-point/error tick fires its configured hook and stops instead of inventing an agent decision.