thumbgate 1.14.1 → 1.16.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 (150) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +60 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +217 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +211 -8
  12. package/config/enforcement.json +59 -7
  13. package/config/evals/agent-safety-eval.json +338 -22
  14. package/config/gates/default.json +33 -0
  15. package/config/gates/routine.json +43 -0
  16. package/config/github-about.json +3 -3
  17. package/config/mcp-allowlists.json +4 -0
  18. package/config/merge-quality-checks.json +2 -1
  19. package/config/model-candidates.json +131 -0
  20. package/openapi/openapi.yaml +118 -2
  21. package/package.json +70 -51
  22. package/public/blog.html +7 -7
  23. package/public/codex-plugin.html +13 -7
  24. package/public/compare.html +29 -23
  25. package/public/dashboard.html +105 -12
  26. package/public/guide.html +28 -28
  27. package/public/index.html +233 -97
  28. package/public/learn.html +87 -20
  29. package/public/lessons.html +26 -2
  30. package/public/numbers.html +271 -0
  31. package/public/pro.html +89 -19
  32. package/scripts/agent-audit-trace.js +55 -0
  33. package/scripts/agent-memory-lifecycle.js +96 -0
  34. package/scripts/agent-readiness-plan.js +118 -0
  35. package/scripts/agentic-data-pipeline.js +21 -1
  36. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  37. package/scripts/ai-org-governance.js +98 -0
  38. package/scripts/ai-search-distribution.js +43 -0
  39. package/scripts/artifact-agent-plan.js +81 -0
  40. package/scripts/billing.js +27 -8
  41. package/scripts/cli-feedback.js +2 -1
  42. package/scripts/cli-schema.js +60 -5
  43. package/scripts/code-mode-mcp-plan.js +71 -0
  44. package/scripts/commercial-offer.js +1 -1
  45. package/scripts/context-engine.js +1 -2
  46. package/scripts/context-manager.js +4 -1
  47. package/scripts/contextfs.js +214 -32
  48. package/scripts/dashboard-render-spec.js +1 -1
  49. package/scripts/dashboard.js +275 -9
  50. package/scripts/decision-journal.js +13 -3
  51. package/scripts/document-workflow-governance.js +62 -0
  52. package/scripts/enterprise-agent-rollout.js +34 -0
  53. package/scripts/experience-replay-governance.js +69 -0
  54. package/scripts/export-hf-dataset.js +1 -1
  55. package/scripts/feedback-loop.js +141 -9
  56. package/scripts/feedback-to-rules.js +17 -23
  57. package/scripts/gates-engine.js +4 -6
  58. package/scripts/growth-campaigns.js +49 -0
  59. package/scripts/harness-selector.js +145 -1
  60. package/scripts/hybrid-supervisor-agent.js +64 -0
  61. package/scripts/inference-cache-policy.js +72 -0
  62. package/scripts/inference-economics.js +53 -0
  63. package/scripts/internal-agent-bootstrap.js +12 -2
  64. package/scripts/knowledge-layer-plan.js +108 -0
  65. package/scripts/lesson-canonical.js +181 -0
  66. package/scripts/lesson-db.js +71 -10
  67. package/scripts/lesson-inference.js +183 -44
  68. package/scripts/lesson-search.js +4 -1
  69. package/scripts/lesson-synthesis.js +23 -2
  70. package/scripts/llm-client.js +157 -26
  71. package/scripts/mailer/resend-mailer.js +112 -1
  72. package/scripts/mcp-transport-strategy.js +66 -0
  73. package/scripts/memory-store-governance.js +60 -0
  74. package/scripts/meta-agent-loop.js +7 -13
  75. package/scripts/model-access-eligibility.js +38 -0
  76. package/scripts/model-migration-readiness.js +55 -0
  77. package/scripts/native-messaging-audit.js +514 -0
  78. package/scripts/operational-integrity.js +96 -3
  79. package/scripts/otel-declarative-config.js +56 -0
  80. package/scripts/perplexity-client.js +1 -1
  81. package/scripts/post-training-governance.js +34 -0
  82. package/scripts/pr-manager.js +47 -7
  83. package/scripts/private-core-boundary.js +72 -0
  84. package/scripts/production-agent-readiness.js +40 -0
  85. package/scripts/profile-router.js +16 -1
  86. package/scripts/prompt-eval.js +564 -32
  87. package/scripts/prompt-programs.js +93 -0
  88. package/scripts/provider-action-normalizer.js +585 -0
  89. package/scripts/rule-validator.js +285 -0
  90. package/scripts/scaling-law-claims.js +60 -0
  91. package/scripts/security-scanner.js +1 -1
  92. package/scripts/self-distill-agent.js +7 -32
  93. package/scripts/seo-gsd.js +400 -43
  94. package/scripts/skill-rag-router.js +53 -0
  95. package/scripts/spec-gate.js +1 -1
  96. package/scripts/student-consistent-training.js +73 -0
  97. package/scripts/synthetic-data-provenance.js +98 -0
  98. package/scripts/task-context-result.js +81 -0
  99. package/scripts/telemetry-analytics.js +149 -0
  100. package/scripts/thompson-sampling.js +2 -2
  101. package/scripts/token-savings.js +7 -6
  102. package/scripts/token-tco.js +46 -0
  103. package/scripts/tool-registry.js +75 -3
  104. package/scripts/verification-loop.js +10 -1
  105. package/scripts/verifier-scoring.js +71 -0
  106. package/scripts/workflow-sentinel.js +284 -28
  107. package/scripts/workspace-agent-routines.js +118 -0
  108. package/skills/thumbgate/SKILL.md +1 -1
  109. package/src/api/server.js +434 -120
  110. package/.claude-plugin/README.md +0 -170
  111. package/adapters/README.md +0 -12
  112. package/scripts/analytics-report.js +0 -328
  113. package/scripts/autonomous-workflow.js +0 -377
  114. package/scripts/billing-setup.js +0 -109
  115. package/scripts/creator-campaigns.js +0 -239
  116. package/scripts/cross-encoder-reranker.js +0 -235
  117. package/scripts/daemon-manager.js +0 -108
  118. package/scripts/decision-trace.js +0 -354
  119. package/scripts/delegation-runtime.js +0 -896
  120. package/scripts/dispatch-brief.js +0 -159
  121. package/scripts/distribution-surfaces.js +0 -110
  122. package/scripts/feedback-history-distiller.js +0 -382
  123. package/scripts/funnel-analytics.js +0 -35
  124. package/scripts/history-distiller.js +0 -200
  125. package/scripts/hosted-job-launcher.js +0 -256
  126. package/scripts/intent-router.js +0 -392
  127. package/scripts/lesson-reranker.js +0 -263
  128. package/scripts/lesson-retrieval.js +0 -148
  129. package/scripts/managed-lesson-agent.js +0 -183
  130. package/scripts/operational-dashboard.js +0 -103
  131. package/scripts/operational-summary.js +0 -129
  132. package/scripts/operator-artifacts.js +0 -608
  133. package/scripts/optimize-context.js +0 -17
  134. package/scripts/org-dashboard.js +0 -206
  135. package/scripts/partner-orchestration.js +0 -146
  136. package/scripts/predictive-insights.js +0 -356
  137. package/scripts/pulse.js +0 -80
  138. package/scripts/reflector-agent.js +0 -221
  139. package/scripts/sales-pipeline.js +0 -681
  140. package/scripts/session-episode-store.js +0 -329
  141. package/scripts/session-health-sensor.js +0 -242
  142. package/scripts/session-report.js +0 -120
  143. package/scripts/swarm-coordinator.js +0 -81
  144. package/scripts/tool-kpi-tracker.js +0 -12
  145. package/scripts/webhook-delivery.js +0 -62
  146. package/scripts/workflow-sprint-intake.js +0 -475
  147. package/skills/agent-memory/SKILL.md +0 -97
  148. package/skills/solve-architecture-autonomy/SKILL.md +0 -17
  149. package/skills/solve-architecture-autonomy/tool.js +0 -33
  150. package/skills/thumbgate-feedback/SKILL.md +0 -49
