Add cross-tool positioning, Python helpers, tiers, and hygiene docs

Five improvements to position the library as a serious engineering project:

1. Cross-tool compatibility — new README "Works With" section honestly
   documenting where skills run (Claude Code natively; SKILL.md bodies
   port to other agents and chat LLMs as system prompts).

2. Python helper scripts (stdlib-only) for the three strongest skills:
   - sprint-planning: capacity_calculator.py (recommended commitment)
   - rice-prioritisation: rice_calculator.py (ranks, flags quick wins/moonshots)
   - cs-health-scorecard: health_score.py (weighted total + RAG)
   Each is wired into its SKILL.md and synced to the plugin copies.

3. Explicit skill tiering — TIERS.md + README section marking 46
   Production-Ready skills and calling out Experimental (external-dependency)
   ones; everything else is Stable.

4. Repository hygiene — new CHANGELOG.md (Keep a Changelog format) and
   SKILL-AUTHORING-STANDARD.md; refreshed SECURITY.md version table and
   helper-script disclosure; added .gitignore.

5. Related Projects — README section linking to alirezarezvani/claude-skills
   and the major awesome-claude-skills / awesome-claude-code lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px
This commit is contained in:
Claude
2026-06-17 07:48:48 +00:00
parent 2299e59d72
commit 760f979365
20 changed files with 1514 additions and 5 deletions
@@ -35,6 +35,20 @@ Score each dimension 15. Weight as shown. Calculate weighted total out of 100
- 6079: Amber (at risk, needs attention)
- 059: Red (high churn risk, escalate)
## Programmatic Helper
This skill ships with a stdlib-only Python script that applies the weights above and converts the weighted total to a RAG status — so the headline score is computed identically every time and weights always sum to 100%.
```bash
# Five scores 1-5 in order: adoption engagement outcomes support commercial
python3 scripts/health_score.py --scores 4 3 4 2 5 --account "Acme Corp"
# Or from JSON (lets you override the default weights per account/segment)
python3 scripts/health_score.py --input account.json
```
It returns the per-dimension weighted points, the **total out of 100**, and the **RAG band** (Green ≥80, Amber 6079, Red <60) with a one-line next step. Run it to set the headline number, then write the dimension detail and actions below around it. Add `--json` for downstream tooling.
## Output Format
---
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Customer health score calculator for the cs-health-scorecard skill.
Takes per-dimension scores (1-5), applies the standard weights, and returns a
weighted total out of 100 plus a RAG status — so the headline number in a health
scorecard is computed the same way every time. Pure Python standard library —
no dependencies, no network access.
Standard dimensions and weights (override with --weights or in the JSON):
Product Adoption 30%
Engagement 20%
Outcomes 20%
Support Health 15%
Commercial 15%
Usage
-----
Quick scoring from flags (order: adoption engagement outcomes support commercial):
python3 health_score.py --scores 4 3 4 2 5
From a JSON file that can also override weights:
python3 health_score.py --input account.json
account.json:
{
"account": "Acme Corp",
"scores": {"Product Adoption": 4, "Engagement": 3, "Outcomes": 4,
"Support Health": 2, "Commercial": 5},
"weights": {"Product Adoption": 0.30, "Engagement": 0.20, "Outcomes": 0.20,
"Support Health": 0.15, "Commercial": 0.15}
}
"""
from __future__ import annotations
import argparse
import json
import sys
DEFAULT_WEIGHTS = {
"Product Adoption": 0.30,
"Engagement": 0.20,
"Outcomes": 0.20,
"Support Health": 0.15,
"Commercial": 0.15,
}
MAX_DIMENSION_SCORE = 5
def rag(total: float) -> str:
if total >= 80:
return "Green"
if total >= 60:
return "Amber"
return "Red"
def compute(scores: dict[str, float], weights: dict[str, float] | None = None) -> dict:
weights = weights or DEFAULT_WEIGHTS
weight_sum = sum(weights.values())
if abs(weight_sum - 1.0) > 0.001:
raise ValueError(f"Weights must sum to 1.0 (got {weight_sum:.3f}).")
breakdown = []
total = 0.0
for dimension, weight in weights.items():
if dimension not in scores:
raise ValueError(f"Missing score for dimension '{dimension}'.")
raw = float(scores[dimension])
if not 1 <= raw <= MAX_DIMENSION_SCORE:
raise ValueError(f"Score for '{dimension}' must be between 1 and {MAX_DIMENSION_SCORE} (got {raw}).")
# Normalise the 1-5 score to a 0-100 contribution weighted by importance.
weighted = (raw / MAX_DIMENSION_SCORE) * weight * 100
total += weighted
breakdown.append({
"dimension": dimension,
"score": raw,
"weight": weight,
"weighted_points": round(weighted, 1),
})
total = round(total, 1)
return {"total": total, "rag": rag(total), "breakdown": breakdown}
def _render(result: dict, account: str | None) -> str:
title = f"Customer Health Scorecard: {account}" if account else "Customer Health Scorecard"
lines = [title, "=" * len(title)]
lines.append(f"{'Dimension':<18} {'Score':>5} {'Weight':>7} {'Weighted':>9}")
lines.append("-" * 41)
for row in result["breakdown"]:
lines.append(
f"{row['dimension']:<18} {row['score']:>5g} {row['weight']*100:>6.0f}% {row['weighted_points']:>9g}"
)
lines.append("-" * 41)
badge = {"Green": "🟢", "Amber": "🟡", "Red": "🔴"}[result["rag"]]
lines.append(f"{'TOTAL':<18} {'':>5} {'100%':>7} {result['total']:>9g}/100")
lines.append("")
lines.append(f"Overall health: {badge} {result['rag']}{result['total']}/100")
guidance = {
"Green": "Healthy — renew likely. Look for expansion signals.",
"Amber": "At risk — needs attention. Build a save/grow plan before renewal.",
"Red": "High churn risk — escalate now and assign an executive sponsor.",
}[result["rag"]]
lines.append(guidance)
return "\n".join(lines)
def _load_inputs(args: argparse.Namespace) -> tuple[dict, dict | None, str | None]:
if args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input).read()
data = json.loads(raw)
return data["scores"], data.get("weights"), data.get("account")
if args.scores:
dims = list(DEFAULT_WEIGHTS.keys())
if len(args.scores) != len(dims):
raise ValueError(f"--scores needs {len(dims)} values in order: {', '.join(dims)}")
return dict(zip(dims, args.scores)), None, args.account
raise ValueError("Provide --input or --scores.")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--input", help="Path to a JSON file (or '-' for stdin).")
parser.add_argument("--scores", nargs="+", type=float,
help="Five scores 1-5 in order: adoption engagement outcomes support commercial.")
parser.add_argument("--account", help="Account name for the report header.")
parser.add_argument("--json", action="store_true", dest="as_json", help="Emit JSON instead of a report.")
args = parser.parse_args(argv)
try:
scores, weights, account = _load_inputs(args)
result = compute(scores, weights)
except (ValueError, KeyError, json.JSONDecodeError, OSError) as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if args.as_json:
result["account"] = account
print(json.dumps(result, indent=2))
else:
print(_render(result, account))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -53,6 +53,22 @@ Availability factor: 0.70.85 depending on holidays/events
Story points to commit = Historical velocity × Availability factor
```
## Programmatic Helper
This skill ships with a stdlib-only Python script that computes capacity instead of estimating it by hand. Use it whenever the team's numbers are known — it applies the availability and 80% commit-ratio rules consistently.
```bash
# Quick estimate from flags
python3 scripts/capacity_calculator.py --team 5 --days 10 --velocity 30 --availability 0.8 --carryover 5
# Detailed estimate from per-member availability (JSON via stdin or --input file.json)
echo '{"sprint_days":10,"historical_velocity":40,"carryover_points":8,
"members":[{"name":"Ada","available_days":10},{"name":"Linus","available_days":7}]}' \
| python3 scripts/capacity_calculator.py --input -
```
The script returns available focus hours, a velocity figure adjusted for real availability, the **recommended commitment** (capped at 80% of velocity), and the remaining **capacity for new work** after carry-overs. Run it first, then build the sprint backlog to fit the recommended number. Add `--json` to pipe the result into other tooling.
## Output Format
### Sprint [N] — [Start Date] to [End Date]
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Sprint capacity calculator for the sprint-planning skill.
Turns team and availability inputs into a recommended sprint commitment so the
numbers in a sprint plan are computed, not guessed. Pure Python standard
library — no dependencies, no network access.
Examples
--------
Quick estimate from flags:
python3 capacity_calculator.py --team 5 --days 10 --velocity 30 \
--availability 0.8 --carryover 5
Detailed estimate from a JSON file describing each team member:
python3 capacity_calculator.py --input team.json
Where team.json looks like:
{
"sprint_days": 10,
"focus_hours_per_day": 6,
"historical_velocity": 30,
"carryover_points": 5,
"commit_ratio": 0.8,
"members": [
{"name": "Ada", "available_days": 10},
{"name": "Linus", "available_days": 7, "note": "2 days PTO, 1 day interview"}
]
}
The recommended commitment deliberately leaves slack for unplanned work — it
never commits 100% of theoretical capacity.
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field
@dataclass
class Member:
name: str
available_days: float
note: str = ""
@dataclass
class CapacityInputs:
sprint_days: int = 10
focus_hours_per_day: float = 6.0
historical_velocity: float | None = None
carryover_points: float = 0.0
commit_ratio: float = 0.8
team_size: int | None = None
availability_factor: float = 0.8
members: list[Member] = field(default_factory=list)
def _availability_from_members(inp: CapacityInputs) -> float:
"""Return the blended availability factor (0-1) from per-member days."""
if not inp.members:
return inp.availability_factor
theoretical = len(inp.members) * inp.sprint_days
if theoretical == 0:
return 0.0
actual = sum(m.available_days for m in inp.members)
return actual / theoretical
def compute(inp: CapacityInputs) -> dict:
team_size = inp.team_size if inp.team_size is not None else len(inp.members)
if not team_size:
raise ValueError("Provide --team or a non-empty members list.")
availability = _availability_from_members(inp)
focus_hours = team_size * inp.sprint_days * inp.focus_hours_per_day * availability
result: dict = {
"team_size": team_size,
"sprint_days": inp.sprint_days,
"focus_hours_per_day": inp.focus_hours_per_day,
"availability_factor": round(availability, 3),
"available_focus_hours": round(focus_hours, 1),
}
if inp.historical_velocity is not None:
velocity_adjusted = inp.historical_velocity * availability
recommended = velocity_adjusted * inp.commit_ratio
new_work_capacity = max(recommended - inp.carryover_points, 0.0)
result.update(
{
"historical_velocity": inp.historical_velocity,
"velocity_adjusted_for_availability": round(velocity_adjusted, 1),
"commit_ratio": inp.commit_ratio,
"carryover_points": inp.carryover_points,
"recommended_commitment_points": round(recommended, 1),
"capacity_for_new_work_points": round(new_work_capacity, 1),
}
)
if inp.carryover_points > recommended:
result["warning"] = (
"Carry-over alone exceeds the recommended commitment — "
"pull in little or no new work this sprint."
)
return result
def _parse_inputs(args: argparse.Namespace) -> CapacityInputs:
if args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input).read()
data = json.loads(raw)
members = [
Member(
name=m.get("name", f"member-{i+1}"),
available_days=float(m.get("available_days", data.get("sprint_days", 10))),
note=m.get("note", ""),
)
for i, m in enumerate(data.get("members", []))
]
return CapacityInputs(
sprint_days=int(data.get("sprint_days", 10)),
focus_hours_per_day=float(data.get("focus_hours_per_day", 6.0)),
historical_velocity=(
float(data["historical_velocity"])
if data.get("historical_velocity") is not None
else None
),
carryover_points=float(data.get("carryover_points", 0.0)),
commit_ratio=float(data.get("commit_ratio", 0.8)),
team_size=data.get("team_size"),
availability_factor=float(data.get("availability_factor", 0.8)),
members=members,
)
return CapacityInputs(
sprint_days=args.days,
focus_hours_per_day=args.focus_hours,
historical_velocity=args.velocity,
carryover_points=args.carryover,
commit_ratio=args.commit_ratio,
team_size=args.team,
availability_factor=args.availability,
)
def _render(result: dict) -> str:
lines = ["Sprint Capacity Estimate", "=" * 24]
label = {
"team_size": "Team size",
"sprint_days": "Sprint days",
"focus_hours_per_day": "Focus hours/day",
"availability_factor": "Availability factor",
"available_focus_hours": "Available focus hours",
"historical_velocity": "Historical velocity (pts)",
"velocity_adjusted_for_availability": "Velocity adj. for availability",
"commit_ratio": "Commit ratio",
"carryover_points": "Carry-over (pts)",
"recommended_commitment_points": "RECOMMENDED commitment (pts)",
"capacity_for_new_work_points": "Capacity for NEW work (pts)",
}
for key, text in label.items():
if key in result:
lines.append(f"{text:<32}: {result[key]}")
if "warning" in result:
lines.append("")
lines.append(f"⚠️ {result['warning']}")
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--input", help="Path to a JSON file describing the team (or '-' for stdin).")
parser.add_argument("--team", type=int, help="Number of team members.")
parser.add_argument("--days", type=int, default=10, help="Working days in the sprint (default: 10).")
parser.add_argument("--focus-hours", type=float, default=6.0, dest="focus_hours",
help="Focus hours per person per day (default: 6).")
parser.add_argument("--velocity", type=float, help="Historical average velocity in story points.")
parser.add_argument("--carryover", type=float, default=0.0, help="Carry-over story points from last sprint.")
parser.add_argument("--availability", type=float, default=0.8,
help="Availability factor 0-1 when not using per-member days (default: 0.8).")
parser.add_argument("--commit-ratio", type=float, default=0.8, dest="commit_ratio",
help="Fraction of velocity to commit, leaving slack (default: 0.8).")
parser.add_argument("--json", action="store_true", help="Emit JSON instead of a formatted report.")
args = parser.parse_args(argv)
try:
result = compute(_parse_inputs(args))
except (ValueError, json.JSONDecodeError, OSError) as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
print(json.dumps(result, indent=2) if args.json else _render(result))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -25,6 +25,24 @@ Ask the user for these if not provided:
## RICE Formula
RICE Score = (Reach × Impact × Confidence) / Effort
## Programmatic Helper
This skill ships with a stdlib-only Python script that calculates and ranks RICE scores so the maths is consistent and the quick-win / moonshot flags are applied by rule, not by feel. Feed it the initiatives once R, I, C, and E are gathered.
```bash
# From a JSON file (confidence accepts 0.8 or 80)
python3 scripts/rice_calculator.py initiatives.json
# Or from a CSV with header: name,reach,impact,confidence,effort
python3 scripts/rice_calculator.py initiatives.csv --format csv
# Or piped in
echo '[{"name":"Onboarding","reach":5000,"impact":2,"confidence":0.8,"effort":3}]' \
| python3 scripts/rice_calculator.py -
```
It outputs a ranked table with computed RICE scores and auto-flags **quick-win** (strong score, low relative effort), **moonshot** (high impact, high effort), and **low-confidence** (≤50%) items. Use the computed ranking as the starting point, then apply the validation step below — never accept a surprising top rank without checking the estimates behind it.
## Process
1. For each initiative provided, gather or estimate R, I, C, E values
2. Flag where estimates are weak and note what data would improve them
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""RICE score calculator for the rice-prioritisation skill.
Computes RICE = (Reach × Impact × Confidence) / Effort for a list of
initiatives, ranks them, and flags quick wins and moonshots so the ranking in a
prioritisation doc is calculated consistently rather than eyeballed. Pure Python
standard library — no dependencies, no network access.
Input
-----
A JSON or CSV list of initiatives. Each needs: name, reach, impact, confidence,
effort.
- impact uses the standard RICE scale (3, 2, 1, 0.5, 0.25) but any number works.
- confidence is a fraction (0.8) or a percentage (80) — both are accepted.
- effort is in person-months and must be > 0.
JSON example (rice.json):
[
{"name": "Onboarding redesign", "reach": 5000, "impact": 2, "confidence": 0.8, "effort": 3},
{"name": "Dark mode", "reach": 8000, "impact": 0.5, "confidence": 1.0, "effort": 1}
]
CSV example (header row required):
name,reach,impact,confidence,effort
Onboarding redesign,5000,2,0.8,3
Dark mode,8000,0.5,1.0,1
Usage
-----
python3 rice_calculator.py rice.json
python3 rice_calculator.py rice.csv --format csv
cat rice.json | python3 rice_calculator.py - --json
"""
from __future__ import annotations
import argparse
import csv
import io
import json
import sys
from dataclasses import dataclass
@dataclass
class Initiative:
name: str
reach: float
impact: float
confidence: float
effort: float
@property
def score(self) -> float:
if self.effort <= 0:
raise ValueError(f"Effort for '{self.name}' must be greater than 0.")
return (self.reach * self.impact * self.confidence) / self.effort
def _normalise_confidence(value: float) -> float:
"""Accept 80 or 0.8; return a fraction between 0 and 1."""
return value / 100.0 if value > 1 else value
def _to_initiative(row: dict) -> Initiative:
try:
return Initiative(
name=str(row["name"]).strip(),
reach=float(row["reach"]),
impact=float(row["impact"]),
confidence=_normalise_confidence(float(row["confidence"])),
effort=float(row["effort"]),
)
except KeyError as exc:
raise ValueError(f"Missing required field {exc} in row: {row}") from None
def load(text: str, fmt: str) -> list[Initiative]:
if fmt == "csv":
rows = list(csv.DictReader(io.StringIO(text)))
else:
rows = json.loads(text)
if not isinstance(rows, list):
raise ValueError("Input must be a list of initiatives.")
return [_to_initiative(r) for r in rows]
def rank(initiatives: list[Initiative]) -> list[dict]:
scored = []
for i in initiatives:
scored.append({
"name": i.name,
"reach": i.reach,
"impact": i.impact,
"confidence": round(i.confidence, 2),
"effort": i.effort,
"rice_score": round(i.score, 1),
})
scored.sort(key=lambda d: d["rice_score"], reverse=True)
if scored:
max_score = max(d["rice_score"] for d in scored) or 1
max_effort = max(d["effort"] for d in scored) or 1
for rank_index, d in enumerate(scored, start=1):
d["rank"] = rank_index
flags = []
# Quick win: strong score relative to the field, low relative effort.
if d["rice_score"] >= 0.5 * max_score and d["effort"] <= 0.33 * max_effort:
flags.append("quick-win")
# Moonshot: high raw impact, high relative effort.
if d["impact"] >= 2 and d["effort"] >= 0.66 * max_effort:
flags.append("moonshot")
# Low-confidence estimates should be revisited before acting.
if d["confidence"] <= 0.5:
flags.append("low-confidence")
d["flags"] = flags
return scored
def _render(scored: list[dict]) -> str:
header = f"{'#':>2} {'Initiative':<32} {'Reach':>8} {'Imp':>4} {'Conf':>5} {'Eff':>5} {'RICE':>8} Flags"
lines = ["RICE Prioritisation", "=" * len(header), header, "-" * len(header)]
for d in scored:
lines.append(
f"{d['rank']:>2} {d['name'][:32]:<32} {d['reach']:>8g} {d['impact']:>4g} "
f"{d['confidence']:>5.2f} {d['effort']:>5g} {d['rice_score']:>8g} {', '.join(d['flags'])}"
)
quick = [d["name"] for d in scored if "quick-win" in d["flags"]]
moon = [d["name"] for d in scored if "moonshot" in d["flags"]]
lowc = [d["name"] for d in scored if "low-confidence" in d["flags"]]
lines.append("")
lines.append(f"Quick wins (do alongside bigger bets): {', '.join(quick) or 'none'}")
lines.append(f"Moonshots (high impact, high effort): {', '.join(moon) or 'none'}")
lines.append(f"Low confidence — revisit estimates: {', '.join(lowc) or 'none'}")
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("input", help="Path to a JSON/CSV file of initiatives, or '-' for stdin.")
parser.add_argument("--format", choices=["json", "csv"], help="Input format (inferred from extension if omitted).")
parser.add_argument("--json", action="store_true", dest="as_json", help="Emit ranked JSON instead of a table.")
args = parser.parse_args(argv)
text = sys.stdin.read() if args.input == "-" else None
fmt = args.format
if text is None:
try:
text = open(args.input).read()
except OSError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if fmt is None:
fmt = "csv" if args.input.lower().endswith(".csv") else "json"
fmt = fmt or "json"
try:
scored = rank(load(text, fmt))
except (ValueError, json.JSONDecodeError) as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
print(json.dumps(scored, indent=2) if args.as_json else _render(scored))
return 0
if __name__ == "__main__":
raise SystemExit(main())