Module: OKF::Pro::Closing
- Defined in:
- lib/okf/pro/closing.rb
Overview
Rule 2 — the day ends with a snapshot, and the delta is the signal.
One of these refuses and one only informs, and that asymmetry is the rule itself: the snapshot is written at the end (stop-gate refuses without it, and refuses one that disagrees with the board), and read at the beginning (session-context puts yesterday's line in front of you before anything else). A counter nobody compares is wallpaper.
Both root through BundleRoot.enclosing — the ancestor walk — because their only input is the session's cwd, and a session parked in a bundle subdirectory must not be a session whose stop gate silently disengaged or mistook a directory index for a broken core. resolve() does neither walk nor discriminate, and was both of those bugs at once.
Constant Summary collapse
- REFRESH =
The line that actually saves the turns: without it the banner is state an agent re-reads out of the files the moment it writes anything.
"Current as of session start. After you write, refresh with `okf pro state` — " \ "do not re-read the files. `okf pro board` lists the lines; `okf pro capture`, " \ "`promote`, `demote`, `journal open` and `close` write the mechanical shapes."
Class Method Summary collapse
-
.friction_line(root) ⇒ Object
Addressed to the ADOPTER, not the maintainer, and that is the whole of its tone: they are the ones paying for a missing verb, and they are the only ones who can say which one it is.
- .session_context(event, today: Date.today) ⇒ Object
-
.snapshot_line(snap) ⇒ Object
The logged line WHOLE, with the day it was logged under and the label that stops a reader taking it for today's.
-
.state_block(payload, today) ⇒ Object
THE WHOLE BANNER, and there is only one of it.
- .stop_gate(event, today: Date.today) ⇒ Object
Class Method Details
.friction_line(root) ⇒ Object
Addressed to the ADOPTER, not the maintainer, and that is the whole of
its tone: they are the ones paying for a missing verb, and they are the
only ones who can say which one it is. Nothing is ever filed
automatically — --issue prints a command a person decides to run.
A recorder that could not write says so rather than printing a zero. A zero and "unknown" are different states, and this is the one line in the banner where confusing them would quietly retire the measurement.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/okf/pro/closing.rb', line 162 def friction_line(root) report = Friction.report(root) unless report.available return [ "Friction: the recorder could not write to .tmp/ at some point, so the count is " \ "unknown rather than zero. `okf pro friction --clear` starts it again." ] end # Only what a VERB covers, and that is the difference between a request and # a nag. Shell redirects are recorded too, and `covered_by` answers "Edit or # Write" for them — the trust guards read a tool event and a redirect # produces none — so counting them here claimed a verb could have done # something no verb covers. Worse: `shell-guard` records INTENT, at # PreToolUse, so the system working exactly as designed (guard fires, owner # denies, agent uses Edit) still incremented a lifetime counter that then # nagged every session until `--clear`. `okf pro friction` shows every row; # this line asks for a verb, so it counts only what a verb would answer. # A log that will not parse is a SHORT count, not a quiet one, and this # is the line where a zero and an unknown must never be confused. Only # the unwritable case was reported; an unreadable one said nothing at # all, which is the same lie one door along. lines = [] if report.unreadable.positive? lines << "Friction: #{report.unreadable} recorded line(s) will not parse, so anything " \ "counted here is short by an unknown amount. `okf pro friction --clear` starts " \ "it again." end covered = report.events.count { |e| Friction.verb_covered?(e["via"], e["what"]) } return lines if covered.zero? # "so far", not "last session", and the difference is not pedantry: the # log is append-only and nothing prunes it, so this is a lifetime total. # A line that framed a cumulative number as this session's would nag # about a week nobody can change, and would keep nagging — which is the # standing-warning failure the whole design is built to avoid. Naming # the reset is what makes the number actionable. lines << "#{covered} bundle edit(s) so far were done by hand that an `okf pro` verb " \ "could do. If one of them should be a verb, please tell the maintainer — `okf pro " \ "friction --issue` prints a ready-to-paste report, and `--clear` resets the count. " \ "It helps more than you think." lines end |
.session_context(event, today: Date.today) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/okf/pro/closing.rb', line 66 def session_context(event, today: Date.today) root = BundleRoot.enclosing(event.cwd) return nil if root.nil? # This channel cannot refuse, so a broken core is said out loud rather # than shown as a suspiciously quiet banner. broken = Audit.structure(root) unless broken.empty? return [ "Bundle at #{root} has a broken core:", *broken.map { |m| " — #{m}" }, "The gates cannot read a bundle missing its skeleton; restore it, then `okf pro audit .`." ] end lines = state_block(State.call(root, today: today), today) lines.concat(Budget.dormancy_questions(root, today: today, board: Pro.read_text(File.join(root, "board.md")))) lines << REFRESH lines.concat(friction_line(root)) lines end |
.snapshot_line(snap) ⇒ Object
The logged line WHOLE, with the day it was logged under and the label that
stops a reader taking it for today's. Both halves earn their place: the
twelve counters are what "read the delta" is read against, and two of them
— unverified briefings and projects with 0 concepts — can only be
recomputed by parsing the corpus, which this banner never does. A second
line restating those two said nothing this one does not.
Latest by date, not last in the file: State reads the log newest-first,
because grepping the whole file for the last hit found its bottom — the
oldest entry — and from day two onward the banner reported the first-ever
counters, inverting the delta it exists to show.
148 149 150 151 152 |
# File 'lib/okf/pro/closing.rb', line 148 def snapshot_line(snap) return "Last snapshot: none yet" if snap.nil? "Last snapshot (#{snap["day"]}, not live): #{snap["line"]}" end |
.state_block(payload, today) ⇒ Object
THE WHOLE BANNER, and there is only one of it.
The bill is turns × context, not payload size. A measured session spent nine tool calls and 40.9% of its tool output rediscovering state the gates already compute — and state delivered here costs no turn at all, because SessionStart runs whether anyone asks or not.
Which is exactly why nothing may be said twice. This began as a structured block appended UNDER three prose lines, and the two overlapped: in flight and inbox were counted in both, and the snapshot was rendered in both. That spends the saving the block exists to make, and leaves a reader working out which of the two numbers is live.
CHEAP SOURCES ONLY, and that is a constraint rather than a preference.
State.call's default payload reads board.md, log.md and two directory
globs, and parses no concept. The two counters that would need
Bundle::Reader.read are never recomputed here — they arrive inside the
logged Snapshot line, printed whole and labelled by its day.
Staleness is already covered elsewhere: the stop gate recomputes independently and refuses a snapshot line that disagrees with the board.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/okf/pro/closing.rb', line 114 def state_block(payload, today) log = payload["log"] open_projects = payload["projects"]["open"] # The board's counters are formatted by `State`, not here: the banner's # whole promise is that it says what `okf pro state` would say, and two # renderings of the same eleven numbers drift into a reader having to # check which one they are looking at. lines = [ State.board_line(payload["board"], "Bundle state at session start —"), "Log — newest day #{log["newest day"] || "none yet"} · journal for #{today} " \ "#{log["journal today"] ? "open" : "not opened"} · open projects #{open_projects.size}" \ "#{" (#{open_projects.join(", ")})" unless open_projects.empty?}", snapshot_line(payload["last snapshot"]), "Read the delta, not the status: a number that moved the wrong way is today's first signal." ] # Crack 2's other half: the collision made visible days before it lands. risk = payload["deadlines at risk"] unless risk.empty? lines << "Deadlines within 7d with nothing in flight against them:" risk.each { |line| lines << " #{line}" } end lines end |
.stop_gate(event, today: Date.today) ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/okf/pro/closing.rb', line 21 def stop_gate(event, today: Date.today) return [] if event.stop_hook_active? root = BundleRoot.enclosing(event.cwd) return [] if root.nil? # The precondition is "work happened". A session that read and changed # nothing owes no snapshot, and asking for one would train people to # write a line that means nothing. return [] unless Pairing.dirty_markdown?(root) # A bundle missing its skeleton used to slip this gate in silence — # no board, no log, no questions asked. Broken core is the first # refusal, and the only one, because every later check reads the core. broken = Audit.structure(root) return [ "RULE 2 — before stopping:\n#{broken.map { |m| "— #{m}" }.join("\n")}" ] unless broken.empty? # One read of the board, one parse of the bundle, shared by every # check below — this gate runs on every Stop, and it used to pay for # both twice. board = Pro.read_text(File.join(root, "board.md")) concepts = ::OKF::Bundle::Reader.read(root).concepts msgs = [] snap = Log.snapshot_line(Pro.read_text(File.join(root, "log.md")), today.to_s) if snap.nil? msgs << "— log.md has no Snapshot line under #{today}. Append it before stopping — " \ "computed from the bundle as it stands:\n #{Snapshot.line(root, today: today, board: board, concepts: concepts)}" else # Presence was never the point; agreement is. This runs at stop time, # which is the one moment the calendar cannot make it lie — the same # check in CI would fail on a push nobody made that day. msgs.concat(Snapshot.verify(root, snap, today: today, board: board, concepts: concepts)) end # A dated line the counters cannot parse counts as zero, and a zero # is indistinguishable from a quiet board — so the snapshot the gate # just verified would agree with itself about a deadline it cannot # see. The unreadable line is a refusal, not a rounding error. msgs.concat(Board.grammar(board).map { |m| "— board: #{m}" }) msgs.concat(Pairing.failures(root, board: board, concepts: concepts)) return [] if msgs.empty? [ "RULE 2 — before stopping:\n#{msgs.join("\n")}" ] end |