@@ -605,6 +605,137 @@ function selectFlatContextItems(candidates, maxItems, maxChars) {
605
605
  };
606
606
  }
607
607
 
608
+ /* ── Summarize-then-expand selection ───────────────────────────────
609
+ *
610
+ * Two-pass retrieval that front-loads recall, then spends remaining char
611
+ * budget on depth for the highest-scoring candidates.
612
+ *
613
+ * Pass 1 — breadth. Walk the ranked candidate list and add each as a
614
+ * compact "summary tier" item: title + one-line hint drawn from the
615
+ * structured fields (whatToChange / whatWentWrong / first content line).
616
+ * A summary is small and bounded (SUMMARY_HINT_MAX chars), so many fit in
617
+ * a fraction of the budget. Stops when maxItems or a summary-reservation
618
+ * budget cap (SUMMARY_RESERVE_FRACTION of maxChars) is hit — this protects
619
+ * enough headroom for Pass 2 to actually do something.
620
+ *
621
+ * Pass 2 — depth. Walk the selected list top-down and try to upgrade each
622
+ * summary to the full structured context. The upgrade cost is the delta
623
+ * between full doc chars and the summary we already accounted for; if it
624
+ * fits under the *overall* maxChars, swap the summary for the full item
625
+ * and tag it tier='expanded'. Stop when the budget is exhausted.
626
+ *
627
+ * Rationale: the flat selector overcommits chars on the first few full-size
628
+ * hits and silently drops the tail. Summarize-then-expand means a consumer
629
+ * always knows which docs matched (full roster of titles), and the model
630
+ * sees full context for the top answers.
631
+ *
632
+ * The option is wired into constructContextPack via `strategy` or the
633
+ * explicit `summarizeThenExpand` flag. Default behavior is unchanged so
634
+ * existing callers / tests don't shift.
635
+ */
636
+
637
+ const SUMMARY_HINT_MAX = 160;
638
+ const SUMMARY_RESERVE_FRACTION = 0.35;
639
+
640
+ function buildSummaryContext(doc) {
641
+ const full = buildStructuredContext(doc);
642
+ // Priority: explicit whatToChange > whatWentWrong > reasoning > first
643
+ // non-empty content line. We truncate aggressively because a summary's
644
+ // purpose is to fit dozens per pack, not to win a precision test.
645
+ const hint = (
646
+ full.whatToChange
647
+ || full.whatWentWrong
648
+ || full.reasoning
649
+ || (doc.content || '').split('\n').map((l) => l.trim()).find(Boolean)
650
+ || ''
651
+ ).slice(0, SUMMARY_HINT_MAX);
652
+ return {
653
+ rawContent: hint,
654
+ reasoning: null,
655
+ whatWentWrong: null,
656
+ whatToChange: null,
657
+ rubricFailure: null,
658
+ };
659
+ }
660
+
661
+ function measureSummaryChars(doc) {
662
+ const hint = buildSummaryContext(doc).rawContent;
663
+ return `${doc.title || ''}\n${hint}`.length;
664
+ }
665
+
666
+ function selectSummarizeThenExpand(candidates, maxItems, maxChars) {
667
+ // Pass 1 — breadth. Pack summaries greedily under a share of the budget.
668
+ const summaryBudget = Math.max(
669
+ Math.floor(maxChars * SUMMARY_RESERVE_FRACTION),
670
+ measureSummaryChars({ title: '', content: '' }) + 1,
671
+ );
672
+ const selected = [];
673
+ let usedChars = 0;
674
+ let skippedByMaxChars = 0;
675
+
676
+ for (const item of candidates) {
677
+ if (selected.length >= maxItems) break;
678
+
679
+ const summaryLen = measureSummaryChars(item.doc);
680
+ if (usedChars + summaryLen > summaryBudget) {
681
+ skippedByMaxChars += 1;
682
+ continue;
683
+ }
684
+
685
+ selected.push({
686
+ id: item.doc.id,
687
+ namespace: item.doc.namespace,
688
+ title: item.doc.title,
689
+ structuredContext: buildSummaryContext(item.doc),
690
+ tags: item.doc.tags || [],
691
+ score: item.score,
692
+ tier: 'summary',
693
+ _doc: item.doc,
694
+ _summaryLen: summaryLen,
695
+ });
696
+ usedChars += summaryLen;
697
+ }
698
+
699
+ // Pass 2 — depth. Upgrade top-ranked summaries to full items while the
700
+ // overall char budget can absorb the delta. Walks in current (score) order
701
+ // so the most relevant docs are expanded first.
702
+ let expandedCount = 0;
703
+ for (const entry of selected) {
704
+ const fullLen = measureDocumentChars(entry._doc);
705
+ const delta = fullLen - entry._summaryLen;
706
+ if (delta <= 0) continue; // already at or under summary size; leave it.
707
+ if (usedChars + delta > maxChars) continue;
708
+
709
+ entry.structuredContext = buildStructuredContext(entry._doc);
710
+ entry.tier = 'expanded';
711
+ usedChars += delta;
712
+ expandedCount += 1;
713
+ }
714
+
715
+ // Strip the private helpers before returning — they're builder-only state.
716
+ const items = selected.map(({ _doc, _summaryLen, ...rest }) => rest);
717
+
718
+ return {
719
+ items,
720
+ usedChars,
721
+ skippedByMaxChars,
722
+ retrieval: {
723
+ strategy: 'summarize-then-expand',
724
+ themeCount: 0,
725
+ semanticCount: 0,
726
+ selectedThemes: [],
727
+ selectedSemanticGroups: [],
728
+ representativeCount: items.length,
729
+ expandedEpisodes: expandedCount,
730
+ summaryCount: items.length - expandedCount,
731
+ summaryBudget,
732
+ queryCoverage: null,
733
+ initialCoverage: null,
734
+ coverageTarget: null,
735
+ },
736
+ };
737
+ }
738
+
608
739
  /* ── Memex-style Indexed Memory ────────────────────────────────── */
