The research skill up close: bmad-deep-recon

This is the skill the whole part exists for. Chapter 31 placed it among BMAD's core skills in one table row; here we open it all the way, because "a research skill" undersells what is actually installed. Deep Recon ships a run-folder contract, an append-only ledger, six typed research packs, three modes, a plan gate, a firewall, a verification ladder, an eight-section report format, and a small Python kit that does the counting so the model does not. Everything below was read from the installed v6.10 source; quoted lines are the skill's own words.

A research director, not a search engine

The skill's own opening line sets the altitude:

You are Deep Recon, a research director, not a search engine. Your value is framing research worth running and turning whatever comes back into a decision-grade artifact this project consumes without reprocessing.

Two words in that sentence do the work. Decision: "every engagement serves a decision, enter a market, pick a stack, scope a product, and is shaped by it from the first question to the final artifact." Research with no decision behind it has no stopping rule and no customer. Artifact: the output is a file other skills read, not a chat you scroll back through.

Deep Recon offers three services, freely combined:

  • Draft a deep-research prompt the user runs in their own tool (ChatGPT, Gemini, Grok, Perplexity), carrying this harness's standards inside the prompt.
  • Process a finished report: file it, extract its claims, distill the cited summary downstream skills read.
  • Run the research natively, through parallel web fan-out.

"Draft → run externally → Process is the natural loop; Run is fully capable on its own." The economic point survives every rewrite of the skill: the expensive part of research is long agentic browsing, and Draft lets that run on a flat-rate consumer subscription while Process pulls the result back into your repo under your standards.

The epistemics: two standing rules

Every subagent Deep Recon spawns inherits these two standing rules. They are the reason to use the skill at all:

  1. Never conclude from training data alone. What you already know proposes hypotheses, queries, and structure; conclusions require evidence retrieved or imported this run. A claim you cannot evidence is stated as an unverified belief or not at all.
  2. The research firewall. Project context (briefs, PRDs, code, memory) shapes what to ask, never what is true. It is inadmissible as evidence: every claim traces to a digest or import file with a source. Research subagents receive only their brief, with no project files and no ambient context.

The firewall is the single most important idea in this part of the book. A model that can see your architecture will flatter it; a research subagent that receives only its question cannot, because it has never met your codebase. Four more working rules round out the discipline:

  • Nothing exists until it is a file. Every digest and report section is written to the run folder the moment it lands; the conversation is a control channel, never the store. A run that dies mid-flight resumes from disk with nothing lost.
  • Extract, don't ingest. Raw reports never enter the parent context whole; subagents return relevance-filtered digests, read just in time.
  • A claim is a sentence with a source. Publisher, publication date, access date. No naked numbers.
  • Report what is real. Thin public data is reported as thin, absence of evidence is a finding, and freshness is part of truth: "a market size from three years ago is history, not fact."

The six typed packs

The skill knows how to research generically; the pack is where it becomes opinionated for a subject. Each of the six shipped packs is a policy card carrying five things: prioritized dimensions (pruned to the decision), non-obvious source craft, freshness bars per claim class, two-source classes (claims that need independent corroboration), and Feeds (which downstream BMM document consumes the result). They apply in all three modes: a Draft prompt carries the pack's craft, a Run obeys it, a processed report is checked against it.

PackForFreshness headlineTwo-source class
marketenter/skip a market, position, pricesize/growth ≤ 18 mo, pricing ≤ 3 momarket size & growth figures
domaincommit to an industry, brief a teamstructure ≤ 3 yr, regulatory: verify currentregulatory & compliance assertions
technicaladopt a tech, ground an architectureversions ≤ 1 mo, landscape ≤ 12 moversion/compat & performance numbers
competitiveteardown of named competitorspricing ≤ 3 mo, trajectory ≤ 6 motraction & market-share claims
user-voicejobs-to-be-done, evidence-based personassentiment ≤ 18 moprevalence claims ("most users…")
academic-litground an approach in papersstate-of-art ≤ 12 mo (ML ≤ 6 mo); seminal work unbarredany empirical claim a conclusion rests on

Our feed's "does image signal help, and what profile shape fits drift?" is academic-lit and domain work. Here is the academic-lit pack in full, so the structure is concrete rather than summarized:

Dimensions (priority order, prune to the decision):
  1 The canon — seminal papers and the best recent surveys
  2 State of the art — current best results, benchmarks, how they're measured
  3 Methods & limitations — what leading approaches assume, where they break
  4 Open problems & live debates — what the field disagrees about now
  5 Who works on this — the labs whose output to watch

Craft (the non-obvious):
  find one good survey before reading twenty abstracts; chase citations both
  directions; label preprint vs peer-reviewed on every citation (arXiv is not
  acceptance); take benchmark numbers from the original paper, never a
  competitor's comparison table; check retraction/replication on load-bearing
  claims; a result only ever shown by one lab is a lead, not a fact.

Two-source classes: any empirical claim a conclusion rests on —
  independent replication, not the same lab twice.

Feeds (bmm): technical & architecture bets · content that cites ·
  build-vs-adopt judgments on research-grade techniques.

Read the craft line again: "a result only ever shown by one lab is a lead, not a fact." That is a working researcher's instinct, written down and applied by every subagent automatically. The six packs are starting points; a team defines its own (the customization section builds a recommender pack that encodes this book's evidence standards).

Two decision shapes: explore vs. select

Orthogonal to the pack is the decision shape. The default is explore: understand, assess, validate. The alternative is select: choose between candidates (technologies, vendors, libraries, an EMA-vs-multi-vector profile). When the shape is select, the skill layers a five-step method over whichever pack fits the subject:

  1. Requirements frame. What must the winner do, under what constraints (scale, compliance, budget, team skills, exit-cost tolerance)? Split hard gates from weighted preferences. Sources are the project and the user; web research does not set requirements. Agree the frame before any candidate research runs.
  2. Candidate screen. Establish the credible field (leaders, challengers, one wildcard), cut anything failing a hard gate, screen to 3-5 finalists, record the cuts.
  3. Evidence per criterion. Score finalists against the frame using the pack's dimensions, verified against current versions; where vendor claims and independent experience diverge, the divergence is a finding.
  4. Cost & lock-in. Total cost over the product's horizon plus the cost of leaving.
  5. Verdict. A weighted decision matrix that shows the scoring, not just totals ("a matrix the user can re-weight is worth more than a verdict they must trust"), the pick, the named runner-up and when it wins instead, the strongest argument against the pick, and the cheapest reversibility hedge.

Select adds its own two-source classes (pricing, performance numbers, any cell that decides between the top two finalists) and its own staleness rule (a selection report older than two quarters is refreshed before anyone acts on it).

The three modes, in detail

ModeWhat it doesReads
Draftcompose a deep-research prompt carrying the pack's craft, save it as brief.md, hand it to the user paste-readythe decision + pack
Processfile a finished report into imports/, extract claims to digests, distill research.mdthe import + pack
Runresolve effort, hold the plan gate, run the acquisition loop once per dimensionthe web + pack

Draft opens the floor ("invite the decision they're facing and anything they already have"), nails decision/topic/type, asks which tool the prompt is for (it changes phrasing: hosted deep-research agents handle wide scopes; social-native tools like Grok earn user-voice dimensions), then composes the prompt from the pack with a non-negotiable citation demand: "every claim with source URL and publication date, contrary evidence reported, gaps admitted rather than padded." It structures the requested output so Process can extract it cleanly.

Process files the original into imports/ "untouched, full fidelity preserved there and nowhere else," records provenance (what tool produced it, when, and what the user wants decided), extracts claims into digests, checks coverage against the pack's dimensions, and distills research.md. "Nobody ever reprocesses the import." Two imports from different tools agreeing is genuine confirmation; their disagreement is a finding.

Run is where the machinery is, and it deserves its own section.

Run mode: effort, the plan gate, the fan-out

The effort knobs

Rigor in Deep Recon is "bought consciously through the knobs, never accreted through extra passes." A preset bundles the three core knobs; any knob can be pinned individually, and what the user says in the request beats both:

Presetsubagentssources/rounddepth (rounds)
quicklow (2)51
standard (default)normal (3)82
deephigh (6, cap 10)123

Depth and sources are caps, not quotas: a dimension stops early on coverage or novelty exhaustion. The grounding is explicit in the skill: "orchestrator-worker research systems document 3-5 parallel workers as the sweet spot; more only for genuinely wide work." Orthogonal knobs tune the rest:

KnobValuesEffect
validationnormal / high / maxhow hard claims are checked (below)
red_teamoff / offer / onfresh-context skeptics hunt disconfirming evidence
use_workflowsoff / offer / onrun the fan-out through deterministic orchestration
preferred_sources / banned_sourcesdomains or descriptionsconsulted first / never cited
output_formatauto / html / md / bothwhether an HTML briefing is rendered
audiencefreeformshapes the synthesis register
external_sourcestool + when-to-useinternal KBs or search MCP tools consulted alongside web
doc_standards / external_handoffsdirectivespolish passes and publish steps at finalize

The plan gate: the one hard stop

Run mode "hard-stops after planning." This is the single mandatory checkpoint, kept light: it presents the decision, the pack-derived dimensions pruned to it, the shape, the decomposition topology, the knobs in force and where each came from, which search surfaces exist, and an honest time estimate. Nothing crawls until you approve. The topology choice is the interesting part:

  • breadth-first: independent sub-questions; assistants split the dimensions.
  • depth-first: one question that needs several perspectives; assistants split by angle or methodology, not by dimension.
  • straightforward: a focused ask; one assistant, a handful of calls, no fan-out. "Never overinvest in a simple query."

On approval the skill binds the run folder, seeds research.md from a template, initializes the ledger, logs the approved plan, and tells you the path. "The user watches the document build, not a spinner."

Rounds, lead-following, and stopping

Each dimension runs in rounds up to the depth cap. Round 1 goes broad-first: "short, wide queries to map what exists, narrowing as the shape emerges, not long specific queries that return nothing." After each round the lead harvests leads (new entities, contradictions, opened questions); "contradictions get priority." A dimension stops before its cap when either holds:

  • Coverage: its questions are answered, critical claims confirmed per the validation level.
  • Novelty exhaustion: a full round surfaced no new load-bearing claim or lead.

The skill says which one ended it, and reports a cap hit with open questions as an open question rather than dropping it silently. There is also a stop-and-write valve: if a run drags well past its estimate, "stop spawning, synthesize from the digests already on disk, and report the remainder as open questions with a route. A shorter honest report beats a longer stale one."

The fan-out and its firewall

Each researcher assistant runs behind the firewall: "it gets its brief and nothing else." The brief is precisely specified, and two parts are worth lifting out. First, budgets scaled to the task: "under 5 for a simple lookup, ~5 medium, ~10 hard, 15 for genuinely multi-part, 20 never exceeded. Either budget spent → synthesize what it has." Second, the query craft: "short queries (roughly five words or fewer) beat hyper-specific ones that return nothing; broaden when results are sparse, narrow when abundant; never repeat an identical query; after every tool result, pause and evaluate before firing again."

Every assistant returns a digest, not raw results, in a fixed contract:

{claim, source, publisher, pub_date, accessed, confidence, class}
  + leads worth chasing
  + what it looked for and could not find

and every assistant and the lead apply one source-quality card: prefer primary sources (filings, regulator text, official docs, original papers) over aggregators; downgrade on sight for speculative language, marketing register, passive voice with unnamed sources, and aggregators recycling one upstream report. Crucially: "answer engines (Perplexity Sonar, Grok) are aggregators too, however good the synthesis: chase their citations and cite those, never the engine."

Don't be confused: parallel subagents vs. party mode. Party mode (Chapter 31) puts several personas in one conversation taking turns; it is still one context window. Deep Recon's fan-out runs assistants in separate context windows at once, each firewalled to its brief. One buys debate; the other buys throughput and independence. Chapter 34 measures when the second is worth its ~10x token cost.

Verification, synthesis, finalize

Verification happens as material lands, per dimension, in fresh-context verifier subagents, never as an end-of-run rewrite pass ("late-pass rewrites degrade reports; landing-time checks improve them"). The memlog claim entries are the ledger. The level is set by validation:

  • normal (default): spot-check the load-bearing claims only, one independent check each. Fast by design.
  • high: cross-check every claim in the pack's two-source classes, and red-team every major conclusion.
  • max: cross-check every ledger claim, red-team at full breadth, and apply primary-source-priority ranking (secondary reporting alone does not verify where a primary source exists).

"Independent" is defined precisely: "a different publisher with different underlying data, not a syndication, quote, or republication of the first source." Each claim lands in one of four states, and the state, not a rewrite, is what changes: verified (independent source agrees within tolerance), disputed (independent sources materially disagree, report both, never average), unverified (no independent check within budget, flagged, joins the staleness map), or overturned (evidence contradicts it, corrected in text, original noted). The red-team pass is the single adversarial mechanism: for each major conclusion, a fresh-context skeptic gets "the conclusion and a search budget, no supporting evidence," and hunts the bear case. What survives gets its counter-argument acknowledged; what does not is revised before the report states it. "Zero findings after a real search is itself reportable."

Synthesis assembles research.md in a fixed eight-section order, "succinct is the contract, findings and verdicts, not essays":

1 Executive summary       decision-first; written last, placed first
2 Dimension sections      written during the loop, now reconciled
3 Cross-dimension insights what only the COMBINATION shows
4 Contrary evidence       surviving counter-arguments from the red-team pass
5 Recommendations         each bound to a decision and a downstream artifact
6 Open questions          what it couldn't answer + what would answer it
7 Source appendix         [n] | finding | publisher | date | accessed | confidence
8 Staleness map           claims that age fastest, with re-check dates

Section 3 is "the harness earning its keep": the market is growing but the regulatory dimension caps the reachable segment; the technically superior option loses on ecosystem health. If there are none, the skill says so rather than manufacture them. Finalize then runs a two-part citation check, mechanical then semantic: a script diffs inline [n] markers against the appendix, and a fresh-context subagent checks that each cited source actually says what the text claims (a mismatch downgrades confidence and is logged, it never licenses a rewrite).

The run folder and the ledger

Every mode writes the same folder shape, and the folder is the state:

{planning_artifacts}/research/{type}-{topic-slug}-{date}/
├── brief.md          drafted prompts (Draft mode)
├── imports/          finished reports, full fidelity (Process mode)
├── digests/          extracted claims, one file per assistant per round
├── research.md       the canonical cited summary — what downstream reads
└── .memlog.md        the append-only process ledger

The memlog is the process memory: an append-only, chronological log where "every decision, source batch, load-bearing claim, plan change, and assumption is one line," written through a shared script so the model never re-reads the file mid-run. Its entry types are decision, source, claim, assumption, question, and event, and a claim carries a machine-readable tail so tooling can read it:

- (claim) ref=[5] status=unverified class=architectural pub=2020-08 -- PinnerSage keeps one medoid per interest cluster

A status change is a new line with the same ref=, and last status wins. That single convention is what lets a script tally claims, and it is why the skill delegates all counting to Python.

The deterministic half, run

Counting entries, computing staleness, and checking citations are exact, repeatable jobs; the skill hands them to recon_kit.py rather than trust an LLM to be exhaustive. recon_kit_lab.py rebuilds the three decision-relevant helpers from scratch, over a toy run about our own feed, so you can watch them work. It is stdlib-only:

