@sonnechasser/ntrp 1.5.8 → 1.8.0

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 +2617 -111
  2. package/dist/mcp/server.js +9131 -6621
  3. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -7530,6 +7530,171 @@ var init_untrusted = __esm({
7530
7530
  }
7531
7531
  });
7532
7532
 
7533
+ // src/data/gtm-counsel/play-routing.ts
7534
+ function getPlayRouting(playId) {
7535
+ return PLAY_ROUTING[playId];
7536
+ }
7537
+ function primaryOwnerForPlay(playId) {
7538
+ return getPlayRouting(playId)?.primary_owner ?? null;
7539
+ }
7540
+ function counselIdsForPlay(playId) {
7541
+ const ids = getPlayRouting(playId)?.counsel_ids;
7542
+ return ids ? [...ids] : [];
7543
+ }
7544
+ function graphForPlay(playId) {
7545
+ return getPlayRouting(playId)?.graph;
7546
+ }
7547
+ function playRoutingIntegrityIssues(validPlayIds3) {
7548
+ const issues = [];
7549
+ for (const [playId, routing] of Object.entries(PLAY_ROUTING)) {
7550
+ if (validPlayIds3 && !validPlayIds3.has(playId)) {
7551
+ issues.push(`${playId}: routing has no built-in play`);
7552
+ }
7553
+ if (routing.counsel_ids[0] !== routing.primary_owner) {
7554
+ issues.push(`${playId}: primary_owner must be counsel_ids[0]`);
7555
+ }
7556
+ if (new Set(routing.counsel_ids).size !== routing.counsel_ids.length) {
7557
+ issues.push(`${playId}: duplicate counsel_ids`);
7558
+ }
7559
+ }
7560
+ return issues;
7561
+ }
7562
+ var PLAY_ROUTING, PLAY_COUNSEL_HOOKS;
7563
+ var init_play_routing = __esm({
7564
+ "src/data/gtm-counsel/play-routing.ts"() {
7565
+ "use strict";
7566
+ PLAY_ROUTING = {
7567
+ "multi-thread-deals": {
7568
+ primary_owner: "counsel_sales",
7569
+ counsel_ids: ["counsel_sales", "counsel_exec"],
7570
+ constraint_signals: ["thread_depth"]
7571
+ },
7572
+ "clean-dead-pipeline": {
7573
+ primary_owner: "counsel_sales",
7574
+ counsel_ids: ["counsel_sales", "counsel_revops"],
7575
+ constraint_signals: ["freshness"]
7576
+ },
7577
+ "fix-handoff-gap": {
7578
+ primary_owner: "counsel_revops",
7579
+ counsel_ids: ["counsel_revops", "counsel_marketing", "counsel_sales"],
7580
+ constraint_signals: ["drop_rate"],
7581
+ graph: {
7582
+ exclusive_group: "handoff_repair",
7583
+ next_if: [
7584
+ {
7585
+ play_id: "harden-routing-sla",
7586
+ when: "The source-to-owner path is known but SLA timers, queues, or acceptance monitoring remain broken."
7587
+ }
7588
+ ]
7589
+ }
7590
+ },
7591
+ "retarget-effort": {
7592
+ primary_owner: "counsel_sales",
7593
+ counsel_ids: ["counsel_sales", "counsel_marketing", "counsel_revops"],
7594
+ constraint_signals: ["signal_to_noise"]
7595
+ },
7596
+ "unstick-pipeline": {
7597
+ primary_owner: "counsel_sales",
7598
+ counsel_ids: ["counsel_sales", "counsel_exec"],
7599
+ constraint_signals: ["flow_rate"]
7600
+ },
7601
+ "reduce-logo-churn": {
7602
+ primary_owner: "counsel_cs",
7603
+ counsel_ids: ["counsel_cs", "counsel_exec"],
7604
+ constraint_signals: ["grr"]
7605
+ },
7606
+ "accelerate-expansion": {
7607
+ primary_owner: "counsel_cs",
7608
+ counsel_ids: ["counsel_cs", "counsel_sales"],
7609
+ constraint_signals: ["nrr"]
7610
+ },
7611
+ "fix-renewal-process": {
7612
+ primary_owner: "counsel_cs",
7613
+ counsel_ids: ["counsel_cs", "counsel_revops"],
7614
+ constraint_signals: ["contraction_arr"],
7615
+ graph: {
7616
+ next_if: [
7617
+ {
7618
+ play_id: "renewal-early-warning",
7619
+ when: "Leading risk indicators are absent, unmonitored, or first appear inside 30 days."
7620
+ }
7621
+ ]
7622
+ }
7623
+ },
7624
+ "rebalance-pipeline-mix": {
7625
+ primary_owner: "counsel_marketing",
7626
+ counsel_ids: ["counsel_marketing", "counsel_sales", "counsel_exec"],
7627
+ constraint_signals: ["pipeline_coverage"]
7628
+ },
7629
+ "compress-sales-cycle": {
7630
+ primary_owner: "counsel_sales",
7631
+ counsel_ids: ["counsel_sales", "counsel_marketing"],
7632
+ constraint_signals: ["avg_sales_cycle", "flow_rate"]
7633
+ },
7634
+ "improve-magic-number": {
7635
+ primary_owner: "counsel_marketing",
7636
+ counsel_ids: ["counsel_marketing", "counsel_exec", "counsel_revops"],
7637
+ constraint_signals: ["magic_number"]
7638
+ },
7639
+ "harden-routing-sla": {
7640
+ primary_owner: "counsel_revops",
7641
+ counsel_ids: ["counsel_revops", "counsel_marketing", "counsel_sales"],
7642
+ constraint_signals: ["drop_rate"],
7643
+ graph: {
7644
+ prerequisites: [
7645
+ {
7646
+ play_id: "fix-handoff-gap",
7647
+ reason: "Map the source-to-owner path before installing SLA timers and observability."
7648
+ }
7649
+ ],
7650
+ exclusive_group: "handoff_repair"
7651
+ }
7652
+ },
7653
+ "demand-quality-over-volume": {
7654
+ primary_owner: "counsel_marketing",
7655
+ counsel_ids: ["counsel_marketing", "counsel_revops"],
7656
+ constraint_signals: ["mql_to_opp", "cost_per_pipeline", "magic_number"],
7657
+ graph: {
7658
+ blocked_by: [
7659
+ {
7660
+ signal: "drop_rate",
7661
+ status: "red",
7662
+ reason: "Do not increase demand spend until the routing path and acceptance SLA are trustworthy."
7663
+ }
7664
+ ]
7665
+ }
7666
+ },
7667
+ "sales-cs-handoff-packet": {
7668
+ primary_owner: "counsel_cs",
7669
+ counsel_ids: ["counsel_cs", "counsel_sales", "counsel_revops"],
7670
+ constraint_signals: ["grr", "packet_field_completeness"]
7671
+ },
7672
+ "renewal-early-warning": {
7673
+ primary_owner: "counsel_cs",
7674
+ counsel_ids: ["counsel_cs", "counsel_revops"],
7675
+ constraint_signals: ["grr", "early_warning_lead_time"]
7676
+ },
7677
+ "abm-orchestration": {
7678
+ primary_owner: "counsel_marketing",
7679
+ counsel_ids: ["counsel_marketing", "counsel_sales"],
7680
+ constraint_signals: ["pipeline_coverage", "named_list_pipeline", "named_list_win_rate"],
7681
+ graph: {
7682
+ tree_exclude: ["smb_velocity"],
7683
+ required_evidence: ["named_account_process"]
7684
+ }
7685
+ },
7686
+ "forecast-ritual-hygiene": {
7687
+ primary_owner: "counsel_revops",
7688
+ counsel_ids: ["counsel_revops", "counsel_sales", "counsel_exec"],
7689
+ constraint_signals: ["freshness", "flow_rate", "weighted_pipeline", "forecast_commit_history"]
7690
+ }
7691
+ };
7692
+ PLAY_COUNSEL_HOOKS = Object.fromEntries(
7693
+ Object.entries(PLAY_ROUTING).map(([id, routing]) => [id, [...routing.counsel_ids]])
7694
+ );
7695
+ }
7696
+ });
7697
+
7533
7698
  // src/data/playbook.ts
7534
7699
  var playbook_exports = {};
