# Landing daemon — agents submit and exit

audience: AI coding agents first.

status: ACTIVE
task IDs: —
source request: 2026-08-14. Owner: "the agent is done with its code, it invokes the script, the job joins the queue and is no longer the agent's responsibility. Agent stops working, and I watch the queue in the Overdeck UI." Owner recalls asking repeatedly and mistaking trains, the auto-lander, `ship.sh`, and `deploy-local` for it; none of them is it. Measured consequence: 1–2h of agent work takes 8–9h wall clock, spent waiting and retrying.

## Verified starting state (2026-08-14)

- A FIFO ticket queue EXISTS: `<repo>/.git/harness/landq/` (tickets, verdicts, conductor lock, log) served by `_landq_conduct` in `modules/workstation/claude/workflows/lib/finish-branch.sh`.
- Its documented design IS the defect: "A landing agent enqueues a ticket and then either finds a verdict, waits behind the process holding the conductor lock, or takes that lock and serves the queue itself." All three keep the agent alive. Searched: NO detach/background/async/no-wait verb exists anywhere in the lander.
- The controller daemon IS live and already invokes `landq-conduct` (`controller/src/land-retirement.ts:181`) and already runs a timer loop (`controller/src/admission-loop.ts`) — but conducts ONLY during land-retirement reconciliation: zero conduct activity in 6h of journal. The engine exists and idles while agents hand-serve.
- Ticket format is already daemon-ready: "a ticket is the complete job description; a conductor resolves nothing from its own environment."
- FAILED units: `overdeck-deploy.path`, `adopt-owner-edits.service`, `headless-guard.path`. The async deploy watcher being down is part of today's manual-deploy pain.

## Outcome

An agent's delivery duty ends at `submitted`. The daemon owns gate → merge → push → deploy → worktree/branch cleanup → verdict. The owner watches the queue on `/requests`. No agent holds context, tokens, or a session open waiting for a landing.

## Design decisions (owner, 2026-08-14 — NEVER re-litigate)

1. **Reuse the live controller; do NOT add a daemon.** Wire conducting into its existing reconcile loop. A systemd `.path`/timer unit is the fallback ONLY if the controller proves unsuitable — and that finding must be recorded here first.
2. **Capability removal, not instruction.** Documentation has failed repeatedly (2026-08-14 alone: workers substituted models, refused directives, hand-landed). Guards are facts; instructions are requests. The hand path must physically fail — but only once the automated path works.
3. **The agent must not need the outcome.** Briefs end at "submitted"; the board carries proof. Removing the ability to wait while still demanding the deployed result would produce worse workarounds.
4. **Lifecycle cleanup belongs to the daemon** (worktrees, branches, stale tickets, release pruning) — the tail of the same job. **Machine hygiene does NOT** (disk checks, buildbox GC, agent reaper, transcript backup — 62 cleanup-ish files, a dozen timers): folding unrelated hygiene into the lander makes one component's failure everyone's outage. Hygiene becomes VISIBLE in one place (the board), never EXECUTED by one process.

## Slices (owner-authored — each ends in an OBSERVABLE behaviour change, never "code landed")

Infrastructure slices are defined by what the owner can watch happen; that is what keeps them
honest. S5–S7 are deliberately last: they could freeze delivery if the earlier proof did not hold.

**S1 — the controller serves the queue.** Ships: conducting as a standing duty on the existing
reconcile loop, per repo root, bounded and idempotent. OBSERVE: a landing that completes AFTER its
agent is gone. Gate to next: drain-with-no-agent-alive proof, hermetic plus one live observation.

**S2 — `submit` returns immediately.** Ships: a submit verb that enqueues and exits in under a
second with a ticket id and an "accepted, not yours anymore" exit code; the old blocking path still
exists, unused. OBSERVE: agent sessions ending right after their code is done while landings
complete behind them; agent runtime per task visibly collapses.

