okf-pro 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.okf/contract/containment-directions.md +107 -0
  3. data/.okf/contract/exit-codes.md +42 -0
  4. data/.okf/contract/gates-only-at-the-hook-door.md +39 -0
  5. data/.okf/contract/index.md +10 -0
  6. data/.okf/contract/silent-skips.md +78 -0
  7. data/.okf/contract/telemetry-does-not-lie.md +218 -0
  8. data/.okf/contract/the-contract.md +43 -0
  9. data/.okf/design/a-comment-is-not-an-implementation.md +85 -0
  10. data/.okf/design/a-rule-you-can-walk-past.md +68 -0
  11. data/.okf/design/derivation-that-writes.md +161 -0
  12. data/.okf/design/failure-modes.md +43 -0
  13. data/.okf/design/index.md +18 -0
  14. data/.okf/design/lineage.md +95 -0
  15. data/.okf/design/structure-laws.md +46 -0
  16. data/.okf/design/the-residue.md +44 -0
  17. data/.okf/design/three-laws.md +71 -0
  18. data/.okf/design/three-pillars.md +48 -0
  19. data/.okf/index.md +31 -0
  20. data/.okf/log.md +74 -0
  21. data/.okf/scaffold/collisions-and-refusals.md +76 -0
  22. data/.okf/scaffold/index.md +8 -0
  23. data/.okf/scaffold/no-date-ships.md +41 -0
  24. data/.okf/scaffold/ownership-not-subject.md +63 -0
  25. data/.okf/scaffold/the-adopters-manual.md +114 -0
  26. data/.okf/seam/bundler-scoping.md +45 -0
  27. data/.okf/seam/identity-not-existence.md +67 -0
  28. data/.okf/seam/index.md +8 -0
  29. data/.okf/seam/the-wrapper.md +42 -0
  30. data/.okf/seam/three-fail-opens.md +44 -0
  31. data/.okf/testing/drills-over-units.md +41 -0
  32. data/.okf/testing/fixture-is-a-client.md +35 -0
  33. data/.okf/testing/index.md +4 -0
  34. data/.okf/trust/index.md +7 -0
  35. data/.okf/trust/read-owed-rule.md +45 -0
  36. data/.okf/trust/scalar-verified.md +48 -0
  37. data/CHANGELOG.md +187 -0
  38. data/LICENSE.txt +201 -0
  39. data/NOTICE +10 -0
  40. data/README.md +383 -0
  41. data/lib/okf/plugin.rb +148 -0
  42. data/lib/okf/pro/attestation.rb +62 -0
  43. data/lib/okf/pro/audit.rb +161 -0
  44. data/lib/okf/pro/board/edit.rb +201 -0
  45. data/lib/okf/pro/board.rb +291 -0
  46. data/lib/okf/pro/budget.rb +144 -0
  47. data/lib/okf/pro/bundle_root.rb +241 -0
  48. data/lib/okf/pro/cli.rb +914 -0
  49. data/lib/okf/pro/closing.rb +206 -0
  50. data/lib/okf/pro/conformance.rb +127 -0
  51. data/lib/okf/pro/conserve.rb +94 -0
  52. data/lib/okf/pro/event.rb +95 -0
  53. data/lib/okf/pro/friction.rb +262 -0
  54. data/lib/okf/pro/guards.rb +92 -0
  55. data/lib/okf/pro/log/edit.rb +68 -0
  56. data/lib/okf/pro/log.rb +162 -0
  57. data/lib/okf/pro/pairing.rb +271 -0
  58. data/lib/okf/pro/reconcile.rb +76 -0
  59. data/lib/okf/pro/records.rb +90 -0
  60. data/lib/okf/pro/scaffold.rb +258 -0
  61. data/lib/okf/pro/shell_guard.rb +164 -0
  62. data/lib/okf/pro/snapshot.rb +156 -0
  63. data/lib/okf/pro/state.rb +174 -0
  64. data/lib/okf/pro/target.rb +91 -0
  65. data/lib/okf/pro/template/gem/.claude/hooks/run +119 -0
  66. data/lib/okf/pro/template/gem/.claude/skills/okf-pro/SKILL.md +233 -0
  67. data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/attribution.md +46 -0
  68. data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/board.md +55 -0
  69. data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/closing.md +21 -0
  70. data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/frontmatter.md +73 -0
  71. data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/okf-rules.md +25 -0
  72. data/lib/okf/pro/template/gem/.githooks/pre-commit +68 -0
  73. data/lib/okf/pro/template/gem/.github/workflows/okf-pro.yml +69 -0
  74. data/lib/okf/pro/template/seed/.claude/settings.json +24 -0
  75. data/lib/okf/pro/template/seed/.okf/areas/corpus.md +73 -0
  76. data/lib/okf/pro/template/seed/.okf/areas/index.md +6 -0
  77. data/lib/okf/pro/template/seed/.okf/board.md +21 -0
  78. data/lib/okf/pro/template/seed/.okf/glossary/index.md +13 -0
  79. data/lib/okf/pro/template/seed/.okf/index.md +30 -0
  80. data/lib/okf/pro/template/seed/.okf/journal/index.md +11 -0
  81. data/lib/okf/pro/template/seed/.okf/learnings/index.md +13 -0
  82. data/lib/okf/pro/template/seed/.okf/log.md +10 -0
  83. data/lib/okf/pro/template/seed/.okf/projects/index.md +23 -0
  84. data/lib/okf/pro/template/seed/.okf/reference/index.md +16 -0
  85. data/lib/okf/pro/template/seed/.okf/roadmap.md +33 -0
  86. data/lib/okf/pro/template/seed/.tmp/.gitkeep +0 -0
  87. data/lib/okf/pro/template/seed/CLAUDE.md +61 -0
  88. data/lib/okf/pro/template/seed/README.md +488 -0
  89. data/lib/okf/pro/template/seed/gitignore +17 -0
  90. data/lib/okf/pro/version.rb +7 -0
  91. data/lib/okf/pro/writes.rb +560 -0
  92. data/lib/okf/pro.rb +198 -0
  93. metadata +169 -0
