Claude Skill

github-ops

GitHub remote operations and README authoring: repo creation, metadata, releases, issue/PR management with preview-before-send, README as a landing page (badge row, features-as-benefits, screenshots, Recent Updates), and read-only security auditing. Triggers on: write a README, i

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_github-ops-3dfaf0b.zip · 69 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/github-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

GitHub Ops

GitHub-side operations skill. Owns everything that talks to api.github.com via gh CLI: repo creation, metadata configuration, releases, and the conventions that govern how 0xDarkMatter repos present on GitHub.

Sits alongside two related skills:

LOCAL                          BRIDGE              REMOTE (GitHub)
─────                          ──────              ───────────────
git-ops                        push-gate           github-ops  (this skill)
Concern Owner
Commits, branches, local tags, rebases, worktrees, stash git-ops
Pre-push secret scan + dirty-tree refusal + confirm push-gate
gh repo create, push to remote, tag push github-ops
Repo description / homepage / topics / visibility github-ops
gh release create + release notes github-ops
README "Recent Updates" section maintenance github-ops
README as a landing page (badge row, features-as-benefits, screenshots) github-ops
Package metadata audit (pyproject/package.json ↔ GH topics ↔ tag ↔ version) github-ops
gh issue operations (view/list/create/comment/edit/triage/close) github-ops
gh pr operations (view/list/diff/checks/create/comment/review/edit/merge/close) github-ops
Security posture audit (Dependabot / secret+code scanning / PVR / SECURITY.md / branch protection) — read-only github-ops (scripts/check-security-posture.sh)
Actions / secrets / social preview / branch-protection writes github-ops (future)

Hard rules

  1. Visibility defaults to private. Pass --private to gh repo create unless the user has explicitly said "public" / "make it public" for this specific repo. See references/repo-visibility.md.
  2. Major version bumps require explicit approval. Default to minor; patch for fix-only ranges. Never auto-suggest a 1.0.0 from BREAKING CHANGE: markers — surface and ask. See references/release-strategy.md.
  3. Always run push-gate before any push to a remote. No exceptions. If push-gate refuses, do not proceed — fix the cause and re-run.
  4. Delegate local git operations to git-ops. Don't reimplement commit/tag/push logic. github-ops orchestrates the GitHub-side calls (gh) and the README/CHANGELOG edits; git-ops handles git itself.
  5. README "Recent Updates" updates on every release. This is the one README touch that always happens, regardless of how minor the release. See references/readme-recent-updates.md for the canonical claude-mods style.
  6. Never push without confirming visibility decision. When creating a new repo, surface visibility as a flippable line in the plan ("creating as private — say 'public' to flip"), not buried in flag soup.
  7. No local-machine paths in committed content. Never bake C:\Users\<name>\…, /home/<name>/…, /Users/<name>/…, /tmp/<one-off-test-dir>, or any other machine-specific path into README entries, Recent Updates bullets, CHANGELOG entries, release notes, tag annotations, or commit messages. Public release artefacts have to read the same on someone else's machine. Use generic placeholders (~/Temp/, <temp-dir>, "a temp directory") or describe the file's purpose abstractly instead. If a path genuinely is part of the project's public API (install location, config path), state it canonically ($HOME/.claude/skills/...), not as a literal absolute that includes a user name.
  8. Preview every public post before sending. Anything with author voice that lands on a third-party surface — gh issue create/comment/edit --body, gh pr create/comment/review/edit --body, gh release create --notes, merge commit --subject/--body — must be quoted verbatim in chat with the exact send command named, then await explicit approval before invoking. Mechanical actions with no body (label, assign, milestone, mark-ready, close-without-message) skip preview. See ~/.claude/rules/public-posts.md for the full rule.

Three modes

Mode new — first publish of a repo

Triggered by: "publish to github", "create repo on github", "push to github" (when no origin remote exists), "ship this repo".

1. Audit (run mode `audit` checklist; abort on critical fail)
   - LICENSE present?
   - README has tagline + install + quickstart?
   - pyproject.toml / package.json has description, keywords, license, repository URL?
   - At least one tag exists (typically v0.1.0)?
   - CHANGELOG.md has an entry for the latest tag?

2. Draft / refine README intro (2–3 paragraphs) — see references/readme-description.md
   - If the README intro is just a tagline or < 80 words, draft a proper 2–3 paragraph
     description: what it is, why it exists, who it's for. Read package metadata, CHANGELOG,
     and the primary entry point first; do not fabricate.
   - Voice: developer-to-developer, concrete, occasional dry wit (earned, never sprayed).
     Anti-patterns ("blazing fast", emoji walls, marketing fluff) listed in the reference.
   - Surface the draft to the user for approval before committing — this is the repo's
     first impression and shouldn't be a one-shot.
   - Commit via git-ops with: docs: Expand README intro

2b. Build the landing-page layer — see references/readme-landing-page.md
   The intro answers "what is this"; this step answers the other three questions a
   cold visitor asks in their first ten seconds (is it alive / what do I get /
   what does it look like). Benefits before mechanics.

   FIRST pick the register, and surface it as a flippable line like visibility:
     "Laying the README out as **Reference** — say 'showcase' to flip"
   - Showcase — reader is deciding WHETHER to adopt. Apps, dashboards, TUIs,
     generators, anything with visible output. Visual high, Features above Install,
     airier prose.
   - Reference — reader has already decided and needs to USE it. Libraries, SDKs,
     plain-output CLIs, internal tooling. Install + a real usage example in the
     first screenful; Features denser and lower.
   Tie-breakers: output is visible → Showcase. It's a dependency of other code →
   Reference. Register controls emphasis and density, NEVER honesty — Showcase is
   not permission for marketing verbs; the readme-description.md anti-patterns
   apply identically to both. Never mix the two.

   Then, in whichever register:
   - Badge row under the title: at most five — license, version, CI, runtime floor,
     project status. One shared labelColor so they read as one row. Never add a badge
     you won't maintain; a stale red CI badge is worse than no badge.
   - ## Features section ABOVE Install, written as benefits (bold lead = what the
     reader gets, then the concrete detail), 4–7 bullets. Not a component inventory.
   - A screenshot or demo ONLY if the project has a visual surface (TUI/GUI/dashboard/
     rendered output, or colourised CLI output). Plain-text CLI and libraries take a
     fenced code block instead. Store under docs/screenshots/, alt text on every image,
     <picture> + prefers-color-scheme so it doesn't glow white in dark mode.
   - Surface the layout to the user with the intro draft; commit together.

3. Add "Recent Updates" section to README if missing
   - Use claude-mods style by default (see references/readme-recent-updates.md)
   - Place after Quickstart, before deep "why this exists" sections — i.e. below the
     Features + visual added in step 2b (see references/readme-landing-page.md for the
     full section order and why liveness sits there, not above Features)
   - For first release, single bullet block describing the initial extraction
   - Commit via git-ops with: docs: Add Recent Updates section

4. Surface the publish plan to user, with visibility as a flippable line:
   "Creating as **private** at github.com/<org>/<repo> — say 'public' to flip"
   Wait for explicit confirmation.

5. Create the repo:
   gh repo create <org>/<repo> --private --source=. --remote=origin \
     --description "<one-line — distilled from the README intro draft in step 2, ≤ 350 chars>" \
     --homepage "<homepage URL or omit>"
   (NEVER pass --push; we want push-gate to run between)
   Note: the GitHub `--description` is a single line and distinct from the README intro.
   Derive it FROM the intro you just wrote, not from package metadata blindly.

6. Run push-gate preflight:
   bash $HOME/.claude/skills/push-gate/scripts/preflight.sh --cwd <repo> origin main
   On any non-zero exit: stop, report, do not push.

7. Push main + tags:
   git -C <repo> push -u origin main
   git -C <repo> push origin --tags

8. Set topics (derived from package keywords + language + frameworks):
   gh repo edit <org>/<repo> --add-topic <t1> --add-topic <t2> ...
   Aim for 6–12 topics. See references/metadata-checklist.md for derivation.

9. Create the release for the latest tag:
   gh release create <tag> --title "<tag> — <one-line headline>" \
     --notes "$(extract from CHANGELOG.md)"

10. Verify:
    gh repo view <org>/<repo>
    gh release view <tag>
    Report URL to user.

Mode update — subsequent release

Triggered by: "ship a release", "cut a release", "release v0.X.Y", "publish update".

1. Audit current state vs last release:
   git -C <repo> log $(git describe --tags --abbrev=0)..HEAD --oneline
   Categorise commits by Conventional Commits prefix.

2. Determine version bump (see references/release-strategy.md):
   - Any feat: → minor (default)
   - Only fix:/chore:/docs:/perf:/style:/test: → patch
   - Any BREAKING CHANGE: or !: → STOP, ask user, never auto-major

3. Update CHANGELOG.md:
   New section for the new version with categorised changes (Added/Changed/Fixed/Removed).
   Delegate the file edit + commit to git-ops with: docs: CHANGELOG for v<N>

4. Update README "Recent Updates":
   Prepend a new version block (claude-mods style) at the top of the section.
   Trim oldest if section exceeds 7 versions.
   Bullets per change, emoji + bold tagline + 1-3 sentence prose.
   See references/readme-recent-updates.md for the emoji vocabulary.

   For minor: update Recent Updates AND scan diff for new commands/config/install steps;
              touch README body sections only if found.
   For patch: update Recent Updates ONLY (single bullet); no body changes unless asked.

   Also: if the README intro is still < 80 words OR the repo's scope has drifted since
   the intro was written, propose an expansion (see references/readme-description.md).
   Don't churn good prose — only act if the intro is genuinely thin or stale.

   Landing-page touch-ups (see references/readme-landing-page.md) — act only on a
   real trigger, never as routine churn. Keep the README's EXISTING register
   (Showcase vs Reference); never switch it silently. A genuine audience change
   (internal tool going public) is worth proposing a switch — done all at once,
   with approval — not drifting into one bullet at a time:
   - The release added a capability worth a Features bullet → add one (benefit-led),
     and cut a weaker one if the section now runs past ~7.
   - The CI badge is red/stale, or the version badge no longer tracks releases →
     fix it or remove it. A badge nobody maintains is worse than no badge.
   - A shipped UI change made an existing screenshot wrong → recapture or drop it.
   - The repo has a visual surface and still has no visual → propose one; don't add
     it unasked.

5. Commit README + CHANGELOG via git-ops:
   docs: Recent Updates + CHANGELOG for v<N>

6. Create local tag via git-ops:
   git tag -a v<N> -m "v<N>"

7. Run push-gate preflight:
   bash $HOME/.claude/skills/push-gate/scripts/preflight.sh --cwd <repo> origin <branch>
   On any non-zero exit: stop, report, do not push.

8. Push commits + tag:
   git push origin <branch>
   git push origin v<N>

9. Create GitHub release:
   gh release create v<N> --title "v<N> — <headline>" \
     --notes "$(extract CHANGELOG section for v<N>)"

10. Verify:
    gh release view v<N>
    Report URL to user.

Mode audit — read-only checklist

Triggered by: "audit github repo", "is this repo ready to publish", "check repo metadata", "score this repo", "how healthy is this repo", "score the fleet".

Headline: scripts/repo-scorecard.sh — one command for a scored repo/fleet health report. It orchestrates the two read-only auditors (check-security-posture.sh + check-issues.sh) and adds metadata / release / actions signals, rolling everything into a single 0–100 score + letter grade per repo, and a matrix + roll-up across an org. Reach for it first; drop to the manual checklist below only when you need a specific row the scorecard doesn't surface.

bash scripts/repo-scorecard.sh --repo 0xDarkMatter/flarecrawl     # single repo: score + dimensions + top 3 fixes
bash scripts/repo-scorecard.sh --org 0xDarkMatter                 # fleet matrix + roll-up (avg/median/worst, fleet open-alert total)
bash scripts/repo-scorecard.sh --org 0xDarkMatter --min-score 75  # CI gate: exit 10 if ANY repo scores < 75
bash scripts/repo-scorecard.sh --repo <o>/<r> --json | jq '.data[0].top_fixes'

Five weighted dimensions — security (35) highest, then metadata (25), release (15), issues (15), actions (10). Each scores its weight in full (ok) / half (warn) / zero (gap or unreadable n/a — an unreadable dimension never counts as healthy). Grade: A≥90 B≥75 C≥60 D≥40 F<40. The full rubric is documented in the script header (--help). It surfaces the top 3 fixes per repo, highest-severity first, each with the exact remediation pointer (e.g. → check-security-posture.sh --repo … --commands, "add CHANGELOG.md", "cut a GitHub release"). Exit 0 healthy · 10 gaps / below --min-score · 7 unavailable (graceful) · 5 gh missing · 2 usage. Strictly read-only — only GET gh api calls + the read-only siblings; the remediation pointers are text, never executed.

Below is the underlying checklist the scorecard's dimensions roll up (and what mode new/update act on). See references/metadata-checklist.md for the complete version; the SKILL enforces these:

LOCAL FILE CHECKS
  [ ] LICENSE file present + matches metadata
  [ ] README has: tagline, install, quickstart, license link
  [ ] README intro is ≥ 80 words (2–3 paragraphs orienting a cold reader)
  [ ] README has "Recent Updates" section near top

LANDING-PAGE CHECKS — all WARN-level, never a hard fail (references/readme-landing-page.md)
  [~] Infer the README's REGISTER first (Showcase = pitch-forward, visual high, Features
      above Install; Reference = install + usage in the first screenful, denser Features)
      and judge every row below against THAT register. A Reference README is not missing
      a hero — it declined one. WARN if the register is visibly mixed (a Showcase hero
      bolted onto a Reference body, or vice versa): it serves neither reader.
  [~] README has a badge row under the title (≤ 5 badges; license + at least one
      liveness signal — CI or version). WARN if absent; WARN if > 7 badges (badge wall)
      or if a CI badge points at a workflow with no runs / a red default branch.
  [~] README has a "## Features" (or equivalent) section ABOVE Install, with bullets
      that lead with what the reader GETS, not what the software contains. WARN if the
      section is missing, or if it is a component inventory / a flag-by-flag table.
  [~] README has a screenshot or demo — CONDITIONAL. Only warn when the project has a
      visual surface (TUI, GUI, dashboard, web UI, rendered/generated output, or
      colourised CLI output). A plain-text CLI, a library, an SDK, or a config/skill
      bundle legitimately has none: report nothing, do not nag. Where images exist,
      WARN on missing alt text or a light-only capture with no <picture> dark variant.
  [ ] CHANGELOG.md present and has entry for latest tag
  [ ] pyproject.toml / package.json: description, keywords, license, repository URL, homepage
  [ ] Latest tag matches version in package metadata

GITHUB STATE CHECKS (skip if no remote)
  [ ] Repo description is set
  [ ] Repo homepage is set (or explicitly N/A)
  [ ] At least 3 topics
  [ ] Topics align with package keywords
  [ ] Default branch is main (not master)
  [ ] Latest tag has a corresponding release
  [ ] Release notes match CHANGELOG entry

SECURITY POSTURE CHECKS (run scripts/check-security-posture.sh — read-only)
  [ ] Dependabot alerts enabled
  [ ] Dependabot security updates enabled
  [ ] Secret scanning + push protection on   (free on public; needs GHAS on private)
  [ ] Code scanning default setup configured  (free on public; needs GHAS on private)
  [ ] Private vulnerability reporting enabled
  [ ] SECURITY.md present (root / .github/ / docs/)
  [ ] Branch protection on the default branch
  [ ] No OPEN dependabot / secret / code-scanning alerts on enabled scanners

The landing-page rows are marked [~] because they are advisory: they never fail an audit and never block mode new. They are also not scored by repo-scorecard.sh — judging "are these bullets benefits" and "does this project have anything to show" needs reading comprehension the script can't do at fleet scale, and a wrong answer there would be charged to every repo. See the reference's closing section for the full rationale.

Output: per-row pass/fail/warn, then a summary score and list of fixes. Fixes are suggested but not applied — the user decides whether to run mode new or mode update to act on them. For the security-posture rows, run scripts/check-security-posture.sh --repo <o>/<r> and fold its checklist in; the enable commands it emits are surfaced for the user to approve, never auto-run.

Operations

Atomic GH-side actions that don't fit the three multi-step modes. Each operation that writes author voice to a third-party surface (issue/PR body, comment, review body, release notes, merge commit subject/body) is governed by hard rule 8 and public-posts: quote the exact body in chat, name the send command, wait for explicit approval, then send. Mechanical actions (labels, assign, close-without-message, mark-ready) skip preview.

Issues

Reads (no preview): gh issue view <n>, gh issue view <n> --comments, gh issue list, gh api repos/<o>/<r>/issues/<n> (for fields not in the default view).

Writes:

Op Command Preview?
Create gh issue create --title --body Yes (title + body)
Comment gh issue comment <n> --body Yes (body)
Edit title/body gh issue edit <n> --title --body Yes
Triage (label/assign/milestone) gh issue edit <n> --add-label … --assignee … --milestone … No (mechanical)
Close / reopen gh issue close <n> / gh issue reopen <n> No, unless closing with a comment — preview the comment
Transfer gh issue transfer <n> <target-repo> No (mechanical), but confirm target with user

See references/issue-ops.md for full playbooks, triage flow, and closing-comment templates.

Pull Requests

Reads (no preview): gh pr view <n>, gh pr view <n> --comments, gh pr list, gh pr diff <n>, gh pr checks <n>, gh pr checks <n> --watch, gh api repos/<o>/<r>/pulls/<n>/comments (inline review comments).

Writes:

Op Command Preview?
Create gh pr create --title --body Yes (title + body)
Comment gh pr comment <n> --body Yes
Review (approve / request changes / comment) gh pr review <n> --approve --body … Yes (body, if any)
Edit title/body gh pr edit <n> --title --body Yes
Edit labels / reviewers gh pr edit <n> --add-label … --add-reviewer … No (mechanical)
Mark ready (un-draft) gh pr ready <n> No (mechanical)
Merge gh pr merge <n> --squash (or --merge / --rebase) No body to preview by default, but explicit user approval required + run pre-merge gate first. If passing --subject / --body, preview those (they become the commit message on main)
Close gh pr close <n> No, unless closing with a comment — preview the comment

PR creation lives here, not in git-ops. git-ops handles local commits/branches/push; the gh pr create call itself talks to api.github.com and belongs in this skill. (Existing git-ops T2 PR-create still works; new flows should route through github-ops.)

Pre-merge gate — never invoke gh pr merge without first confirming:

  1. gh pr view <n> --json mergeable,mergeStateStatus → mergeable: MERGEABLE, mergeStateStatus: CLEAN
  2. gh pr checks <n> → every check passed (or explicitly ignored with user approval)
  3. gh pr diff <n> reviewed — confirm no surprise scope, no committed secrets/local paths, no stale PR-body claims
  4. Merge strategy picked — default squash for fix/feature branches with multiple WIP commits; --merge only when individual commits matter; --rebase for linear-history repos. Ask if uncertain.
  5. Branch deletion is a separate explicit step, not bundled. Default to keeping the branch; delete remote + local after merge only on explicit user OK (it's destructive enough to warrant its own confirmation, and a checked-out branch can't be deleted).

See references/pr-ops.md for full playbooks, review-flow templates, and the merge-strategy decision tree.

Conventions enforced (load reference files for detail)

Convention File Default
Release strategy references/release-strategy.md minor on feat:, patch on fix:-only, major requires approval
README intro (2–3 paragraphs) references/readme-description.md what it is / why it exists / who it's for; concrete, dry, no marketing fluff
README as a landing page references/readme-landing-page.md pick a register first (Showcase pitch-forward vs Reference usage-forward) and never mix; then section order (benefits before mechanics), ≤ 5-badge row with a shared labelColor, features-as-benefits, conditional screenshot in docs/screenshots/ with alt text + dark variant
README Recent Updates style references/readme-recent-updates.md claude-mods per-version blocks (alternate: flarecrawl table)
Repo visibility default references/repo-visibility.md --private unless user says "public"
Metadata audit checklist references/metadata-checklist.md full source-of-truth for mode audit
Issue operations references/issue-ops.md view → triage → comment (with preview) → close; closing comments preview-gated
PR operations references/pr-ops.md create (preview body) → review → pre-merge gate → squash by default; branch deletion separate explicit step

Git authorship

For 0xDarkMatter repos, set repo-local config before any commit work:

git -C <repo> config user.name "0xDarkMatter"
git -C <repo> config user.email "0xDarkMatter@users.noreply.github.com"

Verify with git -C <repo> config user.name. If a commit was made under a different identity before publish (no push has happened), rewrite via:

git -C <repo> rebase --root --exec 'git commit --amend --reset-author --no-edit'

After history rewrite, re-create any tags so they point at the new SHAs:

git -C <repo> tag -d v0.1.0
git -C <repo> tag -a v0.1.0 -m "..."

This is safe pre-publish only. After push, treat history as immutable and set authorship correctly going forward.

Delegation pattern

github-ops           git-ops              push-gate
─────────            ───────              ─────────
mode `new`:
  audit
  edit README   ───► commit (T2)
                                          preflight (before push)
  gh repo create
                ───► push -u origin main
                ───► push --tags
  gh repo edit (topics)
  gh release create
  verify

mode `update`:
                ───► CHANGELOG edit + commit (T2)
  edit Recent Updates
                ───► commit (T2)
                ───► tag (T2)
                                          preflight (before push)
                ───► push (T2)
                ───► push tag (T2)
  gh release create
  verify

When invoking git-ops T2 operations, dispatch to git-agent with a one-shot prompt — no need to load the full git-ops orchestrator state for these mechanical steps.

Future expansion (not yet implemented)

  • Actions — workflow file scaffolding, gh workflow operations
  • Secrets — gh secret set/list/delete (with secure handling)
  • Branch protection — gh api calls for protection rules
  • Social preview — image upload via gh api
  • Org-level — teams, repo templates

When adding any of the above, keep the boundary discipline: anything talking to api.github.com belongs here, anything purely local belongs to git-ops.

Files

File Role
SKILL.md This file — modes, rules, delegation
references/release-strategy.md Version bump policy
references/readme-description.md 2–3 paragraph README intro — voice, structure, anti-patterns
references/readme-landing-page.md The layer between intro and changelog — the Showcase/Reference register choice, section order for each, badge row (shields.io + labelColor), features-as-benefits with a before/after rewrite, screenshot/demo policy, landing-page anti-patterns
references/readme-recent-updates.md "Recent Updates" section format + emoji vocabulary
references/repo-visibility.md Private-by-default policy
references/metadata-checklist.md Audit checklist source of truth
references/issue-ops.md Issue operation playbooks (view/triage/comment/create/close) + preview templates
references/pr-ops.md PR operation playbooks (create/review/merge) + pre-merge gate + merge-strategy decision tree
scripts/repo-scorecard.sh Capstone audit tool. Scored, read-only repo-health matrix — orchestrates check-security-posture.sh + check-issues.sh and adds metadata/release/actions signals into a 0–100 score + grade per repo; --org for a fleet matrix + roll-up; --min-score N to gate CI; --json envelope. Surfaces top-3 fixes per repo. Never mutates
scripts/check-issues.sh Surface open issues you may not have seen (externally-authored + stale) for a repo or remote. Read-only gh issue list; flags author≠owner and untouched-for-N-days
scripts/check-security-posture.sh Read-only repo security-posture auditor. Per-feature checklist (Dependabot alerts/updates, secret scanning + push protection, code scanning, private vuln reporting, SECURITY.md, branch protection), visibility-aware severity, open-alert exposure where a scanner is on, --org fleet sweep. Emits enable commands as text — never applies a change
assets/SECURITY.md.template Copy-ready vulnerability-disclosure policy (supported versions, private reporting via GitHub PVR, response SLAs, scope, safe harbor) — what check-security-posture.sh points at when SECURITY.md is absent

Open-issue awareness (the blind spot)

You don't see issues other people file — your own you know about; a stranger's bug report from two months ago is the gap. scripts/check-issues.sh closes it:

bash scripts/check-issues.sh --repo 0xDarkMatter/flarecrawl   # one repo
bash scripts/check-issues.sh --remote origin --stale-days 14  # derive from a remote
bash scripts/check-issues.sh --json | jq '.data[] | select(.external)'

Exit 0 = nothing you're missing (no open issues, or all are yours and fresh); 10 = external/stale issues present (the things to look at); 7 = unavailable (not a GitHub remote, gh unauthed/offline) — advisory, never a hard failure; 2 usage; 5 gh not installed.

Wired into the pre-push gate (push-gate): preflight.sh calls this in --advisory mode as a post-gate step, so every push surfaces unseen external/stale issues for the target remote. It is read-only, timeout-bounded, and never affects the gate verdict — silent when gh is absent/unauthed or the remote isn't GitHub. Run it standalone any time, or across repos, to find what you've missed. For acting on what it surfaces (view/triage/comment/close), see references/issue-ops.md.

Security posture (the other blind spot)

GitHub ships a stack of free security features — Dependabot alerts, security updates, secret scanning + push protection (free on public repos), code scanning default setup, private vulnerability reporting, branch protection — and most are off by default. You don't see the gap until something leaks. scripts/check-security-posture.sh audits it, read-only:

bash scripts/check-security-posture.sh --repo 0xDarkMatter/flarecrawl   # one repo
bash scripts/check-security-posture.sh --remote origin                  # derive from a remote
bash scripts/check-security-posture.sh --org 0xDarkMatter               # fleet sweep + roll-up
bash scripts/check-security-posture.sh --repo <o>/<r> --commands        # copy-paste enable cmds
bash scripts/check-security-posture.sh --repo <o>/<r> --json | jq '.data[]|select(.state=="off")'

It prints a per-feature checklist — ✓ on / ✗ off [severity] / — n/a (needs GHAS) — and, where a scanner is enabled, the count + max severity of OPEN alerts (the real exposure, not just the toggle). The alert endpoints degrade gracefully: a 403 (token lacks security_events) or 404 (feature off) becomes "n/a — couldn't read", never a false "0 / secure".

Visibility-aware severity is the judgment that makes it usable:

  • public repo → secret scanning, push protection, code scanning are free → a gap is a real finding.
  • private repo without Advanced Security → those three need paid GHAS → reported as a note (n/a), not a nag.
  • Free-on-any-repo (Dependabot alerts/updates, private vuln reporting, SECURITY.md, branch protection) → always a finding when off.
  • Tiers: critical (open critical alerts) · high (open high alerts; push-protection or Dependabot-alerts off on public/active) · medium (secret/code scanning off on public; security-updates off; no branch protection) · low (SECURITY.md absent; private vuln reporting off). Full mapping in the script header.

