#!/usr/bin/env bash
# before — give the attempt a git worktree on its own ticket branch.
#
# ENVIRONMENT:
#   TICKET_ID        four digits; the ticket's id as tasks reported it
#   TICKET_REF       the repo-relative ticket path; fresh build preparation
#                    reads it from origin/main to recognize canonical adoption
#   TICKET_FLOW      the procedure that will run; "quickfix" is exempt from
#                    the green-main gate, because repairing a red main is
#                    the whole job
#   WORKTREE_ROOT    where worktrees live (default $HOME/workspace/worktrees)
#   You start in the repository's registered directory; pwd is the repo.
#
# WHAT IT DOES
#   1. fetch --prune, because everything below reads origin/main
#   2. worktree at $WORKTREE_ROOT/<owner>/<repo>/<TICKET_ID> on branch
#      ticket/<TICKET_ID>, branched from origin/main and NEVER from local
#      main — a local main may be stale, dirty, or different per machine
#   3. merge origin/main into a ticket branch that does not already contain
#      it. A merge conflict is held for human recovery with its worktree
#      intact; nothing prepares or gates that stale tree.
#   4. sdlc/scripts/install in that tree, if the repo has one
#   5. the green-main gate: sdlc/scripts/lint then sdlc/scripts/test in that
#      tree. An empty regular marker named for the current origin/main and
#      younger than 24 hours skips all three commands. The gate otherwise
#      skips for the quickfix flow and
#      for a ticket branch carrying commits of its own — a continued branch is
#      mid-work (a red-first test is its normal state) and its redness says
#      nothing about origin/main; the landing gate protects main instead.
#   Steps 4 and 5 undo the tree and branch when they fail, so a refused
#   before leaves nothing behind — but never a branch holding commits
#   origin/main lacks; destroying a prior attempt's work is worse than
#   leaving a tree for the retry to continue.
#
# STDOUT:
#   dir: <absolute worktree path>   the attempt's directory
#   SDLC_REPO=<absolute repo path>  baggage: where the checkout is, for
#                                   success and failure when the worktree is
#                                   gone and pwd can no longer say
#   SDLC_BRANCH=ticket/<TICKET_ID>  baggage: the branch this attempt owns
# EXIT:    0 the tree is ready
#          3 origin/main is red, or a live bot run holds this ticket's tree:
#            the channel backs off and a quickfix or the other run must
#            resolve it before anything else can fly
#          2 a merge could not complete; refuse this ticket and retain its
#            worktree and in-progress merge for human recovery
#          any other nonzero: a retryable fault (fetch failed, install
#            failed, a missing gate, the path is occupied by something that
#            is not ours).
#
# There is no origin claim branch and no claim commit. The old prepare
# pushed one as a cross-machine lock and called collisions a claim status;
# the factory's lease is that lock now, so a branch that already exists is
# this ticket's own work to continue, not evidence of a rival.
if ((BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 2))); then
	printf 'Bash %s is unsupported; Bash 3.2 or newer is required\n' "$BASH_VERSION" >&2
	exit 2
fi

set -euo pipefail

git_with_optional_timeout() {
	if command -v timeout >/dev/null 2>&1; then
		timeout -k 10 60 git "$@"
	else
		git "$@"
	fi
}

# stdout is reserved for the protocol lines. Everything a git or a gate
# prints is chatter and belongs on stderr.
exec 3>&1 1>&2

: "${TICKET_ID:?before: TICKET_ID is required}"
case "$TICKET_ID" in
[0-9][0-9][0-9][0-9]) ;;
*)
	echo "before: TICKET_ID must be exactly four digits" >&2
	exit 1
	;;
esac

REPO=$(pwd -P)
BRANCH="ticket/$TICKET_ID"
WORKTREES=${WORKTREE_ROOT:-$HOME/workspace/worktrees}

