#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
#
# park — leave this workstation with your half-finished work carried, from any
# folder: `park InkRouter`, or just `park` from inside the estate.
#
# Installed with the other three by `openRepoTools --install`.
#
# IT ADDS NO MECHANICS, exactly as the `openRepoShape` command adds none. It
# FINDS THE ESTATE and runs the estate's own `make park` — the family holder's,
# which fans out to each member's, or the assembly root's — and those targets
# run the SPECKIT GIT EXTENSION's `park.sh`, which is the one implementation of
# the WIP commit, the push and the record (#77, RULING 2026-09-09 ruling 1).
# Then it says what did NOT park and why none of it is park's to touch. It
# creates nothing, commits nothing and pushes nothing of its own.
#
# openRepoShape #82, under Brett Heap's RULING of 2026-09-09: `--repo` matches
# a LOCAL clone (ruling 1); the command is `park` (ruling 4). WITH NO <Name>
# the estate around the current directory is parked — the walk-up, unchanged
# since #82. WITH NO ESTATE AROUND IT EITHER, `park` lists every estate under
# the projects directory and ASKS whether to park them all; `park --all` (or
# `-a`) is the same sweep without the question, from anywhere. That is Brett
# Heap's RULING of 2026-09-10 in this repository, verbatim: "lets change that
# so it parks the current repo. if not in a repo, then asks if want to park
# all. and park all should be park -a or park --all" — and it supersedes his
# #91 ruling of the same day ("so just park will do park-all"), under which
# the bare form PARKED EVERY ESTATE unasked, which had itself superseded
# ruling 3 of #82 (the bare form REFUSES and lists what it found). With no
# terminal on stdin nothing can be asked, so the bare form refuses and names
# `--all`: a script that wants the sweep says so. THE SWEEP ITSELF is #91's —
# every estate in name order, continuing past a refusal, one summary. IN THAT
# SWEEP ONLY, a standalone root carrying no Speckit git overlay is SKIPPED and
# named instead of being run and counted as a refusal — Brett Heap's RULING
# of 2026-09-10 on openRepoShape #92, "skip roots without the overlay": a
# root nobody ever installed the overlay in is not a park that FAILED. Naming
# that same root still relays its own `make park` refusal. `resume` has no
# sweep and no `--all`: its bare form KEEPS ruling 3's refusal (see its
# header) — rebuilding every estate on a fresh machine by accident is the
# opposite risk from failing to park the one you meant.
# openRepoShape #82 wrote this, its #91 and #93 changed it, its #92 moved it
# here, and this repository's #6 and then its #8 last changed it.

set -euo pipefail

SELF_NAME="park"

NAME=""
REPO_SLUG=""
REPO_ARG=""
DRY_RUN=0
LANE=""
PASSTHROUGH=()
#: `flag_value`'s three answers, declared with the other globals so that a
#: `die` reaching `$FLAG_HINT` before any arm has asked is an empty hint under
#: `set -u`, never "unbound variable" at exit 1 — the one exit these arms must
#: not produce (the independent review of this branch, 2026-09-11).
FLAG_VALUE=""
FLAG_SHIFT=2
FLAG_HINT=""
#: 1 when EVERY estate under the projects directory is to be parked: set by
#: `--all` / `-a` on the command line, or by a YES to the question the bare
#: form asks when the walk-up found no estate around the cwd. Every other
#: path leaves this 0. `ALL_ROWS` is what the sweep will run, `<name><TAB>
#: <dir>` per estate, discovered ONCE by whichever of the two set it — so what
#: was listed, or asked about, is exactly what runs.
PARK_ALL=0
ALL_ROWS=""

say() { printf '%s\n' "$*"; }
# THE SAME STDERR SHAPE AS `die`, BUT IT RETURNS: a park-everything run (#91)
# has to report one estate's refusal and carry on to the next, which `die`'s
# own `exit` cannot do. `die` is this plus the exit, so the two can never
# print differently by accident.
refuse() { printf '\nREFUSED: %s\n' "$1" >&2; }
die() { refuse "$1"; exit "${2:-2}"; }

usage() {
    cat <<'USAGE'
park [<Name>] [--repo <owner/name>] [--dry-run] [--lane <name>] [-- ARGS...]
park --all | -a [--dry-run] [--lane <name>] [-- ARGS...]
park --help

Commit, push and RECORD every open feature of one estate, so another
workstation can take the work up with `resume <Name>`.

<Name> is the estate's folder under your projects directory: a FAMILY folder
(<Name>/<Name>/family.yaml) or a standalone project root (<Name>/project.yaml).
With no <Name> the estate around the current directory is parked. With NO
estate around it either, `park` lists every estate under your projects
directory and ASKS whether to park them all; anything but `y` parks nothing.
`park --all` (or `-a`) is that same sweep WITHOUT the question, from anywhere:
every estate in name order, continuing past a refusal or a non-zero `make
park`, then one summary. Where stdin is not a terminal the bare form cannot
ask, so it refuses and names `--all` - a script that wants the sweep says so.
(Brett Heap's RULING of 2026-09-10, superseding his #91 ruling of the same
day, under which the bare form swept unasked.) In the sweep a root with NO
Speckit git overlay is SKIPPED and counted apart rather than failed (his
RULING of the same day on #92); naming that root still relays its own
refusal, which names `setup-openspeckit`. `resume` has no `--all`: rebuilding
every estate on a fresh machine by accident is a worse mistake than failing
to park the one you meant. `--repo <owner/name>` names the estate by the
origin of a clone you ALREADY HAVE - in any spelling: the slug, the slug with
`.git`, or the whole https or ssh url. It never clones (`resume` is the
command that clones).

  --all, -a           park EVERY estate under your projects directory, unasked
  --project <Name>    the positional under a flag (also --name)
  --dry-run           rehearse: `make park ARGS=--dry-run`, which writes nothing
  --lane <name>       the lane recorded in the WIP commit subject
  --flag=<value>      every flag that takes a value takes it after an `=` too
                      (`--repo=<owner/name>`): the one way to give a value
                      that begins with `-`, which the spaced spelling refuses
  -- ARGS...          passed on to the extension through `make park ARGS='...'`

  $PROJECTS_DIR       your projects directory (default: ~/projects, then
                      ~/Projects - people spell it both ways)

WHAT PARK NEVER DOES: it creates nothing, and it moves nobody's HEAD. Commits
on a `main` that were never pushed, a dirty root mid pin-bump, and ignored
local files (.env, credentials) are REPORTED and left exactly where they are.
USAGE
}

# --- BEGIN shared estate resolver (park, resume, status: byte for byte) -----
#
# THIS BLOCK IS DUPLICATED IN THE OTHER COMMANDS, DELIBERATELY, and
# `tests/test_park_resume_commands.py` asserts the three copies are IDENTICAL.
# Each of these is one file a person has on PATH: a shared `orp-estate.sh`
# would be a fifth file for `--install` to place and a broken command the
# first time somebody copied only one of them. The rules themselves are NOT
# new: `<dir>/<Name>/family.yaml` carrying `kind: family-manifest` and naming
# `<Name>` is `setup-project.py`'s `family_folder_here`, doubled-name layout
# and all (#76, RULING ruling 3). What "naming" accepts is widened by #236,
# at `is_family_folder` below: the display `name:` or the repository's name.

# `$PROJECTS_DIR` names ONE directory when it is set. Otherwise both
# spellings, in that order, because people really do use both - and DEDUPED by
# inode, because on a case-insensitive filesystem (a stock Mac) `~/projects`
# and `~/Projects` are the same directory, and listing every estate twice
# would read as two estates.
PROJECTS_DIRS=()
if [ -n "${PROJECTS_DIR:-}" ]; then
    PROJECTS_DIRS=("$PROJECTS_DIR")
else
    PROJECTS_DIRS=("$HOME/projects")
    if [ -d "$HOME/Projects" ]; then
        if [ -d "$HOME/projects" ] && [ "$HOME/Projects" -ef "$HOME/projects" ]; then
            : # one directory under two spellings; name it once
        else
            PROJECTS_DIRS+=("$HOME/Projects")
        fi
    fi
fi

# Where a NEW clone would go: the first spelling that already exists, else the
# first named.
#
# shellcheck disable=SC2329  # `park` and `status` create nothing and never
# call this. The block is byte-identical in all three commands BY DESIGN, so
# each carries a couple of functions only another one asks.
projects_dir_for_new() {
    local dir
    for dir in "${PROJECTS_DIRS[@]}"; do
        if [ -d "$dir" ]; then
            printf '%s\n' "$dir"
            return 0
        fi
    done
    printf '%s\n' "${PROJECTS_DIRS[0]}"
}

# ONE SCALAR'S TEXT, read the way openRepoShape v1.1.0's `parse_yaml` reads
# it (`scripts/repo_shape.py`: `_strip_comment`, `_quote_end`,
# `_quoted_scalar`), so a file both of them read gets one answer (#243). A
# value that OPENS WITH A QUOTE is the text up to the quote that closes it -
# inside `"..."` a `\` takes the character after it along, and inside `'...'`
# a doubled `''` is one quote, so neither closes it - and whatever follows
# the close, a `# comment` included, is not the value: a ` #` inside the
# quotes is text. Any other value is cut at the first `#` that opens it or
# follows a space or tab, which is a comment, and trimmed; `a#b` is the text
# `a#b`. The escapes are honoured to find the close and never unescaped, as
# this reader never unescaped them: no branch, path, folder or repository
# name it is asked for carries one. A quote nothing closes is not YAML at
# all, and the text is kept whole, as before. What is NULL is not said here:
# `null` and `"null"` are both the text `null`, and `yaml_scalar` says which
# of the two it was.
trim_unquote() {
    local value="$1" quote="" ch="" i=1
    value="${value%$'\r'}"
    value="${value#"${value%%[![:space:]]*}"}"
    case "$value" in
    '"'* | "'"*)
        quote="${value:0:1}"
        while [ "$i" -lt "${#value}" ]; do
            ch="${value:$i:1}"
            if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
                i=$((i + 2))
            elif [ "$ch" != "$quote" ]; then
                i=$((i + 1))
            elif [ "$quote" = "'" ] && [ "${value:$((i + 1)):1}" = "'" ]; then
                i=$((i + 2))
            else
                printf '%s' "${value:1:$((i - 1))}"
                return 0
            fi
        done
        ;;
    *)
        value=" $value"
        value="${value%%[[:space:]]#*}"
        value="${value#"${value%%[![:space:]]*}"}"
        ;;
    esac
    printf '%s' "${value%"${value##*[![:space:]]}"}"
}

