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
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.
|