#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
#
# openRepoTools — the short way to put the estate commands on your PATH.
#
# Install them once, from anywhere, with no checkout and no clone:
#
#   gh api repos/opensoft/openRepoTools/contents/openRepoTools \
#       -H 'Accept: application/vnd.github.raw' | bash -s -- --install
#
# (or the raw-URL twin, `curl -fsSL …/main/openRepoTools | bash -s -- --install`).
#
# IT INSTALLS THE ESTATE'S COMMANDS, AND IT OWNS THE ONE ACT THAT CREATES THE
# WORKSPACE THOSE COMMANDS READ. `openRepoShape` is the standard's front door
# and stays there: this scaffolds no product repository, and that refusal is
# not ours to touch. The two verbs are `park` and `resume`, which add no
# mechanics of their own — they find the estate under your projects directory
# and run its own `make park` / `make resume` — and `status` is the read-only
# view of the same estate.
#
# THIS FILE USED TO SAY "There is no verb here and no scaffold", and that line
# is gone on purpose rather than by accident. Brett Heap RULED the command —
# lane-collision-protocol Amendment 9, ruling 4, verbatim "openRepoTools wip
# init" — so the line, the `*)` arm and the test that pinned them are what
# change (Amendment 9(b), and R-A9-1). The identity that survives is narrower
# and truer.
#
# `--install` PLACES SEVENTEEN FILES, not four: this file, `park`, `resume`,
# `status` (openRepoShape #82; the fourth under Brett Heap's RULING of
# 2026-09-10 in this repository, "lets go with a fourth file"), `lane` and
# `lanes` (lane-collision-protocol Amendment 18 Addendum 1 and Amendment 11's
# ratified decision 6 — `lane` is where `restart` was, Addendum 2 having retired
# that word from the PATH), `lane-handoff` (Amendment 17(a), ratified
# 2026-09-14: the swap IS the handoff, and this is its shell form),
# `lane-rename` (Amendment 16, ratified 2026-09-14: a lane is renamed by ONE
# WORD, in ONE COMMIT, and its old name resolves for ever), and the four
# lane helpers `lanes-edit.sh`, `lane-start`, `lane-end` and `link-estates` with
# the alias table `repos.tsv` they read — which came here from
# `opensoft/brett-wip` with their history under Amendment 9(b), because a
# person's workspace repository holds their DATA and the code belongs where code
# goes — `claude-current` and `claude-restart-check`
# (opensoft/workBenches#119, Brett Heap's home ruling of 2026-09-29: the Claude
# Code a launch starts, and the RESTART NEEDED line a running session shows) —
# `lanes-index` (opensoft/openRepoTools#160, lane-collision-protocol
# Amendment 14, ratified by Brett Heap 2026-10-05: the DERIVED INDEX of the lane
# register, which nothing that acts reads) — and `lane-worktrees`
# (opensoft/openRepoTools#162, on Brett Heap's word of 2026-10-05, "build 162":
# the sweep that retires a lane's abandoned worktrees, rescue first). It also
# places THREE skills,
# `/handoff`, `/lane-swap` and `/restart`, at two paths each, THREE command
# files, `/handoff`, `/ctx` and `/swap`, at that same pair of paths (A11
# Addendum 4 ruling 9; Amendment 17(a) and (f)), and merges TWO hook entries into
# `~/.claude/settings.json` — the `SessionStart` one (Amendment 9(b), A8
# Addendum 2 R-A8-5) and, since lane-collision-protocol Amendment 12 adoption
# act 3, the `UserPromptSubmit` NAME GUARD beside it — both of them only once
# `~/projects/xFactory/lanes-edit.sh`, the path they call, answers `guard`
# (opensoft/openRepoTools#237): TWENTY-NINE artifacts always, plus the two hook
# entries once that path answers, and the count is the invariant.
#
# One install line for the whole toolset, so a second engineer gets the estate
# commands from the README's one line and nothing depends on anyone's dotfiles.
#
# openRepoShape #82 wrote the two verbs and these install semantics; its #92
# carved them out into this repository, which added `status`; lane protocol
# Amendment 9 brought the lane tooling home and added `wip init`.

set -euo pipefail

# $OPENREPOTOOLS_REPO is an owner/name slug: it is an API path and a raw-URL
# path, and nothing here clones. Name a fork or a mirror once and both fetch
# forms follow it.
REPO="${OPENREPOTOOLS_REPO:-opensoft/openRepoTools}"
REF="${OPENREPOTOOLS_REF:-main}"
SELF="${BASH_SOURCE[0]:-$0}"

MODE=""
WORKDIR=""
STAGED=""
# THE PREFIX OF THE TEMPORARY A PLACEMENT IS MAKING (#167), named here for the
# `EXIT` trap `workdir` sets, which removes `"$PLACING"*`: `place_by_rename`
# removes its temporary on every failure it sees, and this is what removes it
# on the ones it cannot see — a signal, or an `errexit`. A PREFIX, set before
# `mktemp` runs, so no instant exists in which the file is there and the trap
# does not know its name (Copilot round 2 on #172).
PLACING=""

say() { printf '%s\n' "$*"; }
die() { printf '\nREFUSED: %s\n' "$1" >&2; exit "${2:-2}"; }

usage() {
	cat <<'USAGE'
openRepoTools --install            install (or update) the seventeen estate and
                                   lane commands into ~/.local/bin, plus the
                                   /handoff, /lane-swap and /restart skills, the
                                   /handoff, /ctx and /swap command files, the
                                   SessionStart hook entry and the
                                   UserPromptSubmit name guard, once
                                   ~/projects/xFactory/lanes-edit.sh
                                   answers `guard`
openRepoTools wip init             create your workspace repository, clone it,
                                   seed it, and point this workstation at it
openRepoTools --help | --version

`openRepoShape` is the standard's front door and scaffolds product
repositories; this command scaffolds none. `--install` places seventeen files
beside each other — this command, the two verbs, the read-only view, the four
lane words, the four lane helpers, the alias table they read, the two Claude
Code launch commands, the derived index of the lane register and the worktree
sweep:

  park [<Name>]                    commit, push and RECORD an estate's open
                                   features, so another workstation can take
                                   the work up
  resume [<Name>]                  clone or fast-forward that estate here and
                                   recreate those features
  status [<Name>]                  what is in and out of sync across that
                                   estate, as of the last fetch; changes nothing
  lane [<name> [<profile>]]        the lanes, NUMBERED, and one question: the
                                   one you pick is bound — a parked lane
                                   through the launcher in its own directory
                                   and profile, one live here by attaching to
                                   its window, one bound elsewhere by naming
                                   where. With no terminal it asks nothing
  lanes [--all] [--fetch]          inside a checkout: that repository's lanes
                                   and the next free position, filled in;
                                   outside one, or with --all, every lane.
                                   Reads locally, changes nothing, asks nothing
  lane-start <repo> <n>            open a lane: name the window, register the
                                   row, launch or resume the session — with
                                   --agent <name>, from any agent that can read
                                   the lane (Amendment 17(c))
  lane-handoff [--restart|--exit]  hand this lane off: the identity triple, the
                                   handoff refreshed, the writers polled, the
                                   PAUSED record written, the restart line
                                   printed. --restart is /ctx; --exit ends the
                                   session a second place asked for the lane
  lane-end <lane>                  close it, and refuse while anything is in
                                   flight or any tree, branch, scratch or cache
                                   of it is still on disk (--inventory-only
                                   ends it anyway, and says so)
  lane-rename <old> <new>          rename it: the row, the object log, the
                                   handoff and lanes/aliases.tsv in ONE commit,
                                   one comment per object it holds, and the old
                                   name resolving for ever (Amendment 16)
  lanes-edit.sh <verb> …           the register's and the object logs' only
                                   writer
  link-estates                     place the symlinks every lane path already
                                   uses, into your workspace repository
  repos.tsv                        the organisation's alias table, which
                                   `lanes-edit.sh` reads
  claude-current [--porcelain]     the Claude Code a launch starts: npm's
                                   published version, updated when behind and
                                   verified with --version, by absolute path
                                   and never through PATH (lane-start uses it)
  claude-restart-check             one RESTART NEEDED line for a running
                                   session whose binary moved on disk, for the
                                   shared status line to print
  lanes-index sync|reconcile|status
                                   the DERIVED INDEX of the lane register
                                   (Amendment 14): written behind the sources,
                                   read only by lanes --index and
                                   history --index
  lane-worktrees sweep <lane>      retire a lane's abandoned worktrees: a dry
                                   run by default; --yes pushes or rescues
                                   first and removes only what is on origin
                                   or in a bundle, never a live writer's
  lane-worktrees add <lane> <slice>
                                   make a lane's worktree and record it in the
                                   lane's inventory, in one act

`--install` also places THREE SKILLS at two paths each —
`~/.claude-profiles/shared/skills/<name>/SKILL.md` and
`~/.claude/skills/<name>/SKILL.md`, for `handoff`, `lane-swap` and `restart` —
and THREE COMMAND FILES at the matching pair,
`~/.claude-profiles/shared/commands/<name>.md` and `~/.claude/commands/<name>.md`,
for `handoff`, `ctx` and `swap`. `handoff` is the act itself (Amendment 17(a));
`/ctx` is `/handoff --restart` (clause (f)); `/swap` and `/lane-swap` are the
aliases the amendment keeps so that nothing written about them stops working,
and each adds nothing to the skill it names. It merges TWO entries into
`~/.claude/settings.json` — the `SessionStart` one and, since
lane-collision-protocol Amendment 12, the `UserPromptSubmit` NAME GUARD, which
refuses a prompt whose window, session name and register row are not one lane
(needs `jq`, refuses rather than rewriting a file it cannot parse, and leaves
every `UserPromptSubmit` hook of your own exactly where it is). It merges the
two only once `~/projects/xFactory/lanes-edit.sh`, the path both call, exists,
is executable and answers `guard` — the launcher's own rule (#237). Until then
it says it DEFERRED them, and takes its own guard entry back out of a file that
already carries it; `wip init` merges them after `link-estates` links that
path. It never writes a profile's own settings.json: the launcher owns that one.

It then writes a RECEIPT of the files it placed — one row of
`<name> <destination> <sha256> <UTC>` each — at
`${OPENREPOTOOLS_DATA_DIR:-${XDG_DATA_HOME:-~/.local/share}/openRepoTools}/installed.tsv`,
mode 0600. That is how a later `--install` knows that a command it is about to
RETIRE is a copy it wrote rather than a file of yours that happens to carry the
same header: a digest that still matches is removed, one that has moved — or a
row it cannot check, for want of a digest tool — is named and left for you. A
receipt it cannot write is one line on this terminal and never a refused
install.

`/restart` is the INSIDE half and has to be a skill: a running session cannot
`exec` a launcher over itself, so it binds the window, stamps the record
through `lane-start --no-launch` and prints — never a picker (Amendment 11
clause (f), ratified decisions 3 and 7). `lane` is the OUTSIDE half, and it is
the whole of what a person is given: `restart` left the PATH under Amendment 18
Addendum 2 (ratified 2026-09-14T16:50:32Z, verbatim "ratify").

`wip init` — the workspace repository, which holds your lane register, your
object logs, your handoffs and your workspace manifests, and no code at all:

  openRepoTools wip init [--org <org>] [--login <name>] [--team <team>]
                         [--dry-run]

It asks you at most one question, and only where an administrator has to create
the repository for you — then it prints the exact commands, already filled in.
Everything else is derived: your login from `gh api user`, the organisation
from this toolset's own default, the team from `gh api /user/teams` where
exactly one matches. Idempotent: a workstation that already has a workspace
does nothing to it, and only completes the hook pair — running `link-estates`
again first where that path does not answer and ~/projects/xFactory exists.
Exits 0 (done, or already done) or 2 (refused); a hook reconcile it cannot
make is printed, and the exit stays 0.

`park --help`, `resume --help`, `status --help`, `lane-start --help` and
`lane-end --help` say the rest, and each command's exit codes are its own. The
two verbs implement none of the mechanics: they find the estate under your
projects directory and run its own `make park` / `make resume`, which run the
Speckit git extension's scripts. `status` only reads.

  $OPENREPOTOOLS_REPO       owner/name to fetch from (default opensoft/openRepoTools)
  $OPENREPOTOOLS_REF        the ref to fetch it at   (default main)
  $OPENREPOTOOLS_BIN_DIR    where --install puts them (default ~/.local/bin)
  $OPENREPOTOOLS_DATA_DIR   where the receipt goes    (default
                            $XDG_DATA_HOME/openRepoTools)
  $XDG_DATA_HOME            the data root it falls back to (default ~/.local/share)
  $AGENT_PROTOCOL_ROOT      where workspace.yaml lives (default ~/.agents)
  $CLAUDE_PROFILES_HOME     the profiles root         (default ~/.claude-profiles)
  $PROJECTS_DIR             your projects directory   (default ~/projects)
USAGE
}

# THE TRAP BELONGS TO THE SHELL THAT DOES THE WRITING, so this makes the
# directory and takes no output. A `$(workdir)` would be a COMMAND
# SUBSTITUTION — a subshell, whose `trap … EXIT` fires the instant the
# substitution closes, so `rm -rf` would run before a single fetched byte had
# landed and $WORKDIR out here would still be empty (openRepoShape #74). A trap
# only ever fires in the shell that set it. Callers read $WORKDIR.
workdir() {
	if [ -z "$WORKDIR" ]; then
		WORKDIR="$(mktemp -d)"
		trap 'rm -rf -- "$WORKDIR"; [ -z "$PLACING" ] || rm -f -- "$PLACING"*' EXIT
	fi
}

# THE API FIRST, the raw URL second. `gh api` is authenticated, so it still
# works inside an organisation whose policy blocks raw.githubusercontent.com;
# curl is the fallback for a machine with no `gh`. Each attempt writes to a
# file and is checked before its bytes are used, so a half-written first
# attempt is never mistaken for a fetch.
#
# THESE FORTY LINES ARE A DELIBERATE COPY of openRepoShape's own shim, and the
# duplication is the cheaper of the two mistakes: a shared `orp-fetch.sh` would
# be a FIFTH FILE for `--install` to place, and a broken installer the first
# time somebody copied only some of them. It is the same argument the estate
# resolver inside `park`, `resume` and `status` already carries, for the same
# reason — each of these files is one file a person has on PATH. Two installers that
# fetch the same way is a claim `tests/test_openrepotools_command.py` and
# openRepoShape's own suite each assert against their own script's text.
#
# WHAT IT FETCHES FROM IS THE ONE THING THAT MOVED (#186, T007): the CURRENT
# SOURCE, `$PAYLOAD_REPO` at `$PAYLOAD_REV`, which is `$REPO` itself until
# `payload_resolve_remote` has read and checked `$REPO`'s own code pin, and the
# repository that pin names after that. For a single repository it never
# moves: `$REPO` at `$REF`, exactly as it always was.
fetch_from_repo() {
	local path="$1" dest="$2"
	if command -v gh >/dev/null 2>&1 &&
		gh api "repos/${PAYLOAD_REPO:-$REPO}/contents/$path?ref=${PAYLOAD_REV:-$REF}" \
			-H 'Accept: application/vnd.github.raw' >"$dest" 2>/dev/null &&
		[ -s "$dest" ]; then
		return 0
	fi
	if command -v curl >/dev/null 2>&1 &&
		curl -fsSL "https://raw.githubusercontent.com/${PAYLOAD_REPO:-$REPO}/${PAYLOAD_REV:-$REF}/$path" \
			>"$dest" 2>/dev/null && [ -s "$dest" ]; then
		return 0
	fi
	# RETURNS rather than dying, so a caller fetching SEVERAL files can report
	# the whole set instead of stopping mid-way through an install
	# (openRepoShape #82, F10 of the review on its #83).
	return 1
}

# ------------------------------------------------ WHERE THE PAYLOAD COMES FROM
#
# opensoft/openRepoTools#186 — T007 of `specs/004-migrate-to-triad/tasks.md`,
# on `specs/004-migrate-to-triad/installer-design.md`. There are TWO LAYOUTS,
# and this asks which one it is looking at rather than guessing:
#
#   * A SINGLE REPOSITORY — `opensoft/openRepoTools` today — carries every
#     payload file at its root. NOTHING ABOUT IT CHANGES: a directory beside
#     this file that holds every payload file (a checkout, workBenches'
#     vendored tree) is copied with no network at all, byte for byte and line
#     for line what it always was; one that holds only some of them copies
#     those and fetches the rest; and a run from stdin fetches them all.
#   * AN ADOPTED ASSEMBLY — the same slug after the triad split — carries
#     `contracts/code-pin.yaml`, which names the CODE repository and commit,
#     and a gitlink at the pinned path that must agree with it. The payload is
#     that repository's, at that commit, and nothing else's.
#
# A SINGLE REPOSITORY IS READ EXACTLY AS IT ALWAYS WAS: `$OPENREPOTOOLS_REF`
# as given, file by file, by `gh api …/contents` and then the raw URL — no
# other host, no `jq`, no fallback that was not there before. The one request
# it makes that it did not make before is the question below, by those same
# two transports, and it prints nothing. Only an ADOPTED ASSEMBLY resolves the
# ref:
# `$OPENREPOTOOLS_REF` becomes a commit on the first API round trip and every
# request after it names that commit, so a branch that moves while the files
# come down cannot hand one run two versions. That guarantee needs
# api.github.com (and `jq` on the `curl` path), which a single repository's
# install never needed and still does not: today's layout keeps today's
# network needs, byte for byte (#191's review). `$OPENREPOTOOLS_REPO` and
# `$OPENREPOTOOLS_REF` mean what they always meant: what to install from, and
# at which ref.
#
# WHICH LAYOUT IS DECIDED BY AN ANSWER, NEVER BY A FAILURE. One file is asked
# for by name at `$REF`, through `fetch_from_repo`'s own two transports
# (`probe_path`): `contracts/code-pin.yaml`. A pin that is there is the adopted
# layout. GitHub's 404 for it is a single repository, read as above. An
# authentication failure, a rate limit, a transport failure or any other status
# from both transports is a REFUSAL naming what answered — never evidence of
# the older layout, because reading it that way would install whatever the
# assembly root happened to carry. NOTHING ELSE IS ASKED on that 404: a second
# question (the adoption manifest, `project.yaml`) would be a second request on
# today's layout for a state the adopter never publishes — it writes the pin
# and the manifest in one commit — and that `make validate` refuses in the
# assembly itself (#191's review).
#
# A PIN THAT IS PRESENT AND WRONG REFUSES. Its grammar is fixed, so it is read
# here with `read` and `case` rather than a YAML runtime (`pin_parse`), and
# every field is checked. The payload repository is the pin's own
# `source_repository`, never a name made by appending `-code` to a fork's: a
# fork follows the pin it actually carries.
#
# WHAT A REMOTE INSTALL DOES NOT CLAIM: the pin's `tree_sha256`. That digest is
# over the code leg's WHOLE tree and is the composed validator's to recompute
# (`scripts/validate-pins.py` in the assembly). What is verified here is the
# identity this run consumes — the commit the ref names, the gitlink at the
# pinned path, and that the pinned commit is one the pinned repository has.
#
# LOCALLY — this file run from the code leg MOUNTED in an assembly checkout,
# `<root>/<submodule_path>`, whose `contracts/code-pin.yaml` names this
# directory — the pin and the
# gitlink must agree and every payload file must be the pinned commit's bytes;
# then they are copied, offline. A code checkout under an assembly that is NOT
# its mounted leg (a paired feature worktree, `<root>/worktrees/<NNN>/code`) is
# somebody installing that checkout ON PURPOSE: its bytes are copied as they
# are, and the run names that checkout and its commit and says it is not the
# pinned code. Neither is ever completed from a network — local siblings and
# remote bytes do not make one install between them. THOSE TWO LAYOUTS AND NO
# OTHER: a `contracts/code-pin.yaml` anywhere else above this file — another
# project's assembly that happens to hold a vendored tree deep inside it — is
# not this tree's, is not read, and changes nothing (`assembly_above`).
#
# ALL OF IT HAPPENS BEFORE ANYTHING IS PLACED: `install_commands` asks
# `payload_select` straight after the targets, before the first byte is
# collected, and every refusal here places nothing.
#
# ONE CURRENT SOURCE, AND IT IS `$REPO`'S. Every request this file makes reads
# `${PAYLOAD_REPO:-$REPO}`: `$REPO` while its ref, its tree and its pin are
# read, and — only once that pin has passed every check below — the repository
# `$REPO`'s own pin names. No other repository is ever reached for the payload,
# and `tests/test_repo_hygiene.py` holds every fetching line to naming `$REPO`.

PAYLOAD_MODE=""      # "" until chosen: local | siblings | remote
PAYLOAD_DIR=""       # the directory payload files are copied from, if any
PAYLOAD_REPO=""      # owner/name every fetch reads, once resolved
PAYLOAD_REV=""       # the 40-hex commit every fetch reads, once resolved
SOURCE_SHA=""        # $REF, resolved once
SOURCE_LAYOUT=""     # single | adopted | feature, once known
API_SHA=""
API_WHY=""
PROBE_LAYOUT=""      # single | adopted | unknown, once contracts/code-pin.yaml is asked for
PROBE_WHY=""
RESOLVE_FOR=install  # whose refusal a failed resolution is: install, or wip (`wip init` step 7)

#: THE CHARACTERS GITHUB SPELLS A NAME WITH, WRITTEN OUT. A bracket RANGE such
#: as `[a-z]` follows the locale's collation in bash 3.2 and can admit capitals,
#: and `case` is how this file matches.
ORT_ALNUM='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
ORT_TAB="$(printf '\t')"
ORT_CR="$(printf '\r')"
#: One listing row per entry: `path<TAB>type<TAB>sha`. `gh` evaluates it itself
#: (`--jq`); the curl path hands the same filter to `jq`.
ORT_TREE_JQ='.tree[] | [.path, .type, .sha] | @tsv'

# EVERY FILE `--install` READS, derived from the three lists and from nothing
# else: the commands at the root, then each skill's and each command file's path.
payload_names() {
	local name
	for name in "${INSTALLABLES[@]}"; do printf '%s\n' "$name"; done
	for name in "${SKILLS[@]}"; do skill_source_path "$name"; done
	for name in "${COMMANDS[@]}"; do command_source_path "$name"; done
}

is_lower_hex() {   # <length> <value>
	case "$2" in '' | *[!0123456789abcdef]*) return 1 ;; esac
	[ "${#2}" -eq "$1" ]
}

