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:
@@ -35,6 +35,20 @@ Score each dimension 1–5. Weight as shown. Calculate weighted total out of 100
|
||||
- 60–79: Amber (at risk, needs attention)
|
||||
- 0–59: 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 60–79, 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())
|
||||
Reference in New Issue
Block a user