prlore
v0.5.0
Published
Mine a repo's PR-review history and current code into a portable AGENTS.md conventions file
Readme
prlore
Mines a repository's pull-request discussion history, reconciles it against the current codebase, and emits a portable AGENTS.md-style conventions file with a provenance sidecar. Runs either as an MCP server or as a standalone terminal command.
Usage
prlore has two entry points from the same installed binary: prlore with no
arguments runs the MCP server; prlore mine <owner/repo> runs the full
pipeline directly in a terminal, with an interactive preview-and-confirm step
before anything is written.
Either way, prlore always operates against a local checkout of the target
repo — it runs git grep/git log against repoPath to corroborate what the
PR discussion says against what the code actually does. Point repoPath (or,
for the CLI, --repo-path) at a clone, not a bare owner/name reference.
As a terminal command
npx prlore mine octo/repo --intent "onboard an AI coding agent"This runs the mining pipeline in-process: it starts the job, polls progress to
stderr every 5 seconds, then once the draft is ready prints it to stdout along
with any contested rules (listed on stderr — v1's CLI never auto-resolves
them; use the MCP write tool's resolveContested, or edit the file by hand,
to keep one). It then asks before touching disk: Write AGENTS.md? [y/N] when
it will write the target directly, or Write .prlore/AGENTS.md and add a pointer
block to AGENTS.md? [y/N] when an existing hand-authored AGENTS.md means it
adopts pointer mode instead (see "Adaptive pointer mode" below).
usage: prlore mine <owner/repo> [options]
--intent <text> what the resulting doc should help with
--since <ISO-datetime> only PRs updated at/after this timestamp
--days <n> only PRs updated in the last <n> days
(--since and --days are mutually exclusive)
--author <login> only mine PRs authored by this login
(case-insensitive; repeat --author for multiple
logins). When set and --intent is not given, the
default intent switches to documenting the
review feedback these authors received.
--budget <usd> max USD to spend on model calls (default: 10)
--model <id> model id override
--provider <anthropic|claude-cli|github-models|ollama|openai|auto>
which model backend to use (default: auto)
--base-url <url> base URL for --provider openai (or
OPENAI_BASE_URL); ignored by other providers
--repo-path <path> local checkout to mine against (default: cwd)
--target <file> output file (default: AGENTS.md)
--max-rules <n> cap on rules rendered in full detail; the rest
render as compact one-liners in a trailing
section (default: 60; must be a positive
integer — pass a large number for "no cap")
--yes skip the write confirmation prompt
--dry-run preview only; never writes, even with --yesExit codes: 0 on success (including a confirmed-no / --dry-run "not
written" outcome), 1 if the mining job itself failed or the write was
refused (a target file whose managed block is damaged: a corrupt/reversed
marker pair, or a lone stray marker. An existing file with no prlore markers is
not a refusal; it adopts pointer mode, see "Adaptive pointer mode" below), 2
for a usage or configuration error (bad flags, invalid MineConfig, no model
provider available).
The CLI draws model calls from whichever provider --provider/model.model
resolves to (see "Budget and model notes" below) — with --provider
claude-cli or the auto fallback, that's your local Claude Code CLI running
headless against your subscription, so prlore mine works with zero API
credentials on a machine that already has claude authenticated.
Mining your own PRs
If you're an outside contributor rather than a maintainer, --author narrows
mining down to PRs you authored, so the resulting doc becomes a record of the
review feedback you've gotten and the standing lessons from it, rather than a
general conventions guide. Authority weighting already does the rest here —
maintainer comments on your own PRs already drive rule scoring, so no scoring
changes are needed for this to work; --author is purely a client-side filter
over which PRs get mined at all (GitHub's GraphQL API has no server-side
author filter on the pullRequests connection, so every PR is still fetched
and paid for at the same rate cost — only the kept, and therefore
model-processed, set gets smaller).
prlore mine owner/repo --author yourlogin --days 730--author is repeatable (--author alice --author bob) and matches
case-insensitively. When --author is given and --intent isn't, the
default intent switches from the general onboarding phrasing to documenting
the review feedback these authors received.
Adaptive pointer mode
prlore never rewrites hand-authored prose. How it writes the mined conventions depends on the target file's existing state, chosen automatically from what's on disk (there is no flag):
- Direct mode (target absent, or already prlore-managed with a valid marker
pair). The full mined doc goes straight into
AGENTS.md, inside a<!-- prlore:begin -->/<!-- prlore:end -->block. Bytes outside that block are preserved exactly. This is the original behavior and is unchanged. - Pointer mode (an existing
AGENTS.mdthat prlore doesn't manage, i.e. one with no prlore markers). The full mined doc lands in a separate, prlore-owned file,.prlore/AGENTS.md, and your existingAGENTS.mdis only ever appended to with a small managed pointer block (markers plus a one-line "the mined conventions live in .prlore/AGENTS.md, read that first" note). The prose you wrote above that block stays byte-for-byte identical, and re-runningminereplaces just the pointer block in place. prlore also enters pointer mode whenever.prlore/AGENTS.mdalready exists, so once a repo adopts it the layout stays put.
With a non-default --target, the same rule applies with that filename
substituted throughout (.prlore/<target>, and the pointer references
.prlore/<target>).
Pointer mode is exactly the case that used to refuse. An existing unmarked
AGENTS.md is now adopted, not rejected (see the exit-code note above for what
still refuses).
As an MCP server
Point an MCP host (Claude Code, Claude Desktop, or any other MCP client) at it:
{
"mcpServers": {
"prlore": {
"command": "npx",
"args": ["-y", "prlore"],
"env": {
"GITHUB_TOKEN": "ghp_...",
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
}
}Environment variables
GITHUB_TOKEN— a GitHub token with read access to the target repo. If unset, prlore falls back togh auth token(requires theghCLI to be authenticated).ANTHROPIC_API_KEY— used by the Anthropic model provider (the@anthropic-ai/sdkclient reads this directly; no prlore-specific wiring needed). This remains the documented default for CI and servers, where there's no interactive Claude Code session to draw on.
The five tools + the interview prompt
| Tool | Purpose |
|---|---|
| mine | Starts (or resumes) a mining job. Only required inputs are repo (owner/name) and intent (free text describing what the doc is for) — everything else defaults. Rejects while a job is already running. |
| status | Reports job state, stage, progress counters, and spend so far. Poll it after mine. |
| cancel | Requests a graceful stop; the job checkpoints at the next stage boundary so a later mine call resumes instead of restarting. |
| preview | Once a job reaches ready-for-preview, returns the rendered markdown, any contested rules, summary stats, and a one-time confirmToken. |
| write | Writes the previewed draft to disk. Requires the confirmToken from the immediately preceding preview call — prlore never writes without one. Accepts resolveContested to keep or drop specific contested rules. |
The interview MCP prompt is a conversational script for gathering mine
inputs (repo, intent, focus areas, output preferences) instead of asking the
user to fill in a MineConfig by hand. It's optional — mine({ repo, intent })
works cold.
Budget and model notes
mine's model config controls spend:
model.model— which Anthropic model to use (defaults to prlore's built-in default; seesrc/model/anthropic.tsfor the current price table).model.maxBudgetUsd— a hard USD cap across the whole run (default10). Once tripped during extraction, the run degrades to a partial-corpus draft with a warning; once tripped during synthesis (the highest-value calls), the job fails outright rather than emitting a truncated conventions doc.
model.provider selects which model backend runs the mining calls:
"auto"(the default): uses the Anthropic API ifANTHROPIC_API_KEYis set; otherwise falls back to the localclaudeCLI if it's on PATH; otherwise falls back to GitHub Models ifGITHUB_TOKEN/GH_TOKENis set (present automatically in GitHub Actions, Codespaces, and Copilot); errors naming all remedies if none is available."ollama"is never chosen byauto, since it depends on a local server being up; pass--provider ollamaexplicitly."anthropic": always uses the Anthropic API. RequiresANTHROPIC_API_KEY; fails immediately with a clear error if it's unset (rather than failing deep inside the pipeline on the first call)."claude-cli": always uses your locally installed, already-authenticated Claude Code CLI in headless mode. No API key needed, but usage draws on your Claude subscription's usage limits, not billed API credits. Its default model is whatever theclaudeCLI itself defaults to (prlore doesn't override it unlessmodel.modelis set); the Anthropic provider's default isclaude-sonnet-5(seesrc/model/anthropic.tsfor the current price table)."github-models": uses GitHub Models (https://models.github.ai/inference), an OpenAI-compatible endpoint. RequiresGITHUB_TOKENorGH_TOKEN; default model isopenai/gpt-4o-mini. Free tier, but rate-limited (roughly 50-150 model calls per day, depending on model), so it realistically only covers a small mine; see "Running inside GitHub Copilot / Codespaces" below."ollama": uses a local Ollama server athttp://localhost:11434/v1(orOLLAMA_BASE_URL), no API key needed. Default model isqwen2.5:7b. No rate limits, so this is the option for a full mine with no billed API."openai": generic OpenAI-compatible endpoint for any other provider. Requires--base-url(orOPENAI_BASE_URL),OPENAI_API_KEY, and--model/model.model."sampling"also exists in the config schema (MCP sampling as a fallback when no API key is configured), but that path isn't wired yet.minerejectsprovider: "sampling"with a tool error rather than silently running another provider in its place.
For "github-models", "ollama", and "openai", cost tracking is disabled
(these endpoints are free-tier or arbitrarily priced), so --budget/
model.maxBudgetUsd does not gate them.
Running inside GitHub Copilot / Codespaces
prlore auto-detects the environment's GITHUB_TOKEN and uses GitHub Models, so
npx prlore mine <owner/repo> works with no extra setup. Note the free-tier
rate limits (roughly 50-150 model calls per day, depending on model), which
realistically only cover a small mine; scope it down with --days or a small
window. For a full mine, run a local model with --provider ollama (no rate
limits) or set ANTHROPIC_API_KEY.
Development
npm install
npm test # vitest
npm run typecheck # tsc --noEmit
npm run build # tscNo live network or LLM calls happen in the test suite — GitHub's GraphQL API
and the model provider are faked throughout, including in the end-to-end test
(test/e2e.test.ts), which runs the real mining pipeline against a
script-built fixture git repo.
Limits (v1)
- Local checkout required. prlore reconciles PR discussion against the
current codebase by running real
gitcommands againstrepoPath; it does not fetch or shallow-clone the repo itself. - Managed-block emission only. prlore writes into a fenced
<!-- prlore:begin -->/<!-- prlore:end -->block (or a fresh file if the target doesn't exist). It never merges into hand-written prose: an existing unmarkedAGENTS.mdis adopted via pointer mode (the mined doc goes to.prlore/AGENTS.mdand only a small pointer block is appended to the human file, see "Adaptive pointer mode"), and a target whose managed block is damaged (a corrupt/reversed pair or a lone stray marker) is refused rather than guessed at. - No incremental UI. There's no diff view, no partial-accept-per-rule flow
beyond the contested-item keep/drop list
writeaccepts. Re-runningminere-synthesizes over the (cached) extraction corpus. - Contested rules are never auto-written. When PR discussion and code
trend disagree, or two candidate rules directly conflict, the rule lands in
contested, not in the emitted draft — it only appears in the written file if the caller explicitly resolves it withresolveContested: [{ id, action: "keep" }]onwrite. - Single repo per run; no GitLab/Bitbucket support; no continuous ingestion or webhooks — this is a batch tool you re-run, not a review bot or an enforcement tool.
- Recurrence floor (always on, not configurable). A non-contested rule
backed by fewer than 2 distinct PRs, with no maintainer-tier (owner/member/
collaborator) evidence, is demoted to
provenance.droppedwithdroppedReason: "recurrence-floor"even if it would otherwise clear the score threshold — this filters out single-PR trivia (a one-off nit that happened to get said with confidence) without needing a config knob. Synthetic code-only rules (derived purely from codebase patterns, with no PR evidence) are exempt. Nothing is silently discarded: floored rules stay in the provenance sidecar with their reason, just not in the rendered doc. - Auto layout also triggers on draft size in bytes, not just line count.
layout: "auto"switches from a single file to per-area files once the draft exceeds 400 lines OR 24,000 bytes (~6k tokens), whichever trips first — a handful of very long rule lines can blow past a reasonable doc size while staying under the line-count check alone. - Tiered rendering caps how many rules get full treatment. Only the top
output.maxRules(default 60, by score — rules already arrive score-sorted) render in full inside their planned sections. Anything beyond the cap still ships, just compressed: one line each (- statement _(citations)_, no rationale) in a single trailing## Additional conventions (lower signal)section, before the contested section. Nothing is dropped from the doc by this —provenance.rulesstill carries full records for every rule, tagged withrenderedTier: "full" | "compact". Set--max-rules(CLI) oroutput.maxRules(MCP config) to change the cutoff; it must be a positive integer, so "no cap" means passing a large number, not0.
