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
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OKF
4
+ module Pro
5
+ # What is on the board, in one call.
6
+ #
7
+ # THE MEASUREMENT THIS EXISTS FOR. A session adding one task to a seeded
8
+ # bundle spent 40.9% of its tool output rediscovering state — the board, the
9
+ # log, the project and journal indexes, read raw, one `cat` per question —
10
+ # and called no `okf pro` verb at all. Nine calls answered what the gates
11
+ # already compute on every Stop. So there is no new logic here: every
12
+ # aggregation below already existed and was consumed only by a refusal.
13
+ #
14
+ # CHEAP BY DEFAULT, and that is a contract rather than an optimisation. The
15
+ # default payload reads `board.md`, `log.md` and two directory globs, and
16
+ # parses no concept — the same sources the session banner uses, so a reader
17
+ # can trust the two to agree. `--full` is where `Bundle::Reader.read` lives:
18
+ # the attestation report, the pairing invariants, and the live unverified
19
+ # count, behind ONE parse shared by all three.
20
+ #
21
+ # A contract that names its sources is a contract to check before adding
22
+ # one. The friction log was added here and did not belong: it is telemetry
23
+ # about the tooling rather than state of the bundle, it is append-only with
24
+ # nothing pruning it, and `render` never printed the field — so the cheap
25
+ # default paid for an unbounded read only `--json` could see.
26
+ # `okf pro friction` is the verb that answers it.
27
+ #
28
+ # Shell, not core: it reads the disk. The arithmetic it reports is Board's,
29
+ # Log's, Snapshot's and Pairing's, all of it pure and all of it already
30
+ # tested against text rather than against a fixture.
31
+ module State
32
+ module_function
33
+
34
+ def call(root, today: Date.today, full: false)
35
+ board = Pro.read_text(File.join(root, "board.md"))
36
+ log_path = File.join(root, "log.md")
37
+ log = File.exist?(log_path) ? Pro.read_text(log_path) : ""
38
+ vis = Board.visible(board)
39
+
40
+ payload = {
41
+ "root" => root,
42
+ "as of" => today.to_s,
43
+ "board" => board_state(vis, today),
44
+ "deadlines at risk" => Snapshot.looming_deadlines(vis, today).map(&:strip),
45
+ "log" => log_state(root, log, today),
46
+ "projects" => { "open" => Pairing.open_projects(root).sort },
47
+ "last snapshot" => last_snapshot(log)
48
+ }
49
+ payload["full"] = full_state(root, board) if full
50
+ payload
51
+ end
52
+
53
+ def board_state(vis, today)
54
+ budget = Board.budget(vis)
55
+ inbox = Board.visible_section_lines(vis, "Inbox")
56
+ waiting = Board.visible_section_lines(vis, "Waiting")
57
+ ages = inbox.map { |l| Board.line_date(l) }.compact.map { |d| (today - d).to_i }
58
+ {
59
+ "in flight" => Board.visible_section_lines(vis, "In flight").size,
60
+ "cap" => budget&.cap,
61
+ "declared" => budget&.declared,
62
+ "backlog" => Board.visible_section_lines(vis, "Backlog").size,
63
+ "waiting" => waiting.size,
64
+ "past chase" => waiting.map { |l| Board.chase_date(l) }.compact.count { |d| d < today },
65
+ "inbox" => inbox.size,
66
+ "oldest" => ages.max || 0,
67
+ "to read" => Board.visible_section_lines(vis, "To read").size,
68
+ "deadlines" => Board.visible_section_lines(vis, "Deadlines").size,
69
+ "conflicts open" => Snapshot.conflicts(vis)
70
+ }
71
+ end
72
+
73
+ def log_state(root, log, today)
74
+ {
75
+ "newest day" => Log.newest_day(log),
76
+ "journal today" => File.file?(File.join(root, "journal", "#{today}.md"))
77
+ }
78
+ end
79
+
80
+ # Labelled by the day it was logged under, never as live. The counters
81
+ # here are free — the banner already prints the line, and parsing it costs
82
+ # a regex — but two of them (`unverified briefings`, `projects with 0
83
+ # concepts`) can only be recomputed by parsing the bundle, so a reader
84
+ # who is told them without the date would read a stale number as current.
85
+ def last_snapshot(log)
86
+ day, line = Log.latest_snapshot_entry(log)
87
+ return nil if line.nil?
88
+
89
+ { "day" => day, "line" => line.strip, "counters" => Snapshot.parse(line) }
90
+ end
91
+
92
+ # The one parse, shared by all three. Ordered so the reader meets the
93
+ # invariants before the listing: a pairing failure is a thing to fix, and
94
+ # an unverified concept is the truth about what is owed.
95
+ def full_state(root, board)
96
+ concepts = ::OKF::Bundle::Reader.read(root).concepts
97
+ {
98
+ "pairing" => Pairing.failures(root, board: board, concepts: concepts).map { |m| m.sub(/\A— /, "") },
99
+ "unverified" => Attestation.report(root, concepts: concepts).map(&:strip),
100
+ "unverified briefings" => Pairing.unverified_ids(concepts).size
101
+ }
102
+ end
103
+
104
+ # ── human rendering ──────────────────────────────────────────────────
105
+ #
106
+ # The default, because the consumer is also a person doing QA — and
107
+ # because a human line an agent reads costs the same tokens as JSON it
108
+ # has to re-serialise into prose anyway.
109
+ # The board's counters as one line, and the ONE place they are formatted.
110
+ # `okf pro state` and the session banner both print it, and the banner's
111
+ # whole promise is that it says what the verb would say — two renderings
112
+ # of the same eleven numbers would drift into a reader having to check
113
+ # which one they were looking at.
114
+ def board_line(board, label)
115
+ "#{label} in flight #{board["in flight"]}/#{board["cap"] || "?"} · backlog #{board["backlog"]} · " \
116
+ "waiting #{board["waiting"]} (#{board["past chase"]} past chase) · inbox #{board["inbox"]} " \
117
+ "(oldest #{board["oldest"]}d) · to read #{board["to read"]} · deadlines #{board["deadlines"]} · " \
118
+ "conflicts open #{board["conflicts open"]}"
119
+ end
120
+
121
+ def render(payload)
122
+ b = payload["board"]
123
+ lines = [ board_line(b, "Board —") ]
124
+ if b["declared"] && b["declared"] != b["in flight"]
125
+ lines << " header declares #{b["declared"]} in flight and the section holds " \
126
+ "#{b["in flight"]} — fix the header."
127
+ end
128
+
129
+ risk = payload["deadlines at risk"]
130
+ unless risk.empty?
131
+ lines << "Deadlines within 7d with nothing in flight against them:"
132
+ risk.each { |line| lines << " #{line}" }
133
+ end
134
+
135
+ log = payload["log"]
136
+ lines << "Log — newest day #{log["newest day"] || "none yet"} · journal for " \
137
+ "#{payload["as of"]} #{log["journal today"] ? "open" : "not opened"}"
138
+
139
+ open_projects = payload["projects"]["open"]
140
+ lines << "Open projects (#{open_projects.size})#{": #{open_projects.join(", ")}" unless open_projects.empty?}"
141
+
142
+ lines.concat(render_snapshot(payload["last snapshot"]))
143
+ lines.concat(render_full(payload["full"])) if payload["full"]
144
+ lines
145
+ end
146
+
147
+ def render_snapshot(snap)
148
+ return [ "Last snapshot: none yet" ] if snap.nil?
149
+
150
+ counters = snap["counters"]
151
+ [ "As of #{snap["day"]}, the last logged snapshot — unverified briefings " \
152
+ "#{counters["unverified briefings"] || "?"} · projects with 0 concepts " \
153
+ "#{counters["projects with 0 concepts"] || "?"} (`--full` recomputes these live)" ]
154
+ end
155
+
156
+ def render_full(full)
157
+ lines = [ "Live from the corpus — unverified briefings #{full["unverified briefings"]}" ]
158
+ if full["pairing"].empty?
159
+ lines << "Pairing — the board and the work are in step."
160
+ else
161
+ lines << "Pairing — #{full["pairing"].size} finding(s):"
162
+ full["pairing"].each { |m| lines << " #{m}" }
163
+ end
164
+ if full["unverified"].empty?
165
+ lines << "Awaiting the owner's read — nothing."
166
+ else
167
+ lines << "Awaiting the owner's read (#{full["unverified"].size}):"
168
+ full["unverified"].each { |m| lines << " #{m}" }
169
+ end
170
+ lines
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OKF
4
+ module Pro
5
+ # The bundle root plus the edited path made relative to it.
6
+ #
7
+ # `.for` returns nil whenever a check cannot apply — no bundle under the
8
+ # working directory, a path outside it, not markdown, or behind a dot,
9
+ # which is the same boundary `okf` itself walks. A file at the repository
10
+ # root is outside now: README.md and CLAUDE.md are documentation and
11
+ # instructions, not concepts, and no conformance rule reaches them.
12
+ # Nil is a real answer here and not a failure: a check that does not apply
13
+ # has nothing to say, and saying nothing is different from passing something
14
+ # it could not read. The checks distinguish the two.
15
+ class Target
16
+ attr_reader :root, :rel
17
+
18
+ def self.for(event)
19
+ # The file's own ancestry decides which bundle governs it — alone.
20
+ # There used to be a cwd fallback here, and it was a fence bypass:
21
+ # containing() would refuse a file inside a nested repository, and
22
+ # resolve(cwd) — which neither walks nor fences — would re-adopt it
23
+ # through the physical-prefix check below whenever the session sat
24
+ # in a root-layout bundle. A file genuinely inside a bundle is
25
+ # found by its own walk — the walk's classifier accepts a
26
+ # versioned index or any core file beside a bare one, so even a
27
+ # mid-bootstrap bundle governs its files; only an index.md truly
28
+ # alone is ambiguous with a directory index, and the walk refuses
29
+ # to guess. The fallback could only ever add adoptions the walk
30
+ # had refused.
31
+ root = BundleRoot.containing(event.file_path)
32
+ return nil if root.nil?
33
+
34
+ rel = relative(event.file_path, root)
35
+ return nil if rel.nil? || !rel.end_with?(".md")
36
+ return nil if rel.split("/").any? { |seg| seg.start_with?(".") }
37
+
38
+ new(root, rel)
39
+ end
40
+
41
+ # Relative to the bundle root, or nil when the path is outside it. Made
42
+ # relative before the dot test on purpose: an absolute path can pass
43
+ # through `/Users/someone/.local/...` and have nothing dot-prefixed about
44
+ # the file itself.
45
+ def self.relative(path, root)
46
+ return nil if path.to_s.empty?
47
+
48
+ full = File.expand_path(path)
49
+ full.start_with?("#{root}/") ? full[(root.size + 1)..-1] : nil
50
+ end
51
+
52
+ def initialize(root, rel)
53
+ @root = root
54
+ @rel = rel
55
+ end
56
+
57
+ # Read once per process. Three PostToolUse checks want the same bundle,
58
+ # and the read is the expensive part — it is the whole reason they arrive
59
+ # as one `post-edit` invocation rather than three.
60
+ def bundle
61
+ @bundle ||= ::OKF::Bundle::Reader.read(root)
62
+ end
63
+
64
+ def id
65
+ rel.sub(/\.md\z/, "")
66
+ end
67
+
68
+ def basename
69
+ File.basename(rel)
70
+ end
71
+
72
+ # A Target holds its root, so every read through it is contained: a
73
+ # `board.md` that is a symlink out of the bundle is not this bundle's
74
+ # board, and the gates must not read it as one.
75
+ #
76
+ # Raises rather than rescuing. A caller asking a Target to read a named
77
+ # core file has already established the file exists (`exist?` above), so
78
+ # a Path::Error here is a real containment failure — and the dispatch
79
+ # rescue in CLI.run turns any exception into a refusal, which is the
80
+ # correct answer to "something outside the bundle is pretending to be
81
+ # inside it".
82
+ def read(name)
83
+ Pro.read_contained(root, File.join(root, name))
84
+ end
85
+
86
+ def exist?(name)
87
+ File.exist?(File.join(root, name))
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # The enforcement seam, and the fail-closed wrapper around it.
3
+ #
4
+ # THE CONTRACT: blocking checks fail CLOSED, feedback checks fail LOUD, and no
5
+ # check ever fails SILENT. This file is where the first clause is actually kept.
6
+ #
7
+ # It cannot be collapsed into the checker, and the reason is the whole contract.
8
+ # A script whose interpreter is missing exits 127; a script whose syntax the
9
+ # interpreter rejects exits 1. The hook protocol reads every non-zero code other
10
+ # than 2 as a NON-BLOCKING error — the tool call proceeds. So a Ruby checker
11
+ # structurally cannot refuse on its own absence: something already running has
12
+ # to do it. That is this file's entire job, and it is why settings.json points
13
+ # here and never at `okf` directly.
14
+ #
15
+ # What it refuses with exit 2, each one measured rather than imagined:
16
+ # no check name;
17
+ # no `okf` on PATH;
18
+ # `okf` present but okf-pro not installed (a LoadError out of the deferred
19
+ # require is a ScriptError, outside every rescue in okf's dispatch — the
20
+ # process exits 1, which the protocol reads as "proceed");
21
+ # a syntax error in okf-pro's own plugin.rb (same ScriptError reason, and
22
+ # nothing Ruby-side runs at all — only this file can catch that one);
23
+ # a stray `okf` shim on PATH that exits 0 (status alone cannot tell it from a
24
+ # clean gate, so identity is proved separately — see MARKER below);
25
+ # any other exit code, which means nothing to the protocol and must not be
26
+ # allowed to mean "fine".
27
+ #
28
+ # What it does NOT do is exec. The prototype's wrapper did, and an exec'd
29
+ # process's SyntaxError is unreachable: nothing is left running to report it.
30
+ set -u
31
+
32
+ MARKER='okf-pro-enforcer v1'
33
+
34
+ check="${1:-}"
35
+ if [ -z "$check" ]; then
36
+ echo "ENFORCEMENT MISCONFIGURED — .claude/hooks/run was called with no check name; nothing ran." >&2
37
+ exit 2
38
+ fi
39
+
40
+ okf_bin="$(command -v okf)" || okf_bin=""
41
+ if [ -z "$okf_bin" ]; then
42
+ echo "ENFORCEMENT DEGRADED — \`okf\` is not on PATH; no check ran. Run \`gem install okf okf-pro\`, or every edit lands unchecked." >&2
43
+ exit 2
44
+ fi
45
+
46
+ # The event arrives on stdin and can only be read once, and this wrapper may run
47
+ # the checker twice (see the bundler fallback below). Buffer it.
48
+ work="$(mktemp -d)" || {
49
+ echo "ENFORCEMENT DEGRADED — could not create a temp directory to buffer the hook event; no check ran." >&2
50
+ exit 2
51
+ }
52
+ trap 'rm -rf "$work"' EXIT
53
+ cat > "$work/event" || :
54
+
55
+ # Bundler's variables make gem discovery bundle-scoped, and okf finds its
56
+ # extensions with Gem.find_latest_files — so inside a bundle that does not name
57
+ # okf-pro, `okf pro` is an unknown command and every gate is off. Restoring
58
+ # the pre-bundler environment fixes that. It is done from bundler's OWN
59
+ # restoration data rather than a hard-coded variable list: the list that matters
60
+ # is RUBYOPT and BUNDLER_SETUP, and BUNDLER_SETUP is recent enough that naming it
61
+ # would be a bet on the adopter's bundler version, on a gem whose floor is 2.4.
62
+ unbundle() {
63
+ while IFS='=' read -r k v; do
64
+ case "$k" in BUNDLER_ORIG_*) ;; *) continue ;; esac
65
+ orig="${k#BUNDLER_ORIG_}"
66
+ if [ "$v" = "BUNDLER_ENVIRONMENT_PRESERVER_INTENTIONALLY_NIL" ]; then
67
+ unset "$orig"
68
+ else
69
+ export "$orig=$v"
70
+ fi
71
+ unset "$k"
72
+ done < <(env)
73
+ }
74
+
75
+ # One attempt: run the check, capture both streams, and report whether the
76
+ # binary identified itself as the enforcer. Nothing is emitted from here — an
77
+ # attempt that failed to identify is one whose output must not be believed.
78
+ attempt() {
79
+ "$okf_bin" pro hook "$check" < "$work/event" > "$work/out" 2> "$work/err"
80
+ rc=$?
81
+ grep -q "^${MARKER}\$" "$work/err"
82
+ }
83
+
84
+ if attempt; then
85
+ :
86
+ elif [ -n "${OKF_PRO_NO_UNBUNDLE:-}" ]; then
87
+ # Pinned to the first attempt on purpose. The fallback is not free: inside a
88
+ # bundle, `Gem.find_latest_files` returns nothing at all, so a repo that
89
+ # deliberately vendors okf-pro through its Gemfile is served ONLY by the
90
+ # unstripped run, and stripping would lock it out of its own checker.
91
+ :
92
+ else
93
+ unbundle
94
+ attempt || :
95
+ fi
96
+
97
+ if ! grep -q "^${MARKER}\$" "$work/err"; then
98
+ # Whatever it did say first, then the verdict — the same order the passing
99
+ # path uses, so the last line on stderr is always this wrapper's answer.
100
+ sed "/^${MARKER}\$/d" "$work/err" >&2
101
+ echo "ENFORCEMENT DEGRADED — \`okf\` did not identify itself as the enforcer, so no check ran. Either okf-pro is not installed (\`gem install okf-pro\`), or something else on PATH answers to \`okf\`. Nothing here has been checked." >&2
102
+ exit 2
103
+ fi
104
+
105
+ # stdout is the protocol's own channel — a PreToolUse `ask` decision and the
106
+ # SessionStart banner both travel on it — so it is passed through byte for byte.
107
+ # The prototype's fix for this file captured it in a command substitution and
108
+ # swallowed both: an `ask` became a hard block, and the session banner vanished.
109
+ cat "$work/out"
110
+ sed "/^${MARKER}\$/d" "$work/err" >&2
111
+
112
+ case "$rc" in
113
+ 0) exit 0 ;;
114
+ 2) exit 2 ;;
115
+ *)
116
+ echo "ENFORCEMENT DEGRADED — \`okf pro hook $check\` exited $rc, which the hook protocol reads as non-blocking. Refusing instead: an exit code the gate did not choose is not a verdict." >&2
117
+ exit 2
118
+ ;;
119
+ esac
@@ -0,0 +1,233 @@
1
+ ---
2
+ name: okf-pro
3
+ description: Operating rules for the .okf/ knowledge bundle — filing new concepts, the board, the journal, the daily snapshot, closing work, source attribution, and the generated/verified attestation policy. Use before reading from or writing anything into .okf/.
4
+ ---
5
+
6
+ # Operating the bundle
7
+
8
+ Everything here is relative to `.okf/`, the bundle root — `reference/`
9
+ below is `.okf/reference/` on disk, and a link written
10
+ `/reference/thing.md` inside a concept resolves there.
11
+
12
+ ## What to load
13
+
14
+ This file is the whole of the three rules and the routing question, and
15
+ it is the part every session needs. Two things come before opening any
16
+ guide: the CLI already answers most questions about state, and the line
17
+ shapes are below.
18
+
19
+ ### Ask the CLI before you read a file
20
+
21
+ State is computed, not read. Every row below is one call answering what
22
+ several `cat`s answer, and the first row costs nothing at all — it is
23
+ already in front of you.
24
+
25
+ | to answer | run | not |
26
+ |---|---|---|
27
+ | what is on the board / what is in flight | *(already in the session banner)* | `cat board.md` |
28
+ | the board after you changed it | `okf pro state` | re-reading the file |
29
+ | one row per board line, with dates and links | `okf pro board` | counting headings by eye |
30
+ | what awaits the owner's read | `okf pro unverified` | grepping frontmatter |
31
+ | the day's snapshot line | `okf pro snapshot` | computing it by hand |
32
+ | every invariant at once | `okf pro audit` | — |
33
+
34
+ And five writes have exactly one correct form, so they are verbs rather
35
+ than shapes to reconstruct. Each is additive and targeted: it appends a
36
+ line or edits the line you named, refuses if the change it computed is
37
+ not the change it declared, and never rewrites a file.
38
+
39
+ | to do | run |
40
+ |---|---|
41
+ | capture something into the Inbox | `okf pro capture "the words you heard"` |
42
+ | move a line to In flight (refuses over the cap) | `okf pro promote <selector>` |
43
+ | move one back to Backlog | `okf pro demote <selector>` |
44
+ | open today's journal day and index it | `okf pro journal open` |
45
+ | the three mechanical closing moves | `okf pro close <project>` |
46
+
47
+ A `<selector>` is a `/projects/<slug>` link, a bare slug, or a substring
48
+ only one line carries — never a position. Two lines matching is a
49
+ refusal, not a coin toss.
50
+
51
+ Add `--json` to `state`, `board`, `snapshot`, `unverified` or
52
+ `friction` when you want to compute on the answer rather than read it;
53
+ `okf pro state --full` adds the corpus-derived parts behind one parse.
54
+ `audit` and `records` answer with their exit code and take no flags.
55
+
56
+ **The line shapes, so you do not have to open a guide for them.** A
57
+ board line starts `- ` at column zero — a `*` bullet or an indented dash
58
+ is invisible to every counter, including the cap. Inbox and Deadlines
59
+ lines lead with the date: `- YYYY-MM-DD — <the words>`. A Waiting line
60
+ carries `chase YYYY-MM-DD`, literally. A conflict line is an Inbox line
61
+ reading `- YYYY-MM-DD — Resolve: [/a.md] says X, [/b.md] says Y —
62
+ noticed while <doing what>`. The closure marker is the word then the
63
+ date with only spaces, a colon, a dash, asterisks or an opening
64
+ parenthesis between: `# Title — closed 2026-08-12`. That is a deliberate
65
+ partial duplication of the guides, and the guides remain the full
66
+ reference — this is here so the commonest question does not cost a file
67
+ read.
68
+
69
+ ### The guides
70
+
71
+ Each *act* has its detail one file away. Load the one the task calls
72
+ for, not all five.
73
+
74
+ * [guides/frontmatter.md](guides/frontmatter.md) — the *trust* family
75
+ this profile adds on top of the format: `generated:`, `verified:` and
76
+ the tiers they produce, `status:`, and the `stale_after:` windows.
77
+ **Before writing any concept, and whenever provenance is the
78
+ question.**
79
+ * [guides/board.md](guides/board.md) — the six sections, the date
80
+ grammar the counters actually read, and what makes a line invisible
81
+ to them. **Before editing `board.md`.**
82
+ * [guides/attribution.md](guides/attribution.md) — `sources:`, and the
83
+ keyed footnotes that tie one claim to one source. **Before filing
84
+ anything into `reference/`.**
85
+ * [guides/closing.md](guides/closing.md) — the four moves that close a
86
+ piece of work, and the exact spellings of the closure marker.
87
+ **Before marking a project closed.**
88
+ * [guides/okf-rules.md](guides/okf-rules.md) — the *format's* floor,
89
+ which holds for any OKF bundle and not just this profile: the three
90
+ required frontmatter keys, directory indexes, link form, the log
91
+ entry. **When `okf validate` or `okf lint` disagrees with you.**
92
+
93
+ Read this list permissively. A guide that is missing does not suspend
94
+ the rule it explains — the three rules below are operative on their own,
95
+ and the gates enforce them whether the file explaining them was read or
96
+ not. A file in `guides/` that this list does not mention still counts:
97
+ the index is a map, not a permission list, and adding your own guide
98
+ beside these is expected. Anything here you do not recognise — an extra
99
+ heading, a `<!-- rule: … -->` marker, a file kind you have not seen — is
100
+ left alone rather than treated as an error.
101
+
102
+ ## The three zones
103
+
104
+ | Zone | Directory | What lives there |
105
+ |------|-----------|------------------|
106
+ | **KNOW** | `reference/` | What other people produced — summarised, cited, attributed. |
107
+ | | `learnings/` | What I concluded, true beyond the work that produced it. |
108
+ | | `glossary/` | What a word means here, when it means three things elsewhere. |
109
+ | **ACT** | `projects/` | Work with a definition of done. Closes. |
110
+ | | `areas/` | A standard held indefinitely. Never closes. |
111
+ | **TIME** | `board.md` | The single page of forward state — the whole commitment surface. |
112
+ | | `journal/` | The backward record, one entry per day, append-only. |
113
+ | | `roadmap.md` | The quarterly wavelength. Sparse, links out. |
114
+
115
+ ## Where a new concept goes
116
+
117
+ One question routes almost everything: **does it outlive the piece of
118
+ work that produced it?**
119
+
120
+ * **No — it is work-scoped** → `projects/<slug-or-key>/`. Meeting notes,
121
+ the decision that only makes sense inside this project, the finding
122
+ that is really a status update.
123
+ * **Someone else wrote it** → `reference/`. Their document, their claim,
124
+ your summary of it, with the source recorded.
125
+ * **A conclusion of mine, true beyond one piece of work** → `learnings/`.
126
+ * **A term needing a fixed meaning** → `glossary/`.
127
+ * **What happened on a day** → `journal/`.
128
+
129
+ When two of these fit, the tiebreak is retrieval: file it where you
130
+ would look for it in six months, having forgotten which project it came
131
+ from.
132
+
133
+ When **none** of them fits, prefer the room that nearly does. Do not
134
+ open a catch-all: a room meaning "everything else" is a blind spot by
135
+ construction — things enter it and nothing enumerates them again — and
136
+ nobody has ever searched for the thing that did not fit.
137
+
138
+ The five are the starting set, not the closed set. A sixth room is
139
+ earned by an incident rather than anticipated: once the same misfiling
140
+ has actually cost you something, make the room, name it for the
141
+ question it answers rather than for its subject, and link it from
142
+ `index.md` — an unlinked directory is a room nobody can find.
143
+
144
+ Material you are *mirroring* rather than summarising — a script, run
145
+ instructions, a vendored copy — already has a home in the format: OKF
146
+ §6.3's `references/`. Note the plural: this bundle's `reference/` holds
147
+ your summaries of other people's work, and the two are one character
148
+ apart.
149
+
150
+ Before minting any name — a concept's title, a tag, a type — read the
151
+ naming policy in `/areas/corpus.md`. It is short, it is the adopter's
152
+ own standard, and a name that collides with what a word already means
153
+ in their domain is a file nobody can search for.
154
+
155
+ ## Rule 1 — Writing is reconciliation
156
+
157
+ Before a new concept settles — an inbox line promoted, a briefing
158
+ created, a finding recorded — search the bundle for what it collides
159
+ with: `okf search` on the claim's key terms, and read the bodies of
160
+ what comes back — not a glance at titles.
161
+ Three outcomes. It contradicts nothing: file it. It supersedes
162
+ something: correct the loser **now** — title and `description:` first,
163
+ `status: deprecated` on its frontmatter, superseded reasoning into
164
+ `<details>` — because ingestion is the only moment both claims are in
165
+ front of someone and the cost of noticing is at its minimum. The status
166
+ field is the machine-readable half of that move: it is what lets a later
167
+ search see that a collision was already settled instead of re-litigating
168
+ it, and the reconcile gate says so when it fires next.
169
+
170
+ Or the conflict cannot be settled on the spot: **file it, do not resolve
171
+ it** — one dated line on the board, both sides linked:
172
+
173
+ ```
174
+ - <date> — Resolve: [/reference/a.md] says X, [/glossary/b.md] says Y — noticed while <doing what>
175
+ ```
176
+
177
+ An unresolved contradiction is work. It competes for attention like
178
+ work, and only resolving it takes the line away. Know what this rule
179
+ cannot do: it catches collisions **as well as your search vocabulary
180
+ does, and no better** — which is why it has a second checkpoint:
181
+ whenever any search returns two concepts that disagree, that collision
182
+ gets the same board line, whatever you were looking for. And it is why
183
+ `index.md` curation and `glossary/` naming are load-bearing rather than
184
+ hygiene: the corpus's consistency is bounded by its findability.
185
+
186
+ ## Rule 2 — The day ends with a snapshot, and the delta is the signal
187
+
188
+ The end-of-day sitting (journal entry + board update, one habit)
189
+ appends one line to `log.md`, last entry under the day:
190
+
191
+ ```
192
+ * **Snapshot**: inbox N (oldest Xd) · in flight k/CAP · waiting N (M past chase) · backlog N · to read N · unverified briefings N · conflicts open N · deadlines within 7d not in flight N · projects with 0 concepts N
193
+ ```
194
+
195
+ The line is mechanical and belongs to `log.md`, not the journal —
196
+ counters are neither hunch nor judgment. Mechanical means derivable:
197
+ `okf pro snapshot .` computes the line, and the stop gate
198
+ refuses one that disagrees with the board it summarises — a counter
199
+ that drifted is a confession that lies. The deadlines field is a
200
+ confessed blind spot: a date due within seven days that no in-flight
201
+ line links is a collision visible before it lands. Its value is the
202
+ comparison with yesterday's line: "+8 inbox, oldest now 6d" is
203
+ information; a standing count is wallpaper, and a warning that is
204
+ always present carries zero information. If a delta deserves a sentence
205
+ of judgment, that sentence goes in the journal, which is where judgment
206
+ lives. And a day too heavy to journal gets the mechanical fallback:
207
+ reconstruct the entry from `log.md` and git history, and say in the
208
+ entry that it was reconstructed — a thin honest record beats a gap that
209
+ reads as a quiet day.
210
+
211
+ A brand-new bundle ships dateless — an undated `log.md`, an empty
212
+ `journal/`. That is day-zero state, not a defect: never backfill an
213
+ initialization entry or a day nobody worked. The first working day
214
+ creates its own `## <date>` section and first journal entry, and the
215
+ stop gate hands you the exact snapshot line to append.
216
+
217
+ ## Rule 3 — In flight is a budget
218
+
219
+ At most **5** demands in flight; `board.md` carries `In flight: k/5` at
220
+ the top. Promoting from Backlog requires demoting something — or
221
+ renegotiating the cap, which is allowed, visible, and
222
+ **journal-worthy**, because "the week the cap went to 6" is exactly
223
+ what a review needs and exactly what a silently exceeded cap hides. The
224
+ cap's job is not to make five the right number; it is to make overload
225
+ undeniable on the day it happens. Two exemptions: Deadlines (they come
226
+ due regardless of anyone's budget) and Backlog (captured, not claiming
227
+ attention). One question the budget asks on its own: an in-flight
228
+ demand no journal entry has linked in **5 working days** gets the
229
+ dormancy line — still in flight, or backlog pretending? Either answer
230
+ is fine; holding a slot without moving is not. The session banner asks
231
+ it mechanically, and stays quiet while the journal is younger than the
232
+ window — a bundle in its first week cannot be dormant, only new.
233
+ <!-- rule: okf-pro-dormancy-window -->
@@ -0,0 +1,46 @@
1
+ # Attributing what you did not write
2
+
3
+ Everything in `reference/` is somebody else's claim, and the bundle's
4
+ whole argument is that a claim can be traced back. That is `sources:` — a
5
+ list of what the concept was derived from, each entry with a `resource`
6
+ naming something a reader can follow (a URL, a bundle path) or, when there
7
+ is nothing to follow, the population it came from (`all invoices in
8
+ Q3 2026`).
9
+
10
+ ```yaml
11
+ sources:
12
+ - id: acme-pricing
13
+ resource: https://acme.example/pricing
14
+ title: Acme pricing page
15
+ author: team:acme-marketing
16
+ last_modified: 2026-08-01
17
+ ```
18
+
19
+ **Attribute individual claims with footnotes keyed to a `sources[].id`**,
20
+ not with a citations list at the bottom:
21
+
22
+ ```markdown
23
+ Seats are billed annually, with no monthly option.[^acme-pricing]
24
+ ```
25
+
26
+ The label *is* the join key — the reader resolves attribution through the
27
+ matching entry, and never by parsing the footnote's prose. Keyed rather
28
+ than positional because agents rewrite these documents constantly: a
29
+ `sources[0]` misattributes silently the moment the list is reordered,
30
+ while a stable `id` survives it.
31
+
32
+ Two lint checks hold this up, and both **block** an edit:
33
+
34
+ * a footnote whose label matches no `sources[].id` and has no definition
35
+ of its own — the claim points at nothing, which is worse than an
36
+ unattributed claim because it reads as attributed;
37
+ * a `sources[].resource` naming a bundle path that does not exist.
38
+
39
+ Neither fires on a concept with no `sources[].id` at all: a bundle that has
40
+ not adopted keyed attribution is not at fault, and an ordinary markdown
41
+ footnote is prose. The moment one source carries an `id`, the join is live.
42
+
43
+ A source with no `id` is fine when nothing in the body cites it
44
+ individually — the provenance is still recorded. Give it an `id` when a
45
+ sentence needs to point at it.
46
+