The rules for changing this codebase — for people and for coding assistants (Claude Code, Codex, and anything else that reads an AGENTS.md). Every convention lives here exactly once; other files point at this one rather than repeating it.
GH Reloaded is a Manifest V3 Chrome extension that injects small, focused UI enhancements into github.com. It’s a collection of independent features, not a single-purpose tool: each one lives in its own folder and can be added, removed, or shipped on its own.
Layout:
manifest.json MV3 manifest — wires content scripts and the background service worker
background.js Service worker entry point: opens Settings, routes messages to feature modules
background/<feature>.js One feature's server-side logic (GitHub calls, computation) + its MESSAGE_TYPE
features/<feature>/ One feature's content-script side: DOM injection, rendering, styling
lib/github-api.js Shared GitHub REST/GraphQL client (auth, caching) used by every feature
options/ Settings page (GitHub tokens, quick-create shortcuts, feature toggles)
icons/ Toolbar/extensions-page icons
screenshots/ SVG mockups illustrating each feature's layout and colors
Every feature is two loosely-coupled halves.
features/<name>/ — the content-script side. Runs in the page’s isolated world on whichever github.com pages it targets. Typically:
content.js — finds where on the page to hook in, injects DOM, renders, re-runs on GitHub’s SPA navigations;layout.js, a content.css, etc.It’s a self-contained IIFE, not an ES module — content scripts don’t support import. If it needs data from GitHub, it messages the background rather than fetching directly.
background/<name>.js — the feature’s server-side logic, if it has any. An ES module (the service worker runs with "type": "module") that exports:
export const MESSAGE_TYPE = "MY_FEATURE_FETCH_X"; // unique across features
export async function handleMessage(payload) {
// ... talk to GitHub via lib/github-api.js, compute whatever ...
return { someKey: result }; // spread into the response the content script gets
}
Import it in background.js and add it to the FEATURES array — the generic message router there takes care of the rest (dispatch by MESSAGE_TYPE, token-presence flag, error shape). A feature with no background logic just skips this half.
lib/github-api.js is shared: auth, request caching, and generic REST/GraphQL helpers. Add a genuinely reusable helper there (not feature-specific parsing) if more than one feature needs it.
manifest.json: add your content script’s files to a content_scripts entry — reuse the existing one if your matches overlap with another feature’s, add a new entry otherwise.
The Chrome Web Store caps the manifest description at 132 characters and the name at 75, and version must be one to four dot-separated integers. Every file the manifest references must also be in the release zip (the zip -r step of release.yml). test/unit/manifest.test.js enforces all of this in npm test.
state, not a guess at what a board’s terminal Status column is named — every board can call it whatever it wants.data-testid.BlobContent-module__blobContentSection became blobContentSection__<hash> and index-module__topBarActions became topBarActions__<hash>. Match the stable middle of the class ([class*="blobContentSection"]), or better a role, an ARIA attribute or visible text. Keep fixtures on real class names and cover both the old and the new naming in a test.| Map the sibling routes and views. A control added to a page also exists on its neighbours (blob / blame / raw, Conversation / Files changed, table / board views). Decide what it does on each, keep a toggle set complete on all of them (Preview | Code | Blame everywhere), and test A → B → A plus browser back and forward. |
0 12px padding; a coloured button darkens on hover (never brightens) and shows a 2px #0969da focus ring at offset -2px. Inherit the font, and add no margin to something in a flex row: the parent’s gap already spaces it and a margin doubles it. Assert size, radius and the gaps before and after in an e2e test against a fixture whose layout mirrors the live one.grep -rn "module__" features/) and fix the other occurrences in the same pull request when they are small, or list them under “Follow-ups”.The Settings page (options/) is an application settings UI: a compact app shell, not the product’s marketing website and not a clone of GitHub’s own settings.
prefers-color-scheme. A restrained orange accent for the active item, switches and focus only; no hero, glow or gradient; system UI font for controls. Compact density, no horizontal scroll, visible focus rings, accessible labels and native keyboard operation.test/e2e/options-*.spec.js specs, and make the source of defaults and help text clear.When a feature is added or its behavior changes, in the same pass (not as a follow-up):
README.md’s “Features” section — it is the source of truth for what each feature does and what permissions it needs.screenshots/ when the change is visible (a new UI element, a moved or restyled control, a different layout) — a stale screenshot is worse than none, since it actively misleads. The screenshots are SVG mockups illustrating layout and colors, not literal captures; regenerate them in the same style rather than trying to capture a real page.PRIVACY.md when the change alters what data is stored or which permissions the extension requests.Write one paragraph per line: no line break in the middle of a sentence or paragraph. Keep line breaks only between paragraphs, between list items, around headings, and inside code blocks. Long lines are expected — let the editor soft-wrap them rather than hard-wrapping at a column. This keeps diffs to a paragraph readable as a single changed line.
A change is ready to hand off when all of these hold:
npm test runs the unit and e2e suites (see Running the tests).README.md’s “Features” section is updated when a feature’s behavior changes.screenshots/ mockup is refreshed when the change is visible.PRIVACY.md is updated when stored data or requested permissions change.<type>/<issue-number>-<short-slug>, e.g. docs/20-agents-md or fix/16-html-preview-race. Types: feat, fix, docs, chore, test, ci, refactor. Recommended — some branches are named by the tool that created them; a descriptive name is preferred where you control it.docs: consolidate contributor conventions in AGENTS.md. A title check (pr-title.yml) enforces this on every PR. PRs are squash-merged, so the title becomes the commit on main — and that commit is what drives the next version.Fixes cattanisimone/gh-reloaded#<N> so merging the PR closes the issue.manifest.json’s version in a regular PR. Release Please owns it: it reads the conventional commits merged to main and keeps a single release PR open that bumps the version and updates CHANGELOG.md. Merging that release PR cuts the vX.Y.Z tag, the GitHub Release, and the Chrome Web Store upload. See the README’s Releasing section and the guide below.The PR title’s type decides the next version, so pick it by what a user notices after updating:
feat: something new to see or use (a feature, an option, visible behavior) → minor.fix, perf: a user-visible defect gone, or noticeably faster → patch.feat!: or fix!: plus a BREAKING CHANGE: line in the PR body): a feature or option removed or renamed, saved settings that stop working, or a new permission the user must accept → major, or minor while the version is below 1.0.refactor, test, docs, ci, chore, build, style, revert: nothing changes for users, or the revert waits for the next release → no release on its own. The changelog hides them, so a release made only of these opens no release PR. A PR that mixes types takes the highest one.The changelog lists Features, Bug Fixes and Performance. The rules are in release-please-config.json and test/unit/release-please-config.test.js checks them.
The Chrome Web Store accepts versions made of up to four dot-separated integers that always increase, with no pre-release suffix such as -beta.
No build step. chrome://extensions → Developer mode → Load unpacked → select the repo folder → reload the extension (and the GitHub page) after each change.
The test harness lives under test/ and is dev-only — it is deliberately outside the paths the release workflow zips (manifest.json, background.js, background, features, icons, lib, options), so package.json, the fixtures, and the tests never ship in the extension. There is no bundler: the extension itself still loads unpacked, exactly as above.
One command runs everything, with no GitHub token:
npm install
npx playwright install --with-deps chromium # first time only — the e2e browser
npm test
npm test runs two suites, and the same command runs in CI (.github/workflows/ci.yml) on every pull request:
npm run test:unit) — Node’s built-in test runner over the pure logic: the effort-weighted critical path in background/dependency-graph.js, per-owner token resolution and response caching in lib/github-api.js, the path/URL helpers in background/html-preview.js, and the standup-mode attention rules (stale, orphan block, critical path at risk, bottleneck) in background/standup-mode.js. test/unit/manifest.test.js checks the manifest against the Chrome Web Store limits and the release zip contents. These import the real modules; a small chrome stub (test/support/chrome-stub.js) stands in for the extension storage API they touch at load. The only runtime source change this harness required was adding export to those already-existing pure functions so tests can import them — no behavior change.npm run test:e2e) — Playwright loads the extension unpacked into headless Chromium and drives it against saved fixtures of the GitHub pages each feature targets (test/e2e/fixtures/, see its README for how to refresh them). Every github.com page is served from a fixture and every api.github.com call is mocked at the extension’s boundary, so the suite is deterministic and offline. Each feature has a main-path test and a disabled-state test.The e2e suite needs the full Chromium build (Playwright’s channel: "chromium") — only it loads extensions headless; the default bundled headless shell can’t.