origin_namespace() {
	local origin namespace
	origin=$(git -C "$REPO" remote get-url origin 2>/dev/null) || return 1
	namespace=$(printf '%s\n' "$origin" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/\.git$//; s/:/\//g' | awk -F/ '{ for (i = 1; i <= NF; i++) if ($i != "") part[++n] = $i } END { if (n >= 2) print part[n - 1] "/" part[n] }')
	# The namespace becomes a path below WORKTREES; traversal components are
	# not identities.
	case "$namespace" in ./* | ../* | */. | */..) return 1 ;; esac
	printf '%s\n' "$namespace"
}

if ! NAMESPACE=$(origin_namespace) || [ -z "$NAMESPACE" ]; then
	echo "before: cannot derive owner/repository from origin" >&2
	exit 1
fi
if ! common=$(git -C "$REPO" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) ||
	! REPO_COMMON=$(CDPATH='' cd -- "$common" && pwd -P); then
	echo "before: cannot determine the Git common directory for $REPO" >&2
	exit 1
fi
TREE="$WORKTREES/$NAMESPACE/$TICKET_ID"

# A candidate is ours only if Git identifies that path itself as a worktree
# top-level and both trees share this repository's common directory.
candidate_owned() {
	local candidate=$1 top candidate_path top_path candidate_common
	top=$(git -C "$candidate" rev-parse --show-toplevel 2>/dev/null) || return 1
	candidate_path=$(CDPATH='' cd -- "$candidate" && pwd -P) || return 1
	top_path=$(CDPATH='' cd -- "$top" && pwd -P) || return 1
	[ "$candidate_path" = "$top_path" ] || return 1
	candidate_common=$(git -C "$candidate" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 1
	candidate_common=$(CDPATH='' cd -- "$candidate_common" && pwd -P) || return 1
	[ "$candidate_common" = "$REPO_COMMON" ]
}

lexical_path() {
	local path=$1 part
	local -a parts=() source=()
	case "$path" in
	/*) ;;
	*) return 1 ;;
	esac
	IFS=/ read -r -a source <<< "$path"
	for part in "${source[@]}"; do
		case "$part" in
		'' | .) ;;
		..)
			if [ "${#parts[@]}" -gt 0 ]; then
				unset 'parts[${#parts[@]} - 1]'
			fi
			;;
		*) parts+=("$part") ;;
		esac
	done
	local IFS=/
	printf '/%s\n' "${parts[*]}"
}

orphan_owned() {
	local candidate=$1 candidate_path pointer normalized extra
	[ -d "$candidate" ] || return 1
	[ -f "$candidate/.git" ] && [ ! -L "$candidate/.git" ] || return 1
	{
		IFS= read -r pointer || [ -n "$pointer" ] || return 1
		if IFS= read -r extra || [ -n "$extra" ]; then
			return 1
		fi
	} <"$candidate/.git" || return 1
	case "$pointer" in
	"gitdir: "?*) pointer=${pointer#gitdir: } ;;
	*) return 1 ;;
	esac
	candidate_path=$(CDPATH='' cd -- "$candidate" && pwd -P) || return 1
	case "$pointer" in
	/*) normalized=$(lexical_path "$pointer") ;;
	*) normalized=$(lexical_path "$candidate_path/$pointer") ;;
	esac
	case "$normalized" in
	"$REPO_COMMON"/*) return 0 ;;
	*) return 1 ;;
	esac
}

owned_worktree() {
	candidate_owned "$1" || orphan_owned "$1"
}

# Git is the normal remover. Recursive removal is only for its known stale
# metadata case, after ownership has been proved again at the point of use.
remove_tree() {
	local candidate=$1
	if git -C "$REPO" worktree remove --force "$candidate" >/dev/null 2>&1; then
		return 0
	fi
	[ -e "$candidate" ] || return 0
	if ! owned_worktree "$candidate"; then
		echo "before: refusing to remove $candidate: it is not a worktree owned by $REPO" >&2
		return 1
	fi
	if ! rm -rf "$candidate"; then
		echo "before: could not remove worktree $candidate" >&2
		return 1
	fi
}

# Bot answers 0 while a live run holds its worktree, 1 when it is idle; any
# other result is not proof that cleanup is safe.
worktree_liveness() {
	bot busy "$1"
}

# Remove the tree and the local branch. Called on every refusal below, so a
# refused before leaves nothing behind on this machine — except a branch
# with its own commits, which is a prior attempt's work and never ours to
# destroy.
undo() {
	if git -C "$REPO" show-ref --verify --quiet "refs/heads/$BRANCH" &&
		[ "$(git -C "$REPO" rev-list --count "origin/main..$BRANCH" 2>/dev/null || echo 1)" -gt 0 ]; then
		echo "before: refusing to undo $BRANCH: it holds commits origin/main does not" >&2
		return 1
	fi

	if [ -e "$TREE" ]; then
		local liveness
		if worktree_liveness "$TREE"; then
			echo "before: refusing to undo $BRANCH: a live run holds worktree $TREE" >&2
			return 3
		else
			liveness=$?
		fi
		case "$liveness" in
		1) ;;
		127)
			echo "before: refusing to undo $BRANCH: bot is not installed or not on PATH" >&2
			return 3
			;;
		*)
			echo "before: refusing to undo $BRANCH: could not establish liveness for worktree $TREE" >&2
			return 3
			;;
		esac

		if ! owned_worktree "$TREE"; then
			echo "before: refusing to remove $TREE: it is not a worktree owned by $REPO" >&2
			return 1
		fi
		remove_tree "$TREE" || return 1
	fi
	git -C "$REPO" worktree prune >/dev/null 2>&1 || return 1
	if git -C "$REPO" show-ref --verify --quiet "refs/heads/$BRANCH"; then
		git -C "$REPO" branch -D "$BRANCH" >/dev/null 2>&1 || return 1
	fi
}

# 1. Fetch. Everything below reads origin/main, so it has to be current.
if ! git_with_optional_timeout -C "$REPO" fetch --quiet --prune origin; then
	echo "before: git fetch origin failed" >&2
	exit 1
fi
if ! GREEN_MAIN_SHA=$(git -C "$REPO" rev-parse origin/main); then
	echo "before: could not resolve origin/main" >&2
	exit 1
fi

# 2. The tree. Three shapes: continue an existing tree, put a tree back on an
# existing branch, or make both fresh from origin/main.
tree_branch() {
	git -C "$TREE" symbolic-ref --quiet --short HEAD 2>/dev/null
}

if [ -e "$TREE" ]; then
	if candidate_owned "$TREE"; then
		if [ "$(tree_branch || true)" != "$BRANCH" ]; then
			echo "before: $TREE is checked out on $(tree_branch || echo 'a detached HEAD'), not $BRANCH" >&2
			exit 1
		fi
		if git -C "$REPO" show-ref --verify --quiet "refs/heads/$BRANCH"; then
			echo "before: continuing the existing worktree $TREE on $BRANCH" >&2
		else
			cleanup_status=0
			undo || cleanup_status=$?
			if [ "$cleanup_status" -ne 0 ]; then
				echo "before: cleanup after $BRANCH was gone failed" >&2
				exit "$cleanup_status"
			fi
			echo "before: discarded worktree $TREE because its branch $BRANCH was gone" >&2
		fi
	elif orphan_owned "$TREE"; then
		cleanup_status=0
		undo || cleanup_status=$?
		if [ "$cleanup_status" -ne 0 ]; then
			echo "before: cleanup after unregistered worktree $TREE failed" >&2
			exit "$cleanup_status"
		fi
		echo "before: reclaimed unregistered worktree $TREE" >&2
	else
		echo "before: $TREE exists and is not a worktree owned by $REPO — resolve it by hand" >&2
		exit 1
	fi
fi

if [ ! -e "$TREE" ] && git -C "$REPO" show-ref --verify --quiet "refs/heads/$BRANCH"; then
	# The branch survived a previous attempt; its commits are this ticket's
	# own work, so put a tree back on it rather than throwing the work away.
	mkdir -p "$WORKTREES/$NAMESPACE"
	if ! git -C "$REPO" worktree add --quiet "$TREE" "$BRANCH"; then
		echo "before: $BRANCH exists but a worktree for it could not be made at $TREE" >&2
		exit 1
	fi
	echo "before: reattached $BRANCH to a new worktree at $TREE" >&2
elif [ ! -e "$TREE" ]; then
	mkdir -p "$WORKTREES/$NAMESPACE"
	if ! git -C "$REPO" worktree add --quiet -b "$BRANCH" "$TREE" origin/main; then
		echo "before: could not create worktree $TREE on $BRANCH" >&2
		exit 1
	fi
fi

# Retire the managed traceability hook without touching an unrelated hook.
# A managed hook may have displaced the prior hook into the saved path, so
# restore that hook when it exists and otherwise remove only the managed one.
retire_commit_msg_hook() {
	local hooks hook original grep_status
	if ! hooks=$(git -C "$TREE" rev-parse --path-format=absolute --git-path hooks); then
		echo "before: could not determine the Git hooks directory for $REPO" >&2
		return 1
	fi

	hook="$hooks/commit-msg"
	original="$hooks/commit-msg.sdlc-original"
	if [ -e "$hook" ] || [ -L "$hook" ]; then
		if [ ! -L "$hook" ] && grep -Fqx '# sdlc-managed-traceability-commit-msg-hook' "$hook"; then
			if [ -e "$original" ] || [ -L "$original" ]; then
				if ! mv "$original" "$hook"; then
					echo "before: could not restore the saved commit-msg hook" >&2
					return 1
				fi
			elif ! rm -f "$hook"; then
				echo "before: could not remove the managed traceability commit-msg hook" >&2
				return 1
			fi
		else
			grep_status=$?
			if [ "$grep_status" -gt 1 ]; then
				echo "before: could not inspect the commit-msg hook" >&2
				return 1
			fi
		fi
	elif [ -e "$original" ] || [ -L "$original" ]; then
		if ! mv "$original" "$hook"; then
			echo "before: could not restore the saved commit-msg hook" >&2
			return 1
		fi
	fi
}

if ! retire_commit_msg_hook; then
	cleanup_status=0
	undo || cleanup_status=$?
	if [ "$cleanup_status" -eq 3 ]; then
		exit 3
	fi
	if [ "$cleanup_status" -ne 0 ]; then
		echo "before: cleanup after traceability hook migration failed" >&2
	fi
	echo "before: could not retire the traceability commit-msg hook" >&2
	exit 1
fi

# 3. Continue from current main before this attempt reads the ticket or runs
# anything project-specific. A failed merge remains in the worktree for the
# person who can resolve it; Git's merge diagnostic stays on stderr.
merge_main() {
	local paths
	if git -C "$TREE" merge origin/main; then
		return 0
	fi

	echo "before: could not merge origin/main into $BRANCH; holding the worktree for human recovery" >&2
	if paths=$(git -C "$TREE" diff --name-only --diff-filter=U); then
		if [ -n "$paths" ]; then
			echo "before: conflicting paths:" >&2
			printf '%s\n' "$paths" >&2
		fi
	else
		echo "before: could not list unmerged paths" >&2
	fi
	return 2
}

if git -C "$TREE" merge-base --is-ancestor origin/main "$BRANCH" >/dev/null 2>&1; then
	:
else
	ancestor_status=$?
	if [ "$ancestor_status" -ne 1 ]; then
		echo "before: could not determine whether $BRANCH contains origin/main" >&2
		exit 1
	fi
	merge_main || exit $?
fi

# How far the branch is ahead of origin/main. Zero means the tree's content
# is main's content, so the gates below truly measure main; nonzero means
# this is a continuation of the ticket's own work.
AHEAD=$(git -C "$TREE" rev-list --count "origin/main..$BRANCH" 2>/dev/null || echo 0)

# Deployment declares canonical lifecycle adoption through stable fields. Ignore
# prose and layout, but require the complete identity and target declarations.
# An unreadable or malformed ref is an ordinary build ticket, never an
# exemption or a fault.
canonical_adoption_ticket() {
	local ticket
	[ "${TICKET_FLOW-}" = build ] && [ "$AHEAD" -eq 0 ] || return 1
	case "${TICKET_REF-}" in
	"sdlc/tickets/$TICKET_ID-"*.md) ;;
	*) return 1 ;;
	esac
	case "$TICKET_REF" in *[!A-Za-z0-9_./-]* | *..*) return 1 ;; esac
	ticket=$(mktemp) || return 1
	if ! git -C "$REPO" cat-file blob "origin/main:$TICKET_REF" >"$ticket" 2>/dev/null ||
		[ "$(tail -c 1 "$ticket" | od -An -tu1 | tr -d ' ')" != 10 ]; then
		rm -f "$ticket"
		return 1
	fi
	awk '
		function hash(value) { return length(value) == 64 && value !~ /[^0-9a-f]/ }
		function source(value, parts) {
			if (value !~ /^sdlc\/tickets\/[0-9][0-9][0-9][0-9]-[A-Za-z0-9._-]+\.md@[0-9a-f]+$/) return 0
			split(value, parts, "@")
			return length(parts[2]) == 40
		}
		function finish_target() {
			if (current != "" && (hashes != 1 || !target_hash_valid)) valid = 0
			current = ""
			hashes = 0
			target_hash_valid = 0
		}
		BEGIN { valid = 1; frontmatter = 0 }
		NR == 1 {
			if ($0 != "---") valid = 0
			else frontmatter = 1
			next
		}
		{
			if (frontmatter) {
				if ($0 == "---") frontmatter = 0
				else if ($0 ~ /^[ \t]*flow[ \t]*:/) {
					value = $0
					sub(/^[ \t]*flow[ \t]*:[ \t]*/, "", value)
					sub(/[ \t]*$/, "", value)
					flow_fields++
					if (value != "build") valid = 0
				}
			}
			if ($0 ~ /^[ \t]*Adoption identity[ \t]*:/) {
				value = $0
				sub(/^[ \t]*Adoption identity[ \t]*:[ \t]*/, "", value)
				sub(/[ \t]*$/, "", value)
				identities++
				if (!source(value)) valid = 0
			}
			if ($0 ~ /^[ \t]*-[ \t]+path[ \t]*:/) {
				finish_target()
				path = $0
				sub(/^[ \t]*-[ \t]+path[ \t]*:[ \t]*/, "", path)
				sub(/[ \t]*$/, "", path)
				if (path ~ /^sdlc\/project\/(tasks|before|success|failure|health)$/ ||
				    path == "sdlc/project/provenance.json") {
					current = path
					if (seen[path]++) valid = 0
					if (path == "sdlc/project/provenance.json") provenance++
					else lifecycle++
				}
				next
			}
			if (current != "" && $0 ~ /^[ \t]*sha256[ \t]*:/) {
				value = $0
				sub(/^[ \t]*sha256[ \t]*:[ \t]*/, "", value)
				sub(/[ \t]*$/, "", value)
				hashes++
				if (hash(value)) target_hash_valid = 1
				else valid = 0
			}
		}
		END {
			finish_target()
			exit !(valid && !frontmatter && flow_fields == 1 && identities == 1 &&
			       lifecycle > 0 && provenance == 1)
		}
	' "$ticket"
	local status=$?
	rm -f "$ticket"
	return "$status"
}

