Assess Findings Writer

SkillDev tools

Renders the /assess report from the deterministic run-context.json and the layer scorecard - the scorecard, the verbatim cross-layer findings, lying signals, and the mandatory Top 3 Actions. TRIGGER when the /assess orchestrator reaches the report-writing step; not a standalone user command.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Assess Findings Writer skill

What this skill tells your AI

The instructions your AI receives, as published by bjcoombs/ai-native-toolkit in skills/assess-findings/SKILL.md and read by ahel’s review.

The report-writing half of /assess. The deterministic core has written .assess/run-context.json (the data bus) and the assess-layer-scorer agent has returned the 0-8 scorecard. Your job is to assemble .assess/assess-report.md: the scorecard, the snapshots, the verbatim cross-layer findings, the lying signals, and the Top 3 Actions.

The deterministic parts are not yours to invent - you paste them. You write the prose around a findings section you cannot omit or reorder. This is the deterministic-core-writes-data / LLM-writes-prose split that makes the report reproducible regardless of which model drives the run.

Inputs

  • $REPO_ROOT/.assess/run-context.json - the data bus (findings, attention, keyhole summary, prescribed actions, stats, diff).
  • The scorecard returned by the assess-layer-scorer agent (the 0-8 score, per-layer verdicts, maturity label).

Read the cross-layer findings first

The layers above each measure one axis. The deterministic core also crosses those axes against each other and emits ten named findings - the "where to look" signals no single layer surfaces. Read them once, after the per-layer scans:

jq '.derived_findings, .attention, .keyhole_summary, .prescribed_actions' "$REPO_ROOT/.assess/run-context.json"

derived_findings is a fixed-order list of ten {name, paths, action} objects - all ten always present, paths may be empty. Omit a finding from the report when its paths is empty. Each pairs an axis-crossing with the action it implies:

  • hidden_coupling - modular statically but bleeds across boundaries historically (files that keep changing together). The static map says "isolated"; git says "coupled." Action: investigate the seam before trusting the boundary.
  • lying_map - high complexity under a stale doc: the map exists but no longer matches the territory. Staleness is the decaying-map churn ratio for a hand-written doc, but for a generated doc that declares provenance (frontmatter source: or a [[generated]] config mapping) it is measured against the source - the doc is stale only when its source is newer than it, so an accurate generated doc is never flagged. Action: fix or delete the doc - a wrong map is worse than none.
  • unexplained_complexity - high complexity with no doc and no recorded intent. Action: write the missing contract. Do not auto-generate it - a guessed contract is just another lying map.
  • untrusted_hotspot (E1 trust axis) - a complexity hotspot whose tests are hollow: an opt-in mutation pass let a high fraction of mutants survive, so the suite runs the code but doesn't pin it. Silent without mutation data (the default read-only run never fires it). Action: strengthen tests to pin observable behaviour, not internal state.
  • self_referential_tests (E2 trust axis) - the code and its co-located tests were introduced in the same commit, so the suite may verify the author's mental model rather than independently-specified behaviour. Action: request human review - the tests verify internal consistency, not truth.
  • unactioned_intent (write-side tendency) - files carrying stale promissory markers (TODO / FIXME / "remove after migration") that survived many edits to their own file without being kept: a lying map of intent. Silent when the marker scan is unavailable or the history is too thin to age markers. Action: action the promise - fix it, ticket it, or delete the marker/skip.
  • accretion_ratchet (write-side tendency) - a top-band hotspot whose line count only ever ratcheted upward: monotonic net growth across multiple commits with almost no deletion pressure (deletions below ~15% of total churn). The fingerprint of a file appended to rather than reworked. Only surfaced for files already in the top complexity/size band, so a growing-but-simple file is never flagged. Silent when the scan is unavailable, and disclaimed (not dropped) when the git history is degenerate (shallow clone or squashed import). Action: refactor down - extract, delete dead code, or split the file.
  • orphaned_understanding - high complexity with no human anchor and no intent: nobody owns the knowledge. Action: assign a human anchor before further change.
  • candidate_dead_weight - high complexity with no runtime evidence it is live. The bias is to keep (static reachability can't see external callers - Layer 1's caveat applies). Action: verify liveness, then delete only if confirmed dead.
  • refactor_boundary (positive) - high containment: edits stay local. A safe zone, never an attention row. Action: safe to hand an agent in isolation; cite these paths in Strengths.

attention ranks the few units landing in the most negative findings (refactor_boundary never counts) - the "look here first" list, each row carrying its findings and score. Lead the report's findings with the top of this list.

