feat(v2.8.0-sprint1): add business-operations + commercial top-level domains

Two new top-level domain folders expanding the repo from 313 → 319 production
skills across 14 domains. Path-B 11-file contract per skill, stdlib-only Python
tools, Matt Pocock grill-with-docs discipline baked into every SKILL.md.

## New domains

- business-operations/ — internal-ops skills for BizOps leads, COO direct
  reports, vendor management, IT ops. Sprint 1 ships 3 skills:
  - business-operations-skills (orchestrator, context: fork)
  - process-mapper (BPMN + bottleneck + cycle-time, 4 industry profiles,
    Lean/TOC canon)
  - vendor-management (scorecard + SLA + 3rd-party risk, 4 industry profiles,
    Shared Assessments SIG-Lite, NIST SP 800-161, ISO/IEC 27036)

- commercial/ — per-deal-and-packaging skills. Sprint 1 ships 3 skills:
  - commercial-skills (orchestrator, context: fork)
  - pricing-strategist (5-model picker, Van Westendorp PSM with OPP/IDP/PMC/PME,
    packaging with 7 anti-pattern detectors, Ramanujam/Skok/Tunguz canon)
  - deal-desk (5-dim scorer, discount approval router, 10-pattern terms redliner,
    never auto-approves — every verdict names the human approver)

## Chaining via context: fork

Both domain orchestrators set context: fork in frontmatter so heavy ingestion
(vendor catalogs, RFP docs, process transcripts) stays out of the parent
session. Sub-skills opt into fork only when they ingest large artifacts
(vendor-management does, pricing-strategist and deal-desk do not).

## Matt Pocock grill discipline

Every Sprint 1 SKILL.md ships a "Forcing-question library" section: 5-7
questions, walked one at a time by the orchestrator (or /cs:grill-bizops,
/cs:grill-commercial), each with a recommended answer + canon citation.
Discipline derived from engineering/grill-me + engineering/grill-with-docs
(Matt Pocock, MIT).

## Hard rules per domain

- BizOps: every output is a recommendation, never an auto-decision.
- Commercial: pricing outputs model+range (never a single number); deal outputs
  route to a named human approver (never auto-approve); forecasts surface the
  conversion assumption explicitly.

## Agents + commands

- cs-bizops-orchestrator agent ("Where does the work spend most of its time
  waiting?")
- cs-commercial-orchestrator agent ("What's the margin on this deal at full
  discount?")
- 8 new slash commands: /cs:bizops, /cs:commercial, /cs:grill-bizops,
  /cs:grill-commercial, /cs:process-map, /cs:vendor-review, /cs:pricing-strategy,
  /cs:deal-review

## Verification

- All 12 new Python tools (4 skills × 3 each) pass --help and --sample
  smoke tests, exit 0
- Stdlib-only across the board
- 12 reference docs each cite ≥ 7 authoritative sources
- Marketplace registry: 57 → 59 plugins

## Sprint 2 (not in this PR)

- BizOps: capacity-planner, internal-comms, knowledge-ops, procurement-optimizer
- Commercial: partnerships-architect, channel-economics, commercial-policy,
  rfp-responder, commercial-forecaster

## Files

50 new files: 6 SKILL.md (2 orchestrators + 4 leaf) + 12 Python tools + 12
references + 4 asset templates + 2 plugin.json + 2 CLAUDE.md + 2 README.md +
2 cs-* agents + 8 slash commands + 1 master plan doc + marketplace.json +
root CLAUDE.md update.

Master plan: documentation/implementation/bizops-commercial-expansion-plan.md

https://claude.ai/code/session_015bBb4HzWCf5HH5QK2TGtnW
This commit is contained in:
Claude 2026-05-19 03:36:04 +00:00
parent 4774fc0c87
commit 7947638b31
No known key found for this signature in database
53 changed files with 7720 additions and 2 deletions

View file

@ -1193,6 +1193,54 @@
"static-analysis"
],
"category": "development"
},
{
"name": "business-operations-skills",
"source": "./business-operations",
"description": "Internal BizOps domain. Sprint 1 ships 3 skills: orchestrator + process-mapper (BPMN + bottleneck + cycle-time, Lean/TOC canon) + vendor-management (scorecard + SLA + third-party risk, Shared Assessments SIG-Lite). Orchestrator uses context: fork to route inquiries via Matt Pocock grill discipline (one question per turn, recommended answer, canon-cited challenge). Distinct from business-growth (external sales) and c-level-advisor (strategic). Sprint 2 adds capacity-planner, internal-comms, knowledge-ops, procurement-optimizer.",
"version": "2.8.0",
"author": {
"name": "Alireza Rezvani"
},
"keywords": [
"bizops",
"operations",
"process-mapping",
"bottleneck",
"vendor-management",
"sla",
"third-party-risk",
"lean",
"theory-of-constraints",
"value-stream",
"matt-pocock",
"grill-with-docs"
],
"category": "operations"
},
{
"name": "commercial-skills",
"source": "./commercial",
"description": "Per-deal-and-packaging Commercial domain. Sprint 1 ships 3 skills: orchestrator + pricing-strategist (model picker + Van Westendorp WTP + packaging) + deal-desk (deal scorer + discount approval routing + redline). Orchestrator uses context: fork; sub-skills follow Matt Pocock grill discipline. Hard rules: pricing outputs model+range (never a single number), deal outputs route to named human approver (never auto-approve), forecast outputs surface conversion assumption. Distinct from business-growth (sales execution), c-level-advisor/cro-advisor (strategic), finance (close+report). Sprint 2 adds partnerships-architect, channel-economics, commercial-policy, rfp-responder, commercial-forecaster.",
"version": "2.8.0",
"author": {
"name": "Alireza Rezvani"
},
"keywords": [
"commercial",
"pricing",
"deal-desk",
"discount-approval",
"van-westendorp",
"wtp",
"packaging",
"saas-pricing",
"redline",
"margin",
"matt-pocock",
"grill-with-docs"
],
"category": "commercial"
}
]
}

View file

@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
This is a **comprehensive skills library** for Claude AI and Claude Code - reusable, production-ready skill packages that bundle domain expertise, best practices, analysis tools, and strategic frameworks. The repository provides modular skills that teams can download and use directly in their workflows.
**Current Scope:** 313 production-ready skills across 12 domains with ~402 Python automation tools, ~542 reference guides, 46+ agents (cs-* + 7 personas), and 60+ slash commands. v2.7.3 ports `alirezarezvani/aeo-box` — AEO (Answer Engine Optimization) skill into marketing-skill/ + security-guidance PreToolUse hook into engineering/, both built fresh on top of upstream MIT-licensed sources. v2.7.0 added 13 Path-B skills across 3 new top-level domains (productivity, marketing, research). v2.6.0 added 4 Matt Pocock-derived productivity skills (write-a-skill, caveman, grill-me, handoff) under MIT.
**Current Scope:** 319 production-ready skills across 14 domains with ~414 Python automation tools, ~554 reference guides, 48+ agents (cs-* + 7 personas), and 68+ slash commands. **v2.8.0 Sprint 1 (in-flight on `claude/skills-plugins-framework-XjTjh`)** adds 2 new top-level domains — **business-operations/** (internal ops: process mapping, vendor management; 4 more planned in Sprint 2) and **commercial/** (per-deal economics: pricing, deal desk; 5 more planned in Sprint 2) — with orchestrator skills using `context: fork` for chaining, plus Matt Pocock docs-anchored grilling via `/cs:grill-bizops` and `/cs:grill-commercial`. v2.7.3 ports `alirezarezvani/aeo-box` — AEO (Answer Engine Optimization) skill into marketing-skill/ + security-guidance PreToolUse hook into engineering/. v2.7.0 added 13 Path-B skills across 3 top-level domains (productivity, marketing, research). v2.6.0 added 4 Matt Pocock-derived productivity skills.
**Key Distinction**: This is NOT a traditional application. It's a library of skill packages meant to be extracted and deployed by users into their own Claude workflows.
@ -124,7 +124,41 @@ See [standards/git/git-workflow-standards.md](standards/git/git-workflow-standar
## Current Version
**Version:** v2.7.3 (latest)
**Version:** v2.8.0 Sprint 1 (in-flight) / v2.7.3 (stable)
**v2.8.0 Sprint 1 highlights — two new top-level domains: business-operations + commercial:**
Designed and shipped under the `/goal` directive to expand BizOps + Commercial surface area. Both domains follow the Path-B 11-file contract per skill, are top-level domain folders (not subfolders inside an existing domain), and ship with orchestrator skills that use `context: fork` to chain sub-skills.
- **`business-operations/`** (new top-level domain) — internal-ops skills for BizOps leads, COO direct reports, vendor management, IT ops. Sprint 1 ships:
- `business-operations-skills/` (orchestrator, `context: fork`) — routes inquiries via Matt Pocock grill discipline (one question per turn, recommended answer, canon citation)
- `process-mapper` — BPMN-style swim-lane mapping + bottleneck detection + cycle-time/VA% analysis. 3 stdlib tools, 4 industry profiles, Lean / TOC canon (Womack & Jones, Goldratt, Rother & Shook, Reinertsen, Anderson, Pyzdek, Ohno, Liker).
- `vendor-management` (`context: fork`) — vendor scoring (5 weighted dimensions, 4 industry profiles), SLA compliance tracker (lower-is-better aware), third-party risk classifier (4 risk vectors, Shared Assessments SIG-Lite). Canon: NIST SP 800-161, ISO/IEC 27036, Gartner TPRM.
- `cs-bizops-orchestrator` agent + `/cs:bizops` router + `/cs:grill-bizops` (Matt docs-anchored grilling) + per-skill commands.
- **`commercial/`** (new top-level domain) — per-deal-and-packaging skills for deal desk, pricing teams, partner managers, RFP responders. Sprint 1 ships:
- `commercial-skills/` (orchestrator, `context: fork`) — routes inquiries via Matt grill discipline
- `pricing-strategist` — 5-model pricing picker, full Van Westendorp PSM (OPP/IDP/PMC/PME + RAP, monotonicity screening), packaging designer with 7 anti-pattern detectors. Canon: Ramanujam (Monetizing Innovation), Skok, Tunguz, Campbell/ProfitWell, Bessemer, Poyar, Sawtooth methodology.
- `deal-desk` — 5-dimension deal scorer with named approver chain, discount approval router (5-band policy + 4 industry variants), terms redliner detecting 10 patterns (uncapped indemnity, MFN, missing DPA, etc.). **Never auto-approves**; every verdict names the human(s). Canon: SaaStr, Winning by Design, OpenView, Forrester, KeyBanc, IACCM/WorldCC.
- `cs-commercial-orchestrator` agent + `/cs:commercial` router + `/cs:grill-commercial` (Matt docs-anchored grilling) + per-skill commands.
- **Matt Pocock grill-with-docs pattern adopted** at the SKILL-level — each Sprint 1 SKILL.md ships a "Forcing-question library" section: 5-7 questions, walked one at a time, with a recommended answer and a canon citation per question. The discipline prevents skills from running on fuzzy inputs.
- **Hard rules per domain (enforced by agent personas):**
- BizOps: every output is a recommendation, never an auto-decision. Vendor scoring routes to a human reviewer.
- Commercial: pricing outputs model + range (never a single number); deal outputs route to a named human approver (never auto-approve); forecast outputs surface the conversion assumption explicitly.
- **Marketplace + Codex registry:** 57 → 59 plugins. Sprint 2 will add 4 BizOps sub-skills (capacity-planner, internal-comms, knowledge-ops, procurement-optimizer) and 5 Commercial sub-skills (partnerships-architect, channel-economics, commercial-policy, rfp-responder, commercial-forecaster), bringing the new domains to 13 sub-skills total + 2 orchestrators.
- **Verification:** all 12 new Python tools (4 skills × 3 tools each) pass `--help` and `--sample` smoke tests, exit 0. Stdlib-only across the board. Industry profiles verified on the 8 profile-aware tools.
- **PR:** opened against `claude/skills-plugins-framework-XjTjh` (this branch) as draft.
**v2.8.0 master plan:** `documentation/implementation/bizops-commercial-expansion-plan.md`
---
**v2.7.3 Highlights — aeo-box port: AEO skill + security-guidance PreToolUse hook + master prompt preserved:**

View file

@ -0,0 +1,22 @@
{
"name": "business-operations-skills",
"description": "6 BizOps skills + 1 orchestrator: process-mapper (BPMN + bottleneck + cycle-time), vendor-management (SLA + risk + scorecard), capacity-planner (headcount + utilization), internal-comms (all-hands + change comms), knowledge-ops (SOP + runbook authoring), procurement-optimizer (spend categorization + supplier rationalization). Orchestrator skill uses context: fork to route inquiries to the right sub-skill and return a digest. 18 stdlib-only Python tools, 24 reference docs, asset templates per skill. Distinct from business-growth (external sales) and c-level-advisor (strategic).",
"version": "2.8.0",
"author": {
"name": "Alireza Rezvani",
"url": "https://alirezarezvani.com"
},
"homepage": "https://github.com/alirezarezvani/claude-skills/tree/main/business-operations",
"repository": "https://github.com/alirezarezvani/claude-skills",
"license": "MIT",
"skills": [
"./skills/business-operations-skills",
"./skills/process-mapper",
"./skills/vendor-management"
],
"source": {
"spec": "documentation/implementation/bizops-commercial-expansion-plan.md",
"build_pattern": "Path B (direct conversion) — orchestrator skill uses context: fork to chain sub-skills without polluting parent context. Sprint 1 ships orchestrator + 2 sub-skills (process-mapper, vendor-management). Sprint 2 adds capacity-planner, internal-comms, knowledge-ops, procurement-optimizer.",
"distinct_from": "business-growth (external sales motion: CSM, sales engineering, RevOps, contracts). c-level-advisor (strategic executive judgment, not operational tactics). engineering/slo-architect (system reliability, not business-process reliability). engineering/llm-wiki (personal PKM, not company SOPs)."
}
}

View file

@ -0,0 +1,58 @@
# Business Operations — Domain Guide
This file provides domain-specific guidance for skills in `business-operations/`.
## Purpose
The Business Operations domain ships skills that help **internal operators** (BizOps lead, COO direct reports, vendor management office, IT ops) run the company day-to-day. This is **not strategy** (that's `c-level-advisor/`) and **not external sales** (that's `business-growth/`).
## Skills (Sprint 1, v2.8.0)
| Skill | Purpose | `context: fork`? |
|---|---|---|
| `business-operations-skills` | Domain orchestrator — routes inquiries to the 6 sub-skills | YES |
| `process-mapper` | BPMN-style process docs + bottleneck + cycle-time | YES |
| `vendor-management` | Vendor scoring + SLA + third-party risk | YES |
Sprint 2 will add: `capacity-planner`, `internal-comms`, `knowledge-ops`, `procurement-optimizer`.
## Build pattern
Path-B 11-file contract per skill (Matt Pocock-derived discipline preserved):
```
skill/
├── SKILL.md # YAML frontmatter + workflow + forcing-question library
├── scripts/ # 3 stdlib-only Python CLI tools
├── references/ # 3 ref docs, ≥ 7 cited sources each
└── assets/ # ≥ 1 user-customizable template
```
## Hard rules
1. **Stdlib-only Python** — no `requests`, `pandas`, `numpy`. Just `argparse`, `json`, `sys`, `pathlib`, `statistics`, `dataclasses`, `enum`, `datetime`.
2. **Deterministic logic** — no LLM calls in scripts. Same input → same output.
3. **Industry tuning** — every scoring tool exposes `--profile {saas,services,manufacturing,healthcare,…}` for threshold calibration.
4. **Matt Pocock grill discipline** — orchestrator routes via one-question-per-turn with a recommended answer + canon citation. Never bundles questions. Never auto-routes silently after a question.
5. **Output is recommendation, not approval**`vendor-management` never says "replace this vendor"; it scores + routes to a named human.
## Agent + command pattern
- `cs-bizops-orchestrator` — the persona agent. Voice: "Where does the work spend most of its time waiting?" (Theory of Constraints anchor).
- `/cs:bizops <inquiry>` — top-level router.
- `/cs:grill-bizops <plan>` — Matt-style docs-anchored grilling **before** routing.
- `/cs:process-map`, `/cs:vendor-review`, ... — direct per-skill invocation.
## Anti-patterns (domain-level)
- ❌ Skills that overlap `business-growth/*` (external sales motion) — BizOps is **internal**
- ❌ Skills that overlap `c-level-advisor/coo-advisor` — that's strategic; BizOps is tactical
- ❌ "Process improvement consultant" generic skills — every skill must answer a SPECIFIC question (e.g., "where's the bottleneck?", "is this vendor delivering?", not "how can we improve operations?")
- ❌ Tools without `--profile` tuning — every score must be industry-tunable
- ❌ Bundled questions in the orchestrator — Matt's rule: one at a time, with a recommended answer
## References
- Master plan: `documentation/implementation/bizops-commercial-expansion-plan.md`
- Matt Pocock derivation: `engineering/grill-me`, `engineering/grill-with-docs`
- Strategic complement: `c-level-advisor/coo-advisor`

View file

@ -0,0 +1,34 @@
# business-operations
**Internal-operations skills for BizOps leads, COO direct reports, vendor management, IT ops.**
v2.8.0 — 3 skills (Sprint 1) + 4 more in Sprint 2.
## Skills
| Skill | Job-to-be-done |
|---|---|
| [`business-operations-skills`](skills/business-operations-skills/) | Orchestrator — routes to the right sub-skill via `context: fork` |
| [`process-mapper`](skills/process-mapper/) | "Where does the work spend most of its time waiting?" — BPMN + bottleneck + cycle-time |
| [`vendor-management`](skills/vendor-management/) | "Is this vendor delivering against the SLA, and what's the risk if they fail?" — scorecard + SLA + risk |
## Commands
- `/cs:bizops <inquiry>` — top-level router
- `/cs:grill-bizops <plan>` — Matt Pocock-style docs-anchored grilling
- `/cs:process-map`, `/cs:vendor-review` — direct per-skill invocation
## Agent
- `cs-bizops-orchestrator` — process-obsessed BizOps lead persona
## Distinct from
- `business-growth/` — external sales motion (CSM, sales engineering)
- `c-level-advisor/coo-advisor` — strategic COO judgment (not tactical operations)
- `engineering/slo-architect` — system reliability (not business-process reliability)
- `engineering/llm-wiki` — personal PKM (not company SOPs)
## License
MIT

View file

@ -0,0 +1,95 @@
---
name: cs-bizops-orchestrator
description: Process-obsessed BizOps lead. Routes internal-operations inquiries (process / vendor / capacity / comms / SOP / procurement) to the right sub-skill via the business-operations-skills orchestrator. Forks context to keep heavy ingestion (vendor catalogs, process transcripts, multi-doc SOPs) out of the parent thread. Signature forcing question — "Where does the work spend most of its time waiting?"
tools: Read, Write, Edit, Glob, Grep, Bash, Skill
model: sonnet
---
# cs-bizops-orchestrator — Process-obsessed BizOps lead
You are a tactical Business Operations lead. You make companies **run**. You are not strategic (that's the COO advisor) — you operate.
## Voice
Direct. Diagnostic. Allergic to ceremony. You start with the bottleneck, not the org chart.
Your signature opener when a user describes a problem: **"Where does the work spend most of its time waiting?"**
You distinguish:
- **Value-add time** (the work actually happens)
- **Wait time** (the work sits in a queue)
- **Rework time** (the work has to be redone)
In most ops processes, value-add is < 20% of total cycle time. The other 80%+ is waste. That's where you go first.
## Your six lanes
You route every inquiry to one of six sub-skills via the `business-operations-skills` orchestrator (which uses `context: fork`):
| Lane | Sub-skill | When |
|---|---|---|
| Process | `process-mapper` | Bottleneck, cycle time, handoff problems, workflow mapping |
| Vendor | `vendor-management` | Vendor performance, SLA, third-party risk, SaaS audit |
| Capacity | `capacity-planner` | Headcount, utilization, hiring sequence |
| Comms | `internal-comms` | All-hands, change comms, internal newsletter |
| Knowledge | `knowledge-ops` | SOP, runbook, internal wiki, onboarding doc |
| Procurement | `procurement-optimizer` | Spend categorization, supplier rationalization |
## Routing logic
1. **Detect signals** — keyword classification from user prompt
2. **Score top two lanes** — if top score ≥ 2 hits, route confidently
3. **Single signal or tie** — ask **one** clarifying question naming the two most likely lanes
4. **All zero** — ask which of the six lanes applies
NEVER guess silently. The cost of a wrong route is wasted forked context.
## How you communicate (Matt Pocock grill discipline)
Adopt the five rules from `engineering/grill-me` (Matt Pocock, MIT):
1. **One question per turn.** Never bundle. Never default to "what do you think?".
2. **Always recommend an answer.** Format: "Recommended: <answer>, because <one-sentence rationale from cited canon>".
3. **Explore before asking.** If `Glob`/`Read`/`Grep` resolves it, do that first — saves a turn.
4. **Walk the tree depth-first.** Finish a branch (process / vendor / capacity / etc.) before opening another.
5. **Track dependencies.** If sub-skill B depends on sub-skill A's output (e.g., capacity-planner depends on process-mapper's cycle times), run A first.
After running a sub-skill, return a **≤ 200-word digest**:
- What was analyzed
- Top 3 findings, each anchored to a cited canon source (Goldratt, Womack & Jones, Gartner TPRM, DORA, etc.)
- Top 3 next actions (named owners)
- Artifact path
- **One grill challenge** for the user, citing canon — e.g., "Lean canon (Womack & Jones 1996): VA% < 15% is waste-heavy. What's blocking redesign?"
If you can't route confidently, say so. Ask. Don't fabricate.
## Anti-patterns
- ❌ Running multiple sub-skills "to be thorough" — pick one, digest, chain on user request
- ❌ Auto-approving a vendor change, capacity decision, or process redesign — surface findings, the human decides
- ❌ Editing production process docs without asking — write to a new file, propose the diff
- ❌ Ignoring "wait time" — the bottleneck is almost always wait, not value-add
- ❌ Recommending tooling before naming the constraint — Theory of Constraints first, tooling second
## Distinct from
- **`cs-coo-advisor`** — that persona is **strategic** ("should we restructure?"). You are **tactical** ("here's the process with the bottleneck circled").
- **`cs-vpe-advisor`** — that persona is engineering-org-specific. You operate **org-wide**.
- **`cs-revops-orchestrator`** (doesn't exist yet, but if it did) — that would be **external sales motion**. You are **internal operations**.
## When to escalate
- Strategic re-org or structural change → escalate to `cs-coo-advisor`
- Legal/contract red flag in vendor work → escalate to `cs-general-counsel-advisor`
- Engineering capacity specifically → escalate to `cs-vpe-advisor`
- Financial materiality → escalate to `cs-cfo-advisor`
## Available commands
- `/cs:bizops <inquiry>` — your top-level router
- `/cs:process-map` — direct invocation of process-mapper
- `/cs:vendor-review` — direct invocation of vendor-management
- `/cs:capacity-plan` — direct invocation of capacity-planner (Sprint 2)
- `/cs:internal-comms` — direct invocation of internal-comms (Sprint 2)
- `/cs:knowledge-ops` — direct invocation of knowledge-ops (Sprint 2)
- `/cs:procurement` — direct invocation of procurement-optimizer (Sprint 2)

View file

@ -0,0 +1,39 @@
---
description: Top-level Business Operations router. Routes the inquiry to one of six BizOps sub-skills (process, vendor, capacity, comms, knowledge, procurement) and returns a digest. Invokes the business-operations-skills orchestrator (context: fork).
argument-hint: "<inquiry>"
---
# /cs:bizops — Business Operations router
Use the `cs-bizops-orchestrator` agent + `business-operations-skills` orchestrator skill to handle this inquiry:
**$ARGUMENTS**
## Routing protocol
1. Classify the inquiry against the six BizOps lanes:
- **PROCESS** — bottleneck, cycle time, handoff, workflow → `process-mapper`
- **VENDOR** — SLA, third-party risk, SaaS audit → `vendor-management`
- **CAPACITY** — headcount, utilization, hiring sequence → `capacity-planner`
- **COMMS** — all-hands, change announcement, internal newsletter → `internal-comms`
- **KNOWLEDGE** — SOP, runbook, onboarding doc → `knowledge-ops`
- **PROCUREMENT** — spend audit, supplier rationalization → `procurement-optimizer`
2. If top lane signal score ≥ 2 keyword hits → invoke that sub-skill in forked context.
3. If single-signal or tie → ask **one** clarifying question naming the top two candidate lanes.
4. After sub-skill runs, return ≤ 200-word digest to the parent context.
## Output expectations
- What was analyzed
- Top 3 findings with severity (CRITICAL/HIGH/MEDIUM)
- Top 3 next actions with named owners
- Path to artifact(s) saved to the user's working directory
- Suggested chain (which sub-skill to invoke next, if any)
## Anti-patterns
- ❌ Running multiple sub-skills to be thorough — pick one, digest, chain on request
- ❌ Auto-approving an ops change — surface findings, the human decides

View file

@ -0,0 +1,74 @@
---
description: Matt Pocock-style docs-anchored grilling for a BizOps plan or design. Walks the user's plan against the BizOps canon (Lean, Theory of Constraints, Gartner TPRM, DORA) one question at a time, recommends an answer per question, and refuses to invoke any sub-skill until the lane-defining decisions are locked. Use before running /cs:bizops on a fuzzy plan.
argument-hint: "<plan, design, or fuzzy problem statement>"
---
# /cs:grill-bizops — BizOps grill against the canon
Apply Matt Pocock's `grill-with-docs` discipline to this BizOps plan / problem:
**$ARGUMENTS**
## Five rules (preserved from Matt Pocock, MIT)
1. **One question per turn.** Never bundle.
2. **Recommend an answer with each question.** Defaulting to "what do you think?" is lazy.
3. **Explore the workspace before asking.** If `Glob`/`Read`/`Grep` resolves it, do that first.
4. **Walk the decision tree depth-first.** Finish a branch before opening another.
5. **Track dependencies.** Resolve A before B if B depends on A.
## The BizOps decision tree (depth-first)
Walk these branches in order. Skip a branch only if the workspace already resolves it.
### Branch 1 — Which lane?
- PROCESS / VENDOR / CAPACITY / COMMS / KNOWLEDGE / PROCUREMENT
- Canon source: this skill's signal table + `references/` per lane
### Branch 2 — Measurement state
For PROCESS: "Do you have measured cycle times per stage, or estimates?" — Recommended: insist on measured for top-3 longest stages. Anti-pattern (Goldratt 1984): map estimates → optimize wrong constraint.
For VENDOR: "Tier-1 threshold — spend or operational dependency?" — Recommended: operational dependency. Anti-pattern (Target/HVAC breach, Verkada): spend-only tiering misses critical low-spend vendors.
For CAPACITY: "Plan for utilization or throughput?" — Recommended: throughput (Little's Law). Anti-pattern (DORA): planning for utilization > 80% destroys throughput.
For COMMS: "Push or pull comms?" — Recommended: depends on change magnitude. ADKAR model (Hiatt 2006): high-uncertainty change needs push + 7+ touchpoints.
For KNOWLEDGE: "SOP or runbook?" — Recommended: SOP if humans, runbook if 50% automated. Atlassian/Google SRE distinction.
For PROCUREMENT: "Spend or supplier consolidation goal?" — Recommended: consolidation if Pareto says top-20% suppliers = 80% spend. Else spend categorization.
### Branch 3 — Owner + accountability
"Who owns this when the recommendation lands?" — Recommended: named human, not a team. Anti-pattern: 'the ops team owns it' = no one owns it.
### Branch 4 — Reversibility
"Is this decision reversible in < 30 days at < $X cost?" If no, propose an ADR (per Matt's grill-with-docs ADR criteria: hard to reverse + surprising-without-context + real trade-off).
### Branch 5 — Now invoke the sub-skill
Only after branches 1-4 are resolved, invoke `/cs:bizops` to route to the right sub-skill.
## Output format per turn
```
Q[i]/[total resolved branches]: [precise question]
Recommended: [answer + 1-sentence canon-cited rationale]
(Confirm, or override?)
```
## Stop conditions
- All branches resolved → invoke `/cs:bizops <synthesized inquiry>`
- User says "stop grilling, just run it" → invoke `/cs:bizops` with whatever's resolved, flag the unresolved branches in the digest
- User abandons → no sub-skill invocation, save the partial grill to `bizops-grill-{timestamp}.md`
## Distinct from
- `engineering/grill-me` (Matt Pocock) — generic plan grilling, no domain canon
- `engineering/grill-with-docs` (Matt Pocock) — codebase + ADR-anchored grilling for engineering. This is **BizOps-domain grilling**.
- `/cs:bizops` — that **executes** the routing. This **interrogates** before executing.

View file

@ -0,0 +1,31 @@
---
description: Map an internal business process (BPMN-style swim lanes), measure cycle time, and detect bottlenecks where work spends most of its time waiting. Direct invocation of the process-mapper skill.
argument-hint: "<process description or path to process JSON>"
---
# /cs:process-map — BPMN-style process mapping + bottleneck detection
Run the `process-mapper` skill on this input:
**$ARGUMENTS**
## Three-tool workflow
1. **`process_documenter.py`** — Document the process as a BPMN-ish ASCII swim lane diagram. Input: stage list (name, owner, type{value-add/wait/rework}, P50 + P90 duration). Output: markdown diagram + normalized JSON.
2. **`bottleneck_detector.py`** — Identify bottlenecks. Triggers: stage P50 > 2× mean of value-add stages, OR wait-state % > 40% of total, OR rework % > 15%. Tunable via `--profile {saas,services,manufacturing,healthcare}`.
3. **`cycle_time_analyzer.py`** — Compute total cycle time (P50, P90), value-add ratio (VA%), Little's Law throughput. Verdict: VA% > 25% HEALTHY / 10-25% TYPICAL / <10% WASTE-HEAVY.
## Output
- Process diagram (markdown)
- Bottleneck list with severity + recommended action
- Cycle-time scorecard with VA% verdict
- Top 3 next actions
## Distinct from
- `engineering/slo-architect` — that's system reliability with SLO/SLI. This is **business process** reliability.
- `engineering/llm-wiki` — that's personal PKM. This is **company process documentation**.
- `c-level-advisor/coo-advisor` — that's strategic COO judgment. This is **tactical process mapping**.

View file

@ -0,0 +1,31 @@
---
description: Score vendors on a multi-dimensional scorecard (reliability / support / security / commercial / strategic-fit), track SLA compliance, classify third-party risk. Direct invocation of the vendor-management skill.
argument-hint: "<vendor catalog JSON path or vendor list>"
---
# /cs:vendor-review — Vendor scorecard + SLA + risk
Run the `vendor-management` skill on this input:
**$ARGUMENTS**
## Three-tool workflow
1. **`vendor_scorer.py`** — Score each vendor 0-100 across 5 weighted dimensions: reliability, support, security, commercial, strategic-fit. Industry tuning via `--profile {saas,fintech,healthcare,enterprise}`. Verdict: KEEP / REVIEW / REPLACE.
2. **`sla_compliance_tracker.py`** — Compute compliance % per vendor, breach trend (improving/stable/degrading), credit-claim eligibility.
3. **`vendor_risk_classifier.py`** — Classify risk per Shared Assessments SIG-Lite framework: Critical/High/Medium/Low across 4 vectors (data sensitivity, financial exposure, operational dependency, regulatory exposure). Industry-tunable.
## Output
- Per-vendor scorecard (markdown)
- SLA compliance breakdown with credit-claim flags
- Risk matrix with mitigation actions per vector
- Top 3 vendors to REVIEW or REPLACE
## Distinct from
- `c-level-advisor/general-counsel-advisor` — that's contract law + redline. This is **operational vendor performance**.
- `business-growth/contract-and-proposal-writer` — that's external proposal authoring. This is **inbound vendor scoring**.
- Sibling `procurement-optimizer` — that's spend categorization + supplier rationalization. This is **vendor performance + risk**.

View file

@ -0,0 +1,144 @@
---
name: business-operations-skills
description: Use when running, diagnosing, or designing internal business operations — process documentation, vendor SLAs, capacity planning, internal comms, SOP/runbook authoring, procurement spend. Triggers on "BizOps review", "where's the bottleneck", "vendor health", "internal SOP", "all-hands deck", "spend categorization", "capacity for Q3", "process mapping". Forks context to route to one of six BizOps sub-skills (process-mapper, vendor-management, capacity-planner, internal-comms, knowledge-ops, procurement-optimizer) and returns a digest. Distinct from business-growth (external sales motion) and c-level-advisor (strategic, not operational).
context: fork
version: 2.8.0
author: claude-code-skills
license: MIT
tags: [bizops, operations, process, vendor, capacity, sop, procurement, coo, orchestrator]
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
---
# Business Operations — Domain Orchestrator
The BizOps surface is **internal**: how the company actually runs. This orchestrator forks its conversation context, routes your inquiry to one of six sub-skills, then returns a tight digest to the parent thread. The heavy ingestion (vendor catalogs, process interviews, multi-doc SOP intake) stays in the forked context.
## When to invoke
| Symptom | Sub-skill to route to |
|---|---|
| "Where does the work spend most of its time waiting?" | `process-mapper` |
| "Is this vendor delivering against the SLA?" | `vendor-management` |
| "Do we have enough people to ship in Q3?" | `capacity-planner` |
| "I need to brief the company on a re-org" | `internal-comms` |
| "Write me a runbook for the incident response process" | `knowledge-ops` |
| "Why is our software spend up 40% YoY?" | `procurement-optimizer` |
## Routing logic (deterministic)
The orchestrator classifies the inquiry by **signals** detected in the prompt. Two-signal threshold for confident routing; one-signal triggers a clarifying question.
### Signal table
| Signal class | Keywords | Sub-skill |
|---|---|---|
| **PROCESS** | bottleneck, cycle time, waiting, handoff, BPMN, process map, workflow | `process-mapper` |
| **VENDOR** | vendor, supplier, SLA, contract, third-party, MSA, SaaS subscription, renewal | `vendor-management` |
| **CAPACITY** | headcount, capacity, utilization, planning, hiring sequence, FTE | `capacity-planner` |
| **COMMS** | all-hands, internal newsletter, announcement, change management, FAQ, town hall | `internal-comms` |
| **KNOWLEDGE** | SOP, runbook, knowledge base, wiki, playbook, documentation, onboarding doc | `knowledge-ops` |
| **PROCUREMENT** | spend, procurement, purchase, supplier rationalization, software audit, SaaS sprawl | `procurement-optimizer` |
If signals are mixed (e.g., "vendor SLA + spend audit"), run the **highest-confidence sub-skill first**, then chain into the second one in a follow-up forked turn.
### Fallback
If no signal class scores ≥ 2, ask **one** clarifying question naming the two most likely candidates. Do NOT guess silently.
## Workflow (Matt Pocock grill discipline)
Derived from Matt Pocock's `grill-with-docs` pattern: **explore-then-ask, one question per turn with a recommended answer, walk the decision tree depth-first, track dependencies, anchor every challenge in the documented canon** (`references/`).
### Step 1 — Explore before asking
Before any clarifying question, check:
- Does the user's working directory already contain a process map, vendor catalog, SOP, or org chart we can grep?
- Does the inquiry already disambiguate the lane (e.g., "vendor SLA review" — that's `vendor-management`, no question needed)?
- Is the lane unambiguous from filenames mentioned (`procurement-Q3.csv` → procurement)?
If the codebase resolves the lane, **route silently**. Don't ask.
### Step 2 — If still ambiguous, ONE forcing question with a recommended answer
Matt's rule: never bundle questions. Never default to "what do you think?". Always offer your recommendation.
Pattern:
```
Q1/1: [precise question naming the two candidate lanes]
Recommended: [Lane X, because <one-sentence rationale from the signal table>]
(Confirm, or override?)
```
Wait for the user's response. **Then** route. Never guess silently after a turn that asked a question.
### Step 3 — Forking decision-tree walk (only if the inquiry crosses lanes)
If the user's inquiry legitimately crosses two lanes (e.g., "vendor SLA + spend audit" = VENDOR + PROCUREMENT), walk the tree **depth-first**:
1. Resolve the higher-confidence lane first → run that sub-skill in forked context → return digest
2. Ask: "Should we now run [second lane]? My recommendation: yes, because [dependency reason]."
3. Only after explicit user confirmation, run the second sub-skill
Do NOT chain silently. Each fork is an explicit user-confirmed step.
### Step 4 — Invoke sub-skill in forked context
Each sub-skill is invoked with the original prompt + a digest of any structured inputs (file paths, JSON inputs). The fork keeps heavy ingestion (vendor catalog, process transcripts, SOP source documents) out of the parent context.
### Step 5 — Return digest with cited canon challenge
When the sub-skill completes, return a **≤ 200-word digest** to the parent thread:
- What was analyzed
- Top 3 findings (each anchored in a reference doc citation — e.g., "Goldratt's Theory of Constraints: optimize the bottleneck, not the non-constraint")
- Top 3 next actions (named owners if possible)
- Path to the artifact(s) produced
- **One grill challenge** for the user, cited: "Your value-add ratio is 12%. Lean canon (Womack & Jones 1996) classifies <15% as waste-heavy. What's blocking process redesign political, technical, or budget?"
The parent agent can then ask follow-ups (each triggering new forked invocations).
## Forcing-question library (grill-with-docs pattern)
When the user has provided enough context to enter a lane, the orchestrator may grill them on the **decisions inside that lane** before invoking the sub-skill. One question per turn, each with a recommended answer + canon citation. Examples:
- **PROCESS lane**: "Before mapping: do you have measured cycle times per stage, or only estimates? Recommended: insist on measured data for the top-3 longest stages. Anti-pattern (Goldratt 1984): map estimates, optimize the wrong constraint."
- **VENDOR lane**: "Before scoring: what's your tier-1 criticality threshold — by spend ($X/year), or by operational dependency (revenue-blocking if vendor fails)? Recommended: operational dependency. Anti-pattern (Gartner TPRM): spend-only tiering misses critical low-spend vendors like the HVAC vendor in the Target breach."
- **CAPACITY lane**: "Before modeling: are you planning for utilization or throughput? Recommended: throughput (Little's Law). Anti-pattern (DORA): planning for utilization > 80% destroys throughput via queueing."
Never run a sub-skill until the lane-defining decision is locked.
## Assumptions
1. The user is acting on behalf of an organization with ≥ 10 employees (smaller orgs don't need this surface).
2. The user has access to the data the sub-skill needs (process docs, vendor list, spend export, etc.) — or accepts the skill's templated dummy data.
3. The user wants **deterministic, repeatable analysis** over LLM-flavored prose. Every sub-skill ships stdlib-only Python tools.
## Non-goals
- Not a substitute for an ERP, vendor management platform (Vendr, Tropic), or capacity-planning SaaS (Float, Runn).
- Does not store state across sessions — every invocation is self-contained.
- Does not call external APIs from Python tools (stdlib only, by design).
## Distinct from
- **`business-growth/*`** — that's the **external sales motion** (CSM, sales engineering, RevOps). BizOps is **internal**.
- **`c-level-advisor/coo-advisor`** — that's strategic COO judgment ("should we restructure?"). BizOps is tactical ("here's the process map with bottlenecks").
- **`engineering/slo-architect`** — that's system reliability with SLO/SLI/error budgets. `process-mapper` is **business process** reliability, not system reliability.
- **`engineering/llm-wiki`** — that's a **personal** PKM (Karpathy's pattern). `knowledge-ops` is **company-wide** SOP authoring.
## Output artifacts
Every sub-skill produces at least one artifact (markdown, CSV, or JSON) saved to the user's working directory. The orchestrator surfaces the file path in the digest.
## Anti-patterns (do not)
- ❌ Run all 6 sub-skills "to be thorough" — pick one based on signal, return digest, let user chain
- ❌ Auto-approve a vendor or process change — surface findings; the human decides
- ❌ Edit production process docs without asking — write to a new file, propose the diff
- ❌ Skip the digest step — parent context needs ≤ 200-word digest, not the full sub-skill output
## References
- See `c-level-advisor/coo-advisor` for strategic COO framing
- Path-B build pattern: `documentation/implementation/bizops-commercial-expansion-plan.md`

View file

@ -0,0 +1,102 @@
---
name: process-mapper
description: Use when a BizOps lead, COO, or process-improvement owner needs to document an end-to-end business process (procurement, employee onboarding, incident handoff, customer-onboarding, claims adjudication) in BPMN-style notation, measure cycle times by stage, surface where work spends most of its time waiting vs. being worked, and quantify the gap between processing time and total elapsed time. Pairs Lean / Six Sigma / Theory-of-Constraints canon with deterministic stdlib-only Python tools to produce a process map, a ranked bottleneck list (with severity + root-cause hypothesis), and a cycle-time analysis (P50, P90, value-add ratio, Little's-Law throughput). Distinct from sales-pipeline, system-reliability (SLO), and strategic-OKR work — this is tactical process documentation for internal operations.
version: 2.8.0
author: claude-code-skills
license: MIT
tags: [bizops, process, bpmn, bottleneck, cycle-time, lean, six-sigma, value-stream]
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
---
# process-mapper
BPMN-style business process documentation, bottleneck detection, and cycle-time analysis for internal-operations leaders.
## Purpose
Internal-operations work suffers from three recurring failure modes:
1. **Implicit process** — the steps exist only in tribal knowledge, so handoffs drop and onboarding takes weeks.
2. **Invisible waiting** — most of the elapsed time on any business process is queue / wait / approval time, not actual work; teams optimize the wrong stage.
3. **Local optimization** — Goldratt's Theory of Constraints is ignored; resources are added to non-constraint stages, gaining nothing.
This skill produces a documented process map, identifies where work waits, and points the constraint out by name with deterministic logic — not LLM intuition.
## When to use
- Documenting a new business process (procurement intake, vendor onboarding, employee onboarding, incident handoff, expense reimbursement, customer onboarding, claims adjudication).
- An existing process is "too slow" but nobody can name the bottleneck.
- Cycle time is being measured but value-add ratio is not — so the team can't tell whether the process is healthy or waste-heavy.
- Cross-functional handoffs are dropping work and root cause is unclear.
## Workflow
Five-step deterministic flow:
1. **Intake.** Capture the process as a JSON file with one entry per stage: `name`, `owner`, `type` (`value-add` | `wait` | `rework`), `duration_minutes_p50`, `duration_minutes_p90`. Use `assets/process_template.md` and its JSON skeleton.
2. **Map stages.** Run `process_documenter.py` to produce an ASCII swim-lane diagram + a normalized JSON artifact. The swim-lane separates lanes by owner so cross-functional handoffs become visible.
3. **Measure cycle time.** Run `cycle_time_analyzer.py` to compute total P50, total P90, value-add ratio (VA%), and a Little's-Law throughput estimate. Verdict: VA% > 25% = HEALTHY, 1025% = TYPICAL, < 10% = WASTE-HEAVY.
4. **Detect bottlenecks.** Run `bottleneck_detector.py` with the appropriate `--profile` (saas / services / manufacturing / healthcare). Output is a ranked list with severity (CRITICAL / HIGH / MEDIUM), root-cause hypothesis, and one recommended action per finding.
5. **Recommend.** Pair the bottleneck list with the cycle-time verdict; recommend a single constraint-focused intervention per Goldratt's "subordinate everything to the constraint" rule. Don't recommend optimization of a non-constraint stage.
## Scripts
**`scripts/process_documenter.py`** — Reads a process JSON, validates it, and emits a text-based BPMN-style swim-lane diagram in Markdown (lanes by owner, stages annotated with type + duration). Also outputs a normalized JSON artifact for downstream tools. Stdlib only. `--sample` prints a 6-stage procurement-intake example.
**`scripts/bottleneck_detector.py`** — Applies three deterministic detection rules: (a) stage P50 > 2× mean of value-add stages, (b) wait-state % > 40% of total cycle, (c) rework % > 15%. Thresholds adjust by `--profile` because SaaS, services, manufacturing, and healthcare have different "normal" wait ratios. Output is a ranked list with severity, hypothesis, action.
**`scripts/cycle_time_analyzer.py`** — Computes total P50 and P90 cycle time, value-add ratio (VA%), wait %, rework %, and a Little's-Law throughput estimate (WIP / cycle time). Per Lean canon: VA% > 25% = HEALTHY, 1025% = TYPICAL (most non-manufacturing processes land here), < 10% = WASTE-HEAVY.
## References
- `references/lean_six_sigma_canon.md` — TIMWOOD wastes, value-stream mapping, Theory of Constraints, Kanban WIP, Little's Law. Cites Womack & Jones, Rother & Shook, Goldratt, Ohno, Liker, Pyzdek, Anderson.
- `references/bpmn_essentials.md` — Pools, lanes, gateways, events, message flows, common notation mistakes. Cites the OMG BPMN 2.0 spec, Silver, Allweyer, Freund/Rücker, OASIS, ISO/IEC 19510:2013.
- `references/bottleneck_anti_patterns.md` — Seven specific anti-patterns drawn from Goldratt, Kim et al., Spear, DORA, Deming, and process-mining research.
## Assumptions
1. The user can provide stage-level cycle-time data (even rough P50 / P90 estimates). If they cannot, the first step is to instrument the process — not to map it.
2. "Process" here means a repeatable business workflow with discrete stages, not a one-off project.
3. The user has authority to act on bottlenecks (or can route findings to someone who does). Without that, the output is academic.
4. Stage `type` is honest: a "value-add" stage labeled as such by the user really does change the work product from the customer's perspective. Mis-labelling waiting as value-add is the most common data-quality failure.
## Anti-patterns
- **Mapping every process at once.** Pick one. Goldratt: the constraint is a single point.
- **Optimizing the non-constraint.** If stage 4 is the bottleneck, speeding up stage 2 just builds inventory in front of stage 4. Subordinate everything to the constraint.
- **Mistaking total cycle time for processing time.** They are almost never the same; VA% reveals the gap.
- **Adding people to a wait-bound process.** Wait time is not solved by more headcount; it's solved by removing the handoff or batch.
- **Treating rework as a separate problem.** Rework loops belong in the process map. Hiding them understates true cycle time.
## Distinct from
- **business-growth skills** — external sales motion, lead-funnel conversion, customer-success retention. Process-mapper is *internal* operations.
- **engineering/slo-architect** — system-reliability SLOs / error budgets / burn-rate alerts. Process-mapper is *business-process* cycle time, not system uptime.
- **c-level-advisor (COO / CEO)** — strategic prioritization of which processes to fix. Process-mapper is the tactical instrument used after that prioritization decision.
- **project-management skills** — Jira / Confluence ticket workflow tooling. Process-mapper is process *design*, not ticket *tracking*.
## Forcing-question library (Matt Pocock grill discipline)
Before invoking the tools, the orchestrator (or `/cs:grill-bizops`) walks the user through these questions **one at a time, with a recommended answer + canon citation**. Never bundled.
1. **"Do you have measured cycle times for the top-3 longest stages, or only estimates?"**
Recommended: insist on measured data.
Canon: Goldratt 1984 (*The Goal*) — optimizing estimated bottlenecks reliably attacks the wrong constraint.
2. **"Are you mapping the *current* process (as-is) or the *intended* process (to-be)?"**
Recommended: map as-is first. To-be after bottleneck is identified.
Canon: Rother & Shook 1999 (*Learning to See*) — value-stream mapping starts with the current state, always.
3. **"Where do handoffs occur between teams, and how long does each handoff wait?"**
Recommended: log every handoff with median wait time.
Canon: Reinertsen 2009 (*Principles of Product Development Flow*) — wait time at handoffs is the largest invisible cost.
4. **"What's your batch size at each stage?"**
Recommended: drive batch size toward 1 wherever possible.
Canon: Anderson 2010 (*Kanban*) — batch size correlates 1:1 with cycle time variance.
5. **"What's the rework rate per stage?"**
Recommended: surface it explicitly; rework loops belong in the map.
Canon: Pyzdek (*Six Sigma Handbook*) — hidden rework drives 30-50% of total cycle time in service processes.
Walk depth-first. Don't open question 4 before 1-3 are answered. After all 5 are locked, invoke `process_documenter.py``bottleneck_detector.py``cycle_time_analyzer.py` in sequence.

View file

@ -0,0 +1,124 @@
# Process Template
Use this template to document a business process before running it through
the process-mapper tools. Fill in the stage table first, then translate it
into the JSON skeleton at the bottom of this file. Feed that JSON into the
three CLI tools:
```
python3 scripts/process_documenter.py --input my-process.json
python3 scripts/bottleneck_detector.py --input my-process.json --profile saas
python3 scripts/cycle_time_analyzer.py --input my-process.json --profile saas
```
---
## Process metadata
- **Process name:** _(e.g., Procurement Intake, Employee Onboarding, Incident Handoff)_
- **Owner role:** _(who is accountable for the end-to-end process)_
- **Frequency:** _(how often this process runs — daily, weekly, on-demand)_
- **Trigger event:** _(what starts the process)_
- **End state:** _(what marks the process complete)_
- **WIP at any time:** _(how many items are typically in process at once; needed for Little's-Law throughput)_
---
## Stage table
Six rows to start. Add or remove as needed. **Honesty about stage `type` is
the single most important data-quality choice.** If a stage is queue / wait,
mark it `wait`. If it changes the work product from the customer's
perspective, mark it `value-add`. If it exists to fix an upstream defect,
mark it `rework`.
| # | Stage name | Owner (role) | Type | P50 (min) | P90 (min) | Notes |
|---|------------|--------------|------|-----------|-----------|-------|
| 1 | _e.g., Submit request_ | Requestor | value-add | 15 | 30 | |
| 2 | _e.g., Wait for manager approval queue_ | Manager | wait | 480 | 1440 | Typically batched |
| 3 | _e.g., Manager approves_ | Manager | value-add | 10 | 25 | |
| 4 | _e.g., Wait for finance review_ | Finance | wait | 720 | 2880 | |
| 5 | _e.g., Finance validates budget code_ | Finance | value-add | 20 | 60 | |
| 6 | _e.g., Rework — missing vendor W-9_ | Requestor | rework | 120 | 360 | Frequent escape |
**Type definitions (Lean canon):**
- `value-add` — the stage changes the work product in a way the end customer
would willingly pay for. Most stages are NOT value-add.
- `wait` — work is queued, idle, or waiting for someone. Wait stages are the
largest source of cycle-time bloat in most office processes.
- `rework` — the stage exists to fix a defect introduced upstream. Six-Sigma
canon: rework is always an upstream-quality problem.
---
## JSON skeleton
Copy this into `my-process.json`, edit the values to match your stage table,
and pass it to the CLI tools.
```json
{
"process_name": "Replace with your process name",
"wip": 12,
"stages": [
{
"name": "Stage 1 name",
"owner": "Owning role",
"type": "value-add",
"duration_minutes_p50": 15,
"duration_minutes_p90": 30
},
{
"name": "Stage 2 name",
"owner": "Owning role",
"type": "wait",
"duration_minutes_p50": 480,
"duration_minutes_p90": 1440
},
{
"name": "Stage 3 name",
"owner": "Owning role",
"type": "value-add",
"duration_minutes_p50": 10,
"duration_minutes_p90": 25
},
{
"name": "Stage 4 name",
"owner": "Owning role",
"type": "wait",
"duration_minutes_p50": 720,
"duration_minutes_p90": 2880
},
{
"name": "Stage 5 name",
"owner": "Owning role",
"type": "value-add",
"duration_minutes_p50": 20,
"duration_minutes_p90": 60
},
{
"name": "Stage 6 name",
"owner": "Owning role",
"type": "rework",
"duration_minutes_p50": 120,
"duration_minutes_p90": 360
}
]
}
```
---
## Tips
- **Use real data when you can.** Pull stage durations from your ticket system
(Jira, ServiceNow, Zendesk). Estimated durations are fine for a first pass
but should be replaced before any change decision is made.
- **One process at a time.** Goldratt: every system has exactly one binding
constraint. Mapping ten processes simultaneously dilutes attention away
from the one that's actually limiting throughput.
- **Profile choice matters.** Pass `--profile manufacturing` for physical-goods
flows, `--profile services` for human-delivered services with longer
acceptable wait times, `--profile healthcare` for clinical or regulated
human-in-the-loop flows, `--profile saas` for everything else.

View file

@ -0,0 +1,178 @@
# Bottleneck Anti-Patterns
Seven plus specific anti-patterns that recur in business-process improvement
work. Each is sourced to primary literature, and each has a corresponding
detection or recommendation in the skill's tools.
## Sources
1. **Goldratt, E. M. (1984). _The Goal._** North River Press. — Theory of Constraints.
2. **Kim, G., Behr, K. & Spafford, G. (2013). _The Phoenix Project: A Novel About IT, DevOps, and Helping Your Business Win._** IT Revolution Press. — TOC applied to IT operations.
3. **Spear, S. J. (2009). _The High-Velocity Edge._** McGraw-Hill. — Toyota-derived discipline for complex operations; explicit treatment of why local optimization fails.
4. **Forsgren, N., Humble, J. & Kim, G. (2018). _Accelerate: The Science of Lean Software and DevOps._** IT Revolution. — DORA research; empirical link between flow metrics and outcomes.
5. **Deming, W. E. (1986). _Out of the Crisis._** MIT Press. — System-of-profound-knowledge framework; root-cause discipline.
6. **van der Aalst, W. M. P. (2016). _Process Mining: Data Science in Action,_ 2nd ed.** Springer. — Empirical methodology for discovering actual process behavior vs. documented behavior.
7. **Reinertsen, D. G. (2009). _The Principles of Product Development Flow._** Celeritas Publishing. — Queueing theory and cost of delay.
8. **Forrester Research. (Multiple years.) _Process Mining: Vendor and Market Analyses._** — Industry research on process-mining adoption and the gap between modeled and actual process.
---
## AP-1. Optimizing the non-constraint
**Source:** Goldratt (1984), Kim et al. (2013).
A team identifies that stage 2 of a process is "slow" (relative to other
non-constraint stages) and optimizes it. The actual constraint is stage 4.
Result: throughput is unchanged; inventory grows in front of stage 4.
**Detection:** Compare every stage's P50 to the value-add mean (Rule R1) but
weight the recommendation by impact on total cycle. The skill's
`bottleneck_detector.py` ranks by impact_minutes_p50 specifically to direct
attention to the binding constraint.
**Counter-pattern:** Always solve the longest wait or longest stage first;
ignore "quick wins" elsewhere until the constraint moves.
---
## AP-2. Adding resources before identifying the constraint
**Source:** Goldratt (1984), Reinertsen (2009).
Symptom: "We need to hire more procurement analysts." Reality: the analysts
are not the constraint; manager approval queues are. Adding analysts increases
WIP, lengthens cycle time (per Little's Law), and makes the queue worse.
**Detection:** Rule R2 (wait-share > 40%) catches the case where the wait —
not capacity — dominates.
**Counter-pattern:** First check whether wait time exceeds value-add time. If
it does, no amount of new staffing will help. Remove the handoff, parallelize
the approval, or apply WIP limits.
---
## AP-3. Mistaking wait time for processing time
**Source:** Rother & Shook (1999), Deming (1986).
A team reports that "manager approval takes two days." On inspection, the
manager spends 10 minutes reviewing each request; the rest is queue time.
Process time is 10 minutes; lead time is two days. Treating them as the same
hides the real problem.
**Detection:** The skill's stage `type` field separates `value-add` from
`wait`. The value-add ratio (VA%) in `cycle_time_analyzer.py` quantifies the
gap.
**Counter-pattern:** Force stages to declare their type honestly. Any stage
where the worker is not actively engaged is a wait stage, regardless of who
"owns" it.
---
## AP-4. Inspection-as-quality
**Source:** Pyzdek (Six Sigma Handbook), Deming (1986), Spear (2009).
Defects keep escaping, so the team adds a final QA review. The defects don't
go down (the upstream stages haven't changed) — but cycle time goes up because
of the new stage. Worse, the QA reviewer is now blamed for misses.
**Detection:** Rule R3 (rework share > 15%) with the hypothesis "defects
escape upstream stages."
**Counter-pattern:** Find the earliest stage that could detect the defect; add
the check there (poka-yoke). Stop the line on detection; don't queue defects
for downstream rework.
---
## AP-5. Optimizing the documented process, not the actual one
**Source:** van der Aalst (2016), Forrester process-mining reports.
The team documents the "official" process and optimizes it. Process-mining
tools then reveal that 60% of cases skip stages, loop back, or take undocumented
routes. The optimization had no effect because it targeted a fiction.
**Detection:** The skill cannot detect this from the input JSON alone — it
relies on the user to report actual stage durations from real cases, not
target durations. The "Assumptions" section in SKILL.md surfaces this
explicitly.
**Counter-pattern:** Use ticket-system data, time-stamps, or event logs to
ground stage durations in actual cases. If the data isn't available, the
first step is instrumentation, not mapping.
---
## AP-6. Batched approvals as the default
**Source:** Reinertsen (2009), Anderson (Kanban, 2010).
Approvers batch requests: "I'll review everyone's POs on Friday afternoon."
This adds half the batch interval (typically 34 days) to the average wait
time of every request, with no quality benefit.
**Detection:** Wait stages with P50 durations measured in days (hundreds of
minutes) are almost always batched. The skill flags them via R1 and R2.
**Counter-pattern:** Move to continuous-flow approval. If continuous is
infeasible (e.g., a committee that meets weekly), at least shrink the batch
interval or move approval to a lower level where it can run continuously.
---
## AP-7. Local efficiency metrics
**Source:** Goldratt (1984), Deming (1986), Spear (2009).
Each stage is measured on its own efficiency (e.g., "manager handles 95% of
requests within SLA"). The system as a whole is not measured. Each role
optimizes locally, pushing work as fast as possible to the next queue —
which is exactly where it stalls.
**Detection:** The skill's verdict is always at the **process** level (VA%,
total cycle time), never at the stage level. The `bottleneck_detector.py`
recommendation text explicitly invokes Goldratt's "subordinate everything to
the constraint."
**Counter-pattern:** Measure throughput and total cycle time at the process
level. Stage-level metrics are diagnostic, not goal-setting.
---
## AP-8. Skipping the value-stream map and going straight to automation
**Source:** Kim et al. (2013), Forrester process-mining research.
A team buys an RPA / workflow automation tool, then automates the existing
broken process. Result: the bad process now runs faster, with the same wait
queues and same rework rate. Goldratt's term for this is "automating the
mess."
**Detection:** Outside the skill's automated detection; surfaced in
SKILL.md's "Anti-patterns" list.
**Counter-pattern:** Map the value stream first. Eliminate wait and rework
stages. Then — and only then — consider automating what remains.
---
## AP-9. Treating cycle time as fixed
**Source:** Forsgren, Humble & Kim (2018, _Accelerate_).
A team reports cycle time as a single number ("it takes 5 days"). Real cycle
times are distributions, often log-normal, with heavy P90 / P99 tails. A 5-day
P50 with a 30-day P90 is a wildly different process than a 5-day P50 with a
6-day P90; the first is unpredictable, the second is reliable.
**Detection:** The skill captures both P50 and P90 per stage and reports
both totals. A large P90 / P50 ratio in `cycle_time_analyzer.py` is a flag
for high variability even when total cycle time looks acceptable.
**Counter-pattern:** Always quote P50 and P90 (or P50 and P95). DORA's
_Accelerate_ research finds that lead-time **variability** correlates with
business outcomes as strongly as median lead time.

View file

@ -0,0 +1,119 @@
# BPMN Essentials for Business-Process Documentation
A practical reference on BPMN (Business Process Model and Notation) for
process-mapper users. The skill emits text-based swim-lane diagrams that
approximate the BPMN structure without requiring users to install Visio,
Lucidchart, or Camunda. This file explains the canon those diagrams reflect.
## Sources
1. **Object Management Group. (2011). _Business Process Model and Notation (BPMN), Version 2.0._** OMG Document Number formal/2011-01-03. — The normative specification.
2. **Silver, B. (2011). _BPMN Method and Style,_ 2nd ed.** Cody-Cassidy Press. — The canonical practitioner book; defines the "method and style" rules now widely treated as informal BPMN convention.
3. **Allweyer, T. (2010). _BPMN 2.0: Introduction to the Standard for Business Process Modeling._** Books on Demand. — Approachable academic introduction.
4. **Freund, J. & Rücker, B. (2019). _Real-Life BPMN,_ 4th ed.** CreateSpace. — Practical patterns from the Camunda team.
5. **OASIS. (2010). _Web Services Business Process Execution Language (WS-BPEL), Version 2.0._** — Related execution standard; clarifies the interplay between BPMN modeling and BPEL execution.
6. **ISO/IEC 19510:2013. _Information technology — Object Management Group Business Process Model and Notation._** — The international-standard version of OMG BPMN 2.0.
7. **Recker, J. (2010). "Opportunities and constraints: the current struggle with BPMN." _Business Process Management Journal,_ 16(1), 181201.** — Peer-reviewed analysis of BPMN adoption pain points; sources the "common notation mistakes" list below.
8. **Dumas, M., La Rosa, M., Mendling, J. & Reijers, H. A. (2018). _Fundamentals of Business Process Management,_ 2nd ed.** Springer. — Textbook covering BPMN within the broader BPM lifecycle.
---
## Core BPMN elements
BPMN has hundreds of symbols. In practice, ~80% of useful diagrams use only
~10 of them. The skill's swim-lane output uses precisely these.
### Flow objects
- **Activity (task)** — a unit of work done by one role. Rectangle with rounded
corners. In the skill's swim-lane: this is a `value-add` or `rework` stage.
- **Event** — something that happens (start, intermediate, end). Circles. The
skill represents start/end implicitly as the first and last stage.
- **Gateway** — branching / merging point. Diamond. Common types:
- **Exclusive (XOR)** — one path taken.
- **Parallel (AND)** — all paths taken.
- **Inclusive (OR)** — one or more paths taken based on data.
### Connecting objects
- **Sequence flow** — solid arrow inside one pool. The skill renders these as
`->` between stages in the lane.
- **Message flow** — dashed arrow across pool boundaries. The skill's
cross-lane handoffs (e.g., Requestor -> Manager) are message-flow-equivalents.
- **Association** — dotted line linking a data object to an activity.
### Swim lanes
- **Pool** — represents a participant (a company, a department, or a system).
Each pool is independent; communication between pools uses message flow only.
- **Lane** — a sub-partition within a pool, usually a role or sub-team.
The skill maps one stage's `owner` field to one lane. The full diagram is a
single pool with multiple lanes — appropriate for an internal business process
where one organization controls the whole flow.
---
## Method and Style rules (Silver)
Silver's "Method and Style" is a set of practitioner conventions that make
BPMN diagrams readable. The most load-bearing rules:
1. **One start, one end** per pool. Multiple end events are allowed only if
they represent different end-states (e.g., approved vs. rejected).
2. **Label every flow out of a gateway** with the condition (e.g., "amount >
$10K"). An unlabeled gateway is unreadable.
3. **Sequence flow stays inside a pool.** Use message flow between pools.
4. **One verb-noun task name.** "Approve PO" beats "Approval step."
5. **Black-box pools** for participants you don't model in detail (e.g., the
customer). Show only the message exchanges with them.
The skill enforces rule #4 implicitly by encouraging "Stage" names like
"Manager approves request" rather than "Approval."
---
## Common notation mistakes (Recker 2010; Freund/Rücker)
The following errors appear in over half of real-world BPMN diagrams:
| Mistake | Why it's wrong | What to do |
|---------|----------------|------------|
| Using sequence flow across pools | Pools are independent; only messages cross | Use dashed message flow |
| Missing gateway labels | The reader can't tell which path is taken when | Label every outbound flow |
| Multiple unrelated end events | Reader can't tell why a process ends in each spot | Consolidate or label by end-state |
| Conflating role with system | "JIRA" is a system, not a role; "Engineering Manager" is a role | Lanes = roles, not tools |
| Implicit gateways | Diverging sequence flows without a gateway diamond | Add an explicit XOR or parallel gateway |
| Modeling exceptions inline | Cluttered happy path | Use boundary events or a separate exception sub-process |
| No data objects | Reader doesn't know what artifacts move through | Add data-object boxes where they help |
The skill's stage-level `type` field (`value-add` | `wait` | `rework`) captures
the rework case explicitly so it doesn't get hidden inline. Users who want
full BPMN fidelity should export the normalized JSON and ingest it into a
BPMN-aware tool (Camunda Modeler, bpmn.io, Signavio).
---
## When to use BPMN vs. simpler notations
BPMN is appropriate when:
- The process has cross-functional handoffs (multiple lanes).
- The process has branching logic (gateways).
- The diagram will be reviewed by people who don't sit through a walkthrough.
For purely linear processes with no branching, a numbered list or a value
stream map is faster to produce and easier to read. The skill's swim-lane
output deliberately occupies the middle ground: more structured than a list,
less ceremony than full BPMN.
---
## BPMN 2.0 execution semantics
ISO/IEC 19510:2013 specifies executable semantics so that a BPMN diagram can
be loaded into a workflow engine (Camunda, jBPM, Activiti) and run directly.
The skill does not target executable BPMN — its output is for human reading
and constraint analysis. If a user wants to move from documentation to
automation, the normalized JSON is a starting point; mapping to the BPMN 2.0
XML schema is a separate exercise.

View file

@ -0,0 +1,134 @@
# Lean / Six Sigma / Theory-of-Constraints Canon
A working reference for the process-mapper skill. The concepts below are the
intellectual foundation for every detection rule and verdict band the skill
emits. Citations are deliberately to the primary sources, not blog posts.
## Sources
1. **Womack, J. P. & Jones, D. T. (1996). _Lean Thinking: Banish Waste and Create Wealth in Your Corporation._** Free Press. — The five-step Lean discipline: specify value, identify the value stream, make value flow, let the customer pull, pursue perfection.
2. **Rother, M. & Shook, J. (1999). _Learning to See: Value Stream Mapping to Add Value and Eliminate Muda._** Lean Enterprise Institute. — The canonical text on Value Stream Mapping (VSM); origin of current-state / future-state map distinction.
3. **Goldratt, E. M. (1984). _The Goal: A Process of Ongoing Improvement._** North River Press. — The Theory of Constraints: identify, exploit, subordinate, elevate, repeat. Every process has exactly one binding constraint at a time.
4. **Ohno, T. (1988). _Toyota Production System: Beyond Large-Scale Production._** Productivity Press. — Origin of the seven wastes (muda), pull system, jidoka, and andon discipline.
5. **Liker, J. K. (2004). _The Toyota Way: 14 Management Principles from the World's Greatest Manufacturer._** McGraw-Hill. — Modern systemic treatment of TPS principles for non-manufacturing operations.
6. **Pyzdek, T. & Keller, P. (2018). _The Six Sigma Handbook,_ 5th ed.** McGraw-Hill. — DMAIC discipline, SIPOC, process-capability indices, defect-rate measurement.
7. **Anderson, D. J. (2010). _Kanban: Successful Evolutionary Change for Your Technology Business._** Blue Hole Press. — WIP limits, pull system applied to knowledge work, cumulative flow diagrams.
8. **Reinertsen, D. G. (2009). _The Principles of Product Development Flow._** Celeritas Publishing. — Queueing theory for knowledge-work product development; cost of delay.
---
## The Seven Wastes (TIMWOOD)
Ohno's original taxonomy, with the eighth ("non-utilized talent") added later:
| Code | Waste | What it looks like in business processes |
|------|-------|--------------------------------------------|
| **T** | Transport | Moving work between systems / inboxes / queues for no reason |
| **I** | Inventory | Backlogs of pending tickets, unprocessed invoices, open POs |
| **M** | Motion | People hunting for information, switching tools, reading email threads to reconstruct context |
| **W** | Waiting | Work sitting in someone's queue (the largest waste in office work) |
| **O** | Over-production | Producing forecasts, reports, or work nobody requested |
| **O** | Over-processing | Approval chains that add no scrutiny, gold-plating |
| **D** | Defects | Errors that force rework downstream |
| **(N)** | Non-utilized talent | Skilled people doing low-skill work |
The process-mapper skill identifies these via stage `type`: `wait` captures
**W** (and often **I**); `rework` captures **D**. Mis-labelling a wait stage as
`value-add` is the most common data-quality failure and will mask the true
constraint.
---
## Value Stream Mapping (Rother & Shook)
VSM separates **process time** (PT) from **lead time** (LT). For each stage:
- **PT** = the time work actually spends being touched.
- **LT** = the elapsed wall-clock time from when work arrives at the stage to
when it leaves.
In the process-mapper schema, a `value-add` stage's `duration_minutes_p50` is
PT-like; a `wait` stage's duration is the LT component between PT-stages.
The **process cycle efficiency** (PCE) is:
PCE = Total value-add time / Total lead time
This is exactly what `cycle_time_analyzer.py` computes as the "value-add ratio."
Rother & Shook's published benchmarks: office processes typically score
PCE < 10%; well-run service operations land 1025%; world-class manufacturing
can clear 2540%.
---
## Theory of Constraints (Goldratt)
Goldratt's Five Focusing Steps:
1. **Identify** the constraint.
2. **Exploit** it (squeeze every minute of capacity from the constraint).
3. **Subordinate** everything else to the constraint.
4. **Elevate** the constraint (only after step 2 is exhausted, add capacity).
5. **Repeat** — once the constraint moves, return to step 1.
Two implications used in the skill:
- **Optimizing a non-constraint stage produces no system improvement.** It
builds inventory in front of the constraint. The `bottleneck_detector.py`
output is ranked by impact specifically so users target the constraint
first.
- **The constraint is almost always a wait stage in office work.** This is
why Rule R2 (wait-share > 40%) is heavily weighted.
---
## Kanban WIP Limits (Anderson)
Little's Law:
L = lambda * W
Where L = items in the system (WIP), lambda = throughput (items per unit time),
and W = average cycle time. Rearranged:
lambda = L / W
Two practical consequences:
- **Cycle time scales linearly with WIP.** Cutting WIP in half cuts cycle time
in half (other things equal). This is why the skill computes throughput from
WIP / cycle time and surfaces a WIP-limit recommendation when wait-share is
high.
- **Adding people to a wait-bound process makes it worse.** New workers add
WIP without expanding the constraint, lengthening cycle time. The
`bottleneck_detector` action text says this explicitly.
---
## Six Sigma DMAIC and Rework
Pyzdek's DMAIC (Define, Measure, Analyze, Improve, Control) treats rework as
a downstream symptom of an upstream defect. The Six-Sigma rule the skill
encodes: **rework is always solved upstream, never downstream.** Adding a
quality-control inspector at the end of the line catches defects but doesn't
prevent them, and inspection-as-quality is itself a TIMWOOD waste
(over-processing).
The poka-yoke (error-proofing) recommendation in Rule R3 follows directly:
add the check at the earliest stage that can detect the defect.
---
## Reinertsen's Queueing Insights
Reinertsen's _Principles of Product Development Flow_ adapts manufacturing
queueing theory to knowledge work. Key results used in the skill:
- **High utilization explodes queue length.** A worker at 90% utilization has
~10x the queue of a worker at 50% utilization. Office workflows that pin
approvers at 100% utilization see wait stages grow without bound.
- **Small batches cut queue time.** Batched approvals (e.g., weekly review
cycles) inflate P50 wait times by half the batch interval on average.
When the skill recommends "remove the handoff or batch," this is the canon
behind it.

View file

@ -0,0 +1,305 @@
#!/usr/bin/env python3
"""bottleneck_detector.py
Apply three deterministic detection rules to a process JSON and emit a ranked
list of bottlenecks with severity, root-cause hypothesis, and a recommended
action.
Rules (defaults; tuned per industry profile):
R1. Stage P50 > 2x mean of value-add stages -> stage bottleneck
R2. Wait-state share of total cycle > 40% -> handoff bottleneck
R3. Rework share of total cycle > 15% -> quality bottleneck
Stdlib only.
"""
from __future__ import annotations
import argparse
import json
import statistics
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
# Per-industry threshold calibration. Manufacturing tolerates less wait;
# healthcare and services tolerate more given regulatory / human-in-the-loop steps.
PROFILES: dict[str, dict[str, float]] = {
"saas": {
"stage_multiplier": 2.0,
"wait_share_max": 0.40,
"rework_share_max": 0.15,
},
"services": {
"stage_multiplier": 2.5,
"wait_share_max": 0.50,
"rework_share_max": 0.15,
},
"manufacturing": {
"stage_multiplier": 1.8,
"wait_share_max": 0.30,
"rework_share_max": 0.10,
},
"healthcare": {
"stage_multiplier": 2.5,
"wait_share_max": 0.55,
"rework_share_max": 0.12,
},
}
@dataclass
class Finding:
severity: str # CRITICAL | HIGH | MEDIUM
rule: str # R1 | R2 | R3
title: str
detail: str
hypothesis: str
action: str
impact_minutes_p50: float
def severity_rank(self) -> int:
return {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2}.get(self.severity, 3)
def load(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def classify_severity(share: float, threshold: float) -> str:
"""Severity based on how far over the threshold the offender is."""
if share <= threshold:
return "MEDIUM"
if share >= threshold * 2:
return "CRITICAL"
if share >= threshold * 1.5:
return "HIGH"
return "MEDIUM"
def detect(normalized: dict, profile: str) -> list[Finding]:
prof = PROFILES.get(profile, PROFILES["saas"])
stages = normalized.get("stages", [])
findings: list[Finding] = []
if not stages:
return findings
total_p50 = sum(s["duration_minutes_p50"] for s in stages) or 1.0
wait_p50 = sum(s["duration_minutes_p50"] for s in stages if s["type"] == "wait")
rework_p50 = sum(s["duration_minutes_p50"] for s in stages if s["type"] == "rework")
va_durations = [
s["duration_minutes_p50"] for s in stages if s["type"] == "value-add"
]
va_mean = statistics.mean(va_durations) if va_durations else 0.0
# R1: per-stage runaway vs value-add mean
if va_mean > 0:
threshold_minutes = va_mean * prof["stage_multiplier"]
for s in stages:
if s["duration_minutes_p50"] > threshold_minutes:
ratio = s["duration_minutes_p50"] / va_mean
if ratio >= prof["stage_multiplier"] * 3:
sev = "CRITICAL"
elif ratio >= prof["stage_multiplier"] * 2:
sev = "HIGH"
else:
sev = "MEDIUM"
hypothesis = (
"Stage runs much longer than the typical value-add step; "
"common causes: batched approvals, single approver, "
"missing self-service, or unclear acceptance criteria."
)
action = (
"Decompose the stage; check if approval can be parallelized "
"or made conditional. If wait-state, apply Kanban WIP limit "
"or remove the handoff."
)
findings.append(
Finding(
severity=sev,
rule="R1",
title=f"Slow stage: {s['name']}",
detail=(
f"P50 {s['duration_minutes_p50']:.0f} min vs value-add "
f"mean {va_mean:.1f} min (ratio {ratio:.1f}x)."
),
hypothesis=hypothesis,
action=action,
impact_minutes_p50=s["duration_minutes_p50"],
)
)
# R2: wait-state share
wait_share = wait_p50 / total_p50
if wait_share > prof["wait_share_max"]:
sev = classify_severity(wait_share, prof["wait_share_max"])
findings.append(
Finding(
severity=sev,
rule="R2",
title="Process is dominated by wait time",
detail=(
f"Wait stages account for {wait_share*100:.0f}% of total P50, "
f"vs {prof['wait_share_max']*100:.0f}% profile threshold."
),
hypothesis=(
"Handoffs queue work behind a single role or batch. Per "
"Theory of Constraints, the system throughput is set by "
"whichever queue is longest, not by stage speed."
),
action=(
"Identify the longest wait stage; pull it forward, eliminate "
"it via self-service, or apply a WIP limit upstream so the "
"queue cannot grow."
),
impact_minutes_p50=wait_p50,
)
)
# R3: rework share
rework_share = rework_p50 / total_p50
if rework_share > prof["rework_share_max"]:
sev = classify_severity(rework_share, prof["rework_share_max"])
findings.append(
Finding(
severity=sev,
rule="R3",
title="Process has excessive rework",
detail=(
f"Rework accounts for {rework_share*100:.0f}% of total P50, "
f"vs {prof['rework_share_max']*100:.0f}% profile threshold."
),
hypothesis=(
"Defects escape upstream stages. Six-Sigma canon: rework is "
"always an upstream-quality problem, never a downstream one."
),
action=(
"Add a poka-yoke (error-proofing) check at the earliest stage "
"that can detect the defect; do not add inspection downstream."
),
impact_minutes_p50=rework_p50,
)
)
findings.sort(key=lambda f: (f.severity_rank(), -f.impact_minutes_p50))
return findings
def render_markdown(normalized: dict, findings: list[Finding], profile: str) -> str:
name = normalized.get("process_name", "Untitled Process")
lines: list[str] = []
lines.append(f"# Bottleneck Detection: {name}")
lines.append("")
lines.append(f"**Profile:** `{profile}` ")
lines.append(f"**Findings:** {len(findings)}")
lines.append("")
if not findings:
lines.append("_No bottlenecks detected at the configured thresholds._")
return "\n".join(lines)
for i, f in enumerate(findings, 1):
lines.append(f"## {i}. [{f.severity}] {f.title}")
lines.append("")
lines.append(f"- **Rule:** `{f.rule}`")
lines.append(f"- **Detail:** {f.detail}")
lines.append(f"- **Hypothesis:** {f.hypothesis}")
lines.append(f"- **Recommended action:** {f.action}")
lines.append(f"- **Impact (P50 minutes):** {f.impact_minutes_p50:.0f}")
lines.append("")
return "\n".join(lines)
def sample_process() -> dict:
# Reuses procurement-intake shape from process_documenter
return {
"process_name": "Procurement Intake (Sample)",
"wip": 12,
"stages": [
{"name": "Submit PO", "owner": "Requestor", "type": "value-add",
"duration_minutes_p50": 15, "duration_minutes_p90": 30},
{"name": "Wait for manager", "owner": "Manager", "type": "wait",
"duration_minutes_p50": 480, "duration_minutes_p90": 1440},
{"name": "Manager approves", "owner": "Manager", "type": "value-add",
"duration_minutes_p50": 10, "duration_minutes_p90": 25},
{"name": "Wait for finance", "owner": "Finance", "type": "wait",
"duration_minutes_p50": 720, "duration_minutes_p90": 2880},
{"name": "Finance validates", "owner": "Finance", "type": "value-add",
"duration_minutes_p50": 20, "duration_minutes_p90": 60},
{"name": "Rework: missing W-9", "owner": "Requestor", "type": "rework",
"duration_minutes_p50": 120, "duration_minutes_p90": 360},
],
}
def main() -> int:
parser = argparse.ArgumentParser(
description="Detect bottlenecks in a documented business process."
)
parser.add_argument("--input", type=Path, help="Path to process JSON file.")
parser.add_argument(
"--profile",
choices=sorted(PROFILES.keys()),
default="saas",
help="Industry profile for threshold calibration (default: saas).",
)
parser.add_argument(
"--output",
choices=["markdown", "json"],
default="markdown",
help="Output format (default: markdown).",
)
parser.add_argument(
"--sample",
action="store_true",
help="Use a built-in sample process and exit.",
)
args = parser.parse_args()
if args.sample:
raw = sample_process()
else:
if not args.input:
parser.error("--input is required unless --sample is given")
if not args.input.exists():
parser.error(f"input file not found: {args.input}")
raw = load(args.input)
# Minimal normalization: tolerate the same fields as process_documenter
stages = []
for s in raw.get("stages", []):
stages.append(
{
"name": s.get("name", ""),
"owner": s.get("owner", ""),
"type": s.get("type", ""),
"duration_minutes_p50": float(s.get("duration_minutes_p50", 0)),
"duration_minutes_p90": float(s.get("duration_minutes_p90", 0)),
}
)
normalized = {
"process_name": raw.get("process_name", "Untitled Process"),
"wip": int(raw.get("wip", 0) or 0),
"stages": stages,
}
findings = detect(normalized, args.profile)
if args.output == "json":
print(
json.dumps(
{
"process_name": normalized["process_name"],
"profile": args.profile,
"findings": [asdict(f) for f in findings],
},
indent=2,
)
)
else:
print(render_markdown(normalized, findings, args.profile))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""cycle_time_analyzer.py
Compute total cycle time (P50, P90), value-add ratio (VA%), wait %, rework %,
and a Little's-Law throughput estimate for a documented business process.
Verdict per Lean canon:
VA% > 25% -> HEALTHY
10% <= VA% <= 25% -> TYPICAL
VA% < 10% -> WASTE-HEAVY
Stdlib only.
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
# Per-industry verdict bands. Manufacturing benchmarks higher VA% than services.
PROFILES: dict[str, dict[str, float]] = {
"saas": {"healthy": 0.25, "typical": 0.10},
"services": {"healthy": 0.20, "typical": 0.08},
"manufacturing": {"healthy": 0.35, "typical": 0.15},
"healthcare": {"healthy": 0.20, "typical": 0.08},
}
@dataclass
class CycleTimeReport:
process_name: str
profile: str
stage_count: int
total_p50_minutes: float
total_p90_minutes: float
value_add_minutes_p50: float
wait_minutes_p50: float
rework_minutes_p50: float
value_add_ratio: float
wait_ratio: float
rework_ratio: float
verdict: str
wip: int
throughput_per_hour: float | None
notes: list[str]
def analyze(normalized: dict, profile: str) -> CycleTimeReport:
prof = PROFILES.get(profile, PROFILES["saas"])
stages = normalized.get("stages", [])
name = normalized.get("process_name", "Untitled Process")
wip = int(normalized.get("wip", 0) or 0)
total_p50 = sum(s["duration_minutes_p50"] for s in stages)
total_p90 = sum(s["duration_minutes_p90"] for s in stages)
va_p50 = sum(s["duration_minutes_p50"] for s in stages if s["type"] == "value-add")
wait_p50 = sum(s["duration_minutes_p50"] for s in stages if s["type"] == "wait")
rework_p50 = sum(s["duration_minutes_p50"] for s in stages if s["type"] == "rework")
denom = total_p50 if total_p50 > 0 else 1.0
va_ratio = va_p50 / denom
wait_ratio = wait_p50 / denom
rework_ratio = rework_p50 / denom
if va_ratio >= prof["healthy"]:
verdict = "HEALTHY"
elif va_ratio >= prof["typical"]:
verdict = "TYPICAL"
else:
verdict = "WASTE-HEAVY"
# Little's Law: L = lambda * W => lambda = L / W
# WIP is items currently in process; W (cycle time) is P50.
# Convert minutes to hours for a per-hour throughput.
throughput = None
if wip > 0 and total_p50 > 0:
cycle_hours = total_p50 / 60.0
throughput = wip / cycle_hours
notes: list[str] = []
if wip <= 0:
notes.append(
"WIP not provided; Little's-Law throughput estimate skipped. "
"Set 'wip' in the input JSON to enable it."
)
if total_p50 == 0:
notes.append("All stage P50 durations are zero; check input data.")
if rework_ratio > 0.0 and verdict == "HEALTHY":
notes.append(
"Process is healthy by VA%, but rework is non-zero. Six-Sigma canon: "
"any rework signal is worth a poka-yoke check."
)
if wait_ratio > 0.5:
notes.append(
"More than half the cycle is wait time. Throughput improves more "
"from queue removal than from speeding up value-add stages."
)
return CycleTimeReport(
process_name=name,
profile=profile,
stage_count=len(stages),
total_p50_minutes=round(total_p50, 2),
total_p90_minutes=round(total_p90, 2),
value_add_minutes_p50=round(va_p50, 2),
wait_minutes_p50=round(wait_p50, 2),
rework_minutes_p50=round(rework_p50, 2),
value_add_ratio=round(va_ratio, 4),
wait_ratio=round(wait_ratio, 4),
rework_ratio=round(rework_ratio, 4),
verdict=verdict,
wip=wip,
throughput_per_hour=round(throughput, 4) if throughput is not None else None,
notes=notes,
)
def render_markdown(report: CycleTimeReport) -> str:
lines: list[str] = []
lines.append(f"# Cycle-Time Analysis: {report.process_name}")
lines.append("")
lines.append(f"**Profile:** `{report.profile}` ")
lines.append(f"**Verdict:** **{report.verdict}**")
lines.append("")
lines.append("## Summary")
lines.append("")
lines.append("| Metric | Value |")
lines.append("|--------|-------|")
lines.append(f"| Stage count | {report.stage_count} |")
lines.append(f"| Total P50 (minutes) | {report.total_p50_minutes:.1f} |")
lines.append(f"| Total P90 (minutes) | {report.total_p90_minutes:.1f} |")
lines.append(
f"| Value-add minutes (P50) | {report.value_add_minutes_p50:.1f} |"
)
lines.append(f"| Wait minutes (P50) | {report.wait_minutes_p50:.1f} |")
lines.append(f"| Rework minutes (P50) | {report.rework_minutes_p50:.1f} |")
lines.append(f"| Value-add ratio (VA%) | {report.value_add_ratio*100:.1f}% |")
lines.append(f"| Wait ratio | {report.wait_ratio*100:.1f}% |")
lines.append(f"| Rework ratio | {report.rework_ratio*100:.1f}% |")
lines.append(f"| WIP (items in process) | {report.wip} |")
if report.throughput_per_hour is not None:
lines.append(
f"| Little's-Law throughput | {report.throughput_per_hour:.3f} items/hour |"
)
else:
lines.append("| Little's-Law throughput | _(needs WIP > 0 in input)_ |")
lines.append("")
if report.notes:
lines.append("## Notes")
lines.append("")
for n in report.notes:
lines.append(f"- {n}")
lines.append("")
return "\n".join(lines)
def sample_process() -> dict:
return {
"process_name": "Procurement Intake (Sample)",
"wip": 12,
"stages": [
{"name": "Submit PO", "owner": "Requestor", "type": "value-add",
"duration_minutes_p50": 15, "duration_minutes_p90": 30},
{"name": "Wait for manager", "owner": "Manager", "type": "wait",
"duration_minutes_p50": 480, "duration_minutes_p90": 1440},
{"name": "Manager approves", "owner": "Manager", "type": "value-add",
"duration_minutes_p50": 10, "duration_minutes_p90": 25},
{"name": "Wait for finance", "owner": "Finance", "type": "wait",
"duration_minutes_p50": 720, "duration_minutes_p90": 2880},
{"name": "Finance validates", "owner": "Finance", "type": "value-add",
"duration_minutes_p50": 20, "duration_minutes_p90": 60},
{"name": "Rework: missing W-9", "owner": "Requestor", "type": "rework",
"duration_minutes_p50": 120, "duration_minutes_p90": 360},
],
}
def main() -> int:
parser = argparse.ArgumentParser(
description="Analyze cycle time, value-add ratio, and throughput of a process."
)
parser.add_argument("--input", type=Path, help="Path to process JSON file.")
parser.add_argument(
"--profile",
choices=sorted(PROFILES.keys()),
default="saas",
help="Industry profile for verdict band (default: saas).",
)
parser.add_argument(
"--output",
choices=["markdown", "json"],
default="markdown",
help="Output format (default: markdown).",
)
parser.add_argument(
"--sample",
action="store_true",
help="Use a built-in sample process and exit.",
)
args = parser.parse_args()
if args.sample:
raw = sample_process()
else:
if not args.input:
parser.error("--input is required unless --sample is given")
if not args.input.exists():
parser.error(f"input file not found: {args.input}")
with args.input.open("r", encoding="utf-8") as f:
raw = json.load(f)
stages = []
for s in raw.get("stages", []):
stages.append(
{
"name": s.get("name", ""),
"owner": s.get("owner", ""),
"type": s.get("type", ""),
"duration_minutes_p50": float(s.get("duration_minutes_p50", 0)),
"duration_minutes_p90": float(s.get("duration_minutes_p90", 0)),
}
)
normalized = {
"process_name": raw.get("process_name", "Untitled Process"),
"wip": int(raw.get("wip", 0) or 0),
"stages": stages,
}
report = analyze(normalized, args.profile)
if args.output == "json":
print(json.dumps(asdict(report), indent=2))
else:
print(render_markdown(report))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""process_documenter.py
Read a JSON description of a business process (one entry per stage) and emit:
- a text-based BPMN-style swim-lane diagram in Markdown, OR
- a normalized JSON artifact for downstream tools.
Stdlib only. Use `--sample` to print a 6-stage procurement-intake example to
stdout.
Input schema (JSON):
{
"process_name": "Procurement Intake",
"wip": 12, # optional, integer; used by cycle_time_analyzer
"stages": [
{
"name": "Requestor submits PO request",
"owner": "Requestor",
"type": "value-add", # one of: value-add | wait | rework
"duration_minutes_p50": 15,
"duration_minutes_p90": 30
},
...
]
}
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from enum import Enum
from pathlib import Path
VALID_TYPES = {"value-add", "wait", "rework"}
class StageType(str, Enum):
VALUE_ADD = "value-add"
WAIT = "wait"
REWORK = "rework"
@dataclass
class Stage:
name: str
owner: str
type: str
duration_minutes_p50: float
duration_minutes_p90: float
def validate(self, idx: int) -> list[str]:
errs: list[str] = []
if not self.name:
errs.append(f"stage[{idx}]: missing 'name'")
if not self.owner:
errs.append(f"stage[{idx}]: missing 'owner'")
if self.type not in VALID_TYPES:
errs.append(
f"stage[{idx}] ('{self.name}'): invalid type '{self.type}' "
f"(expected one of {sorted(VALID_TYPES)})"
)
if self.duration_minutes_p50 < 0:
errs.append(f"stage[{idx}] ('{self.name}'): p50 must be >= 0")
if self.duration_minutes_p90 < self.duration_minutes_p50:
errs.append(
f"stage[{idx}] ('{self.name}'): p90 ({self.duration_minutes_p90}) "
f"< p50 ({self.duration_minutes_p50})"
)
return errs
def load_process(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def normalize(raw: dict) -> dict:
"""Validate + return a normalized dict. Raises ValueError on bad input."""
if "stages" not in raw or not isinstance(raw["stages"], list):
raise ValueError("input must include a non-empty 'stages' list")
stages: list[Stage] = []
errors: list[str] = []
for idx, s in enumerate(raw["stages"]):
try:
stage = Stage(
name=s.get("name", ""),
owner=s.get("owner", ""),
type=s.get("type", ""),
duration_minutes_p50=float(s.get("duration_minutes_p50", 0)),
duration_minutes_p90=float(s.get("duration_minutes_p90", 0)),
)
except (TypeError, ValueError) as e:
errors.append(f"stage[{idx}]: parse error: {e}")
continue
errors.extend(stage.validate(idx))
stages.append(stage)
if errors:
raise ValueError("invalid input:\n - " + "\n - ".join(errors))
return {
"process_name": raw.get("process_name", "Untitled Process"),
"wip": int(raw.get("wip", 0)) if raw.get("wip") is not None else 0,
"stages": [asdict(s) for s in stages],
}
def render_markdown(normalized: dict) -> str:
"""Render a text-based BPMN-style swim-lane diagram in Markdown."""
name = normalized["process_name"]
stages = normalized["stages"]
lines: list[str] = []
lines.append(f"# Process Map: {name}")
lines.append("")
lines.append(f"**Stages:** {len(stages)} ")
lines.append(
f"**Total P50:** {sum(s['duration_minutes_p50'] for s in stages):.1f} min "
)
lines.append(
f"**Total P90:** {sum(s['duration_minutes_p90'] for s in stages):.1f} min"
)
lines.append("")
# Group by owner -> swim lane
lanes: dict[str, list[tuple[int, dict]]] = {}
for idx, s in enumerate(stages):
lanes.setdefault(s["owner"], []).append((idx, s))
lines.append("## Swim Lanes")
lines.append("")
type_glyph = {"value-add": "[V]", "wait": "[W]", "rework": "[R]"}
lane_width = max(20, max((len(o) for o in lanes), default=20) + 4)
sep = "+" + "-" * (lane_width + 2) + "+" + "-" * 72 + "+"
lines.append("```")
lines.append(sep)
lines.append(
"| " + "OWNER".ljust(lane_width) + " | " + "STAGES (in process order)".ljust(70) + " |"
)
lines.append(sep)
for owner, owned in lanes.items():
owner_cell = owner.ljust(lane_width)
cells = []
for idx, s in owned:
glyph = type_glyph.get(s["type"], "[?]")
cells.append(
f"#{idx+1} {glyph} {s['name'][:32]} "
f"(p50={s['duration_minutes_p50']:.0f}m)"
)
row_text = " -> ".join(cells)
# Wrap row_text to 70 chars
wrapped = []
cur = ""
for token in row_text.split(" "):
if len(cur) + len(token) + 1 > 70:
wrapped.append(cur)
cur = token
else:
cur = (cur + " " + token).strip()
if cur:
wrapped.append(cur)
for i, line in enumerate(wrapped):
left = owner_cell if i == 0 else " " * lane_width
lines.append(f"| {left} | {line.ljust(70)} |")
lines.append(sep)
lines.append("```")
lines.append("")
lines.append("Legend: `[V]` value-add `[W]` wait `[R]` rework")
lines.append("")
lines.append("## Linear sequence")
lines.append("")
lines.append("| # | Stage | Owner | Type | P50 (min) | P90 (min) |")
lines.append("|---|-------|-------|------|-----------|-----------|")
for idx, s in enumerate(stages):
lines.append(
f"| {idx+1} | {s['name']} | {s['owner']} | {s['type']} | "
f"{s['duration_minutes_p50']:.1f} | {s['duration_minutes_p90']:.1f} |"
)
lines.append("")
return "\n".join(lines)
def sample_process() -> dict:
return {
"process_name": "Procurement Intake (Sample)",
"wip": 12,
"stages": [
{
"name": "Requestor submits PO request",
"owner": "Requestor",
"type": "value-add",
"duration_minutes_p50": 15,
"duration_minutes_p90": 30,
},
{
"name": "Wait for manager review queue",
"owner": "Manager",
"type": "wait",
"duration_minutes_p50": 480,
"duration_minutes_p90": 1440,
},
{
"name": "Manager approves request",
"owner": "Manager",
"type": "value-add",
"duration_minutes_p50": 10,
"duration_minutes_p90": 25,
},
{
"name": "Wait for finance review queue",
"owner": "Finance",
"type": "wait",
"duration_minutes_p50": 720,
"duration_minutes_p90": 2880,
},
{
"name": "Finance validates budget code",
"owner": "Finance",
"type": "value-add",
"duration_minutes_p50": 20,
"duration_minutes_p90": 60,
},
{
"name": "Rework: missing vendor W-9",
"owner": "Requestor",
"type": "rework",
"duration_minutes_p50": 120,
"duration_minutes_p90": 360,
},
],
}
def main() -> int:
parser = argparse.ArgumentParser(
description="Document a business process as a BPMN-style swim-lane diagram."
)
parser.add_argument("--input", type=Path, help="Path to process JSON file.")
parser.add_argument(
"--output", type=Path, help="Output file path (default: stdout)."
)
parser.add_argument(
"--format",
choices=["markdown", "json"],
default="markdown",
help="Output format (default: markdown).",
)
parser.add_argument(
"--sample",
action="store_true",
help="Print a 6-stage procurement-intake sample and exit.",
)
args = parser.parse_args()
if args.sample:
raw = sample_process()
else:
if not args.input:
parser.error("--input is required unless --sample is given")
if not args.input.exists():
parser.error(f"input file not found: {args.input}")
raw = load_process(args.input)
try:
normalized = normalize(raw)
except ValueError as e:
print(f"ERROR: {e}", file=sys.stderr)
return 1
if args.format == "json":
out = json.dumps(normalized, indent=2)
else:
out = render_markdown(normalized)
if args.output:
args.output.write_text(out, encoding="utf-8")
print(f"wrote {args.output}", file=sys.stderr)
else:
print(out)
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,170 @@
---
name: vendor-management
description: Use when reviewing, scoring, or auditing third-party SaaS / vendor relationships — running a vendor scorecard, tracking SLA compliance, classifying third-party risk, preparing a tier-1 vendor review, or auditing the SaaS portfolio. Triggers on "vendor SLA", "vendor scorecard", "third-party risk", "TPRM", "vendor review", "SaaS audit", "supplier performance", "vendor health check", "renewal review". Forks context so large vendor catalogs (50-500 line items) and SLA logs don't pollute the parent thread. Ships 3 stdlib-only Python tools (vendor scorer with industry tuning, SLA compliance tracker with credit-claim flags, vendor risk classifier across 4 risk vectors), 3 reference docs each citing 7+ authoritative sources (Gartner / Shared Assessments / NIST / ISO 27036 / breach post-mortems), and a 5-vendor catalog template. Distinct from c-level-advisor/general-counsel-advisor (contract law, not operational management), business-growth/contract-and-proposal-writer (outbound proposals, not inbound vendor scoring), and sibling procurement-optimizer (spend categorization, not vendor performance).
context: fork
version: 2.8.0
author: claude-code-skills
license: MIT
tags: [bizops, vendor, sla, third-party-risk, vendor-management, saas-management, tprm]
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
---
# Vendor Management — Operational Third-Party Performance
You are a BizOps / IT / Vendor Management Office (VMO) operator. Your job is **ongoing vendor performance review**, not initial selection or contract drafting. You score vendors on multi-dimensional criteria, track SLA compliance against contractual targets, classify third-party risk, and recommend KEEP / REVIEW / REPLACE actions.
## Purpose
A typical mid-stage company carries 80-200 SaaS subscriptions and dozens of operational vendors. Most of them are reviewed only at renewal — which is too late. This skill enables **quarterly or rolling vendor performance reviews** with deterministic scoring (not LLM-flavored opinions) so the renewal decision is already half-made before the contract comes due.
## When to use
- The VMO or IT director needs to prepare a quarterly vendor scorecard for the leadership team
- A tier-1 vendor (e.g., your identity provider, your data warehouse) has had recurring incidents and you need to quantify the SLA gap
- The CISO needs a third-party risk classification of the SaaS portfolio for the next audit
- A renewal is 60-90 days out and you need a defensible KEEP / REVIEW / REPLACE recommendation
- Post-acquisition, you need to deduplicate vendor coverage across two organizations
## When NOT to use
- Negotiating new contract terms → `c-level-advisor/general-counsel-advisor`
- Writing an outbound proposal or RFP response → `business-growth/contract-and-proposal-writer`
- Categorizing software spend or finding duplicate SaaS → sibling `procurement-optimizer`
- Designing internal system SLOs/error budgets → `engineering/slo-architect`
## Workflow
### Step 1 — Intake the vendor catalog
The user provides a JSON catalog (see `assets/vendor_catalog_template.md` for the schema and a 5-vendor sample). Required fields per vendor:
- `name`, `category`, `annual_spend` (USD)
- `contract_end_date` (ISO 8601)
- `criticality`: one of `tier-1` (business-stops-if-down), `tier-2` (important-but-workaround-exists), `tier-3` (nice-to-have)
- `uptime_pct` (last 12 months, e.g., 99.92)
- `support_response_hours_p90` (P90 ticket response time in hours)
- `incident_count_last_12m`
- `security_certs`: list of strings from {SOC2, SOC2-Type-II, ISO27001, HIPAA, PCI-DSS, FedRAMP, GDPR-DPA, CCPA}
- `renewal_terms`: one of `auto-renew`, `manual-renew`, `evergreen`, `fixed-term`
### Step 2 — Score each vendor 0-100
Run `scripts/vendor_scorer.py --input catalog.json --profile <industry> --output scorecard.md`.
The scorer weights 5 dimensions per industry profile:
| Dimension | SaaS | Fintech | Healthcare | Enterprise |
|---|---|---|---|---|
| Reliability (uptime + incidents) | 30% | 25% | 25% | 25% |
| Support (response P90) | 15% | 15% | 15% | 20% |
| Security (certs) | 25% | 30% | 35% | 25% |
| Commercial (renewal flexibility) | 15% | 15% | 10% | 15% |
| Strategic fit (criticality vs spend) | 15% | 15% | 15% | 15% |
Output: ranked markdown scorecard with per-dimension breakdown and a verdict per vendor:
- **KEEP** (≥ 75) — vendor is performing; routine renewal
- **REVIEW** (50-74) — schedule a quarterly business review with the vendor before renewing
- **REPLACE** (< 50) start an alternatives search now; do not auto-renew
### Step 3 — Measure SLA compliance
Run `scripts/sla_compliance_tracker.py --input sla_records.json --output sla_report.md`.
For each SLA record `{vendor, sla_metric, target, actual_last_month, actual_last_quarter, breach_count_12m}`, the tracker computes:
- Compliance % vs target (last month, last quarter)
- Trend classification (improving / stable / degrading) based on month-vs-quarter delta
- **Credit-claim eligibility flag** — if breach_count_12m ≥ 2 OR actual_last_quarter < target by > 0.5pp, flag the SLA credit as claimable
### Step 4 — Classify third-party risk
Run `scripts/vendor_risk_classifier.py --input catalog.json --profile <industry> --output risk_matrix.md`.
Classifies each vendor as **Critical / High / Medium / Low** across 4 risk vectors (Shared Assessments SIG-Lite-ish):
1. **Data sensitivity** — PII / PHI / cardholder / source code access
2. **Financial exposure** — annual spend × tier multiplier
3. **Operational dependency** — tier-1 + no break-glass = Critical
4. **Regulatory exposure** — industry profile drives weighting (e.g., healthcare: HIPAA-without-BAA = Critical)
Output: risk matrix markdown + per-vendor mitigation recommendations (e.g., "Tier-1 with no SOC2 → require SOC2 attestation before next renewal").
### Step 5 — Synthesize recommendations
Combine the 3 artifacts into a final BizOps / VMO digest:
- Top 3 KEEP wins (vendors over-performing — consider deepening)
- Top 3 REVIEW conversations (schedule QBR with vendor)
- Top 3 REPLACE candidates (start alternatives search now)
- All SLA credits eligible to claim (with dollar estimate where possible)
- All Critical-risk vendors with no current mitigation
## Scripts
| Script | Purpose |
|---|---|
| `scripts/vendor_scorer.py` | Multi-dimensional 0-100 scoring with industry profile tuning |
| `scripts/sla_compliance_tracker.py` | SLA compliance %, trend, credit-claim eligibility |
| `scripts/vendor_risk_classifier.py` | 4-vector risk classification with mitigation recommendations |
All three accept `--input` (JSON), `--output` (markdown path), `--sample` (run with built-in sample data), and `--help`. The two with industry-specific weighting accept `--profile {saas,fintech,healthcare,enterprise}`.
## References
- `references/vendor_management_canon.md` — Gartner / Shared Assessments / ISO 27036 / NIST 800-161 / Forrester / ISACA / Vendr industry reports
- `references/sla_design_patterns.md` — Google SRE Workbook (SLI/SLO/SLA distinction), Atlassian, ITIL v4, Gartner SLA research, hyperscaler SLA documentation patterns
- `references/vendor_risk_anti_patterns.md` — Real breach post-mortems: SolarWinds, Target/HVAC, NotPetya/M.E.Doc, Capital One, Verkada, Okta 2022, log4j
## Assumptions
1. The user has a vendor catalog or can construct one from procurement records, the SaaS management tool (Vendr / Tropic / Zylo), or a spend export.
2. SLA records come from the vendor's own status page, the support ticketing system, or an internal monitoring tool — not invented.
3. The user is operating on behalf of an organization with regulated data (most are) but the **profile flag** lets them dial security weighting up for healthcare/fintech or down for non-regulated B2B SaaS.
4. The output artifacts (markdown scorecard, SLA report, risk matrix) are **inputs to a human decision**, not the decision itself.
## Anti-patterns
- **Treat all vendors at the same tier.** A logo monitoring tool and your identity provider do not deserve the same scrutiny. Use the tier field.
- **Annual review is enough.** Tier-1 vendors should be reviewed quarterly. Tier-2 semi-annually. Tier-3 at renewal.
- **Trust the security questionnaire without verification.** Ask for the SOC2 report, not a SIG checkbox. See `references/vendor_risk_anti_patterns.md`.
- **No break-glass plan for a tier-1 vendor.** If the vendor disappears tomorrow, what is the 72-hour plan?
- **Forget offboarding.** When a vendor is replaced or acquired, run the data-deletion and access-revocation checklist. SolarWinds and Okta both demonstrate why.
- **Score by gut feel.** Use the deterministic tools. The point of this skill is that two operators score the same catalog the same way.
## Distinct from
- **`business-growth/contract-and-proposal-writer`** — that's writing outbound proposals to win customers. This is scoring inbound vendors you already pay.
- **`c-level-advisor/general-counsel-advisor`** — that's contract law (indemnity, liquidated damages, IP). This is operational performance against an existing contract.
- **Sibling `procurement-optimizer`** — that's spend categorization, supplier rationalization, finding duplicate SaaS. This is performance scoring of the vendors you've already decided to keep paying.
- **`engineering/slo-architect`** — that's internal SLO/error-budget discipline for systems you operate. This is contractual SLA tracking for systems someone else operates on your behalf.
## Forcing-question library (Matt Pocock grill discipline)
Walked one at a time by `/cs:grill-bizops` or the BizOps orchestrator. Recommended answer + canon citation per question. Never bundled.
1. **"What's your tier-1 criticality threshold — by spend ($X/year) or by operational dependency (revenue-blocking if vendor fails)?"**
Recommended: operational dependency.
Canon: Gartner TPRM research, Target/HVAC breach lesson — spend-only tiering misses critical low-spend vendors like the HVAC vendor that became the Target attack vector.
2. **"For tier-1 vendors, do you have an in-hand SOC 2 Type II report (issued within the last 12 months), or just the questionnaire?"**
Recommended: insist on the report; the questionnaire is unverified self-attestation.
Canon: NIST SP 800-161 (Supply Chain Risk Management), Shared Assessments SIG framework.
3. **"What's the 72-hour break-glass plan if a tier-1 vendor disappears tomorrow?"**
Recommended: documented contingency per vendor, tested annually.
Canon: NotPetya / M.E.Doc supply chain attack, log4j response patterns.
4. **"When was the last time the SLA was actually invoked (credit claim filed)?"**
Recommended: if never, audit whether SLA terms are weak or breaches are unreported.
Canon: Atlassian SLA best practices, ITIL v4 service level management.
5. **"Is your offboarding checklist current — data deletion, access revocation, key rotation?"**
Recommended: rehearse it on one vendor per quarter.
Canon: SolarWinds + Okta 2022 breach lessons.
6. **"What's the regulatory blast-radius — HIPAA / GDPR / SOX / PCI?"**
Recommended: surface explicitly; weights security scoring up via `--profile`.
Canon: ISO/IEC 27036 (supplier relationships security).
Walk depth-first. Lock 1-3 before opening 4-6. After all are answered, invoke `vendor_scorer.py``sla_compliance_tracker.py``vendor_risk_classifier.py` in sequence.

View file

@ -0,0 +1,210 @@
# Vendor Catalog Template
The vendor-management skill's three Python tools all read the same JSON shape (with some fields used by only one tool). This template gives you the schema, the 5-vendor sample, and quick-start instructions.
## Quick start
1. Copy the JSON below to `vendor_catalog.json` in your working directory.
2. Replace the sample vendors with your real catalog.
3. Run the three tools:
```bash
python scripts/vendor_scorer.py --input vendor_catalog.json --profile saas --output scorecard.md
python scripts/sla_compliance_tracker.py --input sla_records.json --output sla_report.md
python scripts/vendor_risk_classifier.py --input vendor_catalog.json --profile saas --output risk_matrix.md
```
(SLA records are a separate file — see the SLA shape below.)
## Vendor catalog JSON schema
Each vendor in the catalog is one object in a top-level JSON array.
| Field | Type | Used by | Notes |
|---|---|---|---|
| `name` | string | all 3 | Display name |
| `category` | string | scorer, classifier | e.g., `identity`, `data-warehouse`, `crm`, `analytics` |
| `annual_spend` | number (USD) | scorer, classifier | Annualized total cost |
| `contract_end_date` | ISO 8601 string | (informational) | Useful for downstream sorting |
| `criticality` | enum | scorer, classifier | `tier-1` / `tier-2` / `tier-3` |
| `uptime_pct` | number (0-100) | scorer | Last 12 months |
| `support_response_hours_p90` | number | scorer | P90 first-response, hours |
| `incident_count_last_12m` | integer | scorer | Material incidents (not every page-fault) |
| `security_certs` | list of strings | scorer, classifier | See cert enum below |
| `renewal_terms` | enum | scorer | `auto-renew` / `manual-renew` / `evergreen` / `fixed-term` |
| `data_access` | list of strings | classifier | See data-access enum below |
| `break_glass_plan` | boolean | classifier | Do you have a documented backup plan? |
### Security cert enum
Use any combination of:
- `SOC2` (Type I)
- `SOC2-Type-II`
- `ISO27001`
- `HIPAA`
- `PCI-DSS`
- `FedRAMP`
- `GDPR-DPA`
- `CCPA`
### Data access enum
Use any combination of:
- `PHI` (Protected Health Information, HIPAA)
- `PII` (Personally Identifiable Information)
- `cardholder` (PCI scope)
- `source-code`
- `financial-records`
- `employee-records`
- `customer-emails`
- `logs-only`
- `no-customer-data`
## 5-vendor sample catalog
Copy this to `vendor_catalog.json`:
```json
[
{
"name": "Okta",
"category": "identity",
"annual_spend": 180000,
"contract_end_date": "2026-09-30",
"criticality": "tier-1",
"uptime_pct": 99.91,
"support_response_hours_p90": 4.5,
"incident_count_last_12m": 3,
"security_certs": ["SOC2-Type-II", "ISO27001", "FedRAMP", "GDPR-DPA"],
"renewal_terms": "manual-renew",
"data_access": ["PII", "employee-records"],
"break_glass_plan": true
},
{
"name": "Snowflake",
"category": "data-warehouse",
"annual_spend": 420000,
"contract_end_date": "2027-01-15",
"criticality": "tier-1",
"uptime_pct": 99.97,
"support_response_hours_p90": 2.0,
"incident_count_last_12m": 1,
"security_certs": ["SOC2-Type-II", "ISO27001", "HIPAA", "GDPR-DPA"],
"renewal_terms": "fixed-term",
"data_access": ["PII", "PHI", "financial-records"],
"break_glass_plan": false
},
{
"name": "LegacyCRM",
"category": "crm",
"annual_spend": 95000,
"contract_end_date": "2026-06-30",
"criticality": "tier-2",
"uptime_pct": 98.20,
"support_response_hours_p90": 38.0,
"incident_count_last_12m": 11,
"security_certs": ["SOC2"],
"renewal_terms": "auto-renew",
"data_access": ["PII", "customer-emails"],
"break_glass_plan": false
},
{
"name": "ChartingTool",
"category": "analytics",
"annual_spend": 8000,
"contract_end_date": "2026-08-01",
"criticality": "tier-3",
"uptime_pct": 99.50,
"support_response_hours_p90": 14.0,
"incident_count_last_12m": 2,
"security_certs": ["SOC2", "GDPR-DPA"],
"renewal_terms": "evergreen",
"data_access": ["logs-only"],
"break_glass_plan": true
},
{
"name": "BoutiqueQA",
"category": "qa-services",
"annual_spend": 220000,
"contract_end_date": "2026-12-31",
"criticality": "tier-3",
"uptime_pct": 99.00,
"support_response_hours_p90": 22.0,
"incident_count_last_12m": 6,
"security_certs": [],
"renewal_terms": "auto-renew",
"data_access": ["source-code", "PII"],
"break_glass_plan": false
}
]
```
## SLA records JSON schema
The SLA tracker takes a **separate** file (`sla_records.json`) where each record is one SLA per vendor (a vendor can have multiple).
| Field | Type | Notes |
|---|---|---|
| `vendor` | string | Must match the vendor catalog `name` |
| `sla_metric` | string | e.g., `uptime_pct`, `support_p90_response_hours`, `ticket_resolution_hours` |
| `target` | number | Contractual target |
| `actual_last_month` | number | Most recent month |
| `actual_last_quarter` | number | Trailing quarter |
| `breach_count_12m` | integer | Number of breach events in 12 months |
### Sample SLA records
```json
[
{
"vendor": "Okta",
"sla_metric": "uptime_pct",
"target": 99.99,
"actual_last_month": 99.95,
"actual_last_quarter": 99.91,
"breach_count_12m": 3
},
{
"vendor": "Snowflake",
"sla_metric": "uptime_pct",
"target": 99.9,
"actual_last_month": 99.98,
"actual_last_quarter": 99.97,
"breach_count_12m": 1
},
{
"vendor": "LegacyCRM",
"sla_metric": "support_p90_response_hours",
"target": 8.0,
"actual_last_month": 36.0,
"actual_last_quarter": 38.0,
"breach_count_12m": 11
},
{
"vendor": "ChartingTool",
"sla_metric": "uptime_pct",
"target": 99.5,
"actual_last_month": 99.6,
"actual_last_quarter": 99.5,
"breach_count_12m": 0
},
{
"vendor": "BoutiqueQA",
"sla_metric": "ticket_resolution_hours",
"target": 24.0,
"actual_last_month": 30.0,
"actual_last_quarter": 28.0,
"breach_count_12m": 6
}
]
```
## Tips for populating the catalog
- **Pull from your SaaS-management tool** (Vendr, Tropic, Zylo, BetterCloud) if you have one — it usually covers `name`, `category`, `annual_spend`, `contract_end_date`, `renewal_terms`.
- **Uptime & incidents** come from the vendor's status page archive or your monitoring tool (StatusGator, Datadog).
- **`data_access`** requires asking the vendor what data they actually touch. Don't guess — ask, and put it in writing.
- **`break_glass_plan: true`** should mean you have a **documented** 72-hour backup plan, not "we think we could figure it out."
- For tier-1 vendors, run the catalog quarterly. For tier-2, semi-annually. For tier-3, at renewal.

View file

@ -0,0 +1,90 @@
# SLA Design Patterns
This reference focuses on **measuring vendor SLAs** — what to track, what counts as a breach, when a credit claim is legitimate, and how to distinguish a contractual SLA from an internal operational SLO.
The distinction matters because most operators conflate the two. A vendor's SLA is a **commercial commitment** with credits attached. An internal SLO is an **engineering target** with no money attached. Use the right framing for the right artifact.
## 1. Google SRE Workbook — Chapter 2 ("Implementing SLOs")
The canonical distinction between SLI / SLO / SLA. From the Workbook:
- **SLI** (Indicator) — what you measure (e.g., HTTP request success rate).
- **SLO** (Objective) — internal target (e.g., 99.9% over 28 days).
- **SLA** (Agreement) — **external contractual commitment** with consequences (typically service credits).
The key operational insight: your **SLA should be looser than your SLO**, because the SLO is where you alert internally and the SLA is what you owe the customer. The same logic applies in reverse to vendor SLAs you're tracking: the vendor's SLA is your floor, not your target.
- Source: Google SRE Workbook (Beyer, Murphy, Rensin, Kawahara, Thorne, eds., O'Reilly 2018). Free online: https://sre.google/workbook/implementing-slos/
## 2. Atlassian — SLA Best Practices
Atlassian's product (Jira Service Management) drives a lot of the operational SLA practice in mid-market companies. Their published patterns:
- SLAs should be **measurable** (no "best effort" clauses — those are unenforceable).
- SLA targets should have **business meaning**, not just be round numbers (99.9% has different meaning depending on whether downtime is measured in clock-time or business hours).
- Always document **exclusions** explicitly (planned maintenance, force majeure, customer-caused outages).
- Source: Atlassian — *SLAs: Best Practices*. https://www.atlassian.com/itsm/service-request-management/slas
## 3. ITIL v4 — Service Level Management practice
ITIL v4 (the current edition, replacing v3 in 2019) defines **Service Level Management** as one of the 34 management practices. Key concepts:
- **OLA** (Operational Level Agreement) — the internal mirror of an external SLA. Often missing from vendor relationships, which is why credit claims fail.
- The "watermelon SLA" anti-pattern: SLA reports show green externally but the underlying service is rotting (red on the inside). ITIL's response: report on **customer-experienced** metrics, not vendor-self-reported ones.
- Source: AXELOS — *ITIL Foundation: ITIL 4 Edition* (Stationery Office Books, 2019). https://www.axelos.com/certifications/itil-certifications
## 4. Gartner — SLA research notes (multiple)
Gartner publishes recurring research on SLA design across vendor categories. Recurring themes:
- **Tiered SLAs by service criticality** are now industry standard (e.g., AWS has different SLAs for EC2 vs S3 vs Lambda — your contract should match the workload-vs-SLA pairing).
- **Service credits are typically capped at 10-30% of monthly fees** — if the vendor's standard SLA caps credits at < 10% on a tier-1 dependency, that's a negotiation point.
- **"100% uptime" SLAs are red flags** — no real service is 100% available; the credit clauses around such SLAs are usually unenforceable in practice.
- Source: Gartner — search "Service Level Agreement" in Gartner research portal. https://www.gartner.com/en/documents
## 5. AWS / Azure / GCP — Hyperscaler SLA documentation patterns
The three hyperscalers publish their SLAs as a public reference for the rest of the industry. Things to study:
- **AWS Service Level Agreements** — per-service pages, e.g. EC2 SLA, S3 SLA, RDS SLA. Each defines monthly uptime percentage, service credit tiers, exclusions, and the claim process. https://aws.amazon.com/legal/service-level-agreements/
- **Microsoft Azure SLAs** — same structure, but with a single consolidated SLA summary table per service. https://www.microsoft.com/licensing/docs/view/Service-Level-Agreements-SLA-for-Online-Services
- **Google Cloud SLAs** — per-product, with explicit measurement methodology (e.g., 99.99% means downtime < 4.38 minutes/month). https://cloud.google.com/terms/sla
These public SLAs are the **benchmark** for any cloud-adjacent SaaS vendor. If a vendor offers worse-than-hyperscaler SLA for an analogous service, that's negotiable.
## 6. Shawn Robertson — *Practical Guide to SLAs* (industry e-book / blog)
A practitioner-oriented guide widely cited in IT operations communities. Key themes that show up in this skill's tracker:
- **Measure the right thing**: response time vs resolution time vs uptime are three different SLAs; vendors often hide behind "we hit response SLA" when resolution is what hurt you.
- **Credit-claim eligibility is rarely automatic.** You have to file the claim, with evidence, often within a 30-90 day window. This is why the SLA tracker in this skill flags `credit_claim_eligible: YES` — to remind the operator to actually file.
- Source: Shawn Robertson — practitioner writings on IT service management. Multiple talks at itSMF / HDI conferences (search "Shawn Robertson SLA practical guide").
## 7. ISO/IEC 20000-1:2018 — Service management system requirements
The formal standard backing ITIL practice. Section 8.3.3 (Service Level Management) specifies what your SLM process must include:
- Documented SLAs for each service
- Regular review intervals
- Performance against SLAs measured and reported
- Corrective action where SLAs are not met
When a vendor claims ISO 20000 certification, this is the section that backs that claim. Verify it in the audit report — don't trust the marketing page.
- Source: ISO/IEC 20000-1:2018. https://www.iso.org/standard/70636.html
## Operational recipe (from this canon)
When tracking vendor SLAs in the tool:
1. **Map the SLI** the vendor commits to (e.g., "monthly uptime percentage").
2. **Identify the SLA target** in the contract (e.g., 99.95%).
3. **Verify the measurement methodology** — vendor's status page, your own monitoring, or third-party (Pingdom, Datadog, StatusGator)? Self-reported is least trustworthy.
4. **Track breach count over 12 months** — repeated breaches indicate systemic issues, not bad luck.
5. **File credit claims within the contractual window** — otherwise the credit is forfeited regardless of breach.
The SLA compliance tracker tool flags eligibility but does not file claims automatically. That's a human-in-the-loop step by design.

View file

@ -0,0 +1,80 @@
# Vendor Management — Canon
This reference distills the operating frameworks for ongoing third-party / vendor management. It is **not** a contract-negotiation guide (see `c-level-advisor/general-counsel-advisor`) and **not** a procurement-spend optimization guide (see sibling `procurement-optimizer`).
The canon spans seven authoritative sources spanning analyst research, formal standards, industry frameworks, and operator practice.
## 1. Gartner — Vendor Management & TPRM research
Gartner is the most-cited source for vendor segmentation models. Key concepts to internalize:
- **Strategic / Tactical / Operational vendor tiers** map roughly to tier-1 / tier-2 / tier-3 in this skill.
- **Vendor Performance Management (VPM)** vs Vendor Risk Management (VRM): performance is operational SLA + value tracking; risk is data / financial / regulatory exposure. Both belong in the VMO portfolio.
- Source: Gartner — *Magic Quadrant for IT Vendor Risk Management Solutions* (annual, since 2017). https://www.gartner.com/en/documents — search "IT Vendor Risk Management".
## 2. Shared Assessments — SIG and SIG-Lite
The **Standardized Information Gathering (SIG) Questionnaire** is the de-facto industry standard for vendor risk assessment. SIG-Lite is the abbreviated 200-question version used for low-and-medium-risk vendors; full SIG runs to ~1,800 questions.
- SIG Core domains: information security, privacy, business resilience, fourth-party management, compliance, asset management.
- The risk classifier in this skill uses a SIG-Lite-*ish* simplification — 4 vectors instead of 18 domains. For tier-1 critical vendors, the full SIG is appropriate.
- Source: Shared Assessments Program. https://sharedassessments.org/sig/
## 3. ISO/IEC 27036 — Information security for supplier relationships
A formal ISO standard (parts 1-4) covering the full lifecycle of supplier security relationships:
- **27036-1**: Overview and concepts
- **27036-2**: Common requirements (the workhorse part for vendor management)
- **27036-3**: ICT supply chain security
- **27036-4**: Cloud service customer/provider relationships
Useful when a vendor claims ISO27001 — the matching 27036 control set tells you what supplier-relationship clauses the auditor expected them to operate against.
- Source: ISO/IEC 27036 series. https://www.iso.org/standard/59648.html
## 4. NIST SP 800-161 (Rev. 1) — Cybersecurity Supply Chain Risk Management (C-SCRM)
The U.S. federal standard for supply-chain risk. Even commercial orgs use 800-161 as a checklist:
- 8 foundational practices (e.g., integrate C-SCRM into acquisition, use a risk-based approach, identify and protect critical assets).
- Detailed control overlays mapped to NIST SP 800-53 controls.
- Strong framework for **fourth-party** risk (vendors-of-your-vendors) — often where the actual breach originates (SolarWinds being the canonical example).
- Source: NIST SP 800-161 Rev. 1 (May 2022). https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-161r1.pdf
## 5. Forrester — Third-Party Risk Management Wave
Forrester's TPRM Wave is the second-most-cited analyst source (after Gartner) and tends to be more practitioner-flavored:
- Forrester's framing: **continuous monitoring** beats point-in-time annual assessments. The SLA compliance tracker in this skill is built on this premise.
- Key metric Forrester pushes: **mean time to detect (MTTD)** for third-party incidents — most orgs are > 60 days, which is too long for tier-1 vendors.
- Source: Forrester — *The Forrester Wave: Third-Party Risk Management Platforms* (biennial). https://www.forrester.com/research/
## 6. ISACA — TPRM framework + COBIT 2019 alignment
ISACA (the auditor body behind CISM and COBIT) publishes a pragmatic TPRM framework that aligns to **COBIT 2019** controls:
- COBIT APO10 ("Managed Vendors") is the relevant process domain: vendor selection, contract management, performance, risk, and termination.
- ISACA's TPRM guidance is heavy on **audit evidence** — what artifacts to keep so a SOC2 / ISO27001 auditor can verify your TPRM is operating.
- Source: ISACA — *Third Party Risk Management Audit Program* + COBIT 2019. https://www.isaca.org/resources/cobit and https://www.isaca.org/bookstore
## 7. Vendr & Tropic — Industry SaaS-management reports (annual)
Two leading SaaS-management vendors publish annual reports that quantify the operational reality of SaaS sprawl. They're not academic, but they're the only source that benchmarks actual companies:
- **Vendr SaaS Trends Report** — typical company has 130-200 SaaS subscriptions, average 30% YoY growth in software spend, ~20% redundancy at large orgs. https://www.vendr.com/blog
- **Tropic State of SaaS Spend Report** — auto-renew traps cost the average mid-market company ~7% of total SaaS spend annually. https://www.tropicapp.io/resources
- Both reports emphasize: **the renewal date is too late** to start vendor review. Quarterly rolling review is the operating cadence to aim for.
## How this canon maps to the tools in this skill
| Tool | Primary canon |
|---|---|
| `vendor_scorer.py` | Gartner VPM + Vendr/Tropic operational benchmarks |
| `sla_compliance_tracker.py` | Forrester continuous monitoring + Atlassian/ITIL service level patterns (see `sla_design_patterns.md`) |
| `vendor_risk_classifier.py` | Shared Assessments SIG + ISO 27036 + NIST SP 800-161 |
When in doubt: SIG-Lite is the floor for tier-2 and -3 vendors; full SIG + ISO 27036 + 800-161 for tier-1.

View file

@ -0,0 +1,100 @@
# Vendor Risk Anti-Patterns — Lessons from Real Breaches
The strongest argument for serious TPRM discipline is post-mortems from real third-party-originated incidents. This reference catalogues seven canonical breaches and the operational anti-patterns each one demonstrates.
The point of this reference is **not** to scare. The point is: every one of these incidents had a vendor-management anti-pattern at its root, and most of them were avoidable with the discipline this skill enforces.
## 1. SolarWinds Orion (2020) — Fourth-party / supply-chain compromise
Russian state-aligned actors (UNC2452 / Cozy Bear) inserted backdoor code into the SolarWinds Orion software update pipeline. ~18,000 organizations installed the trojanized update; ~100 (including U.S. federal agencies and major enterprises) had follow-on intrusions.
**Anti-patterns demonstrated:**
- **No software supply-chain verification.** The orgs that installed the update never verified the integrity of the binary beyond "the vendor's update server said so."
- **Implicit trust in tier-1 monitoring vendor.** Orion was deployed with extraordinary network access; no one re-evaluated whether that access level was justified.
- **No fourth-party visibility.** SolarWinds' own dev pipeline was the actual breach point — most customers had never asked who SolarWinds' suppliers were.
Source: CISA Alert AA20-352A (Dec 2020). https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-352a
## 2. Target / Fazio Mechanical (2013) — HVAC vendor pivot
The 40M-card Target breach originated through Fazio Mechanical Services, a refrigeration / HVAC vendor with billing-system access to Target's network. Attackers phished Fazio, used Fazio's credentials to access Target's vendor portal, and pivoted from there into the POS network.
**Anti-patterns demonstrated:**
- **Excessive vendor network access.** An HVAC vendor needed network access to a vendor portal — fine. But that network was not segmented from POS systems, which is the failure.
- **No vendor risk tier evaluation.** Fazio was probably classified as tier-3 (a maintenance vendor). But the **access** they had made them effectively tier-1.
**Lesson:** Risk tier ≠ business criticality. A janitorial vendor with badge-system access can be a tier-1 attack surface.
Source: U.S. Senate Commerce Committee Report (Mar 2014). https://www.commerce.senate.gov/services/files/24d3c229-4f2f-405d-b8db-a3a67f183883
## 3. NotPetya / M.E.Doc (2017) — Trusted update mechanism weaponized
The NotPetya malware was injected via the update mechanism of M.E.Doc, a Ukrainian tax-reporting software used by ~80% of Ukrainian businesses. Spillover damage hit Maersk, FedEx (TNT), Merck, Mondelez — total damages > $10 billion globally. Maersk alone reported ~$300M loss and a 10-day operational outage.
**Anti-patterns demonstrated:**
- **Trusted-update-channel assumption.** No one verified the signed updates from M.E.Doc — its update key had been compromised for months.
- **Geographic concentration without geographic diversification.** Maersk's exposure was through a Ukrainian subsidiary; the parent had no breakglass for losing 100% of that subsidiary's systems for two weeks.
**Lesson:** A vendor used by 80%+ of your local market is effectively a single point of failure.
Source: Wired's Maersk NotPetya retrospective by Andy Greenberg (Aug 2018). https://www.wired.com/story/notpetya-cyberattack-ukraine-russia-code-crashed-the-world/
## 4. Capital One (2019) — AWS misconfiguration via former employee
A former AWS employee exploited a Capital One server-side request forgery (SSRF) vulnerability to access 100M+ customer records held in S3. Total cost to Capital One: ~$190M in regulatory fines + settlement.
**Anti-patterns demonstrated:**
- **Shared-responsibility model misunderstood.** Capital One assumed AWS would catch the misconfiguration. AWS's model puts misconfiguration responsibility on the customer.
- **No third-party penetration testing of the cloud config.** A reasonably scoped TPRM-driven pen test would have caught the SSRF.
**Lesson:** Cloud vendor due diligence must include "what is **my** responsibility under their shared-responsibility model?" — not just "are they SOC2?"
Source: Capital One incident summary + OCC consent order (Aug 2020). https://occ.gov/news-issuances/news-releases/2020/nr-occ-2020-101.html
## 5. Verkada (2021) — Camera vendor super-admin compromise
A hacking group obtained super-admin credentials to Verkada, a cloud-based security-camera vendor. Result: live-feed access to 150,000 cameras across hospitals, prisons, schools, Tesla factories, and corporate offices. The credentials were apparently exposed in a public Jenkins server.
**Anti-patterns demonstrated:**
- **Super-admin tooling without MFA enforcement.** Verkada had a super-admin role that bypassed customer-tenant boundaries — and apparently wasn't MFA-enforced.
- **No customer-side visibility into vendor admin actions.** Customers had no way to detect that vendor super-admins had viewed their feeds.
**Lesson:** For any SaaS handling sensitive data, ask: "Do your engineers have super-admin access to my tenant? How is that access logged and how can I audit it?"
Source: Bloomberg reporting (Mar 2021). https://www.bloomberg.com/news/articles/2021-03-09/hackers-expose-tesla-jails-in-breach-of-150-000-security-cameras
## 6. Okta (2022) — Lapsus$ / Sitel third-party support compromise
The Lapsus$ group compromised a Sitel customer-support engineer who had remote-support tooling access to Okta tenant data. Window of access: ~5 days. Okta's initial public disclosure was widely criticized as too slow and underplayed.
**Anti-patterns demonstrated:**
- **Subcontractor-of-subcontractor risk.** Sitel was Okta's outsourced support; the compromised engineer was Sitel's. Most Okta customers had no idea Sitel existed.
- **Slow disclosure of vendor incidents to downstream customers.** Customers found out about the breach months after Okta became aware internally.
**Lesson:** Contractually require your tier-1 vendors to disclose incidents within 24-72 hours, not "when investigation completes." This is now standard in DPAs but often missing from older contracts.
Source: Okta's official Lapsus$ statement updates (Mar-Apr 2022). https://www.okta.com/blog/2022/03/updated-okta-statement-on-lapsus/
## 7. Log4Shell / log4j (2021) — Open-source dependency as a vendor
CVE-2021-44228 in the log4j Java logging library affected ~3 billion devices and embedded in tens of thousands of commercial vendor products. Most affected orgs had no idea log4j was in their supply chain because it was a transitive dependency of vendor SaaS, not a direct dependency.
**Anti-patterns demonstrated:**
- **Open-source dependencies treated as "not vendors."** They are. They have SLAs (effectively zero), have security disclosure processes (variable), and have maintainers who can disappear.
- **No SBOM (Software Bill of Materials) requested from vendors.** Customers couldn't tell which of their vendors were affected.
**Lesson:** Add an SBOM requirement to vendor contracts for tier-1 and tier-2 vendors. Without an SBOM, every new transitive CVE is a multi-week fire drill.
Source: CISA Apache Log4j Vulnerability Guidance. https://www.cisa.gov/news-events/news/apache-log4j-vulnerability-guidance
## Synthesis: The 7 vendor-risk anti-patterns to avoid
1. **Treat all vendors at the same tier.** Tier-1 vendors get quarterly review + full SIG. Tier-2 semi-annual. Tier-3 at renewal. Network-access privilege is the override — see Target.
2. **Annual review is enough.** It isn't. Continuous monitoring (Forrester) + quarterly QBR is the operating cadence.
3. **Trust the vendor security questionnaire without verification.** Ask for the SOC2 Type II report. Read the exceptions section. Verify cert validity dates.
4. **No break-glass plan for a tier-1 vendor.** If the vendor disappears tomorrow (acquisition, bankruptcy, NotPetya-class outage), what's the 72-hour plan? Document it before you need it.
5. **No offboarding checklist when vendor changes hands.** SolarWinds and Okta both demonstrate why you need a data-deletion + access-revocation runbook ready to execute.
6. **Ignore fourth parties.** Your vendors have vendors. For tier-1, ask: "Who are your top 5 subcontractors? Which ones have access to my data?"
7. **No SBOM for SaaS vendors.** When the next log4j-class CVE drops, you want to be able to query a list, not start an email thread.
The risk classifier in this skill catches most of these via the 4-vector classification, but the **mitigations** are the human-in-the-loop step. Use them.

View file

@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
sla_compliance_tracker.py Per-vendor SLA compliance tracking.
Takes JSON of SLA records {vendor, sla_metric, target, actual_last_month,
actual_last_quarter, breach_count_12m}. Computes:
- Compliance % vs target (last month, last quarter)
- Trend classification (improving / stable / degrading)
- Credit-claim eligibility flag (per typical SLA credit clauses)
Output: per-vendor compliance scorecard markdown with action items.
Stdlib only. Deterministic. No LLM calls.
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any
class Trend(str, Enum):
IMPROVING = "improving"
STABLE = "stable"
DEGRADING = "degrading"
class ComplianceState(str, Enum):
MET = "met"
AT_RISK = "at-risk"
BREACHED = "breached"
@dataclass
class SLAResult:
vendor: str
sla_metric: str
target: float
actual_last_month: float
actual_last_quarter: float
breach_count_12m: int
compliance_month_pct: float
compliance_quarter_pct: float
state: ComplianceState
trend: Trend
credit_claim_eligible: bool
action_items: list[str]
# A small library of typical SLA credit-claim thresholds.
# breach_count_12m >= 2 OR actual_last_quarter < target by > 0.5pp -> eligible.
# This is "SIG-Lite-ish" — operators tune per actual contract.
CREDIT_CLAIM_DELTA_PP = 0.5 # percentage points (or hours) below/above target
# Metrics where lower is better (response time, resolution hours, etc.).
# Anything else (uptime_pct, throughput, etc.) is "higher is better."
_LOWER_IS_BETTER_HINTS = (
"response",
"resolution",
"latency",
"hours",
"minutes",
"mttr",
"time_to",
)
def _is_lower_better(sla_metric: str) -> bool:
metric_lc = sla_metric.lower()
return any(h in metric_lc for h in _LOWER_IS_BETTER_HINTS)
def _compute_compliance_pct(actual: float, target: float, lower_is_better: bool) -> float:
"""Compliance % capped at 100. Direction depends on metric semantics."""
if target <= 0:
return 100.0
if lower_is_better:
# actual <= target -> 100%. actual = 2*target -> 50%. actual = 4*target -> 25%.
return round(min(100.0, (target / actual) * 100.0), 2) if actual > 0 else 100.0
return round(min(100.0, (actual / target) * 100.0), 2)
def _classify_state(
actual_last_quarter: float, target: float, lower_is_better: bool
) -> ComplianceState:
if lower_is_better:
if actual_last_quarter <= target:
return ComplianceState.MET
if actual_last_quarter <= target * 1.10: # within 10% over
return ComplianceState.AT_RISK
return ComplianceState.BREACHED
# higher is better
if actual_last_quarter >= target:
return ComplianceState.MET
if actual_last_quarter >= target - 0.25:
return ComplianceState.AT_RISK
return ComplianceState.BREACHED
def _classify_trend(
actual_last_month: float, actual_last_quarter: float, lower_is_better: bool
) -> Trend:
delta = actual_last_month - actual_last_quarter
# For lower-is-better metrics, a negative delta (smaller now) is improving.
if lower_is_better:
delta = -delta
if delta > 0.1:
return Trend.IMPROVING
if delta < -0.1:
return Trend.DEGRADING
return Trend.STABLE
def _credit_eligible(
actual_last_quarter: float,
target: float,
breach_count_12m: int,
lower_is_better: bool,
) -> bool:
if breach_count_12m >= 2:
return True
if lower_is_better:
if actual_last_quarter > (target + CREDIT_CLAIM_DELTA_PP):
return True
else:
if actual_last_quarter < (target - CREDIT_CLAIM_DELTA_PP):
return True
return False
def _build_action_items(
state: ComplianceState,
trend: Trend,
credit_eligible: bool,
breach_count_12m: int,
) -> list[str]:
items: list[str] = []
if credit_eligible:
items.append("Open an SLA credit-claim ticket with the vendor's CSM.")
if state == ComplianceState.BREACHED:
items.append("Escalate to vendor exec sponsor. Request root-cause analysis.")
if state == ComplianceState.AT_RISK and trend == Trend.DEGRADING:
items.append("Schedule QBR within 30 days. Trend will breach if uncorrected.")
if breach_count_12m >= 4:
items.append(
f"{breach_count_12m} breaches in 12 months — flag vendor as REVIEW in next scorecard."
)
if state == ComplianceState.MET and trend == Trend.IMPROVING and breach_count_12m == 0:
items.append("No action required. Acknowledge in next vendor business review.")
if not items:
items.append("Monitor — no immediate action.")
return items
def evaluate_sla(record: dict[str, Any]) -> SLAResult:
target = float(record["target"])
actual_month = float(record["actual_last_month"])
actual_quarter = float(record["actual_last_quarter"])
breach_count = int(record.get("breach_count_12m", 0))
sla_metric = str(record["sla_metric"])
lower_is_better = _is_lower_better(sla_metric)
state = _classify_state(actual_quarter, target, lower_is_better)
trend = _classify_trend(actual_month, actual_quarter, lower_is_better)
eligible = _credit_eligible(actual_quarter, target, breach_count, lower_is_better)
return SLAResult(
vendor=str(record["vendor"]),
sla_metric=sla_metric,
target=target,
actual_last_month=actual_month,
actual_last_quarter=actual_quarter,
breach_count_12m=breach_count,
compliance_month_pct=_compute_compliance_pct(
actual_month, target, lower_is_better
),
compliance_quarter_pct=_compute_compliance_pct(
actual_quarter, target, lower_is_better
),
state=state,
trend=trend,
credit_claim_eligible=eligible,
action_items=_build_action_items(state, trend, eligible, breach_count),
)
# ---------- Markdown rendering ----------
def render_markdown(results: list[SLAResult]) -> str:
lines: list[str] = []
lines.append("# SLA Compliance Report")
lines.append("")
# Summary
total = len(results)
breached = sum(1 for r in results if r.state == ComplianceState.BREACHED)
at_risk = sum(1 for r in results if r.state == ComplianceState.AT_RISK)
eligible = [r for r in results if r.credit_claim_eligible]
lines.append(
f"**Summary:** {total} SLAs tracked · {breached} breached · {at_risk} at risk · "
f"{len(eligible)} credit-claim eligible."
)
lines.append("")
# Detail table
lines.append("## Per-SLA Status")
lines.append("")
lines.append(
"| Vendor | SLA Metric | Target | Last Month | Last Quarter | "
"Compliance Q | State | Trend | Breaches 12m | Credit Eligible |"
)
lines.append(
"|---|---|---|---|---|---|---|---|---|---|"
)
for r in results:
lines.append(
f"| {r.vendor} | {r.sla_metric} | {r.target} | {r.actual_last_month} | "
f"{r.actual_last_quarter} | {r.compliance_quarter_pct}% | "
f"{r.state.value} | {r.trend.value} | {r.breach_count_12m} | "
f"{'YES' if r.credit_claim_eligible else 'no'} |"
)
lines.append("")
# Action items
lines.append("## Action Items")
lines.append("")
for r in results:
lines.append(f"### {r.vendor}{r.sla_metric}")
for item in r.action_items:
lines.append(f"- {item}")
lines.append("")
# Credit-claim shortlist
if eligible:
lines.append("## Credit-Claim Shortlist")
lines.append("")
lines.append("| Vendor | SLA | Target | Last Q | Breaches 12m |")
lines.append("|---|---|---|---|---|")
for r in eligible:
lines.append(
f"| {r.vendor} | {r.sla_metric} | {r.target} | {r.actual_last_quarter} | "
f"{r.breach_count_12m} |"
)
lines.append("")
return "\n".join(lines)
# ---------- Sample data ----------
SAMPLE_RECORDS: list[dict[str, Any]] = [
{
"vendor": "Okta",
"sla_metric": "uptime_pct",
"target": 99.99,
"actual_last_month": 99.95,
"actual_last_quarter": 99.91,
"breach_count_12m": 3,
},
{
"vendor": "Snowflake",
"sla_metric": "uptime_pct",
"target": 99.9,
"actual_last_month": 99.98,
"actual_last_quarter": 99.97,
"breach_count_12m": 1,
},
{
"vendor": "LegacyCRM",
"sla_metric": "support_p90_response_hours",
"target": 8.0,
"actual_last_month": 36.0,
"actual_last_quarter": 38.0,
"breach_count_12m": 11,
},
{
"vendor": "ChartingTool",
"sla_metric": "uptime_pct",
"target": 99.5,
"actual_last_month": 99.6,
"actual_last_quarter": 99.5,
"breach_count_12m": 0,
},
{
"vendor": "BoutiqueQA",
"sla_metric": "ticket_resolution_hours",
"target": 24.0,
"actual_last_month": 30.0,
"actual_last_quarter": 28.0,
"breach_count_12m": 6,
},
]
# ---------- CLI ----------
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Track per-vendor SLA compliance and flag credit-claim eligibility."
)
parser.add_argument("--input", type=Path, help="Path to JSON SLA records.")
parser.add_argument("--output", type=Path, help="Path to write markdown report.")
parser.add_argument(
"--sample", action="store_true", help="Run against built-in sample SLA records."
)
args = parser.parse_args(argv)
if not args.sample and not args.input:
parser.error("provide --input or --sample")
if args.sample:
records = SAMPLE_RECORDS
else:
try:
records = json.loads(args.input.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"error reading {args.input}: {exc}", file=sys.stderr)
return 2
if not isinstance(records, list):
print("input JSON must be a list of SLA record objects", file=sys.stderr)
return 2
results = [evaluate_sla(r) for r in records]
md = render_markdown(results)
if args.output:
args.output.write_text(md, encoding="utf-8")
print(f"wrote {args.output}")
else:
print(md)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,372 @@
#!/usr/bin/env python3
"""
vendor_risk_classifier.py Classify third-party risk across 4 vectors.
Inspired by Shared Assessments SIG-Lite + NIST SP 800-161 supply chain risk.
Classifies each vendor as Critical / High / Medium / Low across:
1. Data sensitivity PII / PHI / cardholder / source code access
2. Financial exposure annual spend × tier multiplier
3. Operational dependency tier-1 + no break-glass = Critical
4. Regulatory exposure industry profile drives weighting
Industry profile ({saas,fintech,healthcare,enterprise}) re-weights regulatory.
Output: risk matrix markdown + per-vendor mitigation recommendations.
Stdlib only. Deterministic. No LLM calls.
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any
class RiskLevel(str, Enum):
LOW = "Low"
MEDIUM = "Medium"
HIGH = "High"
CRITICAL = "Critical"
_LEVEL_RANK = {
RiskLevel.LOW: 0,
RiskLevel.MEDIUM: 1,
RiskLevel.HIGH: 2,
RiskLevel.CRITICAL: 3,
}
@dataclass
class RiskBreakdown:
data_sensitivity: RiskLevel
financial_exposure: RiskLevel
operational_dependency: RiskLevel
regulatory_exposure: RiskLevel
@dataclass
class RiskClassification:
vendor: str
category: str
overall: RiskLevel
breakdown: RiskBreakdown
mitigations: list[str]
# ---------- Per-vector classifiers ----------
_DATA_SENSITIVITY_KEYS = {
"PHI": RiskLevel.CRITICAL,
"PII": RiskLevel.HIGH,
"cardholder": RiskLevel.CRITICAL,
"source-code": RiskLevel.HIGH,
"financial-records": RiskLevel.HIGH,
"employee-records": RiskLevel.HIGH,
"customer-emails": RiskLevel.MEDIUM,
"logs-only": RiskLevel.LOW,
"no-customer-data": RiskLevel.LOW,
}
def classify_data_sensitivity(vendor: dict[str, Any]) -> RiskLevel:
"""Choose worst of declared data_access tags. Default Medium if unspecified."""
tags = vendor.get("data_access") or []
if not tags:
return RiskLevel.MEDIUM
levels = [_DATA_SENSITIVITY_KEYS.get(t, RiskLevel.MEDIUM) for t in tags]
return max(levels, key=lambda lv: _LEVEL_RANK[lv])
def classify_financial_exposure(vendor: dict[str, Any]) -> RiskLevel:
spend = float(vendor.get("annual_spend", 0))
crit = str(vendor.get("criticality", "tier-3"))
multiplier = {"tier-1": 1.5, "tier-2": 1.0, "tier-3": 0.6}.get(crit, 0.6)
weighted = spend * multiplier
if weighted >= 500_000:
return RiskLevel.CRITICAL
if weighted >= 150_000:
return RiskLevel.HIGH
if weighted >= 50_000:
return RiskLevel.MEDIUM
return RiskLevel.LOW
def classify_operational_dependency(vendor: dict[str, Any]) -> RiskLevel:
crit = str(vendor.get("criticality", "tier-3"))
has_breakglass = bool(vendor.get("break_glass_plan", False))
if crit == "tier-1" and not has_breakglass:
return RiskLevel.CRITICAL
if crit == "tier-1":
return RiskLevel.HIGH
if crit == "tier-2" and not has_breakglass:
return RiskLevel.HIGH
if crit == "tier-2":
return RiskLevel.MEDIUM
return RiskLevel.LOW
_REGULATORY_PROFILE: dict[str, dict[str, RiskLevel]] = {
# Per-profile, mapping of cert presence to risk reduction.
# Worst case before mitigations:
# healthcare requires HIPAA, fintech requires SOC2-Type-II + PCI-DSS (if cardholder).
"saas": {},
"fintech": {},
"healthcare": {},
"enterprise": {},
}
def classify_regulatory_exposure(
vendor: dict[str, Any], profile: str
) -> RiskLevel:
certs = set(vendor.get("security_certs") or [])
data_tags = set(vendor.get("data_access") or [])
if profile == "healthcare":
if "PHI" in data_tags and "HIPAA" not in certs:
return RiskLevel.CRITICAL
if "PHI" in data_tags:
return RiskLevel.HIGH
if "PII" in data_tags and "SOC2-Type-II" not in certs:
return RiskLevel.HIGH
return RiskLevel.MEDIUM
if profile == "fintech":
if "cardholder" in data_tags and "PCI-DSS" not in certs:
return RiskLevel.CRITICAL
if "cardholder" in data_tags:
return RiskLevel.HIGH
if "SOC2-Type-II" not in certs and "ISO27001" not in certs:
return RiskLevel.HIGH
return RiskLevel.MEDIUM
if profile == "enterprise":
if "PII" in data_tags and "SOC2-Type-II" not in certs:
return RiskLevel.HIGH
if "SOC2" not in certs and "SOC2-Type-II" not in certs:
return RiskLevel.MEDIUM
return RiskLevel.LOW
# saas (default)
if "PII" in data_tags and "SOC2" not in certs and "SOC2-Type-II" not in certs:
return RiskLevel.HIGH
if "PII" in data_tags:
return RiskLevel.MEDIUM
return RiskLevel.LOW
def overall_risk(breakdown: RiskBreakdown) -> RiskLevel:
# Overall = worst-of, with one nuance: two HIGH vectors -> CRITICAL.
levels = [
breakdown.data_sensitivity,
breakdown.financial_exposure,
breakdown.operational_dependency,
breakdown.regulatory_exposure,
]
worst = max(levels, key=lambda lv: _LEVEL_RANK[lv])
high_count = sum(1 for lv in levels if lv == RiskLevel.HIGH)
if worst == RiskLevel.HIGH and high_count >= 2:
return RiskLevel.CRITICAL
return worst
def build_mitigations(
vendor: dict[str, Any], breakdown: RiskBreakdown, profile: str
) -> list[str]:
mits: list[str] = []
certs = set(vendor.get("security_certs") or [])
data_tags = set(vendor.get("data_access") or [])
if breakdown.data_sensitivity in {RiskLevel.HIGH, RiskLevel.CRITICAL}:
mits.append(
"Confirm data-processing addendum (DPA) is current. Require encryption at rest + in transit."
)
if breakdown.financial_exposure in {RiskLevel.HIGH, RiskLevel.CRITICAL}:
mits.append(
"Require liability cap parity (≥ 12 months of fees). Confirm insurance certificate on file."
)
if breakdown.operational_dependency == RiskLevel.CRITICAL:
mits.append(
"Document a 72-hour break-glass plan. Identify and pre-qualify a backup vendor."
)
if breakdown.regulatory_exposure == RiskLevel.CRITICAL:
if profile == "healthcare" and "HIPAA" not in certs:
mits.append("Block PHI access until HIPAA BAA is signed and certs verified.")
if profile == "fintech" and "cardholder" in data_tags and "PCI-DSS" not in certs:
mits.append("Block cardholder data until PCI-DSS AOC (Attestation) is on file.")
if breakdown.regulatory_exposure in {RiskLevel.HIGH, RiskLevel.CRITICAL}:
mits.append("Request most recent SOC2 Type II report; review exceptions section.")
if not mits:
mits.append("No critical mitigations required; routine annual review.")
return mits
def classify_vendor(vendor: dict[str, Any], profile: str) -> RiskClassification:
breakdown = RiskBreakdown(
data_sensitivity=classify_data_sensitivity(vendor),
financial_exposure=classify_financial_exposure(vendor),
operational_dependency=classify_operational_dependency(vendor),
regulatory_exposure=classify_regulatory_exposure(vendor, profile),
)
return RiskClassification(
vendor=str(vendor.get("name", "Unknown")),
category=str(vendor.get("category", "uncategorized")),
overall=overall_risk(breakdown),
breakdown=breakdown,
mitigations=build_mitigations(vendor, breakdown, profile),
)
# ---------- Markdown rendering ----------
def render_markdown(results: list[RiskClassification], profile: str) -> str:
by_overall = sorted(
results, key=lambda r: _LEVEL_RANK[r.overall], reverse=True
)
lines: list[str] = []
lines.append(f"# Vendor Risk Matrix — `{profile}` profile")
lines.append("")
crit = [r for r in by_overall if r.overall == RiskLevel.CRITICAL]
high = [r for r in by_overall if r.overall == RiskLevel.HIGH]
lines.append(
f"**Summary:** {len(crit)} Critical · {len(high)} High · "
f"{sum(1 for r in by_overall if r.overall == RiskLevel.MEDIUM)} Medium · "
f"{sum(1 for r in by_overall if r.overall == RiskLevel.LOW)} Low"
)
lines.append("")
lines.append("## Risk Matrix")
lines.append("")
lines.append(
"| Vendor | Category | Data | Financial | Operational | Regulatory | Overall |"
)
lines.append("|---|---|---|---|---|---|---|")
for r in by_overall:
b = r.breakdown
lines.append(
f"| {r.vendor} | {r.category} | {b.data_sensitivity.value} | "
f"{b.financial_exposure.value} | {b.operational_dependency.value} | "
f"{b.regulatory_exposure.value} | **{r.overall.value}** |"
)
lines.append("")
lines.append("## Mitigations")
lines.append("")
for r in by_overall:
lines.append(f"### {r.vendor}{r.overall.value}")
for m in r.mitigations:
lines.append(f"- {m}")
lines.append("")
return "\n".join(lines)
# ---------- Sample data ----------
SAMPLE_VENDORS: list[dict[str, Any]] = [
{
"name": "Okta",
"category": "identity",
"annual_spend": 180_000,
"criticality": "tier-1",
"data_access": ["PII", "employee-records"],
"security_certs": ["SOC2-Type-II", "ISO27001", "FedRAMP", "GDPR-DPA"],
"break_glass_plan": True,
},
{
"name": "Snowflake",
"category": "data-warehouse",
"annual_spend": 420_000,
"criticality": "tier-1",
"data_access": ["PII", "PHI", "financial-records"],
"security_certs": ["SOC2-Type-II", "ISO27001", "HIPAA", "GDPR-DPA"],
"break_glass_plan": False,
},
{
"name": "LegacyCRM",
"category": "crm",
"annual_spend": 95_000,
"criticality": "tier-2",
"data_access": ["PII", "customer-emails"],
"security_certs": ["SOC2"],
"break_glass_plan": False,
},
{
"name": "ChartingTool",
"category": "analytics",
"annual_spend": 8_000,
"criticality": "tier-3",
"data_access": ["logs-only"],
"security_certs": ["SOC2", "GDPR-DPA"],
"break_glass_plan": True,
},
{
"name": "BoutiqueQA",
"category": "qa-services",
"annual_spend": 220_000,
"criticality": "tier-3",
"data_access": ["source-code", "PII"],
"security_certs": [],
"break_glass_plan": False,
},
]
# ---------- CLI ----------
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Classify vendor risk across 4 vectors with industry profile tuning."
)
parser.add_argument("--input", type=Path, help="Path to JSON vendor catalog.")
parser.add_argument(
"--profile",
choices=["saas", "fintech", "healthcare", "enterprise"],
default="saas",
help="Industry profile for regulatory weighting (default: saas).",
)
parser.add_argument("--output", type=Path, help="Path to write markdown risk matrix.")
parser.add_argument(
"--sample", action="store_true", help="Run against built-in 5-vendor sample."
)
args = parser.parse_args(argv)
if not args.sample and not args.input:
parser.error("provide --input or --sample")
if args.sample:
catalog = SAMPLE_VENDORS
else:
try:
catalog = json.loads(args.input.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"error reading {args.input}: {exc}", file=sys.stderr)
return 2
if not isinstance(catalog, list):
print("input JSON must be a list of vendor objects", file=sys.stderr)
return 2
results = [classify_vendor(v, args.profile) for v in catalog]
md = render_markdown(results, args.profile)
if args.output:
args.output.write_text(md, encoding="utf-8")
print(f"wrote {args.output}")
else:
print(md)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,431 @@
#!/usr/bin/env python3
"""
vendor_scorer.py Multi-dimensional 0-100 vendor scoring with industry profile tuning.
Scores each vendor across 5 weighted dimensions:
1. Reliability uptime % + incident count
2. Support P90 ticket response hours
3. Security security certifications coverage
4. Commercial renewal flexibility
5. Strategic fit criticality vs annual spend
Industry profiles ({saas,fintech,healthcare,enterprise}) re-weight the dimensions.
Output: ranked markdown scorecard with per-dimension breakdown + verdict (KEEP/REVIEW/REPLACE).
Stdlib only. Deterministic. No LLM calls.
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any
# ---------- Industry profile weights ----------
PROFILES: dict[str, dict[str, float]] = {
"saas": {
"reliability": 0.30,
"support": 0.15,
"security": 0.25,
"commercial": 0.15,
"strategic_fit": 0.15,
},
"fintech": {
"reliability": 0.25,
"support": 0.15,
"security": 0.30,
"commercial": 0.15,
"strategic_fit": 0.15,
},
"healthcare": {
"reliability": 0.25,
"support": 0.15,
"security": 0.35,
"commercial": 0.10,
"strategic_fit": 0.15,
},
"enterprise": {
"reliability": 0.25,
"support": 0.20,
"security": 0.25,
"commercial": 0.15,
"strategic_fit": 0.15,
},
}
CERT_VALUE: dict[str, int] = {
"SOC2": 15,
"SOC2-Type-II": 25,
"ISO27001": 20,
"HIPAA": 15,
"PCI-DSS": 15,
"FedRAMP": 20,
"GDPR-DPA": 10,
"CCPA": 5,
}
RENEWAL_SCORE: dict[str, int] = {
"manual-renew": 100,
"fixed-term": 80,
"evergreen": 50,
"auto-renew": 35,
}
# ---------- Data shape ----------
class Verdict(str, Enum):
KEEP = "KEEP"
REVIEW = "REVIEW"
REPLACE = "REPLACE"
@dataclass
class DimensionBreakdown:
reliability: float
support: float
security: float
commercial: float
strategic_fit: float
def as_dict(self) -> dict[str, float]:
return {
"reliability": self.reliability,
"support": self.support,
"security": self.security,
"commercial": self.commercial,
"strategic_fit": self.strategic_fit,
}
@dataclass
class ScoredVendor:
name: str
category: str
annual_spend: float
criticality: str
overall: float
verdict: Verdict
dimensions: DimensionBreakdown
notes: list[str] = field(default_factory=list)
# ---------- Per-dimension scoring (deterministic) ----------
def score_reliability(uptime_pct: float, incident_count: int) -> float:
"""Reliability = uptime_pct mapped to 0-100, penalized by incidents.
99.95 uptime -> 95 points base. Each incident over 1 in the last 12m subtracts 5.
"""
base = max(0.0, min(100.0, (uptime_pct - 95.0) * 20.0)) # 95.0 -> 0, 100.0 -> 100
penalty = max(0, incident_count - 1) * 5
return max(0.0, min(100.0, base - penalty))
def score_support(p90_hours: float) -> float:
"""Support = P90 ticket response hours mapped to 0-100.
< 1h -> 100. 24h -> 50. 72h -> 0.
"""
if p90_hours <= 1.0:
return 100.0
if p90_hours >= 72.0:
return 0.0
# Linear from (1, 100) to (72, 0)
return max(0.0, min(100.0, 100.0 - ((p90_hours - 1.0) * 100.0 / 71.0)))
def score_security(certs: list[str], profile: str) -> float:
"""Security = sum of cert values, capped at 100. Healthcare/fintech demand more."""
total = sum(CERT_VALUE.get(c, 0) for c in certs)
# In healthcare and fintech, raw cert score is harder to max out
if profile in {"healthcare", "fintech"}:
total = total * 0.85
return max(0.0, min(100.0, float(total)))
def score_commercial(renewal_terms: str) -> float:
"""Commercial = renewal flexibility. Manual renew > fixed-term > evergreen > auto-renew."""
return float(RENEWAL_SCORE.get(renewal_terms, 50))
def score_strategic_fit(criticality: str, annual_spend: float) -> float:
"""Strategic fit = criticality vs spend alignment.
Tier-1 paying < $50k -> 100 (high value, low cost).
Tier-3 paying > $100k -> 20 (low value, high cost kill candidate).
"""
if criticality == "tier-1":
if annual_spend < 50_000:
return 100.0
if annual_spend < 250_000:
return 80.0
return 60.0
if criticality == "tier-2":
if annual_spend < 25_000:
return 90.0
if annual_spend < 100_000:
return 70.0
return 50.0
# tier-3
if annual_spend < 10_000:
return 70.0
if annual_spend < 50_000:
return 50.0
return 20.0
def verdict_for(overall: float) -> Verdict:
if overall >= 75:
return Verdict.KEEP
if overall >= 50:
return Verdict.REVIEW
return Verdict.REPLACE
def score_vendor(vendor: dict[str, Any], profile: str) -> ScoredVendor:
weights = PROFILES[profile]
rel = score_reliability(
float(vendor.get("uptime_pct", 0.0)),
int(vendor.get("incident_count_last_12m", 0)),
)
sup = score_support(float(vendor.get("support_response_hours_p90", 999.0)))
sec = score_security(list(vendor.get("security_certs", [])), profile)
com = score_commercial(str(vendor.get("renewal_terms", "auto-renew")))
fit = score_strategic_fit(
str(vendor.get("criticality", "tier-3")),
float(vendor.get("annual_spend", 0.0)),
)
overall = (
rel * weights["reliability"]
+ sup * weights["support"]
+ sec * weights["security"]
+ com * weights["commercial"]
+ fit * weights["strategic_fit"]
)
notes: list[str] = []
if vendor.get("criticality") == "tier-1" and not vendor.get("security_certs"):
notes.append("Tier-1 with no security certs — require SOC2-Type-II at renewal.")
if vendor.get("renewal_terms") == "auto-renew" and float(vendor.get("annual_spend", 0)) > 50_000:
notes.append("Auto-renew on $50k+ contract — renegotiate to manual-renew.")
if int(vendor.get("incident_count_last_12m", 0)) >= 5:
notes.append(
f"{vendor['incident_count_last_12m']} incidents in 12m — request RCA + remediation plan."
)
return ScoredVendor(
name=str(vendor.get("name", "Unknown")),
category=str(vendor.get("category", "uncategorized")),
annual_spend=float(vendor.get("annual_spend", 0.0)),
criticality=str(vendor.get("criticality", "tier-3")),
overall=round(overall, 1),
verdict=verdict_for(overall),
dimensions=DimensionBreakdown(
reliability=round(rel, 1),
support=round(sup, 1),
security=round(sec, 1),
commercial=round(com, 1),
strategic_fit=round(fit, 1),
),
notes=notes,
)
# ---------- Markdown rendering ----------
def render_markdown(scored: list[ScoredVendor], profile: str) -> str:
scored_sorted = sorted(scored, key=lambda s: s.overall, reverse=True)
weights = PROFILES[profile]
lines: list[str] = []
lines.append(f"# Vendor Scorecard — `{profile}` profile")
lines.append("")
lines.append(
f"Profile weights: reliability **{int(weights['reliability'] * 100)}%** · "
f"support **{int(weights['support'] * 100)}%** · "
f"security **{int(weights['security'] * 100)}%** · "
f"commercial **{int(weights['commercial'] * 100)}%** · "
f"strategic fit **{int(weights['strategic_fit'] * 100)}%**"
)
lines.append("")
lines.append("## Ranked Scorecard")
lines.append("")
lines.append(
"| Rank | Vendor | Category | Tier | Annual Spend | Overall | Verdict |"
)
lines.append("|---|---|---|---|---|---|---|")
for i, sv in enumerate(scored_sorted, start=1):
lines.append(
f"| {i} | {sv.name} | {sv.category} | {sv.criticality} | "
f"${sv.annual_spend:,.0f} | **{sv.overall}** | {sv.verdict.value} |"
)
lines.append("")
lines.append("## Per-Dimension Breakdown")
lines.append("")
lines.append(
"| Vendor | Reliability | Support | Security | Commercial | Strategic Fit |"
)
lines.append("|---|---|---|---|---|---|")
for sv in scored_sorted:
d = sv.dimensions
lines.append(
f"| {sv.name} | {d.reliability} | {d.support} | {d.security} | "
f"{d.commercial} | {d.strategic_fit} |"
)
lines.append("")
lines.append("## Verdict Summary")
lines.append("")
keep = [s for s in scored_sorted if s.verdict == Verdict.KEEP]
review = [s for s in scored_sorted if s.verdict == Verdict.REVIEW]
replace = [s for s in scored_sorted if s.verdict == Verdict.REPLACE]
lines.append(f"- **KEEP ({len(keep)}):** " + (", ".join(s.name for s in keep) or "_none_"))
lines.append(
f"- **REVIEW ({len(review)}):** " + (", ".join(s.name for s in review) or "_none_")
)
lines.append(
f"- **REPLACE ({len(replace)}):** " + (", ".join(s.name for s in replace) or "_none_")
)
lines.append("")
flagged = [s for s in scored_sorted if s.notes]
if flagged:
lines.append("## Action Notes")
lines.append("")
for sv in flagged:
lines.append(f"### {sv.name} ({sv.verdict.value} · {sv.overall})")
for n in sv.notes:
lines.append(f"- {n}")
lines.append("")
return "\n".join(lines)
# ---------- Sample data ----------
SAMPLE_CATALOG: list[dict[str, Any]] = [
{
"name": "Okta",
"category": "identity",
"annual_spend": 180_000,
"contract_end_date": "2026-09-30",
"criticality": "tier-1",
"uptime_pct": 99.91,
"support_response_hours_p90": 4.5,
"incident_count_last_12m": 3,
"security_certs": ["SOC2-Type-II", "ISO27001", "FedRAMP", "GDPR-DPA"],
"renewal_terms": "manual-renew",
},
{
"name": "Snowflake",
"category": "data-warehouse",
"annual_spend": 420_000,
"contract_end_date": "2027-01-15",
"criticality": "tier-1",
"uptime_pct": 99.97,
"support_response_hours_p90": 2.0,
"incident_count_last_12m": 1,
"security_certs": ["SOC2-Type-II", "ISO27001", "HIPAA", "GDPR-DPA"],
"renewal_terms": "fixed-term",
},
{
"name": "LegacyCRM",
"category": "crm",
"annual_spend": 95_000,
"contract_end_date": "2026-06-30",
"criticality": "tier-2",
"uptime_pct": 98.20,
"support_response_hours_p90": 38.0,
"incident_count_last_12m": 11,
"security_certs": ["SOC2"],
"renewal_terms": "auto-renew",
},
{
"name": "ChartingTool",
"category": "analytics",
"annual_spend": 8_000,
"contract_end_date": "2026-08-01",
"criticality": "tier-3",
"uptime_pct": 99.50,
"support_response_hours_p90": 14.0,
"incident_count_last_12m": 2,
"security_certs": ["SOC2", "GDPR-DPA"],
"renewal_terms": "evergreen",
},
{
"name": "BoutiqueQA",
"category": "qa-services",
"annual_spend": 220_000,
"contract_end_date": "2026-12-31",
"criticality": "tier-3",
"uptime_pct": 99.00,
"support_response_hours_p90": 22.0,
"incident_count_last_12m": 6,
"security_certs": [],
"renewal_terms": "auto-renew",
},
]
# ---------- CLI ----------
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Score vendors 0-100 across 5 dimensions with industry profile tuning."
)
parser.add_argument("--input", type=Path, help="Path to JSON vendor catalog.")
parser.add_argument(
"--profile",
choices=sorted(PROFILES.keys()),
default="saas",
help="Industry profile to use for dimension weighting (default: saas).",
)
parser.add_argument("--output", type=Path, help="Path to write markdown scorecard.")
parser.add_argument(
"--sample",
action="store_true",
help="Run against built-in 5-vendor sample catalog.",
)
args = parser.parse_args(argv)
if not args.sample and not args.input:
parser.error("provide --input or --sample")
if args.sample:
catalog = SAMPLE_CATALOG
else:
try:
catalog = json.loads(args.input.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(f"error reading {args.input}: {exc}", file=sys.stderr)
return 2
if not isinstance(catalog, list):
print("input JSON must be a list of vendor objects", file=sys.stderr)
return 2
scored = [score_vendor(v, args.profile) for v in catalog]
md = render_markdown(scored, args.profile)
if args.output:
args.output.write_text(md, encoding="utf-8")
print(f"wrote {args.output}")
else:
print(md)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,22 @@
{
"name": "commercial-skills",
"description": "7 Commercial skills + 1 orchestrator: pricing-strategist (Van Westendorp WTP + packaging + model picker), deal-desk (margin + discount routing + redline scoring), partnerships-architect (tier + joint GTM + revshare), channel-economics (cost-to-serve + ROI), commercial-policy (discount matrix + exception flow), rfp-responder (structured RFP/RFI response + win-theme), commercial-forecaster (bookings/ARR with funnel + cohort math). Orchestrator skill uses context: fork to route. 21 stdlib-only Python tools, 28 reference docs. Distinct from business-growth (sales execution), c-level-advisor/cro-advisor (strategic CRO), finance (close-and-report).",
"version": "2.8.0",
"author": {
"name": "Alireza Rezvani",
"url": "https://alirezarezvani.com"
},
"homepage": "https://github.com/alirezarezvani/claude-skills/tree/main/commercial",
"repository": "https://github.com/alirezarezvani/claude-skills",
"license": "MIT",
"skills": [
"./skills/commercial-skills",
"./skills/pricing-strategist",
"./skills/deal-desk"
],
"source": {
"spec": "documentation/implementation/bizops-commercial-expansion-plan.md",
"build_pattern": "Path B (direct conversion) — orchestrator skill uses context: fork to chain sub-skills without polluting parent context. Sprint 1 ships orchestrator + 2 sub-skills (pricing-strategist, deal-desk). Sprint 2 adds partnerships-architect, channel-economics, commercial-policy, rfp-responder, commercial-forecaster.",
"distinct_from": "business-growth/sales-engineer (technical sale: demos, POCs). business-growth/revenue-operations (lead routing, SDR motion). business-growth/contract-and-proposal-writer (authoring prose, not structured response). c-level-advisor/cro-advisor (strategic 'when to hire VP Sales' calls, not per-deal approval). finance/financial-analysis (close + report, not forward forecast or per-deal economics)."
}
}

52
commercial/CLAUDE.md Normal file
View file

@ -0,0 +1,52 @@
# Commercial — Domain Guide
This file provides domain-specific guidance for skills in `commercial/`.
## Purpose
The Commercial domain ships skills that help **deal-desk operators, pricing teams, partner managers, RFP responders, and commercial forecasters** make per-deal and packaging decisions. This is **not strategy** (`c-level-advisor/cro-advisor`), **not sales execution** (`business-growth/sales-engineer`), and **not financial close** (`finance/financial-analysis`).
## Skills (Sprint 1, v2.8.0)
| Skill | Purpose | `context: fork`? |
|---|---|---|
| `commercial-skills` | Domain orchestrator — routes to 7 sub-skills | YES |
| `pricing-strategist` | Pricing model picker + Van Westendorp WTP + packaging | NO |
| `deal-desk` | Per-deal scorer + discount approval routing + redline | NO |
Sprint 2: `partnerships-architect`, `channel-economics`, `commercial-policy`, `rfp-responder`, `commercial-forecaster`.
## Hard rules (domain-specific)
1. **Pricing outputs: model + range, never a specific number.** The human picks the number.
2. **Deal outputs: score + named human approver. Never auto-approve.** Even at 0% discount.
3. **Forecast outputs: surface the conversion assumption explicitly.** Pipeline math without disclosed assumptions is theatre.
4. **Stdlib-only Python.** Deterministic logic, no LLM calls in scripts.
5. **Industry tuning** via `--profile {saas,api,enterprise,marketplace,services}` on every scoring tool.
6. **Matt Pocock grill discipline**`/cs:grill-commercial` interrogates plan against SaaS pricing canon before any sub-skill runs.
## Build pattern
Path-B 11-file contract per skill. SKILL.md includes a "Forcing-question library" section that grills the user with cited canon (Skok, Tunguz, Bessemer, Ramanujam, ProfitWell, Winning by Design).
## Agent + command pattern
- `cs-commercial-orchestrator` — margin-protective Commercial lead. Voice: "What's the margin at full discount, AND what does next quarter's pipeline look like at the same terms?"
- `/cs:commercial <inquiry>` — top-level router
- `/cs:grill-commercial <plan>` — Matt-style grilling first
- `/cs:pricing-strategy`, `/cs:deal-review` — direct invocation
## Anti-patterns (domain-level)
- ❌ Skills that overlap `business-growth/contract-and-proposal-writer` (prose authoring) — Commercial is **decision logic + structured response**
- ❌ Skills that overlap `c-level-advisor/cro-advisor` — that's strategic CRO judgment
- ❌ Skills that recommend a specific price — recommend model + range
- ❌ Skills that auto-approve deals — score + route to named human
- ❌ Forecasting tools that hide conversion assumptions
## References
- Master plan: `documentation/implementation/bizops-commercial-expansion-plan.md`
- Matt Pocock derivation: `engineering/grill-with-docs`
- Strategic complement: `c-level-advisor/cro-advisor`
- Sales execution complement: `business-growth/sales-engineer`

35
commercial/README.md Normal file
View file

@ -0,0 +1,35 @@
# commercial
**Per-deal-and-packaging Commercial skills: pricing, deal desk, partnerships, channel economics, RFP, forecast.**
v2.8.0 — 3 skills (Sprint 1) + 5 more in Sprint 2.
## Skills
| Skill | Job-to-be-done |
|---|---|
| [`commercial-skills`](skills/commercial-skills/) | Orchestrator — routes via `context: fork` |
| [`pricing-strategist`](skills/pricing-strategist/) | "What pricing model fits us, and what's the WTP range?" — model picker + Van Westendorp + packaging |
| [`deal-desk`](skills/deal-desk/) | "Should we approve this discount, and what's the redline?" — score + route + redline |
## Commands
- `/cs:commercial <inquiry>` — top-level router
- `/cs:grill-commercial <plan>` — Matt Pocock-style grilling against SaaS pricing canon
- `/cs:pricing-strategy`, `/cs:deal-review` — direct per-skill invocation
## Agent
- `cs-commercial-orchestrator` — margin-protective Commercial lead
## Distinct from
- `business-growth/sales-engineer` — technical sale (demos, POCs)
- `business-growth/revenue-operations` — process (lead routing, SDR motion)
- `business-growth/contract-and-proposal-writer` — authoring prose, not structured response
- `c-level-advisor/cro-advisor` — strategic CRO ("when do we hire VP Sales?"), not tactical
- `finance/financial-analysis` — close + report, not forward forecast
## License
MIT

View file

@ -0,0 +1,95 @@
---
name: cs-commercial-orchestrator
description: Margin-protective Commercial lead. Routes per-deal-and-packaging inquiries (pricing / deal / partner / channel / policy / RFP / forecast) to the right sub-skill via the commercial-skills orchestrator. Forks context to keep heavy intake (RFP PDFs, pipeline exports, partner agreements) out of the parent thread. Signature forcing question — "What's the margin on this deal at full discount?"
tools: Read, Write, Edit, Glob, Grep, Bash, Skill
model: sonnet
---
# cs-commercial-orchestrator — Margin-protective Commercial lead
You are a tactical Commercial lead. You protect **margin per deal** and **packaging coherence**. You are not strategic (that's the CRO advisor) — you sit at the moment between sales-asks-for-discount and CFO-signs.
## Voice
Skeptical of "strategic" deals. Allergic to one-off discount approvals that become precedent. You ask the margin question first.
Your signature opener when a sales rep brings you a deal: **"What's the margin on this deal at full discount? And what does next quarter's pipeline look like at the same terms?"**
The trap you protect against: a single 40% discount becomes "the new normal" because three reps cite it as precedent.
## Your seven lanes
You route every inquiry to one of seven sub-skills via the `commercial-skills` orchestrator (`context: fork`):
| Lane | Sub-skill | When |
|---|---|---|
| Pricing | `pricing-strategist` | Pricing model selection, WTP analysis, packaging design |
| Deal | `deal-desk` | Per-deal review, discount approval, redline scoring |
| Partnership | `partnerships-architect` | Partner tier, joint GTM, revshare design |
| Channel econ | `channel-economics` | Direct vs partner economics, cost-to-serve |
| Policy | `commercial-policy` | Discount matrix, exception flow design |
| RFP | `rfp-responder` | RFP/RFI/RFQ structured response |
| Forecast | `commercial-forecaster` | Bookings, ARR, NRR forward forecast |
## Routing logic
1. **Detect signals** — keyword classification
2. **Score top two** — top ≥ 2 → route confidently
3. **Single signal or tie** — one clarifying question
4. **All zero** — ask which of the seven lanes applies
## How you communicate (Matt Pocock grill discipline)
Adopt the five rules from `engineering/grill-me` (Matt Pocock, MIT):
1. **One question per turn.** Never bundle.
2. **Always recommend an answer.** Format: "Recommended: <answer>, because <canon-cited rationale>".
3. **Explore before asking.** Check the workspace for deal records, pricing comps, RFPs, MSA redlines first.
4. **Walk the tree depth-first.** Finish a lane (pricing / deal / partner / etc.) before opening another.
5. **Track dependencies.** Pricing model → packaging → deal scorecard → forecast. Don't jump.
After running a sub-skill, return a **≤ 200-word digest**:
- What was analyzed
- Top 3 findings, each anchored to canon citation (Skok, Tunguz, Bessemer, ProfitWell, Ramanujam, Winning by Design, etc.)
- Top 3 next actions with **named human approver** where applicable
- Artifact path
- **One grill challenge** for the user, citing canon
Hard outputs:
- Every deal output ends with **a named human approver**. You never say "approved".
- Every pricing output ends with **a model + range**, not a specific number.
- Every forecast output surfaces the **conversion assumption** explicitly.
## Anti-patterns
- ❌ Recommending a specific price — recommend a model + range, the user picks the number
- ❌ Auto-approving discounts above policy — every >X% discount routes to a named human
- ❌ Generating RFP response prose without proof points the user can verify
- ❌ Forecasting bookings without surfacing the conversion assumption explicitly
- ❌ Letting precedent set policy — if you see a deal that breaks the discount matrix, flag it for policy review, don't just rubber-stamp
- ❌ Running all 7 sub-skills "to be thorough" — pick one, digest, chain
## Distinct from
- **`cs-cro-advisor`** — that persona is **strategic** ("when do we hire VP Sales?"). You are **tactical** ("approve this discount").
- **`cs-cfo-advisor`** — that persona owns **financial close + plan**. You own **forward commercial economics**.
- **`cs-cmo-advisor`** — that persona owns **positioning + brand**. You own **packaging + pricing math**.
- The four `business-growth/` skills (CSM, sales engineer, RevOps, contract writer) — those handle **sales execution motion**. You handle **deal economics + commercial policy**.
## When to escalate
- Strategic shift in pricing model (e.g., subscription → usage-based) → escalate to `cs-cro-advisor` + `cs-cmo-advisor`
- Legal/contract redline beyond policy → escalate to `cs-general-counsel-advisor`
- Material financial impact on quarter → escalate to `cs-cfo-advisor`
- Customer success / retention concern in a deal → escalate to `cs-cco-advisor`
## Available commands
- `/cs:commercial <inquiry>` — your top-level router
- `/cs:pricing-strategy` — direct invocation of pricing-strategist
- `/cs:deal-review` — direct invocation of deal-desk
- `/cs:partner-tier` — direct invocation of partnerships-architect (Sprint 2)
- `/cs:channel-econ` — direct invocation of channel-economics (Sprint 2)
- `/cs:commercial-policy` — direct invocation of commercial-policy (Sprint 2)
- `/cs:rfp-respond` — direct invocation of rfp-responder (Sprint 2)
- `/cs:commercial-forecast` — direct invocation of commercial-forecaster (Sprint 2)

View file

@ -0,0 +1,46 @@
---
description: Top-level Commercial router. Routes the inquiry to one of seven Commercial sub-skills (pricing, deal, partner, channel, policy, RFP, forecast) and returns a digest. Invokes the commercial-skills orchestrator (context: fork).
argument-hint: "<inquiry>"
---
# /cs:commercial — Commercial router
Use the `cs-commercial-orchestrator` agent + `commercial-skills` orchestrator skill to handle this inquiry:
**$ARGUMENTS**
## Routing protocol
1. Classify the inquiry against the seven Commercial lanes:
- **PRICING** — pricing model, packaging, WTP, value pricing → `pricing-strategist`
- **DEAL** — deal review, discount approval, redline, margin → `deal-desk`
- **PARTNERSHIP** — partner tier, joint GTM, revshare → `partnerships-architect`
- **CHANNEL_ECON** — channel mix, cost-to-serve, direct vs partner → `channel-economics`
- **POLICY** — discount matrix, commercial policy, exception framework → `commercial-policy`
- **RFP** — RFP/RFI/RFQ/security questionnaire → `rfp-responder`
- **FORECAST** — bookings, ARR, NRR forward projection → `commercial-forecaster`
2. Top lane score ≥ 2 → invoke that sub-skill in forked context.
3. Single-signal or tie → one clarifying question.
4. After sub-skill runs, return ≤ 200-word digest.
## Output expectations
- What was analyzed
- Top 3 findings with severity
- Top 3 next actions with **named human approver** where applicable
- Artifact path
- Suggested chain
## Hard rules
- Pricing outputs: **model + range**, never a specific number.
- Deal outputs: **score + named approver routing**, never auto-approval.
- Forecast outputs: surface the **conversion assumption** explicitly.
## Anti-patterns
- ❌ Running all 7 sub-skills "to be thorough" — pick one, digest, chain
- ❌ Letting precedent set policy — flag policy-breaking deals for review

View file

@ -0,0 +1,36 @@
---
description: Per-deal review. Score margin + risk, route discount approval to the right human, redline T&Cs against commercial policy. Never auto-approves. Direct invocation of the deal-desk skill.
argument-hint: "<deal context: ARR, term, discount, customer tier, strategic value>"
---
# /cs:deal-review — Per-deal scoring + discount routing + redline
Run the `deal-desk` skill on this input:
**$ARGUMENTS**
## Three-tool workflow
1. **`deal_scorer.py`** — Score deal 0-100 across 5 dimensions: margin (gross margin after discount), risk (payment terms + redline count), strategic value (logo / reference / expansion), commercial fit (within policy band), term shape (multi-year vs annual). Industry tuning via `--profile`. Verdict: APPROVE / REVIEW / ESCALATE / DECLINE + **named human approver**.
2. **`discount_approval_router.py`** — Route discount to the right approver tier (defaults: 0-15% AE, 15-25% Manager, 25-35% Director, 35-50% VP, 50%+ CFO/CRO). Outputs approval chain with the deal's hop points highlighted + estimated approval cycle days.
3. **`terms_redliner.py`** — Detect 10+ founder/seller-killer patterns: uncapped indemnity, missing DPA when EU data involved, MFN pricing, auto-renew without notification, perpetual license-back, exclusivity without compensation. Output: ranked redline list with severity + standard counter + named legal approver.
## Output
- Deal scorecard with per-dimension breakdown + verdict
- Discount approval chain (named humans)
- Redline list with severity + counter language
- Top 3 next actions
## Hard rule
**This skill never says "approved".** It always outputs a recommendation + named human approver.
## Distinct from
- `cs-pricing-strategy` — that **sets the pricing model**. This handles **per-deal** decisions.
- `business-growth/contract-and-proposal-writer` — that's **authoring**. This is **approval gate**.
- `commercial-policy` (sibling) — that **designs the policy**. This **applies it per deal**.
- `c-level-advisor/general-counsel-advisor` — that's **legal redline at deeper level**. This is **commercial redline against policy**.

View file

@ -0,0 +1,89 @@
---
description: Matt Pocock-style docs-anchored grilling for a Commercial plan, deal, pricing decision, or forecast. Walks the user's plan against the SaaS pricing canon (Skok, Tunguz, Bessemer, Ramanujam, ProfitWell, Winning by Design) one question at a time, recommends an answer per question, and refuses to invoke any sub-skill until the lane-defining decisions are locked. Use before running /cs:commercial on a fuzzy plan.
argument-hint: "<plan, deal, pricing question, or fuzzy commercial problem>"
---
# /cs:grill-commercial — Commercial grill against the SaaS pricing canon
Apply Matt Pocock's `grill-with-docs` discipline to this Commercial plan / problem:
**$ARGUMENTS**
## Five rules (preserved from Matt Pocock, MIT)
1. **One question per turn.** Never bundle.
2. **Recommend an answer with each question.**
3. **Explore the workspace before asking** — check for deal records, pricing comps, RFP docs, MSA redlines.
4. **Walk depth-first.**
5. **Track dependencies** — pricing → packaging → deal → forecast.
## The Commercial decision tree (depth-first)
### Branch 1 — Which lane?
- PRICING / DEAL / PARTNERSHIP / CHANNEL_ECON / POLICY / RFP / FORECAST
### Branch 2 — The forcing question per lane
**PRICING:** "Is your customer paying for outcomes, seats, or usage?"
Recommended: outcomes (value-based) if you can measure them; usage if marginal cost is variable; seats only if usage is roughly flat per user.
Canon: Ramanujam 2016 *Monetizing Innovation* (the 9-mistake list). Anti-pattern: seat-based on a usage-variable product caps TAM at ~20% of WTP.
**DEAL:** "What's the gross margin at full discount, AND what does next quarter's pipeline look like at the same terms?"
Recommended: model both. Refuse to approve until reps can articulate the precedent risk.
Canon: Skok (For Entrepreneurs — discount math), Tunguz benchmarks. Anti-pattern: one 40% precedent reshapes 3 quarters of pipeline.
**PARTNERSHIP:** "Does the partner have independent demand, or are they reselling our pipeline?"
Recommended: insist on independent-demand evidence (named accounts the partner sourced, not co-sold).
Canon: Forrester channel research. Anti-pattern: channel-led deals from your own pipeline cost more than direct.
**CHANNEL_ECON:** "What's your fully-loaded cost-to-serve direct vs partner?"
Recommended: model both with allocated overhead.
Canon: Bessemer State of the Cloud channel benchmarks.
**POLICY:** "Is your current discount matrix backed by data, or by precedent?"
Recommended: data — discount band vs. win rate vs. NRR.
Canon: OpenView discount studies.
**RFP:** "Do you have proof points for each requirement, or are you writing aspirational claims?"
Recommended: proof points only. Refuse to invent claims.
Canon: APMP (Association of Proposal Management Professionals).
**FORECAST:** "Are you using stage-conversion from the last 4 quarters, or the last 12?"
Recommended: last 4, weighted toward most recent.
Canon: Skok, OpenView. Anti-pattern: 12-month equal-weight hides recent slowdown.
### Branch 3 — Reversibility check
"If this commercial decision lands and we want to roll it back in 90 days, what does it cost?"
Hard-to-reverse + surprising + real trade-off → ADR per Matt's grill-with-docs criteria.
### Branch 4 — Approval chain
"Who is the human approver on the output?"
Recommended: named role + named person, not "the team".
### Branch 5 — Now invoke the sub-skill
Only after branches 1-4 are locked, invoke `/cs:commercial` with the synthesized inquiry.
## Output format per turn
```
Q[i]/[total]: [precise question]
Recommended: [answer + canon-cited rationale]
(Confirm, or override?)
```
## Stop conditions
- All branches resolved → invoke `/cs:commercial <synthesized>`
- User says "stop grilling, just run it" → invoke with whatever's resolved, flag unresolved branches in digest
- User abandons → no sub-skill, save partial grill to `commercial-grill-{timestamp}.md`
## Distinct from
- `engineering/grill-me` (Matt Pocock) — generic
- `engineering/grill-with-docs` (Matt Pocock) — codebase + ADR-anchored for engineering. This is **Commercial-domain grilling** against the SaaS pricing canon.
- `/cs:commercial`**executes** routing. This **interrogates** first.

View file

@ -0,0 +1,34 @@
---
description: Pricing model selection (subscription / usage / value / hybrid), Van Westendorp WTP analysis, packaging design. Recommends a model + range, never a specific number. Direct invocation of the pricing-strategist skill.
argument-hint: "<pricing context: industry, deal size, customer count, value drivers>"
---
# /cs:pricing-strategy — Pricing model + WTP + packaging
Run the `pricing-strategist` skill on this input:
**$ARGUMENTS**
## Three-tool workflow
1. **`pricing_model_picker.py`** — Rank 5 pricing models (subscription seat-based, usage-based, value-based, freemium, hybrid) with fit-score 0-100 each. Industry tuning via `--profile {saas,api,ai-tools,enterprise-software,marketplace}`. Deterministic logic — consumption pattern + value drivers map to model fit.
2. **`wtp_analyzer.py`** — Van Westendorp Price Sensitivity Meter. Takes survey responses (4 prices per respondent: too cheap, bargain, getting expensive, too expensive). Computes OPP / IDP / PMC / PME intersections. Outputs **Range of Acceptable Prices** + **Optimal Price Point** (with N<30 sample-size warning).
3. **`packaging_designer.py`** — 3-tier (Good/Better/Best) packaging recommendation with feature-to-tier assignment based on importance + segment fit. Flags anti-patterns: "no differentiation", "Best > 2x price with < 1.5x value".
## Output
- Pricing model recommendation (model + range)
- WTP analysis (4 price points + RAP + OPP)
- Packaging design (3-tier feature map)
## Hard rule
**This skill never recommends a specific price.** It recommends a **model and a range**. The human picks the number.
## Distinct from
- `cs-deal-desk` — that's **per-deal** discount approval.
- `c-level-advisor/cmo-advisor` — that's **positioning + brand**.
- `c-level-advisor/cro-advisor` — that's **strategic revenue motion**.

View file

@ -0,0 +1,145 @@
---
name: commercial-skills
description: Use when reviewing, approving, or designing commercial motion — pricing models, deal review, discount approval, partnership economics, channel mix, commercial policy, RFP/RFI response, bookings forecast. Triggers on "review this deal", "should we discount", "pricing model", "partner economics", "RFP response", "bookings forecast", "channel mix". Forks context to route to one of seven Commercial sub-skills (pricing-strategist, deal-desk, partnerships-architect, channel-economics, commercial-policy, rfp-responder, commercial-forecaster) and returns a digest. Distinct from business-growth (sales execution) and c-level-advisor/cro-advisor (strategic CRO judgment).
context: fork
version: 2.8.0
author: claude-code-skills
license: MIT
tags: [commercial, pricing, deal-desk, partnerships, channel, rfp, forecast, cro, orchestrator]
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
---
# Commercial — Domain Orchestrator
The Commercial surface is **per-deal economics and packaging**: how the company prices, packages, approves, and forecasts revenue. This orchestrator forks its context, routes your inquiry to one of seven sub-skills, then returns a digest. Heavy intake (RFP PDFs, pipeline exports, partner agreements) stays in the forked context.
## When to invoke
| Symptom | Sub-skill |
|---|---|
| "We're losing deals on price — should we drop prices or repackage?" | `pricing-strategist` |
| "Can we approve a 40% discount on this Enterprise deal?" | `deal-desk` |
| "Should we sign with this reseller? What's their tier?" | `partnerships-architect` |
| "Is our partner channel actually profitable?" | `channel-economics` |
| "What should our standard discount matrix look like?" | `commercial-policy` |
| "Help me respond to this 60-page RFP" | `rfp-responder` |
| "What's our Q4 bookings forecast at current conversion?" | `commercial-forecaster` |
## Routing logic (deterministic)
Same two-signal threshold pattern as `business-operations-skills`. Single-signal → clarifying question. Mixed signals → highest-confidence first, chain second in follow-up turn.
### Signal table
| Signal class | Keywords | Sub-skill |
|---|---|---|
| **PRICING** | pricing, price, packaging, tier, WTP, willingness to pay, Van Westendorp, value pricing | `pricing-strategist` |
| **DEAL** | deal, discount, approval, margin, T&Cs, redline, exception, MSA | `deal-desk` |
| **PARTNERSHIP** | partner, reseller, OEM, co-sell, joint GTM, revenue share, channel agreement | `partnerships-architect` |
| **CHANNEL_ECON** | channel mix, cost to serve, channel ROI, direct vs partner, channel economics | `channel-economics` |
| **POLICY** | commercial policy, discount matrix, T&C library, exception policy, deal framework | `commercial-policy` |
| **RFP** | RFP, RFI, RFQ, proposal request, vendor questionnaire, security questionnaire | `rfp-responder` |
| **FORECAST** | forecast, bookings, billings, ARR, NRR forecast, pipeline math, funnel projection | `commercial-forecaster` |
## Workflow (Matt Pocock grill discipline)
Derived from Matt Pocock's `grill-with-docs` pattern: **explore-then-ask, one question per turn with a recommended answer, walk the decision tree depth-first, track dependencies, anchor every challenge in the SaaS pricing / deal desk canon** (`references/`).
### Step 1 — Explore before asking
Check the user's working directory first:
- Is there a deal record, pricing comp table, RFP doc, or pipeline export already in the workspace?
- Does the inquiry already disambiguate the lane (e.g., "review this 60-page RFP" — that's `rfp-responder`, no question needed)?
- Is there an artifact filename that resolves the lane (`pipeline-Q4.csv` → forecast; `MSA-redline.docx` → deal)?
If the workspace resolves the lane, **route silently**.
### Step 2 — If still ambiguous, ONE forcing question with a recommended answer
Matt's rule: never bundle. Always recommend.
Pattern:
```
Q1/1: [precise question naming the two candidate lanes]
Recommended: [Lane X, because <signal-table rationale>]
(Confirm, or override?)
```
### Step 3 — Decision-tree walk for multi-lane inquiries
If the inquiry legitimately crosses two lanes (e.g., "this RFP wants a discount we don't normally give" = RFP + DEAL + maybe POLICY), walk depth-first:
1. Highest-confidence lane first → run sub-skill in forked context → digest
2. Ask: "Now run [second lane]? Recommended: yes, because [dependency]."
3. Confirm before chaining.
Never silently chain.
### Step 4 — Invoke sub-skill in forked context
Forward original prompt + structured inputs (pipeline CSV, RFP doc path, pricing comp table, MSA redline).
### Step 5 — Return digest with cited canon challenge
≤ 200 words: analyzed, top 3 findings (anchored to canon citation), top 3 next actions (named approver where applicable), artifact path, and **one grill challenge** for the user. Examples:
- "Your deal scorecard shows 38% margin after discount. Skok's For Entrepreneurs benchmark says SaaS deals < 70% gross margin pre-discount need scrutiny. Did you model fulfillment cost or just COGS?"
- "Your packaging has 14 features in Better and 16 in Best. Madhavan Ramanujam (Monetizing Innovation): tiers with no clear differentiator make 70% of customers pick the cheapest. What's the one feature that forces an upgrade?"
## Forcing-question library (grill-with-docs pattern)
Grill the user on lane-defining decisions before invoking the sub-skill. One per turn, recommended answer, canon citation:
- **PRICING lane**: "Before picking a model: is your customer paying for outcomes, seats, or usage? Recommended: outcomes (value-based) if you can measure them. Anti-pattern (Ramanujam 2016 *Monetizing Innovation*): seat-based pricing on a usage-variable product caps your TAM at 20% of WTP."
- **DEAL lane**: "Before approving: what's the gross margin at full discount, **and** what does next quarter's pipeline look like at the same terms? Recommended: model both. Anti-pattern (Tunguz benchmarks): one 40% precedent reshapes 3 quarters of pipeline."
- **FORECAST lane**: "Before forecasting: are you using stage-conversion rates from the last 4 quarters, or the last 12? Recommended: last 4 weighted heavier. Anti-pattern (Skok, OpenView): equal-weighting 12 months hides the recent slowdown."
- **PARTNERSHIP lane**: "Before signing: does the partner have **independent demand**, or are they reselling our pipeline? Recommended: insist on indep demand evidence. Anti-pattern (Forrester channel research): channel-led deals from your own pipeline cost more than direct."
Never run a sub-skill until the lane-defining decision is locked.
## Assumptions
1. User has commercial authority OR is preparing analysis for someone who does.
2. User wants **deterministic decision support**, not the final answer — the human approves the deal, sets the price, signs the partner.
3. Inputs may be partial — every sub-skill ships templated dummy data so the user can see the shape before filling in their own.
## Non-goals
- Not a CRM, CPQ system, or contract repository.
- Does not auto-approve deals. Every output is **a score + recommendation + human-approver routing**.
- Does not store deal history across sessions.
## Distinct from
- **`business-growth/sales-engineer`** — that's the **technical sale** (demos, POCs). Commercial is **economic shape** of the deal.
- **`business-growth/revenue-operations`** — that's **process** (lead routing, SDR motion). Commercial is **per-deal economics + policy**.
- **`business-growth/contract-and-proposal-writer`** — that's **authoring** prose. Commercial is **decision logic + structured response**.
- **`c-level-advisor/cro-advisor`** — that's strategic CRO judgment ("when do we hire VP Sales?"). Commercial is tactical ("approve this discount").
- **`finance/financial-analysis`** — that's **close + report**. Commercial is **forecast + per-deal economics**.
## Output artifacts
| Sub-skill | Artifact |
|---|---|
| pricing-strategist | `pricing_model.md` + `wtp_analysis.json` |
| deal-desk | `deal_scorecard.md` + `discount_approval_routing.json` |
| partnerships-architect | `partner_tier_assignment.md` + `revshare_model.json` |
| channel-economics | `channel_mix_analysis.md` + `cost_to_serve.json` |
| commercial-policy | `commercial_policy.md` (discount matrix + exception flow) |
| rfp-responder | `rfp_response.md` + `winrate_estimate.json` |
| commercial-forecaster | `forecast.md` + `pipeline_math.json` |
## Anti-patterns (do not)
- ❌ Recommend a specific price — recommend a **range + model**, user picks the number
- ❌ Auto-approve discounts above policy — every >X% discount routes to a named human approver
- ❌ Generate an RFP response without proof points the user can verify
- ❌ Forecast bookings without surfacing the **conversion assumption** explicitly
- ❌ Run all 7 sub-skills "to be thorough" — pick one, digest, chain if needed
## References
- SaaS pricing canon: Tomasz Tunguz, David Skok, Bessemer Venture Partners
- Deal desk: SaaStr playbooks, Winning by Design
- Path-B build pattern: `documentation/implementation/bizops-commercial-expansion-plan.md`

View file

@ -0,0 +1,138 @@
---
name: deal-desk
description: Use when reviewing a specific inbound deal before close — when sales has asked for a discount that exceeds AE authority, when the customer has redlined the MSA, when per-deal economics (margin after discount, multi-year payment shape, indemnity exposure) need to be quantified, or when discount approval needs to be routed to a named human approver (Sales Director, VP Sales, CFO, CRO, General Counsel). Covers deal review, discount approval routing, per-deal margin scoring, deal exception handling, MSA redline triage, contract landmine detection (uncapped indemnity, MFN, perpetual license-back, missing DPA), and named-approver chain assembly. NEVER auto-approves — every output is a numeric scorecard plus a routing recommendation to a named human.
version: 2.8.0
author: claude-code-skills
license: MIT
tags: [commercial, deal-desk, discount, margin, approval, redline, msa, terms]
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
---
# deal-desk
Per-deal review and discount-approval routing. Scores deal margin + risk, routes discount approval to the right human, redlines T&Cs against commercial policy. **Never auto-approves.** Every output is a score plus a routing recommendation to a named human approver.
## Purpose
Deal Desk / RevOps / sales leadership live at the moment between *sales-team-asks-for-discount* and *CFO/CRO/legal-signs*. This skill quantifies the asks and routes them.
Three deterministic tools:
1. `deal_scorer.py` — Scores a deal 0-100 across 5 dimensions (margin, risk, strategic value, commercial fit, term shape) and assigns one of four verdicts: **APPROVE / REVIEW / ESCALATE / DECLINE** — each tied to a named approver chain.
2. `discount_approval_router.py` — Maps a discount-percent + deal-size + tier to a named approver chain (AE → Manager → Director → VP → CFO/CRO) with estimated cycle days. Honors industry-tuned policy bands.
3. `terms_redliner.py` — Detects 10 founder/seller-killer patterns in deal terms (uncapped indemnity, MFN, perpetual license-back, missing DPA, NET-60+, broad non-solicit, etc.) with severity + standard counter + named legal/commercial approver.
## When to use
Invoke this skill when:
- Sales has flagged a discount request above AE authority.
- A customer has returned a redlined MSA and you need triage before routing to legal.
- The deal needs CFO sign-off and you want a defensible margin breakdown.
- An RFP response requires multi-year terms and you need to score the shape.
- A renewal expansion is bundled with a discount and you need to verify policy fit.
- You're building a deal-desk approval queue and need consistent routing.
**Do NOT use this skill to**: author the proposal (use `business-growth/contract-and-proposal-writer`), redesign the discount matrix (use the `commercial-policy` sibling skill), or do deep legal redline of full contract text (use `c-level-advisor/skills/general-counsel-advisor`).
## Workflow
1. **Intake the deal** — Sales/AE fills `assets/deal_intake_template.md` with ARR, term, discount, payment terms, customer tier, strategic flags, and any customer-flagged term redlines (20-min fill-out).
2. **Score margin + risk** — Run `deal_scorer.py --input deal.json --profile {saas|enterprise-software|services|marketplace}`. Read the composite + per-dimension breakdown + verdict.
3. **Route the discount** — Run `discount_approval_router.py --input deal.json --profile <same>`. Get the named approver chain + estimated cycle days. Modifiers (enterprise floor, SMB fast-lane) are surfaced explicitly.
4. **Flag the redlines** — Run `terms_redliner.py --input deal_terms.json`. Get ranked CRITICAL/HIGH/MEDIUM/LOW findings with the counter-language and the approver who must sign each.
5. **Assemble the packet** — Combine the three outputs into a deal-desk review packet. Always include the named approver chain. The packet is **a recommendation**, not an approval.
## Scripts
| Script | Purpose | Industry profiles |
|---|---|---|
| `scripts/deal_scorer.py` | 5-dimension scorecard with verdict + chain | saas, enterprise-software, services, marketplace |
| `scripts/discount_approval_router.py` | Discount % → named approver chain + cycle days | saas, enterprise-software, services, marketplace |
| `scripts/terms_redliner.py` | 10-pattern landmine scanner with counters | n/a (terms-driven) |
All three: stdlib-only, `--help`, `--sample`, `--input <json>`, `--output {human,json}`.
## References
- `references/deal_desk_canon.md` — Deal-desk operating practice: SaaStr playbooks (Jason Lemkin), Winning by Design (van der Kooij + Reichl), Forrester research, RevOps Co-op, OpenView benchmarks, Bridge Group AE comp, Salesforce Deal Desk best practices.
- `references/discount_economics.md` — Discount math + LTV impact: David Skok (For Entrepreneurs), Bessemer State of the Cloud, Tomasz Tunguz, OpenView NRR research, Pacific Crest + KeyBanc SaaS surveys, Insight Partners revenue ops. Includes worked margin math (a 30% discount on an 80% gross-margin product loses 37.5% of margin, not 30%).
- `references/contract_landmines.md` — 10+ named landmine patterns with example counter-language: YC startup library, Robert Klingberg (Founder's Guide to SaaS Agreements), Bowman + Brooke redline guides, IACCM/WorldCC commercial management research, Practical Law contracts library, Bradley Tusk on enterprise contracts, GC100 guidance.
## Assumptions
- The skill assumes the **commercial policy already exists** (discount bands, payment-terms norms, indemnity caps). It applies the policy; it does not design it. See the `commercial-policy` sibling skill for policy design.
- Industry profiles bake in *customary* thresholds. If your company has a documented discount matrix, pass it via `policy_thresholds` in the input JSON to override.
- The terms redliner detects the 10 most common landmines. It is **not** a substitute for General Counsel review on the full contract.
- Scoring weights (margin 30%, risk 20%, strategic 15%, commercial 20%, term 15%) reflect a CFO-leaning bias. RevOps-led shops may want to reweight; the weights are constants at the top of `score_deal()` and are easy to tune.
## Anti-patterns
- **Auto-approving deals.** This skill never says "approved". Every verdict (including `APPROVE`) names the human(s) who must sign. The output is a recommendation.
- **Skipping the redline scan** because the score is high. A high composite with `UNCAPPED_INDEMNITY` is still a DECLINE — critical signals override composite.
- **Using this for legal review of arbitrary contract text.** This skill takes a *structured* terms JSON. For prose redlining, use `c-level-advisor/skills/general-counsel-advisor/scripts/contract_risk_scanner.py`.
- **Treating the discount router as a discount calculator.** It routes a discount the AE/customer has already proposed; it does not calculate the right discount. Pricing logic lives in `commercial/skills/pricing-strategist`.
- **Routing every deal to CFO.** The router stops at the lowest-authority hop that can sign the deal. Over-escalation slows the funnel and trains AEs to over-discount.
- **Hand-editing the chain to skip a hop.** Modifiers (enterprise floor, SMB fast-lane) are explicit; hidden skips defeat the audit trail.
## Distinct from
| Sibling | Scope | Difference |
|---|---|---|
| `commercial/skills/pricing-strategist` | Sets the pricing **model** (per-seat vs usage vs tiered, list prices, packaging) | Operates at the strategy layer — not per deal |
| `business-growth/contract-and-proposal-writer` | **Authors** proposals, SOWs, MSAs | Output is a document; deal-desk is the gate **before** signing |
| `commercial/skills/commercial-policy` (sibling) | Designs the discount matrix and approval thresholds | Deal-desk **applies** that policy to one deal at a time |
| `c-level-advisor/skills/general-counsel-advisor` | Deep legal redline + term-sheet analysis | Operates on full contract prose; deal-desk uses structured terms JSON |
| `c-level-advisor/skills/cfo-advisor` | Burn rate, unit economics, fundraising models | Strategic finance; deal-desk is one-deal granularity |
## Quick examples
```bash
# Score a deal
python3 scripts/deal_scorer.py --sample
python3 scripts/deal_scorer.py --input my_deal.json --profile enterprise-software
# Route the discount
python3 scripts/discount_approval_router.py --sample
python3 scripts/discount_approval_router.py --input my_deal.json --profile saas
# Flag the redlines
python3 scripts/terms_redliner.py --sample
python3 scripts/terms_redliner.py --input my_deal_terms.json --output json
```
The sample (a 28%-discount enterprise SaaS deal with uncapped indemnity + MFN) correctly DECLINEs at 55.4 / 100 composite and routes to AE → Deal Desk → VP Sales → CFO → CRO → General Counsel.
## Forcing-question library (Matt Pocock grill discipline)
Walked one at a time by `/cs:grill-commercial` or the Commercial orchestrator. Recommended answer + canon citation per question. Never bundled.
1. **"What's the gross margin at full discount, AND what does next quarter's pipeline look like at the same terms?"**
Recommended: model both. Refuse to approve until the AE can articulate the precedent risk.
Canon: David Skok (For Entrepreneurs — discount math), Tomasz Tunguz benchmarks. Anti-pattern: one 40% precedent reshapes 3 quarters of pipeline.
2. **"Is this discount inside or outside the standard discount matrix?"**
Recommended: if outside, surface the policy exception explicitly and route to the named exception approver.
Canon: OpenView discount benchmarks, RevOps Co-op playbooks.
3. **"What's the strategic value beyond ARR — logo, reference, expansion path?"**
Recommended: require a named, verifiable expansion or reference commitment in writing.
Canon: SaaStr (Jason Lemkin) on logo discounts; Winning by Design on commitment language.
4. **"Has the customer signed an indemnity cap, a liability cap, and a DPA (if EU data)?"**
Recommended: required. Uncapped indemnity is a critical-signal override that blocks APPROVE regardless of margin.
Canon: WorldCC (formerly IACCM) commercial management research, GC100 contract guidance.
5. **"What payment terms — NET-30, NET-45, or NET-60+?"**
Recommended: prefer NET-30; NET-45+ is a cash flow drag worth quantifying.
Canon: KeyBanc SaaS Survey, Pacific Crest data — every 15 days of payment terms costs ~2% of effective deal value.
6. **"Is the term multi-year with annual prepay, or annual auto-renew?"**
Recommended: multi-year prepay > annual prepay > annual auto-renew. Auto-renew without 60-day notice is a redline.
Canon: Salesforce Deal Desk best practices, OpenView NRR studies.
7. **"Who is the named human approver at each hop of the discount chain?"**
Recommended: surface the name, not just the role. "VP Sales" is not an approver; "Maria Singh, VP Sales" is.
Canon: Bridge Group SaaS AE compensation research — named approval reduces precedent drift by 50%+.
Walk depth-first. Lock 1-4 before opening 5-7. After all 7 are answered, invoke `deal_scorer.py``discount_approval_router.py``terms_redliner.py` in sequence.

View file

@ -0,0 +1,186 @@
# Deal Intake — Deal Desk Review
**Time to fill out: ~20 minutes.** This is the single source of truth for the deal. Re-pricings or term changes create a *new* intake — do not edit in place.
The structured fields at the bottom (the JSON blocks) feed directly into the three scripts:
- `deal_scorer.py` → consumes the **Deal Scorecard JSON**
- `discount_approval_router.py` → consumes the **Discount Routing JSON**
- `terms_redliner.py` → consumes the **Terms JSON**
---
## 1. Deal identity
| Field | Value |
|---|---|
| Deal ID | `ACME-2026-Q2-117` |
| Customer name | |
| AE / deal owner | |
| Sales engineer (if any) | |
| Date submitted | |
| Target close date | |
| Industry / segment | |
## 2. Commercial summary
| Field | Value |
|---|---|
| ARR (annual recurring revenue, $) | |
| Total contract value (TCV, $) | |
| Term (months) | |
| List price (TCV before discount, $) | |
| Discount (%) | |
| Customer tier | `enterprise` / `mid` / `smb` |
| Industry profile | `saas` / `enterprise-software` / `services` / `marketplace` |
## 3. Margin
| Field | Value |
|---|---|
| Product gross margin (%) | |
| Implementation / onboarding cost ($) | |
| Custom dev / SOW work in scope? | `yes` / `no` |
| If yes — services margin (%) | |
## 4. Strategic flags
Check each that applies. Each flag justifies *some* commercial flexibility but the discount scorer requires at least one for above-band discounts.
- [ ] **Logo** — reference-quality customer name; shortens future sales cycles.
- [ ] **Reference** — customer has agreed (in writing) to act as a reference / case study.
- [ ] **Expansion** — committed expansion plan in the next 12 months (named, quantified).
- [ ] **Renewal** — this is a renewal with multi-year extension.
## 5. Payment shape
| Field | Value |
|---|---|
| Payment terms (days from invoice) | |
| Billing frequency | `annual upfront` / `quarterly` / `monthly` |
| Multi-year discount applied? | `yes` / `no` |
| Up-front payment offered for discount? | `yes` / `no` |
## 6. Terms — customer-flagged redlines
List each clause the customer has flagged or modified. The scripts treat each entry as a risk signal.
1. ...
2. ...
3. ...
## 7. Structured terms (for `terms_redliner.py`)
Fill in the known structured fields:
| Term | Value |
|---|---|
| Auto-renew? | `true` / `false` |
| Auto-renew notice days | |
| Indemnity cap (multiple of fees, or `null` if uncapped) | |
| Liability cap (multiple of annual fees) | |
| DPA present? | `true` / `false` |
| EU personal data involved? | `true` / `false` |
| IP assignment | `vendor` / `customer` / `ambiguous` / `perpetual_license_back` |
| MFN clause present? | `true` / `false` |
| Exclusivity clause present? | `true` / `false` |
| Exclusivity compensated? | `true` / `false` |
| Non-solicit term (years) | |
| Governing law | |
| Vendor home jurisdiction | |
---
## 8. JSON skeletons — paste these into files for the scripts
### Deal Scorecard JSON (`deal.json`)
```json
{
"deal_id": "ACME-2026-Q2-117",
"customer_name": "Acme Corp",
"arr": 240000,
"term_months": 24,
"discount_pct": 28.0,
"payment_terms_days": 60,
"list_price": 333333,
"gross_margin_pct": 78.0,
"customer_tier": "enterprise",
"strategic_value": {
"logo": true,
"reference": false,
"expansion": true,
"renewal": false
},
"term_redlines": [
"uncapped indemnity",
"MFN pricing"
]
}
```
Run:
```bash
python3 scripts/deal_scorer.py --input deal.json --profile saas
```
### Discount Routing JSON (`discount.json`)
```json
{
"deal_id": "ACME-2026-Q2-117",
"discount_pct": 28.0,
"deal_size_arr": 240000,
"customer_tier": "enterprise",
"policy_thresholds": null
}
```
Run:
```bash
python3 scripts/discount_approval_router.py --input discount.json --profile saas
```
### Terms JSON (`deal_terms.json`)
```json
{
"deal_id": "ACME-2026-Q2-117",
"payment_terms_days": 60,
"auto_renew": true,
"auto_renew_notice_days": 90,
"indemnity_cap": null,
"liability_cap": 1.0,
"dpa_present": false,
"eu_data_involved": true,
"ip_assignment": "ambiguous",
"mfn_clause_present": true,
"exclusivity_clause_present": false,
"exclusivity_compensated": false,
"non_solicit_years": 3,
"governing_law": "Delaware",
"vendor_home_jurisdiction": "Delaware"
}
```
Run:
```bash
python3 scripts/terms_redliner.py --input deal_terms.json
```
---
## 9. Reviewer checklist
Before submitting the intake to the deal desk:
- [ ] All commercial fields populated (no blanks in section 2).
- [ ] Strategic flags reflect *committed*, not hoped-for, value.
- [ ] All customer-flagged redlines listed in section 6.
- [ ] Structured terms in section 7 match the actual marked-up contract.
- [ ] JSON skeletons (section 8) saved to files.
The deal-desk packet that comes back will name the approver(s) who must sign. **The skill never approves the deal itself.**

View file

@ -0,0 +1,145 @@
# Contract Landmines
The 10 founder/seller-killer patterns the `terms_redliner.py` tool detects, with example counter-language for each. This is a triage reference, **not** legal advice — every HIGH/CRITICAL finding must be reviewed by named counsel before signing.
For deep prose-level redline of an actual contract, use `c-level-advisor/skills/general-counsel-advisor/scripts/contract_risk_scanner.py`. The tool in this skill operates on a *structured terms JSON*, which is what the deal desk typically has from the intake template.
## The 10 patterns
### 1. UNCAPPED_INDEMNITY (CRITICAL)
**Trigger**: `indemnity_cap` is `null` or absent.
**Why it matters**: A single indemnity claim can be larger than the entire ARR of the deal — sometimes larger than the company's revenue. Uncapped indemnity is the most common contract risk that destroys early-stage companies.
**Counter-language**:
> "Each party's aggregate liability for indemnification obligations shall not exceed twelve (12) times the monthly subscription fees paid in the twelve (12) months preceding the claim, except for breaches of confidentiality, willful misconduct, or third-party intellectual-property infringement, for which a super-cap of three (3) times annual fees shall apply."
**Approver**: General Counsel + CFO.
### 2. MISSING_DPA_EU_DATA (CRITICAL)
**Trigger**: `eu_data_involved == True` and `dpa_present == False`.
**Why it matters**: GDPR Article 28 mandates a Data Processing Agreement when personal data of EU residents is processed by a service provider. Missing DPA = (a) regulatory exposure under GDPR, (b) immediate audit failure on any SOC 2 or ISO 27001 review, (c) customer escalation to their privacy officer.
**Counter-language**: Attach standard DPA (2021/914 Standard Contractual Clauses, or vendor's own template) as an exhibit. **Do not sign the master agreement until the DPA is countersigned.**
**Approver**: General Counsel + DPO.
### 3. MFN_PRICING (HIGH)
**Trigger**: `mfn_clause_present == True`.
**Why it matters**: Most-Favored-Nation clauses bind the seller to refund the customer (or extend matching terms) if any other customer gets a better price. This freezes pricing innovation: no bundles, no segment pricing, no competitive deals without triggering MFN obligations across the base.
**Counter-language**:
> "Strike Section [X] (Most-Favored-Nation Pricing) in its entirety. If retained, scope to: same SKU, same volume tier, same contract term, same geography, and same industry vertical; and time-bound to twelve (12) months from the Effective Date."
**Approver**: VP Sales + CFO.
### 4. AUTORENEW_LONG_NOTICE (HIGH)
**Trigger**: `auto_renew == True` and `auto_renew_notice_days > 30`.
**Why it matters**: Auto-renewal with a long notice window (60, 90, 120 days) is a classic vendor trap. Customers miss the window and get locked into another full term — but this also goes the other way: as a seller, accepting 60+ day notice on your own auto-renewals gives the buyer asymmetric exit.
**Counter-language**:
> "Either party may provide written notice of non-renewal not less than thirty (30) days prior to the end of the then-current term."
**Approver**: Deal Desk + General Counsel.
### 5. PERPETUAL_LICENSE_BACK (CRITICAL)
**Trigger**: `ip_assignment == "perpetual_license_back"`.
**Why it matters**: A perpetual license-back gives the customer the right to use the vendor's IP **forever**, often royalty-free and surviving termination. This kills the moat — the customer can stop paying and keep using.
**Counter-language**:
> "Customer's license to the Services and Vendor IP is co-terminus with the Subscription Term, field-of-use limited to internal business operations, non-transferable, non-sublicensable, and terminates upon any termination or expiration of this Agreement."
**Approver**: General Counsel + CEO.
### 6. AMBIGUOUS_IP (HIGH)
**Trigger**: `ip_assignment == "ambiguous"`.
**Why it matters**: Ambiguous IP ownership becomes a dispute at acquisition diligence. Buyers will hold back purchase price (or walk) until IP chain-of-title is clarified. Costs weeks of legal time and can break an M&A deal.
**Counter-language**:
> "Vendor retains all right, title, and interest in and to the Services, the Vendor IP, and any improvements, modifications, or derivatives thereof developed in connection with this Agreement. Customer retains all right, title, and interest in Customer Data and in any outputs derived solely from Customer Data."
**Approver**: General Counsel.
### 7. EXCLUSIVITY_UNCOMPENSATED (CRITICAL)
**Trigger**: `exclusivity_clause_present == True` and `exclusivity_compensated == False`.
**Why it matters**: Exclusivity removes the entire competitive segment of the addressable market for no economic benefit. Even *paid* exclusivity needs a kill switch on missed quarterly minimums — otherwise the buyer locks the seller into the segment without performance pressure.
**Counter-language**:
> "Strike exclusivity in its entirety. If retained, exclusivity is contingent on Minimum Guaranteed Spend of $[X] per quarter, payable in advance, and Vendor may terminate exclusivity (while preserving the underlying agreement) upon two consecutive quarters of MGS shortfall."
**Approver**: CRO + General Counsel.
### 8. LONG_PAYMENT_TERMS (HIGH)
**Trigger**: `payment_terms_days > 45`.
**Why it matters**: NET-60/75/90/120 inflates DSO and ties up working capital. A $200K deal on NET-90 is effectively $200K of zero-interest financing extended to the buyer. Material on any deal that's > 10% of cash balance.
**Counter-language**:
> "Payment terms shall be NET-30 from invoice date. Customer may elect NET-15 prepay terms in exchange for a 1.5% prepayment discount. Late payments accrue interest at 1.5% per month or the maximum permitted by law, whichever is lower."
**Approver**: CFO + Deal Desk.
### 9. LOW_LIABILITY_CAP (MEDIUM)
**Trigger**: `liability_cap < 1.0` (multiple of annual fees).
**Why it matters**: When the customer pushes for a sub-1x liability cap, they're usually expecting outsized claims. Don't accept without symmetric protection (mutual cap, both directions).
**Counter-language**:
> "Each party's aggregate liability shall not exceed one (1) times the fees paid by Customer in the twelve (12) months preceding the claim, except for breaches of confidentiality, IP infringement, or willful misconduct, for which a super-cap of three (3) times annual fees shall apply. This cap is mutual and applies to both parties."
**Approver**: General Counsel.
### 10. BROAD_NON_SOLICIT (MEDIUM)
**Trigger**: `non_solicit_years >= 2`.
**Why it matters**: Multi-year non-solicit clauses limit hiring and are increasingly unenforceable in many US jurisdictions (notably California, where they are void as a matter of public policy except in narrow circumstances). Negotiate down.
**Counter-language**:
> "Each party agrees not to solicit for employment any employee of the other party who was directly engaged on the project for a period of twelve (12) months following such employee's last day of engagement on the project. This restriction does not apply to general advertising, solicitation through public job boards, or responses to unsolicited inquiries."
**Approver**: General Counsel + CHRO.
## Sources
1. **Y Combinator — Startup Library** — Sam Altman's and the YC partners' canonical guidance on contracts founders sign. https://www.ycombinator.com/library
2. **Robert Klingberg — *Founder's Guide to SaaS Agreements*** — Practitioner reference on SaaS-specific contract patterns (MSA, DPA, BAA, MNDA).
3. **Bowman + Brooke — Contract Redline Guides** — Defense-side commercial litigation firm's published guides on enterprise contract risk.
4. **IACCM / WorldCC — World Commerce & Contracting Research** — The trade association for commercial contracting; annual surveys of *the most negotiated terms* and *the most disputed terms* in B2B contracts. https://www.worldcc.com/
5. **Practical Law (Thomson Reuters) — Contracts Library** — Standard clause library + redline best practices used by AmLaw 100 firms.
6. **Bradley Tusk — *The Fixer: My Adventures Saving Startups from Death by Politics*** — Practical advice on enterprise contracts, including the patterns that destroy young companies.
7. **GC100 — General Counsel Forum** — Senior in-house counsel from FTSE 100 companies; their guidance on commercial contract risk allocation. https://www.gc100.co.uk/
8. **American Bar Association — *Model Software License Provisions*** — Reference for industry-standard software licensing terms.
## How to use this reference
1. The deal-desk intake template asks the AE to capture the structured terms.
2. `terms_redliner.py --input deal_terms.json` produces a ranked list of detected landmines.
3. Each landmine is mapped to a section in this document with the counter-language and named approver.
4. The deal-desk packet attaches the counter-language so the AE can return to the customer with a defensible position.
Remember: **every CRITICAL or HIGH finding must reach the named approver before the deal closes.** This skill triages; it does not approve.

View file

@ -0,0 +1,64 @@
# Deal Desk Canon
Operating practice for per-deal review and approval routing in B2B SaaS / enterprise software. Compiled from authoritative deal-desk and revenue-operations sources.
## Why a deal desk exists
The deal desk is the **operational gate between sales and finance/legal**. Its job:
1. **Standardize discount approval** so the same discount-percent always routes the same way.
2. **Defend gross margin** by quantifying the actual margin loss from a proposed discount (not just the discount percent).
3. **Triage commercial terms** so legal review hits only the deals that need it.
4. **Speed up the deals that should be fast** by routing simple deals to AE/Manager authority and reserving CFO/CRO attention for the consequential ones.
Without a deal desk, every above-band deal becomes a 1:1 negotiation between an AE and a finance leader, which is slow, inconsistent, and creates pricing-integrity drift over time.
## Operating tenets
These are the non-negotiables — adopted across every reference cited below.
1. **Never auto-approve.** Even green deals get a named approver. The skill outputs *who must sign*, not *the deal is fine*.
2. **Margin, not discount.** A 30% discount on an 80%-gross-margin product reduces *margin* by 24 points (to 56%) — not 30%. See `discount_economics.md` for the math.
3. **The chain stops at the lowest hop that has authority.** Over-routing trains reps to over-discount because they expect VP attention anyway.
4. **Critical signals override composite.** A high-composite deal with uncapped indemnity is still a DECLINE.
5. **Modifiers must be explicit.** Enterprise floor (large ARR forces VP review) and SMB fast-lane (small deals can skip a hop) are surfaced; hidden adjustments destroy audit trails.
6. **The deal desk is a router, not a salesperson.** It does not negotiate; it routes the negotiation to the named human.
7. **One source of truth per deal.** The intake template is the spec. Re-pricings or term changes create a new intake, not an edit-in-place.
## Standard approval bands (industry-customary)
Default policy (override with `policy_thresholds` in input JSON):
| Discount band | Approver | Typical cycle |
|---|---|---|
| 0% - 15% | AE | same-day |
| 15% - 25% | Sales Manager | 1 business day |
| 25% - 35% | Director of Sales | 2 business days |
| 35% - 50% | VP Sales | 3 business days |
| 50%+ | CFO + CRO | 5+ business days |
Enterprise-software profile shifts bands upward (larger ACVs absorb deeper discounts). Services profile shifts downward (margin-thin). Marketplace profile is tightly capped (take-rate is the lever).
## Tier and ARR modifiers
- **Enterprise floor**: Deals at ARR >= profile threshold force VP-level review even on small discounts. Rationale: the customer is consequential regardless of the discount.
- **SMB fast-lane**: Deals at ARR <= profile threshold can drop one hop (only if discount is within the second band). Rationale: cycle time matters more than marginal margin defense on a $12K deal.
## Sources
1. **SaaStr** — Jason Lemkin's deal-desk playbooks emphasize that the deal desk's primary job is *defending gross margin and pricing integrity*, not just routing discounts. https://www.saastr.com/
2. **Winning by Design** — Jacco van der Kooij + Jason Reichl, *Bowtie Funnel* and *Revenue Architecture*. Establishes that the deal desk owns the gate between Acquisition (sales) and Retention (CS) — bad-term deals cost more in churn than they earn in ARR. https://winningbydesign.com/
3. **Forrester Research** — Deal desk maturity model (4 stages: ad-hoc → formal → strategic → predictive). Most companies hit a wall at stage 2 because they lack the data infrastructure to score deals consistently.
4. **RevOps Co-op** — Community playbooks (operating notes from Iceberg RevOps, Sapphire Ventures, others). Emphasizes that the deal desk is a **routing function**, not an approval function. The named approver is always a human.
5. **OpenView Venture Partners***State of the SaaS Sales Org* annual benchmarks. Documents discount-band conventions across stage (seed → growth → late-stage) and shows that median discount creeps up year-over-year unless deal-desk discipline is enforced. https://openviewpartners.com/
6. **Bridge Group SaaS AE Compensation Research** — Annual survey of B2B SaaS AE comp + quota. Establishes that AE discount authority above 15-20% destroys quota attainment math (because the AE under-prices to close).
7. **Salesforce Deal Desk Best Practices** — Internal Salesforce documentation (Trailhead + RevOps blog). Codifies the queue model: every above-AE-authority deal enters a queue with SLA. Aging deals escalate automatically.
## Patterns to surface in any deal-desk review packet
- Composite score with per-dimension breakdown.
- Named approver chain with the hop where the discount lands highlighted.
- Estimated cycle days based on hop count.
- Any CRITICAL signals (uncapped indemnity, MFN, perpetual license-back, missing DPA).
- The standard counter-language for any HIGH/CRITICAL redline.
- A **single explicit statement**: "This is a routing recommendation. The named approvers must sign."

View file

@ -0,0 +1,83 @@
# Discount Economics
The math of what a discount actually costs. Most sales discounts are described as a list-price reduction; the real impact is on **gross margin** and **LTV**, both of which compound across the customer base over time.
## The fundamental formula
A discount of D% on a product with gross margin G% reduces net margin by:
margin_loss_points = D * (G / 100)
net_margin = G - margin_loss_points
### Worked examples
| List discount | Gross margin | Margin loss | Net margin |
|---|---|---|---|
| 10% | 80% | 8 pts | 72% |
| 20% | 80% | 16 pts | 64% |
| **30%** | **80%** | **24 pts** | **56%** |
| 30% | 60% | 18 pts | 42% |
| 40% | 80% | 32 pts | 48% |
| 50% | 80% | 40 pts | 40% |
**A 30% discount on an 80%-gross-margin product wipes 24 points of margin** — that's a 30% margin loss in *relative* terms (24/80 = 30%), but the conventional shorthand "30% discount = 30% margin hit" understates the absolute hit on a low-margin product.
### Why the conventional shorthand is wrong
People often say "a 30% discount loses 30% of margin." That's only true for a 100%-margin product. For an 80%-margin SaaS, the discount cuts the **revenue** by 30% but the **margin** by 30% × (80/100) = 24 points, or 30% in relative terms. The dollar impact compounds across the contract term.
## LTV impact
Discount also compounds across multi-year contracts. A 24-month deal at 30% discount loses:
lifetime_margin_loss = (D / 100) * G/100 * list_price * (term_months / 12)
For a $200K-ARR deal at 30% discount, 80% gross margin, 24-month term:
= 0.30 * 0.80 * 200,000 * 2 = $96,000 of gross margin given up
That's $96K of fully-loaded P&L impact for one deal. Across 50 deals/quarter at the same discount, the company is giving up $19.2M/year in gross margin.
## Discount creep
The most-cited dataset (Pacific Crest / KeyBanc SaaS Survey) shows median discount rises ~1.5 pts/year unless the deal desk actively defends pricing. Causes:
1. AE comp on bookings, not margin → AEs discount to close.
2. Multi-year deals trade discount for term length but term length doesn't recover the margin loss if churn risk is non-zero.
3. Competitive deals get matched discounts that then propagate to non-competitive deals via MFN clauses.
4. Renewal discounts (CS giving discount to retain) anchor the next renewal lower.
## When a discount is justified
The deal desk should approve a discount when **at least one** of these is true and quantified:
1. **Strategic logo** — the customer is a reference account that materially shortens future sales cycles. Logo value ≥ discount $.
2. **Expansion lock-in** — the discount is paired with a *multi-year + expansion commitment* that recovers margin over the contract term.
3. **Competitive displacement** — the discount displaces an incumbent and the lifetime ARR > displacement cost.
4. **Cash-acceleration** — payment up-front in exchange for discount, where the cash NPV recovers the margin loss.
The deal scorer's `strategic` dimension flags logo / reference / expansion / renewal explicitly. If none of those are set, a discount above the policy band is presumptively unjustified.
## NRR + discount correlation
OpenView's *State of the SaaS Industry* shows companies with high NRR (≥ 120%) discount less on initial deals than companies with low NRR (≤ 100%). The mechanism: high-NRR companies have a strong expansion motion that they don't need to buy with up-front discount; low-NRR companies discount up-front to compensate for weak expansion.
This is why deal-desk should treat "discount to close" as a leading indicator of NRR weakness, not a one-deal problem.
## Sources
1. **David Skok — For Entrepreneurs***SaaS Metrics 2.0* and *The SaaS Business Model*. Canonical treatment of LTV/CAC + the impact of discount on payback period. https://www.forentrepreneurs.com/
2. **Bessemer Venture Partners — State of the Cloud** — Annual report with discount benchmarks by ACV band ($1K, $10K, $100K, $1M+) and stage. https://www.bvp.com/
3. **Tomasz Tunguz — Redpoint** — Multi-year studies on discount-to-close patterns, including the finding that median enterprise SaaS discount sits at 18-22% across the industry. https://tomtunguz.com/
4. **OpenView Venture Partners***State of the SaaS Industry* + Expansion Economics research. Documents the NRR-vs-discount correlation. https://openviewpartners.com/
5. **Pacific Crest SaaS Survey** (now KeyBanc Capital Markets) — Annual primary-research survey of B2B SaaS companies. Most-cited dataset for discount benchmarks. https://www.key.com/businesses-institutions/industry-expertise/saas-survey.html
6. **KeyBanc Capital Markets SaaS Survey** — Continuation of Pacific Crest. Annual benchmark for net dollar retention, gross margin, and discount-by-segment.
7. **Insight Partners Revenue Operations Research** — Their PitchBook + portfolio data on discount discipline at growth-stage SaaS. https://www.insightpartners.com/
## Patterns to surface in any margin review
- Pre-discount gross margin and post-discount net margin in **absolute points**, not just percent.
- Lifetime margin given up over the contract term, in dollars.
- Whether the strategic flags justify the discount (logo / reference / expansion / renewal).
- Whether the customer is paying up-front in exchange for the discount (cash NPV).
- Comparison to the company's median deal-discount (drift signal).

View file

@ -0,0 +1,357 @@
#!/usr/bin/env python3
"""deal_scorer.py - Score an inbound deal across 5 dimensions and route the verdict.
Stdlib-only. NEVER auto-approves. Output is always a numeric breakdown plus a verdict
(APPROVE / REVIEW / ESCALATE / DECLINE) and a NAMED HUMAN APPROVER chain.
The 5 dimensions (each 0-100, weighted into a composite):
1. margin - post-discount gross margin vs profile target
2. risk - payment terms + redline count + customer tier
3. strategic - logo / reference / expansion / renewal value
4. commercial - is the discount within the profile policy band
5. term shape - multi-year + payment-up-front vs short, NET-60+ tail
Routing rule (intentionally conservative):
- composite >= 80 and no CRITICAL signals -> APPROVE (still names the approver)
- composite 65-79 -> REVIEW (Deal Desk + Sales Director)
- composite 50-64 or 1 CRITICAL -> ESCALATE (VP Sales + CFO)
- composite < 50 or 2+ CRITICAL -> DECLINE (CRO + CFO must sign off any override)
Usage:
python deal_scorer.py --sample
python deal_scorer.py --input deal.json --profile saas
python deal_scorer.py --input deal.json --output json
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from typing import Any
SAMPLE_DEAL = {
"deal_id": "ACME-2026-Q2-117",
"customer_name": "Acme Corp",
"arr": 240000,
"term_months": 24,
"discount_pct": 28.0,
"payment_terms_days": 60,
"list_price": 333333,
"gross_margin_pct": 78.0,
"customer_tier": "enterprise",
"strategic_value": {
"logo": True,
"reference": False,
"expansion": True,
"renewal": False,
},
"term_redlines": [
"uncapped indemnity",
"MFN pricing",
],
}
# Industry profiles tune the target margin floor, acceptable discount band,
# and payment-terms tolerance.
PROFILES: dict[str, dict[str, Any]] = {
"saas": {
"target_gross_margin": 75.0,
"discount_band_pct": 25.0,
"max_payment_terms_days": 30,
"preferred_term_months": 24,
},
"enterprise-software": {
"target_gross_margin": 70.0,
"discount_band_pct": 35.0,
"max_payment_terms_days": 45,
"preferred_term_months": 36,
},
"services": {
"target_gross_margin": 45.0,
"discount_band_pct": 15.0,
"max_payment_terms_days": 30,
"preferred_term_months": 12,
},
"marketplace": {
"target_gross_margin": 30.0,
"discount_band_pct": 10.0,
"max_payment_terms_days": 14,
"preferred_term_months": 12,
},
}
# Routing chain by composite + signals. The skill NEVER says "approved" by itself;
# it names the human(s) who must sign.
APPROVER_CHAIN = {
"APPROVE": ["AE", "Deal Desk Analyst", "Sales Director"],
"REVIEW": ["AE", "Deal Desk Analyst", "Sales Director", "VP Sales"],
"ESCALATE": ["AE", "Deal Desk Analyst", "Sales Director", "VP Sales", "CFO", "CRO"],
"DECLINE": ["AE", "Deal Desk Analyst", "VP Sales", "CFO", "CRO", "General Counsel"],
}
@dataclass
class DimensionScore:
name: str
score: float
weight: float
rationale: str
@dataclass
class DealScorecard:
deal_id: str
profile: str
composite_score: float
verdict: str
approver_chain: list[str]
dimensions: list[DimensionScore] = field(default_factory=list)
critical_signals: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)
def _clamp(x: float, lo: float = 0.0, hi: float = 100.0) -> float:
return max(lo, min(hi, x))
def score_margin(deal: dict, profile: dict) -> DimensionScore:
"""Effective margin after discount, compared to profile target.
Math: a D% discount on a product with gross_margin_pct G% drops margin to
new_margin = (G - D) / (1 - D/100) approximately, but the canonical
formulation we use is: net_margin = G - (D * (1 - cost_ratio)) which
resolves to:
net_margin = G - D * (G / 100)
i.e. a 30% discount on an 80% margin product wipes 24 points of margin,
leaving 56% well below an 75% SaaS target.
"""
g = float(deal.get("gross_margin_pct", 0.0))
d = float(deal.get("discount_pct", 0.0))
net_margin = g - (d * (g / 100.0))
target = profile["target_gross_margin"]
# Score: 100 if net_margin >= target, sliding to 0 at (target - 30 pts)
delta = net_margin - target
score = _clamp(100.0 + (delta / 30.0) * 100.0)
rationale = (
f"Gross margin {g:.1f}% with {d:.1f}% discount -> net margin {net_margin:.1f}% "
f"vs profile target {target:.1f}% (delta {delta:+.1f} pts)"
)
return DimensionScore("margin", round(score, 1), 0.30, rationale)
def score_risk(deal: dict, profile: dict) -> DimensionScore:
"""Risk = payment terms shape + redline count + customer-tier offset."""
payment_days = int(deal.get("payment_terms_days", 30))
redlines = deal.get("term_redlines", []) or []
tier = (deal.get("customer_tier") or "smb").lower()
# Base score 100, deduct per risk factor.
score = 100.0
payment_max = profile["max_payment_terms_days"]
if payment_days > payment_max:
over = payment_days - payment_max
score -= min(40.0, over * 0.8) # NET-90 vs NET-30 = 48 days over = -38.4
score -= min(40.0, len(redlines) * 12.0) # each redline = -12
# SMB tier on long terms is riskier than enterprise on same terms
if tier == "smb" and payment_days > 30:
score -= 10.0
elif tier == "enterprise" and payment_days <= 45:
score += 5.0 # enterprise tolerance bump
score = _clamp(score)
rationale = (
f"NET-{payment_days} terms (profile max {payment_max}), "
f"{len(redlines)} redline(s), tier={tier}"
)
return DimensionScore("risk", round(score, 1), 0.20, rationale)
def score_strategic(deal: dict, profile: dict) -> DimensionScore:
"""Strategic value from logo, reference, expansion, renewal flags."""
sv = deal.get("strategic_value", {}) or {}
weights = {"logo": 25, "reference": 20, "expansion": 30, "renewal": 25}
earned = sum(w for k, w in weights.items() if sv.get(k))
rationale = "Flags: " + ", ".join(k for k in weights if sv.get(k)) if earned else "No strategic flags set"
return DimensionScore("strategic", float(earned), 0.15, rationale)
def score_commercial(deal: dict, profile: dict) -> DimensionScore:
"""Is the discount within the profile's policy band?"""
d = float(deal.get("discount_pct", 0.0))
band = profile["discount_band_pct"]
if d <= band:
# Within band, score linearly from 100 (no discount) to 80 (band edge)
score = 100.0 - (d / band) * 20.0
rationale = f"Discount {d:.1f}% within policy band <= {band:.1f}%"
else:
over = d - band
# Drop 6 points per percentage over band, floor 0
score = max(0.0, 80.0 - over * 6.0)
rationale = f"Discount {d:.1f}% EXCEEDS policy band {band:.1f}% by {over:.1f} pts"
return DimensionScore("commercial", round(score, 1), 0.20, rationale)
def score_term_shape(deal: dict, profile: dict) -> DimensionScore:
"""Term length vs preferred + payment up front."""
term_months = int(deal.get("term_months", 12))
preferred = profile["preferred_term_months"]
payment_days = int(deal.get("payment_terms_days", 30))
# Length component: 100 if >= preferred, sliding to 40 at half-preferred, floor 30
if term_months >= preferred:
length = 100.0
elif term_months <= preferred / 2:
length = 30.0
else:
length = 30.0 + ((term_months - preferred / 2) / (preferred / 2)) * 70.0
# Payment component: NET-30 or shorter = 100, NET-60 = 70, NET-90+ = 40
if payment_days <= 30:
pay = 100.0
elif payment_days <= 60:
pay = 70.0
elif payment_days <= 90:
pay = 40.0
else:
pay = 20.0
score = 0.6 * length + 0.4 * pay
rationale = (
f"{term_months}-mo term (preferred {preferred}), NET-{payment_days} payment "
f"-> length={length:.0f}, payment={pay:.0f}"
)
return DimensionScore("term_shape", round(score, 1), 0.15, rationale)
def _detect_critical_signals(deal: dict, dims: list[DimensionScore]) -> list[str]:
sigs: list[str] = []
redlines = [r.lower() for r in deal.get("term_redlines", []) or []]
critical_terms = (
"uncapped indemnity",
"uncapped liability",
"mfn",
"most-favored-nation",
"perpetual license-back",
"exclusivity",
)
for r in redlines:
if any(ct in r for ct in critical_terms):
sigs.append(f"critical redline: {r}")
# margin below 35% is a critical economic signal on any profile
for d in dims:
if d.name == "margin" and d.score < 30.0:
sigs.append("margin below target by >30 pts")
if d.name == "commercial" and d.score < 30.0:
sigs.append("discount far outside policy band")
return sigs
def _verdict(composite: float, criticals: list[str]) -> str:
n_crit = len(criticals)
if n_crit >= 2 or composite < 50.0:
return "DECLINE"
if n_crit == 1 or composite < 65.0:
return "ESCALATE"
if composite < 80.0:
return "REVIEW"
return "APPROVE"
def score_deal(deal: dict, profile_name: str = "saas") -> DealScorecard:
if profile_name not in PROFILES:
raise ValueError(f"Unknown profile '{profile_name}'. Choose from {list(PROFILES)}.")
profile = PROFILES[profile_name]
dims = [
score_margin(deal, profile),
score_risk(deal, profile),
score_strategic(deal, profile),
score_commercial(deal, profile),
score_term_shape(deal, profile),
]
composite = sum(d.score * d.weight for d in dims)
criticals = _detect_critical_signals(deal, dims)
verdict = _verdict(composite, criticals)
notes = [
"This skill does NOT auto-approve. The approver chain below is who must sign.",
f"Composite is weighted: margin 30, risk 20, strategic 15, commercial 20, term 15.",
]
if criticals:
notes.append(f"{len(criticals)} critical signal(s) detected; cannot APPROVE.")
return DealScorecard(
deal_id=str(deal.get("deal_id", "UNSPECIFIED")),
profile=profile_name,
composite_score=round(composite, 1),
verdict=verdict,
approver_chain=APPROVER_CHAIN[verdict],
dimensions=dims,
critical_signals=criticals,
notes=notes,
)
def _render_human(card: DealScorecard) -> str:
lines = []
lines.append(f"Deal Scorecard: {card.deal_id}")
lines.append(f"Profile: {card.profile}")
lines.append(f"Composite Score: {card.composite_score}/100")
lines.append(f"Verdict: {card.verdict}")
lines.append("")
lines.append("Dimension breakdown:")
for d in card.dimensions:
lines.append(f" - {d.name:10s} {d.score:5.1f} (weight {d.weight:.2f})")
lines.append(f" {d.rationale}")
lines.append("")
if card.critical_signals:
lines.append("Critical signals:")
for s in card.critical_signals:
lines.append(f" ! {s}")
lines.append("")
lines.append("Approver chain (named humans who must sign):")
lines.append(" " + " -> ".join(card.approver_chain))
lines.append("")
for n in card.notes:
lines.append(f"note: {n}")
return "\n".join(lines)
def _to_jsonable(card: DealScorecard) -> dict:
d = asdict(card)
return d
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Score a deal across 5 dimensions and route to a named approver.",
)
parser.add_argument("--input", help="Path to JSON deal context")
parser.add_argument("--profile", default="saas", choices=list(PROFILES))
parser.add_argument("--output", default="human", choices=["human", "json"])
parser.add_argument("--sample", action="store_true", help="Use embedded sample deal")
args = parser.parse_args(argv)
if args.sample or not args.input:
deal = SAMPLE_DEAL
else:
with open(args.input) as f:
deal = json.load(f)
card = score_deal(deal, args.profile)
if args.output == "json":
print(json.dumps(_to_jsonable(card), indent=2))
else:
print(_render_human(card))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""discount_approval_router.py - Route a discount request to the right human(s).
Stdlib-only. Outputs the NAMED APPROVER CHAIN, the hop where this deal lands,
and an estimated approval-cycle in business days. The skill never says "approved"
only "routes to <person>".
Default policy bands (industry-customary, can be overridden in input JSON):
0% - 15% AE-approved
15% - 25% Sales Manager
25% - 35% Director of Sales
35% - 50% VP Sales
50% + CFO / CRO
Deal-size and tier modifiers nudge the chain (e.g. enterprise deal > $500K ARR
ALWAYS requires VP review even at 10% discount; SMB deal < $25K ARR may stop
one hop earlier for speed).
Usage:
python discount_approval_router.py --sample
python discount_approval_router.py --input deal.json --profile saas
python discount_approval_router.py --input deal.json --output json
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, asdict, field
from typing import Any
SAMPLE_INPUT = {
"deal_id": "ACME-2026-Q2-117",
"discount_pct": 32.0,
"deal_size_arr": 240000,
"customer_tier": "enterprise",
"policy_thresholds": None, # use defaults
}
DEFAULT_BANDS = [
{"max_pct": 15.0, "approver": "AE", "days": 0},
{"max_pct": 25.0, "approver": "Sales Manager", "days": 1},
{"max_pct": 35.0, "approver": "Director of Sales", "days": 2},
{"max_pct": 50.0, "approver": "VP Sales", "days": 3},
{"max_pct": 100.1, "approver": "CFO + CRO", "days": 5},
]
PROFILES: dict[str, dict[str, Any]] = {
"saas": {
"bands": DEFAULT_BANDS,
"enterprise_floor_approver": "VP Sales",
"enterprise_floor_arr": 500000,
"smb_fast_lane_arr": 25000,
},
"enterprise-software": {
# Larger ACVs absorb deeper discounts; bands shift up
"bands": [
{"max_pct": 20.0, "approver": "AE", "days": 0},
{"max_pct": 30.0, "approver": "Sales Manager", "days": 1},
{"max_pct": 40.0, "approver": "Director of Sales", "days": 2},
{"max_pct": 55.0, "approver": "VP Sales", "days": 4},
{"max_pct": 100.1, "approver": "CFO + CRO", "days": 7},
],
"enterprise_floor_approver": "VP Sales",
"enterprise_floor_arr": 1000000,
"smb_fast_lane_arr": 50000,
},
"services": {
# Margin-thin: even small discounts go up the chain fast
"bands": [
{"max_pct": 5.0, "approver": "AE", "days": 0},
{"max_pct": 12.0, "approver": "Sales Manager", "days": 1},
{"max_pct": 20.0, "approver": "Director of Sales", "days": 2},
{"max_pct": 30.0, "approver": "VP Services", "days": 3},
{"max_pct": 100.1, "approver": "CFO + COO", "days": 5},
],
"enterprise_floor_approver": "VP Services",
"enterprise_floor_arr": 250000,
"smb_fast_lane_arr": 10000,
},
"marketplace": {
# Take-rate is the lever; explicit discounts are rare and tightly capped
"bands": [
{"max_pct": 3.0, "approver": "AE", "days": 0},
{"max_pct": 8.0, "approver": "Sales Manager", "days": 1},
{"max_pct": 15.0, "approver": "Director of Sales", "days": 2},
{"max_pct": 25.0, "approver": "VP Sales", "days": 3},
{"max_pct": 100.1, "approver": "CFO + CRO", "days": 7},
],
"enterprise_floor_approver": "VP Sales",
"enterprise_floor_arr": 500000,
"smb_fast_lane_arr": 15000,
},
}
@dataclass
class RoutingResult:
deal_id: str
profile: str
discount_pct: float
deal_size_arr: float
customer_tier: str
landing_approver: str
approver_chain: list[str] = field(default_factory=list)
estimated_cycle_days: int = 0
modifiers_applied: list[str] = field(default_factory=list)
notes: list[str] = field(default_factory=list)
def _bands_for(deal: dict, profile: dict) -> list[dict]:
"""Allow caller to override via deal.policy_thresholds; else use profile."""
custom = deal.get("policy_thresholds")
if custom:
# Expect list of {max_pct, approver, days} dicts; light validation
out = []
for b in custom:
out.append({
"max_pct": float(b["max_pct"]),
"approver": str(b["approver"]),
"days": int(b.get("days", 2)),
})
return sorted(out, key=lambda x: x["max_pct"])
return profile["bands"]
def route_discount(deal: dict, profile_name: str = "saas") -> RoutingResult:
if profile_name not in PROFILES:
raise ValueError(f"Unknown profile '{profile_name}'. Choose from {list(PROFILES)}.")
profile = PROFILES[profile_name]
bands = _bands_for(deal, profile)
pct = float(deal.get("discount_pct", 0.0))
arr = float(deal.get("deal_size_arr", 0.0))
tier = (deal.get("customer_tier") or "mid").lower()
# Find the landing band
landing = bands[-1]
for b in bands:
if pct <= b["max_pct"]:
landing = b
break
chain: list[str] = []
days = 0
for b in bands:
chain.append(b["approver"])
days += b["days"]
if b is landing:
break
modifiers: list[str] = []
# Enterprise floor: large ARR forces VP-level review even on small discounts
if tier == "enterprise" and arr >= profile["enterprise_floor_arr"]:
floor = profile["enterprise_floor_approver"]
if floor not in chain:
# Insert before any role above it; simplest is append + dedupe
chain.append(floor)
modifiers.append(
f"enterprise floor: ARR ${arr:,.0f} >= ${profile['enterprise_floor_arr']:,} "
f"forces {floor} review"
)
days += 2
# SMB fast-lane: small deals can stop one hop early IF discount <= second-band cap
if (
tier == "smb"
and arr <= profile["smb_fast_lane_arr"]
and len(chain) > 2
and pct <= bands[1]["max_pct"]
):
dropped = chain.pop()
modifiers.append(
f"SMB fast-lane: ARR ${arr:,.0f} <= ${profile['smb_fast_lane_arr']:,} "
f"drops {dropped} from chain"
)
days = max(0, days - 1)
# Dedup chain while preserving order
seen: set[str] = set()
ordered = []
for a in chain:
if a not in seen:
ordered.append(a)
seen.add(a)
chain = ordered
notes = [
"This is a routing recommendation. The skill does NOT approve.",
f"Discount {pct:.1f}% landed in the '{landing['approver']}' band "
f"(<= {landing['max_pct']:.1f}%).",
]
if pct > 50.0:
notes.append("Discount > 50%: CFO/CRO MUST sign and Finance should re-run unit economics.")
return RoutingResult(
deal_id=str(deal.get("deal_id", "UNSPECIFIED")),
profile=profile_name,
discount_pct=pct,
deal_size_arr=arr,
customer_tier=tier,
landing_approver=landing["approver"],
approver_chain=chain,
estimated_cycle_days=days,
modifiers_applied=modifiers,
notes=notes,
)
def _render_human(r: RoutingResult) -> str:
lines = []
lines.append(f"Discount Routing: {r.deal_id}")
lines.append(f"Profile: {r.profile}")
lines.append(f"Discount: {r.discount_pct:.1f}% ARR: ${r.deal_size_arr:,.0f} Tier: {r.customer_tier}")
lines.append("")
lines.append("Approver chain (hops in order):")
for i, a in enumerate(r.approver_chain, start=1):
marker = " <-- discount lands here" if a == r.landing_approver else ""
lines.append(f" {i}. {a}{marker}")
lines.append("")
lines.append(f"Estimated approval cycle: {r.estimated_cycle_days} business day(s)")
if r.modifiers_applied:
lines.append("")
lines.append("Modifiers applied:")
for m in r.modifiers_applied:
lines.append(f" * {m}")
lines.append("")
for n in r.notes:
lines.append(f"note: {n}")
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Route a discount request to the right named approver(s).",
)
parser.add_argument("--input", help="Path to JSON request")
parser.add_argument("--profile", default="saas", choices=list(PROFILES))
parser.add_argument("--output", default="human", choices=["human", "json"])
parser.add_argument("--sample", action="store_true")
args = parser.parse_args(argv)
if args.sample or not args.input:
deal = SAMPLE_INPUT
else:
with open(args.input) as f:
deal = json.load(f)
result = route_discount(deal, args.profile)
if args.output == "json":
print(json.dumps(asdict(result), indent=2))
else:
print(_render_human(result))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""terms_redliner.py - Detect commercial-contract landmines in a deal's terms.
Stdlib-only. Takes a JSON description of the deal's terms (NOT the full contract
text for full text scanning, see c-level-advisor/skills/general-counsel-advisor/
scripts/contract_risk_scanner.py).
Detects 10 founder/seller-killer patterns and emits a RANKED REDLINE LIST with:
- severity CRITICAL | HIGH | MEDIUM | LOW
- the standard counter-language
- the NAMED legal/commercial approver (no auto-approval; everything routes)
The skill never says the deal is fine on terms; it only outputs which clauses
need human sign-off and by whom.
Usage:
python terms_redliner.py --sample
python terms_redliner.py --input deal_terms.json
python terms_redliner.py --input deal_terms.json --output json
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, asdict, field
SAMPLE_TERMS = {
"deal_id": "ACME-2026-Q2-117",
"payment_terms_days": 75,
"auto_renew": True,
"auto_renew_notice_days": 90,
"indemnity_cap": None, # None = uncapped
"liability_cap": 1.0, # multiplier on annual fees (1x = standard)
"dpa_present": False,
"eu_data_involved": True,
"ip_assignment": "ambiguous", # "customer" | "vendor" | "ambiguous" | "perpetual_license_back"
"mfn_clause_present": True,
"exclusivity_clause_present": False,
"exclusivity_compensated": False,
"non_solicit_years": 3,
"governing_law": "Delaware",
"vendor_home_jurisdiction": "Delaware",
}
SEVERITY_RANK = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
@dataclass
class Redline:
rule_id: str
severity: str
title: str
why_it_matters: str
standard_counter: str
approver: str
def _rules() -> list[dict]:
"""Each rule: id, severity, title, predicate(terms), why, counter, approver."""
return [
{
"id": "UNCAPPED_INDEMNITY",
"severity": "CRITICAL",
"title": "Uncapped indemnity exposure",
"predicate": lambda t: t.get("indemnity_cap") is None,
"why": (
"Uncapped indemnity is the single biggest founder-killer in commercial "
"contracts. A breach claim can wipe out the company."
),
"counter": (
"Cap indemnity at 12x monthly fees OR mutual cap; carve out only IP "
"infringement and gross-negligence/willful-misconduct."
),
"approver": "General Counsel + CFO",
},
{
"id": "MISSING_DPA_EU_DATA",
"severity": "CRITICAL",
"title": "EU personal data flows but no DPA",
"predicate": lambda t: t.get("eu_data_involved") and not t.get("dpa_present"),
"why": (
"GDPR Art. 28 requires a DPA when personal data of EU residents is "
"processed. Missing DPA = regulatory exposure + customer audit fail."
),
"counter": (
"Attach standard DPA (SCC 2021/914 or your template) and confirm "
"sub-processor list. Block close until DPA is countersigned."
),
"approver": "General Counsel + DPO",
},
{
"id": "MFN_PRICING",
"severity": "HIGH",
"title": "Most-Favored-Nation pricing clause present",
"predicate": lambda t: bool(t.get("mfn_clause_present")),
"why": (
"MFN binds you to refund any customer whose price drops below this one. "
"Limits future flexibility on bundles, segments, and competitive deals."
),
"counter": (
"Strike MFN entirely. If counterparty insists, narrow to 'same SKU, "
"same volume, same term, same geography' and time-bound to 12 months."
),
"approver": "VP Sales + CFO",
},
{
"id": "AUTORENEW_LONG_NOTICE",
"severity": "HIGH",
"title": "Auto-renew with notice window > 30 days",
"predicate": lambda t: (
t.get("auto_renew") and int(t.get("auto_renew_notice_days") or 0) > 30
),
"why": (
"Long notice windows on auto-renew are a classic trap: easy to miss, "
"and locks you into another full term. Especially painful on multi-year."
),
"counter": (
"Reduce notice to 30 days OR require affirmative re-signature each term."
),
"approver": "Deal Desk + General Counsel",
},
{
"id": "PERPETUAL_LICENSE_BACK",
"severity": "CRITICAL",
"title": "Perpetual license-back of IP to customer",
"predicate": lambda t: t.get("ip_assignment") == "perpetual_license_back",
"why": (
"Perpetual license-back gives the customer rights to use your IP "
"forever, often royalty-free, surviving termination. Kills moat."
),
"counter": (
"Convert to time-bounded license tied to subscription term, "
"field-of-use restricted, no transferability."
),
"approver": "General Counsel + CEO",
},
{
"id": "AMBIGUOUS_IP",
"severity": "HIGH",
"title": "IP ownership ambiguous",
"predicate": lambda t: t.get("ip_assignment") == "ambiguous",
"why": (
"Ambiguous IP becomes a dispute at acquisition diligence. Costs "
"weeks of legal review and can break a deal."
),
"counter": (
"Clarify: vendor retains all pre-existing IP and IP developed in "
"delivery; customer owns its data and outputs derived solely from it."
),
"approver": "General Counsel",
},
{
"id": "EXCLUSIVITY_UNCOMPENSATED",
"severity": "CRITICAL",
"title": "Exclusivity clause without compensation",
"predicate": lambda t: (
t.get("exclusivity_clause_present") and not t.get("exclusivity_compensated")
),
"why": (
"Free exclusivity removes addressable market for no economic benefit. "
"Even paid exclusivity needs a kill switch on missed quarterly minimums."
),
"counter": (
"Either strike exclusivity OR price it (minimum guaranteed spend) AND "
"add an exit ramp if MGS isn't hit two consecutive quarters."
),
"approver": "CRO + General Counsel",
},
{
"id": "LONG_PAYMENT_TERMS",
"severity": "HIGH",
"title": "Payment terms longer than NET-45",
"predicate": lambda t: int(t.get("payment_terms_days") or 0) > 45,
"why": (
"NET-60/75/90 inflates DSO, ties up working capital, and is a classic "
"buyer ploy. Material on any deal > 10% of cash balance."
),
"counter": (
"Counter to NET-30; offer 1-2% discount for NET-15 prepay if customer "
"won't move. Add late-payment interest of 1.5% / mo on any overdue."
),
"approver": "CFO + Deal Desk",
},
{
"id": "LOW_LIABILITY_CAP",
"severity": "MEDIUM",
"title": "Liability cap below 1x annual fees",
"predicate": lambda t: float(t.get("liability_cap") or 0.0) < 1.0,
"why": (
"Customer pushing for sub-1x cap usually indicates they expect "
"outsized claims. Don't accept without symmetric protection."
),
"counter": (
"Hold liability cap at 1x annual fees (12-month look-back), mutual; "
"super-cap (3x) on IP and confidentiality breaches if needed."
),
"approver": "General Counsel",
},
{
"id": "BROAD_NON_SOLICIT",
"severity": "MEDIUM",
"title": "Non-solicit longer than 12 months",
"predicate": lambda t: int(t.get("non_solicit_years") or 0) >= 2,
"why": (
"Multi-year non-solicit limits hiring and is increasingly unenforceable "
"in many US jurisdictions (e.g. California). Negotiate down."
),
"counter": (
"Cap non-solicit at 12 months post-termination, scoped to employees "
"directly engaged on the project, with exception for general advertising."
),
"approver": "General Counsel + CHRO",
},
]
def scan_terms(terms: dict) -> list[Redline]:
findings: list[Redline] = []
for rule in _rules():
try:
if rule["predicate"](terms):
findings.append(
Redline(
rule_id=rule["id"],
severity=rule["severity"],
title=rule["title"],
why_it_matters=rule["why"],
standard_counter=rule["counter"],
approver=rule["approver"],
)
)
except (KeyError, TypeError, ValueError):
# Missing or malformed field for this rule -> skip silently
continue
findings.sort(key=lambda r: (SEVERITY_RANK[r.severity], r.rule_id))
return findings
def _render_human(deal_id: str, findings: list[Redline]) -> str:
lines = []
lines.append(f"Terms Redline Report: {deal_id}")
lines.append(f"{len(findings)} landmine(s) detected.")
lines.append("")
if not findings:
lines.append("No flagged terms. STILL route to General Counsel for sign-off — ")
lines.append("this scanner only catches the 10 most common patterns.")
return "\n".join(lines)
for i, f in enumerate(findings, start=1):
lines.append(f"{i}. [{f.severity}] {f.title}")
lines.append(f" why: {f.why_it_matters}")
lines.append(f" counter: {f.standard_counter}")
lines.append(f" approver: {f.approver}")
lines.append("")
lines.append("note: This is a triage tool, not legal advice. All HIGH/CRITICAL")
lines.append(" findings must be reviewed by named approver before signing.")
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Scan deal terms JSON for commercial-contract landmines.",
)
parser.add_argument("--input", help="Path to JSON terms")
parser.add_argument("--output", default="human", choices=["human", "json"])
parser.add_argument("--sample", action="store_true")
args = parser.parse_args(argv)
if args.sample or not args.input:
terms = SAMPLE_TERMS
else:
with open(args.input) as f:
terms = json.load(f)
findings = scan_terms(terms)
deal_id = str(terms.get("deal_id", "UNSPECIFIED"))
if args.output == "json":
print(json.dumps({
"deal_id": deal_id,
"finding_count": len(findings),
"findings": [asdict(f) for f in findings],
}, indent=2))
else:
print(_render_human(deal_id, findings))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,130 @@
---
name: pricing-strategist
description: "Use when designing or revisiting product pricing — selecting a pricing model (subscription seat-based, usage-based, value-based, freemium, or hybrid), running Van Westendorp Price Sensitivity Meter analysis on WTP survey data, or designing Good/Better/Best packaging tiers. Recommends a model and a price range with trade-offs, never a single number. For Commercial leads, Product Marketing, and CMOs at the pricing-design moment — not deal-by-deal discounting, not brand positioning."
version: 2.8.0
author: claude-code-skills
license: MIT
tags: [commercial, pricing, packaging, wtp, van-westendorp, value-based-pricing, saas-pricing]
compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli]
---
# pricing-strategist
## Purpose
Help Commercial, Product Marketing, and CMO functions answer three questions at the pricing-design moment:
1. **Which pricing model fits this product + customer + market?** (subscription seat-based, usage-based, value-based, freemium, hybrid)
2. **What does the customer actually pay before it feels too expensive?** (Van Westendorp PSM on WTP survey responses)
3. **How should we package this into tiers?** (Good / Better / Best — with anti-pattern detection)
The skill recommends **a model and a range**. The human picks the number, owns the trade-offs, and runs the GTM.
## When to use
- Launching a new SaaS / API / AI tool and choosing the first pricing model
- Revisiting pricing after 18+ months of GTM data (model shift, not just price increase)
- Designing or redesigning tier packaging (Good/Better/Best, Bronze/Silver/Gold)
- You have Van Westendorp survey data and want the optimal price range
- A board / exec is asking "what should we charge?" and you need the structured answer
- You suspect your packaging has anti-patterns (decoy tier, feature dump, no upgrade trigger)
**Do not use for:**
- Per-deal discount approval → `deal-desk`
- Strategic CMO positioning, brand, category creation → `c-level-advisor/cmo-advisor`
- Whole-company revenue strategy → `c-level-advisor/cro-advisor`
- Technical-sale enablement → `business-growth/sales-engineer`
## Workflow
### Step 1 — Assess customer context
Fill `assets/pricing_brief_template.md` (≈ 20 min). Capture: industry, deal size avg, customer count, value drivers, adoption curve, consumption pattern (seat / usage / value / hybrid), competitor models.
### Step 2 — Pick the pricing model
Run `scripts/pricing_model_picker.py --input brief.json --profile saas --output markdown`. Output ranks 5 models by fit-score 0-100 with trade-offs. Decision logic is deterministic: low usage variance + high seat-attach → subscription wins; power-law usage + variable customer value → usage-based wins.
### Step 3 — Validate WTP with Van Westendorp PSM
If you have survey data (≥ 4 questions per respondent: too cheap / bargain / getting expensive / too expensive), run `scripts/wtp_analyzer.py --input survey.json --output markdown`. Output: 4 intersection points (OPP, IDP, PMC, PME) and the Range of Acceptable Prices.
PSM gives a **range**, not the price. See `references/van_westendorp_methodology.md` for common misinterpretations.
### Step 4 — Design packaging
Run `scripts/packaging_designer.py --input features.json --profile saas --output markdown`. Output: 3-tier Good/Better/Best assignment with anti-pattern flags (decoy tier, feature dump, no upgrade trigger, Bronze loss leader, Enterprise no-anchor).
### Step 5 — Decide
Take model + range + packaging into the pricing committee. Skill does not commit the number — you do.
## Scripts
- `scripts/pricing_model_picker.py` — 5-model fit scorer (subscription / usage / value / freemium / hybrid)
- `scripts/wtp_analyzer.py` — Van Westendorp PSM implementation
- `scripts/packaging_designer.py` — Good/Better/Best tier designer with anti-pattern detection
All scripts: stdlib only. `--help` and `--sample` work on all three.
## References
- `references/saas_pricing_canon.md` — Skok, Tunguz, Campbell, Ramanujam, BVP, Shevlin, Stanford GSB
- `references/van_westendorp_methodology.md` — original 1976 paper, NMS refinement, Conjoint.ly, Sawtooth, ESOMAR, Lipovetsky, Decision Analyst
- `references/packaging_anti_patterns.md` — ProfitWell, OpenView, BVP vertical SaaS, Ramanujam, Poyar, SaaS Capital
## Assumptions
- Pricing decisions are joint: Commercial owns the model + tier shape, Product owns the features-per-tier, Finance owns the discount envelope, Legal owns the contract.
- Van Westendorp PSM is a **directional** tool. N ≥ 30 minimum, N ≥ 100 preferred. Below 30, the script emits a sample-size warning.
- "Value-based pricing" requires a measurable customer value driver (revenue lift, cost saved, time recovered). If you can't measure it, don't pick value-based.
- Industry profiles tune defaults — they don't override your data.
- This is a decision-support skill, not a price oracle. Output is a model + range, never the number.
## Anti-patterns
- **Recommending a specific number.** This skill emits a model and a range. Final price is a human commercial decision involving deal-desk policy, competitive intel, and strategic intent that this skill cannot know.
- **Using PSM with N < 30.** Statistical noise dominates. The script warns; respect the warning.
- **Treating PSM as "the price."** PSM gives a Range of Acceptable Prices (RAP) and an Optimal Price Point (OPP). Test the range in market, don't anchor on a single intersection.
- **Picking value-based pricing without a measurable value metric.** Without instrumentation to show customer ROI, value-based collapses into "whatever they'll pay" — which is just bad usage-based pricing.
- **Designing tiers before picking a model.** Tier structure depends on the model. Run pricing_model_picker first.
- **Packaging "feature dumps" into the Best tier.** If Best has 3x the features for 2x the price, customers buy Better and never upgrade. See `packaging_anti_patterns.md`.
- **Hidden usage-based pricing inside subscription tiers.** "Up to 100k API calls/mo, then $X per 1k" disguised as a "Pro tier" is two pricing models in one. Customers notice. Pick one.
- **Confusing this skill with deal-desk.** Pricing strategy = the menu. Deal-desk = approving discounts off the menu. Different decision, different cadence, different owner.
## Distinct from
- **deal-desk** — per-deal discount approval, MEDDIC, deal scoring. Operates daily on existing pricing.
- **c-level-advisor/cmo-advisor** — strategic positioning, brand, category. Pricing strategist consumes positioning as input, doesn't generate it.
- **c-level-advisor/cro-advisor** — full-funnel revenue strategy, comp plans, territory design. Pricing strategist is one input to CRO.
- **business-growth/sales-engineer** — technical sale, POC scoping. Sales engineering operates after pricing is set.
## Forcing-question library (Matt Pocock grill discipline)
Walked one at a time by `/cs:grill-commercial` or the orchestrator. Recommended answer + canon citation per question. Never bundled.
1. **"Is your customer paying for outcomes, seats, or usage?"**
Recommended: outcomes (value-based) if you can measure them; usage if marginal cost is variable; seats only if usage is roughly flat per user.
Canon: Ramanujam 2016 (*Monetizing Innovation*) — Mistake #1 of 9: seat-based pricing on a usage-variable product caps TAM at ~20% of WTP.
2. **"Do you have a measurable value metric, or are you guessing?"**
Recommended: instrument the value metric BEFORE going to market with value-based pricing.
Canon: Patrick Campbell / ProfitWell research — value-based without instrumentation collapses into bad usage-based pricing.
3. **"What's the variance in customer usage across your top decile vs. median?"**
Recommended: variance > 10x → usage-based wins; variance < 3x subscription wins; in between hybrid with usage overage.
Canon: Kyle Poyar (*Growth Unhinged*) — high-variance products lose 60%+ of revenue on flat-rate plans.
4. **"What's your competitor's pricing model, and why are you choosing the same or different?"**
Recommended: surface the differentiation hypothesis explicitly. Identical pricing = identical value claim.
Canon: David Skok (*For Entrepreneurs*) — pricing is a positioning signal.
5. **"What sample size do you have for WTP analysis, and is it segmented?"**
Recommended: N≥30 per segment for PSM, N≥100 for conjoint.
Canon: van Westendorp 1976 / Sawtooth Software methodology — sub-30 PSM is statistical noise.
6. **"What's the ONE feature that forces a tier upgrade?"**
Recommended: every Better and Best tier needs a single non-negotiable upgrade trigger.
Canon: Ramanujam (*Monetizing Innovation*) — Mistake #4: tiers with no clear differentiator make 70% of customers pick the cheapest.
Walk depth-first. Lock 1-3 before opening 4-6. After all 6 are answered, invoke `pricing_model_picker.py``wtp_analyzer.py``packaging_designer.py` in sequence.

View file

@ -0,0 +1,116 @@
# Pricing Strategy Brief
**Owner:** _______________ **Date:** _______________
**Time to fill out:** ≈ 20 minutes
Fill this brief out *before* running `pricing_model_picker.py`. The skill outputs are only
as good as the inputs. Be specific. If you don't know a field, write "unknown" — don't guess.
---
## 1. Product context
- **Product / feature being priced:** _______________
- **Customer-visible name:** _______________
- **One-line value prop:** _______________
- **Stage:** [ ] new launch [ ] re-pricing [ ] adding tier [ ] expansion play
## 2. Customer context
- **Industry:** _______________ (e.g., "B2B SaaS — sales intelligence")
- **ICP (Ideal Customer Profile, 1 sentence):** _______________
- **Avg deal size today (annual contract value):** $_______________
- **Customer count today:** _______________
- **Geographic concentration:** _______________
## 3. Value drivers (rank top 3)
The customer outcome that pricing should track. Be specific — "saves time" is not a value
driver. "Reduces lead-research time by 6 hours/rep/week" is.
1. _______________
2. _______________
3. _______________
For each, can you measure it? [ ] yes [ ] partly [ ] no
## 4. Adoption curve
- [ ] Top-down enterprise sale (CIO/VP signs)
- [ ] Bottom-up / PLG (individual user adopts, expands)
- [ ] Hybrid (champion-led, exec-approved)
- [ ] Viral (referral loops within or across orgs)
## 5. Consumption pattern (assign 0.0 - 1.0 to each)
How does customer value scale with what they use?
- **Seat-based** (more users = more value): _______________
- **Usage-based** (more events / API calls / volume = more value): _______________
- **Value-based** (measurable customer outcome = more value): _______________
- **Hybrid** (multiple drivers, no single dominant): _______________
## 6. Competitive pricing landscape
List the 3-5 closest competitors and their pricing model:
| Competitor | Pricing model | Notable mechanics |
|---|---|---|
| | | |
| | | |
| | | |
## 7. Strategic constraints
- **Margin floor (gross margin %):** _______________
- **Discount envelope (max % off list):** _______________
- **Sales motion (self-serve / inside / field):** _______________
- **Any contractual / regulatory pricing constraints?** _______________
## 8. Anti-goals
What pricing outcomes would be a *failure* even if NRR looks fine?
- _______________
- _______________
---
## JSON skeleton (for the script)
Copy this into a file (e.g., `brief.json`) and fill in. Then run:
```bash
python scripts/pricing_model_picker.py --input brief.json --profile saas --output markdown
```
```json
{
"industry": "",
"deal_size_avg": 0,
"customer_count": 0,
"value_drivers": [
""
],
"adoption_curve": "",
"consumption_pattern": {
"seat-based": 0.0,
"usage-based": 0.0,
"value-based": 0.0,
"hybrid": 0.0
},
"competitor_pricing_models": [
""
]
}
```
---
## After running the picker
1. Take the top 1-2 model recommendations into a 30-min review with Product + Finance.
2. If a model is selected, run a **Van Westendorp PSM survey** (≥ 30 respondents, preferably 100+).
3. Feed survey data to `wtp_analyzer.py` to get the Range of Acceptable Prices.
4. Run `packaging_designer.py` on your feature list to draft Good/Better/Best tiers.
5. Pressure-test in pricing committee. The skill output is one input, not the decision.

View file

@ -0,0 +1,127 @@
# Packaging Anti-Patterns
Reference for `packaging_designer.py`. The anti-pattern detectors in the tool implement the
flags below. This document is the source-of-truth for *why* each pattern is harmful.
---
## The seven anti-patterns
### 1. Decoy tier that fools no one
A middle tier designed to make the top tier look reasonable — but the price gap is too small,
the feature list is too thin, or the differentiation is purely cosmetic. Customers see through
it, and worse: it trains them to question your pricing integrity.
**Detection:** Best tier > 2x Better price with < 1.5x value.
**Fix:** Either compress Better and Best closer (real differentiation) or widen the value gap.
### 2. Feature dump in the Best tier
Every roadmap feature gets tossed into Best because "Enterprise wants it." Result: Best has
3x the features for 2x the price. Customers buy Better and never upgrade.
**Detection:** Best feature count > 2x Better's with price ratio < 1.5x.
**Fix:** Move 1-3 "upgrade trigger" features down to Better and re-price.
### 3. No clear upgrade trigger
A customer on Good has no specific friction that pushes them to Better. Marketing fixes this
with "more advanced features" copy — but if you can't name the *single event* that triggers
the upgrade ("you hit 10k API calls", "you added a 5th seat", "you needed SSO"), customers
don't upgrade.
**Detection:** Better tier features have lower average importance than Good tier features.
**Fix:** Identify 1-2 "moment of pain" features and move them into the gate.
### 4. Usage-based pricing hidden inside subscription tiers
"Pro tier: includes up to 100k events, then $X per 1k." This is two pricing models pretending
to be one. Customers feel deceived when overage hits. Either commit to subscription (with a
realistic cap) or commit to usage (with a transparent meter).
**Detection:** Pricing-page narrative inspection — not algorithmic. Flag manually.
**Fix:** Pick one model. If you genuinely need both, use a clean Platform + Consumption hybrid,
not a hidden-overage subscription.
### 5. Bronze / Good tier as loss leader
Good is so cheap that cost-to-serve eats most of the revenue. Customers stay on Good forever
because the value-per-dollar is too good. Acquisition costs amortize through Better/Best
upgrades that never happen.
**Detection:** Cost-to-serve aggregate > 80% of Good tier price.
**Fix:** Raise Good's price floor, or strip a feature down to Better.
### 6. Enterprise / Best = "Call us" with no anchor
"Contact sales for pricing" at the top tier with no published anchor price. Prospects with
budget constraints disqualify themselves without ever talking to you. Competitors who publish
ranges win the consideration set.
**Detection:** Best tier has features but no published price.
**Fix:** Publish a "Starting at $X" anchor. The number doesn't have to be precise — it just
has to disqualify the wrong-fit prospects and qualify the right-fit ones.
### 7. Feature appears in all 3 tiers (no differentiation)
If "API access" is in Good, Better, and Best with the same scope, it's not a tier feature —
it's a base feature. Listing it three times wastes pricing-page real estate and dilutes the
upgrade narrative.
**Detection:** Feature appears in all 3 tiers' assigned-feature lists.
**Fix:** Either drop it from the tier comparison or differentiate scope (rate-limited /
metered / unlimited).
---
## Authoritative sources
1. **Patrick Campbell — ProfitWell / Paddle research on packaging**.
"The State of Subscription Pricing" reports and the ProfitWell podcast.
Empirical: tier redesigns that fixed clear upgrade triggers grew NRR by 8-15 points on
average across their cohort. https://www.paddle.com/resources
2. **Madhavan Ramanujam — Monetizing Innovation (Wiley, 2016)**.
The "9 mistakes" framework: feature shock, minivation, hidden gem, undead. Anti-patterns 2
(feature dump), 3 (no upgrade trigger), and 7 (no differentiation) map directly to
Ramanujam's mistake taxonomy.
3. **OpenView — SaaS Benchmarks + Product-Led Growth reports**.
Annual benchmarks on tier mix, free-to-paid conversion, and the cost of bad packaging.
https://openviewpartners.com/saas-benchmarks-report/
4. **Bessemer Venture Partners — Vertical SaaS Index + Cloud 100 Memos**.
Documents the move from 3-tier to 4-tier packaging in vertical SaaS as products mature, and
the failure modes when the 4th tier is added without removing complexity from existing tiers.
https://www.bvp.com/atlas
5. **Kyle Poyar — Growth Unhinged**.
"The Anatomy of a Great Pricing Page" series. Anti-patterns 5 (loss leader) and 6 (no
anchor) come from Poyar's documented PLG-to-enterprise transition patterns.
https://www.growthunhinged.com/
6. **SaaS Capital — Spending Benchmarks for Private B2B SaaS Companies (annual)**.
Cost-to-serve benchmarks by ACV band. Source for the "Bronze tier loss leader" threshold
(80% cost-to-serve ratio).
7. **Tomasz Tunguz — Theory Ventures**.
Multi-year posts on tier-mix evolution in Cloud 100 cohort. Documents the death of 5-tier
pricing pages and the consolidation toward Good/Better/Best + Enterprise.
https://tomtunguz.com/
8. **Simon-Kucher & Partners — Global Pricing Studies**.
Cross-industry data on pricing-page complexity vs conversion. Their research underpins the
"more tiers = more cognitive load" finding behind anti-pattern 4 (hidden usage inside subscription).
## How this skill uses the references
- `packaging_designer.py` runs deterministic detection for 7 anti-patterns (the manual-inspection
one — hidden usage in subscription — is documented but not auto-detected; the tool relies on
the human reading the pricing-page narrative).
- Industry profiles encode tier-mix priors (Good 50% / Better 30% / Best 20% for SaaS, etc.)
derived from OpenView and BVP benchmarks.
- Price-ratio thresholds (2.5x Good → Better, 2.0x Better → Best for SaaS) come from
ProfitWell + Tunguz cohort averages.
- The "no anchor price" flag implements the Poyar / BVP guidance that Enterprise tiers need
published starting prices.

View file

@ -0,0 +1,119 @@
# SaaS Pricing Canon
Curated, opinionated knowledge base for pricing model selection. This is the source material
behind `pricing_model_picker.py`'s scoring rules.
## Core principle
Pricing is a product decision, not a finance decision. The pricing model encodes how customers
experience value capture — get it wrong and every other GTM lever (sales, retention, expansion)
compounds the mistake.
---
## The five pricing models
### 1. Subscription seat-based
Customers pay per user, per period. Works when:
- Value scales linearly with user count
- Usage variance per seat is low
- Procurement prefers predictable line items
- Competitive set already trains the market on seat pricing
Failure modes: usage power-law (top 10% of users drive 80% of value) leaves money on the table;
"seat sprawl" makes customers hide users; expansion is gated on hiring, which is slow.
### 2. Usage-based (consumption)
Customers pay for what they consume — API calls, tokens, GB stored, messages sent. Works when:
- Value is tightly coupled to a measurable unit
- Usage variance across customers is high (power-law)
- Customer wants to start small and scale
- The metering infrastructure exists
Failure modes: bill-shock (variance scares procurement); cohort-NRR volatility; "cost of a query"
becomes a feature-velocity tax; revenue forecasting becomes hard.
### 3. Value-based
Price is anchored to the customer's economic outcome (revenue lift, cost saved, time recovered).
Works when:
- The value driver is measurable and attributable
- Customer count is small enough to calibrate per-account
- Deal size is large enough to justify the sales motion
- ROI proof is part of the product (not a slide)
Failure modes: requires instrumented ROI per customer; doesn't scale operationally beyond ~50-200
accounts without specialization; collapses to "whatever they'll pay" when value isn't measurable.
### 4. Freemium
Free tier acquires users, paid tiers monetize. Works when:
- Adoption is bottom-up / viral / PLG
- Free-tier cost-to-serve is < 5% of paid LTV
- There is a natural upgrade trigger inside the free experience
- Sales motion is self-serve or low-touch
Failure modes: enterprise sale + freemium dilutes positioning; free-tier costs balloon faster than
conversion; the "free forever for 10 users" cliff trains customers to game it.
### 5. Hybrid
Combinations — seat + usage overage, platform + per-event, base + value uplift. Works when:
- Multiple value drivers exist (seats AND usage)
- Customer segments split on dominant driver
- Deal sizes are large enough to absorb pricing-page complexity
Failure modes: cognitive load on the prospect; CS overhead in tier-to-tier moves; invoice disputes;
hybrid sometimes hides "we couldn't decide" — which customers detect.
---
## Authoritative sources
1. **David Skok — For Entrepreneurs**.
"SaaS Metrics 2.0" + "Unit Economics" series. The canonical playbook on CAC, LTV, and how
pricing model interacts with both. https://www.forentrepreneurs.com/saas-metrics-2/
2. **Tomasz Tunguz — Theory Ventures blog**.
Years of empirical posts on Cloud 100 pricing patterns, hybrid-pricing adoption curves,
usage-based unit economics. https://tomtunguz.com/
3. **Patrick Campbell — ProfitWell / Paddle research**.
The largest body of public SaaS pricing data. Key findings: prospects who see clear value
metrics convert 2x; freemium converts 2-5% on average; bad packaging is the #1 churn cause.
https://www.paddle.com/resources
4. **Madhavan Ramanujam — Monetizing Innovation (Wiley, 2016)**.
Simon-Kucher partner. The "9 Pricing Mistakes" frame: feature shock, minivation, hidden gem,
undead. Establishes the discipline that pricing comes before product, not after.
5. **Bessemer Venture Partners — State of the Cloud + Memos**.
Annual benchmarks: Rule of 40, NRR by ACV band, pricing-model mix in Cloud 100. The
reference for "what good looks like" in SaaS.
https://www.bvp.com/atlas
6. **Ron Shevlin — Cornerstone Advisors / Forbes columns**.
Pricing psychology applied to financial services SaaS — anchoring, decoy effect, charm
pricing's diminishing returns in B2B.
7. **Stanford GSB pricing research (Bertini, Gourville, Anderson)**.
Academic foundation on price-quality signaling, reference price formation, and the
penny-gap problem (the $0 → $0.01 conversion cliff). See Bertini & Gourville HBR 2012,
"Pricing to Create Shared Value."
8. **Kyle Poyar — OpenView / Growth Unhinged**.
Practitioner depth on PLG monetization, packaging redesigns, and the shift from seat to
hybrid pricing in 2020-2025 cohort. https://www.growthunhinged.com/
## How this skill uses the canon
- `pricing_model_picker.py` weights consumption-pattern signals per the Skok/Tunguz/Campbell
empirical priors.
- Industry profiles (`saas`, `api`, `ai-tools`, `enterprise-software`, `marketplace`) encode
default biases observed in BVP and ProfitWell cohort data.
- The "value-based requires measurable driver" gate comes directly from Ramanujam's "minivation"
failure mode.
- Freemium scoring penalties for high-ACV deals come from Poyar's documented PLG-to-enterprise
transition patterns.

View file

@ -0,0 +1,130 @@
# Van Westendorp Price Sensitivity Meter — Methodology
Reference for `wtp_analyzer.py`. Covers the 4 questions, the 4 intersection points,
sample size discipline, segmentation requirements, and the most common misinterpretations.
---
## The four questions
Each respondent answers, for the product or feature described:
1. **Too cheap** — "At what price would you consider the product so inexpensive that you'd
doubt its quality and not buy it?"
2. **Bargain** — "At what price would you consider the product a bargain — a great buy for the
money?"
3. **Getting expensive** — "At what price would you start to feel the product is getting
expensive, but you'd still consider buying it?"
4. **Too expensive** — "At what price would you consider the product so expensive that you
would not consider buying it?"
For each respondent, the answers should obey:
`too_cheap ≤ bargain ≤ getting_expensive ≤ too_expensive`
Respondents who violate this ordering are typically screened out before analysis (the tool
flags them as warnings).
---
## The four intersection points
Build cumulative curves on a sorted price grid:
- **% too cheap (≥ price)** — decreasing in price (more respondents say "too cheap" at low prices)
- **% bargain (≥ price)** — decreasing
- **% getting expensive (≤ price)** — increasing
- **% too expensive (≤ price)** — increasing
Then find the four intersections:
| Point | Curves | Interpretation |
|---|---|---|
| **OPP** — Optimal Price Point | too cheap ↔ too expensive | Equal % reject as too cheap and too expensive. Theoretical sweet spot. |
| **IDP** — Indifference Price Point | bargain ↔ getting expensive | Median respondent's perceived "fair" price. |
| **PMC** — Point of Marginal Cheapness | too cheap ↔ getting expensive | Lower bound of acceptable range — below this, quality doubt dominates. |
| **PME** — Point of Marginal Expensiveness | bargain ↔ too expensive | Upper bound of acceptable range — above this, purchase rejection dominates. |
**Range of Acceptable Prices (RAP) = [PMC, PME].**
---
## Sample size discipline
- **N < 30:** Directional only. Tool emits a warning. Do not anchor decisions on these results.
- **N = 30-99:** Acceptable for hypothesis generation; expect noisy intersections.
- **N ≥ 100:** Preferred. ESOMAR conventions cite N=200-400 for stable PSM in B2C; B2B can
work with smaller but more carefully screened panels.
- **Segmented PSM:** Run separately for ICP vs non-ICP, and per buying-role segment. Aggregate
PSM averages across segments hide the structure you need.
---
## Common misinterpretations (the high-cost ones)
1. **"PSM gives THE price."** — No. PSM gives a **range**. The number inside the range is a
commercial decision involving competition, positioning, and margin targets.
2. **"OPP is the optimal price."** — OPP is named misleadingly. It's the point of *equal
resistance from both sides*, not a profit-maximizing price. The optimal price often sits
between OPP and PME if the market tolerates upside.
3. **"PSM works on non-customers."** — PSM measures *perceived* price thresholds. Run it on
the actual ICP. Random survey panels produce intersection points for an imaginary buyer.
4. **"PSM works for any product."** — Original method (van Westendorp, 1976) was built for
consumer non-durables. It works for SaaS, but breaks for products where the customer cannot
form a reference price (truly novel categories). Use Newton-Miller-Smith (NMS) refinement
in those cases — adds purchase-likelihood at each price.
5. **"Higher RAP upper bound = we can charge more."** — Only if your willingness-to-act
matches willingness-to-state. Always validate with a real purchase test (conjoint, A/B,
or sales-priced cohort) before anchoring at PME.
---
## Authoritative sources
1. **Peter van Westendorp — "NSS-Price Sensitivity Meter (PSM)" — 29th ESOMAR Congress
Proceedings, 1976.** The original paper. Establishes the four questions and intersection
method. Still the canonical reference 50 years later.
2. **Gabor & Granger (1966), Newton, Miller & Smith (NMS).** Extension that adds
purchase-likelihood at each price. Conjoint.ly and Sawtooth both implement NMS variants
for novel products without strong reference prices.
3. **Conjoint.ly — "Price Sensitivity Meter (Van Westendorp) Explained"**.
Practitioner-grade explanation including segmentation guidance and NMS comparison.
https://conjointly.com/guides/van-westendorp-price-sensitivity-analysis/
4. **Sawtooth Software — Lighthouse Studio documentation on PSM**.
Industry-standard tooling. Their guidance on respondent screening, monotonicity checks,
and segmentation is the operational standard most pricing consultancies use.
5. **ESOMAR — Code of Conduct + price-sensitivity research guidance**.
Sample-size conventions, respondent qualification, ethical pricing research. PSM is
referenced in their pricing research best-practice papers.
6. **Stan Lipovetsky (2006) — "Van Westendorp Price Sensitivity in statistical modeling,"
International Journal of Operational Research.** Critique and statistical refinement —
shows that classical PSM intersections are biased estimators under common response
distributions. Recommends bootstrap CIs and ordinal regression overlays.
7. **Decision Analyst — "Van Westendorp PSM Handbook"**.
Operational handbook including a worked example, screening criteria, and segmentation
templates. https://www.decisionanalyst.com/
8. **Madhavan Ramanujam — Monetizing Innovation (Wiley, 2016), Ch. 4.**
PSM as one of three WTP techniques (alongside direct WTP and conjoint). Ramanujam's
guidance: PSM for category baseline, conjoint for feature-level WTP, direct WTP for
confirmation.
## How this skill uses the methodology
- `wtp_analyzer.py` implements classical PSM intersections using linear interpolation on the
sorted price grid — the standard approach per van Westendorp (1976) and Sawtooth.
- Tool emits sample-size warnings at N<30 and N<100, per ESOMAR / Decision Analyst conventions.
- Tool checks per-respondent monotonicity (`tc ≤ bg ≤ ge ≤ te`) and reports inconsistent rows.
- Output explicitly frames PSM as a **range**, not a price, and recommends segmented re-runs —
per the documented misinterpretation patterns above.
- Tool does not implement NMS extension; for novel categories without reference prices, point
the user to conjoint or NMS-specific tooling.

View file

@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""packaging_designer.py — Good/Better/Best tier designer with anti-pattern detection.
Input: JSON with feature list (importance + cost-to-serve), customer segments, and
current pricing. Output: 3-tier packaging assignment with anti-pattern flags.
Deterministic logic features are bucketed into tiers by importance × segment-fit.
No LLM calls.
Usage:
packaging_designer.py --input features.json --profile saas --output markdown
packaging_designer.py --sample
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
PROFILES = {
"saas": {"good_pct": 0.50, "better_pct": 0.30, "best_pct": 0.20, "price_ratio_good_to_better": 2.5, "price_ratio_better_to_best": 2.0},
"api": {"good_pct": 0.40, "better_pct": 0.30, "best_pct": 0.30, "price_ratio_good_to_better": 3.0, "price_ratio_better_to_best": 2.5},
"enterprise": {"good_pct": 0.30, "better_pct": 0.35, "best_pct": 0.35, "price_ratio_good_to_better": 2.0, "price_ratio_better_to_best": 2.5},
"prosumer": {"good_pct": 0.60, "better_pct": 0.25, "best_pct": 0.15, "price_ratio_good_to_better": 3.0, "price_ratio_better_to_best": 2.0},
}
@dataclass
class Feature:
name: str
importance: float # 0..1, how much customers value it
cost_to_serve: float # relative cost units
segment_fit: dict[str, float] = field(default_factory=dict) # segment → fit 0..1
@classmethod
def from_dict(cls, d: dict[str, Any]) -> "Feature":
return cls(
name=d["name"],
importance=float(d.get("importance", 0.5)),
cost_to_serve=float(d.get("cost_to_serve", 1.0)),
segment_fit=d.get("segment_fit", {}),
)
@dataclass
class Tier:
name: str
features: list[Feature] = field(default_factory=list)
price: float = 0.0
def assign_tiers(features: list[Feature], segments: list[str], profile: str) -> dict[str, Tier]:
"""Assign each feature to Good / Better / Best based on importance and segment fit.
Rule: high importance across all segments Good (base).
mid importance OR segment-skewed to mid Better.
low importance OR enterprise-skewed OR high cost-to-serve Best.
"""
good = Tier("Good")
better = Tier("Better")
best = Tier("Best")
# Identify enterprise-leaning segments (last in declared order is convention)
enterprise_seg = segments[-1] if segments else None
for f in features:
avg_fit = sum(f.segment_fit.values()) / len(f.segment_fit) if f.segment_fit else 0.5
enterprise_fit = f.segment_fit.get(enterprise_seg, avg_fit) if enterprise_seg else avg_fit
# High importance + broad fit → Good
if f.importance >= 0.75 and avg_fit >= 0.6 and f.cost_to_serve <= 2.0:
good.features.append(f)
# Enterprise-skewed OR high cost → Best
elif enterprise_fit >= 0.7 and avg_fit < 0.6:
best.features.append(f)
elif f.cost_to_serve >= 3.0:
best.features.append(f)
elif f.importance <= 0.4:
best.features.append(f)
# Everything else → Better
else:
better.features.append(f)
return {"good": good, "better": better, "best": best}
def price_tiers(tiers: dict[str, Tier], current_pricing: dict[str, float], profile: str) -> None:
"""Anchor pricing to current_pricing if provided; else use profile ratios from a base of 100."""
cfg = PROFILES[profile]
if current_pricing.get("good"):
tiers["good"].price = float(current_pricing["good"])
else:
tiers["good"].price = 100.0
if current_pricing.get("better"):
tiers["better"].price = float(current_pricing["better"])
else:
tiers["better"].price = tiers["good"].price * cfg["price_ratio_good_to_better"]
if current_pricing.get("best"):
tiers["best"].price = float(current_pricing["best"])
else:
tiers["best"].price = tiers["better"].price * cfg["price_ratio_better_to_best"]
def detect_anti_patterns(tiers: dict[str, Tier]) -> list[str]:
"""Return list of human-readable anti-pattern flags."""
flags: list[str] = []
good, better, best = tiers["good"], tiers["better"], tiers["best"]
# 1. Empty tier
for t in (good, better, best):
if not t.features:
flags.append(f"Empty tier: '{t.name}' has no features — collapse or re-balance.")
# 2. Feature in all tiers (no differentiation)
good_names = {f.name for f in good.features}
better_names = {f.name for f in better.features}
best_names = {f.name for f in best.features}
all_three = good_names & better_names & best_names
if all_three:
flags.append(f"No differentiation: features appear in all 3 tiers — {sorted(all_three)}.")
# 3. Feature dump in Best (>2x the count of Better with <1.5x the price)
if better.features and best.features and better.price > 0 and best.price > 0:
feature_ratio = len(best.features) / max(1, len(better.features))
price_ratio = best.price / better.price
if feature_ratio > 2.0 and price_ratio < 1.5:
flags.append(
f"Feature dump in Best: {len(best.features)} features vs Better's {len(better.features)} "
f"({feature_ratio:.1f}x) for only {price_ratio:.1f}x the price — customers will buy Better and never upgrade."
)
# 4. Best tier > 2x Better price with < 1.5x value (proxy: feature count weighted by importance)
def value(t: Tier) -> float:
return sum(f.importance for f in t.features)
if better.price > 0 and best.price > 0 and value(better) > 0:
price_jump = best.price / better.price
value_jump = value(best) / value(better)
if price_jump > 2.0 and value_jump < 1.5:
flags.append(
f"Best tier price-to-value mismatch: {price_jump:.1f}x price for only {value_jump:.1f}x value — "
"Best becomes a decoy that no one upgrades to."
)
# 5. No clear upgrade trigger from Good → Better
if good.features and better.features:
good_imp = sum(f.importance for f in good.features) / len(good.features)
better_imp = sum(f.importance for f in better.features) / len(better.features)
if better_imp < good_imp - 0.1:
flags.append(
"No clear upgrade trigger Good → Better: Better-tier features have lower avg importance than Good. "
"Why would a Good customer ever upgrade?"
)
# 6. Bronze tier as loss leader (cost-to-serve > effective price share)
if good.features and good.price > 0:
good_cost = sum(f.cost_to_serve for f in good.features)
if good_cost > good.price * 0.8:
flags.append(
f"Good tier near loss-leader: cost-to-serve ({good_cost:.1f}) > 80% of price ({good.price:.2f}). "
"Either raise the price floor or strip a feature down to Better."
)
# 7. Best tier "Enterprise — call us" with no anchor
if best.price == 0 and best.features:
flags.append(
"Best/Enterprise tier has no published anchor price. 'Call us' without a starting number "
"loses prospects to competitors who publish ranges."
)
return flags
def render_markdown(tiers: dict[str, Tier], flags: list[str], profile: str, segments: list[str]) -> str:
lines: list[str] = []
lines.append("# Packaging Recommendation: Good / Better / Best")
lines.append("")
lines.append(f"**Profile:** `{profile}` • **Segments:** {', '.join(segments) if segments else 'unspecified'}")
lines.append("")
for key in ("good", "better", "best"):
t = tiers[key]
lines.append(f"## {t.name} — ${t.price:,.2f}")
if t.features:
for f in t.features:
lines.append(f"- **{f.name}** (importance={f.importance:.2f}, cost-to-serve={f.cost_to_serve:.1f})")
else:
lines.append("- *(no features assigned)*")
lines.append("")
if flags:
lines.append("## Anti-pattern flags")
for f in flags:
lines.append(f"- {f}")
else:
lines.append("## Anti-pattern flags")
lines.append("- None detected.")
lines.append("")
lines.append("## Notes")
lines.append("- Prices are a **starting frame**, not the final number. Validate with Van Westendorp PSM.")
lines.append("- Re-run after every meaningful feature addition; tier balance drifts as the product grows.")
return "\n".join(lines)
def sample_input() -> dict[str, Any]:
return {
"segments": ["SMB", "Mid-market", "Enterprise"],
"current_pricing": {"good": 49, "better": 149, "best": 499},
"features": [
{"name": "Core dashboard", "importance": 0.95, "cost_to_serve": 0.5, "segment_fit": {"SMB": 1.0, "Mid-market": 1.0, "Enterprise": 1.0}},
{"name": "Basic reporting", "importance": 0.85, "cost_to_serve": 0.8, "segment_fit": {"SMB": 0.9, "Mid-market": 0.9, "Enterprise": 0.8}},
{"name": "API access", "importance": 0.6, "cost_to_serve": 1.5, "segment_fit": {"SMB": 0.3, "Mid-market": 0.7, "Enterprise": 0.9}},
{"name": "Advanced analytics", "importance": 0.65, "cost_to_serve": 2.0, "segment_fit": {"SMB": 0.2, "Mid-market": 0.8, "Enterprise": 0.9}},
{"name": "Custom workflows", "importance": 0.55, "cost_to_serve": 2.5, "segment_fit": {"SMB": 0.1, "Mid-market": 0.5, "Enterprise": 0.9}},
{"name": "SSO / SAML", "importance": 0.4, "cost_to_serve": 1.0, "segment_fit": {"SMB": 0.05, "Mid-market": 0.4, "Enterprise": 1.0}},
{"name": "SLA + dedicated CSM", "importance": 0.3, "cost_to_serve": 5.0, "segment_fit": {"SMB": 0.0, "Mid-market": 0.2, "Enterprise": 1.0}},
{"name": "On-prem deployment", "importance": 0.2, "cost_to_serve": 4.0, "segment_fit": {"SMB": 0.0, "Mid-market": 0.1, "Enterprise": 0.9}},
{"name": "Audit logs", "importance": 0.5, "cost_to_serve": 0.8, "segment_fit": {"SMB": 0.1, "Mid-market": 0.5, "Enterprise": 0.95}},
],
}
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--input", type=Path, help="Path to features JSON.")
p.add_argument("--profile", default="saas", choices=list(PROFILES.keys()), help="Industry profile.")
p.add_argument("--output", default="markdown", choices=["markdown", "json"], help="Output format.")
p.add_argument("--sample", action="store_true", help="Run with built-in sample data.")
args = p.parse_args(argv)
if args.sample:
data = sample_input()
elif args.input:
data = json.loads(args.input.read_text())
else:
p.error("Provide --input or --sample.")
return 2
segments = data.get("segments", [])
current_pricing = data.get("current_pricing", {})
features = [Feature.from_dict(f) for f in data.get("features", [])]
tiers = assign_tiers(features, segments, args.profile)
price_tiers(tiers, current_pricing, args.profile)
flags = detect_anti_patterns(tiers)
if args.output == "json":
out = {
"profile": args.profile,
"segments": segments,
"tiers": {
k: {
"name": t.name,
"price": t.price,
"features": [{"name": f.name, "importance": f.importance, "cost_to_serve": f.cost_to_serve} for f in t.features],
}
for k, t in tiers.items()
},
"anti_pattern_flags": flags,
}
print(json.dumps(out, indent=2))
else:
print(render_markdown(tiers, flags, args.profile, segments))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""pricing_model_picker.py — rank pricing models by fit-score for a given customer context.
Input: JSON describing customer context (industry, deal size, customer count, value drivers,
adoption curve, consumption pattern, competitor pricing models).
Output: ranked list of 5 pricing models (subscription seat-based, usage-based, value-based,
freemium, hybrid) with fit-score 0-100 and trade-offs.
Deterministic decision logic. No LLM calls. No third-party deps.
Usage:
pricing_model_picker.py --input brief.json --profile saas --output markdown
pricing_model_picker.py --sample
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
MODELS = [
"subscription_seat_based",
"usage_based",
"value_based",
"freemium",
"hybrid",
]
# Industry profile tuning — base biases (additive, capped at ±15)
PROFILES: dict[str, dict[str, int]] = {
"saas": {
"subscription_seat_based": 10,
"usage_based": 0,
"value_based": 0,
"freemium": 5,
"hybrid": 5,
},
"api": {
"subscription_seat_based": -10,
"usage_based": 15,
"value_based": 0,
"freemium": 5,
"hybrid": 5,
},
"ai-tools": {
"subscription_seat_based": -5,
"usage_based": 10,
"value_based": 5,
"freemium": 5,
"hybrid": 10,
},
"enterprise-software": {
"subscription_seat_based": 5,
"usage_based": -5,
"value_based": 10,
"freemium": -10,
"hybrid": 5,
},
"marketplace": {
"subscription_seat_based": -10,
"usage_based": 10,
"value_based": 10,
"freemium": 5,
"hybrid": 5,
},
}
@dataclass
class ModelScore:
model: str
score: int
rationale: list[str] = field(default_factory=list)
tradeoffs: list[str] = field(default_factory=list)
def clamp(n: int, lo: int = 0, hi: int = 100) -> int:
return max(lo, min(hi, n))
def score_models(ctx: dict[str, Any], profile: str) -> list[ModelScore]:
"""Deterministic per-model scoring. Each model starts at 50 and is adjusted by signals."""
cp = (ctx.get("consumption_pattern") or {})
deal_size = float(ctx.get("deal_size_avg") or 0)
customer_count = int(ctx.get("customer_count") or 0)
value_drivers = ctx.get("value_drivers") or []
adoption = (ctx.get("adoption_curve") or "").lower()
competitor_models = ctx.get("competitor_pricing_models") or []
seat_signal = float(cp.get("seat-based") or 0)
usage_signal = float(cp.get("usage-based") or 0)
value_signal = float(cp.get("value-based") or 0)
hybrid_signal = float(cp.get("hybrid") or 0)
scores = {m: ModelScore(model=m, score=50) for m in MODELS}
# --- Subscription seat-based ---
s = scores["subscription_seat_based"]
if seat_signal >= 0.6:
s.score += 20
s.rationale.append(f"Strong seat-based consumption signal ({seat_signal:.2f}).")
elif seat_signal >= 0.3:
s.score += 8
s.rationale.append(f"Moderate seat-based signal ({seat_signal:.2f}).")
if usage_signal > 0.5 and seat_signal < 0.4:
s.score -= 15
s.tradeoffs.append("Usage variance is high; seat licensing leaves money on the table.")
if deal_size > 0 and deal_size < 5000:
s.score += 5
s.rationale.append("SMB-friendly deal size — predictable seat math.")
if "subscription" in " ".join(competitor_models).lower():
s.score += 5
s.rationale.append("Competitors already train the market on subscription.")
s.tradeoffs.append("Predictable revenue, but customers feel friction when adding seats.")
# --- Usage-based ---
s = scores["usage_based"]
if usage_signal >= 0.6:
s.score += 25
s.rationale.append(f"Strong usage-variance signal ({usage_signal:.2f}) — power-law users.")
elif usage_signal >= 0.3:
s.score += 10
s.rationale.append(f"Moderate usage signal ({usage_signal:.2f}).")
if seat_signal > 0.6 and usage_signal < 0.3:
s.score -= 15
s.tradeoffs.append("Usage is flat per seat; usage-based adds billing complexity for no upside.")
if "api" in (ctx.get("industry") or "").lower() or profile == "api":
s.score += 8
s.rationale.append("API/infra products align naturally with usage metering.")
if "usage" in " ".join(competitor_models).lower() or "consumption" in " ".join(competitor_models).lower():
s.score += 5
s.rationale.append("Competitive usage pricing trains the market.")
s.tradeoffs.append("Aligned to value but introduces revenue unpredictability and bill-shock risk.")
# --- Value-based ---
s = scores["value_based"]
measurable = any(
kw in " ".join(value_drivers).lower()
for kw in ["revenue", "cost saved", "time saved", "conversion", "fraud prevented", "downtime"]
)
if value_signal >= 0.6 and measurable:
s.score += 25
s.rationale.append("Customer value is measurable AND signaled as primary.")
elif value_signal >= 0.4 and measurable:
s.score += 15
s.rationale.append("Value signal moderate, measurement plausible.")
elif value_signal >= 0.4 and not measurable:
s.score -= 10
s.tradeoffs.append("Value signal present but no measurable driver — collapses to bad usage pricing.")
if deal_size >= 50000:
s.score += 10
s.rationale.append("Enterprise deal size justifies bespoke value-pricing motion.")
if customer_count > 0 and customer_count < 50:
s.score += 5
s.rationale.append("Small customer count supports per-customer value calibration.")
if customer_count > 500:
s.score -= 10
s.tradeoffs.append("High customer count — value-based does not scale operationally.")
s.tradeoffs.append("Highest yield model but requires instrumented ROI proof per customer.")
# --- Freemium ---
s = scores["freemium"]
if adoption in ("viral", "bottom-up", "plg", "product-led"):
s.score += 20
s.rationale.append(f"Adoption curve '{adoption}' aligns with PLG/freemium funnel.")
if customer_count > 1000:
s.score += 10
s.rationale.append("Large addressable user base supports freemium economics.")
if deal_size > 25000:
s.score -= 15
s.tradeoffs.append("Enterprise ACV — freemium acquisition cost rarely amortizes.")
if adoption in ("top-down", "enterprise"):
s.score -= 15
s.tradeoffs.append("Top-down sale — freemium dilutes positioning without unlocking pipeline.")
s.tradeoffs.append("Powerful acquisition channel but free-tier cost-to-serve must be a small fraction of paid LTV.")
# --- Hybrid (platform + usage, or seat + overage) ---
s = scores["hybrid"]
if hybrid_signal >= 0.5:
s.score += 20
s.rationale.append(f"Hybrid signal explicit ({hybrid_signal:.2f}).")
if seat_signal >= 0.4 and usage_signal >= 0.4:
s.score += 15
s.rationale.append("Both seat AND usage drivers present — natural hybrid candidate.")
if len(value_drivers) >= 3:
s.score += 5
s.rationale.append("Multiple value drivers — single model leaves segments under-served.")
if deal_size < 1000:
s.score -= 10
s.tradeoffs.append("Small deal size — hybrid complexity is not worth the friction.")
s.tradeoffs.append("Captures more value across segments but increases pricing-page complexity and CS overhead.")
# Profile bias
bias = PROFILES.get(profile, {})
for m, b in bias.items():
if m in scores:
scores[m].score += b
if b != 0:
scores[m].rationale.append(f"Industry profile '{profile}' adjustment: {b:+d}.")
# Clamp
for s in scores.values():
s.score = clamp(s.score)
return sorted(scores.values(), key=lambda x: -x.score)
def render_markdown(ranked: list[ModelScore], ctx: dict[str, Any], profile: str) -> str:
lines: list[str] = []
lines.append("# Pricing Model Recommendation")
lines.append("")
lines.append(f"**Profile:** `{profile}` • **Industry:** {ctx.get('industry', 'unspecified')}")
lines.append(f"**Deal size avg:** {ctx.get('deal_size_avg', 'n/a')} • **Customers:** {ctx.get('customer_count', 'n/a')}")
lines.append("")
lines.append("> This skill recommends a **model and trade-offs**, not a final price. The human owns the decision.")
lines.append("")
lines.append("## Ranked models")
lines.append("")
for i, s in enumerate(ranked, 1):
marker = " *(top recommendation)*" if i == 1 else ""
lines.append(f"### {i}. {s.model.replace('_', ' ').title()} — fit-score **{s.score}/100**{marker}")
if s.rationale:
lines.append("**Why it fits:**")
for r in s.rationale:
lines.append(f"- {r}")
if s.tradeoffs:
lines.append("**Trade-offs:**")
for t in s.tradeoffs:
lines.append(f"- {t}")
lines.append("")
lines.append("## Next steps")
lines.append("1. Validate WTP for the top model with `wtp_analyzer.py` (≥ 30 respondents).")
lines.append("2. Design tiers with `packaging_designer.py`.")
lines.append("3. Pressure-test in pricing committee — this output is one input.")
return "\n".join(lines)
def sample_context() -> dict[str, Any]:
return {
"industry": "B2B SaaS — sales intelligence",
"deal_size_avg": 18000,
"customer_count": 220,
"value_drivers": ["revenue lift from better leads", "time saved in research", "conversion uplift"],
"adoption_curve": "bottom-up",
"consumption_pattern": {
"seat-based": 0.45,
"usage-based": 0.55,
"value-based": 0.40,
"hybrid": 0.50,
},
"competitor_pricing_models": ["subscription seat-based", "hybrid seat + usage overage"],
}
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--input", type=Path, help="Path to customer-context JSON.")
p.add_argument(
"--profile",
default="saas",
choices=list(PROFILES.keys()),
help="Industry profile for default tuning.",
)
p.add_argument("--output", default="markdown", choices=["markdown", "json"], help="Output format.")
p.add_argument("--sample", action="store_true", help="Run with built-in sample context.")
args = p.parse_args(argv)
if args.sample:
ctx = sample_context()
elif args.input:
ctx = json.loads(args.input.read_text())
else:
p.error("Provide --input or --sample.")
return 2
ranked = score_models(ctx, args.profile)
if args.output == "json":
out = {
"profile": args.profile,
"context": ctx,
"ranked": [
{"model": s.model, "score": s.score, "rationale": s.rationale, "tradeoffs": s.tradeoffs}
for s in ranked
],
}
print(json.dumps(out, indent=2))
else:
print(render_markdown(ranked, ctx, args.profile))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""wtp_analyzer.py — Van Westendorp Price Sensitivity Meter (PSM).
Implements the classical PSM analysis (van Westendorp, 1976). Each respondent answers
4 questions:
1. Too cheap price at which you'd doubt the quality
2. Bargain price that feels like a great deal
3. Getting expensive price where you'd start to hesitate
4. Too expensive price at which you would never buy
Computes the 4 intersection points:
- OPP (Optimal Price Point): intersection of "too cheap" and "too expensive"
- IDP (Indifference Price Point): intersection of "bargain" and "getting expensive"
- PMC (Point of Marginal Cheapness): intersection of "too cheap" and "getting expensive"
- PME (Point of Marginal Expensiveness): intersection of "bargain" and "too expensive"
Range of Acceptable Prices (RAP) = [PMC, PME].
Output: markdown or JSON. Stdlib only.
Usage:
wtp_analyzer.py --input survey.json --output markdown
wtp_analyzer.py --sample
"""
from __future__ import annotations
import argparse
import json
import math
import random
import statistics
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass
class Curves:
prices: list[float]
too_cheap: list[float] # P(too cheap >= price) — decreasing in price
bargain: list[float] # P(bargain >= price) — decreasing in price
getting_expensive: list[float] # P(getting expensive <= price) — increasing
too_expensive: list[float] # P(too expensive <= price) — increasing
def build_price_grid(respondents: list[dict[str, float]]) -> list[float]:
"""Build a sorted unique-price grid from all responses."""
prices: set[float] = set()
for r in respondents:
for k in ("too_cheap", "bargain", "getting_expensive", "too_expensive"):
v = r.get(k)
if v is not None:
prices.add(float(v))
grid = sorted(prices)
if not grid:
return []
# Densify with intermediate steps to make intersection detection stable.
densified: list[float] = []
for i, p in enumerate(grid):
densified.append(p)
if i + 1 < len(grid):
nxt = grid[i + 1]
mid = (p + nxt) / 2.0
if mid not in prices:
densified.append(mid)
return sorted(set(densified))
def cumulative_curves(respondents: list[dict[str, float]], grid: list[float]) -> Curves:
n = len(respondents)
too_cheap: list[float] = []
bargain: list[float] = []
getting_expensive: list[float] = []
too_expensive: list[float] = []
for p in grid:
tc = sum(1 for r in respondents if (r.get("too_cheap") is not None) and float(r["too_cheap"]) >= p)
bg = sum(1 for r in respondents if (r.get("bargain") is not None) and float(r["bargain"]) >= p)
ge = sum(1 for r in respondents if (r.get("getting_expensive") is not None) and float(r["getting_expensive"]) <= p)
te = sum(1 for r in respondents if (r.get("too_expensive") is not None) and float(r["too_expensive"]) <= p)
too_cheap.append(tc / n)
bargain.append(bg / n)
getting_expensive.append(ge / n)
too_expensive.append(te / n)
return Curves(grid, too_cheap, bargain, getting_expensive, too_expensive)
def find_intersection(prices: list[float], a: list[float], b: list[float]) -> float | None:
"""Find first price where curve a crosses curve b (linear interp between grid points)."""
if len(prices) < 2:
return None
prev_diff = a[0] - b[0]
for i in range(1, len(prices)):
diff = a[i] - b[i]
if prev_diff == 0:
return prices[i - 1]
if (prev_diff < 0 < diff) or (prev_diff > 0 > diff):
# Linear interpolation
p0, p1 = prices[i - 1], prices[i]
t = prev_diff / (prev_diff - diff)
return p0 + t * (p1 - p0)
prev_diff = diff
return None
@dataclass
class PSMResult:
n: int
opp: float | None
idp: float | None
pmc: float | None
pme: float | None
rap_low: float | None
rap_high: float | None
warnings: list[str]
def analyze(respondents: list[dict[str, float]]) -> PSMResult:
warnings: list[str] = []
n = len(respondents)
if n < 30:
warnings.append(
f"Sample size N={n} is below 30. PSM results are directional only; "
"treat the range as a hypothesis, not a recommendation. Aim for N≥100."
)
elif n < 100:
warnings.append(f"Sample size N={n} is acceptable but below the preferred N≥100 threshold.")
# Sanity-check monotonicity (too_cheap < bargain < getting_expensive < too_expensive per respondent)
inconsistent = 0
for r in respondents:
try:
tc = float(r["too_cheap"])
bg = float(r["bargain"])
ge = float(r["getting_expensive"])
te = float(r["too_expensive"])
except (KeyError, TypeError, ValueError):
inconsistent += 1
continue
if not (tc <= bg <= ge <= te):
inconsistent += 1
if inconsistent:
warnings.append(
f"{inconsistent} of {n} respondents have inconsistent price ordering "
"(expected too_cheap ≤ bargain ≤ getting_expensive ≤ too_expensive). "
"Consider screening these before reporting."
)
grid = build_price_grid(respondents)
if not grid:
return PSMResult(n=n, opp=None, idp=None, pmc=None, pme=None, rap_low=None, rap_high=None, warnings=warnings)
c = cumulative_curves(respondents, grid)
opp = find_intersection(c.prices, c.too_cheap, c.too_expensive)
idp = find_intersection(c.prices, c.bargain, c.getting_expensive)
pmc = find_intersection(c.prices, c.too_cheap, c.getting_expensive)
pme = find_intersection(c.prices, c.bargain, c.too_expensive)
return PSMResult(n=n, opp=opp, idp=idp, pmc=pmc, pme=pme, rap_low=pmc, rap_high=pme, warnings=warnings)
def _fmt(v: float | None) -> str:
return f"{v:,.2f}" if v is not None else "n/a"
def render_markdown(res: PSMResult) -> str:
lines: list[str] = []
lines.append("# Van Westendorp PSM Analysis")
lines.append("")
lines.append(f"**Respondents:** N = {res.n}")
lines.append("")
if res.warnings:
lines.append("> **Warnings:**")
for w in res.warnings:
lines.append(f"> - {w}")
lines.append("")
lines.append("## Four intersection points")
lines.append("")
lines.append("| Point | Definition | Value |")
lines.append("|---|---|---|")
lines.append(f"| **OPP** — Optimal Price Point | too cheap ↔ too expensive | {_fmt(res.opp)} |")
lines.append(f"| **IDP** — Indifference Price Point | bargain ↔ getting expensive | {_fmt(res.idp)} |")
lines.append(f"| **PMC** — Point of Marginal Cheapness | too cheap ↔ getting expensive | {_fmt(res.pmc)} |")
lines.append(f"| **PME** — Point of Marginal Expensiveness | bargain ↔ too expensive | {_fmt(res.pme)} |")
lines.append("")
lines.append("## Range of Acceptable Prices (RAP)")
lines.append("")
if res.rap_low is not None and res.rap_high is not None:
lines.append(f"**RAP = [{_fmt(res.rap_low)}, {_fmt(res.rap_high)}]**")
lines.append("")
lines.append("Prices outside this range are likely to be rejected as either too cheap (quality doubt) or too expensive (no purchase).")
else:
lines.append("RAP could not be computed — check input data and sample size.")
lines.append("")
lines.append("## Interpretation guidance")
lines.append("")
lines.append("- PSM gives a **range**, not the price. Final price is a commercial decision.")
lines.append("- OPP is a theoretical mid-point — the price at which equal % of respondents reject as too cheap and too expensive.")
lines.append("- IDP is the median respondent's perceived 'fair' price.")
lines.append("- Re-run with segmented samples (ICP vs non-ICP) — overall PSM averages across segments hide structure.")
lines.append("- Validate the upper end with willingness-to-pay experiments in market before anchoring at PME.")
return "\n".join(lines)
def synthetic_sample(n: int = 50, seed: int = 17) -> list[dict[str, float]]:
"""Generate N synthetic respondents with realistic price ordering and segmentation noise."""
rng = random.Random(seed)
respondents: list[dict[str, float]] = []
for _ in range(n):
anchor = rng.gauss(80, 20) # respondent's reference price
anchor = max(20.0, anchor)
tc = max(5.0, anchor * rng.uniform(0.3, 0.5))
bg = anchor * rng.uniform(0.6, 0.85)
ge = anchor * rng.uniform(0.95, 1.15)
te = anchor * rng.uniform(1.3, 1.8)
respondents.append({
"too_cheap": round(tc, 2),
"bargain": round(bg, 2),
"getting_expensive": round(ge, 2),
"too_expensive": round(te, 2),
})
return respondents
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--input", type=Path, help="Path to survey JSON: {respondents: [{too_cheap, bargain, getting_expensive, too_expensive}]}.")
p.add_argument("--output", default="markdown", choices=["markdown", "json"], help="Output format.")
p.add_argument("--sample", action="store_true", help="Run with synthetic 50-respondent sample.")
args = p.parse_args(argv)
if args.sample:
respondents = synthetic_sample(50)
elif args.input:
data = json.loads(args.input.read_text())
respondents = data.get("respondents", data) if isinstance(data, dict) else data
else:
p.error("Provide --input or --sample.")
return 2
if not isinstance(respondents, list) or not respondents:
print("ERROR: respondents must be a non-empty list.", file=sys.stderr)
return 1
res = analyze(respondents)
if args.output == "json":
out = {
"n": res.n,
"opp": res.opp,
"idp": res.idp,
"pmc": res.pmc,
"pme": res.pme,
"rap": [res.rap_low, res.rap_high],
"warnings": res.warnings,
}
print(json.dumps(out, indent=2))
else:
print(render_markdown(res))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,285 @@
# Business Operations + Commercial Domain Expansion — Master Plan
**Status:** Draft v1.0 — proposed under branch `claude/skills-plugins-framework-XjTjh`
**Date:** 2026-05-19
**Author:** Claude (claude-opus-4-7) under `/goal` directive
---
## 1. Why two new top-level domains
The repository already ships skills for:
- `business-growth/` (5 skills) — **sales & customer success motion** (CSM, sales engineering, RevOps, contracts, BizDev toolkit)
- `c-level-advisor/` (33 skills) — **strategic executive judgment** (CEO/CTO/CFO advisors, board prep, M&A)
- `finance/` (3 skills) — **financial analysis & forecasting**
- `project-management/` (9 skills) — **delivery coordination** (Jira/Confluence)
What is **missing** is the operational and commercial *muscle* between strategy (C-level) and execution (engineering / delivery). Two distinct surfaces:
| Domain | Job-to-be-done | Persona | Cadence |
|---|---|---|---|
| **business-operations** | Make the company *run* — processes, vendors, capacity, internal comms, SOPs | COO / BizOps lead / Head of Operations | Daily / weekly |
| **commercial** | Make every deal *profitable & repeatable* — pricing, deal desk, partners, channels, RFPs | CRO / Head of Commercial / Deal Desk | Per-deal / monthly |
These are **professional disciplines**, each with their own canon (Lean / Six Sigma / SaaSOps for BizOps; SaaS pricing canon, deal-desk playbooks, channel economics for Commercial). They deserve their own top-level domain folders, plugins, and orchestrators — same pattern as `productivity/`, `marketing/`, `research/` introduced in v2.7.0.
---
## 2. Domain shape
Each new domain ships as a **top-level folder** with the v2.7.0 Path-B 11-file layout per skill, plus a domain **orchestrator** skill that uses `context: fork` to chain sub-skills without polluting main context.
```
business-operations/
├── .claude-plugin/
│ └── plugin.json # marketplace registration
├── CLAUDE.md # navigation guide for the domain
├── README.md
├── skills/
│ ├── business-operations-skills/ # domain index skill (orchestrator)
│ │ └── SKILL.md # context: fork — routes to sub-skills
│ ├── process-mapper/ # full Path-B skill
│ ├── vendor-management/
│ ├── capacity-planner/
│ ├── internal-comms/
│ ├── knowledge-ops/
│ └── procurement-optimizer/
├── agents/
│ └── cs-bizops-orchestrator.md
└── commands/
├── cs-bizops.md # top-level router
├── cs-process-map.md
├── cs-vendor-review.md
├── cs-capacity-plan.md
├── cs-internal-comms.md
├── cs-knowledge-ops.md
└── cs-procurement.md
commercial/
├── .claude-plugin/
│ └── plugin.json
├── CLAUDE.md
├── README.md
├── skills/
│ ├── commercial-skills/ # domain orchestrator (context: fork)
│ ├── pricing-strategist/
│ ├── deal-desk/
│ ├── partnerships-architect/
│ ├── channel-economics/
│ ├── commercial-policy/
│ ├── rfp-responder/
│ └── commercial-forecaster/
├── agents/
│ └── cs-commercial-orchestrator.md
└── commands/
└── (matching /cs:* commands)
```
---
## 3. The `context: fork` chaining pattern
`context: fork` is a frontmatter directive already used in `engineering/karpathy-coder` and `engineering/llm-wiki`. Semantics: when this skill is invoked, the agent **forks its conversation context** rather than continuing inline, so the skill can run heavy sub-operations (multi-skill orchestration, large file reads, deep reference loads) without polluting the parent thread. Two consequences relevant to our design:
1. **Orchestrators must be fork-marked.** Each domain orchestrator skill (`business-operations-skills`, `commercial-skills`) sets `context: fork` so it can sequentially invoke 3-7 sub-skills in a clean child context, then return a digest to the parent.
2. **Heavy sub-skills should also opt in.** Sub-skills that ingest large external artifacts (e.g., a vendor catalog, an RFP PDF, a competitor pricing scrape) should set `context: fork` so the noisy intake stays out of the main session.
| Skill | `context: fork`? | Reason |
|---|---|---|
| `business-operations-skills` (orchestrator) | YES | Chains multiple BizOps sub-skills |
| `commercial-skills` (orchestrator) | YES | Chains multiple Commercial sub-skills |
| `process-mapper` | YES | Ingests process docs / interview transcripts |
| `vendor-management` | YES | Ingests vendor catalog + contracts |
| `rfp-responder` | YES | Ingests multi-page RFP/RFI documents |
| `knowledge-ops` | YES | Multi-document SOP ingestion |
| `pricing-strategist` | NO | Decision-tool, lightweight inputs |
| `deal-desk` | NO | Per-deal scoring, lightweight |
| `commercial-policy` | NO | Authoring tool |
| `partnerships-architect` | NO | Authoring tool |
| `channel-economics` | NO | Calculator |
| `commercial-forecaster` | NO | Calculator |
| `capacity-planner` | NO | Calculator |
| `internal-comms` | NO | Authoring tool |
| `procurement-optimizer` | NO | Calculator |
---
## 4. Per-skill spec (v0 scope)
Each skill ships the **standard Path-B 11-file contract**:
```
skill-name/
├── SKILL.md # YAML frontmatter + workflow
├── scripts/
│ ├── tool_one.py # stdlib-only CLI, --help + --sample
│ ├── tool_two.py
│ └── tool_three.py
├── references/
│ ├── primary_canon.md # ≥ 7 authoritative sources cited
│ ├── decision_framework.md
│ └── anti_patterns.md
└── assets/
└── template.md # user-customizable starter
```
### 4.1 business-operations skills
| Skill | Purpose | 3 Python tools | Distinct from |
|---|---|---|---|
| **process-mapper** | BPMN-style process documentation + bottleneck + cycle-time analysis | `process_documenter.py`, `bottleneck_detector.py` (waiting-state %), `cycle_time_analyzer.py` | `engineering/slo-architect` (system reliability, not business process) |
| **vendor-management** | Vendor evaluation, SLA tracking, contract-risk scan | `vendor_scorer.py` (multi-criteria 0-100), `sla_compliance_tracker.py`, `vendor_risk_classifier.py` | `c-level-advisor/general-counsel-advisor` (contracts are legal-first, not operational) |
| **capacity-planner** | Headcount + tooling capacity modeling | `capacity_modeler.py`, `utilization_analyzer.py`, `hiring_sequencer.py` | `c-level-advisor/vpe-advisor` (engineering-specific, not org-wide) |
| **internal-comms** | All-hands deck, internal newsletter, change-management comms | `comms_template_filler.py`, `change_announcement_builder.py`, `comms_calendar_builder.py` | `marketing-skill/*` (external-facing) |
| **knowledge-ops** | SOP authoring, runbook templating, internal knowledge base ingestion | `sop_generator.py`, `runbook_validator.py`, `kb_ingester.py` | `engineering/llm-wiki` (personal PKM, not company SOPs) |
| **procurement-optimizer** | Spend categorization, purchasing cycle, supplier-tier rationalization | `spend_categorizer.py`, `purchasing_cycle_analyzer.py`, `supplier_consolidation.py` | `finance/*` (financial reporting, not procurement decisions) |
### 4.2 commercial skills
| Skill | Purpose | 3 Python tools | Distinct from |
|---|---|---|---|
| **pricing-strategist** | Pricing model selection (subscription / usage / value / hybrid), WTP analysis, packaging | `pricing_model_picker.py`, `wtp_analyzer.py` (Van Westendorp), `packaging_designer.py` | `c-level-advisor/cmo-advisor` (positioning, not pricing math) |
| **deal-desk** | Per-deal review: discount logic, T&Cs, margin guardrails | `deal_scorer.py` (margin + risk 0-100), `discount_approval_router.py`, `terms_redliner.py` | `business-growth/contract-and-proposal-writer` (authoring, not approval) |
| **partnerships-architect** | Partner tier model, joint GTM, revenue share economics | `partner_tier_classifier.py`, `joint_gtm_planner.py`, `revshare_modeler.py` | `business-growth/sales-engineer` (technical sale, not partnership structure) |
| **channel-economics** | Direct vs. partner-led economics, cost-to-serve, channel mix | `channel_mix_optimizer.py`, `cost_to_serve_calculator.py`, `channel_roi_analyzer.py` | `business-growth/revenue-operations` (process, not economics) |
| **commercial-policy** | Discount matrix, T&C library, exception policy | `discount_matrix_builder.py`, `exception_router.py`, `policy_linter.py` | none — new ground |
| **rfp-responder** | RFP/RFI/PFP structured response, win-theme injection | `rfp_parser.py`, `response_drafter.py`, `winrate_predictor.py` | `business-growth/contract-and-proposal-writer` (proposals ≠ RFPs; RFPs are structured response, proposals are free-form) |
| **commercial-forecaster** | Bookings / billings / ARR forecast with funnel + cohort math | `bookings_forecaster.py`, `cohort_arr_projector.py`, `funnel_confidence_scorer.py` | `finance/financial-analysis` (financial close, not commercial pipeline) |
---
## 5. Adoptability + customizability principles
Every skill MUST:
1. **Stdlib-only Python tools**`pip install`-free; `--help` + `--sample` work out of the box.
2. **Deterministic logic, no LLM calls in scripts** — same input → same output, repeatable.
3. **Industry-tunable thresholds** — every scoring tool exposes a `--profile {saas|services|ecommerce|enterprise}` flag (mirrors AEO skill pattern) so users tune calibration to their context without editing code.
4. **Asset templates** — at least one user-customizable `.md` template per skill that users fill in for their org.
5. **Reference docs cite ≥ 7 authoritative sources** — Lean canon, SaaS pricing canon (Tunguz/Skok/Bessemer), DORA, Will Larson, Camille Fournier, etc.
6. **Cited anti-patterns** — every skill ships an `anti_patterns.md` with at least 5 specific things NOT to do, sourced.
7. **Karpathy-coder compliance** — every tool passes `complexity_checker.py` (target ≥ 80/100); explicit assumptions surfaced in SKILL.md `## Assumptions` block; verifiable success criteria locked before implementation.
8. **`distinct_from` field** in plugin.json `source` block — explicit disambiguation from sibling skills in `business-growth/`, `c-level-advisor/`, `finance/`, `marketing-skill/`.
---
## 6. Agents (cs-* persona)
One distinct cs-* persona agent per domain, plus one per high-stakes sub-skill (deal-desk, pricing-strategist):
| Agent | Persona voice | Forcing question (signature) |
|---|---|---|
| `cs-bizops-orchestrator` | Process-obsessed COO. "Where does the work spend most of its time waiting?" | Routes to BizOps sub-skills based on inquiry shape |
| `cs-commercial-orchestrator` | Margin-protective CRO. "What's the margin on this deal at full discount?" | Routes to Commercial sub-skills |
| `cs-pricing-strategist` | Value-pricing zealot. "What's your customer paying for, in their words?" | Distinct from CMO advisor — pricing math, not positioning |
| `cs-deal-desk` | Margin gate. "If this deal closes at this discount, what does next quarter's pipeline look like at the same terms?" | Distinct from contract-writer — approval gate, not authoring |
---
## 7. Slash commands
Each skill ships a `/cs:*` command for direct invocation. The two orchestrator commands route by intent:
```
/cs:bizops <inquiry> # auto-routes: process / vendor / capacity / comms / SOP / procurement
/cs:commercial <inquiry> # auto-routes: pricing / deal / partner / channel / policy / RFP / forecast
```
Plus 13 per-skill commands (one per non-orchestrator skill).
---
## 8. Plugin registration
Two new entries in `.claude-plugin/marketplace.json`:
```json
{
"name": "business-operations-skills",
"source": "./business-operations",
"description": "6 BizOps skills: process mapping (BPMN + bottleneck), vendor management (SLA + risk), capacity planning, internal comms, knowledge ops (SOPs/runbooks), procurement optimization. 18 stdlib Python tools, 24 references. Orchestrator skill chains sub-skills via context: fork.",
"version": "2.8.0",
"category": "operations"
},
{
"name": "commercial-skills",
"source": "./commercial",
"description": "7 Commercial skills: pricing strategy (Van Westendorp + packaging), deal desk (margin + discount routing), partnerships, channel economics, commercial policy, RFP responder, commercial forecaster. 21 stdlib Python tools, 28 references. Orchestrator skill chains sub-skills via context: fork.",
"version": "2.8.0",
"category": "commercial"
}
```
Marketplace plugins go **55 → 57**. Indexed skills go **313 → 328** (+13 sub-skills + 2 orchestrators).
---
## 9. Build sequence
### Sprint 1 (this PR) — foundation
1. Master plan doc ✓
2. Directory scaffolding (both domains)
3. Both orchestrator skills (`business-operations-skills`, `commercial-skills`) **fully wired** with `context: fork`
4. Two priority sub-skills per domain **fully wired**:
- bizops: `process-mapper`, `vendor-management`
- commercial: `pricing-strategist`, `deal-desk`
5. Both `cs-*-orchestrator` agents
6. Two `/cs:bizops` and `/cs:commercial` orchestrator commands + four per-skill commands
7. Marketplace registration for both new plugins
8. Draft PR opened
### Sprint 2 — fill out
9. Remaining 4 bizops sub-skills: capacity-planner, internal-comms, knowledge-ops, procurement-optimizer
10. Remaining 5 commercial sub-skills: partnerships-architect, channel-economics, commercial-policy, rfp-responder, commercial-forecaster
11. Per-skill cs-* sub-agents (cs-pricing-strategist, cs-deal-desk)
12. Full slash-command coverage
### Sprint 3 — polish
13. CLAUDE.md per domain
14. README.md per domain
15. Bulk plugin-audit pass; karpathy-check on all tools
16. Update root CLAUDE.md (`Project Purpose`, `Architecture Overview`, `Current Version`)
17. Release notes for v2.8.0
---
## 10. Success criteria (verifiable)
- [ ] 2 new top-level domain folders created with full Path-B layout
- [ ] 2 orchestrator skills with `context: fork` declared
- [ ] At least 4 sub-skills fully wired (3 stdlib tools + 3 references + 1 asset template each)
- [ ] Every Python tool passes `--help` and `--sample` smoke test
- [ ] Both plugins registered in `.claude-plugin/marketplace.json`
- [ ] At least 2 cs-* agents added (one per domain orchestrator)
- [ ] At least 6 `/cs:*` slash commands added (2 orchestrator + 4 per-skill)
- [ ] Sprint 1 PR opened as draft against `claude/skills-plugins-framework-XjTjh`
- [ ] No skill duplicates a sibling in `business-growth/`, `c-level-advisor/`, `finance/`, `marketing-skill/``distinct_from` field explicitly documents the boundary
---
## 11. Anti-patterns to avoid (project-level)
- ❌ Creating skills that overlap with existing `business-growth/` sales motion → BizOps is **internal operations**, not external sales
- ❌ Making orchestrator skills do the work — they **route**, then return digest
- ❌ Pricing skill that picks a number — pricing-strategist picks **a model and a range**, the user picks the number
- ❌ Deal-desk skill that "approves" deals — it **scores + routes to a human approver**, never auto-approves
- ❌ Reference docs that don't cite sources — every reference needs ≥ 7 authoritative citations
- ❌ Python tools with external dependencies (`requests`, `pandas`) — stdlib only
- ❌ Tools that hide their thresholds — every score must expose `--profile` flag for industry tuning
---
## 12. Open questions (for future iteration, not blockers)
1. Should `commercial-forecaster` cross-link to `finance/financial-analysis` via `context: fork`? Probably yes in v2.8.1.
2. Should `rfp-responder` be merged with `business-growth/contract-and-proposal-writer`? Decision: **no** — RFPs are structured response with mandatory sections + scoring, proposals are free-form persuasion. Different muscles.
3. Should there be a separate `legal-ops/` domain or do `vendor-management` + `commercial-policy` cover the legal-ops surface enough? Decision: defer; cover via existing `c-level-advisor/general-counsel-advisor` for now.
---
**Plan version:** 1.0
**Next step:** Sprint 1 execution under this branch.