# ONE FOLDER NAME, and never a path. A `<Name>` off the command line and a
# `root:` out of somebody's workspace manifest both end up joined to the
# projects directory, so both are held to this: no empty string, no `/`, and
# no leading `.` — which is what stops `resume ../../elsewhere/X` from cloning
# outside the projects directory at all, rather than noticing afterwards.
is_safe_folder_name() {
    local name="$1"
    case "$name" in
    "" | .* | */*) return 1 ;;
    esac
    case "$name" in
    *[!A-Za-z0-9_.+@-]*) return 1 ;;
    esac
    return 0
}

# One indent-zero scalar out of a small YAML file, with parameter expansion
# only: no YAML library, no python per line, no forks per line. The line is
# found the way the Speckit extension's `workspace_read_scalar` finds one, at
# indent zero by its key; the VALUE is `trim_unquote`'s, above, which is
# openRepoShape v1.1.0's reading of it and differs from the extension's where
# a `#` sits inside quotes or with no space in front of it, and on a line that
# is not YAML at all (#243). The keys the three commands ask for are
# `family.yaml`'s and `project.yaml`'s, a record's `family:`, the two of
# `workspace.yaml`, and the shape pin's.
# `YAML_QUOTED` is 1 when the value was QUOTED: YAML's null is an unquoted
# `null` or `~`, and a quoted `"null"` is the text, which is a difference
# `trim_unquote`'s answer alone cannot carry (`named_for_repository`).
YAML_VALUE=""
YAML_QUOTED=0
yaml_scalar() {
    local file="$1" key="$2" line value
    YAML_VALUE=""
    YAML_QUOTED=0
    [ -f "$file" ] || return 1
    while IFS= read -r line || [ -n "$line" ]; do
        case "$line" in
        "$key":*) ;;
        *) continue ;;
        esac
        value="${line#"$key":}"
        case "${value#"${value%%[![:space:]]*}"}" in
        '"'* | "'"*) YAML_QUOTED=1 ;;
        esac
        YAML_VALUE="$(trim_unquote "$value")"
        return 0
    done <"$file"
    return 1
}

# A LINKED WORKTREE, not a checkout of its own: `.git` is a FILE holding a
# `gitdir:` line, and `--git-dir` and `--git-common-dir` disagree. A
# single-repository project's FEATURE worktree is a whole `project.yaml` root
# by construction, so without this the walk-up would answer `<worktree_root>/
# 001-my-feature` as an estate named after the branch — and `park` would run
# the write verb inside the very worktree the extension is about to park.
# GIT'S OWN ANSWER, and not the `.git`-is-a-file shortcut, because that shape
# is shared with a SUBMODULE and a leg must not be mistaken for a worktree:
# in a linked worktree `--git-dir` is `<common>/worktrees/<name>` and the two
# differ; in a submodule both are `<super>/.git/modules/<name>`; in an
# ordinary checkout both are `.git`. Verified against all three.
is_linked_worktree() {
    local dir="$1" gitdir common
    [ -e "$dir/.git" ] || return 1
    gitdir="$(git -C "$dir" rev-parse --git-dir 2>/dev/null || true)"
    common="$(git -C "$dir" rev-parse --git-common-dir 2>/dev/null || true)"
    [ -n "$gitdir" ] && [ -n "$common" ] && [ "$gitdir" != "$common" ]
}

# The PINNED copy inside a family holder: `<Family>/<Family>/members/<X>`, so
# the grandparent carries the `family.yaml`. It is detached, it is what
# `bootstrap` and `validate` read, and nobody's in-flight work is in it — a
# walk-up that answered it would run a WRITE verb on a detached HEAD. The
# working clone BESIDE the holder is the one a person works in, which is the
# same rule #80's holder dispatch follows.
is_pinned_member() {
    local dir="$1" parent grandparent
    parent="${dir%/*}"
    [ -n "$parent" ] && [ "$parent" != "$dir" ] || return 1
    grandparent="${parent%/*}"
    [ -n "$grandparent" ] && [ "$grandparent" != "$parent" ] || return 1
    yaml_scalar "$grandparent/family.yaml" kind || return 1
    [ "$YAML_VALUE" = "family-manifest" ]
}

# Is `<dir>` a FAMILY FOLDER? All three facts have to agree: the holder clone
# sits at `<dir>/<basename>`, its `family.yaml` says `kind: family-manifest`,
# and the folder is named for the family. A family has TWO names and either
# one is the folder's (#236): its display `name:`, which is the rule this
# block began with and what an estate laid out before openRepoShape v1.1.0
# has, or the LAST PART of its `repository:`, which is v1.1.0's rule - the
# holder, and the folder around it, are named for the REPOSITORY, `name:`
# being only a title (opensoft/openRepoShape#629), and the holder's `make
# siblings` stops before cloning when the folder is not that name
# (opensoft/openRepoShape#543). A family whose two names differ has no layout
# that works for both unless both are accepted. A directory called Northwind
# is a directory called Northwind; what makes it a family folder is read,
# never guessed from a name. The repository arm is `named_for_repository`,
# below, which `status` and `resume` also ask when one family is on disk in
# both layouts (#243).
is_family_folder() {
    local dir="$1" name manifest
    name="${dir##*/}"
    [ -n "$name" ] || return 1
    manifest="$dir/$name/family.yaml"
    yaml_scalar "$manifest" kind || return 1
    [ "$YAML_VALUE" = "family-manifest" ] || return 1
    if yaml_scalar "$manifest" name && [ "$YAML_VALUE" = "$name" ]; then
        return 0
    fi
    named_for_repository "$dir"
}

