Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/F1.13.mp4Controls are asserted in a register and never measured, so the first evidence that one was missing is the incident.
Key control indicators computed from source on every change, each gap carrying a named mitigation.
Measure six indicators against the CyberTravels tree and read the five gaps it reports.
The register says the control is in place. Nobody has computed it from the estate, so the first evidence either way will be an incident. An indicator you cannot calculate today is a sentence in a policy wearing a number's clothes.
measure() walks the controls Functions A to E built and calls each one — B2.1's approvers, B3.1's default-deny, B3.2's empty environment, B3.9's expiry, C2.3's five findings, D1.9's coverage, E1.1's matrix. Add a credential to sandbox.CODING_AGENT.env_keys and coverage drops below 1.0, which is what makes it a measurement. known_gaps() ships four absences with the file that admits each one.Measuring the control set against the running system rather than against the register, so removing a control moves the number — with the known absences shipped alongside, since a register reporting no gaps on a system with gaps is one nobody should believe.
A control asserted in a register and never measured has no evidence behind it until an incident supplies some.
Compute the indicators from the repository at run time, and report gaps with the lesson that closes each.
Six indicators against CyberTravels: five gaps and one pass. The pass is what proves the instrument discriminates.
Measure this
| what to measure | denominator or target |
|---|---|
| indicators measured against CyberTravels | six — five gaps and one pass; the pass proves the instrument discriminates |
cybertravels/ (read at run time, not from a register)
|
v
+--------------------------------------------------------+
| KCI-01 handlers comparing an owner == 1.00 0.29 X |
| KCI-02 HTTP calls verifying TLS == 1.00 0.00 X |
| KCI-03 no eval of caller text == 0 1.00 X |
| KCI-04 no shell w/ model argument == 0 1.00 X |
| KCI-05 egress control present == 1 0.00 X |
| KCI-06 no credential literal == 0 0.00 v |
+--------------------------------------------------------+
| |
v v
gap -> the lesson that closes it one PASSING indicator,
(B2.4, C2.7, B3.4, B3.7) so the set can be seen
to discriminateA control indicator is only an indicator if something measures it. Otherwise it is a sentence in a policy, and the first evidence that a control was missing is the incident.
The test is mechanical: can you compute this number from the estate, today, without asking anyone? "Object handlers that compare an owner" passes. "Is authorisation adequate" does not.
So these are computed from cybertravels/ — the same tree C2.3 scans and B1.1 draws — and the output is a gap list, not a maturity score. A governance report whose only artefact is a percentage changes nothing, because nobody can action 72%.
Anchor → F1.1. This is F1.1 executed. Six indicators, computed from the CyberTravels tree at run time, reported as gaps with named mitigations rather than as a score.
A gap with no next step is a complaint. Each one here carries the lesson that closes it — the ownership check, the rule that names your own HTTP wrapper, default-deny on the tool call, the gateway.
One indicator in the set is deliberately one CyberTravels passes. A set that always reports GAP cannot be shown to discriminate, and a reader has no way to tell the instrument works.
KCI-05 is the one worth arguing about: it measures a component that does not exist, so it reads zero and always will until somebody builds a gateway.
The skill — skills/grc/kci-control-measurement/SKILL.md
name: kci-control-measurement
description: >-
Measure key control indicators against a real repository and report each gap
with the lesson that closes it. Use when turning framework controls into
something measurable, when a control is asserted but never verified, or when a
governance report needs an artefact other than a score.
allowed-tools: Read, Grep, GlobOtherwise it is a sentence in a policy. The test is mechanical: can you compute this number from the estate, today, without asking anyone?
So these are computed from cybertravels/ — the same tree C2.3 scans and B1.1 draws — and the output is a list of gaps each naming the lesson that closes it, not a maturity score. A governance report whose only artefact is a number changes nothing.
After control mapping (F1.4) and before any assurance claim. Re-run it after every change to the estate: an indicator measured once is a snapshot, and the whole argument for KCIs is that point-in-time testing fails for systems that change weekly.
1 — Write the indicator as a computation, not a question. "Object handlers that compare an owner" is measurable; "is authorisation adequate" is not.
2 — State the target beside it. An indicator with no target cannot be met or missed.
3 — Compute it from source, not from a register. A register records what somebody believed at the time.
4 — Include at least one indicator the estate passes. A set that always reports GAP cannot be shown to discriminate.
5 — Attach a mitigation to every gap, naming where the fix is taught. A gap with no next step is a complaint.
Input — the cybertravels/ tree, read at run time.
Output — a real run:
KCI-01 object handlers that compare an owner == 1.00 0.29 GAP
KCI-05 egress control present == 1 0.00 GAP
KCI-06 no credential literal in the source == 0 0.00 ok
KCI-05 measures a component that does not exist. That is the backlog, stated in the same units as everything else.
{
"kcis": [{"id": "str", "indicator": "str", "target": "str",
"measured": 0.0, "met": true}],
"gaps": [{"id": "str", "gap": "str", "mitigation": "str"}]
}
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons F1.13Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick f1-13-measuring-the-controls-on-cybertravels:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons F1.13 | type /f1-13-measuring-the-controls-on-cybertravels |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons F1.13 | type /f1-13-measuring-the-controls-on-cybertravels in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons F1.13 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons F1.13 | type $f1-13-measuring-the-controls-on-cybertravels, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py F1.13Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at F1.13 --out work/cybertravels
python3 scripts/checkpoint.py --at F1.13 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
One indicator met and five gaps, each with a named mitigation — and every number computed from the tree at run time rather than read from a register.
Write a seventh indicator for a control CyberTravels does have, and check it reads as met. An instrument that only ever says GAP is not measuring.
What you can do now. A key control indicator you can compute, and everything it needs to be one: an inventory for its denominator, a risk tier for its target, a control map for its subject, an owner against its name, and verification that re-reads it instead of citing last year. F1.13 runs six of them over CyberTravels and returns five gaps.
What you still cannot do. Every one of those indicators answers to you. A regulator does not ask what you measured; they ask which obligation it discharges, on what evidence, and whether the reading predates the incident.
Chapter F2 points the same indicators outward — one measured control set quoted to several regimes, and documentation that is a series of readings rather than a description. Next → F2.1, the regulatory map.
Skill source: skills/grc/kci-control-measurement