thumbgate 1.34.3 → 1.37.1

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 (200) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +373 -5
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +653 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +21 -2
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +10 -1
  34. package/config/model-candidates.json +382 -24
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +401 -147
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/auto-promote-gates.js +178 -27
  89. package/scripts/billing.js +1 -1
  90. package/scripts/broker-execution-receipts.js +719 -0
  91. package/scripts/budget-aware-gates-proof.js +423 -0
  92. package/scripts/claude-feedback-sync.js +29 -3
  93. package/scripts/claw-harness-production.js +237 -0
  94. package/scripts/cli-progress.js +111 -0
  95. package/scripts/cli-schema.js +163 -1
  96. package/scripts/codex-runbook-flywheel.js +318 -0
  97. package/scripts/context-footprint.js +186 -0
  98. package/scripts/contextfs.js +143 -61
  99. package/scripts/dashboard-limits.js +27 -0
  100. package/scripts/dashboard.js +279 -9
  101. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  102. package/scripts/docker-sandbox-planner.js +18 -0
  103. package/scripts/double-blind-eval-protocol.js +252 -0
  104. package/scripts/edotenv-rl-gateway.js +259 -0
  105. package/scripts/ensure-production-search-corpus.js +162 -0
  106. package/scripts/eval-holdout.js +311 -0
  107. package/scripts/feedback-aggregate.js +21 -2
  108. package/scripts/feedback-loop.js +87 -5
  109. package/scripts/feedback-quality.js +9 -0
  110. package/scripts/file-ledger-lock.js +4 -1
  111. package/scripts/financial-control-plane.js +41 -1
  112. package/scripts/find-dormant-requires.js +118 -0
  113. package/scripts/fs-utils.js +84 -8
  114. package/scripts/gate-stats.js +2 -2
  115. package/scripts/gates-engine.js +859 -58
  116. package/scripts/generate-case-study-outreach.js +24 -15
  117. package/scripts/git-at-scale.js +628 -0
  118. package/scripts/governance-conflict-audit.js +1650 -0
  119. package/scripts/governance-difficulty-curriculum.js +328 -0
  120. package/scripts/graphrag-retrieval.js +275 -0
  121. package/scripts/gurobi-optimizer.js +324 -0
  122. package/scripts/gurobi_optimizer.py +485 -0
  123. package/scripts/harness-selector.js +82 -1
  124. package/scripts/hidden-entry-points.js +284 -0
  125. package/scripts/human-escalation.js +199 -1
  126. package/scripts/hybrid-feedback-context.js +152 -19
  127. package/scripts/intent-governed-execution.js +602 -0
  128. package/scripts/intervention-policy.js +123 -20
  129. package/scripts/jit-harness-compose.js +628 -0
  130. package/scripts/jsonl-watcher.js +10 -0
  131. package/scripts/lesson-embedding-index.js +95 -12
  132. package/scripts/lesson-retrieval.js +105 -19
  133. package/scripts/local-model-profile.js +19 -2
  134. package/scripts/mailer/resend-mailer.js +1 -1
  135. package/scripts/matryoshka-embedding.js +235 -0
  136. package/scripts/mcp-oauth.js +42 -4
  137. package/scripts/mcp-session-handles.js +1016 -0
  138. package/scripts/mcp-wiring-doctor.js +314 -0
  139. package/scripts/memory-firewall.js +115 -2
  140. package/scripts/memory-scope-readiness.js +299 -0
  141. package/scripts/memory-vs-rag-route.js +161 -0
  142. package/scripts/model-tier-router.js +148 -21
  143. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  144. package/scripts/openui-catalog-compose-honesty.js +593 -0
  145. package/scripts/operational-integrity.js +19 -1
  146. package/scripts/override-audit.js +213 -0
  147. package/scripts/package-manager-honesty-doctor.js +458 -0
  148. package/scripts/pr-manager.js +63 -1
  149. package/scripts/prove-herdr-adapter.js +52 -0
  150. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  151. package/scripts/prove-workos.js +73 -0
  152. package/scripts/provider-attestation-conformance.js +192 -0
  153. package/scripts/provider-receipt-contract.js +136 -0
  154. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  155. package/scripts/radware-threat-defense.js +280 -0
  156. package/scripts/rag-embedding-identity.js +221 -0
  157. package/scripts/rag-precision-guardrails.js +112 -2
  158. package/scripts/remote-feedback-capture.js +159 -0
  159. package/scripts/research-agent-harness.js +256 -0
  160. package/scripts/rsi-safety-hillclimb.js +200 -0
  161. package/scripts/rule-sprawl.js +188 -0
  162. package/scripts/schedule-manager.js +147 -0
  163. package/scripts/self-heal.js +8 -0
  164. package/scripts/session-lease.js +415 -0
  165. package/scripts/simatree-data-governance.js +347 -0
  166. package/scripts/slo-alert-engine.js +172 -7
  167. package/scripts/solver-parity.js +539 -0
  168. package/scripts/stealth-memory-injection-gate.js +333 -0
  169. package/scripts/switchyard-router.js +366 -0
  170. package/scripts/telemetry-analytics.js +84 -27
  171. package/scripts/temporal-decay-weighting.js +138 -0
  172. package/scripts/test-all.js +165 -0
  173. package/scripts/token-savings.js +42 -0
  174. package/scripts/tool-kpi-tracker.js +108 -5
  175. package/scripts/tool-registry.js +193 -5
  176. package/scripts/universal-claim-evaluator.js +14 -2
  177. package/scripts/vector-store.js +279 -9
  178. package/scripts/workflow-notebook.js +391 -0
  179. package/scripts/workflow-sentinel.js +111 -12
  180. package/scripts/workos-production-guard.js +260 -0
  181. package/scripts/workspace-search-route.js +515 -0
  182. package/server.json +2 -2
  183. package/src/agent-identity-boundary.js +76 -0
  184. package/src/agent-retrieval-cache.js +155 -0
  185. package/src/alert-noise-ledger.js +502 -0
  186. package/src/api/server.js +802 -185
  187. package/src/git-fast-cache.js +220 -0
  188. package/src/git-wal-sync.js +156 -0
  189. package/src/hash-anchored-edit.js +82 -0
  190. package/src/hermes-platform-protocol.js +475 -0
  191. package/src/hermes-sync-plane.js +241 -0
  192. package/src/index.js +30 -1
  193. package/src/iso42001-compliance-guard.js +97 -0
  194. package/src/latency-budget.js +244 -0
  195. package/src/mcp-writeguard.js +316 -0
  196. package/src/miminions-adapter.js +106 -0
  197. package/src/pipeline-compass.js +104 -0
  198. package/src/ppl-alert-pipeline.js +284 -0
  199. package/src/rendezvous-router.js +90 -0
  200. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,423 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Budget-aware gates proof — sales-safe demo of ThumbGate optimization
