#!/usr/bin/env bash
#
# lane-end — the LAST act of a lane: say in the register that the window is
# closing and that nothing is in flight, and refuse to say it while something
# still is.
#
# It NEVER renames the tmux window. The name is the lane's history until the
# next `lane-start` takes that window, and a window renamed back to `claude`
# is a window nobody can attribute.
#
# PROTOCOL  ~/.agents/protocols/lane-collision-protocol.md
#   Rule 4 — a lane appends its row when it starts and UPDATES THAT ROW WHEN
#     IT STOPS; the register is the last file a lane writes.
#   Rule 6 — a row reads LANDING from the moment the lane posts LANDING on a
#     PR until it posts LANDED. Other lanes hold their merges while it does,
#     so a lane that ends without closing its landing window stalls the
#     estate. That is what this script's refusal is for.
#   Rule 9 + Amendment 5 — the write is one commit, made by lanes-edit.sh.
#   Amendment 7 (DRAFT 2026-09-11) — the refusal is now EXACT, read from the
#     lane's own object log rather than guessed at by substring; the lane's
#     last act is an `ENDED` (or `RETIRED`) line in that log; and the handoff
#     fragment the next session needs is GENERATED from it, on stdout.
#
# USAGE
#   lane-end [options] <lane>            # lane-end openRepoShape-2
#   lane-end <lane> --inventory-only     # end it past the CLOSE-OUT GATE: its
#                                        # trees, branches, scratch and caches
#                                        # stay where they are, and the row and
#                                        # the ENDED line say so (#163)
#   lane-end <lane> --retire <pid|uuid>  # retire a live FORK: writes nothing and
#                                        # kills nothing
#   lane-end <lane> --retire <pid>       # a DUPLICATE of the lane's id: TERMs a
#                                        # background holder's pair, only reports
#                                        # any other; the row is not written
#   lane-end --retire-dormant <repo> [--reason "<why>"] [--yes]
#                                        # the DORMANT rows of one repository,
#                                        # dry run by default; --yes is ONE
#                                        # commit (Amendment 19(c))
#   lane-end [options] <repo> <n>        # the split form works too
#   lane-end [options] <n>               # <repo> = basename of the cwd's git root
#
# One argument is taken VERBATIM as the lane name, so the rows named before
# the `<repo>-<n>` rule (`browser-ui-repair`, `hermes-wallet-exercise`) can be
# ended too — their last act is exactly what this is for.
#
# OPTIONS
#   --retire            write the state cell as RETIRED rather than ENDED — the
#                       lane is not coming back, as opposed to a window that is
#                       only closing for the night. Since Amendment 13(a) that
#                       is the SAME write as the closing one (the cell is one
#                       phrase that set-row-state replaces), not a second act
#   --retire <pid|uuid> A DIFFERENT ACT for one of THREE kinds of target, and
#                       it ends nothing in the LANE'S ROW on any of them: for a
#                       live FORK of this lane's transcript it is the DOOR to
#                       Amendment 6(d)'s retire act (Amendment 11 clause
#                       (k) rule (e) — "both print the one act: retire it,
#                       under Amendment 6(d) … Neither kills a process … and
#                       lane-end's --retire is the door"). For a FORK — a live
#                       session under an id the row does NOT record, wearing
#                       this lane's title (decision 8(e)) — it PROVES the pid
#                       or uuid is one and prints 6(d) for it filled in, the
#                       /rename for a session with a prompt or 6(d)'s "ended"
#                       for a kind: bg holder that has none; it writes NOTHING
#                       (a RETIRED carrying a payload would be a seventh edit
#                       to in-force text: Amendment 7(b) gives RETIRED no
#                       payload and A11 clause (c) keeps it that way) and it
#                       KILLS NOTHING there. The other two kinds both answer
#                       Amendment 18(h) — exactly one live process may hold one
#                       transcript — from two different kinds of evidence, and
#                       this door runs both because neither can see what the
#                       other does:
#                         * a DUPLICATE HOLDER — a live `--fork-session`
#                           process under an id the row DOES record, found in
#                           the PROCESS TABLE (opensoft/openRepoTools#39) — is
#                           a background wrapper nobody is answering a prompt
#                           in, so this act TERMs the bg-pty-host parent and
#                           EVERY child of it that resumes this transcript
#                           (opensoft/openRepoTools#86; never the lane's own
#                           live session, which it refuses to touch), reports
#                           each pid it signalled, and says RETIRED only once
#                           every one is gone. Named by the PARENT or any
#                           CHILD PID and never by the transcript uuid
#                           (Copilot round 9, PR #61) — the same reason a
#                           DUPLICATE below never is: the uuid IS the row's
#                           own and cannot distinguish two process-table
#                           holders (or a duplicate beside a not-yet-bound
#                           fork) that carry the identical id;
#                         * a DUPLICATE — any other live process carrying the
#                           row's own id, found by SESSION RECORD
#                           (opensoft/openRepoTools#39, Amendment 12/#52),
#                           which also catches an ordinary INTERACTIVE second
#                           resume the process-table match was never built to
#                           see — is reported and never killed, exactly like a
#                           FORK, because it may have a prompt open; the
#                           lane's OWN session, in its own window, is refused
#                           by name (retiring the session that IS the lane is
#                           how a lane loses the conversation it is — end the
#                           lane instead, the bare form above), and the
#                           harness's OWN COMPANION of that session — the
#                           `kind: bg` record beside it in the same profile,
#                           Amendment 8 ruling (g) — is refused by name too:
#                           it is one half of the live session and not a
#                           rival to it. A DUPLICATE is named by its PID and
#                           never by a uuid, because its uuid IS the row's
#                           own and could not pick between the processes
#                           carrying it; the uuid form above is for a FORK,
#                           whose id is one the row does not record.
#                       Refuses on anything that is none of the three, and on
#                       any of the reads failing — a read that failed is not
#                       "it is not one". `lanes-edit.sh forks <lane>`,
#                       `lanes-edit.sh duplicate-holder <lane>` and
#                       `lanes-edit.sh transcript-holders <uuid>` (the last
#                       against the row's own id, via `last-session <lane>`)
#                       are the reads that name them.
#   --state-was <text>  ACCEPTED AND IGNORED since Amendment 13(a). It named the
#                       exact text `--retire` had to replace, back when the cell
#                       was the row's history and retiring meant flipping the
#                       one word it opened with. The cell is now one phrase and
#                       the closing write carries RETIRED itself
#   --force             end the lane even though its object log still shows
#                       something open, and say so in the lines written. It
#                       does not pass the close-out gate: that is the next word
#   --inventory-only    THE DOOR PAST THE CLOSE-OUT GATE (below): end the lane
#                       although the sweep still finds trees, branches, scratch
#                       or caches of it on disk, refused the lane, or could not
#                       be read. Nothing on disk is touched and the trees stay
#                       in the lane's #97 inventory, where the daily report
#                       lists them as an ended lane's; the row's closing phrase
#                       and the lane's ENDED (or RETIRED) line say that this
#                       door was taken and what the gate found. Where the gate
#                       passes it changes nothing
#   --sweep             NOT BUILT, and refused with that said. Retiring the
#                       trees in the same run as the ending is not one act:
#                       `lane-worktrees sweep <lane> --yes` retires them first,
#                       on its own; the word is held here so that nothing else
#                       takes it meanwhile
#   --home owner/repo   the lane's home repo, for a PRE-CUTOVER lane whose log
#                       has no STARTED line. Accepted for parity with `claim`
#                       and `who` and passed through to the helper; lane-end's
#                       own reads are of canonical object keys, so it changes
#                       nothing they do. It is CHECKED FIRST, through the
#                       writer's own `resolve-home`, because the only line it
#                       reaches is written after the row is already closed
#   --retire-dormant <repo>
#                       A THIRD ACT, and it ends no lane either: the SWEEP of
#                       lane-collision-protocol Amendment 19(c). A DORMANT row
#                       is one with NO object log and no live session here — a
#                       lane from before Amendment 7 gave every lane a log, or
#                       one whose log was never created. No `lane-end <lane>`
#                       can close those: this command wants a live session or a
#                       log to write into, and they have neither, so they sit at
#                       the top of every `lanes` and the next free position is
#                       read past them. Without `--yes` this is a DRY RUN that
#                       names every dormant row of `<repo>` with its start, its
#                       workstation and the head of its state cell, and FLAGS
#                       each row whose cell says the work was unpushed, lost, a
#                       loss or owed, or still says LIVE — so a person sees what
#                       the sweep will close before it closes it. With `--yes`
#                       it is ONE commit (`lanes-edit.sh retire-rows`): each
#                       lane's log is created with a single `RETIRED` line
#                       carrying the reason and the row's own last words, and
#                       the row's state becomes `RETIRED · <UTC> · <why>;
#                       was: <head>`. It REFUSES a lane a live session holds
#                       here and a row whose log says the lane is parked, and it
#                       touches NO file outside the register and the logs —
#                       worktrees, branches and handoffs stay exactly where they
#                       are. An agent's stdin is not a terminal, so `--yes` is a
#                       word a PERSON types.
#   --reason "<why>"    the words the sweep writes into each RETIRED line and
#                       into each row's state cell. `--retire-dormant` only
#   --yes               make the sweep the one act. `--retire-dormant` only
#   --dry-run           print what would be written; write nothing
#   -h, --help
#
# THE REFUSAL
#   EXACT, from `lanes/log/<lane>.md` (Amendment 7): every object whose LAST
#   event in this lane's own log is OPEN — `CLAIMED`, `TAKEOVER`, `OPENED`,
#   `LANDING` or `WITHDRAWN` — is still this lane's, and each one is named. No
#   substring, no guessing, and no false refusal on a row that merely says the
#   words. The rows are read from `origin/main` through `lanes-edit.sh
#   lane-objects`, which answers 8 when the lane has no log at all and 0 with
#   the rows otherwise; ANY other exit is the helper failing, and this script
#   refuses rather than treating a failure as an absence.
#
#   THE FALLBACK, and only for a lane with NO log file — one that has not
#   written a line since the cutover: the old scan of the state cell for the
#   protocol's own words, in which the LAST LANDING or CLAIMED not followed by
#   a LANDED or RELEASED is something in flight. Phrases that SAY there is
#   nothing open — `NOTHING CLAIMED`, `NO LANDING OPEN`, `LANDING WITHDRAWN`,
#   `UNCLAIMED` and their kin — are removed before that scan, because rows are
#   full of them and a substring match on CLAIMED inside NOTHING CLAIMED would
#   refuse on every healthy lane. That scan reads TWO cells — this checkout's
#   row and the PUBLISHED one (`lanes-edit.sh register-row`, which fetches and
#   reads `origin/main`, like every other state read) — and either of them
#   showing a hold is a refusal. A fetch does not move the working tree, so a
#   peer's LANDING pushed on to this row was invisible to the one path that
#   still decides a merge hold from prose.
#
# THE CLOSE-OUT GATE (opensoft/openRepoTools#163)
#   After the in-flight guard and before any write, the lane's own leftovers
#   are asked about: `lane-worktrees sweep <lane> --branches --include-scratch
#   --include-caches --dry-run --porcelain`, which exits 0 (nothing of the
#   lane's to retire), 3 (something to retire) or 2 (refused, or a read
#   failed). 3 and 2 are a REFUSAL, exit 2, naming every tree, branch, scratch
#   directory and cache the table gives the lane, the sweep's own reason where
#   it refused, and the exact sweep command; any other exit, or no
#   `lane-worktrees` at all, is a gate nobody read (exit 1), never a pass.
#   Beside the sweep's table, this command makes two reads of its own:
#     * every tree of the lane's is read with `git status --porcelain
#       --ignored`, so a tree holding nothing but ignored files (an `.env`, a
#       `node_modules`) is named as holding them, never as clean;
#     * every entry directly under the lane's own root, `.lane-worktrees/
#       <lane>/`, that the table does not name as a tree, scratch or a cache is
#       RESIDUE - ignored leftovers the sweep's rows never reach - and refuses
#       even where the sweep answered 0, as a cache row does.
#   Where the lane's checkout is an ASSEMBLY (T018, opensoft/openRepoTools#186)
#   the sweep also prints a `leg` row per leg checkout: one it counts is named
#   as a leg checkout whose remedy is a person's — no sweep acts on a leg — and
#   one that holds work and is not this lane's is listed and NOT counted.
#   `--inventory-only` is the one way past it, and `--sweep` is not built.
#
# THE HANDOFF FRAGMENT
#   On STDOUT, generated from the lane's log, and partitioned by the same two
#   sets the refusal uses, so no object can fall out of both:
#     `## Done` — RELEASED, CLAIM-LOST, CLOSED, LANDED
#     `## Open` — CLAIMED, TAKEOVER, OPENED, LANDING, WITHDRAWN
#   A `CLAIMED` another lane has since taken over is listed under `## Open` as
#   `superseded by TAKEOVER (lane:X) — release it`, because it is this lane's
#   last line on the object and only this lane's `release` closes it — but it is
#   not work the next session still holds, and a handoff that said it was would
#   send that session at an object `who` gives to somebody else.
#   (There is no `MERGED`: Rule 6's `LANDED → <sha>` is the post-merge line.
#   `WITHDRAWN` is OPEN — a withdrawn landing leaves the PR held by the lane
#   that withdrew it.) Paste it into the handoff the row points at. It is the
#   ONLY thing this script writes to stdout; every step and every refusal
#   still goes to stderr.
#
# EXIT
#   0  done
#   1  environment — no register, no writer, `lanes-edit.sh lane-objects`
#      failed (which is NOT the same as the lane having no object log), or the
#      close-out gate could not be read (no `lane-worktrees`, or an exit that
#      is none of its three)
#   2  refusal — bad arguments, no row (or two), something still in flight, or
#      the close-out gate: the lane's trees, branches, scratch, caches or
#      residue still on disk, or the sweep refusing the lane
#   8  nothing there. `--retire-dormant`: no lane of that repository is
#      recorded, or none of its rows is DORMANT. `--retire <pid|uuid>`: nothing
#      of any kind is live for the lane
#   130, 143  stopped by INT or TERM (128 plus the signal): the run ends where
#      the signal found it and removes its temporary file, never resumes
#
# ENVIRONMENT
#   LANES_FILE  default <the workspace repository>/lanes/LANES.md
#   LANES_EDIT  default lanes-edit.sh beside this command
#   LANE_WORKTREES  default lane-worktrees beside this command: the close-out
#               gate's sweep
#   LANES_WORKSPACE_ROOT  the workspace repository, overriding workspace.yaml's
#               `path:` — the test seam
#   AGENT_PROTOCOL_ROOT   dir holding workspace.yaml (default ~/.agents)
#   OPENREPOTOOLS_BIN_DIR where `--install` put the commands (default
#               ~/.local/bin) — searched for lanes-edit.sh
#
# It spells no absolute path of its own. Since Amendment 9(a) the register is
# found through `$AGENT_PROTOCOL_ROOT/workspace.yaml` rather than beside this
# file — this is an installed command now — and its WRITER is found beside it,
# because `--install` places the two together.

set -euo pipefail

prog=lane-end

SELF="${BASH_SOURCE[0]}"
RESOLVED="$(readlink -f -- "$SELF" 2>/dev/null || printf '%s' "$SELF")"
SCRIPT_DIR="$(cd -- "$(dirname -- "$RESOLVED")" && pwd)"

# --- THE WORKSPACE REPOSITORY (lane-collision-protocol Amendment 9(a)) -------
#
# The person's data — the register `lanes/LANES.md`, the object logs
# `lanes/log/`, the handoffs, the manifests `park` writes, and a
# `lanes/repos.tsv` override — is found through
# `$AGENT_PROTOCOL_ROOT/workspace.yaml`, and NEVER from this file's own
# location. `resume:466` and `status:1673` already read that same file under
# that same name, and Amendment 9(a) mints no second one: a second way to find
# it is a second answer.
#
# Deriving it from here stopped being POSSIBLE the moment this became an
# installed regular file in `~/.local/bin` with no register anywhere near it,
# and it stopped being WANTED for a subtler reason: a file's location on disk
# is evidence about the file, not about a person's data.
#
# THESE FOUR FUNCTIONS ARE A DELIBERATE COPY, byte for byte, across
# `lanes-edit.sh`, `lane-start`, `lane-end` and `link-estates` — the same trade
# openRepoTools already makes for its own fetch shim (`openRepoTools:93-100`).
# A shared `lanes-common.sh` would be a TENTH file for `--install` to place and
# a broken helper the first time somebody copied only some of them. Each of
# these is one file a person has on PATH.
#
# FAILING TO FIND IT IS A REFUSAL, NEVER A GUESS. No file, no `path:`, no
# `repository:`, or a `path:` that is not the root of a checkout of the
# `repository:` named beside it → the caller refuses, exit 1 — the code
# `lanes-edit.sh` has always used for *registry not found* — with the fix
# named: `openRepoTools wip init`. Nothing here ever creates that file, writes
# a register, or falls back to a path derived from its own location. THE
# CHECKOUT TEST IS NEW: nothing validated it before this, so a `path:` pointing
# at some other repository used to be accepted and now is not.
#
# `LANES_WORKSPACE_ROOT` is the test seam and the only override — the seam
# `lane-start` already spelled for handoff resolution, widened to the whole
# question so `test_lane_helpers.sh` can point every helper at one sandbox.
# --- BEGIN shared workspace resolver (Amendment 9(a)) ----------------------
# BYTE-IDENTICAL IN ALL FOUR LANE HELPERS, and held there by
# `tests/test_repo_hygiene.py`. The estate commands carry the same discipline
# for their own estate resolver and for the same reason its guard gives: a
# drifted copy is TWO ANSWERS to one question, and "where is this person's
# workspace" is exactly the question Amendment 9(a) exists to give one answer
# to. Edit it in one file and the test names the other three.
LANES_WS_WHY=""

lanes_ws_yaml() { printf '%s\n' "${AGENT_PROTOCOL_ROOT:-$HOME/.agents}/workspace.yaml"; }

# `key: value` at the TOP LEVEL only, first occurrence, with an inline `#`
# comment and surrounding quotes stripped and a leading `~` expanded against
# $HOME (nothing expands a bare `~` inside a file). The anchored `^key:` never
# reaches into the INDENTED `orgs:` map, which Amendment 9(a) puts out of
# scope: these helpers read `repository:` and `path:` and nothing else, because
# one register in the first workspace repository is the whole rule — the
# register never follows that map. Two lines of sed, because a YAML parser is
# not a dependency any of these files will take for one scalar.
lanes_ws_field() {
  local v
  v="$(sed -n -e "s/^$1:[[:space:]]*//p" "$2" 2>/dev/null | head -n 1 |
    sed -e 's/[[:space:]]*#.*$//' -e 's/[[:space:]]*$//')"
  v="${v%\"}"; v="${v#\"}"
  v="${v%\'}"; v="${v#\'}"
  case "$v" in
  '~') v="$HOME" ;;
  '~/'*) v="$HOME/${v#'~/'}" ;;
  esac
  printf '%s\n' "$v"
}