7535
7700
  __export(playbook_exports, {
@@ -7583,6 +7748,8 @@ function addCustomPlay(input) {
7583
7748
  steps: input.steps,
7584
7749
  tools_that_help: input.tools_that_help ?? [],
7585
7750
  expected_outcome: input.expected_outcome ?? "Improvement in the targeted vital sign",
7751
+ exam: input.exam,
7752
+ routing: input.routing,
7586
7753
  source: "learned"
7587
7754
  };
7588
7755
  try {
@@ -7620,9 +7787,16 @@ function withKnownRecommendedPlays(finding) {
7620
7787
  if (next === finding.recommended_plays) return finding;
7621
7788
  return { ...finding, recommended_plays: next };
7622
7789
  }
7623
- function matchTriggeredPlays(vitals, layers) {
7790
+ function matchTriggeredPlays(vitals, layers, options = {}) {
7624
7791
  const bySign = new Map(vitals.map((v) => [v.vital_sign, v]));
7625
7792
  const out = [];
7793
+ const selectedIds = /* @__PURE__ */ new Set();
7794
+ const selectedGroups = /* @__PURE__ */ new Set();
7795
+ const evidence = new Set(options.evidence ?? []);
7796
+ const statuses = {
7797
+ ...Object.fromEntries(vitals.map((v) => [v.vital_sign, v.status])),
7798
+ ...options.signalStatuses ?? {}
7799
+ };
7626
7800
  for (const layer of layers) {
7627
7801
  for (const sign of layer.signs) {
7628
7802
  const vital = bySign.get(sign);
@@ -7631,7 +7805,26 @@ function matchTriggeredPlays(vitals, layers) {
7631
7805
  if (!fires) continue;
7632
7806
  for (const play of getAllPlays()) {
7633
7807
  if (play.trigger_vital_sign === sign) {
7808
+ const graph = (getPlayRouting(play.id) ?? play.routing)?.graph;
7809
+ if (options.tree && graph?.tree_exclude?.includes(options.tree) && !(graph.required_evidence ?? []).every((item) => evidence.has(item))) {
7810
+ continue;
7811
+ }
7812
+ if (graph?.blocked_by?.some(
7813
+ (block) => statuses[String(block.signal)] === block.status
7814
+ )) {
7815
+ continue;
7816
+ }
7817
+ if (graph?.exclusive_group && selectedGroups.has(graph.exclusive_group)) {
7818
+ continue;
7819
+ }
7820
+ if (graph?.prerequisites?.some(
7821
+ (requirement) => !selectedIds.has(requirement.play_id)
7822
+ )) {
7823
+ continue;
7824
+ }
7634
7825
  out.push({ play, vital, layer: layer.layer });
7826
+ selectedIds.add(play.id);
7827
+ if (graph?.exclusive_group) selectedGroups.add(graph.exclusive_group);
7635
7828
  }
7636
7829
  }
7637
7830
  }
@@ -7643,6 +7836,7 @@ var init_playbook = __esm({
7643
7836
  "src/data/playbook.ts"() {
7644
7837
  "use strict";
7645
7838
  init_store();
7839
+ init_play_routing();
7646
7840
  PLAYBOOK = [
7647
7841
  {
7648
7842
  id: "multi-thread-deals",
@@ -7658,7 +7852,13 @@ var init_playbook = __esm({
7658
7852
  "Set an alert when a mid-stage or later deal has one active contact."
7659
7853
  ],
7660
7854
  tools_that_help: ["Buying-committee enrichment (waterfall)", "Job-change signal tracking", "CRM contact roles", "Single-thread alerts"],
7661
- expected_outcome: "Thread Depth score rises above the threshold. Deals with one contact drop by 50% or more in 2 weeks. No late-stage deal has one thread."
7855
+ expected_outcome: "Thread Depth reaches 2+ on late-stage deals; single-threaded deal count drops 50% or more within 2 weeks (instrument: thread_depth).",
7856
+ exam: {
7857
+ instrument_ids: ["thread_depth"],
7858
+ target_guidance: "Thread depth \u22652 on late-stage deals; single-threaded count \u221250% or more.",
7859
+ check_window_days: 14,
7860
+ measurement_mode: "native"
7861
+ }
7662
7862
  },
7663
7863
  {
7664
7864
  id: "clean-dead-pipeline",
@@ -7674,7 +7874,13 @@ var init_playbook = __esm({
7674
7874
  "Set a stale-deal alert at N quiet days. Calibrate N to this motion's cycle."
7675
7875
  ],
7676
7876
  tools_that_help: ["CRM bulk update", "Signal-based reactivation triggers", "Enrichment refresh (waterfall)", "Pipeline hygiene cadence"],
7677
- expected_outcome: "Freshness score rises 20 points or more. The forecast matches reality. The reactivation track produces meetings at a fraction of cold-acquisition cost."
7877
+ expected_outcome: "Freshness rises 20 points or more and stale pipeline dollars fall 40\u201360% within 30 days (instrument: freshness).",
7878
+ exam: {
7879
+ instrument_ids: ["freshness"],
7880
+ target_guidance: "Freshness +20 points; stale pipeline dollars \u221240\u201360%.",
7881
+ check_window_days: 30,
7882
+ measurement_mode: "native"
7883
+ }
7678
7884
  },
7679
7885
  {
7680
7886
  id: "fix-handoff-gap",
@@ -7685,12 +7891,19 @@ var init_playbook = __esm({
7685
7891
  steps: [
7686
7892
  "Audit the leak by source. Find which lead sources never reach the CRM or a rep queue.",
7687
7893
  "Trace the routing path: assignment rules, territory, inactive-rep queues, and the marketing to CRM sync.",
7688
- "Repair routing gaps. Reassign orphaned queues. Dedupe and enrich records so routing has the fields it needs.",
7689
- "Set the SLA for time-to-first-touch on handed-off leads. Assign an owner to the report.",
7690
- "Set a weekly report for marketing-only leads. Set an alert when any source handoff rate drops."
7894
+ "Name the failure class per source: never arrived, late, wrong owner, or unused in queue.",
7895
+ "Repair the broken path: reassign orphaned queues, dedupe/enrich routing fields, fix the sync gap.",
7896
+ "Set a weekly source\u2192owner path report. Alert when any source handoff rate drops.",
7897
+ "If timers, unassigned age, or observability are the leak, switch to Harden Routing & Acceptance SLA (harden-routing-sla). Do not treat this play as the SLA install."
7691
7898
  ],
7692
- tools_that_help: ["Lead routing audit", "Enrichment waterfall (routing fields)", "SLA dashboard", "Handoff-degradation alerts"],
7693
- expected_outcome: "Drop Rate improves 15 points or more. Marketing-only lead count drops by 60% or more. Time-to-first-touch is inside the SLA."
7899
+ tools_that_help: ["Lead routing audit", "Enrichment waterfall (routing fields)", "Source\u2192owner path report", "Handoff-degradation alerts"],
7900
+ expected_outcome: "Drop Rate +15 or more (instrument: drop_rate). Marketing-only lead share \u221260% or more (instrument: source\u2192owner path count). Time-to-first-touch is not this play's exam \u2014 that lives on harden-routing-sla.",
7901
+ exam: {
7902
+ instrument_ids: ["drop_rate"],
7903
+ target_guidance: "Drop Rate +15 points; marketing-only lead share \u221260% or more.",
7904
+ check_window_days: 30,
7905
+ measurement_mode: "native"
7906
+ }
7694
7907
  },
7695
7908
  {
7696
7909
  id: "retarget-effort",
@@ -7706,7 +7919,13 @@ var init_playbook = __esm({
7706
7919
  "Automate or delete noise work such as logging, list building, and manual research."
7707
7920
  ],
7708
7921
  tools_that_help: ["Activity reports by rep", "ICP/propensity scoring", "Signal routing to rep channels", "Enrichment automation"],
7709
- expected_outcome: "Signal-to-Noise improves to 70% or more. Rep hours move from dead accounts to scored pipeline."
7922
+ expected_outcome: "Signal-to-Noise reaches 70% or more and misdirected effort dollars fall 30\u201350% within 30 days (instrument: signal_to_noise).",
7923
+ exam: {
7924
+ instrument_ids: ["signal_to_noise"],
7925
+ target_guidance: "Signal-to-Noise \u226570%; misdirected effort dollars \u221230\u201350%.",
7926
+ check_window_days: 30,
7927
+ measurement_mode: "native"
7928
+ }
7710
7929
  },
7711
7930
  {
7712
7931
  id: "unstick-pipeline",
@@ -7722,7 +7941,13 @@ var init_playbook = __esm({
7722
7941
  "Set an aging alert at the motion-calibrated threshold. Escalate past 2 times median stage duration."
7723
7942
  ],
7724
7943
  tools_that_help: ["Deal inspection reports", "Stage exit criteria", "Aging alerts", "Manager escalation workflow"],
7725
- expected_outcome: "Flow Rate score improves 15 points or more. Stuck deal count drops by 40% or more in 2 weeks. Conversion at the stall stage improves."
7944
+ expected_outcome: "Flow Rate improves 15 points or more and stuck deal count falls 40% or more within 2 weeks (instrument: flow_rate).",
7945
+ exam: {
7946
+ instrument_ids: ["flow_rate"],
7947
+ target_guidance: "Flow Rate +15 points; stuck deal count \u221240% or more.",
7948
+ check_window_days: 14,
7949
+ measurement_mode: "native"
7950
+ }
7726
7951
  },
7727
7952
  {
7728
7953
  id: "reduce-logo-churn",
@@ -7739,7 +7964,13 @@ var init_playbook = __esm({
7739
7964
  "Set early-warning triggers 90 days before renewal."
7740
7965
  ],
7741
7966
  tools_that_help: ["CS platform", "Renewal calendar", "NPS/CSAT surveys"],
7742
- expected_outcome: "GRR moves toward the motion benchmark within 2 quarters."
7967
+ expected_outcome: "GRR closes 25\u201350% of the gap to the motion benchmark within 2 quarters (instrument: grr).",
7968
+ exam: {
7969
+ instrument_ids: ["grr"],
7970
+ target_guidance: "Close 25\u201350% of the GRR benchmark gap.",
7971
+ check_window_days: 180,
7972
+ measurement_mode: "native"
7973
+ }
7743
7974
  },
7744
7975
  {
7745
7976
  id: "accelerate-expansion",
@@ -7756,7 +7987,13 @@ var init_playbook = __esm({
7756
7987
  "Track expansion pipeline apart from new business."
7757
7988
  ],
7758
7989
  tools_that_help: ["Account plans", "Usage analytics", "Expansion playbooks"],
7759
- expected_outcome: "Expansion ARR grows 20% or more quarter over quarter."
7990
+ expected_outcome: "Expansion ARR grows 15\u201325% quarter over quarter without GRR decline (instrument: nrr).",
7991
+ exam: {
7992
+ instrument_ids: ["nrr"],
7993
+ target_guidance: "Expansion ARR +15\u201325% quarter over quarter; GRR does not decline.",
7994
+ check_window_days: 90,
7995
+ measurement_mode: "native"
7996
+ }
7760
7997
  },
7761
7998
  {
7762
7999
  id: "fix-renewal-process",
@@ -7768,12 +8005,20 @@ var init_playbook = __esm({
7768
8005
  steps: [
7769
8006
  "List all contraction events. Categorize the root cause.",
7770
8007
  "Set a standard renewal timeline: 120, 90, 60, and 30-day checkpoints.",
8008
+ "Define leading risk indicators (champion change, usage drop, open severity tickets) and flag them 60\u201390 days before renewal.",
7771
8009
  "Engage the economic buyer before the renewal date.",
7772
8010
  "Make an ROI recap deck template for every renewal.",
7773
- "Escalate contractions above 20% to leadership review."
8011
+ "Escalate contractions above 20% to leadership review.",
8012
+ "Pair with Renewal Early Warning (renewal-early-warning) when leading indicators are missing or unmonitored."
7774
8013
  ],
7775
8014
  tools_that_help: ["Renewal workflow", "QBR templates", "Value realization reports"],
7776
- expected_outcome: "Contraction ARR drops 50% or more within 2 quarters."
8015
+ expected_outcome: "Contraction ARR drops 30\u201350% within 2 quarters (instrument: contraction_arr).",
8016
+ exam: {
8017
+ instrument_ids: ["contraction_arr"],
8018
+ target_guidance: "Contraction ARR \u221230\u201350%.",
8019
+ check_window_days: 180,
8020
+ measurement_mode: "native"
8021
+ }
7777
8022
  },
7778
8023
  {
7779
8024
  id: "rebalance-pipeline-mix",
@@ -7790,7 +8035,13 @@ var init_playbook = __esm({
7790
8035
  "Review discounting and stage inflation that hide a thin pipeline."
7791
8036
  ],
7792
8037
  tools_that_help: ["Pipeline analytics", "Marketing attribution", "Capacity planning"],
7793
- expected_outcome: "Pipeline coverage reaches the motion benchmark within 90 days."
8038
+ expected_outcome: "Pipeline coverage closes 25\u201350% of the benchmark gap within 90 days (instrument: pipeline_coverage).",
8039
+ exam: {
8040
+ instrument_ids: ["pipeline_coverage"],
8041
+ target_guidance: "Close 25\u201350% of the pipeline-coverage benchmark gap.",
8042
+ check_window_days: 90,
8043
+ measurement_mode: "native"
8044
+ }
7794
8045
  },
7795
8046
  {
7796
8047
  id: "compress-sales-cycle",
@@ -7807,7 +8058,13 @@ var init_playbook = __esm({
7807
8058
  "Remove low-probability aged deals to free rep capacity."
7808
8059
  ],
7809
8060
  tools_that_help: ["Stage duration reports", "MAP templates", "Deal coaching"],
7810
- expected_outcome: "Median cycle time drops 20% or more within one quarter."
8061
+ expected_outcome: "Median sales cycle drops 15\u201325% within one quarter (instrument: avg_sales_cycle).",
8062
+ exam: {
8063
+ instrument_ids: ["avg_sales_cycle"],
8064
+ target_guidance: "Median sales cycle \u221215\u201325%.",
8065
+ check_window_days: 90,
8066
+ measurement_mode: "native"
8067
+ }
7811
8068
  },
7812
8069
  {
7813
8070
  id: "improve-magic-number",
@@ -7824,7 +8081,150 @@ var init_playbook = __esm({
7824
8081
  "Review rep ramp time and quota attainment curves."
7825
8082
  ],
7826
8083
  tools_that_help: ["Finance model", "Channel ROI dashboard", "CAC by source"],
7827
- expected_outcome: "Magic number moves toward the benchmark within 2 quarters."
8084
+ expected_outcome: "Magic number closes 25\u201350% of the motion-benchmark gap within 2 quarters (instrument: magic_number).",
8085
+ exam: {
8086
+ instrument_ids: ["magic_number"],
8087
+ target_guidance: "Close 25\u201350% of the magic-number benchmark gap.",
8088
+ check_window_days: 180,
8089
+ measurement_mode: "native"
8090
+ }
8091
+ },
8092
+ {
8093
+ id: "harden-routing-sla",
8094
+ name: "Harden Routing & Acceptance SLA",
8095
+ trigger_vital_sign: "drop_rate",
8096
+ trigger_condition: "Handoff path exists but SLA timers, unassigned queues, or acceptance monitoring are missing or breached. Use fix-handoff-gap first when the source\u2192owner path itself is unknown.",
8097
+ why: "Handoff leaks that survive a path audit are usually SLA and observability failures: unassigned queues, dead routers, silent sync errors, and no timer on first touch. More demand spend cannot fix a broken pipe.",
8098
+ steps: [
8099
+ "Map lead\u2192owner path with timers: create, sync, assign, first touch, accept/reject.",
8100
+ "Instrument unassigned age and SLA breach alerts with a named RevOps owner.",
8101
+ "Fix routing rules and inactive-rep queues before launching new campaigns.",
8102
+ "Publish acceptance reasons AEs must use; review weekly with Demand and SDR leads.",
8103
+ "Prove the fix held for 30 days with monitoring \u2014 do not declare victory after a one-week cleanup."
8104
+ ],
8105
+ tools_that_help: ["CRM assignment logs", "SLA dashboards", "Sync error queues", "Enrichment for routing fields"],
8106
+ expected_outcome: "Median unassigned age inside the published SLA within 30 days (instrument: assignment logs). Drop Rate +10 or more after a 30-day hold (instrument: drop_rate). \u226580% of rejects carry an acceptance reason.",
8107
+ exam: {
8108
+ instrument_ids: ["drop_rate"],
8109
+ target_guidance: "Drop Rate +10 points; median unassigned age inside SLA; rejection reasons \u226580%.",
8110
+ check_window_days: 30,
8111
+ measurement_mode: "native"
8112
+ }
8113
+ },
8114
+ {
8115
+ id: "demand-quality-over-volume",
8116
+ name: "Demand Quality over Volume",
8117
+ trigger_metric: "magic_number",
8118
+ trigger_lens: "revenue_metrics",
8119
+ trigger_condition: "MQL\u2192SQL or SQL\u2192Opp conversion is weak, or accepted-pipeline $ lags MQL volume. Not for routing/SLA leaks \u2014 use fix-handoff-gap / harden-routing-sla when drop_rate is the gating vital.",
8120
+ why: "Optimizing for MQL count burns budget and trust. Dollars come from accepted pipeline that closes \u2014 not form fills Sales will not work.",
8121
+ steps: [
8122
+ "Kill rule: if drop_rate is red, do not increase media spend until the handoff path and acceptance SLA are trustworthy.",
8123
+ "Cut or pause channels whose pipeline does not close; keep channels with proven opp\u2192won.",
8124
+ "Tighten scoring and ICP gates with RevOps; publish disqual rules to SDR/AE.",
8125
+ "Set exams on MQL\u2192SQL\u2192Opp $ and cost per pipeline $, not MQL count.",
8126
+ "Align Demand, SDR, and AE on one definition of accepted handoff."
8127
+ ],
8128
+ tools_that_help: ["Attribution with agreed rules", "Scoring models", "Channel ROI", "Rejection reason reports"],
8129
+ expected_outcome: "MQL\u2192Opp conversion +20% or more, or cost per accepted pipeline $ \u221220% or more, within one quarter (instruments: mql_to_opp, cost_per_pipeline). MQL count is not the exam.",
8130
+ exam: {
8131
+ instrument_ids: ["mql_to_opp", "cost_per_pipeline"],
8132
+ target_guidance: "MQL\u2192Opp +20% or cost per accepted pipeline dollar \u221220%.",
8133
+ check_window_days: 90,
8134
+ measurement_mode: "external"
8135
+ }
8136
+ },
8137
+ {
8138
+ id: "sales-cs-handoff-packet",
8139
+ name: "Sales\u2192CS Handoff Packet",
8140
+ trigger_lens: "revenue_metrics",
8141
+ trigger_metric: "grr",
8142
+ trigger_condition: "Churn or rocky onboarding traces to incomplete sales handoffs, oversell, or missing implementation readiness",
8143
+ why: "Won deals that arrive without scope, success criteria, champion map, or implementation readiness become churn and contraction. The leak is after the booking, not before.",
8144
+ steps: [
8145
+ "Define a minimum handoff packet: ICP fit, sold scope, success criteria, champion/EB contacts, known risks, close notes.",
8146
+ "Block or flag Closed-Won without packet fields (observe + recommend field capture if instruments are thin).",
8147
+ "Involve CS early on complex or high-ACV deals before signature.",
8148
+ "Audit recent churn/contraction for missing handoff evidence; feed findings to Sales managers.",
8149
+ "Review packet completion weekly in Sales + CS ops until compliance holds."
8150
+ ],
8151
+ tools_that_help: ["CRM required fields", "CS handoff checklist", "Onboarding readiness score"],
8152
+ expected_outcome: "Handoff packet completion \u226590% of Closed-Won within 30 days (instrument: packet field completeness). Early-tenure logo churn attributed to oversell/missing context \u221230% or more within two quarters (instrument: grr / tenure-cut churn). Observe + recommend field capture if packet fields do not exist yet.",
8153
+ exam: {
8154
+ instrument_ids: ["packet_field_completeness", "grr"],
8155
+ target_guidance: "Packet completion \u226590%; early-tenure handoff-attributed churn \u221230% or more.",
8156
+ check_window_days: 180,
8157
+ measurement_mode: "capture_required"
8158
+ }
8159
+ },
8160
+ {
8161
+ id: "renewal-early-warning",
8162
+ name: "Renewal Early Warning",
8163
+ trigger_metric: "grr",
8164
+ trigger_lens: "revenue_metrics",
8165
+ trigger_condition: "Renewals scramble late, GRR soft, or risk flags appear only inside 30 days",
8166
+ why: "Last-week renewal heroics are a process failure. Leading indicators 60\u201390 days out beat discount addiction at the deadline.",
8167
+ steps: [
8168
+ "Define 3\u20135 leading risk indicators with owners (usage drop, champion change, open P1s, unpaid invoices).",
8169
+ "Surface risk on the renewal book at 90 and 60 days \u2014 not only at 30.",
8170
+ "Tie each red flag to a save play with a dated exam.",
8171
+ "Align Renewals, CSM, and RevOps on one system of record for renewal dates and risk.",
8172
+ "Review early-warning hit rate after each cohort; calibrate indicators that do not predict churn."
8173
+ ],
8174
+ tools_that_help: ["Renewal calendar", "Health scores validated to churn", "CS risk workflow"],
8175
+ expected_outcome: "Share of the renewal book with a risk flag \u226560 days out reaches \u226580% (instrument: early_warning_lead_time). Emergency discounts on unflagged renewals \u221240% or more within two cohorts (instrument: contraction_arr / discount on late saves).",
8176
+ exam: {
8177
+ instrument_ids: ["early_warning_lead_time", "contraction_arr"],
8178
+ target_guidance: "Risk flags \u226560 days out on \u226580% of the book; emergency discounts \u221240%.",
8179
+ check_window_days: 120,
8180
+ measurement_mode: "capture_required"
8181
+ }
8182
+ },
8183
+ {
8184
+ id: "abm-orchestration",
8185
+ name: "ABM Orchestration on Named Accounts",
8186
+ trigger_lens: "revenue_metrics",
8187
+ trigger_metric: "pipeline_coverage",
8188
+ trigger_condition: "Enterprise or named-account motion with weak on-list pipeline. Not a default play for smb_velocity.",
8189
+ why: "ABM fails when it is spray with logos. It works when Marketing, SDR, and AE share one named list, plays, and account-level exams.",
8190
+ steps: [
8191
+ "Kill rule: do not run this play on smb_velocity without an agreed named-account list and AE commitment.",
8192
+ "Agree the named list and tiers with Sales; align to territories.",
8193
+ "Orchestrate plays across marketing + SDR + AE with clear owners per account.",
8194
+ "Measure pipeline and wins on the named list \u2014 not vanity engagement alone.",
8195
+ "Exit silent accounts on a schedule; do not fund forever."
8196
+ ],
8197
+ tools_that_help: ["ABM platform", "Account plans", "Intent + engagement with RevOps definitions"],
8198
+ expected_outcome: "On-list pipeline $ +25% or more and on-list win rate +5 points or more within two quarters (instruments: named_list_pipeline, named_list_win_rate). Off-list activity share does not rise (instrument: signal_to_noise on named vs rest).",
8199
+ exam: {
8200
+ instrument_ids: ["named_list_pipeline", "named_list_win_rate", "signal_to_noise"],
8201
+ target_guidance: "On-list pipeline +25% or more; on-list win rate +5 points; no off-list noise increase.",
8202
+ check_window_days: 180,
8203
+ measurement_mode: "external"
8204
+ }
8205
+ },
8206
+ {
8207
+ id: "forecast-ritual-hygiene",
8208
+ name: "Restore Forecast Ritual Hygiene",
8209
+ trigger_metric: "weighted_pipeline",
8210
+ trigger_lens: "revenue_metrics",
8211
+ trigger_condition: "Forecast credibility is weak: stale or stuck pipeline, past-due closes, or commit changes are not captured",
8212
+ why: "A forecast ritual cannot repair dirty pipeline, but clean instruments without a commit cadence still produce surprise. Separate input trust from judgment drift, then inspect both.",
8213
+ steps: [
8214
+ "Confirm Freshness and Flow Rate are usable before changing forecast cadence.",
8215
+ "Cut past-due close dollars and stage-age exceptions by manager and segment.",
8216
+ "Snapshot best case / commit weekly with written evidence for material changes.",
8217
+ "Compare commit movement with weighted pipeline and actual closes; observe + recommend commit-history capture when it does not exist.",
8218
+ "Hold a 30-day ritual exam before changing methodology, tooling, or headcount."
8219
+ ],
8220
+ tools_that_help: ["Weighted pipeline report", "Past-due close audit", "Weekly commit snapshot", "Stage-age inspection"],
8221
+ expected_outcome: "Past-due close dollars fall 30\u201350% and weekly commit changes carry evidence within 30 days (instruments: freshness, flow_rate, weighted_pipeline; commit history requires capture).",
8222
+ exam: {
8223
+ instrument_ids: ["freshness", "flow_rate", "weighted_pipeline", "forecast_commit_history"],
8224
+ target_guidance: "Past-due close dollars \u221230\u201350%; 100% of material commit changes carry evidence.",
8225
+ check_window_days: 30,
8226
+ measurement_mode: "capture_required"
8227
+ }
7828
8228
  }
7829
8229
  ];
7830
8230
  PLAYS_FILE = "plays.jsonl";
@@ -10090,10 +10490,10 @@ function hr(width, ch = "\u2500") {
10090
10490
  return ch.repeat(Math.max(0, width));
10091
10491
  }
10092
10492
  function wrapWords(text, maxW) {
10093
- const words = text.split(/\s+/).filter(Boolean);
10493
+ const words2 = text.split(/\s+/).filter(Boolean);
10094
10494
  const lines = [];
10095
10495
  let cur = "";
10096
- for (let word of words) {
10496
+ for (let word of words2) {
10097
10497
  if (visibleWidth(word) > maxW) {
10098
10498
  if (cur) {
10099
10499
  lines.push(cur);
@@ -15883,6 +16283,8 @@ function loadRuminationJob(id) {
15883
16283
  if (!parsed || typeof parsed !== "object" || parsed.id !== id) return null;
15884
16284
  if (parsed.best_plan === void 0) parsed.best_plan = parsed.plan ?? null;
15885
16285
  if (parsed.best_score === void 0) parsed.best_score = parsed.last_critic?.score ?? null;
16286
+ if (parsed.roundtable === void 0) parsed.roundtable = null;
16287
+ if (parsed.roundtable_history === void 0) parsed.roundtable_history = [];
15886
16288
  return parsed;
15887
16289
  } catch {
15888
16290
  return null;
@@ -15914,7 +16316,9 @@ function createRuminationJob(opts) {
15914
16316
  created_at: now2,
15915
16317
  updated_at: now2,
15916
16318
  no_improve_streak: 0,
15917
- last_critic_score: null
16319
+ last_critic_score: null,
16320
+ roundtable: null,
16321
+ roundtable_history: []
15918
16322
  };
15919
16323
  }
15920
16324
  function renderRuminationLog(job) {
@@ -15928,6 +16332,18 @@ function renderRuminationLog(job) {
15928
16332
  );
15929
16333
  if (job.library_path) lines.push(`Strategy: ${job.library_path}`);
15930
16334
  if (job.handoff_path) lines.push(`Handoff: ${job.handoff_path}`);
16335
+ if (job.roundtable) {
16336
+ lines.push("");
16337
+ lines.push("## Counsel roundtable");
16338
+ lines.push("");
16339
+ lines.push(job.roundtable.digest);
16340
+ if (job.roundtable.issues.length) {
16341
+ lines.push("");
16342
+ lines.push(
16343
+ `Validation: ${job.roundtable.issues.map((entry) => entry.code).join(", ")}`
16344
+ );
16345
+ }
16346
+ }
15931
16347
  lines.push("");
15932
16348
  lines.push("## Rounds");
15933
16349
  lines.push("");
@@ -16779,6 +17195,1256 @@ var init_companion = __esm({
16779
17195
  }
16780
17196
  });
16781
17197
 
17198
+ // src/data/gtm-counsel/packs.ts
17199
+ function listCounselPackIds() {
17200
+ return COUNSEL_PACKS.map((p) => p.id);
17201
+ }
17202
+ function getCounselPackById(id) {
17203
+ return COUNSEL_PACKS.find((p) => p.id === id);
17204
+ }
17205
+ var SILENT, COUNSEL_PACKS;
17206
+ var init_packs = __esm({
17207
+ "src/data/gtm-counsel/packs.ts"() {
17208
+ "use strict";
17209
+ SILENT = "Never name methodology brands in customer-visible text. Apply the moves; keep labels silent.";
17210
+ COUNSEL_PACKS = [
17211
+ {
17212
+ id: "counsel_sales",
17213
+ catalog_line: "- counsel_sales \u2014 pipeline conversion, capacity, deal strategy, sales-team structure",
17214
+ internal_name: "Sales counsel",
17215
+ vital_links: ["flow_rate", "thread_depth", "freshness", "signal_to_noise"],
17216
+ board_cut: "Sales constraint in dollars (stuck, single-threaded, stale, misdirected effort) and the one sales-process bet.",
17217
+ operator_cut: "Manager inspection cadence, stage exit criteria, SDR acceptance SLA, multi-thread targets \u2014 function owners.",
17218
+ when_to_use: "flow_rate / thread_depth / opp freshness / activity noise; attainment, cycle, coverage, forecast-from-the-line.",
17219
+ kill_rules: [
17220
+ "No headcount before capacity math + constraint vital.",
17221
+ "No more activity when signal_to_noise is red.",
17222
+ "Do not protect zombie pipeline for coverage optics.",
17223
+ "No invented reorgs or named people \u2014 function owners only.",
17224
+ "Do not kill zombies first unless freshness/zombies are the gating constraint."
17225
+ ],
17226
+ silent_brand_rule: SILENT,
17227
+ role_card_ids: ["vp_sales", "sales_manager", "ae", "sdr", "se", "sales_ops"],
17228
+ tree_weight: { plg: 0.5, smb_velocity: 1, mid_market: 1, enterprise: 1 },
17229
+ 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.
17230
+ 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?
17231
+ CUT: stage-age, owner, segment, amount band, activity-to-opp link.
17232
+ 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.
17233
+ 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.
17234
+ 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.
17235
+ PLAYS: get_play_detail unstick-pipeline / multi-thread-deals / clean-dead-pipeline / retarget-effort / compress-sales-cycle only when that vital is the constraint.`
17236
+ },
17237
+ {
17238
+ id: "counsel_marketing",
17239
+ catalog_line: "- counsel_marketing \u2014 demand quality, ABM, PMM/lifecycle; content/brand/partner as support seats",
17240
+ internal_name: "Marketing counsel",
17241
+ vital_links: ["drop_rate", "signal_to_noise", "nrr"],
17242
+ board_cut: "Marketing's dollar contribution and the leak (creation vs handoff vs conversion narrative).",
17243
+ operator_cut: "SLA, scoring, journey gates, program kill rules, PMM adoption exams \u2014 function owners.",
17244
+ when_to_use: "Pipeline creation, handoff leak, magic number / CAC inputs, messaging stalls, named-account whitespace.",
17245
+ kill_rules: [
17246
+ "Do not buy more top-of-funnel when drop_rate is the constraint.",
17247
+ "Do not optimize MQL count against pipeline dollars.",
17248
+ "No ABM theater on smb_velocity without named-account process.",
17249
+ "Stop at accepted pipeline quality + narrative enablement \u2014 do not own close.",
17250
+ "If content/brand/partner is not the constraint, do not open those cards."
17251
+ ],
17252
+ silent_brand_rule: SILENT,
17253
+ role_card_ids: [
17254
+ "cmo",
17255
+ "demand_gen",
17256
+ "growth",
17257
+ "abm",
17258
+ "pmm",
17259
+ "lifecycle",
17260
+ "mkt_ops",
17261
+ "content",
17262
+ "brand",
17263
+ "field_partner"
17264
+ ],
17265
+ tree_weight: { plg: 1, smb_velocity: 0.85, mid_market: 1, enterprise: 1 },
17266
+ 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.
17267
+ 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?
17268
+ CUT: source, campaign, MQL\u2192SQL\u2192Opp, named-list vs rest, journey vs SDR sequence overlap.
17269
+ 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.
17270
+ 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.
17271
+ 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.
17272
+ 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.`
17273
+ },
17274
+ {
17275
+ id: "counsel_revops",
17276
+ catalog_line: "- counsel_revops \u2014 GTM systems, routing/SLA/hygiene; forecast/capacity/territory governance",
17277
+ internal_name: "RevOps counsel",
17278
+ vital_links: ["drop_rate", "freshness", "signal_to_noise"],
17279
+ board_cut: "Whether the revenue machine's instruments can be trusted; cost of leak from process/system breaks.",
17280
+ operator_cut: "Routing SLA, stage validation, sync observability, hygiene, GTM eng backlog \u2014 RevOps / mkt ops / GTM eng.",
17281
+ when_to_use: "Broken instruments, routing, sync, definitions, hygiene, automation debt; forecast ritual design; capacity/territory ops.",
17282
+ kill_rules: [
17283
+ "No governance theater when routing/SLA is broken.",
17284
+ "Do not automate bad process.",
17285
+ "No boil-the-ocean warehouse while drop_rate burns.",
17286
+ "Do not take sides on credit wars \u2014 impose shared definitions.",
17287
+ "Prefer system/process enforcement over headcount."
17288
+ ],
17289
+ silent_brand_rule: SILENT,
17290
+ role_card_ids: ["vp_revops", "revops_specialist", "gtm_engineer", "mkt_ops", "sales_ops"],
17291
+ tree_weight: { plg: 0.6, smb_velocity: 0.75, mid_market: 1, enterprise: 1 },
17292
+ 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.
17293
+ 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?
17294
+ CUT: source\u2192owner path, unassigned age, stage-exit compliance, sync error class, identity match, commit vs inspection evidence.
17295
+ 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.
17296
+ TREE: plg=product\u2194CRM identity. smb_velocity=lean routing/SLA. mid_market=full OS. enterprise=matrixed + GTM eng.
17297
+ 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.
17298
+ 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.`
17299
+ },
17300
+ {
17301
+ id: "counsel_exec",
17302
+ catalog_line: "- counsel_exec \u2014 merge seats into one constraint, one sequence, board-ready call",
17303
+ internal_name: "Executive counsel",
17304
+ vital_links: ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth", "nrr", "grr"],
17305
+ board_cut: "The call, the dollar, the one bet, the exam date.",
17306
+ operator_cut: "Sequenced workstreams with function owners and contingencies.",
17307
+ when_to_use: "Always in strategist roundtable before plan ship; cross-functional objectives; seat conflicts.",
17308
+ kill_rules: [
17309
+ "Do not average seats into mush \u2014 pick a constraint and sequence.",
17310
+ "No reorg/headcount as first move.",
17311
+ "No parallel initiatives that starve the bottleneck.",
17312
+ "Open with verdict + dollars, not methodology.",
17313
+ "Do not ignore CS when NRR is the dollar engine."
17314
+ ],
17315
+ silent_brand_rule: SILENT,
17316
+ role_card_ids: ["cro", "cmo", "vp_sales", "vp_revops", "vp_cs"],
17317
+ tree_weight: { plg: 1, smb_velocity: 1, mid_market: 1, enterprise: 1 },
17318
+ 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).
17319
+ ASK: What unlocks the others? What dies in 90 days if we are wrong? Which seat's ask is polish?
17320
+ CUT: layer order of vitals; $ at risk by function; in-flight work that collides.
17321
+ 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.
17322
+ TREE: plg=CS/growth weight. smb_velocity=sales machine + routing. mid_market=full GTM. enterprise=thread/ABM/SE + NRR.
17323
+ 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.
17324
+ PLAYS: do not pick plays here \u2014 sequence the constraint owner's play and demote the rest to contingency.`
17325
+ },
17326
+ {
17327
+ id: "counsel_cs",
17328
+ catalog_line: "- counsel_cs \u2014 NRR/GRR, renewals, TTV/onboarding, sales\u2192CS handoff",
17329
+ internal_name: "Customer Success counsel",
17330
+ vital_links: ["freshness", "nrr", "grr", "contraction_arr"],
17331
+ board_cut: "NRR/GRR dollars at risk and the retention/expansion bet.",
17332
+ operator_cut: "Risk flags, renewal cadence, handoff packet fields, touch model by segment \u2014 CS / renewals / onboarding.",
17333
+ when_to_use: "NRR/GRR/churn/expansion; post-sale handoff; renewals; after sales wins that create CS landmines.",
17334
+ kill_rules: [
17335
+ "Do not push expansion into unhealthy accounts.",
17336
+ "No more QBRs when onboarding/TTV is the constraint.",
17337
+ "Do not treat renewals as pure billing.",
17338
+ "Do not ignore sales handoff quality as root cause.",
17339
+ "No CS headcount before segmentation/touch-model math."
17340
+ ],
17341
+ silent_brand_rule: SILENT,
17342
+ role_card_ids: ["vp_cs", "csm", "renewals", "onboarding"],
17343
+ tree_weight: { plg: 1, smb_velocity: 0.7, mid_market: 1, enterprise: 1 },
17344
+ 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.
17345
+ 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?
17346
+ CUT: tenure, segment, product usage, renewal date aging, handoff-packet completeness, champion map.
17347
+ 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.
17348
+ 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.
17349
+ 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).
17350
+ 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.`
17351
+ }
17352
+ ];
17353
+ }
17354
+ });
17355
+
17356
+ // src/data/gtm-counsel/resolve-tree.ts
17357
+ function isOrgTreeId(v) {
17358
+ return typeof v === "string" && TREES.includes(v);
17359
+ }
17360
+ function resolveOrgTree(profile) {
17361
+ if (profile && isOrgTreeId(profile.sales_motion)) return profile.sales_motion;
17362
+ const fromConfig = getConfigValue("sales-motion");
17363
+ if (isOrgTreeId(fromConfig)) return fromConfig;
17364
+ return "mid_market";
17365
+ }
17366
+ function listOrgTreeIds() {
17367
+ return [...TREES];
17368
+ }
17369
+ var TREES;
17370
+ var init_resolve_tree = __esm({
17371
+ "src/data/gtm-counsel/resolve-tree.ts"() {
17372
+ "use strict";
17373
+ init_store();
17374
+ TREES = ["plg", "smb_velocity", "mid_market", "enterprise"];
17375
+ }
17376
+ });
17377
+
17378
+ // src/data/gtm-counsel/role-cards.ts
17379
+ function presence(plg, smb, mid, ent) {
17380
+ return { plg, smb_velocity: smb, mid_market: mid, enterprise: ent };
17381
+ }
17382
+ function listRoleCardIds() {
17383
+ return ROLE_CARDS.map((c) => c.id);
17384
+ }
17385
+ function getRoleCardById(id) {
17386
+ return ROLE_CARDS.find((c) => c.id === id);
17387
+ }
17388
+ var ROLE_CARDS;
17389
+ var init_role_cards = __esm({
17390
+ "src/data/gtm-counsel/role-cards.ts"() {
17391
+ "use strict";
17392
+ ROLE_CARDS = [
17393
+ {
17394
+ id: "cro",
17395
+ title: "Chief Revenue Officer",
17396
+ function: "exec",
17397
+ presence: presence("thin", "present", "core", "core"),
17398
+ owns: [
17399
+ "Revenue-system investment sequencing across Sales/CS/RevOps",
17400
+ "Forecast integrity to CEO/board",
17401
+ "Capacity vs quota realism"
17402
+ ],
17403
+ does_not_own: ["Brand craft", "CRM admin", "Product roadmap", "Counsel-exec merge rules (use the pack)"],
17404
+ success_metrics: ["bookings", "nrr", "grr", "pipeline_coverage", "forecast_accuracy"],
17405
+ typical_decisions: ["Which constraint to fund", "Volume vs conversion vs retention this quarter"],
17406
+ data_they_trust: ["Vital dollars", "Cohort retention", "Pipeline by source/segment"],
17407
+ consult_questions: [
17408
+ "Which vital/stage $ is gating, from the health snapshot?",
17409
+ "If one function is funded, which unlocks the others?",
17410
+ "Where does commit diverge from stage evidence?"
17411
+ ],
17412
+ failure_modes: ["Sales-only reflex", "Parallel bets starving the constraint"],
17413
+ tensions: ["cmo credit", "vp_sales sandbag", "vp_cs new-logo vs NRR", "vp_revops platform spend"],
17414
+ play_hooks: ["rebalance-pipeline-mix", "improve-magic-number", "reduce-logo-churn", "accelerate-expansion"],
17415
+ signal_links: ["nrr", "grr", "pipeline_coverage"],
17416
+ stress_notes: "Do not clone counsel_exec merge rules into this card."
17417
+ },
17418
+ {
17419
+ id: "cmo",
17420
+ title: "Chief Marketing Officer",
17421
+ function: "exec",
17422
+ presence: presence("present", "thin", "core", "core"),
17423
+ owns: ["Demand-system design", "Marketing contribution to accepted pipeline", "Program portfolio mix"],
17424
+ does_not_own: ["Close process", "CRM stage hygiene", "SQL without shared SLA"],
17425
+ success_metrics: ["pipeline_created", "mql_to_sql", "drop_rate", "magic_number"],
17426
+ typical_decisions: ["Volume vs quality", "ABM vs broad", "Kill/scale a channel"],
17427
+ data_they_trust: ["Agreed attribution", "SQL rejection reasons", "Win/loss themes"],
17428
+ consult_questions: [
17429
+ "Creation vs acceptance \u2014 which instrument is red?",
17430
+ "Which source/channel's pipeline actually closes?",
17431
+ "Would fewer named accounts beat more spend?"
17432
+ ],
17433
+ failure_modes: ["MQL vanity", "More spend while drop_rate is red"],
17434
+ tensions: ["vp_sales quality/volume", "vp_revops attribution", "cro brand vs bookings"],
17435
+ play_hooks: ["demand-quality-over-volume", "abm-orchestration", "improve-magic-number", "rebalance-pipeline-mix"],
17436
+ signal_links: ["drop_rate", "magic_number", "mql_to_opp"],
17437
+ stress_notes: "If drop_rate is red, do not prescribe more spend."
17438
+ },
17439
+ {
17440
+ id: "vp_sales",
17441
+ title: "VP / Head of Sales",
17442
+ function: "sales",
17443
+ presence: presence("thin", "core", "core", "core"),
17444
+ owns: ["Quota/capacity system", "SDR\u2194AE design", "Forecast from the line"],
17445
+ does_not_own: ["MAP strategy", "CRM architecture", "Deal-level inspection (manager)", "Close execution (AE)"],
17446
+ success_metrics: ["attainment", "win_rate", "avg_sales_cycle", "flow_rate", "pipeline_coverage"],
17447
+ typical_decisions: ["Hire SDR vs AE vs SE", "Capacity vs win-rate bet"],
17448
+ data_they_trust: ["Capacity model", "Stage conversion", "Ramp curves"],
17449
+ consult_questions: [
17450
+ "Is the miss coverage, conversion, or cycle \u2014 which vital?",
17451
+ "What capacity math says hire vs improve win rate?",
17452
+ "Where is rollup forecast fiction?"
17453
+ ],
17454
+ failure_modes: ["Headcount before math", "Activity mandate when signal_to_noise is red"],
17455
+ tensions: ["cmo leads", "vp_revops process friction", "vp_cs oversell"],
17456
+ play_hooks: ["unstick-pipeline", "compress-sales-cycle", "rebalance-pipeline-mix"],
17457
+ signal_links: ["flow_rate", "avg_sales_cycle", "pipeline_coverage", "win_rate"],
17458
+ stress_notes: "No headcount without capacity math and a constraint vital."
17459
+ },
17460
+ {
17461
+ id: "sales_manager",
17462
+ title: "Sales Manager / Director",
17463
+ function: "sales",
17464
+ presence: presence("thin", "core", "core", "core"),
17465
+ owns: ["Pod inspection cadence", "Team forecast integrity", "Coaching vs PIP"],
17466
+ does_not_own: ["Company capacity model", "Marketing mix"],
17467
+ success_metrics: ["team_attainment", "forecast_accuracy", "flow_rate", "thread_depth", "freshness"],
17468
+ typical_decisions: ["Which deals get air cover", "Death-watch vs coach"],
17469
+ data_they_trust: ["Next-step/date evidence", "Thread counts", "Call/inspection artifacts"],
17470
+ consult_questions: [
17471
+ "Which owners concentrate stuck/stale $ ?",
17472
+ "What % of opps lack next step, date, or second thread?",
17473
+ "Is the miss skill, ICP, or process in this pod?"
17474
+ ],
17475
+ failure_modes: ["Cheerleading forecast", "Protecting zombies for coverage"],
17476
+ tensions: ["vp_sales commit pressure", "ae sandbag", "revops_specialist required fields"],
17477
+ play_hooks: ["unstick-pipeline", "clean-dead-pipeline", "multi-thread-deals"],
17478
+ signal_links: ["flow_rate", "thread_depth", "freshness"],
17479
+ stress_notes: "Coaching needs an inspection cadence exam, not vibes."
17480
+ },
17481
+ {
17482
+ id: "ae",
17483
+ title: "Account Executive",
17484
+ function: "sales",
17485
+ presence: presence("present", "core", "core", "core"),
17486
+ owns: ["Opp progression to close", "Deal multi-thread and commercial integrity"],
17487
+ does_not_own: ["Territory design", "Inbound SLA", "Post-sale delivery"],
17488
+ success_metrics: ["quota", "win_rate", "flow_rate", "thread_depth", "freshness"],
17489
+ typical_decisions: ["Advance/stall/kill", "Bring SE/exec/CS", "Discount vs term"],
17490
+ data_they_trust: ["Stage exit evidence", "Stakeholder map", "Mutual close dates"],
17491
+ consult_questions: [
17492
+ "Stuck on process, product, politics, or single thread \u2014 which vital?",
17493
+ "Which stage-exit criterion is unmet on the largest stuck $ ?",
17494
+ "Assist vs hunt for this account (plg)?"
17495
+ ],
17496
+ failure_modes: ["Happy ears", "Zombies for coverage", "Oversell CS cannot deliver"],
17497
+ tensions: ["sdr acceptance", "se custom POV", "csm handoff"],
17498
+ play_hooks: ["unstick-pipeline", "multi-thread-deals", "clean-dead-pipeline", "sales-cs-handoff-packet"],
17499
+ signal_links: ["flow_rate", "thread_depth", "freshness", "win_rate"],
17500
+ stress_notes: "Name unmet exit criteria and thread count; no rapport-only advice."
17501
+ },
17502
+ {
17503
+ id: "sdr",
17504
+ title: "SDR / BDR",
17505
+ function: "sales",
17506
+ presence: presence("thin", "core", "core", "present"),
17507
+ owns: ["Pipeline creation", "First-pass ICP/DQ", "Handoff quality into AE"],
17508
+ does_not_own: ["Close", "Campaign strategy", "Routing architecture"],
17509
+ success_metrics: ["sql_accepted", "opp_created", "signal_to_noise", "drop_rate"],
17510
+ typical_decisions: ["Persist vs DQ", "Escalate bad lists"],
17511
+ data_they_trust: ["ICP/DQ rules", "AE rejection reasons", "Speed-to-lead timers"],
17512
+ consult_questions: [
17513
+ "Volume low or AE acceptance low \u2014 which instrument?",
17514
+ "Where does inbound SLA break (create\u2192touch)?",
17515
+ "Are meetings becoming opps or activity theater?"
17516
+ ],
17517
+ failure_modes: ["Meeting spam", "Dials when acceptance is the constraint"],
17518
+ tensions: ["ae quality bar", "demand_gen list quality", "revops_specialist routing delay"],
17519
+ play_hooks: ["harden-routing-sla", "demand-quality-over-volume", "retarget-effort"],
17520
+ signal_links: ["drop_rate", "signal_to_noise", "mql_to_opp"],
17521
+ stress_notes: "Do not prescribe more dials when acceptance rate is the constraint."
17522
+ },
17523
+ {
17524
+ id: "se",
17525
+ title: "Sales Engineer / SC",
17526
+ function: "sales",
17527
+ presence: presence("absent", "thin", "present", "core"),
17528
+ owns: ["Technical validation", "POV/demo standards", "Scope honesty"],
17529
+ does_not_own: ["Commercial close", "Lead gen", "Implementation delivery"],
17530
+ success_metrics: ["win_rate_se_touched", "avg_sales_cycle", "flow_rate"],
17531
+ typical_decisions: ["Standard demo vs custom POV", "SE queue go/no-go"],
17532
+ data_they_trust: ["Written success criteria", "Security/architecture checklist"],
17533
+ consult_questions: [
17534
+ "Technical vs commercial stuck \u2014 which?",
17535
+ "Is SE queue time the cycle constraint?",
17536
+ "Are we customizing a bad-fit deal?"
17537
+ ],
17538
+ failure_modes: ["Unsupportable custom", "Late SE on enterprise cycle"],
17539
+ tensions: ["ae one-off custom", "csm overscope", "pmm narrative vs technical truth"],
17540
+ play_hooks: ["compress-sales-cycle", "unstick-pipeline", "multi-thread-deals"],
17541
+ signal_links: ["thread_depth", "flow_rate", "avg_sales_cycle"],
17542
+ stress_notes: "Thin on smb_velocity; do not prescribe an SE pool there."
17543
+ },
17544
+ {
17545
+ id: "sales_ops",
17546
+ title: "Sales Operations",
17547
+ function: "sales",
17548
+ presence: presence("thin", "present", "present", "core"),
17549
+ owns: ["Territory/overlay/quota admin", "Comp mechanics", "Sales reporting packs"],
17550
+ does_not_own: ["Coaching", "MAP creative", "CRM platform choice"],
17551
+ success_metrics: ["territory_balance", "crediting_accuracy", "signal_to_noise"],
17552
+ typical_decisions: ["Territory exception", "Crediting dispute"],
17553
+ data_they_trust: ["Ownership history", "Comp statements"],
17554
+ consult_questions: [
17555
+ "Does territory/comp explain the miss better than skill?",
17556
+ "Which reports disagree with vital instruments?",
17557
+ "Do SPIFs spike unlinked activity?"
17558
+ ],
17559
+ failure_modes: ["Spreadsheet vs CRM truth", "SPIFs that raise signal_to_noise red"],
17560
+ tensions: ["vp_sales exceptions", "revops_specialist CRM ownership"],
17561
+ play_hooks: ["rebalance-pipeline-mix", "retarget-effort"],
17562
+ signal_links: ["pipeline_coverage", "signal_to_noise"],
17563
+ stress_notes: "Fix definitions/ownership before new dashboards."
17564
+ },
17565
+ {
17566
+ id: "demand_gen",
17567
+ title: "Demand Generation Lead",
17568
+ function: "marketing",
17569
+ presence: presence("thin", "core", "core", "present"),
17570
+ owns: ["Accepted-pipeline creation from programs", "Channel mix experiments"],
17571
+ does_not_own: ["Named-account orchestration (abm)", "Positioning (pmm)", "Routing architecture"],
17572
+ success_metrics: ["pipeline_created", "cost_per_pipeline", "mql_to_opp", "magic_number"],
17573
+ typical_decisions: ["Kill/scale a channel", "Tighten scoring vs buy volume"],
17574
+ data_they_trust: ["Opp\u2192won by source", "AE rejection codes"],
17575
+ consult_questions: [
17576
+ "Which channel's pipeline closes \u2014 cut by source?",
17577
+ "Are we optimizing MQL count against $ ?",
17578
+ "Is drop_rate a reject-quality problem or a routing problem?"
17579
+ ],
17580
+ failure_modes: ["MQL vanity", "Spend increase while drop_rate red"],
17581
+ tensions: ["abm budget", "sdr quality", "mkt_ops launch capacity"],
17582
+ play_hooks: ["demand-quality-over-volume", "improve-magic-number", "rebalance-pipeline-mix"],
17583
+ signal_links: ["mql_to_opp", "cost_per_pipeline", "magic_number", "pipeline_coverage"],
17584
+ stress_notes: "If the leak is routing/SLA, hand to revops \u2014 do not buy more leads."
17585
+ },
17586
+ {
17587
+ id: "abm",
17588
+ title: "ABM Lead",
17589
+ function: "marketing",
17590
+ presence: presence("absent", "thin", "present", "core"),
17591
+ owns: ["Named-list tiering with Sales", "Orchestrated plays on that list"],
17592
+ does_not_own: ["Broad demand portfolio", "Deal close"],
17593
+ success_metrics: ["named_list_pipeline", "named_list_win_rate", "thread_depth"],
17594
+ typical_decisions: ["Tier entry/exit", "Stop funding a silent account"],
17595
+ data_they_trust: ["List=territory agreement", "Opps on named accounts"],
17596
+ consult_questions: [
17597
+ "Do AE territories match the named list?",
17598
+ "Pipeline/wins on-list vs engagement vanity?",
17599
+ "Is this smb_velocity without a named-account process?"
17600
+ ],
17601
+ failure_modes: ["Logo spray", "List\u2260territory"],
17602
+ tensions: ["demand_gen credit", "ae commitment", "sdr random outbound"],
17603
+ play_hooks: ["abm-orchestration", "multi-thread-deals", "retarget-effort"],
17604
+ signal_links: ["named_list_pipeline", "named_list_win_rate", "thread_depth", "signal_to_noise"],
17605
+ stress_notes: "Kill this play on smb_velocity without an agreed named list."
17606
+ },
17607
+ {
17608
+ id: "pmm",
17609
+ title: "Product Marketing",
17610
+ function: "marketing",
17611
+ presence: presence("core", "present", "core", "core"),
17612
+ owns: ["Positioning/narrative/competitive proof", "Win/loss synthesis"],
17613
+ does_not_own: ["Media buying", "Quota"],
17614
+ success_metrics: ["win_rate", "avg_sales_cycle", "enablement_adoption"],
17615
+ typical_decisions: ["Segment message vs one narrative", "Loss = product vs narrative vs ICP"],
17616
+ data_they_trust: ["Structured win/loss", "Whether sellers use the current narrative"],
17617
+ consult_questions: [
17618
+ "Are deals stalling at propose/decide for message or process?",
17619
+ "Do SDRs/AEs use the current narrative \u2014 adoption exam?",
17620
+ "Is packaging attracting bad ICP (later churn)?"
17621
+ ],
17622
+ failure_modes: ["Asset counts without adoption", "Monthly message churn"],
17623
+ tensions: ["demand_gen offers", "ae one-pager pressure", "se technical truth"],
17624
+ play_hooks: ["compress-sales-cycle", "unstick-pipeline", "improve-magic-number"],
17625
+ signal_links: ["win_rate", "avg_sales_cycle"],
17626
+ stress_notes: "Exams are win-rate/cycle and asset adoption \u2014 not deck volume."
17627
+ },
17628
+ {
17629
+ id: "lifecycle",
17630
+ title: "Lifecycle / Email Marketing",
17631
+ function: "marketing",
17632
+ presence: presence("core", "present", "core", "present"),
17633
+ owns: ["Stage-gated journeys", "Suppression and frequency caps"],
17634
+ does_not_own: ["Human SDR sequences", "CSM relationships"],
17635
+ success_metrics: ["journey_conversion", "signal_to_noise", "unsubscribe_rate"],
17636
+ typical_decisions: ["Build/kill a journey", "Who owns the human touch"],
17637
+ data_they_trust: ["Stage gates on clean CRM stages", "Holdouts"],
17638
+ consult_questions: [
17639
+ "Are journeys gated on trustworthy stages (freshness/drop_rate)?",
17640
+ "Where do lifecycle and SDR double-touch the same person?",
17641
+ "Creation gap or retention gap?"
17642
+ ],
17643
+ failure_modes: ["Spray that burns domain/trust", "Collision with SDR"],
17644
+ tensions: ["sdr human touch", "csm customer fatigue", "demand_gen calendar"],
17645
+ play_hooks: ["retarget-effort", "accelerate-expansion"],
17646
+ signal_links: ["signal_to_noise", "nrr"],
17647
+ stress_notes: "Kill more emails when deliverability or SDR collision is the constraint."
17648
+ },
17649
+ {
17650
+ id: "growth",
17651
+ title: "Growth Lead",
17652
+ function: "marketing",
17653
+ presence: presence("core", "present", "thin", "thin"),
17654
+ owns: ["Activation\u2192paid loops", "Product-event experiments tied to revenue ids"],
17655
+ does_not_own: ["Enterprise outbound", "Classical demand portfolio"],
17656
+ success_metrics: ["activation_rate", "paid_conversion", "nrr"],
17657
+ typical_decisions: ["Which funnel step to experiment", "Sales-assist threshold"],
17658
+ data_they_trust: ["Product events joined to CRM identity", "Cohort retention by path"],
17659
+ consult_questions: [
17660
+ "Acquisition, activation, or monetization \u2014 which step $ ?",
17661
+ "Is product\u2192CRM identity dropping (drop_rate/freshness)?",
17662
+ "When should a human AE enter?"
17663
+ ],
17664
+ failure_modes: ["Local click metrics that hurt NRR", "Shadow analytics vs RevOps defs"],
17665
+ tensions: ["demand_gen paid ownership", "gtm_engineer event taxonomy", "csm support load"],
17666
+ play_hooks: ["accelerate-expansion", "fix-handoff-gap"],
17667
+ signal_links: ["nrr", "drop_rate", "freshness"],
17668
+ stress_notes: "Core on plg; do not run a growth-loop plan as default on enterprise."
17669
+ },
17670
+ {
17671
+ id: "mkt_ops",
17672
+ title: "Marketing Operations",
17673
+ function: "marketing",
17674
+ presence: presence("present", "present", "core", "core"),
17675
+ owns: ["MAP execution/sync", "UTM/campaign taxonomy", "Scoring implementation"],
17676
+ does_not_own: ["Which programs to run", "Sales stage definitions"],
17677
+ success_metrics: ["sync_error_rate", "drop_rate", "freshness", "time_to_launch"],
17678
+ typical_decisions: ["Block a corrupt launch", "Safe scoring change"],
17679
+ data_they_trust: ["Sync logs", "Error queues", "Field dictionary"],
17680
+ consult_questions: [
17681
+ "Leak in MAP, sync, routing, or human SLA \u2014 which log?",
17682
+ "What taxonomy/scoring debt blocks the instrument?",
17683
+ "Missing vs late vs wrong vs unused campaign data?"
17684
+ ],
17685
+ failure_modes: ["Silent sync failures", "Scoring nobody trusts"],
17686
+ tensions: ["demand_gen speed", "revops_specialist lifecycle ownership", "gtm_engineer who automates"],
17687
+ play_hooks: ["fix-handoff-gap", "harden-routing-sla"],
17688
+ signal_links: ["drop_rate", "freshness"],
17689
+ stress_notes: "Observable routing/sync fixes before more programs."
17690
+ },
17691
+ {
17692
+ id: "content",
17693
+ title: "Content Marketing",
17694
+ function: "marketing",
17695
+ presence: presence("present", "present", "present", "present"),
17696
+ owns: ["Education/SEO/sales-enablement assets that feed demand or PMM"],
17697
+ does_not_own: ["Channel spend", "Named-account orchestration", "Positioning system"],
17698
+ success_metrics: ["assisted_pipeline", "enablement_adoption"],
17699
+ typical_decisions: ["What to publish vs kill", "Sales asset vs SEO asset"],
17700
+ data_they_trust: ["Which assets appear in closed-won paths"],
17701
+ consult_questions: [
17702
+ "Is content the constraint, or is demand/PMM starving for a specific asset?",
17703
+ "Which assets show up in won deals vs vanity traffic?",
17704
+ "Are sellers using the assets (adoption)?"
17705
+ ],
17706
+ failure_modes: ["Publishing volume without pipeline exams"],
17707
+ tensions: ["demand_gen calendar", "pmm narrative control"],
17708
+ play_hooks: ["improve-magic-number", "demand-quality-over-volume"],
17709
+ signal_links: ["magic_number", "pipeline_coverage"],
17710
+ stress_notes: "Thin card \u2014 open only when an asset gap is the constraint."
17711
+ },
17712
+ {
17713
+ id: "brand",
17714
+ title: "Brand Marketing",
17715
+ function: "marketing",
17716
+ presence: presence("thin", "thin", "present", "present"),
17717
+ owns: ["Category/brand system"],
17718
+ does_not_own: ["Pipeline SLA", "MQL definitions"],
17719
+ success_metrics: ["brand_search", "pipeline_influenced"],
17720
+ typical_decisions: ["Brand program vs always-on demand"],
17721
+ data_they_trust: ["Influenced pipeline with an agreed exam"],
17722
+ consult_questions: [
17723
+ "What GTM exam makes this brand bet measurable?",
17724
+ "Is this polish on a non-constraint?",
17725
+ "Enterprise category need vs smb_velocity waste?"
17726
+ ],
17727
+ failure_modes: ["Brand programs with no pipeline/NRR exam"],
17728
+ tensions: ["cro near-term bookings", "demand_gen budget"],
17729
+ play_hooks: ["improve-magic-number"],
17730
+ signal_links: ["magic_number", "pipeline_coverage"],
17731
+ stress_notes: "Thin card \u2014 defer without a dated pipeline or NRR exam."
17732
+ },
17733
+ {
17734
+ id: "field_partner",
17735
+ title: "Field / Partner Marketing",
17736
+ function: "marketing",
17737
+ presence: presence("thin", "thin", "present", "core"),
17738
+ owns: ["Events and partner co-marketing sourced pipeline"],
17739
+ does_not_own: ["Partner contract/comp", "AE close"],
17740
+ success_metrics: ["partner_sourced_pipeline", "event_pipeline"],
17741
+ typical_decisions: ["Which event/partner to fund"],
17742
+ data_they_trust: ["Sourced/influenced opps with source hygiene"],
17743
+ consult_questions: [
17744
+ "Is partner_motion real in custom_context, or are we inventing a channel?",
17745
+ "Event/partner pipeline that closes vs badge scans?",
17746
+ "Does source hygiene let RevOps trust the cut?"
17747
+ ],
17748
+ failure_modes: ["Badge-scan vanity", "Invented partner motion"],
17749
+ tensions: ["ae time", "demand_gen budget"],
17750
+ play_hooks: ["rebalance-pipeline-mix", "fix-handoff-gap"],
17751
+ signal_links: ["pipeline_coverage", "drop_rate"],
17752
+ stress_notes: "Thin card \u2014 skip unless partner/field is in profile context or the tree is enterprise."
17753
+ },
17754
+ {
17755
+ id: "vp_revops",
17756
+ title: "VP / Head of RevOps",
17757
+ function: "revops",
17758
+ presence: presence("thin", "thin", "core", "core"),
17759
+ owns: ["GTM operating system", "Definition dictionary", "Systems vs governance mix", "Change-freeze calls"],
17760
+ does_not_own: ["Quota politics", "Campaign creative", "Closing"],
17761
+ success_metrics: ["drop_rate", "freshness", "routing_sla", "forecast_hygiene"],
17762
+ typical_decisions: ["Automate vs policy vs train", "Freeze CRM mid-quarter"],
17763
+ data_they_trust: ["Routing/stage logs", "Exception queues"],
17764
+ consult_questions: [
17765
+ "Behavior vs instrument \u2014 which log proves it?",
17766
+ "Is this a 65% systems leak or a 35% forecast/capacity ritual gap?",
17767
+ "What 30-day hold exam proves the fix stuck?"
17768
+ ],
17769
+ failure_modes: ["Dashboard theater", "Automating broken process"],
17770
+ tensions: ["vp_sales friction", "cmo scoring", "gtm_engineer roadmap"],
17771
+ play_hooks: ["harden-routing-sla", "fix-handoff-gap", "clean-dead-pipeline", "forecast-ritual-hygiene"],
17772
+ signal_links: ["drop_rate", "freshness", "flow_rate", "weighted_pipeline", "forecast_commit_history"],
17773
+ stress_notes: "Systems-shaped constraints get systems fixes, not a steering committee."
17774
+ },
17775
+ {
17776
+ id: "revops_specialist",
17777
+ title: "RevOps Specialist / Manager",
17778
+ function: "revops",
17779
+ presence: presence("thin", "present", "core", "core"),
17780
+ owns: ["CRM process day-to-day", "Assignment rules/SLA timers", "Hygiene/dupes"],
17781
+ does_not_own: ["GTM strategy", "Unbounded reports"],
17782
+ success_metrics: ["routing_sla", "unassigned_age", "duplicate_rate", "drop_rate", "freshness"],
17783
+ typical_decisions: ["Patch vs proper fix", "Exception vs enforce"],
17784
+ data_they_trust: ["Assignment logs", "Flow errors", "Before/after samples"],
17785
+ consult_questions: [
17786
+ "Missing, late, wrong, or unused data \u2014 which class?",
17787
+ "Which queue/timer maps to the drop_rate $ ?",
17788
+ "What monitor proves 30-day hold?"
17789
+ ],
17790
+ failure_modes: ["One-off workflows", "No monitoring after 'fixed'"],
17791
+ tensions: ["ae required-field friction", "mkt_ops sync ownership", "gtm_engineer code vs declarative"],
17792
+ play_hooks: ["harden-routing-sla", "fix-handoff-gap", "clean-dead-pipeline"],
17793
+ signal_links: ["drop_rate", "freshness"],
17794
+ stress_notes: "Training vs policy vs enforcement \u2014 pick one; require a monitor."
17795
+ },
17796
+ {
17797
+ id: "gtm_engineer",
17798
+ title: "GTM Engineer / Automation",
17799
+ function: "revops",
17800
+ presence: presence("present", "thin", "present", "core"),
17801
+ owns: ["Integrations and event pipelines", "Identity resolution", "Observability/retries"],
17802
+ does_not_own: ["Stage/SLA definitions", "Campaign strategy"],
17803
+ success_metrics: ["freshness", "drop_rate", "identity_match_rate", "error_budget"],
17804
+ typical_decisions: ["Build vs buy", "Batch vs stream", "Backfill vs detect-first"],
17805
+ data_they_trust: ["Dead-letter queues", "Schema diffs", "Latency of lead-to-route"],
17806
+ consult_questions: [
17807
+ "Never arrived, late, or wrong \u2014 which failure class?",
17808
+ "Is identity resolution the hidden drop_rate?",
17809
+ "Observability before another automation?"
17810
+ ],
17811
+ failure_modes: ["Fragile zaps", "Activating automation on broken identity"],
17812
+ tensions: ["revops_specialist declarative vs code", "growth event taxonomy"],
17813
+ play_hooks: ["harden-routing-sla", "fix-handoff-gap"],
17814
+ signal_links: ["freshness", "drop_rate"],
17815
+ stress_notes: "Foundation identity before activation automation."
17816
+ },
17817
+ {
17818
+ id: "vp_cs",
17819
+ title: "VP / Head of Customer Success",
17820
+ function: "cs",
17821
+ presence: presence("core", "present", "core", "core"),
17822
+ owns: ["Retention/expansion operating system", "Touch model by segment", "Where expansion sits"],
17823
+ does_not_own: ["Net-new logos", "Day-to-day save execution (csm)", "Renewal commercial desk (renewals)"],
17824
+ success_metrics: ["grr", "nrr", "logo_churn", "ttv"],
17825
+ typical_decisions: ["Tech-touch vs high-touch", "CS vs AM vs AE for expansion"],
17826
+ data_they_trust: ["Health scores calibrated to churn", "Renewal book aging"],
17827
+ consult_questions: [
17828
+ "NRR limited by churn, failed expansion, or both?",
17829
+ "Is TTV/onboarding the leak (not QBR cadence)?",
17830
+ "Did Sales hand off oversold deals \u2014 packet completeness?"
17831
+ ],
17832
+ failure_modes: ["QBR theater", "Expansion into unhealthy accounts"],
17833
+ tensions: ["vp_sales oversell", "product blockers", "lifecycle fatigue"],
17834
+ play_hooks: ["reduce-logo-churn", "accelerate-expansion", "sales-cs-handoff-packet", "fix-renewal-process", "renewal-early-warning"],
17835
+ signal_links: ["grr", "nrr", "ttv", "contraction_arr"],
17836
+ stress_notes: "If onboarding/TTV is the leak, do not start with more QBRs."
17837
+ },
17838
+ {
17839
+ id: "csm",
17840
+ title: "Customer Success Manager",
17841
+ function: "cs",
17842
+ presence: presence("core", "core", "core", "core"),
17843
+ owns: ["Account health and save plans", "Adoption/value evidence"],
17844
+ does_not_own: ["Net-new logos", "Renewal desk commercial close", "Health-score model build"],
17845
+ success_metrics: ["grr", "logo_churn", "ttv", "nrr"],
17846
+ typical_decisions: ["Save vs churn", "Expand now vs restore health first"],
17847
+ data_they_trust: ["Usage+outcomes", "Champion map", "Handoff packet"],
17848
+ consult_questions: [
17849
+ "Product, value, politics, or champion loss \u2014 which leading indicator failed?",
17850
+ "Handoff packet complete on recent churns?",
17851
+ "Expansion now or after health restore?"
17852
+ ],
17853
+ failure_modes: ["Friendly without commercial clarity", "Expanding unhealthy accounts"],
17854
+ tensions: ["ae oversell", "renewals ownership at deadline", "lifecycle email"],
17855
+ play_hooks: ["reduce-logo-churn", "accelerate-expansion", "sales-cs-handoff-packet"],
17856
+ signal_links: ["grr", "nrr", "ttv"],
17857
+ stress_notes: "Risk must translate to GRR/NRR $ and an exam date."
17858
+ },
17859
+ {
17860
+ id: "renewals",
17861
+ title: "Renewals Manager",
17862
+ function: "cs",
17863
+ presence: presence("present", "present", "core", "core"),
17864
+ owns: ["Renewal forecast and commercial deadline", "Save plays at the boundary"],
17865
+ does_not_own: ["Day-to-day adoption", "Net-new sales"],
17866
+ success_metrics: ["grr", "renewal_rate", "contraction_arr", "early_warning_lead_time"],
17867
+ typical_decisions: ["When to open commercial talk", "Discount vs term vs walk"],
17868
+ data_they_trust: ["Contract dates SoR", "CSM risk flags \u226560 days out"],
17869
+ consult_questions: [
17870
+ "What share of the book is unflagged inside 60 days?",
17871
+ "Commercial, value, or champion-change risk?",
17872
+ "Are AE promises creating renewal landmines?"
17873
+ ],
17874
+ failure_modes: ["Last-week scramble", "Discount addiction"],
17875
+ tensions: ["csm relationship", "ae original promises", "sales_ops discount policy"],
17876
+ play_hooks: ["renewal-early-warning", "fix-renewal-process", "reduce-logo-churn"],
17877
+ signal_links: ["grr", "contraction_arr", "early_warning_lead_time"],
17878
+ stress_notes: "If value never landed (TTV), fix onboarding before renewal training."
17879
+ },
17880
+ {
17881
+ id: "onboarding",
17882
+ title: "Onboarding / Implementation Lead",
17883
+ function: "cs",
17884
+ presence: presence("core", "present", "present", "core"),
17885
+ owns: ["Closed-Won readiness", "Time-to-first-value path", "Implementation risk escalation"],
17886
+ does_not_own: ["Product roadmap", "Renewal commercial close", "Net-new deal close"],
17887
+ success_metrics: ["ttv", "onboarding_completion", "packet_field_completeness", "early_tenure_grr"],
17888
+ typical_decisions: ["Ready to start vs return for missing scope", "Standard path vs risk escalation"],
17889
+ data_they_trust: ["Handoff packet", "Implementation milestones", "Product usage or first-value evidence"],
17890
+ consult_questions: [
17891
+ "Which readiness field is missing on delayed starts?",
17892
+ "Where does signed\u2192kickoff\u2192first value stall by segment?",
17893
+ "Is product usage measurable, or must we observe + recommend event capture?"
17894
+ ],
17895
+ failure_modes: ["Starting without scope or owner", "Calling kickoff completion 'value'"],
17896
+ tensions: ["ae speed vs readiness", "csm relationship ownership", "product blocker escalation"],
17897
+ play_hooks: ["sales-cs-handoff-packet", "reduce-logo-churn"],
17898
+ signal_links: ["ttv", "packet_field_completeness", "grr"],
17899
+ stress_notes: "If TTV events do not exist, recommend capture; never invent onboarding performance."
17900
+ }
17901
+ ];
17902
+ }
17903
+ });
17904
+
17905
+ // src/data/gtm-counsel/constraint-signals.ts
17906
+ function uniq(ids) {
17907
+ return [...new Set(ids)];
17908
+ }
17909
+ function isVitalSignal(value) {
17910
+ return VITAL_SIGNAL_IDS.includes(value);
17911
+ }
17912
+ function isKnownConstraintSignal(value) {
17913
+ return isVitalSignal(value) || Boolean(getMetricExplainer(value)) || ROUTED_SIGNALS.has(value) || Boolean(METRIC_SIGNAL_OWNERS[value]);
17914
+ }
17915
+ function signalKind(id) {
17916
+ if (isVitalSignal(id)) return "vital";
17917
+ if (getMetricExplainer(id) || ROUTED_SIGNALS.has(id) || METRIC_SIGNAL_OWNERS[id]) return "metric";
17918
+ return "stage";
17919
+ }
17920
+ function staleOwners(blob) {
17921
+ const staleDeal = /stale (opp|deal|pipeline)|zombie|dead pipeline|quiet opp/.test(blob);
17922
+ const dataDecay = /sync|enrich|contact decay|data quality|duplicate|hygiene of (people|contact|record)/.test(blob);
17923
+ if (staleDeal && !dataDecay) return ["counsel_sales"];
17924
+ if (dataDecay && !staleDeal) return ["counsel_revops"];
17925
+ return ["counsel_revops", "counsel_sales"];
17926
+ }
17927
+ function noiseOwners(blob) {
17928
+ const nurture = /nurture|campaign|lifecycle|email sequence|drip/.test(blob);
17929
+ const automation = /automat|zap|workflow noise|bad flow|enrich spam/.test(blob);
17930
+ const repActivity = /rep activity|dials?|calls logged|activity theater|unlinked activ/.test(blob);
17931
+ if (automation && !nurture && !repActivity) return ["counsel_revops"];
17932
+ if (nurture && !repActivity) return ["counsel_marketing"];
17933
+ if (repActivity && !nurture) return ["counsel_sales"];
17934
+ if (nurture && repActivity) return ["counsel_sales", "counsel_marketing"];
17935
+ return ["counsel_sales", "counsel_marketing"];
17936
+ }
17937
+ function dropOwners(blob) {
17938
+ const acceptanceQuality = /reject|acceptance|accepted pipeline|mql.?to.?(sql|opp)|sql.?to.?opp|bad fit|lead quality|scoring/.test(
17939
+ blob
17940
+ );
17941
+ return acceptanceQuality ? ["counsel_revops", "counsel_marketing"] : ["counsel_revops"];
17942
+ }
17943
+ function ownersForConstraintSignal(signal, blob = "") {
17944
+ if (signal === "freshness") return staleOwners(blob);
17945
+ if (signal === "signal_to_noise") return noiseOwners(blob);
17946
+ if (signal === "drop_rate") return dropOwners(blob);
17947
+ if (signal === "flow_rate" || signal === "thread_depth") return ["counsel_sales"];
17948
+ return [...METRIC_SIGNAL_OWNERS[signal] ?? []];
17949
+ }
17950
+ function phraseOwners(blob) {
17951
+ if (/\b(churn|nrr|grr|renewal|retention|customer success|onboarding|ttv|expansion arr)\b/.test(blob)) {
17952
+ return ["counsel_cs"];
17953
+ }
17954
+ if (/\b(rout(e|ing)|time-to-lead|assignment (rule|queue)|unassigned|sync (gap|fail)|enrichment coverage)\b/.test(
17955
+ blob
17956
+ )) {
17957
+ return ["counsel_revops"];
17958
+ }
17959
+ if (/\b(mql|sql accept|demand gen|demand-gen|abm|named.account|pipeline created|created pipeline|marketing.sourced|marketing.created)\b/.test(
17960
+ blob
17961
+ )) {
17962
+ return ["counsel_marketing"];
17963
+ }
17964
+ if (/\b(quota|win rate|sales cycle|deal inspect|single.thread|forecast (commit|sandbag)|sdr\b|ae\b)\b/.test(
17965
+ blob
17966
+ )) {
17967
+ return ["counsel_sales"];
17968
+ }
17969
+ return [];
17970
+ }
17971
+ function normalizeSignal(value) {
17972
+ if (typeof value !== "string") return null;
17973
+ const raw = value.trim().toLowerCase();
17974
+ if (!raw) return null;
17975
+ const resolved = resolveMetricId(raw) ?? raw.replace(/[-\s]+/g, "_");
17976
+ return isKnownConstraintSignal(resolved) ? resolved : null;
17977
+ }
17978
+ function digestObjectField(digest, field) {
17979
+ if (!digest || typeof digest !== "object") return null;
17980
+ const value = digest[field];
17981
+ return value && typeof value === "object" ? value : null;
17982
+ }
17983
+ function signalFromDigest(digest, field) {
17984
+ const value = digestObjectField(digest, field);
17985
+ if (!value) return null;
17986
+ const id = normalizeSignal(value.signal ?? value.id);
17987
+ if (!id) return null;
17988
+ return {
17989
+ id,
17990
+ kind: signalKind(id),
17991
+ dollar_value: typeof value.dollar_value === "number" ? value.dollar_value : null,
17992
+ cause: typeof value.cause === "string" ? value.cause.trim() : void 0,
17993
+ evidence: typeof value.evidence === "string" ? value.evidence.trim() : void 0
17994
+ };
17995
+ }
17996
+ function healthGateSignal(ctx, blob) {
17997
+ const structured = signalFromDigest(ctx.digest, "health_gate");
17998
+ if (structured && isVitalSignal(String(structured.id))) {
17999
+ const reading2 = ctx.vital_readings?.find((v) => v.vital_sign === structured.id);
18000
+ return {
18001
+ ...structured,
18002
+ dollar_value: reading2?.dollar_value ?? structured.dollar_value ?? null
18003
+ };
18004
+ }
18005
+ if (ctx.digest && typeof ctx.digest === "object") {
18006
+ const d = ctx.digest;
18007
+ const legacy = normalizeSignal(d.gating_vital);
18008
+ if (legacy && isVitalSignal(legacy)) {
18009
+ const reading2 = ctx.vital_readings?.find((v) => v.vital_sign === legacy);
18010
+ return {
18011
+ id: legacy,
18012
+ kind: "vital",
18013
+ dollar_value: reading2?.dollar_value ?? null,
18014
+ evidence: "Stage A gating vital"
18015
+ };
18016
+ }
18017
+ const lines = Array.isArray(d.worst_problems_ordered) ? d.worst_problems_ordered.filter((x) => typeof x === "string") : [];
18018
+ for (const line of lines) {
18019
+ const lower = line.toLowerCase();
18020
+ const vital = VITAL_SIGNAL_IDS.find(
18021
+ (id) => lower.includes(id) || lower.includes(id.replace(/_/g, " "))
18022
+ );
18023
+ if (vital) {
18024
+ const reading2 = ctx.vital_readings?.find((v) => v.vital_sign === vital);
18025
+ return {
18026
+ id: vital,
18027
+ kind: "vital",
18028
+ dollar_value: reading2?.dollar_value ?? null,
18029
+ evidence: line
18030
+ };
18031
+ }
18032
+ }
18033
+ }
18034
+ if (!ctx.gating_vital) return null;
18035
+ const reading = ctx.vital_readings?.find((v) => v.vital_sign === ctx.gating_vital);
18036
+ return {
18037
+ id: ctx.gating_vital,
18038
+ kind: "vital",
18039
+ dollar_value: reading?.dollar_value ?? null,
18040
+ evidence: blob ? `Health snapshot gate for ${blob.slice(0, 80)}` : "Health snapshot gate"
18041
+ };
18042
+ }
18043
+ function metricMention(blob) {
18044
+ const definitions = listMetricExplainers().filter((m) => m.kind === "saas").flatMap((m) => [m.id, m.label, ...m.aliases ?? []].map((term) => ({ id: m.id, term }))).sort((a, b) => b.term.length - a.term.length);
18045
+ for (const { id, term } of definitions) {
18046
+ const escaped = term.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18047
+ if (new RegExp(`(^|\\b)${escaped}(\\b|$)`, "i").test(blob)) return id;
18048
+ }
18049
+ return null;
18050
+ }
18051
+ function objectiveSignal(ctx, health) {
18052
+ const structured = signalFromDigest(health.digest, "objective_constraint");
18053
+ if (structured) {
18054
+ const reading = health.metric_readings?.find((m) => m.metric === structured.id);
18055
+ if (!reading || !["green", "neutral"].includes(reading.status) || structured.kind === "vital") {
18056
+ return {
18057
+ ...structured,
18058
+ evidence: structured.evidence || "Stage A objective constraint"
18059
+ };
18060
+ }
18061
+ }
18062
+ for (const playId of ctx.play_ids ?? []) {
18063
+ const signal = getPlayRouting(playId)?.constraint_signals[0];
18064
+ if (signal) {
18065
+ return {
18066
+ id: signal,
18067
+ kind: signalKind(String(signal)),
18068
+ evidence: `Draft play: ${playId}`
18069
+ };
18070
+ }
18071
+ }
18072
+ const blob = [ctx.objective, ...ctx.operator_constraints ?? []].join(" ").toLowerCase();
18073
+ const mentionedMetric = metricMention(blob);
18074
+ const metric = mentionedMetric === "pipeline_coverage" && /\b(stale|zombie|inflated|fake|false) (deal|opp|pipeline|coverage)|\b(inflating|faking) coverage\b/.test(
18075
+ blob
18076
+ ) ? null : mentionedMetric;
18077
+ if (metric) {
18078
+ const reading = health.metric_readings?.find((m) => m.metric === metric);
18079
+ return {
18080
+ id: metric,
18081
+ kind: "metric",
18082
+ evidence: reading ? `${reading.formatted ?? reading.value ?? "available"} (${reading.status})` : "Objective metric (not yet computed)"
18083
+ };
18084
+ }
18085
+ const vital = VITAL_SIGNAL_IDS.find(
18086
+ (id) => blob.includes(id) || blob.includes(id.replace(/_/g, " "))
18087
+ );
18088
+ if (vital) return { id: vital, kind: "vital", evidence: "Objective text" };
18089
+ return null;
18090
+ }
18091
+ function resolveConstraint(input) {
18092
+ const blob = [
18093
+ input.objective.objective,
18094
+ ...input.objective.operator_constraints ?? [],
18095
+ ...input.objective.play_ids ?? []
18096
+ ].join(" ").toLowerCase();
18097
+ const healthGate = healthGateSignal(input.health, blob);
18098
+ const objective = objectiveSignal(input.objective, input.health);
18099
+ const redMetrics = (input.health.metric_readings ?? []).filter(
18100
+ (metric) => metric.status === "red" && metric.value != null
18101
+ );
18102
+ const redMetricOwners = redMetrics.flatMap(
18103
+ (metric) => ownersForConstraintSignal(metric.metric, blob)
18104
+ );
18105
+ const healthOwners = uniq([
18106
+ ...healthGate ? ownersForConstraintSignal(String(healthGate.id), blob) : [],
18107
+ ...redMetricOwners
18108
+ ]);
18109
+ const playOwners = (input.objective.play_ids ?? []).map(primaryOwnerForPlay).filter((id) => id != null);
18110
+ const signalOwners = objective ? ownersForConstraintSignal(String(objective.id), blob) : [];
18111
+ const objectiveOwners = uniq([
18112
+ ...playOwners,
18113
+ ...signalOwners,
18114
+ ...phraseOwners(blob)
18115
+ ]);
18116
+ const lockedOwners = uniq([...healthOwners, ...objectiveOwners]);
18117
+ const notes = [];
18118
+ if (healthGate) {
18119
+ notes.push(
18120
+ `Health gate: ${String(healthGate.id)} \u2192 ${healthOwners.join(", ") || "no mapped owner"}`
18121
+ );
18122
+ }
18123
+ if (redMetrics.length > 0) {
18124
+ notes.push(
18125
+ `Verified red metrics: ${redMetrics.map((metric) => metric.metric).join(", ")} \u2192 ${uniq(redMetricOwners).join(", ") || "no mapped owner"}`
18126
+ );
18127
+ }
18128
+ if (objective) {
18129
+ notes.push(
18130
+ `Objective constraint: ${String(objective.id)} \u2192 ${objectiveOwners.join(", ") || "no mapped owner"}`
18131
+ );
18132
+ }
18133
+ if (healthOwners.length > 0 && objectiveOwners.length > 0 && !objectiveOwners.some((id) => healthOwners.includes(id))) {
18134
+ notes.push(
18135
+ `Constraint conflict: preserve ${healthOwners.join(", ")} for health and ${objectiveOwners.join(", ")} for the objective; exec must sequence them.`
18136
+ );
18137
+ }
18138
+ return {
18139
+ health_gate: healthGate,
18140
+ objective_constraint: objective,
18141
+ health_gate_owners: healthOwners,
18142
+ objective_owners: objectiveOwners,
18143
+ locked_owners: lockedOwners,
18144
+ primary_owner: healthOwners[0] ?? objectiveOwners[0] ?? null,
18145
+ notes
18146
+ };
18147
+ }
18148
+ function resolveLegacyConstraint(objective, playIds = [], gatingVital) {
18149
+ return resolveConstraint({
18150
+ health: {
18151
+ gating_vital: isVitalSignal(gatingVital ?? "") ? gatingVital : null
18152
+ },
18153
+ objective: { objective, play_ids: playIds }
18154
+ });
18155
+ }
18156
+ function extractGatingVitalFromDigest(digest, fallbackVital) {
18157
+ const signal = healthGateSignal(
18158
+ {
18159
+ digest,
18160
+ gating_vital: isVitalSignal(fallbackVital ?? "") ? fallbackVital : null
18161
+ },
18162
+ ""
18163
+ );
18164
+ return signal && isVitalSignal(String(signal.id)) ? signal.id : null;
18165
+ }
18166
+ var VITAL_SIGNAL_IDS, METRIC_SIGNAL_OWNERS, ROUTED_SIGNALS;
18167
+ var init_constraint_signals = __esm({
18168
+ "src/data/gtm-counsel/constraint-signals.ts"() {
18169
+ "use strict";
18170
+ init_metric_definitions();
18171
+ init_play_routing();
18172
+ VITAL_SIGNAL_IDS = [
18173
+ "freshness",
18174
+ "flow_rate",
18175
+ "drop_rate",
18176
+ "signal_to_noise",
18177
+ "thread_depth"
18178
+ ];
18179
+ METRIC_SIGNAL_OWNERS = {
18180
+ nrr: ["counsel_cs"],
18181
+ grr: ["counsel_cs"],
18182
+ contraction_arr: ["counsel_cs"],
18183
+ logo_churn: ["counsel_cs"],
18184
+ early_warning_lead_time: ["counsel_cs"],
18185
+ packet_field_completeness: ["counsel_cs"],
18186
+ ttv: ["counsel_cs"],
18187
+ magic_number: ["counsel_marketing"],
18188
+ mql_to_opp: ["counsel_marketing"],
18189
+ cost_per_pipeline: ["counsel_marketing"],
18190
+ named_list_pipeline: ["counsel_marketing"],
18191
+ named_list_win_rate: ["counsel_marketing"],
18192
+ pipeline_coverage: ["counsel_marketing", "counsel_sales"],
18193
+ avg_sales_cycle: ["counsel_sales"],
18194
+ win_rate: ["counsel_sales"],
18195
+ weighted_pipeline: ["counsel_revops", "counsel_sales"],
18196
+ forecast_commit_history: ["counsel_revops", "counsel_sales"]
18197
+ };
18198
+ ROUTED_SIGNALS = new Set(
18199
+ Object.values(PLAY_ROUTING).flatMap(
18200
+ (routing) => routing.constraint_signals.map((signal) => String(signal))
18201
+ )
18202
+ );
18203
+ }
18204
+ });
18205
+
18206
+ // src/data/gtm-counsel/seating.ts
18207
+ function defaultSeatOrder(tree) {
18208
+ const tieBreak = TREE_TIE_BREAKS[tree];
18209
+ return COUNSEL_PACKS.filter(
18210
+ (pack) => pack.id !== "counsel_exec"
18211
+ ).slice().sort((a, b) => {
18212
+ const weightDelta = b.tree_weight[tree] - a.tree_weight[tree];
18213
+ if (weightDelta !== 0) return weightDelta;
18214
+ return tieBreak.indexOf(a.id) - tieBreak.indexOf(b.id);
18215
+ }).map((pack) => pack.id);
18216
+ }
18217
+ function baselineSeatWeight(packId, tree) {
18218
+ const pack = COUNSEL_PACKS.find((candidate) => candidate.id === packId);
18219
+ const weight = pack?.tree_weight[tree] ?? 0.5;
18220
+ if (weight >= 0.8) return "full";
18221
+ if (weight >= 0.5) return "secondary";
18222
+ return "risk_note";
18223
+ }
18224
+ function inferConstraintOwnerPacks(objective, playIds = [], gatingVital) {
18225
+ return resolveLegacyConstraint(objective, playIds, gatingVital).locked_owners;
18226
+ }
18227
+ function inferConstraintOwnerPack(objective, playIds = [], gatingVital) {
18228
+ return inferConstraintOwnerPacks(objective, playIds, gatingVital)[0] ?? null;
18229
+ }
18230
+ function inferRoundtableBias(objective, profile) {
18231
+ const text = [objective, profile?.user_scope, profile?.custom_context].filter(Boolean).join(" ").toLowerCase();
18232
+ const notes = [];
18233
+ const demoteToRiskNote = [];
18234
+ const preferFull = [];
18235
+ const strongMarketing = /marketing (lens|view|perspective)|from marketing|as cmo\b/.test(text);
18236
+ const strongRevops = /revops (lens|view)|rev ops (lens|view)|systems view|from revops/.test(text);
18237
+ const strongCs = /cs (lens|view)|success (lens|view)|retention view|nrr view/.test(text);
18238
+ const strongSales = /sales (lens|view|perspective)|from sales|ae view|quota view/.test(text);
18239
+ const weakMarketing = /\b(cmo|demand gen|demand-gen|mqls?\b|abm\b|pmm\b)\b/.test(text);
18240
+ const weakRevops = /\b(revops|rev ops|routing sla|gtm engineer)\b/.test(text);
18241
+ const weakCs = /\b(customer success|renewals manager|csm\b)\b/.test(text);
18242
+ const weakSales = /\b(vp sales|sales manager|quota attain)\b/.test(text);
18243
+ const notCro = /not cro|ignore cro|operator framing|not ceo.centric|skip exec politics/.test(text);
18244
+ if (strongMarketing || weakMarketing) {
18245
+ preferFull.push("counsel_marketing", "counsel_exec");
18246
+ notes.push(strongMarketing ? "Bias: marketing-weighted roundtable" : "Bias: marketing mentioned \u2014 keep marketing full");
18247
+ }
18248
+ if (strongRevops || weakRevops) {
18249
+ preferFull.push("counsel_revops", "counsel_exec");
18250
+ notes.push(strongRevops ? "Bias: RevOps-weighted roundtable" : "Bias: RevOps mentioned \u2014 keep RevOps full");
18251
+ }
18252
+ if (strongCs || weakCs) {
18253
+ preferFull.push("counsel_cs", "counsel_exec");
18254
+ notes.push(strongCs ? "Bias: CS-weighted roundtable" : "Bias: CS mentioned \u2014 keep CS full");
18255
+ }
18256
+ if (strongSales || weakSales) {
18257
+ preferFull.push("counsel_sales", "counsel_exec");
18258
+ notes.push(strongSales ? "Bias: sales-weighted roundtable" : "Bias: sales mentioned \u2014 keep sales full");
18259
+ }
18260
+ if (notCro) {
18261
+ notes.push("Bias: operator framing for exec seat (not CRO-centric voice)");
18262
+ }
18263
+ const strong = [
18264
+ strongMarketing ? "counsel_marketing" : null,
18265
+ strongRevops ? "counsel_revops" : null,
18266
+ strongCs ? "counsel_cs" : null,
18267
+ strongSales ? "counsel_sales" : null
18268
+ ].filter((p) => p != null);
18269
+ if (strong.length === 1) {
18270
+ for (const p of ALL_FUNCTION) {
18271
+ if (p !== strong[0]) demoteToRiskNote.push(p);
18272
+ }
18273
+ }
18274
+ return { demoteToRiskNote, preferFull, notes };
18275
+ }
18276
+ function buildRoundtableSeating(opts) {
18277
+ const {
18278
+ tree,
18279
+ objective,
18280
+ profile = null,
18281
+ playIds = [],
18282
+ gatingVital = null,
18283
+ operatorConstraints = []
18284
+ } = opts;
18285
+ const bias = inferRoundtableBias(objective, profile);
18286
+ const objectiveContext = {
18287
+ objective,
18288
+ play_ids: playIds,
18289
+ operator_constraints: operatorConstraints
18290
+ };
18291
+ const constraint = opts.resolvedConstraint ?? resolveConstraint({
18292
+ health: opts.health ?? {
18293
+ gating_vital: gatingVital && ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"].includes(
18294
+ gatingVital
18295
+ ) ? gatingVital : null
18296
+ },
18297
+ objective: objectiveContext
18298
+ });
18299
+ const constraintOwners = constraint.locked_owners;
18300
+ const ownerSet = new Set(constraintOwners);
18301
+ const demote = new Set(bias.demoteToRiskNote);
18302
+ const prefer = new Set(bias.preferFull);
18303
+ const seats = [{ pack_id: "counsel_exec", weight: "full" }];
18304
+ for (const packId of defaultSeatOrder(tree)) {
18305
+ let weight = baselineSeatWeight(packId, tree);
18306
+ if (demote.has(packId) && !prefer.has(packId)) weight = "risk_note";
18307
+ if (prefer.has(packId)) weight = "full";
18308
+ if (ownerSet.has(packId)) weight = "full";
18309
+ seats.push({ pack_id: packId, weight });
18310
+ }
18311
+ for (const owner of constraintOwners) {
18312
+ if (!seats.some((s) => s.pack_id === owner)) {
18313
+ seats.push({ pack_id: owner, weight: "full" });
18314
+ }
18315
+ }
18316
+ const bias_notes = [...bias.notes, ...constraint.notes];
18317
+ if (constraintOwners.length) {
18318
+ bias_notes.push(`Constraint-owning pack locked full: ${constraintOwners.join(", ")}`);
18319
+ }
18320
+ return { tree, seats, bias_notes, constraint };
18321
+ }
18322
+ var ALL_FUNCTION, TREE_TIE_BREAKS;
18323
+ var init_seating = __esm({
18324
+ "src/data/gtm-counsel/seating.ts"() {
18325
+ "use strict";
18326
+ init_packs();
18327
+ init_constraint_signals();
18328
+ ALL_FUNCTION = [
18329
+ "counsel_sales",
18330
+ "counsel_marketing",
18331
+ "counsel_revops",
18332
+ "counsel_cs"
18333
+ ];
18334
+ TREE_TIE_BREAKS = {
18335
+ plg: ["counsel_cs", "counsel_marketing", "counsel_revops", "counsel_sales"],
18336
+ smb_velocity: ["counsel_sales", "counsel_revops", "counsel_marketing", "counsel_cs"],
18337
+ mid_market: ["counsel_sales", "counsel_marketing", "counsel_revops", "counsel_cs"],
18338
+ enterprise: ["counsel_sales", "counsel_marketing", "counsel_revops", "counsel_cs"]
18339
+ };
18340
+ }
18341
+ });
18342
+
18343
+ // src/data/gtm-counsel/index.ts
18344
+ var gtm_counsel_exports = {};
18345
+ __export(gtm_counsel_exports, {
18346
+ COUNSEL_PACKS: () => COUNSEL_PACKS,
18347
+ PLAY_COUNSEL_HOOKS: () => PLAY_COUNSEL_HOOKS,
18348
+ PLAY_ROUTING: () => PLAY_ROUTING,
18349
+ ROLE_CARDS: () => ROLE_CARDS,
18350
+ buildCounselCatalogBlock: () => buildCounselCatalogBlock,
18351
+ buildOrgTreeContextLine: () => buildOrgTreeContextLine,
18352
+ buildRoundtableSeating: () => buildRoundtableSeating,
18353
+ counselIdsForPlay: () => counselIdsForPlay,
18354
+ extractGatingVitalFromDigest: () => extractGatingVitalFromDigest,
18355
+ formatCounselDetail: () => formatCounselDetail,
18356
+ formatRoleCardDetail: () => formatRoleCardDetail,
18357
+ getActiveOrgTree: () => getActiveOrgTree,
18358
+ getCounselPackById: () => getCounselPackById,
18359
+ getPlayRouting: () => getPlayRouting,
18360
+ getRoleCardById: () => getRoleCardById,
18361
+ graphForPlay: () => graphForPlay,
18362
+ inferConstraintOwnerPack: () => inferConstraintOwnerPack,
18363
+ inferConstraintOwnerPacks: () => inferConstraintOwnerPacks,
18364
+ inferRoundtableBias: () => inferRoundtableBias,
18365
+ isKnownConstraintSignal: () => isKnownConstraintSignal,
18366
+ listCounselPackIds: () => listCounselPackIds,
18367
+ listOrgTreeIds: () => listOrgTreeIds,
18368
+ listRoleCardIds: () => listRoleCardIds,
18369
+ ownersForConstraintSignal: () => ownersForConstraintSignal,
18370
+ playRoutingIntegrityIssues: () => playRoutingIntegrityIssues,
18371
+ primaryOwnerForPlay: () => primaryOwnerForPlay,
18372
+ resolveConstraint: () => resolveConstraint,
18373
+ resolveOrgTree: () => resolveOrgTree
18374
+ });
18375
+ function buildCounselCatalogBlock() {
18376
+ const lines = COUNSEL_PACKS.map((p) => p.catalog_line).join("\n");
18377
+ 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.
18378
+ ${lines}`;
18379
+ }
18380
+ function buildOrgTreeContextLine(profile) {
18381
+ const p = profile === void 0 ? loadProfile() : profile;
18382
+ const tree = resolveOrgTree(p);
18383
+ return `- Org tree: ${tree}`;
18384
+ }
18385
+ function getActiveOrgTree(profile) {
18386
+ const p = profile === void 0 ? loadProfile() : profile;
18387
+ return resolveOrgTree(p);
18388
+ }
18389
+ function formatCounselDetail(pack, tree) {
18390
+ return {
18391
+ counsel_id: pack.id,
18392
+ catalog_line: pack.catalog_line,
18393
+ vital_links: pack.vital_links,
18394
+ board_cut: pack.board_cut,
18395
+ operator_cut: pack.operator_cut,
18396
+ when_to_use: pack.when_to_use,
18397
+ kill_rules: pack.kill_rules,
18398
+ silent_brand_rule: pack.silent_brand_rule,
18399
+ role_card_ids: pack.role_card_ids,
18400
+ tree_weight: pack.tree_weight[tree],
18401
+ org_tree: tree,
18402
+ body: pack.body,
18403
+ 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."
18404
+ };
18405
+ }
18406
+ function formatRoleCardDetail(card, tree) {
18407
+ return {
18408
+ role_id: card.id,
18409
+ title: card.title,
18410
+ function: card.function,
18411
+ org_tree: tree,
18412
+ presence: card.presence[tree],
18413
+ owns: card.owns,
18414
+ does_not_own: card.does_not_own,
18415
+ success_metrics: card.success_metrics,
18416
+ typical_decisions: card.typical_decisions,
18417
+ data_they_trust: card.data_they_trust,
18418
+ consult_questions: card.consult_questions,
18419
+ failure_modes: card.failure_modes,
18420
+ tensions: card.tensions,
18421
+ play_hooks: card.play_hooks,
18422
+ signal_links: card.signal_links,
18423
+ // Transitional response field for clients built before the split.
18424
+ vital_play_hooks: [...card.play_hooks, ...card.signal_links],
18425
+ stress_notes: card.stress_notes,
18426
+ notice: presenceNotice(card.presence[tree]) + " Function-shaped owners only \u2014 do not invent headcount or named people."
18427
+ };
18428
+ }
18429
+ function presenceNotice(p) {
18430
+ if (p === "absent") return "This role is typically absent on this org tree \u2014 treat advice as thin. ";
18431
+ if (p === "thin") return "This role is thin on this org tree \u2014 keep recommendations light. ";
18432
+ if (p === "present") return "This role is present but not always core on this org tree. ";
18433
+ return "This role is core on this org tree. ";
18434
+ }
18435
+ var init_gtm_counsel = __esm({
18436
+ "src/data/gtm-counsel/index.ts"() {
18437
+ "use strict";
18438
+ init_profile();
18439
+ init_packs();
18440
+ init_resolve_tree();
18441
+ init_role_cards();
18442
+ init_seating();
18443
+ init_play_routing();
18444
+ init_constraint_signals();
18445
+ }
18446
+ });
18447
+
16782
18448
  // src/memory/play-outcomes.ts
16783
18449
  var play_outcomes_exports = {};
16784
18450
  __export(play_outcomes_exports, {
@@ -19540,10 +21206,10 @@ async function insertDirect(dataset) {
19540
21206
  health: null
19541
21207
  };
19542
21208
  }
19543
- function hashString(str2) {
21209
+ function hashString(str3) {
19544
21210
  let hash = 0;
19545
- for (let i = 0; i < str2.length; i++) {
19546
- const char = str2.charCodeAt(i);
21211
+ for (let i = 0; i < str3.length; i++) {
21212
+ const char = str3.charCodeAt(i);
19547
21213
  hash = (hash << 5) - hash + char | 0;
19548
21214
  }
19549
21215
  return Math.abs(hash);
@@ -21017,7 +22683,7 @@ var init_tool_schemas = __esm({
21017
22683
  },
21018
22684
  {
21019
22685
  name: "get_play_detail",
21020
- description: "Read the full definition of a playbook play by id: trigger condition, why it works, step-by-step actions, tools that help, and expected outcome. The system prompt lists only the play catalog \u2014 call this before recommending a play when the user needs the how, or when drafting workstream actions from a play.",
22686
+ description: "Read the full definition of a playbook play by id: trigger condition, why it works, step-by-step actions, counsel owner/contributors, prerequisites/exclusions, and structured exam. The system prompt lists only the play catalog \u2014 call this before recommending a play when the user needs the how, or when drafting workstream actions from a play.",
21021
22687
  parameters: {
21022
22688
  type: "object",
21023
22689
  properties: {
@@ -21045,6 +22711,41 @@ var init_tool_schemas = __esm({
21045
22711
  required: ["framework_id"]
21046
22712
  }
21047
22713
  },
22714
+ {
22715
+ name: "get_counsel_detail",
22716
+ 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.",
22717
+ parameters: {
22718
+ type: "object",
22719
+ properties: {
22720
+ counsel_id: {
22721
+ type: "string",
22722
+ maxLength: 40,
22723
+ description: "Exact id: counsel_sales, counsel_marketing, counsel_revops, counsel_exec, or counsel_cs."
22724
+ }
22725
+ },
22726
+ required: ["counsel_id"]
22727
+ }
22728
+ },
22729
+ {
22730
+ name: "get_role_card",
22731
+ 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.",
22732
+ parameters: {
22733
+ type: "object",
22734
+ properties: {
22735
+ role_id: {
22736
+ type: "string",
22737
+ maxLength: 40,
22738
+ description: "Exact role id listed on the pack you pulled via get_counsel_detail, e.g. 'ae', 'demand_gen', 'gtm_engineer'."
22739
+ },
22740
+ org_tree: {
22741
+ type: "string",
22742
+ enum: ["plg", "smb_velocity", "mid_market", "enterprise"],
22743
+ description: "Optional org tree override; defaults to profile sales_motion / mid_market."
22744
+ }
22745
+ },
22746
+ required: ["role_id"]
22747
+ }
22748
+ },
21048
22749
  {
21049
22750
  name: "get_session_brief",
21050
22751
  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.",
@@ -21171,7 +22872,7 @@ Optional packs via get_framework_detail: meddpicc, challenger, jtbd, porter, mck
21171
22872
  function frameworkIdsForPlay(playId) {
21172
22873
  return PLAY_FRAMEWORK_HOOKS[playId] ?? [];
21173
22874
  }
21174
- var PLAY_FRAMEWORK_HOOKS, SILENT, FRAMEWORK_PACKS;
22875
+ var PLAY_FRAMEWORK_HOOKS, SILENT2, FRAMEWORK_PACKS;
21175
22876
  var init_frameworks = __esm({
21176
22877
  "src/data/frameworks.ts"() {
21177
22878
  "use strict";
@@ -21180,9 +22881,15 @@ var init_frameworks = __esm({
21180
22881
  "unstick-pipeline": ["bottleneck", "okr_measurability", "owner_shape"],
21181
22882
  "fix-handoff-gap": ["bottleneck", "owner_shape", "aar"],
21182
22883
  "retarget-effort": ["bottleneck", "gtm_engineering"],
21183
- "multi-thread-deals": ["owner_shape", "heilmeier"]
22884
+ "multi-thread-deals": ["owner_shape", "heilmeier"],
22885
+ "harden-routing-sla": ["bottleneck", "gtm_engineering", "owner_shape", "aar"],
22886
+ "demand-quality-over-volume": ["bottleneck", "heilmeier", "okr_measurability"],
22887
+ "sales-cs-handoff-packet": ["owner_shape", "aar", "okr_measurability"],
22888
+ "renewal-early-warning": ["aar", "owner_shape", "okr_measurability"],
22889
+ "abm-orchestration": ["bottleneck", "heilmeier", "owner_shape"],
22890
+ "forecast-ritual-hygiene": ["bottleneck", "aar", "owner_shape", "okr_measurability"]
21184
22891
  };
21185
- SILENT = "Never name this framework (or DARPA, McKinsey, Goldratt, etc.) in customer-visible text. Apply the moves; keep the brand silent.";
22892
+ SILENT2 = "Never name this framework (or DARPA, McKinsey, Goldratt, etc.) in customer-visible text. Apply the moves; keep the brand silent.";
21186
22893
  FRAMEWORK_PACKS = [
21187
22894
  {
21188
22895
  id: "bottleneck",
@@ -21197,7 +22904,7 @@ var init_frameworks = __esm({
21197
22904
  "If the recommendation does not move the constraint, demote or drop it.",
21198
22905
  "Do not prescribe parallel fixes that starve the constraint of capacity."
21199
22906
  ],
21200
- silent_brand_rule: SILENT,
22907
+ silent_brand_rule: SILENT2,
21201
22908
  body: `BOTTLENECK (constraint first):
21202
22909
  - Respect layer gating: freshness trust \u2192 flow/drop movement \u2192 signal efficiency \u2192 thread resilience.
21203
22910
  - Name the single constraint vital or pipeline stage with its score and dollar label.
@@ -21214,7 +22921,7 @@ var init_frameworks = __esm({
21214
22921
  operator_cut: "Headline \u226420 words \u2192 2\u20133 non-overlapping drivers \u2192 owner-shaped so-what.",
21215
22922
  when_to_use: "Every executive-facing answer, finding, recap Story, summary_30k.",
21216
22923
  kill_rules: ["Never open with methodology or a full scorecard.", "Merge overlapping drivers."],
21217
- silent_brand_rule: SILENT,
22924
+ silent_brand_rule: SILENT2,
21218
22925
  body: `PACKAGING (answer-first):
21219
22926
  - Governing thought first (verdict + dollars).
21220
22927
  - Then the situation and complication that make the call true (support, not opener).
@@ -21231,7 +22938,7 @@ var init_frameworks = __esm({
21231
22938
  operator_cut: "Belief \u2192 evidence from vitals/metrics \u2192 keep, kill, or re-sequence the play.",
21232
22939
  when_to_use: "Strategy review, session distill calibrations, win/miss logging.",
21233
22940
  kill_rules: ["Do not log vanity narrative without a measured instrument.", "Calibrations supersede; do not pile contradictions."],
21234
- silent_brand_rule: SILENT,
22941
+ silent_brand_rule: SILENT2,
21235
22942
  body: `AFTER-ACTION:
21236
22943
  - BELIEF: what we expected to move (metric + range + check date).
21237
22944
  - EVIDENCE: what the instruments actually showed (baseline \u2192 reading).
@@ -21248,7 +22955,7 @@ var init_frameworks = __esm({
21248
22955
  operator_cut: "RevOps / AE lead / CS lead / Marketing ops \u2014 R does, A decides; avoid named individuals in plans.",
21249
22956
  when_to_use: "Workstream titles, handoff plans, play prescriptions.",
21250
22957
  kill_rules: ["No orphan actions without an accountable function.", "Do not invent headcount or reorgs."],
21251
- silent_brand_rule: SILENT,
22958
+ silent_brand_rule: SILENT2,
21252
22959
  body: `OWNER-SHAPE:
21253
22960
  - Name the function (RevOps, sales manager, CS lead, marketing ops) \u2014 not a person's name from the CRM.
21254
22961
  - Accountable decides; Responsible executes. Consulted optional; Inform via existing channels.
@@ -21264,7 +22971,7 @@ var init_frameworks = __esm({
21264
22971
  operator_cut: "Baseline, target range, check date, measured_by instrument \u2014 or demote to assumption.",
21265
22972
  when_to_use: "Strategist outcomes, Heilmeier exams, milestones.",
21266
22973
  kill_rules: ["No 'improve' without a number.", "No exam faster than sales-cycle physics."],
21267
- silent_brand_rule: SILENT,
22974
+ silent_brand_rule: SILENT2,
21268
22975
  body: `EXAMS (objective + key results):
21269
22976
  - Objective = precise end state in one line.
21270
22977
  - Key results = mid-term check + final exam: metric, baseline, target RANGE, check date, instrument.
@@ -21280,7 +22987,7 @@ var init_frameworks = __esm({
21280
22987
  operator_cut: "Kill restatements; demote unmeasurable ideas.",
21281
22988
  when_to_use: "Deep recommend, think before draft_strategy, strategist Stage C.",
21282
22989
  kill_rules: ["Fail newness, stake, or exams \u2192 do not recommend."],
21283
- silent_brand_rule: SILENT,
22990
+ silent_brand_rule: SILENT2,
21284
22991
  body: `IDEA GATE: end state, status quo + limit, newness, stake + cost of inaction, risks/payoffs, effort, time physics, mid+final exams. Kill on newness/stake/exams failure.`
21285
22992
  },
21286
22993
  {
@@ -21293,7 +23000,7 @@ var init_frameworks = __esm({
21293
23000
  operator_cut: "Same shape for findings and deep answers.",
21294
23001
  when_to_use: "All packaged analyst prose.",
21295
23002
  kill_rules: ["Methodology-first openers die."],
21296
- silent_brand_rule: SILENT,
23003
+ silent_brand_rule: SILENT2,
21297
23004
  body: `PYRAMID: HEADLINE FIRST \u2192 DRIVERS (non-overlapping) \u2192 SO-WHAT. Recall test. Altitude control.`
21298
23005
  },
21299
23006
  {
@@ -21306,7 +23013,7 @@ var init_frameworks = __esm({
21306
23013
  operator_cut: "Pair every cleanup with a mechanism and an instrument.",
21307
23014
  when_to_use: "When recommending action on tool-capable surfaces.",
21308
23015
  kill_rules: ["Skipping the rung below will not hold."],
21309
- silent_brand_rule: SILENT,
23016
+ silent_brand_rule: SILENT2,
21310
23017
  body: `GTM SYSTEMS: three rungs (foundation \u2192 modeling \u2192 activation); signals over lists; CRM is cheapest pipeline; every fix gets a mechanism; instrument what you change.`
21311
23018
  },
21312
23019
  {
@@ -21319,7 +23026,7 @@ var init_frameworks = __esm({
21319
23026
  operator_cut: "Map Metrics, Economic buyer, Decision criteria/process, Paper process, Identify pain, Champion, Competition \u2014 as observation, not CRM surgery.",
21320
23027
  when_to_use: "Thread-depth or stuck late-stage deals when the user asks how to qualify.",
21321
23028
  kill_rules: ["Do not rewrite the CRM; observe and recommend contacts/process gaps."],
21322
- silent_brand_rule: SILENT,
23029
+ silent_brand_rule: SILENT2,
21323
23030
  body: `DEAL QUALIFICATION LENS: use when single-threaded or stuck late-stage deals need a checklist. Prefer introducing a second contact and clarifying decision process over more outbound volume.`
21324
23031
  },
21325
23032
  {
@@ -21332,7 +23039,7 @@ var init_frameworks = __esm({
21332
23039
  operator_cut: "In think: teach with evidence, tailor to this pipeline, take control of next step \u2014 still stethoscope.",
21333
23040
  when_to_use: "Think channel when the user is soft on a weak story.",
21334
23041
  kill_rules: ["Do not turn into a pitch script or CRM sequence builder."],
21335
- silent_brand_rule: SILENT,
23042
+ silent_brand_rule: SILENT2,
21336
23043
  body: `TEACH-TAILOR-TAKE: reframe with verified numbers; tailor to segment/motion; land one owner-shaped next step.`
21337
23044
  },
21338
23045
  {
@@ -21345,7 +23052,7 @@ var init_frameworks = __esm({
21345
23052
  operator_cut: "When explaining ARR/NRR/vitals: what decision does this number unlock?",
21346
23053
  when_to_use: "Metric definitions / board deck framing.",
21347
23054
  kill_rules: ["Ornamental metrics without a decision job die."],
21348
- silent_brand_rule: SILENT,
23055
+ silent_brand_rule: SILENT2,
21349
23056
  body: `JOB OF THE METRIC: for each number, name the decision it serves. If none, cut it from the board package.`
21350
23057
  },
21351
23058
  {
@@ -21358,7 +23065,7 @@ var init_frameworks = __esm({
21358
23065
  operator_cut: "Do not let industry essays displace vital-sign evidence.",
21359
23066
  when_to_use: "Only when the user explicitly asks for competitive structure.",
21360
23067
  kill_rules: ["Never use as a substitute for gating vital diagnosis."],
21361
- silent_brand_rule: SILENT,
23068
+ silent_brand_rule: SILENT2,
21362
23069
  body: `INDUSTRY STRUCTURE: optional context. Prefer vital signs and dollars for recommendations.`
21363
23070
  },
21364
23071
  {
@@ -21371,7 +23078,7 @@ var init_frameworks = __esm({
21371
23078
  operator_cut: "Refuse surgery; observe handoff/ownership symptoms via vitals.",
21372
23079
  when_to_use: "Only if asked; redirect to owner-shape + bottleneck.",
21373
23080
  kill_rules: ["No reorg prescriptions."],
21374
- silent_brand_rule: SILENT,
23081
+ silent_brand_rule: SILENT2,
21375
23082
  body: `ORG ALIGNMENT: out of scope for CRM surgery. Point to function-shaped owners and the constraint vital instead.`
21376
23083
  },
21377
23084
  {
@@ -21384,7 +23091,7 @@ var init_frameworks = __esm({
21384
23091
  operator_cut: "Already covered by Stage A + bottleneck \u2014 do not duplicate jargon.",
21385
23092
  when_to_use: "Rarely; prefer existing grounding.",
21386
23093
  kill_rules: ["Do not invent a parallel loop vocabulary for the user."],
21387
- silent_brand_rule: SILENT,
23094
+ silent_brand_rule: SILENT2,
21388
23095
  body: `OBSERVE-ORIENT: prefer NTRP layer gating and Stage A hypothesis-first over a separate combat loop.`
21389
23096
  }
21390
23097
  ];
@@ -21549,8 +23256,8 @@ async function handleGetVitalSignDetail(input, ctx) {
21549
23256
  if (!vital) return { error: `Vital sign '${vitalSign}' not found` };
21550
23257
  const entitySummary = {};
21551
23258
  for (const detail of vital.entity_details) {
21552
- const issue = detail.issue;
21553
- const key = issue ?? "unclassified";
23259
+ const issue2 = detail.issue;
23260
+ const key = issue2 ?? "unclassified";
21554
23261
  entitySummary[key] = (entitySummary[key] ?? 0) + 1;
21555
23262
  }
21556
23263
  return {
@@ -21791,10 +23498,30 @@ async function handleGetPlayDetail(input) {
21791
23498
  steps: play.steps,
21792
23499
  tools_that_help: play.tools_that_help,
21793
23500
  expected_outcome: play.expected_outcome,
23501
+ exam: play.exam ?? null,
21794
23502
  source: play.source ?? "seed",
21795
23503
  local_track_record: trackRecord,
21796
23504
  framework_ids: frameworkIdsForPlay2(play.id)
21797
23505
  };
23506
+ try {
23507
+ const {
23508
+ counselIdsForPlay: counselIdsForPlay2,
23509
+ getPlayRouting: getPlayRouting2,
23510
+ ownersForConstraintSignal: ownersForConstraintSignal2
23511
+ } = await Promise.resolve().then(() => (init_gtm_counsel(), gtm_counsel_exports));
23512
+ const routing = getPlayRouting2(play.id) ?? play.routing;
23513
+ const fallbackSignal = play.trigger_vital_sign ?? play.trigger_metric ?? "";
23514
+ const fallbackOwner = fallbackSignal ? ownersForConstraintSignal2(fallbackSignal, play.trigger_condition)[0] ?? null : null;
23515
+ detail.primary_owner = routing?.primary_owner ?? fallbackOwner;
23516
+ detail.counsel_ids = routing?.counsel_ids ?? (fallbackOwner ? [fallbackOwner] : counselIdsForPlay2(play.id));
23517
+ detail.constraint_signals = routing?.constraint_signals ?? [play.trigger_vital_sign, play.trigger_metric].filter(Boolean);
23518
+ detail.graph = routing?.graph ?? null;
23519
+ } catch {
23520
+ detail.primary_owner = null;
23521
+ detail.counsel_ids = [];
23522
+ detail.constraint_signals = [];
23523
+ detail.graph = null;
23524
+ }
21798
23525
  if (play.source === "learned") {
21799
23526
  detail.security_notice = UNTRUSTED_CONTENT_NOTICE;
21800
23527
  detail.why = wrapUntrustedContent(String(play.why ?? ""));
@@ -21828,6 +23555,44 @@ async function handleGetFrameworkDetail(input) {
21828
23555
  notice: "Apply the moves in body/board_cut/operator_cut. Never name internal brand labels to the user."
21829
23556
  };
21830
23557
  }
23558
+ async function handleGetCounselDetail(input) {
23559
+ const counselId = typeof input.counsel_id === "string" ? input.counsel_id.trim() : "";
23560
+ const {
23561
+ getCounselPackById: getCounselPackById2,
23562
+ listCounselPackIds: listCounselPackIds2,
23563
+ formatCounselDetail: formatCounselDetail2,
23564
+ getActiveOrgTree: getActiveOrgTree2
23565
+ } = await Promise.resolve().then(() => (init_gtm_counsel(), gtm_counsel_exports));
23566
+ const pack = counselId ? getCounselPackById2(counselId) : void 0;
23567
+ if (!pack) {
23568
+ return {
23569
+ error: `Unknown counsel id '${counselId}'.`,
23570
+ valid_counsel_ids: listCounselPackIds2()
23571
+ };
23572
+ }
23573
+ return formatCounselDetail2(pack, getActiveOrgTree2());
23574
+ }
23575
+ async function handleGetRoleCard(input) {
23576
+ const roleId = typeof input.role_id === "string" ? input.role_id.trim() : "";
23577
+ const {
23578
+ getRoleCardById: getRoleCardById2,
23579
+ listRoleCardIds: listRoleCardIds2,
23580
+ formatRoleCardDetail: formatRoleCardDetail2,
23581
+ getActiveOrgTree: getActiveOrgTree2,
23582
+ listOrgTreeIds: listOrgTreeIds2
23583
+ } = await Promise.resolve().then(() => (init_gtm_counsel(), gtm_counsel_exports));
23584
+ const card = roleId ? getRoleCardById2(roleId) : void 0;
23585
+ if (!card) {
23586
+ return {
23587
+ error: `Unknown role id '${roleId}'.`,
23588
+ valid_role_ids: listRoleCardIds2()
23589
+ };
23590
+ }
23591
+ const treeArg = typeof input.org_tree === "string" ? input.org_tree.trim() : "";
23592
+ const trees = listOrgTreeIds2();
23593
+ const tree = trees.includes(treeArg) ? treeArg : getActiveOrgTree2();
23594
+ return formatRoleCardDetail2(card, tree);
23595
+ }
21831
23596
  async function handleGetRevenueMetrics(_input, ctx) {
21832
23597
  if (!ctx.metrics || ctx.metrics.length === 0) {
21833
23598
  const { computeFullMetrics: computeFullMetrics2 } = await Promise.resolve().then(() => (init_compute(), compute_exports));
@@ -22123,6 +23888,8 @@ var init_tool_handlers = __esm({
22123
23888
  query_entity_counts: (input, _) => handleQueryEntityCounts(input),
22124
23889
  get_play_detail: (input, _) => handleGetPlayDetail(input),
22125
23890
  get_framework_detail: (input, _) => handleGetFrameworkDetail(input),
23891
+ get_counsel_detail: (input, _) => handleGetCounselDetail(input),
23892
+ get_role_card: (input, _) => handleGetRoleCard(input),
22126
23893
  get_session_brief: (input, _) => handleGetSessionBrief(input),
22127
23894
  get_revenue_metrics: handleGetRevenueMetrics,
22128
23895
  get_revenue_metrics_timeseries: (input, _) => handleGetRevenueMetricsTimeseries(input),
@@ -22422,6 +24189,8 @@ ${buildPlaybookBlock()}
22422
24189
 
22423
24190
  ${buildFrameworkCatalogBlock()}
22424
24191
 
24192
+ ${buildCounselCatalogBlock()}
24193
+
22425
24194
  MEASURABILITY CONTRACT (non-negotiable \u2014 this is what separates you from a slide deck):
22426
24195
  - Treat the objective as the end state; treat expected outcomes and leading indicators as key results (mid-term and final exams).
22427
24196
  - Every expected outcome and leading indicator needs: a metric, the current baseline copied from your verified grounding work, a target RANGE, a check date, and the named instrument that will measure it.
@@ -22433,7 +24202,7 @@ MEASURABILITY CONTRACT (non-negotiable \u2014 this is what separates you from a
22433
24202
  SAFETY & EVIDENCE (non-negotiable):
22434
24203
  ${SAFETY_BLOCK}
22435
24204
 
22436
- You will work in three stages. Follow the stage instructions in each message. Use tools deliberately \u2014 each call should answer a specific question you need for the plan.`;
24205
+ You will work in staged passes (ground \u2192 backcast \u2192 roundtable \u2192 stress). Follow the stage instructions in each message. Use tools deliberately \u2014 each call should answer a specific question you need for the plan.`;
22437
24206
  }
22438
24207
  function buildGroundingMessage(input) {
22439
24208
  const sections = [];
@@ -22443,6 +24212,7 @@ function buildGroundingMessage(input) {
22443
24212
  Establish verified reality before any planning. Work hypothesis-first, like an engagement manager on day one: form your top candidate explanations for what stands between today and the objective, then use tools to confirm or kill each one \u2014 don't boil the ocean.
22444
24213
  1. Current state: which vital signs / metrics are worst, what are the exact scores and dollar values, which segments concentrate the problem?
22445
24214
  2. CONSTRAINT: name the single gating vital (layer order) or stage where deals die \u2014 with score and dollar label. This is the bottleneck the plan must move first.
24215
+ Keep the five-vital health gate separate from the user's objective constraint. They may disagree; report both with evidence instead of silently replacing one.
22446
24216
  3. What is already in flight (active strategies below, if any), what has worked before (wins), and what the local play track record says.
22447
24217
  4. What are the binding constraints: data gaps that limit measurability, sales-cycle length that bounds verification speed, capacity signals?
22448
24218
  Rank the problems you verify by dollars at stake \xD7 confidence in the read \xD7 speed to impact \u2014 that ranking becomes the spine of the plan. Non-constraint problems wait.
@@ -22465,6 +24235,18 @@ ${input.constraintsNote}`);
22465
24235
  }
22466
24236
  sections.push(`When you have verified what you need (aim for focused tool use, not exhaustive), respond with a REALITY DIGEST as strict JSON \u2014 no markdown fences, no prose before or after:
22467
24237
  {
24238
+ "health_gate": {
24239
+ "signal": "one of freshness | flow_rate | drop_rate | signal_to_noise | thread_depth",
24240
+ "dollar_value": 0,
24241
+ "cause": "verified cause, not a recommendation",
24242
+ "evidence": "exact score / dollar / entity evidence"
24243
+ },
24244
+ "objective_constraint": {
24245
+ "signal": "verified vital, SaaS metric, or stage tied to the objective",
24246
+ "dollar_value": 0,
24247
+ "cause": "why this blocks the stated objective",
24248
+ "evidence": "exact metric / stage / tool evidence"
24249
+ },
22468
24250
  "current_state": ["One line per verified fact you will build on, each with its exact number"],
22469
24251
  "worst_problems_ordered": ["Problem + number + dollar value, in layer-dependency order"],
22470
24252
  "constraints": ["Binding constraints you verified or were given"],
@@ -22479,15 +24261,70 @@ function buildBackcastMessage(objective) {
22479
24261
 
22480
24262
  Reason in reverse: what must be true immediately before the objective holds? What must be true before that? Chain back to today, then forward-order the chain into 2-5 workstreams. For each: sequence rationale (what it unblocks), linked plays, owner-ready actions, effort hours, dated milestones with verification thresholds, tangible deliverables, an expected outcome RANGE anchored to a baseline from your reality digest, leading indicators that move earlier than the outcome, and a pre-decided contingency.
22481
24263
 
24264
+ Do not treat this as sales-physics-only. If the digest implicates marketing creation or handoff, split quality/acceptance from routing leak before prescribing demand spend. If instruments look untrustworthy, sequence RevOps trust before sales theater. If NRR/GRR/churn is the dollar, include CS. Still one constraint \u2014 do not parallelize.
24265
+
22482
24266
  You may make a small number of additional tool calls to verify a specific baseline you are missing \u2014 but do not re-investigate broadly.
22483
24267
 
22484
24268
  Respond with the full plan as strict JSON matching this schema \u2014 no markdown fences, no prose before or after:
22485
24269
  ${STRATEGIST_PLAN_SCHEMA_BLOCK}`;
22486
24270
  }
22487
- function buildStressTestMessage() {
24271
+ function buildRoundtableMessage(input) {
24272
+ const bias = input.biasNotes.length > 0 ? `Bias notes:
24273
+ - ${input.biasNotes.join("\n- ")}
24274
+
24275
+ ` : "";
24276
+ const owner = input.constraintOwner ? `Constraint-owning pack (must stay full seat): ${input.constraintOwner}
24277
+
24278
+ ` : "";
24279
+ const constraint = input.constraint ? `CONSTRAINT CHANNELS:
24280
+ - Health gate: ${input.constraint.health_gate ? String(input.constraint.health_gate.id) : "not verified"} \u2192 ${input.constraint.health_gate_owners.join(", ") || "no mapped owner"}
24281
+ - Objective constraint: ${input.constraint.objective_constraint ? String(input.constraint.objective_constraint.id) : "not verified"} \u2192 ${input.constraint.objective_owners.join(", ") || "no mapped owner"}
24282
+ - If these differ, preserve both seats and have exec sequence the objective behind the health gate unless evidence supports another order.
24283
+
24284
+ ` : "";
24285
+ return `STAGE R \u2014 ROUNDTABLE. Org tree: ${input.tree}. Before stress-testing, seat GTM counsel packs and challenge the draft.
24286
+
24287
+ ${owner}${constraint}${bias}EXEC MERGE SEAT: counsel_exec (full; call get_counsel_detail; output only in top-level exec).
24288
+ FUNCTION SEATS (weight full = deep challenge; secondary/risk_note = short risk note only):
24289
+ ${input.seatsBlock}
24290
+
24291
+ 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.
24292
+
24293
+ DRAFT PLAN JSON:
24294
+ ${input.draftPlanJson}
24295
+
24296
+ Respond with STRICT JSON only (no prose, no fences):
24297
+ {
24298
+ "seats": [
24299
+ {
24300
+ "pack": "counsel_sales",
24301
+ "stance": "support" | "challenge" | "abstain",
24302
+ "constraint_agree": true,
24303
+ "challenge": "\u226440 words \u2014 what the draft gets wrong or misses; empty only if support + risk or exec pre-mortem",
24304
+ "missing_exam": "metric + date + instrument (or empty string)",
24305
+ "cross_functional_risk": "\u226430 words",
24306
+ "role_cards_used": ["ae"]
24307
+ }
24308
+ ],
24309
+ "exec": {
24310
+ "governing_constraint": "one constraint with dollar label",
24311
+ "sequence": ["play or workstream order"],
24312
+ "drop": ["non-constraint theater to kill"],
24313
+ "merge_verdict": "\u226450 words",
24314
+ "pre_mortem": "if all seats support: the one most likely death cause; else optional"
24315
+ }
24316
+ }
24317
+
24318
+ 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.`;
24319
+ }
24320
+ function buildStressTestMessage(roundtableDigest) {
24321
+ 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.
24322
+ ${roundtableDigest}
24323
+
24324
+ ` : "";
22488
24325
  return `STAGE C \u2014 STRESS TEST. Now attack your own draft the way a skeptical COO would. Audit it against these checks and revise:
22489
24326
 
22490
- 1. PRE-MORTEM: it is the first check date and the plan has visibly failed \u2014 write the one most likely cause of death, then make sure the plan already defends against it (a constraint, a contingency trigger, or a re-sequence). If it doesn't, fix the plan, not the story.
24327
+ ${roundtableBlock}1. PRE-MORTEM: it is the first check date and the plan has visibly failed \u2014 write the one most likely cause of death, then make sure the plan already defends against it (a constraint, a contingency trigger, or a re-sequence). If it doesn't, fix the plan, not the story.
22491
24328
  2. CAPACITY MATH: sum the effort_hours. Does it fit the team implied by the company context and constraints? If overcommitted, cut or re-sequence \u2014 do not shrink the estimates to make it fit.
22492
24329
  3. MEASURABILITY: for every expected_outcome and leading indicator \u2014 is the baseline a real number from your reality digest? Does measured_by name an instrument that exists given the data gaps? Anything unmeasurable gets excluded from targets and recorded in assumptions.
22493
24330
  4. TIMELINE SANITY: can each check_date actually show evidence by then, given the sales cycle and how the metric updates? Fix dates that are faster than physics.
@@ -22554,6 +24391,7 @@ var init_strategist_prompt = __esm({
22554
24391
  "use strict";
22555
24392
  init_prompt_parts();
22556
24393
  init_frameworks();
24394
+ init_gtm_counsel();
22557
24395
  init_prompt();
22558
24396
  STRATEGIST_PLAN_SCHEMA_BLOCK = `{
22559
24397
  "title": "Short plan name, e.g. 'Q4 Pipeline Recovery'",
@@ -22954,7 +24792,7 @@ function validateStrategistPlan(raw, opts) {
22954
24792
  }
22955
24793
  function buildGroundedFallbackPlan(input) {
22956
24794
  const today = parseIsoDate(input.todayIso) ?? /* @__PURE__ */ new Date();
22957
- const triggered = matchTriggeredPlays(input.vitals, LAYERS);
24795
+ const triggered = matchTriggeredPlays(input.vitals, LAYERS, { tree: input.tree });
22958
24796
  const issues = [
22959
24797
  "LLM plan JSON invalid \u2014 using grounded fallback from triggered plays and live vitals"
22960
24798
  ];
@@ -23112,6 +24950,263 @@ var init_strategist_validate = __esm({
23112
24950
  }
23113
24951
  });
23114
24952
 
24953
+ // src/ai/roundtable-validate.ts
24954
+ function str2(value) {
24955
+ return typeof value === "string" ? value.trim() : "";
24956
+ }
24957
+ function strArray2(value) {
24958
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
24959
+ }
24960
+ function words(value) {
24961
+ return value.trim() ? value.trim().split(/\s+/).length : 0;
24962
+ }
24963
+ function issue(issues, code, message, severity = "blocking") {
24964
+ issues.push({ code, severity, message });
24965
+ }
24966
+ function parseSeat(raw, issues, index) {
24967
+ if (!raw || typeof raw !== "object") {
24968
+ issue(issues, "seat_shape", `Seat ${index + 1} is not an object.`);
24969
+ return null;
24970
+ }
24971
+ const obj = raw;
24972
+ const pack = str2(obj.pack);
24973
+ if (pack === "counsel_exec") {
24974
+ issue(issues, "exec_in_seats", "counsel_exec must appear only in the top-level exec object.");
24975
+ return null;
24976
+ }
24977
+ if (!FUNCTION_PACKS.has(pack)) {
24978
+ issue(issues, "unknown_pack", `Seat ${index + 1} has unknown pack '${pack || "(empty)"}'.`);
24979
+ return null;
24980
+ }
24981
+ const stance = str2(obj.stance);
24982
+ if (!STANCES.has(stance)) {
24983
+ issue(issues, "invalid_stance", `${pack} has invalid stance '${stance || "(empty)"}'.`);
24984
+ }
24985
+ if (typeof obj.constraint_agree !== "boolean") {
24986
+ issue(issues, "constraint_agree", `${pack} must set constraint_agree to true or false.`);
24987
+ }
24988
+ const challenge = str2(obj.challenge);
24989
+ const risk = str2(obj.cross_functional_risk);
24990
+ const missingExam = str2(obj.missing_exam);
24991
+ if (words(challenge) > 40) {
24992
+ issue(issues, "challenge_length", `${pack} challenge exceeds 40 words.`);
24993
+ }
24994
+ if (words(risk) > 30) {
24995
+ issue(issues, "risk_length", `${pack} cross_functional_risk exceeds 30 words.`);
24996
+ }
24997
+ const roleCards = strArray2(obj.role_cards_used);
24998
+ const allowed = new Set(getCounselPackById(pack)?.role_card_ids ?? []);
24999
+ const validRoles = [];
25000
+ for (const roleId of roleCards) {
25001
+ if (!getRoleCardById(roleId) || !allowed.has(roleId)) {
25002
+ issue(
25003
+ issues,
25004
+ "role_not_in_pack",
25005
+ `${pack} used role card '${roleId}' that is not listed on that pack.`
25006
+ );
25007
+ continue;
25008
+ }
25009
+ validRoles.push(roleId);
25010
+ }
25011
+ return {
25012
+ pack,
25013
+ stance: STANCES.has(stance) ? stance : "abstain",
25014
+ constraint_agree: obj.constraint_agree === true,
25015
+ challenge,
25016
+ missing_exam: missingExam,
25017
+ cross_functional_risk: risk,
25018
+ role_cards_used: validRoles
25019
+ };
25020
+ }
25021
+ function parseExec(raw, issues) {
25022
+ if (!raw || typeof raw !== "object") {
25023
+ issue(issues, "missing_exec", "Roundtable requires a top-level exec object.");
25024
+ return null;
25025
+ }
25026
+ const obj = raw;
25027
+ const governing = str2(obj.governing_constraint);
25028
+ const verdict = str2(obj.merge_verdict);
25029
+ if (!governing) issue(issues, "exec_constraint", "exec.governing_constraint is required.");
25030
+ if (!verdict) issue(issues, "exec_verdict", "exec.merge_verdict is required.");
25031
+ if (words(verdict) > 50) {
25032
+ issue(issues, "exec_verdict_length", "exec.merge_verdict exceeds 50 words.");
25033
+ }
25034
+ return {
25035
+ governing_constraint: governing,
25036
+ sequence: strArray2(obj.sequence),
25037
+ drop: strArray2(obj.drop),
25038
+ merge_verdict: verdict,
25039
+ pre_mortem: str2(obj.pre_mortem)
25040
+ };
25041
+ }
25042
+ function validateRoundtableResult(raw, options) {
25043
+ const issues = [];
25044
+ if (!raw || typeof raw !== "object") {
25045
+ return {
25046
+ result: null,
25047
+ issues: [{ code: "roundtable_shape", severity: "blocking", message: "Roundtable output is not a JSON object." }],
25048
+ blocking: true
25049
+ };
25050
+ }
25051
+ const obj = raw;
25052
+ if (!Array.isArray(obj.seats)) {
25053
+ issue(issues, "missing_seats", "Roundtable requires a seats array.");
25054
+ }
25055
+ const seats = (Array.isArray(obj.seats) ? obj.seats : []).map((seat, index) => parseSeat(seat, issues, index)).filter((seat) => seat != null);
25056
+ const exec = parseExec(obj.exec, issues);
25057
+ const seen = /* @__PURE__ */ new Set();
25058
+ for (const seat of seats) {
25059
+ if (seen.has(seat.pack)) {
25060
+ issue(issues, "duplicate_seat", `Roundtable contains duplicate seat ${seat.pack}.`);
25061
+ }
25062
+ seen.add(seat.pack);
25063
+ }
25064
+ const expected = options.seating.seats.filter((seat) => seat.pack_id !== "counsel_exec").map((seat) => seat.pack_id);
25065
+ for (const pack of expected) {
25066
+ if (!seen.has(pack)) issue(issues, "missing_seat", `Roundtable omitted seated pack ${pack}.`);
25067
+ }
25068
+ for (const pack of seen) {
25069
+ if (!expected.includes(pack)) issue(issues, "unexpected_seat", `Roundtable emitted unseated pack ${pack}.`);
25070
+ }
25071
+ const pulls = new Set(options.counselPulls ?? []);
25072
+ if (options.requireCounselPulls !== false) {
25073
+ const fullPacks = options.seating.seats.filter((seat) => seat.weight === "full").map((seat) => seat.pack_id);
25074
+ for (const pack of fullPacks) {
25075
+ if (!pulls.has(pack)) {
25076
+ issue(issues, "missing_counsel_pull", `Full seat ${pack} did not call get_counsel_detail.`);
25077
+ }
25078
+ }
25079
+ }
25080
+ if (exec) {
25081
+ const allSupport = seats.length > 0 && seats.every((seat) => seat.stance === "support");
25082
+ if (allSupport && !exec.pre_mortem) {
25083
+ issue(issues, "missing_pre_mortem", "All-support roundtables require exec.pre_mortem.");
25084
+ }
25085
+ for (const seat of seats) {
25086
+ if (!seat.challenge && !(seat.stance === "support" && (Boolean(seat.cross_functional_risk) || Boolean(exec.pre_mortem)))) {
25087
+ issue(
25088
+ issues,
25089
+ "empty_challenge",
25090
+ `${seat.pack} may leave challenge empty only with support plus risk or exec pre-mortem.`
25091
+ );
25092
+ }
25093
+ if (seat.stance === "abstain" && !seat.cross_functional_risk) {
25094
+ issue(issues, "abstain_without_risk", `${seat.pack} abstained without a risk note.`);
25095
+ }
25096
+ }
25097
+ }
25098
+ for (const owner of options.constraintOwners) {
25099
+ const seat = seats.find((candidate) => candidate.pack === owner);
25100
+ if (!seat) continue;
25101
+ if (seat.stance === "abstain" && !seat.cross_functional_risk) {
25102
+ issue(issues, "owner_abstain", `Constraint owner ${owner} abstained without a risk note.`);
25103
+ }
25104
+ }
25105
+ const blocking = issues.some((entry) => entry.severity === "blocking");
25106
+ return {
25107
+ result: exec ? { seats, exec } : null,
25108
+ issues,
25109
+ blocking
25110
+ };
25111
+ }
25112
+ function parseAndValidateRoundtable(text, options) {
25113
+ return validateRoundtableResult(parseJsonObjectFromText(text), options);
25114
+ }
25115
+ function formatRoundtableDigestFromResult(result) {
25116
+ const lines = result.seats.map(
25117
+ (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}` : ""}`
25118
+ );
25119
+ lines.push(`EXEC governing: ${result.exec.governing_constraint || "(none)"}`);
25120
+ if (result.exec.sequence.length) lines.push(`EXEC sequence: ${result.exec.sequence.join(" \u2192 ")}`);
25121
+ if (result.exec.drop.length) lines.push(`EXEC drop: ${result.exec.drop.join("; ")}`);
25122
+ if (result.exec.merge_verdict) lines.push(`EXEC verdict: ${result.exec.merge_verdict}`);
25123
+ if (result.exec.pre_mortem) lines.push(`EXEC pre-mortem: ${result.exec.pre_mortem}`);
25124
+ return lines.join("\n");
25125
+ }
25126
+ function buildRoundtableRetryMessage(validation, seating) {
25127
+ const required = seating.seats.filter((seat) => seat.pack_id !== "counsel_exec").map((seat) => seat.pack_id).join(", ");
25128
+ const failures = validation.issues.filter((entry) => entry.severity === "blocking").map((entry) => `${entry.code}: ${entry.message}`).join("\n- ");
25129
+ return `Roundtable JSON did not validate.
25130
+ - ${failures || "roundtable_shape: unusable JSON"}
25131
+ Required function seats: ${required}.
25132
+ Respond with ONLY corrected roundtable JSON. counsel_exec belongs only in top-level exec. All-support requires exec.pre_mortem.`;
25133
+ }
25134
+ function normalizedPlanText(plan) {
25135
+ return JSON.stringify(plan).toLowerCase();
25136
+ }
25137
+ function significantTokens(value) {
25138
+ return value.toLowerCase().split(/[^a-z0-9_]+/).filter((token) => token.length >= 4).filter((token) => !["that", "with", "from", "this", "when", "must", "risk"].includes(token));
25139
+ }
25140
+ function checkRoundtableReflection(input) {
25141
+ const issues = [];
25142
+ const draft = normalizedPlanText(input.draftPlan);
25143
+ const final = normalizedPlanText(input.finalPlan);
25144
+ for (const dropped of input.roundtable.exec.drop) {
25145
+ const needle = dropped.toLowerCase();
25146
+ if (needle.length >= 4 && draft.includes(needle) && final.includes(needle)) {
25147
+ issue(
25148
+ issues,
25149
+ "roundtable_drop_ignored",
25150
+ `Final plan still contains exec drop '${dropped}'.`,
25151
+ "advisory"
25152
+ );
25153
+ }
25154
+ }
25155
+ for (const seat of input.roundtable.seats) {
25156
+ if (seat.missing_exam) {
25157
+ const tokens = significantTokens(seat.missing_exam);
25158
+ if (tokens.length > 0 && !tokens.some((token) => final.includes(token))) {
25159
+ issue(
25160
+ issues,
25161
+ "roundtable_exam_missing",
25162
+ `${seat.pack} missing exam is not visible in the final plan.`,
25163
+ "advisory"
25164
+ );
25165
+ }
25166
+ }
25167
+ if (seat.stance === "challenge" && seat.challenge && draft === final) {
25168
+ issue(
25169
+ issues,
25170
+ "roundtable_unaddressed",
25171
+ `${seat.pack} challenged the draft but the final plan is unchanged.`,
25172
+ "advisory"
25173
+ );
25174
+ }
25175
+ }
25176
+ const allSupport = input.roundtable.seats.length > 0 && input.roundtable.seats.every((seat) => seat.stance === "support");
25177
+ if (allSupport && input.roundtable.exec.pre_mortem) {
25178
+ const defensiveText = JSON.stringify({
25179
+ risks: input.finalPlan.risks,
25180
+ assumptions: input.finalPlan.assumptions,
25181
+ contingencies: input.finalPlan.workstreams.map((ws) => ws.contingency)
25182
+ }).toLowerCase();
25183
+ const tokens = significantTokens(input.roundtable.exec.pre_mortem);
25184
+ if (tokens.length > 0 && !tokens.some((token) => defensiveText.includes(token))) {
25185
+ issue(
25186
+ issues,
25187
+ "roundtable_premortem_missing",
25188
+ "Final risks and contingencies do not reflect exec.pre_mortem.",
25189
+ "advisory"
25190
+ );
25191
+ }
25192
+ }
25193
+ return { issues, blocking: false };
25194
+ }
25195
+ var FUNCTION_PACKS, STANCES;
25196
+ var init_roundtable_validate = __esm({
25197
+ "src/ai/roundtable-validate.ts"() {
25198
+ "use strict";
25199
+ init_gtm_counsel();
25200
+ init_strategist_validate();
25201
+ FUNCTION_PACKS = new Set(
25202
+ listCounselPackIds().filter(
25203
+ (id) => id !== "counsel_exec"
25204
+ )
25205
+ );
25206
+ STANCES = /* @__PURE__ */ new Set(["support", "challenge", "abstain"]);
25207
+ }
25208
+ });
25209
+
23115
25210
  // src/ai/strategist-rubric.ts
23116
25211
  function numbersMatch2(a, b) {
23117
25212
  if (a === b) return true;
@@ -23278,6 +25373,65 @@ function scoreConsultantPlan(plan, opts) {
23278
25373
  fix: "Copy baselines from the health snapshot exactly."
23279
25374
  });
23280
25375
  }
25376
+ const playPositions = /* @__PURE__ */ new Map();
25377
+ const groups = /* @__PURE__ */ new Map();
25378
+ for (const [workstream, ws] of plan.workstreams.entries()) {
25379
+ for (const [position, playId] of ws.play_ids.entries()) {
25380
+ playPositions.set(playId, { workstream, position });
25381
+ const group = (getPlayRouting(playId) ?? getPlaybook().find((play) => play.id === playId)?.routing)?.graph?.exclusive_group;
25382
+ if (group) {
25383
+ const workstreams = groups.get(group) ?? /* @__PURE__ */ new Set();
25384
+ workstreams.add(workstream);
25385
+ groups.set(group, workstreams);
25386
+ }
25387
+ }
25388
+ }
25389
+ for (const [group, workstreams] of groups) {
25390
+ if (workstreams.size > 1) {
25391
+ gaps.push({
25392
+ code: "play_sequence",
25393
+ severity: "advisory",
25394
+ note: `Exclusive play group '${group}' is split across parallel workstreams.`,
25395
+ fix: "Keep the diagnostic play primary and express the later play as a gated next step."
25396
+ });
25397
+ }
25398
+ }
25399
+ for (const [playId, current] of playPositions) {
25400
+ const routing = getPlayRouting(playId) ?? getPlaybook().find((play) => play.id === playId)?.routing;
25401
+ for (const requirement of routing?.graph?.prerequisites ?? []) {
25402
+ const prior = playPositions.get(requirement.play_id);
25403
+ if (!prior || prior.workstream > current.workstream || prior.workstream === current.workstream && prior.position >= current.position) {
25404
+ gaps.push({
25405
+ code: "play_sequence",
25406
+ severity: "advisory",
25407
+ note: `${playId} appears before prerequisite ${requirement.play_id}.`,
25408
+ fix: requirement.reason
25409
+ });
25410
+ }
25411
+ }
25412
+ const planText = JSON.stringify(plan).toLowerCase();
25413
+ if (opts.orgTree && routing?.graph?.tree_exclude?.includes(opts.orgTree) && !(routing.graph.required_evidence?.includes("named_account_process") && /named[- ]account|named list/.test(planText))) {
25414
+ gaps.push({
25415
+ code: "play_sequence",
25416
+ severity: "advisory",
25417
+ note: `${playId} is excluded on the ${opts.orgTree} tree without its required evidence.`,
25418
+ fix: "Remove the play or state and verify the motion-specific evidence that makes it applicable."
25419
+ });
25420
+ }
25421
+ for (const block of routing?.graph?.blocked_by ?? []) {
25422
+ const vital = opts.snapshot.aggregate.vital_signs.find(
25423
+ (reading) => reading.vital_sign === block.signal
25424
+ );
25425
+ if (vital?.status === block.status) {
25426
+ gaps.push({
25427
+ code: "play_sequence",
25428
+ severity: "advisory",
25429
+ note: `${playId} is blocked while ${String(block.signal)} is ${block.status}.`,
25430
+ fix: block.reason
25431
+ });
25432
+ }
25433
+ }
25434
+ }
23281
25435
  const blocking = gaps.filter((g) => g.severity === "blocking");
23282
25436
  return { pass: blocking.length === 0, gaps, blocking };
23283
25437
  }
@@ -23361,6 +25515,7 @@ var init_strategist_rubric = __esm({
23361
25515
  "src/ai/strategist-rubric.ts"() {
23362
25516
  "use strict";
23363
25517
  init_playbook();
25518
+ init_play_routing();
23364
25519
  init_strategist_validate();
23365
25520
  init_formatters();
23366
25521
  OWNER_TOKENS = [
@@ -23398,7 +25553,9 @@ var init_strategist_rubric = __esm({
23398
25553
  "alternatives_killed",
23399
25554
  "thin_evidence",
23400
25555
  "invented_numbers",
23401
- "objective_drift"
25556
+ "objective_drift",
25557
+ "roundtable_invalid",
25558
+ "play_sequence"
23402
25559
  ];
23403
25560
  }
23404
25561
  });
@@ -23489,7 +25646,7 @@ async function* strategistPlanSession(options) {
23489
25646
  lastMeta = result.meta;
23490
25647
  return result.response;
23491
25648
  };
23492
- async function* runStage(surface, maxRounds, budgetNudge, requireJson = false) {
25649
+ async function* runStage(surface, maxRounds, budgetNudge, requireJson = false, onToolCall) {
23493
25650
  for (let round = 0; round < maxRounds; round++) {
23494
25651
  const response = await callLlm(surface, true);
23495
25652
  if (response.tool_calls.length === 0) {
@@ -23507,6 +25664,7 @@ async function* strategistPlanSession(options) {
23507
25664
  yield { type: "thinking", text: response.text.trim().slice(0, 200) };
23508
25665
  }
23509
25666
  for (const tc of response.tool_calls) {
25667
+ onToolCall?.(tc.name, tc.arguments ?? {});
23510
25668
  yield { type: "tool_call", name: tc.name };
23511
25669
  const result = await executeToolCall(tc.name, tc.arguments ?? {}, toolCtx, {
23512
25670
  allowedTools,
@@ -23533,7 +25691,8 @@ async function* strategistPlanSession(options) {
23533
25691
  vitals: vitalsForFallback,
23534
25692
  todayIso,
23535
25693
  gatingVitalSign: options.computeResult.aggregate.gating_vital_sign,
23536
- totalValueAtRisk: options.computeResult.aggregate.total_value_at_risk
25694
+ totalValueAtRisk: options.computeResult.aggregate.total_value_at_risk,
25695
+ tree: getActiveOrgTree(loadProfile())
23537
25696
  });
23538
25697
  }
23539
25698
  yield { type: "stage", stage: "ground", label: STAGE_LABELS.ground };
@@ -23586,8 +25745,106 @@ ${STRATEGIST_PLAN_SCHEMA_BLOCK}`
23586
25745
  text: "Backcast plan validated \u2014 will use it if the stress-test revision fails validation."
23587
25746
  };
23588
25747
  }
25748
+ const profile = loadProfile();
25749
+ const tree = getActiveOrgTree(profile);
25750
+ const playIds = collectPlayIds(candidatePlan?.plan ?? null);
25751
+ const gatingVital = extractGatingVitalFromDigest(
25752
+ digest,
25753
+ options.computeResult.aggregate.gating_vital_sign
25754
+ );
25755
+ const resolvedConstraint = resolveConstraint({
25756
+ health: {
25757
+ gating_vital: gatingVital,
25758
+ vital_readings: vitalsForFallback,
25759
+ metric_readings: toolCtx.metrics,
25760
+ digest
25761
+ },
25762
+ objective: {
25763
+ objective: options.objective,
25764
+ play_ids: playIds,
25765
+ operator_constraints: options.constraintsNote ? [options.constraintsNote] : []
25766
+ }
25767
+ });
25768
+ const seating = buildRoundtableSeating({
25769
+ tree,
25770
+ objective: options.objective,
25771
+ profile,
25772
+ playIds,
25773
+ gatingVital,
25774
+ resolvedConstraint
25775
+ });
25776
+ const constraintOwner = resolvedConstraint.locked_owners.join(", ") || null;
25777
+ const seatsBlock = seating.seats.filter((s) => s.pack_id !== "counsel_exec").map((s) => `- ${s.pack_id} (${s.weight})`).join("\n");
25778
+ const draftPlanJson = candidatePlan ? JSON.stringify(candidatePlan.plan) : backcastText;
25779
+ const counselPulls = /* @__PURE__ */ new Set();
25780
+ const observeRoundtableTool = (name, args) => {
25781
+ if (name !== "get_counsel_detail") return;
25782
+ const counselId = typeof args.counsel_id === "string" ? args.counsel_id : "";
25783
+ if (["counsel_sales", "counsel_marketing", "counsel_revops", "counsel_exec", "counsel_cs"].includes(
25784
+ counselId
25785
+ )) {
25786
+ counselPulls.add(counselId);
25787
+ }
25788
+ };
25789
+ yield { type: "stage", stage: "roundtable", label: STAGE_LABELS.roundtable };
25790
+ messages.push({
25791
+ role: "user",
25792
+ content: buildRoundtableMessage({
25793
+ objective: options.objective,
25794
+ tree,
25795
+ seatsBlock,
25796
+ biasNotes: seating.bias_notes,
25797
+ draftPlanJson,
25798
+ constraintOwner,
25799
+ constraint: resolvedConstraint
25800
+ })
25801
+ });
25802
+ const roundtableText = yield* runStage(
25803
+ "strategist",
25804
+ ROUNDTABLE_MAX_ROUNDS,
25805
+ "Tool budget reached for roundtable. Respond with the roundtable JSON now \u2014 strict JSON only.",
25806
+ true,
25807
+ observeRoundtableTool
25808
+ );
25809
+ let roundtableValidation = parseAndValidateRoundtable(roundtableText, {
25810
+ seating,
25811
+ constraintOwners: resolvedConstraint.locked_owners,
25812
+ counselPulls,
25813
+ requireCounselPulls: true
25814
+ });
25815
+ if (roundtableValidation.blocking) {
25816
+ messages.push({
25817
+ role: "user",
25818
+ content: buildRoundtableRetryMessage(roundtableValidation, seating)
25819
+ });
25820
+ const retryText = yield* runStage(
25821
+ "strategist",
25822
+ 2,
25823
+ "Respond with ONLY corrected roundtable JSON now. No prose.",
25824
+ true,
25825
+ observeRoundtableTool
25826
+ );
25827
+ roundtableValidation = parseAndValidateRoundtable(retryText, {
25828
+ seating,
25829
+ constraintOwners: resolvedConstraint.locked_owners,
25830
+ counselPulls,
25831
+ requireCounselPulls: true
25832
+ });
25833
+ }
25834
+ const roundtableResult = !roundtableValidation.blocking ? roundtableValidation.result : null;
25835
+ const roundtableIssues = [...roundtableValidation.issues];
25836
+ let roundtableDigest = roundtableResult ? formatRoundtableDigestFromResult(roundtableResult) : "";
25837
+ if (!roundtableResult) {
25838
+ roundtableDigest = "(Roundtable JSON failed structural validation \u2014 Stage C must run a pre-mortem, preserve both constraint channels, and avoid rubber-stamping the draft.)";
25839
+ yield {
25840
+ type: "notice",
25841
+ text: `Roundtable validation failed: ${roundtableValidation.issues.filter((entry) => entry.severity === "blocking").map((entry) => entry.code).join(", ") || "unknown shape"}.`
25842
+ };
25843
+ } else {
25844
+ yield { type: "notice", text: "Roundtable counsel captured \u2014 feeding into stress test." };
25845
+ }
23589
25846
  yield { type: "stage", stage: "stress", label: STAGE_LABELS.stress };
23590
- messages.push({ role: "user", content: buildStressTestMessage() });
25847
+ messages.push({ role: "user", content: buildStressTestMessage(roundtableDigest) });
23591
25848
  const finalText = yield* runStage(
23592
25849
  "strategist_stress",
23593
25850
  STRESS_MAX_ROUNDS,
@@ -23622,14 +25879,38 @@ Respond with ONLY the corrected plan JSON object in the required schema (title,
23622
25879
  text: "Strategist JSON failed validation \u2014 emitting grounded fallback plan from live vitals."
23623
25880
  };
23624
25881
  }
23625
- for (const issue of validated.issues) {
23626
- yield { type: "notice", text: issue };
25882
+ for (const issue2 of validated.issues) {
25883
+ yield { type: "notice", text: issue2 };
25884
+ }
25885
+ if (roundtableResult && candidatePlan) {
25886
+ const reflection = checkRoundtableReflection({
25887
+ roundtable: roundtableResult,
25888
+ draftPlan: candidatePlan.plan,
25889
+ finalPlan: validated.plan
25890
+ });
25891
+ for (const reflectionIssue of reflection.issues) {
25892
+ roundtableIssues.push(reflectionIssue);
25893
+ yield {
25894
+ type: "notice",
25895
+ text: `Counsel reflection (${reflectionIssue.code}): ${reflectionIssue.message}`
25896
+ };
25897
+ }
23627
25898
  }
25899
+ yield {
25900
+ type: "roundtable",
25901
+ result: roundtableResult,
25902
+ digest: roundtableDigest,
25903
+ issues: roundtableIssues,
25904
+ seating,
25905
+ counsel_pulls: [...counselPulls],
25906
+ counsel_fingerprint: planCounselFingerprint(validated.plan)
25907
+ };
23628
25908
  const rubric = scoreConsultantPlan(validated.plan, {
23629
25909
  snapshot: options.computeResult,
23630
25910
  constraintsNote: options.constraintsNote,
23631
25911
  todayIso,
23632
- evidenceText
25912
+ evidenceText,
25913
+ orgTree: tree
23633
25914
  });
23634
25915
  for (const gap of rubric.blocking) {
23635
25916
  yield { type: "notice", text: `Plan gap (${gap.code}): ${gap.note}` };
@@ -23655,6 +25936,29 @@ function validatePlanText(text, evidenceText, todayIso) {
23655
25936
  if (!raw) return null;
23656
25937
  return validateStrategistPlan(raw, { evidenceText, todayIso });
23657
25938
  }
25939
+ function collectPlayIds(plan) {
25940
+ if (!plan?.workstreams) return [];
25941
+ const ids = /* @__PURE__ */ new Set();
25942
+ for (const ws of plan.workstreams) {
25943
+ for (const id of ws.play_ids ?? []) {
25944
+ if (typeof id === "string" && id.trim()) ids.add(id.trim());
25945
+ }
25946
+ }
25947
+ return [...ids];
25948
+ }
25949
+ function planCounselFingerprint(plan) {
25950
+ if (!plan) return "";
25951
+ const plays = collectPlayIds(plan).sort().join(",");
25952
+ const constraint = (plan.constraints ?? []).join("|");
25953
+ const hypo = plan.hypothesis ?? "";
25954
+ const sequence = (plan.workstreams ?? []).slice().sort((a, b) => a.order - b.order).map(
25955
+ (ws) => `${ws.order}:${ws.title}:${ws.problem}:${ws.play_ids.join(",")}:${ws.actions[0] ?? ""}:${ws.expected_outcome.metric}:${ws.expected_outcome.target_range}:${ws.expected_outcome.check_date}`
25956
+ ).join("\u2192");
25957
+ return `${plays}::${constraint}::${hypo}::${sequence}::${plan.summary_30k ?? ""}`.slice(
25958
+ 0,
25959
+ 2e3
25960
+ );
25961
+ }
23658
25962
  function describePlanValidationFailure(text, evidenceText, todayIso) {
23659
25963
  const raw = parseJsonObjectFromText(text);
23660
25964
  if (!raw) {
@@ -23669,7 +25973,7 @@ function describePlanValidationFailure(text, evidenceText, todayIso) {
23669
25973
  }
23670
25974
  return "unknown validation failure";
23671
25975
  }
23672
- var GROUND_MAX_ROUNDS, BACKCAST_MAX_ROUNDS, STRESS_MAX_ROUNDS, STAGE_MAX_TOKENS, PLAN_JSON_MAX_TOKENS, STAGE_LABELS;
25976
+ var GROUND_MAX_ROUNDS, BACKCAST_MAX_ROUNDS, ROUNDTABLE_MAX_ROUNDS, STRESS_MAX_ROUNDS, STAGE_MAX_TOKENS, PLAN_JSON_MAX_TOKENS, STAGE_LABELS;
23673
25977
  var init_strategist2 = __esm({
23674
25978
  "src/ai/strategist.ts"() {
23675
25979
  "use strict";
@@ -23685,16 +25989,20 @@ var init_strategist2 = __esm({
23685
25989
  init_thread();
23686
25990
  init_strategist_prompt();
23687
25991
  init_strategist_validate();
25992
+ init_roundtable_validate();
23688
25993
  init_strategist_rubric();
23689
- init_strategist_prompt();
25994
+ init_profile();
25995
+ init_gtm_counsel();
23690
25996
  GROUND_MAX_ROUNDS = 6;
23691
25997
  BACKCAST_MAX_ROUNDS = 4;
25998
+ ROUNDTABLE_MAX_ROUNDS = 3;
23692
25999
  STRESS_MAX_ROUNDS = 2;
23693
26000
  STAGE_MAX_TOKENS = 4096;
23694
26001
  PLAN_JSON_MAX_TOKENS = 8192;
23695
26002
  STAGE_LABELS = {
23696
26003
  ground: "Grounding \u2014 reading health, metrics, segments, history",
23697
26004
  backcast: "Sequencing \u2014 backcasting from objective",
26005
+ roundtable: "Roundtable \u2014 sales, marketing, RevOps, CS, exec counsel",
23698
26006
  stress: "Stress-testing \u2014 capacity, measurability, timeline"
23699
26007
  };
23700
26008
  }
@@ -23754,13 +26062,43 @@ async function* strategistCraftSession(options) {
23754
26062
  const armedObjective = job.objective;
23755
26063
  let lastMeta = { provider_used: "unknown", model_used: "unknown" };
23756
26064
  let measurable = { measurable_targets: 0, total_targets: 0 };
23757
- const scorePlan = (plan) => scoreConsultantPlan(plan, {
23758
- snapshot: options.computeResult,
23759
- constraintsNote: options.constraintsNote ?? job.constraints_note,
23760
- armedObjective,
23761
- todayIso,
23762
- evidenceText
23763
- });
26065
+ const scorePlan = (plan) => {
26066
+ const scored = scoreConsultantPlan(plan, {
26067
+ snapshot: options.computeResult,
26068
+ constraintsNote: options.constraintsNote ?? job.constraints_note,
26069
+ armedObjective,
26070
+ todayIso,
26071
+ evidenceText,
26072
+ orgTree: getActiveOrgTree(loadProfile())
26073
+ });
26074
+ const currentFingerprint = planCounselFingerprint(plan);
26075
+ if (!job.roundtable || job.roundtable.counsel_fingerprint !== currentFingerprint || job.roundtable.issues.some((entry) => entry.severity === "blocking")) {
26076
+ const gap = {
26077
+ code: "roundtable_invalid",
26078
+ severity: "blocking",
26079
+ 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.",
26080
+ fix: "Re-run Stage R with every seated pack, required counsel pulls, and a valid exec merge."
26081
+ };
26082
+ scored.gaps.push(gap);
26083
+ scored.blocking.push(gap);
26084
+ scored.pass = false;
26085
+ }
26086
+ return scored;
26087
+ };
26088
+ const rememberRoundtable = (event) => {
26089
+ const record = {
26090
+ seating: event.seating,
26091
+ result: event.result,
26092
+ digest: event.digest,
26093
+ counsel_pulls: event.counsel_pulls,
26094
+ counsel_fingerprint: event.counsel_fingerprint,
26095
+ issues: event.issues,
26096
+ at: (/* @__PURE__ */ new Date()).toISOString()
26097
+ };
26098
+ job.roundtable = record;
26099
+ job.roundtable_history.push(record);
26100
+ return record;
26101
+ };
23764
26102
  const rememberBest = (plan, score) => {
23765
26103
  if (score == null) {
23766
26104
  if (!job.best_plan) job.best_plan = plan;
@@ -23804,6 +26142,9 @@ async function* strategistCraftSession(options) {
23804
26142
  if (event.type === "notice" && fallbackNotice(event.text)) {
23805
26143
  job.from_fallback = true;
23806
26144
  }
26145
+ if (event.type === "roundtable") {
26146
+ rememberRoundtable(event);
26147
+ }
23807
26148
  if (event.type === "plan") {
23808
26149
  const locked = lockPlanObjective(event.plan, armedObjective);
23809
26150
  job.plan = locked;
@@ -23859,6 +26200,8 @@ async function* strategistCraftSession(options) {
23859
26200
  };
23860
26201
  let stop;
23861
26202
  const startRound = (job.iterations.at(-1)?.round ?? 0) + 1;
26203
+ let lastRoundtableDigest = job.roundtable?.digest ?? null;
26204
+ let lastCounselFp = job.roundtable?.counsel_fingerprint ?? "";
23862
26205
  for (let round = startRound; round <= maxRounds; round++) {
23863
26206
  if (options.interrupted?.()) {
23864
26207
  stop = "interrupt";
@@ -23880,7 +26223,11 @@ async function* strategistCraftSession(options) {
23880
26223
  buildCriticMessage({
23881
26224
  objective: armedObjective,
23882
26225
  planJson: JSON.stringify(plan),
23883
- rubricGaps: rubric.blocking.map((g) => `${g.code}: ${g.note}`).join("\n") || "(none)",
26226
+ rubricGaps: [
26227
+ rubric.blocking.map((g) => `${g.code}: ${g.note}`).join("\n") || "(none)",
26228
+ lastRoundtableDigest ? `ROUNDTABLE DIGEST (address or reject with instruments):
26229
+ ${lastRoundtableDigest}` : ""
26230
+ ].filter(Boolean).join("\n\n"),
23884
26231
  healthSnapshot
23885
26232
  }),
23886
26233
  CRITIC_MAX_TOKENS
@@ -23938,6 +26285,9 @@ async function* strategistCraftSession(options) {
23938
26285
  for await (const event of runIteration0({ ...options, objective: armedObjective })) {
23939
26286
  if (event.type === "notice" && fallbackNotice(event.text)) {
23940
26287
  job.from_fallback = true;
26288
+ } else if (event.type === "roundtable") {
26289
+ rememberRoundtable(event);
26290
+ yield event;
23941
26291
  } else if (event.type === "plan") {
23942
26292
  job.plan = lockPlanObjective(event.plan, armedObjective);
23943
26293
  rememberBest(job.plan, null);
@@ -23983,6 +26333,135 @@ async function* strategistCraftSession(options) {
23983
26333
  };
23984
26334
  const nextRubric = scorePlan(job.plan);
23985
26335
  job.rubric_gaps = nextRubric.gaps;
26336
+ const nextFp = planCounselFingerprint(job.plan);
26337
+ if (nextFp !== lastCounselFp) {
26338
+ yield {
26339
+ type: "notice",
26340
+ text: "Plan plays/constraints shifted \u2014 re-running counsel roundtable pass."
26341
+ };
26342
+ try {
26343
+ const profile = loadProfile();
26344
+ const tree = getActiveOrgTree(profile);
26345
+ const playIds = (job.plan.workstreams ?? []).flatMap((w) => w.play_ids ?? []);
26346
+ const refreshedConstraint = resolveConstraint({
26347
+ health: {
26348
+ gating_vital: options.computeResult.aggregate.gating_vital_sign,
26349
+ vital_readings: options.computeResult.aggregate.vital_signs.map((v) => ({
26350
+ vital_sign: v.vital_sign,
26351
+ score: v.score,
26352
+ status: v.status,
26353
+ dollar_value: v.dollar_value
26354
+ }))
26355
+ },
26356
+ objective: {
26357
+ objective: armedObjective,
26358
+ play_ids: playIds,
26359
+ operator_constraints: options.constraintsNote ? [options.constraintsNote] : []
26360
+ }
26361
+ });
26362
+ const priorConstraint = job.roundtable?.seating.constraint;
26363
+ const lockedOwners = priorConstraint ? [.../* @__PURE__ */ new Set([
26364
+ ...priorConstraint.health_gate_owners,
26365
+ ...refreshedConstraint.objective_owners
26366
+ ])] : refreshedConstraint.locked_owners;
26367
+ const resolvedConstraint = priorConstraint ? {
26368
+ ...refreshedConstraint,
26369
+ health_gate: priorConstraint.health_gate,
26370
+ health_gate_owners: priorConstraint.health_gate_owners,
26371
+ locked_owners: lockedOwners,
26372
+ primary_owner: priorConstraint.health_gate_owners[0] ?? refreshedConstraint.primary_owner,
26373
+ notes: [
26374
+ .../* @__PURE__ */ new Set([
26375
+ ...priorConstraint.notes.filter(
26376
+ (note) => note.startsWith("Health gate:") || note.startsWith("Verified red metrics:")
26377
+ ),
26378
+ ...refreshedConstraint.notes.filter(
26379
+ (note) => !note.startsWith("Health gate:") && !note.startsWith("Verified red metrics:")
26380
+ )
26381
+ ])
26382
+ ]
26383
+ } : refreshedConstraint;
26384
+ const seating = buildRoundtableSeating({
26385
+ tree,
26386
+ objective: armedObjective,
26387
+ profile,
26388
+ playIds,
26389
+ gatingVital: options.computeResult.aggregate.gating_vital_sign,
26390
+ resolvedConstraint
26391
+ });
26392
+ const seatsBlock = seating.seats.filter((s) => s.pack_id !== "counsel_exec").map((s) => `- ${s.pack_id} (${s.weight})`).join("\n");
26393
+ const rtRaw = await callText(
26394
+ "strategist",
26395
+ buildRoundtableMessage({
26396
+ objective: armedObjective,
26397
+ tree,
26398
+ seatsBlock,
26399
+ biasNotes: [
26400
+ ...seating.bias_notes,
26401
+ "Craft re-entry: no tools this pass \u2014 use counsel catalog from the system prompt."
26402
+ ],
26403
+ draftPlanJson: JSON.stringify(job.plan),
26404
+ constraintOwner: resolvedConstraint.locked_owners.join(", ") || null,
26405
+ constraint: resolvedConstraint
26406
+ }),
26407
+ REVISE_MAX_TOKENS
26408
+ );
26409
+ job.cumulative_input_tokens += rtRaw.input;
26410
+ job.cumulative_output_tokens += rtRaw.output;
26411
+ const validation = parseAndValidateRoundtable(rtRaw.text, {
26412
+ seating,
26413
+ constraintOwners: resolvedConstraint.locked_owners,
26414
+ counselPulls: [],
26415
+ requireCounselPulls: false
26416
+ });
26417
+ const result = validation.blocking ? null : validation.result;
26418
+ lastRoundtableDigest = result ? formatRoundtableDigestFromResult(result) : "(Craft counsel re-entry failed structural validation; critic must not ship until Stage R is valid.)";
26419
+ const record = {
26420
+ seating,
26421
+ result,
26422
+ digest: lastRoundtableDigest,
26423
+ counsel_pulls: [],
26424
+ counsel_fingerprint: nextFp,
26425
+ issues: validation.issues,
26426
+ at: (/* @__PURE__ */ new Date()).toISOString()
26427
+ };
26428
+ job.roundtable = record;
26429
+ job.roundtable_history.push(record);
26430
+ if (validation.blocking) {
26431
+ lastCounselFp = "";
26432
+ yield {
26433
+ type: "notice",
26434
+ text: `Craft roundtable invalid: ${validation.issues.filter((entry) => entry.severity === "blocking").map((entry) => entry.code).join(", ")}.`
26435
+ };
26436
+ } else {
26437
+ lastCounselFp = nextFp;
26438
+ }
26439
+ } catch (err) {
26440
+ lastCounselFp = "";
26441
+ if (job.roundtable) {
26442
+ const failed = {
26443
+ ...job.roundtable,
26444
+ result: null,
26445
+ counsel_fingerprint: "",
26446
+ issues: [
26447
+ ...job.roundtable.issues,
26448
+ {
26449
+ code: "roundtable_reentry_error",
26450
+ severity: "blocking",
26451
+ message: String(err.message ?? err)
26452
+ }
26453
+ ],
26454
+ at: (/* @__PURE__ */ new Date()).toISOString()
26455
+ };
26456
+ job.roundtable = failed;
26457
+ job.roundtable_history.push(failed);
26458
+ }
26459
+ yield {
26460
+ type: "notice",
26461
+ text: `Craft roundtable re-entry failed: ${String(err.message ?? err)}.`
26462
+ };
26463
+ }
26464
+ }
23986
26465
  } else {
23987
26466
  yield { type: "notice", text: "Revise JSON invalid \u2014 keeping prior plan." };
23988
26467
  }
@@ -24041,8 +26520,11 @@ var init_strategist_craft = __esm({
24041
26520
  init_context2();
24042
26521
  init_strategist2();
24043
26522
  init_strategist_validate();
26523
+ init_roundtable_validate();
24044
26524
  init_strategist_rubric();
24045
26525
  init_strategist_prompt();
26526
+ init_profile();
26527
+ init_gtm_counsel();
24046
26528
  init_store3();
24047
26529
  CRITIC_MAX_TOKENS = 2048;
24048
26530
  REVISE_MAX_TOKENS = 8192;
@@ -24096,6 +26578,12 @@ function renderCraftHandoffMarkdown(opts) {
24096
26578
  );
24097
26579
  lines.push("");
24098
26580
  }
26581
+ if (opts.roundtableDigest) {
26582
+ lines.push("## Counsel roundtable");
26583
+ lines.push("");
26584
+ lines.push(opts.roundtableDigest);
26585
+ lines.push("");
26586
+ }
24099
26587
  if (plan.risks.length > 0) {
24100
26588
  lines.push("## Risks");
24101
26589
  lines.push("");
@@ -24135,7 +26623,8 @@ function writeCraftPlanHandoff(opts) {
24135
26623
  constraintLine: opts.constraintLine,
24136
26624
  killedAlternative: opts.killedAlternative,
24137
26625
  outOfScope: opts.outOfScope,
24138
- slug: opts.slug
26626
+ slug: opts.slug,
26627
+ roundtableDigest: opts.roundtableDigest
24139
26628
  });
24140
26629
  const path = resolveArchivePath("prompt:plan", `handoff-plan-${exportStamp()}.md`);
24141
26630
  writeRedactedText(path, markdown);
@@ -24308,7 +26797,8 @@ async function executeStrategistJob(req) {
24308
26797
  sessionId: req.ctx.sessionId,
24309
26798
  constraintLine,
24310
26799
  killedAlternative,
24311
- slug: result.slug
26800
+ slug: result.slug,
26801
+ roundtableDigest: job?.roundtable?.digest
24312
26802
  });
24313
26803
  result.handoff_path = written.path;
24314
26804
  result.inbox_path = written.inboxPath;
@@ -24964,6 +27454,7 @@ async function printKeylessSkeletonPlan(ctx, objective) {
24964
27454
  if (snapshot) {
24965
27455
  const { matchTriggeredPlays: matchTriggeredPlays2 } = await Promise.resolve().then(() => (init_playbook(), playbook_exports));
24966
27456
  const { LAYERS: LAYERS2 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
27457
+ const { getActiveOrgTree: getActiveOrgTree2 } = await Promise.resolve().then(() => (init_gtm_counsel(), gtm_counsel_exports));
24967
27458
  const triggered = matchTriggeredPlays2(
24968
27459
  snapshot.aggregate.vital_signs.map((v) => ({
24969
27460
  vital_sign: v.vital_sign,
@@ -24972,7 +27463,8 @@ async function printKeylessSkeletonPlan(ctx, objective) {
24972
27463
  dollar_value: v.dollar_value,
24973
27464
  dollar_label: v.dollar_label
24974
27465
  })),
24975
- LAYERS2
27466
+ LAYERS2,
27467
+ { tree: getActiveOrgTree2() }
24976
27468
  );
24977
27469
  if (triggered.length > 0) {
24978
27470
  console.log(" " + chalk18.bold("Simple plan") + chalk18.dim(" \u2014 from computed vital signs. No AI."));
@@ -25380,6 +27872,8 @@ ${buildPlaybookBlock()}
25380
27872
 
25381
27873
  ${buildFrameworkCatalogBlock()}
25382
27874
 
27875
+ ${buildCounselCatalogBlock()}
27876
+
25383
27877
  ${commandSection}
25384
27878
  ${formattingSection}
25385
27879
  OUTPUT RULES:
@@ -25409,6 +27903,7 @@ var init_think_prompt = __esm({
25409
27903
  "use strict";
25410
27904
  init_prompt_parts();
25411
27905
  init_frameworks();
27906
+ init_gtm_counsel();
25412
27907
  init_prompt();
25413
27908
  }
25414
27909
  });
@@ -25638,6 +28133,8 @@ ${buildPlaybookBlock()}
25638
28133
 
25639
28134
  ${buildFrameworkCatalogBlock()}
25640
28135
 
28136
+ ${buildCounselCatalogBlock()}
28137
+
25641
28138
  ${commandSection}`;
25642
28139
  const stable = `You are a world-class GTM operating partner \u2014 the kind of analyst a CEO keeps on speed dial. You are exceptionally well-read, rigorous, and commercially sharp, and you have tools to query a local database of this company's CRM and pipeline data. The user is having an ongoing, free-form conversation with you about their go-to-market health and SaaS metrics.
25643
28140
 
@@ -25887,6 +28384,7 @@ var init_agentic_loop = __esm({
25887
28384
  init_untrusted();
25888
28385
  init_prompt_parts();
25889
28386
  init_frameworks();
28387
+ init_gtm_counsel();
25890
28388
  init_think_prompt();
25891
28389
  init_prompt();
25892
28390
  init_context2();
@@ -28774,7 +31272,7 @@ function stripFences2(text) {
28774
31272
  return trimmed;
28775
31273
  }
28776
31274
  function validateTaxonomy(raw, profile) {
28777
- const strArray2 = (key, min = 3) => {
31275
+ const strArray3 = (key, min = 3) => {
28778
31276
  const v = raw[key];
28779
31277
  if (!Array.isArray(v)) throw new Error(`Missing or invalid array: ${key}`);
28780
31278
  const items = v.filter((x) => typeof x === "string" && x.trim().length > 0);
@@ -28804,14 +31302,14 @@ function validateTaxonomy(raw, profile) {
28804
31302
  mid_market: pickTitles("mid_market"),
28805
31303
  smb: pickTitles("smb")
28806
31304
  };
28807
- const rep_titles = strArray2("rep_titles");
28808
- const industries = strArray2("industries");
28809
- const company_name_prefixes = strArray2("company_name_prefixes", 5);
28810
- const company_name_suffixes = strArray2("company_name_suffixes", 3);
28811
- const deal_verbs = strArray2("deal_verbs");
28812
- const deal_modifiers = strArray2("deal_modifiers");
28813
- const topics = strArray2("topics");
28814
- const challenges = strArray2("challenges");
31305
+ const rep_titles = strArray3("rep_titles");
31306
+ const industries = strArray3("industries");
31307
+ const company_name_prefixes = strArray3("company_name_prefixes", 5);
31308
+ const company_name_suffixes = strArray3("company_name_suffixes", 3);
31309
+ const deal_verbs = strArray3("deal_verbs");
31310
+ const deal_modifiers = strArray3("deal_modifiers");
31311
+ const topics = strArray3("topics");
31312
+ const challenges = strArray3("challenges");
28815
31313
  const rwRaw = raw["region_weights"];
28816
31314
  if (!rwRaw || typeof rwRaw !== "object") throw new Error("Missing region_weights");
28817
31315
  const rw = rwRaw;
@@ -29621,8 +32119,8 @@ var init_metrics_report = __esm({
29621
32119
  function summarizeEntityDetails(details = []) {
29622
32120
  const summary = {};
29623
32121
  for (const detail of details) {
29624
- const issue = typeof detail.issue === "string" ? detail.issue : "unclassified";
29625
- summary[issue] = (summary[issue] ?? 0) + 1;
32122
+ const issue2 = typeof detail.issue === "string" ? detail.issue : "unclassified";
32123
+ summary[issue2] = (summary[issue2] ?? 0) + 1;
29626
32124
  }
29627
32125
  return summary;
29628
32126
  }
@@ -29927,31 +32425,31 @@ function stripFences3(text) {
29927
32425
  }
29928
32426
  function validateDraft(raw) {
29929
32427
  const out = {};
29930
- const str2 = (k) => {
32428
+ const str3 = (k) => {
29931
32429
  const v = raw[k];
29932
32430
  return typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0;
29933
32431
  };
29934
- const companyName = str2("company_name");
32432
+ const companyName = str3("company_name");
29935
32433
  if (companyName) out.company_name = companyName;
29936
- const industry = str2("industry");
32434
+ const industry = str3("industry");
29937
32435
  if (industry) out.industry = industry;
29938
- const productDescription = str2("product_description");
32436
+ const productDescription = str3("product_description");
29939
32437
  if (productDescription) out.product_description = productDescription;
29940
- const targetCustomer = str2("target_customer");
32438
+ const targetCustomer = str3("target_customer");
29941
32439
  if (targetCustomer) out.target_customer = targetCustomer;
29942
- const motion = str2("sales_motion")?.toLowerCase();
32440
+ const motion = str3("sales_motion")?.toLowerCase();
29943
32441
  if (motion && ALLOWED_MOTIONS.has(motion)) {
29944
32442
  out.sales_motion = motion;
29945
32443
  }
29946
- const dealSize = str2("average_deal_size");
32444
+ const dealSize = str3("average_deal_size");
29947
32445
  if (dealSize) out.average_deal_size = dealSize;
29948
32446
  const cycleDays = raw["sales_cycle_days"];
29949
32447
  if (typeof cycleDays === "number" && Number.isFinite(cycleDays) && cycleDays > 0) {
29950
32448
  out.sales_cycle_days = Math.round(cycleDays);
29951
32449
  }
29952
- const crm = str2("primary_crm")?.toLowerCase();
32450
+ const crm = str3("primary_crm")?.toLowerCase();
29953
32451
  if (crm && ALLOWED_CRMS.has(crm)) out.primary_crm = crm;
29954
- const engagement = str2("engagement_tool")?.toLowerCase();
32452
+ const engagement = str3("engagement_tool")?.toLowerCase();
29955
32453
  if (engagement && ALLOWED_ENGAGEMENT.has(engagement)) out.engagement_tool = engagement;
29956
32454
  return out;
29957
32455
  }
@@ -30373,31 +32871,31 @@ Emit the refined profile now as STRICT JSON.`;
30373
32871
  }
30374
32872
  function validateRefined(raw) {
30375
32873
  const out = {};
30376
- const str2 = (k) => {
32874
+ const str3 = (k) => {
30377
32875
  const v = raw[k];
30378
32876
  return typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0;
30379
32877
  };
30380
- const industry = str2("industry");
32878
+ const industry = str3("industry");
30381
32879
  if (industry) out.industry = industry;
30382
- const productDescription = str2("product_description");
32880
+ const productDescription = str3("product_description");
30383
32881
  if (productDescription) out.product_description = productDescription;
30384
- const targetCustomer = str2("target_customer");
32882
+ const targetCustomer = str3("target_customer");
30385
32883
  if (targetCustomer) out.target_customer = targetCustomer;
30386
- const motion = str2("sales_motion")?.toLowerCase();
32884
+ const motion = str3("sales_motion")?.toLowerCase();
30387
32885
  if (motion && ALLOWED_MOTIONS2.has(motion)) {
30388
32886
  out.sales_motion = motion;
30389
32887
  }
30390
- const dealSize = str2("average_deal_size");
32888
+ const dealSize = str3("average_deal_size");
30391
32889
  if (dealSize) out.average_deal_size = dealSize;
30392
32890
  const cycleDays = raw["sales_cycle_days"];
30393
32891
  if (typeof cycleDays === "number" && Number.isFinite(cycleDays) && cycleDays > 0) {
30394
32892
  out.sales_cycle_days = Math.round(cycleDays);
30395
32893
  }
30396
- const crm = str2("primary_crm")?.toLowerCase();
32894
+ const crm = str3("primary_crm")?.toLowerCase();
30397
32895
  if (crm && ALLOWED_CRMS2.has(crm)) out.primary_crm = crm;
30398
- const engagement = str2("engagement_tool")?.toLowerCase();
32896
+ const engagement = str3("engagement_tool")?.toLowerCase();
30399
32897
  if (engagement && ALLOWED_ENGAGEMENT2.has(engagement)) out.engagement_tool = engagement;
30400
- const userScope = str2("user_scope");
32898
+ const userScope = str3("user_scope");
30401
32899
  if (userScope) out.user_scope = userScope;
30402
32900
  return out;
30403
32901
  }
@@ -32209,8 +34707,8 @@ function generateMarkdownReport(data) {
32209
34707
  lines.push("");
32210
34708
  for (const entity of vs.entity_details.slice(0, 5)) {
32211
34709
  const name = entity.name ?? entity.id ?? "Unknown";
32212
- const issue = entity.issue ?? "";
32213
- lines.push(`- **${name}**: ${issue}`);
34710
+ const issue2 = entity.issue ?? "";
34711
+ lines.push(`- **${name}**: ${issue2}`);
32214
34712
  }
32215
34713
  if (vs.entity_details.length > 5) {
32216
34714
  lines.push(`- *...and ${vs.entity_details.length - 5} more*`);
@@ -36282,33 +38780,33 @@ Apply the feedback now as STRICT JSON.`;
36282
38780
  }
36283
38781
  function validatePatch(raw) {
36284
38782
  const out = {};
36285
- const str2 = (k) => {
38783
+ const str3 = (k) => {
36286
38784
  const v = raw[k];
36287
38785
  return typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0;
36288
38786
  };
36289
- const industry = str2("industry");
38787
+ const industry = str3("industry");
36290
38788
  if (industry) out.industry = industry;
36291
- const productDescription = str2("product_description");
38789
+ const productDescription = str3("product_description");
36292
38790
  if (productDescription) out.product_description = productDescription;
36293
- const targetCustomer = str2("target_customer");
38791
+ const targetCustomer = str3("target_customer");
36294
38792
  if (targetCustomer) out.target_customer = targetCustomer;
36295
- const motion = str2("sales_motion")?.toLowerCase();
38793
+ const motion = str3("sales_motion")?.toLowerCase();
36296
38794
  if (motion && ALLOWED_MOTIONS3.has(motion)) {
36297
38795
  out.sales_motion = motion;
36298
38796
  }
36299
- const dealSize = str2("average_deal_size");
38797
+ const dealSize = str3("average_deal_size");
36300
38798
  if (dealSize) out.average_deal_size = dealSize;
36301
38799
  const cycleDays = raw["sales_cycle_days"];
36302
38800
  if (typeof cycleDays === "number" && Number.isFinite(cycleDays) && cycleDays > 0) {
36303
38801
  out.sales_cycle_days = Math.round(cycleDays);
36304
38802
  }
36305
- const crm = str2("primary_crm")?.toLowerCase();
38803
+ const crm = str3("primary_crm")?.toLowerCase();
36306
38804
  if (crm && ALLOWED_CRMS3.has(crm)) out.primary_crm = crm;
36307
- const engagement = str2("engagement_tool")?.toLowerCase();
38805
+ const engagement = str3("engagement_tool")?.toLowerCase();
36308
38806
  if (engagement && ALLOWED_ENGAGEMENT3.has(engagement)) out.engagement_tool = engagement;
36309
- const userScope = str2("user_scope");
38807
+ const userScope = str3("user_scope");
36310
38808
  if (userScope) out.user_scope = userScope;
36311
- const customContext = str2("custom_context");
38809
+ const customContext = str3("custom_context");
36312
38810
  if (customContext) out.custom_context = customContext;
36313
38811
  return out;
36314
38812
  }
@@ -40278,12 +42776,13 @@ import { existsSync as existsSync37, readFileSync as readFileSync24 } from "fs";
40278
42776
  import { join as join38 } from "path";
40279
42777
  function buildCompanyProfileBlock() {
40280
42778
  const p = loadProfile();
40281
- if (!p) return "";
42779
+ if (!p) return buildOrgTreeContextLine(null);
40282
42780
  const lines = [];
40283
42781
  lines.push(`- Industry: ${sanitizeExternalText(p.industry)}`);
40284
42782
  lines.push(`- Product: ${sanitizeExternalText(p.product_description)}`);
40285
42783
  lines.push(`- Target customer: ${sanitizeExternalText(p.target_customer)}`);
40286
42784
  lines.push(`- Sales motion: ${sanitizeExternalText(p.sales_motion)}`);
42785
+ lines.push(buildOrgTreeContextLine(p));
40287
42786
  if (p.average_deal_size) lines.push(`- Avg deal size: ${sanitizeExternalText(String(p.average_deal_size))}`);
40288
42787
  if (p.sales_cycle_days !== void 0) lines.push(`- Typical sales cycle: ~${p.sales_cycle_days} days`);
40289
42788
  if (p.primary_crm) lines.push(`- Primary CRM: ${sanitizeExternalText(p.primary_crm)}`);
@@ -40377,6 +42876,7 @@ var init_prompt_parts = __esm({
40377
42876
  "use strict";
40378
42877
  init_profile();
40379
42878
  init_store();
42879
+ init_gtm_counsel();
40380
42880
  init_playbook();
40381
42881
  init_play_outcomes();
40382
42882
  init_registry2();
@@ -40425,9 +42925,15 @@ Restraint matters: NTRP is a stethoscope, not a surgeon. Observe, connect, and r
40425
42925
  Expert read: weight by deal size \u2014 one single-threaded mega-deal outweighs ten small ones. Single-threading late in the cycle is far more dangerous than early. In enterprise motions, thread depth is a leading indicator of slipped quarters: champions change jobs, and there's no second door in.`;
40426
42926
  PLAYBOOK_BLOCK = `- "Multi-Thread Your Deals" (id: multi-thread-deals) \u2014 when thread_depth is low
40427
42927
  - "Clean Dead Pipeline" (id: clean-dead-pipeline) \u2014 when freshness is low
40428
- - "Fix the Handoff Gap" (id: fix-handoff-gap) \u2014 when drop_rate is high
42928
+ - "Fix the Handoff Gap" (id: fix-handoff-gap) \u2014 when drop_rate is high and the source\u2192owner path is unknown
40429
42929
  - "Retarget Misdirected Effort" (id: retarget-effort) \u2014 when signal_to_noise is low
40430
- - "Unstick the Pipeline" (id: unstick-pipeline) \u2014 when flow_rate is low`;
42930
+ - "Unstick the Pipeline" (id: unstick-pipeline) \u2014 when flow_rate is low
42931
+ - "Harden Routing & Acceptance SLA" (id: harden-routing-sla) \u2014 when the handoff path exists but SLA/observability is the leak
42932
+ - "Demand Quality over Volume" (id: demand-quality-over-volume) \u2014 when MQL\u2192SQL\u2192Opp quality is weak (not a drop_rate routing play)
42933
+ - "Sales\u2192CS Handoff Packet" (id: sales-cs-handoff-packet) \u2014 when post-sale handoffs create churn risk
42934
+ - "Renewal Early Warning" (id: renewal-early-warning) \u2014 when renewals scramble late
42935
+ - "ABM Orchestration on Named Accounts" (id: abm-orchestration) \u2014 enterprise/named-account list pipeline; kill on smb_velocity without a named list
42936
+ - "Restore Forecast Ritual Hygiene" (id: forecast-ritual-hygiene) \u2014 after instrument trust, when commit evidence and past-due closes undermine forecast credibility`;
40431
42937
  DESTRUCTIVE_COMMAND_NOTES = {
40432
42938
  reset: "destructive \u2014 wipes all data, requires --force"
40433
42939
  };