609
740
 
610
741
  const MEMEX_INDEX_FILE = 'memex-index.jsonl';
@@ -750,17 +881,38 @@ function constructMemexPack({ query = '', maxItems = 8, maxChars = 6000, namespa
750
881
  return pack;
751
882
  }
752
883
 
753
- function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, namespaces = [] } = {}) {
884
+ function constructContextPack({
885
+ query = '',
886
+ maxItems = 8,
887
+ maxChars = 6000,
888
+ namespaces = [],
889
+ strategy = null,
890
+ summarizeThenExpand = false,
891
+ } = {}) {
754
892
  const normalizedNamespaces = normalizeNamespaces(namespaces);
755
893
  const tokens = tokenizeQuery(query);
756
894
  const sourceHash = getSourceHash(normalizedNamespaces);
757
895
 
758
- const cacheHit = findSemanticCacheHit({
759
- query,
760
- namespaces: normalizedNamespaces,
761
- maxItems,
762
- maxChars,
763
- });
896
+ // Resolve the effective strategy. Explicit `strategy` wins; otherwise
897
+ // `summarizeThenExpand: true` flips the flag. Default remains auto
898
+ // (flat | hierarchical) so callers that don't opt in keep their cached
899
+ // packs addressable.
900
+ const effectiveStrategy = strategy
901
+ || (summarizeThenExpand ? 'summarize-then-expand' : null);
902
+
903
+ // Skip the semantic cache for summarize-then-expand packs. The cache key
904
+ // is (namespaces, maxItems, maxChars) — it doesn't include the strategy,
905
+ // so a cached flat pack would be served to an STE caller (and vice versa)
906
+ // with the wrong shape. Cheaper to recompute than to extend the cache key
907
+ // and invalidate every entry on disk.
908
+ const cacheHit = effectiveStrategy === 'summarize-then-expand'
909
+ ? null
910
+ : findSemanticCacheHit({
911
+ query,
912
+ namespaces: normalizedNamespaces,
913
+ maxItems,
914
+ maxChars,
915
+ });
764
916
 
765
917
  if (cacheHit) {
766
918
  const packId = `pack_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -796,25 +948,51 @@ function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, names
796
948
  .sort((a, b) => b.score - a.score);
797
949
 
798
950
  const hierarchicalRetrievalEnabled = shouldUseHierarchicalRetrieval(normalizedNamespaces);
799
- const selection = hierarchicalRetrievalEnabled
800
- ? retrieveHierarchicalDocuments({
951
+ let selection;
952
+ if (effectiveStrategy === 'summarize-then-expand') {
953
+ // Explicit opt-in: bypass the hierarchical path entirely. The
954
+ // summarize-then-expand selector assumes a flat ranked list where each
955
+ // item is a single episode, and mixing it with theme-based hierarchical
956
+ // retrieval would double-compress the top-of-list.
957
+ selection = selectSummarizeThenExpand(candidates, maxItems, maxChars);
958
+ } else if (hierarchicalRetrievalEnabled) {
959
+ selection = retrieveHierarchicalDocuments({
801
960
  documents: candidates.map((candidate) => candidate.doc),
802
961
  query,
803
962
  maxItems,
804
963
  maxChars,
805
964
  scorer: scoreDocument,
806
965
  measureDocument: measureDocumentChars,
807
- })
808
- : selectFlatContextItems(candidates, maxItems, maxChars);
966
+ });
967
+ } else {
968
+ selection = selectFlatContextItems(candidates, maxItems, maxChars);
969
+ }
809
970
 
810
- const selected = selection.items.map((doc) => ({
811
- id: doc.id,
812
- namespace: doc.namespace,
813
- title: doc.title,
814
- structuredContext: buildStructuredContext(doc),
815
- tags: doc.tags || [],
816
- score: scoreDocument(doc, tokens),
817
- }));
971
+ // The flat + hierarchical paths emit raw docs; summarize-then-expand emits
972
+ // fully-shaped items that already carry structuredContext and a `tier`
973
+ // marker. Detect the shape so we don't double-canonicalize STE items
974
+ // (which would re-expand every summary into full content).
975
+ const selected = selection.items.map((item) => {
976
+ if (item && item.structuredContext) {
977
+ return {
978
+ id: item.id,
979
+ namespace: item.namespace,
980
+ title: item.title,
981
+ structuredContext: item.structuredContext,
982
+ tags: item.tags || [],
983
+ score: typeof item.score === 'number' ? item.score : scoreDocument(item, tokens),
984
+ ...(item.tier ? { tier: item.tier } : {}),
985
+ };
986
+ }
987
+ return {
988
+ id: item.id,
989
+ namespace: item.namespace,
990
+ title: item.title,
991
+ structuredContext: buildStructuredContext(item),
992
+ tags: item.tags || [],
993
+ score: scoreDocument(item, tokens),
994
+ };
995
+ });
818
996
  const usedChars = selection.usedChars;
819
997
  const skippedByMaxChars = selection.skippedByMaxChars;
820
998
 
@@ -848,19 +1026,23 @@ function constructContextPack({ query = '', maxItems = 8, maxChars = 6000, names
848
1026
  };
849
1027
 
850
1028
  appendJsonl(contextFsPath(NAMESPACES.provenance, 'packs.jsonl'), pack);
851
- appendSemanticCacheEntry({
852
- id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
853
- timestamp: nowIso(),
854
- key: buildSemanticCacheKey({
855
- namespaces: normalizedNamespaces,
856
- maxItems,
857
- maxChars,
858
- }),
859
- query,
860
- tokens,
861
- sourceHash,
862
- pack,
863
- });
1029
+ // Symmetric with the cache read: don't persist STE packs into the shared
1030
+ // semantic cache because the cache key is strategy-agnostic.
1031
+ if (effectiveStrategy !== 'summarize-then-expand') {
1032
+ appendSemanticCacheEntry({
1033
+ id: `cache_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1034
+ timestamp: nowIso(),
1035
+ key: buildSemanticCacheKey({
1036
+ namespaces: normalizedNamespaces,
1037
+ maxItems,
1038
+ maxChars,
1039
+ }),
1040
+ query,
1041
+ tokens,
1042
+ sourceHash,
1043
+ pack,
1044
+ });
1045
+ }
864
1046
  recordProvenance({
865
1047
  type: 'context_pack_constructed',
866
1048
  packId,
@@ -243,7 +243,7 @@ function buildIncidentReviewSpec(data) {
243
243
  badge: String(gate.action || 'block').toUpperCase(),
244
244
  tone: gate.action === 'warn' ? 'info' : 'warning',
245
245
  })),
