diff --git a/docs/research/2026-06-21-pm-craft-corpus.json b/docs/research/2026-06-21-pm-craft-corpus.json new file mode 100644 index 0000000..96b0143 --- /dev/null +++ b/docs/research/2026-06-21-pm-craft-corpus.json @@ -0,0 +1,349 @@ +{ + "summary": "Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report.", + "agentCount": 103, + "logs": [ + "Q: How do strong product managers make high-quality product decisions, and what sep…", + "Decomposed into 5 angles: Pillar 1 - Customer/problem grounding & discovery, Pillar 2 - Prioritization frameworks & scope, Pillar 3 - Decision-making under uncertainty & conviction, Failure modes & anti-patterns of weak PMs, Mental models, product sense & frameworks of the best PMs", + "Pillar 1 - Customer/problem grounding & discovery: 6 results", + "Pillar 2 - Prioritization frameworks & scope: 6 results", + "Pillar 3 - Decision-making under uncertainty & conviction: 6 results", + "Failure modes & anti-patterns of weak PMs: 6 results", + "Mental models, product sense & frameworks of the best PMs: 6 results", + "Pillar 3 - Decision-making under uncertainty & conviction: 3 novel (3 filtered)", + "Failure modes & anti-patterns of weak PMs: 4 novel (2 filtered)", + "Mental models, product sense & frameworks of the best PMs: 2 novel (4 filtered)", + "Fetched 21 sources → 94 claims → verifying top 25", + "\"An opportunity solution tree is a visual framework…\": 3-0 ✓", + "\"Visualizing the discovery/opportunity space preven…\": 3-0 ✓", + "\"A concrete test distinguishes a real opportunity f…\": 3-0 ✓", + "\"Cagan defines exactly four product risks that stro…\": 3-0 ✓", + "\"Responsibility for the four risks is split across …\": 3-0 ✓", + "\"Continuous discovery requires engaging with custom…\": 3-0 ✓", + "\"PMs must not ask customers directly what they want…\": 3-0 ✓", + "\"Cagan argues weak product managers systematically …\": 3-0 ✓", + "\"Product discovery is fundamentally a decision-maki…\": 3-0 ✓", + "\"The RICE prioritization score is computed as (Reac…\": 3-0 ✓", + "\"RICE stands for four distinct scoring factors - Re…\": 3-0 ✓", + "\"RICE was created to counteract specific prioritiza…\": 3-0 ✓", + "\"Prioritization is fundamentally an act of saying n…\": 3-0 ✓", + "\"Product strategy (not the roadmap) is the mechanis…\": 3-0 ✓", + "\"Strong product companies decide what to build by p…\": 3-0 ✓", + "\"A defining anti-pattern of weak (feature-team) pro…\": 3-0 ✓", + "\"A JTBD 'job' is defined as the progress a person w…\": 3-0 ✓", + "\"Marty Cagan's core teaching method for product man…\": 3-0 ✓", + "\"The source provides an explicit, falsifiable check…\": 3-0 ✓", + "\"Jobs to Be Done reframes product decisions around …\": 3-0 ✓", + "\"Direct customer feedback questions like 'how can w…\": 3-0 ✓", + "\"Christensen's Four Forces of Progress is a codifia…\": 3-0 ✓", + "\"JTBD distinguishes a real 'job' from an assumption…\": 1-2 ✗", + "\"The LNO framework is a task-prioritization and ant…\": 3-0 ✓", + "\"Shreyas Doshi argues that most execution problems …\": 3-0 ✓", + "Verify done: 25 claims → 24 confirmed, 1 killed" + ], + "result": { + "question": "How do strong product managers make high-quality product decisions, and what separates good PMs from bad ones? Focus on three pillars: (1) Customer/problem grounding - product discovery, jobs-to-be-done, customer behaviour analysis, user research, distinguishing real needs from assumptions, validation before building; (2) Prioritization and scope - frameworks (RICE, opportunity sizing, Kano, MoSCoW, ICE), MVP definition, scope-cutting, saying no, sequencing/roadmapping; (3) Decision-making and conviction - making opinionated defensible calls under uncertainty, structuring decisions with clear rationale and evidence, product strategy, trade-off reasoning, avoiding generic/hedged decisions. Also cover: the mental models and frameworks the best PMs use, common failure modes and anti-patterns of weak PMs, and how 'product sense' is actually developed. Prioritize authoritative sources (Marty Cagan / SVPG / INSPIRED / EMPOWERED, Lenny Rachitsky, Teresa Torres / continuous discovery, Shreyas Doshi, John Cutler, Melissa Perri, Reforge, Intercom, a16z, classic PM texts). The output will become the ground-truth corpus for a 'product manager' persona/enforcement layer in an AI coding agent system, so emphasize concrete, codifiable frameworks, decision rules, checklists, and red-flag anti-patterns over generic advice.", + "summary": "Strong product managers make high-quality decisions by grounding every build choice in validated customer problems, ruthlessly prioritizing a focused set of problems over a long feature list, and making opinionated, evidence-backed calls that explicitly address the four product risks (value, usability, feasibility, viability). The corpus converges on a coherent, codifiable system: discovery is continuous evidence-gathering (weekly customer touchpoints, story-based interviews that never ask customers what they want), prioritization is fundamentally an act of saying no to good ideas mapped against a desired outcome (opportunity solution trees, RICE), and decision-making is principles-first rather than technique-copying. The single sharpest line between good and bad PMs is reactivity vs. vision-driven focus: weak PMs over-index on customer value while avoiding business viability, react to the loudest sales call or support ticket, and ship as many stakeholder features as possible (which Cagan labels \"not a strategy at all\"). Product sense is developed by mastering enduring principles so the PM can judge when each technique applies, and by diagnosing root causes (most \"execution problems\" are really strategy, interpersonal, or culture problems) rather than applying band-aids. The strongest, most enforceable rules in this corpus come from primary sources - Marty Cagan/SVPG, Teresa Torres/Product Talk, and Intercom (RICE's originator) - and are unanimous and current as of 2026.", + "findings": [ + { + "claim": "Customer/problem grounding is continuous and evidence-based, not project-based: good discovery teams engage customers AT LEAST WEEKLY so few build decisions are made without customer input, and discovery is fundamentally a decision-making process that includes the customer throughout (not just at the start). Codifiable cadence rule for the persona: enforce a weekly customer-contact touchpoint and flag any build decision lacking customer evidence.", + "confidence": "high", + "sources": [ + "https://www.producttalk.org/2021/08/product-discovery/" + ], + "evidence": "Primary source (Teresa Torres, originator of continuous discovery). Verbatim: 'Good product discovery teams engage with customers at least weekly, minimizing the number of decisions they make without customer input' and 'Product discovery is a decision-making process. Good product discovery includes the customer throughout that process.' Corroborated by IxDF, Product School, Maze, Mind the Product, Headway. Nuance: 'weekly' is an aspirational best-practice benchmark, not an absolute no-exceptions rule.", + "vote": "3-0 (claims 6, 8)" + }, + { + "claim": "PMs must NOT ask customers directly what they want or need - cognitive biases make opinion-based answers unreliable. Instead, elicit needs, pain points, and desires through specific past-behavior customer STORIES. This is the core story-based-interview rule for distinguishing real needs from assumptions, and a directly enforceable red flag: any requirement justified by 'a customer said they want X' (opinion/hypothetical) rather than an observed story is suspect.", + "confidence": "high", + "sources": [ + "https://www.producttalk.org/2021/08/product-discovery/", + "https://www.fullstory.com/blog/clayton-christensen-jobs-to-be-done-framework-product-development/" + ], + "evidence": "Primary (Torres) verbatim: 'We can't simply ask customers what they need or want. Cognitive biases interfere with our customers ability to answer these questions reliably' followed by 'listen for needs, pain points, and desires in the context of specific stories.' Independently reinforced by Rob Fitzpatrick's 'The Mom Test' (avoid opinions/hypotheticals) and the JTBD milkshake case: direct feedback ('how can we improve?') surfaced superficial taste/feature requests, but the real job was occupying a long boring commute - 'they had a long and boring drive to work and they just needed something to do.' Demographics fail to predict intent.", + "vote": "3-0 (claims 7, 21)" + }, + { + "claim": "The Opportunity Solution Tree is the canonical map from business value to validated solutions: a four-layer hierarchy of (1) desired business OUTCOME at the root, (2) OPPORTUNITY SPACE (customer needs/pains/desires), (3) SOLUTION SPACE, (4) ASSUMPTION TESTS at the bottom. Visualizing the full opportunity space counteracts reactive, anecdote-driven decisions where teams overweight the most recent interview, sales call, or support ticket - letting PMs compare and contrast opportunities instead.", + "confidence": "high", + "sources": [ + "https://www.producttalk.org/opportunity-solution-trees/" + ], + "evidence": "Primary source (Teresa Torres, framework originator). All four layers confirmed verbatim: root = 'desired outcome - the business need that reflects how your team can create business value'; opportunity space = 'customer needs, pain points, and desires'; solution space = 'solutions we are exploring'; assumption tests = 'how we will evaluate which solutions.' Anti-recency mechanism verbatim: 'It is common for product teams to overreact to the most recent customer interview, sales conversation, or support ticket... we are better able to compare and contrast the value of addressing different opportunities.' Corroborated by Shortform, Product School, Amplitude, Maze, Chameleon.", + "vote": "3-0 (claims 0, 1)" + }, + { + "claim": "A concrete, codifiable test separates a real opportunity from a solution disguised as one: ask 'Is there more than one way to address this?' If only ONE way exists, it is a solution, not an opportunity. This is a directly enforceable red-flag check against premature solution-jumping (e.g. 'I want to go out to eat' is a solution; the underlying 'I don't have time to cook' is the opportunity, satisfiable by takeout, meal-prep, restaurants, etc.).", + "confidence": "high", + "sources": [ + "https://www.producttalk.org/opportunity-solution-trees/" + ], + "evidence": "Primary source verbatim: 'The best way to test if an opportunity is really a solution in disguise is to ask, Is there more than one way to address this opportunity?' Illustrated with the cook/eat example. Corroborated by multiple summaries of Torres's 'Continuous Discovery Habits.' A clean yes/no rule fit for an enforcement layer.", + "vote": "3-0 (claim 2)" + }, + { + "claim": "Strong teams must address EXACTLY FOUR product risks before building: VALUE (will customers buy / users choose to use it), USABILITY (can users figure out how to use it), FEASIBILITY (can engineers build it with available time/skills/tech), and BUSINESS VIABILITY (does it work for the various parts of the business - legal, sales, finance, marketing, etc.). Cagan deliberately resists a fifth to keep it 'a way of thinking, not a checklist.' This is the master decision-quality checklist for evaluating any product idea.", + "confidence": "high", + "sources": [ + "https://www.svpg.com/four-big-risks/" + ], + "evidence": "Primary (Marty Cagan / SVPG, INSPIRED 2nd ed.) verbatim enumerates all four with definitions matching the claim near word-for-word. The 'exactly four' framing defended in Cagan's 2023 'Product Risk Taxonomy': 'once you get to more than 4, the real worry is that it just becomes a checklist and not a way of thinking.' Corroborated by roadmap.one, Viget, Medium. Foundational and current.", + "vote": "3-0 (claim 3)" + }, + { + "claim": "Risk ownership is split across the product trio by role: the Product Manager owns VALUE and VIABILITY risk and is accountable for outcomes; the Product Designer owns USABILITY risk and is accountable for the experience; the Lead Engineer owns FEASIBILITY risk and is accountable for delivery. For a PM persona, this scopes exactly what the PM is on the hook for: is it valuable and is it viable for the business.", + "confidence": "high", + "sources": [ + "https://www.svpg.com/four-big-risks/" + ], + "evidence": "Primary (SVPG) verbatim: 'The Product Manager is responsible for the value and viability risks, and overall accountable for the products outcomes. The Product Designer is responsible for the usability risk... The Product Lead Engineer is responsible for the feasibility risk.' Corroborated by Cagan's 'Empowered Product Teams' doctrine. 'Trio' is community shorthand for the three-role structure.", + "vote": "3-0 (claim 4)" + }, + { + "claim": "A NAMED FAILURE MODE of weak PMs: they systematically over-focus on customer value and under-appreciate, under-estimate, or simply AVOID business viability risk. The four-risks taxonomy exists specifically to correct this gap. Enforcement implication: when a PM's reasoning covers desirability/value but is silent on whether the business can actually sell, support, fund, or legally ship it, flag the missing viability analysis.", + "confidence": "high", + "sources": [ + "https://www.svpg.com/four-big-risks/" + ], + "evidence": "Primary (Cagan) verbatim: 'The business value (viability) risks can be substantial and I find that these are too often under-appreciated and under-estimated (or simply avoided) by the product manager,' and the old model was 'making it too easy for product managers to focus on customer value and overlook business value.' The taxonomy is explicitly framed as the corrective. Minor nuance: Cagan attributes this to 'the product manager' generically (driven by the old mental model) rather than literally labeling them 'weak.'", + "vote": "3-0 (claim 5)" + }, + { + "claim": "RICE is the canonical comparative prioritization score, computed as (Reach x Impact x Confidence) / Effort, yielding 'total impact per time worked' to rank competing initiatives. The four factors (Reach, Impact, Confidence, Effort) are each estimated SEPARATELY before being combined into one comparable number. RICE was explicitly created to counteract two prioritization failure modes: personal bias toward favorite/pet ideas, and inconsistent, non-comparable decisions across diverse projects.", + "confidence": "high", + "sources": [ + "https://www.intercom.com/blog/rice-simple-prioritization-for-product-managers/" + ], + "evidence": "Primary source = Intercom, the ORIGINATOR of RICE (built by Sean McBride). Verbatim: 'RICE Score Formula: (Reach x Impact x Confidence) / Effort. The resulting score measures total impact per time worked.' Failure modes verbatim: 'satisfying to work on pet ideas you would use yourself, instead of projects with broad reach' and the difficulty 'consistently combining and comparing these factors.' Corroborated by ProductPlan, Product School, Tempo, Ducalis, Whatfix. Caveat: critics (Dovetail, Medium) note inputs remain gameable (effort sandbagging, confidence inflation) - RICE structures consistency rather than mechanically enforcing it.", + "vote": "3-0 (claims 9, 10, 11)" + }, + { + "claim": "Product STRATEGY (not the roadmap) is the mechanism that decides WHICH problems to solve now, and it works by focusing on a FEW critical business levers - because most companies dilute their efforts by doing too many things at once. Prioritization is fundamentally an act of SAYING NO: focus means rejecting the hundred other good ideas, not just selecting the chosen ones. Enforcement: a 'strategy' that is a long list of everything is a non-strategy; force concentration on 2-3 levers and explicit rejection of the rest.", + "confidence": "high", + "sources": [ + "https://www.svpg.com/changing-how-you-decide-which-problems-to-solve/" + ], + "evidence": "Primary (SVPG / Cagan + Jon Moore) verbatim: 'the product strategy is how we identify the most important problems to solve now... begins by focusing on the most critical areas for business success. Most companies try to do too many things at once, and end up diluting their efforts... on the true levers for the business.' Saying-no principle anchored to authenticated Steve Jobs WWDC 1997 quote: 'focus... means saying no to the hundred other good ideas' (corroborated Farnam Street, CNBC, WWDC transcript). Cagan separately opposes feature-roadmaps, giving teams 'a prioritised set of business problems to solve.' Note: primary URL returned HTTP 403; quotes confirmed via multiple independent renderings.", + "vote": "3-0 (claims 12, 13)" + }, + { + "claim": "The defining anti-pattern of weak/reactive product organizations: they REACT to sales opportunities, competitors, customer requests, and price pressure instead of pursuing a customer-centric product vision; and in most feature-team companies the de facto 'strategy' is to ship as many stakeholder features as possible - 'which is to say, there really isn't a product strategy at all.' These are the highest-value codifiable red flags for distinguishing strong from weak PM behavior.", + "confidence": "high", + "sources": [ + "https://www.svpg.com/changing-how-you-decide-which-problems-to-solve/" + ], + "evidence": "Primary (SVPG) verbatim: 'So many companies spend their time reacting - reacting to new sales opportunities, reacting to competitors offerings, reacting to customer requests, and reacting to price pressure... in strong product companies, while they care about these factors, they are not driven by them. What drives them is the pursuit of a product vision.' And: 'in most feature-team companies, the product strategy is literally to try to deliver as many features as possible for the different stakeholders. Which is to say, there really isn't a product strategy at all' (verified verbatim via Jina reader + independent search). Consistent across INSPIRED/EMPOWERED/TRANSFORMED. Minor counter-nuance: some commentators argue Cagan's feature-team definition is overstated and stakeholders are partners/constraints, not to be dismissed.", + "vote": "3-0 (claims 14, 15)" + }, + { + "claim": "Product sense / PM craft is developed PRINCIPLES-FIRST, not techniques-first. A solid grasp of enduring principles is what gives a PM the mental model to judge WHEN a given technique applies and when it does not, and to evaluate new techniques as they emerge. Techniques churn constantly; principles endure. This is presented as the central differentiator between strong PMs and those who merely copy techniques - directly relevant to building a persona that reasons from fundamentals rather than pattern-matching trendy frameworks.", + "confidence": "high", + "sources": [ + "https://www.svpg.com/principles/" + ], + "evidence": "Primary (Cagan / SVPG) verbatim: 'when a person reaches the point that they have a solid understanding of the principles, they develop a good mental model for when each technique is useful and appropriate, and when it is not. Further, as new techniques emerge, they are able to quickly assess the potential value of the technique.' Also 'while the techniques change fairly constantly, the underlying principles endure.' Caveat: the explicit 'strong vs weak PM' contrast is the claimant's faithful gloss, not literal essay text. Note: canonical URL returned 403; verified via AgileAus mirror + multiple searches.", + "vote": "3-0 (claim 16)" + }, + { + "claim": "Jobs-To-Be-Done reframes product decisions around the JOB a customer 'hires' a product to accomplish (intent/causation) rather than around demographics, which fail to predict purchasing intent. A JTBD 'job' is the PROGRESS a person wants to make under specific circumstances - a process, not a single action - and includes FUNCTIONAL, EMOTIONAL, and SOCIAL dimensions. The operative distinction from a generic feature request is that a job captures progress + context across all three dimensions, not a discrete asked-for feature.", + "confidence": "high", + "sources": [ + "https://gopractice.io/product/jobs-to-be-done-the-theory-and-the-frameworks/", + "https://www.fullstory.com/blog/clayton-christensen-jobs-to-be-done-framework-product-development/" + ], + "evidence": "gopractice (secondary) verbatim: 'A job is the progress a person wants to achieve under specific circumstances. It isn't a single action but a process.' Backed by PRIMARY sources: Christensen Institute ('the progress they're trying to make... functional, social, and emotional dimensions') and 'Competing Against Luck' (2016). 'Hire' framing + demographics critique verbatim in FullStory and Christensen's HBR 2005 'Marketing Malpractice': demographics give correlation not causation - 'Demographics fail to predict intent.' Minor nuance: the Ulwick ODI school treats emotional/social as separate jobs rather than dimensions of one; claim states the mainstream Christensen lineage.", + "vote": "3-0 (claims 17, 20)" + }, + { + "claim": "Christensen's Four Forces of Progress is a codifiable switching predictor: two PUSH forces drive change (F1 frustration with the status quo, F2 attraction to the new product) and two RESISTANCE forces oppose it (F3 anxiety of learning the new, F4 habit/comfort of the current solution). A switch happens ONLY when Push + Pull > Anxiety + Habit. Useful for the persona as a model for predicting adoption and for diagnosing why a 'better' product still fails to win switchers.", + "confidence": "high", + "sources": [ + "https://gopractice.io/product/jobs-to-be-done-the-theory-and-the-frameworks/" + ], + "evidence": "gopractice (secondary) lists F1 push / F2 pull / F3 anxiety / F4 habit. Switching rule corroborated verbatim by learningloop.io and koji.so: 'When Push + Pull > Anxiety + Habit, the switch happens'; also jobstobedone.org (Moesta) and Christensen Institute. Primary grounding: 'Competing Against Luck' (2016). Two minor imprecisions, neither falsifying: the diagram was originated by Bob Moesta & Chris Spiek (~2012) and popularized by Christensen (so 'Christensen's' is loose co-attribution); 'new product' vs canonical 'new solution' is trivial paraphrase.", + "vote": "3-0 (claim 18)" + }, + { + "claim": "There is an explicit, falsifiable CHECKLIST for whether a job statement is valid: it must NOT be a trend, NOT a high-level need, NOT specific to one product class, must NOT imply a single solution, MUST include both the progress sought and the context (external + emotional), and must sit at the appropriate abstraction level. Directly codifiable as a validator gate for any 'job' or problem statement the persona accepts or emits.", + "confidence": "high", + "sources": [ + "https://gopractice.io/product/jobs-to-be-done-the-theory-and-the-frameworks/" + ], + "evidence": "Verified via direct WebFetch: source contains a 'Checklist for defining a job' with 7 numbered items mapping onto every claim condition (isn't a trend / isn't high-level / isn't one product class / doesn't imply a single solution / includes task+progress AND context external+emotional / appropriate abstraction). Each is a testable yes/no criterion ('falsifiable' is fair). Broader JTBD literature (Christensen Institute, Ulwick/Strategyn, Productboard) corroborates that valid jobs are solution-agnostic, tech-independent, appropriately scoped, with functional+emotional dimensions. Secondary-source quality is sufficient since the claim only asserts the source provides such a checklist.", + "vote": "3-0 (claim 19)" + }, + { + "claim": "Diagnostic decision rule (Shreyas Doshi): most 'execution problems' are NOT really execution problems - when a team appears to be failing at execution, the root cause is usually a deeper problem being band-aided. IMPORTANT CORPUS CORRECTION: Doshi names THREE roots, not one - STRATEGY, INTERPERSONAL, and CULTURE problems. Good leaders diagnose and fix the root; bad leaders apply band-aids. The persona should state this as 'strategy OR interpersonal OR culture,' not strategy/prioritization alone.", + "confidence": "high", + "sources": [ + "https://www.lennysnewsletter.com/p/episode-3-shreyas-doshi" + ], + "evidence": "Primary (Doshi's own tweet, Oct 2020): 'Most Execution problems are really 1) Strategy problems, or 2) Interpersonal problems, or 3) Culture problems. Good leaders execute well because they understand this. They fix the root problem. Bad leaders struggle because they are always applying band-aids.' Lenny's Podcast topic verbatim: 'Most execution problems are not really execution problems.' SCOPE DEFECT in original claim: it narrowed root cause to strategy/prioritization, dropping interpersonal and culture - corrected here. Core diagnostic (apparent execution failure masks a deeper root) is accurate and primary-sourced.", + "vote": "3-0 (claim 22)" + }, + { + "claim": "The LNO framework (Shreyas Doshi) is a task-prioritization and anti-procrastination system that classifies work by return on effort into LEVERAGE (10-100x return - invest heavily, pursue excellence/perfectionism), NEUTRAL (~1.1x return - do a solid 'good enough' job), and OVERHEAD (necessary but low return - deliberately do a 'bad'/minimal job). PMs should invest DISPROPORTIONATE effort in high-leverage work rather than treating all tasks equally. Codifiable as an effort-allocation rule: match perfectionism to leverage tier.", + "confidence": "high", + "sources": [ + "https://www.lennysnewsletter.com/p/episode-3-shreyas-doshi" + ], + "evidence": "Primary (Doshi's Coda doc, coda.io/@shreyas/lno-framework): categorizes work by return on effort - Leverage '10x or even 100x return', Neutral '1.0 effort results in 1.1x return', Overhead 'necessary but provide little return' (advice: actively try to do a bad job). Original 2021 X thread: 'all your tasks are not created equal. There are 3 types of PM tasks: 1) Leverage 2) Neutral 3) Overhead.' Anti-procrastination element confirmed in the Coda source (addresses perfectionism-driven avoidance of hard L tasks). Still actively cited 2024-2026.", + "vote": "3-0 (claim 23)" + } + ], + "caveats": "SOURCE QUALITY: 18 of 24 confirmed claims rest on PRIMARY sources from the exact authorities the brief prioritized - Marty Cagan/SVPG (4 risks, ownership, viability failure mode, strategy/saying-no, reactive anti-patterns, principles-first), Teresa Torres/Product Talk (continuous discovery, story-based interviews, opportunity solution trees), and Intercom (RICE's originator). These are the strongest, most enforceable rules in the corpus and should carry the most weight in the persona/enforcement layer. The JTBD cluster (5 claims) is anchored to a SECONDARY aggregator (gopractice.io) but is backed throughout by primary Christensen sources (Christensen Institute, HBR 2005, 'Competing Against Luck' 2016), so confidence remains high. The two Shreyas Doshi claims cite a SECONDARY podcast page but are corroborated by Doshi's own primary tweets/Coda docs.\n\nONE MATERIAL CORRECTION (do not propagate the original wording): Claim 22 as originally stated narrowed the root cause of execution problems to 'strategy/prioritization.' Doshi's actual primary source names THREE roots - strategy, interpersonal, AND culture. The persona must use the three-way version; the strategy-only framing is a scope error.\n\nACCESS NOTE: several SVPG URLs (four-big-risks, changing-how-you-decide, principles) returned HTTP 403 to direct fetches; their verbatim quotes were confirmed via independent mirrors (AgileAus), reader proxies (Jina), and multiple WebSearch renderings that agreed word-for-word. The quotes are reliable, but no single first-party page load backs them.\n\nFRAMING NUANCES (faithful but not literal): 'weekly customer contact' is an aspirational best-practice benchmark, not an absolute no-exceptions law (Torres allows context-dependent exceptions). 'Weak PMs' labels in the Cagan claims are interpretive glosses - Cagan attributes the viability-avoidance tendency to 'the product manager' generically, driven by an outdated mental model, not to a named 'weak PM' category. RICE 'enforces' consistency is slightly strong: inputs (effort, confidence) remain gameable, so RICE structures/encourages comparability rather than mechanically guaranteeing it. The Four Forces diagram is co-attributable to Bob Moesta & Chris Spiek, not Christensen alone.\n\nCOVERAGE GAPS: the confirmed corpus is thin on Pillar 2 frameworks beyond RICE and saying-no - Kano, MoSCoW, ICE, opportunity sizing, and explicit MVP/scope-cutting definitions did not survive into confirmed claims, so the persona's prioritization toolkit is currently RICE + opportunity solution trees + Cagan strategy, not the full named set in the brief. Reforge, a16z, John Cutler, and Melissa Perri are not represented among confirmed sources. TIME-SENSITIVITY: all frameworks here are stable, foundational, and still canonical as of 2026; none are at risk of going stale, but the field's tooling references (e.g., AI-assisted discovery) evolve and were not part of this corpus.", + "openQuestions": [ + "How do the best PMs actually DEFINE and CUT an MVP / minimum scope in practice? The confirmed corpus establishes 'saying no' as a principle and RICE for ranking, but contains no codifiable rule for where to draw the MVP line, how to sequence a roadmap, or how to decide what is the smallest valuable slice - a gap for the enforcement layer.", + "What are the concrete mechanics of the prioritization frameworks named in the brief but absent from confirmed claims - Kano (delighters vs must-haves), MoSCoW, ICE, and opportunity sizing? Only RICE survived verification; the persona currently lacks rules for the others.", + "How is 'product sense' developed beyond the principles-first stance - i.e., what are the repeatable PRACTICES (pattern libraries, deliberate teardowns, mentorship, feedback loops) that the best operators (Doshi, Lenny Rachitsky, Julie Zhuo) prescribe for actually building judgment over time?", + "How should a PM structure and document a defensible DECISION under uncertainty (the explicit rationale/evidence template, pre-mortems, decision logs, disagree-and-commit)? The corpus strongly establishes WHY conviction matters and which anti-patterns to avoid, but offers no concrete decision-writeup format to enforce." + ], + "refuted": [ + { + "claim": "JTBD distinguishes a real 'job' from an assumption/generic need via context: needs are 'fairly universal and don't take into account the person's context,' whereas jobs incorporate the user's specific situation, making context the test for whether a stated need is real.", + "vote": "1-2", + "source": "https://gopractice.io/product/jobs-to-be-done-the-theory-and-the-frameworks/" + } + ], + "sources": [ + { + "url": "https://www.producttalk.org/opportunity-solution-trees/", + "quality": "primary", + "angle": "Pillar 1 - Customer/problem grounding & discovery", + "claimCount": 5 + }, + { + "url": "https://www.svpg.com/four-big-risks/", + "quality": "primary", + "angle": "Pillar 1 - Customer/problem grounding & discovery", + "claimCount": 5 + }, + { + "url": "https://gopractice.io/product/jobs-to-be-done-the-theory-and-the-frameworks/", + "quality": "secondary", + "angle": "Pillar 1 - Customer/problem grounding & discovery", + "claimCount": 5 + }, + { + "url": "https://www.producttalk.org/2021/08/product-discovery/", + "quality": "primary", + "angle": "Pillar 1 - Customer/problem grounding & discovery", + "claimCount": 5 + }, + { + "url": "https://www.fullstory.com/blog/clayton-christensen-jobs-to-be-done-framework-product-development/", + "quality": "secondary", + "angle": "Pillar 1 - Customer/problem grounding & discovery", + "claimCount": 5 + }, + { + "url": "https://blog.logrocket.com/product-management/opportunity-solution-trees-definition-examples-how-to/", + "quality": "blog", + "angle": "Pillar 1 - Customer/problem grounding & discovery", + "claimCount": 5 + }, + { + "url": "https://www.intercom.com/blog/rice-simple-prioritization-for-product-managers/", + "quality": "primary", + "angle": "Pillar 2 - Prioritization frameworks & scope", + "claimCount": 5 + }, + { + "url": "https://www.lennysnewsletter.com/p/episode-3-shreyas-doshi", + "quality": "secondary", + "angle": "Pillar 2 - Prioritization frameworks & scope", + "claimCount": 5 + }, + { + "url": "https://www.svpg.com/changing-how-you-decide-which-problems-to-solve/", + "quality": "primary", + "angle": "Pillar 2 - Prioritization frameworks & scope", + "claimCount": 5 + }, + { + "url": "https://www.productlift.dev/blog/product-prioritization-framework-comparison/", + "quality": "blog", + "angle": "Pillar 2 - Prioritization frameworks & scope", + "claimCount": 5 + }, + { + "url": "https://www.atlassian.com/agile/product-management/prioritization-framework", + "quality": "blog", + "angle": "Pillar 2 - Prioritization frameworks & scope", + "claimCount": 5 + }, + { + "url": "https://www.productplan.com/glossary/rice-scoring-model", + "quality": "secondary", + "angle": "Pillar 2 - Prioritization frameworks & scope", + "claimCount": 5 + }, + { + "url": "https://www.lennyspodcast.com/blog/shreyas-doshi-on-pre-mortems-lno-framework-3-levels-of-product-work-and-prioritization/", + "quality": "unreliable", + "angle": "Pillar 3 - Decision-making under uncertainty & conviction", + "claimCount": 0 + }, + { + "url": "https://blackboxofpm.com/making-good-decisions-as-a-product-manager-c66ddacc9e2b", + "quality": "unreliable", + "angle": "Pillar 3 - Decision-making under uncertainty & conviction", + "claimCount": 0 + }, + { + "url": "https://pmctraining.com/articles-and-resources/decision-quality-vs-outcome-quality/", + "quality": "secondary", + "angle": "Pillar 3 - Decision-making under uncertainty & conviction", + "claimCount": 5 + }, + { + "url": "https://cutlefish.substack.com/p/tbm-852-top-1-product-managers", + "quality": "blog", + "angle": "Failure modes & anti-patterns of weak PMs", + "claimCount": 5 + }, + { + "url": "https://itamargilad.com/pm-antipatterns/", + "quality": "blog", + "angle": "Failure modes & anti-patterns of weak PMs", + "claimCount": 5 + }, + { + "url": "https://www.prodpad.com/blog/agile-anti-patterns/", + "quality": "blog", + "angle": "Failure modes & anti-patterns of weak PMs", + "claimCount": 5 + }, + { + "url": "https://userpilot.com/blog/feature-factory/", + "quality": "blog", + "angle": "Failure modes & anti-patterns of weak PMs", + "claimCount": 5 + }, + { + "url": "https://www.lennysnewsletter.com/p/product-sense", + "quality": "blog", + "angle": "Mental models, product sense & frameworks of the best PMs", + "claimCount": 5 + }, + { + "url": "https://www.svpg.com/principles/", + "quality": "primary", + "angle": "Mental models, product sense & frameworks of the best PMs", + "claimCount": 4 + } + ], + "stats": { + "angles": 5, + "sourcesFetched": 21, + "claimsExtracted": 94, + "claimsVerified": 25, + "confirmed": 24, + "killed": 1, + "afterSynthesis": 16, + "urlDupes": 3, + "budgetDropped": 6, + "agentCalls": 103 + } + } +} \ No newline at end of file diff --git a/docs/superpowers/plans/2026-06-21-pm-persona-vertical.md b/docs/superpowers/plans/2026-06-21-pm-persona-vertical.md new file mode 100644 index 0000000..7f5d694 --- /dev/null +++ b/docs/superpowers/plans/2026-06-21-pm-persona-vertical.md @@ -0,0 +1,1184 @@ +# PM Persona Vertical Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `personas/` 4th vertical and a `product-manager` persona that owns the value+viability product risks via a tiered decision gate, grounded in verified PM-craft research. + +**Architecture:** Three parts. **A** - a `product-manager` MCP plugin (pure `data.ts` + tools, mirrors the `optimizer` plugin: a framework menu, not implementations). **B** - the `personas/` vertical infrastructure (manifest schema + registry + a `Layer 4: Personas` section compiled into the bootstrap). **C** - the `pm-gate` skill + persona manifest + router wiring that bind plugin+skill+role into the tiered gate. Parts are review checkpoints; A is standalone, B is standalone (fixture-tested), C composes A+B. + +**Tech Stack:** TypeScript (ESM, `.js` import specifiers), `@modelcontextprotocol/sdk`, `zod`, `bun:test`. No new dependencies. + +**Branch:** `f-PE-pm-persona-vertical` (already created; spec + corpus already committed there). + +**Source of truth for corpus content:** `docs/research/2026-06-21-pm-craft-corpus.json` (committed). All framework text below is transcribed from confirmed claims in that file. + +--- + +## File Structure + +``` +PART A - plugin (testable, standalone) + Create src/plugins/product-manager/data.ts types + framework data + resolver fns + Create src/plugins/product-manager/index.ts Plugin export, registers 9 tools + Create src/plugins/product-manager/tools/*.ts 9 tool files (one register() each) + Modify src/index.ts import + add to allPlugins + Modify tests/plugin-registry-behaviour.test.ts 13 -> 14; assert PM tools present + Create tests/product-manager-behaviour.test.ts tool output assertions + +PART B - personas vertical (testable, fixture) + Create personas/persona.schema.json manifest JSON Schema + Create personas/persona-registry.ts load + validate manifests + Create personas/README.md vertical overview + Modify skills/hyperstack/SKILL.md + '## Layer 4: Personas' + '## Persona Registry' + Modify src/internal/context-compiler.ts extract/emit Personas + markers + Regen generated/runtime-context/hyperstack.bootstrap.md via compile:context + Create tests/persona-registry-behaviour.test.ts registry loads PM manifest + +PART C - skill + binding (composes A+B) + Create personas/product-manager/persona.json manifest binding plugin+skill+role + Create personas/product-manager/PROFILE.md identity, owns value+viability, voice + Create personas/product-manager/LIFECYCLE.md engage criteria, gate steps, handback + Create personas/product-manager/CHECKS.md the falsifiable gate checklist + Create skills/pm-gate/SKILL.md tiered gate workflow + Modify harness/router.md, harness/transitions.md persona-gate routing +``` + +--- + +# PART A - `product-manager` MCP plugin + +> **Storage (revised during execution):** corpus prose is NOT inlined in `data.ts`. +> It lives in `src/plugins/product-manager/snippets/**.txt` loaded via +> `loader.ts` (`createSnippetLoader`), matching the dominant ecosystem pattern +> (react/golang/designer). `data.ts` keeps typed structure + logic and pulls prose +> via `snippet("...")`. Also required: a `product-manager` entry in +> `scripts/audit/sources.ts` (editorial, `skills: []` until Part C sets `["pm-gate"]`), +> enforced by `tests/audit-harness-behaviour.test.ts`. See the committed plugin +> for the authoritative shape. + +### Task A1: Plugin data layer (types + frameworks + resolvers) + +**Files:** +- Create: `src/plugins/product-manager/data.ts` +- Test: `tests/product-manager-behaviour.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `tests/product-manager-behaviour.test.ts`: + +```ts +import { test, expect } from "bun:test"; +import { + FOUR_RISKS, + classifyOpportunityVsSolution, + validateJobStatement, + scoreRice, + PM_OWNED_RISKS, +} from "../src/plugins/product-manager/data.ts"; + +test("four risks are exactly the canonical four", () => { + expect(FOUR_RISKS.map((r) => r.id).sort()).toEqual( + ["feasibility", "usability", "value", "viability"], + ); +}); + +test("PM owns exactly value and viability", () => { + expect([...PM_OWNED_RISKS].sort()).toEqual(["value", "viability"]); +}); + +test("single-path statement is classified as a solution, not an opportunity", () => { + const r = classifyOpportunityVsSolution("add a dark mode toggle"); + expect(r.isOpportunity).toBe(false); +}); + +test("multi-path statement is classified as an opportunity", () => { + const r = classifyOpportunityVsSolution("users cannot find their past orders"); + expect(r.isOpportunity).toBe(true); +}); + +test("job statement missing context fails validation", () => { + const r = validateJobStatement("I want a faster app"); + expect(r.valid).toBe(false); + expect(r.failed.length).toBeGreaterThan(0); +}); + +test("rice score is (reach*impact*confidence)/effort", () => { + expect(scoreRice({ reach: 100, impact: 2, confidence: 0.8, effort: 4 }).score).toBeCloseTo(40); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun test tests/product-manager-behaviour.test.ts` +Expected: FAIL - cannot find module `../src/plugins/product-manager/data.ts`. + +- [ ] **Step 3: Write `src/plugins/product-manager/data.ts`** + +```ts +// Product-Manager persona - the PM DECISION MENU, not project opinions. +// +// Carries only stable, source-cited PM craft (Cagan/SVPG, Torres, Intercom, +// Christensen, Doshi) transcribed from docs/research/2026-06-21-pm-craft-corpus.json. +// Tools surface these frameworks so the agent makes grounded value+viability +// calls before design/build. No project-specific product opinions live here. + +export interface ProductRisk { + id: "value" | "usability" | "feasibility" | "viability"; + question: string; + owner: "product-manager" | "designer" | "engineer"; +} + +export const FOUR_RISKS: ProductRisk[] = [ + { id: "value", question: "Will customers buy it, or will users choose to use it?", owner: "product-manager" }, + { id: "usability", question: "Can users figure out how to use it?", owner: "designer" }, + { id: "feasibility", question: "Can engineers build it with available time, skills, and tech?", owner: "engineer" }, + { id: "viability", question: "Does it work for the business - legal, sales, finance, marketing, support?", owner: "product-manager" }, +]; + +export const PM_OWNED_RISKS: ReadonlySet = new Set( + FOUR_RISKS.filter((r) => r.owner === "product-manager").map((r) => r.id), +); + +export interface AntiPattern { + id: string; + smell: string; + source: string; +} + +export const ANTI_PATTERNS: AntiPattern[] = [ + { id: "feature-factory", smell: "Shipping as many stakeholder features as possible. Cagan: that is 'really no product strategy at all.'", source: "Cagan/SVPG" }, + { id: "reactivity", smell: "Driven by reacting to sales, competitors, requests, or price pressure instead of a product vision.", source: "Cagan/SVPG" }, + { id: "viability-avoidance", smell: "Reasoning covers value/desirability but is silent on whether the business can sell, support, fund, or legally ship it.", source: "Cagan/SVPG" }, + { id: "execution-misdiagnosis", smell: "Treating a strategy, interpersonal, OR culture problem as an execution problem and band-aiding it.", source: "Doshi" }, + { id: "opinion-requirement", smell: "A requirement justified by 'a customer said they want X' (opinion/hypothetical) instead of an observed past-behaviour story.", source: "Torres / The Mom Test" }, +]; + +export const DISCOVERY_RULES: string[] = [ + "Engage customers continuously (aspirationally weekly); minimize build decisions made without customer evidence.", + "Never ask customers what they want or need - cognitive biases make opinion answers unreliable.", + "Elicit needs, pains, and desires through specific PAST-BEHAVIOUR stories.", + "Demographics do not predict intent; the JOB the customer hires the product for does.", +]; + +export const STRATEGY_RULES: string[] = [ + "Strategy decides which problems to solve NOW by focusing on a FEW (2-3) critical business levers.", + "Prioritization is an act of saying NO: focus means rejecting the hundred other good ideas.", + "A 'strategy' that is a long list rejecting nothing is not a strategy.", +]; + +export interface Jtbd { + definition: string; + dimensions: string[]; + fourForces: { push: string[]; resist: string[]; rule: string }; +} + +export const JTBD: Jtbd = { + definition: "A job is the PROGRESS a person wants to make under specific circumstances - a process, not a single action.", + dimensions: ["functional", "emotional", "social"], + fourForces: { + push: ["frustration with the status quo (F1)", "attraction to the new solution (F2)"], + resist: ["anxiety of learning the new (F3)", "habit/comfort of the current solution (F4)"], + rule: "A switch happens only when Push + Pull > Anxiety + Habit.", + }, +}; + +// JTBD job-statement validator - the regex-checkable SUBSET (context, progress, +// not-a-single-solution). The full 7-point checklist (e.g. "not a trend", +// "appropriate abstraction") is not mechanically testable and is a Phase 2 item. +export interface JobCheck { id: string; test: (s: string) => boolean; failHint: string; } + +export const JOB_CHECKS: JobCheck[] = [ + { id: "has-context", test: (s) => /\b(when|while|after|before|during|because)\b/i.test(s), failHint: "missing context (when/while/because ...)" }, + { id: "not-single-solution", test: (s) => !/\b(button|toggle|page|dropdown|API|endpoint|screen)\b/i.test(s), failHint: "names a specific solution, not a job" }, + { id: "expresses-progress", test: (s) => /\b(so that|in order to|to|need|want to)\b/i.test(s), failHint: "does not express the progress sought" }, +]; + +export interface JobValidation { valid: boolean; passed: string[]; failed: { id: string; failHint: string }[]; } + +export function validateJobStatement(statement: string): JobValidation { + const passed: string[] = []; + const failed: { id: string; failHint: string }[] = []; + for (const c of JOB_CHECKS) { + if (c.test(statement)) passed.push(c.id); + else failed.push({ id: c.id, failHint: c.failHint }); + } + return { valid: failed.length === 0, passed, failed }; +} + +export interface OppClassification { isOpportunity: boolean; reason: string; } + +// Torres test: "is there more than one way to address this?" If a statement +// names a single concrete mechanism, it is a solution disguised as a problem. +const SOLUTION_MARKERS = /\b(add|build|create|implement|use|toggle|button|dropdown|dark mode|integrate)\b/i; + +export function classifyOpportunityVsSolution(statement: string): OppClassification { + if (SOLUTION_MARKERS.test(statement)) { + return { isOpportunity: false, reason: "Names a single concrete mechanism. Restate as the underlying need it serves (there should be more than one way to address it)." }; + } + return { isOpportunity: true, reason: "Stated as a need/pain with more than one possible solution." }; +} + +export interface RiceInput { reach: number; impact: number; confidence: number; effort: number; } +export interface RiceResult { score: number; formula: string; } + +export function scoreRice(i: RiceInput): RiceResult { + const score = (i.reach * i.impact * i.confidence) / i.effort; + return { score, formula: "(Reach x Impact x Confidence) / Effort" }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bun test tests/product-manager-behaviour.test.ts` +Expected: PASS (6 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/plugins/product-manager/data.ts tests/product-manager-behaviour.test.ts +git commit -m "feat(product-manager): plugin data layer - four risks, JTBD, RICE, validators" +``` + +--- + +### Task A2: Lookup tools (`get_four_risks`, `get_jtbd`, `get_discovery_rules`, `get_anti_patterns`, `get_strategy_rules`) + +These five tools share one shape: format a `data.ts` export into markdown. Each is its own file and `register()`. + +**Files:** +- Create: `src/plugins/product-manager/tools/get-four-risks.ts` +- Create: `src/plugins/product-manager/tools/get-jtbd.ts` +- Create: `src/plugins/product-manager/tools/get-discovery-rules.ts` +- Create: `src/plugins/product-manager/tools/get-anti-patterns.ts` +- Create: `src/plugins/product-manager/tools/get-strategy-rules.ts` +- Test: `tests/product-manager-behaviour.test.ts` (append) + +- [ ] **Step 1: Append failing tests** + +```ts +import { captureTool, extractTextContent } from "./helpers.ts"; +import { register as getFourRisks } from "../src/plugins/product-manager/tools/get-four-risks.ts"; +import { register as getAntiPatterns } from "../src/plugins/product-manager/tools/get-anti-patterns.ts"; + +test("get_four_risks names value and viability as PM-owned", async () => { + const tool = captureTool(getFourRisks); + expect(tool.name).toBe("product_manager_get_four_risks"); + const text = extractTextContent(await tool.invoke({})); + expect(text).toMatch(/value/i); + expect(text).toMatch(/viability/i); + expect(text).toMatch(/product-manager/); +}); + +test("get_anti_patterns includes the feature-factory smell", async () => { + const tool = captureTool(getAntiPatterns); + const text = extractTextContent(await tool.invoke({})); + expect(text).toMatch(/feature-factory/); +}); +``` + +- [ ] **Step 2: Run to verify fail** + +Run: `bun test tests/product-manager-behaviour.test.ts` +Expected: FAIL - cannot find the tool modules. + +- [ ] **Step 3: Implement the five tools** + +`src/plugins/product-manager/tools/get-four-risks.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { FOUR_RISKS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_four_risks", + "The four product risks every build must address before shipping (Cagan/SVPG): value, usability, feasibility, viability - with which role owns each. The PM owns value and viability.", + {}, + async () => { + let text = `# The Four Product Risks\n\nAddress all four before building. The PM is accountable for **value** and **viability**.\n\n| risk | question | owner |\n|---|---|---|\n`; + for (const r of FOUR_RISKS) text += `| ${r.id} | ${r.question} | ${r.owner} |\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} +``` + +`src/plugins/product-manager/tools/get-anti-patterns.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ANTI_PATTERNS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_anti_patterns", + "Codifiable PM red flags (Cagan, Doshi): feature-factory, reactivity, viability-avoidance, execution-misdiagnosis, opinion-requirement. Use to flag weak product reasoning.", + {}, + async () => { + let text = `# PM Anti-Patterns (red flags)\n\n`; + for (const a of ANTI_PATTERNS) text += `## ${a.id}\n${a.smell} \n_source: ${a.source}_\n\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} +``` + +`src/plugins/product-manager/tools/get-jtbd.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { JTBD } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_jtbd", + "Jobs-To-Be-Done framing (Christensen): a job is progress-in-context across functional/emotional/social dimensions, plus the Four Forces switching rule. Use to reframe a feature request as the underlying job.", + {}, + async () => { + const f = JTBD.fourForces; + const text = + `# Jobs To Be Done\n\n${JTBD.definition}\n\n` + + `**Dimensions:** ${JTBD.dimensions.join(", ")}\n\n` + + `## Four Forces of Progress\n- Push: ${f.push.join("; ")}\n- Resist: ${f.resist.join("; ")}\n\n**${f.rule}**\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} +``` + +`src/plugins/product-manager/tools/get-discovery-rules.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { DISCOVERY_RULES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_discovery_rules", + "Continuous-discovery rules (Torres): weekly customer contact, never ask 'what do you want', elicit past-behaviour stories. Use before claiming a build is customer-grounded.", + {}, + async () => { + const text = `# Discovery Rules\n\n${DISCOVERY_RULES.map((r) => `- ${r}`).join("\n")}\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} +``` + +`src/plugins/product-manager/tools/get-strategy-rules.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { STRATEGY_RULES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_strategy_rules", + "Product-strategy rules (Cagan): focus on 2-3 levers, saying no is the act of prioritization, a long list is a non-strategy. Use to cut scope.", + {}, + async () => { + const text = `# Strategy Rules\n\n${STRATEGY_RULES.map((r) => `- ${r}`).join("\n")}\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} +``` + +- [ ] **Step 4: Run to verify pass** + +Run: `bun test tests/product-manager-behaviour.test.ts` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/plugins/product-manager/tools/get-four-risks.ts src/plugins/product-manager/tools/get-jtbd.ts src/plugins/product-manager/tools/get-discovery-rules.ts src/plugins/product-manager/tools/get-anti-patterns.ts src/plugins/product-manager/tools/get-strategy-rules.ts tests/product-manager-behaviour.test.ts +git commit -m "feat(product-manager): five framework-lookup tools" +``` + +--- + +### Task A3: Logic tools (`opportunity_vs_solution`, `validate_job_statement`, `score_rice`) + +**Files:** +- Create: `src/plugins/product-manager/tools/opportunity-vs-solution.ts` +- Create: `src/plugins/product-manager/tools/validate-job-statement.ts` +- Create: `src/plugins/product-manager/tools/score-rice.ts` +- Test: `tests/product-manager-behaviour.test.ts` (append) + +- [ ] **Step 1: Append failing tests** + +```ts +import { register as oppVsSol } from "../src/plugins/product-manager/tools/opportunity-vs-solution.ts"; +import { register as scoreRiceTool } from "../src/plugins/product-manager/tools/score-rice.ts"; + +test("opportunity_vs_solution flags a solution-shaped statement", async () => { + const tool = captureTool(oppVsSol); + expect(tool.name).toBe("product_manager_opportunity_vs_solution"); + const text = extractTextContent(await tool.invoke({ statement: "add a dark mode toggle" })); + expect(text).toMatch(/solution/i); +}); + +test("score_rice renders the computed score", async () => { + const tool = captureTool(scoreRiceTool); + const text = extractTextContent(await tool.invoke({ reach: 100, impact: 2, confidence: 0.8, effort: 4 })); + expect(text).toMatch(/40/); +}); +``` + +- [ ] **Step 2: Run to verify fail** + +Run: `bun test tests/product-manager-behaviour.test.ts` +Expected: FAIL - modules not found. + +- [ ] **Step 3: Implement the three tools** + +`src/plugins/product-manager/tools/opportunity-vs-solution.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { classifyOpportunityVsSolution } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_opportunity_vs_solution", + "Torres test: is a statement a real opportunity (a need with more than one way to address it) or a solution in disguise? Rejects premature solution-jumping.", + { statement: z.string().describe("The problem/feature statement to classify.") }, + async ({ statement }) => { + const r = classifyOpportunityVsSolution(statement); + const verdict = r.isOpportunity ? "OPPORTUNITY" : "SOLUTION (reframe needed)"; + return { content: [{ type: "text" as const, text: `# ${verdict}\n\n**Statement:** ${statement}\n\n${r.reason}\n` }] }; + }, + ); +} +``` + +`src/plugins/product-manager/tools/validate-job-statement.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { validateJobStatement } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_validate_job_statement", + "Validate a JTBD job statement against the falsifiable checklist (has context, expresses progress, not a single solution). Returns pass/fail per check.", + { statement: z.string().describe("The job statement to validate.") }, + async ({ statement }) => { + const r = validateJobStatement(statement); + let text = `# Job statement: ${r.valid ? "VALID" : "INVALID"}\n\n**Statement:** ${statement}\n\n`; + text += `Passed: ${r.passed.join(", ") || "none"}\n\n`; + if (r.failed.length) text += `Failed:\n${r.failed.map((f) => `- ${f.id}: ${f.failHint}`).join("\n")}\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} +``` + +`src/plugins/product-manager/tools/score-rice.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { scoreRice } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_score_rice", + "Compute a RICE prioritization score = (Reach x Impact x Confidence) / Effort (Intercom). Confidence is 0-1. Use to rank competing initiatives.", + { + reach: z.number().describe("People affected per time period."), + impact: z.number().describe("Per-person impact (e.g. 3=massive,2=high,1=medium,0.5=low,0.25=minimal)."), + confidence: z.number().describe("Confidence 0-1 (1.0=high, 0.8=medium, 0.5=low)."), + effort: z.number().describe("Person-months (or any consistent effort unit)."), + }, + async ({ reach, impact, confidence, effort }) => { + const r = scoreRice({ reach, impact, confidence, effort }); + return { content: [{ type: "text" as const, text: `# RICE score: ${r.score.toFixed(2)}\n\n\`${r.formula}\` = (${reach} x ${impact} x ${confidence}) / ${effort}\n` }] }; + }, + ); +} +``` + +- [ ] **Step 4: Run to verify pass** + +Run: `bun test tests/product-manager-behaviour.test.ts` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/plugins/product-manager/tools/opportunity-vs-solution.ts src/plugins/product-manager/tools/validate-job-statement.ts src/plugins/product-manager/tools/score-rice.ts tests/product-manager-behaviour.test.ts +git commit -m "feat(product-manager): logic tools - opportunity test, job validator, RICE" +``` + +--- + +### Task A4: Orchestrator tool (`resolve_product_decision`) + plugin index + wiring + +**Files:** +- Create: `src/plugins/product-manager/tools/resolve-product-decision.ts` +- Create: `src/plugins/product-manager/index.ts` +- Modify: `src/index.ts` (import + `allPlugins`) +- Modify: `tests/plugin-registry-behaviour.test.ts:23` (13 -> 14) and add PM presence test +- Test: `tests/product-manager-behaviour.test.ts` (append) + +- [ ] **Step 1: Append failing tests** + +In `tests/product-manager-behaviour.test.ts`: + +```ts +import { register as resolveDecision } from "../src/plugins/product-manager/tools/resolve-product-decision.ts"; + +test("resolve_product_decision emits a verdict and the four risks", async () => { + const tool = captureTool(resolveDecision); + expect(tool.name).toBe("product_manager_resolve_product_decision"); + const text = extractTextContent(await tool.invoke({ description: "add a dark mode toggle" })); + expect(text).toMatch(/VERDICT|NEEDS-INPUT|BLOCK|PASS/); + expect(text).toMatch(/viability/i); +}); +``` + +In `tests/plugin-registry-behaviour.test.ts`, change the count assertion and add a presence test: + +```ts +test("all 14 plugins register at least one tool", () => { + const tools = getRegisteredTools(); + const pluginPrefixes = new Set(tools.map((t) => t.name.split("_")[0])); + expect(pluginPrefixes.size).toBe(14); +}); + +test("product-manager plugin registers its gate tools", () => { + const tools = getRegisteredTools(); + const toolNames = new Set(tools.map((t) => t.name)); + expect(toolNames.has("product_manager_get_four_risks")).toBe(true); + expect(toolNames.has("product_manager_resolve_product_decision")).toBe(true); +}); +``` + +(Delete the old `"all 13 plugins..."` test block - it is replaced by the 14 version above.) + +- [ ] **Step 2: Run to verify fail** + +Run: `bun test tests/product-manager-behaviour.test.ts tests/plugin-registry-behaviour.test.ts` +Expected: FAIL - orchestrator module missing; registry still sees 13. + +- [ ] **Step 3a: Implement the orchestrator** + +`src/plugins/product-manager/tools/resolve-product-decision.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { FOUR_RISKS, classifyOpportunityVsSolution } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_resolve_product_decision", + "The PM gate engine. Takes a build/feature description and returns a structured product decision: opportunity framing, four-risks assessment (esp value+viability), and a PASS/BLOCK/NEEDS-INPUT verdict the gate enforces.", + { + description: z.string().describe("What is being proposed to build."), + isNetNew: z.boolean().optional().describe("True for a net-new feature/product (hard gate); false for a tweak (advisory)."), + }, + async ({ description, isNetNew }) => { + const opp = classifyOpportunityVsSolution(description); + let text = `# Product Decision\n\n**Proposal:** ${description}\n\n`; + text += `## 1. Opportunity framing\n${opp.isOpportunity ? "Framed as an opportunity." : "SOLUTION in disguise - reframe to the underlying need."} ${opp.reason}\n\n`; + text += `## 2. Four-risks assessment (fill before proceeding)\n`; + for (const r of FOUR_RISKS) text += `- **${r.id}** (${r.owner}): ${r.question} -> _unassessed_\n`; + text += `\n## 3. Verdict\n`; + const gate = isNetNew === false ? "ADVISORY" : "HARD"; + if (!opp.isOpportunity) { + text += `NEEDS-INPUT (${gate}): restate as an opportunity, then assess value + viability before build.\n`; + } else { + text += `NEEDS-INPUT (${gate}): assess all four risks - especially value and viability - and state the prioritized call with a rationale. PASS only when value+viability are addressed.\n`; + } + return { content: [{ type: "text" as const, text }] }; + }, + ); +} +``` + +- [ ] **Step 3b: Implement the plugin index** + +`src/plugins/product-manager/index.ts`: + +```ts +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as getFourRisks } from "./tools/get-four-risks.js"; +import { register as getJtbd } from "./tools/get-jtbd.js"; +import { register as getDiscoveryRules } from "./tools/get-discovery-rules.js"; +import { register as getAntiPatterns } from "./tools/get-anti-patterns.js"; +import { register as getStrategyRules } from "./tools/get-strategy-rules.js"; +import { register as opportunityVsSolution } from "./tools/opportunity-vs-solution.js"; +import { register as validateJobStatement } from "./tools/validate-job-statement.js"; +import { register as scoreRice } from "./tools/score-rice.js"; +import { register as resolveProductDecision } from "./tools/resolve-product-decision.js"; + +function register(server: McpServer): void { + getFourRisks(server); + getJtbd(server); + getDiscoveryRules(server); + getAntiPatterns(server); + getStrategyRules(server); + opportunityVsSolution(server); + validateJobStatement(server); + scoreRice(server); + resolveProductDecision(server); +} + +export const productManagerPlugin: Plugin = { + name: "product-manager", + register, +}; +``` + +- [ ] **Step 3c: Wire into `src/index.ts`** + +Add the import beside the other plugin imports: + +```ts +import { productManagerPlugin } from "./plugins/product-manager/index.js"; +``` + +Add to the `allPlugins` array (after `optimizerPlugin`): + +```ts + optimizerPlugin, + productManagerPlugin, +]; +``` + +- [ ] **Step 4: Run the full suite** + +Run: `bun test` +Expected: PASS, including the updated `all 14 plugins` and PM presence tests. + +- [ ] **Step 5: Commit** + +```bash +git add src/plugins/product-manager/tools/resolve-product-decision.ts src/plugins/product-manager/index.ts src/index.ts tests/plugin-registry-behaviour.test.ts tests/product-manager-behaviour.test.ts +git commit -m "feat(product-manager): orchestrator gate tool + register plugin (14th)" +``` + +**Part A checkpoint:** the `product-manager` plugin ships 9 callable tools. Reviewable independently. + +--- + +# PART B - `personas/` vertical infrastructure + +### Task B1: Manifest schema + registry + +**Files:** +- Create: `personas/persona.schema.json` +- Create: `personas/persona-registry.ts` +- Test: `tests/persona-registry-behaviour.test.ts` + +- [ ] **Step 1: Write the failing test** + +`tests/persona-registry-behaviour.test.ts`: + +```ts +import { test, expect } from "bun:test"; +import { loadPersonas } from "../personas/persona-registry.ts"; + +test("registry loads the product-manager persona manifest", () => { + const personas = loadPersonas(); + const pm = personas.find((p) => p.id === "product-manager"); + expect(pm).toBeDefined(); + expect(pm!.owns.risks.sort()).toEqual(["value", "viability"]); + expect(pm!.gate_policy.net_new).toBe("hard"); +}); + +test("a malformed manifest is skipped, not thrown", () => { + expect(() => loadPersonas()).not.toThrow(); +}); +``` + +- [ ] **Step 2: Run to verify fail** + +Run: `bun test tests/persona-registry-behaviour.test.ts` +Expected: FAIL - `personas/persona-registry.ts` missing (and the manifest, created in Part C, also missing - this test fully passes after Task C1; it asserts the registry mechanism here). + +- [ ] **Step 3a: Write the schema** + +`personas/persona.schema.json`: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Hyperstack Persona Manifest", + "type": "object", + "required": ["id", "name", "version", "owns", "engages_when", "gate_policy"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, + "name": { "type": "string" }, + "version": { "type": "string" }, + "owns": { + "type": "object", + "required": ["risks", "plugin", "skills", "agent"], + "properties": { + "risks": { "type": "array", "items": { "type": "string" } }, + "plugin": { "type": "string" }, + "skills": { "type": "array", "items": { "type": "string" } }, + "agent": { "type": "string" } + } + }, + "engages_when": { "type": "array", "items": { "type": "string" } }, + "gate_policy": { + "type": "object", + "required": ["net_new", "tweak", "override"], + "properties": { + "net_new": { "enum": ["hard", "advisory"] }, + "tweak": { "enum": ["hard", "advisory"] }, + "override": { "type": "string" } + } + } + } +} +``` + +- [ ] **Step 3b: Write the registry** + +`personas/persona-registry.ts`: + +```ts +import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const personasDir = dirname(fileURLToPath(import.meta.url)); + +export interface PersonaManifest { + id: string; + name: string; + version: string; + owns: { risks: string[]; plugin: string; skills: string[]; agent: string }; + engages_when: string[]; + gate_policy: { net_new: "hard" | "advisory"; tweak: "hard" | "advisory"; override: string }; +} + +function isValid(m: unknown): m is PersonaManifest { + const p = m as Partial; + return !!p && typeof p.id === "string" && !!p.owns && Array.isArray(p.owns.risks) && !!p.gate_policy; +} + +export function loadPersonas(): PersonaManifest[] { + const out: PersonaManifest[] = []; + for (const entry of readdirSync(personasDir)) { + const manifestPath = join(personasDir, entry, "persona.json"); + if (!existsSync(manifestPath) || !statSync(join(personasDir, entry)).isDirectory()) continue; + try { + const parsed = JSON.parse(readFileSync(manifestPath, "utf8")); + if (isValid(parsed)) out.push(parsed); + else console.error(`Persona manifest invalid, skipped: ${manifestPath}`); + } catch (err) { + console.error(`Persona manifest unreadable, skipped: ${manifestPath}:`, err); + } + } + return out; +} +``` + +- [ ] **Step 4: Run** + +Run: `bun test tests/persona-registry-behaviour.test.ts` +Expected: the "malformed skipped / not throw" test PASSES; the "loads product-manager" test FAILS until Task C1 creates the manifest. That is expected sequencing - leave it failing, it goes green in Part C. + +- [ ] **Step 5: Commit** + +```bash +git add personas/persona.schema.json personas/persona-registry.ts tests/persona-registry-behaviour.test.ts +git commit -m "feat(personas): manifest schema + resilient persona registry" +``` + +--- + +### Task B2: Bootstrap `Layer 4: Personas` (SKILL.md + compiler + regen) + +**Files:** +- Modify: `skills/hyperstack/SKILL.md` (add two sections) +- Modify: `src/internal/context-compiler.ts` (extract + emit + markers) +- Modify: `generated/runtime-context/hyperstack.bootstrap.md` (regenerated, do not hand-edit) +- Test: `tests/context-compiler-behaviour.test.ts` (append) + +- [ ] **Step 1: Append the failing test** + +In `tests/context-compiler-behaviour.test.ts`, extend the inline `source` fixture (add before the closing backtick of the template) with: + +``` +## Layer 4: Personas + +- product-manager - owns value+viability; hard-gates net-new builds + +## Persona Registry + +- product-manager - PM decision gate (plugin product-manager + skill pm-gate) +``` + +Then add an assertion in the first test after the existing `expect(content.length)...` line: + +```ts + expect(content).toMatch(/Personas/); + expect(content).toMatch(/product-manager/); +``` + +- [ ] **Step 2: Run to verify fail** + +Run: `bun test tests/context-compiler-behaviour.test.ts` +Expected: FAIL - compiler does not yet extract a Personas section, and the live SKILL.md/bootstrap sync test fails once you edit SKILL.md (Step 3b/3c order matters; do all edits then regen). + +- [ ] **Step 3a: Add sections to `skills/hyperstack/SKILL.md`** + +Insert after the `## Disallowed Transitions` section (before `## The Rationalization Catalog`): + +```markdown +## Layer 4: Personas + +Personas are judgment lenses that OWN a class of decision and gate it before +execution. They are internal and auto-engaged by `hyper`. + +| Persona | Owns | Gate | +|---|---|---| +| `product-manager` | value + viability product risk | hard for net-new builds, advisory for tweaks, user override always | + +## Persona Registry + +- `product-manager` - grounds build decisions in validated customer problems + (opportunity-vs-solution, four risks, RICE), owns value+viability, hands back + to `hyper`. Engaged before design/build on net-new feature/product/scope work. +``` + +- [ ] **Step 3b: Extend the compiler** (`src/internal/context-compiler.ts`) + +Add two markers to `REQUIRED_BOOTSTRAP_MARKERS` (after the `"website-builder"` entry): + +```ts + "Layer 4: Personas", + "product-manager", +``` + +In `compileUsingHyperstackBootstrap`, after the `roleRegistry` line, add: + +```ts + const personas = extractSimpleBullets(extractSection(body, "Persona Registry")); +``` + +In the `content` array, insert a Personas block after the Internal Roles block (after the `...roleRegistry,` group and its trailing `"",`): + +```ts + "## Personas", + "- Personas are internal judgment lenses that own and gate a decision class.", + ...personas, + "", +``` + +- [ ] **Step 3c: Regenerate the bootstrap artifact** + +Run: `bun run compile:context` +Expected stdout: "Compiled runtime context artifacts: - .../hyperstack.bootstrap.md" and a char-savings line. This rewrites `generated/runtime-context/hyperstack.bootstrap.md` so the sync test matches. + +- [ ] **Step 4: Run the full suite** + +Run: `bun test` +Expected: PASS, including `generated bootstrap artifact stays in sync` and the new Personas assertions. + +- [ ] **Step 5: Commit** + +```bash +git add skills/hyperstack/SKILL.md src/internal/context-compiler.ts generated/runtime-context/hyperstack.bootstrap.md tests/context-compiler-behaviour.test.ts +git commit -m "feat(personas): compile Layer 4 Personas into the runtime bootstrap" +``` + +**Part B checkpoint:** the bootstrap now carries a Personas layer; the registry loads manifests. Reviewable independently (PM-manifest test still pending Part C). + +--- + +# PART C - `pm-gate` skill + persona binding + router + +### Task C1: Persona manifest + role contracts + +**Files:** +- Create: `personas/product-manager/persona.json` +- Create: `personas/product-manager/PROFILE.md` +- Create: `personas/product-manager/LIFECYCLE.md` +- Create: `personas/product-manager/CHECKS.md` +- Create: `personas/README.md` + +- [ ] **Step 1: Create the manifest** `personas/product-manager/persona.json` + +```json +{ + "id": "product-manager", + "name": "Product Manager", + "version": "0.1.0", + "owns": { + "risks": ["value", "viability"], + "plugin": "product-manager", + "skills": ["pm-gate"], + "agent": "product-manager" + }, + "engages_when": ["net-new feature", "new product", "build request", "scope decision"], + "gate_policy": { "net_new": "hard", "tweak": "advisory", "override": "user-explicit" } +} +``` + +- [ ] **Step 2: Create `personas/product-manager/PROFILE.md`** + +```markdown +--- +id: product-manager +kind: persona +owns: + - value risk (will users choose it) + - viability risk (does it work for the business) +delegates_to: + - hyper +must_not_do: + - approve a net-new build without value + viability addressed + - ask customers what they want instead of eliciting past-behaviour stories + - let a solution masquerade as a validated problem +--- + +# Product Manager Persona + +## Mission + +Ground every build decision in a validated customer problem, make the prioritized +call, and own value + viability before design or engineering begins. + +## Voice + +Opinionated and evidence-backed. States a recommendation with a rationale, never +hedges with "here are five options". Cites the customer problem and the business +case. Says no to good ideas that are not the lever. + +## Authority + +- Owns the value and viability product risks (Cagan four-risks split). +- Engaged by `hyper` before specialists on net-new build/scope work. +- Hands back to `hyper` for routing, verification, and ship-gate. Never self-ships. +``` + +- [ ] **Step 3: Create `personas/product-manager/LIFECYCLE.md`** + +```markdown +# Product Manager Persona Lifecycle + +## Engage when +- net-new feature, new product, build request, or scope decision (hard gate) +- tweak/bugfix/refactor (advisory only) + +## Gate steps +1. Frame: `product_manager_opportunity_vs_solution` - reject solutions-in-disguise. +2. Ground: confirm customer evidence; flag opinion-requirements (`get_discovery_rules`). +3. Assess: `product_manager_get_four_risks` - especially value + viability. +4. Prioritize: `product_manager_score_rice` / strategy rules - state the one call. +5. Emit: `product_manager_resolve_product_decision` -> PASS | BLOCK | NEEDS-INPUT + rationale. + +## Verdicts +- PASS: value + viability addressed, prioritized call has a rationale -> hand to `hyper`. +- BLOCK: hard gate, a required risk unaddressed -> stop, report what is missing. +- NEEDS-INPUT: ambiguity -> ask the user; never silently pass. + +## Override +- User may explicitly say "skip PM" -> honour, log the override in the decision trail. + +## Handback +- Always return to `hyper` for routing and ship-gate. The persona never delivers. +``` + +- [ ] **Step 4: Create `personas/product-manager/CHECKS.md`** + +```markdown +# Product Manager Gate Checks (falsifiable) + +- [ ] Statement is an OPPORTUNITY, not a solution in disguise (more than one way to address it). +- [ ] Customer evidence exists; no requirement rests on "they said they want X". +- [ ] VALUE risk addressed: will users choose it? +- [ ] VIABILITY risk addressed: can the business sell/support/fund/legally ship it? +- [ ] A single prioritized call is stated, with a rationale (not a feature list). +- [ ] Scope cut: what is explicitly NOT being built and why. +``` + +- [ ] **Step 5: Create `personas/README.md`** + +```markdown +# Personas (Layer 4) + +Personas are judgment lenses that own and gate a class of decision before +execution. Each persona binds an MCP plugin (ground-truth), one or more skills +(process + gate), and a role identity via `persona.json`. The persona registry +(`persona-registry.ts`) loads manifests; the bootstrap compiles a Personas layer +so `hyper` knows which personas exist and when they engage. + +| Persona | Owns | First | +|---|---|---| +| `product-manager` | value + viability product risk | yes | +``` + +- [ ] **Step 6: Run the registry test (now goes green)** + +Run: `bun test tests/persona-registry-behaviour.test.ts` +Expected: PASS - both tests, including "loads the product-manager persona manifest". + +- [ ] **Step 7: Commit** + +```bash +git add personas/product-manager/ personas/README.md +git commit -m "feat(personas): product-manager manifest + role contracts" +``` + +--- + +### Task C2: `pm-gate` skill + +**Files:** +- Create: `skills/pm-gate/SKILL.md` +- Test: `tests/skills-index-behaviour.test.ts` (verify it still passes / regenerate index if needed) + +- [ ] **Step 1: Create `skills/pm-gate/SKILL.md`** + +```markdown +--- +name: pm-gate +description: Use BEFORE any net-new feature, product, or scope decision - the product-manager persona gate. Grounds the build in a validated customer problem (opportunity-vs-solution, four risks, prioritized call) and emits PASS/BLOCK/NEEDS-INPUT before design or engineering. Advisory for tweaks; user can override. +category: core +--- + +# PM Gate + +The product-manager persona's gate. It owns the value and viability product risks +that nothing else in Hyperstack owns. It runs BEFORE designer/blueprint on net-new +work. + +## Iron Law + +``` +NO NET-NEW BUILD WITHOUT A PASSED PRODUCT DECISION +(value + viability addressed, prioritized call stated with a rationale) +``` + +## When + +| Request | Gate | +|---|---| +| net-new feature / product / scope decision | HARD - must PASS before design/build | +| tweak / bugfix / refactor | ADVISORY - emit brief, do not block | +| user says "skip PM" | OVERRIDE - honour, log it | + +## Steps (MCP-grounded - call the tools, do not reason from memory) + +1. `product_manager_opportunity_vs_solution(statement)` - reject a solution-in-disguise. +2. `product_manager_get_discovery_rules()` - confirm customer evidence; flag opinion-requirements. +3. `product_manager_get_four_risks()` - assess all four, especially VALUE and VIABILITY. +4. `product_manager_score_rice(...)` and `product_manager_get_strategy_rules()` - state the one call, cut scope. +5. `product_manager_resolve_product_decision(description, isNetNew)` - emit the verdict. + +## Verdict handling + +- PASS -> hand to `hyper`; proceed to designer/blueprint. +- BLOCK -> stop; report the unaddressed risk; do not route to build. +- NEEDS-INPUT -> ask the user the specific missing question. + +## Red flags (from the corpus) + +- "A customer said they want X" used as a requirement -> opinion, not evidence. +- Reasoning covers value but is silent on viability -> the named weak-PM failure. +- "Strategy" that is a long feature list rejecting nothing -> not a strategy. +- Treating a strategy/interpersonal/culture problem as an execution problem. +``` + +- [ ] **Step 2: Regenerate the skills index (if the repo tracks it)** + +Run: `bun scripts/generate-skills-index.ts` (or `npm run skills:index`) +Expected: `skills/INDEX.md` regenerated to include `pm-gate` under Core. + +- [ ] **Step 3: Run the suite** + +Run: `bun test` +Expected: PASS - `skills-index-behaviour.test.ts` consistent with the regenerated index. + +- [ ] **Step 4: Commit** + +```bash +git add skills/pm-gate/SKILL.md skills/INDEX.md +git commit -m "feat(pm-gate): product-manager persona gate skill" +``` + +--- + +### Task C3: Router + transitions wiring + +**Files:** +- Modify: `harness/router.md` +- Modify: `harness/transitions.md` +- Test: `tests/role-harness-behaviour.test.ts` (run; update if it asserts an exact transition set) + +- [ ] **Step 1: Run the existing role-harness test to see current assertions** + +Run: `bun test tests/role-harness-behaviour.test.ts` +Expected: PASS (baseline before edits). Read it to learn whether it asserts an exact transition list - if it does, update the expected set in Step 3 to include the persona-gate line. + +- [ ] **Step 2: Add the persona-gate to `harness/router.md`** + +After the "## Default Rule" section, add: + +```markdown +## Persona Gate + +Before routing a net-new feature, product, or scope decision to any specialist, +`hyper` engages the `product-manager` persona gate (`pm-gate` skill). The gate must +return PASS before design/build. Tweaks/bugfixes get an advisory brief, not a block. +The user may explicitly override ("skip PM"), which is honoured and logged. +``` + +- [ ] **Step 3: Add transitions to `harness/transitions.md`** + +Under "## Allowed", add: + +```markdown +- `hyper -> product-manager persona gate (net-new build/scope)` +- `product-manager persona gate -> hyper` +``` + +Under "## Disallowed", add: + +```markdown +- `product-manager persona gate -> ship` (must hand back to hyper) +- `product-manager persona gate -> deliver` +``` + +- [ ] **Step 4: Run the full suite** + +Run: `bun test` +Expected: PASS. If `role-harness-behaviour.test.ts` pins an exact transition set, update its expected array to include the two new allowed lines. + +- [ ] **Step 5: Commit** + +```bash +git add harness/router.md harness/transitions.md tests/role-harness-behaviour.test.ts +git commit -m "feat(personas): wire product-manager gate into router + transitions" +``` + +**Part C checkpoint:** plugin + vertical + skill + role are bound; the PM gate is live in the routing contract. + +--- + +## Final verification + +- [ ] `bun test` - all suites green (registry 14, PM tools, bootstrap sync + Personas, persona registry, skills index). +- [ ] `bun run build` - `tsc --noEmit` clean. +- [ ] `bun run compile:context` - bootstrap regenerates with no diff (already committed). +- [ ] Manual: call `product_manager_resolve_product_decision({description:"add CSV export"})` and confirm a NEEDS-INPUT verdict naming value+viability. + +## Self-review notes (plan author) + +- **Spec coverage:** vertical (B) + plugin 9 tools (A) + skill + manifest + router (C) + tiered gate (C2/C3) + tests - all spec sections mapped. Four research-gap items remain Phase 2 (out of scope, per spec). +- **Type consistency:** tool names use `product_manager_*`; `data.ts` exports (`FOUR_RISKS`, `PM_OWNED_RISKS`, `classifyOpportunityVsSolution`, `validateJobStatement`, `scoreRice`) referenced identically across tasks. +- **Known coupling:** Task A4 bumps the plugin count test 13->14; Task B2 requires `compile:context` regen or the sync test fails. Both called out in-task. +- **Deferred/unverified (from spec):** the `product-manager` *agent* is realized here via persona contracts + `pm-gate` skill (prose), not a registered Claude Code subagent-type; if a discoverable agent-type is later required, that is a separate task pending verification of CC's agent-registration path. +``` \ No newline at end of file diff --git a/docs/superpowers/specs/2026-06-21-pm-persona-design.md b/docs/superpowers/specs/2026-06-21-pm-persona-design.md new file mode 100644 index 0000000..58e0848 --- /dev/null +++ b/docs/superpowers/specs/2026-06-21-pm-persona-design.md @@ -0,0 +1,206 @@ +# Design: Product-Manager Persona + `personas/` Vertical + +Hyperstack enforces *how to work* (Iron Laws, gates) and *how it looks/builds* (designer, engineering-discipline), but nothing owns *what to build and why*. This spec adds a fourth architectural vertical - `personas/` - and its first inhabitant, a `product-manager` persona that grounds build decisions in validated customer problems, prioritizes ruthlessly, and makes opinionated, evidence-backed calls. It bites as a tiered gate before design/build. The persona's rules are not invented: they are 11 framework rules confirmed by adversarial deep research against primary sources (Cagan/SVPG, Torres/Product Talk, Intercom, Christensen, Doshi), persisted at `docs/research/2026-06-21-pm-craft-corpus.json`. + +## Problem statement + +| Symptom (user-reported) | Root cause | Evidence | +|---|---|---| +| "Decisions are random gibberish" | No layer owns product VALUE - what to build is assumed, not validated | Four-Risks taxonomy: PM owns value+viability; Hyperstack has neither | +| "Not real PM-level calls" | No customer-behaviour grounding before work starts | Discovery is continuous + story-based; Hyperstack jumps to how-work | +| "Enforcement leaks" (decision quality, not mechanics) | Output hedges / sprawls; no prioritized, defensible call | "Strategy = saying no"; weak PMs ship everything | + +The four product risks map onto Hyperstack's roles and expose the exact hole: + +``` +RISK OWNER STATUS +────────────────────────────────────────────────────── +VALUE → Product Manager → MISSING ◄ the leak +VIABILITY → Product Manager → MISSING ◄ the leak +USABILITY → Designer (designer skill) → exists +FEASIBILITY → Engineer (eng-discipline) → exists +``` + +The PM persona completes the trio. It is not a bolt-on; it fills a structurally missing accountability. + +## Goals / Non-goals + +| Goals | Non-goals | +|---|---| +| Add `personas/` as a first-class 4th vertical, extensible to future personas | Not a parallel MCP transport - tools register through the existing `loadPlugins` path | +| Ship `product-manager` persona = bound MCP plugin + skill + agent role + manifest | Not project-specific product opinions - corpus is generic, source-cited PM craft (keeps core generic) | +| Tiered enforcement: hard-gate net-new, advisory tweaks, user override always | Not a replacement for designer/engineering-discipline - it precedes them | +| Ground every tool in a verified research claim; stub unproven areas as NEEDS-RESEARCH | Not faking the 4 research-gap areas (MVP-cut, Kano/MoSCoW/ICE, decision-template) | + +## Research foundation (corpus → tools) + +Every PM tool is backed by a confirmed claim. Full provenance in the persisted corpus. + +| Rule | Tool surface | Source (confidence) | +|---|---|---| +| Four Product Risks (value/usability/feasibility/viability); PM owns value+viability | `get_four_risks` | Cagan/SVPG (high) | +| Opportunity-vs-solution test: "more than one way to address this?" | `opportunity_vs_solution` | Torres (high) | +| JTBD job = progress-in-context (functional/emotional/social); Four Forces | `get_jtbd` | Christensen (high) | +| Job-statement validator (7-point falsifiable checklist) | `validate_job_statement` | gopractice/Christensen (high) | +| Story-based discovery; never ask "what do you want"; weekly contact | `get_discovery_rules` | Torres (high) | +| RICE = (Reach x Impact x Confidence) / Effort | `score_rice` | Intercom, RICE originator (high) | +| Strategy = saying no; focus 2-3 levers; long list = non-strategy | `get_strategy_rules` | Cagan/SVPG (high) | +| Anti-patterns: feature-factory, reactivity, viability-avoidance, execution-misdiagnosis (strategy/interpersonal/culture) | `get_anti_patterns` | Cagan + Doshi (high) | +| Orchestrator: description → grounded product decision + verdict | `resolve_product_decision` | composition of above | + +Killed claim (do not encode): "context is THE test for a real job" (1-2 refuted). Correction encoded: execution problems trace to strategy OR interpersonal OR culture (three roots, not one). + +## Architecture: the `personas/` vertical + +``` +hyperstack/ +├── src/plugins/ Layer 1 MCP ground-truth (data) +├── skills/ Layer 2 process / discipline +├── agents/ Layer 3 routing roles (who executes) +└── personas/ Layer 4 ◄ NEW: judgment lenses that OWN decisions + ├── README.md vertical overview + ├── persona-registry.ts loads persona.json manifests; exposes to compiler + router + ├── persona.schema.json manifest schema + └── product-manager/ + ├── persona.json manifest (see below) + ├── PROFILE.md identity, mission, owns value+viability, voice + ├── LIFECYCLE.md engage criteria, gate steps, handback to hyper + ├── CHECKS.md the falsifiable gate checklist + ├── CONTEXT.md context slice it loads + └── corpus/ framework snippets (distilled from research JSON) +``` + +### Persona manifest (`persona.json`) + +```json +{ + "id": "product-manager", + "name": "Product Manager", + "version": "0.1.0", + "owns": { + "risks": ["value", "viability"], + "plugin": "product-manager", + "skills": ["pm-gate"], + "agent": "product-manager" + }, + "engages_when": ["net-new feature", "new product", "build request", "scope decision"], + "gate_policy": { "net_new": "hard", "tweak": "advisory", "override": "user-explicit" } +} +``` + +### Bound components + +| Part | Location | Contract | +|---|---|---| +| MCP plugin `product-manager` | `src/plugins/product-manager/` (existing plugin pattern: `index.ts`/`loader.ts`/`data.ts`/`tools/*`/`snippets/*`) | 9 tools above; stateless; logically persona-owned via manifest | +| Skill `pm-gate` | `skills/pm-gate/SKILL.md` | the product-decision workflow + tiered gate; Iron Law: "NO NET-NEW BUILD WITHOUT A PASSED PRODUCT DECISION" | +| Agent role `product-manager` | bound via manifest; contracts under `personas/product-manager/` | owns value+viability; engaged by hyper; hands back to hyper | +| Binding | `personas/product-manager/persona.json` | ties plugin + skill + agent + corpus + voice | + +Pragmatic concession to the 4th-vertical choice: tool *code* lives in `src/plugins/` so it registers through the only verified MCP path (`src/index.ts` → `loadPlugins`). The persona *owns* it via manifest. Physical relocation under `personas/` is possible but requires a new loader wired into `src/index.ts`; deferred unless required. + +Unverified mechanism (resolve in the plan, do not assume here): how the `product-manager` agent becomes a discoverable agent-type - declared in the plugin manifest, auto-discovered from an `agents/` dir, or realized purely through the persona contracts + `pm-gate` skill - was not confirmed during codebase analysis. The plan must verify Claude Code's agent-registration path before committing to one. + +## Lifecycle - where the gate bites + +``` +user request + │ + ▼ +hyper ── classify ──► build / feature / product call? + │ │ + │ YES │ engage product-manager persona [GATE] + │ ▼ + │ ┌─────────────────────────────────────┐ + │ │ 1 GROUND opportunity_vs_solution │ reject solutions-as-problems + │ │ discovery evidence? │ flag "customer said they want X" + │ │ 2 ASSESS get_four_risks │ esp VALUE + VIABILITY + │ │ 3 PRIORITIZE score_rice / saying-no │ what is the ONE call + │ │ 4 EMIT resolve_product_decision│ PASS | BLOCK | NEEDS-INPUT + rationale + │ └─────────────────────────────────────┘ + │ │ PASS + └── NO (tweak/bug/refactor) ──► advisory brief (no block) + ▼ + hyper routes → designer → builder → ship-gate +``` + +### Tiered enforcement policy + +| Request class | Gate | Behaviour | +|---|---|---| +| Net-new feature / product / scope decision | HARD | must emit PASS before designer/build; BLOCK halts with reason | +| Tweak / bugfix / refactor / non-product | ADVISORY | brief emitted, no block | +| User explicit "skip PM" | OVERRIDE | always honoured, logged in the decision trail | + +## Build integration + +| File | Change | Risk | +|---|---|---| +| `skills/hyperstack/SKILL.md` | add `## Personas` section (bootstrap source of truth) | LOW | +| `src/internal/context-compiler.ts` | extract + emit Personas layer; extend `REQUIRED_BOOTSTRAP_MARKERS` | MED - marker-validated, covered by `context-compiler-behaviour.test.ts` | +| `generated/runtime-context/hyperstack.bootstrap.md` | regenerated via `bun run compile:context` | auto | +| `harness/router.md`, `harness/transitions.md` | add persona-gate step `hyper → persona-gate → specialist`; allowed/disallowed transitions | LOW (prose) | +| `personas/persona-registry.ts`, `persona.schema.json` | new, small | LOW | +| `personas/product-manager/**` | manifest + contracts + corpus | LOW | +| `src/plugins/product-manager/**` | new plugin, mirrors `designer` exactly | LOW | +| `src/index.ts` | import + register `productManagerPlugin` | LOW | +| `tests/` | persona-registry, pm-gate verdict, bootstrap-marker tests | LOW (pattern exists) | + +## Data flow + +``` +research JSON ──(distill, build-time/manual)──► src/plugins/product-manager/snippets/*.txt + │ createSnippetLoader("product-manager") +tool call ──► handler ──► data.ts (typed) + snippet(txt) ──► markdown decision/checklist +pm-gate skill ──► calls product_manager_* tools ──► applies gate_policy ──► PASS|BLOCK|NEEDS-INPUT +persona-registry ──► reads persona.json ──► feeds compiler (bootstrap) + router (engage_when) +``` + +## Error handling / degraded mode + +| Failure | Behaviour | +|---|---| +| MCP unavailable | pm-gate states "MCP unavailable", falls back to corpus text, flags decision as ungrounded (consistent with existing degraded-mode rule) | +| Persona manifest malformed | persona-registry skips it with a surfaced warning (not silent), like plugin-registry resilience | +| Tool references missing snippet | build-time corpus-integrity check (added) fails loud, not at runtime | +| Gate verdict ambiguous | default to NEEDS-INPUT (ask user), never silent PASS | + +## Testing + +Mirror existing `*-behaviour.test.ts`: +- persona-registry loads `product-manager` manifest; malformed manifest is skipped with warning. +- bootstrap compiles with Personas markers present (extends marker test). +- `pm-gate` returns BLOCK when four-risks viability is unaddressed; PASS when all four covered + prioritized call present; NEEDS-INPUT on ambiguity. +- `opportunity_vs_solution` returns is_solution for a single-path statement. +- `validate_job_statement` fails a statement missing context. +- override path bypasses gate and logs. + +## Phasing + +| Phase | Scope | +|---|---| +| **1 (this plan)** | `personas/` scaffold + registry + schema; `product-manager` plugin (9 verified tools); `pm-gate` skill (tiered); bootstrap + router wiring; tests. Gap areas ship as explicit NEEDS-RESEARCH stubs. | +| **2 (later)** | targeted second research pass for the 4 gaps (MVP-cut mechanics, Kano/MoSCoW/ICE, product-sense practices, decision-writeup template); add decision-log template tool. | + +## Open items (research-flagged gaps, not faked) + +1. MVP / minimum-scope cut rule - no codifiable line yet. +2. Kano / MoSCoW / ICE / opportunity-sizing mechanics - only RICE survived verification. +3. Repeatable product-sense-building practices. +4. Defensible decision-writeup template (pre-mortem / decision-log format). + +## Future personas (why the vertical, not a one-off) + +The vertical is justified only if more personas follow. Candidates that fit the same "judgment lens that owns a risk" shape: a security persona (owns risk/threat), a data/analytics persona (owns measurement), an SRE persona (owns reliability). Each = manifest + bound plugin/skill. The registry, schema, and bootstrap layer built here are shared infrastructure. + +## Validation pass (post-implementation correction) + +A secondary validation (1 empirical accuracy battery + 2 independent adversarial red-team agents) found that 3 of the 9 tools as first built were theater, and corrected them. This supersedes the earlier descriptions of those tools above: + +| Tool | As-built flaw | Corrected to | +|---|---|---| +| `resolve_product_decision` | hardcoded `NEEDS-INPUT`, never PASS/BLOCK - the "gate engine" could not gate | takes value+viability assessments as input; deterministic verdict PASS (both addressed) / BLOCK (net-new, missing) / ADVISORY (tweak). PASS earned, not automatic. | +| `opportunity_vs_solution` | ~80% regex classifier emitting an exclusive verdict (the LLM does this better) | returns the Torres rubric + reframe examples for the agent to apply (ground-truth, no verdict) | +| `validate_job_statement` | 3-regex pseudo-validator | returns the 7-point JTBD criteria for the agent to judge against | + +Also corrected: `persona.json` dropped the dangling `agent: product-manager` claim (no such agent exists; the persona is `engaged_by` hyper and realized via the `pm-gate` skill), and LIFECYCLE dropped an unimplemented "decision trail" claim. The 6 framework tools (`get_*` + `score_rice`) were sound and unchanged. Verdict labels are now PASS/BLOCK/ADVISORY (not NEEDS-INPUT). A runtime write-blocking hook was deliberately declined: all Hyperstack skills are prose-enforced via the bootstrap, and the diagnosed problem was decision quality, not enforcement mechanics. diff --git a/generated/runtime-context/hyperstack.bootstrap.md b/generated/runtime-context/hyperstack.bootstrap.md index 87137d0..a5310a3 100644 --- a/generated/runtime-context/hyperstack.bootstrap.md +++ b/generated/runtime-context/hyperstack.bootstrap.md @@ -78,6 +78,10 @@ Hyperstack is a **Three-Layer Ecosystem**: - `hyper` - conductor, classifier, gatekeeper, verifier, and delivery owner - `website-builder` - first specialist for website-facing design and +## Personas +- Personas are internal judgment lenses that own and gate a decision class. +- `product-manager` - grounds build decisions in validated customer problems + ## Routing Summary - Every request enters through `hyper` - `hyper` inspects the workspace first: package manifests, dependency signals, diff --git a/harness/router.md b/harness/router.md index f04c611..35cf828 100644 --- a/harness/router.md +++ b/harness/router.md @@ -6,6 +6,13 @@ Every user request enters through `hyper`. Users do not invoke internal roles directly. Roles are internal and auto-called. +## Persona Gate + +Before routing a net-new feature, product, or scope decision to any specialist, +`hyper` engages the `product-manager` persona gate (`pm-gate` skill). The gate must +return PASS before design/build. Tweaks/bugfixes get an advisory brief, not a block. +The user may explicitly override ("skip PM"), which is honoured and logged. + ## Routing Matrix Route `hyper -> website-builder` when the request is primarily about: diff --git a/harness/transitions.md b/harness/transitions.md index a023723..310d5fc 100644 --- a/harness/transitions.md +++ b/harness/transitions.md @@ -7,6 +7,8 @@ - `website-builder -> hyper` - `hyper -> existing Hyperstack skills/plugins` - `hyper -> verification and delivery gates` +- `hyper -> product-manager persona gate (net-new build/scope)` +- `product-manager persona gate -> hyper` ## Disallowed @@ -14,6 +16,8 @@ - `website-builder -> ship` - `website-builder -> deliver` - `website-builder` claiming final completion directly +- `product-manager persona gate -> ship` (must hand back to hyper) +- `product-manager persona gate -> deliver` ## V1 Principle diff --git a/personas/README.md b/personas/README.md new file mode 100644 index 0000000..bc2bb4c --- /dev/null +++ b/personas/README.md @@ -0,0 +1,27 @@ +# Personas (Layer 4) + +Personas are judgment lenses that own and gate a class of decision before +execution. Each persona binds an MCP plugin (ground-truth), one or more skills +(process + gate), and a role identity via `persona.json`. The persona registry +(`src/personas/registry.ts`) loads manifests; the bootstrap compiles a Personas +layer so `hyper` knows which personas exist and when they engage. + +| Persona | Owns | First | +|---|---|---| +| `product-manager` | value + viability product risk | yes | + +## Anatomy of a persona + +``` +personas/ + persona.schema.json manifest contract + / + persona.json binds plugin + skills + agent + gate_policy + PROFILE.md identity, mission, voice + LIFECYCLE.md engage criteria, gate steps, handback + CHECKS.md the falsifiable gate checklist +``` + +The bound plugin lives under `src/plugins//` and the skill under +`skills//` (registered through their own ecosystems); the persona owns +them logically via the manifest, it does not physically contain them. diff --git a/personas/persona.schema.json b/personas/persona.schema.json new file mode 100644 index 0000000..131916f --- /dev/null +++ b/personas/persona.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Hyperstack Persona Manifest", + "type": "object", + "required": ["id", "name", "version", "owns", "engages_when", "gate_policy"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, + "name": { "type": "string" }, + "version": { "type": "string" }, + "owns": { + "type": "object", + "required": ["risks", "plugin", "skills"], + "properties": { + "risks": { "type": "array", "items": { "type": "string" } }, + "plugin": { "type": "string" }, + "skills": { "type": "array", "items": { "type": "string" } } + } + }, + "engages_when": { "type": "array", "items": { "type": "string" } }, + "gate_policy": { + "type": "object", + "required": ["net_new", "tweak", "override"], + "properties": { + "net_new": { "enum": ["hard", "advisory"] }, + "tweak": { "enum": ["hard", "advisory"] }, + "override": { "type": "string" } + } + } + } +} diff --git a/personas/product-manager/CHECKS.md b/personas/product-manager/CHECKS.md new file mode 100644 index 0000000..65998e5 --- /dev/null +++ b/personas/product-manager/CHECKS.md @@ -0,0 +1,8 @@ +# Product Manager Gate Checks (falsifiable) + +- [ ] Statement is an OPPORTUNITY, not a solution in disguise (more than one way to address it). +- [ ] Customer evidence exists; no requirement rests on "they said they want X". +- [ ] VALUE risk addressed: will users choose it? +- [ ] VIABILITY risk addressed: can the business sell/support/fund/legally ship it? +- [ ] A single prioritized call is stated, with a rationale (not a feature list). +- [ ] Scope cut: what is explicitly NOT being built and why. diff --git a/personas/product-manager/LIFECYCLE.md b/personas/product-manager/LIFECYCLE.md new file mode 100644 index 0000000..cfe4e68 --- /dev/null +++ b/personas/product-manager/LIFECYCLE.md @@ -0,0 +1,23 @@ +# Product Manager Persona Lifecycle + +## Engage when +- net-new feature, new product, build request, or scope decision (hard gate) +- tweak/bugfix/refactor (advisory only) + +## Gate steps +1. Frame: `product_manager_opportunity_vs_solution` - apply the returned rubric to reframe a solution into the underlying need. +2. Ground: confirm customer evidence; flag opinion-requirements (`product_manager_get_discovery_rules`). +3. Assess: `product_manager_get_four_risks` - then write your value + viability assessments. +4. Prioritize: `product_manager_score_rice` / strategy rules - state the one call. +5. Decide: `product_manager_resolve_product_decision(description, valueAssessment, viabilityAssessment, isNetNew)` - returns PASS only when you supplied both assessments. + +## Verdicts +- PASS: value + viability assessed -> hand to `hyper`; proceed to design/build. +- BLOCK: net-new build, a PM-owned risk unaddressed -> assess it and call again; do not route to build. +- ADVISORY: tweak with a gap -> proceed allowed, gap noted. + +## Override +- User may explicitly say "skip PM" -> honour it and note the override in your response. + +## Handback +- Always return to `hyper` for routing and ship-gate. The persona never delivers. diff --git a/personas/product-manager/PROFILE.md b/personas/product-manager/PROFILE.md new file mode 100644 index 0000000..55aa258 --- /dev/null +++ b/personas/product-manager/PROFILE.md @@ -0,0 +1,37 @@ +--- +name: product-manager +kind: persona +auto_invoke_when: + - net-new feature + - new product + - build request + - scope decision +owns: + - value risk (will users choose it) + - viability risk (does it work for the business) +must_not_do: + - approve a net-new build without value + viability addressed + - ask customers what they want instead of eliciting past-behaviour stories + - let a solution masquerade as a validated problem +delegates_to: + - hyper +--- + +# Product Manager Persona + +## Mission + +Ground every build decision in a validated customer problem, make the prioritized +call, and own value + viability before design or engineering begins. + +## Voice + +Opinionated and evidence-backed. States a recommendation with a rationale, never +hedges with "here are five options". Cites the customer problem and the business +case. Says no to good ideas that are not the lever. + +## Authority + +- Owns the value and viability product risks (Cagan four-risks split). +- Engaged by `hyper` before specialists on net-new build/scope work. +- Hands back to `hyper` for routing, verification, and ship-gate. Never self-ships. diff --git a/personas/product-manager/persona.json b/personas/product-manager/persona.json new file mode 100644 index 0000000..a856669 --- /dev/null +++ b/personas/product-manager/persona.json @@ -0,0 +1,13 @@ +{ + "id": "product-manager", + "name": "Product Manager", + "version": "0.1.0", + "owns": { + "risks": ["value", "viability"], + "plugin": "product-manager", + "skills": ["pm-gate"] + }, + "engaged_by": "hyper", + "engages_when": ["net-new feature", "new product", "build request", "scope decision"], + "gate_policy": { "net_new": "hard", "tweak": "advisory", "override": "user-explicit" } +} diff --git a/scripts/audit/sources.ts b/scripts/audit/sources.ts index 33dd6ae..97f8e39 100644 --- a/scripts/audit/sources.ts +++ b/scripts/audit/sources.ts @@ -41,5 +41,6 @@ export const SOURCES: PluginSource[] = [ { plugin: "ui-ux", editorial: true, skip: false, skills: [], packages: [] }, { plugin: "designer", editorial: true, skip: false, skills: ["designer"], packages: [] }, { plugin: "optimizer", editorial: true, skip: false, skills: ["optimizer"], packages: [] }, + { plugin: "product-manager", editorial: true, skip: false, skills: ["pm-gate"], packages: [] }, { plugin: "hyperstack", editorial: false, skip: true, skills: [], packages: [] }, ]; diff --git a/skills/INDEX.md b/skills/INDEX.md index e2aa616..c488852 100644 --- a/skills/INDEX.md +++ b/skills/INDEX.md @@ -25,6 +25,7 @@ Categories: | `lab` | Use when designing or revamping a frontend section or whole page and you want to explore real-React variants in an isola | | `optimizer` | Teaches runtime analysis - deriving Big-O straight from code - and how to derive a better algorithm by removing redundan | | `parallel-dispatch` | Use when facing 2+ independent tasks that can be investigated or executed without shared state or sequential dependencie | +| `pm-gate` | Use BEFORE any net-new feature, product, or scope decision - the product-manager persona gate. Grounds the build in a va | | `run-plan` | Use when you have an existing plan, spec, or task list to execute. Validates the plan for gaps and MCP accuracy before a | | `ship-gate` | Use before claiming any work is complete, fixed, or passing. Run the verification command and show output before making | | `subagent-ops` | Use when executing implementation plans with independent tasks. Dispatches one fresh subagent per task with two-stage re | diff --git a/skills/hyperstack/SKILL.md b/skills/hyperstack/SKILL.md index adf9f19..7583e0e 100644 --- a/skills/hyperstack/SKILL.md +++ b/skills/hyperstack/SKILL.md @@ -246,6 +246,23 @@ The bootstrap and orchestrator (`hyper`) choose the correct role based on the re --- +## Layer 4: Personas + +Personas are judgment lenses that OWN a class of decision and gate it before +execution. They are internal and auto-engaged by `hyper`. + +| Persona | Owns | Gate | +|---|---|---| +| `product-manager` | value + viability product risk | hard for net-new builds, advisory for tweaks, user override always | + +## Persona Registry + +- `product-manager` - grounds build decisions in validated customer problems + (opportunity-vs-solution, four risks, RICE), owns value+viability, hands back + to `hyper`. Engaged before design/build on net-new feature/product/scope work. + +--- + ## The Rationalization Catalog (Read Before Every Session) These are the exact thoughts you will have when you want to skip a skill. Every one is a bug in your reasoning. Every one has been written down because someone (probably you in a past session) used it to ship bad code. diff --git a/skills/pm-gate/SKILL.md b/skills/pm-gate/SKILL.md new file mode 100644 index 0000000..ad51052 --- /dev/null +++ b/skills/pm-gate/SKILL.md @@ -0,0 +1,47 @@ +--- +name: pm-gate +description: Use BEFORE any net-new feature, product, or scope decision - the product-manager persona gate. Grounds the build in a validated customer problem (opportunity-vs-solution, four risks, prioritized call) and emits PASS/BLOCK/NEEDS-INPUT before design or engineering. Advisory for tweaks; user can override. +category: core +--- + +# PM Gate + +The product-manager persona's gate. It owns the value and viability product risks +that nothing else in Hyperstack owns. It runs BEFORE designer/blueprint on net-new +work. + +## Iron Law + +``` +NO NET-NEW BUILD WITHOUT A PASSED PRODUCT DECISION +(value + viability addressed, prioritized call stated with a rationale) +``` + +## When + +| Request | Gate | +|---|---| +| net-new feature / product / scope decision | HARD - must PASS before design/build | +| tweak / bugfix / refactor | ADVISORY - emit brief, do not block | +| user says "skip PM" | OVERRIDE - honour, log it | + +## Steps (MCP-grounded - call the tools, do not reason from memory) + +1. `product_manager_opportunity_vs_solution(statement)` - apply the returned rubric to reframe a solution into the underlying need. The tool gives the test, you make the call. +2. `product_manager_get_discovery_rules()` - confirm customer evidence; flag opinion-requirements. +3. `product_manager_get_four_risks()` - then WRITE your value and viability assessments (cite the customer problem and the business case). +4. `product_manager_score_rice(...)` and `product_manager_get_strategy_rules()` - state the one call, cut scope. +5. `product_manager_resolve_product_decision(description, valueAssessment, viabilityAssessment, isNetNew)` - returns PASS only if you supplied both assessments, else BLOCK (net-new) or ADVISORY (tweak). + +## Verdict handling + +- PASS -> hand to `hyper`; proceed to designer/blueprint. +- BLOCK -> assess the missing PM-owned risk and call again; do not route to build. +- ADVISORY -> tweak with a noted gap; proceed allowed. + +## Red flags (from the corpus) + +- "A customer said they want X" used as a requirement -> opinion, not evidence. +- Reasoning covers value but is silent on viability -> the named weak-PM failure. +- "Strategy" that is a long feature list rejecting nothing -> not a strategy. +- Treating a strategy/interpersonal/culture problem as an execution problem. diff --git a/src/index.ts b/src/index.ts index b0648fc..36cca20 100755 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { designerPlugin } from "./plugins/designer/index.js"; import { shadcnPlugin } from "./plugins/shadcn/index.js"; import { hyperstackPlugin } from "./plugins/hyperstack/index.js"; import { optimizerPlugin } from "./plugins/optimizer/index.js"; +import { productManagerPlugin } from "./plugins/product-manager/index.js"; import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -44,6 +45,7 @@ export const allPlugins = [ shadcnPlugin, hyperstackPlugin, optimizerPlugin, + productManagerPlugin, ]; loadPlugins(server, allPlugins); diff --git a/src/internal/context-compiler.ts b/src/internal/context-compiler.ts index ab73278..e7eb3e1 100644 --- a/src/internal/context-compiler.ts +++ b/src/internal/context-compiler.ts @@ -45,6 +45,8 @@ const REQUIRED_BOOTSTRAP_MARKERS = [ "announce it", "hyper", "website-builder", + "Personas", + "product-manager", "auto-called", "hyper -> website-builder", ]; @@ -168,6 +170,7 @@ export function compileUsingHyperstackBootstrap(source: string): { content: stri const workflowSkills = compactWorkflowTables(body); const instructionPriority = extractInstructionPriority(body); const roleRegistry = extractSimpleBullets(extractSection(body, "Role Registry")); + const personaRegistry = extractSimpleBullets(extractSection(body, "Persona Registry")); const routingSummary = extractSimpleBullets(extractSection(body, "Routing Summary")); const allowedTransitions = extractSimpleBullets(extractSection(body, "Allowed Transitions")); const disallowedTransitions = extractSimpleBullets(extractSection(body, "Disallowed Transitions")); @@ -199,6 +202,10 @@ export function compileUsingHyperstackBootstrap(source: string): { content: stri "- Roles are internal and auto-called. Users do not invoke them directly.", ...roleRegistry, "", + "## Personas", + "- Personas are internal judgment lenses that own and gate a decision class.", + ...personaRegistry, + "", "## Routing Summary", ...routingSummary, "", diff --git a/src/personas/registry.ts b/src/personas/registry.ts new file mode 100644 index 0000000..57c3f76 --- /dev/null +++ b/src/personas/registry.ts @@ -0,0 +1,45 @@ +import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Persona manifests live in the top-level personas/ vertical (content), loaded +// from src/ here the same way the context-compiler reads top-level skills/. +const personasDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "personas"); + +export interface PersonaManifest { + id: string; + name: string; + version: string; + owns: { risks: string[]; plugin: string; skills: string[] }; + engaged_by?: string; + engages_when: string[]; + gate_policy: { net_new: "hard" | "advisory"; tweak: "hard" | "advisory"; override: string }; +} + +function isValid(m: unknown): m is PersonaManifest { + const p = m as Partial; + return ( + !!p && + typeof p.id === "string" && + !!p.owns && + Array.isArray(p.owns.risks) && + !!p.gate_policy + ); +} + +export function loadPersonas(): PersonaManifest[] { + const out: PersonaManifest[] = []; + for (const entry of readdirSync(personasDir)) { + const dir = join(personasDir, entry); + const manifestPath = join(dir, "persona.json"); + if (!existsSync(manifestPath) || !statSync(dir).isDirectory()) continue; + try { + const parsed = JSON.parse(readFileSync(manifestPath, "utf8")); + if (isValid(parsed)) out.push(parsed); + else console.error(`Persona manifest invalid, skipped: ${manifestPath}`); + } catch (err) { + console.error(`Persona manifest unreadable, skipped: ${manifestPath}:`, err); + } + } + return out; +} diff --git a/src/plugins/product-manager/data.ts b/src/plugins/product-manager/data.ts new file mode 100644 index 0000000..9a19d8b --- /dev/null +++ b/src/plugins/product-manager/data.ts @@ -0,0 +1,106 @@ +// Product-Manager persona - typed structure + the one piece of legitimate gate logic. +// +// PHILOSOPHY (see optimizer/data.ts): tools provide GROUND-TRUTH the LLM should +// recall and apply, NOT classification/judgment the LLM does better itself. +// Earlier versions shipped regex "classifiers" (opportunity-vs-solution, +// job-statement validation) that emitted exclusive verdicts - those were brittle +// theater and were removed. The agent applies the rubrics (snippets/rubrics/*.txt); +// the only logic kept here is deterministic STRUCTURE checks, not judgment. +// +// Corpus prose lives in snippets/*.txt via createSnippetLoader, identical to the +// other plugins. Source-cited PM craft (Cagan/SVPG, Torres, Intercom, Christensen, +// Doshi), transcribed from docs/research/2026-06-21-pm-craft-corpus.json. + +import { snippet } from "./loader.js"; + +export interface ProductRisk { + id: "value" | "usability" | "feasibility" | "viability"; + question: string; + owner: "product-manager" | "designer" | "engineer"; +} + +export const FOUR_RISKS: ProductRisk[] = [ + { id: "value", question: snippet("risks/value.txt"), owner: "product-manager" }, + { id: "usability", question: snippet("risks/usability.txt"), owner: "designer" }, + { id: "feasibility", question: snippet("risks/feasibility.txt"), owner: "engineer" }, + { id: "viability", question: snippet("risks/viability.txt"), owner: "product-manager" }, +]; + +export const PM_OWNED_RISKS: ReadonlySet = new Set( + FOUR_RISKS.filter((r) => r.owner === "product-manager").map((r) => r.id), +); + +export interface AntiPattern { + id: string; + smell: string; + source: string; +} + +const ANTI_PATTERN_META: { id: string; source: string }[] = [ + { id: "feature-factory", source: "Cagan/SVPG" }, + { id: "reactivity", source: "Cagan/SVPG" }, + { id: "viability-avoidance", source: "Cagan/SVPG" }, + { id: "execution-misdiagnosis", source: "Doshi" }, + { id: "opinion-requirement", source: "Torres / The Mom Test" }, +]; + +export const ANTI_PATTERNS: AntiPattern[] = ANTI_PATTERN_META.map((m) => ({ + id: m.id, + source: m.source, + smell: snippet(`anti-patterns/${m.id}.txt`), +})); + +export const DISCOVERY_DOC: string = snippet("discovery/rules.txt"); +export const STRATEGY_DOC: string = snippet("strategy/rules.txt"); +export const JTBD_DOC: string = snippet("jtbd/jtbd.txt"); + +// Rubrics the AGENT applies (ground truth, not a verdict the tool computes). +export const OPPORTUNITY_RUBRIC_DOC: string = snippet("rubrics/opportunity-vs-solution.txt"); +export const JOB_CRITERIA_DOC: string = snippet("rubrics/job-criteria.txt"); + +// --- The one legitimate piece of logic: a deterministic STRUCTURE gate. --- +// This does NOT judge whether the value/viability reasoning is good (that is the +// agent's job, supplied as input). It enforces the discipline: a net-new build +// cannot PASS unless the PM-owned risks (value AND viability) were actually +// addressed. A presence/substance check, not a judgment call. + +export interface DecisionInput { + description: string; + valueAssessment?: string; + viabilityAssessment?: string; + isNetNew?: boolean; +} + +export interface Decision { + verdict: "PASS" | "BLOCK" | "ADVISORY"; + gate: "HARD" | "ADVISORY"; + missing: string[]; +} + +const MIN_ASSESSMENT_CHARS = 20; + +function addressed(text?: string): boolean { + return typeof text === "string" && text.trim().length >= MIN_ASSESSMENT_CHARS; +} + +export function computeDecision(input: DecisionInput): Decision { + const gate: Decision["gate"] = input.isNetNew === false ? "ADVISORY" : "HARD"; + const missing: string[] = []; + if (!addressed(input.valueAssessment)) missing.push("value"); + if (!addressed(input.viabilityAssessment)) missing.push("viability"); + + let verdict: Decision["verdict"]; + if (missing.length === 0) verdict = "PASS"; + else if (gate === "ADVISORY") verdict = "ADVISORY"; + else verdict = "BLOCK"; + + return { verdict, gate, missing }; +} + +export interface RiceInput { reach: number; impact: number; confidence: number; effort: number; } +export interface RiceResult { score: number; formula: string; } + +export function scoreRice(i: RiceInput): RiceResult { + const score = (i.reach * i.impact * i.confidence) / i.effort; + return { score, formula: "(Reach x Impact x Confidence) / Effort" }; +} diff --git a/src/plugins/product-manager/index.ts b/src/plugins/product-manager/index.ts new file mode 100644 index 0000000..c31f30f --- /dev/null +++ b/src/plugins/product-manager/index.ts @@ -0,0 +1,28 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as getFourRisks } from "./tools/get-four-risks.js"; +import { register as getJtbd } from "./tools/get-jtbd.js"; +import { register as getDiscoveryRules } from "./tools/get-discovery-rules.js"; +import { register as getAntiPatterns } from "./tools/get-anti-patterns.js"; +import { register as getStrategyRules } from "./tools/get-strategy-rules.js"; +import { register as opportunityVsSolution } from "./tools/opportunity-vs-solution.js"; +import { register as validateJobStatement } from "./tools/validate-job-statement.js"; +import { register as scoreRice } from "./tools/score-rice.js"; +import { register as resolveProductDecision } from "./tools/resolve-product-decision.js"; + +function register(server: McpServer): void { + getFourRisks(server); + getJtbd(server); + getDiscoveryRules(server); + getAntiPatterns(server); + getStrategyRules(server); + opportunityVsSolution(server); + validateJobStatement(server); + scoreRice(server); + resolveProductDecision(server); +} + +export const productManagerPlugin: Plugin = { + name: "product-manager", + register, +}; diff --git a/src/plugins/product-manager/loader.ts b/src/plugins/product-manager/loader.ts new file mode 100644 index 0000000..2ee74d6 --- /dev/null +++ b/src/plugins/product-manager/loader.ts @@ -0,0 +1,3 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; + +export const snippet = createSnippetLoader("product-manager"); diff --git a/src/plugins/product-manager/snippets/anti-patterns/execution-misdiagnosis.txt b/src/plugins/product-manager/snippets/anti-patterns/execution-misdiagnosis.txt new file mode 100644 index 0000000..9a84594 --- /dev/null +++ b/src/plugins/product-manager/snippets/anti-patterns/execution-misdiagnosis.txt @@ -0,0 +1 @@ +Treating a strategy, interpersonal, OR culture problem as an execution problem and band-aiding it. \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/anti-patterns/feature-factory.txt b/src/plugins/product-manager/snippets/anti-patterns/feature-factory.txt new file mode 100644 index 0000000..22cd9bf --- /dev/null +++ b/src/plugins/product-manager/snippets/anti-patterns/feature-factory.txt @@ -0,0 +1 @@ +Shipping as many stakeholder features as possible. Cagan: that is 'really no product strategy at all.' \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/anti-patterns/opinion-requirement.txt b/src/plugins/product-manager/snippets/anti-patterns/opinion-requirement.txt new file mode 100644 index 0000000..8c25eb9 --- /dev/null +++ b/src/plugins/product-manager/snippets/anti-patterns/opinion-requirement.txt @@ -0,0 +1 @@ +A requirement justified by 'a customer said they want X' (opinion/hypothetical) instead of an observed past-behaviour story. \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/anti-patterns/reactivity.txt b/src/plugins/product-manager/snippets/anti-patterns/reactivity.txt new file mode 100644 index 0000000..0d28b11 --- /dev/null +++ b/src/plugins/product-manager/snippets/anti-patterns/reactivity.txt @@ -0,0 +1 @@ +Driven by reacting to sales, competitors, requests, or price pressure instead of a product vision. \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/anti-patterns/viability-avoidance.txt b/src/plugins/product-manager/snippets/anti-patterns/viability-avoidance.txt new file mode 100644 index 0000000..39d535e --- /dev/null +++ b/src/plugins/product-manager/snippets/anti-patterns/viability-avoidance.txt @@ -0,0 +1 @@ +Reasoning covers value/desirability but is silent on whether the business can sell, support, fund, or legally ship it. \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/discovery/rules.txt b/src/plugins/product-manager/snippets/discovery/rules.txt new file mode 100644 index 0000000..02e8537 --- /dev/null +++ b/src/plugins/product-manager/snippets/discovery/rules.txt @@ -0,0 +1,4 @@ +- Engage customers continuously (aspirationally weekly); minimize build decisions made without customer evidence. +- Never ask customers what they want or need - cognitive biases make opinion answers unreliable. +- Elicit needs, pains, and desires through specific PAST-BEHAVIOUR stories. +- Demographics do not predict intent; the JOB the customer hires the product for does. \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/jtbd/jtbd.txt b/src/plugins/product-manager/snippets/jtbd/jtbd.txt new file mode 100644 index 0000000..6c0d0fa --- /dev/null +++ b/src/plugins/product-manager/snippets/jtbd/jtbd.txt @@ -0,0 +1,9 @@ +A job is the PROGRESS a person wants to make under specific circumstances - a process, not a single action. + +**Dimensions:** functional, emotional, social + +## Four Forces of Progress +- Push: frustration with the status quo (F1); attraction to the new solution (F2) +- Resist: anxiety of learning the new (F3); habit/comfort of the current solution (F4) + +**A switch happens only when Push + Pull > Anxiety + Habit.** \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/risks/feasibility.txt b/src/plugins/product-manager/snippets/risks/feasibility.txt new file mode 100644 index 0000000..c8c31ee --- /dev/null +++ b/src/plugins/product-manager/snippets/risks/feasibility.txt @@ -0,0 +1 @@ +Can engineers build it with available time, skills, and tech? \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/risks/usability.txt b/src/plugins/product-manager/snippets/risks/usability.txt new file mode 100644 index 0000000..40f72a2 --- /dev/null +++ b/src/plugins/product-manager/snippets/risks/usability.txt @@ -0,0 +1 @@ +Can users figure out how to use it? \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/risks/value.txt b/src/plugins/product-manager/snippets/risks/value.txt new file mode 100644 index 0000000..2f14648 --- /dev/null +++ b/src/plugins/product-manager/snippets/risks/value.txt @@ -0,0 +1 @@ +Will customers buy it, or will users choose to use it? \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/risks/viability.txt b/src/plugins/product-manager/snippets/risks/viability.txt new file mode 100644 index 0000000..7694a5f --- /dev/null +++ b/src/plugins/product-manager/snippets/risks/viability.txt @@ -0,0 +1 @@ +Does it work for the business - legal, sales, finance, marketing, support? \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/rubrics/job-criteria.txt b/src/plugins/product-manager/snippets/rubrics/job-criteria.txt new file mode 100644 index 0000000..d95d7d2 --- /dev/null +++ b/src/plugins/product-manager/snippets/rubrics/job-criteria.txt @@ -0,0 +1,15 @@ +Judge a JTBD job statement against these 7 criteria yourself (Christensen / Ulwick). This is the rubric, not a pass/fail machine. + +A valid job statement: +1. Is NOT a trend or fad. +2. Is NOT a vague high-level need ("be productive"). +3. Is NOT tied to one product class or technology. +4. Does NOT imply a single solution. +5. States the PROGRESS the person wants to make. +6. Includes the CONTEXT - the circumstances, external and emotional. +7. Sits at an appropriate level of abstraction (not too broad, not a feature). + +Good: "When I'm commuting and bored, help me feel productive without needing focus." +Weak: "I want a faster app." (no context, no progress, names a property not a job) + +Form: "When [situation], I want to [progress], so I can [outcome]." \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/rubrics/opportunity-vs-solution.txt b/src/plugins/product-manager/snippets/rubrics/opportunity-vs-solution.txt new file mode 100644 index 0000000..1bc5433 --- /dev/null +++ b/src/plugins/product-manager/snippets/rubrics/opportunity-vs-solution.txt @@ -0,0 +1,15 @@ +Apply this test yourself to any build request (Torres). This is the rubric, not a verdict. + +ASK: "Is there more than one way to address this?" +- Only ONE way exists -> it is a SOLUTION in disguise. Reframe to the underlying need. +- Several ways exist -> it is an OPPORTUNITY. Keep it. + +Also check: does the statement name a MECHANISM (a verb of construction - add / build / integrate / implement - or a specific UI element) or a desired OUTCOME / need? Run the 5 Whys to reach the need beneath a proposed solution. + +Reframe examples (solution -> opportunity): +- "add a dark mode toggle" -> "reduce eye strain when using the app in low light" +- "build a Slack integration" -> "let users act on alerts without leaving their workflow" +- "add a CSV export button" -> "let users get their data into their own analysis tools" +- "implement SSO" -> "let enterprise users sign in without managing another password" + +Note: "add", "build", "integrate" are not always solutions - "build trust with new users" is a need. Judge by structure (mechanism vs outcome), not by keywords. You make the call. \ No newline at end of file diff --git a/src/plugins/product-manager/snippets/strategy/rules.txt b/src/plugins/product-manager/snippets/strategy/rules.txt new file mode 100644 index 0000000..efdf19d --- /dev/null +++ b/src/plugins/product-manager/snippets/strategy/rules.txt @@ -0,0 +1,3 @@ +- Strategy decides which problems to solve NOW by focusing on a FEW (2-3) critical business levers. +- Prioritization is an act of saying NO: focus means rejecting the hundred other good ideas. +- A 'strategy' that is a long list rejecting nothing is not a strategy. \ No newline at end of file diff --git a/src/plugins/product-manager/tools/get-anti-patterns.ts b/src/plugins/product-manager/tools/get-anti-patterns.ts new file mode 100644 index 0000000..2fa0482 --- /dev/null +++ b/src/plugins/product-manager/tools/get-anti-patterns.ts @@ -0,0 +1,15 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ANTI_PATTERNS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_anti_patterns", + "Codifiable PM red flags (Cagan, Doshi): feature-factory, reactivity, viability-avoidance, execution-misdiagnosis, opinion-requirement. Use to flag weak product reasoning.", + {}, + async () => { + let text = `# PM Anti-Patterns (red flags)\n\n`; + for (const a of ANTI_PATTERNS) text += `## ${a.id}\n${a.smell} \n_source: ${a.source}_\n\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/get-discovery-rules.ts b/src/plugins/product-manager/tools/get-discovery-rules.ts new file mode 100644 index 0000000..95fd171 --- /dev/null +++ b/src/plugins/product-manager/tools/get-discovery-rules.ts @@ -0,0 +1,14 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { DISCOVERY_DOC } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_discovery_rules", + "Continuous-discovery rules (Torres): weekly customer contact, never ask 'what do you want', elicit past-behaviour stories. Use before claiming a build is customer-grounded.", + {}, + async () => { + const text = `# Discovery Rules\n\n${DISCOVERY_DOC}\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/get-four-risks.ts b/src/plugins/product-manager/tools/get-four-risks.ts new file mode 100644 index 0000000..2212d08 --- /dev/null +++ b/src/plugins/product-manager/tools/get-four-risks.ts @@ -0,0 +1,15 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { FOUR_RISKS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_four_risks", + "The four product risks every build must address before shipping (Cagan/SVPG): value, usability, feasibility, viability - with which role owns each. The PM owns value and viability.", + {}, + async () => { + let text = `# The Four Product Risks\n\nAddress all four before building. The PM is accountable for **value** and **viability**.\n\n| risk | question | owner |\n|---|---|---|\n`; + for (const r of FOUR_RISKS) text += `| ${r.id} | ${r.question} | ${r.owner} |\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/get-jtbd.ts b/src/plugins/product-manager/tools/get-jtbd.ts new file mode 100644 index 0000000..61e6370 --- /dev/null +++ b/src/plugins/product-manager/tools/get-jtbd.ts @@ -0,0 +1,14 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { JTBD_DOC } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_jtbd", + "Jobs-To-Be-Done framing (Christensen): a job is progress-in-context across functional/emotional/social dimensions, plus the Four Forces switching rule. Use to reframe a feature request as the underlying job.", + {}, + async () => { + const text = `# Jobs To Be Done\n\n${JTBD_DOC}\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/get-strategy-rules.ts b/src/plugins/product-manager/tools/get-strategy-rules.ts new file mode 100644 index 0000000..ebd433e --- /dev/null +++ b/src/plugins/product-manager/tools/get-strategy-rules.ts @@ -0,0 +1,14 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { STRATEGY_DOC } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_get_strategy_rules", + "Product-strategy rules (Cagan): focus on 2-3 levers, saying no is the act of prioritization, a long list is a non-strategy. Use to cut scope.", + {}, + async () => { + const text = `# Strategy Rules\n\n${STRATEGY_DOC}\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/opportunity-vs-solution.ts b/src/plugins/product-manager/tools/opportunity-vs-solution.ts new file mode 100644 index 0000000..32f8f80 --- /dev/null +++ b/src/plugins/product-manager/tools/opportunity-vs-solution.ts @@ -0,0 +1,16 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { OPPORTUNITY_RUBRIC_DOC } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_opportunity_vs_solution", + "Returns the Torres opportunity-vs-solution rubric (the test + reframe examples) for YOU to apply - it does not classify for you. Use to reframe a solution-shaped request into the underlying need.", + { statement: z.string().optional().describe("Optional: the statement you want to apply the rubric to.") }, + async ({ statement }) => { + let text = `# Opportunity vs Solution (rubric)\n\n${OPPORTUNITY_RUBRIC_DOC}\n`; + if (statement) text += `\n---\nApply the test to: "${statement}"\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/resolve-product-decision.ts b/src/plugins/product-manager/tools/resolve-product-decision.ts new file mode 100644 index 0000000..a6c878b --- /dev/null +++ b/src/plugins/product-manager/tools/resolve-product-decision.ts @@ -0,0 +1,39 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { computeDecision, FOUR_RISKS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_resolve_product_decision", + "The PM gate. Supply YOUR value and viability assessments for the proposal; returns a real verdict - PASS (both addressed), BLOCK (net-new build with a PM-owned risk unaddressed), or ADVISORY (tweak). PASS is earned by actually assessing value AND viability, not automatic. The judgment is yours; this enforces that you made it.", + { + description: z.string().describe("What is being proposed to build."), + valueAssessment: z.string().optional().describe("Your assessment of VALUE risk: will users choose it? Cite the customer problem/evidence."), + viabilityAssessment: z.string().optional().describe("Your assessment of VIABILITY risk: can the business sell/support/fund/legally ship it?"), + isNetNew: z.boolean().optional().describe("True for a net-new feature/product (hard gate); false for a tweak (advisory)."), + }, + async ({ description, valueAssessment, viabilityAssessment, isNetNew }) => { + const d = computeDecision({ description, valueAssessment, viabilityAssessment, isNetNew }); + + let text = `# Product Decision: ${d.verdict}\n\n**Proposal:** ${description}\n\n`; + text += `## PM-owned risk assessments\n`; + text += `- **value:** ${valueAssessment?.trim() || "_not provided_"}\n`; + text += `- **viability:** ${viabilityAssessment?.trim() || "_not provided_"}\n\n`; + text += `## Verdict: ${d.verdict} (${d.gate} gate)\n`; + + if (d.verdict === "PASS") { + text += `Value and viability are addressed. Hand back to \`hyper\`; proceed to design/build.\n`; + } else if (d.verdict === "BLOCK") { + text += `Unaddressed PM-owned risk(s): **${d.missing.join(", ")}**. A net-new build cannot proceed until value AND viability are addressed. Assess the missing risk(s) and call this tool again.\n`; + } else { + text += `Tweak / non-net-new: unaddressed **${d.missing.join(", ")}** noted as advisory. Proceed allowed, but the gap is on the record.\n`; + } + + text += `\n## The other two risks (not PM-owned, confirm before ship)\n`; + for (const r of FOUR_RISKS.filter((r) => r.owner !== "product-manager")) { + text += `- ${r.id} (${r.owner}): ${r.question}\n`; + } + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/score-rice.ts b/src/plugins/product-manager/tools/score-rice.ts new file mode 100644 index 0000000..f0f59da --- /dev/null +++ b/src/plugins/product-manager/tools/score-rice.ts @@ -0,0 +1,20 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { scoreRice } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_score_rice", + "Compute a RICE prioritization score = (Reach x Impact x Confidence) / Effort (Intercom). Confidence is 0-1. Use to rank competing initiatives.", + { + reach: z.number().describe("People affected per time period."), + impact: z.number().describe("Per-person impact (e.g. 3=massive,2=high,1=medium,0.5=low,0.25=minimal)."), + confidence: z.number().describe("Confidence 0-1 (1.0=high, 0.8=medium, 0.5=low)."), + effort: z.number().describe("Person-months (or any consistent effort unit)."), + }, + async ({ reach, impact, confidence, effort }) => { + const r = scoreRice({ reach, impact, confidence, effort }); + return { content: [{ type: "text" as const, text: `# RICE score: ${r.score.toFixed(2)}\n\n\`${r.formula}\` = (${reach} x ${impact} x ${confidence}) / ${effort}\n` }] }; + }, + ); +} diff --git a/src/plugins/product-manager/tools/validate-job-statement.ts b/src/plugins/product-manager/tools/validate-job-statement.ts new file mode 100644 index 0000000..b663d5b --- /dev/null +++ b/src/plugins/product-manager/tools/validate-job-statement.ts @@ -0,0 +1,16 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { JOB_CRITERIA_DOC } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "product_manager_validate_job_statement", + "Returns the 7-point JTBD job-statement criteria (Christensen/Ulwick) for YOU to judge a statement against - it does not pass/fail for you. Use to sharpen a vague job into one with context + progress.", + { statement: z.string().optional().describe("Optional: the job statement you want to judge against the criteria.") }, + async ({ statement }) => { + let text = `# JTBD Job-Statement Criteria (rubric)\n\n${JOB_CRITERIA_DOC}\n`; + if (statement) text += `\n---\nJudge this statement against the 7 criteria: "${statement}"\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/tests/context-compiler-behaviour.test.ts b/tests/context-compiler-behaviour.test.ts index ad4ca29..fe7079c 100644 --- a/tests/context-compiler-behaviour.test.ts +++ b/tests/context-compiler-behaviour.test.ts @@ -48,6 +48,9 @@ ${"x".repeat(2000)} ## Role Registry - Role 1 +## Persona Registry +- product-manager - PM gate + ## Routing Summary - Route 1 @@ -74,6 +77,8 @@ ${"x".repeat(2000)} expect(content).toMatch(/invariant-1/); expect(content).toMatch(/invariant-2/); expect(content.length).toBeLessThan(source.length); + expect(content).toMatch(/Personas/); + expect(content).toMatch(/product-manager/); }); test("generated bootstrap artifact stays in sync with the compiler output", () => { diff --git a/tests/persona-registry-behaviour.test.ts b/tests/persona-registry-behaviour.test.ts new file mode 100644 index 0000000..712a880 --- /dev/null +++ b/tests/persona-registry-behaviour.test.ts @@ -0,0 +1,14 @@ +import { test, expect } from "bun:test"; +import { loadPersonas } from "../src/personas/registry.ts"; + +test("registry loads the product-manager persona manifest", () => { + const personas = loadPersonas(); + const pm = personas.find((p) => p.id === "product-manager"); + expect(pm).toBeDefined(); + expect(pm!.owns.risks.sort()).toEqual(["value", "viability"]); + expect(pm!.gate_policy.net_new).toBe("hard"); +}); + +test("a malformed manifest is skipped, not thrown", () => { + expect(() => loadPersonas()).not.toThrow(); +}); diff --git a/tests/plugin-registry-behaviour.test.ts b/tests/plugin-registry-behaviour.test.ts index 79d25f1..a1e10c8 100644 --- a/tests/plugin-registry-behaviour.test.ts +++ b/tests/plugin-registry-behaviour.test.ts @@ -17,10 +17,17 @@ function getRegisteredTools() { return tools; } -test("all 13 plugins register at least one tool", () => { +test("all 14 plugins register at least one tool", () => { const tools = getRegisteredTools(); const pluginPrefixes = new Set(tools.map((t) => t.name.split("_")[0])); - expect(pluginPrefixes.size).toBe(13); + expect(pluginPrefixes.size).toBe(14); +}); + +test("product-manager plugin registers its gate tools", () => { + const tools = getRegisteredTools(); + const toolNames = new Set(tools.map((t) => t.name)); + expect(toolNames.has("product_manager_get_four_risks")).toBe(true); + expect(toolNames.has("product_manager_resolve_product_decision")).toBe(true); }); test("every registered tool has a non-empty name and description", () => { diff --git a/tests/product-manager-behaviour.test.ts b/tests/product-manager-behaviour.test.ts new file mode 100644 index 0000000..f9b39e5 --- /dev/null +++ b/tests/product-manager-behaviour.test.ts @@ -0,0 +1,111 @@ +import { test, expect } from "bun:test"; +import { + FOUR_RISKS, + PM_OWNED_RISKS, + computeDecision, + scoreRice, +} from "../src/plugins/product-manager/data.ts"; +import { captureTool, extractTextContent } from "./helpers.ts"; +import { register as getFourRisks } from "../src/plugins/product-manager/tools/get-four-risks.ts"; +import { register as getAntiPatterns } from "../src/plugins/product-manager/tools/get-anti-patterns.ts"; +import { register as oppVsSol } from "../src/plugins/product-manager/tools/opportunity-vs-solution.ts"; +import { register as validateJob } from "../src/plugins/product-manager/tools/validate-job-statement.ts"; +import { register as scoreRiceTool } from "../src/plugins/product-manager/tools/score-rice.ts"; +import { register as resolveDecision } from "../src/plugins/product-manager/tools/resolve-product-decision.ts"; + +// --- data layer --- +test("four risks are exactly the canonical four", () => { + expect(FOUR_RISKS.map((r) => r.id).sort()).toEqual(["feasibility", "usability", "value", "viability"]); +}); + +test("PM owns exactly value and viability", () => { + expect([...PM_OWNED_RISKS].sort()).toEqual(["value", "viability"]); +}); + +test("rice score is (reach*impact*confidence)/effort", () => { + expect(scoreRice({ reach: 100, impact: 2, confidence: 0.8, effort: 4 }).score).toBeCloseTo(40); +}); + +// --- the real gate: PASS is earned, BLOCK on missing, ADVISORY for tweaks --- +test("computeDecision PASSes when value and viability are both assessed", () => { + const d = computeDecision({ + description: "x", + valueAssessment: "users repeatedly ask for this in interviews, strong pull", + viabilityAssessment: "no legal/support cost, fits the paid tier", + isNetNew: true, + }); + expect(d.verdict).toBe("PASS"); + expect(d.missing).toEqual([]); +}); + +test("computeDecision BLOCKs a net-new build with viability unaddressed", () => { + const d = computeDecision({ + description: "x", + valueAssessment: "users repeatedly ask for this, strong evidence of pull", + isNetNew: true, + }); + expect(d.verdict).toBe("BLOCK"); + expect(d.missing).toContain("viability"); +}); + +test("computeDecision is ADVISORY (not BLOCK) for a tweak", () => { + const d = computeDecision({ description: "x", isNetNew: false }); + expect(d.verdict).toBe("ADVISORY"); +}); + +// --- tools --- +test("get_four_risks names value and viability as PM-owned", async () => { + const tool = captureTool(getFourRisks); + expect(tool.name).toBe("product_manager_get_four_risks"); + const text = extractTextContent(await tool.invoke({})); + expect(text).toMatch(/value/i); + expect(text).toMatch(/viability/i); + expect(text).toMatch(/product-manager/); +}); + +test("get_anti_patterns includes the feature-factory smell", async () => { + const tool = captureTool(getAntiPatterns); + const text = extractTextContent(await tool.invoke({})); + expect(text).toMatch(/feature-factory/); +}); + +test("opportunity_vs_solution returns the rubric for the agent to apply, not a verdict", async () => { + const tool = captureTool(oppVsSol); + expect(tool.name).toBe("product_manager_opportunity_vs_solution"); + const text = extractTextContent(await tool.invoke({ statement: "add a dark mode toggle" })); + expect(text).toMatch(/more than one way/i); + expect(text).toMatch(/rubric/i); +}); + +test("validate_job_statement returns the criteria rubric", async () => { + const tool = captureTool(validateJob); + const text = extractTextContent(await tool.invoke({ statement: "I want a faster app" })); + expect(text).toMatch(/criteria/i); + expect(text).toMatch(/context/i); +}); + +test("score_rice renders the computed score", async () => { + const tool = captureTool(scoreRiceTool); + const text = extractTextContent(await tool.invoke({ reach: 100, impact: 2, confidence: 0.8, effort: 4 })); + expect(text).toMatch(/40/); +}); + +test("resolve_product_decision BLOCKs a net-new build with no assessments", async () => { + const tool = captureTool(resolveDecision); + expect(tool.name).toBe("product_manager_resolve_product_decision"); + const text = extractTextContent(await tool.invoke({ description: "add CSV export", isNetNew: true })); + expect(text).toMatch(/BLOCK/); + expect(text).toMatch(/value/i); + expect(text).toMatch(/viability/i); +}); + +test("resolve_product_decision PASSes when value and viability are supplied", async () => { + const tool = captureTool(resolveDecision); + const text = extractTextContent(await tool.invoke({ + description: "add CSV export", + valueAssessment: "top request from power users who export to Excel weekly", + viabilityAssessment: "trivial to support, no legal exposure, fits all tiers", + isNetNew: true, + })); + expect(text).toMatch(/PASS/); +});