Personas 0.4.16

dotnet tool install --global Personas --version 0.4.16
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Personas --version 0.4.16
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Personas&version=0.4.16
                    
nuke :add-package Personas --version 0.4.16
                    

Persona Agent

A local-first .NET 10 tool that builds an evidence-grounded reviewer persona of a specific engineer from their history (PRs, review comments, commits, code, docs) and uses it to answer questions and review pull requests the way that person would — in their voice, scoped to their expertise, and with every finding traceable to real evidence.

It is designed to run in addition to generic AI reviewers: it targets the small, repeated, domain-specific issues a particular engineer reliably flags, so the human expert can focus on the hard problems.

How it works

ingest                normalize PRs / comments / commits / code / docs from
                      Azure DevOps, GitHub, local Git, and folders
   -> index           evidence chunks -> SQLite FTS5 (BM25) + local ONNX embeddings, fused with RRF
   -> build           cluster the persona's recurring review comments, mine deterministic
                      "matcher" checks from them via Copilot, back-test each against history,
                      and write a versioned persona pack (memory, rubric, tone, mined checks)
   -> review          run the mined checks on a PR diff (deterministic, evidence-anchored),
                      merged ahead of the LLM's fuzzy residue, severity-ordered
   -> eval / feedback  measure precision/recall against history; record reviewer reactions
                      so dismissed checks stop firing

Key properties:

  • Evidence-grounded. Every persona memory, rubric rule, and mined check cites real artifacts. A review finding without a citation (or pointing away from a changed line) is suppressed.
  • Precision-first. Mined checks are kept only if they back-test well against the PRs whose comments produced them. Reviewer feedback suppresses checks the team keeps dismissing.
  • Local & offline-capable. SQLite + local ONNX embeddings; no cloud vector DB. The only LLM dependency is the GitHub Copilot SDK, and every LLM path falls back to deterministic behavior on failure.
  • Team mode. Run several personas over one PR and merge their findings, attributed to whoever raised each.

Requirements

  • .NET SDK 10
  • Optional: GitHub Copilot CLI / SDK auth for LLM-backed mining and review (llm.provider: github-copilot-sdk)
  • Optional: local ONNX embedding model in models/ for semantic search (keyword search works without it)
  • Optional: Git CLI for localGit ingestion
  • Optional: Azure login compatible with DefaultAzureCredential (or a PAT) for live Azure DevOps ingestion
  • Optional: a GitHub token for GitHub ingestion / PR review

Download the shared embedding model once:

./scripts/download-model.ps1     # writes models/all-MiniLM-L6-v2.onnx and models/vocab.txt

Examples below use persona <command>. From source, that is dotnet run --project "src/Persona.Cli/Persona.Cli.csproj" -- <command>. Build a single-file persona executable with ./scripts/publish.ps1 -Runtime win-x64.

Quick start

persona init --data persona-data --force
# edit persona-data/persona.yaml: set a real source + persona identity
persona validate    --data persona-data
persona ingest      --data persona-data
persona index       --data persona-data
persona build       --data persona-data --persona payments-expert
persona review      --data persona-data --persona payments-expert --diff change.diff
persona eval        --data persona-data --persona payments-expert

Commands default to JSON on stdout for scripts and CI, with deterministic exit codes. ask defaults to a readable answer when stdout is a terminal; redirected or captured stdout remains full JSON.

End-to-end runbook

The examples use persona id payments-expert; substitute your own. --config <path> selects a manifest other than the data directory's persona.yaml. --data <path> is required whenever your dataDirectory is not the default ./persona-data.

1. Initialize and validate

persona init        --data persona-data --force
persona validate    --data persona-data
persona ado-validate --data persona-data    # live ADO probe (only if you configured ADO)

validate returns status: valid with errors: []. ado-validate probes each configured Azure DevOps source (repository listing, sample PR threads/commits/iterations/work items) with retries on 429/502/503/504. Resolve any errors before ingesting.

2. Ingest evidence

persona ingest --data persona-data