is_slug() {   # <owner/name> — a repository as GitHub spells one, and nothing else
	local owner="${1%%/*}" name="${1#*/}"
	[ "$owner" != "$1" ] || return 1
	case "$owner" in '' | -* | *- | *[!${ORT_ALNUM}-]*) return 1 ;; esac
	case "$name" in '' | . | .. | */* | *[!${ORT_ALNUM}._-]*) return 1 ;; esac
	return 0
}

is_segment() {   # <path> — exactly one path segment
	case "$1" in '' | . | .. | *[!${ORT_ALNUM}._-]*) return 1 ;; esac
	return 0
}

# --- the code pin's fixed grammar ---------------------------------------------
#
# `contracts/code-pin.yaml` as openRepoShape's adopter writes it: top-level
# `key: value` lines, comments, blank lines, and one mapping, `digests:`, whose
# indented `tree_sha256:` is read. A scalar may be double- or single-quoted.
# A key this reader does not use (a `tag:` label beside the commit, say) is
# allowed once; a key it does use must appear exactly once; and every value
# it uses must have the one form the contract gives it. LF only.

PIN_SCALAR=""
pin_scalar() {   # <text after the colon> — PIN_SCALAR, or 1 for a quote that does not close
	local v="$1" q rest
	while :; do case "$v" in ' '* | "$ORT_TAB"*) v="${v#?}" ;; *) break ;; esac; done
	case "$v" in
	'"'* | "'"*)
		q="${v:0:1}"
		rest="${v#?}"
		case "$rest" in *"$q"*) ;; *) return 1 ;; esac
		PIN_SCALAR="${rest%%"$q"*}"
		rest="${rest#*"$q"}"
		while :; do case "$rest" in ' '* | "$ORT_TAB"*) rest="${rest#?}" ;; *) break ;; esac; done
		case "$rest" in '' | '#'*) ;; *) return 1 ;; esac
		# No escapes in this grammar: a backslash is a value nobody wrote.
		case "$PIN_SCALAR" in *\\*) return 1 ;; esac
		;;
	'#'*) PIN_SCALAR="" ;;
	*)
		PIN_SCALAR="${v%% #*}"
		while :; do
			case "$PIN_SCALAR" in *' ' | *"$ORT_TAB") PIN_SCALAR="${PIN_SCALAR%?}" ;; *) break ;; esac
		done
		;;
	esac
	return 0
}

pin_bad() {   # <field> <value> <what it must be> — PIN_WHY, and 1
	if [ -z "$2" ]; then
		PIN_WHY="\`$1\` is missing"
	else
		PIN_WHY="\`$1\` is '$2', $3"
	fi
	return 1
}

pin_parse() {   # <file> — the PIN_* fields and 0, or PIN_WHY and 1
	local line body key n=0 seen=" " block="" tree_seen=0
	PIN_SCHEMA="" PIN_KIND="" PIN_LEG_ROLE="" PIN_SOURCE_REPOSITORY=""
	PIN_SUBMODULE_PATH="" PIN_COMMIT="" PIN_REVISION_KIND=""
	PIN_DIGEST_ALGORITHM="" PIN_DIGEST_DEFINITION="" PIN_TREE_SHA256=""
	PIN_VERIFY="" PIN_RUNBOOK="" PIN_WHY=""
	[ -s "$1" ] || { PIN_WHY="it is empty"; return 1; }
	while IFS= read -r line || [ -n "$line" ]; do
		n=$((n + 1))
		case "$line" in
		*"$ORT_CR"*) PIN_WHY="line $n ends in a carriage return, and the grammar is LF-only"; return 1 ;;
		'' | '#'*) continue ;;
		' '* | "$ORT_TAB"*)
			body="$line"
			while :; do case "$body" in ' '* | "$ORT_TAB"*) body="${body#?}" ;; *) break ;; esac; done
			case "$body" in '' | '#'*) continue ;; esac
			# AN INDENTED LINE BELONGS TO THE KEY ABOVE IT, and the only one
			# this reads into is `digests:`.
			[ "$block" = digests ] || continue
			key="${body%%:*}"
			[ "$key" != "$body" ] || { PIN_WHY="line $n is not \`key: value\`"; return 1; }
			pin_scalar "${body#*:}" || { PIN_WHY="line $n has a quoted value that does not close cleanly"; return 1; }
			if [ "$key" = tree_sha256 ]; then
				[ "$tree_seen" -eq 0 ] || { PIN_WHY="\`digests.tree_sha256\` appears twice"; return 1; }
				tree_seen=1
				PIN_TREE_SHA256="$PIN_SCALAR"
			fi
			continue ;;
		esac
		block=""
		key="${line%%:*}"
		[ "$key" != "$line" ] || { PIN_WHY="line $n is not \`key: value\`"; return 1; }
		case "$key" in
		'' | *[!abcdefghijklmnopqrstuvwxyz0123456789_]*)
			PIN_WHY="line $n's key '$key' is not a key this grammar has"; return 1 ;;
		esac
		case "$seen" in *" $key "*) PIN_WHY="\`$key\` appears twice"; return 1 ;; esac
		seen="$seen$key "
		pin_scalar "${line#*:}" || { PIN_WHY="line $n has a quoted value that does not close cleanly"; return 1; }
		case "$key" in
		schema_version) PIN_SCHEMA="$PIN_SCALAR" ;;
		kind) PIN_KIND="$PIN_SCALAR" ;;
		leg_role) PIN_LEG_ROLE="$PIN_SCALAR" ;;
		source_repository) PIN_SOURCE_REPOSITORY="$PIN_SCALAR" ;;
		submodule_path) PIN_SUBMODULE_PATH="$PIN_SCALAR" ;;
		commit) PIN_COMMIT="$PIN_SCALAR" ;;
		revision_kind) PIN_REVISION_KIND="$PIN_SCALAR" ;;
		digest_algorithm) PIN_DIGEST_ALGORITHM="$PIN_SCALAR" ;;
		digest_definition) PIN_DIGEST_DEFINITION="$PIN_SCALAR" ;;
		verify_pin) PIN_VERIFY="$PIN_SCALAR" ;;
		resync_runbook) PIN_RUNBOOK="$PIN_SCALAR" ;;
		digests)
			[ -z "$PIN_SCALAR" ] || { PIN_WHY="\`digests\` is '$PIN_SCALAR', not a mapping"; return 1; }
			block=digests ;;
		esac
	done <"$1"
	[ "$PIN_SCHEMA" = 1 ] || { pin_bad schema_version "$PIN_SCHEMA" "not 1"; return 1; }
	[ "$PIN_KIND" = pinned_contract_manifest ] ||
		{ pin_bad kind "$PIN_KIND" "not pinned_contract_manifest"; return 1; }
	[ "$PIN_LEG_ROLE" = code ] || { pin_bad leg_role "$PIN_LEG_ROLE" "not code"; return 1; }
	is_slug "$PIN_SOURCE_REPOSITORY" ||
		{ pin_bad source_repository "$PIN_SOURCE_REPOSITORY" "not an owner/name repository"; return 1; }
	is_segment "$PIN_SUBMODULE_PATH" ||
		{ pin_bad submodule_path "$PIN_SUBMODULE_PATH" "not one path segment"; return 1; }
	is_lower_hex 40 "$PIN_COMMIT" ||
		{ pin_bad commit "$PIN_COMMIT" "not 40 lowercase hex — a branch, a tag or an abbreviated commit is a moving reference"; return 1; }
	[ "$PIN_REVISION_KIND" = commit ] || { pin_bad revision_kind "$PIN_REVISION_KIND" "not commit"; return 1; }
	[ "$PIN_DIGEST_ALGORITHM" = sha256 ] ||
		{ pin_bad digest_algorithm "$PIN_DIGEST_ALGORITHM" "not sha256"; return 1; }
	[ "$PIN_DIGEST_DEFINITION" = sorted-ls-tree-r-v1 ] ||
		{ pin_bad digest_definition "$PIN_DIGEST_DEFINITION" "not sorted-ls-tree-r-v1"; return 1; }
	is_lower_hex 64 "$PIN_TREE_SHA256" ||
		{ pin_bad digests.tree_sha256 "$PIN_TREE_SHA256" "not 64 lowercase hex"; return 1; }
	[ -n "$PIN_VERIFY" ] || { pin_bad verify_pin "" ""; return 1; }
	[ -n "$PIN_RUNBOOK" ] || { pin_bad resync_runbook "" ""; return 1; }
	return 0
}

# --- the release stamp's fixed grammar ----------------------------------------
#
# `openrepotools/_release.py`, beside a package's payload (opensoft/openRepoTools
# #228; `specs/006-release-pypi/contracts/stamp.md` is the grammar and the
# refusal table, design.md Decision 5 the reason). BASH IS ITS ONE READER AT RUN
# TIME: the entry module reads nothing, so Python and bash can never disagree
# about a stamp. It is read by the rules below and NEVER SOURCED, IMPORTED OR
# RUN, because its docstring is free text and a `$(…)` in it is only text.
#
# EACH REASON IS ONE LINE THAT BEGINS WITH ITS CAUSE, `cannot be read (…)`,
# `is partial: …` or `is malformed: …`, so that `package_init`'s refusal reads
# as a sentence around it. Nothing here prints, on either stream: that refusal
# is the one line a broken stamp gets. And a refused stamp sets none of the
# five values, because a stamp is all or nothing (FR-010) and is never read as
# the part it holds. The five, the state and the reason are set here, so that
# nothing in the environment can set them.

STAMP_STATE=""   # unstamped | stamped, once `stamp_read` has accepted a stamp
STAMP_VERSION="" STAMP_TAG="" STAMP_COMMIT="" STAMP_DIGEST="" STAMP_REPOSITORY=""
STAMP_WHY=""     # why `stamp_read` refused, one line
#: THE THREE LETTERS HATCH'S `(?i)` FOLDS ONTO ONE OF `version`'s, beyond ASCII's
#: own capitals: U+017F onto `s`, and U+0130 and U+0131 onto `i`. They are every
#: such letter, counted with Python's `re` over the whole of Unicode. Written as
#: their UTF-8 bytes, so the guard folds them in any locale.
STAMP_FOLD_S="$(printf '\305\277')"
STAMP_FOLD_I_DOT="$(printf '\304\260')"
STAMP_FOLD_I_DOTLESS="$(printf '\304\261')"

stamp_number() {   # <text> — 0 for a decimal number with no leading zero
	case "$1" in '' | *[!0123456789]* | 0?*) return 1 ;; esac
	return 0
}

#: `X.Y.Z` or `X.Y.ZrcN`, each number with no leading zero: contracts/stamp.md's
#: pattern, by `case` and parameter expansion. Not `=~`, whose quoting bash 3.2
#: reads differently from bash 4, and whose `[0-9]` is a range.
stamp_version_ok() {   # <version>
	local v="$1" rest="" z=""
	case "$v" in
	*rc*)
		stamp_number "${v##*rc}" || return 1
		v="${v%rc*}"
		;;
	esac
	rest="${v#*.}"
	[ "$rest" != "$v" ] && stamp_number "${v%%.*}" || return 1
	z="${rest#*.}"
	[ "$z" != "$rest" ] && stamp_number "${rest%%.*}" && stamp_number "$z"
}

#: THE DOCSTRING GUARD (rule 3): a line that begins, in any case, with `version`
#: or `__version__`, then optional spaces and `=`. Hatch's version source takes
#: the first line ANYWHERE in the file that its pattern matches, docstring or
#: not, so such a line would give the wheel one version and `--version`
#: another. "Any case" is hatch's own, so the three letters it folds are folded
#: here first; the guard is wider than hatch's pattern, which also wants a quote.
stamp_hatch_reads() {   # <docstring line> — 0 when hatch could read it as the version
	local s="$1"
	s=${s//$STAMP_FOLD_S/s}
	s=${s//$STAMP_FOLD_I_DOT/i}
	s=${s//$STAMP_FOLD_I_DOTLESS/i}
	case "$s" in
	[Vv][Ee][Rr][Ss][Ii][Oo][Nn]*) s="${s#???????}" ;;
	__[Vv][Ee][Rr][Ss][Ii][Oo][Nn]__*) s="${s#???????????}" ;;
	*) return 1 ;;
	esac
	while :; do case "$s" in ' '*) s="${s#?}" ;; *) break ;; esac; done
	case "$s" in '='*) return 0 ;; esac
	return 1
}

stamp_names() {   # <name>… — them in prose with their verb: `A is`, `A and B are`, `A, B and C are`
	local out="${1-}" verb=is
	shift || :
	while [ $# -gt 0 ]; do
		verb=are
		if [ $# -eq 1 ]; then out="$out and $1"; else out="$out, $1"; fi
		shift
	done
	printf '%s %s\n' "$out" "$verb"
}

#: contracts/stamp.md's rules 1 to 5, line by line, then the checks after the
#: read. `pin_parse`'s shape: each refusal returns at once with its reason, and
#: the five values are copied out only once the whole file has passed.
stamp_read() {   # <file> — STAMP_STATE, the five STAMP_* values and 0, or STAMP_WHY and 1
	local line="" rest="" name="" value="" n=0 doc=none doc_line=0 guarded=0
	local seen=" " field="" ref="" missing="" set_names="" partial=""
	local f_VERSION="" f_TAG="" f_COMMIT="" f_DIGEST="" f_REPOSITORY=""
	local tq='"""' sep=' = "' dq='"'
	local shape='is not NAME = "VALUE" in column 0, with one space either side of ='
	STAMP_STATE="" STAMP_VERSION="" STAMP_TAG="" STAMP_COMMIT="" STAMP_DIGEST=""
	STAMP_REPOSITORY="" STAMP_WHY=""
	# A FILE THAT CANNOT BE READ is said before any read is tried. A directory
	# opens on Linux and fails at its first `read`, with bash's own line on
	# stderr; a FIFO would wait for ever.
	if [ -d "$1" ]; then
		STAMP_WHY="cannot be read (it is a directory)"; return 1
	elif [ ! -e "$1" ]; then
		STAMP_WHY="cannot be read (there is no such file)"; return 1
	elif [ ! -f "$1" ]; then
		STAMP_WHY="cannot be read (it is not a regular file)"; return 1
	elif [ ! -r "$1" ]; then
		STAMP_WHY="cannot be read (it is not readable by this user)"; return 1
	fi
	while IFS= read -r line || [ -n "$line" ]; do
		n=$((n + 1))
		# RULE 1, BEFORE EVERY OTHER: on a `#` line and in the docstring too.
		case "$line" in *"$ORT_CR"*) STAMP_WHY="is malformed: line $n has a carriage return"; return 1 ;; esac
		# RULE 3, INSIDE THE DOCSTRING: every line is skipped. The first that
		# hatch could read as the version is held until the docstring closes,
		# so that one which never closes is refused as that, and not as one of
		# the assignments it swallowed.
		if [ "$doc" = open ]; then
			if [ "$guarded" -eq 0 ] && stamp_hatch_reads "$line"; then guarded=$n; fi
			case "$line" in *"$tq"*) doc=closed ;; esac
			if [ "$doc" = closed ] && [ "$guarded" -ne 0 ]; then
				STAMP_WHY="is malformed: line $guarded, in the docstring, would be read by hatch as the version"
				return 1
			fi
			continue
		fi
		# RULE 2: a blank line, or a `#` in column 0, before the docstring as
		# well as after it.
		case "$line" in '#'*) continue ;; esac
		rest="$line"
		while :; do case "$rest" in ' '* | "$ORT_TAB"*) rest="${rest#?}" ;; *) break ;; esac; done
		[ -n "$rest" ] || continue
		# RULE 3, ITS OPENING: the one line beginning `"""` before any
		# assignment. It closes on that line when a second `"""` follows.
		case "$line" in
		"$tq"*)
			if [ "$doc" = none ] && [ "$seen" = " " ]; then
				doc=open doc_line=$n
				case "${line#"$tq"}" in *"$tq"*) doc=closed ;; esac
				continue
			fi
			;;
		esac
		# RULE 4: `NAME = "VALUE"` and nothing else, NAME one of the five and
		# VALUE free of `"`, `\` and `$`.
		case "$line" in
		*"$sep"*"$dq") ;;
		*) STAMP_WHY="is malformed: line $n $shape"; return 1 ;;
		esac
		name="${line%%"$sep"*}"
		value="${line#*"$sep"}"
		value="${value%"$dq"}"
		case "$name" in
		VERSION | TAG | COMMIT | DIGEST | REPOSITORY) ;;
		'' | [0123456789]* | *[!${ORT_ALNUM}_]*) STAMP_WHY="is malformed: line $n $shape"; return 1 ;;
		*) STAMP_WHY="is malformed: line $n assigns $name, which is not one of the stamp's five names"; return 1 ;;
		esac
		case "$value" in
		*"$dq"* | *\\* | *'$'*)
			STAMP_WHY="is malformed: line $n has a quote, a backslash or a dollar sign in its value"; return 1 ;;
		esac
		# RULE 5: each of the five exactly once.
		case "$seen" in *" $name "*) STAMP_WHY="is malformed: $name is assigned twice, the second time on line $n"; return 1 ;; esac
		seen="$seen$name "
		case "$name" in
		VERSION) f_VERSION="$value" ;;
		TAG) f_TAG="$value" ;;
		COMMIT) f_COMMIT="$value" ;;
		DIGEST) f_DIGEST="$value" ;;
		REPOSITORY) f_REPOSITORY="$value" ;;
		esac
	done 2>/dev/null <"$1" || { STAMP_WHY="cannot be read (it could not be opened)"; return 1; }
	if [ "$doc" = open ]; then
		STAMP_WHY="is malformed: the docstring opened on line $doc_line never closes"; return 1
	fi
	for field in VERSION TAG COMMIT DIGEST REPOSITORY; do
		case "$seen" in *" $field "*) ;; *) missing="$missing $field" ;; esac
	done
	if [ -n "$missing" ]; then
		STAMP_WHY="is malformed: $(stamp_names $missing) missing"; return 1
	fi
	# UNSTAMPED is TAG, COMMIT and DIGEST all empty, and then VERSION is 0.0.0
	# and REPOSITORY is empty: a REPOSITORY with no commit names nothing.
	if [ -z "$f_TAG$f_COMMIT$f_DIGEST" ]; then
		[ "$f_VERSION" = 0.0.0 ] || partial="VERSION is ${f_VERSION:-empty} rather than 0.0.0"
		[ -z "$f_REPOSITORY" ] || partial="${partial:+$partial and }REPOSITORY is $f_REPOSITORY"
		if [ -n "$partial" ]; then
			STAMP_WHY="is partial: $partial, but TAG, COMMIT and DIGEST are empty"; return 1
		fi
		STAMP_STATE=unstamped
	else
		# STAMPED is everything else, and then every field is set and has the
		# one shape its rule gives it.
		for field in VERSION TAG COMMIT DIGEST REPOSITORY; do
			ref="f_$field"
			if [ -n "${!ref}" ]; then set_names="$set_names $field"; else missing="$missing $field"; fi
		done
		if [ -n "$missing" ]; then
			STAMP_WHY="is partial: $(stamp_names $set_names) set, but $(stamp_names $missing) empty"; return 1
		fi
		if [ "$f_VERSION" = 0.0.0 ]; then
			STAMP_WHY="is malformed: VERSION is 0.0.0, the unstamped version, and TAG, COMMIT and DIGEST are set"; return 1
		fi
		stamp_version_ok "$f_VERSION" ||
			{ STAMP_WHY="is malformed: VERSION is $f_VERSION, not X.Y.Z or X.Y.ZrcN with no leading zeros"; return 1; }
		[ "$f_TAG" = "v$f_VERSION" ] || { STAMP_WHY="is malformed: TAG is $f_TAG, not v$f_VERSION"; return 1; }
		is_lower_hex 40 "$f_COMMIT" ||
			{ STAMP_WHY="is malformed: COMMIT is $f_COMMIT, not 40 lowercase hex digits"; return 1; }
		is_lower_hex 64 "$f_DIGEST" ||
			{ STAMP_WHY="is malformed: DIGEST is $f_DIGEST, not 64 lowercase hex digits"; return 1; }
		is_slug "$f_REPOSITORY" ||
			{ STAMP_WHY="is malformed: REPOSITORY is $f_REPOSITORY, not an owner/name repository"; return 1; }
		STAMP_STATE=stamped
	fi
	STAMP_VERSION="$f_VERSION" STAMP_TAG="$f_TAG" STAMP_COMMIT="$f_COMMIT"
	STAMP_DIGEST="$f_DIGEST" STAMP_REPOSITORY="$f_REPOSITORY"
	return 0
}

# --- one round trip, `gh` first and the API by `curl` second ------------------
#
# The same order as `fetch_from_repo`, for the same reason, and for the same
# identities. Each sets API_WHY to what BOTH answered when neither did, which
# is what a refusal prints.

api_said() {   # <tool> <its stderr> — its first line, as one clause
	local first=""
	IFS= read -r first <"$2" 2>/dev/null || :
	case "$first" in
	'') printf '%s failed and said nothing\n' "$1" ;;
	"$1:"*) printf '%s\n' "$first" ;;
	*) printf '%s: %s\n' "$1" "$first" ;;
	esac
}

#: A COMMIT OF THE CURRENT SOURCE, BY REF: API_SHA and 0, or API_WHY and 1. An
#: answer that is not 40 lowercase hex is no answer — `gh` prints a failed
#: request's JSON on stdout.
api_sha() {   # <ref, URL-escaped>
	local out="$WORKDIR/api-sha" why="" sha=""
	API_SHA="" API_WHY=""
	if command -v gh >/dev/null 2>&1; then
		if gh api "repos/${PAYLOAD_REPO:-$REPO}/commits/$1" -H 'Accept: application/vnd.github.sha' \
			>"$out" 2>"$out.err"; then
			IFS= read -r sha <"$out" || :
			if is_lower_hex 40 "$sha"; then API_SHA="$sha"; return 0; fi
			why="gh: an answer that is not a commit"
		else
			why="$(api_said gh "$out.err")"
		fi
	else
		why="gh: not installed"
	fi
	if command -v curl >/dev/null 2>&1; then
		if curl -fsSL -H 'Accept: application/vnd.github.sha' \
			"https://api.github.com/repos/${PAYLOAD_REPO:-$REPO}/commits/$1" >"$out" 2>"$out.err"; then
			sha=""
			IFS= read -r sha <"$out" || :
			if is_lower_hex 40 "$sha"; then API_SHA="$sha"; return 0; fi
			why="$why; curl: an answer that is not a commit"
		else
			why="$why; $(api_said curl "$out.err")"
		fi
	else
		why="$why; curl: not installed"
	fi
	API_WHY="$why"
	return 1
}

tree_listing_ok() {   # <file> — one or more `path<TAB>blob|tree|commit<TAB>40-hex` rows
	[ -s "$1" ] || return 1
	awk -F'\t' 'NF != 3 || ($2 != "blob" && $2 != "tree" && $2 != "commit") ||
		length($3) != 40 || $3 ~ /[^0123456789abcdef]/ { bad = 1 }
		END { exit bad ? 1 : 0 }' "$1"
}

#: A TREE OF THE CURRENT SOURCE, ONE LEVEL: the listing in <dest> and 0, or
#: API_WHY and 1.
api_tree() {   # <commit or tree sha> <dest>
	local out="$2" why=""
	API_WHY=""
	if command -v gh >/dev/null 2>&1; then
		if gh api "repos/${PAYLOAD_REPO:-$REPO}/git/trees/$1" --jq "$ORT_TREE_JQ" >"$out" 2>"$out.err"; then
			tree_listing_ok "$out" && return 0
			why="gh: an answer that is not a tree listing"
		else
			why="$(api_said gh "$out.err")"
		fi
	else
		why="gh: not installed"
	fi
	if ! command -v curl >/dev/null 2>&1; then
		why="$why; curl: not installed"
	elif ! command -v jq >/dev/null 2>&1; then
		why="$why; curl: not asked, because reading api.github.com's JSON needs \`jq\` and there is none"
	elif ! curl -fsSL "https://api.github.com/repos/${PAYLOAD_REPO:-$REPO}/git/trees/$1" >"$out.json" 2>"$out.err"; then
		why="$why; $(api_said curl "$out.err")"
	elif jq -r "$ORT_TREE_JQ" "$out.json" >"$out" 2>/dev/null && tree_listing_ok "$out"; then
		return 0
	else
		why="$why; curl: an answer that is not a tree listing"
	fi
	API_WHY="$why"
	return 1
}

tree_entry() {   # <listing> <name> — "<type> <sha>" of that entry, or 1
	awk -F'\t' -v name="$2" '$1 == name { print $2 " " $3; found = 1; exit }
		END { exit found ? 0 : 1 }' "$1"
}

url_ref() {   # <ref> — with the two characters a URL path reads otherwise escaped
	local r="$1" out=""
	while [ -n "$r" ]; do
		case "$r" in
		'%'*) out="$out%25" ;;
		'#'*) out="$out%23" ;;
		*) out="$out${r:0:1}" ;;
		esac
		r="${r#?}"
	done
	printf '%s' "$out"
}

# --- the two questions every remote run asks first ----------------------------

#: IS <path> IN THE CURRENT SOURCE? Asked exactly the way `fetch_from_repo`
#: asks for a payload file — `gh api …/contents` first, the raw URL second — and
#: of the same source, which is `$REPO` at `$REF` until an adopted assembly's
#: own commit is known, so the question today's layout is asked needs no host
#: and no tool its install did not already need. 0: it is there, its bytes in
#: <dest>. 3: it is not, which is GitHub's own 404 (`gh` names the status in
#: what it prints; the raw URL's is read with `-w`). 1: no answer either way.
#: PROBE_WHY says what each transport said, for 3 and for 1.
probe_path() {   # <path> <dest>
	local path="$1" dest="$2" status="" why=""
	PROBE_WHY=""
	if command -v gh >/dev/null 2>&1; then
		if gh api "repos/${PAYLOAD_REPO:-$REPO}/contents/$path?ref=${PAYLOAD_REV:-$REF}" \
			-H 'Accept: application/vnd.github.raw' >"$dest" 2>"$dest.err"; then
			return 0
		fi
		why="$(api_said gh "$dest.err")"
		case "$why" in *'HTTP 404'*) PROBE_WHY="$why"; return 3 ;; esac
	else
		why="gh: not installed"
	fi
	if command -v curl >/dev/null 2>&1; then
		if status="$(curl -sSL -o "$dest" -w '%{http_code}' "https://raw.githubusercontent.com/${PAYLOAD_REPO:-$REPO}/${PAYLOAD_REV:-$REF}/$path" 2>"$dest.err")"; then
			case "$status" in
			200) return 0 ;;
			404) PROBE_WHY="$why; curl: HTTP 404"; return 3 ;;
			esac
			why="$why; curl: HTTP ${status:-with no status}"
		else
			why="$why; $(api_said curl "$dest.err")"
		fi
	else
		why="$why; curl: not installed"
	fi
	PROBE_WHY="$why"
	return 1
}

#: WHICH LAYOUT `$REPO` HAS AT `$REF`: PROBE_LAYOUT single, adopted or unknown,
#: with PROBE_WHY for unknown. One request, and it refuses nothing; what each
#: answer means is its callers' (`payload_resolve_remote`, `wip_shape_ref`).
payload_probe() {
	[ -z "$PROBE_LAYOUT" ] || return 0
	local rc=0
	workdir
	probe_path contracts/code-pin.yaml "$WORKDIR/probe-code-pin" || rc=$?
	case "$rc" in
	0) PROBE_LAYOUT=adopted ;;
	3) PROBE_LAYOUT=single ;;
	*) PROBE_LAYOUT=unknown ;;
	esac
}

#: A RESOLUTION THAT FAILED, refused in the words of the run it failed: an
#: `--install`, which has placed nothing yet, or `wip init`'s step 7, which
#: comes after its repository was made and cloned, so "nothing was written"
#: would not be true there.
resolve_refuse() {   # <what happened>
	case "$RESOLVE_FOR" in
	wip) die "$1
    Step 7 seeded nothing: no template file was written into the checkout and
    nothing was staged. What the steps before it made is kept as it is, and a
    re-run picks up from here." ;;
	*) die "$1
    NOTHING was installed and nothing already installed was replaced." ;;
	esac
}

# --- the remote source, resolved once ---------------------------------------

payload_resolve_remote() {
	[ -z "$SOURCE_LAYOUT" ] || return 0
	local listing contracts pin entry link
	workdir
	payload_probe
	case "$PROBE_LAYOUT" in
	single)
		# TODAY'S LAYOUT, READ AS TODAY: `$REPO` at `$REF` as given, file by
		# file, by the transports that always fetched it.
		PAYLOAD_REPO="$REPO" PAYLOAD_REV="" SOURCE_LAYOUT=single
		return 0 ;;
	unknown)
		resolve_refuse "could not fetch contracts/code-pin.yaml from $REPO at $REF, the file that
    says whether it is a single repository or an adopted assembly, and a failed
    answer is a refusal, never a reading of the layout. What answered:
        $PROBE_WHY
    Both ways were tried:
        gh api repos/$REPO/contents/contracts/code-pin.yaml?ref=$REF
        curl -sSL https://raw.githubusercontent.com/$REPO/$REF/contracts/code-pin.yaml" ;;
	esac
	# AN ADOPTED ASSEMBLY: everything again, at ONE commit.
	listing="$WORKDIR/source-root.tsv"
	contracts="$WORKDIR/source-contracts.tsv"
	pin="$WORKDIR/code-pin.yaml"
	is_slug "$REPO" || resolve_refuse "\$OPENREPOTOOLS_REPO is '$REPO', which is not an owner/name repository,
    so the code its contracts/code-pin.yaml names cannot be asked for."
	# THE CURRENT SOURCE IS $REPO until its own pin says otherwise.
	PAYLOAD_REPO="$REPO" PAYLOAD_REV=""
	api_sha "$(url_ref "$REF")" || resolve_refuse "could not resolve $REPO at $REF to a commit. What answered:
        $API_WHY
    Both were asked:
        gh api repos/$REPO/commits/$REF -H 'Accept: application/vnd.github.sha'
        curl -fsSL -H 'Accept: application/vnd.github.sha' https://api.github.com/repos/$REPO/commits/$REF
    An adopted assembly is installed from ONE commit, and a failed answer is a
    refusal, never a reading of the repository's layout."
	SOURCE_SHA="$API_SHA" PAYLOAD_REV="$API_SHA"
	api_tree "$SOURCE_SHA" "$listing" || resolve_refuse "could not list $REPO at $SOURCE_SHA ($REF), so the gitlink its code pin
    must agree with cannot be read. What answered:
        $API_WHY"
	entry=""
	if entry="$(tree_entry "$listing" contracts)" && [ "${entry%% *}" = tree ]; then
		api_tree "${entry#* }" "$contracts" || resolve_refuse "could not list contracts/ in $REPO at $SOURCE_SHA ($REF). What answered:
        $API_WHY"
		entry="$(tree_entry "$contracts" code-pin.yaml)" || entry=""
	else
		entry=""
	fi
	[ -n "$entry" ] || resolve_refuse "$REPO carried contracts/code-pin.yaml at $REF when it was first asked,
    and $SOURCE_SHA, the commit $REF named a moment later, does not: the ref moved
    between the two reads, so this run would not be installing one answer.
    Re-run once it has settled."
	[ "${entry%% *}" = blob ] || resolve_refuse "contracts/code-pin.yaml in $REPO at $SOURCE_SHA is a ${entry%% *}, not a
    file."
	# ASKED BY `probe_path`, not `fetch_from_repo`, so that a refusal can say
	# what answered and not only what was asked (#191's review, lane 2's (d)).
	probe_path contracts/code-pin.yaml "$pin" ||
		resolve_refuse "could not fetch contracts/code-pin.yaml from $REPO at $SOURCE_SHA ($REF), which
    the listing shows is there. What answered:
        $PROBE_WHY
    Both were asked:
        gh api repos/$REPO/contents/contracts/code-pin.yaml?ref=$SOURCE_SHA
        curl -sSL https://raw.githubusercontent.com/$REPO/$SOURCE_SHA/contracts/code-pin.yaml"
	pin_parse "$pin" || resolve_refuse "contracts/code-pin.yaml in $REPO at $SOURCE_SHA ($REF) is not a code pin
    this installer can follow: $PIN_WHY. A pin that is present and wrong is
    refused rather than read around."
	link="$(tree_entry "$listing" "$PIN_SUBMODULE_PATH")" || link=""
	[ "${link%% *}" = commit ] || resolve_refuse "$REPO at $SOURCE_SHA ($REF) records no gitlink at $PIN_SUBMODULE_PATH, where
    contracts/code-pin.yaml mounts the code leg."
	[ "${link#* }" = "$PIN_COMMIT" ] || resolve_refuse "$REPO at $SOURCE_SHA ($REF) records ${link#* } at $PIN_SUBMODULE_PATH
    and its contracts/code-pin.yaml says $PIN_COMMIT. The gitlink and the pin move
    in ONE commit (the lockstep rule), so this assembly does not say which code it
    means."
	# THE PIN HAS PASSED: the source is now the repository it names.
	PAYLOAD_REPO="$PIN_SOURCE_REPOSITORY" PAYLOAD_REV="$PIN_COMMIT"
	if ! api_sha "$PIN_COMMIT" || [ "$API_SHA" != "$PIN_COMMIT" ]; then
		resolve_refuse "contracts/code-pin.yaml in $REPO at $SOURCE_SHA ($REF) pins $PIN_SOURCE_REPOSITORY
    at $PIN_COMMIT, and that is not a commit $PIN_SOURCE_REPOSITORY answers for.
    What answered:
        ${API_WHY:-$PIN_SOURCE_REPOSITORY answered $API_SHA}"
	fi
	SOURCE_LAYOUT=adopted
}

payload_where() {   # the source collected from, for a refusal to name
	case "$SOURCE_LAYOUT" in
	single) printf '%s at %s' "$REPO" "$REF" ;;
	adopted) printf '%s at %s (the code %s at %s pins)' "$PAYLOAD_REPO" "$PAYLOAD_REV" "$REPO" "$REF" ;;
	*) printf '%s at %s' "$REPO" "$REF" ;;
	esac
}

# --- the local source ---------------------------------------------------------

#: DOES <pin> MOUNT ITS CODE LEG AT <name>? Its `submodule_path:` line, read
#: with `pin_scalar`'s grammar (quotes, a trailing `# comment`) and, where that
#: grammar refuses the value, leniently — the strict read is `pin_parse`'s, once
#: the pin is known to be this checkout's — so that another project's pin,
#: however it is written, is only ever compared with this directory's name, and
#: a valid pin of this checkout's is never missed for the way it is written.
pin_mounts() {   # <pin> <name>
	local line value
	while IFS= read -r line || [ -n "$line" ]; do
		case "$line" in
		submodule_path:*)
			value="${line#submodule_path:}"
			value="${value%"$ORT_CR"}"
			if pin_scalar "$value" && [ -n "$PIN_SCALAR" ] && [ "$PIN_SCALAR" = "$2" ]; then
				return 0
			fi
			value="${value#"${value%%[! $ORT_TAB]*}"}"
			value="${value%"${value##*[! $ORT_TAB]}"}"
			value="${value#[\"\']}"
			value="${value%[\"\']}"
			[ -n "$value" ] && [ "$value" = "$2" ]
			return
			;;
		esac
	done <"$1"
	return 1
}

#: THE ASSEMBLY <dir> IS A CODE LEG OF, in one of the two layouts the design
#: names and no other: `<root>/<submodule_path>`, the mounted leg, or
#: `<root>/worktrees/<NNN>/<submodule_path>`, a paired feature worktree — each
#: with `<root>/contracts/code-pin.yaml` naming that `<submodule_path>`. A pin
#: anywhere else above <dir> is another project's (#191's review: workBenches
#: vendors a complete tree deep inside whatever holds it). Logical paths, so
#: what is printed is what the person typed; the one comparison that must see
#: through links (`payload_local_adopted`) asks `pwd -P` itself.
assembly_above() {   # <dir>
	local name parent up
	name="${1##*/}"
	parent="$(dirname -- "$1")"
	if [ -f "$parent/contracts/code-pin.yaml" ] &&
		pin_mounts "$parent/contracts/code-pin.yaml" "$name"; then
		printf '%s\n' "$parent"
		return 0
	fi
	up="$(dirname -- "$parent")"
	[ "${up##*/}" = worktrees ] || return 1
	up="$(dirname -- "$up")"
	if [ -f "$up/contracts/code-pin.yaml" ] &&
		pin_mounts "$up/contracts/code-pin.yaml" "$name"; then
		printf '%s\n' "$up"
		return 0
	fi
	return 1
}

#: THE COMMIT A SUPERPROJECT RECORDS AT <path>: the index first and HEAD
#: second, the order openRepoShape's `recorded_gitlink` asks them in — the index
#: is what the next commit records, so a gitlink staged and not yet committed is
#: the one that counts.
recorded_gitlink() {   # <superproject> <path>
	# `read`, not `awk`: the offline path needs nothing a minimal PATH lacks.
	local listed mode oid stage kind path
	listed="$(git -C "$1" ls-files -s -- "$2" 2>/dev/null)" || listed=""
	read -r mode oid stage path <<<"$listed" || :
	if [ "$mode" != 160000 ] || [ "$stage" != 0 ] || [ "$path" != "$2" ]; then
		listed="$(git -C "$1" ls-tree HEAD -- "$2" 2>/dev/null)" || listed=""
		read -r mode kind oid path <<<"$listed" || :
		[ "$mode" = 160000 ] && [ "$kind" = commit ] && [ "$path" = "$2" ] || return 1
	fi
	is_lower_hex 40 "$oid" || return 1
	printf '%s\n' "$oid"
}

payload_local_adopted() {   # <assembly root> <the directory this file runs from>
	local root="$1" here="$2" pin="$1/contracts/code-pin.yaml" me mount link name blob snap
	local bad="" missing=""
	pin_parse "$pin" || die "$pin is not a code pin this installer can follow: $PIN_WHY.
    It is the pin of the assembly this checkout sits in, and a pin that is present
    and wrong is refused rather than read around. NOTHING was installed and nothing
    already installed was replaced."
	me="$(cd -- "$here" && pwd -P)"
	mount="$(cd -- "$root/$PIN_SUBMODULE_PATH" 2>/dev/null && pwd -P)" || mount=""
	if [ "$mount" != "$me" ]; then
		payload_local_feature "$root" "$here"
		return 0
	fi
	link="$(recorded_gitlink "$root" "$PIN_SUBMODULE_PATH")" ||
		die "$root records no gitlink at $PIN_SUBMODULE_PATH, where its contracts/code-pin.yaml
    mounts this checkout, so which code it pins cannot be checked. NOTHING was
    installed and nothing already installed was replaced."
	[ "$link" = "$PIN_COMMIT" ] || die "$root records $link at $PIN_SUBMODULE_PATH and its
    contracts/code-pin.yaml says $PIN_COMMIT. The gitlink and the pin move in ONE
    commit (the lockstep rule), so this assembly does not say which code it means.
    NOTHING was installed and nothing already installed was replaced. Make them
    agree — \`make validate\` at $root names the drift — then re-run."
	git -C "$here" cat-file -e "$PIN_COMMIT^{commit}" 2>/dev/null ||
		die "the pinned commit $PIN_COMMIT is not in $here's object store, so its
    payload cannot be checked against it. NOTHING was installed and nothing already
    installed was replaced. Fetch it — git -C \"$here\" fetch origin — or run
    \`make bootstrap\` at $root, then re-run."
	# WHAT IS CHECKED IS WHAT IS INSTALLED: each payload file is copied into
	# this run's work directory FIRST, and the copy is what is hashed against
	# the pinned commit and what `collect_commands`/`collect_skills` later read
	# — so an edit to the checkout between the check and the copy cannot put
	# unchecked bytes in place (#191, Copilot's first round).
	workdir
	snap="$WORKDIR/pinned-payload"
	for name in $(payload_names); do
		if [ -L "$here/$name" ]; then bad="${bad:+$bad }$name"; continue; fi
		if [ ! -f "$here/$name" ]; then missing="${missing:+$missing }$name"; continue; fi
		blob="$(git -C "$here" rev-parse --verify --quiet "$PIN_COMMIT:$name" 2>/dev/null)" ||
			{ bad="${bad:+$bad }$name"; continue; }
		mkdir -p -- "$snap/$(dirname -- "$name")" &&
			cp -p -- "$here/$name" "$snap/$name" ||
			{ bad="${bad:+$bad }$name"; continue; }
		[ "$(git -C "$here" hash-object --no-filters -- "$snap/$name" 2>/dev/null)" = "$blob" ] ||
			bad="${bad:+$bad }$name"
	done
	[ -z "$missing" ] || die "the code mounted at $root/$PIN_SUBMODULE_PATH is missing $missing,
    so it is not the payload $root pins ($PIN_SOURCE_REPOSITORY at $PIN_COMMIT), and
    an incomplete local set is never completed from a network. NOTHING was
    installed and nothing already installed was replaced. Restore them —
        git -C \"$here\" checkout $PIN_COMMIT -- $missing
    — or run \`make bootstrap\` at $root, then re-run."
	[ -z "$bad" ] || die "in the code mounted at $root/$PIN_SUBMODULE_PATH, $bad
    differ from $PIN_COMMIT, the commit $root pins, so these are not the pinned
    bytes. NOTHING was installed and nothing already installed was replaced. To
    install the pinned code, restore them:
        git -C \"$here\" checkout $PIN_COMMIT -- $bad
    To install a code feature on purpose, run the openRepoTools of its own worktree
    (<root>/worktrees/<NNN>/code), which installs that checkout's bytes and says so."
	PAYLOAD_MODE=local PAYLOAD_DIR="$snap" SOURCE_LAYOUT=adopted
	say "openRepoTools: source: the code mounted at $root/$PIN_SUBMODULE_PATH, $PIN_SOURCE_REPOSITORY at $PIN_COMMIT as $root pins it; every payload file was checked against that commit and nothing fetched"
}

payload_local_feature() {   # <assembly root> <checkout>
	local root="$1" here="$2" name missing="" top="" head="" dirty=""
	for name in $(payload_names); do
		[ -f "$here/$name" ] || missing="${missing:+$missing }$name"
	done
	[ -z "$missing" ] || die "$here is a code checkout under the assembly $root and not
    the code it mounts, so \`--install\` from it installs THIS checkout's bytes or
    none — and it is missing $missing. NOTHING was installed and nothing already
    installed was replaced. Restore them in $here, or install the code $root pins
    with $root/openRepoTools --install."
	top="$(git -C "$here" rev-parse --show-toplevel 2>/dev/null)" || top=""
	if [ -n "$top" ] && [ "$(cd -- "$top" && pwd -P)" = "$(cd -- "$here" && pwd -P)" ]; then
		head="$(git -C "$here" rev-parse --verify --quiet HEAD 2>/dev/null)" || head=""
		# shellcheck disable=SC2046
		[ -z "$(git -C "$here" status --porcelain -- $(payload_names) 2>/dev/null)" ] ||
			dirty=" with uncommitted changes to its payload"
	fi
	PAYLOAD_MODE=local PAYLOAD_DIR="$here" SOURCE_LAYOUT=feature
	say "openRepoTools: source: the code checkout $here at ${head:-a commit this installer could not read (it is not a git checkout of its own)}$dirty, installed because its own openRepoTools was run — NOT the code $root pins, $PIN_SOURCE_REPOSITORY at $PIN_COMMIT"
}

# --- the one decision `--install` makes ---------------------------------------

payload_select() {
	[ -z "$PAYLOAD_MODE" ] || return 0
	local selfdir="" root name missing=""
	if [ -f "$SELF" ]; then
		selfdir="$(cd -- "$(dirname -- "$SELF")" && pwd)"
		if root="$(assembly_above "$selfdir")"; then
			payload_local_adopted "$root" "$selfdir"
			return 0
		fi
		for name in $(payload_names); do
			[ -f "$selfdir/$name" ] || missing="${missing:+$missing }$name"
		done
		# EVERY PAYLOAD FILE IS BESIDE THIS ONE: copied, and nothing is asked
		# of any network — the run is today's to the line (workBenches runs it
		# with a sentinel repository precisely so that only this can succeed).
		if [ -z "$missing" ]; then
			PAYLOAD_MODE=local PAYLOAD_DIR="$selfdir"
			return 0
		fi
		payload_resolve_remote
		# A SINGLE REPOSITORY KEEPS ITS SIBLING RULE: what is beside this file
		# is copied and only what is missing is fetched, at `$REF`, as it was.
		if [ "$SOURCE_LAYOUT" = single ]; then
			PAYLOAD_MODE=siblings PAYLOAD_DIR="$selfdir"
			return 0
		fi
		# AN ADOPTED SOURCE IS NEVER TOPPED UP: the files beside an installed
		# copy are whatever an earlier run left, and pinned skills beside them
		# would be two versions in one install.
		PAYLOAD_MODE=remote
		say "openRepoTools: source: $REPO at $REF ($SOURCE_SHA) pins $PAYLOAD_REPO at $PAYLOAD_REV; the files beside $selfdir are not used, because an incomplete local set is never topped up with pinned files"
		return 0
	fi
	payload_resolve_remote
	PAYLOAD_MODE=remote
	[ "$SOURCE_LAYOUT" = single ] ||
		say "openRepoTools: source: $REPO at $REF ($SOURCE_SHA) pins $PAYLOAD_REPO at $PAYLOAD_REV"
}

# THE SEVENTEEN FILES `--install` PLACES. One list, so the installer, the fetch
# fallback and the tests cannot disagree about what a complete install is.
# `openRepoTools` is first because it is the one a person types to get the
# others.
#
# EVERY ONE OF THEM IS RESOLVED BY BARE NAME AT THE REPOSITORY ROOT — `cp --
# "$selfdir/$name"` below, and `fetch_from_repo "$name"` — which is why
# Amendment 9's move landed the four lane helpers and the table at the root and
# nowhere else.
#
# `repos.tsv` IS DATA AND IS PLACED WITH THE COMMANDS, at 755 like everything
# else here. That is one honest ugliness — a data file carrying an executable
# bit it has no use for — and the cost of the alternative was a second
# destination and a second mode in this file, in the loop below, and again in
# workBenches' `setup-estate-commands.sh`, whose post-install verify refuses any
# target whose mode is not exactly 755. Amendment 9(b) chose the ugliness.
#
# ELEVEN SINCE AMENDMENT 11. `restart` and `lanes` joined the list under
# RATIFIED DECISIONS 7 and 6 (2026-09-13T18:05:29Z, "Ratify all four" and "Yes,
# build `lanes` in A11"), and `openRepoTools --install` owns them because THIS
# IS THE ONLY INSTALLER IN THE ESTATE THAT PUTS A WORD ON PATH. Adoption act 6
# vendors skills and command files through workBenches' `setup-claude-profiles.sh`
# into the shared commands directory and `~/.claude/commands/`, and neither of
# those is a `PATH` entry — which is exactly what decision 7 point 8 says.
#
# ELEVEN STILL, AND ONE NAME CHANGED: `lane` takes `restart`'s place under
# AMENDMENT 18 ADDENDUM 1 (the word, ratified 2026-09-14T14:05:54Z) and ADDENDUM
# 2 (the retirement, ratified 2026-09-14T16:50:32Z, verbatim "ratify" on Brett
# Heap's ruling *"i think we can drop restart as a cli command and keep it
# inside a claude session with /restart … lane does all the things a user
# wants"*). `lane <name>` is `restart <lane>`'s act to the byte — the launcher's
# path, the lane's own recorded directory and profile, asking nothing — and adds
# the pick, the attach and the handoff branch. The count did not move because
# one word left as another arrived, which is why nothing below it is a number.
# AND TWELVE SINCE AMENDMENT 17(a): `lane-handoff` is the swap's shell form,
# placed beside the rest because it is a word a person has on PATH (#47).
# THIRTEEN SINCE AMENDMENT 16, ratified 2026-09-14T09:24:35Z verbatim "Ratify as
# drafted (Recommended)": `lane-rename <old> <new>` is the word Brett Heap asked
# for that day, and clause (h) is why it is a word rather than a `lanes` option —
# `lanes` writes nothing, and a rename is four files in one commit. Its write is
# `lanes-edit.sh rename-lane`; the word exists because the tmux window and the
# session's own name are not a register writer's to touch (clause (f)).
# FIFTEEN SINCE opensoft/workBenches#119, on Brett Heap's home ruling of
# 2026-09-29 ("this work is really for openRepoTools repo"): `claude-current`
# is the Claude Code a launch starts — npm's published version, verified by
# `--version` and named by absolute path — which `lane-start` and workBenches'
# `claude-profile` call, and `claude-restart-check` is the RESTART NEEDED line
# the shared status line prints for a session whose binary moved on disk. They
# are placed at the END of the list, so every index a test takes into it holds.
# SIXTEEN SINCE opensoft/openRepoTools#160 (lane-collision-protocol Amendment 14,
# ratified by Brett Heap 2026-10-05, verbatim "ratify 48"): `lanes-index` keeps
# the DERIVED INDEX of the lane register — `sync`, `reconcile`, `status` — which
# `lanes-edit.sh` nudges after a write lands and which `lanes --index` and
# `lanes-edit.sh history --index` read; nothing that acts reads it. It is the one
# command in the list that is Python rather than bash, and it is placed at the
# END like the two before it, for the same reason.
# SEVENTEEN SINCE opensoft/openRepoTools#162 (Brett Heap's word of 2026-10-05,
# "build 162"): `lane-worktrees sweep <lane>` retires a lane's abandoned
# worktrees, branches, scratch and killed-suite sandboxes - a dry run unless
# `--yes`, and nothing removed that is not first on origin or in a bundle. It is
# Python like `lanes-index`, and at the END for the same reason.
INSTALLABLES=(openRepoTools park resume status lane lanes lane-handoff lane-rename lanes-edit.sh lane-start lane-end link-estates repos.tsv claude-current claude-restart-check lanes-index lane-worktrees)

# ------------------------------------------ THE RECEIPT OF WHAT IT PLACED
#
# opensoft/openRepoTools#57, and it descends from COPILOT'S ROUND-3 FINDING ON
# #45 (`openRepoTools:263`) — which is a finding about the retirement directly
# below this section:
#
#     "This ownership test is only a searchable content substring, not proof
#     that this installer created the file. A user-owned `restart` script
#     copied from an old installation or containing the same banner will
#     satisfy `grep` and be deleted on the next install […] Use durable
#     ownership metadata or another check that distinguishes installer output
#     from a user file before removing it."
#
# IT WAS DECLINED FOR `restart`, AND THE REASON DOES NOT GENERALISE. No
# installer that ever placed `restart` wrote a receipt, so no receipt can
# identify the copies that are on people's PATHs today: for that population the
# header marker is the only evidence that exists, and it stays exactly where it
# is. What #57 asks is that EVERY RETIREMENT AFTER THIS ONE have better — which
# costs nothing except that this run write down what it did.
#
# SO IT WRITES DOWN WHAT IT PLACED, one row per regular file:
#
#     <name><TAB><destination><TAB><sha256><TAB><UTC>
#
# and the name is the word this command PRINTED beside that placement — `park`,
# `handoff`, `/ctx` — because one name is a skill AND a command file at four
# different paths, and a row's subject is therefore its DESTINATION — spelled as
# its CANONICAL ABSOLUTE PATH (`receipt_dest`), so that a relative
# `$OPENREPOTOOLS_BIN_DIR` means the same file to the run that wrote the row and
# to every later run from any other directory. A run
# REPLACES the row of every destination it placed and KEEPS every other row, so
# a person who moves `$OPENREPOTOOLS_BIN_DIR` keeps the evidence for the copies
# in the old one, which are the copies a later retirement will meet there.
#
# THE ROWS ARE READ OFF DISK, FROM THE SAME LISTS THE PLACEMENTS ARE MADE FROM,
# after the last of them. Nothing is remembered from inside the loops: the
# planners have already refused every target that is not a regular file this
# user can write, the loops have written them, and what a receipt must carry is
# the digest of the bytes that ARE THERE. That is also why this is one call in
# `install_commands` and not a line inside each loop — the smallest hunk in the
# two functions this estate's other branches are editing.
#
# IT IS NOT AN ARTIFACT OF THE COUNT. The count is the invariant of what
# `--install` PLACES; this is the record of that placement. A ROW GOES TO EVERY
# PLACED REGULAR FILE — the `INSTALLABLES` plus the skill files and the command
# files at both of their destinations — and to nothing else: the two hook
# entries get no row, because an entry inside somebody else's JSON file is not a
# file this command placed, and the receipt carries no row for itself.
#
# THE NUMBER THAT COMES OUT IS DERIVED, NOT STATED, because every statement of
# it went stale the next time a list moved (Copilot on #103, `openRepoTools:325`,
# which found twenty-six where there were twenty-seven): `ARTIFACTS - HOOK_ENTRIES`
# in `tests/test_openrepotools_command.py`, which is `len(INSTALLED) + 2 *
# len(SKILL_NAMES) + 2 * len(COMMAND_NAMES)`. Today that is 31 artifacts, 29 of
# them files, 29 rows — 17 + 6 + 6, since opensoft/openRepoTools#162 put
# `lane-worktrees` in `INSTALLABLES` — and it moves with `INSTALLABLES`, `SKILLS`
# and `COMMANDS`, and with nothing else.
#
# A RECEIPT THAT CANNOT BE WRITTEN IS A NOTE AND NEVER A REFUSAL (#57's own
# words, and the same rule the retirement follows one function down). The
# commands, the skills and the hook entries are what a person asked for; a
# record of them this installer could not write is its problem and not theirs,
# and the marker is still there for a retirement to fall back to. Fail closed,
# out loud, in one line, and carry on.
#
# AND THE RECEIPT IS ONLY EVER ASKED ABOUT A PATH THIS COMMAND COMPUTED. It is
# never read as a list of paths to remove: `retire_commands` walks `RETIRED`
# against its own `$dir` and asks the receipt one question about the target it
# already has in hand — is the file at THIS path the one I wrote. So a receipt
# somebody else authored can change an answer and can never name a file for
# removal, which is the difference between a record and an instruction.

#: WHERE THE RECEIPT LIVES. `$OPENREPOTOOLS_DATA_DIR`, else the XDG data
#: directory — `$XDG_DATA_HOME`, else `~/.local/share`. It is DATA and does not
#: belong beside the commands in `$OPENREPOTOOLS_BIN_DIR`: a bin directory is a
#: PATH entry, and a `.tsv` in one is a file every shell walks past for ever
#: (`repos.tsv` is there under protest, and Amendment 9(b) says so). XDG is the
#: one convention that answers "where does a tool keep its own record" on Linux
#: and macOS alike, and the override is there because the bin directory has one.
#:
#: THE DIRECTORY AS CONFIGURED IS ONE THING AND THE PATH THIS FILE READS AND
#: WRITES IS ANOTHER (Copilot round 4 on #103, `openRepoTools:365`). The first
#: shape handed the configured string to every caller, so a relative
#: `$OPENREPOTOOLS_DATA_DIR` was `data/installed.tsv` in every line and every
#: `[ -f ]`: a spelling, not a file. `receipt_file` is the CANONICAL path — the
#: directory resolved by `receipt_dest`, the same `pwd -P` the destinations in
#: the rows go through — whenever the directory is THERE TO RESOLVE, and the
#: configured spelling otherwise: a directory that does not exist has no
#: receipt in it to read, and the one caller that MAKES it (`receipt_replace`)
#: creates it first and asks again. A getter that created directories would
#: make a retirement's read a write.
#:
#: WHAT RESOLVING DOES NOT DO is make a relative `data` mean the same directory
#: from two working directories: that is what being relative IS. So
#: `receipt_record` says so, on the line beside the one that names the
#: receipt, rather than leaving a person to find out from a later run that
#: could not see the first one's evidence.
receipt_data_dir() {   # the directory as configured, which may be relative
	printf '%s\n' "${OPENREPOTOOLS_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/openRepoTools}"
}
receipt_file() {
	receipt_dest "$(receipt_data_dir)/installed.tsv"
}

#: A TAB AND A NEWLINE AS VALUES, because the receipt is a TSV and a
#: destination carrying either would split one row into two columns or into two
#: rows. `$'\t'` is the shorter spelling and no file in this repository uses
#: one; `printf` is the spelling every one of them does use. The `X` is there
#: because `$( )` strips TRAILING newlines and there would otherwise be nothing
#: left to assign.
RECEIPT_TAB="$(printf '\t')"
RECEIPT_NEWLINE="$(printf '\nX')"
RECEIPT_NEWLINE="${RECEIPT_NEWLINE%X}"

RECEIPT_ROWS=""   # this run's rows, staged in $WORKDIR
RECEIPT_DESTS=""  # their destinations, one per line, for the merge
RECEIPT_STAMP=""  # the UTC of this run
RECEIPT_COUNT=0   # how many rows it wrote
RECEIPT_WHY=""    # why it could not, where it could not

# THE DIGEST, IN WHICHEVER SPELLING THIS MACHINE HAS. `sha256sum` is GNU and a
# stock macOS ships none; `shasum -a 256` is in the Perl toolchain macOS does
# ship, and is on every Linux this estate runs. Neither is a hard dependency —
# a machine with neither gets the note and the marker — so this is the one
# place either name is spelled.
#
# THE FILE IS FED ON STDIN AND NEVER NAMED AS AN OPERAND, and that is not a
# style choice: GNU `sha256sum` ESCAPES a filename containing a backslash or a
# newline and prefixes the whole line with `\`, so `${out%% *}` off a named
# operand hands back `\<digest>` for a bin directory somebody spelled with a
# backslash in it. Read from stdin, both spellings print `<digest>  -`.
sha256_of() {   # <path> — its digest, or 1 and nothing at all
	local out=""
	[ -f "$1" ] && [ -r "$1" ] || return 1
	if command -v sha256sum >/dev/null 2>&1; then
		out="$(sha256sum <"$1" 2>/dev/null)" || return 1
	elif command -v shasum >/dev/null 2>&1; then
		out="$(shasum -a 256 <"$1" 2>/dev/null)" || return 1
	else
		return 1
	fi
	out="${out%% *}"
	[ -n "$out" ] || return 1
	printf '%s\n' "$out"
}

# THE ONE LINE A RECEIPT THAT COULD NOT BE WRITTEN GETS. It says where, why,
# that the install is not affected, and what a retirement falls back to — and
# it is `say` and not `die`, which is the whole of #57's ruling on this.
#
# THE EXIT BELONGS TO THE REASON AND NOT TO THE LINE, which is why it is a
# sentence a caller puts in its `why` rather than a tail on every note: a
# machine with no `sha256sum` and no `shasum` is not a machine that wants to be
# told to name a different directory.
RECEIPT_ELSEWHERE='name a directory this installer may write with $OPENREPOTOOLS_DATA_DIR'
receipt_note() {   # <file> <why>
	say "receipt: NOT written to $1 — $2. Everything above is installed; a later retirement falls back to the \`Installed on PATH by\` header, which is the evidence it has today."
}

# THE DESTINATION AS THE RECEIPT SPELLS IT: ABSOLUTE, AND WITH EVERY SYMLINK IN
# ITS DIRECTORY RESOLVED (Copilot on #103, `openRepoTools:524`). A row is
# evidence about ONE FILE, and `$OPENREPOTOOLS_BIN_DIR=bin` — which the planner
# accepts — names a different file from every directory it is run in: the first
# shape stored `$dir/$name` as supplied, so a row written from one working
# directory was compared, as a string, against the `bin/restart` of another, and
# that other file was hashed and could be removed. So the one spelling every
# read and write goes through is the PHYSICAL path of the file's directory plus
# its own name — `record`, `verdict` and `forget` all ask THIS, never the
# `$dir/$name` they were handed, and the line a person reads still prints the
# path as they spelled it.
#
# THE DIRECTORY IS RESOLVED, NEVER THE FILE: `receipt_add` has already refused a
# symlink at the path itself, and a link is somebody's decision about their own
# PATH, which is no business of a row's. `(cd -- "$dir" && pwd -P)` is the
# portable spelling (macOS and bash 3.2 have no `realpath`, and `readlink -f`
# is not in a stock macOS userland — `lanes-edit.sh` asks the same question the
# same way). `CDPATH` is emptied first, because a `cd` through one PRINTS the
# directory it went to and the substitution would hold two lines. And the
# trailing `X` is the one `RECEIPT_NEWLINE` uses: `$( )` strips trailing
# newlines, and a directory whose name ENDS in one would otherwise resolve to a
# DIFFERENT, shorter path — one with no newline in it, past the TSV guard.
#
# IT NEVER FAILS: a directory that cannot be entered answers with the path as
# given, which is not absolute, and the one caller that WRITES a row refuses a
# destination that is not — the callers that only READ simply never match it.
receipt_dest() {   # <path> — its canonical absolute spelling, else the path as given
	local dir="" base="" real=""
	case "$1" in
	*/*) dir="${1%/*}"; base="${1##*/}" ;;
	*) dir="."; base="$1" ;;
	esac
	[ -n "$dir" ] || dir="/"
	real="$(CDPATH= cd -- "$dir" >/dev/null 2>&1 && pwd -P && printf X)" ||
		{ printf '%s\n' "$1"; return 0; }
	real="${real%X}"
	real="${real%"$RECEIPT_NEWLINE"}"
	case "$real" in
	/) printf '/%s\n' "$base" ;;
	*) printf '%s/%s\n' "$real" "$base" ;;
	esac
}

# WHETHER A PATH CANNOT BE A COLUMN OF THE RECEIPT: it carries a tab or a newline.
receipt_tsv_unsafe() {   # <path> — 0 if it cannot, 1 if it can
	case "$1" in
	*"$RECEIPT_TAB"*|*"$RECEIPT_NEWLINE"*) return 0 ;;
	esac
	return 1
}

