GUESTBOOK.EXEcontact.exe
OSSQ

</ossq>

OSSQ

Ranks twelve OSS repos' issues into a 10-item queue. Cannot write to GitHub

594 → 10

issues considered, then queued — one scan of twelve repos, 2026-08-15

63%

of GitHub's issues endpoint was pull requests — 1,430 of 2,287 fetched

76 → 0

API requests for a cold scan, then the same scan six minutes later

81% → 48%

of the pool a scoring signal was awarding full marks to, before and after regrading

SPECIFICATIONS

ROLESOLO BUILD
YEAR2026
TYPECLI TOOL
STATUSPUBLIC REPO · MIT
STACKtypescript · node · cli · github-api · vitest · commander · launchd · ccline
LINKS[github ↗]
AVAILABILITYsource on GitHub

Through 2025 and 2026 maintainers were buried in AI-generated contributions and answered with policy.

OSSQ turns twelve open-source repositories into a ten-item weekly shortlist of issues worth a human's afternoon, each one printed beside that repo's own contribution rules.

README.TXT: OSSQ (13 KB)[full readme →]

== WHAT IS THIS ==

────────────────────────────────────────

OSSQ turns twelve open-source repositories into a ten-item weekly shortlist of issues worth a human's afternoon, each one printed beside that repo's own contribution rules. It has never opened a pull request, filed an issue or posted a comment — every network call in the codebase leaves through one guard module that throws on any HTTP method other than GET or HEAD and any host other than GitHub, and CI holds that line as its own named step. The scan of 2026-08-15 read 2,287 items across the watchlist, discarded 1,693 of them (1,430 were pull requests wearing an issue's URL) and queued ten. Public and MIT at github.com/br9704/oss-contributions, with the raw evidence for every number committed next to the code.

== </the problem> ==

────────────────────────────────────────

Through 2025 and 2026 maintainers were buried in AI-generated contributions and answered with policy. curl ended its bug bounty after fabricated reports; QEMU, NetBSD, Gentoo and Zig banned AI contributions outright; modelcontextprotocol/python-sdk's CONTRIBUTING now says it in as many words — 'If your agent is auto-filing PRs against our open issues, stop.' So an agent that opens pull requests is the exact pattern being banned, and building one would be building the problem. The question worth answering is the one immediately before it: across the handful of repositories you actually know something about, which of several hundred open issues are worth a human's afternoon, and what are that repo's rules before you touch it?

role & context

Solo build — the policy research, the engine and the docs. The scoring is tuned to one contributor's shipped experience, which is why MCP protocol work ranks highest and also why this is not a general-purpose issue recommender.

== </my approach> ==

────────────────────────────────────────

OSSQ reads, ranks, and stops. It walks a twelve-repo watchlist, drops everything that is a pull request, already assigned, contested by an open PR or carrying a label the repo's own policy says is off-limits, scores what survives against eight signals, and writes CANDIDATES.md with the arithmetic shown for every entry. A human reads that file and decides. The boundary is enforced in a module rather than a promise: nothing in the codebase may call fetch or spawn `gh` except through src/guard.ts, which forces the method, restricts the host, counts every call so a test can assert a code path touched the network zero times, and refuses `gh api` with field flags unless `-X GET` is explicit. Each repo's rules are typed, validated data — lane, PR policy, claiming-comment rule, disclosure requirement, auto-close triggers, source URL and the date a human last read it — and the published playbook is generated from that file so the docs cannot promise something the scanner is not doing.

== </the story> ==

────────────────────────────────────────

The brief asked for an agent that would land one or two open-source contributions a month, starting with modelcontextprotocol/inspector, and the first sprint killed that plan by reading the CONTRIBUTING files it named. inspector accepts issues, not pull requests: design and implementation are done by the maintainers, and the rule applies even to org members with write access — if you have already built a fix locally, they want the prompt you used, not the diff. vercel/ai says the most helpful contribution is often a high-quality issue and credits the issue author with co-authorship on the eventual fix. PrefectHQ/fastmcp heads a section 'The best contribution is a great issue' and names claiming comments as the most common noise it receives. Four of the twelve policies came back materially stricter than the brief assumed.

That reframed the whole project. The thing being built was not a contributor; it was the step before one — a triage engine whose output is a file a person reads. And once the deliverable is a file, the most important property is that it stays a file. Through 2025 and 2026 maintainers were being buried in automated contributions and responded with outright bans, so an agent with write access to GitHub is the exact shape of the problem. The write path was therefore closed in code before the scanner was written: one module every request routes through, methods forced, hosts restricted, `gh` on a read-only allowlist that knows `gh api -f` is secretly a POST.

Everything after that was the difference between what the documentation says and what the API does. GitHub's issues endpoint returns pull requests as issues — 1,430 of 2,287 items in a full run — so the first implementation's one-page-per-repo fetch produced three usable candidates from vercel/ai; paginating toward a target of real issues took the run from 332 to 604. Freshness had been computed from when the queue first saw an issue, which made a five-month-old bug read as surfaced this week. The documented zero-auth atom fallback is dead, 406 on every repo tested. Then the scorer was pointed at itself and two of its eight signals turned out to award almost everybody the same points, including the heaviest one in the system, because its keyword list contained the words 'tool', 'prompt' and 'resource' inside a set of MCP repositories.

It ends where it was always going to end. The engine picked modelcontextprotocol/typescript-sdk#2619, that project's CONTRIBUTING exempts straightforward bug fixes with tests from the issue-first step, the bug was reproduced from source and fixed with tests written failing first, and their client suite went from 785 to 794 passing with no regressions. The patch sits in a gitignored drafts folder. Submitting it is a person's job, and the guard makes it not a choice.

== </architecture> ==

────────────────────────────────────────

A single pass, one direction. `ossq scan` loads watchlist.json through a typed schema that validates each repo's policy and checks how old its verified-on date is, then paginates each repo's issues toward a target of non-PR items, dropping anything assigned, contested by a confirmed open PR, or labelled in a way that repo's own policy says gets a contribution auto-closed. What survives is normalised into one candidate shape, scored by eight signals, then capped — ten total, three slots reserved for the issue lane, no more than two per repo — and written out as CANDIDATES.md plus a run artifact and a one-line ledger entry.

Every network call leaves through src/guard.ts, which asserts the method and host, counts the call, and forces GET regardless of what the caller passed. Behind it sits a governor that paces core requests, backs off, and reserves the last hundred units of budget, and an on-disk ETag cache where a corrupt entry is treated as a miss rather than an error. Core REST is the primary path with the search API as a narrow supplement that degrades instead of throwing, an inversion that came out of measurement: search tripped a secondary limit after about three calls while core sat at 4,997 of 5,000.

The two side surfaces read the stored queue and nothing else. `ossq queue` renders it in the terminal; segment/ccline.mjs renders it as a ccline statusline segment that reads one local file, never touches the network in its render path, and is asserted fail-soft in CI — garbage on stdin must exit 0 with an empty stderr. PLAYBOOK.md is generated from the same watchlist the scanner filters on, and CI regenerates it and fails on any diff.

== </key features> ==

────────────────────────────────────────

A guard module, not a good intention

Every request routes through src/guard.ts: non-GET/HEAD methods throw, non-GitHub hosts throw, `gh` runs from a read-only allowlist, and `gh api <path> -f k=v` is refused because field flags silently make it a POST. The file is public and CI runs its suite as its own named step.

Two lanes, because the repos are not the same

Candidates are routed to a lane where a merged PR is reachable, or to one where the contribution is the issue — because inspector accepts issues only and opens its own PRs, and vercel/ai credits issue authors with co-authorship on the fix. Three of the ten queue slots are reserved for the issue lane so the higher-scoring PR lane cannot crowd it out.

Policy as data that writes its own playbook

watchlist.json carries each repo's lane, PR policy, claiming rules, disclosure requirement, auto-close triggers, a source URL and a verified-on date. PLAYBOOK.md is generated from it, and CI regenerates and diffs it, so published rules cannot drift from enforced ones.

Scores that show their working

Eight independently testable signals, each carrying the sentence that explains its own points, and every candidate prints the arithmetic that produced its number. The spread is measured across the whole pool rather than the queue, because at the top of the queue every signal looks saturated and a dead one is invisible.

A weekly scan that costs almost nothing

ETags on disk turn a repeat scan into 304s that consume no rate-limit budget: 76 requests cold, zero warm. It ships a launchd plist and an idempotent installer for a Monday-morning run, plus an optional ccline statusline segment that reads one local file and never touches the network.

Drift detection that has already fired

The 2026-08-15 scan reported that modelcontextprotocol/python-sdk had changed its CONTRIBUTING since the previous day (SHA b0fb9fa → 0925132) and printed a re-read warning above the queue. It does not try to interpret the change — it says the ground moved and names the repo.

== </key decisions> ==

────────────────────────────────────────

DECISION 01

The never-write rule is a module, not an instruction. src/guard.ts is the single point every request passes through: non-GET/HEAD methods throw, non-GitHub hosts throw, and `gh` runs only from a read-only allowlist that refuses `gh api` with field flags unless `-X GET` is explicit, because `-f` silently turns the call into a POST. An instruction in a prompt can be forgotten or overridden halfway through a run; a function that throws cannot. CI runs the guard's suite as its own named step on the stated grounds that failing it is a design failure rather than a test failure.

DECISION 02

Two lanes instead of one pull-request funnel. For several of these repositories the highest-value contribution in 2026 is not a patch — inspector accepts issues only and opens its own PRs, vercel/ai credits issue authors with co-authorship on the fix. A single funnel would have aimed every candidate at a door that is closed, which is both useless and the fastest way to get a contributor blocked.

DECISION 03

Policy is validated data that generates its own documentation. PLAYBOOK.md is generated from watchlist.json and CI regenerates and diffs it, so the published rules cannot drift from what the scanner enforces. Getting a repo's rules wrong is what gets a contribution closed on sight, and a playbook that promises a rule the scanner quietly stopped applying looks correct right up to that moment.

DECISION 04

Core REST with conditional requests first, the search API last. Measured rather than assumed: search tripped a secondary rate limit after roughly three calls spaced four seconds apart while core REST sat at 4,997 of 5,000. Search became a throttled supplement that returns `degraded: true` instead of throwing, so hitting a search limit can never take down a scan.

DECISION 05

An unknown linked-PR status stays unknown. Assuming 'nothing references this issue' would promote a contested issue to the top of the queue and send a human to duplicate someone else's open work in front of the maintainers — the worst failure available to this tool. Only a confirmed open PR causes a drop, and the resolution is bounded by an explicit budget flag.

DECISION 06

Cap the queue at ten, reserve three slots for the issue lane, at most two per repo. Volume is the spam signal the whole project exists to avoid. PR-lane candidates score higher by construction, so without a floor the scoring would have quietly undone the two-lane decision; and the first live queue was seven candidates from one repository, because most signals are repo-level and ties broke alphabetically.

DECISION 07

Budget is read from response headers, never from /rate_limit. The endpoint reported 4,810 remaining and did not move across a dozen real requests while the `x-ratelimit-*` headers on those same responses decremented correctly. A governor that trusts a lagging view is a governor that walks into a 403 believing it has thousands of requests left.

== </what i learned> ==

────────────────────────────────────────
>

Read the target's rules before designing the funnel. The brief's entry repo turned out to accept no outside pull requests at all, which invalidated the success metric on day one — and no amount of clean architecture would have surfaced that.

>

Unit tests that parse a fixture cannot tell you the endpoint is gone. The atom fallback was green the entire time and dead in production; only a run against live GitHub found the 406.

>

A signal that awards nearly everyone the same points is a constant with a weight attached, and you cannot see it from the top of the queue — the top ten score high by construction, so the measurement has to run over the whole pool.

>

GitHub's /rate_limit endpoint is not where the rate limit lives; the response headers are. Trusting the endpoint meant the budget check was comparing a scan against a stale snapshot of itself.

>

The safest boundary is the one the code cannot cross. Writing 'never open a PR' into an instruction file is a hope; making the write path throw is a property, and it is the only version a maintainer has any reason to believe.

next:

Track outcomes. Nothing currently tells the ranking it is wrong: no queued candidate has been submitted, so a score of 125 and a score of 77 have exactly the same evidence behind them. Recording what happens to each candidate — submitted, ignored, closed, shipped by a maintainer — and feeding that back into the eight signals is the only change that would turn the heuristic into something measurable. The prepared typescript-sdk#2619 fix is the first real test of it, and submitting that is a person's job.

typescript · node · cli · github-api · vitest · commander · launchd · ccline

BUILD.TXT: OSSQ (6 KB)[the build record →]

== </what the audit found> ==

────────────────────────────────────────

The audit started with the target repositories rather than with the code, and the first thing it broke was the brief's own locked decision. modelcontextprotocol/inspector — named as the entry repo, with merged pull requests as the goal — accepts no pull requests from anyone outside the maintainer team, explicitly including org members who happen to have write access. Two more of the five named targets say outright that a high-quality issue beats a patch. Reading twelve live CONTRIBUTING files also turned up one that spells its label `good-first-issue` with hyphens, so the obvious query for `good first issue` silently finds nothing there. The second half of the audit was pointing the finished scanner at live GitHub, where three confident assumptions died in one run: the REST issues endpoint hands back pull requests as issues, freshness was being computed from when the queue first saw an issue rather than when it was opened, and the documented zero-auth `issues.atom` fallback returns HTTP 406 on every repo tested while `releases.atom` on the same repos still returns 200. The unit tests passed the whole time, because they parse a synthetic fixture.

== </what was repaired> ==

────────────────────────────────────────

Freshness now measures from `createdAt` alone; computing it from `max(firstSeenAt, createdAt)` had made the first run describe a five-month-old issue as surfaced within the last week. One page per repo produced three candidates from vercel/ai, so pagination now walks toward a target count of non-PR issues — that change alone took a full run from 332 to 604 candidates. The first live queue was seven candidates from a single repo, because most signals are repo-level and ties broke alphabetically, so a cap of two per repo went in. The rate-limit measurement was reading a 304 replay of its own earlier response, comparing the budget against a cached copy of itself. PLAYBOOK.md generation was stamping staleness marks into the file it generated, which made the drift test calendar-sensitive instead of drift-sensitive. `RepoScan.error` was declared and never populated, so a repo whose fetch failed reported zero kept — indistinguishable from a repo with nothing worth keeping. The last defect surfaced on the first public push: CI asserts the ccline statusline segment is fail-soft, and it failed immediately, because a static import of a peer dependency that exists only as a local symlink is an uncatchable resolution failure that exits 1 with a stack trace. The segment had only ever been fail-soft on the one machine where its dependency was installed.

== </what was measured> ==

────────────────────────────────────────

Every figure in the project came from an instrument rather than an estimate, and the instrument kept disagreeing with the documentation. GitHub's issues endpoint returned 1,430 pull requests among 2,287 fetched items — 63% of the output is noise. A cold scan of the twelve-repo watchlist costs 76 core requests and takes 58 seconds; the identical scan repeated six minutes later cost nothing, because every response came back 304 Not Modified, which is what makes a weekly re-scan effectively free. `search/issues` tripped a secondary rate limit after roughly three calls spaced four seconds apart while core REST sat at 4,997 of 5,000 — that measurement inverted the architecture to core-first. GitHub's own `/rate_limit` endpoint turned out to be a lagging view: it reported 4,810 remaining and did not move across a dozen real requests while the `x-ratelimit-*` headers on those same responses decremented correctly, so budget is read from headers. Then the scorer was measured against its own pool and two of its eight signals turned out to be constants wearing a weight: 'few comments' awarded full marks to 81% of candidates, and the MCP keyword list contained 'tool', 'prompt' and 'resource' — words in nearly every issue in an MCP repo — so the heaviest signal in the system fired almost universally. Regrading the first took it to 48%; replacing the second with wire-protocol vocabulary (`tools/call`, `_meta`, `streamable http`, `jsonrpc`) changed what leads the queue from 'Log exceptions in tool calls' to actual protocol bugs. Neither dead signal is visible from the top ten, where everything scores high by construction, which is why the measurement script runs over the whole pool.

== </decisions> ==

────────────────────────────────────────

DECISION 01

The never-write rule is a module, not an instruction

because src/guard.ts is a choke point every request passes through: non-GET/HEAD methods throw, non-GitHub hosts throw, and `gh` runs from a read-only allowlist that refuses `gh api -f` because field flags silently turn a GET into a POST. An instruction in a prompt can be forgotten or overridden mid-run; a function that throws cannot. CI runs the guard suite as its own named step on the stated grounds that failing it is a design failure, not a test failure.

DECISION 02

Two lanes instead of one pull-request funnel

because For several of these repos the highest-value contribution in 2026 is not a patch. inspector accepts issues only and opens its own PRs; vercel/ai says the most helpful contribution is often a high-quality issue and credits the author with co-authorship on the resulting fix. A single funnel would have aimed every candidate at a door that is closed.

DECISION 03

Policy is validated data that generates its own documentation

because PLAYBOOK.md is generated from watchlist.json, and CI regenerates it and fails on any diff, so the published rules cannot drift from what the scanner actually enforces. Getting a repo's rules wrong is the thing that gets a contribution closed on sight, and a playbook promising a rule the scanner stopped applying would look fine right up until that moment.

DECISION 04

Core REST with conditional requests first, the search API last

because Measured, not assumed: search tripped a secondary rate limit after roughly three calls while core sat at 4,997 of 5,000. Search became a throttled supplement that returns `degraded: true` rather than throwing, so a search limit can never take down a scan.

DECISION 05

An unknown linked-PR status stays unknown

because Guessing that nothing references an issue would promote a contested issue to the top of the queue, which is the worst failure this tool has — it would send a human to duplicate someone else's open work in front of the maintainers. Only a confirmed open PR drops a candidate.

DECISION 06

Cap the queue at ten, reserve three slots for the issue lane, at most two per repo

because Volume is the spam signal the whole project exists to avoid. PR-lane candidates score higher by construction, so without a floor the scoring would have quietly undone the two-lane decision, and a queue that offers one repo six times is not a queue.

LIMITS.TXT: OSSQ (1 KB)[where it doesn't work →]

== </where it doesn’t work> ==

────────────────────────────────────────

The ranking has never been validated against anything. No queued candidate has become a merged pull request or a maintainer-shipped fix, so there is no evidence that a score of 125 predicts more than a score of 77 — outcome tracking is the top backlog item and the only signal that would show whether the scoring works at all. Maintainer responsiveness, which the design actually wanted, is not measured: the shipped substitute is the fraction of open issues carrying any comment, which counts non-maintainer noise, so a chatty community with absent maintainers scores better than it deserves. Two of the eight signals are near-constants kept deliberately for comparability, the watchlist is twelve hand-curated repos scored against one person's shipped experience rather than a general-purpose recommender, and policy drift detection is a SHA comparison — a repo that changes its practice without editing CONTRIBUTING goes unnoticed. The zero-auth atom fallback is wired, unit-tested and dead (406 on every repo tested), so a GitHub token is effectively required, and the package is not published to npm: the only install is clone and `npm install`. The first candidate it selected, modelcontextprotocol/typescript-sdk#2619, has been reproduced from source and fixed locally but never submitted — nothing has landed, because submitting is precisely the half of the job this tool is built to be incapable of.

PUBLIC REPO · MIT