From 07d912e64565500144b139b9f6cdb2f5e8f85f7e Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 21 Apr 2026 08:44:18 -0400 Subject: [PATCH 1/2] Reshape HB-001 to org-migration + CodeQL path-gate for docs-only PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HB-001 (HUMAN-BACKLOG): rewritten from admin-toggle ask to durable org-migration intent — AceHack/Zeta → Lucent-Financial-Group/Zeta — with Aaron's four 2026-04-21 quotes, preserve-all-settings constraint, public-from-start, no deadline, interim "accept rebase-tax" policy. Added top-of-file "Name attribution — explicit carve-out" section documenting the narrow exception to the BP no-names rule for the per-addressee For: sub-tables. docs/research/parallel-worktree-safety-2026-04-22.md §10.3: corrected resolution-path block to platform-gate diagnosis (gh api /users/AceHack --jq '.type' == "User" — merge queue is org-only at the platform level, not a public-beta quirk). Removes stale "no HB-001 filing needed" claim. .github/workflows/codeql.yml: added path-gate job so docs-only PRs satisfy the newly-enabled "Require code scanning results" ruleset rule. Path-gate runs git diff base...head over code-scanned paths (src/, test/, *.cs/fs, .github/workflows, .github/codeql, tools/setup); on docs-only PRs uploads two empty-SARIF files (actions + csharp) via github/codeql-action/upload-sarif so the CodeQL aggregate check records SUCCESS (analysis ran, zero alerts) rather than NEUTRAL (no results → fail under the new rule). Analyze matrix now gates on needs.path-gate.outputs.code_changed == 'true'. Push/schedule events always fall through to full analysis (safe default). Shell uses set -uo pipefail + decide_fallback_true() so any error opens the gate to full analysis — false-positive (extra run) beats false-negative (silent skip). --- .github/workflows/codeql.yml | 145 ++++++++++++++++++ docs/HUMAN-BACKLOG.md | 32 +++- .../parallel-worktree-safety-2026-04-22.md | 78 +++++++--- 3 files changed, 228 insertions(+), 27 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 33d91518..bbc09e3a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -34,6 +34,17 @@ # 6. `concurrency: cancel-in-progress` cuts stale runs on # force-push; 30-minute timeout caps the CodeQL DB-build # tail which is a known flaky source. +# 7. `path-gate` job runs before analyze. It checks whether +# any code-scanned paths changed; if not (pure docs / +# memory / .claude PR), it uploads a minimal +# no-findings SARIF to the code-scanning service and +# the `analyze` matrix is skipped. This keeps the +# repository-level `code_scanning` ruleset rule +# ("Require code scanning results") satisfied on every +# PR — docs-only PRs no longer NEUTRAL the aggregated +# `CodeQL` status check, which the rule interprets as +# "no results" and blocks on. Code-touching PRs still +# run the full analysis as before. # # SECURITY NOTE on `${{ ... }}` expressions # ------------------------------------------ @@ -72,8 +83,142 @@ concurrency: cancel-in-progress: true jobs: + # Fast pre-check: decide whether any code-scanned paths + # actually changed in this PR / merge-group. On pure docs / + # memory / .claude-only PRs we short-circuit to uploading a + # minimal no-findings SARIF so the `code_scanning` ruleset + # rule sees "results" and does not block on NEUTRAL. + # + # Push and schedule events always fall through to the full + # analyze matrix - those runs establish the baseline for the + # default branch. + path-gate: + name: Path gate + runs-on: ubuntu-22.04 + permissions: + contents: read + security-events: write + outputs: + code_changed: ${{ steps.decide.outputs.code_changed }} + steps: + - name: Checkout (full history for diff) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Decide whether code paths changed + id: decide + shell: bash + env: + GH_EVENT: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + MG_BASE: ${{ github.event.merge_group.base_sha }} + MG_HEAD: ${{ github.event.merge_group.head_sha }} + run: | + set -uo pipefail + # Safe default: when unsure, run full analysis. + decide_fallback_true() { + echo "code_changed=true" >> "$GITHUB_OUTPUT" + exit 0 + } + # push and schedule always run full analysis. + if [ "${GH_EVENT}" = "push" ] || [ "${GH_EVENT}" = "schedule" ]; then + decide_fallback_true + fi + if [ "${GH_EVENT}" = "pull_request" ]; then + base="${BASE_SHA}"; head="${HEAD_SHA}" + else + base="${MG_BASE}"; head="${MG_HEAD}" + fi + if [ -z "${base}" ] || [ -z "${head}" ]; then + decide_fallback_true + fi + # Ensure base ref is in history (shallow fetches may miss it). + git cat-file -e "${base}^{commit}" 2>/dev/null || { + git fetch --no-tags --depth=200 origin "${base}" 2>/dev/null || true + } + if ! changed=$(git diff --name-only "${base}...${head}" 2>/dev/null); then + decide_fallback_true + fi + # Code-scanned paths: C#/F# sources, project files, + # build props, the GitHub Actions workflows that + # CodeQL's `actions` language leg covers, the + # CodeQL workflow/config themselves (a change to + # either MUST rerun the real analysis), and the + # toolchain install script (affects the csharp build). + # + # Note: `case` pattern `*` matches across `/` in bash, + # so `src/*` covers any depth under `src/`. + code_changed=false + while IFS= read -r f; do + [ -z "${f}" ] && continue + case "${f}" in + src/*|test/*) code_changed=true ;; + *.cs|*.fs|*.fsproj|*.csproj|*.sln) code_changed=true ;; + Directory.Build.props|Directory.Packages.props) code_changed=true ;; + .github/workflows/*|.github/codeql/*) code_changed=true ;; + tools/setup/*) code_changed=true ;; + esac + [ "${code_changed}" = "true" ] && break + done <<< "${changed}" + echo "code_changed=${code_changed}" >> "$GITHUB_OUTPUT" + + # Docs-only branch: synthesise a minimal empty SARIF and + # upload it under each language category so GitHub code + # scanning records "analysis ran, no new alerts" (SUCCESS + # aggregate check) rather than NEUTRAL. The rule is + # satisfied; the repo stays honest about what was analysed. + - name: Emit no-findings SARIF (docs-only PRs) + if: steps.decide.outputs.code_changed != 'true' + env: + HEAD_SHA_ENV: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }} + run: | + set -euo pipefail + mkdir -p sarif + for lang in actions csharp; do + cat > "sarif/empty-${lang}.sarif" <`, etc.), + plus direct quotations in `Source` or `Ask` fields where + redaction would lose evidential value. +- **Out of scope for names:** any prose *outside* the row + schema in this file (e.g. the intro, filing rules, prior + art). Those paragraphs continue to use role-refs. Other + docs / skills / code still follow the BP unchanged. + +This carve-out is also recorded in +`memory/feedback_maintainer_name_redaction.md` (exempt-surfaces +list) so the doc-set and the agent memory stay consistent. + ## Rows Rows are grouped by `For:` addressee so a given human can @@ -204,10 +231,7 @@ are ordered by `State: Open` first, then `Stale`, then | ID | When | Category | Ask | Source | State | Resolution | |---|---|---|---|---|---|---| - -*(Aaron currently has no open asks — the 2026-04-20 pm -signoff released all previously gated items. When new -asks arise for Aaron, they land in this sub-table.)* +| HB-001 | 2026-04-21 | decision / org-migration | Plan + execute the migration of `AceHack/Zeta` → `Lucent-Financial-Group/Zeta` (Aaron's LFG umbrella org — `project_lucent_financial_group_external_umbrella.md`). Drivers: (a) GitHub gates merge queue and other org-level features to organization-owned repos — user-owned repos cannot enable merge queue on any plan tier, which is the real blocker behind the `422 Invalid rule 'merge_queue':` failure against `POST /repos/AceHack/Zeta/rulesets` (see §10.3 of `docs/research/parallel-worktree-safety-2026-04-22.md`); (b) aligns the repo with Aaron's stated destination for external contributors. **Constraints (Aaron 2026-04-21):** (1) **preserve all current settings** — rulesets, required checks (gate + CodeQL + semgrep), branch-protection behaviours, auto-delete-head-branch, auto-merge, Dependabot, CodeScanning, Copilot Code Review, concurrency groups, workflow triggers incl. `merge_group:`; (2) **public from the start** at the new location — no private-during-transition staging period. No deadline — "at some point". Until transferred, the factory accepts the rebase-tax on serial PRs and relies on `gh pr merge --auto --squash` alone (merge queue off). | `docs/research/parallel-worktree-safety-2026-04-22.md` §10.3; session transcript 2026-04-21 (Aaron: "we can move tih to https://github.com/Lucent-Financial-Group at some point it's my org for LFG" + "we need to move it to lucent for contributor at some point anyways, we want to keep all the settings we have now" + "i think we are going to have to go without merge queue parallelism for now" + "we can just make it public from the start") | Open | — | ### For: `any` (any human contributor) diff --git a/docs/research/parallel-worktree-safety-2026-04-22.md b/docs/research/parallel-worktree-safety-2026-04-22.md index 67868d4c..afe7f8b3 100644 --- a/docs/research/parallel-worktree-safety-2026-04-22.md +++ b/docs/research/parallel-worktree-safety-2026-04-22.md @@ -342,11 +342,18 @@ agents both editing `docs/BACKLOG.md` in parallel worktrees still produce a merge conflict at queue time; the queue just makes the firing deterministic. -### 10.3 Action — enable merge queue (2026-04-22, direct) +### 10.3 Action — merge queue prerequisites + platform gate Aaron 2026-04-22: *"i'm the admin you can toggle it all you want"* — -standing permission granted; no HB-001 filing needed, admin toggles -done inline. Discovered state and actions: +standing permission for repo-settings toggles was granted and used +for the workflow-trigger prerequisite (landed inline via PR #41). +Enabling merge queue *itself*, however, turned out to be +platform-gated in a way the permission cannot reach — see the +"Merge queue ruleset config" block below. The durable resolution +moved out of the admin-toggle surface onto the org-migration +surface, filed as `HB-001` in `docs/HUMAN-BACKLOG.md`. + +Discovered state and actions taken: **Already on (no action needed):** @@ -367,26 +374,51 @@ listen to `merge_group` events, or merges deadlock. Added (all six required checks) and `.github/workflows/codeql.yml` (best- effort though not required). Landed as PR #41 to main. -**Merge queue ruleset config (2026-04-22 landed):** - -Created repository ruleset "Merge queue for main": - -- Target: `~DEFAULT_BRANCH` (`main`). -- Rule type: `merge_queue`. -- `merge_method: SQUASH` — matches current `gh pr merge --squash` - convention. -- `grouping_strategy: ALLGREEN` — safer than `HEADGREEN`; a failing - PR in the group fails the whole group rather than partially merging. -- `min_entries_to_merge: 1`, `max_entries_to_merge: 5`, - `min_entries_to_merge_wait_minutes: 5`, - `max_entries_to_merge_wait_minutes: 60`, - `max_entries_to_build: 5`, - `check_response_timeout_minutes: 60` — conservative defaults; - revisit after observing a month of queue traffic. -- Required checks stay the same six classic-branch-protection - requires; the queue re-runs them against the merge-group branch. - -Once both are enabled, the agent convention shifts from: +**Merge queue ruleset config — blocked at the platform level (not the API):** + +Attempted to create a repository ruleset "Merge queue for main" +via `POST /repos/AceHack/Zeta/rulesets` targeting `~DEFAULT_BRANCH`. +GitHub returned `422 Validation Failed` with the body +`{"errors":["Invalid rule 'merge_queue': "]}` — empty error detail. +Tried both the original params and the docs-example params +verbatim; same response in both cases. The failure class initially +looked like the public-beta instability reported in +[community discussion #156625](https://github.com/orgs/community/discussions/156625). + +Escalated to the UI path (Settings → Rules → Rulesets → "New +branch ruleset") expecting a ~30-second toggle. Aaron reported the +UI path equally missing: *"so it's missing for me in the UI too, +it's supposed to be there becasue we are public, i also turned up +the settings"*. Diagnosis via `gh api /users/AceHack --jq '.type'` +returned `"User"` — not `"Organization"`. Per GitHub platform +rules, **merge queue is an organization-only feature** regardless +of plan tier or public/private status; user-owned repos cannot +enable it through the UI, the REST API, or any other surface. The +empty-body `422` from the ruleset API is the platform gate, not a +beta-API quirk. + +**Resolution path — HB-001 (org migration).** Filed for Aaron in +`docs/HUMAN-BACKLOG.md`. The durable fix is migrating +`AceHack/Zeta` → `Lucent-Financial-Group/Zeta` (Aaron's LFG +umbrella org, see `memory/persona/project_lucent_financial_group_external_umbrella.md`), +which Aaron has independently flagged as the right home for +external contributors. **Constraint: preserve all current +settings** — rulesets, required checks (gate + CodeQL + semgrep), +auto-delete-head-branch, auto-merge, Dependabot, CodeScanning, +Copilot Code Review, concurrency groups, workflow triggers incl. +`merge_group:`. No deadline ("at some point"); accepted 2026-04-21. + +**Interim policy — skip merge-queue parallelism, accept +rebase-tax.** Per Aaron 2026-04-21: *"i think we are going to have +to go without merge queue parallelism for now."* The factory keeps +the `merge_group:` workflow triggers landed via PR #41 (they are +no-ops until merge queue is enabled, so harmless) and relies on +`gh pr merge --auto --squash` alone. Serial PRs pay the rebase +cost; the structural fix is the org migration, not a workflow +tweak. + +Once the org migration lands and merge queue can be enabled, the +agent convention shifts from: > Open PR → wait for CI → manually `gh pr merge` From efa9ab9ff9dbba9b0c3ae1da8e7b3ed76905712d Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 21 Apr 2026 09:30:15 -0400 Subject: [PATCH 2/2] codeql path-gate: per-language SARIF categories + fork-PR guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two P1 findings from Copilot review on PR #42: 1. Per-language SARIF upload categories. The previous single upload step used a fixed `category: "path-gate-no-code-change"` that overrode the per-language automationDetails.id set in each SARIF file. Result: GitHub code-scanning would see both languages under one generic category, not matching the `analyze` matrix categories (/language:actions/, /language:csharp/). Split into two upload steps so each language keeps its matching category — empty-SARIF now substitutes cleanly for the analyze-job output on docs-only PRs. 2. Fork-PR guard. The synthetic SARIF upload needs `security-events: write`, which GITHUB_TOKEN has downgraded on PRs from forks. Without a guard, docs-only fork PRs would 403 on upload and stay unmergeable when the code_scanning ruleset rule is on. The `decide` step now sets `code_changed=true` on fork PRs, forcing the full `analyze` matrix instead of the short-circuit; CodeQL's analyze has its own fork-PR permission fallback path. Header comment block #7 updated to document both behaviors. --- .github/workflows/codeql.yml | 75 ++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bbc09e3a..a723590f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,14 +37,25 @@ # 7. `path-gate` job runs before analyze. It checks whether # any code-scanned paths changed; if not (pure docs / # memory / .claude PR), it uploads a minimal -# no-findings SARIF to the code-scanning service and -# the `analyze` matrix is skipped. This keeps the -# repository-level `code_scanning` ruleset rule -# ("Require code scanning results") satisfied on every -# PR — docs-only PRs no longer NEUTRAL the aggregated -# `CodeQL` status check, which the rule interprets as -# "no results" and blocks on. Code-touching PRs still -# run the full analysis as before. +# no-findings SARIF per language (one per analyze-matrix +# category: `/language:actions/` and `/language:csharp/`) +# to the code-scanning service and the `analyze` matrix +# is skipped. This keeps the repository-level +# `code_scanning` ruleset rule ("Require code scanning +# results") satisfied on every PR — docs-only PRs no +# longer NEUTRAL the aggregated `CodeQL` status check, +# which the rule interprets as "no results" and blocks +# on. Code-touching PRs still run the full analysis as +# before. +# +# Fork-PR guard: external-contributor PRs get +# downgraded `GITHUB_TOKEN` permissions, so the +# synthetic SARIF upload (needs `security-events: +# write`) would 403 and leave the required check +# unsatisfied. The decide step forces `code_changed=true` +# on fork PRs so they fall through to full `analyze` +# instead of the short-circuit; CodeQL's analyze handles +# fork-PR permissions via its own fallback path. # # SECURITY NOTE on `${{ ... }}` expressions # ------------------------------------------ @@ -115,6 +126,16 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} MG_BASE: ${{ github.event.merge_group.base_sha }} MG_HEAD: ${{ github.event.merge_group.head_sha }} + # Fork-PR detection: on PRs from forks, GITHUB_TOKEN is + # downgraded to read-only for security-events, so the + # synthetic empty-SARIF upload would 403 and leave the + # required check unsatisfied. Force full-analysis path + # on fork PRs so they don't become unmergeable; CodeQL + # analyze handles fork-PR permissions via its own + # fallback. Same-repo PRs use "" == "" (the head repo + # is the base repo). + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + BASE_REPO: ${{ github.repository }} run: | set -uo pipefail # Safe default: when unsure, run full analysis. @@ -126,6 +147,11 @@ jobs: if [ "${GH_EVENT}" = "push" ] || [ "${GH_EVENT}" = "schedule" ]; then decide_fallback_true fi + # Fork-PR guard: skip the short-circuit on external + # contributor PRs (see HEAD_REPO comment above). + if [ "${GH_EVENT}" = "pull_request" ] && [ -n "${HEAD_REPO}" ] && [ "${HEAD_REPO}" != "${BASE_REPO}" ]; then + decide_fallback_true + fi if [ "${GH_EVENT}" = "pull_request" ]; then base="${BASE_SHA}"; head="${HEAD_SHA}" else @@ -164,11 +190,16 @@ jobs: done <<< "${changed}" echo "code_changed=${code_changed}" >> "$GITHUB_OUTPUT" - # Docs-only branch: synthesise a minimal empty SARIF and - # upload it under each language category so GitHub code - # scanning records "analysis ran, no new alerts" (SUCCESS - # aggregate check) rather than NEUTRAL. The rule is - # satisfied; the repo stays honest about what was analysed. + # Docs-only branch: synthesise a minimal empty SARIF per + # language and upload each under the matching analyze-job + # category (/language:actions/ and /language:csharp/) so + # GitHub code scanning records "analysis ran, no new + # alerts" (SUCCESS aggregate check) rather than NEUTRAL. + # Categories must match the `analyze` matrix categories or + # the code-scanning service treats them as unrelated + # configurations. The `category:` action-param overrides + # whatever `automationDetails.id` is in the SARIF, so the + # upload steps below set it explicitly per language. - name: Emit no-findings SARIF (docs-only PRs) if: steps.decide.outputs.code_changed != 'true' env: @@ -208,12 +239,24 @@ jobs: SARIF done - - name: Upload no-findings SARIF + # One upload step per language; each sets `category:` to + # match the analyze-job category for that language. Split + # (rather than a single directory upload) so the `category:` + # action-param -- which overrides SARIF automationDetails -- + # can differ per file. + - name: Upload no-findings SARIF (actions) + if: steps.decide.outputs.code_changed != 'true' + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + with: + sarif_file: sarif/empty-actions.sarif + category: "/language:actions" + + - name: Upload no-findings SARIF (csharp) if: steps.decide.outputs.code_changed != 'true' uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: - sarif_file: sarif - category: "path-gate-no-code-change" + sarif_file: sarif/empty-csharp.sarif + category: "/language:csharp" analyze: name: Analyze (${{ matrix.language }})