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):