From c10cdf6e100ef30bffd15e2a1fc5e7d411475c4e Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:16:49 +0200 Subject: [PATCH 1/5] ci: keep generated HQ snapshot and ACTIVITY.md off branches GitHub/Mergify merge without our local merge driver, so every PR that committed docs/dev-hq/data.js|json or .pa/ACTIVITY.md re-conflicted with all others. Only main and hq-snapshot branches may change them now. - gate hotspot-guard rejects a branch diff (vs merge-base) touching them - sync.sh note writes to untracked .pa/ACTIVITY.local.md on a branch - post-merge regenerates the snapshot on main only - session-end-hook debounces on the file sync.sh really writes Test-First: scripts/test-hotspot-guard.sh::guard rejects package branch touching docs/dev-hq/data.js Test-First: scripts/test-hotspot-guard.sh::guard rejects package branch touching docs/dev-hq/data.json Test-First: scripts/test-hotspot-guard.sh::guard rejects package branch touching .pa/ACTIVITY.md Test-First: scripts/test-hotspot-guard.sh::guard accepts branch touching other files only Test-First: scripts/test-hotspot-guard.sh::guard accepts main touching the hotspot files Test-First: scripts/test-hotspot-guard.sh::guard accepts a dedicated hq-snapshot branch Test-First: scripts/test-hotspot-guard.sh::guard fails closed without a base Test-First: scripts/test-hotspot-guard.sh::sync note on a package branch leaves the tracked ACTIVITY.md alone Test-First: scripts/lib/hq-post-merge.test.mjs::post-merge leaves the snapshot alone on a non-main branch Co-Authored-By: Claude Sonnet 5.5 --- .githooks/post-merge | 9 ++++ AGENTS.md | 8 +++ scripts/ci/gates.sh | 5 ++ scripts/ci/hotspot-guard.sh | 38 +++++++++++++ scripts/lib/hotspots.sh | 31 +++++++++++ scripts/lib/hq-post-merge.test.mjs | 13 +++++ scripts/session-end-hook.sh | 2 +- scripts/sync.sh | 25 +++++++-- scripts/test-hotspot-guard.sh | 85 ++++++++++++++++++++++++++++++ 9 files changed, 211 insertions(+), 5 deletions(-) create mode 100755 scripts/ci/hotspot-guard.sh create mode 100755 scripts/lib/hotspots.sh create mode 100755 scripts/test-hotspot-guard.sh diff --git a/.githooks/post-merge b/.githooks/post-merge index 0daefb6f..a4a7bfd5 100755 --- a/.githooks/post-merge +++ b/.githooks/post-merge @@ -5,6 +5,15 @@ set -euo pipefail ROOT="$(git rev-parse --show-toplevel)" cd "$ROOT" files=(docs/dev-hq/data.js docs/dev-hq/data.json) +# A branch must not carry a regenerated snapshot: it conflicts with every other +# PR (CI-CONFLICT-01). Only main and hq-snapshot branches regenerate. +# shellcheck source=scripts/lib/hotspots.sh +. "$ROOT/scripts/lib/hotspots.sh" +branch="$(hs_branch "$ROOT")" +if ! hs_may_touch "$branch"; then + echo "post-merge: Branch '${branch:-detached}' erzeugt die HQ-Momentaufnahme nicht neu (Konfliktherd); das tut main." >&2 + exit 0 +fi if [ -n "$(git status --porcelain -- "${files[@]}")" ]; then echo "post-merge: lokale HQ-Aenderungen bleiben erhalten; spaeter npm run hq ausfuehren." >&2 exit 0 diff --git a/AGENTS.md b/AGENTS.md index 6b32839c..b4d0aae1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,6 +145,14 @@ bash scripts/ci/gates.sh lane prepush # the full local lane bash scripts/ci/gates.sh --from clippy lane linux # resume after a failure ``` +Generated and shared files are written on `main` only: a branch must not +commit `docs/dev-hq/data.js|json` or `.pa/ACTIVITY.md` (gate `hotspot-guard`; +PRs merge without our local merge driver, so each such change conflicts with +every other PR). `sync.sh note` writes to the untracked `.pa/ACTIVITY.local.md` +on a branch (summary goes into the PR text), `post-merge` regenerates the +snapshot on `main` only, and the coordinator refreshes it in one small PR from a +`/hq-snapshot-*` branch (`npm run hq`, commit just the two files). + Lanes: `precommit`, `prepush`, `linux`, `windows`, `release`, `audit`. `ci.yml`, `release.yml`, `audit.yml` and both git hooks call exactly these lanes — one step per lane, no list left in the YAML. Drift is not checked, it is impossible. diff --git a/scripts/ci/gates.sh b/scripts/ci/gates.sh index 7b8fc965..911cacd8 100755 --- a/scripts/ci/gates.sh +++ b/scripts/ci/gates.sh @@ -112,6 +112,11 @@ GATES=( # can invoke them directly after checkout. "script-modes|prepush,linux,release|.|bash scripts/ci/script-modes.sh" "selftest-script-modes|prepush,linux,release|.|bash scripts/test-script-modes.sh" + # CI-CONFLICT-01: branches must not commit the generated HQ snapshot or + # .pa/ACTIVITY.md - GitHub/Mergify merge without our merge driver, so each + # such PR conflicts with every other one. The self-test proves it can fail. + "hotspot-guard|prepush,branchpush,linux|.|bash scripts/ci/hotspot-guard.sh" + "selftest-hotspot-guard|prepush,linux,release|.|bash scripts/test-hotspot-guard.sh" "architecture-drift|precommit,prepush,linux|.|node --test scripts/ci/architecture-drift.test.mjs && node scripts/ci/architecture-drift.mjs check" # CI-06: the trailers of the new commits against origin/main, with the very diff --git a/scripts/ci/hotspot-guard.sh b/scripts/ci/hotspot-guard.sh new file mode 100755 index 00000000..83bfdb34 --- /dev/null +++ b/scripts/ci/hotspot-guard.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# hotspot-guard (CI-CONFLICT-01): a branch must not change the generated HQ +# snapshot or the shared journal. Rules and reasons: scripts/lib/hotspots.sh. +# +# Env: HOTSPOT_BRANCH (branch name override), HOTSPOT_BASE (base commit; the +# default is the merge-base with origin/main - no base is a failure, not a pass). +set -uo pipefail + +ROOT="$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)" +# shellcheck source=scripts/lib/hotspots.sh +. "$ROOT/scripts/lib/hotspots.sh" +cd "$ROOT" || exit 1 + +branch="$(hs_branch "$ROOT")" +if [ -n "$branch" ] && hs_may_touch "$branch"; then + echo "hotspot-guard: branch '$branch' may change the generated files - nothing to check" + exit 0 +fi + +base="${HOTSPOT_BASE:-$(git merge-base origin/main HEAD 2> /dev/null)}" +if [ -z "$base" ]; then + echo "hotspot-guard: no base commit (git fetch origin, or set HOTSPOT_BASE)" >&2 + exit 1 +fi + +hit="$(git diff --name-only "$base" HEAD -- "${HS_FILES[@]}")" || exit 1 +if [ -z "$hit" ]; then + echo "hotspot-guard: ok - no hotspot file changed on '${branch:-detached HEAD}'" + exit 0 +fi + +echo "hotspot-guard: branch '${branch:-detached HEAD}' changes files that conflict across PRs:" >&2 +printf ' %s\n' $hit >&2 +echo "These are written on main only (AGENTS.md, Checks). Drop the change and keep the branch:" >&2 +echo " git checkout $base -- $(printf '%s ' $hit)" >&2 +echo " git commit -m 'chore: drop generated hotspot files'" >&2 +echo "A journal note belongs in the PR text; the snapshot is refreshed by the coordinator on a hq-snapshot branch." >&2 +exit 1 diff --git a/scripts/lib/hotspots.sh b/scripts/lib/hotspots.sh new file mode 100755 index 00000000..2abfaf01 --- /dev/null +++ b/scripts/lib/hotspots.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Shared rules for the merge-conflict hotspots (CI-CONFLICT-01). Functions +# only - nothing runs when sourced. +# +# GitHub and Mergify compute mergeability without our local merge driver, so +# every branch that commits one of these files re-conflicts with every other +# one the moment the first lands. Only main and a dedicated snapshot branch +# (`/hq-snapshot...`, made by the coordinator) may commit them. + +HS_FILES=(docs/dev-hq/data.js docs/dev-hq/data.json .pa/ACTIVITY.md) + +# Branch name to judge: explicit override, then the CI refs, then git. Empty +# (detached HEAD, nothing known) is not a branch that may touch the files. +hs_branch() { # [root] + if [ -n "${HOTSPOT_BRANCH:-}" ]; then + printf '%s' "$HOTSPOT_BRANCH" + elif [ -n "${GITHUB_HEAD_REF:-}" ]; then + printf '%s' "$GITHUB_HEAD_REF" + elif [ "${GITHUB_ACTIONS:-}" = "true" ] && [ -n "${GITHUB_REF_NAME:-}" ]; then + printf '%s' "$GITHUB_REF_NAME" + else + git -C "${1:-.}" branch --show-current 2> /dev/null + fi +} + +hs_may_touch() { # branch + case "$1" in + main | master | gh-readonly-queue/* | hq-snapshot* | */hq-snapshot*) return 0 ;; + esac + return 1 +} diff --git a/scripts/lib/hq-post-merge.test.mjs b/scripts/lib/hq-post-merge.test.mjs index abcb745a..5406fa2e 100644 --- a/scripts/lib/hq-post-merge.test.mjs +++ b/scripts/lib/hq-post-merge.test.mjs @@ -27,6 +27,8 @@ function fixture(t) { mkdirSync(join(dir, 'docs/dev-hq'), { recursive: true }); copyFileSync(join(root, '.githooks/post-merge'), join(dir, '.githooks/post-merge')); copyFileSync(join(root, '.githooks/merge-hqdata'), join(dir, '.githooks/merge-hqdata')); + mkdirSync(join(dir, 'scripts/lib')); + copyFileSync(join(root, 'scripts/lib/hotspots.sh'), join(dir, 'scripts/lib/hotspots.sh')); writeFileSync(join(dir, 'STAND.md'), 'base'); writeFileSync(join(dir, 'scripts/dev-hq.mjs'), ` import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; @@ -135,3 +137,14 @@ test('post-merge retains lessons with the real HQ generator', t => { assert.ok(actual.lessons.length > 0); assert.equal(actual.lessons.length, Array.isArray(source) ? source.length : source.lessons.length); }); + +test('post-merge leaves the snapshot alone on a non-main branch', t => { + const f = fixture(t); + f.git('checkout', '-qb', 'claude/some-work'); + writeFileSync(join(f.dir, 'STAND.md'), 'changed source'); + f.git('commit', '-qam', 'change source'); + const before = readFileSync(join(f.dir, 'docs/dev-hq/data.json'), 'utf8'); + f.hook(); + assert.equal(readFileSync(join(f.dir, 'docs/dev-hq/data.json'), 'utf8'), before); + assert.equal(f.git('status', '--porcelain').trim(), ''); +}); diff --git a/scripts/session-end-hook.sh b/scripts/session-end-hook.sh index 0ac1d5cc..282a0bc6 100755 --- a/scripts/session-end-hook.sh +++ b/scripts/session-end-hook.sh @@ -20,7 +20,7 @@ instanz="${1:-agent}" root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0 [ -f "$root/scripts/sync.sh" ] || exit 0 -act="$root/.pa/ACTIVITY.md" +act="$(bash "$root/scripts/sync.sh" where 2>/dev/null)" || exit 0 if [ -f "$act" ]; then alter=$(( $(date +%s) - $(stat -c %Y "$act" 2>/dev/null || echo 0) )) [ "$alter" -lt 1800 ] && exit 0 diff --git a/scripts/sync.sh b/scripts/sync.sh index 017abfea..78a3f3be 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -9,6 +9,10 @@ # Hängt einen Aktivitäts-Eintrag an .pa/ACTIVITY.md an. Zeitstempel, # Branch und Uncommitted-Liste werden automatisch aus git ermittelt. # Neue Top-Level-Dateien/Ordner in der Zusammenfassung namentlich nennen. +# Nur auf main (und hq-snapshot-Zweigen) landet der Eintrag in der +# getrackten Datei; auf jedem anderen Branch in .pa/ACTIVITY.local.md +# (ungetrackt) - die getrackte Datei ist ein Konfliktherd (CI-CONFLICT-01). +# Die Zusammenfassung gehoert dort in den PR-Text. set -u @@ -17,12 +21,17 @@ if [ -z "$ROOT" ]; then echo "sync.sh: nicht in einem git-Repository" >&2 exit 1 fi -ACTIVITY="$ROOT/.pa/ACTIVITY.md" +# shellcheck source=scripts/lib/hotspots.sh +. "$ROOT/scripts/lib/hotspots.sh" +TRACKED="$ROOT/.pa/ACTIVITY.md" +ACTIVITY="$TRACKED" +branch="$(hs_branch "$ROOT")" +hs_may_touch "$branch" || ACTIVITY="$ROOT/.pa/ACTIVITY.local.md" print_activity_tail() { - if [ -f "$ACTIVITY" ]; then + if [ -f "$TRACKED" ]; then echo "== .pa/ACTIVITY.md (letzte Einträge) ==" - tail -n 40 "$ACTIVITY" + tail -n 40 "$TRACKED" else echo "== .pa/ACTIVITY.md existiert noch nicht ==" fi @@ -109,10 +118,18 @@ case "$cmd" in fi } >> "$ACTIVITY" echo "Eintrag an $ACTIVITY angehängt." + if [ "$ACTIVITY" != "$TRACKED" ]; then + echo "Hinweis: Branch '${branch:-detached}' darf .pa/ACTIVITY.md nicht committen (Konfliktherd)." + echo " Der Eintrag liegt in der ungetrackten Datei .pa/ACTIVITY.local.md; die Zusammenfassung gehört in den PR-Text." + fi + ;; + + where) + echo "$ACTIVITY" ;; *) - echo "Aufruf: bash scripts/sync.sh start|note" >&2 + echo "Aufruf: bash scripts/sync.sh start|note|where" >&2 exit 1 ;; esac diff --git a/scripts/test-hotspot-guard.sh b/scripts/test-hotspot-guard.sh new file mode 100755 index 00000000..d5e959fa --- /dev/null +++ b/scripts/test-hotspot-guard.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Self-test for CI-CONFLICT-01: branches must not commit the generated HQ +# snapshot or .pa/ACTIVITY.md (scripts/ci/hotspot-guard.sh, scripts/sync.sh). +# Each case runs in a fresh temp repo; passing cases print `ok `. +set -uo pipefail + +HERE="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +tmp="$(mktemp -d "${TMPDIR:-/tmp}/hotspot-guard.XXXXXX")" +trap 'rm -rf "$tmp"' EXIT +fails=0 + +pass() { echo "ok $1"; } +fail() { echo "FEHLER $1"; fails=$((fails + 1)); } + +# A repo that carries the scripts under test, one base commit on main. +repo() { # name + local d="$tmp/$1" + mkdir -p "$d/scripts/ci" "$d/scripts/lib" "$d/docs/dev-hq" "$d/.pa" + cp "$HERE/scripts/sync.sh" "$d/scripts/" + cp "$HERE/scripts/ci/hotspot-guard.sh" "$d/scripts/ci/" 2> /dev/null + cp "$HERE/scripts/lib/hotspots.sh" "$d/scripts/lib/" 2> /dev/null + ( + cd "$d" || exit 1 + git init -q -b main . && git config user.email t@t && git config user.name t + printf 'a\n' > docs/dev-hq/data.js; printf '{}\n' > docs/dev-hq/data.json + printf '# ACTIVITY\n' > .pa/ACTIVITY.md; printf 'x\n' > other.txt + git add -A && git commit -qm base + ) + echo "$d" +} + +guard() { # dir -> exit code of the guard in dir (CI env stripped) + (cd "$1" && env -u GITHUB_ACTIONS -u GITHUB_HEAD_REF -u GITHUB_REF_NAME -u HOTSPOT_BRANCH \ + HOTSPOT_BASE="$(git rev-parse main)" bash scripts/ci/hotspot-guard.sh > "$1/out" 2>&1) +} + +# branch-with-change : commit a change to on +branch_change() { + local d + d="$(repo "$1")" + (cd "$d" && git checkout -q -b "$2" && printf 'changed\n' >> "$3" && git commit -qam change) + echo "$d" +} + +for f in docs/dev-hq/data.js docs/dev-hq/data.json .pa/ACTIVITY.md; do + d="$(branch_change "rej-$(basename "$f")" claude/some-work "$f")" + if guard "$d"; then fail "guard rejects package branch touching $f"; else + grep -q "$f" "$d/out" && grep -q "git checkout" "$d/out" && pass "guard rejects package branch touching $f" || + fail "guard rejects package branch touching $f (message lacks file or fix)" + fi +done + +d="$(branch_change other claude/some-work other.txt)" +guard "$d" && pass "guard accepts branch touching other files only" || fail "guard accepts branch touching other files only" + +d="$(repo mainc)" +(cd "$d" && printf 'n\n' >> .pa/ACTIVITY.md && git commit -qam note) +(cd "$d" && env -u GITHUB_ACTIONS -u GITHUB_HEAD_REF HOTSPOT_BASE="$(git rev-parse HEAD~1)" bash scripts/ci/hotspot-guard.sh > out 2>&1) && + pass "guard accepts main touching the hotspot files" || fail "guard accepts main touching the hotspot files" + +d="$(branch_change snap claude/hq-snapshot-refresh docs/dev-hq/data.json)" +guard "$d" && pass "guard accepts a dedicated hq-snapshot branch" || fail "guard accepts a dedicated hq-snapshot branch" + +d="$(branch_change nobase claude/some-work docs/dev-hq/data.js)" +(cd "$d" && env -u HOTSPOT_BASE -u GITHUB_ACTIONS -u GITHUB_HEAD_REF bash scripts/ci/hotspot-guard.sh > out 2>&1) && + fail "guard fails closed without a base" || pass "guard fails closed without a base" + +# sync.sh note: on a branch the tracked journal stays untouched. +d="$(repo note-branch)" +(cd "$d" && git checkout -q -b claude/some-work) +out="$(cd "$d" && env -u HOTSPOT_BRANCH bash scripts/sync.sh note test "summary" 2>&1)" +if [ -z "$(git -C "$d" diff --name-only -- .pa/ACTIVITY.md)" ] && grep -q summary "$d/.pa/ACTIVITY.local.md" 2> /dev/null && + printf '%s' "$out" | grep -q "ACTIVITY.local.md"; then + pass "sync note on a package branch leaves the tracked ACTIVITY.md alone" +else + fail "sync note on a package branch leaves the tracked ACTIVITY.md alone" +fi + +d="$(repo note-main)" +(cd "$d" && env -u HOTSPOT_BRANCH bash scripts/sync.sh note test "summary" > /dev/null 2>&1) +grep -q summary "$d/.pa/ACTIVITY.md" && pass "sync note on main appends to the tracked ACTIVITY.md" || + fail "sync note on main appends to the tracked ACTIVITY.md" + +[ "$fails" -eq 0 ] || { echo "$fails Fehler"; exit 1; } +echo "alle Hotspot-Faelle ok" From 5491ddda418146032445630f912dfe553457de73 Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:45:08 +0200 Subject: [PATCH 2/5] test: isolate hotspot self-test from CI branch env vars The sync note cases inherited GITHUB_HEAD_REF in CI, so the main case saw the PR branch. No-Test: fixes the self-test itself; verified with GITHUB_ACTIONS and GITHUB_HEAD_REF set Co-Authored-By: Claude Sonnet 5.5 --- scripts/test-hotspot-guard.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test-hotspot-guard.sh b/scripts/test-hotspot-guard.sh index d5e959fa..e8f445c6 100755 --- a/scripts/test-hotspot-guard.sh +++ b/scripts/test-hotspot-guard.sh @@ -68,7 +68,7 @@ d="$(branch_change nobase claude/some-work docs/dev-hq/data.js)" # sync.sh note: on a branch the tracked journal stays untouched. d="$(repo note-branch)" (cd "$d" && git checkout -q -b claude/some-work) -out="$(cd "$d" && env -u HOTSPOT_BRANCH bash scripts/sync.sh note test "summary" 2>&1)" +out="$(cd "$d" && env -u HOTSPOT_BRANCH -u GITHUB_ACTIONS -u GITHUB_HEAD_REF bash scripts/sync.sh note test "summary" 2>&1)" if [ -z "$(git -C "$d" diff --name-only -- .pa/ACTIVITY.md)" ] && grep -q summary "$d/.pa/ACTIVITY.local.md" 2> /dev/null && printf '%s' "$out" | grep -q "ACTIVITY.local.md"; then pass "sync note on a package branch leaves the tracked ACTIVITY.md alone" @@ -77,7 +77,7 @@ else fi d="$(repo note-main)" -(cd "$d" && env -u HOTSPOT_BRANCH bash scripts/sync.sh note test "summary" > /dev/null 2>&1) +(cd "$d" && env -u HOTSPOT_BRANCH -u GITHUB_ACTIONS -u GITHUB_HEAD_REF bash scripts/sync.sh note test "summary" > /dev/null 2>&1) grep -q summary "$d/.pa/ACTIVITY.md" && pass "sync note on main appends to the tracked ACTIVITY.md" || fail "sync note on main appends to the tracked ACTIVITY.md" From a3bc472e162160ff7d423a03b26a0c331554ceaf Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:06:23 +0200 Subject: [PATCH 3/5] test: isolate hq post-merge tests from CI branch env vars The hook reads GITHUB_HEAD_REF first, so in CI the fixture repos were judged as the PR branch and the hook skipped regeneration. No-Test: fixes the test harness itself; red with GITHUB_ACTIONS=true GITHUB_HEAD_REF=claude/x (5 failed), green with and without (7 passed) Co-Authored-By: Claude Sonnet 5.5 --- scripts/lib/hq-post-merge.test.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/lib/hq-post-merge.test.mjs b/scripts/lib/hq-post-merge.test.mjs index 5406fa2e..4c1b56bc 100644 --- a/scripts/lib/hq-post-merge.test.mjs +++ b/scripts/lib/hq-post-merge.test.mjs @@ -7,13 +7,17 @@ import { spawnSync } from 'node:child_process'; const root = resolve(import.meta.dirname, '../..'); const bash = process.platform === 'win32' ? 'C:/Program Files/Git/bin/bash.exe' : 'bash'; +// The hook judges the branch from CI refs first; the fixture repo must not see them. +function cleanEnv() { + const env = { ...process.env }; + for (const name of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR', 'HOTSPOT_BRANCH', 'GITHUB_ACTIONS', 'GITHUB_HEAD_REF', 'GITHUB_REF_NAME']) delete env[name]; + return env; +} function fixture(t) { const dir = mkdtempSync(join(tmpdir(), 'hq-post-merge-')); t.after(() => rmSync(dir, { recursive: true, force: true })); function run(exe, args) { - const env = { ...process.env }; - for (const name of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR']) delete env[name]; - const result = spawnSync(exe, args, { cwd: dir, env, encoding: 'utf8' }); + const result = spawnSync(exe, args, { cwd: dir, env: cleanEnv(), encoding: 'utf8' }); assert.equal(result.status, 0, result.stdout + result.stderr); return result.stdout + result.stderr; } @@ -117,7 +121,7 @@ test('post-merge generator failure cannot publish a partial pair', t => { writeFileSync(join(process.argv[process.argv.indexOf('--out') + 1], 'data.json'), 'partial'); process.exit(1); `); - const result = spawnSync(bash, ['.githooks/post-merge'], {cwd: f.dir, encoding: 'utf8'}); + const result = spawnSync(bash, ['.githooks/post-merge'], {cwd: f.dir, env: cleanEnv(), encoding: 'utf8'}); assert.equal(result.status, 1, result.stdout + result.stderr); assert.equal(readFileSync(join(f.dir, 'docs/dev-hq/data.json'), 'utf8'), before); }); From 4b626f3182c01d3c9522c109b6bce4d72d0d44ca Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:23:14 +0200 Subject: [PATCH 4/5] test(ci): cover Mergify hotspot queue branches Test-First: scripts/test-hotspot-guard.sh::guard accepts a Mergify merge queue branch --- scripts/test-hotspot-guard.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/test-hotspot-guard.sh b/scripts/test-hotspot-guard.sh index e8f445c6..070eb966 100755 --- a/scripts/test-hotspot-guard.sh +++ b/scripts/test-hotspot-guard.sh @@ -61,6 +61,9 @@ d="$(repo mainc)" d="$(branch_change snap claude/hq-snapshot-refresh docs/dev-hq/data.json)" guard "$d" && pass "guard accepts a dedicated hq-snapshot branch" || fail "guard accepts a dedicated hq-snapshot branch" +d="$(branch_change queue mergify/merge-queue/0123456789 docs/dev-hq/data.json)" +guard "$d" && pass "guard accepts a Mergify merge queue branch" || fail "guard accepts a Mergify merge queue branch" + d="$(branch_change nobase claude/some-work docs/dev-hq/data.js)" (cd "$d" && env -u HOTSPOT_BASE -u GITHUB_ACTIONS -u GITHUB_HEAD_REF bash scripts/ci/hotspot-guard.sh > out 2>&1) && fail "guard fails closed without a base" || pass "guard fails closed without a base" From f5ce8aa99f13024478bfe9046454019fb226523a Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:24:23 +0200 Subject: [PATCH 5/5] fix(ci): allow Mergify hotspot queue branches Test-First: scripts/test-hotspot-guard.sh::guard accepts a Mergify merge queue branch --- scripts/lib/hotspots.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/lib/hotspots.sh b/scripts/lib/hotspots.sh index 2abfaf01..0b7735af 100755 --- a/scripts/lib/hotspots.sh +++ b/scripts/lib/hotspots.sh @@ -5,7 +5,8 @@ # GitHub and Mergify compute mergeability without our local merge driver, so # every branch that commits one of these files re-conflicts with every other # one the moment the first lands. Only main and a dedicated snapshot branch -# (`/hq-snapshot...`, made by the coordinator) may commit them. +# (`/hq-snapshot...`, made by the coordinator) may commit them. Queue +# branches may carry such a commit while Mergify validates it before merging. HS_FILES=(docs/dev-hq/data.js docs/dev-hq/data.json .pa/ACTIVITY.md) @@ -25,7 +26,7 @@ hs_branch() { # [root] hs_may_touch() { # branch case "$1" in - main | master | gh-readonly-queue/* | hq-snapshot* | */hq-snapshot*) return 0 ;; + main | master | gh-readonly-queue/* | mergify/merge-queue/* | hq-snapshot* | */hq-snapshot*) return 0 ;; esac return 1 }