From e9ca589997b042bb38572975c812fda8806be123 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 21 Sep 2026 07:38:07 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(release):=20the=20train's=20identity=20?= =?UTF-8?q?is=20DERIVED=20=E2=80=94=20release=20scripts=20carried=200.68.2?= =?UTF-8?q?'s=20V/MS/EPIC/LAST=5FTAG=20and=20an=20out-of-repo=20AP;=20a=20?= =?UTF-8?q?guard=20now=20refuses=20both=20(#3618)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #3618 #3592 #3459 #3617 The release scripts ported from the out-of-tree 0.68.2 bundle (#3599) kept V=0.68.2, MS=12, EPIC=3477, LAST_TAG=v0.68.1, and AP pointing at an agent worktree on the operator box's RAID mount. Cutting 0.69 through them would have targeted the old version and a directory that may not exist, and the path passed every gate: check_hardcoded_paths.sh does not match that mount (#3592). scripts/release/lib_release_params.sh (new, sourced, option-neutral) ONE input, the version; everything else read from where it already lives. - release_params : V, T, REPO, INFRA; a non-X.Y.Z version is rc 2 (0.69, v0.69.0, 0.69.0.1, 1.2.3a, empty -- all refused). - AP =
/target/release-train/, where
is the parent of the repo's COMMON git dir, so every worktree resolves the SAME AP (prepare_bump writes release_notes.md there, autopilot reads it). target/ is ignored. RELEASE_AP overrides at run time; it is never a literal. - release_milestone_number: the one milestone titled exactly V. - release_epic_number: the one `epic` item in milestone V titled "EPIC: release train V ..." (RELEASE_EPIC overrides, digits only). - release_last_tag: the highest v* tag strictly below T. Each derivation that finds 0 or 2+ answers returns 2 and says why. Proof against history: for 0.68.2 the derivations return milestone 12, epic #3477, last tag v0.68.1 -- exactly the literals they replace. For 0.69.0: milestone 6, epic #3080, last tag v0.68.2. The four scripts take the version as their first argument: autopilot.sh [from] [to] (MS/EPIC derived once, logged as `PARAMS …` in STATUS; ledger.py read from the tree, not $AP -- it would not have existed in the new AP) prepare_bump.sh [--ship] (branch release-, Pmat-Ticket from the derived epic, the /mnt fallback for arm_pr_automerge.sh removed -- the in-tree script is required; launch line prints the in-tree autopilot) publish_strict.sh [--plan] (publish-order.txt read from the tag's own checkout, $WT/scripts/release/, not the old bundle dir) t2_preflight.sh | --self-test (self-test needs no version) ledger_from_run.sh's "tag": "v0.68.1" is now the v* tag the tested sha carries. Each refuses with a usage line (rc 2) when the version is missing. scripts/check_release_scripts_derive_identity.sh (new; guard_tree --no-cargo runs it) -- item 3's second branch; #3592 stays open for the analyser: R1 no absolute path under /mnt /home /Users /opt /srv /media /root on ANY line of scripts/release/, comments included. R2 no literal (no `$`) assigned to V T MS EPIC LAST_TAG AP on a non-comment line. Zero files scanned = ENV 2. 14-row case table; its first real finding was this change's own library comment quoting the old path (reworded). FALSIFIER (item 4), on a copy of the real scripts/release/: planting `AP=/mnt/nvme-raid0/agent-wt/rel-0690-autopilot` in autopilot.sh -> R1+R2 RED, rc 1; planting `MS=0.69.0` in prepare_bump.sh -> R2 RED. Mutants, each RED: R1 without mnt (row 3), R2 reading comments (9, 14), R2 accepting any value (1, 14), zero files as a pass (12), R2 anchored at line start only (6). docs/specifications/06x-release-schedule.md §4: the autopilot sentence now names the in-tree scripts and their one input, not the 0.66 out-of-tree precedent. Not changed, stated: publish_strict.sh's crate count (74) is a 0.68.2-era constant of the same shape, outside #3618's five names; its 4 bashrs errors are pre-existing false positives, identical on main. Gates: guard self-test 14/14 + PASS on the tree; check_tag_step_gated PASS + self-test; check_t2_preflight_verdict PASS; t2_preflight --self-test OK; sourced-libs option-neutral (9 libs); bashrs-gate PASS; pipe-into-grep-q at ceiling; hardcoded-paths +0; apr pinned; guards-are-wired PASS; readme claims; roadmap aggregate idempotent + sorted/unique/additive; fmt, deny, aprender-contracts --lib 1666. Co-Authored-By: Claude Opus 5 (1M context) --- docs/roadmaps/entries/PMAT-3618.yaml | 17 +++ docs/roadmaps/roadmap.yaml | 17 +++ docs/specifications/06x-release-schedule.md | 2 +- .../check_release_scripts_derive_identity.sh | 103 ++++++++++++++++++ scripts/release/autopilot.sh | 33 +++--- scripts/release/ledger_from_run.sh | 6 +- scripts/release/lib_release_params.sh | 97 +++++++++++++++++ scripts/release/prepare_bump.sh | 40 ++++--- scripts/release/publish_strict.sh | 15 ++- scripts/release/t2_preflight.sh | 6 +- 10 files changed, 300 insertions(+), 36 deletions(-) create mode 100644 docs/roadmaps/entries/PMAT-3618.yaml create mode 100644 scripts/check_release_scripts_derive_identity.sh create mode 100644 scripts/release/lib_release_params.sh diff --git a/docs/roadmaps/entries/PMAT-3618.yaml b/docs/roadmaps/entries/PMAT-3618.yaml new file mode 100644 index 0000000000..b53ffdb278 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-3618.yaml @@ -0,0 +1,17 @@ +- id: PMAT-3618 + github_issue: 3618 + item_type: task + title: 'Release scripts still carry 0.68.2''s V/T/MS/EPIC and AP=/mnt/... — and the hardcoded-path ratchet does not match /mnt, so nothing catches it' + status: planned + priority: high + assigned_to: null + created: 2026-09-21T05:36:49Z + updated: 2026-09-21T05:36:49Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: 'ACCEPTANCE (quoted from issue #3618 "done_when"): 1. V/T/MS/EPIC are derived or passed, never literals. 2. AP is derived from the repository, not from a path under /mnt. The scripts are in-tree now; an out-of-tree bundle path is a leftover of where they used to live. 3. Either the /mnt blind spot closes (#3592) or this row ships its own assertion that no release script references a path outside the repo. Prefer the first — the blind spot affects every script, not these. 4. Falsifier: reintroduce a literal /mnt/... in a release script and require a gate to go RED. If nothing can be made to go red, item 3 is unsatisfied regardless of what was edited. SCOPE NOTE (author): this row ships its own assertion (item 3, second branch); #3592 stays open for the analyser itself.' diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 618b9a93ca..9d127733d3 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -18973,6 +18973,23 @@ roadmap: labels: - kind:code notes: 'ACCEPTANCE (hand-entered from issue #3587 "What lands"; minted on the PR branch so the AD-04 quorum can run -- pmat work status refuses a missing item). Two halves: the explicit-command runner (#3616, merged) and the nextest [profile.ci] key (#3626). Lands: 1. **--no-fail-fast on the shard.** The cost is runner minutes on an already-red shard; the benefit is that a red run still measures everything else it was going to measure — which is exactly when you most want the data. 2. **The reducer reports ran / skipped / total** in the shard receipt. The skipped count is a **number**, never an absence. 3. **A receipt with skipped > 0 is Unknown, not Fail and not Pass** — it names what was not measured, per doctrine 4. ' +- id: PMAT-3618 + github_issue: 3618 + item_type: task + title: 'Release scripts still carry 0.68.2''s V/T/MS/EPIC and AP=/mnt/... — and the hardcoded-path ratchet does not match /mnt, so nothing catches it' + status: planned + priority: high + assigned_to: null + created: 2026-09-21T05:36:49Z + updated: 2026-09-21T05:36:49Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: 'ACCEPTANCE (quoted from issue #3618 "done_when"): 1. V/T/MS/EPIC are derived or passed, never literals. 2. AP is derived from the repository, not from a path under /mnt. The scripts are in-tree now; an out-of-tree bundle path is a leftover of where they used to live. 3. Either the /mnt blind spot closes (#3592) or this row ships its own assertion that no release script references a path outside the repo. Prefer the first — the blind spot affects every script, not these. 4. Falsifier: reintroduce a literal /mnt/... in a release script and require a gate to go RED. If nothing can be made to go red, item 3 is unsatisfied regardless of what was edited. SCOPE NOTE (author): this row ships its own assertion (item 3, second branch); #3592 stays open for the analyser itself.' - id: PMAT-3627 github_issue: 3627 item_type: task diff --git a/docs/specifications/06x-release-schedule.md b/docs/specifications/06x-release-schedule.md index d4354e7659..0671c61840 100644 --- a/docs/specifications/06x-release-schedule.md +++ b/docs/specifications/06x-release-schedule.md @@ -279,7 +279,7 @@ The milestone already holds 15 issues; with the 14 rows above the train is over ## §4. Release-day protocol (every train; the FULL tier's only mandatory home) -Numbered so an autopilot (`/mnt/nvme-raid0/agent-wt/rel-066-autopilot/` is the 0.66 precedent) can run it fail-closed, with a `STOP ` line naming the refusing gate. +Numbered so an autopilot can run it fail-closed, with a `STOP ` line naming the refusing gate. Since #3618 that autopilot is in-tree: `scripts/release/autopilot.sh `, with `prepare_bump.sh [--ship]`, `t2_preflight.sh ` and `publish_strict.sh [--plan]` beside it. The version is the only input; the milestone, the release epic, the previous tag and the state directory (`
/target/release-train/v`) are derived by `scripts/release/lib_release_params.sh`, and `scripts/check_release_scripts_derive_identity.sh` refuses a literal identity or an out-of-repo path in that directory. (The 0.66–0.68.2 trains ran from out-of-tree bundles.) 1. **Freeze the platform.** `gh pr list --state open --label `: rows still open move to the next epic with `slipped_from:`. No new pushes while the queue drains (`[[feedback_push_cadence_starves_the_merge_queue]]`). Then read the milestone, before the bump PR opens: `bash scripts/check_milestone_cut.sh X.Y.0` must exit 0, meaning no open issue **or** PR except this train's own release epic (§5), which is open until step 8 by design and is named as `ADMITTED`. An item that does not ride this train is **carried by moving it**: `gh issue edit N --milestone ` (or `gh pr edit`) plus a `slipped_from: X.Y.0` comment. It is never carried by leaving it open here, because step 8 closes the milestone, and the train driver refuses to close one whose `open_issues` is not 0 (PMAT-3445, #3445). 2. **Bump.** `bash scripts/bump-version.sh X.Y.0`; `CHANGELOG.md` section from the epic's rows; the bump PR carries the receipt(s). diff --git a/scripts/check_release_scripts_derive_identity.sh b/scripts/check_release_scripts_derive_identity.sh new file mode 100644 index 0000000000..1389a44402 --- /dev/null +++ b/scripts/check_release_scripts_derive_identity.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# check_release_scripts_derive_identity.sh -- scripts/release/ may not carry a release's +# identity as a literal, nor any path outside the repository (#3618). +# +# THE DEFECT. The release scripts were ported from an out-of-tree 0.68.2 bundle (#3599) +# and kept V=0.68.2, MS=12, EPIC=3477, LAST_TAG=v0.68.1 and +# AP=. Cutting 0.69 through them would have +# targeted the old version and a directory that may not exist. The path was invisible +# to every gate: check_hardcoded_paths.sh does not match that mount at all (#3592, +# proved with a two-path probe scoring +1, not +2). The only thing that named it was a +# paragraph in #3617's PR body. A prose warning is not a guard. +# +# THE RULES, over every file under scripts/release/: +# R1 no absolute path under /mnt /home /Users /opt /srv /media /root -- on ANY line, +# comments included. Erring strict is correct in a seven-file directory: a comment +# naming an operator-box path is how the last one survived review. +# R2 no LITERAL value assigned to a release identity name (V T MS EPIC LAST_TAG AP) +# on a non-comment line: a value with no `$` in it. `MS=$V`, `T="v$V"` and +# `AP="${RELEASE_AP:-...}"` are derivations; `V=0.69.0` and `EPIC=3477` are not. +# scripts/release/lib_release_params.sh is where the identity is derived. +# Zero files scanned is ENV rc=2, never a pass. +# +# check_release_scripts_derive_identity.sh judge scripts/release/ +# check_release_scripts_derive_identity.sh --self-test the case table (fixtures) +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" || exit 2 +DIR="${RELEASE_SCRIPTS_DIR:-$ROOT/scripts/release}" + +R1_RE='(^|[^A-Za-z0-9_.$}-])/(mnt|home|Users|opt|srv|media|root)/' +R2_RE='(^|[;&|[:space:]])(V|T|MS|EPIC|LAST_TAG|AP)=[^[:space:];$]*([[:space:];]|$)' + +# judge -> 0 clean, 1 a finding (each printed), 2 ENV +judge() { + local dir=$1 f n=0 bad=0 hits + [ -d "$dir" ] || { printf 'ENV %s: no such directory -- cannot judge, not a pass\n' "$dir" >&2; return 2; } + while IFS= read -r -d '' f; do + n=$((n + 1)) + hits=$(grep -nE "$R1_RE" "$f") || hits="" + if [ -n "$hits" ]; then + bad=1 + printf 'FAIL R1 %s: a path outside the repository:\n' "${f#"$ROOT/"}" + printf '%s\n' "$hits" | sed 's/^/ /' + fi + # R2 reads non-comment lines only; the line number is kept for the report + hits=$(awk -v re="$R2_RE" '!/^[[:space:]]*#/ && $0 ~ re { printf "%d:%s\n", NR, $0 }' "$f") + if [ -n "$hits" ]; then + bad=1 + printf 'FAIL R2 %s: a release identity assigned a literal (derive it: lib_release_params.sh):\n' "${f#"$ROOT/"}" + printf '%s\n' "$hits" | sed 's/^/ /' + fi + done < <(find "$dir" -maxdepth 1 -type f -print0) + [ "$n" -gt 0 ] || { printf 'ENV %s: zero files scanned -- a scan of nothing is not a pass\n' "$dir" >&2; return 2; } + [ "$bad" -eq 0 ] && printf 'ok %s file(s) under %s: no out-of-repo path, no literal release identity\n' "$n" "${dir#"$ROOT/"}" + return "$bad" +} + +case "${1:-}" in -h|--help) sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac + +if [ "${1:-}" = "--self-test" ]; then + echo "=== release scripts derive their identity: case table ===" + d=$(mktemp -d) || exit 2 + rmtree() { case "${1:-}" in ''|/) return 0 ;; *) [ -d "$1" ] && rm -rf -- "$1" ;; esac; return 0; } + trap 'rmtree "${d:-}"' EXIT + bad=0; n=0 + row() { # row WANT_RC LABEL FILE-BODY + local want=$1 label=$2 body=$3 rc=0; n=$((n + 1)) + rmtree "$d/r"; mkdir -p "$d/r" + printf '%s' "$body" > "$d/r/x.sh" + judge "$d/r" > /dev/null 2>&1 || rc=$? + if [ "$rc" = "$want" ]; then printf 'ok row %-2s rc=%s %s\n' "$n" "$rc" "$label" + else printf 'FAIL row %-2s rc=%s (wanted %s) %s\n' "$n" "$rc" "$want" "$label" >&2; bad=1; fi + } + M=/mnt; H=/home # built, so this file itself carries no literal for R1 to find + row 0 "derivations only -> clean" \ + $'REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"\nrelease_params "$1" "$REPO_ROOT"\nMS=$V\nT="v$V"\nAP="${RELEASE_AP:-$ROOT/target/release-train/$T}"\nexport PATH="$HOME/.cargo/bin:$PATH"\ncmd > /dev/null 2>&1\n' + row 1 "the #3618 shape: AP under the RAID mount -> RED (R1 and R2)" "AP=$M/nvme-raid0/agent-wt/rel-0682-autopilot"$'\n' + row 1 "that path in a COMMENT -> RED (R1 reads comments)" "# the precedent is $M/nvme-raid0/agent-wt/rel-067-autopilot/"$'\n' + row 1 "a home-directory path -> RED" "cd $H/noah/src/aprender"$'\n' + row 1 "V=0.69.0 -> RED (R2)" $'V=0.69.0; T="v$V"\n' + row 1 "MS and EPIC literals after another statement -> RED" $'REPO=paiml/aprender; MS=12; EPIC=3477\n' + row 1 "LAST_TAG=v0.68.1 -> RED" $'LAST_TAG=v0.68.1\n' + row 1 "a quoted literal V=\"0.69.0\" -> RED" $'V="0.69.0"\n' + row 0 "an assignment in a COMMENT is documentation -> clean (R2 skips comments)" $'# V=0.68.2 was the old literal\n' + row 0 "lowercase locals and names that merely end in V/T -> clean" $'local v=$1 t=$2\nENV=prod\nPLOT=1\n' + row 0 "/dev/null, /tmp and \$HOME paths are not operator-box literals -> clean" $'x > /dev/null\nmktemp -p /tmp\nls "$HOME/.cargo/bin"\n' + n=$((n + 1)); rc=0; rmtree "$d/empty"; mkdir -p "$d/empty"; judge "$d/empty" > /dev/null 2>&1 || rc=$? + [ "$rc" -eq 2 ] && printf 'ok row %-2s rc=2 zero files -> ENV, never a pass\n' "$n" \ + || { printf 'FAIL row %-2s rc=%s (wanted 2) zero files\n' "$n" "$rc" >&2; bad=1; } + n=$((n + 1)); rc=0; judge "$d/no-such-dir" > /dev/null 2>&1 || rc=$? + [ "$rc" -eq 2 ] && printf 'ok row %-2s rc=2 missing directory -> ENV\n' "$n" \ + || { printf 'FAIL row %-2s rc=%s (wanted 2) missing directory\n' "$n" "$rc" >&2; bad=1; } + # and the real directory, so a red tree cannot hide behind green fixtures + n=$((n + 1)); rc=0; judge "$DIR" > /dev/null 2>&1 || rc=$? + [ "$rc" -eq 0 ] && printf 'ok row %-2s rc=0 the real %s is clean\n' "$n" "${DIR#"$ROOT/"}" \ + || { printf 'FAIL row %-2s rc=%s the real %s\n' "$n" "$rc" "${DIR#"$ROOT/"}" >&2; bad=1; } + [ "$bad" -eq 0 ] && { printf 'SELF-TEST PASSED: %s rows\n' "$n"; exit 0; } + printf 'SELF-TEST FAILED\n' >&2; exit 1 +fi + +echo "=== scripts/release/ derives the release identity (check_release_scripts_derive_identity.sh) ===" +judge "$DIR"; rc=$? +[ "$rc" -eq 0 ] && echo "PASS" || echo "FAIL (rc=$rc)" >&2 +exit "$rc" diff --git a/scripts/release/autopilot.sh b/scripts/release/autopilot.sh index 44ea8d7881..d30115fe9c 100755 --- a/scripts/release/autopilot.sh +++ b/scripts/release/autopilot.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash -# 0.68.2 release autopilot (interrupt train, EPIC #3477; adapted from 0.68.1) (crates.io release of the 0.68 line; v0.68.0 is GitHub-only per operator 2026-09-17) (PMAT-1098; APR-RELEASE-001 §4 T-0..T-4, rev 2026-09-16). Fail-closed: -# every irreversible step sits behind the repo's own gate. Terminal states only in STATUS, full logs -# beside it. Never --force, never --allow-dirty, never a skip. The 0.66 precedent is -# /mnt/nvme-raid0/agent-wt/rel-067-autopilot/ (0.67, which ran to close with T-4 automated). +# Release autopilot (PMAT-1098; APR-RELEASE-001 §4 T-0..T-4, rev 2026-09-16), first run for 0.68.2 +# (EPIC #3477). Fail-closed: every irreversible step sits behind the repo's own gate. Terminal states +# only in STATUS, full logs beside it. Never --force, never --allow-dirty, never a skip. +# The train's identity is DERIVED (#3618, scripts/release/lib_release_params.sh): the version is the +# one argument; milestone, epic and the state dir AP are read from GitHub and the repo, never literals. # -# autopilot.sh [from-step] [to-step] +# autopilot.sh [from-step] [to-step] # steps: wait deep dogfood tag cleanroom assets preflight cascade install hosts close # T-4 for THIS train (operator 2026-09-17): cascade DRY-RUN receipt, then STOP and report — the cascade # itself is the operator's step. Default to-step is dryrun; `cascade` and later run only when named. @@ -13,9 +14,14 @@ # cascade-publish.sh refuses without a green `clean-room (aprender)` on exactly the tag commit. # (§4.1 freeze and §4.2 bump are prepare_bump.sh: they need review, so they are not in here) set -uo pipefail -V=0.68.2; T="v$V"; MS=12; EPIC=3477; REPO=paiml/aprender; INFRA=paiml/infra -AP=/mnt/nvme-raid0/agent-wt/rel-0682-autopilot; STATUS="$AP/STATUS"; LOG="$AP/autopilot.log" -PR="${1:?usage: autopilot.sh [from-step] [to-step]}"; FROM="${2:-wait}"; TO="${3:-dryrun}" +# D2/D3: $0-derived root, resolved BEFORE any cd and before the params that need it. +# NOT `git rev-parse --show-toplevel` -- git refuses a container bind-mounted tree (#3586). +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" || { echo "cannot resolve the repo root from $0" >&2; exit 2; } +# shellcheck source=scripts/release/lib_release_params.sh +. "$REPO_ROOT/scripts/release/lib_release_params.sh" || exit 2 +release_params "${1:-}" "$REPO_ROOT" || { echo "usage: autopilot.sh [from-step] [to-step]" >&2; exit 2; } +STATUS="$AP/STATUS"; LOG="$AP/autopilot.log" +PR="${2:?usage: autopilot.sh [from-step] [to-step]}"; FROM="${3:-wait}"; TO="${4:-dryrun}" STEPS=(wait deep dogfood tag cleanroom assets preflight dryrun cascade install hosts close) say() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" | tee -a "$STATUS" >> "$LOG"; } die() { say "STOP $*"; exit 1; } @@ -32,9 +38,10 @@ run_step() { # run_step : true when is at or after FROM and at or b # so the old form could fail step-name validation for a reason unrelated to the step name. case " ${STEPS[*]} " in *" $FROM "*) ;; *) die "unknown step '$FROM' (${STEPS[*]})" ;; esac case " ${STEPS[*]} " in *" $TO "*) ;; *) die "unknown to-step '$TO' (${STEPS[*]})" ;; esac -# D2/D3: $0-derived root and CARGO_HOME-relative bin, resolved BEFORE any cd. -# NOT `git rev-parse --show-toplevel` -- git refuses a container bind-mounted tree (#3586). -REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" || die "cannot resolve the repo root from $0" +# the milestone and epic, derived once, before any step can need them (#3618) +MS=$(release_milestone_number) || die "milestone $V: cannot resolve exactly one" +EPIC=$(release_epic_number) || die "release epic for $V: cannot resolve exactly one" +say "PARAMS V=$V T=$T milestone=#$MS epic=#$EPIC AP=$AP" CARGO_BIN="${CARGO_HOME:-$HOME/.cargo}/bin" export PATH="$CARGO_BIN:$PATH" unset CARGO_REGISTRY_TOKEN @@ -292,9 +299,9 @@ fi # 9. close: the epic hears the receipts; the milestone closes only when nothing is left on it if run_step close; then - gh issue comment "$EPIC" --repo $REPO --body "$T released: $(gh release view "$T" --repo $REPO --json url -q .url). Receipts: pre-publish dogfood GO at \`$MC\`, all release assets verified by \`scripts/check_release_assets.sh $T\`, crates.io cascade complete, the CUDA asset downloaded, verified and run on gx10 and yoga. clean-room (aprender) green on the tag (run $(cat "$AP/cleanroom-run-id" 2>/dev/null)), install.sh receipts on intel and gx10. Logs: rel-0682-autopilot/ on lambda." >> "$LOG" 2>&1 || say "WARN epic comment failed" + gh issue comment "$EPIC" --repo $REPO --body "$T released: $(gh release view "$T" --repo $REPO --json url -q .url). Receipts: pre-publish dogfood GO at \`$MC\`, all release assets verified by \`scripts/check_release_assets.sh $T\`, crates.io cascade complete, the CUDA asset downloaded, verified and run on gx10 and yoga. clean-room (aprender) green on the tag (run $(cat "$AP/cleanroom-run-id" 2>/dev/null)), install.sh receipts on intel and gx10. Logs: $AP on $(hostname)." >> "$LOG" 2>&1 || say "WARN epic comment failed" open=$(gh api "repos/$REPO/milestones/$MS" --jq .open_issues); [ "$open" = 0 ] || die "milestone $V still has $open open item(s); not closing" gh api -X PATCH "repos/$REPO/milestones/$MS" -f state=closed >> "$LOG" 2>&1 && say "MILESTONE $V closed" - python3 "$AP/ledger.py" "$AP" "$MC" "$T" "$V" "$STATUS" >> "$LOG" 2>&1 && say "LEDGER record written in $AP (commit under docs/build-ledger/$(date -u +%F)/ via a docs PR)" || say "WARN ledger record not written" # bashrs disable-line=DET002 + python3 "$REPO_ROOT/scripts/release/ledger.py" "$AP" "$MC" "$T" "$V" "$STATUS" >> "$LOG" 2>&1 && say "LEDGER record written in $AP (commit under docs/build-ledger/$(date -u +%F)/ via a docs PR)" || say "WARN ledger record not written" # bashrs disable-line=DET002 say "DONE $V released, published, installed, receipts taken" fi diff --git a/scripts/release/ledger_from_run.sh b/scripts/release/ledger_from_run.sh index 936b8b5412..bf58a8c37c 100644 --- a/scripts/release/ledger_from_run.sh +++ b/scripts/release/ledger_from_run.sh @@ -29,10 +29,14 @@ scc=$(command grep -m1 -oE 'B2-cpu: sccache=.*' "$log" | sed -E 's/B2-cpu: //; s sha=$(command grep -m1 -oE 'tested-sha: [0-9a-f]{40}' "$log" | cut -d' ' -f2 || true) runner=$(command grep -m1 -oE "Runner name: '[^']+'" "$log" | sed -E "s/Runner name: '([^']+)'/\1/" || true) concl=$(gh run view "$run" --repo paiml/infra --json conclusion -q .conclusion) +# the tag is the one the tested commit carries, never a literal (#3618: this said "v0.68.1") +root="$(cd "$(dirname "$0")/../.." && pwd)" +tag="" +[ -n "$sha" ] && tag=$(git -C "$root" tag --points-at "$sha" --list 'v[0-9]*' --sort=-v:refname | sed -n 1p) cat </target/release-train/, +# where
is the directory holding the repo's COMMON git dir, +# so every worktree of the repo resolves the SAME AP (prepare_bump.sh writes +# release_notes.md there and autopilot.sh reads it). target/ is ignored. +# RELEASE_AP overrides it at run time; it is never a literal in the tree. +# milestone the one milestone titled exactly "$V" (release_milestone_number) +# epic the one `epic`-labelled item in that milestone titled +# "EPIC: release train $V ..." (release_epic_number; +# RELEASE_EPIC overrides, digits only) +# last tag the highest v* tag strictly below $T (release_last_tag) +# Each derivation that cannot find EXACTLY one answer returns 2 and says why: +# an ambiguous identity is not a default. +# +# SOURCED, so OPTION-NEUTRAL: no `set` here (check_sourced_libs_option_neutral.sh); +# every entry point reports by return status. +# . "$REPO_ROOT/scripts/release/lib_release_params.sh" || exit 2 +# release_params "$1" || exit 2 + +RELEASE_REPO=paiml/aprender +RELEASE_INFRA=paiml/infra + +# release_state_root -> the main checkout (parent of the common .git dir), or the root itself +release_state_root() { + local root=$1 common="" + common=$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || common="" + case "$common" in + */.git) printf '%s\n' "${common%/.git}" ;; + *) printf '%s\n' "$root" ;; + esac +} + +# release_params [] -> sets V T AP REPO INFRA; rc 2 on a bad version +release_params() { + local v=${1:-} root=${2:-} + [[ $v =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || { printf 'release: version "%s" is not X.Y.Z -- pass the version being cut as the first argument\n' "$v" >&2; return 2; } + [ -n "$root" ] || root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" || return 2 + V=$v + T="v$V" + REPO=$RELEASE_REPO + INFRA=$RELEASE_INFRA + AP="${RELEASE_AP:-$(release_state_root "$root")/target/release-train/$T}" + mkdir -p "$AP" || { printf 'release: cannot create the state dir %s\n' "$AP" >&2; return 2; } + return 0 +} + +# release_milestone_number -> the number of the one milestone titled exactly $V +release_milestone_number() { + local out n + out=$(gh api "repos/$RELEASE_REPO/milestones?state=all&per_page=100" --paginate \ + --jq ".[] | select(.title == \"$V\") | .number") \ + || { printf 'release: reading milestones failed\n' >&2; return 2; } + n=$(printf '%s\n' "$out" | grep -c '[0-9]') || n=0 + [ "$n" = 1 ] || { printf 'release: %s milestone(s) titled "%s"; exactly 1 is required\n' "$n" "$V" >&2; return 2; } + printf '%s\n' "$out" +} + +# release_epic_number -> RELEASE_EPIC, or the one epic-labelled item in milestone $V titled "EPIC: release train $V ..." +release_epic_number() { + local out n + if [ -n "${RELEASE_EPIC:-}" ]; then + case "$RELEASE_EPIC" in *[!0-9]*) printf 'release: RELEASE_EPIC=%s is not an issue number\n' "$RELEASE_EPIC" >&2; return 2 ;; esac + printf '%s\n' "$RELEASE_EPIC"; return 0 + fi + out=$(gh issue list --repo "$RELEASE_REPO" --milestone "$V" --label epic --state all --limit 100 \ + --json number,title --jq ".[] | select(.title == \"EPIC: release train $V\" or (.title | startswith(\"EPIC: release train $V \"))) | .number") \ + || { printf 'release: reading milestone %s failed\n' "$V" >&2; return 2; } + n=$(printf '%s\n' "$out" | grep -c '[0-9]') || n=0 + [ "$n" = 1 ] || { printf 'release: %s item(s) in milestone %s titled "EPIC: release train %s ..."; exactly 1 is required (or set RELEASE_EPIC)\n' "$n" "$V" "$V" >&2; return 2; } + printf '%s\n' "$out" +} + +# release_last_tag -> the highest vX.Y.Z tag strictly below $T +release_last_tag() { + local root=$1 t + while IFS= read -r t; do + case "$t" in *-*) continue ;; esac # no pre-release tags + [ "$t" = "$T" ] && continue + if printf '%s\n%s\n' "$t" "$T" | sort -V -C; then printf '%s\n' "$t"; return 0; fi + done < <(git -C "$root" tag --list 'v[0-9]*' --sort=-v:refname) + printf 'release: no tag below %s in %s\n' "$T" "$root" >&2 + return 2 +} diff --git a/scripts/release/prepare_bump.sh b/scripts/release/prepare_bump.sh index aae4feff6b..81e52f3645 100755 --- a/scripts/release/prepare_bump.sh +++ b/scripts/release/prepare_bump.sh @@ -1,21 +1,29 @@ #!/usr/bin/env bash -# prepare_bump.sh — §4.2 of docs/specifications/06x-release-schedule.md for 0.68.2: the bump and the +# prepare_bump.sh — §4.2 of docs/specifications/06x-release-schedule.md: the bump and the # CHANGELOG section. Two invocations, because the section needs a human-quality summary: -# prepare_bump.sh worktree at origin/main, bump-version.sh 0.68.2 + --check, CHANGELOG [0.68.2] -# drafted from the milestone's merged PRs (with a placeholder --ship refuses) -# prepare_bump.sh --ship pre-push checks, commit, push, PR in milestone 0.68.1 with auto-merge; -# prints the autopilot launch line for that PR -# §4.1 (freeze: open milestone items move to 0.69.0 with slipped_from:) is done by hand before this. +# prepare_bump.sh worktree at origin/main, bump-version.sh + --check, +# CHANGELOG [] drafted from the PRs merged since the last +# tag (with a placeholder --ship refuses) +# prepare_bump.sh --ship pre-push checks, commit, push, PR in milestone with +# auto-merge; prints the autopilot launch line for that PR +# The train's identity is DERIVED (#3618, lib_release_params.sh): milestone, epic, last tag and the +# state dir AP come from GitHub and the repo, never from literals. +# §4.1 (freeze: open milestone items move to the next milestone with slipped_from:) is done by hand before this. set -uo pipefail -V=0.68.2; MS=0.68.2; REPO=paiml/aprender; AP=/mnt/nvme-raid0/agent-wt/rel-0682-autopilot; LAST_TAG=v0.68.1 -B="$AP/bump"; BR="PMAT-3477-release-$V"; MARK='' die() { printf 'STOP %s\n' "$*" >&2; exit 1; } # D4/D5/D6/D7 (PMAT-3459): $0-derived root and CARGO_HOME-relative cargo. Root resolved # BEFORE any cd. NOT `git rev-parse --show-toplevel` — refused on a bind-mounted tree (#3586). REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" || die "cannot resolve the repo root from $0" +# shellcheck source=scripts/release/lib_release_params.sh +. "$REPO_ROOT/scripts/release/lib_release_params.sh" || exit 2 +release_params "${1:-}" "$REPO_ROOT" || die "usage: prepare_bump.sh [--ship]" +MS=$V +EPIC=$(release_epic_number) || die "release epic for $V: cannot resolve exactly one" +LAST_TAG=$(release_last_tag "$REPO_ROOT") || die "no release tag below $T" +B="$AP/bump"; BR="release-$V"; MARK='' export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH" -if [ "${1:-}" != "--ship" ]; then +if [ "${2:-}" != "--ship" ]; then cd "$REPO_ROOT" || die "no repo" git fetch -q origin main || die "fetch failed" [ -e "$B" ] && die "$B exists: review it, or 'git worktree remove' it to start over" @@ -49,14 +57,14 @@ s = open(p).read(); anchor = "## [Unreleased]\n" assert s.count(anchor) == 1, "CHANGELOG has no single [Unreleased] anchor" open(p, "w").write(s.replace(anchor, anchor + "\n" + sec.rstrip("\n") + "\n", 1)) PY - printf 'REVIEW %s/CHANGELOG.md [%s]: replace the placeholder with the train summary, curate the bullets, then run: %s --ship\n' "$B" "$V" "$0" + printf 'REVIEW %s/CHANGELOG.md [%s]: replace the placeholder with the train summary, curate the bullets, then run: %s %s --ship\n' "$B" "$V" "$0" "$V" exit 0 fi cd "$B" || die "no $B (run without --ship first)" # T-2 PREFLIGHT BEFORE THE BUMP (operator 2026-09-17): the bump PR is refused without a GO receipt for its parent sha. parent=$(git rev-parse origin/main); rcpt="$AP/preflight-$parent.verdict" -[ -f "$rcpt" ] && grep -q '^GO ' "$rcpt" || die "no T-2 GO receipt for origin/main $parent ($rcpt) — run: $AP/t2_preflight.sh" +[ -f "$rcpt" ] && grep -q '^GO ' "$rcpt" || die "no T-2 GO receipt for origin/main $parent ($rcpt) — run: $REPO_ROOT/scripts/release/t2_preflight.sh $V" grep -qF "$MARK" CHANGELOG.md && die "CHANGELOG [$V] still carries the EDIT placeholder" bash scripts/bump-version.sh --check > /dev/null 2>&1 || die "bump-version.sh --check failed" awk -v h="## [$V]" 'index($0, h) == 1 {f = 1; next} f && /^## \[/ {exit} f' CHANGELOG.md > "$AP/release_notes.md" @@ -70,17 +78,17 @@ git commit -q -F - < MSG git push -q -u origin "$BR" || die "push failed" -{ printf 'Release bump for **%s** (06x release schedule §4.2): `bump-version.sh %s` across every workspace, and the CHANGELOG section below.\n\nWhen this merges, `rel-0682-autopilot/autopilot.sh` runs the rest of the train, fail-closed (APR-RELEASE-001 §4): T-1 deep run, pre-publish dogfood, tag + release, `clean-room.yml` dispatched on the tag with the run id recorded (T-3), all release assets checked with `scripts/check_release_assets.sh`, publish preflight, the crates.io cascade (T-4, automated per operator 2026-09-13), `install.sh --version` receipts on intel and gx10 plus the CUDA asset receipts on gx10 and yoga, and the epic and milestone close.\n\n' "$V" "$V"; cat "$AP/release_notes.md"; printf '\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n'; } > "$AP/pr_body.md" +{ printf 'Release bump for **%s** (06x release schedule §4.2): `bump-version.sh %s` across every workspace, and the CHANGELOG section below.\n\nWhen this merges, `scripts/release/autopilot.sh` runs the rest of the train, fail-closed (APR-RELEASE-001 §4): T-1 deep run, pre-publish dogfood, tag + release, `clean-room.yml` dispatched on the tag with the run id recorded (T-3), all release assets checked with `scripts/check_release_assets.sh`, publish preflight, the crates.io cascade (T-4, automated per operator 2026-09-13), `install.sh --version` receipts on intel and gx10 plus the CUDA asset receipts on gx10 and yoga, and the epic and milestone close.\n\n' "$V" "$V"; cat "$AP/release_notes.md"; printf '\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n'; } > "$AP/pr_body.md" url=$(gh pr create --repo $REPO --base main --head "$BR" --milestone "$MS" --title "release: $V" --body-file "$AP/pr_body.md") || die "gh pr create failed" n=${url##*/} -ARM="$REPO_ROOT/scripts/arm_pr_automerge.sh"; [ -f "$ARM" ] || ARM=/mnt/nvme-raid0/agent-wt/wedge-3292/scripts/arm_pr_automerge.sh +ARM="$REPO_ROOT/scripts/arm_pr_automerge.sh"; [ -f "$ARM" ] || die "no $ARM" armed=0; for _ in $(seq 1 30); do bash "$ARM" "$n" > "$AP/arm.log" 2>&1; rc=$?; [ $rc -eq 0 ] && { armed=1; break; }; [ $rc -eq 3 ] || break; sleep 120; done [ $armed = 1 ] && printf 'ARMED #%s (guard-tree green on the head)\n' "$n" || printf 'WARN not armed: %s\n' "$(tail -1 "$AP/arm.log")" -printf 'BUMP PR #%s (%s)\nlaunch: setsid nohup %s/autopilot.sh %s > /dev/null 2>&1 < /dev/null & disown\n' "$n" "$url" "$AP" "$n" +printf 'BUMP PR #%s (%s)\nlaunch: setsid nohup %s/scripts/release/autopilot.sh %s %s > /dev/null 2>&1 < /dev/null & disown\n' "$n" "$url" "$REPO_ROOT" "$V" "$n" diff --git a/scripts/release/publish_strict.sh b/scripts/release/publish_strict.sh index 6f57f13731..86e8976067 100755 --- a/scripts/release/publish_strict.sh +++ b/scripts/release/publish_strict.sh @@ -1,12 +1,19 @@ #!/usr/bin/env bash -# publish_strict.sh — 0.68.2 crates.io cascade under the operator's 2026-09-17 authorization. +# publish_strict.sh [--plan] — the crates.io cascade under the operator's 2026-09-17 +# authorization (first run 0.68.2). The version is the one argument; T and the state dir AP are +# derived (#3618, lib_release_params.sh), never literals. # one crate per `cargo publish` call, in the tag's own TIERS order (scripts/cascade-publish.sh); # STOP on the first non-zero (partial publishes are recorded, never rolled back); # never --allow-dirty; a crates.io transient (429/5xx/timeout) is retried <=3 times with backoff, # same inputs; anything else stops. Runs only from a detached checkout whose HEAD == the tag. -# publish_strict.sh --plan prints the ordered plan and uploads nothing +# publish_strict.sh --plan prints the ordered plan and uploads nothing set -uo pipefail -V=0.68.2; T="v$V"; AP=/mnt/nvme-raid0/agent-wt/rel-0682-autopilot; WT="$AP/wt" +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" || { echo "cannot resolve the repo root from $0" >&2; exit 2; } +# shellcheck source=scripts/release/lib_release_params.sh +. "$REPO_ROOT/scripts/release/lib_release_params.sh" || exit 2 +release_params "${1:-}" "$REPO_ROOT" || { echo "usage: publish_strict.sh [--plan]" >&2; exit 2; } +shift +WT="$AP/wt" TSV="$AP/publish-timestamps.tsv"; LOGD="$AP/publish-logs"; STATUS="$AP/STATUS" say() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" | tee -a "$STATUS"; } # bashrs disable-line=DET002 die() { say "STOP publish: $*"; exit 1; } @@ -26,7 +33,7 @@ git symbolic-ref -q HEAD > /dev/null && die "checkout is not detached" # violations; it only ever worked through the drain's retries). publish-order.txt is derived from # `cargo metadata` at the tag (normal + build + versioned dev-deps, acyclic); the facades, which # version independently and resolve their upstream from the registry, go last. Re-proved below. -mapfile -t ORDER < <(cat "$AP/publish-order.txt"; printf '%s\n' provable-contracts provable-contracts-macros provable-contracts-cli) +mapfile -t ORDER < <(cat "$WT/scripts/release/publish-order.txt"; printf '%s\n' provable-contracts provable-contracts-macros provable-contracts-cli) declare -A EXPECT MANIFEST ROOTWS while IFS=$'\t' read -r n v m w; do [ -n "$n" ] && { EXPECT[$n]=$v; MANIFEST[$n]=$m; ROOTWS[$n]=$w; }; done < <(python3 scripts/lib/cascade_universe.py "$WT") [ "${#ORDER[@]}" -eq 74 ] && [ "${#EXPECT[@]}" -eq 74 ] || die "order=${#ORDER[@]} universe=${#EXPECT[@]}, expected 74/74" diff --git a/scripts/release/t2_preflight.sh b/scripts/release/t2_preflight.sh index 7a265a23d4..fc8a1c7f5c 100755 --- a/scripts/release/t2_preflight.sh +++ b/scripts/release/t2_preflight.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # t2_preflight.sh — APR-RELEASE-001 §4 T-0 (operator 2026-09-17): the pre-publish dogfood runs on origin/main HEAD # BEFORE the bump PR opens; writes preflight-.verdict = "GO " or "NO-GO ". prepare_bump.sh --ship refuses without GO. +# t2_preflight.sh the train's state dir AP is derived from it (#3618) +# t2_preflight.sh --self-test decide()'s case table; needs no version set -uo pipefail -AP=/mnt/nvme-raid0/agent-wt/rel-0682-autopilot # D1/D2/D3 (PMAT-3459): $0-derived root and CARGO_HOME-relative bin. Resolved BEFORE any cd. # NOT `git rev-parse --show-toplevel` — git refuses a container bind-mounted tree (#3586). REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" || exit 2 @@ -114,6 +115,9 @@ if [ "${1:-}" = "--self-test" ]; then echo "self-test FAILED: $bad case(s)"; exit 1 fi +# shellcheck source=scripts/release/lib_release_params.sh +. "$REPO_ROOT/scripts/release/lib_release_params.sh" || exit 2 +release_params "${1:-}" "$REPO_ROOT" || { echo "usage: t2_preflight.sh | --self-test" >&2; exit 2; } cd "$REPO_ROOT" || exit 2; git fetch -q origin main || exit 2 sha=$(git rev-parse origin/main); wt="$AP/preflight-wt" [ -d "$wt" ] && git worktree remove --force "$wt" > /dev/null 2>&1 From 9ee985d416a1c71523c2ea31c56c705c2d947d13 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 21 Sep 2026 07:49:08 +0200 Subject: [PATCH 2/2] =?UTF-8?q?PMAT-3618:=20AD-04=20quorum=20receipt=20for?= =?UTF-8?q?=20#3655=20=E2=80=94=203/3=20PASS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lanes gemini-3.1-pro-high / gemini-3.1-pro-low / gemini-3.6-flash-high at e9ca58999; commands 1/1/5; no transcript reads a sibling lane file. Refs #3618 Co-Authored-By: Claude Opus 5 (1M context) --- docs/audits/quorum-PMAT-3618.json | 241 ++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 docs/audits/quorum-PMAT-3618.json diff --git a/docs/audits/quorum-PMAT-3618.json b/docs/audits/quorum-PMAT-3618.json new file mode 100644 index 0000000000..4c92b3f17f --- /dev/null +++ b/docs/audits/quorum-PMAT-3618.json @@ -0,0 +1,241 @@ +{ + "ticket": "PMAT-3618", + "base": "origin/main", + "base_resolved": "origin/main", + "base_note": "no origin/origin/main exists; judged against the local ref", + "head": "e9ca589997b042bb38572975c812fda8806be123", + "diff_sha256": "333c8651632d2988647554afdab71cb73b33e53d41c247688afaa37b64840450", + "width": 3, + "executor": "agy", + "prompt_mode": "inline", + "prompt_bytes": 38781, + "author": { + "model": "claude-opus-5", + "family": "claude", + "source": "flag" + }, + "agreed": true, + "lanes": [ + { + "lane": 1, + "status": "SUCCESS", + "verdict": "PASS", + "summary": "The diff cleanly resolves PMAT-3618 without weakening any gates or making unauthorized changes. \n\n1. **AC 1 & 2 Satisfied:** All literal release identities (`V`, `T`, `MS`, `EPIC`, `LAST_TAG`) and the hardcoded `AP=/mnt/...` path have been replaced with derivations powered by a new `lib_release_params.sh` script. `AP` now properly resolves to the main repository checkout (`target/release-train/$T`).\n2. **AC 3 Satisfied:** The diff ships its own robust assertion script (`check_release_scripts_derive_identity.sh`). It scans all scripts under `scripts/release/` for paths matching `/mnt`, `/home`, etc. (R1) and literal identity assignments (R2). It properly skips comments for R2 and has strict anti-vacuity checks.\n3. **AC 4 Satisfied:** The assertion script includes a `--self-test` mechanism that perfectly models the falsifier requirement: it explicitly tests the `#3618` `/mnt/nvme-raid0/` shape and confirms that the script correctly flags it and fails.\n\nAll other modifications (such as adapting positional arguments in `publish_strict.sh` and `prepare_bump.sh` to accept the `` argument) are necessary consequences of the parameterization and perfectly execute the ticket's intent. No refutations found.", + "findings": [ + { + "claim": "The literal release identities (V, T, MS, EPIC, LAST_TAG) and out-of-repo AP path have been removed from autopilot.sh and other release scripts, satisfying AC 1 and AC 2. They are now dynamically resolved via lib_release_params.sh", + "file": "scripts/release/autopilot.sh", + "grounding": "cited", + "line": 14 + }, + { + "claim": "The row ships its own assertion gate via check_release_scripts_derive_identity.sh to forbid hardcoded literals (R2_RE) and out-of-repo paths (R1_RE), satisfying AC 3.", + "file": "scripts/check_release_scripts_derive_identity.sh", + "grounding": "cited", + "line": 29 + }, + { + "claim": "The assertion gate includes a self-test falsifier that explicitly verifies the gate goes RED when a literal `/mnt/nvme-raid0/...` path is reintroduced, satisfying AC 4.", + "file": "scripts/check_release_scripts_derive_identity.sh", + "grounding": "cited", + "line": 67 + } + ], + "raw_bytes": 6382, + "err_bytes": 0, + "envelope_status": "SUCCESS", + "verdict_source": "structured_output", + "grounding_check": "parity", + "model": "gemini-3.1-pro-high", + "model_measured": "gemini-3.1-pro-high", + "model_source": "measured", + "family": "gemini" + }, + { + "lane": 2, + "status": "SUCCESS", + "verdict": "PASS", + "summary": "The diff successfully implements the requested changes: V/T/MS/EPIC and AP are dynamically derived rather than hardcoded, and out-of-repo paths (like /mnt/) are removed. A new dedicated assertion script `scripts/check_release_scripts_derive_identity.sh` is introduced to enforce this constraint continuously, meeting the ticket's explicit branch 2 requirement. Testing confirms the script correctly flags literal paths.", + "findings": [ + { + "claim": "V/T/MS/EPIC and AP are derived, not literals", + "file": "scripts/release/autopilot.sh", + "grounding": "cited" + }, + { + "claim": "Shipped assertion to enforce no out-of-repo paths or literal identities", + "file": "scripts/check_release_scripts_derive_identity.sh", + "grounding": "cited" + }, + { + "claim": "Falsifier gate accurately goes RED when tested against an out-of-repo path", + "file": "scripts/check_release_scripts_derive_identity.sh", + "grounding": "cited", + "grounding_downgraded": "measured-without-command-or-output" + } + ], + "raw_bytes": 3908, + "err_bytes": 0, + "envelope_status": "SUCCESS", + "verdict_source": "structured_output", + "grounding_check": "parity", + "model": "gemini-3.1-pro-low", + "model_measured": "gemini-3.1-pro-low", + "model_source": "measured", + "family": "gemini" + }, + { + "lane": 3, + "status": "SUCCESS", + "verdict": "PASS", + "summary": "As Lane 3 reviewer, I evaluated the diff against ticket PMAT-3618 and verified all acceptance criteria.\n\nKey Findings & Measured Verifications:\n1. Identity Derivation: `scripts/release/lib_release_params.sh` dynamically derives release identity parameters (`V`, `T`, `AP`, `MS`, `EPIC`, `LAST_TAG`). Scripts (`autopilot.sh`, `prepare_bump.sh`, `publish_strict.sh`, `t2_preflight.sh`, `ledger_from_run.sh`) no longer hardcode 0.68.2 constants or `/mnt/...` out-of-repo paths.\n2. In-Tree Assertion Guard: Introduced `scripts/check_release_scripts_derive_identity.sh` which enforces R1 (no out-of-repo paths) and R2 (no literal identity assignments) across `scripts/release/`. Its self-test suite passes 14 case table rows.\n3. Falsifier Verified: Reintroducing a literal `AP=/mnt/nvme-raid0/test` path into `scripts/release/autopilot.sh` causes `check_release_scripts_derive_identity.sh` to go RED (exit code 1).\n4. CI Wiring Verified: `guard_tree.sh` automatically discovers `scripts/check_release_scripts_derive_identity.sh` via globbing, ensuring execution on every PR/commit in CI.\n5. Preflight Self-Test Verified: `t2_preflight.sh --self-test` passes all 12 case table checks.\n\nNo refuting issues, weakened gates, unbacked receipt claims, or scope violations were found. Verdict: PASS.", + "findings": [ + { + "claim": "scripts/check_release_scripts_derive_identity.sh passes self-test suite (14 case table rows) and validates scripts/release/ cleanroom", + "command": "bash scripts/check_release_scripts_derive_identity.sh --self-test && bash scripts/check_release_scripts_derive_identity.sh", + "file": "scripts/check_release_scripts_derive_identity.sh", + "fix": "N/A", + "grounding": "measured", + "line": 1, + "output": "=== release scripts derive their identity: case table ===\nok row 1 rc=0 derivations only -> clean\nok row 2 rc=1 the #3618 shape: AP under the RAID mount -> RED (R1 and R2)\nok row 3 rc=1 that path in a COMMENT -> RED (R1 reads comments)\nok row 4 rc=1 a home-directory path -> RED\nok row 5 rc=1 V=0.69.0 -> RED (R2)\nok row 6 rc=1 MS and EPIC literals after another statement -> RED\nok row 7 rc=1 LAST_TAG=v0.68.1 -> RED\nok row 8 rc=1 a quoted literal V=\"0.69.0\" -> RED\nok row 9 rc=0 an assignment in a COMMENT is documentation -> clean (R2 skips comments)\nok row 10 rc=0 lowercase locals and names that merely end in V/T -> clean\nok row 11 rc=0 /dev/null, /tmp and $HOME paths are not operator-box literals -> clean\nok row 12 rc=2 zero files -> ENV, never a pass\nok row 13 rc=2 missing directory -> ENV\nok row 14 rc=0 the real scripts/release is clean\nSELF-TEST PASSED: 14 rows\n=== scripts/release/ derives the release identity (check_release_scripts_derive_identity.sh) ===\nok 8 file(s) under scripts/release: no out-of-repo path, no literal release identity\nPASS" + }, + { + "claim": "Falsifier verification: reintroducing a literal /mnt/... path in scripts/release/autopilot.sh causes check_release_scripts_derive_identity.sh to fail with exit code 1 (RED)", + "command": "bash -c 'sed -i \"s/# Release autopilot/AP=\\/mnt\\/nvme-raid0\\/test\\n# Release autopilot/\" scripts/release/autopilot.sh; bash scripts/check_release_scripts_derive_identity.sh; rc=$?; git checkout scripts/release/autopilot.sh; exit $rc'", + "file": "scripts/release/autopilot.sh", + "fix": "N/A", + "grounding": "measured", + "line": 2, + "output": "=== scripts/release/ derives the release identity (check_release_scripts_derive_identity.sh) ===\nFAIL R1 scripts/release/autopilot.sh: a path outside the repository:\n 2:AP=/mnt/nvme-raid0/test\nFAIL R2 scripts/release/autopilot.sh: a release identity assigned a literal (derive it: lib_release_params.sh):\n 2:AP=/mnt/nvme-raid0/test\nFAIL (rc=1)" + }, + { + "claim": "Wiring verification: scripts/check_release_scripts_derive_identity.sh is automatically discovered and dispatched by guard_tree.sh in CI", + "command": "bash scripts/guard_tree.sh --dry-run | grep \"check_release_scripts_derive_identity\"", + "file": "scripts/check_release_scripts_derive_identity.sh", + "fix": "N/A", + "grounding": "measured", + "line": 1, + "output": "run: scripts/check_release_scripts_derive_identity.sh" + }, + { + "claim": "t2_preflight.sh self-test suite executes and passes 12 case table assertions", + "command": "bash scripts/release/t2_preflight.sh --self-test", + "file": "scripts/release/t2_preflight.sh", + "fix": "N/A", + "grounding": "measured", + "line": 56, + "output": "ok crash with zero rows is not GO\nok crash returns 2 (UNKNOWN)\nok missing log is not GO\nok missing log returns 2 (UNKNOWN)\nok no VERDICT line is not GO\nok truncated returns 2 (UNKNOWN)\nok row count below the declared floor is not GO\nok below-floor returns 2 (UNKNOWN)\nok complete run, only version-unpublished red -> GO\nok GO returns 0\nok a genuine FAIL row -> NO-GO\nok NO-GO returns 1\nself-test OK" + } + ], + "raw_bytes": 17442, + "err_bytes": 0, + "envelope_status": "SUCCESS", + "verdict_source": "structured_output", + "model": "gemini-3.6-flash-high", + "model_measured": "gemini-3.6-flash-high", + "model_source": "measured", + "family": "gemini" + } + ], + "dissent": [], + "dedup": [ + { + "file": "scripts/check_release_scripts_derive_identity.sh", + "line": null, + "lanes_agreeing": [ + 2 + ], + "claims": [ + "Falsifier gate accurately goes RED when tested against an out-of-repo path", + "Shipped assertion to enforce no out-of-repo paths or literal identities" + ] + }, + { + "file": "scripts/check_release_scripts_derive_identity.sh", + "line": 1, + "lanes_agreeing": [ + 3 + ], + "claims": [ + "Wiring verification: scripts/check_release_scripts_derive_identity.sh is automatically discovered and dispatched by guard_tree.sh in CI", + "scripts/check_release_scripts_derive_identity.sh passes self-test suite (14 case table rows) and validates scripts/release/ cleanroom" + ] + }, + { + "file": "scripts/check_release_scripts_derive_identity.sh", + "line": 29, + "lanes_agreeing": [ + 1 + ], + "claims": [ + "The row ships its own assertion gate via check_release_scripts_derive_identity.sh to forbid hardcoded literals (R2_RE) and out-of-repo paths (R1_RE), satisfying AC 3." + ] + }, + { + "file": "scripts/check_release_scripts_derive_identity.sh", + "line": 67, + "lanes_agreeing": [ + 1 + ], + "claims": [ + "The assertion gate includes a self-test falsifier that explicitly verifies the gate goes RED when a literal `/mnt/nvme-raid0/...` path is reintroduced, satisfying AC 4." + ] + }, + { + "file": "scripts/release/autopilot.sh", + "line": null, + "lanes_agreeing": [ + 2 + ], + "claims": [ + "V/T/MS/EPIC and AP are derived, not literals" + ] + }, + { + "file": "scripts/release/autopilot.sh", + "line": 2, + "lanes_agreeing": [ + 3 + ], + "claims": [ + "Falsifier verification: reintroducing a literal /mnt/... path in scripts/release/autopilot.sh causes check_release_scripts_derive_identity.sh to fail with exit code 1 (RED)" + ] + }, + { + "file": "scripts/release/autopilot.sh", + "line": 14, + "lanes_agreeing": [ + 1 + ], + "claims": [ + "The literal release identities (V, T, MS, EPIC, LAST_TAG) and out-of-repo AP path have been removed from autopilot.sh and other release scripts, satisfying AC 1 and AC 2. They are now dynamically resolved via lib_release_params.sh" + ] + }, + { + "file": "scripts/release/t2_preflight.sh", + "line": 56, + "lanes_agreeing": [ + 3 + ], + "claims": [ + "t2_preflight.sh self-test suite executes and passes 12 case table assertions" + ] + } + ], + "uncovered": [], + "coverage_source": "lanes", + "partial": false, + "partial_reasons": [ + "lane 3: grounding NOT COMPARED — structured_output holds a verdict but no single verdict object could be read out of .response (17442 raw bytes), so agy's labels stand unchecked for this lane (PMAT-082)" + ], + "auto_merge": { + "checked": true, + "was_armed": false, + "disarmed": false, + "note": "auto-merge not armed" + }, + "lint": { + "ok": true, + "output": "receipt complete: kind=artifact lanes=3 author=claude-opus-5/claude" + } +}