Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1179,3 +1179,37 @@ jobs:
fi
fi
echo "No high-severity security issues found"

all-green:
# Single aggregate required context (task reform-native-merge-queue). Collapses
# the five literal required checks (lint/test/typecheck/web-build/vscode-build)
# into one job so the merge queue can't be permanently stalled by a
# required-context-name mismatch. `if: always()` + treating `skipped` as OK
# preserves the fast/slow split: `test-full-shard` is merge_group-only, so it
# is `skipped` on pull_request events and all-green still passes there.
name: all-green
needs: [lint, typecheck, test, test-full-shard, web-build, vscode-build]
if: always()
runs-on: ubuntu-latest
steps:
- name: Aggregate required CI results
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
set -euo pipefail
echo "Aggregated required-job results: $RESULTS"
failed=0
for r in $RESULTS; do
case "$r" in
success | skipped) ;;
Comment on lines +1203 to +1204
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail all-green when required jobs are skipped unexpectedly

Because all-green does not depend on docs_only_filter or post_merge_duplicate_filter, a failure in either setup job causes downstream jobs like lint, typecheck, web-build, and vscode-build to be skipped; this loop then treats every skipped result as acceptable and reports the aggregate check green. Once branch protection is switched to require only all-green, a broken filter/duplicate-detection step can therefore let a PR or merge-group pass without running the required CI jobs. Please either include the setup jobs in the aggregate or only allow skipped for the explicitly expected cases.

Useful? React with 👍 / 👎.

*)
echo "::error::A required CI job concluded '$r'"
failed=1
;;
esac
done
if [ "$failed" -ne 0 ]; then
echo "all-green: FAIL — at least one required CI job did not succeed."
exit 1
fi
echo "all-green: PASS — all required CI jobs succeeded or were skipped."
56 changes: 56 additions & 0 deletions .github/workflows/governance-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: governance-gate

# Server-side governance required check for the native merge queue
# (task reform-native-merge-queue, part 3).
#
# A PR may enter the merge queue only when its linked cc-task is
# governance-releasable: stage >= S7_RELEASE, release_authorized == true, and an
# authority_case present. That decision needs the local Obsidian cc-task vault,
# which GitHub-hosted runners CANNOT read. So this gate verifies the server-side
# PROOF instead: the `hapax/autoqueue-admission` commit status that the
# vault-reading local autoqueue posts on the PR head SHA after it validates the
# task. scripts/queue-admission-proof-check.py asserts that proof is fresh and
# successful for queued / auto-merge PRs.
#
# Scope: the proof is enforced only for enqueued / auto_merge_enabled PRs (and
# merge_group), so ordinary PR CI is never wedged by this gate. It is the
# governance counterpart to pr-admission.yml's freeze governor — kept as a
# distinct, clearly-named context so it can be made a required status check in
# the main-merge-queue ruleset ("enqueueing itself is governed").

on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
- enqueued
- auto_merge_enabled
- auto_merge_disabled
merge_group:
types: [checks_requested]

permissions:
contents: read
pull-requests: read
statuses: read

jobs:
governance-gate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
Comment on lines +43 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin action to commit SHA and disable credential persistence.

The checkout action should be pinned to a specific commit SHA per repository policy and should set persist-credentials: false to prevent credential leakage through artifacts.

🔒 Proposed security fix
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
         with:
           fetch-depth: 1
+          persist-credentials: false

As per coding guidelines and static analysis (zizmor): actions must be pinned to commit hashes per blanket policy, and credentials should not persist to prevent exposure through artifacts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 43-46: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 44-44: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/governance-gate.yml around lines 43 - 46, The Checkout
step currently uses the floating tag actions/checkout@v4 and leaves credential
persistence defaulted; update the Checkout step (the use: actions/checkout@v4
entry) to pin the action to a repository-specific commit SHA (replace the v4 tag
with the approved commit SHA for actions/checkout) and add the with:
persist-credentials: false setting (you can keep fetch-depth: 1) so credentials
are not persisted to the workspace or artifacts.


- name: Verify queue admission governance proof
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
python3 scripts/queue-admission-proof-check.py \
--repo "$REPO" \
--event-path "$GITHUB_EVENT_PATH"
Comment on lines +54 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the proof verifier from a protected revision

This required gate checks out the candidate ref and then executes scripts/queue-admission-proof-check.py from that same checkout, so any PR that changes the verifier can make governance-gate pass without a valid hapax/autoqueue-admission status. For a server-side admission gate that is meant to constrain queued and merge-group PRs, the verifier needs to come from a protected base revision or pinned action rather than from the code being admitted.

