os-backlog: card-triage + board-audit named agents (refs #15)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XknQRvihHDpYE47RTmUR4N
This commit is contained in:
parent
3bcf3a1b48
commit
27f8e5f2b2
|
|
@ -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 <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 <id> --text "board-audit <date>: <finding, one sentence>"
|
||||
```
|
||||
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.
|
||||
|
|
@ -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 <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 <board> --card <id> --label <P0|P1|P2|P3>
|
||||
${CLAUDE_PLUGIN_ROOT}/bin/os-backlog card-label --board <board> --card <id> --label <hitl|semi|afk-ready>
|
||||
```
|
||||
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.
|
||||
|
|
@ -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 <command> [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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue