thumbgate 1.27.18 → 1.27.20

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 (170) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +4 -3
  9. package/.well-known/agentic-verify.txt +1 -0
  10. package/.well-known/llms.txt +33 -12
  11. package/.well-known/mcp/server-card.json +8 -8
  12. package/README.md +246 -30
  13. package/adapters/claude/.mcp.json +2 -2
  14. package/adapters/codex/config.toml +2 -2
  15. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  16. package/adapters/gemini/function-declarations.json +1 -0
  17. package/adapters/letta/README.md +41 -0
  18. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  19. package/adapters/mcp/server-stdio.js +263 -11
  20. package/adapters/opencode/opencode.json +1 -1
  21. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  22. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  23. package/bench/observability-eval-suite.json +26 -0
  24. package/bench/thumbgate-bench.json +2 -2
  25. package/bin/cli.js +1456 -122
  26. package/bin/dashboard-cli.js +7 -0
  27. package/bin/postinstall.js +11 -27
  28. package/commands/dashboard.md +15 -0
  29. package/commands/thumbgate-dashboard.md +15 -0
  30. package/config/gate-classifier-routing.json +98 -0
  31. package/config/gate-templates.json +216 -0
  32. package/config/gates/claim-verification.json +12 -0
  33. package/config/gates/default.json +31 -2
  34. package/config/github-about.json +2 -2
  35. package/config/mcp-allowlists.json +23 -13
  36. package/config/merge-quality-checks.json +0 -1
  37. package/config/model-candidates.json +121 -6
  38. package/config/post-deploy-marketing-pages.json +80 -0
  39. package/config/tessl-tiles.json +1 -3
  40. package/openapi/openapi.yaml +12 -0
  41. package/package.json +225 -100
  42. package/public/about.html +162 -0
  43. package/public/agent-manager.html +179 -0
  44. package/public/agents-cost-savings.html +153 -0
  45. package/public/ai-malpractice-prevention.html +818 -0
  46. package/public/assets/brand/github-social-preview.png +0 -0
  47. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  48. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  49. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  50. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  51. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  52. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  53. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  54. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  55. package/public/assets/brand/thumbgate-mark.svg +21 -0
  56. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  57. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  58. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  59. package/public/assets/legal-intake-control-flow.svg +66 -0
  60. package/public/blog.html +4 -4
  61. package/public/brand/thumbgate-mark.svg +19 -0
  62. package/public/brand/thumbgate-og.svg +16 -0
  63. package/public/chatgpt-app.html +330 -0
  64. package/public/codex-enterprise.html +123 -0
  65. package/public/codex-plugin.html +72 -20
  66. package/public/compare.html +31 -8
  67. package/public/dashboard.html +930 -166
  68. package/public/diagnostic.html +345 -0
  69. package/public/federal.html +2 -2
  70. package/public/guide.html +33 -13
  71. package/public/index.html +469 -111
  72. package/public/install.html +193 -0
  73. package/public/js/buyer-intent.js +672 -0
  74. package/public/learn.html +183 -18
  75. package/public/lessons.html +168 -10
  76. package/public/numbers.html +7 -7
  77. package/public/pricing.html +399 -0
  78. package/public/pro.html +34 -11
  79. package/scripts/action-receipts.js +324 -0
  80. package/scripts/activation-quickstart.js +187 -0
  81. package/scripts/agent-memory-lifecycle.js +211 -0
  82. package/scripts/agent-operations-planner.js +621 -0
  83. package/scripts/agent-readiness.js +20 -3
  84. package/scripts/agent-reward-model.js +53 -1
  85. package/scripts/ai-component-inventory.js +367 -0
  86. package/scripts/async-eval-observability.js +236 -0
  87. package/scripts/audit.js +65 -0
  88. package/scripts/auto-promote-gates.js +82 -10
  89. package/scripts/auto-wire-hooks.js +14 -0
  90. package/scripts/aws-blocks-guardrails.js +272 -0
  91. package/scripts/billing.js +93 -1
  92. package/scripts/bot-detection.js +61 -3
  93. package/scripts/build-metadata.js +50 -10
  94. package/scripts/classifier-routing.js +130 -0
  95. package/scripts/cli-feedback.js +4 -2
  96. package/scripts/cli-schema.js +97 -0
  97. package/scripts/cli-telemetry.js +6 -1
  98. package/scripts/commercial-offer.js +82 -2
  99. package/scripts/context-manager.js +74 -6
  100. package/scripts/dashboard-chat.js +332 -0
  101. package/scripts/dashboard.js +68 -2
  102. package/scripts/export-databricks-bundle.js +5 -1
  103. package/scripts/export-dpo-pairs.js +7 -2
  104. package/scripts/feedback-aggregate.js +281 -0
  105. package/scripts/feedback-loop.js +123 -1
  106. package/scripts/feedback-quality.js +87 -0
  107. package/scripts/feedback-sanitizer.js +105 -0
  108. package/scripts/filesystem-search.js +35 -10
  109. package/scripts/gate-stats.js +89 -0
  110. package/scripts/gates-engine.js +1176 -85
  111. package/scripts/gemini-embedding-policy.js +2 -1
  112. package/scripts/hook-runtime.js +20 -14
  113. package/scripts/hook-stop-anti-claim.js +301 -0
  114. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  115. package/scripts/hybrid-feedback-context.js +142 -7
  116. package/scripts/install-shim.js +87 -0
  117. package/scripts/lesson-inference.js +8 -3
  118. package/scripts/lesson-search.js +17 -1
  119. package/scripts/license.js +10 -10
  120. package/scripts/llm-client.js +169 -4
  121. package/scripts/local-model-profile.js +15 -8
  122. package/scripts/mcp-config.js +7 -1
  123. package/scripts/mcp-oauth.js +293 -0
  124. package/scripts/memory-scope-readiness.js +159 -0
  125. package/scripts/meta-agent-loop.js +36 -0
  126. package/scripts/noop-detect.js +285 -0
  127. package/scripts/operational-integrity.js +39 -5
  128. package/scripts/oss-pr-opportunity-scout.js +35 -5
  129. package/scripts/parallel-workflow-orchestrator.js +293 -0
  130. package/scripts/plan-gate.js +243 -0
  131. package/scripts/plausible-domain-config.js +99 -0
  132. package/scripts/plausible-server-events.js +9 -6
  133. package/scripts/pro-local-dashboard.js +4 -4
  134. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  135. package/scripts/published-cli.js +0 -8
  136. package/scripts/qa-scenario-planner.js +136 -0
  137. package/scripts/rate-limiter.js +64 -13
  138. package/scripts/repeat-metric.js +137 -0
  139. package/scripts/secret-fixture-tokens.js +61 -0
  140. package/scripts/secret-redaction.js +166 -0
  141. package/scripts/secret-scanner.js +44 -5
  142. package/scripts/security-scanner.js +260 -10
  143. package/scripts/self-distill-agent.js +3 -1
  144. package/scripts/self-harness-optimizer.js +141 -0
  145. package/scripts/self-healing-check.js +193 -0
  146. package/scripts/self-protection.js +90 -0
  147. package/scripts/seo-gsd.js +916 -7
  148. package/scripts/silent-failure-cluster.js +531 -0
  149. package/scripts/statusline-cache-path.js +17 -2
  150. package/scripts/statusline-cache-read.js +57 -0
  151. package/scripts/statusline-local-stats.js +9 -1
  152. package/scripts/statusline-meta.js +28 -2
  153. package/scripts/statusline.sh +20 -4
  154. package/scripts/sync-telemetry-from-prod.js +374 -0
  155. package/scripts/telemetry-analytics.js +357 -0
  156. package/scripts/thompson-sampling.js +31 -10
  157. package/scripts/thumbgate-bench.js +16 -1
  158. package/scripts/thumbgate-search.js +85 -19
  159. package/scripts/tool-contract-validator.js +76 -0
  160. package/scripts/tool-registry.js +169 -1
  161. package/scripts/trajectory-scorer.js +63 -0
  162. package/scripts/vector-store.js +45 -0
  163. package/scripts/verify-marketing-pages-deployed.js +212 -0
  164. package/scripts/visitor-journey.js +172 -0
  165. package/scripts/workflow-sentinel.js +286 -53
  166. package/scripts/workspace-evolver.js +62 -2
  167. package/src/api/server.js +2683 -319
  168. package/.claude-plugin/marketplace.json +0 -85
  169. package/adapters/chatgpt/openapi.yaml +0 -1695
  170. package/scripts/bot-detector.js +0 -50
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * mcp-oauth.js — OAuth 2.1 (PKCE) authorization-server machinery for the remote
6
+ * ThumbGate MCP connector, required by the Claude Connectors Directory
7
+ * ("Use OAuth 2.0 for authenticated services").
8
+ *
9
+ * Pure, dependency-free, IO-free functions + an injectable store, so the full
10
+ * flow is unit-testable without a network. The HTTP wiring (in src/api/server.js)
11
+ * is a thin shell over these.
12
+ *
13
+ * Implements:
14
+ * - RFC 9728 protected-resource metadata
15
+ * - RFC 8414 authorization-server metadata
16
+ * - RFC 7591 dynamic client registration (open registration)
17
+ * - Authorization-code grant with mandatory PKCE S256 (RFC 7636)
18
+ * - Bearer access tokens bound to a ThumbGate API key (so existing gating is unchanged)
19
+ *
20
+ * SECURITY NOTES:
21
+ * - PKCE S256 is mandatory; the plain method is rejected.
22
+ * - Auth codes are single-use and short-lived (60s); access tokens TTL-bound.
23
+ * - redirect_uri is matched exactly against the registered set.
24
+ * - The token never exposes the bound ThumbGate key; it maps server-side only.
25
+ */
26
+
27
+ const crypto = require('crypto');
28
+
29
+ const AUTH_CODE_TTL_MS = 60 * 1000; // 1 minute
30
+ const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
31
+ const DEFAULT_SCOPE = 'mcp:read mcp:write';
32
+
33
+ // Upper bounds on the in-memory store. The registration and authorization
34
+ // endpoints are reachable pre-auth, so without a cap a malicious caller could
35
+ // grow these Maps unboundedly and exhaust server memory. When a Map is full we
36
+ // evict the oldest entry (FIFO) rather than deny service to legitimate clients.
37
+ const MAX_CLIENTS = 10000;
38
+ const MAX_CODES = 10000;
39
+ const MAX_TOKENS = 50000;
40
+
41
+ function now() {
42
+ return Date.now();
43
+ }
44
+
45
+ function randomToken(bytes = 32) {
46
+ return crypto.randomBytes(bytes).toString('base64url');
47
+ }
48
+
49
+ function base64UrlSha256(input) {
50
+ return crypto.createHash('sha256').update(String(input)).digest('base64url');
51
+ }
52
+
53
+ /**
54
+ * Create a fresh in-memory store.
55
+ *
56
+ * DURABILITY (known limitation): this uses plain Maps, so a process restart or a
57
+ * multi-instance / load-balanced deployment will drop issued tokens and
58
+ * registered clients (clients see 401s, in-flight authorizations break). For
59
+ * single-instance use this is fine; production multi-tenancy needs a durable,
60
+ * shared backing (Redis/DB) — tracked as the per-user-data-scoping follow-up.
61
+ * Entry counts are bounded (see MAX_* and capInsert) to prevent memory
62
+ * exhaustion from anonymous calls to the registration/authorization endpoints.
63
+ */
64
+ function createStore() {
65
+ return {
66
+ clients: new Map(),
67
+ codes: new Map(),
68
+ tokens: new Map(),
69
+ };
70
+ }
71
+
72
+ /** Insert into a Map, evicting the oldest entry (FIFO) once `max` is reached. */
73
+ function capInsert(map, key, value, max) {
74
+ if (map.size >= max && !map.has(key)) {
75
+ const oldest = map.keys().next().value;
76
+ if (oldest !== undefined) map.delete(oldest);
77
+ }
78
+ map.set(key, value);
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Metadata (RFC 9728 / RFC 8414)
83
+ // ---------------------------------------------------------------------------
84
+
85
+ function trimSlash(u) {
86
+ // Non-regex trailing-slash strip (avoids a SonarCloud S5852 false-positive on a
87
+ // provably-linear pattern).
88
+ let s = String(u || '');
89
+ while (s.endsWith('/')) s = s.slice(0, -1);
90
+ return s;
91
+ }
92
+
93
+ function buildProtectedResourceMetadata(baseUrl) {
94
+ const base = trimSlash(baseUrl);
95
+ return {
96
+ resource: `${base}/mcp`,
97
+ authorization_servers: [base],
98
+ bearer_methods_supported: ['header'],
99
+ scopes_supported: ['mcp:read', 'mcp:write'],
100
+ resource_documentation: `${base}/docs/connectors`,
101
+ };
102
+ }
103
+
104
+ function buildAuthServerMetadata(baseUrl) {
105
+ const base = trimSlash(baseUrl);
106
+ return {
107
+ issuer: base,
108
+ authorization_endpoint: `${base}/oauth/authorize`,
109
+ token_endpoint: `${base}/oauth/token`,
110
+ registration_endpoint: `${base}/oauth/register`,
111
+ scopes_supported: ['mcp:read', 'mcp:write'],
112
+ response_types_supported: ['code'],
113
+ grant_types_supported: ['authorization_code'],
114
+ code_challenge_methods_supported: ['S256'],
115
+ token_endpoint_auth_methods_supported: ['none'],
116
+ };
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Dynamic client registration (RFC 7591)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ // The MCP authorization spec is explicit: "All redirect URIs MUST be either
124
+ // `localhost` or use HTTPS." We therefore accept only HTTPS and loopback
125
+ // (http://localhost | http://127.0.0.1) and reject every other scheme —
126
+ // including native-app custom schemes (myapp://, intent://, etc.), which the MCP
127
+ // profile does not sanction and the real client (Claude) does not use.
128
+ function isAllowedRedirectUri(uri) {
129
+ const u = String(uri || '');
130
+ if (/^https:\/\//i.test(u)) return true;
131
+ if (/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/i.test(u)) return true;
132
+ return false;
133
+ }
134
+
135
+ function registerClient(store, body = {}) {
136
+ const redirectUris = Array.isArray(body.redirect_uris) ? body.redirect_uris.filter(Boolean) : [];
137
+ if (redirectUris.length === 0) {
138
+ return { error: 'invalid_redirect_uri', error_description: 'redirect_uris is required' };
139
+ }
140
+ for (const uri of redirectUris) {
141
+ if (!isAllowedRedirectUri(uri)) {
142
+ return { error: 'invalid_redirect_uri', error_description: `unsupported redirect_uri: ${uri}` };
143
+ }
144
+ }
145
+ const clientId = `tg_${randomToken(16)}`;
146
+ const record = {
147
+ client_id: clientId,
148
+ redirect_uris: redirectUris,
149
+ token_endpoint_auth_method: 'none',
150
+ grant_types: ['authorization_code'],
151
+ response_types: ['code'],
152
+ client_name: typeof body.client_name === 'string' ? body.client_name.slice(0, 200) : 'mcp-client',
153
+ created_at: now(),
154
+ };
155
+ capInsert(store.clients, clientId, record, MAX_CLIENTS);
156
+ return record;
157
+ }
158
+
159
+ function getClient(store, clientId) {
160
+ return store.clients.get(clientId) || null;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Authorization code (PKCE S256)
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Issue an auth code. `boundKey` is the ThumbGate API key the resulting access
169
+ * token will act as (resolved by the authorize step once the user consents).
170
+ */
171
+ function createAuthorizationCode(store, {
172
+ clientId, redirectUri, codeChallenge, codeChallengeMethod, scope, boundKey, state, resource,
173
+ } = {}) {
174
+ const client = getClient(store, clientId);
175
+ if (!client) return { error: 'invalid_client' };
176
+ if (!client.redirect_uris.includes(redirectUri)) return { error: 'invalid_request', error_description: 'redirect_uri mismatch' };
177
+ if (codeChallengeMethod !== 'S256') return { error: 'invalid_request', error_description: 'code_challenge_method must be S256' };
178
+ if (!codeChallenge || String(codeChallenge).length < 16) return { error: 'invalid_request', error_description: 'code_challenge required' };
179
+
180
+ const code = randomToken(24);
181
+ capInsert(store.codes, code, {
182
+ clientId,
183
+ redirectUri,
184
+ codeChallenge,
185
+ scope: scope || DEFAULT_SCOPE,
186
+ boundKey: boundKey || '',
187
+ resource: resource || '', // RFC 8707 resource indicator (the MCP server URL)
188
+ expiresAt: now() + AUTH_CODE_TTL_MS,
189
+ used: false,
190
+ }, MAX_CODES);
191
+ return { code, state };
192
+ }
193
+
194
+ function verifyPkce(codeChallenge, codeVerifier) {
195
+ if (!codeChallenge || !codeVerifier) return false;
196
+ // RFC 7636: verifier 43–128 chars.
197
+ if (String(codeVerifier).length < 43 || String(codeVerifier).length > 128) return false;
198
+ try {
199
+ return crypto.timingSafeEqual(
200
+ Buffer.from(base64UrlSha256(codeVerifier)),
201
+ Buffer.from(String(codeChallenge)),
202
+ );
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Token exchange + validation
210
+ // ---------------------------------------------------------------------------
211
+
212
+ function exchangeCode(store, { code, codeVerifier, clientId, redirectUri, resource } = {}) {
213
+ const entry = store.codes.get(code);
214
+ if (!entry) return { error: 'invalid_grant', error_description: 'unknown code' };
215
+ // Single-use + expiry: consume regardless of outcome.
216
+ store.codes.delete(code);
217
+ if (entry.used) return { error: 'invalid_grant', error_description: 'code already used' };
218
+ if (now() > entry.expiresAt) return { error: 'invalid_grant', error_description: 'code expired' };
219
+ if (entry.clientId !== clientId) return { error: 'invalid_grant', error_description: 'client mismatch' };
220
+ if (entry.redirectUri !== redirectUri) return { error: 'invalid_grant', error_description: 'redirect_uri mismatch' };
221
+ if (!verifyPkce(entry.codeChallenge, codeVerifier)) return { error: 'invalid_grant', error_description: 'PKCE verification failed' };
222
+ // RFC 8707: the resource at token time must match the one bound at authorize time.
223
+ if (entry.resource && resource && entry.resource !== resource) {
224
+ return { error: 'invalid_target', error_description: 'resource indicator mismatch' };
225
+ }
226
+
227
+ const accessToken = `tgat_${randomToken(32)}`;
228
+ capInsert(store.tokens, accessToken, {
229
+ boundKey: entry.boundKey,
230
+ scope: entry.scope,
231
+ clientId,
232
+ aud: entry.resource || resource || '',
233
+ expiresAt: now() + ACCESS_TOKEN_TTL_MS,
234
+ }, MAX_TOKENS);
235
+ return {
236
+ access_token: accessToken,
237
+ token_type: 'Bearer',
238
+ expires_in: Math.floor(ACCESS_TOKEN_TTL_MS / 1000),
239
+ scope: entry.scope,
240
+ };
241
+ }
242
+
243
+ /** Resolve an access token to its session, or null if invalid/expired. */
244
+ function resolveAccessToken(store, token) {
245
+ if (!token) return null;
246
+ const entry = store.tokens.get(token);
247
+ if (!entry) return null;
248
+ if (now() > entry.expiresAt) {
249
+ store.tokens.delete(token);
250
+ return null;
251
+ }
252
+ return { boundKey: entry.boundKey, scope: entry.scope, clientId: entry.clientId, aud: entry.aud };
253
+ }
254
+
255
+ /**
256
+ * RFC 8707 audience validation: a token is valid for `expectedResource` only if it
257
+ * was issued for it (or carries no audience, for back-compat). MCP servers MUST
258
+ * reject tokens minted for a different resource.
259
+ */
260
+ function tokenAudienceValid(session, expectedResource) {
261
+ if (!session) return false;
262
+ if (!session.aud) return true; // no audience recorded — accept (back-compat)
263
+ return session.aud === expectedResource;
264
+ }
265
+
266
+ /** Best-effort GC of expired codes/tokens (call opportunistically). */
267
+ function pruneExpired(store) {
268
+ const t = now();
269
+ for (const [k, v] of store.codes) if (t > v.expiresAt) store.codes.delete(k);
270
+ for (const [k, v] of store.tokens) if (t > v.expiresAt) store.tokens.delete(k);
271
+ }
272
+
273
+ module.exports = {
274
+ createStore,
275
+ buildProtectedResourceMetadata,
276
+ buildAuthServerMetadata,
277
+ registerClient,
278
+ getClient,
279
+ createAuthorizationCode,
280
+ verifyPkce,
281
+ exchangeCode,
282
+ resolveAccessToken,
283
+ tokenAudienceValid,
284
+ pruneExpired,
285
+ isAllowedRedirectUri,
286
+ base64UrlSha256,
287
+ AUTH_CODE_TTL_MS,
288
+ ACCESS_TOKEN_TTL_MS,
289
+ DEFAULT_SCOPE,
290
+ MAX_CLIENTS,
291
+ MAX_CODES,
292
+ MAX_TOKENS,
293
+ };
@@ -2,6 +2,38 @@
2
2
  'use strict';
3
3
 
4
4
  const REQUIRED_SCOPE_FIELDS = ['entityId', 'projectId', 'processId', 'sessionId'];
5
+ const MEMORY_OS_LAYERS = Object.freeze([
6
+ {
7
+ id: 'file_layer',
8
+ name: 'File Layer',
9
+ purpose: 'Raw feedback, tool receipts, sessions, and memory rows are durably stored before interpretation.',
10
+ },
11
+ {
12
+ id: 'vector_db_layer',
13
+ name: 'Vector DB Layer',
14
+ purpose: 'Semantic retrieval can find related lessons without stuffing every raw memory into context.',
15
+ },
16
+ {
17
+ id: 'structured_facts_layer',
18
+ name: 'Structured Facts Layer',
19
+ purpose: 'Confirmed account, project, policy, and budget facts are typed separately from fuzzy memories.',
20
+ },
21
+ {
22
+ id: 'auto_curation_layer',
23
+ name: 'Auto Curation Layer',
24
+ purpose: 'Duplicate, stale, contradictory, and unscoped memories are consolidated before retrieval quality decays.',
25
+ },
26
+ {
27
+ id: 'context_layer',
28
+ name: 'Context Layer',
29
+ purpose: 'Only relevant scoped memories enter a given tool call, PR, deployment, or support session.',
30
+ },
31
+ {
32
+ id: 'interface_layer',
33
+ name: 'Interface Layer',
34
+ purpose: 'The memory contract is exposed through CLI, MCP, hooks, dashboards, and agent adapters without model lock-in.',
35
+ },
36
+ ]);
5
37
 
6
38
  const FIELD_ALIASES = {
7
39
  entityId: [
@@ -228,6 +260,128 @@ function buildRecommendations({ unscopedRecords, crossScopeDuplicates }) {
228
260
  return recommendations;
229
261
  }
230
262
 
263
+ function hasEmbeddingEvidence(record = {}) {
264
+ return Boolean(
265
+ record.embedding
266
+ || record.vector
267
+ || record.embeddingId
268
+ || record.metadata?.embedding
269
+ || record.metadata?.embeddingId
270
+ || record.metadata?.vectorId
271
+ || record.semanticKey
272
+ || record.metadata?.semanticKey
273
+ );
274
+ }
275
+
276
+ function hasStructuredFactEvidence(record = {}) {
277
+ const type = String(record.type || record.kind || record.memoryType || record.metadata?.type || '').toLowerCase();
278
+ return type === 'fact'
279
+ || type === 'structured_fact'
280
+ || Boolean(record.factKey || record.fact || record.metadata?.factKey || record.metadata?.fact);
281
+ }
282
+
283
+ function hasContextEvidence(record = {}) {
284
+ return Boolean(
285
+ record.contextPackId
286
+ || record.contextPack
287
+ || record.metadata?.contextPackId
288
+ || record.metadata?.contextPack
289
+ || record.retrievalQuery
290
+ || record.metadata?.retrievalQuery
291
+ );
292
+ }
293
+
294
+ function boolCapability(capabilities = {}, ...keys) {
295
+ return keys.some((key) => capabilities[key] === true);
296
+ }
297
+
298
+ function buildMemoryOsLayerReport(records = [], capabilities = {}) {
299
+ const scopeReport = buildMemoryScopeReadinessReport(records);
300
+ const semanticRecords = records.filter(hasEmbeddingEvidence);
301
+ const structuredFactRecords = records.filter(hasStructuredFactEvidence);
302
+ const contextRecords = records.filter(hasContextEvidence);
303
+ const curationReady = scopeReport.unscopedRecords === 0 && scopeReport.crossScopeDuplicates.length === 0;
304
+
305
+ const checks = [
306
+ {
307
+ id: 'file_layer',
308
+ ok: records.length > 0 || boolCapability(capabilities, 'rawStorage', 'fileLayer'),
309
+ evidence: {
310
+ records: records.length,
311
+ durableStore: Boolean(records.length > 0 || capabilities.rawStorage || capabilities.fileLayer),
312
+ },
313
+ recommendation: 'Capture raw feedback, action receipts, and tool outcomes before promoting memories.',
314
+ },
315
+ {
316
+ id: 'vector_db_layer',
317
+ ok: semanticRecords.length > 0 || boolCapability(capabilities, 'semanticSearch', 'vectorDbLayer'),
318
+ evidence: {
319
+ semanticRecords: semanticRecords.length,
320
+ semanticSearch: Boolean(capabilities.semanticSearch || capabilities.vectorDbLayer),
321
+ },
322
+ recommendation: 'Index lessons with semantic keys or embeddings so related failures are retrieved before action.',
323
+ },
324
+ {
325
+ id: 'structured_facts_layer',
326
+ ok: structuredFactRecords.length > 0 || boolCapability(capabilities, 'structuredFacts', 'structuredFactsLayer'),
327
+ evidence: {
328
+ structuredFactRecords: structuredFactRecords.length,
329
+ structuredFacts: Boolean(capabilities.structuredFacts || capabilities.structuredFactsLayer),
330
+ },
331
+ recommendation: 'Store confirmed customer, project, policy, and budget facts as typed records, not just prose.',
332
+ },
333
+ {
334
+ id: 'auto_curation_layer',
335
+ ok: curationReady && boolCapability(capabilities, 'autoCuration', 'dedupe', 'autoCurationLayer'),
336
+ evidence: {
337
+ unscopedRecords: scopeReport.unscopedRecords,
338
+ crossScopeDuplicates: scopeReport.crossScopeDuplicates.length,
339
+ autoCuration: Boolean(capabilities.autoCuration || capabilities.dedupe || capabilities.autoCurationLayer),
340
+ },
341
+ recommendation: 'Run dedupe, contradiction, stale-memory, and scope-isolation checks before memories can become gates.',
342
+ },
343
+ {
344
+ id: 'context_layer',
345
+ ok: contextRecords.length > 0 || boolCapability(capabilities, 'contextPacks', 'contextLayer', 'scopedRetrieval'),
346
+ evidence: {
347
+ contextRecords: contextRecords.length,
348
+ scopedRetrieval: Boolean(capabilities.contextPacks || capabilities.contextLayer || capabilities.scopedRetrieval),
349
+ },
350
+ recommendation: 'Inject scoped context packs per task instead of loading every memory into the model window.',
351
+ },
352
+ {
353
+ id: 'interface_layer',
354
+ ok: boolCapability(capabilities, 'mcp', 'cli', 'hooks', 'dashboard', 'interfaceLayer'),
355
+ evidence: {
356
+ cli: Boolean(capabilities.cli),
357
+ mcp: Boolean(capabilities.mcp),
358
+ hooks: Boolean(capabilities.hooks),
359
+ dashboard: Boolean(capabilities.dashboard),
360
+ },
361
+ recommendation: 'Expose the same memory contract through CLI, MCP, hooks, dashboard, and agent adapters.',
362
+ },
363
+ ].map((check) => {
364
+ const layer = MEMORY_OS_LAYERS.find((candidate) => candidate.id === check.id);
365
+ return {
366
+ ...layer,
367
+ ...check,
368
+ };
369
+ });
370
+
371
+ const missingLayers = checks.filter((check) => !check.ok).map((check) => check.id);
372
+
373
+ return {
374
+ ready: missingLayers.length === 0,
375
+ riskLevel: missingLayers.length === 0 ? 'low' : missingLayers.length <= 2 ? 'medium' : 'high',
376
+ layers: checks,
377
+ missingLayers,
378
+ scopeReport,
379
+ recommendations: checks
380
+ .filter((check) => !check.ok)
381
+ .map((check) => check.recommendation),
382
+ };
383
+ }
384
+
231
385
  function selectRecordsForScope(records = [], requestedScope = {}, options = {}) {
232
386
  const requested = normalizeScope(requestedScope);
233
387
  const requestedKey = memoryScopeKey(requested);
@@ -265,6 +419,7 @@ function buildMemoriStyleBenchmarkRecords() {
265
419
  projectId: 'thumbgate',
266
420
  processId: 'agent-a',
267
421
  sessionId: 'session-1',
422
+ metadata: { semanticKey: 'checkout-readiness', contextPackId: 'checkout-pro' },
268
423
  content: 'Use the paid sprint checklist before changing checkout code.',
269
424
  },
270
425
  {
@@ -298,14 +453,18 @@ function buildMemoriStyleBenchmarkRecords() {
298
453
  processId: 'agent-a',
299
454
  sessionId: 'session-1',
300
455
  visibility: 'shared',
456
+ type: 'fact',
457
+ factKey: 'checkout.mutation_policy',
301
458
  content: 'Shared rule: checkout mutations require audit evidence.',
302
459
  },
303
460
  ];
304
461
  }
305
462
 
306
463
  module.exports = {
464
+ MEMORY_OS_LAYERS,
307
465
  REQUIRED_SCOPE_FIELDS,
308
466
  buildMemoriStyleBenchmarkRecords,
467
+ buildMemoryOsLayerReport,
309
468
  buildMemoryScopeReadinessReport,
310
469
  isSharedMemory,
311
470
  memoryScopeKey,
@@ -282,6 +282,8 @@ function buildPromotedGate(candidate, metrics, runId) {
282
282
  occurrences: metrics.hits,
283
283
  promotedAt: new Date().toISOString(),
284
284
  source: 'meta-agent',
285
+ // origin distinguishes silent-failure-clustered candidates from feedback-derived ones
286
+ origin: candidate.origin || 'user-feedback',
285
287
  runId,
286
288
  score: parseFloat(metrics.score.toFixed(3)),
287
289
  hitRate: parseFloat(metrics.hitRate.toFixed(3)),
@@ -371,6 +373,38 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
371
373
  candidates = generateCandidatesHeuristic(failures, blockPatterns);
372
374
  }
373
375
 
376
+ // Tag existing-pipeline candidates with their origin so downstream precision
377
+ // measurement (silentFailureDerivedGates vs user-feedback-derived) is possible.
378
+ candidates = candidates.map((c) => (c.origin ? c : { ...c, origin: 'user-feedback' }));
379
+
380
+ // Step 3b: Silent-failure clustering — DEFAULT-ON as of 2026-05-21
381
+ // (flipped from opt-in by PR #2289). Opt out via
382
+ // THUMBGATE_SILENT_FAILURE_CLUSTERING=0 (or NODE_ENV=test). Candidates flow
383
+ // through the SAME scoring / fp-rate eval below; we do not bypass any
384
+ // guardrail. The point of this clustering is to cover the case where users
385
+ // are lazy and never give thumbs-down — keeping it opt-in meant the users
386
+ // who need it most never got the benefit.
387
+ let silentFailureStats = null;
388
+ const { isSilentFailureClusteringEnabled, generateSilentFailureCandidates } = require('./silent-failure-cluster');
389
+ if (isSilentFailureClusteringEnabled()) {
390
+ try {
391
+ const sfResult = generateSilentFailureCandidates({ feedbackLogPath });
392
+ silentFailureStats = sfResult.stats;
393
+ if (sfResult.candidates && sfResult.candidates.length > 0) {
394
+ candidates = candidates.concat(sfResult.candidates);
395
+ }
396
+ if (verbose) {
397
+ process.stdout.write(
398
+ `[meta-agent] silent-failure-cluster: candidates=${sfResult.candidates.length} `
399
+ + `failed=${sfResult.stats.failedCalls} clusters=${sfResult.stats.clusters} `
400
+ + `skipped=${sfResult.stats.skippedReason || 'none'}\n`
401
+ );
402
+ }
403
+ } catch (err) {
404
+ if (verbose) process.stdout.write(`[meta-agent] silent-failure-cluster failed (non-fatal): ${err.message}\n`);
405
+ }
406
+ }
407
+
374
408
  if (verbose) {
375
409
  process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
376
410
  }
@@ -507,6 +541,8 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
507
541
  skipped: evolutionResult.skipped || false,
508
542
  }
509
543
  : null,
544
+ silentFailureCluster: silentFailureStats,
545
+ silentFailureDerivedGates: promotedGates.filter((g) => g.origin === 'silent-failure-cluster').length,
510
546
  };
511
547
 
512
548
  if (!dryRun) {