Writes provider-native JSON under raw/ and provider-neutral normalized/*.jsonl. Ingestion captures, where available: PRs, review comments (with thread resolution status and reactions/votes), commits, changed files, work items, local-git commits + C#/TypeScript/JavaScript symbols/chunks, and documents/transcripts. For Azure DevOps, real unified diffs are fetched only for files that received review comments (no unrelated files/PRs). maxPullRequests per source caps ingestion volume.

Targeted re-ingest of only ADO PR iteration changes:

persona ingest --data persona-data --code-changes-only
persona index --data persona-data

Writes indexes/evidence-chunks.jsonl and indexes/evidence.db (SQLite + FTS5, plus ONNX embedding blobs). If the model files under models/ are missing, the output reports embeddings disabled and FTS5 keyword search still works.

4. Resolve identity candidates

build resolves persona aliases against PR/commit/comment authors. Anything it cannot confidently resolve is listed under unresolvedCandidates. Approve or reject manually (persisted in personas/<id>/identity-overrides.json, applied by build/ask/review/eval and stable across rebuilds):

persona identity list    --data persona-data --persona payments-expert
persona identity approve --data persona-data --persona payments-expert --candidate "azure-devops:alice@example.com" --reason "verified alias"
persona identity reject  --data persona-data --persona payments-expert --candidate "local-git:bot@example.com"      --reason "service account"

5. Build the persona pack

persona build --data persona-data --persona payments-expert

Writes personas/payments-expert/persona-pack.json plus the exploded companions (memory.json, review-rubric.json, tone-profile.json, evidence-map.json, matcher-specs.json, build-report.json) and per-stage diagnostics under build-stages/.

With llm.provider: github-copilot-sdk the build also mines matcher specs: it clusters the persona's recurring comments, asks Copilot (concurrently — one fresh CLI instance per cluster, default 10) to express each cluster as a deterministic check, back-tests each candidate against the historical diffs whose comments produced it, and keeps only those clearing the recall / false-positive thresholds. Deterministic builds mine nothing.

6. Ask and review

persona ask    --data persona-data --persona payments-expert --query "How should we review retry behavior?"
persona review --data persona-data --persona payments-expert --diff change.diff --persist-run-artifacts

With an installed persona, ask directly:

personas ask --persona InfraExpert --query "What should I check when adding a new environment to our Ev2/AKS deployment scripts?"

ask uses --format auto by default: readable text on terminal stdout, full JSON when redirected or captured. Text shows the answer and warnings, without evidence/context/session JSON dumps or raw ctx citation IDs. The ask-only --format text|json overrides this choice; add the ask-only --sources for compact numbered references/links to citations used in the answer, deduplicated by URL. --sources affects text only, never JSON.

The answer comes from the persona's recorded experience, and says how. The question selects the positions, lessons and decisions in the pack that bear on it (by wording, rationale and area of the codebase); those name the review threads they came from; the threads are retrieved whole, alongside hybrid search over the index. The model answers in one of three tiers and states which: explicit (a recorded position or thread says it, cited with its reason), inferred (related positions point that way, said as an extrapolation), or none (no recorded experience; it says so and stops rather than offering generic advice). It may call persona_search_memory, persona_get_knowledge_areas, persona_get_evidence and artifacts_search_comments to look further before concluding. Text output ends with Basis: ... ; confidence ... and anything the record does not settle; JSON carries basis, confidence, unknowns, citationsUsed and the knowledge the answer was given. Stores indexed before 0.4.14 have no review-thread chunks, and stores ingested from Azure DevOps before 0.4.16 hold each code change once per pull-request iteration (two to six copies of every file, in the index and in replays); personas index --persona <name> fixes both — it compacts the store, drops the duplicate chunks, adds the threads, and keeps every unchanged embedding.

Measured (EXPERIMENTS.md, Phase I: 109 held-out questions across three personas, two runs per cell, judged against the recorded position): recall of the recorded position's key points went from 0.29 to 0.44 (62 better / 25 worse / 22 tie per case), answers grounded in this repository's own history from 81% to 98%, generic advice from 17% to 0%, with contradictions and fabrication on out-of-experience questions unchanged. The stated basis is calibrated — explicit answers recall 0.5, inferred 0.4, none 0.0 — so a reader can weight an answer by what it claims to rest on. The gain came from reading the threads whole and letting the model look, not from the synthesized pack, which added 0.02 (inside the noise) except for a persona whose template pack had been nearly empty.

For example, append --sources for references, --format json for terminal JSON, or --format text --sources > answer.txt to redirect readable text. --output-file answer.json always writes the full original JSON and prints only a JSON file pointer on stdout, even with --format text. Errors remain JSON on stderr; persisted artifacts and MCP responses are unchanged.

Ask the team. personas ask --persona InfraExpert --persona PortalExpert --query "..." asks each persona the same question in parallel. The answers are not merged into one voice - a team's members having seen different things is the information - so each appears under its own heading with its own basis and confidence, ordered explicit-first, and a short synthesis (its own model call, attributed) says where they agree, where they differ and who had no recorded experience. A persona whose pack is missing is reported and the others still answer. JSON has mode: "team", answers[] and synthesis; the MCP tool is ask_team.

review runs the persona's mined checks over the diff and merges those deterministic, evidence-anchored findings ahead of the LLM's residue, deduped by file/line and ordered by the severity ladder. --persist-run-artifacts writes context-pack.json / agent-input.json / agent-output.json / verified-output.json under runs/<runId>/ for debugging.

The reviewer reads the repository, not just the diff. A unified diff carries three lines either side of each change: no call sites, no interface the changed class implements, no test covering the branch. Judgements about correctness routinely depend on all three, and the engineer being imitated had the whole file. The reviewer can call code_read_file and code_list_files to look:

persona review --data persona-data --persona payments-expert \
  --pr-url https://dev.azure.com/org/project/_git/repo/pullrequest/123

You usually do not need to say where the checkout is. When --repo-path is omitted, the git repository containing the diff (or your working directory) is used — but only after checking that the diff's own changed files actually exist there. Every review reports what was decided under repositoryAccess, so access is never silently present or silently absent. Pass --repo-path <path> to override it, or when reviewing a diff from outside its repository. The same applies to the MCP tools (review_pull_request / team_review take an optional repoPath) and CrossReview's Personas adapter.

Nothing is remembered between runs. Inference is per-invocation and verified against the diff; there is no path-to-repository map in the data directory. Persisting one would buy a little typing back and cost correctness: data directories get copied between machines, and a recorded path that also exists on the receiving machine resolves to a different repository — the reviewer then reads those files and cites them confidently. Checking the diff's files against the candidate is what makes inferring safe, because a different repository does not contain them. A wrong answer delivered with confidence is worse than no answer.

Repository access is not a magic upgrade. Measured over 25 replayed pull requests (EXPERIMENTS.md, Phase H3/H4), a reviewer with the checkout scored no better than one without it once the diff-only arm stopped being handed tools it could not use. Reading is occasionally transformative and usually neutral, and nothing yet predicts which. The capability is on by default because it costs little and sometimes matters a great deal — not because it reliably helps.

Investigate before judging (--investigate)

A human reviewer does not start at the first + line. They work out what the change is for, decide which parts they are unsure about, and go looking — at call sites, at tests, at how the rest of the repository does the same thing — before forming an opinion. --investigate gives the reviewer that shape:

  1. Orient — one pass over the change producing a brief (what it does, what it is for, what looks risky) and a list of checks worth running, shaped by the persona's own rubric so the questions are the ones this reviewer would ask.
  2. Investigate — each check is worked against the repository one at a time, and an answer may raise a follow-up when something does not add up.
  3. Review — the persona reviews with the answers in hand, and is told which checks came back inconclusive so it does not assert what could not be established.
persona review --data persona-data --persona infra-expert --diff change.diff --investigate

The investigation block on the result carries the brief, every check, its answer and confidence, and the files that were read — so a finding can be traced to the check that justified it.

One check kind is worth calling out. A convention check asks whether the repository has a settled practice at all, because that decides whether a style comment is legitimate: "this does not match the house style" is only true if there is a house style. On a real change it came back "silent-no-op is the settled pattern for the rewrite helpers in this file, and the new function lands squarely on that side" — the reviewer talking itself out of a comment it would otherwise have made.

Off by default, on evidence. Measured on the 25-PR historical replay (EXPERIMENTS.md, Phase H7-AB: three arms, six runs, paired per case), the harness does not improve recall of the persona's own review comments — control 0.645, harness 0.628 in the shipped configuration and 0.519 once a dedup-scope confound was removed — at 1.7–1.9× the wall clock and 4× the repository reads. Its effect runs through the reviewer's confidence in findings it already had, in both directions, netting to zero; it is not finding new issues. Phase I tried once more with the persona in the loop: the orientation is now seeded with what the persona has recorded about the changed areas (positions with their reasons, the questions it habitually asks) instead of twelve rubric titles — and over four paired runs it still did not move (0.624 vs 0.639, 6 better / 10 worse / 9 tie). What the harness does do is change the character of the review — specific, checkable investigation notes travel with the result, now traceable to the recorded items that prompted them — and when pointed at its own implementation it found three real gaps the author had missed. That is a use, and it is why the switch exists: --investigate per run, or review.harness.enabled: true; maxConcurrentInvestigations (default 4) bounds the parallelism.

Review from recorded experience is also a switch, not a default. --experience (or review.recordedExperience: true) gives the reviewing prompt what lifted ask: the persona's recorded positions for the changed areas with their reasons, review threads whole, and an instruction to raise a finding where the change does what a recorded position argues against — naming the position. Findings then cite the experience they rest on. Measured (EXPERIMENTS.md, Phase J) it did not move hidden-comment recall (0.538 vs 0.525, 6 better / 16 worse) and its two runs disagreed with each other by more than the arms differed. Phase J also found that every review replay since Phase F had run on diffs that listed each file two to six times — an ingestion bug that stored each change once per PR iteration — so the absolute recall numbers above are inflated; the comparisons between arms, which all saw the same diffs, stand. Clean replays score about 0.52 for the shipped reviewer, and on them matched comments track finding volume (r = 0.80), which is why the next review experiment needs a precision-aware target first.

No persona? Review anyway

--persona is optional for review. The reviewing machinery — investigate, read the repository, judge, de-duplicate — is about the code; the persona supplies whose judgement is being imitated. Requiring one meant that anyone without a relevant persona got nothing, which is a poor trade: a new repository never has one.

# No persona, no data directory, nothing installed.
persona review --diff change.diff --investigate

Without a persona the reviewer judges against what the codebase already does and what the change claims to do, and is explicitly told not to invent house opinions. Everything persona-derived is simply absent: no mined matcher specs, no rubric, no tone mimicry, and no retrieval of anyone's review history — searching the shared store would otherwise pull in whoever else happens to live in that data directory. The review reports personaId: "(none)" so a stored result can never be mistaken for someone's.

The harness works the same way with or without one. On a real change it planned this check unprompted and with no persona loaded:

is Directory.Packages.props' existing house style for such notes consistent enough that remarks about this comment's wording are legitimate rather than noise?

With a persona, the plan is additionally shaped by that reviewer's own rubric, so the questions become the ones they would ask. That is the part a persona adds — and the part you lose without one.

Nothing machine-specific is stored, so nothing machine-specific can leak into a .persona bundle. ${VAR}/%VAR% are expanded in the path, so one host config can name a repository across machines that keep their checkouts in different places.

Review a live PR — by number, or just paste its browser URL — instead of a diff file:

persona review --data persona-data --persona payments-expert --ado-pr 12345 --repo checkout-service
persona review --data persona-data --persona payments-expert --gh-pr  42    --repo checkout-service

# Or paste the PR's browser URL: provider/org/project/repo/id are parsed from it, so you need no
# --repo (and no persona.yaml source) — ideal for one-off dnx/NuGet reviews. Alias: --url.
persona review --persona payments-expert --pr-url "https://dev.azure.com/my-org/MyProject/_git/checkout-service/pullrequest/12345"
persona review --persona payments-expert --pr-url "https://github.com/my-org/checkout-service/pull/42"

Team / domain review — pass --persona more than once. Findings are merged and attributed to whichever personas raised them (mode: team, plus per-persona summaries):

persona review --data persona-data --persona payments-expert --persona platform-expert --diff change.diff

7. Evaluate and calibrate

persona eval --data persona-data --persona payments-expert

With no --eval-file, eval performs historical replay: it picks PRs the persona actually reviewed, hides their comments, re-reviews the diff, and scores recovery. Primary metrics are precision and category recall; misses are attributed to retrieval vs. reasoning vs. verification. The report also includes a threshold calibration suggestion — the lowest review.minConfidence whose false-positive rate stays within target (default 0.25), maximizing recall. It is a suggestion only; nothing is rewritten.

To apply that suggestion, run calibrate (a dry-run by default; --apply surgically writes review.minConfidence into persona.yaml, preserving comments):

persona calibrate --data persona-data --persona payments-expert            # report the suggestion
persona calibrate --data persona-data --persona payments-expert --apply    # write it to persona.yaml

Benchmark several personas at once with scripts/eval-personas.ps1 (a comparison table + combined JSON report).

Diagnose retrieval bottlenecks for hidden comments:

persona eval-diagnose --data persona-data --persona payments-expert

Fixture-based smoke check instead of replay: persona eval --eval-file cases.json (schema under Eval).

8. Close the feedback loop

After deployment, record how reviewers reacted to the persona's findings. Once a mined spec (or an LLM finding category) accrues enough dismissals it is suppressed in later reviews (so the persona stops re-raising rejected checks):

persona feedback --data persona-data --persona payments-expert --spec "matcher:payments-expert:reliability:0" --outcome dismissed --reason "noisy"
persona feedback --data persona-data --persona payments-expert --spec "matcher:payments-expert:testing:1"     --outcome accepted
persona feedback --data persona-data --persona payments-expert --category maintainability                    --outcome dismissed --reason "too nitpicky"

--outcome is accepted | fixed | dismissed | false-positive. Provide exactly one of --spec (the matcher-spec prefix of a review finding's id) or --category (an inferred review category such as security or maintainability, suppressing repeatedly-dismissed LLM findings). Stored append-only under feedback/<persona>.jsonl.

9. Retrain on another machine

Everything needed to retrain lives in the repo, on NuGet (the Personas CLI), or on your Azure DevOps/Artifacts feed — except two git-ignored things: the embedding model and the tuned recipe (persona-data/persona.yaml, which holds real reviewer emails). The recipe's tuning is preserved in a committed, email-redacted template, persona-data/persona.sample.yaml, so it can never be lost. On a second machine with repo + ADO/feed access:

git clone <repo>; cd personas
az login                                    # ADO + private-feed auth (DefaultAzureCredential / credential provider)
./scripts/download-model.ps1                # writes the git-ignored models/all-MiniLM-L6-v2.onnx + models/vocab.txt

# Recipe: copy the committed template to the real (git-ignored) config, then fill the redacted emails.
Copy-Item persona-data/persona.sample.yaml persona-data/persona.yaml
#   edit persona-data/persona.yaml: replace each REPLACE_WITH_*_EMAIL@... identity with the real address
persona validate --data persona-data

# Retrain (fresh ADO evidence): ingest -> index -> build for every persona (omit --persona for all).
persona refresh  --data persona-data        # incremental; add --force to rebuild unchanged personas

# Package the retrained personas under privacy aliases and push to the private feed (bump the version).
./scripts/publish-trained-personas.ps1 -Version <next> -Push -Interactive

From a clone, persona is dotnet run --project src/Persona.Cli/Persona.Cli.csproj -- (see the note under Requirements); with NuGet access you can instead use the published tool, dnx Personas <command>.

  • Same recipe, fresh evidence. refresh re-pulls live ADO PRs at that moment, so a retrain is not byte-identical to a prior build (newer history + LLM nondeterminism). The tuning — sources, per-persona scope, per-persona retrieval budgets, matcher-mining thresholds — is reproduced exactly from the template.
  • If ADO identity resolution surfaces unmatched candidates, approve them (see step 4) and re-run refresh.
  • NuGet versions are immutable, so bump -Version on every push.
  • Keep the template in sync: if you change persona-data/persona.yaml, mirror the change into persona-data/persona.sample.yaml (emails redacted) so the recipe stays version-controlled.

Configuration

persona init writes a documented persona.yaml. Key sections:

Config file discovery

Review/ask/eval/MCP resolve a config file from the first that exists, in order: explicit --config <path> → the config in an explicitly passed --data <project> directory (persona.yaml / personas.{yaml,json} / config.yaml) → $PERSONAS_HOME/personas.{yaml,json} → $XDG_CONFIG_HOME/personas/personas.{yaml,json} (falling back to ~/.config when XDG_CONFIG_HOME is unset) → the default data directory's config (a low fallback, so a stray sample in ~/.personas never shadows an explicit PERSONAS_HOME/XDG config) → built-in defaults. Config files may be YAML or JSON. Installed persona manifests are then merged, so installed personas run with no config file at all.

Data directory

Where all persona data lives (installed personas, evidence, indexes, runs) is resolved separately, in order: --data <path> → $PERSONAS_HOME → a global config's dataDirectory: (from a PERSONAS_HOME/XDG config; relative paths resolve against the config file's folder) → ~/.personas. A dataDirectory: in a config that sits inside the data directory (e.g. an init-generated persona.yaml) is ignored, since it would be self-referential -- only a global config redirects. So one XDG config can point every command at, say, C:/temp/personas. (In YAML, leave the path unquoted or use forward slashes; a double-quoted scalar treats backslashes as escapes.)

Sources

sources:
  azureDevOps:
    - name: main-ado
      organizationUrl: https://dev.azure.com/my-org
      project: MyProject
      authentication: auto            # auto | default-azure-credential | az-cli | pat
      patEnvironmentVariable: AZDO_PAT
      maxPullRequests: 100
      repositories: [checkout-service, billing-api]
  github:
    - name: main-github
      apiBaseUrl: https://api.github.com
      owner: my-org
      authentication: auto            # auto | token | pat | none
      tokenEnvironmentVariable: GITHUB_TOKEN
      maxPullRequests: 100
      repositories: [checkout-service]
  localGit:
    - name: checkout-service
      path: C:/src/checkout-service    # reads git log + C#/TypeScript/JavaScript symbols/chunks
  documents:
    - name: architecture-docs
      path: ./docs
      include: ["**/*.md"]
      exclude: []
  transcripts:
    - name: design-reviews
      enabled: true
      path: ./transcripts
      include: ["**/*.md", "**/*.txt"]
  • ADO auto tries DefaultAzureCredential, then az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798, then the PAT env var. Sample values (e.g. my-org) are skipped with a warning.
  • maxPullRequests is the primary guard on the build-time budget for prolific reviewers.

