92 lines
2.9 KiB
Ruby
92 lines
2.9 KiB
Ruby
|
|
#!/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 board-name convention capture/route already use: an explicit
|
||
|
|
# .cc-os/config `board` key, else — only inside a path the project
|
||
|
|
# heuristic actually recognizes — the repo directory's own name. Outside
|
||
|
|
# any known root with no explicit config, we don't guess (mirrors the
|
||
|
|
# resolver's stop-and-discuss ambiguity gate).
|
||
|
|
def resolve_board_name(root)
|
||
|
|
config_path = File.join(root, ".cc-os", "config")
|
||
|
|
config = Backlog::Config.new(File.exist?(config_path) ? File.read(config_path) : nil)
|
||
|
|
return config.board if config.board
|
||
|
|
return nil unless Backlog::Resolver.project_for(root)
|
||
|
|
|
||
|
|
File.basename(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 # rubocop:disable Lint/RescueException -- a hook must never block a session
|
||
|
|
nil
|
||
|
|
end
|