module Backlog # Pure decision for the triage-trigger SessionStart hook (card # 1818330518584820864): given the label arrays already sitting on a # board's Backlog-column cards, does the hook need to instruct dispatch # of the card-triage agent? Network/board-fetch stays in the hook # script (hooks/triage_check.rb) — this class only classifies + # composes the note text, so the decision is unit-testable without # Planka. # # "Unlabeled" mirrors card-triage's own definition (agents/card-triage.md): # a card is raw if it is missing a priority label (P0-P3) OR an # autonomy label (hitl/semi/afk-ready) — not just totally bare. class TriageCheck PRIORITY_LABELS = %w[P0 P1 P2 P3].freeze AUTONOMY_LABELS = %w[hitl semi afk-ready].freeze # Which board should this session's triage check inspect? Mirrors the # CLI's config semantics (the session cwd's own .cc-os/config wins — # umbrella subdir configs override the repo root's), falling back to # the root config, then to the path heuristic. Honors both the # explicit `board` key and `tracker=planka:` (via # Config#planka_board). Pure: the hook script reads the files and # passes contents (or nil) in. # # @param cwd [String] the session's working directory # @param root [String] the enclosing git toplevel (== cwd outside umbrellas) # @param cwd_config [String, nil] contents of /.cc-os/config # @param root_config [String, nil] contents of /.cc-os/config # @return [String, nil] board name, or nil (don't guess) when nothing # is configured and the path heuristic doesn't recognize the path def self.board_name_for(cwd:, root:, cwd_config:, root_config:) config_home = cwd_config ? cwd : root config = Config.new(cwd_config || root_config) return config.planka_board if config.planka_board return nil unless Resolver.project_for(config_home) File.basename(config_home) end # @param labels [Array] the label names on one card # @return [Boolean] def self.needs_triage?(labels) (labels & PRIORITY_LABELS).empty? || (labels & AUTONOMY_LABELS).empty? end # @param backlog_cards_labels [Array>] label arrays, # one per card currently in the board's Backlog list # @return [Array>] the subset needing triage def self.unlabeled(backlog_cards_labels) backlog_cards_labels.select { |labels| needs_triage?(labels) } end # @param board_name [String] # @param backlog_cards_labels [Array>] # @return [String, nil] the SessionStart additionalContext note, or # nil when there is nothing to triage def self.note_for(board_name, backlog_cards_labels) count = unlabeled(backlog_cards_labels).size return nil if count.zero? card_word = count == 1 ? "card" : "cards" "[os-backlog] #{count} unlabeled Backlog #{card_word} on board #{board_name} -> " \ "dispatch the card-triage agent now (model: haiku, run in background) to label " \ "them. Labeling is this project's job for its own board (a parent umbrella may " \ "also label its child boards) — never label cards on a board that belongs to a " \ "different project." end end end