# THE WORKSTATION'S NAME IS READ, NEVER INVENTED — AND NEVER `hostname` INSIDE A
# CONTAINER (lane-collision-protocol Amendment 11, ratified decision 8(d), as
# corrected by A11 Addendum 3's `R-A11-14`).
#
# WHAT WENT WRONG. `opensoft/brett-wip` `origin/main` carries SIX lines in
# `lanes/log/openRepoProject-1.md` whose workstation is a container id — five
# `@0e7d1a79a07e`, one `@55cceb3e5bc9` — in an append-only file, written by every
# writer inside that container and not only by Evidence 6's fork. A row on a
# workstation that does not exist is a row no reader can match, no crossing can
# detect and no `swapped <ws>` can answer for; and the log is never rewritten, so
# each one is wrong for ever.
#
# WHO OWNS THE VALUE. `R-A11-14`: the **workBenches launcher**, which runs on the
# host and exports `LANES_WORKSTATION=<the host's own hostname -s>` into every
# session AND every container it starts, beside the `CLAUDE_PROFILE_NAME` it
# already exports. These helpers READ it. They do not write it, they do not
# derive it from a second file, and THEY NEVER SUBSTITUTE A PLACEHOLDER: an
# `@unknown-workstation` is the same defect one field along from the `unknown`
# clause (e) refuses in the session field, and the rejected alternative — a
# `workstation:` key in `workspace.yaml` — is a SECOND PLACE FOR THE TRUTH TO BE
# WRONG beside the variable the launcher already sets.
#
# THE THREE ANSWERS, as `<name><TAB><source>`:
#
#   seam              `$LANES_WORKSTATION` is set. This is the ordinary case
#                     wherever the launcher started the session.
#   hostname          no seam, and this is NOT a container: `hostname -s`, which
#                     is what the seam has defaulted to since Amendment 6 and
#                     what every row written before this rung was written by.
#   container-unset   no seam, and this IS a container. The NAME still answers,
#                     because a read in front of every launch may not refuse —
#                     it will simply match nothing, which is the honest answer —
#                     but every WRITER refuses, exit 2, naming the variable and
#                     the launcher that sets it.
#
# IT ANSWERS AS A PAIR, AND THAT IS NOT A STYLE CHOICE. A caller reads the name
# with `$(lanes_workstation)`, which is a COMMAND SUBSTITUTION and therefore a
# subshell: a global the function set there dies with it. The source is on the
# same line as the name, which is the only way out of a subshell there is.
#
#   $LANES_IN_CONTAINER   1 or 0, forcing the probe. THE TEST SEAM, and nothing
#                         else sets it: the suite must be able to exercise both
#                         halves of this rule on one host, and the host it runs
#                         on is a container exactly when it is.
lanes_in_container() {
  case "${LANES_IN_CONTAINER:-}" in
  1) return 0 ;;
  0) return 1 ;;
  esac
  [ -e /.dockerenv ] || [ -e /run/.containerenv ]
}

lanes_workstation_pair() {
  local v
  if [ -n "${LANES_WORKSTATION:-}" ]; then
    printf '%s\t%s\n' "$LANES_WORKSTATION" seam
    return 0
  fi
  v="$(hostname -s 2>/dev/null || hostname 2>/dev/null || printf 'unknown')"
  if lanes_in_container; then
    printf '%s\t%s\n' "$v" container-unset
  else
    printf '%s\t%s\n' "$v" hostname
  fi
}

# THE NAME ALONE, which is what every writer of the workstation column wants.
lanes_workstation() { lanes_workstation_pair | cut -f1; }

# The ONE SENTENCE a writer refuses with, in one place, so the three writers
# cannot word it three ways. Empty on every other source, so
# `[ -n "$(lanes_workstation_why "$src")" ]` is the whole of the test.
lanes_workstation_why() {   # <source>
  [ "${1:-}" = container-unset ] || return 0
  printf '%s\n' "this is a container and \$LANES_WORKSTATION is not set, so there is no workstation name to write. This host's own \`hostname\` is the CONTAINER'S id, not a workstation, and a row on a workstation that does not exist is a row no reader can match — in a log nothing ever rewrites (Amendment 11, decision 8(d); R-A11-14). The value is the workBenches launcher's to export into every session and container it starts. Set it for this one and re-run: export LANES_WORKSTATION=<this host's name>"
}

# One of the spellings `git remote get-url origin` prints for one GitHub
# repository, reduced to a bare lowercased `<owner>/<name>` so it can be
# compared against workspace.yaml's own `<owner>/<name>`. A URL that is not
# GitHub's is compared as it stands, lowercased — which is what lets a sandbox
# name a bare local origin and still be checked.
lanes_ws_norm() {
  local u="${1%.git}"
  case "$u" in
  https://github.com/*) u="${u#https://github.com/}" ;;
  ssh://git@github.com/*) u="${u#ssh://git@github.com/}" ;;
  git@github.com:*) u="${u#git@github.com:}" ;;
  esac
  printf '%s' "$u" | tr '[:upper:]' '[:lower:]'
}

# Do these two name one repository? Equal after normalisation, OR the origin's
# path ENDS in the `<owner>/<name>` the pointer file spells — which is how a
# GitHub Enterprise host, an ssh alias and a local bare mirror laid out by
# owner and name all say the same thing as `https://github.com/<owner>/<name>`.
# The pointer file spells `<owner>/<name>` because that is what a person types;
# `git remote get-url` spells a URL because that is what git stores.
lanes_ws_same_repo() {
  local a b
  a="$(lanes_ws_norm "$1")"
  b="$(lanes_ws_norm "$2")"
  [ -n "$a" ] && [ -n "$b" ] || return 1
  [ "$a" = "$b" ] && return 0
  case "$a" in */"$b") return 0 ;; esac
  return 1
}

# Prints the workspace checkout's real root, or returns 1 having set
# $LANES_WS_WHY to the one sentence that says which of the five ways it failed.
# ROOT-NESS IS ITS OWN CHECK: `rev-parse --is-inside-work-tree` and
# `remote get-url origin` both succeed from any SUBDIRECTORY of a work tree, so
# a `path:` naming a subdirectory would otherwise be accepted and resolve
# `<path>/lanes/LANES.md` inside the wrong directory.
lanes_workspace_root() {
  local yaml repo path origin toplevel real
  LANES_WS_WHY=""
  if [ -n "${LANES_WORKSPACE_ROOT:-}" ]; then
    printf '%s\n' "$LANES_WORKSPACE_ROOT"
    return 0
  fi
  yaml="$(lanes_ws_yaml)"
  if [ ! -r "$yaml" ]; then
    LANES_WS_WHY="there is no $yaml"
    return 1
  fi
  repo="$(lanes_ws_field repository "$yaml")"
  path="$(lanes_ws_field path "$yaml")"
  if [ -z "$repo" ]; then
    LANES_WS_WHY="$yaml names no \`repository:\`"
    return 1
  fi
  if [ -z "$path" ]; then
    LANES_WS_WHY="$yaml names no \`path:\`"
    return 1
  fi
  if ! git -C "$path" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    LANES_WS_WHY="$yaml names path: $path, which is not a git checkout"
    return 1
  fi
  origin="$(git -C "$path" remote get-url origin 2>/dev/null || :)"
  if ! lanes_ws_same_repo "$origin" "$repo"; then
    LANES_WS_WHY="$yaml names repository: $repo, but $path is a checkout of ${origin:-no origin at all}"
    return 1
  fi
  toplevel="$(git -C "$path" rev-parse --show-toplevel 2>/dev/null || :)"
  real="$(cd -- "$path" 2>/dev/null && pwd -P)" || real=""
  if [ -z "$real" ] || [ "$toplevel" != "$real" ]; then
    LANES_WS_WHY="$yaml names path: $path, which is not the ROOT of that checkout (${toplevel:-unknown} is)"
    return 1
  fi
  printf '%s\n' "$real"
}

# The refusal's body, so all four helpers say the same thing in the same words.
#
# THE REASON IS RE-DERIVED HERE, IN THE PARENT SHELL, and that is the whole
# point of this line. Every caller reaches the resolver through a command
# substitution — `LANES_WS_ROOT="$(lanes_workspace_root || :)"` — which runs it
# in a SUBSHELL, so the $LANES_WS_WHY it set died with that subshell and the
# parent still holds the empty string it started with. Six distinct diagnoses
# were being computed and all six were being thrown away; every one of the
# refusals Amendment 9(a) enumerates printed the same "no reason recorded".
# The resolver is read-only and cheap — two `sed`s and three `git` reads — and
# it is reached on the refusal path only, so running it again HERE is the one
# place its answer survives to be printed.
lanes_workspace_why() {
  [ -n "$LANES_WS_WHY" ] || lanes_workspace_root >/dev/null 2>&1 || :
  printf '%s' "the workspace repository could not be found — ${LANES_WS_WHY:-no reason recorded}.
    Amendment 9(a): every lane helper reads \`repository:\` and \`path:\` from
    $(lanes_ws_yaml), never from its own location. One command writes that
    file, creates or clones the repository and places the symlinks:
        openRepoTools wip init"
}
# --- END shared workspace resolver ------------------------------------------

# THE REGISTER AND ITS WRITER, after the move (Amendment 9(b)). The register is
# the workspace repository's `lanes/LANES.md`, found through the pointer file
# above. The writer is `lanes-edit.sh`, which `--install` places BESIDE this
# command — in `~/.local/bin` when installed, in the checkout when run from one
# — so `$SCRIPT_DIR` answers both without a second rule. `$OPENREPOTOOLS_BIN_DIR`
# and `$PATH` are the fallbacks for a command copied somewhere on its own.
LANES_WS_ROOT=""
if [ -z "${LANES_FILE:-}" ] || [ -z "${LANES_REPO:-}" ]; then
  LANES_WS_ROOT="$(lanes_workspace_root || :)"
fi
: "${LANES_REPO:=$LANES_WS_ROOT}"
LANES_FILE="${LANES_FILE:-${LANES_WS_ROOT:+$LANES_WS_ROOT/lanes/LANES.md}}"

lanes_edit_default() {
  local d
  [ -x "$SCRIPT_DIR/lanes-edit.sh" ] && { printf '%s\n' "$SCRIPT_DIR/lanes-edit.sh"; return 0; }
  d="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}"
  [ -x "$d/lanes-edit.sh" ] && { printf '%s\n' "$d/lanes-edit.sh"; return 0; }
  command -v lanes-edit.sh 2>/dev/null && return 0
  printf '%s\n' "$SCRIPT_DIR/lanes-edit.sh"
}
LANES_EDIT="${LANES_EDIT:-$(lanes_edit_default)}"

# THE CLOSE-OUT GATE'S SWEEP (opensoft/openRepoTools#163), found the way the
# writer is: `--install` places `lane-worktrees` beside this command too.
lane_worktrees_default() {
  local d
  [ -x "$SCRIPT_DIR/lane-worktrees" ] && { printf '%s\n' "$SCRIPT_DIR/lane-worktrees"; return 0; }
  d="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}"
  [ -x "$d/lane-worktrees" ] && { printf '%s\n' "$d/lane-worktrees"; return 0; }
  command -v lane-worktrees 2>/dev/null && return 0
  printf '%s\n' "$SCRIPT_DIR/lane-worktrees"
}
LANE_WORKTREES="${LANE_WORKTREES:-$(lane_worktrees_default)}"


LE_TMP=""
cleanup() { [ -n "${LE_TMP:-}" ] && rm -f -- "$LE_TMP"; return 0; }
# A SIGNAL ENDS THE RUN, AND `cleanup` RUNS ON THE WAY OUT. `cleanup` returns
# rather than exits, so the one trap `cleanup EXIT INT TERM` ran it and then
# RESUMED the script at the next command — harmless while nothing was left to
# delete, and a fail-open once the sweep's capture file is `LE_TMP`: the
# resumed script read the deleted file as an empty note and went on to write
# (#138 item 6, the delta review of fe6e38a). The codes are the shell's own,
# 128 plus the signal, and the EXIT trap does the removing once.
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

note() { printf '%s: %s\n' "$prog" "$*" >&2; }
step() { printf '%s: %s\n' "$prog" "$*" >&2; }
plan() { printf '%s: PLAN %s\n' "$prog" "$*" >&2; }
die()  { printf '%s: %s\n' "$prog" "$1" >&2; exit "${2:-1}"; }
usage() { sed -n -e '1,2d' -e '/^#/!q' -e 's/^# \{0,1\}//' -e p "$RESOLVED"; }

# ------------------------------------------------------------------ arguments

retire=0; force=0; dry_run=0; state_was=""; home_override=""; retire_target=""
retire_dormant=""; sweep_reason=""; sweep_yes=0; inventory_only=0
positional=()

# A TRANSCRIPT UUID AND A PID, TOLD APART FROM A LANE ARGUMENT BY SHAPE.
# `--retire` is two acts now (A11 Addendum 4 ruling 8) and the value is what
# tells them apart, so the shapes a value may have are written here once.
is_fork_uuid() { [[ "${1-}" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; }
is_fork_pid()  { case "${1-}" in '' | *[!0-9]*) return 1 ;; esac; return 0; }

while [ $# -gt 0 ]; do
  case "$1" in
    # `--retire` IS TWO ACTS, AND THE VALUE TELLS THEM APART (A11 Addendum 4
    # ruling 8, ratified "a11 addendum 4 yes"). Bare, it is what it has always
    # been: mark THIS LANE's state cell RETIRED as part of ending it. With a
    # `<pid|uuid>` it is the one act clause (k) rule (e) gives for a live FORK
    # of the lane's transcript — *"both print the one act: retire it … and
    # `lane-end`'s `--retire` is the door"* — and it ends nothing.
    #
    # THE ONE AMBIGUOUS SPELLING IS REFUSED RATHER THAN GUESSED AT. A bare
    # number is a legal lane argument (`lane-end 3` ends `<cwd repo>-3`), so
    # `lane-end --retire 3` could be either act. Where no lane has been named
    # yet, that spelling refuses and prints both — the alternative is choosing
    # between two different acts on a coin toss. A uuid is never ambiguous,
    # and `--retire=<value>` is always the fork form.
    --retire=*)
      retire=1; retire_target="${1#--retire=}"
      [ -n "$retire_target" ] || die "--retire= needs a pid or a transcript uuid" 2
      shift ;;
    --retire)
      retire=1
      if is_fork_uuid "${2-}"; then
        retire_target="$2"; shift 2
      elif is_fork_pid "${2-}"; then
        [ "${#positional[@]}" -gt 0 ] || die "'--retire $2' is ambiguous: '$2' is both a lane POSITION — \`$prog $2\` ends lane <repo>-$2 and marks it RETIRED — and a pid, which retires a live FORK of a lane and ends nothing. Name the lane first: $prog <lane> --retire $2   (or spell the fork form $prog <lane> --retire=$2)" 2
        retire_target="$2"; shift 2
      else
        shift
      fi ;;
    # AMENDMENT 19(c) — THE SWEEP. It takes the REPOSITORY as its own value, so
    # that nothing about it is ambiguous with the lane argument this command
    # otherwise takes: `lane-end --retire-dormant openxFactory` names a
    # repository and never a lane, and the bare form with the repository as a
    # positional is accepted for the reader who typed it that way.
    --retire-dormant)
      case "${2-}" in
        '' | -*) retire_dormant="-" ;;
        *) retire_dormant="$2"; shift ;;
      esac
      shift ;;
    --retire-dormant=*)
      retire_dormant="${1#--retire-dormant=}"
      [ -n "$retire_dormant" ] || die "--retire-dormant= needs the repository whose dormant rows are to be swept" 2
      shift ;;
    # A FLAG IS NOT A VALUE (Copilot round 1 on #93): `--reason --yes` took
    # `--yes` as the reason, fell back to the dry run and exited 0 — a malformed
    # line that ran and did not do what it said. A value beginning with `-` is
    # refused, and the `=` spelling is named for a reason that really begins
    # with one.
    --reason)      case "${2-}" in '' | -*) die "--reason needs the words that say why, and '${2-}' is a flag — so the reason was left out and the act it belongs to may have been too. Spell a reason that really begins with a dash as --reason=<why>." 2 ;; esac
                   sweep_reason="$2"; shift 2 ;;
    --reason=*)    sweep_reason="${1#--reason=}"; [ -n "$sweep_reason" ] || die "--reason needs the words that say why" 2; shift ;;
    --yes)         sweep_yes=1; shift ;;
    --home)        home_override="${2-}"; [ -n "$home_override" ] || die "--home needs owner/repo" 2; shift 2 ;;
    --home=*)      home_override="${1#--home=}"; [ -n "$home_override" ] || die "--home needs owner/repo" 2; shift ;;
    --force)       force=1; shift ;;
    # THE CLOSE-OUT GATE'S ONE DOOR, AND THE WORD HELD FOR ITS SECOND (#163).
    # `--sweep` would retire the lane's trees in the same run as its ending,
    # which is not built — `lane-worktrees sweep --yes` is that act, on its own
    # — so the word is refused by name rather than read as an unknown option.
    --inventory-only) inventory_only=1; shift ;;
    --sweep | --sweep=*)
                   die "--sweep is not built: retiring a lane's trees in the same run as its ending is not one act. \`lane-worktrees sweep <lane> --yes\` retires them first, on its own (it removes only what is on origin or in a bundle, never a live writer's tree), and then $prog <lane> ends the lane; or $prog <lane> --inventory-only ends the lane with its trees left where they are, in its inventory. Nothing was written." 2 ;;
    --dry-run)     dry_run=1; shift ;;
    --state-was)   state_was="${2-}"; [ -n "$state_was" ] || die "--state-was needs the exact text to replace" 2; shift 2 ;;
    --state-was=*) state_was="${1#--state-was=}"; [ -n "$state_was" ] || die "--state-was needs the exact text to replace" 2; shift ;;
    -h|--help)     usage; exit 0 ;;
    --)            shift ;;
    -*)            die "unknown option '$1' — see $prog --help" 2 ;;
    *)             positional+=("$1"); shift ;;
  esac
done

# ------------------- AMENDMENT 19(c): `--retire-dormant <repo>`, THE SWEEP ---
#
# IT IS NOT A LANE ARGUMENT AND IT NEVER ENDS A LANE, so it is answered here —
# before the `<lane>` derivation below, which would otherwise refuse this line
# for naming no lane at all.
#
# THE DRY RUN IS THE DEFAULT AND THE AMENDMENT SAYS WHY: *"so the person sees
# what the sweep will close before it closes it; an agent's stdin is not a
# terminal, so the word is `--yes` typed by a person"*. This half makes no
# decision of its own about what is dormant: it asks the READ — `lanes --closed
# --prefix <repo>`, whose fourteenth column is the row's class — so the listing
# and the sweep can never disagree about which rows they are talking about, and
# `lanes-edit.sh retire-rows` re-proves every one of clause (c)'s refusals for
# itself before it writes a byte.
if [ -n "$retire_dormant" ]; then
  [ "$retire" = 0 ] || die "--retire and --retire-dormant are two different acts: --retire is a lane's own ending (or a live fork's), and --retire-dormant sweeps a repository's rows that have no lane left to end. Run one of them." 2
  [ "$force" = 0 ] || die "--force ends a lane that still holds something; it has no meaning for a sweep of rows whose lanes ended before Amendment 7. Nothing was written." 2
  [ "$inventory_only" = 0 ] || die "--inventory-only is the door past ONE lane's close-out gate; the sweep of dormant rows ends no lane and asks no gate. Nothing was written." 2
  [ -z "$state_was" ] || die "--state-was is for the state cell of a lane being ended; the sweep writes each row's cell itself, from the reason and the row's own last words. Nothing was written." 2
  [ -z "$home_override" ] || die "--home is for a pre-cutover lane's own log line; the sweep writes no home into anything. Nothing was written." 2
  [ "$dry_run" = 0 ] || sweep_yes=0
  # THE REPOSITORY, WHICHEVER WAY IT WAS TYPED — as the flag's value, or as the
  # one positional after a bare flag. Two of them is a refusal rather than a
  # choice between them.
  if [ "$retire_dormant" = "-" ]; then
    case "${#positional[@]}" in
      1) retire_dormant="${positional[0]}" ;;
      0) die "which repository? $prog --retire-dormant <repo> [--reason \"<why>\"] [--yes]" 2 ;;
      *) die "--retire-dormant takes ONE repository: $prog --retire-dormant <repo>" 2 ;;
    esac
  elif [ "${#positional[@]}" -gt 0 ]; then
    die "--retire-dormant already names the repository ($retire_dormant), so '${positional[0]}' is one argument too many. It sweeps a repository's dormant ROWS and takes no lane: $prog --retire-dormant $retire_dormant [--reason \"<why>\"] [--yes]" 2
  fi
  case "$retire_dormant" in
    *[!A-Za-z0-9._-]* | '') die "'$retire_dormant' is not a repository name (letters, digits, . _ -)" 2 ;;
  esac
  if [ -z "${LANES_FILE:-}" ]; then die "$(lanes_workspace_why)" 1; fi
  [ -f "$LANES_FILE" ] || die "no register at $LANES_FILE — the workspace repository $(lanes_ws_yaml) names carries no lanes/LANES.md. \`openRepoTools wip init\` seeds one (it is idempotent); or set LANES_FILE" 1
  [ -x "$LANES_EDIT" ] || die "no register writer at $LANES_EDIT — \`lanes-edit.sh\` is installed beside this command by \`openRepoTools --install\`. Run that, or set LANES_EDIT" 1

  # THE REASON IS CHECKED BEFORE THE DRY RUN IS PRINTED (Copilot round 2 on
  # #93). `retire_rows` refuses a reason that would forge a cell, and it is
  # reached only under `--yes` — so a dry run over a bad reason printed a clean
  # preview of an act that could not run, which is the one thing a preview may
  # not do. The rules are the writer's own (Amendment 13(a) and ratified
  # decision O1) and the refusal names the argument.
  if [ -n "$sweep_reason" ]; then
    case "$sweep_reason" in
      *'|'*)   die "--reason may not contain '|': it would forge a cell boundary in every row this sweep rewrites. Got: '$sweep_reason'" 2 ;;
      *' · '*) die "--reason may not contain ' · ': that separator is what divides the state cell's three parts (Amendment 13(a)), and a second one reads back as a fourth part. Use a semicolon. Got: '$sweep_reason'" 2 ;;
    esac
    if [ "${sweep_reason//[$'\n\r']/}" != "$sweep_reason" ]; then
      die "--reason is ONE line: a newline in it would split a row in two and every row after it would be read as a lane" 2
    fi
    sweep_us="$(printf '\037')"
    if [ "${sweep_reason//$sweep_us/}" != "$sweep_reason" ]; then
      die "--reason may not contain the internal field separator: it would corrupt the sweep plan before any row is written" 2
    fi
    [ "${#sweep_reason}" -le 180 ] ||
      die "--reason is ${#sweep_reason} characters. The cell's whole line is capped at 240 (Amendment 13, ratified decision O1) and the row's OWN last words go in beside it, so a reason longer than 180 leaves nothing of them. Shorten it — the long version belongs in the lane's log." 2
  fi

  # THE READ IS FETCHED. Every other listing in this family is local by design —
  # it sits in front of a person who is waiting — but this one sits in front of
  # a WRITE, and a dormant row is a row about which this checkout's copy may be
  # a week old.
  # THE READ'S STDERR IS KEPT (Copilot round 4 on #93). `lanes_rows` leaves the
  # class unset where this workstation's session records could not be read —
  # DORMANT is "no log AND no live session here", and the second half was never
  # established — and it SAYS so on stderr. Without that sentence this command
  # would count zero dormant rows and exit 8 announcing that none are dormant,
  # which is a reason it has not got. The stream is passed through either way:
  # it is the person's.
  # AND THE CAPTURE IS NOT OPTIONAL (#138 item 6, the review of `dbbb117`).
  # Where `mktemp` cannot make the file, the read ran with its stderr
  # uncaptured and `rd_err` stayed empty, so the sentence above never reached
  # the check below that refuses on it and the sweep went on over a fact that
  # was not read. Not being able to read the read's notes is not their absence.
  rd_rc=0
  rd_err=""
  # `LE_TMP` IS WHAT `cleanup` REMOVES ON EVERY EXIT, and it is assigned from
  # the `mktemp` itself so that no signal can land between the file existing
  # and `cleanup` knowing its name. The note is relayed on stderr, and where
  # that write fails — a closed or full stderr — `set -e` leaves before a plain
  # `rm` below is reached and the capture file stayed in `$TMPDIR`.
  LE_TMP="$(mktemp "${TMPDIR:-/tmp}/lane-end-sweep.XXXXXX" 2>/dev/null || printf '')"
  rd_errf="$LE_TMP"
  [ -n "$rd_errf" ] ||
    die "no temporary file could be made to capture the read's own notes, so whether this workstation's session records could be read was never established — and DORMANT is 'no object log AND no live session here'. The rows are not counted over a fact that was not read (Amendment 7(d)). Fix \$TMPDIR (full, gone or not writable) and re-run. Nothing was written." 1
  rd_rows="$("$LANES_EDIT" lanes --fetch --closed --prefix "$retire_dormant" 2>"$rd_errf")" || rd_rc=$?
  # AND A CAPTURE THAT CANNOT BE READ BACK IS NOT AN EMPTY ONE: the empty note
  # is the one that never refuses.
  rd_err="$(cat -- "$rd_errf")" ||
    die "the capture of the read's own notes could not be read back, so whether this workstation's session records could be read was never established — and DORMANT is 'no object log AND no live session here'. The rows are not counted over a fact that was not read (Amendment 7(d)). Nothing was written." 1
  printf '%s' "$rd_err" >&2
  rm -f -- "$rd_errf"
  case "$rd_err" in
    *"session records could not be read"*)
      die "this workstation's session records could not be read (the read says so above), so whether any of $retire_dormant's rows has a LIVE session here was never established — and DORMANT is 'no object log AND no live session here'. The rows are not counted over a fact that was not read (Amendment 7(d)). Fix the read and re-run; nothing was written." 1 ;;
    # AND WHERE THE READ CLASSED NO ROW AT ALL BECAUSE A READ FAILED — the log
    # directory, the published logs' warnings, or the register itself (#138
    # items 4 and 11, the reviews of #230): counting zero here was this
    # command's "there is nothing to do", exit 8, about rows nobody could class.
    *"no row is classed DORMANT here"*)
      die "the read could not establish which of $retire_dormant's rows are dormant (it says why above), so it classed none of them DORMANT — and that is NOT 'none of them are' (Amendment 7(d)). Fix the read and re-run; nothing was written." 1 ;;
  esac
  case "$rd_rc" in
    0) : ;;
    8) die "no lane of $retire_dormant is recorded at all, so there is no dormant row to retire. Nothing was written." 8 ;;
    2) die "the \`lanes-edit.sh\` on this workstation has no \`lanes\` subcommand, so it predates lane-collision-protocol Amendment 11 and cannot say which rows are dormant. This workstation has not taken the install yet:  openRepoTools --install" 2 ;;
    *) die "$LANES_EDIT lanes --closed --prefix $retire_dormant exited $rd_rc, so which rows are dormant was never established — and that is NOT 'none of them are' (Amendment 7(d)). Run it by hand to see what it says. Nothing was written." 1 ;;
  esac

  rd_lanes=(); rd_n=0; rd_flagged=0
  rd_report=""
  TAB="$(printf '\t')"
  # THE READ'S SEVENTEEN COLUMNS BY NAME: 13 is Amendment 18(b)'s locality and
  # 14 to 17 are Amendment 19's class, start, head and flags — so the class this
  # sweep selects on is the fourteenth, never the thirteenth.
  while IFS="$TAB" read -r d_lane d_state d_ws d_pf d_win d_sid d_dir d_obj d_age d_cmd d_home d_fk d_loc d_class d_start d_was d_flags; do
    [ -n "${d_lane:-}" ] || continue
    [ "${d_class:-none}" = dormant ] || continue
    rd_lanes+=("$d_lane")
    rd_n=$((rd_n + 1))
    d_mark="    "
    case "${d_flags:-none}" in
      none | '') d_flags="" ;;
      *) d_mark="FLAG"; rd_flagged=$((rd_flagged + 1)) ;;
    esac
    # THE TWO FREE-TEXT COLUMNS ARE CUT TO THEIR WIDTH AND THE NAME IS NOT.
    # Measured on the live register: `started` holds `2026-09-02 ~22:30Z;
    # RESUMED 09-03 12:55Z and 19:27Z` and the workstation cell `<workstation? —
    # owner fills>`, either of which pushes every column after it out of line in
    # a report a person reads to decide. The LANE is never cut — it is the name
    # they act on — and the cut is a bash expansion rather than printf's `%.Ns`,
    # which counts BYTES and would tear the `≤` and `—` these cells carry.
    d_start_s="${d_start:-unknown}"
    if [ "${#d_start_s}" -gt 20 ]; then d_start_s="${d_start_s:0:19}…"; fi
    d_ws_s="${d_ws:-unknown}"
    if [ "${#d_ws_s}" -gt 10 ]; then d_ws_s="${d_ws_s:0:9}…"; fi
    rd_report="$rd_report$(printf '  %s %-28s %-20s %-10s %s' "$d_mark" "$d_lane" "$d_start_s" "$d_ws_s" "$d_flags")