# ONE ROW, STAGED. The digest is of the bytes at the destination, so a run that
# placed nothing new still records what is there — which is the point: the
# receipt answers "is the file at this path mine", not "did I write it today".
#
# A DESTINATION CARRYING A TAB OR A NEWLINE GETS NO RECEIPT AT ALL, rather than
# a row that is silently two columns or two rows. `$OPENREPOTOOLS_BIN_DIR` is a
# path a person chooses — the retirement one function down already prints its
# `rm` quoted because of the spaces people put in them — and a TSV has no
# escape. Failing the whole receipt rather than dropping the row is the fail-
# closed half: a receipt missing exactly the file a retirement will ask about,
# with nothing on screen, is worse than no receipt at all.
receipt_add() {   # <name> <destination> — 0, or 1 with $RECEIPT_WHY set
	local name="$1" given="$2" dest="" digest=""
	[ -f "$given" ] && [ ! -L "$given" ] || return 0
	# THE TAB AND NEWLINE QUESTION IS ASKED OF BOTH SPELLINGS — the one the person
	# supplied, BEFORE anything resolves it, and the canonical one, which a symlink
	# in the directory can make carry a tab the supplied one never had.
	if receipt_tsv_unsafe "$given"; then
		RECEIPT_WHY="$given has a tab or a newline in it and this file is tab-separated"
		return 1
	fi
	dest="$(receipt_dest "$given")"
	if receipt_tsv_unsafe "$dest"; then
		RECEIPT_WHY="$dest has a tab or a newline in it and this file is tab-separated"
		return 1
	fi
	case "$dest" in
	/*) ;;
	*)
		RECEIPT_WHY="$given could not be resolved to an absolute path"
		return 1
		;;
	esac
	digest="$(sha256_of "$dest")" || {
		RECEIPT_WHY="$dest could not be digested"
		return 1
	}
	printf '%s\t%s\t%s\t%s\n' "$name" "$dest" "$digest" "$RECEIPT_STAMP" \
		>>"$RECEIPT_ROWS" || {
		RECEIPT_WHY="the rows could not be staged in $WORKDIR"
		return 1
	}
	printf '%s\n' "$dest" >>"$RECEIPT_DESTS" || {
		RECEIPT_WHY="the rows could not be staged in $WORKDIR"
		return 1
	}
	RECEIPT_COUNT=$((RECEIPT_COUNT + 1))
	return 0
}

# EVERY ROW OF THE RECEIPT WHOSE DESTINATION IS NOT ONE OF THESE, appended to a
# staged file. ONE implementation for the two callers that need it — the merge,
# which keeps what this run did not replace, and the retirement, which drops
# the row of the file it removed — because two copies of one parse is how the
# two come to disagree about what a row is.
#
# A ROW IS FOUR FIELDS OR MORE, and anything else in that file is ignored
# rather than repaired: a comment somebody added, a truncated line, a column a
# later version appends. Reading "at least four" rather than "exactly four" is
# what lets a fifth column arrive without this half having to be taught about
# it first.
#
# AND A ROW WHOSE FILE IS NO LONGER AT THAT PATH IS NOT KEPT (Copilot round 1
# on #103). `receipt_forget` runs AFTER the `rm` it accompanies, so a write
# that fails in the instant between them leaves the receipt naming a path this
# installer's copy has left — and nothing would ever clear it. This makes that
# window CLOSE BY ITSELF: the next `--install` reads the same file, finds that
# file gone, and drops the row. It is also what stops the receipt growing for
# ever with the bin directories and profile roots people delete.
#
# WHAT IS KEPT IS WHAT COULD HAVE BEEN WRITTEN: a REGULAR FILE THAT IS NOT A
# LINK, which is `receipt_add`'s own test one function up, spelled the same way
# (Copilot round 2 on #103, `openRepoTools:468`). The first shape of this asked
# `[ -e ] || [ -L ]` — "is there anything at all at that path" — which kept the
# row of a destination that had become a directory, a socket, a FIFO or a
# symlink. A row is evidence about a REGULAR FILE this installer placed, and
# where one of those sits at that path the file it is evidence about is not
# there; the two halves now say that in one sentence each rather than two
# different ones.
receipt_rows_except() {   # <receipt> <destinations file> <staged file>
	local row="" rest="" dest=""
	[ -f "$1" ] && [ ! -L "$1" ] || return 0
	# A RECEIPT THAT EXISTS AND CANNOT BE OPENED IS A FAILURE, NOT AN EMPTY ONE
	# (Copilot round 4 on #103, `openRepoTools:575`). The `done <"$1"` below
	# fails quietly when it cannot open the file, and the `return 0` this
	# function ends in then reported a clean scan of no rows — so
	# `receipt_record` replaced the receipt with this run's rows alone and every
	# row about a copy this run did not place was gone. OPENED, not `-r`: the
	# answer that matters is the one the loop is about to get, and for root those
	# two agree anyway. Both callers already print their note on a non-zero.
	: 2>/dev/null <"$1" || return 1
	while IFS= read -r row || [ -n "$row" ]; do
		case "$row" in
		*"$RECEIPT_TAB"*"$RECEIPT_TAB"*"$RECEIPT_TAB"*) ;;
		*) continue ;;
		esac
		rest="${row#*"$RECEIPT_TAB"}"
		dest="${rest%%"$RECEIPT_TAB"*}"
		# A RELATIVE DESTINATION IS NEVER KEPT (Copilot on #103, `openRepoTools:524`).
		# This command writes none any more, and a row that says `bin/park` is a
		# row about whichever directory the next run is standing in: the `-f` just
		# below would answer for THAT run's `bin/park`, which is the cwd-dependence
		# `receipt_dest` removes from every other read.
		case "$dest" in
		/*) ;;
		*) continue ;;
		esac
		if grep -qFx -e "$dest" -- "$2"; then continue; fi
		[ -f "$dest" ] && [ ! -L "$dest" ] || continue
		printf '%s\n' "$row" >>"$3" || return 1
	done <"$1"
	return 0
}

# THE WRITE ITSELF: `mktemp` in the same directory, `mv -f`. It is the shape
# `place_skill_and_hook` writes `settings.json` with and for the same reason —
# a half-written receipt is a receipt that says a file is not this installer's.
# AND NO `chmod`: the receipt's mode is the 0600 `mktemp` gives the temporary,
# whatever the umask, which is `settings.json`'s mode too. Two rules meet here
# and this is how both are kept. #48 (PR #102) rules that EVERY `chmod` this
# command performs fails through `die` — a mode-stamp failure is a refusal —
# and `tests/test_openrepotools_command.py` reads the script's text to hold
# every site to it; #57 rules that a receipt this command cannot write is a
# NOTE and never a refused install. A stamp that may not `die` and may not
# fall silent is a stamp that must not exist, and 0600 wants none: this is one
# account's evidence, read by that account's own retirements, and nothing else
# reads it.
#
# `mv -f` DOES NOT FOLLOW A SYMLINK — it replaces the link itself — which is
# why `receipt_record` refuses one BEFORE reaching this: silently swapping a
# person's link for a regular file is the same act `cp` through one is, in the
# other direction.
receipt_replace() {   # <staged file> — 0, or 1 having changed nothing
	local file="" dir="" tmp=""
	file="$(receipt_file)"
	dir="$(dirname -- "$file")"
	mkdir -p -- "$dir" 2>/dev/null || return 1
	# THE DIRECTORY EXISTS NOW, so the canonical path can be had (round 4 on
	# #103, `openRepoTools:365`): on a first install the answer above was still
	# the configured spelling.
	file="$(receipt_file)"
	dir="$(dirname -- "$file")"
	tmp="$(mktemp -- "$dir/installed.tsv.XXXXXX" 2>/dev/null)" || return 1
	cp -- "$1" "$tmp" 2>/dev/null || { rm -f -- "$tmp"; return 1; }
	mv -f -- "$tmp" "$file" 2>/dev/null || { rm -f -- "$tmp"; return 1; }
	return 0
}

# THE RECEIPT FOR THIS RUN, from the same three lists the placements are made
# from. Called once, after the last placement and before the one removal.
receipt_record() {
	local dir="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}"
	local file="" kind="" name="" target="" why=""
	file="$(receipt_file)"
	# WHAT IS AT THAT PATH, BEFORE A BYTE OF IT IS READ OR WRITTEN — the same
	# question `plan_install_targets` and `plan_skill_targets` put to every
	# path this command writes, asked through the same `path_kind`. A symlink,
	# a directory or anything else there is not this installer's receipt: it is
	# neither read nor replaced, and the note says which it found.
	if kind="$(path_kind "$file")"; then
		receipt_note "$file" "it is $kind, and this installer's receipt is a regular file it makes itself — $RECEIPT_ELSEWHERE"
		return 0
	fi
	# AND WHETHER THERE IS ANYTHING HERE TO DIGEST WITH, asked once and named as
	# the cause it is. Without this the first file would fail `sha256_of` and
	# the note would say that FILE could not be digested — true, and not the
	# sentence a person needs to read on a machine that has neither tool.
	if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then
		receipt_note "$file" "there is no \`sha256sum\` and no \`shasum -a 256\` on this PATH, and a row without a digest is a row that proves nothing"
		return 0
	fi
	workdir
	RECEIPT_ROWS="$WORKDIR/receipt-rows"
	RECEIPT_DESTS="$WORKDIR/receipt-dests"
	RECEIPT_COUNT=0
	RECEIPT_WHY=""
	RECEIPT_STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
	: >"$RECEIPT_ROWS" 2>/dev/null || { receipt_note "$file" "its rows could not be staged in $WORKDIR"; return 0; }
	: >"$RECEIPT_DESTS" 2>/dev/null || { receipt_note "$file" "its rows could not be staged in $WORKDIR"; return 0; }
	for name in "${INSTALLABLES[@]}"; do
		receipt_add "$name" "$dir/$name" || { receipt_note "$file" "$RECEIPT_WHY"; return 0; }
	done
	# THE SKILLS AND THE COMMAND FILES TOO, under the name each was PRINTED
	# with: they are regular files this run placed, at paths it chose, and a
	# record of what was placed that left them out — every one a path a
	# retirement could one day meet — would answer only part of the question
	# anybody asks it.
	for name in "${SKILLS[@]}"; do
		for target in "$(skills_home)/shared/skills/$name/SKILL.md" "$(claude_home)/skills/$name/SKILL.md"; do
			receipt_add "$name" "$target" || { receipt_note "$file" "$RECEIPT_WHY"; return 0; }
		done
	done
	for name in "${COMMANDS[@]}"; do
		for target in "$(skills_home)/shared/commands/$name.md" "$(claude_home)/commands/$name.md"; do
			receipt_add "/$name" "$target" || { receipt_note "$file" "$RECEIPT_WHY"; return 0; }
		done
	done
	# THIS RUN'S ROWS FIRST, THEN EVERY EARLIER ROW IT DID NOT REPLACE.
	receipt_rows_except "$file" "$RECEIPT_DESTS" "$RECEIPT_ROWS" ||
		{ receipt_note "$file" "the rows already there could not be read"; return 0; }
	if receipt_replace "$RECEIPT_ROWS"; then
		file="$(receipt_file)"
		say "receipt: $RECEIPT_COUNT placed files recorded in $file"
		case "$(receipt_data_dir)" in
		/*) ;;
		*)
			say "  the data directory $(receipt_data_dir) is a RELATIVE path, which names a different directory from every working directory — this run resolved it to $(dirname -- "$file"). Name an absolute one with \$OPENREPOTOOLS_DATA_DIR so a later \`--install\` from anywhere reads this receipt."
			;;
		esac
	else
		why="$(unusable_dir_kind "$(dirname -- "$file")")" || why="the write failed"
		receipt_note "$file" "$why — $RECEIPT_ELSEWHERE"
	fi
	return 0
}

# WHAT THE RECEIPT SAYS ABOUT ONE PATH, in one word, and never an exit code: a
# `$( )` that returned non-zero under `set -e` would take the install with it,
# and "I have nothing to say about this file" is an ANSWER here rather than a
# failure.
#
#   placed  — a row names this path and the bytes there still have its digest.
#             This installer's copy beyond argument.
#   edited  — a row names it and the digest has MOVED. Somebody's edit of an
#             installed command, or their own file at a path one used to be:
#             either way not this installer's to remove.
#   unknown — no receipt, or no row for THIS path. The header marker is what
#             answers then, exactly as it does today.
#   unverified — a row names it and NOTHING COULD BE DIGESTED to compare with:
#             no `sha256sum` and no `shasum` on this PATH, a tool that failed, or
#             a file this user cannot read (Copilot on #103, `openRepoTools:711`).
#             It is NOT `unknown`, and the difference is the whole of #57: the
#             header marker is the answer for a path the receipt has NOTHING TO
#             SAY about, and a path it HAS a row for and cannot check is a path
#             it has something to say about and cannot say yet. The first shape
#             of this folded the two into one word, so the marker removed an
#             edited, receipt-tracked command on exactly the machine that could
#             not tell it was edited — the ownership mistake the receipt exists
#             to prevent, made by the code that carries it. Named and left.
#   unreadable — the receipt EXISTS and cannot be OPENED (Copilot round 4 on
#             #103, `openRepoTools:716`). The scan never ran, so "no row" is not
#             something it found: it is the same ignorance as `unverified`, one
#             step earlier, and it gets the same answer — named and left —
#             with its own sentence, because the person's remedy is the
#             receipt's mode and not a digest tool. `unknown` is for a scan that
#             RAN and found nothing.
#
# THE FIRST MATCHING ROW WINS. This command writes one row per destination, so
# a second is a hand-edit, and picking the first is the only rule that does not
# depend on which end of the file somebody appended to.
receipt_verdict() {   # <target> — placed | edited | unverified | unreadable | unknown, always 0
	local file="" row="" rest="" dest="" digest="" disk="" want=""
	file="$(receipt_file)"
	[ -f "$file" ] && [ ! -L "$file" ] || { printf 'unknown\n'; return 0; }
	: 2>/dev/null <"$file" || { printf 'unreadable\n'; return 0; }
	# THE ROW IS ABOUT A FILE, AND THE FILE IS ASKED ABOUT BY THE SAME SPELLING
	# THE ROW WAS WRITTEN IN (`receipt_dest`, Copilot on #103, `openRepoTools:524`).
	want="$(receipt_dest "$1")"
	while IFS= read -r row || [ -n "$row" ]; do
		case "$row" in
		*"$RECEIPT_TAB"*"$RECEIPT_TAB"*"$RECEIPT_TAB"*) ;;
		*) continue ;;
		esac
		rest="${row#*"$RECEIPT_TAB"}"
		dest="${rest%%"$RECEIPT_TAB"*}"
		[ "$dest" = "$want" ] || continue
		digest="${rest#*"$RECEIPT_TAB"}"
		digest="${digest%%"$RECEIPT_TAB"*}"
		disk="$(sha256_of "$1")" || { printf 'unverified\n'; return 0; }
		if [ "$disk" = "$digest" ]; then printf 'placed\n'; else printf 'edited\n'; fi
		return 0
	done <"$file"
	printf 'unknown\n'
	return 0
}

# AND THE ROW OF A FILE THAT IS NO LONGER THERE IS DROPPED, because a receipt
# that still names a removed path would answer `edited` about whatever somebody
# puts there next — which is the safe direction, and still a sentence about
# their file that is not true.
#
# NOT ONE OF ITS FAILURES IS SILENT (Copilot round 1 on #103, `openRepoTools:660`).
# Three of the four steps here used to `return 0` on a write that failed — a
# full disk while staging, a data directory that went away between the read and
# the write — so the receipt could keep a live row for a path the `rm` above had
# just emptied and NOTHING on screen would say so. Every one of them now reaches
# the same note, and the note names the repair: `receipt_rows_except` drops a
# row whose file is gone, so the next `--install` clears it whatever happened
# here. Still never a `die`: the retirement has already done its work.
receipt_forget() {   # <destination>
	local file="" kept="" dests="" dropped=1
	file="$(receipt_file)"
	[ -f "$file" ] && [ ! -L "$file" ] || return 0
	workdir
	kept="$WORKDIR/receipt-kept"
	dests="$WORKDIR/receipt-forget"
	# THE ROW IS DROPPED BY THE SPELLING IT WAS WRITTEN IN (`receipt_dest`); the
	# note below still names the path as the person spelled it.
	if : >"$kept" 2>/dev/null && printf '%s\n' "$(receipt_dest "$1")" >"$dests" 2>/dev/null &&
		receipt_rows_except "$file" "$dests" "$kept" &&
		receipt_replace "$kept"; then
		dropped=0
	fi
	[ "$dropped" = 0 ] ||
		receipt_note "$file" "its row for $1 could not be dropped, so it still names a path this installer's copy has left — the next \`--install\` drops it, because a row whose file is gone is not kept"
	return 0
}

# ---------------------------------------------- THE WORDS THAT WERE RETIRED
#
# A WORD THAT LEAVES THE LIST HAS TO LEAVE THE DIRECTORY (Copilot round 1 on
# #45, `openRepoTools:234`). Dropping `restart` from `INSTALLABLES` stops THIS
# run from copying it and does nothing at all about the copy the LAST run
# placed — so a workstation that took the install before lane-collision-protocol
# Amendment 18 Addendum 2 would keep `restart` on its PATH for ever, which is
# the one thing that addendum says must stop: *"`restart` is no longer a command
# a person is given"*, ratified 2026-09-14T16:50:32Z verbatim "ratify". An
# installer whose only act is to stop writing a file is not a retirement; it is
# a file nobody will ever clean up.
#
# AND IT REMOVES ONLY WHAT IT WROTE. The test is the header this installer puts
# in every command it owns — `Installed on PATH by ` + the installer's own name
# — so a person's own script that happens to be called `restart` is NAMED and
# LEFT EXACTLY AS IT IS, with the one line to run printed beside it. Deleting a
# file this command did not write is not an act an installer may take on
# somebody's behalf, and a retirement that did would be the worst possible way
# to learn the difference.
RETIRED=(restart)
retire_commands() {
	local dir="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}"
	local name target verdict=""
	for name in "${RETIRED[@]}"; do
		target="$dir/$name"
		# `-e` FOLLOWS THE LINK AND IS FALSE FOR A DANGLING ONE (Copilot round 4 on
		# #45, `openRepoTools:261`), so a `restart` SYMLINK whose target has since
		# gone — and the pre-move `link-estates` put exactly such links in a
		# workstation's `~/.local/bin`, pointing into a checkout that may no longer
		# be there — was NEITHER removed NOR named: it stayed on PATH, and the one
		# run that was going to mention it said nothing. `-L` is the other half of
		# "is there anything at this path at all"; what happens next is the same as
		# for any other link, which is that it is named and left.
		[ -e "$target" ] || [ -L "$target" ] || continue
		# `! -L` BEFORE `-f`, BECAUSE `-f` FOLLOWS THE LINK (Copilot round 2 on
		# #45). A person's own `restart` SYMLINK pointing at a file that happens
		# to carry this installer's header would otherwise be unlinked — and
		# `plan_install_targets` refuses a `-L` target for exactly that reason
		# (`unplaceable_kind`, which names the link rather than writing through
		# it). A link is somebody's decision about their own PATH; it is named
		# here and left, like every other file this installer did not write.
		#
		# THE RECEIPT IS ASKED FIRST, AND THE MARKER IS WHAT ANSWERS WHEN IT HAS
		# NOTHING TO SAY (#57, on Copilot's round-3 finding on #45). A row whose
		# digest still matches is this installer's copy beyond argument; a row
		# whose digest has MOVED is a file this installer may not remove even
		# though the header is still in it, because what the header proves is
		# where those lines came from and not who owns the bytes now. It is
		# asked only of a REGULAR FILE THAT IS NOT A LINK, for the reason the
		# `! -L` below gives: a link is somebody's decision about their own
		# PATH, whatever the receipt remembers about the path it sits at.
		verdict=unknown
		if [ ! -L "$target" ] && [ -f "$target" ]; then
			verdict="$(receipt_verdict "$target")"
		fi
		if [ "$verdict" = placed ]; then
			rm -f -- "$target"
			# AND THE ROW GOES WITH THE FILE, so the receipt never names a path
			# nothing of this installer's is at any more.
			receipt_forget "$target"
			say "$name: RETIRED, removed from $target — this installer's own receipt carries its digest (lane-collision-protocol Amendment 18 Addendum 2; \`lane <name>\` is that act now)"
		elif [ "$verdict" = edited ]; then
			say "$name: RETIRED by lane-collision-protocol Amendment 18 Addendum 2, and $target no longer holds the bytes this installer's receipt recorded — an edit of it, or your own file at that path. Left exactly as it is; remove it yourself if it is stale:  rm -f -- \"$target\""
		elif [ "$verdict" = unverified ]; then
			# A ROW IT CANNOT CHECK IS NEVER THE HEADER'S TO ANSWER (Copilot on #103,
			# `openRepoTools:711`): the receipt names this path, so the marker is not
			# the evidence — and the digest that would have settled it could not be
			# taken. One line, the same shape as the two above: where, why, the `rm`.
			say "$name: RETIRED by lane-collision-protocol Amendment 18 Addendum 2, and $target has a row in this installer's receipt that could not be checked, because no digest tool answered (\`sha256sum\`, \`shasum -a 256\`) — so it is NOT removed on the strength of its header either. Left exactly as it is; remove it yourself if it is stale:  rm -f -- \"$target\""
		elif [ "$verdict" = unreadable ]; then
			# A RECEIPT THAT CANNOT BE OPENED CANNOT SAY "NO ROW" (Copilot round 4 on
			# #103, `openRepoTools:716`): it may carry exactly the row that says this
			# file is an edit. Same shape as the arm above, and it names the receipt,
			# because that is the file whose mode the person has to look at.
			say "$name: RETIRED by lane-collision-protocol Amendment 18 Addendum 2, and this installer's receipt $(receipt_file) exists but could not be read, so whether $target is its copy is not known — it is NOT removed on the strength of its header either. Left exactly as it is; remove it yourself if it is stale:  rm -f -- \"$target\""
		elif [ ! -L "$target" ] && [ -f "$target" ] && grep -qF -e 'Installed on PATH by `openRepoTools --install`' -- "$target" 2>/dev/null; then
			rm -f -- "$target"
			say "$name: RETIRED, removed from $target (lane-collision-protocol Amendment 18 Addendum 2; \`lane <name>\` is that act now)"
		else
			# THE PRINTED REMOVAL IS QUOTED, AND IT IS THIS FILE'S OWN SPELLING
			# (Copilot round 9 on #45, `openRepoTools:281`): both planners print
			# `rm -f -- "<path>" "<path>"` out of a `paths` they build with the
			# quotes IN, because `OPENREPOTOOLS_BIN_DIR` is a path a person chooses
			# and a bare one with a space in it is a line that removes nothing. `-f`
			# for the reason those two give: what a removal needs is the DIRECTORY's
			# write bit, not the file's.
			say "$name: RETIRED by lane-collision-protocol Amendment 18 Addendum 2, and $target is NOT this installer's copy — left exactly as it is. Remove it yourself if it is stale:  rm -f -- \"$target\""
		fi
	done
}

# ALL OF THEM IN HAND BEFORE ANY OF THEM IS PLACED (openRepoShape #82, F10 of
# the review on its #83). One file at a time, dying on the first fetch that
# failed, leaves a person with a NEW `openRepoTools` and no `park` — a
# half-install that says `installed at` and is not one — and nothing on screen
# to say which were missing. Every one is collected into the temporary directory
# first: a failure there replaces NOTHING, names what was not fetched, and exits.
#
# NO NUMBER IS SPELLED IN THIS BLOCK, and that is the fix rather than a bigger
# number (#26, the review of `37632b1`, `openRepoTools:226`). It said "ALL FOUR"
# and "which of the four" while `INSTALLABLES` carried ELEVEN — the count grew
# under a comment stating the old one, which is the same defect as the stale
# artifact counts `2f44da0` corrected and as `lane-start`'s "no fifth rung". The
# code below already derives every count it prints from `${#INSTALLABLES[@]}`.
collect_commands() {
	local name missing=""
	# Run from a file, install THOSE bytes: `--install` then needs no network
	# and no `gh` at all — and `park`, `resume` and `status` sit BESIDE this
	# file, in the checkout and in the repository alike, so the siblings that
	# are there are copied and only a missing one is fetched. Run from stdin
	# (`curl … | bash -s -- --install`) there is no file to copy from at all,
	# so each one is fetched at this same ref. WHICH of those it is, and from
	# where, is `payload_select`'s one decision (#186), made before this runs.
	payload_select
	workdir
	STAGED="$WORKDIR/staged"
	mkdir -p "$STAGED"
	for name in "${INSTALLABLES[@]}"; do
		if [ -n "$PAYLOAD_DIR" ] && [ -f "$PAYLOAD_DIR/$name" ]; then
			cp -- "$PAYLOAD_DIR/$name" "$STAGED/$name"
			continue
		fi
		# A LOCAL SOURCE IS NEVER COMPLETED FROM A NETWORK: it was whole when
		# it was chosen, and a file gone since is missing, not fetched.
		if [ "$PAYLOAD_MODE" != local ]; then
			fetch_from_repo "$name" "$STAGED/$name" && continue
		fi
		missing="${missing:+$missing }$name"
	done
	[ -z "$missing" ] || die "could not fetch $missing from $(payload_where), so NOTHING was installed
    and nothing already installed was replaced. \`--install\` places all
    ${#INSTALLABLES[@]} files or none: a new openRepoTools beside a missing park is
    a half-install that reads like a whole one. Both ways were tried:
    gh api repos/${PAYLOAD_REPO:-$REPO}/contents/<file>?ref=${PAYLOAD_REV:-$REF}
    curl -fsSL https://raw.githubusercontent.com/${PAYLOAD_REPO:-$REPO}/${PAYLOAD_REV:-$REF}/<file>"
}

# WHAT MODE A PLACED ARTIFACT IS AT, asked without `stat`. GNU spells that
# `stat -c %a` and BSD `stat -f %Lp`, and nothing this repository ships may
# reach for one of the two without the other beside it — so `find` answers it
# instead, in the one spelling both agree on: with the octal preceded by
# neither `-` nor `+`, GNU and BSD `find` each evaluate `-perm` true only where
# the permission bits are EXACTLY that number.
#
# IT IS ASKED SO THAT A LINE CAN TELL THE TWO CASES APART. `--install` stamps
# the documented mode on every artifact whether or not the bytes changed
# (#40, findings 2 and 3), and a run that repaired a mode and reported
# `unchanged` is a run that told a person there was nothing to look at.
mode_is() {   # <octal> <path>
	[ -n "$(find "$2" -maxdepth 0 -perm "$1" -print 2>/dev/null)" ]
}

# WHO OWNS A PATH, as `<uid>:<gid>`, and nothing where it cannot be read —
# asked without `stat` for the reason `mode_is` gives. `ls -n` prints both as
# numbers, in its third and fourth fields, on GNU, BSD and busybox alike, and
# `-d` keeps the answer about the path itself. The `|| :` keeps a path `ls`
# cannot read an empty answer rather than a `pipefail` exit.
owner_of() {   # <path>
	{ ls -lnd -- "$1" 2>/dev/null || :; } | awk 'NR == 1 { print $3 ":" $4 }'
}

# EVERY FILE `--install` PLACES IS WRITTEN BESIDE ITS TARGET AND RENAMED OVER IT
# (opensoft/openRepoTools#167): a temporary in the target's own directory, the
# bytes, the mode, then `mv -f`. It was `cp` onto the target, which keeps the
# target's INODE and rewrites its bytes — and bash reads a script from its file
# while it runs it, at an offset, so a lane tool that was running when an
# install replaced it went on reading the NEW text from the OLD offset. Measured
# on 2026-09-30: `A.sh: line 4: ript,: command not found`, then the new text's
# commands run from its middle. `lanes-edit.sh`, `lane-start` and the landers
# run for minutes, so a repair install at a busy moment could corrupt a lane's
# register write mid-flight. A rename puts a NEW inode at the path in one step:
# a process that has the old one open reads the old text to its end, and the
# next one to start reads the new. The SAME directory, because a rename is
# atomic only within one filesystem, and the same shape the receipt and
# `settings.json` were already written in.
#
# THE MODE GOES ON THE TEMPORARY, so the target never exists at the wrong mode
# for even an instant, and a stamp that fails (#48) leaves whatever was at the
# target exactly as it was.
#
# EVERY FAILURE REMOVES THE TEMPORARY, then refuses naming the target, and a
# run killed while one exists leaves nothing behind either: `$PLACING` holds
# its PREFIX — `.<name>.openrepotools.<this pid>.` — from BEFORE `mktemp` runs
# until after the `mv`, and the `EXIT` trap removes whatever starts with it. A
# name the trap learnt only from `mktemp`'s answer had a window, between the
# file being made and the answer being assigned, in which a signal left it
# behind (Copilot round 2 on #172); the pid keeps the prefix this run's own.
# The leading dot keeps it out of a listing, and the suffix after the name
# keeps it out of every glob a reader of these directories takes (`*.md` in
# `commands/`).
#
# THE OWNER STAYS WHO IT WAS (Copilot on #172). A rename puts the TEMPORARY's
# inode at the path, and the temporary belongs to whoever runs this — so a root
# `--install` into another user's home, which this file goes out of its way to
# allow (search "WHAT `-w` DOES NOT PROVE"), would hand that user root-owned
# copies that their own next `--install` refuses as unwritable. `cp` kept the
# owner by keeping the inode; this keeps it by giving the temporary the
# target's uid and gid before the rename. Where that `chown` is refused and the
# OWNER differs — an account placing over a file another account owns, which
# only root may give away — the placement refuses rather than take the file
# over. Where only the GROUP differs and cannot be kept, it is placed anyway:
# at 755 and 644 the group's bits are the other bits, so nobody gains or loses
# a permission by it (Copilot round 2 on #172 asked that it not be silent, and
# this is why it may be). And an owner NOBODY COULD READ is not "nothing to
# keep": `owner_of` answers nothing where `ls` cannot say, which is also what an
# absent target gives, so a target that EXISTS with no readable owner is
# refused before the rename rather than handed this run's owner (Copilot on
# #172 at bef4b85).
#
# A DIRECTORY AT THE TARGET IS REFUSED HERE as well as in planning, because `mv`
# moves a file INTO a directory named as its destination rather than failing,
# and the planners are a phase earlier. Two installs that race (#106) each
# still place whole files; serializing them is #106's lock, not this.
place_by_rename() {   # <staged source> <target> <mode> <what a refusal adds>
	local source="$1" target="$2" mode="$3" rest="$4" tmp="" was="" now=""
	PLACING="$(dirname -- "$target")/.${target##*/}.openrepotools.$$."
	tmp="$(mktemp -- "${PLACING}XXXXXX")" ||
		{ rm -f -- "$PLACING"*; PLACING=""; die "could not create a temporary file beside $target, so it was not placed.
    $rest"; }
	cp -- "$source" "$tmp" ||
		{ rm -f -- "$tmp"; PLACING=""; die "could not write $target, so it was not placed and what was there is untouched.
    $rest"; }
	chmod "$mode" "$tmp" ||
		{ rm -f -- "$tmp"; PLACING=""; die "could not set mode $mode on $target, so it was not placed and what was there is untouched.
    $rest"; }
	was="$(owner_of "$target")"
	now="$(owner_of "$tmp")"
	if [ -z "$was" ] && { [ -e "$target" ] || [ -L "$target" ]; }; then
		rm -f -- "$tmp"
		PLACING=""
		die "could not read who owns $target, so it was not placed and what was there is untouched: a rename would give it this run's owner.
    $rest"
	fi
	if [ -n "$was" ] && [ "$was" != "$now" ] &&
		! chown -- "$was" "$tmp" 2>/dev/null &&
		[ "${was%%:*}" != "${now%%:*}" ]; then
		rm -f -- "$tmp"
		PLACING=""
		die "$target belongs to uid ${was%%:*}, and this run cannot give the new copy that owner, so it was not placed and what was there is untouched.
    $rest"
	fi
	if [ -d "$target" ]; then
		rm -f -- "$tmp"
		PLACING=""
		die "$target is a directory, so nothing was placed there: \`mv\` would have put the file inside it.
    $rest"
	fi
	mv -f -- "$tmp" "$target" ||
		{ rm -f -- "$tmp"; PLACING=""; die "could not move the new $target into place, so what was there is untouched.
    $rest"; }
	PLACING=""
}

install_commands() {
	local dir="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}"
	local name target source verb placed=0
	# ALL THIRTY-ONE IN HAND BEFORE ANY IS PLACED. The seventeen files, the
	# three skills, the three command files, and the TWO artifacts that are not
	# whole files — the two hook entries merged into one `settings.json` —
	# are all computed here, so a fetch that failed replaces nothing and a merge
	# that cannot be computed refuses having placed nothing. That last one is
	# the same rule `--install` already had, extended to cover a merge: an
	# installer that got half way through a person's settings is worse than one
	# that did not start. BOTH OUTCOMES OF THAT MERGE ARE IN HAND (#237) — the
	# pair added, and the pair deferred with our guard entry taken out — and
	# which one is written is asked only after the bin loop, because the path
	# the two entries call is a link to the `lanes-edit.sh` that loop places.
	#
	# THE TARGETS ARE ASKED ABOUT FIRST, before even the fetch: a target that
	# cannot be written THROUGH is a refusal that costs nothing at all here,
	# and a network round-trip if it waits (A9 Addendum 4, R-A9-12).
	plan_install_targets
	# THE SOURCE NEXT, AND ONCE (#186): which bytes, from where, at which
	# commit — every refusal in it costs nothing, because nothing is collected
	# or placed until it has answered.
	payload_select
	collect_commands
	collect_skills || die "could not fetch one of the ${#SKILLS[@]} skills (${SKILLS[*]}) or the ${#COMMANDS[@]} command file (${COMMANDS[*]}) from $(payload_where), so
    NOTHING was installed and nothing already installed was replaced."
	plan_hook_merge
	plan_skill_targets
	# EVERY FAILURE FROM HERE IS A `die`, and this was the one that was not
	# (#44 round 1, Sourcery's `README.md:237`). `plan_install_targets` has
	# proved the nearest existing ancestor writable, so what is left for this to
	# meet is a race or a full disk — and under `set -e` an unguarded `mkdir`
	# exits 1, which reads as a report of findings from a run that placed none.
	mkdir -p -- "$dir" || die "could not create $dir for the ${#INSTALLABLES[@]} commands.
    NOTHING was installed and nothing already installed was replaced."
	for name in "${INSTALLABLES[@]}"; do
		source="$STAGED/$name"
		target="$dir/$name"
		# PIPX'S LINK IS LEFT TO PIPX (#228, FR-016), as `plan_install_targets`
		# decided: nothing is placed over it or through it, it is not counted,
		# and its line says what updates it.
		if [ -n "$PIPX_LINK" ] && [ "$target" = "$PIPX_LINK" ]; then
			say "openRepoTools: left to pipx at $PIPX_LINK (a link into pipx's openrepotools environment); update it with \`pipx upgrade openrepotools\`, then run \`openRepoTools --install\` again"
			continue
		fi
		if [ -e "$target" ] && cmp -s "$source" "$target"; then
			# THE BYTES WERE RIGHT, WHICH IS NOT THE SAME AS NOTHING TO DO
			# (#40, finding 2). The `chmod` below has always run on this arm
			# too — the eleven are the one set this installer never got wrong —
			# but the line said `unchanged` over a mode it had just repaired.
			# The mode is asked BEFORE it is stamped, and the line says which
			# of the two happened.
			if mode_is 755 "$target"; then
				verb="already installed at $target (unchanged)"
			else
				verb="already installed at $target (mode restored to 755)"
			fi
			# STAMPED EVERY TIME, AND ITS FAILURE IS A REFUSAL (#48). A copy that
			# is not executable is not a command, and this was the ONE mode stamp
			# in this file with no `die` beside it: under `set -e` a failure here
			# left the shell's own 1, which this toolset spends on "findings were
			# printed", as the answer from a run that was refusing with the
			# commands before it already placed. On THIS arm it is stamped in
			# place — a mode is metadata, and no running copy reads it — and on
			# the other `place_by_rename` stamps the temporary (#167).
			#
			# AND NO OWNERSHIP PREFLIGHT IN FRONT OF IT, which is the decision
			# `unplaceable_kind` already carries — search "WHAT `-w` DOES NOT PROVE"
			# for the whole of it. `[ -O ]` is the question that would catch this
			# first, and it is a guard no test in this suite can reach and one that
			# would refuse a root `--install` into another user's home, where the
			# stamp does in fact succeed. R-A9-15 is the ruling that a guard nobody
			# can test is a guard nobody can rely on, so the stamp's own failure is
			# the refusal, naming the file it could not mode.
			chmod 755 "$target" ||
				die "could not set mode 755 on $target, so that command is not executable.
    Whatever the lines above report is placed; this one has its bytes and not its
    mode, and the skills, the command files and the hook entries are not placed at
    all."
		else
			if [ -e "$target" ]; then verb="updated at $target"; else verb="installed at $target"; fi
			# WRITTEN BESIDE ITSELF AND RENAMED OVER ITSELF, never rewritten in
			# place (#167): a lane tool running from this path finishes on the
			# text it started with. `place_by_rename` says why.
			place_by_rename "$source" "$target" 755 "Whatever the lines above report is placed; the skills, the command files and the
    hook entries are not placed at all."
		fi
		placed=$((placed + 1))
		say "$name: $verb"
	done
	# THE COUNT, out loud: a person reading eight lines cannot tell whether a
	# ninth was meant to be there.
	if [ -z "$PIPX_LINK" ]; then
		say "openRepoTools: $placed of ${#INSTALLABLES[@]} placed in $dir"
	elif pipx_own_run; then
		# BESIDE PIPX'S LINK the count says where the missing one is
		# (contracts/cli-output.md), and from pipx's own environment that is
		# all it says.
		say "openRepoTools: $placed of ${#INSTALLABLES[@]} placed in $dir, and 1 left to pipx"
	else
		# ANY OTHER RUN BESIDE IT — a checkout's, the one-line install's, a
		# package outside pipx's venv — also says where the other sixteen now
		# come from and which release is beside the link (research R5,
		# FR-019): `install_source` and `pipx_release` say why each is spelt so.
		say "openRepoTools: $placed of ${#INSTALLABLES[@]} placed in $dir, and 1 left to pipx; the other $placed now come from $(install_source), not from $(pipx_release)"
	fi
	# THE RETIREMENT IS THE LAST ACT OF THE INSTALL, AFTER EVERY PLACEMENT
	# (Copilot round 7 on #45, `openRepoTools:369`). It is the one DESTRUCTIVE
	# step here, and it ran before `place_skill_and_hook` — so a skill directory
	# or a `settings.json` this run could not write left a workstation with the
	# old word already deleted and the adoption it was migrating to unfinished.
	# Every other refusal in this file places NOTHING rather than half of it; this
	# one removes nothing until the rest of the install has actually happened.
	place_skill_and_hook
	# THE RECEIPT IS WRITTEN BETWEEN THE LAST PLACEMENT AND THE ONE REMOVAL
	# (#57). After the placements, because a row carries the digest of the bytes
	# that are actually at a destination and every destination has to have been
	# written first; before the retirement, because that reads it and then drops
	# the row of anything it removes. A run that REFUSED part way through the
	# placements writes none of it and leaves the header marker as the evidence,
	# which is the population the marker exists for anyway.
	receipt_record
	retire_commands
	# ONE PATH NOTE for everything it placed: the directory is the same one.
	case ":${PATH:-}:" in
	*":$dir:"*) ;;
	*)
		say "  $dir is not on \$PATH. Add it:"
		say "    export PATH=\"$dir:\$PATH\""
		;;
	esac
	venv_note
	# AND LAST, IN A PACKAGE: whether the `openRepoTools` a shell will run is
	# this package's (FR-021). It changes nothing and leaves the exit status.
	path_report
}

# ONE VIRTUAL ENVIRONMENT PER REPOSITORY, OUTSIDE THE ESTATE
# (opensoft/openRepoTools#162, cause 3: "968 of 1,000 removals were dependency
# caches"). `${XDG_CACHE_HOME:-$HOME/.cache}/openRepoTools/venvs/<repo>/` is
# the convention `docs/README-lanes.md` states, and this repository's own is
# NAMED here with its state - and NOT CREATED: making one needs pip and the
# network, and an install from a checkout needs neither. `tests/run.sh` runs
# the suite from it once it is there and has pytest.
venv_note() {
	local venv="${XDG_CACHE_HOME:-$HOME/.cache}/openRepoTools/venvs/openRepoTools"
	if [ -x "$venv/bin/python3" ]; then
		say "openRepoTools: this repository's virtual environment is $venv (present; tests/run.sh runs from it when it has pytest)"
	else
		say "openRepoTools: this repository's virtual environment is $venv (not created; one per repository, outside the estate):"
		# QUOTED FOR THE SHELL IT IS PASTED INTO: `$XDG_CACHE_HOME` is the
		# person's to choose, spaces and all.
		say "    python3 -m venv $(printf '%q' "$venv") && $(printf '%q' "$venv/bin/python3") -m pip install pytest"
	fi
}


# =========================================================================
# THE FOURTEEN ARTIFACTS THAT ARE NOT ONE OF THE SEVENTEEN FILES
# =========================================================================
#
# lane-collision-protocol Amendment 9(b), inheriting A8 Addendum 2's R-A8-5
# unvaried — and that ruling is itself ratified. Measured on Eagle 2026-09-12:
# every `pclaude run` execs with `CLAUDE_CONFIG_DIR=<profile dir>`; 418 profile
# config dirs exist; 0 of them carry a SessionStart hook; and every profile's
# `skills` is a symlink to ONE shared directory. So:
#
#   * EACH SKILL goes to `$CLAUDE_PROFILES_HOME/shared/skills/<name>/SKILL.md`
#     — one write that all 418 profiles read through their own symlinks;
#   * `~/.claude` KEEPS A COPY of each skill for a bare `claude` run outside the
#     launcher, plus ONE MERGED ENTRY in `~/.claude/settings.json`;
#   * THE PROFILE `SessionStart` ENTRY IS NOT THIS COMMAND'S. It stays the
#     launcher's, which already rewrites each profile's settings.json on every
#     run and preserves hook keys. This command never writes a profile's
#     settings.json at all. One path, one writer, in both halves.
#
# TWENTY-NINE ARTIFACTS ALWAYS, PLUS THE TWO HOOK ENTRIES ONCE THEIR TARGET
# ANSWERS, AND THE COUNT IS THE INVARIANT: seventeen files in the bin
# directory, three skills in the shared skills directory, their three bare-run
# copies, THREE COMMAND FILES at those same two paths, and — once
# `~/projects/xFactory/lanes-edit.sh` exists, is executable and answers `guard`
# (opensoft/openRepoTools#237, the launcher's own rule) — TWO merged entries in
# `~/.claude/settings.json`. It was twelve before Amendment 11 added a SECOND
# WORD ON PATH — `restart` then, `lane` since Amendment 18 Addendum 2 retired
# that one — `lanes` and the `/restart` skill, sixteen before ruling 9 added
# `commands/swap.md`, eighteen before Amendment 12 adoption act 3 added the
# `UserPromptSubmit` name guard beside the `SessionStart` entry, and nineteen
# before Amendment 17 added `lane-handoff`, the `handoff` skill and the
# `/handoff` and `/ctx` command files, twenty-six before Amendment 16 added
# `lane-rename`, twenty-seven before opensoft/workBenches#119 added
# `claude-current` and `claude-restart-check`, twenty-nine before
# opensoft/openRepoTools#160 added `lanes-index`, and thirty before
# opensoft/openRepoTools#162 added `lane-worktrees`; #237 then held the last
# two back until the path they call answers, which moves no list. The count
# moves with the lists rather than being restated from memory.
#
# WHY A COMMAND FILE IS HERE AT ALL, WHEN ADOPTION ACT 6 SAYS THE LAUNCHER
# VENDORS THEM (F-X28, A11 Addendum 4 ruling 9, ratified "a11 addendum 4 yes").
# `opensoft/workBenches#74` @`0b7f6bc` deletes the whole `claude/commands` lane
# surface along with the launcher's copy of the `lane-swap` skill — and act 6,
# which would have `--install` inherit the command files, comes AFTER act 3,
# which is this PR's base. So on the day 4b lands, clause (g)'s `/swap` — a
# RATIFIED decision, and one of the six edits to in-force text — would be
# installed by NOBODY. The alternative was a PR of its own between now and then,
# and the ruling took this one.

#: THE SKILLS THIS REPOSITORY SHIPS, each placed at the same two paths. A
#: skill's DIRECTORY NAME is what `/<name>` invokes, so the list is the names
#: and every path is derived from one.
#:
#: `restart` joins `lane-swap` under Amendment 11 clause (f) and ratified
#: decision 7: `/restart` is the INSIDE half and `lane <name>` on `PATH` is the
#: outside one (Amendment 18 Addendum 2 retired `restart` from the PATH and
#: moved its act to that word), and the inside half has to be a skill because a
#: running session cannot `exec` a launcher over itself.
SKILLS=(handoff lane-swap restart)
skill_source_path() { printf 'skills/%s/SKILL.md\n' "$1"; }

#: THE COMMAND FILES THIS REPOSITORY SHIPS, each placed at the same two paths a
#: skill is (A11 Addendum 4 ruling 9). A command file's BASENAME is what
#: `/<name>` invokes, so the list is the names and every path is derived from
#: one — the same shape `SKILLS` has, for the same reason.
#:
#: `swap` is clause (g)'s ratified alias: `/swap` invokes the `lane-swap` skill
#: and adds nothing to it. The file is vendored BYTE FOR BYTE from
#: `opensoft/workBenches#71`, which is where it was written, because two copies
#: of one procedure that must stay byte-equal is the alternative that clause
#: rejects by name — and `#74` deletes that copy, which is why this one exists.
COMMANDS=(handoff ctx swap)
command_source_path() { printf 'commands/%s.md\n' "$1"; }
#: Kept as a single name for the one thing that is still singular: the
#: `SessionStart` hook's own entry, which belongs to the handoff skill's
#: amendment and not to the skill set. The name moved with the skill under
#: Amendment 17(a) — one act, three names — and the ENTRY it keys is unchanged,
#: because its only idempotence is the exact command string.
SKILL_NAME="handoff"
SKILL_SOURCE_PATH="$(skill_source_path "$SKILL_NAME")"

skills_home() { printf '%s\n' "${CLAUDE_PROFILES_HOME:-$HOME/.claude-profiles}"; }
claude_home() { printf '%s\n' "${CLAUDE_USER_DIR:-$HOME/.claude}"; }

#: THE HOOK ENTRY, byte for byte Amendment 8 clause (e)'s and byte for byte
#: what opensoft/workBenches#63's launcher ensures in each profile. The string
#: is the ONLY idempotence any of the three places has — a re-formatted
#: quotation of a string whose only idempotence is exact match is a SECOND
#: string — so it is spelled once here and never rebuilt.
HOOK_COMMAND='~/projects/xFactory/lanes-edit.sh session-start || true'
HOOK_MATCHER='startup|resume|clear|fork'
HOOK_TIMEOUT=5

hook_block_text() {
	cat <<'BLOCK'
  {
    "matcher": "startup|resume|clear|fork",
    "hooks": [
      { "type": "command", "command": "~/projects/xFactory/lanes-edit.sh session-start || true", "timeout": 5 }
    ]
  }
BLOCK
}

#: THE SECOND HOOK — lane-collision-protocol AMENDMENT 12's NAME GUARD, adoption
#: act 3 (opensoft/openRepoTools#25). Same shape, same file, same all-or-nothing
#: merge, and TWO deliberate differences from the entry above:
#:
#:   * NO `|| true`. On `SessionStart` that suffix is the whole safety property —
#:     a hook that dies must not break the session it was meant to orient. On
#:     `UserPromptSubmit` it would be the OPPOSITE of the mechanism: exit 2 is
#:     what BLOCKS a prompt (code.claude.com/docs/en/hooks), and `cmd || true`
#:     exits 0 for every code the command can produce. Copying the string with
#:     its suffix would have installed a guard that refuses nothing, silently,
#:     which is the exact state Amendment 12 exists to end.
#:   * NO MATCHER. `UserPromptSubmit` has no source to match on; the `matcher`
#:     key belongs to the events that do.
#:
#: The `~/projects/xFactory/lanes-edit.sh` spelling is the estate's convention
#: and `link-estates` places that link — the same path the entry above uses, so
#: one workstation has one spelling of one file.
GUARD_COMMAND='~/projects/xFactory/lanes-edit.sh guard'
GUARD_TIMEOUT=5

guard_block_text() {
	cat <<'BLOCK'
  {
    "hooks": [
      { "type": "command", "command": "~/projects/xFactory/lanes-edit.sh guard", "timeout": 5 }
    ]
  }
BLOCK
}

#: THE GATE ON THE PAIR (opensoft/openRepoTools#237, opensoft/workBenches#107).
#: Both entries call `~/projects/xFactory/lanes-edit.sh`, a symlink `link-estates`
#: places — and on a fresh workstation only `openRepoTools wip init` runs that,
#: AFTER this install. Merged before the link exists, the guard is a command that
#: is not there at every prompt: exit 127, a hook error per prompt, and never the
#: refusal (exit 2) it is for. So the pair is merged only once that path EXISTS,
#: IS EXECUTABLE AND ANSWERS `guard`, by the probe opensoft/workBenches'
#: `claude-profile` launcher puts to the same path before it ensures the same two
#: strings in each profile: the `awk` below is that probe byte for byte — comment
#: lines dropped, the case arm anchored where a line begins, one process and no
#: pipe. The `-x` is the one test it adds, because a file that is there and not
#: executable fails the hook the same way (exit 126).
#:
#: UNTIL THEN THE PAIR IS DEFERRED, AND EVERY RUN SAYS SO, with the remedy.
#: Nothing is added, and OUR EXACT GUARD ENTRY, in a file that already carries
#: it, is taken out — the launcher's own downgrade (workBenches#90), filtered on
#: each entry's NESTED array (workBenches#89), so every other hook, grouped with
#: ours or not, stays where it is, and the file is written back at 600. The
#: `SessionStart` entry is LEFT where it is: it ends in `|| true` and cannot fail
#: a session, which is the launcher's rule too. The strings, their exact-match
#: identity, the rival refusal and the receipt do not move; the count does —
#: twenty-nine artifacts always, plus these two once the path answers.
#:
#: `$HOME/projects`, and not `$PROJECTS_DIR`: the `~` in both strings is the
#: harness's to expand, so that is the one directory the hooks ever run from.
hook_target() { printf '%s\n' "$HOME/projects/xFactory/lanes-edit.sh"; }

HOOK_TARGET_WHY=""
hook_target_answers() {   # 0 when the pair may be merged; 1 with $HOOK_TARGET_WHY set
	local target
	target="$(hook_target)"
	HOOK_TARGET_WHY=""
	if [ ! -e "$target" ]; then
		if [ -L "$target" ]; then
			HOOK_TARGET_WHY="${target} is a symlink to $(readlink -- "$target" 2>/dev/null || :), which does not resolve (it is not there, or the links loop)"
		elif [ -d "$HOME/projects/xFactory" ]; then
			HOOK_TARGET_WHY="${target} is not there"
		else
			HOOK_TARGET_WHY="${target} is not there (nor is $HOME/projects/xFactory, which \`link-estates\` links it into)"
		fi
		return 1
	fi
	if [ ! -f "$target" ]; then
		HOOK_TARGET_WHY="${target} is not a regular file"
		return 1
	fi
	if [ ! -x "$target" ]; then
		HOOK_TARGET_WHY="${target} is not executable"
		return 1
	fi
	# A SCRIPT THAT CANNOT BE READ CANNOT BE RUN, and the probe below cannot
	# read it either: without this, mode 111 read as "no `guard` subcommand".
	if [ ! -r "$target" ]; then
		HOOK_TARGET_WHY="${target} is not readable"
		return 1
	fi
	if ! awk '
		/^[[:space:]]*#/ { next }
		/^[[:space:]]*([A-Za-z0-9_-]+\|)*guard(\|[A-Za-z0-9_-]+)*\)/ { found=1; exit }
		END { exit !found }
	' "$target" 2>/dev/null; then
		HOOK_TARGET_WHY="${target} has no \`guard\` subcommand"
		return 1
	fi
	return 0
}

#: The remedy every deferral and every removal prints, in one place.
hook_pair_remedy() {
	# WITH NO `~/projects/xFactory` THE REMEDY BELOW LOOPS: `link-estates` links
	# into that directory only where it exists, so a re-run alone changes
	# nothing. That is said first, where it is the case.
	if [ ! -d "$HOME/projects/xFactory" ]; then
		say "  ~/projects/xFactory has to exist first: \`link-estates\` links that path only into it."
	fi
	say "  The pair is merged once that path answers \`guard\`: \`openRepoTools wip init\` runs"
	say "  \`link-estates\`, which links it where ~/projects/xFactory exists, and then merges"
	say "  the pair; or run \`link-estates\` yourself and \`openRepoTools --install\` again."
}

# EVERY SKILL'S BYTES, by `--install`'s own rule: from the checkout beside this
# file when run from one, else fetched at this same ref. ALL OF THEM IN HAND
# BEFORE ANY IS PLACED, which is the same all-or-nothing rule the seventeen files
# already have — a new `/restart` beside a `handoff` that could not be fetched
# is a half-install that reads like a whole one.
collect_skills() {
	local name src
	payload_select
	workdir
	for name in "${SKILLS[@]}"; do
		src="$(skill_source_path "$name")"
		if [ -n "$PAYLOAD_DIR" ] && [ -f "$PAYLOAD_DIR/$src" ]; then
			cp -- "$PAYLOAD_DIR/$src" "$WORKDIR/skill-$name.md"
			continue
		fi
		[ "$PAYLOAD_MODE" != local ] || return 1
		fetch_from_repo "$src" "$WORKDIR/skill-$name.md" || return 1
	done
	# THE COMMAND FILES IN THE SAME BREATH, under the same all-or-nothing rule
	# (ruling 9). A `/swap` that could not be fetched beside a `/lane-swap` that
	# could is a half-install of an ALIAS PAIR, which is worse than either
	# alone: the skill is installed, the alias the text ratifies is not, and
	# nothing on screen says which.
	for name in "${COMMANDS[@]}"; do
		src="$(command_source_path "$name")"
		if [ -n "$PAYLOAD_DIR" ] && [ -f "$PAYLOAD_DIR/$src" ]; then
			cp -- "$PAYLOAD_DIR/$src" "$WORKDIR/command-$name.md"
			continue
		fi
		[ "$PAYLOAD_MODE" != local ] || return 1
		fetch_from_repo "$src" "$WORKDIR/command-$name.md" || return 1
	done
	return 0
}

# THE TWO MERGES INTO `~/.claude/settings.json`, COMPUTED BEFORE ANYTHING IS
# PLACED. It is the same all-or-nothing rule `--install` already has, extended
# to the two artifacts that are not whole files: a merge that cannot be
# computed refuses having placed nothing — and since Amendment 12 adoption act
# 3 the rule covers the PAIR, because a settings file that took the
# `SessionStart` entry and refused the `UserPromptSubmit` guard is a half
# install of a hook pair and nothing on screen would say which half.
#
#   * it is written with `jq`, and refuses naming `jq` where it is absent. This
#     repository carries no JSON-handling code otherwise, so the dependency is
#     named rather than assumed. It is not bash, so the macOS bash-3.2 parse
#     gate is unaffected;
#   * it NEVER writes the file whole and never replaces it: the file already
#     carries a `UserPromptSubmit` hook of the person's own — the usage guard
#     — and a user's settings are not an installer's to own. THAT IS WHY THE
#     `UserPromptSubmit` ARM KEYS ON THE VERB `guard` AS A WHOLE ARGUMENT and
#     not as a substring: this command adds an entry to an event a person is
#     already using, and every entry already there stays exactly where it is;
#   * it is idempotent by EXACT MATCH on the entry's `command` string;
#   * an entry under SessionStart whose `command` DIFFERS, one under
#     UserPromptSubmit that RUNS `guard` with a different string, a file it
#     cannot parse, or a `hooks` value that is not an object → refuse, print
#     the exact block, place nothing. An installer that repairs a file it does
#     not understand is how a person loses a setting they meant;
#   * it WRITES THE FILE BACK AT MODE 600 — it sets that mode, it does not
#     preserve one (F8 of the #24 review: a pre-existing file at 640 comes out
#     600, measured). workBenches' setup-claude-profiles.sh writes this same
#     file on every ./setup.sh through jq + mktemp + chmod 600 + mv -f, so 600
#     is the mode this file already has on a host that has run that script, and
#     an installer that widened it would be silently undone or would widen it
#     behind that script's back.
HOOK_PLAN=""      # "absent" | "present" | "nofile"
GUARD_PLAN=""     # the same three, for Amendment 12's UserPromptSubmit entry
HOOK_MERGED=""    # a staged whole file, ready to move into place
HOOK_DEFERRED=""  # the same file with OUR guard entry taken out, or "" (#237)
HOOK_STRIP_WHY="" # why that entry could not be taken out, or ""

# THE DESTINATIONS, PROVED WRITABLE BEFORE ANY OF THE SEVENTEEN IS PLACED — every
# directory the artifacts that are not one of the seventeen files go into, derived
# from the two lists and NOT counted in this sentence. It said "THE THREE
# DESTINATIONS" while the loop below built seven of them, which is the same
# defect `collect_commands` names above: a number restated beside a list that
# grew is a number that goes stale where nobody is looking.
#
# Amendment 9(b) computes the merge in hand "so a merge that cannot be computed
# refuses having placed nothing" — and the merge computing fine against a
# directory nobody may write to is the same failure one step later. A
# filesystem gives no transaction, so this cannot make those placements
# atomic; what it does is move the failure that actually happens — a directory
# that is not ours to write — back to the planning phase, where the refusal
# still costs nothing. IT MAKES NONE OF THEM: the question is put to the nearest
# ancestor that exists, so a run that refuses leaves a machine exactly as it
# found it, and the placement's own guarded `mkdir -p` is the one writer of
# these directories (#44 round 1, Sourcery's
# `tests/test_install_skill_and_hook.py:658`).
plan_skill_targets() {
	local d name dirs=() target kind bad="" paths=""
	for name in "${SKILLS[@]}"; do
		dirs+=("$(skills_home)/shared/skills/$name" "$(claude_home)/skills/$name")
	done
	# A COMMAND FILE HAS NO DIRECTORY OF ITS OWN — it is a file in the commands
	# directory, where a skill is a `SKILL.md` in a directory named for it — so
	# the two command destinations are added once rather than once per name.
	if [ "${#COMMANDS[@]}" -gt 0 ]; then
		dirs+=("$(skills_home)/shared/commands" "$(claude_home)/commands")
	fi
	dirs+=("$(claude_home)")
	for d in "${dirs[@]}"; do
		if kind="$(unusable_dir_kind "$d")"; then
			die "\`--install\` places a skill, a command file or the SessionStart entry in
    $d (Amendment 9(b)), and $kind.
    NOTHING was installed and nothing already installed was replaced."
		fi
	done
	# AND THE SIX FILES INSIDE THEM, BY R-A9-12 (F-X17). The loop above proved
	# the DIRECTORIES writable and stopped there, while `place_skill_and_hook`
	# then reaches each `SKILL.md` with `[ -e ]`, `cmp -s` and `cp` — not one of
	# which can tell a regular file from a link to one. So a symlink at
	# `~/.claude/skills/restart/SKILL.md` was followed and WRITTEN THROUGH: the
	# skill stays uninstalled, the target stays a link, and the bytes land
	# wherever it points. That is the same failure R-A9-12 was ruled on for the
	# seventeen in the bin directory, one directory along, and the ruling says
	# nothing about which path it applies to.
	#
	# `unplaceable_kind` is asked rather than re-tested, because two copies of
	# one rule is how the bin half would come to refuse a dangling link and the
	# skill half would not — and that is exactly what happened to the second
	# half of the question it answers: a target that IS a regular file and is
	# not this user's to write (#40, finding 4). The eleven were already placed
	# by the time `cp` met it, on the one path that promises all or none, so the
	# `-w` is asked here beside the `-L`.
	#
	# BOTH QUESTIONS OUTLIVE THE `cp` (#167). The write is a rename now, which
	# follows no link and needs the DIRECTORY's write bit rather than the
	# file's — so neither target would FAIL any more; each would be REPLACED,
	# without a word. A link is somebody's link, and swapping it for a regular
	# file unasked is the same act as writing through it, in the other
	# direction (the receipt's own rule, `receipt_replace`); a read-only file
	# is somebody saying "not this one". The refusal is the same refusal.
	for name in "${SKILLS[@]}"; do
		for target in "$(skills_home)/shared/skills/$name/SKILL.md" "$(claude_home)/skills/$name/SKILL.md"; do
			kind="$(unplaceable_kind "$target")" || continue
			bad="${bad:+$bad
}    $target — $kind"
			paths="${paths:+$paths }\"$target\""
		done
	done
	for name in "${COMMANDS[@]}"; do
		for target in "$(skills_home)/shared/commands/$name.md" "$(claude_home)/commands/$name.md"; do
			kind="$(unplaceable_kind "$target")" || continue
			bad="${bad:+$bad
}    $target — $kind"
			paths="${paths:+$paths }\"$target\""
		done
	done
	[ -z "$bad" ] || die "\`--install\` places a REGULAR FILE IT CAN WRITE at each of these skill and
    command targets, and these are not that:
$bad
    Each file is placed by renaming a new copy over its path, so installing
    over a symlink would replace somebody's link with a regular file, over a
    directory would put the file inside it, and over a file this user cannot
    write would replace a file somebody made read-only — none of it asked for.
    NOTHING was installed and nothing already installed was replaced.
    Clear them, then re-run — \`rm -f\` takes a read-only file, because what a
    removal needs is the DIRECTORY's write bit and not the file's:
        rm -f -- $paths"
}

# THE SEVENTEEN TARGETS, PROVED PLACEABLE BEFORE ANY OF THE THIRTY-ONE IS PLACED
# (A9 Addendum 4, R-A9-12, ruled on F5 of the #24 review).
#
# `cp` FOLLOWS A SYMLINK. The loop below tested `[ -e "$target" ]`, compared
# with `cmp -s` and then copied, and not one of the three can tell a regular
# file from a link to one — so against the bin directory the pre-move
# `link-estates` actually leaves on a workstation, `--install` printed
# `lane-start: updated at …`, `9 of 9 placed`, exit 0, and BOTH claims were
# false: the target stayed a symlink, so the command was never installed, and
# the post-move bytes landed in `opensoft/brett-wip`'s WORKING TREE on the
# other end of it. That repository is the one every lane writes:
# `lanes-edit.sh`'s `refuse_dirty_checkout` then refuses `log`, `claim` and
# `release` on that workstation until somebody runs `git checkout -- lanes/`.
# workBenches' `setup-estate-commands.sh` refuses the whole estate install on a
# target that "already exists as a symlink" too, so a host in this state also
# fails the estate install that follows — the exact failure clause (f)(3) is
# written about.
#
# THE REFUSAL OUTLIVES THE `cp` (#167). The loop places by RENAME now, which
# follows no link: it would swap the link itself for a regular file, without a
# word, which is the same act in the other direction — the person's link gone
# and nothing on screen saying so. `plan_skill_targets` says the same of the
# files it walks.
#
# A HOST STILL CARRYING THOSE SYMLINKS IS THE NORMAL PRE-ACT-5 CASE, not an
# exotic one: adoption act 6 clears them on each workstation before the first
# `--install` that carries this, and act 5 strips `brett-wip` to data. So the
# refusal names every offending path, says what each one IS, and prints the one
# `rm` that clears them — rather than leaving a person to remember a step.
# WHAT IS AT A PATH, WHERE IT IS NOT A REGULAR FILE.
# ONE implementation of R-A9-12's test, for every path this command writes: the
# seventeen in the bin directory, the six `SKILL.md`s (F-X17), the six command
# files, and `~/.claude/settings.json` (#44 round 1, `openRepoTools:878`).
# Prints what is there and returns 0; returns 1 — saying nothing — where the
# path is absent or is a regular file.
#
# `-L` AS WELL AS `-e`, because a DANGLING symlink is the sharper case: `[ -e ]`
# is false on one, so it reads as ABSENT. `cp` through it created the file at the
# far end — a write into a repository this command was never told about — and
# the rename that replaced `cp` (#167) would swap the link for a file unasked.
#
# IT IS ASKED APART FROM THE `-w`, because the `-w` is not asked of every path.
# Every write here is a rename since #167, and a rename needs the DIRECTORY's
# write bit and never the file's: `unplaceable_kind` asks the file's own `-w` of
# the files this installer owns, as a person's "not this one", while the
# SessionStart merge — which has always replaced `settings.json` through
# `mktemp` + `mv -f` — merges into one this user cannot write. A single walk that
# asked `-w` of all three would refuse an install that works.
path_kind() {   # <path>
	local t="$1"
	if [ -L "$t" ]; then
		printf 'a symlink to %s' "$(readlink -- "$t" 2>/dev/null || printf '<unreadable>')"
		return 0
	fi
	if [ -d "$t" ]; then
		printf 'a directory'
		return 0
	fi
	if [ -e "$t" ] && [ ! -f "$t" ]; then
		printf 'not a regular file'
		return 0
	fi
	return 1
}

# AND WHETHER IT MAY BE WRITTEN OVER: `-w` as well as `-f` (#40, finding 4).
# This answered "a regular file" with `return 1` and asked nothing else, so
# every existing file counted as one `--install` may place over — and
# `plan_skill_targets` let a READ-ONLY `SKILL.md` or command file through.
# `install_commands` has already copied the seventeen PATH files by the time
# `place_skill_and_hook` reaches the `cp`, so an ordinary permission failure
# there left a host with seventeen of the thirty-one artifacts placed, on the one
# path in this file that promises all or none. A mode is not an exotic state
# either: `chmod 444` on a file a person did not want overwritten is what a
# person does.
#
# THAT LAST SENTENCE IS NOW THE WHOLE REASON (#167). The write is a rename, which
# needs the directory's write bit and not the file's, so a read-only target would
# no longer FAIL half way — it would be replaced without a word. The question
# stays, so that a person's `chmod 444` is still the refusal it was.
#
# WHAT `-w` DOES NOT PROVE IS THAT THE MODE STAMP CAN SUCCEED, and that limit is
# deliberate (#44 round 1, the suppressed comment at `openRepoTools:635`). A
# GROUP-WRITABLE file owned by another account passes `-w` and refuses the
# `chmod` where the stamp is made IN PLACE — the arm whose bytes were already
# right; the arm that writes stamps its own temporary (#167) — and the question
# that would settle it — `[ -O ]`, this euid against
# the file's owner — is one no test in this suite can put: `tmp_path` cannot
# make a second account, and it would refuse a root `--install` into another
# user's home, where the `chmod` does in fact succeed. A guard no test reaches
# is the guard R-A9-15 was ruled about, so this asks what it can prove and the
# stamp's own failure stays a `die` naming the artifact it could not mode.
#
# THE REFUSAL'S ONE REMEDY STAYS TRUE OF THE NEW KIND, which is why it is
# answered here rather than in a second walk: `rm -f` takes a read-only file —
# what a removal needs is the DIRECTORY's write bit, not the file's — so the
# `rm -f -- <paths>` both planners print clears every path either of them names.
unplaceable_kind() {   # <target>
	local t="$1" kind
	if kind="$(path_kind "$t")"; then
		printf '%s' "$kind"
		return 0
	fi
	if [ -f "$t" ] && [ ! -w "$t" ]; then
		printf 'a regular file this user cannot write'
		return 0
	fi
	return 1
}

# WHETHER A DIRECTORY THIS RUN MUST CREATE A FILE IN CAN TAKE ONE — asked of the
# NEAREST ANCESTOR THAT EXISTS, because a directory that is not there yet is
# exactly as placeable as the directory it would be created in (#44 round 1,
# `openRepoTools:653`, and Sourcery's `README.md:237`). `plan_install_targets`
# asked `[ -d "$dir" ]` first and so asked NOTHING AT ALL where the answer was
# "not yet": a `$OPENREPOTOOLS_BIN_DIR` under a parent this user cannot write
# passed planning and met the `mkdir -p` in `install_commands`, which died with
# the shell's own 1 under `set -e` — the code this toolset spends on "findings
# were printed" — instead of the refusal this phase exists to print.
#
# `-x` AS WELL AS `-w`, because creating a file in a directory takes both of its
# bits (#44 round 1, the suppressed comment at `openRepoTools:658`): a directory
# at 0600 accepts no new file at all, and `-w` alone passed it. ONE
# implementation for the bin directory and for the seven destinations
# `plan_skill_targets` walks, for the reason `unplaceable_kind` is one: two
# copies of one rule is how the bin half comes to refuse what the skill half
# allows.
#
# AND IT CREATES NOTHING. `plan_skill_targets` proved a directory writable by
# `mkdir -p`-ing it first, so a run that then REFUSED had already left seven
# directories on a machine that had none — which is not what "NOTHING was
# installed" says (#44 round 1, Sourcery's
# `tests/test_install_skill_and_hook.py:658`). Every place this command writes
# into is made in the PLACEMENT phase, by the guarded `mkdir -p` each `cp`
# already carries.
#
# `-L` STOPS THE WALK TOO, not only `-e` (#44 round 2, `openRepoTools:701`).
# `-e` follows a symlink, so it is FALSE on a DANGLING one — the walk read
# that as "not there yet" and stepped past it to ask about ITS parent instead,
# so a dangling `skills_home` or bin-directory ancestor passed this planner and
# met `mkdir -p` after other artifacts were already placed, on the one path
# that promises all or none. A symlink, dangling or not, is something AT that
# path; the walk stops at the nearest one of those, exactly as it stops at the
# nearest directory, file or anything else that is there.
unusable_dir_kind() {   # <directory>
	local d="$1" up
	while [ ! -e "$d" ] && [ ! -L "$d" ]; do
		up="$(dirname -- "$d")"
		[ "$up" != "$d" ] || break
		d="$up"
	done
	if [ -L "$d" ] && [ ! -e "$d" ]; then
		printf '%s is a symlink to %s, which is not there' "$d" \
			"$(readlink -- "$d" 2>/dev/null || printf '<unreadable>')"
		return 0
	fi
	if [ ! -d "$d" ]; then
		printf '%s is not a directory' "$d"
		return 0
	fi
	if [ ! -w "$d" ]; then
		printf '%s is not writable' "$d"
		return 0
	fi
	if [ ! -x "$d" ]; then
		printf '%s is not searchable (a create takes its `x` bit as well as its `w`)' "$d"
		return 0
	fi
	return 1
}

# ------------------------------------------------------------- PIPX'S LINK
#
# opensoft/openRepoTools#228, User Story 1: `specs/006-release-pypi/contracts/
# pipx-link.md` is the rule and `contracts/cli-output.md` the exact lines (the
# change's design.md Decision 6). `pipx install openrepotools` places ONE
# command, `<dir>/openRepoTools`, as a symbolic link into its own venv, and that
# link may be the only `openRepoTools` on the workstation that can run
# `--install` at all. So it is the one link `--install` LEAVES WHERE IT IS: it
# is skipped, said out loud, counted as left to pipx, and given no receipt row
# (`receipt_add` writes none for a link); no refusal lists it and no line ever
# advises its `rm`. `retire_commands` never meets it, because `RETIRED` does not
# hold `openRepoTools`.
#
# IT IS DECIDED FROM PATHS AND THE EXISTENCE OF FILES, AND FROM NOTHING ELSE. No
# JSON is parsed, so it needs no `jq` and is decided before the `jq` check,
# which stays where it is (FR-018); `PIPX_HOME` is never read, so it holds under
# every home pipx uses — `$PIPX_HOME`, `~/.local/pipx`, `~/.local/share/pipx`
# and macOS's `~/Library/Application Support/pipx`, whose SPACE is why every
# expansion here is quoted. And it holds in every mode: the package's own
# `--install`, a checkout's and the one-line install's.
#
# A LINK THAT LOOKS LIKE A VENV AND FAILS THE RULE — a later pipx layout, or a
# `pip install` into a plain venv — is still refused, because it is not provably
# pipx's. It is named for what it is, and NO `rm` IS PRINTED FOR IT: removing it
# may remove the only command that can run `--install` (ADV3, SC-006).

PIPX_LINK=""      # <dir>/openRepoTools, once it is known to be pipx's link
PIPX_VENV=""      # the canonical venv that link resolves into
PIPX_PAYLOAD=""   # that venv's canonical lib/python*/site-packages/openrepotools/payload

#: EVERY LINK FOLLOWED, BY PLAIN `readlink`, which prints a link's own value on
#: Linux, macOS and BSD alike: `readlink -f` is not in a stock macOS userland and
#: neither is `realpath`. A relative value is joined to the LINK'S OWN directory,
#: as the kernel reads it. Past 40 hops — a longer chain, or a cycle — it fails,
#: and such a path is not pipx's link, whatever it ends at. Then the last path's
#: DIRECTORY is canonicalised with `cd -P` and `pwd -P`, its own name kept, so
#: the answer is one spelling of one file. `CDPATH` is emptied, because a `cd`
#: through one prints where it went. Nothing outside it is read, because the PATH
#: report reuses it and the suite cuts it out to run alone (`installer_function`).
link_resolve() {   # <path> — its canonical path with every link followed; 1 past 40 hops
	local p="$1" v="" dir="" hops=0
	while [ -L "$p" ]; do
		hops=$((hops + 1))
		[ "$hops" -le 40 ] || return 1
		v="$(readlink -- "$p")" || return 1
		case "$v" in
		/*) ;;
		*) v="$(dirname -- "$p")/$v" ;;
		esac
		p="$v"
	done
	dir="$(CDPATH= cd -P -- "$(dirname -- "$p")" 2>/dev/null && pwd -P)" || return 1
	printf '%s/%s\n' "${dir%/}" "${p##*/}"
}

#: PACKAGE MODE, FROM PATHS (design.md Decision 4): this file's directory,
#: resolved with `cd -P` and `pwd -P`, is named `payload`, its parent is named
#: `openrepotools`, and that parent holds a regular `_release.py` — the layout an
#: installed wheel of this package has, and no other. A checkout's directory is
#: its root, and the one-line install runs from a pipe or from a placed file in
#: `<dir>`, so neither ever matches. Prints that directory, or returns 1.
#:
#: `package_init` asks it once, before the arguments are read, and keeps the
#: answer in PACKAGE_DIR: the payload directory, canonical, in package mode, and
#: empty in a checkout or the one-line install. It is set here, so that nothing
#: in the environment can set it.
PACKAGE_DIR=""
package_payload_dir() {
	local here="" parent=""
	[ -f "$SELF" ] || return 1
	here="$(CDPATH= cd -P -- "$(dirname -- "$SELF")" 2>/dev/null && pwd -P)" || return 1
	[ "${here##*/}" = payload ] || return 1
	parent="${here%/*}"
	[ "${parent##*/}" = openrepotools ] && [ -f "$parent/_release.py" ] || return 1
	printf '%s\n' "$here"
}

#: STEPS 1 TO 3 OF THE RULE, which the match and the venv-shaped refusal share:
#: <target> is a symbolic link, it resolves, and what it resolves to is a regular
#: file named `openRepoTools` in a directory named `bin`. Prints that `bin`'s
#: parent — the venv, as far as a path can say — which is canonical because the
#: resolution is.
link_venv() {   # <target> — the venv it links into, or 1
	local resolved="" bin="" venv=""
	[ -L "$1" ] || return 1
	resolved="$(link_resolve "$1")" || return 1
	[ -f "$resolved" ] && [ "${resolved##*/}" = openRepoTools ] || return 1
	bin="${resolved%/*}"
	[ "${bin##*/}" = bin ] || return 1
	venv="${bin%/*}"
	printf '%s\n' "${venv:-/}"
}

#: PIPX'S LINK (FR-015): steps 1 to 3, then the two things only pipx's venv of
#: this package holds — a regular `pipx_metadata.json` beside its `bin/`, of
#: which nothing but its EXISTENCE is read, and a regular `openRepoTools` in some
#: `lib/python*/site-packages/openrepotools/payload` under it. The glob is looped
#: over with a `-f` test, because bash leaves a glob that matches nothing as its
#: own text. A match records the three values the run reads, and nothing else.
pipx_link_match() {   # <dir>/openRepoTools — 0 with PIPX_LINK, PIPX_VENV and PIPX_PAYLOAD set, else 1
	local venv="" payload="" canonical=""
	venv="$(link_venv "$1")" || return 1
	[ -f "${venv%/}/pipx_metadata.json" ] || return 1
	for payload in "${venv%/}"/lib/python*/site-packages/openrepotools/payload; do
		[ -f "$payload/openRepoTools" ] || continue
		canonical="$(CDPATH= cd -P -- "$payload" 2>/dev/null && pwd -P)" || continue
		PIPX_LINK="$1" PIPX_VENV="$venv" PIPX_PAYLOAD="$canonical"
		return 0
	done
	return 1
}

#: A LINK INTO A PYTHON ENVIRONMENT THAT FAILS THE RULE: steps 1 to 3 hold, the
#: venv holds a regular `pyvenv.cfg`, as every venv does, and the whole match
#: does not hold. Prints the venv, for the refusal to name. The match is asked in
#: a subshell, so that asking it here records nothing.
venv_shaped_link() {   # <dir>/openRepoTools — the venv, or 1
	local venv=""
	venv="$(link_venv "$1")" || return 1
	[ -f "${venv%/}/pyvenv.cfg" ] || return 1
	if (pipx_link_match "$1"); then return 1; fi
	printf '%s\n' "$venv"
}

#: PIPX'S OWN ENVIRONMENT (research R5): a package-mode run whose payload,
#: PACKAGE_DIR, is under the venv pipx's link resolves into. Both paths are
#: canonical, so this compares one spelling of each.
pipx_own_run() {
	[ -n "$PIPX_VENV" ] && [ -n "$PACKAGE_DIR" ] || return 1
	case "$PACKAGE_DIR/" in
	"${PIPX_VENV%/}"/lib/*) return 0 ;;
	esac
	return 1
}

#: WHERE THE OTHER SIXTEEN NOW COME FROM, for the count line of every run beside
#: pipx's link but pipx's own (research R5). A PACKAGE names its own release,
#: from the stamp `package_init` read, in a pipx venv or not: it placed its own
#: payload and fetched from nothing, so `$REPO @ $REF` would name a source it
#: never used. A checkout and the one-line install name the source they
#: collected from, `payload_where`'s pair with `@` in place of `at`. An adopted
#: source mounted in a local assembly fetches nothing and so sets no
#: PAYLOAD_REPO or PAYLOAD_REV: the pin its payload was checked against names
#: that same pair there.
install_source() {
	if [ -n "$PACKAGE_DIR" ]; then
		printf 'openrepotools %s\n' "$STAMP_VERSION"
	elif [ "$SOURCE_LAYOUT" = adopted ]; then
		printf '%s @ %s\n' "${PAYLOAD_REPO:-${PIN_SOURCE_REPOSITORY-}}" "${PAYLOAD_REV:-${PIN_COMMIT-}}"
	else
		printf '%s @ %s\n' "$REPO" "$REF"
	fi
}

#: WHAT IS BESIDE PIPX'S LINK, for the same line: `openrepotools <VERSION>` of
#: the stamp beside pipx's payload, `0.0.0` where it is unstamped. It is read in
#: a SUBSHELL, because `stamp_read` sets the STAMP_* values and this run's own
#: are the ones `install_source` names. A stamp there that fails the reader is
#: another install's, not this one's, so it refuses nothing: the clause says it
#: cannot be read, and the install goes on.
pipx_release() {
	local version=""
	if version="$(stamp_read "${PIPX_PAYLOAD%/*}/_release.py" && printf '%s' "$STAMP_VERSION")"; then
		printf 'openrepotools %s\n' "$version"
	else
		printf '%s\n' "the openrepotools beside pipx's link, whose stamp cannot be read"
	fi
}

#: `^from openrepotools(\.[A-Za-z_][A-Za-z0-9_]*)* import ` by `case`, the
#: import pip's console-script template writes for the entry point at column 0.
#: The name characters are written out, for the reason ORT_ALNUM gives.
pip_imports_entry() {   # <line>
	local rest="" name=""
	case "$1" in "from openrepotools"*) ;; *) return 1 ;; esac
	rest="${1#from openrepotools}"
	while :; do
		case "$rest" in
		' import '*) return 0 ;;
		.*) rest="${rest#.}" ;;
		*) return 1 ;;
		esac
		name="${rest%%[!${ORT_ALNUM}_]*}"
		case "$name" in '' | [0123456789]*) return 1 ;; esac
		rest="${rest#"$name"}"
	done
}

#: PIP'S CONSOLE SCRIPT FOR THIS PACKAGE (FR-020, contracts/pipx-link.md), at a
#: target that is a regular file and not a link: what `pip install --user
#: openrepotools` leaves at `~/.local/bin/openRepoTools`. It is another
#: installer's command, and `--install` refuses to replace it in every mode, its
#: own run included. Both halves of pip's template must hold:
#:
#:   * A PYTHON INTERPRETER, in either form pip writes: a `#!` line whose first
#:     word's last `/` segment begins with `python`, or the three-line `/bin/sh`
#:     trampoline pip writes when that path holds a space or is too long for a
#:     shebang — `#!/bin/sh`, then `'''exec' <python> … "$0" "$@"` with the
#:     interpreter quoted up to the next `"` or unquoted up to the first space,
#:     then `' '''`;
#:   * THE ENTRY POINT'S IMPORT at column 0 in one of the first 20 lines
#:     (`pip_imports_entry`).
#:
#: Read by `IFS= read -r`, `case` and parameter expansion: bash 3.2's.
pip_script_is() {   # <dir>/openRepoTools — 0 when it is pip's console script for openrepotools
	local line="" n=0 first="" second="" third="" imports=1 word="" rest=""
	local exec_word="'''exec' " tail=' "$0" "$@"' close="' '''" dq='"'
	[ -f "$1" ] && [ ! -L "$1" ] && [ -r "$1" ] || return 1
	while [ "$n" -lt 20 ] && { IFS= read -r line || [ -n "$line" ]; }; do
		n=$((n + 1))
		case "$n" in
		1) first="$line" ;;
		2) second="$line" ;;
		3) third="$line" ;;
		esac
		if [ "$imports" -ne 0 ] && pip_imports_entry "$line"; then imports=0; fi
	done 2>/dev/null <"$1" || return 1
	[ "$imports" -eq 0 ] || return 1
	# THE SHEBANG FORM: the first word after `#!` names a Python.
	case "$first" in '#!'*) ;; *) return 1 ;; esac
	word="${first#??}"
	while :; do case "$word" in ' '* | "$ORT_TAB"*) word="${word#?}" ;; *) break ;; esac; done
	word="${word%%[ $ORT_TAB]*}"
	case "${word##*/}" in python*) return 0 ;; esac
	# THE TRAMPOLINE FORM, its three lines exactly as pip writes them.
	[ "$first" = '#!/bin/sh' ] && [ "$third" = "$close" ] || return 1
	case "$second" in "$exec_word"*) ;; *) return 1 ;; esac
	rest="${second#"$exec_word"}"
	case "$rest" in
	"$dq"*)
		rest="${rest#?}"
		case "$rest" in *"$dq"*) ;; *) return 1 ;; esac
		word="${rest%%"$dq"*}"
		rest="${rest#*"$dq"}"
		;;
	*)
		word="${rest%% *}"
		rest="${rest#"$word"}"
		;;
	esac
	case "$rest" in *"$tail") ;; *) return 1 ;; esac
	case "${word##*/}" in python*) return 0 ;; esac
	return 1
}

#: THE `openRepoTools` A SHELL WILL RUN, asked last in a package-mode
#: `--install` (FR-021, research R7). `<prefix>` is five levels above the
#: payload, the parent of its `lib/`, and is named only where the four
#: directories between are an installed wheel's — `openrepotools`,
#: `site-packages`, `python*` and `lib` — so a layout that names no prefix gets
#: no note. The first `openRepoTools` on PATH, as `type -P` finds it, and this
#: package's console script, `<prefix>/bin/openRepoTools`, are each followed
#: through `link_resolve`, so pipx's link and the script it points at are one
#: file. A mismatch, or no `openRepoTools` on PATH at all, is the one note. It
#: changes nothing, and never fails: the exit status stays the install's.
path_report() {
	local up="$PACKAGE_DIR" prefix="" want="" first="" got="" expected=""
	[ -n "$up" ] || return 0
	up="${up%/*}"
	[ "${up##*/}" = openrepotools ] || return 0
	up="${up%/*}"
	[ "${up##*/}" = site-packages ] || return 0
	up="${up%/*}"
	case "${up##*/}" in python*) ;; *) return 0 ;; esac
	up="${up%/*}"
	[ "${up##*/}" = lib ] || return 0
	prefix="${up%/*}"
	want="$prefix/bin/openRepoTools"
	first="$(type -P openRepoTools 2>/dev/null)" || first=""
	if [ -z "$first" ]; then
		say "openRepoTools: note: the first openRepoTools on your PATH is none, not this package's $want, so the release you just installed is not the one that runs; put the directory that holds pipx's link to it on PATH"
		return 0
	fi
	got="$(link_resolve "$first")" || got=""
	expected="$(link_resolve "$want")" || expected=""
	if [ -n "$got" ] && [ "$got" = "$expected" ]; then
		return 0
	fi
	say "openRepoTools: note: the first openRepoTools on your PATH is $first, not this package's $want, so the release you just installed is not the one that runs; put the directory that holds pipx's link to it ahead of $(dirname -- "$first") on PATH"
}

