diff --git a/plugins/os-backlog/hooks/hooks.json b/plugins/os-backlog/hooks/hooks.json index cdb73ee..c92ebf1 100644 --- a/plugins/os-backlog/hooks/hooks.json +++ b/plugins/os-backlog/hooks/hooks.json @@ -8,6 +8,11 @@ "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py", "timeout": 5 + }, + { + "type": "command", + "command": "ruby ${CLAUDE_PLUGIN_ROOT}/hooks/triage_check.rb", + "timeout": 8 } ] } diff --git a/plugins/os-backlog/hooks/session_start.py b/plugins/os-backlog/hooks/session_start.py index 1011d1f..6eca611 100644 --- a/plugins/os-backlog/hooks/session_start.py +++ b/plugins/os-backlog/hooks/session_start.py @@ -7,6 +7,15 @@ Deliberately contains ZERO board state and no briefs (notification policy v2: pull beats push — board state is fetched on demand via /os-backlog:list, never pushed into a session). +Sibling hook triage_check.rb (card 1818330518584820864) is a narrow, sanctioned +exception to that policy: it makes one bounded Planka read to decide whether to +push a single mechanical dispatch directive — not a brief, not board contents — +when the current project's own Backlog has unlabeled cards. The only board- +derived fact it ever surfaces is the unlabeled count that justifies dispatching +card-triage; it never lists card titles or otherwise renders board state. Kept +as a separate script precisely so this file can stay a pure, network-free +function of filesystem state. + git project -> additionalContext note (rules only, near-zero tokens) not a git repo -> silent (process rules only apply inside real projects) diff --git a/plugins/os-backlog/lib/backlog/board_ensurer.rb b/plugins/os-backlog/lib/backlog/board_ensurer.rb index 83b8ca7..d1c6558 100644 --- a/plugins/os-backlog/lib/backlog/board_ensurer.rb +++ b/plugins/os-backlog/lib/backlog/board_ensurer.rb @@ -42,7 +42,8 @@ module Backlog # @param name [String] board name (never an archived---prefixed name; # this method only ever targets the active board) - # @param project_name [String] "Dev" or "Clients" (or any project name) + # @param project_name [String] any Planka project name — created on demand + # if it doesn't already exist # @return [Result] def ensure(name, project_name:) raise ArgumentError, "board name must not use the archived-- prefix" if BoardSpec.archived?(name) diff --git a/plugins/os-backlog/tests/board_ensurer_test.rb b/plugins/os-backlog/tests/board_ensurer_test.rb index a46cb88..eba50e6 100644 --- a/plugins/os-backlog/tests/board_ensurer_test.rb +++ b/plugins/os-backlog/tests/board_ensurer_test.rb @@ -120,6 +120,18 @@ class BoardEnsurerTest < Minitest::Test refute_nil archived end + def test_ensure_creates_an_arbitrary_project_name_on_demand + client = FakePlankaClient.new + ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1") + + result = ensurer.ensure("ovh-prod", project_name: "Servers") + + assert result.created + project = client.projects_store.first + assert_equal "Servers", project.name + assert_equal "Servers", result.project.name + end + def test_ensure_rejects_archived_prefixed_name client = FakePlankaClient.new ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1") diff --git a/plugins/os-backlog/tests/resolver_test.rb b/plugins/os-backlog/tests/resolver_test.rb index fc11a93..b26ff59 100644 --- a/plugins/os-backlog/tests/resolver_test.rb +++ b/plugins/os-backlog/tests/resolver_test.rb @@ -90,4 +90,22 @@ class ResolverTest < Minitest::Test assert_equal "use llf-schema", resolver.resolve_string end + + def test_unconfigured_servers_repo_uses_board_named_after_repo + resolver = Backlog::Resolver.new( + repo_path: "/home/jared/servers/proxmox-ubuntu", + config_contents: nil, + boards: %w[proxmox-ubuntu] + ) + + assert_equal "use proxmox-ubuntu", resolver.resolve_string + end + + def test_project_for_returns_dev_clients_servers_or_nil + assert_equal "Dev", Backlog::Resolver.project_for("/home/jared/dev/some-repo") + assert_equal "Clients", Backlog::Resolver.project_for("/home/jared/clients/acme-co") + assert_equal "Servers", Backlog::Resolver.project_for("/home/jared/servers/ovh-prod") + assert_equal "Servers", Backlog::Resolver.project_for("/home/jared/servers") + assert_nil Backlog::Resolver.project_for("/home/jared/scratch/some-repo") + end end diff --git a/plugins/os-backlog/tests/triage_check_test.rb b/plugins/os-backlog/tests/triage_check_test.rb new file mode 100644 index 0000000..583909d --- /dev/null +++ b/plugins/os-backlog/tests/triage_check_test.rb @@ -0,0 +1,41 @@ +require_relative "test_helper" + +class TriageCheckTest < Minitest::Test + def test_card_missing_both_labels_needs_triage + assert Backlog::TriageCheck.needs_triage?([]) + end + + def test_card_missing_priority_label_needs_triage + assert Backlog::TriageCheck.needs_triage?(["semi"]) + end + + def test_card_missing_autonomy_label_needs_triage + assert Backlog::TriageCheck.needs_triage?(["P1"]) + end + + def test_fully_labeled_card_does_not_need_triage + refute Backlog::TriageCheck.needs_triage?(%w[P1 semi]) + end + + def test_note_for_no_unlabeled_cards_is_nil + assert_nil Backlog::TriageCheck.note_for("cc-os", [%w[P1 semi], %w[P0 hitl]]) + end + + def test_note_for_counts_only_unlabeled_cards + labels = [%w[P1 semi], [], ["P2"]] + note = Backlog::TriageCheck.note_for("cc-os", labels) + + refute_nil note + assert_includes note, "2 unlabeled Backlog cards on board cc-os" + assert_includes note, "card-triage" + assert_includes note, "haiku" + assert_includes note, "background" + end + + def test_note_for_singular_card_uses_singular_wording + note = Backlog::TriageCheck.note_for("cc-os", [[]]) + + assert_includes note, "1 unlabeled Backlog card on board cc-os" + refute_includes note, "1 unlabeled Backlog cards" + end +end