code-metrics
code-metrics tells you what your change did to the numbers — structure, debt,
readability, decay, load risk — at the one moment you can still act on it
cheaply: before you open the PR.
What it solves
Quality metrics regress quietly, and the author is the last to notice. A function creeps past a complexity ceiling, coverage slips a couple of points, a fresh import cycle appears — review rarely catches any of it, and a dashboard only surfaces the trend long after the change shipped. The feedback arrives too late and too far from the person who caused it.
This skill closes that gap by being two things at once: immediate (a local read on your branch, no CI round-trip) and near-free (the numbers are pure tooling output, so running it costs essentially no tokens). It only spends a model when something is actually wrong.
How it works
The skill is hybrid — deterministic measurement, model-assisted only on breach.
The numbers are produced by a per-stack adapter that runs your language’s real tooling and normalizes the output to a common contract. The adapter is selected automatically from the repo’s detected stack, so the same command and the same metric set work across languages. Each metric is declared in a single registry (direction + where its threshold lives), so the catalog grows by one line plus one adapter function — never a new branch in the orchestrator.
What it measures
The catalog is layered by the question it answers and the cost of answering it:
- Structure —
coverage,complexity,module_size,dependency_cycles. - Debt & readability — counts of
TODO/FIXME,@deprecated/[Obsolete], marked dead code, and linter/compiler suppressions; plusnesting_depth, averageparam_count,magic_numbers,lint_density, anddoc_coverage. These are the cheap, objective signals a team can’t argue with — the point of measuring readability with counters instead of a subjective grade. - Decay —
hotspots: the files that change a lot and are complex (git churn × cyclomatic), where risk actually concentrates over time. - Load risk —
perf_risk: a static proxy for queries/allocations inside loops and nested loops. It is heuristic and high-false-positive by nature, so it is reported as info — surfaced, never gated.
Every metric is a deterministic shell heuristic (grep / awk / lizard / git) and ratchet-only by default, so a legacy repo is never born red: only a new regression on your branch is flagged.
Full metric reference
↑ higher is better · ↓ lower is better · info reported, never gated.
“Default” is the behaviour with no .octopus.yml config; the parenthesised key
is the optional absolute threshold you can set under code_metrics:.
| Metric | Layer | Dir | What it measures | Default |
|---|---|---|---|---|
coverage | structure | ↑ | Line coverage % (TS: vitest→LCOV · C#: dotnet-coverage/coverlet→Cobertura) | ratchet (coverage.min) |
complexity | structure | ↓ | Average cyclomatic complexity per function (lizard) | ratchet (complexity.max) |
module_size | structure | ↓ | Average NLOC per function/file (lizard) | ratchet (module_size.max) |
dependency_cycles | structure | ↓ | Import cycles (TS: madge) / project-reference cycles (C#: dotnet list reference + Tarjan) | ratchet (dependencies.cycles_allowed) |
todo_markers | debt | ↓ | Count of TODO/FIXME/HACK/XXX | ratchet |
deprecations | debt | ↓ | @deprecated (TS) / [Obsolete] (C#) | ratchet |
dead_code | debt | ↓ | Marked dead code (#if false, // dead, unused-disable) — not reachability | ratchet |
suppressions | debt | ↓ | eslint-disable/@ts-ignore/@ts-nocheck (TS) · #pragma warning disable/[SuppressMessage] (C#) | ratchet (suppressions.max) |
nesting_depth | readability | ↓ | Deepest brace nesting level | ratchet (nesting_depth.max) |
param_count | readability | ↓ | Average parameters per function (lizard) | ratchet (param_count.max) |
magic_numbers | readability | ↓ | Numeric literals, excluding 0/1/-1, named constants and strings | ratchet |
lint_density | readability | ↓ | Linter findings per 1000 NLOC (TS: eslint · C#: build warnings); 0 if no linter | ratchet |
doc_coverage | docs | ↑ | Documented public/exported symbols ÷ total | ratchet (doc_coverage.min) |
hotspots | decay | ↓ | Count of files that are both high-churn and high-complexity — git log churn × lizard CCN. Tune via hotspots.{window_days,churn_min,ccn_min} | ratchet (hotspots.max) |
perf_risk | load risk | info | Queries/allocations inside loops + nested loops (O(n²) candidates) | info-only (never gated) |
Heuristic caveats (all metrics are shell-grade, approximate by design):
perf_riskkeys on a loop opener with its{on the same line; Allman-brace codebases (idiomatic C#, where{sits on its own line) under-detect and often read0. It is info-only precisely because of this.magic_numbers,nesting_depthanddoc_coveragescan the source tree and do not exclude generated code (e.g. EF migrations), so they can read high — they are ratchet anchors, not absolute quality scores.
It reports a dual delta per metric:
- vs. baseline — your branch against the last-merged-to-main baseline (the trend anchor).
- vs. main — your branch against your local
mainHEAD (this PR’s isolated impact).
The baseline is read (git fetch) from a dedicated, non-protected ref
(octopus/code-metrics) that a push-to-main Action keeps fresh; the read
path never writes it.
Thresholds are ratchet by default — a metric may not regress against the
baseline — with optional absolute targets layered in .octopus.yml
(coverage.min, complexity.max, suppressions.max, nesting_depth.max,
hotspots.max, and so on; perf_risk takes none). A field with no configured
target stays on the ratchet. Only when a gated metric crosses its threshold
does the skill invoke a low-cost model to read the offending change, explain why
the number moved, and suggest a fix. No breach means no model call — the cost
contract that keeps it worth running on every PR.
Usage & parameters
octopus code-metrics [--stack <id>] [--metric <name>] [--verbose] [--emit-baseline]| Flag | What it does | Default |
|---|---|---|
--stack <id> | Force a stack adapter (csharp, typescript) instead of auto-detecting. | auto-detected |
--metric <name> | Report a single metric instead of the full catalog. | all |
--verbose | Show raw tooling output alongside the summary. | off |
--emit-baseline | Print the flat baseline.json from this run (used by the writer-Action). | off |
The numeric run is always model-free; the curation step fires only on a threshold breach, on the harness’s low-cost model.