$(printf '       was: %s' "${d_was:-none}")
"
  done <<EOF
$rd_rows
EOF

  printf '%s --retire-dormant %s — %s (lane-collision-protocol Amendment 19(c))\n' \
    "$prog" "$retire_dormant" "$( [ "$sweep_yes" = 1 ] && printf 'THE ACT' || printf 'DRY RUN, nothing is written' )"
  printf '  register : %s\n' "$LANES_FILE"
  printf '  dormant  : %s row(s), %s of them FLAGGED\n' "$rd_n" "$rd_flagged"
  if [ "$rd_n" = 0 ]; then
    die "no row of $retire_dormant is DORMANT — a dormant row is one with no object log and no live session here (Amendment 19(a)), and every row of that repository has one or the other. There is nothing to do, and nothing was done. \`lanes --closed --prefix $retire_dormant\` shows what its rows say." 8
  fi
  printf '\n       lane                         started              where      flags\n'
  printf '%s' "$rd_report"
  printf '\n  A FLAG is this row saying the work was unpushed, lost, a loss or owed, or\n'
  printf '  still saying LIVE. Retiring the row does not lose that text: it is carried\n'
  printf '  into the RETIRED line of the log this sweep creates, and into the cell.\n'
  printf '  Nothing outside the register and the logs is touched — worktrees, branches\n'
  printf '  and handoffs stay exactly where they are (Amendment 19(c)).\n'
  if [ -n "$sweep_reason" ]; then
    printf '  reason   : %s\n' "$sweep_reason"
  else
    printf '  reason   : none given — the writer names the amendment (--reason "<why>" says it better)\n'
  fi

  # THE OFFERED LINE CARRIES THE REASON AS ONE SHELL-QUOTED `--reason=` WORD
  # (#93, the review of `44b77c3`). Printed as `--reason "<why>"`, an accepted
  # `--reason=--why` came back as a dash-led value the parser refuses, and a
  # reason holding `"` or `$(` came back as shell syntax the person would run.
  if [ "$sweep_yes" != 1 ]; then
    printf '\n  DRY RUN — nothing was written. Make it the one act with:\n'
    printf '    LANES_LANE=<your lane> %s --retire-dormant %s%s --yes\n' \
      "$prog" "$retire_dormant" "$( [ -n "$sweep_reason" ] && printf ' --reason=%q' "$sweep_reason" || printf ' --reason "<why>"' )"
    exit 0
  fi

  # THE WRITER IS A LANE, AND IT IS NOT GUESSED. Every line this sweep appends
  # carries the sweeping lane's own transcript uuid in its session field
  # (Amendment 7(b)), and a session field that is anything else is wrong for
  # ever in an append-only log — so the lane is named by the person running it
  # or the act does not happen.
  [ -n "${LANES_LANE:-}" ] ||
    die "the sweep writes a RETIRED line into each lane's log, and the session field of every one of them is the SWEEPING lane's transcript uuid (Amendment 7(b)) — so this act needs to know which lane is doing it. Re-run it from your lane: LANES_LANE=<your lane> $prog --retire-dormant $retire_dormant --yes. Nothing was written." 2
  rd_argv=(retire-rows)
  for rd_l in ${rd_lanes[@]+"${rd_lanes[@]}"}; do rd_argv+=("$rd_l"); done
  # AN `&&` LIST WHOSE TEST FAILS IS A FAILING COMMAND, and this script runs
  # under `set -e`: written as `[ -n "$x" ] && rd_argv+=(…)` a run with no
  # `--reason` would exit here, silently, having printed the whole dry run.
  #
  # AND IT IS FORWARDED AS ONE ARGUMENT (Copilot round 2 on #93). Split as
  # `--reason <why>`, the documented spelling for a reason that BEGINS with a
  # dash — `--reason=--why`, which this parser accepts — arrived at the writer
  # as two words, and the writer refuses a dash-led value there by design. So
  # the confirmed act refused what the dry run had just accepted.
  if [ -n "$sweep_reason" ]; then rd_argv+=("--reason=$sweep_reason"); fi
  rd_argv+=(--writer "$LANES_LANE")
  step "the one commit: $LANES_EDIT ${rd_argv[*]}"
  "$LANES_EDIT" "${rd_argv[@]}"
  exit $?
fi

# THE SWEEP'S OWN FLAGS BELONG TO THE SWEEP (Copilot round 3 on #93). They are
# parsed above for every invocation, and without this `lane-end <lane> --yes`
# ran the ORDINARY ending — a line that reads like a confirmed sweep doing
# something else entirely — while `lane-end <lane> --reason "<why>"` dropped the
# reason on the floor. Neither is a typo this command may interpret.
if [ -z "$retire_dormant" ]; then
  [ "$sweep_yes" = 0 ] ||
    die "--yes is the word that makes the SWEEP the act (Amendment 19(c)) and it belongs to --retire-dormant. Ending one lane needs no confirmation and takes none: $prog <lane>. Nothing was written." 2
  [ -z "$sweep_reason" ] ||
    die "--reason says why a repository's DORMANT ROWS are being retired (Amendment 19(c)) and it belongs to --retire-dormant; ending one lane writes its own closing line and would drop these words. Nothing was written: $prog <lane>, or $prog --retire-dormant <repo> --reason \"$sweep_reason\"." 2
fi

# A lane name is taken VERBATIM when one argument is given: the register
# still carries lanes named before the `<repo>-<n>` rule (browser-ui-repair,
# hermes-wallet-exercise), and their last act is exactly what this is for.
LANE=""
case "${#positional[@]}" in
  2) LANE="${positional[0]}-${positional[1]}" ;;
  1)
    arg="${positional[0]}"
    if [[ "$arg" =~ ^[0-9]+[A-Za-z]?$ ]]; then
      top="$(git rev-parse --show-toplevel 2>/dev/null || :)"
      [ -n "$top" ] || die "no <repo> given and this directory is not a git checkout — run: $prog <repo> $arg" 1
      LANE="$(basename -- "$top")-$arg"
    else
      LANE="$arg"
    fi
    ;;
  0) usage >&2; die "which lane? $prog <lane>" 2 ;;
  *) die "too many arguments: $prog <lane>" 2 ;;
esac
[[ "$LANE" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die "'$LANE' is not a lane name (allowed: letters, digits, . _ -)" 2

if [ -z "${LANES_FILE:-}" ]; then die "$(lanes_workspace_why)" 1; fi
[ -f "$LANES_FILE" ] || die "no register at $LANES_FILE — the workspace repository $(lanes_ws_yaml) names carries no lanes/LANES.md. \`openRepoTools wip init\` seeds one (it is idempotent); or set LANES_FILE" 1
[ -x "$LANES_EDIT" ] || die "no register writer at $LANES_EDIT — \`lanes-edit.sh\` is installed beside this command by \`openRepoTools --install\`. Run that, or set LANES_EDIT" 1

# ------------------------- AMENDMENT 15: THE ROW'S SPELLING IS THE LANE'S NAME
#
# **A lane name is one name under any case** (ratified 2026-09-14T00:07:18Z),
# and `lanes-edit.sh canon-lane` is the ONE resolver the commands share rather
# than each keeping its own — for the reason Amendment 11 clause (h) gives every
# other read here: an implementation of "which row is this name's" in each
# command is how they would come to disagree about it.
#
# `2` IS TWO ANSWERS AND ONE OF THEM IS A REFUSAL (Copilot round 4 on
# openRepoTools#41). It is what a helper predating this amendment spends on an
# unknown subcommand, and it is also `canon-lane`'s own two-rows refusal. Both
# used to carry on with the name as typed, on the reading that this command's own
# row scan sees the pair anyway — and it does, in `$LANES_FILE`, THE WORKING
# TREE. The register the refusal is about is `origin/<branch>`: on a checkout
# that is behind, the pair is published, the local scan finds exactly one row and
# passes, and the only other place the refusal could still surface is the
# `register-row` read below, whose stderr is suppressed and whose 2 becomes a
# generic exit 1 naming neither spelling. So the refusal is identified by its own
# words — no helper predating the amendment emits them from an unknown
# subcommand — and relayed, with the old-helper fallback left as it was.

cl_out=""; cl_rc=0
cl_out="$("$LANES_EDIT" canon-lane "$LANE" 2>/dev/null)" || cl_rc=$?
cl_why=""; cl_pair=0
case "$cl_rc" in 2 | 66) cl_why="$(LANES_NO_FETCH=1 "$LANES_EDIT" canon-lane "$LANE" 2>&1 >/dev/null || :)" ;; esac
case "$cl_why" in *"Amendment 15(d)"*) cl_pair=1 ;; esac
# AMENDMENT 16(e) — AND 66 IS THE ALIAS TABLE THAT COULD NOT BE READ (Copilot
# round 4 on openRepoTools#81). It is its own number because 2 already carries
# two meanings here, and this command must not END a lane it cannot name: a
# former name that resolves to nothing reads as a lane with no row, and the
# ENDED line would be written under a name no reader resolves.
if [ "$cl_rc" = 66 ]; then
  printf '%s\n' "$cl_why" >&2
  die "lane $LANE's name could not be resolved: the lane alias table could not be read (above, exit 66). Amendment 16(e) makes a renamed lane's old name resolve through that file for ever, so a name it cannot answer for may be a lane whose row is under another name — and this command will not end a lane it cannot name. The row is untouched and nothing was written." 2
fi
if [ "$cl_pair" = 1 ]; then
  printf '%s\n' "$cl_why" >&2
  die "the PUBLISHED register spells lane $LANE two ways (above), so no read and no write under that name is unambiguous and this command will not end a lane it cannot name — Amendment 15(d): merge the newer row's session id(s) into the older row's session cell, in order, remove the newer row in the SAME commit naming both spellings, push, and re-run. The row is untouched and nothing was written." 2
fi
if [ "$cl_rc" = 0 ] && [ -n "$cl_out" ] && [ "$cl_out" != "$LANE" ]; then
  note "lane $LANE → $cl_out (Amendment 15: the register row's spelling is canonical)"
  LANE="$cl_out"
fi

# THE SEAM (ruling 2026-10-04): A MANAGED-OWNED LANE IS NOT THIS COMMAND'S.
# `lanes-edit.sh managed-projection` answers 0 with the owner where the lane's
# register row carries the managed ledger's marker, 8 for a legacy lane, and 1
# where ownership could not be established. ANY other status — a helper that
# predates the verb included — is UNKNOWN as well, and refused the same way:
# a fence that opens because its read failed is no fence (Amendment 7(d)).
seam_refuse() {   # <lane> <the act>
  sr_out=""; sr_rc=0
  sr_out="$("$LANES_EDIT" managed-projection "$1" 2>/dev/null)" || sr_rc=$?
  case "$sr_rc" in
  0) die "lane $1 is owned by the managed ledger (owner ${sr_out:-unnamed}), and $2 is a legacy lane act, refused here, once the lane is named and before a byte is written: no state cell, no ENDED or RETIRED line, no handoff fragment, and no process signalled. Brett Heap's ruling of 2026-10-04: \"managed ledger owns enrolled lanes; #97 owns legacy\". Act on this lane through the managed ledger." 2 ;;
  8) : ;;
  *) die "whether the managed ledger owns lane $1 is UNKNOWN (\`$LANES_EDIT managed-projection $1\` exited $sr_rc): its register row carries managed-owner vocabulary that does not parse, or the read failed. An ownership nobody could establish is never read as legacy (Amendment 7(d)), so $2 is refused here, once the lane is named and before a byte is written: no state cell, no ENDED or RETIRED line, no handoff fragment, and no process signalled. Read it: $LANES_EDIT managed-projection $1" 1 ;;
  esac
}
# CALLED TWICE BELOW, each time before the first thing its path does: at the
# head of the `--retire <pid>` door, which signals processes, and right after
# "the row"'s own refusals on the ending path (the ending and `--retire`),
# before the state cell and the ENDED/RETIRED line. NOT HERE, ONCE: "the row"
# refuses a register that spells this lane two ways (Amendment 15(d)) out of its
# OWN read, so that the words a person needs arrive even through a helper that
# predates `canon-lane` — and such a helper predates `managed-projection` too,
# which this function rightly calls UNKNOWN. Both are refusals that write
# nothing; the pair's comes first so that it is still the one said.
# (`--retire-dormant` is above and is refused by `retire-rows` itself.)

