recon
Trace an existing system end to end — entry points, call paths, every read and write of the state involved, the contracts crossed, the failure and rollback paths — and write it down as a Recon Map bef
SKILL.mdskills/1-hourly/recon/SKILL.md
GitHubRecon
Read the system before you plan a change to it. A plan written from the prompt plus three grepped files is fiction with headings — its steps reference call paths nobody traced, state nobody inventoried, and contracts nobody knew were there, and its blast radius section is invented. Recon makes the trace a deliverable with named artifacts, produced before the first plan heading.
The one rule: no plan step may name a blast radius that includes code nobody read. If a step touches a caller, the caller is in the map at file:line. If it cannot be found, it is in the Unknowns list — never silently assumed absent.
When to run it
Run recon when the ask is to plan, design, spec, refactor, migrate, or size a change to a system that already exists, and the change plausibly reaches beyond a file you have already read in full.
Skip recon when — the calibration counter-example, and it matters as much as the trigger:
- Greenfield — there is no existing system to trace. Recon has nothing to read; go plan.
- The change is confined to a file you have already read in full — and one lookup says so. Spend the single call (
search_graph, or one grep for the symbol name) before claiming this skip; any external reference it returns and the skip does not apply. Asserting the negative without the lookup is the easiest dishonest exit in this list. - A typo, copy edit, comment, or formatting change.
- The plan is not about code (a process, a doc, a rollout schedule).
- A Recon Map for this subsystem exists and no relevant commit has landed since. Re-read it; do not re-run.
- You traced this exact subsystem earlier in this session, nothing has landed since, and the edges are still in your context. Write the map from what you hold rather than re-running the fan-out.
- The user hands you the edges. Skip the fan-out, not the map: fold their edges into the map, cite them to the user, and mark them
supplied— unverified until read.
Say "recon skipped — [one-line reason]" and go. A recon on a contained rename whose references are already enumerated is ceremony, and ceremony is how a gate gets ignored when it counts. A rename whose callers are not yet enumerated is exactly what recon is for.
Step 1 — Scope the trace
Name the subject in one line: the function, module, table, endpoint, or feature the change lands on. Name the change class: local edit, cross-module change, contract change, state/authority change, or replacement. If the class is state/authority, run spike-360 first — it decides whether a new source of truth should exist at all; recon maps the system either way once that is settled.
Ask at most one clarifying question. If the subject is ambiguous, pick the likeliest reading, state it, and trace that — unless the two readings sit on opposite sides of a boundary (application versus infrastructure, this service versus another). There, guessing wrong spends the whole fan-out on the wrong system, so name both readings and ask.
Step 2 — Seed from the knowledge graph if it is installed
If the codebase-memory MCP server is available, use it first — it answers "who touches this" in one call instead of twenty greps:
index_status/list_projects— indexed, and indexed at what revision? Compare it to current HEAD and the dirty worktree; an index older than the last relevant commit is a stale lead, and every edge from it is unconfirmed until read. If unindexed and the repo is large, offerindex_repositoryonce with its cost; if declined, fall through to grep and record the degraded mode in the map.get_architecture— the shape of the system before the details.search_graph(name_pattern | label | qn_pattern)— locate the subject's symbols.trace_path(function_name, mode=calls | data_flow | cross_service)— the edges. This is the tool that earns the skill its name.get_code_snippet(qualified_name)/search_code(pattern)— exact source, and graph-augmented grep for the rest.
The graph is a lead, not a citation. Every edge a plan step will depend on gets confirmed by reading the file; an edge that exists only in the graph is marked graph-only. Depending on the indexer, it may miss config, SQL strings, templates, shell and CI scripts, cron, IaC and deploy manifests, generated code and its generator, database triggers/views/procedures, macros, runtime registries, reflection and dynamic dispatch, and consumers in other repositories. Anything in that list you did not verify by hand is an Unknown — in graph mode and grep mode alike.
No graph installed? Say so in one line and trace with grep, glob, and reads. The output contract does not change.
Step 3 — Fan out, sized to the radius
Recon is wide, shallow, and parallel — the ideal sub-agent shape. Launch the lanes in one message so they run concurrently, each read-only, each returning the Step 4 envelope. Use sub-agents available to the current model through its own runtime and model provider; inherit the current model where supported, without requiring a named model, model lab, or vendor-specific agent type.
Scale the lane count to the radius, and say which you ran: a two-file change with one caller is one lane in your own context; a subsystem with external consumers is all four.
| Lane | Question it answers | Owns |
|---|---|---|
| A. Entry & call paths | How does control reach this code? | Callers and entry points — routes, CLI, cron, hooks, event handlers, tests — plus runtime registration, plugin dispatch, reflection, and anything invoked by name from config |
| B. State & data | What reads and writes the state involved? | Read sites and write sites, active readers and writers, schema, migrations, caches, serialized formats, database triggers/views/procedures, and whether there is a single write path. Step N+1: verify whether consumer behavior accepts, selects, and advances the newly written status/value to a terminal or recovery path (a UI badge alone proves neither). |
| C. Contracts & boundaries | What crosses a line if this changes? | Public APIs, exported symbols with external consumers, events/queues, background worker queues, delayed/asynchronous consumers, config keys, env vars, feature flags, cross-service and cross-repository consumers. Step N+1: verify whether arguments passed into engines/solvers act as constraints (restricting) or generators (creating candidates); check rollout coexistence and rollback feasibility when persisted contracts change. |
| D. Build, failure & operations | How is this built, how does it fail, who notices? | Build/CI config and package metadata, generated code and its generator, IaC and deploy manifests, error paths, retries, timeouts, operational tripwires, lock budgets, existing tests covering the subject, logs/metrics/traces, and the reverse-sync rollback path. Step N+1: trace transparent delegation through wrappers and simulate what the enclosing caller or lease manager does on the next scheduled retry when an error is raised or claim held. |
Budget each lane: read-only, no edits, roughly 8 minutes, report what you found and what the budget cut off. The budget is a prompt instruction, not a timeout — which is exactly why the report-what-you-cut rule is the part that has to hold. An empty lane is a finding, not a failure. Give every lane the same honesty instruction: file:line for everything claimed; anything inferred, unverified, or graph-only is listed as an unknown, never smoothed into the findings.
No sub-agent capability? Run the lanes serially in the main context, same budget, same schema — and name in the map which lanes you curtailed and where you stopped. Serial is not licence to go shallow; it is licence to record what you skipped.
Step 3.5 — Step N+1 Downstream Verification (Default vs Deep)
Recon must never stop at the local mutation site. An LLM agent that stops at the edit site will recommend status = SENDING without checking if a polling worker actually selects and advances it, recommend raise without checking if a distributed lease manager enters an infinite retry loop, or pass items into a solver without checking if the parameter generates entities instead of constraining them.
Default Mode: Logical Downstream Boundary Invariant Checks (Mandatory on Every Seam)
Every recon touching state transitions, error paths, or solver inputs must start with one logical downstream boundary, follow transparent delegation (e.g. wrapper functions, forwarding helpers, retry decorators) until the relevant contract is established within the stated budget, and report unresolved evidence as Unknown:
Consumer Behavior & Progress Hop (Lane B):
- When a change proposes writing or transitioning state $S$ (e.g. status codes, flags, cache values), verify consumer behavior, not merely existence.
- For the exact newly written status or value, trace whether the deployed consumer accepts its schema/value, whether its query/filter/routing actually selects the record, and whether its handler advances it toward a terminal outcome or an actionable, bounded recovery path. (A poller that only queries
QUEUEDdoes not cover a newSENDINGstate even if the same worker updates other records; a UI badge alone proves neither progress nor recovery). - Identify the progress/recovery owner and terminal/timeout path.
- Exceptions: Intentional write-only audit logs or terminal archival records are permitted when documented as terminal/audit-only; distinguish "not found in bounded search" from proven absence.
- If an unhandled non-terminal state is introduced without an advancing consumer, flag it in the map as an Incomplete State Machine / Stranded State.
The Retry & Transparent Delegation Hop (Lane D):
- When a change proposes raising an exception or holding a claim on failure, trace past immediate wrapper helpers and decorators to the enclosing caller and temporal retry mechanism.
- Does the next scheduled execution cycle (e.g. Celery beat, cron, lease expiration) recover cleanly, or does it trigger an infinite stuck retry loop (e.g. retrying RECOVER on an unresolvable ID)?
The Parameter Role Check (Lane C):
- When proposing to pass state or collections into an existing engine or solver (e.g.
compose_calendar(events=...)), inspect the callee to verify whether the argument acts as a constraint (restricts/filters days) or a generator (creates new candidate entities), preventing unintentional entity duplication.
- When proposing to pass state or collections into an existing engine or solver (e.g.
Rollout Coexistence & Rollback Invariant (Lane C / SWE Policy):
- When persisted formats, status values, or external contracts change, check whether old and new readers/writers must coexist during deployment (including delayed/queued consumers).
- Trace whether each supported version can read and interpret the other version's writes, and whether rollback remains viable after new-format data has been written.
- If compatibility requires ordered deployment, quiescent/offline migration, or a point after which rollback is unavailable, state that condition and the last safe rollback point (referencing the SWE migration/rollback policy) rather than assuming universal coexistence.
Deep Mode (recon --deep): Exhaustive Multi-Hop Simulation & Dataset Auditing
Triggered explicitly via recon --deep, /recon --deep, or when analyzing multi-service architecture (reserved for broader transitive analysis, not for finishing an ordinary check hidden behind one wrapper):
- Transitive Call Chains ($N+2, N+3$): Trace downstream side effects through queues, webhooks, and third-party APIs.
- Automated Dataset Parsing: Programmatically parse non-code datasets (e.g. running scripts to audit every article in a Help JSON/CSV catalog against registered router tables, rather than spot-checking documentation summaries).
- Distributed Failure Matrix: Stress-test behavior under clock skew (DST transitions), worker crashes, network drops, and concurrent lease races.
Step 4 — What each lane returns
A common envelope plus that lane's own fields from the table above:
LANE: <A|B|C|D>
FINDINGS:
- <file:line> — <what it is> — <why it matters to the change> — [confirmed | graph-only | supplied]
confirmed = you read the file · graph-only = the index says so, nobody read it · supplied = the user gave it
UNKNOWNS: <what could not be verified, and the one command or file that would settle it>
CUT OFF AT: <where the budget stopped the lane, or "nothing">
Step 5 — Reconcile into the Recon Map
Merge the lanes yourself; do not paste them. Dedupe by file:line, resolve contradictions by reading the file, and write the map to recon-<subject-slug>.md beside where the plan will live. Long output belongs in the file, never in chat.
# Recon Map — <subject>
Commit: <sha> · Mode: <graph+read | grep-only> · Lanes: <which ran>
## Subject and change class
## The seams — where a change here escapes this file
| Seam | Location | Crosses | Breaks if |
## Call paths in
<entry point -> ... -> subject, with file:line>
## State
<read sites / write sites; the single write path, or the fact that there is not one>
## Contracts
<name — consumer — breaking-if — where it is declared>
## Build, failure and rollback today
## Step N+1 Downstream Checks
- Consumer hop: <exact value accepted, selected and advanced by consumer at file:line; progress/recovery owner and terminal/timeout path; documented terminal/audit-only exception or Unknown>
- Retry hop: <recovery behavior on next scheduled run verified at file:line, or "infinite loop risk">
- Parameter role: <callee treats argument as constraint | generator | n/a>
- Rollout/rollback: <supported reader/writer versions and last safe rollback point when contracts change; n/a or Unknown>
## Unknowns
| Unknown | Why it matters | What would settle it |
## Current-state radius, one line
<the systems, data, and people that today depend on what this change touches — named, not "various downstream">
The Unknowns table is load-bearing. Zero unknowns is an honest result on a small, fully verified subject and a warning sign on a large one — say which case you are in rather than manufacturing a gap to look thorough.
Step 6 — Hand off
Recon stops at the map. In chat: the verdict line (Recon complete — N seams, M unknowns, current-state radius: <one line>.), the file path, the one or two unknowns that would change the plan if they resolve the other way, and the next step.
If the user asked for a plan in the same breath, write it next — recon does not withhold the plan, it grounds it. The plan is a separate artifact from the map, drafted against swe, and its Blast section starts from the map's current-state radius and then adds what the plan itself introduces. Copying the radius across unchanged is a Blast failure, not a shortcut.
Escalation and neighbors
- recon — "What is actually there?" Read-only reconnaissance of the current system, before a plan exists.
- spike-360 — "Should this authority exist?" Classify first when state is moving; recon then maps what the approved shape has to live with.
- swe — "Does the plan embody our standards?" Its Pillar 0 is satisfied by recon's map; its Blast pillar extends that map per decision.
phase-0-spike(~/.claude/workflows/phase-0-spike.js) — the deep seam map with contract owners and rollout invariants, for a refactor already committed to. Recon is the cheap universal pass; that is the expensive committed one.blast-radius(not shipped here) — prices a one-way door the map exposes.- debug-mantra — traces a fail path for a bug happening now; recon traces edges for a change that has not happened yet.