5
+ *
6
+ * Uses the Fabrizio Ellis free-pip Gurobi path (when available) to compare:
7
+ * 1) Deterministic heuristic selection
8
+ * 2) MILP solver selection (Gurobi via gurobi-optimizer.js, heuristic fallback in CI)
9
+ *
10
+ * What this proves for buyers:
11
+ * - ThumbGate does not load "every prevention rule always"
12
+ * - Model routing respects cost + latency budgets
13
+ * - Rule knapsacks maximize risk mitigation under eval-time/token budgets
14
+ *
15
+ * What this does NOT claim:
16
+ * - Gurobi partnership / co-sell / affiliation
17
+ * - Captured cash or revenue from the solve
18
+ * - That commercial Gurobi is required (free-pip + fail-open heuristics work)
19
+ *
20
+ * Usage:
21
+ * node scripts/budget-aware-gates-proof.js
22
+ * node scripts/budget-aware-gates-proof.js --json
23
+ * node scripts/budget-aware-gates-proof.js --write proof/budget-aware-gates.json
24
+ */
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+ const {
29
+ optimizeModelRouting,
30
+ optimizeRuleSelection,
31
+ probeGurobi,
32
+ } = require('./gurobi-optimizer');
33
+
34
+ /** Fixed demo workload — deterministic for CI and demos. */
35
+ const DEMO_MODEL_CANDIDATES = [
36
+ { id: 'local-vllm-8b', score: 7.8, cost: 0.0, latency_ms: 180, notes: 'local free' },
37
+ { id: 'qwen-flash', score: 8.4, cost: 0.002, latency_ms: 220, notes: 'cheap cloud' },
38
+ { id: 'claude-sonnet', score: 9.6, cost: 0.018, latency_ms: 900, notes: 'premium' },
39
+ { id: 'claude-opus', score: 9.9, cost: 0.06, latency_ms: 1400, notes: 'over budget' },
40
+ { id: 'gpt-mini', score: 8.1, cost: 0.004, latency_ms: 350, notes: 'mid tier' },
41
+ ];
42
+
43
+ const DEMO_RULES = [
44
+ { id: 'secret-egress', risk_mitigation: 9.5, eval_time_ms: 4, token_footprint: 80 },
45
+ { id: 'deny-network-egress', risk_mitigation: 7.0, eval_time_ms: 3, token_footprint: 60 },
46
+ { id: 'task-scope-lease', risk_mitigation: 8.2, eval_time_ms: 6, token_footprint: 120 },
47
+ { id: 'protected-file-approval', risk_mitigation: 8.8, eval_time_ms: 5, token_footprint: 100 },
48
+ { id: 'spend-guard', risk_mitigation: 6.5, eval_time_ms: 2, token_footprint: 40 },
49
+ { id: 'stealth-memory-injection', risk_mitigation: 9.0, eval_time_ms: 12, token_footprint: 200 },
50
+ { id: 'claim-verification', risk_mitigation: 5.5, eval_time_ms: 15, token_footprint: 250 },
51
+ { id: 'outbound-email-human', risk_mitigation: 7.5, eval_time_ms: 3, token_footprint: 50 },
52
+ { id: 'branch-protection', risk_mitigation: 8.0, eval_time_ms: 4, token_footprint: 70 },
53
+ { id: 'low-value-noise-rule', risk_mitigation: 1.0, eval_time_ms: 20, token_footprint: 300 },
54
+ ];
55
+
56
+ const DEFAULT_BUDGETS = {
57
+ maxBudgetUsd: 0.01,
58
+ maxLatencyMs: 500,
59
+ maxEvalTimeMs: 30,
60
+ maxTokenFootprint: 600,
61
+ };
62
+
63
+ function greedyHeuristicRouting(candidates, { maxBudgetUsd, maxLatencyMs }) {
64
+ const list = Array.isArray(candidates) ? candidates : [];
65
+ if (list.length === 0) {
66
+ return {
67
+ success: false,
68
+ selected: null,
69
+ solver: 'pure-greedy-heuristic',
70
+ objective: 0,
71
+ candidate: null,
72
+ reason: 'no_candidates',
73
+ };
74
+ }
75
+ const valid = list.filter(
76
+ (c) => (c.cost || 0) <= maxBudgetUsd && (c.latency_ms || 0) <= maxLatencyMs
77
+ );
78
+ const pool = valid.length > 0 ? valid : list;
79
+ const best = pool.reduce(
80
+ (prev, curr) => ((curr.score || 0) > (prev.score || 0) ? curr : prev),
81
+ pool[0]
82
+ );
83
+ return {
84
+ success: true,
85
+ selected: best.id,
86
+ solver: 'pure-greedy-heuristic',
87
+ objective: best.score || 0,
88
+ candidate: best,
89
+ };
90
+ }
91
+
92
+ function greedyHeuristicRules(rules, { maxEvalTimeMs, maxTokenFootprint }) {
93
+ const list = Array.isArray(rules) ? rules : [];
94
+ if (list.length === 0) {
95
+ return {
96
+ success: false,
97
+ selected_rules: [],
98
+ solver: 'pure-greedy-knapsack',
99
+ used_time_ms: 0,
100
+ used_tokens: 0,
101
+ total_mitigation: 0,
102
+ reason: 'no_rules',
103
+ };
104
+ }
105
+ const sorted = [...list].sort(
106
+ (a, b) =>
107
+ (b.risk_mitigation || 0) / Math.max(b.eval_time_ms || 0.1, 0.1)
108
+ - (a.risk_mitigation || 0) / Math.max(a.eval_time_ms || 0.1, 0.1)
109
+ );
110
+ const selected = [];
111
+ let curTime = 0;
112
+ let curTokens = 0;
113
+ let mitigation = 0;
114
+ for (const r of sorted) {
115
+ const t = r.eval_time_ms || 0;
116
+ const tok = r.token_footprint || 0;
117
+ if (curTime + t <= maxEvalTimeMs && curTokens + tok <= maxTokenFootprint) {
118
+ selected.push(r.id);
119
+ curTime += t;
120
+ curTokens += tok;
121
+ mitigation += r.risk_mitigation || 0;
122
+ }
123
+ }
124
+ return {
125
+ success: true,
126
+ selected_rules: selected,
127
+ solver: 'pure-greedy-knapsack',
128
+ used_time_ms: curTime,
129
+ used_tokens: curTokens,
130
+ total_mitigation: mitigation,
131
+ };
132
+ }
133
+
134
+ function sumMitigation(rules, ids) {
135
+ const set = new Set(ids || []);
136
+ return rules
137
+ .filter((r) => set.has(r.id))
138
+ .reduce((s, r) => s + (r.risk_mitigation || 0), 0);
139
+ }
140
+
141
+ function findCandidate(id) {
142
+ return DEMO_MODEL_CANDIDATES.find((c) => c.id === id) || null;
143
+ }
144
+
145
+ /**
146
+ * Run the full proof suite.
147
+ * @param {object} options
148
+ * @returns {object} structured proof report
149
+ */
150
+ function runBudgetAwareGatesProof(options = {}) {
151
+ const budgets = { ...DEFAULT_BUDGETS, ...(options.budgets || {}) };
152
+ const started = Date.now();
153
+
154
+ const heuristicRouting = greedyHeuristicRouting(DEMO_MODEL_CANDIDATES, budgets);
155
+ const solverRouting = optimizeModelRouting(DEMO_MODEL_CANDIDATES, {
156
+ maxBudgetUsd: budgets.maxBudgetUsd,
157
+ maxLatencyMs: budgets.maxLatencyMs,
158
+ }, options.solverOpts || {});
159
+
160
+ const heuristicRules = greedyHeuristicRules(DEMO_RULES, budgets);
161
+ const solverRules = optimizeRuleSelection(DEMO_RULES, {
162
+ maxEvalTimeMs: budgets.maxEvalTimeMs,
163
+ maxTokenFootprint: budgets.maxTokenFootprint,
164
+ }, options.solverOpts || {});
165
+
166
+ const probe = options.skipProbe ? { ok: null, gurobi: false } : probeGurobi(options.solverOpts || {});
167
+
168
+ // When Gurobi error-falls-back to unconstrained top score, replace with
169
+ // budget-valid heuristic so buyer narrative never claims compliance for
170
+ // over-budget selections (e.g. claude-opus at $0.06 under a $0.01 cap).
171
+ const routingPick = coerceBudgetCompliantRouting(solverRouting, heuristicRouting, budgets);
172
+ const rulesPick = coerceBudgetCompliantRules(solverRules, heuristicRules, budgets);
173
+
174
+ const routingHeuristicScore = heuristicRouting.objective || 0;
175
+ const routingSolverScore = routingPick.objective != null
176
+ ? routingPick.objective
177
+ : (findCandidate(routingPick.selected)?.score || 0);
178
+ const rulesHeuristicMitigation = heuristicRules.total_mitigation
179
+ || sumMitigation(DEMO_RULES, heuristicRules.selected_rules);
180
+ const rulesSolverMitigation = rulesPick.total_mitigation != null
181
+ ? rulesPick.total_mitigation
182
+ : sumMitigation(DEMO_RULES, rulesPick.selected_rules || []);
183
+
184
+ const solverIsGurobi = isSuccessfulGurobiLabel(routingPick.solver)
185
+ || isSuccessfulGurobiLabel(rulesPick.solver)
186
+ || (probe.gurobi === true && !isGurobiErrorFallback(solverRouting.solver)
187
+ && !isGurobiErrorFallback(solverRules.solver));
188
+
189
+ const report = {
190
+ schema: 'thumbgate.budget_aware_gates_proof.v1',
191
+ mode: 'simulation',
192
+ autoApply: false,
193
+ humanOversightRequired: true,
194
+ capturedRevenueUsd: 0,
195
+ generatedAt: new Date().toISOString(),
196
+ latencyMs: Date.now() - started,
197
+ budgets,
198
+ probe: {
199
+ ok: probe.ok,
200
+ gurobiAvailable: Boolean(
201
+ (probe.gurobi === true && !isGurobiErrorFallback(solverRouting.solver)
202
+ && !isGurobiErrorFallback(solverRules.solver))
203
+ || isSuccessfulGurobiLabel(routingPick.solver)
204
+ || isSuccessfulGurobiLabel(rulesPick.solver)
205
+ ),
206
+ solverLabel: routingPick.solver || null,
207
+ rawSolverLabel: solverRouting.solver || null,
208
+ python: probe.python || null,
209
+ },
210
+ modelRouting: {
211
+ budgetUsd: budgets.maxBudgetUsd,
212
+ maxLatencyMs: budgets.maxLatencyMs,
213
+ heuristic: {
214
+ selected: heuristicRouting.selected,
215
+ score: routingHeuristicScore,
216
+ solver: heuristicRouting.solver,
217
+ candidate: findCandidate(heuristicRouting.selected),
218
+ },
219
+ optimized: {
220
+ selected: routingPick.selected,
221
+ score: routingSolverScore,
222
+ solver: routingPick.solver,
223
+ candidate: findCandidate(routingPick.selected),
224
+ budgetCompliant: isRoutingWithinBudget(findCandidate(routingPick.selected), budgets),
225
+ },
226
+ scoreDelta: routingSolverScore - routingHeuristicScore,
227
+ sameSelection: heuristicRouting.selected === routingPick.selected,
228
+ },
229
+ ruleKnapsack: {
230
+ maxEvalTimeMs: budgets.maxEvalTimeMs,
231
+ maxTokenFootprint: budgets.maxTokenFootprint,
232
+ ruleCount: DEMO_RULES.length,
233
+ heuristic: {
234
+ selected: heuristicRules.selected_rules,
235
+ count: (heuristicRules.selected_rules || []).length,
236
+ mitigation: rulesHeuristicMitigation,
237
+ usedTimeMs: heuristicRules.used_time_ms,
238
+ usedTokens: heuristicRules.used_tokens,
239
+ solver: heuristicRules.solver,
240
+ },
241
+ optimized: {
242
+ selected: rulesPick.selected_rules || [],
243
+ count: (rulesPick.selected_rules || []).length,
244
+ mitigation: rulesSolverMitigation,
245
+ usedTimeMs: rulesPick.used_time_ms,
246
+ usedTokens: rulesPick.used_tokens,
247
+ solver: rulesPick.solver,
248
+ },
249
+ mitigationDelta: rulesSolverMitigation - rulesHeuristicMitigation,
250
+ droppedLowValue: DEMO_RULES
251
+ .map((r) => r.id)
252
+ .filter((id) => !(rulesPick.selected_rules || []).includes(id)),
253
+ },
254
+ buyerNarrative: {
255
+ headline: 'Budget-aware enforcement simulation — fixture knapsack, not live rule loader',
256
+ bullets: [
257
+ `SIMULATION: model routing under $${budgets.maxBudgetUsd} / ${budgets.maxLatencyMs}ms budget selects ${routingPick.selected} (solver=${routingPick.solver}).`,
258
+ `SIMULATION: prevention-rule knapsack keeps ${ (rulesPick.selected_rules || []).length } of ${DEMO_RULES.length} fixture rules under ${budgets.maxEvalTimeMs}ms / ${budgets.maxTokenFootprint} tokens.`,
259
+ `Risk-mitigation score (fixtures): heuristic ${rulesHeuristicMitigation.toFixed(1)} → optimized ${rulesSolverMitigation.toFixed(1)} (Δ ${ (rulesSolverMitigation - rulesHeuristicMitigation).toFixed(1) }).`,
260
+ solverIsGurobi
261
+ ? 'MILP solver path succeeded on this host (free-pip Gurobi or compatible). Heuristics remain the CI/fallback path.'
262
+ : 'Running on deterministic heuristic fallback — same API, fail-open when solver unavailable.',
263
+ ],
264
+ disclaimers: [
265
+ 'This proof is a sales-safe SIMULATION on fixed demo fixtures. Production PreToolUse does not yet load selections from optimizeRuleSelection/optimizeModelRouting.',
266
+ 'ThumbGate product claim is budget-aware gates as an architecture; this script proves the selection math, not live enforcement wiring.',
267
+ 'No Gurobi partnership, co-sell, or affiliation is claimed.',
268
+ 'capturedRevenueUsd is not computed here — optimization ≠ cash collected.',
269
+ ],
270
+ },
271
+ fixtures: {
272
+ candidates: DEMO_MODEL_CANDIDATES,
273
+ rules: DEMO_RULES,
274
+ },
275
+ };
276
+
277
+ return report;
278
+ }
279
+
280
+ function isGurobiErrorFallback(label) {
281
+ const s = String(label || '').toLowerCase();
282
+ return s.includes('gurobi-error')
283
+ || s.includes('error-fallback')
284
+ || /gurobi.*(?:error|unavailable|license)/.test(s);
285
+ }
286
+
287
+ function isSuccessfulGurobiLabel(label) {
288
+ const s = String(label || '').toLowerCase();
289
+ if (!s) return false;
290
+ if (isGurobiErrorFallback(s)) return false;
291
+ // Accept exact successful labels only (not every gurobi* string).
292
+ return s === 'gurobi' || s === 'gurobipy' || s === 'gurobi-milp' || s.startsWith('gurobi-ok');
293
+ }
294
+
295
+ function isRoutingWithinBudget(candidate, budgets) {
296
+ if (!candidate) return false;
297
+ return (candidate.cost || 0) <= budgets.maxBudgetUsd
298
+ && (candidate.latency_ms || 0) <= budgets.maxLatencyMs;
299
+ }
300
+
301
+ function coerceBudgetCompliantRouting(solverRouting, heuristicRouting, budgets) {
302
+ const cand = findCandidate(solverRouting && solverRouting.selected);
303
+ if (
304
+ cand
305
+ && isRoutingWithinBudget(cand, budgets)
306
+ && !isGurobiErrorFallback(solverRouting && solverRouting.solver)
307
+ ) {
308
+ return solverRouting;
309
+ }
310
+ return {
311
+ ...heuristicRouting,
312
+ solver: heuristicRouting.solver
313
+ || (isGurobiErrorFallback(solverRouting && solverRouting.solver)
314
+ ? 'heuristic-after-gurobi-error'
315
+ : 'heuristic-budget-coerce'),
316
+ coercedFrom: solverRouting && solverRouting.selected,
317
+ };
318
+ }
319
+
320
+ function coerceBudgetCompliantRules(solverRules, heuristicRules, budgets) {
321
+ const selected = (solverRules && solverRules.selected_rules) || [];
322
+ if (!selected.length || isGurobiErrorFallback(solverRules && solverRules.solver)) {
323
+ return {
324
+ ...heuristicRules,
325
+ solver: heuristicRules.solver || 'heuristic-after-gurobi-error',
326
+ coercedFrom: selected,
327
+ };
328
+ }
329
+ // Soft check: if used budgets exceed caps, fall back to heuristic.
330
+ const usedTime = solverRules.used_time_ms;
331
+ const usedTokens = solverRules.used_tokens;
332
+ if (
333
+ (usedTime != null && usedTime > budgets.maxEvalTimeMs)
334
+ || (usedTokens != null && usedTokens > budgets.maxTokenFootprint)
335
+ ) {
336
+ return {
337
+ ...heuristicRules,
338
+ solver: heuristicRules.solver || 'heuristic-budget-coerce',
339
+ coercedFrom: selected,
340
+ };
341
+ }
342
+ return solverRules;
343
+ }
344
+
345
+ function formatMarkdown(report) {
346
+ const lines = [
347
+ '# Budget-aware gates proof',
348
+ '',
349
+ `Generated: ${report.generatedAt}`,
350
+ '',
351
+ report.buyerNarrative.headline,
352
+ '',
353
+ '## What buyers should hear',
354
+ '',
355
+ ...report.buyerNarrative.bullets.map((b) => `- ${b}`),
356
+ '',
357
+ '## Model routing',
358
+ '',
359
+ `| Path | Selected | Score | Solver |`,
360
+ `|------|----------|-------|--------|`,
361
+ `| Heuristic | ${report.modelRouting.heuristic.selected} | ${report.modelRouting.heuristic.score} | ${report.modelRouting.heuristic.solver} |`,
362
+ `| Optimized | ${report.modelRouting.optimized.selected} | ${report.modelRouting.optimized.score} | ${report.modelRouting.optimized.solver} |`,
363
+ '',
364
+ '## Rule knapsack',
365
+ '',
366
+ `| Path | Rules | Mitigation | Time ms | Tokens |`,
367
+ `|------|-------|------------|---------|--------|`,
368
+ `| Heuristic | ${report.ruleKnapsack.heuristic.count} | ${report.ruleKnapsack.heuristic.mitigation.toFixed(1)} | ${report.ruleKnapsack.heuristic.usedTimeMs} | ${report.ruleKnapsack.heuristic.usedTokens} |`,
369
+ `| Optimized | ${report.ruleKnapsack.optimized.count} | ${report.ruleKnapsack.optimized.mitigation.toFixed(1)} | ${report.ruleKnapsack.optimized.usedTimeMs ?? '—'} | ${report.ruleKnapsack.optimized.usedTokens ?? '—'} |`,
370
+ '',
371
+ '## Disclaimers',
372
+ '',
373
+ ...report.buyerNarrative.disclaimers.map((d) => `- ${d}`),
374
+ '',
375
+ ];
376
+ return lines.join('\n');
377
+ }
378
+
379
+ function mainCli(argv = process.argv.slice(2)) {
380
+ const json = argv.includes('--json');
381
+ const writeIdx = argv.indexOf('--write');
382
+ const writePath = writeIdx >= 0 ? argv[writeIdx + 1] : null;
383
+
384
+ const report = runBudgetAwareGatesProof();
385
+
386
+ if (writePath) {
387
+ const abs = path.resolve(writePath);
388
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
389
+ const jsonPath = /\.json$/i.test(abs) ? abs : `${abs}.json`;
390
+ const mdPath = jsonPath.replace(/\.json$/i, '.md');
391
+ fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
392
+ if (mdPath !== jsonPath) {
393
+ fs.writeFileSync(mdPath, formatMarkdown(report), 'utf8');
394
+ }
395
+ }
396
+
397
+ if (json) {
398
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
399
+ } else {
400
+ process.stdout.write(formatMarkdown(report));
401
+ }
402
+
403
+ return report;
404
+ }
405
+
406
+ // Path-based main check (Sonar S3403: require.main === module is unreliable under CJS).
407
+ if (
408
+ process.argv[1]
409
+ && path.resolve(process.argv[1]) === path.resolve(__filename)
410
+ ) {
411
+ mainCli();
412
+ }
413
+
414
+ module.exports = {
415
+ DEMO_MODEL_CANDIDATES,
416
+ DEMO_RULES,
417
+ DEFAULT_BUDGETS,
418
+ greedyHeuristicRouting,
419
+ greedyHeuristicRules,
420
+ runBudgetAwareGatesProof,
421
+ formatMarkdown,
422
+ mainCli,
423
+ };
@@ -17,9 +17,12 @@ const {
17
17
  const { refreshStatuslineCache } = require('./hook-thumbgate-cache-updater');
18
18
 
19
19
  const SYNC_STATE_FILE = 'claude-feedback-sync-state.json';
20
- const DEFAULT_RECENT_FEEDBACK_LIMIT = 250;
21
- const DEFAULT_PROCESSED_ID_LIMIT = 512;
20
+ const DEFAULT_RECENT_FEEDBACK_LIMIT = 1000;
21
+ const DEFAULT_PROCESSED_ID_LIMIT = 4096;
22
22
  const DUPLICATE_WINDOW_MS = 5 * 60 * 1000;
23
+ // At this length, identical text + signal is a re-surfaced prompt, not a
24
+ // repeated sentiment; shorter bare signals stay window-bound.
25
+ const IDENTICAL_TEXT_DEDUP_MIN_LENGTH = 20;
23
26
 
24
27
  function getClaudeHistoryPath(options = {}) {
25
28
  if (options.historyPath) return options.historyPath;
@@ -40,6 +43,7 @@ function readSyncState(options = {}) {
40
43
  return {
41
44
  historyOffset: Number(parsed.historyOffset || 0),
42
45
  historySize: Number(parsed.historySize || 0),
46
+ historyIno: parsed.historyIno ? String(parsed.historyIno) : null,
43
47
  processedIds: Array.isArray(parsed.processedIds) ? parsed.processedIds : [],
44
48
  statePath,
45
49
  };
@@ -47,6 +51,7 @@ function readSyncState(options = {}) {
47
51
  return {
48
52
  historyOffset: 0,
49
53
  historySize: 0,
54
+ historyIno: null,
50
55
  processedIds: [],
51
56
  statePath,
52
57
  };
@@ -58,6 +63,7 @@ function writeSyncState(state, options = {}) {
58
63
  const payload = {
59
64
  historyOffset: Number(state.historyOffset || 0),
60
65
  historySize: Number(state.historySize || 0),
66
+ historyIno: state.historyIno ? String(state.historyIno) : null,
61
67
  processedIds: Array.isArray(state.processedIds) ? state.processedIds.slice(-DEFAULT_PROCESSED_ID_LIMIT) : [],
62
68
  updatedAt: new Date().toISOString(),
63
69
  };
@@ -72,11 +78,25 @@ function readHistoryEntriesSince(filePath, state) {
72
78
  entries: [],
73
79
  nextOffset: 0,
74
80
  size: 0,
81
+ ino: null,
75
82
  };
76
83
  }
77
84
 
78
85
  const stat = fs.statSync(filePath);
79
- const safeOffset = state && state.historyOffset > 0 && state.historyOffset <= stat.size
86
+ const currentIno = stat.ino > 0 ? String(stat.ino) : null;
87
+
88
+ // Rotation guard: a replaced file (new inode) or one smaller than the
89
+ // recorded size means the saved offset points into vanished content. Scan
90
+ // the replacement from 0 so entries it already holds are preserved; the
91
+ // processedIds and feedback-log dedup layers stop old signals from
92
+ // mass re-importing. Skipping to end here would permanently drop any
93
+ // signal already present in the replacement file.
94
+ const rotated = Boolean(state) && (
95
+ (state.historyIno && currentIno && state.historyIno !== currentIno)
96
+ || (state.historySize > 0 && stat.size < state.historySize)
97
+ );
98
+
99
+ const safeOffset = !rotated && state && state.historyOffset > 0 && state.historyOffset <= stat.size
80
100
  ? state.historyOffset
81
101
  : 0;
82
102
 
@@ -100,6 +120,7 @@ function readHistoryEntriesSince(filePath, state) {
100
120
  entries,
101
121
  nextOffset: stat.size,
102
122
  size: stat.size,
123
+ ino: currentIno,
103
124
  };
104
125
  }
105
126
 
@@ -204,6 +225,10 @@ function hasMatchingFeedbackEntry(candidate, feedbackEntries) {
204
225
  );
205
226
  if (feedbackText !== candidateText) return false;
206
227
 
228
+ if (candidateText.length >= IDENTICAL_TEXT_DEDUP_MIN_LENGTH) {
229
+ return true;
230
+ }
231
+
207
232
  const feedbackTimestamp = Date.parse(entry.timestamp || '');
208
233
  if (!Number.isFinite(feedbackTimestamp) || !Number.isFinite(candidate.timestampMs)) {
209
234
  return true;
@@ -296,6 +321,7 @@ function syncClaudeHistoryFeedback(options = {}) {
296
321
  writeSyncState({
297
322
  historyOffset: history.nextOffset,
298
323
  historySize: history.size,
324
+ historyIno: history.ino,
299
325
  processedIds: Array.from(processedIds),
300
326
  }, { feedbackDir });
301
327