"""recon_kit_lab.py -- a from-scratch miniature of BMAD's research tooling.

BMAD's `bmad-deep-recon` research skill keeps its *judgment* in the language
model and its *bookkeeping* in a tiny deterministic Python helper, `recon_kit.py`.
The split matters: counting claims by status, computing when a claim goes stale,
and checking that every citation resolves are mechanical jobs an LLM should never
do by hand (it miscounts, and it cannot be trusted to be exhaustive). This file
rebuilds the three decision-relevant helpers from scratch, over a toy research run
about our own feed, so you can watch the machinery work end to end.

Two teaching fixtures stand in for a real run folder:

  * MEMLOG   -- the append-only `.memlog.md` ledger. One line per event, in the
               order it happened. Claim lines carry a machine-readable tail
               `ref=[n] status=... class=... pub=YYYY-MM -- <text>`; a later status
               change is a NEW line with the same `ref=`, and last status wins.
  * RESEARCH -- the `research.md` report, with inline `[n]` citations in the prose
               and a numbered source appendix at the bottom.

Everything here is stdlib only; run it with `python3 recon_kit_lab.py`.
"""
from __future__ import annotations

import calendar
import re
from datetime import date

# Pinned so the printed output is reproducible. The real `recon_kit.py staleness`
# defaults to the real today and accepts --today; a research run stamps the date
# it was made.
TODAY = date(2026, 7, 24)

# --- Fixture 1: the .memlog.md ledger ---------------------------------------
# A domain/academic-lit run answering three questions about the feed: image
# embeddings (Q1), EMA half-life (Q2), diversity re-ranking (Q3). Note ref=[3]
# appears twice: it entered `unverified`, then a verification pass flipped it to
# `verified`. That is two ledger LINES but one tracked claim.
MEMLOG = """\
---
topic: signal & profile shape for the next feed
type: domain
updated: 2026-07-24T16:00
---

- (decision) plan approved: 3 dimensions, breadth-first, standard preset
- (question) Q1 do image embeddings lift engagement vs a strong text baseline?
- (source) batch of 4 read on visual features in ranking
- (claim) ref=[3] status=unverified class=empirical pub=2016-02 -- VBPR folds CNN image features into a BPR factorization; offline lift
- (claim) ref=[4] status=verified class=empirical pub=2019-08 -- Pinterest unified visual embedding, validated in an online A/B
- (claim) ref=[5] status=unverified class=architectural pub=2020-08 -- PinnerSage keeps one medoid per interest cluster, not one averaged vector
- (assumption) thumbnail content reflects story content (weakest assumption from forge)
- (question) Q2 which EMA half-life fits news interest drift?
- (source) batch of 2 read on temporal decay
- (claim) ref=[6] status=unverified class=empirical pub=2005-10 -- time-decayed interaction weighting (time-weighted CF)
- (claim) ref=[8] status=disputed class=contested pub=2022-03 -- a strong text baseline erases the image lift
- (claim) ref=[3] status=verified class=empirical pub=2016-02 -- re-checked VBPR lift against the primary paper
- (claim) ref=[9] status=unverified class=empirical pub=2026-05 -- MMR diversity re-rank costs <1% short-term CTR in a recent study
- (event) session complete
"""

# --- Fixture 2: the research.md report --------------------------------------
# The prose cites [3][4][5][6][8][9] and, deliberately, a stray [11] that has no
# appendix row (a dangling marker). The appendix carries a row [10] that the prose
# never cites (an orphaned row). Both are exactly what the finalize gate catches.
RESEARCH = """\
## Q1: image signal in ranking

Visual features folded into ranking have a decade of positive evidence, from
VBPR's CNN features in a BPR factorization [3] to Pinterest's unified visual
embedding, validated in an online A/B test [4]. Lift against a *strong* text
baseline is thinner and contested [8]; recommend our own ablation [11].

## Q2: profile shape

PinnerSage argues against a single averaged user vector [5]. Recency weighting
itself is well anchored [6]. Diversity re-ranking looks affordable [9].

## Sources

| # | finding | publisher | pub | accessed | confidence |
|---|---|---|---|---|---|
| [3] | VBPR visual features | [He & McAuley, AAAI](https://example.org/vbpr) | 2016-02 | 2026-07-24 | high |
| [4] | Pinterest visual embedding | [Zhai et al., KDD](https://example.org/pins) | 2019-08 | 2026-07-24 | high |
| [5] | PinnerSage multi-embedding | [Pal et al., KDD](https://example.org/pinnersage) | 2020-08 | 2026-07-24 | high |
| [6] | time-weighted CF | [Ding & Li, CIKM](https://example.org/twcf) | 2005-10 | 2026-07-24 | medium |
| [8] | text baseline erases lift | [contested thread](https://example.org/contested) | 2022-03 | 2026-07-24 | low |
| [9] | MMR diversity cost | [recent study](https://example.org/mmr) | 2026-05 | 2026-07-24 | medium |
| [10] | two-tower retrieval survey | [survey](https://example.org/twotower) | 2021-03 | 2026-07-24 | medium |
"""

# Freshness windows in months, per claim class. These come straight from a pack:
# state-of-the-art empirical claims re-check within two years, a settled
# architectural design lasts a decade, a contested claim re-checks after our own
# experiment (short). A class with no window is left un-scheduled, not assumed fresh.
WINDOWS = {"empirical": 24, "architectural": 120, "contested": 6}


# --- tally: count the ledger, last status wins per ref ----------------------
ENTRY_RE = re.compile(r"^- \((?P<type>[\w-]+)\)\s*(?P<body>.*)$")


def tally(memlog: str) -> dict:
    """Count memlog entries by type, and claim entries by status.

    For a given `ref=` the LAST status seen wins, because a status change is
    appended as a fresh line rather than editing history. Claims with no ref are
    counted on their own. This is what feeds the headless run's JSON claim counts.
    """
    by_type: dict[str, int] = {}
    by_ref: dict[int, str] = {}
    unref: dict[str, int] = {}
    entries = 0
    for line in memlog.splitlines():
        m = ENTRY_RE.match(line)
        if not m:
            continue
        entries += 1
        etype = m.group("type")
        by_type[etype] = by_type.get(etype, 0) + 1
        if etype != "claim":
            continue
        body = m.group("body")
        status = (re.search(r"status=([\w-]+)", body) or [None, "unknown"])[1]
        ref = re.search(r"ref=\[?(\d+)\]?", body)
        if ref:
            by_ref[int(ref.group(1))] = status  # last write wins
        else:
            unref[status] = unref.get(status, 0) + 1
    claims = dict(unref)
    for status in by_ref.values():
        claims[status] = claims.get(status, 0) + 1
    return {
        "entries": entries,
        "by_type": dict(sorted(by_type.items())),
        "claims": dict(sorted(claims.items())),
        "claims_total": sum(claims.values()),
    }


def ledger_claims(memlog: str) -> list[dict]:
    """Extract the tracked claims (ref, class, pub_date), last line wins per ref."""
    latest: dict[int, dict] = {}
    for line in memlog.splitlines():
        m = ENTRY_RE.match(line)
        if not m or m.group("type") != "claim":
            continue
        body = m.group("body")
        ref = re.search(r"ref=\[?(\d+)\]?", body)
        cls = re.search(r"class=([\w-]+)", body)
        pub = re.search(r"pub=(\d{4}-\d{2})", body)
        if ref and cls and pub:
            latest[int(ref.group(1))] = {
                "ref": int(ref.group(1)), "class": cls.group(1), "pub_date": pub.group(1)}
    return [latest[r] for r in sorted(latest)]


# --- staleness: when does each claim need a re-check? -----------------------
def add_months(d: date, months: int) -> date:
    total = d.month - 1 + months
    year, month = d.year + total // 12, total % 12 + 1
    return date(year, month, min(d.day, calendar.monthrange(year, month)[1]))


def staleness(claims: list[dict], windows: dict[str, int], today: date) -> dict:
    """Compute each claim's re-check date; flag the stale ones; find the earliest.

    This is Refresh's work order: the stale set is exactly what a refresh run
    re-verifies, leaving everything else at its recorded status untouched.
    """
    results, stale_count, earliest = [], 0, None
    for c in claims:
        pub = date(int(c["pub_date"][:4]), int(c["pub_date"][5:7]), 1)
        months = windows.get(c["class"])
        if months is None:
            results.append({**c, "recheck": None, "stale": None})
            continue
        recheck = add_months(pub, months)
        stale = recheck <= today
        stale_count += stale
        earliest = recheck if earliest is None or recheck < earliest else earliest
        results.append({**c, "recheck": recheck.isoformat(), "stale": stale})
    return {"claims": results, "stale_count": stale_count,
            "earliest_recheck": earliest.isoformat() if earliest else None}