# `--home` IS VALIDATED BEFORE ANYTHING IS WRITTEN. It is for a PRE-CUTOVER
# lane, and `lanes-edit.sh` refuses it once the lane's own log records a home —
# but the only place this script passes it is its LAST act, the ENDED/RETIRED
# line, which is written AFTER the row has been closed and whose failure is
# deliberately never fatal. So a bad option that could have been rejected in a
# second instead closed the row, lost the lane its ENDED line, and exited 0. The
# check is the writer's own `resolve_home`, asked for through `resolve-home`, so
# the preflight and the write cannot disagree: 0 or 8 is an answer, 2 is the
# refusal, and anything else is the helper failing.
if [ -n "$home_override" ]; then
  home_rc=0
  home_err="$("$LANES_EDIT" resolve-home "$LANE" "$home_override" 2>&1 >/dev/null)" || home_rc=$?
  [ -n "$home_err" ] && printf '%s\n' "$home_err" >&2
  case "$home_rc" in
    0|8) : ;;
    2) die "--home $home_override is refused for lane $LANE (the writer's reason is above). Nothing has been written and the row is untouched." 2 ;;
    *) die "$LANES_EDIT resolve-home $LANE $home_override failed (exit $home_rc). That is not a verdict on the option, and this script will not close a row on an argument it could not check. Fix the helper, then re-run." 1 ;;
  esac
fi

# ------------------------------------------- --retire <pid|uuid>: A LIVE FORK
#
# THE ONE ACT EVERY SURFACE PRINTS FOR A FORK (A11 clause (k) rule (e), A11
# Addendum 4 ruling 8, `R-A11-24`): *"both print the one act: **retire it**,
# under Amendment 6(d), which is the operator's act and stays the operator's.
# **Neither kills a process** … and `lane-end`'s `--retire` is the door."*
# Before this round the door did not exist: `restart` printed a READ
# (`lanes-edit.sh forks <lane>`), `lanes` printed *"Name them"*, and two places
# said *"retiring one is `kill <pid>`"* — three spellings of one ratified act,
# none of them it.
#
# IT IS A DOOR AND NOT A RECORD, AND THAT IS THIS ROUND CORRECTING ITSELF. The
# first build of this act wrote `RETIRED — lane <lane>, … , lane:<lane> → fork
# <sid>; pid <n>; kind <k>` into the lane's own log and filtered `forks` on it.
# It cannot: **Amendment 7(b) gives `RETIRED` NO PAYLOAD** — *"`PAUSED`,
# `RESUMED`, `ENDED` and `RETIRED` take no payload"* — and Amendment 11 clause
# (c), which narrows that sentence for `RESUMED` and counts the narrowing as
# **edit 1 of six**, says in the same breath that *"`ENDED` and `RETIRED` stay
# payload-free"*. The other spelling is no better: 7(b)'s *"Every lane-kind line
# takes the object `lane:<name>`"* is enforced by this estate's own writer
# (`lanes-edit.sh`'s `log` arm: *"'$verb' is a lane verb: its object is
# lane:<name>"*), so an object of `fork:<uuid>` narrows that half instead. Either
# way it is a **SEVENTH edit to in-force text** where the count is six and the
# six are ratified (`R-A11-15`) — and the first one also needed the state reader
# to learn a second rule about which of a lane's lines are the lane's, in an
# append-only file. Writing that silently is the exact defect this estate blocks
# on, so the record is dropped rather than smuggled.
#
# WHAT THE RULE ACTUALLY ASKS FOR IS REPORTING, and it says so in its own words:
# *"This is Amendment 8(f)'s own posture — 'reports what 6(d) is owed and
# performs none of it' — extended to the case that produced it, and the
# reporting is what closed Evidence 6 on the day."* So this act **proves** the
# target is THIS lane's live fork, and **prints Amendment 6(d)'s retire act for
# it, filled in**. It writes nothing and it kills nothing.
#
# AND IT IS NOT `forks` WITH EXTRA STEPS. `forks` NAMES every fork; this takes
# ONE, refuses anything that is not one of them (a stranger's pid, a uuid of
# another lane), refuses a `forks` read that FAILED rather than reading it as
# *"it is not a fork"*, and spells 6(d) for the kind of session it actually is.
# One act, one name, on every surface — which is the whole of `R-A11-24`.
#
# `kill <pid>` IS PRINTED BY NOTHING (`R-A11-24` in terms). The pid and the cwd
# are printed as FACTS — `who` and the hook have always printed them — so a
# person can find the process; what is never printed is a command that ends it.
#
# ALL OF THE ABOVE IS ABOUT A FORK, AND ONLY A FORK (opensoft/openRepoTools#39,
# Amendment 18(h)). A DUPLICATE HOLDER is a different thing wearing a similar
# shape, and this door tells them apart before it decides what to do: a fork
# (decision 8(e)) is a live session under an id the row does NOT record,
# wearing this lane's title — it might be somebody's long-running job with a
# prompt still open, so Amendment 8(f) reserves ending it for a person. A
# duplicate holder is a live `--fork-session` process found in the PROCESS
# TABLE under an id the row DOES record — the very thing Amendment 18(h) says
# may never coexist with the lane's own live session ("the tooling refuses to
# build on either… it never appends such an id to the row and never launches a
# second resume of it"). Nobody is answering a prompt in a background
# `bg-pty-host` fork of a transcript this lane's own row already claims for
# itself, so this door TERMS that pair on sight rather than only naming it —
# the retirement Evidence of 2026-09-14 had to do by hand, outside every tool
# this estate has (`kill -TERM`, on Brett Heap's word). It never touches a
# process on another transcript, and it refuses the lane's own live session
# exactly as the fork case refuses to end a lane's row.
if [ -n "$retire_target" ]; then
  [ "$force" = 0 ] || die "--force is for ending a lane that still holds something; it has no meaning for retiring a fork. Run: $prog $LANE --retire $retire_target" 2
  [ "$inventory_only" = 0 ] || die "--inventory-only is the door past a lane's close-out gate; retiring a fork ends no lane and asks no gate. Run: $prog $LANE --retire $retire_target" 2
  [ -z "$state_was" ] || die "--state-was is for the state cell of a lane being ended; retiring a fork touches no cell. Run: $prog $LANE --retire $retire_target" 2
  # THE SEAM, before any process is looked for or signalled (ruling 2026-10-04).
  seam_refuse "$LANE" "retiring a fork or a duplicate holder"

  # THE FORK IS PROVED BEFORE ANYTHING IS PRINTED, and a read that FAILED is
  # never "it is not a fork": `forks` answers 0 with rows, 8 with none and 1
  # where this workstation's session records could not be read (Amendment 7(d)).
  # Only the first can find one. 8 is DEFERRED rather than an immediate die
  # (issue #30/#39): a lane with no live FORK may still have a live duplicate
  # of one of the two other kinds below, and dying here before either gets a
  # turn would make that read unreachable for exactly the lanes it exists for
  # — one with no fork at all.
  fk_rc=0
  fk_rows="$("$LANES_EDIT" forks "$LANE" 2>/dev/null)" || fk_rc=$?
  case "$fk_rc" in
    0 | 8) : ;;
    *) die "$LANES_EDIT forks $LANE failed (exit $fk_rc). That is NOT 'no fork of it is live', and this script will not name an act for a fork it could not prove is one. Run it by hand to see what it says, then re-run." 1 ;;
  esac

  fk_sid=""; fk_pid=""; fk_kind=""; fk_cwd=""
  if [ "$fk_rc" = 0 ]; then
    while IFS="$(printf '\t')" read -r fr_sid fr_pid fr_kind fr_cwd; do
      [ -n "${fr_sid:-}" ] || continue
      if [ "$(printf '%s' "$fr_sid" | tr 'A-F' 'a-f')" = "$(printf '%s' "$retire_target" | tr 'A-F' 'a-f')" ] \
         || { [ -n "$fr_pid" ] && [ "$fr_pid" = "$retire_target" ]; }; then
        fk_sid="$fr_sid"; fk_pid="$fr_pid"; fk_kind="${fr_kind:-interactive}"; fk_cwd="$fr_cwd"
        break
      fi
    done <<FORKS
$fk_rows
FORKS
  fi

  # ISSUE #39 — NOT A TITLE-BASED FORK; TRY A DUPLICATE HOLDER OF THIS LANE'S
  # OWN TRANSCRIPT INSTEAD (Amendment 18(h)). A cross-profile resume leaves a
  # `bg-pty-host` running `claude --fork-session --resume <path>/<uuid>.jsonl`
  # behind, and once a `lane-start --no-launch` binds that forked id as the
  # row's own session, `forks` (decision 8(e), keyed on "an id the row does
  # NOT record") is designed to stay silent about it — which is the gap: two
  # live processes now hold the one transcript Amendment 18(h) says exactly
  # one may. `duplicate-holder` reads the PROCESS TABLE for exactly that
  # shape and already excludes the lane's own live holder from its rows.
  #
  # ONLY WHEN `fk_sid` IS STILL EMPTY (Copilot round 3, PR #61): a proved
  # fork already answers the whole act below, and this second, unrelated read
  # — a separate `lanes-edit.sh` process, its own `git fetch` — must not turn
  # a WORKING fork retirement into a hard failure because `pgrep` happens to
  # be missing or the process table happens to be unreadable at this exact
  # moment. `LANES_NO_FETCH=1` skips the fetch `forks` just made for the same
  # checkout, so this reads the ref that call already brought current rather
  # than paying for it, and possibly disagreeing with it, twice.
  dh_rc=8; dh_rows=""
  if [ -z "$fk_sid" ]; then
    dh_rc=0
    dh_rows="$(LANES_NO_FETCH=1 "$LANES_EDIT" duplicate-holder "$LANE" 2>/dev/null)" || dh_rc=$?
    case "$dh_rc" in
      0 | 8) : ;;
      *) die "$LANES_EDIT duplicate-holder $LANE failed (exit $dh_rc). That is NOT 'no duplicate holder is live', and this script will not name an act against a process it could not prove is one. Run it by hand to see what it says, then re-run." 1 ;;
    esac
  fi

  # AMENDMENT 18(h) — THE LANE'S OWN LIVE SESSION HAS ITS OWN REFUSAL EVEN
  # WHEN NOTHING ELSE IS HOLDING THIS TRANSCRIPT (Copilot round 2, PR #61):
  # with no fork and no OTHER process on this id, `duplicate-holder` rightly
  # excludes the lane's own pid and answers 8, same as "nothing at all" — but
  # "nothing else holds it" and "the pid you named IS the one thing holding
  # it" are different facts, and only the second earns this specific refusal
  # rather than the generic 8 below. Skipped entirely once a fork has already
  # matched, for the same reason the duplicate-holder read above is: a proved
  # fork answers the act, and this is a THIRD process and fetch this checkout
  # does not need to make to use its own answer. A read that FAILS is not
  # "it is not the lane's own session": only rc 8 clears the general checks
  # that follow; rc 0 either dies here or clears them; anything else dies.
  lh_rc=8; lh_pid=""; lh_sid=""; lh_verdict=""
  if [ -z "$fk_sid" ]; then
    lh_rc=0
    lh_out="$(LANES_NO_FETCH=1 "$LANES_EDIT" live-holder "$LANE" 2>/dev/null)" || lh_rc=$?
    lh_sid=""; lh_tgt=""; lh_name=""; lh_pid=""; lh_verdict=""
    case "$lh_rc" in
      0)
        IFS=$'\037' read -r lh_sid lh_tgt lh_name lh_pid lh_verdict <<LIVE || :
$lh_out
LIVE
        if [ -n "$lh_pid" ] && [ "$lh_pid" = "$retire_target" ]; then
          die "pid $lh_pid is lane $LANE's own live session (session $lh_sid, ${lh_verdict:-here}) — Amendment 18(h) refuses to retire the one process a lane's binding actually is: it is not a fork and not a duplicate holder, it is the lane. End the lane instead, or name the OTHER pid of a genuine duplicate pair." 2
        fi
        ;;
      8) : ;;
      *) die "$LANES_EDIT live-holder $LANE failed (exit $lh_rc) while checking whether $retire_target is lane $LANE's own live session. That is NOT 'it is not' — fix the helper or the records, then re-run." 1 ;;
    esac
  fi

  dh_parent=""; dh_children=""; dh_uuid=""
  if [ -z "$fk_sid" ] && [ "$dh_rc" = 0 ]; then
    # `IFS=$'\037'`, NEVER A TAB: `duplicate-holder`'s child field is
    # DOCUMENTED empty for a parent with no confirmable fork-session child
    # (lanes-edit.sh's own `duplicate_holder_pids`, "the child slot stays
    # empty"), and a bare tab is IFS-whitespace to `read` — two adjacent tabs
    # either side of that empty field collapse into one delimiter exactly as
    # repeated spaces do, shifting the uuid into the child field and leaving
    # `dr_uuid` empty. Measured on e4f9054's CI (`tests-no-submodule`): a
    # partly-retired duplicate's still-alive child was reported as
    # `pid <uuid> (child): gone` and its genuinely-killed parent as
    # `STILL ALIVE, TERM withheld` (its own argv check comparing against an
    # emptied `$dh_uuid`). The unit separator is not IFS whitespace, so it
    # never collapses — the same reason `live-holder`'s own line below already
    # reads with it.
    # PID ONLY, NEVER THE TRANSCRIPT UUID (Copilot round 9, PR #61): a
    # duplicate holder is documented as PID-addressed, and matching `dr_uuid`
    # too made the row's own transcript id a valid target — which cannot
    # DISTINGUISH between multiple process-table holders of one transcript
    # (two separate stray `bg-pty-host` duplicates, or a duplicate beside a
    # not-yet-bound fork under the same id, all carry the identical uuid),
    # so a uuid match silently picked whichever row this loop reached first.
    # The uuid form stays reserved for a title-based FORK above, whose id is
    # one the row does NOT record and so cannot repeat across rows the way a
    # duplicate holder's row-recorded id can.
    #
    # THE CHILD FIELD IS A LIST (opensoft/openRepoTools#86): every child
    # `duplicate_holder_pids` verified, space-separated, where it used to name
    # only the first. The target is compared with EACH pid in it, by equality,
    # never by a substring of the list, so `--retire=<two pids>` names none.
    while IFS=$'\037' read -r dr_parent dr_children dr_uuid; do
      [ -n "${dr_parent:-}" ] || continue
      dr_hit=0
      if [ "$dr_parent" = "$retire_target" ]; then dr_hit=1; fi
      for dr_c in $dr_children; do
        if [ "$dr_c" = "$retire_target" ]; then dr_hit=1; fi
      done
      if [ "$dr_hit" = 1 ]; then
        dh_parent="$dr_parent"; dh_children="$dr_children"; dh_uuid="$dr_uuid"
        break
      fi
    done <<DUPES
$dh_rows
DUPES
  fi

  if [ -n "$dh_parent" ]; then
    # THE WINDOW'S OWN LIVE SESSION IS NEVER RETIRED THIS WAY (Amendment
    # 18(h): "the window's own session is the one it refuses to retire") —
    # reusing the SAME `live-holder` read already taken once above, never a
    # second shell-out and never a second liveness primitive for one
    # invocation. Its own case above already dies on anything but 0 or 8, so
    # by the time control reaches here `lh_rc` can only be one of those two:
    # `duplicate-holder`'s own exclusion already keeps a live `dh_parent` from
    # ever equalling `lh_pid`, so this is the residual, cheap check for the
    # child slot alone (a `pgrep -P` result coinciding with the lane's own
    # pid would be bizarre, but the fence costs nothing to keep). Every child
    # in the slot is asked, not only the first (opensoft/openRepoTools#86).
    if [ -n "$lh_pid" ]; then
      for dh_c in $dh_parent $dh_children; do
        if [ "$lh_pid" = "$dh_c" ]; then
          die "pid $lh_pid is lane $LANE's own live session (session $lh_sid, ${lh_verdict:-here}) — Amendment 18(h) refuses to retire the one process a lane's binding actually is. Retire the OTHER pid of the pair ($prog $LANE --retire <pid>), or end the lane itself." 2
        fi
      done
    fi

    # AMENDMENT 18(h) — A DUPLICATE HOLDER IS TERMED, NOT ONLY NAMED. Unlike
    # decision 8(e)'s title-based fork above (never killed: Amendment 8(f), A11
    # clause (k) rule (e) — a fork MIGHT be somebody's long-running job with a
    # prompt to answer), a live process sharing THIS LANE'S OWN transcript id
    # is exactly what Amendment 18(h) says may never coexist with the lane's
    # own session: "the tooling refuses to build on either... it never
    # appends such an id to the row and never launches a second resume of
    # it." Retiring the register's own binding does not end a duplicate that
    # is still holding the transcript file open, so THIS act ends the
    # duplicate itself — never a process on another transcript (the read is
    # scoped to this lane's own ids), and never the pid just proved above to
    # be the lane's own.
    if (( dry_run )); then
      dh_c_said=""
      for dh_c in $dh_children; do dh_c_said="$dh_c_said and pid $dh_c (child)"; done
      note "--dry-run: would send TERM to pid $dh_parent (bg-pty-host)$dh_c_said — lane $LANE's duplicate holder of transcript $dh_uuid. Nothing sent."
      exit 0
    fi
    # RE-VALIDATED IMMEDIATELY BEFORE EACH SIGNAL, not trusted from the read
    # a few lines above (Copilot round 3, PR #61): that read and this signal
    # are two different moments, and a pid can be reaped and reused by an
    # unrelated process in the gap between them. A pid whose CURRENT argv no
    # longer shows `--fork-session --resume …<this transcript>.jsonl` is not
    # signalled — it is treated exactly as "already gone", which is the
    # honest fact whether it truly exited or the number now belongs to
    # something else this act must never touch.
    #
    # AN EXACT PATH COMPONENT, NOT AN ARBITRARY SUBSTRING (Copilot round 4, PR
    # #61, the same fix as `duplicate_holder_pids`'s own discovery match):
    # `*"$uuid.jsonl"*` also matches `other-<uuid>.jsonl` and
    # `<uuid>.jsonl.bak`, neither of which is THIS transcript's basename.
    # `/<uuid>.jsonl` must be preceded by a path separator and followed by
    # the end of the argument or a space.
    # THE ACTUAL `--resume` VALUE, NOT A GLOB THAT CAN SPAN OTHER ARGUMENTS
    # (Copilot round 5, PR #61): `*"--resume"*"/$uuid.jsonl"*` matches
    # `--resume /other.jsonl --label /<uuid>.jsonl` too, since `*` between
    # them freely crosses argument boundaries. The value right after
    # `--resume ` is what the harness actually resumed, and that is the only
    # thing compared.
    #
    # READ WHOLE, SPACES AND ALL (opensoft/openRepoTools#76). `ps -o args=`
    # joins the argv with spaces and quotes nothing, so the cut this used to
    # make at the first space (`${v%% *}`) turned `--resume /home/Jane
    # Doe/…/<uuid>.jsonl` into `/home/jane`, read a still-matching duplicate
    # as "argv no longer matched", and withheld its TERM. The value now ends
    # at the first `.jsonl` that ends an argument (followed by a space or by
    # the end of the line), which keeps round 5's case above reading
    # `/other.jsonl`, and a value with a word opening `--` inside it is
    # refused as a later option rather than a directory. A value with a
    # space in it must be one absolute path, starting with `/` and holding no
    # ` /`, or it is refused too (Brett Heap's ruling of 2026-10-18 on PR
    # #205): a bare session id followed by a prompt naming this transcript, or
    # a `<uuid>.jsonl.bak` followed by a word that reaches it through ` /`,
    # would otherwise be read as one value ending in `/<uuid>.jsonl` and TERMed
    # here. Not every `<uuid>.jsonl.bak` form is refused: one followed by a
    # later word that does not begin with `/` still reads as that value. This
    # is `lanes-edit.sh`'s `dhp_resume_value`, the rule discovery already
    # applied to this pid, word for word, and its comment there gives that
    # limit and the others of reading a flattened argv at all; the two must
    # stay the same rule. It sets `dh_rv` in this shell rather than printing
    # it.
    dh_resume_value() {   # <flattened argv, lowercased> — sets dh_rv
      dh_rv=""
      case "${1-}" in *"--resume "*) : ;; *) return 0 ;; esac
      dh_rv_rest="${1#*--resume }"
      while :; do
        case "$dh_rv_rest" in *.jsonl*) : ;; *) dh_rv=""; return 0 ;; esac
        dh_rv="${dh_rv}${dh_rv_rest%%.jsonl*}.jsonl"
        dh_rv_rest="${dh_rv_rest#*.jsonl}"
        case "$dh_rv_rest" in "" | " "*) break ;; esac
      done
      case "$dh_rv" in *" --"*) dh_rv="" ;; esac
      case "$dh_rv" in *" "*)
        case "$dh_rv" in /*) : ;; *) dh_rv="" ;; esac
        case "$dh_rv" in *" /"*) dh_rv="" ;; esac
      esac
      return 0
    }
    dh_uuid_lc="$(printf '%s' "$dh_uuid" | tr 'A-F' 'a-f')"
    dh_p_match=0
    dh_p_args="$(ps -o args= -p "$dh_parent" 2>/dev/null | tr 'A-F' 'a-f')" || dh_p_args=""
    # `--fork-session ` WITH ITS OWN TRAILING SPACE (Copilot round 11, PR
    # #61): a bare substring also matches `--fork-session-helper` or any
    # other flag that merely STARTS WITH this text — never the process this
    # recheck means. The real invocation always has a boundary (a space,
    # then `--resume`) right after this flag, so requiring it here rules the
    # false one out the same way `--resume ` already rules out
    # `--resume-file` beside it.
    case "$dh_p_args" in *"--fork-session "*"--resume "*)
      dh_resume_value "$dh_p_args"
      case "$dh_rv" in */"$dh_uuid_lc.jsonl") dh_p_match=1 ;; esac
    esac
    # THE SIGNAL'S OWN EXIT STATUS, KEPT APART FROM "DID ARGV MATCH" (Copilot
    # round 11, PR #61): folding both into one `dh_p_sent` flag and
    # swallowing `kill`'s status (`|| :`) made a `kill -TERM` that failed
    # outright — wrong owner, or the pid reaped in the gap since the recheck
    # above — indistinguishable from one truly delivered. The message below
    # must be able to say "nothing was sent" (retrying TERM cannot help;
    # look at who owns the pid) apart from "TERM was sent and ignored" (a
    # KILL, or more time, is the next step) — that needs both bits kept.
    dh_p_killed=0
    if [ "$dh_p_match" = 1 ]; then
      kill -TERM "$dh_parent" 2>/dev/null && dh_p_killed=1
    fi
    # EVERY CHILD IN THE SLOT, NOT ONLY THE FIRST (opensoft/openRepoTools#86):
    # each is rechecked and signalled on its own, and its two bits are kept
    # beside its pid as `<pid>:<match><killed>` for the report below, so one
    # child's verdict is never read as another's.
    dh_c_state=""
    for dh_c in $dh_children; do
      dh_c_match=0
      dh_c_killed=0
      dh_c_args="$(ps -o args= -p "$dh_c" 2>/dev/null | tr 'A-F' 'a-f')" || dh_c_args=""
      case "$dh_c_args" in *"--fork-session "*"--resume "*)
        dh_resume_value "$dh_c_args"
        case "$dh_rv" in */"$dh_uuid_lc.jsonl") dh_c_match=1 ;; esac
      esac
      if [ "$dh_c_match" = 1 ]; then
        kill -TERM "$dh_c" 2>/dev/null && dh_c_killed=1
      fi
      dh_c_state="$dh_c_state $dh_c:$dh_c_match$dh_c_killed"
    done
    # VERIFIED GONE, NOT ASSUMED FROM `kill`'S OWN STATUS (Copilot round 4, PR
    # #61): that status conflates "already gone" (fine — the goal was never
    # the signal, it was the duplicate not existing) with "no permission"
    # (a real failure), and a caller that always reads exit 0 here cannot
    # tell a genuine retirement from a duplicate still running.
    #
    # `kill -0`'S OWN FAILURE HAS THE SAME TWO ANSWERS CONFLATED, ONE LEVEL
    # DOWN (Copilot round 13, PR #61): ESRCH (truly gone) and EPERM (still
    # there, just not ours to signal) both read as "gone" here, so a TERM
    # this process had no permission to deliver in the first place could be
    # reported RETIRED over a duplicate still running. `kill -0` STAYS the
    # primary check — it is a raw syscall and answers instantly — and `ps -p`
    # is asked only where `kill -0` already says gone, to catch the one case
    # that disagrees: a pid EPERM hid from the syscall but a process listing
    # can still see.
    #
    # `command -p`, NOT A BARE `ps` (measured, not merely reasoned to): this
    # suite fakes `ps` and `pgrep` for its WHOLE run (`$SANDBOX/fakebin`,
    # prepended to PATH) so `duplicate_holder_pids`' DISCOVERY never touches
    # the real process table — but that fake is a STATIC table, built once
    # from `$FAKE_PS_RECORDS` at fixture setup and never told about a `kill`
    # this act sends minutes later, so a bare `ps -p` here would ask the very
    # same fake DISCOVERY consulted and get the pre-kill answer back forever,
    # no matter how long this retries. Measured directly: this fallback,
    # written as a bare `ps -p`, reported the DUP/RACE fixtures "STILL ALIVE"
    # after a genuine, verified `kill -TERM`, on a clean CI runner with no
    # other process anywhere near that pid — the fake, not scheduling, not
    # load. `command -p ps` is POSIX's own way to ask for the REAL utility on
    # a default PATH regardless of what the caller's own PATH resolves to,
    # exactly the bypass this verification (of a REAL fixture process this
    # suite itself spawned) needs; `kill -0` needs no such bypass because
    # nothing here fakes `kill`.
    #
    # A BOUNDED RETRY, NOT ONE FIXED PAUSE: a single 0.1s settle is long
    # enough for every genuinely-gone case under ordinary load, but SIGTERM
    # delivery and reaping both wait on the scheduler under real contention
    # from a sibling lane's own concurrent run on a shared workstation.
    # `dh_settle_gone` polls up to two seconds (twenty tries, 0.1s apart) and
    # returns the instant both checks agree the pid is gone, so the common,
    # unloaded case still resolves in one pass.
    #
    # `ps` IN A SUBSHELL OF ITS OWN (opensoft/openRepoTools#292): macOS's bash
    # 3.2 ends a `set -e` script where a utility run through `command` fails,
    # even after `!` in an `if`, and `ps -p` fails exactly when the pid is
    # gone, so this act ended at the moment it had its answer. The subshell's
    # status is ps's either way, and `command -p` keeps its bypass.
    dh_settle_gone() {   # <pid> — 0 once confirmed gone, 1 if still found
      dsg_tries=0
      while [ "$dsg_tries" -lt 20 ]; do
        if ! kill -0 "$1" 2>/dev/null && ! (command -p ps -p "$1" >/dev/null 2>&1); then
          return 0
        fi
        dsg_tries=$((dsg_tries + 1))
        sleep 0.1
      done
      return 1
    }
    dh_p_gone=1; dh_settle_gone "$dh_parent" || dh_p_gone=0
    dh_p_msg="gone"
    [ "$dh_p_gone" = 1 ] || dh_p_msg="STILL ALIVE, TERM withheld: argv no longer matched"
    [ "$dh_p_gone" = 1 ] || [ "$dh_p_match" = 0 ] || [ "$dh_p_killed" = 1 ] || dh_p_msg="STILL ALIVE, TERM NOT SENT: the signal itself failed (check ownership/permissions) — this is not the process ignoring TERM, it never received one"
    [ "$dh_p_gone" = 1 ] || [ "$dh_p_match" = 0 ] || [ "$dh_p_killed" = 0 ] || dh_p_msg="STILL ALIVE despite TERM delivered"
    note "pid $dh_parent (bg-pty-host): $dh_p_msg"
    # EVERY CHILD MUST BE GONE FOR THIS TO SAY RETIRED (opensoft/openRepoTools#86):
    # one child still alive, whichever it is, makes this a partial retirement
    # and the refusal below, never a RETIRED over a process still holding the
    # transcript.
    dh_c_all_gone=1
    for dh_c_tok in $dh_c_state; do
      dh_c="${dh_c_tok%%:*}"; dh_c_bits="${dh_c_tok#*:}"
      dh_c_match="${dh_c_bits%?}"; dh_c_killed="${dh_c_bits#?}"
      dh_c_gone=1; dh_settle_gone "$dh_c" || dh_c_gone=0
      [ "$dh_c_gone" = 1 ] || dh_c_all_gone=0
      dh_c_msg="gone"
      [ "$dh_c_gone" = 1 ] || dh_c_msg="STILL ALIVE, TERM withheld: argv no longer matched"
      [ "$dh_c_gone" = 1 ] || [ "$dh_c_match" = 0 ] || [ "$dh_c_killed" = 1 ] || dh_c_msg="STILL ALIVE, TERM NOT SENT: the signal itself failed (check ownership/permissions) — this is not the process ignoring TERM, it never received one"
      [ "$dh_c_gone" = 1 ] || [ "$dh_c_match" = 0 ] || [ "$dh_c_killed" = 0 ] || dh_c_msg="STILL ALIVE despite TERM delivered"
      note "pid $dh_c (child): $dh_c_msg"
    done
    if [ "$dh_p_gone" = 1 ] && [ "$dh_c_all_gone" = 1 ]; then
      step "RETIRED lane $LANE's duplicate holder of transcript $dh_uuid (Amendment 18(h))"
      note "The lane itself is untouched: its row, and its own live session if it has one, are unaffected."
      exit 0
    fi
    note "The lane itself is untouched: its row, and its own live session if it has one, are unaffected."
    die "lane $LANE's duplicate holder of transcript $dh_uuid was only partly retired — see the pid lines above. Whatever is still alive did not accept TERM (permission, a handler that ignores it, or argv changed before this act reached it); re-run $prog $LANE --retire <pid> once you have looked, or end it by hand." 1
  fi

  # ---------------- A DUPLICATE OF THE ROW'S OWN ID, BY SESSION RECORD
  # (opensoft/openRepoTools#39, Amendment 12/#52) — A DIFFERENT READING OF THE
  # SAME RULE, FROM A DIFFERENT SURFACE, NOT A REPLACEMENT FOR THE ONE ABOVE.
  # `duplicate-holder` above finds a `bg-pty-host` wrapper in the PROCESS
  # TABLE by its `--fork-session --resume <uuid>.jsonl` argv and TERMs it on
  # sight, because nothing is answering a prompt in it. `transcript-holders`
  # instead finds every LIVE SESSION RECORD carrying the row's own id,
  # wherever the harness keeps one — which also catches an ordinary
  # INTERACTIVE second resume the process-table match was never built to see
  # — and only REPORTS it, the same way a title-based fork is only reported,
  # because a second interactive resume may have a prompt open. Both reads
  # run (each on its own evidence, neither replacing the other); whichever
  # finds `$retire_target` first answers, and this one is reached only when
  # the process-table read above did not already match it (`dh_parent` empty).
  #
  # THE SECOND POPULATION, AND THE ONE THE FIRST BUILD OF THIS ACT COULD NOT
  # SEE. Amendment 6(d) allows exactly one live conversation per lane and
  # Amendment 18(h) sharpens it to one live PROCESS per transcript; `forks`
  # answers about ids the row does NOT record, and this answers about the id it
  # DOES. Measured 2026-09-14T12:05–12:30Z on lane openRepoTools-3: a harness
  # `--fork-session` in a background pty host, whose record sat in ANOTHER
  # profile's `sessions/` directory, beside the interactive resume of the same
  # transcript in a tmux window — three live processes on one file at the peak.
  #
  # THE WINDOW'S OWN SESSION IS THE ONE IT REFUSES TO RETIRE, and it is read out
  # of the same rows rather than out of a second helper and a second fetch: the
  # record that is an interactive session in a window tmux still resolves — by
  # `<@id>` AND session name, Amendment 11(h)'s agreement rule, which
  # `transcript-holders` owns — is the lane's session. Every OTHER live process
  # carrying that id is the duplicate.
  dup_pid=""; dup_kind=""; dup_where=""; dup_prof=""
  dup_own=""; dup_rows=""; dup_id=""
  dup_comp=""; dup_comp_kind=""; dup_comp_prof=""
  # `dup_other` IS NOT `dup_rows`'S OWN EMPTINESS (Copilot round 8, PR #61
  # follow-up, caught by this suite rather than by review): `lane-end` is a
  # plain script, never itself a bound claude session, so `transcript_holders`
  # never sees a "here" record and every row it returns — INCLUDING the
  # lane's own perfectly ordinary live session — verdicts `unrelated` rather
  # than `companion` or `duplicate`. Reading raw `dup_rows` as "something is
  # live here" made an entirely healthy lane (one live session, nothing else)
  # answer the SAME as a lane with a genuine second process: `--retire` on a
  # pid that is neither refused with 8 ("nothing of any kind is live") but
  # with 2 ("neither ... nor a live duplicate"), naming a "duplicate" that was
  # only ever the lane's own session all along. `$lh_pid` — proved a few lines
  # above by `live-holder`, the one liveness implementation this file has, and
  # already known before this read ever runs — is the fence: a row that IS it
  # is accounted for and not "other"; a row that is not it genuinely is.
  dup_other=""
  if [ -z "$fk_sid" ] && [ -z "$dh_parent" ]; then
    du_rc=0
    dup_id="$(LANES_NO_FETCH=1 "$LANES_EDIT" last-session "$LANE" 2>/dev/null)" || du_rc=$?
    case "$du_rc" in
      0 | 8) : ;;
      *) die "$LANES_EDIT last-session $LANE exited $du_rc, so the id the row records could not be read and whether '$retire_target' is a DUPLICATE of it is not known. That is not 'it is not one', and this script will not name an act against a process it could not prove is one." 1 ;;
    esac
    if [ "$du_rc" = 0 ] && [ -n "$dup_id" ]; then
      # `thh_rc` AND NOT `dh_rc`: this is `transcript-holders`' OWN exit
      # status, a different read from `duplicate-holder`'s above, and reusing
      # that name here would step on a value `dh_rc` no longer needs by this
      # point but a later reader of this block could mistake for it.
      thh_rc=0
      dup_rows="$("$LANES_EDIT" transcript-holders "$dup_id" 2>/dev/null)" || thh_rc=$?
      case "$thh_rc" in
        0 | 8) : ;;
        2) note "this \`lanes-edit.sh\` has no \`transcript-holders\` (exit 2), so a duplicate of lane $LANE's own transcript cannot be looked for here. Install the one beside this script (\`openRepoTools --install\`)." ;;
        *) die "$LANES_EDIT transcript-holders $dup_id exited $thh_rc, so whether '$retire_target' is a live duplicate of lane $LANE's own transcript could NOT be read — which is not 'it is not one'." 1 ;;
      esac
      if [ "$thh_rc" = 0 ]; then
        # WHICH PROFILES HAVE AN INTERACTIVE HOLDER, FIRST (Copilot round 10,
        # PR #61) — a `kind: bg` row is Amendment 8 ruling (g)'s companion
        # ONLY beside an interactive record IN THE SAME PROFILE; a `kind: bg`
        # row in a profile with no interactive row of its own is not anyone's
        # proven companion and stays a candidate "other" (round 9's own fix
        # read EVERY `kind: bg` row as a companion, which also excluded a
        # genuine duplicate in a DIFFERENT profile — exactly `transcript_
        # holders`' own `duplicate` case, "any record at all in ANOTHER
        # profile's sessions/", which this PR's own repoGD fixture already
        # exercises). Built in its own pass because a row's verdict alone
        # cannot answer this from OUTSIDE any bound window wherever no ONE
        # live window answers for the transcript. `transcript_holders` now
        # reads the companion off the LANE's live window record from any
        # window (openRepoTools#73, item 2) — but where tmux is silent, the
        # session is in no window, or two live windows in two profiles carry
        # it, every row still reads `unrelated`, `companion` included, and
        # `duplicate_holder_pids` applies this very rule, so it stays.
        dup_int_profs=""
        while IFS=$'\037' read -r ip_pid ip_kind ip_tgt ip_prof ip_where ip_verdict ip_file; do
          [ -n "${ip_pid:-}" ] || continue
          [ "$ip_kind" = bg ] && continue
          case "$dup_int_profs" in *" $ip_prof "*) : ;; *) dup_int_profs="${dup_int_profs} $ip_prof " ;; esac
        done <<<"$dup_rows"
        while IFS=$'\037' read -r dr_pid dr_kind dr_tgt dr_prof dr_where dr_verdict dr_file; do
          [ -n "${dr_pid:-}" ] || continue
          # A WINDOW THAT COULD NOT BE ASKED ABOUT IS UNKNOWN AND NOT DEAD
          # (Amendment 18(b)): `(not asked)` — no tmux here, or a record with
          # no `<@id>` in it — PROTECTS the process, and so does `(no
          # answer)` — tmux installed and its server silent, which used to
          # come back `(gone)` and let `--retire <the lane's own pid>` print
          # 6(d)'s retitle for the conversation that IS the lane
          # (openRepoTools#73, item 1). Only a `(gone)` this server actually
          # refuted makes it retirable.
          case "$dr_where" in
            "window "*"(gone)") dr_own=0 ;;
            "window "*) dr_own=1 ;;
            *) dr_own=0 ;;
          esac
          if [ "$dr_own" = 1 ] && [ "$dr_kind" != bg ]; then
            dup_own="${dup_own:+$dup_own }$dr_pid"
          fi
          dr_is_comp=0
          if [ "$dr_verdict" = companion ]; then
            dr_is_comp=1
          elif [ "$dr_kind" = bg ]; then
            case "$dup_int_profs" in *" $dr_prof "*) dr_is_comp=1 ;; esac
          fi
          # NEITHER lane $LANE's OWN KNOWN LIVE PID (proved above by
          # `live-holder`, Copilot round 8 follow-up: that pid is accounted
          # for whatever `transcript_holders` verdicts it here, `unrelated`
          # included) NOR ITS HARNESS COMPANION (Copilot round 9, refined
          # round 10: the companion test above, not `dr_kind = bg` alone —
          # so a stranger pid against a lane with nothing wrong keeps
          # answering 8, never a false "a duplicate exists" 2, while a
          # `kind: bg` row in a DIFFERENT profile with no interactive row
          # of its own still counts as one).
          if { [ -z "$lh_pid" ] || [ "$dr_pid" != "$lh_pid" ]; } && [ "$dr_is_comp" = 0 ]; then
            dup_other=1
          fi
          if [ "$dr_pid" = "$retire_target" ]; then
            # THE HARNESS'S OWN COMPANION IS NOT A DUPLICATE AND MAY NOT BE
            # NAMED AS ONE. `transcript_holders` marks it `companion` — the
            # `kind: bg` record beside THIS window's own record of this id,
            # Amendment 8 ruling (g)'s *"records that share a `sessionId` are
            # one session, not a queue of rival holders"* — and the guard and
            # `lane-start` both pass it over in silence. Selecting it here would
            # have this act print Amendment 6(d)'s ending for one half of a live
            # session that IS the lane (Copilot round 2 on this PR). It is held
            # separately so the refusal below can say WHICH thing the pid is,
            # rather than falling through to "neither" and naming nothing.
            #
            # `$dr_is_comp` (Copilot rounds 9 AND 10, PR #61), NOT `dr_verdict`
            # ALONE: the verdict is `companion` when `lane-end` runs from
            # WITHIN the lane's own bound window, or — since openRepoTools#73 —
            # from anywhere ONE live window answers for the transcript; where
            # none does (tmux silent, no window, two live windows in two
            # profiles) every row here verdicts `unrelated` instead, `companion`
            # included, and round 9's first fix read `dr_kind = bg` alone as
            # the fallback signal — which ALSO excluded a genuine `kind: bg`
            # duplicate in a DIFFERENT profile (`transcript_holders`' own
            # `duplicate` case), since a companion is proven only BESIDE an
            # interactive holder of this id IN THE SAME PROFILE, never by
            # kind alone. `$dr_is_comp`, computed above against every
            # interactive row's own profile, is the one test both routings
            # (this one and `dup_other`'s) now share.
            if [ "$dr_is_comp" = 1 ]; then
              dup_comp="$dr_pid"; dup_comp_kind="${dr_kind:-interactive}"
              dup_comp_prof="$dr_prof"
            else
              dup_pid="$dr_pid"; dup_kind="${dr_kind:-interactive}"
              dup_where="$dr_where"; dup_prof="$dr_prof"
            fi
          fi
        done <<<"$dup_rows"
      fi
    fi
  fi

  # THE COMPANION IS REFUSED BY NAME TOO, and before the rest, because it is
  # the one pid a person can reach for here that is neither a duplicate nor the
  # lane's own session: it is the OTHER HALF of the session that is. Ending it
  # is not Amendment 6(d)'s act on anything — 6(d) ends an idle background
  # session still holding a lane NAME, and this one holds the live session's own
  # id, which ruling (g) reads as one session and not as a rival.
  if [ -z "$fk_sid" ] && [ -z "$dh_parent" ] && [ -z "$dup_pid" ] && [ -n "$dup_comp" ]; then
    die "pid $dup_comp is the HARNESS'S OWN COMPANION of lane $LANE's live session (kind ${dup_comp_kind:-bg}; profile ${dup_comp_prof:-unknown}; the same transcript ${dup_id:-unknown} and the same profile as this window's own record of it). Amendment 8 ruling (g): \"records that share a \`sessionId\` are one session, not a queue of rival holders, and there is no contest between them to win\" — so it is not a duplicate of anything, and ending it would end one half of the session that IS this lane. Nothing is named. To retire a real duplicate pass ITS pid (\`$LANES_EDIT transcript-holders $dup_id\` lists every live process carrying this transcript and says which is which); to end the lane, run: $prog $LANE" 2
  fi

  # THE WINDOW'S OWN SESSION IS REFUSED BY NAME, and the refusal names the act
  # that DOES end a lane, because an operator who typed this meant one of the
  # two and there is no third thing to mean.
  # AND WHERE TMUX DID NOT ANSWER, IT SAYS THAT INSTEAD (openRepoTools#73,
  # item 1): `(no answer)` protects the process because its window could not be
  # read at all, which is not the same as having read it and found the lane, so
  # the refusal names the silence rather than claiming what it could not see.
  if [ -z "$fk_sid" ] && [ -z "$dh_parent" ] && [ -n "$dup_pid" ]; then
    case " $dup_own " in
      *" $dup_pid "*)
        case "$dup_where" in
          *"(no answer)")
            die "pid $dup_pid carries lane $LANE's own transcript in $dup_where, and tmux did not answer at all — its server is down, or unreachable from here — so whether that window is still there could not be read. A window nobody could ask about is NOT a dead one (Amendment 18(b)), and the process in it may be the session that IS the lane, so nothing is named. Re-run this where tmux answers; to end the lane, run: $prog $LANE" 2 ;;
        esac
        die "pid $dup_pid IS lane $LANE's own live session, in its own window ($dup_where) — not a duplicate of it. Retiring the session that IS the lane is how a lane loses the conversation it is, so nothing is named. To end the lane, run: $prog $LANE   ·   to retire a duplicate, pass ITS pid (\`$LANES_EDIT transcript-holders $dup_id\` lists every live process carrying this transcript)." 2 ;;
    esac
  fi

  if [ -z "$fk_sid" ] && [ -z "$dh_parent" ] && [ -n "$dup_pid" ]; then
    fk_today="$(date -u +%Y-%m-%d)"
    step "pid $dup_pid IS a live DUPLICATE of lane $LANE's own transcript $dup_id — proved, and NOTHING has been written or stopped"
    note "  $dup_where  kind $dup_kind  profile $dup_prof"
    note "A transcript is held by ONE live process (Amendment 18(h), ratified 2026-09-14): two of them append to one file, each overwriting what the other wrote. The lane's own session is${dup_own:+ pid $dup_own} in its window."
    case "$dup_kind" in
      bg)
        note "Amendment 6(d)'s retire act for a BACKGROUND holder: \"an idle background session still holding a lane name is ended\". It has no prompt to type into, so the retitle below is not available to it."
        note "Ending it is YOUR act and no tool's (Amendment 8(f) refuses it by name, and A11 clause (k) rule (e) repeats the refusal for this surface): the pid above is the process." ;;
      *)
        note "Amendment 6(d)'s retire act, typed IN THAT SESSION — there is no API to rename one from outside it:"
        note "    /rename $LANE · retired $fk_today" ;;
    esac
    note "Until it is retired, every surface goes on naming it — which is true, because it is still a second live writer of this lane's transcript until one of the two acts above is taken."
    note "The lane itself is untouched: this ended nothing, wrote nothing and killed nothing."
    if (( dry_run )); then
      note "--dry-run changes nothing here: this act writes nothing on any path, so the run above IS the whole of it."
    fi
    exit 0
  fi

  if [ -z "$fk_sid" ] && [ -z "$dh_parent" ]; then
    if [ -n "$fk_rows" ]; then
      note "the live forks of lane $LANE's transcript are:"
      printf '%s\n' "$fk_rows" | while IFS="$(printf '\t')" read -r fr_sid fr_pid fr_kind fr_cwd; do
        [ -n "${fr_sid:-}" ] || continue
        note "  $fr_sid  pid $fr_pid  ${fr_kind:-interactive}  $fr_cwd"
      done
    else
      note "no fork of lane $LANE's transcript is live on this workstation. A fork is found by its transcript's TITLE under an id the row does not record (ratified decision 8(e)); \`$LANES_EDIT forks $LANE\` is the read that names them."
    fi
    if [ -n "${dh_rows:-}" ]; then
      note "the live duplicate holders of lane $LANE's transcript, by PROCESS TABLE, are:"
      printf '%s\n' "$dh_rows" | while IFS=$'\037' read -r dr_parent dr_children dr_uuid; do
        [ -n "${dr_parent:-}" ] || continue
        # Every child in the slot, each named on its own (#86).
        dr_c_said=""
        for dr_c in $dr_children; do dr_c_said="$dr_c_said, pid $dr_c (child)"; done
        note "  pid $dr_parent (bg-pty-host)$dr_c_said  transcript $dr_uuid"
      done
    else
      note "no live duplicate holder of lane $LANE's transcript is in the process table either (Amendment 18(h), opensoft/openRepoTools#39); \`$LANES_EDIT duplicate-holder $LANE\` is the read that names them."
    fi
    if [ -n "$dup_rows" ]; then
      note "and the live processes carrying lane $LANE's OWN transcript ${dup_id:-(none recorded)}, by SESSION RECORD, are:"
      printf '%s\n' "$dup_rows" | while IFS=$'\037' read -r dr_pid dr_kind dr_tgt dr_prof dr_where dr_verdict dr_file; do
        [ -n "${dr_pid:-}" ] || continue
        note "  pid $dr_pid  ${dr_kind:-interactive}  $dr_where  profile $dr_prof"
      done
    else
      note "and no second live session record carries lane $LANE's own transcript ${dup_id:-(none recorded)} either (Amendment 18(h))."
    fi
    # 8 IS KEPT FOR "THERE IS NOTHING OF ANY KIND", which is the code this act
    # has answered since it was written and the one a caller reads as "no
    # fork is live". The 2 is for a target that is not one of the things that
    # ARE there — a stranger's pid, another lane's uuid — and the two stayed
    # one code until issue #39 gave the act a second population, now a third,
    # to look in: `forks` (title, never killed), `duplicate-holder` (process
    # table, TERMed), `transcript-holders` (session record, never killed) —
    # three reads answering one rule (Amendment 18(h): one live process per
    # transcript), kept as three because each is built on a different kind of
    # evidence and this workstation is not the only place any one of them can
    # fail to see what another does.
    if [ -z "$fk_rows" ] && [ -z "${dh_rows:-}" ] && [ -z "$dup_other" ]; then
      die "nothing of any kind is live for lane $LANE on this workstation, so there is nothing to retire: no FORK of its transcript (an id the row does not record, ratified decision 8(e)), no live DUPLICATE HOLDER of the id it does in the process table (Amendment 18(h), opensoft/openRepoTools#39), and no live DUPLICATE of it by session record either (Amendment 18(h))." 8
    fi
    die "'$retire_target' is neither the session id nor the pid of a live fork of lane $LANE, nor the pid (parent or child) of a live duplicate holder of its own transcript in the process table, nor the pid of a live duplicate of it by session record. Naming an act against something that is none of this lane's is how the wrong conversation gets retitled and the wrong process gets stopped, so nothing is named. Pick one of the ids or pids above." 2
  fi

  # AMENDMENT 6(d), FILLED IN — and it is TWO acts because 6(d) is two, one per
  # kind of session: *"When a lineage is superseded, handed off or dead, it is
  # retitled `<lane> · retired <YYYY-MM-DD>`, and an idle background session
  # still holding a lane name is ENDED."* A fork with a prompt can be retitled
  # from inside it and there is no API to do it from out here (clause (f) step
  # 7's own reason); a `kind: bg` fork has no prompt at all, which is exactly
  # what Evidence 6's `--fork-session` daemon was, so the retitle is not
  # available there and 6(d)'s second half is the one that applies.
  fk_today="$(date -u +%Y-%m-%d)"
  step "$fk_sid IS a live fork of lane $LANE's transcript — proved, and NOTHING has been written or stopped"
  note "  pid ${fk_pid:-unknown}  ${fk_kind}${fk_cwd:+  cwd $fk_cwd}"
  case "$fk_kind" in
    bg)
      note "Amendment 6(d)'s retire act for a BACKGROUND holder: \"an idle background session still holding a lane name is ended\". It has no prompt to type into, so the retitle below is not available to it."
      note "Ending it is YOUR act and no tool's (Amendment 8(f) refuses it by name, and clause (k) rule (e) repeats the refusal for this surface): the pid above is the process." ;;
    *)
      note "Amendment 6(d)'s retire act, typed IN THAT SESSION — there is no API to rename one from outside it:"
      note "    /rename $LANE · retired $fk_today" ;;
  esac
  note "Until it is retired, \`$LANES_EDIT forks $LANE\` goes on naming it and every surface goes on marking the lane — which is true, because it is still a fork of this lane until one of the two acts above is taken."
  note "The lane itself is untouched: this ended nothing, wrote nothing and killed nothing."
  if (( dry_run )); then
    note "--dry-run changes nothing here: this act writes nothing on any path, so the run above IS the whole of it."
  fi
  exit 0
fi

# ---------------------------------------------------------------- the row

# AMENDMENT 15 — `tolower` ON BOTH SIDES, exactly as `lane-start`'s function of
# this name now compares and as `row_line` compares one file over. The resolver
# above has usually already made `$LANE` the row's own spelling, and this is
# what happens when it could NOT: a helper predating `canon-lane` spends 2 on
# the unknown subcommand, and a register holding 15(d)'s pair spends 2 as well.
# Byte for byte, `lane-end openxfactory-2` on the lane `openXfactory-2` then
# reads as NO ROW — and the refusal's own remedy is `add-row`, which is the
# 23:51:56Z incident performed by the other end of the lane. (`add-row` refuses
# it, so the register is safe either way; the message was the defect.)
rows_for_lane() {
  awk -v lane="$1" '
    substr($0,1,1) == "|" {
      p1 = index($0, "`"); if (p1 == 0) next
      rest = substr($0, p1 + 1); p2 = index(rest, "`"); if (p2 == 0) next
      if (tolower(substr(rest, 1, p2 - 1)) == tolower(lane)) print
    }' "$LANES_FILE"
}
# The SPELLINGS those rows carry, for a refusal that can name them — two rows
# for one lane may now be two spellings of one name, and a person sent looking
# for a duplicate that is not there to find loses the time twice.
row_names_for_lane() {
  awk -v lane="$1" '
    substr($0,1,1) == "|" {
      p1 = index($0, "`"); if (p1 == 0) next
      rest = substr($0, p1 + 1); p2 = index(rest, "`"); if (p2 == 0) next
      t = substr(rest, 1, p2 - 1)
      if (tolower(t) == tolower(lane)) print t
    }' "$LANES_FILE"
}

row="$(rows_for_lane "$LANE" || :)"
row_count=0
[ -n "$row" ] && row_count="$(printf '%s\n' "$row" | grep -c . || :)"

case "$row_count" in
  0) die "the register has no row for lane $LANE — nothing to end. If the lane predates the helpers, add its row first: lanes-edit.sh add-row \"| \`$LANE\` | … |\"" 2 ;;
  1) : ;;
  *) rfl_names="$(row_names_for_lane "$LANE" | tr '\n' ' ')"
     rfl_distinct="$(row_names_for_lane "$LANE" | LC_ALL=C sort -u | grep -c . || :)"
     rfl_why="and lanes-edit.sh can edit neither. Merge them into one row by hand in $LANES_FILE, then re-run"
     if [ "$rfl_distinct" -gt 1 ]; then
       # BRACED: bash decides where a NAME ends with a locale-dependent
       # `isalnum()`, and bash 3.2 on Darwin reads the em dash's three bytes as
       # part of it (A9 Addendum 4, R-A9-11).
       rfl_why="spelled ${rfl_names}— a lane name is ONE name under any case (Amendment 15), so those are two rows for ONE lane, and lanes-edit.sh can edit neither. Merge them into one row by hand in $LANES_FILE — Amendment 15(d): append the newer row's session id(s) to the older row's session cell, in order, and remove the newer row in the SAME commit, whose message names both spellings — then re-run"
     fi
     die "the register has $row_count rows for lane $LANE, $rfl_why" 2 ;;
esac

# THE SEAM, after the row's own refusals and before the state cell, the in-flight
# guard and every write (ruling 2026-10-04; see `seam_refuse` above).
seam_refuse "$LANE" "a lane end"

# The state cell is the row's SEVENTH column — awk field 8, because $1 is the
# empty string before the leading pipe — and it runs to the trailing pipe, so a
# cell that contains a literal `|` is rejoined rather than truncated. Counting
# back from NF returned the last FRAGMENT of such a cell, and the live register
# has two rows with stray pipes in them: for one, NF-1 is ` re-cut `. A
# fragment that happens not to carry the word LANDING is a fail-open.
state_cell_of() {
  local s
  s="$(printf '%s\n' "$1" | awk -F'|' '{
    if (NF >= 9) { c = $8; for (i = 9; i < NF; i++) c = c "|" $i; print c }
    else { print $(NF-1) }
  }')"
  s="${s#"${s%%[![:space:]]*}"}"   # ltrim
  s="${s%"${s##*[![:space:]]}"}"   # rtrim
  printf '%s' "$s"
}
state_cell="$(state_cell_of "$row")"

# ------------------------------------------------------- the in-flight guard

# FIRST, exactly, from the lane's own object log (Amendment 7). `lane-objects`
# prints this lane's LAST event per object — an object another lane has since
# taken over is no longer this lane's business and is not listed.
# ONLY 0 AND 8 ARE ANSWERS. `lane-objects` exits 8 for "this lane has no log
# file" — the pre-cutover case — and 0 with the rows otherwise. Every other
# exit is the helper FAILING, and the difference is the difference between "the
# lane holds nothing" and "nobody asked". With `2>/dev/null` and a single
# failure code, a broken helper made this script print `no object log for
# <lane> (pre-cutover lane)` about a lane that was holding an open claim, and
# end it. Rule 6's merge hold and Rule 1's claim both stopped being enforced,
# silently, and the fragment the next session reads said there was nothing.
log_rows=""; have_log=0; open_list=""; open_objs=""
LE_TMP="$(mktemp)"
lo_rc=0
log_rows="$("$LANES_EDIT" lane-objects "$LANE" 2>"$LE_TMP")" || lo_rc=$?
[ -s "$LE_TMP" ] && cat -- "$LE_TMP" >&2
case "$lo_rc" in
  0) have_log=1 ;;
  8) have_log=0 ;;
  *) note "the helper's own output is above."
     die "$LANES_EDIT lane-objects $LANE failed (exit $lo_rc). That is NOT 'this lane has no object log', and this script will not guess which it is while the lane may still hold something. Fix the helper (or run it by hand: $LANES_EDIT lane-objects $LANE), then re-run." 1 ;;
esac

if [ "$have_log" = 1 ]; then
  while IFS=$'\037' read -r o_utc o_verb o_obj o_ref o_sup; do
    [ -n "${o_verb:-}" ] || continue
    case "$o_verb" in
      CLAIMED|TAKEOVER|OPENED|LANDING|WITHDRAWN)
        if [ -n "${o_sup:-}" ]; then
          # Another lane took this object over. A lane closes only its OWN
          # hold, so this lane's CLAIMED is still its last line and would
          # refuse for ever. The way out is its own RELEASED line.
          open_list="${open_list}${open_list:+$'\n'}  $o_verb $o_obj ($o_utc) — taken over by lane:${o_sup%@*} at ${o_sup#*@}; run: lanes-edit.sh release $o_obj \"taken over by ${o_sup%@*}\""
        else
          open_list="${open_list}${open_list:+$'\n'}  $o_verb $o_obj${o_ref:+ $o_ref} ($o_utc)"
        fi
        open_objs="${open_objs}${open_objs:+, }$o_obj" ;;
    esac
  done <<<"$log_rows"
fi

# Everything below is CASE-SENSITIVE on purpose: the protocol's words are
# LANDING, CLAIMED, LANDED, RELEASED in capitals, and a row's ordinary prose
# ("nothing claimed tonight") is not a claim.
scrub_cell() {
  printf '%s' "$1" | sed -E 's/(NOTHING IS CLAIMED|NOTHING CLAIMED|NONE CLAIMED|NOT CLAIMED|UNCLAIMED|NO CLAIM|NO LANDING OPEN|NO LANDING|LANDING WITHDRAWN|LANDING CLOSED|NOT LANDING|LANDING WINDOW|NOTHING IN FLIGHT)//Ig'
}
# AMENDMENT 13(a) — WHERE THE CELL IS THE PHRASE, ONLY THE STATE PART IS READ.
# `<STATE> · <UTC> · <one line>`: the STATE is where the lane is and the line is
# PROSE about its last act, which may legitimately say "waiting for LANDING #7"
# or "released the claim". Scanned whole, that prose is a merge hold this
# command then refuses to close the lane over — a false hold in the one path
# that still decides a hold from words rather than from the log (Copilot round 1
# on openRepoTools#82). A LEGACY DIARY CELL IS UNCHANGED and is still scanned
# whole; the two are told apart the way the migration tells them apart, by
# asking whether the cell's SECOND ` · ` field is exactly an instant — a diary's
# second entry begins with one and is never only one.
# BOTH FIELDS ARE CHECKED, NOT ONE (Copilot round 4 on openRepoTools#82). A
# second field that is an instant is not enough: a LEGACY DIARY whose second
# entry is nothing but a timestamp — `ACTIVE · 2026-09-12T10:00:00Z · LANDING
# #7` — would pass that test, the scan would be narrowed to `ACTIVE`, and a
# pre-cutover lane holding an open landing would be CLOSED instead of refused.
# Narrowing is the fail-OPEN direction, so it is taken only where the cell is
# certainly the phrase: the first field one of Amendment 13(a)'s seven states
# (or `MIGRATED`, which `migrate-state-cells` writes and which holds nothing),
# the second exactly an instant, and a third part after it. That is
# `mig_cell_is_phrase`'s rule in `lanes-edit.sh`; the states are the
# amendment's own list, and this is the second place that list is written.
# THE LINE KEEPS DECISION O1's GRAMMAR TOO (opensoft/openRepoTools#90, part
# (a); Copilot's last review of #82). `set-row-state` refuses a line of more
# than 240 characters or one carrying `|` or CR/LF, so a cell with any of them
# was not written by it, and narrowing there dropped its prose from the scan
# all the same: an open LANDING in that prose closed the lane. That cell is
# scanned whole, like a diary. The cap is in CHARACTERS and `${#x}` is BYTES in
# a C locale, where a line of `—` and `→` inside it would read as past it and
# a lane merely waiting for a LANDING would be held by it: past 240, a line
# that is valid UTF-8 is recounted with its continuation bytes taken out, the
# same number in every locale. Text that is not valid UTF-8 has continuation
# bytes that follow nothing and are no characters, so it keeps the first count,
# which can only over-count, and an over-count reads more of the cell.
cell_for_scan() {   # <state cell> — the part of it that says where the lane is
  cfs="$1"
  case "$cfs" in
    *' · '*)
      cfs_one="${cfs%% · *}"
      cfs_rest="${cfs#* · }"
      case "$cfs_rest" in *' · '*) : ;; *) printf '%s' "$cfs"; return 0 ;; esac
      cfs_two="${cfs_rest%% · *}"
      cfs_three="${cfs_rest#* · }"
      # EXACTLY THREE PARTS, AND THE THIRD NOT EMPTY — `mig_cell_is_phrase`'s
      # rule to the letter (Copilot round 5 on openRepoTools#82). A FOURTH ` · `
      # means a diary: `LIVE · <UTC> · a note · <UTC> LANDING #7` has the
      # phrase's first two fields and a history after them, and narrowing to
      # `LIVE` there would close a lane over an open landing.
      case "$cfs_three" in
        '' | *' · '*) printf '%s' "$cfs"; return 0 ;;
      esac
      # O1's OWN RULES FOR THE LINE, which `mig_cell_is_phrase` does not test
      # and `set-row-state` does (#90): no `|`, no CR/LF, at most 240 characters.
      case "$cfs_three" in
        *'|'* | *$'\n'* | *$'\r'*) printf '%s' "$cfs"; return 0 ;;
      esac
      # `iconv -c` drops what is not UTF-8, and its exit status is not to be
      # trusted (musl's is 0 either way), so the text is UTF-8 when it comes
      # back unchanged; with no `iconv` it never does. A count that cannot be
      # made is a long line, and BSD `wc` pads, hence the `tr`.
      cfs_len="${#cfs_three}"
      if [ "$cfs_len" -gt 240 ]; then
        cfs_utf8="$(printf '%s' "$cfs_three" | iconv -c -f UTF-8 -t UTF-8 2>/dev/null)" || :
        if [ "$cfs_utf8" = "$cfs_three" ]; then
          cfs_len="$(printf '%s' "$cfs_three" | LC_ALL=C tr -d '\200-\277' | wc -c | tr -d ' ')" ||
            cfs_len=241
        fi
      fi
      [ "$cfs_len" -le 240 ] || { printf '%s' "$cfs"; return 0; }
      cfs_ok=0
      case "$cfs_one" in
        LIVE|PAUSED|LANDED|ENDED|RETIRED|'HANDED OFF'|MIGRATED) cfs_ok=1 ;;
        'LANDING #'*)
          case "${cfs_one#LANDING #}" in
            '' | *[!0-9]*) : ;;
            *) cfs_ok=1 ;;
          esac ;;
      esac
      case "$cfs_two" in
        [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]Z | [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z) : ;;
        *) cfs_ok=0 ;;
      esac
      [ "$cfs_ok" = 1 ] && cfs="$cfs_one" ;;
  esac
  printf '%s' "$cfs"
}
haystack="$(scrub_cell "$(cell_for_scan "$state_cell")")"

# Start index of the LAST occurrence of $2 in $1, or -1.
last_index() {
  local h="$1" needle="$2" rest="${1##*"$2"}"
  [ "$rest" = "$h" ] && { printf -- '-1'; return 0; }
  printf '%s' "$(( ${#h} - ${#rest} - ${#needle} ))"
}

# The STATE-CELL scan keeps its own word. It used to reset the one the log loop
# had just set, so a lane whose hold came from its log and whose state cell said
# neither LANDING nor CLAIMED ended with an EMPTY verb written into the register
# and into the append-only log — `while  was still open` — and the register line
# blamed the state cell for a hold the script had just announced it read from
# `lanes/log/<lane>.md`.
# It is a FUNCTION because it now runs over two cells, not one — see below. It
# sets cell_at, cell_word and close_at, and RETURNS 0 when that cell is in
# flight: the last LANDING or CLAIMED in it has no LANDED or RELEASED after it.
cell_at=-1; cell_word=""; close_at=-1
scan_cell() {
  local h="$1" w i
  cell_at=-1; cell_word=""
  for w in LANDING CLAIMED; do
    i="$(last_index "$h" "$w")"
    if [ "$i" -gt "$cell_at" ]; then cell_at="$i"; cell_word="$w"; fi
  done
  close_at=-1
  for w in LANDED RELEASED; do
    i="$(last_index "$h" "$w")"
    if [ "$i" -gt "$close_at" ]; then close_at="$i"; fi
  done
  [ "$cell_at" -ge 0 ] && [ "$close_at" -lt "$cell_at" ]
}

cell_source="this checkout's row"
in_flight=0
if [ "$have_log" = 1 ]; then
  # The log is authoritative for a lane that has one. The state cell is the
  # row's prose history and no longer decides anything here.
  [ -n "$open_list" ] && in_flight=1
  step "in-flight read from lanes/log/$LANE.md (exact), not from the state cell"
  scan_cell "$haystack" || :
else
  # PRE-CUTOVER, AND THE ROW IS READ FROM `origin/main` TOO. `lane-objects`
  # reads what has LANDED (Amendment 7, R19) and this fallback read the WORKING
  # TREE — which a fetch does not move. A peer's `LANDING` appended to this
  # row's state cell and pushed is therefore invisible here until something
  # pulls, so the one path that still decides a merge hold from prose could
  # write `NOTHING IN FLIGHT` over a hold that is current, and Rule 6 stops
  # being enforced for exactly the lanes that have nothing else enforcing it.
  # BOTH cells are scanned and EITHER is a refusal: this checkout's row may
  # carry a hold that has not been pushed yet, and the published row a hold
  # that has not been pulled. A hold seen anywhere is a hold.
  scan_cell "$haystack" && in_flight=1
  if [ "$in_flight" = 0 ]; then
    pub_rc=0
    pub_row="$("$LANES_EDIT" register-row "$LANE" 2>/dev/null)" || pub_rc=$?
    case "$pub_rc" in
      0)
        pub_hay="$(scrub_cell "$(cell_for_scan "$(state_cell_of "$pub_row")")")"
        if [ "$pub_hay" != "$haystack" ] && scan_cell "$pub_hay"; then
          in_flight=1; haystack="$pub_hay"; cell_source="the published row on origin/main"
          step "the published row carries an open $cell_word that this checkout has not pulled"
        fi ;;
      8) : ;;                     # the published register has no row for it yet
      *) note "the helper's own output is above."
         die "$LANES_EDIT register-row $LANE failed (exit $pub_rc). This lane has no object log, so its row's state cell is the ONLY thing that can say whether a merge hold is open, and this script will not end a lane on a read it did not get. Fix the helper (or run it by hand: $LANES_EDIT register-row $LANE), then re-run." 1 ;;
    esac
  fi
fi

rflag=""; (( retire )) && rflag=" --retire"

if [ "$in_flight" = 1 ] && [ "$have_log" = 1 ]; then
  if [ "$force" = 0 ]; then
    note "lane $LANE still holds $(printf '%s\n' "$open_list" | grep -c .) object(s) — its own log's last event on each is still open:"
    printf '%s\n' "$open_list" >&2
    # THE REAL CLI GRAMMAR OF THE HELPER, and only verbs it accepts. The arrow is
    # its OWN argument: quoted into the free-text slot, `log LANDED <obj>
    # "→ <sha>"` writes the sha AFTER the ` — `, the line stops matching
    # `LANDED → <sha>`, and the Rule 6 line in the register loses the merge sha for
    # good in a file nothing rewrites. There is no `MERGED` verb at all — the
    # post-merge line IS `LANDED → <sha>` — and `log RELEASED` is refused, since
    # `release` is the subcommand and it posts the Rule 1 comment as well.
    note "the fix, in the order the protocol wants it — one of these per object:"
    note "  LANES_LANE=$LANE lanes-edit.sh log LANDED <object> → <merge sha>    # the PR merged"
    note "  LANES_LANE=$LANE lanes-edit.sh release <object> \"<why>\"             # the claim is given up"
    note "  LANES_LANE=$LANE lanes-edit.sh log CLOSED <object>                  # closed without merging"
    note "  $prog $LANE$rflag"
    note "or end it anyway and record that you did: $prog $LANE --force$rflag"
    exit 2
  fi
  step "--force: ending with $(printf '%s\n' "$open_list" | grep -c .) object(s) still open in the lane's log; the line written says so"
elif [ "$in_flight" = 1 ]; then
  # Bash substring, not `cut -c`: ${#s} counts characters and GNU cut counts
  # bytes, and these cells are full of · — → , so the two disagree.
  from=$(( cell_at > 40 ? cell_at - 40 : 0 ))
  excerpt="${haystack:from:160}"
  # BRACED, AND THE BRACES ARE LOAD-BEARING (A9 Addendum 4, R-A9-11). The line
  # below writes `\u2026$excerpt\u2026`, and bash decides where a variable NAME ends
  # with `isalnum()`, which is LOCALE-DEPENDENT: bash 3.2 on Darwin, under the
  # `en_US.UTF-8` the runner sets, reads the three bytes of `\u2026` as name
  # characters and looks up `excerpt\u2026` instead. Unset, `set -u`, and
  # `lane-end` died where it was about to print the refusal \u2014 the macOS job read
  # `lane-end: line 573: excerpt\u2026: unbound variable`, exit 0 instead of the
  # exit 2 that whole branch exists for. `${excerpt}` says where the name ends.
  if [ "$force" = 0 ]; then
    note "lane $LANE has no object log yet (pre-cutover), so this is the state cell's substring scan, over $cell_source:"
    note "lane $LANE still has something in flight — the last $cell_word in its state cell has no LANDED or RELEASED after it:"
    note "  …${excerpt}…"
    note "the fix, in the order the protocol wants it:"
    note "  lanes-edit.sh set-row-state $LANE \"LANDED · PR #<n> merged as <merge sha>\"      # or ENDED · …"
    note "  $prog $LANE$rflag"
    note "or end it anyway and record that you did: $prog $LANE --force$rflag"
    exit 2
  fi
  step "--force: ending with an open $cell_word in the state cell; the line written says so"
fi

# ---------------------------------------- the close-out gate (#163)
#
# WORKSPACE CREATION OUTPACES WORKSPACE CLOSEOUT, and this is where the second
# catches up with the first (opensoft/openRepoTools#163, on Brett Heap's word
# of 2026-10-05). A lane ended while its writers' trees, their branches, its
# scratch and its caches are still on disk leaves them to nobody: the cleanup
# records show landing and retirement as separate acts for months, and the
# separate act not happening. So the end of a lane asks, exactly as it already
# asks about open objects, and refuses while the answer is not "nothing".
#
# THE QUESTION IS THE SWEEP'S DRY RUN, never a second implementation of it:
# `lane-worktrees sweep <lane> --dry-run --porcelain` is #162's exit contract
# for this gate — 0 nothing of the lane's to retire, 3 something to retire, 2
# refused or a read failed — and its rows are what the refusal names. Any other
# exit, a missing command included, is a gate nobody read, and a read that
# failed is never an answer (Amendment 7(d)): exit 1, and nothing is written.
# `LANES_NO_FETCH=1` because this command has just fetched, in its own reads.
#
# TWO READS OF THIS COMMAND'S OWN SIT BESIDE IT, and both are about what the
# sweep's rows do not say. `git status --porcelain --ignored` in every tree of
# the lane's, because the sweep's own status read leaves ignored files out and
# a tree holding an `.env` or a `node_modules` is not a clean tree to name. And
# every entry directly under the lane's own root, `.lane-worktrees/<lane>/`,
# that no tree, scratch or cache row names — a directory a removed tree left its
# ignored files in, a brief, a log — which the sweep never lists and which is
# still the lane's: RESIDUE, and it refuses even where the sweep answered 0.
#
# `--inventory-only` IS THE ONE DOOR, and it takes the lane past every answer
# but leaves every tree where it is and in the lane's #97 inventory; the reason
# it was let past is written into the row's closing phrase and the ENDED line.
# `--sweep`, which would retire them in the same run, is not built: the act
# that retires them is `lane-worktrees sweep --yes`, on its own.
TAB="$(printf '\t')"
gate_argv=(sweep "$LANE" --branches --include-scratch --include-caches)
gate_shown="lane-worktrees ${gate_argv[*]}"

# A PATH'S PHYSICAL SPELLING, the way the sweep compares paths (`realpath`):
# a symlink under the lane's root is an entry of its own and is not followed.
gate_real() {   # <path>
  gr_p="${1-}"; gr_r=""
  if [ -d "$gr_p" ] && [ ! -L "$gr_p" ]; then
    gr_r="$(CDPATH='' cd -P -- "$gr_p" 2>/dev/null && pwd -P)" || gr_r=""
  else
    gr_r="$(CDPATH='' cd -P -- "$(dirname -- "$gr_p")" 2>/dev/null && pwd -P)" || gr_r=""
    if [ -n "$gr_r" ]; then gr_r="${gr_r%/}/$(basename -- "$gr_p")"; fi
  fi
  printf '%s\n' "${gr_r:-$gr_p}"
}