246
- 'No active gates configured.'
246
+ 'No active checks configured.'
247
247
  ),
248
248
  buildList(
249
249
  'Root cause clusters',
@@ -6,16 +6,36 @@ const path = require('path');
6
6
  const { aggregateFailureDiagnostics } = require('./failure-diagnostics');
7
7
  const { AUDIT_LOG_FILENAME } = require('./audit-trail');
8
8
  const { getBillingSummary, loadFunnelLedger, loadResolvedRevenueEvents } = require('./billing');
9
+ const {
10
+ createUnavailableReport,
11
+ loadOptionalModule,
12
+ } = require('./private-core-boundary');
9
13
  const { getTelemetryAnalytics, loadTelemetryEvents } = require('./telemetry-analytics');
10
14
  const { getAutoGatesPath } = require('./auto-promote-gates');
11
- const { summarizeDelegation } = require('./delegation-runtime');
12
15
  const { loadGatesConfig } = require('./gates-engine');
13
16
  const { filterEntriesForWindow, resolveAnalyticsWindow } = require('./analytics-window');
14
17
  const { resolveHostedBillingConfig } = require('./hosted-config');
15
18
  const { generateAgentReadinessReport } = require('./agent-readiness');
16
19
  const { summarizeGateTemplates } = require('./gate-templates');
17
- const { generateOrgDashboard } = require('./org-dashboard');
18
- const { buildPredictiveInsights } = require('./predictive-insights');
20
+ const { buildPredictiveInsights } = loadOptionalModule('./predictive-insights', () => ({
21
+ buildPredictiveInsights: () => ({
22
+ upgradePropensity: {
23
+ pro: { band: 'unavailable', score: 0 },
24
+ team: { band: 'unavailable', score: 0 },
25
+ },
26
+ revenueForecast: {
27
+ predictedBookedRevenueCents: 0,
28
+ incrementalOpportunityCents: 0,
29
+ },
30
+ anomalySummary: {
31
+ count: 0,
32
+ severity: 'none',
33
+ },
34
+ topCreators: [],
35
+ topSources: [],
36
+ ...createUnavailableReport('Predictive insights'),
37
+ }),
38
+ }));
19
39
  const { routeProfile } = require('./profile-router');
20
40
  const { getSettingsStatus } = require('./settings-hierarchy');
21
41
  const { summarizeWorkflowRuns } = require('./workflow-runs');
@@ -28,6 +48,40 @@ const DEFAULT_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.j
28
48
  const LANDING_PAGE_PATH = path.join(PROJECT_ROOT, 'public', 'index.html');
29
49
  const DASHBOARD_REVIEW_STATE_FILE = 'dashboard-review-state.json';
30
50
 
51
+ function loadOrgDashboardModule() {
52
+ const modulePath = path.resolve(__dirname, 'org-dashboard.js');
53
+ if (!fs.existsSync(modulePath)) return null;
54
+ return loadOptionalModule('./org-dashboard', () => ({
55
+ generateOrgDashboard: ({ windowHours } = {}) => buildUnavailableOrgDashboard(windowHours || 24),
56
+ }));
57
+ }
58
+
59
+ function loadDelegationRuntimeModule() {
60
+ const modulePath = path.resolve(__dirname, 'delegation-runtime.js');
61
+ if (!fs.existsSync(modulePath)) return null;
62
+ return require(modulePath);
63
+ }
64
+
65
+ function buildUnavailableOrgDashboard(windowHours) {
66
+ return {
67
+ available: false,
68
+ windowHours,
69
+ totalAgents: 0,
70
+ activeAgents: 0,
71
+ totalToolCalls: 0,
72
+ totalBlocked: 0,
73
+ totalWarned: 0,
74
+ totalAllowed: 0,
75
+ orgAdherenceRate: 100,
76
+ topBlockedGates: [],
77
+ riskAgents: [],
78
+ agents: [],
79
+ proRequired: true,
80
+ upgradeMessage: 'Org dashboard is available only in the private ThumbGate Core runtime (ThumbGate-Core).',
81
+ availability: 'private_core',
82
+ };
83
+ }
84
+
31
85
  // ---------------------------------------------------------------------------
32
86
  // Data readers
33
87
  // ---------------------------------------------------------------------------
@@ -665,6 +719,197 @@ function countCoverage(entries, resolver) {
665
719
  return safeRate(matched, entries.length);
666
720
  }
667
721
 
722
+ function sumCounterValues(counter = {}) {
723
+ return Object.values(counter).reduce((sum, value) => sum + (Number(value) || 0), 0);
724
+ }
725
+
726
+ function sumKeysMatching(counter = {}, matcher) {
727
+ return Object.entries(counter).reduce((sum, entry) => {
728
+ const [key, value] = entry;
729
+ return matcher(key) ? sum + (Number(value) || 0) : sum;
730
+ }, 0);
731
+ }
732
+
733
+ function rankCounter(counter = {}, limit = 5) {
734
+ return Object.entries(counter)
735
+ .sort((a, b) => b[1] - a[1])
736
+ .slice(0, limit)
737
+ .map(([key, count]) => ({ key, count }));
738
+ }
739
+
740
+ function mapBuyerLossTheme(reasonCode) {
741
+ const normalized = String(reasonCode || 'unknown').toLowerCase();
742
+ if (['too_expensive', 'price_shock', 'budget', 'need_budget_approval'].includes(normalized)) {
743
+ return 'pricing';
744
+ }
745
+ if (['need_more_proof', 'trust_gap', 'security_unclear'].includes(normalized)) {
746
+ return 'trust';
747
+ }
748
+ if (['not_ready', 'later', 'just_researching'].includes(normalized)) {
749
+ return 'timing';
750
+ }
751
+ if (['need_team_features', 'need_team_approval'].includes(normalized)) {
752
+ return 'team';
753
+ }
754
+ if (['integration_unclear', 'setup_confusing'].includes(normalized)) {
755
+ return 'integration';
756
+ }
757
+ if (['prefer_oss'].includes(normalized)) {
758
+ return 'open_source';
759
+ }
760
+ return 'unknown';
761
+ }
762
+
763
+ function buildLossAnalysis(analytics) {
764
+ const telemetry = analytics.telemetry || {};
765
+ const visitors = telemetry.visitors || {};
766
+ const ctas = telemetry.ctas || {};
767
+ const behavior = telemetry.behavior || {};
768
+ const buyerLoss = analytics.buyerLoss || {};
769
+ const conversionFunnel = telemetry.conversionFunnel || {};
770
+ const runtimeConfig = resolveHostedBillingConfig();
771
+ const monthlyPriceCents = Math.round((Number(runtimeConfig.proPriceDollars) || 19) * 100);
772
+ const pageViews = visitors.pageViews || 0;
773
+ const checkoutStarts = ctas.checkoutStarts || 0;
774
+ const paidOrders = analytics.funnel ? analytics.funnel.paidOrders || 0 : 0;
775
+ const trialEmails = conversionFunnel.trialEmails || 0;
776
+ const explicitReasons = buyerLoss.reasonsByCode || {};
777
+ const reasonThemes = {};
778
+ Object.entries(explicitReasons).forEach(([reasonCode, count]) => {
779
+ const theme = mapBuyerLossTheme(reasonCode);
780
+ reasonThemes[theme] = (reasonThemes[theme] || 0) + count;
781
+ });
782
+
783
+ const proImpressions = sumKeysMatching(behavior.ctaImpressionsById || {}, (key) => /pro|pricing/i.test(key));
784
+ const proClicks = sumKeysMatching(ctas.byId || {}, (key) => /pro|pricing/i.test(key));
785
+ const pricingViews = sumKeysMatching(behavior.sectionViewsById || {}, (key) => /pricing/i.test(key));
786
+ const proofViews = sumKeysMatching(behavior.sectionViewsById || {}, (key) => /proof/i.test(key));
787
+ const exitsBeforePricing = sumKeysMatching(behavior.exitsByLastVisibleSection || {}, (key) => !/pricing|faq/i.test(key));
788
+ const checkoutLossCount = Math.max(0, checkoutStarts - paidOrders);
789
+
790
+ const stageDropoff = [
791
+ {
792
+ key: 'landing_to_checkout',
793
+ stage: 'landing',
794
+ lostCount: Math.max(0, pageViews - checkoutStarts),
795
+ rate: safeRate(Math.max(0, pageViews - checkoutStarts), pageViews),
796
+ },
797
+ {
798
+ key: 'cta_impression_to_click',
799
+ stage: 'message',
800
+ lostCount: Math.max(0, proImpressions - proClicks),
801
+ rate: safeRate(Math.max(0, proImpressions - proClicks), proImpressions),
802
+ },
803
+ {
804
+ key: 'email_focus_to_capture',
805
+ stage: 'lead_capture',
806
+ lostCount: Math.max(0, (behavior.emailFocusEvents || 0) - trialEmails),
807
+ rate: safeRate(Math.max(0, (behavior.emailFocusEvents || 0) - trialEmails), behavior.emailFocusEvents || 0),
808
+ },
809
+ {
810
+ key: 'checkout_to_paid',
811
+ stage: 'checkout',
812
+ lostCount: checkoutLossCount,
813
+ rate: safeRate(checkoutLossCount, checkoutStarts),
814
+ },
815
+ ].sort((a, b) => b.lostCount - a.lostCount);
816
+
817
+ const inferredCauses = [];
818
+ if (exitsBeforePricing > 0) {
819
+ inferredCauses.push({
820
+ key: 'message_drop_before_pricing',
821
+ stage: 'landing',
822
+ count: exitsBeforePricing,
823
+ evidence: {
824
+ topExitSection: behavior.topExitSection,
825
+ pricingViews,
826
+ pageExits: behavior.pageExits || 0,
827
+ },
828
+ });
829
+ }
830
+ if (proImpressions > 0 && proClicks < proImpressions) {
831
+ inferredCauses.push({
832
+ key: 'weak_pricing_cta_response',
833
+ stage: 'message',
834
+ count: Math.max(0, proImpressions - proClicks),
835
+ evidence: {
836
+ proImpressions,
837
+ proClicks,
838
+ impressionToClickRate: safeRate(proClicks, proImpressions),
839
+ },
840
+ });
841
+ }
842
+ if ((behavior.emailAbandonEvents || 0) > 0) {
843
+ inferredCauses.push({
844
+ key: 'email_capture_friction',
845
+ stage: 'lead_capture',
846
+ count: behavior.emailAbandonEvents || 0,
847
+ evidence: {
848
+ emailFocusEvents: behavior.emailFocusEvents || 0,
849
+ emailAbandonEvents: behavior.emailAbandonEvents || 0,
850
+ emailAbandonRate: behavior.emailAbandonRate || 0,
851
+ },
852
+ });
853
+ }
854
+ if (checkoutLossCount > 0 || ctas.checkoutFailures || ctas.lookupFailures || ctas.checkoutCancelled || ctas.checkoutAbandoned) {
855
+ inferredCauses.push({
856
+ key: 'checkout_friction',
857
+ stage: 'checkout',
858
+ count: checkoutLossCount,
859
+ evidence: {
860
+ checkoutStarts,
861
+ paidOrders,
862
+ checkoutCancelled: ctas.checkoutCancelled || 0,
863
+ checkoutAbandoned: ctas.checkoutAbandoned || 0,
864
+ checkoutFailures: ctas.checkoutFailures || 0,
865
+ lookupFailures: ctas.lookupFailures || 0,
866
+ },
867
+ });
868
+ }
869
+ const topTheme = rankCounter(reasonThemes, 1)[0] || null;
870
+ if (topTheme) {
871
+ inferredCauses.push({
872
+ key: `explicit_${topTheme.key}`,
873
+ stage: topTheme.key === 'pricing' ? 'pricing' : 'objection',
874
+ count: topTheme.count,
875
+ evidence: {
876
+ theme: topTheme.key,
877
+ topReasons: rankCounter(explicitReasons, 3),
878
+ },
879
+ });
880
+ }
881
+
882
+ inferredCauses.sort((a, b) => b.count - a.count);
883
+
884
+ return {
885
+ primaryIssue: inferredCauses[0] || null,
886
+ stageDropoff,
887
+ inferredCauses,
888
+ explicitThemes: rankCounter(reasonThemes, 6),
889
+ explicitReasons: rankCounter(explicitReasons, 6),
890
+ behaviorSignals: {
891
+ topViewedSection: behavior.topViewedSection || null,
892
+ topExitSection: behavior.topExitSection || null,
893
+ topExitDwellBucket: behavior.topExitDwellBucket || null,
894
+ topImpressionCta: behavior.topImpressionCta || null,
895
+ pricingViews,
896
+ proofViews,
897
+ pageExits: behavior.pageExits || 0,
898
+ exitsBeforePricing,
899
+ averageExitEngagementMs: behavior.averageExitEngagementMs || 0,
900
+ averageExitScrollPercent: behavior.averageExitScrollPercent || 0,
901
+ emailFocusEvents: behavior.emailFocusEvents || 0,
902
+ emailAbandonEvents: behavior.emailAbandonEvents || 0,
903
+ },
904
+ revenueOpportunity: {
905
+ currentMonthlyPriceCents: monthlyPriceCents,
906
+ checkoutLossCount,
907
+ explicitBuyerLossCount: buyerLoss.totalSignals || 0,
908
+ opportunityAtCurrentMonthlyPriceCents: checkoutLossCount * monthlyPriceCents,
909
+ },
910
+ };
911
+ }
912
+
668
913
  function computeAnalyticsSummary(feedbackDir, options = {}) {
669
914
  const analyticsWindow = resolveAnalyticsWindow(options.analyticsWindow || options);
670
915
  const telemetryEntries = filterEntriesForWindow(
@@ -749,6 +994,13 @@ function computeAnalyticsSummary(feedbackDir, options = {}) {
749
994
  topSurface: null,
750
995
  topQuery: null,
751
996
  },
997
+ trackedLinks: telemetry.trackedLinks || {
998
+ totalHits: 0,
999
+ totalCheckoutStarts: 0,
1000
+ overallConversionRate: 0,
1001
+ bySlug: {},
1002
+ topSlug: null,
1003
+ },
752
1004
  efficiency,
753
1005
  revenue: billing.revenue || {
754
1006
  paidProviderEvents: 0,
@@ -1150,9 +1402,19 @@ function generateDashboard(feedbackDir, options = {}) {
1150
1402
  analyticsWindow,
1151
1403
  billingSummary,
1152
1404
  });
1405
+ analytics.lossAnalysis = buildLossAnalysis(analytics);
1153
1406
  const observability = computeObservabilityStats(diagnosticEntries, diagnostics, secretGuard, analytics.telemetry);
1154
1407
  const instrumentation = computeInstrumentationReadiness(analytics, billingSummary);
1155
- const delegation = summarizeDelegation(feedbackDir);
1408
+ const delegationRuntime = loadDelegationRuntimeModule();
1409
+ const delegation = delegationRuntime
1410
+ ? delegationRuntime.summarizeDelegation(feedbackDir)
1411
+ : {
1412
+ totalHandoffs: 0,
1413
+ successfulHandoffs: 0,
1414
+ blockedHandoffs: 0,
1415
+ activePlans: [],
1416
+ availability: 'private_core',
1417
+ };
1156
1418
  const readiness = generateAgentReadinessReport({ projectRoot: PROJECT_ROOT });
1157
1419
  const harness = computeHarnessOverview(feedbackDir, entries);
1158
1420
  const interventionPolicy = getInterventionPolicySummary(feedbackDir);
@@ -1219,11 +1481,15 @@ function generateDashboard(feedbackDir, options = {}) {
1219
1481
  day.lessons = lessonPipeline.lessonsByDay.get(day.dayKey) || 0;
1220
1482
  }
1221
1483
 
1222
- const team = generateOrgDashboard({
1223
- windowHours: resolveTeamWindowHours(analyticsWindow),
1224
- authContext: options.authContext,
1225
- proOverride: options.teamProOverride,
1226
- });
1484
+ const teamWindowHours = resolveTeamWindowHours(analyticsWindow);
1485
+ const orgDashboard = loadOrgDashboardModule();
1486
+ const team = orgDashboard
1487
+ ? orgDashboard.generateOrgDashboard({
1488
+ windowHours: teamWindowHours,
1489
+ authContext: options.authContext,
1490
+ proOverride: options.teamProOverride,
1491
+ })
1492
+ : buildUnavailableOrgDashboard(teamWindowHours);
1227
1493
  const templateLibrary = summarizeGateTemplates();
1228
1494
  const predictive = buildPredictiveInsights({
1229
1495
  telemetryAnalytics: analytics.telemetry,
@@ -192,8 +192,18 @@ function collapseDecisionTimeline(records) {
192
192
  });
193
193
  }
194
194
 
195
- function initializeDaySeries(dayCount) {
196
- const today = new Date();
195
+ function resolveNow(now) {
196
+ if (now instanceof Date) return new Date(now.getTime());
197
+ if (typeof now === 'number' && Number.isFinite(now)) return new Date(now);
198
+ if (typeof now === 'string' && now) {
199
+ const parsed = new Date(now);
200
+ if (!Number.isNaN(parsed.getTime())) return parsed;
201
+ }
202
+ return new Date();
203
+ }
204
+
205
+ function initializeDaySeries(dayCount, now) {
206
+ const today = resolveNow(now);
197
207
  today.setHours(0, 0, 0, 0);
198
208
  const days = [];
199
209
  for (let offset = dayCount - 1; offset >= 0; offset -= 1) {
@@ -224,7 +234,7 @@ function computeDecisionMetrics(feedbackDir, options = {}) {
224
234
  const dayCount = Number.isInteger(options.dayCount) ? options.dayCount : DEFAULT_DAY_COUNT;
225
235
  const records = readDecisionLog(getDecisionLogPath(feedbackDir));
226
236
  const actions = collapseDecisionTimeline(records).filter((entry) => entry.evaluation);
227
- const series = initializeDaySeries(dayCount);
237
+ const series = initializeDaySeries(dayCount, options.now);
228
238
  const dayMap = new Map(series.map((day) => [day.dayKey, day]));
229
239
  const outcomeCounts = {
230
240
  accepted: 0,