@sonnechasser/ntrp 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +21741 -22121
  2. package/dist/mcp/server.js +301 -597
  3. package/package.json +1 -1
@@ -1088,7 +1088,7 @@ import chalk3 from "chalk";
1088
1088
  function printDeepdiveHint(metricId, label) {
1089
1089
  const name = label ?? metricId;
1090
1090
  console.log(
1091
- " " + chalk3.dim("How this number works: ") + paint("accent", `/deepdive ${metricId}`) + chalk3.dim(` \u2014 ${name}`)
1091
+ " " + chalk3.dim("How this number works: ") + paint("accent", `/tour ${metricId}`) + chalk3.dim(` \u2014 ${name}`)
1092
1092
  );
1093
1093
  console.log();
1094
1094
  }
@@ -12296,145 +12296,42 @@ var init_packs = __esm({
12296
12296
  SILENT = "Never name methodology brands in customer-visible text. Apply the moves; keep labels silent.";
12297
12297
  COUNSEL_PACKS = [
12298
12298
  {
12299
- id: "counsel_sales",
12300
- catalog_line: "- counsel_sales \u2014 pipeline conversion, capacity, deal strategy, sales-team structure",
12301
- internal_name: "Sales counsel",
12302
- vital_links: ["flow_rate", "thread_depth", "freshness", "signal_to_noise"],
12303
- board_cut: "Sales constraint in dollars (stuck, single-threaded, stale, misdirected effort) and the one sales-process bet.",
12304
- operator_cut: "Manager inspection cadence, stage exit criteria, SDR acceptance SLA, multi-thread targets \u2014 function owners.",
12305
- when_to_use: "flow_rate / thread_depth / opp freshness / activity noise; attainment, cycle, coverage, forecast-from-the-line.",
12306
- kill_rules: [
12307
- "No headcount before capacity math + constraint vital.",
12308
- "No more activity when signal_to_noise is red.",
12309
- "Do not protect zombie pipeline for coverage optics.",
12310
- "No invented reorgs or named people \u2014 function owners only.",
12311
- "Do not kill zombies first unless freshness/zombies are the gating constraint."
12312
- ],
12313
- silent_brand_rule: SILENT,
12314
- role_card_ids: ["vp_sales", "sales_manager", "ae", "sdr", "se", "sales_ops"],
12315
- tree_weight: { plg: 0.5, smb_velocity: 1, mid_market: 1, enterprise: 1 },
12316
- body: `THINK: Conversion physics \u2014 coverage vs win rate vs cycle vs noise. Structure (pod, hunter/farmer, overlay, SE pool) explains capacity, not the first fix. Inspection beats activity. Forecast fiction is a credibility problem before it is a revenue problem.
12317
- ASK: Coverage, conversion, or cycle? Which stage clusters stuck $ ? Thread count on the largest deals? Which activities lack an opp? Where does manager commit diverge from stage evidence?
12318
- CUT: stage-age, owner, segment, amount band, activity-to-opp link.
12319
- DISAGREE: vs marketing \u2014 rejection reasons before "bad leads"; vs revops \u2014 keep fields that protect exit criteria, cut vanity; vs cs \u2014 oversell shows up in handoff packet; vs exec \u2014 sandbag vs stretch, use instruments.
12320
- TREE: plg=assist/expand not a hunting machine. smb_velocity=SDR\u2192AE pod, speed. mid_market=balanced inspection. enterprise=SE pool, named accounts, multi-thread.
12321
- CARDS: vp_sales for capacity/quota system; sales_manager for inspection; ae for deal strategy; sdr for create/accept; se only if enterprise/complex; sales_ops for territory/comp distortion.
12322
- PLAYS: get_play_detail unstick-pipeline / multi-thread-deals / clean-dead-pipeline / retarget-effort / compress-sales-cycle only when that vital is the constraint.`
12323
- },
12324
- {
12325
- id: "counsel_marketing",
12326
- catalog_line: "- counsel_marketing \u2014 demand quality, ABM, PMM/lifecycle; content/brand/partner as support seats",
12327
- internal_name: "Marketing counsel",
12328
- vital_links: ["drop_rate", "signal_to_noise", "nrr"],
12329
- board_cut: "Marketing's dollar contribution and the leak (creation vs handoff vs conversion narrative).",
12330
- operator_cut: "SLA, scoring, journey gates, program kill rules, PMM adoption exams \u2014 function owners.",
12331
- when_to_use: "Pipeline creation, handoff leak, magic number / CAC inputs, messaging stalls, named-account whitespace.",
12332
- kill_rules: [
12333
- "Do not buy more top-of-funnel when drop_rate is the constraint.",
12334
- "Do not optimize MQL count against pipeline dollars.",
12335
- "No ABM theater on smb_velocity without named-account process.",
12336
- "Stop at accepted pipeline quality + narrative enablement \u2014 do not own close.",
12337
- "If content/brand/partner is not the constraint, do not open those cards."
12338
- ],
12339
- silent_brand_rule: SILENT,
12340
- role_card_ids: [
12341
- "cmo",
12342
- "demand_gen",
12343
- "growth",
12344
- "abm",
12345
- "pmm",
12346
- "lifecycle",
12347
- "mkt_ops",
12348
- "content",
12349
- "brand",
12350
- "field_partner"
12299
+ id: "counsel_gtm",
12300
+ catalog_line: "- counsel_gtm \u2014 one GTM consultant: weigh tradeoffs, pick one constraint, one sequence, one call",
12301
+ internal_name: "GTM consultant",
12302
+ vital_links: [
12303
+ "freshness",
12304
+ "flow_rate",
12305
+ "drop_rate",
12306
+ "signal_to_noise",
12307
+ "thread_depth",
12308
+ "nrr",
12309
+ "grr"
12351
12310
  ],
12352
- tree_weight: { plg: 1, smb_velocity: 0.85, mid_market: 1, enterprise: 1 },
12353
- body: `THINK: Creation vs acceptance vs narrative. Sub-seats own different constraints \u2014 demand=volume/quality of accepted pipeline; ABM=named-list orchestration; PMM=win-rate/cycle via message; lifecycle=nurture/reactivation without colliding SDR; mkt ops=taxonomy/sync; content/brand/partner=support, not default.
12354
- ASK: Is the leak before CRM, in routing, or at AE reject? Which channel's pipeline closes? Are MQLs a shared definition? Would fewer named accounts beat more spend?
12355
- CUT: source, campaign, MQL\u2192SQL\u2192Opp, named-list vs rest, journey vs SDR sequence overlap.
12356
- DISAGREE: vs sales \u2014 instrument acceptance reasons, not blame; vs revops \u2014 shared definitions, not last-touch religion; vs cs \u2014 frequency caps on customers; vs exec \u2014 brand spend needs a GTM exam or defer.
12357
- TREE: plg=growth/lifecycle over classical demand. smb_velocity=inbound+outbound shared; no fake ABM. mid_market=demand-led, ABM opportunistic. enterprise=ABM+PMM first.
12358
- CARDS: demand_gen if creation/quality; abm if named list; pmm if propose/decide stalls; lifecycle if nurture/collision; mkt_ops if scoring/sync; growth if PLG activation; content/brand/field_partner only when that sub-seat is the constraint.
12359
- PLAYS: get_play_detail demand-quality-over-volume when accepts are weak; abm-orchestration when named-list process exists; rebalance-pipeline-mix / improve-magic-number for mix/efficiency. Handoff dollars usually belong with revops (fix-handoff-gap / harden-routing-sla) \u2014 marketing consumes the SLA.`
12360
- },
12361
- {
12362
- id: "counsel_revops",
12363
- catalog_line: "- counsel_revops \u2014 GTM systems, routing/SLA/hygiene; forecast/capacity/territory governance",
12364
- internal_name: "RevOps counsel",
12365
- vital_links: ["drop_rate", "freshness", "signal_to_noise"],
12366
- board_cut: "Whether the revenue machine's instruments can be trusted; cost of leak from process/system breaks.",
12367
- operator_cut: "Routing SLA, stage validation, sync observability, hygiene, GTM eng backlog \u2014 RevOps / mkt ops / GTM eng.",
12368
- when_to_use: "Broken instruments, routing, sync, definitions, hygiene, automation debt; forecast ritual design; capacity/territory ops.",
12369
- kill_rules: [
12370
- "No governance theater when routing/SLA is broken.",
12371
- "Do not automate bad process.",
12372
- "No boil-the-ocean warehouse while drop_rate burns.",
12373
- "Do not take sides on credit wars \u2014 impose shared definitions.",
12374
- "Prefer system/process enforcement over headcount."
12375
- ],
12376
- silent_brand_rule: SILENT,
12377
- role_card_ids: ["vp_revops", "revops_specialist", "gtm_engineer", "mkt_ops", "sales_ops"],
12378
- tree_weight: { plg: 0.6, smb_velocity: 0.75, mid_market: 1, enterprise: 1 },
12379
- body: `THINK: 65% systems \u2014 missing vs late vs wrong vs unused data. Foundation identity before activation automation. 35% governance \u2014 forecast ritual (timely commits, inspection cadence), capacity model math, territory/comp ops that distort behavior. Dashboards without SLA/enforcement are theater.
12380
- ASK: Behavior or instrument? Which timer/queue/sync break maps to the dollar? Do we have observability before another flow? Is forecast wrong because inputs are dirty or because the ritual is missing? Does territory/comp explain attainment better than skill talk?
12381
- CUT: source\u2192owner path, unassigned age, stage-exit compliance, sync error class, identity match, commit vs inspection evidence.
12382
- DISAGREE: vs sales \u2014 keep fields that protect exit criteria; vs marketing \u2014 time-box safe launches; vs cs \u2014 identity + handoff packet before dual-tool debates; vs exec \u2014 platform spend tied to constraint dollar and a 30-day hold exam.
12383
- TREE: plg=product\u2194CRM identity. smb_velocity=lean routing/SLA. mid_market=full OS. enterprise=matrixed + GTM eng.
12384
- CARDS: revops_specialist for day-to-day CRM/SLA; gtm_engineer for integrations/identity; mkt_ops for MAP/sync; sales_ops for territory/comp; vp_revops for OS tradeoffs and freeze decisions.
12385
- PLAYS: get_play_detail harden-routing-sla when timers/observability are the leak; fix-handoff-gap when the path/source audit is missing; forecast-ritual-hygiene only after instrument trust; clean-dead-pipeline / retarget-effort when hygiene/automation feeds noise.`
12386
- },
12387
- {
12388
- id: "counsel_exec",
12389
- catalog_line: "- counsel_exec \u2014 merge seats into one constraint, one sequence, board-ready call",
12390
- internal_name: "Executive counsel",
12391
- vital_links: ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth", "nrr", "grr"],
12392
12311
  board_cut: "The call, the dollar, the one bet, the exam date.",
12393
12312
  operator_cut: "Sequenced workstreams with function owners and contingencies.",
12394
- when_to_use: "Always in strategist roundtable before plan ship; cross-functional objectives; seat conflicts.",
12313
+ when_to_use: "Any GTM advice \u2014 light NL or full plan. Always before plan ship; cross-functional objectives; CS vs new-logo tension.",
12395
12314
  kill_rules: [
12396
- "Do not average seats into mush \u2014 pick a constraint and sequence.",
12315
+ "Merge, do not concatenate \u2014 one governing constraint and one sequence.",
12397
12316
  "No reorg/headcount as first move.",
12398
12317
  "No parallel initiatives that starve the bottleneck.",
12399
12318
  "Open with verdict + dollars, not methodology.",
12400
- "Do not ignore CS when NRR is the dollar engine."
12319
+ "Do not drop retention when NRR/GRR dollars matter alongside new-logo.",
12320
+ "Do not print named-role opinions or pack ids as speakers."
12401
12321
  ],
12402
12322
  silent_brand_rule: SILENT,
12403
- role_card_ids: ["cro", "cmo", "vp_sales", "vp_revops", "vp_cs"],
12323
+ role_card_ids: [],
12404
12324
  tree_weight: { plg: 1, smb_velocity: 1, mid_market: 1, enterprise: 1 },
12405
- body: `THINK: Merge, do not concatenate. One governing constraint. Drop non-constraint theater. Board altitude = verdict + dollar + exam; operator altitude = sequenced owners. CS is the dollar engine when NRR/GRR dwarfs new-logo at risk (typical PLG and installed-base heavy enterprise).
12406
- ASK: What unlocks the others? What dies in 90 days if we are wrong? Which seat's ask is polish?
12325
+ body: `THINK: Merge, do not concatenate. One governing constraint. Drop non-constraint theater. Board altitude = verdict + dollar + exam; operator altitude = sequenced Monday owners (who does the work \u2014 not who is talking).
12326
+ TENSIONS (weigh inside one recommendation \u2014 never as competing seats):
12327
+ - CS vs new-logo: when NRR/GRR dollars and new-logo/freshness dollars both matter, keep both in one sequence under one governing constraint. Do not drop retention.
12328
+ - Quality vs volume: accepted pipeline quality and conversion beat MQL/activity theater when drop_rate or signal_to_noise is the constraint.
12329
+ - Systems vs theater: instrument trust (routing/SLA/hygiene) before narrative bets or dashboards without enforcement.
12330
+ ASK: What unlocks the others? What dies in 90 days if we are wrong? Which ask is polish?
12407
12331
  CUT: layer order of vitals; $ at risk by function; in-flight work that collides.
12408
- DISAGREE: Force other packs to drop non-constraint work. Reject sales-only or marketing-only tunnels when instruments point elsewhere. Hold RevOps to instrument trust before narrative bets. Hold CS in the plan when GRR is the money.
12332
+ DISAGREE (fold in \u2014 do not stage speakers): reject sales-only or marketing-only tunnels when instruments point elsewhere; hold systems work when instruments are untrusted; hold CS in the plan when GRR/NRR is the money; reject more top-of-funnel when drop_rate is the leak.
12409
12333
  TREE: plg=CS/growth weight. smb_velocity=sales machine + routing. mid_market=full GTM. enterprise=thread/ABM/SE + NRR.
12410
- CARDS: cro for sequencing investment; cmo when demand/brand tradeoff; vp_sales / vp_revops / vp_cs when that function must own the Monday bet. Do not clone this pack into the CRO card.
12411
- PLAYS: do not pick plays here \u2014 sequence the constraint owner's play and demote the rest to contingency.`
12412
- },
12413
- {
12414
- id: "counsel_cs",
12415
- catalog_line: "- counsel_cs \u2014 NRR/GRR, renewals, TTV/onboarding, sales\u2192CS handoff",
12416
- internal_name: "Customer Success counsel",
12417
- vital_links: ["freshness", "nrr", "grr", "contraction_arr"],
12418
- board_cut: "NRR/GRR dollars at risk and the retention/expansion bet.",
12419
- operator_cut: "Risk flags, renewal cadence, handoff packet fields, touch model by segment \u2014 CS / renewals / onboarding.",
12420
- when_to_use: "NRR/GRR/churn/expansion; post-sale handoff; renewals; after sales wins that create CS landmines.",
12421
- kill_rules: [
12422
- "Do not push expansion into unhealthy accounts.",
12423
- "No more QBRs when onboarding/TTV is the constraint.",
12424
- "Do not treat renewals as pure billing.",
12425
- "Do not ignore sales handoff quality as root cause.",
12426
- "No CS headcount before segmentation/touch-model math."
12427
- ],
12428
- silent_brand_rule: SILENT,
12429
- role_card_ids: ["vp_cs", "csm", "renewals", "onboarding"],
12430
- tree_weight: { plg: 1, smb_velocity: 0.7, mid_market: 1, enterprise: 1 },
12431
- body: `THINK: GRR/NRR dollars. Churn vs failed expansion vs both. TTV/onboarding leak beats QBR theater. Champion loss vs product vs oversell. Expansion only after health restore when churn risk is high. Sales\u2192CS packet is often the real drop after Closed-Won.
12432
- ASK: Leading indicator 60\u201390 days before churn? Was the sale clean (ICP/scope/handoff)? Onboarding incomplete or value never landed? Expansion owner CS vs AM vs AE?
12433
- CUT: tenure, segment, product usage, renewal date aging, handoff-packet completeness, champion map.
12434
- DISAGREE: vs sales \u2014 oversell and late CS involvement; vs marketing \u2014 customer email fatigue; vs revops \u2014 one SoR for renewal dates/risk; vs exec \u2014 growth plans must carry GRR exams.
12435
- TREE: plg=CS+lifecycle+growth share the constraint. smb_velocity=often folded into CSM/AE; still name GRR. mid_market=full CS OS. enterprise=strategic accounts, renewals specialist.
12436
- CARDS: csm for health/save; renewals for commercial deadline; vp_cs for touch model and where expansion sits; onboarding for readiness/TTV (capture required when events are missing).
12437
- PLAYS: get_play_detail reduce-logo-churn / fix-renewal-process / renewal-early-warning / accelerate-expansion / sales-cs-handoff-packet when that instrument is the constraint.`
12334
+ PLAYS: sequence the constraint owner's play and demote the rest to contingency. Monday owners stay function-shaped ("RevOps starts the path audit") \u2014 never nameplates for who is talking.`
12438
12335
  }
12439
12336
  ];
12440
12337
  }
@@ -13092,21 +12989,21 @@ var init_metric_definitions = __esm({
13092
12989
  kind: "vital",
13093
12990
  label: "Freshness",
13094
12991
  group: "Vital Signs",
13095
- tagline: "Is the CRM still talking about live work \u2014 or a museum of old deals?",
13096
- how_computed: "NTRP computes a weighted average of people, organizations, and opportunities with recent activity. Open opportunities must also not be past-due. Default windows: people and organizations 90 days, opportunities 30 days. Weights are 35 / 30 / 35.",
12992
+ tagline: "Is the CRM talking about live deals \u2014 or a museum of old ones?",
12993
+ how_computed: "We look at how recently people, companies, and deals got a touch. Deals need a fresher pulse than people or companies, and past-due deals count as stale. The formula lines on this card show the exact mix and windows.",
13097
12994
  formula_lines: [
13098
12995
  "freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
13099
12996
  "people/orgs fresh if activity within 90d",
13100
12997
  "opps fresh if activity within 30d AND not past-due"
13101
12998
  ],
13102
- meaning: 'We treat Freshness as a truth check on the pipeline number. Quiet days mean different things by motion: in enterprise, 30 quiet days can be normal cadence; in a velocity book, that same silence often means the deal is already dead. Stale open amount is why we show "pipeline at risk."',
13103
- expert_read: `A useful first cut is by owner and by stage \u2014 reds tend to clump on people or process, not sprinkle evenly. A sudden cliff is usually a broken integration or a departed rep, not gradual decay. Watch for the false positive too: bulk-imported records nobody has touched yet can look "stale" when they're just new.`,
12999
+ meaning: `Freshness asks whether the pipeline number is still true. Quiet deals pad the forecast. Like milk dates on a shelf \u2014 past the date, don't trust the carton. Stale open dollars show as "pipeline at risk."`,
13000
+ expert_read: "Cut by owner and stage first \u2014 reds usually clump, they don't sprinkle. A sudden cliff is often a broken sync or a departed rep, not slow decay. New bulk imports can look stale when they're just new.",
13104
13001
  deepdive: [
13105
- "Status bands: green 80 or more, yellow 60 or more, red below 60. Motion presets can change the windows.",
13106
- 'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
13107
- "Layer 1 of the gating stack. A red score here limits trust in later layers.",
13108
- "Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when the score is below 60.",
13109
- "Levers: stale-deal alert at N quiet days. Weekly hygiene scrub. Enrichment refresh on quiet records. Signal-triggered reactivation for paid dormant accounts."
13002
+ "Green 80+, yellow 60+, red below 60. Motion presets can change the windows.",
13003
+ 'Dollars: sum of amount on stale open deals \u2192 "pipeline at risk".',
13004
+ "Read this first. If Freshness is red, later vitals are harder to trust.",
13005
+ "Weak score \u2192 play Clean Dead Pipeline.",
13006
+ "Useful moves: stale-deal alert, weekly hygiene scrub, enrichment on quiet records."
13110
13007
  ],
13111
13008
  visual: {
13112
13009
  kind: "bars",
@@ -13130,21 +13027,21 @@ var init_metric_definitions = __esm({
13130
13027
  kind: "vital",
13131
13028
  label: "Flow Rate",
13132
13029
  group: "Vital Signs",
13133
- tagline: "Where does deal motion slow \u2014 and what's stuck behind that bottleneck?",
13134
- how_computed: "NTRP sets a base score from average open-deal age versus max_days. It then applies a penalty of up to 20 for the share of stuck deals. A deal is stuck when it has no update beyond stuck_days, or a past-due close. Status uses average open age, not the score alone.",
13030
+ tagline: "Where do deals stop moving \u2014 and what's piled up behind that stage?",
13031
+ how_computed: "We start from how old open deals are versus a max window, then shave points for the share that look stuck. A deal is stuck when it goes quiet too long or its close date is already past. The formula lines show the base, the penalty, and the stuck rules.",
13135
13032
  formula_lines: [
13136
13033
  "base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
13137
13034
  "score = base \u2212 stuckSharePenalty (\u226420)",
13138
13035
  "stuck = no update > stuck_days OR past-due close"
13139
13036
  ],
13140
- meaning: 'We think of Flow Rate as velocity risk with a dollar tag. When deals stop advancing \u2014 and especially when close dates slip past due \u2014 next quarter starts to feel optimistic on paper. The dollar line is the amount still "stuck in pipeline."',
13141
- expert_read: "We prefer stage age over deal age alone: find the stage where advancement collapses \u2014 it's usually one stage, not everywhere. Compare stuck age to this company's own median cycle rather than a generic norm. Stuck deals plus past-due closes are often a forecast-credibility problem before they're a revenue miss.",
13037
+ meaning: 'Flow Rate asks whether deals are still advancing or sitting still. Stuck deals pad next quarter on paper. Like traffic behind a closed lane \u2014 the jam is one stage, not the whole highway. Dollars on stuck deals show as "stuck in pipeline."',
13038
+ expert_read: "Find the stage where advancement collapses first \u2014 it's usually one stage, not everywhere. Compare stuck age to this company's own median cycle rather than a generic norm. Stuck deals plus past-due closes are often a forecast-credibility problem before they're a revenue miss.",
13142
13039
  deepdive: [
13143
- "Status uses average open age. Green is 45 days or less. Yellow is 90 days or less. Else red. Defaults: max_days 120, stuck_days 60.",
13144
- 'Dollar translation: sum of amount on stuck deals \u2192 "stuck in pipeline".',
13145
- "Layer 2 of the gating stack, with Drop Rate.",
13146
- "Trigger play: Unstick the Pipeline (unstick-pipeline) when the score is weak.",
13147
- "Levers: stage-age report. Past-due close cleanup. Progression plans on stuck deals. Forecast hygiene on optimistic close dates."
13040
+ "Status uses average open age. Green \u226445 days, yellow \u226490, else red. Defaults: max_days 120, stuck_days 60.",
13041
+ 'Dollars: sum of amount on stuck deals \u2192 "stuck in pipeline".',
13042
+ "Read with Drop Rate \u2014 stuck deals and handoff leaks often show up together.",
13043
+ "Weak score \u2192 play Unstick the Pipeline.",
13044
+ "Useful moves: stage-age report, past-due close cleanup, progression plans on stuck deals, forecast hygiene on optimistic close dates."
13148
13045
  ],
13149
13046
  visual: {
13150
13047
  kind: "funnel",
@@ -13170,21 +13067,21 @@ var init_metric_definitions = __esm({
13170
13067
  kind: "vital",
13171
13068
  label: "Drop Rate",
13172
13069
  group: "Vital Signs",
13173
- tagline: "Where does paid demand fall between systems before anyone works it?",
13174
- how_computed: "NTRP blends cross-system retention and opportunity retention. Cross-system retention is marketing people also present in sales. Opportunity retention is open opportunities that are not abandoned. Default weights: cross-system 60 percent, opportunity retention 40 percent. Abandoned means open opportunities with no activity in 30 days.",
13070
+ tagline: "Where does paid demand fall between systems before a rep works it?",
13071
+ how_computed: "We blend two checks: do marketing people also show up in sales CRM, and are open deals still being worked (not abandoned). The formula lines show the weights and the 30-day abandon window.",
13175
13072
  formula_lines: [
13176
13073
  "score = crossSystemRetention\xD70.6 + oppRetention\xD70.4",
13177
13074
  "cross-system = marketing people also in sales CRM",
13178
13075
  "abandoned = open opps with no activity in 30d"
13179
13076
  ],
13180
- meaning: 'We read Drop Rate as the price of the handoff leak \u2014 budget already spent on leads that never reach a working rep. The dollar estimate is droppedCount \xD7 conversion \xD7 avg deal size: "est. lost at handoff."',
13181
- expert_read: 'In practice this usually looks like a systems story: routing rules, unassigned territories, dead rep queues, or a sync gap between marketing and CRM \u2014 more often than "lazy reps." A useful first cut is by lead source; the leak tends to concentrate in one or two sources. The cheapest pipeline a business can buy is often the leads it already paid for.',
13077
+ meaning: 'Drop Rate prices the handoff leak \u2014 money you already spent on leads that never reach a working rep. Like buying groceries and leaving half the bags in the parking lot. The dollar line is "est. lost at handoff."',
13078
+ expert_read: "A useful first cut is by lead source \u2014 the leak usually concentrates in one or two, not everywhere. Routing rules, unassigned territories, dead queues, or a sync gap between marketing and CRM show up more often than lazy reps. The cheapest pipeline you can buy is often the leads you already paid for.",
13182
13079
  deepdive: [
13183
- "Status bands: green 80 or more, yellow 60 or more, red below 60.",
13184
- 'Dollar translation: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
13185
- "Layer 2 of the gating stack, with Flow Rate.",
13186
- "Trigger play: Fix the Handoff Gap (fix-handoff-gap) when drop is high.",
13187
- "Levers: source-level handoff audit. Routing and sync repair. Time-to-first-touch SLA. Weekly marketing-only-leads report."
13080
+ "Green 80+, yellow 60+, red below 60.",
13081
+ 'Dollars: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
13082
+ "Read with Flow Rate \u2014 handoff leaks and stuck deals often show up together.",
13083
+ "Weak score \u2192 play Fix the Handoff Gap.",
13084
+ "Useful moves: source-level handoff audit, routing and sync repair, time-to-first-touch SLA, weekly marketing-only-leads report."
13188
13085
  ],
13189
13086
  visual: {
13190
13087
  kind: "funnel",
@@ -13209,21 +13106,21 @@ var init_metric_definitions = __esm({
13209
13106
  kind: "vital",
13210
13107
  label: "Signal:Noise",
13211
13108
  group: "Vital Signs",
13212
- tagline: "How much of the team's effort still aims at deals that can close?",
13213
- how_computed: "Over a 90-day lookback, NTRP computes (signal activities / all activities) \xD7 100. Signal is activity linked to an open opportunity, a pipeline person, or a pipeline organization.",
13109
+ tagline: "How much effort still aims at accounts that can close \u2014 versus dead ends?",
13110
+ how_computed: "We look at the last 90 days of activity and ask what share is signal \u2014 linked to an open deal, a pipeline person, or a pipeline company. The formula lines show the ratio and what counts as signal.",
13214
13111
  formula_lines: [
13215
13112
  "score = (signalCount / activityCount) \xD7 100",
13216
13113
  "signal = linked to open opp / pipeline person / pipeline org",
13217
13114
  "lookback = trailing 90 days"
13218
13115
  ],
13219
- meaning: `We treat Signal:Noise as activity efficiency with a capacity price. Noise hours \xD7 hours_per_activity \xD7 rep_hourly_cost becomes "misdirected effort" \u2014 energy aimed at accounts that can't still close.`,
13116
+ meaning: 'Signal:Noise asks whether activity still points at deals that can close. Noise hours are capacity spent on dead ends. Like a radio stuck between stations \u2014 lots of static, little signal. Those hours show as "misdirected effort."',
13220
13117
  expert_read: "A useful cut is by rep and by account status. Persistent noise often means reps keep working dead accounts they already know \u2014 stale targeting or coverage models more than a coaching gap. Also check whether activity is logged against closed or unlinked records; that can be a hygiene artifact pretending to be effort.",
13221
13118
  deepdive: [
13222
- "Status bands: green 65 or more, yellow 40 or more, red below 40.",
13223
- "Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost).",
13224
- "Layer 3 of the gating stack. Trust Freshness, Flow Rate, and Drop Rate before you read activity efficiency.",
13225
- "Trigger play: Retarget Misdirected Effort (retarget-effort) when the score is low.",
13226
- "Levers: refresh account lists. Signal-based targeting. Stop logging against closed or unlinked records. Coverage-model redesign."
13119
+ "Green 65+, yellow 40+, red below 40.",
13120
+ 'Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost) \u2192 "misdirected effort".',
13121
+ "Read Freshness, Flow Rate, and Drop Rate before you trust this score.",
13122
+ "Weak score \u2192 play Retarget Misdirected Effort.",
13123
+ "Useful moves: refresh account lists, signal-based targeting, stop logging against closed or unlinked records, coverage-model redesign."
13227
13124
  ],
13228
13125
  visual: {
13229
13126
  kind: "split",
@@ -13246,21 +13143,21 @@ var init_metric_definitions = __esm({
13246
13143
  kind: "vital",
13247
13144
  label: "Thread Depth",
13248
13145
  group: "Vital Signs",
13249
- tagline: "How fragile is revenue if one champion changes jobs?",
13250
- how_computed: "Percent of open deals with at least multi_thread_threshold distinct people active in the last 90 days. The default threshold is 2. People include opportunity-direct contacts and same-organization activity.",
13146
+ tagline: "How fragile is a deal if one champion changes jobs?",
13147
+ how_computed: "We ask what share of open deals have at least two people active in the last 90 days. Contacts on the deal and activity at the same company both count. The formula lines show the threshold.",
13251
13148
  formula_lines: [
13252
13149
  "score = % open deals with \u22652 active people (90d)",
13253
13150
  "people counted via opp contacts + same-org activity",
13254
13151
  "threshold configurable (default 2)"
13255
13152
  ],
13256
- meaning: "We read Thread Depth as resilience risk. One single-threaded mega-deal can outweigh ten small ones \u2014 and late-cycle single-threading is especially scary because champions change jobs. The dollar line sums amount on single-threaded deals.",
13257
- expert_read: "Weight by deal size when you triage. Single-threading late in the cycle tends to matter more than early. In enterprise motions, thin threads are often a leading indicator of slipped quarters: the champion leaves and there's no second contact warm enough to carry the deal.",
13153
+ meaning: 'Thread Depth asks how many people keep each deal warm. One contact is a single point of failure \u2014 if they leave, the deal often dies with them. Like a bridge with one cable. Dollars on thin deals show as "single-threaded."',
13154
+ expert_read: "Weight by deal size when you triage \u2014 one thin mega-deal can outweigh ten small ones. Late-cycle single-threading tends to matter more than early. In enterprise motions, thin threads often lead to slipped quarters: the champion leaves and nobody else is warm enough to carry the deal.",
13258
13155
  deepdive: [
13259
- "Status bands: green 65 or more, yellow 40 or more, red below 40.",
13260
- 'Dollar translation: sum of amount on single-threaded deals \u2192 "single-threaded".',
13261
- "Layer 4 of the gating stack. Read this last, after the upstream vital signs.",
13262
- "Trigger play: Multi-Thread Your Deals (multi-thread-deals) when depth is low.",
13263
- "Levers: buying-committee map. Warm internal referral first. CRM contact roles. Mid-stage single-thread alerts. Champion job-change signals."
13156
+ "Green 65+, yellow 40+, red below 40.",
13157
+ 'Dollars: sum of amount on single-threaded deals \u2192 "single-threaded".',
13158
+ "Read this last, after Freshness, Flow Rate, Drop Rate, and Signal:Noise.",
13159
+ "Weak score \u2192 play Multi-Thread Your Deals.",
13160
+ "Useful moves: buying-committee map, warm internal referral first, CRM contact roles, mid-stage single-thread alerts, champion job-change signals."
13264
13161
  ],
13265
13162
  visual: {
13266
13163
  kind: "bars",
@@ -14181,139 +14078,38 @@ var init_constraint_signals = __esm({
14181
14078
  });
14182
14079
 
14183
14080
  // src/data/gtm-counsel/seating.ts
14184
- function defaultSeatOrder(tree) {
14185
- const tieBreak = TREE_TIE_BREAKS[tree];
14186
- return COUNSEL_PACKS.filter(
14187
- (pack) => pack.id !== "counsel_exec"
14188
- ).slice().sort((a, b) => {
14189
- const weightDelta = b.tree_weight[tree] - a.tree_weight[tree];
14190
- if (weightDelta !== 0) return weightDelta;
14191
- return tieBreak.indexOf(a.id) - tieBreak.indexOf(b.id);
14192
- }).map((pack) => pack.id);
14193
- }
14194
- function baselineSeatWeight(packId, tree) {
14195
- const pack = COUNSEL_PACKS.find((candidate) => candidate.id === packId);
14196
- const weight = pack?.tree_weight[tree] ?? 0.5;
14197
- if (weight >= 0.8) return "full";
14198
- if (weight >= 0.5) return "secondary";
14199
- return "risk_note";
14200
- }
14201
14081
  function inferConstraintOwnerPacks(objective, playIds = [], gatingVital) {
14202
14082
  return resolveLegacyConstraint(objective, playIds, gatingVital).locked_owners;
14203
14083
  }
14204
14084
  function inferConstraintOwnerPack(objective, playIds = [], gatingVital) {
14205
14085
  return inferConstraintOwnerPacks(objective, playIds, gatingVital)[0] ?? null;
14206
14086
  }
14207
- function inferRoundtableBias(objective, profile) {
14208
- const text = [objective, profile?.user_scope, profile?.custom_context].filter(Boolean).join(" ").toLowerCase();
14209
- const notes = [];
14210
- const demoteToRiskNote = [];
14211
- const preferFull = [];
14212
- const strongMarketing = /marketing (lens|view|perspective)|from marketing|as cmo\b/.test(text);
14213
- const strongRevops = /revops (lens|view)|rev ops (lens|view)|systems view|from revops/.test(text);
14214
- const strongCs = /cs (lens|view)|success (lens|view)|retention view|nrr view/.test(text);
14215
- const strongSales = /sales (lens|view|perspective)|from sales|ae view|quota view/.test(text);
14216
- const weakMarketing = /\b(cmo|demand gen|demand-gen|mqls?\b|abm\b|pmm\b)\b/.test(text);
14217
- const weakRevops = /\b(revops|rev ops|routing sla|gtm engineer)\b/.test(text);
14218
- const weakCs = /\b(customer success|renewals manager|csm\b)\b/.test(text);
14219
- const weakSales = /\b(vp sales|sales manager|quota attain)\b/.test(text);
14220
- const notCro = /not cro|ignore cro|operator framing|not ceo.centric|skip exec politics/.test(text);
14221
- if (strongMarketing || weakMarketing) {
14222
- preferFull.push("counsel_marketing", "counsel_exec");
14223
- notes.push(strongMarketing ? "Bias: marketing-weighted roundtable" : "Bias: marketing mentioned \u2014 keep marketing full");
14224
- }
14225
- if (strongRevops || weakRevops) {
14226
- preferFull.push("counsel_revops", "counsel_exec");
14227
- notes.push(strongRevops ? "Bias: RevOps-weighted roundtable" : "Bias: RevOps mentioned \u2014 keep RevOps full");
14228
- }
14229
- if (strongCs || weakCs) {
14230
- preferFull.push("counsel_cs", "counsel_exec");
14231
- notes.push(strongCs ? "Bias: CS-weighted roundtable" : "Bias: CS mentioned \u2014 keep CS full");
14232
- }
14233
- if (strongSales || weakSales) {
14234
- preferFull.push("counsel_sales", "counsel_exec");
14235
- notes.push(strongSales ? "Bias: sales-weighted roundtable" : "Bias: sales mentioned \u2014 keep sales full");
14236
- }
14237
- if (notCro) {
14238
- notes.push("Bias: operator framing for exec seat (not CRO-centric voice)");
14239
- }
14240
- const strong = [
14241
- strongMarketing ? "counsel_marketing" : null,
14242
- strongRevops ? "counsel_revops" : null,
14243
- strongCs ? "counsel_cs" : null,
14244
- strongSales ? "counsel_sales" : null
14245
- ].filter((p) => p != null);
14246
- if (strong.length === 1) {
14247
- for (const p of ALL_FUNCTION) {
14248
- if (p !== strong[0]) demoteToRiskNote.push(p);
14249
- }
14250
- }
14251
- return { demoteToRiskNote, preferFull, notes };
14087
+ function inferRoundtableBias(_objective, _profile) {
14088
+ return { demoteToRiskNote: [], preferFull: [], notes: [] };
14252
14089
  }
14253
14090
  function buildRoundtableSeating(opts) {
14254
- const {
14255
- tree,
14256
- objective,
14257
- profile = null,
14258
- playIds = [],
14259
- gatingVital = null,
14260
- operatorConstraints = []
14261
- } = opts;
14262
- const bias = inferRoundtableBias(objective, profile);
14263
- const objectiveContext = {
14264
- objective,
14265
- play_ids: playIds,
14266
- operator_constraints: operatorConstraints
14091
+ const constraint = opts.resolvedConstraint ?? resolveLegacyConstraint(
14092
+ opts.objective,
14093
+ opts.playIds ?? [],
14094
+ opts.gatingVital ?? null
14095
+ );
14096
+ const bias_notes = [...constraint.notes ?? []];
14097
+ if (constraint.locked_owners?.length) {
14098
+ bias_notes.push(
14099
+ `Constraint owners (internal, not speakers): ${constraint.locked_owners.join(", ")}`
14100
+ );
14101
+ }
14102
+ return {
14103
+ tree: opts.tree ?? "mid_market",
14104
+ seats: [],
14105
+ bias_notes,
14106
+ constraint
14267
14107
  };
14268
- const constraint = opts.resolvedConstraint ?? resolveConstraint({
14269
- health: opts.health ?? {
14270
- gating_vital: gatingVital && ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"].includes(
14271
- gatingVital
14272
- ) ? gatingVital : null
14273
- },
14274
- objective: objectiveContext
14275
- });
14276
- const constraintOwners = constraint.locked_owners;
14277
- const ownerSet = new Set(constraintOwners);
14278
- const demote = new Set(bias.demoteToRiskNote);
14279
- const prefer = new Set(bias.preferFull);
14280
- const seats = [{ pack_id: "counsel_exec", weight: "full" }];
14281
- for (const packId of defaultSeatOrder(tree)) {
14282
- let weight = baselineSeatWeight(packId, tree);
14283
- if (demote.has(packId) && !prefer.has(packId)) weight = "risk_note";
14284
- if (prefer.has(packId)) weight = "full";
14285
- if (ownerSet.has(packId)) weight = "full";
14286
- seats.push({ pack_id: packId, weight });
14287
- }
14288
- for (const owner of constraintOwners) {
14289
- if (!seats.some((s) => s.pack_id === owner)) {
14290
- seats.push({ pack_id: owner, weight: "full" });
14291
- }
14292
- }
14293
- const bias_notes = [...bias.notes, ...constraint.notes];
14294
- if (constraintOwners.length) {
14295
- bias_notes.push(`Constraint-owning pack locked full: ${constraintOwners.join(", ")}`);
14296
- }
14297
- return { tree, seats, bias_notes, constraint };
14298
- }
14299
- var ALL_FUNCTION, TREE_TIE_BREAKS;
14108
+ }
14300
14109
  var init_seating = __esm({
14301
14110
  "src/data/gtm-counsel/seating.ts"() {
14302
14111
  "use strict";
14303
- init_packs();
14304
14112
  init_constraint_signals();
14305
- ALL_FUNCTION = [
14306
- "counsel_sales",
14307
- "counsel_marketing",
14308
- "counsel_revops",
14309
- "counsel_cs"
14310
- ];
14311
- TREE_TIE_BREAKS = {
14312
- plg: ["counsel_cs", "counsel_marketing", "counsel_revops", "counsel_sales"],
14313
- smb_velocity: ["counsel_sales", "counsel_revops", "counsel_marketing", "counsel_cs"],
14314
- mid_market: ["counsel_sales", "counsel_marketing", "counsel_revops", "counsel_cs"],
14315
- enterprise: ["counsel_sales", "counsel_marketing", "counsel_revops", "counsel_cs"]
14316
- };
14317
14113
  }
14318
14114
  });
14319
14115
 
@@ -14351,7 +14147,7 @@ __export(gtm_counsel_exports, {
14351
14147
  });
14352
14148
  function buildCounselCatalogBlock() {
14353
14149
  const lines = COUNSEL_PACKS.map((p) => p.catalog_line).join("\n");
14354
- return `GTM counsel packs (function perspectives). Call get_counsel_detail for a pack body. Open get_role_card only for ids listed on the pack you pulled \u2014 do not enumerate every role. Never invent reorgs; never name methodology brands to the user.
14150
+ return `GTM consultant (one voice). Call get_counsel_detail with counsel_gtm for the pack body. Do not open role cards. Never invent reorgs; never name methodology brands to the user; never print pack ids or function seats as speakers.
14355
14151
  ${lines}`;
14356
14152
  }
14357
14153
  function buildOrgTreeContextLine(profile) {
@@ -14377,7 +14173,7 @@ function formatCounselDetail(pack, tree) {
14377
14173
  tree_weight: pack.tree_weight[tree],
14378
14174
  org_tree: tree,
14379
14175
  body: pack.body,
14380
- notice: "Apply moves in body/board_cut/operator_cut. Never name internal methodology brands to the user. Open get_role_card for listed role_card_ids when challenging owners."
14176
+ notice: "Apply moves in body/board_cut/operator_cut. Never name internal methodology brands to the user. Speak as one GTM consultant \u2014 never as competing seats or named-role opinions."
14381
14177
  };
14382
14178
  }
14383
14179
  function formatRoleCardDetail(card, tree) {
@@ -15026,21 +14822,33 @@ Hours saved, weekly activity trend, session counts, AI token usage, and the full
15026
14822
  Type reset to confirm. This clears hours and milestones. The install identity stays.`
15027
14823
  },
15028
14824
  {
15029
- name: "deepdive",
14825
+ name: "tour",
15030
14826
  raw: `---
15031
- name: deepdive
14827
+ name: tour
15032
14828
  description: Show slides for numbers and how to use NTRP
15033
14829
  section: Navigation
15034
14830
  args: [<metric>|guide|list|tour]
15035
- handler: ../commands/deepdive.ts
14831
+ handler: ../commands/tour.ts
15036
14832
  ---
15037
14833
 
15038
14834
  CLI slide deck: SaaS refresher, five vital signs (definition, formula, visual, dollar translation), then a how-to section (talk, ask, ship to your AI, loop).
15039
- Bare \`/deepdive\` runs the full onboarding tour. Type \`/deepdive guide\` to jump to how to use NTRP (includes teaching the inbox once).
15040
- Type \`/deepdive handoff\` for the ship-to-AI slide. Type \`/deepdive <metric>\` to jump to one metric card.
15041
- Type \`/deepdive list\` to print the catalog. Works without an AI key. Re-run anytime from the homescreen.
14835
+ Bare \`/tour\` runs the full onboarding tour. Type \`/tour guide\` to jump to how to use NTRP (includes teaching the inbox once).
14836
+ Type \`/tour handoff\` for the ship-to-AI slide. Type \`/tour <metric>\` to jump to one metric card.
14837
+ Type \`/tour list\` to print the catalog. Works without an AI key. Re-run anytime from the homescreen.
15042
14838
  During the tour: Enter advances, \`b\` / \`back\` goes to the previous slide, \`q\` quits. After the tour, the same \`b\` / \`back\` word steps back one confirm gate in the REPL (scope, data, strategy) \u2014 it does not unload data or undo compute.
15043
14839
  Live values overlay when an analysis exists.`
14840
+ },
14841
+ {
14842
+ name: "deepdive",
14843
+ raw: `---
14844
+ name: deepdive
14845
+ description: Retired \u2014 the slideshow is /tour
14846
+ section: Hidden
14847
+ hidden: true
14848
+ handler: ../commands/deepdive.ts
14849
+ ---
14850
+
14851
+ Former name for the numbers slideshow. Prints a one-line pointer to \`/tour\`. Does not open the deck.`
15044
14852
  },
15045
14853
  {
15046
14854
  name: "thinkwithme",
@@ -16275,39 +16083,19 @@ var init_tool_schemas = __esm({
16275
16083
  },
16276
16084
  {
16277
16085
  name: "get_counsel_detail",
16278
- description: "Read a GTM function counsel pack (sales, marketing, revops, exec, cs): when to use, kill rules, vital links, role cards to open, and instruction body. Use before recommending cross-functional work or during strategist roundtable. Never name methodology brands to the user; no invented reorgs.",
16086
+ description: "Read the GTM consultant pack (counsel_gtm): when to use, kill rules, vital links, and instruction body. Use before recommending cross-functional work or during strategist plan check. One voice \u2014 never seat competing functions. Never name methodology brands to the user; no invented reorgs.",
16279
16087
  parameters: {
16280
16088
  type: "object",
16281
16089
  properties: {
16282
16090
  counsel_id: {
16283
16091
  type: "string",
16284
16092
  maxLength: 40,
16285
- description: "Exact id: counsel_sales, counsel_marketing, counsel_revops, counsel_exec, or counsel_cs."
16093
+ description: "Exact id: counsel_gtm."
16286
16094
  }
16287
16095
  },
16288
16096
  required: ["counsel_id"]
16289
16097
  }
16290
16098
  },
16291
- {
16292
- name: "get_role_card",
16293
- description: "Read a GTM role expertise card (e.g. ae, sdr, vp_revops, csm): owns/does-not-own, metrics, consult questions, failure modes, and tensions. Call after get_counsel_detail when you need how a specific seat thinks. Annotates presence for the active org tree.",
16294
- parameters: {
16295
- type: "object",
16296
- properties: {
16297
- role_id: {
16298
- type: "string",
16299
- maxLength: 40,
16300
- description: "Exact role id listed on the pack you pulled via get_counsel_detail, e.g. 'ae', 'demand_gen', 'gtm_engineer'."
16301
- },
16302
- org_tree: {
16303
- type: "string",
16304
- enum: ["plg", "smb_velocity", "mid_market", "enterprise"],
16305
- description: "Optional org tree override; defaults to profile sales_motion / mid_market."
16306
- }
16307
- },
16308
- required: ["role_id"]
16309
- }
16310
- },
16311
16099
  {
16312
16100
  name: "get_session_brief",
16313
16101
  description: "Read the 1-page context brief of a PRIOR session by id or 4-char suffix: status, dataset, scope, computed scores with dollar values, headline metrics, deliverables, and conversation log. Use when the user references earlier work \u2014 'last week we found\u2026', 'compare with the previous analysis', 'what did session 9297 conclude?'. Read-only.",
@@ -18859,17 +18647,9 @@ function renderRuminationLog(job) {
18859
18647
  );
18860
18648
  if (job.library_path) lines.push(`Strategy: ${job.library_path}`);
18861
18649
  if (job.handoff_path) lines.push(`Handoff: ${job.handoff_path}`);
18862
- if (job.roundtable) {
18863
- lines.push("");
18864
- lines.push("## Counsel roundtable");
18650
+ if (job.roundtable?.digest) {
18865
18651
  lines.push("");
18866
18652
  lines.push(job.roundtable.digest);
18867
- if (job.roundtable.issues.length) {
18868
- lines.push("");
18869
- lines.push(
18870
- `Validation: ${job.roundtable.issues.map((entry) => entry.code).join(", ")}`
18871
- );
18872
- }
18873
18653
  }
18874
18654
  lines.push("");
18875
18655
  lines.push("## Rounds");
@@ -18998,7 +18778,7 @@ function consumeOrientEmptyEnterCoach(ctx) {
18998
18778
  if (!hasValidLicense()) {
18999
18779
  return "Type /activate to paste a key. Type /checkout if you need to sign up.";
19000
18780
  }
19001
- return "Type a question, type use demo data, or type /deepdive. Enter alone does not start a step here.";
18781
+ return "Type a question, type use demo data, or type /tour. Enter alone does not start a step here.";
19002
18782
  }
19003
18783
  function formatPhaseLabel(phase) {
19004
18784
  switch (phase) {
@@ -19918,7 +19698,7 @@ function buildExploreTeachingGhosts(ctx, staticExplore) {
19918
19698
  if (cached2 && cached2.length > 0) {
19919
19699
  const ghosts = asksToGhostHints(cached2);
19920
19700
  const gating = ctx.snapshot.computeResult?.aggregate.gating_vital_sign;
19921
- if (gating) ghosts.push(`try /deepdive ${gating}`);
19701
+ if (gating) ghosts.push(`try /tour ${gating}`);
19922
19702
  ghosts.push('try "how should we fix this?"');
19923
19703
  return dedupeGhosts(ghosts);
19924
19704
  }
@@ -19929,7 +19709,7 @@ function buildExploreTeachingGhosts(ctx, staticExplore) {
19929
19709
  });
19930
19710
  return dedupeGhosts([
19931
19711
  ...asksToGhostHints(asks),
19932
- `try /deepdive ${health.gating_vital_sign}`,
19712
+ `try /tour ${health.gating_vital_sign}`,
19933
19713
  'try "how should we fix this?"'
19934
19714
  ]);
19935
19715
  }
@@ -20535,7 +20315,7 @@ function buildDefinitionsAppendix(bundle, opts = {}) {
20535
20315
  lines.push(`## Metric definitions (for the ${audienceLabel(audience)})`);
20536
20316
  lines.push("");
20537
20317
  lines.push(
20538
- audience === "ops" ? "Formula-first brief for operators who execute the plan. Full slides: `/deepdive <metric>`." : "Meaning-first brief for the room. Full slides: `/deepdive <metric>`."
20318
+ audience === "ops" ? "Formula-first brief for operators who execute the plan. Full slides: `/tour <metric>`." : "Meaning-first brief for the room. Full slides: `/tour <metric>`."
20539
20319
  );
20540
20320
  lines.push("");
20541
20321
  for (const c of candidates) {
@@ -20622,8 +20402,8 @@ function generateMarkdownReport(data) {
20622
20402
  lines.push("");
20623
20403
  for (const entity of vs.entity_details.slice(0, 5)) {
20624
20404
  const name = entity.name ?? entity.id ?? "Unknown";
20625
- const issue2 = entity.issue ?? "";
20626
- lines.push(`- **${name}**: ${issue2}`);
20405
+ const issue = entity.issue ?? "";
20406
+ lines.push(`- **${name}**: ${issue}`);
20627
20407
  }
20628
20408
  if (vs.entity_details.length > 5) {
20629
20409
  lines.push(`- *...and ${vs.entity_details.length - 5} more*`);
@@ -21636,56 +21416,36 @@ Respond with the full plan as strict JSON matching this schema \u2014 no markdow
21636
21416
  ${STRATEGIST_PLAN_SCHEMA_BLOCK}`;
21637
21417
  }
21638
21418
  function buildRoundtableMessage(input) {
21639
- const bias = input.biasNotes.length > 0 ? `Bias notes:
21419
+ const bias = input.biasNotes.length > 0 ? `Weighing notes:
21640
21420
  - ${input.biasNotes.join("\n- ")}
21641
21421
 
21642
21422
  ` : "";
21643
- const owner = input.constraintOwner ? `Constraint-owning pack (must stay full seat): ${input.constraintOwner}
21644
-
21645
- ` : "";
21646
- const constraint = input.constraint ? `CONSTRAINT CHANNELS:
21423
+ const constraint = input.constraint ? `CONSTRAINT CHANNELS (weigh inside one recommendation \u2014 do not print as speakers):
21647
21424
  - Health gate: ${input.constraint.health_gate ? String(input.constraint.health_gate.id) : "not verified"} \u2192 ${input.constraint.health_gate_owners.join(", ") || "no mapped owner"}
21648
21425
  - Objective constraint: ${input.constraint.objective_constraint ? String(input.constraint.objective_constraint.id) : "not verified"} \u2192 ${input.constraint.objective_owners.join(", ") || "no mapped owner"}
21649
- - If these differ, preserve both seats and have exec sequence the objective behind the health gate unless evidence supports another order.
21426
+ - If both matter (e.g. freshness/new-logo dollars and red NRR/GRR), keep both in one sequence under one governing constraint. Do not drop retention.
21650
21427
 
21651
21428
  ` : "";
21652
- return `STAGE R \u2014 ROUNDTABLE. Org tree: ${input.tree}. Before stress-testing, seat GTM counsel packs and challenge the draft.
21429
+ return `STAGE R \u2014 PLAN CHECK. Org tree: ${input.tree}. Before stress-testing, run one GTM consultant check on the draft. Speak as NTRP \u2014 one voice. No seats, no CRO/CMO/CS/sales/marketing/RevOps/Exec nameplates.
21653
21430
 
21654
- ${owner}${constraint}${bias}EXEC MERGE SEAT: counsel_exec (full; call get_counsel_detail; output only in top-level exec).
21655
- FUNCTION SEATS (weight full = deep challenge; secondary/risk_note = short risk note only):
21656
- ${input.seatsBlock}
21657
-
21658
- Call get_counsel_detail only for full seats. risk_note and secondary seats write a short risk without a full pack pull. Cap get_role_card to one id per challenged workstream owner \u2014 pick from that pack's role_card_ids. Call get_play_detail if a play_id is thin. Do not invent reorgs or named people. Never name methodology brands in customer-facing fields.
21431
+ ${constraint}${bias}Call get_counsel_detail for counsel_gtm only. Do not call get_role_card. Call get_play_detail if a play_id is thin. Do not invent reorgs or named people. Never name methodology brands in customer-facing fields. Monday owners on actions stay function-shaped ("RevOps starts the path audit") \u2014 that is who does the work, not who is talking.
21659
21432
 
21660
21433
  DRAFT PLAN JSON:
21661
21434
  ${input.draftPlanJson}
21662
21435
 
21663
21436
  Respond with STRICT JSON only (no prose, no fences):
21664
21437
  {
21665
- "seats": [
21666
- {
21667
- "pack": "counsel_sales",
21668
- "stance": "support" | "challenge" | "abstain",
21669
- "constraint_agree": true,
21670
- "challenge": "\u226440 words \u2014 what the draft gets wrong or misses; empty only if support + risk or exec pre-mortem",
21671
- "missing_exam": "metric + date + instrument (or empty string)",
21672
- "cross_functional_risk": "\u226430 words",
21673
- "role_cards_used": ["ae"]
21674
- }
21675
- ],
21676
- "exec": {
21677
- "governing_constraint": "one constraint with dollar label",
21678
- "sequence": ["play or workstream order"],
21679
- "drop": ["non-constraint theater to kill"],
21680
- "merge_verdict": "\u226450 words",
21681
- "pre_mortem": "if all seats support: the one most likely death cause; else optional"
21682
- }
21438
+ "governing_constraint": "one constraint with dollar label",
21439
+ "sequence": ["first move", "second move", "third move"],
21440
+ "drop": "non-constraint theater to kill",
21441
+ "verdict": "\u226450 words \u2014 one recommendation that already weighed the tradeoffs",
21442
+ "pre_mortem": "required \u2014 the one most likely death cause if this plan ships as-is"
21683
21443
  }
21684
21444
 
21685
- Rules: do not invent a fake challenge. Empty challenge is allowed only with stance support AND a non-empty cross_functional_risk or a non-empty exec.pre_mortem. If every seat supports, exec MUST write a pre-mortem death cause. Constraint-owning pack may not abstain without a risk note.`;
21445
+ Rules: one consultant, one sequence, one governing constraint. Fold CS vs new-logo, quality vs volume, and systems vs theater into the recommendation \u2014 never as competing opinions. pre_mortem is always required. Never emit seats[], exec wrappers, pack ids as speakers, or CRO/CMO/CS says headers.`;
21686
21446
  }
21687
21447
  function buildStressTestMessage(roundtableDigest) {
21688
- const roundtableBlock = roundtableDigest ? `0. ROUNDTABLE DIGEST (mandatory): address every challenge \u2014 accept and revise, or reject with an instrument reason. Add missing_exam items or explicitly defer with why. Reflect exec.drop in the final plan. If every seat supported, exec pre-mortem is the death cause check 1 must defend \u2014 do not invent a fake challenge after the fact.
21448
+ const roundtableBlock = roundtableDigest ? `0. PLAN CHECK DIGEST (mandatory): address every challenge \u2014 accept and revise, or reject with an instrument reason. Add missing_exam items or explicitly defer with why. Reflect Drop in the final plan. If the check was all-agree, Pre-mortem is the death cause check 1 must defend \u2014 do not invent a fake challenge after the fact.
21689
21449
  ${roundtableDigest}
21690
21450
 
21691
21451
  ` : "";
@@ -22252,181 +22012,169 @@ function str2(value) {
22252
22012
  return typeof value === "string" ? value.trim() : "";
22253
22013
  }
22254
22014
  function strArray2(value) {
22015
+ if (typeof value === "string") {
22016
+ const t = value.trim();
22017
+ return t ? [t] : [];
22018
+ }
22255
22019
  return Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
22256
22020
  }
22257
22021
  function words(value) {
22258
22022
  return value.trim() ? value.trim().split(/\s+/).length : 0;
22259
22023
  }
22260
- function issue(issues, code, message, severity = "blocking") {
22024
+ function pushIssue(issues, code, message, severity = "blocking") {
22261
22025
  issues.push({ code, severity, message });
22262
22026
  }
22263
- function parseSeat(raw, issues, index) {
22264
- if (!raw || typeof raw !== "object") {
22265
- issue(issues, "seat_shape", `Seat ${index + 1} is not an object.`);
22266
- return null;
22267
- }
22268
- const obj = raw;
22269
- const pack = str2(obj.pack);
22270
- if (pack === "counsel_exec") {
22271
- issue(issues, "exec_in_seats", "counsel_exec must appear only in the top-level exec object.");
22272
- return null;
22273
- }
22274
- if (!FUNCTION_PACKS.has(pack)) {
22275
- issue(issues, "unknown_pack", `Seat ${index + 1} has unknown pack '${pack || "(empty)"}'.`);
22276
- return null;
22277
- }
22278
- const stance = str2(obj.stance);
22279
- if (!STANCES.has(stance)) {
22280
- issue(issues, "invalid_stance", `${pack} has invalid stance '${stance || "(empty)"}'.`);
22281
- }
22282
- if (typeof obj.constraint_agree !== "boolean") {
22283
- issue(issues, "constraint_agree", `${pack} must set constraint_agree to true or false.`);
22284
- }
22285
- const challenge = str2(obj.challenge);
22286
- const risk = str2(obj.cross_functional_risk);
22287
- const missingExam = str2(obj.missing_exam);
22288
- if (words(challenge) > 40) {
22289
- issue(issues, "challenge_length", `${pack} challenge exceeds 40 words.`);
22290
- }
22291
- if (words(risk) > 30) {
22292
- issue(issues, "risk_length", `${pack} cross_functional_risk exceeds 30 words.`);
22293
- }
22294
- const roleCards = strArray2(obj.role_cards_used);
22295
- const allowed = new Set(getCounselPackById(pack)?.role_card_ids ?? []);
22296
- const validRoles = [];
22297
- for (const roleId of roleCards) {
22298
- if (!getRoleCardById(roleId) || !allowed.has(roleId)) {
22299
- issue(
22027
+ function scanBannedSpeakers(text, issues, field) {
22028
+ for (const pattern of BANNED_SPEAKER_PATTERNS) {
22029
+ if (pattern.test(text)) {
22030
+ pushIssue(
22300
22031
  issues,
22301
- "role_not_in_pack",
22302
- `${pack} used role card '${roleId}' that is not listed on that pack.`
22032
+ "banned_speaker",
22033
+ `${field} must not name competing seats or role speakers.`
22303
22034
  );
22304
- continue;
22035
+ return;
22305
22036
  }
22306
- validRoles.push(roleId);
22307
22037
  }
22308
- return {
22309
- pack,
22310
- stance: STANCES.has(stance) ? stance : "abstain",
22311
- constraint_agree: obj.constraint_agree === true,
22312
- challenge,
22313
- missing_exam: missingExam,
22314
- cross_functional_risk: risk,
22315
- role_cards_used: validRoles
22316
- };
22317
22038
  }
22318
- function parseExec(raw, issues) {
22039
+ function parseConsultantCheck(raw, issues) {
22319
22040
  if (!raw || typeof raw !== "object") {
22320
- issue(issues, "missing_exec", "Roundtable requires a top-level exec object.");
22041
+ pushIssue(issues, "consultant_shape", "Consultant check output is not a JSON object.");
22321
22042
  return null;
22322
22043
  }
22323
22044
  const obj = raw;
22045
+ if ("seats" in obj) {
22046
+ pushIssue(issues, "seats_forbidden", "Consultant check must not include a seats array.");
22047
+ }
22048
+ if (obj.exec && typeof obj.exec === "object") {
22049
+ pushIssue(
22050
+ issues,
22051
+ "exec_wrapper_forbidden",
22052
+ "Consultant check must be a flat object \u2014 not seats[] + exec."
22053
+ );
22054
+ }
22324
22055
  const governing = str2(obj.governing_constraint);
22325
- const verdict = str2(obj.merge_verdict);
22326
- if (!governing) issue(issues, "exec_constraint", "exec.governing_constraint is required.");
22327
- if (!verdict) issue(issues, "exec_verdict", "exec.merge_verdict is required.");
22328
- if (words(verdict) > 50) {
22329
- issue(issues, "exec_verdict_length", "exec.merge_verdict exceeds 50 words.");
22056
+ const sequence = strArray2(obj.sequence);
22057
+ const dropRaw = obj.drop;
22058
+ const drop = Array.isArray(dropRaw) ? strArray2(dropRaw).join("; ") : str2(dropRaw);
22059
+ const verdict = str2(obj.verdict) || str2(obj.merge_verdict);
22060
+ const preMortem = str2(obj.pre_mortem) || str2(obj.preMortem);
22061
+ if (!governing) pushIssue(issues, "governing_constraint", "governing_constraint is required.");
22062
+ if (!verdict) pushIssue(issues, "verdict", "verdict is required.");
22063
+ if (words(verdict) > 50) pushIssue(issues, "verdict_length", "verdict exceeds 50 words.");
22064
+ if (!preMortem) {
22065
+ pushIssue(
22066
+ issues,
22067
+ "missing_pre_mortem",
22068
+ "pre_mortem is required (all-agree checks still need a death cause)."
22069
+ );
22070
+ }
22071
+ for (const [field, text] of [
22072
+ ["governing_constraint", governing],
22073
+ ["verdict", verdict],
22074
+ ["drop", drop],
22075
+ ["pre_mortem", preMortem],
22076
+ ["sequence", sequence.join(" ")]
22077
+ ]) {
22078
+ if (text) scanBannedSpeakers(text, issues, field);
22330
22079
  }
22331
22080
  return {
22332
22081
  governing_constraint: governing,
22333
- sequence: strArray2(obj.sequence),
22334
- drop: strArray2(obj.drop),
22335
- merge_verdict: verdict,
22336
- pre_mortem: str2(obj.pre_mortem)
22082
+ sequence,
22083
+ drop,
22084
+ verdict,
22085
+ pre_mortem: preMortem
22337
22086
  };
22338
22087
  }
22339
- function validateRoundtableResult(raw, options) {
22088
+ function validateRoundtableResult(raw, options = {}) {
22340
22089
  const issues = [];
22341
22090
  if (!raw || typeof raw !== "object") {
22342
22091
  return {
22343
22092
  result: null,
22344
- issues: [{ code: "roundtable_shape", severity: "blocking", message: "Roundtable output is not a JSON object." }],
22093
+ issues: [
22094
+ {
22095
+ code: "consultant_shape",
22096
+ severity: "blocking",
22097
+ message: "Consultant check output is not a JSON object."
22098
+ }
22099
+ ],
22345
22100
  blocking: true
22346
22101
  };
22347
22102
  }
22348
- const obj = raw;
22349
- if (!Array.isArray(obj.seats)) {
22350
- issue(issues, "missing_seats", "Roundtable requires a seats array.");
22351
- }
22352
- const seats = (Array.isArray(obj.seats) ? obj.seats : []).map((seat, index) => parseSeat(seat, issues, index)).filter((seat) => seat != null);
22353
- const exec = parseExec(obj.exec, issues);
22354
- const seen = /* @__PURE__ */ new Set();
22355
- for (const seat of seats) {
22356
- if (seen.has(seat.pack)) {
22357
- issue(issues, "duplicate_seat", `Roundtable contains duplicate seat ${seat.pack}.`);
22358
- }
22359
- seen.add(seat.pack);
22360
- }
22361
- const expected = options.seating.seats.filter((seat) => seat.pack_id !== "counsel_exec").map((seat) => seat.pack_id);
22362
- for (const pack of expected) {
22363
- if (!seen.has(pack)) issue(issues, "missing_seat", `Roundtable omitted seated pack ${pack}.`);
22364
- }
22365
- for (const pack of seen) {
22366
- if (!expected.includes(pack)) issue(issues, "unexpected_seat", `Roundtable emitted unseated pack ${pack}.`);
22367
- }
22103
+ const parsed = parseConsultantCheck(raw, issues);
22368
22104
  const pulls = new Set(options.counselPulls ?? []);
22369
22105
  if (options.requireCounselPulls !== false) {
22370
- const fullPacks = options.seating.seats.filter((seat) => seat.weight === "full").map((seat) => seat.pack_id);
22371
- for (const pack of fullPacks) {
22372
- if (!pulls.has(pack)) {
22373
- issue(issues, "missing_counsel_pull", `Full seat ${pack} did not call get_counsel_detail.`);
22374
- }
22375
- }
22376
- }
22377
- if (exec) {
22378
- const allSupport = seats.length > 0 && seats.every((seat) => seat.stance === "support");
22379
- if (allSupport && !exec.pre_mortem) {
22380
- issue(issues, "missing_pre_mortem", "All-support roundtables require exec.pre_mortem.");
22106
+ if (!pulls.has("counsel_gtm")) {
22107
+ pushIssue(
22108
+ issues,
22109
+ "missing_counsel_pull",
22110
+ "Consultant check must call get_counsel_detail for counsel_gtm."
22111
+ );
22381
22112
  }
22382
- for (const seat of seats) {
22383
- if (!seat.challenge && !(seat.stance === "support" && (Boolean(seat.cross_functional_risk) || Boolean(exec.pre_mortem)))) {
22384
- issue(
22113
+ for (const pull of pulls) {
22114
+ if (pull !== "counsel_gtm") {
22115
+ pushIssue(
22385
22116
  issues,
22386
- "empty_challenge",
22387
- `${seat.pack} may leave challenge empty only with support plus risk or exec pre-mortem.`
22117
+ "forbidden_counsel_pull",
22118
+ `Only counsel_gtm may be pulled (saw '${pull}').`
22388
22119
  );
22389
22120
  }
22390
- if (seat.stance === "abstain" && !seat.cross_functional_risk) {
22391
- issue(issues, "abstain_without_risk", `${seat.pack} abstained without a risk note.`);
22392
- }
22393
22121
  }
22394
22122
  }
22395
- for (const owner of options.constraintOwners) {
22396
- const seat = seats.find((candidate) => candidate.pack === owner);
22397
- if (!seat) continue;
22398
- if (seat.stance === "abstain" && !seat.cross_functional_risk) {
22399
- issue(issues, "owner_abstain", `Constraint owner ${owner} abstained without a risk note.`);
22123
+ if (options.requireRetentionInSequence && parsed) {
22124
+ const blob = `${parsed.governing_constraint} ${parsed.sequence.join(" ")} ${parsed.verdict} ${parsed.drop}`.toLowerCase();
22125
+ const hasRetention = /nrr|grr|retention|renewal|churn|expansion/.test(blob);
22126
+ const hasNewLogo = /freshness|new[- ]?logo|acquisition|inbound|outbound/.test(blob);
22127
+ if (hasNewLogo && !hasRetention) {
22128
+ pushIssue(
22129
+ issues,
22130
+ "retention_dropped",
22131
+ "Dual-dollar check: new-logo/freshness present but retention/NRR/GRR absent from sequence.",
22132
+ "advisory"
22133
+ );
22400
22134
  }
22401
22135
  }
22402
22136
  const blocking = issues.some((entry) => entry.severity === "blocking");
22403
22137
  return {
22404
- result: exec ? { seats, exec } : null,
22138
+ result: blocking ? null : parsed,
22405
22139
  issues,
22406
22140
  blocking
22407
22141
  };
22408
22142
  }
22409
- function parseAndValidateRoundtable(text, options) {
22143
+ function parseAndValidateRoundtable(text, options = {}) {
22410
22144
  return validateRoundtableResult(parseJsonObjectFromText(text), options);
22411
22145
  }
22412
22146
  function formatRoundtableDigestFromResult(result) {
22413
- const lines = result.seats.map(
22414
- (seat) => `- ${seat.pack} [${seat.stance}]: ${seat.challenge || "(no challenge)"}${seat.missing_exam ? ` | missing exam: ${seat.missing_exam}` : ""}${seat.cross_functional_risk ? ` | risk: ${seat.cross_functional_risk}` : ""}`
22415
- );
22416
- lines.push(`EXEC governing: ${result.exec.governing_constraint || "(none)"}`);
22417
- if (result.exec.sequence.length) lines.push(`EXEC sequence: ${result.exec.sequence.join(" \u2192 ")}`);
22418
- if (result.exec.drop.length) lines.push(`EXEC drop: ${result.exec.drop.join("; ")}`);
22419
- if (result.exec.merge_verdict) lines.push(`EXEC verdict: ${result.exec.merge_verdict}`);
22420
- if (result.exec.pre_mortem) lines.push(`EXEC pre-mortem: ${result.exec.pre_mortem}`);
22147
+ const lines = [];
22148
+ if (result.governing_constraint) {
22149
+ lines.push(`Governing constraint: ${result.governing_constraint}`);
22150
+ }
22151
+ if (result.sequence.length) {
22152
+ lines.push(`Sequence: ${result.sequence.join(" then ")}`);
22153
+ }
22154
+ if (result.drop) {
22155
+ lines.push(`Drop: ${result.drop}`);
22156
+ }
22157
+ if (result.verdict) {
22158
+ lines.push(`Verdict: ${result.verdict}`);
22159
+ }
22160
+ if (result.pre_mortem) {
22161
+ lines.push(`Pre-mortem: ${result.pre_mortem}`);
22162
+ }
22421
22163
  return lines.join("\n");
22422
22164
  }
22423
- function buildRoundtableRetryMessage(validation, seating) {
22424
- const required = seating.seats.filter((seat) => seat.pack_id !== "counsel_exec").map((seat) => seat.pack_id).join(", ");
22165
+ function buildRoundtableRetryMessage(validation, _seating) {
22425
22166
  const failures = validation.issues.filter((entry) => entry.severity === "blocking").map((entry) => `${entry.code}: ${entry.message}`).join("\n- ");
22426
- return `Roundtable JSON did not validate.
22427
- - ${failures || "roundtable_shape: unusable JSON"}
22428
- Required function seats: ${required}.
22429
- Respond with ONLY corrected roundtable JSON. counsel_exec belongs only in top-level exec. All-support requires exec.pre_mortem.`;
22167
+ return `Plan check JSON did not validate.
22168
+ - ${failures || "consultant_shape: unusable JSON"}
22169
+ Respond with ONLY corrected flat JSON (no seats, no exec wrapper):
22170
+ {
22171
+ "governing_constraint": "...",
22172
+ "sequence": ["a", "b", "c"],
22173
+ "drop": "...",
22174
+ "verdict": "...",
22175
+ "pre_mortem": "..."
22176
+ }
22177
+ Call get_counsel_detail for counsel_gtm only. pre_mortem is required. Never print CRO/CMO/CS/sales/marketing/RevOps/Exec as speakers.`;
22430
22178
  }
22431
22179
  function normalizedPlanText(plan) {
22432
22180
  return JSON.stringify(plan).toLowerCase();
@@ -22438,69 +22186,55 @@ function checkRoundtableReflection(input) {
22438
22186
  const issues = [];
22439
22187
  const draft = normalizedPlanText(input.draftPlan);
22440
22188
  const final = normalizedPlanText(input.finalPlan);
22441
- for (const dropped of input.roundtable.exec.drop) {
22442
- const needle = dropped.toLowerCase();
22443
- if (needle.length >= 4 && draft.includes(needle) && final.includes(needle)) {
22444
- issue(
22445
- issues,
22446
- "roundtable_drop_ignored",
22447
- `Final plan still contains exec drop '${dropped}'.`,
22448
- "advisory"
22449
- );
22450
- }
22451
- }
22452
- for (const seat of input.roundtable.seats) {
22453
- if (seat.missing_exam) {
22454
- const tokens = significantTokens(seat.missing_exam);
22455
- if (tokens.length > 0 && !tokens.some((token) => final.includes(token))) {
22456
- issue(
22189
+ if (input.roundtable.drop) {
22190
+ for (const dropped of input.roundtable.drop.split(/;|\n/).map((part) => part.trim()).filter(Boolean)) {
22191
+ const needle = dropped.toLowerCase();
22192
+ if (needle.length >= 4 && draft.includes(needle) && final.includes(needle)) {
22193
+ pushIssue(
22457
22194
  issues,
22458
- "roundtable_exam_missing",
22459
- `${seat.pack} missing exam is not visible in the final plan.`,
22195
+ "consultant_drop_ignored",
22196
+ `Final plan still contains drop '${dropped}'.`,
22460
22197
  "advisory"
22461
22198
  );
22462
22199
  }
22463
22200
  }
22464
- if (seat.stance === "challenge" && seat.challenge && draft === final) {
22465
- issue(
22466
- issues,
22467
- "roundtable_unaddressed",
22468
- `${seat.pack} challenged the draft but the final plan is unchanged.`,
22469
- "advisory"
22470
- );
22471
- }
22472
22201
  }
22473
- const allSupport = input.roundtable.seats.length > 0 && input.roundtable.seats.every((seat) => seat.stance === "support");
22474
- if (allSupport && input.roundtable.exec.pre_mortem) {
22202
+ if (input.roundtable.pre_mortem) {
22475
22203
  const defensiveText = JSON.stringify({
22476
22204
  risks: input.finalPlan.risks,
22477
22205
  assumptions: input.finalPlan.assumptions,
22478
22206
  contingencies: input.finalPlan.workstreams.map((ws) => ws.contingency)
22479
22207
  }).toLowerCase();
22480
- const tokens = significantTokens(input.roundtable.exec.pre_mortem);
22208
+ const tokens = significantTokens(input.roundtable.pre_mortem);
22481
22209
  if (tokens.length > 0 && !tokens.some((token) => defensiveText.includes(token))) {
22482
- issue(
22210
+ pushIssue(
22483
22211
  issues,
22484
- "roundtable_premortem_missing",
22485
- "Final risks and contingencies do not reflect exec.pre_mortem.",
22212
+ "consultant_premortem_missing",
22213
+ "Final risks and contingencies do not reflect pre_mortem.",
22486
22214
  "advisory"
22487
22215
  );
22488
22216
  }
22489
22217
  }
22490
22218
  return { issues, blocking: false };
22491
22219
  }
22492
- var FUNCTION_PACKS, STANCES;
22220
+ var BANNED_SPEAKER_PATTERNS;
22493
22221
  var init_roundtable_validate = __esm({
22494
22222
  "src/ai/roundtable-validate.ts"() {
22495
22223
  "use strict";
22496
- init_gtm_counsel();
22497
22224
  init_strategist_validate();
22498
- FUNCTION_PACKS = new Set(
22499
- listCounselPackIds().filter(
22500
- (id) => id !== "counsel_exec"
22501
- )
22502
- );
22503
- STANCES = /* @__PURE__ */ new Set(["support", "challenge", "abstain"]);
22225
+ BANNED_SPEAKER_PATTERNS = [
22226
+ /\bCRO\s+says\b/i,
22227
+ /\bCMO\s+says\b/i,
22228
+ /\bCS\s+says\b/i,
22229
+ /\bsales\s+says\b/i,
22230
+ /\bfrom a marketing lens\b/i,
22231
+ /\bfrom a sales lens\b/i,
22232
+ /\bcounsel_sales\b/,
22233
+ /\bcounsel_marketing\b/,
22234
+ /\bcounsel_revops\b/,
22235
+ /\bcounsel_cs\b/,
22236
+ /\bcounsel_exec\b/
22237
+ ];
22504
22238
  }
22505
22239
  });
22506
22240
 
@@ -23077,9 +22811,7 @@ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`
23077
22811
  const observeRoundtableTool = (name, args) => {
23078
22812
  if (name !== "get_counsel_detail") return;
23079
22813
  const counselId = typeof args.counsel_id === "string" ? args.counsel_id : "";
23080
- if (["counsel_sales", "counsel_marketing", "counsel_revops", "counsel_exec", "counsel_cs"].includes(
23081
- counselId
23082
- )) {
22814
+ if (counselId === "counsel_gtm") {
23083
22815
  counselPulls.add(counselId);
23084
22816
  }
23085
22817
  };
@@ -23099,7 +22831,7 @@ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`
23099
22831
  const roundtableText = yield* runStage(
23100
22832
  "strategist",
23101
22833
  ROUNDTABLE_MAX_ROUNDS,
23102
- "Tool budget reached for roundtable. Respond with the roundtable JSON now \u2014 strict JSON only.",
22834
+ "Tool budget reached for the plan check. Respond with the plan-check JSON now \u2014 strict JSON only.",
23103
22835
  true,
23104
22836
  observeRoundtableTool
23105
22837
  );
@@ -23117,7 +22849,7 @@ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`
23117
22849
  const retryText = yield* runStage(
23118
22850
  "strategist",
23119
22851
  2,
23120
- "Respond with ONLY corrected roundtable JSON now. No prose.",
22852
+ "Respond with ONLY corrected plan-check JSON now. No prose.",
23121
22853
  true,
23122
22854
  observeRoundtableTool
23123
22855
  );
@@ -23132,13 +22864,13 @@ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`
23132
22864
  const roundtableIssues = [...roundtableValidation.issues];
23133
22865
  let roundtableDigest = roundtableResult ? formatRoundtableDigestFromResult(roundtableResult) : "";
23134
22866
  if (!roundtableResult) {
23135
- roundtableDigest = "(Roundtable JSON failed structural validation \u2014 Stage C must run a pre-mortem, preserve both constraint channels, and avoid rubber-stamping the draft.)";
22867
+ roundtableDigest = "Plan check didn't land. Stress-testing still runs.";
23136
22868
  yield {
23137
22869
  type: "notice",
23138
- text: `Roundtable validation failed: ${roundtableValidation.issues.filter((entry) => entry.severity === "blocking").map((entry) => entry.code).join(", ") || "unknown shape"}.`
22870
+ text: "Plan check didn't land. Stress-testing still runs."
23139
22871
  };
23140
22872
  } else {
23141
- yield { type: "notice", text: "Roundtable counsel captured \u2014 feeding into stress test." };
22873
+ yield { type: "notice", text: "Recommendation locked. Stress-testing next." };
23142
22874
  }
23143
22875
  yield { type: "stage", stage: "stress", label: STAGE_LABELS.stress };
23144
22876
  messages.push({ role: "user", content: buildStressTestMessage(roundtableDigest) });
@@ -23176,8 +22908,8 @@ Respond with ONLY the corrected plan JSON object in the required schema (title,
23176
22908
  text: "Strategist JSON failed validation \u2014 emitting grounded fallback plan from live vitals."
23177
22909
  };
23178
22910
  }
23179
- for (const issue2 of validated.issues) {
23180
- yield { type: "notice", text: issue2 };
22911
+ for (const issue of validated.issues) {
22912
+ yield { type: "notice", text: issue };
23181
22913
  }
23182
22914
  if (roundtableResult && candidatePlan) {
23183
22915
  const reflection = checkRoundtableReflection({
@@ -23187,10 +22919,6 @@ Respond with ONLY the corrected plan JSON object in the required schema (title,
23187
22919
  });
23188
22920
  for (const reflectionIssue of reflection.issues) {
23189
22921
  roundtableIssues.push(reflectionIssue);
23190
- yield {
23191
- type: "notice",
23192
- text: `Counsel reflection (${reflectionIssue.code}): ${reflectionIssue.message}`
23193
- };
23194
22922
  }
23195
22923
  }
23196
22924
  yield {
@@ -23299,7 +23027,7 @@ var init_strategist2 = __esm({
23299
23027
  STAGE_LABELS = {
23300
23028
  ground: "Grounding \u2014 reading health, metrics, segments, history",
23301
23029
  backcast: "Sequencing \u2014 backcasting from objective",
23302
- roundtable: "Roundtable \u2014 sales, marketing, RevOps, CS, exec counsel",
23030
+ roundtable: "Checking the plan",
23303
23031
  stress: "Stress-testing \u2014 capacity, measurability, timeline"
23304
23032
  };
23305
23033
  }
@@ -23331,6 +23059,10 @@ function recordScoreStreak(job, score) {
23331
23059
  }
23332
23060
  job.last_critic_score = score;
23333
23061
  }
23062
+ function formatBlockingGapForUser(gap) {
23063
+ if (gap.code === "roundtable_invalid") return "Plan check didn't land.";
23064
+ return `Plan gap (${gap.code}): ${gap.note}`;
23065
+ }
23334
23066
  async function* strategistCraftSession(options) {
23335
23067
  assertReplAi(options.ctx);
23336
23068
  const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -23373,8 +23105,8 @@ async function* strategistCraftSession(options) {
23373
23105
  const gap = {
23374
23106
  code: "roundtable_invalid",
23375
23107
  severity: "blocking",
23376
- note: !job.roundtable ? "No validated counsel roundtable is attached to this plan." : job.roundtable.counsel_fingerprint !== currentFingerprint ? "The latest counsel roundtable is stale for the current plan." : "The latest counsel roundtable failed structural validation.",
23377
- fix: "Re-run Stage R with every seated pack, required counsel pulls, and a valid exec merge."
23108
+ note: !job.roundtable ? "No validated plan check is attached to this plan." : job.roundtable.counsel_fingerprint !== currentFingerprint ? "The latest plan check is stale for the current plan." : "The latest plan check failed structural validation.",
23109
+ fix: "Re-run the plan check until it lands a single consultant recommendation with a pre-mortem."
23378
23110
  };
23379
23111
  scored.gaps.push(gap);
23380
23112
  scored.blocking.push(gap);
@@ -23452,7 +23184,7 @@ async function* strategistCraftSession(options) {
23452
23184
  };
23453
23185
  const rubric = scorePlan(locked);
23454
23186
  job.rubric_gaps = rubric.gaps;
23455
- yield { ...event, plan: locked, issues: [...event.issues, ...rubric.blocking.map((g) => `Plan gap (${g.code}): ${g.note}`)] };
23187
+ yield { ...event, plan: locked, issues: [...event.issues, ...rubric.blocking.map(formatBlockingGapForUser)] };
23456
23188
  } else if (event.type === "done") {
23457
23189
  lastMeta = event.usage ?? lastMeta;
23458
23190
  job.cumulative_input_tokens += event.usage?.input_tokens ?? 0;
@@ -23522,7 +23254,7 @@ async function* strategistCraftSession(options) {
23522
23254
  planJson: JSON.stringify(plan),
23523
23255
  rubricGaps: [
23524
23256
  rubric.blocking.map((g) => `${g.code}: ${g.note}`).join("\n") || "(none)",
23525
- lastRoundtableDigest ? `ROUNDTABLE DIGEST (address or reject with instruments):
23257
+ lastRoundtableDigest ? `PLAN CHECK DIGEST (address or reject with instruments):
23526
23258
  ${lastRoundtableDigest}` : ""
23527
23259
  ].filter(Boolean).join("\n\n"),
23528
23260
  healthSnapshot
@@ -23634,7 +23366,7 @@ ${lastRoundtableDigest}` : ""
23634
23366
  if (nextFp !== lastCounselFp) {
23635
23367
  yield {
23636
23368
  type: "notice",
23637
- text: "Plan plays/constraints shifted \u2014 re-running counsel roundtable pass."
23369
+ text: "Plan shifted. Checking the plan again."
23638
23370
  };
23639
23371
  try {
23640
23372
  const profile = loadProfile();
@@ -23712,7 +23444,7 @@ ${lastRoundtableDigest}` : ""
23712
23444
  requireCounselPulls: false
23713
23445
  });
23714
23446
  const result = validation.blocking ? null : validation.result;
23715
- lastRoundtableDigest = result ? formatRoundtableDigestFromResult(result) : "(Craft counsel re-entry failed structural validation; critic must not ship until Stage R is valid.)";
23447
+ lastRoundtableDigest = result ? formatRoundtableDigestFromResult(result) : "Plan check didn't land.";
23716
23448
  const record = {
23717
23449
  seating,
23718
23450
  result,
@@ -23728,7 +23460,7 @@ ${lastRoundtableDigest}` : ""
23728
23460
  lastCounselFp = "";
23729
23461
  yield {
23730
23462
  type: "notice",
23731
- text: `Craft roundtable invalid: ${validation.issues.filter((entry) => entry.severity === "blocking").map((entry) => entry.code).join(", ")}.`
23463
+ text: "Plan check didn't land."
23732
23464
  };
23733
23465
  } else {
23734
23466
  lastCounselFp = nextFp;
@@ -23755,7 +23487,7 @@ ${lastRoundtableDigest}` : ""
23755
23487
  }
23756
23488
  yield {
23757
23489
  type: "notice",
23758
- text: `Craft roundtable re-entry failed: ${String(err.message ?? err)}.`
23490
+ text: "Plan check didn't land."
23759
23491
  };
23760
23492
  }
23761
23493
  }
@@ -23776,7 +23508,7 @@ ${lastRoundtableDigest}` : ""
23776
23508
  yield {
23777
23509
  type: "plan",
23778
23510
  plan: job.plan,
23779
- issues: rubric.blocking.map((g) => `Plan gap (${g.code}): ${g.note}`),
23511
+ issues: rubric.blocking.map(formatBlockingGapForUser),
23780
23512
  measurable_targets: measurable.measurable_targets,
23781
23513
  total_targets: measurable.total_targets,
23782
23514
  baseline_batch_id: options.baselineBatchId ?? null
@@ -23873,12 +23605,6 @@ function renderCraftHandoffMarkdown(opts) {
23873
23605
  lines.push(formatWorkstreamMarkdown(ws).trimEnd());
23874
23606
  lines.push("");
23875
23607
  }
23876
- if (opts.roundtableDigest) {
23877
- lines.push("## Counsel roundtable");
23878
- lines.push("");
23879
- lines.push(opts.roundtableDigest);
23880
- lines.push("");
23881
- }
23882
23608
  const risks = filterOperatorLines(plan.risks);
23883
23609
  if (risks.length > 0) {
23884
23610
  lines.push("## Risks");
@@ -24922,8 +24648,8 @@ var init_ghost_hints = __esm({
24922
24648
  PHASE_GHOST_HINTS = {
24923
24649
  orient: [
24924
24650
  'try "pipeline health"',
24925
- "try /deepdive",
24926
- "try /deepdive guide",
24651
+ "try /tour",
24652
+ "try /tour guide",
24927
24653
  'try "is our retention real for the board?"',
24928
24654
  'try "what is the most expensive problem to solve?"',
24929
24655
  'try "board deck on Q3"'
@@ -24940,7 +24666,7 @@ var init_ghost_hints = __esm({
24940
24666
  'try "how should we fix this?"',
24941
24667
  'try "which segment is weakest?"',
24942
24668
  'try "what is ARR?"',
24943
- "try /deepdive freshness",
24669
+ "try /tour freshness",
24944
24670
  'try "ship a board deck"'
24945
24671
  ]
24946
24672
  };
@@ -24999,7 +24725,7 @@ function buildSituationalAwarenessBlock(ctx, opts = {}) {
24999
24725
  "Already done: scope confirmed; compute ran; do not re-propose scope or re-run compute unless the user asks to restart"
25000
24726
  );
25001
24727
  lines.push(
25002
- "Available now: answer from the artifact; suggest at most one of /strategy, /handoff, /deepdive <metric>, /thinkwithme, /playbook"
24728
+ "Available now: answer from the artifact; suggest at most one of /strategy, /handoff, /tour <metric>, /thinkwithme, /playbook"
25003
24729
  );
25004
24730
  if (channel === "explore" && responseMode === "deep") {
25005
24731
  lines.push(
@@ -25662,7 +25388,7 @@ function tryKeylessDefinitionAnswer(ctx, input) {
25662
25388
  }
25663
25389
  console.log();
25664
25390
  console.log(
25665
- " " + chalk23.dim("More: ") + paint("accent", `/deepdive ${explainer.id}`) + chalk23.dim(" \xB7 full tour: ") + paint("accent", "/deepdive")
25391
+ " " + chalk23.dim("More: ") + paint("accent", `/tour ${explainer.id}`) + chalk23.dim(" \xB7 full tour: ") + paint("accent", "/tour")
25666
25392
  );
25667
25393
  console.log();
25668
25394
  recordMessage(ctx, "user", input);
@@ -25739,7 +25465,7 @@ async function handleExploreWithoutKey(ctx, input) {
25739
25465
  console.log();
25740
25466
  console.log(" " + chalk24.yellow("No key is connected. Q&A stays off until you type ") + paint("accent", "/connect") + chalk24.yellow("."));
25741
25467
  console.log(" " + chalk24.dim("These commands work without a key:"));
25742
- console.log(" " + paint("accent", "/deepdive") + chalk24.dim(" slides for numbers and how to use NTRP"));
25468
+ console.log(" " + paint("accent", "/tour") + chalk24.dim(" slides for numbers and how to use NTRP"));
25743
25469
  console.log(" " + paint("accent", "/playbook") + chalk24.dim(" recommended plays from your computed vitals"));
25744
25470
  console.log(" " + chalk24.cyan('"how should we fix this?"') + chalk24.dim(" a simple plan. No AI."));
25745
25471
  console.log(" " + paint("accent", "/handoff") + chalk24.dim(" write this analysis for another tool"));
@@ -25747,7 +25473,7 @@ async function handleExploreWithoutKey(ctx, input) {
25747
25473
  recordMessage(
25748
25474
  ctx,
25749
25475
  "agent",
25750
- "No key is connected. Offered paths without a key (/deepdive, /playbook, simple plan, /handoff)."
25476
+ "No key is connected. Offered paths without a key (/tour, /playbook, simple plan, /handoff)."
25751
25477
  );
25752
25478
  }
25753
25479
  var NO_KEY_NUDGES;
@@ -26579,7 +26305,7 @@ function printFindingInline2(finding) {
26579
26305
  if (play) console.log(" " + chalk25.dim("\u2192 " + play.play_name + " \u2014 " + play.rationale));
26580
26306
  if (finding.recommended_focus) {
26581
26307
  console.log(
26582
- " " + chalk25.dim("How this works: ") + paint("accent", `/deepdive ${finding.recommended_focus}`)
26308
+ " " + chalk25.dim("How this works: ") + paint("accent", `/tour ${finding.recommended_focus}`)
26583
26309
  );
26584
26310
  }
26585
26311
  }
@@ -31203,8 +30929,8 @@ var init_diagnose = __esm({
31203
30929
  function summarizeEntityDetails(details = []) {
31204
30930
  const summary = {};
31205
30931
  for (const detail of details) {
31206
- const issue2 = typeof detail.issue === "string" ? detail.issue : "unclassified";
31207
- summary[issue2] = (summary[issue2] ?? 0) + 1;
30932
+ const issue = typeof detail.issue === "string" ? detail.issue : "unclassified";
30933
+ summary[issue] = (summary[issue] ?? 0) + 1;
31208
30934
  }
31209
30935
  return summary;
31210
30936
  }
@@ -31684,8 +31410,8 @@ async function handleGetVitalSignDetail(input, ctx) {
31684
31410
  if (!vital) return { error: `Vital sign '${vitalSign}' not found` };
31685
31411
  const entitySummary = {};
31686
31412
  for (const detail of vital.entity_details) {
31687
- const issue2 = detail.issue;
31688
- const key = issue2 ?? "unclassified";
31413
+ const issue = detail.issue;
31414
+ const key = issue ?? "unclassified";
31689
31415
  entitySummary[key] = (entitySummary[key] ?? 0) + 1;
31690
31416
  }
31691
31417
  return {
@@ -32000,27 +31726,6 @@ async function handleGetCounselDetail(input) {
32000
31726
  }
32001
31727
  return formatCounselDetail2(pack, getActiveOrgTree2());
32002
31728
  }
32003
- async function handleGetRoleCard(input) {
32004
- const roleId = typeof input.role_id === "string" ? input.role_id.trim() : "";
32005
- const {
32006
- getRoleCardById: getRoleCardById2,
32007
- listRoleCardIds: listRoleCardIds2,
32008
- formatRoleCardDetail: formatRoleCardDetail2,
32009
- getActiveOrgTree: getActiveOrgTree2,
32010
- listOrgTreeIds: listOrgTreeIds2
32011
- } = await Promise.resolve().then(() => (init_gtm_counsel(), gtm_counsel_exports));
32012
- const card = roleId ? getRoleCardById2(roleId) : void 0;
32013
- if (!card) {
32014
- return {
32015
- error: `Unknown role id '${roleId}'.`,
32016
- valid_role_ids: listRoleCardIds2()
32017
- };
32018
- }
32019
- const treeArg = typeof input.org_tree === "string" ? input.org_tree.trim() : "";
32020
- const trees = listOrgTreeIds2();
32021
- const tree = trees.includes(treeArg) ? treeArg : getActiveOrgTree2();
32022
- return formatRoleCardDetail2(card, tree);
32023
- }
32024
31729
  async function handleGetRevenueMetrics(_input, ctx) {
32025
31730
  if (!ctx.metrics || ctx.metrics.length === 0) {
32026
31731
  const { computeFullMetrics: computeFullMetrics2 } = await Promise.resolve().then(() => (init_compute(), compute_exports));
@@ -32317,7 +32022,6 @@ var init_tool_handlers = __esm({
32317
32022
  get_play_detail: (input, _) => handleGetPlayDetail(input),
32318
32023
  get_framework_detail: (input, _) => handleGetFrameworkDetail(input),
32319
32024
  get_counsel_detail: (input, _) => handleGetCounselDetail(input),
32320
- get_role_card: (input, _) => handleGetRoleCard(input),
32321
32025
  get_session_brief: (input, _) => handleGetSessionBrief(input),
32322
32026
  get_revenue_metrics: handleGetRevenueMetrics,
32323
32027
  get_revenue_metrics_timeseries: (input, _) => handleGetRevenueMetricsTimeseries(input),