Personas

personas:
  payments-expert:
    displayName: Payments Expert
    toneMimicryPercent: 100
    identities:
      - { kind: email, value: alice@example.com }
      - { kind: azure-devops-user-id, value: aad-user-id-123 }
    scope:
      repositories: [checkout-service, billing-api]
      includeArtifacts: [authored_prs, reviewed_prs, review_comments, docs, transcripts]

Identity kinds include email, azure-devops-user-id, azure-devops-descriptor, github-login, github-user-id, and git-author. Define as many personas as you like; run them individually or together (team mode).

LLM

llm:
  provider: github-copilot-sdk        # github-copilot-sdk | deterministic | disabled
  defaultModel: claude-opus-5.5        # "auto" uses the Copilot default (see `personas models`)
  reasoning: { personaBuild: xhigh, ask: xhigh, review: xhigh, verify: xhigh }   # per model, e.g. low|medium|high|xhigh|max
  # Optional context-window overrides (tokens); unset = the model default. A larger window also auto-scales
  # both the evidence character budget AND the retrieval depth (when --top is not passed) up to the persona's
  # trained budget, so a dense persona's full depth is used.
  # maxContextWindowTokens: 1000000
  # timeoutMinutes: 20                # per-LLM-call timeout; unset scales with reasoning effort ("max" gets the most)
  # maxPromptTokens: 900000
  # maxOutputTokens: 64000
  # maxToolCalls: 400                 # per-session tool budget; a runaway guard, not a quality control. 0 disables tools
  cache:                              # content-addressed model-response cache; on by default
    enabled: true
    # directory: ./persona-data/cache/llm
    storePrompts: true                # record what was asked alongside each response, for audit
    # maxAgeDays: 30