CANONICAL_ADOPTION=0
if canonical_adoption_ticket; then
	CANONICAL_ADOPTION=1
fi

# A green verdict belongs to the fetched main commit. Snapshot trust before
# any project-owned script runs, so install cannot create a marker and skip
# gates. Only an empty, non-symlink regular file is trusted.
GREEN_MAIN_MARKER=
GREEN_MAIN_VERDICT_TRUSTED=0
GREEN_MAIN_NEEDS_PROOF=0
GATE_OUTPUT=
MARKER_TEMP=
trap 'rm -f "${GATE_OUTPUT-}" "${MARKER_TEMP-}"' EXIT

if [ "${TICKET_FLOW-}" != "quickfix" ] && [ "$AHEAD" -eq 0 ]; then
	GREEN_MAIN_MARKER="$WORKTREES/$NAMESPACE/.green-main/$GREEN_MAIN_SHA"
	if [ -f "$GREEN_MAIN_MARKER" ] && [ ! -L "$GREEN_MAIN_MARKER" ] && [ ! -s "$GREEN_MAIN_MARKER" ]; then
		marker_now=$(date +%s 2>/dev/null || true)
		marker_mtime=$(stat -c %Y "$GREEN_MAIN_MARKER" 2>/dev/null || stat -f %m "$GREEN_MAIN_MARKER" 2>/dev/null || true)
		case "$marker_now:$marker_mtime" in
		*[!0-9:]* | :* | *:) ;;
		*)
			marker_age=$((marker_now - marker_mtime))
			if [ "$marker_age" -ge 0 ] && [ "$marker_age" -lt 86400 ]; then
				GREEN_MAIN_VERDICT_TRUSTED=1
				echo "before: trusting green-main verdict for $GREEN_MAIN_SHA; age ${marker_age}s" >&2
			fi
			;;
		esac
	fi
	if [ "$GREEN_MAIN_VERDICT_TRUSTED" -eq 0 ]; then
		GREEN_MAIN_NEEDS_PROOF=1
	fi
fi

# Project-owned setup and gates must not leave an unproven verdict behind.
clear_unproven_marker() {
	[ "$GREEN_MAIN_NEEDS_PROOF" -eq 1 ] || return 0
	if [ -e "$GREEN_MAIN_MARKER" ] || [ -L "$GREEN_MAIN_MARKER" ]; then
		rm -f "$GREEN_MAIN_MARKER"
	fi
}

# 4. Project preparation is optional, unlike the green-main gate. An existing
# non-executable script is a fault rather than an absent install.
install() {
	local script="$TREE/sdlc/scripts/install"
	[ -e "$script" ] || return 0
	if [ ! -x "$script" ]; then
		echo "before: $REPO has no executable sdlc/scripts/install" >&2
		return 2
	fi
	(cd "$TREE" && SDLC_REPO="$REPO" "$script") || return 1
}

if [ "$GREEN_MAIN_VERDICT_TRUSTED" -eq 0 ]; then
	status=0
	install || status=$?
	if [ "$status" -ne 0 ]; then
		clear_unproven_marker || echo "before: could not clear unproven green-main verdict" >&2
		cleanup_status=0
		undo || cleanup_status=$?
		if [ "$cleanup_status" -eq 3 ]; then
			exit 3
		fi
		if [ "$cleanup_status" -ne 0 ]; then
			echo "before: cleanup after a failed install failed" >&2
		fi
		if [ "$status" -ne 2 ]; then
			echo "before: sdlc/scripts/install failed" >&2
		fi
		exit 1
	fi
