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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 89c3d1c35f2504c135c9875a57050aac3288610a5bb812b067faf1284905ab5b
4
+ data.tar.gz: 8d25d56ce3601cc0fd16e72b6ad43c358b69ee859baac16d0abcb855171cbc64
5
+ SHA512:
6
+ metadata.gz: 4f17f3f09f2aac6ceb618bcfb0e871c51cf55cc62b701b55b57154ff10b27c02fa84bdc09cc6b38759113f6536afd7246c74e2a3b4233c202c69b59df6a563b1
7
+ data.tar.gz: d9487c17f613c163b49283b6a626a3e74f0133eafcf1231bd9266d21237573d0d8f799fa4a637da6bda9379e2013d00454979d4be4acea908dd57555072fd774
@@ -0,0 +1,107 @@
1
+ ---
2
+ type: Decision
3
+ title: Containment fails in four different directions, on purpose
4
+ description: A path that escapes the bundle is refused, or ignored, or read as "not closed" — and which one is right is decided per call site by what a wrong answer costs, at the write door as well as the read one.
5
+ ---
6
+
7
+ # The rule
8
+
9
+ Every gate reads through `Pro.read_contained`, which resolves the path and
10
+ refuses one whose symlinks leave the bundle root. A `board.md` symlinked out is
11
+ not this bundle's board; a journal entry reached from outside is not this
12
+ bundle's record of a day.
13
+
14
+ That much is uniform. What happens *next* is not, and the divergence is the
15
+ decision worth recording — because "handle it consistently" is the instinct, and
16
+ here it would be wrong four times over.
17
+
18
+ # The four answers
19
+
20
+ | Site | On escape | Why that direction |
21
+ |---|---|---|
22
+ | `Target#read` | **raises** | The caller already proved the file exists, so an escape is a real containment failure. `CLI.run`'s dispatch rescue turns it into a refusal, which is the correct answer to something outside the bundle pretending to be inside it. |
23
+ | `Budget` (dormancy) | **reads as empty** | Dormancy is a question, not a gate. An unreadable entry means *no evidence of work*, which keeps the question being asked — the conservative direction for a prompt that costs nothing to answer. |
24
+ | `Pairing.closed?` | **reads as open** | A closure marker reached only through a symlink is not one anyone may rely on. Open is the safe answer: an open project keeps its board line, and a closed one loses it. |
25
+ | `Writes.close` (the marker) | **refuses** | The write door, and the same reasoning arrives at a refusal: a marker written outside the bundle is a stranger's file edited to satisfy a checker that will not read it back. |
26
+
27
+ Read the column right to left and the rule generalises: **pick the direction
28
+ that leaves the demand visible.** A refusal, a dormancy question and a retained
29
+ board line are all states a person will notice. Their opposites — a silent pass,
30
+ a project that stops being asked about, a board line quietly removed, a closure
31
+ marker on a file this bundle never reads — are all states nobody finds out
32
+ about.
33
+
34
+ # The read door and the write door are two decisions
35
+
36
+ The table above was written about reads, and that was the whole of the miss.
37
+ `Pairing.closed?` refused to read a project index reached through an escaping
38
+ symlink and answered *open*; `Writes.close` read and rewrote **the same path**
39
+ with the uncontained pair, `Pro.read_text` and `Scaffold.write_atomically`. So
40
+ `okf pro close escapee` put `— closed <date>` on somebody else's `index.md`,
41
+ exited 0, and left `okf pro audit` reporting the project as neither on the board
42
+ nor closed. Run twice it appended a second marker, so it was unbounded as well
43
+ as wrong.
44
+
45
+ Two things follow, and the second is the one worth carrying:
46
+
47
+ * **A containment decision belongs to a call site, not to a file.** The checker
48
+ and the writer touch one path and are two sites; deciding once and assuming
49
+ the other inherited it is how the strictest door and the loosest end up on
50
+ the same `index.md`.
51
+ * **Contain the leaf, not the directory holding it.** The first fix asked the
52
+ question of `projects/<slug>/`, which left the case one level down: an
53
+ `index.md` symlinked out of a perfectly contained directory is read by
54
+ `Pro.read_text`, which follows the link, and written back by `File.rename`,
55
+ which does not — so the marker was computed from a stranger's title and
56
+ landed as a real file *inside* the bundle, and the checker went on refusing
57
+ to read the path it was written from. Containing the leaf covers both, since
58
+ a directory that escapes takes every path under it with it.
59
+ * **Every file the verb writes, not the one the bug was reported against.**
60
+ The leaf fix still only guarded `projects/<slug>/index.md`, and `board.md`,
61
+ `log.md` and `journal/index.md` are rewritten by exactly the same read-and-
62
+ rename pair: `okf pro capture` read a stranger's board through a link,
63
+ appended a line and renamed a temp over it, so the link became a real file
64
+ holding content the bundle never owned. A containment fix that stops at the
65
+ path in the report is a fix for the report, not for the class — `Writes`
66
+ now asks the question in one helper and every verb names the files it
67
+ touches.
68
+ * **The two doors must agree about the same path.** They may reach different
69
+ *answers* — the checker says open, the writer refuses — but those two are the
70
+ same verdict seen from either side, and it is the direction that leaves the
71
+ demand visible. A pair that disagrees is worse than either alone: the write
72
+ lands and the check then denies it happened.
73
+
74
+ Nothing enforces the pairing. It is asked at review, the way this file's rule
75
+ already is, and the question is the same one: *what does this do when it cannot
76
+ answer?*
77
+
78
+ # Contain against the bundle, not against a root invented from the path
79
+
80
+ `Pairing.closed?` derived its root as `dirname(dirname(index_path))`, which is
81
+ `<bundle>/projects` — so `SafeRead` refused any project index whose realpath
82
+ left `projects/`, including one that never leaves the *bundle*. A project
83
+ legitimately archived behind a symlink (`projects/beta -> ../archive/beta`) read
84
+ as open forever, and the audit demanded a board line for work closed months ago.
85
+
86
+ The rule this page states is about **the bundle**, and every caller already held
87
+ it. A containment check that computes its own root is checking a boundary
88
+ nobody drew: it refuses inside the line it was meant to draw, and the direction
89
+ of that error is the one this page warns about from the other side — a demand
90
+ raised against a person who has already done the work is how a gate teaches
91
+ people to stop reading it.
92
+
93
+ # The one place it is deliberately absent
94
+
95
+ `BundleRoot.root_index?` has no root to contain against; it is the code
96
+ *deciding* what the root is. A raise there would be a permanent lockout, and a
97
+ rescue answering "not a root" would mis-root the bundle and disarm the journal
98
+ guard — so it reads plainly, and the comment there says why.
99
+
100
+ # Why `.scrub` is still ours
101
+
102
+ `SafeRead.read!` tags the encoding without validating it. One invalid byte
103
+ anywhere then raises out of the first regex that touches the string, and that
104
+ exception exits 1 — which the hook protocol reads as *proceed*
105
+ ([exit-codes](/contract/exit-codes.md)). The scrub is not tidiness; it is the
106
+ difference between a gate that runs and a gate that lets the edit through while
107
+ appearing to have run.
@@ -0,0 +1,42 @@
1
+ ---
2
+ type: Decision
3
+ title: Exit codes
4
+ description: The hook protocol reads 0 and 2 and treats every other code as non-blocking, which inverts the repo's usual 0/1/2 convention for `hook` alone.
5
+ ---
6
+
7
+ # What the protocol actually reads
8
+
9
+ Claude Code's hook protocol reads **0** as pass and **2** as block. Every other
10
+ exit code — including **1** — is a non-blocking error: the message is surfaced
11
+ and *the tool call proceeds*.
12
+
13
+ That is the whole reason this gem exists in the shape it does. A crash is a
14
+ pass, from the protocol's side. An interpreter that will not start is a pass. A
15
+ `LoadError` is a pass. Any defect that produces a code other than 2 is a defect
16
+ that lets the edit through.
17
+
18
+ # The deviation, and where it stops
19
+
20
+ `AGENTS.md` states the repo's convention as 0 ok / 1 failing bundle / 2 usage
21
+ error. `okf pro hook` cannot keep it: 1 is a code the protocol ignores, so it
22
+ is never returned. Every other verb keeps the convention.
23
+
24
+ | Verb | 0 | 1 | 2 |
25
+ |---|---|---|---|
26
+ | `hook <check>` | pass | *never* | block, crash, misconfiguration |
27
+ | `audit`, `records` | clean | findings | usage, or could not run |
28
+ | `snapshot`, `unverified` | printed | — | usage, or no bundle |
29
+ | `setup`, `upgrade`, `skill` | wrote or staged | — | usage, or refused |
30
+
31
+ # Why a crash inside `audit` is 2 and not 1
32
+
33
+ `audit` runs in CI, where 1 already means "your bundle has findings". If a crash
34
+ also exited 1, a pipeline could not tell *your bundle is broken* from *the
35
+ checker is broken* — and the second one is the case where the green runs either
36
+ side of it mean nothing. So 1 means findings and nothing else, and a crash exits
37
+ 2 with a named cause.
38
+
39
+ The report-only verbs take this further: they have no findings code at all. A
40
+ non-empty `unverified` list is the truth about a corpus, not a defect in it, and
41
+ an exit code that called it a failure would pressure toward the one lie the
42
+ whole system guards against.
@@ -0,0 +1,39 @@
1
+ ---
2
+ type: Decision
3
+ title: Only gates answer at the hook door
4
+ description: The CI verbs are not checks, and accepting one as a check name would report clean without ever reading the event.
5
+ ---
6
+
7
+ # The whitelist
8
+
9
+ `HOOK_NAMES` is `CHECKS.keys` plus `session-context`, and nothing else. The CI
10
+ verbs — `audit`, `records`, `snapshot`, `unverified` — are deliberately not in
11
+ it, so `okf pro hook audit` is a refusal rather than a run.
12
+
13
+ They read like checks. They ask the same invariants, they live in the same CLI,
14
+ and an earlier version of this checker put all of them in one flat list of
15
+ accepted names.
16
+
17
+ # Why that list was a fail-open
18
+
19
+ A CI verb takes a **directory**. A hook check takes an **event on stdin**.
20
+ Dispatch one from the hook door and it never reads the event at all: it inspects
21
+ whatever bundle it resolves, finds nothing wrong with a tree the edit has not
22
+ touched yet, and exits 0.
23
+
24
+ Exit 0 at the hook door means *the gate ran and found nothing*. So a misspelling
25
+ in `settings.json` that happened to land on a CI verb would disarm that gate
26
+ permanently, and the only evidence would be a hook that never once complained —
27
+ which is indistinguishable from a bundle that was always clean.
28
+
29
+ That is the contract's third clause ([no check fails
30
+ silent](/contract/the-contract.md)) broken by the dispatcher rather than by a
31
+ check, and it is why the refusal names the distinction out loud instead of just
32
+ saying "unknown check": *the CI verbs are not gates and would report clean
33
+ without reading the event at all.*
34
+
35
+ # The same reasoning, one door along
36
+
37
+ `okf pro hook --help` is refused for the identical reason. Help exits 0, and a
38
+ `settings.json` that reached a help branch would be a gate switched off in
39
+ silence. It exits 2 and points at `okf pro --help`, where help actually lives.
@@ -0,0 +1,10 @@
1
+ # Contract
2
+
3
+ The three clauses, and what each costs when it is broken.
4
+
5
+ * [the-contract](the-contract.md) - Blocking checks fail closed, feedback checks fail loud, no check fails silent.
6
+ * [exit-codes](exit-codes.md) - Why `hook` reads only 0 and 2, and why `audit` never spells a crash as 1.
7
+ * [gates-only-at-the-hook-door](gates-only-at-the-hook-door.md) - A CI verb accepted as a check name reports clean without reading the event.
8
+ * [containment-directions](containment-directions.md) - An escaping path is refused, ignored, or read as open — one decision per call site.
9
+ * [telemetry-does-not-lie](telemetry-does-not-lie.md) - The third clause applied to the friction recorder, which is not a check and still may not report an uncounted zero.
10
+ * [silent-skips](silent-skips.md) - The third clause broken by the checker itself, on its own gate path.
@@ -0,0 +1,78 @@
1
+ ---
2
+ type: Finding
3
+ title: The checker broke its own third clause
4
+ description: A default Linter.call skips two clock-gated checks and still reports healthy, so the gate reported clean over checks it never ran.
5
+ ---
6
+
7
+ # What was measured
8
+
9
+ On this gem's own gate path, before the fix:
10
+
11
+ ```
12
+ skipped_checks on the gate path: [:expired, :stale]
13
+ healthy? true
14
+ ```
15
+
16
+ `Bundle::Linter.call(bundle)` with no options cannot run `expired` (it has no
17
+ clock) or `stale` (it has no cutoff). Seven of the nine blocking checks ran, the
18
+ gate reported clean, and nothing said so — [the third clause](/contract/the-contract.md)
19
+ broken by the checker itself, in the one place nobody can notice.
20
+
21
+ okf 2.0 confesses it in `stats[:skipped_checks]`, a field added for a reader
22
+ exactly like this one. The gate discarded it.
23
+
24
+ # Why relaying the confession is not the fix
25
+
26
+ The obvious repair — return the confession as a refusal — makes the gate block
27
+ every edit in every bundle until someone supplies a cutoff. A gate that refuses
28
+ constantly is a gate switched off within a day, which is the same outcome by a
29
+ slower route.
30
+
31
+ The fix is to leave nothing skipped:
32
+
33
+ * `today:` is supplied, so `expired` runs. It is `:info`, so this adds no
34
+ refusal — it removes a silence.
35
+ * `stale` is excluded **in source**, with the reason beside it: it asks
36
+ "untouched since when?", and this gem has no cutoff policy and no business
37
+ inventing one. An excluded check is the opposite of a silently skipped one.
38
+
39
+ `skipped_checks` is then empty in normal operation, which turns the confession
40
+ into a live guard rather than a formality: the day okf adds a third clock-gated
41
+ check, this gate reports it instead of quietly not running it.
42
+
43
+ # The general shape
44
+
45
+ A field that reports what did not happen is worthless to a consumer that
46
+ discards it. When a dependency offers one, the question is not whether to log
47
+ it — it is what state makes it non-empty, and whether that state is one the
48
+ caller can act on.
49
+
50
+ # The same shape, at three more doors
51
+
52
+ All three were found the same way and are the same failure: a checker
53
+ reporting clean over something it had not looked at.
54
+
55
+ **The CI door dropped half of what the validator said.** `Audit.conformance`
56
+ returned early on `valid?`, so `result.warnings` went in the bin while the hook
57
+ door reported them at length. A scalar `verified: human:rod` is *conformant* —
58
+ so the attestation guard asks, the owner approves, the reader then drops the
59
+ malformed value, and the trust tier stays `unverified` forever. Caught at the
60
+ agent's tool boundary and waved through by CI and by `pre-commit`, which are the
61
+ two doors an edit made in an **editor** actually passes. Two doors asking one
62
+ question may not answer differently about one bundle; the severity was never the
63
+ disagreement, the silence was.
64
+
65
+ **A whitelist fails open on every letter it does not name.** The append-only gate
66
+ asked git for `--diff-filter=MDR`. Replacing a committed past day with a symlink
67
+ stages as `T`, a typechange — not `M`, not `D` — so the gate reported
68
+ "append-only" and the commit destroyed the one artefact nobody can reconstruct.
69
+ A whitelist's default answer for anything unenumerated is *pass*, which makes the
70
+ set something to justify rather than inherit: `MDRT`, with the reason beside it.
71
+
72
+ **And the clause above was kept at one door and broken at the other.** The whole
73
+ argument at the top of this page — supply the clock, exclude `stale` in source,
74
+ assert the residue — was applied to the hook door and never to `okf pro audit`,
75
+ which called `Linter.call(bundle)` bare and printed `clean.` over `expired` and
76
+ `stale`. A lesson recorded against one call site is not a lesson applied; when
77
+ this page said "the gate", there were three of them, and the fix has to be asked
78
+ of each. The residue is now asserted at both.
@@ -0,0 +1,218 @@
1
+ ---
2
+ type: Learning
3
+ title: The third clause, applied to something that is not a check
4
+ description: The friction recorder neither refuses nor blocks, and still may not report a zero it did not count — because a measurement that quietly degrades to "nothing happened" retires itself.
5
+ ---
6
+
7
+ # What it is
8
+
9
+ `Friction` records the moments an agent did by hand something an `okf pro` verb
10
+ could have done: a markdown write inside the bundle arriving through Bash, and
11
+ an Edit or Write whose target is `board.md` — that one path and no other, for
12
+ the reason *The prescribed path is not friction* gives below. It is the only
13
+ thing in this gem that is neither a gate nor a report: it exists so that
14
+ *"which verb is missing?"* is answered by evidence rather than by the
15
+ maintainer's imagination.
16
+
17
+ Two design constraints shaped it, and neither is about telemetry:
18
+
19
+ * **No new hook event.** `.claude/settings.json` is a *seeded* file
20
+ ([scaffold/ownership-not-subject](/scaffold/ownership-not-subject.md)), so a
21
+ new hook registration never reaches an adopter through `okf pro upgrade`.
22
+ Friction rides code paths that are already wired, or it does not ship.
23
+ * **It records intent, not outcome.** `shell-guard` fires at PreToolUse, before
24
+ the owner may deny. That is the right event anyway: the question is what was
25
+ *attempted* by hand.
26
+
27
+ # The clause, and why it applies here
28
+
29
+ > No check ever fails silent.
30
+
31
+ This is not a check. It refuses nothing and blocks nothing, and a recorder that
32
+ crashed a gate would be a measurement worth less than the thing it measures — so
33
+ every failure path in it is rescued and swallowed. That is the correct trade in
34
+ one direction and a trap in the other: the natural consequence of swallowing a
35
+ failed write is a log that stays empty, a report that says **0**, and a reader
36
+ who concludes the verbs are working.
37
+
38
+ A count of zero and *"the recorder could not write"* are different states, and
39
+ the whole value of the measurement dies the day they are confused. So:
40
+
41
+ * a failed write leaves a **marker** beside the log, and
42
+ * an unwritable scratch directory reaches the same verdict on its own — asked
43
+ directly, because the case where the marker cannot be written either is
44
+ exactly the case that proves a marker alone is not enough, and
45
+ * a recorded line that will not parse is **counted and confessed**, never
46
+ dropped, because a corrupted log must not read as a quieter session.
47
+
48
+ `okf pro friction` and the session banner both say *unknown, not zero* when any
49
+ of that holds. Same shape as [silent-skips](/contract/silent-skips.md): the
50
+ failure was never the missing datum, it was the report that did not mention it
51
+ was missing.
52
+
53
+ # The prescribed path is not friction
54
+
55
+ The obvious list of files to watch is board, log, journal day — and it is
56
+ wrong, in the direction that destroys the measurement. `snapshot` deliberately
57
+ has no `--write` (a writer and a checker sharing a code path agree trivially),
58
+ so appending the Snapshot line to `log.md` by hand is *exactly what this gem
59
+ tells you to do*. `journal open` says in as many words that the day's content
60
+ is yours. Recording either counts the system working as evidence that it is
61
+ not: it inflates the banner's request and points the maintainer at verbs that
62
+ already exist and already declined that job.
63
+
64
+ So the Edit door records `board.md` and nothing else. That is the same rule
65
+ `own_command?` keeps one layer down — **the good path must not count as
66
+ friction** — applied to a path the code prescribes rather than to a command the
67
+ code owns.
68
+
69
+ That narrowing is also how this page went into the tree **wrong, in the same
70
+ commit as the code it describes**. Its opening paragraph read "board.md,
71
+ `log.md` or a journal day" — the list from before the argument above was made —
72
+ and a concept describing behaviour the code does not have is
73
+ [a-comment-is-not-an-implementation](/design/a-comment-is-not-an-implementation.md)
74
+ at one remove, with a wider readership. The narrowing left `classify`'s other
75
+ two answers unreachable from the edit door and its unit test still green, which
76
+ is the shape [fixture-is-a-client](/testing/fixture-is-a-client.md) records: a
77
+ test calling the function directly proves a branch no user can walk to. One
78
+ covered path means the class a row records **is** that path, so the function is
79
+ gone and the call site passes the path.
80
+
81
+ The shell door is different and stays broad, because a shell write is a bypass
82
+ whatever it touched: the trust guards read a tool event and a redirect produces
83
+ none. Which is why the report is keyed on the DOOR as well as the file. An Edit
84
+ to the board says a verb went unused; a shell redirect at the board says the
85
+ guards were bypassed, and what covers those is not the same answer.
86
+
87
+ **And the banner asks the narrower of the two questions.** `okf pro friction`
88
+ reports every row, because a maintainer reading it wants both. The session line
89
+ asks for a *verb*, so it may only count rows a verb would answer — and it
90
+ counted shell rows too, telling the adopter that a verb could have done
91
+ something no verb covers. Worse: `shell-guard` records intent at PreToolUse, so
92
+ the system working exactly as designed — guard fires, owner denies, agent uses
93
+ Edit — still incremented a lifetime counter that then nagged every session until
94
+ `--clear`. A measurement that fires when nothing went wrong is the
95
+ [a-rule-you-can-walk-past](/design/a-rule-you-can-walk-past.md) failure wearing
96
+ telemetry's uniform.
97
+
98
+ # Sticky, with a way out
99
+
100
+ The `.unavailable` marker is never cleared automatically, and that is correct:
101
+ one failed write means the count is short by an unknown amount from then on,
102
+ and a recorder that quietly forgave itself would be back to reporting a zero it
103
+ did not count.
104
+
105
+ Sticky with no way out is a different failure. A momentarily full disk pins the
106
+ answer at *unknown* forever, the banner nags about a week nobody can change,
107
+ and "check that `.tmp/` is writable" is not actionable once the directory is
108
+ perfectly writable and the marker is what is holding the verdict. So the way
109
+ out is explicit, named in the message, and the reader's: `okf pro friction
110
+ --clear`. It touches `.tmp/` and nothing else.
111
+
112
+ The same reasoning fixes the banner's arithmetic. The log is append-only and
113
+ nothing prunes it, so the count is a **lifetime total** and the line says so —
114
+ "so far", never "last session". A standing warning nobody can act on is the
115
+ failure [a-rule-you-can-walk-past](/design/a-rule-you-can-walk-past.md)
116
+ describes, and naming the reset is what keeps this one a request rather than
117
+ wallpaper.
118
+
119
+ # Where it points, and what it never does
120
+
121
+ The banner's friction line addresses the **adopter**, not the maintainer — they
122
+ are the ones paying for a missing verb and the only ones who can say which one
123
+ it is — and `okf pro friction --issue` prints a ready `gh issue create` against
124
+ this gem's repository. **Printed, never run.** Filing an issue is outward-facing
125
+ and irreversible, and a hook that did it unattended would be both without anyone
126
+ asking. If `gh` is absent it prints the URL and the body to paste.
127
+
128
+ **Nor does it draft a report about nothing.** `--issue` with an empty log
129
+ printed a complete, ready-to-paste issue whose body was an empty list and whose
130
+ title said *0 bundle edit(s)*. Nothing stopped an adopter sending it, and it
131
+ asked the maintainer to act on a measurement that had measured nothing. So a
132
+ count that is genuinely zero prints no issue at all.
133
+
134
+ Two states are not that, and both still file. *Unknown* is one — "the recorder
135
+ could not write" is something that happened. So is a log whose every line was
136
+ unparseable: zero rows with a positive `unreadable` count is a **corrupted**
137
+ recorder, not a quiet one, and reading the two as the same would have retired
138
+ the report in precisely the case it was most worth sending.
139
+
140
+ Which then has to hold on **every** surface, and briefly did not: `--issue`
141
+ declined to file for a quiet log and filed for a corrupted one, while the human
142
+ default and the session banner both called a corrupted log "nothing recorded"
143
+ and said nothing at all. A report is only as honest as its least honest surface,
144
+ and the default is the one most people read.
145
+
146
+ The same page, one more time: the issue BODY branched its whole content on
147
+ `available` and printed only "the recorder could not write, so the counts above
148
+ are unknown" — over a body with nothing above it, under a title that still
149
+ counted the rows. The marker is sticky by design, so one failed write weeks ago
150
+ made every later report evidence-free. A degraded recorder suppresses the
151
+ *claim to completeness*, never the evidence: the rows print, and the caveat says
152
+ the total is a floor.
153
+
154
+ The same rule caught the issue *title*. It read "N bundle edit(s) done by hand
155
+ that a verb could cover" over a body whose only row said `covered by: Edit or
156
+ Write` — the title is what a maintainer triages on, and it was asking for a verb
157
+ this gem had just decided not to want. It now says what was counted and claims
158
+ nothing about what covers it; the per-row note answers that.
159
+
160
+ One exclusion keeps the measurement honest: a command matching `okf pro` is not
161
+ recorded. Without it the good path counts as evidence against itself — `okf pro
162
+ snapshot >> .okf/log.md` *is* the prescribed way to add the Snapshot line.
163
+
164
+ But that exclusion is about a **write**, so two questions had to be separated,
165
+ and getting either wrong breaks the measurement in a different direction.
166
+
167
+ *Which command wrote?* Asked of the whole string, a mention anywhere suppressed
168
+ the record: `okf pro state && cat notes.md > .okf/board.md` prompted the owner
169
+ and counted nothing — and an agent chaining a read with a write is the commonest
170
+ shape there is, so the under-count sat exactly where the measurement mattered.
171
+ So the question is asked per command: a segment carrying no mutator is nobody's
172
+ write (`cd repo && okf pro promote alpha` is still this gem's), and a mutator in
173
+ a segment this gem does not own is a hand-write whatever else the line says. A
174
+ **pipeline is one command**, not two — splitting on `|` put this gem's own
175
+ redirect target in a segment of its own and recorded `okf pro snapshot | tee -a
176
+ .okf/log.md`, the prescribed move, as friction.
177
+
178
+ A pipeline being one command is right and is not a licence: `okf pro board |
179
+ sed s/a/b/ > .okf/board.md` starts with this gem and ends by regenerating the
180
+ board, which is failure mode 07 by name. Nothing prescribes piping into a path a
181
+ verb covers — `snapshot` appends to `log.md`, which no verb covers — so an own
182
+ command aimed at `board.md` is friction whatever produced the bytes.
183
+
184
+ *Is this an invocation, or a mention?* Anchored to the start of a segment, not
185
+ matched anywhere in it. Board lines routinely name these verbs, so a hand-append
186
+ whose CONTENT said `okf pro` excused itself: `echo "- see okf pro docs" >>
187
+ .okf/board.md` was never counted. An assignment prefix is skipped rather than
188
+ breaking the anchor — `$OKF_HOME` is a variable this ecosystem uses, so
189
+ `OKF_HOME=/tmp okf pro snapshot >> .okf/log.md` is the prescribed move wearing a
190
+ prefix, and an anchor that missed it would have made the same mistake in the
191
+ other direction. So is a short, named list of wrappers: `bundle exec okf pro
192
+ snapshot >> .okf/log.md` is *this repo's own* invocation of the prescribed move,
193
+ and an anchor that missed it counted a contributor's first command as friction.
194
+ The list is named rather than open, because an open rule is a way back to
195
+ matching a mention.
196
+
197
+ Splitting on separators is not shell parsing and may not become it. The residue
198
+ is written down because a bounded error is only bounded if it is: a separator
199
+ inside quoted content splits a command that is genuinely this gem's, and the
200
+ fragment holding the mutator word is read as somebody else's write. That
201
+ over-counts, which is the lesser error — it points a maintainer at a verb that
202
+ already exists, where the under-count it replaced hid the commonest shape there
203
+ is. Neither can refuse anything, and the prompt is still decided from the whole
204
+ command, so the guard's safety is untouched.
205
+
206
+ # It is telemetry, so it is not state
207
+
208
+ `okf pro state` is *cheap by contract* — `board.md`, `log.md`, two directory
209
+ globs, no concept parsed — and it briefly read the friction log as well, on
210
+ every call, for a field its human rendering never printed. Two things were wrong
211
+ at once, and the second is the durable one: an append-only file nothing prunes
212
+ had been added to the sources of the verb whose whole promise is that it is
213
+ cheap, and the only reader who could see the field was one asking for `--json`.
214
+
215
+ Friction is knowledge about the *tooling*, not about the bundle — which is
216
+ already why the log lives in `.tmp/` at the repository root rather than under a
217
+ root `okf validate` walks. `okf pro friction` is the verb that answers it. A
218
+ contract that names its sources is a contract to check when adding one.
@@ -0,0 +1,43 @@
1
+ ---
2
+ type: Decision
3
+ title: The contract
4
+ description: Blocking checks fail closed, feedback checks fail loud, and no check ever fails silent — the three clauses every other decision in this gem is derived from.
5
+ ---
6
+
7
+ # The three clauses
8
+
9
+ > Blocking checks fail **closed**. If enforcement is missing or cannot run, the
10
+ > call is refused, loudly. A gate that cannot check must not wave things through.
11
+ >
12
+ > Feedback checks fail **loud**. If enforcement is degraded, it says so in the
13
+ > same channel it would use to refuse.
14
+ >
15
+ > No check ever fails **silent**. A gate that is sometimes absent and does not
16
+ > confess converts "unchecked" into "checked and fine", which is worse than
17
+ > having no gate at all.
18
+
19
+ # Why the third clause is the one that matters
20
+
21
+ The first two are ordinary defensive engineering. The third is the reason this
22
+ gem is shaped the way it is, and it is the one people argue with.
23
+
24
+ The argument against it is that a skipped check is not a failure — nothing went
25
+ wrong, one question simply was not asked. The argument for it is what a reader
26
+ does with the answer. A bundle reported clean is a bundle nobody looks at again.
27
+ If the report was clean because seven of nine checks ran, the reader has been
28
+ handed a false negative wearing the costume of a verdict, and there is no
29
+ subsequent moment at which they find out.
30
+
31
+ That is worse than no gate, because no gate is a state a person can reason
32
+ about. "We do not check this" produces caution. "We checked and it is fine"
33
+ produces none.
34
+
35
+ # Where each clause is kept
36
+
37
+ * Fail closed lives in [the wrapper](/seam/the-wrapper.md), because a Ruby
38
+ checker structurally cannot refuse on its own absence — something already
39
+ running has to do it.
40
+ * Fail loud lives in the CLI's refusal messages, which name the cause rather
41
+ than a code.
42
+ * Fail silent is closed in two places: [the exit codes](/contract/exit-codes.md),
43
+ and [what the linter did not run](/contract/silent-skips.md).
@@ -0,0 +1,85 @@
1
+ ---
2
+ type: Learning
3
+ title: A comment is not an implementation
4
+ description: A regex that claimed to anchor `>` to a redirection position never did, and the guard it powered fired on reads for its whole life — which is the shape of noise that retires a gate. The same class reaches comments, docstrings and the bundle's own concepts.
5
+ ---
6
+
7
+ # What happened
8
+
9
+ `ShellGuard::MUTATORS` matched a redirection with `>{1,2}[[:blank:]]*[^|&\s]`,
10
+ and the comment above it said, in as many words, that `>` "is matched only
11
+ where a redirection can sit, so `a > b` in a comparison inside a command is not
12
+ automatically a write."
13
+
14
+ Nothing in the pattern implemented that. There was no lookbehind, no anchor, no
15
+ position test of any kind — the claim was true of the sentence and false of the
16
+ code, and it had been for the regex's whole life.
17
+
18
+ In this bundle that is the worst possible miss. `-->` closes the HTML comment
19
+ the skill mandates on every keyed rule and is also mermaid's edge; `=>` is in
20
+ every quoted Ruby hash. So `grep -rn "a --> b" .okf/` — a **read** — was routed
21
+ to the owner as a suspected write, and so was every grep for a rule marker.
22
+
23
+ # Why that is worse than a gate that is simply absent
24
+
25
+ The guard's honest verdict is *ask*: it cannot parse shell, so it routes the
26
+ decision to a person who can read the command. That verdict is only worth
27
+ anything while the prompts are rare enough to be read. A guard that fires on
28
+ reads produces a stream of prompts whose correct answer is always "approve",
29
+ and the reflex it trains is approval without reading — which is precisely the
30
+ state where the one prompt that mattered gets approved too.
31
+
32
+ So the cost is not the false positives. It is that the true positive stops
33
+ being distinguishable from them, and the gate has been retired without anyone
34
+ deciding to retire it. That is failure mode 1 wearing the checker's uniform,
35
+ the same shape [silent-skips](/contract/silent-skips.md) records from the other
36
+ direction.
37
+
38
+ # The rule
39
+
40
+ **A comment describing a property the code does not have is worse than no
41
+ comment**, because it is what a reviewer reads instead of the code. Two things
42
+ follow, and both are cheap:
43
+
44
+ * When a comment asserts a *property* — anchored, bounded, ordered, exhaustive
45
+ — the test file for that unit must contain the case the property forbids. The
46
+ arrow cases were unwritable as long as the property was only claimed; writing
47
+ them is what made the gap visible in one run.
48
+ * When a guard exists to route a decision to a person, its false-positive rate
49
+ is part of its correctness, not a matter of taste. `test_ascii_arrows_are_not_redirections`
50
+ exists at the same rank as the drills that prove a refusal, because a guard
51
+ people switch off refuses nothing at all.
52
+
53
+ # The same class, at four distances
54
+
55
+ The regex is the sharpest instance and not the only one. Ordered by how far the
56
+ prose sits from the code it misdescribes:
57
+
58
+ * **Beside it.** `Board::Edit.set_declared` found the budget header *per line*
59
+ and put it back with `String#sub`, which searches the whole text — so any
60
+ earlier line whose tail happened to equal the header matched first, the prose
61
+ was rewritten and the header left stale. `Conserve` caught the mismatch and
62
+ refused, which is the system working; but the refusal **named the prose line**,
63
+ and the board stayed unpromotable until somebody edited a line that was never
64
+ the problem. A lookup and its inverse have to agree on their unit, and a
65
+ refusal is only as useful as the thing it points at.
66
+ * **Two definitions away.** `Friction`'s paragraph about reading a class out of
67
+ a shell command sat above `clear`, fused with `clear`'s own comment, two
68
+ definitions from `classify_command`. Nothing is wrong with either method; a
69
+ reader arrives at the wrong one holding the right explanation.
70
+ * **In a message the reader acts on.** The writers share one missing-argument
71
+ message, and `close` does not take what it offers: a project is one directory
72
+ segment, so "a substring only one board line carries" is a refusal, and so
73
+ was `/projects/<slug>/index.md` — the exact link a board line carries and
74
+ `okf pro board` prints. A message naming what the verb rejects is worse than
75
+ no message, for the same reason a wrong comment is: it is what the reader
76
+ acts on instead of the code. Fixed from both ends — the message names one
77
+ directory segment, and `close` now accepts the third spelling
78
+ `Board::Edit.by_target` already treated as the same commitment.
79
+ * **In the bundle.** [telemetry-does-not-lie](/contract/telemetry-does-not-lie.md)
80
+ described a recorder watching three paths while the code watched one — and
81
+ went into the tree that way *in the same commit as the code*, which is the
82
+ commit rule in `AGENTS.md` doing the opposite of its job. A concept has a wider readership
83
+ than a comment and is trusted further, so this is the same failure with the
84
+ blast radius turned up. Maintaining `.okf/` in the same commit means checking
85
+ it against the diff, not merely editing it in the same breath.