github-copilot-sdk uses the official GitHub.Copilot.SDK; any failure falls back to deterministic, citation-first output with a warning. deterministic/disabled are fully local (used by the test suite and offline runs); they also skip matcher mining. Run personas models to list the exact model ids and each model's supported reasoning efforts. review --model <id> / --reasoning <effort> override the config for a single run.

Response cache. Every model call is keyed on kind + model + reasoning + system prompt + user prompt, plus a fingerprint of any store the tools can read that does not appear in the prompt. A single changed character produces a different key, so a hit can never serve an answer computed from different inputs. Beyond cost this removes a variance source: a cached stage returns byte-identical output, so when one stage is A/B'd the others stop moving underneath it. Entries live at <data>/cache/llm as one inspectable JSON file each — safe to delete, and PERSONA_LLM_CACHE_DISABLED=1 turns it off for a run.

Tool budget. A reviewer investigating a real change has legitimate reasons to open many files: a wide diff, a chain of call sites, the tests for each changed symbol. maxToolCalls therefore sits where only a loop can reach it, and each LLM call gets its own budget rather than sharing one across a per-file fan-out.

Review (incl. feedback)

review:
  maxFindings: 0                      # 0 = uncapped. Volume is controlled by dedup, not truncation
  minConfidence: medium               # global floor (low|medium|high)
  categoryMinConfidence: {}           # per-category overrides, e.g. security: high
  postComments: false
  focus: auto                         # auto | single | per-file (auto infers from reviewer density)
  maxReviewGroups: 6                  # cap on file-scoped sub-reviews when focus resolves to per-file
  maxReviewConcurrency: 6             # how many of those sub-reviews run at once (one wave at 6)
  deduplication:                      # what keeps a long review readable; on by default
    mode: llm                         # none | heuristic (no model call) | llm (heuristic + model)
    scope: within-file                # within-file (keeps per-file comments) | cross-file
    similarityThreshold: 0.6          # token overlap at which two same-file findings are one issue
    lineProximity: 8                  # line distance within which they count as the same location
  feedbackDismissalThreshold: 0.5     # suppress a spec dismissed at/above this rate ...
  minFeedbackSamples: 3               # ... once it has this many recorded reactions

