hygiene: gitignore .claude/worktrees/ — parallel-research scratch#40
Merged
hygiene: gitignore .claude/worktrees/ — parallel-research scratch#40
Conversation
…atch Found during the Round-36-committed-P0 periodic allowlist audit for tools/lint/no-empty-dirs.sh: the lint flagged .claude/worktrees/ (new empty dir, not in allowlist, not in .gitignore). Origin is the parallel-worktree-safety research session — the harness creates the parent dir when it takes out a git worktree and can leave it as an empty dir after the worktree is torn down. Right fix is .gitignore rather than allowlist: the dir is harness- private runtime scratch, not a load-bearing empty dir (which is the allowlist's job — Alloy / TLC output paths that install.sh creates and tools fill). The no-empty-dirs script's "Excluded from scan: any path matched by .gitignore" rule keeps the CI gate quiet. Cross-ref: docs/research/parallel-worktree-safety-2026-04-22.md §9 incident log is the full context for why .claude/worktrees/ appears in the first place.
5 tasks
AceHack
added a commit
that referenced
this pull request
Apr 21, 2026
…ngs-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).
AceHack
added a commit
that referenced
this pull request
Apr 21, 2026
…ngs-as-code + drift detector (#45) * 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). * fix(lints): unblock PR #45 — markdownlint, actionlint, shellcheck - 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. * fix: restore administration:read + actionlint -ignore workaround 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. * snapshot: expand coverage — repo-level keys + 5 new endpoints 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. * fix(pr#45): address Copilot actionable findings at source 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 (<name>, 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/ <slug>/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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
.claude/worktrees/to.gitignoreso the no-empty-dirs CI gate stops flagging the parent dir left behind after a Claude Code parallel-worktree research session.Found by
The Round-36-committed-P0 "Empty-folder allowlist review" — one of two P0s queued in
docs/CURRENT-ROUND.md§Round-36 committed P0. Ranbash tools/lint/no-empty-dirs.sh --listlocally;.claude/worktreesappeared as an unexpected flagged entry.Why gitignore and not allowlist
The allowlist (
tools/lint/no-empty-dirs.allowlist) is for load-bearing empty dirs — paths thatinstall.shcreates and that tools later populate (tools/alloy/classes,tools/tla/specs/states)..claude/worktrees/is harness-private runtime scratch; it shouldn't be a repo concern at all..gitignoreis the right layer per the lint script's "Excluded from scan: any path matched by.gitignore" rule.Cross-reference
Origin context:
docs/research/parallel-worktree-safety-2026-04-22.md§9 — the incident log for the parallel-worktree-safety research pass that produced this leftover state.Side-finding (not in scope for this PR)
While running the lint locally on macOS bash 3.2.57, the script crashes with
FILTERED[@]: unbound variableat line 111 when theFILTEREDarray is empty (happens after removing.claude/worktrees/and when the allowlist targets don't exist yet). CI (Ubuntu bash 5.x) doesn't trigger this — empty-array deref underset -uis bash-3.2-specific. Belongs to FACTORY-HYGIENE #48 (cross-platform parity hygiene); noting here for findability but leaving for a separate fix.Test plan
git check-ignore .claude/worktreesreturns path on this branch.claude/worktrees/absent fromgit ls-files🤖 Generated with Claude Code