It never applies a change. It is strictly read-only (only GET gh api calls); the enable commands are emitted as text — gh api -X PUT … for Dependabot alerts/security-updates/private-vuln-reporting/code-scanning, a PATCH body for secret scanning + push protection (push protection requires secret scanning on first), and a pointer to assets/SECURITY.md.template for the policy file. You review and run them yourself, governed by the same preview discipline as any other repo mutation (hard rule 8 — these change repo settings). --commands prints just the enable commands with a # review before running banner on stderr.

Exit 0 = posture clean (all applicable features on, no open alerts); 10 = gaps and/or open alerts (a CI/audit step can branch on it); 7 = unavailable (non-github remote, gh unauthed/offline/timeout) — advisory, never a hard failure; 2 usage; 5 gh not installed. Folds into mode audit (see the Security Posture checklist there).

Files (claude-mods)
  • assets
    • SECURITY.md.template 2.6 KB · in bundle
  • references
    • issue-ops.md 6.6 KB
      # Issue Operations Reference
      
      Per-operation playbooks for `gh issue` workflows. Every write op that has author voice (`--body` / `--title`) is **preview-gated** per hard rule 8 — quote the draft verbatim in chat, name the send command, wait for explicit user approval, then send.
      
      ## Reads (no preview)
      
      ```bash
      # Single issue with metadata
      gh issue view <n> --repo <owner>/<repo>
      
      # Issue + full comment thread (use this when responding — you need the context)
      gh issue view <n> --repo <owner>/<repo> --comments
      
      # Raw JSON for fields the default view omits (createdAt, labels, assignees, etc.)
      gh api repos/<owner>/<repo>/issues/<n> --jq '{number,title,state,user:.user.login,labels:[.labels[].name],body}'
      
      # List + filter
      gh issue list --repo <owner>/<repo> --state open --label bug --limit 20
      
      # Search across repos
      gh search issues "is:open label:bug repo:<owner>/<repo>"
      ```
      
      When reading an issue you intend to respond to, always pull comments too (`--comments`) — replying to the issue body without seeing the discussion is how stale answers happen.
      
      ## Triage (mechanical, no preview)
      
      Labels, assignees, milestones carry no author voice — they're metadata. Surface what you're about to do in chat, but no body preview needed.
      
      ```bash
      # Add labels
      gh issue edit <n> --repo <o>/<r> --add-label "bug,needs-repro"
      
      # Remove labels
      gh issue edit <n> --repo <o>/<r> --remove-label "needs-triage"
      
      # Assign
      gh issue edit <n> --repo <o>/<r> --add-assignee <user>
      
      # Milestone
      gh issue edit <n> --repo <o>/<r> --milestone "v2.11.0"
      ```
      
      If the repo has a label scheme (`bug`/`feat`/`docs`/`question`/`needs-repro`/`good-first-issue` etc.), match it. `gh label list --repo <o>/<r>` to see what exists. Don't invent labels without asking.
      
      ## Comment (preview required)
      
      The flow that gets it wrong without discipline. Always:
      
      1. **Compose** the comment as a quoted block in chat (verbatim, not paraphrased).
      2. **Name** the exact send command.
      3. **Wait** for explicit approval ("send", "ship it", "looks good", or an edit).
      4. **Send** only after approval.
      
      ### Template for the chat preview
      
      > Drafted comment for issue #<n>:
      >
      > ```markdown
      > <comment body, exactly as it would be sent>
      > ```
      >
      > Command: `gh issue comment <n> --repo <o>/<r> --body "..."`
      >
      > Send?
      
      ### Send
      
      ```bash
      gh issue comment <n> --repo <o>/<r> --body "$(cat <<'EOF'
      <approved body>
      EOF
      )"
      ```
      
      Heredoc with single-quoted `'EOF'` so the body is literal (no shell interpolation). Markdown renders on github.com — code fences, links, `@mentions`, `#refs` all work.
      
      ### Tone defaults (project maintainer responding to a reporter)
      
      - Lead with thanks and acknowledge what was right about the report.
      - If a fix shipped, state the version/PR. Link the release if minor+.
      - If the report uncovered related issues, briefly note them — credit goes to the reporter.
      - Sign-off via `@mention` if directly thanking. Don't `@mention` everyone in a thread.
      - Don't apologise excessively or perform humility. Match the project's existing voice.
      - Anti-patterns: marketing fluff, emoji walls, "we'll get right on it" without a concrete plan.
      
      ## Create (preview required — title AND body)
      
      A new issue's title shows in lists and notifications; body is the substance. Preview both.
      
      ### Chat preview
      
      > Drafted issue for `<o>/<r>`:
      >
      > **Title:** `<title>`
      >
      > **Body:**
      > ```markdown
      > <body>
      > ```
      >
      > **Labels:** bug, needs-repro
      > **Assignee:** (none)
      >
      > Command: `gh issue create --title "..." --body "..." --label "..."`
      >
      > Send?
      
      ### Send
      
      ```bash
      gh issue create --repo <o>/<r> \
        --title "<approved title>" \
        --body "$(cat <<'EOF'
      <approved body>
      EOF
      )" \
        --label "<labels>" \
        --assignee "<user>"
      ```
      
      ### Bug report template (recommended body shape)
      
      ```markdown
      ## Problem
      
      <one-paragraph what's wrong + observable symptom>
      
      ## Repro
      
      1. <step>
      2. <step>
      
      ## Expected vs actual
      
      - Expected: <…>
      - Actual: <…>
      
      ## Environment
      
      - Tool / version: <…>
      - OS: <…>
      
      ## Suggested cause / fix (optional)
      
      <…>
      ```
      
      ## Edit title/body (preview required)
      
      Same preview discipline as create — edits are public. The original is also preserved in the issue's edit history so reviewers can see what changed.
      
      ```bash
      gh issue edit <n> --repo <o>/<r> \
        --title "<new title>" \
        --body "$(cat <<'EOF'
      <new body>
      EOF
      )"
      ```
      
      If you're only changing labels/assignees, that's mechanical — preview not required.
      
      ## Close / reopen
      
      Plain close — no preview.
      
      ```bash
      gh issue close <n> --repo <o>/<r>
      gh issue reopen <n> --repo <o>/<r>
      ```
      
      Closing with a reason (`--reason completed|not-planned|duplicate`) — no preview, but state the reason in chat first.
      
      Closing with a parting comment — **preview the comment** (it's a public post), then:
      
      ```bash
      gh issue comment <n> --repo <o>/<r> --body "..."   # preview-gated
      gh issue close <n> --repo <o>/<r>
      ```
      
      ### Closing-comment templates
      
      **Fixed:**
      > Fixed in v<X.Y.Z>. <one-sentence summary of the fix>. <PR link if applicable>.
      
      **Won't fix:**
      > Closing as <reason — out of scope / duplicate of #N / by design>. Brief why: <…>. Happy to reopen if <condition>.
      
      **Not reproducible:**
      > Closing as not reproducible — tried <what>, saw <what>. If you can share <specific thing>, please reopen.
      
      ## Common workflows
      
      ### Respond to a bug report and fix
      
      ```
      1. gh issue view <n> --comments            # context
      2. Reproduce locally; identify cause
      3. Draft fix on branch; PR with "Fixes #<n>" in body
      4. After merge, GitHub auto-closes #<n>; post a closing comment
         with version + link (preview-gated)
      ```
      
      ### Triage incoming issues
      
      ```
      1. gh issue list --label needs-triage      # batch view
      2. For each:
         - gh issue view <n> --comments
         - Apply labels: gh issue edit <n> --add-label … --remove-label needs-triage
         - Assign milestone if scoped
         - If duplicate: comment with "Duplicate of #<m>", close with --reason duplicate
         - If needs more info: comment requesting specifics, add label "needs-repro"
      ```
      
      ### Convert a discussion into an actionable issue
      
      ```
      1. gh issue view <discussion-n> --comments  # distill the actionable scope
      2. gh issue create --title "<scoped title>" --body "<distilled scope, link back to #discussion-n>"
      3. Optionally close the discussion with a link to the new issue
      ```
      
      ## Anti-patterns
      
      - ❌ Replying to an issue without reading its existing comments.
      - ❌ Inventing labels that don't exist in the project's scheme.
      - ❌ Closing with `--reason not-planned` without a comment — leaves the reporter guessing.
      - ❌ `@mentioning` every previous commenter to "ping" them — they already got notified.
      - ❌ Promising a fix or timeline you can't commit to.
      - ❌ Sending a comment without showing the draft to the user first (hard rule 8).
      
    • metadata-checklist.md 3.7 KB
      # Metadata Checklist
      
      Source of truth for mode `audit`. Ordered by criticality.
      
      ## Critical (mode `new` aborts on fail)
      
      | Check | How |
      |---|---|
      | LICENSE file present | `[ -f LICENSE ]` |
      | LICENSE matches package metadata | grep license field in pyproject.toml/package.json, compare to LICENSE header |
      | README.md present | `[ -f README.md ]` |
      | README has tagline | first non-empty paragraph is < 200 chars and not a heading |
      | README intro ≥ 80 words | word-count of prose between title and first `##` heading; see `readme-description.md` |
      | README has install section | `grep -iE '^##\\s+(install|installation|getting started|quickstart)' README.md` |
      | Package metadata file present | `pyproject.toml` (Python) OR `package.json` (Node) OR `Cargo.toml` (Rust) etc. |
      | Package metadata: description set | `jq -r .description` / `tomlq` equivalent |
      | Package metadata: license set | match SPDX identifier |
      | At least one tag | `git tag -l \| head -1` |
      
      ## Important (mode `new` warns; mode `update` requires)
      
      | Check | How |
      |---|---|
      | README has "Recent Updates" section | `grep -iE '^##\\s+recent updates' README.md` |
      | CHANGELOG.md present | `[ -f CHANGELOG.md ]` |
      | CHANGELOG has entry for latest tag | `grep -E "^##?\\s*\\[?v?$(latest_tag)" CHANGELOG.md` |
      | Package version matches latest tag | strip `v` prefix from tag, compare to package version field |
      | Package metadata: keywords ≥ 3 | for topic derivation |
      | Package metadata: repository URL | so install instructions work post-publish |
      | Default branch is `main` | `git symbolic-ref refs/remotes/origin/HEAD` (post-push) or local `git branch --show-current` (pre-push) |
      
      ## GitHub state (skip if no remote yet)
      
      | Check | How |
      |---|---|
      | Repo description set | `gh repo view --json description` |
      | Repo homepage set OR explicitly N/A | `gh repo view --json homepageUrl` |
      | ≥ 3 topics | `gh repo view --json repositoryTopics` |
      | Topics align with package keywords | set comparison; warn on divergence |
      | Latest tag has a release | `gh release view <tag>` exit code |
      | Release notes present (not empty) | `gh release view <tag> --json body` |
      | Release notes match CHANGELOG entry | substring match (allow formatting differences) |
      
      ## Topic derivation
      
      For a fresh publish without explicit topics, derive 6–12 from:
      
      1. **Language** — `python`, `typescript`, `rust`, `go` (from primary language)
      2. **Package keywords** — direct copy from `pyproject.toml` `[project] keywords` or `package.json` `keywords`
      3. **Frameworks** — detected from dependencies (e.g. `react`, `fastapi`, `django`, `astro`, `vue`)
      4. **Domain** — from README headings or package description (e.g. `cli`, `agents`, `ai`, `automation`, `mcp`, `claude-code`)
      5. **Pattern** — recognisable shapes (`job-queue`, `orchestrator`, `daemon`, `headless`, `worktree`)
      
      Cap at 12 (GitHub's limit is 20 but >12 dilutes signal). Validate each topic against GitHub's rules: lowercase, alphanumeric + hyphens, ≤ 50 chars, must start with a letter or number.
      
      ## Output format
      
      For mode `audit`, present results as:
      
      ```
      GITHUB-OPS AUDIT — <repo path>
      
      CRITICAL
        ✓ LICENSE present (MIT)
        ✓ README has tagline + install + quickstart
        ✗ pyproject.toml missing 'description' field
      
      IMPORTANT
        ✓ Recent Updates section present
        ✓ CHANGELOG has entry for v0.1.0
        ⚠ Package keywords: only 2 (recommend ≥ 3 for topic derivation)
      
      GITHUB STATE
        - skipped (no origin remote)
      
      SCORE: 8/11 (1 critical, 1 warning)
      
      NEXT ACTIONS
        1. Add 'description' to pyproject.toml [project] section
        2. Add 1+ keyword to pyproject.toml
        Then: re-run audit, or run mode `new` to publish
      ```
      
      Critical fails block publish. Warnings surface but don't block. GitHub state checks skipped pre-publish.
      
    • pr-ops.md 9.9 KB
      # Pull Request Operations Reference
      
      Per-operation playbooks for `gh pr` workflows. Every write op with author voice (`--body` / `--title` / merge `--subject` / `--body`) is **preview-gated** per hard rule 8 — quote the draft verbatim in chat, name the send command, wait for explicit user approval, then send.
      
      ## Reads (no preview)
      
      ```bash
      # Single PR with metadata
      gh pr view <n> --repo <o>/<r>
      
      # PR + comment thread (the discussion is half the story)
      gh pr view <n> --repo <o>/<r> --comments
      
      # Diff (use this — not local git diff — to see exactly what the PR proposes)
      gh pr diff <n> --repo <o>/<r>
      
      # CI checks (snapshot)
      gh pr checks <n> --repo <o>/<r>
      
      # CI checks (block until all complete — for use during merge gate)
      gh pr checks <n> --repo <o>/<r> --watch --interval 10
      
      # List with filters
      gh pr list --repo <o>/<r> --state open --base main --limit 20
      
      # Inline review comments on specific lines (default --comments shows top-level only)
      gh api repos/<o>/<r>/pulls/<n>/comments --jq '.[] | {path,line,user:.user.login,body}'
      
      # Detailed merge-readiness fields
      gh pr view <n> --repo <o>/<r> --json mergeable,mergeStateStatus,reviewDecision,statusCheckRollup
      ```
      
      ## Create (preview required — title AND body)
      
      PR title shows in lists, notifications, and (for squash merges) becomes the commit message subject on `main`. Body should explain *why* + how to verify. Always preview both.
      
      ### Chat preview
      
      > Drafted PR for `<o>/<r>`:
      >
      > **Title:** `<title>`
      >
      > **Base:** main ← **Head:** `<branch>`
      >
      > **Body:**
      > ```markdown
      > <body>
      > ```
      >
      > Command: `gh pr create --base main --head <branch> --title "..." --body "..."`
      >
      > Send?
      
      ### Send
      
      ```bash
      gh pr create --repo <o>/<r> \
        --base main \
        --head <branch> \
        --title "<approved title>" \
        --body "$(cat <<'EOF'
      <approved body>
      EOF
      )"
      ```
      
      ### Body shape (claude-mods convention)
      
      ```markdown
      ## Summary
      
      <1-3 bullets — what changed and why>
      
      ## Test plan
      
      - [x] <thing that was tested>
      - [ ] <thing the reviewer should test>
      
      Closes #<n>   <!-- if this PR resolves an issue, link it so merge auto-closes -->
      ```
      
      The "Closes #N" / "Fixes #N" footer is load-bearing — it triggers GitHub's auto-close on merge. Without it the issue stays open after the PR lands and someone has to clean up.
      
      ### Draft vs ready
      
      For work-in-progress or pre-review polish, create as draft:
      
      ```bash
      gh pr create … --draft
      ```
      
      Promote later (mechanical, no preview): `gh pr ready <n>`.
      
      ## Comment (preview required)
      
      Same discipline as issue comments. Top-level PR comments are different from inline review comments (next section).
      
      ### Send
      
      ```bash
      gh pr comment <n> --repo <o>/<r> --body "$(cat <<'EOF'
      <approved body>
      EOF
      )"
      ```
      
      ## Review (preview required for body)
      
      Three flavours: `--approve`, `--request-changes`, `--comment` (review-level, not inline). All three may include a body; if they do, **preview the body**.
      
      ```bash
      # Approve with no message — preview not required (the action itself is the signal)
      gh pr review <n> --repo <o>/<r> --approve
      
      # Approve with a parting comment — preview the body
      gh pr review <n> --repo <o>/<r> --approve --body "<approved body>"
      
      # Request changes — body is required and definitely preview-gated
      gh pr review <n> --repo <o>/<r> --request-changes --body "<approved body>"
      
      # Comment-only review (no approve/block) — preview the body
      gh pr review <n> --repo <o>/<r> --comment --body "<approved body>"
      ```
      
      Inline-line comments (`gh api repos/<o>/<r>/pulls/<n>/comments`) are heavier — currently out of scope; use the GitHub UI or extend this reference when first needed.
      
      ## Edit title / body (preview required)
      
      ```bash
      gh pr edit <n> --repo <o>/<r> \
        --title "<new title>" \
        --body "$(cat <<'EOF'
      <new body>
      EOF
      )"
      ```
      
      Mechanical edits (labels, reviewers, milestone, base branch) don't need preview:
      
      ```bash
      gh pr edit <n> --repo <o>/<r> \
        --add-label "ready-for-review" \
        --remove-label "wip" \
        --add-reviewer <user> \
        --milestone "v2.11.0"
      ```
      
      ## Pre-merge gate
      
      **Never invoke `gh pr merge` without running this gate first.** This is the discipline distilled from PR #11.
      
      ```bash
      # 1. Merge-readiness JSON
      gh pr view <n> --repo <o>/<r> --json mergeable,mergeStateStatus,reviewDecision \
        --jq '{mergeable,mergeStateStatus,reviewDecision}'
      # Need: mergeable=MERGEABLE, mergeStateStatus=CLEAN (or UNSTABLE if non-required checks fail)
      
      # 2. All checks pass (use --watch to block during a CI run)
      gh pr checks <n> --repo <o>/<r>
      
      # 3. Review the actual diff one more time
      gh pr diff <n> --repo <o>/<r> | less
      
      # 4. Confirm the PR body still accurately describes the diff
      #    (PRs that grew during review often have stale descriptions — edit first if so)
      gh pr view <n> --repo <o>/<r> --json title,body --jq '.body' | head -50
      ```
      
      Surface the result in chat as a green/red checklist before proposing the merge command. If anything is red, **stop and report** — don't merge through a failing gate.
      
      ## Merge strategy decision tree
      
      | Situation | Strategy | Why |
      |---|---|---|
      | Fix or feature branch with multiple WIP commits all serving one logical change | **Squash** (`--squash`) | Clean single-line history on main; the PR is the unit, individual commits were drafts |
      | Each commit on the branch is independently meaningful and worth preserving in `git log` | **Merge commit** (`--merge`) | Preserves authorship and intermediate steps; useful for collaborative branches |
      | Repo enforces linear history (`gh api repos/<o>/<r> --jq .allow_rebase_merge`) and commits are individually clean | **Rebase** (`--rebase`) | Each commit lands on main as a separate commit, no merge bubble |
      | Mixed / unclear | Ask the user | Don't guess; the project's history style is theirs to decide |
      
      **Default for one-off PRs in this project family (claude-mods, etc.):** `--squash`.
      
      Check repo allowance once via `gh api repos/<o>/<r> --jq '{allow_squash_merge,allow_merge_commit,allow_rebase_merge}'`. If only some are enabled, that constrains the choice.
      
      ### Squash with custom subject/body
      
      When using `--squash`, the merge commit's subject and body land on `main` as a public commit. If passing `--subject` / `--body`, **preview both** (hard rule 8):
      
      ```bash
      gh pr merge <n> --repo <o>/<r> --squash \
        --subject "<approved subject — typically: PR title (#<n>)>" \
        --body "$(cat <<'EOF'
      <approved body — typically: one-paragraph summary of the change>
      EOF
      )"
      ```
      
      Without `--subject` / `--body`, `gh` uses the PR title and the concatenated commit messages — no preview required, but state in chat which you're using.
      
      ## Merge — the final call
      
      After the pre-merge gate is green and the strategy is chosen:
      
      > Pre-merge gate green:
      > - mergeable: MERGEABLE / mergeStateStatus: CLEAN
      > - All 3 checks pass (validate 18s, Socket x2)
      > - Diff reviewed, PR body matches
      >
      > Proposing: `gh pr merge <n> --repo <o>/<r> --squash --subject "<…>" --body "<…>"`
      >
      > Branch deletion: **not bundled** — separate step after merge if you want it.
      >
      > Merge?
      
      Then on approval:
      
      ```bash
      gh pr merge <n> --repo <o>/<r> --squash --subject "..." --body "..."
      ```
      
      ## Branch deletion (separate explicit step)
      
      `gh pr merge --delete-branch` exists but couples merge + delete. **Keep them separate.** Delete branches as a discrete operation after merge, with its own confirmation — both because branch deletion is destructive and because a currently-checked-out branch can't be deleted at all (relevant if the operating worktree is on the PR branch).
      
      ```bash
      # After merge + user OK:
      git -C <local-repo> fetch origin --prune
      git -C <local-repo> checkout --detach origin/main   # leave the branch if it's checked out
      git -C <local-repo> branch -D <branch>              # local
      git push origin --delete <branch>                   # remote
      ```
      
      ## Close (without merging)
      
      ```bash
      # Plain close — no preview
      gh pr close <n> --repo <o>/<r>
      
      # Close with a parting comment — preview the comment first
      gh pr comment <n> --repo <o>/<r> --body "..."   # preview-gated
      gh pr close <n> --repo <o>/<r>
      ```
      
      ### Closing-comment templates
      
      **Superseded:**
      > Closing in favour of #<m>, which takes a different approach: <one sentence>. Thanks for the work here — pieces of the idea live on in the new PR.
      
      **Won't merge:**
      > Closing — after discussion, decided not to land this because <reason>. Detailed why in the thread above. Not a rejection of the code quality, just the direction.
      
      ## Common workflows
      
      ### Standard fix flow
      
      ```
      1. Branch off origin/main
      2. Commit; push branch
      3. Draft PR body in chat, preview with user, gh pr create (preview-gated)
      4. CI runs; gh pr checks --watch
      5. Address review feedback if any (responses preview-gated)
      6. Pre-merge gate
      7. Merge (squash by default; explicit user OK)
      8. Branch cleanup (separate step, explicit OK)
      ```
      
      ### Reviewing a PR
      
      ```
      1. gh pr view <n> --comments     # full context including prior discussion
      2. gh pr diff <n>                # what actually changed
      3. gh pr checks <n>              # CI status
      4. Read the linked issue(s) for original intent
      5. Draft review body in chat → preview → gh pr review (preview-gated)
      ```
      
      ### Hotfix with auto-close
      
      ```
      1. Branch off main, fix, commit, push
      2. PR body includes "Fixes #<bug-issue>" (auto-closes on merge)
      3. Standard merge flow
      4. After merge: post a closing-credit comment on #<bug-issue> with version + PR link
      ```
      
      ## Anti-patterns
      
      - ❌ Merging without running the pre-merge gate (`mergeStateStatus` / checks / diff).
      - ❌ Bundling `--delete-branch` into the merge command — couples two destructive decisions into one.
      - ❌ Defaulting to `--merge` (merge-commit) when squash gives a cleaner history; only use it when individual commits matter.
      - ❌ Sending a review body, PR description, or comment without showing the draft to the user first (hard rule 8).
      - ❌ Promising follow-up work in a PR description and not capturing it as an issue.
      - ❌ Skipping the "Closes #N" footer in PR body — leaves issues unclosed after merge.
      - ❌ Treating `mergeable: UNKNOWN` as green — wait for GitHub to compute (re-poll).
      
    • readme-description.md 7.9 KB
      # README Description
      
      Guidance for the README intro that sits between the title and the first `##` heading. This is the bit a person reads when they land on the repo — not the GitHub one-line description (that's a separate, shorter beast; see `metadata-checklist.md`).
      
      The default tagline-only intro is too thin. Most published 0xDarkMatter repos deserve **2–3 substantial paragraphs** that orient a reader who landed cold from a search result or a link.
      
      ## What it is, not what it does
      
      | Layer | Length | Purpose |
      |---|---|---|
      | Title (`# repo-name`) | ~3 words | Identity |
      | Tagline (one line, optional `>` blockquote) | ≤ 120 chars | The pitch |
      | Intro paragraphs (2–3) | ~150–300 words total | Orientation |
      | Badge row, `## Features`, screenshot | — | The pitch — [readme-landing-page.md](readme-landing-page.md) |
      | Then `## Install` etc. | — | The mechanics |
      
      The intro is *not* a feature list. Save bullets for later sections. This is prose, written like a developer explaining the project to a peer over coffee — concrete, slightly opinionated, not performative.
      
      ## Structure (the three-paragraph shape)
      
      Not a rigid template — a default to vary from when the repo demands it.
      
      ### Paragraph 1 — What it is
      
      - Concrete, specific noun phrase. Not "a powerful framework for..."
      - Name the actual category (CLI, library, plugin, daemon, skill collection, MCP server).
      - One sentence on the *shape* (single binary? plugin pack? long-running daemon? collection of scripts?).
      - One sentence on the *primary surface* (what command/import/endpoint does the user touch first).
      
      ### Paragraph 2 — Why it exists / what it solves
      
      - The pain that prompted it. Real, specific, recognisable.
      - What the existing options were and why they didn't fit. (Tactful — no need to dunk.)
      - The shape of the solution, in one sentence.
      - This is where dry wit can land — naming a frustration accurately is itself a kind of joke.
      
      ### Paragraph 3 — Who it's for / when it's handy
      
      - Who would reach for this tool. Be honest about scope.
      - A scenario or two where it shines.
      - A scenario where it's the wrong choice (this builds trust faster than any feature list).
      - Optional: how it fits alongside related tools the reader probably already knows.
      
      ## Voice
      
      - **Developer-to-developer.** Assume technical literacy; don't explain what a CLI is.
      - **Concrete over abstract.** "Wraps `gh` and adds a confirm step before pushes" beats "streamlines GitHub workflows".
      - **Confidence without pomp.** State what it does. Don't sell.
      - **Occasional dry wit.** Earned, not sprayed. One well-placed observation > three jokes. British understatement scales better than zingers.
      - **Honest about scope.** "Handles the boring 80%" is more trustworthy than "comprehensive solution".
      
      ### Wit calibration
      
      Good wit names something the reader has *also* felt:
      
      > "Because every project eventually needs the same six bash scripts, and writing them again at 11pm is no longer charming."
      
      Bad wit performs cleverness:
      
      > "Behold! A revolutionary new paradigm that will *blow your mind* 🤯"
      
      When in doubt, omit the joke. A clean, plain description is always better than a strained one.
      
      ## Anti-patterns
      
      | Avoid | Why |
      |---|---|
      | "Blazing fast", "powerful", "cutting-edge", "robust" | Marketing words signal nothing. Show specifics. |
      | "Easy to use" | Decided by the reader, not you. |
      | Emoji walls (🚀✨🔥💯) at the top | Reads as AI slop. One contextual emoji is fine; a parade isn't. |
      | Feature bullets in the intro | Save for `## Features` or just let the structure speak. |
      | Comparison tables before saying what the thing is | Orient first, position later. |
      | "This project aims to..." | Just describe what it is, not what it aspires to be. |
      | Auto-generated boilerplate | A reader can spot it instantly. Trust collapses. |
      | Restating the title | "Foo is a tool called foo that does foo things." |
      | Hedging ("might be useful for", "could potentially help") | Either it's for them or it isn't. Say so. |
      
      These cover the intro's *prose*. The layout layer — badge walls, emoji-per-heading,
      oversized demo GIFs, feature tables that restate the API — is covered by
      [readme-landing-page.md](readme-landing-page.md). Note that its Showcase register is
      **not** a licence to relax anything in the table above: every anti-pattern here applies
      in both registers.
      
      ## Process
      
      Before writing, read these in this order:
      
      1. **Existing README** — what's already there? Don't discard prior voice if it's good; refine it.
      2. **Package metadata** — `pyproject.toml` / `package.json` description + keywords. These were chosen for a reason.
      3. **CHANGELOG.md** — the v0.1.0 / first-publish entry often captures the original motivation cleanly.
      4. **Source layout** — top-level dirs and entry points reveal the actual shape.
      5. **Primary entry point file** — read the main script / `__init__.py` / `main.go` opening for any module docstring.
      6. **Tests** — test names often describe the contract more honestly than docs.
      
      Then draft 2–3 paragraphs, read them back as if you'd never seen the repo, and cut every sentence that doesn't add information. The final intro should be **dense** — a reader scanning it should come away knowing what the repo is, why it exists, and whether they should keep reading.
      
      ## When to update vs leave alone
      
      | Situation | Action |
      |---|---|
      | Mode `new` (first publish) | Always draft the intro before publish. This is the reader's first impression. |
      | Mode `update` (subsequent release) | Touch only if scope drifted *or* the original intro was thin. Don't churn good prose. |
      | Mode `audit` | Flag if the intro is < 80 words OR is a single tagline. Suggest, don't auto-edit. |
      | Existing intro is already good | Leave it. Suggest a minor tweak if a release added a major capability. |
      
      ## Worked example
      
      ### Before (the thin version)
      
      ```markdown
      # push-gate
      
      > Pre-push safety checks for git.
      
      ## Install
      ...
      ```
      
      ### After (the 3-paragraph version)
      
      ```markdown
      # push-gate
      
      > Pre-push safety gate for any `git push` to a remote — secret scan, forbidden-file check, divergence check, explicit confirm.
      
      `push-gate` is a Claude Code skill that intercepts pushes to GitHub, GitLab,
      Bitbucket, or any other remote and runs a fast preflight before the bytes
      leave your machine. It layers `gitleaks` with a regex-based secret scan,
      checks for files that shouldn't be in the repo (private keys, `.env`, large
      binaries), confirms the local branch hasn't diverged unexpectedly from its
      upstream, and requires an explicit "yes" before the push proceeds.
      
      It exists because the worst time to discover a leaked AWS key is *after* it's
      in someone else's clone. Pre-commit hooks help, but they only run on commit
      and they're easy to bypass; CI scanners catch leaks too late. `push-gate`
      sits at the last useful checkpoint — the moment between "I've staged
      everything" and "the world has it" — and refuses to let a known-bad push
      through. Refusal is hard, not advisory: there's no `--force-anyway` flag,
      because if there were, you'd use it.
      
      It's most useful for solo developers who don't have org-level secret
      scanning, for repos that mix public and private code, and for the mid-pour
      late-night push where careful review has politely left the building. If you
      already run gitleaks pre-commit and have CI guards on every push, `push-gate`
      is redundant — go enjoy your weekend. If you don't, it's a small skill that
      will eventually save you from a very large incident.
      
      ## Install
      ...
      ```
      
      The difference: a reader of the second version knows what the tool is, why it exists, when to use it, and when *not* to. That's the bar.
      
      ## Length sanity check
      
      | Word count | Verdict |
      |---|---|
      | < 60 | Thin — expand. |
      | 60–150 | Borderline — fine for tiny utilities, light for anything substantial. |
      | 150–300 | The sweet spot. |
      | 300–500 | Acceptable for a complex/foundational repo; tighten if possible. |
      | > 500 | Too long for an intro — split into intro + a `## Why this exists` section. |
      
    • readme-landing-page.md 28 KB
      # README as a Landing Page
      
      Guidance for everything **between** the intro and the deep sections — the part a cold
      visitor actually scans before deciding whether to install. Two sibling references already
      own the ends of that stretch and this one does **not** restate them:
      
      | Layer | Owner |
      |---|---|
      | The 2–3 paragraph prose intro under the title | `readme-description.md` |
      | The `## Recent Updates` changelog block | `readme-recent-updates.md` |
      | Badge row, section order, features-as-benefits, screenshots/demo | **this file** |
      
      The audit floor ("tagline, install, quickstart, license link, intro ≥ 80 words") is a
      *floor*. A README that clears it can still read as a spec sheet. This file is the ceiling.
      
      ## The first ten seconds
      
      A visitor arriving from a search result, a link, or a topic page runs four questions in
      order, mostly below conscious thought:
      
      1. **What is this?** — title + tagline + intro paragraph one.
      2. **Is it alive?** — badge row (CI green, a version that isn't three years old), then
         `## Recent Updates`.
      3. **What do I get?** — `## Features`, written as benefits.
      4. **Can I run it / what does it look like?** — screenshot or demo, then install.
      
      They abandon at the first unanswered question. Almost every weak README fails at (2) or
      (3): it answers "what is this" thoroughly, then jumps straight to `## Installation`,
      leaving the reader to infer value from a `pip install` line.
      
      Note the asymmetry: questions 1–3 are *decisions*, question 4 is *mechanics*. **Benefits
      before mechanics** is not a stylistic preference — it is the order the reader is already
      asking in.
      
      ## Two registers: Showcase and Reference
      
      The four questions are universal; how much room each one gets is not. Pick a **register**
      before laying the page out, and say which one you picked — an unstated register is how a
      README ends up half-pitch and half-manual, serving neither reader.
      
      | | **Showcase** | **Reference** |
      |---|---|---|
      | Reader | Deciding *whether* to adopt | Already decided, needs to *use* it |
      | Arrives from | A link, a topic page, a post, a search | A dependency list, a colleague, their own `go.mod` |
      | Fits | Apps, dashboards, TUIs, generators, plugin packs, anything with visible output | Libraries, SDKs, CLIs with plain output, internal tooling, protocol implementations |
      | Hero | Tagline + badge row + visual, room to breathe | Tagline + badge row, then straight to work |
      | Visual | High, above Features. Often the strongest argument | A fenced code block showing real usage |
      | Features | 4–7 benefit bullets, punchy leads | 4–6 capability bullets, still benefit-led, denser and flatter |
      | Install | Below Features | High — often immediately after the intro |
      | First code | After the pitch | In the first screenful |
      | Prose density | Airier; short paragraphs, whitespace does work | Tight; tables and lists over prose |
      | Length | Longer is fine if it stays scannable | Shorter is a feature; link out for depth |
      
      ### Picking one
      
      Ask what the reader most likely does in the next sixty seconds.
      
      - **They might close the tab** → Showcase. You have to earn the scroll.
      - **They're going to write code against it** → Reference. Get out of the way.
      
      Two useful tie-breakers: if the project's **output is visible**, Showcase is almost always
      right, because a still frame outperforms any paragraph you could write. If the project is
      a **dependency of other code**, Reference is almost always right, because the reader
      reached you from a lockfile and wants a signature, not a story.
      
      **A repo can change register.** An internal tool that gets open-sourced usually should. If
      it does, change it deliberately and all at once — a Reference README with a Showcase hero
      bolted on top reads worse than either.
      
      ### What does NOT vary
      
      This is the load-bearing part. Register controls **emphasis and density**, never honesty.
      Every one of these holds in both:
      
      - No marketing verbs, no "blazing fast", no "powerful", no emoji walls. Showcase is *not*
        permission for the anti-patterns in `readme-description.md` — it earns attention with a
        screenshot and a sharp first sentence, not with adjectives.
      - Bullets still lead with what the reader **gets**. A Reference README's bullets are
        terser and more technical; they are not an inventory of components.
      - Badge discipline is identical: at most five, one shared `labelColor`, none you won't
        maintain.
      - Alt text on every image; the dark-mode variant or a theme-safe capture.
      - Honest scope, including what the thing deliberately does not do.
      
      The difference between the registers is *how much room the pitch gets*, not *whether the
      pitch is true*.
      
      ### Worked contrast
      
      Same project, same facts, both legitimate. Showcase:
      
      > **Stops a leaked key before it leaves your machine.** Runs gitleaks plus a regex
      > layer over the diff and refuses the push on any hit — no `--force-anyway` flag,
      > because you'd use it.
      
      Reference:
      
      > **Refuses on any secret hit** — gitleaks + regex layer over the staged diff. No
      > override flag; exit `1` on detection.
      
      Neither is inflated. The Showcase line spends words on the *reason*; the Reference line
      spends them on the *contract* (exit code, no override). A reader wiring this into CI
      wants the second; a reader deciding whether to install wants the first.
      
      ## Section order
      
      The default shape, in **Showcase** register — the Reference variant follows. Vary either
      when the project demands, but know what you're trading.
      
      ```
      # project-name
      > one-line tagline (<= 120 chars)
      
      [badges: license · version · CI · runtime · status]
      
      <2-3 paragraph intro>                  <- readme-description.md owns this
      
      ## Features                            <- benefits, not inventory
      <screenshot / demo>                    <- inline here, or under Features
      
      ## Install
      ## Quickstart
      
      ## Recent Updates                      <- readme-recent-updates.md owns this
      
      ## Why this exists / How it works / Configuration / Repo layout
      ## Contributing
      ## License
      ```
      
      In **Reference** register the same sections reorder to put working code in the first
      screenful:
      
      ```
      # project-name
      > one-line tagline (<= 120 chars)
      
      [badges: license · version · CI · runtime]
      
      <2 paragraph intro — tighter than Showcase>
      
      ## Install                             <- promoted; one command, no ceremony
      ## Usage                               <- a real, runnable example, not a toy
      ## Features                            <- still benefit-led, denser
      ## API / Configuration / How it works  <- the bulk of the page
      ## Recent Updates
      ## Contributing
      ## License
      ```
      
      Two things survive the reorder and are not negotiable: the **badge row stays under the
      title** (liveness is a zero-scroll signal in both registers), and **Features still leads
      with benefits** — it just sits lower, because a reader who arrived from a lockfile has
      already decided the *what* and needs the *how*.
      
      The status badge matters more here, not less: a library at `alpha` is a load-bearing fact
      for someone about to depend on it.
      
      ### Reconciling "Recent Updates" placement
      
      `readme-recent-updates.md` specifies: *after the hero/tagline + quick install or
      quickstart, before the deep "why this exists" sections.* That still holds — **this file
      does not move it.** What this file adds is that `## Features` and the visual land
      *before* Install, which means Recent Updates now sits after four sections rather than
      two. It stays above the fold-and-a-bit, which was always the point: liveness has to be
      visible without hunting.
      
      The two liveness signals split cleanly by cost:
      
      - The **badge row** is the zero-scroll signal — glanceable, no reading.
      - **Recent Updates** is the confirming signal — read *after* the reader has decided the
        project is interesting enough to check whether it is maintained.
      
      Putting Recent Updates above Features inverts that: you ask someone to read a changelog
      for a thing they have not yet decided they want.
      
      **Exception — high-cadence tooling.** Where releases are the product (a scraper chasing
      anti-bot changes, a wrapper tracking an upstream API), promote Recent Updates above
      Install. Recency *is* the feature there. That is exactly the case
      `readme-recent-updates.md` covers with its table style.
      
      ## Badge row
      
      A short row of shields immediately under the H1 (or under the tagline), answering *what
      is this, is it maintained, can I run it* without a single word being read.
      
      ### Which badges earn their place
      
      Five slots, at most. Each must answer a question a visitor is actually asking:
      
      | Badge | Answers | Include when |
      |---|---|---|
      | **License** | "Can I use this?" | Always. Static, never rots. |
      | **Version / release** | "Is this shipping?" | Once published to a registry, or once tagged releases exist. |
      | **CI status** | "Does it work?" | Only when CI actually runs on every push to the default branch. |
      | **Runtime requirement** | "Can I run it?" | When the floor is a real gate — Python >= 3.11, Node >= 20, a specific engine version. |
      | **Project status** | "What state is this in?" | When the state is not "stable" — `alpha`, `beta`, `experimental`, `private staging`, `archived`. |
      
      The status badge is the one most projects skip and shouldn't. An honest
      `status: experimental` badge does more for trust than a paragraph of hedging, and it
      buys you permission to break things.
      
      ### Which badges are noise
      
      | Badge | Why it's noise |
      |---|---|
      | Downloads / stars / forks | Popularity, not utility. A low number actively repels; a high one persuades nobody who was going to read the code anyway. |
      | Code coverage | A percentage without a denominator. 94% of what? |
      | "PRs welcome" | Say it in CONTRIBUTING, where the reader is when they want it. |
      | "Made with love" / "built with X" | Decoration. |
      | Dependency-freshness services | Third-party uptime you don't control, rendering in your hero. |
      | Chat/community badges on a project with no community | An empty room with a sign on the door. |
      
      **The two failure modes, named:**
      
      - **The badge wall.** Twelve shields wrapped onto three lines reads as insecurity — a
        project arguing for itself before it has said what it is. Five is a row; twelve is a
        plea. If a badge does not change a reader's decision, it costs attention for nothing.
      - **The stale red badge.** A failing CI badge left up for months is *worse than no
        badge*: it converts your one liveness signal into a broadcast that nobody is watching.
        Same for a version badge pinned to a release two years old. **A badge you will not
        maintain should not be added.** If CI is broken and won't be fixed this week, remove
        the badge in the same commit that acknowledges it.
      
      ### shields.io URL construction
      
      Static badge:
      
      ```
      https://img.shields.io/badge/<LABEL>-<MESSAGE>-<COLOR>
      ```
      
      Hyphens inside a segment are escaped by doubling (`--`); underscores or `%20` give a
      space. Live badges use the service endpoints:
      
      ```markdown
      [![License](https://img.shields.io/github/license/OWNER/REPO?labelColor=1b1f24&color=3fb950)](LICENSE)
      [![Release](https://img.shields.io/github/v/release/OWNER/REPO?labelColor=1b1f24&color=3fb950)](https://github.com/OWNER/REPO/releases)
      [![CI](https://img.shields.io/github/actions/workflow/status/OWNER/REPO/ci.yml?branch=main&label=ci&labelColor=1b1f24)](https://github.com/OWNER/REPO/actions/workflows/ci.yml)
      [![Python](https://img.shields.io/badge/python-3.11%2B-blue?labelColor=1b1f24)](https://www.python.org)
      [![Status](https://img.shields.io/badge/status-experimental-orange?labelColor=1b1f24)](#project-status)
      ```
      
      **`labelColor` is the brand lever.** A shields badge has two halves: the left label and
      the right message. `color` paints the message (semantic — green pass, red fail, orange
      warning); `labelColor` paints the label and is *not* semantic, so it is free to carry the
      project's brand colour. Setting the same `labelColor` on every badge is what turns five
      independent shields into one coherent row instead of a ransom note. Pick one dark neutral
      or one brand hex, apply it to all of them, and let only the right half vary.
      
      Other parameters worth knowing, and their costs:
      
      - `style=flat` (default), `flat-square`, `for-the-badge`. Pick one and use it across the
        whole row. `for-the-badge` is loud and doubles the row's height — reserve it for a
        project with exactly one or two badges.
      - `logo=<simple-icons slug>` + `logoColor=` — a logo per badge is charming once and
        cluttered five times. Use it on none or on all.
      - `?branch=main` on the Actions badge. **Omit it and the badge reports the most recent
        run on any branch**, which means a red badge from someone's failed feature branch.
        This is the single most common badge misconfiguration.
      - `cacheSeconds=` — shields caches aggressively anyway; setting this rarely helps and a
        low value just makes your README slower to paint.
      
      Every badge is a link, and the link must go where the badge's claim can be verified:
      license badge to `LICENSE`, CI badge to the workflow's runs page, version badge to
      releases. A badge that links nowhere is decoration wearing a data costume — acceptable
      only for a pure-declaration status badge, and even then prefer an in-README anchor that
      explains the status.
      
      ## Features as benefits, not inventory
      
      The discipline: **each bullet leads with what the reader gets, not what the software
      contains.** An inventory bullet describes the codebase; a benefit bullet describes the
      reader's day after they install it.
      
      The mechanical test — read the bullet and ask *"so what?"*. If there is an obvious
      unstated answer, that answer was the bullet.
      
      ### Worked example — before
      
      ```markdown
      ## Features
      
      - Built-in gitleaks integration
      - Regex-based secret scanning layer
      - Forbidden-file checklist (`.env`, `*.pem`, `id_rsa`)
      - Upstream divergence detection
      - Interactive confirmation prompt
      - Configurable via `.push-gate.toml`
      ```
      
      Six true statements about the implementation. Every one of them makes the reader do the
      translation work themselves, and the last one — configuration — has no business being a
      headline feature at all.
      
      ### Worked example — after
      
      ```markdown
      ## Features
      
      - **Stops a leaked key before it leaves your machine.** Runs gitleaks plus a
        regex layer over the diff, and refuses the push on any hit — no
        `--force-anyway` flag, because you'd use it.
      - **Catches the files you never meant to track.** `.env`, private keys, and
        stray credential dumps are checked by name, not just by content.
      - **Tells you when your branch has drifted.** Compares against upstream before
        the push, so a surprise force-push never happens by reflex.
      - **One confirm step, at the last useful moment.** Between "staged everything"
        and "the world has it" — the only checkpoint that still catches mistakes.
      ```
      
      Four bullets instead of six, more words, and dramatically more decision-value. What
      changed:
      
      - **Bold lead is a claim about the reader**, not a component name. The detail follows in
        the same bullet, so nothing was lost — the gitleaks fact is still there, now attached
        to the reason it matters.
      - **Consequences, stated.** "Refuses the push" and "no `--force-anyway` flag" tell you
        how opinionated the tool is, which is exactly the thing a reader is trying to work out.
      - **The config bullet is gone.** Configurability is a property of nearly all software; it
        belongs in a `## Configuration` section, where it is useful, not in the pitch.
      - **Merged where the reader wouldn't distinguish.** Gitleaks and the regex layer are two
        implementations of one benefit. Splitting them padded the list without informing anyone.
      
      ### The rules that fall out of it
      
      - **4–7 bullets.** Under four and the section looks thin; over seven and nobody finishes
        it. If you have twelve features, you have three benefits and nine details.
      - **Bold lead, <= 10 words**, scannable on its own. A reader who reads only the bold
        fragments must still come away knowing what the project does.
      - **One or two sentences of detail** after the lead, carrying the concrete nouns —
        command names, file names, real numbers.
      - **Verbs the reader owns**, not verbs the software owns: "stops", "catches", "tells
        you", "saves you" — not "provides", "supports", "enables", "leverages", "offers".
      - **No feature that is table stakes.** "Cross-platform", "configurable", "well-tested",
        "documented" — these are absence-noticed, presence-ignored.
      - **Honest scope earns trust.** One bullet naming what it deliberately does *not* do is
        worth three that gild what it does. Same principle as the intro's "when it's the wrong
        choice" paragraph.
      
      ## Screenshots and demo media
      
      ### When a visual earns its place
      
      A visual is worth its weight only when the project has a **visual surface** — something a
      still frame or a short clip can show that prose cannot:
      
      | Project shape | Visual? |
      |---|---|
      | TUI, dashboard, GUI, web UI, generated diagrams/art | **Yes.** The output is the pitch. |
      | CLI with formatted, colourised, or tabular output | **Yes** — a terminal capture of one real run. |
      | CLI with plain text output | Usually a fenced code block, not an image. Cheaper, copyable, searchable, diffable. |
      | Library, SDK, or API surface | **No.** A code block *is* the screenshot. |
      | Agent skill / prompt pack / config bundle | Usually no. If it produces a rendered artefact, show the artefact. |
      
      **A fenced code block beats a screenshot of a terminal every time the content is plain
      text**: it is selectable, greppable by search engines, survives dark mode for free, costs
      no bytes, and shows up in the diff when it goes stale. Reach for an image only when the
      *rendering* is the information — colour, layout, glyph alignment, a real UI.
      
      Conversely, a project *with* a visual surface and no visual is leaving its strongest
      argument on the floor. Nobody installs a dashboard on the strength of a bullet list.
      
      ### Static vs animated
      
      Default to **static**. A well-chosen still of the finished output answers "what does this
      look like" instantly, and the reader controls their own pace.
      
      Reach for animation only when **the motion is the information** — a multi-step flow, a
      progressive reveal, a before/after transition that a still cannot convey.
      
      When you do, the costs are real and worth naming:
      
      - **File size.** An animated GIF of a terminal session runs 5–20 MB with no effort at
        all. It is downloaded by everyone who opens the README, on mobile data included, before
        they have decided they care. A 12 MB GIF above the fold is a hostile act. **Budget: 2
        MB, hard.** Shorter loop, fewer frames, smaller capture window, fewer colours.
      - **Accessibility.** A GIF cannot be paused, and auto-playing motion is a genuine problem
        for readers with vestibular sensitivity — WCAG 2.2 SC 2.2.2 asks that motion lasting
        more than five seconds be pausable, and a GIF offers no control at all. Keep loops under
        five seconds, or use a `<video>` (which can carry `controls`) instead.
      - **Legibility.** GIF's 256-colour palette wrecks anti-aliased terminal text. If the
        reader cannot read the commands in the recording, the recording is decoration.
      
      **Better than a GIF, in order:** an [asciinema](https://asciinema.org) recording linked
      by its still-image badge (text-based, selectable, kilobytes not megabytes); an MP4/WebM
      in a `<video controls loop muted>` block; an animated `.webp` (same motion, a fraction of
      the bytes); a static still linked to a longer recording hosted elsewhere. Plain GIF is
      the last resort, not the default.
      
      ### Where the files live
      
      **`docs/screenshots/`** — never the repo root. This is the `agentic-quality` rule
      ("repo root is sacred") applied to media: a root littered with `screenshot1.png` and
      `demo-final-v2.gif` is exactly the drift that rule exists to stop. Name files for what
      they show and keep the names stable, since the README links them by path:
      
      ```
      docs/screenshots/dashboard-overview.png
      docs/screenshots/dashboard-overview-dark.png
      docs/screenshots/scan-run.webp
      ```
      
      Reference them with a **repo-relative path** (`docs/screenshots/x.png`), not a raw
      `raw.githubusercontent.com` URL — the relative path keeps working in forks, in a local
      preview, and after a rename of the default branch.
      
      If the images are heavy enough to bloat clones, host them off-repo (a release asset, a
      GitHub issue-comment upload) and link by absolute URL. That is a deliberate trade, not
      the default: repo-relative is more durable, off-repo is lighter.
      
      ### Alt text, always
      
      Every image carries alt text describing **what the picture shows**, not what the file is:
      
      ```markdown
      ![Scorecard output: five weighted dimensions with per-repo grades and the top three fixes](docs/screenshots/scorecard-run.png)
      ```
      
      Not `![screenshot]`, not an empty `![]()`, not `![demo gif]`. Screen readers read it
      aloud, and it is what renders when the image 404s after a path change — which is the state
      most stale READMEs are in. Purely decorative images take an empty alt deliberately, but a
      README image is almost never decorative.
      
      ### Dark mode: the `<picture>` pattern
      
      A screenshot captured on a light background glows like a torch inside GitHub's dark
      theme, which is what most readers are using. Ship both and let the browser choose:
      
      ```html
      <picture>
        <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/overview-dark.png">
        <source media="(prefers-color-scheme: light)" srcset="docs/screenshots/overview-light.png">
        <img alt="Fleet matrix: one row per repo, columns for each scored dimension" src="docs/screenshots/overview-light.png">
      </picture>
      ```
      
      The `<img>` fallback is mandatory, not optional — it is what renders anywhere `<picture>`
      is not honoured (npm, PyPI, many mirrors, plain-markdown viewers), so its `src` must be
      the variant that reads acceptably on *either* background, and its `alt` is where the alt
      text lives.
      
      **The cheaper alternative:** capture the shot with a background that survives both
      themes. A terminal capture on a mid-dark background, or a UI shot with a defined border,
      needs no `<picture>` block at all. One asset, no divergence, nothing to keep in sync.
      
      ### GitHub's HTML subset — what actually works
      
      GitHub sanitises README HTML aggressively. Assume this narrow set and nothing more:
      
      - **Works:** `<picture>` / `<source>` / `<img>` (with `width`, `height`, `align`, `alt`),
        `<video>` (with `controls`, `loop`, `muted`, `src`), `<details>` / `<summary>`,
        `<table>`, `<sub>` / `<sup>` / `<kbd>`, `<br>`, `<div align="center">`, `<a>`.
      - **Stripped:** `<style>` blocks and `<script>` entirely; `style=` attributes; `class=`
        and `id=` (heading anchors are generated, not authored); CSS custom properties; iframes;
        form elements.
      
      Consequences to plan around: you cannot theme with CSS, so theme-awareness runs through
      `prefers-color-scheme` in `<picture>` and nowhere else. You cannot centre with CSS, so
      `<div align="center">` is the only lever — use it sparingly (see anti-patterns). Anything
      needing real layout belongs on a docs site the README links to, not in the README.
      
      Markdown inside an HTML block needs a blank line to be parsed; without one it renders
      literally. This is why a `<details>` section's body so often comes out as raw asterisks.
      
      ## Anti-patterns (the landing-page layer)
      
      `readme-description.md` covers prose fluff — "blazing fast", marketing verbs, emoji walls
      in the intro. These extend that list to the layout layer:
      
      | Anti-pattern | Why it fails |
      |---|---|
      | **Emoji per heading** (`## Installation` decorated with a rocket, `## Features` with sparkles) | Adds zero information and burns the reader's novelty budget on navigation furniture. Emoji work as *content* markers (the Recent Updates vocabulary) precisely because headings stay clean. |
      | **Centred everything** | `<div align="center">` on the hero is fine. Applied to prose, feature lists, and code blocks it destroys the left edge the eye scans down, and looks visibly broken at narrow widths. |
      | **A 12 MB demo GIF** | Downloaded by everyone before they've decided they care. Unpausable, unreadable, and the single heaviest thing in most repos. Budget 2 MB. |
      | **A features table restating the API** | A table of every flag and its description is *reference documentation* filed under Features. The reader wanted five reasons to install; they got a man page. Link the reference; keep the benefits. |
      | **The badge wall** | Twelve shields reads as insecurity. Five, one `labelColor`, each answering a real question. |
      | **A red CI badge left standing** | Worse than no badge. It broadcasts that nobody is watching. |
      | **Screenshot of text that should be a code block** | Unsearchable, uncopyable, unreadable on mobile, and stale the moment output changes — with nothing in the diff to say so. |
      | **Broken or absent alt text** | A bare `![screenshot]` tells a screen-reader user nothing and renders as noise when the path rots. |
      | **A table of contents on a short README** | Below ~200 lines, GitHub's own outline widget already does it. A hand-maintained ToC is a second thing to keep in sync. |
      | **"Star this repo" / sponsor plea above the fold** | Asks for payment before delivering value. Bottom of the README, after the reader has decided. |
      | **Duplicated install instructions** (badge, hero, and Install section) | Three copies drift; the reader learns to trust none of them. |
      | **A hero image that is just the project name in a font** | Costs a network round-trip to say what the `# H1` already said, and is invisible to search. |
      | **Mixed register** | A Showcase hero bolted onto a Reference body (or a library that opens with a lifestyle screenshot). Reads as indecision, and both audiences bounce. Pick one and commit. |
      | **"Marketing register" as a licence for fluff** | Showcase means *more room for the pitch*, not *permission to inflate it*. Marketing verbs are banned in both registers. |
      
      ## Applying this in the three modes
      
      | Mode | Action |
      |---|---|
      | `new` | **Pick the register first and say which**, as a flippable line the user can overrule ("laying this out as **Reference** — say 'showcase' to flip"), the same way visibility is surfaced. Then the full treatment: badge row, Features-as-benefits, and a screenshot **if** the project has a visual surface. Surface the draft README for approval before committing — this is the first impression. |
      | `update` | Do not churn a good landing page, and **do not silently switch register** — a repo that reads as Reference stays Reference unless the user asks. Act only when: a release added a capability worth a new Features bullet, the CI badge has gone stale or wrong, or a screenshot no longer matches the UI. A genuine audience change (internal tool going public) is worth *proposing* a register switch, done all at once. |
      | `audit` | Report WARN, never a hard fail. **Infer the register from the existing README and judge against that one** — a Reference README is not missing a hero, it declined one. Missing badge row is a WARN in both. No Features/benefits section is a WARN in both. No screenshot **when the project has a visual surface** is a WARN; when it does not, stay silent. A visibly mixed register is a WARN worth naming. Suggest, don't auto-edit. |
      
      **The conditional matters.** A CLI library legitimately has no screenshot, and a check
      that nags it every audit teaches the reader to ignore the audit. Decide "does this project
      have a visual surface" from the repo's actual shape — entry points, whether it renders
      anything, whether existing docs contain images — and stay quiet when the answer is no.
      
      ### Why this isn't in `repo-scorecard.sh`
      
      Deliberate. The scorecard is a mechanical, fleet-scale, read-only tool scoring signals
      that are unambiguous from the GitHub API (does a LICENSE exist, are there >= 3 topics, is
      the latest tag released). The landing-page checks are not that shape:
      
      - "Has a benefits section" requires judging whether bullets lead with benefits — reading
        comprehension, not pattern matching.
      - "Should this have a screenshot" requires judging whether the project has a visual
        surface, which no README-shaped heuristic can answer.
      - Both would need the README *body* fetched and parsed per repo, adding an API call and a
        pile of false positives to a fleet sweep whose whole value is that its findings are all
        real.
      
      So these live in mode `audit`, where an agent has the repo in hand and can exercise
      judgment, and out of the scorecard, where a wrong answer is charged to every repo in the
      fleet. If a future version does score them, it belongs in the metadata dimension, and the
      rubric in the script's `--help` header must be updated in the same commit.
      
    • readme-recent-updates.md 7.8 KB
      # README "Recent Updates" Section
      
      Every published 0xDarkMatter repo's README has a **"Recent Updates"** section as a first-class element near the top.
      
      **Why:** Visitors immediately see velocity + what's new without clicking through to CHANGELOG.md. Surfaces project liveness and recent capability adds at a glance.
      
      **Canonical example (DEFAULT style):** https://github.com/0xDarkMatter/claude-mods
      **Alternate (denser, table-based):** https://github.com/0xDarkMatter/flarecrawl
      
      ## Default style — claude-mods
      
      Per-version blocks with emoji-prefixed bullets. Use this unless the project's release cadence is so high (multiple per day) that the table style is justified.
      
      ```markdown
      ## Recent Updates
      
      **v2.4.3** (April 2026)
      
      *   🌳 **Worktree-aware `git-ops`** - Folded the briefly-considered `git-status` skill straight into `git-ops` rather than ship a third sibling. T1 inline now exposes `scripts/status.sh` (rich repo overview...)
      *   🛡️ **`push-gate` skill** - Hard pre-push safety gate. Gitleaks + regex layer secret scan, forbidden-file check, divergence check...
      *   📌 **`rules/worktree-boundaries.md`** - Hard rule promoted from user-global into the plugin: never `rm -rf .claude/worktrees/`...
      
      **v2.4.1** (April 2026)
      
      *   🎭 **13 output styles** - Added 8 daemon personalities from private-project: Atlas (strategic advisor), Coach (momentum builder)...
      
      [View full changelog →](https://github.com/0xDarkMatter/<repo>/commits/main)
      ```
      
      ### Style rules
      
      - Version header: `**v2.4.3** (Month YYYY)` — bold version, month-year in parens (NOT ISO date)
      - Each change is a bulleted item under the version
      - Bullet prefix: relevant **emoji** + **bold tagline** (often a skill name in backticks like `` `push-gate` skill `` or a capability label)
      - Followed by ` - ` and a **1–2 sentence** prose description with concrete details (file names, flag names, key counts, links to references)
      - Multiple bullets per version is normal and good — one bullet per discrete change
      - 5–7 most recent versions visible; link "View full changelog →" at the bottom to the commits view
      - External references (other tools, articles, posts) get inline markdown links
      
      ### Length discipline
      
      Each bullet should be **scannable in one breath** — roughly 30–60 words after the bold tagline. If a bullet runs longer:
      
      - Drop parenthetical category lists ("(`PRUNABLE` / `WIP` / `GHOST` / `ORPHAN`)") — these belong in skill docs, not release notes
      - Drop sub-features ("Plus a harness whitelist on Gate 1: ...") — split into a separate bullet or omit
      
      Rule of thumb: a release block of 4 bullets averaging 40 words each (~160 words total) reads cleanly. A block of 5 bullets averaging 80 words each (~400 words) becomes a wall and visitors skim past it.
      
      Long bullets erode the value of the section — visitors should see velocity at a glance, not have to read paragraphs to extract what shipped.
      
      ### Recent Updates is for *features*, not bugs
      
      Recent Updates surfaces **capability changes and direction**. Bug fixes go in `CHANGELOG.md`.
      
      **Include a `🐛` bullet only when one of these is true:**
      
      1. **The bug fix IS the release.** A patch release whose entire purpose is the fix (e.g. `v2.0.1` shipped specifically to address a regression).
      2. **You're closing a loop.** The bug was previously called out in a Recent Updates entry as a known issue, and this release resolves it.
      3. **The fix is the headline of a larger release.** If the most important thing a minor release ships is fixing a long-standing issue, lead with it.
      
      **Exclude bug fixes that are:**
      
      - Pre-existing issues squashed during unrelated feature work (the most common silent failure)
      - Fixes for bugs that weren't user-visible enough to be previously flagged
      - "Technically user-visible" but discovered and fixed without anyone reporting them
      - Anything where the fix is one of several changes in the release rather than the focus
      
      **Test for inclusion:** if the bullet starts with `🐛` and you're writing it because *you remembered the fix happened*, not because *the user is waiting for it* — it doesn't go here. Send it to `CHANGELOG.md`.
      
      The failure mode is silent: a 🐛 bullet appears that probably shouldn't, and there's nothing in the rule that flags it. The section drifts toward CHANGELOG. Recent Updates should answer "*what's new in capability?*", not "*what got fixed?*"
      
      ## Alternate style — flarecrawl (table)
      
      Only use when the project ships so frequently that the per-version block format would dominate the README.
      
      ```markdown
      ## Recent Updates
      
      | Version | Date | Changes |
      | --- | --- | --- |
      | **v0.22.0** | 2026-04-21 | **Secure credential storage.** OS keyring via `flarecrawl[secure]`. Auto-migrates legacy plaintext config.json. 1112 tests |
      | **v0.21.0** | 2026-04-20 | **Auth + crawl fixes.** `--browser-cookies` on scrape/interact/design (was videos-only). `--session` on crawl. `--ignore-robots` made actionable |
      
      For older releases, see [CHANGELOG.md](CHANGELOG.md).
      ```
      
      Table style uses ISO dates (YYYY-MM-DD) since it's denser. One row per version, summary in single cell with bold tagline lead.
      
      ## Update cadence
      
      - **Patch** release: single-bullet block describing the fix
      - **Minor** release: 2–6 bullets covering each shipped change
      - **Major** release: lead with the breaking change, then enhancements
      
      Update on **every** release regardless of size. This is the one README touch that always happens.
      
      ## Placement in README
      
      - After the hero/tagline + quick install or quickstart
      - Before the deep "Why this exists" / feature comparison sections
      - High enough to be visible without scrolling on a typical browser
      
      The full section order — including the badge row and `## Features` that sit above this
      section — is owned by [readme-landing-page.md](readme-landing-page.md). Two things from
      there matter when placing this block:
      
      - **This section is the *confirming* liveness signal, not the first one.** The badge row
        answers "is this alive" at zero scroll; Recent Updates confirms it for a reader who has
        already decided the project is interesting. So it belongs *below* Features, not above —
        don't ask someone to read a changelog for a thing they haven't decided they want.
      - **The exception is high-cadence tooling**, where recency *is* the feature (a scraper
        chasing anti-bot changes, a wrapper tracking an upstream API). There, promote it above
        Install — which is the same case the table style below exists for.
      
      ## Trim policy
      
      When the section grows past ~7 versions, trim oldest version blocks atomically with adding the new one (same commit). CHANGELOG.md keeps the full history.
      
      ## Emoji vocabulary
      
      Used consistently across claude-mods. Pick the closest match for each bullet; introducing new emoji is fine when no existing one fits.
      
      | Emoji | Meaning |
      |---|---|
      | 🚀 | launch / major capability |
      | 🔄 | refactor / rename |
      | 🛠️ | tooling |
      | 🛡️ | security / safety |
      | 🌳 | worktree / structural |
      | 📌 | rule / policy |
      | 📬 | messaging / inter-process |
      | 🎭 | personalities / styles |
      | 🐛 | bug fix |
      | 🆕 | new addition |
      | 📚 | docs |
      | 🎯 | architecture / pattern |
      | 🎨 | design / generative |
      | 📐 | spec / standards |
      | 🔍 | introspection / observability |
      | 🔧 | config / settings |
      | 🗑️ | removal |
      | 🔁 | loop / iteration |
      | ⚡ | performance |
      | 📦 | packaging / distribution |
      | 🧪 | tests |
      | 🔌 | integration / plugin |
      
      ## Adding the section to a new repo (mode `new`)
      
      For first publish (only v0.1.0 exists), generate a single block summarising the initial release:
      
      ```markdown
      ## Recent Updates
      
      **v0.1.0** (Month YYYY)
      
      *   🚀 **Initial release** - <one-paragraph summary of what shipped, including key counts (LOC, tests), capability headlines, and any notable provenance>
      
      [View full changelog →](https://github.com/<org>/<repo>/commits/main)
      ```
      
      Place it between Quickstart and the deep "why this exists" sections.
      
    • release-strategy.md 2.9 KB
      # Release Strategy
      
      Default version-bump policy for github-ops mode `update`.
      
      | Change type | Bump | Example |
      |---|---|---|
      | New feature, capability, command, integration | **minor** (default) | 0.1.0 → 0.2.0 → 0.3.0 |
      | Bug fix, small QoL tweak, doc-only fix, dep bump | **patch** | 0.2.0 → 0.2.1 → 0.2.2 |
      | Breaking change / 1.0.0 promotion | **major** — REQUIRES EXPLICIT APPROVAL | never auto-suggest |
      
      ## Decision logic (apply in order)
      
      ```
      1. Inspect commits since last tag:
         git log $(git describe --tags --abbrev=0)..HEAD --oneline
      
      2. Categorise by Conventional Commits prefix:
         feat:     → feature signal
         fix:      → fix signal
         chore: docs: style: perf: test: refactor: → housekeeping signal
         BREAKING CHANGE: in body, or !: in subject → breaking signal
      
      3. Decide bump:
         IF any breaking signal:
           STOP. Surface to user with the breaking commits listed.
           Ask explicitly: "These changes look breaking. Bump to v<next-major>.0.0,
           or treat as v<current-major>.<next-minor>.0 with breaking-change notes?"
           NEVER auto-major.
      
         ELSE IF any feature signal:
           bump = minor
           New version = bump <current>.<minor + 1>.0
      
         ELSE (only housekeeping/fix signals):
           bump = patch
           New version = <current>.<minor>.<patch + 1>
      ```
      
      ## README touch policy by bump
      
      | Bump | README "Recent Updates" | README body sections |
      |---|---|---|
      | patch | **always** update (single-bullet block) | skip unless explicitly asked |
      | minor | **always** update (multi-bullet block) | scan diff for new commands/config/install steps; touch only if found |
      | major | **always** update (lead with breaking change) | always update (and major needs approval anyway) |
      
      The "Recent Updates" section is the one README touch that always happens. Body changes for minor/major are conditional — checked against the diff, not assumed.
      
      ## Rationale
      
      User-stated preferences for 0xDarkMatter repos (codified 2026-04-26):
      - Most work is feature-shaped, so minor is the default — predictable cadence
      - Patches reserved for genuine fixes — preserves signal of what a patch means
      - Pre-1.0 stays pre-1.0 until explicitly promoted — no accidental "this is stable" signal
      - Treat `BREAKING CHANGE:` markers as a signal to ask, not as authorization to bump major
      
      ## Mapping to standard semver-from-commits
      
      Aligns with the conventional-commits semver mapping with one explicit override: major bump is gated behind user approval even when breaking-change markers are present in commits. Everything else matches the standard mapping.
      
      ## Edge cases
      
      - **Empty range** (no commits since last tag): refuse to release; nothing to ship.
      - **Mixed feat + fix**: minor (feat dominates).
      - **Only chore/docs**: patch (treat as housekeeping release).
      - **First release** (no prior tag): default to v0.1.0, ask for confirmation.
      - **Tag exists for current HEAD already**: refuse (already released this commit).
      
    • repo-visibility.md 1.9 KB
      # Repo Visibility Default
      
      When publishing a new repo to GitHub, **default to private**. Public is opt-in only.
      
      ## Why
      
      User-stated preference (codified 2026-04-26): wants control over what's published openly. Private-by-default prevents accidental public exposure of work-in-progress, unfinished projects, or material that needs review before going public.
      
      ## Application rules
      
      - `gh repo create` → always pass `--private` unless the user has **explicitly** said "public" / "make it public" / "publish openly" for *this specific repo*.
      - Existing private → public flips also require explicit approval. Use:
        ```bash
        gh repo edit <org>/<repo> --visibility public --accept-visibility-change-consequences
        ```
      - "Push to GitHub" / "publish this" / "ship it" alone = **private**.
      - Even if a repo is going to the 0xDarkMatter org and other repos there are public, do not infer this one should be public.
      - When proposing the publish plan, surface the visibility decision as a **flippable line** the user can read and react to:
      
        > Creating as **private** at github.com/0xDarkMatter/<repo> — say 'public' to flip.
      
        Not buried in a flag soup.
      
      ## Per-repo override
      
      If a user says "make this one public" for a specific repo, treat that as authorization for that single repo. It does not change the default for future repos. Always ask again on the next new repo.
      
      ## What private mode loses
      
      For visibility, list these in the publish plan so the user can make an informed call:
      
      - No public README rendering on github.com (still works for the repo owner)
      - No public clone/star/fork
      - GitHub Actions still works but minutes count against private quota
      - Releases are private
      - Issues/PRs are private
      
      If any of these matter for the project's purpose (e.g. a skill plugin that needs public install URLs, a portfolio piece), the user will likely flip to public — but that's their call to surface.
      
  • scripts
    • check-issues.sh 6 KB
      #!/usr/bin/env bash
      # Surface open GitHub issues you may not have seen — externally-authored and stale.
      #
      # Read-only (gh issue list). Built to flag the blind spot: issues filed by someone
      # other than the repo owner, and issues left untouched for a while. Designed to run
      # advisory at push-time without ever gating the push.
      #
      # Usage:   check-issues.sh [--repo OWNER/REPO | --remote NAME] [--stale-days N]
      #                          [--limit N] [--advisory] [--json] [-h|--help]
      # Input:   argv only. Default repo = derived from the 'origin' remote of the cwd.
      # Output:  stdout = data (human summary, or --json envelope). Framing on stderr.
      # Stderr:  headers, the advisory banner, skip notices, errors.
      # Exit:    0 nothing you're missing (no open issues, or all are yours and fresh)
      #          2 usage
      #          5 gh not installed (standalone mode; --advisory downgrades this to a skip)
      #          7 unavailable — not a GitHub remote, gh not authed, offline, rate-limited,
      #            or the lookup timed out (ADVISORY signal; never a real failure)
      #          10 open external and/or stale issues present (the thing to look at)
      #
      # Examples:
      #   check-issues.sh                                  # origin of the cwd
      #   check-issues.sh --repo 0xDarkMatter/flarecrawl
      #   check-issues.sh --remote origin --stale-days 14
      #   check-issues.sh --json | jq '.data[] | select(.external)'
      #   check-issues.sh --advisory --remote origin       # compact, silent when clean
      set -uo pipefail
      
      EX_OK=0; EX_USAGE=2; EX_MISSING_DEP=5; EX_UNAVAILABLE=7; EX_FINDINGS=10
      GH_TIMEOUT="${GH_TIMEOUT:-15}"   # seconds; bounds the network call
      
      # Terminal design system (skills/_lib/term.sh). Framing prints to stderr, so detect
      # color on fd 2. Degrade to plain output if the shared lib isn't reachable.
      __lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" 2>/dev/null && pwd || true)"
      if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
      else
        term_panel_open()  { printf '== %s %s ==\n' "${2:-}" "${3:-}"; }
        term_panel_close() { [ -n "${1:-}" ] && printf '%s\n' "$1"; }
        term_panel_vert()  { :; }
        term_panel_line()  { printf '  %s\n' "$*"; }
        term_color()       { shift; printf '%s' "$*"; }
        term_mark()        { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
        term_health()      { shift; printf '%s' "$*"; }
        TERM_ARROW="->"
      fi
      
      REPO=""; REMOTE="origin"; STALE_DAYS=30; LIMIT=50; ADVISORY=0; JSON=0
      while [ $# -gt 0 ]; do
        case "$1" in
          --repo)       REPO="${2:?--repo needs OWNER/REPO}"; shift 2 ;;
          --remote)     REMOTE="${2:?--remote needs a name}"; shift 2 ;;
          --stale-days) STALE_DAYS="${2:?--stale-days needs N}"; shift 2 ;;
          --limit)      LIMIT="${2:?--limit needs N}"; shift 2 ;;
          --advisory)   ADVISORY=1; shift ;;
          --json)       JSON=1; shift ;;
          -h|--help)    sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit "$EX_OK" ;;
          *) echo "check-issues: unknown argument: $1" >&2; exit "$EX_USAGE" ;;
        esac
      done
      
      # In advisory mode, ANY inability to check is a silent skip (never disturb a push).
      skip() { # message
        [ "$ADVISORY" -eq 1 ] || echo "check-issues: $1" >&2
        exit "$EX_UNAVAILABLE"
      }
      
      command -v gh >/dev/null 2>&1 || {
        [ "$ADVISORY" -eq 1 ] && exit "$EX_UNAVAILABLE"
        echo "check-issues: gh not installed (https://cli.github.com)" >&2
        exit "$EX_MISSING_DEP"
      }
      
      # Resolve OWNER/REPO from the remote if not given explicitly.
      if [ -z "$REPO" ]; then
        url="$(git remote get-url "$REMOTE" 2>/dev/null)" || skip "no '$REMOTE' remote here"
        case "$url" in
          *github.com[:/]*)
            # strip everything up to github.com<sep>, then a trailing .git and/or slash
            REPO="$(printf '%s' "$url" | sed -E 's#^.*github\.com[:/]+##; s#\.git$##; s#/$##')" ;;
          *) skip "remote '$REMOTE' is not a github.com repo" ;;
        esac
      fi
      OWNER="${REPO%%/*}"
      
      # Bounded, read-only lookup. Any failure (auth/offline/rate-limit/timeout) -> skip/7.
      runner() { if command -v timeout >/dev/null 2>&1; then timeout "$GH_TIMEOUT" "$@"; else "$@"; fi; }
      raw="$(runner gh issue list --repo "$REPO" --state open --limit "$LIMIT" \
              --json number,title,author,createdAt,updatedAt,labels 2>/dev/null)" \
        || skip "gh issue list failed for $REPO (not authed / offline / rate-limited?)"
      [ -n "$raw" ] || skip "empty response from gh for $REPO"
      
      # Classify with jq: external = author.login != owner; stale = updatedAt older than N days.
      command -v jq >/dev/null 2>&1 || skip "jq not installed"
      analysis="$(printf '%s' "$raw" | jq -c --arg owner "$OWNER" --argjson stale "$STALE_DAYS" '
        (now - ($stale * 86400)) as $cutoff
        | map(. + {
            external: (.author.login != $owner),
            stale: ((.updatedAt | sub("\\.[0-9]+";"") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) < $cutoff)
          })
        | { total: length,
            flagged: map(select(.external or .stale)),
          }' 2>/dev/null)" || skip "could not parse gh output"
      
      total="$(printf '%s' "$analysis" | jq -r '.total')"
      flagged_n="$(printf '%s' "$analysis" | jq -r '.flagged | length')"
      
      if [ "$JSON" -eq 1 ]; then
        printf '%s' "$analysis" | jq -c --arg repo "$REPO" \
          '{data: .flagged, meta: {repo: $repo, total_open: .total, flagged: (.flagged|length), schema: "claude-mods.github-ops.check-issues/v1"}}'
      fi
      
      # Human / advisory output (stderr framing; the data above is the stdout product).
      if [ "$flagged_n" -eq 0 ]; then
        [ "$ADVISORY" -eq 1 ] || echo "check-issues: $REPO — $total open, none external or stale." >&2
        exit "$EX_OK"
      fi
      
      {
        term_panel_open github-ops "OPEN ISSUES" "$REPO  $flagged_n of $total flagged"
        term_panel_vert
        while IFS= read -r ln; do term_panel_line "$ln"; done < <(printf '%s' "$analysis" | jq -r --arg m "$(term_mark warn)" '.flagged[]
          | "\($m) #\(.number)  [\(if .external then "external" else "yours" end)\(if .stale then ",stale" else "" end)]  by \(.author.login)  \(.title)"')
        term_panel_vert
        term_panel_close \
          "$(term_color dim "${TERM_ARROW} gh issue view <n>    read-only, never blocks a push")" \
          "$(term_health warning "$flagged_n flagged")"
      } >&2
      
      exit "$EX_FINDINGS"
      
    • check-security-posture.sh 20.6 KB
      #!/usr/bin/env bash
      # Audit a GitHub repo's security posture — what's off, what's actually exposed.
      #
      # READ-ONLY. Only GET/HEAD `gh api` calls. The "enable" commands it prints are
      # emitted as TEXT for you to review and run yourself — this script NEVER applies
      # a change. It surfaces the blind spot: security features left off, and (where a
      # scanner is on) the OPEN findings that prove real exposure. Severity is
      # visibility-aware — a public repo gets free secret/push/code scanning, so a gap
      # there is a real finding; a private repo without Advanced Security gets those as
      # a NOTE ("needs GHAS"), not a nag.
      #
      # Usage:   check-security-posture.sh [--repo OWNER/REPO | --remote NAME | --org OWNER]
      #                                    [--commands] [--json] [--strict] [--advisory]
      #                                    [-h|--help]
      # Input:   argv only. Default repo = derived from the 'origin' remote of the cwd.
      # Output:  stdout = data (human checklist, --commands enable list, or --json envelope).
      #          --json schema: claude-mods.github-ops.security-posture/v1
      # Stderr:  headers, the review banner, skip notices, errors.
      # Exit:    0  posture clean (all applicable features on, no open alerts)
      #          2  usage (bad/unknown flag, malformed OWNER/REPO)
      #          5  gh not installed (standalone; --advisory downgrades to a skip)
      #          7  unavailable — non-github remote, gh unauthed/offline, timeout
      #             (ADVISORY signal; never a real failure)
      #          10 gaps and/or open alerts found (the thing to look at)
      #
      # Severity model (visibility-aware; documented so the mapping is auditable):
      #   critical : open CRITICAL alerts present on an enabled scanner
      #   high     : open HIGH alerts; OR (public/active) push-protection off;
      #              OR (public/active) Dependabot alerts off
      #   medium   : (public) secret-scanning or code-scanning off; Dependabot
      #              security-updates off; no branch protection on the default branch
      #   low      : SECURITY.md absent; private vulnerability reporting off
      #   note     : feature needs paid GitHub Advanced Security on a private repo —
      #              reported, but NOT counted as a gap (n/a unless GHAS is on)
      # By default low+medium gaps DO count toward exit 10 (they are real, free gaps).
      # --strict additionally makes any non-clean state exit 10 even in --advisory.
      # Free-on-any-repo features (Dependabot alerts, Dependabot security updates,
      # private vuln reporting, SECURITY.md) are always findings when off.
      #
      # Examples:
      #   check-security-posture.sh --repo 0xDarkMatter/flarecrawl
      #   check-security-posture.sh --remote origin
      #   check-security-posture.sh --org 0xDarkMatter            # fleet sweep
      #   check-security-posture.sh --repo OWNER/REPO --commands  # copy-paste enable cmds
      #   check-security-posture.sh --repo OWNER/REPO --json | jq '.data[] | select(.state=="off")'
      set -uo pipefail
      
      EX_OK=0; EX_USAGE=2; EX_MISSING_DEP=5; EX_UNAVAILABLE=7; EX_FINDINGS=10
      GH_TIMEOUT="${GH_TIMEOUT:-20}"   # seconds; bounds every network call
      
      # Terminal design system (skills/_lib/term.sh). Framing prints to stderr, so detect
      # color on fd 2. Degrade to plain output if the shared lib isn't reachable.
      __lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" 2>/dev/null && pwd || true)"
      if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
      else
        term_panel_open()  { printf '== %s %s ==\n' "${2:-}" "${3:-}"; }
        term_panel_close() { [ -n "${1:-}" ] && printf '%s\n' "$1"; }
        term_panel_vert()  { :; }
        term_panel_line()  { printf '  %s\n' "$*"; }
        term_section()     { printf '%s (%s)\n' "${2:-}" "${3:-}"; }
        term_color()       { shift; printf '%s' "$*"; }
        term_mark()        { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
        term_health()      { shift; printf '%s' "$*"; }
      fi
      
      REPO=""; REMOTE="origin"; ORG=""; COMMANDS=0; JSON=0; STRICT=0; ADVISORY=0
      while [ $# -gt 0 ]; do
        case "$1" in
          --repo)     REPO="${2:?--repo needs OWNER/REPO}"; shift 2 ;;
          --remote)   REMOTE="${2:?--remote needs a name}"; shift 2 ;;
          --org)      ORG="${2:?--org needs an OWNER}"; shift 2 ;;
          --commands) COMMANDS=1; shift ;;
          --json)     JSON=1; shift ;;
          --strict)   STRICT=1; shift ;;
          --advisory) ADVISORY=1; shift ;;
          -h|--help)  sed -n '2,46p' "$0" | sed 's/^# \{0,1\}//'; exit "$EX_OK" ;;
          *) echo "check-security-posture: unknown argument: $1" >&2; exit "$EX_USAGE" ;;
        esac
      done
      
      # In advisory mode, any inability to check is a silent skip.
      skip() { # message
        [ "$ADVISORY" -eq 1 ] || echo "check-security-posture: $1" >&2
        exit "$EX_UNAVAILABLE"
      }
      
      command -v gh >/dev/null 2>&1 || {
        [ "$ADVISORY" -eq 1 ] && exit "$EX_UNAVAILABLE"
        echo "check-security-posture: gh not installed (https://cli.github.com)" >&2
        exit "$EX_MISSING_DEP"
      }
      command -v jq >/dev/null 2>&1 || skip "jq not installed"
      
      runner() { if command -v timeout >/dev/null 2>&1; then timeout "$GH_TIMEOUT" "$@"; else "$@"; fi; }
      
      # Validate OWNER/REPO shape (agent safety — never interpolate a fabricated path).
      valid_repo() { printf '%s' "$1" | grep -Eq '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$'; }
      valid_owner() { printf '%s' "$1" | grep -Eq '^[A-Za-z0-9._-]+$'; }
      
      # --------------------------------------------------------------------------
      # Per-repo audit. Emits one JSON object {repo, visibility, ghas, features:[...]}
      # to stdout via `printf`. Returns 0 clean / 10 findings / 7 unavailable.
      # Never crashes on a read error: unknown reads become state "unknown".
      # --------------------------------------------------------------------------
      audit_repo() { # OWNER/REPO  -> echoes a JSON object, returns 0|10|7
        local R="$1" core owner vis priv ghas ss ssp default_branch
        owner="${R%%/*}"
      
        core="$(runner gh api "repos/$R" 2>/dev/null)" || return 7
        [ -n "$core" ] || return 7
      
        vis="$(printf '%s' "$core" | jq -r '.visibility // (if .private then "private" else "public" end)')"
        priv="$(printf '%s' "$core" | jq -r '.private')"
        ghas="$(printf '%s' "$core" | jq -r '.security_and_analysis.advanced_security.status // "null"')"
        ss="$(printf '%s'  "$core" | jq -r '.security_and_analysis.secret_scanning.status // "null"')"
        ssp="$(printf '%s' "$core" | jq -r '.security_and_analysis.secret_scanning_push_protection.status // "null"')"
        default_branch="$(printf '%s' "$core" | jq -r '.default_branch // "main"')"
      
        local is_public=0; [ "$vis" = "public" ] && is_public=1
        local has_ghas=0; [ "$ghas" = "enabled" ] && has_ghas=1
        # Secret/push/code scanning are "applicable" (a gap if off) when free: public repo,
        # OR private repo with GHAS enabled. Otherwise they are a NOTE ("needs GHAS").
        local scan_applicable=0
        if [ "$is_public" -eq 1 ] || [ "$has_ghas" -eq 1 ]; then scan_applicable=1; fi
      
        # Each feature row appended to this jq array as a compact object.
        local features="[]"
        add() { # feature state applicable severity enable_command [open_alerts] [max_severity]
          features="$(jq -c \
            --arg f "$1" --arg st "$2" --argjson ap "$3" --arg sev "$4" --arg cmd "$5" \
            --arg oa "${6-}" --arg mx "${7-}" \
            '. + [ ($oa|if .=="" then {} else {open_alerts: (.|tonumber)} end)
                   + ($mx|if .=="" then {} else {max_severity: .} end)
                   + {feature:$f, state:$st, applicable:$ap, severity:$sev, enable_command:$cmd} ]' \
            <<<"$features")"
        }
      
        # ---- 1. Dependabot alerts (free on any repo) ----
        local da_state da_cmd="gh api -X PUT repos/$R/vulnerability-alerts"
        if runner gh api "repos/$R/vulnerability-alerts" --silent >/dev/null 2>&1; then
          da_state="on"
        else
          # 404 = disabled (the normal case). A timeout/auth failure also lands here; we
          # can't distinguish without the body, so treat as "off" but it'll be re-checked
          # below only if on. Conservative: report off (never a false "on").
          da_state="off"
        fi
        if [ "$da_state" = "on" ]; then
          # Enabled -> fetch OPEN alerts for real exposure. 403/404 -> n/a couldn't read.
          local da_json da_n da_max=""
          da_json="$(runner gh api "repos/$R/dependabot/alerts?state=open&per_page=100" 2>/dev/null)"
          if [ -n "$da_json" ] && printf '%s' "$da_json" | jq -e 'type=="array"' >/dev/null 2>&1; then
            da_n="$(printf '%s' "$da_json" | jq 'length')"
            da_max="$(printf '%s' "$da_json" | jq -r '
              ([.[].security_advisory.severity] | map(ascii_downcase)) as $s
              | (["critical","high","medium","low"] | map(select(. as $t | $s | index($t))) | .[0]) // ""')"
            add "dependabot_alerts" "on" true "none" "$da_cmd" "$da_n" "$da_max"
          else
            add "dependabot_alerts" "on" true "none" "$da_cmd" "" "unknown"
          fi
        else
          add "dependabot_alerts" "off" true "$( [ "$is_public" -eq 1 ] && echo high || echo high )" "$da_cmd"
        fi
      
        # ---- 2. Dependabot security updates (free on any repo) ----
        local asf asf_cmd="gh api -X PUT repos/$R/automated-security-fixes"
        asf="$(runner gh api "repos/$R/automated-security-fixes" --jq '.enabled' 2>/dev/null | tr -d '\r')"
        case "$asf" in
          true)  add "dependabot_security_updates" "on"  true "none" "$asf_cmd" ;;
          false) add "dependabot_security_updates" "off" true "medium" "$asf_cmd" ;;
          *)     add "dependabot_security_updates" "unknown" true "low" "$asf_cmd" ;;
        esac
      
        # ---- 3. Secret scanning (free on public; GHAS on private) ----
        local ss_cmd='gh api -X PATCH repos/'"$R"' --input - <<<'"'"'{"security_and_analysis":{"secret_scanning":{"status":"enabled"}}}'"'"
        if [ "$scan_applicable" -eq 1 ]; then
          if [ "$ss" = "enabled" ]; then
            # On -> count open secret-scanning alerts. 403/404 -> couldn't read.
            local sj sn
            sj="$(runner gh api "repos/$R/secret-scanning/alerts?state=open&per_page=100" 2>/dev/null)"
            if [ -n "$sj" ] && printf '%s' "$sj" | jq -e 'type=="array"' >/dev/null 2>&1; then
              sn="$(printf '%s' "$sj" | jq 'length')"
              # Any exposed secret is critical.
              local sev=none; [ "$sn" -gt 0 ] && sev=critical
              add "secret_scanning" "on" true "$sev" "$ss_cmd" "$sn"
            else
              add "secret_scanning" "on" true "none" "$ss_cmd" "" "unknown"
            fi
          else
            add "secret_scanning" "off" true "medium" "$ss_cmd"
          fi
        else
          add "secret_scanning" "n/a" false "note" "$ss_cmd"
        fi
      
        # ---- 4. Push protection (free on public; GHAS on private). Needs secret scanning first. ----
        local pp_cmd='gh api -X PATCH repos/'"$R"' --input - <<<'"'"'{"security_and_analysis":{"secret_scanning":{"status":"enabled"},"secret_scanning_push_protection":{"status":"enabled"}}}'"'"
        if [ "$scan_applicable" -eq 1 ]; then
          if [ "$ssp" = "enabled" ]; then
            add "secret_scanning_push_protection" "on" true "none" "$pp_cmd"
          else
            add "secret_scanning_push_protection" "off" true "high" "$pp_cmd"
          fi
        else
          add "secret_scanning_push_protection" "n/a" false "note" "$pp_cmd"
        fi
      
        # ---- 5. Code scanning default setup (free on public; GHAS on private) ----
        local cs_state cs_cmd="gh api -X PUT repos/$R/code-scanning/default-setup -f state=configured"
        cs_state="$(runner gh api "repos/$R/code-scanning/default-setup" --jq '.state' 2>/dev/null | tr -d '\r')"
        if [ "$scan_applicable" -eq 1 ]; then
          if [ "$cs_state" = "configured" ]; then
            local cj cn cmax=""
            cj="$(runner gh api "repos/$R/code-scanning/alerts?state=open&per_page=100" 2>/dev/null)"
            if [ -n "$cj" ] && printf '%s' "$cj" | jq -e 'type=="array"' >/dev/null 2>&1; then
              cn="$(printf '%s' "$cj" | jq 'length')"
              cmax="$(printf '%s' "$cj" | jq -r '
                ([.[].rule.security_severity_level // .[].rule.severity // empty] | map(ascii_downcase)) as $s
                | (["critical","high","medium","low"] | map(select(. as $t | $s | index($t))) | .[0]) // ""')"
              add "code_scanning" "on" true "none" "$cs_cmd" "$cn" "$cmax"
            else
              add "code_scanning" "on" true "none" "$cs_cmd" "" "unknown"
            fi
          elif [ -n "$cs_state" ] && [ "$cs_state" != "null" ]; then
            add "code_scanning" "off" true "medium" "$cs_cmd"   # not-configured
          else
            add "code_scanning" "unknown" true "low" "$cs_cmd"  # couldn't read
          fi
        else
          add "code_scanning" "n/a" false "note" "$cs_cmd"
        fi
      
        # ---- 6. Private vulnerability reporting (free on any repo) ----
        local pvr pvr_cmd="gh api -X PUT repos/$R/private-vulnerability-reporting"
        pvr="$(runner gh api "repos/$R/private-vulnerability-reporting" --jq '.enabled' 2>/dev/null | tr -d '\r')"
        case "$pvr" in
          true)  add "private_vulnerability_reporting" "on"  true "none" "$pvr_cmd" ;;
          false) add "private_vulnerability_reporting" "off" true "low"  "$pvr_cmd" ;;
          *)     add "private_vulnerability_reporting" "unknown" true "low" "$pvr_cmd" ;;
        esac
      
        # ---- 7. SECURITY.md present (root, .github/, docs/) ----
        local sec_found=0 loc
        for loc in "SECURITY.md" ".github/SECURITY.md" "docs/SECURITY.md"; do
          if runner gh api "repos/$R/contents/$loc" --silent >/dev/null 2>&1; then sec_found=1; break; fi
        done
        local sec_cmd="cp assets/SECURITY.md.template SECURITY.md  # edit, commit, push"
        if [ "$sec_found" -eq 1 ]; then
          add "security_policy" "on" true "none" "$sec_cmd"
        else
          add "security_policy" "off" true "low" "$sec_cmd"
        fi
      
        # ---- 8. Branch protection on the default branch (bonus) ----
        local bp_cmd="# branch protection: see github.com/$R/settings/branches (requires a ruleset/protection JSON)"
        if runner gh api "repos/$R/branches/$default_branch/protection" --silent >/dev/null 2>&1; then
          add "branch_protection" "on" true "none" "$bp_cmd"
        else
          # 404 not-protected / 403 no-access -> treat as off (free to set on any repo).
          add "branch_protection" "off" true "medium" "$bp_cmd"
        fi
      
        # Assemble the repo object and decide the per-repo exit.
        local obj
        obj="$(jq -c -n --arg repo "$R" --arg vis "$vis" --argjson priv "${priv:-false}" \
          --arg ghas "$ghas" --argjson feat "$features" \
          '{repo:$repo, visibility:$vis, private:$priv,
            ghas:(if $ghas=="null" then null else $ghas end), features:$feat}')"
        printf '%s' "$obj"
      
        # Findings = any applicable feature that is off/unknown, OR any open_alerts>0.
        local gaps
        gaps="$(printf '%s' "$obj" | jq '
          [ .features[]
            | select(.applicable == true)
            | select( (.state=="off") or (.state=="unknown") or ((.open_alerts // 0) > 0) )
          ] | length')"
        [ "$gaps" -gt 0 ] && return 10
        return 0
      }
      
      # Severity glyph helper for human output.
      sev_tag() { case "$1" in
        critical) printf '[critical]';; high) printf '[high]';;
        medium) printf '[medium]';; low) printf '[low]';;
        note) printf '';; *) printf '';; esac; }
      
      # Human checklist for one repo object (reads JSON on stdin-arg $1).
      print_human() { # repo_json
        local o="$1" repo vis
        repo="$(printf '%s' "$o" | jq -r '.repo')"
        vis="$(printf '%s' "$o" | jq -r '.visibility')"
        local hgaps health
        hgaps="$(printf '%s' "$o" | jq '[.features[]|select(.applicable==true and ((.state=="off") or (.state=="unknown") or ((.open_alerts//0)>0)))]|length')"
        if [ "$hgaps" -gt 0 ]; then health="$(term_health warning "$hgaps gap(s)/alert(s)")"; else health="$(term_health healthy clean)"; fi
        {
          term_panel_open github-ops "SECURITY POSTURE" "$repo  $vis"
          term_panel_vert
          while IFS= read -r ln; do term_panel_line "$ln"; done < <(printf '%s' "$o" | jq -r \
            --arg ok "$(term_mark ok)" --arg bad "$(term_mark bad)" \
            --arg na "$(term_mark na)" --arg unk "$(term_mark unknown)" '
            .features[] |
            if .state=="on" then
              "\($ok) \(.feature)" +
                (if (.open_alerts // 0) > 0 then "  — \(.open_alerts) OPEN alert(s)" + (if .max_severity then ", max \(.max_severity)" else "" end) else "" end) +
                (if .max_severity=="unknown" then "  (alerts: couldn’t read — needs security_events scope)" else "" end)
            elif .state=="n/a" then
              "\($na) \(.feature)  n/a (needs GitHub Advanced Security on a private repo)"
            elif .state=="unknown" then
              "\($unk) \(.feature)  n/a (couldn’t read)"
            else
              "\($bad) \(.feature)  [\(.severity)]"
            end')
          # Enable commands for gaps.
          local has_gap
          has_gap="$(printf '%s' "$o" | jq '[.features[]|select(.applicable==true and (.state=="off"))]|length')"
          if [ "$has_gap" -gt 0 ]; then
            term_panel_vert
            term_section "" "enable commands" "$has_gap"
            while IFS= read -r ln; do term_panel_line "$(term_color dim "$ln")"; done < <(printf '%s' "$o" | jq -r '.features[]|select(.applicable==true and .state=="off")|.enable_command')
          fi
          term_panel_vert
          term_panel_close "$(term_color dim "review before running    this script never runs them")" "$health"
        } >&2
      }
      
      # Emit ONLY the enable commands (data on stdout; banner on stderr).
      print_commands() { # repo_json
        local o="$1"
        echo "# review before running — these change repo settings" >&2
        printf '%s' "$o" | jq -r '.features[]|select(.applicable==true and .state=="off")|.enable_command'
      }
      
      # ==========================================================================
      # Mode dispatch
      # ==========================================================================
      
      # Conflicting selectors.
      sel=0
      [ -n "$REPO" ] && sel=$((sel+1))
      [ -n "$ORG" ]  && sel=$((sel+1))
      if [ "$sel" -gt 1 ]; then
        echo "check-security-posture: --repo and --org are mutually exclusive" >&2; exit "$EX_USAGE"
      fi
      
      # ---- Fleet sweep ----
      if [ -n "$ORG" ]; then
        valid_owner "$ORG" || { echo "check-security-posture: invalid owner '$ORG'" >&2; exit "$EX_USAGE"; }
        list="$(runner gh repo list "$ORG" --no-archived --limit 200 --json nameWithOwner 2>/dev/null)" \
          || skip "gh repo list failed for $ORG (not authed / offline / rate-limited?)"
        [ -n "$list" ] || skip "no repos returned for $ORG"
        mapfile -t repos < <(printf '%s' "$list" | jq -r '.[].nameWithOwner' | tr -d '\r')
        [ "${#repos[@]}" -gt 0 ] || skip "no non-archived repos for $ORG"
      
        human=0; [ "$JSON" -eq 0 ] && [ "$COMMANDS" -eq 0 ] && human=1
        [ "$human" -eq 1 ] && { term_panel_open github-ops "SECURITY POSTURE" "$ORG  fleet sweep" >&2; term_panel_vert >&2; }
      
        all="[]"; any_findings=0; swept=0; unread=0
        for r in "${repos[@]}"; do
          valid_repo "$r" || continue
          obj="$(audit_repo "$r")"; rc=$?
          if [ "$rc" -eq 7 ] || [ -z "$obj" ]; then
            unread=$((unread+1))
            [ "$human" -eq 1 ] && term_panel_line "$(term_mark unknown) $r — couldn't read (skipped)" >&2
            continue
          fi
          swept=$((swept+1))
          [ "$rc" -eq 10 ] && any_findings=1
          all="$(jq -c --argjson o "$obj" '. + [$o]' <<<"$all")"
          if [ "$human" -eq 1 ]; then
            gaps="$(printf '%s' "$obj" | jq '[.features[]|select(.applicable==true and ((.state=="off") or (.state=="unknown") or ((.open_alerts//0)>0)))]|length')"
            vis="$(printf '%s' "$obj" | jq -r '.visibility')"
            if [ "$gaps" -eq 0 ]; then term_panel_line "$(term_mark ok) $r ($vis) — clean" >&2
            else term_panel_line "$(term_mark bad) $r ($vis) — $gaps gap(s)/alert(s)" >&2; fi
          fi
        done
      
        if [ "$JSON" -eq 1 ]; then
          jq -c -n --argjson data "$all" --arg org "$ORG" \
            --argjson swept "$swept" --argjson unread "$unread" --argjson find "$any_findings" \
            '{data:$data, meta:{org:$org, repos_audited:$swept, repos_unreadable:$unread, findings:($find==1), schema:"claude-mods.github-ops.security-posture/v1"}}'
        elif [ "$COMMANDS" -eq 1 ]; then
          echo "# review before running — these change repo settings" >&2
          printf '%s' "$all" | jq -r '.[] | "# \(.repo)", (.features[]|select(.applicable==true and .state=="off")|"  \(.enable_command)")'
        else
          local_health="$([ "$any_findings" -eq 1 ] && term_health warning "$swept swept  gaps found" || term_health healthy "$swept swept  all clean")"
          term_panel_vert >&2
          term_panel_close "$(term_color dim "$unread unreadable")" "$local_health" >&2
        fi
        [ "$any_findings" -eq 1 ] && exit "$EX_FINDINGS"
        exit "$EX_OK"
      fi
      
      # ---- Single repo ----
      if [ -z "$REPO" ]; then
        url="$(git remote get-url "$REMOTE" 2>/dev/null)" || skip "no '$REMOTE' remote here"
        case "$url" in
          *github.com[:/]*)
            REPO="$(printf '%s' "$url" | tr -d '\r' | sed -E 's#^.*github\.com[:/]+##; s#\.git$##; s#/$##')" ;;
          *) skip "remote '$REMOTE' is not a github.com repo" ;;
        esac
      fi
      valid_repo "$REPO" || { echo "check-security-posture: invalid OWNER/REPO '$REPO'" >&2; exit "$EX_USAGE"; }
      
      obj="$(audit_repo "$REPO")"; rc=$?
      if [ "$rc" -eq 7 ] || [ -z "$obj" ]; then skip "couldn't read $REPO (not authed / offline / not found / timeout)"; fi
      
      if [ "$JSON" -eq 1 ]; then
        printf '%s' "$obj" | jq -c \
          '{data: .features, meta: {repo:.repo, visibility:.visibility, private:.private, ghas:.ghas,
              gaps: ([.features[]|select(.applicable==true and ((.state=="off") or (.state=="unknown")))]|length),
              open_alerts: ([.features[].open_alerts // 0]|add),
              schema:"claude-mods.github-ops.security-posture/v1"}}'
      elif [ "$COMMANDS" -eq 1 ]; then
        print_commands "$obj"
      else
        print_human "$obj"
      fi
      
      [ "$rc" -eq 10 ] && exit "$EX_FINDINGS"
      exit "$EX_OK"
      
    • repo-scorecard.sh 26.9 KB
      #!/usr/bin/env bash
      # Scored, read-only repo-health scorecard — orchestrates the github-ops auditors.
      #
      # READ-ONLY. Only GET `gh api` calls plus calls to the read-only sibling scripts
      # (check-security-posture.sh, check-issues.sh). NEVER a -X PUT/PATCH/POST/DELETE.
      # It rolls five dimensions into one 0–100 score + letter grade per repo, and
      # (with --org) a fleet matrix + roll-up. The remediation pointers it prints are
      # TEXT for you to act on — this script applies nothing.
      #
      # Usage:   repo-scorecard.sh [--repo OWNER/REPO | --remote NAME | --org OWNER]
      #                            [--min-score N] [--json] [-h|--help]
      # Input:   argv only. Default repo = derived from the 'origin' remote of the cwd.
      # Output:  stdout = the data product (human matrix, or --json envelope).
      #          --json schema: claude-mods.github-ops.repo-scorecard/v1
      # Stderr:  headers, progress, the review banner, skip notices, errors.
      # Exit:    0  all audited repos healthy (no gaps; or all >= --min-score)
      #          2  usage (bad/unknown flag, malformed OWNER/REPO, mutex selectors)
      #          5  gh not installed
      #          7  unavailable — non-github remote, gh unauthed/offline, timeout
      #             (graceful, like the siblings; never a false "healthy")
      #          10 findings — gaps present, or a repo scored below --min-score
      #
      # SCORING MODEL (transparent rubric, documented so it is auditable):
      #   Each dimension yields a status (ok / warn / gap / n/a) and earns a fraction
      #   of its weight. n/a (couldn't read) earns ZERO and is never treated as ok.
      #
      #     Dimension   Weight   ok(full)        warn(half)              gap(zero)
      #     ─────────   ──────   ────────        ──────────              ─────────
      #     security      35     no gaps,        low/medium gaps only    high/critical gap
      #                          0 open alerts                           OR any open alert
      #     metadata      25     all 6 present   1–2 missing             3+ missing
      #     release       15     >=1 release &   releases exist but      no releases at all
      #                          latest tag      latest tag has no rel
      #                          has a release
      #     issues        15     none external   1–3 external/stale      4+ external/stale
      #                          or stale
      #     actions       10     latest run      no runs found (warn)    latest run = failure
      #                          succeeded
      #                                                    ─────
      #                                          total weight = 100
      #
      #   score = round( sum(weight_i * fraction_i) ), fraction in {1, 0.5, 0}.
      #   n/a dimensions earn 0 of their weight (honest: an unreadable security
      #   dimension can NEVER score full). Grade: A>=90 B>=75 C>=60 D>=40 F<40.
      #   Security is weighted highest by design; a single open critical alert or a
      #   high-severity gap zeroes 35 points and caps the grade hard.
      #
      #   --min-score N: exit 10 if ANY audited repo scores below N (CI-gating knob),
      #   independent of whether other gaps exist.
      #
      # Examples:
      #   repo-scorecard.sh --repo 0xDarkMatter/flarecrawl
      #   repo-scorecard.sh --org 0xDarkMatter
      #   repo-scorecard.sh --repo OWNER/REPO --json | jq '.data[0].top_fixes'
      #   repo-scorecard.sh --org OWNER --min-score 75   # CI gate: fail if any repo < 75
      set -uo pipefail
      
      EX_OK=0; EX_USAGE=2; EX_MISSING_DEP=5; EX_UNAVAILABLE=7; EX_FINDINGS=10
      GH_TIMEOUT="${GH_TIMEOUT:-20}"   # seconds; bounds every network call
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SEC="$HERE/check-security-posture.sh"
      ISS="$HERE/check-issues.sh"
      
      # Terminal design system (skills/_lib/term.sh). Framing prints to stderr, so detect
      # color on fd 2. Degrade to plain output if the shared lib isn't reachable.
      __lib="$(cd "$HERE/../../_lib" 2>/dev/null && pwd || true)"
      if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
      else
        term_panel_open()  { printf '== %s %s ==\n' "${2:-}" "${3:-}"; }
        term_panel_close() { [ -n "${1:-}" ] && printf '%s\n' "$1"; }
        term_panel_vert()  { :; }
        term_panel_line()  { printf '  %s\n' "$*"; }
        term_section()     { printf '%s (%s)\n' "${2:-}" "${3:-}"; }
        term_color()       { shift; printf '%s' "$*"; }
        term_mark()        { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
        term_health()      { shift; printf '%s' "$*"; }
        term_pip_bar()     { :; }
        TERM_ARROW="->"
      fi
      
      REPO=""; REMOTE="origin"; ORG=""; JSON=0; MIN_SCORE=""
      while [ $# -gt 0 ]; do
        case "$1" in
          --repo)      REPO="${2:?--repo needs OWNER/REPO}"; shift 2 ;;
          --remote)    REMOTE="${2:?--remote needs a name}"; shift 2 ;;
          --org)       ORG="${2:?--org needs an OWNER}"; shift 2 ;;
          --min-score) MIN_SCORE="${2:?--min-score needs N}"; shift 2 ;;
          --json)      JSON=1; shift ;;
          -h|--help)   sed -n '2,57p' "$0" | sed 's/^# \{0,1\}//'; exit "$EX_OK" ;;
          *) echo "repo-scorecard: unknown argument: $1" >&2; exit "$EX_USAGE" ;;
        esac
      done
      
      skip() { echo "repo-scorecard: $1" >&2; exit "$EX_UNAVAILABLE"; }
      
      command -v gh >/dev/null 2>&1 || {
        echo "repo-scorecard: gh not installed (https://cli.github.com)" >&2
        exit "$EX_MISSING_DEP"
      }
      command -v jq >/dev/null 2>&1 || skip "jq not installed"
      
      # --min-score must be an integer if given.
      if [ -n "$MIN_SCORE" ] && ! printf '%s' "$MIN_SCORE" | grep -Eq '^[0-9]+$'; then
        echo "repo-scorecard: --min-score needs an integer, got '$MIN_SCORE'" >&2; exit "$EX_USAGE"
      fi
      
      runner() { if command -v timeout >/dev/null 2>&1; then timeout "$GH_TIMEOUT" "$@"; else "$@"; fi; }
      
      # Agent safety — never interpolate a fabricated path into a gh call.
      valid_repo()  { printf '%s' "$1" | grep -Eq '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$'; }
      valid_owner() { printf '%s' "$1" | grep -Eq '^[A-Za-z0-9._-]+$'; }
      
      # Weights (sum = 100).
      W_SECURITY=35; W_METADATA=25; W_RELEASE=15; W_ISSUES=15; W_ACTIONS=10
      
      # --------------------------------------------------------------------------
      # gh api wrapper that distinguishes "exists" from "404" from "couldn't read".
      # Echoes body on success; sets a global GHRC: 0 ok, 4 not-found, 7 unavailable.
      # --------------------------------------------------------------------------
      gh_get() { # path  -> echoes body, sets GHRC
        local out
        out="$(runner gh api "$1" 2>/dev/null)"; local rc=$?
        if [ $rc -ne 0 ]; then
          # gh exits nonzero on 404 too; disambiguate via the error JSON if present.
          if printf '%s' "$out" | grep -q '"status": *"404"' 2>/dev/null; then GHRC=4; else GHRC=7; fi
          printf '%s' "$out"; return
        fi
        GHRC=0; printf '%s' "$out"
      }
      
      # Does a path exist in the repo? 0 yes, 1 no, 2 couldn't-read.
      content_exists() { # OWNER/REPO  PATH
        if runner gh api "repos/$1/contents/$2" --silent >/dev/null 2>&1; then return 0; fi
        # --silent suppresses the body; re-probe to classify 404 vs auth/offline.
        local body; body="$(runner gh api "repos/$1/contents/$2" 2>&1)"
        printf '%s' "$body" | grep -q '404' && return 1
        return 2
      }
      
      # --------------------------------------------------------------------------
      # Score ONE repo. Echoes a compact JSON object; returns 0 healthy / 10 findings
      # / 7 unavailable (couldn't read the core repo object at all).
      # --------------------------------------------------------------------------
      score_repo() { # OWNER/REPO -> echoes JSON object; returns 0|10|7
        local R="$1" owner core vis
        owner="${R%%/*}"
      
        core="$(runner gh api "repos/$R" 2>/dev/null)" || return 7
        [ -n "$core" ] || return 7
        vis="$(printf '%s' "$core" | jq -r '.visibility // (if .private then "private" else "public" end)' | tr -d '\r')"
        local default_branch
        default_branch="$(printf '%s' "$core" | jq -r '.default_branch // "main"' | tr -d '\r')"
      
        # ---- DIMENSION: metadata (6 facets) -----------------------------------
        local md_desc md_home md_topics md_lic md_readme md_changelog md_missing=0 md_detail=""
        md_desc="$(printf '%s' "$core" | jq -r '.description // "" | length' | tr -d '\r')"
        md_home="$(printf '%s' "$core" | jq -r '.homepage // "" | length' | tr -d '\r')"
        # topics live on the core object as .topics (array).
        md_topics="$(printf '%s' "$core" | jq -r '(.topics // []) | length' | tr -d '\r')"
      
        [ "${md_desc:-0}" -gt 0 ] || { md_missing=$((md_missing+1)); md_detail="$md_detail description;"; }
        # homepage is optional — count it only as a soft facet (missing homepage does
        # NOT increment md_missing; it is informational). We track it for detail only.
        if [ "${md_home:-0}" -gt 0 ]; then md_home="set"; else md_home="unset"; fi
        if [ "${md_topics:-0}" -ge 3 ]; then :; else md_missing=$((md_missing+1)); md_detail="$md_detail topics<3;"; fi
      
        if content_exists "$R" "LICENSE"; then md_lic=1
        elif content_exists "$R" "LICENSE.md"; then md_lic=1
        else md_lic=0; md_missing=$((md_missing+1)); md_detail="$md_detail LICENSE;"; fi
        if content_exists "$R" "README.md"; then md_readme=1
        else md_readme=0; md_missing=$((md_missing+1)); md_detail="$md_detail README;"; fi
        if content_exists "$R" "CHANGELOG.md"; then md_changelog=1
        else md_changelog=0; md_missing=$((md_missing+1)); md_detail="$md_detail CHANGELOG;"; fi
      
        # 5 hard facets (description, topics>=3, LICENSE, README, CHANGELOG).
        local md_status md_frac
        if [ "$md_missing" -eq 0 ]; then md_status="ok"; md_frac="1"
        elif [ "$md_missing" -le 2 ]; then md_status="warn"; md_frac="0.5"
        else md_status="gap"; md_frac="0"; fi
        [ -n "$md_detail" ] || md_detail="all present"
        md_detail="${md_detail# }"
      
        # ---- DIMENSION: release ----------------------------------------------
        local rel_status rel_frac rel_detail rel_count latest_tag rel_for_tag
        rel_count="$(runner gh api "repos/$R/releases?per_page=1" --jq 'length' 2>/dev/null | tr -d '\r')"
        latest_tag="$(runner gh api "repos/$R/tags?per_page=1" --jq '.[0].name // ""' 2>/dev/null | tr -d '\r')"
        if [ -z "${rel_count:-}" ]; then
          rel_status="n/a"; rel_frac="0"; rel_detail="couldn't read releases"
        elif [ "$rel_count" -eq 0 ]; then
          rel_status="gap"; rel_frac="0"; rel_detail="no GitHub releases"
        else
          # >=1 release exists. Is the latest TAG backed by a release?
          if [ -n "$latest_tag" ]; then
            if runner gh api "repos/$R/releases/tags/$latest_tag" --silent >/dev/null 2>&1; then
              rel_status="ok"; rel_frac="1"; rel_detail="latest tag $latest_tag has a release"
            else
              rel_status="warn"; rel_frac="0.5"; rel_detail="latest tag $latest_tag has no release"
            fi
          else
            rel_status="ok"; rel_frac="1"; rel_detail="releases present (no tags listed)"
          fi
        fi
      
        # ---- DIMENSION: security (orchestrate the sibling) --------------------
        local sec_status sec_frac sec_detail sec_json sec_rc
        sec_json="$("$SEC" --repo "$R" --json 2>/dev/null)"; sec_rc=$?
        if [ "$sec_rc" -eq 7 ] || [ -z "$sec_json" ] || ! printf '%s' "$sec_json" | jq -e . >/dev/null 2>&1; then
          sec_status="n/a"; sec_frac="0"; sec_detail="security audit unavailable"
          local sec_gaps=-1 sec_alerts=-1 sec_maxsev="unknown"
        else
          # The single-repo envelope: {data:[features], meta:{gaps, open_alerts,...}}.
          local sec_gaps sec_alerts sec_maxsev
          sec_gaps="$(printf '%s' "$sec_json" | jq -r '.meta.gaps // 0')"
          sec_alerts="$(printf '%s' "$sec_json" | jq -r '.meta.open_alerts // 0')"
          # Max severity across (a) gap rows and (b) any open alert.
          sec_maxsev="$(printf '%s' "$sec_json" | jq -r '
            ([ .data[]
               | select(.applicable==true)
               | (if ((.open_alerts // 0) > 0) then .max_severity else empty end),
                 (if (.state=="off" or .state=="unknown") then .severity else empty end) ]
             | map(select(. != null and . != "" and . != "none" and . != "note")
                   | ascii_downcase)) as $s
            | (["critical","high","medium","low"] | map(select(. as $t | $s | index($t))) | .[0]) // "none"')"
          if [ "$sec_gaps" -eq 0 ] && [ "$sec_alerts" -eq 0 ]; then
            sec_status="ok"; sec_frac="1"; sec_detail="no gaps, 0 open alerts"
          elif [ "$sec_maxsev" = "critical" ] || [ "$sec_maxsev" = "high" ] || [ "$sec_alerts" -gt 0 ]; then
            sec_status="gap"; sec_frac="0"
            sec_detail="$sec_gaps gap(s), $sec_alerts open alert(s), max $sec_maxsev"
          else
            sec_status="warn"; sec_frac="0.5"
            sec_detail="$sec_gaps gap(s) (max $sec_maxsev), 0 open alerts"
          fi
        fi
      
        # ---- DIMENSION: issues (orchestrate the sibling) ----------------------
        local iss_status iss_frac iss_detail iss_json iss_rc iss_flagged=-1 iss_total=-1
        iss_json="$("$ISS" --repo "$R" --json 2>/dev/null)"; iss_rc=$?
        if [ "$iss_rc" -eq 7 ] || [ -z "$iss_json" ] || ! printf '%s' "$iss_json" | jq -e . >/dev/null 2>&1; then
          iss_status="n/a"; iss_frac="0"; iss_detail="issue audit unavailable"
        else
          iss_flagged="$(printf '%s' "$iss_json" | jq -r '.meta.flagged // 0')"
          iss_total="$(printf '%s' "$iss_json" | jq -r '.meta.total_open // 0')"
          if [ "$iss_flagged" -eq 0 ]; then
            iss_status="ok"; iss_frac="1"; iss_detail="$iss_total open, none external/stale"
          elif [ "$iss_flagged" -le 3 ]; then
            iss_status="warn"; iss_frac="0.5"; iss_detail="$iss_flagged external/stale of $iss_total open"
          else
            iss_status="gap"; iss_frac="0"; iss_detail="$iss_flagged external/stale of $iss_total open"
          fi
        fi
      
        # ---- DIMENSION: actions (single signal) -------------------------------
        local act_status act_frac act_detail act_json act_concl
        act_json="$(runner gh api "repos/$R/actions/runs?branch=$default_branch&per_page=1" 2>/dev/null)"
        if [ -z "$act_json" ] || ! printf '%s' "$act_json" | jq -e '.workflow_runs' >/dev/null 2>&1; then
          act_status="n/a"; act_frac="0"; act_detail="couldn't read workflow runs"
        else
          act_concl="$(printf '%s' "$act_json" | jq -r '.workflow_runs[0].conclusion // "none"' | tr -d '\r')"
          case "$act_concl" in
            success)            act_status="ok";   act_frac="1";   act_detail="latest run on $default_branch: success" ;;
            none|null|"")       act_status="warn"; act_frac="0.5"; act_detail="no workflow runs on $default_branch" ;;
            failure|timed_out|startup_failure)
                                act_status="gap";  act_frac="0";   act_detail="latest run on $default_branch: $act_concl" ;;
            *)                  act_status="warn"; act_frac="0.5"; act_detail="latest run on $default_branch: $act_concl" ;;
          esac
        fi
      
        # ---- Roll up the score -------------------------------------------------
        local score
        score="$(awk -v ws=$W_SECURITY -v wm=$W_METADATA -v wr=$W_RELEASE -v wi=$W_ISSUES -v wa=$W_ACTIONS \
          -v fs="$sec_frac" -v fm="$md_frac" -v fr="$rel_frac" -v fi="$iss_frac" -v fa="$act_frac" \
          'BEGIN{ printf "%d", int(ws*fs + wm*fm + wr*fr + wi*fi + wa*fa + 0.5) }')"
        local grade
        if   [ "$score" -ge 90 ]; then grade="A"
        elif [ "$score" -ge 75 ]; then grade="B"
        elif [ "$score" -ge 60 ]; then grade="C"
        elif [ "$score" -ge 40 ]; then grade="D"
        else grade="F"; fi
      
        # ---- Top 3 fixes (highest-severity gaps first) -------------------------
        # Build a ranked list. Each entry: severity-rank \t status \t text.
        # rank 0 highest. Only surface dimensions that are gap/warn/n/a.
        local fixes="[]"
        addfix() { # rank status text
          fixes="$(jq -c --argjson r "$1" --arg st "$2" --arg t "$3" \
            '. + [{rank:$r, status:$st, fix:$t}]' <<<"$fixes")"
        }
        # security first (highest weight). Map maxsev to a rank.
        if [ "$sec_status" = "gap" ]; then
          addfix 0 gap "security: $sec_detail ${TERM_ARROW} check-security-posture.sh --repo $R --commands"
        elif [ "$sec_status" = "warn" ]; then
          addfix 3 warn "security: $sec_detail ${TERM_ARROW} check-security-posture.sh --repo $R --commands"
        elif [ "$sec_status" = "n/a" ]; then
          addfix 5 "n/a" "security: couldn't read ${TERM_ARROW} re-run check-security-posture.sh --repo $R"
        fi
        if [ "$md_status" = "gap" ]; then
          addfix 1 gap "metadata: missing ${md_detail} ${TERM_ARROW} set description / >=3 topics / add the missing file(s)"
        elif [ "$md_status" = "warn" ]; then
          addfix 4 warn "metadata: missing ${md_detail} ${TERM_ARROW} set description / >=3 topics / add the missing file(s)"
        fi
        if [ "$rel_status" = "gap" ]; then
          addfix 2 gap "release: $rel_detail ${TERM_ARROW} cut a GitHub release (github-ops mode update)"
        elif [ "$rel_status" = "warn" ]; then
          addfix 4 warn "release: $rel_detail ${TERM_ARROW} gh release create $latest_tag"
        fi
        if [ "$iss_status" = "gap" ]; then
          addfix 2 gap "issues: $iss_detail ${TERM_ARROW} check-issues.sh --repo $R"
        elif [ "$iss_status" = "warn" ]; then
          addfix 5 warn "issues: $iss_detail ${TERM_ARROW} check-issues.sh --repo $R"
        fi
        if [ "$act_status" = "gap" ]; then
          addfix 1 gap "actions: $act_detail ${TERM_ARROW} inspect the failing run (gh run list --repo $R)"
        elif [ "$act_status" = "warn" ]; then
          addfix 5 warn "actions: $act_detail"
        fi
        local top_fixes
        top_fixes="$(printf '%s' "$fixes" | jq -c 'sort_by(.rank) | [ .[] | .fix ] | .[0:3]')"
      
        # ---- Assemble the per-repo object -------------------------------------
        jq -c -n \
          --arg repo "$R" --arg vis "$vis" --argjson score "$score" --arg grade "$grade" \
          --arg md_st "$md_status" --arg md_d "$md_detail" \
          --arg rel_st "$rel_status" --arg rel_d "$rel_detail" \
          --arg sec_st "$sec_status" --arg sec_d "$sec_detail" \
          --argjson sec_gaps "${sec_gaps:-0}" --argjson sec_alerts "${sec_alerts:-0}" --arg sec_mx "${sec_maxsev:-none}" \
          --arg iss_st "$iss_status" --arg iss_d "$iss_detail" --argjson iss_fl "${iss_flagged:-0}" \
          --arg act_st "$act_status" --arg act_d "$act_detail" \
          --argjson topf "$top_fixes" \
          '{repo:$repo, visibility:$vis, score:$score, grade:$grade,
            dimensions:{
              metadata:{status:$md_st, detail:$md_d},
              release:{status:$rel_st, detail:$rel_d},
              security:{status:$sec_st, detail:$sec_d, gaps:(if $sec_gaps<0 then null else $sec_gaps end), open_alerts:(if $sec_alerts<0 then null else $sec_alerts end), max_severity:$sec_mx},
              issues:{status:$iss_st, detail:$iss_d, flagged:(if $iss_fl<0 then null else $iss_fl end)},
              actions:{status:$act_st, detail:$act_d}
            },
            top_fixes:$topf}'
      
        # Per-repo exit: findings if any dimension is gap, n/a, or warn? We count
        # gap/n/a as findings (real problems). warn does not by itself trip exit 10
        # unless --min-score applies. (n/a is a finding — never a clean pass.)
        if [ "$md_status" = "gap" ] || [ "$rel_status" = "gap" ] || [ "$sec_status" = "gap" ] || \
           [ "$iss_status" = "gap" ] || [ "$act_status" = "gap" ] || \
           [ "$md_status" = "n/a" ] || [ "$rel_status" = "n/a" ] || [ "$sec_status" = "n/a" ] || \
           [ "$iss_status" = "n/a" ] || [ "$act_status" = "n/a" ]; then
          return 10
        fi
        return 0
      }
      
      # Colored, ASCII-aware status glyph for a dimension (human card).
      mark() { case "$1" in
        ok) term_mark ok;; warn) term_mark warn;; gap) term_mark bad;; "n/a") term_mark na;; *) term_mark unknown;; esac; }
      
      # Human single-repo card (data to stdout; framing to stderr).
      print_card() { # repo_json
        local o="$1" repo vis score grade
        repo="$(jq -r '.repo' <<<"$o")"; vis="$(jq -r '.visibility' <<<"$o")"
        score="$(jq -r '.score' <<<"$o")"; grade="$(jq -r '.grade' <<<"$o")"
        local health
        case "$grade" in
          A|B) health="$(term_health healthy "grade $grade")" ;;
          C|D) health="$(term_health warning "grade $grade")" ;;
          *)   health="$(term_health critical "grade $grade")" ;;
        esac
        {
          term_panel_open github-ops "REPO SCORECARD" "$repo  $vis"
          term_panel_vert
          term_panel_line "SCORE  $(term_pip_bar score "$score" 100)  $score/100   GRADE $grade"
          term_panel_vert
          term_section "" "dimensions (weight)" 5
          local d name w
          for d in "security:w35" "metadata:w25" "release:w15" "issues:w15" "actions:w10"; do
            name="${d%%:*}"; w="${d##*:}"
            term_panel_line "$(printf '%-9s %s  %s' "$name" "$(mark "$(jq -r ".dimensions.$name.status" <<<"$o")")" "$(jq -r ".dimensions.$name.detail" <<<"$o")  $(term_color dim "($w)")")"
          done
          local nf; nf="$(jq -r '.top_fixes | length' <<<"$o")"
          if [ "$nf" -gt 0 ]; then
            term_panel_vert
            term_section "" "top fixes (highest-severity first)" "$nf"
            while IFS= read -r ln; do term_panel_line "$ln"; done < <(jq -r --arg b "$(term_mark warn)" '.top_fixes[] | "\($b) " + .' <<<"$o")
          fi
          term_panel_vert
          term_panel_close "$(term_color dim "weighted: security 35  metadata 25  release 15  issues 15  actions 10")" "$health"
        } >&2
      }
      
      # ==========================================================================
      # Mode dispatch
      # ==========================================================================
      
      # Mutually exclusive selectors.
      sel=0
      [ -n "$REPO" ] && sel=$((sel+1))
      [ -n "$ORG" ]  && sel=$((sel+1))
      if [ "$sel" -gt 1 ]; then
        echo "repo-scorecard: --repo and --org are mutually exclusive" >&2; exit "$EX_USAGE"
      fi
      
      # ---- Fleet sweep ----------------------------------------------------------
      if [ -n "$ORG" ]; then
        valid_owner "$ORG" || { echo "repo-scorecard: invalid owner '$ORG'" >&2; exit "$EX_USAGE"; }
        echo "$(term_color dim "repo-scorecard: sweeping $ORG ${TERM_ELLIPSIS}")" >&2
        list="$(runner gh repo list "$ORG" --no-archived --limit 200 --json nameWithOwner,visibility 2>/dev/null)" \
          || skip "gh repo list failed for $ORG (not authed / offline / rate-limited?)"
        [ -n "$list" ] || skip "no repos returned for $ORG"
        mapfile -t repos < <(printf '%s' "$list" | jq -r '.[].nameWithOwner' | tr -d '\r')
        [ "${#repos[@]}" -gt 0 ] || skip "no non-archived repos for $ORG"
      
        human=0; [ "$JSON" -eq 0 ] && human=1
        [ "$human" -eq 1 ] && { term_panel_open github-ops "REPO SCORECARD" "$ORG  fleet sweep" >&2; term_panel_vert >&2; }
      
        all="[]"; any_findings=0; swept=0; unread=0; below_min=0
        for r in "${repos[@]}"; do
          valid_repo "$r" || continue
          obj="$(score_repo "$r")"; rc=$?
          if [ "$rc" -eq 7 ] || [ -z "$obj" ] || ! printf '%s' "$obj" | jq -e . >/dev/null 2>&1; then
            unread=$((unread+1))
            [ "$human" -eq 1 ] && term_panel_line "$(term_mark unknown)  $r — couldn't read (skipped)" >&2
            continue
          fi
          swept=$((swept+1))
          [ "$rc" -eq 10 ] && any_findings=1
          all="$(jq -c --argjson o "$obj" '. + [$o]' <<<"$all")"
          sc="$(jq -r '.score' <<<"$obj")"
          if [ -n "$MIN_SCORE" ] && [ "$sc" -lt "$MIN_SCORE" ]; then below_min=$((below_min+1)); fi
          if [ "$human" -eq 1 ]; then
            # matrix row: per-dimension colored marks + score + grade.
            m() { case "$(jq -r ".dimensions.$1.status" <<<"$obj")" in
              ok) term_mark ok;; warn) term_mark warn;; gap) term_mark bad;; "n/a") term_mark na;; *) printf ' ';; esac; }
            term_panel_line "$(printf '%-34s S:%s M:%s R:%s I:%s A:%s  %3s %s' \
              "$r" "$(m security)" "$(m metadata)" "$(m release)" "$(m issues)" "$(m actions)" \
              "$sc" "$(jq -r '.grade' <<<"$obj")")" >&2
          fi
        done
      
        # Roll-up stats. The repo array can be large (a big org), so pipe it via STDIN
        # rather than --argjson on argv — argv has a length cap and a fleet sweep blows
        # past it (observed: jq "Argument list too long" at ~70 repos on MSYS).
        rollup="$(printf '%s' "$all" | jq -c --arg org "$ORG" \
          --argjson swept "$swept" --argjson unread "$unread" \
          '. as $data
           | ($data | map(.score)) as $scores
           | ($scores | length) as $n
           | { org:$org, repos_scored:$swept, repos_unreadable:$unread,
               avg_score: (if $n>0 then (($scores|add)/$n|floor) else null end),
               median_score: (if $n>0 then ($scores|sort|.[($n/2|floor)]) else null end),
               total_open_alerts: ([ $data[].dimensions.security.open_alerts // 0 ] | add),
               failing_by_dimension: {
                 security: ([ $data[]|select(.dimensions.security.status=="gap") ]|length),
                 metadata: ([ $data[]|select(.dimensions.metadata.status=="gap") ]|length),
                 release:  ([ $data[]|select(.dimensions.release.status=="gap") ]|length),
                 issues:   ([ $data[]|select(.dimensions.issues.status=="gap") ]|length),
                 actions:  ([ $data[]|select(.dimensions.actions.status=="gap") ]|length)
               },
               worst: ([ $data[] | {repo, score, grade} ] | sort_by(.score) | .[0:3])
             }')"
      
        if [ "$JSON" -eq 1 ]; then
          # Pipe the (large) data array via stdin; $rollup is small enough for --argjson.
          printf '%s' "$all" | jq -c --argjson roll "$rollup" \
            --argjson find "$any_findings" --argjson below "$below_min" \
            --arg minscore "${MIN_SCORE:-}" \
            '{data:., meta:($roll + {findings:($find==1 or $below>0), below_min:$below,
              min_score:(if $minscore=="" then null else ($minscore|tonumber) end),
              schema:"claude-mods.github-ops.repo-scorecard/v1"})}'
        else
          if [ "$any_findings" -eq 1 ] || [ "$below_min" -gt 0 ]; then health_roll="$(term_health warning "$swept scored")"
          else health_roll="$(term_health healthy "$swept scored")"; fi
          {
            term_panel_vert
            term_section "" "roll-up: $ORG" "$swept"
            while IFS= read -r ln; do term_panel_line "$ln"; done < <(printf '%s' "$rollup" | jq -r '
              "scored: \(.repos_scored)   unreadable: \(.repos_unreadable)",
              "avg score: \(.avg_score)   median: \(.median_score)",
              "total open security alerts (fleet): \(.total_open_alerts)",
              "gaps by dimension  security:\(.failing_by_dimension.security) metadata:\(.failing_by_dimension.metadata) release:\(.failing_by_dimension.release) issues:\(.failing_by_dimension.issues) actions:\(.failing_by_dimension.actions)",
              "worst: " + ([ .worst[] | "\(.repo) (\(.score)/\(.grade))" ] | join(", "))')
            [ -n "$MIN_SCORE" ] && term_panel_line "below --min-score $MIN_SCORE: $below_min repo(s)"
            term_panel_line "$(term_color dim "legend:") $(term_mark ok) ok  $(term_mark warn) warn  $(term_mark bad) gap  $(term_mark na) n/a   $(term_color dim "S M R I A = the five dimensions")"
            term_panel_vert
            term_panel_close "" "$health_roll"
          } >&2
        fi
        { [ "$any_findings" -eq 1 ] || [ "$below_min" -gt 0 ]; } && exit "$EX_FINDINGS"
        exit "$EX_OK"
      fi
      
      # ---- Single repo ----------------------------------------------------------
      if [ -z "$REPO" ]; then
        url="$(git remote get-url "$REMOTE" 2>/dev/null)" || skip "no '$REMOTE' remote here"
        case "$url" in
          *github.com[:/]*)
            REPO="$(printf '%s' "$url" | tr -d '\r' | sed -E 's#^.*github\.com[:/]+##; s#\.git$##; s#/$##')" ;;
          *) skip "remote '$REMOTE' is not a github.com repo" ;;
        esac
      fi
      valid_repo "$REPO" || { echo "repo-scorecard: invalid OWNER/REPO '$REPO'" >&2; exit "$EX_USAGE"; }
      
      obj="$(score_repo "$REPO")"; rc=$?
      if [ "$rc" -eq 7 ] || [ -z "$obj" ] || ! printf '%s' "$obj" | jq -e . >/dev/null 2>&1; then
        skip "couldn't read $REPO (not authed / offline / not found / timeout)"
      fi
      
      score="$(jq -r '.score' <<<"$obj")"
      below=0
      if [ -n "$MIN_SCORE" ] && [ "$score" -lt "$MIN_SCORE" ]; then below=1; fi
      
      if [ "$JSON" -eq 1 ]; then
        jq -c --argjson find "$( [ "$rc" -eq 10 ] && echo 1 || echo 0 )" \
          --argjson below "$below" --arg minscore "${MIN_SCORE:-}" \
          '{data:[.], meta:{repo:.repo, visibility:.visibility, score:.score, grade:.grade,
             findings:($find==1 or $below>0), below_min:$below,
             min_score:(if $minscore=="" then null else ($minscore|tonumber) end),
             schema:"claude-mods.github-ops.repo-scorecard/v1"}}' <<<"$obj"
      else
        print_card "$obj"
      fi
      
      { [ "$rc" -eq 10 ] || [ "$below" -eq 1 ]; } && exit "$EX_FINDINGS"
      exit "$EX_OK"
      
  • tests
    • run.sh 16.5 KB
      #!/usr/bin/env bash
      # Offline self-test for github-ops scripts. No network required — exercises the
      # contract + the gate-safety skip paths (graceful exit 7), not live GitHub data.
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      ROOT="$(cd "$HERE/.." && pwd)"
      SCRIPTS="$ROOT/scripts"
      CI="$SCRIPTS/check-issues.sh"
      SP="$SCRIPTS/check-security-posture.sh"
      
      pass=0; fail=0
      ok() { echo "  PASS  $1"; pass=$((pass+1)); }
      no() { echo "  FAIL  $1"; fail=$((fail+1)); }
      expect() { if [ "$2" = "$3" ]; then ok "$1 (exit $3)"; else no "$1 (want $2 got $3)"; fi; }
      
      echo "-- check-issues.sh (offline contract + skip paths) --"
      
      bash -n "$CI" && ok "bash -n clean" || no "bash -n"
      
      bash "$CI" --help >/dev/null 2>&1; expect "--help" 0 $?
      bash "$CI" --frobnicate >/dev/null 2>&1; expect "unknown flag -> usage" 2 $?
      
      # Non-github remote must skip with exit 7 and NEVER hit the network.
      T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
      git -C "$T" init -q
      git -C "$T" remote add origin "/some/local/path.git"
      ( cd "$T" && bash "$CI" --remote origin >/dev/null 2>&1 ); expect "non-github remote -> unavailable" 7 $?
      
      # Advisory mode on a non-github remote must be SILENT (no stderr) and exit 7 —
      # this is the gate-safety contract: an unusable check never disturbs a push.
      out="$( cd "$T" && bash "$CI" --advisory --remote origin 2>&1 )"; rc=$?
      if [ "$rc" -eq 7 ] && [ -z "$out" ]; then ok "advisory non-github -> silent exit 7"
      else no "advisory non-github (rc=$rc, stderr='$out')"; fi
      
      # Missing remote -> skip 7 (git remote get-url fails; no network).
      ( cd "$T" && bash "$CI" --remote nope-xyz >/dev/null 2>&1 ); expect "missing remote -> unavailable" 7 $?
      
      echo
      echo "-- check-security-posture.sh (offline contract + skip paths) --"
      
      bash -n "$SP" && ok "sp: bash -n clean" || no "sp: bash -n"
      
      bash "$SP" --help >/dev/null 2>&1; expect "sp: --help" 0 $?
      # --help must advertise EXAMPLES so the tool is discoverable.
      # Never assert via `producer | grep -q` in this suite: under `set -o pipefail`,
      # grep -q exits at the first match and the producer dies with SIGPIPE (141),
      # flaking the pipeline non-zero even when the pattern is present. Capture the
      # output once, then grep the variable (a here-string can't SIGPIPE).
      sp_help="$(bash "$SP" --help 2>&1)"
      if grep -q "Examples:" <<<"$sp_help"; then ok "sp: --help has EXAMPLES"
      else no "sp: --help missing EXAMPLES"; fi
      
      bash "$SP" --frobnicate >/dev/null 2>&1; expect "sp: unknown flag -> usage" 2 $?
      # Malformed OWNER/REPO is a usage error, never a network call.
      bash "$SP" --repo "not-a-valid-spec" >/dev/null 2>&1; expect "sp: bad --repo shape -> usage" 2 $?
      # --repo and --org are mutually exclusive.
      bash "$SP" --repo a/b --org c >/dev/null 2>&1; expect "sp: --repo + --org -> usage" 2 $?
      
      # Non-github remote must skip with exit 7 and NEVER hit the network.
      ( cd "$T" && bash "$SP" --remote origin >/dev/null 2>&1 ); expect "sp: non-github remote -> unavailable" 7 $?
      # Advisory mode on a non-github remote must be SILENT and exit 7.
      out="$( cd "$T" && bash "$SP" --advisory --remote origin 2>&1 )"; rc=$?
      if [ "$rc" -eq 7 ] && [ -z "$out" ]; then ok "sp: advisory non-github -> silent exit 7"
      else no "sp: advisory non-github (rc=$rc, stderr='$out')"; fi
      # Missing remote -> skip 7.
      ( cd "$T" && bash "$SP" --remote nope-xyz >/dev/null 2>&1 ); expect "sp: missing remote -> unavailable" 7 $?
      
      # --commands emits the review banner on stderr (offline path: banner prints before
      # any network work would, on a non-github remote it still skips — so assert the
      # banner via the bundled help text instead, which is fully offline).
      # The review banner string must be present in the source contract.
      if grep -q "review before running — these change repo settings" "$SP"; then ok "sp: review banner string present"
      else no "sp: review banner missing"; fi
      
      # The SECURITY.md template asset must exist and be non-trivial.
      if [ -s "$ROOT/assets/SECURITY.md.template" ] && grep -q "Reporting a Vulnerability" "$ROOT/assets/SECURITY.md.template"; then
        ok "sp: SECURITY.md.template asset present"
      else no "sp: SECURITY.md.template asset missing/empty"; fi
      
      # Read-only guarantee. The ONLY executor in this script is `runner gh api …`
      # (every -X PUT/PATCH lives inside an emitted *_cmd string, never executed). Assert
      # no `runner gh api` invocation carries a mutating verb.
      sp_api_calls="$(grep -E 'runner gh api' "$SP")"   # captured, not piped — see SIGPIPE note above
      if grep -Eq '\-X (PUT|PATCH|POST|DELETE)' <<<"$sp_api_calls"; then
        no "sp: found an executed mutating gh api call (must be read-only)"
      else ok "sp: no executed mutating gh api call (read-only)"; fi
      # And every mutating verb that DOES appear must be inside a quoted command string
      # (assigned to a *_cmd var), proving it is emitted-as-text only.
      # Inverted greps (-v) need the emptiness guard: an empty capture would feed the
      # here-string's single empty line to grep -v, which would wrongly match.
      sp_mut="$(grep -nE '\-X (PUT|PATCH|POST|DELETE)' "$SP")"
      if [ -n "$sp_mut" ] && grep -vqE '_cmd=' <<<"$sp_mut"; then
        no "sp: a mutating verb appears outside an emitted *_cmd string"
      else ok "sp: all mutating verbs are emitted text only"; fi
      
      echo
      echo "-- repo-scorecard.sh (offline contract + orchestration + read-only proof) --"
      
      RS="$SCRIPTS/repo-scorecard.sh"
      
      bash -n "$RS" && ok "rs: bash -n clean" || no "rs: bash -n"
      
      bash "$RS" --help >/dev/null 2>&1; expect "rs: --help" 0 $?
      rs_help="$(bash "$RS" --help 2>&1)"   # captured, not piped — see SIGPIPE note above
      if grep -q "Examples:" <<<"$rs_help"; then ok "rs: --help has EXAMPLES"
      else no "rs: --help missing EXAMPLES"; fi
      # The scoring rubric must be documented in the header (transparent, auditable).
      if grep -q "SCORING MODEL" <<<"$rs_help"; then ok "rs: --help documents SCORING MODEL"
      else no "rs: --help missing SCORING MODEL"; fi
      
      bash "$RS" --frobnicate >/dev/null 2>&1; expect "rs: unknown flag -> usage" 2 $?
      # Malformed OWNER/REPO is a usage error, never a network call.
      bash "$RS" --repo "not-a-valid-spec" >/dev/null 2>&1; expect "rs: bad --repo shape -> usage" 2 $?
      # --repo and --org are mutually exclusive.
      bash "$RS" --repo a/b --org c >/dev/null 2>&1; expect "rs: --repo + --org -> usage" 2 $?
      # --min-score must be an integer.
      bash "$RS" --min-score xx >/dev/null 2>&1; expect "rs: bad --min-score -> usage" 2 $?
      
      # Non-github remote must skip with exit 7 and NEVER hit the network.
      ( cd "$T" && bash "$RS" --remote origin >/dev/null 2>&1 ); expect "rs: non-github remote -> unavailable" 7 $?
      # Missing remote -> skip 7.
      ( cd "$T" && bash "$RS" --remote nope-xyz >/dev/null 2>&1 ); expect "rs: missing remote -> unavailable" 7 $?
      
      # Orchestration: it MUST call the sibling auditors by name (the reuse is the point).
      if grep -q "check-security-posture.sh" "$RS"; then ok "rs: references check-security-posture.sh"
      else no "rs: does not reference check-security-posture.sh"; fi
      if grep -q "check-issues.sh" "$RS"; then ok "rs: references check-issues.sh"
      else no "rs: does not reference check-issues.sh"; fi
      
      # Read-only guarantee: no executed mutating gh verb anywhere. Every gh call must
      # be a GET (the remediation pointers it prints are text, not executed). Assert no
      # `gh api -X PUT/PATCH/POST/DELETE` and no `gh repo edit`/`gh release create` etc.
      rs_mut="$(grep -E '\bgh (api )?-X (PUT|PATCH|POST|DELETE)' "$RS")"   # captured — SIGPIPE note above
      if [ -n "$rs_mut" ] && grep -vqE '^\s*#' <<<"$rs_mut"; then
        no "rs: found an executed mutating gh -X call (must be read-only)"
      else ok "rs: no executed mutating gh -X call (read-only)"; fi
      # Belt-and-braces: every `runner gh …` (the only network executor) is a read-only
      # subcommand — `gh api <GET path>` or `gh repo list`. No mutating subcommand runs.
      rs_runner="$(grep -nE 'runner gh ' "$RS")"
      if [ -n "$rs_runner" ] && grep -Evq 'runner gh (api|repo list)' <<<"$rs_runner"; then
        no "rs: a 'runner gh' call uses a non-read-only subcommand"
      else ok "rs: every executed 'runner gh' is read-only (api / repo list)"; fi
      # And mutating gh subcommands, where they appear, are inside printed fix strings only
      # (the remediation pointers), never executed. Verify they sit on addfix/echo lines.
      rs_ghsub="$(grep -nE 'gh (release create|repo edit|release delete|secret set|pr merge)' "$RS")"
      if [ -n "$rs_ghsub" ] && grep -vqE 'addfix|→' <<<"$rs_ghsub"; then
        no "rs: a mutating gh subcommand appears outside a printed remediation string"
      else ok "rs: mutating gh subcommands only appear as printed remediation text"; fi
      
      echo
      echo "-- README landing-page reference (existence, citation, contract) --"
      
      # CONTRACT NOTE for future edit lanes: these assertions bind SKILL.md's *content*,
      # not just the reference file. If you move or rename references/readme-landing-page.md,
      # or drop its citations from the conventions table / mode new / mode update / mode audit,
      # this block fails on purpose — an uncited reference is dead weight the router never finds.
      LP="$ROOT/references/readme-landing-page.md"
      SK="$ROOT/SKILL.md"
      
      if [ -s "$LP" ]; then ok "landing-page reference exists and is non-empty"
      else no "references/readme-landing-page.md missing or empty"; fi
      
      # It must actually cover the four things it owns; a stub that only exists to satisfy
      # the citation check would pass a bare -s test.
      lp_body="$(cat "$LP" 2>/dev/null)"   # captured, not piped — see SIGPIPE note above
      for topic in "Two registers" "Badge row" "Features as benefits" "Screenshots and demo media" "Anti-patterns"; do
        if grep -qF "$topic" <<<"$lp_body"; then ok "reference covers: $topic"
        else no "reference missing section: $topic"; fi
      done
      
      # The badge guidance is worthless without the brand-pairing lever and the
      # most-common misconfiguration; both are named failure modes in the brief.
      if grep -qF "labelColor" <<<"$lp_body"; then ok "reference documents labelColor"
      else no "reference does not mention labelColor"; fi
      if grep -qF "docs/screenshots/" <<<"$lp_body"; then ok "reference pins screenshots to docs/screenshots/"
      else no "reference does not name docs/screenshots/"; fi
      if grep -qF "prefers-color-scheme" <<<"$lp_body"; then ok "reference documents the <picture> dark-mode pattern"
      else no "reference missing prefers-color-scheme guidance"; fi
      
      # Register axis: both registers must be named, AND the reference must state the
      # guard that keeps "Showcase" from becoming a licence for marketing fluff. Without
      # that boundary the register choice silently reopens readme-description.md's
      # anti-patterns, which is the whole risk of offering the choice at all.
      for r in "Showcase" "Reference"; do
        if grep -qF "$r" <<<"$lp_body"; then ok "reference names the $r register"
        else no "reference does not name the $r register"; fi
      done
      if grep -qF "What does NOT vary" <<<"$lp_body"; then ok "reference fences what register does NOT change"
      else no "reference missing the register invariants section (fluff guard)"; fi
      
      # Cross-reference must be BIDIRECTIONAL. The sibling references are the far more
      # common entry points (a release loads readme-recent-updates.md, an intro rewrite
      # loads readme-description.md); if neither points forward, an agent on those paths
      # never learns the landing-page layer exists and reinstates the pre-split picture.
      for sib in readme-description readme-recent-updates; do
        if grep -q "readme-landing-page.md" "$ROOT/references/$sib.md"; then
          ok "$sib.md links forward to the landing page"
        else no "$sib.md has no forward link (landing page unreachable from that path)"; fi
      done
      
      # Public-repo hygiene (hard rule 7 + tests/agnostic.sh): no local machine paths.
      if grep -Eq '[A-Za-z]:[\\/]Users[\\/]|/home/[a-z]|/Users/[A-Za-z]' <<<"$lp_body"; then
        no "reference contains a machine-specific local path"
      else ok "reference has no machine-specific local paths"; fi
      
      # Citation reachability: SKILL.md must point at it from the conventions table AND
      # from each mode that acts on it, or the router never loads it.
      sk_body="$(cat "$SK" 2>/dev/null)"
      cites="$(grep -c "references/readme-landing-page.md" <<<"$sk_body")"
      if [ "${cites:-0}" -ge 4 ]; then ok "SKILL.md cites the reference $cites times (>=4: table + new + update + audit)"
      else no "SKILL.md cites the reference only ${cites:-0} times (want >=4)"; fi
      
      # It must be listed in the Files table, like every other reference.
      if grep -qE '^\| `references/readme-landing-page\.md` \|' <<<"$sk_body"; then
        ok "SKILL.md Files table lists the reference"
      else no "SKILL.md Files table missing the reference row"; fi
      
      # Audit-mode behaviour: the new rows are WARN-level and the screenshot row is
      # CONDITIONAL on the project having something to show. Both are the whole point —
      # a hard fail or an unconditional nag would make the audit noise.
      if grep -qF "LANDING-PAGE CHECKS" <<<"$sk_body"; then ok "audit mode has a landing-page check block"
      else no "audit mode missing the landing-page check block"; fi
      lp_block="$(awk '/LANDING-PAGE CHECKS/,/^$/' "$SK")"
      if grep -qE 'WARN-level, never a hard fail' <<<"$lp_block"; then ok "audit rows declared WARN-level, not hard fails"
      else no "audit landing-page rows not declared WARN-level"; fi
      if grep -qF "CONDITIONAL" <<<"$lp_block"; then ok "screenshot row is conditional on a visual surface"
      else no "screenshot audit row is not conditional (would nag CLI libraries)"; fi
      # Audit must judge against the README's OWN register, or it flags a Reference
      # README for declining a hero — the exact false positive the axis exists to avoid.
      if grep -qF "REGISTER" <<<"$lp_block"; then ok "audit infers the register before judging rows"
      else no "audit rows are register-blind (would flag Reference READMEs for missing a hero)"; fi
      
      # FRONTMATTER CONTRACT — this suite requires README trigger phrases in the skill's
      # `description:` field. The description IS the router's trigger: github-ops owns the
      # README intro, the landing page and Recent Updates, but a request like "write me a
      # README" reaches none of it unless the description says so. A description-trim lane
      # that strips these phrases silently un-routes three references, so the assertion
      # lives here and this comment says why. Keep the phrases; trim elsewhere if needed.
      sk_desc="$(grep -m1 '^description:' "$SK")"
      for cue in "write a README" "README badges"; do
        if grep -qF "$cue" <<<"$sk_desc"; then ok "description carries the '$cue' trigger"
        else no "description missing README trigger: '$cue' (skill unreachable for README work)"; fi
      done
      # Per-skill description cap is 1000 chars (tests/validate.sh) and the catalog-wide
      # budget is already tight — assert we stayed well inside it.
      desc_len=${#sk_desc}
      if [ "$desc_len" -le 1000 ]; then ok "description within the 1000-char cap ($desc_len)"
      else no "description is $desc_len chars (cap 1000)"; fi
      
      # Mode new must offer the register as a user-flippable choice, not decide silently.
      if grep -qE "say 'showcase' to flip" <<<"$sk_body"; then ok "mode new surfaces register as a flippable line"
      else no "mode new does not surface the register choice to the user"; fi
      
      # The scorecard is deliberately NOT extended — assert the decision stayed put, so a
      # later lane that adds scoring has to update the rubric in --help at the same time.
      if grep -qF "readme-landing-page" "$RS"; then
        no "rs: scorecard now references the landing page — its --help SCORING MODEL must be updated in the same commit"
      else ok "rs: scorecard left unscored for landing-page signals (documented decision)"; fi
      
      echo
      echo "-- terminal design system (term.sh adoption + ASCII fallback) --"
      
      # All three auditors must source the shared toolkit, not hand-roll ANSI.
      for s in "$CI" "$SP" "$RS"; do
        b="$(basename "$s")"
        if grep -q '_lib/term.sh' "$s"; then ok "$b sources _lib/term.sh"
        else no "$b does not source _lib/term.sh"; fi
      done
      
      LIBTERM="$ROOT/../_lib/term.sh"
      if [ -f "$LIBTERM" ]; then
        ok "term.sh present"
        # Under TERM_ASCII=1 every framing primitive must fall back to pure ASCII
        # (design principle #3: every glyph has a registered ASCII proxy).
        marks="$(TERM_ASCII=1 LT="$LIBTERM" bash -c '. "$LT"; term_init; printf "%s%s%s%s%s%s%s%s%s%s%s" \
          "$(term_mark ok)" "$(term_mark bad)" "$(term_mark warn)" "$(term_mark na)" \
          "$(term_mark unknown)" "$(term_header hdr)" "$TERM_ARROW" \
          "$(term_panel_open github-ops PANEL meta)" "$(term_panel_line body)" \
          "$(term_section "" sect 3)" "$(term_panel_close hk "$(term_health warning x)")"')"
        if LC_ALL=C grep -q '[^[:print:][:cntrl:]]' <<<"$marks"; then
          no "term.sh TERM_ASCII=1 still emits non-ASCII bytes"
        else ok "term.sh TERM_ASCII=1 primitives are pure ASCII"; fi
        # A fallback that silently drops the glyph (empty) is a bug, not a fallback.
        m="$(TERM_ASCII=1 LT="$LIBTERM" bash -c '. "$LT"; term_init; term_mark ok')"
        [ -n "$m" ] && ok "term_mark renders non-empty in ASCII mode" || no "term_mark ok is empty"
      else
        no "term.sh missing at $LIBTERM"
      fi
      
      echo
      echo "=== $pass passed, $fail failed ==="
      [ "$fail" -eq 0 ]
      
  • SKILL.md 31.6 KB
    ---
    name: github-ops
    description: "GitHub remote operations and README authoring: repo creation, metadata, releases, issue/PR management with preview-before-send, README as a landing page (badge row, features-as-benefits, screenshots, Recent Updates), and read-only security auditing. Triggers on: write a README, improve the README, README badges, README features section, push to github, ship release, gh release, audit github repo, gh issue, gh pr, merge PR, branch protection, secret scanning, SECURITY.md."
    license: MIT
    allowed-tools: "Read Write Edit Bash Glob Grep"
    metadata:
      author: claude-mods
      related-skills: git-ops, push-gate, ci-cd-ops
    ---
    
    # GitHub Ops
    
    GitHub-side operations skill. Owns everything that talks to `api.github.com` via `gh` CLI: repo creation, metadata configuration, releases, and the conventions that govern how 0xDarkMatter repos present on GitHub.
    
    Sits alongside two related skills:
    
    ```
    LOCAL                          BRIDGE              REMOTE (GitHub)
    ─────                          ──────              ───────────────
    git-ops                        push-gate           github-ops  (this skill)
    ```
    
    | Concern | Owner |
    |---|---|
    | Commits, branches, local tags, rebases, worktrees, stash | `git-ops` |
    | Pre-push secret scan + dirty-tree refusal + confirm | `push-gate` |
    | `gh repo create`, push to remote, tag push | **`github-ops`** |
    | Repo description / homepage / topics / visibility | **`github-ops`** |
    | `gh release create` + release notes | **`github-ops`** |
    | README "Recent Updates" section maintenance | **`github-ops`** |
    | README as a landing page (badge row, features-as-benefits, screenshots) | **`github-ops`** |
    | Package metadata audit (pyproject/package.json ↔ GH topics ↔ tag ↔ version) | **`github-ops`** |
    | `gh issue` operations (view/list/create/comment/edit/triage/close) | **`github-ops`** |
    | `gh pr` operations (view/list/diff/checks/create/comment/review/edit/merge/close) | **`github-ops`** |
    | Security posture audit (Dependabot / secret+code scanning / PVR / SECURITY.md / branch protection) — read-only | **`github-ops`** (`scripts/check-security-posture.sh`) |
    | Actions / secrets / social preview / branch-protection *writes* | **`github-ops`** (future) |
    
    ## Hard rules
    
    1. **Visibility defaults to private.** Pass `--private` to `gh repo create` unless the user has explicitly said "public" / "make it public" for this specific repo. See `references/repo-visibility.md`.
    2. **Major version bumps require explicit approval.** Default to minor; patch for fix-only ranges. Never auto-suggest a 1.0.0 from `BREAKING CHANGE:` markers — surface and ask. See `references/release-strategy.md`.
    3. **Always run `push-gate` before any push to a remote.** No exceptions. If push-gate refuses, do not proceed — fix the cause and re-run.
    4. **Delegate local git operations to `git-ops`.** Don't reimplement commit/tag/push logic. github-ops orchestrates the GitHub-side calls (`gh`) and the README/CHANGELOG edits; git-ops handles git itself.
    5. **README "Recent Updates" updates on every release.** This is the one README touch that always happens, regardless of how minor the release. See `references/readme-recent-updates.md` for the canonical claude-mods style.
    6. **Never push without confirming visibility decision.** When creating a new repo, surface visibility as a flippable line in the plan ("creating as **private** — say 'public' to flip"), not buried in flag soup.
    7. **No local-machine paths in committed content.** Never bake `C:\Users\<name>\…`, `/home/<name>/…`, `/Users/<name>/…`, `/tmp/<one-off-test-dir>`, or any other machine-specific path into README entries, Recent Updates bullets, CHANGELOG entries, release notes, tag annotations, or commit messages. Public release artefacts have to read the same on someone else's machine. Use generic placeholders (`~/Temp/`, `<temp-dir>`, "a temp directory") or describe the file's purpose abstractly instead. If a path genuinely is part of the project's public API (install location, config path), state it canonically (`$HOME/.claude/skills/...`), not as a literal absolute that includes a user name.
    8. **Preview every public post before sending.** Anything with author voice that lands on a third-party surface — `gh issue create/comment/edit --body`, `gh pr create/comment/review/edit --body`, `gh release create --notes`, merge commit `--subject`/`--body` — must be quoted verbatim in chat with the exact send command named, then await explicit approval before invoking. Mechanical actions with no body (label, assign, milestone, mark-ready, close-without-message) skip preview. See `~/.claude/rules/public-posts.md` for the full rule.
    
    ## Three modes
    
    ### Mode `new` — first publish of a repo
    
    Triggered by: "publish to github", "create repo on github", "push to github" (when no `origin` remote exists), "ship this repo".
    
    ```
    1. Audit (run mode `audit` checklist; abort on critical fail)
       - LICENSE present?
       - README has tagline + install + quickstart?
       - pyproject.toml / package.json has description, keywords, license, repository URL?
       - At least one tag exists (typically v0.1.0)?
       - CHANGELOG.md has an entry for the latest tag?
    
    2. Draft / refine README intro (2–3 paragraphs) — see references/readme-description.md
       - If the README intro is just a tagline or < 80 words, draft a proper 2–3 paragraph
         description: what it is, why it exists, who it's for. Read package metadata, CHANGELOG,
         and the primary entry point first; do not fabricate.
       - Voice: developer-to-developer, concrete, occasional dry wit (earned, never sprayed).
         Anti-patterns ("blazing fast", emoji walls, marketing fluff) listed in the reference.
       - Surface the draft to the user for approval before committing — this is the repo's
         first impression and shouldn't be a one-shot.
       - Commit via git-ops with: docs: Expand README intro
    
    2b. Build the landing-page layer — see references/readme-landing-page.md
       The intro answers "what is this"; this step answers the other three questions a
       cold visitor asks in their first ten seconds (is it alive / what do I get /
       what does it look like). Benefits before mechanics.
    
       FIRST pick the register, and surface it as a flippable line like visibility:
         "Laying the README out as **Reference** — say 'showcase' to flip"
       - Showcase — reader is deciding WHETHER to adopt. Apps, dashboards, TUIs,
         generators, anything with visible output. Visual high, Features above Install,
         airier prose.
       - Reference — reader has already decided and needs to USE it. Libraries, SDKs,
         plain-output CLIs, internal tooling. Install + a real usage example in the
         first screenful; Features denser and lower.
       Tie-breakers: output is visible → Showcase. It's a dependency of other code →
       Reference. Register controls emphasis and density, NEVER honesty — Showcase is
       not permission for marketing verbs; the readme-description.md anti-patterns
       apply identically to both. Never mix the two.
    
       Then, in whichever register:
       - Badge row under the title: at most five — license, version, CI, runtime floor,
         project status. One shared labelColor so they read as one row. Never add a badge
         you won't maintain; a stale red CI badge is worse than no badge.
       - ## Features section ABOVE Install, written as benefits (bold lead = what the
         reader gets, then the concrete detail), 4–7 bullets. Not a component inventory.
       - A screenshot or demo ONLY if the project has a visual surface (TUI/GUI/dashboard/
         rendered output, or colourised CLI output). Plain-text CLI and libraries take a
         fenced code block instead. Store under docs/screenshots/, alt text on every image,
         <picture> + prefers-color-scheme so it doesn't glow white in dark mode.
       - Surface the layout to the user with the intro draft; commit together.
    
    3. Add "Recent Updates" section to README if missing
       - Use claude-mods style by default (see references/readme-recent-updates.md)
       - Place after Quickstart, before deep "why this exists" sections — i.e. below the
         Features + visual added in step 2b (see references/readme-landing-page.md for the
         full section order and why liveness sits there, not above Features)
       - For first release, single bullet block describing the initial extraction
       - Commit via git-ops with: docs: Add Recent Updates section
    
    4. Surface the publish plan to user, with visibility as a flippable line:
       "Creating as **private** at github.com/<org>/<repo> — say 'public' to flip"
       Wait for explicit confirmation.
    
    5. Create the repo:
       gh repo create <org>/<repo> --private --source=. --remote=origin \
         --description "<one-line — distilled from the README intro draft in step 2, ≤ 350 chars>" \
         --homepage "<homepage URL or omit>"
       (NEVER pass --push; we want push-gate to run between)
       Note: the GitHub `--description` is a single line and distinct from the README intro.
       Derive it FROM the intro you just wrote, not from package metadata blindly.
    
    6. Run push-gate preflight:
       bash $HOME/.claude/skills/push-gate/scripts/preflight.sh --cwd <repo> origin main
       On any non-zero exit: stop, report, do not push.
    
    7. Push main + tags:
       git -C <repo> push -u origin main
       git -C <repo> push origin --tags
    
    8. Set topics (derived from package keywords + language + frameworks):
       gh repo edit <org>/<repo> --add-topic <t1> --add-topic <t2> ...
       Aim for 6–12 topics. See references/metadata-checklist.md for derivation.
    
    9. Create the release for the latest tag:
       gh release create <tag> --title "<tag> — <one-line headline>" \
         --notes "$(extract from CHANGELOG.md)"
    
    10. Verify:
        gh repo view <org>/<repo>
        gh release view <tag>
        Report URL to user.
    ```
    
    ### Mode `update` — subsequent release
    
    Triggered by: "ship a release", "cut a release", "release v0.X.Y", "publish update".
    
    ```
    1. Audit current state vs last release:
       git -C <repo> log $(git describe --tags --abbrev=0)..HEAD --oneline
       Categorise commits by Conventional Commits prefix.
    
    2. Determine version bump (see references/release-strategy.md):
       - Any feat: → minor (default)
       - Only fix:/chore:/docs:/perf:/style:/test: → patch
       - Any BREAKING CHANGE: or !: → STOP, ask user, never auto-major
    
    3. Update CHANGELOG.md:
       New section for the new version with categorised changes (Added/Changed/Fixed/Removed).
       Delegate the file edit + commit to git-ops with: docs: CHANGELOG for v<N>
    
    4. Update README "Recent Updates":
       Prepend a new version block (claude-mods style) at the top of the section.
       Trim oldest if section exceeds 7 versions.
       Bullets per change, emoji + bold tagline + 1-3 sentence prose.
       See references/readme-recent-updates.md for the emoji vocabulary.
    
       For minor: update Recent Updates AND scan diff for new commands/config/install steps;
                  touch README body sections only if found.
       For patch: update Recent Updates ONLY (single bullet); no body changes unless asked.
    
       Also: if the README intro is still < 80 words OR the repo's scope has drifted since
       the intro was written, propose an expansion (see references/readme-description.md).
       Don't churn good prose — only act if the intro is genuinely thin or stale.
    
       Landing-page touch-ups (see references/readme-landing-page.md) — act only on a
       real trigger, never as routine churn. Keep the README's EXISTING register
       (Showcase vs Reference); never switch it silently. A genuine audience change
       (internal tool going public) is worth proposing a switch — done all at once,
       with approval — not drifting into one bullet at a time:
       - The release added a capability worth a Features bullet → add one (benefit-led),
         and cut a weaker one if the section now runs past ~7.
       - The CI badge is red/stale, or the version badge no longer tracks releases →
         fix it or remove it. A badge nobody maintains is worse than no badge.
       - A shipped UI change made an existing screenshot wrong → recapture or drop it.
       - The repo has a visual surface and still has no visual → propose one; don't add
         it unasked.
    
    5. Commit README + CHANGELOG via git-ops:
       docs: Recent Updates + CHANGELOG for v<N>
    
    6. Create local tag via git-ops:
       git tag -a v<N> -m "v<N>"
    
    7. Run push-gate preflight:
       bash $HOME/.claude/skills/push-gate/scripts/preflight.sh --cwd <repo> origin <branch>
       On any non-zero exit: stop, report, do not push.
    
    8. Push commits + tag:
       git push origin <branch>
       git push origin v<N>
    
    9. Create GitHub release:
       gh release create v<N> --title "v<N> — <headline>" \
         --notes "$(extract CHANGELOG section for v<N>)"
    
    10. Verify:
        gh release view v<N>
        Report URL to user.
    ```
    
    ### Mode `audit` — read-only checklist
    
    Triggered by: "audit github repo", "is this repo ready to publish", "check repo metadata", "score this repo", "how healthy is this repo", "score the fleet".
    
    **Headline: `scripts/repo-scorecard.sh` — one command for a scored repo/fleet health report.** It orchestrates the two read-only auditors (`check-security-posture.sh` + `check-issues.sh`) and adds metadata / release / actions signals, rolling everything into a single **0–100 score + letter grade** per repo, and a **matrix + roll-up** across an org. Reach for it first; drop to the manual checklist below only when you need a specific row the scorecard doesn't surface.
    
    ```bash
    bash scripts/repo-scorecard.sh --repo 0xDarkMatter/flarecrawl     # single repo: score + dimensions + top 3 fixes
    bash scripts/repo-scorecard.sh --org 0xDarkMatter                 # fleet matrix + roll-up (avg/median/worst, fleet open-alert total)
    bash scripts/repo-scorecard.sh --org 0xDarkMatter --min-score 75  # CI gate: exit 10 if ANY repo scores < 75
    bash scripts/repo-scorecard.sh --repo <o>/<r> --json | jq '.data[0].top_fixes'
    ```
    
    Five weighted dimensions — **security (35)** highest, then **metadata (25)**, **release (15)**, **issues (15)**, **actions (10)**. Each scores its weight in full (ok) / half (warn) / zero (gap **or** unreadable n/a — an unreadable dimension never counts as healthy). Grade: A≥90 B≥75 C≥60 D≥40 F<40. The full rubric is documented in the script header (`--help`). It surfaces the **top 3 fixes per repo**, highest-severity first, each with the exact remediation pointer (e.g. `→ check-security-posture.sh --repo … --commands`, "add CHANGELOG.md", "cut a GitHub release"). Exit `0` healthy · `10` gaps / below `--min-score` · `7` unavailable (graceful) · `5` gh missing · `2` usage. **Strictly read-only** — only GET `gh api` calls + the read-only siblings; the remediation pointers are text, never executed.
    
    Below is the underlying checklist the scorecard's dimensions roll up (and what mode `new`/`update` act on). See `references/metadata-checklist.md` for the complete version; the SKILL enforces these:
    
    ```
    LOCAL FILE CHECKS
      [ ] LICENSE file present + matches metadata
      [ ] README has: tagline, install, quickstart, license link
      [ ] README intro is ≥ 80 words (2–3 paragraphs orienting a cold reader)
      [ ] README has "Recent Updates" section near top
    
    LANDING-PAGE CHECKS — all WARN-level, never a hard fail (references/readme-landing-page.md)
      [~] Infer the README's REGISTER first (Showcase = pitch-forward, visual high, Features
          above Install; Reference = install + usage in the first screenful, denser Features)
          and judge every row below against THAT register. A Reference README is not missing
          a hero — it declined one. WARN if the register is visibly mixed (a Showcase hero
          bolted onto a Reference body, or vice versa): it serves neither reader.
      [~] README has a badge row under the title (≤ 5 badges; license + at least one
          liveness signal — CI or version). WARN if absent; WARN if > 7 badges (badge wall)
          or if a CI badge points at a workflow with no runs / a red default branch.
      [~] README has a "## Features" (or equivalent) section ABOVE Install, with bullets
          that lead with what the reader GETS, not what the software contains. WARN if the
          section is missing, or if it is a component inventory / a flag-by-flag table.
      [~] README has a screenshot or demo — CONDITIONAL. Only warn when the project has a
          visual surface (TUI, GUI, dashboard, web UI, rendered/generated output, or
          colourised CLI output). A plain-text CLI, a library, an SDK, or a config/skill
          bundle legitimately has none: report nothing, do not nag. Where images exist,
          WARN on missing alt text or a light-only capture with no <picture> dark variant.
      [ ] CHANGELOG.md present and has entry for latest tag
      [ ] pyproject.toml / package.json: description, keywords, license, repository URL, homepage
      [ ] Latest tag matches version in package metadata
    
    GITHUB STATE CHECKS (skip if no remote)
      [ ] Repo description is set
      [ ] Repo homepage is set (or explicitly N/A)
      [ ] At least 3 topics
      [ ] Topics align with package keywords
      [ ] Default branch is main (not master)
      [ ] Latest tag has a corresponding release
      [ ] Release notes match CHANGELOG entry
    
    SECURITY POSTURE CHECKS (run scripts/check-security-posture.sh — read-only)
      [ ] Dependabot alerts enabled
      [ ] Dependabot security updates enabled
      [ ] Secret scanning + push protection on   (free on public; needs GHAS on private)
      [ ] Code scanning default setup configured  (free on public; needs GHAS on private)
      [ ] Private vulnerability reporting enabled
      [ ] SECURITY.md present (root / .github/ / docs/)
      [ ] Branch protection on the default branch
      [ ] No OPEN dependabot / secret / code-scanning alerts on enabled scanners
    ```
    
    The landing-page rows are marked `[~]` because they are **advisory**: they never fail an
    audit and never block mode `new`. They are also **not** scored by `repo-scorecard.sh` —
    judging "are these bullets benefits" and "does this project have anything to show" needs
    reading comprehension the script can't do at fleet scale, and a wrong answer there would
    be charged to every repo. See the reference's closing section for the full rationale.
    
    Output: per-row pass/fail/warn, then a summary score and list of fixes. Fixes are suggested but not applied — the user decides whether to run mode `new` or mode `update` to act on them. For the security-posture rows, run `scripts/check-security-posture.sh --repo <o>/<r>` and fold its checklist in; the enable commands it emits are surfaced for the user to approve, never auto-run.
    
    ## Operations
    
    Atomic GH-side actions that don't fit the three multi-step modes. Each operation that writes author voice to a third-party surface (issue/PR body, comment, review body, release notes, merge commit subject/body) is governed by **hard rule 8** and [public-posts](~/.claude/rules/public-posts.md): quote the exact body in chat, name the send command, wait for explicit approval, then send. Mechanical actions (labels, assign, close-without-message, mark-ready) skip preview.
    
    ### Issues
    
    Reads (no preview): `gh issue view <n>`, `gh issue view <n> --comments`, `gh issue list`, `gh api repos/<o>/<r>/issues/<n>` (for fields not in the default view).
    
    Writes:
    
    | Op | Command | Preview? |
    |---|---|---|
    | Create | `gh issue create --title --body` | **Yes** (title + body) |
    | Comment | `gh issue comment <n> --body` | **Yes** (body) |
    | Edit title/body | `gh issue edit <n> --title --body` | **Yes** |
    | Triage (label/assign/milestone) | `gh issue edit <n> --add-label … --assignee … --milestone …` | No (mechanical) |
    | Close / reopen | `gh issue close <n>` / `gh issue reopen <n>` | No, **unless** closing with a comment — preview the comment |
    | Transfer | `gh issue transfer <n> <target-repo>` | No (mechanical), but confirm target with user |
    
    See `references/issue-ops.md` for full playbooks, triage flow, and closing-comment templates.
    
    ### Pull Requests
    
    Reads (no preview): `gh pr view <n>`, `gh pr view <n> --comments`, `gh pr list`, `gh pr diff <n>`, `gh pr checks <n>`, `gh pr checks <n> --watch`, `gh api repos/<o>/<r>/pulls/<n>/comments` (inline review comments).
    
    Writes:
    
    | Op | Command | Preview? |
    |---|---|---|
    | Create | `gh pr create --title --body` | **Yes** (title + body) |
    | Comment | `gh pr comment <n> --body` | **Yes** |
    | Review (approve / request changes / comment) | `gh pr review <n> --approve --body …` | **Yes** (body, if any) |
    | Edit title/body | `gh pr edit <n> --title --body` | **Yes** |
    | Edit labels / reviewers | `gh pr edit <n> --add-label … --add-reviewer …` | No (mechanical) |
    | Mark ready (un-draft) | `gh pr ready <n>` | No (mechanical) |
    | Merge | `gh pr merge <n> --squash` (or `--merge` / `--rebase`) | No body to preview by default, but **explicit user approval required** + run pre-merge gate first. If passing `--subject` / `--body`, preview those (they become the commit message on `main`) |
    | Close | `gh pr close <n>` | No, **unless** closing with a comment — preview the comment |
    
    **PR creation lives here, not in git-ops.** git-ops handles local commits/branches/push; the `gh pr create` call itself talks to `api.github.com` and belongs in this skill. (Existing git-ops T2 PR-create still works; new flows should route through github-ops.)
    
    **Pre-merge gate** — never invoke `gh pr merge` without first confirming:
    
    1. `gh pr view <n> --json mergeable,mergeStateStatus` → `mergeable: MERGEABLE`, `mergeStateStatus: CLEAN`
    2. `gh pr checks <n>` → every check passed (or explicitly ignored with user approval)
    3. `gh pr diff <n>` reviewed — confirm no surprise scope, no committed secrets/local paths, no stale PR-body claims
    4. Merge strategy picked — **default squash** for fix/feature branches with multiple WIP commits; `--merge` only when individual commits matter; `--rebase` for linear-history repos. Ask if uncertain.
    5. Branch deletion is a **separate explicit step**, not bundled. Default to keeping the branch; delete remote + local after merge only on explicit user OK (it's destructive enough to warrant its own confirmation, and a checked-out branch can't be deleted).
    
    See `references/pr-ops.md` for full playbooks, review-flow templates, and the merge-strategy decision tree.
    
    ## Conventions enforced (load reference files for detail)
    
    | Convention | File | Default |
    |---|---|---|
    | Release strategy | `references/release-strategy.md` | minor on `feat:`, patch on `fix:`-only, major requires approval |
    | README intro (2–3 paragraphs) | `references/readme-description.md` | what it is / why it exists / who it's for; concrete, dry, no marketing fluff |
    | README as a landing page | `references/readme-landing-page.md` | pick a register first (**Showcase** pitch-forward vs **Reference** usage-forward) and never mix; then section order (benefits before mechanics), ≤ 5-badge row with a shared `labelColor`, features-as-benefits, conditional screenshot in `docs/screenshots/` with alt text + dark variant |
    | README Recent Updates style | `references/readme-recent-updates.md` | claude-mods per-version blocks (alternate: flarecrawl table) |
    | Repo visibility default | `references/repo-visibility.md` | `--private` unless user says "public" |
    | Metadata audit checklist | `references/metadata-checklist.md` | full source-of-truth for mode `audit` |
    | Issue operations | `references/issue-ops.md` | view → triage → comment (with preview) → close; closing comments preview-gated |
    | PR operations | `references/pr-ops.md` | create (preview body) → review → pre-merge gate → squash by default; branch deletion separate explicit step |
    
    ## Git authorship
    
    For 0xDarkMatter repos, set repo-local config before any commit work:
    
    ```bash
    git -C <repo> config user.name "0xDarkMatter"
    git -C <repo> config user.email "0xDarkMatter@users.noreply.github.com"
    ```
    
    Verify with `git -C <repo> config user.name`. If a commit was made under a different identity *before* publish (no push has happened), rewrite via:
    
    ```bash
    git -C <repo> rebase --root --exec 'git commit --amend --reset-author --no-edit'
    ```
    
    After history rewrite, re-create any tags so they point at the new SHAs:
    
    ```bash
    git -C <repo> tag -d v0.1.0
    git -C <repo> tag -a v0.1.0 -m "..."
    ```
    
    This is safe pre-publish only. After push, treat history as immutable and set authorship correctly going forward.
    
    ## Delegation pattern
    
    ```
    github-ops           git-ops              push-gate
    ─────────            ───────              ─────────
    mode `new`:
      audit
      edit README   ───► commit (T2)
                                              preflight (before push)
      gh repo create
                    ───► push -u origin main
                    ───► push --tags
      gh repo edit (topics)
      gh release create
      verify
    
    mode `update`:
                    ───► CHANGELOG edit + commit (T2)
      edit Recent Updates
                    ───► commit (T2)
                    ───► tag (T2)
                                              preflight (before push)
                    ───► push (T2)
                    ───► push tag (T2)
      gh release create
      verify
    ```
    
    When invoking git-ops T2 operations, dispatch to git-agent with a one-shot prompt — no need to load the full git-ops orchestrator state for these mechanical steps.
    
    ## Future expansion (not yet implemented)
    
    - **Actions** — workflow file scaffolding, `gh workflow` operations
    - **Secrets** — `gh secret set/list/delete` (with secure handling)
    - **Branch protection** — `gh api` calls for protection rules
    - **Social preview** — image upload via `gh api`
    - **Org-level** — teams, repo templates
    
    When adding any of the above, keep the boundary discipline: anything talking to `api.github.com` belongs here, anything purely local belongs to `git-ops`.
    
    ## Files
    
    | File | Role |
    |---|---|
    | `SKILL.md` | This file — modes, rules, delegation |
    | `references/release-strategy.md` | Version bump policy |
    | `references/readme-description.md` | 2–3 paragraph README intro — voice, structure, anti-patterns |
    | `references/readme-landing-page.md` | The layer between intro and changelog — the Showcase/Reference register choice, section order for each, badge row (shields.io + `labelColor`), features-as-benefits with a before/after rewrite, screenshot/demo policy, landing-page anti-patterns |
    | `references/readme-recent-updates.md` | "Recent Updates" section format + emoji vocabulary |
    | `references/repo-visibility.md` | Private-by-default policy |
    | `references/metadata-checklist.md` | Audit checklist source of truth |
    | `references/issue-ops.md` | Issue operation playbooks (view/triage/comment/create/close) + preview templates |
    | `references/pr-ops.md` | PR operation playbooks (create/review/merge) + pre-merge gate + merge-strategy decision tree |
    | `scripts/repo-scorecard.sh` | **Capstone audit tool.** Scored, read-only repo-health matrix — orchestrates `check-security-posture.sh` + `check-issues.sh` and adds metadata/release/actions signals into a 0–100 score + grade per repo; `--org` for a fleet matrix + roll-up; `--min-score N` to gate CI; `--json` envelope. Surfaces top-3 fixes per repo. Never mutates |
    | `scripts/check-issues.sh` | Surface open issues you may not have seen (externally-authored + stale) for a repo or remote. Read-only `gh issue list`; flags author≠owner and untouched-for-N-days |
    | `scripts/check-security-posture.sh` | Read-only repo security-posture auditor. Per-feature checklist (Dependabot alerts/updates, secret scanning + push protection, code scanning, private vuln reporting, SECURITY.md, branch protection), visibility-aware severity, open-alert exposure where a scanner is on, `--org` fleet sweep. Emits enable commands as text — never applies a change |
    | `assets/SECURITY.md.template` | Copy-ready vulnerability-disclosure policy (supported versions, private reporting via GitHub PVR, response SLAs, scope, safe harbor) — what `check-security-posture.sh` points at when SECURITY.md is absent |
    
    ## Open-issue awareness (the blind spot)
    
    You don't see issues other people file — your own you know about; a stranger's bug report from two months ago is the gap. `scripts/check-issues.sh` closes it:
    
    ```bash
    bash scripts/check-issues.sh --repo 0xDarkMatter/flarecrawl   # one repo
    bash scripts/check-issues.sh --remote origin --stale-days 14  # derive from a remote
    bash scripts/check-issues.sh --json | jq '.data[] | select(.external)'
    ```
    
    Exit `0` = nothing you're missing (no open issues, or all are yours and fresh); `10` = external/stale issues present (the things to look at); `7` = unavailable (not a GitHub remote, gh unauthed/offline) — advisory, never a hard failure; `2` usage; `5` gh not installed.
    
    **Wired into the pre-push gate** ([push-gate](../push-gate/)): `preflight.sh` calls this in `--advisory` mode as a post-gate step, so every push surfaces unseen external/stale issues for the target remote. It is **read-only, timeout-bounded, and never affects the gate verdict** — silent when gh is absent/unauthed or the remote isn't GitHub. Run it standalone any time, or across repos, to find what you've missed. For acting on what it surfaces (view/triage/comment/close), see `references/issue-ops.md`.
    
    ## Security posture (the other blind spot)
    
    GitHub ships a stack of free security features — Dependabot alerts, security updates, secret scanning + push protection (free on **public** repos), code scanning default setup, private vulnerability reporting, branch protection — and most are **off by default**. You don't see the gap until something leaks. `scripts/check-security-posture.sh` audits it, read-only:
    
    ```bash
    bash scripts/check-security-posture.sh --repo 0xDarkMatter/flarecrawl   # one repo
    bash scripts/check-security-posture.sh --remote origin                  # derive from a remote
    bash scripts/check-security-posture.sh --org 0xDarkMatter               # fleet sweep + roll-up
    bash scripts/check-security-posture.sh --repo <o>/<r> --commands        # copy-paste enable cmds
    bash scripts/check-security-posture.sh --repo <o>/<r> --json | jq '.data[]|select(.state=="off")'
    ```
    
    It prints a per-feature checklist — `✓ on` / `✗ off [severity]` / `— n/a (needs GHAS)` — and, **where a scanner is enabled**, the count + max severity of OPEN alerts (the real exposure, not just the toggle). The alert endpoints degrade gracefully: a `403` (token lacks `security_events`) or `404` (feature off) becomes "n/a — couldn't read", **never a false "0 / secure"**.
    
    **Visibility-aware severity** is the judgment that makes it usable:
    
    - **public** repo → secret scanning, push protection, code scanning are **free** → a gap is a real finding.
    - **private** repo *without* Advanced Security → those three need paid GHAS → reported as a **note (n/a)**, not a nag.
    - Free-on-any-repo (Dependabot alerts/updates, private vuln reporting, SECURITY.md, branch protection) → always a finding when off.
    - Tiers: `critical` (open critical alerts) · `high` (open high alerts; push-protection or Dependabot-alerts off on public/active) · `medium` (secret/code scanning off on public; security-updates off; no branch protection) · `low` (SECURITY.md absent; private vuln reporting off). Full mapping in the script header.
    
    **It never applies a change.** It is strictly read-only (only GET `gh api` calls); the enable commands are **emitted as text** — `gh api -X PUT …` for Dependabot alerts/security-updates/private-vuln-reporting/code-scanning, a `PATCH` body for secret scanning + push protection (push protection requires secret scanning on first), and a pointer to `assets/SECURITY.md.template` for the policy file. **You review and run them yourself**, governed by the same preview discipline as any other repo mutation (hard rule 8 — these change repo settings). `--commands` prints just the enable commands with a `# review before running` banner on stderr.
    
    Exit `0` = posture clean (all applicable features on, no open alerts); `10` = gaps and/or open alerts (a CI/audit step can branch on it); `7` = unavailable (non-github remote, gh unauthed/offline/timeout) — advisory, never a hard failure; `2` usage; `5` gh not installed. Folds into mode `audit` (see the Security Posture checklist there).
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related