# Is `<dir>` named for its holder's REPOSITORY: the LAST PART of
# `<dir>/<basename>/family.yaml`'s `repository:`, as v1.1.0's `repo_basename`
# takes it (`rsplit("/", 1)[-1]`)? That is the folder v1.1.0 prescribes, and
# where `make siblings` moves a holder that is not in it. AN UNQUOTED `null`
# OR `~` IS NO REPOSITORY (#243) - nor `Null` or `NULL`, the spellings
# v1.1.0's `_scalar` reads as null too - so a folder called `null` is not
# named for one, and `is_family_folder` has only the display `name:` left to
# go on, which is v1.1.0's own fallback. A QUOTED `"null"` is the text.
named_for_repository() {
    local dir="$1" name
    name="${dir##*/}"
    [ -n "$name" ] || return 1
    yaml_scalar "$dir/$name/family.yaml" repository || return 1
    if [ "$YAML_QUOTED" -eq 0 ]; then
        case "$YAML_VALUE" in null | Null | NULL | '~') return 1 ;; esac
    fi
    [ "${YAML_VALUE##*/}" = "$name" ]
}

is_project_root() {
    local dir="$1"
    yaml_scalar "$dir/project.yaml" kind || return 1
    [ "$YAML_VALUE" = "project-manifest" ] || return 1
    is_pinned_member "$dir" && return 1
    is_linked_worktree "$dir" && return 1
    return 0
}

ESTATE_KIND=""
ESTATE_DIR=""
ESTATE_ROOT=""
ESTATE_NAME=""

# Is THIS directory an estate? A FAMILY FOLDER WINS over a standalone root of
# the same name: the holder is what drives the members, so a family answered as
# one of its own members would act on one project and report the estate done.
estate_here() {
    local dir="$1"
    if is_family_folder "$dir"; then
        ESTATE_KIND="family"
        ESTATE_DIR="$dir"
        ESTATE_NAME="${dir##*/}"
        ESTATE_ROOT="$dir/$ESTATE_NAME"
        return 0
    fi
    if is_project_root "$dir"; then
        ESTATE_KIND="standalone"
        ESTATE_DIR="$dir"
        ESTATE_NAME="${dir##*/}"
        ESTATE_ROOT="$dir"
        return 0
    fi
    return 1
}

# The NEAREST estate at or above `<dir>`: the holder beside you, or the project
# root you are standing in. NEAREST WINS, so standing inside a member of a
# family acts on that member - the estate the person is standing in. A pinned
# copy and a linked worktree are WALKED PAST rather than answered, so
# `<Family>/<Family>/members/<X>` reaches the family folder and a feature
# worktree reaches the root that owns it.
estate_walk_up() {
    local dir="$1"
    while [ -n "$dir" ] && [ "$dir" != "/" ]; do
        estate_here "$dir" && return 0
        dir="${dir%/*}"
    done
    return 1
}

estate_by_name() {
    local want="$1" dir
    is_safe_folder_name "$want" || return 1
    for dir in "${PROJECTS_DIRS[@]}"; do
        estate_here "$dir/$want" && return 0
    done
    return 1
}

# Every estate under the projects directory, one row each. What ruling 3 asks
# a refusal to print: a person who typed the command in the wrong folder needs
# the names, not an instruction to go and look for them.
list_estates() {
    local dir child
    for dir in "${PROJECTS_DIRS[@]}"; do
        [ -d "$dir" ] || continue
        for child in "$dir"/*; do
            [ -d "$child" ] || continue
            if is_family_folder "$child"; then
                printf '  family   %-20s %s\n' "${child##*/}" "$child"
            elif is_project_root "$child"; then
                printf '  project  %-20s %s\n' "${child##*/}" "$child"
            fi
        done
    done
}

estates_or_none() {
    local rows
    rows="$(list_estates)"
    if [ -n "$rows" ]; then
        printf '%s\n' "$rows"
    else
        printf '  (none)\n'
    fi
}

# Every FAMILY FOLDER under the projects directory whose holder's
# `family.yaml` says `id: <id>`, one per line, in `list_estates`' order, and
# nothing for an empty <id>. The id is what `park.sh` files a family's record
# under (`workspace_family_name` reads it), and it is the SAME in every layout
# of one family: v1.1.0's folder named for the repository and the
# display-name folder before it are two directories, two estates and ONE
# record (#243). So this is how `resume` finds a holder its record's folder
# no longer names, and how `status` sees one family laid out twice.
#
# ONE FOLDER IS LISTED ONCE, however many spellings reach it. `PROJECTS_DIRS`
# is deduped by inode, but the folders inside two distinct directories are
# not: a `~/Projects/Quill` that is a LINK to `~/projects/Quill` is one
# holder, and listing it twice made `resume` refuse "2 family folders" and
# `status` report one family "laid out 2 times" (the review of #251, finding
# 1). So a folder that is `-ef` one already listed is skipped, and the first
# spelling, in `PROJECTS_DIRS`' order, is the one named.
#
# shellcheck disable=SC2329  # `park` never asks: it files the record, and
# never looks a holder up by it. The block is byte-identical by design.
family_folders_with_id() {
    local want="$1" dir child prior dup
    local -a found=()
    [ -n "$want" ] || return 0
    for dir in "${PROJECTS_DIRS[@]}"; do
        [ -d "$dir" ] || continue
        for child in "$dir"/*; do
            [ -d "$child" ] || continue
            is_family_folder "$child" || continue
            yaml_scalar "$child/${child##*/}/family.yaml" id || continue
            [ "$YAML_VALUE" = "$want" ] || continue
            dup=0
            for prior in ${found[@]+"${found[@]}"}; do
                if [ "$child" -ef "$prior" ]; then dup=1; fi
            done
            [ "$dup" -eq 0 ] || continue
            found+=("$child")
            printf '%s\n' "$child"
        done
    done
}

lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }

# `owner/name` — CASE PRESERVED, so a refusal prints back what a person can
# type — out of any spelling git accepts: owner/name, owner/name.git,
# https://host/owner/name(.git), git@host:owner/name.git,
# ssh://git@host/owner/name.git, and a token-bearing https url. Callers
# compare with `lower` on both sides, because GitHub owners and names are
# case-insensitive.
#
# A FILESYSTEM PATH IS REFUSED rather than parsed, for the reason the
# extension's own `workspace_org_from_url` refuses one: `/srv/remotes/Name.git`
# has a name but no owner, and inventing `remotes` as the owner would let
# `--repo remotes/Name` match a bare repository on disk. Such a clone answers
# `<Name>` and the walk-up; it does not answer `--repo`.
repo_slug_of() {
    local url="$1" rest owner name
    rest="$url"
    case "$rest" in
    /* | ./* | ../* | [A-Za-z]:[/\\]* | file://*) return 1 ;;
    esac
    rest="${rest%/}"
    rest="${rest%.git}"
    case "$rest" in
    *://*)
        rest="${rest#*://}"
        rest="${rest#*@}"
        rest="${rest#*/}"
        ;;
    *@*:*)
        rest="${rest#*@}"
        rest="${rest#*:}"
        ;;
    esac
    case "$rest" in
    */*) ;;
    *) return 1 ;;
    esac
    name="${rest##*/}"
    owner="${rest%/*}"
    owner="${owner##*/}"
    [ -n "$owner" ] && [ -n "$name" ] || return 1
    case "$owner$name" in
    *[!A-Za-z0-9_.-]*) return 1 ;;
    esac
    printf '%s/%s\n' "$owner" "$name"
}