fi

# 5. Green check, in that tree, using this project's own gate ladder.
emit_gate_verdict() {
	check=$1
	capture=$2
	suffix=${3-}
	header="$check: failed; unparsed$suffix"
	limit=$((1024 - $(printf '%s\n' "$header" | wc -c)))
	failure_lines=$(mktemp) || return 1
	grep_status=0
	LC_ALL=C grep -a '^not ok ' "$capture" >"$failure_lines" || grep_status=$?
	if [ "$grep_status" -eq 1 ]; then
		rm -f "$failure_lines"
		printf '%s\n' "$header" >&2
		tail -c "$limit" "$capture" | iconv -f UTF-8 -t UTF-8 -c >&2
		return
	fi
	if [ "$grep_status" -ne 0 ]; then
		rm -f "$failure_lines"
		return 1
	fi
	failure_bytes=$(wc -c <"$failure_lines")
	if [ "$failure_bytes" -gt "$limit" ]; then
		failure_bytes=$limit
	fi
	tail_bytes=$((limit - failure_bytes))
	printf '%s\n' "$header" >&2
	head -c "$failure_bytes" "$failure_lines" | iconv -f UTF-8 -t UTF-8 -c >&2
	rm -f "$failure_lines"
	tail -c "$tail_bytes" "$capture" | iconv -f UTF-8 -t UTF-8 -c >&2
}

