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
Install
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/github-ops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
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
- Visibility defaults to private. Pass
--privatetogh repo createunless the user has explicitly said "public" / "make it public" for this specific repo. Seereferences/repo-visibility.md. - 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. Seereferences/release-strategy.md. - Always run
push-gatebefore any push to a remote. No exceptions. If push-gate refuses, do not proceed — fix the cause and re-run. - 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. - 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.mdfor the canonical claude-mods style. - 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.
- 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. - 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.mdfor 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:
gh pr view <n> --json mergeable,mergeStateStatus→mergeable: MERGEABLE,mergeStateStatus: CLEANgh pr checks <n>→ every check passed (or explicitly ignored with user approval)gh pr diff <n>reviewed — confirm no surprise scope, no committed secrets/local paths, no stale PR-body claims- Merge strategy picked — default squash for fix/feature branches with multiple WIP commits;
--mergeonly when individual commits matter;--rebasefor linear-history repos. Ask if uncertain. - 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 workflowoperations - Secrets —
gh secret set/list/delete(with secure handling) - Branch protection —
gh apicalls 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://github.com/OWNER/REPO/releases) [](https://github.com/OWNER/REPO/actions/workflows/ci.yml) [](https://www.python.org) [](#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  ``` 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.
Reviews (0)
No reviews yet.
No comments yet.