# The clone under the projects directory whose `origin` IS that repository -
# ruling 1. Two levels deep, because the estates this standard places are
# `<projects>/<Project>` and `<projects>/<Family>/<Project>`.
clone_with_origin() {
    local want dir candidate origin found
    want="$(lower "$1")"
    for dir in "${PROJECTS_DIRS[@]}"; do
        [ -d "$dir" ] || continue
        for candidate in "$dir"/*/ "$dir"/*/*/; do
            candidate="${candidate%/}"
            [ -e "$candidate/.git" ] || continue
            origin="$(git -C "$candidate" remote get-url origin 2>/dev/null || true)"
            [ -n "$origin" ] || continue
            found="$(repo_slug_of "$origin" || true)"
            [ -n "$found" ] || continue
            if [ "$(lower "$found")" = "$want" ]; then
                printf '%s\n' "$candidate"
                return 0
            fi
        done
    done
    return 1
}

# The family's members, by the same `  - project: <Project>` row `family.py`
# writes and `siblings.py` reads. The working clone is `<family folder>/
# <Project>`, beside the holder.
family_members() {
    local manifest="$1" line
    [ -f "$manifest" ] || return 0
    while IFS= read -r line || [ -n "$line" ]; do
        case "$line" in
        "  - project: "*) printf '%s\n' "$(trim_unquote "${line#  - project: }")" ;;
        esac
    done <"$manifest"
}

# The branch a checkout belongs on: its OWN manifest's `tracking_branch:` —
# the field `scripts/bootstrap.py` already reads to place the legs — falling
# back to `main`. The workspace manifest records a `tracking_branch` too, but
# that is PROVENANCE from the machine that parked; the local declaration is
# what this machine's clone actually tracks, and the extension takes the same
# line about `worktree_root`.
tracking_branch_of() {
    local root="$1"
    if yaml_scalar "$root/family.yaml" tracking_branch && [ -n "$YAML_VALUE" ]; then
        printf '%s\n' "$YAML_VALUE"
        return 0
    fi
    if yaml_scalar "$root/project.yaml" tracking_branch && [ -n "$YAML_VALUE" ]; then
        printf '%s\n' "$YAML_VALUE"
        return 0
    fi
    printf 'main\n'
}

# The legs a root declares, by path, out of `project.yaml`'s `legs:` block —
# the same rows `scripts/bootstrap.py` reads, and in the order it reads them,
# `role:` before `path:`. The assembly leg is `path: "."` and is the root
# itself, so it is not one of these.
#
# shellcheck disable=SC2329  # only `resume` bootstraps a root, so only
# `resume` has to ask where its legs are; see the note above.
legs_of() {
    local root="$1" line role="" path=""
    [ -f "$root/project.yaml" ] || return 0
    while IFS= read -r line || [ -n "$line" ]; do
        line="${line%$'\r'}"
        case "$line" in
        '  - role:'*)
            role="$(trim_unquote "${line#*:}")"
            path=""
            ;;
        '    path:'*)
            path="$(trim_unquote "${line#*:}")"
            [ -n "$role" ] || continue
            [ "$role" != "assembly" ] || continue
            [ "$path" != "." ] || continue
            [ -n "$path" ] && printf '%s\n' "$path"
            ;;
        [![:space:]]*) role=""; path="" ;;
        esac
    done <"$root/project.yaml"
}

# The branch a leg's OWN row declares for it: `leg_branch_of <root> <leg path>`
# prints the `branch:` of the `legs:` row whose `path:` is <leg path>, and
# NOTHING when that row declares none, or no row has the path. It is the
# value `scripts/bootstrap.py` puts first — `str(leg.get("branch") or branch)`
# — so a leg is placed on its own `branch:` and on the root's
# `tracking_branch:` only when its row has no `branch:` of its own; a caller
# falls back to `tracking_branch_of` on an empty answer, which is that `or`.
#
# Read the way `legs_of` reads the same rows — `role:` before `path:`, the
# assembly row and a row with no role skipped — except that `branch:` may sit
# on either side of `path:` in its row, so the answer comes the moment the
# row has shown both, and a row that does not open with `- role:` clears what
# the row before it left. A leg is found by its path SPELLED EITHER WAY: the
# caller may hold `.gitmodules`' `spec` (`status`) or the manifest's own text
# (`resume`), and `./spec` and `spec/` are one leg, so a leading `./` and a
# trailing `/` come off both sides before they are compared.
#
# WHAT IS NO BRANCH: an empty value, `null`, `Null`, `NULL` or `~`, once a
# trailing ` # comment` is cut off the value (a branch name holds no space, so
# ` #` cannot be inside one). The loader is not matched to the letter: it keeps
# a QUOTED `"null"` and `'~'` as branches and reads `false` and `0` as no
# branch, while this does the opposite for both. No real branch is named any
# of those, so nothing here chases the difference. (Since #243 `trim_unquote`
# cuts that comment as well; the cut here runs first and is kept.)
#
# WHO ASKS IT: `resume`'s leg guard (`root_legs_ok`), `status`'s
# `estate_repos` and `read_leg`, and `park`'s not-carried report
# (`report_repos`, #242, PR #249) - each to know the branch a leg belongs on.
#
# shellcheck disable=SC2329  # a `park` from before #249 never calls it;
# the disable can go once #249 has landed.
leg_branch_of() {
    local root="$1" want="$2" line role="" path="" branch=""
    [ -n "$want" ] || return 0
    [ -f "$root/project.yaml" ] || return 0
    want="${want#./}"
    want="${want%/}"
    while IFS= read -r line || [ -n "$line" ]; do
        line="${line%$'\r'}"
        case "$line" in
        '  - role:'*)
            role="$(trim_unquote "${line#*:}")"
            path=""
            branch=""
            ;;
        '  - '*) role=""; path=""; branch="" ;;
        '    path:'*)
            path="$(trim_unquote "${line#*:}")"
            path="${path#./}"
            path="${path%/}"
            [ -n "$role" ] || path=""
            [ "$role" != "assembly" ] || path=""
            ;;
        '    branch:'*)
            branch="${line#*:}"
            branch="$(trim_unquote "${branch%%[[:space:]]#*}")"
            case "$branch" in null | Null | NULL | '~') branch="" ;; esac
            ;;
        [![:space:]]*) role=""; path=""; branch="" ;;
        esac
        if [ -n "$path" ] && [ "$path" = "$want" ] && [ -n "$branch" ]; then
            printf '%s\n' "$branch"
            return 0
        fi
    done <"$root/project.yaml"
}
# --- END shared estate resolver ---------------------------------------------

# The legs AS MOUNTED, out of `.gitmodules` — the same file `make bootstrap`
# mounts them from. Not the shared `legs_of`, which reads what `project.yaml`
# DECLARES: the report is about what is on disk and might hold an unpushed
# commit, so a leg mounted somewhere unusual, or one whose manifest row was
# hand-edited away, still has to be asked. `git config --get-regexp` prints
# `key value`, so the value is everything after the first space.
# `-z`, so the key and the value cannot be confused: a submodule NAME may
# carry a space (`[submodule "sp ace"]`), and splitting the plain output at
# its first space would hand back `ace.path spec` and miss the leg; with `-z`
# each entry is `key<LF>value<NUL>`, and a key with no value has no LF at all
# and is not a path.
submodule_paths() {
    local root="$1" record path
    [ -f "$root/.gitmodules" ] || return 0
    while IFS= read -r -d '' record; do
        case "$record" in
        *$'\n'*) path="${record#*$'\n'}" ;;
        *) continue ;;
        esac
        [ -n "$path" ] || continue
        [ -e "$root/$path/.git" ] || continue
        printf '%s\n' "$path"
    done < <(git -C "$root" config --file .gitmodules -z \
        --get-regexp '^submodule\..*\.path$' 2>/dev/null || true)
}

