Skip to content
costajohntPublic

About

Mine a repo's PR-review history and current code into a portable AGENTS.md conventions file

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

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 .prlore/AGENTS.md and add a pointer block to AGENTS.md? [y/N] (see "Where the output goes" 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;
                             no cap with claude-cli unless set)
  --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 name (default: AGENTS.md); the mined
                             doc goes to .prlore/<file> and <file> gets a
                             short pointer block
  --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 --yes

Exit 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 gets a pointer block appended, see "Where the output goes" 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.

Where the output goes

The mined conventions always land in a prlore-owned file, .prlore/AGENTS.md, inside a <!-- prlore:begin --> / <!-- prlore:end --> block. AGENTS.md at the repo root only ever gets a small managed pointer block:

## Mined PR-review conventions
Conventions mined from past PR reviews by prlore: [.prlore/AGENTS.md](.prlore/AGENTS.md). Treat them as suggestions; the code and maintainers win where they disagree.
  • No AGENTS.md yet: prlore creates one containing just the pointer block.
  • An AGENTS.md you wrote: the pointer block is appended. The prose above it stays byte-for-byte identical, and re-running mine replaces just the block.
  • An AGENTS.md from an older prlore that wrote the full doc into the root managed block: the next write moves the mined doc to .prlore/AGENTS.md and replaces that block with the pointer. Anything outside the markers is left alone. Per-area <area>/AGENTS.md stubs an older version wrote are not touched; delete them by hand if you used --layout per-area before.

What to commit. .prlore/ also holds private mining state (the fetched PR corpus, the extraction cache, the checkpoint). On its first write prlore creates .prlore/.gitignore that ignores everything there except AGENTS.md, areas/*.md, and provenance.json, so committing .prlore/ commits exactly the files the pointer and the provenance sidecar need. An existing .prlore/.gitignore is never overwritten. Don't ignore .prlore/ as a whole, or the pointer in AGENTS.md dangles for everyone else.

The mined doc is model output distilled from third-party PR text, which is why the pointer frames it as suggestions rather than rules. With a non-default --target, the same applies with that filename substituted throughout (.prlore/<target>, and the pointer references .prlore/<target>).

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 to gh auth token (requires the gh CLI to be authenticated).
  • ANTHROPIC_API_KEY — used by the Anthropic model provider (the @anthropic-ai/sdk client 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 and low-confidence rules, summary stats, and a one-time confirmToken.
write Writes the previewed draft to .prlore/AGENTS.md and a pointer block to AGENTS.md. 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; see src/model/anthropic.ts for the current price table).
  • model.maxBudgetUsd — a hard USD cap across the whole run (default 10). With claude-cli there is no default cap: its costs are the CLI's API-price estimates against your subscription, not a bill, so it is only capped when you set a budget. Extraction stops at 80% of the cap, counting calls still in flight, so the rest is left for synthesis. 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. Re-running mine after a failed synthesis resumes it: every finished analyze/synthesis reply is saved in .prlore/synthesis-cache/ (cleared after a run with no model failures) and extraction is cached per PR, so only the calls that failed are paid for again.
  • Synthesis runs in batches (clustering 60 statements per call, merged across batches by clustering the batch results again; reconcile 40 clusters per call; dedupe 300 rules per call). With the Anthropic and OpenAI-compatible providers a reply cut off at the output-token cap is detected and its batch is split in half and retried; if a single item still does not fit, the job fails. claude-cli ignores the output cap and reports no stop reason, so a cut-off reply there shows up as an ordinary model failure (fallback plus a warning).

model.provider selects which model backend runs the mining calls:

  • "auto" (the default): uses the Anthropic API if ANTHROPIC_API_KEY is set; otherwise falls back to the local claude CLI if it's on PATH; otherwise falls back to GitHub Models if GITHUB_TOKEN/GH_TOKEN is set (present automatically in GitHub Actions, Codespaces, and Copilot); errors naming all remedies if none is available. "ollama" is never chosen by auto, since it depends on a local server being up; pass --provider ollama explicitly.
  • "anthropic": always uses the Anthropic API. Requires ANTHROPIC_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 the claude CLI itself defaults to (prlore doesn't override it unless model.model is set); the Anthropic provider's default is claude-sonnet-5 (see src/model/anthropic.ts for the current price table).
  • "github-models": uses GitHub Models (https://models.github.ai/inference), an OpenAI-compatible endpoint. Requires GITHUB_TOKEN or GH_TOKEN; default model is openai/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 at http://localhost:11434/v1 (or OLLAMA_BASE_URL), no API key needed. Default model is qwen2.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 (or OPENAI_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. mine rejects provider: "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      # tsc

No 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 git commands against repoPath; it does not fetch or shallow-clone the repo itself.
  • Managed-block emission only. prlore writes into fenced <!-- prlore:begin --> / <!-- prlore:end --> blocks. It never merges into hand-written prose: the mined doc goes to .prlore/AGENTS.md and only a small pointer block goes in AGENTS.md (see "Where the output goes"), 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 write accepts.
  • Re-running mine refreshes the corpus. With the same flags, a re-run fetches only PRs updated since the previous run (newest first, down to the stored updatedAt watermark), adds them to the corpus, extracts only new or changed PRs (the extraction cache covers the rest), and re-synthesizes. --days N slides with the clock, so PRs that age out of the window drop out of the doc, and only the newest maxPrs are used. The CLI prints a corpus: line (PR count, newest PR update, how many are new or changed since the last synthesis), MCP status and preview return it as corpus, and provenance.json records it. A run that finds nothing new says so in a corpus unchanged warning. To refetch from scratch, delete .prlore/checkpoint.json; the extraction cache is kept.
  • 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 with resolveContested: [{ id, action: "keep" }] on write.
  • Low-confidence rules are held back the same way. A rule whose statement shares under 20% of its content words with the reviewer quote that gives it its authority is listed with the contested items (marked [low confidence] in the preview, low-confidence: on CLI stderr, lowConfidence: true in the MCP preview) instead of being written. This catches a rule the model attached to an unrelated maintainer comment, for example because PR text steered the extraction. A kept one is written with a [low confidence] marker. Over MCP, write keeps one only when its id is listed with action: "keep", and echoes every kept one back as keptLowConfidence.
  • Rendered text can't carry markdown structure, links, or HTML. In rule text, titles, headings and contested items every ASCII punctuation character is backslash-escaped (so the raw file reads Don\'t use \any`, and code renders as plain text), &becomes&, and anything a linkifier would link (URLs, bare or Unicode domains, emails, IPs, as found by linkify-it) has its ., :, /and@` entity-encoded. Control and bidi characters are stripped, and the same stripping applies to contested lines on CLI stderr.
  • PR text is treated as untrusted data. Titles, bodies, review comments, file names, and statements derived from them are fenced in delimited blocks in every model prompt, with an instruction that their content is never instructions. This lowers prompt-injection risk; it doesn't remove it, so review the preview before writing.
  • 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.dropped with droppedReason: "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.rules still carries full records for every rule, tagged with renderedTier: "full" | "compact". Set --max-rules (CLI) or output.maxRules (MCP config) to change the cutoff; it must be a positive integer, so "no cap" means passing a large number, not 0.

About

Mine a repo's PR-review history and current code into a portable AGENTS.md conventions file

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages