| name | keel-coverage |
| description | Compute and post the per-PR test-coverage delta (base → head), flag low-coverage × high-risk hot spots, and open issues to close gaps — routed to keel:ship. |
keel-coverage
Use this skill when the user asks to run the keel command coverage (e.g. keel coverage ..., coverage <args>, or /keel:coverage). It reads every project value from .keel/project.yaml via the keel CLI.
/keel:coverage
Live progress — stamp this run (required)
So this run shows live on keel-visual's board, record it with keel activity as you
go. This command's phases are: orient → areas → baseline → head → delta → hotspots → post. Pick one stable --run-id for the whole run
(e.g. coverage-<issue-or-pr>):
- Right now, before the work below, stamp the first phase:
keel activity .keel/project.yaml --root . --write --command coverage --run-id "$RUN" --phase orient
- Re-run with the next
--phase (areas, …) as you advance through the flow.
- At the end:
keel activity .keel/project.yaml --root . --run-id "$RUN" --done
Treat this like any other contractual step — do not skip it. The one allowed exception is a
core too old to ship keel activity (keel < 1.6.0): then skip it silently and never block
the command.
Command step evidence
Every numbered step in this command is contractual. Complete the step, record the
evidence it asks for, or explicitly mark it N/A — <reason> before moving on. If a step
has an external side effect such as a GitHub comment, issue, review, report, branch, or
PR, the side effect must be posted or written through the selected transport and cited in
the final summary. Never silently skip a step because the runtime, agent, or prompt feels
obvious.
Project-neutral coverage report. Every project value — the base branch, the coverage tooling
per area, the risk map, the repo — is read from .keel/project.yaml via the keel CLI. The
coverage command is the project's (its test/coverage command via its toolchain); this
adapter never names a specific coverage tool.
The command is read-only on PR code: it never modifies the PR diff, never closes the PR,
and never merges. It runs coverage twice (base + head), produces a delta table, and posts (or
updates) exactly one PR comment. Re-runs find the existing codename-prefixed comment and update
it in place so the timeline never stacks duplicates. All published artifacts (the PR comment)
MUST be English.
Step 0 — orient + parse arguments
keel validate .keel/project.yaml --root .
keel plan .keel/project.yaml --root .
keel plan .keel/project.yaml --root . --command coverage --live --json
The live plan is the operator-consent preflight. Before creating worktrees, writing local
coverage cache files, posting comments/labels/issues, using secrets, publishing, or calling
production-adjacent systems, parse contract.operator_consent; if
requires_operator_consent is true, STOP and ask the operator to rerun with the required
--approve-scope values.
Arguments:
- Positional, optional: a single positive integer PR number. Default: derive the PR from
the current branch; if no open PR is tied to the current branch and no positional was given,
exit non-zero with
no PR for current branch — pass an explicit PR number. Reject more than
one positional and zero/negative integers.
--base <branch> — consumes exactly one branch name after the flag. Default: base_branch
from the plan. Reject --base with no value after it.
--threshold <pct> — a coverage floor to compare hot spots against (Step 5).
--changed — scope coverage to the PR diff (git diff base...HEAD) rather than the whole
tree.
--open-issues — open a deduped issue per hot spot (Step 6) and route each to /keel:ship.
--dry-run — compute the report and log the would-be comment/label mutations to stdout as
DRY-RUN: …; make no write.
- Reject unknown flags.
Step 1 — detect areas touched + availability
Get the PR's changed-file list and classify which project areas it touches (derive areas
from the repo layout). If the PR touches no instrumented area, post a short
codename-prefixed comment noting there is no coverage signal (no files under an instrumented
area) and exit; under --dry-run, log the would-be body and skip the post.
Coverage needs the host CLI (or MCP read path) for the PR file list and the comment. If that is
unreachable, note it and degrade per the MCP-mode rule in Step 6.
Step 2 — compute the baseline (base-branch head)
Use a dedicated worktree so the PR checkout is not disturbed: fetch --base and add a
worktree at its head. For each touched area, run the project's coverage command for that
area and parse its report for the per-area + overall line coverage percentage
(covered / (covered + missed) * 100). Cache results keyed by <commit-sha>:<area> so a
re-run on the same SHA does not recompute.
Graceful degradation (per area): if an area's coverage tool is not wired up, skip that
area's coverage and add a note (<area> coverage skipped: tooling not wired up), continuing
with the other areas. Discover the exact coverage task by name rather than hardcoding it (task
names vary by tool version).
Step 3 — compute head (PR head)
Run the same commands against the PR-head checkout (the working tree if it is already on the PR
head, else a second worktree), reusing the task discovered in Step 2. Cache keyed by
<head-sha>:<area>.
Step 4 — build the delta report
Do not hand-write the delta table. Assemble the structured coverage data and let the
renderer own the layout — the base@ → head@ header, per-area tables (unit · base % · head % ·
signed Δ · files), the signed-Δ formatting (+1.2% / -0.3% / +0.0%), the bold-whole-row
when |Δ| >= 0.5% rule, the bold overall row, and skipped-area italic lines — then post the
rendered body verbatim:
keel render-report --kind coverage --payload coverage.json > body.md
coverage.json is one object:
{ "codename": "COVERAGE-<PR>-<UTC_TIMESTAMP>", "base_sha": "<short>", "head_sha": "<short>",
"areas": [
{ "name": "<area>",
"rows": [{ "unit": "<module>", "base": 91.2, "head": 92.4, "files": 3 }],
"overall": { "base": 90.0, "head": 91.5 } },
{ "name": "<area>", "skipped": true, "skip_reason": "<why>" }
] }
Codename pin (load-bearing): the codename field becomes the rendered body's literal first
line — COVERAGE-<PR>-<UTC_TIMESTAMP>, with no blank line, whitespace, quoting, or Markdown
prefix above it — because Step 6 finds the existing comment by the COVERAGE-<PR>- prefix to
update it in place. The prefix in Step 6 MUST match this codename; change them together or
find-and-update silently breaks. Omit an untouched area entirely.
Step 5 — flag hot spots (low coverage × high risk)
Cross the per-file/area coverage against tier3_globs: a low-coverage file that also matches
a tier-3 glob is a hot spot — surface these first (high risk × low coverage). Compare each
against --threshold if given. Hot spots are what Step 6 opens issues for.
Step 6 — post / update the PR comment + open hot-spot issues
Find the existing coverage comment by the COVERAGE-<PR>- prefix on the PR.
- Existing found and not
--dry-run → update it in place (edit the existing comment).
- None found and not
--dry-run → post a new comment.
--dry-run → log the body to stdout, no API call.
Only ever one coverage comment per PR after the first run. In-place-update gap: if the
runtime can create comments but cannot edit one in place (e.g. an MCP path with no
update-comment tool), do not post a second comment — compute and log the delta, emit a
one-line note that an existing comment was found and in-place update is unavailable (re-run
locally, or delete the prior comment first), and continue.
Label (compute the worst overall regression across the areas that ran): if at least one area
regressed by ≥ 0.5%, idempotently ensure and add a coverage-regression label; otherwise remove
it if a prior run added it (ignore "not present" errors). The label is informational, not
gating. Under --dry-run, log the would-be transition and skip.
When --open-issues is set, open a deduped issue per hot spot (Step 5), tiered by
tier3_globs, and hand each fix to /keel:ship (window + lock + review). Under --dry-run,
print the would-be issues and route nothing.
Stop conditions / invariants
- Never modify the PR's code; never close or merge — the only writes are one PR comment
(created or edited) and one PR label edit (plus hot-spot issues under
--open-issues).
Gating/merge is /keel:ship's job.
- Fail loudly on a coverage build/test failure — print the failing command and exit
non-zero; do NOT post a half-formed table (reviewers would treat it as authoritative). This is
distinct from a tool not wired up, which degrades gracefully per Step 2.
- Worktree cleanup — always remove the base (and head) worktree in a trap on EXIT so a
mid-run failure never leaks a worktree.
- No silent dry-run mutations — every comment/label/issue write is printed as
DRY-RUN: …
and skipped.
- Deterministic for identical coverage data.