plan_install_targets() {
	local dir="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}"
	local name target kind bad="" paths="" venv="" leave="" clear=""
	PIPX_LINK="" PIPX_VENV="" PIPX_PAYLOAD=""
	# THE DIRECTORY FIRST, which is the `-w` question for every target that is
	# not there yet: an absent file is placeable exactly where the directory it
	# would be created in takes a write. `plan_skill_targets` asks this of the
	# ones it writes into; the bin directory is the same question for the eleven,
	# and without it a `$OPENREPOTOOLS_BIN_DIR` that is not this user's failed at
	# the first `cp` under `set -e` — exit 1, which this toolset spends on
	# "findings were printed", from a run that placed nothing and said nothing.
	# A directory that is not there YET is asked about through the nearest
	# ancestor that is, because `install_commands` creates it and a parent that
	# refuses the `mkdir -p` is a refusal this phase can print for nothing.
	if kind="$(unusable_dir_kind "$dir")"; then
		die "\`--install\` places the ${#INSTALLABLES[@]} commands in $dir, and $kind.
    NOTHING was installed and nothing already installed was replaced."
	fi
	for name in "${INSTALLABLES[@]}"; do
		target="$dir/$name"
		# PIPX'S LINK IS ASKED ABOUT BEFORE ANY LINK IS REFUSED (FR-015), at the
		# one command pipx places. It is no bad target: the placement loop skips
		# it. A venv-shaped link IS one, listed for what it is, with the
		# paragraph that says to leave it, and kept out of the `rm -f` line.
		if [ "$name" = openRepoTools ]; then
			if pipx_link_match "$target"; then
				continue
			fi
			if venv="$(venv_shaped_link "$target")"; then
				bad="${bad:+$bad
}    $target — a link into a Python environment, perhaps pipx's ($venv), in a layout this installer does not recognise"
				leave="
    Leave $target where it is: it may be the only openRepoTools that can
    run \`--install\`. Run \`pipx reinstall openrepotools\`, then \`openRepoTools --install\`
    again, and if this refusal stays, open an issue on opensoft/openRepoTools naming
    the layout under $venv."
				continue
			fi
			# PIP'S CONSOLE SCRIPT IS NOT THIS INSTALLER'S TO REPLACE (FR-020),
			# in any mode: refused here, before anything is fetched or placed.
			if pip_script_is "$target"; then
				die "\`--install\` will not replace $target: it is pip's console script for openrepotools (\`pip install --user openrepotools\` leaves it there), and another installer's command is not this installer's to overwrite.
    NOTHING was installed and nothing already installed was replaced.
    pipx is the one package route. To move to it:
        pip uninstall openrepotools
        pipx install openrepotools
        openRepoTools --install"
			fi
		fi
		kind="$(unplaceable_kind "$target")" || continue
		bad="${bad:+$bad
}    $target — $kind"
		paths="${paths:+$paths }\"$target\""
	done
	[ -n "$bad" ] || return 0
	# THE `rm -f` LINE NAMES ONLY WHAT IT IS SAFE TO REMOVE, so with a
	# venv-shaped link the only bad target there is no "Clear them" at all.
	[ -z "$paths" ] || clear="
    Clear them, then re-run — \`rm -f\` takes a read-only file, because what a
    removal needs is the DIRECTORY's write bit and not the file's:
        rm -f -- $paths
    (The pre-move \`link-estates\` puts exactly these links in a workstation's
    \`~/.local/bin\`, pointing into \`opensoft/brett-wip\` — the checkout every
    lane writes. Amendment 9's adoption act 6 removes them per host before the
    first \`--install\` that carries this, and act 5 strips that repository to
    data.)"
	die "\`--install\` places a REGULAR FILE IT CAN WRITE at each of these, and these are
    not that:
$bad$leave
    Each command is placed by renaming a new copy over its path, so installing
    over a symlink would replace somebody's link with a regular file, over a
    directory would put the file inside it, and over a file this user cannot
    write would replace a file somebody made read-only — none of it asked for.
    NOTHING was installed and nothing already installed was replaced.$clear"
}

# THE RIVALS THEMSELVES, FOR A REFUSAL TO NAME (#59). It is the conflict half of
# the read in `plan_hook_merge` with the answer taken off: every command under
# this event that runs this hook's verb AND IS NOT OURS, one per line and
# indented into the refusal's block.
#
# ONE IMPLEMENTATION FOR BOTH ENTRIES, for the reason `unplaceable_kind` is one:
# what differs between the two arms is the EVENT, OUR OWN STRING and the VERB a
# rival is recognised by, and a second copy of this is how one arm comes to name
# what the other cannot.
#
# IT MATTERS NOW IN A WAY IT DID NOT BEFORE. With `conflict` beating `present`, a
# refused file may carry OUR entry as well as the rival — so "remove that entry"
# with nothing named is a remedy a person can read as "remove the one this
# installer just printed", which would leave the second writer exactly where it
# is and take the working entry away instead.
hook_rivals() {   # <settings> <event> <our command> <verb pattern>
	jq -r --arg e "$2" --arg c "$3" --arg v "$4" '
		[ (.hooks[$e] // [])[]? | (.hooks // [])[]? | .command // empty ]
		| map(select(. != $c and test($v)))
		| map("        " + .) | join("\n")' "$1" 2>/dev/null ||
		printf '        (that file could not be re-read to name them)'
}

plan_hook_merge() {
	local settings existing kind
	settings="$(claude_home)/settings.json"
	command -v jq >/dev/null 2>&1 || die "\`--install\` needs \`jq\` to merge its TWO hook entries into
    $settings, and there is no \`jq\` on this PATH. It refuses rather than
    rewriting a JSON file by hand, and NOTHING was installed. Install jq, then
    re-run. The entries it would have merged — the \`SessionStart\` one and
    Amendment 12's \`UserPromptSubmit\` name guard, which are ONE all-or-nothing
    merge:
$(hook_block_text)
$(guard_block_text)"

	# WHAT IS AT THAT PATH, BEFORE A BYTE OF IT IS READ (#44 round 1,
	# `openRepoTools:878`). This file carries the LAST TWO artifacts — the
	# thirtieth and thirty-first — since Amendment 12 adoption act 3 put the
	# name guard beside the `SessionStart` entry, and it is the only path no
	# walk asked about; the
	# `present` arm now CHMODs it, and `chmod`
	# follows a symlink, so a `~/.claude/settings.json` linked into a dotfiles
	# checkout had the 600 stamped on the file at the far end — an unrelated
	# file re-moded by an installer that reported `already installed
	# (unchanged)` for it. The two merge arms are the older half of the same
	# defect: `mv -f` REPLACES a symlink with the regular file it wrote, so the
	# link a person put there was swallowed and the far end silently stopped
	# being what `claude` reads.
	#
	# `path_kind` AND NOT `unplaceable_kind`, because this file is not placed
	# with `cp`: the merge writes a temporary beside it and moves that into
	# place, which takes the DIRECTORY's write bit and not the file's, so a
	# read-only `settings.json` is one this command may still merge into.
	if kind="$(path_kind "$settings")"; then
		die "\`--install\` merges its two hook entries into a REGULAR FILE, and this is not
    one:
    $settings — $kind
    A \`chmod\` follows a link, so the mode this installer stamps would land on
    whatever is on the other end of it; and the merge itself writes a temporary
    beside this path and moves it here, which would take the link away.
    NOTHING was installed and nothing already installed was replaced.
    Clear it — what is on the other end keeps its bytes and its mode — then
    re-run:
        rm -f -- \"$settings\""
	fi

	workdir
	HOOK_MERGED="$WORKDIR/settings.json"
	HOOK_DEFERRED=""
	HOOK_STRIP_WHY=""
	local staged="$WORKDIR/settings.stage.json"

	if [ ! -e "$settings" ]; then
		HOOK_PLAN="nofile"; GUARD_PLAN="nofile"
		jq -n --arg c "$HOOK_COMMAND" --arg m "$HOOK_MATCHER" --argjson t "$HOOK_TIMEOUT" \
		      --arg g "$GUARD_COMMAND" --argjson gt "$GUARD_TIMEOUT" \
			'{hooks: {SessionStart: [{matcher: $m, hooks: [{type: "command", command: $c, timeout: $t}]}],
			          UserPromptSubmit: [{hooks: [{type: "command", command: $g, timeout: $gt}]}]}}' \
			>"$HOOK_MERGED" || die "could not compose $settings; nothing was installed."
		return 0
	fi

	jq -e . "$settings" >/dev/null 2>&1 || die "$settings is not valid JSON, so \`--install\` cannot merge its two entries into
    it and will not rewrite it. NOTHING was installed and nothing already installed was
    replaced. Fix that file, then re-run. The entries it would have merged:
$(hook_block_text)
$(guard_block_text)"

	jq -e '(has("hooks") | not) or (.hooks | type == "object")' "$settings" >/dev/null 2>&1 ||
		die "$settings has a \`hooks\` that is not an object, so there is nothing to merge
    either entry into. NOTHING was installed. The entries it would have merged:
$(hook_block_text)
$(guard_block_text)"

	# THE THREE ANSWERS, in one read: our exact command already there; some
	# OTHER SessionStart entry that runs `session-start` (a second writer of
	# this very hook, which is the case Amendment 9(b) says must refuse); or
	# nothing of ours at all.
	#
	# THE CONFLICT ARM IS NARROWED, AND THE NARROWING IS RATIFIED: A9 Addendum 3
	# R-A9-8 with A9 Addendum 4 R-A9-14, on brettheap/new-workstation#17.
	# Amendment 9(b)'s clause reads "an entry under `SessionStart` whose
	# `command` DIFFERS … → refuse, print the exact block, and place nothing at
	# all", unqualified. Read literally, a person whose `~/.claude/settings.json`
	# carries ANY unrelated SessionStart hook — a statusline refresh, a `git
	# fetch` — could never run `--install` at all, and no amount of removing OUR
	# entry would help them. This workstation carries exactly such an entry
	# today (`herdr-agent-state.sh session`, matcher `*`), so the narrowing is
	# not hypothetical.
	#
	# IT KEYS ON `session-start`, WHICH IS WHAT R-A9-8 SAYS, and not on
	# `lanes-edit.sh`, which is what this line said until R-A9-14 (F7 of the #24
	# review). The two are not the same set either way round: an entry running
	# `lanes-edit.sh who` is no competing writer of this hook and was refused,
	# and `~/bin/lane-hook.sh session-start` IS one — a second program running
	# the same hook verb — and was merged beside, which fires the hook twice,
	# the one harm the clause names. The verb is what collides; the file that
	# carries it is not.
	#
	# AND THE RIVAL IS ASKED ABOUT FIRST, WHICH IS THE WHOLE OF #59. `present`
	# was tested first, so a settings file carrying our exact entry AND a second
	# writer of the same hook answered `present`: `--install` added nothing,
	# said nothing, and left a person with the two writers this arm exists to
	# refuse, with the run reporting success. Nothing was installed twice by
	# such a run — with the exact entry there, there is nothing to add — but the
	# state was never NAMED, and somebody who runs `--install` expecting it to
	# police this hook was told everything was fine.
	#
	# WHICH IS WHY THE RIVAL TEST EXCLUDES OUR OWN STRING (`. != $c`) rather
	# than being asked second: our entry is no rival to itself, and an arm that
	# read it as one would refuse every re-run of `--install` on a workstation
	# that had already taken it. `present` keeps the rest of its meaning — our
	# entry there and nothing competing with it — and `conflict` means what it
	# says whatever else the file carries.
	#
	# THE VERB IS SPELLED ONCE, in `hook_verb` here and `guard_verb` below: the
	# answer and the list of rivals the refusal prints are the same question
	# asked twice, and a pattern written twice is a pair that drifts.
	local hook_verb='session-start'
	existing="$(jq -r --arg c "$HOOK_COMMAND" --arg v "$hook_verb" '
		[ (.hooks.SessionStart // [])[]? | (.hooks // [])[]? | .command // empty ] as $cmds
		| if ($cmds | map(select(. != $c and test($v))) | length) > 0 then "conflict"
		  elif ($cmds | index($c)) then "present"
		  else "absent" end' "$settings" 2>/dev/null || printf 'error')"

	# AND THE SAME THREE ANSWERS FOR AMENDMENT 12's ENTRY, keyed on its own verb.
	#
	# `guard` AS A WHOLE ARGUMENT, and not as a substring — the one place this
	# read differs from the one above, and the difference is load-bearing. The
	# `SessionStart` arm keys on `session-start`, a word that appears in nothing
	# else; `guard` appears in `~/projects/xFactory/guard.sh`, a plain
	# `UserPromptSubmit` hook a person's settings may already carry and one this
	# installer must leave exactly where it is. So the test is the verb with a
	# boundary on both sides: a command RUNNING `guard` collides, a file merely
	# NAMED guard does not. That is R-A9-14's rule one event over — the verb is
	# what collides; the file that carries it is not.
	#
	# THE ORDER IS THE ONE ABOVE'S, FOR THE REASON ABOVE (#59), and so is every
	# other byte of this read: the two arms differ in the EVENT, in OUR OWN
	# STRING and in the verb a rival is recognised by, and in nothing else. An
	# arm that answered a question the other does not is how a person comes to
	# be refused for one entry of a pair and not for the other.
	local existing_guard guard_verb='(^|\s)guard(\s|$)'
	existing_guard="$(jq -r --arg c "$GUARD_COMMAND" --arg v "$guard_verb" '
		[ (.hooks.UserPromptSubmit // [])[]? | (.hooks // [])[]? | .command // empty ] as $cmds
		| if ($cmds | map(select(. != $c and test($v))) | length) > 0 then "conflict"
		  elif ($cmds | index($c)) then "present"
		  else "absent" end' "$settings" 2>/dev/null || printf 'error')"

	case "$existing" in
	conflict)
		die "$settings already carries a SessionStart entry that runs \`session-start\` with a
    DIFFERENT command string:
$(hook_rivals "$settings" SessionStart "$HOOK_COMMAND" "$hook_verb")
    Exact-string match is the only idempotence this entry has, so merging would fire
    the hook twice — and repairing a setting a person meant is not an installer's to
    do. A SECOND WRITER REFUSES WHETHER OR NOT OUR OWN ENTRY IS BESIDE IT: two writers
    of one hook is the state this arm exists to refuse, and a file carrying both is
    that state. NOTHING was installed.
    Remove EVERY entry listed above — all of them are second writers of this hook,
    and one left behind refuses the next run the same way — but not the block below,
    which is this installer's own. Then re-run. The entry this places, byte for byte:
$(hook_block_text)
    (Amendment 9's adoption act 6 removes the hand-installed entry on each
    workstation before the first \`--install\` that carries this.)"
		;;
	present | absent) HOOK_PLAN="$existing" ;;
	*) die "$settings could not be read for a SessionStart entry; nothing was installed." ;;
	esac

	case "$existing_guard" in
	conflict)
		die "$settings already carries a UserPromptSubmit entry that runs \`guard\` with a
    DIFFERENT command string:
$(hook_rivals "$settings" UserPromptSubmit "$GUARD_COMMAND" "$guard_verb")
    Exact-string match is the only idempotence this entry has, so merging would fire
    the name guard twice — two refusals for one prompt. Repairing a setting a person
    meant is not an installer's to do, and A SECOND WRITER REFUSES WHETHER OR NOT OUR
    OWN ENTRY IS BESIDE IT: two writers of one hook is that state either way. So
    NOTHING was installed. Remove EVERY entry listed above — all of them are second
    writers of this hook, and one left behind refuses the next run the same way —
    but not the block below, which is this installer's own. Then re-run.
    The entry this places, byte for byte:
$(guard_block_text)"
		;;
	present | absent) GUARD_PLAN="$existing_guard" ;;
	*) die "$settings could not be read for a UserPromptSubmit entry; nothing was installed." ;;
	esac

	# BOTH MERGES ARE COMPUTED BEFORE EITHER IS PLACED, into one staged file, so
	# the all-or-nothing rule covers the PAIR: a settings file that took the
	# SessionStart entry and refused the guard is a half-install of a hook pair,
	# and nothing on screen would say which half.
	cp -- "$settings" "$HOOK_MERGED" ||
		die "could not stage $settings for merging; nothing was installed."
	if [ "$HOOK_PLAN" = absent ]; then
		jq --arg c "$HOOK_COMMAND" --arg m "$HOOK_MATCHER" --argjson t "$HOOK_TIMEOUT" '
			.hooks = (.hooks // {})
			| .hooks.SessionStart = ((.hooks.SessionStart // []) + [{
				matcher: $m,
				hooks: [{type: "command", command: $c, timeout: $t}]
			  }])' "$HOOK_MERGED" >"$staged" ||
			die "could not merge the SessionStart entry into $settings; nothing was installed."
		mv -f -- "$staged" "$HOOK_MERGED" ||
			die "could not stage the merged $settings; nothing was installed."
	fi
	if [ "$GUARD_PLAN" = absent ]; then
		jq --arg c "$GUARD_COMMAND" --argjson t "$GUARD_TIMEOUT" '
			.hooks = (.hooks // {})
			| .hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) + [{
				hooks: [{type: "command", command: $c, timeout: $t}]
			  }])' "$HOOK_MERGED" >"$staged" ||
			die "could not merge the UserPromptSubmit entry into $settings; nothing was installed."
		mv -f -- "$staged" "$HOOK_MERGED" ||
			die "could not stage the merged $settings; nothing was installed."
	fi

	# THE OTHER OUTCOME, STAGED BESIDE THAT ONE (#237): this same file with OUR
	# EXACT GUARD ENTRY taken out, for a run whose target does not answer
	# `guard`. Only where the file carries it — with nothing of ours to take out,
	# a deferred run has nothing to write, and writes nothing.
	#
	# THE NESTED ARRAY IS FILTERED, NOT THE OUTER ENTRY (workBenches#89): an
	# entry that grouped our command with somebody else's keeps the other
	# command and every other key it has; an entry we took nothing from passes
	# through untouched; and an outer entry goes only when our command was the
	# last thing in it. A shape this does not take apart — a `UserPromptSubmit`
	# or an entry's `hooks` that is not an array — is left exactly as it is, and
	# the read below, the same one `existing_guard` asks, says so.
	if [ "$GUARD_PLAN" = present ]; then
		HOOK_DEFERRED="$WORKDIR/settings.deferred.json"
		jq --arg g "$GUARD_COMMAND" '
			if (.hooks.UserPromptSubmit | type) == "array" then
			  .hooks.UserPromptSubmit = [ .hooks.UserPromptSubmit[]
			    | if type == "object" and ((.hooks | type) == "array") then
			        .hooks as $inner
			        | ($inner | map(select((type == "object" and .command == $g) | not))) as $kept
			        | if ($kept | length) == ($inner | length) then .
			          elif ($kept | length) > 0 then .hooks = $kept
			          else empty end
			      else . end ]
			else . end' "$settings" >"$HOOK_DEFERRED" ||
			die "could not compute $settings without its UserPromptSubmit name guard; nothing was installed."
		if jq -e --arg g "$GUARD_COMMAND" '
			[ (.hooks.UserPromptSubmit // [])[]? | (.hooks // [])[]? | .command // empty ]
			| index($g)' "$HOOK_DEFERRED" >/dev/null 2>&1; then
			HOOK_STRIP_WHY="its \`UserPromptSubmit\`, or the entry carrying the guard, is not the array of entries this command takes apart"
		fi
	fi

	# A GUARD ENTRY THIS COMMAND CANNOT TAKE OUT, AGAINST A TARGET THAT WILL NOT
	# ANSWER, IS A REFUSAL — AND IT IS DECIDED HERE, BEFORE ANYTHING IS PLACED
	# (the #237 review, M2), like every other file this merge cannot handle. The
	# one target the bin loop can still repair is let through to placement: a
	# link to the bin directory's own `lanes-edit.sh`, which that loop puts back
	# and which then answers, so the run merges instead. `hook_pair_defer` keeps
	# the same refusal for the case this read got wrong.
	if [ -n "$HOOK_STRIP_WHY" ] && ! hook_target_answers && ! hook_target_is_ours_to_place; then
		die "$(hook_strip_refusal "NOTHING was installed and nothing already installed was replaced.")"
	fi
}

# THE ONE TARGET A RUN OF `--install` CAN STILL MAKE ANSWER: a symlink spelled
# the way `link-estates` spells it, to the bin directory's own `lanes-edit.sh`,
# which the bin loop places. A bench recreate that emptied that directory
# leaves exactly this link dangling.
hook_target_is_ours_to_place() {
	local target
	target="$(hook_target)"
	[ -L "$target" ] &&
		[ "$(readlink -- "$target" 2>/dev/null || :)" = "${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}/lanes-edit.sh" ]
}

hook_strip_refusal() {   # <what was and was not placed>
	printf '%s' "$(claude_home)/settings.json carries the UserPromptSubmit name guard, and ${HOOK_TARGET_WHY},
    so that entry would fail every prompt — but ${HOOK_STRIP_WHY}, and a file this
    command does not understand is not one it rewrites. Nothing in that file was
    changed. $1 Remove the entry below by hand, then re-run:
$(guard_block_text)"
}

# The fourteen artifacts that are not bin-directory files, in thirteen writes
# (the two hook entries go into settings.json in one), AFTER the seventeen files
# and the merge are all in hand. The last two only once their target answers
# (#237): `place_hook_pair` asks, and says which.
place_skill_and_hook() {
	local shared_dir bare_dir target verb name
	for name in "${SKILLS[@]}"; do
	shared_dir="$(skills_home)/shared/skills/$name"
	bare_dir="$(claude_home)/skills/$name"

	# EVERY FAILURE HERE IS A `die`, never the shell's own 1. `--install`'s
	# codes are this toolset's — 0 done, 1 findings printed, 2 a refusal — and
	# an unguarded `mkdir` or `cp` under `set -e` exits 1, which reads as a
	# report of findings from a command that placed seventeen of thirty-one artifacts.
	for target in "$shared_dir/SKILL.md" "$bare_dir/SKILL.md"; do
		mkdir -p -- "$(dirname -- "$target")" ||
			die "could not create $(dirname -- "$target") for the $name skill.
    The $placed commands above are placed; the skills and the hook are not."
		if [ -e "$target" ] && cmp -s "$WORKDIR/skill-$name.md" "$target"; then
			# THE MODE IS STAMPED WHETHER OR NOT THE BYTES MOVED (#40, finding
			# 2). This arm reported the file unchanged and then SKIPPED the
			# `chmod` below, which sat inside the `else` — so a `SKILL.md` that
			# had drifted to 755, or to anything else, kept that mode for as
			# long as its bytes stayed right and no re-run of `--install` ever
			# put it back. The eleven in the bin directory have been stamped
			# every time all along; this is that same rule, one directory on.
			if mode_is 644 "$target"; then
				verb="already installed at $target (unchanged)"
			else
				verb="already installed at $target (mode restored to 644)"
			fi
			chmod 644 "$target" || die "could not set the mode on $target."
		else
			if [ -e "$target" ]; then verb="updated at $target"; else verb="installed at $target"; fi
			# BY RENAME, as the bin directory's files are (#167). A session reads
			# a `SKILL.md` whole rather than running it, so nothing here executes
			# garbage — but one way to place a file is one rule to read, and a
			# reader that opens it mid-install still sees the old text or the
			# new, never a truncated one.
			place_by_rename "$WORKDIR/skill-$name.md" "$target" 644 "The $placed commands above are placed; the skills and the hook are not."
		fi
		# 644: a SKILL.md is a document a session READS, not a command. The
		# seventeen in the bin directory are stamped 755 for the reason that loop
		# gives — "a copy that is not executable is not a command" — and
		# that reason does not reach this file. Amendment 9(b) describes
		# `--install` as stamping 755 "on everything it places" while
		# arguing why adoption act 4b must delete workBenches#63's 0644
		# loop; the deletion is required either way, because two writers of
		# one path is the defect and the mode was only its symptom.
		say "$name: $verb"
	done
	done

	# THE COMMAND FILES, AT THE SAME TWO DESTINATIONS AND THE SAME MODE
	# (A11 Addendum 4 ruling 9). `setup-claude-profiles.sh` makes
	# `<profiles>/shared/commands` and links it into every profile as
	# `<profile>/commands`, exactly as it does for `skills` — so a file placed
	# in the shared directory is `/swap` in every profile, and the bare copy in
	# `~/.claude/commands/` is `/swap` in a `claude` run outside one. Two
	# destinations, for the two ways a session is started, which is the same
	# reason a skill has two.
	#
	# 644 for the same reason a `SKILL.md` is: a command file is a document a
	# session READS. The 755 argument belongs to the seventeen in the bin
	# directory, where a copy that is not executable is not a command.
	for name in "${COMMANDS[@]}"; do
		for target in "$(skills_home)/shared/commands/$name.md" "$(claude_home)/commands/$name.md"; do
			mkdir -p -- "$(dirname -- "$target")" ||
				die "could not create $(dirname -- "$target") for the /$name command.
    The $placed commands above are placed; the skills, the command files and the hook are not."
			if [ -e "$target" ] && cmp -s "$WORKDIR/command-$name.md" "$target"; then
				# The same stamp on the same arm, for the same reason a skill's
				# is (#40, finding 2): the mode is this installer's to set on
				# every artifact it places, and a mode it repaired is a line
				# that says so rather than `unchanged`.
				if mode_is 644 "$target"; then
					verb="already installed at $target (unchanged)"
				else
					verb="already installed at $target (mode restored to 644)"
				fi
				chmod 644 "$target" || die "could not set the mode on $target."
			else
				if [ -e "$target" ]; then verb="updated at $target"; else verb="installed at $target"; fi
				place_by_rename "$WORKDIR/command-$name.md" "$target" 644 "The $placed commands above are placed; the skills, the command files and the hook are not."
			fi
			say "/$name: $verb"
		done
	done

	place_hook_pair
}

# THE PAIR, BY ONE OF THE TWO OUTCOMES `plan_hook_merge` STAGED (#237), and the
# question that picks it is asked HERE, after the bin loop: the path both entries
# call is a link to the `lanes-edit.sh` that loop has just placed, so a link a
# bench recreate left dangling over an emptied `~/.local/bin` answers again by
# now, and the pair goes in on this same run. Asked earlier, it would defer the
# pair on the one run that put the link's far end back.
#
# `wip init` places the pair through this same function (`wip_hook_pair`), after
# `link-estates`, so the two writers of this file cannot come to two rules.
place_hook_pair() {
	local settings
	settings="$(claude_home)/settings.json"
	if hook_target_answers; then
		hook_pair_merge "$settings"
	else
		hook_pair_defer "$settings"
	fi
}

# WRITTEN THROUGH A TEMPORARY IN THE SAME DIRECTORY, then moved into place:
# `mktemp` + `chmod 600` + `mv -f`, which is what workBenches'
# `scripts/setup-claude-profiles.sh:167-194` does to this very file on every
# `./setup.sh` and what Amendment 9(b) cites approvingly. A plain `cp` that fails
# half way leaves a person's settings TRUNCATED, and this is the one file in this
# installer that is not ours to own. 600, the mode workBenches' own writer of this
# file uses: an installer that widened it would be silently undone, or would widen
# it behind that script's back. Stamped on the TEMPORARY, so the file never
# exists at a wider mode for even an instant. One writer for both outcomes.
hook_write_settings() {   # <the staged whole file> <settings>
	local staged="$1" settings="$2" hook_tmp
	mkdir -p -- "$(dirname -- "$settings")" ||
		die "could not create $(dirname -- "$settings") for the hook entries."
	hook_tmp="$(mktemp -- "$(dirname -- "$settings")/settings.json.openrepotools.XXXXXX")" ||
		die "could not create a temporary file beside $settings."
	cp -- "$staged" "$hook_tmp" ||
		{ rm -f -- "$hook_tmp"; die "could not stage the merged $settings."; }
	chmod 600 "$hook_tmp" ||
		{ rm -f -- "$hook_tmp"; die "could not set mode 600 on the merged $settings."; }
	mv -f -- "$hook_tmp" "$settings" ||
		{ rm -f -- "$hook_tmp"; die "could not move the merged settings into $settings."; }
}

# THE TARGET ANSWERS: the pair, merged exactly as it was before #237.
hook_pair_merge() {   # <settings>
	local settings="$1"
	case "$HOOK_PLAN" in
	present)
		# THE ENTRY BEING THERE SAYS NOTHING ABOUT THE MODE (#40, finding 3).
		# The two arms below write this file through `mktemp` + `chmod 600` +
		# `mv -f`, so a file THEY wrote is 600 — and a file whose entry was
		# already present was never given one, which is how a direct `--install`
		# could leave `~/.claude/settings.json` world-readable for ever. 600 is
		# the narrowing of the two and the mode workBenches'
		# `setup-claude-profiles.sh` writes this same file at, so an installer
		# that left it wider would be silently undone or would widen it behind
		# that script's back.
		#
		# THIS CHMOD CAN FAIL FOR THE SAME REASON `unplaceable_kind`'s CAN (#44
		# round 1, the suppressed comment at `openRepoTools:880`): a
		# `settings.json` this euid can write but does not own takes `jq` and
		# every check `plan_hook_merge` puts to it, then refuses the mode
		# stamp, with the seventeen commands, the skills and the command files —
		# twenty-nine of the thirty-one artifacts — already placed. The guard that
		# would catch it first is the same `[ -O ]` `unplaceable_kind`'s own
		# comment declines, for the same reason: no test in this suite can
		# make a second account to own this file, and the check would refuse a
		# root `--install` into another user's home, where this `chmod` does
		# in fact succeed. The failure stays a `die` naming this file.
		if mode_is 600 "$settings"; then
			say "SessionStart hook: already installed in $settings (unchanged)"
		else
			chmod 600 "$settings" ||
				die "could not set mode 600 on $settings, which already carries this entry.
    Nothing in that file was changed."
			say "SessionStart hook: already installed in $settings (mode restored to 600)"
		fi
		;;
	esac
	case "$GUARD_PLAN" in
	present)
		# THE GUARD ENTRY'S OWN LINE, AND NO SECOND `chmod`: the mode belongs to
		# the FILE and not to either entry, so the arm above has already asked
		# for it on every run that reaches this one. Saying it twice would tell
		# a person their settings were re-moded twice for one file.
		say "UserPromptSubmit guard: already installed in $settings (unchanged)"
		;;
	esac
	# ONE WRITE FOR THE TWO ENTRIES: they live in one file, and writing it twice
	# would move a person's settings through a state carrying one of the pair.
	if [ "$HOOK_PLAN" != present ] || [ "$GUARD_PLAN" != present ]; then
		hook_write_settings "$HOOK_MERGED" "$settings"
		case "$HOOK_PLAN" in
		nofile) say "SessionStart hook: installed in $settings (the file was created, mode 600)" ;;
		absent) say "SessionStart hook: merged into $settings (one entry added; written back at mode 600)" ;;
		esac
		case "$GUARD_PLAN" in
		nofile) say "UserPromptSubmit guard: installed in $settings (the file was created, mode 600)" ;;
		absent) say "UserPromptSubmit guard: merged into $settings (one entry added; written back at mode 600)" ;;
		esac
	fi
}

# THE TARGET DOES NOT ANSWER: the pair is DEFERRED, and each entry's line says
# what this run did about it. Nothing is added. OUR GUARD ENTRY, where the file
# carries it, is taken out — an entry calling a path that does not answer
# `guard` fails every prompt, by exit 127, by exit 126 or by `unknown
# subcommand`'s 2 — and the file is written back at 600 with every other hook
# where it was. Our `SessionStart` entry, where it is there, is LEFT: its
# `|| true` keeps it from failing a session, and the file that carries it keeps
# the mode this installer stamps on it. No line here says `unchanged` or
# `installed`, so a deferred run's count is the twenty-nine it placed.
hook_pair_defer() {   # <settings>
	local settings="$1" wrote=0
	if [ "$GUARD_PLAN" = present ]; then
		[ -z "$HOOK_STRIP_WHY" ] ||
			die "$(hook_strip_refusal "Whatever the lines above report is placed.")"
		hook_write_settings "$HOOK_DEFERRED" "$settings"
		wrote=1
	fi
	case "$HOOK_PLAN" in
	present)
		if [ "$wrote" -eq 1 ] || mode_is 600 "$settings"; then
			say "SessionStart hook: left in $settings as an earlier run placed it (it ends in \`|| true\`, so it cannot fail a session while the pair is deferred)"
		else
			chmod 600 "$settings" ||
				die "could not set mode 600 on $settings, which already carries the SessionStart entry.
    Nothing in that file was changed."
			say "SessionStart hook: left in $settings as an earlier run placed it (it ends in \`|| true\`, so it cannot fail a session while the pair is deferred; mode restored to 600)"
		fi
		;;
	*)
		say "SessionStart hook: deferred, not added to $settings, because ${HOOK_TARGET_WHY}"
		;;
	esac
	case "$GUARD_PLAN" in
	present)
		say "UserPromptSubmit guard: REMOVED from $settings, because ${HOOK_TARGET_WHY}, and left there it would fail every prompt. Only that entry was taken out: every other hook is where it was, and the file was written back at mode 600"
		;;
	*)
		say "UserPromptSubmit guard: deferred, not added to $settings, because ${HOOK_TARGET_WHY}"
		;;
	esac
	hook_pair_remedy
}
# =========================================================================
# `wip init` — THE ONE ACT THAT CREATES THE WORKSPACE THOSE COMMANDS READ
# =========================================================================
#
# lane-collision-protocol Amendment 9(c), under Brett Heap's ruling 4,
# "openRepoTools wip init". It is why the line at the top of this file changed:
# this command installs the estate's commands, and owns the one act that
# creates the workspace repository those commands read. It still scaffolds no
# product repository — that refusal is `openRepoShape`'s, and nothing here
# touches it.
#
# IT ASKS AT MOST ONE QUESTION, and only where an administrator has to act
# (step 5). Brett Heap, 2026-09-12T18:56Z, on the onboarding chain: "there are
# two options in resume. we need to also keep that clean so there is the least
# choices possible to not confuse the user." Everything else is DERIVED, and no
# value it prints is ever left as a placeholder for a person to fill (R-A9-3,
# ratified).
#
# EXIT CODES ARE 0 AND 2, and 2 only. 0 is done or already done; 2 is a
# refusal. There is no 1 here: `1` in this toolset means "findings were
# printed", and this command reports no findings. THE MOVED LANE HELPERS KEEP
# THEIR OWN CODES, which are the lane protocol's — `lanes-edit.sh` exits 1 for
# *registry not found*. Two toolsets ship under one installer now, so one
# number means two things across that seam: no caller may read a number without
# knowing which command produced it.
#
# A REFUSAL FROM HERE IS NOT A CAPABILITY SIGNAL. `die`'s default exit is also
# 2, so an unknown argument and a genuine refusal are the same number. A caller
# that wants to know whether `wip` exists PROBES `openRepoTools --help` for the
# verb pair, never an exit code — read the other way round, a genuine refusal
# with the administrator's `gh repo create` block already on the terminal would
# be reported as "no such subcommand", and a degradation recipe printed over
# the top of the one thing the person needed. opensoft/workBenches#68 probes
# `--help` for exactly this reason.

#: The organisation the repository is created in. Shipped organisation data,
#: the same kind `repos.tsv` is (ruling 3) — never asked for and never printed
#: unfilled. The repository is org-owned rather than personal so that the index
#: survives offboarding and a later lane can read the handoffs.
# THE SHIPPED DEFAULT ORGANISATION, a constant and not an environment
# variable. Amendment 9(c) step 3 derives the organisation from `--org` where
# given and otherwise from "the toolset's shipped default organisation", and
# clause (e) says of workBenches' setup step that it passes `--org` only where
# an operator set `WORKBENCHES_WORKSPACE_ORG`, "because an operator override
# has NO OTHER WAY IN". A second way in here would falsify that sentence and
# give one value three sources.
WIP_ORG_DEFAULT="opensoft"

#: `{{SHAPE_REPOSITORY}}`'s value: a constant here, by clause (c) step 7's
#: substitution table.
SHAPE_REPOSITORY="opensoft/openRepoShape"

#: The template, `templates/workspace-root/` in openRepoShape (clause (d)).
#: Nine files, named here because the amendment names them: a directory listing
#: fetched at runtime would make the set whatever upstream happened to hold.
WIP_TEMPLATE_DIR="templates/workspace-root"
WIP_TEMPLATE_FILES=(
	README.md
	AGENTS.md
	CLAUDE.md
	.gitattributes
	.gitignore
	handoffs/README.md
	lanes/LANES.md
	lanes/log/README.md
	workspaces/.gitkeep
)

WIP_LOGIN=""
WIP_ORG=""
WIP_TEAM=""
WIP_TEAM_WHY=""
WIP_DRY_RUN=0

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

# Where a new clone goes. `$PROJECTS_DIR` names one directory when it is set;
# otherwise `~/projects`, then `~/Projects` where that exists and is a
# different directory — the block `park`, `resume` and `status` already carry,
# narrowed to the one question this command asks of it.
wip_projects_dir() {
	if [ -n "${PROJECTS_DIR:-}" ]; then printf '%s\n' "$PROJECTS_DIR"; return 0; fi
	[ -d "$HOME/projects" ] && { printf '%s\n' "$HOME/projects"; return 0; }
	if [ -d "$HOME/Projects" ]; then printf '%s\n' "$HOME/Projects"; return 0; fi
	printf '%s\n' "$HOME/projects"
}

wip_yaml_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"
}

wip_norm_repo() {
	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:]'
}

# Is $2 the ROOT of a checkout of $1? The same three-part test the four lane
# helpers make, because step 1's idempotence and their refusal have to agree on
# what "already has a workspace" means, or a host can pass one and fail the
# other. Local only; no network.
wip_is_checkout_of() {
	local want="$1" path="$2" origin toplevel real
	[ -n "$path" ] || return 1
	git -C "$path" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
	origin="$(wip_norm_repo "$(git -C "$path" remote get-url origin 2>/dev/null || :)")"
	want="$(wip_norm_repo "$want")"
	if [ -z "$origin" ] || [ "$origin" != "$want" ]; then
		# The origin's path ENDING in the `<owner>/<name>` the pointer file
		# spells is the same repository: a GitHub Enterprise host, an ssh alias
		# and a local bare mirror laid out by owner and name all say that, and
		# the pointer file spells `<owner>/<name>` because that is what a person
		# types. The four lane helpers make this same test, in the same words.
		case "$origin" in
		*/"$want") ;;
		*) return 1 ;;
		esac
	fi
	toplevel="$(git -C "$path" rev-parse --show-toplevel 2>/dev/null || :)"
	real="$(cd -- "$path" 2>/dev/null && pwd -P)" || return 1
	[ "$toplevel" = "$real" ]
}

wip_have_gh() { command -v gh >/dev/null 2>&1; }

# --- step 2: the login ------------------------------------------------------
#
# `gh api user -q .login`, LOWERCASED — the naming contract's own definition of
# `<user>`. A login that cannot form a compliant name under that family's
# pattern is a refusal naming the pattern and its source, never a name repaired
# by guesswork (R-A9-3).
wip_login_is_compliant() {
	case "$1" in
	'' | *[![:lower:][:digit:]-]* | -* | *- | *--*) return 1 ;;
	*) return 0 ;;
	esac
}