# WHAT IS IN ONE TREE, IGNORED FILES INCLUDED. `GIT_OPTIONAL_LOCKS=0`: a read
# that refreshed the index would be a write, and this gate writes nothing.
gate_tree_state() {   # <path>
  gts_p="${1-}"
  if [ ! -d "$gts_p" ]; then printf 'its directory is gone'; return 0; fi
  gts_top="$(git -C "$gts_p" rev-parse --show-toplevel 2>/dev/null)" || gts_top=""
  if [ -z "$gts_top" ] || [ "$(gate_real "$gts_top")" != "$(gate_real "$gts_p")" ]; then
    if ! ls -A -- "$gts_p" >/dev/null 2>&1; then
      printf 'no checkout of its own, and what is in it could not be listed'
      return 0
    fi
    gts_n=0
    for gts_e in "$gts_p"/* "$gts_p"/.[!.]* "$gts_p"/..?*; do
      if [ -e "$gts_e" ] || [ -L "$gts_e" ]; then gts_n=$((gts_n + 1)); fi
    done
    printf 'no checkout of its own; %s entr(ies) in it' "$gts_n"
    return 0
  fi
  gts_st="$(GIT_OPTIONAL_LOCKS=0 git -C "$gts_p" status --porcelain=v1 --ignored --untracked-files=normal 2>/dev/null)" || {
    printf 'git status --ignored could not be read in it'
    return 0
  }
  gts_m=0; gts_u=0; gts_i=0
  gts_c="$(printf '%s\n' "$gts_st" | awk 'NF { k = substr($0, 1, 2); if (k == "!!") i++; else if (k == "??") u++; else m++ } END { printf "%d %d %d\n", m + 0, u + 0, i + 0 }')"
  read -r gts_m gts_u gts_i <<GTS || :
$gts_c
GTS
  if [ "$gts_m" = 0 ] && [ "$gts_u" = 0 ] && [ "$gts_i" = 0 ]; then
    printf 'git status --ignored: clean, nothing ignored in it either'
    return 0
  fi
  gts_ign="$(printf '%s\n' "$gts_st" | awk 'substr($0, 1, 3) == "!! " { print substr($0, 4) }' | head -n 3 | tr '\n' ' ')"
  printf 'git status --ignored: %s changed; %s untracked; %s ignored' "$gts_m" "$gts_u" "$gts_i"
  if [ -n "$gts_ign" ]; then printf ' (%s)' "${gts_ign% }"; fi
  return 0
}

gate_rc=0; gate_out=""; gate_err=""
if [ -x "$LANE_WORKTREES" ]; then
  : > "$LE_TMP"
  gate_out="$(LANES_EDIT="$LANES_EDIT" LANES_NO_FETCH=1 "$LANE_WORKTREES" "${gate_argv[@]}" --dry-run --porcelain 2>"$LE_TMP")" || gate_rc=$?
  gate_err="$(cat -- "$LE_TMP" 2>/dev/null || :)"
else
  gate_rc=127
  gate_err="there is no executable lane-worktrees at $LANE_WORKTREES; \`openRepoTools --install\` places it beside this command (or set LANE_WORKTREES)"
fi

gate_holder=""; gate_refused=""; gate_lines=""; gate_named=""
gate_trees=0; gate_branches=0; gate_scratch=0; gate_caches=0; gate_residue=0; gate_foreign=0
gate_legs=0; gate_leg_lines=""   # T018: an assembly's leg checkouts, counted and listed
gate_person=0     # counted rows `--yes` leaves too: a person's to settle
gate_lf='
'
while IFS="$TAB" read -r g_kind g_a g_b g_c g_d g_e g_f; do
  [ -n "${g_kind:-}" ] || continue
  # THE FIELDS ARE ESCAPED (`\\`, `\t`, `\n`, `\r`) so a path is one field
  # whatever it holds, and `%b` is exactly the inverse: every backslash of the
  # original arrives doubled, so no other escape can appear.
  case "$g_kind" in
    lane)    gate_holder="${g_c:-}" ;;
    refused) gate_refused="${g_a:-}" ;;
    tree)
      g_path="$(printf '%b' "${g_c:-}")"
      # A FOREIGN ROW NAMES NOTHING FOR THE RESIDUE READ (S1 of the review of
      # #175): it is counted for nobody, so a standalone clone under the lane's
      # own root, which the sweep keeps FOREIGN, must still hold the gate.
      case "${g_a:-}" in
        foreign) gate_foreign=$((gate_foreign + 1)); continue ;;
      esac
      gate_named="${gate_named}$(gate_real "$g_path")${gate_lf}"
      [ "${g_b:-}" = retire ] || continue
      gate_trees=$((gate_trees + 1))
      case "${g_a:-}" in keep | live | load-bearing) gate_person=$((gate_person + 1)) ;; esac
      g_head="${g_e:-?}"
      gate_lines="${gate_lines}  tree     ${g_a}  ${g_path}  [${g_d:-?} ${g_head:0:12}]${gate_lf}             $(gate_tree_state "$g_path")${gate_lf}             ${g_f:-}${gate_lf}" ;;
    branch)
      [ "${g_b:-}" = retire ] || continue
      gate_branches=$((gate_branches + 1))
      case "${g_a:-}" in keep | list) gate_person=$((gate_person + 1)) ;; esac
      gate_lines="${gate_lines}  branch   ${g_a}  $(printf '%b' "${g_c:-}")  ${g_d:-}${gate_lf}             ${g_f:-}${gate_lf}" ;;
    scratch)
      g_path="$(printf '%b' "${g_c:-}")"
      gate_named="${gate_named}$(gate_real "$g_path")${gate_lf}"
      gate_scratch=$((gate_scratch + 1))
      case "${g_a:-}" in keep | live | load-bearing) gate_person=$((gate_person + 1)) ;; esac
      gate_lines="${gate_lines}  scratch  ${g_a}  ${g_path}  (${g_e:-0} bytes)${gate_lf}             ${g_f:-}${gate_lf}" ;;
    cache)
      g_path="$(printf '%b' "${g_c:-}")"
      gate_named="${gate_named}$(gate_real "$g_path")${gate_lf}"
      gate_caches=$((gate_caches + 1))
      gate_lines="${gate_lines}  cache    ${g_a}  ${g_path}  (${g_e:-0} bytes)${gate_lf}" ;;
    # T018 (opensoft/openRepoTools#186): A LEG CHECKOUT OF AN ASSEMBLY — only
    # where the lane's checkout is one; a single repository prints no such row.
    # The sweep counts it (`retire`) where its HEAD's own Lane: trailer is this
    # lane's and it holds work origin lacks or edits no commit carries, or
    # where it could not be read; one that holds work and is not this lane's
    # is listed below the table and counted for nobody, as the assembly's own
    # checkout is. No sweep acts on one, so its remedy is a person's.
    leg)
      g_path="$(printf '%b' "${g_c:-}")"
      g_head="${g_e:-?}"
      if [ "${g_b:-}" = retire ]; then
        gate_legs=$((gate_legs + 1))
        gate_lines="${gate_lines}  leg      ${g_a}  ${g_path}  [${g_d:-?} ${g_head:0:12}]${gate_lf}             ${g_f:-}${gate_lf}"
      else
        case "${g_a:-}" in
          clean | absent) : ;;
          *) gate_leg_lines="${gate_leg_lines}  leg      ${g_a}  ${g_path}  [${g_d:-?} ${g_head:0:12}]${gate_lf}             ${g_f:-}${gate_lf}" ;;
        esac
      fi ;;
  esac
done <<GATE
$gate_out
GATE

# RESIDUE: what is directly under the lane's own root and is named by no row.
# The root is beside the lane's recorded directory, as the sweep derives it; a
# lane that records none has none. A read of that directory that FAILED is the
# gate unread, never an empty root.
gate_unread=""
gd_rc=0; gd_dir=""
gd_dir="$(LANES_NO_FETCH=1 "$LANES_EDIT" lane-dir "$LANE" 2>/dev/null)" || gd_rc=$?
gate_root=""
case "$gd_rc" in
  0) gd_dir="${gd_dir%/}"
     if [ -n "$gd_dir" ]; then gate_root="${gd_dir%/*}/.lane-worktrees/$LANE"; fi ;;
  8) : ;;
  *) gate_unread="\`$LANES_EDIT lane-dir $LANE\` exited $gd_rc, so the lane's own root could not be read for residue" ;;
esac
gate_is_named() {   # <physical path>
  gin_p="${1-}"
  while IFS= read -r gin_l; do
    if [ -n "$gin_l" ] && [ "$gin_l" = "$gin_p" ]; then return 0; fi
  done <<GIN
$gate_named
GIN
  return 1
}
# A ROOT THAT CANNOT BE LISTED IS NOT AN EMPTY ONE (Copilot round 1 on #175):
# a glob over a directory nobody may read matches nothing, and every entry of
# it would pass unseen. `ls -A`'s status is the probe, and it is asked first.
if [ -n "$gate_root" ] && [ -d "$gate_root" ] && ! ls -A -- "$gate_root" >/dev/null 2>&1; then
  gate_unread="${gate_unread:+$gate_unread; }the lane's own root $gate_root could not be listed, so what is in it is unknown"
elif [ -n "$gate_root" ] && [ -d "$gate_root" ]; then
  for g_ent in "$gate_root"/* "$gate_root"/.[!.]* "$gate_root"/..?*; do
    if [ ! -e "$g_ent" ] && [ ! -L "$g_ent" ]; then continue; fi
    if gate_is_named "$(gate_real "$g_ent")"; then continue; fi
    gate_residue=$((gate_residue + 1))
    if [ -L "$g_ent" ]; then
      g_what="a symlink"
    elif [ -d "$g_ent" ]; then
      g_what="$(gate_tree_state "$g_ent")"
    else
      g_what="a file"
    fi
    gate_lines="${gate_lines}  residue  ${g_ent}${gate_lf}             ${g_what}; under the lane's own root and neither a tree nor scratch the sweep knows${gate_lf}"
  done
elif [ -n "$gate_root" ] && [ -e "$gate_root" ]; then
  gate_unread="${gate_unread:+$gate_unread; }the lane's own root $gate_root is there and is no directory"
fi

gate_left=""
gate_part() {   # <count> <noun>
  if [ "${1:-0}" -gt 0 ]; then gate_left="${gate_left:+$gate_left and }$1 $2"; fi
  return 0
}
gate_part "$gate_trees" "tree(s)"
gate_part "$gate_branches" "branch(es)"
gate_part "$gate_scratch" "scratch dir(s)"
gate_part "$gate_caches" "cache(s)"
gate_part "$gate_residue" "residue path(s)"
gate_part "$gate_legs" "leg checkout(s)"

# THE VERDICT. 0 passes only with nothing named and nothing unread beside it;
# 3 and 2 never pass; anything else is the gate nobody read.
gate_pass=0; gate_exit=2; gate_log=""
case "$gate_rc" in
  0) if [ -z "$gate_left" ] && [ -z "$gate_unread" ]; then gate_pass=1; fi
     gate_log="the close-out gate (sweep exit 0) found ${gate_left:-nothing} left${gate_unread:+ and could not read its own root for residue}" ;;
  3) gate_log="the close-out gate (sweep exit 3) found ${gate_left:-something} left" ;;
  2) gate_log="the close-out gate's sweep refused this lane (exit 2)${gate_left:+ with $gate_left left}" ;;
  *) gate_exit=1
     gate_log="the close-out gate could not be read (lane-worktrees exit $gate_rc)" ;;
esac
# A ROOT THAT COULD NOT BE READ IS THE GATE UNREAD WHATEVER THE SWEEP ANSWERED
# (Copilot round 2 on #175): exit 1, the environment, on 3 and 2 as on 0.
if [ -n "$gate_unread" ]; then
  gate_exit=1
  case "$gate_rc" in
    2 | 3) gate_log="$gate_log; and its own root could not be read for residue" ;;
  esac
fi

if [ "$gate_pass" = 1 ]; then
  step "close-out gate: nothing of lane $LANE's is left on disk ($gate_shown --dry-run answered 0)"
  if [ -n "$gate_leg_lines" ]; then
    note "the assembly's leg checkout(s) below hold work that is not this lane's by its HEAD's own Lane: trailer; they are listed and NOT counted, as the assembly's own checkout is not:"
    printf '%s' "$gate_leg_lines" >&2
  fi
  if [ "$inventory_only" = 1 ]; then
    note "--inventory-only changed nothing: the close-out gate passed, so the lane ends as any other does"
  fi
  inventory_only=0
else
  fflag=""; (( force )) && fflag=" --force"
  if [ "$inventory_only" = 0 ]; then
    case "$gate_rc" in
      0 | 3)
        if [ -z "$gate_left" ] && [ -n "$gate_unread" ]; then
          note "lane $LANE is NOT ended: the close-out gate (opensoft/openRepoTools#163) could not read all it asks about, and a read that failed is never an answer (Amendment 7(d))."
        else
          note "lane $LANE is NOT ended: the close-out gate (opensoft/openRepoTools#163) found what is still the lane's: ${gate_left:-the rows below}"
        fi ;;
      2)     note "lane $LANE is NOT ended: the close-out gate's sweep refused it (exit 2): ${gate_refused:-it printed no reason; run the table below}" ;;
      *)     note "lane $LANE is NOT ended: the close-out gate could not be read — \`$gate_shown --dry-run --porcelain\` exited $gate_rc, and a read that failed is never an answer (Amendment 7(d))." ;;
    esac
    if [ -n "$gate_lines" ]; then printf '%s' "$gate_lines" >&2; fi
    if [ -n "$gate_leg_lines" ]; then
      note "and the assembly's leg checkout(s) below hold work that is not this lane's by its HEAD's own Lane: trailer; they are listed and NOT counted:"
      printf '%s' "$gate_leg_lines" >&2
    fi
    if [ "$gate_foreign" -gt 0 ]; then
      note "($gate_foreign FOREIGN tree(s) beside these are not this lane's, and were not counted.)"
    fi
    if [ -n "$gate_unread" ]; then note "and: $gate_unread"; fi
    if [ "$gate_exit" = 1 ] && [ -n "$gate_err" ]; then
      note "what the gate said on stderr:"
      printf '%s\n' "$gate_err" | sed -e 's/^/    /' >&2
    fi
    note "the table, which changes nothing:"
    note "  $gate_shown"
    # THE ACT IS OFFERED ONLY WHERE THE SWEEP WOULD TAKE IT: on 2 its `--yes`
    # refuses for the same reason its dry run did.
    gate_act=0
    if [ "$gate_rc" = 3 ]; then gate_act=1; fi
    if [ "$gate_rc" = 0 ] && [ "$gate_caches" -gt 0 ]; then gate_act=1; fi
    # T018: A LEG IS NO SWEEP'S TO RETIRE, so where legs are all that is
    # counted the act offered is the person's, not `--yes`.
    if [ "$gate_legs" -gt 0 ] && [ $((gate_trees + gate_branches + gate_scratch + gate_caches)) = 0 ]; then gate_act=0; fi
    if [ "$gate_act" = 1 ]; then
      note "the one act that retires them (--yes removes only what is on origin or in a bundle, never a live writer's tree):"
      if [ "$gate_holder" = live ]; then
        note "  $gate_shown --yes --live <each live writer's worktree> | --live none"
      else
        note "  $gate_shown --yes"
      fi
      if [ "$gate_person" -gt 0 ]; then
        note "  ($gate_person row(s) above marked keep, live, load-bearing or list are left by --yes as well: each says why, and settling it is a person's)"
      fi
    fi
    if [ "$gate_residue" -gt 0 ]; then
      note "residue is no sweep's to retire: move it to \${XDG_STATE_HOME:-~/.local/state}/openRepoTools/lanes/$LANE/scratch/ or remove it by hand"
    fi
    if [ "$gate_legs" -gt 0 ]; then
      note "a leg checkout is the assembly's and no sweep acts on one: commit and push its work from inside it (git -C <leg> status, git -C <leg> push), then end the lane again"
    fi
    note "or end the lane and leave them where they are, in its inventory, with the reason written down:"
    note "  $prog $LANE$rflag$fflag --inventory-only"
    exit "$gate_exit"
  fi
  step "--inventory-only: ending lane $LANE past the close-out gate; $gate_log. Nothing on disk is touched, and the lines written say so"
fi

# --- BEGIN shared one-line cut (lane-collision-protocol Amendment 13(a)) ---
# BYTE-IDENTICAL IN `lane-start`, `lane-end` AND `lane-handoff`, the three
# commands that write a row's state cell — the same trade the four lane helpers
# already make for their workspace resolver, and for the same reason: a drifted
# copy is TWO ANSWERS to one question, and "what may this line contain, and
# where may it be cut" is one question.
#
# FOUR THINGS THE CELL'S LINE MAY NOT CARRY, and `set-row-state` REFUSES all
# four — which would cost the row its stamp or its closing write, so they are
# fixed here rather than met there:
#
#   ` · `  the phrase's own separator. A second one reads back as a fourth part
#          of `<STATE> · <UTC> · <one line>`; it is spelled `; `.
#   `|`    a cell boundary in a Markdown table row. A launch command or a
#          directory may legitimately hold one — `claude --flag 'a|b'`, a path
#          with a pipe in it — and the row is where it would forge a column
#          (Copilot round 1 on openRepoTools#82). It is spelled `¦`, the broken
#          bar, which is not a separator anywhere here; the object log's own
#          line keeps the character itself.
#   CR/LF  a row is ONE LINE of a table, so a newline in this text would split
#          it in two and every row after it would be read as a lane. Spelled
#          `; ` for the same reason the separator is (Copilot round 2).
#   >240   ratified decision O1's cap. Cut at 230 with the last partial WORD
#          dropped, which is what makes the cut safe: `${s:0:n}` counts BYTES
#          wherever the locale is not a UTF-8 one, and a byte offset can land
#          inside the very characters above. 230 and not 240, so the ` ...`
#          marker fits inside the cap however the two count.
cut_to_line() {   # <line> — the row's one line, safe to hand to set-row-state
  ctl="${1//$'\r'/ }"
  ctl="${ctl//$'\n'/; }"
  ctl="${ctl// · /; }"
  ctl="${ctl//|/¦}"
  [ "${#ctl}" -gt 230 ] || { printf '%s' "$ctl"; return 0; }
  ctl="${ctl:0:230}"
  case "$ctl" in *' '*) ctl="${ctl% *}" ;; esac
  printf '%s ...' "$ctl"
}
# --- END shared one-line cut ---

# --------------------------------------------------------------- the write

utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# ONE SPELLING OF THE WORKSTATION, the one `lanes-edit.sh:438` uses and
# `lane-start` now uses — `${LANES_WORKSTATION:-$(hostname -s)}` (A9 Addendum
# 4, R-A9-13). This line is the `lane-end on <ws>` free text of the row's
# status, which a reader compares with the workstation column beside it.
ws_pair="$(lanes_workstation_pair)"
ws="${ws_pair%%	*}"
# A CONTAINER WITH NO CONFIGURED WORKSTATION WRITES NOTHING (Amendment 11,
# decision 8(d), `R-A11-14`): this value is the `lane-end on <ws>` free text of
# the row's status, which a reader compares with the workstation column beside
# it, and a container id there is a comparison that can never match.
ws_why="$(lanes_workstation_why "${ws_pair##*	}")"
[ -z "$ws_why" ] || die "$ws_why" 2
# The register line names the source it actually read (the log for a lane that
# has one, the state cell for a pre-cutover lane) and the objects it read there.
# AMENDMENT 13(a) — THE CLOSING WRITE IS THE WHOLE CELL, AND IT CARRIES THE
# STATE WORD. The `state` column holds ONE PHRASE, `<STATE> · <UTC> · <one
# line>`, and `set-row-state` stamps the UTC itself, so this builds the line
# alone. `RETIRED` and `ENDED` are both clause (a) states, which is why
# `--retire` no longer needs a second write of its own — see below.
state_word=ENDED; (( retire )) && state_word=RETIRED
state_line="lane-end on $ws: window closing; NOTHING IN FLIGHT"
if [ "$in_flight" = 1 ] && [ "$have_log" = 1 ]; then
  # THE COUNT AND NOT THE LIST. Amendment 13(a)'s line is ONE line about the
  # lane's state, and a lane forced closed over eleven open objects names them
  # in 300 characters — measured in the suite's own `repoE-1`, which is exactly
  # the row this write then could not make. The objects are in the ENDED line of
  # the lane's own log, one section below, and in the refusal this `--force`
  # overrode; the row says how many there were and where they are.
  state_line="lane-end on $ws: window closing; ENDED WITH --force while this lane's own object log still showed $(printf '%s\n' "$open_list" | grep -c .) object(s) open; they are in lanes/log/$LANE.md"
elif [ "$in_flight" = 1 ]; then
  state_line="lane-end on $ws: window closing; ENDED WITH --force while the state cell of $cell_source still showed an open $cell_word; read the archived cell before trusting it"
fi
# THE DOOR PAST THE CLOSE-OUT GATE IS WRITTEN DOWN (#163): the row says it was
# taken and what the gate found, so an ended lane whose trees are still on disk
# is never read as one that closed out clean. It goes AFTER whatever the line
# already says, so a forced end keeps its own words first.
if [ "$inventory_only" = 1 ]; then
  state_line="$state_line; ENDED WITH --inventory-only: $gate_log; they stay in its #97 inventory"
fi
# The cell's own separator never appears INSIDE the line, and a line over
# Amendment 13's cap is cut here rather than costing the row its whole closing
# write — the object log one section along carries the full text either way.
state_line="$(cut_to_line "$state_line")"

if (( dry_run )); then
  plan "lanes-edit.sh set-row-state $LANE \"$state_word · $state_line\""
else
  LANES_LANE="$LANE" "$LANES_EDIT" set-row-state "$LANE" "$state_word · $state_line"
  step "row closed: $state_word"
fi

# ------------------------------------------------------------------ --retire

# `--retire` USED TO BE A SECOND WRITE, and Amendment 13(a) is what removed it.
# The cell was the row's history, so retiring a lane meant REPLACING THE STATE
# WORD it opened with and leaving everything after that word alone —
# `replace-in-row` with an anchor that had to occur exactly once in the row,
# which is what `--state-was` existed to supply when the derived word did not.
# The cell is now ONE PHRASE that every write replaces, so the closing write
# above already carries `RETIRED` as its state and there is nothing left for a
# second act to flip. `--state-was` is accepted and ignored, because a person
# with the old habit must not be refused mid-teardown.
if (( retire )); then
  [ -z "$state_was" ] ||
    note "--state-was is no longer needed: Amendment 13(a) made the state cell one phrase that set-row-state REPLACES, so the closing write above already carries RETIRED. It was ignored."
  (( dry_run )) || step "state cell reads RETIRED"
fi

# ------------------------------------------- the lane's own object log line

# Amendment 7. Written after the row, and it never fails the end of a lane:
# a lane that cannot write its log has still ended, and says that it could not.
# THE EVENT LINE FOLLOWS THE GRAMMAR, AND ITS FREE TEXT CARRIES NO ` — ` (R21).
# The line reads
#   ENDED — lane X, session u@W, T, lane:X — forced; open: <object list>
# so ` — ` occurs exactly twice, which is the guarantee that makes one parser
# read every line. The old text spent a third one on prose and named an EMPTY
# verb; the writer now refuses any free text carrying that separator, so this is
# enforced rather than hoped for.
lane_verb=ENDED; (( retire )) && lane_verb=RETIRED
lane_text="window closing; NOTHING IN FLIGHT"
if [ "$in_flight" = 1 ] && [ "$have_log" = 1 ]; then
  lane_text="forced; open: $open_objs"
elif [ "$in_flight" = 1 ]; then
  lane_text="forced; open: $cell_word in the state cell (pre-cutover lane, no object log)"
fi
# AND THE LANE'S OWN LOG CARRIES THE REASON IN FULL, which the row's 240
# characters may have cut (#163). No ` — ` and no `, ` in it: the line's own
# separators.
if [ "$inventory_only" = 1 ]; then
  lane_text="$lane_text; inventory-only: $gate_log; $gate_shown names them"
fi

if (( dry_run )); then
  plan "lanes-edit.sh log $lane_verb lane:$LANE --text \"$lane_text\""
elif LANES_LANE="$LANE" "$LANES_EDIT" log "$lane_verb" "lane:$LANE" ${home_override:+--home "$home_override"} --text "$lane_text" >/dev/null; then
  step "object log: $lane_verb"
else
  note "the lane's object log was NOT written ($lane_verb). Write it by hand:"
  note "  LANES_LANE=$LANE $LANES_EDIT log $lane_verb lane:$LANE --text \"$lane_text\""
fi

# ------------------------------------------------------- the handoff fragment
#
# On STDOUT, so it can be piped straight into the handoff the row points at.
# Generated from the log, because a handoff written from memory is how an
# object goes missing between two sessions of one lane.

# The headers NAME the two sets in the printed fragment, so a reader can check
# the partition against the amendment without reading the code that produced
# it — and so that nobody reads `## Done` as "merged" or `## Open` as "unclaimed".
printf '## Done (RELEASED, CLAIM-LOST, CLOSED, LANDED)\n\n'
done_n=0
if [ "$have_log" = 1 ]; then
  while IFS=$'\037' read -r o_utc o_verb o_obj o_ref o_sup; do
    [ -n "${o_verb:-}" ] || continue
    case "$o_verb" in
      CLOSED|LANDED|RELEASED|CLAIM-LOST)
        printf -- '- %s — %s%s (%s)\n' "$o_verb" "$o_obj" "${o_ref:+ $o_ref}" "$o_utc"
        done_n=$((done_n + 1)) ;;
    esac
  done <<<"$log_rows"
fi
[ "$done_n" = 0 ] && printf -- '- nothing recorded as done in this lane'"'"'s log\n'

# AND IT READS `lane_objects`' FIFTH FIELD, like the refusal above. An object
# another lane has taken over is still this lane's LAST LINE — a lane closes
# only its own hold — so it belongs under `## Open`; what it is not is OPEN. Left
# unqualified it is pasted into the next session's handoff as work that lane
# still has, about an object `who` says another lane holds: this fragment is the
# one surface that becomes a durable document, and the refusal, `who <object>`
# and `who --lane` all carry the takeover already.
printf '\n## Open (CLAIMED, TAKEOVER, OPENED, LANDING, WITHDRAWN)\n\n'
open_n=0
if [ "$have_log" = 1 ]; then
  while IFS=$'\037' read -r o_utc o_verb o_obj o_ref o_sup; do
    [ -n "${o_verb:-}" ] || continue
    case "$o_verb" in
      CLAIMED|TAKEOVER|OPENED|LANDING|WITHDRAWN)
        if [ -n "${o_sup:-}" ]; then
          printf -- '- %s — %s%s (%s) — superseded by TAKEOVER (lane:%s) — release it\n' \
            "$o_verb" "$o_obj" "${o_ref:+ $o_ref}" "$o_utc" "${o_sup%@*}"
        else
          printf -- '- %s — %s%s (%s)\n' "$o_verb" "$o_obj" "${o_ref:+ $o_ref}" "$o_utc"
        fi
        open_n=$((open_n + 1)) ;;
    esac
  done <<<"$log_rows"
else
  printf -- '- no object log for %s (pre-cutover lane) — see its row'"'"'s state cell in LANES.md\n' "$LANE"
fi
[ "$have_log" = 1 ] && [ "$open_n" = 0 ] && printf -- '- nothing open\n'

(( dry_run )) && step "--dry-run: nothing was written"
exit 0