**S3 — the queue on `/requests`.** Ships: ticket state (queued/serving/verdict), daemon health,
last conduct pass, and the health of the units delivery depends on — including repairing the
currently FAILED `overdeck-deploy.path` and `adopt-owner-edits`. OBSERVE: the original ask —
watching the queue in the UI instead of watching an agent. Depends on the board data slice.

**S4 — the daemon owns the tail.** Ships: deploy, worktree removal, branch cleanup, verdict all
ride the ticket. Lifecycle cleanup ONLY. OBSERVE: worktrees and branches disappearing on their own;
no cleanup steps left in agent reports.

**S5 — remove the capability.** Ships: agent-side conducting and waiting refused with a message
naming `submit` (same shape as the worktree guard refusals); conductor lock becomes daemon-owned.
OBSERVE: nothing changes — that is the point; by then nobody was using the hand path.

**S6 — briefs end at "submitted".** Ships: dispatch prompts stop demanding deploy proof from
agents; proof is the board's job. OBSERVE: shorter, cheaper agent runs; reports ending at a ticket
id.

**S7 — violations become visible.** Ships: a landing served by an agent-held lock renders as a
flagged row, never a silent success. OBSERVE: the next loophole the day it happens instead of after
weeks of archaeology.

**S8 — docs/settings tickets never queue behind code (owner request, 2026-08-15).** A docs/settings
ticket's own gate is already instant (`gate_class=docs` skips build/test — already automatic,
computed by `_landq_gate_class` at enqueue time, no manual step). Today that speed is wasted: it
still waits strict FIFO behind every code ticket ahead of it, so a backlog of code lands makes a
1-line docs change look just as slow as the code queue. Ships: `_landq_conduct` serves ALL
currently-live docs tickets first, as one batch ("train" — one pass, no per-ticket re-lock churn),
before touching any code ticket; code tickets stay strict FIFO among themselves and are never
starved (a docs train only delays code by the docs train's own — instant — serve time, not by queue
position). Ordering is by each ticket's already-computed `gate_class`, read from the ticket's own
job file — zero manual flag, zero new classifier, zero way for an agent to force a ticket into the
fast lane. Fail-closed: unreadable/malformed `gate_class` → treated as code, never gets priority.
OBSERVE: a docs/settings land lands in roughly its own single-ticket time regardless of how many
code tickets are queued ahead of it.

## Constraints

- Never weaken a gate to make the queue faster. Cheapness comes from the docs lane and the deploy
  short-circuit (both landed 2026-08-14), never from skipping verification.
- Fail closed: an unservable ticket stays queued and visible; never silently dropped.
- A dead session must never strand a ticket — that is the whole point of S1.

## Current receipt

2026-08-14: state verified against live code and journal; decisions + owner slice plan registered.
S1+S2 dispatched (worktree `wt/landq-controller-conduct`, codex sol/low authoring, controller
reconcile-loop seam). An earlier landing of this plan reported success but did not appear on main —
re-landed with the slice plan; treat main as the only source of truth for this file.