wip_derive_login() {
	local login
	wip_have_gh || die "\`wip init\` needs your GitHub login and there is no \`gh\` to read it with.
    Install GitHub CLI, run \`gh auth login\`, then run \`openRepoTools wip init\` again.
    Nothing was created."
	if command -v timeout >/dev/null 2>&1; then
		login="$(timeout 20 gh api user -q .login 2>/dev/null || :)"
	else
		login="$(gh api user -q .login 2>/dev/null || :)"
	fi
	login="$(printf '%s' "$login" | tr '[:upper:]' '[:lower:]')"
	[ -n "$login" ] || die "\`gh api user\` did not answer with a login, so this command does not know whose
    workspace to create. That is the one precondition it has:
        gh auth login
    Then run \`openRepoTools wip init\` again. Nothing was created."
	wip_login_is_compliant "$login" || die "the GitHub login '$login' cannot form a compliant \`<user>-wip\` name.
    openRepoShape's naming contract requires \`^[a-z0-9]+(-[a-z0-9]+)*-wip\$\`, and an
    underscore, a doubled hyphen or a leading or trailing one does not fit it. This
    command does not repair a name by guesswork. Name the repository yourself:
        openRepoTools wip init --login <name>
    Nothing was created."
	printf '%s\n' "$login"
}

# --- step 4: the team, or no team ------------------------------------------
#
# `gh api /user/teams` filtered to the organisation. EXACTLY ONE match is used.
# None, or more than one, and `--team` is omitted ENTIRELY — from what is run
# and from what is printed — with one line saying why. A placeholder a person
# must fill is a question wearing a different hat, and this command asks at
# most once. (Ratified decision 2.)
wip_derive_team() {
	local org="$1" teams count
	WIP_TEAM=""
	WIP_TEAM_WHY=""
	if ! wip_have_gh; then
		WIP_TEAM_WHY="no \`gh\` to read /user/teams with"
		return 0
	fi
	teams="$(gh api --paginate /user/teams \
		-q ".[] | select((.organization.login | ascii_downcase) == \"$(printf '%s' "$org" | tr '[:upper:]' '[:lower:]')\") | .slug" \
		2>/dev/null || :)"
	count="$(printf '%s' "$teams" | grep -c . || :)"
	case "${count:-0}" in
	1)
		WIP_TEAM="$(printf '%s' "$teams" | head -n 1)"
		;;
	0)
		WIP_TEAM_WHY="you are in no team in $org"
		;;
	*)
		WIP_TEAM_WHY="you are in $count teams in $org, and this command does not choose between them"
		;;
	esac
	return 0
}

# --- step 5: the block an administrator runs -------------------------------
#
# THREE ADMINISTRATOR ACTS, NOT TWO (Addendum 1(2) on brettheap/new-workstation#17).
# Every value of steps 2 to 4 is already substituted, so what is printed runs as
# it stands. The third is not a fixed command, because a ruleset's id is not
# derivable without reading the organisation's rulesets: the block prints the
# one call that lists them, with the organisation substituted, and names the
# change to make.
# GIT'S OWN WORDS, KEPT (F9 of the #24 review, ruled R-A9-15). The three
# failures a FRESH workstation actually meets are here — the clone, the stage
# and the commit — and all three ran `>/dev/null 2>&1` and died with a summary
# of this command's own devising. A machine with no `user.email` configured got
# `could not commit the seed in <path>.` and nothing else, from the command
# whose whole job is to set up a new machine, when git had already said exactly
# what to do about it. So the command runs, its stderr is kept, and the last of
# it is printed UNDER the refusal — the shape `fetch_from_repo`'s refusal
# already has, where it names both ways it tried.
#
# STDOUT IS STILL DROPPED FROM THE REPORT, and it is now dropped INTO A FILE
# rather than into `/dev/null`: `gh repo clone` and `git commit` are chatty on
# success and this command's own `say` lines are what a person reads, so nothing
# here ever prints it. It is the WORDS OF A FAILURE that were being thrown away,
# not progress. The file is what lets a command whose OUTPUT is the answer — step
# 6a's `git status`, #49 — run through this one wrapper instead of carrying a
# second copy of the stderr-capture below: `$WIP_LAST_OUT` names it, it lives in
# the workdir the `EXIT` trap removes, and it is rewritten by every call, so a
# caller reads it before making the next one.
WIP_LAST_ERR=""
WIP_LAST_OUT=""
wip_quietly() {   # <command> [args…] — 0 or 1, stderr in $WIP_LAST_ERR, stdout AT $WIP_LAST_OUT
	workdir
	WIP_LAST_ERR=""
	WIP_LAST_OUT="$WORKDIR/cmd-out"
	if "$@" >"$WIP_LAST_OUT" 2>"$WORKDIR/cmd-err"; then
		return 0
	fi
	WIP_LAST_ERR="$(tail -n 6 -- "$WORKDIR/cmd-err" 2>/dev/null | sed -e 's/^/    | /')"
	[ -n "$WIP_LAST_ERR" ] || WIP_LAST_ERR="    | (it printed nothing at all)"
	return 1
}

wip_admin_block() {
	local target="$1" org="$2" team="$3"
	say ""
	say "  # 1. create the repository (org-owned, private)"
	if [ -n "$team" ]; then
		say "  gh repo create $target --private --team $team \\"
	else
		say "  gh repo create $target --private \\"
	fi
	say "      --description \"${WIP_LOGIN}'s workspace repository: lanes, handoffs, workspaces\""
	say ""
	if [ -n "$team" ]; then
		say "  # 2. give the team read; --team alone grants the org default, not read"
		say "  gh api --method PUT \\"
		say "      /orgs/$org/teams/$team/repos/$target -f permission=pull"
		say ""
		say "  # 3. exclude it from the organisation's PR-only ruleset. Without this,"
	else
		say "  # 2. exclude it from the organisation's PR-only ruleset. Without this,"
	fi
	say "  #    Rule 9's one commit per write cannot land and the register is"
	say "  #    unwritable. List the rulesets, then add $target to the"
	say "  #    PR-only one's exempt repositories:"
	say "  gh api /orgs/$org/rulesets"
	say ""
	if [ -z "$team" ]; then
		say "  (No \`--team\` and no team grant: ${WIP_TEAM_WHY:-no single team matched}. The"
		say "   repository is created without one and an administrator may add one.)"
		say ""
	fi
}

# --- step 7: the template, and the whole of its substitution table ----------
#
# openRepoTools substitutes the placeholders ITSELF. openRepoShape's engine
# (`scripts/shape_materialize.py`) is reached only by that repository's own
# tools, and `grep -rn 'workspace-root'` finds no code path there for this
# template at all — so the consumer substitutes, the consumer is this command,
# and the tokens are exactly these three (clause (c) step 7):
#
#   {{LOGIN}}             the lowercased GitHub login      step 2
#   {{ORG}}               the organisation                 step 3
#   {{SHAPE_REPOSITORY}}  opensoft/openRepoShape           constant here
#
# A TOKEN THE TABLE DOES NOT CARRY IS A REFUSAL NAMING THE TOKEN — never a
# blank, and never a literal `{{…}}` written into a person's repository. That
# is what openRepoShape's own engine does with an unsubstituted token
# (`Refusal("template-unsubstituted", …)`), and it is why the table is written
# here rather than left to a private function that is no published contract.
wip_substitute() {
	local file="$1" left
	# `|` as the sed delimiter would collide with the table rows in LANES.md;
	# these three values are a login, an `<org>` and an `<owner>/<name>`, so `#`
	# cannot appear in any of them and is safe as the delimiter.
	sed -e "s#{{LOGIN}}#$WIP_LOGIN#g" \
		-e "s#{{ORG}}#$WIP_ORG#g" \
		-e "s#{{SHAPE_REPOSITORY}}#$SHAPE_REPOSITORY#g" \
		-- "$file" >"$file.subst" || die "could not substitute the template file $file"
	left="$(grep -o -e '{{[A-Z_]*}}' "$file.subst" 2>/dev/null | sort -u | tr '\n' ' ' || :)"
	if [ -n "$left" ]; then
		rm -f -- "$file.subst"
		die "the template carries a placeholder this command has no value for: $left
    (in ${file##*/}). Amendment 9(c) step 7's substitution table is {{LOGIN}}, {{ORG}}
    and {{SHAPE_REPOSITORY}}, and a token outside it is a refusal naming the token —
    never a blank, and never a literal {{…}} written into your repository.
    Nothing was created."
	fi
	# THE WORKSPACE'S `.gitignore` CARRIES THE BYTECODE AND CACHE RULES
	# (opensoft/openRepoTools#162, cause 19: generated bytecode committed into
	# handoff attachments). Added HERE, in the one function both step 7 paths
	# go through, so the bytes a seed writes and the bytes a re-run compares
	# an earlier run's leftover against are the same bytes. A line the template
	# already carries is not repeated.
	case "${file##*/}" in
	tpl-.gitignore) wip_hygiene_lines "$file.subst" >>"$file.subst" ||
		die "could not add the bytecode and cache rules to the template file $file" ;;
	esac
	mv -f -- "$file.subst" "$file"
}

#: The lines every `<user>-wip` ignores (#162): bytecode, test and type-check
#: caches, dependency trees and virtual environments - a handoff attachment
#: is evidence, and none of these is.
WIP_HYGIENE_LINES=(
	"__pycache__/"
	"*.pyc"
	"*.pyo"
	".pytest_cache/"
	".mypy_cache/"
	".ruff_cache/"
	"node_modules/"
	".venv/"
	"venv/"
	"site-packages/"
)

# The hygiene block, less every line `$1` already carries; nothing at all when
# it carries them all. A bare `grep`, never `command grep`
# (opensoft/openRepoTools#292): its failure IS the missing line, and macOS's
# bash 3.2 ends a `set -e` script where a utility run through `command` fails,
# even on the left of `||`.
wip_hygiene_lines() {   # <the .gitignore as it stands>
	local line missing=""
	for line in "${WIP_HYGIENE_LINES[@]}"; do
		grep -Fqx -- "$line" "$1" 2>/dev/null || missing="$missing$line
"
	done
	[ -n "$missing" ] || return 0
	printf '\n# Bytecode, caches, dependency trees and virtual environments are never\n'
	printf '# a workspace repository'"'"'s: a handoff attachment is evidence, not a build\n'
	printf '# (opensoft/openRepoTools#162). `lanes-edit.sh` refuses to commit them.\n'
	printf '%s' "$missing"
}

# The template's bytes, materialized EXACTLY the way `--install` gets its own
# files: from the checkout beside this command when run from one, else
# `gh api …/contents/<path>` at the pinned ref, else the raw URL. *That the
# template travels by the installer's own fetch path is the drafter's; the
# issue named the template and not its transport.*
wip_shape_ref() {
	local pin selfdir=""
	[ -f "$SELF" ] && selfdir="$(cd -- "$(dirname -- "$SELF")" && pwd)"
	if [ -n "$selfdir" ] && [ -f "$selfdir/contracts/openreposhape-pin.yaml" ]; then
		pin="$(sed -n -e 's/^commit:[[:space:]]*//p' "$selfdir/contracts/openreposhape-pin.yaml" | head -n 1)"
		pin="${pin%\"}"; pin="${pin#\"}"
		[ -n "$pin" ] && { printf '%s\n' "$pin"; return 0; }
	fi
	workdir
	# NO PIN BESIDE THIS FILE — the installed copy, a vendored tree: the pin is
	# the one the CODE carries (#186). A single repository's is read exactly as
	# it always was, at `$REF`; an adopted assembly's is the code leg's, at the
	# commit the assembly pins, never the assembly's own `contracts/`. A source
	# that does not answer leaves `main`, as it always did, and so does a pin
	# the source does not carry. An adopted assembly whose code cannot be
	# resolved refuses, in step 7's words (`resolve_refuse`).
	RESOLVE_FOR=wip
	payload_probe
	case "$PROBE_LAYOUT" in
	unknown) printf 'main\n'; return 0 ;;
	single) : ;;
	*) payload_resolve_remote ;;
	esac
	if fetch_from_repo "contracts/openreposhape-pin.yaml" "$WORKDIR/pin.yaml"; then
		pin="$(sed -n -e 's/^commit:[[:space:]]*//p' "$WORKDIR/pin.yaml" | head -n 1)"
		pin="${pin%\"}"; pin="${pin#\"}"
		[ -n "$pin" ] && { printf '%s\n' "$pin"; return 0; }
	fi
	printf 'main\n'
}

wip_fetch_template_file() {
	local rel="$1" dest="$2" ref="$3" selfdir="" path="$WIP_TEMPLATE_DIR/$rel"
	[ -f "$SELF" ] && selfdir="$(cd -- "$(dirname -- "$SELF")" && pwd)"
	if [ -n "$selfdir" ] && [ -f "$selfdir/upstream/openRepoShape/$path" ]; then
		cp -- "$selfdir/upstream/openRepoShape/$path" "$dest"
		return 0
	fi
	if command -v gh >/dev/null 2>&1 &&
		gh api "repos/$SHAPE_REPOSITORY/contents/$path?ref=$ref" \
			-H 'Accept: application/vnd.github.raw' >"$dest" 2>/dev/null &&
		[ -s "$dest" ]; then
		return 0
	fi
	if command -v curl >/dev/null 2>&1 &&
		curl -fsSL "https://raw.githubusercontent.com/$SHAPE_REPOSITORY/$ref/$path" \
			>"$dest" 2>/dev/null && [ -s "$dest" ]; then
		return 0
	fi
	# `workspaces/.gitkeep` is zero bytes upstream, so `-s` above is false for
	# it on every path. It carries no placeholder and no content by definition:
	# git tracks no empty directory, and that is the whole of its job.
	case "$rel" in
	*/.gitkeep | .gitkeep) : >"$dest"; return 0 ;;
	esac
	return 1
}

# --- step 6a's history question: is this commit one `wip init` made? --------
#
# STEP 6a LETS THROUGH, AHEAD OF origin's `main`, THIS COMMAND'S OWN SEED AND
# NOTHING ELSE (#64), so the answer here is ALL of what step 8's own commit is
# and never one part of it a person's commit could happen to share. Four tests,
# and a commit has to pass every one:
#
#   * ONE PARENT OR NONE. Step 8 commits on top of HEAD — on an empty `main`
#     that is a root commit — and never merges. It is asked first because the
#     path test below cannot ask it: `diff-tree` prints NOTHING for a merge
#     unless it is asked for a combined diff, so a merge would answer "no path
#     outside the template" by naming no path at all.
#   * THE SUBJECT step 8 writes, `Seed this workspace repository from …` — a
#     PREFIX, because what follows it names the template's repository and
#     directory, which are this file's constants and not a part of the promise.
#   * THE BODY'S `Written by \`openRepoTools wip init\`` line, which is the
#     sentence in that message that says whose commit it is.
#   * AND A TREE THAT DIFFERS FROM ITS PARENT'S — the empty tree, for a root
#     commit, which is what `--root` compares it with — AT ONE OR MORE TEMPLATE
#     PATHS AND AT NO OTHER. Step 8 commits `"${written[@]}"` by pathspec, which
#     is template paths and nothing else, and commits only where `diff --cached`
#     says there is something to commit, so it has never made an empty one.
#
# A MESSAGE IS SOMETHING ANYBODY CAN TYPE, which is why the tree is asked as
# well as the words: a commit wearing the seed's subject and trailer and
# touching a path the template does not name is somebody's own work in a
# borrowed coat, and it is refused like any other. What this does NOT do is
# compare the bytes at those paths with the template: the seed it is looking
# at was written from the template at whatever commit THAT run pinned, which
# need not be this run's, and Brett Heap's ruling on #64 asks for the subject,
# the trailer and the tree — the parent count is the tree test made sound —
# and not the bytes. A person who writes this command's own message over
# their own edit of a template file has said, in so many words, that it is
# the seed.
#
# `--no-show-signature` ON EVERY `log` HERE, because `log.showSignature` is a
# person's own config and it prints the signature check's report ON STDOUT,
# ahead of the format: a seed this command committed on a machine with
# `commit.gpgSign` set would then read back with that report as its first
# line — `No signature`, `Good "git" signature …` — instead of its subject,
# and be refused as somebody else's.
#
# EVERY READ THAT FAILS IS A NO, so a commit this could not ask about is
# refused and never passed (#49's lesson, asked of history). Paths are
# compared as `diff-tree` prints them: one it had to C-quote is never one of
# `$WIP_TEMPLATE_FILES`, which are nine plain ASCII paths, so it is refused
# rather than misread.
wip_is_own_seed() {   # <checkout> <commit> — 0 only for a commit `wip init` itself made
	local checkout="$1" commit="$2" parents subject body paths path rel known
	parents="$(git -C "$checkout" log -1 --no-show-signature --format=%P "$commit" 2>/dev/null)" ||
		return 1
	case "$parents" in *' '*) return 1 ;; esac
	subject="$(git -C "$checkout" log -1 --no-show-signature --format=%s "$commit" 2>/dev/null)" ||
		return 1
	case "$subject" in
	'Seed this workspace repository from '*) ;;
	*) return 1 ;;
	esac
	body="$(git -C "$checkout" log -1 --no-show-signature --format=%b "$commit" 2>/dev/null)" ||
		return 1
	case "$body" in
	*'Written by `openRepoTools wip init`'*) ;;
	*) return 1 ;;
	esac
	paths="$(git -C "$checkout" diff-tree --root --no-commit-id --name-only -r "$commit" 2>/dev/null)" ||
		return 1
	[ -n "$paths" ] || return 1
	while IFS= read -r path; do
		known=0
		for rel in "${WIP_TEMPLATE_FILES[@]}"; do
			if [ "$path" = "$rel" ]; then
				known=1
				break
			fi
		done
		[ "$known" -eq 1 ] || return 1
	done <<EOF
$paths
EOF
	return 0
}

# --- step 10's two acts: the links, then the hook pair (#237) ---------------
#
# `link-estates` places the symlinks and reports the ones it skipped. One of
# them is `~/projects/xFactory/lanes-edit.sh`, the path both of `--install`'s
# hook entries call — which is why the pair is completed here, after it, and
# why step 1 runs this again on a workstation whose link is missing.
wip_run_link_estates() {
	local linker="${OPENREPOTOOLS_BIN_DIR:-$HOME/.local/bin}/link-estates"
	[ -x "$linker" ] || linker="$(command -v link-estates 2>/dev/null || :)"
	if [ -n "$linker" ] && [ -x "$linker" ]; then
		say ""
		say "  link-estates:"
		# ONE PROJECTS TREE ACROSS THE SEAM. This toolset spells that directory
		# `$PROJECTS_DIR` (`park:131`, `resume:114`, and `wip_init`'s step 6); the lane
		# protocol's commands, which arrived from the workspace repository,
		# spell it `$PROJECTS_ROOT` (`link-estates:200`, `lane-start:427`).
		# Amendment 9(g) keeps every mechanic of Amendments 6 to 8, so the
		# helpers keep their name — but step 10 must run the linker against the
		# tree step 6 actually cloned into, or a host with `$PROJECTS_DIR` set
		# to anything but the default gets a clone in one tree and its symlinks
		# looked for in another. Passed only where the person has not set
		# `$PROJECTS_ROOT` themselves: their answer wins over our inference.
		PROJECTS_ROOT="${PROJECTS_ROOT:-$(wip_projects_dir)}" "$linker" ||
			say "  (link-estates reported the above and changed nothing else)"
	else
		say ""
		say "  link-estates is not installed, so no symlinks were placed."
		say "  \`openRepoTools --install\` places it; then run \`link-estates\`."
	fi
}

# THE HOOK PAIR, BY `--install`'s OWN READ AND ITS OWN WRITE (#237): the rival
# refusal, the two staged outcomes and the gate are `plan_hook_merge` and
# `place_hook_pair`, word for word, so `wip init` and `--install` are one rule
# for one file. In a SUBSHELL, because a refusal there is `die`: it prints what
# `--install` would have refused with and exits 2 — the subshell, not this run.
# The workspace this command exists to make is made by then, so the refusal is a
# note beside it and the exit stays 0, the way a `link-estates` that refused is
# (step 10). ONE RULE ON BOTH PATHS: on step 1's the reconcile is the only act,
# and the exit is still 0 — a 2 there would tell a caller the workspace failed,
# which it did not — so the usage and the README say so where the exit codes
# are (the #237 review, S1). `set -e` is put back on INSIDE the subshell: the
# `set +e` that lets this read the subshell's status would otherwise reach
# everything the two functions run, and they are written for `--install`'s
# `set -e`.
wip_hook_pair() {
	local rc
	say ""
	say "  hook entries in $(claude_home)/settings.json:"
	workdir
	set +e
	( set -e; plan_hook_merge; place_hook_pair )
	rc=$?
	set -e
	if [ "$rc" -ne 0 ]; then
		say "  (not reconciled: the refusal above is the one \`openRepoTools --install\` makes, and"
		say "   nothing in that file was changed. Fix what it names, then run \`openRepoTools wip init\` again.)"
	fi
	return 0
}

wip_hook_pair_would() {
	say "  would merge   the SessionStart and UserPromptSubmit hook entries into"
	say "                $(claude_home)/settings.json, once ~/projects/xFactory/lanes-edit.sh"
	say "                answers \`guard\` (until then each run says it deferred them)"
}

