Landing lander
Use when the reasoning landing agent owns the PR queue and release path: hold the host-global owner lease, gather and reason over ready PRs, create and land multi-PR integration batches, resolve or defer conflicts, reconcile/close source PRs, track deploy/smoke evidence, and keep LANDING_AGENT_OWNS_QUEUE single-owner behavior safe. Also defines the always-on runner, takeover/recovery, and the rules non-owner agents must follow.From its SKILL.md
npx -y skills add ottto-ai/agent-skills --skill landing-landerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
38.3 KB, ~9.2k tokens by cl100k_base, as published. Nobody here has run it
landing-lander
Own the ready-PR to production loop. This skill is for the one active landing agent, not for ordinary PR authors. The agent is expected to reason over the queue state, use git freely in scratch/integration branches, decide what to batch, resolve easy conflicts, defer risky PRs, and keep source PR authors informed until their work is merged, deployed, or explicitly blocked.
Do not reduce this to a deterministic FIFO script. The scripts below are safety rails and state helpers. They do not replace agent judgment.
This skill must be self-sufficient. A lander invoked with only this skill and the objective "own and drain the ready queue" should know the queue contract, batching preference, auth requirements, source-PR bookkeeping, and deploy closeout behavior without needing a one-off prompt.
Why batching is the point
Every integration PR costs a full CI + queue + deploy round. Serial singletons are the single biggest source of queue latency and CI/deploy spend. The owner's success metric is: most eligible ready PRs ride the next integration, singleton integrations are recorded exceptions, and one bad PR never blocks the clean rest.
Operating Contract
- Exactly one lander owns the queue at a time, enforced by TWO layers:
the host-global owner lease (
landing_owner_guard.py, one lease per repo per host, shared across all linked worktrees) for the agent session, and the process lock insidelanding_lander.pyfor each command invocation. - Active queue ownership requires repository variable
LANDING_AGENT_OWNS_QUEUE=1.0, unset, or delete means stand down and let the legacy queue path work. - The owner intake label is
ready-for-agent. ready-to-landis the legacy label. During cutover, migrate current ready PRs toready-for-agentwith the label helper.- The main throughput objective is to collapse many clean source PRs into a
small number of integration PRs. Prefer batches up to the configured max
(
LANDING_BATCH_MAX_SIZE, default10) whenever risk, checks, conflicts, and deploy surface allow it. - One-PR integration batches are exceptions, not the steady-state goal. Use one only when the queue has a single clean source PR, no other source PR can be safely combined, or the operator explicitly asks for a singleton. Record the singleton reason in the owner log.
- Existing
batch-land:*integration PRs are queue debt to reconcile, not normal source candidates for a new batch. Be aware of them so you do not duplicate or strand source PRs, but do not let blocked singleton integrations monopolize the cycle when there are other cleanready-for-agentsource PRs that can be batched. - Existing singleton integrations are replaceable queue debt until the Landing
Queue actually owns them. If a one-PR integration is still open/unqueued and a
better multi-PR batch is available, the owner may close/supersede that
integration, restore its source PRs to
ready-for-agent, and build one larger integration branch instead. Do this only after confirming the old integration has no active queue run and the source PR heads still match the integration body. - While an integration is queue-owned and merging, do not idle: prepare the next candidate batch, repair dirty sources, and reconcile deploy evidence. Check-pipelining (manual path): once the CURRENT integration is queue-owned (its queue run dispatched and past validate start), you MAY open the NEXT integration early so its 14-15m head checks run while the current one rides the queue — under ALL of these conditions: the next batch uses only sources NOT in the current one; its changed files do not overlap the current batch's (overlap = wait, or you buy a conflict rebuild); and you dispatch the next integration's OWN queue run only after the current is terminal (the queue slot stays serial). If the current merges first and master moves, the next's queue validate handles it (replacement run); rebuild only on an actual conflict. Never run two queue slots at once, and never pipeline more than one integration ahead.
- The lander must not silently strand source PRs. Every source PR included in an integration branch needs comments/labels pointing to its replacement integration PR, merge SHA, deploy/smoke evidence, or blocker.
- Close source PRs after their integration PR reaches
master. It is acceptable for deploy evidence to be added as comments/labels after close. - Use
batch-land,pr-landing, andrelease-deployskills as needed. Re-read theirSKILL.mdfiles before active landing, merge monitoring, or deploy monitoring.
Rules For Every OTHER Agent (non-owners)
While LANDING_AGENT_OWNS_QUEUE=1:
- Route a finished source PR to the owner ONCE with
.github/scripts/pr_landing.py land --pr <n>. That appliesready-for-agentand stops. Do NOT re-invoke it in a loop, re-add labels the owner removed, or poll-with-mutation. Watch progress read-only (gh pr view). - Never invoke
landing_lander.py --mode active,batch_land.py --apply, or deploy dispatch for queue work. Active lander commands refuse while a live owner lease belongs to another agent — do not work around that withLANDING_LANDER_SKIP_LEASE_CHECK(that seam is for tests only). - A source PR labeled
landing-batch-active(or commented as covered by an integration PR) is the owner's; do not relabel it back to ready intake. - If the owner looks dead, do not start landing yourself mid-conversation: check
landing_owner_guard.py status --json; if the verdict is stale, either touch<state>/owner-wakeand let the ensure loop revive an owner, or runrun_landing_owner.sh(withLANDING_OWNER_TAKEOVER=1only for a stale lease). Takeover of a LIVE lease is operator-only.
Always-On Owner Runtime
The owner is a headless Claude Code session (default claude-opus-4-8,
--effort high, budget-capped) spawned by the runner and revived by a launchd
ensure loop. All state is host-global under the MAIN checkout:
<main>/.local-state/landing-lander/ (the shared root resolves via
git rev-parse --git-common-dir, so every linked worktree sees the same lease,
lock, ledger, and owner log).
Components (all in .agents/skills/landing-lander/scripts/):
landing_owner_guard.py— the owner lease.claim(refuses a LIVE other owner;--takeoveradopts a STALE one),heartbeat,status --json,release. Liveness is a bundle: lease heartbeat age, the Python lander's active-owner marker for the same agent id, the Claude transcript mtime (~/.claude/projects/*/<agent-id>.jsonl), a spawned pid (dead pid = takeable immediately), and a phase-declared--expected-wait-untilwindow. Fresh window default 900s (LANDING_OWNER_FRESH_SECONDS), wait grace 600s.run_landing_owner.sh— claim-before-spawn runner: claims the lease, prepares the dedicated workspace worktree (~/worktrees/landing-owner-workspace, detached atorigin/master), rendersowner_prompt.md, spawns headlessclaude -pwith--session-id <lease id>, records the pid into the lease, waits, releases. The owner runs UNRESTRICTED by an operator decision: no spend cap, web tools and MCP available, permission prompts disabled. If the Claude spawn fails fast (non-zero exit withinLANDING_OWNER_FALLBACK_WINDOW_S, default 180s — auth/quota/plan exhaustion), the runner hands the SAME lease to a Codex fallback owner (codex exec,LANDING_OWNER_CODEX_MODELdefault gpt-5.5) so the queue never stalls on one vendor;owner-runs.logrecords which engine spawned. Knobs:LANDING_OWNER_MODEL/EFFORT/FALLBACK_MODEL.- Headless discipline: owners are one-shot
-psessions — the process ends when the response ends, killing background children and releasing the lease. Owners must wait in the FOREGROUND in bounded slices (re-invoking the resume-safe lander commands), never background-and-exit to "wait for a notification". ensure_landing_owner.sh— the revive loop (launchdStartInterval300s, one instance per label). Live lease → exit. Stale lease + work → spawn with takeover. No owner + work (ready labels, openbatch-land/*PRs, orrepair-requestedrepairs) → spawn.operator-holdPRs do not spawn an owner by themselves; they are parked on a human trust/scope decision. Idle → exit (this is the low-traffic light mode: no agent runs when the queue is empty, and the next labeled PR brings one up within ~5 minutes).install_landing_owner_launchd.sh install|uninstall|status— managescom.example.landing-owner-ensure.
Operator controls:
- Approve an
operator-holdPR (the alert IS the approval surface — the decision is recorded on the PR, not in any agent session; owners are ephemeral, so whichever owner spawns next continues the work): click Approve & Land on the macOS hold dialog, OR run the one-liner from the Slack alert:gh pr edit <n> --add-label ready-for-agent --remove-label operator-hold(add a PR comment for any nuance/constraints — the next owner reads the thread). Rejecting = comment the decision and close the PR, or leave it on hold with a note. - Force a spawn now:
touch <state>/owner-wake(next tick) or run.agents/skills/landing-lander/scripts/run_landing_owner.shdirectly. - Hold everything:
touch <state>/owner-disabled(and/or set the repo variableLANDING_AGENT_OWNS_QUEUE=0). Remove the file to resume. - Inspect:
landing_owner_guard.py status --json(who owns, phase, one-line note, activity age),tail <state>/owner-runs.log(one line per claim/spawn/release with engine=claude|codex),tail <state>/owner-log.md(per-cycle packets: what landed, evidence, deferrals),tail <state>/ensure.log, raw stdout under<state>/agent-logs/. - Read an owner session like a conversation (reasoning + every tool call):
python3 .agents/skills/landing-lander/scripts/show_owner_session.py(--listfor all sessions newest-first;latest/id-prefix;-n 80;--all). This is the first-class review surface — the interactiveclaude --resumepicker HIDES headless (-p) sessions, so owner sessions never appear there, from any directory. - Open one in the Claude Code UI anyway (verified): from
~/worktrees/landing-owner-workspacerunclaude --resume <session-id> --fork-sessionwith an id fromowner-runs.logor the viewer's--list.--fork-sessionkeeps the historical transcript untouched. Do not continue an owner session to issue new queue work (spawn via the runner instead). - Alerting: repeated fast spawn failures (both engines dying within the
fallback window — e.g. Claude quota exhausted AND Codex broken) raise a macOS
notification at 2 consecutive failures (~10 min) and every ~30 min after,
plus Slack when
LANDING_QUEUE_ALERT_SLACK_WEBHOOK_URLis set for the LaunchAgent. Streak state:<state>/spawn-fast-failures.count(cleared on any healthy session). The Python lander separately Slack-alerts breaker trips and deploy failures via the same webhook. - Engines: primary Claude Code
claude-opus-4-8at--effort high(overload fallbackclaude-sonnet-5mid-session); Codex fallbackgpt-5.5atmodel_reasoning_effort=high(LANDING_OWNER_CODEX_MODEL/CODEX_EFFORT).
Owner heartbeat discipline
The owner heartbeats the lease at every phase change and at least every 10
minutes of reasoning: guard heartbeat --agent-id <id> --phase <phase> --note "<one line>". Before a bounded external wait (queue run, CI, deploy), declare
it: --expected-wait-until "+<seconds>"; clear with --clear-wait after. Long
blocking landing_lander.py waits are covered automatically — the Python lander
touches its active-owner marker every poll, and the guard counts that as
activity for the same --session-id.
Takeover and recovery
Owners die (crash, budget cap, sleep, kill). Recovery contract:
- The ensure loop notices (stale verdict or dead pid — dead pid is takeable
immediately) and spawns a successor with
--takeoverwhen work exists. - The successor inherits context from three places, in order of authority:
GitHub state (open integrations, labels, queue runs, deploys) >
<state>/owner-log.mdcycle packets + leasehistory(previous owner's last phase/note) >state.json(current_integration_pr, cooldowns). - Reconcile before creating anything new: resume watching an open integration,
re-queue it exactly once if checks settled, close out merged ones
(
batch_land.py --reconcile-pr), then resume batching. Never open a duplicate integration for the same sources. - Every owner appends a cycle packet to
<state>/owner-log.mdafter each cycle and a final packet before exit — that file is the cross-owner memory. Keep packets compact: backlog seen, batch built, integration PR + head SHA, queue run ids, merge SHA, deploy/smoke evidence, deferrals with reasons, next action. This is MANDATORY, not best-effort: sessions that skip packets break the next owner's inheritance (the most common owner failure observed live). - The apply engine can produce a NARROWER batch than the shadow preview (a source's required checks may still be pending at apply time). Always read the cycle JSON's excluded list, record each exclusion + reason in the packet, and treat excluded-but-still-ready sources as your own next-cycle intake — they ride the next batch once their checks settle.
- An open integration you created or inherited keeps you on duty until it merges and its sources reconcile; exiting mid-watch forces a respawn churn cycle (~5 min gap + fresh-context cost) that a bounded foreground wait avoids.
GitHub Auth
Active owner work needs the repo-scoped fine-grained workflow token in macOS
Keychain service YOUR_GITHUB_TOKEN_KEYCHAIN_SERVICE, account $USER unless
LANDING_GITHUB_TOKEN_KEYCHAIN_ACCOUNT is set. Do not print the token.
Use it only for operations that need elevated repo-scoped authority, such as
workflow dispatch/requeue, owner-mode labels/comments when normal gh auth is
insufficient, repository variables, deploy orchestration, and workflow-file git
pushes. Prefer helpers such as landing_lander.py, pr_landing.py, and
release_deploy.sh; they read the approved Keychain token where needed.
If you must call gh directly with the token:
GH_TOKEN="$(security find-generic-password -a "${LANDING_GITHUB_TOKEN_KEYCHAIN_ACCOUNT:-$USER}" -s YOUR_GITHUB_TOKEN_KEYCHAIN_SERVICE -w)" \
gh <command>
The token must be limited to selected repository
<your-org>/<your-repo>. Required permissions for the full owner
path: Metadata: read, Actions: read/write, Contents: read/write,
Issues: read/write, Pull requests: read/write, Variables: read/write,
Environments: read/write, and Workflows: read/write when changing or pushing
workflow files.
If pat_readable is false in landing_lander.py status --json, or a helper
reports missing/insufficient fine-grained permissions, stop with an auth blocker.
Do not broaden the normal GitHub CLI OAuth token.
First Checks
Run these before active work:
python3 .agents/skills/landing-lander/scripts/landing_owner_guard.py --json status
python3 .agents/skills/landing-lander/scripts/landing_lander.py status --json
python3 .agents/skills/landing-lander/scripts/landing_lander.py host-preflight
python3 .github/scripts/landing_agent_labels.py ensure-labels --apply
status --json now includes owner_lease (verdict + holder) and
shared_state_root. If the lease belongs to a live different agent, stand down.
If owner_flag_enabled is false, do not mutate the queue; ask the operator to
flip gh variable set LANDING_AGENT_OWNS_QUEUE --body 1 (revert with --body 0
or delete).
Cutover migration:
python3 .github/scripts/landing_agent_labels.py migrate-ready
python3 .github/scripts/landing_agent_labels.py migrate-ready --apply
The apply path refuses unless LANDING_AGENT_OWNS_QUEUE=1, unless an operator
explicitly passes the helper's force flag.
While the flag is enabled, normal source PR agents that still call
.github/scripts/pr_landing.py land --pr <number> are routed into this owner
intake: eligible source PRs get ready-for-agent instead of ready-to-land or
a regular PR-backed merge. Owner integration branches such as
batch-land/integration-* are allowed to continue through merge/queue only when
they also carry the owner intake label and batch-land body markers, so the lander
can finish release closeout without letting source PRs spoof the integration
path. Because branch names, labels, and PR bodies are PR-controlled, the
integration exception is not automatic: only the active owner should pass
--allow-owner-integration when intentionally landing its own integration PR.
If the landing CLI cannot read LANDING_AGENT_OWNS_QUEUE, it fails closed and
does not merge, queue, or relabel the PR until repository variable state is
readable.
Scope Classes (what can ride the queue, and what to do instead)
The trusted Landing Queue validates changed paths SERVER-SIDE against
.github/scripts/landing_policy.py. Batching cannot bypass that, and
batch_land.py --include only helps for paths the queue actually accepts.
Classify every candidate PR:
| Class | Paths (summary) | Owner action |
|---|---|---|
| Queue-eligible | backend/, frontend/src|public|tests, .github/scripts/, docs/ai/kb/provider-impact/, tools/desktop-app/ app code/tests (an example project-specific allowlist widening), NON-PRODUCTION workflows frontend-ci.yml docs-kb-ci.yml zizmor.yml master-merge-notification.yml backend-ci.yml backend-full-tests.yml landing-queue-stale-label.yml (operator-approved 2026-07-05; double-keyed in NON_PRODUCTION_WORKFLOW_EXCEPTIONS + policy allowlist), top-level docs/dev/*.html + docs/efforts/*.html, and the exact allowlist files (incl. AGENTS.md, docs/efforts/current-efforts.html, generated frontend contract files) | Batch normally. For macOS app code note the queue has no swift CI lane: validation is author-side swift build/test plus the docs/KB + policy lanes; when judgment warrants (large/risky app change), run swift build/swift test --package-path tools/desktop-app in your workspace before including. |
| Additive DB migration | green backend PR with additive/well-understood Alembic migration | Batchable; inspect migration + combined deploy surface. Defer only destructive/risky/unreadable ones. |
| Repo tooling & docs outside queue scope | .agents/, .claude/, scripts/ (non-allowlisted), docs/** markdown, evals/ | NOT queue-eligible. Small validated operator-flow/docs fixes: land via the audited fast lane (batch_land.py --fast-lane --fast-lane-reason ... or the repo fast-lane flow) after running the paths' own validators. Otherwise defer to the author/owner lane with a precise comment and atomically replace ready-for-agent with operator-hold so intake counts stay true without respawning no-op owners. Never leave such a PR sitting in ready intake cycle after cycle. |
| macOS release/signing/publish actions | release dispatch, notarization, appcast, cask | macos-stable-release skill lane only; never batch through the queue. AGENT-RUN without per-release operator approval (operator standing approval) — do NOT park these on operator-hold; route to the release lane or a release agent. |
| Operator-gated | production workflows (the SENSITIVE_WORKFLOW_EXACT set) + all other non-excepted .github/workflows/** (incl. ci-runner-image-build.yml, oidc-role-guard.yml, video-render.yml), infra/terraform/**, secrets, prod infra, destructive migrations, landing_policy.py trust widenings | Never include without the operator's approval for the exact path. Defer atomically to operator-hold with exact risky paths named. |
When a non-queue-eligible class keeps recurring, propose a landing_policy.py
allowlist widening to the operator as its own reviewed change — widening the queue's
trust boundary is an operator decision, not an owner default.
Migration landing lanes, explicitly:
- Additive / well-understood (new tables, new nullable/defaulted columns,
new indexes, constraint rebuilds where existing rows trivially satisfy the new
grain): batch normally. Inspect
upgrade()yourself — adrop_that appears only indowngrade()is NOT destructive (marker false-positives happen; the 2026-07-02 rev-202 case is the canonical example). - Destructive or operationally risky (dropping/renaming used columns or tables, data rewrites, long-locking backfills, ordering-sensitive deploys): never inside a wide batch. Get the operator's approval for the exact migration, then land it as a SINGLETON integration so the deploy blast radius is isolated, watch backend deploy + smoke to terminal state before resuming batching, and record the sequencing in the owner log. If it cannot ride the queue (marker gate), the lane is an operator-approved fast-lane with the same isolation.
- Unreadable/ambiguous: defer with the exact revision file and why, ping the author, remove it from intake until answered.
Do not treat ordinary release-bookkeeping PRs as operator-sensitive just because they mention a release: a macOS stable pin/docs PR that only updates private repo pin JSON, tests, docs/KB, or the effort board after the release passed is a normal batching candidate. The operator-gated boundary is the release/publish/signing/infra/workflow ACTION itself, plus secrets, Terraform/OIDC, production workflow edits, destructive/risky migrations, or unclear approval.
Shadow Reasoning
Use shadow mode to inspect the queue without mutation:
python3 .agents/skills/landing-lander/scripts/landing_lander.py cycle --json
python3 .agents/skills/landing-lander/scripts/landing_lander.py gather --json
python3 .agents/skills/landing-lander/scripts/landing_lander.py assemble --json --max 10
Shadow cycle is a merge preview. It gathers ready-for-agent, builds scratch
candidate branches through batch_land.py --dry-run --skip-heavy --required-label ready-for-agent, reports included PRs, conflicts, excluded PRs, and does not run
heavy local tests. CI validates the final integration PR when active.
Reason over the report:
- Separate source PRs from existing integration PRs. PRs titled
batch-land:*or branches namedbatch-land/integration-*are already integrations; monitor, merge, close, defer, or supersede them as needed, but do not include them as source PRs inside a new integration batch. - Confirm the proposed batch makes sense by files, risk, ownership, CI, and deployment surface.
- For each conflict, inspect the conflict locally. Resolve generated/simple conflicts when clear; otherwise exclude the PR from this batch and comment why.
- For
outside trusted-queue scopeexclusions, apply the Scope Classes table above: batch what the queue accepts, fast-lane or defer the rest with exact paths, and never recycle the same exclusion without a decision. - Do not treat database migrations as automatically sensitive. A green backend PR with an additive or well-understood Alembic migration can be batched; inspect it and the combined deploy surface. Defer only if the migration is destructive, operationally risky, unreadable from the validated PR head, or needs explicit operator sequencing.
- Treat
docs/efforts/current-efforts.html, docs/KB source-card/index churn, and adjacent durable docs conflicts as normal lander-owned merge work when the conflict is textual. Use the batch-land owner repair path first; if it unions both sides, commits rerere-staged generated-doc resolutions, refreshes only generated KB source-card/index artifacts, and docs/KB validation is green, keep the PR in the batch. Defer only with concrete conflict files, validation tail, and author action when the text directly contradicts, validation fails, or the conflict includes code/tests/workflows, infra, migrations, secrets, or runtime behavior that needs a real owner review. For current-efforts.html specifically: preserve both sides' new links/PR/deploy evidence, keep the latest true state per effort row, and defer only when two edits make incompatible factual claims that GitHub/CI/deploy evidence cannot decide. - Prefer a clean multi-PR batch over singleton landing. Prefer a smaller clean batch over a large fragile one.
- Do not let one bad PR block the rest of the queue. Defer it, label/comment it, and keep landing clean work.
Active Cycle
After the operator flips LANDING_AGENT_OWNS_QUEUE=1 and the host preflight is
clean, run:
python3 .agents/skills/landing-lander/scripts/landing_lander.py --mode active cycle \
--session-id <lease-agent-id> \
--session-name '<name>' \
--session-source '<source>' \
--max 10
--session-id must equal your owner-lease agent id, so the lease gate, the
active-owner marker, and PR metadata all agree on who is acting.
Active cycle:
- Verifies the owner flag, owner lease, host, local lock, active-owner marker, and breaker.
- Gathers
ready-for-agentPRs and separates source PRs from existingbatch-landintegration PRs. - Reconciles existing integration PRs only enough to avoid duplicate work: monitor active queue runs, queue/requeue green integrations, close/deploy already-merged integrations, and defer or supersede blocked singletons with clear comments. Pending-check singleton integrations should not monopolize the cycle while other clean source PRs can be batched.
- Uses
batch_land.py --apply --skip-heavy --fill-to-max --allow-conflicting-sources --required-label ready-for-agentto open one integration PR for the safely mergeable source set.DIRTY/CONFLICTINGsources are scratch-merged instead of pre-excluded; only actual conflicts, policy gates, or validation failures are deferred.--skip-heavyskips the expensive backend/frontend lanes only; generated KB source-card/index repair and docs/KB validation still run locally so stale generated docs do not create red integration PRs. A pre-dispatch freshness gate re-checks the authoritativeorigin/mastertip (ls-remote) AFTER assembly and BEFORE pushing: if master moved it drops any now-landed source and rebuilds on the fresh tip, so aCONFLICT_REBASE_REQUIREDqueue-dispatch failure from a stale base should now be rare (it killed a 10-PR integration on 2026-07-05). The gate's decision is in the JSON output underfreshness_gate(outcome=not_moved/refreshed/freshness_gate_conflict); afreshness_gate_conflictis a clean no-op (nothing pushed) — rebuild and rerun. - Actively ensures that integration PR is queue-owned before and during the merge watch. If the first queue run skipped because PR-head checks were still pending, the owner reruns the canonical landing helper once checks settle instead of waiting for the full watch timeout. A watch budget expiring on a STILL-HEALTHY integration (open, checks pending, or a queue run queued/active) is not a failure: re-arm the watch or hand off with the integration OPEN. Close-and-recover only on terminal evidence (validate failed, closed unmerged, cancelled queue with red checks) — closing a healthy pending integration wastes its CI round and bounces sources back to intake. An in_progress/queued queue run is never "stale": quiet 10-15 minute gaps between its jobs (validate → push) are normal, so judge runs only by job states, never cancel a queue run or close its PR mid-run, and treat only 45+ minutes with zero job progress as an operator escalation, not a cancellation.
- Reconciles source PRs after merge.
- Deploys/monitors/smokes by merged SHA unless
--no-deployis set. Before dispatching the deploy helper, refreshorigin/masterand verify the merged SHA is included, then use the helper's latest-master path so trusted candidate auto-heal still works. If release-train evidence is temporarily unreadable after the bounded watch window, record deploy/smoke as deferred, do not claimlast_live_sha, and continue batching while the owner session keeps monitoring/returning to the deploy evidence gap. The deploy evidence wait usesLANDING_LANDER_DEPLOY_MAX_POLLS(default8) so long merge-watch windows do not monopolize intake. - Loops until the ready backlog drains, hits max iterations, or reaches a real blocker.
Use drain only when you explicitly want the older land-only loop. cycle is
the owner-facing command for the full release responsibility.
Manual Reasoning Path
When cycle output is insufficient, do the work manually in your workspace or a
scratch worktree:
- Fetch current
origin/master. - Create a scratch integration branch.
- Select source PRs, not existing integration PRs. Skip or separately reconcile
PRs titled
batch-land:*or branches namedbatch-land/integration-*. - Fetch each source PR head and verify the selected SHA still matches GitHub.
- Merge PR heads one at a time when the source PR will be closed by the batch.
Cherry-pick or equivalent workflows are allowed only when
batch_land.pycan prove the source patch is already represented in the candidate tree. - Resolve easy conflicts. For stale source branches, merge or rebase their head
onto current
origin/masterin the scratch integration branch and inspect the actual conflict. If the fix is local and obvious, keep the PR in the batch via the candidate-ref path. Abort and defer PRs whose conflicts are risky or need their author. - If fixes live only in the owner branch, push an explicit candidate ref such
as
landing-agent/batch-<id>and record its SHA. Prefer real merge commits from each source head, then conflict-resolution commits, so provenance is mechanically provable. - Validate and open the integration PR with
batch_land.py --candidate-ref <ref> --candidate-sha <sha> --prs "<numbers>" --required-label ready-for-agent --apply .... This keeps the exact fixed tree while the queue still validates and lands the integration PR. Candidate validation rejects final diffs with sensitive paths, destructive migration markers, or unsupported paths before any source PR labels are removed. - For clean PR-head batches with no owner-only fixes, use the default
batch_land.py --apply --required-label ready-for-agent ...path. - Remove
ready-for-agentand any legacyready-to-landlabel from included source PRs only after the integration PR exists, then comment with the replacement PR. - After merge, close source PRs with the integration PR and merge SHA.
- Monitor deploy/smoke by merged SHA; update source PRs with
deploy-pending,deployed,deploy-deferred, ordeploy-failedlabels/comments as evidence becomes available.
This path is intentionally flexible. Use it when the agent can make a better batch than the default helper, especially for easy conflict resolution or a high-load queue.
Source PR Communication
For every source PR included in a batch:
- Before integration PR merge: comment that the integration PR replaces the
source PR, ensure it carries
landing-batch-active, and removeready-for-agentplus any legacyready-to-landso the source does not race either queue. - After integration PR merge: comment
Landed via <integration PR> (<sha>)and close the source PR. - During deployment: add or update deploy labels and comments with backend,
frontend, smoke, run URLs, and the production verdict. If deploy is skipped
because there is no runtime change, say that explicitly. If release-train
evidence is unreadable, say
deploy-deferred, include the train-status verdict/current-pointer error, and do not state the PR is live. - On failure: remove any active-batch ambiguity, add
deploy-failedor a clear blocker comment, and either fix/delegate or return the PR to an appropriate ready/hold state.
Label vocabulary (created/maintained by landing_agent_labels.py ensure-labels):
ready-for-agent— source PR waiting for owner intake (ONLY sources)landing-integration— owner-built integration PR riding the trusted queue (batch_landapplies it at creation; the queue resolves it vialanding_queue.resolve_required_label; pre-split integrations still carryingready-for-agentkeep landing during the transition)repair-requested— deferred source PR whose branch repair is owned by the author session or a spawned fixer (see Deferred-PR Repair Ownership)operator-hold— deferred source PR parked on a human trust/scope/sequencing decision; not a mechanical fixer job and not counted by the ensure loop as work. The operator or author re-enters it by applyingready-for-agent.landing-batch-active— source covered by an open integrationlanded-in-batch,deploy-pending,deployed,deploy-failed
Blockers And Defer Rules
Defer a PR from the current batch when:
- Its head changed after readiness.
- It is draft, blocked by review, failed required checks, or cannot fetch.
- It touches operator-gated paths without explicit approval.
- It has merge conflicts that are not clearly safe to fix in the integration branch.
- It causes combined validation to fail and the failure is not obviously a baseline issue.
When deferring, leave a concrete PR comment: why it was deferred (files, base/
head SHAs, attempted operation), what would unblock it, which label now owns its
state (repair-requested for mechanical repair, operator-hold for human
policy/scope decisions), and whether the author, a spawned fixer, or the
operator owns the next step. Record the deferral in the owner log so the next
owner does not re-litigate it from scratch.
Deferred-PR Repair Ownership (no PR left behind)
A deferral is a handoff with a deadline, never an exit. When you defer a source PR for branch repair (conflicts vs current master, stale generated files):
- Change the label ATOMICALLY, repair-requested ON before ready-for-agent OFF,
in ONE edit:
gh pr edit <pr> --add-label repair-requested --remove-label ready-for-agent. NEVER removeready-for-agentas a separate first step — a deferral is multi-step (label, comment, maybe spawn a fixer) and your lease can end mid-deferral (owner transitions happen), so a source left with NEITHER label is stranded: invisible to intake AND unmarked for repair. One atomic edit means the worst interruption leaves both labels briefly, never neither. Then leave the structured deferral comment (files, base/head SHAs, attempted op, exact fix needed) and record it in the owner log. A source PR must ALWAYS carry exactly one of: ready-for-agent, repair-requested, operator-hold, landing-batch-active, or be closed/merged — never zero. For operator-gated trust/scope decisions, useoperator-holdinstead ofrepair-requested; repair owners and fixers cannot make progress there, and the ensure loop intentionally ignores that label. - Ping the author first: the PR body's
Agent session idnames the authoring session. For Claude Code sessions check~/.claude/projects/*/<session-id>.jsonlmtime — fresh (under ~30 min) means the author is likely active and your deferral comment is the ping; give it until your next cycle or ~45 minutes. - Then spawn a fixer: if the author session is inactive, unidentifiable,
or the deadline passed:
.agents/skills/landing-lander/scripts/spawn_pr_fixer.sh <pr> "<one-line evidence>"(headless repair agent in its own worktree; it merges master into the PR branch, resolves per the deferral evidence, validates the touched lanes, pushes, removesrepair-requested, and routes the PR back to intake). Record the spawn in the owner log. - You own the outcome. Every cycle, list
repair-requestedPRs (gh pr list --label repair-requested): repaired (head moved + MERGEABLE + checks green) → it re-enters intake and batches normally; fixer finished but the PR is still broken or silent → respawn ONCE with sharper evidence; after two failed fixer attempts → stop, comment the blocking decision on the PR, escalate to the operator in the owner log. No infinite loops. - Priority is unchanged: repairs run in the background while ready PRs batch
first — but a
repair-requestedPR older than ~6h with no active fixer is a stalled handoff you must act on this cycle.
Deployment Closeout
After an integration PR merges:
.github/scripts/pr_landing.py monitor --pr <integration-pr> --once
.agents/skills/release-deploy/scripts/release_deploy.sh train-status all <merge-sha> --json
.agents/skills/release-deploy/scripts/release_deploy.sh monitor backend <merge-sha> --once
.agents/skills/release-deploy/scripts/release_deploy.sh monitor frontend <merge-sha> --once
Treat train-status and deploy monitor output as production truth:
live,covered by newer, ordeploy_skipped_no_runtime_changeis terminal for that service.- Active CI/orchestrator/deploy means wait or hand off with exact run URLs.
- Missing candidate with no active owner means dispatch the normal deploy helper
for latest
master(release_deploy.sh allor the service-specific helper), not backend/frontend deploy workflows directly. - Failed deploy/smoke means fix, delegate, or trip the breaker.
Do not report "fully QAed" from default smoke. Report exactly what was checked.
Safety Limits
- Never edit or push
masterdirectly for normal owner work. - Never force-push, reset, or rebase shared worktrees.
- Never keep two active lander hosts armed.
- Never let the deterministic helper's FIFO selection override obvious agent reasoning about conflicts, risk, deploy surface, or author ownership.
- Never use the deploy PAT as the normal
ghOAuth reader unless a helper explicitly requires it. The fine-grained PAT is for workflow dispatch, repo variables, workflow-file git push, and deploy authority. - Never take over a LIVE lease without the operator;
--force-takeoveris an operator emergency tool.
Tests
python3 .agents/skills/landing-lander/scripts/tests/test_landing_lander.py
python3 .agents/skills/landing-lander/scripts/tests/test_landing_owner_guard.py
python3 .agents/skills/batch-land/scripts/tests/test_batch_land.py
What ships with it: 19 files
482.0 KB alongside SKILL.md, 14 of them executable
scripts/
- automation.toml.template2.3 KB
- com.example.landing-lander-watchdog.plist2.3 KB
- ensure_landing_owner.shruns10.9 KB
- fixer_prompt.md3.6 KB
- host_preflight.pyruns4.4 KB
- install_landing_owner_launchd.shruns2.6 KB
- lander_state.pyruns23.8 KB
- lander_watchdog_post.pyruns1.7 KB
- lander_watchdog.shruns3.7 KB
- landing_lander.pyruns184.4 KB
- landing_owner_guard.pyruns19.4 KB
- owner_prompt.md9.5 KB
- run_landing_owner.shruns13.3 KB
- self_handle.pyruns4.3 KB
- show_owner_session.pyruns6.1 KB
- spawn_pr_fixer.shruns2.8 KB
- tests/test_landing_lander.pyruns165.3 KB
- tests/test_landing_owner_guard.pyruns15.7 KB
- README.md5.9 KB
Gives 0 of the 12 instructions most ship operate skills give in ~9.2k tokens
Counted across 779 of the 1,178 authors here whose files we hold, read 2026-08-07
- Document a rollback plan before deploymentin 41 of 779, across 22 files
- Update the changelogin 21 of 779, across 19 files
- Run the test suitein 20 of 779
- Create an annotated git tagin 20 of 779
- Clean up feature flags after full rolloutin 18 of 779, across 10 files
- Verify deployment health after launchin 18 of 779, across 10 files
- Test both feature flag statesin 17 of 779, across 9 files
- Verify the working tree is cleanin 17 of 779
- Make database migrations backward-compatiblein 16 of 779, across 8 files
- Set up error monitoring before launchin 15 of 779, across 7 files
- Monitor metrics at each rollout stagein 14 of 779, across 5 files
- Create a GitHub releasein 14 of 779
Said here and by no other author read
- hold the host-global owner lease
- reason over ready pull requests
- create multi-pull-request integration batches
- resolve easy merge conflicts
- defer risky pull requests
- inform source authors until merge or block
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.