Immutable. This exact content is served forever at /api/v1/blob/6a978df99c1d2006.
# AGENTS.md
Maintainer guide for okf-pro — `okf-pro` on RubyGems. It turns an
[Open Knowledge Format](https://github.com/serradura/okf-gem) bundle into a
working memory an agent is held to: `okf pro setup` writes the bundle and the
governance around it, and `okf pro hook` runs one gate against one hook event.
This file documents how to change the code without breaking its contracts.
This gem is a sibling in the okf-gem monorepo, one directory per gem under
`gems/`, beside the baseline `gems/okf/` it depends on.
[`../../AGENTS.md`](../../AGENTS.md) is the repo-level
guide and owns everything above a single gem — the layout, the PR shape, the
release-title convention, the Git attribution rule. What is here is okf-pro's
own: the contract, the exit codes, the seam, and the scaffold. Where the two
overlap, the root is the general rule and this is the instance.
## The contract, which outranks everything below it
> Blocking checks fail **closed**. If enforcement is missing or cannot run, the
> call is refused, loudly.
>
> Feedback checks fail **loud**. If enforcement is degraded, it says so in the
> same channel it would use to refuse.
>
> No check ever fails **silent**. A gate that is sometimes absent and does not
> confess converts "unchecked" into "checked and fine", which is worse than
> having no gate at all.
Every defect this gem has ever had failed in the direction of silence. A gate
that cannot run, a check that was skipped, a shim on `PATH`, a status code the
protocol reads as "proceed" — each produces an unchecked bundle that is
indistinguishable from a clean one. When you change anything here, the question
to ask is not "is this correct?" but "what does it do when it cannot answer?"
The argument in full, and the failures behind each clause, is in
[`.okf/contract/`](.okf/contract/the-contract.md).
## Map
```
lib/okf/pro.rb the contract, the exit codes, the two reads, the requires
lib/okf/pro/
bundle_root.rb pure where the bundle is, given where the agent is
event.rb pure the hook event — the only place untrusted input is parsed
target.rb shell bundle root + edited path, or nil when a check cannot apply
board.rb pure board.md as data: sections, budget header, links, dates
log.rb pure log.md as data: the snapshot line, the newest day
pairing.rb shell the board↔work invariants both ways, plus the one git shell-out
guards.rb pure the trust rules — attestation, and the append-only record
shell_guard.rb shell the same rules at the door a shell command comes through
records.rb shell the append-only record, asked of git at the commit door
conformance.rb shell okf validate + okf lint, in process
reconcile.rb shell Rule 1
budget.rb shell Rule 3 — the cap, and the dormancy question
closing.rb shell Rule 2 — the stop gate, and the session banner
snapshot.rb shell Rule 2's counters, derived — checker, never generator
attestation.rb shell what still awaits the owner's read
state.rb shell the readers' payload — cheap by contract; --full is the one parse
conserve.rb pure the write contract, enforced: line multisets in, refusals out
board/edit.rb pure the board's text transforms, and the keyed selectors
log/edit.rb pure a dated line, under its day, newest-first
writes.rb shell the mechanical writers: read, transform, guard, rename
friction.rb shell what the verbs did not cover, recorded — never enforced
audit.rb shell the CI door: the same invariants minus the tool event
scaffold.rb shell the generator: setup, upgrade, skill
cli.rb shell dispatch, and the exit codes the hook protocol reads
template/ the tree `setup` writes — gem/ (upgrade rewrites) + seed/ (yours)
lib/okf/plugin.rb the okf extension seam, and the last Ruby-side line of the contract
```
`require "okf/pro"` loads everything: this gem is not an embedding library,
it is a checker, and every module is on the path of some gate.
**One door.** This gem ships **no executable**. `lib/okf/plugin.rb` registers a
`pro` command with okf's registry, and `okf pro` is how a user gets here.
That is load-bearing rather than tidy, and more so than for the siblings that
made the same call: the scaffold's wrapper dispatches to **one** absolute `okf`
and refuses unless that binary identifies itself as the enforcer. A second entry
point would be a second thing for the wrapper to recognise, on the one code path
where being wrong means a gate waves an edit through.
## Hard constraints
1. **Ruby >= 2.4**, okf's own floor. It matters more here than anywhere else in
the repo: this code runs inside a git hook and a CI step on machines nobody
chose, and a checker that cannot parse is a checker that is off. The forbidden
APIs are listed in [`../../AGENTS.md`](../../AGENTS.md); the ones this port had to
undo were `filter_map` (2.7) and endless ranges (2.6). The floor is checked
twice — in the gemspec, and at the top of `lib/okf/pro.rb`, which refuses
with `exit 2` — and `test/unit/pro_test.rb` pins that the two agree.
2. **Runtime dependencies are exactly `okf`.** Everything else is stdlib. A
gate with a dependency tree is a gate that fails to install on the machine
that needed it most.
3. **`hook`'s exit codes are the protocol's, not this repo's.** `0` passes, `2`
blocks, and **every other code — including `1` — is non-blocking**: the tool
call proceeds. So `hook` never returns 1. Every other verb keeps the repo's
0/1/2 convention, and `audit` in particular reserves 1 for *findings* and
spells "the checker broke" as 2 — a pipeline that cannot tell those apart
learns to ignore both. The table is in
[`.okf/contract/exit-codes.md`](.okf/contract/exit-codes.md).
Which is why **every verb in `CLI::READERS` routes through `parse_flags`**,
listed in `FLAGS` or not: absence from that table means "accepts none", and a
verb that skips the parser hands its undeclared flag to `BundleRoot.resolve`
as a directory and reports "holds no OKF bundle" as a finding — a pipeline's
own typo, spelled as a broken bundle. `test/integration/cli_test.rb` pins it
over `READERS`, which is the invariant and the whole of it: the writers go
through `writer_flags` instead (their first argument is content, so a flag
there is data), and the scaffold verbs and `hook` through neither.
4. **The seam is where the contract's last Ruby line lives.** `plugin.rb` holds
a `rescue Exception` that re-raises `SystemExit`, with the cop disabled and
the reason beside it. That is not laziness: the failures it catches are
`ScriptError`s, which are not `StandardError`s, and it must sit **outside**
the `require "okf/pro"` it guards — inside `Pro::CLI.run` it could not
catch the LoadError of the require that reaches it. See
[`.okf/seam/three-fail-opens.md`](.okf/seam/three-fail-opens.md).
5. **A `SyntaxError` in `plugin.rb` is unreachable from Ruby.** Discovery
rescues `LoadError, StandardError`; a `SyntaxError` is neither, the CLI dies
with a parse dump and exit 1, and *no gem code runs at all*. Only the
scaffold's `.claude/hooks/run` can catch it, which is why that wrapper no
longer `exec`s. If you make it `exec` again, you have reopened this.
6. **`hook` whitelists its argument.** `Pro::CLI.run` dispatches the CI verbs
off the same first argv element a check name arrives in, so an adapter that
only stripped `hook` would make `okf pro hook audit` run a CI verb —
measured status 0, "clean.", reading no stdin and never blocking. The
whitelist is `Pro::CLI::HOOK_NAMES`, read from the library rather than
copied, so it cannot drift from the composition table.
7. **The gate leaves no check silently unrun.** `Linter.call` with no options
skips `expired` and `stale` and still reports `healthy?`. Supply what a check
needs, or exclude it in source with its reason; never let
`stats[:skipped_checks]` come back non-empty and be discarded.
`test/integration/conformance_test.rb` asserts the residue is empty.
8. **A write verb is additive and targeted, never regenerative**, and that is
enforced rather than promised. Each one computes its new text purely (in
`board/edit.rb` or `log/edit.rb`, which cannot touch the disk), declares the
delta it intends, and `Conserve` refuses with exit 2 — nothing written — if
the actual delta differs in either direction. No verb writes a concept body
or sets `verified:`; `snapshot` gains no `--write`, because a writer and a
checker sharing a code path agree trivially. Agent text reaches a board line
body or a journal entry body and nowhere else, and text spanning lines is
refused rather than escaped — a verb invoked through Bash is seen by neither
`guard-verified` nor `shell-guard`, so the safety is by construction. And a
verb **refuses a missing file rather than writing one**: `capture` will not
create `board.md`, `journal open` will not create `journal/index.md`, because
an index rebuilt from the one line a verb knows is regeneration wearing an
append's clothes. A name the caller supplies is contained twice over — one
directory segment, *and* resolving inside the bundle, because
`projects/<slug>` can be a symlink out and the checker that reads it refuses
to. See
[`.okf/design/derivation-that-writes.md`](.okf/design/derivation-that-writes.md)
and [`.okf/contract/containment-directions.md`](.okf/contract/containment-directions.md).
9. **Friction is recorded at paths that already run, and never at a new hook
event.** `.claude/settings.json` is seeded, so a registration added there
would never reach an adopter through `upgrade`. The recorder refuses nothing
and blocks nothing — and still may not report a zero it did not count: a
failed write leaves a marker, an unwritable `.tmp/` is asked about directly,
and an unparseable line is confessed. Nor may it ask for a verb nothing is
missing: the session banner counts only rows an `okf pro` verb would answer
(a shell redirect's answer is Edit or Write), and `--issue` prints nothing
when nothing was recorded. It is telemetry about the tooling, so it is not
one of `okf pro state`'s sources.
[`.okf/contract/telemetry-does-not-lie.md`](.okf/contract/telemetry-does-not-lie.md).
10. **The scaffold splits by ownership, not by subject.** `upgrade` rewrites
the four gem-owned files and never touches a seeded one. Reclassifying
`CLAUDE.md`, `.gitignore` or `settings.json` as machinery makes `upgrade`
destroy exactly the hand-merge `setup` told the adopter to perform. See
[`.okf/scaffold/`](.okf/scaffold/ownership-not-subject.md).
11. **No date ships in the generated bundle**, outside a code span. The
exemption is required rather than cosmetic — `projects/index.md` teaches the
closure marker and `Pairing::MARKER` requires a date — and both directions
of that coupling are pinned by `test/unit/closure_grammar_test.rb`.
12. **`lib/okf/pro/template/**` is what `setup` writes**, and it ships:
`test/integration/scaffold_test.rb` compares the generated list against
`spec.files`, *not* against a glob of the template, because a glob-versus-glob
comparison ignores `.gitignore` on both sides and would pass in a checkout
while the installed gem was short files. The `.gitignore` template is stored
as `gitignore`, without the dot, for the same class of reason.
## Testing: drills first
The critical layer is `test/integration/wrapper_test.rb`, and it is unusual
enough to state plainly: **it runs the wrapper as a subprocess**, with a real
`PATH`, a real event on stdin, and the process's real exit status read back.
That is not thoroughness for its own sake. The statements being tested are
statements *about a process* — "the protocol reads this as non-blocking", "the
shebang exits 127 when the interpreter is missing", "a bare locale makes
`Encoding.default_external` US-ASCII and an em dash raises" — and none of them
is observable from inside one interpreter with injected streams.
Every drill there names a way the seam was found to break, and **every one of
them was a gate that passed**. When you add a way for enforcement to be absent,
add its drill. When you fix one, the drill goes in first and fails for the
reason you predicted.
Half the drills assert a **pass**, and they are not optional: the event reaches
the check, an `ask` decision reaches stdout intact, the session banner survives,
the identity marker is stripped before anything downstream sees it. A wrapper
that refused everything would satisfy every refusal drill in the file.
Beyond that, the repo's rules apply: one file per verb and subcommand, real
fixtures rather than mocks, a failing test before the fix. Note that
`BundleFixture` is a **client of the code under test** — `write_log` computes
its snapshot line by calling the code — so "the suite is green, therefore
nothing changed" holds only for changes that do not touch what the fixture
calls. See [`.okf/testing/`](.okf/testing/fixture-is-a-client.md).
## Commands
```sh
bin/setup # install dependencies
bundle exec rake # test + rubocop — the default task, what CI runs
bundle exec rake test # just the suite (SimpleCov report in coverage/)
ruby -Ilib -I../okf/lib ../okf/exe/okf pro audit . # the CLI from the checkout
```
The Gemfile points `okf` at the checkout next door, so every run here — local,
CI, the floor container — resolves the checkout and nothing crosses to RubyGems
by default. Two things stand in that gap. `test/unit/gemspec_test.rb` is the
standing one: it fails the moment okf bumps and the declared floor does not
follow. And a run against the **published** kernel is the one that catches the
rest — this gem pins a frozen snapshot of okf's `Linter::SEVERITIES`, so a
released kernel that reclassified a check changes what the gate blocks on, which
is a difference no floor expresses:
```sh
sed '/gem "okf", path:/d' Gemfile > Gemfile.ci-check
BUNDLE_GEMFILE=Gemfile.ci-check bundle install && BUNDLE_GEMFILE=Gemfile.ci-check bundle exec rake
```
The 2.4 floor is proven the way the repo's is, from the root, stepping into this
gem:
```sh
docker run --rm -v "$PWD":/src:ro ruby:2.4 bash -c \
"cp -a /src /build && cd /build/okf-pro && rm -f Gemfile.lock && bundle install --quiet && bundle exec rake test"
```
## Its own bundle
`.okf/` ships inside the gem, and `rake okf` at the repo root validates and
lints it. It carries what the code cannot say for itself: the three fail-opens
in the seam, the check the gate skipped in silence, why identity is not
existence, and what the ownership split in the scaffold is protecting. Maintain
it in the same commit as the code it documents.