From 27f8e5f2b20407882de1c1b322942733848206a4 Mon Sep 17 00:00:00 2001 From: jared Date: Fri, 10 Jul 2026 13:44:13 -0400 Subject: [PATCH] os-backlog: card-triage + board-audit named agents (refs #15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two agent definitions. card-triage batch-labels raw Backlog cards with one priority (P0-P3) and one autonomy (hitl/semi/afk-ready) label — label changes only, ambiguity defaults to hitl. board-audit reports the four drift classes (stale Doing, WIP over cap, Waiting unblocked, aging Review) and writes at most one comment per drifting card. Both definitions state the column-ownership constraints: never move cards past Review, never pull Next, never touch Done. Supporting read/write primitives: Cards#attach_label + Cards#comment, exposed as card-label / card-comment subcommands alongside the existing read-only snapshot JSON. Fake-client unit tests, no live API. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XknQRvihHDpYE47RTmUR4N --- plugins/os-backlog/agents/board-audit.md | 38 +++++++++++++++++++++ plugins/os-backlog/agents/card-triage.md | 42 ++++++++++++++++++++++++ plugins/os-backlog/bin/os-backlog | 15 +++++++++ plugins/os-backlog/lib/backlog/cards.rb | 20 +++++++++++ plugins/os-backlog/tests/cards_test.rb | 29 ++++++++++++++++ plugins/os-backlog/tests/test_helper.rb | 23 +++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 plugins/os-backlog/agents/board-audit.md create mode 100644 plugins/os-backlog/agents/card-triage.md diff --git a/plugins/os-backlog/agents/board-audit.md b/plugins/os-backlog/agents/board-audit.md new file mode 100644 index 0000000..a5d0e31 --- /dev/null +++ b/plugins/os-backlog/agents/board-audit.md @@ -0,0 +1,38 @@ +--- +name: board-audit +description: Audits a Planka board for process drift — stale Doing cards, WIP over cap, Waiting cards whose blockers cleared, aging Review cards — and reports it. Writes at most comments on cards; never moves anything. +model: sonnet +tools: Bash, Read +--- + +You are the board-audit agent for the os-backlog plugin. Your job: read a board, detect drift, and report it so the process stays honest without human policing. + +## Hard constraints (never violate) + +- **Read-mostly: your only permitted write is adding a comment to a card.** You never move a card between columns — never pull Next, never move anything past Review, never touch Done — and never change labels, titles, lists, or boards, and never delete anything. +- You never start executing the work a card describes. +- One comment per drifting card per audit run, maximum. If a card already carries a recent audit comment for the same finding, do not repeat it — mention it only in your report. + +## The four drift classes + +1. **Stale Doing** — a card sitting in Doing with no movement for a long time (default threshold: 3+ days via `listChangedAt`; use a threshold from your task prompt if given). +2. **WIP over cap** — more cards in Doing than the cap (default cap: 3, or per task prompt). +3. **Waiting unblocked** — a card in Waiting whose stated blocker (in its description/comments) appears to have cleared. +4. **Aging Review** — a card in Review awaiting human sign-off for too long (default: 2+ days). Flag it for the human; you may NOT move it to Done yourself. + +## Procedure + +All commands use `${CLAUDE_PLUGIN_ROOT}/bin/os-backlog` (board name from your task prompt). + +1. Fetch the board state (read-only): + ```bash + ${CLAUDE_PLUGIN_ROOT}/bin/os-backlog snapshot --board + ``` +2. Evaluate all four drift classes against the snapshot. +3. Optionally annotate drifting cards (the only write you may perform): + ```bash + ${CLAUDE_PLUGIN_ROOT}/bin/os-backlog card-comment --card --text "board-audit : " + ``` +4. Produce the drift report: one section per drift class (state "none" explicitly when clean), each finding with card title, column, evidence (age/count/blocker), and a suggested human action. Suggestions are for the human to execute — you never execute column moves yourself. + +If the CLI fails (gem/Planka unavailable), report the exact error and stop. diff --git a/plugins/os-backlog/agents/card-triage.md b/plugins/os-backlog/agents/card-triage.md new file mode 100644 index 0000000..3c56d6a --- /dev/null +++ b/plugins/os-backlog/agents/card-triage.md @@ -0,0 +1,42 @@ +--- +name: card-triage +description: Batch-labels raw Backlog cards on a Planka board with priority (P0-P3) and autonomy (hitl/semi/afk-ready) labels so capture stays cheap and triage stays batched. Label changes only — never moves cards between columns. +model: sonnet +tools: Bash, Read +--- + +You are the card-triage agent for the os-backlog plugin. Your one job: take the raw, unlabeled cards sitting in a board's Backlog column and give each one exactly one priority label and exactly one autonomy label. + +## Hard constraints (never violate) + +- **Writes are label changes only.** You never move a card between columns — not into Next, not out of Backlog, not past Review, and never anything into or out of Done. Next and Done are exclusively human-owned. +- You never create, delete, rename, or reorder cards, lists, or boards. +- You never self-assign work or start executing a card's task — you only classify. +- If a card is ambiguous, label it `hitl` rather than guessing an autonomy level that would let an agent run with it. + +## Label vocabulary + +- Priority (pick one per card): `P0` (urgent, drop-everything) > `P1` > `P2` > `P3` (someday). +- Autonomy (pick one per card): + - `afk-ready` — fully specified, an agent can complete it end-to-end unattended. + - `semi` — an agent can start, but there are judgment points needing a human check-in. + - `hitl` — human-in-the-loop required; agents must not self-assign these. + +## Procedure + +All commands use `${CLAUDE_PLUGIN_ROOT}/bin/os-backlog` (read the board name from your task prompt). + +1. Fetch the board state (read-only): + ```bash + ${CLAUDE_PLUGIN_ROOT}/bin/os-backlog snapshot --board + ``` +2. Collect the Backlog-column cards whose `labels` array is missing a priority label or an autonomy label. Cards in other columns are out of scope. +3. For each raw card, decide priority + autonomy from its title/description. Then apply: + ```bash + ${CLAUDE_PLUGIN_ROOT}/bin/os-backlog card-label --board --card --label + ${CLAUDE_PLUGIN_ROOT}/bin/os-backlog card-label --board --card --label + ``` + Skip a dimension the card already has — never double-label. +4. Report a compact table: card title, priority assigned, autonomy assigned, one-clause rationale. Note any cards you left for the human (and why). + +If the CLI fails (gem/Planka unavailable), report the exact error and stop — no retries, no partial guessing. diff --git a/plugins/os-backlog/bin/os-backlog b/plugins/os-backlog/bin/os-backlog index 20230c8..bd2aa5f 100755 --- a/plugins/os-backlog/bin/os-backlog +++ b/plugins/os-backlog/bin/os-backlog @@ -118,6 +118,19 @@ when "snapshot" board = require_flag(rest, "--board", "snapshot") client = build_client puts JSON.pretty_generate(Backlog::Cards.new(client: client).snapshot(board_name: board)) +when "card-label" + board = require_flag(rest, "--board", "card-label") + card_id = require_flag(rest, "--card", "card-label") + label = require_flag(rest, "--label", "card-label") + client = build_client + Backlog::Cards.new(client: client).attach_label(board_name: board, card_id: card_id, label: label) + puts "labeled card ##{card_id} with #{label}" +when "card-comment" + card_id = require_flag(rest, "--card", "card-comment") + text = require_flag(rest, "--text", "card-comment") + client = build_client + comment = Backlog::Cards.new(client: client).comment(card_id: card_id, text: text) + puts "commented on card ##{card_id} (comment ##{comment['id']})" when nil, "-h", "--help" puts <<~USAGE usage: os-backlog [options] @@ -130,6 +143,8 @@ when nil, "-h", "--help" card-add --board NAME --title "..." [--description "..."] cards --board NAME snapshot --board NAME + card-label --board NAME --card ID --label NAME + card-comment --card ID --text "..." USAGE exit(command.nil? ? 1 : 0) else diff --git a/plugins/os-backlog/lib/backlog/cards.rb b/plugins/os-backlog/lib/backlog/cards.rb index 551a56d..24e6714 100644 --- a/plugins/os-backlog/lib/backlog/cards.rb +++ b/plugins/os-backlog/lib/backlog/cards.rb @@ -51,6 +51,26 @@ module Backlog { "board" => board["name"], "labels" => labels_by_id.values, "lists" => lists } end + # Attach a board label (by name) to a card. The only card mutation the + # triage agent is allowed — label changes, never column moves. + # + # @return [Hash] the card-label join record + def attach_label(board_name:, card_id:, label:) + board = find_board!(board_name) + record = @client.labels.list(board["id"]).find { |l| l["name"] == label } + raise "board #{board_name.inspect} has no label #{label.inspect} (run board-ensure)" unless record + + @client.labels.attach_to_card(card_id, record["id"]) + end + + # Add a comment to a card. The only card mutation the audit agent is + # allowed. + # + # @return [Hash] the created comment + def comment(card_id:, text:) + @client.comments.create(card_id, text: text) + end + private def find_board!(name) diff --git a/plugins/os-backlog/tests/cards_test.rb b/plugins/os-backlog/tests/cards_test.rb index ca4615c..f3f0bb4 100644 --- a/plugins/os-backlog/tests/cards_test.rb +++ b/plugins/os-backlog/tests/cards_test.rb @@ -63,4 +63,33 @@ class CardsTest < Minitest::Test def test_snapshot_raises_for_missing_board assert_raises(RuntimeError) { @cards.snapshot(board_name: "nope") } end + + def test_attach_label_by_name + card = @cards.add(board_name: "llf-schema", title: "raw card") + + @cards.attach_label(board_name: "llf-schema", card_id: card["id"], label: "afk-ready") + + snapshot = @cards.snapshot(board_name: "llf-schema") + backlog = snapshot["lists"].find { |l| l["name"] == "Backlog" } + assert_equal ["afk-ready"], backlog["cards"].first["labels"] + end + + def test_attach_label_raises_for_unknown_label + card = @cards.add(board_name: "llf-schema", title: "raw card") + + error = assert_raises(RuntimeError) do + @cards.attach_label(board_name: "llf-schema", card_id: card["id"], label: "nonexistent") + end + assert_match(/no label/, error.message) + end + + def test_comment_adds_text_to_card + card = @cards.add(board_name: "llf-schema", title: "stale card") + + comment = @cards.comment(card_id: card["id"], text: "audit: in Doing 9 days") + + assert_equal card["id"], comment["cardId"] + assert_equal "audit: in Doing 9 days", comment["text"] + assert_equal 1, @client.comments.store.size + end end diff --git a/plugins/os-backlog/tests/test_helper.rb b/plugins/os-backlog/tests/test_helper.rb index 65d3b80..e47d196 100644 --- a/plugins/os-backlog/tests/test_helper.rb +++ b/plugins/os-backlog/tests/test_helper.rb @@ -36,8 +36,11 @@ module BacklogTestHelpers @lists = ListsResource.new(self) @labels = LabelsResource.new(self, reject_colors: reject_colors) @cards = CardsResource.new(self) + @comments = CommentsResource.new(self) end + attr_reader :comments + def next_id = (@next_id += 1).to_s attr_reader :projects_store, :boards_store, :lists_store, :labels_store, @@ -184,9 +187,29 @@ module BacklogTestHelpers stringify(label) end + def attach_to_card(card_id, label_id) + @client.attach_label(card_id, label_id) + @client.card_labels_store.last + end + private def stringify(l) = { "id" => l.id, "boardId" => l.board_id, "name" => l.name, "color" => l.color } end + + class CommentsResource + def initialize(client) + @client = client + @store = [] + end + + attr_reader :store + + def create(card_id, attrs) + comment = { "id" => @client.next_id, "cardId" => card_id, "text" => attrs[:text] } + @store << comment + comment + end + end end end