Copy findings_markdown verbatim. run-context.json carries a pre-rendered findings_markdown string - the deterministic findings section (the ten findings with their paths and actions, then the attention list). Paste it into the report verbatim, inside the 🔎 Cross-layer findings fold below - do not paraphrase, summarise, reorder, or drop findings. You write framing prose (the "why these matter here") inside the block, directly under its ## Cross-Layer Findings (Keyhole Readiness) heading - the block already opens with that heading, so never add your own heading above or around it; the heading must appear exactly once in the report. The section itself is the deterministic core's product, not yours. This is what makes the findings impossible to omit regardless of which LLM drives the run.

keyhole_summary rolls the same findings into a one-line readiness summary (summary_text), reported alongside the 0-8 score - see the score headline in the report template below. prescribed_actions lists the attention-derived Top-3 actions the report MUST include - see the Mandatory attention rule in the Top 3 Actions section below.

Score and Write the Report

Calculate the score (0-8 based on layers present, +0.5 for partial) and write the report to $REPO_ROOT/.assess/assess-report.md.

Two audiences, one file. The report serves a human (wants a short, picture-led skim) and an AI agent (wants the full structured detail). The structure below satisfies both: the default-visible surface is short - the score headline, the Top 3 Actions, and the two SVG images, plus one caveat line - and everything verbose is relocated into collapsible <details> folds. A collapsed <details> keeps its content fully present in the raw markdown, so an agent reading the file sees everything inside the folds; the fold only hides it from a human's rendered view. Folding is presentation, not removal - never delete content to shorten, relocate it into a fold. Two things never fold: the score headline and the Top 3 Actions. A section-ablation A/B showed that removing the Top 3 made a fresh agent confidently act on the wrong (lower-priority) item, so they stay visible and complete.

Use the CodeRabbit fold style: <details>, then <summary>emoji label</summary>, a blank line, the content, a blank line, then </details>. The blank lines are required for tables and lists to render inside the fold. Nesting is allowed.

Report format (write this to disk verbatim, filling in the placeholders; the bolded paragraphs - inside the folds and under the Top 3 Actions table - are instructions for how to fill that section, not literal report text):

# Codebase Assessment: <repo-name>

_Generated <YYYY-MM-DD>. Generated by `/assess` v<version>._

**Score: X / <denominator> - <maturity-label>** - a readiness snapshot, not a verdict · Keyhole: <paste `keyhole_summary.summary_text` from run-context.json verbatim>
<!-- <denominator> is 8 for a software repo (almost always) or the applicable-layer count for a detected archetype - see "Archetype-aware scoring" in the Score derivation fold. Resolve it before writing the headline; it is not a literal. -->
<!-- The " - a readiness snapshot, not a verdict" clause is a FIXED string: paste it verbatim on the score line. It is the SHORT not-a-verdict frame that sits with the score so the reader never mistakes the LLM grade for a pass/fail gate. Do NOT expand it into the full "This is an improvement roadmap, not a verdict" sentence here - that longer framing stays in the "How to read" fold, and duplicating it on the surface regresses the bespoke-opening contract. -->

<Bespoke summary - the report's emotional headline and the first thing a human reads. Write 2-3 sentences specific to **this** repo and **this** run; it must be **unrepeatable** - a sentence that would print identically under a different score is the boilerplate tell to avoid. Hit recognition before remediation:
> 1. **Lead with the genuine standout strengths**, named from the actual findings (the enforced gates, the complexity ratchet, a navigability jump, the closing feedback loop) - so the maintainer feels *seen* and a skeptic sees the tool read *this* code, not a template.
> 2. **Then** name the single most important next step in one clause, and - when prior-run data exists - a one-beat trajectory note (what moved since last run: score, a layer that flipped, reachability).
>
> Keep the non-verdict reassurance alive in the *tone* (a Missing or Partial locates where someone is partly blind, it is not a mark against the code) but do not lead with it or restate it as a standalone caveat - the full framing lives in the "How to read" fold. Celebrate what is working before pointing at what to improve.>

<MUTATION_CAVEAT - render this line VERBATIM and above the fold only when mutation testing did not run; omit it entirely when mutation ran. Read `mutation_not_run_cap.applies` from run-context.json (equivalently `mutation_run` is false; task 8 lands this data - read it, do not recompute). When it applies, paste exactly:>
_Note: Mutation testing was not run. Layer 6 (Coverage) is capped at Partial and truth-pressure remains unproven._

> **Agents start here.** The prioritized Top 3 actions below are also machine-readable in `.assess/actions.json` (schema v2: every entry carries `rank`, `action`, `done_when`, and `scope_fence`, plus the lifecycle fields `status` / `claimed_by` / `completed_sha` and a derived execution `mode`). Read that file to pick up the work - even with a smaller model - without parsing this report's prose.
<!-- Paste this "Agents start here" pointer VERBATIM (it is a deterministic fixed string keyed to the actions.json v2 schema, not prose to rewrite or embellish). It must sit above the first fold, pointing the reader at the durable machine-readable Top-3 contract before the human-readable table below. -->