data/.okf/log.md ADDED
@@ -0,0 +1,74 @@
1
+ # Update Log
2
+
3
+ ## 2026-08-17
4
+
5
+ * **okf-pro 1.0.0.** The prototype checker — carried inside a template
6
+ repository that was cloned to start a bundle — becomes the fourth gem in this
7
+ monorepo, entered through the okf plugin seam as `okf pro <cmd>` rather than a
8
+ repository to fork. It ships no executable: a second entry point would be a
9
+ second thing the scaffold's wrapper has to recognise, on the one code path
10
+ where being wrong means a gate waves an edit through.
11
+
12
+ * **Three doors, and a contract that outranks them.** The same invariants are
13
+ asked at the agent's tool boundary, at `git commit` against the *staged* tree,
14
+ and again in CI, because each door sees edits the others structurally cannot.
15
+ Blocking checks fail closed, feedback checks fail loud, and no check ever fails
16
+ silent — the clause that shapes the code, since every defect this gem has had
17
+ failed in the direction of silence
18
+ ([contract/the-contract](/contract/the-contract.md)). Getting there closed
19
+ three ways the plugin seam let an unchecked edit through
20
+ ([seam/three-fail-opens](/seam/three-fail-opens.md)), the case where a shim on
21
+ `PATH` passes every gate by exit status alone
22
+ ([seam/identity-not-existence](/seam/identity-not-existence.md)), bundler
23
+ scoping that switched every gate off inside a bundle that did not name this
24
+ gem ([seam/bundler-scoping](/seam/bundler-scoping.md)), and the checker
25
+ breaking its own third clause by reporting clean over checks it never ran
26
+ ([contract/silent-skips](/contract/silent-skips.md)). The `hook` verb is the
27
+ one place in this repo where exit 1 is *non-blocking* and 2 is the refusal
28
+ ([contract/exit-codes](/contract/exit-codes.md)).
29
+
30
+ * **The CLI answers what is on the board, and writes the shapes with one form.**
31
+ The prototype's surface was gates and nothing else, so an agent in a seeded
32
+ bundle rediscovered state by reading raw markdown and reconstructed a line's
33
+ grammar from the guides on every use. `state`, `board`, `snapshot`,
34
+ `unverified` and `friction` answer, cheaply and in JSON when the consumer is an
35
+ agent; `capture`, `promote`, `demote`, `journal open` and `close` write.
36
+ Writers are additive and targeted, never regenerative — each computes its new
37
+ text purely, declares the delta it intends, and a conservation guard refuses
38
+ with nothing written when the actual delta differs in either direction
39
+ ([design/derivation-that-writes](/design/derivation-that-writes.md)). A name a
40
+ caller supplies is contained twice over
41
+ ([contract/containment-directions](/contract/containment-directions.md)).
42
+
43
+ * **A recorder that neither gates nor lies.** `friction` writes down what was
44
+ done by hand that a verb could have done, at paths that already run rather than
45
+ at a new hook event, since `settings.json` is seeded and a registration added
46
+ there would never reach an adopter through `upgrade`. It refuses nothing and
47
+ still may not report a zero it did not count
48
+ ([contract/telemetry-does-not-lie](/contract/telemetry-does-not-lie.md)).
49
+
50
+ * **`okf pro setup` replaces the template repository.** It writes the bundle and
51
+ the governance around it, splitting what it may later rewrite from what becomes
52
+ the adopter's by ownership rather than by subject
53
+ ([scaffold/ownership-not-subject](/scaffold/ownership-not-subject.md)). It
54
+ never refuses wholesale ([scaffold/collisions-and-refusals](/scaffold/collisions-and-refusals.md)),
55
+ and no date ships in the generated tree, because dormancy measures a bundle's
56
+ age by its oldest journal entry and a shipped entry makes a fresh clone read as
57
+ an old one ([scaffold/no-date-ships](/scaffold/no-date-ships.md)).
58
+
59
+ * **The seeded README sells the practice, not the command set.** It is the only
60
+ file in the generated tree addressed to the owner rather than the agent, and it
61
+ carries what the skill never will: the exit, the limits, the price, and a week
62
+ showing the system in use. The offer is that being good at any craft has a
63
+ shape and you reach it by structuring how you work
64
+ ([scaffold/the-adopters-manual](/scaffold/the-adopters-manual.md)).
65
+
66
+ * **The trust policy is the format's, not this gem's.** "Awaiting the owner's
67
+ read" is derived from OKF v0.2 §5.3's tiers rather than from a truthiness test
68
+ on `verified:`, across all four call sites at once — they cannot move
69
+ separately ([trust/read-owed-rule](/trust/read-owed-rule.md)). A
70
+ `process:`-verified briefing is machine-confirmed and still owes the owner a
71
+ read. A scalar `verified:` is conformant and unreadable, which is the worst
72
+ direction to fail in ([trust/scalar-verified](/trust/scalar-verified.md)).
73
+ `freshness` is gone: `stale_after` is the format's own field, and a second
74
+ implementation disagreeing by a day was worse than none.
@@ -0,0 +1,76 @@
1
+ ---
2
+ type: Decision
3
+ title: Setup writes what it can, and refuses exactly one thing
4
+ description: A collision is staged beside the adopter's file rather than refused; a root-level bundle is refused outright, because writing there manufactures an ambiguous layout.
5
+ ---
6
+
7
+ # Never refuse wholesale
8
+
9
+ A repository that already has a `CLAUDE.md` is the common case, not an error.
10
+ `setup` writes everything it can and stages the rest as `<path>.okf-pro-new`,
11
+ with a printed merge instruction. A stale staged file is **refreshed**, never
12
+ doubled — there is no `.okf-pro-new.okf-pro-new`.
13
+
14
+ Which files can collide at all is a question of [who owns them](/scaffold/ownership-not-subject.md):
15
+ inside `.okf/` nothing is ever staged, only completed.
16
+
17
+ # And the one refusal has to ask the question it cites
18
+
19
+ The refusal names `Audit.ambiguous_layout` as the state it prevents, and for a
20
+ while it did not ask what that finding asks. The finding tests `root_kind` —
21
+ `index.md` **plus** `board.md` or `log.md`, or an `okf_version` in the
22
+ frontmatter. The refusal tested `bundle?`, which is `index.md` alone.
23
+
24
+ So every Jekyll section, every Hugo directory, every repository with an
25
+ `index.md` README was refused at its first `okf pro setup`, and told to
26
+ `git mv index.md log.md <your dirs> .okf/` — files that have nothing to do with
27
+ OKF, for an ambiguity the audit would never have reported. A first run is the
28
+ one moment an adopter has no reason to trust the tool yet, and this spent that.
29
+
30
+ **A refusal that cites a finding has to be answering the question that finding
31
+ asks.** Two predicates for one condition is one predicate too many, and the
32
+ looser one always wins the argument by refusing more.
33
+
34
+ The generated `.gitignore` lists `*.okf-pro-new`, and that line is
35
+ load-bearing: inside `.okf/` those files are invisible to `validate` and `lint`
36
+ because they are not `.md`, so without it they get committed and nothing ever
37
+ says so.
38
+
39
+ Which is why the merge instruction **reads the adopter's tree rather than
40
+ assuming it**. The closing line used to state flatly that `.gitignore` already
41
+ ignores them, and that was false in the one case where it mattered: when
42
+ `.gitignore` is itself the collision, the adopter's own file stays on disk and
43
+ the template's — the copy carrying the line — is what got staged beside it. A
44
+ report that says "already ignored" about files git is about to commit is the
45
+ same silence one paragraph up, produced by the fix for it.
46
+
47
+ # The one refusal
48
+
49
+ A bundle whose root **is** the repository root. Writing `.okf/index.md` beside
50
+ it collides with nothing, so a naive generator succeeds — and leaves two bundle
51
+ roots in one directory, which is exactly the state `Audit.ambiguous_layout`
52
+ exists to report, where the three doors disagree until one of the two is
53
+ retired.
54
+
55
+ `setup` detects it and exits 2 naming the migration, which is one `git mv` the
56
+ adopter can see. Manufacturing a state your own audit is written to complain
57
+ about is worse than refusing to start.
58
+
59
+ # Atomicity, stated honestly
60
+
61
+ Each file is written to a temp path in its destination directory and renamed, so
62
+ **no half-written file exists**. A nineteen-file sequence is not atomic as a
63
+ *set*, and this does not pretend otherwise. The real safety is that `setup` is
64
+ idempotent and re-runnable, which is what makes an interrupted run harmless.
65
+
66
+ # Two mechanics worth keeping
67
+
68
+ The `.gitignore` template is stored as `gitignore`, without the dot. As a real
69
+ dotfile in the gem's own tree it would be a live gitignore governing its own
70
+ directory — silently deciding what `git ls-files` reports, and therefore what
71
+ ships.
72
+
73
+ The generated file list is compared against `spec.files`, **not** against a glob
74
+ of the template. Both sides of a glob-versus-glob comparison ignore
75
+ `.gitignore`, so such a check passes in a checkout while the installed gem is
76
+ short files — which is the failure it exists to catch.
@@ -0,0 +1,8 @@
1
+ # Scaffold
2
+
3
+ What `okf pro setup` writes, and the decisions that shape it.
4
+
5
+ * [ownership-not-subject](ownership-not-subject.md) - The gem-owned / seeded split, and why the obvious cut destroys the adopter's own files.
6
+ * [no-date-ships](no-date-ships.md) - A shipped date ages into somebody else's calendar, and dormancy reads a fresh clone as an old bundle.
7
+ * [collisions-and-refusals](collisions-and-refusals.md) - Never refuse wholesale, except into a flat-layout bundle.
8
+ * [the-adopters-manual](the-adopters-manual.md) - The seeded README is the only file written for the owner; it sells the practice rather than the command set, and a fresh reader can measure whether it answers them.
@@ -0,0 +1,41 @@
1
+ ---
2
+ type: Decision
3
+ title: No date ships in the generated bundle
4
+ description: A template is cloned an unknowable number of days after it is built, and every shipped date ages into somebody else's calendar.
5
+ ---
6
+
7
+ # The rule
8
+
9
+ No visible date anywhere in the generated tree: no log heading, no journal
10
+ entry, no `updated` line on the board. The empty-state paths are the design —
11
+ an undated `log.md` passes the audit, the session banner says `Last snapshot:
12
+ none yet`, dormancy stays quiet while the journal is empty, and the first dirty
13
+ day's stop gate hands over the computed snapshot line, which is the ritual
14
+ teaching itself.
15
+
16
+ # The failure that decided it
17
+
18
+ Dormancy measures a bundle's age by its **oldest journal entry**. A shipped
19
+ day-zero entry therefore makes a fresh clone read as an old bundle, and the
20
+ adopter's first promotion draws a dormancy question it never earned — on their
21
+ first day, from a gate they have no reason yet to trust.
22
+
23
+ The journal guard compounds it: a past day is append-only, so a foreign entry is
24
+ locked against correction forever.
25
+
26
+ None of this is a rule about tidiness. It falls out of the same question
27
+ [the ownership split](/scaffold/ownership-not-subject.md) answers: the bundle is
28
+ the adopter's from the moment it is written, and a date in it is a claim about
29
+ their days.
30
+
31
+ # The exemption, which is required rather than cosmetic
32
+
33
+ `projects/index.md` teaches the closure marker, and `Pairing::MARKER` **requires**
34
+ a date. The example cannot lose its date without teaching a spelling the gate
35
+ rejects, so dates inside code spans are exempt — and the exemption is what the
36
+ invariant test encodes, rather than a blanket ban it would then have to violate.
37
+
38
+ That coupling is pinned in both directions: every spelling the template and the
39
+ skill teach must be one `MARKER` accepts, and every counter-example the skill
40
+ names must be one it refuses. A spelling taught and rejected leaves the project
41
+ open and tells the person who followed instructions that they did it wrong.
@@ -0,0 +1,63 @@
1
+ ---
2
+ type: Decision
3
+ title: The scaffold splits by ownership, not by subject
4
+ description: Knowledge-versus-machinery is the obvious cut and the wrong one — it makes `upgrade` destroy exactly the hand-merge that adopting the gem asked for.
5
+ ---
6
+
7
+ # The two classes
8
+
9
+ **Gem-owned** — the hook wrapper, the pre-commit hook, the CI workflow, the
10
+ skill. These carry [the contract](/contract/the-contract.md) and the exit-code
11
+ protocol, so they must track the gem or a gate goes quietly wrong. `upgrade`
12
+ rewrites them outright.
13
+
14
+ **Seeded** — `README.md`, `CLAUDE.md`, `.gitignore`, `.claude/settings.json`,
15
+ and the whole of `.okf/`. Written once, and the adopter's from that moment.
16
+
17
+ # Why the obvious cut is wrong
18
+
19
+ Sorting by subject matter — knowledge in one pile, machinery in the other — puts
20
+ `CLAUDE.md`, `.gitignore` and `settings.json` on the machinery side, because
21
+ that is what they look like. Then `upgrade` overwrites them.
22
+
23
+ But `CLAUDE.md` at a repository root is the adopter's agent-instruction file;
24
+ `.gitignore` accumulates project entries from day one; `settings.json` is where
25
+ they add hooks of their own. Every one of them is a file the scaffold's own
26
+ instructions tell them to merge into. An upgrade that rewrote them would destroy
27
+ exactly the work adopting this thing asked for — and would do it on the run
28
+ after the one where they did it.
29
+
30
+ The question is not *what is this file about*. It is *whose file is it once it
31
+ exists*.
32
+
33
+ # The two front doors say the same thing twice, deliberately
34
+
35
+ `README.md` and `CLAUDE.md` both describe the three doors, in nearly the same
36
+ words. That is not drift waiting to be collapsed, and the argument for
37
+ collapsing it is a real one that has to be answered rather than ignored: one
38
+ operative copy beats two agreeing copies until the day they stop agreeing, and
39
+ this bundle applies that rule everywhere else.
40
+
41
+ It does not apply here, because these two files have **different readers and no
42
+ shared moment**. `CLAUDE.md` is loaded by an agent at the start of every session
43
+ that touches the bundle; `README.md` is read by a person, once, on the day they
44
+ arrive. Neither reader sees the other's file, so a pointer from one to the other
45
+ is a dead end rather than a single source: the agent will not follow a link to
46
+ prose written for a human, and the human should not have to read the agent's
47
+ instructions to learn how to arm a git hook.
48
+
49
+ What the rule actually forbids is two copies of a *fact that changes* — a count,
50
+ a path, a version. The doors are a fixed shape, and each file states them for
51
+ its own reader in its own register. The seeded files are the adopter's from the
52
+ moment they are written ([nothing rewrites them](/scaffold/no-date-ships.md)),
53
+ so even a real divergence stays local to one repository rather than shipping.
54
+
55
+ # The `.okf/` exemption
56
+
57
+ Seeded files are staged beside the adopter's as `<path>.okf-pro-new` on a
58
+ collision — except inside `.okf/`, where only missing files are added.
59
+
60
+ A bundle that exists is a bundle already adopted, and eleven shadow files
61
+ scattered through someone's knowledge is not a merge prompt, it is litter — in
62
+ the one directory whose entire value is that everything in it was put there
63
+ deliberately.
@@ -0,0 +1,114 @@
1
+ ---
2
+ type: Learning
3
+ title: The seeded README is the only document written for the owner
4
+ description: Every other seeded file addresses the agent, so the adopter's README carries the exit, the limits and the price — and a fresh reader can measure whether it does.
5
+ ---
6
+
7
+ # What the seeded README is for
8
+
9
+ `setup` writes two prose files for humans and one for the machine. `CLAUDE.md`
10
+ and `.claude/skills/okf-pro/` address the **agent**: the routing table, the
11
+ frontmatter policy, the line grammars. `README.md` is the only thing in the tree
12
+ addressed to the **person who owns the repository**, and it is the file they
13
+ land on when they open the directory.
14
+
15
+ That makes it a product surface rather than a courtesy, and it has to answer the
16
+ questions the skill never will, because the agent has no use for them: what does
17
+ a day cost me, what will this refuse, how do I change the numbers, and how do I
18
+ turn it off.
19
+
20
+ # The exit is the section that buys the rest
21
+
22
+ A layer built out of refusals owes the reader a way to stop being refused, and
23
+ the section documenting it is worth more than any section arguing the benefits.
24
+ Stated as four escalating levers — one gate, all the agent-time gates, the
25
+ commit door, everything — plus what survives removal, it converts "this thing
26
+ constrains me" into "this thing constrains me *and told me where the door is*".
27
+
28
+ Two properties make it real rather than reassuring:
29
+
30
+ * **Every lever is verified by running it**, not described from the source. A
31
+ documented escape that does not work is worse than none — see
32
+ [a rule you can walk past](/design/a-rule-you-can-walk-past.md), whose fourth
33
+ clause is exactly this.
34
+ * **What survives is stated.** Delete the machinery and `.okf/` is still a
35
+ bundle `okf validate` passes, because the format was never the enforcement's
36
+ to own. That is the payoff of building on a spec, and it is only worth
37
+ anything if the README says so out loud.
38
+
39
+ The absence of an environment kill switch belongs here too, as a *feature* with
40
+ its reason attached: a gate a stray `export` could disable is one nobody has
41
+ reason to trust.
42
+
43
+ # Measuring a README instead of proof-reading it
44
+
45
+ A document's defects are not a matter of taste, and the maintainer is the worst
46
+ available judge of their own page because they cannot un-know what it omits. The
47
+ method that works: hand a reader **only that file**, ask a fixed set of a
48
+ reader's real questions, and record which ones came back CONFIDENT, PARTIAL and
49
+ BLANK.
50
+
51
+ The blanks are the finding. Three surfaced here that no amount of re-reading had
52
+ caught, in a file its author considered finished: no exit, no non-promises, and
53
+ no picture of `board.md` — the one file the whole system pivots on, quoted only
54
+ in fragments. A reader cannot report a section that was never there; they can
55
+ report a question they could not answer, and those are the same fact.
56
+
57
+ # The two rules the measurement produced
58
+
59
+ **The same content can be right in one document and wrong in another, because of
60
+ where the reader is standing.** The four-way gloss on the word *Pro* is good
61
+ writing and belongs on the package page, where "is this a paid tier?" is a live
62
+ misreading worth killing in the first screen. Dropped into the adopter's own
63
+ repository it was the single sentence that lost the reader — someone standing in
64
+ a new directory is asking what to do, not what the name means. Neither placement
65
+ is a quality judgement about the prose. Position is the variable.
66
+
67
+ **The loudest claim is the one that must carry evidence.** This page's method is
68
+ claim-plus-evidence — the cap refusal, the snapshot line and the promote
69
+ conflict are all shown as real output. The attestation gate was asserted as "the
70
+ most valuable thing here" four separate times and shown exactly never, and it
71
+ was the only claim a fresh reader singled out as unevidenced. A document that
72
+ proves its cheap claims and asserts its expensive one has inverted its own
73
+ argument: the reader calibrates on what you demonstrate, so the thing you most
74
+ want believed is the thing that can least afford to be described.
75
+
76
+ # The manual is for the practice, not the command set
77
+
78
+ The first draft of this page was a CLI manual: every worked example a
79
+ `$ okf pro …` transcript, a command table high on the page, and a "first hour"
80
+ that was a list of verbs to type. It read as competent and it sold the wrong
81
+ thing, because it described the machinery instead of the practice the machinery
82
+ exists to hold.
83
+
84
+ The system is agent-facilitated. The owner's actual moves are: *say what
85
+ happened* to their agent, *triage* once a day, *approve or deny* an attestation,
86
+ and *read the delta* at the next session start. The verbs are what the agent
87
+ reaches for — the session banner tells it so in as many words — and the owner
88
+ can go a week without typing one. A page whose every example is a shell prompt
89
+ teaches the reader they are operating a tool, when what is being offered is a
90
+ way of working.
91
+
92
+ So the narrative shows board states and refusal messages, and the commands get
93
+ one section, late, framed as what your agent uses and what you may occasionally
94
+ want to run yourself. The gem's README can afford to lead with the command set —
95
+ its reader is choosing a package — and the adopter's cannot.
96
+
97
+ # The pitch is the practice, not the preservation
98
+
99
+ The defensive framing — *your knowledge will rot and this stops it* — is true and
100
+ undersells the thing. The offer is that being good at any craft has a shape:
101
+ knowing what you are committed to, telling what you know from what you have
102
+ merely read, not relearning a lesson twice, being able to say where a claim came
103
+ from. Most people reach that shape after a decade, by accident, and many never
104
+ do — because nothing in an ordinary week forces the question.
105
+
106
+ The mechanism is structure rather than effort, and saying so matters: a small
107
+ number of shapes, each with one place it lives and one form it takes, and gates
108
+ that refuse the edits that break them. Give the practice a structure and the
109
+ discipline stops being something you have to remember to have.
110
+
111
+ That reading also settles a question a fresh reader raised twice: whether this is
112
+ for software. None of the rules names code, so the answer is that the domain is
113
+ irrelevant, and the page has to say it outright rather than let a week of
114
+ tech-flavoured examples answer it by implication.
@@ -0,0 +1,45 @@
1
+ ---
2
+ type: Finding
3
+ title: Bundler scoping switches the gates off, and the fix locks out the in-bundle adopter
4
+ description: Gem discovery is bundle-scoped once bundler's variables are exported, so `okf pro` is an unknown command inside a bundle that does not name okf-pro.
5
+ ---
6
+
7
+ # Both directions, both measured
8
+
9
+ okf finds its extensions with `Gem.find_latest_files`, which is bundle-scoped
10
+ once bundler's variables are in the environment.
11
+
12
+ ```
13
+ BUNDLE_GEMFILE=okf/Gemfile bundle exec okf tui --help
14
+ → unknown command, exit 2
15
+ ```
16
+
17
+ An agent working inside any bundled Ruby project would find every gate silently
18
+ absent. But the repair has a mirror image: **inside a bundle,
19
+ `Gem.find_latest_files("okf/plugin.rb")` returns `[]` outright**. A repository
20
+ that deliberately vendors okf-pro through its own Gemfile is served *only* by
21
+ the un-stripped run, so a wrapper that sanitised up front would lock it out of
22
+ its own checker.
23
+
24
+ The wrapper therefore tries un-stripped **first** and falls back to stripped,
25
+ with `OKF_PRO_NO_UNBUNDLE=1` to pin the first. Both directions are drilled.
26
+
27
+ Both branches live in [the wrapper](/seam/the-wrapper.md), which is the only
28
+ place that can act before okf starts.
29
+
30
+ # The variable list was wrong, and any list would be
31
+
32
+ A first attempt stripped `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_BIN_PATH`.
33
+ Bisected: `BUNDLE_PATH` and `BUNDLE_BIN_PATH` are inert, and the two that matter
34
+ are `RUBYOPT` and `BUNDLER_SETUP`. `BUNDLER_SETUP` is recent enough that naming
35
+ it is a bet on the adopter's bundler version — on a gem whose Ruby floor is 2.4.
36
+
37
+ So the wrapper uses bundler's **own** restoration data, the `BUNDLER_ORIG_*`
38
+ variables it exports for exactly this purpose, and is version-proof by
39
+ construction rather than by an enumeration someone has to maintain.
40
+
41
+ # The general shape
42
+
43
+ When a dependency already records how to undo what it did, use that record. A
44
+ hand-written inverse of somebody else's transformation is correct only for the
45
+ versions you tested.
@@ -0,0 +1,67 @@
1
+ ---
2
+ type: Learning
3
+ title: Existence is not identity
4
+ description: A stray `okf` on PATH that exits 0 passes every gate, and no exit-code normalisation can tell it from a clean check.
5
+ ---
6
+
7
+ # The claim that was wrong
8
+
9
+ "Normalise the exit codes — map anything that is not 0 or 2 to 2 — and the stray
10
+ binary is handled."
11
+
12
+ It is not. A shim that exits **0** maps to 0. Every gate is off, silently, and
13
+ the status carries no evidence either way: a clean check and a program that did
14
+ nothing produce the same byte.
15
+
16
+ # What replaced it
17
+
18
+ `okf pro hook` writes `okf-pro-enforcer v1` to stderr as the first act of
19
+ every check, and the wrapper refuses unless it sees that line. Costs one process
20
+ and 0.16 s against 0.14 s for a bare normalised wrapper.
21
+
22
+ The earlier design was a **handshake** — a second invocation, `okf pro
23
+ contract`, answering the same string. It proved less at twice the cost: the
24
+ handshake passed while the library was missing, because answering a question is
25
+ not the same as reaching a check. The marker is emitted at the point where
26
+ dispatch has already committed to running one, so its presence means the check
27
+ was reached, not merely that the plugin loaded.
28
+
29
+ # The door that had the argument written down and not applied
30
+
31
+ The commit hook asked `command -v okf` and then trusted the exit status — the
32
+ exact claim the section above calls wrong, at the one door that exists for the
33
+ edit the agent hooks never see. A `#!/bin/sh\nexit 0` shim named `okf` made
34
+ `okf pro records` and `okf pro audit` both pass, and the commit went through
35
+ with nothing checked and nothing said.
36
+
37
+ It proves identity the same way now, and it has to borrow the hook door to do
38
+ it: the marker is written by `okf pro hook`, before a check runs, and the CI
39
+ verbs do not emit it. So the handshake runs a real gate — `okf pro hook
40
+ guard-verified` on an empty event, which reaches the check and gives it nothing
41
+ to say — and requires the marker on stderr. Borrowing the door is the point:
42
+ identity is proved by *reaching a check*, which is what the retired `okf pro
43
+ contract` handshake could not do.
44
+
45
+ The general form is worth stating, because this door had the reasoning
46
+ available and did not apply it: **a defence written in one file is not a
47
+ defence of the system.** Three doors exist precisely because each sees what the
48
+ others cannot, so a proof that only one of them makes is a proof with a hole
49
+ the shape of the other two.
50
+
51
+ The check itself is [the wrapper's](/seam/the-wrapper.md), for the same reason
52
+ everything else there is: it has to happen before the program being identified
53
+ gets to answer for itself.
54
+
55
+ # What it does not defend against
56
+
57
+ A shim that deliberately prints the marker. That is a forgery, and no wrapper
58
+ defends against one — the threat model here is a leaked variable, a stale shim,
59
+ a half-installed gem. Claiming more would be the false confidence that is worse
60
+ than no rule.
61
+
62
+ # The general shape
63
+
64
+ When a check's "pass" and a check's "absence" produce the same observable, no
65
+ amount of interpreting that observable will separate them. A second, orthogonal
66
+ observable has to exist — and it has to be emitted from the point that proves
67
+ what you actually want proven.
@@ -0,0 +1,8 @@
1
+ # Seam
2
+
3
+ The entry point, and the three ways it lets an unchecked edit through.
4
+
5
+ * [three-fail-opens](three-fail-opens.md) - A ScriptError outside every rescue, a non-Integer status, and a SyntaxError nothing Ruby-side can reach.
6
+ * [the-wrapper](the-wrapper.md) - Why the shell script cannot be collapsed into the checker, and why it stopped exec'ing.
7
+ * [identity-not-existence](identity-not-existence.md) - A stray `okf` exiting 0 is indistinguishable from a clean gate by status alone.
8
+ * [bundler-scoping](bundler-scoping.md) - Gem discovery is bundle-scoped, and the fix for that locks out the deliberate in-bundle adopter.
@@ -0,0 +1,42 @@
1
+ ---
2
+ type: Decision
3
+ title: Why the wrapper is a separate shell script
4
+ description: A Ruby checker structurally cannot refuse on its own absence, so something already running has to — and it must not exec.
5
+ ---
6
+
7
+ # The argument
8
+
9
+ A script whose interpreter is missing exits 127. A script whose syntax the
10
+ interpreter rejects exits 1. [Both are non-blocking](/contract/exit-codes.md).
11
+
12
+ So a checker written in Ruby cannot refuse when it is missing, broken, or
13
+ unparseable: by the time anything could report, nothing is running. That is the
14
+ whole job of `.claude/hooks/run`, and it is why `settings.json` points there and
15
+ never at `okf` directly.
16
+
17
+ # It does not exec, and that is a change
18
+
19
+ The prototype's wrapper ended in `exec`, and carried a comment saying a
20
+ SyntaxError "no wrapper can catch, because nothing ran". That was true of an
21
+ exec'ing wrapper and is false of this one: after `exec`, the wrapper is gone and
22
+ the failing process's status is the answer. Staying resident is what lets it
23
+ catch [the third fail-open](/seam/three-fail-opens.md).
24
+
25
+ # Two things it must not do
26
+
27
+ **It must not capture stdout.** A `PreToolUse` `ask` decision is JSON on stdout
28
+ with exit 0, and the SessionStart banner travels the same way. A draft of this
29
+ fix used `rc=$( okf pro hook "$check" )` to read the status — command
30
+ substitution captures stdout, so `rc` became output-plus-status, the `case` fell
31
+ through to its default, and the most load-bearing gate in the system stopped
32
+ asking and started denying. The hardening broke the thing it was hardening.
33
+
34
+ **It must not resolve `okf` twice.** The binary that proved itself is the binary
35
+ that runs: `okf_bin="$(command -v okf)"` once, dispatched to absolutely. A
36
+ re-resolution after changing the environment is a different program.
37
+
38
+ # The residue
39
+
40
+ `"$CLAUDE_PROJECT_DIR"` stays quoted in `settings.json`. Unquoted, a path with a
41
+ space word-splits, the shell exits 127, and all four gates are disarmed by a
42
+ directory name.
@@ -0,0 +1,44 @@
1
+ ---
2
+ type: Finding
3
+ title: Three fail-opens in the plugin seam
4
+ description: Moving the checker behind okf's plugin seam introduced three paths where a failure exits 1 — which the hook protocol reads as "proceed".
5
+ ---
6
+
7
+ # The measurements
8
+
9
+ `okf/exe/okf` is `exit OKF::CLI.start(ARGV)`, and `CLI#dispatch` calls
10
+ `command.new(...).call(argv)` with **no rescue**. Discovery's own rescue wraps
11
+ `require path` and catches `LoadError, StandardError`.
12
+
13
+ 1. **A `LoadError` from the deferred `require "okf/pro"` is a `ScriptError`**,
14
+ which is not a `StandardError` and is outside every rescue on the path.
15
+ Measured: process exit **1**, edit proceeds. This is what a half-installed or
16
+ partially-deleted gem looks like.
17
+ 2. **A non-Integer status** lands in the same place. `exit(true)` is 0 and
18
+ `exit(nil)` is 1; neither is a verdict, and a CLI ported from a standalone
19
+ binary plausibly calls `exit`.
20
+ 3. **A `SyntaxError` in `lib/okf/plugin.rb`** is a `ScriptError` too, so
21
+ discovery does not catch it either. The whole CLI dies with a parse dump and
22
+ exit 1, and **no gem code runs at all** — so neither Ruby-side guard can
23
+ reach it. Only [the wrapper](/seam/the-wrapper.md) can.
24
+
25
+ # Where the guard has to live, and where it must not
26
+
27
+ The rescue is in `OKF::CLI::Pro#call`, **outside** the require it guards. It
28
+ cannot be inside `Pro::CLI.run`: that method is reached *through* the require
29
+ that fails, so it would be a guard against the one thing it cannot witness.
30
+
31
+ It is `rescue Exception`, which is the cop's textbook mistake everywhere else in
32
+ this repo and the correct call exactly here — the failures are `ScriptError`s,
33
+ and catching less than `Exception` is the same as catching nothing.
34
+
35
+ `SystemExit` is re-raised rather than swallowed. `okf/pro.rb` refuses an
36
+ under-floor Ruby with `exit 2`, and a rescue that turned that refusal into an
37
+ error report would be the fail-open the whole method exists to close.
38
+
39
+ # The general shape
40
+
41
+ A deferred require is a load-time failure moved to call time. When the caller's
42
+ error handling was written for the code being loaded rather than for the loading
43
+ itself, the move creates a hole exactly the width of the exception hierarchy the
44
+ caller does not catch.
@@ -0,0 +1,41 @@
1
+ ---
2
+ type: Learning
3
+ title: Drills, not unit tests, at the enforcement seam
4
+ description: Every defect found in the entry point was a gate that passed while unable to check, which only a real process with a real exit status can catch.
5
+ ---
6
+
7
+ # What a drill is here
8
+
9
+ A subprocess, a real `PATH`, a real event on stdin, and the process's real exit
10
+ status read back. Not a method call.
11
+
12
+ That is not thoroughness for its own sake. The statements being tested are
13
+ statements *about a process*: "the hook protocol reads this as non-blocking",
14
+ "the shebang exits 127 when the interpreter is missing", "a bare locale makes
15
+ `Encoding.default_external` US-ASCII and an em dash raises". None of them is
16
+ observable from inside one interpreter with injected streams.
17
+
18
+ # The list is the failure list
19
+
20
+ Each drill names a way the seam was found to break, and every one of them is a
21
+ gate that **passed**:
22
+
23
+ | Drill | Before |
24
+ |---|---|
25
+ | okf-pro's library unloadable | exit 1, edit proceeds |
26
+ | a syntax error in `plugin.rb` | exit 1, no gem code ran at all |
27
+ | a stray `okf` exiting 0 | exit 0, every gate off |
28
+ | `hook audit` | exit 0, "clean.", no stdin read |
29
+ | a bundled environment | unknown command, exit 2, gates absent |
30
+ | a project path with a space | exit 127, four gates disarmed |
31
+
32
+ None of them is a wrong answer. They are all *no answer*, wearing the costume of
33
+ a clean one — which is [the third clause](/contract/the-contract.md), from the
34
+ outside.
35
+
36
+ # And what must still get through
37
+
38
+ Half the drills assert a **pass**: the event reaches the check, an `ask`
39
+ decision reaches stdout intact, the session banner survives, the identity marker
40
+ is stripped before anything downstream sees it. A wrapper that refused
41
+ everything would satisfy every refusal drill in the table above.