When focus resolves to per-file, the diff is split into file-scoped groups reviewed concurrently (up to maxReviewConcurrency, capped at maxReviewGroups), then merged — reducing guidance dilution on wide PRs.

De-duplication is what controls review volume, and it is on by default. A finding cap cannot do that job: it cannot tell a change with fifteen real problems from one with three findings restated five times, and truncates both to the same number — deleting genuine defects in the first case and leaving the redundancy in the second. So maxFindings defaults to 0 (uncapped) and dedup removes what is repeated instead.

Two layers, selected by mode. heuristic is deterministic and free: same file, nearby lines, and token overlap at or above similarityThreshold collapse into one finding, keeping the most severe representative. llm (the default) runs that pass first and then asks the model to group equivalents that share no vocabulary — one extra call per review, degrading to heuristic if it fails or the provider has no model. scope: within-file (the default) preserves per-file comments; cross-file also merges a concern spanning files. The deterministic layer is always within-file: two files exhibiting the same pattern is usually worth flagging in both places, not one finding written twice. Everything merged away is recorded in the suppressed-findings audit with the survivor's id.

Override per run with review --mode none|heuristic|llm [--scope within-file|cross-file], or validate a strategy on labelled findings with dedup-eval. The semantic layer is LLM-only rather than embedding/hybrid on the evidence: on labelled findings, embedding cosine could not separate true duplicates from distinct issues that merely share vocabulary, while LLM adjudication scored precision = recall = 1.0.