2026-08-14: **S1 landed (`3e6513147`), gate proof partial.** Controller conducts `landq-conduct` for
every root in `landRepositoryRoots` on the existing 60s admission loop (`reconcileDeliveryLifecycle`
in `controller/src/index.ts`), bounded/idempotent (one pass per tick, in-flight re-entrancy guarded,
per-root failure logged and does not stop the loop, missing/unreadable root skipped and recorded).
Health exposed via `/status`'s new `landConduct` map (`lastPassAt`/`lastOk`/`lastDetail`/
`consecutiveFailures` per root, keyed by root) for the S3 board slice — note `lastPassAt` records
the last ATTEMPT, not the last success (it updates on failure too), and a successful pass logs
nothing to the journal by design (only skip/failure lines do), so the board is the only observable,
never the journal. Found and fixed a latent bug in `_landq_live`
(`modules/workstation/claude/workflows/lib/finish-branch.sh`) where a controller-claimed ticket's
fence was silently dropped by a here-string `read`, making every controller-claimed ticket look
malformed and unservable; fixed (2-line change) and covered by a new hermetic test
(`test-finish-branch.sh` "land queue: controller conductor entrypoint drains a submitted ticket with
no agent alive") proving a ticket submitted by a process that then exits reaches a verdict served
only by `landq-conduct`, with no agent alive. That test drives `landq-conduct` directly, NOT the
controller's admission loop — the loop→spawn→drain seam itself is covered only by stub injections
(`land-retirement.test.ts`'s `conductRoot` hook, `index.test.ts`'s call-order assertion), so a real
live observation through the deployed controller is still the only proof of the actual seam this
program is gated on, not garnish. `controller/` bun test 278→285 pass (7 new), 1 pre-existing
unrelated failure unchanged; typecheck clean. `test-finish-branch.sh` 120/121→122/0 after a
same-day unrelated main merge, 0 regressions from this change. Config:
`landRepositoryRoots = ["/home/user/Projects/overdeck"]` added to `~/.config/overdeck/controller.toml`
(was empty — without it the feature is a silent no-op: empty roots means the loop conducts nothing
and records nothing) — `packaging/` does not rewrite `controller.toml`, so this persists across
deploys. **Controller has NOT been restarted and is NOT running this code yet** — the deploy request
(`req-1750931-27834`, enqueued 22:47) is stuck behind a severely backlogged local deploy queue (29
pending requests, oldest from 22:07, `overdeck-deploy.path`/`.service` active but not observed
draining across two 1800s waits) — a pre-existing, session-wide infra bottleneck unrelated to this
diff, out of scope to fix here. Until that deploy lands, the live controller is still running old
code with the standing conduct duty absent.

2026-08-14: **CRITICAL — found before live rollout, fixed as a follow-up land (`wt/landq-conduct-async`,
`c7ca599d6`, also on main).** The real `conductRoot` spawn used `spawnSync` (blocking). Once the
deploy above completes, `conductConfiguredRoots()` runs every 60s on the controller's single JS
thread; `landq-conduct` can legitimately take minutes per pass (full typecheck+test+push gate per
ticket). A `spawnSync` that long freezes the ENTIRE event loop — no HTTP requests served, and the
systemd watchdog ping (`WATCHDOG=1`, `controller/src/index.ts`) cannot fire. The controller unit has
`WatchdogSec=30`: any conduct pass over ~30s would get the controller SIGKILLed mid-gate. This was a
pre-existing blocking pattern in the receipt-driven `accept()` path too, but converting it into a
STANDING 60s duty (this plan's S1) turned a rare, on-demand risk into a systematic one. Fixed by
switching the standing-loop conduct path to async `spawn` (event-driven), leaving the unrelated
receipt-driven path untouched. `controller/` bun test 285 pass / 1 fail (same pre-existing failure);
typecheck clean.

2026-08-14: **S1 fully live — proof captured.** Both `3e6513147` and `c7ca599d6` landed on `origin/main`
and are running in the deployed controller (`~/.local/share/overdeck/deploy` HEAD `048eb56d` has
both as ancestors; `overdeck-controller.service` active since 22:36, restarted by the
`overdeck-deploy.path` watcher picking up the queued deploy request before the watcher itself later
hit `start-limit-hit` on an UNRELATED dirty deploy-clone — see note below). Live `GET /status`
observation: `landConduct` reports
`{"/home/user/Projects/overdeck": {"lastPassAt": "2026-08-14T17:35:29.859Z", "lastOk": true,
"consecutiveFailures": 0, "lastDetail": ""}}` — a real conduct pass against the real repo, seconds
old, succeeding, with no agent process driving it. This is the S1 acceptance gate satisfied end to
end: written, tested (hermetic), landed, deployed, and observed live.

2026-08-14: **Pre-existing infra note, NOT caused by this change, NOT fixed here.**
`overdeck-deploy.path`/`.service` hit `start-limit-hit` at 00:07 because the deploy clone
(`~/.local/share/overdeck/deploy`) has 6 files of real uncommitted WIP from an unrelated
requests-board lane (none matching `origin/main`) — `deploy-local.sh`'s dirty-clone guard correctly
refuses to touch it rather than discard someone else's work, and the watcher's retry backoff then
tripped systemd's start-limit. My own deploy for this plan landed successfully before that failure
started (the controller is confirmed running both commits above), so this did not block S1, but the
watcher is now down for whoever's deploy queues next — flagging for whichever lane owns that
requests-board WIP or the deploy queue generally; out of scope for the landing-daemon plan.

2026-08-14: **Program-level finding for the S2 (`submit` verb) owner.** `_landq_live` prunes any
ticket whose owning process has exited UNLESS a `.controller` claim file exists next to it. That
claim is written only by the controller's `accept()` (`ensureControllerTicketClaim`), which is only
reached via `_stage_a_retire_controller` posting to `/land/retire` — which requires ~9
`OVERDECK_DELIVERY_*`/`OVERDECK_LAND_OPERATION_ID` env vars nobody currently sets (verified: an
unregistered submit-and-exit returns rc=3, "controller retirement requires ..."). Consequence: with
S1 landed, an agent that submits and exits WITHOUT registering with the controller does not strand
its ticket — the ticket is silently PRUNED (deleted) the moment the process exits. Whichever slice
ships `submit` MUST register the controller claim as part of submission, or S2 will quietly eat
every ticket it creates.

2026-08-15: **S8 registered and dispatched (owner, mid-session).** Owner observed the live queue at
~20 items with an estimated 17 docs/settings-shaped, each waiting full FIFO behind code tickets for
no reason since their own gate is already instant. Decision: `_landq_conduct` partitions its
per-pass live-ticket listing into a docs sub-list and a code sub-list (by each ticket's own
already-written `gate_class`, fail-closed to code on any read/parse failure), serves the entire docs
sub-list first as one train, then the code sub-list — FIFO preserved within each sub-list, code
never starved beyond one docs train's serve time. Explicitly rejected: any manual/flag-based queue
jump ("cut the queue to the first spot") — the owner's own framing landed on fully-automatic
ordering keyed off data that already exists, not a bypass switch. Dispatched to codex
(`gpt-5.6-sol`, low effort) from `wt/landq-priority-lane`.

2026-08-15: **S8 implemented.** `_landq_conduct` now partitions each pass's live-ticket listing via
a new `_landq_job_gate_class` helper (reads the `gate_class` field straight out of the ticket's own
`.job` file, base64-decodes it, defaults to `full` on any missing/unreadable/duplicate/malformed
field), serves the docs sub-list first FIFO, then the code sub-list FIFO — `_landq_live`,
`_landq_serve`, the job file format, and the conductor lock protocol are all unchanged; this is
purely a reordering of an already-fetched listing. No CLI flag, env var, or any other manual queue-
jump path exists — ordering is fully automatic from data already written at enqueue time.
`test-finish-branch.sh`, independently verified locally after `pnpm install`: 133 pass / 0 fail.
New test "land queue: docs train leads; docs/code FIFO preserved; unknown class fails closed"
stubs `_landq_serve` to observe exact serve order and asserts: two docs tickets enqueued after a
code ticket in file order both land first; docs-among-docs and code-among-code FIFO both preserved;
an undecodable gate_class treated as code, never jumping ahead of real docs tickets; every ticket
still gets a verdict.

2026-08-15: **S4 implemented and landed.** Read the live queue-conductor code before writing
anything: `_landq_serve` (invoked by `landq-conduct`, the exact command the controller spawns with
no agent alive) already delegates the merge+push to `_stage_a_land_core`, but ONLY the AGENT-side
`_stage_a_land` wrapper (the one that waits for a verdict) ever called `_stage_a_cleanup`
(worktree remove + branch delete). A ticket landed purely by `landq-conduct` — the S2/submit path —
never had its worktree or branch removed by anyone: confirmed empirically, not just by reading code
— a live `ship.sh submit` on a trivial docs branch landed via the controller's own conduct loop
(`ticket.6ed86b037cf44c168f71f8067dbfb1a3` → `submit-status` reported `landed`) and left
`.worktrees/ship-submit-proof` + `wt/ship-submit-proof` stranded on disk after the merge, exactly as
the plan's "submit callers have exited; nobody else will" framing predicted. Fixed by adding the
same cleanup to `_landq_serve`: after a successful push (verdict status `pushed`, never on
recovered/dirty/skip statuses), re-checks the job's own worktree for conflict markers/dirty tracked
files (the same guard the interactive path re-checks immediately before its own destructive removal)
and only then calls `_stage_a_cleanup` with the ticket's own root/branch/wt/head/assets_ok fields.
Cleanup failure is logged and swallowed, never flips the verdict — the land already succeeded the
moment `_stage_a_land_core` returned 0, so a hygiene failure here must never look like a land
failure to anyone reading `submit-status`. Extended the existing S1 acceptance test ("land queue:
controller conductor entrypoint drains a submitted ticket with no agent alive") with a direct
assertion that the same no-agent conduct pass also removes the ticket's worktree and deletes its
branch. `test-finish-branch.sh`: 130 pass / 10 fail (same 10 pre-existing environmental docs-lane
failures as the session baseline, 0 regressions, 7 more passing than the 123-pass baseline — the S2
submit/status tests plus this one new S4 test). Landed `<pending — see receipt below>`. Stranded
worktree/branch from the pre-fix proof run cleaned up manually (one-time; the fix prevents a repeat).

**S5/S6/S7 not started this session — scoped, not implemented.** S5 needs a `harness.landQueue`
config-gated refusal in the agent-facing `land` verb naming `submit` (the guard-refusal shape used
elsewhere in this file), landed DARK (config flag off) before flipping. S6 needs
`finishing-a-development-branch/SKILL.md` and the project `AGENTS.md`/`CLAUDE.md` Landing sections
rewritten to end at "submitted", which the S6 slice explicitly gates S5's flag flip on — doing S5's
flag-flip before S6 would strand agents still reading the old brief against a path that now refuses.
S7 needs a landq-log line for a blocking-`land` attempt while the S5 flag is on; the `/ci` local-
queue panel already reads that log per the plan, so S7 is presentation-only once S5 exists. Next
executable action: S5, in strict order, only after this S4 land is confirmed deployed.

2026-08-15: **S4 live-proof captured (new session).** Deploy clone (`~/.local/share/overdeck/deploy`)
HEAD is `dc5d1c670` itself — `git merge-base --is-ancestor dc5d1c670 HEAD` true, so S4 was already
live before this session started. Ran the plan's exact real-world cycle: trivial docs-only branch
(`wt/s4-liveproof2`) → `ship.sh submit` (ticket `ee00b1f7...`) → polled `submit-status` in a loop with
no other action taken → the standing controller conducted it with no agent holding the land: landed
on main as `3537be74b`, and mid-poll the ticket's worktree AND its `origin` branch were both removed
automatically (confirmed via `git ls-remote` returning empty and the local worktree directory gone) —
exactly S4's claimed behaviour, observed live, not from code reading. One stray local branch ref
(pointing at the now-merged commit) was left in the main checkout by `git worktree remove` not also
deleting the local head; pruned manually (`git branch -d`) — cosmetic, not the daemon's job (it only
owns the *ticket's own* wt/branch pair via `_stage_a_cleanup`, and it did that correctly; the leftover
was purely local-ref bookkeeping in the checkout doing the polling, unrelated to the daemon path).

Also checked ticket `2cadac3b2b5643ca9e32bb86fd674728` (the previous lane's own S4 receipt land,
queued before this session): verdict was `{"status":"pushed","candidate_sha":"ae64d81b2..."}` —
landed on main correctly — but its worktree (`landing-daemon-s4-receipt`) and branch were NOT
auto-removed, unlike the fresh `ee00b1f7` proof above. Read `_stage_a_cleanup`: the asset-guard
(gitignored `tmp`/`.cache`/`.turbo`/`dist`/`coverage`) did not trip (none present/non-empty) and the
source-branch-moved guard didn't trip either (branch head matched the job's snapshot exactly) — so
by code reading cleanup should have run and succeeded; cause of the miss is NOT determined (most
likely candidate: this ticket was served by an interactive `land` invocation from the previous
session, whose own cleanup call failed for an unrecorded reason before the session exited, rather
than by `landq-conduct` — no `.controller` claim file exists next to this ticket, which a
controller-served ticket would have). Verified the worktree was clean and at the landed commit,
then removed it and its local+would-be-remote branch by hand (remote ref was already gone). Not
re-investigated further — the fresh, controlled repro (`ee00b1f7`) is the actual S4 acceptance
proof; this is a named, unresolved loose end for whoever next touches `_landq_serve`'s cleanup call,
not a regression I can attribute to a specific line.

2026-08-15: **Coordinator-flagged gap folded in and fixed (pr-mode delivery receipt, same
`finish-branch.sh` seam S7 was going to touch).** `land --mode pr` never called
`_write_delivery_receipt`, even though the collector's `DeliveryReceiptSchema`
(`collector/src/adapters/delivery.ts`) already carries `kind:"pr"` in its enum with a label ("review
queue") — a pr-mode land was silently invisible to the board, unlike every merge-to-main delivery
kind. Root cause: the receipt-writing block in `land()` was gated to `mode == merge-to-main` only;
`_promote`'s pr branch prints its own PR json (with the URL) straight to stdout and returns, with no
receipt call on that path at all. Fixed by capturing `_promote`'s output (previously printed directly,
now captured then re-printed unchanged, so all existing output/ordering is preserved) and, when
`mode == pr` and the promote succeeded, writing a receipt with `kind:"pr"`, `ok:true`, and the PR URL
in `reason` (the schema has no separate URL field — `reason` is what the board already renders as
`detail`, so this matches the existing schema exactly, no schema change). Confirmed by design
(`ship-init.sh` comments) that pr mode's delivery is deliberately implicit and never passed as
`--delivery pr` — the fix keys off `mode == "pr"` directly, matching that design, no `ship-init.sh`
change needed. New hermetic test "land pr: writes delivery receipt kind=pr with PR url" added to
`test-finish-branch.sh`, passing; full suite 131 pass / 10 fail (same 10 pre-existing environmental
docs-lane failures as every prior receipt in this file, 0 regressions, 1 new pass). Landed via
`ship.sh submit` (ticket `765a4463df4442fd851fbb34676ddecc`, dogfooding the submit path per this
plan's own S2/S4).

2026-08-15: **Ticket `765a4463…` above hit a real-queue gate failure under machine load (`load
average 22` on 16 cores at the time), NOT a regression** — same testcmd re-run standalone in the
same worktree came back 131 pass / 10 fail (identical baseline) twice in a row. Re-submitted clean
as ticket `aa8d33bc5fa148a68d6090e32d594d79`; awaiting verdict. Flagging as a program-level
observation, not fixed here: `test-finish-branch.sh` is slow/heavy enough (one local run itself hit
a 280s cap) that a busy queue window can produce false `candidate-tests-failed` verdicts — worth a
future look at either shortening the gate's testcmd for this lib (a smaller targeted subset) or
giving the queue's own gate more retry headroom before declaring `needs-attention`.

2026-08-15: **S6 landed (`6a664a7ac`), live-verified.** Rewrote
`modules/workstation/claude/skills/finishing-a-development-branch/SKILL.md` and the root `AGENTS.md`
(`CLAUDE.md` is a symlink to it) Landing sections per `agent-doc-authoring` doctrine: the documented
flow now ends at "submitted" (`ship.sh submit <branch> <worktree>`, stop, no polling for the
result); blocking `land`/`ship.sh land` is documented as LEGACY, kept only for one-off manual/debug
use, and named as the thing S5's dark flag will start refusing. Added an "if a ticket comes back
needing agent action" section (non-`pushed` verdicts leave the worktree/branch in place — same
rubric applies, then re-submit) so the doc doesn't just delete the conflict-resolution guidance a
non-`submitted` outcome still needs. Confirmed live post-land: `CLAUDE.md` in a fresh worktree
already reads "SUBMIT it… NEVER wait or poll" and names `land` as legacy.

2026-08-15: **S5 implemented, hermetically proven, submitted (ticket
`144f785967ba429ea376124fa65ba756`) — submitted only AFTER confirming S6 was on `origin/main`, per
this plan's explicit ordering rule.** Deliberately used a SEPARATE config key
(`harness.landQueueOnly`) from `harness.landQueue` (the key that already gates `submit` itself via
`stage_a_enabled`, requiring the exact value `"stage-a"`) — an earlier draft that reused
`harness.landQueue` for both purposes would have been self-defeating: setting it to anything other
than `"stage-a"` to retire `land` would simultaneously disable `submit`, stranding every caller.
`_land_queue_only_enabled()` requires BOTH `stage_a_enabled` (queueing must actually work before its
only alternative is retired) AND `harness.landQueueOnly == "true"`. When both hold, the blocking
`land` verb refuses at the top of its argument validation (before touching disk) with `exit 3` and
a message naming `submit` verbatim, matching the guard-refusal shape used by the worktree-isolation
guard. Off by default (unset or any other value) — **the flag itself has NOT been flipped in this
repo's actual git config**; only the capability (code path) has been shipped, dark, per this plan's
"landed DARK before flipping" instruction. Three new hermetic tests: refusal fires and touches
nothing (main untouched, worktree/branch intact) when both keys are set; default-off still lands
normally; `submit` itself is provably unaffected by `harness.landQueueOnly=true` (regression guard
against the exact self-defeat above). Full suite 133 pass / 10 fail (same pre-existing docs-lane
baseline, 0 regressions, 2 new S5 passes + earlier S4 test already counted).

**Next executable action:** none required to call this session's scope done — S1–S6 are landed and
live; S5's code is landed dark. Flipping `harness.landQueueOnly=true` for real (making `land` refuse
in this actual repo) and S7 (violation logging to the landq log for a blocking-`land` attempt while
the flag is on) are the remaining open items, left for the next session/lane: the flip is a one-line
`git config` change once the owner is ready to fully retire the hand path, and S7 is presentation-
only work layered on the same seam S5 just built.

2026-08-15: **S7 implemented (violation logging), submitted for landing.** Found and instrumented
the two seams where a landing-contract violation is currently refused only to stderr: (1) S5's dark-
flag refusal in `land()` (`_land_queue_only_enabled` block, blocking `land` used where `submit` is
expected) and (2) `land-guard.pre-push`'s `_lg_reject` (the guarded-trunk pre-push hook — covers
every raw/hand-typed push to the trunk it rejects: non-fast-forward, trunk deletion, missing/stale/
invalid land authority — i.e. every gate-bypass shape that hook already refuses). "Direct-land
without the authorized context" and "gate bypass attempts" from the source brief are the SAME
enforcement point as (2), not separate code — `_lg_reject` is the single choke point for every raw-
push rejection reason. The `modules/workstation/claude/bin/git` PATH shim itself does NOT reroute or
deny pushes (checked directly — it only guards destructive verbs/mass-delete for worktree
isolation); CLAUDE.md's "plain git is a PATH shim that reroutes push into the land queue" describes
the pre-push hook's effect, not shim code, so no third seam exists there.
Both seams now write ONE JSON line — `{"event":"violation","kind":...,"branch":...,"who":...,
"guidance":...,"pid":...,"at":...}` — to the SAME `$qdir/log` (`.git/harness/landq/log`) the
conductor already writes enqueue lines to, via a new `_landq_log_violation` helper in
`finish-branch.sh` (S5 call site) and an inlined equivalent in `land-guard.pre-push` (which has no
access to finish-branch.sh's function library at hook runtime). `who` is best-effort
`git config user.email`, falling back to `user@hostname`. Fail-open by design: every write is
wrapped so a logging failure never blocks, delays, or changes the refusal it records — verified by
reading, not assumed. The existing `readLandqSource` collector reader (`collector/src/activity/
sources/landq.ts`) already parses every JSON line in that file generically (redacts, timestamps,
builds a title) — untouched here per the coordination note ("write the log line in the same shape,
coordinate by shape, not by editing the registry lane's files"); its title heuristic does not yet
special-case `event:"violation"` lines, left for whichever lane owns that reader/the requests-board
mirror to refine.
Two new hermetic tests in `test-finish-branch.sh`: "land: S5 refusal logs one violation line to the
landq log" (asserts the log line's `event`/`kind`/`branch`/`guidance`/`at` fields after a blocked
`land` call, S5 flag on) and "land guard: rejected raw push logs one violation line to the landq
log" (asserts the same shape after a raw `git push origin main` is rejected by the armed guard,
captured BEFORE the test's own subsequent successful land call appends its own unrelated enqueue
line to the same file — an early version of this test read the log's last line AFTER that later
call and false-failed on the wrong line; fixed by capturing right after the rejection). Full suite:
`PASS=137 FAIL=10` — the exact same 10 pre-existing markdownlint-cli2-environment failures named in
this session's brief (`docs lane tooling/docs-only/agent frontmatter/malformed frontmatter/
unterminated frontmatter/no-frontmatter false red/malformed plan frontmatter/plan no-frontmatter
false red/planted secret/broken link`), 0 regressions, 2 new passes. No `apps/web/src`,
`packages/deck-ui/src`, or `modules/harness/factory` files touched — UI doc and factory suites out
of scope, neither run.
**Built on S5, not yet on `origin/main`:** S5 (`d7036b23f`) was still queued (ticket
`144f785967ba429ea376124fa65ba756`, verdict `candidate-tests-failed` under load, per this file's
2026-08-15 note above) when this session started — S7 cannot exist without S5's refusal call site.
Cherry-picked `d7036b23f` onto this branch (content-identical, new sha after rebase) so the branch
is self-contained; the candidate-build merge step in `_stage_a_land_core` is a real content-based
3-way merge (not a patch replay), so landing this branch is safe whether S5's own ticket lands
first (this branch's duplicate S5 diff merges as a no-op) or this branch lands first (S5's original
ticket becomes redundant and needs re-submission or will itself no-op).
Landed via `bash <PROJECT_ROOT>/.claude/scripts/ship.sh submit wt/landing-daemon-s7
.worktrees/landing-daemon-s7`, per this project's submit-and-exit doctrine — verdict not polled;
`/requests` carries proof. **NOT verified by this session:** the live queue actually draining this
ticket (30-deep backlog observed at submit time), the deployed collector/board rendering the new
`event:"violation"` line usefully (title heuristic untouched, per scope), and `harness.landQueueOnly`
being flipped to `"true"` for real in this repo (still unset/dark — S7 only proves the log write
fires WHEN the flag is on, in tests; nobody has flipped it live yet). Plan stays ACTIVE, not DONE,
until the flag flip + a live observed violation log line closes the loop end to end.