## Top 3 Actions

<one orientation sentence: which attention paths Actions 1-2 cover and which scorecard gap Action 3 closes.>

| # | Action | Layer | Effort | Command / First Step | Done when | Scope fence | Hotspot files this addresses | Issue |
|---|--------|-------|--------|---------------------|-----------|-------------|------------------------------|-------|
| 1 | <one-line action - if `prescribed_actions` is non-empty, rank 1's action and path go here> | <layer number> | <small/medium/large> | `<exact command or file to edit>` | <verifiable exit criterion> | <what NOT to touch> | <paths from top_hotspots / top_complex / top_large, or "-" if not file-specific> | - |
| 2 | <one-line action - rank 2 from `prescribed_actions` if present> | <layer number> | <small/medium/large> | `<exact command or file to edit>` | <verifiable exit criterion> | <what NOT to touch> | <paths or -> | - |
| 3 | <one-line action - rank 3 from `prescribed_actions` if present> | <layer number> | <small/medium/large> | `<exact command or file to edit>` | <verifiable exit criterion> | <what NOT to touch> | <paths or -> | - |

**Done when** is the action's exit criterion and it must be *checkable, not aspirational*: a command whose pass state defines done (`golangci-lint run` reports 0 new issues; `uv run assess_gate.py .` exits 0; `max_fn_ccn` of the named function drops below the threshold in a re-run's stats sidecar). An executing agent - especially a smaller model - uses this to know when to stop; an action without a verifiable exit invites either premature "done" claims or runaway scope.

**Scope fence** names what the action must NOT touch: the files, behaviours, or conventions out of bounds ("only `.golangci.yml`; no source edits", "extract the one function; no drive-by reformatting of the rest of the file"). Smaller models over-extend without an explicit fence.

