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:
+12
@@ -0,0 +1,12 @@
|
||||
# Python (helper scripts)
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
*.swp
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -0,0 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project broadly follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html):
|
||||
each new wave of skills bumps the **major** version, extensions and fixes bump
|
||||
**minor** / **patch**.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Programmatic helpers (stdlib Python) for three flagship skills.** Each runs with
|
||||
zero dependencies and computes part of the work instead of estimating by hand:
|
||||
- `sprint-planning/scripts/capacity_calculator.py` — recommended sprint commitment
|
||||
from team size, availability, velocity, and carry-over (caps at 80% of velocity).
|
||||
- `rice-prioritisation/scripts/rice_calculator.py` — calculates and ranks RICE
|
||||
scores from JSON/CSV and auto-flags quick wins, moonshots, and low-confidence items.
|
||||
- `cs-health-scorecard/scripts/health_score.py` — weighted health total out of 100
|
||||
with RAG banding and weight validation.
|
||||
- **`CHANGELOG.md`** — this file, back-filled from the release history.
|
||||
- **`SKILL-AUTHORING-STANDARD.md`** — the canonical structure every SKILL.md follows
|
||||
(frontmatter, required sections, quality bar, anti-patterns).
|
||||
- **Skill tiers** — a `TIERS.md` reference and README section marking skills as
|
||||
**Production-Ready**, **Stable**, or **Experimental** so new users start with the
|
||||
strongest work.
|
||||
- **Cross-tool compatibility** — README now documents which platforms the skills work
|
||||
on (Claude Code natively; the SKILL.md bodies port to other agents and chat LLMs).
|
||||
- **Related Projects** — README section linking to other community Claude Skills
|
||||
libraries and the `awesome-claude-skills` / `awesome-claude-code` lists.
|
||||
|
||||
### Changed
|
||||
- `SECURITY.md` supported-versions table updated to the current release line.
|
||||
|
||||
## [14.0.0] — Writers & Content Creators + 7 Community Skills
|
||||
|
||||
### Added
|
||||
- New profession **Writers & Content Creators** (`pm-writers`): Instagram Post
|
||||
Downloader, AEO Optimizer, Thumbnail Creator, Substack Notes Scraper, Notes Humanizer.
|
||||
- `pm-cross` (+3): Sycophancy Challenger, Last 30 Days Research, NotebookLM Connector.
|
||||
- `pm-operations` (+2): Email Triage, Morning Intelligence.
|
||||
- `pm-engineering` (+2): Context Mode, Claude Superpowers.
|
||||
|
||||
Library now spans **167 skills** across **18 professions** + 4 agent templates.
|
||||
|
||||
## [13.0.0] — Social Media Profession
|
||||
|
||||
### Added
|
||||
- New bundle `pm-social`: Social Media Audit, Influencer Brief, Community Management
|
||||
Playbook, Social Ad Campaign, Viral Content Framework.
|
||||
|
||||
## [12.0.0] — 150 Skills Milestone
|
||||
|
||||
### Added
|
||||
- 15 skills across 10 bundles, including Cohort Analysis, Data Pipeline Spec, Renewal
|
||||
Playbook, Customer Success Plan, 360-Degree Feedback Template, Team Health Check, Risk
|
||||
Register, RACI Matrix, Social Media Strategy, Product Positioning Doc, Customer Journey
|
||||
Map, User Story Writer, AI Ethics Review, Partnership Proposal, Design System Audit.
|
||||
|
||||
Library reached **150 skills** across **16 professions**.
|
||||
|
||||
## [11.0.0] — Engineering Expansion (500 ⭐)
|
||||
|
||||
### Added
|
||||
- `pm-engineering` expanded to 35 skills — CI/CD, SLOs, capacity planning, DR plans,
|
||||
threat models, schema/migration design, and more.
|
||||
|
||||
## [10.0.0] — Customer Success + Engineering
|
||||
|
||||
### Added
|
||||
- **Customer Success** bundle (`pm-cs`, 250 ⭐ milestone): Customer Health Scorecard,
|
||||
QBR Deck, Escalation Brief, Churn Analysis.
|
||||
- **Engineering** (500 ⭐ milestone): CI/CD Playbook, SLO & Error Budget, Developer
|
||||
Onboarding Doc, On-Call Runbook — plus Debugging Log Analyser, PR Description Writer,
|
||||
System Design Interview, Changelog Generator, Test Strategy Doc, Runbook Writer.
|
||||
|
||||
Library reached **114 skills** across **16 professions**.
|
||||
|
||||
## [6.0.0] — 100 Skills Milestone
|
||||
|
||||
### Added
|
||||
- Quality rebuild across all existing skills, plus 10 Figma skills.
|
||||
- 7 new skills: Teaching Lesson Plan, SEO Content Brief, Media Pitch, Change Management
|
||||
Plan, Workshop Facilitation Guide, Sales Forecasting Model, Tax Planning Checklist.
|
||||
|
||||
---
|
||||
|
||||
Earlier releases (v1.0.0 – v5.0.0) predate this changelog. See the
|
||||
[article series](README.md#-the-article-series) for the full history of how the
|
||||
library grew from the first PM toolkit to 100+ skills.
|
||||
|
||||
[Unreleased]: https://github.com/mohitagw15856/pm-claude-skills/compare/v14.0.0...HEAD
|
||||
[14.0.0]: https://github.com/mohitagw15856/pm-claude-skills/releases
|
||||
@@ -18,12 +18,15 @@ A community-built library of Claude Skills for professionals across every field
|
||||
## Contents
|
||||
|
||||
- [🚀 Quick Install](#-quick-install-2-minutes)
|
||||
- [🔌 Works With — Cross-Tool Compatibility](#-works-with--cross-tool-compatibility)
|
||||
- [🌐 Skill Playground — try any skill in your browser](#-skill-playground--try-any-skill-in-your-browser)
|
||||
- [📦 Plugin Directory](#-plugin-directory)
|
||||
- [🤖 Building Blocks for Agent Templates](#-building-blocks-for-agent-templates)
|
||||
- [🏷️ Skill Tiers — start with the strongest](#️-skill-tiers--start-with-the-strongest)
|
||||
- [🗂️ All 167 Skills](#️-all-167-skills)
|
||||
- [📋 Changelog](#-changelog)
|
||||
- [🤝 Contributing](#-contributing--add-your-skill)
|
||||
- [🔗 Related Projects](#-related-projects)
|
||||
|
||||
---
|
||||
|
||||
@@ -74,6 +77,31 @@ ln -s ~/pm-claude-skills/skills/* ~/.claude/skills/
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Works With — Cross-Tool Compatibility
|
||||
|
||||
These skills were built for Claude Code, but they aren't locked to it. Each `SKILL.md` is
|
||||
two portable parts: a small **frontmatter** block (`name` + `description`) and a
|
||||
**markdown body** that is just a well-structured set of instructions and output templates.
|
||||
The body is plain English — so it works anywhere a capable model reads instructions.
|
||||
|
||||
| Platform | How it works | Auto-trigger? |
|
||||
|---|---|---|
|
||||
| **Claude Code** (CLI / desktop / web / IDE) | Native. Install via the plugin marketplace; Claude loads a skill automatically when your request matches its description. | ✅ Yes |
|
||||
| **Claude.ai & Claude API** | Upload a skill, or paste the body in as a system prompt / project instruction. | ⚙️ Manual |
|
||||
| **Other coding agents that read the `SKILL.md` format** (e.g. Codex, Gemini CLI, Cursor) | Point the agent at the skill folder, or paste the body. The frameworks are tool-agnostic; only the auto-discovery mechanism differs per tool. | ⚙️ Varies by tool |
|
||||
| **General chat LLMs** (ChatGPT, Gemini, Copilot, etc.) | Copy the body of any `SKILL.md` into a custom instruction / system prompt / custom GPT. You keep the full framework and output format. | ❌ Paste per use |
|
||||
|
||||
**What's verified vs. what varies:** the skill **bodies** — the frameworks, rubrics, and
|
||||
output templates that do the actual work — are model-agnostic and have been used across
|
||||
Claude and other chat LLMs. What's **Claude Code-specific** is the convenience layer:
|
||||
plugin install, automatic skill discovery from the `description`, and the helper-script
|
||||
invocation flow. On other tools you copy the body in manually and lose only the
|
||||
auto-triggering, not the substance.
|
||||
|
||||
> Prefer ChatGPT? There's also a [companion Custom GPT library](#-companion-repository--chatgpt-custom-gpts) built from the same frameworks.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Skill Playground — Try Any Skill in Your Browser
|
||||
|
||||
**▶ Live: [mohitagw15856.github.io/pm-claude-skills](https://mohitagw15856.github.io/pm-claude-skills/)**
|
||||
@@ -225,6 +253,8 @@ More templates will follow. If you want to contribute one, see the [template con
|
||||
|
||||
## 📋 Changelog
|
||||
|
||||
The highlights are below. For the structured, [Keep a Changelog](https://keepachangelog.com/)-format history (including unreleased changes), see **[CHANGELOG.md](CHANGELOG.md)**.
|
||||
|
||||
<details>
|
||||
<summary><strong>Release history — v6.0.0 → v14.0.0</strong> (click to expand)</summary>
|
||||
|
||||
@@ -398,6 +428,21 @@ This repo was built alongside a published article series. Read the full story:
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ Skill Tiers — Start With the Strongest
|
||||
|
||||
A 170+ skill library doesn't have 170 equally-mature skills, and pretending otherwise
|
||||
wastes your time. Skills are tiered honestly so you can start with the best work:
|
||||
|
||||
- 🟢 **Production-Ready (46)** — battle-tested, stable output, used in real work. Includes the three skills with computed Python helpers (sprint planning, RICE, customer health). **Start here.**
|
||||
- 🔵 **Stable** — solid, reliable, well-structured; the default for most of the library.
|
||||
- 🟡 **Experimental** — newer or dependent on an external tool/API/scrape (Gemini, Gmail, browser automation, social scraping). Useful, but more setup and more moving parts.
|
||||
|
||||
**👉 Full breakdown: [TIERS.md](TIERS.md)** — every Production-Ready and Experimental skill listed by name.
|
||||
|
||||
If you're new, install `pm-essentials` and try a couple of Production-Ready skills before going wide.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ All 167 Skills
|
||||
|
||||
The [Plugin Directory](#-plugin-directory) above summarises every bundle. Expand below for the full per-skill breakdown with folder paths.
|
||||
@@ -773,6 +818,8 @@ description: "One sentence. Use when [trigger condition]. Produces [output descr
|
||||
- Produces consistent, structured output
|
||||
- Works without needing extensive setup or context
|
||||
|
||||
**Before you submit:** read the **[Skill Authoring Standard](SKILL-AUTHORING-STANDARD.md)** — it documents the exact section structure, frontmatter rules, and quality bar every skill in this library follows (including optional stdlib-only helper scripts).
|
||||
|
||||
**Skills wishlist** (most requested — up for grabs):
|
||||
|
||||
| Skill | Profession | Use Case |
|
||||
@@ -859,6 +906,31 @@ Read the full breakdown: [Part 12 — I Built the Same Skills Library for ChatGP
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Projects
|
||||
|
||||
Claude Skills is a fast-growing open ecosystem. If this library doesn't have what you
|
||||
need, these community projects are worth a look — and if you maintain one of the lists
|
||||
below, a reciprocal link is always welcome. 🙌
|
||||
|
||||
**Other skill libraries**
|
||||
|
||||
- **[alirezarezvani/claude-skills](https://github.com/alirezarezvani/claude-skills)** — a large engineering-leaning library (300+ skills, agents, and commands) with explicit multi-tool support across Claude Code, Codex, Gemini CLI, Cursor, and more.
|
||||
|
||||
**Curated "awesome" lists** (great for discovery)
|
||||
|
||||
- **[hesreallyhim/awesome-claude-code](https://github.com/hesreallyhim/awesome-claude-code)** — the broad list of skills, hooks, slash-commands, and plugins for Claude Code.
|
||||
- **[travisvn/awesome-claude-skills](https://github.com/travisvn/awesome-claude-skills)** — curated Claude Skills, resources, and tools.
|
||||
- **[karanb192/awesome-claude-skills](https://github.com/karanb192/awesome-claude-skills)** — verified skills for Claude Code, Claude.ai, and the API.
|
||||
- **[ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills)** — skills and tools for customizing Claude workflows.
|
||||
|
||||
**From this author**
|
||||
|
||||
- **[professional-gpt-library](https://github.com/mohitagw15856/professional-gpt-library)** — the same frameworks rebuilt as ChatGPT Custom GPTs.
|
||||
|
||||
> Maintain a Claude Skills project and want to be listed here? [Open a PR](../../pulls) or an [issue](../../issues).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Custom Skills for Your Team
|
||||
|
||||
The 155 skills in this library are built for general professional workflows. But the most powerful version of Claude Skills is one built specifically for *your* team — your templates, your terminology, your processes, your quality standards.
|
||||
|
||||
+9
-4
@@ -10,9 +10,12 @@ That said, security matters here in two specific ways: **skill file safety** and
|
||||
|
||||
| Version | Supported |
|
||||
|---|---|
|
||||
| v4.0.0 (latest) | ✅ Active |
|
||||
| v3.0.0 | ✅ Security fixes only |
|
||||
| < v3.0.0 | ❌ No longer supported |
|
||||
| v14.x (latest) | ✅ Active |
|
||||
| v12.x – v13.x | ✅ Security fixes only |
|
||||
| < v12.0.0 | ❌ No longer supported |
|
||||
|
||||
Because skills are plain markdown, "support" means we review and correct any reported
|
||||
safety issue (prompt injection, unsafe instructions) in the listed versions.
|
||||
|
||||
## Skill File Safety
|
||||
|
||||
@@ -24,7 +27,9 @@ All skills in this repo are reviewed before merging to ensure they:
|
||||
- Do not contain malicious commands disguised as skill instructions
|
||||
- Do not include hardcoded credentials, API keys, or personally identifiable information
|
||||
|
||||
**If you are installing skills from this repo:** skills are plain text markdown files. They do not execute code, make network requests, or access your file system on their own. Review any skill file before installing if you have concerns.
|
||||
**If you are installing skills from this repo:** the skills themselves are plain markdown instruction files. They do not execute code, make network requests, or access your file system on their own. Review any skill file before installing if you have concerns.
|
||||
|
||||
**A few skills ship optional helper scripts** (in a `scripts/` folder, e.g. the sprint, RICE, and customer-health calculators). These are pure Python standard-library programs — no third-party dependencies, no network calls, no file writes outside what you pass them. They only run when you explicitly invoke them. Read any script before running it, exactly as you would any code from the internet.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Skill Authoring Standard
|
||||
|
||||
This is the canonical structure every skill in this library follows. It exists so
|
||||
that 160+ skills feel like one coherent product rather than a folder of loose prompts,
|
||||
and so contributors know exactly what "done" looks like. If you are adding or editing a
|
||||
skill, match this standard.
|
||||
|
||||
It complements [CONTRIBUTING.md](CONTRIBUTING.md) (how to submit) — this document is
|
||||
about *what a good skill contains*.
|
||||
|
||||
---
|
||||
|
||||
## 1. File layout
|
||||
|
||||
```
|
||||
skills/
|
||||
your-skill-name/
|
||||
SKILL.md # required — the skill itself
|
||||
scripts/ # optional — stdlib-only helper programs
|
||||
your_helper.py
|
||||
```
|
||||
|
||||
- One skill per folder. Folder name = skill name = `name` in the frontmatter.
|
||||
- Use lowercase, hyphenated names (`customer-journey-map`, not `CustomerJourneyMap`).
|
||||
- A skill must be useful with `SKILL.md` alone. Scripts are an enhancement, never a
|
||||
prerequisite.
|
||||
|
||||
## 2. Frontmatter (required)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: your-skill-name
|
||||
description: "One sentence on what it does. Use when [trigger conditions]. Produces [the concrete output]."
|
||||
---
|
||||
```
|
||||
|
||||
The `description` is the single most important line — it is all the model sees when
|
||||
deciding whether to load the skill. It must contain three things:
|
||||
|
||||
1. **What** the skill does, in one clause.
|
||||
2. **Use when…** — explicit trigger phrases a user would actually say.
|
||||
3. **Produces…** — the concrete artifact, so the model knows the payoff.
|
||||
|
||||
Keep it under ~3 sentences. Write triggers from the user's vocabulary, not internal jargon.
|
||||
|
||||
## 3. Body sections
|
||||
|
||||
Use this section order. Not every skill needs every section, but strong skills include
|
||||
most of them, and the **bold** ones are required.
|
||||
|
||||
| Section | Purpose |
|
||||
|---|---|
|
||||
| `# Skill Title` + one-line summary | **Required.** Restate the value in plain language. |
|
||||
| **What This Skill Produces** | Bullet list of the deliverables. Sets expectations. |
|
||||
| **Required Inputs** | What to ask the user for if it isn't provided. Prevents guessing. |
|
||||
| Framework / Formula / Scale | The method, rubric, weights, or formula the skill applies. |
|
||||
| Programmatic Helper | If the skill has a script, show how to run it and what it returns. |
|
||||
| **Output Format** | A concrete template (headings, tables) of the final artifact. |
|
||||
| **Quality Checks** | A checklist the output must pass before it's handed over. |
|
||||
| **Anti-Patterns** | Explicit "Do not…" rules — the mistakes this skill prevents. |
|
||||
|
||||
## 4. Quality bar
|
||||
|
||||
A skill is ready to merge when:
|
||||
|
||||
- [ ] The `description` has all three parts (what / use when / produces).
|
||||
- [ ] It solves a **recurring** professional workflow, not a one-off task.
|
||||
- [ ] It asks for missing inputs rather than inventing them.
|
||||
- [ ] The output format is concrete enough that two runs look like the same product.
|
||||
- [ ] It includes **Quality Checks** and **Anti-Patterns** — these are what make a skill
|
||||
trustworthy, not just a prompt.
|
||||
- [ ] It works with no setup beyond reading the file (scripts excepted, and those are
|
||||
stdlib-only).
|
||||
|
||||
## 5. Helper scripts (optional)
|
||||
|
||||
Some skills ship a `scripts/` folder that computes part of the work. Rules:
|
||||
|
||||
- **Standard library only.** No `pip install`. No third-party imports.
|
||||
- **No network access, no surprise file writes.** Read input, print output.
|
||||
- Accept input via flags *and* JSON (file or stdin); offer `--json` output for chaining.
|
||||
- Include a module docstring with runnable examples and a `--help` via `argparse`.
|
||||
- The script augments the skill — the SKILL.md must still produce a good result without it.
|
||||
|
||||
See `skills/rice-prioritisation/scripts/rice_calculator.py` for a reference example.
|
||||
|
||||
## 6. Tone and safety
|
||||
|
||||
- Write instructions *to the model* ("Ask for…", "Flag any…", "Never write…").
|
||||
- British or American spelling is fine; be consistent within a skill.
|
||||
- No prompt injection, no instructions to override model guidelines, no requests to
|
||||
collect or transmit user data. See [SECURITY.md](SECURITY.md).
|
||||
|
||||
## 7. Tiering
|
||||
|
||||
New skills enter as **Experimental**. Once a skill has a stable output format, quality
|
||||
checks, and real-world use, it can be promoted to **Stable** or **Production-Ready** in
|
||||
[TIERS.md](TIERS.md). Tiering is honest signposting, not a value judgement on effort.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Skill Tiers
|
||||
|
||||
Not every skill in a 170+ library is at the same level of maturity — and pretending
|
||||
otherwise wastes your time. This page tiers the skills honestly so you can start with the
|
||||
strongest work and know what to expect from the rest.
|
||||
|
||||
| Tier | What it means |
|
||||
|---|---|
|
||||
| 🟢 **Production-Ready** | Battle-tested, stable output format, used in real work. Includes the skills with computed helper scripts. Start here. |
|
||||
| 🔵 **Stable** | Solid and well-structured. Reliable output; smaller track record than Production-Ready. This is the default tier for most of the library. |
|
||||
| 🟡 **Experimental** | Newer, niche, or dependent on an external tool/API/scrape (Gemini, Gmail, browser automation, social scraping). Useful, but more setup and more moving parts — expect rough edges. |
|
||||
|
||||
> ⚙️ = ships a stdlib-only Python helper script that computes part of the work.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Production-Ready (46)
|
||||
|
||||
These are the skills to reach for first — the most-used, most-refined frameworks in the
|
||||
library.
|
||||
|
||||
**Product core**
|
||||
`prd-template` · `meeting-notes` · `stakeholder-update` · `user-research-synthesis` · `competitive-analysis`
|
||||
|
||||
**Prioritisation & planning**
|
||||
`rice-prioritisation` ⚙️ · `feature-prioritisation` · `okr-builder` · `roadmap-narrative` · `rice-impact-matrix`
|
||||
|
||||
**Delivery**
|
||||
`sprint-planning` ⚙️ · `sprint-brief` · `user-story-writer` · `retro-analysis` · `ab-test-planner` · `product-launch-checklist` · `technical-spec-template`
|
||||
|
||||
**Discovery**
|
||||
`customer-journey-map` · `assumption-mapper` · `user-interview-synthesis` · `discovery-interview-guide` · `job-story-mapper`
|
||||
|
||||
**Data & analytics**
|
||||
`data-analysis-standard` · `retention-analysis` · `cohort-analysis` · `metrics-framework` · `product-health-analysis`
|
||||
|
||||
**Customer success**
|
||||
`cs-health-scorecard` ⚙️ · `churn-analysis` · `qbr-deck` · `renewal-playbook` · `customer-success-plan` · `cs-escalation-brief`
|
||||
|
||||
**Engineering**
|
||||
`code-review-checklist` · `incident-postmortem` · `architecture-decision-record` · `api-docs-writer` · `runbook-writer` · `changelog-generator` · `pr-description-writer` · `technical-debt-register`
|
||||
|
||||
**GTM & strategy**
|
||||
`go-to-market` · `competitor-teardown` · `product-positioning-doc`
|
||||
|
||||
**Cross-profession**
|
||||
`executive-summary` · `press-release`
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Experimental
|
||||
|
||||
These depend on external services, scraping, or browser/desktop automation. They can be
|
||||
genuinely useful, but they have more setup and more failure modes than a self-contained
|
||||
markdown skill — treat output as a strong draft, and expect to adapt them to your
|
||||
environment.
|
||||
|
||||
| Skill | Why it's experimental |
|
||||
|---|---|
|
||||
| `instagram-post-downloader` | Depends on Instagram's page structure; can break when the site changes. |
|
||||
| `substack-notes-scraper` | Scrapes Substack engagement data; fragile to layout changes. |
|
||||
| `thumbnail-creator` | Requires a Gemini API key and image generation. |
|
||||
| `notebooklm-connector` | Drives NotebookLM via a Chrome extension / browser automation. |
|
||||
| `email-triage` | Requires Gmail access and a configured time window. |
|
||||
| `morning-intelligence` | Designed for scheduled-task / routine setups; depends on your news sources. |
|
||||
| `last-30-days-research` | Relies on live Reddit / X / web search availability and quality. |
|
||||
| `competitor-signal-tracker` | Depends on the live sources you point it at. |
|
||||
| `multi-source-signal-synthesiser` | Quality depends on the breadth/quality of sources supplied. |
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Stable (everything else)
|
||||
|
||||
Every skill not listed above is **Stable**: well-structured, reliable output, broadly
|
||||
useful — just with a shorter track record than the Production-Ready set. Browse the full
|
||||
list in the [README](README.md#️-all-167-skills).
|
||||
|
||||
---
|
||||
|
||||
*Tiers are reviewed as skills mature. New skills enter as Experimental and are promoted
|
||||
once they have a stable output format and real-world use — see
|
||||
[SKILL-AUTHORING-STANDARD.md](SKILL-AUTHORING-STANDARD.md#7-tiering). Think a skill is
|
||||
mis-tiered? [Open an issue](../../issues).*
|
||||
@@ -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())
|
||||
@@ -53,6 +53,22 @@ Availability factor: 0.7–0.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())
|
||||
@@ -55,6 +55,7 @@ cp skills/feature-prioritisation/SKILL.md plugins/pm-planning/skills/feature-
|
||||
cp skills/roadmap-presentation/SKILL.md plugins/pm-planning/skills/roadmap-presentation/SKILL.md
|
||||
cp skills/pricing-strategy/SKILL.md plugins/pm-planning/skills/pricing-strategy/SKILL.md
|
||||
cp skills/rice-prioritisation/SKILL.md plugins/pm-planning/skills/rice-prioritisation/SKILL.md
|
||||
cp -r skills/rice-prioritisation/scripts plugins/pm-planning/skills/rice-prioritisation/ 2>/dev/null || true
|
||||
cp skills/roadmap-narrative/SKILL.md plugins/pm-planning/skills/roadmap-narrative/SKILL.md
|
||||
cp skills/rice-impact-matrix/SKILL.md plugins/pm-planning/skills/rice-impact-matrix/SKILL.md
|
||||
|
||||
@@ -73,6 +74,7 @@ mkdir -p plugins/pm-delivery/skills/sprint-brief
|
||||
mkdir -p plugins/pm-delivery/skills/retro-analysis
|
||||
|
||||
cp skills/sprint-planning/SKILL.md plugins/pm-delivery/skills/sprint-planning/SKILL.md
|
||||
cp -r skills/sprint-planning/scripts plugins/pm-delivery/skills/sprint-planning/ 2>/dev/null || true
|
||||
cp skills/technical-spec-template/SKILL.md plugins/pm-delivery/skills/technical-spec-template/SKILL.md
|
||||
cp skills/ab-test-planner/SKILL.md plugins/pm-delivery/skills/ab-test-planner/SKILL.md
|
||||
cp skills/go-to-market-planner/SKILL.md plugins/pm-delivery/skills/go-to-market-planner/SKILL.md
|
||||
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -53,6 +53,22 @@ Availability factor: 0.7–0.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())
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user