feat(org): rank the register by WSJF from Fibonacci estimates
🧪 Test / 🧪 Lock + lint (push) Waiting to run

kvb-org-wsjf: the chair asks its subordinates for Fibonacci estimates of business value and complexity, rasci.py estimate records them per party, and WSJF = median value / median complexity ranks the tasks. rasci.py wsjf prints the WSJF table with OKR and the task id; the register's show takes its Prio from that rank (an operator --prio pin first) and adds a WSJF column. Four descriptions were tightened to keep the listing budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-01 01:49:03 +01:00
1 parent 35dc37641d
commit 5a93b0e5a9
10 files changed
+181 -27

No files matched your search

+4
View File
@@ -59,6 +59,7 @@ flowchart LR
org --> org_matrix[kvb-org-matrix]
org --> org_rasci[kvb-org-rasci]
org --> org_artefacts[kvb-org-artefacts]
org --> org_wsjf[kvb-org-wsjf]
org --> org_robot[kvb-org-robot]
skills --> skills_dream[kvb-skills-dream]
skills --> skills_tune[kvb-skills-tune]
@@ -128,6 +129,7 @@ flowchart LR
status_matrix[kvb-org-matrix]
rasci[kvb-org-rasci]
artefacts[kvb-org-artefacts]
wsjf[kvb-org-wsjf]
robot[kvb-org-robot]
end
subgraph cat_skills [skills]
@@ -176,6 +178,8 @@ flowchart LR
status_matrix --> rasci
cleanup --> artefacts
artefacts --> rasci
democracy --> wsjf
wsjf --> rasci
dream --> rasci
observe --> triage
+2 -2
View File
@@ -2,8 +2,8 @@
name: kvb-code-test
description: >
Discover the Makefile's test targets, let the operator pick, run and report;
fix mode loops until green. Trigger on /kvb-code-test [--fix], or when the
operator asks to run the tests, make them pass, or get the suite green.
fix mode loops until green. Trigger on /kvb-code-test [--fix], or asked to
run the tests, make them pass, or get the suite green.
---
The Makefile is the source of truth for how this project runs tests. Never
+1 -1
View File
@@ -3,7 +3,7 @@ name: kvb-git-commit-next
description: >
Check every branch for the staged fix, commit the staged bundle with
--no-verify, then stage and brief the next bundle. Trigger on /kvb-git-commit-next
or when the operator asks to commit and stage the next bundle in one step.
or a request to commit and stage the next bundle in one step.
---
1. `git fetch --all --prune --tags`, then hunt the staged fix across every
+3 -3
View File
@@ -76,9 +76,9 @@ records the result.
- Before starting anything that consumes shared capacity (gates, deploys,
iteration loops, swarm or cloud resources, cleanup of shared data), an agent
sends the chair what, where, for how long and its expected disk need, waits
for the grant, and reports the release. The chair keeps one queue as the
priorities in the `kvb-org-rasci` register, answers every request with its
position, enforces the limits the operator set,
for the grant, and reports the release. The chair keeps one queue, ordered
by the WSJF rank of the `kvb-org-wsjf` skill (it asks its subordinates for
the Fibonacci estimates), answers every request with its position, enforces the limits the operator set,
checks every grant with `watch.py admit --need-gb <N>` (or `--kind <slot>`;
test kinds also need CPU and RAM below the configured limits).
- A running job (deploy, gate, test, swarm run, build) is never cut short to
+2 -2
View File
@@ -1,8 +1,8 @@
---
name: kvb-org-perform
description: >
Pick `model` and `effort` per subagent from a fixed table: cheaper only on a
clear match at 90 % that the result equals the default's; log spawns and
Pick `model` and `effort` per subagent from a fixed table: cheaper only at
90 % confidence that the result equals the default's; log spawns and
quality. Apply AUTOMATICALLY before every Agent tool call.
---
+5 -2
View File
@@ -20,6 +20,8 @@ rasci.py get <task>
rasci.py rm <task>
rasci.py show [--all]
rasci.py artifacts [--ended] [--task <task>]
rasci.py estimate <task> "<party>" --value <fib> --complexity <fib>
rasci.py wsjf [--all]
rasci.py tools
```
@@ -52,8 +54,9 @@ published). `done` is refused while one is open; a task ended short of them is
- Artifacts share this table and follow the `kvb-org-artefacts` skill:
record each one when it appears, drop it once deleted. Add every script or
tool the task ran (`--tool`): `tools` ranks them for `kvb-skills-dream`.
- Priority: the operator's order first, then the chair's queue; the chair
writes it.
- Priority is the rank of the `kvb-org-wsjf` skill: an operator's pin
(`--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.
- 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`
+92 -9
View File
@@ -20,6 +20,7 @@ import importlib.util
import json
import os
import re
import statistics
import sys
import tempfile
from collections.abc import Iterator
@@ -41,6 +42,7 @@ FIELDS = (
TEXT_FIELDS = ("title", "objective", "description", "worker", "workdir", "branch")
LIST_FIELDS = {"tools": "tool"}
ARTIFACT = re.compile(r"^[a-z]+:\S")
FIBONACCI = (1, 2, 3, 5, 8, 13, 21, 34, 55, 89)
OPERATOR = "Operator"
@@ -192,6 +194,33 @@ def when(value: str | None) -> str:
return datetime.datetime.fromisoformat(value).astimezone().strftime("%m-%d %H:%M")
def wsjf(row: dict) -> tuple[float, float, float] | None:
estimates = list(row.get("estimates", {}).values())
if not estimates:
return None
value = statistics.median(e["value"] for e in estimates)
complexity = statistics.median(e["complexity"] for e in estimates)
return value, complexity, round(value / complexity, 2)
def ranked(rows: dict[str, dict]) -> list[str]:
def key(task: str) -> tuple:
row = rows[task]
score = wsjf(row)
pinned = row.get("prio")
return (pinned is None, pinned or 0, score is None, -(score[2] if score else 0), task)
return sorted(rows, key=key)
def okr(row: dict) -> str:
results = row.get("key_results", [])
text = cell(row.get("objective") or "-", 40)
if results:
text += f" ({sum(kr['met'] for kr in results)}/{len(results)} KR)"
return text
def gaps(task: str, row: dict) -> list[str]:
found = []
roles = row["roles"]
@@ -297,6 +326,53 @@ def cmd_get(args: argparse.Namespace) -> int:
print(f"{flag}: {item}")
roles = ", ".join(f"{p}={v}" for p, v in sorted(row["roles"].items()))
print(f"roles: {roles or '-'}")
for party, estimate in sorted(row.get("estimates", {}).items()):
print(f"estimate {party}: business value {estimate['value']}, complexity {estimate['complexity']}")
score = wsjf(row)
if score:
print(f"wsjf: {score[2]:g}")
return 0
def cmd_estimate(args: argparse.Namespace) -> int:
check_task(args.task)
if not args.drop:
for name in ("value", "complexity"):
if getattr(args, name) not in FIBONACCI:
fail(f"--{name} must be one of {', '.join(map(str, FIBONACCI))}")
with database() as tasks:
entry = tasks.setdefault(args.task, {"roles": {}})
estimates = entry.setdefault("estimates", {})
if args.drop:
estimates.pop(args.party, None)
else:
estimates[args.party] = {"value": args.value, "complexity": args.complexity, "at": stamp()}
if not estimates:
entry.pop("estimates")
print(f"{args.task}: estimate {'dropped' if args.drop else 'recorded'} for {args.party}")
return 0
def cmd_wsjf(args: argparse.Namespace) -> int:
rows = merged(args.all)
if not rows:
print("no tasks")
return 0
header = ["Rank", "Task", "OKR", "Business value", "Complexity", "WSJF", "Estimators", "Status"]
print("| " + " | ".join(header) + " |")
print("|" + "---|" * len(header))
unestimated = []
for rank, task in enumerate(ranked(rows), 1):
row = rows[task]
score = wsjf(row)
if score is None:
unestimated.append(task)
figures = [f"{n:g}" for n in score] if score else ["-", "-", "-"]
estimators = ", ".join(sorted(row.get("estimates", {}), key=str.lower)) or "-"
values = [rank, task, okr(row), *figures, cell(estimators, 40), row["status"]]
print("| " + " | ".join(str(v) for v in values) + " |")
if unestimated:
print("unestimated: " + ", ".join(unestimated))
return 0
@@ -338,22 +414,19 @@ def cmd_show(args: argparse.Namespace) -> int:
if not rows:
print("no tasks")
return 0
order = sorted(rows, key=lambda t: (rows[t].get("prio") is None, rows[t].get("prio") or 0, t))
header = ["Prio", "Task", "OKR", "Status", "Worker", "Artifacts", "Start", "End", *ORDER]
header = ["Prio", "WSJF", "Task", "OKR", "Status", "Worker", "Artifacts", "Start", "End", *ORDER]
print("| " + " | ".join(header) + " |")
print("|" + "---|" * len(header))
problems = []
for task in order:
for rank, task in enumerate(ranked(rows), 1):
row = rows[task]
title = f"{task}: {row['title']}" if row.get("title") else task
results = row.get("key_results", [])
okr = cell(row.get("objective") or "-", 40)
if results:
okr += f" ({sum(kr['met'] for kr in results)}/{len(results)} KR)"
score = wsjf(row)
values = [
row.get("prio") or "-",
rank,
f"{score[2]:g}" if score else "-",
cell(title, 50),
okr,
okr(row),
row["status"],
cell(row.get("worker") or "-", 30),
len(row.get("artifacts", [])) or "-",
@@ -384,6 +457,16 @@ def main() -> int:
p.add_argument(f"--drop-{flag}", action="append", default=[])
p.add_argument("--status", choices=STATUSES)
p.set_defaults(func=cmd_set)
p = sub.add_parser("estimate")
p.add_argument("task")
p.add_argument("party")
p.add_argument("--value", type=int)
p.add_argument("--complexity", type=int)
p.add_argument("--drop", action="store_true")
p.set_defaults(func=cmd_estimate)
p = sub.add_parser("wsjf")
p.add_argument("--all", action="store_true")
p.set_defaults(func=cmd_wsjf)
p = sub.add_parser("kr")
p.add_argument("task")
p.add_argument("action", choices=("add", "met", "open", "rm"))
+40
View File
@@ -0,0 +1,40 @@
---
name: kvb-org-wsjf
description: >
WSJF priority from Fibonacci estimates. Trigger on /kvb-org-wsjf, "wsjf",
"priorisieren".
---
Weighted Shortest Job First ranks the tasks of the `kvb-org-rasci` register
by cost of delay over job size: here business value over complexity, each the
median of Fibonacci estimates (1, 2, 3, 5, 8, 13, 21, 34, 55, 89). The
estimates live in the register's table; `wsjf` is its WSJF view, one row per
task with its OKR and the task id the register uses, and the register's Prio
column is this rank. The script is
`${CLAUDE_SKILL_DIR}/../kvb-org-rasci/scripts/rasci.py`.
```bash
rasci.py estimate <task> "<party>" --value <fib> --complexity <fib>
rasci.py estimate <task> "<party>" --drop
rasci.py wsjf [--all]
```
## Procedure
1. **The chair asks.** For every new task, and when its objective or a key
result changes, the resource chair of the `kvb-org-organize` skill asks
its subordinates (the task's worker and R, the representatives of the
lines it touches) for one Fibonacci estimate each of business value and
complexity, one message per agent.
2. **Record each answer** under the party that gave it; a new estimate
replaces the old one. An agent may record its own.
3. **Rank**: WSJF = median business value / median complexity, highest
first. An operator's pin (`rasci.py set <task> --prio <n>`) outranks every
score; unestimated tasks come last and are named under `unestimated:`.
4. **Grant in rank order**: the chair hands out capacity by this rank.
## Rules
- Estimate relative to the other open tasks, never in hours.
- Never invent an estimate for a party that did not answer; a task without
one stays unestimated.
+2 -2
View File
@@ -1,8 +1,8 @@
---
name: kvb-skills-dream
description: >
Consolidate the last 24 hours of work in this repository: what held, what
broke, which hypotheses follow, into memory and improvement proposals.
Consolidate the last 24 hours of work in this repository (what held, what
broke) into memory and improvement proposals.
Trigger on /kvb-skills-dream or a request to reflect on or learn from recent
work.
---
+30 -6
View File
@@ -14,7 +14,7 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
RASCI = REPO_ROOT / "skills" / "kvb-org-rasci" / "scripts" / "rasci.py"
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
HEADER = "| Prio | Task | OKR | Status | Worker | Artifacts | Start | End | R | A | S | C | I |"
HEADER = "| Prio | WSJF | Task | OKR | Status | Worker | Artifacts | Start | End | R | A | S | C | I |"
def local(iso: str) -> str:
@@ -81,11 +81,11 @@ class TestRasci(unittest.TestCase):
lines = self._out("show")
self.assertEqual(lines[0], HEADER)
self.assertEqual(
lines[2], "| 1 | work/b: Beta | - | queued | subagent Explore (haiku) | - | - | - | Pkgmgr | | | | |"
lines[2], "| 1 | - | work/b: Beta | - | queued | subagent Explore (haiku) | - | - | - | Pkgmgr | | | | |"
)
self.assertEqual(
lines[3],
f"| 2 | work/a: Alpha | Ship A | running | Skills | - | {local(start)} | - "
f"| 2 | - | work/a: Alpha | Ship A | running | Skills | - | {local(start)} | - "
"| Skills | Skills | | Operator, Pkgmgr | Operator |",
)
self.assertEqual(
@@ -159,9 +159,9 @@ class TestRasci(unittest.TestCase):
self._run("set", "work/live", "--prio", "1", "--status", "running")
self._run("role", "work/live", "Two", "S")
lines = self._out("show")
self.assertTrue(lines[2].startswith("| 1 | work/live | - | running | One |"))
self.assertTrue(lines[2].startswith("| 1 | - | work/live | - | running | One |"))
self.assertTrue(lines[2].endswith("| One | One | Two | | |"))
self.assertTrue(any(line.startswith("| - | cluster/dead | - | stale | Gone |") for line in lines))
self.assertTrue(lines[3].startswith("| 2 | - | cluster/dead | - | stale | Gone |"))
self.assertFalse(any("chair" in line for line in lines))
def test_council_claim_takeover_and_release_write_through(self):
@@ -203,6 +203,8 @@ class TestRasci(unittest.TestCase):
("get", "work/none"),
("kr", "work/a", "met", "1"),
("set", "work/a", "--artifact", "no-kind"),
("estimate", "work/a", "One", "--value", "4", "--complexity", "2"),
("estimate", "work/a", "One", "--value", "8"),
):
with self.subTest(args=args):
self.assertEqual(self._run(*args).returncode, 2)
@@ -211,7 +213,29 @@ class TestRasci(unittest.TestCase):
def test_pipe_in_a_title_does_not_break_the_table(self):
self._run("set", "work/a", "--title", "a | b")
self._run("role", "work/a", "Skills", "RA")
self.assertTrue(self._out("show")[2].startswith("| - | work/a: a \\| b | - | queued |"))
self.assertTrue(self._out("show")[2].startswith("| 1 | - | work/a: a \\| b | - | queued |"))
def test_wsjf_ranks_by_median_estimates(self):
self._run("set", "work/a", "--objective", "Ship A")
self._run("kr", "work/a", "add", "released")
self._run("estimate", "work/a", "One", "--value", "8", "--complexity", "2")
self._run("estimate", "work/a", "Two", "--value", "13", "--complexity", "3")
self._run("estimate", "work/b", "One", "--value", "5", "--complexity", "5")
self._run("set", "work/c", "--title", "Gamma")
lines = self._out("wsjf")
self.assertEqual(
lines[0], "| Rank | Task | OKR | Business value | Complexity | WSJF | Estimators | Status |"
)
self.assertEqual(lines[2], "| 1 | work/a | Ship A (0/1 KR) | 10.5 | 2.5 | 4.2 | One, Two | queued |")
self.assertEqual(lines[3], "| 2 | work/b | - | 5 | 5 | 1 | One | queued |")
self.assertEqual(lines[4], "| 3 | work/c | - | - | - | - | - | queued |")
self.assertEqual(lines[5], "unestimated: work/c")
self.assertTrue(self._out("show")[2].startswith("| 1 | 4.2 | work/a |"))
self.assertIn("wsjf: 4.2", self._out("get", "work/a"))
self._run("set", "work/c", "--prio", "1")
self.assertTrue(self._out("show")[2].startswith("| 1 | - | work/c: Gamma |"))
self._run("estimate", "work/b", "One", "--drop")
self.assertFalse(any(line.startswith("estimate ") for line in self._out("get", "work/b")))
class TestOrgSkillsUseTheRegister(unittest.TestCase):