2026-07-13 14:48:19 +00:00
|
|
|
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
|
|
|
|
|
|
2026-07-13 16:27:43 +00:00
|
|
|
# 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:<board>` (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 <cwd>/.cc-os/config
|
|
|
|
|
# @param root_config [String, nil] contents of <root>/.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
|
|
|
|
|
|
2026-07-13 14:48:19 +00:00
|
|
|
# @param labels [Array<String>] 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<Array<String>>] label arrays,
|
|
|
|
|
# one per card currently in the board's Backlog list
|
|
|
|
|
# @return [Array<Array<String>>] 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<Array<String>>]
|
|
|
|
|
# @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
|