# --- citations: do the markers and the appendix agree? ----------------------
MARKER_RE = re.compile(r"\[(\d+)\](?!\()")  # [3] but not a [3](url) link
ROW_RE = re.compile(r"^\|\s*\[?(\d+)\]?\s*\|")  # an appendix row starts with | [n] |


def citations(research: str) -> dict:
    """Cross-check inline [n] markers against the source appendix.

    A dangling marker cites a source that is not in the appendix; an orphaned row
    is a source nobody cited. Both are defects the finalize step must fix before
    the report is trusted.
    """
    rows = {int(m.group(1)) for line in research.splitlines()
            if (m := ROW_RE.match(line.strip()))}
    markers = {int(n) for line in research.splitlines()
               if not ROW_RE.match(line.strip())
               for n in MARKER_RE.findall(line)}
    dangling = sorted(markers - rows)
    orphaned = sorted(rows - markers)
    return {"markers": sorted(markers), "appendix_rows": sorted(rows),
            "dangling_markers": dangling, "orphaned_rows": orphaned,
            "ok": not dangling and not orphaned}


def main() -> None:
    print("== tally: what is in the ledger? ==")
    t = tally(MEMLOG)
    print(f"  entries: {t['entries']}")
    print(f"  by type: {t['by_type']}")
    print(f"  claims by status (last status wins per ref): {t['claims']}")
    print(f"  tracked claims: {t['claims_total']}  "
          f"(7 claim lines -> 6 claims; ref=[3] was updated, not duplicated)")

    print("\n== staleness: the refresh work order (as of "
          f"{TODAY.isoformat()}) ==")
    s = staleness(ledger_claims(MEMLOG), WINDOWS, TODAY)
    for c in s["claims"]:
        flag = "STALE" if c["stale"] else "fresh"
        print(f"  [{c['ref']}] {c['class']:<13} pub {c['pub_date']}  "
              f"re-check {c['recheck']}  {flag}")
    print(f"  -> {s['stale_count']} stale; refresh these first. "
          f"earliest re-check was due {s['earliest_recheck']}.")
    print("  note: [3] is verified AND stale -- verification is not freshness.")

    print("\n== citations: the finalize gate ==")
    c = citations(RESEARCH)
    print(f"  markers cited in prose: {c['markers']}")
    print(f"  rows in the appendix:   {c['appendix_rows']}")
    print(f"  dangling markers (cited, no source row): {c['dangling_markers']}")
    print(f"  orphaned rows (source listed, never cited): {c['orphaned_rows']}")
    print(f"  clean? {c['ok']}  -> fix [11] (add the source) and [10] "
          f"(cite it or cut it) before shipping.")


if __name__ == "__main__":
    main()

Running it:

$ python3 recon_kit_lab.py
== tally: what is in the ledger? ==
  entries: 14
  by type: {'assumption': 1, 'claim': 7, 'decision': 1, 'event': 1, 'question': 2, 'source': 2}
  claims by status (last status wins per ref): {'disputed': 1, 'unverified': 3, 'verified': 2}
  tracked claims: 6  (7 claim lines -> 6 claims; ref=[3] was updated, not duplicated)

== staleness: the refresh work order (as of 2026-07-24) ==
  [3] empirical     pub 2016-02  re-check 2018-02-01  STALE
  [4] empirical     pub 2019-08  re-check 2021-08-01  STALE
  [5] architectural pub 2020-08  re-check 2030-08-01  fresh
  [6] empirical     pub 2005-10  re-check 2007-10-01  STALE
  [8] contested     pub 2022-03  re-check 2022-09-01  STALE
  [9] empirical     pub 2026-05  re-check 2028-05-01  fresh
  -> 4 stale; refresh these first. earliest re-check was due 2007-10-01.
  note: [3] is verified AND stale -- verification is not freshness.

