diff --git a/packages/mosaic/framework/tools/git/pr-merge.sh b/packages/mosaic/framework/tools/git/pr-merge.sh index 89d63bb0..8ae78808 100755 --- a/packages/mosaic/framework/tools/git/pr-merge.sh +++ b/packages/mosaic/framework/tools/git/pr-merge.sh @@ -1,6 +1,6 @@ #!/bin/bash # pr-merge.sh - Merge pull requests on Gitea or GitHub -# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--no-ci-expected] [--co-author-trailers --escalate-to PRINCIPAL] +# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--no-ci-expected] [--base-line BRANCH] [--co-author-trailers --escalate-to PRINCIPAL] set -euo pipefail @@ -30,6 +30,12 @@ Options: -d, --delete-branch Delete the head branch after merge --dry-run Run metadata/login preflight without merging --expect-head SHA Refuse unless the PR head matches this full commit SHA + --base-line BRANCH Documented intra-line exception (B5, ruled + 2026-08-29): authorize a merge whose base is + neither main nor next (stacked PR lines). The + value must MATCH the PR base; all other gates + (queue guard, head pin, CI) still run and the + exception is recorded in the merge audit. --no-ci-expected Assert the target repository has no CI: forward --no-ci-expected to the queue guard (requires repository admin) --co-author-trailers Build verified trailers from linked PR commit authors --escalate-to NAME Named principal for an unresolved-author BLOCK @@ -46,6 +52,7 @@ EOF } # Parse arguments +BASE_LINE_OVERRIDE="" while [[ $# -gt 0 ]]; do case $1 in -n|--number) @@ -64,6 +71,11 @@ while [[ $# -gt 0 ]]; do DRY_RUN=true shift ;; + --base-line) + [[ $# -ge 2 ]] || { echo "Error: --base-line requires a branch name." >&2; exit 1; } + BASE_LINE_OVERRIDE="$2" + shift 2 + ;; --expect-head) if [[ $# -lt 2 ]]; then echo "Error: --expect-head requires one full commit SHA." >&2 @@ -172,8 +184,17 @@ if [[ "$DECL_STATE" == valid && "$DECL_SCHEMA" == 2 ]]; then else repo_decl_warn_absent_irreversible "pr-merge" if [[ "$BASE_BRANCH" != "main" && "$BASE_BRANCH" != "next" ]]; then - echo "Error: Mosaic policy allows merges only for PRs targeting 'main' or 'next' (found '$BASE_BRANCH')." >&2 - exit 1 + if [[ -n "$BASE_LINE_OVERRIDE" && "$BASE_LINE_OVERRIDE" != "$BASE_BRANCH" ]]; then + echo "Error: --base-line '$BASE_LINE_OVERRIDE' does not match the PR base '$BASE_BRANCH' (refusing; the exception must name the real base)." >&2 + exit 1 + fi + if [[ "$BASE_LINE_OVERRIDE" == "$BASE_BRANCH" ]]; then + echo "audit: base-line exception — merge into '$BASE_BRANCH' authorized by explicit --base-line (B5 ruling 2026-08-29); queue guard, head pin, and CI gates unchanged." >&2 + else + echo "Error: Mosaic policy allows merges only for PRs targeting 'main' or 'next' (found '$BASE_BRANCH')." >&2 + echo " A ruled intra-line merge may pass --base-line '$BASE_BRANCH' (same gates; the exception is recorded)." >&2 + exit 1 + fi fi fi if [[ -z "$HEAD_BRANCH" || -z "$HEAD_REPO" || ! "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then diff --git a/packages/mosaic/framework/tools/git/test-pr-merge-baseline.sh b/packages/mosaic/framework/tools/git/test-pr-merge-baseline.sh new file mode 100755 index 00000000..d59aa344 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-merge-baseline.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# B5 (ruled 2026-08-29, orch-01-adopted): pr-merge --base-line — the +# documented intra-line exception. Arms: +# 1. Base neither main nor next, no flag: policy refusal (rc 1), queue +# guard NOT invoked, hint names the exception. +# 2. Base neither main nor next, matching --base-line: authorized; the +# queue guard IS invoked with the same args (rc from the stub proves +# gates still run) and the audit line is emitted. +# 3. Mismatched --base-line (different branch than the PR base): refusal +# rc 1 with the mismatch named; queue guard NOT invoked. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-baseline}" +FIXTURE_DIR="$WORK_DIR/tools/git" +CALL_LOG="$WORK_DIR/queue-call.log" +OUT_LOG="$WORK_DIR/out.log" + +rm -rf "$WORK_DIR" +mkdir -p "$FIXTURE_DIR" +cp "$SCRIPT_DIR/pr-merge.sh" "$FIXTURE_DIR/pr-merge.sh" +cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh" + +cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH' +#!/usr/bin/env bash +printf '%s\n' '{"baseRefName":"mosaic-cli-p0","baseRepository":"mosaicstack/stack","headRefName":"mosaic-cli-p2-socket-res","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"mosaicstack/stack"}' +SH + +cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$*" > "${MOSAIC_QUEUE_CALL_LOG:?}" +exit 42 +SH +chmod +x "$FIXTURE_DIR"/*.sh + +run_case() { # run_case + : > "$CALL_LOG" + set +e + ( + cd "$WORK_DIR" + export MOSAIC_QUEUE_CALL_LOG="$CALL_LOG" + "$FIXTURE_DIR/pr-merge.sh" -n 123 "$@" + ) >"$OUT_LOG" 2>&1 + rc=$? + set -e +} + +# 1. No flag: policy refusal, no gate invocation. +run_case +[[ "$rc" -eq 1 ]] || { echo "FAIL arm1: rc=$rc want 1" >&2; cat "$OUT_LOG" >&2; exit 1; } +[[ ! -s "$CALL_LOG" ]] || { echo "FAIL arm1: queue guard ran without authorization" >&2; exit 1; } +grep -q "only for PRs targeting" "$OUT_LOG" || { echo "FAIL arm1: policy message missing" >&2; exit 1; } +grep -q -- "--base-line 'mosaic-cli-p0'" "$OUT_LOG" || { echo "FAIL arm1: hint missing" >&2; exit 1; } + +# 2. Matching flag: authorized, audit line emitted, gates RUN (stub rc 42). +run_case --base-line mosaic-cli-p0 +[[ "$rc" -eq 42 ]] || { echo "FAIL arm2: rc=$rc want 42 (gate stub rc must propagate)" >&2; cat "$OUT_LOG" >&2; exit 1; } +[[ -s "$CALL_LOG" ]] || { echo "FAIL arm2: queue guard NOT invoked despite authorization" >&2; exit 1; } +grep -q -- '-B mosaic-cli-p2-socket-res' "$CALL_LOG" || { echo "FAIL arm2: guard args wrong" >&2; cat "$CALL_LOG" >&2; exit 1; } +grep -q "base-line exception" "$OUT_LOG" || { echo "FAIL arm2: audit line missing" >&2; exit 1; } + +# 3. Mismatched flag: refusal, mismatch named, no gate invocation. +run_case --base-line some-other-line +[[ "$rc" -eq 1 ]] || { echo "FAIL arm3: rc=$rc want 1" >&2; cat "$OUT_LOG" >&2; exit 1; } +[[ ! -s "$CALL_LOG" ]] || { echo "FAIL arm3: queue guard ran on a refused merge" >&2; exit 1; } +grep -q "does not match the PR base" "$OUT_LOG" || { echo "FAIL arm3: mismatch message missing" >&2; exit 1; } + +echo "pr-merge --base-line exception regression passed (B5)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index b047a4e8..de8f4569 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-milestone-close-usage-contract.sh && bash framework/tools/git/test-milestone-list-usage-contract.sh && bash framework/tools/git/test-issue-view-usage-contract.sh && bash framework/tools/git/test-issue-list-usage-contract.sh && bash framework/tools/git/test-milestone-create-usage-contract.sh && bash framework/tools/git/test-lane-brief-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-baseline.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-milestone-close-usage-contract.sh && bash framework/tools/git/test-milestone-list-usage-contract.sh && bash framework/tools/git/test-issue-view-usage-contract.sh && bash framework/tools/git/test-issue-list-usage-contract.sh && bash framework/tools/git/test-milestone-create-usage-contract.sh && bash framework/tools/git/test-lane-brief-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*",