# --- what did not park ------------------------------------------------------

# `label<TAB>path` per repository the report reads.
#
# `members/<Project>` INSIDE A FAMILY HOLDER IS DELIBERATELY NOT HERE. That
# copy is pinned and DETACHED, for `bootstrap` and `validate`; nobody's
# in-flight work is in it, and it carries no local `main` to be ahead of. The
# working clone BESIDE the holder is where a person works, so it is what is
# read - the same reason the holder's own `park` target dispatches there (#80).
report_repos() {
    local leg member branch legbranch
    if [ "$ESTATE_KIND" = "family" ]; then
        printf 'holder\t%s\t%s\n' "$(tracking_branch_of "$ESTATE_ROOT")" \
            "$ESTATE_ROOT"
        while IFS= read -r member; do
            [ -e "$ESTATE_DIR/$member/.git" ] || continue
            branch="$(tracking_branch_of "$ESTATE_DIR/$member")"
            printf 'root\t%s\t%s\n' "$branch" "$ESTATE_DIR/$member"
            while IFS= read -r leg; do
                # A LEG'S branch is the `branch:` its own row declares, else
                # the ROOT's `tracking_branch:` — what `scripts/bootstrap.py`
                # places it on (#235, #238; #242 here). Its unpushed commits
                # are on THAT branch, so that is the one the report counts.
                # `|| true`: a `project.yaml` that cannot be read fails
                # `leg_branch_of`, and under `set -e` that would end this
                # subshell and drop this leg's row and every row after it,
                # silently; the root's branch is the answer then.
                legbranch="$(leg_branch_of "$ESTATE_DIR/$member" "$leg" || true)"
                printf 'leg\t%s\t%s\n' "${legbranch:-$branch}" "$ESTATE_DIR/$member/$leg"
            done < <(submodule_paths "$ESTATE_DIR/$member")
        done < <(family_members "$ESTATE_ROOT/family.yaml")
        return 0
    fi
    branch="$(tracking_branch_of "$ESTATE_ROOT")"
    printf 'root\t%s\t%s\n' "$branch" "$ESTATE_ROOT"
    while IFS= read -r leg; do
        # The leg's own `branch:`, else the root's, and `|| true` for the
        # reason the family loop gives.
        legbranch="$(leg_branch_of "$ESTATE_ROOT" "$leg" || true)"
        printf 'leg\t%s\t%s\n' "${legbranch:-$branch}" "$ESTATE_ROOT/$leg"
    done < <(submodule_paths "$ESTATE_ROOT")
}

# Commits on the TRACKING BRANCH that origin has never seen. The branch is
# the root's own `tracking_branch:` — or, for a leg, the `branch:` its own row
# declares (`report_repos`, #242) — and not a hard-coded `main`: a project
# that ships from `trunk` would otherwise have been asked a question about a
# branch it does not have and answered "nothing", which is worse than not
# asking. BOTH refs must exist locally or there is no question to ask — a
# detached pinned checkout has no local branch, and a clone with no remote has
# no remote-tracking one. It is read against the remote-tracking ref this
# machine already has: park fetches nothing, so the count is as of the last
# fetch and the report says so.
unpushed_on_tracking() {
    local repo="$1" branch="$2"
    [ -n "$branch" ] || return 1
    git -C "$repo" rev-parse --verify -q "refs/heads/$branch" >/dev/null 2>&1 || return 1
    git -C "$repo" rev-parse --verify -q "refs/remotes/origin/$branch" >/dev/null 2>&1 || return 1
    git -C "$repo" rev-list --count "origin/$branch..$branch" 2>/dev/null || return 1
}

# A dirty ROOT, which is where a pin bump happens. It also answers for a dirty
# LEG: `git status --porcelain` at a root with submodules reports a leg whose
# checkout has changes as ` M <leg>`, so one question covers both.
dirty_count() {
    local repo="$1" out
    out="$(git -C "$repo" status --porcelain 2>/dev/null || true)"
    [ -n "$out" ] || return 1
    printf '%s\n' "$out" | wc -l | tr -d ' '
}

LEFT=0

not_parked_report() {
    local label branch path count
    say ""
    say "what park does NOT carry, and why none of it is park's to touch:"
    while IFS=$'\t' read -r label branch path; do
        [ -n "${path:-}" ] || continue
        count="$(unpushed_on_tracking "$path" "$branch" || true)"
        if [ -n "$count" ] && [ "$count" != "0" ]; then
            LEFT=$((LEFT + 1))
            printf '  unpushed %-6s %-7s %s\n' "$branch" "$label" "$path"
            printf '                 %s commit(s) on %s that origin has not seen (as of the last fetch).\n' "$count" "$branch"
            if [ "$label" = "leg" ]; then
                printf '                 A leg goes up as a pull request too; park carries FEATURE branches only.\n'
            else
                printf '                 The root is PR-only - land them as a pull request; park never pushes %s.\n' "$branch"
            fi
        fi
        case "$label" in
        holder | root)
            count="$(dirty_count "$path" || true)"
            if [ -n "$count" ]; then
                LEFT=$((LEFT + 1))
                printf '  uncommitted    %-7s %s\n' "$label" "$path"
                printf '                 %s path(s) changed at the root. A pin bump in progress is not\n' "$count"
                printf "                 park's to commit: the root carries no feature branch at all.\n"
            fi
            ;;
        esac
    done < <(report_repos)
    printf '  ignored files  never travel. .env, credentials and every other\n'
    printf '                 gitignored file come back from your secrets manager, not from git.\n'
    if [ "$LEFT" -eq 0 ]; then
        say "  nothing else was left behind: no unpushed tracking-branch commit, no dirty root."
    fi
}

