From 269590a93dbf49d80113f53bd15b9ea1b2f91206 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 3 Nov 2026 21:04:21 +0100 Subject: [PATCH] feat(kvb-org-priority): let the operator reorder the agents' priority rasci.py gains agents, which groups the open tasks by worker in rank order (operator pins first, then WSJF) and prints rank, agent, count and tasks, and order "A,B,...", which checks the names against that list and, under the register lock, pins the listed agents' tasks 1..n in the chosen order, keeping each agent's own task order, and drops the pins of every agent left out so they rank by WSJF after them. The new kvb-org-priority skill shows the current order, offers keep, up to two reasoned suggestions or the operator's own order through Other, applies it and tells the chair and every reordered agent. kvb-org-rasci documents both commands and names order as the one exception to writing only your own rows. test_rasci covers the order, the pins in show, the dropped pins and the usage errors. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++ skills/kvb-org-priority/SKILL.md | 53 +++++++++++++++++++++++++++ skills/kvb-org-rasci/SKILL.md | 4 ++ skills/kvb-org-rasci/scripts/rasci.py | 36 ++++++++++++++++++ tests/test_rasci.py | 23 ++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 skills/kvb-org-priority/SKILL.md diff --git a/README.md b/README.md index cd94016..f7a7144 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ flowchart LR org --> org_wsjf[kvb-org-wsjf] org --> org_robot[kvb-org-robot] org --> org_operator[kvb-org-operator] + org --> org_priority[kvb-org-priority] skill --> skill_tune[kvb-skill-tune] skill --> skill_auto[kvb-skill-auto] skill --> skill_essence[kvb-skill-essence] @@ -157,6 +158,7 @@ flowchart LR wsjf[kvb-org-wsjf] robot[kvb-org-robot] operator[kvb-org-operator] + priority[kvb-org-priority] end subgraph cat_skill [skill] autotune[kvb-skill-tune] @@ -224,6 +226,8 @@ flowchart LR artefacts --> rasci democracy --> wsjf wsjf --> rasci + priority --> rasci + priority -. pins outrank .-> wsjf operator --> rasci dream --> rasci diff --git a/skills/kvb-org-priority/SKILL.md b/skills/kvb-org-priority/SKILL.md new file mode 100644 index 0000000..1a36924 --- /dev/null +++ b/skills/kvb-org-priority/SKILL.md @@ -0,0 +1,53 @@ +--- +name: kvb-org-priority +description: > + Let the operator reorder the agents' priority from a selection; pins their + tasks in that order. Trigger on /kvb-org-priority, "prioritaet", "reihenfolge + der agents". +--- + +The operator's order of the agents outranks every WSJF score. It is written +as `--prio` pins into the `kvb-org-rasci` register, so the chair's grants of +the `kvb-org-wsjf` skill follow it. The script is +`${CLAUDE_SKILL_DIR}/../kvb-org-rasci/scripts/rasci.py`. + +```bash +rasci.py agents +rasci.py order ",,..." +``` + +## Step 1: current order + +`rasci.py agents` prints one line per agent with open tasks, in the +register's rank: number, agent, task count, tasks. Show it as a table with +each agent's status and OKR from `rasci.py show`. + +## Step 2: selection + +One `AskUserQuestion`, `multiSelect: false`, the order of each option in its +`preview`: + +- **Keep the current order**: nothing changes. +- **Suggested order**, at most two, each with its reason in the + description: hotfix lines of the default branch first, then by WSJF; or + blocked agents' unblockers first. +- The operator's own order arrives through `Other`, as agent names or the + numbers of step 1 (map them to names), comma-separated. + +An agent left out of the chosen order loses its pins and ranks by WSJF after +the listed ones; say so in the question. + +## Step 3: apply + +`rasci.py order ",,..."` pins the listed agents' tasks 1..n in that +order, keeping each agent's own task order. It writes other sessions' rows: +the operator decides priority, so this is the one exception to "write only +your own rows". Show `rasci.py agents` again, then tell the resource chair +of the `kvb-org-organize` skill and every reordered agent their new rank, one +message each. + +## Rules + +- Never reorder without the operator's pick; a suggestion is not a decision. +- A name the register does not know stops the step with the list of known + agents; never guess the closest one. diff --git a/skills/kvb-org-rasci/SKILL.md b/skills/kvb-org-rasci/SKILL.md index 3a25b17..3e71c9c 100644 --- a/skills/kvb-org-rasci/SKILL.md +++ b/skills/kvb-org-rasci/SKILL.md @@ -23,6 +23,8 @@ rasci.py artifacts [--ended] [--task ] rasci.py estimate "" --value --complexity rasci.py wsjf [--all] rasci.py tools +rasci.py agents +rasci.py order ",,..." ``` ## Objective and key results @@ -58,6 +60,8 @@ published). `done` is refused while one is open; a task ended short of them is (`--prio`) first, then WSJF from the estimates the chair collects; `show` prints the rank as Prio and the score as WSJF. - Write only your own rows; another session's task belongs to that session. + The one exception is `order`, the operator's pick through the + `kvb-org-priority` skill. - On /kvb-org-rasci print `show` unchanged; its `gaps:` line names tasks without exactly one A, without R, or open without a key result. `get` prints one task in full, key results numbered. diff --git a/skills/kvb-org-rasci/scripts/rasci.py b/skills/kvb-org-rasci/scripts/rasci.py index 5ff9736..b8ce383 100755 --- a/skills/kvb-org-rasci/scripts/rasci.py +++ b/skills/kvb-org-rasci/scripts/rasci.py @@ -213,6 +213,38 @@ def ranked(rows: dict[str, dict]) -> list[str]: return sorted(rows, key=key) +def by_agent(rows: dict[str, dict]) -> dict[str, list[str]]: + agents: dict[str, list[str]] = {} + for task in ranked(rows): + agents.setdefault(rows[task].get("worker") or "-", []).append(task) + return agents + + +def cmd_agents(_: argparse.Namespace) -> int: + for rank, (agent, tasks) in enumerate(by_agent(merged(include_done=False)).items(), 1): + print(f"{rank}\t{agent}\t{len(tasks)}\t{', '.join(tasks)}") + return 0 + + +def cmd_order(args: argparse.Namespace) -> int: + names = [name.strip() for name in args.agents.split(",") if name.strip()] + agents = by_agent(merged(include_done=False)) + unknown = [name for name in names if name not in agents] + if not names or unknown or len(set(names)) != len(names): + fail(f"order needs distinct agents from `agents`; unknown: {', '.join(unknown) or '-'}") + with database() as tasks: + prio = 0 + for name in names: + for task in agents[name]: + prio += 1 + tasks.setdefault(task, {})["prio"] = prio + for name in agents.keys() - set(names): + for task in agents[name]: + tasks.get(task, {}).pop("prio", None) + print(f"pinned {prio} tasks of {len(names)} agents; the others rank by WSJF after them") + return 0 + + def okr(row: dict) -> str: results = row.get("key_results", []) text = cell(row.get("objective") or "-", 40) @@ -489,6 +521,10 @@ def main() -> int: p.add_argument("--task") p.set_defaults(func=cmd_artifacts) sub.add_parser("tools").set_defaults(func=cmd_tools) + sub.add_parser("agents").set_defaults(func=cmd_agents) + p = sub.add_parser("order") + p.add_argument("agents") + p.set_defaults(func=cmd_order) args = parser.parse_args() return args.func(args) diff --git a/tests/test_rasci.py b/tests/test_rasci.py index e634fc7..cb1af7b 100644 --- a/tests/test_rasci.py +++ b/tests/test_rasci.py @@ -237,6 +237,29 @@ class TestRasci(unittest.TestCase): self._run("estimate", "work/b", "One", "--drop") self.assertFalse(any(line.startswith("estimate ") for line in self._out("get", "work/b"))) + def test_order_pins_the_agents_in_the_operators_order(self): + self._run("set", "work/a1", "--worker", "Alpha", "--prio", "1") + self._run("set", "work/a2", "--worker", "Alpha", "--prio", "2") + self._run("set", "work/b1", "--worker", "Beta", "--prio", "3") + self._run("set", "work/c1", "--worker", "Gamma") + self._run("estimate", "work/c1", "One", "--value", "8", "--complexity", "2") + self.assertEqual( + self._out("agents"), + ["1\tAlpha\t2\twork/a1, work/a2", "2\tBeta\t1\twork/b1", "3\tGamma\t1\twork/c1"], + ) + self.assertEqual( + self._out("order", "Gamma, Beta"), ["pinned 2 tasks of 2 agents; the others rank by WSJF after them"] + ) + self.assertEqual( + self._out("agents"), + ["1\tGamma\t1\twork/c1", "2\tBeta\t1\twork/b1", "3\tAlpha\t2\twork/a1, work/a2"], + ) + self.assertTrue(self._out("show")[2].startswith("| 1 | 4 | work/c1 |")) + self.assertNotIn("prio: 1", self._out("get", "work/a1")) + for args in (("order", "Gamma,Nobody"), ("order", "Beta,Beta"), ("order", " , ")): + with self.subTest(args=args): + self.assertEqual(self._run(*args).returncode, 2) + class TestOrgSkillsUseTheRegister(unittest.TestCase): def test_every_org_skill_names_the_register(self):