#!/usr/bin/env ruby # frozen_string_literal: true # # triage_check.rb — SessionStart hook (card 1818330518584820864): checks # whether the CURRENT project's own resolved board has unlabeled Backlog # cards and, if so, emits additionalContext instructing dispatch of the # card-triage agent (haiku, background). # # Design intent: labeling is the RESPONSIBLE project's job, not capture's # and not whichever project happened to file a card cross-project. This # hook only ever looks at the board the CURRENT session's project resolves # to (the same board /os-backlog:capture would file onto from here) — a # session run from the owning project, or a parent umbrella, naturally # only ever triggers triage for a board it owns; it never inspects or # labels a board belonging to a different project. # # This is deliberately separate from session_start.py (which stays a pure, # network-free function of filesystem state per its own docstring): this # script does one bounded Planka read to decide whether to fire, then # hands off the actual labeling to the card-triage agent — it never # labels anything itself. # # Fails soft everywhere: no git project, no board name derivable, no # network/credentials, any Planka error, or a board that doesn't exist yet # -> silent, exit 0. Never blocks a session. require "json" require_relative "../lib/backlog" def find_project_root(cwd) dir = File.expand_path(cwd) loop do return dir if Dir.exist?(File.join(dir, ".git")) parent = File.dirname(dir) return nil if parent == dir dir = parent end end # Same config semantics as the CLI: the session cwd's own .cc-os/config # wins (umbrella subdir configs override the repo root's for sessions # started there), then the root config, then the path heuristic — and # `tracker=planka:` counts, not just the explicit `board` key. # The decision itself is pure (TriageCheck.board_name_for); this only # reads the two candidate config files. def resolve_board_name(root) read = lambda do |dir| path = File.join(dir, ".cc-os", "config") File.exist?(path) ? File.read(path) : nil end cwd = File.expand_path(Dir.pwd) Backlog::TriageCheck.board_name_for(cwd: cwd, root: root, cwd_config: read.call(cwd), root_config: read.call(root)) end def build_client require "planka_api" client = Planka::Client.new client.login client end def main root = find_project_root(Dir.pwd) return unless root board_name = resolve_board_name(root) return unless board_name client = build_client snapshot = Backlog::Cards.new(client: client).snapshot(board_name: board_name) backlog_list = snapshot["lists"].find { |l| l["name"] == "Backlog" } return unless backlog_list labels = backlog_list["cards"].map { |c| c["labels"] } note = Backlog::TriageCheck.note_for(board_name, labels) return unless note puts JSON.generate( hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: note } ) end begin main rescue Exception => e # rubocop:disable Lint/RescueException -- a hook must never block a session warn "os-backlog triage_check: #{e.class}: #{e.message}" if ENV["CC_OS_DEBUG"] end