require_make() {
    command -v make >/dev/null 2>&1 ||
        die "no \`make\` on PATH, so the estate's own \`make park\` cannot be run.
    Install it (\`xcode-select --install\` on macOS, \`sudo apt-get install -y
    make\` on Debian and Ubuntu), then re-run \`$SELF_NAME\`."
}

# --- run one estate's own `make park` ---------------------------------------
#
# FACTORED OUT so park-everything (#91, below) calls exactly what `park
# <Name>` calls: one `make park`, one not-carried report, one outcome line.
# ESTATE_KIND / ESTATE_DIR / ESTATE_ROOT / ESTATE_NAME must already be set —
# by `estate_walk_up`, `estate_by_name`, `clone_with_origin`, or, for
# park-everything, a direct `estate_here` on a discovered child. RETURNS a
# status rather than exiting, so a park-everything run can carry on to the
# next estate instead of dying on the first refusal or non-zero `make`; the
# single-estate call site below turns that same return straight into its own
# exit code, so nothing here behaves differently for either caller.
run_one_estate() {
    LEFT=0
    if [ ! -f "$ESTATE_ROOT/Makefile" ]; then
        refuse "$ESTATE_ROOT has no Makefile, so it has no \`park\` target to run.
    A scaffolded root and a family holder both ship one; a root that has lost
    it is re-synced with \`update-shape.py\`."
        return 2
    fi

    # ONE `ARGS=` STRING, because that is the whole interface the two
    # Makefiles offer (#80): the recipe word-splits `$(ARGS)`, so an argument
    # carrying a space cannot survive the trip and this command does not
    # pretend otherwise.
    MAKE_ARGS=""
    [ "$DRY_RUN" -eq 0 ] || MAKE_ARGS="--dry-run"
    if [ -n "$LANE" ]; then
        MAKE_ARGS="${MAKE_ARGS:+$MAKE_ARGS }--lane $LANE"
    fi
    if [ ${#PASSTHROUGH[@]} -gt 0 ]; then
        MAKE_ARGS="${MAKE_ARGS:+$MAKE_ARGS }${PASSTHROUGH[*]}"
    fi

    if [ "$ESTATE_KIND" = "family" ]; then
        say "park: $ESTATE_NAME - the family holder at $ESTATE_ROOT"
        say "  the members' working clones beside it are where the verb runs."
    else
        say "park: $ESTATE_NAME - the assembly root at $ESTATE_ROOT"
    fi
    # shellcheck disable=SC2016  # the single quotes are LITERAL: this line is
    # the `make` invocation a person can copy, and `ARGS='...'` is how they
    # must type it.
    say "  make park${MAKE_ARGS:+ ARGS='$MAKE_ARGS'}"
    say ""

    # THE EXIT CODE IS MAKE'S, UNCHANGED. `park.sh`'s own 0/1/2/3 do not
    # survive make - GNU make reports ANY non-zero recipe status as its own
    # exit 2 and prints `Error <code>` - and this command neither rewrites
    # nor rescues that: the code is in make's diagnostic, and a run that
    # refused must never read as success. #80's Makefile says the same thing
    # at the target.
    # AN ARRAY, so `ARGS=--feature 001-a --dry-run` reaches make as ONE
    # assignment. `${var:+"..."}` unquoted is one word in bash today and a
    # thing a reader has to squint at; an array is neither.
    local -a park_call=(make park)
    [ -z "$MAKE_ARGS" ] || park_call+=("ARGS=$MAKE_ARGS")
    local one_status
    set +e
    (cd "$ESTATE_ROOT" && exec "${park_call[@]}")
    one_status=$?
    set -e

    not_parked_report

    say ""
    say "park: $ESTATE_NAME - \`make park\` exited $one_status; $LEFT thing(s) above are not park's to carry."
    if [ "$DRY_RUN" -eq 1 ]; then
        say "  --dry-run: nothing was committed, pushed or recorded."
    fi
    return "$one_status"
}

# --- park-everything (#91): asked for with --all, or asked about; and what it
# --- SKIPS (#92) -------------------------------------------------------------
#
# THE SWEEP is #91's - Brett Heap's RULING of 2026-09-10, verbatim: "for the
# park-all, lets make that be park with no estate option. so just park will
# do park-all." HOW IT IS REACHED changed the same day, under his later
# RULING in this repository, verbatim: "lets change that so it parks the
# current repo. if not in a repo, then asks if want to park all. and park all
# should be park -a or park --all." So: `park --all` / `park -a` runs the
# sweep from anywhere, unasked. Bare `park` with no <Name> and no estate
# around the cwd LISTS the estates and ASKS (`ask_before_parking_every_estate`,
# below); a YES runs the same sweep, anything else parks nothing, and with no
# terminal on stdin nothing is asked - the bare form refuses and names
# `--all`, because a sweep a script did not spell out is a sweep nobody
# chose. `park <Name>`, `park --repo` and the walk-up above (NEAREST STILL
# WINS - standing inside an estate parks THAT estate, never the workstation)
# are exactly as they were under #82. `resume` has no sweep and no `--all`:
# its bare form keeps ruling 3 of #82's refusal - see the note in its header
# - because rebuilding every estate on a fresh machine by accident is the
# opposite risk from failing to park the one you meant.
#
# Brett Heap's SECOND RULING of that day, on openRepoShape #92, verbatim:
# "rule on the #92 open item: skip roots without the overlay." The sweep on
# a real workstation found four roots with no Speckit git overlay, whose own
# `make park` refuses with exit 2 - so a bare `park` there could only ever
# exit 1, and the one summary line that matters was four false failures deep.
# A root nobody installed the overlay in is not a park that FAILED; it is a
# park there was never anything to run. So THE SWEEP skips it, names it, and
# counts it apart. ONLY THE SWEEP: `park <Name>` and `park --repo` named a
# root, and a person who named one gets its refusal, which names the
# installer.

# Every estate under the projects directory, `<name><TAB><dir>` per row, in
# NAME order rather than discovery order: `~/projects` and `~/Projects` can
# each hold estates when both exist, and reading them in discovery order
# would print every estate under one spelling before any estate under the
# other, instead of together, alphabetically. THE SAME RULES `list_estates`
# USES - a family folder or a standalone project root, one level under a
# projects directory - which is what already leaves out a pinned
# `members/<X>` copy (three levels down) and a linked feature worktree
# (rejected by `is_project_root` itself).
all_estate_dirs() {
    local dir child
    for dir in "${PROJECTS_DIRS[@]}"; do
        [ -d "$dir" ] || continue
        for child in "$dir"/*; do
            [ -d "$child" ] || continue
            if is_family_folder "$child" || is_project_root "$child"; then
                printf '%s\t%s\n' "${child##*/}" "$child"
            fi
        done
    done | LC_ALL=C sort -t $'\t' -k1,1
}

# Does this root carry the Speckit git overlay its own `make park` requires?
# THE SAME TEST THE MAKEFILE MAKES, spelled the same way: `templates/
# assembly-root/Makefile` opens the target with `test -x $(PARK) || { echo
# 'make park $(OVERLAY_MISSING)' >&2; exit 2; }`, where `$(PARK)` is
# `.specify/extensions/git/scripts/bash/park.sh`. Asking the question the
# target's own way is what keeps the sweep's answer and the target's answer
# from ever disagreeing - a probe that tested for the DIRECTORY, say, would
# skip a root the target would have run, or run one it would refuse.
#
# ONLY A STANDALONE ROOT IS ASKED. A family holder's Makefile makes no such
# test: it fans out with `scripts/siblings.py --make park`, and each member's
# own root refuses for itself and is REPORTED BY THE HOLDER. Pre-checking a
# holder here would skip a whole family for want of a file its own Makefile
# never reads, and pre-checking its members would answer, worse and earlier,
# a question the holder already answers per member.
has_speckit_overlay() {
    [ -x "$1/.specify/extensions/git/scripts/bash/park.sh" ]
}

# Run `run_one_estate` for every estate in `$1` (`all_estate_dirs`'s rows),
# in name order, CONTINUING PAST A REFUSAL OR A NON-ZERO `make park`: one
# estate's trouble is not a reason to leave the others unparked. A SKIP IS
# NOT A PASS (#80's rule), so an estate whose own report is not clean - a
# non-zero `make park`, or a zero one that still left something behind -
# counts against the overall exit code exactly as a refusal does.
#
# THE CALLER HAS ALREADY LISTED THE ROWS - `--all` under its own heading, the
# bare form above its question - so nothing prints here before the first
# `=== park <Name> ===`, and the rows are passed in rather than discovered
# again: what was listed is exactly what runs.
#
# A ROOT WITHOUT THE OVERLAY IS THE ONE THING THAT COUNTS NEITHER WAY (#92's
# ruling, above): nothing ran in it, so there is no report to be clean or
# unclean, and the exit code is computed over the estates that DID run. That
# is not #80's rule bent - #80 is about a verb that was asked and did not
# answer; this is a verb that was never askable.
park_every_estate() {
    local rows="$1" name dir estate_status total=0 bad=0 skipped=0 skipped_clause=""
    local -a summary=()

    while IFS=$'\t' read -r name dir; do
        [ -n "$name" ] || continue
        estate_here "$dir" || continue
        say "=== park $ESTATE_NAME ==="
        # ONE LINE, AND NOTHING RUNS HERE (#92's ruling). The block still
        # gets its `=== park <Name> ===` heading, because a root left out of
        # the run silently is a root a person has to go and look for.
        #
        # `= standalone` AND NOT `!= family`, though `estate_here` sets only
        # those two today: the kinds are matched POSITIVELY so that a kind
        # nobody has invented yet is RUN and answered by its own Makefile,
        # rather than silently skipped by a test written before it existed.
        # Skipping is the answer for the one shape whose Makefile we have
        # read (Copilot's review of #7).
        if [ "$ESTATE_KIND" = "standalone" ] && ! has_speckit_overlay "$ESTATE_ROOT"; then
            if [ "$DRY_RUN" -eq 1 ]; then
                say "  would skip (no Speckit overlay; install it with: setup-openspeckit)"
            else
                say "  SKIPPED (no Speckit overlay; install it with: setup-openspeckit)"
            fi
            summary+=("  skipped    $ESTATE_NAME (no Speckit overlay)")
            skipped=$((skipped + 1))
            say ""
            continue
        fi
        total=$((total + 1))
        estate_status=0
        run_one_estate || estate_status=$?
        if [ "$estate_status" -ne 0 ]; then
            summary+=("  refused    $ESTATE_NAME (exit $estate_status)")
            bad=$((bad + 1))
        elif [ "$LEFT" -ne 0 ]; then
            summary+=("  findings   $ESTATE_NAME ($LEFT thing(s) not carried)")
            bad=$((bad + 1))
        else
            summary+=("  parked     $ESTATE_NAME")
        fi
        say ""
    done <<<"$rows"

    say "park: summary"
    local line
    for line in "${summary[@]}"; do
        say "$line"
    done
    # THE #91 LINE, UNCHANGED, when nothing was skipped - which is every
    # workstation whose roots all carry the overlay. The clause appears only
    # when there is something to say, and it says what was NOT run rather
    # than padding the line with a zero.
    [ "$skipped" -eq 0 ] || skipped_clause=", $skipped skipped (no overlay)"
    say "park: $((total - bad)) estate(s) parked, $bad refused or with findings$skipped_clause"
    if [ "$total" -eq 0 ] && [ "$skipped" -ne 0 ]; then
        say "park: nothing here was park-able: every estate found is without the"
        say "      Speckit git overlay, so nothing ran and nothing failed. Install"
        say "      it where you want park to work: setup-openspeckit"
    fi
    [ "$bad" -eq 0 ]
}

# The bare form, standing outside every estate: LIST, then ASK. Brett Heap's
# RULING of 2026-09-10 in this repository (quoted at the section head): "if
# not in a repo, then asks if want to park all." ONLY A TERMINAL CAN ANSWER,
# so with anything else on stdin - a pipe, /dev/null, a cron job, an
# assistant's tool call - nothing is asked and nothing is parked: the refusal
# names `--all`, which is how a script says it meant the sweep. Anything but
# y/yes is a no, in either case: parking every estate on a workstation is an
# answer a person has to give, not one they get by pressing Enter. Sets
# PARK_ALL and ALL_ROWS on a yes; every other way out is a `die`.
ask_before_parking_every_estate() {
    local answer
    ALL_ROWS="$(all_estate_dirs)"
    [ -n "$ALL_ROWS" ] ||
        die "no estate around $PWD, no <Name> was given, and there is no estate under
    ${PROJECTS_DIRS[*]} at all, so there is nothing to park. An estate is a
    FAMILY folder (<Name>/<Name>/family.yaml) or a standalone project root
    (<Name>/project.yaml), one level under your projects directory;
    \`resume <Name>\` brings one back."
    [ -t 0 ] ||
        die "no estate around $PWD, and no <Name> was given. Outside every estate
    \`$SELF_NAME\` ASKS before parking them all, and stdin is not a terminal
    here, so nobody could answer: nothing was parked. \`$SELF_NAME --all\`
    parks every estate below without asking; \`$SELF_NAME <Name>\` parks one:

$(estates_or_none)"
    require_make
    say "park: no estate around $PWD, and no <Name> was given. Every estate under ${PROJECTS_DIRS[*]}:"
    say ""
    say "$(estates_or_none)"
    say ""
    # THE QUESTION GOES TO STDERR, where a prompt belongs: with stdout sent to
    # a file the question still reaches the terminal rather than the file,
    # and a run hung on a question nobody can see is the one shape this must
    # never take. `read` fails on end-of-file (Ctrl-D), which is a no.
    printf 'park every estate above? [y/N] ' >&2
    IFS= read -r answer || answer=""
    case "$answer" in
    [Yy] | [Yy][Ee][Ss]) PARK_ALL=1 ;;
    *)
        die "nothing was parked: the answer was not yes. \`$SELF_NAME <Name>\` parks one of
    the estates above, \`$SELF_NAME --all\` parks every one of them without
    asking, and \`$SELF_NAME\` from inside an estate parks that one."
        ;;
    esac
}

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

# THE VALUE OF A VALUE-TAKING FLAG, in either spelling, read in ONE place so
# that the ways of not giving one are ONE refusal. `flag_value "$@"` from an
# arm sets `FLAG_VALUE` and `FLAG_SHIFT` — the arguments that arm then
# consumes, 2 for `--flag value` and 1 for `--flag=value` — or it returns 1
# with `FLAG_HINT` carrying whatever that refusal has to add. THE WORDS STAY
# AT THE ARM: each flag says what its own value is, and this says only whether
# one was given.
# A VALUE BEGINNING WITH `-` IS NOT ONE in the spaced spelling — the hole #17
# named and left open. `park Atlas --lane --dry-run` ran a REAL park with the
# lane set to `--dry-run`: the flag a person typed was swallowed as a value,
# `--dry-run` itself never took effect, and the only sign was a WIP commit
# subject labelled with it. Nothing here swallows the next word whatever it
# is, so that is a refusal.
# `--flag=value` IS THE OTHER HALF, and the escape that refusing `-*` would
# otherwise foreclose: a value that really does begin with `-` is spelled
# `--lane=--weird`, which takes whatever follows the FIRST `=` and asks
# nothing about it. An `=` with nothing after it is an empty value and is
# refused as #17 refuses one.
# BYTE-IDENTICAL IN `park`, `resume` AND `status`, held by
# `tests/test_repo_hygiene.py` the way the estate resolver and `expand_home`
# are: three files a person has on PATH, and one answer to what a flag's value
# is.
flag_value() {
    FLAG_VALUE=""
    FLAG_SHIFT=2
    FLAG_HINT=""
    case "$1" in
    *=*)
        FLAG_SHIFT=1
        FLAG_VALUE="${1#*=}"
        [ -n "$FLAG_VALUE" ] || return 1
        return 0 ;;
    esac
    [ $# -ge 2 ] && [ -n "$2" ] || return 1
    # THE NEXT ARGUMENT, ONLY IF IT CANNOT BE A FLAG. The hint names the `=`
    # spelling because that is the whole exit: this refuses nothing a person
    # cannot then say.
    case "$2" in
    -*)
        FLAG_HINT="
    \`$2\` begins with a \`-\`, so it is a flag and not this one's value:
    nothing here swallows the next word whatever it is. A value that really
    does begin with \`-\` is given after an \`=\`:

      $1=$2"
        return 1 ;;
    esac
    FLAG_VALUE="$2"
}

while [ $# -gt 0 ]; do
    case "$1" in
    -h | --help) usage; exit 0 ;;
    --dry-run) DRY_RUN=1; shift ;;
    # THE SWEEP, ASKED FOR: every estate under the projects directory, from
    # anywhere, with no question (the RULING of 2026-09-10 at the sweep's
    # section head: "park all should be park -a or park --all").
    -a | --all) PARK_ALL=1; shift ;;
    # A MISSING, EMPTY OR FLAG-SHAPED VALUE IS THIS COMMAND'S REFUSAL, exit 2,
    # in its own words: `${2:?…}` would be bash's message and bash's exit 1,
    # which is not a status this command means anything by (`status` gave the
    # reason first: its 1 is "findings"; here, a typo must not read like `make
    # park`'s own exit).
    # EMPTY IS REFUSED THE SAME WAY, and so is a flag where a value belongs:
    # `flag_value` above asks all three questions once for every arm below,
    # and each arm keeps its own words. `${2:?…}` rejected
    # unset AND null; `[ $# -ge 2 ]` alone rejects only the first, and what it
    # lets through is read later as "was this given at all?" — so
    # `park --lane ""` would carry on with `LANE` empty, the lane is then left
    # out of the `make park` invocation, and the WIP commit subject a person
    # asked to be labelled comes back unlabelled. `park --repo ""` is the same
    # silence with more behind it: an empty slug falls past the `--repo` branch
    # of the resolver into the walk-up, which is the one thing naming a clone's
    # origin was there to prevent. A value that came out empty is a flag that
    # was typed and lost, never one that was never typed, and the two must not
    # do different things (Copilot's review of #14).
    --lane | --lane=*)
        flag_value "$@" ||
            die "--lane needs a value: the lane recorded in the WIP commit subject.$FLAG_HINT"
        LANE="$FLAG_VALUE"; shift "$FLAG_SHIFT" ;;
    --repo | --repo=*)
        flag_value "$@" ||
            die "--repo needs a value: <owner>/<name>, or any url spelling of one.$FLAG_HINT"
        REPO_SLUG="$FLAG_VALUE"; REPO_ARG="$REPO_SLUG"; shift "$FLAG_SHIFT" ;;
    # `--project <Name>` and `--name <Name>` are the positional under a flag,
    # because the issue's own words are "park --project <project> or park
    # --repo <repo> and it defaults to project" (Brett Heap, 2026-09-09). The
    # value is never mistaken for the positional this way, which is the same
    # reason `openRepoShape` names its value-taking flags.
    --name | --project | --name=* | --project=*)
        flag_value "$@" ||
            die "${1%%=*} needs a value: one estate folder name under your projects directory.$FLAG_HINT"
        NAME="$FLAG_VALUE"; shift "$FLAG_SHIFT" ;;
    # EVERYTHING AFTER `--` IS THE EXTENSION'S, verbatim. `park.sh` has flags
    # this command has no opinion about (--feature, --message, --no-push,
    # --retire-parked-wip) and a command that ate them would be a command
    # people have to stop using the moment the extension grows one.
    --) shift; PASSTHROUGH+=("$@"); break ;;
    -*)
        usage >&2
        die "$SELF_NAME does not know $1. Its own flags are listed above; the
    extension's own flags travel after a \`--\`, so what you want is:

      $SELF_NAME -- $1"
        ;;
    *)
        [ -z "$NAME" ] ||
            die "two estate names, '$NAME' and '$1'. One positional <Name>, then flags."
        NAME="$1"; shift ;;
    esac
