Two models read every item before a person does
Two language models from different developers label the same issues and the same candidate repositories, with the same prompt and the same protocol text. Where they agree and where they split orders the human annotation round; their labels are never ground truth and never a result of the study.
- Results
- What the models judge
- Issues
- Repositories
- Stability
- To review
- How it runs
- The raters
- Reading the agreement
- Cost
- Guarantees
- Refresh
- Reproduce
- Limits
Results at a glance
Two raters agree part of the time by chance alone. Cohen's κ keeps only the agreement beyond chance: 0 is what chance gives, 1 is perfect agreement. Each κ below is a dot on this scale.
These values say how consistently two models apply the written protocol, not whether they are right. They order the human work and point at protocol text to amend; the human round is the reference.
What the models judge
Both models receive the annotators' own protocol, word for word, and answer one item per call in a closed JSON schema. In short:
RELATED only when all four conditions hold
- A cost expectation was violated. The reporter expected some memory use, speed or battery life and saw something else: stated, derived, or a regression.
- The deviation is quantified. A number with a unit, written by the reporter: MB, tok/s, ms, °C, mAh, W, %. A third-party benchmark does not count.
- It runs on the device. Phone, embedded board, laptop or consumer desktop; a datacenter does not count.
- It is attributable to a model, a configuration (quantization, context, backend) or an identifiable platform.
resource_category- The cost axis. MemoryLatencyEnergyTemperatureThroughputOther
entry_point- Where the cost originates: the model, the runtime, or the device. ArtifactToolchainPlatform
Included when M1 and M2 both pass
M3 is recorded as a caveat and never excludes.
M1Runs or packages inference
Its own code is on the execution path: it loads a model and produces tokens or transcripts, or compiles, packages or serves something that does.
M2Its own docs show an end-user device
The documentation, not the possibility: nearly anything compiles for ARM in principle.
M3Caveat: the tracker receives cost reports
Reporters describe memory, latency, energy or thermal cost they observed themselves. The study's query hit count is shown to the model as a starting point to check, not as the answer.
What each model saw, and never saw
An issue
- Repository, number, title and labels
- The whole body, never cut
A repository
- Name, description and topics
- The study's query hit count for its tracker
- A neutral sample of its issue titles: the most commented and the most recent
- The README up to a fixed cap, with the cut stated in the prompt
- The
suggested_*columns of the annotation sheet - The filter score, or any field the filter extracted
- The study's cost queries themselves, which would answer M3 for the model
- The other model's answer
- Any human label
A model that sees the filter's guess tends to agree with it, and then its agreement says nothing. Evidence is built from an allow-list of raw fields.
The exact criteria text both models received: issues
The exact criteria text both models received: repositories
Prompt templates in scripts/msr/03_annotation/prompts/; answer schemas in common/prescreen.py; the criteria text above is frozen with the run.
Issues: how the two models voted
Examples, with each model's justification
The two RQ1 labels, on the issues both called RELATED
All metrics and the confusion matrix
Repositories: criterion by criterion
Examples, with each model's justification
All metrics and the confusion matrix
Is each model consistent with itself?
Each model rated the first items of each funnel a second time, with a different seed. A model that agrees with itself less than with the other model is noisier than the disagreement it takes part in.
What goes to a person
Splits show where the protocol is read in two ways; a sample of agreements checks that the models are not wrong
together. Both wait in <funnel>/review_queue.csv.
The queue and the run folder show the models' labels. Anyone who will label the same items in the human round must not see them before that round closes, or the model answers anchor the ground truth. The examples on this page are public: an item shown here can no longer be labelled blind.
How the pre-screen works
How it runs
A run has three steps and lives in its own folder, data/msr/llm_prescreen/<run_id>/. The evidence and the
prompt are frozen first; each model then rates the frozen items on its own; the agreement step compares the two
sheets and writes the queue a person reviews.
llm_evidence.py
one record per item, the prompt template and the protocol section, a manifest
llm_rate.py
llm_rate.py
llm_agreement.py
Kappa per funnel, retest stability
Both raters receive the identical prompt: one template per funnel, section 1 of the protocol injected verbatim, and the output schema. Each call carries one item, so items never share a context and one answer cannot lean on the previous one.
Figure 1 — The two raters never see each other's answers; they meet only in the agreement step.
| Step | Script | Writes under <run_id>/<funnel>/ |
|---|---|---|
| 1 · Freeze evidence | 03_annotation/ |
evidence.jsonl, the frozen prompt_template.md and protocol_section.md, a block of the run's manifest.json |
| 2 · Rate, once per model | 03_annotation/ |
responses_<rater>.jsonl (the raw answers, which are also the cache), rater_<rater>.csv, retest_<rater>.csv |
| 3 · Agreement | 04_agreement/ |
sample_eval.csv, kappa_report.md, disagreements.csv, review_queue.csv, and one llm_agreement_report.md for the whole run |
The pre-screen runs after the automatic filter (stage 02) and before the human annotation round (stage 03).
The filter score and the suggested_* columns already order work for people; the pre-screen adds a second
ordering that reads the protocol itself. Once the human round exists, the same tooling compares each model with a
human sheet and measures how far it can be trusted.
The raters
Independence is the reason for using two models. Two fine-tunes of one base model are not independent raters, and neither is a distilled model and its base: a DeepSeek-R1 distill is a Qwen or a Llama underneath. The two raters must come from different developers and training pipelines.
| Provider | Flag | How it is used |
|---|---|---|
| Local open-weight models through Ollama | --provider ollama |
Free. One model at a time; the digest of the weights is recorded with each answer. |
| Anthropic API | --provider anthropic |
Structured output enforced by the API. Key read from ANTHROPIC_API_KEY, never written anywhere. |
| Gemini API | --provider gemini |
Structured output enforced by the API. Key read from GEMINI_API_KEY. |
| Simulated rater | --provider fake |
A labeller with a known error rate, for tests and dry runs. Its reports carry a SYNTHETIC banner. |
gpt-oss:20b (OpenAI) with think=medium and qwen3:8b (Alibaba) with
think=true, both free and local through Ollama: two developers, so two independent raters, at no
monetary cost.
No temperature, so stability is measured
Current Claude models reject a temperature parameter, so no provider sets one and every model samples with its publisher's defaults. Local runs add a fixed seed per replicate: the run reproduces on the same machine, and the retest is still a genuine second draw. Each model re-rates a subset of the items and is compared with itself, so stability is a number in the report rather than an assumption. No fallback model is enabled either: a refusal rescued by a different model would change who the rater is.
Reading the agreement
The binary decisions go through the plumbing of the human round unchanged:
merge_annotations.py, calculate_kappa.py --prescreen and split_screening.py --merge
run on the two model sheets as they would on two annotators' files. The report therefore carries the same observed
agreement, prevalence and bias indices and PABAK as the human Kappa, and is read with the same rules.
llm_agreement.py adds what two models need and two people do not:
| Measure | Why |
|---|---|
Kappa over the declared classes of resource_category and entry_point, on items both models call RELATED |
These are the RQ1 labels. PABAK is generalised to (k·p₀ − 1)/(k − 1) with the declared number of classes, not the observed one. |
| Kappa per repository criterion: M1, M2, inclusion, M3 | Locates the criterion the models read differently. The human screening merge reports the same four values. |
| Retest Kappa, each model against itself | The sampling cannot be fixed, so stability is measured. A model that agrees with itself less than with the other model bounds the inter-model value by noise. |
review_queue.csv |
Every disagreement and invalid answer, plus a seeded sample of agreements: two models can be wrong together. |
- Kappa ≥ 0.61 between the models means they apply the protocol consistently with each other, not that they are right. The sample of agreements in the review queue is what checks correlated error.
- If a model's retest Kappa is below the inter-model Kappa, the model is unstable: do not interpret the inter-model value.
- Disagreements concentrated on one criterion (M2, or one condition of RELATED) point at protocol text to amend. It is the same diagnostic the human pilot gave.
Why a local run takes hours
With two free local models on a laptop, the first run takes hours rather than minutes. That is expected for this hardware, not a defect of the tooling, and the reasons are the very costs Stargazer sets out to estimate.
| Per model | Items | Retest | Calls |
|---|---|---|---|
| Issues | 60 | 20 | 80 |
| Repositories | 30 | 10 | 40 |
| Two models | 240 |
Defaults of refresh_pipeline.sh (SAMPLE_N, ISSUE_RETEST, REPO_LIMIT, REPO_RETEST). One item per call, one call at a time.
Measured in the first run
The models do not fit in GPU memory
The laptop GPU (RTX 4050 Laptop) has 6 GB. gpt-oss:20b is 13 GB, so more than half of it runs from
system RAM; qwen3:8b (5.2 GB) almost fits, but the cache for its 40,960-token context spills. Decoding is
then bound by memory bandwidth, not compute.
Long issues take minutes to read
Some issue bodies run to tens of thousands of tokens and are sent whole. The laptop needs minutes just to process such a prompt before the first output token appears.
Both models think before answering
With thinking enabled, each model writes hundreds to thousands of reasoning tokens before its JSON answer, at laptop decode speed.
No parallelism
There is a single GPU, so items are rated one after another. The measured wall time of each call is kept with the run.
Model sizes as reported by ollama list; context length by ollama show.
It would answer in seconds, mostly because it runs on datacenter GPUs, but it fails three requirements: it is a single model; all items share one context, so one label can influence the next and nothing guarantees each item was read in full; and it leaves no record to regenerate from (no frozen prompt, no protocol hash, no raw answers, no replay). A number that cannot be regenerated cannot go into the paper.
| Faster option | Trade-off |
|---|---|
The same design through a paid API (--provider anthropic or gemini) |
Minutes instead of hours, at a monetary cost. Worth it if the pre-screen is extended to the whole filtered corpus. |
| Reasoning off, or a smaller model that fits the GPU | Faster, but a weaker rater. That is a change of method and has to be documented. |
| A smaller context window | Not recommended: long issues would no longer be sent, biasing the sample toward short issues. |
Guarantees
Each property below is enforced in code and covered by a test. The suite runs offline, with no token and no gitignored corpus.
- testedHuman annotation files are unreachable. Every write
goes through a guard that refuses
annotator_*.csv,repo_screening_{1,2}.csv(also through a symlink) and anything underdata/msr/annotation/. - testedNo anchoring. Evidence comes from the allow-list above; no suggestion, score or extracted field reaches a model.
- testedOne prompt for both models. The run freezes the template and the protocol section and records the SHA-256 of the live protocol file, so a later amendment needs a new run instead of silently changing an old one.
- testedStrict answers. Truncated JSON, a value outside the
enums, an empty justification or an incomplete stop leave the row blank, with the reason in
parse_status. Nothing is repaired or guessed into a label. - testedCache, resume, replay. Each answer is stored under
a key built from the provider, the model, its parameters, the hashes of prompt, template, protocol section, evidence
and schema, and the replicate number. An interrupted run never pays twice, and
--replayrebuilds every CSV byte for byte with no network and no key. - testedNo silent truncation. A prompt that may not fit the
local context window (
--num-ctx) is recorded ascontext_limitand not sent, because Ollama would cut it without saying so. - testedReproducible reports. The report time is the end of the last rating, read from the manifest, so re-running the agreement step rewrites every file identically. A review queue a person has started filling in is never overwritten.
A mutation check confirms the suite has teeth: breaking the guard, the evidence allow-list, the cache key or
the enum check each makes it fail. The Kappa code itself moved to
common/agreement.py,
and a golden-file test holds the human Kappa report to its output before the move.
Unattended refresh, from selection to pre-screen
The pre-screen is the last step of
scripts/msr/tools/refresh_pipeline.sh,
which rebuilds the collection from repository selection onwards without anyone at the keyboard.
- 00 · Select
select_repos.py→bias_report.py→split_screening.py, the screening sheets for the pilot rows - 01 · Mine
mine_issues.pyover the repository list; started fresh once, resumed afterwards - 02 · Filter
filter_issues.py, theninverse_filter.pywhile its audit sheet is still unlabelled - 05 · Funnel
build_funnel.py, the counts on the funnel page - Pre-screena new stratified sample →
llm_evidence.py→llm_rate.pyfor two local models, both funnels, with retest →llm_agreement.py
Every step leaves a marker when it succeeds, so running the script again resumes at the first unfinished step. The miner also resumes at the query it stopped at instead of replaying the whole query plan, which on the 86-repository collection would cost most of a day of rate-limited API calls. The previous corpus is backed up before mining starts, and nothing is committed: the run ends with files to review.
tmux new-session -d -s msr-refresh \
"systemd-inhibit --what=sleep:idle:handle-lid-switch --who=msr-refresh \
--why='MSR pipeline refresh' --mode=block \
bash scripts/msr/tools/refresh_pipeline.sh"
Detached in tmux and immune to sleep and to closing the lid. GITHUB_TOKEN comes from the environment or from gh auth token and is never printed or written to a file.
Reproduce
- Scripts
scripts/msr/03_annotation/llm_evidence.py,llm_rate.py,scripts/msr/04_agreement/llm_agreement.py- Shared code
common/prescreen.py(funnels, evidence, parsing, guard),common/llm_providers.py,common/agreement.py- Output
data/msr/llm_prescreen/<run_id>/- Guide
03_annotation/README.md, section LLM pre-screen; the/msr-llm-prescreenskill- Tests
python -m pytest scripts/msr/tests, offline
P=.venv/bin/python
# 1. freeze the evidence (a new protocol needs a new run id)
$P scripts/msr/03_annotation/llm_evidence.py --run-id R --funnel issues
GITHUB_TOKEN=... $P scripts/msr/03_annotation/llm_evidence.py --run-id R --funnel repos --limit 30
# 2. one rater per model; re-running resumes from the cache, --replay rebuilds offline
$P scripts/msr/03_annotation/llm_rate.py --run-id R --funnel issues \
--rater gptoss --provider ollama --model gpt-oss:20b --think medium --retest 20
$P scripts/msr/03_annotation/llm_rate.py --run-id R --funnel issues \
--rater qwen --provider ollama --model qwen3:8b --think true --retest 20
# 3. agreement, both funnels
$P scripts/msr/04_agreement/llm_agreement.py --run-id R --raters gptoss qwen
Limits
- Agreement is not correctness. Two models can share a blind spot; the sample of agreements in the review queue is the only check against it until the human round exists.
- Reproducible on one machine. A fixed seed replays a local run on the same hardware and build; elsewhere the cached raw answers and the weights digest are the record, not a re-run.
- A snapshot, not the repository. The repository funnel decides from a capped README and a sample of titles, without browsing. The prompt asks the model to say when the snapshot is not enough.
- Frozen per run. A changed template or protocol section needs a new run id; an old run keeps the text it was rated with.
- Not a result. The study's reported agreement comes from the human round. Nothing on this page, and nothing a run produces, is quoted as a finding.