# --- the command itself -----------------------------------------------------
wip_init() {
	local config target checkout ref rel dest seeded=0 owed=0 kind
	local existing_repo existing_path
	local ancestor rest comp staged_mode staged_entry index_tag ancestor_mode
	# THE TEMPLATE PATHS THIS CHECKOUT HAS IN HEAD AND NOT ON DISK, because the
	# index marks them `skip-worktree` (#69). Counted as well as collected, for
	# the reason `written` is: an empty array expanded as `"${sparse_paths[@]}"`
	# is an UNBOUND VARIABLE under `set -u` in the bash 3.2 macOS ships.
	local sparse=0 sparse_paths
	sparse_paths=()
	# THE PATHS THE SEED OWES THIS CHECKOUT, which is what step 8 stages and
	# commits and the whole of what it commits: the ones step 7 wrote (`seeded`)
	# and the ones an earlier run wrote and could not commit (`owed`). Two
	# counters rather than `${#written[@]}`: an empty array expanded as
	# `"${written[@]}"` is an UNBOUND VARIABLE under `set -u` in the bash 3.2
	# macOS ships, so every reader of this list is guarded on a count, and the
	# count that says how many were COPIED is the one step 7's line prints.
	local written
	written=()

	# ---- step 1: DETECT. A file test, not a network call, and the whole of the
	# idempotence. A host that already has a workspace does nothing at all.
	config="$(wip_config_file)"
	if [ -f "$config" ]; then
		existing_repo="$(wip_yaml_field repository "$config")"
		existing_path="$(wip_yaml_field path "$config")"
		if [ -n "$existing_repo" ] && [ -n "$existing_path" ] &&
			wip_is_checkout_of "$existing_repo" "$existing_path"; then
			say "workspace repository: $existing_repo"
			say "  checked out at      $existing_path"
			say "  named by            $config"
			say "openRepoTools wip init: nothing to do for the workspace itself."
			# THE HOOK PAIR IS THE ONE THING A RE-RUN STILL OWES (#237). A run cut
			# off between step 9 and step 10 — or one whose `link-estates` could
			# not link yet — left a workspace this file test accepts and no link
			# for `--install`'s two entries to call, and nothing else would ever
			# heal it: every later run returned here. So, only where that path
			# does not answer `guard` and `~/projects/xFactory` is there for the
			# link to go into, `link-estates` runs again; then the pair is
			# reconciled, which on a healthy workstation writes nothing.
			if [ "$WIP_DRY_RUN" -eq 1 ]; then
				say ""
				say "--dry-run, so nothing below was done:"
				if ! hook_target_answers && [ -d "$HOME/projects/xFactory" ]; then
					say "  would run     link-estates (${HOOK_TARGET_WHY})"
				fi
				wip_hook_pair_would
				return 0
			fi
			if ! hook_target_answers && [ -d "$HOME/projects/xFactory" ]; then
				say "  ${HOOK_TARGET_WHY}, so link-estates runs again:"
				wip_run_link_estates
			fi
			wip_hook_pair
			return 0
		fi
		# A file that names a repository the path is NOT a checkout of is a
		# refusal, never a silent overwrite: which private repository holds a
		# person's unfinished work is theirs to name, and this command has
		# exactly one write to that file and only on a machine that has none.
		if [ -n "$existing_repo" ]; then
			die "$config already names \`repository: $existing_repo\`, and its \`path: ${existing_path:-<none>}\`
    is not the root of a checkout of it. This command never overwrites that file.
    Either fix the two lines by hand, or move the file aside and re-run. Nothing
    was created."
		fi
		die "$config exists and names no \`repository:\`. This command never overwrites
    that file — a half-written pointer is still somebody's. Fix it by hand, or move
    it aside and re-run. Nothing was created."
	fi

	# ---- steps 2 to 4: DERIVE. Nothing here is asked.
	[ -n "$WIP_LOGIN" ] || WIP_LOGIN="$(wip_derive_login)"
	[ -n "$WIP_ORG" ] || WIP_ORG="$WIP_ORG_DEFAULT"
	target="$WIP_ORG/$WIP_LOGIN-wip"
	if [ -z "$WIP_TEAM" ]; then
		wip_derive_team "$WIP_ORG"
	fi
	say "workspace repository: $target"
	say "  login               $WIP_LOGIN (derived from gh api user)"
	say "  organisation        $WIP_ORG"
	if [ -n "$WIP_TEAM" ]; then
		say "  team                $WIP_TEAM (the one team you are in there)"
	else
		say "  team                none — ${WIP_TEAM_WHY:-no single team matched}, so --team is omitted"
		say "                      entirely and an administrator may add one later."
	fi

	checkout="$(wip_projects_dir)/$WIP_LOGIN-wip"

	if [ "$WIP_DRY_RUN" -eq 1 ]; then
		say ""
		say "--dry-run, so nothing below was done:"
		say "  would create  $target (private, org-owned)"
		say "  would clone   $target -> $checkout"
		say "  would seed    ${#WIP_TEMPLATE_FILES[@]} files from $SHAPE_REPOSITORY $WIP_TEMPLATE_DIR"
		say "  would push    one commit to main (which is also the ruleset probe)"
		say "  would write   $config (repository:, path:)"
		say "  would run     link-estates"
		wip_hook_pair_would
		return 0
	fi

	# ---- step 5: CREATE IT, OR PRINT THE COMMANDS — ruling 2.
	wip_have_gh || die "\`wip init\` needs \`gh\` to create or find $target. Install GitHub CLI,
    run \`gh auth login\`, then run this again. Nothing was created."
	if gh repo view "$target" >/dev/null 2>&1; then
		say ""
		say "  $target is already there; this run adopts it."
	else
		say ""
		say "  creating $target"
		local create_args
		create_args=(repo create "$target" --private)
		[ -n "$WIP_TEAM" ] && create_args+=(--team "$WIP_TEAM")
		create_args+=(--description "$WIP_LOGIN's workspace repository: lanes, handoffs, workspaces")
		if gh "${create_args[@]}" >/dev/null 2>&1; then
			say "  created $target"
			if [ -n "$WIP_TEAM" ]; then
				# `--team` alone grants the ORG DEFAULT, not read.
				# THE LINE ITSELF, not a pointer to a block that is not printed
				# on this path: `wip_admin_block` is reached only where the
				# CREATE failed, and this arm is the one where it succeeded
				# (F8 of the #24 review).
				gh api --method PUT \
					"/orgs/$WIP_ORG/teams/$WIP_TEAM/repos/$target" -f permission=pull \
					>/dev/null 2>&1 || {
					say "  (could not give team $WIP_TEAM read. An administrator can:"
					say "     gh api --method PUT /orgs/$WIP_ORG/teams/$WIP_TEAM/repos/$target -f permission=pull)"
				}
			fi
		else
			# RULING 2: PRINT THE COMMAND, and continue once the repository
			# exists. It never asks for a token and never tries a second
			# credential.
			say ""
			# NAMED FOR WHAT HAPPENED, not for a cause this has not proved. The
			# usual cause is that the account may not create a repository in
			# this organisation, and the block below is the answer to that; a
			# network that was down is the other, and the same block re-run
			# costs nothing. Claiming the first outright would send a person to
			# an administrator over a dropped connection.
			say "\`gh repo create $target\` did not succeed — most often because this account"
			say "may not create a repository in $WIP_ORG."
			say "These are the acts an administrator runs. Every value is already filled in:"
			wip_admin_block "$target" "$WIP_ORG" "$WIP_TEAM"
			# ONE QUESTION, ONE RE-CHECK, no poll loop and no timeout to choose:
			# an unbounded wait is a decision the person never asked to make.
			if [ -t 0 ]; then
				printf '%s' "Press Enter when the administrator has run these (Ctrl-C to stop): " >&2
				read -r _ || :
				if gh repo view "$target" >/dev/null 2>&1; then
					say "  $target is there now; carrying on."
				else
					die "$target is still not there, so there is nothing to clone. Run the block
    above, then: openRepoTools wip init   (it is idempotent and picks up here)."
				fi
			else
				# An unattended launch must not block on a person.
				die "$target does not exist and this run has no terminal to wait on. Run the block
    above, then: openRepoTools wip init"
			fi
		fi
	fi

	# ---- step 6: CLONE IT. Before the seed, because the seed is a commit and a
	# commit needs a checkout.
	if [ -d "$checkout/.git" ]; then
		wip_is_checkout_of "$target" "$checkout" ||
			die "$checkout is already a checkout, and not of $target. This command will not
    write into a clone of somebody else's repository, and it will not move one aside.
    Move it yourself, or pass a different projects directory with \$PROJECTS_DIR.
    $target exists; NOTHING was written into that checkout, and a re-run picks
    up from here."
		say "  $checkout is already a checkout of $target"
	elif [ -e "$checkout" ]; then
		die "$checkout already exists and is not a git checkout. $target exists;
    NOTHING was written into that path, and a re-run picks up from here once it
    is moved aside."
	else
		mkdir -p -- "$(dirname -- "$checkout")"
		say "  cloning $target -> $checkout"
		wip_quietly gh repo clone "$target" "$checkout" -- --quiet ||
			die "could not clone $target into $checkout. Nothing else was changed.
    What it said:
$WIP_LAST_ERR"
	fi

	# ---- step 6a: AN ADOPTED CHECKOUT CARRIES THE TEMPLATE AND NOTHING ELSE,
	# OR THIS COMMAND STOPS (#40, finding 1).
	#
	# Step 8 staged with `git add -A -- .` and committed whatever
	# `status --porcelain` had found, so a checkout ADOPTED at the derived path —
	# or one a half-finished earlier run left behind — had every unrelated
	# modified, deleted and untracked file it carried staged, committed and
	# `push origin HEAD:main`ed as part of the seed. The repository on the other
	# end of that push is the one a person keeps their unfinished work in, which
	# is the last place a file should arrive by accident.
	#
	# ASKED HERE, BEFORE STEP 7 WRITES A SINGLE BYTE INTO THAT CHECKOUT, for the
	# reason `plan_install_targets` is asked before the fetch: a refusal costs
	# nothing at all at this point, and the person's own files are still exactly
	# where they left them when they read it.
	#
	# THE TEMPLATE'S OWN PATHS ARE NOT `UNRELATED`, which is why the question is
	# put to git as an EXCLUSION and not as "is it clean": a first run that
	# fetched and copied the ${#WIP_TEMPLATE_FILES[@]} files and then could not
	# commit them — no `user.email`, the case F9 is written about — leaves
	# exactly those paths behind, and step 7's promise is that a re-run after a
	# half-finished run is safe. `--untracked-files=all` because git collapses an
	# untracked DIRECTORY to one line otherwise, and `handoffs/` is not a path
	# any exclusion below spells.
	#
	# AND A `git status` THIS COMMAND COULD NOT RUN IS A REFUSAL, NEVER A CLEAN
	# ANSWER (#49). The question was asked as
	# `… 2>/dev/null | sed … || :`, which discarded git's own diagnostic and then
	# turned ANY failure of the pipeline — under this file's `set -o pipefail`,
	# `git status`'s own: a corrupt index, an unreadable `.git`, a submodule
	# error — into a plain empty string. An empty change list is exactly what
	# "nothing unrelated, proceed" looks like, so a checkout this command could
	# not even ASK about was read as clean and step 7 wrote template bytes into
	# it, past the one gate that exists to refuse before a byte is written. The
	# run is `wip_quietly`'s now: its stdout is the change list, read from
	# `$WIP_LAST_OUT` and never from a capture a failure can empty, and its
	# stderr is git's own words under the refusal — the shape every other
	# failure in this function already has.
	local status_args
	status_args=(status --porcelain --untracked-files=all -- .)
	for rel in "${WIP_TEMPLATE_FILES[@]}"; do
		status_args+=(":(exclude)$rel")
	done
	wip_quietly git -C "$checkout" "${status_args[@]}" ||
		die "could not ask git what $checkout carries, so this run cannot tell whether that
    checkout holds anything this command did not write — and it will not seed a checkout
    it could not put that question to. NOTHING was written into that checkout on this run
    and nothing was staged.
    What it said:
$WIP_LAST_ERR"
	if [ -s "$WIP_LAST_OUT" ]; then
		die "$checkout carries changes this command did not write:
$(sed -e 's/^/    /' -- "$WIP_LAST_OUT")
    A seed commit is $SHAPE_REPOSITORY's $WIP_TEMPLATE_DIR and nothing else, and the
    push that follows it goes straight to $target's main — so anything else
    staged beside it is somebody's unfinished work published. NOTHING was written
    into that checkout on this run and nothing was staged.
    Commit, stash or remove those paths, then re-run:
        openRepoTools wip init"
	fi

	# ---- AND A CHECKOUT AHEAD OF origin's `main` BY ANYTHING BUT THIS
	# COMMAND'S OWN SEED IS REFUSED THE SAME WAY (#64, Brett Heap's RULING of
	# 2026-10-02, verbatim "go with option 1 on #64").
	#
	# The question above is the WORKTREE and the INDEX, and it never looked at
	# HISTORY. Step 8 pushes `HEAD:main` wherever HEAD is not what origin's
	# `main` already carries, and HEAD carries every commit beneath it — so a
	# clean adopted checkout holding commits of a person's own that never left
	# this workstation (a register edit made here and not pushed, a note
	# committed on the wrong clone) had every one of them published to the
	# `main` of their workspace repository by a command that came to place a
	# template. That is #40's finding 1 one layer down, and it is closed at the
	# same gate for the same reason: before step 7 writes a byte, where a
	# refusal costs nothing and the person's commits are exactly where they
	# left them, with the list in front of them. Pushing those commits, or
	# discarding them, is theirs to do; this command does neither.
	#
	# THE SEED IS EXEMPT, AND HAS TO BE, or step 8's own promise breaks. A
	# first run whose push the organisation's PR-only ruleset refused leaves a
	# clean worktree and EXACTLY ONE commit ahead of `main` — its seed — and
	# step 8's refusal tells the person that "a re-run after that has only to
	# push". So a commit this command made is let through, and it is known by
	# everything that makes it this command's own, never by its message alone:
	# `wip_is_own_seed` above. Every OTHER commit in `<origin main>..HEAD` is a
	# refusal naming all of them, seed or not, because what step 8 would push
	# is the whole range and the person should see the whole range.
	#
	# THE REMOTE IS ASKED HERE, ONCE, AND STEP 8 READS THE SAME ANSWER. One
	# `ls-remote`, one `remote_head`: the range this gate checked and the push
	# step 8 decides on can never be two different readings of `main`. And the
	# line read is `refs/heads/main` BY NAME, never the first line: a pattern
	# of `main` matches every ref whose name ENDS in it, so a branch such as
	# `refs/heads/backup/main` sorts before `refs/heads/main`, and the first
	# line was then that branch's tip read as `main` — which would wave through
	# every commit the branch carries, and tell step 8 that a `main` it never
	# pushed to already had them.
	#
	# A REMOTE THAT COULD NOT BE ASKED IS STEP 8'S OWN ARM, KEPT, as the ruling
	# keeps it: this check is skipped, said in one line, and the run goes on to
	# the push, which is the probe. Not a refusal, because there is nothing
	# true to refuse with — no `main` to name commits against — and because the
	# remote that will not answer `ls-remote` is the remote step 8 pushes to,
	# by the same URL, transport and credential, so the push almost always
	# meets the same wall and step 8 stops the run before the pointer file is
	# written. And NOT an empty `main` either: `remote_asked` is kept apart
	# from `remote_head` because an unanswered question read as "no `main`
	# yet" would put EVERY local commit ahead — refusing the very seed re-run
	# the exemption above exists for. The window this leaves is a remote that
	# refuses the question and takes the push a moment later, and that is the
	# trade the ruling chose with this arm.
	#
	# A RANGE THAT CANNOT BE LISTED IS A REFUSAL, NEVER AN EMPTY ONE (#49's
	# lesson, asked of history). Its usual cause is a `main` this checkout has
	# never fetched — another workstation pushed to it since — and an empty
	# list read from that failure is exactly what "nothing ahead, proceed"
	# looks like.
	local remote_head="" remote_asked=0 remote_refs history_range where ahead ahead_log commit
	local seeds_ahead=0
	if remote_refs="$(git -C "$checkout" ls-remote origin main 2>/dev/null)"; then
		remote_asked=1
		remote_head="$(printf '%s\n' "$remote_refs" |
			awk '$2 == "refs/heads/main" && !seen { print $1; seen = 1 }')"
	fi
	if [ "$remote_asked" -eq 0 ]; then
		say "  could not ask origin where $target's main is, so the commits this checkout"
		say "  carries were not checked against it here; the push below is the probe."
	elif git -C "$checkout" rev-parse -q --verify 'HEAD^{commit}' >/dev/null 2>&1; then
		if [ -n "$remote_head" ]; then
			history_range="$remote_head..HEAD"
			where="$target's main ($(printf '%.7s' "$remote_head"))"
		else
			history_range="HEAD"
			where="$target's main (it has none yet)"
		fi
		wip_quietly git -C "$checkout" rev-list "$history_range" ||
			die "could not list the commits $checkout carries that are not on $where,
    so this run cannot tell whether any of them is somebody's unreviewed work — and a
    history this command could not read is never one it may push. Most often that \`main\`
    is a commit this checkout has never fetched, because another workstation pushed to it
    since. NOTHING was written into that checkout on this run and nothing was staged.
    Fetch it, merge or rebase this checkout's own commits onto origin/main, then re-run:
        git -C $checkout fetch origin
        openRepoTools wip init
    What it said:
$WIP_LAST_ERR"
		ahead="$(cat -- "$WIP_LAST_OUT")"
		for commit in $ahead; do
			if wip_is_own_seed "$checkout" "$commit"; then
				seeds_ahead=$((seeds_ahead + 1))
				continue
			fi
			ahead_log="$(git -C "$checkout" log --oneline --no-decorate --no-color \
				--no-show-signature "$history_range" 2>/dev/null)" || ahead_log="$ahead"
			die "$checkout carries commits that are not on $where,
    and not every one of them is this command's own seed:
$(printf '%s\n' "$ahead_log" | sed -e 's/^/    /')
    This command ends by pushing HEAD to $target's main, so it would publish every
    commit above to your workspace repository unreviewed — and what it came to push
    is $SHAPE_REPOSITORY's $WIP_TEMPLATE_DIR and nothing else.
    NOTHING was written into that checkout on this run and nothing was staged.
    Push those commits yourself, or discard them, then re-run:
        openRepoTools wip init"
		done
		case "$seeds_ahead" in
		0) ;;
		1) say "  one seed an earlier run committed is not on $target main yet; the push below carries it" ;;
		*) say "  $seeds_ahead seeds earlier runs committed are not on $target main yet; the push below carries them" ;;
		esac
	fi

	# ---- step 7: SEED IT FROM THE TEMPLATE, additively. A repository that
	# already has a commit on `main` is NOT re-seeded: files the template would
	# place and that are already there are left exactly as they are, and the
	# missing ones are added — which is what makes a re-run after a
	# half-finished first run safe.
	ref="$(wip_shape_ref)" || exit "$?"
	workdir
	for rel in "${WIP_TEMPLATE_FILES[@]}"; do
		dest="$WORKDIR/tpl-$(printf '%s' "$rel" | tr '/' '_')"
		# ---- THE PATH'S OWN ANCESTORS ARE ASKED ABOUT FIRST, AND A SYMLINK AT
		# ONE OF THEM IS REFUSED BEFORE ANY BRANCH BELOW OPENS, CREATES OR
		# WRITES THROUGH IT (#62).
		#
		# `path_kind` below asks what is at `$rel` ITSELF; nothing asked what is
		# at `handoffs`, `lanes`, `lanes/log` or `workspaces` — every directory
		# component the ${#WIP_TEMPLATE_FILES[@]} template paths have. An
		# adopted checkout can carry a TRACKED symlink at one of those: a person
		# whose `handoffs` is a link into another tree of their own, committed,
		# which step 6a sees as a clean worktree because it IS what HEAD says.
		# Every test the branches below make then answers as though nothing were
		# at `$rel` at all — `[ -e ]` and `[ -L ]` are false when the link's
		# target holds no such file, and git has no notion of a path INSIDE a
		# tracked symlink, so `:$rel`, `HEAD:$rel` and `ls-files -u` are false
		# too — and the MISSING branch is reached: its `mkdir -p` succeeds
		# without creating anything, because the link resolves to a directory
		# that is already there, and its `cp` then writes this run's template
		# bytes through the link, OUTSIDE this checkout if that is where the
		# link leads. Only the `git add` two steps later notices, with
		# `fatal: … is beyond a symbolic link` — after the bytes have landed
		# somewhere this command was never pointed at.
		#
		# AND AN ANCESTOR THAT IS NOT A DIRECTORY AT ALL IS REFUSED IN THE SAME
		# WALK, because the same MISSING branch meets it one line earlier: a
		# regular file committed at `handoffs` fails that `mkdir -p`, and a
		# failing `mkdir` under this file's `set -e` exits 1 — the code this
		# toolset spends on "findings were printed" — from a run that meant to
		# refuse with 2 and never printed a word about why.
		#
		# `unusable_dir_kind` IS NOT THIS QUESTION, which is why this walk is
		# written rather than that one called: it climbs UP to the nearest
		# ancestor that EXISTS and asks whether a file can be created there, so
		# a symlink to a directory that is really there answers it `-d`, `-w`
		# and `-x` — usable — and it never looks at the components below. This
		# walk goes DOWN the exact relative path and asks what each component
		# IS.
		ancestor=""
		rest=""
		case "$rel" in */*) rest="${rel%/*}" ;; esac
		while [ -n "$rest" ]; do
			case "$rest" in
			*/*) comp="${rest%%/*}"; rest="${rest#*/}" ;;
			*) comp="$rest"; rest="" ;;
			esac
			ancestor="${ancestor:+$ancestor/}$comp"
			if [ -L "$checkout/$ancestor" ]; then
				die "$checkout/$ancestor is $(path_kind "$checkout/$ancestor"), and the template's
    $rel sits under it — so a \`mkdir -p\` there creates nothing and the \`cp\` that follows
    writes this run's template bytes through that link, outside this checkout if that is
    where it leads. A seed commit is the template's own bytes at a REGULAR FILE inside this
    checkout, and nothing else there could be. Nothing beyond this run's own template bytes was
    written into that checkout, and nothing was staged.
    Replace that component with a real directory, then re-run:
        openRepoTools wip init"
			fi
			if [ -e "$checkout/$ancestor" ] && [ ! -d "$checkout/$ancestor" ]; then
				die "$checkout/$ancestor is not a directory, and the template's $rel sits under it —
    so this run can neither create that directory nor write the file inside it. Nothing beyond
    this run's own template bytes was written into that checkout, and nothing was staged.
    Replace that component with a real directory, then re-run:
        openRepoTools wip init"
			fi
			# AND A DIRECTORY THAT IS ANOTHER REPOSITORY'S IS NOT THIS ONE'S TO
			# WRITE IN (#101 round 2, on this walk's own diff). A tracked
			# SUBMODULE at `handoffs` is a gitlink in this repository and a
			# whole repository on disk: `-d` is true, `-L` is false, step 6a
			# sees a CLEAN worktree because a clean submodule reports nothing,
			# and every presence test below answers false because a gitlink
			# carries no `handoffs/README.md` for `:$rel` or `HEAD:$rel` to
			# find. So the MISSING branch wrote this run's template bytes
			# INSIDE somebody's nested repository, and the `git add` two steps
			# later answered `fatal: Pathspec … is in submodule …` — after the
			# bytes had landed there, in a repository this command has no
			# business in and does not clean up.
			#
			# ASKED OF THE INDEX, because that is what makes it a submodule and
			# it is true whether or not the thing is checked out: a `deinit`ed
			# submodule is an EMPTY directory with no `.git` in it at all, and
			# the `git add` fails exactly the same way. `$4` is the path column
			# of `ls-files -s` and every component here comes from
			# `$WIP_TEMPLATE_FILES`, which is nine fixed ASCII paths, so it is
			# never a quoted or split field. The `.git` test beside it catches
			# the other shape — a nested repository nobody tracked, which an
			# ignore rule can hide from step 6a as well.
			ancestor_mode="$(git -C "$checkout" ls-files -s -- "$ancestor" 2>/dev/null |
				awk -v p="$ancestor" '$4 == p { print $1; exit }' || :)"
			if [ "$ancestor_mode" = "160000" ] || [ -e "$checkout/$ancestor/.git" ]; then
				die "$checkout/$ancestor is another git repository's working tree — a submodule of this
    one, or a clone somebody left there — and the template's $rel sits under it. A seed commit
    is this repository's own files: bytes written in there belong to that repository, this one
    cannot stage them (\`git add\` answers \`Pathspec … is in submodule …\`), and this command
    does not write into a repository it was never pointed at. Nothing beyond this run's own
    template bytes was written into that checkout, and nothing was staged.
    Move that repository aside, or seed a workspace that does not carry one at $ancestor, then
    re-run:
        openRepoTools wip init"
			fi
		done
		if [ -e "$checkout/$rel" ] || [ -L "$checkout/$rel" ] ||
			git -C "$checkout" cat-file -e ":$rel" 2>/dev/null ||
			git -C "$checkout" cat-file -e "HEAD:$rel" 2>/dev/null ||
			[ -n "$(git -C "$checkout" ls-files -u -- "$rel" 2>/dev/null)" ]
		then
			# ALREADY THERE, OR AS GOOD AS — AND THE SEED CAN STILL OWE IT. A template path the
			# WORKTREE carries and HEAD does not is what an earlier run of this
			# command leaves behind when it copied the template and could not
			# commit it: no `user.email`, the case the refusal below is written
			# about. Step 8 has to carry those, or the re-run this step promises
			# commits nothing, pushes nothing, and writes the pointer file over
			# an EMPTY main — the one state no later run can dig out of.
			#
			# A TEMPLATE PATH HEAD ALREADY CARRIES IS THE PERSON'S, whatever it
			# says now: this command has never modified a committed file, so a
			# change at one is not its leftover and never enters its commit
			# (#44 round 1, `openRepoTools:1525`).
			#
			# `! cat-file -e HEAD:…` ALONE IS NOT PROOF (#44 round 2,
			# `openRepoTools:1621`): a template path that is UNTRACKED and
			# carries a PERSON'S OWN CONTENT is absent from HEAD exactly as
			# this command's own leftover is, so it answered this question the
			# same way and went into the seed regardless of whose bytes they
			# were. The bytes settle what the question alone could not: this
			# command's own leftover is what THIS RUN would itself write
			# there, so the template is fetched and substituted the same way
			# a MISSING file below is, and the two are compared before either
			# is trusted as this command's own.
			#
			# THE WORKTREE TEST ALONE MISSES FOUR MORE SHAPES (#44 round 3,
			# `openRepoTools:1613`/`:1615`/`:1699`): a DANGLING symlink answers
			# `[ -e ]` false exactly as nothing-there-at-all does, so without
			# the `-L` above the MISSING branch below would meet it at `cp` and
			# follow it — writing this run's bytes wherever it points, inside
			# this checkout or far outside it; a person's own `git add` of a
			# template path, followed by removing the worktree file, leaves
			# the INDEX carrying something the worktree no longer does, which
			# the same MISSING branch would silently overwrite at `cp` and
			# step 8 would then stage over; a `git rm` of a path HEAD already
			# carries — staged, worktree gone, index gone too — passes none of
			# the first three tests either, so without asking `HEAD` directly
			# here the same MISSING branch would put the file straight back,
			# undoing a deletion this command has never modified a committed
			# file to make; and an UNRESOLVED MERGE CONFLICT can leave a path
			# at stages 1-3 with no stage 0, no worktree file and no `HEAD`
			# entry at all — invisible to all four other tests — so it is
			# asked for directly too. All four are asked for here instead,
			# beside the worktree.
			if ! git -C "$checkout" cat-file -e "HEAD:$rel" 2>/dev/null; then
				# THE SAME QUESTION `unplaceable_kind` ASKS OF AN INSTALL TARGET,
				# asked of an owed candidate: a SYMLINK is never this command's
				# own, dangling or not, because `cmp` reads through a live one to
				# whatever it names — so one whose target happens to hold the
				# template's own bytes would pass the comparison below, and a
				# `git add` of it stages the LINK ITSELF, not the bytes at its far
				# end — and a FIFO, a socket or a device node is worse: `cmp`
				# opens it and can block waiting for a writer, or read whatever a
				# device answers, rather than refusing. `path_kind` already
				# answers all three the same way `--install`'s own planners rely
				# on; this command has placed none of them at a template path, so
				# any of them here is refused before either branch can act on it,
				# open it, or write through it.
				if kind="$(path_kind "$checkout/$rel")"; then
					die "$checkout/$rel is $kind, and a seed commit is the template's own bytes
    at a REGULAR FILE and nothing else there could be. Nothing beyond this run's own
    template bytes was written into that checkout, and nothing was staged.
    Remove or replace that path, then re-run:
        openRepoTools wip init"
				fi
				# AN UNRESOLVED MERGE CONFLICT'S STAGES ARE INVISIBLE TO A
				# STAGE-0 QUESTION: `:$rel` reads stage 0 alone, so a path with
				# only stages 1-3 (added differently on both sides of a merge,
				# say) answers `cat-file -e ":$rel"` false exactly as an empty
				# index does, and step 6a's own exclusion of template paths
				# means no earlier gate has seen it either. This command has
				# never resolved anybody's merge, so a conflict here is refused
				# rather than silently settled by whichever branch the
				# worktree's own state happens to satisfy.
				if [ -n "$(git -C "$checkout" ls-files -u -- "$rel" 2>/dev/null)" ]; then
					die "$checkout has an unresolved merge conflict at $rel, and HEAD does not
    carry $rel either, so this command cannot tell which side, if either, is its own
    template substitution. This command has never resolved anybody's merge. Nothing beyond
    this run's own template bytes was written into that checkout, and nothing was staged.
    Resolve that conflict, then re-run:
        openRepoTools wip init"
				fi
				# AND THE INDEX CAN OWE SOMETHING THE WORKTREE NO LONGER HAS AT ALL:
				# this command has never staged a path and then removed its own file
				# from under it, so an index entry with nothing on disk behind it is
				# somebody else's, whatever it holds.
				if [ ! -e "$checkout/$rel" ]; then
					die "$checkout/$rel does not exist, HEAD does not carry $rel, and something is
    staged there anyway — so this is somebody's own staged addition, removed from the
    worktree after being added, and not this command's to overwrite or commit over. Nothing
    beyond this run's own template bytes was written into that checkout, and nothing
    already staged was changed.
    Commit, stash or unstage that path, then re-run:
        openRepoTools wip init"
				fi
				# AND THE WORKTREE CARRIES A MODE OF ITS OWN, which is the half
				# the index question below cannot see (#101 round 1, on the #63
				# fix's own diff). Step 8 stages from the WORKTREE, so an owed
				# candidate whose file is EXECUTABLE goes into the seed commit
				# as `100755` — and it reaches here two ways the stage-0
				# question never asks about: an UNTRACKED template path whose
				# bytes match and whose file somebody chmod'ed, and a path
				# staged `100644` whose worktree file got the bit afterwards.
				# This command copies the template's own bytes from a 0644 file
				# and has never chmod'ed anything it wrote, so an executable
				# file at a template path is not its leftover whatever its
				# bytes say — it is somebody's own, and this branch's answer to
				# that is a refusal.
				#
				# ASKED ONLY WHERE GIT WOULD RECORD IT. On a filesystem that
				# reports every file executable, `git clone` sets
				# `core.fileMode=false` and git stages `100644` regardless; a
				# refusal there would refuse every re-run on that mount over a
				# bit that means nothing, so the config that decides it is
				# asked. Unset is TRUE — the read answers 1 and nothing, which
				# is not `false`, so the refusal stands: an answer this command
				# could not get is never permission to publish a mode. `--bool`
				# because git's own spelling of false is `false`, `no`, `off` or
				# `0` and this compares one string.
				if [ -x "$checkout/$rel" ] &&
					[ "$(git -C "$checkout" config --bool --get core.fileMode 2>/dev/null || :)" != "false" ]
				then
					die "$checkout/$rel is executable, and every file in $SHAPE_REPOSITORY's
    $WIP_TEMPLATE_DIR is a plain 100644 — this command copies the template's own bytes from a
    0644 file and has never chmod'ed anything it wrote, so an executable file at a template
    path is somebody's own, whatever its bytes say. Staging it would record 100755 in the seed
    commit and push that to $target's main. Nothing beyond this run's own template bytes was
    written into that checkout, and nothing was staged.
    \`chmod 644\` that path, or commit, stash or remove it, then re-run:
        openRepoTools wip init"
				fi
				# AND WHAT IS STAGED IS A MODE AS WELL AS A BLOB (#63). The
				# index comparison this branch makes below is
				# `cat-file -p ":$rel"`, which prints a blob's BYTES and never
				# its mode — so a template path a person staged with
				# `git add --chmod=+x`, byte-identical to the template and
				# executable, passed it as "the index carries the template", and
				# step 8's `git add` then restaged that path from the worktree
				# at the worktree's own mode: their `100755` silently replaced
				# by `100644` and committed. Every file in $SHAPE_REPOSITORY's
				# $WIP_TEMPLATE_DIR is a plain `100644`, and this command has
				# never chmod'ed anything it wrote, so a stage-0 entry at any
				# other mode is NOT an earlier run's leftover whatever its bytes
				# say — it is somebody's own, which in this branch is the same
				# refusal the bytes themselves get below and not a thing to
				# stage over. Asked BEFORE the fetch, with the branch's other
				# local questions: the answer does not depend on the template's
				# bytes, and a machine that cannot reach the network deserves
				# the refusal that names the real reason rather than one about
				# a fetch.
				# AND THE INDEX IS ASKED OF THE INDEX (#101 round 3, on this
				# guard's own diff). `cat-file -e ":$rel"` answers false in TWO
				# states — no stage-0 entry at all, and an entry whose BLOB the
				# object store has lost — because it resolves the name through
				# the index and then looks the OBJECT up. Gating on it spent the
				# second state as though it were the first: the mode guard was
				# skipped, the comparison below took its "no entry" arm, a
				# worktree that reads back as the template was accepted, and
				# step 8's `add` replaced what the person had staged with this
				# run's copy — measured, the staged sha changes and the `add`
				# reports nothing. `ls-files -s` reads the index and never the
				# object store, so presence is ITS answer now; a read that FAILS
				# is a refusal rather than an empty index, which is #49's own
				# lesson one question over; and an entry whose blob cannot be
				# read gets its own refusal, because an entry this command
				# cannot READ is not one it may stage over. `$3` is the stage
				# column and `$4` the path — every path here comes from
				# `$WIP_TEMPLATE_FILES`, nine fixed ASCII paths.
				if staged_entry="$(git -C "$checkout" ls-files -s -- "$rel" 2>/dev/null)"; then
					staged_mode="$(printf '%s\n' "$staged_entry" |
						awk -v p="$rel" '$3 == "0" && $4 == p { print $1; exit }')"
				else
					die "could not read what $checkout has staged at $rel, so this run cannot tell whether
    anything is staged there at all — and an index this command could not read is never an
    empty one. Nothing beyond this run's own template bytes was written into that checkout,
    and nothing already staged was changed.
    Check that checkout (\`git status\`), then re-run:
        openRepoTools wip init"
				fi
				if [ -n "$staged_mode" ]; then
					git -C "$checkout" cat-file -e ":$rel" 2>/dev/null ||
						die "$checkout has $rel staged and this command cannot read what is staged there: the
    index carries an entry and git's object store has no blob behind it. An entry this command
    cannot READ is not one it may stage over — step 8's \`git add\` would replace it with this
    run's copy, silently, and commit that. Nothing beyond this run's own template bytes was
    written into that checkout, and nothing already staged was changed.
    Repair that object (\`git fsck\`), or commit, stash or unstage that path, then re-run:
        openRepoTools wip init"
					[ "$staged_mode" = "100644" ] ||
						die "$checkout has $rel staged with mode $staged_mode, and every file in
    $SHAPE_REPOSITORY's $WIP_TEMPLATE_DIR is a plain 100644 — this command has never chmod'ed
    anything it wrote, so a mode of its own at that path is somebody's own \`git add\`, not an
    earlier run's leftover, whatever the staged bytes say. Staging this run's copy over it would
    replace that mode with the worktree's and commit the change. Nothing beyond this run's own
    template bytes was written into that checkout, and nothing already staged was changed.
    Commit, stash or unstage that path, then re-run:
        openRepoTools wip init"
				fi
				wip_fetch_template_file "$rel" "$dest" "$ref" ||
					die "could not fetch $WIP_TEMPLATE_DIR/$rel from $SHAPE_REPOSITORY at $ref, so $rel
    itself was NOT seeded. Nothing beyond an earlier template path's own bytes in
    this same run, if it reached one before this fetch, was written into that
    checkout, and nothing was staged; a re-run picks up from here. Both ways were tried:
    gh api repos/$SHAPE_REPOSITORY/contents/$WIP_TEMPLATE_DIR/$rel?ref=$ref
    curl -fsSL https://raw.githubusercontent.com/$SHAPE_REPOSITORY/$ref/$WIP_TEMPLATE_DIR/$rel"
				wip_substitute "$dest"
				# THE WORKTREE IS NOT THE ONLY PLACE THOSE BYTES CAN LIE EITHER (#44
				# round 3, `openRepoTools:1647`): HEAD lacking `$rel` says nothing
				# about the INDEX, so a person's own edit `git add`ed at this path
				# — staged before this run and never committed — sits exactly
				# where step 8's `add` would overwrite it, and a worktree that
				# happens to read back as the template is not permission to stage
				# over what is already staged there. The index is asked the same
				# question the worktree already answers: no entry is this
				# command's own leftover exactly as no file is, and an entry
				# whose bytes are not the template fails it exactly as the
				# worktree's would.
				if cmp -s -- "$dest" "$checkout/$rel" &&
					{ [ -z "$staged_mode" ] ||
					git -C "$checkout" cat-file -p ":$rel" 2>/dev/null |
						cmp -s -- "$dest" -; }
				then
					written+=("$rel")
					owed=$((owed + 1))
				else
					die "$checkout/$rel exists, HEAD does not carry it, and its worktree bytes, its staged
    bytes, or both are not what $SHAPE_REPOSITORY's $WIP_TEMPLATE_DIR/$rel substitutes to at
    $ref — so this is not an earlier run's own leftover; it is somebody's own file, staged or
    not, at a path the template also names. A seed commit is the template and nothing else, and
    this command will not publish it or stage over what is already staged there. Nothing beyond
    this run's own template bytes was written into that checkout, and nothing was staged.
    Commit, stash or remove that path, then re-run:
        openRepoTools wip init"
				fi
			else
				# ---- A PATH HEAD CARRIES THAT IS NOT ON DISK BECAUSE THE INDEX
				# SAYS SO IS NAMED, NOT SEEDED AND NOT SILENTLY PASSED (#69).
				#
				# `skip-worktree` — which is also what `git sparse-checkout`
				# sets — tells git not to compare the worktree at that path at
				# all: HEAD carries it, the stage-0 entry carries it, `git
				# status` is clean, and the file is nonetheless physically
				# absent. That answers this branch's question exactly as a
				# person's own deliberate `git rm` does, and the principle above
				# still stands — a template path HEAD already carries is the
				# person's, and this command has never modified a committed file
				# — so it is still not seeded, not written into and not
				# committed for. What it is not is INVISIBLE: the run would
				# otherwise print "the workspace already carries every template
				# file" and write the pointer file, while a file this command
				# seeds and the lane tooling reads is not on disk. So it is
				# counted and said, and the person decides.
				#
				# `ls-files -v`'s tag is `S` for skip-worktree and the LOWERCASE
				# `s` where that entry is also assume-unchanged — `-v`'s
				# lowercase means assume-unchanged, whatever the letter — and
				# BOTH are this case. `h`, assume-unchanged ALONE, is not: that
				# bit is a person telling git to stop stat-ing a path they own
				# and keep on disk, not a checkout that never materialized one,
				# and naming it "skip-worktree" would be false. An ordinary
				# tracked path whose file a person simply deleted is `H`, which
				# `git status` reports for itself.
				if [ ! -e "$checkout/$rel" ] && [ ! -L "$checkout/$rel" ]; then
					index_tag="$(git -C "$checkout" ls-files -v -- "$rel" 2>/dev/null |
						awk 'NR == 1 { print $1 }' || :)"
					case "$index_tag" in
					S | s)
						sparse=$((sparse + 1))
						sparse_paths+=("$rel")
						;;
					esac
				fi
			fi
			continue
		fi
		wip_fetch_template_file "$rel" "$dest" "$ref" ||
			die "could not fetch $WIP_TEMPLATE_DIR/$rel from $SHAPE_REPOSITORY at $ref, so $rel
    itself was NOT seeded. Nothing beyond an earlier template path's own bytes in
    this same run, if it reached one before this fetch, was written into that
    checkout, and nothing was staged; a re-run picks up from here. Both ways were tried:
    gh api repos/$SHAPE_REPOSITORY/contents/$WIP_TEMPLATE_DIR/$rel?ref=$ref
    curl -fsSL https://raw.githubusercontent.com/$SHAPE_REPOSITORY/$ref/$WIP_TEMPLATE_DIR/$rel"
		wip_substitute "$dest"
		mkdir -p -- "$checkout/$(dirname -- "$rel")"
		cp -- "$dest" "$checkout/$rel"
		written+=("$rel")
		seeded=$((seeded + 1))
	done
	if [ "$seeded" -gt 0 ]; then
		say "  seeded $seeded of ${#WIP_TEMPLATE_FILES[@]} template files from $SHAPE_REPOSITORY @ $ref"
	else
		say "  the workspace already carries every template file; none was replaced"
	fi
	# THE ONE LINE #69 IS FOR, and the whole of what this command does about it.
	if [ "$sparse" -gt 0 ]; then
		for rel in "${sparse_paths[@]}"; do
			say "  $rel: HEAD carries it, the index marks it skip-worktree, and it is not on disk"
		done
		say "      (a sparse checkout leaves a tracked path there and off the disk; nothing"
		say "       was written or committed at it. \`git update-index --no-skip-worktree --\`"
		say "       that path, or \`git sparse-checkout\`, is what brings it back.)"
	fi

	# ---- step 8: COMMIT AND PUSH — AND THAT PUSH IS THE RULESET PROBE.
	# A newly created repository is not excluded from the organisation's PR-only
	# ruleset until an administrator excludes it, and until it is, every register
	# write will be refused by the gate. So this push is not only the seed
	# landing; it is the verification (Addendum 1(2)).
	#
	# EACH PATH BY NAME, NEVER `git add -A -- .` (#40, finding 1). Step 6a means
	# there is nothing unrelated left in the worktree for a `-A` to sweep up —
	# and this means that a path step 6a could not see as unrelated, because the
	# template names it and a person had edited it, is still not published by a
	# command that came to place a template. Two answers to one finding, and the
	# cheaper one is the one that runs first.
	if [ "$((seeded + owed))" -gt 0 ]; then
		wip_quietly git -C "$checkout" add -- "${written[@]}" ||
			die "could not stage the seed in $checkout.
    What it said:
$WIP_LAST_ERR"
	fi
	# AND THE COMMIT IS THOSE PATHS AND NO OTHERS (#44 round 1,
	# `openRepoTools:1525`). `git commit` with no pathspec commits THE INDEX, so
	# a person's own `git add` of a file step 6a is deliberately blind to — a
	# TEMPLATE path they had edited — rode into the seed commit and out to
	# `main` on the push below, which is the finding this round was opened for
	# one layer down. The pathspec commits the worktree at those paths and
	# leaves every other staged change exactly where the person left it: staged,
	# uncommitted, unpushed.
	#
	# THE `diff --cached` ASKS ABOUT THE SAME PATHS, because after an `add` of
	# paths that are already in HEAD there can be nothing to commit at all — a
	# checkout that had a template file DELETED gets it back from the template
	# above, and where those bytes match HEAD `git commit` refuses with one.
	if [ "$((seeded + owed))" -gt 0 ] &&
		! git -C "$checkout" diff --cached --quiet -- "${written[@]}" 2>/dev/null; then
		wip_quietly git -C "$checkout" commit -q \
			-m "Seed this workspace repository from $SHAPE_REPOSITORY $WIP_TEMPLATE_DIR

Written by \`openRepoTools wip init\` (lane-collision-protocol Amendment 9(c)).
It holds data only: the register, the object logs, the handoffs and the
workspace manifests. The lane tooling is installed from opensoft/openRepoTools." \
			-- "${written[@]}" ||
			die "could not commit the seed in $checkout. A machine with no
    \`user.email\` configured is the usual reason, and git says so itself:
$WIP_LAST_ERR"
		say "  committed the seed in $checkout"
	fi

	# THE PUSH IS GATED ON THE REMOTE, NOT ON THE WORKTREE, and that is the
	# whole of what makes the re-run this step promises actually work. A first
	# run whose push the ruleset rejected leaves a CLEAN worktree and an
	# unpushed commit — so a `status --porcelain` gate would report "already
	# carries the seed", write the pointer file and exit 0 over an empty
	# `main`, and step 1's file test would then return on every later run.
	# That is the failure step 8 exists to prevent, met one step further on.
	#
	# AND THE REMOTE IS THE ONE STEP 6a ASKED (#64). `remote_head` is that one
	# `ls-remote`'s answer, so the commits 6a checked against `main` and the
	# push decided here are a single reading of it, never two. Where 6a could
	# not ask, `remote_head` is empty and the push below runs: the push IS the
	# probe, and a question we could not put to the remote is not an answer
	# from it.
	local local_head
	local_head="$(git -C "$checkout" rev-parse HEAD 2>/dev/null || :)"
	if [ -z "$local_head" ]; then
		say "  $checkout has no commit to push"
	elif [ -n "$remote_head" ] && [ "$local_head" = "$remote_head" ]; then
		say "  $target main already carries this commit ($(printf '%.7s' "$local_head"))"
	elif ! git -C "$checkout" push -q origin HEAD:main >/dev/null 2>&1; then
		die "the seed commit could not be pushed to $target's main.
    THAT IS THE RULESET, and it is the thing this push exists to find out: the
    organisation's PR-only ruleset refuses a direct commit, and Rule 9's one commit
    per write cannot land through a PR gate — so the register would be unwritable.
    An administrator excludes $target from it:
        gh api /orgs/$WIP_ORG/rulesets
    then adds $target to the PR-only ruleset's exempt repositories.
    The clone and the seed commit are left exactly where they are, so a re-run after
    that has only to push:
        openRepoTools wip init"
	else
		say "  pushed the seed to $target main (which is also the ruleset probe)"
	fi

	# ---- step 9: WRITE THE POINTER FILE. The SECOND thing in this toolset that
	# writes a file outside a repository, after `resume --workspace`, for the
	# same reason and under the same condition: only on a machine that has none,
	# and only because the person asked for the repository by running this. It
	# never overwrites one — step 1 already returned where one existed.
	local written="$checkout"
	case "$checkout" in "$HOME"/*) written="~${checkout#"$HOME"}" ;; esac
	mkdir -p -- "$(dirname -- "$config")" ||
		die "could not create $(dirname -- "$config") for the pointer file.
    The repository is created, cloned, seeded and pushed; only this file is
    missing, and a re-run writes it."
	[ ! -e "$config" ] ||
		die "$config appeared while this ran; it was not overwritten."
	# GUARDED, because this command's exit codes are 0 and 2 and nothing else:
	# an unguarded redirect fails under `set -e` with the shell's own status 1,
	# which is the code this toolset reserves for "findings were printed". A
	# refusal that reads as a report is the defect openRepoTools' own convention
	# exists to prevent (`status:3076-3078`).
	printf 'repository: %s\npath: %s\n' "$target" "$written" >"$config" ||
		die "could not write $config. The repository is created, cloned, seeded and
    pushed; only this file is missing, and a re-run writes it."
	say "  wrote $config"
	say "        repository: $target"
	say "        path: $written"

	# ---- step 10: LINK-ESTATES, which places the symlinks and reports the ones
	# it skipped — and then THE HOOK PAIR (#237), which `--install` deferred on a
	# workstation that had no link yet for its two entries to call.
	wip_run_link_estates
	wip_hook_pair

	# ---- step 11: THE NEXT STEP, which is one line. `<profile>` is the one
	# value a person supplies and the one this command cannot derive: it is not
	# a function of the workspace repository.
	say ""
	say "Done. The next step is one line:"
	if command -v lclaude >/dev/null 2>&1; then
		say "    lclaude --lane <repo>-<n> <profile>"
	else
		say "    pclaude --lane <repo>-<n> <profile>"
	fi
	say ""
	say "  <profile>   \`pclaude list\` names them"
	say "  <repo>-<n>  the lane naming rule: the repository the lane is homed on,"
	say "              and its position — openRepoProject-1, workBenches-2"
	return 0
}

# `wip <verb>` — one noun, one verb, and a refusal that names the one there is.
wip_dispatch() {
	local verb="${1-}"
	shift || :
	case "$verb" in
	init) ;;
	'') die "\`wip\` takes one verb and it is \`init\`:
        openRepoTools wip init [--org <org>] [--login <name>] [--team <team>] [--dry-run]" ;;
	*) die "\`wip $verb\` is not a thing this command does. The one verb is \`init\`:
        openRepoTools wip init" ;;
	esac
	while [ $# -gt 0 ]; do
		case "$1" in
		--org) shift; [ $# -gt 0 ] || die "--org needs a value"; WIP_ORG="$1"; shift ;;
		--login) shift; [ $# -gt 0 ] || die "--login needs a value"; WIP_LOGIN="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"; shift ;;
		--team) shift; [ $# -gt 0 ] || die "--team needs a value"; WIP_TEAM="$1"; shift ;;
		--dry-run) WIP_DRY_RUN=1; shift ;;
		*) die "\`wip init\` takes --org, --login, --team and --dry-run, and nothing else.
    It got '$1'." ;;
		esac
	done
	if [ -n "$WIP_LOGIN" ] && ! wip_login_is_compliant "$WIP_LOGIN"; then
		die "--login '$WIP_LOGIN' cannot form a compliant \`<user>-wip\` name: openRepoShape's
    naming contract requires \`^[a-z0-9]+(-[a-z0-9]+)*-wip\$\`. Nothing was created."
	fi
	wip_init
}

# --- package mode -----------------------------------------------------------
#
# A PACKAGE READS ITS OWN STAMP BEFORE IT ACTS ON ANY ARGUMENT (#228, FR-012,
# design.md Decision 4). So a broken one refuses every invocation, `--help`,
# `--version`, `--install`, `wip` and none at all alike, and a build that
# cannot name its release places nothing. Package mode is decided from this
# file's own path (`package_payload_dir`), and the stamp is the file beside its
# payload and nothing else: no environment variable names it, mends it or
# stands in for it (FR-011, research R4). In a checkout or the one-line install
# this sets nothing and reads nothing.
package_init() {
	local here=""
	here="$(package_payload_dir)" || return 0
	PACKAGE_DIR="$here"
	stamp_read "${here%/*}/_release.py" ||
		die "this package's stamp (openrepotools/_release.py) $STAMP_WHY, so this build is broken and nothing ran. A package's stamp is a release's whole stamp or the unstamped one. Reinstall it: pipx reinstall openrepotools"
}

# `--version` IN PACKAGE MODE names the release this command IS, from the stamp
# `package_init` read: never the release of the files `--install` placed, and
# never the source a fetch would read (contracts/cli-output.md, FR-014).
package_version() {
	if [ "$STAMP_STATE" = stamped ]; then
		say "openRepoTools $STAMP_VERSION ($STAMP_REPOSITORY @ $STAMP_COMMIT)"
		say "  version   $STAMP_VERSION"
		say "  tag       $STAMP_TAG"
		say "  commit    $STAMP_COMMIT"
		say "  digest    $STAMP_DIGEST (sorted-ls-tree-r-v1)"
	else
		say "openRepoTools 0.0.0 (an unstamped package)"
		say "  version   0.0.0, unstamped: a package built from a checkout, not a release"
		say "  tag       none"
		say "  commit    none"
		say "  digest    none"
	fi
}

# ONCE, AFTER EVERY FUNCTION IS DEFINED AND BEFORE THE FIRST ARGUMENT IS READ.
package_init

# --- arguments ------------------------------------------------------------
#
# THREE FLAGS AND ONE VERB PAIR, and no others. The refusal names the commands
# rather than repeating the usage: somebody typing `openRepoTools park` wants
# `park`, and the shortest true answer is where it is.
#
# `wip` CONSUMES THE REST OF THE LINE, because `wip init --org x` is one
# command with its own options and not four arguments to this one.
while [ $# -gt 0 ]; do
	case "$1" in
	-h | --help) usage; exit 0 ;;
	--version)
		# A PACKAGE NAMES ITS RELEASE. A checkout and the one-line install name
		# the repository and the ref they fetch from, as they always have.
		if [ -n "$PACKAGE_DIR" ]; then package_version; else say "openRepoTools ($REPO @ $REF)"; fi
		exit 0 ;;
	--install) MODE="install"; shift ;;
	wip) shift; wip_dispatch "$@"; exit $? ;;
	*)
		die "openRepoTools installs the estate commands and creates the workspace they read.
    It takes --install, \`wip init\`, --help or --version. The commands themselves:
    \`park --help\`, \`resume --help\`, \`status --help\`."
		;;
	esac
done

if [ "$MODE" = "install" ]; then
	install_commands
	exit 0
fi

# NO ARGUMENT AT ALL is the usage, on stdout and exit 0: a person who typed the
# name to see what it is has asked a question, not made a mistake.
usage
exit 0