Useful? React with 👍 / 👎.

1 change: 1 addition & 0 deletions hooks/gate-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ runtimes:
- vscode-build
- secrets-scan
- security
- all-green
run_markers:
- 'astral-sh/ruff-action@v3'
- 'uv run python scripts/check-unused-functions.py'
Expand Down
37 changes: 25 additions & 12 deletions scripts/cc-pr-autoqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

The merge queue should not depend on a human/session remembering to run
``gh pr merge`` after a governed PR is ready. This reconciler scans open PRs,
matches each PR to a cc-task in the local Obsidian vault, and queues or arms
auto-merge only when Hapax governance and GitHub protection state both pass.
matches each PR to a cc-task in the local Obsidian vault, and ARMS auto-merge
only when Hapax governance and GitHub protection state both pass.

GitHub's current CLI behavior for branches that require a merge queue is the
primitive this script uses: ``gh pr merge`` adds a ready PR to the queue, and
``gh pr merge --auto`` arms auto-merge until required checks/reviews pass.
Arm-only (task reform-native-merge-queue): the sole positive GitHub mutation is
one idempotent ``gh pr merge --auto --squash``. GitHub's native merge queue then
owns batching, speculative ``gh-readonly-queue`` branches, auto-rebase, and
bisect-on-failure — this script no longer issues a direct ``--merge`` or manages
the queue itself, which previously raced GitHub's own batching and stranded PRs.

Usage::

Expand Down Expand Up @@ -114,9 +116,15 @@
# concurrently in flight in a DIFFERENT lane (the actual hazard); same-lane serial
# work is never held, and a deterministic lowest-PR tiebreak keeps two
# different-lane epic PRs from dead-holding each other.
SHARED_FILE_EPIC_PARENT_SPECS = {
# parent_spec basename -> serialized-epic id (the shared file it contends on)
"clog-frontend-elevation-design-2026-06-01.md": "clog-dashboard-lisp",
SHARED_FILE_EPIC_PARENT_SPECS: dict[str, str] = {
# parent_spec basename -> serialized-epic id (the shared file it contends on).
#
# Emptied 2026-06-01 (task reform-native-merge-queue): the native GitHub merge
# queue serializes shared-file contention through its speculative
Comment on lines +119 to +123
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep parent-spec hold for unqueued epic work

This registry is now empty, so existing CLOG tasks that only carry the documented parent_spec no longer participate in shared_file_epic_affinity_blockers unless every note has already been migrated to epic_serialize. The native merge queue can serialize PRs that have reached the queue, but it cannot see an in-progress sibling with no PR yet; in that common case a ready CLOG PR can be admitted while another lane is still editing the same shared file, reintroducing the conflict this hold was designed to prevent.

Useful? React with 👍 / 👎.

# gh-readonly-queue branches (auto-rebase + bisect-on-failure), so a
# pre-admission affinity hold is no longer needed to keep two different-lane
# epic PRs from merge-conflicting. Re-add an entry here only to re-enable the
# local pre-queue hold for a specific shared-file epic.
}
EPIC_INFLIGHT_STATUSES = frozenset(
{"claimed", "in_progress", "pr_open", "in_review", "merge_queue", "ready_for_merge"}
Expand Down Expand Up @@ -899,10 +907,15 @@ def merge_pr(
]
else:
cmd = ["gh", "pr", "merge", str(decision.pr.number), "--repo", repo]
if decision.action == "enable_auto_merge":
cmd.extend(["--auto", "--merge"])
elif decision.action == "queue":
cmd.append("--merge")
if decision.action in ("enable_auto_merge", "queue"):
# Arm-only (task reform-native-merge-queue): the local autoqueue's sole
# positive mutation is to ARM auto-merge with one idempotent command.
# GitHub's native merge queue then owns batching, speculative
# gh-readonly-queue branches, auto-rebase, and bisect-on-failure — we no
# longer issue a direct `--merge` (which raced the queue's own management).
# Re-arming an already-armed PR is a no-op; `--squash` matches the queue's
# configured merge method.
cmd.extend(["--auto", "--squash"])
Comment on lines +910 to +918
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In the official GitHub CLI documentation for gh pr merge, is the --match-head-commit flag supported, and does it prevent merging or enabling auto-merge when the PR head SHA has changed?

💡 Result:

Yes, the --match-head-commit flag is supported in the official GitHub CLI documentation for the gh pr merge command [1][2][3]. This flag requires a commit SHA as an argument, which serves as a safety check: the pull request's head must match the provided SHA to allow the merge to proceed [1][4][2]. If the pull request head SHA has changed (i.e., it does not match the SHA provided), the command will prevent the merge [4][5]. While the documentation notes that the flag ensures the branch has not changed out from under the user [4], its primary function is to block the operation if the specified HEAD condition is not met. It is not explicitly stated in the manual that this flag disables the enabling of auto-merge, but rather that it serves as a constraint to allow or block the merge process based on the current state of the pull request head [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show context around the cited lines
sed -n '860,960p' scripts/cc-pr-autoqueue.py | nl -ba | sed -n '1,140p'

# Find and inspect set_autoqueue_admission_status()
rg -n "def set_autoqueue_admission_status|set_autoqueue_admission_status\(" -S scripts/cc-pr-autoqueue.py
python3 - <<'PY'
import re, itertools, pathlib
path = pathlib.Path("scripts/cc-pr-autoqueue.py")
txt = path.read_text(encoding="utf-8", errors="replace").splitlines()
# locate function start
start = None
for i,line in enumerate(txt):
    if re.match(r"def set_autoqueue_admission_status\s*\(", line):
        start = i
        break
if start is None:
    raise SystemExit("set_autoqueue_admission_status not found")
# print ~200 lines from start
for j in range(start, min(len(txt), start+220)):
    print(f"{j+1:5d}:{txt[j]}")
PY

# Search for gh pr merge invocation / cmd construction near that block
rg -n "gh pr merge|pr merge|--auto|--squash|merge queue" scripts/cc-pr-autoqueue.py

Repository: hapax-systems/hapax-council

Length of output: 114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show context around the cited region without relying on `nl`
echo "=== scripts/cc-pr-autoqueue.py lines 880-960 (approx) ==="
sed -n '880,960p' scripts/cc-pr-autoqueue.py | sed -n '1,120p'

echo
echo "=== Locate set_autoqueue_admission_status definition ==="
rg -n "def set_autoqueue_admission_status\s*\(" scripts/cc-pr-autoqueue.py

echo
echo "=== Print set_autoqueue_admission_status body (first ~240 lines) ==="
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("scripts/cc-pr-autoqueue.py")
lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
start = None
for i,l in enumerate(lines):
    if re.match(r"def set_autoqueue_admission_status\s*\(", l):
        start = i
        break
assert start is not None
end = min(len(lines), start+240)
for idx in range(start, end):
    print(f"{idx+1:5d}:{lines[idx]}")
PY

echo
echo "=== Search for gh pr merge command construction/execution ==="
rg -n "gh pr merge|--auto|--squash|--match-head-commit|subprocess\.run|check_call|Popen" scripts/cc-pr-autoqueue.py

echo
echo "=== Extract the specific if block around cmd.extend([\"--auto\", \"--squash\"]) ==="
rg -n "cmd\.extend\(\[\s*\"--auto\"\s*,\s*\"--squash\"\s*\]\)" -n scripts/cc-pr-autoqueue.py || true
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("scripts/cc-pr-autoqueue.py")
lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
for i,l in enumerate(lines):
    if 'cmd.extend(["--auto", "--squash"])' in l.replace(" ", "") or 'cmd.extend(["--auto", "--squash"])' in l:
        # print ~25 lines around
        s=max(0,i-20); e=min(len(lines), i+25)
        print(f"Match near line {i+1}")
        for j in range(s,e):
            print(f"{j+1:5d}:{lines[j]}")
PY

Repository: hapax-systems/hapax-council

Length of output: 16441


🌐 Web query:

gh pr merge --match-head-commit --auto behavior when PR head changes

💡 Result:

Behavior summary for gh pr merge --match-head-commit <SHA> --auto when the PR head changes: 1) --auto does not “track” the PR head for you. When you run gh pr merge --auto, the CLI only enables auto-merge (or queues it) based on the PR state it reads and on the merge payload it sends at that moment; it doesn’t re-issue the merge request automatically if the PR head commit changes later. This is evidenced by gh pr merge’s documented intent for --auto (“Automatically merge only after necessary requirements are met”) and the implementation discussion where the CLI sets auto-merge based on fetched PR state and then sends that directly through the merge payload without continuing to re-evaluate on later PR updates.[1][2] 2) --match-head-commit is a hard precondition: the head must still match the specified commit SHA. The --match-head-commit SHA flag is explicitly defined as “Commit SHA that the pull request head must match to allow merge”.[1] Therefore, if you specify a head SHA and then the PR head changes (new commits are pushed), GitHub will no longer satisfy that precondition, so the already-configured auto-merge/merge attempt will not proceed for that earlier head SHA. 3) Practical effect: you should expect auto-merge to stall/fail for the prior head SHA, and you must re-run the command (or otherwise update the auto-merge configuration) for the new head. Because the head-matching is required “to allow merge”[1], the common outcome when the PR head diverges is that merge will not be permitted until the configuration matches the latest head again (e.g., by reissuing gh pr merge with --match-head-commit set to the new head SHA). 4) Related nuance: --auto can be backed by merge queue behavior. If the target base branch uses a merge queue, gh pr merge will “enable auto-merge” when required checks have not passed yet, or “add the pull request to the merge queue” when required checks have passed.[1] In either case, --match-head-commit still represents the required head SHA for the merge to be allowed.[1] Bottom line If the PR head changes after you run gh pr merge --match-head-commit <oldSHA> --auto, GitHub will not merge that PR for <oldSHA> because the head no longer matches the required SHA.[1] Re-run gh pr merge --auto --match-head-commit <newSHA> (or omit --match-head-commit if you don’t need this safety gate) so the precondition matches the updated head commit.