**Keyhole-size the action.** When an action targets code (not config), prefer the *function* over the *file*: the stats sidecar carries per-function data (`max_fn_ccn` and the worst function's name), so "extract the dispatch branch of `parseLine` (fn ccn 41), characterization test first" fits one context window where "refactor parser.go (ccn 67)" does not. An action a fresh agent cannot complete inside one keyhole is two actions.

### Why these three?

<2-3 sentences explaining why these are highest leverage. Connect to specific gaps from the scorecard and to hotspot files where relevant. Be concrete about what each action prevents.>

## Snapshots

### Complexity - riskiest to change

[![Complexity hotspot](./complexity-heatmap.svg)](./complexity-heatmap.svg)

<one-line takeaway: where the risk concentrates and whether the per-function complexity is fenced - e.g. "Every hotspot is in `skills/assess/`: the engine auditing itself; per-function ccn p95 9 sits under the 15 gate.">

### Doc navigability - can an agent find its way?

[![Doc map](./doc-graph.svg)](./doc-graph.svg)

<one-line takeaway: reachability plus whether it is a wayfinding or a truth gap - e.g. "~10% link-reachable: a curation/wayfinding gap, not hidden content; no lying maps.">

<details>
<summary>📈 Snapshot detail (commit, hotspots, navigability, lying maps)</summary>

#### Complexity profile

- **Measured at commit:** `<head_short>` (<committed_date>)<staleness-suffix>
- **Files scored:** <N>
- **Churn window chosen:** <last 12mo | last 24mo | last 5y | all-time>
- **Complexity profile:** per-function ccn p95 <N> (max <M>); file-aggregate ccn p95 <N> (max <M>); p95 est. tokens <N> (max <M>); p95 LOC <N> (max <M>)
- **Top hotspots** (composite `sqrt(ccn) × sqrt(1 + commits) × sqrt(est_tokens)` - a sub-linear blend of complexity, recent churn, and context-window size, so a file high on *multiple* axes - big AND complex AND churning - is the worst keyhole and leads; a frozen-but-complex file ranks below an equally-sized active one; a churny-but-trivial file can't top on churn alone; and a big-but-simple-stable file can't top on size alone). `est_tokens` is the char-based estimate (~chars/4), `ccn` here is the **file aggregate**; the worst single function per file is in parentheses:
  1. `<path>` - <est_tokens> est. tokens (<loc> LOC), aggregate ccn <N> (worst function <max_fn_ccn>), <M> commits in window
  2. ...
  3. ...

- **Keyhole budget** (`stats_summary.est_tokens.budget`): state the repo total estimated tokens and how many files / top-level subtrees exceed one context-window keyhole (the documented `budget`, an estimate). This is the literal "does the relevant slice fit one keyhole?" measure. e.g. _"Repo is ~<total> est. tokens; <N> subtree(s) exceed the ~200k keyhole budget: `<name>` (~<tokens>)."_ When `est_tokens` is absent (pre-token snapshot), omit this line.

Size encodes estimated tokens (~chars/4 - the keyhole size unit; the familiar LOC is one hover away in the tooltip), colour encodes cyclomatic complexity (dark red = high), saturation encodes recent git churn (vivid = active). Tokens track what fills an agent's context window better than LOC, which undercounts dense/wide files and overcounts sparse code. Vivid red blocks are the migration risk. When the treemap carries a **hatched** overlay (only when opt-in mutation results exist), those blocks are covered-but-unpinned code - tests run them without constraining them - so they stop reading as safe green; the heatmap's own legend keys the diagonal (>30% survivor density) and cross-hatch (>50%, severe).

**Always explain the hatching - even when it is absent.** The hatched overlay only appears when an opt-in mutation pass ran, but its *absence* must never pass silently: a green-but-unverified treemap reading as safe is exactly the lie the overlay exists to prevent. Read `test_pressure.mutation_config_present` (and whether `test_pressure.survivor_clusters` carry data) and write one of these two readings right after the treemap, always:

- **Mutation ran and hatches exist:** explain the overlay - a diagonal hatch marks >30% survivor density, a cross-hatch >50% (severe); both flag covered-but-unpinned code, tests that execute the lines without constraining behaviour, so the action is to strengthen assertions to pin observable behaviour, not internal state.
- **Mutation did NOT run:** write it verbatim - _"No hatching visible - mutation analysis was not run. The hatching would mark covered-but-unpinned code (tests that execute without constraining). Run `/assess` and accept the mutation offer to enable it."_

**Pin the snapshot to its commit (issue #59).** Read `measured_commit` from `run-context.json` and fill the "Measured at commit" line so every absolute LOC/CCN figure in this report reads as a snapshot of one commit, not a current truth:

```bash
jq '.measured_commit' "$REPO_ROOT/.assess/run-context.json"
  • When available: false, omit the "Measured at commit" line (no git history to pin to).
  • Render head_short and committed_date. Add a <staleness-suffix> warning when the snapshot is stale, so a reader knows the numbers may have drifted:
    • dirty: true → append " - working tree had uncommitted changes; figures include un-committed edits".
    • behind is a positive integer → append " - HEAD was commit(s) behind <upstream>; absolute figures are a snapshot and may read low against current code".
    • Clean and up to date (dirty: false, behind 0 or null) → no suffix.
Where to focus testing

The cheap, always-on read of which risky files most need test work, and which kind - the hotspot risk band crossed against the parsed coverage report and the hollow-test heuristics, pre-joined into one ranked list by the deterministic core. Render it from run-context.json .test_focus.entries (the top 5-10). Omit the whole subsection when .test_focus.available is false or entries is empty.

Open with the coverage provenance line from .coverage_report, so a reader knows whether the signals rest on a real coverage report or on heuristics alone:

  • available: true"Coverage data: <source> (Cobertura)" (name the report file from .coverage_report.source; the format is cobertura or lcov).
  • available: false"Coverage data: none found - test signals are heuristic-only."

Then the table, one row per entry, mapping the raw .test_focus.entries values to human-readable labels:

FileRiskTest SignalSuggested Action
<path>High/Medium/Low<test_signal label><suggested_action label>
  • Riskrisk_band: high → High, medium → Medium, low → Low.
  • Test Signaltest_signal: no_covering_test → "No covering test", covered_but_hollow → "Covered but hollow", unknown_no_coverage → "Unknown (no coverage)".
  • Suggested Actionsuggested_action: add_tests → "Add tests", strengthen_assertions → "Strengthen assertions".

Cross-reference, don't duplicate. This table surfaces the cheap signals - risk band, coverage presence, and the hollow-test heuristics. The expensive confirmation lives elsewhere and is referenced, not repeated: the untrusted_hotspot finding confirms which files mutation testing proved hollow, and the Layer 6 green-but-hollow row in the Lying Signals table pairs a file's coverage against its mutation score. A file flagged covered_but_hollow here and confirmed by untrusted_hotspot there is the same file seen through the cheap then the expensive lens - so point at the finding, don't restate it.

Disclose active declines. When run-context.json .decline_disclosures is non-empty, the user has permanently declined one or more optional tools for this repo, so a capability was deliberately silenced. Render each line verbatim under a short "Declined tools" note so the read stays honest about its own depth - e.g. "Mutation testing permanently declined by ben on 2026-07-07" tells the reader the Layer 1/6 depth was capped by choice, not by a gap. Omit the note entirely when decline_disclosures is empty (no markers, the common case).

Doc navigability

Read the structured signal from run-context.json (.doc_graph, .doc_staleness, .stale_hubs) and write this in plain language - explain the metrics, don't just dump numbers. Define each term the first time you use it:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
30
Forks
5
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
assess-findings
Source
github.com/bjcoombs/ai-native-toolkit