feat(org): rank the register by WSJF from Fibonacci estimates
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
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:
1 parent
35dc37641d
commit
5a93b0e5a9
10 files changed
+181
-27
No files matched your search
@@ -59,6 +59,7 @@ flowchart LR
|
|||||||
org --> org_matrix[kvb-org-matrix]
|
org --> org_matrix[kvb-org-matrix]
|
||||||
org --> org_rasci[kvb-org-rasci]
|
org --> org_rasci[kvb-org-rasci]
|
||||||
org --> org_artefacts[kvb-org-artefacts]
|
org --> org_artefacts[kvb-org-artefacts]
|
||||||
|
org --> org_wsjf[kvb-org-wsjf]
|
||||||
org --> org_robot[kvb-org-robot]
|
org --> org_robot[kvb-org-robot]
|
||||||
skills --> skills_dream[kvb-skills-dream]
|
skills --> skills_dream[kvb-skills-dream]
|
||||||
skills --> skills_tune[kvb-skills-tune]
|
skills --> skills_tune[kvb-skills-tune]
|
||||||
@@ -128,6 +129,7 @@ flowchart LR
|
|||||||
status_matrix[kvb-org-matrix]
|
status_matrix[kvb-org-matrix]
|
||||||
rasci[kvb-org-rasci]
|
rasci[kvb-org-rasci]
|
||||||
artefacts[kvb-org-artefacts]
|
artefacts[kvb-org-artefacts]
|
||||||
|
wsjf[kvb-org-wsjf]
|
||||||
robot[kvb-org-robot]
|
robot[kvb-org-robot]
|
||||||
end
|
end
|
||||||
subgraph cat_skills [skills]
|
subgraph cat_skills [skills]
|
||||||
@@ -176,6 +178,8 @@ flowchart LR
|
|||||||
status_matrix --> rasci
|
status_matrix --> rasci
|
||||||
cleanup --> artefacts
|
cleanup --> artefacts
|
||||||
artefacts --> rasci
|
artefacts --> rasci
|
||||||
|
democracy --> wsjf
|
||||||
|
wsjf --> rasci
|
||||||
dream --> rasci
|
dream --> rasci
|
||||||
|
|
||||||
observe --> triage
|
observe --> triage
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
name: kvb-code-test
|
name: kvb-code-test
|
||||||
description: >
|
description: >
|
||||||
Discover the Makefile's test targets, let the operator pick, run and report;
|
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
|
fix mode loops until green. Trigger on /kvb-code-test [--fix], or asked to
|
||||||
operator asks to run the tests, make them pass, or get the suite green.
|
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
|
The Makefile is the source of truth for how this project runs tests. Never
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: kvb-git-commit-next
|
|||||||
description: >
|
description: >
|
||||||
Check every branch for the staged fix, commit the staged bundle with
|
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
|
--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
|
1. `git fetch --all --prune --tags`, then hunt the staged fix across every
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ records the result.
|
|||||||
- Before starting anything that consumes shared capacity (gates, deploys,
|
- Before starting anything that consumes shared capacity (gates, deploys,
|
||||||
iteration loops, swarm or cloud resources, cleanup of shared data), an agent
|
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
|
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
|
for the grant, and reports the release. The chair keeps one queue, ordered
|
||||||
priorities in the `kvb-org-rasci` register, answers every request with its
|
by the WSJF rank of the `kvb-org-wsjf` skill (it asks its subordinates for
|
||||||
position, enforces the limits the operator set,
|
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>`;
|
checks every grant with `watch.py admit --need-gb <N>` (or `--kind <slot>`;
|
||||||
test kinds also need CPU and RAM below the configured limits).
|
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
|
- A running job (deploy, gate, test, swarm run, build) is never cut short to
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: kvb-org-perform
|
name: kvb-org-perform
|
||||||
description: >
|
description: >
|
||||||
Pick `model` and `effort` per subagent from a fixed table: cheaper only on a
|
Pick `model` and `effort` per subagent from a fixed table: cheaper only at
|
||||||
clear match at 90 % that the result equals the default's; log spawns and
|
90 % confidence that the result equals the default's; log spawns and
|
||||||
quality. Apply AUTOMATICALLY before every Agent tool call.
|
quality. Apply AUTOMATICALLY before every Agent tool call.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ rasci.py get <task>
|
|||||||
rasci.py rm <task>
|
rasci.py rm <task>
|
||||||
rasci.py show [--all]
|
rasci.py show [--all]
|
||||||
rasci.py artifacts [--ended] [--task <task>]
|
rasci.py artifacts [--ended] [--task <task>]
|
||||||
|
rasci.py estimate <task> "<party>" --value <fib> --complexity <fib>
|
||||||
|
rasci.py wsjf [--all]
|
||||||
rasci.py tools
|
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:
|
- 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
|
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`.
|
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
|
- Priority is the rank of the `kvb-org-wsjf` skill: an operator's pin
|
||||||
writes it.
|
(`--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.
|
- 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
|
- 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`
|
without exactly one A, without R, or open without a key result. `get`
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import importlib.util
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import statistics
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
@@ -41,6 +42,7 @@ FIELDS = (
|
|||||||
TEXT_FIELDS = ("title", "objective", "description", "worker", "workdir", "branch")
|
TEXT_FIELDS = ("title", "objective", "description", "worker", "workdir", "branch")
|
||||||
LIST_FIELDS = {"tools": "tool"}
|
LIST_FIELDS = {"tools": "tool"}
|
||||||
ARTIFACT = re.compile(r"^[a-z]+:\S")
|
ARTIFACT = re.compile(r"^[a-z]+:\S")
|
||||||
|
FIBONACCI = (1, 2, 3, 5, 8, 13, 21, 34, 55, 89)
|
||||||
OPERATOR = "Operator"
|
OPERATOR = "Operator"
|
||||||
|
|
||||||
|
|
||||||
@@ -192,6 +194,33 @@ def when(value: str | None) -> str:
|
|||||||
return datetime.datetime.fromisoformat(value).astimezone().strftime("%m-%d %H:%M")
|
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]:
|
def gaps(task: str, row: dict) -> list[str]:
|
||||||
found = []
|
found = []
|
||||||
roles = row["roles"]
|
roles = row["roles"]
|
||||||
@@ -297,6 +326,53 @@ def cmd_get(args: argparse.Namespace) -> int:
|
|||||||
print(f"{flag}: {item}")
|
print(f"{flag}: {item}")
|
||||||
roles = ", ".join(f"{p}={v}" for p, v in sorted(row["roles"].items()))
|
roles = ", ".join(f"{p}={v}" for p, v in sorted(row["roles"].items()))
|
||||||
print(f"roles: {roles or '-'}")
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -338,22 +414,19 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|||||||
if not rows:
|
if not rows:
|
||||||
print("no tasks")
|
print("no tasks")
|
||||||
return 0
|
return 0
|
||||||
order = sorted(rows, key=lambda t: (rows[t].get("prio") is None, rows[t].get("prio") or 0, t))
|
header = ["Prio", "WSJF", "Task", "OKR", "Status", "Worker", "Artifacts", "Start", "End", *ORDER]
|
||||||
header = ["Prio", "Task", "OKR", "Status", "Worker", "Artifacts", "Start", "End", *ORDER]
|
|
||||||
print("| " + " | ".join(header) + " |")
|
print("| " + " | ".join(header) + " |")
|
||||||
print("|" + "---|" * len(header))
|
print("|" + "---|" * len(header))
|
||||||
problems = []
|
problems = []
|
||||||
for task in order:
|
for rank, task in enumerate(ranked(rows), 1):
|
||||||
row = rows[task]
|
row = rows[task]
|
||||||
title = f"{task}: {row['title']}" if row.get("title") else task
|
title = f"{task}: {row['title']}" if row.get("title") else task
|
||||||
results = row.get("key_results", [])
|
score = wsjf(row)
|
||||||
okr = cell(row.get("objective") or "-", 40)
|
|
||||||
if results:
|
|
||||||
okr += f" ({sum(kr['met'] for kr in results)}/{len(results)} KR)"
|
|
||||||
values = [
|
values = [
|
||||||
row.get("prio") or "-",
|
rank,
|
||||||
|
f"{score[2]:g}" if score else "-",
|
||||||
cell(title, 50),
|
cell(title, 50),
|
||||||
okr,
|
okr(row),
|
||||||
row["status"],
|
row["status"],
|
||||||
cell(row.get("worker") or "-", 30),
|
cell(row.get("worker") or "-", 30),
|
||||||
len(row.get("artifacts", [])) or "-",
|
len(row.get("artifacts", [])) or "-",
|
||||||
@@ -384,6 +457,16 @@ def main() -> int:
|
|||||||
p.add_argument(f"--drop-{flag}", action="append", default=[])
|
p.add_argument(f"--drop-{flag}", action="append", default=[])
|
||||||
p.add_argument("--status", choices=STATUSES)
|
p.add_argument("--status", choices=STATUSES)
|
||||||
p.set_defaults(func=cmd_set)
|
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 = sub.add_parser("kr")
|
||||||
p.add_argument("task")
|
p.add_argument("task")
|
||||||
p.add_argument("action", choices=("add", "met", "open", "rm"))
|
p.add_argument("action", choices=("add", "met", "open", "rm"))
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: kvb-skills-dream
|
name: kvb-skills-dream
|
||||||
description: >
|
description: >
|
||||||
Consolidate the last 24 hours of work in this repository: what held, what
|
Consolidate the last 24 hours of work in this repository (what held, what
|
||||||
broke, which hypotheses follow, into memory and improvement proposals.
|
broke) into memory and improvement proposals.
|
||||||
Trigger on /kvb-skills-dream or a request to reflect on or learn from recent
|
Trigger on /kvb-skills-dream or a request to reflect on or learn from recent
|
||||||
work.
|
work.
|
||||||
---
|
---
|
||||||
|
|||||||
+30
-6
@@ -14,7 +14,7 @@ from pathlib import Path
|
|||||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
RASCI = REPO_ROOT / "skills" / "kvb-org-rasci" / "scripts" / "rasci.py"
|
RASCI = REPO_ROOT / "skills" / "kvb-org-rasci" / "scripts" / "rasci.py"
|
||||||
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.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:
|
def local(iso: str) -> str:
|
||||||
@@ -81,11 +81,11 @@ class TestRasci(unittest.TestCase):
|
|||||||
lines = self._out("show")
|
lines = self._out("show")
|
||||||
self.assertEqual(lines[0], HEADER)
|
self.assertEqual(lines[0], HEADER)
|
||||||
self.assertEqual(
|
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(
|
self.assertEqual(
|
||||||
lines[3],
|
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 |",
|
"| Skills | Skills | | Operator, Pkgmgr | Operator |",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -159,9 +159,9 @@ class TestRasci(unittest.TestCase):
|
|||||||
self._run("set", "work/live", "--prio", "1", "--status", "running")
|
self._run("set", "work/live", "--prio", "1", "--status", "running")
|
||||||
self._run("role", "work/live", "Two", "S")
|
self._run("role", "work/live", "Two", "S")
|
||||||
lines = self._out("show")
|
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(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))
|
self.assertFalse(any("chair" in line for line in lines))
|
||||||
|
|
||||||
def test_council_claim_takeover_and_release_write_through(self):
|
def test_council_claim_takeover_and_release_write_through(self):
|
||||||
@@ -203,6 +203,8 @@ class TestRasci(unittest.TestCase):
|
|||||||
("get", "work/none"),
|
("get", "work/none"),
|
||||||
("kr", "work/a", "met", "1"),
|
("kr", "work/a", "met", "1"),
|
||||||
("set", "work/a", "--artifact", "no-kind"),
|
("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):
|
with self.subTest(args=args):
|
||||||
self.assertEqual(self._run(*args).returncode, 2)
|
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):
|
def test_pipe_in_a_title_does_not_break_the_table(self):
|
||||||
self._run("set", "work/a", "--title", "a | b")
|
self._run("set", "work/a", "--title", "a | b")
|
||||||
self._run("role", "work/a", "Skills", "RA")
|
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):
|
class TestOrgSkillsUseTheRegister(unittest.TestCase):
|
||||||
|
|||||||
Reference in new issue
Block a user