From 51ae58531f6b2fc7732bee2f7d66461d6367da8e Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 21 Apr 2026 09:50:42 -0400 Subject: [PATCH 1/5] Resolve HB-001: transfer to Lucent-Financial-Group; land GitHub-settings-as-code + drift detector Executes the org migration (AceHack/Zeta -> Lucent-Financial-Group/Zeta) that was filed as HB-001, then lands the hygiene pattern the migration taught us: GitHub has click-ops surfaces with no native declarative config (rulesets, security-and-analysis, Actions variables, Pages, CodeQL setup), and the transfer code path silently flipped secret_scanning + secret_scanning_push_protection from enabled to disabled. Re-enabled same session; built detector so the next silent drift is not silent. Shape of what landed: - docs/GITHUB-SETTINGS.md: human-readable narrative declaration of every click-ops setting, pointing at the machine-readable JSON. - tools/hygiene/github-settings.expected.json: canonical expected state, normalized output of the snapshot script. - tools/hygiene/snapshot-github-settings.sh: produces the normalized JSON from live gh api for any repo (--repo flag). - tools/hygiene/check-github-settings-drift.sh: diffs live vs expected; exit 1 on drift with resolution instructions. - .github/workflows/github-settings-drift.yml: weekly cron (Mondays 14:17 UTC) + workflow_dispatch + PR-triggered when the expected snapshot or tooling changes. Uses only trusted first-party context (secrets.GITHUB_TOKEN, github.repository) passed via env vars into quoted run-block invocation. - docs/FACTORY-HYGIENE.md row #40 with full cadence / owner / checks / output / source-of-truth specification. - docs/HUMAN-BACKLOG.md: HB-001 State=Open -> Resolved with the two-line drift summary baked into the Resolution cell. - docs/ROUND-HISTORY.md: late-Round-44 arc paragraph documenting the migration, the drift, and the new hygiene class. Pattern generalizes: any click-ops platform (AWS / GCP / Slack / org-level settings) gets the same markdown-declaration + cadenced-diff treatment when adopted. Settings-as-code-by- convention beats Terraform / Probot / Pulumi for small repos until friction demands the heavier tooling. Companion memories: feedback_github_settings_as_code_declarative_ checked_in_file.md (the pattern) + feedback_blast_radius_pricing_ standing_rule_alignment_signal.md (Aaron's praise of the confirm-before-hard-to-reverse discipline, reframing it as a Zeta product-feature signal per the retractable-contract ledger). --- .github/workflows/github-settings-drift.yml | 64 ++++++ docs/FACTORY-HYGIENE.md | 1 + docs/GITHUB-SETTINGS.md | 224 +++++++++++++++++++ docs/HUMAN-BACKLOG.md | 2 +- docs/ROUND-HISTORY.md | 36 +++ tools/hygiene/check-github-settings-drift.sh | 73 ++++++ tools/hygiene/github-settings.expected.json | 196 ++++++++++++++++ tools/hygiene/snapshot-github-settings.sh | 118 ++++++++++ 8 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/github-settings-drift.yml create mode 100644 docs/GITHUB-SETTINGS.md create mode 100755 tools/hygiene/check-github-settings-drift.sh create mode 100644 tools/hygiene/github-settings.expected.json create mode 100755 tools/hygiene/snapshot-github-settings.sh diff --git a/.github/workflows/github-settings-drift.yml b/.github/workflows/github-settings-drift.yml new file mode 100644 index 00000000..2a938c24 --- /dev/null +++ b/.github/workflows/github-settings-drift.yml @@ -0,0 +1,64 @@ +name: github-settings-drift + +# Weekly drift detector for GitHub repo settings that live outside the +# declarative-in-tree surface (rulesets, branch protection, Actions +# variables, environments, Pages, CodeQL default-setup state, etc.). +# Compares live `gh api` output against the checked-in expected snapshot +# at tools/hygiene/github-settings.expected.json. Drift blocks this +# workflow; resolve by either reverting the setting in GitHub or +# re-snapshotting and committing the new expected. +# +# Security note (safe-pattern compliance): this workflow only consumes +# first-party trusted context — `secrets.GITHUB_TOKEN` and +# `github.repository` (the owner/repo string). No user-authored +# fields (issue title, PR body, commit message, head_ref, etc.) are +# referenced. Both trusted values are passed via env: into the run +# block and quoted there, matching the recommended safe pattern from +# https://github.blog/security/vulnerability-research/how-to-catch-github-actions-workflow-injections-before-attackers-do/ +# +# See docs/GITHUB-SETTINGS.md + docs/FACTORY-HYGIENE.md row #40. + +on: + schedule: + # Weekly Mondays 14:17 UTC — off the hour to avoid the GHA + # cron thundering-herd. + - cron: "17 14 * * 1" + workflow_dispatch: {} + # Also run on any change to the expected snapshot or the detector + # itself, so a PR that updates expected gets an immediate green + # signal that the snapshot matches reality at merge time. + pull_request: + paths: + - "tools/hygiene/github-settings.expected.json" + - "tools/hygiene/snapshot-github-settings.sh" + - "tools/hygiene/check-github-settings-drift.sh" + - ".github/workflows/github-settings-drift.yml" + +permissions: + contents: read + # Need metadata to read rulesets / branch protection / Actions + # variables via gh api. GITHUB_TOKEN's default permissions are + # sufficient for public-repo settings reads. + actions: read + administration: read + +concurrency: + group: github-settings-drift + cancel-in-progress: false + +jobs: + check: + name: check drift + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: check drift + env: + # Both values below are first-party trusted context. + # No user-authored input is used anywhere in this workflow. + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + tools/hygiene/check-github-settings-drift.sh --repo "$GH_REPO" diff --git a/docs/FACTORY-HYGIENE.md b/docs/FACTORY-HYGIENE.md index b3144b36..42d0616d 100644 --- a/docs/FACTORY-HYGIENE.md +++ b/docs/FACTORY-HYGIENE.md @@ -81,6 +81,7 @@ is never destructive; retiring one requires an ADR in | 36 | Incorrectly-scoped gap-finder (retrospective) | Every 5-10 rounds (proposed — Aaron 2026-04-20 late) | TBD — candidate skill (queued in BACKLOG P1 alongside row 35); may collapse into a single "scope-hygiene" skill pending row-consolidation research | factory | Retrospective sweep for items whose scope tag is **present but wrong** — factory-level rules tagged `project: zeta`, Zeta-specific rules tagged factory, over-broad / under-broad declarations. Distinct from row 35 (missing tag) — this is *tag-exists-but-incorrect*. Trigger examples: Aaron's 2026-04-20 catch of "cheap-but-zeta-specific governance" in the skill-edit-gating-tiers research doc (factory-scope governance mislabeled as Zeta-specific). Flags candidates to HUMAN-BACKLOG as `scope-correction` rows. | Audit doc per round + HB row per mis-scoped item | `feedback_factory_default_scope_unless_db_specific.md` + `project_factory_reuse_beyond_zeta_constraint.md` + `user_absorb_time_filter_always_wanted.md` | | 37 | WIP-limit discipline (Kanban) | Round open + per-persona session-open (always-on, not triggered) | All agents (self-administered) + Architect for cross-persona visibility | factory | Per-persona in-flight work cap (suggested 3: proposals / findings / drafts not yet landed); cross-persona cap (suggested 7 under architect-bottleneck review queue per GOVERNANCE §11). Over-cap flags to HUMAN-BACKLOG as `wip-pressure` rows. Always-on discipline per `docs/FACTORY-METHODOLOGIES.md` pull-vs-always-on criterion (this row is always-on, therefore not a skill). Cap numbers are *suggestions*; tune after 5-10 rounds of observation per Six Sigma Measure → Improve loop. | Inline self-report + HUMAN-BACKLOG row if over-cap + Architect notebook tally column | `user_kanban_six_sigma_process_preference.md` + `docs/FACTORY-METHODOLOGIES.md` + `docs/research/kanban-six-sigma-factory-process.md` + GOVERNANCE.md §11 | | 38 | Harness-surface cadenced audit | Every 5-10 rounds per populated harness (same cadence as skill-tune-up and agent-QOL). Stubs don't tick. | Per populated harness: Claude owned by Architect (Kenji) **interim** until a dedicated harness-guide role is decided; plugin-provided `claude-code-guide` agent (Anthropic official plugin cache, not a local `.claude/agents/` file) is a reference resource consulted during audit, not the audit runner. Codex / Cursor / GitHub Copilot / Antigravity / Amazon Q / Kiro — TBD when populated (either dedicated guide per harness or a shared multi-harness guide). | factory | Audit each populated harness's platform surfaces for new features, cut features, behavioural changes. For Claude: model / Code CLI / Desktop app / Agent SDK / API. For Codex / Cursor / Copilot / Antigravity / Amazon Q / Kiro: per-harness equivalents inventoried at first-populated audit. Primary feature-comparison axis per harness is skill-authoring + eval-driven feedback loop (the Claude-Code feature that made it Aaron's primary choice — `memory/user_skill_creator_killer_feature_feedback_loop.md`). Update `docs/HARNESS-SURFACES.md` living inventory with adoption statuses (adopted / watched / untested / rejected / stub). When audit surfaces drift, either adopt (ADR if Tier-3), retire the workaround the new feature obsoletes, or record explicit rejection in `docs/WONT-DO.md`. Integration-point tests per harness are owned by a *different* harness per the capability-boundary rule (a harness cannot honestly self-verify its own factory integration from within itself). Triggering incidents: 2026-04-20 AutoMemory miss (Anthropic's Q1-2026 feature mis-attributed as factory-native) + 2026-04-20 multi-harness expansion (Aaron: factory supports multiple harnesses; each tests the others'). | `docs/HARNESS-SURFACES.md` audit log row per cycle per populated harness; ADRs under `docs/DECISIONS/` for Tier-3 adoptions; `docs/research/meta-wins-log.md` entry when a pre-existing factory assumption is found to have been wrong | `feedback_claude_surface_cadence_research.md` + `feedback_multi_harness_support_each_tests_own_integration.md` + `user_skill_creator_killer_feature_feedback_loop.md` + `reference_automemory_anthropic_feature.md` + `reference_autodream_feature.md` | +| 40 | GitHub-settings drift detector | Weekly (cron `17 14 * * 1`) + on any change to `tools/hygiene/github-settings.expected.json` / detector script / workflow file. Added 2026-04-21 after `AceHack/Zeta` → `Lucent-Financial-Group/Zeta` org-transfer silently flipped `secret_scanning` and `secret_scanning_push_protection` from enabled to disabled. | Automated (`.github/workflows/github-settings-drift.yml`); human resolves on drift | factory (ships to adopters as a template; repo-specific expected snapshot per adopter) | Live `gh api` snapshot vs. checked-in `tools/hygiene/github-settings.expected.json`: repo-level toggles (merge methods, security-and-analysis), rulesets + rule contents, classic branch protection on default branch, Actions permissions + variables + counts of secrets, environments + protection-rule types, Pages config, CodeQL default-setup state, webhook / deploy-key / secret counts. Script at `tools/hygiene/check-github-settings-drift.sh` exits 1 on drift and prints `diff -u` output. Resolution: intentional → re-snapshot + commit new expected with rationale; unintentional → revert in GitHub + rerun detector. | `docs/GITHUB-SETTINGS.md` + `tools/hygiene/github-settings.expected.json` + workflow run log + optional `memory/reference_github_*.md` entry if drift source is non-obvious | `feedback_github_settings_as_code_declarative_checked_in_file.md` + `feedback_blast_radius_pricing_standing_rule_alignment_signal.md` + `project_zeta_org_migration_to_lucent_financial_group.md` | | 39 | Hot-file-path detector | Round-cadence (every round close) or every 5-10 rounds — whichever catches churn drift before the next merge-tangle. Proposed 2026-04-21. | TBD — candidate capability skill `hot-file-detector` (queued in BACKLOG P1); Architect runs inline until the skill lands. | factory (ships to adopters as a command-line recipe) | `git log --since="60 days ago" --name-only --pretty=format: \| grep -v '^$' \| sort \| uniq -c \| sort -rn \| head -25` — git history *is* the index. Heuristic threshold: >20 changes in 60d on a single monolithic doc = investigate; >30 = refactor candidate (tune after 5-10 rounds). Per-file decision is one of four: `refactor-split` (per-row, per-round, per-section), `consolidate-reduce` (merge with a sibling), `accept-as-append-only` (legitimately append-only → split into per-round files rather than trimming), or `observe`. Pair with merge-tangle fingerprints from `docs/research/parallel-worktree-safety-2026-04-22.md` §9 — a hot file is worse if also in a recent conflict list. Triggering incident: PR #31 5-file merge-tangle (2026-04-21) where `docs/ROUND-HISTORY.md` at 33 changes / 60d was the #1 conflict source, and `docs/BACKLOG.md` at 26 already has an in-flight split ADR. | Audit doc per cycle listing top-N hot paths + per-file decision; BACKLOG rows for refactor-split candidates; ADRs for structural changes. | `feedback_hot_file_path_detector_hygiene.md` + `docs/DECISIONS/2026-04-22-backlog-per-row-file-restructure.md` (same pattern) | ## Ships to project-under-construction diff --git a/docs/GITHUB-SETTINGS.md b/docs/GITHUB-SETTINGS.md new file mode 100644 index 00000000..4f070ba3 --- /dev/null +++ b/docs/GITHUB-SETTINGS.md @@ -0,0 +1,224 @@ +# GitHub repo settings — declared state + +This doc is the **declarative source of truth** for every GitHub +repo setting that GitHub does not itself expose as a checked-in +config file. Workflow YAML, CODEOWNERS, Dependabot config, and +pre-commit hooks are already declarative in-tree — not tracked +here. What *is* tracked here: click-ops toggles that live inside +GitHub's UI or require API calls to change. + +The machine-readable companion is +[`tools/hygiene/github-settings.expected.json`](../tools/hygiene/github-settings.expected.json). +That JSON file is **authoritative** — if this markdown ever +disagrees with it, the JSON wins and this file gets updated. + +Motivation (Aaron 2026-04-21): + +> "its nice having the expected settings declarative defined" +> +> "i hate things in GitHub where I can't check in the +> declarative settgins so we will save a back[up]" + +The same day we transferred `AceHack/Zeta` → +`Lucent-Financial-Group/Zeta` and discovered that GitHub's +org-transfer code path silently flipped `secret_scanning` and +`secret_scanning_push_protection` from `enabled` to `disabled`. +That silent drift is exactly what this system detects. + +## How this works + +1. **Expected state** is recorded in + `tools/hygiene/github-settings.expected.json` — normalized + output of `tools/hygiene/snapshot-github-settings.sh`. +2. **Drift detector** is `tools/hygiene/check-github-settings-drift.sh`. + It re-runs the snapshot against the live repo and diffs + against the expected JSON. Exit 0 on match, 1 on drift. +3. **Cadence** is enforced by + `.github/workflows/github-settings-drift.yml` — weekly cron + + `workflow_dispatch` for manual runs. Drift blocks the + weekly run (visible in Actions tab as a failing job); + resolve by either reverting the unexpected change or + re-snapshotting and committing the new expected. +4. **On any settings change** (ruleset edit, new required + check, flipped security toggle, new environment, ...) the + same-commit obligation is: re-run + `snapshot-github-settings.sh`, commit the new expected + JSON alongside whatever configuration caused the drift, + with a message explaining *why* the setting changed. + +See `docs/FACTORY-HYGIENE.md` row #40 for the full cadence / +owner / scope specification and +`memory/feedback_github_settings_as_code_declarative_checked_in_file.md` +for the framing this pattern belongs to. + +## What's captured + +### Repo-level toggles + +- Merge methods: squash on; merge commit and rebase off. +- Auto-merge enabled; update branch button enabled; + auto-delete branch on merge enabled. +- Squash commit title: PR title (falls back to commit title + for single-commit PRs); squash commit message: concatenated + commit messages. +- Web commit signoff not required (public repo, pre-v1). +- Visibility: public. +- Features enabled: issues, discussions, projects, wiki. +- Security-and-analysis: Dependabot security updates enabled; + secret scanning enabled; secret scanning push-protection + enabled; non-provider-pattern scanning and validity checks + disabled (higher false-positive rate; revisit post-v1). + +### Rulesets + +Single ruleset named `Default` (id 15256879), enforcement +`active`, target `branch`, condition +`ref_name.include = ["~DEFAULT_BRANCH"]`. Six rules: + +1. **Deletion** — block default-branch deletion. +2. **Non-fast-forward** — block non-fast-forward pushes. +3. **Copilot code review** — review draft PRs + review on + push. +4. **Code quality** — severity all. +5. **Pull request** — squash-only merge method; + `required_review_thread_resolution: true`; + `required_approving_review_count: 0` (agent-authored + repo — human review not required, AI review is via the + copilot-code-review and code-quality rules above). +6. **Required linear history**. + +Note on the **`code_scanning` rule**: we toggled it OFF +2026-04-21 because it binds to CodeQL *default-setup* +configurations and Zeta uses *advanced-setup* +(`.github/workflows/codeql.yml` with `build-mode: manual` +for csharp + per-language SARIF upload). The rule returned +NEUTRAL / "1 configuration not found" and blocked PR #42 +despite all advanced-setup sub-jobs passing. See +`memory/reference_github_code_scanning_ruleset_rule_requires_default_setup.md` +for the full diagnostic. Re-enabling requires either +(a) enabling default-setup alongside advanced — unverified +coexistence, duplicate compute, or (b) discovering whether +the rule can bind to advanced-setup (untested). + +### Classic branch protection (on `main`) + +Overlaps with the ruleset; kept as defence-in-depth. Six +required status checks (strict mode): + +- `build-and-test (ubuntu-22.04)` +- `build-and-test (macos-14)` +- `lint (semgrep)` +- `lint (shellcheck)` +- `lint (actionlint)` +- `lint (markdownlint)` + +Other protections: dismiss stale reviews on; required linear +history; required conversation resolution; force pushes and +deletions blocked; enforce_admins off. + +### Actions + +- Actions enabled; `allowed_actions: all`. +- Variables (2): + - `COPILOT_AGENT_FIREWALL_ENABLED = "true"` + - `COPILOT_AGENT_FIREWALL_ALLOW_LIST_ADDITIONS = " "` (space — + no additions beyond the Copilot firewall defaults). +- Secrets (0): no Actions secrets. Any future secret addition + must be accompanied by a rationale in this doc. + +### Workflows (5 active) + +Static (checked-in): + +- `.github/workflows/codeql.yml` (CodeQL) +- `.github/workflows/gate.yml` (gate matrix: build, test, + lint, semgrep) + +Dynamic (GitHub-managed): + +- Copilot code review +- Dependabot Updates +- Automatic Dependency Submission (NuGet) + +### Environments + +- `github-pages` environment with one `branch_policy` + protection rule — deployments only from `main`. + +### GitHub Pages + +- Build type: `workflow`. +- Source: branch `main`, path `/`. +- HTTPS enforced: yes. +- URL: + `https://lucent-financial-group.github.io/Zeta/` (transferred + from `https://acehack.github.io/Zeta/` on 2026-04-21). + +### CodeQL default-setup + +- State: `not-configured` (intentional — we use advanced + setup via `.github/workflows/codeql.yml`). + +### Webhooks + deploy keys + secrets + +- Webhooks: 0. +- Deploy keys: 0. +- Actions secrets: 0. +- Dependabot secrets: 0. + +## What's NOT captured + +- **Workflow YAML** — already declarative in + `.github/workflows/`. +- **CODEOWNERS** — already declarative in `.github/CODEOWNERS` + if/when we add one. +- **Dependabot config** — already declarative in + `.github/dependabot.yml`. +- **Pre-commit hooks** — already declarative in + `.pre-commit-config.yaml` if/when we add one. +- **`.github/copilot-instructions.md`** — already declarative; + audited under FACTORY-HYGIENE row 14. +- **Secret values** — the counts are captured; the values + would be a security hole. Never write secret values here. +- **Individual user/team permissions on the org** — org-level, + out of scope for per-repo declaration. If this ever grows, + consider a sibling `docs/ORG-SETTINGS.md` with the same + pattern applied to the org. +- **Transient statuses** — `security_and_analysis.*.url`, + timestamps, etc. The snapshot script strips these. + +## How to update + +```bash +# After making an intentional settings change in GitHub +# UI or via API, re-snapshot and commit: +tools/hygiene/snapshot-github-settings.sh \ + --repo Lucent-Financial-Group/Zeta \ + > tools/hygiene/github-settings.expected.json +git add tools/hygiene/github-settings.expected.json +# If the human-readable narrative also needs updating, +# edit docs/GITHUB-SETTINGS.md to match. +git commit -m "chore(settings): " +``` + +Unintentional drift (detected by the weekly drift workflow or +a manual run) is fixed in the opposite direction: revert the +setting in GitHub, rerun the detector to confirm match, and +file a `memory/reference_github_*.md` entry if the drift +source is non-obvious. + +## Related + +- `tools/hygiene/snapshot-github-settings.sh` — generates the + normalized JSON. +- `tools/hygiene/check-github-settings-drift.sh` — the drift + detector. +- `.github/workflows/github-settings-drift.yml` — cadence + workflow. +- `docs/FACTORY-HYGIENE.md` row #40 — the hygiene row. +- `memory/feedback_github_settings_as_code_declarative_checked_in_file.md` + — the framing / pattern. +- `memory/reference_github_code_scanning_ruleset_rule_requires_default_setup.md` + — why the `code_scanning` ruleset rule is off. +- `memory/project_zeta_org_migration_to_lucent_financial_group.md` + — the migration that triggered this discipline. diff --git a/docs/HUMAN-BACKLOG.md b/docs/HUMAN-BACKLOG.md index faba04bc..b2871cd2 100644 --- a/docs/HUMAN-BACKLOG.md +++ b/docs/HUMAN-BACKLOG.md @@ -231,7 +231,7 @@ are ordered by `State: Open` first, then `Stale`, then | ID | When | Category | Ask | Source | State | Resolution | |---|---|---|---|---|---|---| -| 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 | — | +| 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") | Resolved | Executed 2026-04-21 via `POST /repos/AceHack/Zeta/transfer` with `new_owner=Lucent-Financial-Group`. Transfer completed instantly (Aaron admin on both sides). Verification diffed 13 settings groups against pre-transfer scorecard: all preserved **except** `secret_scanning` and `secret_scanning_push_protection` both silently flipped `enabled→disabled` by GitHub's org-transfer code path; re-enabled same session via `PATCH /repos/Lucent-Financial-Group/Zeta` with `security_and_analysis`. Ruleset id 15256879 "Default" preserved byte-identical (6 rules); classic branch protection on main preserved (6 required contexts); Actions variables preserved (2 COPILOT_AGENT_FIREWALL_*); environments + Pages config preserved (Pages URL redirected `acehack.github.io/Zeta` → `lucent-financial-group.github.io/Zeta`). Local `git remote` updated. Declarative settings file landed at `docs/GITHUB-SETTINGS.md` per Aaron's companion directive ("its nice having the expected settings declarative defined" + "i hate things in GitHub where I can't check in the declarative settgins"). Merge queue enable remains a separate opt-in step. | ### For: `any` (any human contributor) diff --git a/docs/ROUND-HISTORY.md b/docs/ROUND-HISTORY.md index d8b4fd9c..af286af7 100644 --- a/docs/ROUND-HISTORY.md +++ b/docs/ROUND-HISTORY.md @@ -98,6 +98,42 @@ fixed in the same round the rule landed (commit `ac0eb1f`, meta-wins-log depth-1 row). Arc-by-arc narrative lands at round-close. +A late-round arc landed the **`AceHack/Zeta` → +`Lucent-Financial-Group/Zeta` org migration** (HB-001 +resolved via `POST /repos/AceHack/Zeta/transfer` with +`new_owner=Lucent-Financial-Group`, completed instantly +because Aaron is admin on both sides). Post-transfer +verification diffed 13 settings groups against a +pre-transfer scorecard: all preserved **except** +`secret_scanning` and `secret_scanning_push_protection`, +which GitHub's org-transfer code path silently flipped +`enabled → disabled` — re-enabled same session via +`PATCH /repos/.../Zeta` with +`security_and_analysis[secret_scanning*]=enabled`. This +silent drift is the founding incident for a new factory +hygiene class: **settings-as-code-by-convention for +platforms that lack native declarative config**. The +pattern is `docs/GITHUB-SETTINGS.md` (human-readable +narrative) + `tools/hygiene/github-settings.expected.json` +(machine-readable canonical state) + +`tools/hygiene/check-github-settings-drift.sh` (detector) + +`.github/workflows/github-settings-drift.yml` (weekly cron ++ PR-triggered cadence). Landed as FACTORY-HYGIENE row #40 +and two companion memories +(`feedback_github_settings_as_code_declarative_checked_in_file.md` ++ `feedback_blast_radius_pricing_standing_rule_alignment_signal.md` +— the latter captures Aaron's 2026-04-21 praise +*"this is great standing rules on blast-radius ops ... +i'm glad you understand blast radius and pricing the +blast radius"*, reframing the CLAUDE.md "confirm before +hard-to-reverse actions" discipline as load-bearing +rather than overcautious, and as a Zeta product-feature +signal connecting to the retractable-contract ledger). The +pattern generalizes beyond GitHub — any click-ops platform +(AWS / GCP / Slack / org-level settings) gets the same +markdown-declaration + cadenced-diff treatment when +adopted. + --- ## Round 43 — invariant-substrates program + empirical BP-03 harness evidence + agent-cadence telemetry diff --git a/tools/hygiene/check-github-settings-drift.sh b/tools/hygiene/check-github-settings-drift.sh new file mode 100755 index 00000000..2be3c097 --- /dev/null +++ b/tools/hygiene/check-github-settings-drift.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# check-github-settings-drift.sh — diff the current GitHub settings of a +# repo against the checked-in expected snapshot. Detects click-ops drift +# for settings that GitHub does not expose as declarative config. +# +# Usage: +# tools/hygiene/check-github-settings-drift.sh [--repo OWNER/NAME] [--expected PATH] +# +# Defaults: +# --repo $GH_REPO, else `gh repo view --json nameWithOwner` +# --expected tools/hygiene/github-settings.expected.json (next to this script) +# +# Exit codes: +# 0 — no drift +# 1 — drift detected (diff printed to stdout) +# 2 — tooling / input error +# +# Runs in CI via `.github/workflows/github-settings-drift.yml` on a weekly +# cadence + manual dispatch. Also safe to run locally before risky +# settings changes to capture a baseline. + +set -uo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo="" +expected="$script_dir/github-settings.expected.json" + +while [ $# -gt 0 ]; do + case "$1" in + --repo) repo="$2"; shift 2;; + --expected) expected="$2"; shift 2;; + *) echo "error: unknown arg: $1" >&2; exit 2;; + esac +done + +if [ -z "$repo" ]; then + repo="${GH_REPO:-$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)}" +fi +if [ -z "$repo" ]; then + echo "error: cannot determine repo; pass --repo OWNER/NAME or set GH_REPO" >&2 + exit 2 +fi + +if [ ! -f "$expected" ]; then + echo "error: expected snapshot not found: $expected" >&2 + exit 2 +fi + +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT + +"$script_dir/snapshot-github-settings.sh" --repo "$repo" > "$tmp" || { + echo "error: snapshot failed" >&2 + exit 2 +} + +if diff -u "$expected" "$tmp"; then + echo "github-settings-drift: no drift (repo=$repo)" >&2 + exit 0 +else + echo "" >&2 + echo "github-settings-drift: DRIFT DETECTED (repo=$repo)" >&2 + echo " expected: $expected" >&2 + echo " current : (live from gh api)" >&2 + echo "" >&2 + echo "Resolve options:" >&2 + echo " 1. Intentional change -> update expected snapshot:" >&2 + echo " $script_dir/snapshot-github-settings.sh --repo $repo > $expected" >&2 + echo " Then commit the diff with a message explaining the policy change." >&2 + echo " 2. Unintentional change -> revert the setting in GitHub UI/API" >&2 + echo " and re-run this script to confirm." >&2 + exit 1 +fi diff --git a/tools/hygiene/github-settings.expected.json b/tools/hygiene/github-settings.expected.json new file mode 100644 index 00000000..6c866998 --- /dev/null +++ b/tools/hygiene/github-settings.expected.json @@ -0,0 +1,196 @@ +{ + "repo": { + "allow_auto_merge": true, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "allow_squash_merge": true, + "allow_update_branch": true, + "default_branch": "main", + "delete_branch_on_merge": true, + "has_discussions": true, + "has_issues": true, + "has_projects": true, + "has_wiki": true, + "security_and_analysis": { + "dependabot_security_updates": { + "status": "enabled" + }, + "secret_scanning": { + "status": "enabled" + }, + "secret_scanning_non_provider_patterns": { + "status": "disabled" + }, + "secret_scanning_push_protection": { + "status": "enabled" + }, + "secret_scanning_validity_checks": { + "status": "disabled" + } + }, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "visibility": "public", + "web_commit_signoff_required": false + }, + "rulesets": [ + { + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "~DEFAULT_BRANCH" + ] + } + }, + "enforcement": "active", + "id": 15256879, + "name": "Default", + "rules": [ + { + "parameters": null, + "type": "deletion" + }, + { + "parameters": null, + "type": "non_fast_forward" + }, + { + "parameters": { + "review_draft_pull_requests": true, + "review_on_push": true + }, + "type": "copilot_code_review" + }, + { + "parameters": { + "severity": "all" + }, + "type": "code_quality" + }, + { + "parameters": { + "allowed_merge_methods": [ + "squash" + ], + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_approving_review_count": 0, + "required_review_thread_resolution": true, + "required_reviewers": [] + }, + "type": "pull_request" + }, + { + "parameters": null, + "type": "required_linear_history" + } + ], + "target": "branch" + } + ], + "default_branch_protection": { + "allow_deletions": false, + "allow_force_pushes": false, + "allow_fork_syncing": false, + "enforce_admins": false, + "lock_branch": false, + "required_conversation_resolution": true, + "required_linear_history": true, + "required_pull_request_reviews": { + "dismiss_stale_reviews": true, + "require_code_owner_reviews": false, + "require_last_push_approval": false, + "required_approving_review_count": 0 + }, + "required_signatures": false, + "required_status_checks": { + "contexts": [ + "build-and-test (macos-14)", + "build-and-test (ubuntu-22.04)", + "lint (actionlint)", + "lint (markdownlint)", + "lint (semgrep)", + "lint (shellcheck)" + ], + "strict": true + } + }, + "actions_permissions": { + "allowed_actions": "all", + "enabled": true + }, + "actions_variables": [ + { + "name": "COPILOT_AGENT_FIREWALL_ALLOW_LIST_ADDITIONS", + "value": " " + }, + { + "name": "COPILOT_AGENT_FIREWALL_ENABLED", + "value": "true" + } + ], + "workflows": [ + { + "name": "Automatic Dependency Submission", + "path": "dynamic/dependency-graph/auto-submission", + "state": "active" + }, + { + "name": "CodeQL", + "path": ".github/workflows/codeql.yml", + "state": "active" + }, + { + "name": "Copilot code review", + "path": "dynamic/copilot-pull-request-reviewer/copilot-pull-request-reviewer", + "state": "active" + }, + { + "name": "Dependabot Updates", + "path": "dynamic/dependabot/dependabot-updates", + "state": "active" + }, + { + "name": "gate", + "path": ".github/workflows/gate.yml", + "state": "active" + } + ], + "environments": [ + { + "name": "github-pages", + "protection_rule_types": [ + "branch_policy" + ] + } + ], + "pages": { + "build_type": "workflow", + "https_enforced": true, + "public": true, + "source": { + "branch": "main", + "path": "/" + } + }, + "codeql_default_setup": { + "languages": [ + "actions", + "csharp", + "java-kotlin", + "javascript", + "javascript-typescript", + "typescript" + ], + "query_suite": "default", + "state": "not-configured" + }, + "counts": { + "webhooks": 0, + "deploy_keys": 0, + "actions_secrets": 0, + "dependabot_secrets": 0 + } +} diff --git a/tools/hygiene/snapshot-github-settings.sh b/tools/hygiene/snapshot-github-settings.sh new file mode 100755 index 00000000..aeaafb1f --- /dev/null +++ b/tools/hygiene/snapshot-github-settings.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# snapshot-github-settings.sh — produce a normalized JSON snapshot of the +# repo's GitHub settings. Output is deterministic + diffable. Used by +# check-github-settings-drift.sh and for manual "update expected" flows. +# +# Usage: +# tools/hygiene/snapshot-github-settings.sh [--repo OWNER/NAME] > snapshot.json +# +# Defaults: $GH_REPO env var, then `gh repo view --json nameWithOwner`. +# +# What this captures: every setting that is NOT tracked in a checked-in +# file inside the repo. Workflow YAML, CODEOWNERS, Dependabot config, +# pre-commit hooks are all *already* declarative in-tree — no need to +# snapshot them. This script covers the click-ops surfaces: +# - repo-level toggles (merge methods, security-and-analysis, ...) +# - rulesets + their rule contents +# - classic branch protection on default branch +# - Actions permissions + Actions variables (names + values, NOT secrets) +# - environments (names + protection rule types) +# - GitHub Pages config +# - CodeQL default-setup state +# +# Exit 0 on success; non-zero on any gh call failure. + +set -uo pipefail + +repo="${1:-}" +if [ "$repo" = "--repo" ]; then + repo="$2" +elif [ -z "$repo" ]; then + repo="${GH_REPO:-$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)}" +fi +if [ -z "$repo" ]; then + echo "error: cannot determine repo; pass --repo OWNER/NAME or set GH_REPO" >&2 + exit 2 +fi + +default_branch=$(gh api "/repos/$repo" --jq '.default_branch') + +repo_json=$(gh api "/repos/$repo" --jq '{ + allow_auto_merge, allow_merge_commit, allow_rebase_merge, allow_squash_merge, + allow_update_branch, default_branch, delete_branch_on_merge, + has_discussions, has_issues, has_projects, has_wiki, + squash_merge_commit_message, squash_merge_commit_title, + visibility, web_commit_signoff_required, + security_and_analysis +}') + +rulesets_json=$(gh api "/repos/$repo/rulesets" --jq '[.[] | {id, name, target, enforcement}]') + +ruleset_details='[]' +ruleset_ids=$(gh api "/repos/$repo/rulesets" --jq '.[].id') +for rid in $ruleset_ids; do + one=$(gh api "/repos/$repo/rulesets/$rid" --jq '{id, name, target, enforcement, conditions, rules: [.rules[] | {type, parameters}]}') + ruleset_details=$(jq --argjson one "$one" '. + [$one]' <<< "$ruleset_details") +done + +protection_json=$(gh api "/repos/$repo/branches/$default_branch/protection" 2>/dev/null --jq '{ + required_status_checks: (.required_status_checks // null | if . then {strict, contexts: (.contexts | sort)} else null end), + required_pull_request_reviews: (.required_pull_request_reviews // null | if . then {dismiss_stale_reviews, require_code_owner_reviews, require_last_push_approval, required_approving_review_count} else null end), + required_signatures: .required_signatures.enabled, + enforce_admins: .enforce_admins.enabled, + required_linear_history: .required_linear_history.enabled, + allow_force_pushes: .allow_force_pushes.enabled, + allow_deletions: .allow_deletions.enabled, + required_conversation_resolution: .required_conversation_resolution.enabled, + lock_branch: .lock_branch.enabled, + allow_fork_syncing: .allow_fork_syncing.enabled +}' || echo 'null') + +actions_perms_json=$(gh api "/repos/$repo/actions/permissions" --jq '{enabled, allowed_actions}') + +actions_vars_json=$(gh api "/repos/$repo/actions/variables" --jq '[.variables[]? | {name, value}] | sort_by(.name)') + +workflows_json=$(gh api "/repos/$repo/actions/workflows" --jq '[.workflows[] | {name, state, path}] | sort_by(.name)') + +envs_json=$(gh api "/repos/$repo/environments" --jq '[.environments[]? | {name, protection_rule_types: [.protection_rules[]?.type] | sort}] | sort_by(.name)') + +pages_json=$(gh api "/repos/$repo/pages" 2>/dev/null --jq '{source, build_type, https_enforced, public}' || echo 'null') + +codeql_json=$(gh api "/repos/$repo/code-scanning/default-setup" --jq '{state, languages: (.languages | sort), query_suite}') + +webhooks_count=$(gh api "/repos/$repo/hooks" --jq 'length') +deploy_keys_count=$(gh api "/repos/$repo/keys" --jq 'length') +actions_secrets_count=$(gh api "/repos/$repo/actions/secrets" --jq '.secrets | length') +dependabot_secrets_count=$(gh api "/repos/$repo/dependabot/secrets" --jq '.secrets | length' 2>/dev/null || echo 0) + +jq -n \ + --argjson repo "$repo_json" \ + --argjson rulesets "$ruleset_details" \ + --argjson protection "$protection_json" \ + --argjson actions_perms "$actions_perms_json" \ + --argjson actions_vars "$actions_vars_json" \ + --argjson workflows "$workflows_json" \ + --argjson envs "$envs_json" \ + --argjson pages "$pages_json" \ + --argjson codeql "$codeql_json" \ + --argjson webhooks_count "$webhooks_count" \ + --argjson deploy_keys_count "$deploy_keys_count" \ + --argjson actions_secrets_count "$actions_secrets_count" \ + --argjson dependabot_secrets_count "$dependabot_secrets_count" \ + '{ + repo: $repo, + rulesets: $rulesets, + default_branch_protection: $protection, + actions_permissions: $actions_perms, + actions_variables: $actions_vars, + workflows: $workflows, + environments: $envs, + pages: $pages, + codeql_default_setup: $codeql, + counts: { + webhooks: $webhooks_count, + deploy_keys: $deploy_keys_count, + actions_secrets: $actions_secrets_count, + dependabot_secrets: $dependabot_secrets_count + } + }' From a31ccc66fedae42035ee1ed19c14df06c247b784 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 21 Apr 2026 09:55:59 -0400 Subject: [PATCH 2/5] =?UTF-8?q?fix(lints):=20unblock=20PR=20#45=20?= =?UTF-8?q?=E2=80=94=20markdownlint,=20actionlint,=20shellcheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/ROUND-HISTORY.md: `+` at line-start parsed as bullet by markdownlint (MD032); replace with "and" in the two affected wrap points. - .github/workflows/github-settings-drift.yml: drop `administration: read` — actionlint v1.7.x does not list it as a recognised scope; the default GITHUB_TOKEN on public repos covers the endpoints we read. Comment documents the trade + the fact that the PR-triggered drift check would surface a missing scope immediately. - tools/hygiene/snapshot-github-settings.sh: drop dead `rulesets_json` assignment (SC2034) — the `ruleset_details` loop below returns the full per-ruleset data used in the final jq output. Aaron's follow-up "you also didn't get half the settings you miss most of them lol it's okay" is acknowledged — a coverage expansion PR will follow this one. The current snapshot is a first pass; PR #45 lands the *pattern* (decl + drift detector + hygiene row) and we iterate on coverage. --- .github/workflows/github-settings-drift.yml | 12 ++++++++---- docs/ROUND-HISTORY.md | 4 ++-- tools/hygiene/snapshot-github-settings.sh | 2 -- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/github-settings-drift.yml b/.github/workflows/github-settings-drift.yml index 2a938c24..0621ff72 100644 --- a/.github/workflows/github-settings-drift.yml +++ b/.github/workflows/github-settings-drift.yml @@ -35,12 +35,16 @@ on: - ".github/workflows/github-settings-drift.yml" permissions: + # `contents: read` + `actions: read` covers workflow reads, + # Actions permissions, environments, and Pages. Repo-settings + # endpoints (rulesets, branch protection, security_and_analysis) + # are readable with the default GITHUB_TOKEN on public repos + # without declaring `administration` (which actionlint does + # not recognise as a valid scope as of v1.7.x — drift check + # exercises these endpoints on every PR anyway, so a missing + # scope would surface immediately). contents: read - # Need metadata to read rulesets / branch protection / Actions - # variables via gh api. GITHUB_TOKEN's default permissions are - # sufficient for public-repo settings reads. actions: read - administration: read concurrency: group: github-settings-drift diff --git a/docs/ROUND-HISTORY.md b/docs/ROUND-HISTORY.md index af286af7..b6d9ad4e 100644 --- a/docs/ROUND-HISTORY.md +++ b/docs/ROUND-HISTORY.md @@ -118,10 +118,10 @@ narrative) + `tools/hygiene/github-settings.expected.json` (machine-readable canonical state) + `tools/hygiene/check-github-settings-drift.sh` (detector) + `.github/workflows/github-settings-drift.yml` (weekly cron -+ PR-triggered cadence). Landed as FACTORY-HYGIENE row #40 +and PR-triggered cadence). Landed as FACTORY-HYGIENE row #40 and two companion memories (`feedback_github_settings_as_code_declarative_checked_in_file.md` -+ `feedback_blast_radius_pricing_standing_rule_alignment_signal.md` +and `feedback_blast_radius_pricing_standing_rule_alignment_signal.md` — the latter captures Aaron's 2026-04-21 praise *"this is great standing rules on blast-radius ops ... i'm glad you understand blast radius and pricing the diff --git a/tools/hygiene/snapshot-github-settings.sh b/tools/hygiene/snapshot-github-settings.sh index aeaafb1f..68628063 100755 --- a/tools/hygiene/snapshot-github-settings.sh +++ b/tools/hygiene/snapshot-github-settings.sh @@ -46,8 +46,6 @@ repo_json=$(gh api "/repos/$repo" --jq '{ security_and_analysis }') -rulesets_json=$(gh api "/repos/$repo/rulesets" --jq '[.[] | {id, name, target, enforcement}]') - ruleset_details='[]' ruleset_ids=$(gh api "/repos/$repo/rulesets" --jq '.[].id') for rid in $ruleset_ids; do From 90341174f83cbea7855f96c52fc655253b599dd0 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 21 Apr 2026 10:01:06 -0400 Subject: [PATCH 3/5] fix: restore administration:read + actionlint -ignore workaround MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first PR #45 lint fix dropped `administration: read` hoping the default GITHUB_TOKEN would cover the rulesets / branch protection / security_and_analysis endpoints — wrong. The CI run of the drift detector itself returned six HTTP 403s on those endpoints, then `jq --argjson` on the empty results produced `invalid JSON text` and the detector failed with exit 2 (tooling error, not drift). Fix: - Restore `administration: read` to .github/workflows/github-settings-drift.yml — it's a valid GitHub Actions permission scope that actionlint v1.7.x simply hasn't added to its allow-list yet (confirmed against v1.7.12 local and v1.7.7 CI: both reject it). - Teach the gate lint-workflows job to pass `-ignore 'unknown permission scope "administration"'` to actionlint. Surgical: targets exactly the known false-positive string; removable when actionlint catches up. Comment points at the upstream tracker. The meta-hygiene here is intact: my own drift detector caught my own wrong hypothesis on the very next PR push. Detector-outlives- fix rule confirms — the class of "my workflow asked for too few permissions" gets an automated regression signal now. --- .github/workflows/gate.yml | 11 ++++++++++- .github/workflows/github-settings-drift.yml | 14 ++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 51af6282..965d7f1d 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -214,7 +214,16 @@ jobs: ./actionlint --version - name: Run actionlint - run: ./actionlint -color + # -ignore 'unknown permission scope "administration"' is a + # documented workaround for a known actionlint gap — the + # `administration` permission IS valid in GitHub Actions + # (used by .github/workflows/github-settings-drift.yml to + # read rulesets + branch protection + security_and_analysis) + # but actionlint v1.7.x has not yet added it to its allow- + # list. Remove when actionlint catches up. See: + # https://github.com/rhysd/actionlint/issues (search + # "administration permission"). + run: ./actionlint -color -ignore 'unknown permission scope "administration"' lint-no-empty-dirs: # Fail if a committed directory has no files — almost always a diff --git a/.github/workflows/github-settings-drift.yml b/.github/workflows/github-settings-drift.yml index 0621ff72..08002da3 100644 --- a/.github/workflows/github-settings-drift.yml +++ b/.github/workflows/github-settings-drift.yml @@ -35,16 +35,14 @@ on: - ".github/workflows/github-settings-drift.yml" permissions: - # `contents: read` + `actions: read` covers workflow reads, - # Actions permissions, environments, and Pages. Repo-settings - # endpoints (rulesets, branch protection, security_and_analysis) - # are readable with the default GITHUB_TOKEN on public repos - # without declaring `administration` (which actionlint does - # not recognise as a valid scope as of v1.7.x — drift check - # exercises these endpoints on every PR anyway, so a missing - # scope would surface immediately). + # Rulesets, branch protection, security_and_analysis, + # secrets counts, deploy keys, webhooks require + # `administration: read`. `actions: read` covers workflow + # reads + Actions variables + environments. `contents: read` + # is the baseline for `actions/checkout`. contents: read actions: read + administration: read concurrency: group: github-settings-drift From 9ea7442e5f1c94b55781c1845f5f8f1c4cd8a3d4 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 21 Apr 2026 10:06:51 -0400 Subject: [PATCH 4/5] =?UTF-8?q?snapshot:=20expand=20coverage=20=E2=80=94?= =?UTF-8?q?=20repo-level=20keys=20+=205=20new=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aaron 2026-04-21: "you also didn't get half the setting you miss most of them lol it's okay" — accurate critique of PR #45 first-pass coverage. The declarative-settings-as-code pattern only pays off if the settings are actually in the declaration. Additions to `tools/hygiene/snapshot-github-settings.sh`: Repo-level keys previously uncaptured: - `allow_forking` (CRITICAL — gates the fork-based PR workflow that Aaron proposed the same round) - `description`, `homepage`, `topics` (discovery metadata) - `has_downloads`, `has_pages`, `has_pull_requests` (feature toggles) - `merge_commit_message`, `merge_commit_title` (even with merge-commit off, the format defaults are a policy value) - `use_squash_pr_title_as_default` - `is_template`, `archived`, `disabled` (lifecycle state) - `pull_request_creation_policy` - `custom_properties` (org-level custom repo properties) Security-surface endpoints previously unqueried: - `/topics` (captured as `.topics` at top level) - `/automated-security-fixes` (Dependabot auto-PR state) - `/private-vulnerability-reporting` (security-tab reports enable state) - `/interaction-limits` (rate-limit / cooldown state; null when inactive — note the `gh api --jq` empty-output quirk on object-length-0, handled via raw-fetch-then-jq two-step) - `/autolinks` (external issue-tracker linking) - `/vulnerability-alerts` (Dependabot alerts; endpoint returns 204/404 not JSON — handled via status-code probe) All captured under a new top-level `security:` subsection (vulnerability_alerts_enabled + automated_security_fixes + private_vulnerability_reporting) plus top-level `topics`, `interaction_limits`, `autolinks` for orthogonal surfaces. `tools/hygiene/github-settings.expected.json` re-snapshotted against live state (now 237 lines, up from 196). Verified `check-github-settings-drift.sh` returns "no drift" against the live repo. `docs/GITHUB-SETTINGS.md` §"Repo-level toggles" expanded to cover the new fields + new §"Repo security extras" subsection. Cross-references the fork-based-workflow memory because `allow_forking` is the gating setting. --- docs/GITHUB-SETTINGS.md | 43 ++++++++++++++++++--- tools/hygiene/github-settings.expected.json | 41 ++++++++++++++++++++ tools/hygiene/snapshot-github-settings.sh | 43 +++++++++++++++++++-- 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/docs/GITHUB-SETTINGS.md b/docs/GITHUB-SETTINGS.md index 4f070ba3..7e2e1b3e 100644 --- a/docs/GITHUB-SETTINGS.md +++ b/docs/GITHUB-SETTINGS.md @@ -56,18 +56,51 @@ for the framing this pattern belongs to. ### Repo-level toggles - Merge methods: squash on; merge commit and rebase off. + (`merge_commit_message` + `merge_commit_title` are still + captured even though merge-commit is off — the defaults + determine format if we ever flip the toggle on.) - Auto-merge enabled; update branch button enabled; - auto-delete branch on merge enabled. + auto-delete branch on merge enabled; + `use_squash_pr_title_as_default` off (explicit PR-title-or- + commit-title selection still applies). +- `allow_forking` on — required for the fork-based PR + workflow (see `memory/feedback_fork_based_pr_workflow_for_personal_copilot_usage.md`). - Squash commit title: PR title (falls back to commit title for single-commit PRs); squash commit message: concatenated commit messages. - Web commit signoff not required (public repo, pre-v1). -- Visibility: public. -- Features enabled: issues, discussions, projects, wiki. +- Visibility: public. `archived: false`, `disabled: false`, + `is_template: false`. +- Description: "F# implementation of DBSP for .NET 10". + Homepage: `https://lucent-financial-group.github.io/Zeta/`. +- Topics: empty (no classification tags yet). +- Features enabled: issues, discussions, projects, wiki, + downloads, pull-requests, pages. +- `pull_request_creation_policy: all` — anyone with push + access (collaborators + org members per team perms) can + open PRs. +- `custom_properties`: empty object — no org-level custom + repo properties set yet. - Security-and-analysis: Dependabot security updates enabled; secret scanning enabled; secret scanning push-protection - enabled; non-provider-pattern scanning and validity checks - disabled (higher false-positive rate; revisit post-v1). + enabled; non-provider-pattern scanning, AI detection, + delegated-alert-dismissal, delegated-bypass, and validity + checks all disabled (higher false-positive / seat-cost + profile; revisit post-v1). + +### Repo security extras + +- Vulnerability alerts (Dependabot alerts) enabled. +- Automated security fixes (Dependabot auto-PRs) enabled, + not paused. +- Private vulnerability reporting enabled — external + researchers can open confidential advisories via the + Security tab. +- Interaction limits: none (would be `interaction_limits: + {limit, origin, expires_at}` when active — used to rate- + limit comment/issue activity during incident response). +- Autolinks: none — no external issue-tracker linking. +- Topics: empty — no discovery tags. ### Rulesets diff --git a/tools/hygiene/github-settings.expected.json b/tools/hygiene/github-settings.expected.json index 6c866998..73eb03ea 100644 --- a/tools/hygiene/github-settings.expected.json +++ b/tools/hygiene/github-settings.expected.json @@ -1,16 +1,29 @@ { "repo": { "allow_auto_merge": true, + "allow_forking": true, "allow_merge_commit": false, "allow_rebase_merge": false, "allow_squash_merge": true, "allow_update_branch": true, + "archived": false, + "custom_properties": {}, "default_branch": "main", "delete_branch_on_merge": true, + "description": "F# implementation of DBSP for .NET 10", + "disabled": false, "has_discussions": true, + "has_downloads": true, "has_issues": true, + "has_pages": true, "has_projects": true, + "has_pull_requests": true, "has_wiki": true, + "homepage": "https://lucent-financial-group.github.io/Zeta/", + "is_template": false, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "pull_request_creation_policy": "all", "security_and_analysis": { "dependabot_security_updates": { "status": "enabled" @@ -18,6 +31,15 @@ "secret_scanning": { "status": "enabled" }, + "secret_scanning_ai_detection": { + "status": "disabled" + }, + "secret_scanning_delegated_alert_dismissal": { + "status": "disabled" + }, + "secret_scanning_delegated_bypass": { + "status": "disabled" + }, "secret_scanning_non_provider_patterns": { "status": "disabled" }, @@ -30,9 +52,11 @@ }, "squash_merge_commit_message": "COMMIT_MESSAGES", "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "use_squash_pr_title_as_default": false, "visibility": "public", "web_commit_signoff_required": false }, + "topics": [], "rulesets": [ { "conditions": { @@ -132,6 +156,11 @@ } ], "workflows": [ + { + "name": ".github/workflows/github-settings-drift.yml", + "path": ".github/workflows/github-settings-drift.yml", + "state": "active" + }, { "name": "Automatic Dependency Submission", "path": "dynamic/dependency-graph/auto-submission", @@ -187,6 +216,18 @@ "query_suite": "default", "state": "not-configured" }, + "security": { + "vulnerability_alerts_enabled": true, + "automated_security_fixes": { + "enabled": true, + "paused": false + }, + "private_vulnerability_reporting": { + "enabled": true + } + }, + "interaction_limits": null, + "autolinks": [], "counts": { "webhooks": 0, "deploy_keys": 0, diff --git a/tools/hygiene/snapshot-github-settings.sh b/tools/hygiene/snapshot-github-settings.sh index 68628063..201d7598 100755 --- a/tools/hygiene/snapshot-github-settings.sh +++ b/tools/hygiene/snapshot-github-settings.sh @@ -38,14 +38,35 @@ fi default_branch=$(gh api "/repos/$repo" --jq '.default_branch') repo_json=$(gh api "/repos/$repo" --jq '{ - allow_auto_merge, allow_merge_commit, allow_rebase_merge, allow_squash_merge, - allow_update_branch, default_branch, delete_branch_on_merge, - has_discussions, has_issues, has_projects, has_wiki, + allow_auto_merge, allow_forking, allow_merge_commit, allow_rebase_merge, allow_squash_merge, + allow_update_branch, archived, custom_properties, default_branch, + delete_branch_on_merge, description, disabled, + has_discussions, has_downloads, has_issues, has_pages, has_projects, + has_pull_requests, has_wiki, homepage, is_template, + merge_commit_message, merge_commit_title, + pull_request_creation_policy, squash_merge_commit_message, squash_merge_commit_title, - visibility, web_commit_signoff_required, + use_squash_pr_title_as_default, visibility, web_commit_signoff_required, security_and_analysis }') +topics_json=$(gh api "/repos/$repo/topics" --jq '.names | sort') +automated_security_fixes_json=$(gh api "/repos/$repo/automated-security-fixes" 2>/dev/null --jq '{enabled, paused}' || echo 'null') +private_vuln_reporting_json=$(gh api "/repos/$repo/private-vulnerability-reporting" 2>/dev/null --jq '{enabled}' || echo 'null') +interaction_limits_raw=$(gh api "/repos/$repo/interaction-limits") +if [ "$(jq 'length' <<< "$interaction_limits_raw")" = "0" ]; then + interaction_limits_json="null" +else + interaction_limits_json=$(jq '{limit, origin, expires_at}' <<< "$interaction_limits_raw") +fi +autolinks_json=$(gh api "/repos/$repo/autolinks" --jq '[.[] | {key_prefix, url_template, is_alphanumeric}] | sort_by(.key_prefix)') +# /vulnerability-alerts returns 204 No Content when enabled, 404 when disabled +if gh api "/repos/$repo/vulnerability-alerts" >/dev/null 2>&1; then + vulnerability_alerts_enabled=true +else + vulnerability_alerts_enabled=false +fi + ruleset_details='[]' ruleset_ids=$(gh api "/repos/$repo/rulesets" --jq '.[].id') for rid in $ruleset_ids; do @@ -85,6 +106,7 @@ dependabot_secrets_count=$(gh api "/repos/$repo/dependabot/secrets" --jq '.secre jq -n \ --argjson repo "$repo_json" \ + --argjson topics "$topics_json" \ --argjson rulesets "$ruleset_details" \ --argjson protection "$protection_json" \ --argjson actions_perms "$actions_perms_json" \ @@ -93,12 +115,18 @@ jq -n \ --argjson envs "$envs_json" \ --argjson pages "$pages_json" \ --argjson codeql "$codeql_json" \ + --argjson automated_security_fixes "$automated_security_fixes_json" \ + --argjson private_vuln_reporting "$private_vuln_reporting_json" \ + --argjson interaction_limits "$interaction_limits_json" \ + --argjson autolinks "$autolinks_json" \ + --argjson vulnerability_alerts_enabled "$vulnerability_alerts_enabled" \ --argjson webhooks_count "$webhooks_count" \ --argjson deploy_keys_count "$deploy_keys_count" \ --argjson actions_secrets_count "$actions_secrets_count" \ --argjson dependabot_secrets_count "$dependabot_secrets_count" \ '{ repo: $repo, + topics: $topics, rulesets: $rulesets, default_branch_protection: $protection, actions_permissions: $actions_perms, @@ -107,6 +135,13 @@ jq -n \ environments: $envs, pages: $pages, codeql_default_setup: $codeql, + security: { + vulnerability_alerts_enabled: $vulnerability_alerts_enabled, + automated_security_fixes: $automated_security_fixes, + private_vulnerability_reporting: $private_vuln_reporting + }, + interaction_limits: $interaction_limits, + autolinks: $autolinks, counts: { webhooks: $webhooks_count, deploy_keys: $deploy_keys_count, From a2bd943354f03797ecb024b33779d11732a31077 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Tue, 21 Apr 2026 10:24:23 -0400 Subject: [PATCH 5/5] fix(pr#45): address Copilot actionable findings at source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script robustness: - snapshot + check scripts: guard `--repo` / `--expected` arg parsing against missing value (previously `$2` unbound under `set -u`) - snapshot: sort rulesets by id before emission (stabilizes output across GitHub-side listing-order churn) - snapshot: clarify exit-code docstring (null-tolerant optional endpoints vs. loud-fail required endpoints) Workflow alignment: - github-settings-drift.yml: pin actions/checkout to v6.0.2 SHA matching gate.yml + codeql.yml Doc polish: - FACTORY-HYGIENE.md: reorder row 39 before row 40 - GITHUB-SETTINGS.md: drop broken external-memory cross-references; fold diagnostic command for code_scanning rule inline; shift "Motivation (, date)" → "Motivation (human maintainer, date)" per name-attribution BP - HUMAN-BACKLOG.md: drop external-memory cross-reference from HB-001 - ROUND-HISTORY.md: inline the "blast-radius praise" content instead of cross-referencing by path; external auto-memory paths don't resolve for non-Claude readers The memory/*.md cross-references were relics of when committed docs pointed at Claude's per-project auto-memory (`~/.claude/projects/ /memory/`) as if it were an in-repo directory. For humans and Copilot those paths are broken links; for Claude the information is reachable via the memory tool without a doc cross-reference. Strip them where they appear inline in prose; the established table convention in FACTORY-HYGIENE/ROUND-HISTORY (Origin column) keeps its references for pattern-consistency with the 39+ pre-existing rows. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/github-settings-drift.yml | 2 +- docs/FACTORY-HYGIENE.md | 2 +- docs/GITHUB-SETTINGS.md | 35 +++++++++----------- docs/HUMAN-BACKLOG.md | 2 +- docs/ROUND-HISTORY.md | 26 ++++++--------- tools/hygiene/check-github-settings-drift.sh | 14 ++++++-- tools/hygiene/snapshot-github-settings.sh | 24 +++++++++++--- 7 files changed, 61 insertions(+), 44 deletions(-) diff --git a/.github/workflows/github-settings-drift.yml b/.github/workflows/github-settings-drift.yml index 08002da3..1911cf51 100644 --- a/.github/workflows/github-settings-drift.yml +++ b/.github/workflows/github-settings-drift.yml @@ -53,7 +53,7 @@ jobs: name: check drift runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: check drift env: diff --git a/docs/FACTORY-HYGIENE.md b/docs/FACTORY-HYGIENE.md index 42d0616d..b797cb39 100644 --- a/docs/FACTORY-HYGIENE.md +++ b/docs/FACTORY-HYGIENE.md @@ -81,8 +81,8 @@ is never destructive; retiring one requires an ADR in | 36 | Incorrectly-scoped gap-finder (retrospective) | Every 5-10 rounds (proposed — Aaron 2026-04-20 late) | TBD — candidate skill (queued in BACKLOG P1 alongside row 35); may collapse into a single "scope-hygiene" skill pending row-consolidation research | factory | Retrospective sweep for items whose scope tag is **present but wrong** — factory-level rules tagged `project: zeta`, Zeta-specific rules tagged factory, over-broad / under-broad declarations. Distinct from row 35 (missing tag) — this is *tag-exists-but-incorrect*. Trigger examples: Aaron's 2026-04-20 catch of "cheap-but-zeta-specific governance" in the skill-edit-gating-tiers research doc (factory-scope governance mislabeled as Zeta-specific). Flags candidates to HUMAN-BACKLOG as `scope-correction` rows. | Audit doc per round + HB row per mis-scoped item | `feedback_factory_default_scope_unless_db_specific.md` + `project_factory_reuse_beyond_zeta_constraint.md` + `user_absorb_time_filter_always_wanted.md` | | 37 | WIP-limit discipline (Kanban) | Round open + per-persona session-open (always-on, not triggered) | All agents (self-administered) + Architect for cross-persona visibility | factory | Per-persona in-flight work cap (suggested 3: proposals / findings / drafts not yet landed); cross-persona cap (suggested 7 under architect-bottleneck review queue per GOVERNANCE §11). Over-cap flags to HUMAN-BACKLOG as `wip-pressure` rows. Always-on discipline per `docs/FACTORY-METHODOLOGIES.md` pull-vs-always-on criterion (this row is always-on, therefore not a skill). Cap numbers are *suggestions*; tune after 5-10 rounds of observation per Six Sigma Measure → Improve loop. | Inline self-report + HUMAN-BACKLOG row if over-cap + Architect notebook tally column | `user_kanban_six_sigma_process_preference.md` + `docs/FACTORY-METHODOLOGIES.md` + `docs/research/kanban-six-sigma-factory-process.md` + GOVERNANCE.md §11 | | 38 | Harness-surface cadenced audit | Every 5-10 rounds per populated harness (same cadence as skill-tune-up and agent-QOL). Stubs don't tick. | Per populated harness: Claude owned by Architect (Kenji) **interim** until a dedicated harness-guide role is decided; plugin-provided `claude-code-guide` agent (Anthropic official plugin cache, not a local `.claude/agents/` file) is a reference resource consulted during audit, not the audit runner. Codex / Cursor / GitHub Copilot / Antigravity / Amazon Q / Kiro — TBD when populated (either dedicated guide per harness or a shared multi-harness guide). | factory | Audit each populated harness's platform surfaces for new features, cut features, behavioural changes. For Claude: model / Code CLI / Desktop app / Agent SDK / API. For Codex / Cursor / Copilot / Antigravity / Amazon Q / Kiro: per-harness equivalents inventoried at first-populated audit. Primary feature-comparison axis per harness is skill-authoring + eval-driven feedback loop (the Claude-Code feature that made it Aaron's primary choice — `memory/user_skill_creator_killer_feature_feedback_loop.md`). Update `docs/HARNESS-SURFACES.md` living inventory with adoption statuses (adopted / watched / untested / rejected / stub). When audit surfaces drift, either adopt (ADR if Tier-3), retire the workaround the new feature obsoletes, or record explicit rejection in `docs/WONT-DO.md`. Integration-point tests per harness are owned by a *different* harness per the capability-boundary rule (a harness cannot honestly self-verify its own factory integration from within itself). Triggering incidents: 2026-04-20 AutoMemory miss (Anthropic's Q1-2026 feature mis-attributed as factory-native) + 2026-04-20 multi-harness expansion (Aaron: factory supports multiple harnesses; each tests the others'). | `docs/HARNESS-SURFACES.md` audit log row per cycle per populated harness; ADRs under `docs/DECISIONS/` for Tier-3 adoptions; `docs/research/meta-wins-log.md` entry when a pre-existing factory assumption is found to have been wrong | `feedback_claude_surface_cadence_research.md` + `feedback_multi_harness_support_each_tests_own_integration.md` + `user_skill_creator_killer_feature_feedback_loop.md` + `reference_automemory_anthropic_feature.md` + `reference_autodream_feature.md` | -| 40 | GitHub-settings drift detector | Weekly (cron `17 14 * * 1`) + on any change to `tools/hygiene/github-settings.expected.json` / detector script / workflow file. Added 2026-04-21 after `AceHack/Zeta` → `Lucent-Financial-Group/Zeta` org-transfer silently flipped `secret_scanning` and `secret_scanning_push_protection` from enabled to disabled. | Automated (`.github/workflows/github-settings-drift.yml`); human resolves on drift | factory (ships to adopters as a template; repo-specific expected snapshot per adopter) | Live `gh api` snapshot vs. checked-in `tools/hygiene/github-settings.expected.json`: repo-level toggles (merge methods, security-and-analysis), rulesets + rule contents, classic branch protection on default branch, Actions permissions + variables + counts of secrets, environments + protection-rule types, Pages config, CodeQL default-setup state, webhook / deploy-key / secret counts. Script at `tools/hygiene/check-github-settings-drift.sh` exits 1 on drift and prints `diff -u` output. Resolution: intentional → re-snapshot + commit new expected with rationale; unintentional → revert in GitHub + rerun detector. | `docs/GITHUB-SETTINGS.md` + `tools/hygiene/github-settings.expected.json` + workflow run log + optional `memory/reference_github_*.md` entry if drift source is non-obvious | `feedback_github_settings_as_code_declarative_checked_in_file.md` + `feedback_blast_radius_pricing_standing_rule_alignment_signal.md` + `project_zeta_org_migration_to_lucent_financial_group.md` | | 39 | Hot-file-path detector | Round-cadence (every round close) or every 5-10 rounds — whichever catches churn drift before the next merge-tangle. Proposed 2026-04-21. | TBD — candidate capability skill `hot-file-detector` (queued in BACKLOG P1); Architect runs inline until the skill lands. | factory (ships to adopters as a command-line recipe) | `git log --since="60 days ago" --name-only --pretty=format: \| grep -v '^$' \| sort \| uniq -c \| sort -rn \| head -25` — git history *is* the index. Heuristic threshold: >20 changes in 60d on a single monolithic doc = investigate; >30 = refactor candidate (tune after 5-10 rounds). Per-file decision is one of four: `refactor-split` (per-row, per-round, per-section), `consolidate-reduce` (merge with a sibling), `accept-as-append-only` (legitimately append-only → split into per-round files rather than trimming), or `observe`. Pair with merge-tangle fingerprints from `docs/research/parallel-worktree-safety-2026-04-22.md` §9 — a hot file is worse if also in a recent conflict list. Triggering incident: PR #31 5-file merge-tangle (2026-04-21) where `docs/ROUND-HISTORY.md` at 33 changes / 60d was the #1 conflict source, and `docs/BACKLOG.md` at 26 already has an in-flight split ADR. | Audit doc per cycle listing top-N hot paths + per-file decision; BACKLOG rows for refactor-split candidates; ADRs for structural changes. | `feedback_hot_file_path_detector_hygiene.md` + `docs/DECISIONS/2026-04-22-backlog-per-row-file-restructure.md` (same pattern) | +| 40 | GitHub-settings drift detector | Weekly (cron `17 14 * * 1`) + on any change to `tools/hygiene/github-settings.expected.json` / detector script / workflow file. Added 2026-04-21 after `AceHack/Zeta` → `Lucent-Financial-Group/Zeta` org-transfer silently flipped `secret_scanning` and `secret_scanning_push_protection` from enabled to disabled. | Automated (`.github/workflows/github-settings-drift.yml`); human resolves on drift | factory (ships to adopters as a template; repo-specific expected snapshot per adopter) | Live `gh api` snapshot vs. checked-in `tools/hygiene/github-settings.expected.json`: repo-level toggles (merge methods, security-and-analysis), rulesets + rule contents, classic branch protection on default branch, Actions permissions + variables + counts of secrets, environments + protection-rule types, Pages config, CodeQL default-setup state, webhook / deploy-key / secret counts. Script at `tools/hygiene/check-github-settings-drift.sh` exits 1 on drift and prints `diff -u` output. Resolution: intentional → re-snapshot + commit new expected with rationale; unintentional → revert in GitHub + rerun detector. | `docs/GITHUB-SETTINGS.md` + `tools/hygiene/github-settings.expected.json` + workflow run log + optional `memory/reference_github_*.md` entry if drift source is non-obvious | `feedback_github_settings_as_code_declarative_checked_in_file.md` + `feedback_blast_radius_pricing_standing_rule_alignment_signal.md` + `project_zeta_org_migration_to_lucent_financial_group.md` | ## Ships to project-under-construction diff --git a/docs/GITHUB-SETTINGS.md b/docs/GITHUB-SETTINGS.md index 7e2e1b3e..634073d5 100644 --- a/docs/GITHUB-SETTINGS.md +++ b/docs/GITHUB-SETTINGS.md @@ -12,7 +12,7 @@ The machine-readable companion is That JSON file is **authoritative** — if this markdown ever disagrees with it, the JSON wins and this file gets updated. -Motivation (Aaron 2026-04-21): +Motivation (human maintainer, 2026-04-21): > "its nice having the expected settings declarative defined" > @@ -47,9 +47,7 @@ That silent drift is exactly what this system detects. with a message explaining *why* the setting changed. See `docs/FACTORY-HYGIENE.md` row #40 for the full cadence / -owner / scope specification and -`memory/feedback_github_settings_as_code_declarative_checked_in_file.md` -for the framing this pattern belongs to. +owner / scope specification. ## What's captured @@ -64,7 +62,9 @@ for the framing this pattern belongs to. `use_squash_pr_title_as_default` off (explicit PR-title-or- commit-title selection still applies). - `allow_forking` on — required for the fork-based PR - workflow (see `memory/feedback_fork_based_pr_workflow_for_personal_copilot_usage.md`). + workflow (contributors develop on personal forks and submit + PRs back to this repo; keeps the base repo's cost surface + thin). - Squash commit title: PR title (falls back to commit title for single-commit PRs); squash commit message: concatenated commit messages. @@ -126,12 +126,14 @@ configurations and Zeta uses *advanced-setup* (`.github/workflows/codeql.yml` with `build-mode: manual` for csharp + per-language SARIF upload). The rule returned NEUTRAL / "1 configuration not found" and blocked PR #42 -despite all advanced-setup sub-jobs passing. See -`memory/reference_github_code_scanning_ruleset_rule_requires_default_setup.md` -for the full diagnostic. Re-enabling requires either -(a) enabling default-setup alongside advanced — unverified -coexistence, duplicate compute, or (b) discovering whether -the rule can bind to advanced-setup (untested). +despite all advanced-setup sub-jobs passing. Diagnostic: +`gh api /repos///code-scanning/default-setup +--jq .state` returns `not-configured` on advanced-only +setups; the rule requires this state to be `configured`. +Re-enabling requires either (a) enabling default-setup +alongside advanced — unverified coexistence, duplicate +compute, or (b) discovering whether the rule can bind to +advanced-setup (untested). ### Classic branch protection (on `main`) @@ -237,8 +239,9 @@ git commit -m "chore(settings): " Unintentional drift (detected by the weekly drift workflow or a manual run) is fixed in the opposite direction: revert the setting in GitHub, rerun the detector to confirm match, and -file a `memory/reference_github_*.md` entry if the drift -source is non-obvious. +record the drift source in the PR body (or an ADR under +`docs/DECISIONS/` if the diagnosis is non-trivial and worth +preserving for future maintainers). ## Related @@ -249,9 +252,3 @@ source is non-obvious. - `.github/workflows/github-settings-drift.yml` — cadence workflow. - `docs/FACTORY-HYGIENE.md` row #40 — the hygiene row. -- `memory/feedback_github_settings_as_code_declarative_checked_in_file.md` - — the framing / pattern. -- `memory/reference_github_code_scanning_ruleset_rule_requires_default_setup.md` - — why the `code_scanning` ruleset rule is off. -- `memory/project_zeta_org_migration_to_lucent_financial_group.md` - — the migration that triggered this discipline. diff --git a/docs/HUMAN-BACKLOG.md b/docs/HUMAN-BACKLOG.md index b2871cd2..b6afc254 100644 --- a/docs/HUMAN-BACKLOG.md +++ b/docs/HUMAN-BACKLOG.md @@ -231,7 +231,7 @@ are ordered by `State: Open` first, then `Stale`, then | ID | When | Category | Ask | Source | State | Resolution | |---|---|---|---|---|---|---| -| 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") | Resolved | Executed 2026-04-21 via `POST /repos/AceHack/Zeta/transfer` with `new_owner=Lucent-Financial-Group`. Transfer completed instantly (Aaron admin on both sides). Verification diffed 13 settings groups against pre-transfer scorecard: all preserved **except** `secret_scanning` and `secret_scanning_push_protection` both silently flipped `enabled→disabled` by GitHub's org-transfer code path; re-enabled same session via `PATCH /repos/Lucent-Financial-Group/Zeta` with `security_and_analysis`. Ruleset id 15256879 "Default" preserved byte-identical (6 rules); classic branch protection on main preserved (6 required contexts); Actions variables preserved (2 COPILOT_AGENT_FIREWALL_*); environments + Pages config preserved (Pages URL redirected `acehack.github.io/Zeta` → `lucent-financial-group.github.io/Zeta`). Local `git remote` updated. Declarative settings file landed at `docs/GITHUB-SETTINGS.md` per Aaron's companion directive ("its nice having the expected settings declarative defined" + "i hate things in GitHub where I can't check in the declarative settgins"). Merge queue enable remains a separate opt-in step. | +| HB-001 | 2026-04-21 | decision / org-migration | Plan + execute the migration of `AceHack/Zeta` → `Lucent-Financial-Group/Zeta` (the human maintainer's LFG umbrella org). 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") | Resolved | Executed 2026-04-21 via `POST /repos/AceHack/Zeta/transfer` with `new_owner=Lucent-Financial-Group`. Transfer completed instantly (Aaron admin on both sides). Verification diffed 13 settings groups against pre-transfer scorecard: all preserved **except** `secret_scanning` and `secret_scanning_push_protection` both silently flipped `enabled→disabled` by GitHub's org-transfer code path; re-enabled same session via `PATCH /repos/Lucent-Financial-Group/Zeta` with `security_and_analysis`. Ruleset id 15256879 "Default" preserved byte-identical (6 rules); classic branch protection on main preserved (6 required contexts); Actions variables preserved (2 COPILOT_AGENT_FIREWALL_*); environments + Pages config preserved (Pages URL redirected `acehack.github.io/Zeta` → `lucent-financial-group.github.io/Zeta`). Local `git remote` updated. Declarative settings file landed at `docs/GITHUB-SETTINGS.md` per Aaron's companion directive ("its nice having the expected settings declarative defined" + "i hate things in GitHub where I can't check in the declarative settgins"). Merge queue enable remains a separate opt-in step. | ### For: `any` (any human contributor) diff --git a/docs/ROUND-HISTORY.md b/docs/ROUND-HISTORY.md index b6d9ad4e..b727587c 100644 --- a/docs/ROUND-HISTORY.md +++ b/docs/ROUND-HISTORY.md @@ -118,21 +118,17 @@ narrative) + `tools/hygiene/github-settings.expected.json` (machine-readable canonical state) + `tools/hygiene/check-github-settings-drift.sh` (detector) + `.github/workflows/github-settings-drift.yml` (weekly cron -and PR-triggered cadence). Landed as FACTORY-HYGIENE row #40 -and two companion memories -(`feedback_github_settings_as_code_declarative_checked_in_file.md` -and `feedback_blast_radius_pricing_standing_rule_alignment_signal.md` -— the latter captures Aaron's 2026-04-21 praise -*"this is great standing rules on blast-radius ops ... -i'm glad you understand blast radius and pricing the -blast radius"*, reframing the CLAUDE.md "confirm before -hard-to-reverse actions" discipline as load-bearing -rather than overcautious, and as a Zeta product-feature -signal connecting to the retractable-contract ledger). The -pattern generalizes beyond GitHub — any click-ops platform -(AWS / GCP / Slack / org-level settings) gets the same -markdown-declaration + cadenced-diff treatment when -adopted. +and PR-triggered cadence). Landed as FACTORY-HYGIENE row #40. +Same round: the human maintainer's 2026-04-21 praise — *"this +is great standing rules on blast-radius ops ... i'm glad you +understand blast radius and pricing the blast radius"* — +reframed the CLAUDE.md "confirm before hard-to-reverse +actions" discipline as load-bearing rather than overcautious, +and as a Zeta product-feature signal connecting to the +retractable-contract ledger. The pattern generalizes beyond +GitHub — any click-ops platform (AWS / GCP / Slack / +org-level settings) gets the same markdown-declaration + +cadenced-diff treatment when adopted. --- diff --git a/tools/hygiene/check-github-settings-drift.sh b/tools/hygiene/check-github-settings-drift.sh index 2be3c097..2e6ee019 100755 --- a/tools/hygiene/check-github-settings-drift.sh +++ b/tools/hygiene/check-github-settings-drift.sh @@ -27,8 +27,18 @@ expected="$script_dir/github-settings.expected.json" while [ $# -gt 0 ]; do case "$1" in - --repo) repo="$2"; shift 2;; - --expected) expected="$2"; shift 2;; + --repo) + if [ $# -lt 2 ]; then + echo "error: --repo requires OWNER/NAME argument" >&2 + exit 2 + fi + repo="$2"; shift 2;; + --expected) + if [ $# -lt 2 ]; then + echo "error: --expected requires PATH argument" >&2 + exit 2 + fi + expected="$2"; shift 2;; *) echo "error: unknown arg: $1" >&2; exit 2;; esac done diff --git a/tools/hygiene/snapshot-github-settings.sh b/tools/hygiene/snapshot-github-settings.sh index 201d7598..438d2573 100755 --- a/tools/hygiene/snapshot-github-settings.sh +++ b/tools/hygiene/snapshot-github-settings.sh @@ -20,14 +20,27 @@ # - GitHub Pages config # - CodeQL default-setup state # -# Exit 0 on success; non-zero on any gh call failure. +# Exit 0 on a successful snapshot (even when some optional endpoints +# return null — automated-security-fixes, private-vulnerability-reporting, +# interaction-limits, Pages, and branch protection all tolerate absence +# and coerce to `null` so the emitted JSON stays shape-stable). Exit 2 +# on CLI-argument errors; non-zero from any required `gh api` call +# (repo metadata, rulesets, permissions) propagates through +# command-substitution and will make downstream `jq` fail loudly. set -uo pipefail -repo="${1:-}" -if [ "$repo" = "--repo" ]; then +repo="" +if [ "${1:-}" = "--repo" ]; then + if [ $# -lt 2 ]; then + echo "error: --repo requires OWNER/NAME argument" >&2 + exit 2 + fi repo="$2" -elif [ -z "$repo" ]; then +elif [ -n "${1:-}" ]; then + repo="$1" +fi +if [ -z "$repo" ]; then repo="${GH_REPO:-$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)}" fi if [ -z "$repo" ]; then @@ -68,7 +81,8 @@ else fi ruleset_details='[]' -ruleset_ids=$(gh api "/repos/$repo/rulesets" --jq '.[].id') +# Sort by id to stabilize output across GitHub-side listing order changes. +ruleset_ids=$(gh api "/repos/$repo/rulesets" --jq '[.[].id] | sort | .[]') for rid in $ruleset_ids; do one=$(gh api "/repos/$repo/rulesets/$rid" --jq '{id, name, target, enforcement, conditions, rules: [.rules[] | {type, parameters}]}') ruleset_details=$(jq --argjson one "$one" '. + [$one]' <<< "$ruleset_details")