Module: OKF::Pro::CLI
- Defined in:
- lib/okf/pro/cli.rb
Overview
Dispatch, and the exit codes the hook protocol reads.
Every check has the same signature — event in, messages out, empty means pass — so this table is the only place a check name is bound to behaviour, and the only place an exit code is chosen. A drill runs exactly what a session runs because both arrive here.
Constant Summary collapse
- CHECKS =
{ "guard-verified" => ->(event) { Guards.guard_verified(event) }, "journal-guard" => ->(event) { Guards.journal_guard(event) }, "shell-guard" => ->(event) { ShellGuard.check(event) }, "check-okf" => ->(event) { Conformance.check(Target.for(event)) }, "cap-check" => ->(event) { Budget.cap_check(Target.for(event)) }, "reconcile-search" => ->(event) { Reconcile.search(Target.for(event), event) }, "post-edit" => ->(event) { CLI.post_edit(event) }, "stop-gate" => ->(event) { Closing.stop_gate(event) } }.freeze
- SCAFFOLD =
Every name this module answers to. The hook door accepts a strictly narrower set — CHECKS.keys plus session-context — enforced one layer up, in OKF::CLI::Pro, because
rundispatches the CI verbs off the same first element: a settings.json typo spellinghook auditwould otherwise install a gate that reads no stdin, never blocks, and reports clean. The generator's verbs. They take a destination rather than a bundle, so they do not go throughdir_argument—setupinto an empty directory is the whole point, and refusing one that holds no bundle would refuse every first run. %w[setup upgrade skill].freeze
- READERS =
The readers: one call answers what nine calls answered. No new logic — every aggregation they print already existed and was consumed only by a gate, which is why an agent working in a seeded bundle rediscovered the board by reading raw markdown while the stop gate was computing it.
%w[audit records snapshot unverified state board friction].freeze
- WRITERS =
The writers: the shapes with exactly one correct form. Each one is additive and targeted, and each refuses through
Conserverather than promising to be careful — seewrites.rbfor the contract. %w[capture promote demote journal close].freeze
- NAMES =
(CHECKS.keys + [ "session-context" ] + READERS + WRITERS + SCAFFOLD).freeze
- HOOK_NAMES =
The names the hook door accepts, and the only ones.
rundispatches the CI verbs off the same first element, so without this the adapter would forwardhook auditinto a verb that reads no stdin and cannot block. (CHECKS.keys + [ "session-context" ]).freeze
- MARKER =
Written to stderr immediately before a check runs, and read by the scaffold's
.claude/hooks/run. It is the whole of the wrapper's identity proof: a strayokfon PATH that exits 0 is indistinguishable from a clean gate by status alone, and that is a gate silently switched off. The wrapper strips this line before passing stderr on.Emitted here rather than at the door, so that its presence means the check was actually reached — not merely that the plugin loaded. The prototype proved the difference: its
okf pro contracthandshake answered fine while the library was missing. "okf-pro-enforcer v1"- REFUSED =
What
dir_argumentreturns instead of a path. Distinct from nil, which is the legitimate "no argument given, use the working directory". :refused- USAGE =
The command list, and the single place it is described.
okf helpprints a second copy through OKF::CLI::Pro.help_rows, and it has to: reading this one would make everyokf helpload the whole library, which is the cost deferring the require exists to avoid. The two are joined by a test instead — the arrangement the closure grammar and the dormancy window already use. [ [ "setup", "[DIR]", "create or complete an agent's brain in DIR (default .)" ], [ "upgrade", "[DIR]", "rewrite the gem-owned governance files; stage the rest" ], [ "state", "[DIR]", "what is on the board, in one call — add --full for the corpus" ], [ "board", "[DIR]", "one row per board line: section, dates, age, links" ], [ "capture", "TEXT", "append a dated Inbox line" ], [ "promote", "SEL", "Inbox or Backlog to In flight, refusing over the cap" ], [ "demote", "SEL", "In flight back to Backlog" ], [ "journal", "open", "create today's journal day and index it" ], [ "close", "SLUG", "the three mechanical closing moves for a project" ], [ "audit", "[DIR]", "every invariant at once — the CI door" ], [ "records", "[DIR]", "does the staged commit rewrite a past journal day?" ], [ "snapshot", "[DIR]", "compute the day's counter line (prints, never writes)" ], [ "unverified", "[DIR]", "generated concepts still awaiting the owner's read" ], [ "friction", "[DIR]", "what was done by hand that a verb could do" ], [ "skill", "DEST", "(re)install okf-pro's agent skill on its own" ], [ "hook", "CHECK", "run one gate against a hook event on stdin" ] ].freeze
- FLAGS =
Which flags each verb accepts, declared rather than discovered: an undeclared flag is a usage error the parser names, instead of a positional that
dir_argumentthen reports as a second directory.Absence from this table means "accepts none", not "is exempt from it":
parse_flagsreadsFLAGS.fetch(verb, []), so a verb that routes through it refuses an undeclared flag whether or not it is listed. The verbs that skipped the parser entirely are howokf pro audit --jsoncame to hand--jsontoBundleRoot.resolveand report "holds no OKF bundle" as a FINDING — exit 1, which a pipeline reads as a broken bundle rather than as its own typo. { "state" => %i[json pretty full], "board" => %i[json pretty section], "snapshot" => %i[json pretty], "unverified" => %i[json pretty], "friction" => %i[json pretty issue clear] }.freeze
- FLAG_HELP =
How each flag is spelled in the usage. Read FROM
FLAGSrather than typed beside it, so a flag added to one verb cannot be missing from the help —parse_flagstells a user thatokf pro --helplists what each verb takes, and for a while that was simply untrue. { json: "--json", pretty: "--pretty", full: "--full", section: "--section NAME", issue: "--issue", clear: "--clear" }.freeze
- ISSUE_REPO =
Where a friction report goes. It is about okf-pro, not about the adopter's knowledge base, so it is this repository regardless of whose tree the bundle lives in. Nothing is ever filed automatically.
"serradura/okf-gem"- HELP =
%w[--help -h help].freeze
- VERSION_FLAGS =
The bare semantic version, matching
okf --versionandokf mcp --version. No gem name: the caller has already named the extension on the command line, so a name in the output buys nothing and costs a script acut. (okf tui --versionprintsokf-tui 1.0.0and is the odd one out; changing a released gem's output is not this gem's to do.) %w[--version -v version].freeze
- UNAVAILABLE_NOTE =
Names the file, because "check that .tmp/ is writable" is not actionable once the marker is the thing keeping the answer unknown — the directory may be perfectly writable now and the report still says unknown, which is correct and infuriating without the next sentence.
"okf pro friction — the recorder could not write at some point, so this is " \ "not zero, it is unknown. Check that .tmp/ is writable at the repository " \ "root, then `okf pro friction --clear` to start counting again."
- CLOSE_ARGUMENT =
closetakes a PROJECT, not a board-line selector, and the difference is not pedantry:Writes.closerequires one directory segment, so the/projects/<slug>/index.mdform a board line actually carries — the oneokf pro boardprints — is a refusal, and so is a substring. The shared message offered both, which sends the reader to try what the verb rejects. "takes the project to close: its directory name under projects/, or the " \ "`/projects/<slug>/` link a board line carries (with or without its " \ "`index.md`). It names one directory segment either way, so a substring " \ "of a board line is not one. `okf pro state` lists the open ones."
- SELECTOR_ARGUMENT =
"takes what to act on: a `/projects/<slug>` link, a slug, or a " \ "substring only one board line carries. `okf pro board` lists them."
Class Method Summary collapse
- .audit(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
- .board(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
- .board_row(row, today) ⇒ Object
- .bundle_for(argv, verb, stderr) ⇒ Object
- .bundle_with_board(argv, verb, stderr) ⇒ Object
-
.capture(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
── the writers ──────────────────────────────────────────────────────.
-
.clear_friction(stdout, root) ⇒ Object
The way out of a sticky marker, and the way to stop a lifetime total nagging about a week nobody can change.
- .close(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
- .demote(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
-
.dir_argument(argv, verb, stderr) ⇒ Object
Every bundle-taking verb reads one directory and nothing else.
- .emit(stdout, options, payload) ⇒ Object
- .friction(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
-
.friction_rows(report) ⇒ Object
Grouped by the DOOR as well as the file.
-
.guarded(verb, stderr) ⇒ Object
A verb that cannot run exits 2, never 1 — 1 means findings, and a pipeline that cannot tell a broken bundle from a broken checker learns to ignore both.
-
.help_answer(stdout) ⇒ Object
Not the options Hash and not nil: the caller has already been answered, and there is nothing left to do but exit 0.
- .issue_body(_root, report, rows) ⇒ Object
-
.issue_report(stdout, root, report, rows) ⇒ Object
Printed, never run.
- .journal(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
-
.parse_flags(argv, verb, stdout, stderr) ⇒ Object
Parsed BEFORE
dir_argument, which refuses any second positional and would otherwise report `okf pro state . -
.post_edit(event) ⇒ Object
The three PostToolUse checks in one process, sharing one bundle read — the read is the expensive part, and three separate invocations paid for it three times over.
- .promote(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
-
.records(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
The append-only record, asked of the index.
- .render_board(rows) ⇒ Object
- .render_friction(report, rows) ⇒ Object
- .resolve_or_complain(start, verb, stderr) ⇒ Object
- .run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ Object
- .scaffold(verb, argv, stdout: $stdout, stderr: $stderr) ⇒ Object
- .section?(text, name) ⇒ Boolean
- .selector_verb(verb, argv, stdout, stderr, missing: SELECTOR_ARGUMENT) ⇒ Object
-
.session_context(stdin, stdout: $stdout) ⇒ Object
The one check that does not refuse.
-
.shell_quote(text) ⇒ Object
Single-quoted with the one escape a POSIX shell accepts inside them.
-
.snapshot(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
The checker-first half of derivation: computes the mechanical line for a person to append.
-
.state(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
── the readers ──────────────────────────────────────────────────────.
-
.unreadable_note(report) ⇒ Object
Report-only, exit 0 — the family
unverifiedbelongs to. -
.unverified(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
Report-only, and for a shape of reason worth stating: absent attestation is the truth, and a gate here would pressure toward the one lie the system guards against.
-
.usage(stream, status) ⇒ Object
The command list, plus the two things that surprise people — a ref is not a path here, and
hookdoes not speak this repo's exit codes. - .version_answer(stdout) ⇒ Object
- .which(name) ⇒ Object
-
.write_verb(verb, argv, stdout, stderr) ⇒ Object
The shape every writer shares: resolve, run, print, choose the code.
-
.writer_flags(argv, verb, stdout, stderr) ⇒ Object
The writers take no flags, and that is precisely why they need this.
Class Method Details
.audit(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/okf/pro/cli.rb', line 240 def audit(argv, stdout: $stdout, stderr: $stderr) = parse_flags(argv, "audit", stdout, stderr) return == :handled ? PASS : BLOCK unless .is_a?(Hash) root = dir_argument(argv, "audit", stderr) return BLOCK if root == REFUSED begin msgs = Audit.call(root || Dir.pwd) rescue StandardError => e stderr.puts "okf pro audit — could not run (#{e.class}: #{e.}); nothing was " \ "checked. This is exit 2, not 1: 1 means findings, and a pipeline that " \ "cannot tell a broken bundle from a broken checker learns to ignore both." return BLOCK end if msgs.empty? stdout.puts "okf pro audit — clean." return PASS end stderr.puts "okf pro audit — #{msgs.size} finding(s):\n#{msgs.join("\n")}" FAIL end |
.board(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/okf/pro/cli.rb', line 358 def board(argv, stdout: $stdout, stderr: $stderr) = parse_flags(argv, "board", stdout, stderr) return == :handled ? PASS : BLOCK unless .is_a?(Hash) root = bundle_with_board(argv, "board", stderr) return BLOCK if root.nil? text = Pro.read_text(File.join(root, "board.md")) rows = Board.rows(text) if [:section] # An empty section and a misspelled one look identical in the output # and mean opposite things, so the heading is asked for by name. This # is the quiet-zero class the board's own grammar check exists for. unless section?(text, [:section]) stderr.puts "okf pro board — the board has no '## #{[:section]}' section. " \ "An empty answer here would be indistinguishable from a section that is simply empty." return BLOCK end rows = rows.select { |row| row.section.casecmp([:section]).zero? } end today = Date.today payload = rows.map { |row| board_row(row, today) } emit(stdout, , payload) { render_board(payload) } PASS end |
.board_row(row, today) ⇒ Object
390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/okf/pro/cli.rb', line 390 def board_row(row, today) date = row.line_date { "section" => row.section, "text" => row.text, "date" => date&.to_s, "age" => date && (today - date).to_i, "chase" => row.chase_date&.to_s, "targets" => row.targets } end |
.bundle_for(argv, verb, stderr) ⇒ Object
781 782 783 784 785 786 |
# File 'lib/okf/pro/cli.rb', line 781 def bundle_for(argv, verb, stderr) start = dir_argument(argv, verb, stderr) return nil if start == REFUSED resolve_or_complain(start, verb, stderr) end |
.bundle_with_board(argv, verb, stderr) ⇒ Object
788 789 790 791 792 793 794 795 |
# File 'lib/okf/pro/cli.rb', line 788 def bundle_with_board(argv, verb, stderr) root = bundle_for(argv, verb, stderr) return nil if root.nil? return root if File.exist?(File.join(root, "board.md")) stderr.puts "okf pro #{verb} — #{root} has no board.md to read." nil end |
.capture(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
── the writers ──────────────────────────────────────────────────────
595 596 597 598 599 600 601 602 603 604 605 606 |
# File 'lib/okf/pro/cli.rb', line 595 def capture(argv, stdout: $stdout, stderr: $stderr) answered = writer_flags(argv, "capture", stdout, stderr) return answered == :handled ? PASS : BLOCK unless answered == :ok text = argv.shift if text.nil? stderr.puts "okf pro capture — takes the words to capture: `okf pro capture \"what you heard\"`." return BLOCK end write_verb("capture", argv, stdout, stderr) { |root| Writes.capture(root, text) } end |
.clear_friction(stdout, root) ⇒ Object
The way out of a sticky marker, and the way to stop a lifetime total
nagging about a week nobody can change. It touches .tmp/ and nothing
else — this is telemetry, not knowledge, and no bundle file is involved.
448 449 450 451 452 453 454 455 456 457 458 |
# File 'lib/okf/pro/cli.rb', line 448 def clear_friction(stdout, root) removed = Friction.clear(root) if removed.nil? stdout.puts "okf pro friction — could not clear #{File.dirname(Friction.log_path(root))}. " \ "Remove the files by hand: #{Friction.log_path(root)} and #{Friction.marker_path(root)}." return PASS end stdout.puts "okf pro friction — cleared (#{removed} file(s) removed). The count starts again from here." PASS end |
.close(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
642 643 644 |
# File 'lib/okf/pro/cli.rb', line 642 def close(argv, stdout: $stdout, stderr: $stderr) selector_verb("close", argv, stdout, stderr, missing: CLOSE_ARGUMENT) { |root, slug| Writes.close(root, slug) } end |
.demote(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
612 613 614 |
# File 'lib/okf/pro/cli.rb', line 612 def demote(argv, stdout: $stdout, stderr: $stderr) selector_verb("demote", argv, stdout, stderr) { |root, selector| Writes.demote(root, selector) } end |
.dir_argument(argv, verb, stderr) ⇒ Object
Every bundle-taking verb reads one directory and nothing else.
A leading @ is refused by name rather than treated as a path. okf help promises "Anywhere a <dir> goes, an @slug goes", and that
promise is the kernel's; this gem does not keep it yet, and okf-tui
already shipped the failure of pretending otherwise — a ref reached the
filesystem and came back "not a directory", which tells the user nothing
about what is actually missing.
A second positional is refused for the reason ../AGENTS.md records
against okf lint a b: silently reading the first and ignoring the rest
is a wrong answer with a green exit code.
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 |
# File 'lib/okf/pro/cli.rb', line 869 def dir_argument(argv, verb, stderr) start = argv.shift unless argv.empty? stderr.puts "okf pro #{verb} — takes one directory, and #{argv.size + 1} were given " \ "(#{([ start ] + argv).join(" ")}). Reading the first and ignoring the rest " \ "would be a wrong answer with a clean exit code." return REFUSED end if start.to_s.start_with?("@") stderr.puts "okf pro #{verb} — '#{start}' is a registry ref, and this verb takes a " \ "path. The registry answers where a bundle is; a brain is the repository " \ "around one, which a ref does not name. Give the directory." return REFUSED end start end |
.emit(stdout, options, payload) ⇒ Object
762 763 764 765 766 767 768 |
# File 'lib/okf/pro/cli.rb', line 762 def emit(stdout, , payload) if [:json] stdout.puts([:pretty] ? JSON.pretty_generate(payload) : JSON.generate(payload)) else stdout.puts yield end end |
.friction(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 |
# File 'lib/okf/pro/cli.rb', line 412 def friction(argv, stdout: $stdout, stderr: $stderr) = parse_flags(argv, "friction", stdout, stderr) return == :handled ? PASS : BLOCK unless .is_a?(Hash) root = bundle_for(argv, "friction", stderr) return BLOCK if root.nil? return clear_friction(stdout, root) if [:clear] report = Friction.report(root) rows = friction_rows(report) return issue_report(stdout, root, report, rows) if [:issue] payload = { "available" => report.available, "recorded" => report.events.size, "unreadable" => report.unreadable, "by" => rows } emit(stdout, , payload) { render_friction(report, rows) } PASS end |
.friction_rows(report) ⇒ Object
Grouped by the DOOR as well as the file. An Edit to the board and a shell redirect at it are different findings about the same path: one says a verb went unused, the other says the trust guards were bypassed entirely, and what covers them is not the same answer.
435 436 437 438 439 440 441 442 443 |
# File 'lib/okf/pro/cli.rb', line 435 def friction_rows(report) counts = report.events.each_with_object({}) do |event, acc| key = [ event["via"].to_s, event["what"].to_s ] acc[key] = (acc[key] || 0) + 1 end counts.sort_by { |(via, what), n| [ -n, via, what ] }.map do |(via, what), n| { "via" => via, "what" => what, "count" => n, "covered by" => Friction.covered_by(via, what) } end end |
.guarded(verb, stderr) ⇒ Object
A verb that cannot run exits 2, never 1 — 1 means findings, and a
pipeline that cannot tell a broken bundle from a broken checker learns
to ignore both. The same reasoning audit states at length.
773 774 775 776 777 778 779 |
# File 'lib/okf/pro/cli.rb', line 773 def guarded(verb, stderr) yield rescue StandardError => e stderr.puts "okf pro #{verb} — could not run (#{e.class}: #{e.}); nothing was " \ "read or written. Exit 2, for the reason `audit` gives." nil end |
.help_answer(stdout) ⇒ Object
Not the options Hash and not nil: the caller has already been answered, and there is nothing left to do but exit 0.
752 753 754 755 |
# File 'lib/okf/pro/cli.rb', line 752 def help_answer(stdout) usage(stdout, PASS) :handled end |
.issue_body(_root, report, rows) ⇒ Object
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 |
# File 'lib/okf/pro/cli.rb', line 550 def issue_body(_root, report, rows) lines = [ "`okf pro friction` recorded these while working in a bundle. Each is an edit made", "by hand inside it — some to a file a command already covers, some through a door", "the trust guards cannot see at all. The `covered by` note says which.", "" ] # The evidence is printed whatever the recorder's state. Branching the # whole body on `available` dropped every row it had and then said "the # counts above are unknown" over a body with nothing above it — a # sentence about text that was not there, under a title that still # counted the rows. The marker is sticky by design, so one failed write # weeks ago made every later report evidence-free. rows.each do |row| covered = row["covered by"] lines << "* `#{row["via"]} #{row["what"]}` — #{row["count"]} time(s)#{" (covered by: #{covered})" if covered}" end lines << "" << "Recorded over #{report.events.size} event(s)." lines << "#{report.unreadable} recorded line(s) were unparseable and are excluded." if report.unreadable.positive? unless report.available lines << "The recorder could not write at some point, so this is a floor rather than a " \ "total: the real count is higher by an unknown amount." end lines << "" << "okf-pro #{VERSION}, ruby #{RUBY_VERSION}." lines.join("\n") end |
.issue_report(stdout, root, report, rows) ⇒ Object
Printed, never run. Filing an issue is outward-facing and irreversible, and a hook that did it unattended would be both without anyone asking.
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 |
# File 'lib/okf/pro/cli.rb', line 506 def issue_report(stdout, root, report, rows) # Nothing counted, and the recorder says it counted honestly — so there # is nothing to file. The report drafted here was complete and # ready-to-paste and said "Recorded over 0 event(s)", which asks the # maintainer to act on an empty list. # # Two states are NOT that, and both still file. "The recorder could not # write" is something that happened; so is a log whose every line was # unparseable, which is a corrupted recorder rather than a quiet one — # zero rows with a positive `unreadable` is the shape that would # otherwise be reported as nothing at all. if rows.empty? && report.available && report.unreadable.zero? stdout.puts "okf pro friction — nothing recorded, so there is no report to file. " \ "Either the verbs covered it or nothing was written by hand; an issue " \ "whose body is an empty list asks the maintainer to act on nothing." return PASS end # Says what was counted, and claims nothing about what covers it. The # banner had just established that a shell redirect's answer is Edit or # Write rather than a verb, and a title reading "that a verb could # cover" over a body whose only row says otherwise asks the maintainer # for a verb this gem decided not to want. The `covered by` note on # each row is where that question is actually answered. title = "okf-pro: #{report.events.size} bundle edit(s) recorded by hand" body = issue_body(root, report, rows) if which("gh") stdout.puts "Run this — it is not run for you, because filing an issue is yours to decide:" stdout.puts stdout.puts "gh issue create --repo #{ISSUE_REPO} \\" stdout.puts " --title #{shell_quote(title)} \\" stdout.puts " --body #{shell_quote(body)}" else stdout.puts "`gh` is not on PATH, so here is the issue to paste:" stdout.puts stdout.puts " https://github.com/#{ISSUE_REPO}/issues/new" stdout.puts stdout.puts "Title: #{title}" stdout.puts stdout.puts body end PASS end |
.journal(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
616 617 618 619 620 621 622 623 624 625 626 627 628 629 |
# File 'lib/okf/pro/cli.rb', line 616 def journal(argv, stdout: $stdout, stderr: $stderr) answered = writer_flags(argv, "journal", stdout, stderr) return answered == :handled ? PASS : BLOCK unless answered == :ok sub = argv.shift unless sub == "open" stderr.puts "okf pro journal — takes one subcommand, `open`, and was given " \ "#{sub.nil? ? "none" : "'#{sub}'"}. `okf pro journal open` creates today's day " \ "file and its index line; what goes in it is yours to write." return BLOCK end write_verb("journal open", argv, stdout, stderr) { |root| Writes.journal_open(root) } end |
.parse_flags(argv, verb, stdout, stderr) ⇒ Object
Parsed BEFORE dir_argument, which refuses any second positional and
would otherwise report okf pro state . --json as two directories.
--help and --version are declared rather than left to OptionParser's
"officious" defaults, which call exit from inside the parse — a
process exit out of a library call, in a gem whose whole subject is
which exit code a gate returns.
Returns the options Hash, :help when the caller asked for the usage,
or nil on a parse error already reported.
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 |
# File 'lib/okf/pro/cli.rb', line 726 def parse_flags(argv, verb, stdout, stderr) = { json: false, pretty: false, full: false, issue: false, clear: false, section: nil } allowed = FLAGS.fetch(verb, []) parser = OptionParser.new do |o| o. = "Usage: okf pro #{verb} [DIR]" o.on("--json") { [:json] = true } if allowed.include?(:json) o.on("--pretty") { [:json] = [:pretty] = true } if allowed.include?(:pretty) o.on("--full") { [:full] = true } if allowed.include?(:full) o.on("--issue") { [:issue] = true } if allowed.include?(:issue) o.on("--clear") { [:clear] = true } if allowed.include?(:clear) o.on("--section NAME") { |value| [:section] = value } if allowed.include?(:section) o.on("-h", "--help") { [:help] = true } o.on("-v", "--version") { [:version] = true } end parser.parse!(argv) return version_answer(stdout) if [:version] return help_answer(stdout) if [:help] rescue OptionParser::ParseError => e stderr.puts "okf pro #{verb} — #{e.}. `okf pro --help` lists what each verb takes." nil end |
.post_edit(event) ⇒ Object
The three PostToolUse checks in one process, sharing one bundle read — the read is the expensive part, and three separate invocations paid for it three times over.
149 150 151 152 153 154 155 156 157 158 |
# File 'lib/okf/pro/cli.rb', line 149 def post_edit(event) target = Target.for(event) # The second friction point, and the same argument as the first: this # runs on every Edit/Write already, so recording here costs no hook # registration an adopter would never receive. Only the files a verb # covers count — an Edit to a concept body is judgment and always will # be, and counting it would report the system working as friction. Friction.record(target.root, "edit", target.rel) if target && Friction.covered_path?(target.rel) Conformance.check(target) + Budget.cap_check(target) + Reconcile.search(target, event) end |
.promote(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
608 609 610 |
# File 'lib/okf/pro/cli.rb', line 608 def promote(argv, stdout: $stdout, stderr: $stderr) selector_verb("promote", argv, stdout, stderr) { |root, selector| Writes.promote(root, selector) } end |
.records(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
The append-only record, asked of the index. Separate from audit
because it reads a CHANGE (git's staged diff) rather than a STATE,
and the pre-commit door materialises a tree where that change is no
longer visible as one.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/okf/pro/cli.rb', line 268 def records(argv, stdout: $stdout, stderr: $stderr) = parse_flags(argv, "records", stdout, stderr) return == :handled ? PASS : BLOCK unless .is_a?(Hash) root = dir_argument(argv, "records", stderr) return BLOCK if root == REFUSED begin msgs = Records.staged_violations(root || Dir.pwd) rescue StandardError => e stderr.puts "okf pro records — could not run (#{e.class}: #{e.}); the staged " \ "diff was never read. Exit 2, for the reason `audit` gives." return BLOCK end if msgs.empty? stdout.puts "okf pro records — append-only." return PASS end stderr.puts "okf pro records — #{msgs.size} finding(s):\n#{msgs.join("\n")}" FAIL end |
.render_board(rows) ⇒ Object
402 403 404 405 406 407 408 409 410 |
# File 'lib/okf/pro/cli.rb', line 402 def render_board(rows) return [ "okf pro board — no lines." ] if rows.empty? rows.map do |row| age = row["age"] ? " (#{row["age"]}d)" : "" chase = row["chase"] ? " [chase #{row["chase"]}]" : "" "[#{row["section"]}]#{age}#{chase} #{row["text"]}" end end |
.render_friction(report, rows) ⇒ Object
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 |
# File 'lib/okf/pro/cli.rb', line 474 def render_friction(report, rows) return [ UNAVAILABLE_NOTE ] unless report.available if rows.empty? return [ unreadable_note(report) ] if report.unreadable.positive? return [ "okf pro friction — nothing recorded. Either the verbs covered it, or nothing was written by hand." ] end lines = [ "okf pro friction — #{report.events.size} bundle edit(s) recorded so far that a " \ "command could have done:" ] rows.each do |row| label = "#{row["via"]} #{row["what"]}" lines << " #{label.ljust(16)} #{row["count"]}#{" now: #{row["covered by"]}" if row["covered by"]}" end lines << " #{report.unreadable} recorded line(s) could not be parsed and are not counted above." if report.unreadable.positive? lines << "If one of these should be an `okf pro` verb, please tell the maintainer — " \ "`okf pro friction --issue` prints a ready-to-paste report. It helps more than you think." lines << "The count is cumulative; `okf pro friction --clear` starts it again." lines end |
.resolve_or_complain(start, verb, stderr) ⇒ Object
889 890 891 892 893 894 895 |
# File 'lib/okf/pro/cli.rb', line 889 def resolve_or_complain(start, verb, stderr) root = BundleRoot.resolve(start || Dir.pwd) if root.nil? stderr.puts "okf pro #{verb} — #{File.((start || Dir.pwd).to_s)} holds no OKF bundle." end root end |
.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ Object
160 161 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 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/okf/pro/cli.rb', line 160 def run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr) check = argv.shift.to_s # Asking for help is not an error, so it goes to stdout and exits 0. # Asking for nothing is: `okf pro` alone did nothing anyone requested, # and a 0 there would say it did. return usage(stdout, PASS) if HELP.include?(check) if VERSION_FLAGS.include?(check) stdout.puts VERSION return PASS end return usage(stderr, BLOCK) if check.empty? # Past this line the answer can only be PASS-with-a-check-run or BLOCK, # which is exactly what the marker claims. It is not emitted for the CI # verbs: they are read by a person and a pipeline, not by the wrapper. stderr.puts MARKER if HOOK_NAMES.include?(check) if READERS.include?(check) || WRITERS.include?(check) # Whitelisted before it is sent: the list above is the composition # table, and dispatching off an unvalidated name is how `hook audit` # became a gate that always said fine. return send(check, argv, stdout: stdout, stderr: stderr) end return scaffold(check, argv, stdout: stdout, stderr: stderr) if SCAFFOLD.include?(check) return session_context(stdin, stdout: stdout) if check == "session-context" handler = CHECKS[check] unless handler # An unknown check name is enforcement that did not run. The shell # dispatcher this replaces fell off the end of its `case` and exited 0 # on a typo — silence in the one place the contract names. stderr.puts "ENFORCEMENT MISCONFIGURED — no check named '#{check}'. Known: #{NAMES.join(", ")}." return BLOCK end event = Event.from_stdin(stdin) if event.parse_error? stderr.puts "ENFORCEMENT DEGRADED — '#{check}' could not read its input: #{event.parse_error}. " \ "No check ran, so nothing here has been checked." return BLOCK end # The hook protocol reads every exit but 2 as NON-BLOCKING, so an # exception that escapes a check is not an error report — it is the # edit sailing through while the gate lies on the floor. Refusing on # any crash is the contract's floor: a gate that cannot check must # not wave things through. begin result = handler.call(event) rescue StandardError => e stderr.puts "ENFORCEMENT ERROR — '#{check}' crashed (#{e.class}: #{e.}); " \ "nothing was checked, so the call is refused. A crash that passed " \ "would be indistinguishable from a clean bundle." return BLOCK end return PASS if result.empty? # A check that returns {"ask" => reason} is routing the decision to the # owner instead of refusing: the hook protocol reads exit 0 plus this # JSON as "prompt the user". Interactive, the owner approves or denies # in the moment; unattended, there is no approver and the write fails # closed at the permission layer. Arrays stay refusals, as ever. if result.is_a?(Hash) stdout.puts JSON.generate( "hookSpecificOutput" => { "hookEventName" => "PreToolUse", "permissionDecision" => "ask", "permissionDecisionReason" => result.fetch("ask") } ) return PASS end stderr.puts result.join("\n") BLOCK end |
.scaffold(verb, argv, stdout: $stdout, stderr: $stderr) ⇒ Object
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 |
# File 'lib/okf/pro/cli.rb', line 797 def scaffold(verb, argv, stdout: $stdout, stderr: $stderr) dest = argv.shift unless argv.empty? stderr.puts "okf pro #{verb} — takes one directory, and #{argv.size + 1} were given." return BLOCK end if verb == "skill" if dest.nil? stderr.puts "okf pro skill — needs a destination directory: `okf pro skill .claude/skills/okf-pro`." return BLOCK end else dest ||= Dir.pwd end if dest.to_s.start_with?("@") stderr.puts "okf pro #{verb} — '#{dest}' is a registry ref, and this verb takes a path. " \ "The registry names where a bundle is; this writes a repository around one." return BLOCK end Scaffold.public_send(verb, dest, out: stdout, err: stderr) end |
.section?(text, name) ⇒ Boolean
385 386 387 388 |
# File 'lib/okf/pro/cli.rb', line 385 def section?(text, name) heading = "## #{name}" Board.visible(text).each_line.any? { |line| line.start_with?("## ") && line.chomp.rstrip.casecmp(heading).zero? } end |
.selector_verb(verb, argv, stdout, stderr, missing: SELECTOR_ARGUMENT) ⇒ Object
649 650 651 652 653 654 655 656 657 658 659 660 |
# File 'lib/okf/pro/cli.rb', line 649 def selector_verb(verb, argv, stdout, stderr, missing: SELECTOR_ARGUMENT) answered = writer_flags(argv, verb, stdout, stderr) return answered == :handled ? PASS : BLOCK unless answered == :ok selector = argv.shift if selector.nil? stderr.puts "okf pro #{verb} — #{missing}" return BLOCK end write_verb(verb, argv, stdout, stderr) { |root| yield(root, selector) } end |
.session_context(stdin, stdout: $stdout) ⇒ Object
The one check that does not refuse. Its output is context, not a verdict, and SessionStart has no blocking channel — so it says what it could not do and lets the session start.
900 901 902 903 904 905 906 907 908 909 910 911 |
# File 'lib/okf/pro/cli.rb', line 900 def session_context(stdin, stdout: $stdout) event = Event.from_stdin(stdin) if event.parse_error? stdout.puts "Bundle state unavailable — #{event.parse_error}. " \ "The gates still run; only this banner is blind." return PASS end lines = Closing.session_context(event) stdout.puts lines if lines PASS end |
.shell_quote(text) ⇒ Object
Single-quoted with the one escape a POSIX shell accepts inside them.
The body is assembled from recorded what values, which are this gem's
own vocabulary — but printing a command a reader will paste is exactly
the place not to assume that.
578 579 580 581 582 583 584 |
# File 'lib/okf/pro/cli.rb', line 578 def shell_quote(text) # Block form, and it is load-bearing: in a replacement STRING, `\'` is # gsub's post-match special, so the string form of this substitution # spliced the rest of the text back in after every apostrophe. A block # return is taken literally. "'#{text.to_s.gsub("'") { "'\\''" }}'" end |
.snapshot(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
The checker-first half of derivation: computes the mechanical line for a person to append. It prints and never writes — Agent Drift killed the generator, and this is not one.
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/okf/pro/cli.rb', line 294 def snapshot(argv, stdout: $stdout, stderr: $stderr) = parse_flags(argv, "snapshot", stdout, stderr) return == :handled ? PASS : BLOCK unless .is_a?(Hash) root = bundle_for(argv, "snapshot", stderr) return BLOCK if root.nil? unless File.exist?(File.join(root, "board.md")) stderr.puts "okf pro snapshot — #{root} has no board.md to count." return BLOCK end counters = guarded("snapshot", stderr) { Snapshot.counters(root) } return BLOCK if counters.nil? # The rendered line travels WITH the counters rather than instead of # them. It is what a person appends to `log.md`, and a consumer that # had to re-render it from the twelve numbers would be a second # implementation of the one shape the stop gate verifies. line = Snapshot.render(counters) emit(stdout, , { "line" => line, "counters" => counters }) { line } PASS end |
.state(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
── the readers ──────────────────────────────────────────────────────
344 345 346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/okf/pro/cli.rb', line 344 def state(argv, stdout: $stdout, stderr: $stderr) = parse_flags(argv, "state", stdout, stderr) return == :handled ? PASS : BLOCK unless .is_a?(Hash) root = bundle_with_board(argv, "state", stderr) return BLOCK if root.nil? payload = guarded("state", stderr) { State.call(root, full: [:full]) } return BLOCK if payload.nil? emit(stdout, , payload) { State.render(payload) } PASS end |
.unreadable_note(report) ⇒ Object
Report-only, exit 0 — the family unverified belongs to. It measures
this gem, not the adopter's bundle, and a measurement that gated
something would start being gamed the day someone noticed.
Nothing readable and nothing recorded are different states, and this is
the surface a person meets first. --issue already treats them apart —
it declines to file for one and files for the other — so a default that
called an unparseable log "nothing recorded" made the human output the
one that lied.
468 469 470 471 472 |
# File 'lib/okf/pro/cli.rb', line 468 def unreadable_note(report) "okf pro friction — #{report.unreadable} recorded line(s) will not parse and nothing else " \ "is recorded, so this is a corrupted log rather than a quiet one. Nothing here is a " \ "zero. `okf pro friction --clear` starts the count again." end |
.unverified(argv, stdout: $stdout, stderr: $stderr) ⇒ Object
Report-only, and for a shape of reason worth stating: absent attestation is the truth, and a gate here would pressure toward the one lie the system guards against.
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
# File 'lib/okf/pro/cli.rb', line 321 def unverified(argv, stdout: $stdout, stderr: $stderr) = parse_flags(argv, "unverified", stdout, stderr) return == :handled ? PASS : BLOCK unless .is_a?(Hash) root = bundle_for(argv, "unverified", stderr) return BLOCK if root.nil? rows = guarded("unverified", stderr) { Attestation.rows(root) } return BLOCK if rows.nil? emit(stdout, , rows) do if rows.empty? [ "okf pro unverified — nothing awaits a read." ] else [ "okf pro unverified — #{rows.size} concept(s) awaiting the owner's read " \ "(the state is the truth, not a defect):", *Attestation.render(rows) ] end end PASS end |
.usage(stream, status) ⇒ Object
The command list, plus the two things that surprise people — a ref is not
a path here, and hook does not speak this repo's exit codes. Both cost
something when they surprise someone, which is why they are in the usage
rather than only in the guide.
827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 |
# File 'lib/okf/pro/cli.rb', line 827 def usage(stream, status) width = USAGE.map { |verb, arg, _| "#{verb} #{arg}".length }.max stream.puts "Usage: okf pro <command> [DIR]" stream.puts USAGE.each do |verb, arg, description| stream.puts format(" %-#{width}s %s", "#{verb} #{arg}", description) end stream.puts stream.puts " hook checks: #{HOOK_NAMES.first(5).join(", ")}," stream.puts " #{HOOK_NAMES.drop(5).join(", ")}" stream.puts stream.puts "Flags:" FLAGS.each do |verb, flags| stream.puts format(" %-#{width}s %s", verb, flags.map { |flag| FLAG_HELP.fetch(flag) }.join(" ")) end stream.puts " --pretty implies --json. `audit` and `records` take none — what they answer" stream.puts " with is the exit code. The writers take no flags at all either: their first" stream.puts " argument is content, so `--` is what escapes content that starts with a dash." stream.puts stream.puts "DIR takes a path, not an @slug: the registry names where a bundle is, and a" stream.puts "brain is the repository around one — the hooks, the git hooks, the workflow." stream.puts stream.puts "Exit codes: `hook` speaks the agent hook protocol — 0 pass, 2 block, and never" stream.puts "1, which that protocol reads as non-blocking. Every other command: 0 clean," stream.puts "1 findings, 2 could not run." stream.puts stream.puts "okf pro --version" status end |
.version_answer(stdout) ⇒ Object
757 758 759 760 |
# File 'lib/okf/pro/cli.rb', line 757 def version_answer(stdout) stdout.puts VERSION :handled end |
.which(name) ⇒ Object
586 587 588 589 590 591 |
# File 'lib/okf/pro/cli.rb', line 586 def which(name) ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir| path = File.join(dir, name) File.file?(path) && File.executable?(path) end end |
.write_verb(verb, argv, stdout, stderr) ⇒ Object
The shape every writer shares: resolve, run, print, choose the code.
The write itself never happens here — Writes runs the conservation
guard and refuses before touching the disk, so this method cannot make
a partial write even by getting the ordering wrong.
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 |
# File 'lib/okf/pro/cli.rb', line 698 def write_verb(verb, argv, stdout, stderr) root = bundle_for(argv, verb, stderr) return BLOCK if root.nil? result = guarded(verb, stderr) { yield(root) } return BLOCK if result.nil? if result.ok stdout.puts result. unless result..empty? return PASS end stderr.puts result. BLOCK end |
.writer_flags(argv, verb, stdout, stderr) ⇒ Object
The writers take no flags, and that is precisely why they need this.
Their first positional is CONTENT — the words to capture, the line to
move — so a mistyped or misremembered flag is not an error, it is data:
okf pro capture --help appended - <date> — --help to the Inbox and
exited 0. A verb whose failure mode is committing a garbage board line
must refuse a leading dash rather than swallow it.
--help and --version are answered because every other verb answers
them, and -- is the POSIX escape for the rare legitimate case of
content that really does begin with a dash.
Returns :ok to carry on, :handled when the caller has been answered,
or nil on a refusal already reported.
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 |
# File 'lib/okf/pro/cli.rb', line 676 def writer_flags(argv, verb, stdout, stderr) first = argv.first.to_s return help_answer(stdout) if HELP.include?(first) return version_answer(stdout) if VERSION_FLAGS.include?(first) if first == "--" argv.shift return :ok end return :ok unless first.start_with?("-") stderr.puts "okf pro #{verb} — '#{first}' looks like a flag, and this verb takes none: its " \ "first argument is content, so a flag swallowed here becomes a board line " \ "nobody meant to write. If you really meant to #{verb} something starting with " \ "a dash, put `--` in front of it." nil end |