gate() {
	local script="$TREE/sdlc/scripts/$1" status=0
	if [ ! -x "$script" ]; then
		echo "before: $REPO has no executable sdlc/scripts/$1" >&2
		return 2
	fi
	(cd "$TREE" && SDLC_REPO="$REPO" "$script") >"$GATE_OUTPUT" 2>&1 || status=$?
	return "$status"
}

if [ "${TICKET_FLOW-}" != "quickfix" ] && [ "$AHEAD" -gt 0 ]; then
	echo "before: skipping the green-main gate: $BRANCH carries $AHEAD commit(s) of its own work, so this tree is not origin/main" >&2
fi
if [ "$GREEN_MAIN_NEEDS_PROOF" -eq 1 ]; then
	GATE_OUTPUT=$(mktemp) || {
		echo "before: could not capture green-main gate output" >&2
		exit 1
	}
	for g in lint test; do
		: >"$GATE_OUTPUT"
		status=0
		gate "$g" || status=$?
		cat "$GATE_OUTPUT" >&2
		if [ "$status" -eq 0 ]; then
			continue
		fi
		clear_unproven_marker || echo "before: could not clear unproven green-main verdict" >&2
		# Missing, unstartable, and signal-killed gates are faults, not red
		# main, and adoption does not exempt them.
		gate_fault=0
		case "$status" in
		2 | 126 | 127) gate_fault=1 ;;
		esac
		if [ "$status" -gt 128 ]; then
			gate_fault=1
		fi
		if [ "$gate_fault" -eq 0 ] && [ "$CANONICAL_ADOPTION" -eq 1 ]; then
			emit_gate_verdict "$g" "$GATE_OUTPUT" "; origin/main $GREEN_MAIN_SHA"
			echo "before: canonical lifecycle adoption is proceeding despite failing gate sdlc/scripts/$g" >&2
			ADOPTION_RED=1
			break
		fi
		cleanup_status=0
		undo || cleanup_status=$?
		if [ "$cleanup_status" -eq 3 ]; then
			exit 3
		fi
		if [ "$cleanup_status" -ne 0 ]; then
			echo "before: cleanup after the failed $g gate failed" >&2
			exit 1
		fi
		if [ "$gate_fault" -eq 1 ]; then
			exit 1
		fi
		echo "before: origin/main is not green — sdlc/scripts/$g failed; a quickfix has to land first" >&2
		emit_gate_verdict "$g" "$GATE_OUTPUT" "; origin/main $GREEN_MAIN_SHA"
		exit 3
	done

	if [ "${ADOPTION_RED-0}" -eq 1 ]; then
		:
	else
		# The filename is the commit key. Publish an empty private file over
		# stale state only after install and every gate succeed.
		marker_dir=$(dirname "$GREEN_MAIN_MARKER")
		if ! mkdir -p "$marker_dir" ||
			! MARKER_TEMP=$(mktemp "$marker_dir/.green-main.XXXXXX") ||
			! mv -fT -- "$MARKER_TEMP" "$GREEN_MAIN_MARKER"; then
			rm -f "${MARKER_TEMP-}"
			MARKER_TEMP=
			clear_unproven_marker || true
			echo "before: could not store green-main verdict for $GREEN_MAIN_SHA" >&2
			exit 1
		fi
		MARKER_TEMP=
		if [ ! -f "$GREEN_MAIN_MARKER" ] || [ -L "$GREEN_MAIN_MARKER" ] || [ -s "$GREEN_MAIN_MARKER" ]; then
			clear_unproven_marker || true
			echo "before: could not store green-main verdict for $GREEN_MAIN_SHA" >&2
			exit 1
		fi
	fi
fi

# 6. The place, and the two facts settlement needs when the place is gone.
printf 'dir: %s\n' "$TREE" >&3
printf 'SDLC_REPO=%s\n' "$REPO" >&3
printf 'SDLC_BRANCH=%s\n' "$BRANCH" >&3