Citations:


🌐 Web query:

cli.github.com manual gh pr merge --match-head-commit requires head match fail if head changed

💡 Result:

If the PR head commit changes after you captured the SHA and you run gh pr merge with --match-head-commit , the merge will not proceed because that flag specifies “Commit SHA that the pull request head must match to allow merge” (i.e., it acts as a precondition). The official gh pr merge manual documents the flag semantics but does not describe the exact wording of the error message you’ll see, only that the merge is allowed only when the head matches the provided SHA [1]. References: [1] https://cli.github.com/manual/gh_pr_merge

Citations:


Bind the auto-merge/merge-queue arming command to the classified head SHA.

scripts/cc-pr-autoqueue.py currently enables auto-merge/queue via gh pr merge <pr-number> --auto --squash (keyed only by PR number), while set_autoqueue_admission_status() posts the admission proof to decision.pr.head_sha. If the PR head changes between those steps, the arming request can target a different head; add --match-head-commit <head_sha> so GitHub requires the PR head to still match the proof SHA to allow the operation.

Suggested change
     if decision.action in ("enable_auto_merge", "queue"):
         # Arm-only (task reform-native-merge-queue): the local autoqueue's sole
         # positive mutation is to ARM auto-merge with one idempotent command.
         # GitHub's native merge queue then owns batching, speculative
         # gh-readonly-queue branches, auto-rebase, and bisect-on-failure — we no
         # longer issue a direct `--merge` (which raced the queue's own management).
         # Re-arming an already-armed PR is a no-op; `--squash` matches the queue's
         # configured merge method.
         cmd.extend(["--auto", "--squash"])
+        if decision.pr.head_sha:
+            cmd.extend(["--match-head-commit", decision.pr.head_sha])
     elif decision.action == "disable_auto_merge":
         cmd.append("--disable-auto")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if decision.action in ("enable_auto_merge", "queue"):
# Arm-only (task reform-native-merge-queue): the local autoqueue's sole
# positive mutation is to ARM auto-merge with one idempotent command.
# GitHub's native merge queue then owns batching, speculative
# gh-readonly-queue branches, auto-rebase, and bisect-on-failure — we no
# longer issue a direct `--merge` (which raced the queue's own management).
# Re-arming an already-armed PR is a no-op; `--squash` matches the queue's
# configured merge method.
cmd.extend(["--auto", "--squash"])
if decision.action in ("enable_auto_merge", "queue"):
# Arm-only (task reform-native-merge-queue): the local autoqueue's sole
# positive mutation is to ARM auto-merge with one idempotent command.
# GitHub's native merge queue then owns batching, speculative
# gh-readonly-queue branches, auto-rebase, and bisect-on-failure — we no
# longer issue a direct `--merge` (which raced the queue's own management).
# Re-arming an already-armed PR is a no-op; `--squash` matches the queue's
# configured merge method.
cmd.extend(["--auto", "--squash"])
if decision.pr.head_sha:
cmd.extend(["--match-head-commit", decision.pr.head_sha])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/cc-pr-autoqueue.py` around lines 910 - 918, The arming command for
auto-merge in the block where decision.action is ("enable_auto_merge","queue")
currently extends cmd with ["--auto","--squash"] without binding to the proven
head SHA; update the call site that builds cmd (where cmd.extend is used) to
also append "--match-head-commit" followed by the admission proof SHA from
decision.pr.head_sha (the same SHA used by set_autoqueue_admission_status()) so
GitHub will require the PR head to match the proof before arming.

elif decision.action == "disable_auto_merge":
cmd.append("--disable-auto")
elif decision.action != "dequeue":
Expand Down
Loading
Loading