Ripwire Fresh Eyes logo

Ripwire Fresh Eyes

OrganizationPopular
redhat-et
ripwire-fresh-eyes

Maintenance risk in code you did NOT write — inheriting a module, planning a refactor, a suspected god object, 'what's gnarly / where's the rot / safe to touch?': hotspots, dead or inactive code, switches/defaults (--flags), clones, a function's real shape, bus factor, co-change. Naming the fix or judging YOUR new code → quality-bar. A single-lens question is a single call.

Overview

Publisherredhat-et
Repositoryripwire
Skill nameripwire-fresh-eyes
Stars
2.2K
Forks
141
Bundled files
1
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by redhat-et on GitHub. Read the source before you install it.

Installation

Install the Ripwire Fresh Eyes AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/redhat-et/ripwire.git /tmp/ripwire
mkdir -p .claude/skills
cp -r /tmp/ripwire/skills/ripwire-fresh-eyes .claude/skills/ripwire-fresh-eyes
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Ripwire Fresh Eyes in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Ripwire Fresh Eyes on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Ripwire Fresh Eyes is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Fresh-eyes risk review with ripwire

Routing: • Any DIFF — your own, or an incoming PR you're reviewing — about to push or merge → ripwire-change-check. • You have the measurement and now need the FIX — which refactor a measured shape calls for, its precondition, and the fix-then-prove loop → ripwire-quality-bar's shape → refactor playbook. • Architecture health (deps metrics, layering rules, modules) → ripwire-layers. • Finding where a specific BUG lives → ripwire-find-bug. • Not sure which skill? → ripwire-router.

Scope it to one unfamiliar subsystem you're about to touch, instead of the whole tree, two ways: point <dir> at the subdirectory (ripwire src/net --hotspots), or keep the repo root and --exclude the rest. --dead-code=DIR also scopes directly. This is how you turn the repo-wide sweep into a "what's-gnarly-in-code-I-didn't-write" read of one module.

<dir> = repo root. Add --exclude=build --exclude=.git (repeatable) to keep build artifacts out — they otherwise show up as false clones and dead code. This composes descriptive passes into one snapshot; run the sections your question needs.

Inheriting this subsystem? Check ripwire <dir> --notes --legend=compact first — a prior agent's pinned gotcha (--note-add'd trap, flake, or invariant) is the cheapest fact you can pick up before you start reading.

Core health passes

  1. Maintenance hotspotsripwire <dir> --hotspots --legend=compact
    <hotspots window="12mo" ranked="33"><f p="./src/main.cpp" churn="53" ccx="1365" score="72345" top="main:1306"/> …
    score = churn × ccx (commits × Σ cognitive complexity). Top files = where developers keep working and the code is hard — the highest-leverage places to improve. top= names the worst function. Recent regression, not an all-time read? --hotspots --since="2 weeks ago" (or --since=HEAD~20) scopes churn to commits after that point; unresolvable/absent --since degrades silently to all-history. On C-family/C# code, discount a body carrying ppalt=: its cx=/ccx=/nest=/loc=/locals= are summed over EVERY #else/#ifdef branch (deterministic, but an over-count vs any single build), so a high score on a ppalt=-heavy function can be the preprocessor fan-out, not the logic — read ppalt= before calling a row the worst in the file.

1a. "What changed recently / who touched this / what is moving here?"ripwire <dir> --rank-by=churn-decay --legend=compact

<recent n="40" of="1857" merge_bombs_skipped="5"><rc p="src/graph.h" w="0.91" age="2"/> …

The HISTORY question, not the working-tree one: --situ answers what you changed and have not committed, this answers what the repository has been moving, newest-and-heaviest first. Scope it to a directory: ripwire <dir> --rank-by=churn-decay --in=DIR (root-relative, and it must exist under the root) — a second <recent scope="DIR" …> block follows the global one and the symbol map collapses to a counted stub, so the answer is the window rather than a whole-repo map. merge_bombs_skipped="N" is the disclosure that N commits touching more than 100 indexed files were left out of the weighting — if the commit you are hunting is a giant rename, it is in that N and not in the rows. --since=REV|DATE bounds the window; --limit/--offset page the scoped block. ripwire <dir> --help-task="what changed recently in DIR" --legend=compact routes here on its own.

1b. What is BUILT but DARK here?ripwire <dir> --flags --legend=compact (=SUBSTR to narrow)

<flags gates="98" dark_gates="90" compile="32" cmake="9" env="57"><gate name="X_HARMONY_SFX" kind="compile"
  default="0" dark="1" regions="7" loc="45" reads="8" p="sound/audioWiringFlags.h" l="47"/> …

