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.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +5 -5
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +60 -35
- package/adapters/chatgpt/openapi.yaml +118 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +217 -84
- package/adapters/opencode/opencode.json +1 -1
- package/bench/prompt-eval-suite.json +5 -1
- package/bin/cli.js +211 -8
- package/config/enforcement.json +59 -7
- package/config/evals/agent-safety-eval.json +338 -22
- package/config/gates/default.json +33 -0
- package/config/gates/routine.json +43 -0
- package/config/github-about.json +3 -3
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +2 -1
- package/config/model-candidates.json +131 -0
- package/openapi/openapi.yaml +118 -2
- package/package.json +70 -51
- package/public/blog.html +7 -7
- package/public/codex-plugin.html +13 -7
- package/public/compare.html +29 -23
- package/public/dashboard.html +105 -12
- package/public/guide.html +28 -28
- package/public/index.html +233 -97
- package/public/learn.html +87 -20
- package/public/lessons.html +26 -2
- package/public/numbers.html +271 -0
- package/public/pro.html +89 -19
- package/scripts/agent-audit-trace.js +55 -0
- package/scripts/agent-memory-lifecycle.js +96 -0
- package/scripts/agent-readiness-plan.js +118 -0
- package/scripts/agentic-data-pipeline.js +21 -1
- package/scripts/agents-sdk-sandbox-plan.js +57 -0
- package/scripts/ai-org-governance.js +98 -0
- package/scripts/ai-search-distribution.js +43 -0
- package/scripts/artifact-agent-plan.js +81 -0
- package/scripts/billing.js +27 -8
- package/scripts/cli-feedback.js +2 -1
- package/scripts/cli-schema.js +60 -5
- package/scripts/code-mode-mcp-plan.js +71 -0
- package/scripts/commercial-offer.js +1 -1
- package/scripts/context-engine.js +1 -2
- package/scripts/context-manager.js +4 -1
- package/scripts/contextfs.js +214 -32
- package/scripts/dashboard-render-spec.js +1 -1
- package/scripts/dashboard.js +275 -9
- package/scripts/decision-journal.js +13 -3
- package/scripts/document-workflow-governance.js +62 -0
- package/scripts/enterprise-agent-rollout.js +34 -0
- package/scripts/experience-replay-governance.js +69 -0
- package/scripts/export-hf-dataset.js +1 -1
- package/scripts/feedback-loop.js +141 -9
- package/scripts/feedback-to-rules.js +17 -23
- package/scripts/gates-engine.js +4 -6
- package/scripts/growth-campaigns.js +49 -0
- package/scripts/harness-selector.js +145 -1
- package/scripts/hybrid-supervisor-agent.js +64 -0
- package/scripts/inference-cache-policy.js +72 -0
- package/scripts/inference-economics.js +53 -0
- package/scripts/internal-agent-bootstrap.js +12 -2
- package/scripts/knowledge-layer-plan.js +108 -0
- package/scripts/lesson-canonical.js +181 -0
- package/scripts/lesson-db.js +71 -10
- package/scripts/lesson-inference.js +183 -44
- package/scripts/lesson-search.js +4 -1
- package/scripts/lesson-synthesis.js +23 -2
- package/scripts/llm-client.js +157 -26
- package/scripts/mailer/resend-mailer.js +112 -1
- package/scripts/mcp-transport-strategy.js +66 -0
- package/scripts/memory-store-governance.js +60 -0
- package/scripts/meta-agent-loop.js +7 -13
- package/scripts/model-access-eligibility.js +38 -0
- package/scripts/model-migration-readiness.js +55 -0
- package/scripts/native-messaging-audit.js +514 -0
- package/scripts/operational-integrity.js +96 -3
- package/scripts/otel-declarative-config.js +56 -0
- package/scripts/perplexity-client.js +1 -1
- package/scripts/post-training-governance.js +34 -0
- package/scripts/pr-manager.js +47 -7
- package/scripts/private-core-boundary.js +72 -0
- package/scripts/production-agent-readiness.js +40 -0
- package/scripts/profile-router.js +16 -1
- package/scripts/prompt-eval.js +564 -32
- package/scripts/prompt-programs.js +93 -0
- package/scripts/provider-action-normalizer.js +585 -0
- package/scripts/rule-validator.js +285 -0
- package/scripts/scaling-law-claims.js +60 -0
- package/scripts/security-scanner.js +1 -1
- package/scripts/self-distill-agent.js +7 -32
- package/scripts/seo-gsd.js +400 -43
- package/scripts/skill-rag-router.js +53 -0
- package/scripts/spec-gate.js +1 -1
- package/scripts/student-consistent-training.js +73 -0
- package/scripts/synthetic-data-provenance.js +98 -0
- package/scripts/task-context-result.js +81 -0
- package/scripts/telemetry-analytics.js +149 -0
- package/scripts/thompson-sampling.js +2 -2
- package/scripts/token-savings.js +7 -6
- package/scripts/token-tco.js +46 -0
- package/scripts/tool-registry.js +75 -3
- package/scripts/verification-loop.js +10 -1
- package/scripts/verifier-scoring.js +71 -0
- package/scripts/workflow-sentinel.js +284 -28
- package/scripts/workspace-agent-routines.js +118 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +434 -120
- package/.claude-plugin/README.md +0 -170
- package/adapters/README.md +0 -12
- package/scripts/analytics-report.js +0 -328
- package/scripts/autonomous-workflow.js +0 -377
- package/scripts/billing-setup.js +0 -109
- package/scripts/creator-campaigns.js +0 -239
- package/scripts/cross-encoder-reranker.js +0 -235
- package/scripts/daemon-manager.js +0 -108
- package/scripts/decision-trace.js +0 -354
- package/scripts/delegation-runtime.js +0 -896
- package/scripts/dispatch-brief.js +0 -159
- package/scripts/distribution-surfaces.js +0 -110
- package/scripts/feedback-history-distiller.js +0 -382
- package/scripts/funnel-analytics.js +0 -35
- package/scripts/history-distiller.js +0 -200
- package/scripts/hosted-job-launcher.js +0 -256
- package/scripts/intent-router.js +0 -392
- package/scripts/lesson-reranker.js +0 -263
- package/scripts/lesson-retrieval.js +0 -148
- package/scripts/managed-lesson-agent.js +0 -183
- package/scripts/operational-dashboard.js +0 -103
- package/scripts/operational-summary.js +0 -129
- package/scripts/operator-artifacts.js +0 -608
- package/scripts/optimize-context.js +0 -17
- package/scripts/org-dashboard.js +0 -206
- package/scripts/partner-orchestration.js +0 -146
- package/scripts/predictive-insights.js +0 -356
- package/scripts/pulse.js +0 -80
- package/scripts/reflector-agent.js +0 -221
- package/scripts/sales-pipeline.js +0 -681
- package/scripts/session-episode-store.js +0 -329
- package/scripts/session-health-sensor.js +0 -242
- package/scripts/session-report.js +0 -120
- package/scripts/swarm-coordinator.js +0 -81
- package/scripts/tool-kpi-tracker.js +0 -12
- package/scripts/webhook-delivery.js +0 -62
- package/scripts/workflow-sprint-intake.js +0 -475
- package/skills/agent-memory/SKILL.md +0 -97
- package/skills/solve-architecture-autonomy/SKILL.md +0 -17
- package/skills/solve-architecture-autonomy/tool.js +0 -33
- package/skills/thumbgate-feedback/SKILL.md +0 -49
package/scripts/contextfs.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
800
|
-
|
|
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
|
-
|
|
966
|
+
});
|
|
967
|
+
} else {
|
|
968
|
+
selection = selectFlatContextItems(candidates, maxItems, maxChars);
|
|
969
|
+
}
|
|
809
970
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
|
246
|
+
'No active checks configured.'
|
|
247
247
|
),
|
|
248
248
|
buildList(
|
|
249
249
|
'Root cause clusters',
|
package/scripts/dashboard.js
CHANGED
|
@@ -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 {
|
|
18
|
-
|
|
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
|
|
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
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
|
196
|
-
|
|
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,
|