Every command takes the form:
npm run compass -- <command> [flags]The -- matters. It tells npm to pass what follows to Compass rather than interpreting it itself. If
you forget it, npm will silently swallow your flags.
There are shortcuts for the three most common:
npm run migrate # same as: npm run compass -- migrate
npm run status # same as: npm run compass -- status
npm run serve # same as: npm run compass -- servenpm run compass -- --help prints a summary at any time.
- Setup
- Fetching data
- Finding work
- Recording decisions
- Corpus reports
- Housekeeping
- The server
- Flags that appear everywhere
Applies any database migrations that have not run yet.
npm run compass -- migrateSafe to run repeatedly — it tracks what has already been applied and skips it. Run this after every
git pull.
Needs DATABASE_URL. Does not need a GitHub token.
All four of these talk to GitHub and spend your hourly request allowance. They stop before exhausting it and resume where they stopped, so running them repeatedly is safe.
They must run in this order the first time, because each depends on the one before:
seed → sync issues → sync metrics → sync setup
Adds a project you care about, and makes it rankable.
npm run compass -- add django/django
npm run compass -- add https://github.com/django/django # a pasted URL works
npm run compass -- add django/django --metadata-only # just the row, no scans| Flag | Meaning |
|---|---|
--metadata-only |
Fetch the repository row only, leaving issues, metrics and setup for later |
By default this also pulls the project's issues, measures its maintainer attention, and reads its setup cost — because adding a project and then being shown nothing would be a strange thing to offer. For one repository that is only a handful of requests.
Two things worth knowing:
- A manually added project is never paused by
prune. Otherwiseprune --dormantwould quietly undo what you just asked for. - Discovery is not required first. Before this command existed, the corpus was whatever the seed
searches happened to find, and
sync repos --repo django/djangomatched nothing and reportedNothing to refresh— which reads like success.
Discovers repositories by running the searches defined in src/seeds/queries.ts, which target a
1,000–30,000 star band.
npm run compass -- seed # everything
npm run compass -- seed --dry-run # show what it would search, write nothing
npm run compass -- seed --only python,rust # only named queries
npm run compass -- seed --max-pages 2 # cap pages per query, to keep it cheap| Flag | Meaning |
|---|---|
--dry-run |
Print the resolved queries and result counts without writing |
--only id,id |
Restrict to specific seed query ids |
--max-pages N |
Override each query's page cap |
Refreshes stars, primary language, topics and default branch for repositories you already have.
npm run compass -- sync repos # everything stale
npm run compass -- sync repos --limit 500
npm run compass -- sync repos --repo facebook/react # one project
npm run compass -- sync repos --stale-hours 0 # force-refresh everything| Flag | Default | Meaning |
|---|---|---|
--stale-hours N |
24 | Skip repos refreshed more recently than this |
--limit N |
all | How many repositories to process |
--repo owner/name |
— | One repository, regardless of staleness |
Cheap: unchanged repositories answer 304 Not Modified and cost no quota at all.
With more than 1,000 repositories, one pass will not cover them. Run it twice.
Pulls open issues. The first run for a repository is a full backfill; after that it fetches only what changed.
npm run compass -- sync issues --limit 100
npm run compass -- sync issues --repo owner/name
npm run compass -- sync issues --backfill-max-pages 5| Flag | Meaning |
|---|---|
--limit N |
How many repositories to process |
--repo owner/name |
One repository only |
--backfill-max-pages N |
Page cap for a repository's first full pull, so one huge tracker cannot eat a whole run |
--incremental-max-pages N |
Page cap for later incremental passes |
The slowest step. 100 issues per request.
The step that makes the ranking meaningful. Reads external pull requests and derives whether maintainers review outside work, how fast, and how often they merge it.
npm run compass -- sync metrics --limit 100
npm run compass -- sync metrics --repo owner/name
npm run compass -- sync metrics --stale-days 0 # recompute everything| Flag | Default | Meaning |
|---|---|---|
--stale-days N |
7 | Skip repos measured more recently. Maintainer behaviour moves slowly, so a week-old metric is still good |
--limit N |
all | How many repositories |
--repo owner/name |
— | One repository |
--window-days N |
180 | How far back to look for external PRs |
--pr-count N |
40 | Pull requests to examine per repository |
--batch-size N |
5 | Repositories per GraphQL request |
--grace-days N |
7 | Open, unanswered PRs older than this count as ignored |
Uses GraphQL, so it is efficient per repository but the queries are large.
Reads each repository's files — compose files, env templates, task runners, CI config, CONTRIBUTING —
and derives a light / moderate / heavy verdict, plus any CLA or DCO requirement.
npm run compass -- sync setup --limit 100
npm run compass -- sync setup --stale-days 0| Flag | Default | Meaning |
|---|---|---|
--stale-days N |
30 | Skip repos read more recently. Layout changes slowly |
--limit N |
all | How many repositories |
--repo owner/name |
— | One repository |
--batch-size N |
3 | Repositories per request. Kept low because this returns file contents |
Reads the whole file tree since migration 008, so a project keeping its compose file in
build/or its env template inconfig/no longer reads as simpler than it is. Root-level facts (Makefile, lockfiles) still come from the root deliberately, so the change did not re-score the corpus for unrelated reasons. A tree GitHub truncated yieldsunknown, never a confident verdict.Also reports a CLA or DCO requirement, found in CONTRIBUTING (root,
.github/ordocs/) and in CLA/DCO bot configuration.noneis only reported when a CONTRIBUTING file was actually read — otherwise the answer is unmeasured, because a confident "no CLA" that walks you into a signature wall is worse than no answer.
Runs repos, issues, metrics, then setup in order.
npm run compass -- sync allConvenient, but it can run for hours. Prefer the individual commands with --limit until you know how
long each takes on your corpus.
The organisation table: which organisations are worth your time, asked before which issue.
Every competitor lists issues carrying a label. None of them measures whether maintainers merge work from outsiders — which is what the three middle columns here are, and the only reason this table can exist.
npm run compass -- orgs
npm run compass -- orgs --gsoc 2026 # only GSoC participants
npm run compass -- orgs --gsoc 2026 --uncovered # …that you have never measured
npm run compass -- orgs --sort candidates --limit 20| Flag | Default | Meaning |
|---|---|---|
--sort attention|candidates|name |
attention |
Ordering. An unknown value is refused, not ignored |
--gsoc YEAR|any |
any | Only organisations tagged as GSoC participants. A four-digit year or any |
--language X |
any | Modal primary language, matched case-insensitively |
--min-repos N |
any | Drop organisations with fewer repositories in the corpus |
--momentum hype|rising|steady|cooling |
any | The organisation's modal momentum verdict |
--uncovered |
off | Only organisations with no repositories in the corpus |
--limit N |
50 | Rows to show |
--offset N |
0 | Rows to skip |
Organisation Maintainers reply? Merge rate Setup Open GSoC
─────────────────────────────────────────────────────────────────────────────────────────────────────────
hog responsive · 9h 86% of 14 1 light 6 2026
acme slow 1/2 · 34h 76% of 37 1 light 1 mod 5 2026
CLA in 1 of 2 repos — resolve before writing code
cern-hsf not in corpus — — 0 2026
Nothing here is a score. The ordering is an ordinal cascade — verdict, then merge rate, then how much work is actually available, then name — so any position can be explained by pointing at a column. A composite number would be a fifth invented measurement and would hide the tradeoff you are here to make.
Read the columns as follows:
- The verdict is modal across an organisation's measured repositories, with the count shown
(
slow 1/2). Ties break toward the worse verdict: being told an organisation replies when half of it does not costs an evening, while the reverse costs a second look. - Median reply is a median of per-repository medians. It describes the typical repository, not the typical pull request.
- Merge rate is pooled, not averaged, and always carries its denominator.
100% of 2and76% of 37are not the same claim. - Setup is a distribution. These are ordinals; averaging them would invent a number. It sums to fewer than the repository count when some have not been read.
not in corpusis a real row. It means the organisation came from a curated list and nothing about it has been measured — which is exactly the list to runaddagainst.
Momentum is growth crossed with the ability to absorb it, and the column that makes this table worth more than a star ranking:
| Verdict | Meaning | What it means for you |
|---|---|---|
hype |
Surging, and the measurements say nobody can review the result | The worst place to spend five hours, and the one every star-ranked list puts at the top |
rising |
Surging, and maintainers are demonstrably reading outside work | The best place to be early: visible project, active mentors, and your pull request lands |
steady |
Growing normally | Most good projects, most of the time |
cooling |
Losing stars, or gaining none across the window | |
— |
Unmeasured, never "not growing" | Needs two star samples a week or more apart |
hype is never reached from growth alone. "This project is popular" is not a criticism, and a verdict
amounting to one would be the tool substituting taste for measurement — so it also requires a measured
capacity concern: dormant or slow replies, a queue of 100+ open pull requests, or a merge rate at or below
40% over at least ten decided pull requests.
Dormant organisations are shown by default, unlike the shortlist, which excludes dormant repositories outright. That is deliberate: "this GSoC organisation has 40 open issues and has not replied to an outsider in 31 days" is the most valuable row this table can produce.
Drill in with shortlist --org <login>.
Tags organisations as GSoC participants for a year, from a hand-checked file.
npm run compass -- gsoc import gsoc-2026.txt --year 2026 --source "official list, read 2026-08-04"
npm run compass -- gsoc import gsoc-2026.txt --year 2026 --source "…" --replace| Flag | Default | Meaning |
|---|---|---|
--year N |
— | Required. The programme year |
--source "…" |
— | Required. Where the list came from |
--replace |
off | Delete that year's existing tags first, after the file validates |
The file is one GitHub login per line. # starts a comment, blank lines are ignored, owner/name
reduces to owner, and duplicates collapse.
# GSoC 2026, mapped by hand from the official list
python # Python Software Foundation
cern-hsf
postgres/postgres
Why a file and not a fetch. The published list carries programme names — "Python Software
Foundation", "CERN-HSF" — which are not GitHub logins. Something has to map one to the other, and no
scraper does that reliably. A human doing it once a year is what the curated provenance class means.
--source is required, not defaulted. A curated value with no provenance is indistinguishable from
a measurement, and it is the kind that goes stale without anyone noticing. reviewed_at is stamped
with today automatically and cannot be omitted.
Two things are refused rather than imported:
- An empty file. A changed page, a failed download, or a wrong path all produce one, and accepting
it would record "no organisation participates in GSoC" — a false finding, which is worse than a gap.
This is the same rule as
nullnever meaning0. - A file where more lines fail to parse than succeed. An HTML dump or a list of programme names would otherwise import the handful of lines that happened to look like logins, producing a plausible, dated, wrong claim.
Organisations not already in the corpus are created as identity-only rows rather than skipped, and the command reports how many. That count is the point of the import:
GSoC 2026: 5 organisation(s) tagged.
3 organisation(s) were new to the corpus and now exist as rows.
3 of them have no repositories in your corpus, so nothing about them is measured yet.
Ingests a ROSS Index dataset: which organisations are growing fastest, and who funds them.
npm run compass -- ross import ross-2026q1.csv --quarter 2026Q1 --source "RunaCapital/ROSS-Index, read 2026-08-04"| Flag | Default | Meaning |
|---|---|---|
--quarter Q |
— | Required. The dataset's quarter, e.g. 2026Q1 |
--source "…" |
— | Required. Where the dataset came from |
Runa Capital publishes the index as datasets in a git repository (RunaCapital/ROSS-Index), already
joined: organisation, owner/repo, stars, growth multiple, founding year, location, and funding including
YC. No scraping, no terms-of-service question, no brittle selectors. See
fixtures/ross-index.example.csv for the shape.
Columns are matched by name, not by position. A positional parser would read growth multiples as star counts the first time somebody inserted a column, and nothing in the output would look wrong. The command prints which column it read for each field so a surprising import can be explained rather than re-guessed.
Growth is deliberately not imported. Compass measures that itself from repo_stars_history, and
storing somebody else's growth figure beside its own would create two numbers for one question with no way
to tell which you were looking at. What the import contributes is the two things the corpus cannot derive:
funding, and who is on the list at all — a discovery feed rather than a signal.
Everything written is curated: somebody else's numbers, on their date, stored with a reviewed_at
saying so. Refusals match gsoc import — an empty dataset, a file with no recognisable
owner column, or one where more rows fail than parse.
Repositories named by the dataset that are absent from the corpus are reported as add commands. Until
they are added, nothing about them is measured.
The ranked list, with the evidence for each row.
npm run compass -- shortlist
npm run compass -- shortlist --min-score 0 # include weak candidates
npm run compass -- shortlist --language Python --max-setup light
npm run compass -- shortlist --labelled --min-stars 1000 --max-stars 30000
npm run compass -- shortlist --per-repo 5 --limit 40| Flag | Default | Meaning |
|---|---|---|
--limit N |
20 | Rows to show |
--min-score N |
20 | Score threshold. Can be 0 or negative |
--per-repo N |
2 | Most rows from any one repository |
--stack X |
any | What the project is built with: react, django, js. See below |
--language X |
any | Primary language, matched exactly (case-insensitively). The strict form |
--labelled |
off | Only issues carrying an invitation label |
--max-setup light|moderate |
any | Setup ceiling |
--min-stars N |
any | Star floor |
--max-stars N |
any | Star ceiling |
--include-dormant |
off | Include projects where nobody answers outside PRs |
--org login |
any | One organisation. The drill-down from orgs |
--exclude-claimed |
off | Drop issues a claim check found taken. Unchecked issues stay in |
--momentum hype|rising|steady|cooling |
any | Growth crossed with review capacity. Excludes repositories whose velocity is unmeasured |
--weights career-leverage |
default | Score against a named weight set for this run only, without changing the saved profile |
--stack matches evidence, not names. It reads declared dependencies (package.json,
pyproject.toml, requirements.txt, go.mod, Cargo.toml, pom.xml) plus GitHub topics. A repository
called awesome-react-tips is not a React project and will not match; one that declares react as a
dependency will, whatever it is called.
npm run compass -- shortlist --stack react
npm run compass -- shortlist --stack django
npm run compass -- shortlist --stack js # JavaScript *and* TypeScript projects--stack js and --stack javascript both include TypeScript, because someone looking for JavaScript
work will nearly always take a TypeScript project. --stack ts stays narrow, because the implication
only runs one way. When you want strictly one language, --language JavaScript is exact.
An unrecognised term matches nothing rather than everything. /api/stacks lists what your corpus
actually contains, and the web interface offers it as a dropdown.
Frameworks come from the setup scan. Until
sync setuphas run,--stack reactwill find only projects carrying a matching GitHub topic.
Why --per-repo exists. Repository-level signals dominate the score, so without a cap one good
project takes over the list. On a real run, twelve of the top twenty came from the same repository, all
on an identical score.
Assigned, locked, and already-judged issues are excluded outright — they are someone else's work, not a weak option.
The full itemised breakdown for one issue.
npm run compass -- why facebook/react#12345Output is split into "the project" and "this issue", each line carrying the raw value behind it, then a list of anything that could not be measured.
Works on issues the shortlist rejected, which is most of the reason to use it. If it says the issue is not a current candidate, it is closed, assigned, already judged, or in a repository you have not synced.
This is the part that turns the tool from a guess into a measurement. Skipping it is why the weights are still unvalidated.
Is this issue actually free? Reads the comment thread and gives a dated verdict.
npm run compass -- claims acme/widgets#412
npm run compass -- claims acme/widgets#412 --cached # reuse an earlier check| Flag | Default | Meaning |
|---|---|---|
--cached |
off | Return an earlier verdict instead of fetching |
The problem it solves: a good first issue with 23 comments is usually twenty people asking "can I work
on this?" and one person three days in without an assignment. GitHub's assignee field is empty in every
one of those cases, so the shortlist — which correctly excludes assigned issues — treats the whole pile
as free work. It is the largest remaining way this tool can waste an evening.
| Verdict | Meaning | What to do |
|---|---|---|
in-progress |
Somebody reported actual work, or a pull request is linked | Skip it. A second pull request helps nobody |
contested |
Several people asked and nobody was assigned | The evening-waster. Unless you want to race, go elsewhere |
claimed |
One recent request, nobody assigned | Comment before you start |
stale-claim |
A request went quiet for longer than an intention survives, about a fortnight | Probably yours. Say so in the thread |
free |
Nobody asked | Nothing in the thread suggests anyone else is on it |
A verdict is true as of the moment it was made, and is always printed with its age and its coverage.
free from three weeks ago, read from 100 comments of a 412-comment thread, is a much weaker claim than
a fresh check that read everything — and only the age tells you which you have.
Costs one request, and only when the issue has comments; a thread with none is answered from the
corpus without touching the network. Results are cached, so a later shortlist shows what you already
know for nothing, and --exclude-claimed can act on it.
What is deliberately not a claim. Each of these appears constantly in issue threads and each would match otherwise:
- "Is anyone working on this?" — a question about other people's claims
- "@ada can you take this one?" — delegation, usually from a maintainer
- "Are you still working on this?" — a maintainer chasing a stale claim
- "I'll take a look" — looking is not doing
- "You can use /assign me to claim issues here" — instructions, not intent
A claim must be first-person and volitional. "I would like to work on this if nobody else is" is a
claim, deliberately: that person is volunteering, and treating it as a question would produce a false
free, which is the expensive direction to be wrong in.
Nothing here changes the score. See How ranking works.
npm run compass -- decide owner/name#123 started --hours 4
npm run compass -- decide owner/name#123 merged --actual-hours 9
npm run compass -- decide owner/name#456 rejected --reason "needs a design discussion first"Verdicts:
| Verdict | Means |
|---|---|
shortlisted |
Worth doing, not started |
started |
Working on it now |
submitted |
Pull request opened, waiting on review |
merged |
Merged |
stalled |
Open but going nowhere |
abandoned |
Started and dropped |
closed_unmerged |
Closed without merging |
rejected |
Not worth doing |
| Flag | Meaning |
|---|---|
--hours N |
How long you expect it to take. Record when you start |
--actual-hours N |
How long it did take. Record when you finish |
--reason "..." |
Free text. This is the part you will reread |
Any verdict removes the issue from future shortlists. Record several over time for the same issue —
they accumulate into a trail, and --hours plus a later --actual-hours form the pair the calibration
figure is built from.
What you decided, and how your estimates compared to reality.
npm run compass -- journal
npm run compass -- journal --limit 10The average estimate error appears only once three issues have both a prediction and an outcome. Below that it says how many you have. This is deliberate: an average over one or two ratios is exactly the false precision the tool refuses everywhere else.
These have no web equivalent yet. They exist to interrogate the measurements rather than the ranking.
Repositories sorted by how they treat outside contributors.
npm run compass -- maintainers --sort median --limit 20
npm run compass -- maintainers --bucket responsive --min-prs 10| Flag | Meaning |
|---|---|
--sort median|ignored|stale|reviewed|merge |
Sort key |
--limit N |
Rows |
--min-prs N |
Only repositories with at least this many external PRs measured |
--bucket dormant|slow|moderate|responsive |
One responsiveness bucket |
The per-pull-request evidence behind one repository's metrics. Use this when a number looks wrong.
npm run compass -- explain facebook/reactWho actually answers external pull requests. Exposes bot first-responders — an account with dozens of responses at a near-zero median is automation, and counting it as maintainer attention both flatters the median and hides the true ignore rate.
npm run compass -- responders --limit 30
npm run compass -- responders --repo owner/nameWhen you find one, add it to COMPASS_IGNORE_LOGINS — see Configuration.
Repositories sorted by what it costs to get them running.
npm run compass -- setup --sort weight --limit 20
npm run compass -- setup --weight heavy --max-services 3| Flag | Meaning |
|---|---|
--sort weight|services|env|runtime |
Sort key |
--weight light|moderate|heavy |
One bucket |
--max-services N |
Cap on compose service count |
--limit N |
Rows |
Corpus counts, recent runs, and how much of your GitHub allowance they used.
npm run statusThe first thing to run when something looks wrong.
Pauses repositories not worth syncing issues from, so later runs spend their budget on projects that might actually produce a candidate. Dry run by default.
npm run compass -- prune --dormant # show what would be paused
npm run compass -- prune --dormant --apply # actually pause them
npm run compass -- prune --heavy --min-confidence high --apply
npm run compass -- prune --unpause --apply # undo everything| Flag | Meaning |
|---|---|
--dormant |
Repositories where nobody answers external PRs |
--heavy |
Repositories with heavy setup cost |
--min-confidence medium|high |
Only act where the measurement has a real sample behind it |
--apply |
Actually make the change. Without this, nothing is written |
--unpause |
Restore every paused repository to active |
Reversible by design: pausing sets a flag, it does not delete anything.
Starts the JSON API and serves the web interface.
npm run serve
npm run compass -- serve --port 3000
npm run compass -- serve --host 0.0.0.0 # think first, see below| Flag | Default | Meaning |
|---|---|---|
--port N |
8787 | Port. Also settable via COMPASS_PORT |
--host X |
127.0.0.1 | Interface. Also settable via COMPASS_HOST |
Localhost by default, on purpose. There is no authentication, and
POST /api/decisionswrites to your database. Binding0.0.0.0puts an unauthenticated write endpoint on your network.
The web interface is served only if it has been built. npm run web:build does that, and npm start
does both in one step.
| Flag | Applies to | Meaning |
|---|---|---|
--limit N |
all sync and report commands | How much to process or show |
--repo owner/name |
all sync commands, explain, responders |
One repository |
--help, -h |
anywhere | Print the command summary |
Two conventions worth knowing:
--limitmust be positive. A limit of zero is meaningless, so it is rejected rather than silently treated as "no limit".- Staleness flags accept zero, meaning "refresh everything now" —
--stale-days 0is a legitimate and useful instruction.
Bad values are refused with a message rather than coerced. --limit banana is an error, not a
silently-ignored flag.