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.
- checksums.yaml +7 -0
- data/.okf/contract/containment-directions.md +107 -0
- data/.okf/contract/exit-codes.md +42 -0
- data/.okf/contract/gates-only-at-the-hook-door.md +39 -0
- data/.okf/contract/index.md +10 -0
- data/.okf/contract/silent-skips.md +78 -0
- data/.okf/contract/telemetry-does-not-lie.md +218 -0
- data/.okf/contract/the-contract.md +43 -0
- data/.okf/design/a-comment-is-not-an-implementation.md +85 -0
- data/.okf/design/a-rule-you-can-walk-past.md +68 -0
- data/.okf/design/derivation-that-writes.md +161 -0
- data/.okf/design/failure-modes.md +43 -0
- data/.okf/design/index.md +18 -0
- data/.okf/design/lineage.md +95 -0
- data/.okf/design/structure-laws.md +46 -0
- data/.okf/design/the-residue.md +44 -0
- data/.okf/design/three-laws.md +71 -0
- data/.okf/design/three-pillars.md +48 -0
- data/.okf/index.md +31 -0
- data/.okf/log.md +74 -0
- data/.okf/scaffold/collisions-and-refusals.md +76 -0
- data/.okf/scaffold/index.md +8 -0
- data/.okf/scaffold/no-date-ships.md +41 -0
- data/.okf/scaffold/ownership-not-subject.md +63 -0
- data/.okf/scaffold/the-adopters-manual.md +114 -0
- data/.okf/seam/bundler-scoping.md +45 -0
- data/.okf/seam/identity-not-existence.md +67 -0
- data/.okf/seam/index.md +8 -0
- data/.okf/seam/the-wrapper.md +42 -0
- data/.okf/seam/three-fail-opens.md +44 -0
- data/.okf/testing/drills-over-units.md +41 -0
- data/.okf/testing/fixture-is-a-client.md +35 -0
- data/.okf/testing/index.md +4 -0
- data/.okf/trust/index.md +7 -0
- data/.okf/trust/read-owed-rule.md +45 -0
- data/.okf/trust/scalar-verified.md +48 -0
- data/CHANGELOG.md +187 -0
- data/LICENSE.txt +201 -0
- data/NOTICE +10 -0
- data/README.md +383 -0
- data/lib/okf/plugin.rb +148 -0
- data/lib/okf/pro/attestation.rb +62 -0
- data/lib/okf/pro/audit.rb +161 -0
- data/lib/okf/pro/board/edit.rb +201 -0
- data/lib/okf/pro/board.rb +291 -0
- data/lib/okf/pro/budget.rb +144 -0
- data/lib/okf/pro/bundle_root.rb +241 -0
- data/lib/okf/pro/cli.rb +914 -0
- data/lib/okf/pro/closing.rb +206 -0
- data/lib/okf/pro/conformance.rb +127 -0
- data/lib/okf/pro/conserve.rb +94 -0
- data/lib/okf/pro/event.rb +95 -0
- data/lib/okf/pro/friction.rb +262 -0
- data/lib/okf/pro/guards.rb +92 -0
- data/lib/okf/pro/log/edit.rb +68 -0
- data/lib/okf/pro/log.rb +162 -0
- data/lib/okf/pro/pairing.rb +271 -0
- data/lib/okf/pro/reconcile.rb +76 -0
- data/lib/okf/pro/records.rb +90 -0
- data/lib/okf/pro/scaffold.rb +258 -0
- data/lib/okf/pro/shell_guard.rb +164 -0
- data/lib/okf/pro/snapshot.rb +156 -0
- data/lib/okf/pro/state.rb +174 -0
- data/lib/okf/pro/target.rb +91 -0
- data/lib/okf/pro/template/gem/.claude/hooks/run +119 -0
- data/lib/okf/pro/template/gem/.claude/skills/okf-pro/SKILL.md +233 -0
- data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/attribution.md +46 -0
- data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/board.md +55 -0
- data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/closing.md +21 -0
- data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/frontmatter.md +73 -0
- data/lib/okf/pro/template/gem/.claude/skills/okf-pro/guides/okf-rules.md +25 -0
- data/lib/okf/pro/template/gem/.githooks/pre-commit +68 -0
- data/lib/okf/pro/template/gem/.github/workflows/okf-pro.yml +69 -0
- data/lib/okf/pro/template/seed/.claude/settings.json +24 -0
- data/lib/okf/pro/template/seed/.okf/areas/corpus.md +73 -0
- data/lib/okf/pro/template/seed/.okf/areas/index.md +6 -0
- data/lib/okf/pro/template/seed/.okf/board.md +21 -0
- data/lib/okf/pro/template/seed/.okf/glossary/index.md +13 -0
- data/lib/okf/pro/template/seed/.okf/index.md +30 -0
- data/lib/okf/pro/template/seed/.okf/journal/index.md +11 -0
- data/lib/okf/pro/template/seed/.okf/learnings/index.md +13 -0
- data/lib/okf/pro/template/seed/.okf/log.md +10 -0
- data/lib/okf/pro/template/seed/.okf/projects/index.md +23 -0
- data/lib/okf/pro/template/seed/.okf/reference/index.md +16 -0
- data/lib/okf/pro/template/seed/.okf/roadmap.md +33 -0
- data/lib/okf/pro/template/seed/.tmp/.gitkeep +0 -0
- data/lib/okf/pro/template/seed/CLAUDE.md +61 -0
- data/lib/okf/pro/template/seed/README.md +488 -0
- data/lib/okf/pro/template/seed/gitignore +17 -0
- data/lib/okf/pro/version.rb +7 -0
- data/lib/okf/pro/writes.rb +560 -0
- data/lib/okf/pro.rb +198 -0
- 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.
|
data/.okf/seam/index.md
ADDED
|
@@ -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.
|