done

[ -z "$NAME" ] || [ -z "$REPO_SLUG" ] ||
    die "both <Name> ('$NAME') and --repo '$REPO_SLUG'. One estate per run: name it
    either way, not both."
if [ "$PARK_ALL" -eq 1 ]; then
    [ -z "$NAME" ] ||
        die "both <Name> ('$NAME') and --all. \`--all\` parks EVERY estate under your
    projects directory; <Name> parks one. Drop one of them."
    [ -z "$REPO_SLUG" ] ||
        die "both --repo '$REPO_ARG' and --all. \`--all\` parks EVERY estate under your
    projects directory; \`--repo\` parks the one that clone belongs to. Drop one
    of them."
fi

# --- resolve ---------------------------------------------------------------
if [ "$PARK_ALL" -eq 1 ]; then
    # `--all`: nothing to resolve, and nothing to ask. The sweep discovers for
    # itself, and it runs from ANYWHERE - standing inside an estate does not
    # narrow a flag that says every one of them.
    ALL_ROWS="$(all_estate_dirs)"
    [ -n "$ALL_ROWS" ] ||
        die "--all found no estate under ${PROJECTS_DIRS[*]}, so nothing was parked.
    An estate is a FAMILY folder (<Name>/<Name>/family.yaml) or a standalone
    project root (<Name>/project.yaml), one level under your projects
    directory; \`resume <Name>\` brings one back."
    require_make
    say "park --all: parking every estate under ${PROJECTS_DIRS[*]}:"
    say ""
    say "$(estates_or_none)"
    say ""
elif [ -n "$REPO_SLUG" ]; then
    # ANY SPELLING A PERSON MIGHT PASTE, through the same reader that reads a
    # clone's `origin`: `TestOrg/Atlas`, `TestOrg/Atlas.git`, a trailing
    # slash, or the whole https url they copied out of the browser. Both
    # sides of the comparison then come from one parser, and the refusal
    # below can print a clone line that is a url rather than a url wrapped
    # around another one.
    REPO_SLUG="$(repo_slug_of "$REPO_SLUG" || true)"
    [ -n "$REPO_SLUG" ] || die "--repo '$REPO_ARG' is not a repository: it wants <owner>/<name>, or any
    url spelling of one (https://…/<owner>/<name>.git, git@host:<owner>/<name>).
    A family's members may live in another organisation, so there is no owner
    to assume, and a path on disk has none to read."
    MATCH="$(clone_with_origin "$REPO_SLUG" || true)"
    [ -n "$MATCH" ] || die "no clone under your projects directory has origin $REPO_SLUG.
    \`--repo\` names a clone you ALREADY HAVE; it never clones one (RULING
    2026-09-09 ruling 1). Get it, or bring the whole estate back from its
    record:

      git clone --recurse-submodules https://github.com/$REPO_SLUG.git
      resume <Name>          # rebuilds the estate from your workspace record

estates found under your projects directory:
$(estates_or_none)"
    estate_walk_up "$MATCH" ||
        die "$MATCH is a clone of $REPO_SLUG, but nothing at or above it is an
    estate: no \`family.yaml\` holder beside it and no \`project.yaml\` root. It
    is a clone, not a project of this shape - \`park\` has no verb to run in it."
elif [ -n "$NAME" ]; then
    # A FOLDER NAME, NEVER A PATH. `park ../x` or `park /abs` would otherwise
    # be joined to the projects directory and land somewhere nobody named.
    is_safe_folder_name "$NAME" ||
        die "'$NAME' is not an estate name: it is a path, or it carries a
    character an estate folder may not. <Name> is ONE folder name under your
    projects directory — no slash, no leading dot. To act on a checkout
    somewhere else, stand in it and run \`$SELF_NAME\` with no <Name>."
    estate_by_name "$NAME" || die "no estate named '$NAME' under your projects directory.
    An estate is a FAMILY folder ($NAME/$NAME/family.yaml) or a standalone
    project root ($NAME/project.yaml). Bring it back with \`resume $NAME\`, or
    park one of these:

$(estates_or_none)"
else
    # THE WALK-UP WINS FIRST: standing inside an estate parks THAT estate,
    # never the whole workstation — "it parks the current repo" (the RULING
    # of 2026-09-10 quoted at the sweep's section head). With no estate
    # around the cwd the estates are listed and the question is asked;
    # `--all` is the way past the question, not a folder to stand in.
    estate_walk_up "$PWD" || ask_before_parking_every_estate
fi

if [ "$PARK_ALL" -eq 1 ]; then
    STATUS=0
    park_every_estate "$ALL_ROWS" || STATUS=$?
    exit "$STATUS"
fi

require_make

STATUS=0
run_one_estate || STATUS=$?
exit "$STATUS"