Persona build (incl. matcher mining)

personaBuild:
  enableMatcherMining: true           # github-copilot-sdk only
  matcherMiningConcurrency: 10        # fresh Copilot CLI instance per cluster
  minMatcherClusterSupport: 3         # min distinct comments before mining a cluster
  minMatcherRecall: 0.5               # back-test gate (recall over source PRs)
  maxMatcherFalsePositiveRate: 0.25   # back-test gate (firing on non-commented files)
  # extraction floors:
  minDeterministicCategoryRescueCount: 3
  minDeterministicMemoryKindRescueCount: 3
  minMemoryStatementLength: 20
  minRubricTitleLength: 12
  minRubricDescriptionLength: 20

Retrieval

retrieval: { maxContextItems: 40, keywordTopK: 50, semanticTopK: 50, finalTopK: 40 }

Copilot environment variables

  • PERSONA_COPILOT_GITHUB_TOKEN — explicit GitHub token for the SDK
  • PERSONA_COPILOT_CLI_PATH / PERSONA_COPILOT_CLI_URL — spawn a specific Copilot CLI binary over stdio / connect to a running runtime. If neither is set and the tool does not bundle the CLI (a CopilotSkipCliDownload build), a copilot/copilot.exe on PATH is discovered automatically (bring-your-own-Copilot); otherwise the SDK's bundled runtime is used.
  • PERSONA_COPILOT_HOME — Copilot state directory
  • PERSONA_COPILOT_LOG_LEVEL — CLI log level (default error)
  • PERSONA_COPILOT_TIMEOUT_MINUTES — override the per-call timeout (minutes); unset scales with the review/ask reasoning effort (low→5, medium→8, high→12, xhigh→15, max→20)
  • PERSONA_COPILOT_SDK_DISABLED=true — force deterministic fallback (tests / offline)
  • PERSONA_DUMP_RAW_FINDINGS_PATH — debug: write the raw per-group findings (pre-merge/dedup) to this path as JSON
  • PERSONA_LLM_CACHE_DISABLED=1 — bypass the model-response cache for a run (measuring true wall-clock cost, or debugging a stage)
  • Embedding model paths come from embedding.modelPath / embedding.vocabPath.

Commands

Command Purpose
init Create the data directory layout and a sample persona.yaml
version Print CLI version metadata
status Report local data-directory counts as JSON
validate Validate persona.yaml; report errors/warnings
ado-validate Live-probe configured Azure DevOps sources
ingest Fetch + normalize source artifacts (--code-changes-only for ADO re-ingest)
index Build evidence chunks + SQLite FTS5/embedding index
search Hybrid BM25 + semantic search over evidence (--query, --top)
build Build the persona pack (incl. matcher-spec mining)
refresh One-shot incremental ingest → index → build for a persona (or all); unchanged personas are reused
ask Answer a question (--query) from the persona's recorded experience, stating basis and confidence; several --persona values ask the team (each answers, then an attributed synthesis); readable terminal output, JSON when captured; --format auto\|text\|json, --sources for text references
review Review a diff or PR; --diff, --ado-pr/--gh-pr + --repo (or --pr-url <url> to paste the PR's browser URL; add --ado-org+--project to skip config), --repo-path <path> to let the reviewer read whole files, repeat --persona for team mode, --model/--reasoning to override the LLM, --mode none\|heuristic\|llm [--scope within-file\|cross-file] to override finding de-dup (on by default)
eval Historical replay (or --eval-file fixtures) + threshold calibration
eval-diagnose Classify hidden-comment retrieval bottlenecks
calibrate Replay to suggest review.minConfidence; --apply writes it to persona.yaml
dedup-eval Calibration: run finding de-dup over a findings JSON and report merges + pair precision/recall vs optional ground-truth labels (--mode none\|heuristic\|llm, --scope within-file\|cross-file)
models List the GitHub Copilot models available to you (id + supported reasoning efforts) for llm.defaultModel / llm.reasoning
identity list / approve / reject unresolved identity candidates
feedback Record reviewer reaction to a finding (--spec matcher, or --category LLM finding) + --outcome
export Export a persona to a portable .persona bundle (--pack brain-only, --full default)
pack Package one or more personas into a NuGet package (--package-id, repeat --persona)
pack-diff Compare two persona packs (names, persona-pack.json/dirs, or .persona bundles) and emit a changelog
install Install personas from .persona files, or a NuGet feed (--source)
list List installed/built personas in the data directory
uninstall Remove an installed persona
mcp Start the MCP server (stdio) for VS Code Copilot / the GitHub Copilot CLI

Global flags: --data <path>, --config <path>, --top <n>, --output-file <path>, --persist-run-artifacts, --run-artifacts-dir <path>, --force.

review/ask show a live progress indicator (stage + elapsed) on stderr so a slow high-reasoning run doesn't look like a hang — on by default in an interactive terminal, off when redirected (agents/CI/pipes). Force it with --progress or off with --no-progress (alias --quiet). It never touches stdout.

Eval

Fixture file schema for persona eval --eval-file cases.json:

{
  "cases": [
    { "id": "ask-idempotency", "kind": "ask", "personaId": "payments-expert",
      "query": "How should idempotency behave during retry storms?",
      "expectedContains": ["Payments Expert"], "requireCitations": true },
    { "id": "review-idempotency", "kind": "review", "personaId": "payments-expert",
      "diffPath": "change.diff", "requireCitations": true, "minFindings": 1 }
  ]
}

Data layout

persona-data/
  persona.yaml
  raw/            provider-native JSON, kept before normalization
  normalized/     *.jsonl: pull-requests, review-comments, commits, code-changes,
                  work-items, documents, transcripts, code-symbols, code-chunks
  indexes/        evidence-chunks.jsonl, evidence.db (SQLite + FTS5)
  personas/<id>/  persona-pack.json + memory/review-rubric/tone-profile/evidence-map/
                  matcher-specs/build-report.json, build-stages/, identity-map.json,
                  identity-overrides.json
  feedback/       <persona>.jsonl  (reviewer reactions)
  runs/<runId>/   persisted ask/review/eval artifacts
  cache/llm/      content-addressed model responses. Safe to delete

Persona-attributed data (raw/, normalized/, indexes/) is redirected under personas/<id>/ when a persona scope is in play, so building one persona never reads or writes another's store. models/, feedback/, runs/ and cache/ stay at the global root — they are shared infrastructure rather than facts about a persona.

Because of that split, personas status needs --persona <id> to see a persona's own store; without it only the global root is counted, and a healthy persona reports zero for every artifact type. Documents, transcripts, code symbols and code chunks are never persona-attributed and are always counted — and indexed — at the global root.

Note what is not here: no repository paths. Where a checkout sits is a fact about one machine, and a persona is meant to be shippable, so the reviewer is handed a path per run instead of remembering one.

A .persona bundle carries only the persona's own brain and history: pack/ (persona-pack + companions) and, at full tier, indexes/ and normalized/. Nothing machine-specific — no paths, no cache, no registry — can travel in one.

Run as a published tool (dnx)

Personas is published to NuGet as Personas — a single portable, framework-dependent package that runs on every supported desktop RID (win-x64/arm64, linux-x64/arm64, osx-arm64). The embedding model is baked in, so installed personas work with no extra downloads. Run it without installing using dnx (.NET 10's npx-equivalent), or install it globally:

# one-shot, no install (resolves + caches the tool on first use):
dnx Personas review --persona <name> --diff change.diff --output-file review.json

# or install once; the command is then `personas`:
dotnet tool install -g Personas
personas review --persona <name> --diff change.diff --output-file review.json

--output-file <path> always writes the full original JSON result to a file (recommended for automation); stdout then prints only {"status":"ok","outputFile":"..."}, even for ask --format text. With no --data, the data directory defaults to PERSONAS_HOME (~/.personas).

MCP server (VS Code Copilot & GitHub Copilot CLI)

personas mcp starts a Model Context Protocol server over stdio (built on the official ModelContextProtocol .NET SDK) so coding agents can use an installed persona. It exposes read/compute-only tools:

Tool What it does
review_pull_request Review a change as a persona — a unified diff (diffPath/diffText) or a live Azure DevOps / GitHub PR (azureDevOpsPullRequestId/gitHubPullRequestId + repository); returns evidence-grounded findings
team_review Review one change with several personas at once and merge their findings, each attributed to the personas that raised it
ask_persona Ask a persona a question, answered from its recorded positions and review threads; returns basis (explicit / inferred / none), confidence, unknowns and the knowledge used
ask_team Ask several personas the same question; each answers from its own record with its basis, ordered explicit-first, followed by an attributed synthesis of agreements, differences and who had no recorded experience. Answers are not merged into one voice
search_persona_evidence Search a persona's evidence corpus (no LLM call)
list_personas List installed/built personas
get_persona_profile Return a persona's rubric, memories, and tone for in-session reviewing

Install the tool first so the server's stdout stays a clean JSON-RPC channel (prefer an installed personas mcp over dnx as the launch command), then register it with your client:

// VS Code — .vscode/mcp.json (the GitHub Copilot CLI takes an equivalent MCP config)
{
  "servers": {
    "personas": { "type": "stdio", "command": "personas", "args": ["mcp"] }
  }
}

Then, in agent mode, ask e.g. "Review this PR as matan." The review_pull_request / team_review / ask_persona tools drive the persona's Copilot engine, so they need GitHub Copilot auth (the same login you already use); the other tools are pure reads. The server resolves personas from --data (PERSONAS_HOME by default) and writes all logs to stderr.

Distribute personas

Ship a built persona as a portable .persona bundle, or as a NuGet package containing one or more personas:

# Export to a .persona file (full tier by default; --pack for the brain only):
personas export --persona <id> --out <id>.persona

# Package one or more personas into a NuGet, then push:
personas pack --package-id MyTeam.Personas --persona <id> [--persona <id2>] --package-version 1.0.0 --out ./out
dotnet nuget push ./out/MyTeam.Personas.1.0.0.nupkg --source <feed> --api-key <key>

# Install (from a file or a feed), then run by name with no persona.yaml:
personas install ./<id>.persona
personas install MyTeam.Personas --source <feed>
personas list

Installing from an authenticated feed (e.g. Azure Artifacts) uses the same auth as dotnet/dnx: the Azure Artifacts Credential Provider plus your Az CLI token (az login) — no prompt. For a first-time interactive (device-flow) sign-in, add --interactive. A 401/403 returns a clean auth_error with guidance (not a stack trace).

Agent skills

skills/ contains Agent Skills that teach AI agents to drive this CLI: review-with-persona, create-persona, and distribute-personas (each a SKILL.md plus PowerShell Core helper scripts under scripts/).

Troubleshooting

  • Semantic search disabled — expected until models/all-MiniLM-L6-v2.onnx and models/vocab.txt exist; run ./scripts/download-model.ps1. Keyword search still works.
  • Azure DevOps skips ingestion — check organizationUrl isn't the sample value, project is set, authentication is supported, and the shell can authenticate (or AZDO_PAT is set).
  • Ask/review says persona pack missing — run persona build --persona <id> first.
  • Copilot falls back to deterministic — auth/SDK startup failed; the run still completes with a warning. Force it locally with $env:PERSONA_COPILOT_SDK_DISABLED = "true".
  • Invalid YAML — run persona validate; parser errors are returned as JSON validation errors.

Development

dotnet build "PersonaAgent.slnx"
dotnet test  "PersonaAgent.slnx"
./scripts/publish.ps1 -Runtime win-x64

Limitations

  • The GitHub Copilot SDK integration uses the public preview SDK and may need updates if its API changes; it has not yet been validated against a live account end-to-end in this environment.
  • Live Azure DevOps / GitHub API behavior is covered by test doubles but should be validated against a real org before relying on it.
  • Code structural analysis covers C# (Roslyn) and TypeScript/JavaScript (a dependency-free heuristic analyzer); other languages rely on the regex/manifest/path matcher primitives.
  • GitHub inline-comment thread resolution requires GraphQL and is not yet captured (reactions are).
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.4.16 112 10/14/2026
0.4.15 87 10/14/2026
0.4.14 91 10/13/2026
0.4.13 97 10/12/2026
0.4.12 182 7/20/2026
0.4.11 222 7/20/2026
0.4.7 249 7/4/2026
0.4.6 245 7/4/2026
0.4.5 249 7/3/2026
0.4.4 250 7/3/2026
0.4.3 254 7/2/2026
0.4.2 243 7/2/2026
0.4.1 251 7/2/2026
0.4.0 295 6/18/2026
0.3.0 285 6/17/2026
0.2.0 388 6/17/2026
0.1.0 277 6/17/2026