== citations: the finalize gate ==
  markers cited in prose: [3, 4, 5, 6, 8, 9, 11]
  rows in the appendix:   [3, 4, 5, 6, 8, 9, 10]
  dangling markers (cited, no source row): [11]
  orphaned rows (source listed, never cited): [10]
  clean? False  -> fix [11] (add the source) and [10] (cite it or cut it) before shipping.

Three lessons the output makes concrete. tally shows seven claim lines collapsing to six tracked claims, because ref=[3] was a status update, not a new claim: this is the count the headless run reports as JSON, and hand-counting would get it wrong. staleness is Refresh's work order: it flags four aging claims and, pointedly, marks claim [3] both verified and stale, because a claim can be confirmed today and still be due for a re-check tomorrow. citations is the finalize gate: it catches a marker ([11]) with no source row and a source row ([10]) nobody cited. The real recon_kit.py adds slug (deterministic run-folder naming so draft, process, and refresh land in the same folder) and escape-sources (safe HTML for the briefing), on the same principle: give the machine the mechanical half and the model never has to fake it.

Lifecycle: refresh and deepen

Two intents operate on an existing run folder rather than starting over. Refresh reads research.md and the ledger, "never re-researches from scratch": it builds the stale set mechanically (exactly the staleness output above), confirms it in one exchange, re-verifies only those claims, and appends a delta report (confirmed / changed / overturned, new sources). Claims outside the set keep their status, and an overturned load-bearing claim "triggers an explicit warning naming the downstream artifacts that consumed it." Deepen drills into one dimension (or adds a new one) without touching the rest, "a deepening that changes no conclusion says so." Together they are why the staleness map is a work order and not decoration: research becomes a thing you maintain, not a thing you redo.

The HTML briefing

When output_format calls for it, finalize renders research-briefing.html, "a full-fidelity presentation of the report, never a second source of truth, same claims, same numbers, same citations." It is a single self-contained file (inline CSS/JS, no external requests, renders offline forever), confidence is visual (every claim carries a badge, and "unverified and disputed must be more prominent than verified, not less"), and source URLs are escaped through the kit's escape-sources because "source URLs are untrusted content, never hand-escape them." The markdown is the record; the briefing is its regenerable face.

Customize: a recommender evidence pack

The six shipped packs are starting points. Using the sparse-override mechanism from Chapter 31, a recommender team can encode this book's evidence standards as a team override in _bmad/custom/bmad-deep-recon.toml:

[workflow]
# Decision context only — the firewall keeps it out of findings.
persistent_facts = [
  "file:{project-root}/docs/research/evidence-policy.md",
  "External half-lives are hypotheses, never our defaults.",
]

# Prefer deployed-system reports and primary papers; distrust roundups.
preferred_sources = ["deployed-system engineering blogs", "peer-reviewed venues"]
banned_sources = ["SEO listicles", "vendor comparison pages as capability proof"]

# Add a recommender-specific research type alongside the shipped six.
[[workflow.research_types]]
code = "recsys-evidence"
name = "Recsys Evidence"
when = "Ranking, retrieval, or profile-representation bets for our feed."
pack = "file:{project-root}/_bmad/custom/packs/recsys-evidence.md"

The pack file itself carries the same five parts as a shipped pack, with our standards written in: dimensions ordered offline-metric-on-a-held-out-split (Chapter 13) → rubric scorecard (Chapter 27) → online guardrails; a source rule that "offline-only papers rank below deployed systems"; and a Feeds line binding findings to the PRD and the Chapter 28 profile schema. Because overrides append to arrays and replace by key, this adds a seventh type without touching the six, and it survives every upgrade.

We now have the skill in full: its rules, its packs, its modes, its folder, its ledger, and its Python. The next chapter stops describing and starts doing: one research cycle for our feed, from four candidate features to a story a dev agent implements, with a single finding traced down every arrow of the SDD chain. 👉