Inheriting a repo, the invisible half is the code that ships compiled OUT. This harvests all three gate patterns — #ifndef/#define header gates, CMake option(), getenv() reads — with each gate's default and the size of the code it guards, dark first. Two things --grep cannot tell you: when a name is BOTH a header gate defaulting to 0 and a CMake option(... ON), the CMake default wins (it is what the build passes) and the header appears as an <also> row — the contradiction a reader who greps the header gets wrong; and a gate whose default IS another gate's name resolves through the alias chain, so a master switch reports <aliases n=…> instead of a misleading loc="0". Limits: lexical, not preprocessed — this is the in-repo default, never the value your build used. A gate needs a value (#ifndef F / #define F 0); valueless pairs are include guards and are excluded. A gate read as a value (constexpr bool k = F != 0; then if constexpr) honestly reports regions="0" here — --flip below lifts exactly that limit.

1b-ii. If I flip THIS one, what lights up?ripwire <dir> --flags --legend=compact --flip=NAME

<flip gate="X_RRF_ALL" kind="cmake" default="ON" dark="0" family="12" regions="0" loc="0" branches="43"
  bindings="11" hosts="11" downstream="95" dependents="146" tests="23" untested="0">
  <member name="X_RRF_ISLANDS" via="alias" branches="12"/> …
  <lights r="0" b="43"><b p="canyon/simLevel.cpp" l="133" gate="X_RRF_WIDTHWAVE" via="kWidthWave"
    sym="buildStripTable"/> …

A list of ~90 dark gates is a map, not a decision. This is the decision: for ONE gate, the code that becomes live (#if regions and the if constexpr branch sites a value-style gate governs), the symbols holding it, what those transitively call (downstream — what starts executing), who depends on them (dependents), the tests that reach them, and untested — the hosts no test covers. That last number is the honest answer to "is it safe to flip?". Alias chains run both ways: flipping a master rolls up every child that #defines to it, flipping a child lights only that child and names its <parent> plus the siblings the parent's flip would add. kind="cmake" also steers the build graph (an if(NAME) target_sources(...) can add whole files) — those sites are listed as <c> rows and deliberately not followed. kind="env" is runtime="1": no delimited region, so the hosts are the symbols that consult the variable. Limits: lexical and single-line, never preprocessed. A binding split across two lines is missed. The value lane reads C-family source only and treats a file declaring its own constant of that name as shadowing the gate's (C++ scoping) — but a third header's same-named constant, included rather than redeclared, would still count. A lit site inside no indexed def (a guarded member field, a file-scope constexpr, a test-macro body) counts into filescope= instead of a host. --detail=N lifts the row caps. Report only — always exit 0; an unknown gate name refuses (exit 1) and names the near-misses.

1c. Can I TRUST this repo's docs?ripwire <dir> --doc-drift --legend=compact (=SUBSTR to narrow to one doc)

<doc-drift docs="129" clean="110" anchors="1995" checked="716" unchecked="1279" drift="110">
  <doc p="docs/ARCHITECTURE.md" anchors="50" checked="34" drift="9"><a k="file-line" l="186" c="80"
    why="line-moved" ref="src/main.cpp:317" sym="isGitUrl" got="communityPresentation"/> …

Inheriting a repo, the design docs and audits are how you learn it — and the stale ones are how you learn it wrong. Run this before you read them: it verifies the CHECKABLE anchors (file:line refs, backticked symbol mentions, = N constants, [N] array extents) against the live index and reports only what no longer holds, per doc. A doc with drift="9" gets read with suspicion; the 110 clean ones do not. why= names the cause — missing-file, past-eof, line-moved (with got= naming the symbol that now occupies the line), undefined, const-value, array-extent. Limits: anchors, not prose — §Status lines, dates and "N of M done" tallies are NOT checked, and it says so rather than pretending. Every lane under-reports deliberately (a name is stale only if it occurs nowhere in the code as a token; a number only if the corpus binds it uniquely in a declaration), and checked + unchecked = anchors with each declined check named in an <unchecked> row. Exit is always 0: drift is a report, not a gate.

Want it to BECOME a gate?ripwire <dir> --doc-drift --legend=compact --gateability. The reason CI can't gate on drift is usually a handful of UNDATED design docs: with no date the lexical lanes can't tell "stale" from "a record of what was true then", so their rows stay unclassifiable. This lists exactly those docs and what one annotation each would fix:

<gateability docs="17" projected_drift="0">
  <fix p="docs/ARCHITECTURE.md" live="6"/> …

live= is that doc's still-failing anchors; projected_drift= is the repo-wide drift you'd be left with if every listed doc got one ISO date in its H1 or a front-matter self-date line. Treat it as an upper bound on the win, not a mandate — dating a doc that is genuinely live hides real rot rather than resolving it. Use it to turn "CI stays non-gating forever" into a finite to-do list.

Add --with-history when the mention lane matters. undefined only means "defined nowhere here", which on a repo whose PLAN docs name unbuilt features is expected, not rot — measured on a 2900-file repo, 243 of 325 undefined rows were names that had never existed there. --with-history makes one git log pass over everything reachable from HEAD and splits the lane: why="deleted" with got="removed in <commit> (<date>)" and at= the file (real rot, ~75% fewer rows but every true positive kept), versus <unchecked r="never-in-history"> for names the repo never had. It is opt-in because the walk costs ~3 s on that repo against a 0.64 s default; the result is memoized per (repo, HEAD sha), so every later question on the same commit — including --whereis --with-history — is a cache load.

1d. Did a task in this plan just never get launched?ripwire <dir> --plan-lint=FILE --legend=compact. Where 1c explicitly does NOT check §Status lines, dates or "N of M done" tallies, this is the narrow structural check that closes that one gap — a wave-closer's job, mechanized. It is opt-in per FILE (never a directory sweep) because the house PLAN dialect it recognizes is deliberately narrow: a task card is exactly an H3 heading opening with a task id (T5, T10, T7b), a status ledger is exactly one heading (any level) reading "Status" once a leading section mark is stripped. A file showing neither an H3 card nor a ledger heading reports dialect="0" and nothing further runs — most plans are exactly that file, and this is not a failing lint for them.

<plan-lint file="wave.md" dialect="1" cards="6" ledger="1" git="1" stale_commits="20" gating="1">
  <card id="T5" line="40" status="missing" why="unlaunched" gating="1"/> …

Once dialect="1": a card's status is satisfied EITHER by a glyph on the LAST non-blank line of its own body OR by a §Status ledger line naming its id — folded by DIGITS, so a bare card T7 is also answered by a lettered ledger mention T7a/T7b (the real house shape when a card's own body names lettered sub-tasks and the ledger tracks each separately) — with the card's own body winning when it has one. Unresolved is status="missing", with why= naming the exact shape: unlaunched (a ledger exists and never names this id at all — the exact field failure this verb exists to catch: a card that was planned but never started, silent until a human happened to notice), unresolved (the ledger names it but no line carries a glyph), or no-glyph (this document carries no ledger at all, so the card's own body was the only possible source). An hourglass line whose git-blamed commit sits more than stale_commits= commits behind HEAD is stale="1" (never claimed outside a git repo; blames whichever line the status actually resolved to, marked src="ledger" when that is the ledger and not the card); a task id named in the ledger with no matching card (same digit fold) is a ledger-orphan; an owed/OWED mention with no check-mark or cross anywhere LATER in the SAME document is undischarged. Limits, stated up front: never semantics — it does not judge whether a card's claims are true (that stays 1c's job); single-document only (a successor plan's discharge of an owed item is invisible here); substring matching on "owed"/"OWED" with no disambiguation (a doc that merely quotes the word reads the same as a real marker); a document that uses card headings as plain labels with NO status mechanism anywhere (no ledger, no glyph on any card) still reads every card as missing — a known, disclosed gap, not a guessed-at fix. Unlike --doc-drift's always-0 report, this gates: exit 2 when dialect="1" and any row carries gating="1", exit 0 clean or dialect="0", exit 1 only when FILE could not be read.

  1. Dead coderipwire <dir> --dead-code[=DIR] --legend=compact <dead-code count="N" confidence="high" evidence="internal-linkage+zero-callers"><d n="orphan" …/> — source-defined free functions with explicit internal linkage and in-degree 0 in the indexed call graph. --dead-code=DIR scopes to a sub-tree. Methods, declarations, headers, and external-linkage entry points are excluded, so count="0" means "no high-confidence candidates," not "no dead code." Name-based and tree-local — verify before deleting: confirm against the compiler's unused-symbol diagnostics or a linker map; ripwire narrows the field, the toolchain proves it.

  2. Duplicate bodiesripwire <dir> --clones --legend=compact <clones groups="N" type3="M"><group type="2" tokens="161" n="2"><f n="line" p="…:31"/><f n="line" p="…:28"/></group>. type="2" = exact/renamed (identifiers + literals normalized, so a renamed copy still matches); type="3" = a gapped NEAR-miss (similarity 0.80–1.0, an inserted/changed statement) — check type= before assuming two members are byte-identical. Larger tokens= = more dedup value; a fix to one likely belongs in all. Rule of Three: extract on the third occurrence, not the second — a little duplication beats the wrong abstraction. A group carrying idiom="…" (threshold-ladder, switch-name-table, builder-chain) plus demoted="1" is one the detector judged an IDIOM COLLISION, not shared logic: same shape, no shared domain identifiers, different scopes — five unrelated bucketing ladders in five subsystems match each other's token stream and consolidating them would be the wrong abstraction. Demoted groups still print and still count; they stop gating. The idiom is named precisely so you can overrule it by reading the bodies — treat it as the tool showing its work, not as a row it decided for you.

  3. AST smellsripwire <dir> --lint --legend=compact <lint findings="N"><rule name="magic-number" count="82"/> … then per-finding <f rule= p= …> with the enclosing symbol. No-build, AST-only — a fast complement to clang-tidy, not a replacement. Skim the per-rule counts for the dominant smell.

    Two rule prefixes are also family evidence. naming-* (naming-case/naming-confusable/ naming-predicate/naming-series/naming-setter/naming-short/naming-underscore/naming-wordy) is exactly --ensemble's/--quality-panel's lexical family; atom-* (atom-assign-as-value/ atom-comma-operator/atom-embedded-crement/atom-implicit-predicate/atom-nested-ternary/ atom-octal-literal/atom-reversed-subscript) is confusion. A --lint finding under either prefix is the identical predicate the family join runs — reading it here just skips the join. Not a moment to reach for mid-task: --naming-calibration scores both spellings of this repo's own historical renames (old identifier vs. what it was renamed to) against the same naming-* predicates — it is how those rules were validated, a maintainer calibration harness (test/namingcalibrationcheck.sh pins the per-rule floor) with a pairs= sample size and its own legend calling itself a noisy proxy, not a verdict on any function you're looking at. It always exits 0.

    What rules exist, and are they even structurally possible on this corpus? — ripwire <dir> --lint-catalog. The full built-in registry: every rule's severity, category, one-line rationale, its registered languages, and the release it shipped in — no corpus needed, so it answers "what could --lint ever find here" before you run it. A plain --lint run then discloses the corpus-specific half: applicable="0" on a <rule> row means none of that rule's registered languages are present at all (its count="0" is structural inertness, not a measurement — e.g. magic-number is C/C++/ObjC-only and reports applicable="0" on a pure-Go tree), and the root's inert_rules=N tallies how many rows that is true for. Scoping a --lint run to one family — --lint-select=PREFIX[,...] / --lint-ignore=PREFIX[,...] (comma-separated, * for "every rule"): --lint-select=cache- runs only the cache-* family, disclosing selected="K of N" on the root so a filtered zero is never confusable with an unfiltered one; an unresolvable PREFIX refuses with a near-miss rather than silently matching nothing.

    The one naming lens that proposes a FIX, not just evidence — ripwire <dir> --naming-consistency --legend=compact. Every other pass on this page tells you WHAT is wrong; this one is the deliberate exception, and only for one narrow reason: case-CONVENTION is Tier A (the research record's own tiering for "propose a fix only where the correct replacement is derivable from the corpus" — see ripwire-quality-bar's zoom-in table for the other nine --quality-delta kinds, which stay evidence-only on purpose). It votes the corpus's OWN dominant case style (camel/pascal/snake/screaming) per (language, kind) group — decided only past a 20-name sample floor AND 90% agreement, UNAVAILABLE with why= otherwise, never a guessed winner — and every off-convention name in a decided group gets propose=: its own subtokens mechanically recombined into that style. No dictionary, no synonym judgment — which is exactly why this is the one place a rename suggestion is safe to make. It is still only a suggestion, not a safe-to-blind-apply rename: applying one for real needs --uses to prove the complete reference set first. Exit 0 always, a lens like the rest of this page — abbreviation inconsistency and synonym unification (the other two Tier-A categories) stay unimplemented on purpose; both need a dictionary or a semantic judgment call this verb's design deliberately avoids.

    Local-variable naming, opt-in and explicitly not the default — ripwire <dir> --lint --legend=compact --naming-locals. A --lint modifier, not a standalone verb (a no-op without --lint): runs naming-short/naming-wordy/ naming-underscore/naming-case — the same tags, same predicates as the Symbol-scoped checks above — against LOCAL variable names too, C/C++ only, and only where the risk is real: inside a function that already clears an existing size/complexity gate (loc>80 OR nest>4 OR ccx>=15) AND has locals>=8 (measured, not guessed: median 9 among this repo's own 377 gated functions). naming-short additionally requires the local's own declaration depth >=2 — a depth-1 int n = argc; never fires, a nested int x two blocks deep can. This deliberately reverses naminglens.h's own stated invariant ("an un-indexed loop local can never be flagged") — read the WITHDRAWN note atop that file before trusting it. NOT enabled inside a plain --lint run and not a candidate for that yet: the plan's own hard blocker (a hand-curated fixture corpus AND a manual real-corpus audit for idiomatic-short-name skew — i/j/k/ buf/tmp/err) has not run. Treat a --naming-locals finding as a candidate to eyeball, not a verdict, until that audit lands.

    Your own rulesripwire <dir> --lint-rules=DIR loads YAML ast-grep-style rules from DIR and runs them alongside (or instead of) the built-ins; findings share the same <f rule= sev= p= …>message</f> shape. A malformed rule alerts to stderr and is skipped (sibling rules still load); zero loaded rules exits 1. Writing a rule (the query grammar, the inside/not-inside/not-matches combinators, a worked example) → lint-rules.md.

  4. Ownership / knowledge riskripwire <dir> --owners[=SYM] --legend=compact <owners files="N"><f p="…" authors="1" bf="1" top="…@…" share="1.00"/> (recency-weighted, 6-month half-life). authors= distinct authors; share= the top author's fraction of weighted commits; bf="1" = one person holds >80% (bus-factor risk); top= = who to ask / who should review. --owners=SYM resolves to just that symbol's defining file — the "who should review my change to buildGraph" answer. Measures git authorship (recency-biased), not who should own the code. Worst case: a file that's BOTH a top hotspot (step 1) AND bf="1" — gnarly code only one person understands.

    Per-function readabilityripwire <dir> --readability --legend=compact <readability functions="N" shown="40" capped="1"><fn p="…:512" n="buildGraph" lines="1244" toks="6753" ops="4359" vocab="382" vol="57923.4" ent="6.26" posnett="0.000"/> — the Posnett/Hindle/Devanbu (MSR 2011) closed-form model, least readable first: vol= Halstead volume, ent= Shannon token entropy, lines= the definition's line span, posnett= the fitted sigmoid. Complements step 1: --hotspots ranks by complexity × churn (where pain has been paid), this ranks by how hard the text is to read right now, with no git history needed. Two caveats it states in its own legend and you should carry: one token-class table serves every language, so vol= is a cross-language approximation; and the formula was fitted on snippets of 20 lines or fewer, so on a 1,000-line function posnett= saturates at 0.000read the ORDER of the rows, never the number as a grade.

    Reachable non-local mutable stateripwire <dir> --nonlocal-state --legend=compact <fn p="src/infra/profilePmc.h:288" n="ensure_global_init" writes="2" reads="3" direct_writes="1" direct_reads="3" cells_total="3"><cell n="g_perf" p="…:284" dir="rw" at="…:339" at_dir="rw"/>… — per function, the globals / file-scope statics / function-local statics / Python module globals it or its transitive callees touch, most writes first, with reads and writes kept apart. writes=/reads= fold in the callee closure; direct_* is what this body does itself; each <cell> names the declaration and either the use site here (at=, with at_dir= for what THIS body does) or the callee it came through (via=). Answers a different "is it safe to touch this?" than --impact: --impact is who calls you, this is what state you can perturb from here — the reason a change with a tiny caller set still breaks something across the repo. const/constexpr declarations are not cells, so a big writes= is genuinely shared mutable state, not a table of constants. Three things to carry: it is unsound by construction — no indirect calls, no pointer aliasing, no macro-named cells — so every count is a counts_floor="1" floor and a zero means none found; a local shadowing a cell's name can be charged to the cell; and it covers C++/ObjC/Python only, with every other indexed language named in unanalyzed_langs= on the root, which is not measured, not measured zero.

    THE SINGLE COMMAND — ripwire <dir> --quality-panel[=strict|default|lenient] --legend=compact. When the question is just "where is the rot", start HERE, not at step 1. It is the whole panel in one ranked report: the four families --ensemble joins, plus colocation (how much of what you must read to understand this function lives outside its own file) and state (this function's OWN BODY touching non-local mutable state) — six families, ranked by the count of distinct families and never by a composite. <s p="./src/graph.h:462" n="buildGraph" fam="4" of="6" fired="structural,confusion,historical,colocation" uncounted="" unavail="" join="deep+untested"><e f="structural" counted="1" why="ccx=724 loc=1244 nest=9 humps=30 deep=308 rrank=1"/><e f="colocation" counted="1" why="crank=33"/>… Pick the preset by what you are doing, and note that a preset only selects families and cuts on the count — it never weights: lenient (all six, 1 must agree) is a reading order, about a third of any tree; default (all six, 2 must agree) is a review list; strict (the four families measured stable enough to stand behind a gate, 2 must agree) is the only rung to point CI at. historical and colocation are deliberately out of strict — each is a fixed-size worst-40 cut over a ranking whose population moves, so both re-shuffle across commits on code that did not change. A row still SHOWS them, as uncounted= with counted="0", so you can see what the preset set aside. Everything the --ensemble paragraph below says about ordinal cuts and UNAVAILABLE applies here unchanged.

    Read the structural why= as a PROFILE — nest= alone is a max, and on a subsystem you did not write that is exactly the wrong number to act on. nest=9 is one deepest line; it cannot tell a tangled body from a long blocked-sequential one whose max is a single inner loop. The row carries the profile beside it: humps= (how many maximal control-nesting regions reach the bar — CodeScene's "bumpy road", EXACT) and deep= (how many LINES lie inside them — a FLOOR, deep_floor="1"), against the loc= already on the row. Both are absent exactly when nest < the bar — not-deep, never a hidden 0 — and deep below humps is legal output, not a defect (deep counts lines, humps counts regions, and a one-line if(c){x;}else{y;} at the bar is two regions on one line). Two ratios do the discriminating: deep/loc separates tangled (high — depth is sustained) from blocked-sequential (low — a dispatch table or a setup block), and deep/humps separates one giant tangle (high — the expensive fix) from many tiny touches (low — repeated missing abstractions, each its own cheap extraction). locals= on the same row is the reader's working set — a FLOOR (locals_floor="1"), C/C++ only, absent rather than 0 elsewhere. ev= on the same row (essential complexity, McCabe: absent/1 = single-entry/single-exit, extract-method applies mechanically; 2+ = a jump — see ev_why=) — tells you whether the shape you are about to prescribe a fix for is a cheap lift or an expensive rewrite before you commit to one. Which fix each shape calls for, and its precondition → ripwire-quality-bar's shape → refactor playbook. The numbers in the example rows on this page are ILLUSTRATIVE OF A SHAPE, not values to expect — these counters are under active calibration (an else-clause over-count fix in flight moves humps= down substantially, and ccx=/nest= with it, on else-heavy functions). Durable is what you do with the row: humps=1 vs many, the two ratios, and the semantics above. Read the row in front of you.

    join="deep+untested" is an annotation, not a seventh family: this row carries deep= and no indexed test reaches it. It changes nothing — not fam=, not of=, not the ordering, not which rows appear — and it is exactly the pair where a refactor is most wanted and least safe (test first, → ripwire-write-tests). It is suppressed on every row when tested_scope="0", because on a corpus whose tests were never crawled "untested" describes the crawl and not the code, so read tested_scope= on the root before treating its absence as good news; deep_untested= counts them across the whole row set, which the limit= window does not change.

    The historical family's unit is the FILE, not the symbol. churn= and hrank= are file facts inherited verbatim by every symbol in that file, so a symbol in a busy file collects this family without any property of its own. On a subsystem read that is worth discounting explicitly: confirm at the symbol (git log -p <file>, or --hotspots --since=) before calling one function the churny one.

    Corroborated rot, four families only — ripwire <dir> --ensemble --legend=compact. The narrower join, and the one whose numbers are published; reach for it when you want exactly the calibrated four plus the per-file rollup. One lens firing is an opinion; the same function flagged by several kinds of evidence at once is a finding. --ensemble joins four orthogonal families — structural (the shape: ccx/loc/nest/params bars plus the readability rank), lexical (the naming-* rules on the identifier text), confusion (the atom-* rules on the syntactic construct), historical (git change frequency) — and ranks by the count of distinct families: <s p="./src/graph.h:462" n="buildGraph" fam="3" of="4" fired="structural,confusion,historical" unavail=""><e f="structural" why="ccx=724 loc=1244 nest=9 humps=30 deep=308 rrank=1"/>… — plus a per-file rollup whose top_fam= is the strongest single-symbol corroboration in that file. (The humps=/deep= profile and the per-FILE historical unit read exactly as in the panel above; this verb has no join= annotation.) There is deliberately no composite score: averaging correlated metrics re-weights one signal and calls it three (the Maintainability-Index failure), so fam= is ordinal and every row carries the evidence behind it — you never need a second command to see WHY. Two things to carry: rrank=/hrank= are relative cuts (the worst decile of this corpus, so something always fires), while the ccx/loc/ nest/params bars are absolute and printed on the root; and a family that could not be measured is named in unavail= with of= dropping to 3 — UNAVAILABLE is not the same as clean, so on a corpus with no git history do not read the missing historical as "nothing is churning".

    How much is NOT in front of you — ripwire <dir> --context-ratio --legend=compact. The other lenses ask how hard a function is to read once you have it open; this one asks how much you must open besides it. <s p="src/main.cpp:10177" n="main" sites="1050" ents="131" ents_out="85" ent_ratio="0.649" files="37" files_out="36" rtok="165317" rtok_out="61968" read_ratio="0.375" ext="141" amb_names="19"/>ents= distinct in-corpus definitions its reference sites resolve to, ents_out=/ent_ratio= how many live outside its own file, and rtok=/rtok_out=/read_ratio= the same split weighted by the tokens a reader must actually read. Read the two ratios together: main above needs 65% of its entities from elsewhere but only 37% of its reading volume, because the biggest things it touches are next door. A per-file rollup follows the symbol rows and is a union over the whole file (includes and imports included), not the sum of those rows. Three things to carry: it credits its own prior art — the fraction is Beck & Diehl's per-class congruence (FSE 2011) flipped, and the refinement is the reader weighting plus the use of every reference role, not just calls; resolution is name-based, so amb_names= names resolved to several definitions and ents= is a floor; and ext= is dominated by locals and parameters, so it is not an external-dependency count and never enters either ratio.

    Comments that say nothing — ripwire <dir> --comment-coherence --legend=compact. Per function/method with a doc comment, two content measures, most name-restating first: <fn p="…:41" n="computeTotal" c_coeff="1.000" words="2" restate="2" cic="0.667" c_terms="2" i_terms="3" shared="2"/>. c_coeff (Steidl/Hummel/Juergens, ICPC 2013) is the fraction of the comment's words within Levenshtein distance 2 of a word in the name — HIGH is BAD, it means the comment just restates the name (// compute total above computeTotal scores c_coeff="1.000"), the opposite of the naive "high coherence sounds good" reading. cic (Scalabrino, ICPC 2016/JSEP 2018) is the Jaccard overlap between the comment's vocabulary and the method's own identifier vocabulary (params, locals, callees, fields) — a different axis, expected to disagree with c_coeff, so read both rather than either alone. UNAVAILABLE, never a zero, where no doc comment exists at all (counted in no_comment= on the root) — this pass never guesses at an absent comment's quality. Different question from --doc-drift above: doc-drift asks whether a markdown claim is still true; this asks whether a source doc-comment carries information — neither checks the other's axis.

Hidden-coupling pass — "a change here keeps breaking unrelated files" (Fowler's Shotgun Surgery, in its measurable form: change coupling)

  1. Behavioural couplingripwire <dir> --cochange --legend=compact Bare --cochange emits ONLY the surprising pairs — files that change together in git but share no static dependency either way: <cochange pairs="N" sub_windows="3"><pair a="…" b="…" together="17" deg="1.00" conf_ab="1.00" conf_ba="0.34" driver="a" recur="3" surprising="1"/>… (together = commits co-appearing, deg = fraction of the rarer file's commits). Pure behavioural coupling — shared global state, parallel ownership, or an implicit protocol; statically-linked pairs are filtered out as expected. For ONE file's full partner list, --cochange=FILE.

    Read recur= before you act on a row. It is how many of the header's sub_windows= equal-commit-count slices of the window the pair actually co-changed in. recur="1" at any together= is a single burst — a refactor sprint, a rename wave — not a standing coupling, and it is the biggest source of rows that look alarming and are not. ripwire <dir> --cochange --legend=compact --cochange-recur=2 drops them; the header then carries min_recur= so the shorter list is explained rather than mysterious.

    driver= names the file to look at. conf_ab is "of a's commits, the fraction that also touched b"; conf_ba is the reverse. driver="a" means a is the one that never moves alone, so a is where the implicit dependency lives. deg= is just the larger of the two, and driver= is omitted on a tie.

    ripwire <dir> --cochange --legend=compact --cochange-groups collapses the violating pairs around the file each one implicates: <group core="…" partners="3"> says "this file co-changes with three files it does not depend on" in one row instead of three. That is the row to act on — it names the fix target. The cover is greedy (cover="greedy"), so groups= is an upper bound on the minimum, not the minimum.

  2. Structural couplingripwire <dir> --deps --legend=compact <deps> with god-files ranked by afferent (dependents) and each file's instab (1 = leaf, 0 = core). High-afferent, high-instability files are unstable hubs — every change there fans out. Cross with the step-6 surprising partners: a clone group (step 3) that also co-changes is a maintenance coupling — a fix must land in both copies.

Planning a refactor / "this class is a god object"

When the job is "I'm about to restructure this — where's the seam and what will it break?", chain three passes into a refactor brief:

  1. Find the high-complexity clusterripwire <dir> --communities --legend=compact (cohesive call-graph modules) or ripwire <dir> --metrics --legend=compact --top-k=40 and read ccx/lcom4. lcom4>1 is a god object by the numbers — N disjoint method clusters that want to be N types; the community/--zoom view names the members that belong together. Pick the cluster to break apart.

  2. Its blast radiusripwire <dir> --impact=SYM --legend=compact on the cluster's lead symbol → reaches="N" = every caller a signature change would touch. Large reaches= → stage behind a shim, don't big-bang it. --affected=<files> names the tests to keep green through the move.

  3. Its co-change seamsripwire <dir> --cochange=FILE --legend=compact on the file you're splitting → files that historically move with it. A surprising="1" partner (no static dependency) is hidden coupling the refactor must preserve or explicitly cut — fold it into the plan before you start.

  4. Pick the fix from the SHAPE, not the scoreripwire <dir> --quality-panel --legend=compact (or --metrics) on the cluster, then read humps=/deep=/locals= as above: many shallow humps → extract each one (cheap); one deep tangle → guard-clause inversion then state extraction (expensive); small-and-dense → read it first, density is often the algorithm; join="deep+untested" or high fan-in → test before you touch it. The full shape → fix → precondition table, and the fix-then-prove loop that closes it (--quality-delta--edit-check=SYM--affected), are in ripwire-quality-bar.

Refactor brief: the cluster to split (with its lcom4/ccx evidence and its nesting profile), the blast radius + test gate, the co-change partners to keep in sync, and the named fix each shape calls for. For CI-enforcing the boundary you extract → ripwire-layers.

Output

A one-page health snapshot:

  • Hotspots — top 3 files by score, each with its worst function.
  • Cleanup wins — count of high-confidence dead-code candidates (all still marked verify) + the largest clone group (biggest dedup payoff).
  • Dominant smell — the highest-count lint rule and where it clusters.
  • Knowledge riskbf="1" files, flagging any that are also hotspots.
  • Hidden coupling — top surprising co-change pairs + any that overlap a clone group.
  • One-line verdict — healthy / has-debt / needs-attention, with the single highest-priority item first.

This is the structural read — it doesn't run tests or a compiler. Pair with the build's warnings and test suite; use ripwire-change-check for a specific diff rather than the whole repo.

Date the snapshot. A health report is quoted back weeks later, long after the numbers moved. The git-aware verbs stamp the commit they measured at — --map-diff/--doc-drift carry at="<sha>[+dirty]", --stray-content carries head="<sha>" — but most of this skill's passes (--hotspots, --clones, --lint, --dead-code, --cochange, --owners) carry none. Open the snapshot with the sha you ran at (git rev-parse --short HEAD) so a reader can tell a live finding from an archaeological one.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Ripwire Fresh Eyes AI skill do?

Maintenance risk in code you did NOT write — inheriting a module, planning a refactor, a suspected god object, 'what's gnarly / where's the rot / safe to touch?': hotspots, dead or inactive code, switches/defaults (--flags), clones, a function's real shape, bus factor, co-change. Naming the fix or judging YOUR new code → quality-bar. A single-lens question is a single call.

Why use Ripwire Fresh Eyes on TypingMind?

Because you install it once and use it with any model. Ripwire Fresh Eyes is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Ripwire Fresh Eyes in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/redhat-et/ripwire/tree/main/skills/ripwire-fresh-eyes. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Ripwire Fresh Eyes?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Ripwire Fresh Eyes?

As many as you like. As long as a model supports skills, you can use Ripwire Fresh Eyes with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Ripwire Fresh Eyes AI skill free?

Yes. It is published on GitHub by redhat-et under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